中国人口密度公里格网栅格数据:从读取、分区统计到暴露度分析的完整避坑指南

发布时间:2026/10/27 17:35:02
中国人口密度公里格网栅格数据:从读取、分区统计到暴露度分析的完整避坑指南

简介:这份《中国人口密度公里格网栅格数据》面向地理信息系统从业者、城市规划人员、社会科学研究者及高校师生,用于开展人口分布的空间分析与可视化研究。资源以1平方公里格网为单位组织人口密度信息,可直接导入ArcGIS、QGIS等软件,支撑城市规划、公共服务布局、生态承载力评估及人口迁移与城市化趋势研究等场景。压缩包共240个文件,约60.58MB,以shp、dbf、prj、shx、cpg等Shapefile配套文件为主,辅以adf栅格文件与xml元数据,覆盖矢量边界、属性表、投影信息与栅格像元值,便于分层管理与空间叠加分析。目前已有2732人学习下载。数据经过清洗、标准化与格式转换,读者可据此快速完成人口密集区与稀疏区的识别、专题地图渲染及空间查询建模,为政策制定与学术研究提供可靠的数据支撑。

1. 中国人口密度公里格网栅格数据:从压缩包到可分析图层的完整路径

拿到一个名为「中国人口密度公里格网栅格数据.zip」的压缩包,很多人第一反应是解压、拖进 GIS 软件、出图,然后发现坐标系对不上、值域全是 0 到几万、沿海亮成一片而西部几乎全黑,根本没法直接做城乡对比或选址分析。这个数据集本质上是一套 1km×1km 分辨率的全国人口空间分布栅格,每个像元的值代表该平方公里内的人口数或人口密度,常见来源包括 WorldPop、GPW、LandScan 以及国内一些课题组发布的格网化成果。它能解决的问题很具体:把行政区级别的人口统计拆到公里尺度,用于站点选址、灾害影响人口估算、商业热力分析、遥感与人口耦合建模。适合谁?做空间分析、城市规划、应急评估、遥感应用的一线从业者,以及需要把人口变量落到栅格上跑模型的研究生和工程师。这一章不展开操作,先把「这个包到底是什么、能干什么、边界在哪」讲清楚,后面几章再一步步拆解怎么用、怎么避坑。

2. 先搞懂公里格网人口数据的结构与选型逻辑

2.1 公里格网栅格的像元值到底代表什么

「公里格网」通常指 1km 空间分辨率的规则栅格,地理坐标系多为 WGS84(EPSG:4326),投影坐标系常见 Albers 等积投影或 Web Mercator。像元值有两种主流表达:一种是人口总数(每个像元内有多少人),另一种是人口密度(人/平方公里)。这两者在 1km 格网下数值恰好接近,但含义不同。如果你拿到的数据像元值范围在 0 到几万之间,大概率是人口总数;如果范围在 0 到几千甚至上万,且沿海城市核心区特别高,可能是密度。判断方法很简单:用统计工具看最大值和直方图,再对照已知城市的人口量级。比如上海某核心区 1km 格网人口总数可能在 3 万到 5 万,密度也是同一量级,但如果你要做区域汇总,必须确认单位,否则汇总结果会差几个数量级。

常见数据源各有侧重:WorldPop 提供逐年、多年龄性别分组,适合做动态分析;GPW 版本稳定、全球覆盖,适合做长时序对比;LandScan 利用遥感与夜间灯光等辅助数据,对城市内部异质性刻画更细。国内也有基于普查数据反演的公里格网产品,精度在东部较高、西部较低。选型时先问自己:要的是总数还是密度?要单年还是多年?要全国还是分省?答案不同,选的包也不同。压缩包名字里只写了「中国人口密度公里格网栅格数据」,没有版本和年份,解压后第一件事就是看元数据文件或文件名里的年份标识。

2.2 坐标系与投影:为什么直接叠加会错位

很多新手把栅格拖进 QGIS 或 ArcGIS,再叠加一个行政区边界,发现边界和栅格对不上,偏移几百米甚至几公里。原因通常是坐标系不一致。WGS84 地理坐标系下,1km 格网在经度方向的实际地面距离随纬度变化,直接量算面积会失真。正确做法是:如果做全国尺度统计,先投影到等积投影(如 Albers Equal Area,中央经线 105°E,双标准纬线 25°N 和 47°N),再按格网汇总;如果只是出图展示,用 Web Mercator 也可以,但不要用它做面积计算。另一个常见问题是栅格原点(左上角坐标)和行政区边界的对齐方式,不同数据源可能采用角点对齐或中心点对齐,叠加时会出现半个像元的偏移。解决办法是统一重采样到同一网格,或者用「按掩膜提取」时设置对齐参数。

