Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

CCI Examples

Step 1. Setup & Imports

UKSEDS Training - Monitoring climate change with satellite-derived data

cci_logo.png

1. Introduction:

In this beginner-friendly notebook, we will use real satellite-based data from the ESA Climate Change Initiative (CCI) to explore recent climate trends. For more information on how these datasets are produces, have a look at our Webinars Series - Monitoring climate change from space : https://climate.esa.int/en/climate-change-initiative-training/webinars/

Learning Objectives:

We will use simple Python code and visualisations to explore questions such as:

  • Has land surface temperature increased over the last decades in London?

  • What differences can we observe between rural and urban areas?

This notebook is designed for participants with no prior experience in Python or climate data analysis. You will learn:

  • How to access CCI climate data using the CCI Toolbox python Package

  • How to navigate and subset climate data using Xarray

  • How to create basic plots and statistics with Matplotlib

  • How to save a figure you generated

  • How to map and interpret ECVs changes over time and space

Step 1. Setup & Imports

Before we can start working with the data, we need to import a few Python libraries. These libraries provide useful tools for working with satellite data, performing simple calculations, and creating visualizations.

Don’t worry if you’re not familiar with these yet — we’ll explain what you need as we go.

# ESA Climate Toolbox imports for accessing and plotting ESA CCI data
from esa_climate_toolbox.core import get_op              # Get predefined operations (e.g., time series, averages)
from esa_climate_toolbox.core import list_ecv_datasets   # List available datasets per ECV (Essential Climate Variable)
from esa_climate_toolbox.core import get_store           # Connect to an ESA data store
from esa_climate_toolbox.core import list_datasets       # List all datasets in a store
from esa_climate_toolbox.core import search
from esa_climate_toolbox.ops import plot                 # High-level plotting functions for CCI data

# Used for connecting to remote data sources (e.g., ESA CCI ODP)
from xcube.core.store import new_data_store

# For displaying geographic regions interactively (if desired)
from IPython.display import GeoJSON
import shapely.geometry   # Handling geometric objects like bounding boxes

# Core data science libraries
import pandas as pd       # For tabular data handling and time manipulation
import numpy as np        # For numerical operations
import xarray as xr       # For multi-dimensional climate data structures

# Mapping and plotting
import matplotlib.pyplot as plt       # Plotting library
import cartopy.crs as ccrs            # Cartographic projections for spatial data
import IPython.display 

# Notebook settings
import warnings
warnings.filterwarnings("ignore") # Suppressing warnings to keep notebook output clean
%matplotlib inline

What we just did:

  • numpy: helps with numbers and arrays.

  • matplotlib: lets us create plots and maps.

  • xarray: makes it easy to work with climate datasets.

  • cartopy: helps plot maps with geographic context.

  • Toolbox: a package that facilitates access and computation of CCI data

Up next: We’ll learn how to access ECVs from the CCI Toolbox.

Step 2: Load & Visualize Global LST

Define the Dataset ID

To work with a specific ESA CCI dataset, we need to specify its dataset ID. This unique identifier tells the toolbox which variable and product we want to access.

In this section, we are using a the sea surface temperature (LST) zarr file. You can find the variables of the products under data_vars. For the LST, we will use the variable analysed_LST.

First, we define the dataset ID and the store from which we retrieve the data (for the LST it is esa-cci-zarr). For this notebook we will focus on this dat With the command describe_data, we can have a look at the structure of the file, such as time, dimension and the variable stored within.

