简介:本资源是面向GIS从业者与空间分析学习者的ArcGIS工具箱,用于在ArcGIS中快速生成正六边形渔网(蜂窝图),解决传统渔网工具只能生成矩形网格、难以满足蜂窝状采样与空间统计需求的问题。压缩包共83个文件,约889KB,包含tbx工具箱、lyr图层文件、mxd地图文档、py脚本以及配套的gdb地理数据库文件(gdbtable、gdbtablx、gdbindexes、atx索引等),结构完整,解压后可直接加载使用。使用时只需双击工具箱,输入研究区范围与正六边形边长即可完成生成,支持ArcGIS 10.2及以上版本,永久可用。目前已有363人学习下载,适合从事城市分析、生态评估、格网采样等工作的初、中级用户,可帮助快速构建蜂窝状空间单元,提升空间统计与制图效率。
1. 六边形渔网为什么比方格网更适合做空间聚合
做过城市POI聚合、格网统计或者生态样方采样的同行,大概率都遇到过同一个尴尬:用 ArcGIS 自带的 Create Fishnet 生成的是正方形格网,可一旦把点数据往里一丢,边缘效应立刻显现——同一个商圈被切成四块,相邻格子的统计值忽高忽低,做热力图时还会出现明显的棋盘状伪影。六边形渔网(蜂窝图)就是冲着这个问题来的。正六边形在平面上等距铺展时,每个单元到相邻单元的中心距离完全一致,没有正方形那种「对角邻居比正交邻居远 1.414 倍」的各向异性,所以做密度、可达性、邻域平滑时更接近真实的空间连续场。ArcGIS 本身没有一键生成蜂窝图的按钮,但用 fishnet 加几何变换、或者直接走 ArcPy 脚本,都能把它做成一个可复用的工具箱。这篇就按我实际交付过的做法,把工具箱从参数设计到脚本落地讲透,适合已经会用 ArcMap 或 ArcGIS Pro 基础工具、想把它固化成批量生产能力的从业者。
2. 蜂窝渔网工具箱的三种实现路线与选型
2.1 先想清楚:你要的是「视觉蜂窝」还是「统计单元」
很多人第一次做六边形渔网,是被底图上的蜂窝效果吸引,但真正决定实现方式的,是用途。如果只是出图好看,用符号系统把正方形格网渲染成六边形标记就行,数据本身还是方的;但如果你要做空间聚合、邻域分析、格网统计,那单元几何必须真的是正六边形,否则后续的 Spatial Join、Zonal Statistics 全都会带着方格网的偏差。
我一般把需求分成三类:第一类是纯可视化,第二类是点/面数据的聚合统计,第三类是作为采样单元或分析单元参与建模。第一类不碰几何,第二、三类必须生成真实六边形面。工具箱要覆盖的是后两类,因为第一类用不着工具箱。
真实六边形的生成,在 ArcGIS 体系里有三条常见路线,各有适用边界。
第一条是「Fishnet + 几何旋转」。先用 Create Fishnet 生成一个覆盖范围的正方形格网,然后对每个格子做旋转和偏移,拼出六边形。这条路的问题在于,正方形格网的行列间距和六边形的边长之间存在 √3 关系,手工调参很容易错位,而且旋转后的面之间会有缝隙或重叠,做叠加分析时会出现大量 sliver 多边形。除非你只是要个大概形状,否则不推荐。
第二条是「ArcPy 直接构造六边形几何」。用 arcpy.Polygon 按数学公式逐个生成六边形的顶点环,再写入要素类。这条路最干净,几何精度完全可控,不依赖任何中间格网,也不会有缝隙。缺点是脚本要自己写顶点计算,对不熟悉六边形几何的人有点门槛。工具箱的核心价值就在这里——把公式封装好,用户只填范围和边长。
第三条是「借助第三方库生成后导入」。比如用 Python 的 h3 库或者 shapely 生成六边形,再通过 arcpy 转成要素类。h3 是 Uber 开源的六边形层级索引,适合做全球尺度的聚合,但它的六边形是球面投影下的,和 ArcGIS 平面坐标系的六边形不完全一致,做本地高精度分析时会有形变。shapely 生成的是平面六边形,导入 ArcGIS 没问题,但多了一层依赖,部署到别人的机器上容易翻车。
我最终选的是第二条路线,纯 ArcPy 构造。理由很简单:不引入外部依赖,几何可控,参数少,容易做成工具箱里的脚本工具。下面把这条路线拆开讲。
2.2 六边形几何的数学关系:边长、行距、列距怎么定
在动手写脚本之前,必须把六边形的铺展参数搞清楚,否则生成出来的蜂窝要么重叠,要么有缝。正六边形有两种常见朝向:尖顶朝上(pointy-top)和平顶朝上(flat-top)。ArcGIS 里做渔网,我一般用平顶朝上,因为它的行是水平的,和常规地图的阅读习惯一致。
平顶六边形的关键参数是边长 s(也就是外接圆半径)。它的宽度(左右顶点之间的距离)是 2s,高度(上下边之间的距离)是 √3·s。当多个六边形铺展时,同一行内相邻六边形的中心水平距离是 1.5s,相邻两行的中心垂直距离是 √3·s,而奇数行要水平偏移 1.5s 的一半,也就是 0.75s。这三个数字——1.5s、√3·s、0.75s——是整个生成逻辑的骨架,记错了就会错位。
举个具体例子:如果边长 s = 100 米,那么同一行相邻中心距 150 米,行距约 173.2 米,奇数行偏移 75 米。覆盖一个 1 公里 × 1 公里的范围,大约需要 7 列 × 6 行,总共 40 多个六边形。这个量级用脚本生成是毫秒级的,不用担心性能。
提示:如果你的分析范围跨越多个 UTM 带,建议先投影到以范围中心为原点的自定义投影,否则六边形在不同纬度上的实际面积会有差异。
2.3 用 ArcPy 构造六边形要素类的完整脚本
下面是我工具箱里实际用的核心脚本,输入是范围要素类或四个坐标值,输出是六边形面要素类。脚本里所有关键参数都有注释,直接改 s 和范围就能跑。
# -*- coding: utf-8 -*- import arcpy import math import os def create_hex_fishnet(out_fc, extent, side_length, spatial_ref): """ 生成平顶六边形渔网 out_fc: 输出要素类路径 extent: (xmin, ymin, xmax, ymax) side_length: 六边形边长(地图单位) spatial_ref: 空间参考对象 """ xmin, ymin, xmax, ymax = extent s = float(side_length) # 平顶六边形的铺展参数 col_step = 1.5 * s # 同行相邻中心水平距离 row_step = math.sqrt(3) * s # 相邻行中心垂直距离 odd_offset = 0.75 * s # 奇数行水平偏移 # 计算需要多少行多少列,多生成一圈保证覆盖完整 n_cols = int(math.ceil((xmax - xmin) / col_step)) + 2 n_rows = int(math.ceil((ymax - ymin) / row_step)) + 2 # 创建要素类 out_path, out_name = os.path.split(out_fc) if arcpy.Exists(out_fc): arcpy.Delete_management(out_fc) arcpy.CreateFeatureclass_management( out_path, out_name, "POLYGON", spatial_reference=spatial_ref ) # 六边形顶点相对中心的偏移(平顶,从最右顶点开始逆时针) angles = [0, 60, 120, 180, 240, 300] offsets = [(s * math.cos(math.radians(a)), s * math.sin(math.radians(a))) for a in angles] # 逐行逐列生成 with arcpy.da.InsertCursor(out_fc, ["SHAPE@"]) as cursor: for row in range(n_rows): cy = ymin + row * row_step # 奇数行偏移半个列距 x_offset = odd_offset if row % 2 == 1 else 0 for col in range(n_cols): cx = xmin + col * col_step + x_offset # 跳过完全在范围外的六边形 if cx + s < xmin or cx - s > xmax: continue if cy + s < ymin or cy - s > ymax: continue # 构造六边形顶点数组 ring = [arcpy.Point(cx + dx, cy + dy) for dx, dy in offsets] ring.append(ring[0]) # 闭合 polygon = arcpy.Polygon(arcpy.Array(ring), spatial_ref) cursor.insertRow([polygon]) # 添加中心点坐标字段,方便后续聚合 arcpy.AddField_management(out_fc, "CX", "DOUBLE") arcpy.AddField_management(out_fc, "CY", "DOUBLE") with arcpy.da.UpdateCursor(out_fc, ["SHAPE@", "CX", "CY"]) as cursor: for row in cursor: centroid = row[0].centroid row[1] = centroid.X row[2] = centroid.Y cursor.updateRow(row) return out_fc这段脚本的逻辑分四步。第一步是参数换算,把边长 s 转成列距、行距和偏移量,这三个值决定了蜂窝的铺展方式。第二步是计算行列数,多生成一圈是为了保证边缘的六边形能完整覆盖范围,后续可以用 Select by Location 裁掉多余的。第三步是逐行逐列构造顶点环,平顶六边形的六个顶点角度是 0、60、120、180、240、300 度,从最右顶点开始逆时针走一圈,最后闭合。第四步是补充中心点坐标字段,这个字段在做 Spatial Join 或者生成热力图时非常有用,省得每次都重新算质心。
参数方面,side_length 是最核心的,它决定了六边形的粒度。做城市商圈分析,一般取 200 到 500 米;做生态样方,可能取 1 公里以上;做全国尺度的聚合,边长可能到几十公里。范围 extent 建议比实际分析范围大一圈,避免边缘六边形被裁得只剩一半。spatial_ref 一定要用投影坐标系,地理坐标系下六边形的实际面积会随纬度变化,做统计时会有偏差。
2.4 把脚本封装成 ArcGIS 工具箱的步骤
脚本能跑通之后,下一步是把它做成工具箱,让别人不用看代码也能用。ArcGIS 里封装脚本工具的标准流程是:新建工具箱 → 添加脚本 → 设置参数 → 关联脚本文件。具体操作如下。
第一步,在 ArcCatalog 或 ArcGIS Pro 的 Catalog 面板里,右键目标文件夹,新建一个 Toolbox,命名为「HexFishnetToolbox.tbx」。
第二步,右键工具箱,选择 Add Script,弹出向导。名称填「生成六边形渔网」,标签和描述按需填,脚本文件选上面保存的 .py 文件。
第三步,设置参数。这是最关键的一步,参数顺序和类型必须和脚本里的接收顺序一致。我一般设四个参数:输出要素类(Output Feature Class,类型 Feature Class)、分析范围(Extent,类型 Extent)、六边形边长(Double)、空间参考(Spatial Reference,类型 Spatial Reference)。每个参数都要设方向为 Input 或 Output,输出要素类设为 Output,其余为 Input。
第四步,在脚本文件的末尾加上参数接收代码,把工具箱传进来的参数转成函数调用。这段代码不能少,否则工具跑起来会报参数数量不匹配。
if __name__ == "__main__": out_fc = arcpy.GetParameterAsText(0) extent_str = arcpy.GetParameterAsText(1) side_length = float(arcpy.GetParameterAsText(2)) spatial_ref = arcpy.GetParameterAsText(3) # 解析 Extent 参数,格式为 "xmin ymin xmax ymax" extent = tuple(float(v) for v in extent_str.split()) sr = arcpy.SpatialReference() sr.loadFromString(spatial_ref) create_hex_fishnet(out_fc, extent, side_length, sr) arcpy.AddMessage("六边形渔网生成完成:{}".format(out_fc))参数说明:GetParameterAsText 按索引取工具箱里的参数,索引从 0 开始,顺序必须和向导里设置的一致。Extent 参数在工具箱里传进来是字符串,格式是「xmin ymin xmax ymax」,用 split 拆开后转 float。SpatialReference 参数传进来是 WKT 字符串,用 loadFromString 还原成空间参考对象。最后用 AddMessage 输出提示,方便在工具运行窗口看到进度。
封装好之后,这个工具箱可以拷贝给同事,只要他们的 ArcGIS 版本支持 ArcPy,双击就能用,不需要装任何额外依赖。
3. 蜂窝渔网在空间聚合中的参数调优与性能边界
3.1 边长怎么选:从分析尺度反推
边长是六边形渔网最重要的参数,没有之一。选大了,聚合后的单元太少,空间异质性被抹平;选小了,单元太多,统计噪声大,而且要素数量爆炸。我一般用「分析尺度的 1/10 到 1/5」作为初始值。比如研究一个 5 公里的商圈,边长取 500 米到 1 公里;研究一个 50 公里的城市群,边长取 5 到 10 公里。
还有一个经验法则:让每个六边形里平均有 30 到 50 个样本点。如果做 POI 密度分析,先数一下总点数,除以期望的单元数,再反推边长。比如 10 万个 POI,想要 2000 个单元,每个单元 50 个点,那么单元面积约等于研究区面积除以 2000,六边形面积公式是 (3√3/2)·s²,反解出 s 就行。
注意:边长一旦确定,后续所有分析都基于这个粒度,改边长意味着全部重做。建议在正式跑之前,用几个候选边长各生成一版,叠加原始点数据看一眼分布,再定稿。
3.2 用 Spatial Join 做点聚合的两种匹配方式
六边形生成之后,最常见的操作是把点数据聚合到六边形里。ArcGIS 里用 Spatial Join 工具,目标要素选六边形,连接要素选点,匹配选项选 CONTAINS 或 INTERSECT。CONTAINS 要求点完全落在六边形内部,INTERSECT 则允许点在边界上也算。我一般用 CONTAINS,因为六边形之间没有缝隙,边界上的点归到哪一边对整体统计影响很小,用 CONTAINS 更干净。
Join 之后,输出要素类会带一个 Join_Count 字段,就是每个六边形里的点数。如果要算密度,再除以六边形面积。面积可以用 Add Geometry Attributes 工具算,选 AREA 字段。注意单位,投影坐标系下算出来是平方米,要换算成平方公里就除以 10^6。
如果点数据量很大,比如几百万个点,Spatial Join 可能会跑很久。这时候可以先用「Generate Near Table」或者「Point Distance」做空间索引,但更简单的办法是给点数据建空间索引,然后在 Spatial Join 的环境设置里把输出坐标系设成和六边形一致,减少投影转换的开销。
3.3 大数据量下的分块生成与内存控制
六边形渔网本身生成很快,但如果研究区很大、边长很小,要素数量会非常可观。比如覆盖一个省,边长 100 米,六边形数量可能上千万,这时候一次性生成会吃光内存。我的做法是分块生成,把研究区切成若干个子区域,每个子区域单独生成六边形,最后用 Merge 合并。
分块的关键是块与块之间要有重叠,重叠宽度至少一个六边形的宽度,否则接缝处的六边形会被切掉。合并之后,用 Dissolve 或者 Delete Identical 去掉重复的六边形。重复判断可以用中心点坐标,因为每个六边形的中心点是唯一的。
另一个内存控制技巧是关闭不必要的字段。生成六边形时只保留 SHAPE 和中心点坐标,不要一上来就加一堆属性字段。等聚合完成后再按需添加,能省不少内存。
4. 六边形渔网落地时的避坑与排查
4.1 生成的六边形之间有缝隙或重叠
现象:把六边形放大看,相邻单元之间要么有一条细缝,要么边缘叠在一起。
原因:列距、行距或奇数行偏移量算错了。最常见的是把平顶六边形的参数用成了尖顶的,或者把 √3 写成了 1.732 但实际用了 1.73 导致累积误差。
解决:严格按平顶六边形的三个参数来——列距 1.5s、行距 √3·s、奇数行偏移 0.75s。如果还是有缝,检查空间参考是不是地理坐标系,地理坐标系下六边形的实际形状会随纬度变形,必须投影。
4.2 Spatial Join 之后 Join_Count 全是 0
现象:六边形生成没问题,但和点数据做 Spatial Join 后,每个六边形的 Join_Count 都是 0。
原因:两个数据的空间参考不一致,或者点的坐标系是地理坐标系而六边形是投影坐标系,ArcGIS 在 Join 时没有自动转换。
解决:在 Spatial Join 的环境设置里,把 Output Coordinate System 设成和六边形一致,或者先用 Project 工具把点数据投影到和六边形相同的坐标系。另外检查一下点的 SHAPE 字段是否有效,有时候从 CSV 导入的点没有正确生成几何。
4.3 工具箱在别人机器上跑报「找不到 arcpy」
现象:自己机器上跑得好好的工具箱,拷给同事后双击报错,提示 No module named arcpy。
原因:对方的 ArcGIS 没有安装 Python 环境,或者用的是 ArcGIS Pro 但 Python 环境没配好。ArcPy 是 ArcGIS 自带的,不是独立安装的。
解决:确认对方装的是完整版 ArcGIS Desktop 或 ArcGIS Pro,并且 Python 环境已经初始化。ArcGIS Pro 里要在「工程 → 环境 → Python」里确认 arcpy 可用。如果对方用的是 ArcGIS Pro 但脚本工具关联的是 Python 2 的脚本,也会报错,需要把脚本改成 Python 3 语法。
4.4 六边形数量太多导致 ArcGIS 卡死
现象:生成六边形后,地图一缩放就卡,或者 Spatial Join 跑到一半 ArcGIS 无响应。
原因:要素数量超过了 ArcGIS 的渲染和内存承受能力。单个要素类超过百万级要素时,ArcGIS 的默认渲染会非常吃力。
解决:分块生成,或者先用 Select by Attribute 按区域筛选,只加载当前视图范围内的六边形。另外在图层属性里关闭不必要的标注和符号化,用简单填充代替复杂样式。如果只是做统计,不需要在地图上显示全部六边形,可以在跑完 Spatial Join 后再导出结果表。
4.5 边缘六边形被裁得不完整
现象:研究区边缘的六边形只有一半甚至更少,做统计时这些单元的面积和内部单元不一致,导致密度值偏低。
原因:生成时范围没留够余量,边缘六边形被范围边界切掉了。
解决:生成时把 extent 向外扩一圈,至少扩一个六边形的宽度。生成后用 Select by Location 把完全落在研究区内的六边形选出来,或者用 Clip 裁切,但裁切后要重新算面积,不能直接用原始边长算。
5. 把蜂窝渔网接进自动化流程的一个技巧
工具箱能手动跑之后,真正提升效率的是把它接进自动化流程。我自己的习惯是,把六边形生成、Spatial Join、密度计算、符号化这四步串成一个 ArcPy 脚本,用 Python 的 subprocess 或者 ArcGIS Pro 的 Task 来调度。这样每次有新数据进来,改一下输入路径就能一键出图。
这里分享一个具体技巧:用六边形的中心点坐标做唯一标识,而不是用 OBJECTID。因为 OBJECTID 在数据合并、导出后会变,但中心点坐标是稳定的。在生成六边形时就把 CX、CY 字段算好,后续所有关联、去重、更新都用这两个字段做 key。我吃过这个亏——有一次用 OBJECTID 关联,结果数据重新导出后 ID 全变了,整个聚合结果对不上,排查了半天才发现是 ID 的问题。从那以后,凡是做空间聚合,我都用坐标做稳定标识。
另外,如果你经常需要换边长重新生成,可以把边长做成脚本参数,用循环批量跑几个候选值,每个值生成一版六边形和对应的聚合结果,最后用图表对比不同粒度下的密度分布,选最合适的那个。这个做法在写分析报告时特别有用,能直接回答「为什么选这个粒度」的问题。
希望帮到你。
本文还有配套的精品资源,点击获取