Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
97d7f1f
New module that handles weather disruptions in the TLO model. Based o…
RachelMurray-Watson Jan 29, 2026
9897b95
Added in reading of parameters, including ones needed for creating th…
RachelMurray-Watson Jan 29, 2026
80bb573
Included resource file for WeatherDisruptions module. Includes all pa…
RachelMurray-Watson Jan 29, 2026
30be6ba
Including the precipitation files for ssp126 and 585 low/medium/high…
RachelMurray-Watson Jan 29, 2026
4ce9e7a
Adding in assignation of facilities to individuals using cKDTree (nee…
RachelMurray-Watson Jan 30, 2026
3d63fc4
Resource file from rmw/climate_change_health with data needed for lin…
RachelMurray-Watson Jan 30, 2026
e65cdd4
Renamed
RachelMurray-Watson Jan 30, 2026
9e47f85
Added in weather_disruptions to full model. Default is not on.
RachelMurray-Watson Jan 30, 2026
16e7d39
Added worldpop files for demography
RachelMurray-Watson Jan 30, 2026
bbe6a84
Updated paths
RachelMurray-Watson Jan 30, 2026
1aed208
Included Weather Disruptions w/o if statement
RachelMurray-Watson Jan 30, 2026
caa9fed
Renamed variables for clarity
RachelMurray-Watson Jan 30, 2026
3261340
Added in coefficients for model as parameters
RachelMurray-Watson Jan 30, 2026
fef6f3f
Added logging for regional DALYs and YLL
RachelMurray-Watson Feb 5, 2026
1d0d270
Added in month as a coefficient
RachelMurray-Watson Feb 5, 2026
5e2ba3f
Fixed so that the continuous variables were multiplied correctly also…
RachelMurray-Watson Feb 5, 2026
95629d0
Added month coef
RachelMurray-Watson Feb 5, 2026
625c8c3
Increase accuracy of some coef
RachelMurray-Watson Feb 6, 2026
fc9ac95
Formatting
RachelMurray-Watson Feb 6, 2026
3faf104
Fixed indenting to ensure logging worked
RachelMurray-Watson Feb 6, 2026
32d2c2c
Added in link to the WeatherDisruption module. Now does checks once a…
RachelMurray-Watson Feb 6, 2026
51d2813
Added handling of on_simulation end
RachelMurray-Watson Feb 6, 2026
a18b389
Checking behaviour
RachelMurray-Watson Feb 9, 2026
a832a5e
Merge branch 'refs/heads/master' into rmw/climate_change_module
RachelMurray-Watson Feb 19, 2026
e4d0ef8
*** REMOVING FILES FROM GIT LFS ***
RachelMurray-Watson Feb 19, 2026
6fc2d68
Update from master
RachelMurray-Watson Feb 19, 2026
6b8f6de
Merge branch 'refs/heads/master' into rmw/climate_change_module
RachelMurray-Watson Mar 3, 2026
f1dc751
Added in demography function that ensures that individuals are spread…
RachelMurray-Watson Mar 5, 2026
a8a1481
Added in function
RachelMurray-Watson Mar 5, 2026
bf59b60
Clean up WeatherDisruptions.py: fix hardcoded year = 2025 and zero-pr…
RachelMurray-Watson Mar 5, 2026
cbf5ff6
added real facility id
RachelMurray-Watson Mar 5, 2026
9ca1443
isort
RachelMurray-Watson Mar 5, 2026
c2603ed
Added geopandas and scipy
RachelMurray-Watson Mar 5, 2026
78aca68
(index=self.multi_index_for_age_and_wealth_and_time_and_region)
RachelMurray-Watson Mar 5, 2026
f76187c
tox environment
RachelMurray-Watson Mar 5, 2026
27dbd0b
fix(demography): correct facility level categorical dtype mismatch in…
RachelMurray-Watson Mar 6, 2026
173683a
added geopandas and scipy to deps
RachelMurray-Watson Mar 6, 2026
a4defbc
updated names of facility characteristics for clarity
RachelMurray-Watson Mar 6, 2026
a47756d
Removed the Facilities_with_lat_long from demog as it was a replicati…
RachelMurray-Watson Mar 6, 2026
263c630
Removed the Facilities_with_lat_long from demog as it was a replicati…
RachelMurray-Watson Mar 6, 2026
d26f2b0
removed unneccessary duplicated file
RachelMurray-Watson Mar 6, 2026
5c3a31e
unused import
RachelMurray-Watson Mar 6, 2026
f62b932
File for testing local runs
RachelMurray-Watson Mar 10, 2026
c082dc3
Added checking if supply or demand side and updating capabilities for…
RachelMurray-Watson Mar 25, 2026
8602b05
Added scale factor for urgency
RachelMurray-Watson Mar 25, 2026
2a4944c
Added test script
RachelMurray-Watson Mar 26, 2026
436cd82
isort
RachelMurray-Watson Mar 26, 2026
7aee745
Tidied up comments, unneccessary checks/print statements
RachelMurray-Watson Mar 26, 2026
26c1868
Refactored logic to minimise code duplication
RachelMurray-Watson Mar 26, 2026
72b2d5f
Refactored logic to minimise code duplication
RachelMurray-Watson Mar 26, 2026
a02be94
Re-introduced clipping of probabilities as was done in ANC paper (int…
RachelMurray-Watson Mar 26, 2026
cab47b9
Fixed bug - parameters were being incorrectly overridden.
RachelMurray-Watson Mar 27, 2026
209e119
Merge branch 'master' into rmw/climate_change_module
RachelMurray-Watson Mar 27, 2026
bea9c02
Return to default
RachelMurray-Watson Mar 27, 2026
03f7628
Return to default
RachelMurray-Watson Mar 27, 2026
507e08b
Return to default
RachelMurray-Watson Mar 27, 2026
817091a
Return to default
RachelMurray-Watson Mar 27, 2026
a23611e
Ensure dtypes are correct
RachelMurray-Watson Mar 27, 2026
b62f2a8
isort src tests
RachelMurray-Watson Mar 27, 2026
0d703f3
Fix HealthBurden multi-index to restore 4-level (sex/age_range/li_wea…
RachelMurray-Watson Apr 1, 2026
5c55a5e
Fixed determinism issue
RachelMurray-Watson Apr 1, 2026
8f1eb8b
Fixed determinism issue
RachelMurray-Watson Apr 9, 2026
cb910a8
Add in more secure check for year
RachelMurray-Watson Apr 9, 2026
0ade703
Ensured climate logging actually happens as expected
RachelMurray-Watson Apr 9, 2026
9acf68b
Rename file
RachelMurray-Watson Apr 10, 2026
e370ee6
fixed date so disruptions would actually run
RachelMurray-Watson Apr 10, 2026
7c806a7
Refactor print statements for weather disruption analysis
RachelMurray-Watson Apr 10, 2026
1ccefc1
Fix print statement formatting in simulation summary
RachelMurray-Watson Apr 10, 2026
464852a
Remove unused import in local_run_analysis.py
RachelMurray-Watson Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions resources/ResourceFile_WeatherDisruption/parameter_values.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
parameter_name,value,param_label,prior_min,prior_max,prior_note,reference
climate_ssp,ssp245,scenario,NA,NA,Structural assumption,
climate_model_ensemble_model,mean,scenario,NA,NA,Structural assumption,
year_effective_climate_disruptions,2025,scenario,NA,NA,Structural assumption,
services_affected_precip,none,scenario,NA,NA,Structural assumption,
delay_in_seeking_care_weather,28,undetermined,0,,Structural assumption,"None, uncertainty analysis"
scale_factor_reseeking_healthcare_post_disruption,1,undetermined,0,,Structural assumption,
scale_factor_prob_disruption,1,undetermined,0,,Structural assumption,
scale_factor_severity_disruption_and_delay,1,undetermined,0,,Structural assumption,
prop_supply_side_disruptions,0.3,undetermined,0,,Structural assumption,
baseline_coef_year,0.002102417,local,0.00205256,0.002152274,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_month,0.0047,local,,,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_altitude,0.0000551,local,1.28E-06,0.00010902,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_min_distance,3.023354283,local,2.760548389,3.286160177,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_urban,1.302601059,local,1.255023345,1.350178772,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_central_west,0.12713551,local,0.072699365,0.181571655,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_northern,-0.582897987,local,-0.63408374,-0.531712235,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_south_east,0.108568778,local,0.054119891,0.163017665,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_south_west,0.047290058,local,-0.012948704,0.107528819,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_government,0.263573318,local,0.197132521,0.330014115,95% confidence interval,"Murray-Watson et al, 2025'"
baseline_coef_private,-1.83913218,local,-2.011993238,-1.666271123,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_year,0.002138682,local,0.002092911,0.002184453,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_month,0.0048,local,,,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_altitude,0.0000402,local,-1.08E-05,9.12E-05,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_min_distance,3.013236002,local,2.752061982,3.274410021,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_urban,1.300494948,local,1.252983312,1.348006584,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_central_west,0.10664008,local,0.058978636,0.154301524,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_northern,-0.584470779,local,-0.629405521,-0.539536038,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_south_east,0.08240437,local,0.040770555,0.124038185,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_south_west,0,local,-0.012948704,0.107528819,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_government,0.261964142,local,0.195516568,0.328411715,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_private,-1.820902432,local,-1.992445895,-1.649358968,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_precip_monthly,-0.000439395,local,-0.000713725,-0.000165064,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_precip_5day,0.000551989,local,-6.46E-05,0.001168602,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_lag_4month,-0.000148841,local,-0.000281277,-1.64E-05,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_lag_9month,-0.000330814,local,-0.000670456,8.83E-06,95% confidence interval,"Murray-Watson et al, 2025'"
precipitation_coef_lag_1_5day,0.000322579,local,3.18E-06,0.000641975,95% confidence interval,"Murray-Watson et al, 2025'"
10 changes: 10 additions & 0 deletions resources/demography/ResourceFile_Facility_Characteristics.csv

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions resources/demography/mwi_pd_2020_1km_ASCII_XYZ_worldpop.txt
Git LFS file not shown
3 changes: 3 additions & 0 deletions resources/demography/worldpop_density_with_districts.dbf
Git LFS file not shown
3 changes: 3 additions & 0 deletions resources/demography/worldpop_density_with_districts.prj
Git LFS file not shown
3 changes: 3 additions & 0 deletions resources/demography/worldpop_density_with_districts.shp
Git LFS file not shown
3 changes: 3 additions & 0 deletions resources/demography/worldpop_density_with_districts.shx
Git LFS file not shown
276 changes: 276 additions & 0 deletions src/scripts/weather_disruptions/local_run_analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
"""
run_weather_disruptions_analysis.py

A single-run simulation script for the WeatherDisruptions module.
Usage:
python run_weather_disruptions_analysis.py
"""

from pathlib import Path

import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from tlo import Date, Simulation, logging
from tlo.analysis.utils import parse_log_file
from tlo.methods.fullmodel import fullmodel

# CONFIGURATION

seed = 0

log_config = {
"filename": "weather_disruptions_analysis",
"directory": "./outputs",
"custom_levels": {
"*": logging.WARNING,
"tlo.methods.weather_disruptions": logging.INFO,
"tlo.methods.weather_disruptions.summary": logging.INFO,
"tlo.methods.healthsystem.summary": logging.INFO,
"tlo.methods.demography": logging.CRITICAL,
"tlo.methods.healthsystem": logging.CRITICAL,
},
}

start_date = Date(2010, 1, 1)
end_date = Date(2030, 1, 1)
pop_size = 5_000

resourcefilepath = Path("./resources")

# COLOURS

COLOUR_DELAYED = "#CC7000"
COLOUR_CANCELLED = "#A8102E"
COLOUR_TOTAL = "#1E3A8A"
COLOUR_RATE = "#5b3f8c"

# SIMULATION SETUP

sim = Simulation(start_date=start_date, seed=seed, log_config=log_config, resourcefilepath=resourcefilepath)

sim.register(
*fullmodel(
module_kwargs={
"WeatherDisruptions": {
"climate_ssp": "ssp245",
"climate_model_ensemble_model": "mean",
"year_effective_climate_disruptions": 2025,
"services_affected_precip": "all",
"scale_factor_prob_disruption": 10.0,
"delay_in_seeking_care_weather": 28.0,
"scale_factor_reseeking_healthcare_post_disruption": 1.0,
"scale_factor_appointment_urgency": 1.0,
"scale_factor_severity_disruption_and_delay": 1.0,
"prop_supply_side_disruptions": 0.5,
}
}
)
)

sim.make_initial_population(n=pop_size)
sim.simulate(end_date=end_date)

# PARSE LOGS

log_df = parse_log_file(sim.log_filepath)

# Monthly summary from the WeatherDisruptions logger
monthly = log_df["tlo.methods.weather_disruptions.summary"]["weather_disruptions_monthly"].copy()
monthly["date"] = pd.to_datetime(monthly["date"])
monthly = monthly.sort_values("date").set_index("date")

# Individual event logs (one row per disrupted HSI)
delayed_events = log_df["tlo.methods.weather_disruptions.summary"].get(
"Weather_delayed_HSI_Event_full_info", pd.DataFrame()
)
cancelled_events = log_df["tlo.methods.weather_disruptions.summary"].get(
"Weather_cancelled_HSI_Event_full_info", pd.DataFrame()
)

# DERIVED QUANTITIES

monthly["total_disrupted"] = monthly["cancelled"] + monthly["delayed"]
monthly["disruption_rate"] = (
monthly["total_disrupted"] / (monthly["hsi_total"] + monthly["total_disrupted"])
).clip(upper=1.0)

cumulative_cancelled = monthly["cancelled"].cumsum()
cumulative_delayed = monthly["delayed"].cumsum()
cumulative_total = monthly["total_disrupted"].cumsum()

# Annual rollup
annual = monthly.resample("YE").agg(
hsi_total=("hsi_total", "sum"),
cancelled=("cancelled", "sum"),
delayed=("delayed", "sum"),
total_disrupted=("total_disrupted", "sum"),
)
annual["disruption_rate"] = (
annual["total_disrupted"] / (annual["hsi_total"] + annual["total_disrupted"])
).clip(upper=1.0)

# Per-treatment-type breakdown from event logs (top N most common)
TOP_N = 15


def _count_by_treatment(df):
if df.empty or "TREATMENT_ID" not in df.columns:
return pd.Series(dtype=float)
return df["TREATMENT_ID"].value_counts()


cancelled_by_type = _count_by_treatment(cancelled_events)
delayed_by_type = _count_by_treatment(delayed_events)

all_types = cancelled_by_type.index.union(delayed_by_type.index)
by_type = pd.DataFrame({
"cancelled": cancelled_by_type.reindex(all_types, fill_value=0),
"delayed": delayed_by_type.reindex(all_types, fill_value=0),
})
by_type["total"] = by_type["cancelled"] + by_type["delayed"]
top_types = by_type.nlargest(TOP_N, "total")

# PLOT 1: Monthly counts — cancelled, delayed, total

fig1, ax1 = plt.subplots(figsize=(12, 5))

ax1.plot(monthly.index, monthly["delayed"], color=COLOUR_DELAYED, lw=1.5, alpha=0.8, label="Delayed")
ax1.plot(monthly.index, monthly["cancelled"], color=COLOUR_CANCELLED, lw=1.5, alpha=0.8, label="Cancelled")
ax1.plot(monthly.index, monthly["total_disrupted"], color=COLOUR_TOTAL, lw=2.5, label="Total disrupted")

ax1.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
ax1.xaxis.set_major_locator(mdates.MonthLocator(interval=6))
plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45, ha="right")
ax1.set_xlabel("Month", fontsize=11, fontweight="bold")
ax1.set_ylabel("Number of HSIs disrupted", fontsize=11, fontweight="bold")
ax1.set_title(
f"Monthly weather-disrupted HSIs — SSP2-4.5 mean (n={pop_size:,})",
fontsize=12, fontweight="bold",
)
ax1.set_ylim(bottom=0)
ax1.legend(fontsize=10, framealpha=0.9)
fig1.tight_layout()
fig1.savefig("./outputs/weather_disruptions_monthly_counts.png", dpi=300, bbox_inches="tight")
plt.show()
plt.close(fig1)

# PLOT 2: Monthly disruption rate (%)

fig2, ax2 = plt.subplots(figsize=(12, 5))

ax2.plot(
monthly.index, monthly["disruption_rate"] * 100,
color=COLOUR_RATE, lw=2, label="Disruption rate (%)",
)

ax2.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=6))
plt.setp(ax2.xaxis.get_majorticklabels(), rotation=45, ha="right")
ax2.set_xlabel("Month", fontsize=11, fontweight="bold")
ax2.set_ylabel("% HSIs disrupted", fontsize=11, fontweight="bold", color=COLOUR_RATE)
ax2.set_ylim(bottom=0)
ax2.set_title(
"Monthly disruption rate and total HSI volume",
fontsize=12, fontweight="bold",
)