#list_datasets()
list_ecv_datasets("LST")
[('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.r1', 'esa-cci'), ('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.3-00.00:00UTC', 'esa-cci'), ('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.3-00.03:00UTC', 'esa-cci'), ('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.3-00.06:00UTC', 'esa-cci'), ('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.3-00.09:00UTC', 'esa-cci'), ('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.3-00.12:00UTC', 'esa-cci'), ('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.3-00.15:00UTC', 'esa-cci'), ('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.3-00.18:00UTC', 'esa-cci'), ('esacci.LST.3-hours.L3S.LST.multi-sensor.multi-platform.IRMGP.3-00.21:00UTC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AATSR.Envisat.ATSR_3.3-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.AATSR.Envisat.ATSR_3.3-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.AATSR.Envisat.ATSR_3.4-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.AATSR.Envisat.ATSR_3.4-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.ATSR-2.ERS-2.ATSR_2.3-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.ATSR-2.ERS-2.ATSR_2.3-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.ATSR-2.ERS-2.ATSR_2.4-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.ATSR-2.ERS-2.ATSR_2.4-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.Metop-A.AVHRMA.2-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.Metop-A.AVHRMA.2-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-15.AVHR15-0-05deg_1DAILY.1-50.ASC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-15.AVHR15-0-05deg_1DAILY.1-50.DESC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-16.AVHR16-0-05deg_1DAILY.1-50.ASC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-16.AVHR16-0-05deg_1DAILY.1-50.DESC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-17.AVHR17-0-05deg_1DAILY.1-50.ASC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-17.AVHR17-0-05deg_1DAILY.1-50.DESC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-18.AVHR18-0-05deg_1DAILY.1-50.ASC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-18.AVHR18-0-05deg_1DAILY.1-50.DESC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-19.AVHR19-0-05deg_1DAILY.1-50.ASC', 'esa-cci'), ('esacci.LST.day.L3C.LST.AVHRR-3.NOAA-19.AVHR19-0-05deg_1DAILY.1-50.DESC', 'esa-cci'), ('esacci.LST.day.L3C.LST.MODIS.Aqua.MODISA.3-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.MODIS.Aqua.MODISA.3-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.MODIS.Aqua.MODISA.4-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.MODIS.Aqua.MODISA.4-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.MODIS.Terra.MODIST.3-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.MODIS.Terra.MODIST.3-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.MODIS.Terra.MODIST.4-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.MODIS.Terra.MODIST.4-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.SLSTR.Sentinel-3A.SLSTRA.3-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.SLSTR.Sentinel-3A.SLSTRA.3-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.SLSTR.Sentinel-3A.SLSTRA.4-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.SLSTR.Sentinel-3A.SLSTRA.4-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.SLSTR.Sentinel-3B.SLSTRB.3-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.SLSTR.Sentinel-3B.SLSTRB.3-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.SLSTR.Sentinel-3B.SLSTRB.4-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.SLSTR.Sentinel-3B.SLSTRB.4-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.VIIRS.NOAA-20.VIIRS1.1-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.VIIRS.NOAA-20.VIIRS1.1-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.VIIRS.SNPP.VIIRS0.1-00.DAY', 'esa-cci'), ('esacci.LST.day.L3C.LST.VIIRS.SNPP.VIIRS0.1-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3C.LST.multi-sensor.multi-platform.SSMI_SSMIS.v2-33.ASC', 'esa-cci'), ('esacci.LST.day.L3C.LST.multi-sensor.multi-platform.SSMI_SSMIS.v2-33.DES', 'esa-cci'), ('esacci.LST.day.L3S.LST.multi-sensor.multi-platform.IRCDR.2-00.DAY', 'esa-cci'), ('esacci.LST.day.L3S.LST.multi-sensor.multi-platform.IRCDR.2-00.NIGHT', 'esa-cci'), ('esacci.LST.day.L3S.LST.multi-sensor.multi-platform.IRCDR.3-00.DAY', 'esa-cci'), ('esacci.LST.day.L3S.LST.multi-sensor.multi-platform.IRCDR.3-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3C.LST.AATSR.Envisat.ATSR_3.3-00.DAY', 'esa-cci'), ('esacci.LST.mon.L3C.LST.AATSR.Envisat.ATSR_3.3-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3C.LST.ATSR-2.ERS-2.ATSR_2.3-00.DAY', 'esa-cci'), ('esacci.LST.mon.L3C.LST.ATSR-2.ERS-2.ATSR_2.3-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3C.LST.MODIS.Aqua.MODISA.3-00.DAY', 'esa-cci'), ('esacci.LST.mon.L3C.LST.MODIS.Aqua.MODISA.3-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3C.LST.MODIS.Terra.MODIST.3-00.DAY', 'esa-cci'), ('esacci.LST.mon.L3C.LST.MODIS.Terra.MODIST.3-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3C.LST.SLSTR.Sentinel-3A.SLSTRA.3-00.DAY', 'esa-cci'), ('esacci.LST.mon.L3C.LST.SLSTR.Sentinel-3A.SLSTRA.3-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3C.LST.SLSTR.Sentinel-3B.SLSTRB.3-00.DAY', 'esa-cci'), ('esacci.LST.mon.L3C.LST.SLSTR.Sentinel-3B.SLSTRB.3-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3C.LST.multi-sensor.multi-platform.SSMI_SSMIS.v2-33.ASC', 'esa-cci'), ('esacci.LST.mon.L3C.LST.multi-sensor.multi-platform.SSMI_SSMIS.v2-33.DES', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRCDR.2-00.DAY', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRCDR.2-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRCDR.3-00.DAY', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRCDR.3-00.NIGHT', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.00:00UTC', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.03:00UTC', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.06:00UTC', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.09:00UTC', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.12:00UTC', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.15:00UTC', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.18:00UTC', 'esa-cci'), ('esacci.LST.mon.L3S.LST.multi-sensor.multi-platform.IRMGP.1-00.21:00UTC', 'esa-cci'), ('esacci.LST.yr.L3C.LST.multi-sensor.multi-platform.SSMI_SSMIS.v2-33.ASC', 'esa-cci'), ('esacci.LST.yr.L3C.LST.multi-sensor.multi-platform.SSMI_SSMIS.v2-33.DES', 'esa-cci'), ('ESACCI-LST-L3C-LST-SSMI17-0.25deg_1DAILY_DES-19960101000000-20201231000000-fv2.33-kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-ATSR_3-0.01deg_1MONTHLY_DAY-200208-201203-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-ATSR_3-0.01deg_1MONTHLY_NIGHT-200208-201203-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-ATSR_2-0.01deg_1MONTHLY_DAY-199508-200306-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-ATSR_2-0.01deg_1MONTHLY_NIGHT-199508-200306-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-MODISA-0.01deg_1MONTHLY_DAY-200207-201812-fv3.00-kr1.1', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-MODISA-0.01deg_1MONTHLY_NIGHT-200207-201812-fv3.00-kr1.1', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-MODIST-0.01deg_1MONTHLY_DAY-200003-201812-fv3.00-kr1.1', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-MODIST-0.01deg_1MONTHLY_NIGHT-200003-201812-fv3.00-kr1.1', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SLSTRA-0.01deg_1MONTHLY_DAY-201605-202012-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SLSTRA-0.01deg_1MONTHLY_NIGHT-201605-202012-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SLSTRB-0.01deg_1MONTHLY_DAY-201812-202012-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SLSTRB-0.01deg_1MONTHLY_NIGHT-201812-202012-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SSMI-0.25deg_1MONTHLY_ASC-199601-202012-fv2.33_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SSMI-0.25deg_1MONTHLY_DES-199601-202012-fv2.33_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1MONTHLY_DAY-199508-202012-fv2.00-kr1.1', 'esa-cci-kc'), ('ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1MONTHLY_NIGHT-199508-202012-fv2.00-kr1.1', 'esa-cci-kc'), ('ESACCI-LST-L3S-LST-IRMGP_-0.05deg_1MONTHLY-20090101000000-20201201000000-fv1.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3S-LST-IRMGP_-0.05deg_1MONTHLY-20090101030000-20201201030000-fv1.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3S-LST-IRMGP_-0.05deg_1MONTHLY-20090101090000-20201201090000-fv1.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3S-LST-IRMGP_-0.05deg_1MONTHLY-20090101120000-20201201120000-fv1.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SSMI13-0.25deg_1YEARLY_ASC-1996-2020-fv2.33_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SSMI13-0.25deg_1YEARLY_DES-1996-2020-fv2.33_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SLSTRB-0.01deg_1DAILY_DAY-20181117-20201231-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-SLSTRB-0.01deg_1DAILY_NIGHT-20181117-20201231-fv3.00_kr1.0', 'esa-cci-kc'), ('ESACCI-LST-L3C-LST-MODISA-0.01deg_1DAILY_DAY-2002-2018-fv3.00.zarr', 'esa-cci-zarr'), ('ESACCI-LST-L3C-LST-MODISA-0.01deg_1DAILY_NIGHT-2002-2018-fv3.00.zarr', 'esa-cci-zarr'), ('ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1DAILY_DAY-1995-2020-fv3.00.zarr', 'esa-cci-zarr'), ('ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1DAILY_NIGHT-1995-2020-fv3.00.zarr', 'esa-cci-zarr'), ('ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1MONTHLY_DAY-1995-2020-fv3.00.zarr', 'esa-cci-zarr'), ('ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1MONTHLY_NIGHT-1995-2020-fv3.00.zarr', 'esa-cci-zarr')]
# Open the ESA CCI zarr store
cci_zarr_store = new_data_store("esa-cci-zarr")
# Which data do we have in the ESA CCI zarr store ?
list_datasets("esa-cci-zarr")
['ESACCI-BIOMASS-L4-AGB-MERGED-100m-2010-2018-fv2.0.zarr', 'ESACCI-BIOMASS-L4-AGB-MERGED-100m-2010-2020-fv4.0.zarr', 'ESACCI-GHG-L2-CH4-SCIAMACHY-WFMD-2002-2011-fv1.zarr', 'ESACCI-GHG-L2-CO2-OCO-2-FOCAL-2014-2021-v10.zarr', 'ESACCI-GHG-L2-CO2-SCIAMACHY-WFMD-2002-2012-fv1.zarr', 'ESACCI-ICESHEETS_Antarctica_GMB-2002-2016-v1.1.zarr', 'ESACCI-ICESHEETS_Greenland_GMB-2003-2016-v1.1.zarr', 'ESACCI-L3C_CLOUD-CLD_PRODUCTS-AVHRR_NOAA-1982-2016-fv3.0.zarr', 'ESACCI-L3C_SNOW-SWE-1979-2018-fv1.0.zarr', 'ESACCI-L3C_SNOW-SWE-1979-2020-fv2.0.zarr', 'ESACCI-L4_FIRE-BA-MODIS-2001-2022-fv5.1.zarr', 'ESACCI-L4_GHRSST-SST-GMPE-GLOB_CDR2.0-1981-2016-v02.0-fv01.0.zarr', 'ESACCI-LC-L4-LCCS-Map-300m-P1Y-1992-2015-v2.0.7b.zarr', 'ESACCI-LST-L3C-LST-MODISA-0.01deg_1DAILY_DAY-2002-2018-fv3.00.zarr', 'ESACCI-LST-L3C-LST-MODISA-0.01deg_1DAILY_NIGHT-2002-2018-fv3.00.zarr', 'ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1DAILY_DAY-1995-2020-fv3.00.zarr', 'ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1DAILY_NIGHT-1995-2020-fv3.00.zarr', 'ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1MONTHLY_DAY-1995-2020-fv3.00.zarr', 'ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1MONTHLY_NIGHT-1995-2020-fv3.00.zarr', 'ESACCI-OC-L3S-IOP-MERGED-1M_MONTHLY_4km_GEO_PML_OCx_QAA-1997-2020-fv5.0.zarr', 'ESACCI-OC-L3S-IOP-MERGED-1M_MONTHLY_4km_GEO_PML_OCx_QAA-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-IOP-MERGED-1D_DAILY_4km_GEO_PML_OCx_QAA-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-IOP-MERGED-1Y_YEARLY_4km_GEO_PML_OCx_QAA-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-IOP-MERGED-5D_DAILY_4km_GEO_PML_OCx_QAA-2022-fv6.0.zarr', 'ESACCI-OC-L3S-IOP-MERGED-8D_DAILY_4km_GEO_PML_OCx_QAA-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-OC_PRODUCTS-MERGED-1D_DAILY_4km_GEO_PML_OCx_QAA-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-OC_PRODUCTS-MERGED-1M_MONTHLY_4km_GEO_PML_OCx_QAA-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-OC_PRODUCTS-MERGED-1Y_YEARLY_4km_GEO_PML_OCx_QAA-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-OC_PRODUCTS-MERGED-5D_DAILY_4km_GEO_PML_OCx_QAA-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-OC_PRODUCTS-MERGED-8D_DAILY_4km_GEO_PML_OCx_QAA-2022-fv6.0.zarr', 'ESACCI-OC-L3S-RRS-MERGED-1M_MONTHLY_4km_GEO_PML_RRS-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-RRS-MERGED-1Y_YEARLY_4km_GEO_PML_RRS-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-RRS-MERGED-1D_DAILY_4km_GEO_PML_RRS-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-RRS-MERGED-5D_DAILY_4km_GEO_PML_RRS-1997-2022-fv6.0.zarr', 'ESACCI-OC-L3S-RRS-MERGED-8D_DAILY_4km_GEO_PML_RRS-1997-2022-fv6.0.zarr', 'ESACCI-PERMAFROST-L4-ALT-MODISLST-AREA4_PP-1997-2018-fv02.0.zarr', 'ESACCI-SEAICE-L3C-SITHICK-RA2_ENVISAT-NH25KMEASE2-2002-2012-fv2.0.zarr', 'ESACCI-SEAICE-L3C-SITHICK-SIRAL_CRYOSAT2-NH25KMEASE2-2010-2017-fv2.0.zarr', 'ESACCI-SEAICE-L4-SICONC-AMSR_50.0kmEASE2-NH-2002-2017-fv2.1.zarr', 'ESACCI-SEALEVEL-IND-MSLTR-MERGED-1993-2016-fv02.zarr', 'ESACCI-SEALEVEL-L4-MSLA-MERGED-1993-2015-fv02.zarr', 'ESACCI-SOILMOISTURE-L3S-SSMV-COMBINED-1978-2020-fv05.3.zarr', 'ESACCI-SOILMOISTURE-L3S-SSMV-COMBINED-1978-2021-fv07.1.zarr', 'ESACCI-WATERVAPOUR-L3C-TCWV-meris-005deg-2002-2017-fv3.2.zarr']
data_id ='ESACCI-LST-L3S-LST-IRCDR_-0.01deg_1MONTHLY_DAY-1995-2020-fv3.00.zarr'

