Gridding agricultural census data - EURO-CORDEX#

Gridding based on https://james-brennan.github.io/posts/fast_gridding_geopandas/

import os
import itertools
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import shapely
import xarray as xr
import climag.climag as cplt

Open some gridded climate data#

TS_FILE = os.path.join(
    "data", "EURO-CORDEX", "IE", "IE_EURO-CORDEX_RCA4_EC-EARTH_rcp45.nc"
)
data = xr.open_dataset(TS_FILE, chunks="auto", decode_coords="all")
data
<xarray.Dataset>
Dimensions:       (rlat: 33, rlon: 37, time: 11323, bnds: 2)
Coordinates:
    lat           (rlat, rlon) float64 dask.array<chunksize=(33, 37), meta=np.ndarray>
    lon           (rlat, rlon) float64 dask.array<chunksize=(33, 37), meta=np.ndarray>
  * rlat          (rlat) float64 3.685 3.795 3.905 4.015 ... 6.985 7.095 7.205
  * rlon          (rlon) float64 -17.27 -17.16 -17.05 ... -13.53 -13.41 -13.3
  * time          (time) datetime64[ns] 2040-01-01T12:00:00 ... 2070-12-31T12...
    height        float64 ...
    rotated_pole  |S1 ...
    time_bnds     (time, bnds) datetime64[ns] dask.array<chunksize=(11323, 2), meta=np.ndarray>
    spatial_ref   int64 ...
Dimensions without coordinates: bnds
Data variables:
    PET           (time, rlat, rlon) float32 dask.array<chunksize=(11323, 33, 37), meta=np.ndarray>
    PP            (time, rlat, rlon) float32 dask.array<chunksize=(11323, 33, 37), meta=np.ndarray>
    T             (time, rlat, rlon) float32 dask.array<chunksize=(11323, 33, 37), meta=np.ndarray>
    PAR           (time, rlat, rlon) float32 dask.array<chunksize=(11323, 33, 37), meta=np.ndarray>
Attributes: (12/24)
    Conventions:                    CF-1.4
    contact:                        rossby.cordex@smhi.se
    creation_date:                  2013-07-08-T12:54:04Z
    experiment:                     RCP4.5
    experiment_id:                  rcp45
    driving_experiment:             ICHEC-EC-EARTH, rcp45, r12i1p1
    ...                             ...
    tracking_id:                    0646d817-8046-4c9b-b02b-b9e5feb3325c
    rossby_comment:                 201138: CORDEX Europe 0.11 deg | RCA4 v1 ...
    rossby_run_id:                  201138
    rossby_grib_path:               /nobackup/rossby16/rossby/joint_exp/corde...
    dataset:                        IE_EURO-CORDEX_RCA4_EC-EARTH_rcp45
    comment:                        This dataset has been clipped with the Is...
# keep only one variable
data = data.drop_vars(names=["PET", "PP", "PAR"])
data
<xarray.Dataset>
Dimensions:       (rlat: 33, rlon: 37, time: 11323, bnds: 2)
Coordinates:
    lat           (rlat, rlon) float64 dask.array<chunksize=(33, 37), meta=np.ndarray>
    lon           (rlat, rlon) float64 dask.array<chunksize=(33, 37), meta=np.ndarray>
  * rlat          (rlat) float64 3.685 3.795 3.905 4.015 ... 6.985 7.095 7.205
  * rlon          (rlon) float64 -17.27 -17.16 -17.05 ... -13.53 -13.41 -13.3
  * time          (time) datetime64[ns] 2040-01-01T12:00:00 ... 2070-12-31T12...
    height        float64 ...
    rotated_pole  |S1 ...
    time_bnds     (time, bnds) datetime64[ns] dask.array<chunksize=(11323, 2), meta=np.ndarray>
    spatial_ref   int64 ...
Dimensions without coordinates: bnds
Data variables:
    T             (time, rlat, rlon) float32 dask.array<chunksize=(11323, 33, 37), meta=np.ndarray>
