Tutorials › Exam PA - Predictive Analytics › K-Fold Cross-Validation Mechanics

Exam PA · Splits, Resampling, and Cross-Validation · Tutorial 691 of 966

K-Fold Cross-Validation Mechanics

Build and run a K-fold validation loop in R, then combine held-out predictions and scores correctly.

Intermediate 12 min read

What You'll Learn

  • Assign every observation to exactly one of K folds, with fold sizes differing by at most one.
  • Construct training and held-out indices for each iteration in R.
  • Explain why K-fold validation produces one held-out prediction per observation.
  • Calculate fold-level errors and combine them into a pooled metric.
  • Distinguish pooled RMSE from the unweighted mean of fold RMSE values.
  • Combine out-of-fold classification predictions into a confusion matrix.

How K-Fold Cross-Validation Works

K-fold cross-validation uses the observed data repeatedly, but keeps each observation out of the model fit used to predict it. First, divide the \(n\) observations into \(K\) non-overlapping folds. Then fit \(K\) models: for iteration \(k\), use every fold except fold \(k\) for training and use fold \(k\) only for evaluation. The resulting held-out predictions can be combined to assess the model’s predictive performance.

The mechanics are simple, but small implementation errors can undermine the evaluation. An observation omitted from every held-out set, included in its own training data, or counted twice in the final metric changes what the result measures. A reliable workflow makes the fold assignment, training indices, held-out predictions, and metric aggregation explicit.

Definition: In K-fold cross-validation, the data are partitioned into \(K\) disjoint subsets, or folds. For each fold \(k\), fit the specified modelling procedure using observations outside fold \(k\), then evaluate predictions on observations in fold \(k\). Each observation is held out once. The \(K\) held-out prediction sets together are called the out-of-fold predictions.

For ordinary K-fold cross-validation, \(K\) is an integer of at least 2 and no larger than the number of observations. When \(n\) is not divisible by \(K\), fold sizes should differ by at most one: some folds have \(\lfloor n/K \rfloor\) observations and the rest have \(\lceil n/K \rceil\). The assignment method must reflect the prediction problem. In particular, ordinary random folds are not automatically appropriate when observations have a time order or repeated-entity dependence; the split must preserve the structure relevant to the intended prediction task.

Creating Folds and Building the R Loop

An observation’s fold assignment is an integer from 1 to \(K\). In R, the following approach randomly assigns observations while keeping fold sizes as even as possible: set \(K\) to the chosen number of folds, run set.seed(410) for reproducibility, and create the assignment with fold_id <- sample(rep(seq_len(K), length.out = nrow(dat))). The seed is an example; the important point is to record it when reproducibility matters.

For fold \(k\), the held-out row indices are those for which fold_id == k; the training indices are those for which fold_id != k. A compact implementation for a regression model is: create pred_oof <- rep(NA_real_, nrow(dat)); then, for each \(k\) from 1 to \(K\), set test_idx <- which(fold_id == k) and train_idx <- which(fold_id != k), fit lm(y ~ x, data = dat[train_idx, ]), and write predict(fit, newdata = dat[test_idx, ]) into pred_oof[test_idx]. The vector pred_oof is indexed in the original row order, making it straightforward to compare every prediction with its observed outcome.

The code is a template, not a complete prescription for every modelling task: replace the formula and fitting method as needed. If a model requires data-dependent preprocessing, estimate that preprocessing using the training rows within each iteration, then apply it to the held-out rows. Applying a transformation learned from all rows before assigning folds can allow held-out information to affect the fit.

1
Assign fold IDs.
Give every row exactly one integer fold label. Check the fold counts and confirm that no row is missing an assignment.
2
Fit using the other folds.
For each \(k\), train on rows whose fold label is not \(k\). Keep the held-out fold out of the entire fitting procedure.
3
Predict the held-out rows.
Store predictions in positions corresponding to their original observations. Continue until each fold has been held out.
4
Combine held-out results.
Calculate the desired metric from the out-of-fold predictions and corresponding outcomes, or combine fold-level metrics using the appropriate weights.

Why Each Observation Is Held Out Once

The key property follows from the partition. Each observation belongs to exactly one fold, say fold \(j\). It is excluded from the training rows when iteration \(j\) is fitted, so it receives a held-out prediction in that iteration. In every other iteration, it belongs to the training set. Because the folds are disjoint and cover all observations, it cannot belong to a second held-out fold.

This also explains the out-of-fold vector’s structure: after all \(K\) iterations, each of its \(n\) positions should contain one prediction. A simple check in R is all(!is.na(pred_oof)), assuming the prediction method itself returns no missing values. If that check fails, inspect the fold assignment and indexing before calculating a score.

$$ \bigcup_{k=1}^{K} F_k = \{1,\ldots,n\}, \qquad F_j \cap F_k = \varnothing \ \text{for } j \ne k. $$

Here \(F_k\) denotes the set of row indices in fold \(k\). The first condition says that the folds cover the full data set; the second says they do not overlap. Together, they establish that every observation is held out exactly once.

