> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encodebox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Which API?

> Choose EventFrame, Cohort, or Table/Column for the same query.

EncodeBox has three ways to write the same query. Prefer the highest level that fits; drop down only when you need to.

| Name               | Use when                               | How you get it                 |
| ------------------ | -------------------------------------- | ------------------------------ |
| **EventFrame**     | Find events (codes, gaps, first/last)  | `db.diagnosis`                 |
| **Cohort**         | Define a study population + attrition  | `eb.Cohort(name, database=db)` |
| **Table / Column** | Custom predicates verbs cannot express | `db.diagnosis.as_table()`      |

```text theme={null}
Cohort        who is in the study (entry → include → exclude)
EventFrame    which events qualify (match → occur → pick one row)
Table/Column  raw boolean filters on columns
──────────────────────────────────────────────────────────────
              same IR → same SQL → DataFrame
```

## Setup

```python theme={null}
import encodebox as eb

eb.register_connector("connectors/postgres_local_sentinel/connector.yml")
db = eb.connect(
    "postgres",
    host="localhost",
    port=5433,
    database="encodebox_sample",
    username="postgres",
    password="postgres",
    connector="postgres_local_sentinel",
)
```

With `connector=`, you get a **Database**. Domains are attributes: `db.diagnosis`, `db.procedure`, `db.medication`, `db.enrollment`, …

## EventFrame — finding events

An **EventFrame** is a lazy list of clinical events (one row ≈ one code on one date for one patient). You chain verbs; nothing hits the database until `.to_df()`.

### Typical index-date chain

```python theme={null}
index = (
    db.diagnosis
    .matching("E11", code_type="10", match="starts_with")
    .occurring(at_least=2, gap_days=30)
    .with_index_date()
    .first_per_patient()
)
```

"Patients with at least two E11\* diagnoses on different days, with at least 30 days between some consecutive visits; use the start of the first such pair as their index date."

### EventFrame verbs

| Verb                                           | Meaning                                                                                                                                                           |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `matching(codes, code_type=…, match=…)`        | Keep rows whose code matches. `"E11"` prefix, `["A","B"]` = either code, `[["A"],["B"]]` = UNION of two code sets.                                                |
| `in_position("primary")`                       | Keep only primary (or non-primary) diagnosis positions.                                                                                                           |
| `occurring(at_least=n, at_most=m, gap_days=d)` | Keep **patients** who have enough events. Counts **distinct dates** by default. `gap_days` requires a **consecutive** day gap (`lead()`), not first-to-last span. |
| `with_index_date()`                            | Expose `index_date` (after a gap rule: first qualifying consecutive-pair start).                                                                                  |
| `first_per_patient()` / `last_per_patient()`   | Keep one row per patient. After `occurring(gap_days=…)`, "first" is gap-aware.                                                                                    |
| `between(start, end)` / `in_years(2020)`       | Calendar filter on `event_date`.                                                                                                                                  |
| `with_age(...)` / `with_followup(...)`         | Attach age or follow-up time columns.                                                                                                                             |
| `union(other)`                                 | Stack two event streams.                                                                                                                                          |
| `label("name")`                                | Name a step (shows in repr / explain / attrition).                                                                                                                |
| `to_sql()` / `to_df()` / `explain()`           | Inspect SQL, run query, or show the IR tree.                                                                                                                      |

### Gap semantics

`gap_days=30` means: look at ordered **distinct** dates and require some **neighboring** pair to be ≥30 days apart.

| Patient dates        | Span (max−min) | Max consecutive gap | `gap_days=30` |
| -------------------- | -------------- | ------------------- | ------------- |
| Jan 1, 11, 21, 31    | 30 days        | 10 days             | **out**       |
| Jan 1, Jan 16, Mar 1 | 60 days        | 45 days             | **in**        |

<Warning>
  Legacy `filter_event(..., gap=30)` used **span** (max−min). New `gap_days` uses consecutive gaps. They do not select the same patients.
</Warning>

## Cohort — defining a study population

A **Cohort** starts from an entry EventFrame (people + index date), then **requires** or **excludes** criteria. Each step is a set operation on `patient_id`. `.attrition()` counts how many people remain after each step.

```python theme={null}
t1dm = (
    db.diagnosis
    .matching("E10", code_type="10", match="starts_with")
    .occurring(at_least=2, gap_days=30)
    .with_index_date()
    .first_per_patient()
)

cohort = (
    eb.Cohort("t2dm", database=db)
    .entry(index, index="first")
    .include(db.demographic.age_at_index(18, 89))
    .include(db.enrollment.covering_index(days_before=365, days_after=0))
    .include(t1dm, window=(-365, 0))
    .exclude(db.diagnosis.matching("C", match="starts_with"), window=(-365, 0))
)

print(cohort.attrition())
final = cohort.to_df()
```

### Cohort building blocks

| Piece                           | Meaning                                                                  |
| ------------------------------- | ------------------------------------------------------------------------ |
| `.entry(frame, index="first")`  | Starting population and each person's **index date**. Called once.       |
| `.include(source, window=…)`    | Inclusion: age / enrollment / EventFrame (optional day window vs index). |
| `.exclude(source, window=…)`    | Exclusion: remove patients who meet the criterion.                       |
| `.attrition()`                  | Remaining / dropped counts at each labeled step.                         |
| `.spec()` / `eb.from_spec(...)` | Serialize / restore the definition.                                      |

### Built-in criteria

| Criterion                                                   | Meaning                                                         |
| ----------------------------------------------------------- | --------------------------------------------------------------- |
| `db.diagnosis.matching(...)` on include/exclude             | Event-based patient set (optional `window=` relative to index). |
| `db.enrollment.covering_index(days_before=a, days_after=b)` | Continuous enrollment covering `[index−a, index+b]`.            |
| `db.demographic.age_at_index(lo, hi)`                       | Age at index in `[lo, hi]`.                                     |
| `db.demographic.sex(...)` / `.race(...)`                    | Demographic match at index.                                     |
| `db.death.recorded(window=…)`                               | Death recorded (optionally in a window).                        |

Windows are always `(days_before, days_after)` relative to the cohort entry index date. Example: `window=(-365, 0)` = the year ending on the index day.

## Table / Column — custom predicates

```python theme={null}
dx = db.diagnosis.as_table()
t2dm = dx.filter(dx.code.startswith("E11") & (dx.code_type == "10"))
frame = t2dm.as_events()
```

Use column expressions when EventFrame verbs cannot say what you mean. Prefer EventFrame for matching / occurrence / ranking.

## Inspect anything

| Method         | Meaning                                     |
| -------------- | ------------------------------------------- |
| `print(obj)`   | Show the pipeline steps.                    |
| `.explain()`   | Show the IR tree / SQL fragments.           |
| `.to_sql()`    | Full SQL without running it.                |
| `.to_df()`     | Run the query.                              |
| `.attrition()` | Cohort: remaining and dropped at each step. |

## Legacy API

`query_event` / `filter_event` still import but warn. Their gap rule is **span**, not consecutive days — see [Migration](/migration).
