当前位置:首页>python>Python+GEE | KBDI干旱指数

Python+GEE | KBDI干旱指数

  • 2026-10-11 06:59:36
Python+GEE | KBDI干旱指数
Keetch–Byram干旱指数(KBDI)用于表征土壤水分亏缺程度,即土壤恢复至田间持水量所需的水量。KBDI越高,说明土壤和可燃物越干燥,潜在火险通常越高。本文基于ERA5逐日降水和最高气温,计算2010—2019年黄土高原KBDI。

依赖库导入与全局参数配置

from pathlib import Pathimport eeimport geemapimport geopandas as gpdimport matplotlib as mplimport matplotlib.pyplot as pltimport numpy as npimport pandas as pdimport rioxarrayimport xarray as xrfrom matplotlib import font_managerfrom shapely.geometry import shapefrom xee import helpersimport xclim.indicators.atmos as xindexPROJECT = "giseryu"ROI_ASSET = "projects/giseryu/assets/HUANGTUGAOYUAN"SPINUP_START = "2009-01-01"ANALYSIS_START = "2010-01-01"ANALYSIS_END = "2020-01-01"GRID_DEGREES = 0.3HIGH_KBDI_THRESHOLD = 100.0OUTPUT_DIR = Path(r"E:\黄土高原_KBDI结果")available_fonts = {font.name for font in font_manager.fontManager.ttflist}chinese_candidates = ["Microsoft YaHei", "SimHei", "Noto Sans CJK SC", "Arial Unicode MS"]CHINESE_FONT = next((name for name in chinese_candidates if name in available_fonts), "DejaVu Sans")mpl.rcParams.update({    "font.family": "sans-serif",    "font.sans-serif": [CHINESE_FONT, "Arial", "DejaVu Sans"],    "axes.unicode_minus": False,    "svg.fonttype": "none",    "pdf.fonttype": 42,    "font.size": 9,    "axes.spines.top": False,    "axes.spines.right": False,    "figure.dpi": 120,})print(f"Chinese font: {CHINESE_FONT}")

连接Earth Engine

FORCE_AUTH = Falseif FORCE_AUTH:    ee.Authenticate(force=True)try:    ee.Initialize(        project=PROJECT,        opt_url="https://earthengine-highvolume.googleapis.com",    )except Exception:    ee.Authenticate()    ee.Initialize(        project=PROJECT,        opt_url="https://earthengine-highvolume.googleapis.com",    )print(f"Earth Engine initialized with project: {PROJECT}")

研究区概况与交互地图

roi_fc = ee.FeatureCollection(ROI_ASSET)roi = roi_fc.geometry().simplify(maxError=1000)feature_count = roi_fc.size().getInfo()area_km2 = roi.area(maxError=1000).divide(1e6).getInfo()bounds = roi.bounds(maxError=1000).getInfo()print(f"Feature count: {feature_count}")print(f"Area: {area_km2:,.2f} km2")print(f"Bounds: {bounds}")
study_map = geemap.Map(basemap="SATELLITE")study_map.centerObject(roi_fc, 5)study_map.addLayer(roi_fc, {"color": "yellow"}, "黄土高原研究区")study_map

ERA5逐日气象数据

era5 = (    ee.ImageCollection("ECMWF/ERA5/DAILY")    .filterDate(SPINUP_START, ANALYSIS_END)    .filterBounds(roi)    .select(        ["maximum_2m_air_temperature", "total_precipitation"],        ["max_temp", "pr"],    ))print(f"ERA5 image count including spin-up: {era5.size().getInfo()}")print(f"Bands: {ee.Image(era5.first()).bandNames().getInfo()}")
era5_map = geemap.Map()era5_map.centerObject(roi_fc, 5)mean_temperature = era5.select("max_temp").mean().subtract(273.15).clip(roi)annual_precipitation_gee = (    era5.filterDate(ANALYSIS_START, ANALYSIS_END)    .select("pr")    .sum()    .divide(10)    .multiply(1000)    .clip(roi))era5_map.addLayer(    mean_temperature,    {"min": 5, "max": 25, "palette": ["313695", "74add1", "ffffbf", "f46d43", "a50026"]},    "多年平均日最高温度(°C)",)era5_map.addLayer(    annual_precipitation_gee,    {"min": 200, "max": 800, "palette": ["fff7fb", "9ecae1", "3182bd", "08519c"]},    "多年平均年降水量(mm)",    False,)era5_map.addLayer(roi_fc, {"color": "black"}, "研究区边界")era5_map

转换为xarray并进行单位处理

