当NASA的卫星划过北美大陆上空时,那些看似简单的温度读数背后,隐藏着气候变化的密码。作为数据分析师,我们如何从这些时间序列数据中提取有价值的信息?SARIMA模型正是解开这个谜题的钥匙。本文将带你完整走完从数据清洗到预测建模的全流程,手把手教你用Python实现专业级的气候数据分析。
工欲善其事,必先利其器。在开始建模前,我们需要配置合适的Python环境。推荐使用Anaconda创建独立环境:
bash复制conda create -n timeseries python=3.8
conda activate timeseries
pip install statsmodels pandas matplotlib seaborn
NASA提供的北美地表温度数据集可以从GISS Surface Temperature Analysis (GISTEMP)获取。为方便读者实践,我已将处理好的数据上传至示例代码仓库:
python复制import pandas as pd
import numpy as np
# 加载数据集
temp_data = pd.read_csv('north_america_temp.csv',
parse_dates=['date'],
index_col='date')
print(f"数据集时间范围:{temp_data.index.min()} 至 {temp_data.index.max()}")
数据包含1990年1月至2020年12月的月度平均温度,共372条记录。让我们先进行初步可视化:
python复制import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
temp_data['temp'].plot(title='北美地表温度变化趋势 (1990-2020)')
plt.ylabel('温度(℃)')
plt.grid(True)
plt.show()
原始时间序列通常包含趋势、季节性和噪声三种成分。有效的预处理是模型成功的关键。
使用statsmodels进行季节性分解,直观展示各成分:
python复制from statsmodels.tsa.seasonal import seasonal_decompose
decomposition = seasonal_decompose(temp_data['temp'],
model='additive',
period=12)
fig = decomposition.plot()
fig.set_size_inches(12, 8)
观察分解结果,我们可以清晰看到:
SARIMA模型要求序列平稳。我们使用ADF检验验证平稳性:
python复制from statsmodels.tsa.stattools import adfuller
def adf_test(series):
result = adfuller(series.dropna())
print(f'ADF统计量: {result[0]:.4f}')
print(f'p-value: {result[1]:.4f}')
print('临界值:')
for key, value in result[4].items():
print(f'\t{key}: {value:.4f}')
print("原始序列检验:")
adf_test(temp_data['temp'])
输出结果显示p值为0.9985,远大于0.05,说明序列不平稳。我们需要进行差分处理:
python复制# 一阶常规差分
temp_data['diff_1'] = temp_data['temp'].diff(1)
# 季节性差分(周期12个月)
temp_data['diff_seasonal'] = temp_data['diff_1'].diff(12)
print("一阶季节性差分后检验:")
adf_test(temp_data['diff_seasonal'])
经过一阶常规差分和12步季节性差分后,p值降至0.0000,序列已达到平稳要求。
SARIMA模型有7个关键参数:(p,d,q)×(P,D,Q,s)。确定这些参数需要结合ACF/PACF图和信息准则:
python复制from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
plot_acf(temp_data['diff_seasonal'].dropna(), lags=36, ax=ax1)
plot_pacf(temp_data['diff_seasonal'].dropna(), lags=36, ax=ax2)
plt.show()
通过观察自相关图,我们可以初步判断:
为找到最优参数组合,我们编写自动化搜索函数:
python复制from statsmodels.tsa.statespace.sarimax import SARIMAX
from itertools import product
import warnings
warnings.filterwarnings('ignore')
def optimize_sarima(series, seasonal_period=12):
p = d = q = range(0, 3)
P = D = Q = range(0, 2)
param_combinations = list(product(p, d, q, P, D, Q))
results = []
for params in param_combinations:
try:
model = SARIMAX(series,
order=(params[0], params[1], params[2]),
seasonal_order=(params[3], params[4], params[5], seasonal_period),
enforce_stationarity=False,
enforce_invertibility=False)
result = model.fit(disp=0)
results.append({
'params': params,
'aic': result.aic,
'bic': result.bic
})
except:
continue
return pd.DataFrame(results)
results_df = optimize_sarima(temp_data['temp'])
best_params = results_df.loc[results_df['aic'].idxmin()]
print(f"最优参数组合: {best_params['params']}, AIC: {best_params['aic']:.2f}")
根据搜索结果,我们建立最终模型:
python复制best_order = (1, 1, 1)
best_seasonal_order = (0, 1, 1, 12)
final_model = SARIMAX(temp_data['temp'],
order=best_order,
seasonal_order=best_seasonal_order,
enforce_stationarity=False)
model_result = final_model.fit(disp=0)
# 模型诊断图
model_result.plot_diagnostics(figsize=(15, 12))
plt.show()
诊断图显示:
将数据分为训练集(1990-2019)和测试集(2020):
python复制train = temp_data.loc[:'2019-12-31']
test = temp_data.loc['2020-01-31':]
# 重新训练模型
final_model = SARIMAX(train['temp'],
order=best_order,
seasonal_order=best_seasonal_order)
model_result = final_model.fit(disp=0)
# 进行12步预测
forecast = model_result.get_forecast(steps=12)
forecast_mean = forecast.predicted_mean
confidence_int = forecast.conf_int()
python复制plt.figure(figsize=(12, 6))
plt.plot(train.index, train['temp'], label='训练数据')
plt.plot(test.index, test['temp'], label='实际值')
plt.plot(forecast_mean.index, forecast_mean, label='预测值')
plt.fill_between(confidence_int.index,
confidence_int.iloc[:, 0],
confidence_int.iloc[:, 1],
color='pink', alpha=0.3)
plt.title('北美地表温度预测效果 (2020年)')
plt.legend()
plt.grid(True)
plt.show()
python复制from sklearn.metrics import mean_squared_error, mean_absolute_error
def evaluate_forecast(actual, predicted):
mse = mean_squared_error(actual, predicted)
mae = mean_absolute_error(actual, predicted)
rmse = np.sqrt(mse)
print(f'MSE: {mse:.4f}')
print(f'MAE: {mae:.4f}')
print(f'RMSE: {rmse:.4f}')
evaluate_forecast(test['temp'], forecast_mean)
评估结果显示:
这些指标表明模型预测误差在±0.43℃范围内,对于气候数据来说是可接受的精度。
将所有步骤整合为可复用的Pipeline:
python复制class TemperatureForecaster:
def __init__(self, data_path):
self.data = pd.read_csv(data_path, parse_dates=['date'], index_col='date')
self.model = None
def preprocess(self):
"""数据预处理"""
self.data['diff_1'] = self.data['temp'].diff(1)
self.data['diff_seasonal'] = self.data['diff_1'].diff(12)
return self.data.dropna()
def train_model(self, train_end='2019-12-31'):
"""模型训练"""
train = self.data.loc[:train_end]
self.model = SARIMAX(train['temp'],
order=(1,1,1),
seasonal_order=(0,1,1,12))
self.result = self.model.fit(disp=0)
return self.result
def evaluate(self, test_start='2020-01-01'):
"""模型评估"""
test = self.data.loc[test_start:]
forecast = self.result.get_forecast(steps=len(test))
return evaluate_forecast(test['temp'], forecast.predicted_mean)
def forecast(self, steps=12):
"""未来预测"""
forecast = self.result.get_forecast(steps=steps)
return forecast.predicted_mean, forecast.conf_int()
实际项目中,还可以考虑以下优化方向:
在Jupyter Notebook中运行完整代码时,建议使用tqdm显示进度条,这对长时间运行的参数搜索特别有用:
python复制from tqdm.notebook import tqdm
for params in tqdm(param_combinations, desc='参数搜索进度'):
# 模型训练代码
pass
记得定期保存模型结果,避免重复计算:
python复制import joblib
# 保存模型
joblib.dump(model_result, 'sarima_model.pkl')
# 加载模型
loaded_model = joblib.load('sarima_model.pkl')