Python处理地级市债务数据:zip解开到面板重打包全流程
简介这份数据集覆盖2007—2023年我国地级市层面的地方债、政府债与隐性债务关键指标适用于研究地方政府债务风险、财政可持续性、区域金融稳定以及城市面板数据建模的学者、研究生和行业分析人员。数据按地级市口径逐年整理时间跨度17年原始债务数据结构完整可支撑跨年份、跨地区比较。包内共4个文件其中两个dta文件分别提供剔除区县后的纯净样本与合并后的完整分析表便于直接进入Stata等统计软件操作xlsx文件适合快速浏览年度债务明细html文件收录数据来源说明方便核查口径与追溯出处。压缩包整体约3.43MB体量紧凑、下载快速。目前已有51人学习下载。数据集已初步完成清污合并能显著减少研究者整理数据的时间适用于债务规模测算、风险预警指标构建、地方财政压力与经济增长关系等实证场景初学者也可借助xlsx文件快速熟悉数据结构用作教学或调研的基础数据。1. 打开 zip 之前先想清楚地级市债务数据为什么难用拿到“地级市-地方债、政府债、隐债数据2007-2023年.zip”这类压缩包第一反应通常是解压后直接读 Excel。但真正麻烦的不是 zip 解不开而是包里装着横跨 17 年、三种口径的地级市面板地方债、政府债、隐性债统计范围不同发布节奏不同“债”的定义还可能在某一年发生过切换。不提前设计就直接解压只会得到一堆名字相近的表格而不是能分析的数据。后面按我处理这类数据资产的常规流程展开先用 zipfile 核对文件清单再清洗三套债务口径然后对齐年份与地级市名称最后校验并重打包。适合需要把历史债务数据落成结构化表格的分析师、金融从业者与数据工程师。zip 在这里不只是压缩格式更是一份数据交付契约。2. 用 zipfile 盘点 zip 里的债务文件清单2.1 解压前先读 namelist把目录当成数据库来审拿到这种带年份区间的 zip直接解压到桌面是失败率最高的做法。文件名里的“2007-2023 年”很容易让人以为数据已经整理好实际包内文件往往按年份、地市、指标切碎还带着“最终版”“核对版”这类后缀。我一般会把 zip 当成一个只读数据库先列一遍全量文件清单再决定后续怎么读。import zipfile from pathlib import Path zpath Path(地级市-地方债、政府债、隐债数据2007-2023年.zip) with zipfile.ZipFile(zpath, r) as zf: for info in zf.infolist(): print(f{info.filename}\t{info.file_size}\t{info.compress_size})infolist() 返回每个压缩条目的 ZipInfo 对象filename 是包内路径file_size 是解压后大小compress_size 是压缩后大小。两者接近说明该文件本来已压过基本不用再压缩差异很大则说明里面是文本型内容。看这几列就能快速判断包里是一张总表还是几百个碎片文件后续用内存读还是分批处理从这里就能定下来。清单确认无误后先跑一遍完整性检查。testzip() 会逐个校验 CRC返回第一个损坏的文件名全部正常则返回 None。这一步放在正式读取前面能避免读一半才发现某个年份文件损坏。with zipfile.ZipFile(zpath, r) as zf: bad zf.testzip() if bad is not None: print(f损坏条目: {bad}) else: print(全部条目 CRC 校验通过)实际排障中常见的报错 error read zip archive多数不是密码问题而是包体在传输中被截断。先用 testzip() 定位是哪个文件坏了再决定是找接收方重新要一份还是从其他镜像目录补齐比盲目用修复工具压一遍更可靠。2.2 中文文件名乱码与 Excel 字节流读取Windows 上生成的 zip文件名经常用本地代码页编码而 ZipFile 默认按 cp437 解释于是中文文件名变成一串乱码。这不是压缩包损坏而是文件名解码错了。常见做法是把乱码字符串重新编码回原始字节再用 gbk 或 utf-8 解码。def fix_zip_name(raw: str) - str: for enc in (gbk, utf-8): try: return raw.encode(cp437).decode(enc) except (UnicodeDecodeError, UnicodeEncodeError): continue return raw with zipfile.ZipFile(zpath, r) as zf: for info in zf.infolist(): print(fix_zip_name(info.filename))fix_zip_name 会先按 gbk 解码失败再试 utf-8最后回退原字符串。这套逻辑对 2007-2023 这种跨年度归档包很有效因为不同年份的产出者可能用了不同的压缩客户端。文件名纠正后再用 open() 配合 pandas 读内存不需要先解压到磁盘。import pandas as pd from io import BytesIO with zipfile.ZipFile(zpath, r) as zf: target 地级市债务余额.xlsx with zf.open(target, r) as f: df pd.read_excel(BytesIO(f.read()), dtypestr)read_excel 的 dtypestr 是我处理债务数据时坚持的参数。地级市代码、债券编码、年份这类列如果让 pandas 按数值推断前导零和超长编号会被截断后续 join 时会出现一堆莫名不匹配。字符串读入后需要参与计算的列再单独 to_numeric。2.3 加密 zip 与文件组织方式先判断再决定处理路径用 zf.open 读取加密条目时如果没传 pwd 参数会看到 RuntimeError 提示口令错误部分用 AES 加密的 zip 在 Python 3.12 之前根本无法读入需要先用 7-Zip 转成传统 ZipCrypto 或直接手工解压。债务数据包绝大多数不加密但一旦上了口令批量处理逻辑会立刻复杂起来我会先确认是整包口令还是单文件口令。市面上的 zip 压缩包密码破解工具只能做口令探测无法绕过算法本身更实际的路径是从接收渠道确认口令或者让上游改走 7-Zip 的标准加密。zip 内文件组织方式典型表现推荐读取策略单文件大表一个 Excel 含 17 年多市直接 read_excel重点处理多级表头按年份拆文件每年一个 xlsx循环读取并补充年份列按地市拆文件每市一个 xlsx从文件名提取地级市读入后打标签嵌套目录多版本子目录含 V1/V2/最终版先按时间选版本不全量合并3. 地方债、政府债、隐性债三套口径的字段清洗3.1 先定义三张表各算什么债标题里三个词不是同义反复。地方债是地方政府作为发行主体发行的债券政府债是纳入预算管理的政府债务口径通常用限额和余额表达隐性债则不是政府直接债务而是通过城投平台等主体形成的负担需要另行估算或从城投报表里取数。三者统计范围不同口径切换点也可能落在 2015 年前后。口径常见字段标记指标含义地方债一般债券、专项债券、新增债、再融资债地方政府发行债券的余额与发行量政府债债务限额、债务余额纳入预算管理的政府债务总额隐性债城投债、平台有息债务、表外债务城投主体负担、需另行估算的债务拿到 zip 里的原始 Excel第一步不要着急 merge而是先看每个文件的“指标名称”列确认它属于哪一类。部分文件名写的是“政府债”实际内容却是地方政府债券余额这时以表内字段为准。3.2 用口径映射字典一次性收拢指标名称原始表格里对同一类债的写法往往五花八门比如“一般债券”“一般债”“新增一般债”可能指向同一个字段。常见做法不是写一长串 if else而是维护一张口径映射字典用 Series.map 一次收拢未命中的值原样保留方便回头检查。debt_type_map { 一般债券: 地方债-一般, 一般债: 地方债-一般, 新增一般债: 地方债-新增一般, 专项债券: 地方债-专项, 专项债: 地方债-专项, 新增专项债: 地方债-新增专项, 再融资债券: 地方债-再融资, 再融资债: 地方债-再融资, 债务限额: 政府债-限额, 债务余额: 政府债-余额, 城投债: 隐性债-城投债, 平台有息债务: 隐性债-平台有息, } df[债务口径] df[指标名称].map(debt_type_map).fillna(df[指标名称]) unmatched df[df[债务口径] df[指标名称]] matched df[df[债务口径] ! df[指标名称]]map 的查找速度比 apply if else 快而且映射表可以作为字段字典随 zip 一起交付。fillna 保证了不在映射表里的新写法不会静默丢失而是留下来人工补录。清洗时我习惯把 matched 和 unmatched 分开存放等 un-matched 列表处理完再合并避免边清洗边丢数据。提示如果原始 Excel 是多级表头比如第一行是“余额/发行/限额”第二行才是具体指标名用 pd.read_excel(header[0, 1]) 读入再对列名做 join 生成单层字段名。3.3 流量与存量、余额与限额要分开建模清洗过程中最容易出错的不是字段匹配而是把不同时间含义的指标混在一列里。新增债券、再融资债券是流量指标债务余额、债务限额是存量指标两者相加没有任何业务含义。我一般会在数据里增加一列“指标类型”取值分为“存量”“流量”“限额”三类后续建模时按类型分开使用。df[指标类型] df[债务口径].map({ 地方债-一般: 存量, 地方债-专项: 存量, 地方债-新增一般: 流量, 地方债-新增专项: 流量, 地方债-再融资: 流量, 政府债-限额: 限额, 政府债-余额: 存量, 隐性债-城投债: 存量, 隐性债-平台有息: 存量, })加完指标类型后可以对存量口径做一次重复检查避免同一地级市、同一年份、同一债务口径出现多条记录。dup_check df[df[指标类型] 存量].groupby( [地级市, year, 债务口径] ).size().reset_index(name条数) assert dup_check[条数].max() 1, 存量口径存在重复记录债务余额是时点数债务限额是预算上限两者不能混用。做时间序列图时选余额做合规分析时选限额维度不一样合并后也要保留两个字段名不要统一改成“债务总额”。4. 2007-2023 地级市面板的时间对齐与区划衔接4.1 把年份从列名里拆出来从宽表转成长表债务数据最常见的版式是“地级市一行2007 到 2023 各占一列”。这种宽表适合人看不适合机器做时间序列。拿到 zip 里的 Excel 之后第一步是把年份列收拢成一个 year 字段。df_wide.columns [str(c).replace(年, ).strip() for c in df_wide.columns] df_long df_wide.melt( id_vars[地级市, 债务口径], value_vars[str(y) for y in range(2007, 2024)], var_nameyear, value_namevalue, ) df_long[year] df_long[year].astype(int)melt 的 id_vars 是保留不变的维度列value_vars 是需要摊平的年份列var_name 和 value_name 指定转换后的列名。先统一列名里的“2007年”为“2007”可以避免 value_vars 列表与真实列名不一致。所有口径都转成长表后后续才能用“地级市 年份”做主键合并。4.2 区划名称统一后再做复合键合并2007-2023 横跨 17 年地级市的名称和范围并不是固定不变的。撤地设市、市辖区调整、县级市改区等都会让同一个城市在不同年份使用不同名称。zip 内的历史数据如果按当年名称登记直接按“地级市”去重就会把同一城市拆成两条。常见做法是维护一张区划映射表列为历史名称、当前名称、生效年份。用 map 关联到主表生成一个“地级市_当前”字段再做合并。region_map pd.read_excel(区划映射表.xlsx) df_long[地级市_当前] df_long[地级市].map( region_map.set_index(历史名称)[当前名称] ).fillna(df_long[地级市])这个逻辑有个前提映射表本身要先按年份生效范围去维护。比如某市在 2015 年才更名那么 2015 年之后的记录用新名称之前的记录原样保留。把生效年份也放进映射表会比全量替换更稳妥。名称合并后出现的重复行多半是当年部分区县已划出、数据按新旧口径各记录了一次需要逐条核对。4.3 用 _merge 定位两侧缺失的年份与地级市区划名称统一后再把地方债表、政府债表和隐性债表按复合键合并。合并时不要直接用“年份”或“地级市”单独 merge否则会产生笛卡尔积。df_place df_long[df_long[债务口径].str.startswith(地方债)] df_gov df_long[df_long[债务口径].str.startswith(政府债)] merged pd.merge( df_place, df_gov, on[地级市_当前, year], howouter, indicatorTrue, ) print(merged[_merge].value_counts())howouter 在这里优先保证不丢城市indicatorTrue 会在结果中生成 _merge 列标记每条记录来自 left_only、right_only 还是 both。看到 left_only 或 right_only 占比过高通常是两侧年份范围或地级市名称不一致需要回溯第 3 章的口径映射或第 4.2 的区划映射。| _merge 取值 | 含义 | 优先检查方向 | | left_only | 只在地方债表出现 | 政府债表缺城市或年份 | | right_only | 只在政府债表出现 | 地方债表缺城市或年份 | | both | 两侧键能对上 | 继续检查数值口径与单位 |5. 批量校验与 zip 重打包交付前再跑一遍完整检查5.1 用断言校验面板主键与数值边界清洗完成后不要急着交付。先写一组断言把“能通过”变成硬性条件再往下跑。assert merged[地级市_当前].notna().all(), 存在空地级市 assert merged[year].between(2007, 2023).all(), 年份越界 assert merged[[地级市_当前, year]].duplicated().sum() 0, 主键不唯一 assert (merged[balance] 0).all(), 余额列出现负数这些检查比随机抽查可靠主键不唯一是最隐蔽的问题多出现在区划调整年份负数则是口径清洗阶段流量和存量混用的典型后遗症。断言的错误信息要写清楚不要只抛一个 AssertionError。5.2 重打包时设置压缩级别与可选口令交付时直接把最终 CSV 和字段字典写回一个新的 zip不要在原始 zip 上增删。写回时用 ZIP_DEFLATED 并控制 compresslevel既能压缩又不会让大文件卡死。with zipfile.ZipFile( 地级市债务面板_清洗版.zip, w, zipfile.ZIP_DEFLATED, compresslevel6, ) as zf: zf.write(地级市债务_面板.csv, data/地级市债务_面板.csv) zf.write(字段字典.csv, doc/字段字典.csv)compresslevel6 是压缩率与速度的默认平衡点。如果交付文件走网盘分发还可以用 setpassword(byour-pass) 给整个压缩包加口令。工程上我不推荐依赖 zip 密码移除工具做后处理更常见的做法是明确口令规则、写进交付说明避免接收方二次解压成本过高。5.3 跑 testzip 确认交付物完整重打包完成后再用 testzip() 自检一次作为交付前最后一道工序。这个动作不是形式主义因为写回过程中一旦进程被杀或磁盘占满zip 的中央目录已经写入但部分数据未落盘系统仍然会判定写入成功。testzip 返回 None说明这个 zip 可以直接交给下游。本文还有配套的精品资源点击获取