Describe Dataset (Check Available Variables and Metadata)

Before loading the full dataset, it’s helpful to inspect the metadata to understand its structure. This includes:

  • Available variables (e.g., LST, uncertainty estimates)

  • Temporal and spatial coverage

  • Data format and structure

This step ensures we know what the dataset contains and how to work with it. It also helps confirm that the variable we want to plot or analyze is actually included.

🛠️ Tip: You can use the description to verify variable names, dimensions (e.g., lat, lon, time), and time coverage.

📘 More on dataset structure:
🔗 ESA Climate Toolbox – Data Access

# Open the ESA CCI zarr store
cci_zarr_store.describe_data(data_id)
Loading...
#open the LST dataset
LST_ds = cci_zarr_store.open_data(
    data_id=data_id)
display(LST_ds)
Loading...

Has land surface temperature increased over the last decades?

Let’s have a look at the timeseries. For this, we can use the Xarray library functions to our “LST_dataset”. We will here show you how to basic coumputation with a dedicated Python package: esa-cci-toolbox.

First let’s have a look at the different operators from the Toolbox that we can use:

We will here show you how to basic coumputation with a dedicated Python package: esa-cci-toolbox.

First let’s have a look at the different operators from the Toolbox that we can use:

from esa_climate_toolbox.core import list_operations
list_operations()
['add_dataset_values_to_geodataframe', 'adjust_spatial_attrs', 'adjust_temporal_attrs', 'aggregate_statistics', 'animate_map', 'anomaly_external', 'anomaly_internal', 'arithmetics', 'as_geodataframe', 'climatology', 'coregister', 'data_frame_max', 'data_frame_min', 'data_frame_subset', 'detect_outliers', 'diff', 'find_closest', 'fourier_analysis', 'gapfill', 'merge', 'normalise_vars', 'normalize', 'pairwise_var_correlation', 'pixelwise_group_correlation', 'plot', 'plot_contour', 'plot_hist', 'plot_line', 'plot_map', 'plot_scatter', 'query', 'reduce', 'resample', 'select_features', 'select_var', 'standardise_vars', 'statistics', 'subset_spatial', 'subset_temporal', 'subset_temporal_index', 'temporal_aggregation', 'to_dataframe', 'to_dataset', 'tseries_mean', 'tseries_point']

