L'inflation en Haïti : diagnostic 1990-2025 et projection 2026-2030¶

Autrice : Woodnalie Joseph. Août 2026. Données arrêtées à décembre 2025 pour les prix, mars 2026 pour le change, téléchargées de la Banque mondiale le 15 août 2026.

Objet. Ce carnet documente le comportement de l'indice des prix à la consommation haïtien depuis 1990, identifie les régularités exploitables (persistance, saisonnalité, lien avec le taux de change), puis produit une projection à cinq ans, de janvier 2026 à décembre 2030, assortie d'intervalles de confiance et de trois scénarios de change.

Données. Indice des prix à la consommation mensuel et taux de change officiel HTG/USD, séries publiées par la Banque mondiale dans le Global Economic Monitor, construites à partir des publications de l'IHSI et de la BRH. Cadre macroéconomique annuel issu des World Development Indicators. La collecte est faite par src/fetch_data.py.

Plan.

  1. Données, cassure de série et couverture
  2. Faits stylisés
  3. Diagnostics de série temporelle
  4. Le canal du change : mesure du pass-through
  5. Modèles concurrents et validation hors échantillon
  6. Projection 2026-2030 et scénarios
  7. Limites et lecture des résultats

Résultat principal, annoncé d'emblée. Sur cinq ans, la fourchette plausible est large et le désaccord entre modèles est plus informatif que n'importe quel chiffre unique : les modèles à retour à la moyenne ramènent l'inflation vers 12 à 16 % par an, les modèles de persistance la maintiennent près de 25 %. La section 6 montre pourquoi, et ce qui départage les deux.

In [1]:
from __future__ import annotations

import sys
import warnings
from pathlib import Path

import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import statsmodels.api as sm
from matplotlib.colors import LinearSegmentedColormap, TwoSlopeNorm
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from statsmodels.tsa.statespace.sarimax import SARIMAX
from statsmodels.tsa.stattools import adfuller, kpss

warnings.simplefilter("ignore")

RACINE = Path.cwd()
if not (RACINE / "data").exists():
    RACINE = RACINE.parent
sys.path.insert(0, str(RACINE / "src"))

import theme  # noqa: E402

theme.appliquer_theme()

DONNEES = RACINE / "data" / "processed"
SORTIES = RACINE / "outputs"
SORTIES.mkdir(parents=True, exist_ok=True)

ALEA = np.random.default_rng(20260815)
pd.set_option("display.width", 120)
pd.set_option("display.float_format", lambda v: f"{v:,.2f}")


def exporter(objet, nom: str, **kwargs) -> Path:
    """Écrit une table dans outputs/ en UTF-8 avec BOM, lisible par Excel."""
    chemin = SORTIES / nom
    objet.to_csv(chemin, encoding="utf-8-sig", **kwargs)
    return chemin


vintage = (RACINE / "data" / "raw" / "_telechargement.txt").read_text(encoding="utf-8").strip()
print(vintage)
Données téléchargées le 2026-08-15 19:48 UTC depuis api.worldbank.org

1. Données, cassure de série et couverture¶

1.1 Une cassure de raccordement en janvier 1997¶

La série mensuelle brute contient une chute de 42 % entre décembre 1996 et janvier 1997. Ce n'est pas de la déflation : c'est un changement de base mal raccordé par le fournisseur. Le contrôle est simple. Rapporté à l'indice annuel officiel des World Development Indicators, le rapport des deux séries est exactement constant à partir de 1997 et vaut 1,75 à 1,85 avant. Le segment antérieur est donc sur une autre base.

La correction appliquée est un chaînage : le segment d'avant 1997 est multiplié par le facteur qui rend l'inflation moyenne de 1997 égale à celle publiée dans les comptes annuels. Elle ne touche que le carnet : le fichier data/processed/haiti_mensuel.csv garde la série brute telle que reçue, ce qui rend la correction vérifiable et réversible.

In [2]:
mensuel = pd.read_csv(DONNEES / "haiti_mensuel.csv", parse_dates=["date"], index_col="date").asfreq("MS")
annuel = pd.read_csv(DONNEES / "haiti_annuel.csv", index_col="annee")

RUPTURE = pd.Timestamp("1997-01-01")
moyenne_1996 = mensuel.loc[mensuel.index.year == 1996, "ipc"].mean()
moyenne_1997 = mensuel.loc[mensuel.index.year == 1997, "ipc"].mean()
inflation_1997_officielle = float(annuel.loc[1997, "inflation_pct"])
facteur = (moyenne_1997 / (1 + inflation_1997_officielle / 100)) / moyenne_1996

print(f"Variation brute décembre 1996 vers janvier 1997 : "
      f"{(mensuel.loc['1997-01-01', 'ipc'] / mensuel.loc['1996-12-01', 'ipc'] - 1) * 100:.1f} %")
print(f"Inflation 1997 publiée dans les comptes annuels : {inflation_1997_officielle:.2f} %")
print(f"Facteur de chaînage appliqué avant 1997 : {facteur:.4f}")

mensuel.loc[mensuel.index < RUPTURE, "ipc"] *= facteur
base_2010 = mensuel.loc[mensuel.index.year == 2010, "ipc"].mean()
mensuel["ipc"] = mensuel["ipc"] / base_2010 * 100

print(f"Variation corrigée décembre 1996 vers janvier 1997 : "
      f"{(mensuel.loc['1997-01-01', 'ipc'] / mensuel.loc['1996-12-01', 'ipc'] - 1) * 100:.1f} %")

exporter(pd.DataFrame({
    "rupture": [RUPTURE.strftime("%Y-%m")],
    "facteur de chaînage": [round(facteur, 4)],
    "ancrage": [f"inflation 1997 des comptes annuels ({inflation_1997_officielle:.2f} %)"],
}), "correction_rupture_1997.csv", index=False)
Variation brute décembre 1996 vers janvier 1997 : -41.8 %
Inflation 1997 publiée dans les comptes annuels : 20.56 %
Facteur de chaînage appliqué avant 1997 : 0.5881
Variation corrigée décembre 1996 vers janvier 1997 : -1.1 %
Out[2]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/outputs/correction_rupture_1997.csv')
In [3]:
ipc = mensuel["ipc"].dropna()
change = mensuel["taux_change"].dropna()

mensuel["inflation_ga"] = mensuel["ipc"].pct_change(12) * 100
mensuel["inflation_mm"] = mensuel["ipc"].pct_change() * 100
mensuel["depreciation_ga"] = mensuel["taux_change"].pct_change(12) * 100

couverture = pd.DataFrame(
    {
        "début": [ipc.index.min().strftime("%Y-%m"), change.index.min().strftime("%Y-%m")],
        "fin": [ipc.index.max().strftime("%Y-%m"), change.index.max().strftime("%Y-%m")],
        "observations": [len(ipc), len(change)],
        "dernière valeur": [round(ipc.iloc[-1], 1), round(change.iloc[-1], 2)],
    },
    index=["IPC (base 2010 = 100)", "Taux de change HTG/USD"],
)
couverture
Out[3]:
début fin observations dernière valeur
IPC (base 2010 = 100) 1990-01 2025-12 432 996.90
Taux de change HTG/USD 1990-01 2026-03 435 131.11

1.2 Contrôle de cohérence avec les comptes annuels¶

La série mensuelle et l'indice annuel officiel sont deux publications distinctes. Agréger la première en moyennes annuelles et comparer à la seconde teste l'intégrité du raccordement sur toute la période. C'est ce contrôle qui a révélé la cassure de 1997, et il délimite précisément la zone à traiter avec prudence.

In [4]:
controle = pd.DataFrame({
    "inflation reconstituée (%)": ipc.groupby(ipc.index.year).mean().pct_change() * 100,
    "inflation officielle (%)": annuel["inflation_pct"],
}).dropna()
controle["écart (points)"] = (controle["inflation reconstituée (%)"]
                              - controle["inflation officielle (%)"])
exporter(controle, "controle_coherence_annuelle.csv", float_format="%.3f")

apres_rupture = controle.loc[1998:, "écart (points)"]
print(f"À partir de 1998 : écart absolu maximal de {apres_rupture.abs().max():.4f} point de %. "
      f"Les deux publications coïncident, la série mensuelle est fiable sur cette plage.")
suspect = controle.loc[1991:1996, "écart (points)"]
print(f"Avant 1997 : écarts de {suspect.min():+.1f} à {suspect.max():+.1f} point de %. "
      f"Le segment reproduit l'inflation officielle en 1991 et 1992 puis s'en éloigne "
      f"de 1993 à 1996, ce qui trahit un assemblage de sources différentes.")
print(f"À noter, une hausse isolée de "
      f"{(mensuel.loc['1996-11-01', 'ipc'] / mensuel.loc['1996-10-01', 'ipc'] - 1) * 100:.1f} % "
      f"en novembre 1996, visible comme une marche sur la figure 1 : elle appartient à ce "
      f"segment douteux, jamais à l'échantillon d'estimation qui démarre en 2004.")
controle.loc[1991:2000].round(2)
À partir de 1998 : écart absolu maximal de 0.0000 point de %. Les deux publications coïncident, la série mensuelle est fiable sur cette plage.
Avant 1997 : écarts de -7.1 à +3.2 point de %. Le segment reproduit l'inflation officielle en 1991 et 1992 puis s'en éloigne de 1993 à 1996, ce qui trahit un assemblage de sources différentes.
À noter, une hausse isolée de 13.2 % en novembre 1996, visible comme une marche sur la figure 1 : elle appartient à ce segment douteux, jamais à l'échantillon d'estimation qui démarre en 2004.
Out[4]:
inflation reconstituée (%) inflation officielle (%) écart (points)
1991 15.42 15.42 0.00
1992 19.36 19.36 0.00
1993 22.56 29.71 -7.15
1994 42.56 39.33 3.23
1995 25.46 27.61 -2.14
1996 19.66 20.58 -0.92
1997 20.56 20.56 -0.00
1998 5.27 5.27 -0.00
1999 3.00 3.00 -0.00
2000 9.33 9.33 -0.00
In [5]:
derniere = mensuel["inflation_ga"].dropna()
print(f"Inflation en glissement annuel, {theme.mois_fr(derniere.index[-1])} : {derniere.iloc[-1]:.1f} %")
print(f"Pic des trois dernières années : {derniere.loc['2023':].max():.1f} % "
      f"en {theme.mois_fr(derniere.loc['2023':].idxmax())}")
