ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

气象统计实习数据包处理全攻略:从缺测值到趋势分析

气象统计实习数据包处理全攻略:从缺测值到趋势分析

简介:这份气象统计实习资料面向气象、大气科学及数据科学方向的初学者与进阶学习者,围绕Python在气象数据处理中的完整流程展开,帮助解决从原始观测数据到统计分析与可视化呈现的实操问题。压缩包共74个文件,以72张png结果图、1个py脚本和1个dat数据文件为主,整体约4.38MB,其中脚本负责数据读取与计算,dat文件提供H500等气象场原始数据,png则对应气候场、均方差、距平场等分析图件,便于对照代码与输出结果。目前已有1298人学习下载。资源覆盖气象数据获取与清洗、缺失值与异常值处理、基于numpy和scipy的均值与标准差等统计量计算,以及matplotlib、seaborn绘图和geopandas、plotly空间可视化等环节,读者可借此掌握一套可复用的气象数据分析流程,理解温度、湿度、风速、气压等变量的时空变化规律,为气象预报与气候研究打下编程与统计基础。

1. 气象统计实习一.rar:一个压缩包背后藏着多少气象数据处理的门道

拿到「气象统计实习一.rar」这个标题,很多人第一反应是去找解压密码,但真正做过气象数据处理的人会先问一句:里面装的是站点观测数据、格点再分析资料,还是某次实习课的示例数据集?这个压缩包本质上是一个教学或实训场景下的数据与脚本集合,核心用途是让学习者用真实气象观测记录跑通「数据清洗→统计量计算→可视化→结果解读」这条链路。它适合气象、大气科学、环境科学方向的学生,也适合刚转入气象数据分析岗位、需要快速补齐实操经验的开发者。你不需要气象学博士背景,但得会一点 Python 或 R,能看懂 CSV 和 NetCDF 的基本结构。这个包的价值不在文件本身,而在于它逼你面对真实气象数据里那些教科书不讲的脏活:缺测值、单位不统一、时间戳错位、站点迁移导致的序列断裂。

2. 先搞清楚压缩包里该有什么:气象统计实习的数据组织逻辑

2.1 典型气象统计实习的数据分层

一个结构清晰的气象统计实习压缩包,通常不会把所有东西堆在一个文件夹里。我见过比较合理的组织方式是三层:原始数据层、中间处理层、结果输出层。原始数据层放站点观测的 CSV 或 Excel,比如某地区多个气象站逐日或逐小时的气温、气压、风速、降水量;中间处理层放清洗后的统一格式文件,通常是 CSV 或 Parquet;结果输出层放统计图表和汇总表。如果压缩包里只有一堆命名混乱的 xlsx,那说明整理者自己也没想清楚流程,你得先做一轮文件审计。

常见的气象数据格式有几种:站点数据多用 CSV 或 Excel,格点数据多用 NetCDF 或 GRIB,再分析资料如 ERA5 下载下来是 NetCDF。实习包一般以站点数据为主,因为格点数据体量大、读取门槛高。你拿到包后先看文件扩展名分布,用一条命令就能摸清底细:

# 统计压缩包内文件类型分布,先解压到临时目录 mkdir -p /tmp/met_insight && tar -xf 气象统计实习一.rar -C /tmp/met_insight 2>/dev/null || unzip -q 气象统计实习一.rar -d /tmp/met_insight find /tmp/met_insight -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn

这段命令先尝试用 tar 解压,失败则用 unzip,兼容 rar 和 zip 两种常见封装。find列出所有文件,sed提取扩展名,uniq -c统计每种扩展名出现次数。如果输出里 csv 占大头,说明是站点数据;如果 nc 或 grib 多,说明涉及格点。参数上注意-C指定解压目录,避免污染当前工作区。

2.2 站点数据的字段含义与单位陷阱

