Dev Site — You are viewing the development build. Go to Main Site

  • English
  • Français
  1. 2. Assemblage et gestion des données
  2. 2.3 Données de cas de routine (DHIS2)
  3. Prétraitement des données DHIS2
  • Bibliothèque de code pour l'adaptation infranationale
    Version française
  • 1. Pour commencer
    • 1.1 À propos et comment nous contacter
    • 1.2 Pour tous les utilisateurs
    • 1.3 Pour l’équipe SNT
    • 1.4 Pour les analystes
    • 1.5 Acronymes et bibliothèque de ressources
    • 1.6 Produire des résultats de haute qualité
  • 2. Assemblage et gestion des données
    • 2.1 Utilisation des shapefiles
      • Aperçu des données spatiales
      • Utilisation et visualisation de base des shapefiles
      • Gestion et personnalisation des shapefiles
      • Fusion des shapefiles avec des données tabulaires
    • 2.2 Formations sanitaires
      • Correspondance approximative des noms entre jeux de données
      • Coordonnées des établissements de santé et données ponctuelles
    • 2.3 Données de cas de routine (DHIS2)
      • Détermination du statut actif et inactif
      • Data extraction from DHIS2
      • Prétraitement des données DHIS2
      • Méthodes de détection des données manquantes
      • Outlier correction
      • Considérations contextuelles
      • Taux de notification des établissements de santé
      • Quality control/checks
      • Outlier detection methods
      • Imputation of missing data
      • Final database
    • 2.4 Données du stock
      • lmis
    • 2.5 Données démographiques
      • Données démographiques nationales
      • Raster de population WorldPop
    • 2.6 Enquêtes nationales auprès des ménages
      • DHS Data Overview and Preparation
      • All-Cause Child Mortality
      • Wealth quintiles analysis
      • Extraction of ITN ownership, access, and usage
      • Extracion of prevalence data
      • Calculation of treatment-seeking data
    • 2.7 Données entomologiques
      • Données entomologiques
    • 2.8 Données climatiques et environnementales
      • Extraction de données climatiques et environnementales à partir de données raster
    • 2.9 Données modélisées
      • Generating spatial modeled estimates
      • Travailler avec les estimations modélisées géospatiales
      • Modeled Estimates of Entomological Indicators
      • Mortality estimates from IHME
    • 2.10 Données financières
  • 3. Analyse de la situation
    • 3.1 Revue des interventions historiques
      • Prise en charge des cas
      • Interventions de routine
      • Les campagnes de masse de moustiquaires
      • Les campagnes de chimioprévention
      • Autres interventions lutte antivectorielle
    • 3.2 Analyse des tendances
    • 3.3 Analyse des facteurs de risque
    • 3.4 Évaluation de l’impact des interventions
    • 3.5 Analyse des coûts
  • 4. Stratification
    • 4.1 Stratification épidémiologique
      • Aperçu de l’incidence et incidence brute
      • Ajustement de l’incidence 1 : noncomplétude du dépistage
      • Ajustement de l’incidence 2 : noncomplétude du rapportage
      • Ajustement de l’incidence 2 : recherche des soins
      • Incidence stratification
      • Stratification par prévalence et mortalité
      • Risk categorization
      • Combined risk categorization
      • Risk categorization REMOVE?
    • 4.2 Accès aux soins
    • 4.3 Saisonnalité
      • Définir les zones saisonnières
      • Durées de saisonnalité
    • 4.4 Microstratification urbaine
  • 5. Ciblage et priorisation des interventions
    • 5.1 Ciblage des interventions
    • 5.2 Priorisation
    • 5.3 Optimisation dans la limite des ressources

On this page

  • Aperçu
  • Considérations clés
    • Résolution des problèmes
    • Bonnes pratiques lors du travail avec des données de routine
    • À quoi doit ressembler un ensemble de données propre et bien structuré
  • Étape par étape
    • Étape 1 : Importer les paquets et les données
      • Étape 1.1 : Importer les paquets
      • Étape 1.2 : Importer les données
    • Étape 2 : Diagnostics après importation et fusion de plusieurs extraits DHIS2
      • Étape 2.1 : Vérifier que tous les groupes attendus sont présents après l’importation
      • Étape 2.2 : Vérifier les variables complètement manquantes
    • Étape 3 : Renommer les colonnes
    • Étape 4 : Standardiser les colonnes de dates
    • Étape 5 : Gérer les colonnes de localisation
      • Étape 5.1 : Harmoniser les noms administratifs
      • Étape 5.2 : Créer des identifiants uniques et des libellés de localisation
    • Étape 6 : Calculer les variables
      • Étape 6.1 : Calculer les totaux des indicateurs et les nouvelles variables
      • Étape 6.2 : Contrôle qualité des totaux des indicateurs
      • Étape 6.3 : Exporter les lignes avec des totaux incohérents
      • Étape 6.4 : Ajouter la spécification IPD/OPD
    • Étape 7 : Finaliser les données
      • Étape 7.1 : Résoudre les enregistrements en double établissement-mois avec des données différentes
      • Étape 7.2 : Générer le dictionnaire de données final
      • Étape 7.3 : Organiser l’ordre final des colonnes
    • Étape 8 : Agréger et sauvegarder les données
      • Étape 8.1 : Sauvegarder les données au niveau des établissements de santé
      • Étape 8.2 : Agréger et sauvegarder les données à chaque niveau d’unité administrative
  • Résumé
  • Code complet
  1. 2. Assemblage et gestion des données
  2. 2.3 Données de cas de routine (DHIS2)
  3. Prétraitement des données DHIS2

Prétraitement des données DHIS2

Débutant

Aperçu

DHIS2 est une source de données centrale pour la surveillance de routine du paludisme, qui enregistre des indicateurs tels que le nombre de consultations ambulatoires, les tests de diagnostic et la prise en charge thérapeutique. Cependant, les exports bruts de DHIS2 peuvent ne pas être prêts pour une analyse SNT s’ils présentent des problèmes structurels ou de qualité : données désordonnées, noms de colonnes incohérents ou illisibles, valeurs manquantes, noms d’établissements non harmonisés, doublons et formats variables selon les mois ou les localisations. Pour rendre ces données utilisables pour le SNT, elles doivent être nettoyées, restructurées et alignées sur une structure spatiale et temporelle qui soutient les étapes ultérieures de l’analyse : identification des établissements de santé actifs et inactifs, calcul des taux de notification, gestion des valeurs aberrantes, calcul de l’incidence du paludisme, etc.

Dans cette section, nous parcourons les étapes de prétraitement des données mensuelles de paludisme provenant de DHIS2 pour les rendre prêtes pour le SNT, en utilisant des données d’exemple de Sierra Leone. Cela comprend le nettoyage et la standardisation des données au niveau des établissements, l’analyse et la standardisation des formats de date, l’harmonisation des noms administratifs avec les shapefiles de référence pour les jointures spatiales, le calcul des indicateurs dérivés et l’agrégation aux niveaux administratifs appropriés pour produire des ensembles de données structurés.

Les données DHIS2 avec lesquelles nous travaillons peuvent nécessiter certaines ou toutes les étapes spécifiques détaillées ci-dessous, ou des étapes supplémentaires peuvent être nécessaires pour préparer les données à l’analyse.

NoteObjectifs
  • Importer et combiner les fichiers de données DHIS2 provenant de sources multiples
  • Vérifier l’exhaustivité des données après importation (vérifier que tous les groupes sont présents, identifier les variables manquantes)
  • Renommer les colonnes à l’aide d’un dictionnaire de données pour une dénomination standardisée
  • Standardiser les colonnes de dates (analyser les dates, créer les champs année/mois/yearmon)
  • Harmoniser les noms administratifs avec les shapefiles de référence pour les jointures spatiales
  • Créer des identifiants uniques (UID) pour les établissements et les enregistrements
  • Calculer les totaux des indicateurs et les variables dérivées (par exemple, les cas présumés)
  • Effectuer des contrôles qualité sur les indicateurs calculés
  • Identifier et résoudre les doublons au niveau établissement-mois
  • Agréger les données au niveau administratif sélectionné
  • Exporter les ensembles de données nettoyés au niveau établissement et au niveau agrégé

Considérations clés

Résolution des problèmes

Les données de surveillance de routine peuvent être plus difficiles à traiter que d’autres sources de données plus standardisées. Il faut s’attendre à rencontrer et à résoudre des problèmes de données tout au long du processus de prétraitement des données DHIS2. Nous devrons probablement :

  • Analyser les patterns inattendus dans les données
  • Prendre des décisions sur la façon de gérer les incohérences
  • Collaborer avec l’équipe SNT sur les problèmes spécifiques au pays
  • Développer des solutions personnalisées pour des problèmes non couverts ici

Les approches de résolution de problèmes présentées dans ce guide constituent un cadre de départ, mais le nettoyage des données réelles nécessite souvent une résolution itérative des problèmes et une expertise locale. Bien que ce guide aborde les difficultés rencontrées par d’autres lors de la préparation des données DHIS2 pour l’analyse, chaque pays est différent. Le contexte spécifique peut présenter des problèmes uniques qui ne sont pas encore couverts dans la bibliothèque de code. Nous encourageons la persévérance et la créativité si nécessaire, et recommandons de consulter d’autres personnes si un problème ne peut pas être résolu de manière indépendante.

Si nous rencontrons un nouveau défi lors du prétraitement des données DHIS2 et souhaitons que du nouveau contenu soit ajouté à la bibliothèque de code, veuillez remplir notre formulaire de retour d’information.

Bonnes pratiques lors du travail avec des données de routine

1. Comprendre le contexte du système

  • Flux de notification : savoir comment les données passent des établissements vers DHIS2 (saisie papier ou directe, règles d’agrégation).
  • Définitions des indicateurs : confirmer les définitions de cas et les inclusions

2. Trianguler de manière stratégique

Croiser avec :

  • Des références externes (par exemple, rapports HMIS, données d’enquête)
  • Des systèmes parallèles (par exemple, données de gestion des stocks pour les kits de test)
  • Des patterns temporels (comparer les tendances saison sèche et saison des pluies)

3. Documenter le processus

Tenir un journal de résolution des problèmes comprenant :

  • Les problèmes rencontrés
  • Les sources de données consultées
  • Les décisions prises (avec justification)

Décalages temporels : les établissements peuvent notifier en retard, notamment après les jours fériés ou les pannes de système.

À quoi doit ressembler un ensemble de données propre et bien structuré

Nos données peuvent se présenter dans un format différent de l’exemple de Sierra Leone ci-dessous. Que nous puissions utiliser le bloc de code fourni, ou des parties de celui-ci, pour importer les données, veuillez vous assurer que les données, après importation, contiennent les informations suivantes :

  • Une colonne avec la date (année et mois) du rapport
  • Une colonne avec le nom de l’établissement de santé
  • Plusieurs colonnes avec les unités administratives parentes - au minimum nous aurons besoin du nom de l’unité administrative pour l’analyse SNT (par exemple, la chefferie pour la Sierra Leone)
  • Les colonnes des indicateurs de données

Voici un exemple de ce à quoi les données devraient ressembler après importation :

adm0 adm1 adm2 adm3 hf date month year allout_u5 allout_ov5 conf_u5 conf_5_14
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-01 01 2021 44 48 36 22
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-02 02 2021 37 27 20 30
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-03 03 2021 44 42 18 22
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-04 04 2021 28 23 30 30
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-05 05 2021 138 141 72 40
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-06 06 2021 127 82 59 16
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-07 07 2021 130 175 86 24
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-08 08 2021 144 203 93 17
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-09 09 2021 159 159 78 18
SIERRA LEONE BO DISTRICT BO CITY COUNCIL BO CITY AETHEL CHP 2021-10 10 2021 88 128 6 80

Étape par étape

Dans cette section, nous parcourons les étapes clés de prétraitement nécessaires pour préparer les données de paludisme DHIS2 pour l’analyse SNT, en utilisant des données DHIS2 d’exemple de Sierra Leone. L’accent est mis ici sur le nettoyage, la restructuration et l’agrégation des données au niveau établissement pour produire des sorties structurées à la fois au niveau établissement et au niveau administratif.

Chaque étape est conçue pour nous guider à travers le processus de manière claire. Suivez les notes dans le code, en particulier là où des modifications sont requises (par exemple, les noms de colonnes ou les niveaux administratifs). L’objectif est de rendre les données prêtes pour l’analyse sans rompre les liens avec les unités spatiales ou les structures de notification.

N’oubliez pas d’adapter ce processus au contexte spécifique du pays dans lequel nous travaillons.

Pour passer directement à l’explication étape par étape, accédez au code complet en bas de cette page.

Étape 1 : Importer les paquets et les données

Nous utiliserons les données de paludisme DHIS2 de Sierra Leone comme exemple de travail. Les noms de variables ou les structures de base de données peuvent différer selon les pays. Examinez et mettez à jour les noms de variables pour correspondre à la configuration DHIS2 du pays.

Nous commençons par installer et charger les paquets nécessaires pour notre prétraitement, puis nous importons les données.

Pour utiliser Python, consultez la section Démarrage pour l’installation des paquets.

Étape 1.1 : Importer les paquets

  • R
  • Python
# installer pacman uniquement s'il n'est pas déjà installé
if (!requireNamespace("pacman", quietly = TRUE)) {
  install.packages("pacman")
}

# installer ou charger les paquets pertinents
pacman::p_load(
  tidyverse, # manipulation, restructuration et visualisation des données
  rio,       # importation/exportation de plusieurs types de fichiers
  DT,        # aperçu interactif des tableaux
  here,      # chemins de fichiers relatifs au projet
  readxl,    # lire les fichiers excel
  writexl,   # écrire les fichiers excel
  knitr      # rendu du code dans quarto/rmarkdown
)
import pandas as pd  # charger les outils de données principaux
from pathlib import Path  # gérer les chemins du système de fichiers
from pyprojroot import here  # construire des chemins relatifs au projet
import os  # utilitaires de chemins optionnels
import locale  # pour définir la locale de langue
import geopandas as gpd  # pour importer les shapefiles
import xxhash  # pour le hachage
import pyarrow.parquet as pq  # pour exporter les fichiers parquet
import pyarrow as pa  # pour exporter les fichiers parquet

Étape 1.2 : Importer les données

Le bloc de code ci-dessous définit une fonction auxiliaire pour importer différents types de données, par exemple des données Excel ou CSV. Si l’extraction DHIS2 a produit plusieurs fichiers, cette fonction les fusionnera en un seul ensemble de données. Nous l’utilisons ensuite pour lire notre ou nos fichiers de données.

Lors de l’appel de la fonction d’importation des données DHIS2, nous lisons les fichiers depuis un dossier à l’aide du paquet here. Le paquet here construit les chemins à partir de la racine du projet, ce qui maintient le code portable et fiable.

ImportantInspecter vos données avant de continuer

Avant d’exécuter tout code, prenez un moment pour inspecter visuellement les données. Cela est particulièrement important lorsque l’on travaille avec des données DHIS2 ou d’autres sources de santé de routine. Comme expliqué dans la section Structures de données de la page Démarrage : Pour les analystes, les données doivent suivre les principes des données ordonnées (tidy data) : chaque colonne représente une variable, chaque ligne une observation et chaque cellule contient une seule valeur.

Pour vérifier cela, ouvrez directement le fichier Excel ou CSV et examinez :

  • En-têtes : Les noms de colonnes sont-ils dans la première ligne ? Sont-ils clairs et cohérents ?
  • Colonnes et lignes :
    • Chaque colonne doit représenter une variable (par exemple, health_facility, month, confirmed_cases).
    • Chaque ligne doit représenter une seule observation (par exemple, un établissement-mois).
  • Cellules : Chaque cellule doit contenir une seule valeur (non fusionnée ou groupée). Évitez les cellules contenant plusieurs valeurs (par exemple, 10/5 pour testé/positif).
  • Lignes supplémentaires : Supprimez toutes les lignes du haut utilisées pour les titres, les notes ou les en-têtes fusionnés.

Si l’ensemble de données est déjà ordonné, nous sommes prêts à continuer. Sinon, nettoyez-le avant de l’utiliser. Cela peut être fait :

  • Dans Excel, en modifiant une copie du fichier et en supprimant tout formatage supplémentaire, puis en enregistrant la nouvelle copie pour l’importer et l’utiliser dans les étapes suivantes.

  • Ou dans R, en utilisant des paquets tels que :

    • rio::import(file, skip = X) pour ignorer X lignes d’en-tête supplémentaires
    • tidyr::pivot_longer() pour restructurer les données larges en format long
    • dplyr::mutate() et separate() pour diviser les valeurs combinées en colonnes séparées
  • Ou dans Python, en utilisant :

    • pandas.read_excel(file, skiprows=X) pour ignorer X lignes d’en-tête indésirables
    • pandas.melt() pour restructurer les données larges en format long
    • pandas.Series.str.split() et pandas.assign() pour diviser et créer de nouvelles colonnes

Pour des problèmes de formatage plus complexes, comme les en-têtes multi-lignes ou fusionnés, consultez cet excellent guide étape par étape : Tidying Multi-Header Excel Data with R de Paul Campbell (2019).

Bien faire cela dès le début permettra de gagner du temps et de réduire la confusion dans la suite du workflow, car toutes les étapes suivantes supposent que les données sont déjà dans un format ordonné et prêtes pour l’analyse.

  • R
  • Python

Définir d’abord les chemins vers les données DHIS2.

# définir le chemin vers les données
core_routine_path <- here::here(
  "1.1.2_epidemiology",
  "1.1.2a_routine_surveillance",
  "raw"
)

# définir le chemin complet DHIS2
path_to_dhis2 <- here::here(core_routine_path, "sle_dhis2_2015_2022.xlsx")

Si l’ensemble de données est un seul tableau Excel, il peut être importé directement avec rio::import, qui lit plusieurs formats de fichiers. Cette approche suppose que tous les indicateurs, localisations et périodes sont stockés dans un seul fichier.

# lire les données
dhis2_df <- rio::import(file = path_to_dhis2)

Dans les cas où les données sont réparties sur plusieurs onglets, par exemple divisées en onglets annuels, utilisez l’approche suivante.

dhis2_df <- rio::import_list(
  file = path_to_dhis2,
  rbind = TRUE,        # empiler tous les onglets en un seul tableau
  rbind_label = "year" # nom de l'onglet stocké dans la colonne 'year'
)

Les exports DHIS2 peuvent être difficiles à gérer lorsque les données couvrent de nombreux établissements ou années. La taille des fichiers et l’utilisation de la mémoire augmentent lors de l’empilement de nombreux onglets ou fichiers. Pour cette raison, les données peuvent être exportées dans plusieurs fichiers ou feuilles. Si les données sont dans une archive compressée, ou divisées en plusieurs fichiers du même format, importez-les ensemble avec rio::import_list(). Cela suppose que tous les fichiers partagent la même structure, les mêmes noms de colonnes et les mêmes types de données. On suppose également que les feuilles ne contiennent que des données rectangulaires et que l’extension de fichier est cohérente pour que rio puisse détecter le format. Des exemples incluent des fichiers nommés par district ou unité administrative exportés par plages d’années, tels que Bombali_District_2015-2023.xls ou Kambia_District_2015-2023.xls.

dhis2_df <- rio::import_list(
  file = list.files(
    path = core_routine_path,
    # les extensions de fichiers
    pattern = "\\.(xls)$",
    full.names = TRUE
  ),
  # empiler tous les onglets/fichiers en un seul tableau
  rbind = TRUE,
  # nom de la source stocké dans la colonne 'sheet_admin'
  rbind_label = "sheet_admin",
  # remplir les colonnes manquantes avec NA lors de l'empilement
  rbind_fill = TRUE
)

# vérifier les données
dhis2_df |>
  dplyr::select(1:20) |>
  dplyr::glimpse()
NoteSortie
Rows: 184,056
Columns: 10
$ orgunitlevel1                                  <chr> "Sierra Leone", "Sierra…
$ orgunitlevel2                                  <chr> "Bo District", "Bo Dist…
$ orgunitlevel3                                  <chr> "Bo City Council", "Bo …
$ orgunitlevel4                                  <chr> "Bo City", "Bo City", "…
$ orgunitlevel5                                  <chr> "Aethel CHP", "Aethel C…
$ organisationunitname                           <chr> "Aethel CHP", "Aethel C…
$ periodname                                     <chr> "January 2015", "Februa…
$ `OPD (New and follow-up curative) 0-59m_X`     <dbl> NA, NA, NA, NA, NA, NA,…
$ `OPD (New and follow-up curative) 5+y_X`       <dbl> NA, NA, NA, NA, NA, NA,…
$ `Admission - Child with malaria 0-59 months_X` <dbl> NA, NA, NA, NA, NA, NA,…

Pour adapter le code :

  • Ligne 3 : Définissez core_routine_path vers votre dossier cible.
  • Ligne 4 : Modifiez le pattern pour correspondre à votre extension (xls, xlsx, csv).
  • Ligne 8 : Définissez rbind_label = 'sheet_admin' pour stocker l’onglet ou le fichier source, comme les unités administratives ; utilisez year pour les onglets/fichiers annuels.

Définir d’abord les chemins vers les données DHIS2.

# définir le chemin vers les données dhis2
core_routine_path = Path(
    here("1.1.2_epidemiology/1.1.2a_routine_surveillance/raw")
)

# définir le chemin complet dhis2
path_to_dhis2 = core_routine_path / "sle_dhis2_2015_2022.xlsx"

Si l’ensemble de données est un seul fichier Excel, importez-le directement avec pd.read_excel. Cela suppose que tous les indicateurs, localisations et périodes sont dans un seul tableau.

# lire les données
dhis2_df = pd.read_excel(path_to_dhis2)

Dans les cas où les données sont stockées dans plusieurs onglets, comme des divisions annuelles, chargez tous les onglets et empilez-les en un seul tableau. Ajoutez une colonne pour conserver la source de l’onglet, comme l’année.

# importer tous les onglets et les empiler en un seul tableau
dhis2_df = pd.concat(
    [
        pd.read_excel(path_to_dhis2, sheet_name=s).assign(year=s)
        for s in pd.ExcelFile(path_to_dhis2).sheet_names
    ],
    ignore_index=True,
)
dhis2_df.head(10).style

Les exports cumulatifs sur de nombreux établissements ou années sont souvent répartis sur plusieurs fichiers. La taille des fichiers et l’utilisation de la mémoire augmentent lors de l’empilement. On suppose que tous les fichiers et feuilles partagent les mêmes colonnes et types. Utilisez pandas pour détecter les feuilles, lire chaque fichier, les empiler et stocker la source du fichier ou de l’onglet dans une colonne de libellé, comme sheet_admin. Des exemples incluent des fichiers nommés par district ou unité administrative exportés par plages d’années, tels que Bombali_District_2015-2023.xls ou Kambia_District_2015-2023.xls.

# définir l'extension de fichier
extension = "xls"

# trouver tous les fichiers avec l'extension spécifiée dans le répertoire
files = list(core_routine_path.glob(f"*.{extension}"))

# initialiser le dataframe
dhis2_df = pd.DataFrame()

# itérer sur les fichiers, concaténer en un seul dataframe
for file in files:
    temp = pd.read_excel(file, sheet_name="Sheet1")
    dhis2_df = pd.concat([dhis2_df, temp])

# inspecter les données
dhis2_df.head(10)
  orgunitlevel1  ... Malaria treated with ACT >24 hours 15+y_X
0  Sierra Leone  ...                                       NaN
1  Sierra Leone  ...                                       1.0
2  Sierra Leone  ...                                       NaN
3  Sierra Leone  ...                                       2.0
4  Sierra Leone  ...                                       7.0
5  Sierra Leone  ...                                       5.0
6  Sierra Leone  ...                                       5.0
7  Sierra Leone  ...                                       4.0
8  Sierra Leone  ...                                       7.0
9  Sierra Leone  ...                                       3.0

[10 rows x 56 columns]

Pour adapter le code :

  • Ligne 1 : Mettez à jour l’extension vers xls, xlsx ou csv.
  • Ligne 4 : Définissez core_routine_path vers votre dossier de données.
  • Ligne 11 : Supprimez sheet_name="Sheet1" si vos fichiers n’utilisent pas un nom d’onglet fixe.

Étape 2 : Diagnostics après importation et fusion de plusieurs extraits DHIS2

Avant tout nettoyage ou construction d’indicateur, confirmer que l’ensemble de données fusionné contient tous les groupes et composantes des exports DHIS2 originaux.

Étape 2.1 : Vérifier que tous les groupes attendus sont présents après l’importation

Les exports DHIS2 sont souvent répartis sur plusieurs fichiers ou feuilles. La structure varie selon les pays. Les fichiers peuvent être séparés par unités administratives, par années, ou par d’autres regroupements définis par DHIS2 tels que des lots mensuels, des régions infranationales, des circonscriptions, des districts assignés par partenaires, ou des groupes DHIS2 personnalisés tels que les totaux ambulatoires. Après avoir importé et fusionné ces extraits en un seul ensemble de données, nous devons effectuer un ensemble de vérifications pour confirmer que tous les composantes ont été incluses correctement et que rien n’a été omis ou dupliqué.

L’exemple ci-dessous utilise adm2 (actuellement orgunitlevel3 dans nos données) comme variable de regroupement, mais remplacez adm2 par year ou tout autre regroupement utilisé dans l’export.

  • R
  • Python
# vérifier les unités administratives dans nos données
dhis2_df |>
  dplyr::distinct(orgunitlevel3)
                         orgunitlevel3
1                      Bo City Council
2                  Bo District Council
3             Bombali District Council
4                  Makeni City Council
5              Bonthe District Council
6             Bonthe Municipal Council
7              Falaba District Council
8            Kailahun District Council
9              Kambia District Council
10             Karene District Council
11                 Kenema City Council
12             Kenema District Council
13          Koinadugu District Council
14               Kono District Council
15     Koidu New Sembehun City Council
16            Moyamba District Council
17          Port Loko District Council
18              Port Loko City Council
19            Pujehun District Council
20          Tonkolili District Council
21 Western Area Rural District Council
22               Freetown City Council

Pour adapter le code :

  • Ligne 3 : Remplacez orgunitlevel3 par le regroupement utilisé dans votre export (par exemple, adm2, adm1, year, circonscription, district ou tout regroupement DHIS2 personnalisé).
# vérifier les unités administratives dans nos données
dhis2_df[["orgunitlevel3"]].drop_duplicates().reset_index(drop=True)
                          orgunitlevel3
0              Moyamba District Council
1              Bombali District Council
2                   Makeni City Council
3               Kambia District Council
4            Tonkolili District Council
5            Port Loko District Council
6                Port Loko City Council
7               Karene District Council
8            Koinadugu District Council
9                 Kono District Council
10      Koidu New Sembehun City Council
11  Western Area Rural District Council
12            Kailahun District Council
13             Pujehun District Council
14                  Kenema City Council
15              Kenema District Council
16              Bonthe District Council
17             Bonthe Municipal Council
18                      Bo City Council
19                  Bo District Council
20              Falaba District Council
21                Freetown City Council

Pour adapter le code :

  • Ligne 2 : Remplacez orgunitlevel3 par le regroupement utilisé dans votre export (par exemple, adm2, adm1, year, circonscription, district ou tout regroupement DHIS2 personnalisé).

Ce diagnostic vérifie que tous les groupes attendus des extraits apparaissent dans l’ensemble de données fusionné, que les fichiers aient été divisés par unités administratives, années, circonscriptions ou tout autre regroupement DHIS2.

Si nous avons importé 15 fichiers ou feuilles, nous devrions voir 15 groupes distincts. Un nombre inférieur de groupes indique généralement un fichier manquant, des noms d’onglets incohérents ou des différences d’orthographe. Le nombre de lignes varie entre les groupes, donc concentrez-vous sur la présence du groupe, pas sur sa taille.

Si des groupes manquent, vérifiez que tous les fichiers sources sont présents, que les noms de feuilles suivent un pattern cohérent et que les noms de groupes correspondent entre les extraits. Après avoir corrigé les problèmes et réimporté, continuez avec les étapes de prétraitement.

Étape 2.2 : Vérifier les variables complètement manquantes

Certaines variables, telles que le nom de l’établissement de santé, la date de notification et l’unité administrative, ne doivent pas être manquantes. Les lignes avec des valeurs manquantes dans ces champs ne peuvent pas être utilisées directement et doivent être examinées avec le point focal DHIS2.

Les colonnes entièrement manquantes doivent être examinées avant de continuer. Elles peuvent refléter des changements dans les indicateurs DHIS2, des fichiers manquants ou des indicateurs qui n’ont jamais été collectés. Vérifiez avec l’équipe SNT si la variable doit être conservée, renommée ou supprimée. Résoudre cela tôt prévient les problèmes dans toutes les étapes SNT ultérieures.

Le code ci-dessous affiche le nombre de valeurs manquantes pour chaque variable et met en évidence toute valeur manquante dans les identifiants critiques nécessaires à l’analyse SNT.

  • R
  • Python
# identifier les valeurs entièrement manquantes par colonne
dhis2_df |>
  dplyr::summarise(
    dplyr::across(
      dplyr::everything(),
      ~ mean(is.na(.x)) * 100
    )
  ) |>
  tidyr::pivot_longer(
    everything(),
    names_to = "variable",
    values_to = "pct_missing"
  ) |>
  dplyr::filter(pct_missing == 100)
# A tibble: 0 × 2
# ℹ 2 variables: variable <chr>, pct_missing <dbl>
# calculer le pourcentage de valeurs manquantes pour chaque colonne
pct_missing = dhis2_df.isna().mean() * 100

# convertir au format long
missing_table = (
    pct_missing.reset_index()
    .rename(columns={"index": "variable", 0: "pct_missing"})
)

# filtrer les colonnes entièrement manquantes
missing_table[missing_table["pct_missing"] == 100]
Empty DataFrame
Columns: [variable, pct_missing]
Index: []

Ne pas modifier ce code.

Étape 3 : Renommer les colonnes

Une manière pratique de standardiser et renommer les noms de colonnes DHIS2 est d’utiliser un dictionnaire de données. Le dictionnaire contient au moins deux colonnes : 1. les noms de colonnes DHIS2 originaux tels qu’ils apparaissent dans l’export brut 2. les noms de colonnes SNT correspondants que nous préférons utiliser.

Les noms SNT sont généralement plus courts et plus cohérents, ce qui aide à maintenir une structure claire tout au long du pipeline analytique.

TipDictionnaire de données
indicator_label snt_var
orgunitlevel1 adm0
orgunitlevel2 adm1
orgunitlevel3 adm2
orgunitlevel4 adm3
organisationunitname hf
OPD (New and follow-up curative) 0-59m_X allout_u5
OPD (New and follow-up curative) 5+y_X allout_ov5
Admission - Child with malaria 0-59 months_X maladm_u5
Admission - Child with malaria 5-14 years_X maladm_5_14
Admission - Malaria 15+ years_X maladm_ov15
Child death - Malaria 1-59m_X maldth_1_59m
Child death - Malaria 10-14y_X maldth_10_14
Child death - Malaria 5-9y_X maldth_5_9
Death malaria 15+ years Female maldth_fem_ov15
Death malaria 15+ years Male maldth_mal_ov15
Separation - Child with malaria 0-59 months_X Death maldth_u5
Separation - Child with malaria 5-14 years_X Death maldth_5_14
Separation - Malaria 15+ years_X Death maldth_ov15
Fever case - suspected Malaria 0-59m_X susp_u5_hf
Fever case - suspected Malaria 5-14y_X susp_5_14_hf
Fever case - suspected Malaria 15+y_X susp_ov15_hf
Fever case in community (Suspected Malaria) 0-59m_X susp_u5_com
Fever case in community (Suspected Malaria) 5-14y_X susp_5_14_com
Fever case in community (Suspected Malaria) 15+y_X susp_ov15_com
Fever case in community tested for Malaria (RDT) - Negative 0-59m_X tes_neg_rdt_u5_com
Fever case in community tested for Malaria (RDT) - Positive 0-59m_X tes_pos_rdt_u5_com
Fever case in community tested for Malaria (RDT) - Negative 5-14y_X tes_neg_rdt_5_14_com
Fever case in community tested for Malaria (RDT) - Positive 5-14y_X tes_pos_rdt_5_14_com
Fever case in community tested for Malaria (RDT) - Negative 15+y_X tes_neg_rdt_ov15_com
Fever case in community tested for Malaria (RDT) - Positive 15+y_X tes_pos_rdt_ov15_com
Fever case tested for Malaria (Microscopy) - Negative 0-59m_X test_neg_mic_u5_hf
Fever case tested for Malaria (Microscopy) - Positive 0-59m_X test_pos_mic_u5_hf
Fever case tested for Malaria (Microscopy) - Negative 5-14y_X test_neg_mic_5_14_hf
Fever case tested for Malaria (Microscopy) - Positive 5-14y_X test_pos_mic_5_14_hf
Fever case tested for Malaria (Microscopy) - Negative 15+y_X test_neg_mic_ov15_hf
Fever case tested for Malaria (Microscopy) - Positive 15+y_X test_pos_mic_ov15_hf
Fever case tested for Malaria (RDT) - Negative 0-59m_X tes_neg_rdt_u5_hf
Fever case tested for Malaria (RDT) - Positive 0-59m_X tes_pos_rdt_u5_hf
Fever case tested for Malaria (RDT) - Negative 5-14y_X tes_neg_rdt_5_14_hf
Fever case tested for Malaria (RDT) - Positive 5-14y_X tes_pos_rdt_5_14_hf
Fever case tested for Malaria (RDT) - Negative 15+y_X tes_neg_rdt_ov15_hf
Fever case tested for Malaria (RDT) - Positive 15+y_X tes_pos_rdt_ov15_hf
Malaria treated in community with ACT <24 hours 0-59m_X maltreat_u24_u5_com
Malaria treated in community with ACT >24 hours 0-59m_X maltreat_ov24_u5_com
Malaria treated in community with ACT <24 hours 5-14y_X maltreat_u24_5_14_com
Malaria treated in community with ACT >24 hours 5-14y_X maltreat_ov24_5_14_com
Malaria treated in community with ACT <24 hours 15+y_X maltreat_u24_ov15_com
Malaria treated in community with ACT >24 hours 15+y_X maltreat_ov24_ov15_com
Malaria treated with ACT <24 hours 0-59m_X maltreat_u24_u5_hf
Malaria treated with ACT >24 hours 5-14y_X maltreat_ov24_5_14_hf
Malaria treated with ACT <24 hours 5-14y_X maltreat_u24_5_14_hf
Malaria treated with ACT >24 hours 15+y_X maltreat_ov24_ov15_hf
Malaria treated with ACT >24 hours 0-59m_X maltreat_ov24_u5_hf
Malaria treated with ACT <24 hours 15+y_X maltreat_u24_ov15_hf

