PolarsRows228Columns10 | ||||||||||
| lung: NCCTG advanced lung cancer, 228 patients | ||||||||||
inst i64 |
time i64 |
status i64 |
age i64 |
sex i64 |
ph.ecog i64 |
ph.karno i64 |
pat.karno i64 |
meal.cal i64 |
wt.loss i64 |
|
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 3 | 306 | 2 | 74 | 1 | 1 | 90 | 100 | 1175 | None |
| 1 | 3 | 455 | 2 | 68 | 1 | 0 | 90 | 90 | 1225 | 15 |
| 2 | 3 | 1010 | 1 | 56 | 1 | 0 | 90 | 90 | None | 15 |
| 3 | 5 | 210 | 2 | 57 | 1 | 1 | 90 | 60 | 1150 | 11 |
| 4 | 1 | 883 | 2 | 60 | 1 | 0 | 100 | 90 | None | 0 |
| 225 | 32 | 105 | 1 | 75 | 2 | 2 | 60 | 70 | 1025 | 5 |
| 226 | 6 | 174 | 1 | 66 | 1 | 1 | 90 | 100 | 1075 | 1 |
| 227 | 22 | 177 | 1 | 58 | 2 | 1 | 80 | 90 | 1060 | 0 |
Outcome
A reusable description of a survival response: column names and expressions, no data.
Usage
Outcome()An Outcome is the structured form of a formula’s left-hand side. Outcome.surv() takes the arguments of R’s Surv() and Outcome.event_time() those of event_time(), each written as it would be inside a formula: a column name, or an expression such as "status == 2". Pass the Outcome to any estimator’s fit method together with data=. The estimator reads every column it needs from that one frame, drops rows with missing values once (as R’s na.omit does), evaluates the expressions, and builds the response with Surv() or event_time().
Because an Outcome is made of names and expressions rather than DataFrame code, the same description works on any backend: pandas, Polars, PyArrow, DuckDB, or a lazy frame. That makes it, together with the equivalent formula strings, the recommended way to start from a data frame. Use Surv() or event_time() directly when the values are already in hand.
The expressions understood here and in formulas are:
- a column name (backticks quote unusual names inside an expression),
- a comparison:
status == 2,status != 0,status in (1, 2)(R’s%in% c(1, 2)works), orstatus not in (1, 2), factor(x, levels, labels), R’s factor, for a multi-state status whose first level means censored,duration(start, end, unit="days")for a time computed from two date columns.
Examples
We’ll use the bundled lung dataset, from a North Central Cancer Treatment Group trial in advanced lung cancer. time is days of follow-up and status is 1 (censored) or 2 (died). Here is what the dataset looks like:
Describe the endpoint once, with the same arguments Surv() takes:
import greenwood as gw
death = gw.Outcome.surv(time="time", event="status == 2")
deathOutcome.surv(time='time', event='status == 2')
Pass it to estimators along with the frame. Column names work for covariates and by= too:
lung = gw.load_dataset("lung")
cox = gw.CoxPH().fit(death, covariates=["age", "sex"], data=lung)
km = gw.KaplanMeier().fit(death, by="sex", data=lung)
coxCoxPH (Cox proportional hazards model, ties='efron')
coef exp(coef) se(coef) z p
age 0.01705 1.017 0.009223 1.848 0.06459
sex -0.5132 0.5986 0.1675 -3.065 0.002178
n = 228, events = 165
Likelihood ratio test = 14.12 on 2 df, p = 0.0008574
The same response written as a formula parses to an equal Outcome:
gw.Outcome.from_formula(formula="Surv(time, status == 2)") == deathTrue
Attributes
| Name | Description |
|---|---|
| arguments | The arguments of the response, by name, written as they would be in a formula. |
| column_names | The data columns this response reads, in the order they first appear. |
| endpoints |
For a first_event outcome, each endpoint’s time and event expressions.
|
| kind |
Which function builds the response: "surv", "event_time", or "first_event".
|
| origin |
The origin= passed to Surv(): a time subtracted from every time when bound.
|
| type |
The type= passed to Surv(), or None to let it be inferred when the data is bound.
|
arguments
The arguments of the response, by name, written as they would be in a formula.
arguments: dict[str, str]
For a surv outcome the names are those of Surv() that were given (time=, time2=, event=), and for an event_time outcome those of event_time() (time=, status=, time_max=). Each value is a column name or an expression such as "status == 2". As in R, Surv(time, x) without event= reads x as the status, so it is listed as event=. For a first_event outcome the endpoints are in endpoints, and this holds only censor_at= and start= when they were given. The type and origin have their own properties, Outcome.type and Outcome.origin.
Examples
import greenwood as gw
gw.Outcome.from_formula(formula="Surv(tstart, tstop, status == 2)").arguments{'time': 'tstart', 'time2': 'tstop', 'event': 'status == 2'}
column_names
The data columns this response reads, in the order they first appear.
column_names: tuple[str, …]
These are the columns named by every argument, including both columns of a duration() and the column inside a comparison or factor(). When a model is fitted from an Outcome and data=, rows with a missing value in any of these columns are dropped (together with any covariate columns) before the response is built.
Examples
import greenwood as gw
died = gw.Outcome.from_formula(formula="Surv(duration(enroll, exit), outcome == 'died')")
died.column_names('enroll', 'exit', 'outcome')
endpoints
For a first_event outcome, each endpoint’s time and event expressions.
endpoints: dict[str, tuple[str, str]] | None
The endpoints are in priority order, which decides ties: when two endpoints happen at the same time, the first one listed wins. Each value is a (time, event) pair of expression texts, as passed to Outcome.first_event(). None for other kinds of outcome.
Examples
import greenwood as gw
cr = gw.Outcome.first_event(
endpoints={"pcm": ("ptime", "pstat"), "death": ("futime", "death == 1")}
)
cr.endpoints{'pcm': ('ptime', 'pstat'), 'death': ('futime', 'death == 1')}
kind
Which function builds the response: "surv", "event_time", or "first_event".
kind: str
An Outcome from Outcome.surv() (or a Surv(...) formula) is bound with Surv(). One from Outcome.event_time() (or an event_time(...) formula) is bound with event_time() and then converted with as_surv(). One from Outcome.first_event() is bound with first_event(). Every kind produces a Surv when bound.
Examples
import greenwood as gw
gw.Outcome.from_formula(formula="event_time(time, code)").kind'event_time'
origin
The origin= passed to Surv(): a time subtracted from every time when bound.
origin: float
0 (the default) leaves the times unchanged. A non-zero origin shifts every time column, as R’s Surv(origin=) does. Always 0 for event_time and first_event outcomes.
Examples
import greenwood as gw
gw.Outcome.from_formula(formula="Surv(age_at_exit, died, origin=40)").origin40.0
type
The type= passed to Surv(), or None to let it be inferred when the data is bound.
type: str | None
One of R’s types: "right", "left", "interval", "counting", "interval2", or "mstate". None, the usual case, means Surv() infers right-censored or counting-process data from the arguments, or multi-state data from a factor() status. The type of the bound response is reported by Surv.type. Always None for event_time and first_event outcomes.
Examples
import greenwood as gw
gw.Outcome.surv(time="lower", time2="upper", type="interval2").type'interval2'
Methods
| Name | Description |
|---|---|
| bind() |
Build the Surv response from the columns of data.
|
| event_time() |
Describe an event_time() response by column names.
|
| first_event() |
Describe a first_event() competing-risks response by column names.
|
| from_formula() |
Parse a formula response such as "Surv(time, status == 2)" into an Outcome.
|
| surv() | Describe a Surv() response by column names and expressions. |
bind()
Build the Surv response from the columns of data.
Usage
bind(data)Each argument is evaluated against data on its own backend, and the results go through Surv(), through event_time() and then as_surv(), or through first_event(), depending on kind. bind() is what an estimator does internally when it is given an Outcome and data=, apart from one difference. bind() keeps rows with missing values, as Surv() does, while fitting first drops rows with a missing value in any column the model uses, so the response and the covariates stay aligned.
Use bind() to inspect the response a description produces, or to pass the Surv to a function that takes values rather than an Outcome.
Parameters
data: Any- A data frame (pandas, Polars, PyArrow, DuckDB, a lazy frame, …) or a mapping of column names to values. Only the columns the Outcome names are read.
Returns
Surv- The response for this data.
Raises
KeyError-
If a column named by the Outcome is not in
data. The message suggests the closest column name. TypeError-
If
datais not a data frame or a mapping, or Surv() orevent_time()rejects the values (for example, a text status column used without a comparison). ValueError-
If Surv(),
event_time(), orfirst_event()rejects the values.
Warns
UserWarning- When Surv() turns invalid values into missing ones, such as a status code R doesn’t accept. The affected rows are missing in the result.
Examples
We’ll use the bundled lung dataset, where status is 1 (censored) or 2 (died). R’s 1/2 coding passes through:
import greenwood as gw
lung = gw.load_dataset("lung")
death = gw.Outcome.surv(time="time", event="status")
y = death.bind(lung)
ySurv(type=right, n=228, events=165)
The bound response is an ordinary Surv, with R’s columns:
y.to_frame(format="polars").head(3)| time | status |
|---|---|
| f64 | f64 |
| 306.0 | 1.0 |
| 455.0 | 1.0 |
| 1010.0 | 0.0 |
event_time()
Describe an event_time() response by column names.
Usage
event_time(
time,
status,
time_max=None,
)event_time() writes each observation as a time and a one-letter status: "e" (exact event), "r" (right-censored), "l" (left-censored), or "i" (interval-censored, with the upper bound in a second time column). This constructor names the columns that hold those values. When the Outcome is bound to data, the columns are passed to event_time(), so its validation applies, and the result is converted to a Surv with as_surv().
This is the same description that the formula response "event_time(time, status)" gives.
Parameters
time: str | Duration-
The time column (the lower bound for interval-censored rows), or
duration(...)for a time computed from two date columns. status: str-
The column of status codes:
"e","r","l", or"i". time_max: str | Duration | None = None- The column of interval upper bounds, missing for rows that are not interval-censored. Needed only when some rows are interval-censored.
Returns
Outcome- A data-free description of the response.
Raises
TypeError-
If an argument is not a string or a
gw.duration()object.
Examples
A small study where the event time is known for some subjects and only bracketed for others:
import greenwood as gw
import polars as pl
visits = pl.DataFrame({
"time": [7.0, 5.0, 3.0, 2.0],
"code": ["e", "r", "l", "i"],
"upper": [None, None, None, 4.0],
})
observed = gw.Outcome.event_time(time="time", status="code", time_max="upper")
observed.bind(visits)Surv(type=interval, n=4, events=3)
Interval-censored responses fit with Turnbull:
gw.Turnbull().fit(observed, data=visits)Turnbull (self-consistent NPMLE for interval-censored data)
n atoms ambiguous iters converged
all 1 1 1 1 True
(3 observations deleted due to missingness)
first_event()
Describe a first_event() competing-risks response by column names.
Usage
first_event(
endpoints,
*,
censor_at=None,
start=None,
)Some datasets record each endpoint in its own pair of columns: a time and whether the endpoint was observed. A competing-risks analysis needs one time and one cause per subject: the first endpoint observed, or censoring if none was. This constructor names the column pairs. When the Outcome is bound to data, the pairs are evaluated and passed to first_event(), which keeps the earliest observed event for each row.
Parameters
endpoints: Mapping[str, tuple[str | Duration, str]]-
A mapping of state name to a
(time, event)pair, in priority order: when two endpoints happen at the same time, the one listed first wins. The time is a column name (orduration(...)), and the event is a column or a comparison such as"pstat == 1". censor_at: str | Duration | None = None-
The column holding the time at which rows with no observed event are censored. The default is the latest of the endpoint times.
start: str | Duration | None = None- The column of entry times, for late entry. Gives a multi-state counting-process response.
Returns
Outcome- A data-free description of the response.
Raises
TypeError-
If a time or event is not a string or a
gw.duration()object. ValueError-
If endpoints is empty, or an endpoint is not a
(time, event)pair.
Examples
We’ll use the bundled mgus2 dataset, which follows patients with monoclonal gammopathy. ptime and pstat record progression to plasma-cell malignancy (PCM), and futime and death record death:
import greenwood as gw
cr = gw.Outcome.first_event(
endpoints={"pcm": ("ptime", "pstat"), "death": ("futime", "death")}
)
crOutcome.first_event(endpoints={'pcm': ('ptime', 'pstat'), 'death': ('futime', 'death')})
Fit the cumulative incidence of each cause:
mgus2 = gw.load_dataset("mgus2")
gw.AalenJohansen().fit(cr, data=mgus2)AalenJohansen (Aalen-Johansen cumulative incidence)
states: pcm, death
n = 1384
final CIF
pcm 0.1613
death 0.8387
from_formula()
Parse a formula response such as "Surv(time, status == 2)" into an Outcome.
Usage
from_formula(formula)The response is a call to Surv() or event_time(), with the arguments R’s functions take: Surv(time, time2, event, type=, origin=) and event_time(time, status, time_max). Arguments bind by position or by name, as in R. Each one is a column name or one of the expressions listed under Outcome. Column names that aren’t valid Python identifiers are quoted with backticks, and dotted R names such as ph.ecog work without quoting.
The formula is parsed, not evaluated: only column names, literal values, comparisons, and the functions duration(), factor(), and c() are accepted. Estimators parse full formulas the same way, so fit("Surv(time, status == 2) ~ age", data=df) uses exactly this response.
Parameters
formula: str-
The response, optionally followed by
~ 1. To include covariates, pass the full formula to an estimator’s fit method instead.
Returns
Outcome- A data-free description of the response, equal to the one built with surv() or event_time() from the same arguments.
Raises
ValueError-
If the formula can’t be parsed, the response is not a call to Surv() or
event_time(), it has an unknown or repeated argument (such as the removedevent_value=), it has a right-hand side other than1, or an expression is not supported.
Examples
import greenwood as gw
gw.Outcome.from_formula(formula="Surv(time, status == 2)")Outcome.surv(time='time', event='status == 2')
It equals the Outcome built from the same arguments:
gw.Outcome.from_formula(formula="Surv(time, status == 2)") == gw.Outcome.surv(
time="time", event="status == 2"
)True
R’s other forms work too:
gw.Outcome.from_formula(formula="Surv(tstart, tstop, status %in% c(1, 2))")Outcome.surv(time='tstart', time2='tstop', event='status in (1, 2)')
gw.Outcome.from_formula(formula="event_time(time, code, upper)")Outcome.event_time(time='time', status='code', time_max='upper')
surv()
Describe a Surv() response by column names and expressions.
Usage
surv(
time,
time2=None,
event=None,
*,
type=None,
origin=0.0,
)The arguments are those of Surv() (and of R’s survival::Surv()), each written as it would be inside a formula: a column name, or an expression such as "status == 2". Nothing is read yet. When the Outcome is bound to data, at fit time or with bind(), each argument is evaluated on the data’s own backend and the results are passed to Surv(). R’s rules then apply unchanged:
1/2status coding passes through, and other codings are written as a comparison.- A second argument without
event=is read as the status, soOutcome.surv(time="t", time2="d")means the same asOutcome.surv(time="t", event="d"). type=selects left-censored or interval data. Without it, the type is inferred: right-censored from a time and a status, counting-process from a start and a stop time, and multi-state from afactor()status.
This is the same description that the formula response "Surv(time, status == 2)" gives, and the two compare equal.
Parameters
time: str | Duration-
The time column: the follow-up time for right-censored data, the start time for counting-process data, or the lower bound for interval data. Can also be
duration(...)(or agw.duration()object) for a time computed from two date columns. time2: str | Duration | None = None-
The stop time for counting-process data, or the upper bound for interval data. Without
event=, a second argument is the status, as in R. event: str | None = None-
The status: a column name, a comparison such as
"status == 2"or"status %in% c(1, 2)", or"factor(cause, c(0, 1, 2), c('censor', 'pcm', 'death'))"for a multi-state response whose first label means censored. type: str | None = None-
As in Surv():
"right","left","interval","counting","interval2", or"mstate". The default (None) infers the type from the arguments when the data is bound. origin: float = 0.0- A time subtracted from every time when the data is bound, as in Surv().
Returns
Outcome- A data-free description of the response.
Raises
TypeError-
If an argument is not a string or a
gw.duration()object. To build a response from values instead of column names, use Surv(). ValueError-
If
type=is not one of the types above, an expression uses an unsupported operator, or the status is a duration().
Examples
We’ll use the bundled pbc dataset, from a Mayo Clinic trial in primary biliary cholangitis. time is days of follow-up and status is 0 (censored), 1 (transplant), or 2 (died). Death is the event, so the status is a comparison:
import greenwood as gw
death = gw.Outcome.surv(time="time", event="status == 2")
deathOutcome.surv(time='time', event='status == 2')
Pass it to an estimator together with the data. Rows with a missing value in any column used are dropped first:
pbc = gw.load_dataset("pbc")
gw.KaplanMeier().fit(death, by="trt", data=pbc)KaplanMeier (Kaplan-Meier survival estimate)
n events median 0.95LCL 0.95UCL
trt=1 158 65 3282 2583 NA
trt=2 154 60 3428 3090 NA
(106 observations deleted due to missingness)
A factor() status gives a multi-state response, here with transplant and death as competing events:
competing = gw.Outcome.surv(
time="time",
event="factor(status, c(0, 1, 2), c('censor', 'transplant', 'death'))",
)
competing.bind(pbc)Surv(type=mright, n=418, events=186, states=('transplant', 'death'))