Surv

A survival response, built exactly as R’s survival::Surv() builds one.

Usage

Source

Surv(
    time,
    time2=None,
    event=None,
    *,
    type=None,
    origin=0.0,
)

Surv() records a time-to-event outcome in one of R’s forms. The type is inferred from the arguments, as in R:

  • Surv(time, event) is right-censored. event= may be logical, 0/1, or 1/2 (as in the lung data, where 2 means died).
  • Surv(start, stop, event) is a counting-process response, for late entry (left truncation) and time-varying covariates.
  • A categorical event= (a pandas or Polars categorical, or an Arrow dictionary) gives a multi-state response, whose first level means censored.
  • type="left", type="interval", and type="interval2" must be asked for.

Status values R doesn’t accept become missing, with a warning. Missing values are kept, and rows with any missing value are dropped when a model is fitted. For exact, right-, left-, and interval-censored data, event_time() is often clearer, and as_surv() converts it.

Surv() takes values: lists, arrays, or series already in hand. To start from a data frame, name the columns instead, in a formula such as "Surv(time, status == 2) ~ age" or with Outcome.surv(time="time", event="status == 2"), and pass data= when fitting. That form is evaluated on the frame’s own backend, so it works the same on pandas, Polars, PyArrow, DuckDB, and lazy frames.

Parameters

time: Any

Follow-up time for right-censored data, or the start time for counting-process data, or the lower bound for interval data. Numeric, with None or nan for missing values.

time2: Any = None

Stop time for counting-process data, or the upper bound for interval data. When event= is not given and type= is not an interval type, a second argument is the status (R’s rule), so Surv(time, status) works positionally.

event: Any = None

The status. Logical, 0/1, or 1/2 for right, left, and counting data. Codes 0 (right), 1 (exact), 2 (left), and 3 (interval) for type="interval". A categorical for a multi-state response.

type: str | None = None

One of "right", "left", "interval", "counting", "interval2", or "mstate". The default infers "right" or "counting" from the number of arguments.

origin: float = 0.0
Subtracted from every time, as in R.

Raises

TypeError

If a time or the status isn’t of a type R accepts. The messages match R’s.

ValueError
If lengths differ, or the arguments don’t fit the requested type.

Warns

UserWarning
When status values are invalid or intervals run backwards. Those rows become missing, as in R.

Examples

Four subjects: events at times 5 and 4, censored at 6 and 9:

import greenwood as gw

