survival-analysis-tools 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dina Daitchman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: survival_analysis_tools
3
+ Version: 0.1.1
4
+ Summary: Survival analysis helpers: Kaplan–Meier plots and Cox HR utilities.
5
+ Author-email: Dina Daitchman <ddaitchman@novocure.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy>=1.20
14
+ Requires-Dist: pandas>=1.3
15
+ Requires-Dist: matplotlib>=3.5
16
+ Requires-Dist: lifelines>=0.27
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0; extra == "dev"
19
+ Requires-Dist: build>=1.0; extra == "dev"
20
+ Requires-Dist: twine>=4.0; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # survival_analysis_tools
24
+
25
+ Kaplan–Meier plotting and Cox HR helpers for survival analysis workflows.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install survival_analysis_tools
31
+ ```
32
+
33
+ From TestPyPI:
34
+
35
+ ```bash
36
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ survival_analysis_tools
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from survival_analysis_tools import plot_km, cox_over_df
43
+
44
+ summary, hr, pval = plot_km(
45
+ df,
46
+ time_col="survival",
47
+ event_col="event",
48
+ group_col="usage_bin",
49
+ )
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ```powershell
55
+ pip install -e ".[dev]"
56
+ python -m pytest
57
+ python -m build
58
+ ```
59
+
60
+ ## Publishing
61
+
62
+ See [PUBLISHING.md](PUBLISHING.md) for TestPyPI and production PyPI upload steps with twine.
@@ -0,0 +1,40 @@
1
+ # survival_analysis_tools
2
+
3
+ Kaplan–Meier plotting and Cox HR helpers for survival analysis workflows.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install survival_analysis_tools
9
+ ```
10
+
11
+ From TestPyPI:
12
+
13
+ ```bash
14
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ survival_analysis_tools
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from survival_analysis_tools import plot_km, cox_over_df
21
+
22
+ summary, hr, pval = plot_km(
23
+ df,
24
+ time_col="survival",
25
+ event_col="event",
26
+ group_col="usage_bin",
27
+ )
28
+ ```
29
+
30
+ ## Development
31
+
32
+ ```powershell
33
+ pip install -e ".[dev]"
34
+ python -m pytest
35
+ python -m build
36
+ ```
37
+
38
+ ## Publishing
39
+
40
+ See [PUBLISHING.md](PUBLISHING.md) for TestPyPI and production PyPI upload steps with twine.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "survival_analysis_tools"
7
+ version = "0.1.1"
8
+ description = "Survival analysis helpers: Kaplan–Meier plots and Cox HR utilities."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [{ name = "Dina Daitchman", email = "ddaitchman@novocure.com" }]
12
+ license = { text = "MIT" }
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
17
+ ]
18
+
19
+ dependencies = [
20
+ "numpy>=1.20",
21
+ "pandas>=1.3",
22
+ "matplotlib>=3.5",
23
+ "lifelines>=0.27",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "pytest>=7.0",
29
+ "build>=1.0",
30
+ "twine>=4.0",
31
+ ]
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ from survival_analysis_tools.plot_km import cox_over_df, plot_km
2
+
3
+
4
+ def hello(name: str = "world") -> str:
5
+ return f"Hello, {name} from survival_analysis_tools!"
6
+
7
+
8
+ __all__ = [
9
+ "hello",
10
+ "cox_over_df",
11
+ "plot_km",
12
+ ]
@@ -0,0 +1,379 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import matplotlib.pyplot as plt
4
+ import warnings
5
+
6
+ from lifelines import KaplanMeierFitter, CoxPHFitter
7
+ from lifelines.plotting import add_at_risk_counts
8
+
9
+
10
+ def cox_over_df(
11
+ df,
12
+ time_col='survival',
13
+ event_col='event',
14
+ group_col='usage_bin',
15
+ weights_col=None,
16
+ cluster_col=None,
17
+ robust=False,
18
+ ):
19
+ """
20
+ Computes HR, 95% CI, and p-value from Cox proportional-hazards model, optionally using weights and robust variance clustering.
21
+
22
+ Parameters
23
+ ----------
24
+ df : pd.DataFrame
25
+ time_col : str
26
+ event_col : str
27
+ group_col : str
28
+ weights_col : str or None
29
+ Optional column of weights.
30
+ cluster_col : str or None
31
+ Optional column for robust variance clustering. If provided, passed as cluster_col to lifelines' CoxPHFitter.
32
+
33
+ Returns
34
+ -------
35
+ list: [HR, pval, ci_low, ci_high]
36
+ HR (hazard ratio), p-value, lower 95% CI, upper 95% CI
37
+ """
38
+ # Keep only needed columns and copy
39
+ needed_cols = [time_col, event_col, group_col]
40
+ if weights_col is not None and weights_col in df.columns:
41
+ needed_cols.append(weights_col)
42
+ if cluster_col is not None and cluster_col in df.columns:
43
+ needed_cols.append(cluster_col)
44
+ X = df[needed_cols].copy()
45
+
46
+ # Coerce time to numeric
47
+ X[time_col] = pd.to_numeric(X[time_col], errors='coerce')
48
+ # Coerce event to numeric 0/1
49
+ X[event_col] = pd.to_numeric(X[event_col], errors='coerce')
50
+ # Drop rows with missing essentials
51
+ missing_cols = [time_col, event_col, group_col]
52
+ if cluster_col is not None:
53
+ missing_cols.append(cluster_col)
54
+ X = X.dropna(subset=missing_cols)
55
+ # Keep only valid event codes 0/1
56
+ X = X[X[event_col].isin([0, 1])]
57
+ # Exactly two groups required
58
+ X[group_col] = X[group_col].astype('category')
59
+ if X[group_col].nunique() != 2 or len(X) < 2:
60
+ return [np.nan, np.nan, np.nan, np.nan]
61
+
62
+ try:
63
+ cph = CoxPHFitter()
64
+ fit_kwargs = dict(
65
+ duration_col=time_col,
66
+ event_col=event_col,
67
+ formula=f"C({group_col})",
68
+ robust=robust
69
+ )
70
+ if weights_col is not None and weights_col in X.columns:
71
+ fit_kwargs["weights_col"] = weights_col
72
+ if cluster_col is not None and cluster_col in X.columns:
73
+ fit_kwargs["cluster_col"] = cluster_col
74
+ cph.fit(X, **fit_kwargs)
75
+ # With 2 groups there is a single coefficient row
76
+ row = cph.summary.iloc[0]
77
+ HR = row["exp(coef)"]
78
+ pval = row["p"] # Wald test p-value from the Cox model
79
+ ci_low = row.get("exp(coef) lower 95%", np.nan)
80
+ ci_high = row.get("exp(coef) upper 95%", np.nan)
81
+ return [HR, pval, ci_low, ci_high]
82
+ except Exception:
83
+ return [np.nan, np.nan, np.nan, np.nan]
84
+
85
+
86
+ def median_ci(fitter):
87
+ """
88
+ Returns (lower, upper) time values such that survival probability 95% CI crosses 0.5.
89
+ """
90
+ try:
91
+ ci = fitter.confidence_interval_
92
+ times = ci.index
93
+ lower, upper = ci.iloc[:, 0], ci.iloc[:, 1]
94
+ l = times[lower < 0.5][0] if any(lower < 0.5) else np.nan
95
+ h = times[upper < 0.5][0] if any(upper < 0.5) else np.nan
96
+ return l, h
97
+ except Exception:
98
+ return np.nan, np.nan
99
+
100
+
101
+ def fmt(m, ci):
102
+ if not np.isfinite(m): return "NA"
103
+ l, h = ci
104
+ if np.isfinite(l) and np.isfinite(h): return f"{m:.1f} [{l:.1f}-{h:.1f}]"
105
+ if np.isfinite(l): return f"{m:.1f} [{l:.1f}-NA]"
106
+ if np.isfinite(h): return f"{m:.1f} [NA-{h:.1f}]"
107
+ return f"{m:.1f}"
108
+
109
+
110
+ def plot_km(
111
+ df,
112
+ time_col="survival",
113
+ event_col="event",
114
+ group_col="usage_bin",
115
+ group_order=None,
116
+ show_censors=True,
117
+ show_ci=True,
118
+ show_at_risk=True,
119
+ xlabel='Time [months]',
120
+ ylabel="Survival probability",
121
+ title="Survival probability vs. Usage Bin",
122
+ font_scale: float = 1.3,
123
+ plot_height=5,
124
+ xlim=False,
125
+ show_stats=True,
126
+ ax=None,
127
+ show=True,
128
+ weights_col=None,
129
+ cluster_col=None,
130
+ robust=False,
131
+ created_fig=True,
132
+ flip_legend_order=False
133
+ ):
134
+ """
135
+ Plot Kaplan-Meier curves grouped by `group_col`. If group_col is None, plot KM for all data.
136
+
137
+ Now returns a DataFrame with cols: group_name, n, median, median_ci_low, median_ci_high.
138
+
139
+ Parameters
140
+ ----------
141
+ df : pd.DataFrame
142
+ Dataframe containing the survival data.
143
+ time_col : str
144
+ Column for durations.
145
+ event_col : str
146
+ Column for event indicators.
147
+ group_col : str or None
148
+ Column for group assignment (if None, all data is treated as one group).
149
+ group_order : list, optional
150
+ List controlling the plotting order for groups.
151
+ show_censors : bool
152
+ Show censors (default True).
153
+ show_ci : bool
154
+ Show confidence intervals.
155
+ show_at_risk : bool
156
+ Add at-risk counts table.
157
+ xlabel, ylabel, title : str
158
+ Plot labels.
159
+ font_scale : float
160
+ Font scaling.
161
+ plot_height : float
162
+ Figure height.
163
+ xlim : float or False
164
+ X-axis upper bound.
165
+ show_stats : bool
166
+ Whether to show HR/p-value on plot.
167
+ ax : matplotlib Axes, optional
168
+ Axes to plot into.
169
+ show : bool
170
+ Whether to call plt.show().
171
+ weights_col : str or None
172
+ Optional column of weights to use in fitting KM curves, Cox model, and logrank test.
173
+ flip_legend_order : bool
174
+ If True, reverse the legend order.
175
+ robust : bool
176
+ Set 'robust' parameter when calling CoxPHFitter. Default False.
177
+ """
178
+
179
+ created_fig = False
180
+ if ax is None:
181
+ fig, ax = plt.subplots(figsize=(8, plot_height))
182
+ created_fig = True
183
+
184
+ rows = []
185
+ fitters = []
186
+
187
+ if group_col is None:
188
+ # All one group; just plot one KM curve
189
+ kmf = KaplanMeierFitter(label="All")
190
+ fit_args = dict(
191
+ durations=df[time_col],
192
+ event_observed=df[event_col],
193
+ )
194
+ if weights_col is not None and weights_col in df.columns:
195
+ fit_args["weights"] = df[weights_col]
196
+ # lifelines <=0.27.5 emits warning if any weights==0, so filter those out
197
+ if weights_col is not None and weights_col in df.columns:
198
+ mask = (df[weights_col] > 0)
199
+ # If all weights are zero, skip plotting
200
+ if mask.sum() == 0:
201
+ # No data to plot
202
+ if created_fig and show:
203
+ plt.tight_layout()
204
+ plt.show()
205
+ return pd.DataFrame([])
206
+ for k in ["durations", "event_observed", "weights"]:
207
+ fit_args[k] = fit_args[k][mask]
208
+ with warnings.catch_warnings():
209
+ warnings.simplefilter("ignore")
210
+ kmf.fit(**fit_args)
211
+ kmf.plot(
212
+ ax=ax,
213
+ ci_show=show_ci,
214
+ lw=2,
215
+ show_censors=show_censors,
216
+ censor_styles={'ms': 6, 'marker': '+'}
217
+ )
218
+ med = kmf.median_survival_time_
219
+ median_low, median_high = median_ci(kmf)
220
+ fitters.append(kmf)
221
+
222
+ if weights_col is not None and weights_col in df.columns:
223
+ n = float(df[weights_col].sum())
224
+ else:
225
+ n = int(df.shape[0])
226
+
227
+ rows.append({
228
+ "group_name": "All",
229
+ "n": int(n),
230
+ "median": float(med) if np.isfinite(med) else np.nan,
231
+ "median_ci_low": float(median_low) if np.isfinite(median_low) else np.nan,
232
+ "median_ci_high": float(median_high) if np.isfinite(median_high) else np.nan,
233
+ })
234
+ else:
235
+ if group_order is None:
236
+ group_order = sorted(df[group_col].unique())
237
+ else:
238
+ group_order = [g for g in group_order if g in df[group_col].unique()]
239
+ for grp_name in group_order:
240
+ g = df[df[group_col] == grp_name]
241
+ if g.empty:
242
+ continue
243
+
244
+ kmf = KaplanMeierFitter(label=str(grp_name))
245
+
246
+ fit_args = dict(
247
+ durations=g[time_col],
248
+ event_observed=g[event_col],
249
+ )
250
+ if weights_col is not None and weights_col in g.columns:
251
+ fit_args["weights"] = g[weights_col]
252
+ if weights_col is not None and weights_col in g.columns:
253
+ mask = (g[weights_col] > 0)
254
+ if mask.sum() == 0:
255
+ continue # skip group with all zero weights
256
+ for k in ["durations", "event_observed", "weights"]:
257
+ fit_args[k] = fit_args[k][mask]
258
+
259
+ with warnings.catch_warnings():
260
+ warnings.simplefilter("ignore")
261
+ kmf.fit(**fit_args)
262
+ kmf.plot(
263
+ ax=ax,
264
+ ci_show=show_ci,
265
+ lw=2,
266
+ show_censors=show_censors,
267
+ censor_styles={'ms': 6, 'marker': '+'}
268
+ )
269
+
270
+ med = kmf.median_survival_time_
271
+ median_low, median_high = median_ci(kmf)
272
+ fitters.append(kmf)
273
+
274
+ if weights_col is not None and weights_col in g.columns:
275
+ n = float(g[weights_col].sum())
276
+ else:
277
+ n = int(g.shape[0])
278
+
279
+ rows.append({
280
+ "group_name": str(grp_name),
281
+ "n": int(n),
282
+ "median": float(med) if np.isfinite(med) else np.nan,
283
+ "median_ci_low": float(median_low) if np.isfinite(median_low) else np.nan,
284
+ "median_ci_high": float(median_high) if np.isfinite(median_high) else np.nan,
285
+ })
286
+
287
+ # ---- cosmetics ----
288
+ if title is None:
289
+ title = f"Survival Probability" if group_col is None else f"Survival Probability by {group_col}"
290
+ ax.set_title(title, fontsize=16 * font_scale, weight="bold")
291
+ ax.set_xlabel(xlabel, fontsize=14 * font_scale)
292
+ ax.set_ylabel(ylabel, fontsize=14 * font_scale)
293
+ ax.tick_params(axis="both", labelsize=12 * font_scale)
294
+ ax.grid(True, ls="--", alpha=0.35)
295
+ ax.set_ylim(0, 1.0)
296
+
297
+ if xlim:
298
+ ax.set_xlim(0, xlim)
299
+
300
+ # ---- Legend with n & median ----
301
+ handles, labels = ax.get_legend_handles_labels()
302
+
303
+ medians = {r["group_name"]: r["median"] for r in rows}
304
+ cis = {r["group_name"]: (r["median_ci_low"], r["median_ci_high"]) for r in rows}
305
+
306
+ if handles:
307
+ if flip_legend_order:
308
+ handles = handles[::-1]
309
+ labels = labels[::-1]
310
+ ax.legend(
311
+ handles,
312
+ [f"{lab} (median={fmt(medians.get(lab, np.nan), cis.get(lab, (np.nan, np.nan)))})" for lab in labels],
313
+ fontsize=14 * font_scale,
314
+ loc='upper right',
315
+ frameon=False,
316
+ )
317
+
318
+ # ---- At-risk counts ----
319
+ if show_at_risk and fitters:
320
+ # lifelines' add_at_risk_counts ignores weights, so this may mismatch with weighted.
321
+ add_at_risk_counts(
322
+ *fitters,
323
+ ax=ax,
324
+ rows_to_show=['At risk'],
325
+ fontsize=10 * font_scale,
326
+ )
327
+
328
+ # ---- HR, CI & p-value (respect group_order, only if group_col provided) ----
329
+ if show_stats and group_col is not None and len(fitters) > 1:
330
+ df_cox = df.copy()
331
+ df_cox[group_col] = pd.Categorical(
332
+ df_cox[group_col],
333
+ categories=group_order,
334
+ ordered=True,
335
+ )
336
+ HR, pval, ci_low, ci_high = cox_over_df(
337
+ df_cox,
338
+ time_col=time_col,
339
+ event_col=event_col,
340
+ group_col=group_col,
341
+ weights_col=weights_col,
342
+ cluster_col=cluster_col,
343
+ robust=robust, # <-- propagate robust!
344
+ )
345
+ if pd.notna(HR):
346
+ # p-value formatting: show <0.001 if smaller than 0.001
347
+ if pd.notna(pval) and pval < 0.001:
348
+ pval_str = "<0.001"
349
+ elif pd.notna(pval):
350
+ pval_str = f"{pval:.3g}"
351
+ else:
352
+ pval_str = "NA"
353
+ # CI formatting
354
+ if pd.notna(ci_low) and pd.notna(ci_high):
355
+ ci_str = f"{HR:.2f} [{ci_low:.2f}, {ci_high:.2f}]"
356
+ elif pd.notna(ci_low):
357
+ ci_str = f"{HR:.2f} [{ci_low:.2f}, NA]"
358
+ elif pd.notna(ci_high):
359
+ ci_str = f"{HR:.2f} [NA, {ci_high:.2f}]"
360
+ else:
361
+ ci_str = f"{HR:.2f}"
362
+
363
+ ax.text(
364
+ 0.02, 0.2,
365
+ f"HR = {ci_str}\np = {pval_str}",
366
+ transform=ax.transAxes,
367
+ ha='left', va='top',
368
+ fontsize=12 * font_scale,
369
+ bbox=dict(
370
+ boxstyle='round,pad=0.3',
371
+ fc='white', ec='0.5', alpha=0.9,
372
+ ),
373
+ )
374
+
375
+ if created_fig and show:
376
+ plt.tight_layout()
377
+ plt.show()
378
+
379
+ return pd.DataFrame(rows), HR, pval
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: survival_analysis_tools
3
+ Version: 0.1.1
4
+ Summary: Survival analysis helpers: Kaplan–Meier plots and Cox HR utilities.
5
+ Author-email: Dina Daitchman <ddaitchman@novocure.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy>=1.20
14
+ Requires-Dist: pandas>=1.3
15
+ Requires-Dist: matplotlib>=3.5
16
+ Requires-Dist: lifelines>=0.27
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0; extra == "dev"
19
+ Requires-Dist: build>=1.0; extra == "dev"
20
+ Requires-Dist: twine>=4.0; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # survival_analysis_tools
24
+
25
+ Kaplan–Meier plotting and Cox HR helpers for survival analysis workflows.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install survival_analysis_tools
31
+ ```
32
+
33
+ From TestPyPI:
34
+
35
+ ```bash
36
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ survival_analysis_tools
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from survival_analysis_tools import plot_km, cox_over_df
43
+
44
+ summary, hr, pval = plot_km(
45
+ df,
46
+ time_col="survival",
47
+ event_col="event",
48
+ group_col="usage_bin",
49
+ )
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ```powershell
55
+ pip install -e ".[dev]"
56
+ python -m pytest
57
+ python -m build
58
+ ```
59
+
60
+ ## Publishing
61
+
62
+ See [PUBLISHING.md](PUBLISHING.md) for TestPyPI and production PyPI upload steps with twine.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/survival_analysis_tools/__init__.py
5
+ src/survival_analysis_tools/plot_km.py
6
+ src/survival_analysis_tools.egg-info/PKG-INFO
7
+ src/survival_analysis_tools.egg-info/SOURCES.txt
8
+ src/survival_analysis_tools.egg-info/dependency_links.txt
9
+ src/survival_analysis_tools.egg-info/requires.txt
10
+ src/survival_analysis_tools.egg-info/top_level.txt
11
+ tests/test_hello.py
@@ -0,0 +1,9 @@
1
+ numpy>=1.20
2
+ pandas>=1.3
3
+ matplotlib>=3.5
4
+ lifelines>=0.27
5
+
6
+ [dev]
7
+ pytest>=7.0
8
+ build>=1.0
9
+ twine>=4.0
@@ -0,0 +1,40 @@
1
+ import pandas as pd
2
+
3
+ from survival_analysis_tools import cox_over_df, hello, plot_km
4
+
5
+
6
+ def test_hello_default():
7
+ assert hello() == "Hello, world from survival_analysis_tools!"
8
+
9
+
10
+ def test_plot_km_imports_and_runs():
11
+ df = pd.DataFrame(
12
+ {
13
+ "survival": [3, 5, 8, 10, 12, 15],
14
+ "event": [1, 1, 0, 1, 0, 1],
15
+ "usage_bin": ["low", "low", "low", "high", "high", "high"],
16
+ }
17
+ )
18
+ summary, hr, pval = plot_km(df, show=False, show_at_risk=False)
19
+ assert list(summary.columns) == [
20
+ "group_name",
21
+ "n",
22
+ "median",
23
+ "median_ci_low",
24
+ "median_ci_high",
25
+ ]
26
+ assert len(summary) == 2
27
+ assert pd.notna(hr)
28
+ assert pd.notna(pval)
29
+
30
+
31
+ def test_cox_over_df():
32
+ df = pd.DataFrame(
33
+ {
34
+ "survival": [3, 5, 8, 10, 12, 15],
35
+ "event": [1, 1, 0, 1, 0, 1],
36
+ "usage_bin": ["low", "low", "low", "high", "high", "high"],
37
+ }
38
+ )
39
+ hr, pval, ci_low, ci_high = cox_over_df(df)
40
+ assert all(isinstance(x, float) for x in (hr, pval, ci_low, ci_high))