简介这是一份面向Python初学者与高校课程设计学生的天气预测与可视化综合实践项目聚焦气象数据爬取、时序建模预测及多维度图表呈现适用于期末大作业、课程实训或数据分析入门实战。资源共24个文件包含4个核心Python脚本main.py、GetData.py、ProcessData.py、GetModel.py、4个CSV数据集训练/验证/测试/今日天气、12张效果截图展示预测曲线、热力图、地图叠加等可视化成果、1份Markdown使用文档及1个预训练模型pkl文件包体仅1.42MB轻量易部署。已有203人学习下载项目经助教审定、本地实测可运行评审分达95分以上配套文档详述环境配置、数据获取路径、模型调参逻辑与绘图定制方法特别适合理解从原始数据采集到结果可视化的完整闭环流程。1. 这不是「调个 API 显示天气」的玩具项目它用真实气象数据跑通了从爬虫→特征工程→LSTM 预测→动态可视化全链路期末答辩被助教当场截图存档你见过多少“Python 天气预测”项目点开全是 requests.get(https://api.xxx.com/weather) print(f今天{data[temp]}℃) —— 这类代码连数据清洗都没有更别说模型验证和时序建模。而这个资源包是真正跑在本地、用中国 31 个省会城市近 3 年实测气象数据date_train.csv / date_valid.csv / date_test.csv训练出可复现 LSTM 模型Model.pkl并用 PlotlyFlask 渲染出带时间滑块、多变量联动、支持导出 PNG 的交互式天气热力图wps23.jpg ~ wps28.jpg 全是运行截图。它不是 demo是完整闭环GetData.py 爬取天气网历史数据 → ProcessData.py 做缺失值插补、滑动窗口构造时序样本 → GetModel.py 定义 LSTM 结构并保存权重 → main.py 一键启动预测可视化服务。适合 Python 初学者练手有详细 readme.md 和 step-by-step 使用文档也经得起课程设计答辩拷问——95 分高分背后是每个模块都留了 debug 日志开关、每个 CSV 都标注了字段含义、每个绘图参数都可调。如果你正卡在「学完 Pandas/Numpy/Scikit-learn 却不知道怎么串成项目」或者需要一份能直接交作业、还能讲清楚技术选型理由的期末大作业它就是那个不翻车的底牌。2. 数据获取与预处理为什么不用公开 API 而坚持爬取「天气网」三个硬约束决定了必须自己动手2.1 爬虫逻辑拆解GetData.py 不是简单抓 HTML而是对抗反爬结构化清洗双线程这个项目没用任何第三方天气 API如和风、心知原因很实际期末作业要求「数据来源可追溯、过程可复现」。公开 API 的 key 有效期短、调用频次受限、返回字段不稳定比如某天突然把「湿度」字段名从humidity改成rh答辩时老师一句「你这数据哪来的」就可能扣分。所以 GetData.py 直接定位「天气网tianqi.com」的历史天气页用 requests BeautifulSoup 抓取关键在于它绕过了两个典型障碍动态 UA 轮换天气网对固定 User-Agent 会返回 403代码里内置了 5 个主流浏览器 UA 字符串每次请求随机切换日期参数防错机制URL 中的年月日必须是合法日期如不能传 2023-02-30GetData.py 内置了 datetime 校验自动跳过非法日期并记录 warning# GetData.py 片段UA 轮换与日期校验核心逻辑 import requests from datetime import datetime, timedelta import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ] def is_valid_date(year, month, day): try: datetime(year, month, day) return True except ValueError: return False def fetch_weather_data(city_code, year, month, day): if not is_valid_date(year, month, day): print(f[WARN] Invalid date: {year}-{month}-{day}, skipped) return None url fhttps://www.tianqi.com/{city_code}/{year}{month:02d}{day:02d}/ headers {User-Agent: random.choice(USER_AGENTS)} try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() # 后续解析 HTML 表格... return parse_weather_table(resp.text) except Exception as e: print(f[ERROR] Failed to fetch {url}: {e}) return None提示city_code是天气网为每个城市分配的英文缩写如北京是beijing广州是guangzhou项目已内置china_today.csv提供全国主要城市 code 映射表无需手动查。2.2 数据清洗关键ProcessData.py 如何把「脏 HTML 表格」变成 LSTM 可吃的时序张量爬下来的数据是 HTML 表格字段混杂如「气温」列含「12℃~23℃」、「多云转晴」等非数值ProcessData.py 的核心任务是统一单位、填充缺失、构造滑动窗口、标准化。它不做简单 dropna而是用「前向填充 线性插值」组合策略对连续型字段温度、湿度、气压先用前向填充ffill补短期断点再对长段缺失用线性插值interpolate(methodlinear)对离散型字段天气状况、风向转换为 one-hot 编码如「晴」「多云」「小雨」→ [1,0,0], [0,1,0], [0,0,1]避免模型误判数值大小关系# ProcessData.py 片段温度字段清洗与滑动窗口构造 import pandas as pd import numpy as np def clean_temperature_series(df): # 提取最高温字符串中第一个数字 df[high_temp] df[weather_text].str.extract(r(\d)℃).astype(float) # 提取最低温字符串中第二个数字 df[low_temp] df[weather_text].str.extract(r℃~(\d)℃).astype(float) # 前向填充 线性插值 df[high_temp] df[high_temp].ffill().interpolate(methodlinear) df[low_temp] df[low_temp].ffill().interpolate(methodlinear) return df def create_sliding_window(data, window_size7, pred_horizon1): 构造滑动窗口输入过去7天数据预测第8天最高温 data: shape (n_samples, n_features) returns: X (n_samples-window_size, window_size, n_features), y (n_samples-window_size, 1) X, y [], [] for i in range(len(data) - window_size - pred_horizon 1): X.append(data[i:(i window_size)]) y.append(data[i window_size pred_horizon - 1, 0]) # 预测 high_temp return np.array(X), np.array(y).reshape(-1, 1) # 实际调用 df_clean clean_temperature_series(raw_df) features [high_temp, low_temp, humidity, pressure, wind_speed] X_train, y_train create_sliding_window( df_clean[features].values, window_size7, pred_horizon1 )这段代码输出的X_train是三维数组(n_samples, 7, 5)正好喂给 LSTM 层y_train是一维目标向量。注意window_size7是项目默认值你完全可以改成 14 或 30 —— 但改之前得确认数据量是否足够len(data) window_size pred_horizon否则create_sliding_window会返回空数组后续训练直接报错。2.3 数据集文件清单与字段说明别急着 run先看懂 date_train.csv 里每一列在说什么项目提供的三个 CSV 文件不是随便命名的它们对应机器学习标准流程文件名用途行数范围关键字段共 12 列字段说明date_train.csv训练集模型学习规律~2000 行date,city,high_temp,low_temp,weather,humidity,pressure,wind_direction,wind_speed,air_quality,uv_index,precipitationweather是原始文本如「多云」air_quality是数值0-500precipitation单位 mmdate_valid.csv验证集调参时监控过拟合~500 行同上与 train 同分布但时间上连续train 是 2021-01~2022-06valid 是 2022-07~2022-12date_test.csv测试集最终评估模型泛化能力~300 行同上时间最晚2023-01~2023-03确保无时间穿越注意所有 CSV 的date列格式为YYYY-MM-DDcity列是中文城市名如「北京」不是拼音或 code。ProcessData.py 会自动做 city → one-hot 映射所以你新增城市只需往 CSV 里加行不用改代码。3. 模型构建与训练LSTM 不是玄学它的输入维度、层数、Dropout 值全由数据长度和预测目标决定3.1 GetModel.py 的 LSTM 结构为什么用 2 层 LSTM Dropout0.3参数选择有据可依很多初学者以为 LSTM 层数越多越好其实不然。本项目GetModel.py定义的模型结构是经过验证的平衡点输入层(batch_size, 7, 12)→ 7 天 × 12 个特征注意date_train.csv有 12 列ProcessData.py 未删减LSTM 层1return_sequencesTrue输出(batch_size, 7, 64)保留时间步以便下一层继续处理LSTM 层2return_sequencesFalse输出(batch_size, 64)压缩为单个向量Dense 层units32Dropout(0.3)→ 防止过拟合0.3 是经验值小于 0.2 泛化差大于 0.5 收敛慢输出层units1回归预测单个数值如最高温# GetModel.py 片段LSTM 模型定义Keras 2.x 风格 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam def build_lstm_model(input_shape(7, 12)): model Sequential([ LSTM(64, return_sequencesTrue, input_shapeinput_shape), Dropout(0.3), LSTM(64, return_sequencesFalse), Dropout(0.3), Dense(32, activationrelu), Dropout(0.3), Dense(1) # 输出单个预测值 ]) model.compile( optimizerAdam(learning_rate0.001), lossmse, metrics[mae] ) return model # 实际调用main.py 中 model build_lstm_model(input_shape(7, 12)) history model.fit( X_train, y_train, validation_data(X_valid, y_valid), epochs100, batch_size32, verbose1 ) model.save(Model.pkl) # 注意这里用的是 keras.models.save_model不是 pickle.dump关键细节input_shape(7, 12)必须与create_sliding_window输出的X_train.shape[1:]严格一致。如果改了window_size这里必须同步改否则model.fit()会报ValueError: Input 0 of layer sequential is incompatible with layer。3.2 模型评价指标为什么 MAE 比 RMSE 更适合作为天气预测的核心指标项目在main.py中同时计算了MAE平均绝对误差和RMSE均方根误差但答辩时重点展示MAE原因很务实MAE 单位与原始数据一致比如最高温预测 MAE2.1℃老师一眼看懂「平均误差 2 度」RMSE 对异常值敏感某天模型把 35℃ 预测成 15℃误差 20℃RMSE 会被平方放大导致整体分数虚高掩盖日常预测稳定性业务场景更看重「日常误差」气象服务用户关心「明天大概几度」而不是「极端天气能否精准捕捉」# main.py 片段模型评价逻辑 from sklearn.metrics import mean_absolute_error, mean_squared_error y_pred model.predict(X_test) mae mean_absolute_error(y_test, y_pred) rmse np.sqrt(mean_squared_error(y_test, y_pred)) print(fTest MAE: {mae:.2f}℃) print(fTest RMSE: {rmse:.2f}℃) # 附加绘制预测 vs 真实值散点图用于答辩 PPT import matplotlib.pyplot as plt plt.scatter(y_test, y_pred, alpha0.6) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--, lw2) plt.xlabel(True Temperature (℃)) plt.ylabel(Predicted Temperature (℃)) plt.title(Prediction Accuracy) plt.savefig(prediction_scatter.png, dpi300, bbox_inchestight)这段代码生成的prediction_scatter.png就是wps25.jpg的来源——它比单纯报数字更有说服力。3.3 避坑LSTM 训练失败的四个高频现象、原因与秒级修复方案现象 1ValueError: Input 0 of layer lstm is incompatible with layer原因X_train.shape与input_shape不匹配。常见于修改window_size后忘记同步改build_lstm_model的input_shape。解决打印X_train.shape确认第二维时间步和第三维特征数与input_shape一致。例如X_train.shape(1950, 7, 12)→input_shape(7,12)。现象 2训练 loss 不下降始终在 100 波动原因数据未归一化。LSTM 对输入尺度极度敏感原始温度0~40、湿度0~100、气压950~1050量纲差异太大。解决在ProcessData.py中加入MinMaxScaler或StandardScaler。项目已预留接口取消# scaler StandardScaler()注释并对X_train/X_valid/X_test统一 fit-transform。现象 3CUDA out of memoryGPU 显存不足原因默认用 GPU 训练但学生笔记本显存仅 2GBbatch_size32 超载。解决在main.py开头加两行import os os.environ[CUDA_VISIBLE_DEVICES] -1 # 强制 CPU 模式CPU 训练慢 3~5 倍但 100 epoch 仍可在 20 分钟内完成且结果一致。现象 4Model.pkl加载后预测结果全为 nan原因模型保存/加载方式错误。项目用model.save(Model.pkl)是 Keras 原生保存HDF5 格式但有人误用pickle.dump(model, open(Model.pkl,wb))导致结构丢失。解决严格按项目 README 执行model tf.keras.models.load_model(Model.pkl)不要用 pickle。4. 可视化系统搭建Flask Plotly 不只是「画个图」而是实现「城市切换变量联动时间回溯」三合一交互4.1main.py的 Flask 服务架构为什么用/predict接口而非直接渲染 HTML项目没有用 Jupyter Notebook 或 Matplotlibplt.show()而是启动一个本地 Web 服务http://127.0.0.1:5000原因在于答辩演示刚需老师用手机扫二维码就能看效果不用装 Python 环境交互逻辑解耦前端HTML/CSS/JS只负责展示后端Flask只负责计算符合工程规范动态更新友好点击「切换城市」按钮前端发 AJAX 请求到/predict?city上海后端实时加载对应数据并返回 JSON页面局部刷新# main.py 片段Flask 路由定义 from flask import Flask, render_template, request, jsonify import pandas as pd import numpy as np from tensorflow.keras.models import load_model app Flask(__name__) model load_model(Model.pkl) app.route(/) def index(): return render_template(天气网.html) # 静态首页 app.route(/predict) def predict(): city request.args.get(city, 北京) # 加载该城市的 test 数据date_test.csv 中过滤 df_test pd.read_csv(date_test.csv) df_city df_test[df_test[city] city].copy() # 构造输入同 training 逻辑 X_input df_city[FEATURES].values[-7:].reshape(1, 7, len(FEATURES)) pred model.predict(X_input)[0, 0] # 返回 JSON前端用 Plotly 动态渲染 return jsonify({ city: city, predicted_high_temp: float(pred), actual_high_temp: float(df_city[high_temp].iloc[-1]) }) if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)注意FEATURES [high_temp,low_temp,humidity,pressure,wind_speed]是硬编码在main.py顶部的列表如果你在ProcessData.py中增删了特征这里必须同步更新否则X_input维度错乱。4.2天气网.html的 Plotly 动态图表如何用 30 行 JS 实现「时间滑块拖拽即重绘」天气网.html不是静态图片而是嵌入了 Plotly.js 的交互式图表。核心逻辑在script标签里初始化图表加载时默认显示北京未来 7 天预测调用/predict?city北京时间滑块绑定input typerange拖动时触发updateChart()函数动态数据源滑块值映射到date_test.csv的行索引前端直接读取 CSV通过fetch(date_test.csv)并提取对应日期数据!-- 天气网.html 片段Plotly 图表初始化 -- div idchart stylewidth:100%;height:400px;/div input typerange min0 max299 value0 idtimeSlider script srchttps://cdn.plot.ly/plotly-latest.min.js/script script let chartData []; async function loadData() { const response await fetch(date_test.csv); const csvText await response.text(); const rows csvText.split(\n).slice(1); // skip header chartData rows.map(row { const cols row.split(,); return { date: cols[0], city: cols[1], high_temp: parseFloat(cols[2]), low_temp: parseFloat(cols[3]) }; }); } function updateChart(index) { const data chartData.slice(index, index7); const dates data.map(d d.date); const highs data.map(d d.high_temp); const lows data.map(d d.low_temp); Plotly.react(chart, [{ x: dates, y: highs, name: 预测最高温, mode: linesmarkers }, { x: dates, y: lows, name: 预测最低温, mode: linesmarkers }], { title: 未来7天天气预测${data[0].date} ~ ${data[6].date}, xaxis: {title: 日期}, yaxis: {title: 温度 (℃)} }); } document.getElementById(timeSlider).oninput function() { updateChart(parseInt(this.value)); }; loadData().then(() updateChart(0)); /script这段 JS 直接操作 CSV 文本不依赖后端所以即使 Flask 服务关闭滑块依然能本地运行——这是答辩时的「后悔药」万一现场网络故障你还能指着屏幕说「看纯前端也能跑」。4.3 可视化参数自定义如何修改wps23.jpg那种热力图的颜色映射和分辨率项目截图wps23.jpg是用plotly.express.imshow()生成的「城市-时间-温度」热力图其核心参数在main.py的generate_heatmap()函数里参数当前值修改建议效果zmin/zmaxNone自动设为zmin0, zmax40固定色标范围避免单日极端值拉伸整个颜色条color_continuous_scaleViridis改为RdBu或Plasma更符合气象习惯红热蓝冷width/height800/600改为1200/800适配答辩 PPT 宽屏导出 PNG 更清晰# main.py 片段热力图生成函数可直接复制修改 import plotly.express as px import pandas as pd def generate_heatmap(df, output_pathheatmap.png): # df 格式index城市, columns日期, values最高温 fig px.imshow( df.T, # 转置使城市为 y 轴 labels{x: 城市, y: 日期, color: 最高温 (℃)}, color_continuous_scaleRdBu, zmin0, zmax40, width1200, height800 ) fig.write_image(output_path, enginekaleido) # 需 pip install kaleido print(fHeatmap saved to {output_path}) # 调用示例 # df_heat load_city_temp_matrix() # 你自己的数据构造逻辑 # generate_heatmap(df_heat)提示kaleido是 Plotly 官方推荐的高质量导出引擎比orca更稳定。安装命令pip install kaleido若报错kaleido not found请先升级 pippython -m pip install --upgrade pip。5. 从零运行全流程五步走通「下载→环境配置→数据准备→模型训练→可视化部署」附每步耗时与验证点5.1 第一步环境配置耗时 ≤ 3 分钟验证点python --version和pip list这不是「装 Python」教程而是针对本项目最小化依赖清单# 创建虚拟环境推荐避免污染全局 python -m venv weather_env weather_env\Scripts\activate # Windows # source weather_env/bin/activate # macOS/Linux # 安装核心依赖版本锁定避免兼容问题 pip install tensorflow2.13.0 pip install pandas1.5.3 pip install numpy1.23.5 pip install scikit-learn1.2.2 pip install flask2.2.5 pip install plotly5.18.0 pip install kaleido0.2.1 # 导出高清图必需验证点运行python -c import tensorflow as tf; print(tf.__version__)输出2.13.0且无ImportError。若报No module named tensorflow检查是否激活了weather_env。5.2 第二步数据准备耗时 ≤ 1 分钟验证点date_train.csv行数 ≥ 1800项目 ZIP 包里已含全部 CSV无需重新爬取GetData.py是备用方案。只需确认解压后目录结构正确weather/ ├── main.py ├── ProcessData.py ├── GetModel.py ├── GetData.py ├── date_train.csv ├── date_valid.csv ├── date_test.csv └── 天气网.html用 Excel 或head -n 5 date_train.csv查看前 5 行确认有date,city,high_temp,...12 列且high_temp列数值合理非空、非负。验证点python -c import pandas as pd; print(len(pd.read_csv(date_train.csv)))输出1950或接近值。若为0说明 CSV 路径错误或文件损坏。5.3 第三步模型训练耗时 15~25 分钟验证点Model.pkl生成 history.history[loss]末尾 5直接运行main.py即可启动全流程python main.py它会自动执行加载date_train.csv→ProcessData.py清洗 → 构造X_train/y_train调用GetModel.py创建模型 →model.fit()训练 100 epoch用date_valid.csv验证 → 保存Model.pkl启动 Flask 服务 → 打开浏览器访问http://127.0.0.1:5000。验证点训练日志末尾出现100/100 [] - loss: 3.2145 - mae: 1.8923loss 5 即合格当前目录生成Model.pkl文件大小 ≥ 2MB浏览器打开后显示天气网.html页面。5.4 第四步可视化验证耗时 ≤ 30 秒验证点wps23.jpg类似热力图成功渲染在浏览器中点击「城市选择」下拉框选「上海」→ 页面右上角显示「上海28.3℃」拖动底部时间滑块 → 图表日期自动更新线条平滑无断裂点击「生成热力图」按钮 → 弹出heatmap.png下载提示打开后确认是 1200×800 像素、红蓝渐变的城市温度矩阵。验证点若点击按钮无反应检查浏览器控制台F12 → Console是否有Failed to load resource: date_test.csv错误——说明date_test.csv未与天气网.html放在同一目录。5.5 第五步答辩交付物打包耗时 ≤ 2 分钟验证点压缩包内含 4 个必需文件答辩前务必打包这 4 个文件其他可删Model.pkl训练好的模型date_test.csv测试数据用于现场演示天气网.html前端页面readme.md使用说明老师必看验证点将这 4 个文件放入新文件夹双击天气网.html在无 Flask 服务情况下时间滑块仍能本地运行证明前端独立可用。6. 我踩过的最深的坑date_test.csv的日期顺序必须严格升序否则时间滑块会倒放——从那以后我每次生成测试集都强制加一行df.sort_values(date).to_csv(...)这个坑让我在答辩前夜调试到凌晨两点。现象是拖动时间滑块图表显示的日期从2023-03-01→2023-02-28→2023-02-27… 完全倒序。排查了 JS、Flask、甚至怀疑浏览器缓存最后发现date_test.csv里date列是乱序的——因为爬虫抓取时按网页发布时间排序而非自然日期。根本原因天气网.html的updateChart()函数假设date_test.csv按date升序排列slice(index, index7)才能取到连续 7 天。如果 CSV 本身乱序slice取出的就是随机日期组合。血泪解决方案在ProcessData.py末尾加强制排序# ProcessData.py 末尾追加确保测试集有序 def ensure_date_order(csv_path): df pd.read_csv(csv_path) df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) df[date] df[date].dt.strftime(%Y-%m-%d) # 恢复字符串格式 df.to_csv(csv_path, indexFalse) print(f[INFO] {csv_path} sorted by date) # 在 main.py 训练前调用 ensure_date_order(date_test.csv)但这只是补救。更彻底的做法是在GetData.py爬取完成后就对每批数据按date排序再写入 CSV。我现在的习惯是只要涉及时间序列的 CSV生成后第一件事就是pandas.read_csv().sort_values().to_csv()哪怕多花 0.1 秒也比答辩时面对老师「你这图怎么倒着走」的沉默强。另一个隐形坑是china_today.csv的城市编码。项目用它做城市筛选但文件里「呼和浩特」写成了「呼市」导致GetData.py爬取失败。我的做法是打开china_today.csvCtrlF 搜索所有「市」字把「哈市」「沈市」等简称全替换成「哈尔滨」「沈阳」——城市名必须与date_train.csv中的city列完全一致一个字都不能差。希望帮到你。本文还有配套的精品资源点击获取