简介:这份资源是一篇本科毕业设计论文,主题为基于机器学习的德国地区气候变化数据挖掘与可视化系统,面向从事气候数据分析、数据挖掘与可视化研究的科研人员及高校师生。论文以德国耶拿地区2009至2016年气象数据为对象,采用Hadoop框架与Python爬虫完成数据采集与清洗,运用随机森林等算法预测天气变化趋势,并借助Echarts实现气温、湿度、降水量等参数的可视化展示。系统功能模块涵盖日照时数、平均相对湿度、年降水量、平均气温、气象分析与气温对比等,后端以MySQL存储数据,采用面向对象方式开发,具备模块化设计与后续扩展能力。资源包内为1个docx文档,压缩包约2.74MB,完整呈现论文摘要、目录与正文结构。目前已有139人学习下载,适合需要参考完整项目方案、算法选型与系统设计思路的读者研读。
1. 从一份本科毕设拆起:德国耶拿气候数据怎么从 CSV 走到可视化大屏
德国耶拿地区 2009 到 2016 年的气象数据,听起来像是个冷门选题,但真正动手做过的人都知道,这类"区域气候 + 机器学习 + 可视化"的组合,恰好踩中了数据工程里最典型的几个坑:数据源格式不统一、时间序列缺失值多、Hadoop 集群配置玄学、前后端联调时字段对不上。这份本科毕业设计把整条链路走通了——Python 爬虫抓取原始气象记录,Hadoop 做分布式存储与预处理,随机森林等算法做趋势预测,最后用 ECharts 把气温、湿度、降水量、日照时数这些指标推到大屏上。它适合谁?正在做类似课题的高校学生、需要快速搭一套气候数据展示原型的数据分析从业者,以及想看看"机器学习 + 可视化"在真实小数据集上怎么落地的人。下面我按自己拆包复现的顺序,把这份资源里真正能用的部分讲清楚。
2. 数据链路拆解:从爬虫到 HDFS 的四个关键环节
2.1 数据获取:Python 爬虫抓什么、怎么存
德国耶拿地区的气象数据来源通常是公开气象站的历史记录,常见做法是用 Python 的requests配合BeautifulSoup或lxml抓取页面表格,再落成 CSV。这份资源里没有绑定特定网站,但给出了字段结构:日期、气温、湿度、降水量、日照时数。我一般会先把抓取逻辑写成可重跑的脚本,避免每次手动补数据。
import requests from bs4 import BeautifulSoup import csv from datetime import datetime # 目标:抓取耶拿地区某气象站 2009-2016 年逐日数据 # 注意:实际 URL 需替换为公开气象数据页面,这里用占位符 BASE_URL = "https://example-weather-site.org/jena/history" def fetch_month(year, month): """抓取指定年月的页面并解析表格""" params = {"year": year, "month": month} resp = requests.get(BASE_URL, params=params, timeout=15) resp.raise_for_status() soup = BeautifulSoup(resp.text, "lxml") rows = [] for tr in soup.select("table.data-table tr")[1:]: # 跳过表头 cols = [td.get_text(strip=True) for td in tr.find_all("td")] if len(cols) < 5: continue rows.append({ "date": cols[0], "temp_c": cols[1], "humidity_pct": cols[2], "precip_mm": cols[3], "sunshine_h": cols[4] }) return rows def save_csv(all_rows, path="jena_climate_raw.csv"): """统一写入 CSV,字段名保持英文,方便后续入库""" with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=["date", "temp_c", "humidity_pct", "precip_mm", "sunshine_h"]) writer.writeheader() writer.writerows(all_rows) if __name__ == "__main__": collected = [] for y in range(2009, 2017): for m in range(1, 13): collected.extend(fetch_month(y, m)) save_csv(collected) print(f"共抓取 {len(collected)} 条记录")这段代码的逻辑很直白:按年月循环请求,解析 HTML 表格,把每行转成字典,最后统一写 CSV。参数上要注意timeout别设太大,否则某个页面卡住会拖死整个循环;encoding="utf-8"是必须的,气象数据里偶尔会有特殊符号。常见翻车点是网站对频繁请求做限制,我一般会在循环里加time.sleep(1),或者把已抓取的月份记录到本地文件,断点续跑。
2.2 数据预处理:缺失值、异常值和标准化
原始气象数据几乎没有一份是干净的。耶拿地区这 8 年的数据里,常见问题包括:某天湿度缺失、降水量出现负值(传感器故障)、气温单位混用摄氏和华氏。这份资源在预处理阶段做了三件事:缺失值填充、异常值剔除、特征标准化。
import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler df = pd.read_csv("jena_climate_raw.csv", parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) # 1. 数值列强制转换,无法转换的置为 NaN for col in ["temp_c", "humidity_pct", "precip_mm", "sunshine_h"]: df[col] = pd.to_numeric(df[col], errors="coerce") # 2. 异常值处理:降水量不能为负,湿度限定 0-100 df.loc[df["precip_mm"] < 0, "precip_mm"] = np.nan df.loc[(df["humidity_pct"] < 0) | (df["humidity_pct"] > 100), "humidity_pct"] = np.nan # 3. 缺失值用前后 7 天滑动中位数填充,比全局均值更合理 for col in ["temp_c", "humidity_pct", "precip_mm", "sunshine_h"]: df[col] = df[col].fillna(df[col].rolling(window=7, min_periods=1, center=True).median()) # 4. 标准化:只对建模用的特征做,日期列保留 feature_cols = ["temp_c", "humidity_pct", "precip_mm", "sunshine_h"] scaler = StandardScaler() df[[f"{c}_scaled" for c in feature_cols]] = scaler.fit_transform(df[feature_cols]) df.to_csv("jena_climate_clean.csv", index=False) print(df.describe())这里的关键决策是缺失值填充策略。用全局均值会把季节性波动抹平,用前后 7 天滑动中位数能保留局部趋势。标准化那一步是为后续随机森林和神经网络准备的,树模型其实不强制要求,但做了也没坏处,统一流程更省心。参数上window=7可以根据数据粒度调整,如果是逐小时数据,窗口可以缩到 24。
2.3 存入 HDFS:为什么小数据集也要走 Hadoop
很多人会问:8 年逐日数据也就几千条,用 pandas 直接读不就行了,为什么要上 Hadoop?这份资源的答案是"教学完整性"——它要演示的是从采集到分布式存储的完整链路。实际做法是把清洗后的 CSV 上传到 HDFS,再用 MapReduce 或 Spark 做聚合统计。
# 启动 HDFS 后,创建气象数据目录 hdfs dfs -mkdir -p /user/climate/jena/raw hdfs dfs -mkdir -p /user/climate/jena/clean # 上传本地清洗后的 CSV hdfs dfs -put jena_climate_clean.csv /user/climate/jena/clean/ # 验证上传结果 hdfs dfs -ls /user/climate/jena/clean/ hdfs dfs -cat /user/climate/jena/clean/jena_climate_clean.csv | head -5Hadoop 集群配置里最容易出问题的是core-site.xml和hdfs-site.xml的端口与路径。常见配置是fs.defaultFS指向hdfs://namenode:9000,dfs.replication在单机伪分布式下设为 1。如果你只是本地复现,用伪分布式模式就够了,没必要真的开三台机器。格式化 NameNode 那一步(hdfs namenode -format)只能执行一次,重复执行会导致集群 ID 不一致,这是血泪经验。
2.4 从 HDFS 回读:用 Spark 做聚合分析
数据进了 HDFS 之后,下一步是用 Spark 或 MapReduce 做按月、按年的聚合统计,为可视化准备汇总表。
from pyspark.sql import SparkSession from pyspark.sql.functions import year, month, avg, sum, col spark = SparkSession.builder \ .appName("JenaClimateAgg") \ .config("spark.sql.warehouse.dir", "/user/climate/warehouse") \ .getOrCreate() # 从 HDFS 读取清洗后的 CSV df = spark.read.csv("hdfs://namenode:9000/user/climate/jena/clean/jena_climate_clean.csv", header=True, inferSchema=True) # 按年月聚合:平均气温、总降水、平均湿度、总日照 monthly = df.withColumn("year", year(col("date"))) \ .withColumn("month", month(col("date"))) \ .groupBy("year", "month") \ .agg(avg("temp_c").alias("avg_temp"), sum("precip_mm").alias("total_precip"), avg("humidity_pct").alias("avg_humidity"), sum("sunshine_h").alias("total_sunshine")) # 写回 HDFS,供后端 API 读取 monthly.write.mode("overwrite").parquet("hdfs://namenode:9000/user/climate/jena/monthly_agg") monthly.show(12)这段 Spark 代码的核心是groupBy("year", "month")加聚合函数。输出格式选 Parquet 而不是 CSV,因为 Parquet 列式存储读取更快,后端 API 查询时只取需要的列。参数上inferSchema=True让 Spark 自动推断类型,但如果日期格式不标准,建议手动指定 schema,否则year(col("date"))可能返回 null。
3. 模型训练与对比:随机森林、贝叶斯网络、神经网络怎么选
3.1 特征工程:把时间序列转成监督学习样本
气候数据是典型的时间序列,要喂给随机森林或神经网络,得先构造特征和标签。这份资源的做法是用"过去 N 天预测下一天"的滑动窗口。
import pandas as pd import numpy as np df = pd.read_csv("jena_climate_clean.csv", parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) WINDOW = 7 # 用过去 7 天预测第 8 天 features = ["temp_c", "humidity_pct", "precip_mm", "sunshine_h"] X, y = [], [] for i in range(WINDOW, len(df)): window_data = df[features].iloc[i-WINDOW:i].values.flatten() X.append(window_data) y.append(df["temp_c"].iloc[i]) # 预测目标:当日气温 X = np.array(X) y = np.array(y) # 按时间顺序切分,不能随机打乱 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] print(f"训练集 {X_train.shape},测试集 {X_test.shape}")这里有个容易翻车的点:时间序列切分绝对不能随机打乱,否则测试集里混入了未来信息,模型评估会虚高。WINDOW=7是个经验值,对应一周的天气周期,你可以试 3、14、30,看哪个在验证集上 MAE 最低。
3.2 随机森林回归:参数怎么调、特征重要性怎么看
随机森林是这份资源的主力模型,原因是它对缺失值和非线性关系容忍度高,调参也不像神经网络那么敏感。
from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score rf = RandomForestRegressor( n_estimators=200, # 树的数量,200 在几千条数据上足够 max_depth=12, # 限制深度防过拟合 min_samples_leaf=3, # 叶子最少样本,避免噪声被单独成叶 random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) print(f"MAE: {mean_absolute_error(y_test, y_pred):.2f} °C") print(f"R2: {r2_score(y_test, y_pred):.3f}") # 特征重要性:看哪些天的哪些指标对预测贡献大 importances = rf.feature_importances_ feat_names = [f"{f}_d{i}" for i in range(WINDOW, 0, -1) for f in features] top_idx = np.argsort(importances)[-10:] for idx in top_idx: print(f"{feat_names[idx]}: {importances[idx]:.4f}")参数上n_estimators=200是精度和速度的平衡点,再往上加收益递减。max_depth=12和min_samples_leaf=3是为了防止每棵树把训练集背下来。特征重要性输出能告诉你:预测今天气温,是昨天的气温最重要,还是前天的湿度更重要。这个信息对后续做可视化很有用,可以在大屏上标注"关键影响因子"。
3.3 贝叶斯网络与神经网络:什么时候值得上
这份资源还对比了贝叶斯网络和神经网络。贝叶斯网络适合做变量间的因果推断,比如"湿度高是否导致降水概率上升",但它的结构学习在几千条数据上容易过拟合。神经网络(MLP 或 LSTM)在数据量足够时能捕捉更复杂的时序模式,但这份资源的数据规模只有几千条,LSTM 的优势发挥不出来,反而训练慢、调参难。
我的建议是:如果只是做课程设计或原型验证,随机森林足够;如果数据量上万条且有 GPU,可以试 LSTM;贝叶斯网络更适合做分析报告里的因果解释,而不是预测主力。
3.4 模型对比表:MAE、R2 和训练耗时
| 模型 | MAE(°C) | R2 | 训练耗时(秒) | 适用场景 |
|---|---|---|---|---|
| 随机森林 | 1.8-2.5 | 0.82-0.88 | 3-8 | 主力预测,调参省心 |
| 决策树 | 2.5-3.2 | 0.72-0.80 | <1 | 快速基线,可解释性强 |
| 贝叶斯网络 | 2.8-3.5 | 0.65-0.75 | 10-30 | 因果分析,非预测 |
| 神经网络(MLP) | 2.0-2.8 | 0.78-0.85 | 15-60 | 数据量大时再考虑 |
表格里的数值是基于类似规模数据集的常见范围,实际结果取决于你的特征工程和切分方式。别盯着绝对数字,看相对排序和稳定性。
4. 可视化大屏与后端接口:ECharts 怎么接真实数据
4.1 后端 API:Flask 从 HDFS 读聚合结果
后端用 Python 的 Flask 或 Django 都行,这份资源用的是轻量级方案。核心是提供一个接口,把 HDFS 里的 Parquet 聚合结果转成 JSON 返回给前端。
from flask import Flask, jsonify import pandas as pd app = Flask(__name__) # 实际项目中从 HDFS 读取,这里用本地 Parquet 模拟 AGG_PATH = "monthly_agg.parquet" @app.route("/api/climate/monthly") def monthly_climate(): df = pd.read_parquet(AGG_PATH) # 转成前端 ECharts 友好的格式 result = { "months": df.apply(lambda r: f"{int(r['year'])}-{int(r['month']):02d}", axis=1).tolist(), "avg_temp": df["avg_temp"].round(2).tolist(), "total_precip": df["total_precip"].round(2).tolist(), "avg_humidity": df["avg_humidity"].round(2).tolist(), "total_sunshine": df["total_sunshine"].round(2).tolist() } return jsonify(result) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)接口设计上,我习惯把时间轴和各个指标分开成数组,前端 ECharts 的xAxis.data和series[].data直接对应,不用再做转换。debug=True只在开发时开,上线要关掉。
4.2 前端 ECharts:气温折线 + 降水柱状的双轴图
大屏上最核心的图是气温和降水量的双轴组合图,一个看趋势,一个看累积。
// 假设已经从 /api/climate/monthly 拿到数据 fetch('/api/climate/monthly') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('climate-chart')); chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: ['平均气温', '总降水量'] }, xAxis: { type: 'category', data: data.months, axisLabel: { rotate: 45 } }, yAxis: [ { type: 'value', name: '气温 (°C)', position: 'left' }, { type: 'value', name: '降水 (mm)', position: 'right' } ], series: [ { name: '平均气温', type: 'line', data: data.avg_temp, smooth: true, yAxisIndex: 0 }, { name: '总降水量', type: 'bar', data: data.total_precip, yAxisIndex: 1, itemStyle: { color: '#5470c6' } } ] }); });双轴图的关键是yAxisIndex的对应关系,左轴给气温,右轴给降水,否则量级差异会让其中一条线贴底。axisLabel.rotate: 45是防止月份标签重叠,数据超过 24 个月时必加。
4.3 大屏布局:栅格与响应式
前端用 Vue + Element UI 的栅格系统,把页面分成顶部指标卡、中间主图、底部明细表三块。响应式靠el-col的:xs、:sm、:md断点控制。大屏分辨率通常是 1920×1080,但也要保证在 1366 宽度的笔记本上不崩。常见做法是给主图容器设min-height: 400px,避免数据加载前高度塌陷。
5. 避坑与排查:这份资源复现时最容易翻车的五个点
5.1 爬虫抓到的日期格式不统一
现象:CSV 里日期列有的写2009-01-01,有的写01/01/2009,pd.to_datetime直接报错或产生 NaT。原因:不同月份页面模板不一致,或者网站改过版。解决:用pd.to_datetime(df["date"], format="mixed", dayfirst=False),或者写自定义解析函数逐个尝试多种格式,解析失败的记录单独存一份人工检查。
5.2 Hadoop 格式化后 DataNode 起不来
现象:start-dfs.sh执行后jps看不到 DataNode 进程,NameNode 日志报 "cluster ID mismatch"。原因:重复执行了hdfs namenode -format,或者dfs.namenode.name.dir和dfs.datanode.data.dir指向了同一个目录。解决:停掉集群,删除 NameNode 和 DataNode 的数据目录,重新格式化一次,确保两个目录分开配置。这个操作会清空 HDFS 数据,格式化前先确认没有重要文件。
5.3 Spark 读 CSV 时数值列全是 null
现象:inferSchema=True之后,temp_c等列显示为 null,聚合结果为空。原因:CSV 里数值列混入了非数字字符,比如"12.5°C"或"N/A"。解决:读取时先用mode="PERMISSIVE"并指定columnNameOfCorruptRecord,或者读进来后用regexp_replace清洗掉单位符号再cast("double")。
5.4 随机森林预测值全部接近均值
现象:模型在测试集上的预测值几乎是一条水平线,MAE 看起来还行但 R2 很低。原因:特征构造时把标签泄漏进了特征,或者WINDOW太大导致每条样本高度相似。解决:检查X的构造逻辑,确保第i条样本的特征只来自i-WINDOW到i-1,标签是i。把WINDOW从 7 降到 3 试试,看 R2 是否回升。
5.5 前端图表数据对不上后端
现象:ECharts 显示的月份数和后端返回的数组长度不一致,图表错位。原因:后端聚合时某些月份没有数据,groupBy后缺失了那几个月,前端按连续月份渲染就错位了。解决:在后端生成一个完整的年月序列,用left join把聚合结果挂上去,缺失月份填 0 或 null,保证数组长度和顺序与前端 xAxis 一致。
6. 进阶技巧:把模型预测结果叠到历史曲线上做验证
这份资源最后的大屏只展示了历史数据的可视化,但既然训练了随机森林,完全可以把预测值也叠上去,直观对比"模型认为应该是什么样"和"实际是什么样"。具体做法是在后端加一个/api/climate/predict接口,接收日期范围,返回模型对那段时间的预测值,前端在同一个 ECharts 实例里加一条虚线系列。
# 后端:加载已训练的模型,对指定区间做预测 import joblib import numpy as np model = joblib.load("rf_climate_model.pkl") scaler = joblib.load("climate_scaler.pkl") @app.route("/api/climate/predict") def predict_range(): # 简化示例:实际需要根据请求参数取对应窗口数据 df = pd.read_csv("jena_climate_clean.csv", parse_dates=["date"]) features = ["temp_c", "humidity_pct", "precip_mm", "sunshine_h"] WINDOW = 7 preds = [] for i in range(WINDOW, len(df)): window = df[features].iloc[i-WINDOW:i].values.flatten().reshape(1, -1) pred = model.predict(window)[0] preds.append(round(float(pred), 2)) return jsonify({"dates": df["date"].iloc[WINDOW:].dt.strftime("%Y-%m-%d").tolist(), "predicted_temp": preds})前端拿到预测值后,在原有折线图上加一条type: 'line'、lineStyle: { type: 'dashed' }的系列,颜色用橙色区分。这样一眼就能看出模型在哪些月份偏差大——通常季节交替的月份误差会偏高,因为气温变化剧烈,滑动窗口的特征跟不上。
验证模型不能只看 MAE 一个数字。我习惯把预测曲线和历史曲线画在一起,再算一个"方向准确率":预测的升降温方向对不对。方向准确率能到 70% 以上,这个模型在实际展示场景里就够用了。如果方向准确率低于 60%,说明特征里缺少关键变量,比如气压或风向,得回去补数据。
从那以后我每次做完时序预测,都强制把预测曲线叠到历史曲线上肉眼过一遍,数字指标会骗人,曲线不会。希望帮到你。
本文还有配套的精品资源,点击获取