print(f"Niveau des prix : l'indice a été multiplié par "
      f"{ipc.iloc[-1] / ipc.loc['2019-12-01']:.1f} depuis décembre 2019.")
print(f"Taux de change : {change.loc['2019-12-01']:.1f} HTG/USD en décembre 2019, "
      f"{change.iloc[-1]:.1f} en {theme.mois_fr(change.index[-1])}.")
Inflation en glissement annuel, décembre 2025 : 25.0 %
Pic des trois dernières années : 49.3 % en janvier 2023
Niveau des prix : l'indice a été multiplié par 4.3 depuis décembre 2019.
Taux de change : 96.9 HTG/USD en décembre 2019, 131.1 en mars 2026.

2. Faits stylisés¶

2.1 Un niveau de prix qui change d'échelle¶

L'échelle logarithmique est ici la bonne lecture : une pente constante correspond à un taux d'inflation constant. Les ruptures de pente marquent les changements de régime.

In [6]:
evenements = [
    ("1991-10-01", "Embargo", 0.965, "left"),
    ("2004-02-01", "Crise politique", 0.965, "left"),
    ("2008-04-01", "Crise alimentaire", 0.88, "left"),
    ("2010-01-01", "Séisme", 0.965, "left"),
    ("2019-09-01", "Peyi lòk", 0.88, "left"),
    ("2022-09-01", "Fin des subventions\nsur les carburants", 0.965, "right"),
]

fig, ax = plt.subplots(figsize=(10.5, 5.4))
ax.plot(ipc.index, ipc.values, color=theme.SERIES["bleu"])
ax.set_yscale("log")
ax.set_yticks([3, 10, 30, 100, 300, 1000])
ax.get_yaxis().set_major_formatter(plt.ScalarFormatter())
transformation = ax.get_xaxis_transform()
for date, libelle, hauteur, cote in evenements:
    d = pd.Timestamp(date)
    ax.axvline(d, color=theme.ENCRE["axe"], linewidth=0.8, zorder=0)
    ax.annotate(libelle, xy=(mdates.date2num(d), hauteur), xycoords=transformation,
                xytext=(5 if cote == "left" else -5, 0), textcoords="offset points",
                fontsize=8.5, color=theme.ENCRE["attenue"], va="top", ha=cote)
theme.etiqueter_fin(ax, ipc.index[-1], ipc.iloc[-1], f"{ipc.iloc[-1]:,.0f}".replace(",", " "),
                    theme.SERIES["bleu"], decalage=(8, -14))
theme.titrer(ax, "Le niveau des prix en Haïti, 1990-2025",
             "Indice des prix à la consommation, base 2010 = 100, échelle logarithmique")
ax.set_xlabel("")
ax.set_ylabel("Indice (échelle log)")
ax.xaxis.set_major_locator(mdates.YearLocator(5))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
ax.set_xlim(ipc.index[0], pd.Timestamp("2028-06-01"))
theme.source(fig, "Source : Banque mondiale, Global Economic Monitor (données IHSI), "
                  "segment antérieur à 1997 chaîné sur les comptes annuels.")
theme.sauver(fig, "fig01_niveau_prix.png")
Out[6]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig01_niveau_prix.png')
No description has been provided for this image

2.2 Trois régimes d'inflation¶

La série en glissement annuel sépare nettement trois épisodes : les années d'embargo, une longue phase à un chiffre entre 2011 et 2018, puis le régime ouvert en 2019 où l'inflation dépasse durablement 20 %.

In [7]:
sous_periodes = [
    ("1991-1995", "1991", "1995"),
    ("1996-2003", "1996", "2003"),
    ("2004-2010", "2004", "2010"),
    ("2011-2018", "2011", "2018"),
    ("2019-2025", "2019", "2025"),
]
resume_periodes = pd.DataFrame(
    [
        {
            "période": nom,
            "inflation moyenne (%)": mensuel.loc[debut:fin, "inflation_ga"].mean(),
            "médiane (%)": mensuel.loc[debut:fin, "inflation_ga"].median(),
            "minimum (%)": mensuel.loc[debut:fin, "inflation_ga"].min(),
            "maximum (%)": mensuel.loc[debut:fin, "inflation_ga"].max(),
            "écart-type (%)": mensuel.loc[debut:fin, "inflation_ga"].std(),
            "dépréciation moyenne (%)": mensuel.loc[debut:fin, "depreciation_ga"].mean(),
        }
        for nom, debut, fin in sous_periodes
    ]
).set_index("période")
exporter(resume_periodes, "faits_stylises_sous_periodes.csv", float_format="%.2f")
resume_periodes.round(1)
Out[7]:
inflation moyenne (%) médiane (%) minimum (%) maximum (%) écart-type (%) dépréciation moyenne (%)
période
1991-1995 25.20 22.10 8.90 56.40 11.30 38.60
1996-2003 13.40 9.60 2.20 35.50 9.40 11.10
2004-2010 10.60 10.30 -4.60 32.90 7.30 0.50
2011-2018 7.60 6.60 2.60 15.40 3.50 7.20
2019-2025 26.50 25.20 10.90 49.30 9.20 12.20
In [8]:
infl = mensuel["inflation_ga"].dropna()
moyenne_longue = infl.mean()

fig, ax = plt.subplots(figsize=(10.5, 5.4))
ax.axhspan(0, 10, color=theme.SERIES["aqua"], alpha=0.07, zorder=0)
ax.plot(infl.index, infl.values, color=theme.SERIES["bleu"], label="Inflation en glissement annuel")
ax.axhline(moyenne_longue, color=theme.SERIES["orange"], linewidth=1.4,
           label=f"Moyenne 1991-2025 ({moyenne_longue:.1f} %)")
ax.axhline(0, color=theme.ENCRE["axe"], linewidth=0.8)
theme.etiqueter_fin(ax, infl.index[-1], infl.iloc[-1], f"{infl.iloc[-1]:.0f} %", theme.SERIES["bleu"])
ax.annotate("Zone à un chiffre", xy=(pd.Timestamp("1998-06-01"), 4.2), fontsize=8.5,
            color=theme.ENCRE["attenue"], va="center")
theme.titrer(ax, "Trois régimes d'inflation en trente-cinq ans",
             "Variation de l'IPC sur douze mois, en pourcentage")
ax.set_ylabel("%")
ax.set_xlabel("")
ax.xaxis.set_major_locator(mdates.YearLocator(5))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
ax.set_xlim(infl.index[0], pd.Timestamp("2027-06-01"))
ax.legend(loc="upper right")
theme.source(fig, "Source : Banque mondiale, Global Economic Monitor (données IHSI).")
theme.sauver(fig, "fig02_inflation_glissement_annuel.png")
Out[8]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig02_inflation_glissement_annuel.png')
No description has been provided for this image

2.3 Une saisonnalité marquée¶

Le profil mensuel n'est pas plat : octobre concentre la plus forte hausse mensuelle moyenne, ce qui correspond à la rentrée scolaire et à la fin de la période de soudure agricole. Janvier est le mois le plus calme.

In [9]:
mm = np.log(mensuel["ipc"]).diff().mul(100).dropna()
mm_recent = mm.loc["2004":]
profil = mm_recent.groupby(mm_recent.index.month).mean()
mois_nom = ["jan", "fév", "mar", "avr", "mai", "juin", "juil", "août", "sep", "oct", "nov", "déc"]

fig, axes = plt.subplots(1, 2, figsize=(12.5, 4.8), gridspec_kw={"width_ratios": [1, 1.35]})

ax = axes[0]
couleurs = [theme.SERIES["orange"] if v == profil.max() else theme.SERIES["bleu"] for v in profil.values]
ax.bar(range(1, 13), profil.values, color=couleurs, width=0.68)
ax.set_xticks(range(1, 13), mois_nom)
ax.annotate(f"{profil.max():.2f} %", xy=(profil.idxmax(), profil.max()), xytext=(0, 5),
            textcoords="offset points", ha="center", fontsize=9, weight="600",
            color=theme.SERIES["orange"])
theme.titrer(ax, "Octobre porte la hausse", "Variation mensuelle moyenne de l'IPC, 2004-2025, en %")
ax.set_ylabel("%")

ax = axes[1]
matrice = mm.loc["2010":].to_frame("v")
matrice["annee"] = matrice.index.year
matrice["mois"] = matrice.index.month
grille = matrice.pivot_table(index="annee", columns="mois", values="v")
exporter(grille, "saisonnalite_mensuelle.csv", float_format="%.3f")

divergente = LinearSegmentedColormap.from_list(
    "divergente",
    ["#0d366b", "#256abf", "#6da7ec", "#cde2fb", "#f0efec", "#f7cfcf", "#e88b8b", "#cf4d4d", "#8f2323"],
)
norme = TwoSlopeNorm(vmin=-2, vcenter=0, vmax=4)
im = ax.pcolormesh(np.arange(1, 14), np.arange(grille.index.min(), grille.index.max() + 2),
                   grille.values, cmap=divergente, norm=norme,
                   edgecolors=theme.ENCRE["surface"], linewidth=1.2)
ax.set_xticks(np.arange(1.5, 13.5), mois_nom)
ax.set_yticks(np.arange(grille.index.min() + 0.5, grille.index.max() + 1.5, 2),
              [str(a) for a in range(grille.index.min(), grille.index.max() + 1, 2)])
