Service de surveillance de la qualité de l’eau (WQMS) de Digital Earth Africa#

Mots-clés qualité de l’eau, données utilisées ; wq_annual

Aperçu#

Ce notebook fait partie d’une collection de notebooks d’analyse de la qualité de l’eau qui utilisent des données d’observation de la Terre pour évaluer les conditions des eaux de surface. Ensemble, ces notebooks offrent une vision plus globale de la qualité de l’eau en permettant une analyse à travers de multiples indicateurs.

Ce notebook présente les différentes variables disponibles dans le produit de qualité de l’eau annuel. Des flux de travail sont fournis pour surveiller ces variables sur des échelles de temps plus courtes, comme pour la turbidité et l” indice des algues flottantes (Floating Algae Index).

Le Service de surveillance de la qualité de l’eau (WQMS) fournit des informations sur la qualité de l’eau des plans d’eau de surface à travers le continent africain. Les données d’observation de la Terre (EO) offrent une couverture spatiale continue et une surveillance des changements à une fraction du coût et de la complexité logistique de la surveillance in-situ. Alors que les gouvernements nationaux à travers l’Afrique travaillent pour répondre au sous-indicateur 2 de l” Objectif de développement durable (ODD) 6.6.1 — Qualité de l’eau des lacs et des plans d’eau artificiels — le Système de surveillance de la qualité de l’eau (WQMS) fournira des informations opportunes et pertinentes pour la prise de décision afin de soutenir la gestion et l’utilisation durable des ressources en eau de surface. Cela inclut des indicateurs tels que la turbidité globale de l’eau et l’indice d’état trophique, qui sert de substitut à la santé biologique d’un plan d’eau.

Description#

Ce notebook démontre comment charger les données annuelles de qualité de l’eau de DE Africa en utilisant le logiciel Open Data Cube. Les sujets abordés incluent :

  1. Charger et inspecter les données annuelles de qualité de l’eau

  2. Surveillance des changements annuels

  3. Animation dans le temps

  4. Résumés de la qualité de l’eau

Remarque : Consultez le Guide de l’utilisateur de la qualité de l’eau de DE Africa pour des informations techniques détaillées, notamment sur les méthodes, la qualité et l’accès aux données.

Commencer#

Pour exécuter cette analyse, exécutez toutes les cellules du bloc-notes, en commençant par la cellule « Charger les packages ».

Charger des paquets#

Importez les packages Python utilisés pour l’analyse.

[6]:
%matplotlib inline

import datacube
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap, BoundaryNorm
from matplotlib.gridspec import GridSpec
import cmocean  # For nice ocean colour maps
from deafrica_tools.plotting import display_map, xr_animation
from deafrica_tools.waterbodies import get_waterbody, get_water_quality_summary, get_water_quality_rankings
from IPython.core.display import Video

Se connecter au datacube#

Connectez-vous au datacube pour que nous puissions accéder aux données de DE Africa. Le paramètre « app » est un nom unique pour l’analyse qui est basé sur le nom du fichier du notebook.

[7]:
dc = datacube.Datacube(app="DE_Africa_Water_Quality_Monitoring_Service")

Charger et inspecter les données annuelles de qualité de l’eau#

Définissez l’emplacement (gamme lat/lon) et la période pour extraire les données de qualité de l’eau. L’emplacement par défaut est le lac Ichkeul dans le nord de la Tunisie.

Nous définissons explicitement la liste des couches de qualité de l’eau à charger.