Worked Example: Checking a Fold Assignment

A hypothetical portfolio dataset contains 10 policy records. Use three folds with the assignment vector fold_id = c(2, 1, 3, 2, 1, 3, 1, 2, 3, 1). Find the held-out rows and training rows in each iteration, and check that the sizes are appropriate.

Method. List the row numbers with each fold label. The fold memberships are \(F_1=\{2,5,7,10\}\), \(F_2=\{1,4,8\}\), and \(F_3=\{3,6,9\}\). Their sizes are 4, 3, and 3. Since \(10/3=3.333\ldots\), fold sizes of 3 or 4 are as balanced as possible.

Iteration 1. Hold out rows 2, 5, 7, and 10. Train on rows 1, 3, 4, 6, 8, and 9. The training size is 6 and the held-out size is 4.

Iteration 2. Hold out rows 1, 4, and 8. Train on rows 2, 3, 5, 6, 7, 9, and 10. The sizes are 7 and 3.

Iteration 3. Hold out rows 3, 6, and 9. Train on rows 1, 2, 4, 5, 7, 8, and 10. Again, the sizes are 7 and 3.

The folds are disjoint, their sizes sum to \(4+3+3=10\), and every row number from 1 to 10 appears once among the held-out sets. Common mistake avoided: treating a fold as the training data rather than the held-out data. In each iteration, the fold labelled \(k\) is the test fold; the other \(K-1\) folds form the training data.

Combining Regression Errors

For a quantitative response, a common metric is the root mean squared error (RMSE). Let \(y_i\) be the observed response and \(\hat{y}_i^{\text{OOF}}\) its held-out prediction. The pooled mean squared error (MSE) averages squared errors over all \(n\) observations, and the pooled RMSE is the square root of that average. The prediction for observation \(i\) must come from the iteration in which \(i\) was held out.

$$ \operatorname{MSE}_{\text{pooled}} = \frac{1}{n}\sum_{i=1}^{n} \left(y_i-\hat{y}_i^{\text{OOF}}\right)^2, \qquad \operatorname{RMSE}_{\text{pooled}} = \sqrt{\operatorname{MSE}_{\text{pooled}}}. $$

If fold \(k\) has \(n_k\) held-out observations and fold MSE \(\operatorname{MSE}_k\), the pooled MSE is a size-weighted average of the fold MSEs. This follows by splitting the sum of squared errors into its fold-specific sums: fold \(k\) contributes \(n_k\operatorname{MSE}_k\), so the total squared error is \(\sum_k n_k\operatorname{MSE}_k\). Divide by the total number of observations, \(n=\sum_k n_k\), to obtain the result.

$$ \operatorname{MSE}_{\text{pooled}} = \frac{\sum_{k=1}^{K}n_k\operatorname{MSE}_k}{\sum_{k=1}^{K}n_k}. $$

When all folds have the same size, this is also the unweighted mean of the fold MSEs. When fold sizes differ, an unweighted average gives every fold equal influence even though the folds contain different numbers of observations. Also, the pooled RMSE is the square root of the pooled MSE; it is generally not the mean of the fold RMSEs.

Worked Example: Calculating Pooled RMSE From Held-Out Predictions

A hypothetical pricing team uses a four-fold intercept-only regression model to predict an outcome measured in thousands of dollars. The eight observed outcomes, in order, are 2, 4, 6, 8, 10, 12, 14, and 16. Assign consecutive pairs to folds: fold 1 contains outcomes 2 and 4, fold 2 contains 6 and 8, fold 3 contains 10 and 12, and fold 4 contains 14 and 16. Calculate the fold predictions and the pooled RMSE.

Method. An intercept-only least-squares model predicts the training-sample mean for every held-out observation. The total of all eight outcomes is \(72\). For each iteration, subtract the held-out outcomes from 72, divide by the six training observations, and use that mean as both predictions for the held-out pair.

Fold 1. The training total is \(72-(2+4)=66\), so each prediction is \(66/6=11\). The two squared errors are \((2-11)^2=81\) and \((4-11)^2=49\). Thus the fold sum of squared errors is \(130\) and the fold MSE is \(130/2=65\).

Fold 2. The training total is \(72-(6+8)=58\), so each prediction is \(58/6=29/3\). The squared errors are \((6-29/3)^2=121/9\) and \((8-29/3)^2=25/9\). Their sum is \(146/9\), giving fold MSE \(73/9\).

Fold 3. The training total is \(72-(10+12)=50\), so each prediction is \(50/6=25/3\). The squared errors are \((10-25/3)^2=25/9\) and \((12-25/3)^2=121/9\). Their sum is \(146/9\), giving fold MSE \(73/9\).

Fold 4. The training total is \(72-(14+16)=42\), so each prediction is \(42/6=7\). The squared errors are \((14-7)^2=49\) and \((16-7)^2=81\). The fold sum of squared errors is \(130\) and the fold MSE is \(65\).

