_plotting/_analytical_plots.py provides two renderers for caller-precomputed
analytical results. Neither function fits a model, estimates a survival curve,
calculates confidence limits, derives risk sets, predicts effects, jitters
observations, or classifies outcomes.
def kaplan_meier_plot(
curve_df: pd.DataFrame,
risk_table_df: pd.DataFrame,
*,
censor_df: pd.DataFrame | None = None,
time: str = "time",
survival: str = "survival",
ci_lower: str = "ci_lower",
ci_upper: str = "ci_upper",
group: str = "group",
risk_time: str = "time",
risk_count: str = "n_at_risk",
group_order: Sequence[Any] | None = None,
palette: Mapping[Any, Any] | Sequence[Any] | str | None = None,
ci_alpha: float = 0.20,
censor_marker: str = "+",
censor_size: float = 42,
xlabel: str = "Time",
ylabel: str = "Survival probability",
title: str | None = None,
legend_title: str | None = None,
legend_labels: Mapping[Any, str] | None = None,
figsize: tuple[float, float] = (8, 6.5),
show: bool = True,
) -> tuple[
plt.Figure,
dict[str, plt.Axes],
pd.DataFrame,
pd.DataFrame,
]:The function draws one post-step survival curve and step confidence band per
group. censor_df, when supplied, uses the same time, survival, and
group column arguments and is drawn at its exact supplied coordinates. In
particular, a censor at time zero is not filtered out or moved.
risk_table_df supplies the displayed risk values. Its observed groups must
exactly match the curve groups. The union of supplied risk times defines a
common grid, and every group must have exactly one row at every grid time.
Risk values are displayed at their exact supplied time and are not
recalculated. Time-zero values are retained.
Group order is resolved from group_order, then categorical dtype order, then
first appearance in curve_df. An explicit order may contain unobserved
groups, but it must contain every observed group. Unobserved entries reserve
their palette positions and are not drawn. This keeps sequence colors stable
when a configured group is absent. Palette mappings must cover the full
configured order; extra mapping entries are ignored.
legend_labels is a partial mapping. Unmapped displayed groups use
str(group). The main legend contains one curve entry per displayed group in
resolved order; confidence bands and censor markers do not add entries. The
risk panel uses the same resolved labels and colors.
import pandas as pd
import adata_science_tools as adtl
curves = pd.DataFrame(
{
"time": [0, 4, 8, 0, 4, 8],
"survival": [1.00, 0.86, 0.72, 1.00, 0.78, 0.61],
"ci_lower": [0.96, 0.75, 0.58, 0.95, 0.64, 0.45],
"ci_upper": [1.00, 0.94, 0.84, 1.00, 0.89, 0.76],
"group": ["A", "A", "A", "B", "B", "B"],
}
)
risk = pd.DataFrame(
{
"time": [0, 4, 8, 0, 4, 8],
"n_at_risk": [20, 16, 11, 22, 15, 9],
"group": ["A", "A", "A", "B", "B", "B"],
}
)
censors = pd.DataFrame(
{
"time": [4, 6],
"survival": [0.86, 0.70],
"group": ["A", "B"],
}
)
fig, axes, plotted_curves, plotted_risk = adtl.kaplan_meier_plot(
curves,
risk,
censor_df=censors,
group_order=["A", "B"],
palette={"A": "#4477AA", "B": "#CC6677"},
show=False,
)grouped_risk_censor — Survival curves with numbers at risk. Data and analysis provenance.
Curve, censor, and risk coordinates must be complete finite real numbers.
Survival coordinates and confidence bounds must lie within [0, 1], and each
curve row must satisfy ci_lower <= survival <= ci_upper. Risk counts must be
nonnegative. Group values must be complete and hashable. Referenced columns
must exist exactly once.
The renderer does not impose scientific conditions that are not part of its input contract. It does not require survival or risk counts to be monotone, does not require risk counts to be integral, does not require nonnegative times, and does not require censor or risk times to lie within the curve time range. Duplicate curve times are retained in stable source order and may represent caller-prepared step geometry.
The axes mapping always has these keys:
| Key | Meaning |
|---|---|
main |
Survival curves, confidence bands, censors, and legend. |
risk_table |
Vertically aligned numbers-at-risk panel sharing the time axis. |
The third return value is a normalized curve table in group/time/source order:
| Column | Meaning |
|---|---|
group |
Supplied group value. |
time, survival, ci_lower, ci_upper |
Exact numeric curve coordinates. |
group_position |
Zero-based displayed group position. |
curve_position |
Zero-based row position in the returned curve table. |
resolved_color |
Effective Matplotlib RGBA color. |
The fourth return value is a normalized risk table in group/risk-time order:
| Column | Meaning |
|---|---|
group, time, n_at_risk |
Exact supplied risk values under normalized names. |
group_position |
Zero-based displayed group position. |
risk_time_position |
Zero-based common risk-time position. |
risk_y |
Exact y coordinate used by the risk-table text. |
resolved_color |
Effective Matplotlib RGBA color. |
Caller column names and unrelated columns are not copied into these normalized tables. Censors are not returned because the public tuple contains prepared curve and risk tables. All caller-owned frames remain unchanged.
def continuous_effect_plot(
curve_df: pd.DataFrame,
*,
x: str,
estimate: str,
ci_lower: str,
ci_upper: str,
observed_df: pd.DataFrame | None = None,
observed_x: str | None = None,
observed_y: str | None = None,
observed_category: str | None = None,
observed_order: Sequence[Any] | None = None,
observed_styles: Mapping[Any, Mapping[str, Any]] | None = None,
line_color: Any = "#4477AA",
ci_alpha: float = 0.20,
xscale: str = "log",
ylims: Sequence[float] | None = None,
y_reference_lines: Sequence[Mapping[str, Any]] | None = None,
xlabel: str | None = None,
ylabel: str | None = None,
title: str | None = None,
annotation: str | None = None,
annotation_xy: tuple[float, float] = (0.03, 0.97),
ax: plt.Axes | None = None,
figsize: tuple[float, float] = (6.5, 5),
show: bool = True,
) -> tuple[
plt.Figure,
plt.Axes,
pd.DataFrame,
pd.DataFrame,
]:The estimate line and confidence band use the exact supplied values in stable
numeric-x/source order. Duplicate x coordinates are retained. xscale
supports "linear" and "log"; logarithmic display requires positive curve
and observed x coordinates.
Supplying observed_df requires both observed_x and observed_y. Without
observed_category, observations are one neutral gray circle layer in source
order and do not add a legend entry. observed_order and observed_styles
therefore require observed_category.
Categorized order is explicit order, categorical dtype order, then first appearance. An explicit order must include every observed category and may include unobserved categories. Rows are drawn and returned category-major, with source order retained within a category.
Each observed_styles value may contain only:
markerfilledlabelfacecoloredgecolorsizealpha
Mappings are partial overrides. Default styles use deterministic marker and
color cycles, filled markers, category text as the label, size 36, and alpha
0.70. facecolor=None or edgecolor=None inherits the category default.
Open markers retain their intended RGBA face color in the returned data and
use "none" as the rendered face color. Styles for configured but unobserved
categories are validated before drawing.
Categorized legend entries follow displayed category order. Labeled
y_reference_lines follow them in configured reference order. Reference
mappings use the shared schema: finite numeric value plus optional label,
color, linestyle, linewidth, alpha, and zorder.
curve = pd.DataFrame(
{
"exposure": [0.5, 1, 2, 4, 8],
"estimate": [0.82, 0.95, 1.08, 1.25, 1.47],
"lower": [0.68, 0.81, 0.92, 1.05, 1.18],
"upper": [0.99, 1.11, 1.27, 1.49, 1.83],
}
)
observed = pd.DataFrame(
{
"exposure": [0.7, 1.4, 3.2, 6.0],
"outcome": [0, 1, 0, 1],
"status": ["No", "Yes", "No", "Yes"],
}
)
fig, ax, plotted_curve, plotted_observed = adtl.continuous_effect_plot(
curve,
x="exposure",
estimate="estimate",
ci_lower="lower",
ci_upper="upper",
observed_df=observed,
observed_x="exposure",
observed_y="outcome",
observed_category="status",
observed_order=["No", "Yes"],
observed_styles={
"No": {"marker": "o", "facecolor": "0.65"},
"Yes": {"marker": "^", "facecolor": "#CC6677"},
},
y_reference_lines=[
{"value": 1, "label": "Reference", "linestyle": "--"},
],
annotation="Caller-supplied effect",
show=False,
)observed_categories — Continuous exposure effect. Data and analysis provenance.
Curve and observed coordinates must be complete finite real numbers. Every
curve row must satisfy ci_lower <= estimate <= ci_upper. Limits must be two
finite increasing values. Colors, alpha values, markers, style keys,
annotation coordinates, and references are validated before drawing.
When ax=None, the function creates a figure using figsize, applies layout,
and calls plt.show() only when show=True. A successfully returned figure
remains open when show=False. If rendering fails, only a figure created by
the function is closed.
When ax is supplied, the function returns ax.figure, draws only on that
axis, ignores figsize, and never calls show, close, or a figure layout
method. This makes the function safe for caller-managed multi-panel figures.
The normalized curve table has exactly:
xestimateci_lowerci_uppercurve_position
The normalized observed table has exactly:
observed_xobserved_yobserved_categorycategory_positionobserved_positionresolved_markerresolved_marker_filledresolved_marker_labelresolved_marker_facecolorrendered_marker_facecolorresolved_marker_edgecolorresolved_marker_sizeresolved_marker_alpha
With no observed layer, the fourth return value is an empty DataFrame with that exact schema. Caller input frames are never mutated.

