简介:这份资源面向数据分析初学者与需要完成课程实验的学生,围绕气象数据展开完整实践:通过爬虫代码抓取中国天气网某一城市的天气数据,再对数据做清洗与统计分析,并生成雷达图、条形图等多种可视化图表,配合文字说明形成一份可复用的实验范例。压缩包共39个文件,以20个png图表截图、15个xml与4个rels文档结构文件为主,整体约1.69MB,其中png直观呈现各类分析结果,xml与rels则对应整理后的实验报告文档结构,便于直接查阅与二次编辑。资源已有1854人学习下载,包含全部源文件、源代码与整理过后的实验报告,代码可在jupyter notebook中直接运行,读者既能复现爬取与分析流程,也能借鉴图表选型与结论撰写思路,适合作为数据分析入门练习或课程作业参考。
1. 气象分析不是画几张曲线:从一份逐时观测表说起
手头拿到一份气象逐时观测表,字段无非是时间、气温、气压、湿度、风速风向、降水量。很多人第一反应是画几条折线,看看趋势,写两句“气温先升后降”。但真正做过气象数据分析的人都知道,麻烦从来不在画图,而在数据本身:缺测、跳变、单位不统一、时间戳错位、风向用度数还是十六方位、降水是累计还是瞬时。这些问题不处理,后面所有结论都是空中楼阁。这一篇讲的就是把一份原始气象观测数据,从清洗、质控、特征构造一路做到可解释的分析结论,适合刚接手气象数据、又不想只停留在画图层面的从业者。核心词气象分析会贯穿始终,数据分析的落地路径也会一步步拆开。
2. 气象数据先过质控这一关:字段、单位与时间轴怎么对齐
气象分析的第一步不是建模,而是让数据“可信”。一份逐时观测表看起来规整,实际打开经常是另一回事:气温列里混着字符串“—”、气压单位有的是 hPa 有的是 Pa、时间列有的是本地时有的带时区偏移、风向用 0–360 度但 0 和 360 都表示北。这些不统一,后面算露点、算体感温度、算风玫瑰全部会翻车。
我一般会先做三件事:统一时间轴到 UTC 或明确的本地时区并排序去重;统一单位到一套标准(气温摄氏度、气压 hPa、风速 m/s、降水 mm);把缺测标记成 NaN 而不是 0 或空字符串。顺序不能乱,先时间后单位再缺测,否则去重和插值都会出错。
2.1 用 pandas 把逐时观测表读成可分析的结构
下面这段是最小可复现的读取与规范化骨架,字段名按常见观测表命名,实际替换成你自己的列名即可。
import pandas as pd import numpy as np # 读取原始观测表,缺失值统一识别 df = pd.read_csv( "obs_hourly.csv", na_values=["", "—", "NA", "999999", "9999"], # 常见缺测占位符 parse_dates=["datetime"], # 时间列直接解析 ) # 1) 时间轴:排序 + 去重 + 设为索引 df = df.sort_values("datetime").drop_duplicates(subset="datetime") df = df.set_index("datetime") # 2) 单位统一:气压若为 Pa 则换算成 hPa if df["pressure"].median() > 2000: # 经验阈值,Pa 量级远大于 hPa df["pressure"] = df["pressure"] / 100.0 # 3) 数值列强制转 float,非法值变 NaN num_cols = ["temp", "pressure", "rh", "wind_speed", "precip"] df[num_cols] = df[num_cols].apply(pd.to_numeric, errors="coerce") print(df[num_cols].isna().mean().round(3)) # 先看各列缺测比例这段代码的逻辑说明:na_values把观测系统常见的缺测占位符一次性识别掉,避免它们被当成真实数值参与统计;parse_dates让时间列变成真正的 datetime 类型,后续重采样才有意义;气压换算用中位数判断量级,比逐行判断稳,因为个别异常值不会带偏整体判断;errors="coerce"把无法解析的字符串转成 NaN,保证列是数值类型。参数上,na_values要按你数据源的实际占位符补全,pressure的 2000 阈值只是经验值,拿到数据先打印describe()看一眼量级再定。
2.2 缺测、跳变与物理界限:三条质控规则
单位对齐之后,质控才是重头戏。气象数据有三类典型问题:缺测(NaN)、跳变(相邻小时气温差 20 度)、越界(湿度 120%、风速为负)。我一般用物理界限加变化率双重规则,规则要写死成可复用的函数,而不是每次手改。
def quality_control(df): rules = { "temp": (-60, 60), # 摄氏度物理界限 "pressure": (800, 1100), # hPa 海平面附近合理范围 "rh": (0, 100), # 相对湿度 "wind_speed":(0, 75), # m/s,超过多为仪器异常 "precip": (0, 200), # 逐时降水 mm } for col, (lo, hi) in rules.items(): df.loc[(df[col] < lo) | (df[col] > hi), col] = np.nan # 变化率规则:逐时气温跳变超过 15 度视为可疑 jump = df["temp"].diff().abs() > 15 df.loc[jump, "temp"] = np.nan return df df = quality_control(df)逻辑说明:先按物理界限把越界值置 NaN,再用diff()抓相邻时刻的异常跳变。这里把跳变值也置 NaN 而不是直接删行,是为了保留时间连续性,后面插值或标记都能用。参数上,气温 15 度的跳变阈值偏宽松,如果你做的是分钟级数据要相应调小;降水上限 200 mm/h 是极端情况下的宽松上限,常规站点可以收紧到 50。质控之后一定要统计每列被置 NaN 的比例,超过 20% 的列要回头查数据源,而不是硬插值。
提示:质控规则不要一次写太严,先把被标记的点导出来和原始记录对照,确认规则没有误杀真实极端天气,再固化下来。
3. 从原始观测到分析特征:派生量、重采样与风玫瑰
质控过关后,原始字段往往不够用。气象分析里真正有解释力的,很多是派生量:露点温度、体感温度、风向的十六方位、日较差、滑动平均。这一步的目标是把“观测值”变成“分析特征”,让后面的趋势判断和异常识别有抓手。
3.1 派生量计算:露点、体感温度与风向离散化
露点用 Magnus 公式近似即可,体感温度用简化热指数,风向从度数转十六方位。这些公式不复杂,但参数和单位要对齐,否则算出来的露点比气温还高就闹笑话了。
import numpy as np def dew_point(temp, rh): # Magnus 近似,temp 摄氏度,rh 百分比 a, b = 17.27, 237.7 gamma = (a * temp) / (b + temp) + np.log(np.clip(rh, 1, 100) / 100.0) return (b * gamma) / (a - gamma) def wind_direction_16(deg): dirs = ["N","NNE","NE","ENE","E","ESE","SE","SSE", "S","SSW","SW","WSW","W","WNW","NW","NNW"] idx = int((deg % 360) / 22.5 + 0.5) % 16 return dirs[idx] df["dew_point"] = dew_point(df["temp"], df["rh"]) df["wind_dir_16"] = df["wind_dir"].apply( lambda d: wind_direction_16(d) if pd.notna(d) else np.nan )逻辑说明:dew_point里对rh做了clip(1,100),防止湿度为 0 时取对数出错;wind_direction_16用(deg % 360) / 22.5 + 0.5取整再模 16,保证 348.75–360 度落到 N 而不是越界。参数上,Magnus 公式的 a、b 是常用近似常数,精度对分析足够;如果你要做精细的农业气象,可以换更精确的 Goff-Gratch 公式,但计算量会上去。
3.2 重采样与滑动统计:日值、日较差和 24 小时滑动平均
逐时数据直接看趋势噪声很大,重采样到日值、再算日较差和滑动平均,趋势才看得清。重采样时聚合函数要按字段选:气温用 mean,降水用 sum,风向不能平均(要用矢量平均或众数)。
# 日值聚合:气温均值、降水累计、气压均值 daily = df.resample("1D").agg({ "temp": "mean", "precip": "sum", "pressure": "mean", "wind_speed": "mean", }) # 日较差 daily["temp_range"] = df["temp"].resample("1D").max() - df["temp"].resample("1D").min() # 24 小时滑动平均,用于平滑短期波动 df["temp_ma24"] = df["temp"].rolling(window=24, min_periods=18).mean()逻辑说明:resample("1D")按自然日聚合,agg字典让不同字段走不同聚合,避免降水被平均成小数;temp_range单独用 max 减 min,因为agg里不方便直接写差值;rolling的min_periods=18允许前几小时缺测时仍能出值,但不会在数据太少时硬算。参数上,滑动窗口 24 对应逐时数据的日周期,如果你做的是分钟级数据,窗口要改成 1440;min_periods一般设成窗口的 70%–80%,太低会让滑动平均失真。
3.3 风玫瑰与主导风向:别用平均风向骗自己
风向是角度,直接求平均会得到完全错误的结果——北风 350 度和北风 10 度平均出来是 180 度,南风。正确做法是转成矢量(u、v 分量)再平均,或者直接统计十六方位频次画风玫瑰。
# 转 u/v 分量 rad = np.deg2rad(df["wind_dir"]) df["u"] = -df["wind_speed"] * np.sin(rad) df["v"] = -df["wind_speed"] * np.cos(rad) # 矢量平均得到主导风向 u_mean, v_mean = df["u"].mean(), df["v"].mean() dominant_dir = (np.rad2deg(np.arctan2(-u_mean, -v_mean)) + 360) % 360 # 十六方位频次,用于风玫瑰 freq = df["wind_dir_16"].value_counts(normalize=True).round(3)逻辑说明:u、v 分量用-sin、-cos是气象约定(u 为东风分量,v 为北风分量),符号错了主导风向会反 180 度;arctan2再转回角度并归一化到 0–360。参数上,矢量平均适合看整体主导方向,但会掩盖双峰分布,所以频次统计要一起看。如果频次显示两个方向各占 30%,说明该站点存在明显的日夜风向转换,这时候平均风向没有意义,要分时段统计。
4. 气象分析的避坑清单:五个让结论翻车的细节
这一章是我踩过的坑里挑出来最典型的五条,每条按现象、原因、解决写,照着排查能省不少返工。
4.1 缺测被当成 0:降水统计虚高的隐形推手
现象:月降水累计比邻近站点高出一大截,逐日看又找不到明显暴雨日。原因:原始表里缺测用 0 或空字符串表示,读取时没识别成 NaN,sum()把缺测当 0 累计,看似没影响,但缺测比例高时会把“有雨日”和“无雨日”混在一起,日均值被拉低、累计被稀释,和邻近站点对比就失真。解决:读取阶段就把所有缺测占位符列进na_values,质控后打印每列 NaN 比例,降水列缺测超过 5% 的月份直接标记为不完整,不参与累计对比。
4.2 时间戳时区错位:日值聚合整体偏移一小时
现象:日最高气温总是出现在上午 11 点而不是下午 2 点,日降水峰值时间和实况对不上。原因:原始时间列是本地时,但某段数据混入了 UTC 时间,或者解析时没指定时区,resample("1D")按错误的时间边界切日。解决:读取时统一tz_localize到明确时区,跨时区数据先tz_convert到同一时区再排序;聚合前打印首尾时间和相邻时间差,确认没有 23 小时或 25 小时的“日”。
4.3 风向直接平均:主导风向反了 180 度
现象:风玫瑰显示主导风向是南风,但现场记录和邻近站点都是北风。原因:把风向度数当普通数值求平均,350 度和 10 度平均成 180 度。解决:一律转 u、v 分量再平均,或者直接统计十六方位频次;检查 u、v 公式的符号约定,u = -speed*sin(dir)、v = -speed*cos(dir)是气象常用约定,符号反了方向就反。
4.4 插值过度:把缺测插成了“真实极端值”
现象:质控后某段连续缺测被线性插值填平,结果这段数据在趋势分析里表现为“异常平稳”,掩盖了真实过程。原因:连续缺测超过几个小时还用线性插值,等于凭空造数据。解决:插值前先看缺测连续长度,连续缺测超过 3 小时的段落标记为缺失段,不插值;短缺口用线性或前向填充,并在结果里保留一个is_filled标记列,分析时能区分真实观测和填充值。
4.5 物理界限一刀切:误杀真实极端天气
现象:某次强对流过程的气温跳变被质控规则置成 NaN,导致过程分析缺了关键时段。原因:变化率阈值设得太严,把真实的快速降温当成仪器异常。解决:质控规则分两级,一级是硬物理界限(如气温 -60–60 度)直接置 NaN,二级是变化率可疑值只做标记不置 NaN,导出标记点和原始记录人工核对后再决定是否剔除;极端天气频发的站点,变化率阈值要相应放宽。
5. 让气象分析结论站得住:交叉验证与可复现的检查习惯
分析做完,最怕的是“图很好看,结论经不起问”。我一般会用三个动作给结论上保险:和邻近站点做交叉对比、用不同时间粒度复算关键指标、把整条处理链固化成可重跑的脚本。交叉对比不是找“标准答案”,而是看同一时段两个站点的趋势是否同向、量级是否合理,偏差超过预期就去查数据源而不是硬解释。不同粒度复算,比如日降水和逐时降水累计对不上,多半是重采样边界或缺测处理的问题。脚本固化则是把读取、质控、派生、聚合写成函数串起来,每次换数据只改输入路径,避免手改中间步骤引入的不一致。
下面这个检查表是我每次出结论前会过一遍的,字段和阈值按项目调整:
| 检查项 | 具体动作 | 不通过时的信号 |
|---|---|---|
| 时间轴连续性 | 打印相邻时间差分布 | 出现 23/25 小时或重复时间戳 |
| 缺测比例 | 按列、按月统计 NaN 占比 | 关键列某月缺测超 20% |
| 单位量级 | 打印各数值列 describe | 气压中位数在 100000 量级 |
| 派生量合理性 | 露点不高于气温、湿度 0–100 | 露点高于气温 |
| 聚合一致性 | 日降水累计与逐时累计对比 | 两者偏差超 1% |
| 风向矢量 | 主导风向与频次最高方位对比 | 两者相差超过 45 度 |
一个具体技巧:把质控前后的数据各存一份,分析结论只基于质控后数据,但所有图表旁边标注被剔除和填充的点数。这样别人质疑某个峰值时,你能立刻说清它是真实观测还是插值产物。我吃过最大的亏就是早期没留这个标记,后来复盘一次异常高温,翻遍脚本也说不清那个值是实测还是插出来的,只能整段重跑。从那以后,is_filled和qc_flag两列成了我处理气象数据的固定习惯,宁可多存一列,也不给自己留说不清的账。希望帮到你。
本文还有配套的精品资源,点击获取