cassionAnalyse de données

Retour à la leçonLeçon 2 sur 8Comment 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.

Diapositives · PDFDiapositives · PowerPoint

  1. Diapositive 1 / 28

    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.
  2. Diapositive 2 / 28

    Un arbre, et tout y est accroché — Exemple

    Level 1  Country
    Level 2  Region
    Level 3  District
    Level 4  Facility        <- our 38 units
    Notes 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é.
  3. Diapositive 3 / 28

    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.
  4. Diapositive 4 / 28

    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.
  5. Diapositive 5 / 28

    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 colonne facility_type, et il vaut la peine d'être clair sur ce qu'elle est.
  6. Diapositive 6 / 28

    Les groupes ne sont pas des niveaux — En R

    vax |> summarise(facilities = n_distinct(facility_id), .by = facility_type)
  7. Diapositive 7 / 28

    Les groupes ne sont pas des niveaux

    Type de formationFormations
    Poste de santé20
    Centre de santé16
    Hôpital de district2
  8. Diapositive 8 / 28

    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.
  9. Diapositive 9 / 28

    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.
  10. Diapositive 10 / 28

    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.
  11. Diapositive 11 / 28

    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"
  12. Diapositive 12 / 28

    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)))
  13. Diapositive 13 / 28

    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 est many_to_one et 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.
  14. Diapositive 14 / 28

    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 None
    Notes 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.
  15. Diapositive 15 / 28

    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"
    )
  16. Diapositive 16 / 28

    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.
  17. Diapositive 17 / 28

    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.
  18. Diapositive 18 / 28

    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.
  19. Diapositive 19 / 28

    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"]]
  20. Diapositive 20 / 28

    Agréger vers le haut sans compter deux fois — En R

    stopifnot(!any(duplicated(org_units$facility_id)))
  21. Diapositive 21 / 28

    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.
  22. Diapositive 22 / 28

    Agréger vers le haut sans compter deux fois — En Python

    print(org_units["level"].value_counts())
  23. Diapositive 23 / 28

    Agréger vers le haut sans compter deux fois — En R

    org_units |> count(level)
  24. Diapositive 24 / 28

    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.
  25. Diapositive 25 / 28

    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.
  26. Diapositive 26 / 28

    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.
  27. Diapositive 27 / 28

    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.
  28. Diapositive 28 / 28

    La suite

    Lire la leçon complète, avec le code exécutable Retour à la leçon