To have more details on the functions and the arguments needed, you can check the following documentation: https://esa-climate-toolbox.readthedocs.io/en/stable/notebooks/Using_Operations/2-ECT_Using_Operations.html#Retrieve-and-plot-timeseries

Defining the Region of Interest (London)

In this section, we define the bounding box for the London region to extract a dataset sample covering both urban and non-urban areas. This will help to analyze temperature variations between these areas. In addition, the data size to download will be much smaller.

# Set bounding box for London
lon_min, lon_max = -0.7, 0.5
lat_min, lat_max = 51.0, 51.9

#It is located between latitudes 51°40′ and 51°1′ N and longitudes 0°30′ W and 0°20′ E;
bbox = (lon_min, lat_min, lon_max, lat_max)
##Display bounding box for reference
IPython.display.GeoJSON(shapely.geometry.box(*bbox).__geo_interface__)
Loading...
# Getting the subset operator
subset_spatial_op = get_op('subset_spatial')
lst_ds_LDN = subset_spatial_op(ds=LST_ds, region=bbox)
lst_ds_LDN
Loading...
# We need to call our operator first, in this case: 'tseries_mean'
# Getting the time-series mean operator
ts_mean_op = get_op('tseries_mean')
# Apply the operation to our dataset , specifying the variable of interest 
lst_LDN_mean = ts_mean_op(
    ds=lst_ds_LDN,
    var='lst'    
)
#this operator gives us a dataset with same coordinates, dimension but with a new variable: the time series of the mean of LST in London
lst_LDN_mean
Loading...
lst_LDN_mean_plot = lst_LDN_mean.lst_mean.compute()  
# Let's make our first plot of the computed yearly mean variable
# Plotting the global mean LST
fig = plt.figure(figsize=(12,3))
# Daily mean LST
(lst_LDN_mean_plot-273.15).plot(c='k',linewidth=1,label='monthly')
# Yearly mean LST
# Add the legend
plt.legend()
# Add a grid 
plt.grid(True, which='both',linestyle='--')
# Add a title and axis labels
plt.title('Monthly LST averaged over London')
plt.ylabel('LST (°C)')
plt.xlabel('Year')
#how to save the figure
#plt.savefig('Mean-LST-1995-2020.png')
plt.show()
<Figure size 1200x300 with 1 Axes>
#Now let's take a look at yearly mean LST , first we use the Toolbox again to compute the yearly mean:
resample_time = get_op('temporal_aggregation') #fetching the operator to temporally aggregate our data