Conserver ce dictionnaire dans un fichier externe (par exemple, Excel ou CSV) permet à d’autres de le consulter et de le mettre à jour, et les équipes nationales peuvent confirmer directement la dénomination des indicateurs. Cela réduit également le risque d’erreurs liées à des règles de renommage codées en dur, qui peuvent se rompre lorsque les formats de colonnes changent ou lorsque les scripts sont modifiés.

L’utilisation d’un dictionnaire crée une référence commune pour le renommage DHIS2 et aide à maintenir une approche de dénomination cohérente tout au long du workflow SNT.

ImportantConsulter l’équipe SNT

Examinez les définitions des variables pour le pays afin de mieux comprendre et analyser les données de manière appropriée. Envisagez d’examiner des questions telles que :

  • Les admissions sont-elles incluses dans les consultations ambulatoires ?
  • Les données sur les femmes enceintes sont-elles incluses dans les données relatives aux adultes ?
  • Y a-t-il un double comptage entre les résultats RDT et microscopie ? Si oui, est-il approprié de n’utiliser que les résultats RDT ?
  • Les données du secteur privé sont-elles incluses ici ? Si oui, quel pourcentage du secteur privé déclare dans DHIS2 ?
  • Les données des agents de santé communautaires sont-elles incluses dans les données de leur établissement de santé assigné ou les données sont-elles séparées ?
  • Des variables ont-elles été incluses ou adaptées au fil des années ? Si oui, comment doivent-elles être traitées tout au long de la série temporelle ?

Confirmez toujours les définitions des variables avec l’équipe SNT.

  • R
  • Python
# importer le dictionnaire de données
data_dict <- rio::import(
  here::here(core_routine_path, "sle_dhis2_data_dict.xlsx")
)
# créer le vecteur de renommage : objet = anciens noms, noms = nouveaux noms
rename_vector <- stats::setNames(
  object = data_dict$indicator_label,
  nm = data_dict$snt_var
)

# renommer les données DHIS2
dhis2_df <- dhis2_df |>
  dplyr::rename(
    !!!rename_vector
  ) |>
  # on supprime orgunitlevel5 car c'est identique à hf
  dplyr::select(-orgunitlevel5)

# vérifier les noms
colnames(dhis2_df)
NoteSortie
 [1] "adm0"                   "adm1"                   "adm2"                  
 [4] "adm3"                   "hf"                     "periodname"            
 [7] "allout_u5"              "allout_ov5"             "maladm_u5"             
[10] "maladm_5_14"            "maladm_ov15"            "maldth_u5"             
[13] "maldth_5_14"            "maldth_ov15"            "susp_u5_hf"            
[16] "susp_5_14_hf"           "susp_ov15_hf"           "susp_u5_com"           
[19] "susp_5_14_com"          "susp_ov15_com"          "maldth_fem_ov15"       
[22] "maldth_mal_ov15"        "maldth_1_59m"           "maldth_10_14"          
[25] "maldth_5_9"             "tes_neg_rdt_u5_com"     "tes_pos_rdt_u5_com"    
[28] "tes_neg_rdt_5_14_com"   "tes_pos_rdt_5_14_com"   "tes_neg_rdt_ov15_com"  
[31] "tes_pos_rdt_ov15_com"   "test_neg_mic_u5_hf"     "test_pos_mic_u5_hf"    
[34] "test_neg_mic_5_14_hf"   "test_pos_mic_5_14_hf"   "test_neg_mic_ov15_hf"  
[37] "test_pos_mic_ov15_hf"   "tes_neg_rdt_u5_hf"      "tes_pos_rdt_u5_hf"     
[40] "tes_neg_rdt_5_14_hf"    "tes_pos_rdt_5_14_hf"    "tes_neg_rdt_ov15_hf"   
[43] "tes_pos_rdt_ov15_hf"    "maltreat_u24_u5_com"    "maltreat_ov24_u5_com"  
[46] "maltreat_u24_5_14_com"  "maltreat_ov24_5_14_com" "maltreat_u24_ov15_com" 
[49] "maltreat_ov24_ov15_com" "maltreat_u24_u5_hf"     "maltreat_ov24_u5_hf"   
[52] "maltreat_u24_5_14_hf"   "maltreat_ov24_5_14_hf"  "maltreat_u24_ov15_hf"  
[55] "maltreat_ov24_ov15_hf"  "sheet_admin"           

Pour adapter le code :

  • Ligne 3 : Pointez vers votre propre fichier et dossier de dictionnaire.
  • Lignes 7–8 : Mettez à jour les noms de colonnes du dictionnaire pour les variables originales et SNT.
  • Ligne 12 : Remplacez dhis2_df par votre ensemble de données DHIS2.
  • Ligne 17 : Supprimez ou changez orgunitlevel5 si cette colonne n’est pas présente.
# importer le dictionnaire de données
dict_path = os.path.join(core_routine_path, "sle_dhis2_data_dict.xlsx")
data_dict = pd.read_excel(dict_path)

# construire le dictionnaire de renommage : {ancien_nom: nouveau_nom}
rename_dict = dict(zip(data_dict["indicator_label"], data_dict["snt_var"]))

# renommer les données dhis2
dhis2_df = dhis2_df.rename(columns=rename_dict).drop(
    columns=["orgunitlevel5"], errors="ignore"
)

# afficher les noms de colonnes mis à jour
dhis2_df.columns.tolist()
NoteSortie
['adm0', 'adm1', 'adm2', 'adm3', 'hf', 'periodname', 'allout_u5', 'allout_ov5', 'maladm_u5', 'maladm_5_14', 'maladm_ov15', 'maldth_u5', 'maldth_5_14', 'maldth_ov15', 'susp_u5_hf', 'susp_5_14_hf', 'susp_ov15_hf', 'susp_u5_com', 'susp_5_14_com', 'susp_ov15_com', 'maldth_fem_ov15', 'maldth_mal_ov15', 'maldth_1_59m', 'maldth_10_14', 'maldth_5_9', 'tes_neg_rdt_u5_com', 'tes_pos_rdt_u5_com', 'tes_neg_rdt_5_14_com', 'tes_pos_rdt_5_14_com', 'tes_neg_rdt_ov15_com', 'tes_pos_rdt_ov15_com', 'test_neg_mic_u5_hf', 'test_pos_mic_u5_hf', 'test_neg_mic_5_14_hf', 'test_pos_mic_5_14_hf', 'test_neg_mic_ov15_hf', 'test_pos_mic_ov15_hf', 'tes_neg_rdt_u5_hf', 'tes_pos_rdt_u5_hf', 'tes_neg_rdt_5_14_hf', 'tes_pos_rdt_5_14_hf', 'tes_neg_rdt_ov15_hf', 'tes_pos_rdt_ov15_hf', 'maltreat_u24_u5_com', 'maltreat_ov24_u5_com', 'maltreat_u24_5_14_com', 'maltreat_ov24_5_14_com', 'maltreat_u24_ov15_com', 'maltreat_ov24_ov15_com', 'maltreat_u24_u5_hf', 'maltreat_ov24_u5_hf', 'maltreat_u24_5_14_hf', 'maltreat_ov24_5_14_hf', 'maltreat_u24_ov15_hf', 'maltreat_ov24_ov15_hf']

Pour adapter le code :

  • Ligne 2 : Mettez à jour le chemin du fichier si votre dictionnaire est stocké ailleurs.
  • Ligne 6 : Changez indicator_label et snt_var pour correspondre aux noms de colonnes de votre dictionnaire.
  • Ligne 9 : Remplacez dhis2_df par votre ensemble de données DHIS2.
  • Ligne 10 : Ajustez ou supprimez "orgunitlevel5" si cette colonne n’est pas présente.

Le dictionnaire de données Excel utilisé ici est basé sur l’export DHIS2 de Sierra Leone. Si nous travaillons avec un autre pays, mettez à jour les lignes du fichier Excel afin que les colonnes « nom DHIS2 original » et « nom SNT » correspondent aux indicateurs.

Par exemple, si l’ensemble de données utilise « Admission d’un enfant atteint de paludisme - 5–14 ans », entrez ce nom exact dans le dictionnaire et définissez le nom SNT préféré à côté. Une fois le fichier Excel mis à jour, le code de renommage applique automatiquement le mapping.

Warning

Le dictionnaire doit refléter l’export DHIS2 du pays. Ajoutez, modifiez ou supprimez des lignes dans le fichier Excel afin qu’il s’aligne sur la dénomination des indicateurs et la structure de notification.

Étape 4 : Standardiser les colonnes de dates

La plupart des workflows SNT nécessitent un ensemble cohérent de variables de date : une colonne de date au format YYYY-MM-DD, des champs year et month séparés, et un champ yearmon au format YYYY-MM pour les travaux de séries temporelles ordonnées. L’objectif de cette étape est de prendre le format de date qui apparaît dans l’export DHIS2 et de le convertir en cette structure standard.

Les étapes que nous appliquons dépendent du format de départ. Certains formats sont analysés automatiquement (par exemple, YYYY-MM-DD). D’autres, comme "Jan 2020" ou en français "Janvier 2020", nécessitent quelques étapes supplémentaires.

Le bloc de code ci-dessous montre comment analyser les principaux formats de date présents dans les exports DHIS2 et les convertir en une structure cohérente. Une fois le champ de date standardisé, la création des champs year, month et yearmon devient simple.

TipAnalyser les dates
  • R
  • Python

Format déjà en YYYY-MM-DD

# créer des données factices
dates_ex1 <- tibble::tibble(
  raw_date = c("2020-01-15", "2021-03-01", "2022-12-20")
)

# analyser les dates
dates_ex1 |>
  dplyr::mutate(
    date = lubridate::ymd(raw_date)
  )
NoteSortie
# A tibble: 3 × 2
  raw_date   date      
  <chr>      <date>    
1 2020-01-15 2020-01-15
2 2021-03-01 2021-03-01
3 2022-12-20 2022-12-20

Format « Jan 2020 » ou « January 2020 »

# créer des données factices
dates_ex2 <- tibble::tibble(
  raw_date = c("Jan 2020", "February 2021", "Mar 2022")
)

# analyser les dates
dates_ex2 |>
  dplyr::mutate(
    date = lubridate::parse_date_time(
      raw_date,
      orders = c("b Y", "B Y")
    ) |>
      as.Date()
  )
NoteSortie
# A tibble: 3 × 2
  raw_date      date      
  <chr>         <date>    
1 Jan 2020      2020-01-01
2 February 2021 2021-02-01
3 Mar 2022      2022-03-01

Noms de mois en français (par exemple, « Janvier 2020 »)

# créer des données factices
dates_ex3 <- tibble::tibble(
  raw_date = c(
    "Janvier 2020",
    "Février 2021",
    "décembre 2022",
    "Jan 2021",
    "Fe 2022",
    "Dec 2023"
  )
)

# analyser les dates
dates_ex3 |>
  dplyr::mutate(
    date = paste0("01 ", raw_date) |>
      lubridate::dmy(locale = "fr_FR") |>
      as.Date()
  )
NoteSortie
# A tibble: 6 × 2
  raw_date      date      
  <chr>         <date>    
1 Janvier 2020  2020-01-01
2 Février 2021  2021-02-01
3 décembre 2022 2022-12-01
4 Jan 2021      2021-01-01
5 Fe 2022       2022-02-01
6 Dec 2023      2023-12-01

Noms de mois en portugais (par exemple, « Janeiro 2020 »)

# créer des données factices
dates_ex4 <- tibble::tibble(
  raw_date = c("Janeiro 2020", "fevereiro 2021", "Dezembro 2022",
               "Jan 2021", "Fev 2022", "Dez 2023")
)

# analyser les dates
dates_ex4 |>
  dplyr::mutate(
    date = paste0("01 ", raw_date) |>
      lubridate::dmy(locale = "pt_PT") |>
      as.Date()
  )
NoteSortie
# A tibble: 6 × 2
  raw_date       date      
  <chr>          <date>    
1 Janeiro 2020   2020-01-01
2 fevereiro 2021 2021-02-01
3 Dezembro 2022  2022-12-01
4 Jan 2021       2021-01-01
5 Fev 2022       2022-02-01
6 Dez 2023       2023-12-01

Format « 2023-01 » ou « 2023/01 »

# créer des données factices
dates_ex5 <- tibble::tibble(
  raw_date = c("2020-01", "2021/05", "2023-12")
)

# analyser les dates
dates_ex5 |>
  dplyr::mutate(
    date = lubridate::parse_date_time(raw_date, orders = c("Y-m", "Y/m")),
    date = lubridate::floor_date(date, unit = "month") |> as.Date()
  )
NoteSortie
# A tibble: 3 × 2
  raw_date date      
  <chr>    <date>    
1 2020-01  2020-01-01
2 2021/05  2021-05-01
3 2023-12  2023-12-01

Format « 01/2020 » ou « 01-2020 » (Mois–Année)

# créer des données factices
dates_ex6 <- tibble::tibble(
  raw_date = c("01/2020", "06-2021", "11/2022")
)

# analyser les dates
dates_ex6 |>
  dplyr::mutate(
    date = lubridate::parse_date_time(raw_date, orders = c("m/Y", "m-Y")),
    date = lubridate::floor_date(date, unit = "month") |> as.Date()
  )
NoteSortie
# A tibble: 3 × 2
  raw_date date      
  <chr>    <date>    
1 01/2020  2020-01-01
2 06-2021  2021-06-01
3 11/2022  2022-11-01

Format compact « 202301 » (YYYYMM)

# créer des données factices
dates_ex7 <- tibble::tibble(
  raw_date = c("202301", "202102", "202212")
)

dates_ex7 |>
  dplyr::mutate(
    year = substr(raw_date, 1, 4),
    month = substr(raw_date, 5, 6),
    date = lubridate::ymd(sprintf("%s-%s-01", year, month)) |> as.Date()
  )
NoteSortie
# A tibble: 3 × 4
  raw_date year  month date      
  <chr>    <chr> <chr> <date>    
1 202301   2023  01    2023-01-01
2 202102   2021  02    2021-02-01
3 202212   2022  12    2022-12-01

Formats mixtes ou ambigus

# créer des données factices
dates_ex8 <- tibble::tibble(
  raw_date = c(
    "2020-01-15", # date ISO complète
    "Jan 2021",   # mois anglais abrégé
    "202203",     # AAAAMM compact
    "03/2022",    # mois/année avec barre oblique
    "2021-07",    # année-mois
    "July 2020",  # mois anglais complet
    "15-02-2021", # JJ-MM-AAAA
    "2020/11/05", # AAAA/MM/JJ
    "2020.12.25", # date avec points
    "2022.07",    # AAAA.MM
    "10-2020",    # MM-AAAA
    "20210105",   # AAAAMMJJ
    "2020 Jan",   # année puis mois (anglais)
    "2020.01.01"  # AAAA.MM.JJ avec points
  )
)

# analyser les dates
dates_ex8 |>
  dplyr::mutate(
    date = lubridate::parse_date_time(
      raw_date,
      orders = c(
        "Y-m-d", # 2020-01-15
        "Y/m/d", # 2020/11/05
        "Y.m.d", # 2020.12.25
        "b Y",   # Jan 2021
        "B Y",   # January 2021
        "Y b",   # 2020 Jan
        "Y B",   # 2020 January
        "Ym",    # 202203
        "m/Y",   # 03/2022
        "m-Y",   # 10-2020
        "Y-m",   # 2021-07
        "Y.m",   # 2022.07
        "d-m-Y", # 15-02-2021
        "d/m/Y", # 15/02/2021 (if present)
        "Ymd"    #  20210105
      )
    ) |>
      as.Date()
  )
NoteSortie
# A tibble: 14 × 2
   raw_date   date      
   <chr>      <date>    
 1 2020-01-15 2020-01-15
 2 Jan 2021   2021-01-01
 3 202203     2022-03-01
 4 03/2022    2022-03-01
 5 2021-07    2021-07-01
 6 July 2020  2020-07-01
 7 15-02-2021 2021-02-15
 8 2020/11/05 2020-11-05
 9 2020.12.25 2020-12-25
10 2022.07    2022-07-01
11 10-2020    2020-10-01
12 20210105   2021-01-05
13 2020 Jan   2020-01-01
14 2020.01.01 2020-01-01

Format déjà en YYYY-MM-DD

# créer des données factices
dates_ex1 = pd.DataFrame({
    "raw_date": ["2020-01-15", "2021-03-01", "2022-12-20"]
})

# analyser les dates
dates_ex1["date"] = pd.to_datetime(dates_ex1["raw_date"], format="%Y-%m-%d")

dates_ex1
NoteSortie
     raw_date       date
0  2020-01-15 2020-01-15
1  2021-03-01 2021-03-01
2  2022-12-20 2022-12-20

Format « Jan 2020 » ou « January 2020 »

# créer des données factices
dates_ex2 = pd.DataFrame({
    "raw_date": ["Jan 2020", "February 2021", "Mar 2022"]
})

# analyser les dates
dates_ex2["date"] = pd.to_datetime(
    dates_ex2["raw_date"],
    format=None,  # permettre l'analyse flexible des noms de mois abrégés et complets
)

dates_ex2
NoteSortie
        raw_date       date
0       Jan 2020 2020-01-01
1  February 2021 2021-02-01
2       Mar 2022 2022-03-01

Noms de mois en français (par exemple, « Janvier 2020 »)

# définir la locale française (ajuster si nécessaire selon le système)
# options courantes : 'fr_FR.UTF-8', 'fr_FR'
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')

# créer des données factices
dates_ex3 = pd.DataFrame({
    "raw_date": [
        "Janvier 2020",
        "Février 2021",
        "décembre 2022"
    ]
})

# s'assurer que le jour est présent pour l'analyse
dates_ex3["date"] = pd.to_datetime(
    "01 " + dates_ex3["raw_date"],
    format="%d %B %Y",
    errors="coerce"
)

# secours pour les mois abrégés
mask_missing = dates_ex3["date"].isna()
dates_ex3.loc[mask_missing, "date"] = pd.to_datetime(
    "01 " + dates_ex3.loc[mask_missing, "raw_date"],
    format="%d %b %Y",
    errors="coerce"
)

dates_ex3
NoteSortie
'fr_FR.UTF-8'
        raw_date       date
0   Janvier 2020 2020-01-01
1   Février 2021 2021-02-01
2  décembre 2022 2022-12-01

Noms de mois en portugais (par exemple, « Janeiro 2020 »)

# définir la locale portugaise
# options courantes : 'pt_PT.UTF-8', 'pt_PT'
locale.setlocale(locale.LC_TIME, 'pt_PT.UTF-8')

# créer des données factices
dates_ex4 = pd.DataFrame({
    "raw_date": [
        "Janeiro 2020",
        "fevereiro 2021",
        "Dezembro 2022",
        "Jan 2021",
        "Fev 2022",
        "Dez 2023"
    ]
})

# première tentative : noms de mois portugais complets
dates_ex4["date"] = pd.to_datetime(
    "01 " + dates_ex4["raw_date"],
    format="%d %B %Y",
    errors="coerce"
)

# secours : noms de mois portugais abrégés (ex. Jan, Fev, Dez)
mask_missing = dates_ex4["date"].isna()
dates_ex4.loc[mask_missing, "date"] = pd.to_datetime(
    "01 " + dates_ex4.loc[mask_missing, "raw_date"],
    format="%d %b %Y",
    errors="coerce"
)

dates_ex4
NoteSortie
'pt_PT.UTF-8'
         raw_date       date
0    Janeiro 2020 2020-01-01
1  fevereiro 2021 2021-02-01
2   Dezembro 2022 2022-12-01
3        Jan 2021 2021-01-01
4        Fev 2022 2022-02-01
5        Dez 2023 2023-12-01

Formats mixtes ou ambigus

# créer des données factices
dates_ex5 = pd.DataFrame(
    {
        "raw_date": [
            "2020-01-15",  # date ISO complète
            "Jan 2021",    # mois anglais abrégé
            "202203",      # AAAAMM compact
            "03/2022",     # month/year
            "2021-07",     # année-mois
            "July 2020",   # mois anglais complet
            "15-02-2021",  # JJ-MM-AAAA
            "2020/11/05",  # AAAA/MM/JJ
            "2020.12.25",  # AAAA.MM.JJ
            "2022.07",     # YYYY.MM
            "10-2020",     # MM-AAAA
            "20210105",    # AAAAMMJJ
            "2020 Jan",    # année puis mois
            "2020.01.01",  # date complète avec points
        ]
    }
)

# liste des formats possibles
formats = [
    "%Y-%m-%d",
    "%Y/%m/%d",
    "%Y.%m.%d",
    "%d-%m-%Y",
    "%d/%m/%Y",
    "%b %Y",
    "%B %Y",
    "%Y %b",
    "%Y %B",
    "%Y-%m",
    "%Y/%m",
    "%Y.%m",
    "%m-%Y",
    "%m/%Y",
    "%Y%m",
    "%Y%m%d",
]


def try_parse(value):
    # essayer d'abord les formats stricts
    for fmt in formats:
        try:
            return pd.to_datetime(value, format=fmt)
        except:
            pass
    # secours : laisser pandas deviner librement
    return pd.to_datetime(value, errors="coerce")


dates_ex5["date"] = dates_ex5["raw_date"].apply(try_parse)

dates_ex5["date"] = dates_ex5["date"].dt.to_period("M").dt.to_timestamp()
dates_ex5
NoteSortie
      raw_date       date
0   2020-01-15 2020-01-01
1     Jan 2021 2021-01-01
2       202203 2022-03-01
3      03/2022 2022-03-01
4      2021-07 2021-07-01
5    July 2020 2020-07-01
6   15-02-2021 2021-02-01
7   2020/11/05 2020-11-01
8   2020.12.25 2020-12-01
9      2022.07 2022-07-01
10     10-2020 2020-10-01
11    20210105 2021-01-01
12    2020 Jan 2020-01-01
13  2020.01.01 2020-01-01

Pour l’ensemble de données de Sierra Leone, le champ de date est stocké dans periodname et apparaît dans des formats tels que « January 2019 » ou « February 2019 ». Nous standardisons d’abord ce format en une date YYYY-MM-DD correcte. Une fois la colonne de date nettoyée, nous pouvons générer les champs year, month et yearmon utilisés tout au long du workflow SNT.

  • R
  • Python
# définir la locale selon la langue de votre export DHIS2
# options possibles : "en_US.UTF-8", "fr_FR.UTF-8", "pt_PT.UTF-8"
Sys.setlocale("LC_TIME", "en_US.UTF-8")

dhis2_df <- dhis2_df |>
  # analyser la date brute en un objet Date correct
  dplyr::mutate(
    date = lubridate::parse_date_time(
      periodname,
      orders = c("B Y", "b Y")
    ) |>
      as.Date()
  ) |>
  # créer les champs année, mois et libellé yearmon ordonné
  dplyr::mutate(
    year = lubridate::year(date),
    month = lubridate::month(date),
    # libellé lisible, ex. "Jan 2020"
    yearmon = format(date, "%b %Y"),
    # ordonner le facteur par date chronologique réelle
    yearmon = factor(yearmon, levels = unique(yearmon[order(date)]))
  )

# vérifier les premières lignes
dhis2_df |>
  dplyr::select(date, year, month, yearmon) |>
  head()
NoteSortie
[1] "en_US.UTF-8"
        date year month  yearmon
1 2015-01-01 2015     1 Jan 2015
2 2015-02-01 2015     2 Feb 2015
3 2015-03-01 2015     3 Mar 2015
4 2015-04-01 2015     4 Apr 2015
5 2015-05-01 2015     5 May 2015
6 2015-06-01 2015     6 Jun 2015

Pour adapter le code :

  • Ligne 3 : Définissez la locale pour correspondre à la langue des noms de mois dans votre export, par exemple "en_US.UTF-8", "fr_FR.UTF-8", "pt_PT.UTF-8".
  • Ligne 9 : Mettez à jour le nom de la colonne si votre champ de date ne s’appelle pas periodname.
  • Ligne 10 : Ajustez la liste orders = c("B Y", "b Y") uniquement si vos dates utilisent une structure différente.
  • Ligne 19 Modifiez l’affichage de yearmon (par exemple, "%b %Y" vers "%B %Y") selon la façon dont vous souhaitez afficher les noms de mois.
# définir la locale selon la langue de votre export DHIS2
# options possibles : "en_US.UTF-8", "fr_FR.UTF-8", "pt_PT.UTF-8"
locale.setlocale(locale.LC_TIME, "en_US.UTF-8")

# analyser la date brute en un objet datetime correct
dhis2_df["date"] = pd.to_datetime(
    dhis2_df["periodname"],
    format="%B %Y",
    errors="coerce"
)

# créer les colonnes année, mois et yearmon ordonné
dhis2_df["year"] = dhis2_df["date"].dt.year
dhis2_df["month"] = dhis2_df["date"].dt.month
dhis2_df["yearmon"] = dhis2_df["date"].dt.strftime("%b %Y")

# vérifier la sortie
dhis2_df[["date", "year", "month", "yearmon"]].head()
NoteSortie
'en_US.UTF-8'
        date  year  month   yearmon
0 2015-01-01  2015      1  Jan 2015
1 2015-02-01  2015      2  Feb 2015
2 2015-03-01  2015      3  Mar 2015
3 2015-04-01  2015      4  Apr 2015
4 2015-05-01  2015      5  May 2015

Pour adapter le code :

  • Ligne 3 : Définissez la locale pour correspondre à la langue des noms de mois dans votre export, par exemple "en_US.UTF-8", "fr_FR.UTF-8", "pt_PT.UTF-8".
  • Ligne 6 : Mettez à jour le nom de la colonne si votre champ de date ne s’appelle pas periodname.
  • Ligne 7 : Ajustez format="%B %Y" si vos dates utilisent une structure différente (par exemple, "%b %Y" pour les mois abrégés).
  • Ligne 14 : Modifiez le format strftime (par exemple, "%b %Y" vers "%B %Y") selon la façon dont vous souhaitez afficher les noms de mois.

Étape 5 : Gérer les colonnes de localisation

Étape 5.1 : Harmoniser les noms administratifs

Les différences d’orthographe, de casse, d’espacement ou de conventions de dénomination entre les exports DHIS2 et les shapefiles sont courantes. Ces différences entraîneront des échecs de jointure et produiront des unités géographiques manquantes ou dupliquées. Avant toute jointure spatiale, nous devons harmoniser les noms administratifs utilisés dans l’ensemble de données DHIS2 avec ceux définis dans le shapefile.

Une façon pratique de le faire est d’utiliser la fonction prep_geonames() du paquet sntutils. La fonction détecte les divergences, suggère des correspondances probables à l’aide de méthodes de distance de chaînes de caractères, et permet une confirmation ou une édition interactive. Elle sauvegarde également les décisions dans un fichier de cache afin que l’harmonisation soit reproductible et cohérente entre les analystes et les réexécutions.

Nous ne montrons ici que la structure de base. Le workflow complet, incluant la correspondance interactive et la réutilisation scriptée, est expliqué dans la section Fusion des shapefiles avec des données tabulaires de la bibliothèque de code SNT. Pour un article de blog approfondi sur cette méthode, voir Cleaning and standardising geographic names in R with prep_geonames() de l’AMMnet Hackathon.

Avant toute correspondance interactive, nous nous assurons que les colonnes sont correctement assignées. D’après l’observation des noms administratifs, il s’est avéré que adm1 était en réalité adm2, nous créons donc une correspondance simple pour assigner les districts (adm2) à leurs provinces respectives (adm1).

  • R
  • Python
# installer le paquet sntutils depuis GitHub
# contient plusieurs fonctions utilitaires pratiques
devtools::install_github("ahadi-analytics/sntutils")

# définir l'emplacement pour sauvegarder le cache
cache_path <- "1.1_foundational/1d_cache_files"

# récupérer le shapefile adm3 pour l'utiliser comme référence de correspondance
shp_adm3 <- sntutils::read(
  here::here("data/shapefiles/processed/sle_spatial_adm3_2021.rds")
) |>
  # supprimer la géométrie, on n'a besoin que des noms admin
  sf::st_drop_geometry()

# standardiser les noms administratifs
dhis2_df <- dhis2_df |>
  dplyr::mutate(
    adm0 = toupper(adm0),
    adm2 = toupper(adm1),
    adm3 = toupper(adm3),
    hf = toupper(hf),
    # le adm2 dans le shapefile de référence n'a pas
    # "DISTRICT" dans le nom, on le supprime pour permettre la correspondance
    adm2 = stringr::str_remove_all(adm2, " DISTRICT"),
    adm3 = stringr::str_remove_all(adm3, " CHIEFDOM")
  ) |>
  dplyr::mutate(
    # assigner les provinces selon les regroupements de districts
    # (pas d'adm1 car l'adm1 actuel est adm2)
    adm1 = dplyr::case_when(
      adm2 %in% c("KAILAHUN", "KENEMA", "KONO") ~ "EASTERN",
      adm2 %in% c("BOMBALI", "FALABA", "KOINADUGU", "TONKOLILI") ~ "NORTH EAST",
      adm2 %in% c("KAMBIA", "KARENE", "PORT LOKO") ~ "NORTH WEST",
      adm2 %in% c("BO", "BONTHE", "MOYAMBA", "PUJEHUN") ~ "SOUTHERN",
      adm2 %in% c("WESTERN AREA RURAL", "WESTERN AREA URBAN") ~ "WESTERN"
    )
  )

# harmoniser les noms admin entre les données DHIS2 et le shapefile
dhis2_df <-
  sntutils::prep_geonames(
    target_df = dhis2_df,
    lookup_df = lookup_keys,
    level0 = "adm0",
    level1 = "adm1",
    level2 = "adm2",
    level3 = "adm3",
    interactive = TRUE,
    cache_path = here::here(cache_path, "geoname_decisions_cache.xlsx")
  )
NoteSortie
── ℹ Match Summary ─────────────────────────────────────────────────────────────
! Both sides have unmatched names; see per-level lines below.

Target data as base N                                                       
• adm0 (level0): 1 out of 1 matched                                         
• adm1 (level1): 5 out of 5 matched                                         
• adm2 (level2): 16 out of 16 matched                                       
• adm3 (level3): 199 out of 207 matched                                     
Lookup data as base N                                                       
• adm0 (level0): 1 out of 1 matched                                         
• adm1 (level1): 5 out of 5 matched                                         
• adm2 (level2): 16 out of 16 matched                                       
• adm3 (level3): 199 out of 208 matched                                     
ℹ Partial match completed. There are still matches to be made.
Would you like to do interactive matching? (yes/no):
ℹ Exiting without interactive matching...

Pour adapter le code :

  • Ligne 6 : Mettez à jour cache_path vers l’emplacement souhaité pour sauvegarder les fichiers de cache.
  • Lignes 9–12 : Remplacez le chemin du shapefile par le chemin vers votre shapefile de référence.
  • Ligne 15 : Remplacez dhis2_df par votre ensemble de données DHIS2 ou cible.
  • Lignes 15–36 : Ce bloc de standardisation est spécifique à la Sierra Leone. Adaptez la logique toupper(), str_remove_all() et case_when() pour correspondre à la structure administrative et aux conventions de dénomination de votre pays.
  • Ligne 39 : Remplacez dhis2_df par le nom de votre dataframe cible.
  • Ligne 42 : Remplacez lookup_keys par votre jeu de données de correspondance ou de référence.
  • Lignes 43–46 : Ajustez level0, level1, level2 et level3 pour correspondre à vos noms de colonnes admin réels s’ils diffèrent (par exemple, “country”, “region”, “district”, “ward”). Assurez-vous que les colonnes existent dans les deux jeux de données.
  • Ligne 48 : Mettez à jour le nom du fichier de cache si souhaité.

Une fois mis à jour, exécutez le code pour harmoniser les noms admin des données avec le shapefile.

# si ce n'est pas déjà fait, installer le paquet python sntutils depuis GitHub
# contient plusieurs fonctions utilitaires pratiques
pip install git+https://github.com/ahadi-analytics/sntutils-py.git
from sntutils.geo import prep_geonames # pour la correspondance des noms

