Leçon 7 sur 8
Unité · La table que vous analysez
Quand le registre et le rapport divergent
Recalculer l'indicateur depuis le registre ligne à ligne, le poser à côté de ce qui a été remonté, et décomposer l'écart par unité de rapportage au lieu de le noyer dans une moyenne.
Deux chiffres pour la même chose
Quelque part se trouve un registre ligne à ligne — marques de présence journalières, cahier de dépistage, registre de patients. Ailleurs se trouve un chiffre mensuel qui en a été remonté. Les deux divergent, et cet écart est le nombre le plus instructif de ce cours.
Il est instructif parce qu’il est borné. Contrairement à la plupart des questions de qualité des données, celle-ci a une bonne réponse rangée dans un tiroir : le registre est la source, le rapport est une affirmation sur le registre, et la différence entre les deux est une quantité mesurable et non une opinion.
Les audits bailleurs appellent leur rapport le facteur de vérification — recompter les documents sources, diviser le recomptage par ce qui a été rapporté. Un facteur de vérification de 1,0 signifie que le rapportage est exact. Toute autre valeur est un nombre qui a une cause.
Recalculer depuis le registre
Agrégez le registre à la granularité de rapportage, exactement comme dans la leçon précédente.
attendance["marked"] = attendance["present"].isin(["true", "false"])
attendance["is_present"] = attendance["present"] == "true"
from_register = (
attendance.merge(roster[["student_id", "school_id"]], on="student_id",
how="left", validate="many_to_one")
.groupby("school_id")
.agg(marks=("marked", "sum"), present=("is_present", "sum"))
)
from_register["rate"] = from_register["present"] / from_register["marks"]
from_register <- attendance |>
left_join(select(roster, student_id, school_id), by = "student_id",
relationship = "many-to-one") |>
summarise(
marks = sum(present %in% c("true", "false")),
present = sum(present == "true", na.rm = TRUE),
.by = school_id
) |>
mutate(rate = present / marks)
Notez ce que compte marked. Une marque existe là où la valeur vaut true ou
false. Les valeurs Y, N et vide ne sont pas des marques selon cette
définition — et cette définition est une décision, ce qui fait toute la section
suivante.
La décision de codage qui déplace un dénominateur
Une école a enregistré la présence avec Y et N au lieu de true et false.
Recalculez des deux façons.
mapping = {"true": True, "false": False, "Y": True, "N": False}
attendance["is_present_mapped"] = attendance["present"].map(mapping)
both_ways = (
attendance.merge(roster[["student_id", "school_id"]], on="student_id")
.groupby("school_id")
.agg(
marks_strict=("is_present", lambda s: s.notna().sum()),
rate_strict=("is_present", "mean"),
marks_mapped=("is_present_mapped", lambda s: s.notna().sum()),
rate_mapped=("is_present_mapped", "mean"),
)
)
print(both_ways.loc["SCH09"])
both_ways <- attendance |>
left_join(select(roster, student_id, school_id), by = "student_id") |>
mutate(mapped = dplyr::recode(present, "Y" = "true", "N" = "false")) |>
summarise(
marks_strict = sum(present %in% c("true", "false")),
rate_strict = mean(present[present %in% c("true", "false")] == "true"),
marks_mapped = sum(mapped %in% c("true", "false")),
rate_mapped = mean(mapped[mapped %in% c("true", "false")] == "true"),
.by = school_id
)
| SCH09 | Strict | Y/N recodés |
|---|---|---|
| Marques au dénominateur | 2 015 | 2 865 |
| Taux de présence | 86,05 % | 85,72 % |
Lisez les deux colonnes ensemble, car la leçon est dans le contraste. Le dénominateur croît de 42 %. Le taux bouge d’un tiers de point.
C’est la forme la plus courante de ce problème et la raison pour laquelle il franchit si aisément les relectures. Quiconque contrôle le taux conclut que le codage n’avait pas d’importance. Quiconque rapporte ensuite « nombre de journées de présence enregistrées » se trompe de 850. Un défaut qui touche à peine un rapport peut être énorme sur un effectif, et le rapportage de programme est plein d’effectifs.
Sur l’ensemble du fichier, la même opération fait passer le taux du district de 88,47 % à 88,42 % et le dénominateur de 69 101 à 69 974.
Mettez les deux sources dans une seule table
Ne comparez jamais deux chiffres en regardant deux affichages. Joignez-les, avec l’unité de rapportage et la période pour clé, et calculez l’écart comme une colonne.
comparison = (
from_register.rename(columns={"present": "register_present"})
.join(reported.rename(columns={"present": "reported_present"}), how="outer")
)
comparison["difference"] = comparison["register_present"] - comparison["reported_present"]
comparison["verification_factor"] = (
comparison["register_present"] / comparison["reported_present"]
)
print(comparison.sort_values("difference").head(10))
comparison <- from_register |>
full_join(reported, by = c("school_id", "month"),
suffix = c("_register", "_reported")) |>
mutate(
difference = present_register - present_reported,
verification_factor = present_register / present_reported
) |>
arrange(difference)
Une jointure complète, délibérément. Une unité présente dans le registre et absente du rapport a cessé de rapporter ; une unité présente dans le rapport sans rien dans le registre remonte des chiffres que personne ne peut retrouver. Les deux sont des constats et une jointure interne supprime les deux.
Décomposez, ne moyennez pas
L’erreur la plus fréquente ici est de calculer un facteur de vérification unique pour tout le district. Un facteur de 1,02 pour le district peut être vingt écoles à 1,00 et une à 1,40, et la moyenne a masqué la seule chose sur laquelle agir.
print(comparison["verification_factor"].describe())
print(comparison[comparison["verification_factor"].sub(1).abs() > 0.05])
comparison |>
summarise(across(verification_factor, list(min = min, median = median, max = max)))
comparison |> filter(abs(verification_factor - 1) > 0.05)
Rapportez la distribution et les valeurs extrêmes, jamais la moyenne seule. Une bande de tolérance — tout ce qui sort de 0,95 à 1,05 est examiné — transforme ceci d’un nombre en une liste de travail, ce dont un superviseur peut effectivement se servir.
Pourquoi deux sources divergent
Quatre causes, appelant des réponses totalement différentes. Parcourez-les dans cet ordre.
- Des définitions différentes. Le registre compte des marques ; le rapport compte des enfants inscrits. De loin la cause la plus fréquente, la moins souvent soupçonnée, et ce n’est pas du tout un problème de qualité des données — ce sont deux indicateurs sous un seul nom.
- Des périodes différentes. Le rapport couvre un mois calendaire, le registre a été extrait le 28. Vérifiez les bornes avant toute chose.
- Des couvertures différentes. Le rapport inclut une école que l’extraction du registre excluait, ou l’inverse. L’anti-jointure de la leçon 1 y répond en une ligne.
- La transcription. Quelqu’un a additionné la colonne à la main. C’est la cause que tout le monde suppose et la moins fréquente des quatre.
Seule la dernière est une erreur au sens ordinaire. Les trois premières sont des problèmes de rapprochement, et les rapporter comme des « problèmes de qualité des données » fait porter à un superviseur la responsabilité de ce qui a été décidé dans une réunion de conception de formulaire.
Rapportez l’écart, ne le faites pas disparaître
La tentation est d’ajuster le chiffre du registre jusqu’à ce qu’il corresponde au rapport, ou de ne publier que celui qui a la meilleure allure. Publiez les deux, avec l’écart.
summary = pd.DataFrame({
"source": ["Line-level register", "Reported monthly returns"],
"attendance days recorded": [69101, 68240],
"attendance rate": ["88.5%", "89.1%"],
})
summary <- tibble::tibble(
source = c("Line-level register", "Reported monthly returns"),
days = c(69101, 68240),
rate = c("88.5%", "89.1%")
)
Deux lignes et une phrase nommant le principal contributeur à l’écart. C’est le matériau d’une évaluation de la qualité des données, et le cours Évaluation de la qualité des données — le suivant et le dernier de ce module — en fait une routine avec une stratégie d’échantillonnage et un plan d’action corrective.
Un écart que vous rapportez est un constat. Un écart que vous refermez en silence est un chiffre que personne ne peut reproduire, vous compris, dans six mois.
La suite
Toutes les pièces sont en place — des jointures démontrables, une granularité énoncée, la bonne forme, un dénominateur, un calendrier complet et une source rapprochée. La dernière leçon les assemble en une table d’analyse unique : une ligne par unité d’analyse, chaque colonne traçable jusqu’à sa provenance, construite par un script qui va des fichiers bruts à la table finale en une seule commande.