这套"全国长时序月度省市气温数据(190101-202312)"我前前后后折腾了大半个月,期间换过四种处理方案,重写过三版清洗脚本,最后才形成一套相对顺手的处理流程。老实说,这类长时序数据在气象圈子里不难找,但真正要落到省市一级、直接能用于分析和可视化的程度,中间要跨过的坎比想象中多得多。这篇博文就把我从拿到原始数据到最终产出图表、完成趋势检验的全过程拆开来讲,包括字段怎么理解、缺测怎么补、行政区划变更怎么对齐、Mann-Kendall检验怎么做,以及那些文档里绝对不会写出来的坑。
如果你正在做气候分析、农业气象灾害评估、城市热岛研究,或者单纯想画一张从1901年到2023年的全国月度气温变化动图,这篇文章应该能帮你省下不少试错的时间。
1. 这份数据集到底装了什么:字段结构与空间粒度的一次说清
先说结论:这是一份覆盖1901年1月至2023年12月、全国省市两个行政级别、逐月一条记录的气温面板数据。时间跨度长达123年,共1476个月,理论上每个省市单元应该有1476条记录。拿这个体量做趋势分析、周期提取或异常事件回溯,时间分辨率上完全够用。
1.1 最基本的字段构成
打开原始文件(我拿到的是一份CSV,约200MB),逐列核对后,核心字段大概可以分成五组:
- 时间标识:年、月两个字段,部分版本会合并成"1901-01"这样的字符串
- 空间标识:省(province)、市(city),两级粒度
- 气温观测值:月平均气温(temp_mean)、月最高气温(temp_max)、月最低气温(temp_min)
- 派生特征:气温距平(anomaly)、气温标准差(temp_std)等,这类字段在不同来源的数据集中差异很大
- 质量标识:数据来源标记(source)、插值标记(interp_flag)等
需要特别提醒的是,不同渠道发布的数据,字段命名差异非常大。比如有的叫"tmax",有的叫"TN",有的直接叫"最高气温",合并前一定要先通过describe()和info()确认字段语义,不然后面画图时单位对不上,整个分析就得推倒重来。
1.2 "省市"两级到底指什么
很多初学者会默认"省"就是34个省级行政区,"市"就是所有地级市,实际处理时你会发现根本不是这么回事。
第一,数据的时间覆盖并不完整。1901年时点的行政建制与2023年差别巨大,早期很多省市单元的记录从某一时期才开始出现,并非每个单元都有完整的1476条记录。这意味着数据是典型的非平衡面板,不能直接拿来做需要完整序列的方法(比如部分频谱分析)。
第二,直辖市、特别区域、省直辖县级行政区的归属是历史遗留问题。有些分析者会把省级记录直接复制给下辖每个地级市,这种做法在空间精度要求不高时可以接受,但如果要做市县尺度的精细分析,就会引入明显的空间误差。我的处理策略是单独保留省级序列和市级序列,不同分析场景各取所需,而不是强行合并。
第三,部分记录存在"省级已覆盖、市级缺失"的情况,比如某省在1950年代之前只有省级记录,1950年代之后才有地级市记录。做全时段分析时,我建议以省级数据为主;做分时段分析时,市级数据按"数据可得期"截取,避免拿不同空间粒度的数据混算趋势。
1.3 这份数据能做什么、不能做什么
能做的事情包括:
- 全国尺度、区域尺度的气温长期趋势分析(年际、年代际)
- 逐月气温距平与极端冷暖事件的识别
- 农业热量资源(积温、生长季长度)的时空演变
- 城市扩展与局地增温的关联分析(配合夜间灯光或土地利用数据)
- 教育、健康、能源等领域的气温敏感性回归建模
不能做的事情,说得直白一点:
- 不能替代站点观测做逐日气象服务(月度数据抹平了日内和逐日波动)
- 不能直接用于小时级或天气过程分析
- 不能把市级月均值当作真正的"城市气象站实测值"来引用,因为很多数值是通过网格降尺度或插值得到的
对数据能力边界有清醒认知,后面的分析才不会提出不切实际的问题。这也是我处理所有数据集时最重要的一条原则:先摸清数据能回答什么,再决定问什么。
2. 数据从哪来、可信度如何:原始数据源的加工链路与质控逻辑
面对一份1901年起的数据,第一反应都该是:1901年的气象站才几个,怎么可能有全国省市完整的气温记录?这个问题问得非常好,因为答案直接决定了你后续分析的下限。
2.1 长时序"重建"数据的基本思路
1901年之后,全国范围内的实测气象站数量非常有限,早期站点更是稀少。因此,这类跨越上百年的数据几乎不可能完全来自实测,一般会采用两条技术路径:
路径一:网格化再分析产品的时间降尺度。先利用全球或区域的再分析数据集(比如以月为单位的全球气温网格),再通过双线性插值、地形校正等手段把网格值降到省市尺度。1901-1950年的记录基本都靠这条路径。
路径二:站点观测的空间化插值。利用尽可能收集到的历史站点记录,结合海拔、纬度等协变量,对无站点区域的月值进行插值估计(如薄板样条、克里金等)。1951年之后的记录精度会明显提升,因为站点密度上来了。
这两种路径产生的数据,质量特征截然不同。早期段(1901-1950)空间平滑性强,极端值容易被抹平;近期段(1980年后)与实测序列吻合度高,极端冷暖事件的可信度也更高。所以处理阶段,对早期数据做结论时要多留余地。
2.2 我如何做了一套"轻量级质控"
结合实测站点数据对重建数据进行抽样交叉验证是质控的核心手段。如果手头有部分城市1951年后的逐月实测值,可以按城市匹配后计算偏差、均方根误差和相关系数。我的实操经验是:
- 重点检查**夏季(6-8月)和冬季(12-2月)**两个季节段的偏差,因为这是热量需求最旺盛、也是对气温最敏感的两个窗口
- 计算标准化偏差(差值除以该城市气温标准差),超过1.5个标准差的月份标红处理,再逐个查看是否有真实气候事件(如强厄尔尼诺年份)支撑
- 对偏差较大的城市,回溯其海拔与地形特征,山区城市的插值偏差一般大于平原城市
如果发现某城市与实测数据的系统性偏差超过1-2℃,我的建议是:放弃直接用该城市的绝对值做结论,转而用距平序列(实测与重建的距平往往比绝对值吻合得多)。这也算一种很实用的容错策略。
2.3 单位换算:最容易翻车的隐藏地雷
拿到数据别急着画图,先花30秒确认温度单位。很多全球网格产品的原始单位是开尔文(K),发布方在输出省市统计值时可能忘记减273.15;而有些产品虽然输出的是摄氏度,字段名里却带"K"后缀,误导性极强。
我的验证方法非常笨但非常有效:随机抽5个城市的1月平均气温。如果数值在-30到10之间,大概率是摄氏度;如果数值在240到290之间,基本可以确定是开尔文;如果1月出现35以上,那就要怀疑是不是把华氏度当摄氏度了。换算函数顺手列一下:
# 开尔文转摄氏度 def k_to_c(temp_k): return temp_k - 273.15 # 华氏度转摄氏度 def f_to_c(temp_f): return (temp_f - 32) * 5 / 9这类低级错误一旦混进分析流程,后面所有结论都会跟着出错,而且错得毫无痕迹。所以我把"单位确认"列为所有气象数据预处理的第一道关卡,谁来都不例外。
3. 预处理绕不开的四个环节:缺测、对齐、标准化与气候态基准期
原始数据往往没有直接达到"可分析"的状态。我花了最多时间的不是分析本身,而是把数据洗到让分析工具满意的过程。以下四个环节任何一个偷懒,后面都要付出更大的代价。
3.1 缺测与无效值处理策略
首先是缺失值形态识别。缺测在CSV里可能以NaN、空字符串、-9999、9999等形式存在。直接用read_csv读进来后,先把所有异常值统一替换为np.nan:
import pandas as pd import numpy as np df = pd.read_csv('temp_data.csv') df.replace([-9999, 9999, -999, 999, ''], np.nan, inplace=True)然后是缺测比例的评估。我的判断标准是:
- 缺失比例低于5%的城市:直接用线性插值补全
- 缺失比例在5%-20%之间:改用季节分解插值(先拆出季节分量和趋势分量,分别插值后合并)
- 缺失比例超过20%:放弃插值,分析时按"有数据的月份"进行
线性插值看似简单,但月度气温序列有明显的季节周期,直接用前后月线性插值会严重低估冬夏峰值。更好的方式是用同月多年均值 + 相邻年趋势调整,比如1月缺测,就用前后两年的1月均值做加权估计,再加入长期趋势修正。这样补出来的序列不会出现"平直折线"的违和感。
3.2 行政区划变更与空间单元对齐
120多年里,地市级别的合并、拆分、更名频繁发生。如果不处理行政区划变更,前后数据根本不是同一个空间实体,趋势分析就失去了意义。
我处理的办法分几步:
第一步,建立城市标识字典。以2023年的行政区划为基准,对历史上存在过的城市单元做映射。比如某地区历史上曾分属不同城市,则按现行政区划归属重新聚合单元。
第二步,聚合规则明确:省级数据用面积加权平均,市级数据对撤并情况做记录合并。有些被撤销的市,其气象记录需要归并到现属城市,直接用现属城市编码替换即可。
第三步,手动校验关键跳跃点。行政区划调整的年份,序列常会出现异常的台阶式跳跃——这不是气候突变,而是空间口径变了。画时间序列图时如果看到某个年份前后均值突然整体抬升或下降,就要立刻怀疑是否口径变化。
3.3 标准化与归一化:什么时候做、怎么做
如果你的分析只是看某个城市的绝对温度变化,不一定要标准化;但如果你要对全国几百个城市做横向对比,不标准化的结果就是北方城市的方差天然比南方大(因为年较差大),推理时会有偏差。
我的建议是分城市做z-score标准化:
df['temp_std'] = df.groupby(['city'])['temp_mean'].transform( lambda x: (x - x.mean()) / x.std() )这样每个城市的气温序列都以自身历史均值为中心,方差被压缩到同一量纲,1500多个月的变化幅度就可以在同一张图里直接比较,不同气候背景的城市之间也能做聚类、相关分析等操作。
不过要记住:z-score标准化之后,城市间的绝对温差信息消失了。如果需要保留"南方比北方暖"这个绝对信息做展示,就用原始值画图;如果做模式识别,就用标准化后的序列。
3.4 气候态基准期:气象分析约定俗成的"原点"
气候态基准期是什么?说白了就是气象行业公认的"正常水平"参照系。世界气象组织推荐使用最近三个完整的10年作为基准期,常见的有1961-1990、1981-2010、1991-2020。
计算距平(anomaly)时必须明确基准期:
base = df[(df['year'] >= 1981) & (df['year'] <= 2010)] base_avg = base.groupby('city')['temp_mean'].mean() # 计算逐年距平 df['anomaly'] = df.apply( lambda row: row['temp_mean'] - base_avg[row['city']], axis=1 )这里有一个实操细节:基准期不能跨到未来数据。如果用1981-2010做基准期,那2010年前后的距平天然一正一负,这是方法本身的边界,不表示真实突变。我在绘制距平图时,会把基准期标记出来,让图例自己说明问题。
4. 从数据到图表:把123年的气温变化"讲"出来
数据清洗干净之后,真正有意思的部分才开始。我按"全局概览→区域聚焦→事件识别→趋势检验"四个层次推进可视化与分析,每一步都有自己的细节坑。
4.1 全国与区域的时间序列图:滑动平均和置信区间
直接绘制1476个月的原始序列,图会非常"毛",一眼看去全是高频波动,长期趋势反而看不清。我的处理是叠加不同窗口的滑动平均(11年滑动平均用于滤除年代际波动,31年滑动平均用于凸显长期趋势)。
# 以全国月度序列为例 monthly_ts = df.groupby(['year', 'month'])['temp_mean'].mean().reset_index() monthly_ts['date'] = pd.to_datetime(monthly_ts[['year', 'month']].assign(day=1)) # 计算11年滑动平均(132个月窗口) monthly_ts['ma_11y'] = monthly_ts['temp_mean'].rolling(132, center=True, min_periods=60).mean()顺便说一句,画这种长序列图时,x轴的年份刻度不要每年都标,否则全是数字挤成一团。我用的是"每20年一个大刻度、每10年一个小刻度"的方案,图面干净很多。区域聚焦时我会把东北、华北、华东、华南、西南、西北拆开分别画子图,逐区域描述趋势与波动特征。
4.2 空间分布热力图:用网格化表达省市温度格局
月度数据在空间上有一个特有优势:可以挑选特定年份或特定季节做空间切片,画出"某年1月全国平均气温分布"这种热力图。选择一个年份(比如2023),提取该年12个月的均值,再按省市填色。
import matplotlib.pyplot as plt from matplotlib.patches import Polygon from matplotlib.colors import Normalize # 假设已读取省级GeoJSON/边界坐标 fig, ax = plt.subplots(figsize=(12, 8)) # 根据温度范围设置颜色映射 cmap = plt.cm.RdYlBu_r norm = Normalize(vmin=df_sub['temp_mean'].min(), vmax=df_sub['temp_mean'].max()) # 遍历省级边界绘制 # 绘图循环略... plt.colorbar(plt.cm.ScalarMappable(cmap=cmap, norm=norm), ax=ax)空间热力图容易犯的错是颜色分级太粗或太细。太粗看不出省际差异,太细则颜色噪音大。我通常把温度范围分为10-12个等级,使用顺序色带(从冷色到暖色),并手动设定上下限避免个别极值把整体色带拉偏。
4.3 距平图与极端冷暖事件:怎么定义"异常"
有了基准期之后,距平序列就是最直观的"异常探测器"。逐月距平超过历史标准差2倍的月份,可以标记为"极端暖月"或"极端冷月"。
分析这一步时我的习惯是:先把年度距平算出来,然后按十年分组统计极端事件次数,找出极端事件发生概率最高的年代。比如某区域1998年前后的异常高温、2008年前后的异常低温等,都能从距平序列上快速捕捉到。
用代码示意:
df['zscore'] = df.groupby('city')['temp_mean'].transform( lambda x: (x - x.mean()) / x.std() ) extreme = df[df['zscore'].abs() >= 2] yearly_count = extreme.groupby(['year', 'province']).size().reset_index(name='count')从实际数据看,极端暖月的出现频率在近30年明显高于早期,这个结论方向与很多公开研究一致,说明该数据在"捕捉大尺度气候异常"方面具备可信度。但我不建议直接引用单条极端记录作为"历史极值"证据——插值数据的极值经常不够极端。
4.4 趋势检验:用Mann-Kendall和Sen斜率替代简单回归
很多人在做气温趋势时,画一条散点图再加一条线性回归线,就算完事。这在统计上不够严谨,因为气象序列普遍存在自相关,普通最小二乘回归的显著性检验容易失真。我习惯用Mann-Kendall(MK)非参数趋势检验配合Sen斜率估计,这两个方法对异常值不敏感,也不要求数据正态分布。
# 简化版MK检验与Sen斜率(可用pymannkendall库) import pymannkendall as mk # 以某省会城市年度均值序列为例 result = mk.original_test(city_annual_series.dropna()) print(result) # trend, h, p, z, Tau, s, var_s, slope, intercept输出里的trend给出趋势方向(increasing/decreasing/no trend),h是显著性标志,slope是Sen斜率,也就是每年的时间变化速率(℃/年)。把这个值乘以10,就是"每10年升温/降温多少摄氏度"。
我的结果:大多数省级单元的全年平均气温呈显著上升趋势,Sen斜率约在0.015-0.035℃/年之间;季节分解后升温趋势冬季强于夏季。这与已有研究基本吻合,也反向验证了数据集中长期趋势的可靠性。
计算Sen斜率的分区域汇总时要用矩阵化的思路,写一层循环逐个城市跑太慢,改成groupby加apply快很多。
5. 我踩过的几个坑和对应的绕坑方案
这部分全部来自真实操作中翻过的车,如果提前有人告诉我,至少能省一周时间。
5.1 坑一:"逐月数据"不等于"月度平均"
拿到数据时,我先入为主以为temp_mean是当月日平均气温的算术平均,后面才发现部分月份的值是"月内逐日平均温度的中位数"或"月内气象站观测次数加权平均"。这种统计口径的差异会导致夏季和冬季的偏差被放大。
识别方法:抽一个城市,与该城市气象站公布的月平均气温做散点图比较。如果两者接近1:1线,说明口径一致;如果存在小但系统的偏移,就要调整口径说明。比较后我认为该数据集更接近算术平均,但仍建议在分析报告中注明口径来源。
5.2 坑二:行政代码"同一个数字"在不同时期不是同一个地方
某些地级市从2000年起代码不变,但所辖县区范围发生过调整;某些市名字相同,代码相同,实际辖区面积却被大幅划小再划大。如果只用城市代码关联,不校验辖区范围变化,趋势分析会把"辖区扩展带来的海拔变化"错误归因为"气温变化"。
绕坑办法:行政区划大调整年份,对比调整前后5年连续年份的气温均值,如果出现1℃以上的系统跳跃,结合城市海拔和辖区面积变化判断是否口径问题。无法合理解释的跳跃,直接从突变点处断开,分前后两段单独分析,避免用一条趋势线硬拟合跨越突变点的序列。
5.3 坑三:分组聚合时把"省级数据"混进了市级聚合
做全国市级平均时,如果不注意数据层级标记,很容易把省级记录也算进市级平均值里,导致样本重复加权。比如某省既有省级记录、又有下辖某市级记录,直接groupby('city')时省级记录会变成一个叫某省名字的"市",和其他市混在一起。
解决方法是加一个level字段区分"省"与"市",聚合时用query("level == 'city'")过滤层级。
df_clean = df_clean[df_clean['level'] == '市'] city_stats = df_clean.groupby(['year'])['temp_mean'].mean()后续每个分析步骤前都确认一下当前用的是哪一层级的数据,不然最后报告画出来的"全国市级平均曲线"可能混杂了不同粒度的信息。
5.4 坑四:存下来的csv中文编码
这个坑听起来很小,杀伤力却极大。早期我保存清洗后的数据时用了默认编码,再次读取时省名全部乱码,地图匹配直接失败。后来统一用utf-8-sig写入,既能在Excel打开不乱码,也能被pandas正常读回:
df_clean.to_csv('temp_data_clean.csv', index=False, encoding='utf-8-sig')顺便说一句,如果之后还有GIS/地图匹配需求,省级名称必须和地图文件里的名称完全一致,包括"省"和"壮族自治区""维吾尔自治区"这类全称后缀写法。我的处理方式是把所有区域名称统一到一个标准对照表里,作为唯一主键,后续所有横向关联都用对照表join,而不是用中文名直接匹配。
6. 进阶方向:从"看趋势"到"做预测"
如果你不满足于画图,想把这份数据用得更深,我提供几个亲测可行的扩展方向。
6.1 季节分解与周期分析
用STL(Seasonal-Trend decomposition using Loess)把每个城市的月度序列拆成趋势项、季节项、残差项,然后对残差项做突变点检测(比如Pettitt检验),定位异常变化的年份。这套思路在研究"某区域气温在哪个年代发生系统性抬升"时很好用。
from statsmodels.tsa.seasonal import STL res = STL(monthly_series, period=12, robust=True).fit() # res.trend, res.seasonal, res.resid6.2 与GDP、人口、用电量等社会经济数据匹配
气温数据非常适合作为社会经济模型的解释变量。比如研究夏季高温对某省用电负荷的影响,可以把月度气温与月度用电量对齐,构建面板回归模型,城市固定效应+时间固定效应可以剥离掉大部分混淆因素。
6.3 构建简单的温度预测基准模型
月度气温序列有明显的季节性,用一个简单的季节朴素模型(未来某月等于过去多年同月均值)作为基准,再用带外生变量的模型(比如加入Niño 3.4区海温指数)对比,能直观看出外部气候因子对预报精度的增量贡献。
这类分析不需要特别复杂的深度学习方法,反而能产出更稳定、更可解释的结果,对非气象专业的从业者也比较友好。
7. 写在最后的一点实在话
用长时序气温数据做分析,最大的感受是:数据价值不在于它有多大,而在于你有没有把它洗到能回答问题的状态。120多年的月度序列,能支撑的研究方向很多,但与数据本身较劲的时间往往比分析时间长得多。我在处理这份数据时,早期段(1901-1950)的记录主要用来观察年代际尺度的冷暖背景,1951年之后的数据才用于精确的统计推断和极端事件归因。
我个人最后再分享两个小技巧:一是所有中间产出一律落盘,每个清洗步骤单独存一份文件,否则后面发现某个处理逻辑错了,要从源头重跑一遍几十GB的流程非常难受;二是画图时默认把"数据覆盖范围"用半透明色带标出来,比如早期只有省级数据、后期有市级数据的范围差异,让读者一眼看清结论在时间和空间上的适用范围。数据规整与分析是两件事,能把这两件事分开并各自做扎实,这份长时序气温数据就能变成你自己研究里相当硬核的一块基石。