ax.invert_yaxis()
ax.grid(False)
for cote in ("top", "right", "left", "bottom"):
    ax.spines[cote].set_visible(False)
barre = fig.colorbar(im, ax=ax, pad=0.02, aspect=28, extend="max")
barre.set_label("Variation mensuelle, %", color=theme.ENCRE["secondaire"], fontsize=9)
barre.outline.set_visible(False)
barre.ax.tick_params(colors=theme.ENCRE["attenue"], labelsize=9)
theme.titrer(ax, "Chaque mois depuis 2010", "Variation mensuelle de l'IPC, en %")
theme.source(fig, "Source : calculs sur données Banque mondiale. "
                  "Valeurs dans outputs/saisonnalite_mensuelle.csv.")
theme.sauver(fig, "fig03_saisonnalite.png")
Out[9]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig03_saisonnalite.png')
No description has been provided for this image

2.4 Prix et taux de change¶

Les deux séries sont indexées sur une même base pour être comparées sur un seul axe. Jusqu'en 2023 elles avancent ensemble. Depuis 2024, la gourde est stable autour de 131 pour un dollar alors que les prix continuent de monter de près de 25 % par an. Le change ne peut donc pas être le moteur principal de l'inflation actuelle.

In [10]:
base = pd.Timestamp("2015-01-01")
ipc_idx = ipc / ipc.loc[base] * 100
fx_idx = change / change.loc[base] * 100
depuis = pd.Timestamp("2012-01-01")

fig, ax = plt.subplots(figsize=(10.5, 5.4))
ax.plot(ipc_idx.loc[depuis:].index, ipc_idx.loc[depuis:].values,
        color=theme.SERIES["bleu"], label="Indice des prix à la consommation")
ax.plot(fx_idx.loc[depuis:].index, fx_idx.loc[depuis:].values,
        color=theme.SERIES["orange"], label="Taux de change HTG/USD")
theme.etiqueter_fin(ax, ipc_idx.index[-1], ipc_idx.iloc[-1], "Prix", theme.SERIES["bleu"])
theme.etiqueter_fin(ax, fx_idx.index[-1], fx_idx.iloc[-1], "Change", theme.SERIES["orange"])
ax.axvspan(pd.Timestamp("2024-01-01"), fx_idx.index[-1], color=theme.ENCRE["grille"], alpha=0.45, zorder=0)
ax.annotate("Gourde stable,\nprix toujours en hausse", xy=(pd.Timestamp("2024-02-01"), 520),
            fontsize=9, color=theme.ENCRE["secondaire"], va="top")
ax.annotate("Appréciation brutale\nde septembre 2020", xy=(pd.Timestamp("2020-10-01"), 135),
            xytext=(pd.Timestamp("2017-06-01"), 330), fontsize=9,
            color=theme.ENCRE["secondaire"], va="center",
            arrowprops={"arrowstyle": "-", "color": theme.ENCRE["axe"], "linewidth": 0.8})
theme.titrer(ax, "La gourde s'est stabilisée, les prix non",
             "Indices en base 100 = janvier 2015, une seule échelle")
ax.set_ylabel("Base 100 = janvier 2015")
ax.set_xlabel("")
ax.legend(loc="upper left")
ax.set_xlim(depuis, pd.Timestamp("2027-06-01"))
ax.set_ylim(0, 900)
theme.source(fig, "Source : Banque mondiale, Global Economic Monitor.")
theme.sauver(fig, "fig04_prix_et_change.png")
Out[10]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig04_prix_et_change.png')
No description has been provided for this image
In [11]:
correl = (mensuel["inflation_ga"].rolling(36).corr(mensuel["depreciation_ga"])).dropna()

fig, axes = plt.subplots(1, 2, figsize=(12.5, 4.8))

ax = axes[0]
ax.plot(correl.index, correl.values, color=theme.SERIES["violet"])
ax.axhline(0, color=theme.ENCRE["axe"], linewidth=0.8)
theme.etiqueter_fin(ax, correl.index[-1], correl.iloc[-1], f"{correl.iloc[-1]:.2f}", theme.SERIES["violet"])
theme.titrer(ax, "Corrélation glissante prix-change", "Fenêtre de 36 mois, glissements annuels")
ax.set_ylabel("Coefficient de corrélation")
ax.set_xlim(correl.index[0], pd.Timestamp("2027-06-01"))
ax.xaxis.set_major_locator(mdates.YearLocator(5))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))

ax = axes[1]
nuage = mensuel.loc["2004":, ["depreciation_ga", "inflation_ga"]].dropna()
recent = nuage.index >= pd.Timestamp("2024-01-01")
ax.scatter(nuage.loc[~recent, "depreciation_ga"], nuage.loc[~recent, "inflation_ga"],
           s=26, color=theme.SERIES["bleu"], alpha=0.55, edgecolors=theme.ENCRE["surface"],
           linewidths=1.2, label="2004-2023")
ax.scatter(nuage.loc[recent, "depreciation_ga"], nuage.loc[recent, "inflation_ga"],
           s=34, color=theme.SERIES["orange"], edgecolors=theme.ENCRE["surface"],
           linewidths=1.2, label="2024-2025")
pente, ordonnee = np.polyfit(nuage["depreciation_ga"], nuage["inflation_ga"], 1)
grille_x = np.linspace(nuage["depreciation_ga"].min(), nuage["depreciation_ga"].max(), 50)
ax.plot(grille_x, ordonnee + pente * grille_x, color=theme.ENCRE["secondaire"], linewidth=1.2,
        label=f"Ajustement : pente {pente:.2f}")
ax.set_xlabel("Dépréciation de la gourde sur 12 mois, %")
ax.set_ylabel("Inflation sur 12 mois, %")
theme.titrer(ax, "Les mois récents sortent du nuage", "Chaque point est un mois, 2004-2025")
ax.legend(loc="upper left")
theme.source(fig, "Source : calculs sur données Banque mondiale. Les points de 2024-2025 combinent "
                  "une gourde stable, voire en appréciation, et une inflation de 25 à 32 %.")
theme.sauver(fig, "fig05_correlation_change.png")
Out[11]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig05_correlation_change.png')
No description has been provided for this image

2.5 Une inflation sans croissance¶

Le contexte compte pour interpréter la projection. L'inflation haïtienne n'accompagne pas une expansion : le PIB réel recule chaque année depuis 2019. C'est le profil d'une contraction de l'offre, pas d'une surchauffe de la demande, et cela pèse sur la lecture des scénarios de la section 6.

In [12]:
contexte = annuel.loc[2018:, ["croissance_pib_pct", "inflation_pct",
                              "pib_par_tete_usd2015", "transferts_pib_pct"]]
contexte.columns = ["croissance du PIB réel (%)", "inflation (%)",
                    "PIB par tête (USD 2015)", "transferts reçus (% du PIB)"]
exporter(contexte, "contexte_macroeconomique.csv", float_format="%.2f")

annees_recul = annuel.loc[2019:, "croissance_pib_pct"]
perte = (annuel.loc[2025, "pib_par_tete_usd2015"] / annuel.loc[2018, "pib_par_tete_usd2015"] - 1) * 100
print(f"Le PIB réel recule {(annees_recul < 0).sum()} années consécutives, de 2019 à 2025.")
print(f"Le PIB par tête perd {abs(perte):.0f} % sur la même période, "
      f"de {annuel.loc[2018, 'pib_par_tete_usd2015']:.0f} à "
      f"{annuel.loc[2025, 'pib_par_tete_usd2015']:.0f} dollars constants de 2015.")
contexte.round(1)
Le PIB réel recule 7 années consécutives, de 2019 à 2025.
Le PIB par tête perd 23 % sur la même période, de 1437 à 1111 dollars constants de 2015.
Out[12]:
croissance du PIB réel (%) inflation (%) PIB par tête (USD 2015) transferts reçus (% du PIB)
annee
2018 1.70 12.50 1,437.40 16.70
2019 -1.70 18.70 1,394.40 17.90
2020 -3.30 22.80 1,331.70 22.50
2021 -1.80 16.80 1,292.70 19.20
2022 -1.70 34.00 1,256.70 18.90
2023 -1.90 36.80 1,219.10 19.20
2024 -4.20 26.90 1,154.70 16.90
2025 -2.70 28.60 1,110.80 NaN

3. Diagnostics de série temporelle¶

Trois questions avant de modéliser : le niveau des prix est-il intégré, l'inflation mensuelle est-elle stationnaire, et y a-t-il une rupture de régime à prendre en compte dans le choix de l'échantillon d'estimation.

In [13]:
def tester(serie: pd.Series, nom: str) -> dict:
    adf = adfuller(serie.dropna(), autolag="AIC")
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        stat_kpss, p_kpss, *_ = kpss(serie.dropna(), regression="c", nlags="auto")
    return {
        "série": nom,
        "observations": int(serie.dropna().shape[0]),
        "ADF (stat)": adf[0],
        "ADF (p)": adf[1],
        "KPSS (stat)": stat_kpss,
        "KPSS (p approx.)": p_kpss,
    }


log_ipc = np.log(mensuel["ipc"]).dropna()
diagnostics = pd.DataFrame(
    [
        tester(log_ipc, "log(IPC), 1990-2025"),
        tester(log_ipc.diff(), "inflation mensuelle, 1990-2025"),
        tester(log_ipc.loc["2004":].diff(), "inflation mensuelle, 2004-2025"),
        tester(log_ipc.loc["2004":].diff().diff(12), "inflation mensuelle différenciée à 12 mois"),
        tester(mensuel["inflation_ga"].loc["2004":], "inflation en glissement annuel, 2004-2025"),
    ]
).set_index("série")
exporter(diagnostics, "diagnostics_stationnarite.csv", float_format="%.4f")
diagnostics.round(3)
Out[13]:
observations ADF (stat) ADF (p) KPSS (stat) KPSS (p approx.)
série
log(IPC), 1990-2025 432 0.06 0.96 3.24 0.01
inflation mensuelle, 1990-2025 431 -2.88 0.05 0.44 0.06
inflation mensuelle, 2004-2025 263 -2.41 0.14 1.39 0.01
inflation mensuelle différenciée à 12 mois 251 -5.64 0.00 0.07 0.10
inflation en glissement annuel, 2004-2025 264 -1.35 0.60 1.25 0.01