[8]:
query = {
    "product": "wq_annual",
    "x": (9.56, 9.77),
    "y": (37.11, 37.21),
    "crs": "EPSG:4326",
    "time": ("2000-01-01", "2025-12-31"),
    "output_crs": "EPSG:6933",
    "resolution": (-10, 10),
    "measurements": [
        "tsm",  # Turbidity
        "chla",  # Chlorophyll-A
        "tsi",  # Trophic State Index
        "agm_ndvi",  # NDVI
        "agm_hue",  # Water Hue
        "agm_owt",  # Optical Water Type
        "agm_fai",  # Floating Algal Index
        "clear_water",  # Clear water mask
        "water_mask",  # Water presence mask
        "tirs_st_ann_max",  # Max water surface temperature
        "tirs_st_ann_med",  # Median water surface temperature
        "tirs_st_ann_min",  # Min water surface temperature
    ],
}
[9]:
display_map(x=query["x"], y=query["y"])
[9]:
Make this Notebook Trusted to load map: File -> Trust Notebook
[10]:
ds = dc.load(**query)
[11]:
ds
[11]:
<xarray.Dataset> Size: 3GB
Dimensions:          (time: 26, y: 1022, x: 2028)
Coordinates:
  * time             (time) datetime64[ns] 208B 2000-07-01T23:59:59.999999 .....
  * y                (y) float64 8kB 4.428e+06 4.427e+06 ... 4.417e+06 4.417e+06
  * x                (x) float64 16kB 9.224e+05 9.224e+05 ... 9.427e+05
    spatial_ref      int32 4B 6933
