北京人口密度与二十年趋势分析:从数据清洗到可视化实战
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 python3.10 conda activate population conda install geopandas matplotlib pandas numpy如果你不用condapip也能装但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, encodingutf-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区县, howleft) merged[密度] merged[人口] / merged[面积_km2]这里我取了2020年作为截面你可以换成任何你想分析的年份。合并之后检查一下有没有匹配不上的区县如果有说明名称还是没对齐得回去改映射表。print(merged[merged[人口].isna()][区县])如果打印出来是空的说明全部匹配成功。4.5 绘制密度分布图出图这一步关键是分级方式和配色。fig, ax plt.subplots(1, 1, figsize(12, 10)) merged.plot(column密度, axax, schemenatural_breaks, k6, cmapYlOrRd, legendTrue, legend_kwds{loc: lower right, title: 人口密度(人/km2)}) ax.set_title(北京各区人口密度分布) ax.axis(off) plt.savefig(density_map.png, dpi300, bbox_inchestight)schemenatural_breaks就是自然断点分级k6是分六级。配色我用了黄到红的渐变密度越高颜色越深符合直觉。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], markero, labeldistrict) ax.set_xlabel(年份) ax.set_ylabel(常住人口) ax.legend() plt.savefig(trend.png, dpi300, bbox_inchestight)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, axis1) print(result.sort_values(密度, ascendingFalse))这张表就是整个分析的核心产出。高密度高增长的区说明还在持续承压高密度低增长的区说明已经趋于饱和低密度高增长的区是未来的潜力区低密度低增长的区基本没什么变化。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、产业结构看人口变化和经济变化是否同步。二是加入交通数据比如地铁站点分布看人口密度和交通便利度的相关性。三是做预测用时间序列模型外推未来几年的人口变化。四是做更细颗粒度的分析比如按街道而不是按区颗粒度越细结论越精确但数据获取难度也越大。我个人觉得最值得做的是第二个方向因为交通和人口的关系最直接而且数据相对好获取。地铁一响黄金万两这句话虽然俗但背后确实有数据支撑。最后分享一个我在实际操作中的体会人口分析最难的从来不是技术而是数据清洗和口径对齐。技术部分你花一天就能学会但数据里的坑可能花你一周。所以如果你准备做类似的项目建议把至少一半的时间留给数据准备别急着出图。数据干净了后面的分析自然顺。