简介这份资源面向气象、环境科学与气候研究方向的Python学习者提供一套从数据读取到可视化呈现的完整实践素材帮助解决气象观测数据处理与图表展示的实际问题。压缩包共15个文件以10个CSV气象观测数据为主配合4个Python脚本和1份说明文档整体约14KB体积轻便便于快速上手运行与二次修改。内容围绕pandas、numpy、scipy等核心库展开涵盖NetCDF、GRIB等气象格式的读取思路以及缺失值处理、异常值检测、时间序列分析等预处理环节并借助matplotlib、seaborn、plotly等工具绘制温度、湿度、风速随时间和空间变化的图形。脚本按data1至data4分模块组织配套多个意大利城市的气象站数据可对照代码理解数据导入、清洗、分析与出图的完整链路。目前已有9030人学习适合希望系统掌握气象数据分析流程、积累可视化案例的读者参考。1. 气象数据分析与可视化从原始数据到可交付图表的完整链路很多人拿到一份气象观测数据第一反应是打开 Excel 画折线图结果发现几万行数据卡到崩溃时间轴挤成一团风向数据根本没法用折线表示。气象数据分析与可视化这件事核心难点从来不是画图本身而是数据清洗、时间序列对齐、缺失值处理和图表类型选择这四件事。Python 在这个场景里的价值是把从原始 CSV 到最终可交付图表的链路全部脚本化换一批数据只需要改路径和列名。这篇文章面向的是手头有气象观测数据、需要用 Python 做分析和出图的从业者不管你是气象站运维、农业环境监测还是能源行业的功率预测岗位这套流程都能直接复用。下面从环境搭建讲到进阶技巧每一步都有可复现的代码和参数说明。2. 环境搭建与数据读取把气象 CSV 正确加载进内存2.1 依赖选型与安装命令气象数据分析的依赖不多但版本兼容性有讲究。核心四件套是 pandas、numpy、matplotlib、scipy。pandas 负责表格操作和时间序列重采样numpy 处理数值计算和缺失值matplotlib 出图scipy 做插值和统计检验。如果要做交互式图表再加 plotly 或 pyecharts。安装命令如下pip install pandas numpy matplotlib scipy plotly如果你用的是 conda 环境建议单独建一个环境避免和系统 Python 冲突conda create -n weather python3.10 conda activate weather pip install pandas numpy matplotlib scipy plotly参数说明Python 版本建议 3.9 以上pandas 2.x 对时间序列的支持更完善。matplotlib 中文字体需要额外配置后面会讲。不要用 pip install 全局安装到系统 Python气象数据处理经常需要固定版本复现结果。2.2 读取气象 CSV 的三种典型格式气象数据常见的存储格式有三种标准 CSV逗号分隔、固定宽度文本气象站老设备导出、Excel 多 sheet。最常见的是 CSV但编码和表头行数经常出问题。import pandas as pd import numpy as np # 场景一标准 CSV第一行是列名 df pd.read_csv( weather_data.csv, encodingutf-8, parse_dates[datetime], # 指定时间列自动解析 na_values[, NaN, 999999, ////] # 气象数据常见缺失标记 ) # 场景二表头不在第一行前几行是站点信息 df pd.read_csv( station_data.csv, skiprows5, # 跳过前5行元数据 encodinggbk, # 国产气象设备常用GBK parse_dates[观测时间] ) # 场景三固定宽度文本 df pd.read_fwf( old_station.txt, widths[10, 8, 8, 8, 8], # 每列字符宽度 names[站号, 日期, 气温, 气压, 风速] )逻辑说明parse_dates让 pandas 自动把时间列转成 datetime 类型后续重采样和绘图都依赖这个类型。na_values是关键参数气象数据里缺失值经常用 999999、////、空字符串混合表示不统一处理后面全是坑。skiprows和encoding是国产气象设备导出的高频问题GBK 编码不指定会直接报 UnicodeDecodeError。参数说明parse_dates可以传列名或列索引列表多列时间字段用嵌套列表。na_values接受字符串或列表建议把你能见到的所有缺失标记都列进去。read_fwf的widths必须和原始文件严格对齐差一个字符整列错位。2.3 数据初探与类型修正读进来之后不要急着画图先做三件事看形状、看类型、看缺失。# 基础信息 print(df.shape) # 行数和列数 print(df.dtypes) # 每列数据类型 print(df.isnull().sum()) # 每列缺失值数量 # 类型修正气温、气压、风速应该是float df[气温] pd.to_numeric(df[气温], errorscoerce) df[风速] pd.to_numeric(df[风速], errorscoerce) # 时间列设为索引方便重采样 df df.set_index(datetime).sort_index() # 检查时间索引是否连续 full_range pd.date_range(startdf.index.min(), enddf.index.max(), freqh) missing_hours full_range.difference(df.index) print(f缺失时间点数量{len(missing_hours)})逻辑说明pd.to_numeric配合errorscoerce能把无法解析的值变成 NaN比直接 astype 安全。set_index加sort_index是时间序列操作的前提不排序后面重采样会报错。检查时间索引连续性是为了发现数据断档气象数据经常有整段缺失不检查的话画出来的图会骗你。参数说明freqh表示小时频率如果数据是分钟级就改freqmin日级改freqD。difference返回的是存在但数据里没有的时间点数量超过总长度 5% 就要考虑插值还是直接截断。3. 清洗与特征工程缺失值、异常值和气象派生量3.1 缺失值处理的四种策略与选择依据气象数据的缺失值处理没有万能方案取决于缺失比例和后续用途。我一般按这个决策链走缺失比例推荐策略适用场景 5%线性插值连续观测短时断档5% - 20%时间序列插值 标记需要保留缺失标记供后续分析20% - 50%同日同时刻均值填充有明显日周期的要素 50%删除该列或该时段数据质量不可用# 策略一线性插值适合短时断档 df[气温] df[气温].interpolate(methodlinear, limit3) # 策略二时间插值考虑时间间隔 df[气温] df[气温].interpolate(methodtime) # 策略三按同日同时刻均值填充 df[气温] df.groupby([df.index.hour, df.index.month])[气温].transform( lambda x: x.fillna(x.mean()) ) # 策略四标记缺失不填充 df[气温_缺失] df[气温].isnull().astype(int)逻辑说明interpolate(methodlinear)按索引顺序线性插值limit3限制最多连续插 3 个点超过就保留 NaN防止长段缺失被平滑掉。methodtime会考虑时间间隔不均匀的情况比 linear 更准。按小时和月份分组填充利用了气象要素的日变化和季节规律适合气温、湿度这类周期性强的变量。参数说明limit参数控制最大插值长度气象数据建议不超过 6。transform保证返回的索引和原数据一致不会因为分组打乱顺序。3.2 异常值检测3σ 还是 IQR气象数据异常值检测常用两种方法3σ 法和 IQR 法。3σ 假设数据正态分布IQR 不假设分布。气温、气压近似正态用 3σ风速、降水偏态分布用 IQR。def detect_outliers_3sigma(series, threshold3): 3σ法检测异常值返回布尔索引 mean series.mean() std series.std() return np.abs(series - mean) threshold * std def detect_outliers_iqr(series, factor1.5): IQR法检测异常值 q1 series.quantile(0.25) q3 series.quantile(0.75) iqr q3 - q1 lower q1 - factor * iqr upper q3 factor * iqr return (series lower) | (series upper) # 气温用3σ temp_outliers detect_outliers_3sigma(df[气温]) # 风速用IQR wind_outliers detect_outliers_iqr(df[风速]) # 异常值处理标记而非直接删除 df[气温_异常] temp_outliers.astype(int) df.loc[temp_outliers, 气温] np.nan # 先置NaN再插值 df[气温] df[气温].interpolate(methodtime)逻辑说明先标记再处理保留异常标记列供后续追溯。直接删除异常值会丢失信息置 NaN 再插值能保持时间序列连续。3σ 的 threshold 参数在气象数据里常用 3但极端天气事件本身就是小概率阈值设太小会把真实极端值当异常。参数说明factor1.5是 IQR 的标准参数气象数据可以放宽到 2.0 或 3.0避免把大风天气误判为异常。异常值处理完要检查数据分布是否合理气温突然从 35 度插值到 5 度肯定是哪里出了问题。3.3 气象派生量计算体感温度、露点、风矢量原始气象数据往往只有气温、湿度、风速、风向但分析时经常需要派生量。# 露点温度Magnus公式 def dew_point(temp, humidity): temp: 摄氏度, humidity: 相对湿度(0-100) a, b 17.27, 237.7 alpha (a * temp) / (b temp) np.log(humidity / 100.0) return (b * alpha) / (a - alpha) df[露点] dew_point(df[气温], df[相对湿度]) # 体感温度简化公式考虑风速 def feels_like(temp, wind_speed): temp: 摄氏度, wind_speed: m/s return 13.12 0.6215 * temp - 11.37 * wind_speed**0.16 0.3965 * temp * wind_speed**0.16 df[体感温度] feels_like(df[气温], df[风速]) # 风向转u/v分量风向为角度0度为正北 def wind_uv(wind_speed, wind_dir): wind_dir: 气象风向角度0北90东 rad np.deg2rad(wind_dir) u -wind_speed * np.sin(rad) # 东西分量 v -wind_speed * np.cos(rad) # 南北分量 return u, v df[u], df[v] wind_uv(df[风速], df[风向]) df[风向_弧度] np.deg2rad(df[风向])逻辑说明露点用 Magnus 公式参数 a17.27、b237.7 是气象学标准值。体感温度公式里风速要转成 m/s如果原始数据是 km/h 要先除以 3.6。风向转 u/v 分量是为了后续做风玫瑰图和矢量平均直接对角度求平均是错的0 度和 360 度平均出来是 180 度完全反了。参数说明np.deg2rad把角度转弧度numpy 三角函数只认弧度。u 分量前面的负号是因为气象风向定义是风的来向u 分量指向东为正所以来向为西时 u 为正公式里加负号修正。4. 可视化实战时间序列、风玫瑰和热力图的代码实现4.1 时间序列图解决横坐标太密集的问题气象数据动辄几千上万个时间点直接 plot 横坐标会挤成一团黑。核心解决思路是控制刻度密度和旋转标签。import matplotlib.pyplot as plt import matplotlib.dates as mdates # 中文字体配置Windows plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False # 负号正常显示 fig, ax plt.subplots(figsize(14, 5)) # 绘制气温曲线 ax.plot(df.index, df[气温], linewidth0.8, color#e74c3c, label气温) # 横坐标刻度控制自动选择合适密度 ax.xaxis.set_major_locator(mdates.AutoDateLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter(%m-%d %H:%M)) plt.xticks(rotation45, haright) # 或者手动指定每6小时一个刻度 # ax.xaxis.set_major_locator(mdates.HourLocator(interval6)) ax.set_xlabel(时间) ax.set_ylabel(气温 (℃)) ax.set_title(气温时间序列) ax.legend() ax.grid(alpha0.3) plt.tight_layout() plt.savefig(temperature_timeseries.png, dpi150) plt.show()逻辑说明AutoDateLocator让 matplotlib 自动决定刻度密度数据多的时候自动稀疏。DateFormatter控制显示格式%m-%d %H:%M是月-日 时:分。rotation45加haright让标签右对齐旋转避免重叠。tight_layout自动调整边距防止标签被裁掉。参数说明figsize宽度建议 12 以上气象时间序列横向信息多。dpi150是屏幕查看和文档插入的平衡值印刷用 300。如果数据点超过 10000建议先重采样再画否则渲染很慢。4.2 风玫瑰图用极坐标展示风向风速分布风玫瑰图是气象可视化的标志性图表用极坐标的扇形长度表示风向频率颜色表示风速大小。import numpy as np import matplotlib.pyplot as plt # 风向分箱16个方位 dir_bins np.arange(0, 361, 22.5) dir_labels [N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW] # 风速分箱 speed_bins [0, 2, 4, 6, 8, 10, 100] speed_labels [0-2, 2-4, 4-6, 6-8, 8-10, 10] colors [#2ecc71, #3498db, #f1c40f, #e67e22, #e74c3c, #8e44ad] # 统计每个风向每个风速段的频率 df[风向段] pd.cut(df[风向], binsdir_bins, labelsdir_labels, include_lowestTrue) df[风速段] pd.cut(df[风速], binsspeed_bins, labelsspeed_labels) # 交叉表 ct pd.crosstab(df[风向段], df[风速段]) ct ct.reindex(dir_labels).fillna(0) # 保证16个方位都有 # 转成频率 ct_pct ct.div(ct.sum(axis1), axis0) * 100 # 绘图 fig plt.figure(figsize(8, 8)) ax fig.add_subplot(111, projectionpolar) ax.set_theta_zero_location(N) # 0度在正北 ax.set_theta_direction(-1) # 顺时针 angles np.deg2rad(np.arange(0, 360, 22.5)) bottom np.zeros(len(angles)) for i, label in enumerate(speed_labels): values ct_pct[label].values ax.bar(angles, values, widthnp.deg2rad(22.5), bottombottom, colorcolors[i], labellabel, edgecolorwhite, linewidth0.5) bottom values ax.set_xticks(angles) ax.set_xticklabels(dir_labels) ax.legend(locupper right, bbox_to_anchor(1.3, 1.0), title风速 (m/s)) ax.set_title(风玫瑰图, pad20) plt.tight_layout() plt.savefig(wind_rose.png, dpi150) plt.show()逻辑说明pd.cut把连续的风向和风速切成离散区间。crosstab生成交叉表行是风向段列是风速段。reindex保证 16 个方位顺序正确缺的补 0。div(ct.sum(axis1), axis0)把计数转成每个风向内的频率百分比。极坐标里set_theta_zero_location(N)让 0 度朝北set_theta_direction(-1)让角度顺时针增加符合气象习惯。参数说明widthnp.deg2rad(22.5)是每个扇形的角度宽度16 方位正好 22.5 度。bottom累加实现堆叠效果。bbox_to_anchor把图例放到图外避免遮挡。4.3 热力图用颜色矩阵展示多要素日变化热力图适合展示「时间 × 要素」的二维模式比如一天 24 小时、一年 12 个月的气温分布。import seaborn as sns # 构造 月份 × 小时 的气温矩阵 pivot df.pivot_table( values气温, indexdf.index.month, columnsdf.index.hour, aggfuncmean ) fig, ax plt.subplots(figsize(14, 6)) sns.heatmap( pivot, cmapRdYlBu_r, # 红黄蓝反转红高温 annotFalse, # 不显示数值太密 fmt.1f, cbar_kws{label: 气温 (℃)}, axax ) ax.set_xlabel(小时) ax.set_ylabel(月份) ax.set_title(气温月-小时热力图) plt.tight_layout() plt.savefig(temp_heatmap.png, dpi150) plt.show()逻辑说明pivot_table把数据按月份和小时聚合aggfuncmean求平均。cmapRdYlBu_r是气象常用色带红色高温蓝色低温_r表示反转。annotFalse因为 12×24 格子太密标数值看不清。参数说明pivot_table的aggfunc可以换成max、min、std看不同统计量。cmap可选coolwarm、viridis但气象温度用红蓝色带最直观。如果数据有缺月pivot_table会自动跳过热力图会出现空白行。5. 避坑与排查气象可视化里最容易翻车的五个地方5.1 中文字体显示成方块现象图表标题、轴标签里的中文全部变成方框英文和数字正常。原因matplotlib 默认字体不含中文字形Windows 下 SimHei 需要显式指定Linux 和 macOS 字体名不同。解决先查系统可用字体再设置 rcParams。import matplotlib.font_manager as fm # 查看系统所有可用字体 fonts [f.name for f in fm.fontManager.ttflist] print([f for f in fonts if Hei in f or Song in f or YaHei in f]) # Windows plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] # macOS # plt.rcParams[font.sans-serif] [Arial Unicode MS, PingFang SC] # Linux # plt.rcParams[font.sans-serif] [WenQuanYi Micro Hei, Noto Sans CJK SC] plt.rcParams[axes.unicode_minus] False注意axes.unicode_minus必须设 False否则负号也会变方块。如果换了环境字体又失效把字体文件路径直接传给 FontProperties 更稳。5.2 时间轴排序错乱导致折线来回跳现象折线图出现大量来回穿插的竖线时间轴顺序明显不对。原因数据读取后没有按时间排序或者时间列被当成字符串排序10:00 排在 9:00 前面。解决读入后立即转 datetime 并排序。df[datetime] pd.to_datetime(df[datetime], errorscoerce) df df.dropna(subset[datetime]).set_index(datetime).sort_index()排查方法df.index.is_monotonic_increasing返回 False 就说明没排好。另外检查dtypes时间列必须是datetime64[ns]而不是object。5.3 风向平均算成 180 度反向现象计算平均风向时结果和实际感受完全相反或者风玫瑰图主风向和原始数据对不上。原因风向是角度0 度和 360 度是同一个方向直接对角度求算术平均会出错。比如 350 度和 10 度平均算术平均是 180 度实际应该是 0 度。解决转成 u/v 分量再平均最后反算角度。# 错误做法 # mean_dir df[风向].mean() # 不要这样 # 正确做法 u_mean df[u].mean() v_mean df[v].mean() mean_dir (np.rad2deg(np.arctan2(-u_mean, -v_mean))) % 360参数说明arctan2返回弧度np.rad2deg转角度% 360保证结果在 0-360。u/v 分量的正负号取决于坐标系定义气象上 u 向东为正v 向北为正。5.4 重采样后数据量暴增或暴减现象resample之后行数不对或者出现大量 NaN。原因resample的频率参数和数据实际频率不匹配或者label、closed参数没设对。解决先确认原始数据频率再设 resample 参数。# 查看原始数据时间间隔 time_diff df.index.to_series().diff().value_counts() print(time_diff.head()) # 看最常见的时间间隔 # 小时数据重采样为日数据 daily df.resample(D).agg({ 气温: mean, 降水: sum, # 降水要累加 风速: max }) # 关键参数label控制标签在区间左还是右closed控制区间开闭 daily df.resample(D, labelleft, closedleft).mean()注意不同要素的聚合方式不同气温用 mean降水用 sum风速用 max 或 mean。统一用 mean 会把降水量算错。5.5 图片保存后空白或尺寸不对现象plt.savefig保存的图片是空白或者尺寸和figsize不一致。原因savefig在show之后调用或者bbox_inches参数没设导致标签被裁。解决先 save 再 show加bbox_inchestight。plt.tight_layout() plt.savefig(output.png, dpi150, bbox_inchestight, facecolorwhite) plt.show() # show放最后参数说明bbox_inchestight自动扩展边界包含所有标签。facecolorwhite防止透明背景在文档里显示成黑色。如果图片还是空白检查是不是在plt.show()之后又创建了新 figure。6. 进阶技巧让气象图表从能看变成好用6.1 双轴图气温和气压叠加显示气温和气压量纲不同放同一个轴上气压会被压成直线。双轴图是标准解法。fig, ax1 plt.subplots(figsize(14, 5)) # 左轴气温 ax1.plot(df.index, df[气温], color#e74c3c, linewidth0.8, label气温) ax1.set_ylabel(气温 (℃), color#e74c3c) ax1.tick_params(axisy, labelcolor#e74c3c) # 右轴气压 ax2 ax1.twinx() ax2.plot(df.index, df[气压], color#3498db, linewidth0.8, label气压) ax2.set_ylabel(气压 (hPa), color#3498db) ax2.tick_params(axisy, labelcolor#3498db) # 合并图例 lines1, labels1 ax1.get_legend_handles_labels() lines2, labels2 ax2.get_legend_handles_labels() ax1.legend(lines1 lines2, labels1 labels2, locupper left) ax1.set_xlabel(时间) ax1.xaxis.set_major_formatter(mdates.DateFormatter(%m-%d %H:%M)) plt.xticks(rotation45, haright) plt.tight_layout() plt.savefig(temp_pressure.png, dpi150, bbox_inchestight) plt.show()逻辑说明twinx()创建共享 x 轴的第二个 y 轴。两个轴的曲线颜色和轴标签颜色对应读者一眼能分清。图例合并需要手动收集两个轴的 handles 和 labels。参数说明tick_params的labelcolor让轴刻度数字和曲线同色。如果两条曲线重叠严重可以给其中一条加alpha0.7透明度。6.2 用 plotly 做可交互图表静态图适合报告交互图适合探索。plotly 的graph_objects可以做出带缩放、悬停提示的图表。import plotly.graph_objects as go from plotly.subplots import make_subplots fig make_subplots(specs[[{secondary_y: True}]]) fig.add_trace( go.Scatter(xdf.index, ydf[气温], name气温, linedict(color#e74c3c)), secondary_yFalse ) fig.add_trace( go.Scatter(xdf.index, ydf[气压], name气压, linedict(color#3498db)), secondary_yTrue ) fig.update_layout( title气温与气压交互图, xaxis_title时间, hovermodex unified, # 悬停显示所有曲线值 templateplotly_white ) fig.update_yaxes(title_text气温 (℃), secondary_yFalse) fig.update_yaxes(title_text气压 (hPa), secondary_yTrue) fig.write_html(interactive.html) fig.show()逻辑说明make_subplots配合secondary_yTrue实现双轴。hovermodex unified让鼠标悬停时显示该时间点所有曲线的值比默认的逐点悬停好用。write_html导出独立 HTML 文件可以直接发给别人用浏览器打开。参数说明templateplotly_white是白色背景主题适合文档插入。数据量超过 5000 点时建议先降采样否则浏览器渲染卡顿。6.3 批量出图循环生成多站点图表实际项目里往往有几十个站点手动一个个画不现实。用循环加函数封装。import os def plot_station(df, station_name, output_diroutput): 为单个站点生成气温时间序列图 os.makedirs(output_dir, exist_okTrue) fig, ax plt.subplots(figsize(12, 4)) ax.plot(df.index, df[气温], linewidth0.8, color#e74c3c) ax.set_title(f{station_name} 气温时间序列) ax.set_ylabel(气温 (℃)) ax.xaxis.set_major_formatter(mdates.DateFormatter(%m-%d)) plt.xticks(rotation45, haright) plt.tight_layout() filepath os.path.join(output_dir, f{station_name}_temp.png) plt.savefig(filepath, dpi150, bbox_inchestight) plt.close(fig) # 关键关闭figure释放内存 return filepath # 批量处理 stations df[站号].unique() for station in stations: station_df df[df[站号] station].copy() plot_station(station_df, station) print(f已生成{station})逻辑说明plt.close(fig)是批量出图的关键不关闭的话内存会持续增长几十张图之后可能崩溃。os.makedirs(exist_okTrue)保证输出目录存在。函数返回文件路径方便后续记录。参数说明dpi150批量出图时不要设太高300 会让文件体积翻倍。如果站点数据量差异大可以在函数里加判断数据点少于 100 的跳过或单独处理。6.4 一个我踩过的坑早期做气象可视化的时候我习惯把所有处理步骤写在一个脚本里读数据、清洗、画图混在一起。结果换一批数据要改十几处路径和列名改漏一处就报错。后来改成配置字典加函数分离读数据、清洗、绘图各是一个函数主流程只负责调用和传参。换数据只改配置字典里的路径和列名映射其他代码不动。这个习惯帮我省了大量重复调试时间也让脚本能直接交给同事复用。另外气象数据的时间列一定要在第一时间转成 datetime 并设为索引不要等到画图的时候才转。我见过太多因为时间列是字符串导致排序错误、重采样失败的案例提前处理好能避免后面 80% 的玄学问题。希望帮到你。本文还有配套的精品资源点击获取