lst_LDN_yr_mean  =resample_time(
    ds=lst_LDN_mean,
    method='mean',
    period='1YE'
) #note: it is also possible to use this operator for different periods, as well as computing maximum and minimum for a given period.
lst_LDN_yr_mean
Loading...
lst_LDN_yr_mean_plot = lst_LDN_yr_mean.lst_mean.compute()  
# Let's make our first plot of the computed yearly mean variable
# Plotting the global mean LST
fig = plt.figure(figsize=(12,3))
# Daily mean LST
(lst_LDN_yr_mean_plot-273.15).plot(c='k',linewidth=1,label='yearly')
# Yearly mean LST
# Add the legend
plt.legend()
# Add a grid 
plt.grid(True, which='both',linestyle='--')
# Add a title and axis labels
plt.title('Yearly LST averaged over London')
plt.ylabel('LST (°C)')
plt.xlabel('Year')
#how to save the figure
#plt.savefig('Mean-LST-1995-2020.png')
plt.show()
<Figure size 1200x300 with 1 Axes>

Your turn: Let’s explore a practical example with ECV data

Evaluating Urban Heat Island Effect in London

During heatwaves, cities may experience more intense heat than rural areas due to the heat accumulation of buildings and streets, wind circulation and less vegetation. This phenomena is called Urban Heat Island (UHI). In this part, we will investigate the UHI effect in London.

