# CSV Format

sleap-io supports multiple CSV formats for exporting pose tracking data. Each format has different structures suited for various analysis workflows.

## Supported Formats

| Format | Description | Best For |
|--------|-------------|----------|
| `sleap` | SLEAP Analysis CSV | Native SLEAP exports, one row per instance |
| `dlc` | DeepLabCut format | DLC compatibility, multi-header structure |
| `points` | One row per point | Database imports, normalized data |
| `instances` | One row per instance | Analysis pipelines |
| `frames` | One row per frame | Time series analysis, wide format |

## Format Details

### SLEAP Format (`sleap`)

The default SLEAP Analysis CSV format produces one row per instance.

**Columns:**

| Column | Type | Description |
|--------|------|-------------|
| `track` | string | Track name (empty if untracked) |
| `frame_idx` | int | Frame index in video |
| `instance.score` | float | Instance confidence score |
| `{node}.x` | float | X coordinate for node |
| `{node}.y` | float | Y coordinate for node |
| `{node}.score` | float | Point confidence score |

**Example:**

```csv
track,frame_idx,instance.score,nose.x,nose.y,nose.score,tail.x,tail.y,tail.score
mouse_1,0,0.95,100.5,200.3,0.98,150.2,250.1,0.92
mouse_1,1,0.94,102.1,198.5,0.97,148.8,248.3,0.91
mouse_2,0,0.93,300.2,400.1,0.96,350.5,450.8,0.89
```

### DeepLabCut Format (`dlc`)

Multi-header format compatible with DeepLabCut analysis tools.

**Structure:**

- Row 1: Scorer name (repeated)
- Row 2: Body part names (repeated for x, y, likelihood)
- Row 3: Coordinate type (x, y, likelihood)
- Data rows: One per frame

**Example:**

```csv
scorer,MyModel,MyModel,MyModel,MyModel,MyModel,MyModel
bodyparts,nose,nose,nose,tail,tail,tail
coords,x,y,likelihood,x,y,likelihood
0,100.5,200.3,0.98,150.2,250.1,0.92
1,102.1,198.5,0.97,148.8,248.3,0.91
```

**Multi-animal DLC format** adds an `individuals` row:

```csv
scorer,MyModel,MyModel,MyModel,MyModel,MyModel,MyModel
individuals,mouse_1,mouse_1,mouse_1,mouse_2,mouse_2,mouse_2
bodyparts,nose,nose,nose,nose,nose,nose
coords,x,y,likelihood,x,y,likelihood
0,100.5,200.3,0.98,300.2,400.1,0.96
```

### Points Format (`points`)

The most normalized format with one row per point. Ideal for database imports.

**Columns:**

| Column | Type | Description |
|--------|------|-------------|
| `video_path` | string | Path to video file |
| `frame_idx` | int | Frame index |
| `track` | string | Track name |
| `instance_idx` | int | Instance index within frame |
| `instance_score` | float | Instance confidence |
| `node` | string | Node/keypoint name |
| `x` | float | X coordinate |
| `y` | float | Y coordinate |
| `score` | float | Point confidence |

**Example:**

```csv
video_path,frame_idx,track,instance_idx,instance_score,node,x,y,score
video.mp4,0,mouse_1,0,0.95,nose,100.5,200.3,0.98
video.mp4,0,mouse_1,0,0.95,tail,150.2,250.1,0.92
video.mp4,0,mouse_2,1,0.93,nose,300.2,400.1,0.96
video.mp4,0,mouse_2,1,0.93,tail,350.5,450.8,0.89
```

### Instances Format (`instances`)

One row per instance with all node coordinates as columns.

**Columns:**

| Column | Type | Description |
|--------|------|-------------|
| `video_path` | string | Path to video file |
| `frame_idx` | int | Frame index |
| `track` | string | Track name |
| `instance_idx` | int | Instance index within frame |
| `instance_score` | float | Instance confidence |
| `{node}.x` | float | X coordinate for node |
| `{node}.y` | float | Y coordinate for node |
| `{node}.score` | float | Point confidence for node |

**Example:**

```csv
video_path,frame_idx,track,instance_idx,instance_score,nose.x,nose.y,nose.score,tail.x,tail.y,tail.score
video.mp4,0,mouse_1,0,0.95,100.5,200.3,0.98,150.2,250.1,0.92
video.mp4,0,mouse_2,1,0.93,300.2,400.1,0.96,350.5,450.8,0.89
video.mp4,1,mouse_1,0,0.94,102.1,198.5,0.97,148.8,248.3,0.91
```

