# Python ML/AI Rules

Full coding rules for this stack — covers scikit-learn, PyTorch, TensorFlow/Keras, and experiment tracking (MLflow / wandb / DVC). Read this in full before writing or modifying any ML/AI code in this project — not just once, keep applying it to every edit in the session, not only the first.

---

## Project Structure

```
project/
├── data/                # raw/, interim/, processed/ (git-ignored, DVC-tracked)
├── notebooks/           # EDA only — not productionized code
├── src/
│   ├── data/            # loading + splitting
│   ├── features/        # feature engineering (fit on train only)
│   ├── models/          # model definitions + training entrypoints
│   └── eval/            # metric + evaluation harness
├── configs/             # YAML/Hydra hyperparameter configs
├── experiments/         # tracked run outputs / logs
├── models/              # saved artifacts (DVC/registry-tracked)
├── requirements.txt     # pinned
└── pyproject.toml
```

---

## Reproducibility Rules

- Set **all seeds** before any randomness: `random.seed(n)`, `numpy.random.seed(n)`, and the framework seed.
- Enable deterministic flags where feasible; document any performance trade-off.
- **Pin the environment:** `requirements.txt` or a lockfile committed alongside every artifact.
- Log the seed, config, and git commit SHA with every tracked run.

---

## Config Management

- Hyperparameters live in YAML / Hydra / argparse configs — **never hardcoded**.
- The config file is saved alongside every model artifact.

---

## Leakage Prevention

- **Split before fit** — perform the split BEFORE fitting any transformer, scaler, or encoder.
- **Fit transforms on train fold only** — all preprocessing lives inside a `Pipeline` fit only on training data.
- **Time-aware splits** — for temporal data, use `TimeSeriesSplit`; never shuffle time-indexed data.

---

## Framework Idioms

### scikit-learn

- Use `Pipeline` + `ColumnTransformer` for all preprocessing — never transform outside a pipeline.
- Use `set_output(transform="pandas")` (sklearn ≥ 1.2) to preserve feature names.

### PyTorch

- Seed the `DataLoader` worker init function for full reproducibility.
- Always switch between `model.train()` and `model.eval()` modes; use `torch.no_grad()` for validation.

### TensorFlow / Keras

- Use callbacks for checkpointing (save best by validation metric) and early stopping.
- Set `tf.random.set_seed` at startup.

### HuggingFace (LLM fine-tuning)

- Use a freeze/unfreeze schedule; start with a small learning rate and add warmup.
- Log the base model name, adapter config, and dataset version alongside the run.

---

## Experiment Tracking

- Log params, metrics per epoch/fold, seed, data version, and git commit to MLflow or wandb.
- Name each run per the scheme in `custom/rules/ml-conventions.md`: `[ticket-id]_[approach]_[yyyymmdd-n]`.
- Version datasets with DVC; store the DVC data hash in the run metadata.

---

## Notebook Hygiene

- `notebooks/` is for EDA and exploration only — not production code.
- Productionized logic (features, models, eval) migrates to `src/` as importable modules.
- Notebooks must not import from each other; shared utilities go to `src/`.
- Clear outputs before committing notebooks (or use `nbstripout`).

---

## Testing Rules

- Use **Pytest** for all `src/` utilities.
- Test the **metric harness** directly: assert known inputs produce the expected metric value.
- Test **data-split functions** for leakage: verify no sample ID appears in both train and test sets.
- Test feature-engineering functions independently with synthetic data.

---

## Naming Conventions

| Element | Convention | Example |
|---------|-----------|---------|
| Module/package | snake_case | `feature_engineering.py` |
| Class | PascalCase | `TemporalSplit`, `TrainConfig` |
| Function/variable | snake_case | `train_model`, `val_score` |
| Constant | UPPER_SNAKE_CASE | `DEFAULT_SEED`, `MAX_EPOCHS` |
| Experiment run | `[ticket-id]_[approach]_[yyyymmdd-n]` | `ML-42_lgbm-tfidf_20241115-1` |

---

## Common Anti-Patterns to Avoid

- ❌ Fitting scalers/encoders on the full dataset before splitting — always fit inside a `Pipeline` on the train fold only
- ❌ Hardcoded hyperparameters — use configs
- ❌ No seeds — always set `random`, `numpy`, and framework seeds
- ❌ Evaluating on validation data used for tuning — keep a held-out test set for final Gate 4 reporting
- ❌ Production logic in notebooks — migrate to `src/`
- ❌ Committing data files or model binaries to git — track with DVC or a registry
- ❌ Single aggregate metric without error analysis — always inspect failure cases and slices