👉 Task 1: Using the Toolbox functions, plot the map of LST data in London for July 2019, where an intense heatwave occured. Higlight King's College London and Aldbury on the map.

To have more details on the functions and the arguments needed for your plots, you can check the following documentation: https://esa-climate-toolbox.readthedocs.io/en/latest/api_reference.html#operations

#Type your code here
london_lat, london_lon = 51.511448, -0.116414 # Kings College 
ald_lat, ald_lon =  51.8, -0.6  #Aldbury

#...
### Solution
time_slice="2019-07-01", "2019-07-01"
subset_temporal_op=get_op('subset_temporal')
lst_ds_sub_july=subset_temporal_op(ds=lst_ds_LDN,time_range=time_slice )
plot_op = get_op('plot')
plot(lst_ds_sub_july,
     var='lst', 
    indexers={'time': '2011-06-01'},  # Specify the date
    properties="cmap='viridis'") # Pass other properties here, such as color
london_lat, london_lon = 51.511448, -0.116414 # KC is 51.511448, and the longitude is -0.116414
stalb_lat, stalb_lon =  51.8, -0.6  #51.755001, and the longitude is -0.336000
plt.plot(london_lon, london_lat, linestyle='--', marker='o', color='k', label='London')
plt.plot(stalb_lon, stalb_lat, linestyle='--', marker='o', color='r', label='Aldbury')
#Aldbury ; Latitude · 51.800 ; Longitude · -0.600
plt.legend()
<Figure size 640x480 with 2 Axes>
👉 Task 2: Compute the monthly LST climatogy, that is the mean over the months for 1995-2020 in London.
#Type your code here
#Solution
climatology_op = get_op('climatology')
lst_ds_clim = climatology_op(ds=lst_ds_LDN, var='lst')
👉 Task 3:
  1. Open the Land Cover (LC) data set for 2015. You can use the data_id = ‘ESACCI-LC-L4-LCCS-Map-300m-P1Y-1992-2015-v2.0.7b.zarr’

  2. We want to look at LST for urban pixels and compare it to LST for non-urban pixels, for this, since LC and LST have different longitude and latitude dimensions, regrid LC data to the LST grid.