提示:拿到任何格网数据,先看.prj文件或配套的.xml元数据,确认坐标系、像元大小、左上角坐标和无数据值,再动手分析。

2.3 用 Python 读取并快速体检数据

下面这段代码用rasterio读取栅格,输出基本信息并画直方图。这是每次拿到新数据的第一步,能帮你判断像元值含义、有无异常值、无数据值设成了多少。

import rasterio import numpy as np import matplotlib.pyplot as plt # 打开栅格文件,替换为你的实际路径 path = "china_pop_1km.tif" with rasterio.open(path) as src: print("坐标系:", src.crs) print("像元大小:", src.res) print("行列数:", src.height, src.width) print("无数据值:", src.nodata) data = src.read(1) # 读第一波段 # 把无数据值替换为 nan if src.nodata is not None: data = data.astype(float) data[data == src.nodata] = np.nan print("最小值:", np.nanmin(data)) print("最大值:", np.nanmax(data)) print("均值:", np.nanmean(data)) # 画直方图,去掉 nan valid = data[~np.isnan(data)] plt.hist(valid, bins=100) plt.xlabel("像元值") plt.ylabel("频数") plt.title("人口格网值分布") plt.show()

逻辑说明:rasterio.open读取元数据,src.read(1)读第一波段。无数据值必须显式替换为nan,否则统计会被拉偏。直方图能直观看出分布形态:如果绝大多数像元集中在 0 附近,少数极高值,说明是典型的人口稀疏分布,后续做回归或聚类时可能需要对数变换。参数方面,bins=100可根据数据量调整,数据量大时用 200 到 500 更细。如果最大值异常大(比如超过 100 万),检查是否把无数据值设成了 0 或 -9999 但没处理。

2.4 重采样与裁剪:把全国数据切到研究区

全国 1km 栅格大约有几十万行乘几十万列,直接全图加载会吃满内存。常见做法是先按研究区边界裁剪,再重采样到目标分辨率。裁剪用rasterio.mask,重采样用rasterio.warp或gdalwarp。下面给出裁剪的代码示例。

import geopandas as gpd import rasterio from rasterio.mask import mask # 读取研究区边界,例如某个市的 shp gdf = gpd.read_file("study_area.shp") # 确保边界和栅格坐标系一致,不一致先 to_crs with rasterio.open("china_pop_1km.tif") as src: gdf = gdf.to_crs(src.crs) geoms = [geom for geom in gdf.geometry] out_image, out_transform = mask(src, geoms, crop=True) out_meta = src.meta.copy() out_meta.update({ "height": out_image.shape[1], "width": out_image.shape[2], "transform": out_transform }) with rasterio.open("study_area_pop.tif", "w", **out_meta) as dest: dest.write(out_image, 1)

逻辑说明:mask函数按几何边界裁剪,crop=True会缩小输出范围。out_meta继承原元数据并更新行列数和变换参数。注意边界 shp 的坐标系必须和栅格一致,否则to_crs转换后再裁剪。如果研究区跨多个投影带,建议先统一到等积投影再裁剪。参数上,crop=True是必须的,否则输出还是全国范围。裁剪后文件大小会大幅下降,后续分析更快。

3. 从像元值到业务指标:统计、分区与可视化

3.1 按行政区汇总人口:分区统计的三种实现

拿到公里格网后,最常见的需求是「按省/市/县汇总人口」。栅格转矢量再汇总是一种方式,但更高效的是用分区统计。QGIS 里有「分区统计」工具,Python 里可以用rasterstats。下面用rasterstats做按行政区求和。

import geopandas as gpd from rasterstats import zonal_stats # 读取行政区边界 gdf = gpd.read_file("counties.shp") # 分区统计,categorical=False 表示数值型求和 stats = zonal_stats(gdf, "china_pop_1km.tif", stats=["sum", "mean", "max"], nodata=-9999) # 把结果写回 GeoDataFrame gdf["pop_sum"] = [s["sum"] for s in stats] gdf["pop_mean"] = [s["mean"] for s in stats] gdf.to_file("counties_pop.shp")

