---
title: "Getting started with lineager"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Getting started with lineager}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

You build an analysis dataset. Along the way rows disappear — filtered
out, dropped in joins, excluded by criteria. Later someone asks:
*"Which records were removed, why, and what happened to row 42 between
source and analysis?"*

Without `lineager`, that answer requires manual reconstruction.
With `lineager`, it is a single function call.

`lineager` tags every row of every dataset with a unique lineage
identifier that survives filters, joins, and derivations. Every row
removal requires a documented reason. At any point, `lg_trace()` returns
any row's complete journey across the pipeline. `lg_report()` compiles
everything into a structured provenance document.

## 1. Start a session

All `lineager` state lives in a session store reset by `lg_start()`.
Call it once at the top of your analysis.

```{r start}
lg_start(study_id = "PROJECT-001", analysis_id = "primary")
```

The optional `study_id` and `analysis_id` appear in reports. They can be
any strings — or omitted entirely.

## 2. Tag your source datasets

`lg_tag()` is the entry point. It assigns a `lineage_id` (lineage ID) to
every row at position 1. This ID persists through all operations.

```{r tag-basic}
patients <- data.frame(
  USUBJID = c("P001", "P002", "P003", "P004", "P005", "P006"),
  age = c(34L, 19L, 52L, 28L, 61L, 44L),
  group = c("A", "B", "A", "B", "A", "B"),
  eligible = c(TRUE, FALSE, TRUE, TRUE, FALSE, TRUE),
  stringsAsFactors = FALSE
)

tagged <- lg_tag(patients,
  dataset_id = "PATIENTS",
  label = "Patient registry"
)

tagged
```

The lineage ID format embeds the dataset ID and a zero-padded sequence.
When a `USUBJID` column is present (CDISC datasets), it is also embedded
for human readability:

```
PATIENTS_000001   ← non-CDISC: dataset + sequence
DM_0001_01-042    ← CDISC: dataset + sequence + USUBJID
```

### Tagging multiple datasets

Tag all source datasets before any transformations:

```{r tag-multiple}
labs <- data.frame(
  USUBJID = c("P001", "P001", "P003", "P004", "P006"),
  test = c("ALT", "AST", "ALT", "ALT", "ALT"),
  value = c(28.4, 31.2, 45.1, 22.8, 38.6),
  stringsAsFactors = FALSE
)

labs_tagged <- lg_tag(labs, dataset_id = "LABS", label = "Laboratory results")

cat("Patients tagged:", nrow(tagged), "rows\n")
cat("Labs tagged:    ", nrow(labs_tagged), "rows\n")
```

## 3. Derive new variables

`lg_derive()` works like `dplyr::mutate()` but requires a `description`
argument that is recorded in the operation log.

```{r derive-basic}
derived <- lg_derive(tagged,
  age_group = ifelse(age >= 40L, ">=40", "<40"),
  adult = age >= 18L,
  description = "age_group: >=40 vs <40 from age; adult: age >= 18"
)

derived[, c("USUBJID", "age", "age_group", "adult")]
```

The `lineage_id` column is preserved unchanged through derivations:

```{r lid-check}
all(derived[["lineage_id"]] == tagged[["lineage_id"]])
```

Chain derivations naturally:

```{r derive-chain}
derived2 <- lg_derive(derived,
  label = paste0(USUBJID, " (", group, ")"),
  description = "Display label combining USUBJID and group"
)

derived2[, c("lineage_id", "USUBJID", "group", "label")]
```

Each call adds one `DERIVE` operation to the session log.

## 4. Join datasets with lineage tracking

`lg_join()` performs a tracked join. It preserves `lineage_id` from the
left dataset and adds `lineage_id_y` to record which rows from the right
dataset contributed — enabling bilateral tracing.

```{r join-left}
joined <- lg_join(tagged, labs_tagged,
  by          = "USUBJID",
  type        = "left",
  description = "Merge ALT lab values from LABS onto PATIENTS"
)

joined[, c("lineage_id", "USUBJID", "eligible", "test", "value", "lineage_id_y")]
```

The `lineage_id_y` column shows which LABS row contributed to each PATIENTS
row. Rows with no matching lab record have `NA` in `lineage_id_y`.

Supported join types:

```{r join-types, eval = FALSE}
lg_join(x, y, by = "USUBJID", type = "left") # all rows of x
lg_join(x, y, by = "USUBJID", type = "inner") # only matching rows
lg_join(x, y, by = "USUBJID", type = "full") # all rows of both
lg_join(x, y, by = "USUBJID", type = "right") # all rows of y
```

`"left"` and `"full"` never drop rows of `x`, so `description` stays
optional for them. `"inner"` and `"right"` can drop unmatched `x` rows —
the moment a drop actually happens, `lg_join()` requires a `description`
(used as the exclusion reason for those dropped rows) and errors if one
isn't supplied. If no rows end up dropped, `description` remains optional
even for `"inner"`/`"right"`.

## 5. Filter with mandatory exclusion reasons

`lg_filter()` works like `dplyr::filter()` but the `reason` argument is
**mandatory with no default**. Every row removal must be documented.

Excluded rows and their IDs are captured in the session exclusion
registry automatically.

```{r filter-basic}
eligible_only <- lg_filter(tagged,
  eligible == TRUE,
  reason = "Not eligible for analysis (eligible != TRUE)"
)

cat("Before:", nrow(tagged), "\n")
cat("After: ", nrow(eligible_only), "\n")
```