# définir l'emplacement pour sauvegarder le cache
cache_path = Path("1.1_foundational/1d_cache_files")

# récupérer le shapefile adm3 pour l'utiliser comme référence de correspondance
shp_adm3 = gpd.read_file(
    Path(here("data/shapefiles/processed/sle_spatial_adm3_2021.geojson"))
).drop(columns="geometry")

# standardiser les noms administratifs
dhis2_df = dhis2_df.assign(
    adm0=lambda x: x["adm0"].str.upper(),
    # le adm2 dans le shapefile de référence n'a pas
    # "DISTRICT" dans le nom, on le supprime pour permettre la correspondance
    adm2=lambda x: x["adm1"]
    .str.upper()
    .str.replace(" DISTRICT", "", regex=False)
    .str.strip(),
    adm3=lambda x: x["adm3"]
    .str.upper()
    .str.replace(" CHIEFDOM", "", regex=False)
    .str.strip(),
    hf=lambda x: x["hf"]
    .str.upper()
)

# assigner les provinces selon les regroupements de districts
# (pas d'adm1 car l'adm1 actuel est adm2)
district_to_province = {
    "KAILAHUN": "EASTERN", "KENEMA": "EASTERN", "KONO": "EASTERN",
    "BOMBALI": "NORTH EAST", "FALABA": "NORTH EAST",
    "KOINADUGU": "NORTH EAST", "TONKOLILI": "NORTH EAST",
    "KAMBIA": "NORTH WEST", "KARENE": "NORTH WEST", "PORT LOKO": "NORTH WEST",
    "BO": "SOUTHERN", "BONTHE": "SOUTHERN",
    "MOYAMBA": "SOUTHERN", "PUJEHUN": "SOUTHERN",
    "WESTERN AREA RURAL": "WESTERN", "WESTERN AREA URBAN": "WESTERN"
}

dhis2_df["adm1"] = dhis2_df["adm2"].map(district_to_province)

# harmoniser les noms admin entre les données dhis2 et le shapefile
# note : sntutils::prep_geonames() n'a pas d'équivalent python direct
# correspondance floue manuelle ou fusion exacte requise
dhis2_df = dhis2_df.merge(
    lookup_keys,
    on=["adm0", "adm1", "adm2", "adm3"],
    how="left"
)

# harmoniser les noms admin entre les données de population et le shapefile
dhis2_df = prep_geonames(
    target_df=dhis2_df,
    lookup_df=shp_adm3,
    level0="adm0",
    level1="adm1",
    level2="adm2",
    level3="adm3",
    cache_path=here(cache_path, "geoname_decisions_cache.xlsx")
)
NoteSortie
Info: Loaded cache from /Users/mohamedyusuf/ahadi-analytics/code/GitHub/snt-code-library/english/data_r/routine_cases/geoname_decisions_cache.xlsx

ℹ Match Summary

╭───────────────┬─────────┬───────────────┬───────────────╮
│ Level         │ Matched │ Target Data N │ Lookup Data N │
├───────────────┼─────────┼───────────────┼───────────────┤
│ adm0 (level0) │    1    │       1       │       1       │
│ adm1 (level1) │    5    │       5       │       5       │
│ adm2 (level2) │   16    │      16       │      16       │
│ adm3 (level3) │   207   │      207      │      208      │
╰───────────────┴─────────┴───────────────┴───────────────╯
ℹ Lookup has extra names not in data                       

Success: All records matched; process completed. Exiting...

Pour adapter le code :

  • Ligne 3 : Exécuter une fois dans le terminal pour installer le paquet sntutils-py depuis GitHub.
  • Ligne 4 : Instruction d’importation pour la fonction prep_geonames.
  • Ligne 7 : Mettez à jour cache_path vers l’emplacement souhaité pour sauvegarder les fichiers de cache.
  • Lignes 10–12 : Remplacez le chemin du shapefile par le chemin vers votre shapefile de référence.
  • Ligne 15 : Remplacez dhis2_df par votre ensemble de données DHIS2 ou cible.
  • Lignes 15–29 : Ce bloc de standardisation est spécifique à la Sierra Leone. Adaptez les méthodes .str.upper(), .str.replace() et le dictionnaire district_to_province pour correspondre à la structure administrative et aux conventions de dénomination de votre pays.
  • Lignes 48–52 : Supprimez ce bloc de fusion si vous utilisez prep_geonames() ci-dessous, car cela duplique l’étape de correspondance.
  • Ligne 55 : Remplacez dhis2_df par le nom de votre dataframe cible.
  • Ligne 57 : Remplacez shp_adm3 par votre jeu de données de correspondance ou de référence.
  • Lignes 58–61 : Ajustez level0, level1, level2 et level3 pour correspondre à vos noms de colonnes admin réels s’ils diffèrent (par exemple, “country”, “region”, “district”, “ward”). Assurez-vous que les colonnes existent dans les deux jeux de données.
  • Ligne 62 : Mettez à jour le nom du fichier de cache si souhaité.

Une fois mis à jour, exécutez le code pour harmoniser les noms admin des données avec le shapefile.

Une fois la correspondance interactive terminée et les résultats sauvegardés, le message de sortie confirme que tous les niveaux administratifs ont été alignés avec succès entre l’ensemble de données et le shapefile de référence. Si des divergences subsistent, la fonction prep_geoname nous invitera à les résoudre de manière interactive.

Pour vérifier les décisions, partagez le fichier de cache geoname_decisions_cache.xlsx avec l’équipe SNT du pays pour s’assurer que toutes les décisions de correspondance sont correctes.

Étape 5.2 : Créer des identifiants uniques et des libellés de localisation

À partir de nos noms admin corrigés, nous pouvons maintenant créer des colonnes clés pour l’analyse et la cartographie en aval :

  • hf_uid : Un identifiant unique d’établissement de santé généré par hachage de la hiérarchie administrative complète et du nom de l’établissement. Cela garantit que chaque établissement a un identifiant cohérent et reproductible.
  • location_short : Un libellé concaténé de province et de district (adm1 ~ adm2), utile pour les tableaux récapitulatifs et les vues agrégées.
  • location_full : La hiérarchie administrative complète incluant la chefferie et le nom de l’établissement, utile pour les info-bulles cartographiques détaillées et les libellés d’exploration.
  • record_id : Un identifiant unique d’enregistrement combinant l’identifiant de l’établissement et la période temporelle, assurant que chaque combinaison établissement-mois est identifiable de manière unique.
  • R
  • Python
# créer les identifiants
dhis2_df <- dhis2_df |>
  dplyr::mutate(
    # créer un identifiant unique d'établissement de santé
    # à partir de la hiérarchie admin
    hf_uid = sntutils::vdigest(
      paste0(adm0, adm1, adm2, adm3, hf),
      algo = "xxhash32"
    ),
    # créer des libellés de localisation pour la cartographie
    location_short = paste(adm1, adm2, sep = " ~ "),
    location_full = paste(adm1, adm2, adm3, hf, sep = " ~ "),
    # générer un identifiant d'enregistrement cohérent (établissement + période)
    record_id = sntutils::vdigest(
      paste(hf_uid, yearmon),
      algo = "xxhash32"
    )
  )

# vérifier
dhis2_df |>
  dplyr::arrange(location_full) |>
  dplyr::distinct(location_full, hf_uid, record_id) |>
  head()
NoteSortie
                           location_full   hf_uid record_id
1 EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP cff1ec2b  20724f8d
2 EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP cff1ec2b  cc1562d5
3 EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP cff1ec2b  2d42fbf9
4 EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP cff1ec2b  6958bd08
5 EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP cff1ec2b  d2cf4c99
6 EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP cff1ec2b  0bcfc2a4

Pour adapter le code :

  • Ligne 2 : Remplacez dhis2_df par votre ensemble de données cible.
  • Ligne 6 : Ajustez les arguments paste0() pour correspondre à vos noms de colonnes admin (par exemple, adm0, adm1, adm2, adm3) et à la colonne d’établissement (par exemple, hf).
  • Ligne 11 : Modifiez location_short pour inclure les niveaux admin souhaités pour les vues récapitulatives.
  • Ligne 12 : Modifiez location_full pour inclure tous les niveaux admin et le nom de l’établissement pour un libellé détaillé.
  • Ligne 14 : Assurez-vous que yearmon correspond au nom de votre colonne de période temporelle. Si vous utilisez une unité temporelle différente (par exemple, year, epiweek), mettez à jour en conséquence.

Une fois mis à jour, exécutez le code pour générer des identifiants uniques pour vos établissements de santé et enregistrements.

# fonction auxiliaire pour créer un condensé xxhash32 (équivalent à sntutils::vdigest)
def vdigest(x, algo="xxhash32"):
    """Créer un condensé de hachage vectorisé."""
    return x.apply(lambda val: xxhash.xxh32(str(val)).hexdigest())


# créer les identifiants
dhis2_df = dhis2_df.assign(
    # créer un identifiant unique d'établissement de santé à partir de la hiérarchie admin
    # utiliser la sémantique paste0 (sans séparateur) pour correspondre à R's paste0(adm0, adm1, adm2, adm3, hf)
    hf_uid=lambda x: vdigest(
        x["adm0"] + x["adm1"] + x["adm2"] + x["adm3"] + x["hf"]
    ),
    # créer des libellés de localisation pour la cartographie
    location_short=lambda x: x["adm1"] + " ~ " + x["adm2"],
    location_full=lambda x: (
        x["adm1"] + " ~ " + x["adm2"] + " ~ " + x["adm3"] + " ~ " + x["hf"]
    ),
)

# générer un identifiant d'enregistrement cohérent (établissement + période)
# utiliser un espace pour correspondre au séparateur par défaut de R's paste(hf_uid, yearmon)
dhis2_df["record_id"] = vdigest(
    dhis2_df["hf_uid"] + " " + dhis2_df["yearmon"].astype(str)
)

# vérifier
dhis2_df[["location_full", "hf_uid", "record_id"]].drop_duplicates().sort_values(
    "location_full"
).head()
NoteSortie
                              location_full    hf_uid record_id
128  EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP  95a9f6ec  8eceb7b6
96   EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP  95a9f6ec  af7f4517
97   EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP  95a9f6ec  ab2697b8
98   EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP  95a9f6ec  a6473869
99   EASTERN ~ KAILAHUN ~ DEA ~ BAIWALA CHP  95a9f6ec  249ddfd4

Pour adapter le code :

  • Ligne 8 : Remplacez dhis2_df par votre ensemble de données cible.
  • Lignes 11–13 : Ajustez les colonnes concaténées pour correspondre à vos noms de colonnes admin (par exemple, adm0, adm1, adm2, adm3) et à la colonne d’établissement (par exemple, hf).
  • Ligne 15 : Modifiez location_short pour inclure les niveaux admin souhaités pour les vues récapitulatives.
  • Lignes 16–18 : Modifiez location_full pour inclure tous les niveaux admin et le nom de l’établissement pour un libellé détaillé.
  • Lignes 23–25 : Assurez-vous que yearmon correspond au nom de votre colonne de période temporelle. Si vous utilisez une unité temporelle différente (par exemple, year, date), mettez à jour en conséquence.

Une fois mis à jour, exécutez le code pour générer des identifiants uniques pour vos établissements de santé et enregistrements.

Étape 6 : Calculer les variables

Étape 6.1 : Calculer les totaux des indicateurs et les nouvelles variables

Maintenant que nous avons importé nos données DHIS2 et nettoyé les colonnes, nous allons calculer des variables dérivées pour créer des totaux pour des indicateurs spécifiques. DHIS2 peut fournir des totaux, mais ces colonnes doivent être extraites spécifiquement. Dans l’ensemble de données DHIS2 brut, les indicateurs sont généralement désagrégés par groupe d’âge, communauté et niveau d’établissement de santé. Les analystes doivent s’efforcer d’obtenir les bases de données les plus désagrégées possible.

Par exemple, si nous voulons calculer le nombre total de consultations ambulatoires, DHIS2 peut ne pas fournir un total direct, mais inclut des composantes qui peuvent être additionnées, telles que allout_u5 et allout_ov5. N’oubliez pas de vérifier les définitions des variables pour vous assurer que les agrégations ne comptent pas les cas en double.

La même logique s’applique aux autres indicateurs, notamment le total des cas suspects (susp), des cas testés (test), des cas confirmés (conf), des cas traités (maltreat) et des cas présumés (pres). Le code ci-dessous montre comment calculer ces totaux en combinant les composantes pertinentes. Le code inclut également l’agrégation des indicateurs par groupes d’âge, comme test_hf_u5 qui capture tous les enfants de moins de cinq ans testés par RDT ou microscopie dans un établissement de santé.

Il est préférable de vérifier avec l’équipe SNT quels éléments de données spécifiques de DHIS2 doivent être additionnés pour obtenir le total correct. Bien que certains calculs puissent paraître évidents, d’autres ne le sont pas. L’encadré à l’Étape 3 liste des exemples de questions auxquelles nous devons obtenir des réponses avant de sommer les éléments de données désagrégés. Selon le DHIS2 du pays et les pratiques de notification des données, d’autres questions peuvent également être pertinentes.

ImportantConsulter l’équipe SNT

Bien que certains pays aient des cas présumés dans leurs données DHIS2, ce n’est pas le cas de tous ! Voici trois options pour les cas présumés que les pays ont utilisées :

  • Option 1 : Les données ont déjà une colonne pres, aucun calcul supplémentaire n’est nécessaire.
  • Option 2 : Calculer les cas présumés en utilisant la différence entre les cas traités et les cas confirmés : maltreat - conf
  • Option 3 : Calculer les cas présumés en utilisant la différence entre les cas suspects et les cas testés : susp - test

Le code ci-dessous utilise l’Option 2 (lignes 52 à 55). Les tests sont soumis à la disponibilité des ressources, ce qui signifie que les établissements traitent les cas présumés différemment.

Consultez l’équipe SNT pour déterminer la meilleure approche pour le calcul des cas présumés, qu’il s’agisse d’une des options ci-dessus ou d’une approche différente.

  • R
  • Python

Nous créons ci-dessous deux nouvelles fonctions : fallback_row_sum() et fallback_diff(). La fonction fallback_row_sum() remplace rowSums(..., na.rm = TRUE), qui renvoie 0 quand toutes les valeurs d’une ligne sont NA. Ce comportement par défaut peut masquer les données manquantes.

Dans les données de santé de routine, il est important de distinguer :

  • Les vrais zéros : par exemple, un établissement a déclaré 0 cas
  • Les valeurs manquantes : par exemple, l’établissement n’a pas déclaré du tout

fallback_row_sum() renvoie NA quand trop peu de valeurs sont présentes, préservant cette distinction et évitant la surestimation de l’exhaustivité.

De même, fallback_diff() renvoie la différence absolue si les deux valeurs sont présentes, la valeur non manquante si une seule est présente, et NA si les deux sont manquantes. Elle applique pmax() pour s’assurer que les résultats respectent un seuil minimum.

Ces deux fonctions protègent contre des sorties trompeuses lorsque les données sont incomplètes.

# somme par ligne intelligente avec gestion des données manquantes
fallback_row_sum <- function(..., min_present = 1, .keep_zero_as_zero = TRUE) {
  vars_matrix <- cbind(...)
  valid_count <- rowSums(!is.na(vars_matrix))
  raw_sum <- rowSums(vars_matrix, na.rm = TRUE)

  ifelse(valid_count >= min_present, raw_sum, NA_real_)
}

# différence absolue de secours entre deux vecteurs
fallback_diff <- function(col1, col2, minimum = 0) {
  dplyr::case_when(
    is.na(col1) & is.na(col2) ~ NA_real_,
    is.na(col1) ~ pmax(col2, minimum),
    is.na(col2) ~ pmax(col1, minimum),
    TRUE ~ pmax(col1 - col2, minimum)
  )
}

Nous utilisons maintenant ces fonctions pour agréger nos colonnes.

Afficher le code
# calculer les totaux des indicateurs dans les données DHIS2
dhis2_df <- dhis2_df |>
  dplyr::mutate(
    # consultations ambulatoires
    allout = fallback_row_sum(allout_u5, allout_ov5),

    # cas suspects
    susp = fallback_row_sum(
      susp_u5_hf,
      susp_5_14_hf,
      susp_ov15_hf,
      susp_u5_com,
      susp_5_14_com,
      susp_ov15_com
    ),

    # cas testés
    test_hf = fallback_row_sum(
      test_neg_mic_u5_hf,
      test_pos_mic_u5_hf,
      test_neg_mic_5_14_hf,
      test_pos_mic_5_14_hf,
      test_neg_mic_ov15_hf,
      test_pos_mic_ov15_hf,
      tes_neg_rdt_u5_hf,
      tes_pos_rdt_u5_hf,
      tes_neg_rdt_5_14_hf,
      tes_pos_rdt_5_14_hf,
      tes_neg_rdt_ov15_hf,
      tes_pos_rdt_ov15_hf
    ),

    test_com = fallback_row_sum(
      tes_neg_rdt_u5_com,
      tes_pos_rdt_u5_com,
      tes_neg_rdt_5_14_com,
      tes_pos_rdt_5_14_com,
      tes_neg_rdt_ov15_com,
      tes_pos_rdt_ov15_com
    ),

    test = fallback_row_sum(test_hf, test_com),

    # cas confirmés (établissement et communauté)
    conf_hf = fallback_row_sum(
      test_pos_mic_u5_hf,
      test_pos_mic_5_14_hf,
      test_pos_mic_ov15_hf,
      tes_pos_rdt_u5_hf,
      tes_pos_rdt_5_14_hf,
      tes_pos_rdt_ov15_hf
    ),

    conf_com = fallback_row_sum(
      tes_pos_rdt_u5_com,
      tes_pos_rdt_5_14_com,
      tes_pos_rdt_ov15_com
    ),

    conf = fallback_row_sum(conf_hf, conf_com),

    # cas traités
    maltreat_com = fallback_row_sum(
      maltreat_u24_u5_com,
      maltreat_ov24_u5_com,
      maltreat_u24_5_14_com,
      maltreat_ov24_5_14_com,
      maltreat_u24_ov15_com,
      maltreat_ov24_ov15_com
    ),

    maltreat_hf = fallback_row_sum(
      maltreat_u24_u5_hf,
      maltreat_ov24_u5_hf,
      maltreat_u24_5_14_hf,
      maltreat_ov24_5_14_hf,
      maltreat_u24_ov15_hf,
      maltreat_ov24_ov15_hf
    ),

    maltreat = fallback_row_sum(maltreat_hf, maltreat_com),

    # cas présumés
    pres_com = fallback_diff(maltreat_com, conf_com),
    pres_hf = fallback_diff(maltreat_hf, conf_hf),
    pres = fallback_row_sum(pres_com, pres_hf),

    # hospitalisations pour paludisme
    maladm = fallback_row_sum(
      maladm_u5,
      maladm_5_14,
      maladm_ov15
    ),

    # décès dus au paludisme
    maldth = fallback_row_sum(
      maldth_u5,
      maldth_1_59m,
      maldth_10_14,
      maldth_5_9,
      maldth_5_14,
      maldth_ov15,
      maldth_fem_ov15,
      maldth_mal_ov15
    ),

    # agrégations par groupe d'âge
    # cas testés par groupe d'âge (établissement uniquement)
    test_hf_u5 = fallback_row_sum(
      test_neg_mic_u5_hf,
      test_pos_mic_u5_hf,
      tes_neg_rdt_u5_hf,
      tes_pos_rdt_u5_hf
    ),

    test_hf_5_14 = fallback_row_sum(
      test_neg_mic_5_14_hf,
      test_pos_mic_5_14_hf,
      tes_neg_rdt_5_14_hf,
      tes_pos_rdt_5_14_hf
    ),

    test_hf_ov15 = fallback_row_sum(
      test_neg_mic_ov15_hf,
      test_pos_mic_ov15_hf,
      tes_neg_rdt_ov15_hf,
      tes_pos_rdt_ov15_hf
    ),

    # cas testés par groupe d'âge (Community only)
    test_com_u5 = fallback_row_sum(
      tes_neg_rdt_u5_com,
      tes_pos_rdt_u5_com
    ),

    test_com_5_14 = fallback_row_sum(
      tes_neg_rdt_5_14_com,
      tes_pos_rdt_5_14_com
    ),

    test_com_ov15 = fallback_row_sum(
      tes_neg_rdt_ov15_com,
      tes_pos_rdt_ov15_com
    ),

    # total des cas testés par groupe d'âge (établissement + communauté)
    test_u5 = fallback_row_sum(test_hf_u5, test_com_u5),
    test_5_14 = fallback_row_sum(test_hf_5_14, test_com_5_14),
    test_ov15 = fallback_row_sum(test_hf_ov15, test_com_ov15),

    # cas suspects par groupe d'âge (HF only)
    susp_hf_u5 = susp_u5_hf,

    susp_hf_5_14 = susp_5_14_hf,

    susp_hf_ov15 = susp_ov15_hf,

    # cas suspects par groupe d'âge (Community only)
    susp_com_u5 = susp_u5_com,

    susp_com_5_14 = susp_5_14_com,

    susp_com_ov15 = susp_ov15_com,

    # total suspected by age group (HF + Community)
    susp_u5 = fallback_row_sum(susp_hf_u5, susp_com_u5),
    susp_5_14 = fallback_row_sum(susp_hf_5_14, susp_com_5_14),
    susp_ov15 = fallback_row_sum(susp_hf_ov15, susp_com_ov15),

    # cas confirmés par groupe d'âge (établissement uniquement)
    conf_hf_u5 = fallback_row_sum(
      test_pos_mic_u5_hf,
      tes_pos_rdt_u5_hf
    ),

    conf_hf_5_14 = fallback_row_sum(
      test_pos_mic_5_14_hf,
      tes_pos_rdt_5_14_hf
    ),

    conf_hf_ov15 = fallback_row_sum(
      test_pos_mic_ov15_hf,
      tes_pos_rdt_ov15_hf
    ),

    # cas confirmés par groupe d'âge (communauté uniquement)
    conf_com_u5 = tes_pos_rdt_u5_com,
    conf_com_5_14 = tes_pos_rdt_5_14_com,
    conf_com_ov15 = tes_pos_rdt_ov15_com,

    # total des cas confirmés par groupe d'âge (établissement + communauté)
    conf_u5 = fallback_row_sum(conf_hf_u5, conf_com_u5),
    conf_5_14 = fallback_row_sum(conf_hf_5_14, conf_com_5_14),
    conf_ov15 = fallback_row_sum(conf_hf_ov15, conf_com_ov15),

    # cas traités par groupe d'âge (HF only)
    maltreat_hf_u5 = fallback_row_sum(
      maltreat_u24_u5_hf,
      maltreat_ov24_u5_hf
    ),

    maltreat_hf_5_14 = fallback_row_sum(
      maltreat_u24_5_14_hf,
      maltreat_ov24_5_14_hf
    ),

    maltreat_hf_ov15 = fallback_row_sum(
      maltreat_u24_ov15_hf,
      maltreat_ov24_ov15_hf
    ),

    # cas traités par groupe d'âge (Community only)
    maltreat_com_u5 = fallback_row_sum(
      maltreat_u24_u5_com,
      maltreat_ov24_u5_com
    ),

    maltreat_com_5_14 = fallback_row_sum(
      maltreat_u24_5_14_com,
      maltreat_ov24_5_14_com
    ),

    maltreat_com_ov15 = fallback_row_sum(
      maltreat_u24_ov15_com,
      maltreat_ov24_ov15_com
    ),

    # total des cas traités par groupe d'âge (établissement + communauté)
    maltreat_u5 = fallback_row_sum(maltreat_hf_u5, maltreat_com_u5),
    maltreat_5_14 = fallback_row_sum(maltreat_hf_5_14, maltreat_com_5_14),
    maltreat_ov15 = fallback_row_sum(maltreat_hf_ov15, maltreat_com_ov15),

    # total des cas traités dans les 24 heures (établissement uniquement)
    maltreat_u24_hf = fallback_row_sum(
      maltreat_u24_u5_hf,
      maltreat_u24_5_14_hf,
      maltreat_u24_ov15_hf
    ),

    # total des cas traités après 24 heures (établissement uniquement)
    maltreat_ov24_hf = fallback_row_sum(
      maltreat_ov24_u5_hf,
      maltreat_ov24_5_14_hf,
      maltreat_ov24_ov15_hf
    ),

    # total des cas traités dans les 24 heures (communauté uniquement)
    maltreat_u24_com = fallback_row_sum(
      maltreat_u24_u5_com,
      maltreat_u24_5_14_com,
      maltreat_u24_ov15_com
    ),

    # total des cas traités après 24 heures (communauté uniquement)
    maltreat_ov24_com = fallback_row_sum(
      maltreat_ov24_u5_com,
      maltreat_ov24_5_14_com,
      maltreat_ov24_ov15_com
    ),

    # totaux globaux (établissement + communauté)
    maltreat_u24_total = fallback_row_sum(maltreat_u24_hf, maltreat_u24_com),
    maltreat_ov24_total = fallback_row_sum(maltreat_ov24_hf, maltreat_ov24_com),

    # cas présumés par groupe d'âge
    pres_com_u5 = fallback_diff(maltreat_com_u5, conf_com_u5),
    pres_com_5_14 = fallback_diff(maltreat_com_5_14, conf_com_5_14),
    pres_com_ov15 = fallback_diff(maltreat_com_ov15, conf_com_ov15),

    pres_hf_u5 = fallback_diff(maltreat_hf_u5, conf_hf_u5),
    pres_hf_5_14 = fallback_diff(maltreat_hf_5_14, conf_hf_5_14),
    pres_hf_ov15 = fallback_diff(maltreat_hf_ov15, conf_hf_ov15),

    pres_u5 = fallback_row_sum(pres_com_u5, pres_hf_u5),
    pres_5_14 = fallback_row_sum(pres_com_5_14, pres_hf_5_14),
    pres_ov15 = fallback_row_sum(pres_com_ov15, pres_hf_ov15)
  )

# check to see the aggregation worked
dhis2_df |>
  dplyr::filter(
    record_id %in%
      c("6a29143b", "0e7ba814", "943c5f5f", "4fbe05fd", "40cc411c", "51194842")
  ) |>
  dplyr::arrange(allout) |>
  dplyr::select(
    allout,
    allout_u5,
    allout_ov5,
    pres_hf_u5,
    maltreat_hf_u5,
    conf_hf_u5
  ) |>
  head()
NoteSortie
  allout allout_u5 allout_ov5 pres_hf_u5 maltreat_hf_u5 conf_hf_u5
1      2         2         NA          0             19         19
2     30        NA         30          0             41         58
3    123        NA        123          0            118        118
4    139        86         53         46             NA         46
5    165       156          9         59             NA         59
6    378       256        122         50             50         NA

Pour adapter le code : - Lignes 4–64 : Ajustez les noms de variables pour refléter ceux pertinents pour votre ensemble de données lors du calcul des nouvelles variables. - Une fois mis à jour, exécutez le code.

Afficher le code
# fonction auxiliaire : somme par ligne qui retourne la valeur si une seule est non-NA
def fallback_row_sum(df, cols):
    return df[cols].sum(axis=1, skipna=True, min_count=1)


# fonction auxiliaire : différence avec plancher à 0, gestion des NA comme R
# si les deux valeurs sont NA retourner NA ; si une seule est NA retourner l'autre
# limité à 0 ; si les deux sont présentes retourner max(col1 - col2, 0)
import numpy as np

def fallback_diff(df, col1, col2):
    a = df[col1]
    b = df[col2]
    both_na = a.isna() & b.isna()
    only_b_na = a.notna() & b.isna()
    only_a_na = a.isna() & b.notna()
    result = (a - b).clip(lower=0)
    result = result.where(~only_b_na, a.clip(lower=0))
    result = result.where(~only_a_na, b.clip(lower=0))
    result = result.where(~both_na, np.nan)
    return result