逻辑说明:zonal_stats对每个几何对象内的像元做统计,stats指定需要的指标。nodata参数要和栅格的无数据值一致,否则会把无数据当 0 参与求和。如果行政区边界和栅格坐标系不一致,zonal_stats内部会做投影转换,但最好提前统一。参数上,sum得到总人口,mean得到平均密度,max得到最密集的 1km 格网人口。注意:如果栅格是密度而非总数,sum得到的是「密度之和」,没有直接意义,需要乘以像元面积(1km²)才近似总数。

3.2 人口密度分级与制图:让地图会说话

直接展示原始像元值,地图会一片糊。常见做法是分级设色,分级方法有等间距、分位数、自然断点(Jenks)。人口数据强烈右偏,等间距会让绝大多数区域落在最低一级,看不出差异。推荐用分位数或自然断点。下面用mapclassify做自然断点分级,并出图。

import rasterio import numpy as np import matplotlib.pyplot as plt from mapclassify import NaturalBreaks with rasterio.open("china_pop_1km.tif") as src: data = src.read(1).astype(float) data[data == src.nodata] = np.nan valid = data[~np.isnan(data)] # 自然断点分成 7 级 nb = NaturalBreaks(valid, k=7) print("分级边界:", nb.bins) # 按分级边界离散化 classified = np.digitize(data, nb.bins) classified[np.isnan(data)] = 0 plt.imshow(classified, cmap="YlOrRd") plt.colorbar(label="人口密度等级") plt.title("中国人口密度公里格网分级图") plt.show()

逻辑说明:NaturalBreaks根据数据分布找最优分割点,k=7是常用级数。np.digitize把连续值映射到等级。出图时用YlOrRd色带,低值浅黄、高值深红,符合人口密度直觉。参数上,k可以根据需要调整,5 到 9 都常见。如果数据量太大,先抽样再算断点,否则计算慢。注意classified中 0 代表无数据或最低级,出图时把 0 设为透明或白色。

3.3 与遥感、POI 数据叠加的注意事项

人口格网常和夜间灯光、NDVI、POI 密度做叠加分析。这里最大的坑是空间分辨率不匹配。夜间灯光常见 500m 或 1km,NDVI 可能 250m 或 30m,POI 是点数据。叠加前统一到同一网格,用最近邻或双线性重采样。人口数据不适合双线性,因为会平滑掉城市内部差异,最近邻更保真。POI 点转栅格时,用核密度估计再重采样到 1km,比直接计数更合理。另外,时间年份要对齐,人口数据是 2020 年,灯光是 2013 年,叠加分析结论会不可靠。

注意:不同来源的人口格网对同一地区的估计可能差 20% 以上,尤其是西部县域。做跨源对比时,先在同一组行政区上汇总,看总量差异,再决定是否融合。

4. 避坑与排查:公里格网人口数据最常见的五个翻车现场

4.1 像元值全是 0 或异常大

现象:打开栅格,大片区域值为 0,或者最大值达到几百万。原因:无数据值被设成了 0,或者单位是「人/平方公里」但误当总数汇总。解决:先看元数据里的nodata,用rasterio读取时替换为nan;再看直方图,如果 0 值占比超过 60% 且集中在西部,可能是真实稀疏,也可能是无数据。对照行政区边界,如果边界内也全是 0,基本是无数据。最大值异常时,检查是否把无数据值 -9999 当有效值统计了。

4.2 坐标系不一致导致面积算错

现象:按行政区汇总的人口和统计年鉴对不上,偏差超过 30%。原因:栅格是地理坐标系,直接算面积时用了度数而非米,或者投影不适合等积。解决:先投影到 Albers 等积投影,再算面积和汇总。QGIS 里用「重新投影图层」,Python 里用gdalwarp -t_srs EPSG:...。确认投影参数:中央经线 105°E,标准纬线 25°N 和 47°N,这是全国尺度常用参数。

4.3 裁剪后像元值变了

现象:裁剪前后同一位置的值不一致。原因:裁剪时用了双线性重采样,改变了像元值。解决:裁剪用最近邻,保持原始值不变。rasterio.mask默认不重采样,但如果你先重采样再裁剪,值就变了。正确顺序是先裁剪再按需重采样,且重采样方法选最近邻。

4.4 分区统计把无数据当 0 求和

现象:某些行政区人口汇总明显偏低。原因:zonal_stats的nodata参数没设,或者栅格无数据值是 0 但实际是缺失。解决:显式设置nodata,并在统计前把无数据替换为nan。如果无数据值就是 0,需要结合边界判断:边界内 0 值像元是否合理,不合理则视为缺失。

