Blog des Gens Compliqués

Je comprends rien aux stats

30/10/2025 15:01:31+01:00|Par DkVZ
8 minutes de lecture (facile)

Vous avez sûrement déjà lu mon dernier article en date. Je vais tout de même sommairement vous le résumer.

Je voudrais ajouter une estimation du temps de lecture pour mes articles sur mon blog et au lieu de rapidement parcourir le texte pour déterminer le nombre de mots côté client, je me suis perdu dans un plan semi-foireux à base de modèle statistique de prédiction du nombre de mots à partir de la "longueur" des articles.

Durant cet exercice absolument pas nécessaire (superfétatoire dans le texte), je découvre que le meilleur modèle est manifestement la régression linéaire.

Pour ceux qui ne connaissent pas, c'est une des relations les plus simples pour expliquer l'évolution d'une variable par rapport à une autre parce que la dite évolution suit une simple ligne droite (au lieu d'une forme plus complexe avec des courbes et tout ça).

Bon nombre de grandeurs physiques du monde qui nous entoure évoluent de la sorte.

Par exemple, le volume de gaz de météorisme flatulatoire expulsé en fonction de la masse de cassoulet ingérée est linéaire. Enfin je pense.

Au moment de mes "calculs" et de la création d'un programme pas nécessaire écrit en Go pour les réaliser, je me suis quelque peu découragé face à la complexité mathématique (toute relative) de la formule associée.

une formule de calcul de régression que j'ai trouvée sur le site référencé dans la légende
Bon là ça a l'air facile (lol) mais c'est parce qu'il manque la formule de la covariance [source]

La solution était de se résoudre à utiliser un tableur comme tout le monde, à partir de mes données exportées en BON VIEUX CSV.

Okay, très bien. Entre-temps je me suis décidé à copier coller l'algorithme de quelqu'un d'autre, que j'ai trouvé dans la partie "stat" d'une librairie Go nommée Gonum.

Pourquoi ne pas avoir utilisé cette librairie pour tous les calculs de stats?

  1. Je suis pas normal
  2. Je voulais rafraîchir mes "connaissances" en stats

Adapter l'affaire à mon code revient à transformer ceci:

func LinearRegression(x, y, weights []float64, origin bool) (alpha, beta float64) {
	if len(x) != len(y) {
		panic("stat: slice length mismatch")
	}
	if weights != nil && len(weights) != len(x) {
		panic("stat: slice length mismatch")
	}

	w := 1.0
	if origin {
		var x2Sum, xySum float64
		for i, xi := range x {
			if weights != nil {
				w = weights[i]
			}
			yi := y[i]
			xySum += w * xi * yi
			x2Sum += w * xi * xi
		}
		beta = xySum / x2Sum

		return 0, beta
	}

	xu, xv := MeanVariance(x, weights)
	yu := Mean(y, weights)
	cov := covarianceMeans(x, y, weights, xu, yu)
	beta = cov / xv
	alpha = yu - beta*xu
	return alpha, beta
}

Faisant évidemment référence à d'autres fonctions pour calculer la variance, la fameuse covariance qu'il faudra aussi se taper et la moyenne.

Je retire toute cette affaire de poids qui m'est inutile pour en arriver à ceci qui semble prometteur:

// Computes normal linear regression for the series
// Returns beta, alpha
// Where: y = alpha + beta * x
//
// Slices x and y have to be the same size or things will go wrong
func ComputeLinearReg(
	x []float64,
	y []float64,
	varianceX float64,
	averageX float64,
	averageY float64,
) (float64, float64) {
	if len(x)-1 == 0 || varianceX == 0 {
		// Would cause divide by 0 errors
		return 0, 0
	}

	// Compute the covariance:
	var ss, xcompensation, ycompensation float64

	for i, xv := range x {
		yv := y[i]
		xd := xv - averageX
		yd := yv - averageY
		ss += xd * yd
		xcompensation += xd
		ycompensation += yd
	}
	cov := (ss - xcompensation*ycompensation/float64(len(x))) / float64(len(x)-1)

	beta := cov / varianceX
	alpha := averageY - (beta * averageX)

	return beta, alpha
}

J'ai aussi une autre fonction pour le calcul de la régression linéaire forcée de passer par l'origine et ce calcul là est beaucoup plus simple donc je vous le met pas. Vous aviez mêmem pas demandé ces autres calculs de toutes manière.

Je suis très joie sauf que, voilà, je constate avec une certaine tristesse que ça ne donne pas la même valeur que le tableur.

La différence n'est pas énorme mais le modèle généré par mon programme est clairement moins bon en terme de prédictions du nombre de mots que ce qui est calculé par le tableur.

Après un nombre non-négligeable d'itérations non fructueuses j'essaye ni-vu ni-connu en appelant la librairie que j'ai copié/collé en direct après avoir rangé ma fierté avec les paquets de couscous et de quinoa périmés depuis 2021 que je garde quand même au cas où.

He ben... Là, ça marche et donne le même résultat que le tableur.

Ben mince alors.

S'ensuit une danse endiablée de "debug par console.log" (enfin, fmt.Println() en l'occurence) d'une intensité probablement interdite par la convention de Genève.

Le coupable finit par se montrer.

Mon calcul de la variance est tout pourri

J'ai semble-t-il quelque peu sous-estimé les problèmes d'arrondis en virgule flottante. D'autant plus qu'on a des grandeurs du type 0,121234567 qu'il faut encore mettre au carré, ce qui augmente encore plus la flottaison de la virgule.

Dans l'article original, je parle tous les trois paragraphes de cette manie de mathématiciens de tout mettre au carré partout plutôt que d'utiliser des valeurs absolues et j'ai pas pensé que ça pouvait amplifier les erreurs de précision en virgule flottante.