Optional arguments enrich the exclusion record:

| Argument | Purpose | Example |
|---|---|---|
| `reason` | Why these rows are excluded (required) | `"Under age threshold"` |
| `reason_code` | Short controlled-vocabulary code | `"UNDERAGE"` |
| `population` | Which population/cohort this relates to | `"ANALYSIS_SET"` |

```{r filter-enriched}
# reason_code and population enrich the exclusion record
step1 <- lg_filter(tagged,
  eligible == TRUE,
  reason = "Screening criteria not met (eligible != TRUE)",
  reason_code = "SCREEN_FAIL",
  population = "ELIGIBLE_SET"
)

step2 <- lg_filter(step1,
  age >= 18L,
  reason = "Under minimum age threshold (age < 18)",
  reason_code = "UNDERAGE",
  population = "ADULT_SET"
)

cat("Enrolled: ", nrow(tagged), "\n")
cat("Eligible: ", nrow(step1), "\n")
cat("Adult:    ", nrow(step2), "\n")
```

## 6. The session operation log

Every `lg_derive()`, `lg_join()`, and `lg_filter()` call adds an entry
to the session operation log. Retrieve it with `lg_operations()`:

```{r operations}
ops <- lg_operations()
ops[, c("op_id", "op_type", "description", "rows_in", "rows_out")]
```

The operation log is the backbone of the provenance report — it shows
the complete sequence of transformations applied to the data.

`lg_operations()` returns the log for the **whole session** — every
dataset, every operation. When you only want the history behind one
specific object, `lg_history()` returns just the operations that produced
it, in order:

```{r history}
lg_history(step2)
```

`step2` above went through two `lg_filter()` calls (eligibility, then
age), so `lg_history(step2)` returns both of those operation records —
useful when you have several derived objects in scope and want to check
what actually produced a particular one, without filtering the full
session log by hand.

## 7. End the session

```{r end}
lg_end()
```

`lg_end()` marks the session inactive and prints a summary. The store
remains in memory and is still queryable via `lg_trace()`,
`lg_exclusions()`, etc. until `lg_start()` is called again.

## 8. Complete minimal workflow

```{r complete}
lg_start(study_id = "DEMO")

# Source data
raw <- data.frame(
  id = sprintf("P%03d", 1:8),
  value = c(12.4, NA, 8.1, 15.2, 9.8, NA, 11.3, 7.4),
  group = rep(c("treatment", "control"), 4),
  include = c(TRUE, TRUE, FALSE, TRUE, TRUE, TRUE, FALSE, TRUE),
  stringsAsFactors = FALSE
)

# Tag, derive, filter
ds <- lg_tag(raw, dataset_id = "RAW", label = "Raw analysis dataset")

ds <- lg_derive(ds,
  log_value = log(value),
  value_cat = ifelse(!is.na(value) & value >= 10, "high", "low/missing"),
  description = "Log-transform value; categorise as high (>=10) vs low/missing"
)

ds_clean <- ds |>
  lg_filter(include == TRUE,
    reason = "Excluded by study protocol (include != TRUE)"
  ) |>
  lg_filter(!is.na(value),
    reason = "Missing primary endpoint value"
  )

cat("Rows after cleaning:", nrow(ds_clean), "\n")

# Visualise the pipeline
lin <- lg_lineage()
print(lin)

lg_end()
```

## 9. Visualise the pipeline

`lg_lineage()` builds a complete lineage graph of the pipeline —
every dataset, operation, and exclusion branch — and returns a Graphviz
DOT string. Render it inline or export to a file.

```{r lineage-demo}
lg_start()
raw <- lg_tag(
  data.frame(
    USUBJID = sprintf("P%02d", 1:6),
    group = rep(c("A", "B"), 3L),
    flag = c(TRUE, TRUE, FALSE, TRUE, FALSE, TRUE),
    stringsAsFactors = FALSE
  ),
  dataset_id = "RAW"
)
raw <- lg_derive(raw,
  group_n = ifelse(group == "A", 1L, 2L),
  description = "Numeric group code"
)
lg_filter(raw, flag == TRUE, reason = "Flag not set")

lin <- lg_lineage()
print(lin)
lg_end()
```

```{r lineage-plot, eval = FALSE}
# Render inline (requires DiagrammeR)
lg_plot(lin)

# Export DOT file for Graphviz / online renderers
lg_plot(lin, output = "outputs/pipeline.dot")
```

**Node colour legend:**

| Colour | Shape | Meaning |
|---|---|---|
| Blue | Box | Source dataset from `lg_tag()` |
| Yellow | Ellipse | `lg_derive()` operation |
| Green | Diamond | `lg_join()` operation |
| Orange | Ellipse | `lg_filter()` operation |
| Red label | — | Rows excluded at that filter step |
| White | Box | Dataset state after each operation |

The next two vignettes cover exclusion tracking and reporting in depth:

- `vignette("exclusion-tracking")` — `lg_filter()`, `lg_exclusions()`,
  `lg_disposition()`, and `lg_trace()` with detailed examples
- `vignette("populations-and-reporting")` — `lg_population()`,
  `lg_spec()`, `lg_report()`, and `lg_lineage()` / `lg_plot()` for
  structured documentation and pipeline visualisation
