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.

Agricultural land from satellite data

This short how-to guides you through the steps to create a timeseries of the agricultural land fraction using satellite-based land cover data and save it as a csv file. The final csv file can be used as, e.g., exposure data for the drought risk estimation.

We use the high-resolution land cover dataset from the Copernicus Climate Data Store: “Land cover classification gridded maps from 1992 to present derived from satellite observations” at 300m spatial resolution. See Copernicus Climate Change Service, Climate Data Store (2017) and the Agricultural Land reference page for more details on this dataset.

⏱️ Time needed! Requesting and downloading this high-resolution data for just one year takes about ~9 minutes. For all years currently available (1992-2023), the download takes about 4 hours.

Settings

User settings

admin_id = "EL64"  # Example admin ID for Central Greece

# Year range for data download
start_year = 1992
end_year = 2022  # Adjust based on latest available data

Setup of environment

import cdsapi
import zipfile
import xarray as xr
import numpy as np
import pandas as pd
import geopandas as gpd
import regionmask
from pathlib import Path
import os

# Set up data directories
data_dir = Path("../data")
satellite_dir = data_dir / "satellite_landcover"
satellite_dir.mkdir(exist_ok=True)

print(f"\nSatellite data directory: {satellite_dir}")

Satellite data directory: ../data/satellite_landcover

Setup region specifics

# Read NUTS shapefiles
regions_dir = data_dir / 'regions'
nuts_shp = regions_dir / 'NUTS_RG_20M_2024_4326' / 'NUTS_RG_20M_2024_4326.shp'
nuts_gdf = gpd.read_file(nuts_shp)

# Select the region of interest
sel_gdf = nuts_gdf[nuts_gdf['NUTS_ID'] == admin_id]
print(f"Found {admin_id} region: {sel_gdf['NUTS_NAME'].values[0]}")
print(f"Bounding box: {sel_gdf.geometry.total_bounds}")
lon_min, lat_min, lon_max, lat_max = sel_gdf.geometry.total_bounds

# Create a regionmask from the admin region geometry
admin_mask = regionmask.from_geopandas(sel_gdf, names='NUTS_ID')
Found EL64 region: Στερεά Ελλάδα
Bounding box: [21.39637798 37.98898161 24.67199242 39.27219519]

Download

Download satellite land cover data from the Copernicus Climate Data Store. The dataset contains two versions:

  • Version 2.0.7cds: Years 1992-2015

  • Version 2.1.1: Years 2016-present

Dataset information: https://cds.climate.copernicus.eu/datasets/satellite-land-cover

# Download land cover data for each year
for yyyy in range(start_year, end_year + 1):
    print(f"Downloading land cover data for {yyyy}")

    # Check if file already exists
    zip_file = satellite_dir / f'land_cover_{admin_id}_{yyyy}.zip'
    if zip_file.exists():
        # print(f"  - File {zip_file.name} already exists. Skipping download.")
        continue
    
    # Select version based on year
    if yyyy <= 2015:
        lc_version = "v2_0_7cds"
    else:
        lc_version = "v2_1_1"
    
    dataset = "satellite-land-cover"
    request = {
        "variable": "all",
        "year": [str(yyyy)],
        "version": [lc_version],
        "area": [lat_max, lon_min, lat_min, lon_max]  # [North, West, South, East]
    }

    client = cdsapi.Client()
    client.retrieve(dataset, request).download(str(zip_file))
    print(f"  - Downloaded {zip_file.name}")

print("\nLand cover download complete!")
Downloading land cover data for 1992
Downloading land cover data for 1993
Downloading land cover data for 1994
Downloading land cover data for 1995
Downloading land cover data for 1996
Downloading land cover data for 1997
Downloading land cover data for 1998
Downloading land cover data for 1999
Downloading land cover data for 2000
Downloading land cover data for 2001
Downloading land cover data for 2002
Downloading land cover data for 2003
Downloading land cover data for 2004
Downloading land cover data for 2005
Downloading land cover data for 2006
Downloading land cover data for 2007
Downloading land cover data for 2008
Downloading land cover data for 2009
Downloading land cover data for 2010
Downloading land cover data for 2011
Downloading land cover data for 2012
Downloading land cover data for 2013
Downloading land cover data for 2014
Downloading land cover data for 2015
Downloading land cover data for 2016
Downloading land cover data for 2017
Downloading land cover data for 2018
Downloading land cover data for 2019
Downloading land cover data for 2020
Downloading land cover data for 2021
Downloading land cover data for 2022

Land cover download complete!

Process

Create region mask


# Inspect one zip file to create mask
first_year = start_year
zip_file = satellite_dir / f'land_cover_{admin_id}_{first_year}.zip'

with zipfile.ZipFile(zip_file, 'r') as zip_ref:
    file_list = zip_ref.namelist()
    nc_files_in_zip = [f for f in file_list if f.endswith('.nc')]
    
    if nc_files_in_zip:
        lc_filename = nc_files_in_zip[0]
        zip_ref.extract(lc_filename, satellite_dir)
    else:
        print("No NetCDF file found in zip!")

# Load land cover data to get grid
lc_file = satellite_dir / lc_filename
lc_ds = xr.open_dataset(lc_file)

