矢栅处理(1):分区统计sjoin|zonal

按照多边形(如格网/行政区划)边界汇总统计其内的点/栅格是空间分析中的常用操作(栅格实际上可以看做点数据)。

对于软件,如果点是矢量,则可以直接通过ArcMap/QGIS的spatial join计算,点为栅格,则称zonal statistic……实际中数据多即需要批处理,可以采用QGIS、ArcPy或者基于Python(如geopandas rasterstats等模块)……

具体的实现流程视数据情况而定,数据量大的时候可以考虑看能不能先分块再算或者多分几步,这里仅给出一些我的处理案例,数据量更大的可能就需要考虑进一步优化实现思路了。此外:ArcGIS的底层是32位貌似且Python还是2.7的,因此运行会更慢,当然现在慢慢迁移到ArcGIS Pro,但是Pro比较吃电脑配置很多操作我更加推荐开源的QGIS,一方面QGIS参数的控制和结果更加细致,且运行速度(Python3.6)和结果预览都更快!

∑栅格×矢量zonal

以统计格网(polygons)内栅格值为例:

Python

  1. ArcPy,实现Zonal Statistic功能,应该在空间分析工具下,可自行查看文档
  2. 一个专门用于做分区统计的模块rasterstats文档,较推荐,这里给一个简单的个人案例,统计区划内的高程信息。经过简单测试发现时间为50秒,而在QGIS中运行耗时150秒,ArcGIS理论上更慢,说明该工具还可以(毕竟底层C++)。
    • 传入统计的矢量数据或geometry类型+栅格图层文件路径即可,输出一个字典,可以直接转成df
    • 也可以自定义计算函数作为统计参数stats
    • 对于分类的栅格数据可以添加categorical = True参数
1
2
3
4
5
6
7
8
9
10
11
12
from rasterstats import zonal_stats
import pandas as pd
import geopandas as gpd
# 读取shape矢量文件为gpd,空间区域,也可以通过csv文件创建gpd
gdf = gpd.read_file("./青海西藏县.shp", encoding = 'utf-8')
tiffile = "A:/Data/dem.tif" #栅格文件
stats = zonal_stats(gdf,demfile,stats=['min', 'max', 'median', 'majority', 'sum'])
stats2 = zonal_stats("./青海西藏县.shp",demfile,stats='count')
# 统计的stats是一个字典,也可以输出geojson,参数geojson_out=True
# 这里我选择拼接两张表并导出统计的结果
res = pd.concat([gdf, pd.DataFrame(stats)], axis=1) #geodataframe
# 再视情况写出即可res.to_file(...)
  1. 其他暂不探索

软件

之前汇总过一次全国960w格网的栅格计算,分省计算的,基于的是QGIS的批处理(通过导入参数json文件实现)/也尝试了其Python脚本发现不好用,可能是我还没上手习惯

  • ArcGIS略

  • QGIS:在工具箱搜索zonal或者分区统计即可

QGIS批处理,QGIS的脚本不如ArcPy好用,所以批处理之前是用参数导入的方式实现的,但是其参数设置还需要经过一道转换,略显麻烦,但也还行,代码见:

参数设置代码参考,其要求比较严格需要标准的路径、json对象和json文件等,当时debug了好一会~,json的介绍可以我写过的这里,需要设置输入文件、输出文件、栅格波段、计算方式(数量、均值等)、前缀等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from glob import glob
import json
# infiles是输入的拆分好的960w格网文件
inpath = "A:/Data/ChinaGrid/grids_v1/shp"
infiles = glob(inpath+"/*.shp")
# 这里是为了把双反斜杠\\换成正斜杠/
infiles = [inpath+"/"+os.path.basename(file) for file in infiles]
# 全部输出文件,视情况改,我这里因为只换了文件夹
outfiles = [infile.replace('v1','v2') for infile in infiles]
paras = []
for i in range(len(outfiles)):
infile = json.dumps(infiles[i])
# outfile = json.dumps(outfiles[i])
# 输入栅格,此处我计算的是同一个栅格,如果多个另外传参数
inraster = json.dumps("A:/Data/ChinaGrid/base/china-gaia.tif")
prefix = json.dumps('imp_') # 前缀
# 这里搜一下qgis脚本 zonal的参数说明文档,或者先小文件跑看一下输出记录写的参数,1代表数量
statis = json.dumps([1])
para = {
"PARAMETERS":{
"INPUT":infile,
"INPUT_RASTER":inraster,
"RASTER_BAND":'1',
"COLUMN_PREFIX": prefix,
"STATISTICS": statis},
"OUTPUTS": {"OUTPUT": outfiles[i]}
}
paras.append(para)
print(paras[0])# 预览一下
with open("A:\Data\ChinaGrid\zonal_batch.json",'w',encoding='utf-8') as f:
json.dump(paras,f)

∑点×矢量sjoin

以统计格网(polygons)内点的数量为例,比如我这里统计8w个0.05°格网内微博签到点的数量:

软件

  • ArcMap:右击多边形直接join汇总点,或者通过ArcToolbox~Tools.tbxJoin
  • QGIS:按位置连接,在QGIS工具箱搜索join或者连接都行,其工具检索双语支持,也可以批处理

Python

  1. 一种是基于ArcPy实现Spatial Join工具的脚本即可,查看tool help的代码即可,支持批处理,略
  2. 使用geopandas sjoin,但是没法直接一步到位,以下是我的处理,先sjoin在merge,剔除无关字段

我这里需要按月统计所有格网内的微博点数量,列名等参数可以自行更改,count()也可以换成求和或求值等,或者同时计算更多的类型使用pivot table,此处最后的结果是:在原有的shapefile表上增加了12列,每一列对应当月格网内微博点的数量

主要是以下几步:空间连接→计数→连接到polygon表

1
2
3
table_sjoin = gpd.sjoin(points_table, polygons[['gid', 'geometry']])
table_df = pd.DataFrame(table_sjoin.groupby('gid')['_id'].count())
polygons = pd.merge(left = polygons,right=table_df, on='gid', how='left').fillna(0)

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
outdir = r"A:\my_research\dataset\wb1720\2017m"
# grids是我的格网文件,wb2017是全年的微博
polygons = grids.copy(deep=True)
for month, table in wb2017.groupby('month'):
outfile = outdir+"\\m"+str(month)+".shp"
# 按月输出微博文件,也可以忽略
table.to_file(outfile, encoding='utf-8',index=False)
# 统计数量,按照格网id gid。_id为微博的唯一di用于计数
# 如果是某列求和则更改groupby函数及count()为其他函数即可
table_sjoin = gpd.sjoin(table, polygons[['gid', 'geometry']])
table_df = pd.DataFrame(table_sjoin.groupby('gid')['_id'].count())
# 调整列名
table_df = table_df.reset_index()
new_name = "count_m"+str(month)
table_df = table_df.rename(columns={'_id':new_name})
# 连接表格
polygons = pd.merge(left = polygons,right=table_df, on='gid', how='left').fillna(0)
polygons[new_name] = polygons[new_name].astype('int32')

print(month)
polygons.to_file("./data/results.shp", encoding = 'utf-8')