| 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 |
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.
- 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 :
É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
# 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.
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).
- Chaque colonne doit représenter une variable (par exemple,
- Cellules : Chaque cellule doit contenir une seule valeur (non fusionnée ou groupée). Évitez les cellules contenant plusieurs valeurs (par exemple,
10/5pour 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émentairestidyr::pivot_longer()pour restructurer les données larges en format longdplyr::mutate()etseparate()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ésirablespandas.melt()pour restructurer les données larges en format longpandas.Series.str.split()etpandas.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.
Définir d’abord les chemins vers les données DHIS2.
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.
Dans les cas où les données sont réparties sur plusieurs onglets, par exemple divisées en onglets annuels, utilisez l’approche suivante.
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()Pour adapter le code :
- Ligne 3 : Définissez
core_routine_pathvers 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.
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.
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.
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,xlsxoucsv. - Ligne 4 : Définissez
core_routine_pathvers 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.
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
orgunitlevel3par le regroupement utilisé dans votre export (par exemple,adm2,adm1,year, circonscription, district ou tout regroupement DHIS2 personnalisé).
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
orgunitlevel3par 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.
# 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.
| 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.
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.
# 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)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_dfpar votre ensemble de données DHIS2. - Ligne 17 : Supprimez ou changez
orgunitlevel5si 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()Pour adapter le code :
- Ligne 2 : Mettez à jour le chemin du fichier si votre dictionnaire est stocké ailleurs.
- Ligne 6 : Changez
indicator_labeletsnt_varpour correspondre aux noms de colonnes de votre dictionnaire. - Ligne 9 : Remplacez
dhis2_dfpar 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.
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.
Format déjà en YYYY-MM-DD
Format « Jan 2020 » ou « January 2020 »
Noms de mois en français (par exemple, « Janvier 2020 »)
Noms de mois en portugais (par exemple, « Janeiro 2020 »)
Format « 2023-01 » ou « 2023/01 »
Format « 01/2020 » ou « 01-2020 » (Mois–Année)
Format compact « 202301 » (YYYYMM)
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()
)Format déjà en YYYY-MM-DD
Format « Jan 2020 » ou « January 2020 »
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_ex3Noms 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_ex4Formats 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_ex5Pour 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.
# 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()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()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).
# 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")
)Pour adapter le code :
- Ligne 6 : Mettez à jour
cache_pathvers 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_dfpar 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()etcase_when()pour correspondre à la structure administrative et aux conventions de dénomination de votre pays. - Ligne 39 : Remplacez
dhis2_dfpar le nom de votre dataframe cible. - Ligne 42 : Remplacez
lookup_keyspar votre jeu de données de correspondance ou de référence. - Lignes 43–46 : Ajustez
level0,level1,level2etlevel3pour 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")
)Pour adapter le code :
- Ligne 3 : Exécuter une fois dans le terminal pour installer le paquet
sntutils-pydepuis GitHub. - Ligne 4 : Instruction d’importation pour la fonction
prep_geonames. - Ligne 7 : Mettez à jour
cache_pathvers 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_dfpar 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 dictionnairedistrict_to_provincepour 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_dfpar le nom de votre dataframe cible. - Ligne 57 : Remplacez
shp_adm3par votre jeu de données de correspondance ou de référence. - Lignes 58–61 : Ajustez
level0,level1,level2etlevel3pour 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.
# 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()Pour adapter le code :
- Ligne 2 : Remplacez
dhis2_dfpar 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_shortpour inclure les niveaux admin souhaités pour les vues récapitulatives. - Ligne 12 : Modifiez
location_fullpour inclure tous les niveaux admin et le nom de l’établissement pour un libellé détaillé. - Ligne 14 : Assurez-vous que
yearmoncorrespond 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()Pour adapter le code :
- Ligne 8 : Remplacez
dhis2_dfpar 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_shortpour inclure les niveaux admin souhaités pour les vues récapitulatives. - Lignes 16–18 : Modifiez
location_fullpour inclure tous les niveaux admin et le nom de l’établissement pour un libellé détaillé. - Lignes 23–25 : Assurez-vous que
yearmoncorrespond 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.
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.
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()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)
)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.
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.
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_summaryPour adapter le code :
- Ligne 3 : Remplacez
dhis2_dfpar 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_summaryPour adapter le code :
- Lignes 10–156 : Remplacez
dhis2_dfpar 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.
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_dfpar 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_dfpar 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é
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.
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)Pour adapter le code :
- Ligne 2 : Remplacez
dhis2_dfpar 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()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.
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_dfpar 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_dfpar 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.
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.
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()Pour adapter le code :
- Lignes 4–77 : Examinez et mettez à jour
computed_varspour 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)Pour adapter le code :
- Lignes 7–49 : Examinez et mettez à jour
computed_varspour 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.
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)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())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.
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_pathvers 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_pathvers 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
.parquetest efficace pour les grands ensembles de données. Utilisez.csvsi 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.
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.
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"))Pour adapter le code :
- Lignes 2-30 : Mettez à jour
sum_colspour 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_idest créé avecsntutils::vdigest()en utilisant la hiérarchie admin complète (par exemple,adm0+adm1+adm2+adm3+yearmonpour le niveauadm3) pour garantir des identifiants uniques. Le code crée également les libelléslocation_shortetlocation_fullpour 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,
adm3exclu du dictionnaireadm2). - 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)Pour adapter le code :
- Lignes 2-30 : Mettez à jour
sum_colspour 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_idest créé avecvdigest()en utilisant la hiérarchie admin complète (par exemple,adm0+adm1+adm2+adm3+yearmonpour le niveauadm3) pour garantir des identifiants uniques. Le code crée également les libelléslocation_shortetlocation_fullpour 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,
adm3exclu du dictionnaireadm2). - 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.
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)