dhis2_df = (
    dhis2_df.assign(
        # consultations ambulatoires
        allout=lambda x: fallback_row_sum(x, ["allout_u5", "allout_ov5"]),
        # cas suspects
        susp=lambda x: fallback_row_sum(
            x,
            [
                "susp_u5_hf",
                "susp_5_14_hf",
                "susp_ov15_hf",
                "susp_u5_com",
                "susp_5_14_com",
                "susp_ov15_com",
            ],
        ),
        # cas testés
        test_hf=lambda x: fallback_row_sum(
            x,
            [
                "test_neg_mic_u5_hf",
                "test_pos_mic_u5_hf",
                "test_neg_mic_5_14_hf",
                "test_pos_mic_5_14_hf",
                "test_neg_mic_ov15_hf",
                "test_pos_mic_ov15_hf",
                "tes_neg_rdt_u5_hf",
                "tes_pos_rdt_u5_hf",
                "tes_neg_rdt_5_14_hf",
                "tes_pos_rdt_5_14_hf",
                "tes_neg_rdt_ov15_hf",
                "tes_pos_rdt_ov15_hf",
            ],
        ),
        test_com=lambda x: fallback_row_sum(
            x,
            [
                "tes_neg_rdt_u5_com",
                "tes_pos_rdt_u5_com",
                "tes_neg_rdt_5_14_com",
                "tes_pos_rdt_5_14_com",
                "tes_neg_rdt_ov15_com",
                "tes_pos_rdt_ov15_com",
            ],
        ),
        # confirmed cases (HF and COM)
        conf_hf=lambda x: fallback_row_sum(
            x,
            [
                "test_pos_mic_u5_hf",
                "test_pos_mic_5_14_hf",
                "test_pos_mic_ov15_hf",
                "tes_pos_rdt_u5_hf",
                "tes_pos_rdt_5_14_hf",
                "tes_pos_rdt_ov15_hf",
            ],
        ),
        conf_com=lambda x: fallback_row_sum(
            x,
            [
                "tes_pos_rdt_u5_com",
                "tes_pos_rdt_5_14_com",
                "tes_pos_rdt_ov15_com",
            ],
        ),
        # cas traités
        maltreat_com=lambda x: fallback_row_sum(
            x,
            [
                "maltreat_u24_u5_com",
                "maltreat_ov24_u5_com",
                "maltreat_u24_5_14_com",
                "maltreat_ov24_5_14_com",
                "maltreat_u24_ov15_com",
                "maltreat_ov24_ov15_com",
            ],
        ),
        maltreat_hf=lambda x: fallback_row_sum(
            x,
            [
                "maltreat_u24_u5_hf",
                "maltreat_ov24_u5_hf",
                "maltreat_u24_5_14_hf",
                "maltreat_ov24_5_14_hf",
                "maltreat_u24_ov15_hf",
                "maltreat_ov24_ov15_hf",
            ],
        ),
        # hospitalisations pour paludisme
        maladm=lambda x: fallback_row_sum(
            x, ["maladm_u5", "maladm_5_14", "maladm_ov15"]
        ),
        # décès dus au paludisme
        maldth=lambda x: fallback_row_sum(
            x,
            [
                "maldth_u5",
                "maldth_1_59m",
                "maldth_10_14",
                "maldth_5_9",
                "maldth_5_14",
                "maldth_ov15",
                "maldth_fem_ov15",
                "maldth_mal_ov15",
            ],
        ),
        # AGE-GROUP SPECIFIC AGGREGATIONS
        # cas testés par groupe d'âge (établissement uniquement)
        test_hf_u5=lambda x: fallback_row_sum(
            x,
            [
                "test_neg_mic_u5_hf",
                "test_pos_mic_u5_hf",
                "tes_neg_rdt_u5_hf",
                "tes_pos_rdt_u5_hf",
            ],
        ),
        test_hf_5_14=lambda x: fallback_row_sum(
            x,
            [
                "test_neg_mic_5_14_hf",
                "test_pos_mic_5_14_hf",
                "tes_neg_rdt_5_14_hf",
                "tes_pos_rdt_5_14_hf",
            ],
        ),
        test_hf_ov15=lambda x: fallback_row_sum(
            x,
            [
                "test_neg_mic_ov15_hf",
                "test_pos_mic_ov15_hf",
                "tes_neg_rdt_ov15_hf",
                "tes_pos_rdt_ov15_hf",
            ],
        ),
        # cas testés par groupe d'âge (communauté uniquement)
        test_com_u5=lambda x: fallback_row_sum(
            x, ["tes_neg_rdt_u5_com", "tes_pos_rdt_u5_com"]
        ),
        test_com_5_14=lambda x: fallback_row_sum(
            x, ["tes_neg_rdt_5_14_com", "tes_pos_rdt_5_14_com"]
        ),
        test_com_ov15=lambda x: fallback_row_sum(
            x, ["tes_neg_rdt_ov15_com", "tes_pos_rdt_ov15_com"]
        ),
        # cas suspects par groupe d'âge (renommer pour cohérence)
        susp_hf_u5=lambda x: x["susp_u5_hf"],
        susp_hf_5_14=lambda x: x["susp_5_14_hf"],
        susp_hf_ov15=lambda x: x["susp_ov15_hf"],
        susp_com_u5=lambda x: x["susp_u5_com"],
        susp_com_5_14=lambda x: x["susp_5_14_com"],
        susp_com_ov15=lambda x: x["susp_ov15_com"],
        # cas confirmés par groupe d'âge (établissement uniquement)
        conf_hf_u5=lambda x: fallback_row_sum(
            x, ["test_pos_mic_u5_hf", "tes_pos_rdt_u5_hf"]
        ),
        conf_hf_5_14=lambda x: fallback_row_sum(
            x, ["test_pos_mic_5_14_hf", "tes_pos_rdt_5_14_hf"]
        ),
        conf_hf_ov15=lambda x: fallback_row_sum(
            x, ["test_pos_mic_ov15_hf", "tes_pos_rdt_ov15_hf"]
        ),
        # cas confirmés par groupe d'âge (communauté uniquement)
        conf_com_u5=lambda x: x["tes_pos_rdt_u5_com"],
        conf_com_5_14=lambda x: x["tes_pos_rdt_5_14_com"],
        conf_com_ov15=lambda x: x["tes_pos_rdt_ov15_com"],
        # cas traités par groupe d'âge (établissement uniquement)
        maltreat_hf_u5=lambda x: fallback_row_sum(
            x, ["maltreat_u24_u5_hf", "maltreat_ov24_u5_hf"]
        ),
        maltreat_hf_5_14=lambda x: fallback_row_sum(
            x, ["maltreat_u24_5_14_hf", "maltreat_ov24_5_14_hf"]
        ),
        maltreat_hf_ov15=lambda x: fallback_row_sum(
            x, ["maltreat_u24_ov15_hf", "maltreat_ov24_ov15_hf"]
        ),
        # cas traités par groupe d'âge (communauté uniquement)
        maltreat_com_u5=lambda x: fallback_row_sum(
            x, ["maltreat_u24_u5_com", "maltreat_ov24_u5_com"]
        ),
        maltreat_com_5_14=lambda x: fallback_row_sum(
            x, ["maltreat_u24_5_14_com", "maltreat_ov24_5_14_com"]
        ),
        maltreat_com_ov15=lambda x: fallback_row_sum(
            x, ["maltreat_u24_ov15_com", "maltreat_ov24_ov15_com"]
        ),
        # Total treated cases within/after 24 hours
        maltreat_u24_hf=lambda x: fallback_row_sum(
            x,
            ["maltreat_u24_u5_hf", "maltreat_u24_5_14_hf", "maltreat_u24_ov15_hf"],
        ),
        maltreat_ov24_hf=lambda x: fallback_row_sum(
            x,
            ["maltreat_ov24_u5_hf", "maltreat_ov24_5_14_hf", "maltreat_ov24_ov15_hf"],
        ),
        maltreat_u24_com=lambda x: fallback_row_sum(
            x,
            [
                "maltreat_u24_u5_com",
                "maltreat_u24_5_14_com",
                "maltreat_u24_ov15_com",
            ],
        ),
        maltreat_ov24_com=lambda x: fallback_row_sum(
            x,
            [
                "maltreat_ov24_u5_com",
                "maltreat_ov24_5_14_com",
                "maltreat_ov24_ov15_com",
            ],
        ),
    )
    .assign(
        # second pass: computed from first pass columns
        test=lambda x: fallback_row_sum(x, ["test_hf", "test_com"]),
        conf=lambda x: fallback_row_sum(x, ["conf_hf", "conf_com"]),
        maltreat=lambda x: fallback_row_sum(x, ["maltreat_hf", "maltreat_com"]),
        # totaux by age group
        test_u5=lambda x: fallback_row_sum(x, ["test_hf_u5", "test_com_u5"]),
        test_5_14=lambda x: fallback_row_sum(x, ["test_hf_5_14", "test_com_5_14"]),
        test_ov15=lambda x: fallback_row_sum(x, ["test_hf_ov15", "test_com_ov15"]),
        susp_u5=lambda x: fallback_row_sum(x, ["susp_hf_u5", "susp_com_u5"]),
        susp_5_14=lambda x: fallback_row_sum(x, ["susp_hf_5_14", "susp_com_5_14"]),
        susp_ov15=lambda x: fallback_row_sum(x, ["susp_hf_ov15", "susp_com_ov15"]),
        conf_u5=lambda x: fallback_row_sum(x, ["conf_hf_u5", "conf_com_u5"]),
        conf_5_14=lambda x: fallback_row_sum(x, ["conf_hf_5_14", "conf_com_5_14"]),
        conf_ov15=lambda x: fallback_row_sum(x, ["conf_hf_ov15", "conf_com_ov15"]),
        maltreat_u5=lambda x: fallback_row_sum(
            x, ["maltreat_hf_u5", "maltreat_com_u5"]
        ),
        maltreat_5_14=lambda x: fallback_row_sum(
            x, ["maltreat_hf_5_14", "maltreat_com_5_14"]
        ),
        maltreat_ov15=lambda x: fallback_row_sum(
            x, ["maltreat_hf_ov15", "maltreat_com_ov15"]
        ),
        maltreat_u24_total=lambda x: fallback_row_sum(
            x, ["maltreat_u24_hf", "maltreat_u24_com"]
        ),
        maltreat_ov24_total=lambda x: fallback_row_sum(
            x, ["maltreat_ov24_hf", "maltreat_ov24_com"]
        ),
        # cas présumés
        pres_com=lambda x: fallback_diff(x, "maltreat_com", "conf_com"),
        pres_hf=lambda x: fallback_diff(x, "maltreat_hf", "conf_hf"),
        pres_com_u5=lambda x: fallback_diff(x, "maltreat_com_u5", "conf_com_u5"),
        pres_com_5_14=lambda x: fallback_diff(x, "maltreat_com_5_14", "conf_com_5_14"),
        pres_com_ov15=lambda x: fallback_diff(x, "maltreat_com_ov15", "conf_com_ov15"),
        pres_hf_u5=lambda x: fallback_diff(x, "maltreat_hf_u5", "conf_hf_u5"),
        pres_hf_5_14=lambda x: fallback_diff(x, "maltreat_hf_5_14", "conf_hf_5_14"),
        pres_hf_ov15=lambda x: fallback_diff(x, "maltreat_hf_ov15", "conf_hf_ov15"),
    )
    .assign(
        # third pass: totals from presumed
        pres=lambda x: fallback_row_sum(x, ["pres_com", "pres_hf"]),
        pres_u5=lambda x: fallback_row_sum(x, ["pres_com_u5", "pres_hf_u5"]),
        pres_5_14=lambda x: fallback_row_sum(x, ["pres_com_5_14", "pres_hf_5_14"]),
        pres_ov15=lambda x: fallback_row_sum(x, ["pres_com_ov15", "pres_hf_ov15"]),
    )
)

# inspect results
(
    dhis2_df[
        dhis2_df["record_id"].isin(
            ["6a29143b", "0e7ba814", "943c5f5f", "4fbe05fd", "40cc411c", "51194842"]
        )
    ][
        [
            "allout",
            "allout_u5",
            "allout_ov5",
            "pres_hf_u5",
            "maltreat_hf_u5",
            "conf_hf_u5",
        ]
    ]
    .sort_values("allout")
    .head(6)
)
NoteSortie
Empty DataFrame
Columns: [allout, allout_u5, allout_ov5, pres_hf_u5, maltreat_hf_u5, conf_hf_u5]
Index: []

Pour adapter le code :

  • Lignes 11–270 : Ajustez les noms de variables pour refléter ceux pertinents pour votre ensemble de données lors du calcul des nouvelles variables.

Une fois mis à jour, exécutez le code.

Nous pouvons voir que l’agrégation a fonctionné comme prévu avec nos fonctions de secours. La colonne allout est la somme de allout_u5 et allout_ov5. Quand l’une des valeurs est NA, la valeur non-NA est préservée plutôt que de renvoyer NA. De même, pres_hf_u5 est dérivé de la différence entre maltreat_hf_u5 et conf_hf_u5. Quand l’une des valeurs est NA, le résultat reflète les données disponibles plutôt que de renvoyer NA par défaut.

Étape 6.2 : Contrôle qualité des totaux des indicateurs

Vérifions maintenant que les totaux des indicateurs sont égaux à la somme de leurs composantes désagrégées. Le bloc de code ci-dessous compte le nombre de lignes où le total de l’indicateur n’est pas égal à la somme de ses composantes. Par exemple, les lignes où allout n’est pas égal à la somme de allout_u5 et allout_ov5.

WarningPourquoi vérifier les totaux des indicateurs ?

Si tous les totaux ont été manuellement calculés lors de l’étape précédente, nous pouvons choisir de sauter cette étape. Cependant, même si les totaux ont été calculés manuellement à l’étape précédente, leur vérification ici peut aider à identifier des erreurs.

Dans le cas où les colonnes de totaux sont extraites de DHIS2, il est nécessaire d’effectuer ce contrôle qualité pour la cohérence. Cette vérification confirme quelles composantes entrent dans les totaux agrégés extraits de DHIS2.

  • R
  • Python
Afficher le code
# créer des indicateurs de divergence pour chaque groupe d'indicateurs
mismatch_summary <- dhis2_df |>
  dplyr::summarise(
    # vérification des consultations ambulatoires
    allout_mismatch = sum(
      allout != (allout_u5 + allout_ov5),
      na.rm = TRUE
    ),

    # vérification des hospitalisations pour paludisme
    maladm_mismatch = sum(
      maladm != (maladm_u5 + maladm_5_14 + maladm_ov15),
      na.rm = TRUE
    ),

    # vérification du total des tests
    test_mismatch = sum(
      test != (test_hf + test_com),
      na.rm = TRUE
    ),

    # vérification du total des cas confirmés
    conf_mismatch = sum(
      conf != (conf_hf + conf_com),
      na.rm = TRUE
    ),

    # vérification du total des cas traités
    maltreat_mismatch = sum(
      maltreat != (maltreat_hf + maltreat_com),
      na.rm = TRUE
    ),

    # vérification du total des cas présumés
    pres_mismatch = sum(
      pres != (pres_hf + pres_com),
      na.rm = TRUE
    ),

    # vérification des décès dus au paludisme
    maldth_mismatch = sum(
      maldth !=
        (maldth_1_59m +
          maldth_u5 +
          maldth_5_9 +
          maldth_10_14 +
          maldth_5_14 +
          maldth_fem_ov15 +
          maldth_mal_ov15 +
          maldth_ov15),
      na.rm = TRUE
    ),

    # cas testés par groupe d'âge
    test_u5_mismatch = sum(
      test_u5 != (test_hf_u5 + test_com_u5),
      na.rm = TRUE
    ),
    test_5_14_mismatch = sum(
      test_5_14 != (test_hf_5_14 + test_com_5_14),
      na.rm = TRUE
    ),
    test_ov15_mismatch = sum(
      test_ov15 != (test_hf_ov15 + test_com_ov15),
      na.rm = TRUE
    ),

    # cas confirmés par groupe d'âge
    conf_u5_mismatch = sum(
      conf_u5 != (conf_hf_u5 + conf_com_u5),
      na.rm = TRUE
    ),
    conf_5_14_mismatch = sum(
      conf_5_14 != (conf_hf_5_14 + conf_com_5_14),
      na.rm = TRUE
    ),
    conf_ov15_mismatch = sum(
      conf_ov15 != (conf_hf_ov15 + conf_com_ov15),
      na.rm = TRUE
    ),

    # cas présumés par groupe d'âge
    pres_u5_mismatch = sum(
      pres_u5 != (pres_hf_u5 + pres_com_u5),
      na.rm = TRUE
    ),
    pres_5_14_mismatch = sum(
      pres_5_14 != (pres_hf_5_14 + pres_com_5_14),
      na.rm = TRUE
    ),
    pres_ov15_mismatch = sum(
      pres_ov15 != (pres_hf_ov15 + pres_com_ov15),
      na.rm = TRUE
    ),

    # cas suspects par groupe d'âge
    susp_u5_mismatch = sum(
      susp_u5 != (susp_u5_hf + susp_u5_com),
      na.rm = TRUE
    ),
    susp_5_14_mismatch = sum(
      susp_5_14 != (susp_5_14_hf + susp_5_14_com),
      na.rm = TRUE
    ),
    susp_ov15_mismatch = sum(
      susp_ov15 != (susp_ov15_hf + susp_ov15_com),
      na.rm = TRUE
    ),

    # cas traités par groupe d'âge
    maltreat_u5_mismatch = sum(
      maltreat_u5 != (maltreat_hf_u5 + maltreat_com_u5),
      na.rm = TRUE
    ),
    maltreat_5_14_mismatch = sum(
      maltreat_5_14 != (maltreat_hf_5_14 + maltreat_com_5_14),
      na.rm = TRUE
    ),
    maltreat_ov15_mismatch = sum(
      maltreat_ov15 != (maltreat_hf_ov15 + maltreat_com_ov15),
      na.rm = TRUE
    ),

    # vérifications du calendrier de traitement
    maltreat_u24_mismatch = sum(
      maltreat_u24_total != (maltreat_u24_hf + maltreat_u24_com),
      na.rm = TRUE
    ),
    maltreat_ov24_mismatch = sum(
      maltreat_ov24_total != (maltreat_ov24_hf + maltreat_ov24_com),
      na.rm = TRUE
    )
  ) |>
  # pivoter au format long pour une lecture facilitée
  tidyr::pivot_longer(
    cols = dplyr::everything(),
    names_to = "indicator",
    values_to = "n_mismatches"
  ) |>
  # filtrer pour afficher uniquement les indicateurs avec divergences
  dplyr::filter(n_mismatches > 0)

mismatch_summary
NoteSortie

Note : comme ce code affiche les lignes où les totaux des indicateurs ne sont pas égaux à la somme de leurs composantes, nous devons nous attendre à voir une sortie avec <0 rows> si tous les calculs sont corrects. Si des lignes apparaissent dans la sortie, utilisez hf et periodname pour approfondir l’investigation de la divergence.

# A tibble: 0 × 2
# ℹ 2 variables: indicator <chr>, n_mismatches <int>

Pour adapter le code :

  • Ligne 3 : Remplacez dhis2_df par votre ensemble de données cible.
  • Lignes 6–52 : Ajustez les vérifications des indicateurs pour correspondre à vos données. Chaque vérification compare une colonne de total à la somme de ses composantes. Ajoutez ou supprimez des vérifications selon les indicateurs disponibles dans votre ensemble de données.
  • Lignes 54–126 : Ces vérifications concernent les indicateurs désagrégés par âge. Modifiez les noms de colonnes pour correspondre à vos conventions de dénomination des groupes d’âge (par exemple, _u5, _5_14, _ov15).
  • Ligne 135 : La sortie filtre pour afficher uniquement les indicateurs avec des divergences. Supprimez ce filtre pour voir tous les indicateurs quel que soit leur statut de divergence.
Afficher le code
# créer des indicateurs de divergence pour chaque groupe d'indicateurs
# helper function to count mismatches, ignoring NAs
def count_mismatch(total, components):
    """Compter les divergences entre le total et la somme des composantes, en ignorant les NA."""
    calculated = components.sum(axis=1)
    # comparer uniquement là où le total et le calculé ne sont pas NA
    mask = total.notna() & calculated.notna()
    return (total[mask] != calculated[mask]).sum()

mismatch_summary = pd.DataFrame({
    # vérification des consultations ambulatoires
    "allout_mismatch": [
        count_mismatch(
            dhis2_df["allout"],
            dhis2_df[["allout_u5", "allout_ov5"]]
        )
    ],

    # vérification des hospitalisations pour paludisme
    "maladm_mismatch": [
        count_mismatch(
            dhis2_df["maladm"],
            dhis2_df[["maladm_u5", "maladm_5_14", "maladm_ov15"]]
        )
    ],

    # vérification du total des tests
    "test_mismatch": [
        count_mismatch(
            dhis2_df["test"],
            dhis2_df[["test_hf", "test_com"]]
        )
    ],

    # vérification du total des cas confirmés
    "conf_mismatch": [
        count_mismatch(
            dhis2_df["conf"],
            dhis2_df[["conf_hf", "conf_com"]]
        )
    ],

    # vérification du total des cas traités
    "maltreat_mismatch": [
        count_mismatch(
            dhis2_df["maltreat"],
            dhis2_df[["maltreat_hf", "maltreat_com"]]
        )
    ],

    # vérification du total des cas présumés
    "pres_mismatch": [
        count_mismatch(
            dhis2_df["pres"],
            dhis2_df[["pres_hf", "pres_com"]]
        )
    ],

    # vérification des décès dus au paludisme
    "maldth_mismatch": [
        count_mismatch(
            dhis2_df["maldth"],
            dhis2_df[[
                "maldth_1_59m", "maldth_u5", "maldth_5_9", "maldth_10_14",
                "maldth_5_14", "maldth_fem_ov15", "maldth_mal_ov15", "maldth_ov15"
            ]]
        )
    ],

    # cas testés par groupe d'âge
    "test_u5_mismatch": [
        count_mismatch(
            dhis2_df["test_u5"],
            dhis2_df[["test_hf_u5", "test_com_u5"]]
        )
    ],
    "test_5_14_mismatch": [
        count_mismatch(
            dhis2_df["test_5_14"],
            dhis2_df[["test_hf_5_14", "test_com_5_14"]]
        )
    ],
    "test_ov15_mismatch": [
        count_mismatch(
            dhis2_df["test_ov15"],
            dhis2_df[["test_hf_ov15", "test_com_ov15"]]
        )
    ],

    # cas confirmés par groupe d'âge
    "conf_u5_mismatch": [
        count_mismatch(
            dhis2_df["conf_u5"],
            dhis2_df[["conf_hf_u5", "conf_com_u5"]]
        )
    ],
    "conf_5_14_mismatch": [
        count_mismatch(
            dhis2_df["conf_5_14"],
            dhis2_df[["conf_hf_5_14", "conf_com_5_14"]]
        )
    ],
    "conf_ov15_mismatch": [
        count_mismatch(
            dhis2_df["conf_ov15"],
            dhis2_df[["conf_hf_ov15", "conf_com_ov15"]]
        )
    ],

    # cas présumés par groupe d'âge
    "pres_u5_mismatch": [
        count_mismatch(
            dhis2_df["pres_u5"],
            dhis2_df[["pres_hf_u5", "pres_com_u5"]]
        )
    ],
    "pres_5_14_mismatch": [
        count_mismatch(
            dhis2_df["pres_5_14"],
            dhis2_df[["pres_hf_5_14", "pres_com_5_14"]]
        )
    ],
    "pres_ov15_mismatch": [
        count_mismatch(
            dhis2_df["pres_ov15"],
            dhis2_df[["pres_hf_ov15", "pres_com_ov15"]]
        )
    ],

    # cas suspects par groupe d'âge
    "susp_u5_mismatch": [
        count_mismatch(
            dhis2_df["susp_u5"],
            dhis2_df[["susp_hf_u5", "susp_com_u5"]]
        )
    ],
    "susp_5_14_mismatch": [
        count_mismatch(
            dhis2_df["susp_5_14"],
            dhis2_df[["susp_hf_5_14", "susp_com_5_14"]]
        )
    ],
    "susp_ov15_mismatch": [
        count_mismatch(
            dhis2_df["susp_ov15"],
            dhis2_df[["susp_hf_ov15", "susp_com_ov15"]]
        )
    ],

    # cas traités par groupe d'âge
    "maltreat_u5_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_u5"],
            dhis2_df[["maltreat_hf_u5", "maltreat_com_u5"]]
        )
    ],
    "maltreat_5_14_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_5_14"],
            dhis2_df[["maltreat_hf_5_14", "maltreat_com_5_14"]]
        )
    ],
    "maltreat_ov15_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_ov15"],
            dhis2_df[["maltreat_hf_ov15", "maltreat_com_ov15"]]
        )
    ],

    # vérifications du calendrier de traitement
    "maltreat_u24_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_u24_total"],
            dhis2_df[["maltreat_u24_hf", "maltreat_u24_com"]]
        )
    ],
    "maltreat_ov24_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_ov24_total"],
            dhis2_df[["maltreat_ov24_hf", "maltreat_ov24_com"]]
        )
    ]
})

# pivot to long format for easier viewing
mismatch_summary = (
    mismatch_summary
    .melt(var_name="indicator", value_name="n_mismatches")
    # filtrer pour afficher uniquement les indicateurs avec divergences
    .query("n_mismatches > 0")
)

mismatch_summary
NoteSortie

Note : comme ce code affiche les lignes où les totaux des indicateurs ne sont pas égaux à la somme de leurs composantes, nous devons nous attendre à voir une sortie avec <0 rows> si tous les calculs sont corrects. Si des lignes apparaissent dans la sortie, utilisez hf et periodname pour approfondir l’investigation de la divergence.

Empty DataFrame
Columns: [indicator, n_mismatches]
Index: []

Pour adapter le code :

  • Lignes 10–156 : Remplacez dhis2_df par votre ensemble de données cible.
  • Lignes 10–58 : Ajustez les vérifications des indicateurs pour correspondre à vos données. Chaque vérification compare une colonne de total à la somme de ses composantes. Ajoutez ou supprimez des vérifications selon les indicateurs disponibles dans votre ensemble de données.
  • Lignes 60–156 : Ces vérifications concernent les indicateurs désagrégés par âge. Modifiez les noms de colonnes pour correspondre à vos conventions de dénomination des groupes d’âge (par exemple, _u5, _5_14, _ov15).
  • Ligne 164 : La sortie filtre pour afficher uniquement les indicateurs avec des divergences. Supprimez .query("n_mismatches > 0") pour voir tous les indicateurs quel que soit leur statut de divergence.

Étape 6.3 : Exporter les lignes avec des totaux incohérents

Si des totaux incohérents sont détectés, ils peuvent être exportés pour une évaluation plus approfondie à l’aide du code ci-dessous. Les sorties exportées doivent être partagées avec l’équipe SNT pour révision et conseils.

  • R
  • Python
Afficher le code
# identifier les lignes avec des totaux incohérents
incoherent_rows <- dhis2_df |>
  dplyr::filter(
    # vérification des consultations ambulatoires
    allout != (allout_u5 + allout_ov5) |
    # vérification des hospitalisations pour paludisme
    maladm != (maladm_u5 + maladm_5_14 + maladm_ov15) |
    # vérification du total des tests
    test != (test_hf + test_com) |
    # vérification du total des cas confirmés
    conf != (conf_hf + conf_com) |
    # vérification du total des cas traités
    maltreat != (maltreat_hf + maltreat_com) |
    # vérification du total des cas présumés
    pres != (pres_hf + pres_com)
  ) |>
  dplyr::select(
    hf, periodname,
    dplyr::matches("allout|maladm|test|conf|maltreat|pres")
  )

# définir le chemin pour la sauvegarde
output_file <- here::here(
  "1.1.2_epidemiology",
  "1.1.2a_routine_surveillance",
  "processed",
  "sle_incoherent_totals_dhis2.xlsx"
)
# exporter en xlsx
rio::export(incoherent_rows, file = output_file)

Pour adapter le code :

  • Ligne 2 : Remplacez dhis2_df par votre ensemble de données cible.
  • Lignes 3–16 : Ajustez les conditions de filtrage pour correspondre aux totaux et composantes de vos indicateurs.
  • Lignes 23–29 : Mettez à jour le chemin du fichier de sortie vers votre emplacement préféré.

Une fois mis à jour, exécutez le code pour exporter les lignes avec des totaux incohérents pour révision avec l’équipe SNT.

Afficher le code
# identifier les lignes avec des totaux incohérents
incoherent_rows = dhis2_df[
    # vérification des consultations ambulatoires
    (dhis2_df["allout"] != (dhis2_df["allout_u5"] + dhis2_df["allout_ov5"])) |
    # vérification des hospitalisations pour paludisme
    (dhis2_df["maladm"] != (dhis2_df["maladm_u5"] + dhis2_df["maladm_5_14"] + dhis2_df["maladm_ov15"])) |
    # vérification du total des tests
    (dhis2_df["test"] != (dhis2_df["test_hf"] + dhis2_df["test_com"])) |
    # vérification du total des cas confirmés
    (dhis2_df["conf"] != (dhis2_df["conf_hf"] + dhis2_df["conf_com"])) |
    # vérification du total des cas traités
    (dhis2_df["maltreat"] != (dhis2_df["maltreat_hf"] + dhis2_df["maltreat_com"])) |
    # vérification du total des cas présumés
    (dhis2_df["pres"] != (dhis2_df["pres_hf"] + dhis2_df["pres_com"]))
]

# select relevant columns
incoherent_rows = incoherent_rows[
    ["hf", "periodname"] +
    [col for col in incoherent_rows.columns
     if any(x in col for x in ["allout", "maladm", "test", "conf", "maltreat", "pres"])]
]

# définir le chemin pour la sauvegarde
output_file = Path(
    "1.1.2_epidemiology",
    "1.1.2a_routine_surveillance",
    "processed",
    "sle_incoherent_totals_dhis2.xlsx"
)

# exporter en xlsx
incoherent_rows.to_excel(output_file, index=False)

Pour adapter le code :

  • Ligne 2 : Remplacez dhis2_df par votre ensemble de données cible.
  • Lignes 2–15 : Ajustez les conditions de filtrage pour correspondre aux totaux et composantes de vos indicateurs.
  • Lignes 25–30 : Mettez à jour le chemin du fichier de sortie vers votre emplacement préféré.

Une fois mis à jour, exécutez le code pour exporter les lignes avec des totaux incohérents pour révision avec l’équipe SNT.

Étape 6.4 : Ajouter la spécification IPD/OPD

Il peut être utile dans les analyses ultérieures de filtrer les données de routine pour ne conserver que les établissements ou départements hospitaliers (IPD) ou ambulatoires (OPD). Par exemple, nous pouvons vouloir analyser les admissions pour paludisme uniquement dans les établissements ayant une capacité d’hospitalisation, ou nous concentrer sur les indicateurs de prise en charge ambulatoire des établissements de soins primaires.

Ici, nous créons une colonne qui spécifie si un établissement fournit des services hospitaliers (IPD) ou ambulatoires (OPD). Cette classification peut être basée sur :

  • Les conventions de dénomination des types d’établissements (par exemple, « Hôpital » = IPD, « CHP » = OPD)
  • La présence d’indicateurs hospitaliers (par exemple, les établissements déclarant des admissions ou des décès)
  • Un fichier de référence de l’équipe SNT ou du ministère de la santé
WarningConsulter l’équipe SNT

La classification des établissements varie selon les pays. Confirmez avec l’équipe SNT quels types d’établissements doivent être classés comme IPD ou OPD. Certains établissements peuvent offrir les deux services et nécessiter un traitement spécial.

  • R
  • Python
Afficher le code
# option 1 : classifier selon les patterns de noms d'établissements
dhis2_df <- dhis2_df |>
  dplyr::mutate(
    facility_type = dplyr::case_when(
      # établissements hospitaliers (hôpitaux)
      stringr::str_detect(
        hf,
        regex("hospital|hosp", ignore_case = TRUE)
      ) ~ "IPD",
      stringr::str_detect(
        hf,
        regex("district hospital|regional hospital", ignore_case = TRUE)
      ) ~ "IPD",
      # établissements ambulatoires (cliniques,
      # postes de santé, santé communautaire)
      stringr::str_detect(
        hf,
        regex(
          "CHP|CHC|MCHP|clinic|health post|health centre",
          ignore_case = TRUE
        )
      ) ~ "OPD",
      # par défaut OPD si aucun pattern ne correspond
      TRUE ~ "OPD"
    )
  )

# option 2 : classifier selon la présence d'indicateurs hospitaliers
dhis2_df <- dhis2_df |>
  dplyr::group_by(hf_uid) |>
  dplyr::mutate(
    # l'établissement est IPD s'il déclare au moins une admission ou un décès
    has_ipd = any(!is.na(maladm) & maladm > 0, na.rm = TRUE) |
      any(!is.na(maldth) & maldth > 0, na.rm = TRUE),
    facility_type = dplyr::if_else(has_ipd, "IPD", "OPD")
  ) |>
  dplyr::ungroup() |>
  dplyr::select(-has_ipd)

# option 3 : utiliser un fichier de référence de correspondance
hf_path <-
  rio::import(
    here::here(
      "01_data",
      "1.1_foundational",
      "1.1b_health_facilities",
      "processed",
      "health_facility_master_list.xlsx"
    )
  )

dhis2_df <- dhis2_df |>
  dplyr::left_join(
    facility_lookup |> dplyr::select(hf_uid, facility_type),
    by = "hf_uid"
  )

# vérifier la distribution de la classification
dhis2_df |>
  dplyr::distinct(hf_uid, facility_type) |>
  dplyr::count(facility_type)
NoteSortie
  facility_type    n
1           IPD   89
2           OPD 1682

Pour adapter le code :

  • Ligne 2 : Remplacez dhis2_df par votre ensemble de données cible.
  • Lignes 6–23 (Option 1) : Ajustez les patterns str_detect() pour correspondre aux conventions de dénomination des établissements de votre pays (par exemple, “dispensary”, “health center”, “referral hospital”).
  • Lignes 32–33 (Option 2) : Modifiez les colonnes d’indicateurs (maladm, maldth) si votre ensemble de données utilise des noms différents pour les indicateurs hospitaliers.
  • Lignes 42–55 (Option 3) : Mettez à jour le chemin vers votre fichier de référence de classification des établissements si vous utilisez une liste de référence de l’équipe SNT.

Choisissez l’option qui correspond le mieux à vos données et à votre contexte. L’Option 3 (référence lookup) est recommandée lorsqu’une classification officielle des établissements existe.

Afficher le code
# option 1 : classifier selon les patterns de noms d'établissements
def classify_facility(hf_name):
    """Classifier l'établissement comme IPD ou OPD selon les patterns de noms."""
    hf_lower = hf_name.lower() if pd.notna(hf_name) else ""

    # inpatient facilities (hospitals)
    if any(x in hf_lower for x in ["hospital", "hosp"]):
        return "IPD"
    # outpatient facilities (clinics, health posts, community health)
    elif any(x in hf_lower for x in ["chp", "chc", "mchp", "clinic", "health post", "health centre"]):
        return "OPD"
    # default to OPD
    else:
        return "OPD"

dhis2_df["facility_type"] = dhis2_df["hf"].apply(classify_facility)

# option 2 : classifier selon la présence d'indicateurs hospitaliers
has_ipd = (
    dhis2_df
    .groupby("hf_uid")
    .apply(
        lambda x: ((x["maladm"].notna() & (x["maladm"] > 0)).any() |
                   (x["maldth"].notna() & (x["maldth"] > 0)).any())
    )
    .reset_index(name="has_ipd")
)

dhis2_df = dhis2_df.merge(has_ipd, on="hf_uid", how="left")
dhis2_df["facility_type"] = dhis2_df["has_ipd"].map({True: "IPD", False: "OPD"})
dhis2_df = dhis2_df.drop(columns="has_ipd")

# option 3 : utiliser un fichier de référence de correspondance
facility_lookup = pd.read_excel(
    Path("path/to/facility_classification.xlsx")
)

dhis2_df = dhis2_df.merge(
    facility_lookup[["hf_uid", "facility_type"]],
    on="hf_uid",
    how="left"
)

# vérifier la distribution de la classification
dhis2_df[["hf_uid", "facility_type"]].drop_duplicates()["facility_type"].value_counts()
NoteSortie
  facility_type     n
0           OPD  1682
1           IPD    89

Pour adapter le code :

  • Lignes 6–13 (Option 1) : Ajustez les patterns str_detect() / chaînes de caractères pour correspondre aux conventions de dénomination des établissements de votre pays (par exemple, “dispensary”, “health center”, “referral hospital”).
  • Lignes 17–25 (Option 2) : Modifiez les colonnes d’indicateurs (maladm, maldth) si votre ensemble de données utilise des noms différents pour les indicateurs d’hospitalisation.
  • Lignes 28–35 (Option 3) : Mettez à jour le chemin vers votre fichier de référence de classification des établissements si vous utilisez une liste de référence de l’équipe SNT.

Choisissez l’option qui correspond le mieux à vos données et à votre contexte. L’Option 3 (référence lookup) est recommandée lorsqu’une classification officielle des établissements existe.

Étape 7 : Finaliser les données

Étape 7.1 : Résoudre les enregistrements en double établissement-mois avec des données différentes

Nous recherchons ici les lignes de l’ensemble de données qui correspondent au même rapport établissement-mois mais qui ont des données différentes. Si des doublons sont trouvés, ils doivent être nettoyés à ce stade pour s’assurer que l’ensemble de données ne contient qu’un seul rapport pour chaque combinaison établissement-mois. Attendez-vous à travailler en étroite collaboration avec l’équipe SNT ou le point focal DHIS2 à ce stade pour vous assurer que les décisions de gestion des données correctes sont prises.

Les enregistrements en double établissement-mois peuvent survenir suite à plusieurs soumissions de saisie de données pour la même période de notification, à des importations de données provenant de différentes sources combinées, à des erreurs système lors de l’extraction DHIS2, ou à des établissements qui déclarent à la fois sur papier et par voie électronique.

Les doublons non résolus gonfleront les comptages de cas et fausseront les indicateurs. Les identifier et les résoudre est nécessaire avant toute analyse.

  • R
  • Python
Afficher le code
# identifier les combinaisons établissement-mois en double
duplicates <- dhis2_df |>
  dplyr::group_by(record_id) |>
  dplyr::filter(dplyr::n() > 1) |>
  dplyr::ungroup()

# compter le nombre de paires de doublons
n_duplicates <- duplicates |>
  dplyr::distinct(record_id) |>
  nrow()

