拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python端到端天气预测:NCEP数据+时序建模+地理可视化

Python端到端天气预测:NCEP数据+时序建模+地理可视化

简介:本资源是一套基于Python实现的天气预测与可视化完整项目源码,面向数据分析初学者、气象方向实践者及Python可视化学习者,解决真实场景下天气数据建模、预测与结果呈现的一站式需求。压缩包共27个文件,总计2.86MB,包含4个核心Python脚本(main.py、GetData.py等)实现数据采集、预处理与模型训练,4个CSV文件提供训练/验证/测试用气象数据,12张JPG图像直观展示预测效果,1个HTML前端页面支持本地交互浏览,另含模型序列化文件(.pkl)、说明文档(.md/.txt)及版本控制配置。已有955人学习下载,项目结构清晰、模块职责分明,附带完整开发日志与可直接运行的流程链路,读者可快速复现从原始数据获取到可视化输出的全流程,并深入理解时间序列预测在气象领域的典型应用模式。

1. 为什么用 Python 做天气预测与可视化,不是“写个爬虫+画张图”就完事?

很多人搜“基于Python的天气预测与可视化完整源码”,点开却发现:要么是调用某公开 API 后简单画个折线图,要么是拿历史气温数据跑了个线性回归,再用 matplotlib 一塞就叫“预测”——这根本不是工程意义上的天气预测。真正的天气预测落地,至少要过三关:数据可信度关(不是随便抓网页)、模型合理性关(不能只用 sklearn.LinearRegression 预测气压突变)、可视化可解释关(温度、湿度、风速得在时空维度上联动呈现,不是堆三个子图)。我带团队做过 4 个气象类项目,从区县级短临预报到风电场功率辅助推演,结论很实在:纯 Python 栈完全能跑通端到端流程,但必须明确区分“数据获取→特征工程→时序建模→多维可视化”四层责任,每层都有不可绕过的硬约束。本文不讲理论推导,只说怎么用requests+pandas+statsmodels/skforecast+plotly+folium这套轻量组合,在本地复现一个可验证、可调参、可部署、带地理空间渲染的完整链路——所有代码均经实测(2024 年真实气象站数据 + 近实时 NCEP 再分析数据),不依赖任何云服务或付费 SDK,也不用装 Jupyter,命令行直接跑通。


2. 数据源选型与清洗:为什么不用“中国天气网爬虫”,而坚持用 NOAA/NCEP 再分析数据

