28  初始数据划分

28.1 训练集与测试集

  • 机器学习的目标是建立一个预测模型, 该模型可以对未来数据进行预测。 因此, 需要把数据分为训练集(training set)和测试集(testing set)。当模型所需的调优参数越多,训练集就应该越大。将75%的数据分配给训练集,剩余25%分配给测试集是一个常见的做法,但这并非固定规则,具体比例应根据数据量和模型复杂度进行调整。

  • 当初始数据较小时,可以使用交叉验证(cross-validation)方法来代替测试集。 交叉验证方法在 章节 34 中有详细介绍。

  • 测试集应该只在验证最终模型时被使用,且只使用一次,以避免信息泄漏

要特别避免测试集数据在模型建立时候被使用,这会导致过拟合(章节 33)。
library(tidymodels)
tidymodels::tidymodels_prefer()

data(ames, package = "modeldata")

# fmt: skip
ames <- ames |>
  select(
    Sale_Price, Bldg_Type, Neighborhood, Year_Built, Gr_Liv_Area, Full_Bath, Half_Bath, Year_Sold, Lot_Area, Central_Air, Longitude, Latitude
  ) |>
  mutate(
    Sale_Price = log10(Sale_Price),
    Bath = Full_Bath + 0.5 * Half_Bath
  ) |>
  select(-Full_Bath, -Half_Bath)
glimpse(ames)
Rows: 2,930
Columns: 11
$ Sale_Price   <dbl> 5.332438, 5.021189, 5.235528, 5.387390, 5.278525, 5.29114…
$ Bldg_Type    <fct> OneFam, OneFam, OneFam, OneFam, OneFam, OneFam, TwnhsE, T…
$ Neighborhood <fct> North_Ames, North_Ames, North_Ames, North_Ames, Gilbert, …
$ Year_Built   <int> 1960, 1961, 1958, 1968, 1997, 1998, 2001, 1992, 1995, 199…
$ Gr_Liv_Area  <int> 1656, 896, 1329, 2110, 1629, 1604, 1338, 1280, 1616, 1804…
$ Year_Sold    <int> 2010, 2010, 2010, 2010, 2010, 2010, 2010, 2010, 2010, 201…
$ Lot_Area     <int> 31770, 11622, 14267, 11160, 13830, 9978, 4920, 5005, 5389…
$ Central_Air  <fct> Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, Y, …
$ Longitude    <dbl> -93.61975, -93.61976, -93.61939, -93.61732, -93.63893, -9…
$ Latitude     <dbl> 42.05403, 42.05301, 42.05266, 42.05125, 42.06090, 42.0607…
$ Bath         <dbl> 1.0, 1.0, 1.5, 2.5, 2.5, 2.5, 2.0, 2.0, 2.0, 2.5, 2.5, 2.…

28.2 简单数据划分

在划分数据时,模型与预测结果的用途应该被重点考虑。换句话说,我们需要判断模型是否会对当前数据集中存在的同一群体进行预测。

  • 例如在AMES房价数据集中,建立模型目标是对该镇范围内未来可能出现的房产进行预测。由于现有数据已涵盖该镇房产的典型类型(如房屋结构、区位特征等),未来新房屋本质是现有数据群体的 “延伸”,因此这种预测属于插值(interpolation)—— 基于已有数据覆盖的特征范围做预测,而非超出范围的外推。

  • 而在含每日列车通勤人数的数据集中,模型的目标是预测未来客流量。数据受时间因素显著影响(如工作日 / 周末、季节、节假日等规律),导致客流量模式随时间变化明显。这意味着我们需要对未来时间点进行预测,而这些时间点的数据不在现有数据范围内,因此这种预测属于外推(extrapolation)—— 基于已有数据的时间规律推断未来未发生时间点的数据。

  • 以上两个例子的差异表现在:AMES房价预测是对 “同一区域内未来可能出现的同类房产” 进行插值预测(数据分布在已有范围内部),数据拆分的逻辑可以进行随机拆分,如抽取随机的25%数据作为测试集;而列车通勤数据需预测 “未来时间的客流量”,属于对现有数据时间范围之外的外推预测(需基于历史时间规律推断未发生的时间点数据),因此数据拆分和建模逻辑需重点考虑时间维度的特殊性(如按时间顺序划分训练 / 测试集,将最接近待预测群体的数据(即时间最新的数据)作为测试集,而非随机拆分)。

resample包中的数据划分函数主要包括:

  • initial_split():简单随机划分,用于将数据集划分为训练集和测试集,通常用于初步的数据拆分。
  • initial_time_split():时间序列数据划分,适用于时间序列数据,确保训练集和测试集的时间顺序不被打乱,保证最新的数据用于测试。
  • initial_validation_split()initial_validation_time_split():验证集划分,将数据集划分为训练集和验证集,适用于需要模型调优的场景。
  • group_initial_split():分组数据划分,适用于分组数据,确保同一组内的数据不会被拆分到不同的训练集和测试集中。
set.seed(3024)
ames_split <- initial_split(ames, prop = 0.75) # prop默认为0.75
ames_split
<Training/Testing/Total>
<2197/733/2930>
ames_train <- training(ames_split)
ames_test <- testing(ames_split)

28.3 使用结果变量进行数据分割-分层抽样

有时候需要确保训练集和测试集中某些关键变量的分布一致,尤其是当目标变量(因变量)类别不平衡时。此时可以使用分层抽样(stratified sampling)方法。

set.seed(3024)
ames_block_split <- initial_split(ames, strata = Sale_Price)
ames_block_split
<Training/Testing/Total>
<2197/733/2930>

28.4 验证集划分

