Accès aux soins obstétricaux d'urgence sous blocage routier, Haïti
Une ONG a désigné ses communes cibles il y a deux ans. Cette analyse estime le temps nécessaire aux femmes qui y vivent pour atteindre un hôpital. La page suit le calcul depuis les fichiers bruts jusqu'aux cinq scénarios de crise, avec le code utilisé à chaque étape.
- data/README_data.md
- notebooks/
- analyse.ipynb
- documentation_figures.py
- outputs/
- figures/11 images
- tables/9 CSV
- carte_interactive.html
- src/
- acces.py
- config.py
- p01_preparer_couches.py
- p02_construire_graphe.py
- p03_accessibilite_reference.py
- p04_scenarios_blocage.py
- p05_cartes.py
- style.py
- telecharger_donnees.py
- utils.py
- README.md
- Screenshot 2026-08-16 234553.png
- requirements.txt
- run_all.py
Sommaire
Sommaire
Accès routier aux hôpitaux
Deux ans après avoir identifié les communes où concentrer son programme de santé sexuelle et reproductive, l'ONG fictive Santé Commune Initiative examine le trajet jusqu'aux soins. Une urgence obstétricale exige une structure capable d'opérer, des routes praticables et des hôpitaux ouverts. Le premier ciblage reposait sur la demande potentielle et la densité des infrastructures sans mesurer ces conditions.
L'analyse estime la part de la demande obstétricale haïtienne située à moins d'une heure d'un hôpital, puis recalcule cette couverture lorsque des routes sont coupées ou que des structures ferment. Le commanditaire et son mandat sont inventés. Les données sont réelles et publiques, et tous les chiffres de cette page sortent de la chaîne décrite ici.
En un coup d'œil
Avec la définition la plus stricte de l'hôpital, un peu moins de huit femmes sur dix vivent à moins d'une heure d'une structure. Cette couverture dépend d'un petit nombre de tronçons.
Les six sources
Les six sources sont publiques. Les volumes ci-dessous sortent des journaux de la chaîne de traitement ; ils diffèrent parfois des chiffres annoncés par les producteurs.
| Source | Jeu | Niveau | Licence |
|---|---|---|---|
| HOT / OpenStreetMap | Réseau routier | 133 449 tronçons, dont 97 650 carrossables | ODbL |
| WorldPop | Population maillée 2020, contrainte, ajustée ONU | 100 m, agrégée à 1 km | CC BY 4.0 |
| OCHA COD-AB | Limites administratives | 140 communes | CC BY-IGO |
| OCHA COD-PS | Population 2024 par sexe et âge | Commune | CC BY-IGO |
| HOT / OpenStreetMap | Structures de santé | 2 073 points | ODbL |
| ACLED via HDX | Violences contre les civils | Commune, mensuel | Conditions ACLED |
Les fichiers exigent plusieurs ajustements avant leur lecture. Le millésime COD-AB décide si la colonne des noms de commune s'appelle ADM2_FR ou adm2_name. Les exports OCHA alternent entre UTF-8 et CP1252 selon la date de génération, ce qui suffit à faire échouer une lecture. La population féminine de 15 à 49 ans est reconstituée à partir de sept tranches quinquennales du COD-PS. Le code repère les noms de colonnes par motif et tente trois encodages avant d'abandonner.
Lecture tolérante aux millésimessrc/p01_preparer_couches.py
def lire_csv_tolerant(chemin: Path) -> pd.DataFrame:
"""Les exports OCHA alternent entre UTF-8 et CP1252 selon le millésime."""
for encodage in ("utf-8", "cp1252", "latin-1"):
try:
return pd.read_csv(chemin, encoding=encodage)
except UnicodeDecodeError:
continue
raise UnicodeDecodeError(f"encodage non reconnu pour {chemin}")
Nettoyer, qualifier, redresser
Trois opérations séparent les fichiers bruts des couches exploitables. Les choix faits à ce stade se retrouvent dans tous les chiffres qui suivent.
Qualifier les 2 073 points de santé
OpenStreetMap mélange dans un même fichier hôpitaux, cliniques, pharmacies, laboratoires et cabinets vétérinaires. Le tri se fait sur deux attributs qui ne concordent pas toujours, amenity et healthcare.
La règle exclut d'abord les pharmacies et les laboratoires, même lorsqu'une autre étiquette les associe aux soins. Elle répartit ensuite les autres points entre trois niveaux de certitude.
La règle de classementsrc/p01_preparer_couches.py
def classer(a: str, h: str) -> str:
# Une pharmacie ou un laboratoire sort du périmètre même s'il porte par
# ailleurs une étiquette de soins.
if a in TAGS_HORS_PERIMETRE or h in TAGS_HORS_PERIMETRE:
return "hors_perimetre"
if h == TAG_SONUC:
return "SONUC" # les deux etiquettes concordent
if a == TAG_SONUC:
return "HOPITAL_NC" # etablissement reel, nature incertaine
if a in TAGS_SONUB or h in TAGS_SONUB:
return "SONUB" # premiere ligne, sans cesarienne
return "hors_perimetre"
| Niveau | Critère | Points |
|---|---|---|
| SONUC | healthcare=hospital, capacité chirurgicale présumée | 198 |
| HOPITAL_NC | amenity=hospital seul, nature incertaine | 984 |
| SONUB | cliniques, cabinets, centres de santé | 233 |
| Hors périmètre | pharmacies, laboratoires, vétérinaires | 654 |
Les 984 points de la catégorie intermédiaire proviennent de la cartographie d'urgence de 2010. Des dispensaires et des centres de santé y ont été saisis comme hôpitaux sous l'étiquette amenity=hospital. L'étiquette healthcare, ajoutée plus tard et mieux renseignée, sert de filtre de confirmation.
À 60 minutes, la couverture passe de 78,1 % avec les 198 structures confirmées à 94,7 % en y ajoutant les 984 incertaines ; à 30 minutes, l'écart atteint 33,6 points. Dans les tests publiés ici, la définition de l'offre produit l'écart le plus important. L'analyse retient la définition stricte et publie les deux autres.
Rattraper les cellules du littoral
Chaque cellule de population reçoit le code de sa commune, nécessaire au calcul de la part de femmes en âge de procréer. La jointure spatiale échoue sur les cellules côtières. Le trait de côte des limites administratives est généralisé, celui de la grille de population ne l'est pas, et des cellules habitées tombent quelques centaines de mètres en mer.
Leur exclusion réduirait artificiellement la population littorale couverte par l'analyse. Elles sont donc rattachées à la commune la plus proche par sjoin_nearest.
Redresser WorldPop sur le recensement
WorldPop date de 2020 et décrit la répartition de la population à l'intérieur de chaque commune ; le COD-PS fixe l'effectif communal de 2024 sans le localiser. Les cellules sont donc multipliées, commune par commune, par le facteur qui ramène leur somme sur le total 2024.
Ce facteur vaut 1,24 en médiane, soit quatre années de croissance démographique. Son étendue va de 0,10 à 2,68 : dans 31 communes, WorldPop sous-estime de plus de moitié ; dans 6, il surestime d'un tiers. La couche finale hérite là de l'incertitude du COD-PS. Le code journalise médiane et étendue à chaque exécution, et vérifie que le redressement boucle à 1 % près.
Redressement et garde-fousrc/p01_preparer_couches.py
totaux_2020 = dedans.groupby("pcode")["pop_2020"].transform("sum")
cible = dedans["pcode"].map(communes.set_index("pcode")["pop_totale"])
facteur = np.where(totaux_2020 > 0, cible / totaux_2020, 0.0)
dedans["pop"] = dedans["pop_2020"] * facteur
total, attendu = dedans["pop"].sum(), communes["pop_totale"].sum()
exiger(
abs(total - attendu) / attendu < 0.01,
f"le redressement ne boucle pas : {total:,.0f} contre {attendu:,.0f}",
)
Après ces trois opérations : 8 259 cellules, 11,90 millions d'habitants, 3,25 millions de femmes de 15 à 49 ans.
Construction du graphe routier
Chaque polyligne OpenStreetMap stocke sa géométrie sans relation explicite avec les autres. Le calcul d'un plus court chemin exige de construire ces relations.
Chaque paire de points consécutifs devient une arête, et les points de forme identiques deviennent un même sommet. Ce dédoublonnage soude le réseau : deux tronçons qui se rejoignent partagent alors un nœud, et un itinéraire peut passer de l'un à l'autre.
Le cœur de la conversionsrc/p02_construire_graphe.py
# Toutes les positions sauf le dernier point de chaque polyligne ouvrent une
# arête vers la position suivante.
debut_arete = np.ones(len(coords), dtype=bool)
debut_arete[offsets[1:] - 1] = False
idx_u = np.nonzero(debut_arete)[0]
idx_v = idx_u + 1
# Dédoublonnage des sommets au mètre.
cle = np.round(coords).astype(np.int64)
sommets, inverse = np.unique(cle, axis=0, return_inverse=True)
u = inverse.ravel()[idx_u]
v = inverse.ravel()[idx_v]
Effet de l'arrondi au mètre
Le code arrondit les coordonnées avant de dédoublonner, au motif que la reprojection vers l'UTM 18 Nord passe par de l'arithmétique flottante et peut séparer de quelques millimètres deux extrémités identiques à la source.
Mesuré, l'effet est marginal. Sur 1 458 558 points de forme du réseau carrossable, le dédoublonnage exact fusionne 128 461 sommets, l'arrondi en ajoute 285. Soit 0,22 % des fusions.
La topologie vient d'OpenStreetMap, où les tronçons qui se croisent partagent déjà un nœud. L'arrondi corrige uniquement les écarts créés par l'arithmétique flottante et évite quelques micro-coupures. Son effet marginal ne rétablit pas la topologie du réseau.
Le graphe compte 1 329 812 sommets, 1 360 702 arêtes et 36 755 km de routes.
Les vitesses de parcours
Le poids d'une arête est un temps de parcours, calculé à partir d'une vitesse attribuée par classe de route. Après la définition de l'offre, ce paramètre est celui qui déplace le plus les résultats.
Les valeurs retenues sont des hypothèses de modélisation pour des vitesses effectives de porte à porte. Elles restent sensiblement plus basses que les vitesses réglementaires afin de tenir compte de l'état des revêtements, des franchissements non bitumés et des traversées d'agglomération.
| Classe OSM | km/h | Classe OSM | km/h |
|---|---|---|---|
| motorway | 70 | tertiary | 30 |
| trunk | 60 | unclassified | 25 |
| primary | 50 | residential | 20 |
| secondary | 40 | service, track | 12 |
Les valeurs retenues décrivent des vitesses effectives de porte à porte, inférieures aux vitesses réglementaires. Le réseau combine revêtement dégradé, franchissements de ravines non bitumés et traversées d'agglomération sans voie réservée. Les vitesses réglementaires produiraient des isochrones optimistes de 30 à 50 % dans ce type d'analyse fondée sur OpenStreetMap.
Dans l'aire métropolitaine de Port-au-Prince, le modèle réduit ces vitesses de 45 % supplémentaires pour représenter la congestion. Les vitesses observées en journée descendent couramment sous 15 km/h sur les axes secondaires. La réduction s'applique si le milieu de l'arête tombe dans la zone, ce qui évite de découper les tronçons à la frontière.
Application de la congestion urbainesrc/p02_construire_graphe.py
# Congestion urbaine : appliquée si le milieu de l'arête tombe dans la ZMPP.
mx = (coords[idx_u, 0] + coords[idx_v, 0]) / 2
my = (coords[idx_u, 1] + coords[idx_v, 1]) / 2
dans_zmpp = shapely.contains_xy(zmpp, mx, my)
vitesses = np.where(dans_zmpp, vitesses * FACTEUR_CONGESTION_ZMPP, vitesses)
temps = longueurs / (vitesses * 1000.0 / 60.0)
La cohérence d'ensemble des vitesses est contrôlée sur le trajet de Port-au-Prince à Saint-Marc, environ 95 km par la RN1. Le modèle l'estime autour de deux heures, conformément au temps de référence retenu hors période de blocage.
Les classes piétonnes sont exclues du graphe. Un transfert obstétrical n'emprunte ni sentier ni escalier.
La demande obstétricale
La couche de demande sort du redressement décrit plus haut, avec une pondération supplémentaire : chaque cellule est multipliée par la part communale des femmes de 15 à 49 ans, celle reconstituée en sommant les sept tranches quinquennales du COD-PS.
Deux arbitrages fixent la résolution. La grille WorldPop à 100 m est agrégée par blocs de dix, soit environ 1 km, parce que router depuis chaque cellule de 100 m serait coûteux et faussement précis au regard du réseau routier. Une cellule agrégée portant moins de dix habitants est écartée comme bruit de désagrégation.
Restent 8 259 cellules à router vers 198 hôpitaux.
Le calcul d'accessibilité
Résolu de front, le problème demande 8 259 recherches de plus court chemin sur un graphe de 1,3 million de sommets, à recommencer intégralement à chaque scénario simulé.
Le graphe étant non orienté, le temps d'une structure vers un nœud vaut aussi le temps de ce nœud vers la structure. On peut donc inverser le sens du calcul : partir simultanément des 198 hôpitaux et propager vers le réseau. Un seul parcours donne, en tout point, le temps vers la structure la plus proche.
Dijkstra multi-sourcessrc/acces.py
matrice = graphe.matrice(aretes_coupees)
resultat = dijkstra(
matrice,
directed=False,
indices=np.unique(sommets_offre), # toutes les structures a la fois
min_only=True, # on ne garde que le minimum
return_predecessors=avec_chemins,
)
...
minutes = minutes_acces + distances[sommets_demande]
Avec min_only=True, SciPy conserve pour chaque nœud la distance à la source la plus proche sans matérialiser une matrice de 198 lignes sur 1,3 million de colonnes.
Le dernier kilomètre
Une cellule de population tombe rarement sur un nœud routier. Le segment terminal, du centre de la cellule au nœud le plus proche, se parcourt à pied ou en moto-taxi sur piste. Il est compté à 4 km/h, et la distance à vol d'oiseau est multipliée par 1,3 puisqu'on ne marche pas en ligne droite.
Au-delà de 10 km à vol d'oiseau, la cellule est déclarée non rattachable. Lui attribuer une marche de plusieurs heures produirait un chiffre calculable qui ne décrit aucun trajet réel.
Rattachement au réseausrc/acces.py
distance, sommet = graphe.arbre().query(np.column_stack([x, y]), k=1)
minutes = distance * FACTEUR_DETOUR / (VITESSE_ACCES_KMH * 1000.0 / 60.0)
minutes = np.where(distance > DISTANCE_MAX_RATTACHEMENT_M, np.inf, minutes)
return sommet.astype("int64"), minutes
Deux tronçons OpenStreetMap peuvent relier exactement les mêmes sommets. Si la matrice creuse additionne les doublons, le temps entre ces deux sommets devient la somme des deux tronçons au lieu du plus rapide, et rien dans les sorties ne le signale. Le code regroupe donc les arêtes parallèles et retient le minimum, une fois pour toutes, avant les simulations.
Charge et criticité
Protéger en priorité les tronçons les plus fréquentés produit un mauvais classement.
La charge sert d'abord à présélectionner les tronçons en cumulant la demande le long de chaque itinéraire vers un hôpital. Le dommage est ensuite mesuré directement en recalculant l'accessibilité du pays sans chaque tronçon présélectionné.
Mesurer le dommage d'une coupuresrc/p04_scenarios_blocage.py
for rang, idx in enumerate(candidats, start=1):
masque = np.zeros(len(graphe.u), dtype=bool)
masque[idx] = True
minutes, _, _ = temps_vers_offre(
graphe, sommets_sonuc, sommets_demande, minutes_acces, aretes_coupees=masque
)
decroche = couvert_avant & (minutes > SEUIL_REFERENCE)
perdues = np.subtract(minutes, reference, out=np.zeros_like(reference), where=comparable)
Une contrainte d'espacement s'impose dès la présélection. Les arêtes sont les segments entre points de forme d'une polyligne, si bien que les cinquante tronçons les plus chargés du pays décrivent deux ou trois kilomètres du même axe. Sans espacement minimal, on évaluerait cent cinquante fois le même barrage. La règle impose 3 km entre deux candidats.
La configuration autorise 150 candidats. Le filtrage sur les communes exposées, les classes d'axes, la charge non nulle et l'espacement ramène la liste à 18 emplacements distincts, évalués un par un.
La corrélation de rang de Spearman entre les deux classements vaut 0,44. Le tronçon le plus chargé du pays porte 93 995 femmes et sa coupure coûte 52 033 minutes pondérées ; le plus coûteux en porte moitié moins, 47 243, pour 1 210 501 minutes, vingt-trois fois plus.
L'explication est géométrique. Une artère fréquentée d'un quartier maillé se contourne, sa coupure coûte quelques minutes. Un tronçon modeste qui constitue le seul franchissement d'une ravine coûte des heures. La charge compte les femmes dont l'itinéraire emprunte le tronçon, sans rien dire des détours possibles.
Le dommage est lui-même très concentré. Le tronçon le plus coûteux fait basculer 19 689 femmes au-delà d'une heure, soit 85 % de la perte totale des quinze coupures testées. À partir du sixième, chaque coupure supplémentaire n'ajoute presque rien.
Cinq scénarios
Portée des scénarios
Ces scénarios ne sont pas des observations. Le jeu ACLED diffusé sur HDX est agrégé au mois et à la commune. Il ne contient aucune coordonnée d'incident et ne permet donc pas de localiser un barrage réel.
La géographie des coupures présentée ici est un produit du modèle, calculé par une règle explicite et reproductible. Elle ne cartographie aucun barrage existant.
Trois scénarios portent sur le réseau routier, un sur l'offre de soins, un cumule les deux.
| Scénario | À 30 min | À 60 min | Médiane | Demande décrochée |
|---|---|---|---|---|
| Référence | 56,9 % | 78,1 % | 19 min | sans objet |
| A. 10 points de contrôle | 56,6 % | 77,3 % | 19 min | 23 131 |
| B. Encerclement de la ZMPP | 55,8 % | 76,3 % | 20 min | 57 056 |
| C. A et B cumulés | 55,4 % | 75,6 % | 20 min | 80 186 |
| D. Fermeture des hôpitaux de la ZMPP | 45,4 % | 77,5 % | 32 min | 18 235 |
| E. C et D cumulés | 41,3 % | 74,4 % | 33 min | 118 721 |
La nature du choc oriente l'affectation des moyens. La réhabilitation des routes et le maintien des structures ouvertes relèvent de lignes budgétaires différentes.
Les fermetures d'hôpitaux et les coupures de route affectent des indicateurs différents. Fermer les 77 hôpitaux de l'aire métropolitaine fait chuter la couverture à 30 minutes de 56,9 % à 45,4 % et porte le temps médian de 19 à 32 minutes, tout en laissant la couverture à 60 minutes quasi intacte à 77,5 %. Sous l'encerclement, la part de la demande sans aucun accès routier passe de 4,8 % à 5,9 %. Ces femmes n'ont alors plus d'itinéraire routier vers un hôpital.
Dans le scénario combiné, 118 721 femmes de 15 à 49 ans passent au-delà d'une heure. Les pertes suivent le corridor de la RN1 et touchent surtout Gressier, avec 72 % de la demande communale, Croix-des-Bouquets à 60 %, Cabaret à 56 % et Ganthier à 55 %.
Le choix du seuil
Le seuil de 120 minutes correspond à l'indicateur d'accès aux soins obstétricaux d'urgence de l'OMS. Il a été écarté comme indicateur principal pour une raison mesurée sur ces données.
À 120 minutes, la couverture de départ atteint 94,1 %. Fermer 77 hôpitaux sur 198 la fait passer à 94,0 %. Une baisse d'un dixième de point après la fermeture de 39 % du parc hospitalier rend ce seuil trop peu sensible pour comparer les scénarios.
Le seuil de référence est donc de 60 minutes. L'hémorragie du post-partum peut tuer en une à deux heures sans prise en charge.
Les seuils de 30 et 120 minutes restent publiés dans tous les tableaux. Ils permettent la comparaison avec les études existantes, et le seuil court est le seul à capter l'allongement des trajets provoqué par les fermetures, que 60 minutes manque entièrement.
Limites
La qualification SONU reste un proxy
OpenStreetMap localise les structures sans rien dire de leur capacité obstétricale : ni bloc opératoire fonctionnel, ni réserve de sang, ni équipe de nuit. Les couvertures mesurent donc la distance-temps à des structures étiquetées hôpitaux, sans garantie qu'une prise en charge chirurgicale y soit possible.
Les points de contrôle sont des scénarios
La règle de placement des points de contrôle est un choix de modélisation parmi d'autres possibles. Une version d'ACLED avec coordonnées s'intégrerait à la même chaîne en remplaçant le seul fichier de points.
Le réseau est traité comme non orienté
Les sens uniques ne sont pas modélisés. À l'échelle des déplacements intercommunaux, leur effet estimé reste inférieur à l'incertitude sur les vitesses. Cette omission demeure une limite, d'autant que l'attribut oneway est inégalement renseigné dans l'OpenStreetMap haïtien.
La capacité des structures n'est pas modélisée
L'analyse mesure un temps d'accès. Elle ignore la capacité d'une structure à recevoir une patiente de plus : un établissement à 20 minutes compte comme accessible quels que soient ses lits libres, son personnel ou la demande simultanée.
Reproduire
Six étapes, chacune écrivant ses sorties sur disque et lisant celles de la précédente : la chaîne reprend à mi-parcours sans refaire les étapes déjà passées.
Lancer la chaînerun_all.py
python -m venv .venv
python -m pip install -r requirements.txt
python run_all.py # chaine complete
python run_all.py --depuis 3 # reprend apres la construction du graphe
python run_all.py --etape 4 # relance les seuls scenarios
| Étape | Opération | Sortie principale |
|---|---|---|
| 0 | Téléchargement des sources | data/raw/ |
| 1 | Nettoyage, qualification, redressement | cellules.parquet |
| 2 | Construction du graphe routier | graphe.npz |
| 3 | Accessibilité de référence | reference.npz |
| 4 | Criticité des tronçons et scénarios | scenarios.parquet |
| 5 | Cartes et graphiques | outputs/ |
Sources déjà téléchargées, la chaîne complète prend une vingtaine de minutes, dont une dizaine pour la seule étape 4 au premier lancement, puisqu'elle recalcule l'accessibilité nationale pour chaque tronçon candidat. Elle descend à trois minutes quand les couches sont en cache disque.
L'étape 5 écrit sept cartes, huit tableaux et une carte interactive. Cette dernière rassemble en un seul fichier les couches que les cartes statiques présentent séparément : déficit d'accès par commune, tronçons les plus chargés, cellules décrochées dans le scénario combiné, hôpitaux et points de contrôle simulés, chacune activable indépendamment.
Tous les paramètres de modélisation vivent dans src/config.py, jamais en dur dans les scripts, et chacun y porte sa justification.
Les quatre figures propres à cette page sortent d'un carnet séparé, notebooks/documentation_figures.ipynb, qui relit les sorties de la chaîne sans rien recalculer.