cassionAnalyse de données

Leçon 6 sur 8

Unité · Sortir les données

Une extraction que l'on peut désigner

Un script, un dossier daté, un manifeste qui consigne ce qui a été demandé et quand, et la réponse brute conservée à côté du tableau propre — pour qu'un chiffre publié en mars soit encore défendable en septembre.

PythonR75 minDéfinitions d'indicateurs de l'UNICEFNorme humanitaire fondamentale (CHS)Gestion axée sur les résultats (GAR)

La question à laquelle cette leçon répond

Six mois après avoir publié un taux de couverture de district, quelqu’un relance la requête et obtient autre chose. Cela arrivera, et il y a quatre raisons légitimes — saisie tardive, dénominateur révisé, formation réaffectée, et tables analytiques qui n’avaient pas tourné.

Vous ne pouvez en empêcher aucune. Vous pouvez rendre possible de dire laquelle c’était, et cela tient entièrement à ce que vous avez conservé.

La forme

extracts/
  2026-07-28/
    manifest.json          what was requested, by whom, when
    raw/
      analytics.json       the response, untouched
      dataValueSets.json
      dataElements.json
      organisationUnits.json
      dataSets.json
      indicators.json
    tidy/
      coverage_by_facility_period.csv

Trois règles donnent sa valeur à ce dossier.

Un dossier daté par extraction, jamais écrasé. Le disque coûte moins cher que la réunion.

La réponse brute est conservée intacte. Si votre analyse syntaxique s’avère fausse, la donnée est toujours là. Un CSV propre est un artefact dérivé, et un artefact dérivé ne se dé-dérive pas.

Le manifeste est l’essentiel. Tout le reste s’en récupère.

Le manifeste

import json
import datetime as dt
from pathlib import Path

def write_manifest(folder, request, system_info):
    manifest = {
        "extracted_at": dt.datetime.now(dt.timezone.utc).isoformat(),
        "extracted_by": "me-officer@example.org",
        "instance": request["base_url"],
        "dhis2_version": system_info.get("version"),
        "analytics_last_run": system_info.get("lastAnalyticsTableSuccess"),
        "request": {
            "endpoint": request["endpoint"],
            "dx": request["dx"],
            "ou": request["ou"],
            "pe": request["pe"],
        },
        "rows_returned": request["rows"],
        "script_version": "extract.py 1.3",
    }
    Path(folder, "manifest.json").write_text(json.dumps(manifest, indent=2))
    return manifest
manifest <- list(
  extracted_at        = format(Sys.time(), "%Y-%m-%dT%H:%M:%SZ", tz = "UTC"),
  instance            = base_url,
  analytics_last_run  = system_info$lastAnalyticsTableSuccess,
  request             = list(endpoint = "analytics", dx = dx, ou = ou, pe = pe),
  rows_returned       = nrow(tidy),
  script_version      = "extract.R 1.3"
)

jsonlite::write_json(manifest, file.path(folder, "manifest.json"), auto_unbox = TRUE)

Deux champs justifient à eux seuls leur place. analytics_last_run dit si une saisie tardive a pu être incluse. pe consigne les périodes exactement demandées, ce qui rend une fenêtre relative comme « les douze derniers mois » reconstructible au lieu d’être la description du jour où le script a tourné.

Le reste est la provenance que réclamait déjà le cours Jointures et restructuration, arrivant ici avec un système d’où la tirer.

Le script

def extract(base_url, dx, ou, pe, token, out_root="extracts"):
    session = requests.Session()
    session.headers["Authorization"] = f"ApiToken {token}"

    folder = Path(out_root, dt.date.today().isoformat())
    (folder / "raw").mkdir(parents=True, exist_ok=True)
    (folder / "tidy").mkdir(exist_ok=True)

    info = session.get(f"{base_url}/system/info", timeout=60).json()

    payload = analytics(dx, ou, pe, session)
    (folder / "raw" / "analytics.json").write_text(json.dumps(payload))

    for name, path in METADATA_ENDPOINTS.items():
        meta = session.get(f"{base_url}/{path}", timeout=120).json()
        (folder / "raw" / f"{name}.json").write_text(json.dumps(meta))

    tidy = to_frame(payload)
    tidy.to_csv(folder / "tidy" / "coverage_by_facility_period.csv", index=False)

    write_manifest(folder, {"base_url": base_url, "endpoint": "analytics",
                            "dx": dx, "ou": ou, "pe": pe, "rows": len(tidy)}, info)
    return folder
extract <- function(base_url, dx, ou, pe, token) {
  folder <- file.path("extracts", Sys.Date())
  dir.create(file.path(folder, "raw"), recursive = TRUE, showWarnings = FALSE)
  dir.create(file.path(folder, "tidy"), showWarnings = FALSE)
  # ... same shape: system info, analytics, metadata, tidy, manifest
  folder
}

