home

Train-Test Split

Illustration of the train-test split concept in machine learning. The diagram shows how a full dataset is divided into features (X) and target (y) matrices, which are then split into training and testing subsets. The training data (X_train, y_train) is used to train a machine learning model, while the test data (X_test, y_test) is used to evaluate the model's performance on unseen data. This separation helps assess how well the model generalizes to new examples. The diagram uses color coding to distinguish between training data (darker) and test data (lighter), with a neural network representation of the ML model.


Train-Test Split

Download

PNG PDF SVG

Code

train-test-split.typ (248 lines)

#import "@preview/cetz:0.5.2": canvas, draw
#import draw: circle, content, line, rect

#set page(width: auto, height: auto, margin: 5pt, fill: none)
#set text(size: 12pt)

#canvas({
  let data-color = rgb("#00bfbf")
  let data-color-alt = rgb("#00a8a8")
  let test-data-color = rgb("#80dfdf")
  let target-color = rgb("#ffcc00")
  let target-color-alt = rgb("#e6b800")
  let test-target-color = rgb("#ffe680")
  let (data-header, target-header) = (rgb("#008080"), rgb("#cc9900"))
  let arrow-style = (stroke: black + 2pt, mark: (end: "stealth", size: 10pt))
  let step-text-style = (fill: black, weight: "bold", size: 16pt)
  let label-text-style = (fill: black, weight: "bold", size: 18.2pt)
  let header-text-style = (fill: white, weight: "bold", size: 13pt)
  let neuron-text-style = (fill: black, weight: "bold", size: 12pt)
  let matrix-stroke = 0.5pt + rgb("#0099cc")

  let vertical-center = 0
  let label-offset = 0.7

  let full-data-width = 6
  let feature-width = 5
  let target-width = 1
  let header-height = 1.0
  let row-height = 1.0
  let dataset-rows = 7
  let full-data-height = header-height + dataset-rows * row-height

  let full-data-x = -15
  let features-x = -6
  let target-x = features-x + feature-width + 0.5
  let train-x = 6
  let test-x = train-x
  let test-y = -8.0
  let model-x = 18
  let top-tables-y-offset = -3.5
  let nn-y-offset = -4.5

  // rows selected for the test set (0-indexed), simulating random sampling
  let test-indices = (1, 4, 6)
  let test-rows = test-indices.len()
  let train-rows = dataset-rows - test-rows
  let feature-headers = ("X1", "X2", "X3", "X4", "X5")

  // color a data row: test rows are highlighted, the rest alternate two shades
  let striped(base, alt, highlight) = idx => {
    if test-indices.contains(idx) { highlight } else if calc.rem(idx, 2) == 0 {
      base
    } else { alt }
  }

  let tables = (
    (
      x: full-data-x,
      y: top-tables-y-offset,
      width: full-data-width,
      height: full-data-height,
      label: "Full Dataset",
      headers: feature-headers + ("Y",),
      fill: rgb("#cdd3da"),
      header-fill: rgb("#0099cc"),
    ),
    (
      x: features-x,
      y: top-tables-y-offset,
      width: feature-width,
      height: full-data-height,
      label: "Features",
      headers: feature-headers,
      fill: data-color,
      header-fill: data-header,
      rows: dataset-rows,
      row-color: striped(data-color, data-color-alt, test-data-color),
    ),
    (
      x: target-x,
      y: top-tables-y-offset,
      width: target-width,
      height: full-data-height,
      label: "Target",
      headers: ("Y",),
      fill: target-color,
      header-fill: target-header,
      rows: dataset-rows,
      row-color: striped(target-color, target-color-alt, test-target-color),
    ),
    ..{
      for (base_x, center_y, split, rows, feature_fill, target_fill) in (
        (train-x, vertical-center, [train], train-rows, data-color, target-color),
        (test-x, test-y, [test], test-rows, test-data-color, test-target-color),
      ) {
        for (offset, width, label, headers, fill, header_fill) in (
          (0, feature-width, [X], feature-headers, feature_fill, data-header),
          (feature-width + 0.5, target-width, [y], ("Y",), target_fill, target-header),
        ) {
          (
            (
              x: base_x + offset,
              y: center_y,
              width: width,
              height: header-height + rows * row-height,
              label: [#label#sub(split)],
              headers: headers,
              fill: fill,
              header-fill: header_fill,
              rows: rows,
              row-color: idx => fill,
            ),
          )
        }
      }
    },
  )

  for (x, y, width, height, label, ..) in tables {
    content(
      (x + width / 2, y + height / 2 + label-offset),
      text(..label-text-style)[#label],
      anchor: "center",
    )
  }

  for spec in tables {
    let (x, y, width, height, headers) = spec
    let top = y + height / 2
    rect((x, top), (x + width, y - height / 2), stroke: matrix-stroke, fill: spec.fill)
    rect(
      (x, top),
      (x + width, top - header-height),
      stroke: matrix-stroke,
      fill: spec.header-fill,
    )
    for (idx, header) in headers.enumerate() {
      content(
        (x + 0.5 + idx, top - header-height / 2),
        text(..header-text-style)[#header],
        anchor: "center",
      )
    }
    for idx in range(int((height - header-height) / row-height) + 1) {
      let row-y = top - header-height - idx * row-height
      line((x, row-y), (x + width, row-y), stroke: matrix-stroke)
    }
    for idx in range(int(width) + 1) {
      line((x + idx, top - header-height), (x + idx, y - height / 2), stroke: matrix-stroke)
    }
    for idx in range(spec.at("rows", default: 0)) {
      let row-top = top - header-height - idx * row-height
      rect(
        (x, row-top),
        (x + width, row-top - row-height),
        stroke: matrix-stroke,
        fill: (spec.row-color)(idx),
      )
    }
  }

  // === Neural network ===
  let nn-x = model-x
  let nn-y = vertical-center + nn-y-offset
  let (nn-width, nn-height) = (6, 6)
  let neuron-radius = 0.65
  let input-x = nn-x - nn-width / 3
  let output-x = nn-x + nn-width / 3

  content(
    (nn-x, nn-y + nn-height / 2 + 1.2),
    text(..label-text-style)[ML Model],
    anchor: "center",
  )

  let neuron(x, y, name) = {
    circle((x, y), radius: neuron-radius, fill: rgb("#aaddff"), stroke: none)
    content((x, y), text(..neuron-text-style)[#name], anchor: "center")
  }
  let input-y = idx => nn-y - 2 + idx * 2
  let hidden-y = idx => nn-y - 3 + idx * 2

  for idx in range(3) { neuron(input-x, input-y(idx), "i" + str(idx + 1)) }
  for idx in range(4) { neuron(nn-x, hidden-y(idx), "h" + str(idx + 1)) }
  neuron(output-x, nn-y, "o")

  for idx in range(3) {
    for jdx in range(4) {
      line(
        (input-x + neuron-radius, input-y(idx)),
        (nn-x - neuron-radius, hidden-y(jdx)),
        stroke: black + 0.5pt,
      )
    }
  }
  for jdx in range(4) {
    line(
      (nn-x + neuron-radius, hidden-y(jdx)),
      (output-x - neuron-radius, nn-y),
      stroke: black + 0.5pt,
    )
  }

  // === Step arrows ===
  let step-arrow(start, end, label, label-offset) = {
    line(start, end, ..arrow-style)
    if label != "" {
      content(
        ((start.at(0) + end.at(0)) / 2, (start.at(1) + end.at(1)) / 2 + label-offset),
        text(..step-text-style)[#label],
        anchor: "center",
      )
    }
  }

  step-arrow(
    (full-data-x + full-data-width + 0.5, top-tables-y-offset),
    (features-x - 0.5, top-tables-y-offset),
    [1. Arrange\ data],
    1.2,
  )

  // one arrow head fans out to both the train and the test table
  let split-from = (target-x + target-width + 0.5, top-tables-y-offset)
  step-arrow(split-from, (train-x - 0.5, vertical-center + .5), "", 0)
  step-arrow(split-from, (test-x - 0.5, test-y), "", 0)
  content(
    (target-x + target-width + 3.0, split-from.at(1)),
    text(..step-text-style)[2. Train-Test\ Split],
    anchor: "center",
  )

  let tables-right = feature-width + 0.5 + target-width + 0.5
  let model-left = input-x - neuron-radius - 0.5
  step-arrow(
    (train-x + tables-right, vertical-center),
    (model-left, nn-y + 1),
    [3. Use for\ training],
    2,
  )
  step-arrow(
    (test-x + tables-right, test-y),
    (model-left, nn-y - 1),
    [4. Use for\ testing],
    -2.2,
  )
})