Lecture : le diagnostic s'inverse selon la fenêtre retenue. Sur 1990-2025, ADF rejette la racine unitaire de l'inflation mensuelle (p = 0,047) et KPSS ne rejette pas tout à fait la stationnarité (p = 0,060). Sur 2004-2025, ADF ne rejette plus (p = 0,140) et c'est KPSS qui rejette (p ≤ 0,01). Deux fenêtres de la même série donnent donc deux verdicts opposés, ce qui est la signature d'une série stationnaire par morceaux, avec une moyenne qui se déplace. Le test de rupture ci-dessous localise ce déplacement.

In [14]:
serie_test = log_ipc.loc["1998":].diff().dropna() * 100
scr_contrainte = float(((serie_test - serie_test.mean()) ** 2).sum())

candidats, statistiques = [], []
marge = int(0.15 * len(serie_test))
for k in range(marge, len(serie_test) - marge):
    avant, apres = serie_test.iloc[:k], serie_test.iloc[k:]
    scr_libre = float(((avant - avant.mean()) ** 2).sum() + ((apres - apres.mean()) ** 2).sum())
    f = ((scr_contrainte - scr_libre) / 1) / (scr_libre / (len(serie_test) - 2))
    candidats.append(serie_test.index[k])
    statistiques.append(f)

rupture = pd.Series(statistiques, index=candidats)
date_rupture = rupture.idxmax()
print(f"Rupture la plus probable de la moyenne de l'inflation mensuelle : "
      f"{theme.mois_fr(date_rupture)} (statistique de Chow maximale = {rupture.max():.1f}, "
      f"échantillon 1998-2025)")
print(f"Inflation mensuelle moyenne avant : {serie_test.loc[:date_rupture].mean():.2f} % "
      f"soit {((1 + serie_test.loc[:date_rupture].mean() / 100) ** 12 - 1) * 100:.1f} % par an")
print(f"Inflation mensuelle moyenne après : {serie_test.loc[date_rupture:].mean():.2f} % "
      f"soit {((1 + serie_test.loc[date_rupture:].mean() / 100) ** 12 - 1) * 100:.1f} % par an")

DEBUT_ESTIMATION = "2004-01-01"
print(f"\nÉchantillon d'estimation retenu : {DEBUT_ESTIMATION[:7]} à {ipc.index[-1]:%Y-%m} "
      f"({len(log_ipc.loc[DEBUT_ESTIMATION:])} mois)")
print("Ce départ écarte deux zones de fragilité : le segment mal raccordé d'avant 1997 "
      "et la crise monétaire de 2002-2003, où l'inflation atteint "
      f"{mensuel.loc['2002':'2003', 'inflation_ga'].max():.0f} %. La section 6.2 chiffre "
      "ce que ce choix implique pour la projection.")
Rupture la plus probable de la moyenne de l'inflation mensuelle : juillet 2021 (statistique de Chow maximale = 83.1, échantillon 1998-2025)
Inflation mensuelle moyenne avant : 0.84 % soit 10.6 % par an
Inflation mensuelle moyenne après : 2.25 % soit 30.6 % par an

Échantillon d'estimation retenu : 2004-01 à 2025-12 (264 mois)
Ce départ écarte deux zones de fragilité : le segment mal raccordé d'avant 1997 et la crise monétaire de 2002-2003, où l'inflation atteint 36 %. La section 6.2 chiffre ce que ce choix implique pour la projection.
In [15]:
fig, axes = plt.subplots(1, 2, figsize=(12.5, 4.4))
serie_acf = log_ipc.loc[DEBUT_ESTIMATION:].diff().dropna()
plot_acf(serie_acf, lags=36, ax=axes[0], color=theme.SERIES["bleu"],
         vlines_kwargs={"colors": theme.SERIES["bleu"]})
plot_pacf(serie_acf, lags=36, ax=axes[1], method="ywm", color=theme.SERIES["bleu"],
          vlines_kwargs={"colors": theme.SERIES["bleu"]})
for ax, titre in zip(axes, ["Autocorrélations", "Autocorrélations partielles"]):
    theme.titrer(ax, titre, "Inflation mensuelle, 2004-2025, 36 retards")
    ax.set_xlabel("Retard, en mois")
    ax.set_ylim(-0.35, 1.05)
    for collection in ax.collections:
        collection.set_facecolor(theme.SERIES["bleu"])
        collection.set_alpha(0.15)
theme.source(fig, "La décroissance lente de l'autocorrélation traduit une inflation très persistante ; "
                  "la bosse autour de douze mois signale la composante saisonnière.")
theme.sauver(fig, "fig06_autocorrelations.png")
Out[15]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig06_autocorrelations.png')
No description has been provided for this image

4. Le canal du change : mesure du pass-through¶

On estime un modèle à retards échelonnés sur données mensuelles :

$$\Delta p_t = c + \sum_{i=1}^{3}\alpha_i \Delta p_{t-i} + \sum_{j=0}^{6}\beta_j \Delta e_{t-j} + \sum_{m} \delta_m D_m + \varepsilon_t$$

où $p$ est le logarithme de l'IPC, $e$ le logarithme du taux de change et $D_m$ des indicatrices mensuelles. Le pass-through de long terme vaut $\sum_j \beta_j / (1 - \sum_i \alpha_i)$ : c'est l'effet cumulé sur le niveau des prix d'une dépréciation permanente de 1 %. Les écarts-types sont corrigés de l'autocorrélation et de l'hétéroscédasticité (Newey-West).

In [16]:
RETARDS_PRIX = 3
RETARDS_CHANGE = 6

cadre = pd.DataFrame({
    "dp": np.log(mensuel["ipc"]).diff() * 100,
    "de": np.log(mensuel["taux_change"]).diff() * 100,
}).loc[DEBUT_ESTIMATION:]

for i in range(1, RETARDS_PRIX + 1):
    cadre[f"dp_{i}"] = cadre["dp"].shift(i)
for j in range(0, RETARDS_CHANGE + 1):
    cadre[f"de_{j}"] = cadre["de"].shift(j)
indicatrices = pd.get_dummies(cadre.index.month, prefix="m", drop_first=True).astype(float)
indicatrices.index = cadre.index
cadre = pd.concat([cadre, indicatrices], axis=1)

colonnes_x = ([f"dp_{i}" for i in range(1, RETARDS_PRIX + 1)]
              + [f"de_{j}" for j in range(0, RETARDS_CHANGE + 1)]
              + list(indicatrices.columns))
estimation = cadre.dropna(subset=["dp"] + colonnes_x)
X = sm.add_constant(estimation[colonnes_x])
y = estimation["dp"]
ardl = sm.OLS(y, X).fit(cov_type="HAC", cov_kwds={"maxlags": 12})

somme_alpha = float(sum(ardl.params[f"dp_{i}"] for i in range(1, RETARDS_PRIX + 1)))
somme_beta = float(sum(ardl.params[f"de_{j}"] for j in range(0, RETARDS_CHANGE + 1)))
pt_long_terme = somme_beta / (1 - somme_alpha)

print(f"Persistance de l'inflation, somme des alpha : {somme_alpha:.3f}")
print(f"Effet direct cumulé du change, somme des bêta : {somme_beta:.3f}")
print(f"Pass-through de long terme : {pt_long_terme:.3f} "
      f"(une dépréciation permanente de 10 % élève le niveau des prix de {pt_long_terme * 10:.1f} %)")
print(f"R2 ajusté : {ardl.rsquared_adj:.3f}  |  observations : {int(ardl.nobs)}")

residu_recent = ardl.resid.loc["2024":]
print(f"\nRésidu moyen sur 2024-2025 : {residu_recent.mean():+.2f} point de % par mois, "
      f"soit {residu_recent.mean() * 12:+.1f} point de % par an d'inflation que le modèle "
      f"n'explique ni par l'inertie ni par le change.")
Persistance de l'inflation, somme des alpha : 0.680
Effet direct cumulé du change, somme des bêta : 0.065
Pass-through de long terme : 0.204 (une dépréciation permanente de 10 % élève le niveau des prix de 2.0 %)
R2 ajusté : 0.439  |  observations : 258

Résidu moyen sur 2024-2025 : +0.28 point de % par mois, soit +3.4 point de % par an d'inflation que le modèle n'explique ni par l'inertie ni par le change.

Le pass-through estimé est faible : environ 0,20. Une dépréciation permanente de 10 % de la gourde n'élève le niveau des prix que de 2 %. Deux lectures se combinent. D'abord la persistance interne est forte (somme des alpha proche de 0,7), donc l'inflation se nourrit surtout d'elle-même. Ensuite la période récente pèse dans l'estimation : depuis 2024, le change est plat et l'inflation reste à 25 %, ce que le modèle ne peut attribuer au change.

In [17]:
def reponse_cumulee(parametres: pd.Series, horizon: int = 24) -> np.ndarray:
    """Effet cumulé sur le niveau des prix d'une dépréciation permanente de 1 %."""
    alpha = np.array([parametres[f"dp_{i}"] for i in range(1, RETARDS_PRIX + 1)])
    beta = np.array([parametres[f"de_{j}"] for j in range(0, RETARDS_CHANGE + 1)])
    choc = np.zeros(horizon + 1)
    choc[0] = 1.0  # dépréciation de 1 % le mois 0, taux ensuite stable
    dp = np.zeros(horizon + 1)
    for t in range(horizon + 1):
        valeur = sum(beta[j] * choc[t - j] for j in range(len(beta)) if t - j >= 0)
        valeur += sum(alpha[i - 1] * dp[t - i] for i in range(1, len(alpha) + 1) if t - i >= 0)
        dp[t] = valeur
    return np.cumsum(dp)