lines_2, labels_2 = ax2.get_legend_handles_labels()
ax2.legend(lines_2, labels_2, fontsize=10, framealpha=0.9)

fig2.tight_layout()
fig2.savefig("./outputs/weather_disruptions_monthly_rate.png", dpi=300, bbox_inches="tight")
plt.show()
plt.close(fig2)

# PLOT 3: Annual disruption counts and rate

fig3, ax3a = plt.subplots(figsize=(10, 5))
ax3b = ax3a.twinx()

years = annual.index.year
x = np.arange(len(years))
width = 0.35

ax3a.bar(x - width / 2, annual["delayed"], width, color=COLOUR_DELAYED, alpha=0.85, label="Delayed")
ax3a.bar(x + width / 2, annual["cancelled"], width, color=COLOUR_CANCELLED, alpha=0.85, label="Cancelled")
ax3b.plot(x, annual["disruption_rate"] * 100, "o--", color=COLOUR_RATE, lw=2, ms=6, label="Disruption rate (%)")

ax3a.set_xticks(x)
ax3a.set_xticklabels(years, rotation=45, ha="right")
ax3a.set_xlabel("Year", fontsize=11, fontweight="bold")
ax3a.set_ylabel("Number of HSIs disrupted", fontsize=11, fontweight="bold")
ax3b.set_ylabel("% HSIs disrupted", fontsize=11, fontweight="bold", color=COLOUR_RATE)
ax3a.set_title(
"Annual weather-disrupted HSIs and disruption rate",
fontsize=12, fontweight="bold",
)

