最近在处理气候数据时手里正好过了一遍全国长时序月度省市气温数据(190101-202312)。这份数据的名字已经说得很清楚以月份为粒度、以省市为空间单元时间从1901年1月一路排到2023年12月前后跨越120多年。对做气候变化、区域规划、农业气象、能源负荷或者环境健康评估的人来说它的价值不只是“能查到某个月多少度”而在于它把分散的观测、再分析资料变成了一条连续可计算的序列。这篇文章我不会去念说明书而是结合我实际处理这类长序列数据的经验把数据本身、能分析的场景、清洗方法、趋势分析的完整流程以及最容易踩的坑都摊开讲。如果你是那种拿到数据就想先画一条曲线看看的人我建议你耐住性子从数据结构开始看起。长时序数据最怕的不是分析难而是在第一步就把口径理解错了。1. 先摸清底细这份数据到底长什么样1.1 从1901年到2023年时间维度意味着什么只看文件名的190101-202312很容易被“完整”两个字误导。这里有个必须想清楚的前提1901年的时候远没有覆盖全国的高密度气象站。2023年的自动站密度和1901年的站点数量完全不是一个量级。所以所谓长时序全国省市气温通常是实测站点数据与再分析、插值产品融合的结果。这意味着越往早期数据里“估算”的成分越多越到现代实测的权重越大。我的处理习惯是先把时间索引建出来再逐省逐市看有效记录覆盖率。不要一上来就假设每个市从1901年就有连续数据。很多市级单元真正连续记录的时间要晚得多前面几十年的数值可能是用大区域背景场插出来的。这个信息不会直接写进文件名但会藏在数据说明里或者藏在数据的缺失模式里。处理时先做覆盖率检查能避免后面拿着一个“虚假完整”的序列做趋势检验。另一个容易忽略的点是时间口径。1901年的1月到2023年的12月包含123个自然年共1476个月。月份这个粒度其实很合适比日值稳定比年值精细。月平均气温能比较自然地反映季节循环又不会像日值那样被天气过程搅得乱七八糟。你如果只想回答“是不是变暖了”年值够了但如果你想回答“是不是春天来早了、秋天变长了”就必须用月值甚至旬值。1.2 省级和市级两级空间粒度差异在哪省市维度是这份数据的核心卖点。它给了两个观察尺度省级尺度适合宏观气候分区、全国升温速率比较、资源带迁移分析。市级尺度适合城市级适应规划、农业区划、局地热环境研究。但省与市并不是简单的上下级平均关系。市一级站点的位置往往就在城区受城市热岛影响明显省一级如果直接拿省内所有市平均会把城市化信号也平均进去。所以当你做“全国升温了哪些区域升得更快”这类分析时一定要标注使用的到底是市尺度还是省尺度。我习惯把数据拆成三张逻辑表一张省月表、一张市月表、一张站点参考表。站点参考表哪怕只有经纬度和海拔也极有用。它能帮你在做城市热岛分析时把市区站和同省份邻近山区站区分开避免把海拔差异误判成升温差异。气温数据只有长时间序列还不够没有属性信息支撑很多结论下不去。1.3 典型字段结构和单位陷阱一般这类数据至少包含以下字段我按常见情况列出来字段名含义常见样例省代码省级行政区划代码用数字或拼音缩写省名称省级名称某省市代码市级行政区划代码六位代码市名称市级名称某市年份记录年份1901月份记录月份1到12平均气温当月平均气温摄氏度为单位的数值最高气温当月平均最高气温可选字段最低气温当月平均最低气温可选字段数据来源观测/插值/再分析标志用于质量评估单位是一个必须核对的坑。很多气象产品为了压缩存储会把气温乘以10用整数保存比如数值-23代表-2.3°C。也有产品直接存浮点。如果不看清楚说明画出来曲线整体差10倍还不知道错在哪。我的建议是拿到文件后先随机抽几个行政单元与公开的月平均气候值做交叉对照确认量纲再继续。还有一类文件会用“Jan、Feb”或者“1、2”这种月份表示法年份也可能有“1901/1”格式。无所谓只要你能统一成“YYYY-MM”格式后续透视、筛选、窗口计算都会顺手很多。2. 这套数据能解决哪些实际问题2.1 判断某地是否真的在变暖以及变暖速度长时序数据最核心的场景就是算长期趋势。做法很简单把某省或某市每年12个月做平均得到年值然后对这个年值序列做线性回归得到的斜率就是年平均气温的线性趋势单位通常是摄氏度每十年或每百年。举个例子假设某市1901年到2023年的年平均气温序列算出来斜率是0.012那就表示每十年升温0.12°C一百二十多年累计升温大约1.5°C。这个数字有没有意义需要配合显著性检验看比如p值是否小于0.05。另外还要强调线性趋势只是整个序列的“平均姿态”它掩盖了阶段性的冷暖波动。可能前80年几乎没动近40年猛增。所以除了做全序列回归我更推荐再算几个子时段比如1951年至2023年、1981年至2023年、2001年至2023年分别看速率变化。2.2 识别异常暖月和极端冷事件的出现频率月度数据虽然不如日值敏锐但对识别“这个月是不是史上最热”已经非常有用。做法是选一个气候基准期通常用世界气象组织推荐的30年像1991-2020年。算出每个行政区每个月的长期平均和标准差。把历史每个月的气温减去对应月份的平均值得到“气温距平”。再除以标准差得到标准化距平也叫Z值。如果某个月的Z值大于2在统计上就是个异常偏暖月份。把1901年到2023年所有超过阈值的月份找出来按月历排列就能看到异常暖事件在哪个季节变多。这个信息对基础设施标准设计、农业风险管理和公共卫生热浪预警都很有参考价值。2.3 城市热岛和局地差异分析同样一份省市气温数据还能用来观察城市尺度差异。做法是拿城市站与其所在省份的农村/山地背景做差值。如果没有站点参考表也可以做城市增速与全省平均增速的比较某个城市升温速率明显高于全省平均水平基本可以推断城市化贡献了额外的增温。但是要注意不能单纯用“城市比乡村高1°C”就说是热岛还要排除海拔和纬度的影响。一般处理办法是把海拔作为协变量做回归或者只比较海拔接近的城市与乡村站。如果数据里没有海拔字段可以找一张高程数据按经纬度提取。2.4 延伸应用积温、采暖制冷度日、物候期月度气温数据的应用场景绝对不止“看看曲线”。在农业上月均温可以粗略估算生长季的积温尤其适合分析≥10°C活动积温的分布北界。在能源行业可以用月度平均气温推算采暖度日HDD或制冷度日CDD粗略判断电力负荷对气温的敏感程度。在生态领域可以用3月到5月的月均温变化来推断春季物候提前的幅度。对做行业研究的人来说这份数据不是终点而是输入参数。3. 预处理长时序数据是七分清洗三分分析3.1 第一步把格式和编码统一不管拿到的原始文件是CSV、Excel还是NetCDF我第一个动作永远是把它读成统一格式的三列长表省、市、时间、气温。宁可多占一点内存也要让数据进入“一行是一个观测”的长表结构。因为这极大方便后续groupby和透视。用Python读一个类CSV文件的典型流程我放在这里import pandas as pd df pd.read_csv(TEMPERATURE_1901_2023.csv, encodingutf-8) # 查看前几行和字段类型 print(df.head()) print(df.dtypes) # 把年月字符串统一成日期 df[年月] pd.to_datetime( df[年份].astype(str) - df[月份].astype(str), format%Y-%m ) # 检查重复行 print(df.duplicated().sum())如果年月字段出现在文件名里比如“190101”也可以用字符串切片处理df[年月] pd.to_datetime(df[YYYYMM].astype(str), format%Y%m)这一步看似简单但能把后面所有时间窗口计算都变得可靠。长时序数据最容易出现的问题是不同年份的日期格式不一致比如1901年写成“1901/1”2000年写成“2000-01”。统一成datetime对象后排序、切片、重采样都不容易错。3.2 缺失值、极端值和断点怎么处理长时序气温数据常见的质量问题有三种整段缺失。比如某市1950年到1960年没有数据。单点离群。比如某个月数值比前后月份异常高7°C通常来自录入错误或站点替换。断点。同一个城市在1985年换了观测站导致序列前后均值突然变化。面对整段缺失不要随便插值尤其是跨度超过5年的大段缺失。对月均温来说用周围城市同期的月均温做空间插值只能说“可用但不完美”。我通常标注这些记录为“插值”并在最终分析时剔除或做敏感性测试。面对单点离群可以用每个省每个月份的历史分布来识别。即先按“省月份”分组计算平均和标准差然后标记距离均值超过3个标准差的点。这个方法不一定百分百准但至少能筛出可疑值。切记不要直接把可疑值删掉而要逐一点开看上下文。# 按省和月份统计历史分布 grp df.groupby([省名称, 月份])[平均气温] df[均值] grp.transform(mean) df[标准差] grp.transform(std) # 标记极端可疑点 df[异常标记] (df[平均气温] - df[均值]).abs() / df[标准差]断点检测比较复杂常见的算法是分段线性回归寻找突变点。日常处理可以先用可视化的方式看序列是否存在台阶式跳跃。如果发现某个年份前后年均值突然抬升0.8°C且周围省份没有类似变化很可能就是站点迁移或不连续记录被拼在了一起。定性判断后再决定是做同化还是分段分析。3.3 建立一张可持续使用的“温度档案表”长期做数据的人最忌讳的是把清洗和分析逻辑全留在脚本里下次换一批数据重新来一遍。我比较推荐建一个中间层存档一张宽表行是行政单元列是每年12个月或者行是月份、列是省/市。释放中间结果时同时写一份清洗日志记录做了什么、删了什么、插了什么。比如这样一个输出文件结构很有用TEMP_CLEAN/ 01_original.csv 02_dedup.csv 03_outlier_flagged.csv 04_imputed.csv cleaning_log.md这看起来多此一举但当你分析到一半发现某个月数据有问题时能很快定位是在哪一步引入的不用重头开始。4. 手把手跑一个省市气温趋势分析4.1 读取并透视成“市-年”面板数据用上面的长表我可以先把月值聚合成年度平均值。注意如果存在缺失月份直接求平均会系统性地低估或高估。因为缺失可能集中在冬季也可能集中在夏季。稳妥的做法是先检查每年是否有12个月齐全再决定是否计算年均值。# 检查每个市每年的月份数 month_count df.groupby([省名称, 市名称, 年份])[月份].count() # 只保留完整12个月的记录 complete month_count[month_count 12].reset_index() df_full df.merge(complete, on[省名称, 市名称, 年份])然后计算年度均值并透视annual df_full.groupby([省名称, 市名称, 年份])[平均气温].mean().reset_index() pivot annual.pivot_table( index年份, columns[省名称, 市名称], values平均气温 )透视后的宽表每行是一个年份每列是一个市非常适合批量计算每个序列的趋势。4.2 批量计算线性趋势和显著性下面这段代码可以同时对几十个市做线性回归import numpy as np from scipy import stats records [] for col in pivot.columns: s pivot[col].dropna() if len(s) 30: records.append({省: col[0], 市: col[1], 趋势: np.nan, p: np.nan, 样本量: len(s)}) continue x np.arange(len(s)) slope, intercept, r_value, p_value, std_err stats.linregress(x, s.values) records.append({ 省: col[0], 市: col[1], 趋势: slope * 10, # 转化为每十年摄氏度 p: p_value, 样本量: len(s), }) trend_df pd.DataFrame(records)为什么趋势要乘以10因为linregress里x的单位是“1年”斜率表示每年上升或下降多少°C。气象行业习惯用“每十年”来描述乘以10更直观。比如斜率0.016就是每十年上升0.16°C。如果拿到的数据不是从1901年开始而是后期拼接的样本量字段会提醒你序列到底有多长。4.3 计算气候态距平看阶段性演变趋势是长期的平均距平则能暴露波动。接下来我按每个市每个月份的气候基准期计算距平# 选择1991-2020作为气候基准期 clim df[(df[年份] 1991) (df[年份] 2020)] base clim.groupby([省名称, 市名称, 月份])[平均气温].mean().reset_index() base base.rename(columns{平均气温: 气候态}) # 合并并计算距平 df_merge df.merge(base, on[省名称, 市名称, 月份]) df_merge[距平] df_merge[平均气温] - df_merge[气候态]把距平再按年聚合得到年距平序列。这样能一眼看出某年是否偏暖。如果再把距平做十年滑动平均就能看到更平滑的年代际变化。4.4 作图与解读不只画线还要验证画一条简单的年度气温曲线只需要三行代码import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(pivot.index, pivot[(某省, 某市)], linewidth1) plt.xlabel(年份) plt.ylabel(年均气温 (°C)) plt.title(某市1901-2023年年平均气温变化) plt.grid(alpha0.3) plt.show()但这张图只能说明“看着在上升”。真正写结论的时候要把斜率、显著性、样本量都放在一起。不要看到趋势为正就写“显著变暖”。p值大于0.05时只能说“趋势为正但统计上并不显著”。长序列尤其如此早期数据噪声大趋势置信区间可能很宽。再有就是空间解释。算出所有城市的趋势后可以做分级填色图把升温速率高和低的区域分开。但不要直接说“升温快的就是人为影响大”还需要结合海拔、纬度、城镇化率、环流变化综合判断。数据能告诉你“哪里升得快”不能直接告诉你“为什么升得快”。5. 长时序气温数据最容易踩的五个坑5.1 行政区划调整导致“同一城市”不可比过去一百多年里地级市边界发生过大量调整。有的市在2000年才从地区改为地级市有的市曾被合并再拆分。如果你直接拿前后两个名称相同的市做连续序列实际记录的覆盖范围可能完全不是一回事。对策保留行政区划代码不要只靠名称。跨时段比较前先去查一张历史行政区划变更表。如果发现代码变化要么按新代码重新聚合要么只分析省级尺度减少区划变动的影响。5.2 气象站点迁移和城市热岛污染一个城市的气温序列如果一直在同一个位置观测它的变化更多反映气候背景。但实际中很多城市站点在几十年的时间里发生了位移比如从郊区机场搬到市中心或者从山顶搬进城区。这种迁移会带来“非气候因素”的升温。判断方法很简单画出该市年际序列如果斜率在全省异常突出且年代际变化和周边城市不一致就需要警惕站点迁移。有条件的话可以找同省其他城市做差值序列。差值序列稳定则说明目标城市序列可信差值序列出现台阶则多半有站点变更。5.3 早期插值资料和现代实测资料拼接1901年的数据并不可靠。很多产品前期依赖树轮、冰芯、器测记录和再分析场后期才转换到密集站点实测。不同资料来源之间的系统偏差可能在零点几摄氏度到一摄氏度之间。应对办法有两个方向分析全序列时把早期和现代分成两段分别做趋势看是否稳健。做长期距平时基准期选在资料最可靠的时期比如1991-2020年。不要把一个早期的插值跳变当成真实的气候突变成因来解释。5.4 月份口径不一致原始数据中的“月平均气温”算法可能不统一。有些是当月每日平均气温再平均有些是最高最低气温平均后再平均有些采用当地时区有些采用UTC。更麻烦的是文件名里“190101”可能表示数据起始月也可能表示版本号。拿到数据后一定要看说明文档做一个月的抽样值与公开资料核对。5.5 把栅格产品的网格值直接当城市值如果这份全国省市数据本质上是从更粗的网格数据按行政区划面积加权得到的那么它代表的是该市全市面上的平均气温而不是气象站点的实际观测值。城市热岛分析时这类产品的城区信号会被稀释。我的建议是先确认数据来源标志。如果每个城市只有一个值且没有站点位置那更适合做区域尺度趋势不适合做站点尺度的热岛分析。6. 我的一点实操体会在我处理类似长序列数据时始终记住一句话没有“唯一正确”的清洗方式只有“可复现、可解释”的处理流程。每一步选择都可能改变最后趋势的数值所以关键操作都要写进日志。比如我用哪个基准期、插值了哪些缺失月份、删除了哪些可疑点都要留痕。最后分享一个小技巧不要一拿到数据就画全国地图。先选几个覆盖完整、背景差异明显的市把年际曲线画在一张图上横轴是年份纵轴是气温每个市一条线颜色区分区域。这张图能最直观地告诉你数据里有没有跳变、有没有明显不合理的部分。等这张图看着顺眼了再去做复杂的插值、检验和建模。数据基础不稳的时候模型再漂亮都是空中楼阁。这套全国长时序月度省市气温数据(190101-202312)能玩的东西很多从温度变化诊断、季节节奏迁移到行业应用建模都能找到合适的切入口。希望你拿到手之后也能挖掘出自己的分析视角。