HORIZON_IRF = 24
irf_centrale = reponse_cumulee(ardl.params, HORIZON_IRF)

# Le modèle contient des retards de la variable expliquée : un bootstrap des
# résidus à régresseurs figés y est invalide, puisqu'il laisserait les colonnes
# dp_1 à dp_3 incohérentes avec la série rééchantillonnée. Les bandes sont donc
# tirées de la loi asymptotique des coefficients, avec la covariance Newey-West
# déjà estimée.
N_TIRAGES = 4000
tirages_params = ALEA.multivariate_normal(ardl.params.values, ardl.cov_params().values,
                                          size=N_TIRAGES)
tirages_params = pd.DataFrame(tirages_params, columns=ardl.params.index)

stable = tirages_params[[f"dp_{i}" for i in range(1, RETARDS_PRIX + 1)]].sum(axis=1) < 1
tirages = np.array([reponse_cumulee(ligne, HORIZON_IRF)
                    for _, ligne in tirages_params[stable].iterrows()])
print(f"{(~stable).sum()} tirage(s) sur {N_TIRAGES} écarté(s) pour non-stationnarité "
      f"(somme des alpha supérieure ou égale à 1, donc sans pass-through de long terme).")

bande_basse = np.percentile(tirages, 10, axis=0)
bande_haute = np.percentile(tirages, 90, axis=0)

exporter(pd.DataFrame({
    "horizon_mois": np.arange(HORIZON_IRF + 1),
    "pass_through_cumule": irf_centrale,
    "borne_10": bande_basse,
    "borne_90": bande_haute,
}), "passthrough_cumule.csv", index=False, float_format="%.4f")

fig, ax = plt.subplots(figsize=(10, 5.2))
horizons = np.arange(HORIZON_IRF + 1)
ax.fill_between(horizons, bande_basse, bande_haute, color=theme.SERIES["bleu"], alpha=0.16, linewidth=0)
ax.plot(horizons, irf_centrale, color=theme.SERIES["bleu"], label="Effet cumulé estimé")
ax.axhline(pt_long_terme, color=theme.SERIES["orange"], linewidth=1.4,
           label=f"Long terme : {pt_long_terme:.2f}")
ax.axhline(0, color=theme.ENCRE["axe"], linewidth=0.8)
theme.titrer(ax, "Une dépréciation de 1 % élève les prix de 0,2 % en un an",
             "Effet cumulé sur le niveau des prix, en %, intervalle à 80 %")
ax.set_xlabel("Mois après la dépréciation")
ax.set_ylabel("Hausse cumulée du niveau des prix, %")
ax.set_xticks(range(0, HORIZON_IRF + 1, 3))
ax.legend(loc="lower right")
theme.source(fig, "Modèle à retards échelonnés estimé sur 2004-2025, écarts-types Newey-West, "
                  "bandes simulées à partir de la loi asymptotique des coefficients.")
theme.sauver(fig, "fig07_passthrough.png")
0 tirage(s) sur 4000 écarté(s) pour non-stationnarité (somme des alpha supérieure ou égale à 1, donc sans pass-through de long terme).
Out[17]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig07_passthrough.png')
No description has been provided for this image

5. Modèles concurrents et validation hors échantillon¶

Quatre familles sont mises en concurrence :

Modèle Idée
Marche aléatoire avec dérive l'inflation future égale l'inflation moyenne récente
Naïf saisonnier l'inflation de chaque mois reproduit celle du même mois l'an dernier
Lissage exponentiel à tendance amortie la dynamique récente s'atténue progressivement
SARIMA structure autorégressive et saisonnière estimée

Les deux premiers prolongent la dynamique récente : ce sont des modèles de persistance. Le SARIMA, lui, ramène l'inflation vers la moyenne de son échantillon d'estimation : c'est un modèle de retour à la moyenne. Cette opposition est le vrai enjeu de la projection à cinq ans.

La spécification SARIMA est choisie par balayage sur le critère AIC, puis tous les modèles sont évalués par origines glissantes : on se replace à plusieurs dates passées, on prévoit sans utiliser l'information postérieure, et on compare l'erreur sur l'inflation en glissement annuel.

In [18]:
serie_modele = np.log(mensuel["ipc"]).loc[DEBUT_ESTIMATION:].dropna()

resultats_grille = []
for p in range(0, 3):
    for q in range(0, 3):
        for P in range(0, 2):
            for Q in range(0, 2):
                try:
                    ajustement = SARIMAX(
                        serie_modele, order=(p, 1, q), seasonal_order=(P, 0, Q, 12),
                        trend="c", enforce_stationarity=True, enforce_invertibility=True,
                    ).fit(disp=False, maxiter=200)
                    resultats_grille.append({
                        "ordre": f"({p},1,{q})({P},0,{Q})[12]",
                        "p": p, "q": q, "P": P, "Q": Q,
                        "AIC": ajustement.aic, "BIC": ajustement.bic,
                    })
                except Exception:
                    continue

grille_sarima = pd.DataFrame(resultats_grille).sort_values("AIC").reset_index(drop=True)
exporter(grille_sarima, "selection_sarima.csv", index=False, float_format="%.2f")
meilleur = grille_sarima.iloc[0]
ORDRE_SARIMA = (int(meilleur["p"]), 1, int(meilleur["q"]))
ORDRE_SAISONNIER = (int(meilleur["P"]), 0, int(meilleur["Q"]), 12)
LIBELLE_SARIMA = meilleur["ordre"]
print(f"Spécification retenue : SARIMA {LIBELLE_SARIMA}, AIC = {meilleur['AIC']:.1f}")
grille_sarima.head(8)[["ordre", "AIC", "BIC"]]
Spécification retenue : SARIMA (1,1,0)(0,0,1)[12], AIC = -1731.0
Out[18]:
ordre AIC BIC
0 (1,1,0)(0,0,1)[12] -1,731.01 -1,716.72
1 (1,1,2)(0,0,1)[12] -1,729.72 -1,708.29
2 (1,1,0)(1,0,0)[12] -1,729.50 -1,715.21
3 (2,1,1)(0,0,1)[12] -1,729.37 -1,707.94
4 (2,1,0)(0,0,1)[12] -1,729.01 -1,711.15
5 (1,1,1)(0,0,1)[12] -1,728.97 -1,711.11
6 (1,1,2)(1,0,0)[12] -1,728.26 -1,706.83
7 (2,1,1)(1,0,0)[12] -1,727.79 -1,706.36
In [19]:
def prevoir_sarima(historique: pd.Series, horizon: int) -> pd.Series:
    modele = SARIMAX(historique, order=ORDRE_SARIMA, seasonal_order=ORDRE_SAISONNIER,
                     trend="c", enforce_stationarity=True, enforce_invertibility=True)
    return modele.fit(disp=False, maxiter=200).get_forecast(steps=horizon).predicted_mean


def prevoir_ets(historique: pd.Series, horizon: int) -> pd.Series:
    modele = ExponentialSmoothing(historique, trend="add", damped_trend=True, seasonal=None)
    return modele.fit().forecast(horizon)


def prevoir_derive(historique: pd.Series, horizon: int) -> pd.Series:
    derive = historique.diff().dropna().iloc[-60:].mean()
    index = pd.date_range(historique.index[-1] + pd.offsets.MonthBegin(), periods=horizon, freq="MS")
    return pd.Series(historique.iloc[-1] + derive * np.arange(1, horizon + 1), index=index)


def prevoir_naif_saisonnier(historique: pd.Series, horizon: int) -> pd.Series:
    variations = historique.diff()
    index = pd.date_range(historique.index[-1] + pd.offsets.MonthBegin(), periods=horizon, freq="MS")
    valeurs, courant = [], historique.iloc[-1]
    for date in index:
        meme_mois = variations[(variations.index.month == date.month)
                               & (variations.index <= historique.index[-1])]
        courant = courant + (meme_mois.iloc[-1] if len(meme_mois) else variations.mean())
        valeurs.append(courant)
    return pd.Series(valeurs, index=index)


MODELES = {
    "SARIMA": prevoir_sarima,
    "Lissage exponentiel": prevoir_ets,
    "Marche aléatoire avec dérive": prevoir_derive,
    "Naïf saisonnier": prevoir_naif_saisonnier,
}

HORIZONS = [6, 12, 24]
origines = pd.date_range("2015-12-01", "2023-12-01", freq="6MS")
lignes = []
for origine in origines:
    historique = serie_modele.loc[:origine]
    if len(historique) < 60:
        continue
    for nom, fonction in MODELES.items():
        try:
            prevision = fonction(historique, max(HORIZONS))
        except Exception:
            continue
        for h in HORIZONS:
            cible = origine + pd.offsets.MonthBegin(h)
            if cible not in serie_modele.index or (cible - pd.offsets.MonthBegin(12)) not in serie_modele.index:
                continue
            reel = float(np.exp(serie_modele.loc[cible] - serie_modele.loc[cible - pd.offsets.MonthBegin(12)]) - 1) * 100
            serie_complete = pd.concat([historique, prevision])
            predit = float(np.exp(serie_complete.loc[cible] - serie_complete.loc[cible - pd.offsets.MonthBegin(12)]) - 1) * 100
            lignes.append({"modèle": nom, "origine": origine, "horizon": h,
                           "erreur": predit - reel, "réel": reel, "prédit": predit})