#Type your code here
#Solution 1
from xcube.core.store import new_data_store
cci_zarr_store = new_data_store("esa-cci-zarr")
lc_ds = cci_zarr_store.open_data(
    'ESACCI-LC-L4-LCCS-Map-300m-P1Y-1992-2015-v2.0.7b.zarr'
)
lc_ds_sub = subset_temporal_op(ds=lc_ds, time_range=['2015-07-03', '2015-07-03'])
#Type your code here
#Solution 2
select_var_op = get_op('select_var')
lc_ds_sub = select_var_op(ds=lc_ds_sub, var="lccs_class")
normalize_op = get_op('normalize')
lc_ds_sub = normalize_op(ds=lc_ds_sub)

coregister_op = get_op('coregister')

downsampled_lc = coregister_op(
    ds_primary=lst_ds_sub_july,
    ds_replica=lc_ds_sub,
    method_ds="mode"
)
downsampled_lc
Loading...
👉 Task 4: Plot the LC map in London for the year 2015.
#Type your code here
# Solution
plot_op(
    ds=downsampled_lc,           # Dataset variable (should be xarray.Dataset or DataArray)
    var='lccs_class',            # Variable name
    title="Land Cover Map",
    properties="cmap='tab20'"    # Choose a colormap suitable for categorical data
)
<Figure size 640x480 with 2 Axes>
👉 (Optional Task): Create the LC map in London using the colormap of Land Cover.

For more information on the colour code, please refer to the Product User Guide. And for your convenience, we define it in the cell below.

# Colour map:
from matplotlib.colors import ListedColormap, BoundaryNorm

#Define some simplified example LCCS classes (you can expand as needed)
class_ids = [10, 50, 100, 130, 150, 160, 190, 200, 210, 220]

class_names = [
    "Cropland",
    "Forest", 
    "Shrubland",
    "Grassland & Herbaceous",
    "Sparse Vegetation",
    "Wetlands",
    "Urban",
    "Bare Areas",
    "Water",
    "Snow/Ice"
]
colors = [
    "#ffff64", "#006400", "#8ca000", "#ffb432", "#ffebaf",
    "#00785a", "#c31400", "#fff5d7", "#0046c8", "#ffffff"
]

# Create colormap and normalization
cmap = ListedColormap(colors)
#Type your code here
# Solution 
norm = BoundaryNorm(class_ids + [max(class_ids)+1], ncolors=cmap.N)
#Plot
plt.figure(figsize=(10, 6))
ax = plt.axes(projection=ccrs.PlateCarree())
img = downsampled_lc.lccs_class[0,:,:].plot(
    ax=ax,
    cmap=cmap,
    norm=norm,
    transform=ccrs.PlateCarree(),
    add_colorbar=False
)
ax.coastlines()
london_lat, london_lon = 51.511448, -0.116414 # KC is 51.511448, and the longitude is -0.116414
ald_lat, ald_lon =  51.815 , -0.58 #, and the longitude is -0.336000
plt.plot(london_lon, london_lat, linestyle='--', marker='x', color='white', label='Kings College')
plt.plot(ald_lon, ald_lat, linestyle='--', marker='x', color='k', label='Aldbury')
#Aldbury ; Latitude · 51.800 ; Longitude · -0.600

# Add lat/lon gridlines
gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=0.5, color="gray", alpha=0.7, linestyle="--")

ax.set_title("Land Cover Map in London")

# Add custom legend
from matplotlib.patches import Patch
legend_handles = [Patch(color=colors[i], label=class_names[i]) for i in range(len(class_ids))]
plt.legend(handles=legend_handles, bbox_to_anchor=(1.05, 1), loc="upper left")

plt.tight_layout()
<Figure size 1000x600 with 1 Axes>
👉 Task 5: We want to look at LST for urban pixels and compare it to LST for non-urban pixels, for this:
  1. Since LC and LST have different longitude and latitude dimensions, regrid LC data to the LST grid.

  2. Create a mask with urban pixels and non-urban pixels. Hint: the urban land cover pixels are coded with the number 190 which you can use for masking.

