Show the code
import os
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib.colors import TwoSlopeNorm
os.makedirs('../images', exist_ok=True)This notebook produces the three Visualisations-section plots (Vis1-3) for the CHIRPS data source page: district-level rainfall anomalies, province-vs-district annual rainfall, and extreme wet-season counts, all for Tete Province, Mozambique.
Inputs (from ../data/, produced by CHIRPS_Download&Process.ipynb): - moz_boundaries/moz_admin0.shp (country boundary, from the extracted HDX download) - PRCPTOT_anomaly_CHIRPS-3.0-0p05-rnl_201601-202512_vs_199101-202012_TeteProvince_districts.gpkg - PRCPTOT_annual_CHIRPS-3.0-0p05-rnl_198101-202512_TeteProvince.csv - PRCPTOT_annual_CHIRPS-3.0-0p05-rnl_198101-202512_TeteProvince_districts.csv - PRCPTOT_DJF_extreme_seasons_CHIRPS-3.0-0p05-rnl_201601-202512_TeteProvince_districts.gpkg
Outputs: Vis1_CHIRPS_AnnualRainfallAnomaly_Districts_TeteProvince_2016-2025.png, Vis2_CHIRPS_AnnualRainfall_ProvinceDistricts_Tete_1981-2025.png, Vis3_CHIRPS_ExtremeDJF_Districts_TeteProvince_2016-2025.png, saved to ../images/.
How mean annual rainfall in each district over 2016-2025 compares to the 1991-2020 baseline, as a percentage anomaly.
# Centre the colour scale on 0% (no change), with -10% to +20% as the range
norm = TwoSlopeNorm(vmin=-10, vcenter=0, vmax=20)
fig, ax = plt.subplots(figsize=(10, 8))
# Mozambique outline in the background for context
countries.plot(ax=ax, facecolor='#d0d0d0', edgecolor='black', linewidth=1)
tete_districts_anomaly.plot(
column='pct_anomaly',
cmap='RdBu',
legend=True,
norm=norm,
legend_kwds={'label': 'Rainfall Anomaly (%)', 'orientation': 'horizontal'},
ax=ax,
edgecolor='black',
linewidth=0.5,
)
# Label each district at its centre point
for idx, row in tete_districts_anomaly.iterrows():
ax.annotate(
row['adm2_name'],
xy=(row.geometry.centroid.x, row.geometry.centroid.y),
ha='center', fontsize=7
)
# Zoom the map to just Tete Province, with a small buffer
buf = 0.5
ax.set_xlim([tete_districts_anomaly.total_bounds[0] - buf,
tete_districts_anomaly.total_bounds[2] + buf])
ax.set_ylim([tete_districts_anomaly.total_bounds[1] - buf,
tete_districts_anomaly.total_bounds[3] + buf])
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
ax.tick_params(axis='both', labelsize=8)
ax.grid(True, linewidth=0.3, color='grey', alpha=0.5)
ax.set_title(
'Mean Annual Rainfall Anomaly by District, Tete Province\n'
'2016-2025 relative to 1991-2020 baseline (CHIRPS v3)',
fontsize=12
)
plt.savefig(
'../images/Vis1_CHIRPS_AnnualRainfallAnomaly_Districts_TeteProvince_2016-2025.png',
dpi=300, bbox_inches='tight'
)
plt.show()Annual rainfall for Tete Province as a whole (bold blue line), with each individual district shown as a thin grey line in the background, so district-level spread around the province mean is visible.
fig, ax = plt.subplots(figsize=(12, 6))
# Each district as a thin grey line
years = district_annual_loaded.columns.astype(int)
for district in district_annual_loaded.index:
ax.plot(
years, district_annual_loaded.loc[district],
color='grey', alpha=0.4, linewidth=0.8
)
# Province mean as a bold line
ax.plot(
province_annual['year'], province_annual['prcptot'],
color='tab:blue', linewidth=2, label='Tete Province'
)
# 1991-2020 baseline mean, for reference
baseline_mean = province_annual[
(province_annual['year'] >= 1991) &
(province_annual['year'] <= 2020)
]['prcptot'].mean()
ax.axhline(
baseline_mean, color='black', linestyle='--',
linewidth=1, label=f'1991-2020 baseline mean ({baseline_mean:.0f} mm)'
)
ax.set_xlabel('Year')
ax.set_ylabel('Annual Rainfall (mm)')
ax.set_title(
'Annual Rainfall for Tete Province and Districts (1981-2025) - CHIRPS v3',
fontsize=12
)
ax.legend()
ax.grid(axis='y', linewidth=0.5, alpha=0.7)
plt.savefig(
'../images/Vis2_CHIRPS_AnnualRainfall_ProvinceDistricts_Tete_1981-2025.png',
dpi=300, bbox_inches='tight'
)
plt.show()For each district, how many of the last 10 wet seasons (2016-2025) were “extreme” - i.e. exceeded the 90th percentile of DJF rainfall totals seen in the 1991-2020 baseline.
fig, ax = plt.subplots(figsize=(10, 8))
countries.plot(ax=ax, facecolor='#d0d0d0', edgecolor='black', linewidth=1)
tete_districts_extreme.plot(
column='n_extreme',
cmap='Blues',
legend=True,
legend_kwds={'label': 'Number of extreme DJF seasons (out of 10)',
'orientation': 'horizontal'},
ax=ax,
edgecolor='black',
linewidth=0.5,
vmin=0, vmax=3
)
for idx, row in tete_districts_extreme.iterrows():
ax.annotate(
f'{row["adm2_name"]}\n({int(row["n_extreme"])})',
xy=(row.geometry.centroid.x, row.geometry.centroid.y),
ha='center', fontsize=7
)
buf = 0.5
ax.set_xlim([tete_districts_extreme.total_bounds[0] - buf,
tete_districts_extreme.total_bounds[2] + buf])
ax.set_ylim([tete_districts_extreme.total_bounds[1] - buf,
tete_districts_extreme.total_bounds[3] + buf])
# Force the colourbar to show whole numbers only, since these are counts
cbar = ax.get_figure().axes[-1]
cbar.xaxis.set_major_locator(ticker.MaxNLocator(integer=True))
ax.set_title(
'Number of Extreme DJF Seasons by District, Tete Province\n'
'Seasons exceeding 90th percentile of 1991-2020 baseline, 2016-2025 (CHIRPS v3)',
fontsize=12
)
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
ax.tick_params(axis='both', labelsize=8)
ax.grid(True, linewidth=0.3, color='grey', alpha=0.5)
plt.savefig(
'../images/Vis3_CHIRPS_ExtremeDJF_Districts_TeteProvince_2016-2025.png',
dpi=300, bbox_inches='tight'
)
plt.show()