erreurs = pd.DataFrame(lignes)
backtest = (erreurs.groupby(["modèle", "horizon"])["erreur"]
            .agg(RMSE=lambda s: float(np.sqrt((s ** 2).mean())),
                 MAE=lambda s: float(s.abs().mean()),
                 biais="mean")
            .reset_index())
exporter(backtest, "backtest_rmse.csv", index=False, float_format="%.3f")
print(f"{len(origines)} origines, de {origines[0]:%Y-%m} à {origines[-1]:%Y-%m}")
tableau_backtest = backtest.pivot(index="modèle", columns="horizon", values="RMSE").round(2)
tableau_backtest.columns = [f"{h} mois" for h in tableau_backtest.columns]
tableau_backtest.sort_values("12 mois")
17 origines, de 2015-12 à 2023-12
Out[19]:
6 mois 12 mois 24 mois
modèle
Naïf saisonnier 8.62 11.83 13.12
Marche aléatoire avec dérive 7.91 11.84 13.47
SARIMA 8.80 14.78 17.09
Lissage exponentiel 9.36 15.64 22.23
In [20]:
fig, ax = plt.subplots(figsize=(10.5, 5.2))
noms = list(tableau_backtest.sort_values("12 mois").index)
largeur = 0.26
positions = np.arange(len(noms))
couleurs_h = [theme.SERIES["bleu"], theme.SERIES["orange"], theme.SERIES["aqua"]]
for k, h in enumerate(HORIZONS):
    valeurs = [float(backtest[(backtest["modèle"] == n) & (backtest["horizon"] == h)]["RMSE"].iloc[0])
               for n in noms]
    barres = ax.bar(positions + (k - 1) * (largeur + 0.02), valeurs, width=largeur,
                    color=couleurs_h[k], label=f"{h} mois")
    for barre_k, valeur in zip(barres, valeurs):
        ax.annotate(f"{valeur:.1f}", xy=(barre_k.get_x() + barre_k.get_width() / 2, valeur),
                    xytext=(0, 3), textcoords="offset points", ha="center", fontsize=8.5,
                    color=theme.ENCRE["secondaire"])
ax.set_xticks(positions, [n.replace(" avec ", "\navec ").replace("Lissage ", "Lissage\n") for n in noms])
ax.set_ylim(0, max(backtest["RMSE"]) * 1.25)
theme.titrer(ax, "Les modèles de persistance ont mieux prévu que le SARIMA",
             "RMSE sur l'inflation en glissement annuel, en points de pourcentage, plus bas est meilleur")
ax.set_ylabel("RMSE, points de %")
ax.legend(title="Horizon", loc="upper left", title_fontsize=9)
theme.source(fig, "17 origines glissantes tous les six mois entre décembre 2015 et décembre 2023. "
                  "Valeurs dans outputs/backtest_rmse.csv.")
theme.sauver(fig, "fig08_backtest.png")
Out[20]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig08_backtest.png')
No description has been provided for this image

Ce résultat compte pour la suite. Sur 2015-2023, les modèles de persistance battent le SARIMA à tous les horizons. La raison est identifiable : la période testée est une phase d'accélération continue, où tout modèle qui ramène l'inflation vers sa moyenne historique sous-estime systématiquement. Le classement du tableau ne dit donc pas que la persistance est le bon mécanisme, il dit qu'elle a été la bonne hypothèse pendant une décennie d'accélération. Les deux vues sont conservées dans la projection.

6. Projection 2026-2030¶

6.1 Projection statistique et intervalles¶

Le SARIMA retenu est réestimé sur l'ensemble de l'échantillon, puis simulé sur soixante mois. Les intervalles proviennent de 3 000 trajectoires simulées : chacune tire une séquence de chocs et suit la dynamique estimée. Cette méthode donne des intervalles corrects pour le glissement annuel et pour la moyenne annuelle, ce que le seul intervalle sur le niveau ne permettrait pas. La variante de persistance, gagnante du test hors échantillon, est superposée : elle prolonge la dérive moyenne des soixante derniers mois, exactement la règle qui a été testée en section 5.

In [21]:
HORIZON = 60
modele_final = SARIMAX(serie_modele, order=ORDRE_SARIMA, seasonal_order=ORDRE_SAISONNIER,
                       trend="c", enforce_stationarity=True, enforce_invertibility=True)
resultat_final = modele_final.fit(disp=False, maxiter=300)
print(resultat_final.summary().tables[1])

coefficients_ar = [resultat_final.params[nom] for nom in resultat_final.params.index if nom.startswith("ar.L")]
ancrage_mensuel = float(resultat_final.params["intercept"]) / (1 - sum(coefficients_ar)) * 100
print(f"\nAncrage de long terme du modèle : {ancrage_mensuel:.2f} % par mois, "
      f"soit {((1 + ancrage_mensuel / 100) ** 12 - 1) * 100:.1f} % par an.")
print("C'est vers cette valeur que la projection centrale converge.")
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
intercept      0.0042      0.001      4.250      0.000       0.002       0.006
ar.L1          0.6181      0.025     24.837      0.000       0.569       0.667
ma.S.L12       0.2163      0.049      4.393      0.000       0.120       0.313
sigma2      7.829e-05   3.64e-06     21.528      0.000    7.12e-05    8.54e-05
==============================================================================

Ancrage de long terme du modèle : 1.09 % par mois, soit 13.9 % par an.
C'est vers cette valeur que la projection centrale converge.
In [22]:
N_SIM = 3000
np.random.seed(20260815)  # simulate() tire du générateur global : graine fixée pour la reproductibilité
simulations = resultat_final.simulate(nsimulations=HORIZON, anchor="end", repetitions=N_SIM)
simulations = np.asarray(simulations).reshape(HORIZON, N_SIM)
index_futur = pd.date_range(serie_modele.index[-1] + pd.offsets.MonthBegin(), periods=HORIZON, freq="MS")

historique_complet = np.repeat(serie_modele.values[:, None], N_SIM, axis=1)
trajectoires = pd.DataFrame(np.vstack([historique_complet, simulations]),
                            index=list(serie_modele.index) + list(index_futur))
niveaux = np.exp(trajectoires)
glissement_futur = ((niveaux / niveaux.shift(12) - 1) * 100).loc[index_futur]

derive_recente = serie_modele.diff().dropna().iloc[-60:].mean()
print(f"Variante de persistance : dérive moyenne des 60 derniers mois = "
      f"{derive_recente * 100:.2f} % par mois, soit {(np.exp(derive_recente * 12) - 1) * 100:.1f} % par an. "
      f"C'est la moyenne 2021-2025, au-dessus des {derniere.iloc[-1]:.0f} % de décembre 2025 "
      f"parce que le pic de 2022-2023 y pèse encore.")
chemin_persistance = pd.Series(
    np.exp(serie_modele.iloc[-1] + derive_recente * np.arange(1, HORIZON + 1)), index=index_futur)
serie_persistance = pd.concat([ipc, chemin_persistance])
ga_persistance = (serie_persistance / serie_persistance.shift(12) - 1).loc[index_futur] * 100

projection = pd.DataFrame({
    "inflation_ga_centrale": glissement_futur.median(axis=1),
    "borne_10": glissement_futur.quantile(0.10, axis=1),
    "borne_90": glissement_futur.quantile(0.90, axis=1),
    "borne_025": glissement_futur.quantile(0.025, axis=1),
    "borne_975": glissement_futur.quantile(0.975, axis=1),
    "ipc_central": niveaux.loc[index_futur].median(axis=1),
    "variante_persistance": ga_persistance,
})
projection.index.name = "date"
exporter(projection, "projection_mensuelle.csv", float_format="%.3f")
projection.loc[["2026-12-01", "2027-12-01", "2028-12-01", "2029-12-01", "2030-12-01"]].round(1)
Variante de persistance : dérive moyenne des 60 derniers mois = 2.13 % par mois, soit 29.2 % par an. C'est la moyenne 2021-2025, au-dessus des 25 % de décembre 2025 parce que le pic de 2022-2023 y pèse encore.
Out[22]:
inflation_ga_centrale borne_10 borne_90 borne_025 borne_975 ipc_central variante_persistance
date
2026-12-01 14.50 4.10 25.70 -0.50 31.50 1,141.00 29.20
2027-12-01 13.80 2.90 25.10 -2.10 32.30 1,295.40 29.20
2028-12-01 14.10 3.30 25.60 -1.90 32.20 1,476.30 29.20
2029-12-01 14.00 3.20 25.90 -2.20 33.10 1,679.30 29.20
2030-12-01 13.40 2.70 25.50 -2.50 31.90 1,903.80 29.20
In [23]:
observe = mensuel["inflation_ga"].loc["2012":].dropna()

fig, ax = plt.subplots(figsize=(11, 5.8))
ax.fill_between(projection.index, projection["borne_025"], projection["borne_975"],
                color=theme.SERIES["bleu"], alpha=0.10, linewidth=0, label="Intervalle à 95 %")
ax.fill_between(projection.index, projection["borne_10"], projection["borne_90"],
                color=theme.SERIES["bleu"], alpha=0.22, linewidth=0, label="Intervalle à 80 %")
ax.plot(observe.index, observe.values, color=theme.ENCRE["primaire"], linewidth=1.6, label="Observé")
ax.plot(projection.index, projection["inflation_ga_centrale"], color=theme.SERIES["bleu"],
        linewidth=2.0, label="Projection centrale (retour à la moyenne)")
ax.plot(projection.index, projection["variante_persistance"], color=theme.SERIES["orange"],
        linewidth=1.8, label="Variante de persistance")
ax.axvline(serie_modele.index[-1], color=theme.ENCRE["axe"], linewidth=0.8)
ax.axhline(0, color=theme.ENCRE["axe"], linewidth=0.8)
theme.etiqueter_fin(ax, projection.index[-1], projection["inflation_ga_centrale"].iloc[-1],
                    f"{projection['inflation_ga_centrale'].iloc[-1]:.0f} %", theme.SERIES["bleu"])
