Surv

A validated time-to-event response for survival analysis.

Usage

Source

Surv(
    type,
    stop,
    status,
    start=None,
    lower=None,
    states=None,
    weights=None,
)

Surv represents the outcome in survival models: a time at which each subject either experienced an event (observed) or was censored (did not experience the event during follow-up). Surv supports multiple censoring types:

  • Right-censored (most common): The event time is at or after the recorded time. Use Surv.right(time, event).
  • Left-censored: The event time is before the recorded time. Use Surv.left(time, event).
  • Counting-process (left truncation, time-varying): Each subject enters the risk set at start and exits at stop. Use Surv.counting(start, stop, event).
  • Interval-censored: The event occurred within a time interval [lower, upper). Use Surv.interval(lower, upper).
  • Multi-state / competing risks: Multiple mutually exclusive events. Use Surv.multistate(time, event, states).

Use the class methods (right, left, counting, interval, multistate) to construct Surv objects. They validate your input and set the censoring type appropriately. As such, direct instantiation is not recommended.

Attributes

type: CensoringType

The CensoringType enum indicating the censoring mechanism.

stop: Array

Exit time (for interval censoring, the upper bound).

status: Array

Integer event code per observation: 0 = censored, 1+ = event code (for multi-state, codes >= 1 index into states).

start: Array | None

Entry time for the counting-process form (left truncation); None otherwise.

lower: Array | None

Lower bound for interval censoring; None otherwise.

states: tuple[str, …] | None

Event-state labels for multi-state/competing-risks endpoints; None for the single-event case.

weights: Array | None
Optional case weights (strictly positive); None if no weights provided.

Examples

Here’s an example of direct instantiation of Surv:

import greenwood as gw
import numpy as np

y = gw.Surv(
    type=gw.CensoringType.RIGHT,
    stop=np.array([5, 6, 4, 9]),
    status=np.array([1, 0, 1, 0])
)
y
Surv(type=right, n=4, events=2)

While this is fine, the preferred approach is to use the class method constructors for each censoring type. They handle validation and conversion automatically.

Right-censored (the most common case): each subject has an exit time and an event indicator.

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

Counting-process form with left truncation (late entry):

y = gw.Surv.counting(start=[0, 2, 1], stop=[5, 6, 4], event=[1, 0, 1])
y
Surv(type=counting, n=3, events=2, truncated)

Interval-censored (event known to occur in a time window):

y = gw.Surv.interval(lower=[1, 3], upper=[3, 8])
y
Surv(type=interval, n=2, events=2)

Multi-state (competing risks, multiple mutually exclusive events):

y = gw.Surv.multistate(time=[5, 6, 4], event=[1, 0, 2], states=("pcm", "death"))
y
Surv(type=right, n=3, events=2, states=('pcm', 'death'))

Attributes

Name Description
entry Entry times for each observation (or \(-\infty\) if no left truncation).
event Boolean event indicator: True if any event occurred, False if censored.
is_multistate Whether the response has multiple competing event states.
is_truncated Whether the response has left truncation (late entry).
n Number of observations in the response.
n_censored Count of censored observations.
n_events Count of observations where an event occurred (any state in multi-state data).

entry

Entry times for each observation (or \(-\infty\) if no left truncation).

entry: Array

For counting-process data (late entry), this returns the start time when each subject became at risk. For standard right-censored data with no left truncation, all values are \(-\infty\), indicating subjects entered at the beginning of follow-up.

Examples

Right-censored data (no left truncation) has all \(-\infty\) entry times:

import greenwood as gw

y_right = gw.Surv.right(time=[5, 6, 4], event=[1, 0, 1])
y_right.entry
array([-inf, -inf, -inf])

Counting-process data shows each subject’s entry time:

import greenwood as gw

y_counting = gw.Surv.counting(start=[0, 2, 1], stop=[5, 6, 4], event=[1, 0, 1])
y_counting.entry
array([0., 2., 1.])

The entry property is primarily used internally by survival estimators to correctly compute risk sets. You rarely need it directly, but it’s available for custom analyses.


event

Boolean event indicator: True if any event occurred, False if censored.

event: Array

Converts the integer status codes to a simple boolean: 1 or more -> True (event), 0 -> False (censored). This is a convenient summary when you only care about event occurrence, not which specific state occurred in multi-state data.