站点气象数据最常见的字段包括:站号、纬度、经度、海拔、观测时间、气温、气压、相对湿度、风速、风向、降水量。这里有几个单位陷阱必须提前确认。气温可能是摄氏度也可能是开尔文,气压可能是百帕也可能是帕斯卡,风速可能是米每秒也可能是节。降水量更麻烦,有的是累计值,有的是逐小时增量,有的在整点记录的是过去一小时累计。你不确认单位就直接算平均,结果能偏出十万八千里。

我一般会先读一个样本文件,看字段名和数值范围。气温如果在 250 到 320 之间,基本是开尔文;在 -30 到 45 之间,基本是摄氏度。气压如果在 1000 附近,是百帕;在 100000 附近,是帕斯卡。风速如果在 0 到 30 之间,大概率是米每秒;在 0 到 60 之间,可能是节。这些经验判断能帮你快速定位单位问题。

import pandas as pd # 读取单个站点文件,先看前几行和统计摘要 df = pd.read_csv('/tmp/met_insight/station_001.csv', encoding='utf-8') print(df.head(10)) print(df.describe()) # 检查气温字段范围,判断单位 temp_col = [c for c in df.columns if 'temp' in c.lower() or '气温' in c][0] temp_min, temp_max = df[temp_col].min(), df[temp_col].max() if temp_min > 200: print(f"气温范围 {temp_min:.1f}-{temp_max:.1f},疑似开尔文,需减 273.15") elif -60 < temp_min < 60: print(f"气温范围 {temp_min:.1f}-{temp_max:.1f},疑似摄氏度") else: print(f"气温范围异常:{temp_min:.1f}-{temp_max:.1f},需人工核查")

这段代码先加载 CSV 并打印摘要,然后自动判断气温单位。describe()给出均值、标准差、四分位数,能快速发现异常值。单位判断逻辑用阈值法,开尔文气温不会低于 200,摄氏度不会高于 60。参数上注意encoding要跟文件实际编码一致,中文气象数据常用 gbk 或 utf-8,读错了会报 UnicodeDecodeError。

2.3 时间序列的完整性检查

气象统计最怕时间序列有洞。缺测值可能被标成 -999、9999、NaN 或者空字符串,不同来源的数据标记方式不一样。你得先统一缺测标记,再检查时间轴是否连续。逐小时数据应该有 24 条每天,逐日数据应该有 365 或 366 条每年。如果某站某年只有 300 条,说明有 65 天缺失,算年平均值时直接除 365 会偏低。

# 统一缺测标记并检查时间连续性 import numpy as np df = pd.read_csv('/tmp/met_insight/station_001.csv', encoding='utf-8') df.replace([-999, 9999, 99999, ''], np.nan, inplace=True) # 解析时间列,假设列名为 'time' df['time'] = pd.to_datetime(df['time'], errors='coerce') df = df.dropna(subset=['time']).sort_values('time') # 检查逐小时数据的每日记录数 daily_count = df.groupby(df['time'].dt.date).size() print(f"每日记录数分布:\n{daily_count.value_counts().head()}") # 找出记录数不足 24 的日期 incomplete_days = daily_count[daily_count < 24] print(f"不完整天数:{len(incomplete_days)},示例:\n{incomplete_days.head(10)}")

这段代码先把常见缺测标记替换成 NaN,再解析时间列并排序。groupby按日期分组统计每日记录数,value_counts看分布。如果大部分日期是 24 条,少数不足,说明那些天有缺测。参数上errors='coerce'让无法解析的时间变成 NaT,避免整行报错。dropna(subset=['time'])只删时间缺失的行,保留其他字段的 NaN 供后续插补。

3. 用 Python 跑通气象统计核心计算:从平均到趋势

3.1 基础统计量的计算与缺测处理

气象统计实习的核心任务通常包括:计算月平均气温、年降水量、风速频率分布、气温日较差、相关系数等。这些计算本身不复杂,难的是缺测值怎么处理。常见策略有三种:整段删除、线性插补、气候态替代。整段删除适合缺测比例低于 5% 的情况;线性插补适合短时缺测,比如连续几小时;气候态替代适合长期缺测,用多年同期平均填充。

