# Pitfalls: zyplib.dataset Common Mistakes and Best Practices

## 1. Data leakage (segment-level vs file-level split)

**The most common mistake.** When data comes from EEG files (one file = one subject), segment-level splitting causes segments from the same subject to appear in both train and val sets.

### Wrong approach

```python
# Load all data first, then split at segment level -> data leakage!
X, y = basic_pipeline(all_files, ...)
train_loader, val_loader = make_split_dataloader((X, y), split_size=[0.8, 0.2])
```

Different time windows from the same subject will be scattered across train and val. The model may learn subject-specific features rather than task-relevant features.

### Correct approach

```python
# Split at file level, then load separately -> no leakage
X_train, X_val, X_test, y_train, y_val, y_test = train_test_pipeline(
    split_size=(0.7, 0.15, 0.15),
    all_files=(asd_files, td_files),
    random_seed=42,
    fn_pipeline=my_load_fn,
)
```

### Decision rule

| Scenario | Recommended approach |
|----------|---------------------|
| One file = one subject | File-level split (`train_test_pipeline` / `kfold_pipeline`) |
| Data is already independent samples (e.g. MNIST) | Segment-level split is acceptable |
| Uncertain | Default to file-level split |

## 2. make_kfold_dataloader vs kfold_pipeline

Two easily confused K-fold APIs:

| API | Split level | Input | Use case |
|-----|-------------|-------|----------|
| `kfold_pipeline` | **File-level** | File list | EEG/subject data (recommended) |
| `make_kfold_dataloader` | **Segment-level** | (X, y) or Dataset | Independent sample data |

The source code comment in `make_kfold_dataloader` explicitly states:

> Note: this performs K-fold after importing XY data. In general, splitting should happen at file import time. Prefer using `kfold_pipeline` from `make_xy_pipeline`.

## 3. fn_encode_label default behavior

Both `fileseg_pipeline` and `fileloader_pipeline` default `fn_encode_label` to `to_onehot(label, squeeze_2_class=True)`:

- Binary classification: labels are reshaped to `[N, 1]` (values 0 or 1, **not** squeezed to 1D)
- Multi-class: labels become one-hot `[N, n_classes]`

If your model expects a different label format (e.g. integer class indices), pass a custom `fn_encode_label`:

```python
# Keep original integer labels
X, y = basic_pipeline(files, ..., fn_encode_label=lambda labels: np.array(labels))
```

Note: `fn_encode_label=None` does NOT skip encoding. The source code uses `fn_encode_label or _default_encode_label`, so `None` falls through to the default. To truly skip encoding, pass an identity function:

```python
fn_encode_label=lambda labels: np.asarray(labels)
```

## 4. make_basic_dataloader return type

```python
# Single dataset -> returns a single DataLoader (not a tuple)
loader = make_basic_dataloader((X, y), batch_size=64)

# Multiple datasets -> returns a tuple
train_loader, val_loader = make_basic_dataloader(
    (X_train, y_train), (X_val, y_val), batch_size=64
)
```

Tuple-unpacking a single-dataset call will raise an error.

## 5. Shuffle misconfiguration

### Common mistake: shuffling the validation set

```python
# Wrong: all loaders shuffle
loaders = make_basic_dataloader(..., shuffle=True)

# Correct: only train shuffles
loaders = make_basic_dataloader(..., shuffle='shuffle-first')
```

### Shuffle depends on argument order

`'shuffle-first'` assumes the first dataset passed is the training set. If the order is wrong, shuffle will be applied to the wrong loader.

```python
# Correct order: train first
train_loader, val_loader = make_basic_dataloader(
    (X_train, y_train), (X_val, y_val), shuffle='shuffle-first'
)

# If order is uncertain, use explicit list
val_loader, train_loader = make_basic_dataloader(
    (X_val, y_val), (X_train, y_train), shuffle=[False, True]
)
```

## 6. dtype mismatch

`make_basic_dataloader` defaults both data and target to `torch.float32`.

If your model expects `long` targets (e.g. `CrossEntropyLoss`), specify explicitly:

```python
loader = make_basic_dataloader(
    (X, y),
    dtype=(torch.float32, torch.long),  # data=float32, target=long
)
```

## 7. fn_pipeline parameter modes in train_test_pipeline

`fn_pipeline` has two usage modes that are easily confused:

```python
# Mode 1: Pass a custom function (recommended)
# Signature must be (files: Sequence[str]) -> (ndarray, ndarray)
train_test_pipeline(..., fn_pipeline=my_load_fn)

# Mode 2: Use a string preset + pipeline_kwargs
train_test_pipeline(
    ...,
    fn_pipeline='fileseg',
    pipeline_kwargs=dict(fn_read=read_mat, fn_segment=seg, label=0),
)
```

Mode 1 is more flexible (supports caching, custom logic). Mode 2 is more concise. Do not mix them — passing a function while also providing `pipeline_kwargs` has no effect.

## 8. random_split vs torch.utils.data.random_split

`zyplib.dataset.random_split` returns actual data slices (ndarray / list), while `torch.utils.data.random_split` returns `Subset` objects.

```python
from zyplib.dataset import random_split

# Returns actual ndarray slices
X_train, X_test = random_split(X, [0.8, 0.2], seed=42)
type(X_train)  # ndarray

# torch version returns Subset
from torch.utils.data import random_split as torch_split
subset_a, subset_b = torch_split(dataset, [0.8, 0.2])
type(subset_a)  # Subset
```

## Best-practice combination quick reference

| Scenario | Pipeline | Split | DataLoader |
|----------|----------|-------|------------|
| EEG fixed split | `train_test_pipeline` + `'fileseg'` | File-level | `make_basic_dataloader` |
| EEG K-fold | `kfold_pipeline` + custom function | File-level | `make_basic_dataloader` |
| In-memory (X, y) training | None | `random_split` manual split | `make_basic_dataloader` |
| In-memory (X, y) K-fold | None | Segment-level | `make_kfold_dataloader` |
| Custom file format | `train_test_pipeline` + `'simple'` | File-level | `make_basic_dataloader` |
| With data augmentation | Any | Any | `BasicXyDataset` + `transform` |
