☰
新疆历史降水量数据可视化分析:从数据清洗到机器学习建模全链路实战
2026/10/1 5:00:04 网站建设 项目流程

简介:这份资源面向计算机、数据科学相关专业的毕业设计与课程设计场景,围绕新疆1980至2018年历史降水量数据,提供一套完整的大数据可视化分析与机器学习预测方案,适合具备Python基础、需要数据分析或数据挖掘实战案例的学生与开发者参考。压缩包共4个文件,约5.16MB,包含xlsx原始数据集、ipynb分析源码、html结果导出页面以及txt依赖说明,覆盖从数据读取到建模预测的完整链路。内容涵盖数据预处理、降水量时间趋势折线图、年度与月度分布条形图及饼图、雨季旱季对比、监测站均值分析,并基于ARIMA模型完成月度降水量预测与效果验证,可帮助读者理解异常年份识别、季节性规律挖掘与时间序列建模思路。目前已有150人学习,适合作为数据分析类毕业设计的参考模板与排错对照。

1. 新疆历史降水量数据可视化分析:从一份毕业设计标题拆出可复现的数据链路

新疆历史降水量数据可视化分析这个题目,几乎每年都出现在大数据毕业设计的选题池里。它看起来像"画几张图"的活,真正动手才发现,卡人的从来不是可视化,而是数据本身:站点分布稀疏、时间跨度长、缺测值多、量纲不统一,随便一个环节没处理好,后面机器学习模型跑出来的结果就是玄学。这个方向适合三类人:一是要交毕业设计、需要一条完整可演示链路的同学;二是想练手 Python 数据分析与数据挖掘实战、但缺真实长时序气象数据的从业者;三是做校园大数据或商业数据分析看板、想找一个数据量适中又足够"脏"的练手项目的人。整条链路我一般拆成四段:数据获取与清洗、存储与聚合、可视化看板、机器学习建模。下面按这个顺序讲透,每一步都给能直接抄的命令和参数。

2. 数据获取与清洗:把原始降水记录变成能进库的表

2.1 先搞清楚数据长什么样再动手

新疆历史降水数据常见的来源是气象站点观测记录,典型字段包括站号、站名、经纬度、年、月、日、降水量。真正拿到手的第一件事不是写代码,而是用文本工具打开前 200 行,确认三件事:编码是 UTF-8 还是 GBK、缺失值用什么符号表示(常见是 32766、999999、空字符串三种)、降水量单位是毫米还是 0.1 毫米。这三件事决定了后面清洗脚本怎么写,我见过太多人直接pd.read_csv然后发现中文列名乱码,回头返工。

一个稳妥的读取模板如下,注意encoding和na_values两个参数:

import pandas as pd # 新疆站点降水原始数据读取 df = pd.read_csv( "xinjiang_precip_raw.csv", encoding="gbk", # 中文气象数据常见编码,乱码就换 utf-8 na_values=["32766", "999999", "", " "], # 三种常见缺测标记统一转 NaN dtype={"站号": str} # 站号必须按字符串读,否则前导 0 丢失 ) print(df.shape) print(df.isnull().sum()) # 先看每列缺多少,再决定清洗策略 print(df["降水量"].describe()) # 看极值,判断单位是否需要除以 10

逻辑说明:dtype={"站号": str}是最容易被忽略的一行,站号一旦被当成整数,50136这种还好,但带前导零的站号会直接错位,后续按站聚合全乱。na_values把多种缺测标记一次性归一成 NaN,比后面用replace逐个处理干净得多。describe()看最大值,如果出现 3000 以上,基本可以确定单位是 0.1 毫米,需要除以 10。

2.2 缺测值处理:别急着删,先看缺测模式

缺测值处理是这条链路里最容易翻车的地方。新手习惯dropna()一把梭,结果新疆某些站点因为建站晚,早期整段缺失,一删就把整个站删没了。正确做法是先统计每个站的缺测比例,再分情况处理:

# 按站点统计缺测率 missing_rate = df.groupby("站号")["降水量"].apply(lambda s: s.isnull().mean()) print(missing_rate.sort_values(ascending=False).head(10)) # 缺测率低于 5% 的站点,用该站历史同月均值填充 df["月"] = pd.to_datetime(df["日期"]).dt.month df["降水量"] = df.groupby(["站号", "月"])["降水量"].transform( lambda s: s.fillna(s.mean()) ) # 缺测率高于 30% 的站点直接剔除,避免引入偏差 bad_stations = missing_rate[missing_rate > 0.3].index df = df[~df["站号"].isin(bad_stations)]