La fenêtre est calculée, puis passée en argument. L’appelant décide quels douze mois ; le script les consigne. Ce seul choix sépare une extraction que l’on peut désigner d’une extraction que l’on peut seulement relancer.

def last_full_months(n, today=None):
    today = today or dt.date.today()
    first_of_this = today.replace(day=1)
    months = []
    for i in range(n, 0, -1):
        m = first_of_this - dt.timedelta(days=1)
        for _ in range(i - 1):
            m = m.replace(day=1) - dt.timedelta(days=1)
        months.append(m.strftime("%Y%m"))
    return months
last_full_months <- function(n) {
  ends <- seq(Sys.Date(), by = "-1 month", length.out = n + 1)[-1]
  rev(format(ends, "%Y%m"))
}

Notez last_full_months, non last_months. Le mois en cours est toujours incomplet, et l’inclure fait paraître toute tendance en baisse — le piège de la période partielle du cours sur les jointures, arrivant avec un système qui vous la servira volontiers.

Validez à la frontière

L’extraction est une lecture dans le système de quelqu’un d’autre, ce qui en fait exactement le raccord sur lequel le cours de nettoyage posait un contrat.

def check(tidy, expected_org_units, expected_periods):
    problems = []
    if tidy["ou"].nunique() != expected_org_units:
        problems.append(f"{tidy['ou'].nunique()} org units, expected {expected_org_units}")
    if tidy["pe"].nunique() != expected_periods:
        problems.append(f"{tidy['pe'].nunique()} periods, expected {expected_periods}")
    if tidy["value"].isna().any():
        problems.append("null values in the response")
    return problems
check <- function(tidy, expected_ou, expected_pe) {
  c(if (n_distinct(tidy$ou) != expected_ou) "unexpected org unit count",
    if (n_distinct(tidy$pe) != expected_pe) "unexpected period count")
}

Le plus précieux est le décompte d’unités d’organisation. Une extraction silencieusement plus petite est la défaillance d’API la plus fréquente en pratique — un changement de droits, une réorganisation, une unité fermée — et elle produit un total de district discrètement plus bas, sans rien dans le fichier pour dire pourquoi.

Comparez à l’extraction précédente

Deux dossiers datés rendent une comparaison possible, et c’est là que les quatre explications se séparent.

def compare(old_folder, new_folder, keys=("ou", "pe", "dx")):
    old = pd.read_csv(Path(old_folder, "tidy", "coverage_by_facility_period.csv"))
    new = pd.read_csv(Path(new_folder, "tidy", "coverage_by_facility_period.csv"))

    merged = old.merge(new, on=list(keys), how="outer",
                       suffixes=("_old", "_new"), indicator=True)
    changed = merged[merged["value_old"] != merged["value_new"]]
    return {
        "in_both": int((merged["_merge"] == "both").sum()),
        "new_only": int((merged["_merge"] == "right_only").sum()),
        "gone": int((merged["_merge"] == "left_only").sum()),
        "values_changed": len(changed),
    }
compare <- function(old, new) {
  full_join(old, new, by = c("ou", "pe", "dx"), suffix = c("_old", "_new")) |>
    summarise(changed = sum(value_old != value_new, na.rm = TRUE),
              gone = sum(is.na(value_new)), added = sum(is.na(value_old)))
}

Quatre nombres, et chacun désigne une cause différente. Des valeurs modifiées sur les mêmes clés, c’est une saisie tardive ou une révision. Des lignes disparues, c’est une réaffectation ou un changement de droits. Des lignes ajoutées, c’est du rapportage tardif qui arrive. Et un gros écart avec analytics_last_run qui a bougé entre-temps, ce sont les tables analytiques et non les données.

Faites-le chaque mois et conservez la sortie. C’est la série de constats du cours d’évaluation, bâtie sur une source que vous maîtrisez désormais.

Ce que cela remplace

Before:  a spreadsheet in an email, monthly, method unknown
After:   extracts/2026-07-28/, one command, manifest, raw responses,
         a diff against last month, and a figure you can defend in September

Le script fait environ quatre-vingts lignes. C’est le morceau de code au meilleur rendement de ce cours, et celui que la plupart des équipes n’écrivent jamais parce que l’export manuel marche très bien le jour même.

La suite

Vous avez un chiffre de routine que vous pouvez désigner. La dernière unité le pose à côté d’une estimation d’enquête portant sur la même chose — trois nombres pour la malnutrition aiguë, de 8,7 % à 14,9 % — et décompose l’écart selon les deux causes qui le produisent réellement.

Animer cette leçon

La leçon en diaporama, la prose étant reléguée dans les notes du présentateur plutôt que projetée. Produit à partir de cette page, dont il ne peut donc pas s'écarter.

Lancer le diaporamaLire les diapositives

Le PDF ne requiert aucun logiciel et se projette depuis n'importe quel poste. Le fichier PowerPoint est fait pour être modifié : appliquez la charte de votre organisation, retirez une section pour une séance plus courte, ou fusionnez deux leçons en atelier.