4.5 出图时高值被低值淹没

现象:地图上只有沿海几个红点,内陆全黑。原因:用了等间距分级,人口右偏分布导致高值拉大区间。解决:改用分位数或自然断点分级,或者对数据做对数变换后再出图。对数变换后,1 到 10 和 1000 到 10000 的差异都能显示出来。出图时加图例说明分级方法,避免误读。

5. 进阶技巧:用人口格网做可达性与暴露度分析

5.1 基于格网的人口加权距离

一个典型进阶用法是计算「某设施的服务人口」。比如一个医院,服务半径 5km,要算覆盖多少人口。做法是:以医院为中心,生成 5km 缓冲区,和人口栅格做分区统计求和。但更精细的是做成本距离,考虑路网和地形。如果只用直线距离,用rasterio和scipy就能实现。下面给出缓冲区统计的代码。

import geopandas as gpd from shapely.geometry import Point from rasterstats import zonal_stats # 医院位置 hospital = Point(116.4, 39.9) # 生成 5km 缓冲区,注意要投影到米制坐标系 gdf = gpd.GeoDataFrame([{"geometry": hospital}], crs="EPSG:4326") gdf = gdf.to_crs("EPSG:3857") # Web Mercator 近似米 gdf["geometry"] = gdf.buffer(5000) gdf = gdf.to_crs("EPSG:4326") # 转回栅格坐标系 stats = zonal_stats(gdf, "china_pop_1km.tif", stats=["sum"], nodata=-9999) print("5km 覆盖人口:", stats[0]["sum"])

逻辑说明:缓冲区必须在米制坐标系下生成,否则 5km 会变成 5 度。Web Mercator 在高纬度有面积变形,但做小范围缓冲区近似可用。更严谨的做法是用 UTM 或 Albers。zonal_stats求和得到覆盖人口。参数上,buffer(5000)是 5km,改半径就改这个数。如果要做多个设施,循环处理即可。

5.2 人口暴露度:叠加洪水、高温等灾害图层

暴露度分析是人口格网的高频应用。以洪水为例,先拿到洪水淹没范围栅格(0/1 或水深),和人口栅格做逐像元乘法,再汇总。关键是空间对齐:两个栅格必须同一网格、同一坐标系、同一分辨率。用rasterio的reproject和resample对齐。下面是对齐后相乘的代码。

import rasterio from rasterio.warp import reproject, Resampling import numpy as np # 读取人口栅格作为参考 with rasterio.open("china_pop_1km.tif") as src: pop = src.read(1).astype(float) pop[pop == src.nodata] = np.nan profile = src.profile.copy() # 读取洪水栅格,重采样到人口栅格网格 with rasterio.open("flood.tif") as fsrc: flood = np.zeros_like(pop) reproject( source=rasterio.band(fsrc, 1), destination=flood, src_transform=fsrc.transform, src_crs=fsrc.crs, dst_transform=profile["transform"], dst_crs=profile["crs"], resampling=Resampling.nearest ) # 暴露人口 = 人口 × 洪水淹没(1 为淹没) exposed = pop * (flood > 0) total_exposed = np.nansum(exposed) print("暴露人口总数:", total_exposed)

逻辑说明:reproject把洪水栅格对齐到人口栅格,Resampling.nearest保持淹没边界。flood > 0生成布尔掩膜,乘以人口得到暴露人口。np.nansum忽略 nan 求和。参数上,如果洪水栅格是水深,可以改成pop * flood得到人口×水深的暴露度。注意两个栅格的 nodata 都要处理,否则 0 值会参与计算。

5.3 一个我常犯的错误:忽略年份和口径

最后说一个血泪经验。我曾经拿 2015 年的 WorldPop 和 2020 年的行政区人口做对比,结论是「某市人口被低估 15%」,后来发现 WorldPop 的 2015 版和 2020 版用了不同的普查基准和建模方法,直接比年份就是错的。人口格网数据不是实测,是反演结果,不同版本、不同机构之间的差异可能比年份变化还大。做任何跨源对比前,先在同一组行政区上汇总,看总量差异,再决定是否融合。如果要做时间序列,尽量用同一机构同一方法论的多年产品。另一个习惯是:每次分析前把元数据、坐标系、无数据值、年份、单位这五项写在一个 README 里,三个月后回头看还能复现。希望帮到你。

本文还有配套的精品资源,点击获取