Faire la somme ou la moyenne d’une colonne
Additionner, compter ou moyenner une colonne d’un fichier, sans qu’une ligne illisible fausse le total en silence.
Pas besoin d’IA
GratuitInstantanéVos données ne sortent pas
Le verdict
N0, et la seule chose qui mérite d’être écrite est ce qu’on met autour de l’addition. Les chiffres sont lus comme ils sont écrits, alignés sur le plus grand nombre de décimales de la colonne, et additionnés comme des entiers : la somme est exacte sur dix mille lignes comme sur six. La comparaison est dans le test — sur six montants de facture ordinaires, l’addition en flottants rend 244.98000000000002 quand la facture imprime 244,98. Et la réponse classique à ce problème, la somme compensée de Kahan ou le `fsum` de Python, rend exactement la même valeur fausse : l’erreur n’est pas dans l’accumulation, elle est dans la représentation, puisque « 83.87 » n’est déjà pas 83,87 en binaire. Le reste est de la discipline de compte : toute ligne qui n’est pas un nombre revient avec son numéro et sa raison, une colonne sans aucun nombre ne rend pas zéro mais rien, et la moyenne — seul résultat qui puisse demander un arrondi — est rendue avec la règle d’arrondi annoncée, la somme exacte et le compte à côté d’elle.
Une addition d’entiers sur les chiffres écrits, et le compte de ce qui a été laissé
Pas besoin d’IA
Règle et algorithme classiqueN0GratuitInstantané
Python
"""
Additionner une colonne d'un fichier, exactement, et dire ce qui a été laissé.
Niveau N0. Additionner est ce qu'un ordinateur a su faire avant tout le
reste : l'intéressant, dans ce travail, est donc partout sauf dans
l'addition.
C'est dans l'arithmétique, d'abord. Une colonne d'argent additionnée en
nombres à virgule flottante dérive : le test de cette fiche additionne six
montants ordinaires et montre que le résultat n'est pas celui qu'imprime la
facture. Rien ici ne devient donc un flottant. Une valeur est lue comme les
chiffres qui ont été écrits, toutes les valeurs sont alignées sur le même
nombre de décimales, et la somme est une addition d'entiers — exacte, quelle
que soit la longueur de la colonne.
C'est dans ce qui n'est pas un nombre, ensuite. Une vraie colonne porte une
cellule vide, un « n/a », une ligne de total, une valeur avec un symbole
monétaire. Les passer en silence, c'est ainsi qu'une moyenne finit par porter
sur huit cents lignes sur mille sans que personne ne le remarque. Toute
valeur qui n'a pas pu être lue revient avec son numéro de ligne et sa raison.
Et c'est dans la moyenne, le seul résultat qui puisse demander un arrondi :
elle est rendue deux décimales au-delà des données, arrondie en s'éloignant
de zéro, et la somme exacte comme le compte reviennent à côté d'elle pour
qu'un appelant puisse arrondir autrement.
"""
from __future__ import annotations
import re
# What a number looks like in a file written for a program: a sign, digits, and
# at most one decimal separator. Grouping separators are not read here — the
# entry on extracting amounts from a text is where « 1 250,00 » is settled.
NUMBER = re.compile(r"\A[+-]?[0-9]+(?:[.,][0-9]+)?\Z")
# The two decimal signs this rung knows. The caller declares one; a value that
# uses the other is not misread, it is refused and counted.
SIGNS = (".", ",")
# How many decimals beyond the data's own the average is given with.
MEAN_EXTRA = 2
def aggregate(values, decimal_sign: str = ".") -> dict:
"""
The count, the exact sum, the average and the bounds of a column.
`values` is what a file gives: text, one item per row. Everything that is
not a number comes back in `skipped` rather than being passed over.
"""
if decimal_sign not in SIGNS:
return _report(None, [], f"the decimal sign must be one of {' '.join(SIGNS)}")
if not isinstance(values, (list, tuple)):
# A text is iterable and is not a column: taken as one, it would be
# read letter by letter.
return _report(None, [], "expected a list of values")
rows = list(values)
read, skipped, scale = [], [], 0
for index, value in enumerate(rows):
# A boolean is an integer in Python and is not one anywhere else: it
# is refused on both sides rather than counted as 0 or 1.
numeric = isinstance(value, (int, float)) and not isinstance(value, bool)
text = value.strip() if isinstance(value, str) else str(value) if numeric else None
if text is None or not NUMBER.match(text) or _other_sign(text, decimal_sign):
skipped.append({"row": index, "value": value, "why": _why(value, decimal_sign)})
continue
digits, places = _scaled(text, decimal_sign)
read.append((digits, places))
scale = max(scale, places)
if not read:
return _report({"count": 0, "sum": None, "mean": None, "minimum": None,
"maximum": None, "decimals": 0}, skipped, None)
aligned = [digits * 10 ** (scale - places) for digits, places in read]
total = sum(aligned)
mean = _half_away(total * 10 ** MEAN_EXTRA, len(aligned))
return _report({"count": len(aligned), "sum": _text(total, scale),
"mean": _text(mean, scale + MEAN_EXTRA),
"minimum": _text(min(aligned), scale),
"maximum": _text(max(aligned), scale),
"decimals": scale}, skipped, None)
def _scaled(text: str, sign: str):
"""The digits that were written, as an integer, and how many decimals."""
whole, _, fraction = text.replace(sign, ".").partition(".")
return int(f"{whole}{fraction}"), len(fraction)
def _other_sign(text: str, sign: str) -> bool:
return any(other in text for other in SIGNS if other != sign)
def _why(value, sign: str) -> str:
if not isinstance(value, str) and not (isinstance(value, (int, float))
and not isinstance(value, bool)):
return "not text"
text = str(value).strip()
if not text:
return "empty"
if _other_sign(text, sign):
return f"written with « {SIGNS[0] if sign == SIGNS[1] else SIGNS[1]} » as the decimal sign"
return "not a number"
def _half_away(numerator: int, denominator: int) -> int:
"""Rounded away from zero on a half, which is the rule stated in the docstring."""
sign = -1 if numerator < 0 else 1
return sign * ((2 * abs(numerator) + denominator) // (2 * denominator))
def _text(digits: int, places: int) -> str:
"""The integer written back with its decimal point, and no float on the way."""
sign = "-" if digits < 0 else ""
body = str(abs(digits)).rjust(places + 1, "0")
return f"{sign}{body}" if places == 0 else f"{sign}{body[:-places]}.{body[-places:]}"
def _report(figures, skipped, reason) -> dict:
return {"figures": figures, "skipped": skipped, "reason": reason}JavaScript
/**
* Additionner une colonne d'un fichier, exactement, et dire ce qui a été laissé.
*
* Niveau N0. Additionner est ce qu'un ordinateur a su faire avant tout le
* reste : l'intéressant, dans ce travail, est donc partout sauf dans
* l'addition.
*
* C'est dans l'arithmétique, d'abord. Une colonne d'argent additionnée en
* nombres à virgule flottante dérive : le test de cette fiche additionne six
* montants ordinaires et montre que le résultat n'est pas celui qu'imprime la
* facture. Rien ici ne devient donc un flottant. Une valeur est lue comme les
* chiffres qui ont été écrits, toutes les valeurs sont alignées sur le même
* nombre de décimales, et la somme est une addition d'entiers — exacte, quelle
* que soit la longueur de la colonne.
*
* C'est dans ce qui n'est pas un nombre, ensuite. Une vraie colonne porte une
* cellule vide, un « n/a », une ligne de total, une valeur avec un symbole
* monétaire. Les passer en silence, c'est ainsi qu'une moyenne finit par porter
* sur huit cents lignes sur mille sans que personne ne le remarque. Toute
* valeur qui n'a pas pu être lue revient avec son numéro de ligne et sa raison.
*
* Et c'est dans la moyenne, le seul résultat qui puisse demander un arrondi :
* elle est rendue deux décimales au-delà des données, arrondie en s'éloignant
* de zéro, et la somme exacte comme le compte reviennent à côté d'elle pour
* qu'un appelant puisse arrondir autrement.
*/
// What a number looks like in a file written for a program: a sign, digits,
// and at most one decimal separator. Grouping separators are not read here —
// the entry on extracting amounts from a text is where « 1 250,00 » is settled.
const NUMBER = /^[+-]?[0-9]+(?:[.,][0-9]+)?$/;
// The two decimal signs this rung knows. The caller declares one; a value that
// uses the other is not misread, it is refused and counted.
export const SIGNS = ['.', ','];
// How many decimals beyond the data's own the average is given with.
export const MEAN_EXTRA = 2;
/**
* The count, the exact sum, the average and the bounds of a column.
*
* `values` is what a file gives: text, one item per row. Everything that is
* not a number comes back in `skipped` rather than being passed over.
*/
export function aggregate(values, decimalSign = '.') {
if (!SIGNS.includes(decimalSign)) {
return report(null, [], `the decimal sign must be one of ${SIGNS.join(' ')}`);
}
if (!Array.isArray(values)) {
// A text is iterable and is not a column: taken as one, it would be read
// letter by letter.
return report(null, [], 'expected a list of values');
}
const read = [];
const skipped = [];
let scale = 0;
values.forEach((value, index) => {
// A boolean is an integer in Python and is not one anywhere else: it is
// refused on both sides rather than counted as 0 or 1.
const numeric = typeof value === 'number' && Number.isFinite(value);
const text = typeof value === 'string' ? value.trim() : (numeric ? String(value) : null);
if (text === null || !NUMBER.test(text) || otherSign(text, decimalSign)) {
skipped.push({ row: index, value, why: why(value, decimalSign) });
return;
}
const [digits, places] = scaled(text, decimalSign);
read.push([digits, places]);
scale = Math.max(scale, places);
});
if (read.length === 0) {
return report({
count: 0, sum: null, mean: null, minimum: null, maximum: null, decimals: 0,
}, skipped, null);
}
const aligned = read.map(([digits, places]) => digits * 10n ** BigInt(scale - places));
const total = aligned.reduce((sum, item) => sum + item, 0n);
const mean = halfAway(total * 10n ** BigInt(MEAN_EXTRA), BigInt(aligned.length));
const least = aligned.reduce((low, item) => (item < low ? item : low));
const most = aligned.reduce((high, item) => (item > high ? item : high));
return report({
count: aligned.length,
sum: asText(total, scale),
mean: asText(mean, scale + MEAN_EXTRA),
minimum: asText(least, scale),
maximum: asText(most, scale),
decimals: scale,
}, skipped, null);
}
/** The digits that were written, as an integer, and how many decimals. */
function scaled(text, sign) {
const [whole, fraction = ''] = text.replace(sign, '.').split('.');
return [BigInt(`${whole}${fraction}`), fraction.length];
}
function otherSign(text, sign) {
return SIGNS.some((other) => other !== sign && text.includes(other));
}
function why(value, sign) {
if (typeof value !== 'string' && !(typeof value === 'number' && Number.isFinite(value))) {
return 'not text';
}
const text = String(value).trim();
if (!text) return 'empty';
if (otherSign(text, sign)) {
const other = sign === SIGNS[1] ? SIGNS[0] : SIGNS[1];
return `written with « ${other} » as the decimal sign`;
}
return 'not a number';
}
/** Rounded away from zero on a half, which is the rule stated in the docstring. */
function halfAway(numerator, denominator) {
const sign = numerator < 0n ? -1n : 1n;
const size = numerator < 0n ? -numerator : numerator;
return sign * ((2n * size + denominator) / (2n * denominator));
}
/** The integer written back with its decimal point, and no float on the way. */
function asText(digits, places) {
const sign = digits < 0n ? '-' : '';
const body = (digits < 0n ? -digits : digits).toString().padStart(places + 1, '0');
return places === 0 ? `${sign}${body}`
: `${sign}${body.slice(0, -places)}.${body.slice(-places)}`;
}
function report(figures, skipped, reason) {
return { figures, skipped, reason };
}Essayer
interactifLe code ci-dessus tourne dans votre navigateur. Écrivez dans le champ : le résultat se refait sous vos yeux.
Rien ne part sur le réseau : l’addition se fait dans votre navigateur, sur des entiers. La ligne « en flottants » est la même colonne additionnée comme le ferait un code ordinaire.
Ou partez d’un exemple
La saisie libre et les autres exemples ont besoin de JavaScript. L’exemple affiché ci-dessous, lui, a été calculé à la construction du site.
Ce qu’il renvoie
6 ligne(s) lue(s)
Le détail
| Ce que le code rend | Valeur |
|---|---|
| Lignes lues | 6 |
| Somme exacte | 244.98 |
| La même somme en flottants | 244.98000000000002 |
| Moyenne des lignes lues | 40.8300 |
| Plus petite et plus grande | 1.38 … 83.87 |
Là, ça casse
Ce que ça implique
- Vos données ne sortent pas
- Résultat toujours identique
- Se teste unitairement, cas par cas
- Aucun fournisseur dans la boucle
- Empreinte négligeable
Ce que cela ajoute à votre périmètre réglementaire
- Aucun périmètre spécifique ajouté : la colonne est lue dans votre processus et n’est transmise à personne
- Un fichier de chiffres d’affaires ou de salaires reste chez vous, ce qui est la seule chose que ce niveau demande
Point de rupture
La moyenne porte sur les lignes lues, pas sur les lignes du fichier : dans le test, trois des sept lignes de la colonne ne sont pas des nombres, et la moyenne rendue est celle des quatre autres. Rien ne peut le deviner à votre place — une ligne vide est peut-être un zéro, peut-être une donnée manquante, et les deux ne donnent pas la même moyenne. Le témoin est dans le même test : chaque ligne écartée revient avec son numéro et sa raison, et le compte des lignes lues est rendu à côté de la moyenne ; une moyenne publiée sans ce compte est une moyenne dont on ne sait pas sur quoi elle porte.
Quand passer au niveau suivant
Il n’y a pas de niveau au-dessus. Le jour où la colonne ne tient plus en mémoire, ce n’est pas un modèle qu’il faut, c’est la même addition faite par la base de données qui détient déjà les lignes.
Les quatre options
- RecommandéN0Une addition d’entiers sur les chiffres écrits, et le compte de ce qui a été laisséGratuit · Instantané
- N1Niveau sans objet ici
- N2Niveau sans objet ici
- N3Niveau sans objet ici
Les autres options
N1Niveau sans objet ici
Niveau sans objet ici
Modèle classique légerN1
Il n’y a rien à apprendre, et il n’y a rien à approcher : une somme a une valeur exacte, et un modèle qui l’estimerait à un pour cent près serait faux de la première ligne à la dernière. La seule décision de cette fiche — que faire d’une ligne qui n’est pas un nombre — n’est pas une décision statistique : elle appartient à la personne qui sait ce que ce fichier contient.
N2Niveau sans objet ici
Niveau sans objet ici
Petit modèle spécialisé auto-hébergéN2
Même argument, avec le budget en plus. Charger un modèle résident pour additionner une colonne reviendrait à payer une machine pour refaire, moins bien, l’opération que le processeur exécute en une instruction.
N3Niveau sans objet ici
Niveau sans objet ici
API de LLM généralisteN3
C’est le cas d’école de ce site. Un modèle généraliste rend un nombre qui ressemble à un total, sans l’avoir calculé, et la seule façon de vérifier sa réponse est de faire la somme — c’est-à-dire de faire ce qu’on voulait lui confier. S’y ajoute l’envoi du fichier lui-même, qui est souvent le fichier qu’on ne voulait pas envoyer.
Pour aller plus loin
- Python — « Floating-Point Arithmetic: Issues and Limitations », la page qui explique pourquoi 0,1 n’est pas 0,1
- Python — le module decimal, la réponse de la bibliothèque standard
- TC39 — la proposition Decimal pour JavaScript, qui n’a pas encore de type décimal
- Extraire les montants d’un texte — la fiche qui lit « 1 250,00 » avant que celle-ci ne l’additionne
Source des chiffres
- Python, module decimal (consulté) : « Decimal numbers can be represented exactly » — la bibliothèque standard décrit elle-même ce que le binaire ne fait pas
- Python, math.fsum (consulté) : « Return an accurate floating-point sum of values » — la somme compensée corrige l’accumulation, et la mesure de cette fiche montre qu’elle ne corrige pas la représentation
- TC39, proposition Decimal (consultée) : JavaScript n’a pas de type décimal, ce qui est la raison pour laquelle l’extrait additionne des `BigInt` plutôt que d’emprunter un type
- MDN, Number.MAX_SAFE_INTEGER (consulté) : au-delà de 2^53, un entier n’est plus représentable exactement en nombre JavaScript ordinaire, d’où les `BigInt`
Métadonnées
Information générale, tenue à jour au mieux. Ce n’est pas un avis juridique. Faites valider votre situation par un professionnel.