
简介这份使用Python进行气象数据分析与可视化的完整可运行项目源码面向高校数据科学、气象课程期末大作业也适合想通过实际案例入门数据分析的Python学习者。项目完整覆盖数据采集、清洗、特征处理、模型训练与天气可视化展示代码按GetData、ProcessData、GetModel、main等模块组织配有文档说明训练集、测试集、验证集及中国天气数据等CSV文件划分清晰HTML页面和JPG图表可直接查看预测与可视化结果方便对照理解完整分析流程并参考其数据划分与模型保存方式。压缩包共22个文件包含4个py脚本、4个csv数据集、12张jpg结果图、1个html页面及1个pkl模型文件整体仅1.42MB轻量易部署。项目评审分在95分以上经过严格调试可正常运行已有509人学习下载适合需要快速掌握气象分析全流程、借鉴期末大作业设计思路并复用源码的读者。1. 气象数据分析与可视化这份高分项目包先认清再动手一份标题为“使用Python进行气象数据分析与可视化源码文档说明高分项目”打开的瞬间最容易踩的坑是直奔源码去跑train.py结果缺依赖、报编码错误折腾半小时还没看到一张图。实际上这套项目包的核心不是某个模型而是一条完整的气象数据分析链路把气象站的温度、降水、风速等原始观测数据读进来用pandas完成清洗和归并再用matplotlib、seaborn等库把趋势和相关性画出来最后配合文档讲清楚结论。它适合三类人拿它当课程设计参考的学生刚接触气象数据、想熟悉处理流程的开发者以及需要快速搭建内部气象报表的从业者。下面按我实际做这类项目的顺序把每个环节怎么落地、参数怎么调、坑在哪里拆开讲。2. 从CSV到DataFrame气象数据清洗与站点归并的五个关键步骤气象数据分析与可视化项目里最花时间的不是画图而是把原始观测文件整理成一份干净、按时间排序的表格。高分项目之所以能拿高分往往不是因为图表画得花哨而是清洗逻辑经得起复查。常见做法是先用pandas读入然后做五步处理识别字段类型、时间解析与时区归一化、阈值过滤异常值、多站点合并去重、重采样到月均值。每步做错的代价会在后续分析阶段成倍放大所以不建议直接跳去画图。2.1 先看字段类型和分隔符再决定怎么读文件拿到数据不要急着跑代码。先打开数据文件前5行确认是纯文本、CSV还是Excel导出文件再看表头单位。气象数据的CSV经常混用逗号和分号某些站点甚至用小数点逗号表示小数这一步错了后面全是乱码。我一般会写一个小型的探查脚本先确认分隔符和编码避免read_csv读出一堆NaN。编码方面常见的有UTF-8和GBK读文件时给出errorsreplace能在编码出错时保留有用部分而不是直接中断。from pathlib import Path import pandas as pd fpath Path(data/weather_station_raw.csv) with fpath.open(encodingutf-8, errorsreplace) as f: for i, line in enumerate(f): if i 5: break print(repr(line))这段代码用Python原生方式打印前5行解决“表头里有隐藏字符却不知道”的问题。repr会把换行符、制表符都显示出来如果看到文件里实际是分号分隔而代码里写的是逗号pd.read_csv会静默地把所有字段读成一列等到做日期解析时才报错。探查完之后再按实际情况读入df_raw pd.read_csv( fpath, sep,, # 如果探查到分号改成 ; enginepython, # 文件不规则时用 python 引擎 na_values[, NaN, 9999, 9999.0], # 缺测标记 low_memoryFalse, dtype{station_id: str}, # 站点ID固定按字符串读 )参数里面na_values一定要按源数据文档填很多气象站会用9999、-9999表示缺测不把这两个值标记成NaN的话后面求均值时会得到很离谱的数值。engine参数只在分隔符是多个字符或者文件不规则时再切到python默认的c引擎更快但遇到转义容易报错。dtype把station_id声明为str是最关键的一步否则01001这种编号会被读成1001后面做站点归并时直接错位。2.2 时间解析与时区归一化把“8点”变成标准时刻解析时间和时区归一是气象数据分析与可视化中最容易翻车的第一步。站点记录的时间有两种情况一种是不带时区的本地时间另一种是UTC标准时间项目文档里没写清楚时必须靠原始数据说明确认。统一做法是全部转成UTC时间再存一列本地时间这样后面做日平均、月平均的时候不会被夏令时弄乱。如果数据里只有日期没有小时那就只解析日期不要擅自补00:00。df_raw[datetime] pd.to_datetime( df_raw[date] df_raw[hour].astype(str), format%Y-%m-%d %H, errorscoerce, ) # 先把无时区时间标注为站点当地时区再转 UTC df_raw[datetime_utc] ( df_raw[datetime] .dt.tz_localize(Asia/Shanghai) .dt.tz_convert(UTC) ) df_raw[year] df_raw[datetime].dt.year df_raw[month] df_raw[datetime].dt.month df_raw[day] df_raw[datetime].dt.day这里有三处要特别说明。第一errorscoerce会把解析失败的时间变成NaT如果NaT比例超过5%要回头查日期格式而不是继续往下跑。第二tz_localize和tz_convert不能搞反前者是给无时区时间贴标签后者才是换算顺序错了会得到偏移8小时的数据。第三时间列名不要用中文后面和绘图库打交道时字段名越简单越好。还有一个细节是hour列可能有“24”这个值某些站点用24:00表示第二天0点pd.to_datetime不认识需要先替换成0并给日期加一天否则整行记录会解析失败。2.3 缺测值和异常值的阈值识别先讲物理再讲统计气象数据的异常值往往不是设备坏了而是传感器在特定天气下进入保护状态比如温度传感器的量程是-40到60摄氏度一旦超过这个范围就会输出边界值。处理思路是两层缺测值直接置NaN异常值按物理常识判断后也置NaN而不是删除整行。删除整行会连带丢掉同一时刻其他变量的有效观测在后续多变量分析里造成不必要的样本损失。import numpy as np df df_raw.copy() # 温度范围检查不超过物理极限就保留 df.loc[(df[temp] 60) | (df[temp] -60), temp] np.nan # 降水范围检查降水量不可能为负数 df.loc[df[precip] 0, precip] np.nan # 风速范围检查常规站点超过 80 m/s 基本是故障 df.loc[df[wind_speed] 80, wind_speed] np.nan # 分月统计缺失率而不是只看整体 miss df.groupby(df[month])[[temp, precip, wind_speed]].apply( lambda x: x.isna().mean() ) print(miss.round(3))这里走的是物理范围过滤不是3倍标准差过滤。做气象数据分析与可视化时如果直接按统计方法去异常会把极端台风过程当成异常值删掉导致后续趋势分析偏保守。物理阈值需要从数据说明书里抄说明书没写就按传感器型号查量程。分月统计缺失率比看整体缺失率有用得多因为缺测往往集中在冬季整体缺失率5%看起来不高但1月可能到了20%这部分数据参与回归会扭曲趋势。2.4 站点归并多站数据不要直接纵向拼接高分项目里经常出现多个站点文件很多初学者直接pd.concat一把梭结果站点编号列变成object类型或者同名站点被算了两遍。常见的归并做法是先统一列名再以站点编号和时间共同组成唯一键。dfs [] for f in station_files: _df pd.read_csv(f, dtype{station_id: str}) _df _df.rename(columns{ Stn: station_id, Date: date_str, Time: hour, }) dfs.append(_df) df_all pd.concat(dfs, ignore_indexTrue) df_all df_all.drop_duplicates(subset[station_id, date_str, hour])drop_duplicates这一步很重要因为同一文件在不同存储路径下可能被重复读入。subset参数指定的是“一个站点、一个时间点只能有一条记录”的规则而不是把所有列完全相同的行去重否则两个站点在温度上恰好相同就会被误删。归并之后还要做一次交叉验证用groupby检查每个站点的记录数量如果某个站点缺失月份成片就要回到源文件确认是否漏了文件。另一个常见验证是检查站点的经纬度是否在合理范围内经纬度字段常常被Excel自动转成了科学计数法读取时要当字符串处理否则后续画站点分布图时会出现坐标漂移。2.5 重采样到月均值消除日期权重不均的影响站点观测频率可能是小时级直接对小时数据做长期趋势分析会引入日内波动和不等权重。常见做法是把数据重采样成月均值同时保留每月样本量这样后续绘图和统计检验都更稳定。这里我用datetime_utc作为索引df_monthly ( df_all.set_index(datetime_utc) .resample(ME) .agg({ temp: mean, precip: sum, wind_speed: mean, station_id: count, }) )resample(ME)表示月末频率旧版本写法是M新版如果传M会报警告。聚合字典里temp和wind_speed用均值precip用月累计值station_id用count来统计这个月有多少条观测。月均值序列做趋势分析时样本量过少的月份要剔掉一般阈值是一个月少于20天有效观测就不参与趋势计算。这一层处理到位后面的统计检验才不会因为某个月只有三天数据而算出虚假的突变。3. 用Python做气象趋势分析的三个核心统计量数据洗干净之后才进入气象数据分析与可视化这个标题里更核心的“分析”部分。趋势分析不要只画一条折线然后目测要有统计量支撑。常见的做法是先算滑动平均看波动再用Sen斜率估计趋势强度最后用Mann-Kendall检验判断趋势是否显著。这三个统计量组合是气象论文和课程设计里最常出现的配套。3.1 滑动平均与气候态基线先去掉季节循环温度数据最明显的特征是冬天低夏天高直接对原序列回归时季节波动会淹没长期趋势。去掉季节循环的常用做法是用“距平”序列也就是每天的值减去同一天多年的平均值。另一条路是直接做30天滑动平均平滑掉日尺度噪音。两种做法可以同时用先做滑动平均做展示再做距平做统计检验。df_daily df_all.groupby(date_str)[temp].mean().reset_index() df_daily df_daily.sort_values(date_str) # 30天滑动平均min_periods 控制两端缺失 df_daily[temp_avg30] df_daily[temp].rolling( window30, centerTrue, min_periods15, ).mean() # 以365天窗口做气候态基线计算距平 df_daily[anomaly] ( df_daily[temp] - df_daily[temp].rolling( window365, centerTrue, min_periods180, ).mean() )注意这里rolling窗口里min_periods不要省。缺测较多的站点如果min_periods设得和window一样大整段结果都会有大量NaN后面画图时曲线会断成很多截。365天窗口的距平计算会让每个点都减去“过去一年滑动平均状态”季节循环基本被削掉剩下的就是偏离常年状态的波动。centerTrue让滑动平均值落在窗口中心画图时不会看到曲线整体向右偏移。如果项目数据跨度不足两年365天窗口会缺掉开头和结尾各半年这种情况下改用“逐月气候态”更合适先按月算多年平均再用原始值减月平均。3.2 Sen斜率估计抗异常值的趋势强度线性回归的斜率对异常值非常敏感气象数据里一次传感器故障或极端事件就能把拟合线拉偏。Sen斜率估计取所有数据对之间斜率的中位数比最小二乘稳定得多。scipy内置了现成实现直接用就行。from scipy import stats y df_daily[temp_avg30].dropna().values x np.arange(len(y)) sen_result stats.mstats.theilslopes(y, xx, alpha0.95) sen_slope, intercept, lo_slope, hi_slope sen_result print(fSen斜率: {sen_slope:.4f} °C/天) print(f95% 置信区间: [{lo_slope:.4f}, {hi_slope:.4f}])参数说明里有几个容易忽略的点。第一x必须填等间隔的数值索引如果把日期的字符串传进去排序就会出错。第二alpha0.95表示置信区间覆盖率为95%区间如果包含0说明趋势在统计上不显著。第三theilslopes返回顺序是斜率、截距、区间下限、区间上限写代码时不要只取第一个值然后丢掉区间否则后面写文档时没有依据。Sen斜率的单位需要换算。如果按天算得到的是“摄氏度每天”在报告里通常换算成“摄氏度每十年”只需要乘3650。换算后的数通常很小比如0.18摄氏度每十年这才是气象报告里常见的表达方式。3.3 Mann-Kendall检验趋势是否显著的正式说法Sen斜率只说增长或下降的强度不回答“这个趋势是不是随机波动造成的”。Mann-Kendall检验是气象和气候研究里最常见的非参数检验它只关心序列是单调上升还是下降不要求数据正态分布。用pymannkendall这个库可以一行算出多个指标也可以自己实现核验。import pymannkendall as mk daily_clean df_daily[temp_avg30].dropna() result mk.original_test(daily_clean, alpha0.05) print(trend:, result.trend) print(p-value:, result.p) print(tau:, result.Tau) print(slope:, result.slope)result的几个属性要会用。trend表示趋势方向是increasing、decreasing或no trend三种p小于设定的alpha时才能说趋势在统计上显著Tau是秩相关系数值接近1说明单调性很强特别注意result.slope是Sen斜率和前面用scipy算出来的应该是一致的如果两个结果不一致先检查输入序列是否缺测过多。这个库的输入只接受一维序列索引乱序没关系它会自己处理顺序。如果原始数据是月均值且季节性强不要直接用original_test换成mk.seasonal_test()它把数据按月份分组后检验避免春夏秋冬的规则波动被误判成趋势。3.4 趋势结果的三方交叉验证别只信一个函数计算完三个统计量之后还要做一次信息核对。常见的做法是做一个表把原始序列日距平、月距平、季节平均三套数据分别跑一遍MK检验如果三套结果方向不一致说明异常值或缺测填充方式在影响结论。特别是夏季平均和冬季平均很多时候冬季升温显著而夏季不显著这在气象上是有意义的现象直接合并成年平均会丢掉这个信息。我在做这类项目时会额外输出每个季节的Sen斜率表格而不是只在文档里写一个总体斜率。这样既方便检查也让项目的分析深度明显提升。4. 选择正确的气象图三种高频图表与参数校准分析统计量算完接下来是把结果变成可视化。标题里的“可视化”三个字很多时候决定读者第一印象。这里不是一张图走天下而是按数据特征选图温度用折线加距平柱状风场用风玫瑰多站相关性用热力图。每张图都要调坐标轴和配色否则在报告里打印出来会糊成一团。4.1 温度趋势折线图坐标轴、配色与刻度间距温度图最容易被扣分的点是Y轴起点不标数据范围或者是两条曲线叠在一起但颜色区分度太低。定好画布大小后先把Y轴范围限制在数据上下界附近再画原始距平与滑动平均。import matplotlib.pyplot as plt import matplotlib.dates as mdates fig, ax plt.subplots(figsize(12, 5)) ax.plot(df_daily[date_str], df_daily[anomaly], color#cccccc, linewidth0.8, label日距平) ax.plot(df_daily[date_str], df_daily[temp_avg30], color#c0392b, linewidth2, label30天滑动平均) ax.axhline(0, colorblack, linewidth0.6) ax.xaxis.set_major_locator(mdates.YearLocator(2)) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y)) ax.set_ylabel(温度距平 (°C)) ax.legend(frameonFalse) fig.tight_layout() fig.savefig(output/temp_anomaly.png, dpi200)legend里frameonFalse去掉边框让图像更干净。YearLocator(2)表示每两年一个主刻度如果数据跨度只有五年要改成YearLocator(1)否则横坐标会显得很空。dpi设成200已经满足大多数课程设计和报告打印需求不需要更高。颜色选择上灰色表示原始数据深红表示平滑曲线即使打印成黑白文档灰度差异也能让两条线区分。ax.axhline画在两条线之后确保0基准线不会被线覆盖。4.2 风玫瑰与降水的联合展示风速风向数据的可视化常用风玫瑰图matplotlib没有直接内置常见做法是用windrose库或者自己用极坐标实现。课程设计里用库能省很多时间但库更新慢旧环境可能装不上所以这里讲自制方式。import numpy as np df_wind df_all[[wind_dir, wind_speed]].dropna() df_wind[dir_bin] pd.cut( df_wind[wind_dir], binsnp.arange(0, 361, 45), labels[N, NE, E, SE, S, SW, W, NW], ) ax plt.subplot(projectionpolar) counts df_wind.groupby(dir_bin, observedTrue).size() angles np.linspace(0, 2 * np.pi, len(counts), endpointFalse) bars ax.bar(angles, counts, width2 * np.pi / len(counts)) ax.set_theta_zero_location(N) ax.set_theta_direction(-1)这段代码的关键在风向的语义。很多初学者直接把数据里的风向当数学里的角度画结果北风跑到了右边。气象上的风向是指风的来向北风角度是0度东风是90度所以要set_theta_zero_location(N)让0度在正上方set_theta_direction(-1)让角度顺时针增加。pd.cut的bins设计成45度一组labels要按顺序写否则风向标签会错位。如果还想叠加风速信息可以在每个方向上按风速分成三档分别着色但那样画法会更容易让图例拥挤我通常只在课程设计里保留单色版本。4.3 多站点相关矩阵热力图颜色映射的边界如果项目包含多个站点的数据画一个站点间温度相关矩阵是加分项。这里最容易犯的错是直接对包含NaN的DataFrame求corr矩阵里出现大量空白。处理方式是先用dropna对齐时间索引再计算相关性。import seaborn as sns pivot_df df_all.pivot_table( indexdate_str, columnsstation_id, valuestemp, aggfuncmean, ) clean_pivot pivot_df.dropna(threshlen(pivot_df) * 0.8) corr_matrix clean_pivot.corr() fig, ax plt.subplots(figsize(8, 7)) sns.heatmap( corr_matrix, annotTrue, fmt.2f, cmapRdBu_r, vmin-1, vmax1, cbar_kws{label: 相关系数}, )先pivot_table把长表变成宽表dropna时thresh参数控制只保留有效观测超过80%的行列相关矩阵不会因为个别站点缺测而全是NaN。cmap选RdBu_r红色代表正相关蓝色代表负相关vmin和vmax固定为-1和1避免相关性接近1时颜色饱和过度。如果打印成黑白annotTrue的数字还能保留信息所以不要去掉这个参数。cbar_kws里加一个label说明色标含义这是评审时经常被问到的细节。4.4 组合图布局一张报告页放下关键结论课程设计通常需要把多张图组织到一起常见做法是用GridSpec划分画布上排放温度趋势和降水累计下排放风玫瑰和相关矩阵。布局时把风玫瑰放在极坐标子图里需要注意subplot的投影参数只能在创建时指定不能在已经创建的子图上临时改。多图组合的另一个问题是字体大小报告打印时小字号在A4纸上会看不清坐标轴刻度字号建议至少10号。我不会在一张图里塞超过四个子图子图越多读者越难抓住重点。5. 常见问题与注意点气象项目里5个容易丢分的坑气象数据分析与可视化这个方向里踩坑记录比技术本身更值钱。下面这五个问题是我在类似项目里反复遇到的每条按现象、原因、解决来写可以直接对照检查。5.1 日平均气温偏移半个季节现象算出来的日平均气温序列比真实气候态偏高冬季尤其明显。原因站点原始数据用的是当地时区但代码里没有做时区标注直接当UTC时间用把当地时间当成了UTC导致凌晨0点的数据被归到前一天日平均值的采样时段发生偏移。解决解析出时间后先确认站点时区。如果数据文件里注明Local Standard Time就用tz_localize加上时区名再转成UTC存备份列。最稳妥的做法是做一次抽样校验手动取某一天把当天24小时均值与相邻两天的均值做比较如果出现不连续跳变说明时区处理反了。5.2 某一列缺测率超过20%但仍然进了回归现象趋势分析结果显示显著升温但换一种缺测填充方式结果就反过来。原因缺测集中在特定季节比如冬季传感器结冰后长时间不恢复。直接用插值补全后冬季数据全是插值拟合出来的假值回归时这部分假值会放大趋势。解决填充前先统计每个月的缺测率按月输出缺失日历。如果某个季节缺测率超过20%后续分析只使用完整月份或者把该站点标记为不可作趋势分析。不要用全局中位数填充它会抹掉季节信号。要填充也只按同月份中位数填充并打上标记列。5.3 跨年边界处滑动平均曲线断开现象1月1日附近的滑动平均曲线突然消失形成一个V形缺口。原因rolling窗口默认要求窗口内所有值都非NaN跨年时数据可能因为节假日文件缺失而不足。min_periods参数没有设置或者设置成与window相同。解决滚动窗口设置min_periods为窗口的60%比如30天窗口设min_periods18。这会保留边界附近的有效平滑值同时避免只有三五个点也强行平滑。要说明的是滑动平均的缺口用插值补在图上看起来像原始数据会误导读者所以我不建议对平滑曲线做插值保留缺口反而更诚实。5.4 站点ID被当成数字读入后丢失前导零现象图例里站点编号从01001变成1001或者合并后同一站点出现了两种类型。原因CSV里的站点ID列被pandas自动推断为int64类型前导零被丢掉。气象站点ID一般是字符串特别是在把多个文件合并时不同类型ID还会引发类型错误。解决读取时直接指定dtype参数。df pd.read_csv(fpath, dtype{station_id: str, wind_dir: float})5.5 可视化中文乱码现象图表的标题和坐标轴中文全部变成方框。原因matplotlib默认字体不包含中文字形而且不同操作系统可用的字体路径不一样同一份代码在Windows和Linux下效果不同。解决在绘图脚本开始时设置字体。import matplotlib matplotlib.rcParams[font.sans-serif] [ SimHei, Noto Sans CJK SC, Arial Unicode MS ] matplotlib.rcParams[axes.unicode_minus] Falsefont.sans-serif里列了多个字体名是因为不同系统的字体名称不同matplotlib会按顺序找第一个存在的字体。axes.unicode_minus设为False解决的是负号变成方块的问题负号的Unicode和普通连字符在不同字体里表现不一样。设置完后再用plt.rcParams[font.family] sans-serif兜底。检查是否生效时不用重新跑全部代码画一个最简单的plt.title(测试)就能看出来。6. 把气象分析包装成可复用模板三种进阶做法当课程设计或者一次业务分析已经跑通下一步是把整套流程变成下次能直接复用的模板。我自己的习惯是:硬编码路径滚出战配置文件管变量封装函数但不过度封装。下面三个做法按从轻到重的顺序介绍。6.1 用yaml配置文件替换硬编码路径把站点列表、时间范围、阈值参数从代码里提出来放到一个config.yaml里。下次换数据源时不用动代码只改配置。data: station_files: - data/station_a.csv - data/station_b.csv encoding: utf-8 sep: , na_values: [9999, -9999] temp_range: [-60, 60] wind_max: 80.0 output: dir: output dpi: 200 language: zh在代码里用PyYAML读取后所有阈值都从config引用。这个做法能把逻辑和参数分离回头检查时不需要从代码里翻阈值。注意language设置也会影响字体如果语言设定成zh脚本里就自动启用中文字体否则使用默认字体。6.2 命令行入口与输出目录规范化给脚本加一个argparse入口让模板可以通过命令行传参控制输入文件与输出目录。我一般会加两个参数--config指定配置文件--output覆盖输出目录方便批量跑多个站点时按日期建子目录。import argparse def parse_args(): parser argparse.ArgumentParser(description气象数据分析与可视化模板) parser.add_argument(--config, typestr, defaultconfig.yaml) parser.add_argument(--output, typestr, defaultoutput) return parser.parse_args() if __name__ __main__: args parse_args() run(args.config, args.output)这里不要直接读sys.argvargparse会自动处理帮助信息和错误提示。output参数允许覆盖配置文件里的默认目录比如output/20240528/这样不同批次的图不会互相覆盖。6.3 生成带内嵌图片的HTML报告如果希望结果能分享给不懂代码的人把图片和表格嵌入一个HTML报告是更直观的交付方式。用pandas的to_html生成表格片段再拼接到模板里图片用base64内嵌这样整个报告只有一个文件发邮件不会丢图。import base64 from pathlib import Path def image_to_base64(path: Path) - str: return base64.b64encode(path.read_bytes()).decode(utf-8) img_b64 image_to_base64(Path(output/temp_anomaly.png)) html_content f html body h1气象趋势分析报告/h1 img srcdata:image/png;base64,{img_b64} / /body /html Path(output/report.html).write_text(html_content, encodingutf-8)base64内嵌后图片体积会膨胀三分之一左右所以报告里只放关键的三张图趋势图、风玫瑰和相关矩阵其他图放到output目录里供人自行查看。做了几轮这样的模板之后我最大的教训是把时间留给数据质量检查而不是画图调颜色。图表再好看数据里的时区错误和缺测处理不对最后都会被一句话问穿。先跑通最小案例再逐步加功能希望帮到你。本文还有配套的精品资源点击获取