# si des doublons existent, les inspecter
if (n_duplicates > 0) {
  # afficher les enregistrements en double avec les indicateurs clés
  duplicate_details <- duplicates |>
    dplyr::select(
      record_id,
      adm0, adm1, adm2, adm3, hfm yearmon,
      allout,
      test,
      conf,
      maltreat
    ) |>
    dplyr::arrange(record_id)

  print(duplicate_details)

  # exporter pour révision avec l'équipe SNT
  rio::export(
    duplicate_details,
    here::here(
      "1.1.2_epidemiology",
      "1.1.2a_routine_surveillance",
      "processed",
      "sle_duplicate_records_dhis2.xlsx"
    )
  )
}

# option 1 : conserver le premier enregistrement
# (si les doublons sont des copies exactes)
dhis2_df <- dhis2_df |>
  dplyr::distinct(record_id, .keep_all = TRUE)

# option 2 : conserver l'enregistrement le plus complet
dhis2_df <- dhis2_df |>
  dplyr::group_by(record_id) |>
  dplyr::slice_max(
    # compter les valeurs non-NA dans les colonnes d'indicateurs
    order_by = rowSums(!is.na(dplyr::across(dplyr::where(is.numeric)))),
    n = 1,
    with_ties = FALSE
  ) |>
  dplyr::ungroup()

# option 3 : additionner les doublons
# (si les enregistrements représentent des rapports partiels)
dhis2_df <- dhis2_df |>
  dplyr::group_by(record_id, hf, adm0, adm1, adm2, adm3) |>
  dplyr::summarise(
    dplyr::across(dplyr::where(is.numeric), ~ sum(.x, na.rm = TRUE)),
    .groups = "drop"
  )

Pour adapter le code :

  • Ligne 2 : Remplacez dhis2_df par votre ensemble de données cible.
  • Lignes 18–21 : Ajustez les colonnes sélectionnées pour l’inspection des doublons selon vos indicateurs clés.
  • Lignes 26–33 : Mettez à jour le chemin du fichier de sortie pour l’exportation des doublons.
  • Lignes 36–56 : Choisissez l’option de résolution appropriée selon votre contexte.

Une fois mis à jour, exécutez le code pour identifier et résoudre les enregistrements en double établissement-mois.

Afficher le code
# identifier les combinaisons établissement-mois en double
duplicates = dhis2_df.groupby(["hf_uid", "yearmon"]).filter(lambda x: len(x) > 1)

# compter le nombre de paires de doublons
n_duplicates = duplicates[["hf_uid", "yearmon"]].drop_duplicates().shape[0]

# si des doublons existent, les inspecter
if n_duplicates > 0:
    # afficher les enregistrements en double avec les indicateurs clés
    duplicate_details = duplicates[
        ["record_id", "allout", "test", "conf", "maltreat"]
    ].sort_values(["hf_uid", "yearmon"])

    print(duplicate_details)

    # exporter pour révision avec l'équipe SNT
    duplicate_details.to_excel(
        Path(
            "1.1.2_epidemiology",
            "1.1.2a_routine_surveillance",
            "processed",
            "sle_duplicate_records_dhis2.xlsx",
        ),
        index=False,
    )

# option 1 : conserver le premier enregistrement (si les doublons sont des copies exactes)
dhis2_df = dhis2_df.drop_duplicates(subset=["hf_uid", "yearmon"], keep="first")

# option 2 : conserver l'enregistrement le plus complet
dhis2_df = (
    dhis2_df.assign(
        n_complete=lambda x: x.select_dtypes(include="number").notna().sum(axis=1)
    )
    .sort_values("n_complete", ascending=False)
    .drop_duplicates(subset=["record_id"], keep="first")
    .drop(columns="n_complete")
)

# option 3 : additionner les doublons (si les enregistrements représentent des rapports partiels)
group_cols = ["hf_uid", "yearmon", "hf", "adm0", "adm1", "adm2", "adm3"]
numeric_cols = dhis2_df.select_dtypes(include="number").columns.tolist()

dhis2_df = dhis2_df.groupby(group_cols, as_index=False)[numeric_cols].sum()

# verify duplicates resolved
n_remaining = dhis2_df.groupby(["record_id"]).filter(lambda x: len(x) > 1).shape[0]

Pour adapter le code :

  • Ligne 2 : Remplacez dhis2_df par votre ensemble de données cible.
  • Lignes 10–12 : Ajustez les colonnes sélectionnées pour l’inspection des doublons selon vos indicateurs clés.
  • Lignes 17–25 : Mettez à jour le chemin du fichier de sortie pour l’exportation des doublons.
  • Lignes 36–56 : Choisissez l’option de résolution appropriée selon votre contexte.

Une fois mis à jour, exécutez le code pour identifier et résoudre les enregistrements en double établissement-mois.

TipChoisir une stratégie de résolution

Consultez l’équipe SNT avant de choisir comment résoudre les doublons. L’approche correcte dépend de la raison pour laquelle les doublons existent. Les doublons exacts provenant d’erreurs système peuvent être gérés en conservant le premier enregistrement (Option 1). Lorsque les enregistrements ont une exhaustivité différente suite à des re-soumissions, conservez l’enregistrement le plus complet (Option 2). Les rapports partiels provenant de soumissions fractionnées peuvent nécessiter d’être additionnés (Option 3). Les données conflictuelles nécessitant une investigation doivent être exportées et résolues manuellement avec l’équipe SNT.

Étape 7.2 : Générer le dictionnaire de données final

Un dictionnaire de données complet documente toutes les colonnes de l’ensemble de données prétraité, incluant à la fois les variables DHIS2 originales et les colonnes calculées/dérivées créées lors du prétraitement. Ce dictionnaire sert de référence pour les analyses ultérieures et facilite la collaboration avec l’équipe SNT.

  • R
  • Python
Afficher le code
# définir les descriptions pour les colonnes calculées/dérivées
computed_vars <- tibble::tribble(
  ~snt_var,              ~indicator_label,
  # colonnes de temps
  "date",                "Report date (YYYY-MM-DD)",
  "year",                "Report year",
  "month",               "Report month (1-12)",
  "yearmon",             "Year-month label (e.g., Jan 2020)",
  # colonnes d'identifiants
  "hf_uid",              "Unique health facility identifier (hash)",
  "record_id",           "Unique record identifier (facility + month hash)",
  "location_short",      "Location label: adm1 ~ adm2",
  "location_full",       "Location label: adm1 ~ adm2 ~ adm3 ~ hf",
  "facility_type",       "Facility type (IPD/OPD)",
  # totaux agrégés
  "allout",              "Total outpatient visits (allout_u5 + allout_ov5)",
  "susp",                "Total suspected cases (all ages, HF + COM)",
  "test",                "Total tested (test_hf + test_com)",
  "test_hf",             "Total tested at health facility",
  "test_com",            "Total tested in community",
  "conf",                "Total confirmed cases (conf_hf + conf_com)",
  "conf_hf",             "Total confirmed cases at health facility",
  "conf_com",            "Total confirmed cases in community",
  "maltreat",            "Total treated cases (maltreat_hf + maltreat_com)",
  "maltreat_hf",         "Total treated cases at health facility",
  "maltreat_com",        "Total treated cases in community",
  "pres",                "Total presumed cases (pres_hf + pres_com)",
  "pres_hf",             "Total presumed cases at health facility",
  "pres_com",            "Total presumed cases in community",
  "maladm",              "Total malaria admissions (all ages)",
  "maldth",              "Total malaria deaths (all ages)",
  # totaux par groupe d'âge
  "test_u5",             "Total tested under 5 (HF + COM)",
  "test_5_14",           "Total tested 5-14 years (HF + COM)",
  "test_ov15",           "Total tested over 15 years (HF + COM)",
  "test_hf_u5",          "Tested under 5 at health facility",
  "test_hf_5_14",        "Tested 5-14 years at health facility",
  "test_hf_ov15",        "Tested over 15 years at health facility",
  "test_com_u5",         "Tested under 5 in community",
  "test_com_5_14",       "Tested 5-14 years in community",
  "test_com_ov15",       "Tested over 15 years in community",
  "susp_u5",             "Suspected cases under 5 (HF + COM)",
  "susp_5_14",           "Suspected cases 5-14 years (HF + COM)",
  "susp_ov15",           "Suspected cases over 15 years (HF + COM)",
  "susp_hf_u5",          "Suspected cases under 5 at health facility",
  "susp_hf_5_14",        "Suspected cases 5-14 years at health facility",
  "susp_hf_ov15",        "Suspected cases over 15 years at health facility",
  "susp_com_u5",         "Suspected cases under 5 in community",
  "susp_com_5_14",       "Suspected cases 5-14 years in community",
  "susp_com_ov15",       "Suspected cases over 15 years in community",
  "conf_u5",             "Confirmed cases under 5 (HF + COM)",
  "conf_5_14",           "Confirmed cases 5-14 years (HF + COM)",
  "conf_ov15",           "Confirmed cases over 15 years (HF + COM)",
  "conf_hf_u5",          "Confirmed cases under 5 at health facility",
  "conf_hf_5_14",        "Confirmed cases 5-14 years at health facility",
  "conf_hf_ov15",        "Confirmed cases over 15 years at health facility",
  "conf_com_u5",         "Confirmed cases under 5 in community",
  "conf_com_5_14",       "Confirmed cases 5-14 years in community",
  "conf_com_ov15",       "Confirmed cases over 15 years in community",
  "maltreat_u5",         "Treated cases under 5 (HF + COM)",
  "maltreat_5_14",       "Treated cases 5-14 years (HF + COM)",
  "maltreat_ov15",       "Treated cases over 15 years (HF + COM)",
  "maltreat_hf_u5",      "Treated cases under 5 at health facility",
  "maltreat_hf_5_14",    "Treated cases 5-14 years at health facility",
  "maltreat_hf_ov15",    "Treated cases over 15 years at health facility",
  "maltreat_com_u5",     "Treated cases under 5 in community",
  "maltreat_com_5_14",   "Treated cases 5-14 years in community",
  "maltreat_com_ov15",   "Treated cases over 15 years in community",
  "maltreat_u24_hf",     "Treated cases within 24hrs at health facility",
  "maltreat_ov24_hf",    "Treated cases after 24hrs at health facility",
  "maltreat_u24_com",    "Treated cases within 24hrs in community",
  "maltreat_ov24_com",   "Treated cases after 24hrs in community",
  "maltreat_u24_total",  "Total treated cases within 24hrs (HF + COM)",
  "maltreat_ov24_total", "Total treated cases after 24hrs (HF + COM)",
  "pres_u5",             "Presumed cases under 5 (HF + COM)",
  "pres_5_14",           "Presumed cases 5-14 years (HF + COM)",
  "pres_ov15",           "Presumed cases over 15 years (HF + COM)",
  "pres_hf_u5",          "Presumed cases under 5 at health facility",
  "pres_hf_5_14",        "Presumed cases 5-14 years at health facility",
  "pres_hf_ov15",        "Presumed cases over 15 years at health facility",
  "pres_com_u5",         "Presumed cases under 5 in community",
  "pres_com_5_14",       "Presumed cases 5-14 years in community",
  "pres_com_ov15",       "Presumed cases over 15 years in community"
)

# combiner les dictionnaires original et calculé
full_data_dict <- dplyr::bind_rows(
  data_dict,
  computed_vars
) |>
  # conserver uniquement les colonnes présentes dans l'ensemble de données final
  dplyr::filter(snt_var %in% names(dhis2_df)) |>
  dplyr::arrange(snt_var) |>
  dplyr::select(snt_variable = snt_var, label = indicator_label)

# vérifier
full_data_dict |>
    head()
NoteSortie
  snt_variable                                            label
1         adm0                                    orgunitlevel1
2         adm1                                    orgunitlevel2
3         adm2                                    orgunitlevel3
4         adm3                                    orgunitlevel4
5       allout Total outpatient visits (allout_u5 + allout_ov5)
6   allout_ov5           OPD (New and follow-up curative) 5+y_X

Pour adapter le code :

  • Lignes 4–77 : Examinez et mettez à jour computed_vars pour correspondre à vos variables calculées. Ajoutez ou supprimez des lignes selon les besoins.
  • Lignes 85–91 : Mettez à jour le chemin du fichier de sortie pour le dictionnaire de données final.

Une fois mis à jour, exécutez le code pour générer et exporter le dictionnaire de données final.

Afficher le code
# définir les descriptions pour les colonnes calculées/dérivées
computed_vars = pd.DataFrame([
    # colonnes de temps
    {"snt_var": "date", "indicator_label": "Report date (YYYY-MM-DD)"},
    {"snt_var": "year", "indicator_label": "Report year"},
    {"snt_var": "month", "indicator_label": "Report month (1-12)"},
    {"snt_var": "yearmon", "indicator_label": "Year-month label (e.g., Jan 2020)"},
    # colonnes d'identifiants
    {"snt_var": "hf_uid", "indicator_label": "Unique health facility identifier (hash)"},
    {"snt_var": "record_id", "indicator_label": "Unique record identifier (facility + month hash)"},
    {"snt_var": "location_short", "indicator_label": "Location label: adm1 ~ adm2"},
    {"snt_var": "location_full", "indicator_label": "Location label: adm1 ~ adm2 ~ adm3 ~ hf"},
    {"snt_var": "facility_type", "indicator_label": "Facility type (IPD/OPD)"},
    # totaux agrégés
    {"snt_var": "allout", "indicator_label": "Total outpatient visits (allout_u5 + allout_ov5)"},
    {"snt_var": "susp", "indicator_label": "Total suspected cases (all ages, HF + COM)"},
    {"snt_var": "test", "indicator_label": "Total tested (test_hf + test_com)"},
    {"snt_var": "test_hf", "indicator_label": "Total tested at health facility"},
    {"snt_var": "test_com", "indicator_label": "Total tested in community"},
    {"snt_var": "conf", "indicator_label": "Total confirmed cases (conf_hf + conf_com)"},
    {"snt_var": "conf_hf", "indicator_label": "Total confirmed cases at health facility"},
    {"snt_var": "conf_com", "indicator_label": "Total confirmed cases in community"},
    {"snt_var": "maltreat", "indicator_label": "Total treated cases (maltreat_hf + maltreat_com)"},
    {"snt_var": "maltreat_hf", "indicator_label": "Total treated cases at health facility"},
    {"snt_var": "maltreat_com", "indicator_label": "Total treated cases in community"},
    {"snt_var": "pres", "indicator_label": "Total presumed cases (pres_hf + pres_com)"},
    {"snt_var": "pres_hf", "indicator_label": "Total presumed cases at health facility"},
    {"snt_var": "pres_com", "indicator_label": "Total presumed cases in community"},
    {"snt_var": "maladm", "indicator_label": "Total malaria admissions (all ages)"},
    {"snt_var": "maldth", "indicator_label": "Total malaria deaths (all ages)"},
    # totaux par groupe d'âge
    {"snt_var": "test_u5", "indicator_label": "Total tested under 5 (HF + COM)"},
    {"snt_var": "test_5_14", "indicator_label": "Total tested 5-14 years (HF + COM)"},
    {"snt_var": "test_ov15", "indicator_label": "Total tested over 15 years (HF + COM)"},
    {"snt_var": "conf_u5", "indicator_label": "Confirmed cases under 5 (HF + COM)"},
    {"snt_var": "conf_5_14", "indicator_label": "Confirmed cases 5-14 years (HF + COM)"},
    {"snt_var": "conf_ov15", "indicator_label": "Confirmed cases over 15 years (HF + COM)"},
    {"snt_var": "maltreat_u5", "indicator_label": "Treated cases under 5 (HF + COM)"},
    {"snt_var": "maltreat_5_14", "indicator_label": "Treated cases 5-14 years (HF + COM)"},
    {"snt_var": "maltreat_ov15", "indicator_label": "Treated cases over 15 years (HF + COM)"},
    {"snt_var": "pres_u5", "indicator_label": "Presumed cases under 5 (HF + COM)"},
    {"snt_var": "pres_5_14", "indicator_label": "Presumed cases 5-14 years (HF + COM)"},
    {"snt_var": "pres_ov15", "indicator_label": "Presumed cases over 15 years (HF + COM)"},
    # ajouter les variables par groupe d'âge restantes si nécessaire...
])

# combiner les dictionnaires original et calculé
full_data_dict = (
    pd.concat([data_dict, computed_vars], ignore_index=True)
    .rename(columns={"snt_var": "snt_variable", "indicator_label": "label"})
    [["snt_variable", "label"]]
)

# conserver uniquement les colonnes présentes dans l'ensemble de données final
full_data_dict = (
    full_data_dict[full_data_dict["snt_variable"].isin(dhis2_df.columns)]
    .sort_values("snt_variable")
)

# vérifier
full_data_dict.head(10)
NoteSortie
   snt_variable                                             label
0          adm0                                     orgunitlevel1
1          adm1                                     orgunitlevel2
2          adm2                                     orgunitlevel3
3          adm3                                     orgunitlevel4
63       allout  Total outpatient visits (allout_u5 + allout_ov5)
6    allout_ov5            OPD (New and follow-up curative) 5+y_X
5     allout_u5          OPD (New and follow-up curative) 0-59m_X
68         conf        Total confirmed cases (conf_hf + conf_com)
83    conf_5_14             Confirmed cases 5-14 years (HF + COM)
70     conf_com                Total confirmed cases in community

Pour adapter le code :

  • Lignes 7–49 : Examinez et mettez à jour computed_vars pour correspondre à vos variables calculées. Ajoutez ou supprimez des lignes selon les besoins.
  • Lignes 55–61 : Mettez à jour le chemin du fichier de sortie pour le dictionnaire de données final.

Une fois mis à jour, exécutez le code pour générer et exporter le dictionnaire de données final.

Étape 7.3 : Organiser l’ordre final des colonnes

Avant l’exportation, nous organisons les colonnes dans un ordre logique pour faciliter la navigation dans l’ensemble de données. Les identifiants et les colonnes de localisation viennent en premier, suivis des variables temporelles, puis de toutes les colonnes d’indicateurs.

  • R
  • Python
Afficher le code
# organiser les colonnes dans un ordre logique
dhis2_df <- dhis2_df |>
  dplyr::select(
    # identifiants
    record_id,
    # hiérarchie de localisation
    adm0,
    adm1,
    adm2,
    adm3,
    hf,
    hf_uid,
    location_short,
    location_full,
    facility_type,
    # variables temporelles
    date,
    yearmon,
    year,
    month,
    # all remaining indicator columns
    dplyr::everything()
  )

# vérifier l'ordre des colonnes
colnames(dhis2_df) |> head(20)
NoteSortie
 [1] "record_id"      "adm0"           "adm1"           "adm2"          
 [5] "adm3"           "hf"             "hf_uid"         "location_short"
 [9] "location_full"  "facility_type"  "date"           "yearmon"       
[13] "year"           "month"          "periodname"     "allout_u5"     
[17] "allout_ov5"     "maladm_u5"      "maladm_5_14"    "maladm_ov15"   

Pour adapter le code :

  • Lignes 6–7 : Ajustez les colonnes d’identifiants selon votre ensemble de données.
  • Lignes 9–16 : Modifiez les colonnes de localisation pour correspondre à votre hiérarchie administrative.
  • Lignes 18–21 : Mettez à jour les colonnes temporelles si vous utilisez des variables temporelles différentes (par exemple, epiweek).

Une fois mis à jour, exécutez le code pour réordonner les colonnes avant d’exporter l’ensemble de données final.

Afficher le code
# définir l'ordre des colonnes
id_cols = ["record_id"]
location_cols = ["adm0", "adm1", "adm2", "adm3", "hf", "hf_uid",
                 "location_short", "location_full", "facility_type"]
time_cols = ["date", "yearmon", "year", "month"]

# obtenir les colonnes restantes in original order
ordered_cols = id_cols + location_cols + time_cols
remaining_cols = [col for col in dhis2_df.columns if col not in ordered_cols]

# réordonner le dataframe
dhis2_df = dhis2_df[ordered_cols + remaining_cols]

# vérifier l'ordre des colonnes
print(dhis2_df.columns[:20].tolist())
NoteSortie
['record_id', 'adm0', 'adm1', 'adm2', 'adm3', 'hf', 'hf_uid', 'location_short', 'location_full', 'facility_type', 'date', 'yearmon', 'year', 'month', 'periodname', 'allout_u5', 'allout_ov5', 'maladm_u5', 'maladm_5_14', 'maladm_ov15']

Pour adapter le code :

  • Ligne 2 : Ajustez les colonnes d’identifiants selon votre ensemble de données.
  • Lignes 3–4 : Modifiez les colonnes de localisation pour correspondre à votre hiérarchie administrative.
  • Ligne 5 : Mettez à jour les colonnes temporelles si vous utilisez des variables temporelles différentes (par exemple, epiweek).

Une fois mis à jour, exécutez le code pour réordonner les colonnes avant d’exporter l’ensemble de données final.

Étape 8 : Agréger et sauvegarder les données

Étape 8.1 : Sauvegarder les données au niveau des établissements de santé

Certaines analyses SNT s’appuyant sur des informations spécifiques aux établissements, nous allons maintenant sauvegarder les données au niveau des établissements de santé. Conserver les données au niveau des établissements garantit que les analyses au niveau établissement peuvent être effectuées avec précision et sans perte de détail.

  • R
  • Python
Afficher le code
# définir le chemin de sortie
save_path <- here::here(
  "01_data",
  "02_epidemiology",
  "2a_routine_surveillance",
  "processed"
)

# créer la liste à sauvegarder
dhis2_hf_list <- list(
  data = dhis2_df,
  dictionary = full_data_dict
)

# sauvegarder en xlsx
rio::export(
  dhis2_hf_list,
  here::here(save_path, "sle_dhis2_health_facility_data.xlsx")
)

# save to RDS
rio::export(
  dhis2_hf_list,
  here::here(save_path, "sle_dhis2_health_facility_data.rds")
)

Pour adapter le code :

  • Lignes 2-7 : Mettez à jour save_path vers votre répertoire de sortie préféré.
  • Lignes 10-13 : La liste inclut à la fois les données nettoyées et le dictionnaire de données. Ajoutez ou supprimez des éléments selon les besoins.
  • Lignes 16-19 : Mettez à jour le préfixe du nom de fichier (par exemple, sle_) pour correspondre à votre code de pays.

Une fois mis à jour, exécutez le code pour exporter l’ensemble de données prétraité final et le dictionnaire de données.

Code
save_path = Path(
    here("01_data/02_epidemiology/2a_routine_surveillance/processed")
)

# sauvegarder les données en xlsx
dhis2_df.to_excel(
    save_path / "sle_dhis2_health_facility_data.xlsx",
    index=False
)

# sauvegarder le dictionnaire en xlsx
full_data_dict.to_excel(
    save_path / "sle_dhis2_health_facility_dict.xlsx",
    index=False
)

# sauvegarder en parquet
dhis2_df.to_parquet(
    save_path / "sle_dhis2_health_facility_data.parquet",
    compression="zstd",
    index=False
)

Pour adapter le code :

  • Lignes 2-4 : Mettez à jour save_path vers votre répertoire de sortie préféré.
  • Lignes 7-10 : Mettez à jour le préfixe du nom de fichier (par exemple, sle_) pour correspondre à votre code de pays.
  • Lignes 13-16 : Sauvegardez le dictionnaire séparément si nécessaire.
  • Lignes 19-23 : Le format .parquet est efficace pour les grands ensembles de données. Utilisez .csv si vous partagez avec des utilisateurs non-Python.

Une fois mis à jour, exécutez le code pour exporter l’ensemble de données prétraité final et le dictionnaire de données.

Étape 8.2 : Agréger et sauvegarder les données à chaque niveau d’unité administrative

Nous agrégeons et sauvegardons maintenant les données à différents niveaux administratifs pour soutenir différents types d’analyses et assurer l’alignement avec la façon dont les interventions sont généralement planifiées et suivies. Cela offre une flexibilité lors du calcul des indicateurs, de la comparaison des tendances entre régions ou de la liaison avec d’autres ensembles de données structurés aux niveaux adm0, adm1, adm2, adm3 ou établissement.

WarningÉvitez de sommer les taux pré-calculés

Lors de l’agrégation des données aux niveaux administratifs, tous les indicateurs ne peuvent pas simplement être additionnés. Par exemple, si un taux de positivité des tests ou un taux de traitement a déjà été calculé, ces indicateurs doivent être recalculés au nouveau niveau administratif.

  • R
  • Python
Afficher le code
# définir les colonnes numériques à additionner
# (hors identifiants niveau établissement)
sum_cols <- c(
  # totaux
  "allout", "susp", "test", "conf", "pres", "maltreat", "maladm", "maldth",
  # par localisation
  "test_hf", "test_com", "conf_hf", "conf_com",
  "maltreat_hf", "maltreat_com", "pres_hf", "pres_com",
  # par groupe d'âge - totaux
  "allout_u5", "allout_ov5",
  "test_u5", "test_5_14", "test_ov15",
  "conf_u5", "conf_5_14", "conf_ov15",
  "maltreat_u5", "maltreat_5_14", "maltreat_ov15",
  "pres_u5", "pres_5_14", "pres_ov15",
  "susp_u5", "susp_5_14", "susp_ov15",
  "maladm_u5", "maladm_5_14", "maladm_ov15",
  "maldth_u5", "maldth_5_14", "maldth_ov15",
  # par âge et localisation
  "test_hf_u5", "test_hf_5_14", "test_hf_ov15",
  "test_com_u5", "test_com_5_14", "test_com_ov15",
  "conf_hf_u5", "conf_hf_5_14", "conf_hf_ov15",
  "conf_com_u5", "conf_com_5_14", "conf_com_ov15",
  "maltreat_hf_u5", "maltreat_hf_5_14", "maltreat_hf_ov15",
  "maltreat_com_u5", "maltreat_com_5_14", "maltreat_com_ov15",
  "pres_hf_u5", "pres_hf_5_14", "pres_hf_ov15",
  "pres_com_u5", "pres_com_5_14", "pres_com_ov15",
  # calendrier de traitement
  "maltreat_u24_hf", "maltreat_ov24_hf",
  "maltreat_u24_com", "maltreat_ov24_com",
  "maltreat_u24_total", "maltreat_ov24_total"
)

# fonction pour agréger à un niveau admin donné
aggregate_admin <- function(df, group_cols, sum_cols) {
  df |>
    dplyr::group_by(dplyr::across(dplyr::all_of(group_cols))) |>
    dplyr::summarise(
      dplyr::across(
        dplyr::any_of(sum_cols),
        ~ sum(.x, na.rm = TRUE)
      ),
      n_facilities = dplyr::n(),
      .groups = "drop"
    )
}

# agréger au niveau adm3
group_cols_adm3 <- c("adm0", "adm1", "adm2", "adm3", "year", "month", "yearmon")
dhis2_adm3 <- aggregate_admin(dhis2_df, group_cols_adm3, sum_cols) |>
  dplyr::mutate(
    record_id = sntutils::vdigest(
      paste(adm0, adm1, adm2, adm3, yearmon),
      algo = "xxhash32"
    ),
    location_short = paste(adm1, adm2, sep = " ~ "),
    location_full = paste(adm1, adm2, adm3, sep = " ~ ")
  )

# agréger au niveau adm2
group_cols_adm2 <- c("adm0", "adm1", "adm2", "year", "month", "yearmon")
dhis2_adm2 <- aggregate_admin(dhis2_df, group_cols_adm2, sum_cols) |>
  dplyr::mutate(
    record_id = sntutils::vdigest(
      paste(adm0, adm1, adm2, yearmon),
      algo = "xxhash32"
    ),
    location_short = paste(adm1, adm2, sep = " ~ "),
    location_full = paste(adm1, adm2, sep = " ~ ")
  )

# agréger au niveau adm1
group_cols_adm1 <- c("adm0", "adm1", "year", "month", "yearmon")
dhis2_adm1 <- aggregate_admin(dhis2_df, group_cols_adm1, sum_cols) |>
  dplyr::mutate(
    record_id = sntutils::vdigest(
      paste(adm0, adm1, yearmon),
      algo = "xxhash32"
    ),
    location_short = adm1,
    location_full = adm1
  )

# créer des dictionnaires de données pour chaque niveau
# (filtrer sur les colonnes pertinentes uniquement)
id_cols <- c("n_facilities", "record_id", "location_short", "location_full")

# dictionnaire adm3 : inclut adm0, adm1, adm2, adm3
adm3_dict <- full_data_dict |>
  dplyr::filter(snt_variable %in% c(group_cols_adm3, sum_cols, id_cols))

# dictionnaire adm2 : exclut adm3 (absent à ce niveau)
adm2_dict <- full_data_dict |>
  dplyr::filter(
    snt_variable %in% c(group_cols_adm2, sum_cols, id_cols),
    !snt_variable %in% c("adm3")
  )

# dictionnaire adm1 : exclut adm2, adm3 (absents à ce niveau)
adm1_dict <- full_data_dict |>
  dplyr::filter(
    snt_variable %in% c(group_cols_adm1, sum_cols, id_cols),
    !snt_variable %in% c("adm2", "adm3")
  )

# sauvegarder les données adm3 avec le dictionnaire
rio::export(
  list(data = dhis2_adm3, dictionary = adm3_dict),
  here::here(save_path, "sle_dhis2_adm3_data.xlsx")
)
rio::export(dhis2_adm3, here::here(save_path, "sle_dhis2_adm3_data.rds"))

# sauvegarder les données adm2 avec le dictionnaire
rio::export(
  list(data = dhis2_adm2, dictionary = adm2_dict),
  here::here(save_path, "sle_dhis2_adm2_data.xlsx")
)
rio::export(dhis2_adm2, here::here(save_path, "sle_dhis2_adm2_data.rds"))

# sauvegarder les données adm1 avec le dictionnaire
rio::export(
  list(data = dhis2_adm1, dictionary = adm1_dict),
  here::here(save_path, "sle_dhis2_adm1_data.xlsx")
)
rio::export(dhis2_adm1, here::here(save_path, "sle_dhis2_adm1_data.rds"))
NoteSortie
Aperçu de l'agrégation ADM3 :
# A tibble: 5 × 6
  adm3  yearmon  record_id location_full             conf n_facilities
  <chr> <fct>    <chr>     <chr>                    <dbl>        <int>
1 DEA   Jan 2015 a13ef3b8  EASTERN ~ KAILAHUN ~ DEA   336            3
2 DEA   Feb 2015 a6e77ff1  EASTERN ~ KAILAHUN ~ DEA   353            3
3 DEA   Mar 2015 1a075bd7  EASTERN ~ KAILAHUN ~ DEA   318            3
4 DEA   Apr 2015 9e096499  EASTERN ~ KAILAHUN ~ DEA   235            3
5 DEA   May 2015 cf41c82e  EASTERN ~ KAILAHUN ~ DEA   450            3

Pour adapter le code :

  • Lignes 2-30 : Mettez à jour sum_cols pour inclure tous les indicateurs numériques pertinents pour votre ensemble de données.
  • Lignes 33-42 : La fonction aggregate_admin() gère le regroupement par niveau admin.
  • Lignes 45-79 : Ajustez les colonnes de regroupement si votre hiérarchie administrative diffère. Le record_id est créé avec sntutils::vdigest() en utilisant la hiérarchie admin complète (par exemple, adm0 + adm1 + adm2 + adm3 + yearmon pour le niveau adm3) pour garantir des identifiants uniques. Le code crée également les libellés location_short et location_full pour chaque niveau.
  • Lignes 82-94 : Filtrez le dictionnaire de données pour correspondre aux colonnes de chaque niveau, en excluant les colonnes admin absentes à ce niveau (par exemple, adm3 exclu du dictionnaire adm2).
  • Lignes 97-112 : Mettez à jour les préfixes de nom de fichier (par exemple, sle_) pour correspondre à votre code de pays.

Une fois mis à jour, exécutez le code pour agréger et sauvegarder les données aux niveaux adm3, adm2 et adm1.

Afficher le code
# définir les colonnes numériques à additionner (hors identifiants niveau établissement)
sum_cols = [
    # totaux
    "allout", "susp", "test", "conf", "pres", "maltreat", "maladm", "maldth",
    # par localisation
    "test_hf", "test_com", "conf_hf", "conf_com",
    "maltreat_hf", "maltreat_com", "pres_hf", "pres_com",
    # par groupe d'âge - totaux
    "allout_u5", "allout_ov5",
    "test_u5", "test_5_14", "test_ov15",
    "conf_u5", "conf_5_14", "conf_ov15",
    "maltreat_u5", "maltreat_5_14", "maltreat_ov15",
    "pres_u5", "pres_5_14", "pres_ov15",
    "susp_u5", "susp_5_14", "susp_ov15",
    "maladm_u5", "maladm_5_14", "maladm_ov15",
    "maldth_u5", "maldth_5_14", "maldth_ov15",
    # par âge et localisation
    "test_hf_u5", "test_hf_5_14", "test_hf_ov15",
    "test_com_u5", "test_com_5_14", "test_com_ov15",
    "conf_hf_u5", "conf_hf_5_14", "conf_hf_ov15",
    "conf_com_u5", "conf_com_5_14", "conf_com_ov15",
    "maltreat_hf_u5", "maltreat_hf_5_14", "maltreat_hf_ov15",
    "maltreat_com_u5", "maltreat_com_5_14", "maltreat_com_ov15",
    "pres_hf_u5", "pres_hf_5_14", "pres_hf_ov15",
    "pres_com_u5", "pres_com_5_14", "pres_com_ov15",
    # calendrier de traitement
    "maltreat_u24_hf", "maltreat_ov24_hf",
    "maltreat_u24_com", "maltreat_ov24_com",
    "maltreat_u24_total", "maltreat_ov24_total"
]