theme.etiqueter_fin(ax, projection.index[-1], projection["variante_persistance"].iloc[-1],
                    f"{projection['variante_persistance'].iloc[-1]:.0f} %", theme.SERIES["orange"])
ax.annotate("Projection", xy=(index_futur[1], 46), fontsize=9, color=theme.ENCRE["secondaire"])
theme.titrer(ax, "Projection de l'inflation, janvier 2026 à décembre 2030",
             f"Glissement annuel, en %. SARIMA {LIBELLE_SARIMA}, {N_SIM} trajectoires simulées")
ax.set_ylabel("%")
ax.set_xlabel("")
ax.legend(loc="upper left", ncols=2, fontsize=9)
ax.set_xlim(pd.Timestamp("2012-01-01"), pd.Timestamp("2031-09-01"))
ax.set_ylim(min(0, projection["borne_025"].min() - 2), 55)
theme.source(fig, "Lecture : la bande claire contient 95 % des trajectoires simulées. "
                  "Valeurs dans outputs/projection_mensuelle.csv.")
theme.sauver(fig, "fig09_projection.png")
Out[23]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig09_projection.png')
No description has been provided for this image
In [24]:
annees_projection = list(range(2026, 2031))
niveaux_annuels = niveaux.groupby(niveaux.index.year).mean()
inflation_moyenne = (niveaux_annuels / niveaux_annuels.shift(1) - 1) * 100
decembre = niveaux.loc[niveaux.index.month == 12]
decembre.index = decembre.index.year
inflation_decembre = (decembre / decembre.shift(1) - 1) * 100

persistance_complete = serie_persistance.groupby(serie_persistance.index.year).mean()
persistance_annuelle = (persistance_complete / persistance_complete.shift(1) - 1) * 100

tableau_annuel = pd.DataFrame({
    "inflation moyenne (%)": inflation_moyenne.loc[annees_projection].median(axis=1),
    "borne 10 %": inflation_moyenne.loc[annees_projection].quantile(0.10, axis=1),
    "borne 90 %": inflation_moyenne.loc[annees_projection].quantile(0.90, axis=1),
    "fin d'année (%)": inflation_decembre.loc[annees_projection].median(axis=1),
    "variante persistance (%)": persistance_annuelle.loc[annees_projection],
    "IPC moyen (2010 = 100)": niveaux_annuels.loc[annees_projection].median(axis=1),
})
tableau_annuel.index.name = "année"
exporter(tableau_annuel, "projection_annuelle.csv", float_format="%.2f")
tableau_annuel.round(1)
Out[24]:
inflation moyenne (%) borne 10 % borne 90 % fin d'année (%) variante persistance (%) IPC moyen (2010 = 100)
année
2026 17.40 11.40 23.70 14.50 25.60 1,073.30
2027 13.80 4.30 24.40 13.80 29.20 1,221.50
2028 13.80 4.50 24.00 14.10 29.20 1,389.80
2029 14.00 4.60 24.60 14.00 29.20 1,583.50
2030 13.70 3.80 24.40 13.40 29.20 1,800.40

6.2 Ce que l'échantillon d'estimation change¶

L'ancrage de long terme du SARIMA est la moyenne de l'inflation sur la période d'estimation. Le choix de cette période détermine donc largement le niveau vers lequel la projection converge. Le tableau ci-dessous rend cette dépendance explicite plutôt que de la laisser dans les coulisses.

In [25]:
lignes_robustesse = []
for debut in ["1998-01-01", "2004-01-01", "2010-01-01", "2015-01-01", "2019-01-01"]:
    echantillon = np.log(mensuel["ipc"]).loc[debut:].dropna()
    ajustement = SARIMAX(echantillon, order=ORDRE_SARIMA, seasonal_order=ORDRE_SAISONNIER,
                         trend="c", enforce_stationarity=True, enforce_invertibility=True
                         ).fit(disp=False, maxiter=300)
    ar = [ajustement.params[nom] for nom in ajustement.params.index if nom.startswith("ar.L")]
    ancrage = float(ajustement.params["intercept"]) / (1 - sum(ar)) * 100
    prevision = ajustement.get_forecast(steps=HORIZON).predicted_mean
    serie_prevue = pd.concat([echantillon, prevision])
    ga_2030 = float(np.exp(serie_prevue.loc["2030-12-01"] - serie_prevue.loc["2029-12-01"]) - 1) * 100
    lignes_robustesse.append({
        "échantillon": f"{debut[:4]}-2025",
        "observations": len(echantillon),
        "inflation moyenne de l'échantillon (%/an)":
            (np.exp(echantillon.diff().dropna().mean() * 12) - 1) * 100,
        "ancrage du modèle (%/an)": ((1 + ancrage / 100) ** 12 - 1) * 100,
        "glissement annuel projeté fin 2030 (%)": ga_2030,
    })

robustesse = pd.DataFrame(lignes_robustesse).set_index("échantillon")
exporter(robustesse, "robustesse_echantillon.csv", float_format="%.2f")
robustesse.round(1)
Out[25]:
observations inflation moyenne de l'échantillon (%/an) ancrage du modèle (%/an) glissement annuel projeté fin 2030 (%)
échantillon
1998-2025 336 13.60 13.40 13.50
2004-2025 264 14.00 13.90 14.00
2010-2025 192 15.70 15.60 15.70
2015-2025 132 21.00 20.20 20.40
2019-2025 84 26.50 25.60 25.90

6.3 Trois scénarios de change¶

La projection précédente est univariée : elle prolonge la dynamique des prix sans hypothèse explicite sur la gourde. Le modèle de pass-through de la section 4 permet de conditionner la trajectoire des prix à une hypothèse de change. Trois trajectoires sont retenues :

  • Stabilité : la gourde reste à son niveau de mars 2026, prolongement du régime observé depuis 2024.
  • Dépréciation modérée : 8 % par an, ordre de grandeur de la moyenne de longue période.
  • Dépréciation forte : 20 % par an, rythme des années 2021 à 2023.

Ces simulations héritent du retour à la moyenne du modèle à retards échelonnés : elles montrent l'effet marginal du change, pas le niveau absolu le plus probable.

In [26]:
print(f"Dépréciation annuelle moyenne 2004-2026 : "
      f"{(np.exp(np.log(change).diff().loc['2004':].mean() * 12) - 1) * 100:.1f} %")
print(f"Dépréciation annuelle moyenne 2021-2023 : "
      f"{(np.exp(np.log(change).diff().loc['2021':'2023'].mean() * 12) - 1) * 100:.1f} %")
print(f"Dépréciation sur les douze derniers mois : "
      f"{(change.iloc[-1] / change.iloc[-13] - 1) * 100:+.1f} %")

SCENARIOS = {
    "Stabilité de la gourde": 0.0,
    "Dépréciation modérée (8 %/an)": 8.0,
    "Dépréciation forte (20 %/an)": 20.0,
}

dernier_dp = cadre["dp"].dropna()
dernier_de = cadre["de"].dropna()
alpha_hat = np.array([ardl.params[f"dp_{i}"] for i in range(1, RETARDS_PRIX + 1)])
beta_hat = np.array([ardl.params[f"de_{j}"] for j in range(0, RETARDS_CHANGE + 1)])
constante = float(ardl.params["const"])
effets_mois = {m: float(ardl.params.get(f"m_{m}", 0.0)) for m in range(1, 13)}
residus_ardl = ardl.resid.values


def simuler_scenario(taux_annuel: float, n_tirages: int = 1500) -> pd.DataFrame:
    """Simule l'inflation mensuelle sous une hypothèse de dépréciation annuelle."""
    de_mensuel = np.log1p(taux_annuel / 100) / 12 * 100
    de_futur = np.full(HORIZON, de_mensuel)
    observes = dernier_de.loc[index_futur[0]:]
    for k, date in enumerate(index_futur):
        if date in observes.index:
            de_futur[k] = float(observes.loc[date])

    # Le change est publié trois mois plus loin que l'IPC : les retards initiaux
    # doivent s'arrêter au dernier mois observé des prix, sinon la simulation
    # démarre avec des valeurs postérieures à sa propre origine.
    historique_dp = list(dernier_dp.iloc[-RETARDS_PRIX:].values)
    historique_de = list(dernier_de.loc[:dernier_dp.index[-1]].iloc[-RETARDS_CHANGE:].values)
    resultats = np.empty((HORIZON, n_tirages))
    for b in range(n_tirages):
        dp_courant = list(historique_dp)
        de_courant = list(historique_de)
        chocs = ALEA.choice(residus_ardl, size=HORIZON, replace=True)
        for t, date in enumerate(index_futur):
            de_courant.append(de_futur[t])
            valeur = constante + effets_mois.get(date.month, 0.0)
            for i in range(1, RETARDS_PRIX + 1):
                valeur += alpha_hat[i - 1] * dp_courant[-i]
            for j in range(0, RETARDS_CHANGE + 1):
                valeur += beta_hat[j] * de_courant[-1 - j]
            valeur += chocs[t]
            dp_courant.append(valeur)
            resultats[t, b] = valeur
    return pd.DataFrame(resultats, index=index_futur)


def repeter_observe(serie: pd.Series, n_colonnes: int) -> pd.DataFrame:
    """Duplique une série observée sur n colonnes pour la coller aux simulations."""
    return pd.DataFrame(np.repeat(serie.values[:, None], n_colonnes, axis=1), index=serie.index)


scenarios_niveaux = {}
for nom, taux in SCENARIOS.items():
    dp_sim = simuler_scenario(taux)
    scenarios_niveaux[nom] = np.exp(np.log(ipc.iloc[-1]) + dp_sim.cumsum() / 100)
    print(f"{nom} : simulation terminée")