roi_shapely = shape(roi.getInfo())grid_definition = helpers.fit_geometry(    geometry=roi_shapely,    grid_crs="EPSG:4326",    grid_scale=(GRID_DEGREES, -GRID_DEGREES),)dataset = xr.open_dataset(era5, engine="ee", **grid_definition)dataset = dataset.sortby("time") * 1dataset
dataset["pr"] = dataset["pr"] * 1000.0dataset["pr"].attrs["units"] = "mm/day"dataset["max_temp"] = dataset["max_temp"] - 273.15dataset["max_temp"].attrs["units"] = "degC"analysis_dataset = dataset.sel(time=slice(ANALYSIS_START, "2019-12-31"))mean_annual_precipitation = (    analysis_dataset["pr"]    .resample(time="YE")    .sum(dim="time", skipna=True)    .mean(dim="time", skipna=True))mean_annual_precipitation.attrs["units"] = "mm/year"roi_gdf = geemap.ee_to_gdf(roi_fc)roi_gdf = roi_gdf.set_crs("EPSG:4326") if roi_gdf.crs is None else roi_gdf.to_crs("EPSG:4326")dataset = dataset.rio.write_crs("EPSG:4326")mean_annual_precipitation = mean_annual_precipitation.rio.write_crs("EPSG:4326")dataset_clip = dataset.rio.clip(roi_gdf.geometry, roi_gdf.crs, drop=True)precipitation_climatology = mean_annual_precipitation.rio.clip(roi_gdf.geometry, roi_gdf.crs, drop=True)analysis_clip = dataset_clip.sel(time=slice(ANALYSIS_START, "2019-12-31"))print(dataset_clip)

检查数据的合理性

temperature_climatology = analysis_clip["max_temp"].mean("time", skipna=True)valid_fraction = analysis_clip["pr"].notnull().mean("time") * 100fig, axes = plt.subplots(1, 3, figsize=(15, 4.5), constrained_layout=True)precipitation_climatology.plot(    ax=axes[0], cmap="Blues", robust=True,    cbar_kwargs={"label": "Annual precipitation (mm/year)"},)temperature_climatology.plot(    ax=axes[1], cmap="RdYlBu_r", robust=True,    cbar_kwargs={"label": "Maximum temperature (°C)"},)valid_fraction.plot(    ax=axes[2], cmap="Greens", vmin=95, vmax=100,    cbar_kwargs={"label": "Valid observations (%)"},)titles = ["多年平均年降水量", "多年平均日最高温度", "有效观测覆盖率"]for ax, title in zip(axes, titles):    roi_gdf.boundary.plot(ax=ax, color="black", linewidth=0.7)    ax.set_title(title)    ax.set_xlabel("Longitude")    ax.set_ylabel("Latitude")plt.show()
monthly_precipitation = analysis_clip["pr"].groupby("time.month").sum("time", skipna=True) / 10monthly_temperature = analysis_clip["max_temp"].groupby("time.month").mean("time", skipna=True)regional_monthly_precipitation = monthly_precipitation.mean(("x", "y"), skipna=True)regional_monthly_temperature = monthly_temperature.mean(("x", "y"), skipna=True)months = np.arange(1, 13)fig, ax1 = plt.subplots(figsize=(9, 4.5))ax1.bar(months, regional_monthly_precipitation, color="#5DA5DA", width=0.72, label="Precipitation")ax1.set_xlabel("Month")ax1.set_ylabel("Precipitation (mm/month)", color="#2878B5")ax1.tick_params(axis="y", labelcolor="#2878B5")ax1.set_xticks(months)ax2 = ax1.twinx()ax2.plot(months, regional_monthly_temperature, color="#D9534F", marker="o", linewidth=1.8, label="Temperature")ax2.set_ylabel("Maximum temperature (°C)", color="#C43C39")ax2.tick_params(axis="y", labelcolor="#C43C39")ax2.spines["right"].set_visible(True)ax1.set_title("黄土高原月平均降水与最高温度季节循环")ax1.grid(axis="y", alpha=0.2)plt.tight_layout()plt.show()

计算KBDI

kbdi_full = xindex.keetch_byram_drought_index(    pr=dataset_clip["pr"],    tasmax=dataset_clip["max_temp"],    pr_annual=precipitation_climatology,)kbdi = kbdi_full.sel(time=slice(ANALYSIS_START, "2019-12-31"))kbdi.name = "KBDI"kbdi.attrs["long_name"] = "Keetch-Byram drought index"kbdi.attrs["units"] = "mm"print(f"KBDI range: {float(kbdi.min()):.2f} to {float(kbdi.max()):.2f} mm")print(f"Missing fraction: {float(kbdi.isnull().mean() * 100):.3f}%")kbdi

KBDI的空间分布

