Site WWW de Laurent Bloch
Slogan du site

ISSN 2271-3905
Cliquez ici si vous voulez visiter mon autre site, orienté vers des sujets moins techniques.

Pour recevoir (au plus une fois par semaine) les nouveautés de ce site, indiquez ici votre adresse électronique :

L’informatique peut provoquer des erreurs de calcul

Un bon moyen d’obtenir des résultats faux lors de calculs scientifiques, c’est de recopier dans un ouvrage mathématique la formule de définition de la grandeur à calculer, et de la transposer telle quelle dans le langage de programmation utilisé.

En effet les nombres des ordinateurs ne sont pas les nombres réels de l’ensemble $\mathbbR$, mais un sous-ensemble fini des entiers de l’ensemble $\mathbbN$, que l’on peut agencer astucieusement pour représenter des nombres fractionnaires avec un nombre fini de chiffres après la virgule.

Calcul itératif par un algorithme de Donald Knuth

Donald Knuth, qui fait référence à B.P. Welford, a publié un algorithme qui répond à notre attente et qui a été abondamment analysé et trouvé robuste.

L’idée est simple : si nous connaissons la moyenne des $n-1$ premières valeurs, plutôt que de multiplier cette moyenne par $n-1$, de lui ajouter la $n$\fupième valeur et de diviser le résultat par $n$, il est plus simple d’ajouter à cette moyenne la différence entre elle et cette $n$\fupième valeur, divisée par $n$, qui est en fait la contribution de cette nouvelle valeur à la nouvelle moyenne.

Pour la variance, nous additionnons à chaque étape non pas les carrés des valeurs selon la formule classique, mais le produit de l’écart entre la nouvelle valeur et la moyenne précédente par l’écart entre le nouvelle valeur et la nouvelle moyenne, ce qui donne des valeurs numériques plus faibles (en général).

Soit l’algorithme :

Algo   : MoyenneVariance -- selon Donald E. Knuth (1998). 
Donnée : Observations tableau
Soient : nbObservations <- longueur(Observations)
         moyenne <- Observations[0]
         S2      <- 0
pour i allant de 1 à nbObservations - 1 faire
   tmp     <- moyenne
   moyenne <- moyenne + (Observations[i] - moyenne)/(i+1)
   S2      <- S2 + (Observations[i] - tmp) 
              * (Observations[i] - moyenne)
fin pour
Variance <- S2 / (nbObservations - 1)
retourne moyenne, Variance
fin MoyenneVariance

et en TypeScript :

const Observations: number[] = [12, 16, 13.5, 8, 11.5, 18]

function MoyenneVariance(T: number[]): number[] {
    const nbObservations: number = T.length;
    let moyenne = T[0];
    let S2 = 0;
    for (let i: number = 1; i < nbObservations; i++) {
        let tmp = moyenne;
        moyenne = moyenne + ((T[i] - moyenne) / (i + 1));
        S2 = S2 + ((T[i] - tmp) * (T[i] - moyenne));
    }
    let Variance = S2 / (nbObservations - 1);
    return [moyenne, Variance];
}
console.log(MoyenneVariance(Observations));

Ce programme lit les valeurs dans un tableau qui comporte un nombre par ligne et retourne un tableau constitué de la moyenne et de la variance.

Calcul avec un flux de données

Finalement il serait aussi commode de considérer que les données parviennent au programme par un flux d’origine quelconque, fichier, réseau, capteur ou appareil de mesure. Cela peut être commode si les données sont issues d’un appareil de mesure en continu.

La question :

Bonjour ChatGPT, je veux calculer la moyenne et la variance d’un flux de données, au fur et à mesure de leur lecture donnée par le fichier Data-2 ci-dessous :

13
11
45
78
22
12
58
32
23
55
24
56

Voici le programme principal :

#!/usr/bin/env -S npx tsx
// ne pas oublier : $ npm install commander
// et :  $ chmod a+x main.ts 

import { Command } from "commander";
import * as readline from "readline"; //
import * as fs from "fs";
import { MoyenneVariance } from "./class_MoyenneVariance.js";

let moyenne_variance = new MoyenneVariance;

