ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

中国地面气候日值数据集V3.0处理指南:缺测值与格式陷阱详解

中国地面气候日值数据集V3.0处理指南:缺测值与格式陷阱详解 干过中国地面气候日值数据集(V3.0)的人多少都经历过这种崩溃瞬间明明从数据网下载了标准化产品跑出来的气温曲线却直接飙到三千多摄氏度降水序列里无缘无故出现一条四位数毫米的“极端暴雨”。我最早处理这批数据的时候第一反应也以为是文件损坏反复下载了三次最后才在说明文档里发现是自己没处理缺测标记。这个数据集本身质量是靠谱的真正出问题的几乎都在处理流程里——文件结构、缺测值、站点匹配、时间解析这几个环节只要有一个偷懒后面所有分析都会跟着翻车。这篇文章把我这些年趟过的坑整理了一遍。不管你是做气象、农业、水文还是环境相关的研究只要需要跟V3.0日值数据打交道这些经验基本都能用得上。我会把每个坑的成因讲清楚再给出对应的解法最后附一套比较完整的Python读取流程你可以直接拿去做底子改。1. 误读文件结构后面的全白搭1.1 行是“宽表”还是“窄表”先别急着 read_csvV3.0数据集最常见的发布形态是文本文件文件名通常类似SURF_CLI_CHN_MUL_DAY_XXXX_YYYY.txt这种规则。但这里有个很坑的地方同是V3.0不同要素打包下载后文件结构可能不一样。有的文件是每一行一个站点一天把所有要素横着铺开有的则是把要素拆成多个文件每个文件里列更少。很多人不看数据说明默认拿pandas.read_csv()一把梭结果要么列数对不上要么某一行因为值里有多个连续空格而解析串位。我踩过的具体例子是这样的有的行在要素字段中间出现了多个空格用默认的分隔符读不进来pandas会把整行塞进一个单独的列还有的文件开头带有一个简短的说明行或者表头行如果没加skiprows或者comment参数第一行数据就直接变成列名了。提示拿到文件第一件事不是跑脚本而是用文本编辑器打开看前二十行加上head -20或记事本直接看。确认清楚有没有表头、分隔符是空格还是逗号、一行多少列再写读取代码。1.2 站号、经纬度、海拔和日期列最容易错位V3.0很多版本把站点信息放在数据行的最前面几个字段比如“站号、纬度、经度、海拔高度、年、月、日”后面才是要素值。这种结构本身不复杂但坑在于如果某个站点某一天所有要素全部缺测文件里可能只写了站点和日期后面的字段直接用一堆分隔符空着。用正则分隔符sepr\s读取时空字段会被跳过去导致这一行的实际列数比表头少pandas读进来之后整行错位。更隐蔽的情况是经纬度和海拔的数值长度不固定。纬度可以是39.93也可以是39.9333海拔可以是54.7也可以是-154.0。如果靠固定字符位置去切分很容易在边界字段上切出半个数字。所以我建议优先按空白符切分但切分之前先做一个数据清洗把连续多个空格替换成统一的一个空格再检查每行分出来的字段数量是否等于标准列数。import re with open(SURF_CLI_CHN_MUL_DAY_SURF_2020.txt, r, encodinggbk) as f: lines f.readlines() standard_cols 20 # 以你的数据说明为准 for i, line in enumerate(lines[:100]): parts re.split(r\s, line.strip()) if len(parts) ! standard_cols: print(f第{i1}行字段数异常: {len(parts)})那段re.split(r\s)的作用是把连续空格当成一个分隔符来切分比split()更稳。如果报出异常行再人工去看具体格式不要直接进后续处理。2. 把缺测值当成真实观测32744、32766、32755背后的数据逻辑2.1 三种缺测标记到底怎么区分V3.0数据集里的缺测值是一个大坑而且坑得很系统。常见的缺测标记有32744、32766、32755这三种它们并不是同一个东西32744表示该要素在当日无观测或观测无效可以理解为“真正的缺测”。32766在很多要素里同样表示缺测但它的语义在某些文件中被定义为“数据未录入”或“极值要素无效”。32755更多出现在降水、风等要素中表示“无此现象”比如当日无降水、无大风过程并不是缺测。还有一个容易被忽略的问题这些缺测标记在文件里通常是以整数形式存储的但气象要素往往有量纲和换算关系。比如气温在V3.0里经常用0.1℃为单位存储也就是说文件里的数值需要除以10才是实际温度。如果你不做缺测判断直接先处理单位换算32744就会变成3274.4℃看起来像是地球进入了炼狱模式。提示处理顺序必须是“先判断缺测再缩放量纲”。也就是先把32744、32766、32755这些标记值替换成NaN剩余有效值再做除以10、除以100之类的运算。反过来就会把缺测值放大后面所有统计全部污染。2.2 缺测值误当真值后会产生哪些次生错误把缺测值当真实观测造成的后果远不止画图难看。最典型的是计算气候极值和累加值算年降水总量时如果某个月有几天是32755无降水或32744缺测你直接把所有大于三位数的值放进求和公式年降水总量能出来一个比长江流域多年平均还高的数字。算极端气温出现的频率时数值都是四位数排序后前一百名全被缺测值霸榜真实极值反而排不进去。另一个隐蔽的次生错误是空间插值。很多人拿到数据后急着插值成格点直接把含缺测标记的原始值丢进插值算法。结果就是缺测站周围出现一个以32744℃为中心的“热岛”整张插值图的色标都会被扯爆。插值前必须删掉缺测站或者对该站做时间填补否则结果没有任何可信度。2.3 缺测处理策略删、留、补要分场景缺测之后到底该怎么处理没有统一答案要看你的分析目标算平均值、趋势分析建议保留缺测为NaN用pandas的mean(skipnaTrue)计算让每个站点的样本量自然不同。要注意缺失比例高的站点需要单独标记别混在一起做结论。算年总量、累计值如果当月缺测天数较多年总量会有偏低的系统误差。可以设置一个阈值比如某月缺测超过5天该月总量标记为不可用年总量也标记为缺测。做时间序列模型可以考虑插值但插值方法要谨慎。逐日气温有强烈的季节周期线性插值对短期空缺可用对长期空缺建议用多年同日平均值填充并加上插值标记。我自己在建数据集的时候习惯把一个原始缺测标记列保留下来这样后续无论做哪一步分析都能随时回头定位哪些值是原始记录、哪些是填补出来的。别把缺测信息删得一干二净否则后面审稿人问起数据来源就麻烦了。3. 站号匹配与站点元数据的“假匹配”3.1 站号要用字符串而不是整数V3.0数据集的站号是五位区站号比如北京是54511上海是58362。表面上看是数字但处理时我强烈建议直接用字符串读取。原因在于有些站号开头是0比如部分区域的站点号是0开头整数读取后前导零直接丢失匹配站点元数据时就会对不上。更麻烦的是不同渠道下载的站点元数据文件比如站号、站名、经纬度、海拔的对照表可能是CSV格式站号同样需要读成字符串。如果你一边用int读数据文件、一边用带前导零的字符串读元数据两张表永远匹配不上还很难排查。统一用字符串是成本最低的解法。df_data[STN] df_data[STN].astype(str).str.zfill(5) df_meta[Station_Id_C] df_meta[Station_Id_C].astype(str).str.zfill(5)zfill(5)的作用是把站号补足到5位保证像05032这样的站号不会变成5032。这一步看起来简单但能省掉后面大量merge后出现NaN的心力交瘁。3.2 经纬度范围与海拔负值的判断陷阱做数据质量检验的时候很多人喜欢用“数据是否在物理可能范围”来筛异常值。这个方法对经纬度和海拔来说很好用但要注意边界条件。中国区域气象站的纬度大体在18度到54度之间经度在75度到135度左右你可以用这样的范围做粗筛。但这里藏着一个特别容易误伤的坑海拔可以为负值。新疆吐鲁番盆地内的气象站海拔就在海平面以下比如著名的东坎站海拔是负几十米。如果你写了一个if elev 0: 判定为异常的规则这些站会整批被误杀。合理的海拔判断范围应该是-200到9000米而不是简单的大于零。另外注意有些文件里缺测的经纬度是用32744或9999来表示的。如果拿这种值去画站点分布图图上会冒出一个坐标在经度3274度的“幽灵站”。所以经纬度的缺测判断也要走同一个缺测处理逻辑。3.3 台站迁址给连续序列造成的断层这可能是最难发现的坑之一。气象站在几十年间经常会有迁址有的只是平移几百米有的则从市区迁到郊区。V3.0数据在站号上不一定区分迁站前后的变化但台站元数据文件里通常记录了站址变动信息。如果你只按站号合并数据可能把两个不同地理位置的数据当成同一站点的时间序列来用。实际处理时我建议在合并站点元数据之后检查每一站的经纬度序列是否有突然跳变。如果某站经纬度在某个日期后发生明显变化就要单独标记为“迁站”在长时间序列分析时要么截断、要么做均一化订正。否则你统计出的气候趋势可能只是迁站带来的虚假变化。4. 时间字段没有你想象的那么简单4.1 类型溢出和日期拼装错误V3.0日值数据的时间字段通常就是年、月、日三个整数列看起来人畜无害。但常见的一个坑是有人为了拼接方便直接写year * 10000 month * 100 day得到20200101这种整数日期。这个做法本身没有问题问题出在你把它当成普通整数去排序、画图的时候。比如20200101和20201231之间差值不是“日期间隔”而是一个毫无意义的整数差12130。更稳妥的做法是从一开始就转成pandas的datetime64类型import pandas as pd df[DATE] pd.to_datetime(df[[YEAR, MONTH, DAY]])pd.to_datetime直接传入年份、月份、日期三列的DataFrame子集就能自动拼装成标准日期。别小看这一步很多后续时间重采样、按季节分组、画时间序列图都依赖真正的日期类型。4.2 闰年、月份缺失与“无记录”的区别日值数据的另一个经典问题是没有记录和缺测的区别。有时某一天在原文里根本没有这一行这时你如果用reindex补全日期范围要特别小心没有行不等于缺测值它可能表示该站在这一天没有上传记录也可能表示观测站还没建立或已经撤销。比如建站时间在1980年你在做1960—2020年的连续序列时就别去补1960—1979年的数据。还有一种情况是某月数据里缺少31日的记录这不是数据缺失而是当月本来就没有31日。要避免在拼装完整日期索引时给2月强行加上30日、31日。处理技巧是先groupby(STN)对每个站点单独构造完整日期范围然后merge回去。这样每个站点的时间跨度是独立的不会因为站点建站时间不同而互相污染。4.3 时间连续性核查的正确做法在进入正式分析之前我建议做一次时间连续性检查。逐站点统计日期是否有重复、是否有缺失日期dup df.groupby([STN, DATE]).size() dup dup[dup 1] print(重复日期数量:, len(dup)) missing_dates [] for stn, grp in df.groupby(STN): full_idx pd.date_range(grp[DATE].min(), grp[DATE].max(), freqD) miss full_idx.difference(grp[DATE]) missing_dates.append((stn, len(miss)))这段代码跑出来的结果能帮你快速定位“哪些站点在哪个时间区间是断档的”。我实测中最常见的断档集中在站点建站初期和特殊年份比如部分台站在上世纪六七十年代缺测较多。明确断档后在后续分析里可以对这些区间单独做标记而不是笼统地当成正常序列参与计算。5. 质量控制标记、风速风向和降水量的隐藏细节5.1 质量控制标记不能一刀切丢弃V3.0的有些版本里每个要素后面带有质量控制标记字段。很多人看到标记就直接删除或者只保留质量等级最高的数据。问题是质量控制标记的语义并不是“1好2坏”这么简单。有的标记体系里0表示数据正确、1表示数据可疑但经过人工审核、2表示数据错误已被替换。如果一刀切只保留0你会丢弃大量经过人工订正的可用数据样本量缩小到没法做统计。处理方式建议是把质量控制标记单独存成一列在最终筛选时记录一下删除了多少样本、删除比例有多高。如果删除比例超过某个阈值就要回头想想是不是标记理解错了。凡是涉及“删除数据”的操作都要保证能回溯、能解释这在论文审稿阶段尤其重要。5.2 典型要素的精度与量纲V3.0数据中不同要素的量纲和缩放比例并不统一这是绕不开的细节。以我常处理的要素为例要素存储单位实际物理单位缩放处理平均气温0.1℃℃除以10降水量0.1mmmm除以10平均风速0.1m/sm/s除以10日照时数0.1hh除以10平均气压0.1hPahPa除以10这个问题看似简单但一旦某个要素不需要除以10或者需要乘以某个系数你的整列数据就会系统性偏大或偏小。判断办法是看有效值的数值范围气温合理范围基本在-50到50度之间如果你看到文件里的有效气温在-500到500之间那它一定是按0.1℃存储的。5.3 极端值校验中容易被误杀的合法记录很多人做数据清洗时会用“超过X倍标准差”来剔除异常值。这个操作对气象日值数据来说风险很高。逐日气温本身有强烈的季节变化冬季和夏季的均值差个三四十度很正常你拿全年所有日值算一个标准差然后认为超过5倍标准差就是异常这会在冬季误杀寒潮、在夏季误杀热浪。我试过的可行思路是先按站点、按月份分组再在每个组内计算均值和标准差然后判断离群值。这样能保留季节信号只剔除组内真正离谱的值。比如1月气温组内出现一个等于32744的值在缺测判断之后它已经被换成NaN不会被误杀但如果缺测判断没做它绝对会出现在异常值名单里。对降水要素更要小心因为降水量分布高度右偏均值加几倍标准差的判据对强降水极值极不友好。要么使用百分位数法要么只对小于0的值做物理一致性判断对大的正值先保留再结合人工审计。6. 一套已验证的V3.0数据读取流程附Python脚本6.1 步骤一先“探伤”再读入在正式读取之前我先做一次全文件的格式探测把列数异常、非数值内容、编码问题一次性暴露出来。V3.0旧版本的文件很多是GBK或ANSI编码直接用UTF-8读会报错所以读取时要指定编码。探测脚本大概长这样import pandas as pd import numpy as np file_path SURF_CLI_CHN_MUL_DAY_SURF_2020.txt with open(file_path, r, encodinggbk, errorsreplace) as f: lines f.readlines() field_counts [len(line.split()) for line in lines if line.strip()] from collections import Counter print(Counter(field_counts))这一段的目的是看字段数分布。如果绝大多数行都是同一个列数极少数行不一致那基本可以确定是缺测导致的分隔符跳变。如果列数分布很散就要重新核对数据格式说明。6.2 步骤二缺测替换与比例因子还原确认格式之后按列名读入然后做缺测替换。这里我把关键操作都写清楚raw_cols [STN, LAT, LON, ELEV, YEAR, MONTH, DAY, PRS, TEM, RHU, PRE, WIN, SSD] df pd.read_csv(file_path, sepr\s, headerNone, namesraw_cols, encodinggbk, dtype{STN: str}) missing_vals [32744, 32766, 32755] df df.replace(missing_vals, np.nan) scale_map {PRS: 0.1, TEM: 0.1, RHU: 1.0, PRE: 0.1, WIN: 0.1, SSD: 0.1} for col, scale in scale_map.items(): df[col] pd.to_numeric(df[col], errorscoerce) * scale关于缩放因子的提醒RHU相对湿度在部分版本里已经是百分数整数不需要除以10有些版本又是0.1%。所以不要照抄我这里的表拿到你自己的数据后务必先看说明文档再做一次统计量验证。6.3 步骤三元数据校验与最终落盘读取并缩放完之后做一个最终校验检查站点数、时间范围、经纬度范围、每个要素的非缺失样本量。校验通过后把数据按“站点日期”排好序保存为parquet或netCDF格式方便后续调用。df[DATE] pd.to_datetime(df[[YEAR, MONTH, DAY]]) df df.sort_values([STN, DATE]).reset_index(dropTrue) report df.groupby(STN).agg( start(DATE, min), end(DATE, max), count(DATE, nunique), missing_TEM(TEM, lambda s: s.isna().sum()) ) print(report.head()) df.to_parquet(clim_day_v3_clean.parquet)我输出的这份文件里保留了经纬度和海拔列也保留了缺测转换后的NaN这样后续无论是做站点级别分析还是做区域平均、空间插值都能从同一份干净数据出发。建议不要在这一步把DATE设成索引因为多站点长序列数据用STN DATE两列作为键更灵活。最后再分享两个小经验。一是拿到V3.0数据后建议在下一次下载时对比一下文件字节数和行数如果差异异常大说明你中间的解析逻辑可能把数据弄丢了。二是处理过程里不要怕多打印中间统计量哪怕只是print(df.shape)和print(df.isna().sum())都能帮你尽早发现上面的坑。这个数据集的坑基本都藏在细节里把格式和缺测问题解决掉后面多数分析其实就不怎么需要折腾了。
返回列表