---
title: "Quick start: from a metadata table to a split_spec in ten minutes"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Quick start: from a metadata table to a split_spec in ten minutes}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

splitGraph turns a sample-level metadata table into a **typed dependency
graph**, validates it, derives a **split constraint** (which samples must stay
on the same side of any train/test split), and emits a tool-agnostic
**`split_spec`** that downstream resampling tools consume. It never creates
folds itself.

This is the shortest complete path. Each step links to the vignette that goes
deeper.

```{r load}
library(splitGraph)
```

## 1. A metadata table

One row per sample. Only `sample_id` is required; every other canonical column
is optional, and absent ones are simply skipped.

```{r meta}
meta <- data.frame(
  sample_id    = paste0("S", 1:8),
  subject_id   = c("P1", "P1", "P2", "P2", "P3", "P3", "P4", "P4"),
  batch_id     = c("B1", "B1", "B2", "B2", "B3", "B3", "B3", "B4"),
  site_id      = c("NYC", "NYC", "NYC", "NYC", "BOS", "BOS", "BOS", "BOS"),
  timepoint_id = rep(c("T0", "T1"), 4),
  time_index   = rep(c(0, 1), 4),
  outcome_id   = c("case", "case", "ctrl", "ctrl", "case", "case", "ctrl", "ctrl"),
  stringsAsFactors = FALSE
)
```

Four subjects, two samples each. Note batch `B3`: it holds samples from *two*
different subjects, which matters in step 3.

`graph_from_metadata()` auto-detects these columns:

| Canonical column | Creates | Available as |
|---|---|---|
| `sample_id` (required) | `Sample` nodes | every mode |
| `subject_id` | `Subject` + `sample_belongs_to_subject` | `mode = "subject"`, and the basis of `"relatedness"` |
| `batch_id` | `Batch` + `sample_processed_in_batch` | `mode = "batch"` |
| `study_id` | `Study` + `sample_from_study` | `mode = "study"` |
| `site_id` | `Site` + `sample_collected_at_site` | `mode = "site"` |
| `region_id` | `Region` + `sample_located_in_region` | `mode = "region"` |
| `platform_id` | `Platform` + `sample_run_on_platform` | `mode = "platform"` |
| `assay_id` | `Assay` + `sample_measured_by_assay` | `mode = "assay"` |
| `timepoint_id` (with `time_index`) | `Timepoint` + `sample_collected_at_timepoint` (+ `timepoint_precedes`) | `mode = "time"`, and `order_rank` |
| `featureset_id` | `FeatureSet` + `sample_uses_featureset` | not a constraint mode; feeds the shared-provenance advisory and the dependency queries |
| `outcome_id` or `outcome_value` | `Outcome` + `sample_has_outcome` | the `stratum` annotation |

Identifier columns may be character, factor, or numeric; they are coerced to
character. If your columns are named differently, map them with `columns =`,
for example `columns = c(subject_id = "donor", batch_id = "run")`. Relations
that have no column, such as genetic relatedness or spatial proximity, are
built from their own helpers and added to the graph; see
`vignette("modeling-structure")`.

## 2. Build and validate

```{r build}
g <- graph_from_metadata(meta, graph_name = "quick-start")
g
validate_graph(g)
```

Validation runs three layers, and each issue carries a severity. **Structural**
problems (a dangling edge, a duplicate id) are errors and stop the build.
**Semantic** problems (a sample assigned to two subjects, a `time_index` that
contradicts the precedence edges) are errors or warnings. **Leakage** findings
are warnings or advisories: a subject appearing in two studies or at two sites
is a warning, while repeated measures of one subject are an advisory. This
cohort produces four of those advisories, one per subject, which is exactly the
structure the split has to respect.

Use `levels =` and `severities =` to narrow the report, and
`validate_graph(g, error_on_fail = TRUE)` to stop on any error.

Nothing has decided a split yet. The report describes what the data contains.

## 3. Choose a constraint mode

| Your question | `mode` | What ends up in one group |
|---|---|---|
| Same individual measured several times? | `"subject"` | all samples of a subject |
| Processing batch, plate, or run effects? | `"batch"` | all samples of a batch |
| Several studies or cohorts pooled? | `"study"` | all samples of a study |
| Multi-centre collection? | `"site"` | all samples of a site |
| Tissue region, sequencing platform, assay? | `"region"`, `"platform"`, `"assay"` | likewise |
| Longitudinal, and train must precede test? | `"time"` | samples of a timepoint, plus an `order_rank` |
| Genetic relatives or spatial neighbours? | `"relatedness"`, `"spatial"` | connected components over thresholded edges; see `?pairwise_edges` |
| Several of these at once? | `"composite"` | `strict`: one group per connected component over every relation in `via` (any of the above, including the pairwise ones). `rule_based`: the first relation in `priority` that the sample has |