### Frames Format (`frames`)

One row per frame with all instances multiplexed into columns. Best for time series analysis.

**Columns:**

| Column | Type | Description |
|--------|------|-------------|
| `frame_idx` | int | Frame index |
| `video_path` | string | Path to video file |
| `inst{N}.{node}.x` | float | X coordinate for instance N, node |
| `inst{N}.{node}.y` | float | Y coordinate for instance N, node |
| `inst{N}.{node}.score` | float | Point confidence for instance N, node |

**Example:**

```csv
frame_idx,video_path,inst0.nose.x,inst0.nose.y,inst0.nose.score,inst0.tail.x,inst0.tail.y,inst0.tail.score,inst1.nose.x,inst1.nose.y,inst1.nose.score,inst1.tail.x,inst1.tail.y,inst1.tail.score
0,video.mp4,100.5,200.3,0.98,150.2,250.1,0.92,300.2,400.1,0.96,350.5,450.8,0.89
1,video.mp4,102.1,198.5,0.97,148.8,248.3,0.91,,,,,,
```

!!! note "Empty Frames"
    When `all_frames=True` (or `include_empty=True`), frames without instances are included with NaN values for all coordinates.

!!! note "Points that are not visible"
    Points marked as not visible (e.g. occluded body parts) are written with NaN coordinates in every format, matching `Instance.numpy()` and the Analysis HDF5 export. Loading the CSV back reads these points as not visible.

## Frame Padding

By default, CSV export only includes frames with instances. To include all frames:

```python
import sleap_io as sio

labels = sio.load_slp("predictions.slp")

# Include empty frames (padded with NaN)
sio.save_csv(labels, "all_frames.csv", include_empty=True)

# Only frames with instances (sparse)
sio.save_csv(labels, "sparse.csv", include_empty=False)

# Specific frame range
sio.save_csv(labels, "clip.csv", start_frame=100, end_frame=500)
```

When `include_empty=True` and no explicit `end_frame` is given, padding spans the
full video length when it is known (e.g. instances stopping at frame 1000 in a
2000-frame video still export rows through frame 1999). This matches the Analysis
HDF5 export. If the video length cannot be resolved, padding falls back to the last
labeled frame.

## Metadata Sidecar

CSV files cannot store all Labels information (skeleton edges, symmetries, suggestions). To enable full round-trip reconstruction, use `save_metadata=True`:

```python
sio.save_csv(labels, "data.csv", save_metadata=True)
# Creates: data.csv and data.json
```

### Metadata JSON Structure

```json
{
    "version": "1.0",
    "videos": [
        {
            "filename": "video.mp4",
            "backend_metadata": {}
        }
    ],
    "skeletons": [
        {
            "name": "Skeleton-0",
            "nodes": ["nose", "head", "neck", "tail"],
            "edges": [[0, 1], [1, 2], [2, 3]],
            "symmetries": []
        }
    ],
    "tracks": ["mouse_1", "mouse_2"],
    "suggestions": [],
    "provenance": {}
}
```

| Field | Description |
|-------|-------------|
| `version` | Metadata schema version (currently `"1.0"`) |
| `videos` | Video metadata; each entry has `filename` and a backend-specific `backend_metadata` object (e.g. `shape`, `dataset`, `grayscale`) |
| `skeletons` | List of skeleton definitions, each with `nodes`, `edges`, and `symmetries` |
| `tracks` | Track names in order |
| `suggestions` | Suggested frame indices |
| `provenance` | Source-file / creation metadata, mirroring `labels.provenance` |

## Loading CSV Files

```python
import sleap_io as sio

# Load CSV (auto-detects format)
labels = sio.load_csv("data.csv")

# If metadata sidecar exists, it's loaded automatically
# data.json provides skeleton edges, symmetries, etc.
```

## CLI Usage

```bash
# Export to CSV
sio export predictions.slp -o analysis.csv

# Specify format
sio export predictions.slp -o dlc.csv --csv-format dlc --scorer MyModel

# Include all frames
sio export predictions.slp -o all.csv --empty-frames

# Frame range
sio export predictions.slp -o clip.csv --start 100 --end 500

# With metadata sidecar
sio export predictions.slp -o data.csv --save-metadata
```

## API Reference

::: sleap_io.io.main.load_csv
    options:
      heading_level: 3
      show_root_toc_entry: false

::: sleap_io.io.main.save_csv
    options:
      heading_level: 3
      show_root_toc_entry: false