Examples
import greenwood as gw

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

The True/False values indicate which subjects experienced any event. This is useful for filtering, counting events, or checking data quality. For multi-state data, this collapses all states into a single “any event” indicator:

import greenwood as gw

y_multi = gw.Surv.multistate(
    time=[5, 6, 7, 8],
    event=[1, 2, 0, 1],
    states=("relapse", "death")
)

y_multi.event
array([ True,  True, False,  True])

is_multistate

Whether the response has multiple competing event states.

is_multistate: bool

Multi-state responses track which of several competing outcomes occurred (e.g., “relapse” vs. “death”). When False, there is only one event type (censored or not). When True, the states property contains the outcome labels.

Examples

Right-censored data has a single outcome:

import greenwood as gw

y_right = gw.Surv.right(time=[5, 6, 4], event=[1, 0, 1])
y_right.is_multistate
False

Multi-state data with competing risks:

y_multi = gw.Surv.multistate(
    time=[5, 6, 7, 8],
    event=[1, 2, 0, 1],
    states=("relapse", "death")
)

y_multi.is_multistate
True

This property is useful for determining how to interpret the event codes and what kind of survival estimation is needed.


is_truncated

Whether the response has left truncation (late entry).

is_truncated: bool

Left truncation occurs in counting-process data when subjects enter the risk set at different times (late entry). This is common in studies with age-based entry or complex follow-up patterns. When True, the entry() property contains the actual start times; when False, all subjects implicitly start at time 0.

Examples

Right-censored data has no left truncation:

import greenwood as gw

y_right = gw.Surv.right(time=[5, 6, 4], event=[1, 0, 1])
y_right.is_truncated
False

Counting-process data with late entry is truncated:

y_counting = gw.Surv.counting(start=[0, 2, 1], stop=[5, 6, 4], event=[1, 0, 1])
y_counting.is_truncated
True

This property is useful for understanding data structure and for conditional logic that handles truncated vs. non-truncated data differently.


n

Number of observations in the response.

n: int

Returns the total count of subjects/observations, regardless of event status. Equivalent to len(surv_object).

Examples
import greenwood as gw

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

This is useful for loops, validation, or allocating arrays. Often used to determine sample size or for sanity checks on data shape.


n_censored

Count of censored observations.

n_censored: int

Counts all observations where the event was not observed (status == 0). These are subjects whose true event time is unknown but exceeds their observation time.

Examples
import greenwood as gw

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

Often used for descriptive summary: “We observed 2 events and 2 censored subjects out of 4 total.” Can validate data quality:

assert y.n_events + y.n_censored == y.n

Higher censoring rates reduce the information available for estimation and may require larger sample sizes for stable inference.


n_events

Count of observations where an event occurred (any state in multi-state data).

n_events: int

Counts all observations with status >= 1. For multi-state responses, this counts all events regardless of which specific state occurred. For single-event data, this is the count of subjects who experienced the event.

Examples
import greenwood as gw

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

This is useful for descriptive statistics, event rate calculations, or validating data: assert y.n_events + y.n_censored == y.n.

For multi-state data, this gives the total event count across all states:

y_multi = gw.Surv.multistate(
    time=[5, 6, 7, 8],
    event=[1, 2, 0, 1],
    states=("relapse", "death")
)
y_multi.n_events
3

Methods

Name Description
counting() Counting-process response: track subjects entering and exiting the risk set at
from_dict() Rebuild a response from to_dict output.
from_json() Deserialize from to_json output.
interval() Interval-censored response: event time is known to lie within a range.
left() Left-censored response: event occurred before the observation time.
multistate() Multi-state or competing-risks response: track which of multiple outcomes occurs.
right() Right-censored response: the standard and most common form of survival data.
to_dict() Return a JSON-ready mapping fully describing the response.
to_frame() Return the response as a DataFrame (one row per observation).
to_json() Serialize to a deterministic JSON string.

counting()

Counting-process response: track subjects entering and exiting the risk set at

Usage

Source

counting(start, stop, event=None, *, weights=None)

different times.

The counting-process form handles two important real-world complexities:

  1. Late entry (left truncation): Not all subjects start being at risk at time 0. For example, a study might enroll subjects at different ages, or you might analyze a subset of follow-up time after some subjects are already older. The start time marks when each subject becomes eligible to experience the event.

  2. Time-varying covariates: The counting-process form naturally accommodates covariates that change over time. Each row represents one interval of time for a subject, allowing you to track how covariate values change.