# fonction pour agréger à un niveau admin donné
def aggregate_admin(df, group_cols, sum_cols):
    # filter to columns that exist in dataframe
    existing_sum_cols = [c for c in sum_cols if c in df.columns]

    # aggregate
    agg_df = (
        df
        .groupby(group_cols, as_index=False)
        .agg(
            **{col: (col, "sum") for col in existing_sum_cols},
            n_facilities=("hf_uid", "count")
        )
    )

    return agg_df

# agréger au niveau adm3
group_cols_adm3 = ["adm0", "adm1", "adm2", "adm3", "year", "month", "yearmon"]
dhis2_adm3 = aggregate_admin(dhis2_df, group_cols_adm3, sum_cols)
dhis2_adm3["record_id"] = vdigest(
    dhis2_adm3["adm0"] + " " + dhis2_adm3["adm1"] + " " +
    dhis2_adm3["adm2"] + " " + dhis2_adm3["adm3"] + " " +
    dhis2_adm3["yearmon"].astype(str)
)
dhis2_adm3["location_short"] = dhis2_adm3["adm1"] + " ~ " + dhis2_adm3["adm2"]
dhis2_adm3["location_full"] = dhis2_adm3["adm1"] + " ~ " + dhis2_adm3["adm2"] + " ~ " + dhis2_adm3["adm3"]

# agréger au niveau adm2
group_cols_adm2 = ["adm0", "adm1", "adm2", "year", "month", "yearmon"]
dhis2_adm2 = aggregate_admin(dhis2_df, group_cols_adm2, sum_cols)
dhis2_adm2["record_id"] = vdigest(
    dhis2_adm2["adm0"] + " " + dhis2_adm2["adm1"] + " " +
    dhis2_adm2["adm2"] + " " + dhis2_adm2["yearmon"].astype(str)
)
dhis2_adm2["location_short"] = dhis2_adm2["adm1"] + " ~ " + dhis2_adm2["adm2"]
dhis2_adm2["location_full"] = dhis2_adm2["adm1"] + " ~ " + dhis2_adm2["adm2"]

# agréger au niveau adm1
group_cols_adm1 = ["adm0", "adm1", "year", "month", "yearmon"]
dhis2_adm1 = aggregate_admin(dhis2_df, group_cols_adm1, sum_cols)
dhis2_adm1["record_id"] = vdigest(
    dhis2_adm1["adm0"] + " " + dhis2_adm1["adm1"] + " " +
    dhis2_adm1["yearmon"].astype(str)
)
dhis2_adm1["location_short"] = dhis2_adm1["adm1"]
dhis2_adm1["location_full"] = dhis2_adm1["adm1"]

# créer des dictionnaires de données pour chaque niveau (filtrer sur les colonnes pertinentes uniquement)
id_cols = ["n_facilities", "record_id", "location_short", "location_full"]

# dictionnaire adm3 : inclut adm0, adm1, adm2, adm3
adm3_cols = group_cols_adm3 + [c for c in sum_cols if c in dhis2_adm3.columns] + id_cols
adm3_dict = full_data_dict[full_data_dict["snt_variable"].isin(adm3_cols)]

# dictionnaire adm2 : exclut adm3 (absent à ce niveau)
adm2_cols = group_cols_adm2 + [c for c in sum_cols if c in dhis2_adm2.columns] + id_cols
adm2_dict = full_data_dict[
    (full_data_dict["snt_variable"].isin(adm2_cols)) &
    (~full_data_dict["snt_variable"].isin(["adm3"]))
]

# dictionnaire adm1 : exclut adm2, adm3 (absents à ce niveau)
adm1_cols = group_cols_adm1 + [c for c in sum_cols if c in dhis2_adm1.columns] + id_cols
adm1_dict = full_data_dict[
    (full_data_dict["snt_variable"].isin(adm1_cols)) &
    (~full_data_dict["snt_variable"].isin(["adm2", "adm3"]))
]

# sauvegarder les données adm3
with pd.ExcelWriter(save_path / "sle_dhis2_adm3_data.xlsx") as writer:
    dhis2_adm3.to_excel(writer, sheet_name="data", index=False)
    adm3_dict.to_excel(writer, sheet_name="dictionary", index=False)
dhis2_adm3.to_parquet(save_path / "sle_dhis2_adm3_data.parquet", compression="zstd", index=False)

# sauvegarder les données adm2
with pd.ExcelWriter(save_path / "sle_dhis2_adm2_data.xlsx") as writer:
    dhis2_adm2.to_excel(writer, sheet_name="data", index=False)
    adm2_dict.to_excel(writer, sheet_name="dictionary", index=False)
dhis2_adm2.to_parquet(save_path / "sle_dhis2_adm2_data.parquet", compression="zstd", index=False)

# sauvegarder les données adm1
with pd.ExcelWriter(save_path / "sle_dhis2_adm1_data.xlsx") as writer:
    dhis2_adm1.to_excel(writer, sheet_name="data", index=False)
    adm1_dict.to_excel(writer, sheet_name="dictionary", index=False)
dhis2_adm1.to_parquet(save_path / "sle_dhis2_adm1_data.parquet", compression="zstd", index=False)
NoteSortie

Pour adapter le code :

  • Lignes 2-30 : Mettez à jour sum_cols pour inclure tous les indicateurs numériques pertinents pour votre ensemble de données.
  • Lignes 33-46 : La fonction aggregate_admin() gère le regroupement par niveau admin.
  • Lignes 49-75 : Ajustez les colonnes de regroupement si votre hiérarchie administrative diffère. Le record_id est créé avec vdigest() en utilisant la hiérarchie admin complète (par exemple, adm0 + adm1 + adm2 + adm3 + yearmon pour le niveau adm3) pour garantir des identifiants uniques. Le code crée également les libellés location_short et location_full pour chaque niveau.
  • Lignes 78-90 : Filtrez le dictionnaire de données pour correspondre aux colonnes de chaque niveau, en excluant les colonnes admin absentes à ce niveau (par exemple, adm3 exclu du dictionnaire adm2).
  • Lignes 93-108 : Mettez à jour les préfixes de nom de fichier (par exemple, sle_) pour correspondre à votre code de pays.

Une fois mis à jour, exécutez le code pour agréger et sauvegarder les données aux niveaux adm3, adm2 et adm1.

Résumé

Nous avons maintenant parcouru les étapes clés pour nettoyer, restructurer et agréger les données de paludisme DHIS2 pour les rendre prêtes pour le SNT. Le code a couvert tout, depuis l’importation des fichiers bruts, la standardisation des noms de colonnes, le calcul des indicateurs clés et la sauvegarde des sorties à plusieurs niveaux administratifs. Pour plus de commodité, un script complet de bout en bout est inclus ci-dessous dans un bloc de code réduit. Réutilisez-le ou adaptez-le au contexte spécifique du pays en ajustant les noms de colonnes, les chemins de fichiers et les niveaux administratifs selon les besoins.

Code complet

Le script de code complet pour accéder aux données de routine et les traiter se trouve ci-dessous.

  • R
  • Python
Show full code
################################################################################
################ ~ Prétraitement des données DHIS2 full code ~ #################
################################################################################

### Step -----------------------------------------------------------------------

# installer pacman uniquement s'il n'est pas déjà installé
if (!requireNamespace("pacman", quietly = TRUE)) {
  install.packages("pacman")
}

# installer ou charger les paquets pertinents
pacman::p_load(
  tidyverse, # manipulation, restructuration et visualisation des données
  rio,       # importation/exportation de plusieurs types de fichiers
  DT,        # aperçu interactif des tableaux
  here,      # chemins de fichiers relatifs au projet
  readxl,    # lire les fichiers excel
  writexl,   # écrire les fichiers excel
  knitr      # rendu du code dans quarto/rmarkdown
)

# définir le chemin vers les données
core_routine_path <- here::here(
  "1.1.2_epidemiology",
  "1.1.2a_routine_surveillance",
  "raw"
)

# définir le chemin complet DHIS2
path_to_dhis2 <- here::here(core_routine_path, "sle_dhis2_2015_2022.xlsx")

# lire les données
dhis2_df <- rio::import(file = path_to_dhis2)

dhis2_df <- rio::import_list(
  file = path_to_dhis2,
  rbind = TRUE,        # empiler tous les onglets en un seul tableau
  rbind_label = "year" # nom de l'onglet stocké dans la colonne 'year'
)

dhis2_df <- rio::import_list(
  file = list.files(
    path = core_routine_path,
    # les extensions de fichiers
    pattern = "\\.(xls)$",
    full.names = TRUE
  ),
  # empiler tous les onglets/fichiers en un seul tableau
  rbind = TRUE,
  # nom de la source stocké dans la colonne 'sheet_admin'
  rbind_label = "sheet_admin",
  # remplir les colonnes manquantes avec NA lors de l'empilement
  rbind_fill = TRUE
)

# vérifier les données
dhis2_df |>
  dplyr::select(1:20) |>
  dplyr::glimpse()

# vérifier les unités administratives dans nos données
dhis2_df |>
  dplyr::distinct(orgunitlevel3)

# identifier les valeurs entièrement manquantes par colonne
dhis2_df |>
  dplyr::summarise(
    dplyr::across(
      dplyr::everything(),
      ~ mean(is.na(.x)) * 100
    )
  ) |>
  tidyr::pivot_longer(
    everything(),
    names_to = "variable",
    values_to = "pct_missing"
  ) |>
  dplyr::filter(pct_missing == 100)

# importer le dictionnaire de données
data_dict <- rio::import(
  here::here(core_routine_path, "sle_dhis2_data_dict.xlsx")
)
# créer le vecteur de renommage : objet = anciens noms, noms = nouveaux noms
rename_vector <- stats::setNames(
  object = data_dict$indicator_label,
  nm = data_dict$snt_var
)

# renommer les données DHIS2
dhis2_df <- dhis2_df |>
  dplyr::rename(
    !!!rename_vector
  ) |>
  # on supprime orgunitlevel5 car c'est identique à hf
  dplyr::select(-orgunitlevel5)

# vérifier les noms
colnames(dhis2_df)

# créer des données factices
dates_ex1 <- tibble::tibble(
  raw_date = c("2020-01-15", "2021-03-01", "2022-12-20")
)

# analyser les dates
dates_ex1 |>
  dplyr::mutate(
    date = lubridate::ymd(raw_date)
  )

# créer des données factices
dates_ex2 <- tibble::tibble(
  raw_date = c("Jan 2020", "February 2021", "Mar 2022")
)

# analyser les dates
dates_ex2 |>
  dplyr::mutate(
    date = lubridate::parse_date_time(
      raw_date,
      orders = c("b Y", "B Y")
    ) |>
      as.Date()
  )

# créer des données factices
dates_ex3 <- tibble::tibble(
  raw_date = c(
    "Janvier 2020",
    "Février 2021",
    "décembre 2022",
    "Jan 2021",
    "Fe 2022",
    "Dec 2023"
  )
)

# analyser les dates
dates_ex3 |>
  dplyr::mutate(
    date = paste0("01 ", raw_date) |>
      lubridate::dmy(locale = "fr_FR") |>
      as.Date()
  )

# créer des données factices
dates_ex4 <- tibble::tibble(
  raw_date = c("Janeiro 2020", "fevereiro 2021", "Dezembro 2022",
               "Jan 2021", "Fev 2022", "Dez 2023")
)

# analyser les dates
dates_ex4 |>
  dplyr::mutate(
    date = paste0("01 ", raw_date) |>
      lubridate::dmy(locale = "pt_PT") |>
      as.Date()
  )

# créer des données factices
dates_ex5 <- tibble::tibble(
  raw_date = c("2020-01", "2021/05", "2023-12")
)

# analyser les dates
dates_ex5 |>
  dplyr::mutate(
    date = lubridate::parse_date_time(raw_date, orders = c("Y-m", "Y/m")),
    date = lubridate::floor_date(date, unit = "month") |> as.Date()
  )

# créer des données factices
dates_ex6 <- tibble::tibble(
  raw_date = c("01/2020", "06-2021", "11/2022")
)

# analyser les dates
dates_ex6 |>
  dplyr::mutate(
    date = lubridate::parse_date_time(raw_date, orders = c("m/Y", "m-Y")),
    date = lubridate::floor_date(date, unit = "month") |> as.Date()
  )

# créer des données factices
dates_ex7 <- tibble::tibble(
  raw_date = c("202301", "202102", "202212")
)

dates_ex7 |>
  dplyr::mutate(
    year = substr(raw_date, 1, 4),
    month = substr(raw_date, 5, 6),
    date = lubridate::ymd(sprintf("%s-%s-01", year, month)) |> as.Date()
  )

# créer des données factices
dates_ex8 <- tibble::tibble(
  raw_date = c(
    "2020-01-15", # date ISO complète
    "Jan 2021",   # mois anglais abrégé
    "202203",     # AAAAMM compact
    "03/2022",    # mois/année avec barre oblique
    "2021-07",    # année-mois
    "July 2020",  # mois anglais complet
    "15-02-2021", # JJ-MM-AAAA
    "2020/11/05", # AAAA/MM/JJ
    "2020.12.25", # date avec points
    "2022.07",    # AAAA.MM
    "10-2020",    # MM-AAAA
    "20210105",   # AAAAMMJJ
    "2020 Jan",   # année puis mois (anglais)
    "2020.01.01"  # AAAA.MM.JJ avec points
  )
)

# analyser les dates
dates_ex8 |>
  dplyr::mutate(
    date = lubridate::parse_date_time(
      raw_date,
      orders = c(
        "Y-m-d", # 2020-01-15
        "Y/m/d", # 2020/11/05
        "Y.m.d", # 2020.12.25
        "b Y",   # Jan 2021
        "B Y",   # January 2021
        "Y b",   # 2020 Jan
        "Y B",   # 2020 January
        "Ym",    # 202203
        "m/Y",   # 03/2022
        "m-Y",   # 10-2020
        "Y-m",   # 2021-07
        "Y.m",   # 2022.07
        "d-m-Y", # 15-02-2021
        "d/m/Y", # 15/02/2021 (if present)
        "Ymd"    #  20210105
      )
    ) |>
      as.Date()
  )

# définir la locale selon la langue de votre export DHIS2
# options possibles : "en_US.UTF-8", "fr_FR.UTF-8", "pt_PT.UTF-8"
Sys.setlocale("LC_TIME", "en_US.UTF-8")

dhis2_df <- dhis2_df |>
  # analyser la date brute en un objet Date correct
  dplyr::mutate(
    date = lubridate::parse_date_time(
      periodname,
      orders = c("B Y", "b Y")
    ) |>
      as.Date()
  ) |>
  # créer les champs année, mois et libellé yearmon ordonné
  dplyr::mutate(
    year = lubridate::year(date),
    month = lubridate::month(date),
    # libellé lisible, ex. "Jan 2020"
    yearmon = format(date, "%b %Y"),
    # ordonner le facteur par date chronologique réelle
    yearmon = factor(yearmon, levels = unique(yearmon[order(date)]))
  )

# vérifier les premières lignes
dhis2_df |>
  dplyr::select(date, year, month, yearmon) |>
  head()

# installer le paquet sntutils depuis GitHub
# contient plusieurs fonctions utilitaires pratiques
devtools::install_github("ahadi-analytics/sntutils")

# définir l'emplacement pour sauvegarder le cache
cache_path <- "1.1_foundational/1d_cache_files"

# récupérer le shapefile adm3 pour l'utiliser comme référence de correspondance
shp_adm3 <- sntutils::read(
  here::here("data/shapefiles/processed/sle_spatial_adm3_2021.rds")
) |>
  # supprimer la géométrie, on n'a besoin que des noms admin
  sf::st_drop_geometry()

# standardiser les noms administratifs
dhis2_df <- dhis2_df |>
  dplyr::mutate(
    adm0 = toupper(adm0),
    adm2 = toupper(adm1),
    adm3 = toupper(adm3),
    hf = toupper(hf),
    # le adm2 dans le shapefile de référence n'a pas
    # "DISTRICT" dans le nom, on le supprime pour permettre la correspondance
    adm2 = stringr::str_remove_all(adm2, " DISTRICT"),
    adm3 = stringr::str_remove_all(adm3, " CHIEFDOM")
  ) |>
  dplyr::mutate(
    # assigner les provinces selon les regroupements de districts
    # (pas d'adm1 car l'adm1 actuel est adm2)
    adm1 = dplyr::case_when(
      adm2 %in% c("KAILAHUN", "KENEMA", "KONO") ~ "EASTERN",
      adm2 %in% c("BOMBALI", "FALABA", "KOINADUGU", "TONKOLILI") ~ "NORTH EAST",
      adm2 %in% c("KAMBIA", "KARENE", "PORT LOKO") ~ "NORTH WEST",
      adm2 %in% c("BO", "BONTHE", "MOYAMBA", "PUJEHUN") ~ "SOUTHERN",
      adm2 %in% c("WESTERN AREA RURAL", "WESTERN AREA URBAN") ~ "WESTERN"
    )
  )

# harmoniser les noms admin entre les données DHIS2 et le shapefile
dhis2_df <-
  sntutils::prep_geonames(
    target_df = dhis2_df,
    lookup_df = lookup_keys,
    level0 = "adm0",
    level1 = "adm1",
    level2 = "adm2",
    level3 = "adm3",
    interactive = TRUE,
    cache_path = here::here(cache_path, "geoname_decisions_cache.xlsx")
  )

# créer les identifiants
dhis2_df <- dhis2_df |>
  dplyr::mutate(
    # créer un identifiant unique d'établissement de santé
    # à partir de la hiérarchie admin
    hf_uid = sntutils::vdigest(
      paste0(adm0, adm1, adm2, adm3, hf),
      algo = "xxhash32"
    ),
    # créer des libellés de localisation pour la cartographie
    location_short = paste(adm1, adm2, sep = " ~ "),
    location_full = paste(adm1, adm2, adm3, hf, sep = " ~ "),
    # générer un identifiant d'enregistrement cohérent (établissement + période)
    record_id = sntutils::vdigest(
      paste(hf_uid, yearmon),
      algo = "xxhash32"
    )
  )

# vérifier
dhis2_df |>
  dplyr::arrange(location_full) |>
  dplyr::distinct(location_full, hf_uid, record_id) |>
  head()

# somme par ligne intelligente avec gestion des données manquantes
fallback_row_sum <- function(..., min_present = 1, .keep_zero_as_zero = TRUE) {
  vars_matrix <- cbind(...)
  valid_count <- rowSums(!is.na(vars_matrix))
  raw_sum <- rowSums(vars_matrix, na.rm = TRUE)

  ifelse(valid_count >= min_present, raw_sum, NA_real_)
}

# différence absolue de secours entre deux vecteurs
fallback_diff <- function(col1, col2, minimum = 0) {
  dplyr::case_when(
    is.na(col1) & is.na(col2) ~ NA_real_,
    is.na(col1) ~ pmax(col2, minimum),
    is.na(col2) ~ pmax(col1, minimum),
    TRUE ~ pmax(col1 - col2, minimum)
  )
}

# calculer les totaux des indicateurs dans les données DHIS2
dhis2_df <- dhis2_df |>
  dplyr::mutate(
    # consultations ambulatoires
    allout = fallback_row_sum(allout_u5, allout_ov5),

    # cas suspects
    susp = fallback_row_sum(
      susp_u5_hf,
      susp_5_14_hf,
      susp_ov15_hf,
      susp_u5_com,
      susp_5_14_com,
      susp_ov15_com
    ),

    # cas testés
    test_hf = fallback_row_sum(
      test_neg_mic_u5_hf,
      test_pos_mic_u5_hf,
      test_neg_mic_5_14_hf,
      test_pos_mic_5_14_hf,
      test_neg_mic_ov15_hf,
      test_pos_mic_ov15_hf,
      tes_neg_rdt_u5_hf,
      tes_pos_rdt_u5_hf,
      tes_neg_rdt_5_14_hf,
      tes_pos_rdt_5_14_hf,
      tes_neg_rdt_ov15_hf,
      tes_pos_rdt_ov15_hf
    ),

    test_com = fallback_row_sum(
      tes_neg_rdt_u5_com,
      tes_pos_rdt_u5_com,
      tes_neg_rdt_5_14_com,
      tes_pos_rdt_5_14_com,
      tes_neg_rdt_ov15_com,
      tes_pos_rdt_ov15_com
    ),

    test = fallback_row_sum(test_hf, test_com),

    # cas confirmés (établissement et communauté)
    conf_hf = fallback_row_sum(
      test_pos_mic_u5_hf,
      test_pos_mic_5_14_hf,
      test_pos_mic_ov15_hf,
      tes_pos_rdt_u5_hf,
      tes_pos_rdt_5_14_hf,
      tes_pos_rdt_ov15_hf
    ),

    conf_com = fallback_row_sum(
      tes_pos_rdt_u5_com,
      tes_pos_rdt_5_14_com,
      tes_pos_rdt_ov15_com
    ),

    conf = fallback_row_sum(conf_hf, conf_com),

    # cas traités
    maltreat_com = fallback_row_sum(
      maltreat_u24_u5_com,
      maltreat_ov24_u5_com,
      maltreat_u24_5_14_com,
      maltreat_ov24_5_14_com,
      maltreat_u24_ov15_com,
      maltreat_ov24_ov15_com
    ),

    maltreat_hf = fallback_row_sum(
      maltreat_u24_u5_hf,
      maltreat_ov24_u5_hf,
      maltreat_u24_5_14_hf,
      maltreat_ov24_5_14_hf,
      maltreat_u24_ov15_hf,
      maltreat_ov24_ov15_hf
    ),

    maltreat = fallback_row_sum(maltreat_hf, maltreat_com),

    # cas présumés
    pres_com = fallback_diff(maltreat_com, conf_com),
    pres_hf = fallback_diff(maltreat_hf, conf_hf),
    pres = fallback_row_sum(pres_com, pres_hf),

    # hospitalisations pour paludisme
    maladm = fallback_row_sum(
      maladm_u5,
      maladm_5_14,
      maladm_ov15
    ),

    # décès dus au paludisme
    maldth = fallback_row_sum(
      maldth_u5,
      maldth_1_59m,
      maldth_10_14,
      maldth_5_9,
      maldth_5_14,
      maldth_ov15,
      maldth_fem_ov15,
      maldth_mal_ov15
    ),

    # agrégations par groupe d'âge
    # cas testés par groupe d'âge (établissement uniquement)
    test_hf_u5 = fallback_row_sum(
      test_neg_mic_u5_hf,
      test_pos_mic_u5_hf,
      tes_neg_rdt_u5_hf,
      tes_pos_rdt_u5_hf
    ),

    test_hf_5_14 = fallback_row_sum(
      test_neg_mic_5_14_hf,
      test_pos_mic_5_14_hf,
      tes_neg_rdt_5_14_hf,
      tes_pos_rdt_5_14_hf
    ),

    test_hf_ov15 = fallback_row_sum(
      test_neg_mic_ov15_hf,
      test_pos_mic_ov15_hf,
      tes_neg_rdt_ov15_hf,
      tes_pos_rdt_ov15_hf
    ),

    # cas testés par groupe d'âge (Community only)
    test_com_u5 = fallback_row_sum(
      tes_neg_rdt_u5_com,
      tes_pos_rdt_u5_com
    ),

    test_com_5_14 = fallback_row_sum(
      tes_neg_rdt_5_14_com,
      tes_pos_rdt_5_14_com
    ),

    test_com_ov15 = fallback_row_sum(
      tes_neg_rdt_ov15_com,
      tes_pos_rdt_ov15_com
    ),

    # total des cas testés par groupe d'âge (établissement + communauté)
    test_u5 = fallback_row_sum(test_hf_u5, test_com_u5),
    test_5_14 = fallback_row_sum(test_hf_5_14, test_com_5_14),
    test_ov15 = fallback_row_sum(test_hf_ov15, test_com_ov15),

    # cas suspects par groupe d'âge (HF only)
    susp_hf_u5 = susp_u5_hf,

    susp_hf_5_14 = susp_5_14_hf,

    susp_hf_ov15 = susp_ov15_hf,

    # cas suspects par groupe d'âge (Community only)
    susp_com_u5 = susp_u5_com,

    susp_com_5_14 = susp_5_14_com,

    susp_com_ov15 = susp_ov15_com,

    # total suspected by age group (HF + Community)
    susp_u5 = fallback_row_sum(susp_hf_u5, susp_com_u5),
    susp_5_14 = fallback_row_sum(susp_hf_5_14, susp_com_5_14),
    susp_ov15 = fallback_row_sum(susp_hf_ov15, susp_com_ov15),

    # cas confirmés par groupe d'âge (établissement uniquement)
    conf_hf_u5 = fallback_row_sum(
      test_pos_mic_u5_hf,
      tes_pos_rdt_u5_hf
    ),

    conf_hf_5_14 = fallback_row_sum(
      test_pos_mic_5_14_hf,
      tes_pos_rdt_5_14_hf
    ),

    conf_hf_ov15 = fallback_row_sum(
      test_pos_mic_ov15_hf,
      tes_pos_rdt_ov15_hf
    ),

    # cas confirmés par groupe d'âge (communauté uniquement)
    conf_com_u5 = tes_pos_rdt_u5_com,
    conf_com_5_14 = tes_pos_rdt_5_14_com,
    conf_com_ov15 = tes_pos_rdt_ov15_com,

    # total des cas confirmés par groupe d'âge (établissement + communauté)
    conf_u5 = fallback_row_sum(conf_hf_u5, conf_com_u5),
    conf_5_14 = fallback_row_sum(conf_hf_5_14, conf_com_5_14),
    conf_ov15 = fallback_row_sum(conf_hf_ov15, conf_com_ov15),

    # cas traités par groupe d'âge (HF only)
    maltreat_hf_u5 = fallback_row_sum(
      maltreat_u24_u5_hf,
      maltreat_ov24_u5_hf
    ),

    maltreat_hf_5_14 = fallback_row_sum(
      maltreat_u24_5_14_hf,
      maltreat_ov24_5_14_hf
    ),

    maltreat_hf_ov15 = fallback_row_sum(
      maltreat_u24_ov15_hf,
      maltreat_ov24_ov15_hf
    ),

    # cas traités par groupe d'âge (Community only)
    maltreat_com_u5 = fallback_row_sum(
      maltreat_u24_u5_com,
      maltreat_ov24_u5_com
    ),

    maltreat_com_5_14 = fallback_row_sum(
      maltreat_u24_5_14_com,
      maltreat_ov24_5_14_com
    ),

    maltreat_com_ov15 = fallback_row_sum(
      maltreat_u24_ov15_com,
      maltreat_ov24_ov15_com
    ),

    # total des cas traités par groupe d'âge (établissement + communauté)
    maltreat_u5 = fallback_row_sum(maltreat_hf_u5, maltreat_com_u5),
    maltreat_5_14 = fallback_row_sum(maltreat_hf_5_14, maltreat_com_5_14),
    maltreat_ov15 = fallback_row_sum(maltreat_hf_ov15, maltreat_com_ov15),

    # total des cas traités dans les 24 heures (établissement uniquement)
    maltreat_u24_hf = fallback_row_sum(
      maltreat_u24_u5_hf,
      maltreat_u24_5_14_hf,
      maltreat_u24_ov15_hf
    ),

    # total des cas traités après 24 heures (établissement uniquement)
    maltreat_ov24_hf = fallback_row_sum(
      maltreat_ov24_u5_hf,
      maltreat_ov24_5_14_hf,
      maltreat_ov24_ov15_hf
    ),

    # total des cas traités dans les 24 heures (communauté uniquement)
    maltreat_u24_com = fallback_row_sum(
      maltreat_u24_u5_com,
      maltreat_u24_5_14_com,
      maltreat_u24_ov15_com
    ),

    # total des cas traités après 24 heures (communauté uniquement)
    maltreat_ov24_com = fallback_row_sum(
      maltreat_ov24_u5_com,
      maltreat_ov24_5_14_com,
      maltreat_ov24_ov15_com
    ),

    # totaux globaux (établissement + communauté)
    maltreat_u24_total = fallback_row_sum(maltreat_u24_hf, maltreat_u24_com),
    maltreat_ov24_total = fallback_row_sum(maltreat_ov24_hf, maltreat_ov24_com),

    # cas présumés par groupe d'âge
    pres_com_u5 = fallback_diff(maltreat_com_u5, conf_com_u5),
    pres_com_5_14 = fallback_diff(maltreat_com_5_14, conf_com_5_14),
    pres_com_ov15 = fallback_diff(maltreat_com_ov15, conf_com_ov15),

    pres_hf_u5 = fallback_diff(maltreat_hf_u5, conf_hf_u5),
    pres_hf_5_14 = fallback_diff(maltreat_hf_5_14, conf_hf_5_14),
    pres_hf_ov15 = fallback_diff(maltreat_hf_ov15, conf_hf_ov15),

    pres_u5 = fallback_row_sum(pres_com_u5, pres_hf_u5),
    pres_5_14 = fallback_row_sum(pres_com_5_14, pres_hf_5_14),
    pres_ov15 = fallback_row_sum(pres_com_ov15, pres_hf_ov15)
  )

# check to see the aggregation worked
dhis2_df |>
  dplyr::filter(
    record_id %in%
      c("6a29143b", "0e7ba814", "943c5f5f", "4fbe05fd", "40cc411c", "51194842")
  ) |>
  dplyr::arrange(allout) |>
  dplyr::select(
    allout,
    allout_u5,
    allout_ov5,
    pres_hf_u5,
    maltreat_hf_u5,
    conf_hf_u5
  ) |>
  head()

# créer des indicateurs de divergence pour chaque groupe d'indicateurs
mismatch_summary <- dhis2_df |>
  dplyr::summarise(
    # vérification des consultations ambulatoires
    allout_mismatch = sum(
      allout != (allout_u5 + allout_ov5),
      na.rm = TRUE
    ),

    # vérification des hospitalisations pour paludisme
    maladm_mismatch = sum(
      maladm != (maladm_u5 + maladm_5_14 + maladm_ov15),
      na.rm = TRUE
    ),

    # vérification du total des tests
    test_mismatch = sum(
      test != (test_hf + test_com),
      na.rm = TRUE
    ),

    # vérification du total des cas confirmés
    conf_mismatch = sum(
      conf != (conf_hf + conf_com),
      na.rm = TRUE
    ),

    # vérification du total des cas traités
    maltreat_mismatch = sum(
      maltreat != (maltreat_hf + maltreat_com),
      na.rm = TRUE
    ),

    # vérification du total des cas présumés
    pres_mismatch = sum(
      pres != (pres_hf + pres_com),
      na.rm = TRUE
    ),

    # vérification des décès dus au paludisme
    maldth_mismatch = sum(
      maldth !=
        (maldth_1_59m +
          maldth_u5 +
          maldth_5_9 +
          maldth_10_14 +
          maldth_5_14 +
          maldth_fem_ov15 +
          maldth_mal_ov15 +
          maldth_ov15),
      na.rm = TRUE
    ),

    # cas testés par groupe d'âge
    test_u5_mismatch = sum(
      test_u5 != (test_hf_u5 + test_com_u5),
      na.rm = TRUE
    ),
    test_5_14_mismatch = sum(
      test_5_14 != (test_hf_5_14 + test_com_5_14),
      na.rm = TRUE
    ),
    test_ov15_mismatch = sum(
      test_ov15 != (test_hf_ov15 + test_com_ov15),
      na.rm = TRUE
    ),

    # cas confirmés par groupe d'âge
    conf_u5_mismatch = sum(
      conf_u5 != (conf_hf_u5 + conf_com_u5),
      na.rm = TRUE
    ),
    conf_5_14_mismatch = sum(
      conf_5_14 != (conf_hf_5_14 + conf_com_5_14),
      na.rm = TRUE
    ),
    conf_ov15_mismatch = sum(
      conf_ov15 != (conf_hf_ov15 + conf_com_ov15),
      na.rm = TRUE
    ),

    # cas présumés par groupe d'âge
    pres_u5_mismatch = sum(
      pres_u5 != (pres_hf_u5 + pres_com_u5),
      na.rm = TRUE
    ),
    pres_5_14_mismatch = sum(
      pres_5_14 != (pres_hf_5_14 + pres_com_5_14),
      na.rm = TRUE
    ),
    pres_ov15_mismatch = sum(
      pres_ov15 != (pres_hf_ov15 + pres_com_ov15),
      na.rm = TRUE
    ),

    # cas suspects par groupe d'âge
    susp_u5_mismatch = sum(
      susp_u5 != (susp_u5_hf + susp_u5_com),
      na.rm = TRUE
    ),
    susp_5_14_mismatch = sum(
      susp_5_14 != (susp_5_14_hf + susp_5_14_com),
      na.rm = TRUE
    ),
    susp_ov15_mismatch = sum(
      susp_ov15 != (susp_ov15_hf + susp_ov15_com),
      na.rm = TRUE
    ),

    # cas traités par groupe d'âge
    maltreat_u5_mismatch = sum(
      maltreat_u5 != (maltreat_hf_u5 + maltreat_com_u5),
      na.rm = TRUE
    ),
    maltreat_5_14_mismatch = sum(
      maltreat_5_14 != (maltreat_hf_5_14 + maltreat_com_5_14),
      na.rm = TRUE
    ),
    maltreat_ov15_mismatch = sum(
      maltreat_ov15 != (maltreat_hf_ov15 + maltreat_com_ov15),
      na.rm = TRUE
    ),

    # vérifications du calendrier de traitement
    maltreat_u24_mismatch = sum(
      maltreat_u24_total != (maltreat_u24_hf + maltreat_u24_com),
      na.rm = TRUE
    ),
    maltreat_ov24_mismatch = sum(
      maltreat_ov24_total != (maltreat_ov24_hf + maltreat_ov24_com),
      na.rm = TRUE
    )
  ) |>
  # pivoter au format long pour une lecture facilitée
  tidyr::pivot_longer(
    cols = dplyr::everything(),
    names_to = "indicator",
    values_to = "n_mismatches"
  ) |>
  # filtrer pour afficher uniquement les indicateurs avec divergences
  dplyr::filter(n_mismatches > 0)