The pooled sum of squared errors is \(130+146/9+146/9+130=2632/9\). Therefore the pooled MSE is \((2632/9)/8=329/9=36.5556\), and the pooled RMSE is \(\sqrt{329/9}=6.0461\) thousand dollars, rounded to four decimal places. As a check, the four fold MSEs have equal weight here, and their mean is \((65+73/9+73/9+65)/4=329/9\). Common mistake avoided: averaging the fold RMSEs instead of pooling squared errors and then taking the square root. The fold RMSE average here is approximately 5.4552, not the pooled RMSE of 6.0461.

Combining Classification Predictions

For classification, retain the held-out predicted probability for each observation, not just a fold-level score. If a class prediction is needed, apply a stated threshold to those probabilities after they have been generated. For example, a threshold rule may classify an observation as positive when its held-out predicted probability is at least 0.5. The pooled confusion matrix then counts outcomes and predictions across all held-out observations.

Worked Example: Combining Out-of-Fold Classification Results

A hypothetical renewal study has eight records with binary outcomes \(0,1,0,1,0,1,0,1\). Use four folds, each containing one 0 and one 1. In each iteration, fit an intercept-only logistic regression using the other six records. Use a threshold of at least 0.5 to classify an observation as positive. Find the pooled confusion matrix, accuracy, sensitivity, and specificity.

Method. In every training set, three outcomes are 0 and three are 1. The fitted intercept is the log odds, \(\log(3/3)=0\), so each held-out predicted probability is \(1/(1+\exp(0))=0.5\). Under the stated threshold rule, every held-out prediction is positive.

Across all eight held-out predictions, the four actual positives are true positives and the four actual negatives are false positives. Thus \(TP=4\), \(FP=4\), \(FN=0\), and \(TN=0\). Accuracy is \((TP+TN)/8=4/8=0.5\). Sensitivity is \(TP/(TP+FN)=4/4=1\), and specificity is \(TN/(TN+FP)=0/4=0\).

The result checks against the threshold rule: all eight observations were classified positive, so accuracy is 0.5 because half are actually positive, sensitivity is 1, and specificity is 0. Common mistake avoided: storing only the fold’s accuracy and losing the individual predictions. Out-of-fold predictions allow the confusion matrix and other pooled measures to be calculated directly from the complete set of held-out cases.

Implementation Checks and Common Traps

Before interpreting a cross-validation score, verify the mechanics. The following checks catch many avoidable errors:

  • One assignment per row: every observation must have a fold ID from 1 through \(K\).
  • One held-out prediction per row: the out-of-fold prediction vector should have no unfilled entries.
  • No held-out rows in their own fit: the training indices in iteration \(k\) must exclude every row in fold \(k\).
  • Metric units and aggregation: calculate the metric on held-out results, and account for fold sizes when combining fold-level averages.
  • Repeat the complete fitting procedure: any preprocessing estimated from data must be estimated within each training iteration and then applied to that iteration’s held-out fold.

A particularly common exam trap is to report the unweighted mean of fold RMSEs as if it were the pooled RMSE. Even with equal fold sizes, the mean of square roots is generally not the square root of the mean. For example, in the preceding regression example, the pooled MSE is \(329/9\), so the pooled RMSE is \(\sqrt{329/9}\). Averaging the fold RMSEs instead performs a different calculation. State which quantity is being reported and show how it was formed.

K-fold cross-validation is a procedure for generating held-out results, not a guarantee that any chosen fold design is suitable. The model, preprocessing, fold assignment, and performance measure must match the prediction task. The central mechanical rule remains: fit on \(K-1\) folds, predict the one excluded fold, and combine the held-out results without losing track of which observation each prediction belongs to. This workflow is aligned with the SOA Exam PA (Predictive Analytics) syllabus.

Check Your Understanding

For each problem, identify the relevant fold or metric calculation. Answers are included so you can check your work.

  1. A dataset has 11 observations and is divided into 4 folds as evenly as possible. What are the fold sizes? (Answer: 3, 3, 3, and 2 observations, in any order.)
  2. In a five-fold procedure, what fraction of the observations is used for training in each iteration when all folds have equal size? (Answer: \(4/5\), or 80%.)
  3. Two held-out folds have sizes 2 and 4, with MSE values 1 and 9, respectively. Calculate the pooled MSE and pooled RMSE. (Answer: pooled MSE \(=(2(1)+4(9))/6=38/6=6.3333\); pooled RMSE \(=\sqrt{38/6}=2.5166\), to four decimal places.)
  4. A three-fold evaluation has 15 observations. After all iterations, 14 positions in the out-of-fold prediction vector are filled. What should be checked first? (Answer: inspect the fold assignments and held-out indexing for a missing or misassigned observation; valid folds must cover all 15 rows exactly once.)
  5. For binary classification, the pooled held-out counts are \(TP=18\), \(FP=6\), \(FN=2\), and \(TN=14\). Calculate accuracy and sensitivity. (Answer: accuracy \(=(18+14)/40=0.8000\); sensitivity \(=18/(18+2)=0.9000\).)
Clinical Biostats