> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ovalbee.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ovalbee.com/_mcp/server.

# Train/validation split

> Splits a dataset's images, with their annotations, into train and validation datasets and an optional test dataset, either copying them or moving them out of the source.

## Overview

Splits a dataset's images, with their annotations, into train and validation datasets, plus a test dataset when you turn on **Add a test split** or select one.
**Operation mode** decides whether the images stay in the source (**Copy**) or leave it (**Move**).

The train and validation datasets don't need to exist beforehand.
Left empty, each is the node's own output dataset, `Train split`, `Val split` or `Test split`, and a rerun
writes a new version of each instead of creating another set. Images keep the split they got on
earlier runs, and only new source images are placed. The test split exists only when **Add a test split** is on
or a test dataset is selected.

Use it to split the data before training.

```mermaid
flowchart LR
  source["Dataset"] --> split["Train/validation split"]
  split --> train["Train Dataset"]
  split --> val["Val Dataset"]
```

## Prerequisites

| Requirement           | What you need                                                                        |
| --------------------- | ------------------------------------------------------------------------------------ |
| **Source dataset**    | Existing [Dataset](/node-library/nodes/dataset-ops-dataset) with at least one image. |
| **Separate datasets** | A different dataset for the source and for each destination.                         |
| **Available images**  | Enough images for every enabled split to hold at least one.                          |

## How it works

Uses the destination datasets you select, keeping everything they hold.
For a split not given, it takes its own output
dataset for that split — created on the first run, a new version of it on a rerun.
With **Keep previous split** on, the new version starts from the images the previous one held.

Treats the source images no split holds as new.
Images already in a split stay there, so an image never moves from validation to train.

**Shuffle data** randomizes the new images by default.
Without a test set, training gets `80%` by default and validation gets the rest.
With a test set, the default is `70/15/15` for train, validation, and test.
All three values must be positive integers that add up to `100`.

Places the new images so the split totals come as close to those ratios as they can. For example, 10 images at `70/15/15` produce `7/2/1`, and 3 new images added to an `8/2` split at `80/20` bring it to `10/3`. Each enabled split must end up with at least one image.

**Copy** keeps the image and annotation memberships in the source.
**Move** removes those memberships from the source, but the assets and files stay in OvalBee.
Both modes add the images and their source annotations to the destinations. An image the source no longer holds keeps the annotation from the previous version of the split.

## FAQ

#### What data can this node split?

It splits images only. Videos stay in the source, and the node does not balance the split by class. There is no setting for a random seed, so splitting the whole source again with **Keep previous split** off can give a different split.

#### Why is a ratio rejected or a split empty?

The ratio format or total is invalid, or too few unassigned images remain after rounding. Use valid percentages and make sure every enabled split gets at least one image.

#### Why did a rerun leave existing images where they were?

An image stays in the split it was first placed in, so models trained on different runs are validated on the same images. When you change the ratios, only new images move the totals toward the new ratios. If no new images remain, a rerun returns selected datasets unchanged.

#### What does a rerun do with the node's own split datasets?

Each rerun writes a new version of every split dataset the node created. With **Keep previous split** on, it holds the previous split plus the new images, and earlier versions stay in the history. Images are never removed from it, in **Copy** and **Move** alike, even when the source no longer holds them, so a source that holds only the latest batch still builds up one split. Turn **Keep previous split** off to ignore earlier splits and split the whole source again.

#### Does a rerun remove images from datasets I selected?

No. A dataset you selected keeps everything it holds, and **Keep previous split** does not apply to it. The ratios count every image it holds.

#### What metadata do destination datasets keep?

Each destination receives the source class list without losing its existing classes. A split dataset the node created is pinned to the source's ontology, while a destination you selected keeps its own ontology, widened with the source's classes.

## Inputs

**`From dataset`** `dataset` — required

Dataset whose images are split. Images already in a split stay where they are, so a rerun only places what is new. Key: `SRC_DATASET`.

---

**`Train dataset`** `dataset`

Where the train images go. Leave empty to use the node's own train output dataset. Key: `TRAIN_DATASET`.

---

**`Validation dataset`** `dataset`

Where the validation images go. Leave empty to use the node's own validation output dataset. Key: `VAL_DATASET`.

---

**`Add a test split`** `boolean` — default: false

Split into train, validation and test instead of train and validation only. The test images go to the node's own test output dataset unless one is chosen below. Key: `CREATE_TEST_SPLIT`.

---

**`Test dataset`** `dataset`

Where the test images go. Choosing one adds the test split even with the switch above off. Leave empty to use the node's own test output dataset. Key: `TEST_DATASET`.

---

**`Train split ratio (%)`** `integer` — required, default: 80

Share of images the train split should hold. New images are placed so the splits come as close to it as they can, and validation gets the rest. Minimum: `1`. Maximum: `100`. Step: `1`. Visible when `CREATE_TEST_SPLIT` is `False`. Key: `TRAIN_SPLIT_RATIO`.

---

**`Split ratios (%)`** `string` — required, default: 70/15/15

Three slash-separated percentages for train, validation, and test, for example 70/15/15. Split values must sum to 100. New images are placed so the splits come as close to them as they can. Visible when `CREATE_TEST_SPLIT` is `True`. Key: `SPLIT_RATIOS`.

---

**`Shuffle data`** `boolean` — required, default: true

Mix the images up before cutting them into splits. Turn this off to split them in the order they come in. Key: `RANDOM`.

---

**`Operation mode`** `select` — required, default: copy