mismatch_summary

# identifier les lignes avec des totaux incohérents
incoherent_rows <- dhis2_df |>
  dplyr::filter(
    # vérification des consultations ambulatoires
    allout != (allout_u5 + allout_ov5) |
    # vérification des hospitalisations pour paludisme
    maladm != (maladm_u5 + maladm_5_14 + maladm_ov15) |
    # vérification du total des tests
    test != (test_hf + test_com) |
    # vérification du total des cas confirmés
    conf != (conf_hf + conf_com) |
    # vérification du total des cas traités
    maltreat != (maltreat_hf + maltreat_com) |
    # vérification du total des cas présumés
    pres != (pres_hf + pres_com)
  ) |>
  dplyr::select(
    hf, periodname,
    dplyr::matches("allout|maladm|test|conf|maltreat|pres")
  )

# définir le chemin pour la sauvegarde
output_file <- here::here(
  "1.1.2_epidemiology",
  "1.1.2a_routine_surveillance",
  "processed",
  "sle_incoherent_totals_dhis2.xlsx"
)
# exporter en xlsx
rio::export(incoherent_rows, file = output_file)

# option 1 : classifier selon les patterns de noms d'établissements
dhis2_df <- dhis2_df |>
  dplyr::mutate(
    facility_type = dplyr::case_when(
      # établissements hospitaliers (hôpitaux)
      stringr::str_detect(
        hf,
        regex("hospital|hosp", ignore_case = TRUE)
      ) ~ "IPD",
      stringr::str_detect(
        hf,
        regex("district hospital|regional hospital", ignore_case = TRUE)
      ) ~ "IPD",
      # établissements ambulatoires (cliniques,
      # postes de santé, santé communautaire)
      stringr::str_detect(
        hf,
        regex(
          "CHP|CHC|MCHP|clinic|health post|health centre",
          ignore_case = TRUE
        )
      ) ~ "OPD",
      # par défaut OPD si aucun pattern ne correspond
      TRUE ~ "OPD"
    )
  )

# option 2 : classifier selon la présence d'indicateurs hospitaliers
dhis2_df <- dhis2_df |>
  dplyr::group_by(hf_uid) |>
  dplyr::mutate(
    # l'établissement est IPD s'il déclare au moins une admission ou un décès
    has_ipd = any(!is.na(maladm) & maladm > 0, na.rm = TRUE) |
      any(!is.na(maldth) & maldth > 0, na.rm = TRUE),
    facility_type = dplyr::if_else(has_ipd, "IPD", "OPD")
  ) |>
  dplyr::ungroup() |>
  dplyr::select(-has_ipd)

# option 3 : utiliser un fichier de référence de correspondance
hf_path <-
  rio::import(
    here::here(
      "01_data",
      "1.1_foundational",
      "1.1b_health_facilities",
      "processed",
      "health_facility_master_list.xlsx"
    )
  )

dhis2_df <- dhis2_df |>
  dplyr::left_join(
    facility_lookup |> dplyr::select(hf_uid, facility_type),
    by = "hf_uid"
  )

# vérifier la distribution de la classification
dhis2_df |>
  dplyr::distinct(hf_uid, facility_type) |>
  dplyr::count(facility_type)

# identifier les combinaisons établissement-mois en double
duplicates <- dhis2_df |>
  dplyr::group_by(record_id) |>
  dplyr::filter(dplyr::n() > 1) |>
  dplyr::ungroup()

# compter le nombre de paires de doublons
n_duplicates <- duplicates |>
  dplyr::distinct(record_id) |>
  nrow()

# si des doublons existent, les inspecter
if (n_duplicates > 0) {
  # afficher les enregistrements en double avec les indicateurs clés
  duplicate_details <- duplicates |>
    dplyr::select(
      record_id,
      adm0, adm1, adm2, adm3, hfm yearmon,
      allout,
      test,
      conf,
      maltreat
    ) |>
    dplyr::arrange(record_id)

  print(duplicate_details)

  # exporter pour révision avec l'équipe SNT
  rio::export(
    duplicate_details,
    here::here(
      "1.1.2_epidemiology",
      "1.1.2a_routine_surveillance",
      "processed",
      "sle_duplicate_records_dhis2.xlsx"
    )
  )
}

# option 1 : conserver le premier enregistrement
# (si les doublons sont des copies exactes)
dhis2_df <- dhis2_df |>
  dplyr::distinct(record_id, .keep_all = TRUE)

# option 2 : conserver l'enregistrement le plus complet
dhis2_df <- dhis2_df |>
  dplyr::group_by(record_id) |>
  dplyr::slice_max(
    # compter les valeurs non-NA dans les colonnes d'indicateurs
    order_by = rowSums(!is.na(dplyr::across(dplyr::where(is.numeric)))),
    n = 1,
    with_ties = FALSE
  ) |>
  dplyr::ungroup()

# option 3 : additionner les doublons
# (si les enregistrements représentent des rapports partiels)
dhis2_df <- dhis2_df |>
  dplyr::group_by(record_id, hf, adm0, adm1, adm2, adm3) |>
  dplyr::summarise(
    dplyr::across(dplyr::where(is.numeric), ~ sum(.x, na.rm = TRUE)),
    .groups = "drop"
  )

# définir les descriptions pour les colonnes calculées/dérivées
computed_vars <- tibble::tribble(
  ~snt_var,              ~indicator_label,
  # colonnes de temps
  "date",                "Report date (YYYY-MM-DD)",
  "year",                "Report year",
  "month",               "Report month (1-12)",
  "yearmon",             "Year-month label (e.g., Jan 2020)",
  # colonnes d'identifiants
  "hf_uid",              "Unique health facility identifier (hash)",
  "record_id",           "Unique record identifier (facility + month hash)",
  "location_short",      "Location label: adm1 ~ adm2",
  "location_full",       "Location label: adm1 ~ adm2 ~ adm3 ~ hf",
  "facility_type",       "Facility type (IPD/OPD)",
  # totaux agrégés
  "allout",              "Total outpatient visits (allout_u5 + allout_ov5)",
  "susp",                "Total suspected cases (all ages, HF + COM)",
  "test",                "Total tested (test_hf + test_com)",
  "test_hf",             "Total tested at health facility",
  "test_com",            "Total tested in community",
  "conf",                "Total confirmed cases (conf_hf + conf_com)",
  "conf_hf",             "Total confirmed cases at health facility",
  "conf_com",            "Total confirmed cases in community",
  "maltreat",            "Total treated cases (maltreat_hf + maltreat_com)",
  "maltreat_hf",         "Total treated cases at health facility",
  "maltreat_com",        "Total treated cases in community",
  "pres",                "Total presumed cases (pres_hf + pres_com)",
  "pres_hf",             "Total presumed cases at health facility",
  "pres_com",            "Total presumed cases in community",
  "maladm",              "Total malaria admissions (all ages)",
  "maldth",              "Total malaria deaths (all ages)",
  # totaux par groupe d'âge
  "test_u5",             "Total tested under 5 (HF + COM)",
  "test_5_14",           "Total tested 5-14 years (HF + COM)",
  "test_ov15",           "Total tested over 15 years (HF + COM)",
  "test_hf_u5",          "Tested under 5 at health facility",
  "test_hf_5_14",        "Tested 5-14 years at health facility",
  "test_hf_ov15",        "Tested over 15 years at health facility",
  "test_com_u5",         "Tested under 5 in community",
  "test_com_5_14",       "Tested 5-14 years in community",
  "test_com_ov15",       "Tested over 15 years in community",
  "susp_u5",             "Suspected cases under 5 (HF + COM)",
  "susp_5_14",           "Suspected cases 5-14 years (HF + COM)",
  "susp_ov15",           "Suspected cases over 15 years (HF + COM)",
  "susp_hf_u5",          "Suspected cases under 5 at health facility",
  "susp_hf_5_14",        "Suspected cases 5-14 years at health facility",
  "susp_hf_ov15",        "Suspected cases over 15 years at health facility",
  "susp_com_u5",         "Suspected cases under 5 in community",
  "susp_com_5_14",       "Suspected cases 5-14 years in community",
  "susp_com_ov15",       "Suspected cases over 15 years in community",
  "conf_u5",             "Confirmed cases under 5 (HF + COM)",
  "conf_5_14",           "Confirmed cases 5-14 years (HF + COM)",
  "conf_ov15",           "Confirmed cases over 15 years (HF + COM)",
  "conf_hf_u5",          "Confirmed cases under 5 at health facility",
  "conf_hf_5_14",        "Confirmed cases 5-14 years at health facility",
  "conf_hf_ov15",        "Confirmed cases over 15 years at health facility",
  "conf_com_u5",         "Confirmed cases under 5 in community",
  "conf_com_5_14",       "Confirmed cases 5-14 years in community",
  "conf_com_ov15",       "Confirmed cases over 15 years in community",
  "maltreat_u5",         "Treated cases under 5 (HF + COM)",
  "maltreat_5_14",       "Treated cases 5-14 years (HF + COM)",
  "maltreat_ov15",       "Treated cases over 15 years (HF + COM)",
  "maltreat_hf_u5",      "Treated cases under 5 at health facility",
  "maltreat_hf_5_14",    "Treated cases 5-14 years at health facility",
  "maltreat_hf_ov15",    "Treated cases over 15 years at health facility",
  "maltreat_com_u5",     "Treated cases under 5 in community",
  "maltreat_com_5_14",   "Treated cases 5-14 years in community",
  "maltreat_com_ov15",   "Treated cases over 15 years in community",
  "maltreat_u24_hf",     "Treated cases within 24hrs at health facility",
  "maltreat_ov24_hf",    "Treated cases after 24hrs at health facility",
  "maltreat_u24_com",    "Treated cases within 24hrs in community",
  "maltreat_ov24_com",   "Treated cases after 24hrs in community",
  "maltreat_u24_total",  "Total treated cases within 24hrs (HF + COM)",
  "maltreat_ov24_total", "Total treated cases after 24hrs (HF + COM)",
  "pres_u5",             "Presumed cases under 5 (HF + COM)",
  "pres_5_14",           "Presumed cases 5-14 years (HF + COM)",
  "pres_ov15",           "Presumed cases over 15 years (HF + COM)",
  "pres_hf_u5",          "Presumed cases under 5 at health facility",
  "pres_hf_5_14",        "Presumed cases 5-14 years at health facility",
  "pres_hf_ov15",        "Presumed cases over 15 years at health facility",
  "pres_com_u5",         "Presumed cases under 5 in community",
  "pres_com_5_14",       "Presumed cases 5-14 years in community",
  "pres_com_ov15",       "Presumed cases over 15 years in community"
)

# combiner les dictionnaires original et calculé
full_data_dict <- dplyr::bind_rows(
  data_dict,
  computed_vars
) |>
  # conserver uniquement les colonnes présentes dans l'ensemble de données final
  dplyr::filter(snt_var %in% names(dhis2_df)) |>
  dplyr::arrange(snt_var) |>
  dplyr::select(snt_variable = snt_var, label = indicator_label)

# vérifier
full_data_dict |>
    head()

# organiser les colonnes dans un ordre logique
dhis2_df <- dhis2_df |>
  dplyr::select(
    # identifiants
    record_id,
    # hiérarchie de localisation
    adm0,
    adm1,
    adm2,
    adm3,
    hf,
    hf_uid,
    location_short,
    location_full,
    facility_type,
    # variables temporelles
    date,
    yearmon,
    year,
    month,
    # all remaining indicator columns
    dplyr::everything()
  )

# vérifier l'ordre des colonnes
colnames(dhis2_df) |> head(20)

# définir le chemin de sortie
save_path <- here::here(
  "01_data",
  "02_epidemiology",
  "2a_routine_surveillance",
  "processed"
)

# créer la liste à sauvegarder
dhis2_hf_list <- list(
  data = dhis2_df,
  dictionary = full_data_dict
)

# sauvegarder en xlsx
rio::export(
  dhis2_hf_list,
  here::here(save_path, "sle_dhis2_health_facility_data.xlsx")
)

# save to RDS
rio::export(
  dhis2_hf_list,
  here::here(save_path, "sle_dhis2_health_facility_data.rds")
)

# définir les colonnes numériques à additionner
# (hors identifiants niveau établissement)
sum_cols <- c(
  # totaux
  "allout", "susp", "test", "conf", "pres", "maltreat", "maladm", "maldth",
  # par localisation
  "test_hf", "test_com", "conf_hf", "conf_com",
  "maltreat_hf", "maltreat_com", "pres_hf", "pres_com",
  # par groupe d'âge - totaux
  "allout_u5", "allout_ov5",
  "test_u5", "test_5_14", "test_ov15",
  "conf_u5", "conf_5_14", "conf_ov15",
  "maltreat_u5", "maltreat_5_14", "maltreat_ov15",
  "pres_u5", "pres_5_14", "pres_ov15",
  "susp_u5", "susp_5_14", "susp_ov15",
  "maladm_u5", "maladm_5_14", "maladm_ov15",
  "maldth_u5", "maldth_5_14", "maldth_ov15",
  # par âge et localisation
  "test_hf_u5", "test_hf_5_14", "test_hf_ov15",
  "test_com_u5", "test_com_5_14", "test_com_ov15",
  "conf_hf_u5", "conf_hf_5_14", "conf_hf_ov15",
  "conf_com_u5", "conf_com_5_14", "conf_com_ov15",
  "maltreat_hf_u5", "maltreat_hf_5_14", "maltreat_hf_ov15",
  "maltreat_com_u5", "maltreat_com_5_14", "maltreat_com_ov15",
  "pres_hf_u5", "pres_hf_5_14", "pres_hf_ov15",
  "pres_com_u5", "pres_com_5_14", "pres_com_ov15",
  # calendrier de traitement
  "maltreat_u24_hf", "maltreat_ov24_hf",
  "maltreat_u24_com", "maltreat_ov24_com",
  "maltreat_u24_total", "maltreat_ov24_total"
)

# fonction pour agréger à un niveau admin donné
aggregate_admin <- function(df, group_cols, sum_cols) {
  df |>
    dplyr::group_by(dplyr::across(dplyr::all_of(group_cols))) |>
    dplyr::summarise(
      dplyr::across(
        dplyr::any_of(sum_cols),
        ~ sum(.x, na.rm = TRUE)
      ),
      n_facilities = dplyr::n(),
      .groups = "drop"
    )
}

# agréger au niveau adm3
group_cols_adm3 <- c("adm0", "adm1", "adm2", "adm3", "year", "month", "yearmon")
dhis2_adm3 <- aggregate_admin(dhis2_df, group_cols_adm3, sum_cols) |>
  dplyr::mutate(
    record_id = sntutils::vdigest(
      paste(adm0, adm1, adm2, adm3, yearmon),
      algo = "xxhash32"
    ),
    location_short = paste(adm1, adm2, sep = " ~ "),
    location_full = paste(adm1, adm2, adm3, sep = " ~ ")
  )

# agréger au niveau adm2
group_cols_adm2 <- c("adm0", "adm1", "adm2", "year", "month", "yearmon")
dhis2_adm2 <- aggregate_admin(dhis2_df, group_cols_adm2, sum_cols) |>
  dplyr::mutate(
    record_id = sntutils::vdigest(
      paste(adm0, adm1, adm2, yearmon),
      algo = "xxhash32"
    ),
    location_short = paste(adm1, adm2, sep = " ~ "),
    location_full = paste(adm1, adm2, sep = " ~ ")
  )

# agréger au niveau adm1
group_cols_adm1 <- c("adm0", "adm1", "year", "month", "yearmon")
dhis2_adm1 <- aggregate_admin(dhis2_df, group_cols_adm1, sum_cols) |>
  dplyr::mutate(
    record_id = sntutils::vdigest(
      paste(adm0, adm1, yearmon),
      algo = "xxhash32"
    ),
    location_short = adm1,
    location_full = adm1
  )

# créer des dictionnaires de données pour chaque niveau
# (filtrer sur les colonnes pertinentes uniquement)
id_cols <- c("n_facilities", "record_id", "location_short", "location_full")

# dictionnaire adm3 : inclut adm0, adm1, adm2, adm3
adm3_dict <- full_data_dict |>
  dplyr::filter(snt_variable %in% c(group_cols_adm3, sum_cols, id_cols))

# dictionnaire adm2 : exclut adm3 (absent à ce niveau)
adm2_dict <- full_data_dict |>
  dplyr::filter(
    snt_variable %in% c(group_cols_adm2, sum_cols, id_cols),
    !snt_variable %in% c("adm3")
  )

# dictionnaire adm1 : exclut adm2, adm3 (absents à ce niveau)
adm1_dict <- full_data_dict |>
  dplyr::filter(
    snt_variable %in% c(group_cols_adm1, sum_cols, id_cols),
    !snt_variable %in% c("adm2", "adm3")
  )

# sauvegarder les données adm3 avec le dictionnaire
rio::export(
  list(data = dhis2_adm3, dictionary = adm3_dict),
  here::here(save_path, "sle_dhis2_adm3_data.xlsx")
)
rio::export(dhis2_adm3, here::here(save_path, "sle_dhis2_adm3_data.rds"))

# sauvegarder les données adm2 avec le dictionnaire
rio::export(
  list(data = dhis2_adm2, dictionary = adm2_dict),
  here::here(save_path, "sle_dhis2_adm2_data.xlsx")
)
rio::export(dhis2_adm2, here::here(save_path, "sle_dhis2_adm2_data.rds"))

# sauvegarder les données adm1 avec le dictionnaire
rio::export(
  list(data = dhis2_adm1, dictionary = adm1_dict),
  here::here(save_path, "sle_dhis2_adm1_data.xlsx")
)
rio::export(dhis2_adm1, here::here(save_path, "sle_dhis2_adm1_data.rds"))
Show full code
################################################################################
################ ~ Prétraitement des données DHIS2 full code ~ #################
################################################################################

### Step -----------------------------------------------------------------------

import pandas as pd  # charger les outils de données principaux
from pathlib import Path  # gérer les chemins du système de fichiers
from pyprojroot import here  # construire des chemins relatifs au projet
import os  # utilitaires de chemins optionnels
import locale  # pour définir la locale de langue
import geopandas as gpd  # pour importer les shapefiles
import xxhash  # pour le hachage
import pyarrow.parquet as pq  # pour exporter les fichiers parquet
import pyarrow as pa  # pour exporter les fichiers parquet

# définir le chemin vers les données dhis2
core_routine_path = Path(
    here("1.1.2_epidemiology/1.1.2a_routine_surveillance/raw")
)

# définir le chemin complet dhis2
path_to_dhis2 = core_routine_path / "sle_dhis2_2015_2022.xlsx"

# lire les données
dhis2_df = pd.read_excel(path_to_dhis2)

# importer tous les onglets et les empiler en un seul tableau
dhis2_df = pd.concat(
    [
        pd.read_excel(path_to_dhis2, sheet_name=s).assign(year=s)
        for s in pd.ExcelFile(path_to_dhis2).sheet_names
    ],
    ignore_index=True,
)
dhis2_df.head(10).style

# définir l'extension de fichier
extension = "xls"

# trouver tous les fichiers avec l'extension spécifiée dans le répertoire
files = list(core_routine_path.glob(f"*.{extension}"))

# initialiser le dataframe
dhis2_df = pd.DataFrame()

# itérer sur les fichiers, concaténer en un seul dataframe
for file in files:
    temp = pd.read_excel(file, sheet_name="Sheet1")
    dhis2_df = pd.concat([dhis2_df, temp])

# inspecter les données
dhis2_df.head(10)

# vérifier les unités administratives dans nos données
dhis2_df[["orgunitlevel3"]].drop_duplicates().reset_index(drop=True)

# calculer le pourcentage de valeurs manquantes pour chaque colonne
pct_missing = dhis2_df.isna().mean() * 100

# convertir au format long
missing_table = (
    pct_missing.reset_index()
    .rename(columns={"index": "variable", 0: "pct_missing"})
)

# filtrer les colonnes entièrement manquantes
missing_table[missing_table["pct_missing"] == 100]

# importer le dictionnaire de données
dict_path = os.path.join(core_routine_path, "sle_dhis2_data_dict.xlsx")
data_dict = pd.read_excel(dict_path)

# construire le dictionnaire de renommage : {ancien_nom: nouveau_nom}
rename_dict = dict(zip(data_dict["indicator_label"], data_dict["snt_var"]))

# renommer les données dhis2
dhis2_df = dhis2_df.rename(columns=rename_dict).drop(
    columns=["orgunitlevel5"], errors="ignore"
)

# afficher les noms de colonnes mis à jour
dhis2_df.columns.tolist()

# créer des données factices
dates_ex1 = pd.DataFrame({
    "raw_date": ["2020-01-15", "2021-03-01", "2022-12-20"]
})

# analyser les dates
dates_ex1["date"] = pd.to_datetime(dates_ex1["raw_date"], format="%Y-%m-%d")

dates_ex1

# créer des données factices
dates_ex2 = pd.DataFrame({
    "raw_date": ["Jan 2020", "February 2021", "Mar 2022"]
})

# analyser les dates
dates_ex2["date"] = pd.to_datetime(
    dates_ex2["raw_date"],
    format=None,  # permettre l'analyse flexible des noms de mois abrégés et complets
)

dates_ex2

# définir la locale française (ajuster si nécessaire selon le système)
# options courantes : 'fr_FR.UTF-8', 'fr_FR'
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')

# créer des données factices
dates_ex3 = pd.DataFrame({
    "raw_date": [
        "Janvier 2020",
        "Février 2021",
        "décembre 2022"
    ]
})

# s'assurer que le jour est présent pour l'analyse
dates_ex3["date"] = pd.to_datetime(
    "01 " + dates_ex3["raw_date"],
    format="%d %B %Y",
    errors="coerce"
)

# secours pour les mois abrégés
mask_missing = dates_ex3["date"].isna()
dates_ex3.loc[mask_missing, "date"] = pd.to_datetime(
    "01 " + dates_ex3.loc[mask_missing, "raw_date"],
    format="%d %b %Y",
    errors="coerce"
)

dates_ex3

# définir la locale portugaise
# options courantes : 'pt_PT.UTF-8', 'pt_PT'
locale.setlocale(locale.LC_TIME, 'pt_PT.UTF-8')

# créer des données factices
dates_ex4 = pd.DataFrame({
    "raw_date": [
        "Janeiro 2020",
        "fevereiro 2021",
        "Dezembro 2022",
        "Jan 2021",
        "Fev 2022",
        "Dez 2023"
    ]
})

# première tentative : noms de mois portugais complets
dates_ex4["date"] = pd.to_datetime(
    "01 " + dates_ex4["raw_date"],
    format="%d %B %Y",
    errors="coerce"
)

# secours : noms de mois portugais abrégés (ex. Jan, Fev, Dez)
mask_missing = dates_ex4["date"].isna()
dates_ex4.loc[mask_missing, "date"] = pd.to_datetime(
    "01 " + dates_ex4.loc[mask_missing, "raw_date"],
    format="%d %b %Y",
    errors="coerce"
)

dates_ex4

# créer des données factices
dates_ex5 = pd.DataFrame(
    {
        "raw_date": [
            "2020-01-15",  # date ISO complète
            "Jan 2021",    # mois anglais abrégé
            "202203",      # AAAAMM compact
            "03/2022",     # month/year
            "2021-07",     # année-mois
            "July 2020",   # mois anglais complet
            "15-02-2021",  # JJ-MM-AAAA
            "2020/11/05",  # AAAA/MM/JJ
            "2020.12.25",  # AAAA.MM.JJ
            "2022.07",     # YYYY.MM
            "10-2020",     # MM-AAAA
            "20210105",    # AAAAMMJJ
            "2020 Jan",    # année puis mois
            "2020.01.01",  # date complète avec points
        ]
    }
)

# liste des formats possibles
formats = [
    "%Y-%m-%d",
    "%Y/%m/%d",
    "%Y.%m.%d",
    "%d-%m-%Y",
    "%d/%m/%Y",
    "%b %Y",
    "%B %Y",
    "%Y %b",
    "%Y %B",
    "%Y-%m",
    "%Y/%m",
    "%Y.%m",
    "%m-%Y",
    "%m/%Y",
    "%Y%m",
    "%Y%m%d",
]

def try_parse(value):
    # essayer d'abord les formats stricts
    for fmt in formats:
        try:
            return pd.to_datetime(value, format=fmt)
        except:
            pass
    # secours : laisser pandas deviner librement
    return pd.to_datetime(value, errors="coerce")

dates_ex5["date"] = dates_ex5["raw_date"].apply(try_parse)

dates_ex5["date"] = dates_ex5["date"].dt.to_period("M").dt.to_timestamp()
dates_ex5

# définir la locale selon la langue de votre export DHIS2
# options possibles : "en_US.UTF-8", "fr_FR.UTF-8", "pt_PT.UTF-8"
locale.setlocale(locale.LC_TIME, "en_US.UTF-8")

# analyser la date brute en un objet datetime correct
dhis2_df["date"] = pd.to_datetime(
    dhis2_df["periodname"],
    format="%B %Y",
    errors="coerce"
)

# créer les colonnes année, mois et yearmon ordonné
dhis2_df["year"] = dhis2_df["date"].dt.year
dhis2_df["month"] = dhis2_df["date"].dt.month
dhis2_df["yearmon"] = dhis2_df["date"].dt.strftime("%b %Y")

# vérifier la sortie
dhis2_df[["date", "year", "month", "yearmon"]].head()

# si ce n'est pas déjà fait, installer le paquet python sntutils depuis GitHub
# contient plusieurs fonctions utilitaires pratiques
pip install git+https://github.com/ahadi-analytics/sntutils-py.git
from sntutils.geo import prep_geonames # pour la correspondance des noms

# définir l'emplacement pour sauvegarder le cache
cache_path = Path("1.1_foundational/1d_cache_files")

# récupérer le shapefile adm3 pour l'utiliser comme référence de correspondance
shp_adm3 = gpd.read_file(
    Path(here("data/shapefiles/processed/sle_spatial_adm3_2021.geojson"))
).drop(columns="geometry")

# standardiser les noms administratifs
dhis2_df = dhis2_df.assign(
    adm0=lambda x: x["adm0"].str.upper(),
    # le adm2 dans le shapefile de référence n'a pas
    # "DISTRICT" dans le nom, on le supprime pour permettre la correspondance
    adm2=lambda x: x["adm1"]
    .str.upper()
    .str.replace(" DISTRICT", "", regex=False)
    .str.strip(),
    adm3=lambda x: x["adm3"]
    .str.upper()
    .str.replace(" CHIEFDOM", "", regex=False)
    .str.strip(),
    hf=lambda x: x["hf"]
    .str.upper()
)

# assigner les provinces selon les regroupements de districts
# (pas d'adm1 car l'adm1 actuel est adm2)
district_to_province = {
    "KAILAHUN": "EASTERN", "KENEMA": "EASTERN", "KONO": "EASTERN",
    "BOMBALI": "NORTH EAST", "FALABA": "NORTH EAST",
    "KOINADUGU": "NORTH EAST", "TONKOLILI": "NORTH EAST",
    "KAMBIA": "NORTH WEST", "KARENE": "NORTH WEST", "PORT LOKO": "NORTH WEST",
    "BO": "SOUTHERN", "BONTHE": "SOUTHERN",
    "MOYAMBA": "SOUTHERN", "PUJEHUN": "SOUTHERN",
    "WESTERN AREA RURAL": "WESTERN", "WESTERN AREA URBAN": "WESTERN"
}

dhis2_df["adm1"] = dhis2_df["adm2"].map(district_to_province)

# harmoniser les noms admin entre les données dhis2 et le shapefile
# note : sntutils::prep_geonames() n'a pas d'équivalent python direct
# correspondance floue manuelle ou fusion exacte requise
dhis2_df = dhis2_df.merge(
    lookup_keys,
    on=["adm0", "adm1", "adm2", "adm3"],
    how="left"
)

# harmoniser les noms admin entre les données de population et le shapefile
dhis2_df = prep_geonames(
    target_df=dhis2_df,
    lookup_df=shp_adm3,
    level0="adm0",
    level1="adm1",
    level2="adm2",
    level3="adm3",
    cache_path=here(cache_path, "geoname_decisions_cache.xlsx")
)

# fonction auxiliaire pour créer un condensé xxhash32 (équivalent à sntutils::vdigest)
def vdigest(x, algo="xxhash32"):
    """Créer un condensé de hachage vectorisé."""
    return x.apply(lambda val: xxhash.xxh32(str(val)).hexdigest())

# créer les identifiants
dhis2_df = dhis2_df.assign(
    # créer un identifiant unique d'établissement de santé à partir de la hiérarchie admin
    # utiliser la sémantique paste0 (sans séparateur) pour correspondre à R's paste0(adm0, adm1, adm2, adm3, hf)
    hf_uid=lambda x: vdigest(
        x["adm0"] + x["adm1"] + x["adm2"] + x["adm3"] + x["hf"]
    ),
    # créer des libellés de localisation pour la cartographie
    location_short=lambda x: x["adm1"] + " ~ " + x["adm2"],
    location_full=lambda x: (
        x["adm1"] + " ~ " + x["adm2"] + " ~ " + x["adm3"] + " ~ " + x["hf"]
    ),
)

# générer un identifiant d'enregistrement cohérent (établissement + période)
# utiliser un espace pour correspondre au séparateur par défaut de R's paste(hf_uid, yearmon)
dhis2_df["record_id"] = vdigest(
    dhis2_df["hf_uid"] + " " + dhis2_df["yearmon"].astype(str)
)

# vérifier
dhis2_df[["location_full", "hf_uid", "record_id"]].drop_duplicates().sort_values(
    "location_full"
).head()

# fonction auxiliaire : somme par ligne qui retourne la valeur si une seule est non-NA
def fallback_row_sum(df, cols):
    return df[cols].sum(axis=1, skipna=True, min_count=1)

# fonction auxiliaire : différence avec plancher à 0, gestion des NA comme R
# si les deux valeurs sont NA retourner NA ; si une seule est NA retourner l'autre
# limité à 0 ; si les deux sont présentes retourner max(col1 - col2, 0)
import numpy as np

def fallback_diff(df, col1, col2):
    a = df[col1]
    b = df[col2]
    both_na = a.isna() & b.isna()
    only_b_na = a.notna() & b.isna()
    only_a_na = a.isna() & b.notna()
    result = (a - b).clip(lower=0)
    result = result.where(~only_b_na, a.clip(lower=0))
    result = result.where(~only_a_na, b.clip(lower=0))
    result = result.where(~both_na, np.nan)
    return result