Attributes: (12/24)
    Conventions:                    CF-1.4
    contact:                        rossby.cordex@smhi.se
    creation_date:                  2013-07-08-T12:54:04Z
    experiment:                     RCP4.5
    experiment_id:                  rcp45
    driving_experiment:             ICHEC-EC-EARTH, rcp45, r12i1p1
    ...                             ...
    tracking_id:                    0646d817-8046-4c9b-b02b-b9e5feb3325c
    rossby_comment:                 201138: CORDEX Europe 0.11 deg | RCA4 v1 ...
    rossby_run_id:                  201138
    rossby_grib_path:               /nobackup/rossby16/rossby/joint_exp/corde...
    dataset:                        IE_EURO-CORDEX_RCA4_EC-EARTH_rcp45
    comment:                        This dataset has been clipped with the Is...
# copy CRS
crs = data.rio.crs
crs
CRS.from_wkt('GEOGCRS["undefined",BASEGEOGCRS["undefined",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ID["EPSG",6326]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8901]]],DERIVINGCONVERSION["Pole rotation (netCDF CF convention)",METHOD["Pole rotation (netCDF CF convention)"],PARAMETER["Grid north pole latitude (netCDF CF convention)",39.25,ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]],PARAMETER["Grid north pole longitude (netCDF CF convention)",-162,ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]],PARAMETER["North pole grid longitude (netCDF CF convention)",0,ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]]],CS[ellipsoidal,2],AXIS["longitude",east,ORDER[1],ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]],AXIS["latitude",north,ORDER[2],ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]]]')

Use the gridded data’s bounds to generate a gridded vector layer#

data.rio.bounds()
(-17.32, 3.6299999999999986, -13.25, 7.260000000000002)
xmin, ymin, xmax, ymax = data.rio.bounds()
# the difference between two adjacent rotated lat or lon values is the
# cell size
cell_size = float(data["rlat"][1] - data["rlat"][0])
# create the cells in a loop
grid_cells = []
for x0 in np.arange(xmin, xmax + cell_size, cell_size):
    for y0 in np.arange(ymin, ymax + cell_size, cell_size):
        # bounds
        x1 = x0 - cell_size
        y1 = y0 + cell_size
        grid_cells.append(shapely.geometry.box(x0, y0, x1, y1))