Each subject contributes one or more (start, stop] intervals. The subject is at risk only during their interval(s) and cannot experience the event before entering at start.

Parameters
start: Any

Entry times (when each subject becomes at risk). Must be finite and non-negative. Represents when the subject enters the risk set. In standard studies, this is 0; in studies with late entry, it’s the age/time at enrollment.

stop: Any

Exit times (when follow-up ends). Must be finite, non-negative, and strictly greater than the corresponding start. Represents when the subject leaves follow-up (event, censoring, or end of study).

event: Any = None

Event indicators (1 = event occurred, 0 = censored at stop time). If None, all subjects are treated as having experienced the event.

weights: Any = None
Case weights (strictly positive, one per subject). Used to weight subjects differently in survival analysis. Default is None (all weights = 1).
Returns
Surv
A counting-process Surv response object with potential left truncation.
Examples

Here we have 3 subjects with different entry times:

import greenwood as gw

y = gw.Surv.counting(start=[0, 2, 1], stop=[5, 6, 4], event=[1, 0, 1])
y
Surv(type=counting, n=3, events=2, truncated)

The display shows:

  • Subject 1: Entered at time 0, exited with an event at time 5
  • Subject 2: Entered at time 2 (late entry), exited censored at time 6
  • Subject 3: Entered at time 1, experienced an event at time 4

Only subjects 2 and 3 benefit from the late entry handling, but the counting-process form elegantly handles all cases uniformly. This representation is also essential for studies with time-varying covariates, where you create multiple rows per subject as their covariate values change.


from_dict()

Rebuild a response from to_dict output.

Usage

Source

from_dict(data)

This is the inverse of to_dict(): it reconstructs an equivalent Surv object from a dictionary previously created by to_dict(). Useful for deserializing stored or transmitted data, or for round-tripping through storage formats.

Parameters
data: dict[str, Any]
A dictionary produced by to_dict() containing keys type, stop, status, and optional keys for start, lower, states, weights.
Returns
Surv
A new Surv object with the same data and structure as the input dictionary.
Examples

Rebuild an equivalent response from its dictionary representation. Here we serialize a response and immediately deserialize it:

import greenwood as gw