Data variables:
    tsm              (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    chla             (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    tsi              (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    agm_ndvi         (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    agm_hue          (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    agm_owt          (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    agm_fai          (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    clear_water      (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    water_mask       (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    tirs_st_ann_max  (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    tirs_st_ann_med  (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
    tirs_st_ann_min  (time, y, x) float32 216MB nan nan nan nan ... nan nan nan
Attributes:
    crs:           EPSG:6933
    grid_mapping:  spatial_ref

Masques d’eau#

Comme on peut le voir ci-dessus, plusieurs variables sont disponibles. Ci-dessous, nous traçons le masque de présence d’eau, généré à partir du produit WOfS sur une période glissante de 5 ans (car la végétation flottante peut dominer la surface de l’eau pendant de longues périodes).

Pour les premières couches de qualité de l’eau ci-dessous, nous visualiserons des cartes de 2005 et 2025. Différentes années peuvent être sélectionnées pour différents plans d’eau.

[12]:
ds.sel(time=ds.time.dt.year.isin([2005, 2025])).water_mask.plot(col="time");
../../../_images/sandbox_notebooks_Datasets_Water_Quality_15_0.png

Indice d’algues flottantes (FAI)#

L’indice des algues flottantes (Floating Algae Index - FAI) est utilisé pour détecter et surveiller la végétation flottante et les efflorescences algales, qui sont souvent problématiques ou dangereuses. Le FAI soutient l’alerte précoce et la gestion des risques. Les zones d’algues et de végétation de surface doivent également être cartographiées avant d’appliquer les algorithmes pour la chlorophylle-A ou la turbidité. Le FAI n’est fourni que lorsqu’il est supérieur à un seuil de 0,05, c’est-à-dire lorsqu’il y a une présence probable d’algues flottantes.

[13]:
ds.agm_fai.sel(time=ds.time.dt.year.isin([2005, 2025])).plot(
    robust=True, col="time", cmap="cmo.algae"
);
../../../_images/sandbox_notebooks_Datasets_Water_Quality_17_0.png

Masque d’eau claire#

Le masque d’eau claire est défini comme les zones où l’eau est détectée et où le FAI est inférieur au seuil de 0,05. Les couches suivantes — y compris le type d’eau optique, la teinte (hue), les matières en suspension totales, la chlorophylle-a et l’indice d’état trophique — ne contiennent des valeurs qu’à l’intérieur du masque clear_water.

[14]:
ds.clear_water.sel(time=ds.time.dt.year.isin([2005, 2025])).plot(col="time");
../../../_images/sandbox_notebooks_Datasets_Water_Quality_19_0.png

Matières totales en suspension (MTS)#

Les matières en suspension totales (TSM) annuelles sont fournies comme un indicateur sur une échelle relative, avec des valeurs conçues pour correspondre à \(\mathrm{mg}/\mathrm{m^3}\) de particules en suspension. Les valeurs TSM sont liées à la turbidité ou à l’opacité de l’eau. Des valeurs TSM plus élevées peuvent obstruer les habitats aquatiques et réduire la productivité de l’écosystème et la génération d’oxygène.

[15]:
ds.tsm.sel(time=ds.time.dt.year.isin([2005, 2025])).plot(
    robust=True, col="time", cmap="cmo.turbid"
);
../../../_images/sandbox_notebooks_Datasets_Water_Quality_21_0.png

Chlorophylle-A (ChlA)#

Chlorophyll-A is an indicator of algal content, used to identify conditions conducive to algal blooms and assess biological productivity. ChlA is provided as an indicator on a relative scale, with values designed to correspond to \(\mathrm{mg}/\mathrm{m^3}\). ChlA is readily transformed to the Trophic State Index used in SDG reporting, as is done in the next cell.

[16]:
ds.chla.sel(time=ds.time.dt.year.isin([2005, 2025])).plot(robust=True, col="time");
../../../_images/sandbox_notebooks_Datasets_Water_Quality_23_0.png

Trophic State Index (TSI)#

Derived from chlorophyll-a, the TSI provides an indicator of lake trophic status and productivity. Values typically range from 0 to 100, with higher values reflecting greater algal biomass and increasing eutrophication. In this product, the TSI is calculated as a logarithmic function of chlorophyll-a concentration, following Carlson 1977, with a continuous relationship fitted to the published ChlA–TSI class relationships to estimate TSI directly rather than using discrete class bins.

[17]:
ds.tsi.sel(time=ds.time.dt.year.isin([2005, 2025])).plot(robust=True, col="time");
../../../_images/sandbox_notebooks_Datasets_Water_Quality_25_0.png

Hue#

The water colour, also known as the hue of the water, derived from the spectral reflectances measured by the EO instruments. Hue is expressed as an angle from 0-360. The hue of the waterbody is an objective way to determine changes in water colour.

[18]:
ds.agm_hue.sel(time=ds.time.dt.year.isin([2005, 2025])).plot(
    robust=True, col="time", cmap="hsv", vmin=0, vmax=360
);
../../../_images/sandbox_notebooks_Datasets_Water_Quality_27_0.png

Optical Water Type (OWT)#

OWT (Spyrakos et al., 2018) is a classification of water types used to support EO based monitoring of water quality. For inland waters 13 OWTs are identified based on their spectral characteristics. These can then be characterised as oligotrophic, eutrophic, or hypereutrophic water types. The OWT of a waterbody may change seasonally or over the longer term.

OWT

Class

Dominant Characteristics

OWT1

eutrophic_and_blue_green

Hypereutrophic waters with scum of cyanobacterial bloom and vegetation-like Rrs

OWT2

eutrophic_and_blue_green

Common case waters with diverse reflectance shape and marginal dominance of pigments and Colored Dissolved Organic Matter (CDOM) over inorganic suspended particles

OWT3

oligotrophic_clear

Clear waters

OWT4

eutrophic_and_blue_green

Turbid waters with high organic content

OWT5

eutrophic_and_blue_green

Sediment-laden waters

OWT6

hyper_eutrophic_green_brown

Balanced effects of optically active constituents at shorter wavelengths

OWT7

hyper_eutrophic_green_brown

Highly productive waters with high cyanobacteria abundance and elevated reflectance at red/near-infrared spectral region

OWT8

hyper_eutrophic_green_brown

Productive waters with cyanobacteria presence and with remote sensing reflectance (Rrs) peak close to 700 nm

OWT9

oligotrophic_clear

Optically neighboring to OWT2 waters but with higher Rrs at shorter wavelengths

OWT10

hyper_eutrophic_green_brown

CDOM-rich waters

OWT11

eutrophic_and_blue_green

Waters high in CDOM with cyanobacteria presence and high absorption efficiency by Non-Algal Particles (NAP)

OWT12

eutrophic_and_blue_green

Turbid, moderately productive waters with cyanobacteria presence

OWT13

oligotrophic_clear

Very clear blue waters

[19]:
# Same colours as DE Africa Maps
colors = [
    "#7A0177",
    "#41B6C4",
    "#2C7BB6",
    "#1A9850",
    "#A6611A",
    "#66C2A5",
    "#D73027",
    "#FDAE61",
    "#74ADD1",
    "#542788",
    "#2D004B",
    "#FEE08B",
    "#081D58",
]

levels = np.arange(1, 14)
bounds = np.arange(0.5, 14.5, 1)
labels = [f"OWT{i}" for i in levels]

cmap = ListedColormap(colors)
norm = BoundaryNorm(bounds, cmap.N)

im = ds.agm_owt.sel(time=ds.time.dt.year.isin([2005, 2025])).plot(
    cmap=cmap, norm=norm, col="time", add_colorbar=True
)

cbar = im.cbar
cbar.set_ticks(levels)
cbar.set_ticklabels(labels)

# Remove extra ticks at class boundaries
cbar.ax.minorticks_off()

# Share y axis ticks and labels
im.axs.flat[-1].tick_params(left=False, labelleft=False)

plt.show()
../../../_images/sandbox_notebooks_Datasets_Water_Quality_29_0.png

Water Temperature#

The surface water temperature, derived from the Thermal Infrared Sensor (TIRS) onboard Landsat-8 and Landsat-9, is a key variable influencing biological activity, stratification, and ecosystem dynamics. For each pixel, the 10th, 50th, and 90th percentiles of surface temperature measured in a year are recorded as the minimum, median, and maximum values. Below, we plot the surface temperature measurements for 2024.

[20]:
# Temperature variables to plot
temp_vars = ["tirs_st_ann_min", "tirs_st_ann_med", "tirs_st_ann_max"]
titles = ["Min Temp", "Median Temp", "Max Temp"]

# Shared min and max for colourbar
vmin = min(ds[v].sel(time="2024").min().item() for v in temp_vars)
vmax = max(ds[v].sel(time="2024").max().item() for v in temp_vars)

# Calculate figure size based on previous plot to preserve proportions
figsize_facetgrid = im.fig.get_size_inches()
panel_width = figsize_facetgrid[0] / len(im.axs.flat)
panel_height = figsize_facetgrid[1]

# Account for three plots and a colourbar
plot_ratios = [1, 1, 1]
colorbar_ratio = 0.05
total_ratio = sum(plot_ratios) + colorbar_ratio

figsize = (
    panel_width * total_ratio,
    panel_height
)

fig = plt.figure(figsize=figsize)

gs = GridSpec(
    1, 4,
    width_ratios=[1, 1, 1, 0.05],
    wspace=0.05
)

axs = [fig.add_subplot(gs[i]) for i in range(3)]
cax = fig.add_subplot(gs[3])

for ax, var, title in zip(axs, temp_vars, titles):
    im_temp = ds[var].sel(time="2024").squeeze().plot.contourf(
        ax=ax,
        cmap="cmo.thermal",
        vmin=vmin,
        vmax=vmax,
        add_colorbar=False
    )
    ax.set_title(title)

# Remove ylabel and yticks for 2nd and 3rd plot
for i, ax in enumerate(axs):
    if i > 0:
        ax.set_ylabel("")
        ax.set_yticks([])

# Shared colorbar
fig.colorbar(im_temp, cax=cax)

plt.show()
../../../_images/sandbox_notebooks_Datasets_Water_Quality_31_0.png

Yearly change monitoring#

The annual water quality maps can be used to track year-to-year trends in water quality. We plot the spatial median of each yearly timestep as a time series. We also plot the interquartile ranges, demonstrating a simple method for anomaly detection.

[21]:
# Spatially-averaged time series
ts = ds.median(dim=["x", "y"])
vals = ts["tsm"].values

# Median
median_value = np.nanmedian(vals)

# IQR anomaly thresholds
q1 = np.nanpercentile(vals, 25)
q3 = np.nanpercentile(vals, 75)
iqr = q3 - q1

lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr

# Figure layout
fig = plt.figure(figsize=(10, 4))
gs = GridSpec(1, 2, width_ratios=[4, 1], wspace=0.05)

ax = fig.add_subplot(gs[0])
ax_hist = fig.add_subplot(gs[1], sharey=ax)

# Plot median of medians
ax.axhline(
    median_value, color="black", linestyle="--", label=f"Median = {median_value:.2f}"
)

# Plot time series
ts["tsm"].plot.line(ax=ax, marker="o")

# Plot IQR bounds
ax.axhline(lower_bound, color="black", linestyle=":")
ax.axhline(upper_bound, color="black", linestyle=":")

# Shade anomaly regions
ymin, ymax = ax.get_ylim()
ax.axhspan(ymin, lower_bound, color="red", alpha=0.15, label = "High range (>1.5xIQR)")
ax.axhspan(upper_bound, ymax, color="red", alpha=0.15)

# Shade normal region
ax.axhspan(
    lower_bound, upper_bound, color="green", alpha=0.15, label="Normal range (<1.5×IQR)"
)

# Labels
ax.set_title("Yearly, Spatially-Averaged Total Suspended Matter (TSM)")
ax.set_xlabel("Time")
ax.set_ylabel("TSM (Relative units)")
ax.legend()

# Align histogram bins to yticks
yticks = np.unique(ax.get_yticks())  # Remove duplicates
bins = np.sort(np.concatenate(([ymin], yticks, [ymax])))  # Ensure monotonic

ax_hist.hist(vals, bins=bins, orientation="horizontal")

# Histogram labels
ax_hist.set_xlabel("Count")
ax_hist.set_ylabel("TSM (Relative units)")
ax_hist.yaxis.set_label_position("right")
ax_hist.yaxis.tick_right()

plt.show()
../../../_images/sandbox_notebooks_Datasets_Water_Quality_33_0.png

Animation over time#

The annual maps can be visualised in an animation as demonstrated below using the xr_animation function. The interval parameter specifies the time in milliseconds between each image in the animation.

[22]:
# Calculate inverse scaling of data shape to remove spatial distortion
ny, nx = ds.tsm.isel(time=0).shape
inverse_data_aspect = nx / ny

xr_animation(
    ds,
    bands="tsm",
    output_path="water_quality.mp4",
    interval=500,
    show_date="%Y",
    annotation_kwargs={"fontsize": 28},
    imshow_kwargs={"cmap": "cmo.turbid", "aspect": inverse_data_aspect},
    colorbar_kwargs={"labelcolor": "black"}
)

plt.close()
Video(f"water_quality.mp4", embed=True)
Exporting animation to water_quality.mp4
[22]:

Water Quality Summaries#

Together with the annual variables, the Water Quality Monitoring Service also provides per waterbody annual variable summaries for the over 700,000 waterbodies across the continent identified in the Waterbodies Service. The cell below demonstrates how to load the water quality summary for a waterbody.

For the NDVI and FAI variables, the summaries provided are ndvi_cover and fai_cover respectively. The ndvi_cover indicates the percentage of the waterbody that showed persistent vegetation cover throughout the year. The fai_cover indicates the percentage of the waterbody that showed persistent algal bloom cover throughout the year.

For the remaining variables i.e. hue (hue), optical water type (owt), median temperature (st_median), maximum temperature (st_max), minimum temperature (st_min), chlorophyll-a (chla), turbidity (tsm) and trophic state (ts), the value for each 10th percentile increment of a variable for a waterbody is provided.

The identifier for the waterbody can be found by loading the Waterbodies product in DE Africa Maps and clicking on the waterbody. Here is an example for Lake Ichkeul: https://maps.digitalearth.africa/#share=s-gVaEjpEAOcjPcUjX7N3oOxVBXnx

[23]:
# Waterbody uid for Lake Ichkeul
waterbody_uid = "snwg7u50bd"
waterbody = get_waterbody(waterbody_uid)
waterbody.explore()
[23]:
Make this Notebook Trusted to load map: File -> Trust Notebook
[24]:
wq_summary = get_water_quality_summary(waterbody_uid, start_date="2000-01-01", end_date="2025-12-31")
wq_summary.head()
[24]:
hue_q0_1 hue_q0_2 hue_q0_3 hue_q0_4 hue_q0_5 hue_q0_6 hue_q0_7 hue_q0_8 hue_q0_9 owt_q0_1 ... st_min_q0_2 st_min_q0_3 st_min_q0_4 st_min_q0_5 st_min_q0_6 st_min_q0_7 st_min_q0_8 st_min_q0_9 fai_cover ndvi_cover
date
2000-07-01 40.755914 42.516296 43.284092 43.759880 44.137211 44.484035 44.863377 45.323807 45.998134 1.0 ... 10.447911 11.090235 11.662890 12.091141 12.401832 12.697342 12.984018 13.412905 0.082582 3.034801
2001-07-02 43.807373 46.435708 47.598160 48.390659 49.231701 49.961061 50.539479 51.093925 51.751480 1.0 ... 11.765969 12.053548 12.345901 12.616390 12.846532 13.099591 13.426577 14.073029 0.066233 1.852661
2002-07-02 41.002720 43.746305 45.104209 46.083991 46.902491 47.673332 48.460795 49.372364 50.403860 1.0 ... 11.159149 11.305438 11.429169 11.546671 11.658873 11.790919 11.956471 12.315331 0.061111 1.502174
2003-07-02 48.976505 51.967916 53.384142 54.629906 55.841576 56.796713 57.683242 58.588321 59.644947 1.0 ... 11.747325 12.003418 12.260449 12.437958 12.604795 12.754241 12.938187 13.209305 0.004036 0.093451
2004-07-01 47.050282 48.767092 50.081455 51.027034 51.813360 52.556026 53.325633 54.222552 55.418394 1.0 ... 9.457001 9.807001 10.151760 10.605438 11.629942 12.435919 13.004912 13.464972 7.070729 15.904574

5 rows × 74 columns

[25]:
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(wq_summary.index, wq_summary["fai_cover"], color="#E1BE6A", linestyle="-", marker="o", label="FAI Cover")
ax.plot(wq_summary.index, wq_summary["ndvi_cover"], color="#40B0A6", linestyle="-", marker="o", label="NDVI Cover")
ax.set_xlabel("Time")
ax.set_ylim(0, 100)
ax.set_ylabel("% of waterbody")
ax.set_title(f"Algal Bloom and Vegetation Cover for waterbody {waterbody_uid}")
ax.legend()
plt.tight_layout()
plt.show()
../../../_images/sandbox_notebooks_Datasets_Water_Quality_40_0.png

The Water Quality Monitoring Service also provides a ranking for each waterbody compared to all other waterbodies in the Waterbodies Service. The service averages the water quality summary metrics: fai_cover, ndvi_cover, hue_q0_5, owt_q0_5, chla_q0_5, tsi_q0_5, tsm_q0_5, st_max_q0_5, st_median_q0_5 and st_min_q0_5 over the period 2020 – 2025 and provides a percentile ranking for each waterbody.

As can be seen below, Lake Ichkeul ranks highly for Total Suspended Matter (TSM) compared to other African waterbodies.

[26]:
wq_rank = get_water_quality_rankings(waterbody_uid)
wq_rank
[26]:
fai_cover_percentile ndvi_cover_percentile hue_q0_5_percentile owt_q0_5_percentile chla_q0_5_percentile tsi_q0_5_percentile tsm_q0_5_percentile st_max_q0_5_percentile st_median_q0_5_percentile st_min_q0_5_percentile
0 1.0 2.0 49.0 14.0 5.0 5.0 94.0 8.0 9.0 5.0

Informations Complémentaires#

Licence : Le code de ce carnet est sous licence Apache, version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>. Les données de Digital Earth Africa sont sous licence Creative Commons par attribution 4.0 <https://creativecommons.org/licenses/by/4.0/>.

Contact : Si vous avez besoin d’aide, veuillez poster une question sur le canal Slack Open Data Cube <http://slack.opendatacube.org/>`__ ou sur le GIS Stack Exchange en utilisant la balise open-data-cube (vous pouvez consulter les questions posées précédemment ici). Si vous souhaitez signaler un problème avec ce bloc-notes, vous pouvez en déposer un sur Github.

Version de Datacube compatible :

[27]:
print(datacube.__version__)
1.9.13

Dernier test :

[28]:
from datetime import datetime

datetime.today().strftime("%Y-%m-%d")
[28]:
'2026-03-24'