简介:本资源是一份面向计算机、电子信息工程及数学等专业本科生的机器学习综合实践项目,聚焦重庆地区天气与空气质量数据的采集、分析与建模全流程,适用于课程设计、期末大作业及毕业设计等教学场景。压缩包共6个文件(3个xlsx数据表用于存储爬取的天气、空气质量及合并后的结构化数据;3个Python脚本分别实现网页爬虫抓取、数据质量分析与基础预测功能),整体大小仅369KB,轻量易部署。已有367人下载学习,代码采用参数化设计,关键变量如城市名、时间范围、API接口均封装为可配置项,配合详尽中文注释与完整运行结果截图,确保开箱即用;所有模块均经实测验证通过,逻辑清晰、调试友好,特别适合初学机器学习与网络爬虫的学生快速掌握从数据获取到分析建模的闭环能力。
1. 项目概述:从数据采集到智能分析的完整闭环
最近在带学生做机器学习相关的课程设计,发现一个非常普遍的现象:很多同学拿到“数据分析”或“预测模型”这类题目后,第一反应就是去Kaggle或者UCI找现成的数据集。这当然没问题,但对于理解一个真实机器学习项目的全生命周期来说,缺失了至关重要的一环——数据从哪里来?一个只做“数据清洗-建模-评估”的项目,就像只学了炒菜却不会买菜和备料,总感觉少了点根基。
所以,当有同学提出想做一个结合重庆本地特色的天气分析项目时,我立刻觉得这是个绝佳的机会。这个项目麻雀虽小,五脏俱全:它要求你从一个具体的、动态的目标(重庆天气)出发,亲自动手解决数据获取(爬虫)、数据整理、探索性分析,并最终运用机器学习方法去发现规律或做出预测。整个过程,恰好覆盖了从数据生产到数据消费的完整链条。重庆作为一个典型的山地城市,其天气系统复杂,多雾、夏季高温高湿、冬季阴冷,这些特点都为数据分析提供了丰富的素材。你不仅能练手技术,还能对身边的环境有更数据化的认识。
这个项目非常适合有一定Python基础,正在学习数据分析或机器学习入门的朋友。无论你是学生想完成一份出色的课程作业,还是职场新人想构建自己的第一个数据作品集,跟着这个思路走一遍,你会对“数据驱动”有更实在的体会。接下来,我就把这个项目的完整实现路径、技术选型的考量、实操中踩过的坑以及一些进阶思考,毫无保留地分享出来。
2. 核心需求解析与方案设计
2.1 需求拆解:我们要的不仅仅是一份数据
乍看标题“重庆天气爬虫-质量分析”,需求似乎很直白:爬点天气数据,然后分析一下。但深究下去,我们需要明确几个层次的目标:
- 数据获取的可持续性与合规性:我们需要的是一个能稳定、持续获取重庆地区历史与实时天气数据的方案。数据源必须可靠、公开,且爬取行为不能对目标服务器造成压力,必须遵守
robots.txt协议。这意味着我们不能简单粗暴地无限循环请求。 - 数据字段的完整性与业务贴合度:天气数据包含哪些维度?除了常规的温度、湿度、风力、天气现象,对于重庆这样的城市,空气质量(AQI)、能见度、体感温度、气压等字段可能对后续分析(如舒适度研究、雾霾分析)至关重要。我们需要根据分析目标,反推需要爬取哪些字段。
- 数据质量的评估与清洗:爬下来的数据直接就是干净的吗?几乎不可能。缺失值、异常值(比如湿度超过100%)、格式不一致(天气现象描述的中英文混杂)等问题普遍存在。因此,“质量分析”的第一步,其实是对数据本身质量进行评估和清洗。
- 从描述到预测的分析进阶:基础分析可以包括历史趋势、季节性规律、各气象要素的相关性等。但机器学习作业的要求更高,我们需要设定明确的建模目标,例如:基于前几天的天气数据预测明天的最高温度,或者根据气象要素预测空气质量等级。这决定了我们后续特征工程和模型选型的方向。
基于以上拆解,我设计的方案核心思路是:构建一个模块化、可配置的爬虫系统,获取结构化、带时间戳的天气数据,存入本地数据库或文件,然后利用Pandas进行数据质量探查与清洗,最后使用Scikit-learn库构建预测模型,并完成模型评估与可视化。
2.2 技术栈选型与理由
为什么选这些工具?每个选择背后都有具体的考量:
爬虫核心:Requests + BeautifulSoup4 / PyQuery
Requests:HTTP库的“事实标准”,简单易用,功能强大。对于大多数静态天气网站,它完全够用。BeautifulSoup4或PyQuery:HTML解析库。BeautifulSoup更普及,文档丰富;PyQuery的语法类似jQuery,对于前端熟悉的同学更友好。本项目选择BeautifulSoup4,适用性更广。- 为什么不直接用Scrapy?Scrapy是强大的框架,适合构建大型、复杂的爬虫项目。但对于我们这个目标单一、页面结构相对固定的天气爬虫,用
Requests+BeautifulSoup组合更轻量、更直接,学习曲线平缓,更能让初学者聚焦于数据提取逻辑本身。
数据存储:SQLite / CSV
SQLite:轻量级数据库,无需安装服务器,单个文件即可管理,非常适合本地项目。它能方便地按日期查询、去重,是比纯CSV更规范的选择。CSV:作为备份或中间格式,便于用Excel快速查看和Pandas读取。最佳实践是:爬虫数据先入SQLite保证结构化和可管理,分析时导出或连接所需数据到Pandas。
数据分析与机器学习:Pandas + NumPy + Scikit-learn + Matplotlib/Seaborn
Pandas:数据操作的基石,数据清洗、转换、聚合离不开它。NumPy:底层数值计算支持。Scikit-learn:提供了几乎所有经典的机器学习算法(回归、分类、聚类)和完整的模型工具链(训练、评估、调参)。Matplotlib+Seaborn:可视化黄金搭档。Matplotlib定制性强,Seaborn统计图表美观,默认样式好看。
调度与容错(进阶):Schedule / APScheduler
- 如果想实现每日定时自动爬取,可以引入轻量级的定时任务库,如
schedule。更复杂的可以用APScheduler。 - 关键点:必须加入异常处理(
try...except)和日志记录(logging模块),记录爬取成功、失败的情况,便于后期维护和排查。
- 如果想实现每日定时自动爬取,可以引入轻量级的定时任务库,如
注意:伦理与合规红线:在编写爬虫前,务必仔细查看目标网站的
robots.txt文件(通常在网站根目录,如https://目标网站/robots.txt),尊重其中关于爬取频率和禁止爬取目录的规定。在代码中,通过设置requests.get()的headers参数,模拟真实浏览器访问,并在请求间添加随机延时(如time.sleep(random.uniform(1, 3))),避免高频请求对服务器造成干扰,这是每个数据采集者应遵守的基本准则。
3. 爬虫核心实现:稳定获取重庆天气数据
3.1 目标网站分析与字段确定
国内有许多天气数据来源,例如中国天气网、心知天气(API)、和风天气(API)等。考虑到项目的练习性质和可访问性,我们选择一个结构清晰的公开天气历史数据查询网站作为示例(请注意:实际项目中请务必确认该网站允许爬取,并遵守其服务条款)。
假设我们目标页面能按城市和日期查询历史天气。通过浏览器开发者工具(F12)查看网络请求,我们发现数据可能是通过后端接口返回的JSON,也可能是直接渲染在HTML中。这里我们以更常见的HTML直接渲染为例进行解析。
我们需要爬取的典型字段包括:
date: 日期 (主键)high_temp: 最高气温 (℃)low_temp: 最低气温 (℃)weather: 天气现象 (如:晴、多云、阴、小雨)wind_direction: 风向wind_force: 风力等级aqi: 空气质量指数 (可选,如果页面提供)aqi_level: 空气质量等级 (可选)
3.2 爬虫代码模块化构建
我将爬虫构建为几个函数,提高代码可读性和可维护性。
import requests from bs4 import BeautifulSoup import pandas as pd import sqlite3 import time import random import logging from datetime import datetime, timedelta # 配置日志,方便追踪运行状态和错误 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s') class ChongqingWeatherSpider: def __init__(self, base_url, start_date, end_date): """ 初始化爬虫 :param base_url: 基础URL,通常包含城市和日期占位符 :param start_date: 开始日期,字符串格式 'YYYY-MM-DD' :param end_date: 结束日期,字符串格式 'YYYY-MM-DD' """ self.base_url = base_url self.start_date = datetime.strptime(start_date, '%Y-%m-%d') self.end_date = datetime.strptime(end_date, '%Y-%m-%d') self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } self.data_list = [] # 临时存储爬取的数据 def _parse_date_range(self): """生成需要爬取的日期列表""" date_list = [] current_date = self.start_date while current_date <= self.end_date: date_list.append(current_date.strftime('%Y-%m-%d')) current_date += timedelta(days=1) return date_list def _fetch_html(self, url): """请求网页,返回BeautifulSoup对象""" try: # 添加随机延时,模拟人工操作 time.sleep(random.uniform(1, 2)) resp = requests.get(url, headers=self.headers, timeout=10) resp.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 resp.encoding = resp.apparent_encoding # 自动识别编码 return BeautifulSoup(resp.text, 'html.parser') except requests.RequestException as e: logging.error(f"请求失败 {url}: {e}") return None def _parse_single_day(self, soup, query_date): """ 解析单日天气数据,这是核心函数,需要根据目标网站HTML结构具体调整 这里是一个示例解析逻辑,你需要用浏览器开发者工具定位实际元素 """ day_data = {'date': query_date} try: # 示例:假设数据在一个class为‘weather-info’的div里 # 你需要根据实际网站结构修改这些选择器 weather_div = soup.find('div', class_='weather-info') if not weather_div: logging.warning(f"{query_date} 页面结构可能已变化,未找到关键元素") return None # 解析最高温和最低温 (示例:文本可能是“高温 32℃ 低温 25℃”) temp_text = weather_div.find('p', class_='temp').text # 这里需要编写具体的文本提取和清洗逻辑,例如用正则表达式 # 假设提取出数字 import re temps = re.findall(r'\d+', temp_text) if len(temps) >= 2: day_data['high_temp'] = int(temps[0]) day_data['low_temp'] = int(temps[1]) # 解析天气现象 weather_span = weather_div.find('span', class_='weather') if weather_span: day_data['weather'] = weather_span.text.strip() # 解析风力风向 wind_div = weather_div.find('div', class_='wind') if wind_div: parts = wind_div.text.split() if len(parts) >= 2: day_data['wind_direction'] = parts[0] day_data['wind_force'] = parts[1] # 检查必要字段是否齐全 if all(k in day_data for k in ['high_temp', 'low_temp', 'weather']): logging.info(f"成功解析 {query_date} 数据") return day_data else: logging.warning(f"{query_date} 数据解析不完整: {day_data}") return None except Exception as e: logging.error(f"解析 {query_date} 数据时发生异常: {e}") return None def run(self): """主运行函数""" dates_to_crawl = self._parse_date_range() logging.info(f"开始爬取 {self.start_date.date()} 至 {self.end_date.date()} 的天气数据,共{len(dates_to_crawl)}天") for single_date in dates_to_crawl: # 构造具体日期的URL,例如 base_url 可能是 "http://example.com/weather/chongqing-{date}" target_url = self.base_url.format(date=single_date) logging.info(f"正在处理 {single_date}, URL: {target_url}") soup = self._fetch_html(target_url) if not soup: continue day_data = self._parse_single_day(soup, single_date) if day_data: self.data_list.append(day_data) logging.info(f"爬取结束,共获取到 {len(self.data_list)} 条有效数据") return self.data_list def save_to_sqlite(self, db_path='weather_data.db'): """将数据保存到SQLite数据库""" if not self.data_list: logging.warning("没有数据可保存") return conn = sqlite3.connect(db_path) df = pd.DataFrame(self.data_list) # 如果表不存在则创建,如果存在则替换(可根据需要改为追加‘append’) df.to_sql('chongqing_weather', conn, if_exists='replace', index=False) conn.close() logging.info(f"数据已保存至数据库: {db_path}") def save_to_csv(self, csv_path='weather_data.csv'): """将数据保存到CSV文件""" if not self.data_list: logging.warning("没有数据可保存") return df = pd.DataFrame(self.data_list) df.to_csv(csv_path, index=False, encoding='utf-8-sig') # utf-8-sig 解决Excel打开中文乱码 logging.info(f"数据已保存至CSV文件: {csv_path}") # 使用示例 if __name__ == '__main__': # !!!请务必替换为真实、合规的目标URL格式!!! BASE_URL_TEMPLATE = "https://tianqi.example.com/chongqing/{date}.html" spider = ChongqingWeatherSpider( base_url=BASE_URL_TEMPLATE, start_date='2023-01-01', end_date='2023-01-07' # 初始测试时范围小一点 ) data = spider.run() spider.save_to_csv('chongqing_weather_2023_sample.csv') spider.save_to_sqlite()关键点解析:
- 类封装:使用类
ChongqingWeatherSpider将数据和方法封装在一起,结构清晰,便于管理状态(如data_list)。 - 健壮性设计:
_fetch_html方法包含了异常处理(try...except)和状态码检查(raise_for_status)。_parse_single_day方法内部有try...except,防止因某一天页面结构异常导致整个程序崩溃。- 添加了详细的日志(
logging),运行情况一目了然。
- 遵守爬虫礼仪:在
_fetch_html中加入了随机延时time.sleep(random.uniform(1, 2)),并设置了合理的User-Agent。 - 数据持久化:提供了保存到
SQLite数据库和CSV文件两种方式,SQLite便于后续复杂查询,CSV便于交换和快速查看。
3.3 反爬策略应对与实战技巧
在实际操作中,你可能会遇到一些简单的反爬措施:
- 请求头(Headers)缺失:像上面代码中设置
User-Agent是最基本的。有时还需要添加Referer、Accept-Language等字段。用浏览器开发者工具查看一次正常请求的Headers并复制,是最高效的方法。 - IP限制:如果频繁请求,可能会被临时封禁IP。对于个人小规模爬取,降低请求频率是最有效的方法(如将延时增加到3-5秒)。本项目数据量不大,此方法足够。大规模爬取需要考虑代理IP池,但这超出了课程作业范围,且涉及更高风险与成本。
- 动态加载数据:如果数据是通过JavaScript异步加载的(在“网络”选项卡中看到XHR或Fetch请求),那么
Requests获取的HTML就不包含数据。这时需要分析这些异步请求的接口,直接模拟请求接口URL(通常返回JSON),这往往比解析HTML更简单。可以使用浏览器开发者工具的“网络”面板进行抓包分析。 - 页面结构变更:这是爬虫失效最常见的原因。所以,核心解析函数
_parse_single_day中的选择器(如find(‘div’, class_=‘weather-info’))非常脆弱。一个好的习惯是将这些选择器用的class或id名定义为类常量,一旦网站改版,只需修改一处。更好的做法是使用相对更稳定的定位方式,比如通过标签层级关系,而不是完全依赖易变的class名。
实操心得:不要试图一次性写出完美的、能应对所有情况的爬虫。应该采用“小步快跑”的策略:先针对一个具体的日期页面,在Python交互环境(如Jupyter Notebook)里,用
Requests和BeautifulSoup手动调试解析代码,成功提取出该页面的所有目标字段。确认逻辑无误后,再将其封装成函数,并加入循环和异常处理。这样能极大降低调试复杂度。
4. 数据质量分析与清洗实战
拿到原始数据后,切忌直接开始建模。脏数据会导致错误的结论。我们需要先用Pandas进行彻底的数据质量探查(EDA, Exploratory Data Analysis)。
4.1 数据加载与初步探查
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 加载数据 df = pd.read_csv('chongqing_weather_2023_sample.csv') # 或从SQLite加载 # import sqlite3 # conn = sqlite3.connect('weather_data.db') # df = pd.read_sql_query("SELECT * FROM chongqing_weather", conn) print("数据形状(行,列):", df.shape) print("\n前5行数据:") print(df.head()) print("\n数据基本信息:") print(df.info()) print("\n数值型字段描述性统计:") print(df.describe())运行df.info()会立刻告诉我们是否有缺失值(Non-Null Count)。df.describe()可以查看数值字段的分布(均值、标准差、最小值、最大值),快速发现异常,比如最高温度出现100℃这种明显错误。
4.2 数据清洗的具体操作
清洗通常是一个迭代的过程,以下是一些常见操作:
# 1. 处理日期字段 df['date'] = pd.to_datetime(df['date']) # 转换为datetime类型 df['year'] = df['date'].dt.year df['month'] = df['date'].dt.month df['day'] = df['date'].dt.day df['day_of_week'] = df['date'].dt.dayofweek # 周一=0,周日=6 # 2. 处理缺失值 print("缺失值统计:") print(df.isnull().sum()) # 根据情况处理,例如: # - 对于数值列(如温度),可以用前后天的平均值填充,或该月的历史平均值填充 # - 对于类别列(如天气现象),可以用众数填充,或标记为‘未知’ df['high_temp'].fillna(df['high_temp'].interpolate(), inplace=True) # 线性插值 df['weather'].fillna(df['weather'].mode()[0], inplace=True) # 用出现最多的天气填充 # 3. 处理异常值 # 基于业务逻辑判断,例如重庆最高气温通常不会超过45℃,最低不会低于-5℃ temp_upper_limit = 45 temp_lower_limit = -5 # 将超出范围的温度视为异常,可以用边界值替换或设为NaN df.loc[df['high_temp'] > temp_upper_limit, 'high_temp'] = temp_upper_limit df.loc[df['low_temp'] < temp_lower_limit, 'low_temp'] = temp_lower_limit # 或者使用统计学方法,如3σ原则(假设数据近似正态分布) # mean, std = df['high_temp'].mean(), df['high_temp'].std() # df = df[(df['high_temp'] > mean - 3*std) & (df['high_temp'] < mean + 3*std)] # 4. 数据格式标准化 # 例如,天气现象字段可能有‘晴’,‘晴天’,‘Sunny’,需要统一 weather_mapping = {'晴天': '晴', 'Sunny': '晴', '多云': '多云', 'Cloudy': '多云', '小雨': '小雨'} df['weather'] = df['weather'].map(weather_mapping).fillna(df['weather']) # 映射已知的,未知的保留原值 # 风力字段可能包含‘3-4级’,需要提取数字 df['wind_force_num'] = df['wind_force'].str.extract(r'(\d+)').astype(float) # 5. 创建衍生特征(特征工程初步) df['temp_range'] = df['high_temp'] - df['low_temp'] # 日温差 # 将天气现象转换为类别编码或独热编码(为建模准备) df['weather_encoded'] = pd.Categorical(df['weather']).codes # 或者使用独热编码(如果类别不多) # weather_dummies = pd.get_dummies(df['weather'], prefix='weather') # df = pd.concat([df, weather_dummies], axis=1) print("\n清洗后的数据信息:") print(df.info()) print(df[['date', 'high_temp', 'low_temp', 'weather', 'temp_range']].head())4.3 可视化探查数据质量与分布
可视化能帮助我们直观发现问题。
# 设置绘图风格 sns.set_style("whitegrid") # 1. 检查温度随时间的变化趋势(同时查看缺失和异常) plt.figure(figsize=(15, 5)) plt.subplot(1, 2, 1) plt.plot(df['date'], df['high_temp'], label='High Temp', marker='o', markersize=2, linewidth=0.5) plt.plot(df['date'], df['low_temp'], label='Low Temp', marker='s', markersize=2, linewidth=0.5) plt.xlabel('Date') plt.ylabel('Temperature (°C)') plt.title('Daily Temperature Trend (Raw)') plt.legend() plt.xticks(rotation=45) # 2. 查看最高温度的分布直方图 plt.subplot(1, 2, 2) sns.histplot(df['high_temp'], kde=True, bins=20) plt.xlabel('High Temperature (°C)') plt.ylabel('Frequency') plt.title('Distribution of High Temperature') plt.tight_layout() plt.show() # 3. 检查缺失值分布(使用热图) plt.figure(figsize=(10, 4)) sns.heatmap(df.isnull(), cbar=False, cmap='viridis', yticklabels=False) plt.title('Missing Values Heatmap') plt.show() # 4. 查看天气现象的频次 plt.figure(figsize=(8, 5)) weather_counts = df['weather'].value_counts() sns.barplot(x=weather_counts.index, y=weather_counts.values) plt.xlabel('Weather Phenomenon') plt.ylabel('Count') plt.title('Frequency of Weather Types') plt.xticks(rotation=45) plt.show()通过以上步骤,我们不仅清洗了数据,还对数据的分布、质量和特征有了深入的了解,为后续的机器学习建模打下了坚实的基础。
5. 基于机器学习的天气分析与预测
数据准备就绪后,我们就可以进入核心的机器学习环节。这里我们设定一个具体的预测任务:利用前N天的天气数据,预测未来第M天的最高温度。这是一个经典的时序回归问题。
5.1 问题定义与特征工程
我们预测的是未来某一天(t+1天)的最高温度。因此,特征(X)可以选取过去几天(t, t-1, t-2, ...)的各类天气数据,标签(y)就是t+1天的最高温度。
# 假设我们想用过去3天的数据预测下一天的最高温 look_back = 3 forecast_ahead = 1 features = [] labels = [] # 确保数据按日期排序 df_sorted = df.sort_values('date').reset_index(drop=True) for i in range(look_back, len(df_sorted) - forecast_ahead): # 提取过去look_back天的特征 past_features = [] for j in range(look_back): idx = i - look_back + j # 选择你认为有用的特征,这里以温度和天气编码为例 row = df_sorted.iloc[idx] past_features.extend([ row['high_temp'], row['low_temp'], row['temp_range'], row['weather_encoded'], # 使用编码后的天气 row['wind_force_num'] if not pd.isna(row['wind_force_num']) else 0 ]) features.append(past_features) # 标签是未来forecast_ahead天的最高温 labels.append(df_sorted.iloc[i + forecast_ahead]['high_temp']) X = np.array(features) y = np.array(labels) print(f"特征矩阵 X 的形状: {X.shape}") # (样本数, look_back * 每天特征数) print(f"标签向量 y 的形状: {y.shape}")特征工程思考:
- 时序特征:我们手动构造了滞后特征(lag features),这是处理时序预测问题的基本方法。
- 领域特征:除了原始数据,我们加入了计算出的
temp_range(日温差),这可能是一个重要特征。 - 周期性特征:还可以加入
month,day_of_week的sin/cos编码,以捕捉季节性和周周期性。 - 天气类别处理:天气现象是分类变量,我们使用了简单的标签编码(
Categorical().codes)。对于线性模型,更好的做法是使用独热编码(One-Hot Encoding),但对于树模型(如随机森林),标签编码通常也可以。
5.2 模型选择、训练与评估
我们尝试几种经典的回归模型,并比较其性能。
from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression, Ridge from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score # 1. 划分训练集和测试集(注意:时序数据不能随机打乱!) # 我们按时间顺序划分,前80%作为训练,后20%作为测试 split_idx = int(len(X) * 0.8) X_train, X_test = X[:split_idx], X[split_idx:] y_train, y_test = y[:split_idx], y[split_idx:] print(f"训练集大小: {X_train.shape}, 测试集大小: {X_test.shape}") # 2. 特征标准化(对线性模型很重要,对树模型非必须但无害) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 3. 初始化模型 models = { '线性回归': LinearRegression(), '岭回归 (Ridge)': Ridge(alpha=1.0), # 加入L2正则化防止过拟合 '随机森林': RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1) } # 4. 训练、预测并评估 results = {} for name, model in models.items(): model.fit(X_train_scaled, y_train) y_pred = model.predict(X_test_scaled) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) r2 = r2_score(y_test, y_pred) results[name] = {'MAE': mae, 'RMSE': rmse, 'R2': r2} print(f"\n{name}:") print(f" 平均绝对误差 (MAE): {mae:.2f}°C") print(f" 均方根误差 (RMSE): {rmse:.2f}°C") print(f" 决定系数 (R²): {r2:.4f}") # 5. 可视化预测结果 vs 真实值 best_model_name = min(results, key=lambda x: results[x]['MAE']) # 选择MAE最小的模型 best_model = models[best_model_name] best_model.fit(X_train_scaled, y_train) y_pred_best = best_model.predict(X_test_scaled) plt.figure(figsize=(12, 6)) plt.plot(range(len(y_test)), y_test, label='True Temperature', marker='o', linewidth=1) plt.plot(range(len(y_test)), y_pred_best, label=f'Predicted ({best_model_name})', marker='x', linewidth=1, alpha=0.7) plt.xlabel('Test Sample Index') plt.ylabel('High Temperature (°C)') plt.title(f'Weather Temperature Prediction - {best_model_name}') plt.legend() plt.grid(True, linestyle='--', alpha=0.5) plt.show() # 6. 分析特征重要性(对于随机森林) if hasattr(best_model, 'feature_importances_'): importances = best_model.feature_importances_ feature_names = [f'Lag{i+1}_Feature{j+1}' for i in range(look_back) for j in range(5)] # 根据实际特征数调整 feat_imp_df = pd.DataFrame({'feature': feature_names, 'importance': importances}) feat_imp_df = feat_imp_df.sort_values('importance', ascending=False) plt.figure(figsize=(10, 6)) sns.barplot(x='importance', y='feature', data=feat_imp_df.head(15)) # 看最重要的15个 plt.title('Top 15 Feature Importances (Random Forest)') plt.tight_layout() plt.show()模型选择解读:
- 线性回归 / 岭回归:作为基线模型,简单、可解释性强。如果特征与目标存在近似线性关系,它们可能表现不错。岭回归通过正则化能缓解过拟合。
- 随机森林:集成学习模型,能捕捉非线性关系,对异常值不敏感,通常能取得比线性模型更好的性能,也提供了特征重要性评估。
- 为什么不用更复杂的LSTM?对于单变量或多变量时序预测,循环神经网络(如LSTM)确实是强大工具。但对于入门级项目,且数据量可能不大的情况下,传统机器学习模型更容易训练、调参和解释,作为起点更合适。你完全可以在后续进阶中尝试LSTM。
5.3 模型优化思路与调参
如果对初步结果不满意,可以从以下几个方面优化:
- 特征工程:
- 尝试不同的
look_back(回顾天数)。 - 加入更多衍生特征:前几天的平均温度、温度变化率、滑动窗口统计量(均值、标准差)。
- 更精细地处理天气现象,比如将“小雨转多云”拆分为两个特征。
- 加入节假日、季节(sin/cos编码)等外部特征。
- 尝试不同的
- 模型调参:
- 对于随机森林,可以调整
n_estimators(树的数量)、max_depth(树的最大深度)、min_samples_split(内部节点再划分所需最小样本数)等。 - 使用
GridSearchCV或RandomizedSearchCV进行自动化超参数搜索。
- 对于随机森林,可以调整
- 尝试其他模型:梯度提升树(如XGBoost, LightGBM)通常在表格数据上表现优异。也可以尝试简单的神经网络(MLP)。
- 交叉验证:使用时间序列交叉验证(TimeSeriesSplit)来更可靠地评估模型性能,避免信息泄露。
6. 项目总结与扩展思考
走完从爬虫到建模的整个流程,你会发现一个完整的机器学习项目远不止调包和跑算法。数据获取的艰辛、数据清洗的繁琐,往往占据了项目大部分时间,但也正是这些工作决定了模型效果的上限。
我个人在带学生做这类项目时,最深的体会是:一定要先明确分析或预测的目标,然后根据目标去反推需要什么样的数据,再设计爬虫方案。很多同学本末倒置,先爬一堆数据,再想能做什么,结果往往发现数据不符合需求。例如,如果你的目标是分析“雾霾与气象条件的关系”,那么爬虫时就必须确保包含AQI和PM2.5等关键字段。
关于项目扩展,这里有几个方向供你参考:
- 分析维度深化:不局限于温度预测。可以分析重庆的“雾日”与湿度、风速、气压的关系;可以研究夏季高温热浪的持续时间和强度变化;可以做空气质量(AQI)的预测。
- 爬虫系统化:将爬虫脚本部署到云服务器,使用
APScheduler或Celery实现每日定时自动爬取、数据入库、异常报警,构建一个微型的数据管道。 - 可视化仪表盘:使用
Plotly Dash或Streamlit快速搭建一个交互式Web仪表盘,展示重庆天气的历史趋势、实时状态和模型预测结果,让项目成果更直观。 - 模型服务化:将训练好的最佳模型用
Flask或FastAPI包装成一个简单的REST API,提供“根据过去几天天气预测明日高温”的在线服务。
最后,务必整理好你的源代码、文档说明和数据集。文档说明(README.md)至少应包括:项目简介、环境依赖(requirements.txt)、如何运行爬虫、如何运行分析脚本、关键结果展示。一个结构清晰、文档齐全的项目仓库,是你能力最好的证明。这个“重庆天气分析”项目,完全可以成为你简历或作品集中一个亮眼的、体现全栈数据技能的实际案例。
本文还有配套的精品资源,点击获取