annual_maximum = kbdi.resample(time="YE").max("time", skipna=True)median_annual_maximum = annual_maximum.median("time", skipna=True)temporal_p95 = kbdi.quantile(0.95, dim="time", skipna=True)high_kbdi_frequency = (kbdi >= HIGH_KBDI_THRESHOLD).mean("time") * 100absolute_maximum = kbdi.max("time", skipna=True)fig, axes = plt.subplots(2, 2, figsize=(12, 9), constrained_layout=True)maps = [median_annual_maximum, temporal_p95, high_kbdi_frequency, absolute_maximum]titles = [    "多年中位年最大KBDI",    "逐日KBDI的95百分位",    f"高KBDI日频率(≥{HIGH_KBDI_THRESHOLD:.0f} mm)",    "2010—2019年绝对最大KBDI",]cmaps = ["YlOrRd", "YlOrRd", "OrRd", "YlOrRd"]labels = ["KBDI (mm)", "KBDI (mm)", "Frequency (%)", "KBDI (mm)"]for ax, data, title, cmap, label in zip(axes.flat, maps, titles, cmaps, labels):    data.plot(ax=ax, cmap=cmap, robust=True, cbar_kwargs={"label": label})    roi_gdf.boundary.plot(ax=ax, color="black", linewidth=0.7)    ax.set_title(title)    ax.set_xlabel("Longitude")    ax.set_ylabel("Latitude")plt.show()
year_labels = annual_maximum["time"].dt.year.valuesfig, axes = plt.subplots(2, 5, figsize=(16, 6.8), constrained_layout=True, sharex=True, sharey=True)shared_min = float(annual_maximum.quantile(0.02))shared_max = float(annual_maximum.quantile(0.98))for index, (ax, year) in enumerate(zip(axes.flat, year_labels)):    annual_maximum.isel(time=index).plot(        ax=ax,        cmap="YlOrRd",        vmin=shared_min,        vmax=shared_max,        add_colorbar=False,    )    roi_gdf.boundary.plot(ax=ax, color="black", linewidth=0.45)    ax.set_title(f"{int(year)}年")    ax.set_xlabel("")    ax.set_ylabel("")normalizer = mpl.colors.Normalize(vmin=shared_min, vmax=shared_max)colorbar = fig.colorbar(    mpl.cm.ScalarMappable(norm=normalizer, cmap="YlOrRd"),    ax=axes,    orientation="horizontal",    fraction=0.04,    pad=0.06,)colorbar.set_label("Annual maximum KBDI (mm)")fig.suptitle("黄土高原逐年最大KBDI空间分布", fontsize=13)plt.show()

KBDI的时间变化与季节循环

regional_daily_mean = kbdi.mean(("x", "y"), skipna=True)fig, ax = plt.subplots(figsize=(13, 4.5))regional_daily_mean.plot(ax=ax, color="#C43C39", linewidth=0.8)ax.axhline(HIGH_KBDI_THRESHOLD, color="#4D4D4D", linestyle="--", linewidth=0.9, label=f"{HIGH_KBDI_THRESHOLD:.0f} mm")ax.set_title("黄土高原逐日空间平均KBDI")ax.set_xlabel("Date")ax.set_ylabel("KBDI (mm)")ax.grid(alpha=0.2)ax.legend()plt.tight_layout()plt.show()
day_of_year = regional_daily_mean.groupby("time.dayofyear")seasonal_median = day_of_year.median("time", skipna=True)seasonal_p10 = day_of_year.quantile(0.10, dim="time", skipna=True)seasonal_p90 = day_of_year.quantile(0.90, dim="time", skipna=True)fig, ax = plt.subplots(figsize=(9, 4.5))ax.fill_between(    seasonal_median["dayofyear"],    seasonal_p10,    seasonal_p90,    color="#F4A582",    alpha=0.35,    label="10th–90th percentile",)ax.plot(seasonal_median["dayofyear"], seasonal_median, color="#B2182B", linewidth=1.8, label="Median")ax.set_title("黄土高原KBDI多年平均季节循环")ax.set_xlabel("Day of year")ax.set_ylabel("KBDI (mm)")ax.set_xlim(1, 366)ax.grid(alpha=0.2)ax.legend()plt.tight_layout()plt.show()

年变化与极端统计

regional_annual_maximum = annual_maximum.mean(("x", "y"), skipna=True)regional_annual_mean = kbdi.resample(time="YE").mean("time", skipna=True).mean(("x", "y"), skipna=True)regional_high_days = (kbdi >= HIGH_KBDI_THRESHOLD).resample(time="YE").sum("time").mean(("x", "y"), skipna=True)years = regional_annual_maximum["time"].dt.year.valuesfig, axes = plt.subplots(1, 3, figsize=(15, 4.2), constrained_layout=True)axes[0].plot(years, regional_annual_maximum, marker="o", color="#B2182B")axes[0].set_title("区域平均年最大KBDI")axes[0].set_ylabel("KBDI (mm)")axes[1].plot(years, regional_annual_mean, marker="o", color="#EF8A62")axes[1].set_title("区域平均年均KBDI")axes[1].set_ylabel("KBDI (mm)")axes[2].bar(years, regional_high_days, color="#D6604D")axes[2].set_title(f"年均高KBDI日数(≥{HIGH_KBDI_THRESHOLD:.0f} mm)")axes[2].set_ylabel("Days/year")for ax in axes:    ax.set_xlabel("Year")    ax.set_xticks(years)    ax.tick_params(axis="x", rotation=45)    ax.grid(axis="y", alpha=0.2)plt.show()
annual_summary = pd.DataFrame({    "year": years.astype(int),    "regional_mean_kbdi_mm": regional_annual_mean.values,    "regional_mean_annual_max_kbdi_mm": regional_annual_maximum.values,    f"regional_mean_days_kbdi_ge_{int(HIGH_KBDI_THRESHOLD)}": regional_high_days.values,})annual_summary.round(2)

最新文章

随机文章