参数说明:5% 和 30% 这两个阈值不是拍脑袋,5% 以内用均值填充对整体分布影响可忽略,30% 以上填充等于编数据。中间区间(5%~30%)我一般保留 NaN,在建模阶段用模型自带的缺失处理,或者单独标记一个is_missing特征。这一步做完,数据量通常会掉 10%~20%,属于正常范围。

2.3 异常值检测:用 IQR 而不是 3σ

降水数据天然右偏,用 3σ 会把大量真实暴雨当异常值删掉。我一般用 IQR(四分位距)配合业务上限:

Q1 = df["降水量"].quantile(0.25) Q3 = df["降水量"].quantile(0.75) IQR = Q3 - Q1 upper = Q3 + 3 * IQR # 降水用 3 倍 IQR,比常规 1.5 倍宽松 df.loc[df["降水量"] > upper, "降水量"] = upper # 截断而非删除

这里用截断(capping)而不是删除,是因为极端降水本身就是有价值的信息,直接删会丢失信号。3 倍 IQR 是气象数据里比较常用的宽松系数,1.5 倍对降水太激进。

3. 存储与聚合:用 SQL 把日数据滚成月度和年度

3.1 为什么这一步要落到数据库

清洗完的数据如果只在 pandas 里转,做可视化时每次都要重新读 CSV、重新聚合,几百万行数据每次等十几秒,调试体验极差。常见做法是落到 SQLite 或 MySQL,把聚合逻辑固化成视图。SQLite 零配置,适合毕业设计这种单机场景:

import sqlite3 conn = sqlite3.connect("xinjiang_precip.db") df.to_sql("daily_precip", conn, if_exists="replace", index=False) # 建月度聚合视图 conn.execute(""" CREATE VIEW IF NOT EXISTS monthly_precip AS SELECT 站号, substr(日期, 1, 7) AS 年月, SUM(降水量) AS 月降水, COUNT(*) AS 有效天数, AVG(降水量) AS 日均降水 FROM daily_precip GROUP BY 站号, 年月 """) conn.commit()

逻辑说明:substr(日期,1,7)直接截取YYYY-MM,比在 SQL 里做日期函数转换快得多,前提是日期字段格式统一。有效天数这个字段很关键,月降水总量必须配合有效天数看,否则一个只有 10 天记录的月份和一个完整月份放在一起比,结论完全错。

3.2 年度和站点维度聚合

可视化看板通常需要三个粒度:站点、月度、年度。年度聚合直接基于月度视图再滚一层:

CREATE VIEW IF NOT EXISTS yearly_precip AS SELECT 站号, substr(年月, 1, 4) AS 年份, SUM(月降水) AS 年降水, AVG(月降水) AS 月均降水 FROM monthly_precip GROUP BY 站号, 年份;

参数说明:AVG(月降水)而不是SUM(月降水)/12,是因为缺测月份已经被排除,除以 12 会系统性低估。这个细节在答辩时经常被问,值得单独记一笔。

3.3 用 Spark 处理更大数据量的场景

如果数据量上到千万行级别,或者毕业设计要求体现大数据集群部署策略,可以把清洗逻辑搬到 Spark。核心代码结构不变,只是把 pandas 换成 Spark SQL:

from pyspark.sql import SparkSession, functions as F spark = SparkSession.builder.appName("xj_precip").getOrCreate() sdf = spark.read.csv("xinjiang_precip_raw.csv", header=True, inferSchema=True) monthly = (sdf .withColumn("年月", F.date_format("日期", "yyyy-MM")) .groupBy("站号", "年月") .agg(F.sum("降水量").alias("月降水"), F.count("降水量").alias("有效天数"))) monthly.write.mode("overwrite").parquet("monthly_precip.parquet")

注意inferSchema=True在大数据量下会多扫一遍数据,生产环境建议显式指定 schema。Parquet 格式比 CSV 小 3~5 倍,后续可视化读取也快。

4. 可视化看板:从静态图到可交互的降水分布