Ceci dit, je crois comprendre que c'est pas 100% la faute des carrés.

Nonon, voyez vous, le calcul de la variance est naturellement vulnérable aux erreurs d'approximations parce qu'il implique des différences entre des termes qui sont proches (enfin, à moins que votre variance soit énorme mais aucuns de mes modèles ne sont pourris à a ce point).

Je rappelle qu'il s'agit effectivement de calculer une somme des différences des valeurs de la série par rapport à la moyenne (avec des carrés qui-vont-pas-nous-aider):

Formule de la variance en tant que somme des carrés des différences à la moyenne, divisée par le nombre d'éléments dans la population
Volé sur [Wikipedia]

Ce qui nous amène à la découverte principale du jour qui va changer votre vie: la notion mathématique d'ANNULATION CATASTROPHIQUE.

Wikipedia dit que l'article sur l'annulation catastrophique est orphelin parce que seulement lié par moins de trois autres articles
Soyez rassurés personne d'autre ne s'inquiète de l'existence de ce concept

En fait c'est bien simple (lol) si on possède deux grandeurs dont on connaît les valeurs en précision absolue, par ex:

  • 0.12345
  • 0.12344

Si je les soustrais, ça donne 0.00001.

Imaginons maintenant que j'avais une erreur de précision de 0.00001 sur la seconde valeur et qu'on avait:

  • 0.12345
  • 0.12343

La différence vaut maintenant 0.00002. C'est DEUX FOIS PLUS que la réalité. Si on met la somme au carré, c'est pire. Si on ajoute une centaine de ces sommes, c'est encore pire. Si on a une erreur de précision sur l'autre terme de la soustraction dans le mauvais sens, c'est encore-encore pire.

Une des manières simples de l'expliquer c'est que si l'erreur de précision commence à s'approcher du résultat attendu par la soustraction de ces deux nombres similaires (résultat donc proche de 0 par nature), on est dans la merde.

Une manière plus compliquée de l'expliquer parle de ce concept et du fait que les fonctions de soustractions peuvent avoir un résultat qui varie grandement sur de petites variations des variables d'entrée.

Maintenant que vous avez tout compris il faut trouver un moyen de sauver mon calcul de variance.

Je vais dès lors copier coller ce qu'ils ont fait dans la librairie Gonum.

Je sauve mon calcul

Dans la librairie Gonum ils parlent d'utiliser un algorithme en deux passes basé sur un document écrit par Chan, Tony F., Gene H. Golub et Randall J. LeVeque.

D'abord j'ai mis une heure à comprendre pourquoi ça parle de deux passes. Cela signifie simplement qu'on calcule d'abord la moyenne, puis la variance.

Mais euh... C'est déjà ce que je faisais et je vois même pas comment on pourrait faire autrement.

En fait, c'est possible de calculer la variance et la moyenne ensembles en parcourant une seule fois le jeu de données.

Je pensais que j'avais un problème en m'obstinant à éviter de parcourir le texte de mes articles une fois de plus pour calculer un truc (ici le nombre de mots) mais les mathématiciens sont pires, ils veulent pas avoir deux boucles for pour calculer la moyenne puis la variance alors que franchement une vieille somme de mes deux de milliers d'articles (j'ai pas 1000 articles) c'est juste immédiat pour un microprocesseur moderne. Mais bon.

De toutes manières l'algorithme en une passe accentue massivement les erreurs d'approximation. Enfin, c'est ce que j'ai lu, je suis obligé de croire deux ou trois experts dans cette aventure parce qu'on a qu'une seule vie et je dois encore préparer le spag bolo de ce soir.

Bon mais alors c'est quoi la différence en pratique?

Ils calculent une valeur obscure en plus nommée compensation dans le code qui est ensuite soustraite (au carré bien entendu) de la valeur de la variance.

Je crois avoir vaguement trouvé le document scientifique évoqué dans le code de Gonum et le terme de compensation:

Formule de la variance avec un terme en plus pour retirer la compensation due à la perte de précision
J'ai aucune idée de pourquoi ce terme en plus retire les erreurs de précision

On se retrouve avec une bonne vieille différence de plus qu'avant mais ils expliquent dans le texte que les termes sont censés être fort différents et donc ne pas menser à une ANNULATION CATASTROPHIQUE. Ouf alors.

Quoi qu'il en soit, après un bon vieux copier/coller ajusté, mon calcul de régression linéaire correspond désormais à celui du tableur.

J'imagine qu'ils utilisent le même algorithme pour la variance (j'utilisais LibreOffice pour l'exercice).

Je peux ajouter mes petites valeurs calculées au code du nouveau blog:

Les valeurs de régression linéaires que j'utilise dans le code du nouveau blog

Et préparer l'estimation de temps de lecture dans toute sa splendeur:

export const readingTimeDescription = (length: number): string => {
  const time = evaluateReadingTime(length)
  // The time is in minutes
  if (time > 50) {
    return "Beaucoup trop de minutes de lecture"
  } else if (time < 1) {
    return "Temps de lecture extrêmement court"
  } else {
    return `${time} minutes de lecture${time > 30 ? ' (désolé)' : ' (facile)'}`
  }
}

Avec tout ça j'aurai appris deux ou trois trucs sur les maths d'ordinateur, les stats et que les hippopotames suent de la crème solaire. Bon celui-là est sans rapport mais non sans importance!

Commentaires

Il faut JavaScript activé pour écrire des commentaires ici

Ajouter un commentaire

Votre commentaire a été ajouté
(enfin, je pense)