简介:本资源是一套完整的Python课程设计项目源码,面向计算机专业本科生及数据可视化初学者,聚焦天气数据的采集、建模与可视化分析全流程实践。项目基于机器学习实现天气预测,并集成多维度图表展示(如温度趋势、降水分布、风速热力图等),适合作为数据可视化分析期末大作业或课程设计参考方案。压缩包共24个文件,含4个核心Python脚本(main.py、GetData.py、ProcessData.py等)、4个CSV数据集(train/test/valid及中国今日天气)、12张可视化结果JPG图、1个HTML交互页面、1个训练好的Model.pkl模型文件及README说明文档,整体仅1.42MB,轻量易部署。已有1487人学习下载,提供从原始数据获取、特征工程、模型训练到结果可视化的完整闭环代码,结构清晰、注释充分,且经导师指导获97分高分,具备较强教学示范性与工程复用价值。
1. 这不是“画个折线图就交差”的天气项目:一个能跑通真实API、自动清洗异常值、按城市/时段分层聚合、并支持交互式导出的Python可视化分析系统
你手头可能正压着一份名为weather_analysis_system.zip的压缩包,解压后看到main.py、config.yaml和requirements.txt——但直接python main.py却报错ModuleNotFoundError: No module named 'plotly',或者数据拉下来全是None或-999。这不是代码写错了,而是天气数据可视化分析系统天然具备的三重门槛:数据源不可靠性(API限频、字段缺失、单位混杂)、时空维度复杂性(经纬度网格 vs 行政区划、UTC时间戳 vs 本地时区、逐小时 vs 日均值)、分析目标模糊性(是看某市未来7天温度趋势?还是对比长三角16城过去30年降水极值?)。本系统不是教学Demo,它默认对接中国气象数据网(CMA)公开接口或OpenWeatherMap国际源,内置空值插补策略(时间序列线性填充+空间邻近加权)、单位统一引擎(℃/℉/K 自动识别与转换)、以及基于Pandas MultiIndex的多维切片器——这意味着你改一行配置就能从“北京2023年逐日最高温”切换到“华东地区2020–2024年梅雨期降水量箱线图”。适合需要交付可复用分析模块的数据工程师、气象方向研究生,以及要快速搭建部门级天气看板的运维/产品团队。
2. 为什么选Requests + Pandas + Plotly组合:避开Matplotlib硬编码陷阱,用声明式语法定义时空分析逻辑
2.1 天气数据的特殊结构决定技术栈必须分层解耦
天气数据不是标准CSV表格:原始响应常为嵌套JSON(如OpenWeatherMap返回{"city":{"name":"Beijing","coord":{"lat":39.9,"lon":116.4}},"data":[{"dt":1712345678,"main":{"temp":12.5,"humidity":65},"weather":[{"main":"Clear"}]}]}),其中data数组含数百条时间序列,每条含main、weather、wind等子对象。若用纯Matplotlib逐点绘制,需手动展开三层嵌套、处理时间戳转换、拼接城市名字段——代码易错且无法复用。而Pandas DataFrame天然支持json_normalize()扁平化嵌套结构,并通过set_index(['city', 'dt'])构建双索引,使df.loc[('Shanghai', slice('2024-01-01', '2024-01-07')), 'temp']一句即可切片。Plotly则提供px.line(df, x='dt', y='temp', color='city')声明式绘图,无需管理Figure对象生命周期。这种组合让分析逻辑聚焦在“我要什么数据”,而非“怎么把数据塞进图表”。
提示:不要用
matplotlib.pyplot.plot()硬编码坐标轴——当需求变为“叠加历史同期均值虚线”时,Matplotlib需重写ax.plot()和ax.axhline(),而Plotly只需追加fig.add_hline(y=df['temp'].mean(), line_dash="dot")。
2.2 实现最小可运行天气抓取与解析:5行代码验证API连通性
import requests import pandas as pd from datetime import datetime # 替换为你的API Key(OpenWeatherMap免费版限60次/分钟) API_KEY = "your_api_key_here" CITY_ID = "1816670" # 上海City ID url = f"http://api.openweathermap.org/data/2.5/forecast?id={CITY_ID}&appid={API_KEY}&units=metric" response = requests.get(url) data = response.json() # 验证关键字段存在性(避免后续KeyError) assert "list" in data, "API响应缺少list字段,请检查API Key或网络" assert len(data["list"]) > 0, "未获取到预报数据,请检查城市ID" # 提取核心字段并转为DataFrame df = pd.json_normalize( data["list"], sep="_" ).rename(columns={ "main_temp": "temp_c", "main_humidity": "humidity_pct", "wind_speed": "wind_mps", "dt": "timestamp_utc" }) print(f"成功获取{len(df)}条预报记录,时间范围:{datetime.fromtimestamp(df['timestamp_utc'].min())} ~ {datetime.fromtimestamp(df['timestamp_utc'].max())}")这段代码输出应类似:成功获取40条预报记录,时间范围:2024-04-05 12:00:00 ~ 2024-04-09 09:00:00
关键点在于pd.json_normalize()自动展开list数组,sep="_"将main.temp转为main_temp列名,避免后续访问data['main']['temp']引发的嵌套键错误。若报错requests.exceptions.ConnectionError,说明网络或API域名问题;若data["list"]为空,需确认CITY_ID是否有效(可通过https://openweathermap.org/find 搜索城市获取ID)。
2.3 数据清洗必须前置:用Pandas链式操作处理常见天气数据脏点
天气API返回的脏数据有三类典型模式:
- 温度异常值:
temp_c出现-273.15(绝对零度)或1000(传感器故障) - 湿度越界:
humidity_pct超出0–100范围 - 时间重复:同一
timestamp_utc出现多条记录(API缓存或重试导致)
以下清洗逻辑封装为可复用函数:
def clean_weather_data(df: pd.DataFrame) -> pd.DataFrame: """清洗天气DataFrame,返回无异常值、时间去重、单位标准化的版本""" # 步骤1:过滤明显异常温度(-50℃ ~ 60℃为合理区间) df = df[(df["temp_c"] >= -50) & (df["temp_c"] <= 60)].copy() # 步骤2:修正湿度越界值(截断至0-100) df["humidity_pct"] = df["humidity_pct"].clip(lower=0, upper=100) # 步骤3:去重时间戳(保留第一条,因后续预报数据通常按时间排序) df = df.drop_duplicates(subset=["timestamp_utc"], keep="first") # 步骤4:添加本地时间列(上海UTC+8) df["timestamp_local"] = pd.to_datetime(df["timestamp_utc"], unit="s").dt.tz_localize("UTC").dt.tz_convert("Asia/Shanghai") # 步骤5:添加日期和小时字段,便于分组 df["date"] = df["timestamp_local"].dt.date df["hour"] = df["timestamp_local"].dt.hour return df # 应用清洗 clean_df = clean_weather_data(df) print(f"清洗后剩余{len(clean_df)}条记录,温度范围:{clean_df['temp_c'].min():.1f}℃ ~ {clean_df['temp_c'].max():.1f}℃")参数说明:
clip(lower=0, upper=100)比np.where()更高效,且自动处理NaNtz_localize("UTC").tz_convert("Asia/Shanghai")确保时区转换正确,避免+8硬编码导致夏令时错误drop_duplicates(keep="first")针对OpenWeatherMap偶发的重复响应,若需保留最新值则设keep="last"
执行后输出应显示清洗前后记录数对比,例如清洗后剩余38条记录,证明脏数据已被剔除。
3. 构建可配置的时空分析管道:从单城市趋势到跨区域对比的4种核心视图
3.1 基于YAML的配置驱动分析逻辑:修改config.yaml即可切换分析维度
系统通过config.yaml定义分析行为,避免硬编码城市名或时间范围。典型配置如下:
# config.yaml data_source: api: "openweathermap" # 支持 openweathermap / cma(中国气象数据网) api_key: "your_api_key_here" cities: - name: "Beijing" id: "1816670" - name: "Shanghai" id: "1816670" - name: "Guangzhou" id: "1816670" time_range: start_date: "2024-04-01" end_date: "2024-04-07" interval: "hourly" # hourly / daily analysis: aggregation: "mean" # mean / max / min / sum(对daily适用) metrics: ["temp_c", "humidity_pct", "wind_mps"] export: format: "html" # html / png / json output_dir: "./output"加载配置的Python代码:
import yaml def load_config(config_path: str = "config.yaml") -> dict: """加载YAML配置,返回字典""" with open(config_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) config = load_config() print(f"配置加载成功:分析{len(config['cities'])}个城市,时间范围{config['time_range']['start_date']}至{config['time_range']['end_date']}")此设计使非开发人员(如气象分析师)仅需编辑YAML即可调整分析范围,无需触碰Python代码。若新增城市,只需在cities列表中添加name和id;若需改为日均值分析,将interval改为daily并确保aggregation设为mean。
3.2 实现城市温度趋势对比图:Plotly动态图例与悬停提示
使用清洗后的数据生成多城市温度对比图,关键在于px.line()的color参数绑定城市名:
import plotly.express as px # 假设clean_df已包含多个城市的记录,且有'city_name'列标识来源 fig = px.line( clean_df, x="timestamp_local", y="temp_c", color="city_name", # 自动为每个城市生成不同颜色曲线 title=f"{config['cities'][0]['name']}等{len(config['cities'])}城温度趋势对比", labels={"timestamp_local": "时间(本地)", "temp_c": "温度(℃)"}, markers=True, # 显示数据点 line_shape="spline" # 平滑曲线,更符合气象变化特征 ) # 增强交互:悬停显示完整时间戳和湿度 fig.update_traces( hovertemplate="<b>%{x}</b><br>温度: %{y:.1f}℃<br>湿度: %{customdata[0]:.0f}%<extra></extra>", customdata=clean_df[["humidity_pct"]] ) # 导出为HTML(支持缩放、下载PNG) fig.write_html(f"{config['export']['output_dir']}/temperature_comparison.html") print(f"温度对比图已保存至 {config['export']['output_dir']}/temperature_comparison.html")注意:
hovertemplate中%{x}自动格式化时间为本地时区字符串,%{customdata[0]}引用传入的湿度列,<extra></extra>隐藏默认的trace名。若发现图例文字重叠,可在fig.update_layout(legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1))中调整。
3.3 构建区域降水热力图:用Plotly Heatmap展示时空分布密度
当分析目标转向“某区域降雨强度时空分布”,需将时间序列转为二维矩阵(行=日期,列=小时,值=降水量)。OpenWeatherMap不直接提供降水,但可通过weather.main字段统计“Rain”出现频次:
# 统计每小时“Rain”出现次数(模拟降水强度) rain_count = clean_df.groupby(["date", "hour"]).apply( lambda x: (x["weather_0_main"] == "Rain").sum() ).unstack(fill_value=0) # 转为date×hour矩阵 # 生成热力图 fig_heat = px.imshow( rain_count, labels={"x": "小时", "y": "日期", "color": "Rain次数"}, title="区域降雨频次热力图(按小时统计)", aspect="auto" ) fig_heat.write_html(f"{config['export']['output_dir']}/rain_heatmap.html")此处unstack(fill_value=0)确保缺失小时(如某日无0点数据)填充为0,避免热力图出现空白列。aspect="auto"防止长宽比失真——当日期跨度大时,自动拉伸高度以保证可读性。
3.4 生成城市气候箱线图:用Plotly Box比较多年温度离散度
若需评估城市间气候差异,箱线图比折线图更直观。假设已有2020–2024年每日最高温数据(daily_max_temp.csv):
# 加载多年日最高温数据(示例结构:date, city, max_temp_c) daily_data = pd.read_csv("data/daily_max_temp.csv", parse_dates=["date"]) daily_data["year"] = daily_data["date"].dt.year # 按城市和年份分组,计算箱线图所需统计量 box_data = daily_data.groupby(["city", "year"])["max_temp_c"].agg( ["min", "q1", "median", "q3", "max", "count"] ).reset_index() # Plotly箱线图(需将数据转为长格式) fig_box = px.box( daily_data, x="city", y="max_temp_c", color="city", points="outliers", # 显示离群值 title="各城市历年日最高温分布(箱线图)" ) fig_box.update_yaxes(title_text="日最高温(℃)") fig_box.write_html(f"{config['export']['output_dir']}/city_climate_box.html")关键点:points="outliers"保留极端高温/低温点,便于识别异常年份(如2022年长江流域极端高温);color="city"自动匹配图例颜色,与趋势图保持视觉一致性。
4. 解决高频落地问题:时区错乱、API限频、中文标签乱码的3个硬核修复方案
4.1 时区转换失效?用pytz显式绑定时区而非简单+8
常见错误:pd.to_datetime(df["timestamp_utc"], unit="s") + pd.Timedelta(hours=8)
问题:未考虑夏令时(如欧洲城市),且+8在跨年时可能出错。正确做法:
from pytz import timezone def convert_to_local_time(timestamp_series: pd.Series, tz_name: str = "Asia/Shanghai") -> pd.Series: """安全转换UTC时间戳为指定时区本地时间""" utc_tz = timezone("UTC") local_tz = timezone(tz_name) return pd.to_datetime(timestamp_series, unit="s", utc=True).dt.tz_convert(local_tz) # 应用 clean_df["timestamp_local"] = convert_to_local_time(clean_df["timestamp_utc"])pd.to_datetime(..., utc=True)强制将时间戳解释为UTC,dt.tz_convert()调用pytz数据库处理夏令时规则。测试方法:取timestamp_utc=1609459200(2021-01-01 00:00:00 UTC),转换后应为2021-01-01 08:00:00 CST(非+08:00,因CST是标准时间缩写)。
4.2 API请求被限频?用tenacity实现指数退避重试
OpenWeatherMap免费版限60次/分钟,连续请求易触发429 Too Many Requests。tenacity库提供优雅重试:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import time @retry( stop=stop_after_attempt(3), # 最多重试3次 wait=wait_exponential(multiplier=1, min=2, max=10), # 初始等待2s,指数增长至10s retry=retry_if_exception_type(requests.exceptions.RequestException) ) def fetch_weather_data(city_id: str) -> dict: url = f"http://api.openweathermap.org/data/2.5/forecast?id={city_id}&appid={API_KEY}&units=metric" response = requests.get(url, timeout=10) response.raise_for_status() # 抛出HTTP异常 return response.json() # 使用 try: data = fetch_weather_data("1816670") except Exception as e: print(f"API请求失败,已重试3次仍失败:{e}")wait_exponential确保第二次重试等待2*2=4s,第三次4*2=8s,避免雪崩式请求。timeout=10防止网络卡死阻塞主线程。
4.3 中文图表标题乱码?强制指定Noto Sans CJK字体
Plotly默认字体不支持中文,导致标题显示方块。解决方案:
import plotly.io as pio # 设置全局字体(需系统安装Noto Sans CJK,Linux/macOS通常预装,Windows需手动下载) pio.templates["custom"] = pio.templates["plotly"] pio.templates["custom"].layout.font.family = "Noto Sans CJK SC, sans-serif" pio.templates["custom"].layout.title.font.size = 18 # 应用模板 fig = px.line(clean_df, x="timestamp_local", y="temp_c", title="上海温度趋势图") fig.update_layout(template="custom") # 强制使用中文字体 fig.write_html("shanghai_temp.html")验证方法:打开生成的HTML,检查标题是否正常显示“上海温度趋势图”。若仍乱码,需确认系统字体路径——Linux执行fc-list :lang=zh,macOS检查/System/Library/Fonts/,Windows下载 Noto Sans CJK 并安装。
5. 进阶技巧:用Plotly Dash构建可交互的天气分析仪表盘,支持实时筛选与导出
5.1 构建基础Dash应用框架:3个核心组件定义交互逻辑
Dash将Plotly图表封装为Web组件,用户可通过下拉框、滑块实时筛选数据。最小可行仪表盘代码:
import dash from dash import dcc, html, Input, Output, callback import plotly.express as px # 初始化Dash应用 app = dash.Dash(__name__) # 布局:包含城市选择下拉框、日期范围滑块、图表容器 app.layout = html.Div([ html.H1("天气数据分析仪表盘"), html.Div([ html.Label("选择城市:"), dcc.Dropdown( id="city-dropdown", options=[{"label": c["name"], "value": c["name"]} for c in config["cities"]], value=config["cities"][0]["name"] ), html.Label("选择日期范围:"), dcc.DatePickerRange( id="date-range", start_date=config["time_range"]["start_date"], end_date=config["time_range"]["end_date"] ) ]), dcc.Graph(id="weather-graph") ]) # 回调函数:当用户操作控件时更新图表 @callback( Output("weather-graph", "figure"), Input("city-dropdown", "value"), Input("date-range", "start_date"), Input("date-range", "end_date") ) def update_graph(selected_city, start_date, end_date): # 筛选对应城市和日期的数据 filtered_df = clean_df[ (clean_df["city_name"] == selected_city) & (clean_df["date"] >= pd.to_datetime(start_date).date()) & (clean_df["date"] <= pd.to_datetime(end_date).date()) ] # 生成温度折线图 fig = px.line( filtered_df, x="timestamp_local", y="temp_c", title=f"{selected_city}温度趋势({start_date} 至 {end_date})" ) fig.update_layout(template="custom") # 复用中文字体 return fig # 启动服务(调试模式) if __name__ == "__main__": app.run_server(debug=True, host="0.0.0.0", port=8050)启动后访问http://localhost:8050,即可看到带下拉框和日期选择器的网页界面。@callback装饰器将UI控件与图表逻辑绑定,Input监听用户操作,Output更新图表——这是Dash的核心范式。
5.2 添加一键导出功能:用dcc.Download组件实现前端触发文件下载
用户常需将当前视图导出为PNG或CSV。Dash提供dcc.Download组件:
# 在app.layout中添加下载按钮和Download组件 app.layout = html.Div([ # ... 原有布局 ... html.Button("导出当前图表为PNG", id="btn-png"), html.Button("导出数据为CSV", id="btn-csv"), dcc.Download(id="download-data") ]) # 定义导出回调 @callback( Output("download-data", "data"), Input("btn-png", "n_clicks"), Input("btn-csv", "n_clicks"), prevent_initial_call=True ) def download_file(png_clicks, csv_clicks): # 判断哪个按钮被点击 ctx = dash.callback_context if not ctx.triggered: return None button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id == "btn-png": # 截图当前图表(需前端js支持,此处简化为占位) return dict(content="PNG截图功能需集成browser-shot库", filename="weather_chart.png") elif button_id == "btn-csv": # 导出筛选后的数据 filtered_df = clean_df.head(100) # 实际应复用回调中的筛选逻辑 return dcc.send_data_frame(filtered_df.to_csv, "weather_data.csv")dcc.send_data_frame()自动生成CSV并触发浏览器下载,prevent_initial_call=True避免页面加载时自动下载。生产环境需集成browser-shot实现真正的图表截图。
5.3 性能优化:用Redis缓存API响应,降低重复请求开销
当多人同时访问仪表盘,频繁调用API会快速耗尽配额。用Redis缓存响应:
import redis import json # 初始化Redis连接(需提前启动redis-server) cache = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True) def get_cached_weather(city_id: str) -> dict: """从Redis获取缓存天气数据,未命中则请求API并缓存""" cache_key = f"weather:{city_id}" cached = cache.get(cache_key) if cached: print(f"从Redis缓存获取{city_id}") return json.loads(cached) # 缓存未命中,请求API data = fetch_weather_data(city_id) # 复用前述tenacity重试函数 cache.setex(cache_key, 3600, json.dumps(data)) # 缓存1小时 print(f"API请求{city_id}并写入Redis") return data # 在Dash回调中使用 @callback(Output("weather-graph", "figure"), Input("city-dropdown", "value")) def update_graph_with_cache(selected_city): # 根据城市名映射到ID(需维护city_name→city_id映射表) city_map = {c["name"]: c["id"] for c in config["cities"]} data = get_cached_weather(city_map[selected_city]) # ... 后续解析与绘图逻辑cache.setex()设置过期时间(3600秒),避免缓存永久占用内存。首次访问某城市时触发API请求,后续1小时内相同请求直接从Redis读取,响应时间从秒级降至毫秒级。
| Redis缓存键 | TTL(秒) | 用途 |
|---|---|---|
weather:1816670 | 3600 | 单城市预报数据,1小时更新一次 |
config:latest | 86400 | YAML配置文件内容,1天更新一次 |
agg:beijing_daily_2024 | 1800 | 北京日均值聚合结果,30分钟更新 |
缓存策略依据数据时效性设定:预报数据1小时足够,配置文件可缓存1天,而实时监控类数据TTL应设为60秒以内。
本文还有配套的精品资源,点击获取