lines_a, labels_a = ax3a.get_legend_handles_labels()
lines_b, labels_b = ax3b.get_legend_handles_labels()
ax3a.legend(lines_a + lines_b, labels_a + labels_b, fontsize=10, framealpha=0.9)

fig3.tight_layout()
fig3.savefig("./outputs/weather_disruptions_annual.png", dpi=300, bbox_inches="tight")
plt.show()
plt.close(fig3)

# PLOT 4: Top N HSI types disrupted — horizontal bar chart

if not top_types.empty:
fig4, ax4 = plt.subplots(figsize=(10, max(5, TOP_N * 0.5)))

y_pos = np.arange(len(top_types))
ax4.barh(
y_pos, top_types["delayed"].values,
color=COLOUR_DELAYED, alpha=0.85, label="Delayed",
)
ax4.barh(
y_pos, top_types["cancelled"].values,
left=top_types["delayed"].values,
color=COLOUR_CANCELLED, alpha=0.85, label="Cancelled",
)

ax4.set_yticks(y_pos)
ax4.set_yticklabels(top_types.index, fontsize=9)
ax4.invert_yaxis()
ax4.set_xlabel("Number of HSI events disrupted", fontsize=11, fontweight="bold")
ax4.set_title(
f"Top {TOP_N} HSI types by weather disruption count ({start_date.year}–{end_date.year - 1})",
fontsize=12, fontweight="bold",
)
ax4.legend(fontsize=10, framealpha=0.9)

