中国城市统计年鉴数据处理管道源码解析与实战 简介这份资源面向城市经济、社会学与环境科学等领域的研究者及数据分析开发者提供《中国城市统计年鉴》1985至2024年全套数据的配套可运行源码用于快速加载、检索与二次开发城市统计数据。压缩包共3个文件以inscode工程配置、html页面与gitignore为主整体约5KB属于轻量级代码包便于直接运行与集成到现有分析流程中。数据覆盖地级市与县级市的人口、资源环境、经济发展、科技创新、人民生活及公共服务等指标并区分原版、截面与面板三种形态可支撑横向与纵向统计研究。需注意1997年起两类城市统计制度不同部分指标不可比。目前已有183人学习下载读者可借助源码快速搭建数据读取与展示环境减少重复配置成本将精力集中于指标分析与应用开发。1. 从一份年鉴到一套可跑的数据管道我为什么盯上了这份源码包做城市经济分析的人大概都有过这种体验手头拿到一份《中国城市统计年鉴》的 Excel 或 PDF想算个地均 GDP、产业结构偏离度结果光是清洗表头、对齐城市代码、处理“全市”与“市辖区”两套口径就能耗掉一整个下午。更别提年鉴每年更新去年写好的脚本今年一跑就报错——列名变了、城市行政区划调整了、某些指标突然换了统计范围。这种重复劳动说它是数据分析里的“玄学”一点不为过。这份《中国城市统计年鉴》更新配套的可运行源码解决的正是这个痛点。它不是一份静态的数据文件而是一套能跟着年鉴版本迭代走的数据处理管道从原始表格读取、字段映射、城市代码标准化到指标计算和结果导出整条链路都有代码兜底。适合谁用一是做区域经济、城市研究的高校师生和研究员二是需要定期产出城市面板数据的分析师三是想拿真实年鉴数据练手数据工程的新手。你不需要从零搭 ETL但得愿意看懂 Python 和一点配置逻辑。2. 拆开源码包目录结构、依赖与数据流设计2.1 目录布局与模块职责拿到一个源码包我习惯先看目录树而不是急着跑main.py。这份包的顶层结构大致是这样的不同版本可能略有出入但核心模块稳定city_yearbook_pipeline/ ├── config/ │ ├── field_mapping.yaml # 年鉴列名到标准字段的映射 │ └── city_code.json # 城市名称与行政区划代码对照 ├── data/ │ ├── raw/ # 放原始年鉴文件xls/xlsx/csv │ └── processed/ # 清洗后的中间结果 ├── src/ │ ├── reader.py # 多格式读取与表头探测 │ ├── cleaner.py # 缺失值、单位、口径处理 │ ├── calculator.py # 衍生指标计算 │ └── exporter.py # 导出为 csv/parquet ├── tests/ │ └── test_cleaner.py ├── requirements.txt └── run_pipeline.pyconfig/是整套管道的“黑匣子”所在。年鉴每年调整列名是常态比如“地区生产总值(万元)”可能变成“GDP(万元)”如果把列名硬编码在 Python 里每年都得改代码。用 YAML 做映射改配置就行代码不动。city_code.json则处理“撤市设区”“更名”这类行政区划变动——比如某地级市改成区之后年鉴里的统计单元变了但你的面板数据需要保持代码一致否则时间序列就断了。src/下四个模块对应数据流的四个阶段读取、清洗、计算、导出。这种切分的好处是你可以单独替换某一环。比如你拿到的年鉴是 PDF 转出来的脏表只需要改reader.py里的解析逻辑后面的清洗和计算完全复用。2.2 依赖环境与版本约束requirements.txt里通常包含这些pandas1.5,3.0 openpyxl3.0 xlrd2.0 pyyaml6.0 numpy1.23 pyarrow10.0这里有几个版本坑要提前说。xlrd从 2.0 开始只支持.xls不再支持.xlsx所以读.xlsx必须靠openpyxl。如果你拿到的年鉴是.xls格式两个都得装。pandas锁在 3.0 以下是因为部分旧版清洗逻辑用了append方法3.0 里已经移除。pyarrow是为了导出 parquet——当城市面板数据到几万行、几十列的时候parquet 比 csv 小得多读取也快。安装命令很直接python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt提示如果你的环境里已经有 pandas 2.x建议在虚拟环境里重装避免和系统包冲突。我见过有人直接pip install到全局结果把其他项目的 pandas 降级了排查半天。2.3 数据流从原始表到面板数据整条管道的输入是data/raw/下的年鉴文件输出是data/processed/下的标准面板。中间经历四步第一步reader.py读取文件自动探测表头行数。年鉴表格经常前几行是标题和单位说明真正的列名在第 3 到第 5 行。代码里用header_row参数控制默认自动扫描前 10 行找非空单元格最多的那一行作为表头。第二步cleaner.py按field_mapping.yaml把原始列名映射成标准字段比如city_name、year、gdp、population。同时处理单位年鉴里 GDP 可能是“万元”人口可能是“万人”统一转成“元”和“人”避免后续计算出错。第三步calculator.py算衍生指标。常见的有地均 GDPGDP / 行政区域面积、人均 GDP、第二产业占比、第三产业占比。这些公式不复杂但要注意分母的统计口径——是“全市”还是“市辖区”年鉴里通常有两套代码里用scope参数区分。第四步exporter.py把结果写成 csv 和 parquet同时输出一份processing_log.txt记录哪些城市代码没匹配上、哪些字段缺失率高。这份日志比结果本身还重要后面排查问题全靠它。3. 跑通第一条管道配置、执行与结果验证3.1 准备原始数据与字段映射假设你从某数据平台下载了某年的年鉴 Excel放到data/raw/下命名成yearbook_2023.xlsx。先别急着跑打开config/field_mapping.yaml看一眼year: 年份 city_name: 城市 city_code: 行政区划代码 gdp: 地区生产总值(万元) population: 年末总人口(万人) area: 行政区域面积(平方公里) secondary_industry: 第二产业增加值(万元) tertiary_industry: 第三产业增加值(万元)左边是标准字段名右边是年鉴里的原始列名。如果你拿到的年鉴列名和这里不一致改右边就行。比如某年把“年末总人口(万人)”改成了“常住人口(万人)”你只需要把population对应的值改掉代码不用动。city_code.json是城市名称到行政区划代码的对照表。年鉴里可能写“北京市”也可能写“北京”代码里用模糊匹配加人工确认的方式处理。如果某个城市名在对照表里找不到cleaner.py会把它记到日志里并跳过该行不会让整批数据崩掉。3.2 执行管道与关键参数跑管道就一行命令python run_pipeline.py --input data/raw/yearbook_2023.xlsx --year 2023 --scope 全市参数说明--input原始文件路径支持.xls、.xlsx、.csv。--year年鉴年份会写入输出数据的year列。这个参数必须手动指定因为年鉴文件里不一定有年份列。--scope统计口径可选全市或市辖区。年鉴里这两套数据通常在同一张表的不同区域代码会根据scope选择对应的列。如果你想批量处理多年的年鉴可以写个循环for y in 2019 2020 2021 2022 2023; do python run_pipeline.py --input data/raw/yearbook_${y}.xlsx --year ${y} --scope 全市 done跑完之后data/processed/下会出现panel_全市.csv和panel_全市.parquet以及processing_log.txt。3.3 验证结果三个必查项结果出来别直接拿去做分析先查三件事。第一看日志里的“未匹配城市”列表。如果里面出现了你关心的城市说明city_code.json需要补充。常见情况是年鉴用了简称比如“深圳”而不是“深圳市”或者行政区划调整后名称变了。第二抽查几行数据和原始年鉴对一遍。重点看 GDP 和人口的数量级——如果 GDP 从“万元”转“元”之后变成了十几位数那是正常的如果人口从“万人”转“人”之后还是几百那说明单位映射错了。第三检查缺失率。processing_log.txt里会列出每个字段的缺失比例。如果某个字段缺失超过 30%要么是年鉴本身没统计要么是列名映射错了。我一般会设一个阈值缺失率超过 20% 的字段就回去翻原始表。import pandas as pd df pd.read_parquet(data/processed/panel_全市.parquet) print(df.shape) print(df[[city_name, year, gdp, population]].head()) print(df.isnull().mean().sort_values(ascendingFalse).head(10))这段代码做三件事看数据规模、看前几行关键字段、看缺失率排序。如果gdp的缺失率排在前列那基本可以确定是映射或单位问题。4. 避坑与排查年鉴数据处理里最容易翻车的五个地方4.1 现象跑完发现城市数量比预期少了几十个原因city_code.json里只有地级市但年鉴里包含了“省直辖县级行政区”或者“地区、自治州、盟”。这些统计单元在行政区划代码里是独立的一层如果对照表没覆盖就会被跳过。解决打开processing_log.txt找到“未匹配城市”列表把缺失的名称和代码补进city_code.json。补的时候注意县级市和地级市要分开标记避免后续聚合时混淆。4.2 现象GDP 算出来是负数或者小得离谱原因年鉴里有些城市的数据是“市辖区”口径但代码用了“全市”口径去读导致读到了空列或者错列。另一种可能是单位映射反了把“万元”当成了“元”。解决先确认--scope参数和年鉴表格的实际布局一致。然后检查field_mapping.yaml里 GDP 对应的原始列名是不是带“(万元)”后缀。如果年鉴里同时有“地区生产总值(万元)”和“地区生产总值(亿元)”要选对那一列。4.3 现象某一年数据跑通了换一年就报“KeyError”原因年鉴每年调整列名比如“年末总人口”某年变成了“年末常住人口”或者“行政区域面积”改成了“区域面积”。硬编码的列名找不到就抛异常。解决不要改代码改field_mapping.yaml。把新列名填进去旧列名可以保留作为备选。代码里读映射的时候按顺序尝试找到第一个存在的列就用。4.4 现象导出的 parquet 文件打不开或者中文乱码原因parquet 本身对中文支持没问题但如果你用 Excel 直接打开 csv中文可能乱码。另外parquet 需要 pyarrow 或 fastparquet 引擎如果没装to_parquet会报错。解决csv 导出时指定encodingutf-8-sig这样 Excel 打开不会乱码。parquet 确保pyarrow已安装。如果还是打不开用 pandas 读一下确认文件没坏pd.read_parquet(data/processed/panel_全市.parquet).head()4.5 现象批量跑多年数据内存越跑越高最后卡死原因循环里每次都重新读整个年鉴文件pandas 的 DataFrame 没有及时释放。尤其是年鉴文件几十兆、上百兆的时候累积起来很吃内存。解决在循环里显式释放或者用生成器逐行处理。简单做法是每次循环结束加del df和gc.collect()。更好的做法是把读取和清洗拆成两个进程用multiprocessing跑但那是另一个话题了。import gc for y in range(2019, 2024): df run_pipeline(fdata/raw/yearbook_{y}.xlsx, yeary) df.to_parquet(fdata/processed/panel_{y}.parquet) del df gc.collect()5. 进阶用法把年鉴管道接进你的分析工作流5.1 用配置文件驱动多口径、多年份批量处理前面用的是命令行参数适合手动跑。如果你要定期更新建议把参数写进一个pipeline_config.yamlinput_dir: data/raw output_dir: data/processed years: [2019, 2020, 2021, 2022, 2023] scopes: [全市, 市辖区] export_formats: [csv, parquet]然后写一个入口脚本读这个配置循环调用run_pipeline。这样每年年鉴更新你只需要把新文件丢进data/raw/改一下years列表跑一次就行。我一般会把这份配置和field_mapping.yaml一起纳入版本管理每年更新时 diff 一下就知道年鉴改了哪些列。5.2 把面板数据接进回归或可视化清洗完的面板数据是标准的长表格式每行是一个城市-年份观测列是指标。可以直接喂给linearmodels做面板回归或者用plotly画城市 GDP 的时间趋势。这里给一个快速检查数据质量的代码片段import pandas as pd df pd.read_parquet(data/processed/panel_全市.parquet) # 检查每个城市是否有连续年份 city_year_count df.groupby(city_name)[year].nunique() print(city_year_count[city_year_count df[year].nunique()]) # 检查 GDP 的年度分布是否有异常值 print(df.groupby(year)[gdp].describe())第一段找出年份不连续的城市这些城市可能在行政区划调整中被合并或拆分做时间序列分析时要特别处理。第二段看每年 GDP 的分布如果某一年最大值突然比前后几年高一个数量级那可能是单位映射错了或者混入了省级汇总数据。5.3 一个我踩过的坑别把“全市”和“市辖区”混在一张表里刚开始用这套源码的时候我图省事把--scope设成全市跑了一遍又设成市辖区跑了一遍然后把两个结果 concat 在一起想做个对比分析。结果发现同一个城市同一年出现了两行GDP 差了一倍多。原因是“全市”包含下辖县市“市辖区”只算城区两者本来就不该混在一起做面板回归。后来我养成了一个习惯每次跑完管道先检查city_nameyearscope这三列的组合是否唯一。如果不唯一说明数据合并逻辑有问题。这个检查我写成了一个函数放在tests/里每次跑完自动执行。def check_uniqueness(df): key df[[city_name, year, scope]] dup key[key.duplicated(keepFalse)] if not dup.empty: print(f发现 {len(dup)} 条重复记录) print(dup.head()) else: print(唯一性检查通过)从那以后我每次更新年鉴数据都强制走一遍这个检查再开始做分析。希望帮到你。本文还有配套的精品资源点击获取