ArcPy 脚本:批量生成郑州市 1990-2019 年空间分析结果(核密度、热点、平均中心、标准差椭圆)

ArcPy 脚本:批量生成郑州市 1990-2019 年空间分析结果(核密度、热点、平均中心、标准差椭圆)

背景介绍

在城市研究中,我们常常需要分析多年数据的空间分布模式,比如建筑物高度在郑州市的聚集情况、热点区域变化、整体中心位置迁移以及分布方向趋势。如果每年手动在 ArcGIS 中运行四种空间分析工具,会非常耗时且容易出错。这个脚本就是为了解决这个问题而写的------它能自动批量处理 1990 到 2019 共 30 年的数据,一键生成四类分析结果,让你把精力放在结果解读上,而不是重复操作上。

代码功能说明

这个脚本的核心功能是:自动读取每一年郑州市的点要素数据(shp 文件),依次完成四种常见空间统计分析,并将结果整齐保存到对应的文件夹中

具体能实现的效果:

  • 核密度分析 → 生成每年建筑物高度的密度栅格图(tif)
  • 冷热点分析(Getis-Ord Gi*) → 生成每年显著的高值/低值聚集区域(shp)
  • 平均中心 → 计算每年高度加权的中心点(shp)
  • 标准差椭圆(方向分布) → 绘制每年数据的分布方向和离散程度(shp)

适用场景:城市地理、规划、人口、经济等时空格局研究,特别是需要对比多年变化的场景。

运行完成后,你会在输出文件夹里看到四个子文件夹(核密度、冷热点、平均中心、椭圆),每个文件夹里按年份命名整齐存放对应结果文件,总共约 120 个文件(30 年 × 4 类)。

运行环境准备

要运行这个脚本,你需要:

  • 安装 ArcGIS Pro (推荐 2.8 或更高版本)或 ArcGIS Desktop 10.x(带 Spatial Analyst 扩展)
  • 脚本使用的是 ArcGIS 自带的 Python 环境和 arcpy 模块,不需要额外用 pip 安装任何包
  • 确保你的 ArcGIS 许可证已启用 Spatial Analyst 扩展(脚本会自动 checkout)

注意:普通 Python(如 Anaconda)无法运行此脚本,必须在 ArcGIS 提供的 Python 环境中执行。

详细运行步骤

