ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Python的天气预测与可视化:从数据采集到Flask部署

基于Python的天气预测与可视化:从数据采集到Flask部署 简介这是一份基于Python的天气预测与可视化毕业设计项目主要面向计算机相关专业的学生和有毕业设计、课程设计、期末大作业需求的学习者旨在帮助大家快速掌握从数据采集、模型训练到结果展示的完整流程。资源包共包含38个文件约12.17MB其中Python源文件负责天气数据爬取、模型训练与GUI界面实现Jupyter Notebook提供交互式演示pkl、h5、joblib等文件为训练好的模型csv和json为原始天气数据与城市信息png和jpg是可视化结果图片docx文档则包含详细说明结构清晰便于按模块查阅。项目经过导师指导并获99分高分评价代码完整可运行涵盖了全国及临沧历史天气数据爬取、多种机器学习与深度学习气温预测模型如随机森林、线性回归、决策树、LSTM、MLP等以及天气可视化界面等内容适合作为实战练习和答辩展示项目。目前已有98人浏览学习资料配套详细文档和图表即使是新手也能独立完成部署和复现。1. 为什么“天气预报不准”恰好值得做成Python项目很多人把天气预报当成一句结论准不准全靠体感。真正做天气预测和天气可视化项目时要看的是另一套能力把历史观测数据拉下来清洗成连续时间序列再用模型外推未来48小时最后把预测区间画成交互页面。这套流程天然覆盖Python爬虫、数据处理、机器学习和Web可视化正是一个毕业设计最容易拉开分数的地方。我之前帮人看这类题目时最遗憾的不是模型不够深而是项目停在notebook里没有接口、没有页面、没有可验证的误差数字。这篇文章会从数据源选择、模型基线搭建、Flask可视化一直讲到检验预测效果的具体指标适合想完整交付一个Python项目的人。2. 先解决数据源把天气历史数据采集成时间序列CSV2.1 免费天气API怎么选为什么建议优先考虑免注册接口做天气预测项目第一步不是写模型而是把数据源固定下来。常见做法是申请一个天气API的免费额度用requests按城市坐标拉历史观测值。为什么不直接写Python爬虫抓天气网站天气站点页面结构经常调整今天能用的CSS选择器下周可能失效而且很多页面会在响应里加载动态数据爬下来还要重新对齐字段。相比而言API返回结构化JSON字段名稳定错误信息直接。选型时我一般看三个维度是否提供至少30天逐小时历史数据、接口是否需要复杂鉴权、返回的时间是否可以直接转成本地时间。现在有些开源天气服务甚至不需要注册直接在URL里传坐标和时间范围就能拿到历史数据对毕业设计非常友好。先不管模型把下面这些字段存成CSV后续不管是做传统时序还是深度学习数据格式都是统一的。字段名含义类型用途time观测时间ISO字符串作为时间索引统一转成datetimetemp2米气温float温度预测的目标值humidity相对湿度int温度预测的输入特征clouds云量百分比int辅助判断阴晴变化wind_speed10米风速float辅助特征噪声较大时可选拿到这些字段后还要人为补一个city或坐标字段。理由是预测结果是和地理位置强绑定的同一时刻北京和广州温度差十几度没有坐标信息后续想扩展多城市可视化就要返工。2.2 用requests写一个免注册的历史天气采集脚本下面这段代码以Open-Meteo的Archive接口为例不需要API Key请求参数直接放在URL里。它返回的hourly对象包含时间数组和数值数组遍历时按下标对齐即可。# fetch_weather_history.py import csv from datetime import date, timedelta import requests API_URL https://archive-api.open-meteo.com/v1/archive def fetch_history(lat: float, lon: float, days: int 30) - list[dict]: 获取最近days天的逐小时天气历史数据。 end date.today() - timedelta(days1) start end - timedelta(daysdays - 1) params { latitude: lat, longitude: lon, start_date: start.isoformat(), end_date: end.isoformat(), hourly: temperature_2m,relative_humidity_2m,cloud_cover,wind_speed_10m, timezone: Asia/Shanghai, } resp requests.get(API_URL, paramsparams, timeout15) resp.raise_for_status() payload resp.json()[hourly] rows [] for i, moment in enumerate(payload[time]): rows.append({ time: moment, temp: payload[temperature_2m][i], humidity: payload[relative_humidity_2m][i], clouds: payload[cloud_cover][i], wind_speed: payload[wind_speed_10m][i], }) with open(weather_history.csv, w, newline, encodingutf-8) as f: writer csv.DictWriter( f, fieldnames[time, temp, humidity, clouds, wind_speed] ) writer.writeheader() writer.writerows(rows) print(f保存 {len(rows)} 条记录到 weather_history.csv) return rows if __name__ __main__: fetch_history(lat39.9042, lon116.4074, days30)start_date和end_date采用ISO格式接口要求闭区间所以时间范围是end - timedelta(daysdays - 1)。timezoneAsia/Shanghai能直接返回本地时间字符串省掉手动处理UTC偏移的麻烦。resp.raise_for_status()会在接口返回4xx/5xx时直接抛异常避免程序把错误页面内容当作天气数据写进CSV。每天24条30天就是720条这个数据量对Prophet足够对LSTM偏少后面要靠构造窗口来凑。提示如果网络环境限制导致访问超时可以把timeout15调大并考虑把数据采集脚本拆成“按天请求”的循环单天失败不影响已经写入的数据。2.3 清洗与构造时间特征避免预测“空气温度”拉回来的数据直接进入模型通常有两个问题。第一time是字符串需要统一转成pandas.Timestamp并按时间排序第二接口偶发缺行例如某个小时没有返回temperature_2m训练时出现空值会让模型在缺失位置输出异常预测。import pandas as pd df pd.read_csv(weather_history.csv) df[datetime] pd.to_datetime(df[time]) df df.sort_values(datetime) df.set_index(datetime, inplaceTrue) df df[~df.index.duplicated(keeplast)] # 对短时间缺失做线性插值 for col in [temp, humidity, clouds, wind_speed]: if df[col].isna().any(): df[col] df[col].interpolate(methodlinear) # 补充周期特征 df[hour] df.index.hour df[weekday] df.index.dayofweekinterpolate(methodlinear)只适合连续缺3小时以内的场景如果一段数据连续缺一周建议直接删除否则模型会学到一条向相邻日期“平均”的假曲线。hour和weekday这两个周期特征是天气时序里很关键的信息凌晨和午后温度差异很大加入后预测曲线不会在24小时周期里平得像一条直线。特征准备好之后就可以进入建模环节。3. 天气预测模型先用基线再上Prophet或LSTM3.1 用滞后温度做基线一百行内得到MAE很多第一次做天气预测的开发者会直接跳到LSTM结果发现训练曲线一直不收敛最后反而觉得题目太难。我建议先做一个“昨天同一小时温度等于今天温度”的持久性基线。做法是把temp下移24行再和当前温度计算平均绝对误差。这个基线值就是后面所有复杂模型需要打败的线如果复杂模型只比它好一点点那真正该改进的是数据时间跨度而不是继续堆模型。import pandas as pd from sklearn.metrics import mean_absolute_error df[temp_lag24] df[temp].shift(24) # 前一天的同一小时 df df.dropna() mae_baseline mean_absolute_error(df[temp], df[temp_lag24]) print(fbaseline MAE: {mae_baseline:.2f} °C)这里有几个会让MAE变差的坑需要提前知道。第一数据是逐小时时直接用昨天的同一小时预测今天很准因为气温有很强的连续性但要在答辩里展示“未来48小时预测”这个基线会在第二段预测窗口后快速失效。第二不要对湿度用同样逻辑湿度在凌晨和午后波动剧烈昨天同一时刻的参考价值比气温弱。第三报告里同时列MAE和RMSERMSE对偶尔出现的8度大偏差更敏感能反映尾部风险。3.2 Prophet适合天气序列的最短实现Prophet对时间序列中的季节性和缺失值处理很省心通常零调参就能给出一条合理的预测线。它的接口面向两列ds是时间y是目标值。以下代码在刚才清洗好的DataFrame上外推48小时from prophet import Prophet prophet_df df.reset_index()[[datetime, temp]].rename( columns{datetime: ds, temp: y} ) model Prophet( yearly_seasonalityFalse, # 30天数据里看不出年周期关掉减少过拟合 daily_seasonalityTrue, # 保留日内周期因为温度有强日波动 weekly_seasonalityFalse, changepoint_prior_scale0.05 ) model.fit(prophet_df) future model.make_future_dataframe(periods48, freqH) forecast model.predict(future) forecast[[ds, yhat, yhat_lower, yhat_upper]].to_csv(forecast.csv, indexFalse) print(forecast[[ds, yhat, yhat_lower, yhat_upper]].tail(10))changepoint_prior_scale是Prophet里最值得调的参数控制趋势转折点的弹性。设置越大模型越容易捕捉突然的冷空气过程但也越容易把随机波动当成趋势我一般用0.02到0.1之间做一轮搜索。make_future_dataframe(periods48, freqH)表示预测未来48小时如果上一步数据是日粒度freq必须改成D否则时间轴会错位。yhat_lower和yhat_upper是模型给出的置信区间这个区间在后端可视化时是重点因为你可以对用户说“明天下午两点温度有70%概率落在18到23度之间”而不是只丢一个点估计。3.3 LSTM如何替换以及两个不稳定因素如果导师希望你展示神经网络常见做法是把连续6小时的特征切成滑动窗口用前6小时预测下1小时温度。下面这段Keras代码需要先确认Python环境里装好了tensorflow样本量小的时候训练时间很短但效果不稳定。import numpy as np from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.layers import Dense, LSTM from tensorflow.keras.models import Sequential seq_len 6 values df[[temp, humidity, clouds]].values.astype(float) scaler MinMaxScaler() values_scaled scaler.fit_transform(values) x, y [], [] for i in range(seq_len, len(values_scaled)): x.append(values_scaled[i - seq_len:i]) y.append(values_scaled[i, 0]) # 预测目标仍是温度 x np.array(x) y np.array(y) model Sequential([ LSTM(32, input_shape(x.shape[1], x.shape[2])), Dense(1) ]) model.compile(optimizeradam, lossmse) model.fit(x, y, epochs50, batch_size8, validation_split0.2, verbose1)这段代码能跑通但有两个地方经常让新手卡住。第一MinMaxScaler必须只用训练集拟合不能在完整序列上拟合否则验证集信息会泄漏进训练流程测试误差虚低。正确顺序是先切分训练集和测试集再scaler.fit(train)再分别transform。第二y取的是values_scaled[i, 0]表示“第i时刻的温度”这是单步预测。如果要做未来48小时需要把上一步预测结果重新拼进输入窗口形成自回归滚动预测但误差会随步长累积所以论文里要明确区分“单步预测”和“多步预测”两个指标。LSTM在小数据量上往往不稳定超过Prophet这是正常现象不用迷信神经网络。4. 天气可视化落地用Flask和Pyecharts把预测结果发布成网页4.1 为什么用Flask而不是直接在Jupyter里画图天气预测的可视化与普通数据图表不同用户要看的不只是“静态折线图”而是“切换城市、查看未来48小时、对比历史实测值”这些交互。Jupyter里用Matplotlib画折线很快但没法让非技术用户直接打开查看纯前端图表库又需要单独处理跨域和JSON解析。最常见的一体化方案是Flask作为后端服务Pyecharts作为图表生成器把图表HTML片段嵌进Jinja2模板。这样所有代码仍是Python部署时一个python app.py就能起服务。4.2 在Flask里注册页面路由和JSON接口我的建议是让模型训练和Web服务分离train.py负责读取历史数据、训练Prophet并把forecast.csv写盘app.py只读取forecast.csv并提供页面和接口。这样页面每次刷新不会触发一次新的模型训练服务端压力小很多。# app.py import pandas as pd from flask import Flask, jsonify, render_template from pyecharts import options as opts from pyecharts.charts import Line app Flask(__name__) def get_forecast() - pd.DataFrame: # 读取上一章保存的预测结果避免每次请求重新训练 fc pd.read_csv(forecast.csv, parse_dates[ds]) fc fc.tail(48).reset_index(dropTrue) # 只保留未来48小时 fc[ds] fc[ds].dt.strftime(%m-%d %H:%M) # 格式化时间轴 fc[[yhat, yhat_lower, yhat_upper]] fc[ [yhat, yhat_lower, yhat_upper]].fillna(0) return fc app.route(/) def index(): forecast get_forecast() line ( Line() .add_xaxis(forecast[ds].tolist()) .add_yaxis( 预测温度, forecast[yhat].round(2).tolist(), is_smoothTrue, ) .add_yaxis( 置信上界, forecast[yhat_upper].round(2).tolist(), is_smoothTrue, ) .add_yaxis( 置信下界, forecast[yhat_lower].round(2).tolist(), is_smoothTrue, ) .set_global_opts( title_optsopts.TitleOpts(title未来48小时温度预测), datazoom_opts[opts.DataZoomOpts()], ) ) return render_template(index.html, chartline.render_embed()) app.route(/api/forecast) def api_forecast(): forecast get_forecast() return jsonify( forecast[[ds, yhat, yhat_lower, yhat_upper]].to_dict( orientrecords ) ) if __name__ __main__: app.run(debugTrue, port5000)render_embed()会把图表所需的JavaScript直接写进HTML片段模板里不需要额外配置静态资源路径。get_forecast()里把时间列转成字符串是为了避免JSON序列化时遇到Timestamp类型报错。datazoom_opts给图表加上底部缩放条当预测跨度从48小时扩展到7天时用户能在小屏上拖拽查看。to_dict(orientrecords)会把DataFrame变成数组嵌套字典的前端友好格式。对应的templates/index.html只需要很短的一页!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title天气预测可视化/title /head body h1天气预测可视化/h1 div{{ chart|safe }}/div /body /html4.3 可视化图表里值得先调好的参数Pyecharts里不同参数对出图影响差别很大下面这几组是我在天气可视化里最常用的参数作用推荐初始值is_smooth折线是否平滑True让曲线更像天气趋势datazoom_opts底部缩放条给出48小时以上跨度时建议开启set_global_opts全局标题、坐标轴标签axislabel_optsopts.LabelOpts(rotate30)AreaStyleOpts区间带填充opacity0.2用半透明带表示置信区间rotate30这个参数专门解决“python画图横坐标太密集”的问题。时间点一多横轴标签会叠成一片黑旋转30度之后可读性立刻改善。这里有个更深的坑用三条普通折线展示yhat_upper和yhat_lower在两条线交叉处会造成视觉误导所以真正做预测区间展示时应该用下一章说的面积堆叠方式而不是简单把两个边界折线画出来。4.4 两个高频报错JSON序列化失败和图表不刷新Flask Pyecharts最常见的两个报错都和数据格式有关。第一个是TypeError: Object of type Timestamp is not JSON serializable解决办法就是把所有时间列先转成字符串。第二个是前端图表不变明明重新训练了模型页面却还是旧数据。这通常不是Python代码问题而是浏览器缓存了render_embed()生成的HTML。调试时可以在Flask响应头上加Cache-Control: no-store或者在路由函数前用app.after_request统一禁用缓存。开发阶段也可以直接按住CtrlF5强制刷新。5. 验证预测效果的三个数字和让天气可视化项目像产品的三个技巧5.1 MAE、RMSE、MAPE三组数字怎么算、怎么看模型做出来后不要只print一次预测结果要写一个回测函数。把最后7天数据切出来当测试集前面所有天做训练再让模型输出这7天的预测值然后计算指标from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(test[temp], test[pred]) rmse mean_squared_error(test[temp], test[pred], squaredFalse) mape (abs(test[temp] - test[pred]) / test[temp].abs()).mean() * 100 print({mae: round(mae, 2), rmse: round(rmse, 2), mape: f{mape:.2f}%})温度接近0度时MAPE会爆炸因为分母接近0所以不能只盯这一个指标。更推荐的做法是计算“平均绝对误差除以训练集标准差”这个比值在1以下说明模型比直观猜测有效超过1则说明复杂模型没有带来增益。天气数据必须按时间顺序切分不能用随机洗牌否则模型会偷看未来得到的指标在真实场景里完全不成立。5.2 用定时任务刷新预测而不是每次请求都训练一旦项目从本地脚本变成在线页面就不该让每个访客的请求都触发模型训练。常见做法是拆成两个脚本train.py每天凌晨读取最新历史数据重新训练并生成forecast.csvapp.py只负责读取CSV并提供服务。定时任务可以用系统crontab也可以在Python里用schedule库凌晨2点跑一次即可。这样服务端CPU占用会从“一次请求几秒”降到“一次请求几十毫秒”页面即使同时被多个人打开也不会卡住。5.3 把预测区间画成半透明色带而不是堆三条折线前面提到上界和下界用普通折线画会出现交叉视觉上也像三条曲线在互相打架。我常用的做法是让两个系列使用同一个stack一条画置信区间的基线另一条画上界与下界的差值叠加出来的面积就是置信区间带fc get_forecast() lower fc[yhat_lower].round(2).tolist() band (fc[yhat_upper] - fc[yhat_lower]).clip(lower0).round(2).tolist() line.add_yaxis( 置信区间基带, lower, stackband, is_smoothTrue, areastyle_optsopts.AreaStyleOpts(opacity0.0) ) line.add_yaxis( 置信区间, band, stackband, is_smoothTrue, areastyle_optsopts.AreaStyleOpts(opacity0.2) )stackband是关键两个系列在纵向叠加后视觉边界正好是yhat_upper半透明填充区域就是模型认为最可能的温度范围。再叠加一条yhat折线作为点预测图上信息就不只有一条单调曲线而是“区间趋势”的组合。把最后这个页面跑在app.run(host0.0.0.0)上同一局域网里的同学用手机就能打开你的可视化大屏这比把截图放进PPT更接近一个真实可交付的Python项目。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进