grid_cells = gpd.GeoDataFrame(grid_cells, columns=["geometry"], crs=crs)
grid_cells.shape
(1292, 1)
grid_cells.head()
geometry
0 POLYGON ((-17.43000 3.63000, -17.43000 3.74000...
1 POLYGON ((-17.43000 3.74000, -17.43000 3.85000...
2 POLYGON ((-17.43000 3.85000, -17.43000 3.96000...
3 POLYGON ((-17.43000 3.96000, -17.43000 4.07000...
4 POLYGON ((-17.43000 4.07000, -17.43000 4.18000...
grid_cells.crs
<Derived Geographic 2D CRS: GEOGCRS["undefined",BASEGEOGCRS["undefined",DATUM[ ...>
Name: undefined
Axis Info [ellipsoidal]:
- lon[east]: Longitude (degree)
- lat[north]: Latitude (degree)
Area of Use:
- undefined
Coordinate Operation:
- name: Pole rotation (netCDF CF convention)
- method: Pole rotation (netCDF CF convention)
Datum: World Geodetic System 1984
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich

Subset climate data to visualise the cells#

data_ = data.sel(time="2041-06-21")
data_
<xarray.Dataset>
Dimensions:       (rlat: 33, rlon: 37, time: 1, bnds: 2)
Coordinates:
    lat           (rlat, rlon) float64 dask.array<chunksize=(33, 37), meta=np.ndarray>
    lon           (rlat, rlon) float64 dask.array<chunksize=(33, 37), meta=np.ndarray>
  * rlat          (rlat) float64 3.685 3.795 3.905 4.015 ... 6.985 7.095 7.205
  * rlon          (rlon) float64 -17.27 -17.16 -17.05 ... -13.53 -13.41 -13.3
  * time          (time) datetime64[ns] 2041-06-21T12:00:00
    height        float64 ...
    rotated_pole  |S1 ...
    time_bnds     (time, bnds) datetime64[ns] dask.array<chunksize=(1, 2), meta=np.ndarray>
    spatial_ref   int64 ...
Dimensions without coordinates: bnds
Data variables:
    T             (time, rlat, rlon) float32 dask.array<chunksize=(1, 33, 37), meta=np.ndarray>
Attributes: (12/24)
    Conventions:                    CF-1.4
    contact:                        rossby.cordex@smhi.se
    creation_date:                  2013-07-08-T12:54:04Z
    experiment:                     RCP4.5
    experiment_id:                  rcp45
    driving_experiment:             ICHEC-EC-EARTH, rcp45, r12i1p1
    ...                             ...
    tracking_id:                    0646d817-8046-4c9b-b02b-b9e5feb3325c
    rossby_comment:                 201138: CORDEX Europe 0.11 deg | RCA4 v1 ...
    rossby_run_id:                  201138
    rossby_grib_path:               /nobackup/rossby16/rossby/joint_exp/corde...
    dataset:                        IE_EURO-CORDEX_RCA4_EC-EARTH_rcp45
    comment:                        This dataset has been clipped with the Is...
# find number of grid cells with data
len(data_["T"].values.flatten()[np.isfinite(data_["T"].values.flatten())])
765
plot_transform = cplt.rotated_pole_transform(data_)
# plt.figure(figsize=(9, 7))
axs = plt.axes(projection=cplt.projection_hiresireland)

# plot data for the variable
data_["T"].plot(
    ax=axs,
    cmap="Spectral_r",
    x="rlon",
    y="rlat",
    robust=True,
    transform=plot_transform,
)
grid_cells.to_crs(cplt.projection_hiresireland).boundary.plot(
    ax=axs, color="darkslategrey", linewidth=0.2
)

axs.set_title(None)
plt.axis("equal")
plt.tight_layout()
plt.show()
../_images/0dcf72d603ab0ca8f182c17aabf2ec5b8b85cccac371206f4a4c778a5eec8e0a.png

Drop grid cells without climate data#

grid_centroids = {"wkt": [], "rlon": [], "rlat": []}

for rlon, rlat in itertools.product(
    range(len(data.coords["rlon"])), range(len(data.coords["rlat"]))
):
    data__ = data.isel(rlon=rlon, rlat=rlat)

    # ignore null cells
    if not data__["T"].isnull().all():
        grid_centroids["wkt"].append(
            f"POINT ({float(data__['rlon'].values)} "
            f"{float(data__['rlat'].values)})"
        )
        grid_centroids["rlon"].append(float(data__["rlon"].values))
        grid_centroids["rlat"].append(float(data__["rlat"].values))
grid_centroids = gpd.GeoDataFrame(
    grid_centroids,
    geometry=gpd.GeoSeries.from_wkt(grid_centroids["wkt"], crs=crs),
)
grid_centroids.head()
wkt rlon rlat geometry
0 POINT (-17.265 4.344999999999999) -17.265 4.345 POINT (-17.26500 4.34500)
1 POINT (-17.265 4.455000000000002) -17.265 4.455 POINT (-17.26500 4.45500)
2 POINT (-17.265 4.565000000000001) -17.265 4.565 POINT (-17.26500 4.56500)
3 POINT (-17.265 4.675000000000001) -17.265 4.675 POINT (-17.26500 4.67500)
4 POINT (-17.265 4.785) -17.265 4.785 POINT (-17.26500 4.78500)
grid_centroids.shape
(765, 4)
grid_centroids.crs
<Derived Geographic 2D CRS: GEOGCRS["undefined",BASEGEOGCRS["undefined",DATUM[ ...>
Name: undefined
Axis Info [ellipsoidal]:
- lon[east]: Longitude (degree)
- lat[north]: Latitude (degree)
Area of Use:
- undefined
Coordinate Operation:
- name: Pole rotation (netCDF CF convention)
- method: Pole rotation (netCDF CF convention)
Datum: World Geodetic System 1984
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich
grid_cells = gpd.sjoin(grid_cells, grid_centroids.to_crs(grid_cells.crs))
grid_cells.drop(columns=["wkt", "index_right"], inplace=True)
grid_cells.head()
geometry rlon rlat
40 POLYGON ((-17.32000 4.29000, -17.32000 4.40000... -17.265 4.345
41 POLYGON ((-17.32000 4.40000, -17.32000 4.51000... -17.265 4.455
42 POLYGON ((-17.32000 4.51000, -17.32000 4.62000... -17.265 4.565
43 POLYGON ((-17.32000 4.62000, -17.32000 4.73000... -17.265 4.675
44 POLYGON ((-17.32000 4.73000, -17.32000 4.84000... -17.265 4.785
grid_cells.shape
(765, 3)
# plt.figure(figsize=(9, 7))
axs = plt.axes(projection=cplt.projection_hiresireland)

# plot data for the variable
data_["T"].plot(
    ax=axs,
    cmap="Spectral_r",
    x="rlon",
    y="rlat",
    robust=True,
    transform=plot_transform,
)

grid_cells.to_crs(cplt.projection_hiresireland).plot(
    ax=axs, edgecolor="darkslategrey", facecolor="none", linewidth=0.5
)

grid_centroids.to_crs(cplt.projection_hiresireland).plot(
    ax=axs, color="darkslategrey", markersize=2.5
)

axs.set_title(None)
plt.axis("equal")
plt.tight_layout()
plt.show()
../_images/274df8026bf16e18789d6d44464baec1674378e6e3bea96cad3bc62a6de4f98a.png

Read stocking rate data#

stocking_rate = gpd.read_file(
    os.path.join("data", "agricultural_census", "agricultural_census.gpkg"),
    layer="stocking_rate",
)
stocking_rate.crs
<Derived Projected CRS: EPSG:2157>
Name: IRENET95 / Irish Transverse Mercator
Axis Info [cartesian]:
- E[east]: Easting (metre)
- N[north]: Northing (metre)
Area of Use:
- name: Ireland - onshore. United Kingdom (UK) - Northern Ireland (Ulster) - onshore.
- bounds: (-10.56, 51.39, -5.34, 55.43)
Coordinate Operation:
- name: Irish Transverse Mercator
- method: Transverse Mercator
Datum: IRENET95
- Ellipsoid: GRS 1980
- Prime Meridian: Greenwich
stocking_rate.head()
ENGLISH COUNTY PROVINCE GUID total_cattle total_sheep total_grass_hectares electoral_division WD22CD WD22NM ward_2014_name stocking_rate geometry
0 TURNAPIN DUBLIN Leinster 2ae19629-1cea-13a3-e055-000000000001 0.0 0.0 0.0 Turnapin, Co.Dublin, 04042 0 0 0 0.000000 POLYGON ((717716.712 741601.510, 717759.461 74...
1 DRUMLUMMAN CAVAN Ulster 2ae19629-1caa-13a3-e055-000000000001 2673.0 231.0 1249.1 Drumlumman, Co.Cavan, 32089 0 0 0 1.730446 POLYGON ((637756.185 787640.988, 637753.646 78...
2 CASTLEFORE LEITRIM Connacht 2ae19629-171c-13a3-e055-000000000001 630.0 0.0 805.9 Castlefore, Co.Leitrim, 28063 0 0 0 0.625388 POLYGON ((608196.069 807618.950, 608244.536 80...
3 RAHONA CLARE Munster 2ae19629-1fec-13a3-e055-000000000001 2369.0 0.0 1349.9 Rahona, Co.Clare, 16101 0 0 0 1.403956 POLYGON ((484212.068 651795.629, 484231.866 65...
4 CROSSAKEEL MEATH Leinster 2ae19629-1861-13a3-e055-000000000001 4826.0 671.0 2014.1 Crossakeel, Co.Meath, 11061 0 0 0 1.950201 POLYGON ((663308.409 776111.796, 663305.294 77...
stocking_rate.shape
(3917, 13)
stocking_rate["stocking_rate"].max()
5.627624825011666
stocking_rate["stocking_rate"].min()
0.0
stocking_rate.plot(column="stocking_rate", cmap="Spectral_r")
plt.tick_params(labelbottom=False, labelleft=False)
plt.show()
../_images/240d094ff83e32ea26278d1bad9264e9b1682f2882e29ebf438c0770da44378a.png

Reproject cells to the CRS of the stocking rate data#

# use a projected CRS (e.g. 2157) instead of a geographical CRS (e.g. 4326)
grid_cells = grid_cells.to_crs(stocking_rate.crs)
grid_cells.head()
geometry rlon rlat
40 POLYGON ((419870.326 550490.054, 415715.725 56... -17.265 4.345
41 POLYGON ((415715.725 562009.785, 411561.600 57... -17.265 4.455
42 POLYGON ((411561.600 573530.159, 407407.964 58... -17.265 4.565
43 POLYGON ((407407.964 585051.187, 403254.829 59... -17.265 4.675
44 POLYGON ((403254.829 596572.878, 399102.207 60... -17.265 4.785
axs = stocking_rate.plot(column="stocking_rate", cmap="Spectral_r")
grid_cells.boundary.plot(color="darkslategrey", linewidth=0.5, ax=axs)
axs.tick_params(labelbottom=False, labelleft=False)
plt.show()
../_images/403797e472095d67f4f1d260f3a7523cfd13f622b9705940ccae0cc1e2cbc469.png

Create gridded stocking rate data#

merged = gpd.sjoin(stocking_rate, grid_cells, how="left")
merged.head()
ENGLISH COUNTY PROVINCE GUID total_cattle total_sheep total_grass_hectares electoral_division WD22CD WD22NM ward_2014_name stocking_rate geometry index_right rlon rlat
0 TURNAPIN DUBLIN Leinster 2ae19629-1cea-13a3-e055-000000000001 0.0 0.0 0.0 Turnapin, Co.Dublin, 04042 0 0 0 0.000000 POLYGON ((717716.712 741601.510, 717759.461 74... 998 -14.185 5.005
1 DRUMLUMMAN CAVAN Ulster 2ae19629-1caa-13a3-e055-000000000001 2673.0 231.0 1249.1 Drumlumman, Co.Cavan, 32089 0 0 0 1.730446 POLYGON ((637756.185 787640.988, 637753.646 78... 834 -14.735 5.665
2 CASTLEFORE LEITRIM Connacht 2ae19629-171c-13a3-e055-000000000001 630.0 0.0 805.9 Castlefore, Co.Leitrim, 28063 0 0 0 0.625388 POLYGON ((608196.069 807618.950, 608244.536 80... 768 -14.955 5.885
2 CASTLEFORE LEITRIM Connacht 2ae19629-171c-13a3-e055-000000000001 630.0 0.0 805.9 Castlefore, Co.Leitrim, 28063 0 0 0 0.625388 POLYGON ((608196.069 807618.950, 608244.536 80... 769 -14.955 5.995
3 RAHONA CLARE Munster 2ae19629-1fec-13a3-e055-000000000001 2369.0 0.0 1349.9 Rahona, Co.Clare, 16101 0 0 0 1.403956 POLYGON ((484212.068 651795.629, 484231.866 65... 249 -16.605 4.895
merged.shape
(8403, 16)
axs = merged.plot(column="stocking_rate", cmap="Spectral_r")
grid_cells.boundary.plot(color="darkslategrey", linewidth=0.5, ax=axs)
axs.tick_params(labelbottom=False, labelleft=False)
plt.show()
../_images/5bc4ba70c05573aede0d111b78907e3a2a1d27686685abdbac918076c570568a.png
# compute stats per grid cell, use the mean stocking rate
dissolve = merged[["stocking_rate", "index_right", "geometry"]].dissolve(
    by="index_right", aggfunc=np.mean
)
dissolve.shape
(691, 2)
dissolve.head()
geometry stocking_rate
index_right
40 MULTIPOLYGON (((424582.660 560554.883, 424573.... 0.763591
72 MULTIPOLYGON (((444183.052 537759.340, 444191.... 1.091547
73 MULTIPOLYGON (((444720.334 556193.935, 444722.... 0.606959
74 MULTIPOLYGON (((424582.660 560554.883, 424573.... 0.686445
75 MULTIPOLYGON (((424573.377 560561.357, 424570.... 0.910934
len(dissolve.index.unique())
691
# merge with cell data
grid_cells.loc[dissolve.index, "sr"] = dissolve["stocking_rate"].values
grid_cells.head()
geometry rlon rlat sr
40 POLYGON ((419870.326 550490.054, 415715.725 56... -17.265 4.345 0.763591
41 POLYGON ((415715.725 562009.785, 411561.600 57... -17.265 4.455 NaN
42 POLYGON ((411561.600 573530.159, 407407.964 58... -17.265 4.565 NaN
43 POLYGON ((407407.964 585051.187, 403254.829 59... -17.265 4.675 NaN
44 POLYGON ((403254.829 596572.878, 399102.207 60... -17.265 4.785 NaN
grid_cells.shape
(765, 4)
len(grid_cells["geometry"].unique())
765
grid_cells["sr"].max()
2.9436027416019805
grid_cells["sr"].min()
0.0
plt.figure(figsize=(9, 7))
axs = plt.axes(projection=cplt.projection_hiresireland)

# plot data for the variable
data_["T"].plot(
    ax=axs,
    cmap="Spectral_r",
    x="rlon",
    y="rlat",
    robust=True,
    transform=plot_transform,
)

grid_cells.to_crs(cplt.projection_hiresireland).plot(
    ax=axs, edgecolor="darkslategrey", facecolor="none", linewidth=0.5
)

axs.set_title(None)
plt.axis("equal")
plt.tight_layout()
plt.show()
../_images/9271384ffad831d8c15ba0b8d55659f5501c980197b688455449970e265643fc.png
axs = grid_cells.plot(
    column="sr",
    cmap="Spectral_r",
    scheme="equal_interval",
    edgecolor="darkslategrey",
    linewidth=0.2,
    figsize=(6, 7),
    legend=True,
    legend_kwds={
        "loc": "upper left",
        "fmt": "{:.2f}",
        "title": "Stocking rate [LU ha⁻¹]",
    },
    missing_kwds={
        "color": "darkslategrey",
        "edgecolor": "darkslategrey",
        "label": "No data",
    },
)
for legend_handle in axs.get_legend().legend_handles:
    legend_handle.set_markeredgewidth(0.2)
    legend_handle.set_markeredgecolor("darkslategrey")
axs.tick_params(labelbottom=False, labelleft=False)
plt.axis("equal")
plt.tight_layout()
plt.show()
../_images/e8b2e9faf2a87e59e427c3590a08c18aa35c1df3ea13ab94a45e4e8f10bb76c9.png
# fill no data with min value
grid_cells["sr"] = grid_cells["sr"].fillna(grid_cells["sr"].min())
grid_cells.head()
geometry rlon rlat sr
40 POLYGON ((419870.326 550490.054, 415715.725 56... -17.265 4.345 0.763591
41 POLYGON ((415715.725 562009.785, 411561.600 57... -17.265 4.455 0.000000
42 POLYGON ((411561.600 573530.159, 407407.964 58... -17.265 4.565 0.000000
43 POLYGON ((407407.964 585051.187, 403254.829 59... -17.265 4.675 0.000000
44 POLYGON ((403254.829 596572.878, 399102.207 60... -17.265 4.785 0.000000
axs = grid_cells.plot(
    column="sr",
    cmap="Spectral_r",
    scheme="equal_interval",
    edgecolor="darkslategrey",
    linewidth=0.2,
    figsize=(6, 7),
    legend=True,
    legend_kwds={
        "loc": "upper left",
        "fmt": "{:.2f}",
        "title": "Stocking rate [LU ha⁻¹]",
    },
    missing_kwds={
        "color": "darkslategrey",
        "edgecolor": "darkslategrey",
        "label": "No data",
    },
)
for legend_handle in axs.get_legend().legend_handles:
    legend_handle.set_markeredgewidth(0.2)
    legend_handle.set_markeredgecolor("darkslategrey")
axs.tick_params(labelbottom=False, labelleft=False)
plt.axis("equal")
plt.tight_layout()
plt.show()
../_images/d0693e583b9a8c05131714d99e7ab7e666f2189090aa8bd47a92f26f627c99e4.png
grid_cells.to_file(
    os.path.join("data", "ModVege", "params.gpkg"), layer="eurocordex"
)