Skip to content

Commit 0825315

Browse files
authored
Merge pull request #318 from uriahf/agent/remove-legacy-lifelines
Remove legacy lifelines Cox smoothing
2 parents 4efafe0 + 500d7ac commit 0825315

2 files changed

Lines changed: 11 additions & 150 deletions

File tree

src/rtichoke/calibration/__init__.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,11 @@
44

55
from . import calibration as _calibration
66
from ._interactive_aspect import enforce_square_calibration_panel
7-
from ._secondary_cox import calculate_secondary_cox_smooth
87

98
_original_create_calibration_curve = _calibration.create_calibration_curve
109
_original_create_calibration_curve_times = _calibration.create_calibration_curve_times
1110

1211

13-
# Route the existing private secondary-Cox hook through smoothstate while
14-
# preserving rtichoke's Aalen-Johansen fallback behavior.
15-
def _smoothstate_secondary_cox(df_adj, horizon, performance_type):
16-
return calculate_secondary_cox_smooth(
17-
df_adj,
18-
horizon,
19-
performance_type,
20-
aj_risk_at_horizon=_calibration._aj_risk_at_horizon,
21-
)
22-
23-
24-
_calibration._calculate_secondary_cox_smooth = _smoothstate_secondary_cox
25-
26-
2712
def create_calibration_curve(*args, **kwargs):
2813
"""Create an interactive calibration plot with a square main panel."""
2914
return enforce_square_calibration_panel(

src/rtichoke/calibration/calibration.py

Lines changed: 11 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import polars as pl
1212
import numpy as np
1313
from polarstate import predict_aj_estimates, prepare_event_table
14+
from ._secondary_cox import calculate_secondary_cox_smooth
1415

1516
# from rtichoke.helpers.send_post_request_to_r_rtichoke import send_requests_to_rtichoke_r
1617

@@ -1174,137 +1175,6 @@ def _make_adjusted_deciles_data(
11741175
return pl.DataFrame(rows).sort(["reference_group", "decile"])
11751176

11761177

1177-
def _calculate_rcs_basis_3knots(
1178-
x: np.ndarray, knots: Union[np.ndarray, None] = None
1179-
) -> tuple[np.ndarray, np.ndarray]:
1180-
"""Calculate 3-knot restricted cubic spline basis matrix.
1181-
1182-
Follows Harrell's RCS formulation (RMS Section 2.4.1) / rms::rcs in R.
1183-
For 3 knots (10th, 50th, 90th percentiles of x):
1184-
basis matrix has 2 columns: [x, u1(x)]
1185-
"""
1186-
x = np.asarray(x, dtype=float)
1187-
if knots is None:
1188-
knots = np.percentile(x, [10, 50, 90])
1189-
knots = np.sort(np.asarray(knots, dtype=float))
1190-
1191-
t1, t2, t3 = knots[0], knots[1], knots[2]
1192-
1193-
# Handle edge case where knots are duplicate / non-unique
1194-
if len(np.unique(knots)) < 3 or (t3 - t2) == 0 or (t2 - t1) == 0 or (t3 - t1) == 0:
1195-
return x[:, None], knots
1196-
1197-
denom = (t3 - t1) ** 2
1198-
1199-
def pos_cube(val: np.ndarray) -> np.ndarray:
1200-
return np.maximum(val, 0) ** 3
1201-
1202-
u1 = (
1203-
pos_cube(x - t1)
1204-
- ((t3 - t1) / (t3 - t2)) * pos_cube(x - t2)
1205-
+ ((t2 - t1) / (t3 - t2)) * pos_cube(x - t3)
1206-
) / denom
1207-
1208-
basis = np.column_stack([x, u1])
1209-
return basis, knots
1210-
1211-
1212-
def _calculate_secondary_cox_smooth(
1213-
df_adj: pl.DataFrame,
1214-
horizon: float,
1215-
performance_type: str,
1216-
) -> pl.DataFrame:
1217-
"""Calculate smoothed calibration curve using secondary Cox regression (Austin et al. 2020 & McLernon et al. 2023 method)."""
1218-
from lifelines import CoxPHFitter
1219-
1220-
smooth_frames = []
1221-
1222-
for key, group_df in df_adj.group_by("reference_group", maintain_order=True):
1223-
group_name = str(key[0])
1224-
probs = group_df["prob"].to_numpy()
1225-
reals = group_df["real"].to_numpy()
1226-
times = group_df["time"].to_numpy()
1227-
1228-
p_clipped = np.clip(probs, 1e-6, 1 - 1e-6)
1229-
x = np.log(-np.log(1 - p_clipped))
1230-
events = (reals == 1).astype(int)
1231-
1232-
if len(np.unique(x)) <= 1 or events.sum() == 0:
1233-
y_est = _aj_risk_at_horizon(group_df, horizon)
1234-
xout = np.linspace(0, 1, 101)
1235-
smooth_frames.append(
1236-
pl.DataFrame(
1237-
{
1238-
"x": xout,
1239-
"y": [y_est] * len(xout),
1240-
"reference_group": [group_name] * len(xout),
1241-
}
1242-
)
1243-
)
1244-
continue
1245-
1246-
basis, knots = _calculate_rcs_basis_3knots(x)
1247-
1248-
if basis.shape[1] == 2:
1249-
fit_df = pl.DataFrame(
1250-
{
1251-
"time": times,
1252-
"event": events,
1253-
"rcs_1": basis[:, 0],
1254-
"rcs_2": basis[:, 1],
1255-
}
1256-
)
1257-
else:
1258-
fit_df = pl.DataFrame(
1259-
{"time": times, "event": events, "rcs_1": basis[:, 0]}
1260-
)
1261-
1262-
try:
1263-
cph = CoxPHFitter(penalizer=0.01)
1264-
cph.fit(fit_df.to_pandas(), duration_col="time", event_col="event")
1265-
1266-
xout = np.linspace(0.001, 0.999, 101)
1267-
x_grid = np.log(-np.log(1 - xout))
1268-
grid_basis, _ = _calculate_rcs_basis_3knots(x_grid, knots=knots)
1269-
1270-
if grid_basis.shape[1] == 2 and "rcs_2" in fit_df.columns:
1271-
grid_df = pl.DataFrame(
1272-
{"rcs_1": grid_basis[:, 0], "rcs_2": grid_basis[:, 1]}
1273-
)
1274-
else:
1275-
grid_df = pl.DataFrame({"rcs_1": grid_basis[:, 0]})
1276-
1277-
surv_at_t = cph.predict_survival_function(
1278-
grid_df.to_pandas(), times=[horizon]
1279-
).values.ravel()
1280-
yout = np.clip(1.0 - surv_at_t, 0.0, 1.0)
1281-
except Exception:
1282-
y_est = _aj_risk_at_horizon(group_df, horizon)
1283-
xout = np.linspace(0, 1, 101)
1284-
yout = np.array([y_est] * len(xout))
1285-
1286-
smooth_frames.append(
1287-
pl.DataFrame(
1288-
{
1289-
"x": xout,
1290-
"y": yout,
1291-
"reference_group": [group_name] * len(xout),
1292-
}
1293-
)
1294-
)
1295-
1296-
if not smooth_frames:
1297-
return pl.DataFrame(
1298-
schema={
1299-
"x": pl.Float64,
1300-
"y": pl.Float64,
1301-
"reference_group": pl.Utf8,
1302-
}
1303-
)
1304-
1305-
smooth_dat = pl.concat(smooth_frames)
1306-
return smooth_dat
1307-
13081178

13091179
def _calculate_local_aj_smooth(
13101180
df_adj: pl.DataFrame,
@@ -1469,8 +1339,11 @@ def _create_calibration_curve_list_times(
14691339
df_adj, horizon, performance_type, bandwidth=bandwidth
14701340
)
14711341
elif smooth_method == "secondary_cox":
1472-
smooth_data = _calculate_secondary_cox_smooth(
1473-
df_adj, horizon, performance_type
1342+
smooth_data = calculate_secondary_cox_smooth(
1343+
df_adj,
1344+
horizon,
1345+
performance_type,
1346+
aj_risk_at_horizon=_aj_risk_at_horizon,
14741347
)
14751348
elif smooth_method == "pseudo_values":
14761349
pseudo_by_group = _calculate_adjusted_pseudostates(
@@ -1536,8 +1409,11 @@ def _create_calibration_curve_list_times(
15361409
df_adj, horizon, performance_type, bandwidth=bandwidth
15371410
)
15381411
elif smooth_method == "secondary_cox":
1539-
smooth_data = _calculate_secondary_cox_smooth(
1540-
df_adj, horizon, performance_type
1412+
smooth_data = calculate_secondary_cox_smooth(
1413+
df_adj,
1414+
horizon,
1415+
performance_type,
1416+
aj_risk_at_horizon=_aj_risk_at_horizon,
15411417
)
15421418
elif smooth_method == "pseudo_values":
15431419
smooth_data = _calculate_smooth_curve(

0 commit comments

Comments
 (0)