You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何针对样本量随年份变化的面板数据使用TimeSeriesSplit做交叉验证?

问题描述

我尝试用sklearn的TimeSeriesSplit处理面板数据(逐年人口快照数据),希望按年份拆分数据,但因为人口随时间演变,每年样本量不同,无法直接使用TimeSeriesSplit。

我想要实现的交叉验证方案为:按时间顺序递进拆分,每次训练集包含当前测试年份之前的所有年份数据,测试集仅包含下一个年份的数据(例如:训练集2010,测试集2011;训练集2010-2011,测试集2012;直到训练集2010-2019,测试集2020)。

我已经通过拆分年份的方式实现了该方案(代码如下),想知道是否有标准方法,能通过sklearn的CV对象来拆分数据,以便直接用于cross_val_score函数中。

import pandas as pd, numpy as np
import seaborn as sns, matplotlib.pyplot as plt

from sklearn.datasets import make_regression
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import TimeSeriesSplit

X_test, y_test = [], []

start_year = 2010
end_year = 2020

for year in np.arange(start_year, end_year+1):
    X_year, y_year = make_regression(n_samples=5+year-start_year, n_features=2, bias=100, noise=1, random_state=year)
    X_year = pd.DataFrame(X_year).rename(columns={0:'X1', 1:'X2'})
    X_year['year'] = year
    y_year = pd.Series(y_year)
    X_test.append(X_year)
    y_test.append(y_year)
    
X_test, y_test = pd.concat(X_test), pd.concat(y_test)

# modelling

X = X_test
y = y_test
years = np.unique(X_test['year'])

# modelisation
model = DummyRegressor(strategy="mean")
metric = mean_squared_error
cv = TimeSeriesSplit(n_splits=len(years)-1)

years_folds = []
res = []

for i, (train_year, test_year) in enumerate(cv.split(years)):
    
    print(f"Fold {i}:")
    print(f"  Train: index={years[train_year]}")
    print(f"  Test:  index={years[test_year]}")
    
    years_folds.append((years[train_year], years[test_year]))
    
    train_filter = X['year'].isin(years[train_year])
    test_filter = X['year'].isin(years[test_year])
    
    X_train, y_train = X.loc[train_filter.values], y[train_filter.values]
    X_test, y_test = X.loc[test_filter.values], y[test_filter.values]
    
    model.fit(X_train, y_train)
    score = metric(model.predict(X_test), y_test)
    print(f' {score=:.3}')
    res.append((years[test_year][0], score))

# plot_year_folds(years_folds)
    
folds_res = pd.DataFrame(res,columns=['test_year', metric.__name__])
folds_res.plot.scatter(x='test_year', y=metric.__name__, title=f'{metric.__name__} over test_year');

注:为提供可运行示例,我使用了虚拟数据集和虚拟模型,这并非核心内容。


解决方案

有两种标准方法可以实现你的需求,让交叉验证逻辑能直接适配cross_val_score:

方法1:使用官方GroupTimeSeriesSplit(推荐)

sklearn从0.24版本开始提供了GroupTimeSeriesSplit,专门用于按分组(此处分组为年份)进行时间序列交叉验证,完全匹配你的按年份递进拆分的需求:

from sklearn.model_selection import GroupTimeSeriesSplit
from sklearn.model_selection import cross_val_score

# 初始化交叉验证器,n_splits设为年份总数-1
cv = GroupTimeSeriesSplit(n_splits=len(years)-1)

# 调用cross_val_score时,将年份列作为groups参数传入
scores = cross_val_score(
    model, 
    X.drop('year', axis=1),  # 训练时排除年份列
    y, 
    cv=cv, 
    groups=X['year'], 
    scoring='neg_mean_squared_error'
)

# 转换为MSE(sklearn默认用负向分数优化)
mse_scores = -scores
print("各折MSE分数:", mse_scores.round(3))

方法2:自定义交叉验证迭代器

如果你的sklearn版本较低,可继承BaseCrossValidator类实现自定义的年份拆分逻辑,同样能适配cross_val_score:

from sklearn.model_selection import BaseCrossValidator
from sklearn.model_selection import cross_val_score

class YearTimeSeriesSplit(BaseCrossValidator):
    def __init__(self, n_splits=None):
        self.n_splits = n_splits
    
    def get_n_splits(self, X=None, y=None, groups=None):
        # 未指定n_splits时,默认用年份数量-1
        if self.n_splits is None:
            return len(np.unique(groups)) - 1
        return self.n_splits
    
    def split(self, X, y=None, groups=None):
        if groups is None:
            raise ValueError("必须传入年份作为groups参数")
        
        years = np.unique(groups)
        ts_split = TimeSeriesSplit(n_splits=self.get_n_splits(X, y, groups))
        
        for train_year_idx, test_year_idx in ts_split.split(years):
            train_years = years[train_year_idx]
            test_years = years[test_year_idx]
            
            # 获取对应年份的样本索引
            train_idx = np.where(groups.isin(train_years))[0]
            test_idx = np.where(groups.isin(test_years))[0]
            
            yield train_idx, test_idx

# 使用自定义验证器
cv = YearTimeSeriesSplit()
scores = cross_val_score(
    model, 
    X.drop('year', axis=1), 
    y, 
    cv=cv, 
    groups=X['year'], 
    scoring='neg_mean_squared_error'
)

mse_scores = -scores
print("各折MSE分数:", mse_scores.round(3))

这两种方法都能替代你手动拆分年份的逻辑,直接与sklearn的cross_val_score、GridSearchCV等工具兼容,其中GroupTimeSeriesSplit是官方提供的标准方案,更简洁可靠。


内容的提问来源于stack exchange,提问作者Lucas Morin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 13:19:57