ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

中国地面气候日值数据集V3.0处理避坑指南:缺测值、质控码与批量操作

2026/9/18 12:59:07 拓冰建站 浏览量
中国地面气候日值数据集V3.0处理避坑指南:缺测值、质控码与批量操作 做气候数据处理的人十有八九都跟“中国地面气候日值数据集(V3.0)”打过交道。这套数据由气象部门整理发布要素全、序列长、站点多做气象、水文、农业、生态分析基本绕不开它。但说实话质量好的数据不代表好处理V3.0的门槛不在数据本身而在数据说明、格式细节和那些藏在角落里的特殊编码。我见过不少人拿着别人分享的代码跑出“看似合理”的结果最后因为缺测值没处理、站点号被转成整数导致连接错位、质控码被当成气象要素参与统计整个结论全部作废。这篇东西就是我个人踩坑的记录。我不会从“什么是气象数据”这种基础概念讲起而是直接进入实战数据文件怎么读、缺测值怎么识别、质控码怎么理解、降水微量怎么区分、批量处理怎么提升效率最后附一张错误速查表。不管你是刚接触这套数据的研究生还是已经写了一年处理脚本的工程师只要按着这个思路走一遍能省下大量无意义的Debug时间。1. 拿到数据集后的第一关读懂文件结构和数据说明1.1 数据说明才是真正的“避坑地图”每次看到有人下载完数据集二话不说直接解压跳过说明文档就开始读数据我就知道后面大概率要出事。V3.0数据集压缩包内除了逐站数据文件一定有几份关键的说明文件比如数据集说明、站点信息表、质量控制码说明。这些文档表面上不起眼实际上决定了你后续所有处理逻辑是否正确。先说文件组织方式。V3.0日值数据集通常是一个站点一个文本文件或者按要素、按年份拆分文件。文件名以区站号命名例如“54511.txt”代表北京站的逐日记录。文件内部每行对应一个观测日列之间用空格或制表符分隔依次包含区站号、纬度、经度、观测场海拔高度、年、月、日、各要素观测值、质量控制码等信息。我强烈建议拿到数据的头十分钟不要急着写代码先把说明文档完整读一遍。重点看三样东西缺测值标注方式、质控码含义、要素单位。V3.0里面各要素的缺测值通常用“32766”“32744”“9999”这类特殊大数表示不是空字符质控码有专门一列含义可能是0正确、1可疑、2错误也可能因版本不同略有差异一切以配套文档为准。不看说明就写代码等跑出负数降水、上千毫米降水的时候再回头查文档就晚了。1.2 别用记事本看大文件先搞清楚编码和分隔符V3.0的文本文件在Windows环境下生成编码经常是GBK或GB18030直接按UTF-8读取大概率报解码错误或者更糟读进来一堆乱码但不报错。处理办法是读文件时显式指定编码import pandas as pd df pd.read_csv( 54511.txt, sep\\s, # 兼容多个连续空格或制表符 encodinggb18030, enginepython, headerNone )第二个坑是分隔符。这套数据的列之间不是严格的单空格而是多个空格或缩进导致直接用sep 会解析出大量空列。用sep\\s配合enginepython能稳定处理连续空白但如果文件特别大python引擎会比较慢。这时候改用read_fwf按固定宽度读取会更可靠——前提是你已经从说明文档里拿到了准确的列宽。import pandas as pd df pd.read_fwf( 54511.txt, widths[5, 8, 8, 7, 4, 2, 2, 8, 8, 8], encodinggb18030 )注意不同年份、不同版本的V3.0文件列数和列宽可能有细微差别。最稳妥的方式是先打印几行原始文本数清楚列号再选择read_csv或read_fwf。这十几分钟的前期确认能让你后面少折腾好几个小时。2. 高频翻车点之一缺测值当成正常数值参与计算2.1 32766、32744、9999到底怎么处理这个坑我见得最多。V3.0里缺测值和微量降水等特殊观测值是用特定的大数标识的。举个例子气温要素的缺测值经常是32766降水要素里微量降水可能用32744或32700系列表示还有一些版本把“无降水”记成“0”把“微量降水”单独编码。如果这些值不处理直接参与求和、平均、极值统计结果会非常离谱。拿平均气温举例某天缺测气温字段写的是32766假设当天真实温度是20度你直接求整月平均一个32766能把月均值拉到上千度任何统计分析都没有意义。更隐蔽的是用Pandas读取时这些大数会被识别为整数或浮点反而让代码“正常”地跑出来一个错得离谱的结果没有任何报错提示。我的做法是读取完原始数据后立刻做一个缺失值映射步骤把这些特殊值统一转成NaN。import numpy as np import pandas as pd # 缺测值列表根据V3.0说明文档和实际文件中出现的特殊值灵活补充 missing_values [9999, 32700, 32744, 32766, 32744, 32766] df.replace(missing_values, np.nan, inplaceTrue)这里有个细节不同要素的缺测编码未必一样有些要素用32766有些用32744还有用9999的一定不能只用一张固定列表。正确做法是先加载数据统计每个要素列的取值分布找出那些“异常大”的极值再对照说明文档确认最后统一替换。2.2 质控码是单列还是伴随列别把质控信息当气象值V3.0的一个典型特点是每个要素后面经常伴随一个质量控制码列。比如气温列后面跟着气温质量控制码降水列后面跟着降水质量控制码。质量控制码用来标记该时次数据的可靠程度通常0表示数据正确1表示可疑2表示错误8表示缺测具体定义以文档为准。很多新手不区分这些列读完数据后把所有列都当成数值型气象要素参与绘图、统计结果图上出现一条“质控码序列”或者质控码被求了平均莫名其妙得到一个“平均质控值”说出去都丢人。所以我建议在读文件时就给列名做好规划把质控码和要素值从逻辑上分开df.columns [ station, lat, lon, elev, year, month, day, tem, tem_qc, pre, pre_qc, wind, wind_qc ]处理时可以先按质控码过滤掉不可靠的记录再填充缺测值。比如只保留质控码为0的数据df df[(df[tem_qc] 0) | (df[tem].isna())] df[pre] df[pre].where(df[pre_qc] 0, np.nan)注意过滤质控码和替换缺测值这两步的先后顺序不同场景结论不一样。做站点气候态统计时我通常先替换缺测为NaN再按质控码把“可疑”和“错误”的数据也置为NaN最后统一做描述统计。但如果是做极端事件提取则需要保留“可疑”数据人工复核不能一刀切。3. 高频翻车点之二降水、蒸发、日照的特殊性3.1 0毫米降水与微量降水不能混为一谈降水数据的处理是重灾区。V3.0里“0”通常表示当天无降水“微量降水”是用某个特殊编码记录的比如32744或32700意思是降水量小于0.1毫米、雨量器测不到但确实发生了降水。这两种情况性质完全不同0代表干旱日微量代表湿润日计算降水日数、连续无降水日数、干旱事件时混在一起会让结论严重偏差。举个例子你统计某地春季降水日数如果把微量降水编码当成缺测值删掉或当成0那么3月份若干“微量降水日”就全部变成了“无降水日”连续无降水日数被拉长干旱评估结果会被高估。反过来如果直接把微量编码当作真实降水量327.44毫米参与求和那一个月的降水量能顶半年的量彻底的灾难。所以处理降水数据时我一般单独做一列# trace: 是否为微量降水 df[trace_pre] df[pre].isin([32700, 32744]).astype(int) df[pre] df[pre].replace([32700, 32744], 0.0)这样既保留了“发生过降水”这一信息又能让降水量值正确参与统计。后续写论文时也可以清楚交代微量降水的处理方式审稿人看了挑不出毛病。3.2 蒸发量和日照时数里藏着的小计量单位蒸发量和日照时数这两个要素或者单位不是常规值或者存在进阶的观测状态编码也很容易埋雷。V3.0里的蒸发量单位可能是0.1毫米或者1毫米取决于版本和站点。如果数据用整数形式存在比如“235”表示23.5毫米你直接当毫米去和其他数据对比就会整体放大10倍。日照时数也有类似问题有的版本单位是0.1小时有的直接用整数表示十分之一小时。我的经验是拿到文件后先拿已知站点、已知年份做一次简单的数量级验证。例如夏季某站日照时数不会超过14小时如果发现某天的日照值写成“136”大概率是13.6小时被放大了10倍。或者对比相邻站点的同期数据看量级是否协调。这个验证动作只要五分钟但能避免后续整套分析全部返工。还有一种情况蒸发量在结冰期会缺测或标记为负值不同版本可能有特殊编码。处理时不能只盯着气温和降水一定要看完整字段说明把所有要素的特殊状态都列出来。4. 站号、日期、经纬度最容易出错的主键与索引4.1 站号前导零丢失后所有连接操作全部错位V3.0的区站号是5位数字理论上“54511”这类站号没有前导零所以不少人习惯把它读成整数。但如果你要跟其他来源的站点数据合并比如气象站点信息表、环境监测站点坐标表那些表里的站号很多是以字符串或者Excel文本格式存储的一旦两边类型不一致连接操作要么匹配不上要么莫名其妙出现重复行。更危险的情况是某些站号确实存在前导零或者某些自定义台站编号包含字母比如区域站“A1234”。把站号当整数读前导零自动被丢弃再转回字符串时已经无法修复。我的统一做法是所有标识型字段一律用字符串读入并且在代码开头就固定这一约定。df[station] df[station].astype(str).str.zfill(5)拼接文件时也把station、year、month、day组成复合主键保证多源数据对齐不出错。4.2 日期字符串直接排序的坑字母序和日历序是两回事日期列从文本读进来后通常是整数或字符串比如“20230515”。直接用字符串排序表面上看起来对因为“2023-01-01”在字典序上确实小于“2023-12-31”。但一旦日期格式不标准或者你后续要按月份筛选、计算季节平均不做类型转换就各种别扭。正确做法是构造真正的日期时间索引df[date] pd.to_datetime( df[[year, month, day]].rename( columns{year: year, month: month, day: day} ) ) df.set_index(date, inplaceTrue) df.sort_index(inplaceTrue)处理日值转月值、季值、年值时先按日期索引重采样monthly_mean df[tem].resample(ME).mean() yearly_mean df[tem].resample(YE).mean()这里有个小细节闰年的2月29日在日值转月值时会自动归入2月如果没有统一处理可能造成2月天数在不同年份不一致。做气候平均时一般问题不大但如果做严格的水文日数统计需要决定统一使用“民用日历”还是“气候日历”并在方法部分写清楚。4.3 站点经纬度直接浮点数存储遇到坐标转换会怀疑人生V3.0的经纬度字段一般是度为单位浮点数比如“39.80”“116.47”。直接读取没问题但有人会顺手把经纬度当成普通数值求个平均产生一个“平均站点”这在空间分析里毫无意义。而且如果你要做空间插值、绘制站点分布图需要把经纬度转换成公里网格或投影坐标到时候必须注意数据集中经纬度基准是WGS84还是CGCS2000。不同版本的V3.0说明文档对此不一定明确稳妥做法是对比真实站点的坐标和第三方权威资料确认基准后再做地图投影。5. 批量处理别傻傻for循环效率、内存与可复现性5.1 多站点文件合并Pandas逐文件拼接的内存陷阱V3.0数据动辄成百上千个站点文件每个文件几十万行。最常见的新手写法是循环读取所有文件、拼接成一个超级DataFrame然后统一处理。这种做法在站点数量50以内还好一旦超过300个文件内存占用轻松吃掉十几个GB跑着跑着就死机。我的策略是“按需加载边读边算”。如果目标是生成全国站点的月平均数据不需要把所有日值都驻留在内存里完全可以逐文件处理把每个站点的月平均结果保存下来最后只合并“小结果”。from pathlib import Path import pandas as pd def process_one_file(path): df pd.read_csv(path, sep\\s, encodinggb18030, enginepython) # ... 缺测值替换、质控过滤 ... df[date] pd.to_datetime(...) monthly df.groupby(pd.Grouper(keydate, freqME))[tem].mean() return monthly.rename(path.stem) results [] for file_path in Path(data).glob(*.txt): results.append(process_one_file(file_path)) out pd.concat(results, axis1) out.to_csv(monthly_temperature_stations.csv)这样内存占用小中途出错也容易定位到具体是哪个文件出了问题不用从头排查。5.2 多进程读取和Parquet格式大数据量下的提速方案单线程逐文件读取200个文件光I/O可能就要十几分钟。更高效的做法是先用多进程并行读取再把中间结果落盘成Parquet格式后续调试就不用反复解析原始文本了。from concurrent.futures import ProcessPoolExecutor with ProcessPoolExecutor(max_workers8) as executor: monthly_list list(executor.map(process_one_file, file_list)) result pd.concat(monthly_list, axis1) result.to_parquet(monthly_temperature.parquet)这里强调两点一是process_one_file函数必须是自包含的所有导入和依赖都写在函数内部否则多进程会报错二是如果只需要单个变量的月值把结果写成窄表列名带站号后续透视和画图都方便。Parquet格式比CSV体积小、读取快算是数据处理环节“磨刀”的最佳选择。6. 我踩过的坑汇总一份可直接对号入座的排查表6.1 常见报错与解决方案对照表下面这张表是我在实际项目里反复遇到、帮别人排查时也高频出现的问题。遇到异常时先对照这张表定位大多数情况都能就地解决。现象可能原因解决方案读文件报“UnicodeDecodeError”文件是GBK编码不是UTF-8改用encodinggb18030读取读进来列数不对出现大量空列分隔符是多个空格不是单个改用sep\\s或read_fwf固定宽度读取气温月均值出现几百上千度缺测值32766没替换成NaN按文档统一替换缺测值后再做统计降水总量突然大得离谱微量降水编码被当成了毫米数值将微量降水编码分离单独标识降水日数比实际明显偏多或偏少微量降水处理与统计定义不一致明确“降水日”是否包含微量降水保持一致站号右连接后大量NaN站号一边是字符串一边是整数统一转为字符串并zfill(5)按年份分组统计时结果顺序错乱日期没转成datetime就排序构造date索引并sort_index多文件合并时内存爆掉把所有文件读入一个超大DataFrame改为边读边汇总落盘小结果月平均结果比预期偏大10倍要素单位可能是0.1毫米或0.1小时核对数据说明中的单位必要时除以10这张表不是万能的但覆盖了V3.0处理中八成以上的翻车场景。6.2 验证数据质量的小技巧五分钟发现“带毒”数据数据处理完最后一步也是最容易被跳过的一步是验证产出结果的合理性。我一般用三种方式快速校验第一极值检验。逐要素设定气候学合理范围比如气温在-60到50摄氏度之间日降水量不超过500毫米特殊情况另说超过就直接标记异常。当年我处理西北站点的气温时就是靠这个检查发现某个站点有半年的气温列全被质控码污染了。第二时间连续性检验。对每个站点检查时间索引是否连续是否有重复日期是否有“跳跃”。日值数据中间缺失几天可以接受但如果出现2023年1月1日直接跳到2023年3月1日那多半是原始文件解析出了问题。idx df.index gaps idx.to_series().diff().dt.days print(gaps.value_counts())第三空间一致性检验。把相邻站点的同期月均值画成时间序列叠在一起如果某条曲线突然跳变而周边站点没有响应八成是数据问题。这个方法看似简单但非常可靠尤其适合大范围自动批处理后的质量筛选。分享一个我自己的习惯每次处理V3.0数据我会把“原始输入文件路径、Python脚本版本、缺测值列表、质控过滤规则”全部记录在一个README文件里。这样若干个月后论文需要补充方法细节时还能准确还原当时每一步做了什么。数据处理的结果会随着代码版本、缺测规则变化而不同做好记录不是形式主义是对自己的劳动负责。最后再说一点很实际的体会气象数据处理的门槛不在于会调用多高级的模型或算法而在于理解和尊重数据本身的规则。V3.0的设计并不复杂但它承载了多年累积的观测方式、特殊编码、质量控制体系。把说明文档当作第一优先级把缺测值、质控码、站号类型这些细节当成第一等大事你的处理流程就能少走一大半弯路。希望这篇避坑指南能帮你省下几个通宵把更多精力留给真正值得研究的科学问题。