按照以下顺序操作,新手也能一步步跑通。每一步都说明了"为什么要这么做"。

  1. 准备输入数据

    为什么:脚本会按年份逐个读取 shapefile 文件,如果路径或文件名不对就会报错。

    操作:

    • 在你的磁盘上新建一个输入文件夹(例如 D:\CMAB_data\input
    • 在里面按年份建立子文件夹:19901991、...、2019
    • 将每年的点数据命名为 郑州市_XXXX.shp(XXXX 为年份),放入对应年份的子文件夹中
      (数据结构示例:输入文件夹\1990\郑州市_1990.shp
  2. 创建输出和临时文件夹

    为什么:脚本需要地方存放结果和临时文件,避免污染其他目录。

    操作:

    • 新建输出文件夹,例如 D:\CMAB_data\output\郑州
    • 新建临时文件夹(scratch),例如 D:\CMAB_data\scratch(空间足够大即可)
  3. 保存并修改代码

    为什么:路径必须指向你自己的文件夹,否则找不到文件。

    操作:

    • 将下面的完整代码复制到一个新文件中,保存为 zhengzhou_spatial_analysis.py
    • 重点修改以下三行路径(用你的实际路径替换方括号内的内容):
    python 复制代码
    input_base_dir = r"【你的输入基础目录】"    # 示例: r"D:\CMAB_data\input"
    output_base_dir = r"【你的输出基础目录】"   # 示例: r"D:\CMAB_data\output\郑州"
    scratch_dir = r"【你的临时目录】"            # 示例: r"D:\CMAB_data\scratch"
  4. 运行脚本

    为什么:在 ArcGIS 自带的 Python 环境中运行才能调用 arcpy。

    操作(两种方式任选其一):

    • 方式一(推荐) :打开 ArcGIS Pro → 顶部菜单"分析" → "Python" → 打开 Python 窗口 → 拖入你的 .py 文件 → 回车执行
    • 方式二 :在 Windows 开始菜单找到 "Python Command Prompt(ArcGIS Pro)" → 输入 python C:\path\to\zhengzhou_spatial_analysis.py
      运行过程会在控制台打印进度,大约几分钟到十几分钟(取决于电脑性能和数据量)。
  5. 验证结果

    为什么:确认脚本是否成功生成了所有文件。

    操作:

    • 运行结束后,控制台会打印统计信息(每个文件夹多少文件)
    • 打开输出文件夹,检查四个子文件夹是否都有按年份命名的文件
    • 用 ArcGIS Pro 打开任意几个结果文件查看是否正常显示

核心代码解析

下面是完整代码(已将个人路径模糊化)。我们用大白话逐段解释关键逻辑。

python 复制代码
import arcpy
import os
import shutil

def clean_previous_results(output_base_dir, sub_folders):
    """删除之前的结果文件"""
    print("正在清理之前的输出结果...")
  
    for folder_name in sub_folders.values():
        folder_path = os.path.join(output_base_dir, folder_name)
        if os.path.exists(folder_path):
            try:
                # 删除文件夹内所有内容,但保留文件夹本身
                for filename in os.listdir(folder_path):
                    file_path = os.path.join(folder_path, filename)
                    try:
                        if os.path.isfile(file_path) or os.path.islink(file_path):
                            os.unlink(file_path)
                        elif os.path.isdir(file_path):
                            shutil.rmtree(file_path)
                    except Exception as e:
                        print(f" 删除 {file_path} 失败: {e}")
                print(f" 已清理: {folder_name}")
            except Exception as e:
                print(f" 清理 {folder_name} 文件夹时出错: {e}")
        else:
            # 如果文件夹不存在,创建它
            os.makedirs(folder_path, exist_ok=True)
            print(f" 创建了: {folder_name}")

def run_analysis():
    # ================= 配置区域 =================
    input_base_dir = r"【你的输入基础目录】"
    output_base_dir = r"【你的输出基础目录】"
    scratch_dir = r"【你的临时目录】"
  
    arcpy.CheckOutExtension("Spatial")
    arcpy.env.overwriteOutput = True
  
    sub_folders = {
        "kde": "核密度",
        "hotspot": "冷热点",
        "mean_center": "平均中心",
        "ellipse": "椭圆"
    }
  
    # ================= 清理之前的输出结果 =================
    clean_previous_results(output_base_dir, sub_folders)
  
    # ================= 开始循环处理 (1990-2019) =================
    for year in range(1990, 2020):
        input_shp = os.path.join(input_base_dir, str(year), f"郑州市_{year}.shp")
      
        if not os.path.exists(input_shp):
            print(f"文件不存在,跳过: {input_shp}")
            continue
          
        print(f"\n正在处理: {year} 年数据...")
      
        try:
            # --- 任务1: 核密度 ---
            temp_point_path = os.path.join(scratch_dir, f"temp_pts_{year}.shp")
            arcpy.management.FeatureToPoint(input_shp, temp_point_path, "INSIDE")
          
            out_raster_path = os.path.join(output_base_dir, sub_folders["kde"], f"{year}年郑州市.tif")
            out_kde = arcpy.sa.KernelDensity(temp_point_path, "height", 30, None, "SQUARE_METERS")
            out_kde.save(out_raster_path)
            arcpy.management.Delete(temp_point_path)
            print(f" √ 任务1完成: 核密度")
            
            # --- 任务2: 热点分析 ---
            out_hotspot_path = os.path.join(output_base_dir, sub_folders["hotspot"], f"{year}年郑州市冷热点.shp")
          
            try:
                arcpy.stats.HotSpotAnalysis(
                    input_shp, "height", out_hotspot_path,
                    "FIXED_DISTANCE_BAND", "EUCLIDEAN_DISTANCE", "NONE",
                    None, None, "FEATURE_CLASS"
                )
            except AttributeError:
                try:
                    arcpy.stats.HotSpotAnalysis_GetisOrdGi(
                        input_shp, "height", out_hotspot_path,
                        "FIXED_DISTANCE_BAND", "EUCLIDEAN_DISTANCE", "NONE",
                        None, None, "FEATURE_CLASS"
                    )
                except AttributeError:
                    arcpy.HotSpots_stats(input_shp, "height", out_hotspot_path)
                  
            print(f" √ 任务2完成: 热点分析")
            
            # --- 任务3: 平均中心 ---
            out_center_path = os.path.join(output_base_dir, sub_folders["mean_center"], f"{year}郑州.shp")
            arcpy.stats.MeanCenter(input_shp, out_center_path, "height")
          
            arcpy.management.AddField(out_center_path, "年份", "TEXT", field_length=10)
            with arcpy.da.UpdateCursor(out_center_path, ["年份"]) as cursor:
                for row in cursor:
                    row[0] = str(year)
                    cursor.updateRow(row)
            print(f" √ 任务3完成: 平均中心 (已添加年份字段)")
            
            # --- 任务4: 方向分布 ---
            out_ellipse_path = os.path.join(output_base_dir, sub_folders["ellipse"], f"{year}郑州.shp")
            arcpy.stats.DirectionalDistribution(input_shp, out_ellipse_path, "1_STANDARD_DEVIATION", "height")
          
            arcpy.management.AddField(out_ellipse_path, "年份", "TEXT", field_length=10)
            with arcpy.da.UpdateCursor(out_ellipse_path, ["年份"]) as cursor:
                for row in cursor:
                    row[0] = str(year)
                    cursor.updateRow(row)
            print(f" √ 任务4完成: 标准差椭圆 (已添加年份字段)")
          
            print(f" ✓ {year} 年数据处理完成!")
          
        except Exception as e:
            print(f" ✗ 处理 {year} 年数据时发生错误: {str(e)}")
            import traceback
            traceback.print_exc()
            
    # ================= 最终统计 =================
    print("\n" + "="*50)
    print("所有处理已完成!")
    print("\n输出结果统计:")
    print("-"*30)
  
    total_files = 0
    for folder_key, folder_name in sub_folders.items():
        folder_path = os.path.join(output_base_dir, folder_name)
        if os.path.exists(folder_path):
            if folder_key == "kde":
                files = [f for f in os.listdir(folder_path) if f.endswith('.tif')]
            else:
                files = [f for f in os.listdir(folder_path) if f.endswith('.shp')]
          
            count = len(files)
            total_files += count
            print(f"{folder_name} 文件夹: {count} 个文件")
          
            if count > 0:
                print(f" 示例: {', '.join(files[:3])}" + ("..." if count > 3 else ""))
        else:
            print(f"{folder_name} 文件夹: 不存在")
  
    print(f"\n总计生成: {total_files} 个结果文件")
    print("="*50)

if __name__ == '__main__':
    run_analysis()

大白话解释核心逻辑

  • for year in range(1990, 2020):就像一个自动翻页的书,从 1990 年到 2019 年一张张处理。
  • 每一年先检查文件是否存在,不存在就跳过(避免程序直接崩溃)。
  • 核密度:先把面转成点(因为核密度工具需要点),然后计算"哪里点最密集",输出一张热力图一样的栅格。
  • 热点分析:直接告诉 ArcGIS "用高度字段找出显著的高值聚集和低值聚集区"。
  • 平均中心:计算所有点的高度加权中心,就像找一群人的"平均位置",但高楼影响更大。
  • 标准差椭圆:画一个椭圆包裹大部分点,显示整体分布的方向和范围。
  • 每次分析完都会在结果文件里加一个"年份"字段,方便后期合并所有年份一起分析。
  • 最后统一统计生成了多少文件,给你一个清晰的完成反馈。

常见问题解决

  • 报错 "RuntimeError: Cannot check out Spatial Analyst"

    原因:许可证未启用 Spatial Analyst 扩展。

    解决:在 ArcGIS Pro 中 → 项目 → 许可 → 确认 Spatial Analyst 已勾选。

  • 找不到输入文件,提示文件不存在

    原因:路径写错或文件名不完全匹配(区分大小写、后缀完整)。

    解决:仔细检查 input_base_dir 和文件实际位置,确保是原始字符串(加 r 前缀)。

  • 热点分析函数报 AttributeError

    原因:不同 ArcGIS 版本函数名不同。

    解决:脚本已内置多种尝试,通常能自动适配;若仍失败,请确认你的 ArcGIS 版本。

  • 磁盘空间不足或权限问题

    原因:输出/临时目录无写权限或空间不够。

    解决:选择有足够空间的磁盘,确保当前用户对文件夹有写权限。

  • 运行特别慢

    原因:数据量大或电脑配置一般。

    解决:可以先拿几年的数据测试(修改 range(1990, 1995)),确认没问题再跑全部。

按照上面步骤操作,新手也能顺利跑通并得到完整的多年空间分析结果。祝你分析愉快!