Leçon 3 sur 8
Unité · Faire entrer l'export
CSV, Excel et largeur fixe, lus sans dommage
Encodages, séparateurs, classeurs multi-feuilles, lignes d'en-tête fusionnées et spécifications de colonnes — les pièges propres à chaque format, qui corrompent un fichier avant même que vous n'ayez regardé une seule valeur.
Ce que cette leçon suppose
Fondamentaux de l’analyse de données couvre le principe : déclarez vos types, déclarez vos sentinelles, et ne laissez jamais un lecteur en inférer l’un ou l’autre. Cette leçon le tient pour acquis et entre dans les formats eux-mêmes, car chacun échoue différemment et les défaillances ne se voient plus dans les données ensuite.
Le CSV n’est pas un format unique
read_csv compte une cinquantaine de paramètres. Quatre d’entre eux expliquent
l’essentiel des dégâts.
Le séparateur
Un fichier produit sur une machine Windows française ou espagnole est très souvent séparé par des points-virgules, car la virgule y est le séparateur décimal.
import pandas as pd
# Faux : tout atterrit dans une colonne unique nommee d'apres l'en-tete entier.
wrong = pd.read_csv("export.csv")
print(wrong.shape) # (2736, 1)
right = pd.read_csv("export.csv", sep=";", decimal=",")
print(right.shape) # (2736, 7)
Le symptôme est sans ambiguïté une fois connu : un DataFrame à exactement une
colonne. Vérifiez .shape immédiatement après chaque lecture.
L’encodage
Les caractères accentués dans les noms de communes — Gonaïves, L’Estère, Anse-Rouge — sont l’endroit où cela mord.
# UnicodeDecodeError, ou pire, silencieusement abime : "Gona\xefves"
muac = pd.read_csv(path)
muac = pd.read_csv(path, encoding="utf-8") # ce que vous voulez
muac = pd.read_csv(path, encoding="latin-1") # ce qu'Excel produit souvent
latin-1 ne lève jamais d’erreur, car tout octet est un caractère latin-1
valide. Cela en fait un mauvais diagnostic et un repli correct : si un fichier lu
en latin-1 affiche Gonaïves, il était en UTF-8 depuis le début et vous avez
dit le contraire au lecteur.
Les séparateurs de milliers transforment les nombres en texte
# target_population arrive en "1,480" et pandas le lit comme une chaine.
epi = pd.read_csv(path, thousands=",")
Sans thousands, cette colonne est de type objet et toute opération
arithmétique dessus échoue ou concatène. Vérifiez .dtypes après lecture, à
chaque fois.
Les zéros initiaux
Le réglage par défaut le plus coûteux de ce secteur.
# facility_id "007" devient l'entier 7 ; la jointure a la liste des formations
# echoue exactement pour celles dont le code portait un zero initial.
epi = pd.read_csv(path)
epi = pd.read_csv(path, dtype={"facility_id": "string"})
La règle se généralise : si vous n’allez jamais faire d’arithmétique dessus, ce n’est pas un nombre. Codes de formations sanitaires, numéros de téléphone, identifiants de grappes, numéros de ménages et codes administratifs sont du texte qui s’écrit avec des chiffres.
Une habitude défensive pour un fichier dont vous ne connaissez pas encore les colonnes :
# Tout lire en texte d'abord, regarder, puis convertir deliberement.
peek = pd.read_csv(path, dtype="string", nrows=200)
print(peek.head())
print(peek.columns.tolist())
Deux cents lignes suffisent à voir la forme et coûtent peu sur un gros fichier.
Excel
Il y a plus d’une feuille
book = pd.ExcelFile(path)
print(book.sheet_names)
# ['Cover', 'Data', 'Codebook', 'Sheet3']
data = pd.read_excel(path, sheet_name="Data")
read_excel sans sheet_name renvoie la première feuille, qui dans un
classeur de programme est le plus souvent une page de garde. Listez toujours les
feuilles d’abord.
sheet_name=None renvoie un dictionnaire de toutes les feuilles, ce qui permet
de traiter un classeur à une feuille par mois :
sheets = pd.read_excel(path, sheet_name=None)
monthly = pd.concat(sheets, names=["sheet"]).reset_index(level="sheet")
L’en-tête n’est pas en ligne 1
Les classeurs de programme portent couramment un titre, une ligne de logo et une ligne vide au-dessus du véritable en-tête.
data = pd.read_excel(path, sheet_name="Data", skiprows=3)
Le symptôme, ce sont des noms de colonnes comme Unnamed: 0, Unnamed: 1. Si
vous les voyez, la ligne d’en-tête est ailleurs.
Les cellules fusionnées produisent des valeurs manquantes
Une cellule fusionnée porte sa valeur en haut à gauche et rien dans le reste. Quand un nom de district est fusionné sur ses formations sanitaires, seule la première ligne de chaque district en porte un.
data["district"] = data["district"].ffill()
ffill est correct ici et dangereux en général : il n’est juste que parce que
les blancs proviennent d’une fusion, non d’une non-réponse. Ne l’appliquez jamais
à une colonne où un blanc pourrait signifier « non renseigné ».
Les dates Excel
Excel stocke les dates comme un nombre de jours depuis le 30/12/1899.
read_excel les convertit généralement, mais une colonne typée en texte dans le
classeur arrive sous forme de numéro de série brut.
# 45306 correspond au 15/01/2024
data["screening_date"] = pd.to_datetime(
data["screening_date"], unit="D", origin="1899-12-30"
)
Si une colonne de dates arrive en entiers à cinq chiffres, voilà pourquoi.
Fichiers à largeur fixe
Encore courants depuis les anciens systèmes d’information sanitaire et depuis les extractions ministérielles. Il n’y a pas de séparateur ; chaque champ occupe une plage fixe de caractères.
CH00854Terre-Neuve 2024-01-15022m133false
CH01207Gonaives 2024-01-15034f118false
COLSPECS = [(0, 7), (7, 22), (22, 32), (32, 35), (35, 36), (36, 39), (39, 44)]
NAMES = ["child_id", "commune", "screening_date", "age_months", "sex",
"muac_mm", "oedema"]
muac = pd.read_fwf(
path,
colspecs=COLSPECS,
names=NAMES,
dtype={"child_id": "string", "commune": "string"},
)
muac["commune"] = muac["commune"].str.strip()
Trois remarques :
- Les plages sont semi-ouvertes, comme les tranches Python :
(0, 7)couvre les caractères 0 à 6. Une erreur d’un rang ici décale tous les champs suivants d’un caractère et produit un fichier qui paraît presque juste. - Retirez le remplissage. Les champs à largeur fixe sont complétés par des
espaces :
"Terre-Neuve "ne sera pas égal à"Terre-Neuve". - Prenez la spécification dans la documentation, pas en comptant à l’écran.
read_fwfsait inférercolspecs, et il les infère depuis les espaces — ce qui échoue dès qu’un champ est plein ou qu’une valeur contient une espace.
Vérifier la lecture avant d’en faire quoi que ce soit
Chaque lecture devrait être suivie des quatre mêmes contrôles. Ensemble ils prennent une minute et attrapent presque tout ce qui précède.
def check(df, expected_rows=None):
print("forme ", df.shape)
print("types ", df.dtypes.value_counts().to_dict())
print("manquants ", df.isna().sum().to_dict())
print("1re ligne ", df.iloc[0].to_dict())
if expected_rows is not None:
assert len(df) == expected_rows, f"attendu {expected_rows}, obtenu {len(df)}"
muac = pd.read_csv(
RAW / "muac-screening-artibonite-2024.v1.csv",
dtype={"child_id": "string", "commune": "string", "sex": "string"},
na_values={"muac_mm": ["-99"]},
)
check(muac, expected_rows=4218)
- la forme attrape le mauvais séparateur et un téléchargement tronqué.
- les types attrapent les zéros initiaux perdus, les séparateurs de milliers et une colonne numérique lue comme du texte.
- les manquants attrapent un mauvais encodage et une sentinelle non déclarée.
- la première ligne attrape un décalage d’en-tête — si elle ressemble à un en-tête, c’en était un.
L’assertion sur le nombre de lignes est celle qu’on saute. Un fichier qui arrive avec 4 190 lignes au lieu de 4 218 a perdu quelque chose entre le serveur et vous, et le script doit le dire plutôt que de rapporter discrètement une charge de cas plus faible.
Lire un gros fichier
Si un fichier ne tient pas confortablement en mémoire, lisez les colonnes nécessaires plutôt que toutes :
COLUMNS = ["student_id", "attendance_date", "present"]
attendance = pd.read_csv(path, usecols=COLUMNS, dtype={"student_id": "string"})
usecols économise beaucoup sur un export large. Au-delà, chunksize renvoie un
itérateur de morceaux que vous agrégez au fil de l’eau :
totals = []
for chunk in pd.read_csv(path, chunksize=100_000, dtype={"student_id": "string"}):
totals.append(chunk.groupby("student_id")["present"].sum())
per_student = pd.concat(totals).groupby(level=0).sum()
Le fichier de présence de 70 000 lignes n’en a pas besoin. Une extraction DHIS2 pluriannuelle, si.
Ce qui vient ensuite
Le fichier est en mémoire, forme et types intacts. La leçon suivante traite de ce qui est écrit à l’intérieur des colonnes : codes sentinelles, vocabulaires catégoriels, valeurs booléennes saisies de cinq façons différentes, et les étiquettes de valeurs qu’un fichier Stata porte et qu’un CSV jette.