1. 为什么值得认真做一次人口密度与趋势分析
人口数据这东西,乍看就是一堆统计表格,但真正动手拆过的人都知道,它其实是所有城市研究里性价比最高的一类数据。原因很简单:获取门槛低、颗粒度够细、时间跨度长,而且几乎能和任何其他领域的数据挂上钩。我这次做的项目,核心目标就两件事——把北京各区县的人口密度在地图上还原出来,再把过去二十年的常住人口变化趋势拉成一条能看懂的曲线。
先说清楚这个项目能干什么。做完之后你能得到三样东西:一张按行政区划着色的人口密度分布图,一套从2000年代到最近年份的人口总量与增长率趋势表,以及一份按区域分组的对比分析。它解决的核心问题是:把"北京人口到底多密、往哪儿流、未来怎么走"这三个模糊的直觉问题,变成有数字支撑的明确结论。
适合谁来参考?我认为有三类人最用得上。第一类是做城市研究、区域经济分析的学生和研究者,这套流程可以直接复用到其他城市;第二类是做商业选址、门店布局的从业者,人口密度和趋势直接决定了一个区域的消费潜力;第三类是对数据可视化感兴趣的开发者,整个项目用到的工具链不复杂,但能练到数据清洗、地理信息处理、可视化三个环节。
我自己的背景是做数据分析的,之前做过几个城市相关的项目,这次重新捡起北京人口这个题目,是因为发现很多公开的分析要么只讲总量不讲密度,要么只画图不讲趋势,把两个维度结合起来做的反而不多。所以这篇文章我会把完整思路、踩过的坑、参数怎么定都写清楚,你照着做基本能复现。
2. 数据源选择与整体设计思路
2.1 数据从哪来:三类数据源的取舍
做人口分析,第一步永远是数据。我实际用到的数据分三层,每层的来源和用途都不一样。
第一层是人口总量数据,也就是各区县历年的常住人口数。这类数据最权威的来源是统计年鉴,按年度发布,颗粒度到区县级。我用的时间跨度是从2005年前后到最近可获取的年份,大概十五到二十年的区间。这里有个细节要注意:不同年份的统计口径可能调整过,比如某些年份把功能区的数据单独拆出来,某些年份又合并回去,直接拉时间序列会出现跳变。我的处理方式是先做口径对齐,把明显因为区划调整导致的突变年份标注出来,避免误读成真实的人口变动。
第二层是行政区划边界数据,也就是每个区的多边形轮廓。这是做密度分布图的基础,没有边界就没法算面积、没法着色。边界数据我用的是通用的行政区划矢量文件,格式是常见的GeoJSON。这里要提醒一句:区划边界是会变的,比如某些年份有撤县设区,如果你用的边界文件和人口数据的年份对不上,算出来的密度就是错的。我的做法是统一用最新一版的边界,然后把历史人口数据按最新区划重新归并。
第三层是面积数据,用来算密度。面积可以从边界数据里直接算出来,但要注意坐标系问题。如果用经纬度坐标直接算面积,结果会偏,因为经纬度不是等面积投影。正确做法是先投影到等面积坐标系再算,或者直接用官方公布的各区面积。我两种都试过,差异在个别区能到百分之几,所以最后以官方面积为准,边界数据只用来画图。
2.2 整体设计:为什么这么搭
整个项目的设计思路可以概括成一句话:先算密度,再看趋势,最后交叉分析。这个顺序不是随便定的。
先算密度,是因为密度是静态的截面指标,它回答的是"现在哪里最挤"。这一步依赖的是某一年的数据加边界加面积,逻辑简单、结果直观,适合作为整个分析的起点。而且密度图做出来之后,你会对北京的人口分布有个空间直觉,后面看趋势的时候脑子里有画面。
再看趋势,是因为趋势是动态的时间序列,它回答的是"人口往哪儿流"。这一步依赖的是多年的总量数据,重点在于处理口径变化和计算增长率。趋势分析单独看容易枯燥,但和密度图结合之后,你就能看出"哪些高密度区还在涨、哪些已经在跌"。
最后交叉分析,是把密度和趋势两个维度叠在一起,把区县分成四类:高密度高增长、高密度低增长、低密度高增长、低密度低增长。这个分类是整篇分析最有价值的部分,因为它直接指向结论——哪些区域在承载压力,哪些区域在承接外溢。
2.3 工具选型:够用就好
工具这块我没追求花哨,核心就三个:Python做数据处理,GeoPandas做地理信息处理,Matplotlib或类似库做可视化。为什么选这套?
Python的生态最全,pandas处理表格数据、geopandas处理矢量数据、matplotlib出图,一条龙。GeoPandas是基于pandas扩展的,学过pandas的人上手很快,它的核心数据结构GeoDataFrame就是在普通表格上加了一列几何对象,读写GeoJSON、做空间连接、算面积都是一行代码的事。可视化方面,静态图用matplotlib足够,如果需要交互式的可以换plotly,但静态图在报告里更实用。
我试过用纯Excel做,做到密度计算那步就卡住了,因为要处理多边形面积和空间匹配,Excel根本做不了。也试过用在线可视化工具,出图快但没法做复杂的数据清洗和趋势计算。所以最后还是回到Python这套,虽然前期配置环境花点时间,但后面全程顺畅。
提示:GeoPandas在Windows上安装偶尔会有依赖问题,建议用conda装而不是pip,能省掉很多编译报错。如果实在装不上,可以退而求其次用geojson加shapely手动处理,但代码量会大不少。
3. 核心细节解析与实操要点
3.1 人口密度到底怎么算才靠谱
密度等于人口除以面积,这个公式谁都懂,但实际操作里有三个坑。
第一个坑是面积单位。官方面积一般给的是平方公里,人口给的是万人或人,算之前要统一。我习惯把人口统一成"人",面积统一成"平方公里",最后密度单位就是"人每平方公里"。北京核心区的密度能到每平方公里两万人以上,远郊区可能只有几百人,差了两个数量级,所以可视化的时候用线性色阶会让远郊全糊成一片,得用对数色阶或者分位数分级。
第二个坑是区划口径。前面提过,历史数据里有些区是后来才设的,比如某些县改区。如果你直接用原始数据算密度,会出现某个区某年突然从零变成几十万的情况,那不是人口增长,是区划变了。我的处理方式是建一张对照表,把老口径的数据映射到新口径上,映射不了的年份就标注缺失,不硬凑。
第三个坑是常住人口和户籍人口的区别。这两个概念差很多,常住人口包含外来人口,户籍人口只算本地户口。做密度和趋势分析,一般用常住人口,因为它反映的是实际居住压力。但有些年份的公开数据只有户籍人口,这时候要么找替代来源,要么在分析里明确标注口径差异,不能混着用。
3.2 趋势分析里的增长率怎么算才不误导
趋势分析的核心指标是增长率,但增长率有好几种算法,用错了结论会完全相反。
最简单的是同比增长率,就是今年减去年再除以去年。这个指标适合看短期波动,但受基数影响大。比如一个区从10万人涨到12万人,增长率是20%;另一个区从100万人涨到110万人,增长率是10%。单看增长率会以为第一个区发展更快,但绝对增量其实是第二个区更大。
所以我一般会同时算两个指标:增长率和绝对增量。增长率看势头,绝对增量看体量。两个指标结合,才能判断一个区域是真在快速扩张,还是只是基数小显得增长快。
还有一个坑是年均增长率。如果要跨多年算平均增速,不能用简单的算术平均,得用复合增长率公式,也就是期末除以期初,开年数次方再减一。这个公式考虑了复利效应,比算术平均准确得多。我见过有人直接把每年的增长率加起来除以年数,算出来的结果偏高,尤其是波动大的年份。
3.3 可视化分级:怎么让图既好看又准确
密度图的分级方式直接决定了读者看到什么。我试过三种分级,效果差别很大。
第一种是等间距分级,就是把密度范围平均切成几段。这种方法的问题是,如果数据分布极不均匀,大部分区域会挤在最低的那一段,图上看起来大片同色,没有区分度。北京的人口密度就是典型的极不均匀,核心区远高于郊区,等间距分级基本废掉。
第二种是分位数分级,保证每一级里的区域数量差不多。这种方法视觉上最均衡,每个颜色都有足够的区域,但缺点是同一级内的实际密度可能差很多,读者容易误读。比如最高级里可能既有每平方公里三万人的区,也有两万人的区,看起来一样但其实差不少。
第三种是自然断点分级,让组内差异最小、组间差异最大。这是我最推荐的方式,它能在数据分布不均匀的情况下仍然分出有意义的层级。GeoPandas和很多可视化库都内置了这个方法,一行参数就能切换。
注意:不管用哪种分级,图例上一定要标清楚每一级的数值范围,不能只写"高、中、低"。读者需要知道"高"到底是多高。
3.4 时间序列对齐:最容易被忽略的脏活
做趋势分析,最耗时的不是算指标,而是对齐时间序列。我这次的数据里,至少有三分之一的精力花在这上面。
具体来说,问题出在几个地方。一是年份不连续,有些年份的数据缺失,得决定是插值还是跳过。我的原则是:如果缺一年,用前后两年线性插值;如果缺两年以上,直接标注缺失,不硬补。二是区县名称不一致,同一个区在不同年份的文件里可能叫法不同,比如带不带"区"字、有没有别名,得建一张名称映射表统一。三是数据格式不一致,有的年份是Excel,有的是CSV,有的是PDF里的表格,得先统一成结构化格式再合并。
这些活听起来琐碎,但不做的话后面全是错。我的建议是专门花时间做一张"数据字典",把每个字段的含义、单位、来源年份都记下来,后面出问题的时候能快速定位。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先把环境搭起来。我用的是conda创建独立环境,避免和系统里的其他包冲突。
conda create -n population python=3.10 conda activate population conda install geopandas matplotlib pandas numpy如果你不用conda,pip也能装,但geopandas在Windows上可能需要额外装GDAL之类的底层库,比较折腾。conda的好处是这些依赖它会自动处理好。
装完之后验证一下:
import geopandas as gpd import pandas as pd import matplotlib.pyplot as plt print(gpd.__version__)能正常打印版本号就说明环境没问题。
4.2 读取与清洗人口数据
假设你已经有了一份区县人口数据,格式是CSV,包含年份、区县名称、常住人口三列。第一步是读进来看看长什么样。
df = pd.read_csv('population.csv', encoding='utf-8') print(df.head()) print(df.dtypes) print(df['区县'].unique())重点看三件事:区县名称有没有不一致的、人口列是不是数值类型、年份范围覆盖多少。如果人口列是字符串(比如带了"万"字),得先清洗成数值。
df['人口'] = df['人口'].str.replace('万', '').astype(float) * 10000这一步的意图是把所有人口统一成"人"为单位。如果原始数据单位不统一,比如有的年份是万人有的是人,一定要先统一,否则后面算密度全错。
然后是区县名称对齐。我一般会建一张标准名称表,然后用映射把原始名称转成标准名称。
name_map = {'某县': '某区', '某区县': '某区'} df['区县'] = df['区县'].replace(name_map)4.3 读取边界数据并计算面积
边界数据用GeoJSON格式,读进来是一个GeoDataFrame。
gdf = gpd.read_file('beijing_districts.geojson') print(gdf.columns) print(gdf.crs)crs是坐标参考系,如果是经纬度(一般是EPSG:4326),直接算面积会偏。正确做法是先投影到等面积坐标系。
gdf_proj = gdf.to_crs('EPSG:4526') # 投影到适合该区域的等面积坐标系 gdf['面积_km2'] = gdf_proj.geometry.area / 1e6这里除以1e6是因为投影坐标系的单位通常是米,算出来是平方米,要转成平方公里。EPSG:4526是一个常用的等面积投影,适合中国区域。如果你不确定用哪个,可以查一下适合你数据范围的投影代码,或者直接用官方公布的面积数据,省去这一步。
4.4 合并数据并计算密度
把人口数据和边界数据按区县名称合并。
merged = gdf.merge(df[df['年份'] == 2020], on='区县', how='left') merged['密度'] = merged['人口'] / merged['面积_km2']这里我取了2020年作为截面,你可以换成任何你想分析的年份。合并之后检查一下有没有匹配不上的区县,如果有,说明名称还是没对齐,得回去改映射表。
print(merged[merged['人口'].isna()]['区县'])如果打印出来是空的,说明全部匹配成功。
4.5 绘制密度分布图
出图这一步,关键是分级方式和配色。
fig, ax = plt.subplots(1, 1, figsize=(12, 10)) merged.plot(column='密度', ax=ax, scheme='natural_breaks', k=6, cmap='YlOrRd', legend=True, legend_kwds={'loc': 'lower right', 'title': '人口密度(人/km2)'}) ax.set_title('北京各区人口密度分布') ax.axis('off') plt.savefig('density_map.png', dpi=300, bbox_inches='tight')scheme='natural_breaks'就是自然断点分级,k=6是分六级。配色我用了黄到红的渐变,密度越高颜色越深,符合直觉。legend_kwds里的title是图例标题,一定要写清楚单位。
出图之后我建议手动检查一下:核心区是不是最深色、远郊是不是最浅色、有没有哪个区颜色明显异常。如果发现异常,多半是数据匹配错了或者面积算错了。
4.6 计算多年趋势并出图
趋势分析需要把多年的数据拉成时间序列。假设你的CSV里有多个年份,可以这样处理。
pivot = df.pivot_table(index='区县', columns='年份', values='人口') growth = (pivot[2020] - pivot[2010]) / pivot[2010] * 100这是十年增长率。如果要算年均复合增长率:
years = 10 cagr = ((pivot[2020] / pivot[2010]) ** (1/years) - 1) * 100出趋势图的时候,我建议按区域分组画,比如核心区一组、近郊一组、远郊一组,每组画几条线。这样比把所有区画在一张图上清楚得多。
fig, ax = plt.subplots(figsize=(12, 6)) for district in core_districts: ax.plot(pivot.columns, pivot.loc[district], marker='o', label=district) ax.set_xlabel('年份') ax.set_ylabel('常住人口') ax.legend() plt.savefig('trend.png', dpi=300, bbox_inches='tight')4.7 交叉分析:把密度和趋势叠起来
最后一步是把两个维度合起来。做法很简单,建一张表,每行是一个区,列包括密度、增长率、绝对增量,然后按密度和增长率的中位数分成四象限。
result = pd.DataFrame({ '密度': merged.set_index('区县')['密度'], '增长率': growth }) density_median = result['密度'].median() growth_median = result['增长率'].median() def classify(row): if row['密度'] > density_median and row['增长率'] > growth_median: return '高密度高增长' elif row['密度'] > density_median: return '高密度低增长' elif row['增长率'] > growth_median: return '低密度高增长' else: return '低密度低增长' result['类型'] = result.apply(classify, axis=1) print(result.sort_values('密度', ascending=False))这张表就是整个分析的核心产出。高密度高增长的区说明还在持续承压,高密度低增长的区说明已经趋于饱和,低密度高增长的区是未来的潜力区,低密度低增长的区基本没什么变化。
5. 常见问题与排查技巧实录
5.1 数据匹配不上的排查顺序
合并数据的时候最常见的报错就是匹配不上,表现为合并后某些区的人口是空的。排查顺序我总结成三步。
第一步,检查名称。把两个数据集的区县名称列都打印出来,肉眼对比。常见问题包括:一个带"区"一个不带、有空格、有全角半角差异。用set(a) - set(b)能快速找出差集。
第二步,检查数据类型。有时候名称看起来一样,但一个是字符串一个是数值,或者有隐藏字符。用repr()打印出来能看到隐藏字符。
第三步,检查年份。如果你筛选了特定年份,确认那个年份在两个数据集里都存在。
5.2 面积算出来不对怎么办
面积算错通常有两个原因。一是坐标系没投影,直接用经纬度算,结果会偏。二是投影坐标系选错了,不同投影适合不同区域,选错了面积会有系统偏差。
排查方法:拿一个你知道官方面积的区,用你的方法算一遍,对比差异。如果差异在百分之一以内,基本可以接受;如果差很多,说明投影有问题。最稳妥的办法是直接用官方面积,边界数据只用来画图。
5.3 图上颜色分布异常
如果出图后发现某个区颜色明显不对,比如一个远郊区和核心区一样深,多半是数据错了。排查顺序:先看这个区的密度数值是否合理,再看它的人口和面积是否匹配,最后看边界数据里这个区的几何是否正常(有没有面积异常大或小)。
我遇到过一次,某个区的边界数据里混进了另一个区的多边形,导致面积翻倍、密度减半。这种问题只能靠肉眼检查边界图发现,所以出图之后一定要看一眼整体形状对不对。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 合并后人口为空 | 区县名称不一致 | 打印名称列对比 | 建名称映射表统一 |
| 密度数值异常大或小 | 面积单位或坐标系错误 | 对比官方面积 | 统一单位、投影后算面积 |
| 趋势图某年突变 | 区划调整或口径变化 | 查该年区划记录 | 标注或重新归并 |
| 图上大片同色 | 分级方式不合适 | 换分级方法对比 | 用自然断点或分位数 |
| 增长率算出来离谱 | 基数太小或口径混用 | 检查原始数据 | 同时看绝对增量 |
5.5 几个我踩过的坑
第一个坑是忽略了口径变化。我一开始直接拿原始数据算增长率,结果某个区某年增长率超过百分之百,查了半天才发现是那年区划调整了。后来我养成了习惯,做任何时间序列分析之前,先查一遍区划变更历史。
第二个坑是用了错误的投影。我最早用经纬度直接算面积,算出来的密度整体偏大,因为纬度越高经度对应的实际距离越短。后来投影之后才对上。
第三个坑是可视化过度设计。我一开始想在一张图上同时展示密度和趋势,用了气泡大小表示密度、颜色表示增长率,结果图太复杂没人看得懂。后来拆成两张图,一张密度图一张趋势图,反而清楚。
提示:做数据分析,图越简单越好。一张图只讲一件事,讲清楚比讲全更重要。
6. 分析结论怎么读才有价值
6.1 密度分布说明了什么
从密度分布看,北京的人口高度集中在核心的几个区,这些区的密度是远郊的几十倍甚至上百倍。这种极化的分布不是偶然的,它和就业机会、公共服务、交通便利度直接相关。核心区集中了大部分的就业岗位和优质资源,人口自然往那里挤。
但密度高不一定是好事。密度过高意味着人均公共资源被摊薄,交通拥堵、住房紧张、环境压力都会加剧。所以看密度图的时候,不能只看哪里最红,还要想这个密度是否可持续。
6.2 趋势变化透露了什么
从趋势看,核心区的人口增长已经明显放缓甚至转负,而近郊和部分远郊还在增长。这个信号很重要,它说明人口正在从核心区向外溢出。溢出的原因可能是核心区生活成本太高、也可能是近郊的就业和配套跟上来了。
这个趋势对做商业选址的人特别有用。核心区虽然密度高,但增长见顶,增量空间有限;近郊密度中等但增长快,可能是更值得布局的区域。当然具体还要结合业态,高频消费还是得靠核心区的人流,但仓储、物流这类对密度不敏感的业态,往近郊走更划算。
6.3 交叉分类的实用价值
把密度和趋势交叉之后,四类区域的策略含义就很清楚了。高密度高增长的区,重点是疏解和优化,不能再无限加码;高密度低增长的区,重点是提升质量,把存量人口服务好;低密度高增长的区,重点是提前布局基础设施,别等人口涌进来才补课;低密度低增长的区,重点是找准定位,靠特色而不是靠规模。
这套分类框架不只适用于北京,换任何一个城市都能用。你只需要把数据换掉,逻辑完全一样。这也是我觉得这个项目值得做一遍的原因——它给你的不只是一张图,而是一套能复用的分析思路。
6.4 后续可以怎么扩展
如果想把分析做得更深,有几个方向可以延伸。一是加入经济数据,比如GDP、产业结构,看人口变化和经济变化是否同步。二是加入交通数据,比如地铁站点分布,看人口密度和交通便利度的相关性。三是做预测,用时间序列模型外推未来几年的人口变化。四是做更细颗粒度的分析,比如按街道而不是按区,颗粒度越细结论越精确,但数据获取难度也越大。
我个人觉得最值得做的是第二个方向,因为交通和人口的关系最直接,而且数据相对好获取。地铁一响黄金万两这句话虽然俗,但背后确实有数据支撑。
最后分享一个我在实际操作中的体会:人口分析最难的从来不是技术,而是数据清洗和口径对齐。技术部分你花一天就能学会,但数据里的坑可能花你一周。所以如果你准备做类似的项目,建议把至少一半的时间留给数据准备,别急着出图。数据干净了,后面的分析自然顺。