#Type your code here
#Solution
#class_ids = [10, 50, 100, 130, 150, 160, 190, 200, 210, 220]
#[10, 50, 100, 130, 150, 160, 200, 220]
urban_mask = downsampled_lc['lccs_class'] == 190 # Boolean mask for urban points
urban_mask_da = urban_mask.astype(int).to_dataset(name="urban_mask")  # Convert to dataset with int values

# Apply the threshold to create the non-urban mask
non_urban_mask = ~urban_mask  # Invert the urban mask
non_urban_mask_da = non_urban_mask.astype(int).to_dataset(name="non_urban_mask")  # Convert to dataset with int values

# Apply the threshold to create another mask, f.i vegetation
#veg_mask =  downsampled_lc['lccs_class'] <= 50
#veg_mask_da =  veg_mask.astype(int).to_dataset(name="veg_mask")  # Convert to dataset with int values
# Plot the urban mask
plot_op(
    ds=urban_mask_da,          # Dataset variable
    var='urban_mask',          # Variable name
    title="Urban Mask",
    properties="cmap='Greys'"  # Use a greyscale colormap
)


# Plot the non-urban mask
plot_op(
    ds=non_urban_mask_da,      # Dataset variable
    var='non_urban_mask',      # Variable name
    title="Non-Urban Mask",
    properties="cmap='Greys'"  # Use a greyscale colormap
)


# Plot the vegetation max
#plot_op( ds=veg_mask_da,  var='veg_mask',     title="Veg Mask",  properties="cmap='Greys'"  )
<Figure size 640x480 with 2 Axes>
<Figure size 640x480 with 2 Axes>
👉 Task 6: Next, apply the urban and non-urban mask to LST data to plot:
  1. The climatology of LST for urban pixels

  2. The climatology of LST for non-urban pixels

Highlight Aldbury and King’s College London.

#Type your code here
#Solution
lst_data_array = lst_ds_clim['lst']  # Replace 'lst' with the correct variable name

# Flip the urban_mask along the latitude axis
urban_mask_flipped = np.flipud(urban_mask)  # Flip along the vertical axis

# Apply the flipped mask to the LST data
urban_lst = xr.where(urban_mask_flipped, lst_data_array, np.nan)
non_urban_lst = xr.where(~urban_mask_flipped, lst_data_array, np.nan)

# Plot an example time step
time_step = lst_data_array.time[5]  # Choose the sixth time step for June
plt.figure(figsize=(12, 6))

# Plot Urban LST
plt.subplot(1, 2, 1)
(urban_lst-273.15).sel(time=time_step).plot(cmap="plasma_r",vmin=25, vmax = 35)
plt.title(f"Urban LST at {time_step.values}")
plt.plot(london_lon, london_lat, linestyle='--', marker='x', color='red', label='Kings College')
plt.plot(stalb_lon, stalb_lat, linestyle='--', marker='x', color='k', label='Aldbury')
# Plot Non-Urban LST
plt.subplot(1, 2, 2)
(non_urban_lst-273.15).sel(time=time_step).plot(cmap="plasma_r",vmin=25, vmax = 35)
plt.plot(london_lon, london_lat, linestyle='--', marker='x', color='red', label='Kings College')
plt.plot(stalb_lon, stalb_lat, linestyle='--', marker='x', color='k', label='Aldbury')
plt.title(f"Non-Urban LST at {time_step.values}")

plt.tight_layout()
plt.show()
<Figure size 1200x600 with 4 Axes>
👉 Task 7: Plot the UHI over London. Hint: the UHI is defined as difference of LST between urban and rural areas.
#Type your code here
#Solution
#Compute the mean LST for urban and non-urban pixels over time
urban_mean = urban_lst.mean(dim=["lat", "lon"])
non_urban_mean = non_urban_lst.mean(dim=["lat", "lon"])
UHI = urban_mean - non_urban_mean 
# Plot the time series
plt.figure(figsize=(10, 6))
(urban_mean- 273.15).plot(label="Urban")
UHI.plot(label="UHI")
plt.title("UHI over London")
plt.ylabel("UHI (K)")
plt.xlabel("Time")
plt.legend()
plt.show()
<Figure size 1000x600 with 1 Axes>