fig4.tight_layout()
fig4.savefig("./outputs/weather_disruptions_by_hsi_type.png", dpi=300, bbox_inches="tight")
plt.show()
plt.close(fig4)

# ─────────────────────────────────────────────────────────────────────────────
# PRINTED SUMMARY
# ─────────────────────────────────────────────────────────────────────────────

total_cancelled = int(monthly["cancelled"].sum())
total_delayed = int(monthly["delayed"].sum())
total_disrupted = total_cancelled + total_delayed
total_hsi_seen = int(monthly["hsi_total"].sum())
mean_monthly_rate = monthly["disruption_rate"].mean() * 100
peak_month = monthly["disruption_rate"].idxmax()
peak_rate = monthly["disruption_rate"].max() * 100

print("\n" + "=" * 60)
print(" WEATHER DISRUPTIONS — SIMULATION SUMMARY")
print("=" * 60)
print(" Period : {start_date} → {end_date}")
print(" Population size : {pop_size:,}")
print(" SSP / model : ssp245 / mean")
print("-" * 60)
print("Total HSIs seen : {total_hsi_seen:,}")
print(" Total disrupted : {total_disrupted:,}")
print(" Cancelled : {total_cancelled:,} ({total_cancelled / total_disrupted * 100:.1f}%)")
print(" Delayed : {total_delayed:,} ({total_delayed / total_disrupted * 100:.1f}%)")
print(" Mean monthly rate : {mean_monthly_rate:.2f}%")
print(" Peak month : {peak_month.strftime('%Y-%m')} ({peak_rate:.2f}%)")
print("=" * 60)
Loading
Loading