Copy leaves the images in the source dataset as well. Move takes them out of it. Key: `OPERATION_MODE`.

Options:

* Move (`move`)
* Copy (`copy`)

---

**`Keep previous split`** `boolean` — required, default: true

On a rerun, the node's own splits keep every image they hold and only new source images are added. Turn this off to ignore earlier splits and split the whole source again. Datasets you selected always keep what they hold. Key: `KEEP_PREVIOUS_SPLIT`.

---

## Outputs

**`Train Dataset`** `dataset`

ID of the train dataset. Shown as an artifact. Key: `TRAIN_DATASET`.

---

**`Val Dataset`** `dataset`

ID of the validation dataset. Shown as an artifact. Key: `VAL_DATASET`.

---

**`Test Dataset`** `dataset`

ID of the test dataset. Shown as an artifact. Key: `TEST_DATASET`.

---

## JSON config

Machine-readable node interface for automation and advanced usage.

#### Show JSON

```json
{
  "name": "Train/validation split",
  "description": "Splits a dataset's images, with their annotations, into train and validation datasets and an optional test dataset, either copying them or moving them out of the source.",
  "category": "Sample",
  "namespace": "ovalbee",
  "templateKey": "sample/train_validation_split",
  "version": "v1",
  "inputs": [
    {
      "key": "SRC_DATASET",
      "label": "From dataset",
      "type": "dataset",
      "description": "Dataset whose images are split. Images already in a split stay where they are, so a rerun only places what is new.",
      "required": true,
      "default": null,
      "visibleWhen": null,
      "options": {
        "creatable": false
      }
    },
    {
      "key": "TRAIN_DATASET",
      "label": "Train dataset",
      "type": "dataset",
      "description": "Where the train images go. Leave empty to use the node's own train output dataset.",
      "required": false,
      "default": null,
      "visibleWhen": null
    },
    {
      "key": "VAL_DATASET",
      "label": "Validation dataset",
      "type": "dataset",
      "description": "Where the validation images go. Leave empty to use the node's own validation output dataset.",
      "required": false,
      "default": null,
      "visibleWhen": null
    },
    {
      "key": "CREATE_TEST_SPLIT",
      "label": "Add a test split",
      "type": "boolean",
      "description": "Split into train, validation and test instead of train and validation only. The test images go to the node's own test output dataset unless one is chosen below.",
      "required": false,
      "default": false,
      "visibleWhen": null
    },
    {
      "key": "TEST_DATASET",
      "label": "Test dataset",
      "type": "dataset",
      "description": "Where the test images go. Choosing one adds the test split even with the switch above off. Leave empty to use the node's own test output dataset.",
      "required": false,
      "default": null,
      "visibleWhen": null
    },
    {
      "key": "TRAIN_SPLIT_RATIO",
      "label": "Train split ratio (%)",
      "type": "number",
      "description": "Share of images the train split should hold. New images are placed so the splits come as close to it as they can, and validation gets the rest.",
      "required": true,
      "default": 80,
      "visibleWhen": {
        "key": "CREATE_TEST_SPLIT",
        "operator": "equals",
        "value": false
      },
      "options": {
        "min": 1,
        "max": 100,
        "step": 1,
        "type": "integer"
      }
    },
    {
      "key": "SPLIT_RATIOS",
      "label": "Split ratios (%)",
      "type": "string",
      "description": "Three slash-separated percentages for train, validation, and test, for example 70/15/15. Split values must sum to 100. New images are placed so the splits come as close to them as they can.",
      "required": true,
      "default": "70/15/15",
      "visibleWhen": {
        "key": "CREATE_TEST_SPLIT",
        "operator": "equals",
        "value": true
      }
    },
    {
      "key": "RANDOM",
      "label": "Shuffle data",
      "type": "boolean",
      "description": "Mix the images up before cutting them into splits. Turn this off to split them in the order they come in.",
      "required": true,
      "default": true,
      "visibleWhen": null
    },
    {
      "key": "OPERATION_MODE",
      "label": "Operation mode",
      "type": "select",
      "description": "Copy leaves the images in the source dataset as well. Move takes them out of it.",
      "required": true,
      "default": "copy",
      "visibleWhen": null,
      "options": {
        "options": [
          {
            "label": "Move",
            "value": "move"
          },
          {
            "label": "Copy",
            "value": "copy"
          }
        ]
      }
    },
    {
      "key": "KEEP_PREVIOUS_SPLIT",
      "label": "Keep previous split",
      "type": "boolean",
      "description": "On a rerun, the node's own splits keep every image they hold and only new source images are added. Turn this off to ignore earlier splits and split the whole source again. Datasets you selected always keep what they hold.",
      "required": true,
      "default": true,
      "visibleWhen": null
    }
  ],
  "outputs": [
    {
      "key": "TRAIN_DATASET",
      "label": "Train Dataset",
      "type": "dataset",
      "description": "ID of the train dataset.",
      "artifact": true,
      "badge": false,
      "badgeOpens": null,
      "hidden": false
    },
    {
      "key": "VAL_DATASET",
      "label": "Val Dataset",
      "type": "dataset",
      "description": "ID of the validation dataset.",
      "artifact": true,
      "badge": false,
      "badgeOpens": null,
      "hidden": false
    },
    {
      "key": "TEST_DATASET",
      "label": "Test Dataset",
      "type": "dataset",
      "description": "ID of the test dataset.",
      "artifact": true,
      "badge": false,
      "badgeOpens": null,
      "hidden": false
    }
  ]
}
```