Dépréciation annuelle moyenne 2004-2026 : 5.4 %
Dépréciation annuelle moyenne 2021-2023 : 23.2 %
Dépréciation sur les douze derniers mois : -0.0 %
Stabilité de la gourde : simulation terminée
Dépréciation modérée (8 %/an) : simulation terminée
Dépréciation forte (20 %/an) : simulation terminée
In [27]:
lignes_scenarios = []
for nom, niveaux_sc in scenarios_niveaux.items():
    complet = pd.concat([repeter_observe(ipc.loc["2025":], niveaux_sc.shape[1]), niveaux_sc])
    moyennes = complet.groupby(complet.index.year).mean()
    croissance = (moyennes / moyennes.shift(1) - 1) * 100
    for annee in annees_projection:
        lignes_scenarios.append({
            "scénario": nom,
            "année": annee,
            "inflation moyenne (%)": float(croissance.loc[annee].median()),
            "borne 10 %": float(croissance.loc[annee].quantile(0.10)),
            "borne 90 %": float(croissance.loc[annee].quantile(0.90)),
        })

tableau_scenarios = pd.DataFrame(lignes_scenarios)
exporter(tableau_scenarios, "scenarios_annuels.csv", index=False, float_format="%.2f")
tableau_scenarios.pivot(index="année", columns="scénario", values="inflation moyenne (%)").round(1)
Out[27]:
scénario Dépréciation forte (20 %/an) Dépréciation modérée (8 %/an) Stabilité de la gourde
année
2026 16.30 16.10 15.70
2027 15.40 13.70 11.80
2028 15.90 14.10 12.10
2029 16.10 14.10 11.80
2030 16.50 13.30 11.70
In [28]:
fig, axes = plt.subplots(1, 2, figsize=(13, 5.4), gridspec_kw={"width_ratios": [1.25, 1]})
couleurs_scenarios = {
    "Stabilité de la gourde": theme.SERIES["aqua"],
    "Dépréciation modérée (8 %/an)": theme.SERIES["bleu"],
    "Dépréciation forte (20 %/an)": theme.SERIES["orange"],
}

ax = axes[0]
histo = mensuel["inflation_ga"].loc["2020":].dropna()
ax.plot(histo.index, histo.values, color=theme.ENCRE["primaire"], linewidth=1.6, label="Observé")
for nom, niveaux_sc in scenarios_niveaux.items():
    depart = pd.concat([repeter_observe(ipc.loc["2025":], niveaux_sc.shape[1]), niveaux_sc])
    serie_ga = (depart / depart.shift(12) - 1) * 100
    mediane = serie_ga.loc[index_futur].median(axis=1)
    ax.plot(mediane.index, mediane.values, color=couleurs_scenarios[nom], label=nom)
    theme.etiqueter_fin(ax, mediane.index[-1], mediane.iloc[-1], f"{mediane.iloc[-1]:.0f} %",
                        couleurs_scenarios[nom])
ax.axvline(ipc.index[-1], color=theme.ENCRE["axe"], linewidth=0.8)
theme.titrer(ax, "Trois trajectoires selon la gourde", "Inflation en glissement annuel, en %")
ax.set_ylabel("%")
ax.set_xlim(pd.Timestamp("2020-01-01"), pd.Timestamp("2031-09-01"))
ax.set_ylim(0, 62)
ax.legend(loc="upper right", fontsize=8.5)

ax = axes[1]
positions = np.arange(len(annees_projection))
largeur = 0.26
for k, (nom, couleur) in enumerate(couleurs_scenarios.items()):
    valeurs = [float(tableau_scenarios[(tableau_scenarios["scénario"] == nom)
                                       & (tableau_scenarios["année"] == a)]["inflation moyenne (%)"].iloc[0])
               for a in annees_projection]
    ax.bar(positions + (k - 1) * (largeur + 0.02), valeurs, width=largeur, color=couleur, label=nom)
ax.set_xticks(positions, [str(a) for a in annees_projection])
ax.set_ylim(0, 25)
theme.titrer(ax, "Inflation moyenne annuelle", "En %, par scénario")
ax.set_ylabel("%")
ax.legend(loc="upper left", fontsize=8.5)
theme.source(fig, "Simulations du modèle à retards échelonnés, 1 500 tirages par scénario. "
                  "Valeurs dans outputs/scenarios_annuels.csv.")
theme.sauver(fig, "fig10_scenarios.png")
Out[28]:
WindowsPath('C:/Users/woodn/OneDrive/Desktop/Research/inflation_haiti/figures/fig10_scenarios.png')
No description has been provided for this image

6.4 Ce que la projection implique pour le pouvoir d'achat¶

In [29]:
central = tableau_annuel["inflation moyenne (%)"]
cumul_central = float(np.prod(1 + central / 100))
cumul_persistance = float(np.prod(1 + tableau_annuel["variante persistance (%)"] / 100))

print(f"Scénario central, retour à la moyenne : inflation cumulée 2026-2030 de "
      f"{(cumul_central - 1) * 100:.0f} %")
print(f"  un panier à 1 000 gourdes fin 2025 en coûtera "
      f"{1000 * cumul_central:,.0f} fin 2030".replace(",", " "))
print(f"  une gourde de 2025 vaudra {100 / cumul_central:.0f} centimes de 2030")
print(f"\nVariante de persistance : inflation cumulée de {(cumul_persistance - 1) * 100:.0f} %")
print(f"  le même panier en coûterait {1000 * cumul_persistance:,.0f}".replace(",", " "))
print(f"  une gourde de 2025 vaudrait {100 / cumul_persistance:.0f} centimes de 2030")
print("\nPar scénario de change, retour à la moyenne du modèle à retards échelonnés :")
for nom in SCENARIOS:
    sous_ensemble = tableau_scenarios[tableau_scenarios["scénario"] == nom]["inflation moyenne (%)"]
    cumul_sc = float(np.prod(1 + sous_ensemble.values / 100))
    print(f"  {nom} : {(cumul_sc - 1) * 100:.0f} % sur cinq ans")
Scénario central, retour à la moyenne : inflation cumulée 2026-2030 de 97 %
  un panier à 1 000 gourdes fin 2025 en coûtera 1 972 fin 2030
  une gourde de 2025 vaudra 51 centimes de 2030

Variante de persistance : inflation cumulée de 250 %
  le même panier en coûterait 3 497
  une gourde de 2025 vaudrait 29 centimes de 2030

Par scénario de change, retour à la moyenne du modèle à retards échelonnés :
  Stabilité de la gourde : 81 % sur cinq ans
  Dépréciation modérée (8 %/an) : 95 % sur cinq ans
  Dépréciation forte (20 %/an) : 110 % sur cinq ans

7. Limites et lecture des résultats¶

Les données. L'IPC mensuel provient de l'IHSI via la Banque mondiale. La collecte des prix a été perturbée par l'insécurité dans plusieurs départements, ce qui affecte la couverture géographique du panier. La série contenait une cassure de raccordement en janvier 1997, corrigée en section 1 ; le segment antérieur reste moins fiable que le reste. Le taux de change utilisé est le taux officiel de référence : le taux du marché informel peut s'en écarter, et cet écart n'est pas mesuré ici.

Les modèles. Une projection à cinq ans d'une série qui a changé de régime trois fois en trente-cinq ans est une extrapolation, non une prévision au sens strict. Les intervalles reproduisent la variabilité des chocs passés, pas la possibilité d'un nouveau changement de régime : une réforme monétaire, un retour de la sécurité ou au contraire un effondrement des importations sortiraient des bandes affichées.

Le désaccord entre modèles est le résultat, pas un défaut. Le SARIMA ramène l'inflation vers 14 % parce que c'est la moyenne de 2004-2025 ; la variante de persistance la maintient près de 25 % parce que c'est le rythme des cinq dernières années. La section 6.2 montre que déplacer l'échantillon d'estimation déplace l'ancrage d'autant. Aucune de ces valeurs n'est plus vraie que l'autre : elles répondent à deux questions différentes, et le choix entre elles est un jugement sur la persistance du régime ouvert en 2019, pas une conclusion statistique.

La spécification SARIMA a été choisie sur l'échantillon complet, puis réestimée à chaque origine du test hors échantillon. Cette procédure conserve une part d'information future dans le choix de l'ordre, ce qui flatte légèrement le modèle dans la comparaison.

Le canal du change est estimé sur 2004-2025, période qui mélange le couplage prix-change d'avant 2024 et la stabilité de la gourde depuis. Le pass-through mesuré, environ 0,20, est donc une moyenne des deux régimes.

Ce qui manque et qui compterait. Une décomposition du panier par fonction de consommation (alimentation, transport, logement) permettrait de séparer le choc importé du choc local. Les agrégats monétaires mensuels de la BRH, en particulier le financement monétaire du déficit, ajouteraient le canal qui explique probablement l'inflation à gourde stable observée depuis 2024.

In [30]:
print("Figures produites :")
for fichier in sorted((RACINE / "figures").glob("*.png")):
    print(f"  {fichier.name}")
print("\nTables exportées :")
for fichier in sorted((RACINE / "outputs").glob("*.csv")):
    print(f"  {fichier.name}")
Figures produites :
  fig01_niveau_prix.png
  fig02_inflation_glissement_annuel.png
  fig03_saisonnalite.png
  fig04_prix_et_change.png
  fig05_correlation_change.png
  fig06_autocorrelations.png
  fig07_passthrough.png
  fig08_backtest.png
  fig09_projection.png
  fig10_scenarios.png

Tables exportées :
  backtest_rmse.csv
  contexte_macroeconomique.csv
  controle_coherence_annuelle.csv
  correction_rupture_1997.csv
  diagnostics_stationnarite.csv
  faits_stylises_sous_periodes.csv
  passthrough_cumule.csv
  projection_annuelle.csv
  projection_mensuelle.csv
  robustesse_echantillon.csv
  saisonnalite_mensuelle.csv
  scenarios_annuels.csv
  selection_sarima.csv