Start with the one relation you are most sure about. Here that is the subject:

```{r derive-subject}
subject_constraint <- derive_split_constraints(g, mode = "subject")
subject_constraint
grouping_vector(subject_constraint)
```

`grouping_vector()` returns the group per sample as a named character vector,
which is the handle most resampling tools want. The full table, with the reason
each sample landed where it did, is one call away:

```{r constraint-table}
head(as.data.frame(subject_constraint)[, c("sample_id", "group_id", "explanation")], 2)
```

If more than one relation has to be respected, combine them. Grouping by
subject *and* batch gives three groups rather than four, because batch `B3`
links subjects `P3` and `P4`, so they cannot be separated without splitting a
batch:

```{r derive-composite}
composite_constraint <- derive_split_constraints(
  g, mode = "composite", via = c("subject", "batch")
)
grouping_vector(composite_constraint)
```

That is the trade-off to watch: each relation you add can only merge groups,
never split them. With a few large batches a strict composite can collapse the
whole cohort into one group, which leaves nothing to hold out.
`vignette("faq-design-notes")` shows when that happens and what to do instead.

## 4. Emit the split_spec

```{r spec}
spec <- as_split_spec(subject_constraint, graph = g)
spec
```

Passing `graph = g` enriches the spec with everything the constraint did not
use as the primary grouping, so a downstream tool can block, order, or
stratify without ever touching the graph:

```{r spec-roles}
spec$group_var    # the split unit
spec$block_vars   # coarser axes that ideally should not straddle a fold
spec$time_var     # ordering, when the graph carries one
spec$stratum_var  # the outcome level each sample has
```

Those names point into one sample-level table:

```{r spec-table}
as.data.frame(spec)[, c("sample_id", "group_id", "batch_group", "site_group",
                        "stratum", "order_rank")]
```

The spec is checked before it leaves R:

```{r spec-validate}
validate_split_spec(spec)
```

The `stratum` column is an *annotation*: it records which outcome level each
sample carries so a consumer can stratify. splitGraph never balances folds
itself.

## 5. See which leakage paths the choice closes

`summarize_leakage_risks()` folds the graph validation, the constraint, and the
spec into one object. The `severed` column is the useful part: it says whether
the mode you picked structurally eliminates each finding.

```{r risks}
risks <- summarize_leakage_risks(g, constraint = subject_constraint, split_spec = spec)
risks
unique(as.data.frame(risks)[, c("category", "severity", "severed")])
```

## 6. Hand off

Write the spec to JSON for another session, another language, or an archive.
The format is a versioned contract with a JSON Schema shipped in the package:

```{r write, eval = requireNamespace("jsonlite", quietly = TRUE)}
path <- tempfile(fileext = ".json")
write_split_spec(spec, path)

validate_split_spec_json(path)$valid

# Reading with validate = TRUE re-checks the file against the schema and runs
# the preflight validator, instead of trusting whatever is on disk.
back <- read_split_spec(path, validate = TRUE)
identical(back$sample_data$group_id, spec$sample_data$group_id)
```

In R, the reference consumer is bioLeak, which turns the spec into an
executable, leakage-audited split plan:

```{r bioleak, eval = FALSE}
bioLeak::as_leaksplits(spec, data = my_frame, outcome = "y")
```

The released bioLeak (0.3.8) accepts the `subject`, `batch`, `study` and
`time` modes. For the others, including `composite`, hand it the grouping
directly; the split is identical, only the route differs:

```{r bioleak-workaround, eval = FALSE}
joined <- merge(my_frame, spec$sample_data[, c("sample_id", "group_id")],
                by = "sample_id")
bioLeak::make_split_plan(joined, outcome = "y",
                         mode = "subject_grouped", group = "group_id")
```

In Python, the reader ships with the package and needs only the standard
library:

```python
from splitspec import load_split_spec

spec = load_split_spec("split_spec.json")
spec.groups()       # group per sample, for GroupKFold(groups=...)
spec.strata()       # stratum per sample, for StratifiedGroupKFold(y=...)
spec.order_ranks()  # sort by this before TimeSeriesSplit
```

## Where to go next

- `vignette("leakage-aware-workflow")` — the same path in full, with querying,
  validation overrides, every constraint mode, and graph editing.
- `vignette("modeling-structure")` — site, region, platform, assay, and the
  thresholded relatedness and spatial relations.
- `vignette("cross-language-handoff")` — R to JSON to Python to scikit-learn.
- `vignette("adapter-cookbook")` — writing your own adapter.
- `vignette("case-study-gse60424")` — a real public cohort end to end.
- `vignette("faq-design-notes")` — why the design is shaped this way.

```{r cleanup, include = FALSE}
if (exists("path")) unlink(path)
```
