国内做气候序列分析的人,大概都经历过这种狼狈:想找一份能覆盖几十年甚至上百年、天天连续的站点数据,结果不同平台导出的文件一个格式一个样,表头还是中文缩写,下载下来光清洗字段就要一整天。我最近刚好把1942—2025年中国气象观测站点逐日气象数据从头到尾整理了一遍,从原始申请、格式统一、质控、断点排查,到最终出统计结果,踩了不少坑。这份数据以国家级地面气象观测站为主体、逐日颗粒度、覆盖1942到2025年,适合做气候趋势、极端事件和工程参数估算的人参考。这篇不打算写成官方手册,就把我在实际整理和分析里觉得最值得说的事讲清楚。
1. 从1942到2025:这份逐日数据到底装了什么
1.1 起点为什么是1942
很多人看到1942这个年份,第一反应是“怎么不从更早开始”。其实中国近代气象观测起步并不算晚,上海徐家汇一带在19世纪70年代就有人开始做连续观测,但那是分散的个别站点,谈不上国家层面的观测网络。到了上世纪40年代,国家层面的台站体系才初步成型,有一批持续记录的站能撑起来,所以很多长序列数据产品都会把起点定在这个附近。更早的资料当然有,但多数停留在纸档案里,数字化重建的成本非常高,而且站点覆盖太稀疏,分析价值有限。
这里要先打个预防针:1942年起点不等于所有站都从1942年有数据。实际上,40年代能连续保留下逐日气压、气温、降水记录的站点,全国加起来可能只有几十个到百余个,且集中在东部和中部城市。50年代之后站点数量才明显往上走。所以拿到数据后不要默认早期年份是全站齐全的,先查站数再谈分析。
1.2 逐日颗粒度承载的要素结构
逐日数据的价值在于它正好卡在“看大趋势”和“看小时级过程”之间。日平均气温、日最高气温、日最低气温、日降水量这四件套,可以支撑很多分析:气温年较差、高温日数、连续降水日、极端降水强度、热浪事件统计,这些都是月平均数据做不了的。比如同样是夏季,两个月均温可能看不出差别,但逐日数据里连续5天最高气温超过35℃,就是一次明确的极端高温过程。只有月资料的话,这类事件基本损失掉了。
整理这份数据时,大概会遇到以下常见要素:
| 字段 | 含义 | 常用单位 | 注意事项 |
|---|---|---|---|
| 区站号 | 5位站点编号 | 无 | 同一站号历史上可能迁移过 |
| 年月日 | 观测日期 | 北京时 | 注意早期资料可能存在日界差异 |
| 平均气温 | 日平均气温 | ℃ | 人工观测时代多为02/08/14/20时四次平均 |
| 最高气温 | 日最高气温 | ℃ | 注意观测环境变化带来的系统偏差 |
| 最低气温 | 日最低气温 | ℃ | 极端值要和气候界限值比对 |
| 降水量 | 日降水量 | mm | 需确认是0.1mm精度还是0.01mm精度 |
| 平均风速 | 日平均风速 | m/s | 自动站前与后统计时段可能不一 |
| 相对湿度 | 日平均相对湿度 | % | 常与降水字段做一致性校验 |
| 日照时数 | 日日照时数 | h | 部分站测站搬迁后缺测较多 |
我统一处理后习惯用长表格式:station, date, tmean, tmax, tmin, prcp, rh, wind, sun, qc_flag。这样按站点聚合、按年份聚合都很方便,后续做分位数突变检验也不会被宽表结构卡住。
1.3 站点网络从稀疏到稠密
站点数量变化是整理过程中必须心里有数的一件事。40年代可用站很少,50年代建站明显加密,60到70年代全国基本形成2000个左右国家站的格局,之后国家级站长期维持在2400个上下。2000年后大量区域自动站补充进来,站点总数一下子膨胀到数万,但如果要做长序列一致分析,区域自动站和国家级站的观测仪器、维护水平、质量控制标准都不一样,不建议直接混合使用。我一般只保留国家级地面气象观测站,把区域站数据单独归档,绝不塞进同一条长序列里。
这个站点密度变化直接决定分析边界:早期站点稀疏,只能做单站或多站串联的长序列分析,不太适合做空间插值;后期站点稠密,才谈得上做区域平均、格点化。换句话说,1942—1955年这段,最靠谱的应用方式是把有连续观测的站点一条条拎出来看变化,而不是画一张全国温度分布图。
2. 数据到手后的第一步:统一口径与质量控制
2.1 渠道与文件格式差异
整理这种时间跨度超过80年的数据,基本不太可能只从一个地方拿全。常用的公开渠道包括气象数据服务平台提供的标准数据集、GHCN-Daily里收录的中国站点数据,以及部分地方气象部门发布的整编资料。GHCN-D的好处是字段规范、国际通用,但对中国站点覆盖不全,更新也存在滞后。我的做法是以国内版本为主,国外版本做交叉检查:同一个站同一天同一要素,两套数据的差值超过阈值时,以质控标记更完整的那套为准。
渠道一多,格式就乱。有的CSV表头是中文缩写,有的用拼音缩写,有的把气温单位写成0.1摄氏度,有的把降水写成0.1毫米。我记得第一次处理时就吃过亏,读进来没细看单位,算出一个站点年降水上万毫米,检查了半天才发现是原始数据没除10。这类问题的共性是:拿到文件先读数据字典,再写pandas读取逻辑,不要看到数字就急着处理。
2.2 统一表头与单位的标准流程
我一般按下面几步把多源数据灌成同一套格式:
- 校对站点清单。以站号为主键,保留经纬度、海拔、建站年份等元数据。
- 统一日期解析。把所有日期转成
YYYY-MM-DD,同时明确时制。国内地面站资料默认是北京时,但如果混了UTC源,时间差8小时会导致日期错位。 - 统一命名与单位。全部转成国际单位:气温摄氏度、降水毫米、风速米每秒、湿度百分比。
- 去重。同一
station + date出现多条记录时,参考质控标记选择保留下一条,并在qc_flag里注明。 - 重建连续时间轴。对每个站,生成从首次记录到最新记录的全日期序列,缺测日保留
NaN,不填0。特别是降水,缺测和0毫米在物理含义上完全不同。
做完这几步,数据才算“可分析”,否则后面所有统计结果都是建立在沙子上的。
import pandas as pd # 假设原始文件已完成最基础的列名映射 df = pd.read_csv("raw_obs.csv") df["date"] = pd.to_datetime(df["date"]) df = df.sort_values(["station", "date"]).drop_duplicates( subset=["station", "date"], keep="last" ) # 单位转换:0.1℃ -> ℃,0.1mm -> mm df["tmean"] = df["tmean_raw"] / 10.0 df["prcp"] = df["prcp_raw"] / 10.0 df.loc[df["prcp"] < 0, "prcp"] = None # 保存成统一长表 df.to_csv("daily_obs_standard.csv", index=False)2.3 质量控制的三层检查
质量控制是我认为最值得花时间的一步,尤其是历史数据。自动站时代很多问题能被后端程序挡掉,但40到60年代不少资料是人工观测手工录入的,什么笔误都有可能出现。我只做三层检查,不搞花哨算法:
第一层是界限值检验。给每个要素设一个物理上不可能的范围。比如中国范围内,日最高气温低于-60℃或高于55℃,基本可以直接标记为异常。降水量单日超过500mm也要警惕,需要去和周边站做核对。
第二层是内部一致性检验。判断不同要素之间是不是互相矛盾。比如日最高气温低于日最低气温,这就是数据逻辑错误;再比如降水大于0但相对湿度为0,这种记录也不能直接信。
第三层是时空连续性检验。把相邻两天的差值算出来,日平均气温相邻日跳变超过15℃时标记可疑;和周边站同一天的值做比较,如果一个站明显偏离周围站3个标准差,就要查是不是录入错误或站点迁移造成的。
质控的最终结果不是直接删数据,而是加标记。我会额外生成一列qc_flag,区分正常、可疑、错误和插补值。这样后续做极端事件统计时,可以随时选择只用高质量观测,而不用从头再处理一遍原始数据。
3. 真正会咬人的坑:迁站、断测与时间口径
3.1 站号没变不代表观测没变
这是我在整理过程中遇到的最大的一个坑:站点迁移。很多站的区站号几十年不变,但观测场可能从老城区搬到了郊区,或者从低海拔挪到了高海拔。温度序列对这种迁移极其敏感,因为城市热岛效应、海拔变化都会造成系统性的冷暖偏移。
有一个典型情况:某个站在1970年前位于城区,1990年前后迁到郊区,迁站后年平均气温序列一下子低了将近0.8℃。如果不管迁站,直接拿整段序列去算长期趋势,很容易得出一个“显著降温”的错误结论。所以整理阶段必须把站点元数据里的经纬度、海拔变动记录调出来,逐站排查两个关键指标:经纬度是否动了,海拔是否变了。
处理办法一般是两步走。第一步做断点检测,可以用R语言的RHtestsV4这类工具,也可以简单点:把站点序列和周边参考站的差值序列画出来,差值出现持续跳变的位置,就是疑似断点。第二步做分段距平合并,以断点为界,把前后两段分别减去各自时期内的区域参考值,转成距平后再拼接。这样迁站造成的系统误差会被压制,保留下来的是真实气候波动。
3.2 缺测序列:补还是不补
1942到2025年跨度实在太长,中间缺测是必然的。尤其是40到50年代,不少站只有某几个月有记录,冬季可能整段断掉。我的原则很简单:先分清楚缺测原因,是“站点还没建”,是“仪器故障”,还是“战争和动乱造成的观测中断”,然后区别对待。
对缺测超过20%的年份,我一般不在年尺度上直接算均值,避免用半年数据代表一整年。对核心站的少量缺测,如果只是零星几天,可以用临近站的回归关系做插补,但还是那句话——插补值必须带标记,不能混进原始观测里。对早期站点不足的问题,不强行插值,直接缩小分析范围,只保留那些连续记录超过一定年限的站。
有人会问,能不能用再分析资料把缺测全部填满?可以,但要清楚填进去的东西已经不是“观测”了,而是模式重建。对趋势分析来说,引入再分析资料补齐缺测等于引入另一套系统误差,操作不当会把信号搞混。
3.3 北京时、世界时与20-20时雨量
时间口径是另一个容易被轻视的问题。国内地面观测资料绝大多数用北京时,国际交换数据常用UTC。如果两套资料混用时没有转时制,日期差出8小时,温度序列看上去只是错位一天,降水序列则可能把同日降水劈成两天,直接影响极端降水频率计算。
还有一个更隐蔽的口径问题:日降水量的日界。人工观测时代,中国地面气象站日降水量按20—20时累计,也就是今天20点到明天20点的量记为明天的日期。自动站普及后,虽然可以输出任意时段的累计量,但很多整编产品为了历史延续,仍会统一到20时日界。做分析前一定要确认手上这份数据用的是20时日界还是00时日界,否则连续降水日数会非常难看。
3.4 日平均气温的算法差异
日平均气温的定义在不同年代不完全一样。人工观测时代多用02、08、14、20时四次定时观测求平均,自动站时代有些产品则直接用24小时逐时数据求平均。两种算法在多数情况下差异不大,但在强冷空气过境或者昼夜温差极大的日子里,差值可能超过1℃。做长期趋势分析时,最好先确认数据产品说明里写的是哪种算法。如果产品是拼接出来的,早期和近期算法不一致,会对趋势项引入人为跳变。
4. 三个拿来即用的分析场景与Python示例
4.1 逐年平均气温与气候距平
最基础也最常用的分析就是单站逐年平均气温序列。这里有一个易被忽略的前提:年平均值要限定最少有效天数,比如一年里有效观测日少于330天就不参与统计。否则缺得七零八落的年份,均值偏差会很大。
import pandas as pd import numpy as np df = pd.read_csv("daily_obs_standard.csv", parse_dates=["date"]) df["year"] = df["date"].dt.year # 每年每个站的有效观测天数 valid_days = ( df[["station", "year", "tmean"]] .dropna() .groupby(["station", "year"]) .size() .rename("valid_days") .reset_index() ) # 只保留有效天数>=330的站年 df_valid = df.merge(valid_days, on=["station", "year"]) df_valid = df_valid[df_valid["valid_days"] >= 330] # 逐年平均气温 annual_mean = ( df_valid.groupby(["station", "year"])["tmean"] .mean() .reset_index() )算气候距平的时候,基准期选择也很关键。目前气候业务上常用1991—2020年作为标准气候态,用这个基准期算距平,后续和公开气候产品对比时更容易对上。如果站点在基准期内缺测太多,就要考虑改用更长时期或借助参考站重建。
4.2 高温日数与极端高温指数
逐日数据最擅长的一项工作是极端气候事件统计。比如统计每年日最高气温超过35℃的天数,这个指标在夏季高温评估里非常常用。
# 只用气温要素,筛选主要站点 df_t = df.dropna(subset=["tmax"]).copy() df_t["tx35"] = (df_t["tmax"] >= 35).astype(int) tx35_count = ( df_t.groupby(["station", "year"])["tx35"] .sum() .reset_index() )如果要做国际通用的极端温度指数,比如每年最热日最高气温(TXx),做法也不复杂:先按站按年取日最高气温的最大值。需要注意的是,极端指数对站点迁移和观测仪器更换非常敏感,一旦序列里混入了迁站断点,极端值统计里会出现明显异常的跳峰。所以做这类分析前,先把第3章的迁站排查做掉。
4.3 降水频率与连续干期统计
降水数据处理有一些特殊套路。先定义“雨日”:日降水量大于等于0.1mm算有雨;气象干旱分析里,也有用大于等于1mm来标定有效降水日。计算年降水量时要保留所有实测值,包括0值,不要直接把降水量取对数后再求均值。
连续干期统计是一个很实用的例子:每年最长连续无降水日数,这个指标对农业灌溉规划和水资源调度都有参考价值。逻辑上用时间轴切分雨日序列,把连续0.1mm以下的天数段找出来,取最大段长。
def longest_dry_spell(prcp_series): dry = (prcp_series < 0.1).astype(int) # 用累计和区分连续段 group = (dry == 0).cumsum() spells = dry.groupby(group).sum() return int(spells.max()) if len(spells) else 0 # 以单个站点为例:给定站号和时间范围 one_station = df[df["station"] == "54511"].sort_values("date") spell = longest_dry_spell(one_station["prcp"].values)这个函数实现灵感来自“分组连续性”的常见技巧,实测下来比手写循环快很多,数据量大时也扛得住。
5. 和再分析资料搭配使用:什么时候信站点,什么时候信格点
5.1 站点观测和再分析的本质差异
整理完这份站点数据后,难免会碰到一个问题:要不要直接用再分析资料,比如ERA5,覆盖更全、格点均匀,看起来更方便。但站点观测和再分析在本质上不是一类东西。
站点观测是“一个点上的真实记录”,它捕捉的是真实观测环境下的天气气候,缺点是点密度不均匀、存在迁站和仪器变动问题。再分析资料则是“用模式把零星观测融合到规则网格上”,它的优势是空间完整、时间连续,缺点是结果受同化系统和模式偏差影响,年代越早不确定性越大,而且它并不等于“实测”。
我通常会做一个很简单的对比:把站点观测的逐年气温序列和对应位置的ERA5格点序列画在同一张图上,看差值序列是否存在明显的阶段性漂移。如果有漂移,说明再分析资料在那一时期可能引入了不随时间均匀的系统偏差,需要特别小心。
5.2 不同场景下的选型建议
| 分析需求 | 建议做法 | 说明 |
|---|---|---|
| 单站长序列趋势 | 只用站点观测 | 必须先做均一化处理,迁站、换仪器要处理 |
| 极端值频率分析 | 优先站点观测 | 工程设计参数需要真实观测极值 |
| 全国或区域空间分布 | 再分析或站点插值 | 站点太稀疏时直接画空间图会误导 |
| 缺测区域的历史补全 | 再分析+站点验证 | 再分析结果要用站点数据做偏差校正 |
| 农业气象服务 | 再分析业务产品 | 时效性和空间覆盖更重要 |
如果需要把站点数据转成格点,常用的方法包括反距离权重插值、双线性插值、克里金插值。做趋势研究时,我一般推荐先做区域平均,而不是直接插值成格点。区域平均可以避免插值过程的人为平滑和边缘效应,对趋势信号影响更小。
5.3 我的习惯与一次失败教训
有一年我做某区域夏季降水趋势,直接用了再分析格点资料,算出来的下降趋势非常显著,感觉能发一篇文章。后来想用站点观测验证一下,结果发现那段时间几个主要站点降水并没有同样幅度的下降。再仔细查资料,才发现再分析产品在该年份前后的同化数据源发生了变化,导致降水场出现系统性跳变。从那次之后,我养成了习惯:任何再分析产品得出的气候结论,至少要拿观测站点做一次交叉验证;任何纯站点得出的空间结论,也至少要确认站点密度足够支撑。
这种验证其实花不了多少时间:把站点插值到再分析格点位置,或者把再分析格点插值到站点位置,计算两套数据的年际距平相关系数和差值标准差就行。相关性低或差值方差大,说明两套数据里的可解释信号不一致,结论最好留有余地。
整理1942—2025年中国气象观测站点逐日气象数据的整个过程,对我来说最大的收获不是跑出了多少个统计图表,而是彻底理解了“数据干净”四个字的分量。单一站的趋势、极端指数、降水资源评估,全部依赖底层观测质量。我最后养成了一个习惯:每发一版处理后的数据,都附一份核对清单,写清楚站号范围、日期范围、每个站的有效记录数、质控标记次数以及单位版本。这个习惯救了我好几次,也推荐给所有长期和气象数据打交道的人。