Repeated, optionally stratified k-fold cross-validation, implemented with base R and data.table. split_cv() works on a list of tables, nest_cv() on a nested table such as the output of w2l_nest(). Folds are returned as row indices and, optionally, as training / validation subsets (data.table or data.frame).

Cross-validation for a list of tables: split_cv()

# Prepare example data: Convert first 3 columns of iris dataset to long format and split
dt_split <- w2l_split(data = iris, cols = 1:3)
# dt_split is now a list containing 3 data tables for Sepal.Length, Sepal.Width, and Petal.Length

# Example 1: Single cross-validation (no repeats)
split_cv(
  data = dt_split,      # Input list of split data
  v = 3,                # Set 3-fold cross-validation
  repeats = 1,          # Perform cross-validation once (no repeats)
  seed = 123            # Reproducible folds
)
#> $Sepal.Length
#>        id                  train_idx              validate_idx
#>    <char>                     <list>                    <list>
#> 1:  Fold1  1, 2, 5, 7, 9,10,...[100]  3, 4, 6, 8,15,19,...[50]
#> 2:  Fold2       3,4,5,6,7,8,...[100]  1, 2, 9,10,11,14,...[50]
#> 3:  Fold3       1,2,3,4,6,8,...[100]  5, 7,12,13,16,17,...[50]
#>                  train           validate
#>                 <list>             <list>
#> 1: <data.table[100x3]> <data.table[50x3]>
#> 2: <data.table[100x3]> <data.table[50x3]>
#> 3: <data.table[100x3]> <data.table[50x3]>
#> 
#> $Sepal.Width
#>        id                  train_idx              validate_idx
#>    <char>                     <list>                    <list>
#> 1:  Fold1  2, 4, 5, 6, 7,13,...[100]  1, 3, 8, 9,10,11,...[50]
#> 2:  Fold2       1,2,3,4,6,8,...[100]  5, 7,13,14,17,21,...[50]
#> 3:  Fold3       1,3,5,7,8,9,...[100]  2, 4, 6,15,18,22,...[50]
#>                  train           validate
#>                 <list>             <list>
#> 1: <data.table[100x3]> <data.table[50x3]>
#> 2: <data.table[100x3]> <data.table[50x3]>
#> 3: <data.table[100x3]> <data.table[50x3]>
#> 
#> $Petal.Length
#>        id                  train_idx              validate_idx
#>    <char>                     <list>                    <list>
#> 1:  Fold1  1, 2, 8, 9,10,11,...[100]  3, 4, 5, 6, 7,12,...[50]
#> 2:  Fold2       2,3,4,5,6,7,...[100]  1,10,11,17,18,19,...[50]
#> 3:  Fold3       1,3,4,5,6,7,...[100]  2, 8, 9,14,15,20,...[50]
#>                  train           validate
#>                 <list>             <list>
#> 1: <data.table[100x3]> <data.table[50x3]>
#> 2: <data.table[100x3]> <data.table[50x3]>
#> 3: <data.table[100x3]> <data.table[50x3]>
# Returns a list where each element contains:
# - id: fold labels (Fold1, Fold2, Fold3)
# - train_idx / validate_idx: row indices of each fold
# - train / validate: training and validation subsets

