Retour à la leçon·Leçon 2 sur 8·Comment la base est structurée
La hiérarchie qui bouge sous vos pieds
Le même diaporama que les téléchargements, rendu sous forme de page. Lancez le diaporama pour le présenter en plein écran — les flèches ou un clic avancent d'une diapositive, Échap quitte.
Ce que couvre cette leçon
- Un arbre, et tout y est accroché
- Les groupes ne sont pas des niveaux
- La propriété dont personne ne vous prévient
- Que faire
- La réaffectation, reconstituée
- Agréger vers le haut sans compter deux fois
- La couverture est un indicateur de niveau district
- La suite
Notes du présentateur
Unités d'organisation, niveaux et groupes, et la propriété dont personne ne vous prévient — l'arbre est courant et non historique, si bien qu'une formation réaffectée en mars réécrit en silence les totaux de district de l'an dernier.Un arbre, et tout y est accroché — Exemple
Level 1 Country Level 2 Region Level 3 District Level 4 Facility <- our 38 unitsNotes du présentateur
Toute valeur du système est rattachée à une unité d'organisation, et les unités forment un arbre unique — pays, région, district, formation sanitaire. Le niveau où siège une unité est une propriété de l'arbre, non une colonne de l'unité.Un arbre, et tout y est accroché — En Python
import pandas as pd vax = pd.read_csv("vaccination-coverage-2024.v1.csv", parse_dates=["period"]) print(f"{vax['facility_id'].nunique()} facilities in the extract")Notes du présentateur
L'agrégation est le parcours de l'arbre vers le haut. Demandez un chiffre de district et le système somme toutes les formations situées dessous ; demandez un chiffre national et il somme tous les districts. C'est la même discipline « agréger à la granularité » qu'enseignait le cours sur les jointures, avec une granularité fournie par une hiérarchie que quelqu'un d'autre entretient.Un arbre, et tout y est accroché — En R
library(dplyr) n_distinct(vax$facility_id)Notes du présentateur
Trente-huit, toutes au niveau 4. L'extraction ne porte aucun parent, ce qui est le cas habituel et la raison pour laquelle la plupart des totaux de district de ce secteur se calculent en joignant une liste de formations plutôt que par le système.Les groupes ne sont pas des niveaux — En Python
by_type = ( vax.groupby("facility_type")["facility_id"].nunique().sort_values(ascending=False) ) print(by_type)Notes du présentateur
L'extraction a une colonnefacility_type, et il vaut la peine d'être clair sur ce qu'elle est.Les groupes ne sont pas des niveaux — En R
vax |> summarise(facilities = n_distinct(facility_id), .by = facility_type)Les groupes ne sont pas des niveaux
Type de formation Formations Poste de santé 20 Centre de santé 16 Hôpital de district 2 Les groupes ne sont pas des niveaux
- Une unité a exactement un parent et un nombre quelconque de groupes. Poste de santé est un groupe ; cela ne dit pas…
- Les groupes peuvent se chevaucher. Une formation peut être à la fois « poste de santé », « difficile d'accès » et «…
- Les jeux de groupes sont la version sûre. Un jeu de groupes est un ensemble de groupes mutuellement exclusifs — le…
- Demandez si ce sur quoi vous regroupez est un jeu de groupes — Si ce n'en est pas un, vérifiez que les groupes…
Notes du présentateur
C'est un groupe d'unités d'organisation, non un niveau. La distinction compte de trois façons pratiques. Demandez si ce sur quoi vous regroupez est un jeu de groupes. Si ce n'en est pas un, vérifiez que les groupes partitionnent avant de sommer.La propriété dont personne ne vous prévient
- DHIS2 stocke la hiérarchie courante, non une hiérarchie historique — Une valeur est rattachée à une formation
- Un total de district différent de la même requête lancée six mois plus tôt, sans aucune saisie entre-temps.
- Un chiffre annuel publié qu'on ne peut pas reproduire.
- Deux rapports de la même période qui divergent, tous deux exacts le jour où ils ont été produits.
Notes du présentateur
Voici celle qui coûte un trimestre. DHIS2 stocke la hiérarchie courante, non une hiérarchie historique. Une valeur est rattachée à une formation. La formation est rattachée à un parent maintenant. Il n'existe, dans le modèle de données ordinaire, aucune trace du parent qu'elle avait en mars. Aussi, lorsqu'une formation est réaffectée du district A au district B — révision de limites, réorganisation, correction — toutes ses données historiques la suivent. Le total du district A pour janvier dernier, déjà rapporté et déjà dans le tableur d'un bailleur, change dès que quelqu'un le redemande. Trois symptômes, tous d'allure de problèmes de qualité et aucun n'en étant un.Que faire
- Figez la hiérarchie avec l'extraction — Tirez la liste des unités d'organisation en même temps que les données et…
Notes du présentateur
Vous ne pouvez pas empêcher la hiérarchie de bouger. Vous pouvez l'empêcher de déplacer vos chiffres en silence. Figez la hiérarchie avec l'extraction. Tirez la liste des unités d'organisation en même temps que les données et stockez-la à côté des valeurs.Que faire — En Python
org_units = pd.read_csv("outputs/extract/2026-07/org-units.csv") # ea/facility id, name, parent_id, level, extracted_on pinned = vax.merge(org_units[["facility_id", "district_id"]], on="facility_id", how="left", validate="many_to_one") assert pinned["district_id"].notna().all(), "facility with no district in the pinned tree"Que faire — En R
org_units <- readr::read_csv(here::here("outputs", "extract", "2026-07", "org-units.csv")) pinned <- vax |> left_join(select(org_units, facility_id, district_id), by = "facility_id", relationship = "many-to-one") stopifnot(!any(is.na(pinned$district_id)))Que faire
- Agrégez depuis l'arbre figé, pas depuis l'arbre vivant — Un chiffre publié en mars est alors reproductible en…
- Consignez la date d'extraction sur les deux — L'avant-dernière leçon en fait une propriété du script d'extraction…
Notes du présentateur
La jointure estmany_to_oneet l'assertion n'est pas décorative. Une formation présente dans les données et absente de l'arbre figé signifie que l'arbre a été tiré à un autre moment que les données, précisément la situation que ceci doit prévenir. Agrégez depuis l'arbre figé, pas depuis l'arbre vivant. Un chiffre publié en mars est alors reproductible en septembre, car l'arbre qu'il a employé est sur disque. Consignez la date d'extraction sur les deux. L'avant-dernière leçon en fait une propriété du script d'extraction plutôt qu'une discipline.La réaffectation, reconstituée — En Python
reassignments = pd.DataFrame([ {"facility_id": "FAC017", "from_district": "D01", "to_district": "D02", "effective": "2024-04-01", "reason": "boundary revision"}, ]) def district_at(facility, period, log=reassignments): moves = log[(log["facility_id"] == facility) & (pd.to_datetime(log["effective"]) <= period)] return moves["to_district"].iloc[-1] if len(moves) else NoneNotes du présentateur
Là où la date de réaffectation est connue — et quelqu'un la connaît toujours, même si le système ne la connaît pas — le traitement honnête consiste à la porter explicitement.La réaffectation, reconstituée — En R
reassignments <- tibble::tribble( ~facility_id, ~from_district, ~to_district, ~effective, ~reason, "FAC017", "D01", "D02", as.Date("2024-04-01"), "boundary revision" )La réaffectation, reconstituée
Les totaux de district sont calculés sur la hiérarchie d'unités d'organisation au 28/07/2026. Une formation a été réaffectée de D01 à D02 en avril 2024 ; ses données 2024 figurent sous D02 sur toute l'année, y compris pour les périodes antérieures à la réaffectation.
La réaffectation, reconstituée
- Cette phrase est le livrable — Elle ne coûte rien et transforme un chiffre irreproductible en chiffre reproductible
Notes du présentateur
C'est une dimension à évolution lente, et la traiter comme telle est la bonne réponse. C'est aussi plus de travail que la plupart des équipes n'en feront, si bien que le repli est un étiquetage honnête. Cette phrase est le livrable. Elle ne coûte rien et transforme un chiffre irreproductible en chiffre reproductible.Agréger vers le haut sans compter deux fois
- Une formation apparaissant deux fois dans l'arbre — Impossible dans DHIS2 — une unité a un parent — mais tout à fait…
Notes du présentateur
Deux modes de défaillance quand vous faites l'agrégation vous-même. Une formation apparaissant deux fois dans l'arbre. Impossible dans DHIS2 — une unité a un parent — mais tout à fait possible dans le CSV de formations que quelqu'un vous a envoyé, où une formation ayant déménagé figure sous les deux districts.Agréger vers le haut sans compter deux fois — En Python
duplicated = org_units["facility_id"].duplicated(keep=False) assert not duplicated.any(), org_units.loc[duplicated, ["facility_id", "district_id"]]Agréger vers le haut sans compter deux fois — En R
stopifnot(!any(duplicated(org_units$facility_id)))Agréger vers le haut sans compter deux fois
- Sommer entre niveaux — Un total de district plus ses formations, c'est le district compté deux fois
Notes du présentateur
Sommer entre niveaux. Un total de district plus ses formations, c'est le district compté deux fois. Cela arrive quand une extraction mélange les niveaux — demandez le seul niveau 4, et vérifiez.Agréger vers le haut sans compter deux fois — En Python
print(org_units["level"].value_counts())Agréger vers le haut sans compter deux fois
- Toute extraction devrait être à exactement un niveau — Si ce n'est pas le cas, la première chose que fait votre script…
Notes du présentateur
Toute extraction devrait être à exactement un niveau. Si ce n'est pas le cas, la première chose que fait votre script est de filtrer sur un seul, en disant lequel.La couverture est un indicateur de niveau district — En Python
by_facility = ( vax[vax["report_submitted"] == True] .query("antigen == 'penta3'") .groupby("facility_id") .agg(doses=("doses_administered", "sum"), target=("target_population", "sum")) ) by_facility["coverage"] = by_facility["doses"] / by_facility["target"] print(by_facility["coverage"].describe()[["min", "max"]].round(3))Notes du présentateur
Un point du cours sur les enquêtes qui arrive ici avec son mécanisme. Les bassins de desserte se chevauchent et les gens traversent les limites pour se faire vacciner, si bien qu'un taux de couverture par formation est un numérateur d'une population sur un dénominateur d'une autre.La couverture est un indicateur de niveau district — En R
vax |> filter(report_submitted, antigen == "penta3") |> summarise(coverage = sum(doses_administered) / sum(target_population), .by = facility_id) |> summarise(min = min(coverage), max = max(coverage))Notes du présentateur
La dispersion entre formations est bien plus large que toute différence réelle de prestation, car la traversée de limites est un bruit au niveau de la formation et s'annule au niveau du district. Agrégez au niveau où la traversée se produit à l'intérieur de l'unité, et rapportez les chiffres par formation comme une charge de travail plutôt que comme une couverture.La suite
- Vous savez placer une valeur dans l'espace.
Notes du présentateur
Vous savez placer une valeur dans l'espace. L'unité suivante la place dans le temps — types de période, à quelle période appartient une saisie tardive, et l'enregistrement de complétude qui décide si la valeur était attendue.