dhis2_df = (
    dhis2_df.assign(
        # consultations ambulatoires
        allout=lambda x: fallback_row_sum(x, ["allout_u5", "allout_ov5"]),
        # cas suspects
        susp=lambda x: fallback_row_sum(
            x,
            [
                "susp_u5_hf",
                "susp_5_14_hf",
                "susp_ov15_hf",
                "susp_u5_com",
                "susp_5_14_com",
                "susp_ov15_com",
            ],
        ),
        # cas testés
        test_hf=lambda x: fallback_row_sum(
            x,
            [
                "test_neg_mic_u5_hf",
                "test_pos_mic_u5_hf",
                "test_neg_mic_5_14_hf",
                "test_pos_mic_5_14_hf",
                "test_neg_mic_ov15_hf",
                "test_pos_mic_ov15_hf",
                "tes_neg_rdt_u5_hf",
                "tes_pos_rdt_u5_hf",
                "tes_neg_rdt_5_14_hf",
                "tes_pos_rdt_5_14_hf",
                "tes_neg_rdt_ov15_hf",
                "tes_pos_rdt_ov15_hf",
            ],
        ),
        test_com=lambda x: fallback_row_sum(
            x,
            [
                "tes_neg_rdt_u5_com",
                "tes_pos_rdt_u5_com",
                "tes_neg_rdt_5_14_com",
                "tes_pos_rdt_5_14_com",
                "tes_neg_rdt_ov15_com",
                "tes_pos_rdt_ov15_com",
            ],
        ),
        # confirmed cases (HF and COM)
        conf_hf=lambda x: fallback_row_sum(
            x,
            [
                "test_pos_mic_u5_hf",
                "test_pos_mic_5_14_hf",
                "test_pos_mic_ov15_hf",
                "tes_pos_rdt_u5_hf",
                "tes_pos_rdt_5_14_hf",
                "tes_pos_rdt_ov15_hf",
            ],
        ),
        conf_com=lambda x: fallback_row_sum(
            x,
            [
                "tes_pos_rdt_u5_com",
                "tes_pos_rdt_5_14_com",
                "tes_pos_rdt_ov15_com",
            ],
        ),
        # cas traités
        maltreat_com=lambda x: fallback_row_sum(
            x,
            [
                "maltreat_u24_u5_com",
                "maltreat_ov24_u5_com",
                "maltreat_u24_5_14_com",
                "maltreat_ov24_5_14_com",
                "maltreat_u24_ov15_com",
                "maltreat_ov24_ov15_com",
            ],
        ),
        maltreat_hf=lambda x: fallback_row_sum(
            x,
            [
                "maltreat_u24_u5_hf",
                "maltreat_ov24_u5_hf",
                "maltreat_u24_5_14_hf",
                "maltreat_ov24_5_14_hf",
                "maltreat_u24_ov15_hf",
                "maltreat_ov24_ov15_hf",
            ],
        ),
        # hospitalisations pour paludisme
        maladm=lambda x: fallback_row_sum(
            x, ["maladm_u5", "maladm_5_14", "maladm_ov15"]
        ),
        # décès dus au paludisme
        maldth=lambda x: fallback_row_sum(
            x,
            [
                "maldth_u5",
                "maldth_1_59m",
                "maldth_10_14",
                "maldth_5_9",
                "maldth_5_14",
                "maldth_ov15",
                "maldth_fem_ov15",
                "maldth_mal_ov15",
            ],
        ),
        # AGE-GROUP SPECIFIC AGGREGATIONS
        # cas testés par groupe d'âge (établissement uniquement)
        test_hf_u5=lambda x: fallback_row_sum(
            x,
            [
                "test_neg_mic_u5_hf",
                "test_pos_mic_u5_hf",
                "tes_neg_rdt_u5_hf",
                "tes_pos_rdt_u5_hf",
            ],
        ),
        test_hf_5_14=lambda x: fallback_row_sum(
            x,
            [
                "test_neg_mic_5_14_hf",
                "test_pos_mic_5_14_hf",
                "tes_neg_rdt_5_14_hf",
                "tes_pos_rdt_5_14_hf",
            ],
        ),
        test_hf_ov15=lambda x: fallback_row_sum(
            x,
            [
                "test_neg_mic_ov15_hf",
                "test_pos_mic_ov15_hf",
                "tes_neg_rdt_ov15_hf",
                "tes_pos_rdt_ov15_hf",
            ],
        ),
        # cas testés par groupe d'âge (communauté uniquement)
        test_com_u5=lambda x: fallback_row_sum(
            x, ["tes_neg_rdt_u5_com", "tes_pos_rdt_u5_com"]
        ),
        test_com_5_14=lambda x: fallback_row_sum(
            x, ["tes_neg_rdt_5_14_com", "tes_pos_rdt_5_14_com"]
        ),
        test_com_ov15=lambda x: fallback_row_sum(
            x, ["tes_neg_rdt_ov15_com", "tes_pos_rdt_ov15_com"]
        ),
        # cas suspects par groupe d'âge (renommer pour cohérence)
        susp_hf_u5=lambda x: x["susp_u5_hf"],
        susp_hf_5_14=lambda x: x["susp_5_14_hf"],
        susp_hf_ov15=lambda x: x["susp_ov15_hf"],
        susp_com_u5=lambda x: x["susp_u5_com"],
        susp_com_5_14=lambda x: x["susp_5_14_com"],
        susp_com_ov15=lambda x: x["susp_ov15_com"],
        # cas confirmés par groupe d'âge (établissement uniquement)
        conf_hf_u5=lambda x: fallback_row_sum(
            x, ["test_pos_mic_u5_hf", "tes_pos_rdt_u5_hf"]
        ),
        conf_hf_5_14=lambda x: fallback_row_sum(
            x, ["test_pos_mic_5_14_hf", "tes_pos_rdt_5_14_hf"]
        ),
        conf_hf_ov15=lambda x: fallback_row_sum(
            x, ["test_pos_mic_ov15_hf", "tes_pos_rdt_ov15_hf"]
        ),
        # cas confirmés par groupe d'âge (communauté uniquement)
        conf_com_u5=lambda x: x["tes_pos_rdt_u5_com"],
        conf_com_5_14=lambda x: x["tes_pos_rdt_5_14_com"],
        conf_com_ov15=lambda x: x["tes_pos_rdt_ov15_com"],
        # cas traités par groupe d'âge (établissement uniquement)
        maltreat_hf_u5=lambda x: fallback_row_sum(
            x, ["maltreat_u24_u5_hf", "maltreat_ov24_u5_hf"]
        ),
        maltreat_hf_5_14=lambda x: fallback_row_sum(
            x, ["maltreat_u24_5_14_hf", "maltreat_ov24_5_14_hf"]
        ),
        maltreat_hf_ov15=lambda x: fallback_row_sum(
            x, ["maltreat_u24_ov15_hf", "maltreat_ov24_ov15_hf"]
        ),
        # cas traités par groupe d'âge (communauté uniquement)
        maltreat_com_u5=lambda x: fallback_row_sum(
            x, ["maltreat_u24_u5_com", "maltreat_ov24_u5_com"]
        ),
        maltreat_com_5_14=lambda x: fallback_row_sum(
            x, ["maltreat_u24_5_14_com", "maltreat_ov24_5_14_com"]
        ),
        maltreat_com_ov15=lambda x: fallback_row_sum(
            x, ["maltreat_u24_ov15_com", "maltreat_ov24_ov15_com"]
        ),
        # Total treated cases within/after 24 hours
        maltreat_u24_hf=lambda x: fallback_row_sum(
            x,
            ["maltreat_u24_u5_hf", "maltreat_u24_5_14_hf", "maltreat_u24_ov15_hf"],
        ),
        maltreat_ov24_hf=lambda x: fallback_row_sum(
            x,
            ["maltreat_ov24_u5_hf", "maltreat_ov24_5_14_hf", "maltreat_ov24_ov15_hf"],
        ),
        maltreat_u24_com=lambda x: fallback_row_sum(
            x,
            [
                "maltreat_u24_u5_com",
                "maltreat_u24_5_14_com",
                "maltreat_u24_ov15_com",
            ],
        ),
        maltreat_ov24_com=lambda x: fallback_row_sum(
            x,
            [
                "maltreat_ov24_u5_com",
                "maltreat_ov24_5_14_com",
                "maltreat_ov24_ov15_com",
            ],
        ),
    )
    .assign(
        # second pass: computed from first pass columns
        test=lambda x: fallback_row_sum(x, ["test_hf", "test_com"]),
        conf=lambda x: fallback_row_sum(x, ["conf_hf", "conf_com"]),
        maltreat=lambda x: fallback_row_sum(x, ["maltreat_hf", "maltreat_com"]),
        # totaux by age group
        test_u5=lambda x: fallback_row_sum(x, ["test_hf_u5", "test_com_u5"]),
        test_5_14=lambda x: fallback_row_sum(x, ["test_hf_5_14", "test_com_5_14"]),
        test_ov15=lambda x: fallback_row_sum(x, ["test_hf_ov15", "test_com_ov15"]),
        susp_u5=lambda x: fallback_row_sum(x, ["susp_hf_u5", "susp_com_u5"]),
        susp_5_14=lambda x: fallback_row_sum(x, ["susp_hf_5_14", "susp_com_5_14"]),
        susp_ov15=lambda x: fallback_row_sum(x, ["susp_hf_ov15", "susp_com_ov15"]),
        conf_u5=lambda x: fallback_row_sum(x, ["conf_hf_u5", "conf_com_u5"]),
        conf_5_14=lambda x: fallback_row_sum(x, ["conf_hf_5_14", "conf_com_5_14"]),
        conf_ov15=lambda x: fallback_row_sum(x, ["conf_hf_ov15", "conf_com_ov15"]),
        maltreat_u5=lambda x: fallback_row_sum(
            x, ["maltreat_hf_u5", "maltreat_com_u5"]
        ),
        maltreat_5_14=lambda x: fallback_row_sum(
            x, ["maltreat_hf_5_14", "maltreat_com_5_14"]
        ),
        maltreat_ov15=lambda x: fallback_row_sum(
            x, ["maltreat_hf_ov15", "maltreat_com_ov15"]
        ),
        maltreat_u24_total=lambda x: fallback_row_sum(
            x, ["maltreat_u24_hf", "maltreat_u24_com"]
        ),
        maltreat_ov24_total=lambda x: fallback_row_sum(
            x, ["maltreat_ov24_hf", "maltreat_ov24_com"]
        ),
        # cas présumés
        pres_com=lambda x: fallback_diff(x, "maltreat_com", "conf_com"),
        pres_hf=lambda x: fallback_diff(x, "maltreat_hf", "conf_hf"),
        pres_com_u5=lambda x: fallback_diff(x, "maltreat_com_u5", "conf_com_u5"),
        pres_com_5_14=lambda x: fallback_diff(x, "maltreat_com_5_14", "conf_com_5_14"),
        pres_com_ov15=lambda x: fallback_diff(x, "maltreat_com_ov15", "conf_com_ov15"),
        pres_hf_u5=lambda x: fallback_diff(x, "maltreat_hf_u5", "conf_hf_u5"),
        pres_hf_5_14=lambda x: fallback_diff(x, "maltreat_hf_5_14", "conf_hf_5_14"),
        pres_hf_ov15=lambda x: fallback_diff(x, "maltreat_hf_ov15", "conf_hf_ov15"),
    )
    .assign(
        # third pass: totals from presumed
        pres=lambda x: fallback_row_sum(x, ["pres_com", "pres_hf"]),
        pres_u5=lambda x: fallback_row_sum(x, ["pres_com_u5", "pres_hf_u5"]),
        pres_5_14=lambda x: fallback_row_sum(x, ["pres_com_5_14", "pres_hf_5_14"]),
        pres_ov15=lambda x: fallback_row_sum(x, ["pres_com_ov15", "pres_hf_ov15"]),
    )
)

# inspect results
(
    dhis2_df[
        dhis2_df["record_id"].isin(
            ["6a29143b", "0e7ba814", "943c5f5f", "4fbe05fd", "40cc411c", "51194842"]
        )
    ][
        [
            "allout",
            "allout_u5",
            "allout_ov5",
            "pres_hf_u5",
            "maltreat_hf_u5",
            "conf_hf_u5",
        ]
    ]
    .sort_values("allout")
    .head(6)
)

# créer des indicateurs de divergence pour chaque groupe d'indicateurs
# helper function to count mismatches, ignoring NAs
def count_mismatch(total, components):
    """Compter les divergences entre le total et la somme des composantes, en ignorant les NA."""
    calculated = components.sum(axis=1)
    # comparer uniquement là où le total et le calculé ne sont pas NA
    mask = total.notna() & calculated.notna()
    return (total[mask] != calculated[mask]).sum()

mismatch_summary = pd.DataFrame({
    # vérification des consultations ambulatoires
    "allout_mismatch": [
        count_mismatch(
            dhis2_df["allout"],
            dhis2_df[["allout_u5", "allout_ov5"]]
        )
    ],

    # vérification des hospitalisations pour paludisme
    "maladm_mismatch": [
        count_mismatch(
            dhis2_df["maladm"],
            dhis2_df[["maladm_u5", "maladm_5_14", "maladm_ov15"]]
        )
    ],

    # vérification du total des tests
    "test_mismatch": [
        count_mismatch(
            dhis2_df["test"],
            dhis2_df[["test_hf", "test_com"]]
        )
    ],

    # vérification du total des cas confirmés
    "conf_mismatch": [
        count_mismatch(
            dhis2_df["conf"],
            dhis2_df[["conf_hf", "conf_com"]]
        )
    ],

    # vérification du total des cas traités
    "maltreat_mismatch": [
        count_mismatch(
            dhis2_df["maltreat"],
            dhis2_df[["maltreat_hf", "maltreat_com"]]
        )
    ],

    # vérification du total des cas présumés
    "pres_mismatch": [
        count_mismatch(
            dhis2_df["pres"],
            dhis2_df[["pres_hf", "pres_com"]]
        )
    ],

    # vérification des décès dus au paludisme
    "maldth_mismatch": [
        count_mismatch(
            dhis2_df["maldth"],
            dhis2_df[[
                "maldth_1_59m", "maldth_u5", "maldth_5_9", "maldth_10_14",
                "maldth_5_14", "maldth_fem_ov15", "maldth_mal_ov15", "maldth_ov15"
            ]]
        )
    ],

    # cas testés par groupe d'âge
    "test_u5_mismatch": [
        count_mismatch(
            dhis2_df["test_u5"],
            dhis2_df[["test_hf_u5", "test_com_u5"]]
        )
    ],
    "test_5_14_mismatch": [
        count_mismatch(
            dhis2_df["test_5_14"],
            dhis2_df[["test_hf_5_14", "test_com_5_14"]]
        )
    ],
    "test_ov15_mismatch": [
        count_mismatch(
            dhis2_df["test_ov15"],
            dhis2_df[["test_hf_ov15", "test_com_ov15"]]
        )
    ],

    # cas confirmés par groupe d'âge
    "conf_u5_mismatch": [
        count_mismatch(
            dhis2_df["conf_u5"],
            dhis2_df[["conf_hf_u5", "conf_com_u5"]]
        )
    ],
    "conf_5_14_mismatch": [
        count_mismatch(
            dhis2_df["conf_5_14"],
            dhis2_df[["conf_hf_5_14", "conf_com_5_14"]]
        )
    ],
    "conf_ov15_mismatch": [
        count_mismatch(
            dhis2_df["conf_ov15"],
            dhis2_df[["conf_hf_ov15", "conf_com_ov15"]]
        )
    ],

    # cas présumés par groupe d'âge
    "pres_u5_mismatch": [
        count_mismatch(
            dhis2_df["pres_u5"],
            dhis2_df[["pres_hf_u5", "pres_com_u5"]]
        )
    ],
    "pres_5_14_mismatch": [
        count_mismatch(
            dhis2_df["pres_5_14"],
            dhis2_df[["pres_hf_5_14", "pres_com_5_14"]]
        )
    ],
    "pres_ov15_mismatch": [
        count_mismatch(
            dhis2_df["pres_ov15"],
            dhis2_df[["pres_hf_ov15", "pres_com_ov15"]]
        )
    ],

    # cas suspects par groupe d'âge
    "susp_u5_mismatch": [
        count_mismatch(
            dhis2_df["susp_u5"],
            dhis2_df[["susp_hf_u5", "susp_com_u5"]]
        )
    ],
    "susp_5_14_mismatch": [
        count_mismatch(
            dhis2_df["susp_5_14"],
            dhis2_df[["susp_hf_5_14", "susp_com_5_14"]]
        )
    ],
    "susp_ov15_mismatch": [
        count_mismatch(
            dhis2_df["susp_ov15"],
            dhis2_df[["susp_hf_ov15", "susp_com_ov15"]]
        )
    ],

    # cas traités par groupe d'âge
    "maltreat_u5_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_u5"],
            dhis2_df[["maltreat_hf_u5", "maltreat_com_u5"]]
        )
    ],
    "maltreat_5_14_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_5_14"],
            dhis2_df[["maltreat_hf_5_14", "maltreat_com_5_14"]]
        )
    ],
    "maltreat_ov15_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_ov15"],
            dhis2_df[["maltreat_hf_ov15", "maltreat_com_ov15"]]
        )
    ],

    # vérifications du calendrier de traitement
    "maltreat_u24_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_u24_total"],
            dhis2_df[["maltreat_u24_hf", "maltreat_u24_com"]]
        )
    ],
    "maltreat_ov24_mismatch": [
        count_mismatch(
            dhis2_df["maltreat_ov24_total"],
            dhis2_df[["maltreat_ov24_hf", "maltreat_ov24_com"]]
        )
    ]
})

# pivot to long format for easier viewing
mismatch_summary = (
    mismatch_summary
    .melt(var_name="indicator", value_name="n_mismatches")
    # filtrer pour afficher uniquement les indicateurs avec divergences
    .query("n_mismatches > 0")
)

mismatch_summary

# identifier les lignes avec des totaux incohérents
incoherent_rows = dhis2_df[
    # vérification des consultations ambulatoires
    (dhis2_df["allout"] != (dhis2_df["allout_u5"] + dhis2_df["allout_ov5"])) |
    # vérification des hospitalisations pour paludisme
    (dhis2_df["maladm"] != (dhis2_df["maladm_u5"] + dhis2_df["maladm_5_14"] + dhis2_df["maladm_ov15"])) |
    # vérification du total des tests
    (dhis2_df["test"] != (dhis2_df["test_hf"] + dhis2_df["test_com"])) |
    # vérification du total des cas confirmés
    (dhis2_df["conf"] != (dhis2_df["conf_hf"] + dhis2_df["conf_com"])) |
    # vérification du total des cas traités
    (dhis2_df["maltreat"] != (dhis2_df["maltreat_hf"] + dhis2_df["maltreat_com"])) |
    # vérification du total des cas présumés
    (dhis2_df["pres"] != (dhis2_df["pres_hf"] + dhis2_df["pres_com"]))
]

# select relevant columns
incoherent_rows = incoherent_rows[
    ["hf", "periodname"] +
    [col for col in incoherent_rows.columns
     if any(x in col for x in ["allout", "maladm", "test", "conf", "maltreat", "pres"])]
]

# définir le chemin pour la sauvegarde
output_file = Path(
    "1.1.2_epidemiology",
    "1.1.2a_routine_surveillance",
    "processed",
    "sle_incoherent_totals_dhis2.xlsx"
)

# exporter en xlsx
incoherent_rows.to_excel(output_file, index=False)

# option 1 : classifier selon les patterns de noms d'établissements
def classify_facility(hf_name):
    """Classifier l'établissement comme IPD ou OPD selon les patterns de noms."""
    hf_lower = hf_name.lower() if pd.notna(hf_name) else ""

    # inpatient facilities (hospitals)
    if any(x in hf_lower for x in ["hospital", "hosp"]):
        return "IPD"
    # outpatient facilities (clinics, health posts, community health)
    elif any(x in hf_lower for x in ["chp", "chc", "mchp", "clinic", "health post", "health centre"]):
        return "OPD"
    # default to OPD
    else:
        return "OPD"

dhis2_df["facility_type"] = dhis2_df["hf"].apply(classify_facility)

# option 2 : classifier selon la présence d'indicateurs hospitaliers
has_ipd = (
    dhis2_df
    .groupby("hf_uid")
    .apply(
        lambda x: ((x["maladm"].notna() & (x["maladm"] > 0)).any() |
                   (x["maldth"].notna() & (x["maldth"] > 0)).any())
    )
    .reset_index(name="has_ipd")
)

dhis2_df = dhis2_df.merge(has_ipd, on="hf_uid", how="left")
dhis2_df["facility_type"] = dhis2_df["has_ipd"].map({True: "IPD", False: "OPD"})
dhis2_df = dhis2_df.drop(columns="has_ipd")

# option 3 : utiliser un fichier de référence de correspondance
facility_lookup = pd.read_excel(
    Path("path/to/facility_classification.xlsx")
)

dhis2_df = dhis2_df.merge(
    facility_lookup[["hf_uid", "facility_type"]],
    on="hf_uid",
    how="left"
)

# vérifier la distribution de la classification
dhis2_df[["hf_uid", "facility_type"]].drop_duplicates()["facility_type"].value_counts()

# identifier les combinaisons établissement-mois en double
duplicates = dhis2_df.groupby(["hf_uid", "yearmon"]).filter(lambda x: len(x) > 1)

# compter le nombre de paires de doublons
n_duplicates = duplicates[["hf_uid", "yearmon"]].drop_duplicates().shape[0]

# si des doublons existent, les inspecter
if n_duplicates > 0:
    # afficher les enregistrements en double avec les indicateurs clés
    duplicate_details = duplicates[
        ["record_id", "allout", "test", "conf", "maltreat"]
    ].sort_values(["hf_uid", "yearmon"])

    print(duplicate_details)

    # exporter pour révision avec l'équipe SNT
    duplicate_details.to_excel(
        Path(
            "1.1.2_epidemiology",
            "1.1.2a_routine_surveillance",
            "processed",
            "sle_duplicate_records_dhis2.xlsx",
        ),
        index=False,
    )

# option 1 : conserver le premier enregistrement (si les doublons sont des copies exactes)
dhis2_df = dhis2_df.drop_duplicates(subset=["hf_uid", "yearmon"], keep="first")

# option 2 : conserver l'enregistrement le plus complet
dhis2_df = (
    dhis2_df.assign(
        n_complete=lambda x: x.select_dtypes(include="number").notna().sum(axis=1)
    )
    .sort_values("n_complete", ascending=False)
    .drop_duplicates(subset=["record_id"], keep="first")
    .drop(columns="n_complete")
)

# option 3 : additionner les doublons (si les enregistrements représentent des rapports partiels)
group_cols = ["hf_uid", "yearmon", "hf", "adm0", "adm1", "adm2", "adm3"]
numeric_cols = dhis2_df.select_dtypes(include="number").columns.tolist()

dhis2_df = dhis2_df.groupby(group_cols, as_index=False)[numeric_cols].sum()

# verify duplicates resolved
n_remaining = dhis2_df.groupby(["record_id"]).filter(lambda x: len(x) > 1).shape[0]

# définir les descriptions pour les colonnes calculées/dérivées
computed_vars = pd.DataFrame([
    # colonnes de temps
    {"snt_var": "date", "indicator_label": "Report date (YYYY-MM-DD)"},
    {"snt_var": "year", "indicator_label": "Report year"},
    {"snt_var": "month", "indicator_label": "Report month (1-12)"},
    {"snt_var": "yearmon", "indicator_label": "Year-month label (e.g., Jan 2020)"},
    # colonnes d'identifiants
    {"snt_var": "hf_uid", "indicator_label": "Unique health facility identifier (hash)"},
    {"snt_var": "record_id", "indicator_label": "Unique record identifier (facility + month hash)"},
    {"snt_var": "location_short", "indicator_label": "Location label: adm1 ~ adm2"},
    {"snt_var": "location_full", "indicator_label": "Location label: adm1 ~ adm2 ~ adm3 ~ hf"},
    {"snt_var": "facility_type", "indicator_label": "Facility type (IPD/OPD)"},
    # totaux agrégés
    {"snt_var": "allout", "indicator_label": "Total outpatient visits (allout_u5 + allout_ov5)"},
    {"snt_var": "susp", "indicator_label": "Total suspected cases (all ages, HF + COM)"},
    {"snt_var": "test", "indicator_label": "Total tested (test_hf + test_com)"},
    {"snt_var": "test_hf", "indicator_label": "Total tested at health facility"},
    {"snt_var": "test_com", "indicator_label": "Total tested in community"},
    {"snt_var": "conf", "indicator_label": "Total confirmed cases (conf_hf + conf_com)"},
    {"snt_var": "conf_hf", "indicator_label": "Total confirmed cases at health facility"},
    {"snt_var": "conf_com", "indicator_label": "Total confirmed cases in community"},
    {"snt_var": "maltreat", "indicator_label": "Total treated cases (maltreat_hf + maltreat_com)"},
    {"snt_var": "maltreat_hf", "indicator_label": "Total treated cases at health facility"},
    {"snt_var": "maltreat_com", "indicator_label": "Total treated cases in community"},
    {"snt_var": "pres", "indicator_label": "Total presumed cases (pres_hf + pres_com)"},
    {"snt_var": "pres_hf", "indicator_label": "Total presumed cases at health facility"},
    {"snt_var": "pres_com", "indicator_label": "Total presumed cases in community"},
    {"snt_var": "maladm", "indicator_label": "Total malaria admissions (all ages)"},
    {"snt_var": "maldth", "indicator_label": "Total malaria deaths (all ages)"},
    # totaux par groupe d'âge
    {"snt_var": "test_u5", "indicator_label": "Total tested under 5 (HF + COM)"},
    {"snt_var": "test_5_14", "indicator_label": "Total tested 5-14 years (HF + COM)"},
    {"snt_var": "test_ov15", "indicator_label": "Total tested over 15 years (HF + COM)"},
    {"snt_var": "conf_u5", "indicator_label": "Confirmed cases under 5 (HF + COM)"},
    {"snt_var": "conf_5_14", "indicator_label": "Confirmed cases 5-14 years (HF + COM)"},
    {"snt_var": "conf_ov15", "indicator_label": "Confirmed cases over 15 years (HF + COM)"},
    {"snt_var": "maltreat_u5", "indicator_label": "Treated cases under 5 (HF + COM)"},
    {"snt_var": "maltreat_5_14", "indicator_label": "Treated cases 5-14 years (HF + COM)"},
    {"snt_var": "maltreat_ov15", "indicator_label": "Treated cases over 15 years (HF + COM)"},
    {"snt_var": "pres_u5", "indicator_label": "Presumed cases under 5 (HF + COM)"},
    {"snt_var": "pres_5_14", "indicator_label": "Presumed cases 5-14 years (HF + COM)"},
    {"snt_var": "pres_ov15", "indicator_label": "Presumed cases over 15 years (HF + COM)"},
    # ajouter les variables par groupe d'âge restantes si nécessaire...
])

# combiner les dictionnaires original et calculé
full_data_dict = (
    pd.concat([data_dict, computed_vars], ignore_index=True)
    .rename(columns={"snt_var": "snt_variable", "indicator_label": "label"})
    [["snt_variable", "label"]]
)

# conserver uniquement les colonnes présentes dans l'ensemble de données final
full_data_dict = (
    full_data_dict[full_data_dict["snt_variable"].isin(dhis2_df.columns)]
    .sort_values("snt_variable")
)

# vérifier
full_data_dict.head(10)

# définir l'ordre des colonnes
id_cols = ["record_id"]
location_cols = ["adm0", "adm1", "adm2", "adm3", "hf", "hf_uid",
                 "location_short", "location_full", "facility_type"]
time_cols = ["date", "yearmon", "year", "month"]

# obtenir les colonnes restantes in original order
ordered_cols = id_cols + location_cols + time_cols
remaining_cols = [col for col in dhis2_df.columns if col not in ordered_cols]

# réordonner le dataframe
dhis2_df = dhis2_df[ordered_cols + remaining_cols]

# vérifier l'ordre des colonnes
print(dhis2_df.columns[:20].tolist())

save_path = Path(
    here("01_data/02_epidemiology/2a_routine_surveillance/processed")
)

# sauvegarder les données en xlsx
dhis2_df.to_excel(
    save_path / "sle_dhis2_health_facility_data.xlsx",
    index=False
)

# sauvegarder le dictionnaire en xlsx
full_data_dict.to_excel(
    save_path / "sle_dhis2_health_facility_dict.xlsx",
    index=False
)

# sauvegarder en parquet
dhis2_df.to_parquet(
    save_path / "sle_dhis2_health_facility_data.parquet",
    compression="zstd",
    index=False
)

# définir les colonnes numériques à additionner (hors identifiants niveau établissement)
sum_cols = [
    # totaux
    "allout", "susp", "test", "conf", "pres", "maltreat", "maladm", "maldth",
    # par localisation
    "test_hf", "test_com", "conf_hf", "conf_com",
    "maltreat_hf", "maltreat_com", "pres_hf", "pres_com",
    # par groupe d'âge - totaux
    "allout_u5", "allout_ov5",
    "test_u5", "test_5_14", "test_ov15",
    "conf_u5", "conf_5_14", "conf_ov15",
    "maltreat_u5", "maltreat_5_14", "maltreat_ov15",
    "pres_u5", "pres_5_14", "pres_ov15",
    "susp_u5", "susp_5_14", "susp_ov15",
    "maladm_u5", "maladm_5_14", "maladm_ov15",
    "maldth_u5", "maldth_5_14", "maldth_ov15",
    # par âge et localisation
    "test_hf_u5", "test_hf_5_14", "test_hf_ov15",
    "test_com_u5", "test_com_5_14", "test_com_ov15",
    "conf_hf_u5", "conf_hf_5_14", "conf_hf_ov15",
    "conf_com_u5", "conf_com_5_14", "conf_com_ov15",
    "maltreat_hf_u5", "maltreat_hf_5_14", "maltreat_hf_ov15",
    "maltreat_com_u5", "maltreat_com_5_14", "maltreat_com_ov15",
    "pres_hf_u5", "pres_hf_5_14", "pres_hf_ov15",
    "pres_com_u5", "pres_com_5_14", "pres_com_ov15",
    # calendrier de traitement
    "maltreat_u24_hf", "maltreat_ov24_hf",
    "maltreat_u24_com", "maltreat_ov24_com",
    "maltreat_u24_total", "maltreat_ov24_total"
]

# fonction pour agréger à un niveau admin donné
def aggregate_admin(df, group_cols, sum_cols):
    # filter to columns that exist in dataframe
    existing_sum_cols = [c for c in sum_cols if c in df.columns]

    # aggregate
    agg_df = (
        df
        .groupby(group_cols, as_index=False)
        .agg(
            **{col: (col, "sum") for col in existing_sum_cols},
            n_facilities=("hf_uid", "count")
        )
    )

    return agg_df

# agréger au niveau adm3
group_cols_adm3 = ["adm0", "adm1", "adm2", "adm3", "year", "month", "yearmon"]
dhis2_adm3 = aggregate_admin(dhis2_df, group_cols_adm3, sum_cols)
dhis2_adm3["record_id"] = vdigest(
    dhis2_adm3["adm0"] + " " + dhis2_adm3["adm1"] + " " +
    dhis2_adm3["adm2"] + " " + dhis2_adm3["adm3"] + " " +
    dhis2_adm3["yearmon"].astype(str)
)
dhis2_adm3["location_short"] = dhis2_adm3["adm1"] + " ~ " + dhis2_adm3["adm2"]
dhis2_adm3["location_full"] = dhis2_adm3["adm1"] + " ~ " + dhis2_adm3["adm2"] + " ~ " + dhis2_adm3["adm3"]

# agréger au niveau adm2
group_cols_adm2 = ["adm0", "adm1", "adm2", "year", "month", "yearmon"]
dhis2_adm2 = aggregate_admin(dhis2_df, group_cols_adm2, sum_cols)
dhis2_adm2["record_id"] = vdigest(
    dhis2_adm2["adm0"] + " " + dhis2_adm2["adm1"] + " " +
    dhis2_adm2["adm2"] + " " + dhis2_adm2["yearmon"].astype(str)
)
dhis2_adm2["location_short"] = dhis2_adm2["adm1"] + " ~ " + dhis2_adm2["adm2"]
dhis2_adm2["location_full"] = dhis2_adm2["adm1"] + " ~ " + dhis2_adm2["adm2"]

# agréger au niveau adm1
group_cols_adm1 = ["adm0", "adm1", "year", "month", "yearmon"]
dhis2_adm1 = aggregate_admin(dhis2_df, group_cols_adm1, sum_cols)
dhis2_adm1["record_id"] = vdigest(
    dhis2_adm1["adm0"] + " " + dhis2_adm1["adm1"] + " " +
    dhis2_adm1["yearmon"].astype(str)
)
dhis2_adm1["location_short"] = dhis2_adm1["adm1"]
dhis2_adm1["location_full"] = dhis2_adm1["adm1"]

# créer des dictionnaires de données pour chaque niveau (filtrer sur les colonnes pertinentes uniquement)
id_cols = ["n_facilities", "record_id", "location_short", "location_full"]

# dictionnaire adm3 : inclut adm0, adm1, adm2, adm3
adm3_cols = group_cols_adm3 + [c for c in sum_cols if c in dhis2_adm3.columns] + id_cols
adm3_dict = full_data_dict[full_data_dict["snt_variable"].isin(adm3_cols)]

# dictionnaire adm2 : exclut adm3 (absent à ce niveau)
adm2_cols = group_cols_adm2 + [c for c in sum_cols if c in dhis2_adm2.columns] + id_cols
adm2_dict = full_data_dict[
    (full_data_dict["snt_variable"].isin(adm2_cols)) &
    (~full_data_dict["snt_variable"].isin(["adm3"]))
]

# dictionnaire adm1 : exclut adm2, adm3 (absents à ce niveau)
adm1_cols = group_cols_adm1 + [c for c in sum_cols if c in dhis2_adm1.columns] + id_cols
adm1_dict = full_data_dict[
    (full_data_dict["snt_variable"].isin(adm1_cols)) &
    (~full_data_dict["snt_variable"].isin(["adm2", "adm3"]))
]

# sauvegarder les données adm3
with pd.ExcelWriter(save_path / "sle_dhis2_adm3_data.xlsx") as writer:
    dhis2_adm3.to_excel(writer, sheet_name="data", index=False)
    adm3_dict.to_excel(writer, sheet_name="dictionary", index=False)
dhis2_adm3.to_parquet(save_path / "sle_dhis2_adm3_data.parquet", compression="zstd", index=False)

# sauvegarder les données adm2
with pd.ExcelWriter(save_path / "sle_dhis2_adm2_data.xlsx") as writer:
    dhis2_adm2.to_excel(writer, sheet_name="data", index=False)
    adm2_dict.to_excel(writer, sheet_name="dictionary", index=False)
dhis2_adm2.to_parquet(save_path / "sle_dhis2_adm2_data.parquet", compression="zstd", index=False)

# sauvegarder les données adm1
with pd.ExcelWriter(save_path / "sle_dhis2_adm1_data.xlsx") as writer:
    dhis2_adm1.to_excel(writer, sheet_name="data", index=False)
    adm1_dict.to_excel(writer, sheet_name="dictionary", index=False)
dhis2_adm1.to_parquet(save_path / "sle_dhis2_adm1_data.parquet", compression="zstd", index=False)
 

©2026 Applied Health Analytics for Delivery and Innovation. All rights reserved