Python大数据实战(八):SVM回归+极区图——气象数据分析与可视化全流程实战
文章目录
前言
“靠海的城市是不是更凉快?”
每到夏天,这个问题总会浮现在脑海。住在内陆城市的人羡慕海边的凉爽,而海边的人却说"也没差多少"。那么,海洋到底对气候有多大影响?这个看似感性的问题,其实可以用数据科学给出一个理性的答案。
本文将以意大利波河流域的 10 个城市为研究对象,使用 Python 对气象数据(温度、湿度、风向、风速)进行全面的分析与可视化。我们将使用 matplotlib 绘制多种图表,使用 scikit-learn 的 SVR(支持向量回归) 对温度与离海距离的关系进行建模,并使用 极区图(玫瑰图) 展示风向分布。全文超过 6000 字,包含完整可运行代码和详细踩坑记录。
一、项目全景概览
1.1 项目目标
| 阶段 | 任务 | 技术栈 |
|---|---|---|
| 数据加载 | 读取 10 个城市的气象 CSV 数据 | Pandas |
| 温度分析 | 分析气温日变化趋势、最高/最低温与离海距离关系 | Matplotlib |
| 回归建模 | 使用 SVR 拟合温度-距离关系,找到海洋影响分界点 | Scikit-learn SVR |
| 湿度分析 | 分析近海/内陆城市湿度差异 | Matplotlib |
| 风向分析 | 绘制风向频率玫瑰图(极区图) | Matplotlib + NumPy |
| 风速分析 | 计算各方向平均风速并可视化 | 自定义函数 |
1.2 技术路线架构图
1.3 前置知识要求
| 知识领域 | 具体要求 | 重要程度 |
|---|---|---|
| Python基础 | 列表推导式、函数定义、NumPy数组操作 | ⭐⭐⭐⭐⭐ |
| Pandas | DataFrame读取、列筛选、统计函数(max/min/mean) | ⭐⭐⭐⭐ |
| Matplotlib | subplot、plot、极坐标绘图 | ⭐⭐⭐⭐ |
| Scikit-learn | SVR回归器的基本使用 | ⭐⭐⭐ |
| 数学基础 | 线性回归、直线方程、交点求解 | ⭐⭐⭐ |
二、研究背景与数据准备
2.1 研究问题定义
我们要回答的核心问题是:海洋对一个地区的气候(温度、湿度)有多大影响?影响范围是多少公里?
为了控制变量,我们选择了意大利波河流域——这是一片从亚得里亚海向内陆延伸数百公里的平原,排除了山地海拔因素的干扰。
2.2 样本城市选择
选取 10 个城市作为研究样本,按离海距离分为两组:
| 分组 | 城市 | 离海距离 |
|---|---|---|
| 近海组(<100km) | Ravenna(拉文纳) | 最近 |
| Cesena(切塞纳) | 近 | |
| Faenza(法恩莎) | 近 | |
| Ferrara(费拉拉) | 近 | |
| Bologna(博洛尼亚) | 近 | |
| 内陆组(100-400km) | Mantova(曼托瓦) | 远 |
| Piacenza(皮亚琴察) | 远 | |
| Milano(米兰) | 远 | |
| Asti(阿斯蒂) | 远 | |
| Torino(都灵) | 最远 |
2.3 数据加载
每个城市的气象数据以 CSV 格式存储,包含日期时间、温度、湿度、风速、风向等字段。
import numpy as np
import pandas as pd
import datetime
# 加载10个城市的气象数据
df_ferrara = pd.read_csv('WeatherData/ferrara_270615.csv')
df_milano = pd.read_csv('WeatherData/milano_270615.csv')
df_mantova = pd.read_csv('WeatherData/mantova_270615.csv')
df_ravenna = pd.read_csv('WeatherData/ravenna_270615.csv')
df_torino = pd.read_csv('WeatherData/torino_270615.csv')
df_asti = pd.read_csv('WeatherData/asti_270615.csv')
df_bologna = pd.read_csv('WeatherData/bologna_270615.csv')
df_piacenza = pd.read_csv('WeatherData/piacenza_270615.csv')
df_cesena = pd.read_csv('WeatherData/cesena_270615.csv')
df_faenza = pd.read_csv('WeatherData/faenza_270615.csv')
# 查看数据结构
print(df_ravenna.head())
print(f"数据形状: {df_ravenna.shape}")
三、温度数据分析
3.1 单城市气温日变化
首先以米兰为例,分析一天中气温的变化趋势。
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from dateutil import parser
# 提取温度和日期数据
y1 = df_milano['temp'] # 温度列
x1 = df_milano['day'] # 日期时间列
# 将字符串日期转换为 datetime 对象
day_milano = [parser.parse(x) for x in x1]
# 创建画布和坐标轴
fig, ax = plt.subplots()
# 调整 x 轴刻度旋转角度,方便查看
plt.xticks(rotation=70)
# 设置时间显示格式为 时:分
hours = mdates.DateFormatter('%H:%M')
ax.xaxis.set_major_formatter(hours)
# 绘制红色折线图
ax.plot(day_milano, y1, 'r')
plt.title('米兰一天气温变化趋势')
plt.xlabel('时间')
plt.ylabel('温度 (℃)')
plt.show()
观察结论:气温走势接近正弦曲线——早上开始逐渐升高,最高温出现在下午 2-6 点,随后逐渐下降,凌晨 6 点达到最低值。
3.2 近海 vs 内陆城市温度对比
为了验证"海洋影响气温"的假设,我们同时绘制 3 个近海城市和 3 个内陆城市的气温曲线。
# 近海城市(红色)
y1, x1 = df_ravenna['temp'], df_ravenna['day']
y2, x2 = df_faenza['temp'], df_faenza['day']
y3, x3 = df_cesena['temp'], df_cesena['day']
# 内陆城市(绿色)
y4, x4 = df_milano['temp'], df_milano['day']
y5, x5 = df_asti['temp'], df_asti['day']
y6, x6 = df_torino['temp'], df_torino['day']
# 日期转换
day_ravenna = [parser.parse(x) for x in x1]
day_faenza = [parser.parse(x) for x in x2]
day_cesena = [parser.parse(x) for x in x3]
day_milano = [parser.parse(x) for x in x4]
day_asti = [parser.parse(x) for x in x5]
day_torino = [parser.parse(x) for x in x6]
# 绘制对比图
fig, ax = plt.subplots()
plt.xticks(rotation=70)
hours = mdates.DateFormatter('%H:%M')
ax.xaxis.set_major_formatter(hours)
# 近海组用红色,内陆组用绿色
ax.plot(day_ravenna, y1, 'r', day_faenza, y2, 'r', day_cesena, y3, 'r')
ax.plot(day_milano, y4, 'g', day_asti, y5, 'g', day_torino, y6, 'g')
plt.title('近海城市(红) vs 内陆城市(绿) 气温对比')
plt.legend(['近海城市', '内陆城市'])
plt.show()
关键发现:近海城市(红色)的最高气温明显低于内陆城市(绿色),但最低气温差异不大。这说明海洋对白天最高温的"降温"效果更显著。
3.3 最高温/最低温与离海距离的关系
接下来,我们收集 10 个城市的最高温和最低温,绘制它们与离海距离的散点图。
# 各城市离海距离(km)
dist = [
df_ravenna['dist'][0], df_cesena['dist'][0], df_faenza['dist'][0],
df_ferrara['dist'][0], df_bologna['dist'][0], df_mantova['dist'][0],
df_piacenza['dist'][0], df_milano['dist'][0], df_asti['dist'][0],
df_torino['dist'][0]
]
# 各城市最高温度
temp_max = [
df_ravenna['temp'].max(), df_cesena['temp'].max(), df_faenza['temp'].max(),
df_ferrara['temp'].max(), df_bologna['temp'].max(), df_mantova['temp'].max(),
df_piacenza['temp'].max(), df_milano['temp'].max(), df_asti['temp'].max(),
df_torino['temp'].max()
]
# 各城市最低温度
temp_min = [
df_ravenna['temp'].min(), df_cesena['temp'].min(), df_faenza['temp'].min(),
df_ferrara['temp'].min(), df_bologna['temp'].min(), df_mantova['temp'].min(),
df_piacenza['temp'].min(), df_milano['temp'].min(), df_asti['temp'].min(),
df_torino['temp'].min()
]
# 绘制最高温-距离散点图
fig, ax = plt.subplots()
ax.plot(dist, temp_max, 'ro') # 红色圆点
plt.title('最高温度与离海距离的关系')
plt.xlabel('离海距离 (km)')
plt.ylabel('最高温度 (℃)')
plt.show()
# 绘制最低温-距离散点图
plt.axis((0, 400, 15, 25))
plt.plot(dist, temp_min, 'bo') # 蓝色圆点
plt.title('最低温度与离海距离的关系')
plt.xlabel('离海距离 (km)')
plt.ylabel('最低温度 (℃)')
plt.show()
关键发现:
- 最高温随离海距离增加而升高,但在 60-70km 后趋于平缓
- 最低温与离海距离无明显关系——海洋对夜间低温的影响不显著
四、SVR回归建模——量化海洋影响
4.1 SVR 方法简介
SVR(Support Vector Regression,支持向量回归)是 SVM 在回归问题上的扩展。与普通线性回归不同,SVR 允许一定范围内的误差,通过核函数可以处理非线性关系。这里我们使用线性核(kernel='linear')来拟合温度-距离关系。
4.2 分组拟合与交点求解
将 10 个城市分为近海组(前 5 个)和内陆组(后 5 个),分别用 SVR 拟合两条直线,然后求交点——这个交点就是海洋影响的分界距离。
from sklearn.svm import SVR
from scipy.optimize import fsolve
# 分组
dist1 = dist[0:5] # 近海组距离
dist2 = dist[5:10] # 内陆组距离
# 将列表转为 SVR 需要的二维数组格式
dist1 = [[x] for x in dist1]
dist2 = [[x] for x in dist2]
temp_max1 = temp_max[0:5] # 近海组最高温
temp_max2 = temp_max[5:10] # 内陆组最高温
# 创建 SVR 模型(线性核,C=1000 尽量拟合数据)
svr_lin1 = SVR(kernel='linear', C=1e3)
svr_lin2 = SVR(kernel='linear', C=1e3)
# 拟合数据(这一步可能较慢,耐心等待)
svr_lin1.fit(dist1, temp_max1)
svr_lin2.fit(dist2, temp_max2)
# 生成预测用的 x 值
xp1 = np.arange(10, 100, 10).reshape((9, 1))
xp2 = np.arange(50, 400, 50).reshape((7, 1))
# 预测 y 值
yp1 = svr_lin1.predict(xp1)
yp2 = svr_lin2.predict(xp2)
# 绘制拟合结果
fig, ax = plt.subplots()
ax.set_xlim(0, 400)
ax.plot(xp1, yp1, c='b', label='Strong sea effect') # 近海趋势
ax.plot(xp2, yp2, c='g', label='Light sea effect') # 内陆趋势
ax.plot(dist, temp_max, 'ro') # 原始数据点
plt.legend()
plt.title('SVR 拟合:温度-距离关系')
plt.xlabel('离海距离 (km)')
plt.ylabel('最高温度 (℃)')
plt.show()
# 输出斜率和截距
print(f"近海组斜率: {svr_lin1.coef_[0][0]:.4f}, 截距: {svr_lin1.intercept_[0]:.4f}")
print(f"内陆组斜率: {svr_lin2.coef_[0][0]:.4f}, 截距: {svr_lin2.intercept_[0]:.4f}")
# 求两条直线的交点
def line1(x):
"""近海组拟合直线"""
a1 = svr_lin1.coef_[0][0]
b1 = svr_lin1.intercept_[0]
return a1 * x + b1
def line2(x):
"""内陆组拟合直线"""
a2 = svr_lin2.coef_[0][0]
b2 = svr_lin2.intercept_[0]
return a2 * x + b2
def findIntersection(fun1, fun2, x0):
"""使用 fsolve 求解两函数交点"""
return fsolve(lambda x: fun1(x) - fun2(x), x0)
result = findIntersection(line1, line2, 0.0)
print(f"交点坐标: [x, y] = [{result[0]:.0f}, {line1(result)[0]:.0f}]")
# 可视化交点
x = np.linspace(0, 300, 31)
plt.plot(x, line1(x), x, line2(x), result, line1(result), 'ro')
plt.title('两条拟合直线的交点')
plt.xlabel('离海距离 (km)')
plt.ylabel('最高温度 (℃)')
plt.show()
结论:两条直线的交点约为 [53, 30],意味着在离海约 53 公里处是海洋影响气温的分界点。53 公里以内,气温随距离快速上升(从 28℃ 到 31℃);53 公里以外,气温增速明显放缓。
五、湿度数据分析
5.1 近海 vs 内陆湿度日变化
# 读取湿度数据
y1, x1 = df_ravenna['humidity'], df_ravenna['day']
y2, x2 = df_faenza['humidity'], df_faenza['day']
y3, x3 = df_cesena['humidity'], df_cesena['day']
y4, x4 = df_milano['humidity'], df_milano['day']
y5, x5 = df_asti['humidity'], df_asti['day']
y6, x6 = df_torino['humidity'], df_torino['day']
# 日期转换
day_ravenna = [parser.parse(x) for x in x1]
day_faenza = [parser.parse(x) for x in x2]
day_cesena = [parser.parse(x) for x in x3]
day_milano = [parser.parse(x) for x in x4]
day_asti = [parser.parse(x) for x in x5]
day_torino = [parser.parse(x) for x in x6]
# 绘制湿度对比图
fig, ax = plt.subplots()
plt.xticks(rotation=70)
hours = mdates.DateFormatter('%H:%M')
ax.xaxis.set_major_formatter(hours)
# 近海红色,内陆绿色
ax.plot(day_ravenna, y1, 'r', day_faenza, y2, 'r', day_cesena, y3, 'r')
ax.plot(day_milano, y4, 'g', day_asti, y5, 'g', day_torino, y6, 'g')
plt.title('近海城市(红) vs 内陆城市(绿) 湿度对比')
plt.xlabel('时间')
plt.ylabel('湿度 (%)')
plt.show()
5.2 湿度极值与距离关系
# 收集各城市最大湿度和最小湿度
hum_max = [
df_ravenna['humidity'].max(), df_cesena['humidity'].max(),
df_faenza['humidity'].max(), df_ferrara['humidity'].max(),
df_bologna['humidity'].max(), df_mantova['humidity'].max(),
df_piacenza['humidity'].max(), df_milano['humidity'].max(),
df_asti['humidity'].max(), df_torino['humidity'].max()
]
hum_min = [
df_ravenna['humidity'].min(), df_cesena['humidity'].min(),
df_faenza['humidity'].min(), df_ferrara['humidity'].min(),
df_bologna['humidity'].min(), df_mantova['humidity'].min(),
df_piacenza['humidity'].min(), df_milano['humidity'].min(),
df_asti['humidity'].min(), df_torino['humidity'].min()
]
# 绘制最大湿度-距离关系
plt.plot(dist, hum_max, 'bo')
plt.title('最大湿度与离海距离的关系')
plt.xlabel('离海距离 (km)')
plt.ylabel('最大湿度 (%)')
plt.show()
# 绘制最小湿度-距离关系
plt.plot(dist, hum_min, 'bo')
plt.title('最小湿度与离海距离的关系')
plt.xlabel('离海距离 (km)')
plt.ylabel('最小湿度 (%)')
plt.show()
关键发现:
- 近海城市的湿度普遍高于内陆城市(全天差距约 20%)
- 但湿度和距离之间不存在明显的线性关系——10 个数据点太少,不足以描述趋势
六、风向频率玫瑰图(极区图)
6.1 为什么用极区图?
风向数据是 0°~360° 的角度值,用普通笛卡尔坐标系散点图展示效果很差——数据点散落在整个圆上,难以看出分布规律。极区图(Polar Chart) 是展示角度分布数据的最佳选择。
6.2 构建风向直方图
# 将 360° 分为 8 个面元,每个 45°
hist, bins = np.histogram(df_ravenna['wind_deg'], 8, [0, 360])
print("各面元风向频次:", hist)
print("面元边界:", bins)
# 输出示例: hist = [0, 5, 11, 1, 0, 1, 0, 0]
# bins = [0, 45, 90, 135, 180, 225, 270, 315, 360]
6.3 封装极区图绘制函数
def showRoseWind(values, city_name, max_value):
"""
绘制风向频率玫瑰图(极区图)
参数:
values: 各面元的风向频次数组(长度=8)
city_name: 城市名称(用于标题)
max_value: 最大频次值(用于颜色映射)
"""
N = 8 # 8个扇区
# 计算每个扇区的角度位置(弧度制)
# theta = [π/8, 3π/8, 5π/8, ..., 15π/8]
theta = np.arange(2 * np.pi / 16, 2 * np.pi, 2 * np.pi / 8)
radii = np.array(values)
# 创建极坐标系
plt.axes([0.025, 0.025, 0.95, 0.95], polar=True)
# 颜色映射:值越大颜色越接近蓝色
colors = [(1 - x / max_value, 1 - x / max_value, 0.75) for x in radii]
# 绘制柱状图(极坐标)
plt.bar(theta, radii, width=(2 * np.pi / N), bottom=0.0, color=colors)
# 设置标题
plt.title(city_name, x=0.2, fontsize=20)
# 绘制 Ravenna 的风向玫瑰图
showRoseWind(hist, 'Ravenna', max(hist))
plt.show()
解读:Ravenna 当天大部分时间风向为西南和正西方向(对应面元频次最高)。
6.4 风速均值玫瑰图
除了风向频次,我们还可以展示各方向的平均风速。
def RoseWind_Speed(df_city):
"""
计算 8 个方向面元的平均风速
参数:
df_city: 某个城市的 DataFrame
返回:
各方向平均风速的 NumPy 数组(长度=8)
"""
degs = np.arange(45, 361, 45) # [45, 90, 135, ..., 360]
tmp = []
for deg in degs:
# 筛选风向在 (deg-46, deg) 范围内的数据,计算平均风速
avg_speed = df_city[
(df_city['wind_deg'] > (deg - 46)) &
(df_city['wind_deg'] < deg)
]['wind_speed'].mean()
tmp.append(avg_speed)
return np.array(tmp)
# 绘制 Ravenna 的风速玫瑰图
showRoseWind(RoseWind_Speed(df_ravenna), 'Ravenna - Wind Speed', max(hist))
plt.show()
# 对比 Ferrara 的风向分布
hist_ferrara, _ = np.histogram(df_ferrara['wind_deg'], 8, [0, 360])
showRoseWind(hist_ferrara, 'Ferrara', max(hist_ferrara))
plt.show()
七、常见错误与解决方案
错误案例 1:SVR.fit() 报错 “Expected 2D array, got 1D array”
现象:
ValueError: Expected 2D array, got 1D array instead:
array=[...]
Reshape your data either using array.reshape(-1, 1)
原因:scikit-learn 要求特征矩阵必须是二维数组(n_samples × n_features),而直接传入列表会被当作一维数组。
解决方案:
# ❌ 错误写法
dist1 = [10, 20, 30, 40, 50]
svr_lin1.fit(dist1, temp_max1) # 报错!
# ✅ 正确写法:将列表转为二维数组
dist1 = [[x] for x in dist1] # 列表推导式
# 或者使用 NumPy
dist1 = np.array(dist1).reshape(-1, 1)
svr_lin1.fit(dist1, temp_max1) # 正常运行
错误案例 2:极区图中文标题显示为方框
现象:极区图标题中的中文显示为 □□□。
原因:matplotlib 默认字体不支持中文。
解决方案:
import matplotlib.pyplot as plt
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans', 'WenQuanYi Micro Hei']
# 解决负号显示问题
plt.rcParams['axes.unicode_minus'] = False
# 如果仍不生效,可以指定字体路径
from matplotlib.font_manager import FontProperties
font = FontProperties(fname='/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
plt.title('拉文纳风向玫瑰图', fontproperties=font)
错误案例 3:fsolve 不收敛或返回错误交点
现象:fsolve 返回的结果与预期不符,或报错 “The iteration is not making good progress”。
原因:初始猜测值 x0 离真实交点太远,导致迭代不收敛。
解决方案:
from scipy.optimize import fsolve
def findIntersection(fun1, fun2, x0):
return fsolve(lambda x: fun1(x) - fun2(x), x0)
# ❌ 不好的初始值
result = findIntersection(line1, line2, 0.0) # 如果真实交点在 200 附近,可能不收敛
# ✅ 根据散点图观察,给出合理的初始猜测
# 观察温度-距离散点图,两条趋势线的交点大约在 50-60km 之间
result = findIntersection(line1, line2, 50.0) # 更好的初始值
# ✅ 更稳健的做法:尝试多个初始值
for x0 in [10, 50, 100, 200]:
try:
result = findIntersection(line1, line2, x0)
print(f"x0={x0}: 交点={result[0]:.1f}")
except Exception as e:
print(f"x0={x0}: 失败 - {e}")
错误案例 4:np.histogram 面元边界设置不当
现象:风向数据无法正确分配到各个面元,某些面元频次始终为 0。
原因:np.histogram 的 bins 参数如果设置不当,边界值可能无法覆盖所有数据点。
解决方案:
# ❌ 可能遗漏边界数据
hist, bins = np.histogram(df['wind_deg'], 8, [0, 360])
# 360° 的数据可能被遗漏(因为区间是左闭右开 [0, 360))
# ✅ 方案1:将范围设为 [0, 361)
hist, bins = np.histogram(df['wind_deg'], 8, [0, 361])
# ✅ 方案2:将 360° 的数据合并到 0° 面元
df['wind_deg'] = df['wind_deg'].apply(lambda x: 0 if x == 360 else x)
hist, bins = np.histogram(df['wind_deg'], 8, [0, 360])
八、实验总结与展望
8.1 核心结论
| 分析维度 | 关键发现 |
|---|---|
| 温度-距离 | 最高温随离海距离增加而升高,SVR 拟合交点约 53km 为海洋影响分界 |
| 最低温 | 与离海距离无明显关系,海洋对夜间低温影响不显著 |
| 湿度 | 近海城市湿度高于内陆约 20%,但无明确线性关系 |
| 风向 | 极区图清晰展示风向分布,Ravenna 当天以西南/西风为主 |
8.2 项目亮点
- 科学假设驱动:从"海洋影响气候"的感性认知出发,用数据验证假设
- 多维度分析:覆盖温度、湿度、风向、风速四个维度
- SVR 回归量化:不仅定性分析,还通过回归建模找到具体的分界距离
- 极区图可视化:针对角度数据选择了最合适的可视化方式
8.3 改进方向
- 时间维度扩展:本文仅分析了一天的数据,可以扩展到多日/多季节
- 更多城市样本:10 个城市的数据点偏少,增加样本可提高统计显著性
- 引入更多特征:加入气压、降雨量等气象指标进行多变量分析
- 非线性模型:尝试 RBF 核的 SVR 或其他非线性回归方法
参考链接
- 图灵教育《Python 数据分析实战》第 2 章 — 本文核心参考来源
- scikit-learn SVR 官方文档 — SVR 参数详解
- Matplotlib 极坐标绘图指南 — 极区图官方教程
- SciPy fsolve 使用文档 — 方程组求解方法
- OpenWeatherMap API — 全球气象数据获取
下一篇预告
下一篇我们将进入金融风控领域,带来 《Python大数据实战(九):申请评分卡——银行信贷风控模型全流程实战》。我们将学习:
- WOE 与 IV 值计算与特征筛选
- 分箱(Binning)技术详解
- 逻辑回归构建评分卡
- KS 值与 AUC 评估模型效果
- 评分刻度转换(Scorecard Scaling)
敬请期待!🚀
如果本文对你有帮助,欢迎点赞、收藏、关注三连!你的支持是我持续创作的动力 💪
1522

被折叠的 条评论
为什么被折叠?