y = gw.Surv.right(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
reconstructed = gw.Surv.from_dict(y.to_dict())
print("Objects equal:", y.to_dict() == reconstructed.to_dict())
Objects equal: True

The reconstructed object is equivalent to the original in every way.


from_json()

Deserialize from to_json output.

Usage

Source

from_json(text)

This is the inverse of to_json(): it reconstructs a Surv object from a JSON string previously created by to_json(). Useful for loading data from stored files, API responses, or any other JSON source. The reconstructed object is guaranteed to be equivalent to the original.

Parameters
text: str
A JSON string produced by to_json() containing the serialized Surv data.
Returns
Surv
A new Surv object restored from the JSON representation.
Examples

Deserialize from JSON. Build a response, then round-trip through to_json() and back:

import greenwood as gw

y = gw.Surv.right(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
json_text = y.to_json()
restored = gw.Surv.from_json(json_text)
print("Round-trip successful:", y.to_json() == restored.to_json())
Round-trip successful: True

The restored object is an exact copy of the original Surv object.


interval()

Interval-censored response: event time is known to lie within a range.

Usage

Source

interval(lower, upper, *, weights=None)

Interval censoring occurs when you know the event happened sometime between two observation times, but not exactly when. Common in:

  • Medical follow-up: Disease detection between clinic visits. You might know a patient’s disease status at two checkups, but not the exact time of onset.
  • Equipment reliability: Failure detected between inspections. You know failure happened between the last working inspection and the current failed one.
  • Longitudinal surveys: Event reported between survey waves but exact timing unknown.

The interval-censored form captures this uncertainty. The event happened somewhere in the interval (lower, upper]. If lower == upper, it’s an exact (uncensored) event. Use infinity for upper to represent right-censoring, and 0 for lower to represent left-censoring.

Parameters
lower: Any

Interval lower bounds (one per subject). Must be finite and non-negative. Event happened after this time (possibly at this time). Set to 0 to mark left-censored subjects (event happened before first observation).

upper: Any

Interval upper bounds (one per subject). Must be finite, non-negative, and >= lower. Event happened by this time. Set to numpy.inf to mark right-censored subjects (no event observed by end of study).

weights: Any = None
Case weights (strictly positive, one per subject). Used to weight subjects differently in survival analysis. Default is None (all weights = 1).
Returns
Surv
An interval-censored Surv response object.
Examples

Here we have 3 subjects with different levels of observation precision:

import greenwood as gw
import numpy as np

y = gw.Surv.interval(lower=[1, 2, 3], upper=[2, np.inf, 5])
y
Surv(type=interval, n=3, events=2)

The display shows: - Subject 1: Exact event at time 2 (lower == upper) - Subject 2: Right-censored at time 2 (upper = infinity means event never observed) - Subject 3: Interval-censored between times 3 and 5 (event happened somewhere in that window)

Interval censoring gives you more information than right censoring alone. Rather than just knowing “no event by time X,” you may know “event was definitely before time Y but after time X,” which allows for more precise estimation when multiple observations bracket the event.

See Also
left()

Event occurred before the observation time.

right()

Event occurred after the observation time.

counting()

Track subjects entering and exiting at different times.


left()

Left-censored response: event occurred before the observation time.

Usage

Source

left(time, event=None, *, weights=None)

Left censoring occurs when all you know is that an event happened before you observed the subject. For example, an infection that must have occurred before a patient was tested, or a failure that was known to have happened sometime before equipment was inspected. The exact event time is unknown, but you know it was no later than the recorded time.

This is less common than right censoring, but important in scenarios where you cannot pinpoint when something happened, only that it already had.

Parameters
time: Any

Observation times (the upper bound on when the event occurred). Must be finite and non-negative. Each value represents “the event happened by this time”.

event: Any = None

Event indicators:

  • 1 = event occurred before time (left-censored)
  • 0 = subject was event-free at time (not censored)

If None, all subjects are treated as having experienced the event.

weights: Any = None
Case weights (strictly positive, one per subject). Used to weight subjects differently in survival analysis. Default is None (all weights = 1).
Returns
Surv
A left-censored Surv response object.
Examples

Here we have 3 subjects. Two experienced the event before the recorded time (event=1), and one was event-free at observation (event=0):

import greenwood as gw

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

The display shows the data structure:

  • The < symbol indicates left-censored observations (event occurred before time)
  • The + symbol indicates subjects who were still event-free at the observation time
  • The left-censoring type "left" is displayed at the top
See Also
right()

Right-censored response (event after the observation time).

counting()

Time intervals with late entry.

interval()

Event lies in a known interval.


multistate()

Multi-state or competing-risks response: track which of multiple outcomes occurs.

Usage

Source

multistate(time, event, states, *, start=None, weights=None)

Real-world studies often involve multiple competing outcomes. A patient in a cancer study might relapse, die from cancer, or die from other causes. Each subject can only experience one outcome, and once it happens, no other outcome is possible.

The multi-state framework elegantly handles this by:

  1. Defining possible states: You specify the labeled outcomes (e.g., “relapse”, “death from cancer”, “death from other causes”) that are mutually exclusive.
  2. Recording which state occurred: Rather than a simple 0/1 event, you record which specific state the subject transitioned to (or 0 if censored).
  3. Separate risk estimation: You can estimate the risk of each state independently, accounting for the fact that other states prevent each outcome.

This is essential for realistic survival modeling: accounting for competing risks often substantially changes the estimated risk curves compared to treating all non-events identically.

Parameters
time: Any

Event or censoring times (one per subject). Must be finite and non-negative. Represents when the subject experienced an outcome (or was censored).

event: Any

Event codes indicating which state occurred:

  • 0 = censored (no event observed)
  • 1 = transitioned to states[0] (first outcome)
  • 2 = transitioned to states[1] (second outcome)
  • … and so on for each defined state

Must be in range [0, len(states)].

states: tuple[str, …]

Labels for the possible outcomes. Event codes index into this tuple. Example: states=(“relapse”, “death”) means:

  • event code 1 → relapse occurred
  • event code 2 → death occurred

Labels are arbitrary strings describing what the transition represents.

start: array - like = None

Optional entry times (for late entry / left truncation). If provided, each subject is only at risk from start until time. Default is None (all subjects enter at time 0).

weights: array - like = None
Case weights (strictly positive, one per subject). Used to weight subjects differently in survival analysis. Default is None (all weights = 1).
Returns
Surv
A multi-state / competing-risks Surv response object.
Examples

Here we have 4 subjects with 2 competing outcomes (relapse and death):

import greenwood as gw

y = gw.Surv.multistate(
    time=[5, 6, 7, 8],
    event=[1, 2, 0, 1],
    states=("relapse", "death")
)

y
Surv(type=right, n=4, events=3, states=('relapse', 'death'))

The display shows:

  • Subject 1: Transitioned to “relapse” (event code 1) at time 5
  • Subject 2: Transitioned to “death” (event code 2) at time 6
  • Subject 3: Censored (event code 0) at time 7
  • Subject 4: Transitioned to “relapse” (event code 1) at time 8

You can then estimate the probability of each outcome separately, capturing the full picture: not just “will something happen?” but “which specific outcome is most likely?” This avoids the bias of artificially grouping competing outcomes together.

See Also
right()

Simple right-censored Surv response object (only one possible outcome).

counting()

Time intervals with late entry.

left()

Event occurred before the observation time.



to_dict()

Return a JSON-ready mapping fully describing the response.

Usage

Source

to_dict()

This method serializes the entire Surv object into a plain Python dictionary, making it suitable for JSON serialization, storage, or transmission. All array data is converted to plain Python lists. The dictionary captures the censoring type and every array (time, status, optional fields), with None for fields that a given censoring flavor does not use.

Returns
dict[str, Any]
A dictionary with keys: type (CensoringType as string), stop, status, and optional keys start, lower, states, weights (as lists or None).
Examples

The mapping structure varies by censoring type, but always includes type, stop, and status. Unused fields are None. Here we build a response and convert it to a dictionary:

import greenwood as gw

y = gw.Surv.right(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
y.to_dict()
{'type': 'right',
 'stop': [5.0, 6.0, 4.0, 9.0],
 'status': [1, 0, 1, 0],
 'start': None,
 'lower': None,
 'states': None,
 'weights': None}

This is the serialized form that underpins to_json() and enables round-tripping via from_dict().


to_frame()

Return the response as a DataFrame (one row per observation).

Usage

Source

to_frame(*, format=None)

Exports the Surv object to a tidy table where each row represents one observation. The table includes the stop and status columns, plus optional columns for start (entry time in counting-process form), lower (lower bound for interval censoring), and weight (case weights). This is convenient for inspection, export to CSV, or integration with other DataFrame workflows.

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
A tidy table with one row per observation, including columns for stop, status, and optional start, lower, weight columns.
Raises
ImportError
If the requested (or, when auto-detecting, any) DataFrame library is not installed.
Examples

Build a right-censored response and export it as a Polars frame. Each row represents one observation with its event time and status:

import greenwood as gw

y = gw.Surv.right(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
y.to_frame(format="polars")
shape: (4, 2)
stopstatus
f64i64
5.01
6.00
4.01
9.00

Request a different backend with format=:

y.to_frame(format="pandas")
stop status
0 5.0 1
1 6.0 0
2 4.0 1
3 9.0 0

to_json()

Serialize to a deterministic JSON string.

Usage

Source

to_json(*, indent=2)

This method converts the entire Surv object to a compact, JSON-formatted string suitable for storage in files, databases, or transmission over APIs. By default, the output is human-readable with indentation; pass indent=None for a compact form. The serialization is deterministic: the same Surv object always produces the identical JSON string.

Parameters
indent: int | None = 2
Number of spaces to use for indentation. If None, produces compact JSON without whitespace. Default is 2 (human-readable).
Returns
str
A JSON string representing the Surv object, including censoring type and all arrays.
Examples

Serialize to JSON. By default, output is indented for readability. Here we build a response and show just the first 120 characters of compact JSON:

import greenwood as gw

y = gw.Surv.right(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
json_compact = y.to_json(indent=None)
print(json_compact[:120])
{"type": "right", "stop": [5.0, 6.0, 4.0, 9.0], "status": [1, 0, 1, 0], "start": null, "lower": null, "states": null, "w

The full JSON includes all data in a structured format that can be parsed back with from_json().

See Also

  • right(): Simple right-censored response (all subjects start at time 0).
  • interval(): Event lies in a known interval.
  • multistate(): Track transitions to multiple competing states.