|
11 | 11 | import polars as pl |
12 | 12 | import numpy as np |
13 | 13 | from polarstate import predict_aj_estimates, prepare_event_table |
| 14 | +from ._secondary_cox import calculate_secondary_cox_smooth |
14 | 15 |
|
15 | 16 | # from rtichoke.helpers.send_post_request_to_r_rtichoke import send_requests_to_rtichoke_r |
16 | 17 |
|
@@ -1174,137 +1175,6 @@ def _make_adjusted_deciles_data( |
1174 | 1175 | return pl.DataFrame(rows).sort(["reference_group", "decile"]) |
1175 | 1176 |
|
1176 | 1177 |
|
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 | | - |
1308 | 1178 |
|
1309 | 1179 | def _calculate_local_aj_smooth( |
1310 | 1180 | df_adj: pl.DataFrame, |
@@ -1469,8 +1339,11 @@ def _create_calibration_curve_list_times( |
1469 | 1339 | df_adj, horizon, performance_type, bandwidth=bandwidth |
1470 | 1340 | ) |
1471 | 1341 | 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, |
1474 | 1347 | ) |
1475 | 1348 | elif smooth_method == "pseudo_values": |
1476 | 1349 | pseudo_by_group = _calculate_adjusted_pseudostates( |
@@ -1536,8 +1409,11 @@ def _create_calibration_curve_list_times( |
1536 | 1409 | df_adj, horizon, performance_type, bandwidth=bandwidth |
1537 | 1410 | ) |
1538 | 1411 | 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, |
1541 | 1417 | ) |
1542 | 1418 | elif smooth_method == "pseudo_values": |
1543 | 1419 | smooth_data = _calculate_smooth_curve( |
|
0 commit comments