我一般先用整段删除跑一遍,看结果是否合理,再决定要不要插补。因为插补会引入人为信号,在趋势分析里可能造成虚假趋势。如果缺测集中在某几年,插补后算出的趋势可能完全不可信。

# 计算月平均气温,分别用删除和插补两种策略对比 df['month'] = df['time'].dt.to_period('M') # 策略一:直接删除缺测 monthly_drop = df.dropna(subset=['temp']).groupby('month')['temp'].mean() # 策略二:线性插补后计算 df['temp_interp'] = df['temp'].interpolate(method='linear', limit=6) monthly_interp = df.groupby('month')['temp_interp'].mean() # 对比两种策略的差异 diff = (monthly_interp - monthly_drop).abs() print(f"两种策略月平均气温最大差异:{diff.max():.2f} 摄氏度") print(f"差异超过 0.5 度的月份数:{(diff > 0.5).sum()}")

这段代码先按月分组,分别用删除和插补两种方式算月平均。interpolate的limit=6表示最多连续插补 6 个缺测,超过就保留 NaN,避免长段插补失真。对比差异能帮你判断缺测对结果的影响程度。如果差异超过 0.5 度的月份很多,说明缺测不可忽视,得在报告里说明处理方式。

3.2 气温趋势分析:线性回归与 Mann-Kendall 检验

趋势分析是气象统计实习的进阶内容。最简单的是对年均气温做线性回归,斜率就是趋势。但气象数据往往不满足正态分布和方差齐性,所以更稳健的做法是 Mann-Kendall 非参数检验。Python 里可以用pymannkendall库,也可以自己实现。

# 年均气温趋势:线性回归 + Mann-Kendall import numpy as np from scipy import stats annual = df.dropna(subset=['temp']).groupby(df['time'].dt.year)['temp'].mean() years = annual.index.values temps = annual.values # 线性回归 slope, intercept, r_value, p_value, std_err = stats.linregress(years, temps) print(f"线性趋势:{slope:.4f} 摄氏度/年,p={p_value:.4f},R²={r_value**2:.3f}") # Mann-Kendall 简易实现 def mann_kendall(x): n = len(x) s = 0 for i in range(n-1): for j in range(i+1, n): s += np.sign(x[j] - x[i]) var_s = n*(n-1)*(2*n+5)/18 if s > 0: z = (s-1)/np.sqrt(var_s) elif s < 0: z = (s+1)/np.sqrt(var_s) else: z = 0 p = 2*(1 - stats.norm.cdf(abs(z))) return z, p z, p_mk = mann_kendall(temps) print(f"Mann-Kendall:Z={z:.3f},p={p_mk:.4f}")

线性回归给出趋势斜率和显著性,linregress返回的p_value小于 0.05 说明趋势显著。Mann-Kendall 的s统计量累加所有点对的符号差,var_s是方差,z是标准化统计量。参数上注意years和temps要一一对应,缺测年份要么插补要么剔除,不能留空。两种方法结果一致时趋势更可信,不一致时优先信 Mann-Kendall,因为它对异常值不敏感。

3.3 风玫瑰图与降水频率分布

风玫瑰图是气象统计的经典可视化,展示风向频率和风速分布。Python 里用windrose库最方便,没有的话用 matplotlib 的极坐标图也能画。降水频率分布通常按等级统计,比如小雨、中雨、大雨、暴雨各占多少天。

# 风玫瑰图数据准备 from windrose import WindroseAxes import matplotlib.pyplot as plt # 假设 df 有 'wind_speed' 和 'wind_direction' 列 fig = plt.figure(figsize=(8, 8)) ax = WindroseAxes.from_ax(fig=fig) ax.bar(df['wind_direction'], df['wind_speed'], normed=True, opening=0.8, edgecolor='white') ax.set_legend(title='风速 (m/s)') plt.savefig('/tmp/met_insight/windrose.png', dpi=150, bbox_inches='tight') # 降水等级统计 bins = [0, 0.1, 10, 25, 50, 100, 9999] labels = ['无降水', '小雨', '中雨', '大雨', '暴雨', '大暴雨'] df['precip_level'] = pd.cut(df['precip'], bins=bins, labels=labels, right=False) level_counts = df['precip_level'].value_counts().reindex(labels) print(level_counts)