// --- Programme principal avec Commander ---
const program = new Command();

program
    .name("calcul_moyenne_variance")
    .description("Lire un flux de données et calculer moyenne et variance au fur et à mesure")
    .argument("<flux>", "Fichier de données numériques")
    .action(async (flux: string) => {
	await lireLignes(flux);
  });

program.parse(); // déclenche le parsing des arguments

// --- Fonction utilitaire ---
async function lireLignes(flux: string) {
    let S2: number = 0;
    let n: number = 0;
    let moyenne: number = 0;
    let tmp: number = moyenne;
    let Variance: number = 0;
    const rl = readline.createInterface({
        input: fs.createReadStream(flux),
        crlfDelay: Infinity, // gère correctement \n et \r\n
    });

    for await (const ligne of rl) { 
        moyenne_variance.MoyenneVariance(
	    Number(ligne),
	    S2,
	    n,
	    moyenne,
	    Variance
	);
    }
}
console.log(lireLignes(flux));

et voici le programme de calcul :

export class MoyenneVariance {
    
    public MoyenneVariance(xi,
			   S2,
			   n,
			   moyenne,
			   Variance) {
	let tmp: number;
	while (xi) {
	    n = n + 1;
	    tmp = moyenne;
	    moyenne = moyenne + (xi - moyenne) / n;
	    S2 = S2 + (xi - tmp) * (xi - moyenne);
	    Variance = S2 / (n - 1);
	}
    }
}

Je pense que je ne détecte pas correctement la fin de fichier, parce que le programme tourne indéfiniment. Où est l’erreur ?

La réponse :

Excellent — ton intuition est très juste : ton programme tourne en boucle infinie à cause du while (xi) dans ta classe MoyenneVariance.

Voici la portion fautive :

while (xi) {
    n = n + 1;
    tmp = moyenne;
    moyenne = moyenne + (xi - moyenne) / n;
    S2 = S2 + (xi - tmp) * (xi - moyenne);
    Variance = S2 / (n - 1);
}

Le while (xi) signifie :

« tant que xi est une valeur "truthy" (différente de 0, undefined, null, etc.), répéter cette boucle. »

Or, xi ne change jamais dans cette boucle.
Donc si xi ≠ 0, la boucle ne se termine jamais → boucle infinie.

La solution :

Tu ne veux pas de boucle while ici — tu veux simplement mettre à jour les valeurs à chaque lecture d’un nouvel échantillon.

Donc, il faut remplacer cette méthode par quelque chose comme ceci :

export class MoyenneVariance {
    private n: number = 0;
    private moyenne: number = 0;
    private S2: number = 0;

    public ajouter(xi: number) {
        this.n += 1;
        const tmp = this.moyenne;
        this.moyenne = this.moyenne + (xi - this.moyenne) / this.n;
        this.S2 = this.S2 + (xi - tmp) * (xi - this.moyenne);
    }

    public getMoyenne(): number {
        return this.moyenne;
    }

    public getVariance(): number {
        return this.n > 1 ? this.S2 / (this.n - 1) : 0;
    }
}

Et dans ton programme principal :

Adapte la boucle pour utiliser cette méthode incrémentale :

async function lireLignes(flux: string) {
    const rl = readline.createInterface({
        input: fs.createReadStream(flux),
        crlfDelay: Infinity,
    });

    for await (const ligne of rl) {
        const x = Number(ligne);
        moyenne_variance.ajouter(x);
        console.log(
            `x = ${x}, moyenne = ${moyenne_variance.getMoyenne().toFixed(2)}, variance = ${moyenne_variance.getVariance().toFixed(2)}`
        );
    }

    console.log("\nRésultat final :");
    console.log(`Moyenne = ${moyenne_variance.getMoyenne().toFixed(2)}`);
    console.log(`Variance = ${moyenne_variance.getVariance().toFixed(2)}`);
}

Et surtout, ne fais pas :

console.log(lireLignes(flux));

car lireLignes() est asynchrone. Le console.log() affichera juste une Promise.
Il suffit de laisser Commander appeler la fonction via :

.action(async (flux) => await lireLignes(flux));.