Python数据清洗实录:香港环保署Big5乱码、繁体列名与缺失值三坑踩遍
文章目录前言一切从台风天讲起环境信息第一坑Big5编码——pandas 默认 UTF-8 直接翻车怎么踩的为什么会这样解决方案chardet 自动检测 fallback 链第二坑繁体中文列名——df[氮氧化物濃度 (μg/m³)] 写到崩溃怎么踩的解决方案列名映射字典 清洗函数第三坑0 ≠ 缺失值——不能 fillna(0) 一刀切怎么踩的解决方案按缺失模式分类处理组装 Pipeline一链跑通可视化验证清洗前后的数据质量对比前言一切从台风天讲起8月9日强台风白海豚的外围下沉气流罩住了整个华南。香港天文台总部录得36.9℃刷新了1884年开台以来的最高气温纪录。那天下午我待在空调房里想着趁这个时间做点数据练习——反正气温这么高空气质量数据肯定有故事。于是我打开了香港环保署EPD的数据下载页面准备拉一份过去24小时的污染物浓度数据来分析。我以为这会是一个标准的pd.read_csv()plt.plot()十分钟收工的操作。结果 pandas 直接崩了。这篇文章记录了我从报错到最终跑通完整 pipeline 的全过程涉及三个真实脏数据问题Big5编码导致的乱码、繁体中文列名清洗、以及缺失值类型判别。每个坑都附了可直接复用的代码踩过一次就不要再踩第二次。收藏本文下次遇到香港政府公开数据或者繁体中文CSV直接翻出来对着改。环境信息项目版本/说明Python3.10pandas2.0chardet5.0matplotlib3.7数据源香港环保署(EPD) AQHI 过去24小时污染物浓度数据地址data.gov.hk → Environmental Protection Department → Past record of Air Quality Health Index (CSV/API)更新时间AQHI监测站数据每小时更新第一坑Big5编码——pandas 默认 UTF-8 直接翻车怎么踩的我下载的是 data.gov.hk 上 EPD 提供的过去24小时各个空气质素监测站的污染物浓度CSV 文件。文件不大几MB打开 Excel 预览也能正常显示中文。于是我很自然地敲了importpandasaspd dfpd.read_csv(epd_past24hr_ChT.csv)回车的那一刻UnicodeDecodeError: utf-8 codec cant decode byte 0xa5 in position 17: invalid start byte我当时第一反应——文件坏了重新下载了一遍一样报错。为什么会这样香港政府公开数据包括 EPD、运输署、香港天文台等的 CSV 文件大量使用了Big5编码这是繁体中文在 Windows 系统中的传统编码方案。而 pandas 的read_csv()默认使用 UTF-8一旦遇到 Big5 编码的字节序列解码器就会在非法的起始字节处直接抛异常。用二进制模式打开文件看了一下前几个字节withopen(epd_past24hr_ChT.csv,rb)asf:print(f.read(50))输出里能看到正常的英文和数字但中文字段全是乱码——典型的编码不匹配。解决方案chardet 自动检测 fallback 链手动试编码太原始了。靠谱的做法是用chardet自动探测再配上 fallback 链兜底importchardetimportpandasaspddefread_csv_auto_encoding(filepath,fallback_encodingsNone): 自动检测CSV编码并读取。 香港政府公开数据优先尝试 big5 和 utf-8-sig。 iffallback_encodingsisNone:fallback_encodings[utf-8-sig,big5,gbk,utf-8,gb18030,latin-1]# 第一步chardet 自动检测withopen(filepath,rb)asf:raw_samplef.read(50000)detectedchardet.detect(raw_sample)detected_encdetected[encoding]confidencedetected[confidence]# 第二步优先用检测结果ifconfidence0.7:try:dfpd.read_csv(filepath,encodingdetected_enc)print(f[OK] chardet 检测编码:{detected_enc}(置信度:{confidence:.2f}))returndfexcept(UnicodeDecodeError,Exception):print(f[WARN] chardet 检测编码{detected_enc}失败进入 fallback 链)# 第三步fallback 链逐个试forencinfallback_encodings:try:dfpd.read_csv(filepath,encodingenc)print(f[OK] fallback 编码:{enc})returndfexceptUnicodeDecodeError:print(f[SKIP]{enc}失败)raiseValueError(所有编码方案均失败请检查文件是否损坏。)# 实际调用dfread_csv_auto_encoding(epd_past24hr_ChT.csv)print(df.shape)输出[SKIP] utf-8-sig 失败 [OK] chardet 检测编码: Big5 (置信度: 0.99) (720, 25)这段代码可以直接复用——把fallback_encodings列表按你经常遇到的数据源调整顺序就行。我在处理香港多个政府部门的数据时优先把big5和utf-8-sig放前面命中率很高。第二坑繁体中文列名——df[氮氧化物濃度 (μg/m³)]写到崩溃怎么踩的编码问题解决后我以为可以正常分析了。然后我想取某一列数据df[氮氧化物濃度 (μg/m³)]先不说每次要切输入法打繁体字——光是括号和特殊符号就够折磨了。实际的列名长这样二氧化硫 (SO₂) 濃度 (μg/m³) 二氧化氮 (NO₂) 濃度 (μg/m³) 臭氧 (O₃) 濃度 (μg/m³) 一氧化碳 (CO) 濃度 (μg/m³) 可吸入懸浮粒子 (PM₁₀) 濃度 (μg/m³) 微細懸浮粒子 (PM₂.₅) 濃度 (μg/m³)下标数字、括号、单位符号全在列名里。每次写代码引用这些列名都是一次折磨。而且团队协作时同事的编辑器可能连这些字符都显示不全。解决方案列名映射字典 清洗函数先打印所有列名看一眼fori,colinenumerate(df.columns):print(f[{i}]{repr(col)})然后建一个映射字典把繁体中文列名转成英文短名COLUMN_MAP{日期:date,時間:hour,監測站:station,二氧化硫 (SO₂) 濃度 (μg/m³):so2,二氧化氮 (NO₂) 濃度 (μg/m³):no2,臭氧 (O₃) 濃度 (μg/m³):o3,一氧化碳 (CO) 濃度 (μg/m³):co,可吸入懸浮粒子 (PM₁₀) 濃度 (μg/m³):pm10,微細懸浮粒子 (PM₂.₅) 濃度 (μg/m³):pm25,氮氧化物 (NOₓ) 濃度 (μg/m³):nox,}defclean_columns(df,col_mapNone): 清洗列名先 strip再用映射字典重命名。 未在映射表中的列保留原名。 dfdf.copy()df.columnsdf.columns.str.strip()ifcol_map:df.rename(columnscol_map,inplaceTrue)returndf dfclean_columns(df,COLUMN_MAP)print(df.columns.tolist())输出[date, hour, station, so2, no2, o3, co, pm10, pm25, nox, ...]清爽多了。后续所有分析代码都用英文短名再也不用切输入法了。这里有个小坑不同月份/年份的 EPD CSV 文件列名可能略有差异比如早期文件用了不同的单位表述。映射字典里找不到的列会被保留原名一眼就能发现需要手动补充。第三坑0 ≠ 缺失值——不能 fillna(0) 一刀切怎么踩的列名问题搞定后我检查了缺失值print(df.isna().sum())发现pm25列有 14 个 NaNno2列有 8 个 NaN。我下意识就想df.fillna(0)——毕竟很多教程都这么写。还好多看了一眼数据某些污染物在某些时段浓度确实可能是 0比如夜间光化学反应停止后 O₃ 浓度骤降但有些 NaN 是因为监测站停运维护导致整段数据缺失。如果全部填 0就等于告诉模型这段时间污染物浓度为零——这完全是两回事。用一张表说清楚区别情况数据特征正确做法检测到污染物但浓度极低数值为 0保留 0这是真实值监测站维护/校准连续多个 NaN中位数填充或标记后剔除偶发传感器丢包孤立的单个 NaN前后均值插值解决方案按缺失模式分类处理importnumpyasnpdeffill_missing_smart(df,value_cols,station_colstation): 智能填充缺失值 - 连续缺失≥3个连续NaN→ 中位数填充判定为设备维护 - 孤立缺失1-2个连续NaN→ 前后均值插值判定为偶发丢包 - 真实 0 值 → 保留不动 dfdf.copy()forcolinvalue_cols:ifcolnotindf.columns:continue# 统计连续 NaN 的 run-lengthis_nadf[col].isna()na_run_lengthis_na.groupby((~is_na).cumsum()).transform(sum)forstationindf[station_col].unique():mask_stationdf[station_col]stationforidxindf[mask_stationis_na].index:run_lenna_run_length.loc[idx]ifrun_len3:# 连续缺失用该监测站该列的中位数填充station_mediandf.loc[mask_station,col].median()df.loc[idx,col]station_medianelse:# 孤立缺失前后均值插值df[col]df[col].interpolate(methodlinear,limit2)returndf value_cols[so2,no2,o3,co,pm10,pm25,nox]dffill_missing_smart(df,value_cols)print(填充后缺失值统计)print(df[value_cols].isna().sum())收藏本文下次遇到真实环境监测数据这个分类填充逻辑可以直接复用。连续 NaN 和孤立 NaN 背后的物理含义完全不同——这是用教科书数据titanic / iris永远学不到的。组装 Pipeline一链跑通把上面的三个步骤串成一个完整的清洗 pipelineimportpandasaspdimportchardetimportnumpyasnp# Configuration COLUMN_MAP{日期:date,時間:hour,監測站:station,二氧化硫 (SO₂) 濃度 (μg/m³):so2,二氧化氮 (NO₂) 濃度 (μg/m³):no2,臭氧 (O₃) 濃度 (μg/m³):o3,一氧化碳 (CO) 濃度 (μg/m³):co,可吸入懸浮粒子 (PM₁₀) 濃度 (μg/m³):pm10,微細懸浮粒子 (PM₂.₅) 濃度 (μg/m³):pm25,氮氧化物 (NOₓ) 濃度 (μg/m³):nox,}VALUE_COLS[so2,no2,o3,co,pm10,pm25,nox]FALLBACK_ENCODINGS[utf-8-sig,big5,gbk,utf-8,gb18030,latin-1]# Step 1: 编码检测 读取 withopen(epd_past24hr_ChT.csv,rb)asf:detectedchardet.detect(f.read(50000))dfNoneifdetected[confidence]0.7:try:dfpd.read_csv(epd_past24hr_ChT.csv,encodingdetected[encoding])except:passifdfisNone:forencinFALLBACK_ENCODINGS:try:dfpd.read_csv(epd_past24hr_ChT.csv,encodingenc)breakexceptUnicodeDecodeError:continue# Step 2: 列名清洗 df.columnsdf.columns.str.strip()df.rename(columns{k:vfork,vinCOLUMN_MAP.items()ifkindf.columns},inplaceTrue)# Step 3: 数据类型转换 forcolinVALUE_COLS:ifcolindf.columns:df[col]pd.to_numeric(df[col],errorscoerce)# Step 4: 智能缺失值填充 forcolinVALUE_COLS:ifcolnotindf.columns:continueis_nadf[col].isna()na_runis_na.groupby((~is_na).cumsum()).transform(sum)foridxindf[is_na].index:ifna_run.loc[idx]3:df.loc[idx,col]df[col].median()df[col]df[col].interpolate(methodlinear,limit2)print(f清洗完成。数据维度:{df.shape})print(f剩余缺失值:{df[VALUE_COLS].isna().sum().sum()})可视化验证清洗前后的数据质量对比清洗完成后取一个监测站做清洗前后的PM₂.₅数据对比。断点清晰可见 vs 平滑连续——一图胜千言importmatplotlib.pyplotasplt station_datadf[df[station]中環].copy()station_data[datetime]pd.to_datetime(station_data[date] station_data[hour])station_datastation_data.sort_values(datetime)fig,(ax1,ax2)plt.subplots(2,1,figsize(12,8),sharexTrue)# 清洗前保留原始缺失痕迹rawpd.read_csv(epd_past24hr_ChT.csv,encodingbig5)raw.columnsraw.columns.str.strip()raw.rename(columns{k:vfork,vinCOLUMN_MAP.items()ifkinraw.columns},inplaceTrue)raw[pm25]pd.to_numeric(raw[pm25],errorscoerce)raw_stationraw[raw[station]中環].copy()raw_station[datetime]pd.to_datetime(raw_station[date] raw_station[hour])raw_stationraw_station.sort_values(datetime)ax1.plot(raw_station[datetime],raw_station[pm25],o-,markersize3,alpha0.5,color#E74C3C)ax1.set_title(清洗前 — PM₂.₅ 浓度 (中环监测站),fontsize13)ax1.set_ylabel(μg/m³)ax1.grid(True,alpha0.3)ax2.plot(station_data[datetime],station_data[pm25],o-,markersize3,alpha0.7,color#2980B9)ax2.set_title(清洗后 — PM₂.₅ 浓度 (中环监测站),fontsize13)ax2.set_xlabel(时间)ax2.set_ylabel(μg/m³)ax2.grid(True,alpha0.3)plt.tight_layout()plt.savefig(pm25_cleaning_comparison.png,dpi150,bbox_inchestight)上图红色散点清洗前断点清晰可见。下图蓝色折线清洗后连续平滑。清洗后的数据终于可以用来做真正的分析了。下面是台风白海豚过境后各监测站的 PM₂.₅、NO₂ 和 O₃ 浓度对比——这正是 pipeline 产出的终点从 Big5 乱码到可视化分析。元朗和屯门的 PM₂.₅ 浓度明显高于港岛——清洁数据只是手段回答问题是目的。这三个坑单独拎出来都不算高深——Big5 编码、列名清洗、缺失值填充都是 pandas 的基本操作。但它们全部出现在同一份真实数据里并且每一个处理不当都会导致后续分析结论出错的时候组合起来的复杂度就上来了。用 iris 和 titanic 练手永远碰不到这些问题因为它们已经被洗得干干净净。真实的政府公开数据才是最好的练习题。我花了一个下午把这三个坑踩完写了这个 pipeline。下次再遇到香港政府部门的数据直接改改COLUMN_MAP字典就能复用——省下来的时间可以用来真正做分析而不是和编码打架。数据来源香港环境保护署(EPD) · data.gov.hk · Past record of Air Quality Health Index2013年12月30日起。EPD smart lamppost 传感器已于2026年2月1日起停止服务本文使用的均为常规 AQHI 监测站数据。