gw.Surv(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
Surv(type=right, n=4, events=2)

1/2 status coding works as it does in R, where 2 means an event:

gw.Surv(time=[5, 6, 4, 9], event=[2, 1, 2, 1])
Surv(type=right, n=4, events=2)

A start time gives a counting-process response:

gw.Surv(time=[0, 2, 1], time2=[5, 6, 4], event=[1, 0, 1])
Surv(type=counting, n=3, events=2)

A categorical status gives a multi-state response, with the first level meaning censored:

import pandas as pd

outcome = pd.Categorical(
    ["death", "censor", "relapse", "death"], categories=["censor", "relapse", "death"]
)
gw.Surv(time=[5, 6, 7, 8], event=outcome)
Surv(type=mright, n=4, events=3, states=('relapse', 'death'))

Interval data uses R’s coding, or lower and upper bounds with type="interval2":

import numpy as np

# The event happened in (1, 2], after 2, and in (3, 5]
gw.Surv(time=[1, 2, 3], time2=[2, np.inf, 5], type="interval2")
Surv(type=interval, n=3, events=2)

Starting from a data frame, name the columns and let the model read them. We’ll use the bundled pbc dataset, from a Mayo Clinic trial in primary biliary cholangitis, where status is 0 (censored), 1 (transplant), or 2 (died):

pbc = gw.load_dataset("pbc")

# The same as Surv(time, status == 2) in R, evaluated on the frame's own backend
gw.CoxPH().fit("Surv(time, status == 2) ~ age + bili", data=pbc)
CoxPH (Cox proportional hazards model, ties='efron')

         coef  exp(coef)  se(coef)       z          p
age   0.04313      1.044  0.008055   5.354  8.594e-08
bili   0.1436      1.154   0.01142  12.578    2.8e-36

n = 418, events = 161
Likelihood ratio test = 126.1 on 2 df, p = 4.097e-28

Attributes

Name Description
entry The time each row enters the risk set, defined for every type.
event Whether each row ended in an event, as a boolean array.
is_multistate Whether the response has more than one kind of event.
is_truncated Whether the response has start times (late entry or time-varying data).
lower The first time column of interval data (R’s time1), otherwise None.
n The number of rows, including any with missing values.
n_censored The number of censored rows (status 0).
n_events The number of rows that ended in an event.
start The time each row starts being at risk, for counting-process data.
states The names of the event states of a multi-state response, otherwise None.
status The status column, one code per row, stored as floats as R stores it.
stop The main time column, one value per row.
type The kind of response, using R’s type names.

entry

The time each row enters the risk set, defined for every type.

entry: FloatArray

For counting-process data this is the start time of each interval, the same as start. For every other type, subjects are at risk from the beginning, so every value is minus infinity (-inf). Estimators use entry to build risk sets without having to check whether the response has start times.

Examples

import greenwood as gw

gw.Surv(time=[5, 6], event=[1, 0]).entry
array([-inf, -inf])
gw.Surv(time=[0, 2], time2=[5, 6], event=[1, 0]).entry
array([0., 2.])

event

Whether each row ended in an event, as a boolean array.

event: npt.NDArray[np.bool_]

A row counts as an event when its status is 1 or more. For multi-state data that means any state, not a particular one. For interval data it means the event is known to have happened (exactly, before a time, or within an interval), so only right-censored rows are False. Rows with a missing status are False.

Examples

import greenwood as gw

gw.Surv(time=[5, 6, 4, 9], event=[1, 0, 1, 0]).event
array([ True, False,  True, False])

is_multistate

Whether the response has more than one kind of event.

is_multistate: bool

True for multi-state and competing-risks responses (types MRIGHT and MCOUNTING), which come from a categorical event= or from first_event(). Their state names are in states. Estimators for a single event type, such as KaplanMeier and CoxPH, reject these responses. Competing-risks estimators such as AalenJohansen require them.

Examples

import pandas as pd
import greenwood as gw

outcome = pd.Categorical(["pcm", "censor"], categories=["censor", "pcm", "death"])
gw.Surv(time=[5, 6], event=outcome).is_multistate
True

is_truncated

Whether the response has start times (late entry or time-varying data).

is_truncated: bool

True for counting-process responses, built as Surv(time=start, time2=stop, event=...), including multi-state ones. Estimators that can’t handle delayed entry check this and raise an error rather than silently ignoring the start times.

Examples

import greenwood as gw

gw.Surv(time=[0, 2], time2=[5, 6], event=[1, 0]).is_truncated
True

lower

The first time column of interval data (R’s time1), otherwise None.

lower: FloatArray | None

For interval data this is the time that goes with each row’s status: the censoring time for right-censored rows, the event time for exact rows, the time by which the event had happened for left-censored rows, and the lower bound for interval-censored rows (whose upper bound is in stop). For every other type it is None.

Examples

import numpy as np
import greenwood as gw

# Rows: an interval (1, 2], right-censored at 2, an interval (3, 5]
y = gw.Surv(time=[1, 2, 3], time2=[2, np.inf, 5], type="interval2")
y.lower, y.stop, y.status
(array([1., 2., 3.]), array([2., 1., 5.]), array([3., 0., 3.]))

n

The number of rows, including any with missing values.

n: int

Each row is one observation: one subject for most data, or one interval of a subject’s follow-up for counting-process data. Rows with missing values are counted here but dropped when a model is fitted, so a fit may use fewer rows. len(y) gives the same number.

Examples

import greenwood as gw

gw.Surv(time=[5, 6, 4, 9], event=[1, 0, 1, 0]).n
4

n_censored

The number of censored rows (status 0).

n_censored: int

For right, counting, and multi-state data these are the rows that were event-free when last seen. For left data, status 0 means left-censored, and for interval data it means right-censored. Rows with a missing status are not counted, so n_events + n_censored is less than n when some statuses are missing.

Examples

import greenwood as gw

y = gw.Surv(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
y.n_censored, y.n_events + y.n_censored == y.n
(2, True)

n_events

The number of rows that ended in an event.

n_events: int

This counts the True values of event: rows with status 1 or more, in any state for multi-state data. Rows with a missing status are not counted.

Examples

import greenwood as gw

gw.Surv(time=[5, 6, 4, 9], event=[1, 0, 1, 0]).n_events
2

start

The time each row starts being at risk, for counting-process data.

start: FloatArray | None

A counting-process response, Surv(time=start, time2=stop, event=...), describes each row as an interval (start, stop]. This is how late entry (left truncation) and time-varying covariates are expressed. A row whose start is not before its stop has its start set to nan, with a warning, as in R. For every other type there are no start times and this is None. Use entry for a value that is defined for every type.

Examples

import greenwood as gw

# Subject 2 joins the study at time 2
gw.Surv(time=[0, 2, 1], time2=[5, 6, 4], event=[1, 0, 1]).start
array([0., 2., 1.])

states

The names of the event states of a multi-state response, otherwise None.

states: tuple[str, …] | None

A multi-state response comes from a categorical event= (or first_event()). Its first category means censored, and the remaining categories, in order, are the states. A row with status k reached states[k - 1]. For single-event responses this is None.

Examples

import pandas as pd
import greenwood as gw

outcome = pd.Categorical(
    ["death", "censor", "relapse"], categories=["censor", "relapse", "death"]
)
y = gw.Surv(time=[5, 6, 7], event=outcome)
y.states, y.status
(('relapse', 'death'), array([2., 0., 1.]))

status

The status column, one code per row, stored as floats as R stores it.

status: FloatArray

The codes depend on the type:

  • Right and counting data: 1 for an event, 0 for censored. Logical status and R’s 1/2 coding are converted to these codes when the response is built.
  • Left data: 1 for an exact event, 0 for left-censored.
  • Interval data: 0 right-censored, 1 exact, 2 left-censored, 3 interval-censored.
  • Multi-state data: 0 for censored, k for the k-th entry of states.

Codes that R doesn’t accept become missing (nan) when the response is built, with a warning, as do backwards intervals. Use int(y.status[i]) when a code is used as an index.

Examples

R’s 1/2 coding (as in the lung data) becomes 0/1:

import greenwood as gw

gw.Surv(time=[5, 6, 4], event=[2, 1, 2]).status
array([1., 0., 1.])

stop

The main time column, one value per row.

stop: FloatArray

What it holds depends on the type, following R’s Surv matrix:

  • Right, left, and multi-state data: the follow-up time (R’s time column).
  • Counting-process data: the time each interval ends (R’s stop column).
  • Interval data: the upper bound (R’s time2 column). As in R, rows that are not interval-censored (status other than 3) hold the placeholder 1, and their time is in lower.

Any origin offset given to Surv(origin=...) has already been subtracted. Missing times are nan.

Examples

import greenwood as gw

gw.Surv(time=[5, 6, 4, 9], event=[1, 0, 1, 0]).stop
array([5., 6., 4., 9.])

type

The kind of response, using R’s type names.

type: CensoringType

One of CensoringType.RIGHT, LEFT, INTERVAL, COUNTING, MRIGHT (multi-state), or MCOUNTING (multi-state with start times). The type is chosen when the response is built: inferred from the arguments, as in R, or set with type=. A type="interval2" response reports INTERVAL, because R converts it to interval coding. Because CensoringType is a str enum, the value also compares equal to R’s type strings.

Examples

import greenwood as gw

y = gw.Surv(time=[0, 2, 1], time2=[5, 6, 4], event=[1, 0, 1])
y.type, y.type == "counting"
(<CensoringType.COUNTING: 'counting'>, True)

Methods

Name Description
__getitem__() Select rows, returning a new response of the same type.
from_dict() Rebuild a response from the output of to_dict().
from_json() Rebuild a response from the output of to_json().
is_na() Return which rows have a missing value in any column, as R’s is.na() does.
to_dict() Return a JSON-ready mapping that fully describes the response.
to_frame() Return the response as a table with the same columns as R’s Surv matrix.
to_json() Serialize the response to a JSON string.

__getitem__()

Select rows, returning a new response of the same type.

Usage

Source

__getitem__(key)

Indexing works like indexing a NumPy array and always returns a Surv, even for a single row (as subsetting an R Surv with y[i] does). Every column is subset together, and the type and state names are kept. Negative integers count from the end, as usual in Python.

Parameters

key: Any
An integer, a slice, an array of integer positions, or a boolean mask with one value per row.

Returns

Surv
The selected rows.

Raises

IndexError

If an integer position is out of range.

TypeError
If key is not an integer, a slice, an integer array, or a boolean mask.

Examples

import greenwood as gw

y = gw.Surv(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
y[1:3]
Surv(type=right, n=2, events=1)

A boolean mask keeps the rows where it is True, here the events:

y[y.event]
Surv(type=right, n=2, events=2)

from_dict()

Rebuild a response from the output of to_dict().

Usage

Source

from_dict(data)

The mapping is read as it was written, without applying Surv()’s rules again: the stored type, columns, and state names are used directly. The result compares equal to the response that produced the mapping.

Parameters

data: Mapping[str, Any]
A mapping produced by to_dict(), possibly after a round trip through JSON.

Returns

Surv
The rebuilt response.

Raises

ValueError
If the mapping has an unsupported layout version, such as one written by an earlier Greenwood release.

Examples

import greenwood as gw

y = gw.Surv(time=[5, 6], event=[1, 0])
data = y.to_dict()
gw.Surv.from_dict(data) == y
True

from_json()

Rebuild a response from the output of to_json().

Usage

Source

from_json(text)

The text is parsed with json.loads() and passed to from_dict(), so the same rules apply: the stored type, columns, and state names are used as they are, and the result compares equal to the response that was serialized.

Parameters

text: str
A JSON string produced by to_json().

Returns

Surv
The rebuilt response.

Raises

ValueError
If the text is not valid JSON, or has an unsupported layout version.

Examples

import greenwood as gw

y = gw.Surv(time=[5, 6], event=[1, 0])
text = y.to_json()
gw.Surv.from_json(text) == y
True

is_na()

Return which rows have a missing value in any column, as R’s is.na() does.

Usage

Source

is_na()

A row is missing when any of its columns is nan: a missing time, a missing status, or (for counting data) a missing start time. Surv() keeps such rows rather than dropping them, as R does. They come from missing input values, and also from values R turns into missing when the response is built: an invalid status code, a start time that is not before its stop time, or an interval that runs backwards. Estimators drop these rows when fitting and report how many in n_dropped_.

Returns

numpy.ndarray
A boolean array with one value per row, True where the row has a missing value.

Examples

The second subject’s time is missing:

import greenwood as gw

y = gw.Surv(time=[5, None, 4], event=[1, 0, 1])
y.is_na()
array([False,  True, False])

Select the complete rows with the inverted mask:

y[~y.is_na()]
Surv(type=right, n=2, events=2)

to_dict()

Return a JSON-ready mapping that fully describes the response.

Usage

Source

to_dict()

The mapping holds only plain Python values (strings, floats, lists, and None), so it can be written with json.dumps(), stored, or sent to another process, and turned back into an equal response with from_dict(). Missing values become None. The layout is versioned: version 2 stores R’s Surv columns.

Returns

dict
A mapping with the keys "version" (the layout version), "type" (the response type, as one of R’s type names), "states" (the state names, or None), and "columns" (R’s column names mapped to lists of values).

Examples

import greenwood as gw

gw.Surv(time=[5, 6], event=[1, 0]).to_dict()
{'version': 2,
 'type': 'right',
 'states': None,
 'columns': {'time': [5.0, 6.0], 'status': [1.0, 0.0]}}

to_frame()

Return the response as a table with the same columns as R’s Surv matrix.

Usage

Source

to_frame(
    *,
    format=None,
)

Each row of the table is one row of the response. The columns depend on the type, and match the columns of the matrix R’s Surv() returns:

  • Right, left, and multi-state data: time and status.
  • Counting-process data: start, stop, and status.
  • Interval data: time1, time2, and status. As in R, time2 is 1 for rows that are not interval-censored.

Status is stored as a float, as in R. Missing values are nulls. Use this table to inspect a response, to join it back onto other data, or to export it.

Parameters

format: str | None = None
Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table
One row per observation, with the columns listed above.

Raises

ImportError
If the requested DataFrame library (or, with format=None, any of them) is not installed.

Examples

import greenwood as gw

gw.Surv(time=[0, 2, 1], time2=[5, 6, 4], event=[1, 0, 1]).to_frame(format="polars")
shape: (3, 3)
startstopstatus
f64f64f64
0.05.01.0
2.06.00.0
1.04.01.0

Interval data uses R’s interval columns:

import numpy as np

gw.Surv(time=[1, 2, 3], time2=[2, np.inf, 5], type="interval2").to_frame(format="polars")
shape: (3, 3)
time1time2status
f64f64f64
1.02.03.0
2.01.00.0
3.05.03.0

to_json()

Serialize the response to a JSON string.

Usage

Source

to_json(
    *,
    indent=2,
)

The string is the JSON encoding of to_dict(): the layout version, the type, the state names, and R’s columns, with missing values as null. Turn it back into a response with from_json().

Parameters

indent: int | None = 2
Indentation passed to json.dumps(). The default 2 is readable, and None gives a compact single-line string.

Returns

str
The JSON text.

Examples

import greenwood as gw

print(gw.Surv(time=[5, 6], event=[1, 0]).to_json(indent=None))
{"version": 2, "type": "right", "states": null, "columns": {"time": [5.0, 6.0], "status": [1.0, 0.0]}}