# Example 2: Repeated cross-validation
split_cv(
  data = dt_split,      # Input list of split data
  v = 3,                # Set 3-fold cross-validation
  repeats = 2,          # Perform cross-validation twice
  seed = 123
)
#> $Sepal.Length
#>         id    id2                  train_idx              validate_idx
#>     <char> <char>                     <list>                    <list>
#> 1: Repeat1  Fold1  1, 2, 5, 7, 9,10,...[100]  3, 4, 6, 8,15,19,...[50]
#> 2: Repeat1  Fold2       3,4,5,6,7,8,...[100]  1, 2, 9,10,11,14,...[50]
#> 3: Repeat1  Fold3       1,2,3,4,6,8,...[100]  5, 7,12,13,16,17,...[50]
#> 4: Repeat2  Fold1  2, 4, 5, 6, 7,13,...[100]  1, 3, 8, 9,10,11,...[50]
#> 5: Repeat2  Fold2       1,2,3,4,6,8,...[100]  5, 7,13,14,17,21,...[50]
#> 6: Repeat2  Fold3       1,3,5,7,8,9,...[100]  2, 4, 6,15,18,22,...[50]
#>                  train           validate
#>                 <list>             <list>
#> 1: <data.table[100x3]> <data.table[50x3]>
#> 2: <data.table[100x3]> <data.table[50x3]>
#> 3: <data.table[100x3]> <data.table[50x3]>
#> 4: <data.table[100x3]> <data.table[50x3]>
#> 5: <data.table[100x3]> <data.table[50x3]>
#> 6: <data.table[100x3]> <data.table[50x3]>
#> 
#> $Sepal.Width
#>         id    id2                  train_idx              validate_idx
#>     <char> <char>                     <list>                    <list>
#> 1: Repeat1  Fold1  1, 2, 8, 9,10,11,...[100]  3, 4, 5, 6, 7,12,...[50]
#> 2: Repeat1  Fold2       2,3,4,5,6,7,...[100]  1,10,11,17,18,19,...[50]
#> 3: Repeat1  Fold3       1,3,4,5,6,7,...[100]  2, 8, 9,14,15,20,...[50]
#> 4: Repeat2  Fold1  1, 4, 5, 7, 9,10,...[100]  2, 3, 6, 8,14,20,...[50]
#> 5: Repeat2  Fold2       2,3,6,7,8,9,...[100]  1, 4, 5,12,17,21,...[50]
#> 6: Repeat2  Fold3       1,2,3,4,5,6,...[100]  7, 9,10,11,13,15,...[50]
#>                  train           validate
#>                 <list>             <list>
#> 1: <data.table[100x3]> <data.table[50x3]>
#> 2: <data.table[100x3]> <data.table[50x3]>
#> 3: <data.table[100x3]> <data.table[50x3]>
#> 4: <data.table[100x3]> <data.table[50x3]>
#> 5: <data.table[100x3]> <data.table[50x3]>
#> 6: <data.table[100x3]> <data.table[50x3]>
#> 
#> $Petal.Length
#>         id    id2                  train_idx              validate_idx
#>     <char> <char>                     <list>                    <list>
#> 1: Repeat1  Fold1       1,2,4,5,6,8,...[100]  3, 7, 9,15,22,23,...[50]
#> 2: Repeat1  Fold2       1,2,3,4,5,7,...[100]  6, 8,12,14,16,17,...[50]
#> 3: Repeat1  Fold3  3, 6, 7, 8, 9,12,...[100]  1, 2, 4, 5,10,11,...[50]
#> 4: Repeat2  Fold1  1, 2, 5, 8,10,12,...[100]  3, 4, 6, 7, 9,11,...[50]
#> 5: Repeat2  Fold2       3,4,5,6,7,8,...[100]  1, 2,12,14,20,21,...[50]
#> 6: Repeat2  Fold3       1,2,3,4,6,7,...[100]  5, 8,10,15,16,25,...[50]
#>                  train           validate
#>                 <list>             <list>
#> 1: <data.table[100x3]> <data.table[50x3]>
#> 2: <data.table[100x3]> <data.table[50x3]>
#> 3: <data.table[100x3]> <data.table[50x3]>
#> 4: <data.table[100x3]> <data.table[50x3]>
#> 5: <data.table[100x3]> <data.table[50x3]>
#> 6: <data.table[100x3]> <data.table[50x3]>
# Returns a list where each element contains:
# - id: repeat labels (Repeat1, Repeat2)
# - id2: fold labels (Fold1, Fold2, Fold3)
# - train_idx / validate_idx, train / validate

# Example 3: Stratified CV, indices only (memory friendly)
res <- split_cv(dt_split, v = 5, strata = "Species", seed = 1,
                materialize = FALSE)
# Rebuild the training set of fold 1 of the first dataset when needed
head(dt_split[[1]][res[[1]]$train_idx[[1]], ])
#>    Petal.Width Species value
#>          <num>  <fctr> <num>
#> 1:         0.2  setosa   5.1
#> 2:         0.2  setosa   4.9
#> 3:         0.2  setosa   4.7
#> 4:         0.2  setosa   4.6
#> 5:         0.2  setosa   5.0
#> 6:         0.4  setosa   5.4