风玫瑰图的normed=True表示按频率归一化,opening=0.8控制扇形间隙。降水等级用pd.cut分箱,right=False表示左闭右开区间,避免 10.0 被分到小雨。参数上 bins 的边界要根据当地气候调整,干旱区暴雨阈值可以降低,湿润区可以调高。

4. 避坑指南:气象统计实习里最容易翻车的五个地方

4.1 缺测值标记不统一导致统计量失真

现象:算出的月平均气温比邻近站点低好几度,或者年降水量明显偏少。原因:不同文件用不同缺测标记,有的用 -999,有的用 9999,有的用空字符串,你只替换了一种,其他被当成真实值参与计算。解决:读入所有文件后,先扫描每列的数值范围,把明显超出物理极限的值统一替换成 NaN。气温低于 -100 或高于 60、气压低于 800 或高于 1100、风速大于 80 的值都值得怀疑。

4.2 时间戳时区不统一造成日界错位

现象:逐小时数据按日期分组时,某些天多出或少了几条记录。原因:部分数据用 UTC 时间,部分用北京时间,混在一起后日界不一致。解决:统一转成同一时区再分组。如果原始数据没有时区标记,看数据来源说明,或者用日出日落时间反推。气象上常用 UTC,但国内站点数据很多是北京时间,转换时加 8 小时或减 8 小时要搞清楚方向。

4.3 站点迁移导致序列断裂被误判为气候突变

现象:某站气温序列在某一年突然跳变,Mann-Kendall 检验显示显著突变。原因:站点从山顶搬到山脚,海拔变化导致气温系统性偏移,不是气候突变。解决:查站史元数据,看是否有迁移记录。没有元数据时,对比邻近站点,如果只有这一个站跳变,大概率是迁移或仪器更换。处理方式是把迁移前后分成两段分别分析,或者做均一性检验。

4.4 插补方法选择不当引入虚假趋势

现象:插补后算出的升温趋势比删除缺测后强很多。原因:线性插补在缺测段两端数值差异大时,会人为拉平或拉陡序列,长期缺测插补后趋势被放大。解决:缺测比例超过 10% 的年份直接剔除,不要插补。短时缺测用线性插补,但限制连续插补长度不超过 6 个时间步。趋势分析前先画缺测分布图,看缺测是否集中在某段时期。

4.5 单位换算遗漏导致物理量量级错误

现象:风速频率分布图里风速集中在 0 到 5 之间,但实际该地常年大风。原因:原始风速单位是节,你没换算成米每秒,1 节约等于 0.514 米每秒,数值偏小一半。解决:读数据前先确认单位,气温、气压、风速、降水都要查。换算关系记牢:开尔文减 273.15 得摄氏度,帕斯卡除以 100 得百帕,节乘以 0.514 得米每秒。

5. 从实习包到可复用流程:把一次性分析变成自动化脚本

5.1 用配置文件管理站点和参数

每次实习都改代码里的路径和参数很烦,我习惯用一个 YAML 配置文件把站点列表、字段映射、单位换算、缺测标记都写进去。这样换一批数据只改配置,不动代码。

# config.yaml stations: - id: "001" path: "/tmp/met_insight/station_001.csv" time_col: "time" temp_col: "temp" temp_unit: "celsius" precip_col: "precip" precip_unit: "mm" - id: "002" path: "/tmp/met_insight/station_002.csv" time_col: "datetime" temp_col: "temperature" temp_unit: "kelvin" precip_col: "rain" precip_unit: "mm" missing_values: [-999, 9999, 99999, ""] interp_limit: 6 output_dir: "/tmp/met_insight/output"

