Python气候数据分析:判断七月高温是否破纪录 法国七月的高温和干旱在近年多次刷新历史纪录。“打破纪录”这个说法放到气候数据分析里并不是修辞而是一组可计算、可检验的统计结论某年 7 月的气温或降水指标是否超过了该地区有气象记录以来的对应阈值。要复现这样的分析需要完成数据获取、质量清洗、气候态基准期计算、距平分析、异常检测和干旱指数计算等一系列步骤。下面这套流程从公开气象数据出发搭建一套可运行的 Python 分析链路覆盖从原始数据到“是否破纪录”结论的完整闭环适合正在学习 pandas、xarray、时间序列分析或者从事气象、农业、环境数据分析的读者。1. 先理解气候“破纪录”是怎么被定义出来的1.1 “比往年热”“干旱”不等于破纪录“比往年热”是对当前温度与近期记忆的比较属于主观感受。“破纪录”则是一个统计判断必须放到足够长的历史序列里进行对比。比如某个站点 7 月平均气温比当地 1991-2020 年基准值高了 1.5 摄氏度可以说当季气温明显偏高但只有这个值超过该站点自建站以来的历史最大值才能说明该站点层面的气温纪录被刷新。干旱的情况更复杂。降水偏少不一定直接等于干旱因为干旱还取决于温度、风速、太阳辐射等因素对水分蒸散的影响。高温天气可能让土壤水分快速消耗即使降水量接近常年作物或自然植被也可能出现水分亏缺。因此在分析“七月干旱破纪录”时不能只看降水量还应该看蒸散、土壤湿度、径流等变量。1.2 气候异常分析依赖三类基础数据要分析法国七月高温干旱这类问题通常需要把数据分成三类站点观测数据、格点再分析数据、衍生干旱数据。三类数据各有适用场景落地前要先想清楚自己需要哪一种。数据类别常见字段典型来源分析用途站点观测数据平均气温、最高气温、最低气温、降水量NOAA GHCN、ECAD、各国气象机构公开接口计算站点尺度的月均值、距平、历史纪录格点再分析数据2 米气温、总降水、蒸散量、土壤湿度ERA5、CRU TS空间覆盖完整适合画区域分布图和面积加权统计干旱衍生数据SPI、SPEI、PDSI、土壤湿度指数官方干旱监测数据或基于再分析数据自算描述气象干旱、农业干旱程度站点数据的优点是时间序列长、物理意义直接缺点是空间分布不均。格点数据的优点是空间连续缺点是不同版本之间存在差异历史序列长度也有限。对于初学者建议先用站点数据完成指标计算理解“距平”和“历史排序”的逻辑再切换到格点数据做空间分析。1.3 一次理清关键术语距平、历史百分位、气候基准期“距平”是观测值与气候态基准值之间的差。比如某站 7 月平均气温为 24.3 摄氏度而当地 1991-2020 年 7 月平均气温为 22.8 摄氏度则气温距平为 1.5 摄氏度。距平描述的是相对于历史平均状态的偏离并不是绝对温度本身。“历史百分位”用于衡量某个值在历史序列中的位置。如果某年 7 月的区域平均气温排在历史序列第 99 百分位说明只有约 1% 的年份像当年一样热这是判断“极端”和“破纪录”的重要依据。要注意“破纪录”和“超过 99 分位”并不是同一个概念。破纪录要求超过历史最大值而 99 分位只是超过绝大多数年份二者阈值不同。“气候基准期”是计算气候态的参考时间段。WMO 推荐使用连续的 30 年例如 1981-2010 或 1991-2020。选择不同基准期会直接改变距平值因为基准期本身也受到气候变化影响。分析中必须固定基准期并在结果报告里写清楚否则别人无法复现你的结论。2. 环境准备用 Python 搭建一套可复现的分析环境2.1 Python 依赖安装与版本说明这套分析主要依赖 pandas、numpy、xarray、matplotlib、scipy。如果处理 netCDF 格点数据还需要 netCDF4。下面是一个相对稳定的依赖组合实际安装时可以根据自己的 Python 版本微调。# requirements.txt 示例 pandas2.1.4 numpy1.26.3 xarray2024.1.1 netCDF41.6.5 matplotlib3.8.2 scipy1.12.0pip install -r requirements.txt如果机器上已经装了较新的 pandas 或 numpy不要盲目降级。优先用虚拟环境隔离项目避免影响其他工程。创建虚拟环境的常用命令如下。python -m venv .venv source .venv/bin/activate pip install -r requirements.txt代码示例中的 API 基于 pandas 2.x如果使用 pandas 1.x部分行为可能略有差异例如parse_dates、groupby.apply的结果类型会更依赖旧版实现。学习阶段建议直接使用 2.x。2.2 公开气候数据源怎么选不同数据源适合不同任务。如果只想验证方法优先选下载简单、格式稳定、字段清晰的数据如果要处理真实的法国区域事件则要检查数据的时间范围和空间覆盖。数据源数据形态空间范围获取方式适合场景NOAA GHCN-Daily站点观测 CSV全球FTP 或 API按站点做长序列分析ECAD欧洲站点观测欧洲页面下载或 API法国及欧洲站点级分析Copernicus ERA5格点再分析 netCDF全球CDS API空间分布、区域面积加权统计CRU TS格点插值降水气温全球陆地页面下载月尺度快速分析字段简单获取时序数据时要重点确认三件事数据是否更新到目标年份、站点在法国区域内是否有足够的连续记录、缺失值标记是什么。很多数据集用-9999表示缺失直接用均值计算会把缺失值当成真实数值导致结果严重失真。2.3 下载前的数据字段检查清单在写任何分析代码之前先统计一份字段检查清单避免程序写着写着才发现单位或时间频率不对。时间频率是日值、月值还是小时值。日期是否已经统一到 UTC是否需要转换到欧洲时区。温度单位是摄氏度还是开尔文。降水单位是毫米还是千克每平方米。站点经纬度是十进制度还是度分秒。缺省标记是 NaN、-9999 还是 32767。数据版本号是什么是否会影响和历史数据的拼接。注意不要只检查前几行数据。要检查整个时间范围的最小日期、最大日期、站点数量和缺失值比例这些信息决定了后续计算的可靠性。3. 一个最小分析案例处理法国区域 7 月气温与降水数据3.1 项目目录结构和数据文件布局推荐把原始数据、处理结果、脚本、图表分开存放方便追踪数据版本和分析步骤。一个最小项目结构如下。climate-france-july/ ├── data/ │ ├── raw/ │ │ ├── temperature_station.csv │ │ └── precipitation_station.csv │ ├── processed/ │ └── external/ ├── scripts/ │ ├── 01_load_and_clean.py │ ├── 02_climate_anomaly.py │ └── 03_drought_index.py ├── output/ │ ├── fig/ │ └── report/ └── README.mddata/raw下面的文件要保持只读所有清洗和派生结果都写入data/processed。这样一旦计算结果异常可以直接回到原始文件重新处理不会因为中间过程被覆盖而丢失线索。3.2 读取与清洗把杂乱的气象数据整理成统一 DataFrame假设下载到的站点数据包含日期、站点编号、站点名称、平均气温、降水量等字段。实际文件里列名可能叫tavg、tmean、tg也可能叫time、date所以第一步是统一列名。import pandas as pd # 假设原始 CSV 包含 date, station_id, station_name, tavg, prcp 等字段 df_raw pd.read_csv(data/raw/temperature_station.csv, parse_dates[date]) print(df_raw.head()) # 统一列名真实项目按数据源元数据确认 column_map { date: date, time: date, tavg: tavg, tmean: tavg, tg: tavg, prcp: prcp, rr: prcp, } rename_map {k: v for k, v in column_map.items() if k in df_raw.columns} df_raw df_raw.rename(columnsrename_map) # 只保留 7 月数据 df_july df_raw[df_raw[date].dt.month 7].copy() df_july[year] df_july[date].dt.year # 去重同一站点同一日期出现多次时保留最后一条 df_july df_july.drop_duplicates(subset[station_id, date], keeplast) # 缺失值标记统一转成 NaN df_july[tavg] df_july[tavg].replace(-9999.0, pd.NA) df_july df_july.dropna(subset[tavg])这一步最容易出错的地方是只按月份筛选没有考虑年份。date.dt.month 7会选出所有年份的 7 月这是正确的。但如果后续要按日聚合就要同时保留 year、month、day 三个字段避免直接按字符串月份过滤。3.3 计算月均值和距平破纪录判断的核心指标站点数据通常是日值。要做“法国 7 月气温破纪录”分析需要先把日值按站点和年份聚合为月均值再选择气候基准期计算距平。# 按站点和年份计算 7 月平均气温 july_tavg ( df_july.groupby([station_id, station_name, year])[tavg] .mean() .reset_index() ) # 选择气候基准期例如 1991-2020 base_start, base_end 1991, 2020 base july_tavg[(july_tavg[year] base_start) (july_tavg[year] base_end)] clim base.groupby(station_id)[tavg].mean().rename(tavg_clim) # 合并并计算距平 july_tavg july_tavg.merge(clim, onstation_id, howleft) july_tavg[anomaly] july_tavg[tavg] - july_tavg[tavg_clim]这里有两个关键点。第一基准期选择会影响所有距平结果所以必须显式定义变量base_start、base_end而不是在代码里写死多个地方。第二groupby聚合时默认会忽略 NaN但如果某年 7 月有大量缺失日数算出来的月均值会有偏需要设定最小有效日数检查。比如某站点 7 月只有 5 天有观测这 5 天的平均不能代表整月。3.4 用历史百分比排名判断是否“创纪录”拿到每个站点每年的 7 月均值后可以用rank计算该值在历史序列中的百分位再判断是否为历史最大值。需要注意这里计算的是“月平均气温”序列而不是“日最高气温”序列。这是初学者最容易混淆的地方。# 为每个站点计算历史百分位和历史最高值 def judge_record(frame): frame frame.sort_values(year).copy() frame[rank_pct] frame[tavg].rank(pctTrue) * 100 frame[is_hist_max] frame[tavg] frame[tavg].max() return frame station_result ( july_tavg.groupby(station_id, group_keysFalse) .apply(judge_record) .reset_index(dropTrue) ) # 查看单个站点结果 one_station station_result[station_result[station_id] FR000123] print(one_station.tail(5))rank(pctTrue)计算的是百分位排名。如果一个站点的 2023 年 7 月均温排名是 100说明它超过了历史上所有年份可以被认定为该站点层面的“破纪录”。如果是 99说明它超过了 99% 的年份属于极端高温但还没有刷新历史纪录。对于“法国七月破纪录”这种区域级结论不能只凭一个站点判断。需要先计算区域内所有站点的平均值或面积加权平均值生成一条区域时间序列再在区域序列上做历史排名。站点数不均匀也会造成偏差比如某个地区站点特别多简单平均会高估该地区的权重。4. 干旱维度不只降水少更要看蒸散和土壤湿度4.1 为什么只看降水量不够降水偏少是干旱的常见触发因素但不是唯一因素。高温会造成潜在蒸散增大让土壤和植被失去更多水分。如果某年 7 月降水接近常年但气温显著偏高实际可用水量可能仍然不足。因此在干旱分析中要引入水分收支的概念降水是收入项蒸散是支出项。在气象干旱分级里常见指标包括 SPI 和 SPEI。SPI 只基于降水计算简单SPEI 同时考虑降水和潜在蒸散更贴近高温驱动的干旱。法国夏季高温干旱场景中SPEI 通常比 SPI 更能反映水分亏缺。4.2 用标准化降水蒸散指数描述干旱SPEI 的基本思路是把逐月降水和潜在蒸散的差值也就是气候水平衡累积到指定时间尺度然后拟合概率分布转换成标准正态分布的分位数。负值代表干旱正值代表湿润。直接使用成熟的库会比手写可靠。科学计算里常见的选择是 Python 的spei、climindvis或者 R 语言相关包。如果只是学习原理也可以先写一个简化版用正太分布近似代替复杂分布拟合。4.3 把 SPEI 计算封装成通用函数下面是一个用于说明计算思路的简化函数它接受月降水和月潜在蒸散序列返回近似 SPEI 值。真实项目建议替换为成熟实现并做分布拟合检验。import numpy as np import pandas as pd from scipy.stats import gamma, norm def spei_like(prcp, pet, timescale1, fit_periodNone): 简化版标准化降水蒸散指数计算。 参数 prcp: 月降水序列 pet: 月潜在蒸散序列 timescale: 累积月尺度例如 1、3、6 fit_period: 分布拟合所用的时间范围例如 (1991-01-01, 2020-12-31) bal prcp - pet if timescale 1: bal bal.rolling(timescale, min_periods1).sum() valid bal.dropna() if fit_period is not None: valid valid.loc[fit_period[0]:fit_period[1]] # 去掉非正值避免 gamma 分布拟合失败 pos valid[valid 0] if len(pos) 12: return pd.Series(np.nan, indexbal.index, namespei) shape, loc, scale gamma.fit(pos) prob gamma.cdf(bal, shape, locloc, scalescale) spei norm.ppf(prob.clip(1e-6, 1 - 1e-6)) return pd.Series(spei, indexbal.index, namespei)使用示例# 假设已有逐月数据precip 和 pet 都是 pandas Series索引为月份 spei_series spei_like(precip, pet, timescale1) print(spei_series.tail(6))注意这个简化版不具备严格统计学意义。SPEI 的正式计算需要选择合适的概率分布并对 0 值做专门处理。只有理解了原始算法才能判断简化版在什么情况下可用。5. 数据可视化与结果解释5.1 画时间序列和距平柱状图分析结果如果只用表格呈现很难快速定位“破纪录”的年份。推荐至少画两张图一张是某站点或区域 7 月均温的时间序列另一张是所有年份相对基准期的距平柱状图。时间序列图适合看长期趋势和极值距平图适合看正负偏离。import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(10, 5)) ax.plot(one_station[year], one_station[tavg], markero, linewidth0.8) max_year one_station.loc[one_station[is_hist_max], year].iloc[-1] ax.scatter( [max_year], one_station.loc[one_station[year] max_year, tavg], colorred, zorder3, labelfrecord: {int(max_year)}, ) ax.axhline(one_station[tavg_clim].iloc[-1], colorgray, linestyle--, labelclim 1991-2020) ax.set_xlabel(year) ax.set_ylabel(July mean temp (C)) ax.set_title(July mean temperature at one station) ax.legend() plt.tight_layout() plt.savefig(output/fig/july_tavg_station.png, dpi150)绘图时要注意单位。如果数据源里温度是开尔文必须先减 273.15 转成摄氏度否则图上纵轴会显示 300 左右的数值容易误导。5.2 空间分布图怎么处理如果使用的是 netCDF 格点数据可以用 xarray 读取变量再通过 contourf 或 pcolormesh 绘制空间分布。法国区域需要保留合适的经纬度范围通常只裁剪到法国本土范围避免把大西洋和北非大范围区域也画出来。import xarray as xr ds xr.open_dataset(data/raw/era5_july_t2m.nc) # 根据实际变量名修改 tas ds[t2m] - 273.15 july_mean tas.sel(timetas[time].dt.month 7).groupby(time.year).mean(time) # 提取某一年与基准期距平 anomaly july_mean.sel(year2023) - july_mean.sel(yearslice(1991, 2020)).mean(year)如果没有安装 cartopy格点空间图也可以先用站点散点图代替把站点气温距平按颜色画到经纬度坐标上。这样做虽然不如格点图平滑但也能看出空间差异。5.3 结果报告应包含哪些内容任何“破纪录”结论都必须能被复核所以最终报告至少包含五部分数据来源与版本、处理脚本位置、指标定义、阈值标准、不确定性说明。缺少任何一部分读者都无法判断结论成立的条件。一个可复用的输出结构是数据源名称、下载时间、数据版本。站点筛选条件如最少有效观测天数、最长连续缺失时间。气候基准期如 1991-2020。指标定义如“站点 7 月平均气温”或“区域面积加权平均气温距平”。破纪录判定规则如“超过历史序列最大值”。结果表包括站点编号、年份、月均值、距平、百分位、是否破纪录。6. 常见问题与排查路径6.1 数据下载失败或字段对不上现象是脚本读取 CSV 时报KeyError或ParserError。常见原因是数据源更新了列名、文件编码不是 UTF-8、或者下载的文件实际是 JSON 格式。排查顺序是先看文件头再查数据源文档最后再改代码。head -20 data/raw/temperature_station.csv问题现象常见原因检查方式处理建议读取报 UnicodeDecodeError文件编码不是 UTF-8检查文件头查看编码声明用encodinglatin1或encoding_errorsignore调整列名对不上数据源更新字段名print(df_raw.columns)维护 column_map 映射表不要按位置取列时间列解析失败日期格式不统一查看原始字符串样例指定date_format或用pd.to_datetime的多种格式尝试下载后文件只有几 KB请求失败或需要登录查看文件大小和首行内容检查网络请求状态码、认证参数重新下载6.2 月份时间戳错位一整天或一个月现象是每年 7 月的数据里混入了 6 月末或 8 月初的数据或者月均值偏低。常见原因有两个数据源使用 UTC 时间而法国本土在东一时区夜间 0 点到 1 点的数据在 UTC 下仍然显示为前一天另一个原因是重采样时使用了M而不是ME造成日期偏移。处理方式是把时间统一到本地时区并明确“日数据交换时间”。很多气象日值数据以当地 06 时为日界而不是自然日 0 点。# 示例把 UTC 时间校正到欧洲/巴黎时区 df[date_paris] df[date_utc].dt.tz_convert(Europe/Paris) # 如果数据源每日 06 UTC 交换代表当地当日数据 df[date_local] (df[date_utc] - pd.Timedelta(hours6)).dt.tz_localize(None)时间错位问题通常不会报错只会在最终结果里表现为“1 月数据偏少”或“7 月均值偏低”这类隐性错误最需要警惕。建议在聚合后输出每个站点的有效天数分布如果发现某年 7 月只有 28 天就说明日界或过滤逻辑有问题。6.3 结果出现 NaN 或极端离群值NaN 过多时先确认缺失值标记是否已经统一。极端离群值则需要区分是真实极端天气还是单位错误。例如温度数据混入了开尔文和摄氏度两种单位会造成 300 左右的巨大离群值。# 查看温度分布快速发现单位问题 print(df_july[tavg].describe()) # 统计缺失比例 missing_ratio df_july[tavg].isna().groupby(df_july[station_id]).mean() print(missing_ratio.sort_values(ascendingFalse).head(10))如果某个站点的温度超过 55 摄氏度或低于 -20 摄氏度就要检查是否站点搬迁、传感器故障或单位转换遗漏。不要轻易删除离群值而是先结合站点元数据判断。6.4 “破纪录”被误判的三种情况第一种误判是用单日极值代替月均值纪录。“7 月某天最高气温达到 42 摄氏度”和“7 月平均气温达到历史最高”是两个不同指标不能混用。新闻标题经常说高温破纪录指的可能是单日最高温但气候统计里的“7 月破纪录”通常指月均值。第二种误判是混用不同气候基准期。一个脚本用 1981-2010 计算距平另一个脚本用 1991-2020 计算距平两者比较时会出现系统性偏差。必须在所有图表和表格中统一基准期。第三种误判是数据序列长度不一致。有的站点只有 10 年历史有的站点有 80 年历史把它们放在一起比百分位并不公平。处理办法是设定最小年份数比如要求至少 30 年完整记录才参与“历史纪录”排名。7. 生产环境与长期监测建议7.1 从研究脚本到定时任务分析脚本在 notebook 里跑通只是第一步。如果要做长期监测比如每年 8 月自动发布上一月的高温干旱简报就需要把脚本结构化为可执行文件并配置定时调度。生产环境下可以参考如下流程把数据下载、清洗、指标计算、图表输出拆成独立 Python 文件。每个文件都使用if __name__ __main__:入口方便命令行执行。使用 cron 或调度平台定时运行并保留每次运行的日志。输出文件带上日期或数据版本号避免覆盖历史结果。增加运行状态检查例如数据行数小于预期时发送告警。# 示例每天凌晨 2 点检查并下载最新数据 0 2 * * * cd /path/to/climate-france-july /path/to/.venv/bin/python scripts/01_download_data.py logs/fetch.log 217.2 数据版本管理和元数据气候数据会不断更新同一份 7 月数据在不同时间下载可能因为延迟订正而略有不同。为了让“破纪录”结论可追溯必须在输出目录中记录数据源版本和下载时间。推荐把元数据写入一个 JSON 文件随结果一起保存。{ dataset: example_climate_station, download_time: 2025-07-01T06:00:00Z, version: v2, base_period: 1991-2020, variable_unit: { tavg: celsius, prcp: mm }, missing_flag: -9999 }生产环境还需要考虑数据源接口是否稳定、是否需要登录认证、是否限制每秒请求次数。不要把下载逻辑和分析逻辑混在同一个脚本里否则一旦数据源变更会影响整个流程。7.3 分析结论的确定性表述气候分析结论要谨慎尤其是涉及公共传播时。与其说“法国七月已经破纪录”不如说“在某数据集、某区域范围内某年 7 月区域平均气温超过了该数据集历史最高值”。限定条件越清晰结论越稳健。建议在报告中使用以下表述模板“基于 A 数据集覆盖 B 区域使用 C 指标某年 7 月达到历史第 1 位。”“历史序列长度为 N 年站点有效记录比例超过 90%。”“干旱指数采用 SPEI-1负值代表气象干旱。”这样即使数据源后续更新也能定位是数据问题还是计算问题。8. 扩展方向从月尺度到事件归因8.1 聚焦热浪事件“7 月均温破纪录”是月尺度统计而热浪事件研究通常关注连续高温日数、热浪频次、热浪强度。两者可以结合某年 7 月均温很高可能是若干天极端高温拉高的结果也可能是整月持续偏高。如果只看月均值会掩盖热浪过程的细节。常用指标包括连续高温日数最高气温连续超过阈值的天数。热浪强度超过阈值部分与持续时间的累积。热浪频次某年夏季出现热浪过程的次数。热浪覆盖面积高温格点占区域总面积的比例。8.2 从统计异常走向机制分析统计异常回答“是否破纪录”机制分析回答“为什么破纪录”。后者通常需要看大气环流背景例如副热带高压位置、急流形态、海温异常等。这部分涉及更多再分析数据比如位势高度场、海平面气压场、海表温度分析方法也从时间序列走向物理过程诊断。对于初学者不建议一上来直接做归因。先完成“观测事实”层面的分析把气温、降水、干旱指数的时间序列和空间分布做扎实再引入环流背景检查。8.3 给学习者的实践清单如果要从零上手这类气候事件分析可以按下面清单逐项完成。下载一个站点 30 年以上的逐月气温和降水数据。清洗缺失值并统一日期格式。计算每年的 7 月平均值和相对基准期的距平。计算历史百分位标记是否破纪录。下载或计算潜在蒸散序列计算 SPEI-1。绘制时间序列图、距平图和干旱指数图。在报告里写清数据来源、基准期、缺失值处理和判定规则。这套流程完成后整个项目就能覆盖从数据到结论的完整链路并且具备可复现性。月平均气温或降水距平只是气候描述的第一步真正对“破纪录”结论负责的是阈值定义、数据版本和时空口径。一个可靠的气候异常分析脚本必须让这三者都能回溯。后续遇到法国七月高温干旱或类似事件直接按同一套方法分析即可不需要重新设计框架。