# Subset to our region (initial bounding box)
lc_region = lc_ds.sel(
    lon=slice(lon_min, lon_max),
    lat=slice(lat_max, lat_min)
)

# Create admin region mask: 1 where inside region, 0 elsewhere
lc_admin_mask_raw = admin_mask.mask(lc_region.lon, lc_region.lat)
lc_admin_mask = xr.DataArray(
    (~np.isnan(lc_admin_mask_raw.values)).astype(float),
    coords={'lat': lc_region.lat, 'lon': lc_region.lon},
    dims=['lat', 'lon']
)

print(f"Admin region area (grid cells): {lc_admin_mask.sum().values:.0f}")
print(f"Grid shape: {lc_admin_mask.shape}")

# Clean up extracted file
lc_file.unlink()
Admin region area (grid cells): 215798
Grid shape: (462, 1179)

Calculate agricultural land fraction

Agricultural land corresponds to LCCS classes 10-40 in the satellite dataset.


# Initialize list to store results
ag_data = []
latest_ag_mask = None  # Store latest year for saving as raster
latest_lc_region = None  # Store latest region data for coordinate info

for yyyy in range(start_year, end_year + 1):
    # Extract and load NetCDF file
    zip_file = satellite_dir / f'land_cover_{admin_id}_{yyyy}.zip'
    
    with zipfile.ZipFile(zip_file, 'r') as zip_ref:
        file_list = zip_ref.namelist()
        nc_files_in_zip = [f for f in file_list if f.endswith('.nc')]
        
        if nc_files_in_zip:
            lc_filename = nc_files_in_zip[0]
            zip_ref.extract(lc_filename, satellite_dir)
        else:
            print(f"  - No NetCDF file found in {zip_file.name}. Skipping.")
            continue

    # Load land cover data
    lc_file = satellite_dir / lc_filename
    lc_ds = xr.open_dataset(lc_file)

    # Subset to our region
    lc_region = lc_ds.sel(
        lon=slice(lon_min, lon_max),
        lat=slice(lat_max, lat_min)
    )

    # Create agricultural land mask (LCCS classes 10-40)
    ag_mask = ((lc_region['lccs_class'] >= 10) & (lc_region['lccs_class'] <= 40)).astype(float)

    # Calculate agricultural land fraction within admin region
    ag_fraction = (ag_mask * lc_admin_mask).sum().values / lc_admin_mask.sum().values
    
    # Store result
    ag_data.append({'year': yyyy, 'ag_fraction': ag_fraction})
    
    # Save latest year for raster output
    if yyyy == end_year:
        latest_ag_mask = ag_mask.copy()
        latest_lc_region = lc_region.copy()
    
    # Clean up extracted file
    lc_file.unlink()

# Create pandas DataFrame
ag_timeseries = pd.DataFrame(ag_data)
ag_timeseries
Loading...

Save

Save results to CSV

# Create output directory
output_dir = data_dir / admin_id / 'satellite_land_cover'
output_dir.mkdir(parents=True, exist_ok=True)

# Save to CSV
csv_file = output_dir / f'agricultural_land_fraction_{admin_id}.csv'
ag_timeseries.to_csv(csv_file, index=False)
print(f"Saved agricultural land fraction timeseries to: {csv_file}")
print(f"\nSummary statistics:")
print(ag_timeseries['ag_fraction'].describe())
Saved agricultural land fraction timeseries to: ../data/EL64/satellite_land_cover/agricultural_land_fraction_EL64.csv

Summary statistics:
count    31.000000
mean      0.284541
std       0.009277
min       0.276912
25%       0.278232
50%       0.281008
75%       0.285858
max       0.304581
Name: ag_fraction, dtype: float64

Save spatial raster (latest year)

# Save the most recent year as GeoTIFF for use in tutorial
if latest_ag_mask is not None and latest_lc_region is not None:
    # Set spatial dimensions and add CRS information
    latest_ag_mask = latest_ag_mask.rio.set_spatial_dims(x_dim='lon', y_dim='lat')
    latest_ag_mask.rio.write_crs("EPSG:4326", inplace=True)
    
    # Save as GeoTIFF
    tif_file = output_dir / f"satellite_landcover_{admin_id}.tif"
    latest_ag_mask.rio.to_raster(tif_file, driver="GTiff")
    print(f"Saved {end_year} satellite land cover raster to: {tif_file}")
else:
    print("No latest year data available to save as raster.")
Saved 2022 satellite land cover raster to: ../data/EL64/satellite_land_cover/satellite_landcover_EL64.tif

Output summary

Output files created:

  • agricultural_land_fraction_{admin_id}.csv - Agricultural land fraction timeseries (1992-2022)

  • satellite_landcover_{admin_id}.tif - Spatial raster of satellite land cover (latest year)

These files can now be used in the agricultural land tutorial for visualization and comparison with CORINE data.

References
  1. Copernicus Climate Change Service, Climate Data Store. (2017). Land cover classification gridded maps from 1992 to present derived from satellite observations [Data set]. Copernicus Climate Change Service (C3S) Climate Data Store (CDS). https://cds.climate.copernicus.eu/datasets/satellite-land-cover