验证集在 小节 34.1 中有详细介绍。
  • 本质上,验证集是从训练集中划分出来的一部分数据,可以视作对训练集的单次重采样(仅划分 1 次训练子集与验证子集),用于在模型训练过程中评估模型性能,帮助选择最佳模型和调整超参数。

  • 当数据量非常大时,多次重采样的 “额外成本”(如时间、算力)会超过其价值,此时单次验证集的评估精度已足够可靠,多次重采样无法显著提升精度,反而造成资源浪费,因此更适合用验证集。

  • tidymodels中使用验证集时,保持数据的适当顺序非常重要。

set.seed(4)
ames_val_split <- initial_validation_split(
  ames,
  strata = Sale_Price, # 分层抽样
  prop = c(0.8, 0.1) # 80%训练,10%验证,10%测试
)
ames_val_split
<Training/Validation/Testing/Total>
<2342/293/295/2930>

28.5 多层数据划分

  • 多层数据的核心特征:非独立性与相关性。当数据集的行(数据点)来自 “对同一主体多次观测” 时(如患者的长期随访数据、客户的多次购买记录),这些行不满足统计独立性,即同一主体的多个数据点会高度相关(比如某患者不同时间的血压数据,比不同患者的血压数据关联性更强)。这类数据有多个名称(如多层数据、纵向数据等),部分还存在多层级结构(如 “医院→科室→患者”)。

  • 在对多层数据进行训练/测试集划分时,需确保同一主体的所有观测数据都被分配到同一数据集中(即训练集或测试集),以避免数据泄漏和过拟合问题。例如,在患者数据中,需确保同一患者的所有记录都在训练集或测试集中,而不是跨集分布。

data(Orthodont, package = "nlme")
glimpse(Orthodont)
Rows: 108
Columns: 4
$ distance <dbl> 26.0, 25.0, 29.0, 31.0, 21.5, 22.5, 23.0, 26.5, 23.0, 22.5, 2…
$ age      <dbl> 8, 10, 12, 14, 8, 10, 12, 14, 8, 10, 12, 14, 8, 10, 12, 14, 8…
$ Subject  <ord> M01, M01, M01, M01, M02, M02, M02, M02, M03, M03, M03, M03, M…
$ Sex      <fct> Male, Male, Male, Male, Male, Male, Male, Male, Male, Male, M…
set.seed(93)
orth_split <- group_initial_split(
  Orthodont,
  group = Subject, # 按Subject分组,确保同一Subject的所有数据点都在同一数据集中
  prop = 2 / 3 # 2/3数据用于训练,1/3数据用于测试
)
orth_split
<Training/Testing/Total>
<72/36/108>
# split data
orth_train <- training(orth_split)
orth_test <- testing(orth_split)

# check if there any overlap in the subjects?
subjects_train <- unique(orth_train$Subject)
subjects_test <- unique(orth_test$Subject)
intersect(subjects_train, subjects_test) # finds all rows in both subjects_train, subjects_test
ordered()
27 Levels: M16 < M05 < M02 < M11 < M07 < M08 < M03 < M12 < M13 < ... < F11

28.6 空间数据分割

  • 空间数据的核心特征:空间自相关性。空间数据中的观测值通常受地理位置影响,导致相邻位置的数据点更相似(如气温、污染水平等),而远距离的数据点差异较大。这种空间相关性违反了统计独立性的假设,需在数据划分时加以考虑。

  • 对空间数据进行训练/测试集划分时,需采用空间分层抽样(spatial stratified sampling)或基于空间距离的划分方法,确保训练集和测试集在地理空间上有足够的分离度,以减少空间自相关对模型评估的影响。

    • 我们需要将经度和纬度转换为一种特殊类型的向量,称为几何向量。使用sf包中的st_as_sf()函数将数据框转换为空间数据框。
    • 使用tidysdm::spatial_initial_split()函数进行空间数据划分。在该函数中,可以通过strategy参数设定划分的方法。
  • 小节 34.2 小结另有详述。

我们要确保相邻或相近的位置不会被分别分配到训练集和测试集中。
library(sf)
library(tidysdm)
library(spatialsample)

# define geometry
ames_sf <- ames |>
  st_as_sf(coords = c("Longitude", "Latitude"), crs = 4326)
ames_sf |>
  select(geometry)
Simple feature collection with 2930 features and 0 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: -93.69315 ymin: 41.9865 xmax: -93.57743 ymax: 42.06339
Geodetic CRS:  WGS 84
# A tibble: 2,930 × 1
               geometry
            <POINT [°]>
 1 (-93.61975 42.05403)
 2 (-93.61976 42.05301)
 3 (-93.61939 42.05266)
 4 (-93.61732 42.05125)
 5  (-93.63893 42.0609)
 6 (-93.63893 42.06078)
 7 (-93.63379 42.06298)
 8 (-93.63383 42.06073)
 9 (-93.63285 42.06112)
10 (-93.63907 42.05919)
# ℹ 2,920 more rows
# split data
set.seed(318)
ames_spatial_split <- tidysdm::spatial_initial_split(
  ames_sf,
  prop = 0.2, # 20%数据用于测试,80%数据用于训练
  strategy = spatial_block_cv, # 设定策略
  method = "continuous", # 连续型变量
  n = 25, # 划分为25个空间块
  square = FALSE, # 使用非方形块-可以让块的形状更贴合真实的地理分布
  buffer = 250 # 设置训练集和测试集之间的缓冲区为250米,可以进一步降低相邻位置样本相互泄漏信息的风险
)
ames_spatial_split
<Training/Testing/Total>
<1587/604/2930>
# plot-缓冲点颜色为灰色,训练集点颜色为蓝色,测试集点颜色为红色
autoplot(ames_spatial_split, cex = 2) +
  labs(title = "Spatial Data Split") +
  theme_bw()