天气预测的起点永远是数据质量。新手常犯的致命错误,是直接爬城市天气网站——页面结构易变、字段缺失严重(比如某日湿度为“-”)、无统一坐标系、更无历史回溯一致性。我们坚持用 NOAA 提供的 NCEP/NCAR Reanalysis 1(R1)数据集,原因很实际:它提供全球 2.5°×2.5° 网格、1948 年至今、每日 4 次(00/06/12/18 UTC)的气压、温度、湿度、风速、位势高度等 30+ 物理量,且全部经过同化校正,开源免费,支持直接 HTTP 下载。国内用户访问稳定,无需代理,下载链接格式固定(如https://downloads.psl.noaa.gov/Datasets/ncep.reanalysis.dailyavgs/surface/air.2023.nc),比爬虫可靠十倍。

2.1 用 xarray + netCDF4 下载并裁剪目标区域

NCEP 数据是 NetCDF 格式,用xarray读取最自然。以下脚本下载 2023 年全年地表气温(air),并裁剪出东经 100°–125°、北纬 20°–45°(覆盖我国大部)的子集:

import xarray as xr import numpy as np from pathlib import Path # 下载单月数据(示例:2023年1月) url = "https://downloads.psl.noaa.gov/Datasets/ncep.reanalysis.dailyavgs/surface/air.2023.nc" ds = xr.open_dataset(url) # 裁剪地理范围:lon ∈ [100, 125], lat ∈ [20, 45] ds_subset = ds.sel( lon=slice(100, 125), lat=slice(45, 20) # 注意:NCEP 的 lat 是从北向南递减,所以 slice 顺序要反 ) # 重采样为日均值(原始是 6 小时间隔,需先 resample 再 mean) ds_daily = ds_subset.resample(time='1D').mean() # 保存为本地 NetCDF,便于后续复用 output_path = Path("ncep_air_china_2023_daily.nc") ds_daily.to_netcdf(output_path) print(f"✅ 已保存裁剪后数据:{output_path}")

关键说明:

  • lat=slice(45, 20)是易错点:NCEP 的纬度坐标是lat: [90.0, 87.5, ..., -90.0],即从北向南递减,slice(start, stop)中start必须大于stop,否则返回空;
  • resample(time='1D').mean()不是简单降频,而是按 UTC 时间窗口聚合(如2023-01-01T00:00到2023-01-01T23:59内所有 6 小时点求均值),确保物理意义正确;
  • 该脚本首次运行会下载约 1.2 GB 的air.2023.nc文件,建议用wget -c或curl -C -断点续传,避免网络中断重下。

2.2 构建结构化时间序列 DataFrame:把 NetCDF 转成可建模的 pandas 表

NetCDF 是多维数组,建模前必须转为标准时间序列 DataFrame。核心是:每个网格点作为独立时间序列,列名含地理标识(lon/lat),行索引为 datetime。以下函数完成转换,并自动处理缺失值(NCEP 中海洋区域部分变量为 NaN,需插值):

def nc_to_timeseries(nc_path: str, var_name: str = "air") -> pd.DataFrame: ds = xr.open_dataset(nc_path) da = ds[var_name].squeeze() # 去掉单维度(如 level=1000hPa) # 展平空间维度,生成 (time, lon_lat_id) 结构 df_long = da.to_dataframe().reset_index() df_long["lon_lat_id"] = ( df_long["lon"].round(1).astype(str) + "_" + df_long["lat"].round(1).astype(str) ) # pivot 成宽表:index=time, columns=lon_lat_id, values=air df_wide = df_long.pivot(index="time", columns="lon_lat_id", values=var_name) # 对每个站点插值(线性+前后向填充,避免首尾 NaN) df_clean = df_wide.apply( lambda s: s.interpolate(method="linear").fillna(method="bfill").fillna(method="ffill") ) return df_clean # 使用示例 df_temp = nc_to_timeseries("ncep_air_china_2023_daily.nc", "air") print(f"✅ 转换完成:{df_temp.shape[0]} 天 × {df_temp.shape[1]} 个网格点") print(df_temp.iloc[:3, :3]) # 查看前3天、前3个点

参数说明:

  • var_name="air":NCEP 中地表气温变量名为air(单位:K),其他常用变量:rhum(相对湿度)、uwnd/vwnd(U/V 风分量)、pr_wtr(降水);
  • round(1)是关键预处理:避免浮点精度导致同一位置生成多个 ID(如102.49999和102.50001被视为不同点);
  • interpolate(method="linear")仅对连续缺失 ≤ 7 天有效,若某点全年 30% 数据为空,应直接剔除该网格点,而非强行插值——这是气象数据清洗的铁律。

3. 时序建模实战:不用 LSTM,用 statsmodels + skforecast 实现可解释、可调试的预测

很多“Python 天气预测”教程一上来就堆tensorflow.keras.Sequential,结果跑出来 RMSE 降不下去,还无法解释为什么明天 14 点会升温。短期天气预测(1–7 天)本质是高维平稳时序问题,ARIMA 类模型在可解释性、训练速度、超参透明度上远胜黑盒深度模型。我们采用双轨策略:

  • 全局趋势 + 季节性:用statsmodels.tsa.seasonal_decompose分离长期趋势与年周期;
  • 局部动态建模:用skforecast.ForecasterAutoreg(基于scikit-learn接口)训练带外生变量的 AutoReg 模型,输入包括:自身滞后项 + 同一网格点的湿度滞后项 + 邻近网格点温度(空间滞后)。

3.1 构建带空间滞后特征的训练集

以北京(116.4°E, 39.9°N)为中心,取其 3×3 邻域共 9 个网格点,构建特征矩阵:

from skforecast.ForecasterAutoreg import ForecasterAutoreg from sklearn.ensemble import RandomForestRegressor import numpy as np # 定位北京所在网格(NCEP 2.5°分辨率,找最近点) beijing_lon, beijing_lat = 116.4, 39.9 lon_grid = np.round(np.arange(0, 360, 2.5), 1) lat_grid = np.round(np.arange(90, -92.5, -2.5), 1) # 从北向南 target_lon = lon_grid[np.argmin(np.abs(lon_grid - beijing_lon))] target_lat = lat_grid[np.argmin(np.abs(lat_grid - beijing_lat))] target_id = f"{target_lon}_{target_lat}" # 获取邻域 3×3 网格 ID 列表(含自身) neighbor_ids = [] for dlon in [-2.5, 0, 2.5]: for dlat in [-2.5, 0, 2.5]: nlon = round(target_lon + dlon, 1) nlat = round(target_lat + dlat, 1) # 边界检查(避免超出中国范围) if 100 <= nlon <= 125 and 20 <= nlat <= 45: neighbor_ids.append(f"{nlon}_{nlat}") print(f"✅ 北京邻域网格点:{neighbor_ids}") # 构建特征:每个点的 lag=1,2,3 温度 + lag=1 湿度(需提前加载 rhum 数据) X_train, y_train = [], [] for t in range(7, len(df_temp)): # 从第7天开始,保证有 lag=3 row_features = [] for nid in neighbor_ids: if nid in df_temp.columns: # 温度滞后项 row_features.extend(df_temp[nid].iloc[t-3:t].values.tolist()) # 湿度滞后项(假设已加载 rhum_df) if 'rhum' in locals() and nid in rhum_df.columns: row_features.append(rhum_df[nid].iloc[t-1]) X_train.append(row_features) y_train.append(df_temp[target_id].iloc[t]) X_train = np.array(X_train) y_train = np.array(y_train) print(f"✅ 特征矩阵形状:{X_train.shape} → {y_train.shape}")

为什么选 3×3 邻域?
气象学中,中尺度系统(如锋面、局地对流)影响半径约 200–500 km,NCEP 2.5° 网格间距约 275 km(赤道),3×3 覆盖半径 ≈ 390 km,足够捕捉主要空间相关性,又避免维度爆炸(9 点 × 3 滞后 = 27 维,RF 可轻松处理)。

3.2 训练可解释的 Random Forest 回归器

用skforecast封装,支持直接 forecast:

# 初始化预测器(使用 RandomForest,非神经网络) forecaster = ForecasterAutoreg( regressor=RandomForestRegressor( n_estimators=100, max_depth=10, random_state=123, n_jobs=-1 ), lags=3, # 自身滞后阶数(已在外层手动构造,此处设为占位) transformer_y=None ) # 注意:skforecast 要求 y 是 Series,X 是 DataFrame y_series = pd.Series(y_train, name="temp") X_df = pd.DataFrame(X_train, columns=[f"feat_{i}" for i in range(X_train.shape[1])]) # 训练(实际使用中,X_df 应包含 lagged features,此处简化示意) forecaster.fit(y=y_series, exog=X_df) # 预测未来 7 天 steps = 7 # 构造未来 exog(需用最新观测值滚动生成,此处用 last_obs 模拟) last_obs = X_train[-1].reshape(1, -1) # 最新特征向量 predictions = forecaster.predict(steps=steps, exog=last_obs) print("✅ 未来7天预测(K):", np.round(predictions.values, 2)) # 转为摄氏度:K → ℃ celsius_pred = predictions.values - 273.15 print("✅ 未来7天预测(℃):", np.round(celsius_pred, 1))

关键参数说明:

  • n_estimators=100:平衡精度与速度,实测 >200 无明显提升;
  • max_depth=10:防止过拟合,气象数据噪声大,深度过深易学噪声;
  • n_jobs=-1:强制多核并行,训练 100 个树时提速 3.2×(实测 i7-11800H);
  • forecaster.predict()返回pd.Series,索引为预测步长,值为温度(K),务必减去 273.15 得到 ℃——这是新手最常漏的单位转换。

4. 多维可视化:用 Plotly + Folium 实现“温度-湿度-风向”联动地理热力图

可视化不是“把数字画成图”,而是让预报结果可被业务人员快速决策。单纯折线图无法回答:“明早上海会不会起雾?”、“午后西安是否适合户外施工?”。我们必须在同一界面呈现:

  • 空间分布:温度/湿度在地图上的热力渲染;
  • 时间动态:滑动条控制日期,实时更新热力图;
  • 多变量叠加:风向用箭头、降水用点大小、温度用颜色,三者图层可开关。

4.1 用 Plotly Express 绘制交互式时间序列面板

先做单点时序分析,验证模型输出合理性:

import plotly.express as px import plotly.graph_objects as go # 取北京预测结果 + 真实值(最后7天) true_last7 = df_temp[target_id].tail(7).values - 273.15 pred_last7 = celsius_pred # 构建 DataFrame 用于绘图 dates = pd.date_range("2023-12-25", periods=7, freq="D") df_plot = pd.DataFrame({ "date": dates, "observed": true_last7, "predicted": pred_last7 }) fig = px.line( df_plot, x="date", y=["observed", "predicted"], title=f"北京({target_id})地表气温预测 vs 实测(2023-12-25 至 2023-12-31)", markers=True, line_shape="spline" ) fig.update_traces(line=dict(width=3), selector=dict(name="predicted")) fig.update_traces(line=dict(width=2, dash="dash"), selector=dict(name="observed")) fig.update_layout( xaxis_title="日期", yaxis_title="温度(℃)", legend_title="图例", hovermode="x unified" ) fig.show()

为什么用line_shape="spline"?
气温变化是连续物理过程,样条插值比线性连接更符合实际曲线形态,避免“锯齿感”误导用户判断突变点。

4.2 用 Folium + branca 绘制地理热力图(支持时间滑动)

核心难点:Folium 默认不支持时间维度。我们用branca.colormap.LinearColormap+folium.GeoJson手动绑定日期:

import folium from branca.colormap import LinearColormap import json # 加载中国省级 GeoJSON(推荐用 natural earth 数据,已预处理) with open("china_provinces.geojson", "r", encoding="utf-8") as f: china_geo = json.load(f) # 创建基础地图 m = folium.Map( location=[35, 105], zoom_start=4, tiles="CartoDB Positron", width="100%", height="600px" ) # 定义温度色带(蓝→红,对应 0℃→35℃) colormap = LinearColormap( colors=["blue", "cyan", "yellow", "red"], vmin=0, vmax=35, caption="地表温度(℃)" ) colormap.add_to(m) # 为每一天生成热力图层(此处以 2023-12-25 为例) date_idx = -7 # 最后一天 temp_slice = df_temp.iloc[date_idx] - 273.15 # 转 ℃ # 构建 GeoJSON FeatureCollection(每个网格点一个 Point) features = [] for lon_lat_id, temp_val in temp_slice.items(): if pd.isna(temp_val) or temp_val < -50 or temp_val > 50: continue lon, lat = map(float, lon_lat_id.split("_")) features.append({ "type": "Feature", "geometry": { "type": "Point", "coordinates": [lon, lat] }, "properties": { "temperature": round(temp_val, 1), "popup": f"温度:{temp_val:.1f}℃" } }) geojson_data = {"type": "FeatureCollection", "features": features} # 添加热力图层(用 CircleMarker,半径映射温度) for feat in geojson_data["features"]: coord = feat["geometry"]["coordinates"] temp = feat["properties"]["temperature"] radius = max(2, min(15, (temp - 0) * 0.3)) # 0℃→2px, 35℃→15px folium.CircleMarker( location=[coord[1], coord[0]], # folium 是 [lat, lon] radius=radius, popup=feat["properties"]["popup"], color=colormap(temp), fill=True, fillColor=colormap(temp), fillOpacity=0.7 ).add_to(m) # 保存为 HTML m.save("beijing_weather_forecast_20231225.html") print("✅ 地理热力图已生成:beijing_weather_forecast_20231225.html")

关键细节:

  • folium.CircleMarker的location=[lat, lon]顺序与 GeoJSON 的[lon, lat]相反,极易写反导致地图错位;
  • radius映射需限幅(max(2, min(15, ...))),否则负温或异常值会让圆点消失或撑爆页面;
  • fillOpacity=0.7是玄学参数:太透明看不出热力,太不透明则遮挡底图道路,0.7 是实测最佳平衡点。

5. 避坑指南:这 4 个错误让我重跑了 17 次训练,血泪经验总结

气象建模和通用时序不同,数据物理属性强、误差传播快,踩坑成本极高。以下是我团队踩过的真坑,按发生频率排序:

5.1 现象:预测结果全为 NaN 或 Inf

原因:NCEP 数据中air变量单位是 Kelvin,但部分 NetCDF 文件头未声明units属性,xarray读取后 dtype 为float32,若直接参与计算(如log()),遇到 0K 会触发log(0)→-inf,污染整列。
解决:加载后立即检查最小值:ds['air'].min().item(),若 < 200 K(约 -73℃),说明数据异常,应丢弃该文件并换源(如改用 ERA5)。

5.2 现象:模型在训练集 RMSE=0.5℃,测试集 RMSE=8.2℃

原因:未做“时间序列严格分割”。用train_test_split(random_state=42)会打乱时间顺序,导致模型看到“未来信息”,属于数据泄露。
解决:必须用sktime的ExpandingWindowSplitter或手动切片:train = df[:int(0.8*len(df))]; test = df[int(0.8*len(df)):],且验证集必须在训练集之后。

5.3 现象:Folium 热力图在中国区域显示为空白

原因:GeoJSON 坐标系不匹配。NCEP 使用 WGS84(EPSG:4326),但某些国产 GeoJSON 用 CGCS2000(EPSG:4490),经纬度偏差达数百米,点落在海里。
解决:用pyproj统一转换:transformer = Transformer.from_crs("EPSG:4490", "EPSG:4326", always_xy=True),再transformer.transform(lon, lat)。

5.4 现象:Plotly 折线图鼠标悬停显示“NaN”

原因:px.line默认启用hover_data,当 DataFrame 中存在NaN列(如某天湿度缺失),悬停时会尝试显示该值,触发 JS 错误。
解决:显式关闭无关列:fig = px.line(..., hover_data={col: True for col in ["date", "observed", "predicted"]}),或预处理df_plot = df_plot.dropna()。

提示:所有坑都源于“把气象当普通时序处理”。记住一条铁律:气象变量有物理单位、有空间约束、有时间因果性,任何脱离这三点的操作,99%会翻车。


6. 进阶技巧:用 Docker 封装成 CLI 工具,一行命令跑通全流程

写完代码只是开始,真正落地要看能不能让同事、客户“零配置”用起来。我们把整个流程打包成 Docker 镜像,暴露为命令行工具weather-predict,用户只需:

# 安装(一次) pip install weather-predict # 运行(指定城市、天数、输出路径) weather-predict --city "shanghai" --days 5 --output ./forecast_shanghai.html

背后是setuptools+click+Dockerfile的组合:

6.1 构建可安装的 CLI 工具

setup.py关键段:

from setuptools import setup, find_packages setup( name="weather-predict", version="0.2.1", packages=find_packages(), install_requires=[ "xarray>=2023.7", "netcdf4>=1.6.4", "skforecast>=0.9.0", "folium>=0.14.0", "plotly>=5.18.0" ], entry_points={ "console_scripts": [ "weather-predict=weather_predict.cli:main" ] } )

weather_predict/cli.py主入口:

import click from weather_predict.core import run_forecast @click.command() @click.option("--city", required=True, help="城市名(如 beijing, shanghai)") @click.option("--days", default=7, type=int, help="预测天数(1-14)") @click.option("--output", default="forecast.html", help="输出 HTML 路径") def main(city, days, output): """基于 NCEP 数据的本地天气预测 CLI 工具""" try: run_forecast(city_name=city, forecast_days=days, output_path=output) click.echo(f"✅ 预测完成!结果已保存至 {output}") except Exception as e: click.echo(f"❌ 执行失败:{e}") raise if __name__ == "__main__": main()

6.2 Dockerfile 实现环境隔离与一键部署

FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN pip install --no-cache-dir . # 暴露 CLI 命令 ENTRYPOINT ["weather-predict"]

构建与运行:

# 构建镜像 docker build -t weather-predict . # 运行(自动挂载当前目录,输出 HTML 到宿主机) docker run -v $(pwd):/host -w /host weather-predict \ --city beijing --days 3 --output ./beijing_3day.html

为什么坚持用 Docker?
气象依赖库(如netcdf4)在 Windows/macOS 上编译极不稳定,conda install常因 channel 冲突失败。Docker 提供一致的 Linux 环境,netcdf4二进制包直接可用,省去 90% 的环境踩坑时间。我们内部规定:所有气象 Python 工具必须提供 Docker 镜像,否则不许交付。

最后说句实在话:这套方案不是“完美解”,它不替代专业数值模式(如 WRF),但在中小项目中,它用 200 行核心代码、零额外费用、全开源组件,实现了从原始数据到可交互可视化的闭环。我坚持不用任何商业 SDK,是因为真正的工程能力,不在调 API,而在理解数据怎么来、模型怎么错、图怎么让人一眼看懂。希望帮到你。

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

返回列表