4.1 空间分布:用 Pyecharts 画站点降水地图

新疆地域辽阔,纯折线图看不出空间规律,必须上地图。Pyecharts 的Geo或Map组件配合经纬度散点最直观:

from pyecharts.charts import Geo from pyecharts import options as opts stations = df.groupby(["站号", "站名", "经度", "纬度"])["降水量"].sum().reset_index() geo = (Geo() .add_schema(maptype="新疆") .add("年降水", [list(z) for z in zip(stations["站名"], stations["降水量"])], type_="heatmap") .set_global_opts( title_opts=opts.TitleOpts(title="新疆各站点年降水分布"), visualmap_opts=opts.VisualMapOpts(max_=800, is_piecewise=True) )) geo.render("precip_map.html")

参数说明:max_=800需要根据实际数据调整,设太小所有点都是深色,设太大全是浅色。is_piecewise=True分段显示比连续渐变更容易读出量级差异。maptype="新疆"依赖 Pyecharts 内置地图包,如果报错说明地图资源没装全,需要单独引入。

4.2 时间序列:月度降水的季节规律

时间维度上,新疆降水集中在夏季,用折线图叠加多年均值能看出趋势:

import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams["font.sans-serif"] = ["SimHei"] # 中文字体,否则方块 matplotlib.rcParams["axes.unicode_minus"] = False monthly_avg = df.groupby("月")["降水量"].mean() plt.figure(figsize=(10, 5)) plt.plot(monthly_avg.index, monthly_avg.values, marker="o", color="#2c7fb8") plt.fill_between(monthly_avg.index, monthly_avg.values, alpha=0.2) plt.xlabel("月份") plt.ylabel("平均降水量 (mm)") plt.title("新疆多年月均降水分布") plt.xticks(range(1, 13)) plt.grid(alpha=0.3) plt.savefig("monthly_trend.png", dpi=150)

逻辑说明:fill_between填充让趋势更直观,alpha=0.2避免遮挡网格线。中文字体那两行是血泪经验,不设的话所有中文标签变成方块,答辩现场很尴尬。

4.3 看板整合:Streamlit 十分钟搭一个可交互页面

静态图交作业够用,但想加分就上 Streamlit,把筛选和联动做出来:

import streamlit as st import plotly.express as px st.title("新疆历史降水量分析看板") year = st.sidebar.selectbox("选择年份", sorted(df["年份"].unique())) filtered = df[df["年份"] == year] fig = px.scatter_mapbox(filtered, lat="纬度", lon="经度", size="降水量", color="降水量", color_continuous_scale="Blues", zoom=4) fig.update_layout(mapbox_style="open-street-map") st.plotly_chart(fig)

参数说明:size="降水量"让点的大小反映降水强度,color_continuous_scale="Blues"用蓝色系符合降水语义。mapbox_style用开源底图,避免依赖需要 token 的服务。

5. 机器学习建模:降水预测到底能做成什么样

5.1 先想清楚预测目标再选模型

降水预测分两类:一是预测某个站点未来某月的降水量(回归),二是预测某天是否降水(分类)。毕业设计里回归更容易讲清楚,但降水数据零值多、分布右偏,直接回归效果通常很差。我一般先做分类(是否降水),再做回归,两个结果互相印证。

特征工程是重点,常用的有:月份、站点经纬度、海拔、前 1/3/6 个月的降水量(滞后特征)、季节编码。滞后特征构造如下:

df = df.sort_values(["站号", "年月"]) for lag in [1, 3, 6]: df[f"lag_{lag}"] = df.groupby("站号")["月降水"].shift(lag) # 季节编码,用 sin/cos 避免 12 月和 1 月被当成距离最远 df["月_sin"] = np.sin(2 * np.pi * df["月"] / 12) df["月_cos"] = np.cos(2 * np.pi * df["月"] / 12)

逻辑说明:shift(lag)必须配合groupby("站号"),否则会把上一个站的数据串到下一个站。季节用 sin/cos 编码是处理周期性特征的标准做法,比 one-hot 更省维度且保留连续性。

5.2 模型选型:从 LightGBM 开始而不是 LSTM

很多人一上来就 LSTM,觉得时序数据必须用深度学习。实际上这种站点数量有限、特征维度不高的表格数据,LightGBM 几乎总是更快更准:

import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error features = ["月_sin", "月_cos", "经度", "纬度", "lag_1", "lag_3", "lag_6"] data = df.dropna(subset=features + ["月降水"]) tscv = TimeSeriesSplit(n_splits=5) for train_idx, test_idx in tscv.split(data): train, test = data.iloc[train_idx], data.iloc[test_idx] model = lgb.LGBMRegressor(n_estimators=300, learning_rate=0.05, num_leaves=31, min_child_samples=20) model.fit(train[features], train["月降水"]) pred = model.predict(test[features]) print("MAE:", mean_absolute_error(test["月降水"], pred))

参数说明:n_estimators=300配合learning_rate=0.05是稳妥起点,num_leaves=31控制复杂度防止过拟合,min_child_samples=20在样本量不大时避免叶子节点过细。用TimeSeriesSplit而不是普通 KFold,是因为时序数据不能随机打乱,否则未来信息泄漏到训练集,指标虚高。

5.3 特征重要性:模型能不能讲出业务逻辑

模型跑完必须看特征重要性,否则答辩时被问"为什么这么预测"答不上来:

importance = pd.DataFrame({ "feature": features, "gain": model.booster_.feature_importance(importance_type="gain") }).sort_values("gain", ascending=False) print(importance)

如果lag_1和lag_3排在最前,说明降水有强自相关,符合气象常识;如果经纬度权重异常高,要检查是不是站点数据泄漏。这一步是验证模型合理性的关键,比单纯看 MAE 更有说服力。

6. 避坑与排查:这条链路上最容易翻车的五个地方

6.1 中文乱码导致列名对不上

现象:KeyError: '降水量',但明明 CSV 里能看到这一列。原因:文件是 GBK 编码,用默认 UTF-8 读进来列名变成乱码。解决:读取时显式指定encoding="gbk",或者先用chardet.detect探测编码再读。

6.2 站号被当成整数导致前导零丢失

现象:按站聚合后站点数量比预期少。原因:pd.read_csv默认把数字列推断为 int,050136变成50136,和另一个站号撞车。解决:读取时用dtype={"站号": str}强制字符串。

6.3 时间序列交叉验证用了随机划分

现象:模型 MAE 低得离谱,换真实数据预测一塌糊涂。原因:用了train_test_split随机划分,未来数据混进训练集。解决:改用TimeSeriesSplit,按时间顺序切分。

6.4 缺测值填充引入未来信息

现象:模型在训练集表现好,测试集崩。原因:用全量数据的均值填充缺测,等于把未来信息泄漏到过去。解决:填充时只用训练集统计量,或者用该站历史同月均值(不含当月)。

6.5 可视化地图底图加载失败

现象:Pyecharts 地图渲染出来是空白。原因:地图资源包没装全,或者maptype名称不对。解决:确认pyecharts版本,必要时单独安装地图资源,maptype用官方支持的名称如"新疆"。

7. 把这条链路跑成可复用的模板

整条链路跑通之后,最有价值的不是某一张图或某一个模型,而是把它固化成可复用的模板。我的习惯是分三层:data/放原始和清洗后数据,scripts/放清洗、聚合、建模三个独立脚本,app/放 Streamlit 看板。每个脚本都能单独运行,用if __name__ == "__main__"包住入口,这样调试时不用每次跑全流程。

验证方法上,我一般做两个检查:一是把清洗后的数据抽样 20 条,人工对照原始记录核对;二是把模型预测结果和该站历史同期均值对比,如果模型还不如均值,说明特征或参数有问题。这两个检查花不了十分钟,但能挡掉大部分低级错误。

一个具体技巧:把常用的清洗和特征工程逻辑封装成函数,参数用配置文件管理。比如缺测阈值、滞后阶数、模型超参都写进config.yaml,换数据集时只改配置不改代码。这个习惯在毕业设计答辩时特别加分,因为老师一看就知道你考虑过复用性。

我自己踩过最深的坑是早期直接用dropna()处理缺测,结果新疆一个建站较晚的站点整段被删,年度趋势图直接少了一条线,答辩前一周才发现。从那以后我养成了一个习惯:任何删除操作之前,先打印删除前后的行数和站点数,确认变化在预期内再往下走。这个习惯帮我省了不止一次后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询