Cross-validation inside a nested table: nest_cv()

# Example: Cross-validation for nested data.table demonstrations

# Setup test data
dt_nest <- w2l_nest(
  data = iris,                   # Input dataset
  cols = 1:2                     # Nest first 2 columns
)

# Example 1: Basic 2-fold cross-validation (reproducible)
nest_cv(
  data = dt_nest,                # Input nested data.table
  v = 2,                         # Number of folds (2-fold CV)
  seed = 123                     # Reproducible folds
)
#>            name     id                 train_idx              validate_idx
#>          <char> <char>                    <list>                    <list>
#> 1: Sepal.Length  Fold1       1,2,3,5,6,7,...[75]  4, 8, 9,11,12,15,...[75]
#> 2: Sepal.Length  Fold2  4, 8, 9,11,12,15,...[75]       1,2,3,5,6,7,...[75]
#> 3:  Sepal.Width  Fold1       1,2,3,4,6,9,...[75]  5, 7, 8,11,12,13,...[75]
#> 4:  Sepal.Width  Fold2  5, 7, 8,11,12,13,...[75]       1,2,3,4,6,9,...[75]
#>                 train           validate
#>                <list>             <list>
#> 1: <data.table[75x4]> <data.table[75x4]>
#> 2: <data.table[75x4]> <data.table[75x4]>
#> 3: <data.table[75x4]> <data.table[75x4]>
#> 4: <data.table[75x4]> <data.table[75x4]>

# Example 2: Repeated 2-fold CV, keeping only the split objects
nest_cv(
  data = dt_nest,                # Input nested data.table
  v = 2,                         # Number of folds (2-fold CV)
  repeats = 2,                   # Number of repetitions
  seed = 123,
  materialize = FALSE            # No train/validate copies (saves memory)
)
#>            name      id    id2                 train_idx
#>          <char>  <char> <char>                    <list>
#> 1: Sepal.Length Repeat1  Fold1       1,2,3,5,6,7,...[75]
#> 2: Sepal.Length Repeat1  Fold2  4, 8, 9,11,12,15,...[75]
#> 3: Sepal.Length Repeat2  Fold1       1,2,3,4,6,9,...[75]
#> 4: Sepal.Length Repeat2  Fold2  5, 7, 8,11,12,13,...[75]
#> 5:  Sepal.Width Repeat1  Fold1  2, 3, 7, 8, 9,11,...[75]
#> 6:  Sepal.Width Repeat1  Fold2  1, 4, 5, 6,10,14,...[75]
#> 7:  Sepal.Width Repeat2  Fold1  3, 9,10,14,15,16,...[75]
#> 8:  Sepal.Width Repeat2  Fold2       1,2,4,5,6,7,...[75]
#>                 validate_idx
#>                       <list>
#> 1:  4, 8, 9,11,12,15,...[75]
#> 2:       1,2,3,5,6,7,...[75]
#> 3:  5, 7, 8,11,12,13,...[75]
#> 4:       1,2,3,4,6,9,...[75]
#> 5:  1, 4, 5, 6,10,14,...[75]
#> 6:  2, 3, 7, 8, 9,11,...[75]
#> 7:       1,2,4,5,6,7,...[75]
#> 8:  3, 9,10,14,15,16,...[75]

# Example 3: data.frame subsets, ready for ASReml-R / lm() / glm()
cv_df <- nest_cv(dt_nest, v = 2, seed = 123, out_type = "df")
class(cv_df$train[[1]])        # "data.frame"
#> [1] "data.frame"

# Example 4: masking-style CV (keep all rows, hide validation phenotypes)
cv_idx <- nest_cv(dt_nest, v = 2, seed = 123, materialize = FALSE)
cv_idx[dt_nest, on = "name", full := i.data]   # attach the full nested table
masked <- as.data.frame(cv_idx$full[[1]])      # a copy: dt_nest stays intact
masked$value[cv_idx$validate_idx[[1]]] <- NA
sum(is.na(masked$value))       # validation records to be predicted
#> [1] 75