配置文件把每个站点的列名和单位都列清楚,missing_values统一缺测标记,interp_limit控制插补长度。参数上注意 YAML 对缩进敏感,用空格不用 Tab。站点多了以后,配置文件比代码更易维护。

5.2 批量处理与结果汇总

有了配置后,写一个循环批量处理所有站点,把月统计、年统计、趋势结果汇总到一张表里。这样对比不同站点时不用来回翻文件。

import yaml import pandas as pd import numpy as np with open('config.yaml', 'r', encoding='utf-8') as f: cfg = yaml.safe_load(f) results = [] for st in cfg['stations']: df = pd.read_csv(st['path'], encoding='utf-8') df.replace(cfg['missing_values'], np.nan, inplace=True) df['time'] = pd.to_datetime(df[st['time_col']], errors='coerce') df = df.dropna(subset=['time']).sort_values('time') # 单位换算 if st['temp_unit'] == 'kelvin': df[st['temp_col']] = df[st['temp_col']] - 273.15 # 月统计 df['month'] = df['time'].dt.to_period('M') monthly = df.groupby('month')[st['temp_col']].mean() # 年统计 annual = df.groupby(df['time'].dt.year)[st['temp_col']].mean() results.append({ 'station': st['id'], 'monthly_mean': monthly.mean(), 'annual_trend': np.polyfit(annual.index, annual.values, 1)[0] if len(annual) > 2 else np.nan }) summary = pd.DataFrame(results) summary.to_csv(f"{cfg['output_dir']}/summary.csv", index=False) print(summary)

这段代码遍历配置里的站点,统一做缺测替换、时间解析、单位换算,然后算月平均和年趋势。np.polyfit做一次多项式拟合,返回斜率。参数上len(annual) > 2保证至少三年数据才拟合趋势,否则返回 NaN。汇总表输出到配置指定的目录,方便后续做站点对比。

5.3 结果验证:用邻近站点交叉检查

自动化跑完后,别急着写报告。挑几个邻近站点,对比它们的月平均气温和年降水量,如果差异超过气候学合理范围,说明某个站的数据有问题。我一般会画一张散点图,横轴是站点 A 的月平均,纵轴是站点 B 的月平均,理想情况下点应该落在 1:1 线附近。偏离太远的月份回去查原始数据,看是缺测、单位还是时间戳问题。

import matplotlib.pyplot as plt # 假设 summary 里有 station 和 monthly_mean 列 pivot = summary.pivot_table(index='station', values='monthly_mean') # 这里简化处理,实际应该按月对齐两个站点的序列 # 画散点图对比两个站点的月平均气温 plt.scatter(monthly_a, monthly_b, alpha=0.6) plt.plot([min(monthly_a), max(monthly_a)], [min(monthly_a), max(monthly_a)], 'r--') plt.xlabel('站点 A 月平均气温 (摄氏度)') plt.ylabel('站点 B 月平均气温 (摄氏度)') plt.savefig('/tmp/met_insight/cross_check.png', dpi=150)

散点图落在红线附近说明两站一致,偏离红线说明有系统偏差。参数上alpha=0.6让重叠点可见,r--画红色虚线参考线。这个检查能抓住大部分数据质量问题,比单站看统计量更可靠。

5.4 一个我常犯的错误:忽略站点海拔对气温的影响

最后说个血泪经验。有次做区域平均气温,两个站直线距离不到 20 公里,但一个在山顶一个在山谷,海拔差 800 米,月平均气温差 5 度。我没做海拔订正就直接平均,结果整个区域的气温被山顶站拉低。后来学乖了,做区域统计前先看站点海拔,海拔差超过 200 米的站点要么分开分析,要么做气温直减率订正,一般每升高 100 米气温降 0.6 度左右。这个教训让我养成了一个习惯:拿到站点数据先画一张海拔分布图,心里有数再动手算。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进