简介:这份资源为1982至2024年地级市逐月平均气温数据集,面向气象气候研究、区域经济实证与深度学习建模等场景的科研人员和数据分析者,用于分析城市气候长期变化规律、支撑面板数据回归与时间序列建模。压缩包共13个文件,以xml表格数据文件为主,辅以rels关系描述文件与vml绘图文件,整体约2.6MB,结构紧凑便于直接读取与二次处理。数据字段涵盖地级市名称、月份与平均气温,覆盖四十余年逐月记录,可消除短期天气波动影响,反映各地气候特征。目前已有186人学习下载。读者可据此开展气候特征分析、社会经济与生态保护等维度的实证研究,也可作为人工智能与深度学习任务中的时序样本来源,快速搭建城市气温变化分析框架。
1. 地级市逐月气温面板:从1982到2024,这份数据到底能干什么
如果你做过城市层面的实证研究,大概率遇到过这种局面:想控制气候变量,手里只有省级年均气温,或者只有几个气象站点的点数据,最后只能拿省会城市的气温去代表整个省。这种粗糙的代理变量,在审稿人眼里就是活靶子。1982-2024年地级市逐月平均气温数据,解决的正是这个痛点——它把时间粒度压到月、空间粒度压到地级市,覆盖四十多年,让你能在城市面板回归里放进真正随时间、随城市变化的气候控制变量。适合谁用?做区域经济、能源消费、农业生产、健康经济学、城市碳排放这类研究的从业者,只要你的被解释变量和天气沾边,这份数据就能当控制变量或工具变量的原料。但拿到数据不等于能用,网格怎么匹配到行政区、缺测怎么补、月度怎么聚合到年度,每一步都有翻车的可能。下面按我实际处理这类数据的顺序,把选型、复现、参数和坑一次讲清。
2. 数据从哪来、怎么落到地级市:源选择与空间匹配的完整链路
2.1 为什么是再分析资料而不是气象站观测
气象站观测数据的精度毋庸置疑,但做地级市面板时它有个致命缺陷:站点分布极不均匀。东部沿海一个地级市可能有好几个国家站,西部某些地级市方圆几百公里只有一个站,甚至没有。用站点插值到地级市,西部城市的误差会大到没法看。所以业内做长时序、全覆盖的城市气温面板,主流选择是再分析资料(reanalysis),常见的有 ERA5、NCEP/NCAR、JRA-55 这几套。
ERA5 是我最常用的。它的空间分辨率是 0.25°×0.25°(约 31 公里),时间从 1940 年至今,逐小时输出,变量里直接有 2 米气温(2m temperature)。对地级市尺度来说,0.25° 的网格虽然不能精确刻画城市内部差异,但用来做城市间的气候控制变量完全够用。NCEP/NCAR 分辨率粗一些(约 1.9°),时间更长但空间精度差,做地级市面板不太合适。JRA-55 分辨率约 0.56°,介于两者之间,也可以作为稳健性检验的替代源。
选 ERA5 的另一个理由是它提供了月平均的现成产品,但我的习惯是下载逐小时或逐日原始数据自己聚合,这样中间过程可控,缺测和异常值能及时发现。直接下月均值当然省事,但你不知道它是怎么算出来的,出了问题没法追溯。
2.2 用 xarray 读取 ERA5 并裁剪到研究区
假设你已经从数据源拿到了 ERA5 的 NetCDF 文件,变量名是 t2m,单位是开尔文。第一步是读取和裁剪。下面这段代码是我处理这类数据的标准开头:
import xarray as xr import numpy as np import pandas as pd # 读取ERA5逐小时2米气温,文件按年份存放 ds = xr.open_dataset('era5_t2m_1982.nc') # 变量t2m单位是K,先转成摄氏度 ds['t2m_c'] = ds['t2m'] - 273.15 # 裁剪到中国范围,经度用0-360还是-180到180要看数据源 # ERA5默认经度是0-360,中国大约在73E-135E,需要转换 ds = ds.assign_coords(longitude=(((ds.longitude + 180) % 360) - 180)) ds = ds.sortby('longitude') china = ds.sel(latitude=slice(54, 18), longitude=slice(73, 135)) # 检查时间范围和缺失情况 print(china.time.min().values, china.time.max().values) print(china['t2m_c'].isnull().sum().values)这段代码做了三件事:单位转换、经度重排、空间裁剪。经度重排是最容易出错的地方,ERA5 原始经度是 0 到 360,如果你不转换直接按 -180 到 180 裁剪,会得到一片空白或者错位的区域。转换逻辑是先把经度加 180 再对 360 取模再减 180,这样 0-360 就映射到了 -180 到 180。裁剪范围我一般留一点余量,纬度 18-54、经度 73-135 基本覆盖中国全部地级市。
参数说明:slice(54, 18)里纬度是从大到小,因为 ERA5 的纬度坐标是递减的;如果你的数据纬度是递增的,要反过来写。isnull().sum()用来快速检查有没有整片缺失,正常情况应该是 0。
2.3 把网格气温聚合到地级市:面积加权还是简单平均
裁剪完网格,下一步是把落在每个地级市范围内的网格点聚合成一个城市级气温值。这里有两种做法:简单平均和面积加权。简单平均就是取城市边界内所有网格点的算术平均,面积加权则要考虑每个网格点代表的面积随纬度变化。
对地级市这种尺度,我的经验是:如果城市跨越的纬度范围不大(大多数地级市确实不大),简单平均和面积加权的差异很小,通常不到 0.1°C。但如果你研究的是跨度很大的地区(比如某些面积特别大的地级市),面积加权更稳妥。面积加权的权重可以用网格点纬度的余弦值来近似:
import geopandas as gpd from shapely.geometry import Point # 读取地级市边界shapefile cities = gpd.read_file('china_cities.shp') # 把网格转成点,方便做空间连接 china_points = china['t2m_c'].stack(grid=('latitude', 'longitude')).reset_index() china_points['geometry'] = china_points.apply( lambda r: Point(r['longitude'], r['latitude']), axis=1 ) china_points = gpd.GeoDataFrame(china_points, crs='EPSG:4326') # 空间连接:每个网格点落在哪个城市 joined = gpd.sjoin(china_points, cities[['city_name', 'geometry']], how='inner', predicate='within') # 面积加权:权重为纬度的余弦 joined['weight'] = np.cos(np.deg2rad(joined['latitude'])) # 按城市和月份聚合 joined['month'] = joined['time'].dt.to_period('M') city_month = joined.groupby(['city_name', 'month']).apply( lambda g: np.average(g['t2m_c'], weights=g['weight']) ).reset_index(name='temp_c')逻辑说明:先把三维的网格数据(时间×纬度×经度)堆叠成带经纬度的点表,再转成 GeoDataFrame 做空间连接。sjoin的predicate='within'表示网格点必须落在城市边界内,边界上的点可能被漏掉,但影响可以忽略。权重用cos(latitude)是因为高纬度地区同样经纬度间隔对应的实际面积更小。
参数说明:crs='EPSG:4326'是 WGS84 地理坐标系,和 ERA5 的经纬度一致,不需要投影转换。如果你的城市边界是投影坐标系,要先转成 4326 再做连接。to_period('M')把时间戳转成月份周期,方便后续按年月聚合。
这一步跑完,你得到的就是一张 city_name × month × temp_c 的长表,每个地级市每个月一个平均气温值。从 1982 年 1 月到 2024 年 12 月,理论上每个城市应该有 516 条记录。实际会有缺失,下一章专门讲怎么排查。
3. 缺测、异常与口径统一:让面板真正可回归的三个处理步骤
3.1 缺测的三种来源和对应补法
地级市气温面板的缺测,来源无非三种:原始再分析资料本身有缺、空间连接时城市边界内没有网格点、时间聚合时某些月份数据不完整。
第一种情况在 ERA5 里很少见,但偶尔会有个别时次因为卫星轨道或质量控制被标记为缺失。处理方式是用前后时次线性插值,或者直接用同纬度邻近网格的值填补。我的习惯是:如果某个网格点连续缺测不超过 6 小时,线性插值;超过 6 小时,标记为 NaN,后续聚合时该月该城市如果缺测天数超过 5 天,整月标记为缺失。
第二种情况发生在面积很小的地级市,比如某些资源型城市或者海岛城市,0.25° 的网格可能一个点都没落进去。解决办法有两个:一是把空间连接从within改成intersects,让边界穿过的网格也算进来;二是对实在没有网格点的城市,用最近邻网格的值代替。我一般先用intersects试一遍,如果还有空值,再手动检查这些城市的边界是不是有问题。
第三种情况最隐蔽:某个月份里有一部分天缺测,你直接求月平均,结果被少数几天拉偏了。所以聚合前一定要先算每个城市每个月的有效天数:
# 统计每个城市每月的有效观测天数 daily_count = joined.groupby(['city_name', 'month'])['t2m_c'].count().reset_index(name='n_days') # 标记有效天数不足的月份 daily_count['valid'] = daily_count['n_days'] >= 25 # 每月至少25天有效 # 合并回主表 city_month = city_month.merge(daily_count[['city_name', 'month', 'valid']], on=['city_name', 'month'], how='left') city_month.loc[~city_month['valid'], 'temp_c'] = np.nan参数说明:n_days >= 25这个阈值是我根据经验设的,一个月 30 天里缺 5 天以内,月均值的偏差通常在 0.2°C 以内,可以接受。如果你做的是极端气候研究,阈值要收紧到 28 天甚至更高。
3.2 异常值的识别:不要用 3σ 一刀切
气温数据的异常值识别有个陷阱:气温本身有强烈的季节性,冬天零下二三十度在东北是正常的,你用全年数据的标准差去卡,会把正常的冬季低温当成异常值删掉。正确做法是按城市和月份分组,在组内做异常检测。
我一般用两种方法交叉验证:一是基于历史同月分布的 Z-score,二是基于相邻网格的空间一致性检查。Z-score 的做法是,对每个城市每个月份,计算该月多年均值和标准差,然后标记偏离超过 3 倍标准差的年份。但 1982-2024 有 43 年,某些月份只有 43 个样本,标准差本身不稳定,所以我会把阈值放宽到 3.5 倍,并且只标记不删除,人工检查后再决定。
空间一致性检查更直接:如果某个城市某个月的气温比周围所有邻近城市都高出或低出 5°C 以上,那大概率是数据问题。实现方式是先构建城市邻接矩阵,然后逐月比较:
# 构建城市邻接关系(基于边界相交) cities_adj = cities.copy() cities_adj['neighbors'] = cities_adj.geometry.apply( lambda g: cities[cities.geometry.intersects(g)]['city_name'].tolist() ) # 对每个城市每月,计算与邻居的差值 def check_spatial_outlier(row, city_month_pivot): city, month = row['city_name'], row['month'] neighbors = cities_adj[cities_adj['city_name'] == city]['neighbors'].values[0] if not neighbors: return np.nan neighbor_temps = city_month_pivot.loc[neighbors, month] return row['temp_c'] - neighbor_temps.mean() city_month['spatial_diff'] = city_month.apply( lambda r: check_spatial_outlier(r, city_month.pivot(index='city_name', columns='month', values='temp_c')), axis=1 ) # 标记空间差异过大的记录 city_month['spatial_outlier'] = city_month['spatial_diff'].abs() > 5逻辑说明:intersects构建邻接关系,比用距离阈值更符合行政区划的实际邻接情况。spatial_diff是目标城市与所有邻居均值的差,超过 5°C 标记为可疑。这个阈值不是绝对的,西部地区城市稀疏,邻居少,阈值可以放宽到 6-7°C。
参数说明:city_month.pivot把长表转成城市×月份的宽表,方便按列取邻居值。注意 pivot 后列名是月份周期,索引是城市名,取邻居时要确保索引对齐。
3.3 月度到年度的聚合口径:生长季均温还是全年均温
很多研究最终用的是年度气温,但年度怎么定义,直接影响结果。常见的口径有:全年 12 个月简单平均、生长季(4-10 月)平均、采暖度日/制冷度日。选哪个取决于你的研究问题。
做农业相关研究,生长季均温比全年均温更有解释力,因为作物对非生长季的温度不敏感。做能源消费研究,采暖度日和制冷度日比简单均温更合适,因为它们直接对应能耗需求。做健康研究,极端气温天数(比如超过 35°C 的天数)可能比均值更重要。
我的建议是:主回归用全年均温,稳健性检验换生长季均温和度日指标。不要只报一个口径,审稿人一定会问。聚合代码很简单:
# 从月度数据聚合到年度 city_month['year'] = city_month['month'].dt.year city_month['month_num'] = city_month['month'].dt.month # 全年均温 annual_all = city_month.groupby(['city_name', 'year'])['temp_c'].mean().reset_index(name='temp_annual') # 生长季均温(4-10月) growing = city_month[city_month['month_num'].between(4, 10)] annual_grow = growing.groupby(['city_name', 'year'])['temp_c'].mean().reset_index(name='temp_growing') # 合并 annual = annual_all.merge(annual_grow, on=['city_name', 'year'], how='left')参数说明:between(4, 10)包含 4 月和 10 月,这是北半球常用的生长季定义。如果你研究的是南方双季稻区,生长季可能要延长到 3-11 月。groupby后mean()会自动跳过 NaN,但如果你已经把缺测月份标记为 NaN,这里求均值时有效月份不足 10 个月的年份应该再标记为缺失。
4. 避坑与排查:地级市气温面板处理中最容易翻车的五个地方
4.1 经度没转换,裁剪出来一片空白
现象:按中国范围裁剪 ERA5 数据后,发现所有值都是 NaN,或者地图上显示的区域明显偏移到了太平洋。
原因:ERA5 的经度坐标默认是 0 到 360,而你的裁剪范围写的是 73 到 135。在 0-360 坐标系里,73-135 对应的是东经 73-135 度,这本身没错,但如果你同时用了sortby('longitude')或者做了其他假设经度是 -180 到 180 的操作,就会错位。
解决:统一坐标系。要么把数据转成 -180 到 180,要么把裁剪范围写成 73 到 135 并确保不跟其他假设 -180 到 180 的代码混用。我习惯统一转成 -180 到 180,因为 geopandas 和大多数 shapefile 都用这个范围。
4.2 空间连接用了 within,小城市全部丢失
现象:空间连接后,发现某些地级市一条记录都没有,尤其是面积小的城市。
原因:predicate='within'要求网格点完全落在城市边界内。0.25° 的网格点间距约 31 公里,如果城市面积小于这个尺度,可能一个点都不在内部。
解决:改用predicate='intersects',让边界穿过的网格也算进来。如果还有缺失,检查城市边界是不是有拓扑错误,或者用最近邻网格值填补。注意intersects会让边界网格被两个相邻城市同时选中,聚合时要注意去重或者接受这种轻微重叠。
4.3 月度聚合时把缺测天数当有效值
现象:某城市某月的平均气温明显偏离历史同期,检查后发现该月只有几天有数据。
原因:groupby().mean()默认跳过 NaN,如果一个月 30 天里只有 5 天有数据,均值就是这 5 天的平均,代表性极差。
解决:聚合前先统计有效天数,设定阈值(我一般用 25 天),不足阈值的月份标记为 NaN。这一步必须在求月均值之前做,否则均值的偏差已经产生,事后没法修正。
4.4 用全年标准差检测异常值,冬季正常低温被误删
现象:异常值检测后,东北地区冬季大量记录被标记为异常,删除后冬季样本严重不足。
原因:气温有强季节性,全年标准差远大于单月标准差。用全年 3σ 卡,冬季低温在全年分布里确实偏离均值很远,但在同月分布里完全正常。
解决:按城市和月份分组,在组内做异常检测。或者直接用空间一致性检查,比较目标城市与邻近城市的同期差异。两种方法交叉验证,只标记不自动删除,人工确认后再处理。
4.5 年度聚合时没有检查有效月份数
现象:某城市某年的年均温看起来正常,但实际上只有 6 个月有数据,另外 6 个月缺测被 mean() 跳过了。
原因:从月度聚合到年度时,groupby().mean()同样会跳过 NaN,如果一年 12 个月里缺了 6 个月,年均值就是剩下 6 个月的平均,但代码不会报错。
解决:年度聚合前先统计每年有效月份数,不足 10 个月的年份标记为缺失。这个阈值可以根据研究需要调整,但一定要有这个检查步骤。我见过太多论文直接 groupby 求均值,缺测问题被完全掩盖。
5. 从面板到回归:让气温数据真正进入模型的三个进阶技巧
5.1 用气温数据构造工具变量
如果你研究的是气温对经济产出的影响,内生性是个绕不开的问题。气温本身外生,但城市的经济活动可能影响局部气候(城市热岛效应),而且遗漏变量可能同时影响气温和产出。常见的工具变量做法是用全球或区域平均气温作为本地气温的工具,或者用大气环流指数(如 ENSO、NAO)作为外生冲击。
用这份面板数据构造工具变量的具体操作:先计算每个城市的气温对全球均温的回归,取拟合值作为工具变量。或者更简单,用同纬度其他城市的气温均值作为该城市的工具。后者的逻辑是,同纬度城市共享大尺度气候信号,但其他城市的气温不会直接影响本地经济产出(排除本地溢出效应)。
# 计算同纬度带其他城市的平均气温作为工具变量 city_month['lat_band'] = (city_month['latitude'] // 5) * 5 # 5度纬度带 iv_temp = city_month.groupby(['lat_band', 'month']).apply( lambda g: (g['temp_c'].sum() - g['temp_c']) / (len(g) - 1) ).reset_index(name='iv_temp') city_month = city_month.merge(iv_temp, on=['lat_band', 'month'], how='left')参数说明:// 5 * 5把纬度分到 5 度带,这个宽度可以调整,太窄会导致带内城市太少,太宽会削弱外生性。(sum - own) / (n - 1)是留一法均值,排除自身。注意这里需要先有城市纬度,可以从城市边界质心提取。
5.2 月度面板的固定效应设定
月度数据做面板回归,固定效应怎么设很有讲究。城市固定效应吸收不随时间变化的城市特征,月份固定效应吸收全国性的季节波动,年份固定效应吸收全国性的年际趋势。但月份和年份的交互项(比如 2020 年 3 月)可以吸收特定年份特定月份的全国性冲击,比如疫情。
我的经验是:基准回归用城市+年份+月份固定效应,稳健性检验加入年份×月份交互固定效应。如果样本量允许,还可以加入城市×月份固定效应,吸收每个城市特有的季节模式。但这样会消耗大量自由度,地级市数量在 300 左右,43 年×12 月=516 个月,城市×月份交互会有 300×12=3600 个虚拟变量,样本量不够时不要轻易加。
5.3 用分位数回归看极端气温的异质性影响
均值回归告诉你平均气温每升高 1°C 对产出的影响,但极端气温的影响可能完全不同。用这份数据做分位数回归,可以看气温分布不同位置的影响差异。实现上,把月度气温按城市分组,计算每个城市每个月的分位数(比如 10%、50%、90%),然后分别作为解释变量进入回归。
# 计算每个城市的气温分位数(基于1982-2024全样本) city_quantiles = city_month.groupby('city_name')['temp_c'].quantile([0.1, 0.5, 0.9]).unstack() city_quantiles.columns = ['temp_p10', 'temp_p50', 'temp_p90'] city_month = city_month.merge(city_quantiles, on='city_name', how='left') # 然后可以用 temp_p90 作为极端高温的代理变量进入回归参数说明:quantile([0.1, 0.5, 0.9])计算三个分位点,unstack()把结果转成宽表。注意这里的分位数是基于全样本计算的,如果你想看时变的分位数,要按年份滚动计算。分位数回归的系数解释和均值回归不同,它告诉你的是在产出分布特定位置上的影响,适合做异质性分析。
最后说一个我自己的习惯:每次处理完这类面板数据,我都会随机抽三个城市、三个年份,手动去核对原始网格数据、空间连接结果和最终面板值,确保中间没有静默的错误。这个习惯帮我抓到过好几次经度转换和缺测标记的问题。数据清洗没有捷径,只有反复验证。希望帮到你。
本文还有配套的精品资源,点击获取