当前位置:首页>python>Python | 重心迁移

Python | 重心迁移

  • 2026-10-11 07:14:29
Python | 重心迁移

重心迁移模型是一种经典的空间分析方法,通过计算不同年份数据的空间重心位置及其变化轨迹,揭示空间格局的演化方向。

一、数据准备

以黄河流域2000—2025年NDVI数据为例,构建 NDVI 重心迁移轨迹。

二、Python代码

import osimport numpy as npimport pandas as pdimport rasterioimport matplotlib.pyplot as pltimport geopandas as gpdfrom shapely.geometry import Point, LineStringfrom pyproj import Transformer, Geod, CRSfrom matplotlib.ticker import FormatStrFormatterfolder = r"C:\Users\Yu\Pictures\NDVI"tif_items = [    {"year": 2000, "file": "NDVI_2000.tif"},    {"year": 2005, "file": "NDVI_2005.tif"},    {"year": 2010, "file": "NDVI_2010.tif"},    {"year": 2015, "file": "NDVI_2015.tif"},    {"year": 2020, "file": "NDVI_2020.tif"},    {"year": 2025, "file": "NDVI_2025.tif"},]out_dir = os.path.join(folder, "NDVI_centroid_result")os.makedirs(out_dir, exist_ok=True)out_csv = os.path.join(out_dir, "NDVI_centroid_lonlat.csv")out_point_shp = os.path.join(out_dir, "NDVI_centroid_points.shp")out_segment_shp = os.path.join(out_dir, "NDVI_centroid_segments.shp")out_trajectory_shp = os.path.join(out_dir, "NDVI_centroid_trajectory.shp")out_png = os.path.join(out_dir, "NDVI_centroid_trajectory.png")assume_crs_if_missing = Nonedef calc_ndvi_centroid(tif_path):    with rasterio.open(tif_path) as src:        transform = src.transform        nodata = src.nodata        src_crs = src.crs        if src_crs is None:            if assume_crs_if_missing is None:                raise ValueError(f"{tif_path} has no CRS. Please define its CRS first.")            src_crs = CRS.from_user_input(assume_crs_if_missing)        sum_w = 0.0        sum_x = 0.0        sum_y = 0.0        for _, window in src.block_windows(1):            arr = src.read(1, window=window).astype("float64")            mask = np.isfinite(arr)            if nodata is not None:                mask &= arr != nodata            mask &= (arr >= -1) & (arr <= 1)            mask &= arr > 0            if not np.any(mask):                continue            weights = arr            rows, cols = np.indices(arr.shape)            rows = rows + window.row_off            cols = cols + window.col_off            rows_valid = rows[mask]            cols_valid = cols[mask]            w_valid = weights[mask]            xs, ys = transform * (cols_valid + 0.5, rows_valid + 0.5)            sum_w += np.sum(w_valid)            sum_x += np.sum(xs * w_valid)            sum_y += np.sum(ys * w_valid)        if sum_w == 0:            return np.nan, np.nan, src_crs        centroid_x = sum_x / sum_w        centroid_y = sum_y / sum_w        return centroid_x, centroid_y, src_crsrecords = []for item in tif_items:    year = item["year"]    tif_path = os.path.join(folder, item["file"])    if not os.path.exists(tif_path):        print(f"Missing file: {tif_path}")        continue    x_src, y_src, src_crs = calc_ndvi_centroid(tif_path)    transformer = Transformer.from_crs(src_crs, "EPSG:4326", always_xy=True)    lon, lat = transformer.transform(x_src, y_src)    records.append({        "year": year,        "lon": lon,        "lat": lat,        "x_src": x_src,        "y_src": y_src    })df = pd.DataFrame(records)df = df.dropna()df = df.sort_values("year").reset_index(drop=True)df["lon"] = df["lon"].round(2)df["lat"] = df["lat"].round(2)df.to_csv(out_csv, index=False, encoding="utf-8-sig")point_gdf = gpd.GeoDataFrame(    df.copy(),    geometry=[Point(lon, lat) for lon, lat in zip(df["lon"], df["lat"])],    crs="EPSG:4326")point_gdf.to_file(out_point_shp, driver="ESRI Shapefile", encoding="utf-8")geod = Geod(ellps="WGS84")segment_records = []for i in range(len(df) - 1):    row1 = df.iloc[i]    row2 = df.iloc[i + 1]    lon1 = row1["lon"]    lat1 = row1["lat"]    lon2 = row2["lon"]    lat2 = row2["lat"]    azimuth, back_azimuth, distance_m = geod.inv(lon1, lat1, lon2, lat2)    segment_records.append({        "from_yr": int(row1["year"]),        "to_yr": int(row2["year"]),        "lon_st": round(lon1, 2),        "lat_st": round(lat1, 2),        "lon_end": round(lon2, 2),        "lat_end": round(lat2, 2),        "dist_km": round(distance_m / 1000, 2),        "azimuth": round(azimuth % 360, 2),        "geometry": LineString([(lon1, lat1), (lon2, lat2)])    })segment_gdf = gpd.GeoDataFrame(segment_records, crs="EPSG:4326")segment_gdf.to_file(out_segment_shp, driver="ESRI Shapefile", encoding="utf-8")trajectory_gdf = gpd.GeoDataFrame(    [{        "start_yr": int(df.iloc[0]["year"]),        "end_yr": int(df.iloc[-1]["year"]),        "geometry": LineString(list(zip(df["lon"], df["lat"])))    }],    crs="EPSG:4326")trajectory_gdf.to_file(out_trajectory_shp, driver="ESRI Shapefile", encoding="utf-8")plt.rcParams["font.family"] = "Times New Roman"plt.rcParams["font.size"] = 24plt.rcParams["axes.titlesize"] = 24plt.rcParams["axes.labelsize"] = 24plt.rcParams["xtick.labelsize"] = 24plt.rcParams["ytick.labelsize"] = 24plt.rcParams["legend.fontsize"] = 24fig, ax = plt.subplots(figsize=(9, 8), dpi=300)for i in range(len(df) - 1):    x1 = df.loc[i, "lon"]    y1 = df.loc[i, "lat"]    x2 = df.loc[i + 1, "lon"]    y2 = df.loc[i + 1, "lat"]    ax.plot(        [x1, x2],        [y1, y2],        color="black",        linewidth=1.8,        zorder=2    )    ax.annotate(        "",        xy=(x2, y2),        xytext=(x1, y1),        arrowprops=dict(            arrowstyle="->",            color="black",            lw=1.8        ),        zorder=3    )ax.scatter(    df["lon"],    df["lat"],    s=70,    color="black",    zorder=4)for _, row in df.iterrows():    ax.text(        row["lon"],        row["lat"],        str(int(row["year"])),        fontsize=24,        ha="left",        va="bottom",        color="black",        zorder=5    )ax.set_title("NDVI Centroid Migration Trajectory from 2000 to 2025")ax.set_xlabel("Longitude (°E)", fontsize=24)ax.set_ylabel("Latitude (°N)", fontsize=24)ax.xaxis.set_major_formatter(FormatStrFormatter("%.2f"))ax.yaxis.set_major_formatter(FormatStrFormatter("%.2f"))xmin, xmax = df["lon"].min(), df["lon"].max()ymin, ymax = df["lat"].min(), df["lat"].max()x_pad = (xmax - xmin) * 0.25 if xmax > xmin else 0.01y_pad = (ymax - ymin) * 0.25 if ymax > ymin else 0.01ax.set_xlim(xmin - x_pad, xmax + x_pad)ax.set_ylim(ymin - y_pad, ymax + y_pad)ax.grid(False)plt.tight_layout()plt.savefig(out_png, dpi=300, bbox_inches="tight")plt.show()print(df)print(f"CSV saved: {out_csv}")print(f"Point SHP saved: {out_point_shp}")print(f"Segment SHP saved: {out_segment_shp}")print(f"Trajectory SHP saved: {out_trajectory_shp}")print(f"Figure saved: {out_png}")

三、结果

输出重心迁移轨迹图和相关shp及表格数据。

到时候就可以这么写了,基于重心迁移模型与标准差椭圆分析,2000—2025年黄河流域NDVI空间重心总体呈东北方向偏移,重心由(106.82°E, 36.09°N)迁移至(107.17°E, 36.41°N),累计迁移约57.38 km。同期,标准差椭圆长轴由542.33 km缩减至518.40 km,表明NDVI高值区趋于集聚;旋转角由14.71°增至17.22°,空间分布主方向呈逆时针偏转。

最新文章

随机文章