lifelines 0.30.0__tar.gz → 0.30.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.
Files changed (90) hide show
  1. {lifelines-0.30.0/lifelines.egg-info → lifelines-0.30.1}/PKG-INFO +14 -3
  2. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/__init__.py +8 -4
  3. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/aalen_additive_fitter.py +4 -4
  4. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/aalen_johansen_fitter.py +19 -13
  5. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/cox_time_varying_fitter.py +5 -3
  6. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/coxph_fitter.py +8 -4
  7. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/plotting.py +5 -2
  8. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/statistics.py +2 -3
  9. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/test_estimation.py +21 -0
  10. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/test_npmle.py +1 -0
  11. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/test_plotting.py +17 -0
  12. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/test_statistics.py +18 -0
  13. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/utils/test_utils.py +40 -0
  14. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/utils/__init__.py +51 -0
  15. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/version.py +1 -1
  16. {lifelines-0.30.0 → lifelines-0.30.1/lifelines.egg-info}/PKG-INFO +14 -3
  17. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines.egg-info/requires.txt +1 -1
  18. {lifelines-0.30.0 → lifelines-0.30.1}/reqs/base-requirements.txt +1 -1
  19. {lifelines-0.30.0 → lifelines-0.30.1}/LICENSE +0 -0
  20. {lifelines-0.30.0 → lifelines-0.30.1}/MANIFEST.in +0 -0
  21. {lifelines-0.30.0 → lifelines-0.30.1}/README.md +0 -0
  22. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/__init__.py +0 -0
  23. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/calibration.py +0 -0
  24. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/CuZn-LeftCensoredDataset.csv +0 -0
  25. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/__init__.py +0 -0
  26. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/anderson.csv +0 -0
  27. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/c_botulinum_lag_phase.csv +0 -0
  28. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/canadian_senators.csv +0 -0
  29. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/dd.csv +0 -0
  30. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/dfcv_dataset.py +0 -0
  31. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/divorce.dat +0 -0
  32. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/g3.csv +0 -0
  33. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/gbsg2.csv +0 -0
  34. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/gehan.dat +0 -0
  35. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/holly_molly_polly.tsv +0 -0
  36. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/interval_diabetes.csv +0 -0
  37. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/kidney_transplant.csv +0 -0
  38. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/larynx.csv +0 -0
  39. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/lung.csv +0 -0
  40. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/lymph_node.csv +0 -0
  41. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/lymphoma.csv +0 -0
  42. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/merrell1955.csv +0 -0
  43. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/mice.csv +0 -0
  44. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/multicenter_aids_cohort.tsv +0 -0
  45. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/nh4.csv +0 -0
  46. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/panel_test.csv +0 -0
  47. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/psychiatric_patients.csv +0 -0
  48. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/recur.csv +0 -0
  49. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/regression.csv +0 -0
  50. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/rossi.csv +0 -0
  51. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/stanford_heart.csv +0 -0
  52. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/static_test.csv +0 -0
  53. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/datasets/waltons_dataset.csv +0 -0
  54. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/exceptions.py +0 -0
  55. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/breslow_fleming_harrington_fitter.py +0 -0
  56. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/crc_spline_fitter.py +0 -0
  57. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/exponential_fitter.py +0 -0
  58. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/generalized_gamma_fitter.py +0 -0
  59. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/generalized_gamma_regression_fitter.py +0 -0
  60. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/kaplan_meier_fitter.py +0 -0
  61. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/log_logistic_aft_fitter.py +0 -0
  62. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/log_logistic_fitter.py +0 -0
  63. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/log_normal_aft_fitter.py +0 -0
  64. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/log_normal_fitter.py +0 -0
  65. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/mixins.py +0 -0
  66. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/mixture_cure_fitter.py +0 -0
  67. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/nelson_aalen_fitter.py +0 -0
  68. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/npmle.py +0 -0
  69. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/piecewise_exponential_fitter.py +0 -0
  70. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/piecewise_exponential_regression_fitter.py +0 -0
  71. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/spline_fitter.py +0 -0
  72. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/weibull_aft_fitter.py +0 -0
  73. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/fitters/weibull_fitter.py +0 -0
  74. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/generate_datasets.py +0 -0
  75. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/__init__.py +0 -0
  76. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/test_generate_datasets.py +0 -0
  77. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/utils/test_btree.py +0 -0
  78. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/tests/utils/test_concordance.py +0 -0
  79. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/utils/btree.py +0 -0
  80. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/utils/concordance.py +0 -0
  81. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/utils/lowess.py +0 -0
  82. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/utils/printer.py +0 -0
  83. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines/utils/safe_exp.py +0 -0
  84. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines.egg-info/SOURCES.txt +0 -0
  85. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines.egg-info/dependency_links.txt +0 -0
  86. {lifelines-0.30.0 → lifelines-0.30.1}/lifelines.egg-info/top_level.txt +0 -0
  87. {lifelines-0.30.0 → lifelines-0.30.1}/reqs/dev-requirements.txt +0 -0
  88. {lifelines-0.30.0 → lifelines-0.30.1}/reqs/docs-requirements.txt +0 -0
  89. {lifelines-0.30.0 → lifelines-0.30.1}/setup.cfg +0 -0
  90. {lifelines-0.30.0 → lifelines-0.30.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: lifelines
3
- Version: 0.30.0
3
+ Version: 0.30.1
4
4
  Summary: Survival analysis in Python, including Kaplan Meier, Nelson Aalen and regression
5
5
  Home-page: https://github.com/CamDavidsonPilon/lifelines
6
6
  Author: Cameron Davidson-Pilon
@@ -18,11 +18,22 @@ Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: numpy>=1.14.0
20
20
  Requires-Dist: scipy>=1.7.0
21
- Requires-Dist: pandas>=2.1
21
+ Requires-Dist: pandas<3.0,>=2.1
22
22
  Requires-Dist: matplotlib>=3.0
23
23
  Requires-Dist: autograd>=1.5
24
24
  Requires-Dist: autograd-gamma>=0.3
25
25
  Requires-Dist: formulaic>=0.2.2
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: home-page
32
+ Dynamic: license
33
+ Dynamic: license-file
34
+ Dynamic: requires-dist
35
+ Dynamic: requires-python
36
+ Dynamic: summary
26
37
 
27
38
  ![](http://i.imgur.com/EOowdSD.png)
28
39
 
@@ -2,7 +2,7 @@
2
2
  from __future__ import annotations
3
3
  from functools import partial, wraps
4
4
  from inspect import getfullargspec
5
- from datetime import datetime
5
+ from datetime import datetime, UTC
6
6
  from textwrap import dedent
7
7
  import typing as t
8
8
  import collections
@@ -1229,11 +1229,15 @@ class ParametricUnivariateFitter(UnivariateFitter):
1229
1229
  # use numerical solver to find the value p = e^{-H(t)}. I think I could use `root` in scipy
1230
1230
  # instead of the scalar version. TODO
1231
1231
  def _find_root(_p):
1232
- f = lambda t: _p - self.survival_function_at_times(t).values
1233
- fprime = lambda t: self.survival_function_at_times(t).values * self.hazard_at_times(t).values
1232
+ survival_at_t = lambda t: float(self.survival_function_at_times(t).values[0])
1233
+ hazard_at_t = lambda t: float(self.hazard_at_times(t).values[0])
1234
+ f = lambda t: _p - survival_at_t(t)
1235
+ fprime = lambda t: survival_at_t(t) * hazard_at_t(t)
1234
1236
  return root_scalar(f, bracket=(1e-10, 2 * self.timeline[-1]), fprime=fprime, x0=1.0).root
1235
1237
 
1236
1238
  try:
1239
+ if np.isscalar(p):
1240
+ return float(_find_root(p))
1237
1241
  find_root = np.vectorize(_find_root, otypes=[float])
1238
1242
  return find_root(p)
1239
1243
  except ValueError:
@@ -1776,7 +1780,7 @@ class ParametricRegressionFitter(RegressionFitter):
1776
1780
  fit_options: Optional[dict] = None,
1777
1781
  ) -> ParametricRegressionFitter:
1778
1782
 
1779
- self._time_fit_was_called = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") + " UTC"
1783
+ self._time_fit_was_called = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S") + " UTC"
1780
1784
  self._n_examples = df.shape[0]
1781
1785
  self.weights_col = weights_col
1782
1786
  self.entry_col = entry_col
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import warnings
3
- from datetime import datetime
3
+ from datetime import datetime, UTC
4
4
  import time
5
5
 
6
6
  import numpy as np
@@ -153,7 +153,7 @@ class AalenAdditiveFitter(RegressionFitter):
153
153
  aaf.print_summary()
154
154
 
155
155
  """
156
- self._time_fit_was_called = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") + " UTC"
156
+ self._time_fit_was_called = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S") + " UTC"
157
157
 
158
158
  df = df.copy()
159
159
 
@@ -244,7 +244,7 @@ class AalenAdditiveFitter(RegressionFitter):
244
244
 
245
245
  hazards_[i, :] = v
246
246
 
247
- variance_hazards_[i, :] = (V ** 2).sum(1)
247
+ variance_hazards_[i, :] = (V**2).sum(1)
248
248
 
249
249
  X[exits, :] = 0
250
250
 
@@ -500,7 +500,7 @@ It's important to know that the naive variance estimates of the coefficients are
500
500
  # normally (weights * X).dot(Y) / X.dot(weights * X), but we have a slightly different form here.
501
501
  beta = X.dot(Y) / X.dot(weights * X)
502
502
  errors = Y.values - np.outer(X, beta)
503
- var = (errors ** 2).sum(0) / (Y.shape[0] - 2) / X.dot(weights * X)
503
+ var = (errors**2).sum(0) / (Y.shape[0] - 2) / X.dot(weights * X)
504
504
  return beta, np.sqrt(var)
505
505
 
506
506
  weights = survival_table_from_events(self.durations, self.event_observed).loc[self._index, "at_risk"].values
@@ -5,7 +5,7 @@ import pandas as pd
5
5
  import warnings
6
6
 
7
7
  from lifelines.fitters import NonParametricUnivariateFitter
8
- from lifelines.utils import _preprocess_inputs, inv_normal_cdf, CensoringType, coalesce
8
+ from lifelines.utils import _preprocess_inputs, inv_normal_cdf, CensoringType, coalesce, LinearAccumulator, QuadraticAccumulator
9
9
  from lifelines import KaplanMeierFitter
10
10
  from lifelines.plotting import _plot_estimate
11
11
 
@@ -219,22 +219,28 @@ class AalenJohansenFitter(NonParametricUnivariateFitter):
219
219
  ci_labels = ["%s_upper_%g" % (self._label, ci), "%s_lower_%g" % (self._label, ci)]
220
220
  assert len(ci_labels) == 2, "ci_labels should be a length 2 array."
221
221
 
222
- # Have to loop through each time independently. Don't think there is a faster way
222
+ # Use prefix sum algorithm to reduce time complexity to O(n), by cumulatively updating terms.
223
+ first_term_function = QuadraticAccumulator()
224
+ second_term = 0
225
+ third_term_function = LinearAccumulator()
223
226
  all_vars = []
224
227
  for _, r in df.iterrows():
225
- sf = df.loc[df.index <= r.name].copy()
226
228
  F_t = float(r["Ft"])
227
- first_term = np.sum((F_t - sf["Ft"]) ** 2 * sf["observed"] / sf["at_risk"] / (sf["at_risk"] - sf["observed"]))
228
- second_term = np.sum(
229
- sf["lagS"] ** 2
230
- / sf["at_risk"]
231
- * sf[self.label_cmprisk]
232
- / sf["at_risk"]
233
- * (sf["at_risk"] - sf[self.label_cmprisk])
234
- / sf["at_risk"]
229
+
230
+ first_term_coefficient = r["observed"] / r["at_risk"] / (r["at_risk"] - r["observed"])
231
+ first_term_function.add_quadratic_term(a = first_term_coefficient, b = F_t)
232
+
233
+ second_term += (
234
+ (r["lagS"] ** 2)
235
+ * r[self.label_cmprisk]
236
+ * (r["at_risk"] - r[self.label_cmprisk])
237
+ / (r["at_risk"] ** 3)
235
238
  )
236
- third_term = np.sum((F_t - sf["Ft"]) / sf["at_risk"] * sf["lagS"] * sf[self.label_cmprisk] / sf["at_risk"])
237
- variance = first_term + second_term - 2 * third_term
239
+
240
+ third_term_coefficient = r["lagS"] * r[self.label_cmprisk] / (r["at_risk"] ** 2)
241
+ third_term_function.add_linear_term(a = third_term_coefficient, b = F_t)
242
+
243
+ variance = first_term_function.evaluate(F_t) + second_term - 2 * third_term_function.evaluate(F_t)
238
244
  all_vars.append(variance)
239
245
  df["variance"] = all_vars
240
246
 
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
 
4
- from datetime import datetime
4
+ from datetime import datetime, UTC
5
5
  import warnings
6
6
  import time
7
7
  from typing import Optional
@@ -172,7 +172,7 @@ class CoxTimeVaryingFitter(SemiParametricRegressionFitter, ProportionalHazardMix
172
172
  self.stop_col = stop_col
173
173
  self.start_col = start_col
174
174
  self.formula = formula
175
- self._time_fit_was_called = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") + " UTC"
175
+ self._time_fit_was_called = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S") + " UTC"
176
176
 
177
177
  df = df.copy()
178
178
 
@@ -801,7 +801,9 @@ See https://stats.stackexchange.com/questions/11109/how-to-deal-with-perfect-sep
801
801
  hazards = self.predict_partial_hazard(tv_data).values
802
802
 
803
803
  unique_death_times = np.unique(stop[events.values])
804
- baseline_hazard_ = pd.DataFrame(np.zeros_like(unique_death_times).astype(float), index=unique_death_times, columns=["baseline hazard"])
804
+ baseline_hazard_ = pd.DataFrame(
805
+ np.zeros_like(unique_death_times).astype(float), index=unique_death_times, columns=["baseline hazard"]
806
+ )
805
807
 
806
808
  for t in unique_death_times:
807
809
  ix = (start.values < t) & (t <= stop.values)
@@ -2,7 +2,7 @@
2
2
  from __future__ import annotations
3
3
  from typing import Callable, Iterator, List, Optional, Tuple, Union, Any, Iterable
4
4
  from textwrap import dedent, fill
5
- from datetime import datetime
5
+ from datetime import datetime, UTC
6
6
  import warnings
7
7
  import time
8
8
 
@@ -1214,7 +1214,7 @@ class SemiParametricPHFitter(ProportionalHazardMixin, SemiParametricRegressionFi
1214
1214
  cph.predict_median(df)
1215
1215
 
1216
1216
  """
1217
- self._time_fit_was_called = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") + " UTC"
1217
+ self._time_fit_was_called = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S") + " UTC"
1218
1218
  self.duration_col = duration_col
1219
1219
  self.event_col = event_col
1220
1220
  self.robust = robust
@@ -2693,9 +2693,13 @@ See https://stats.stackexchange.com/q/11109/11867 for more.\n",
2693
2693
  if self.strata:
2694
2694
  df = df.set_index(self.strata)
2695
2695
 
2696
- df = df.sort_values([self.duration_col, self.event_col])
2696
+ df = df.sort_values([col for col in [self.duration_col, self.event_col] if (col is not None)])
2697
2697
  T = df.pop(self.duration_col).astype(float)
2698
- E = df.pop(self.event_col).astype(bool)
2698
+ E = (
2699
+ df.pop(self.event_col)
2700
+ if (self.event_col is not None)
2701
+ else pd.Series(np.ones(len(df.index)), index=df.index, name="E")
2702
+ ).astype(bool)
2699
2703
  W = df.pop(self.weights_col) if self.weights_col else pd.Series(np.ones_like(E), index=T.index)
2700
2704
  entries = df.pop(self.entry_col) if self.entry_col else None
2701
2705
 
@@ -540,7 +540,10 @@ def add_at_risk_counts(
540
540
  event_table_slice.loc[:tick, ["at_risk", "censored", "observed"]]
541
541
  .agg(
542
542
  {
543
- "at_risk": lambda x: x.tail(1).values,
543
+ # `Series.tail(1).values` is a 1D array of length 1. In NumPy>=2.4,
544
+ # `int(np.array([1]))` raises `TypeError: only 0-dimensional arrays can be converted to Python scalars`.
545
+ # Extract a Python scalar for compatibility.
546
+ "at_risk": lambda x: x.tail(1).values.item(),
544
547
  "censored": "sum",
545
548
  "observed": "sum",
546
549
  }
@@ -554,7 +557,7 @@ def add_at_risk_counts(
554
557
  )
555
558
  .fillna(0)
556
559
  )
557
- counts.extend([int(c) for c in event_table_slice.loc[rows_to_show]])
560
+ counts.extend([int(np.asarray(c).item()) for c in event_table_slice.loc[rows_to_show]])
558
561
  else:
559
562
  counts.extend([0 for _ in range(n_rows)])
560
563
  if n_rows > 1:
@@ -114,9 +114,9 @@ class StatisticalResult:
114
114
  self._print_specific_style(style, decimals=decimals, **kwargs)
115
115
  else:
116
116
  try:
117
- from IPython.display import display
117
+ from IPython.display import HTML, display
118
118
 
119
- display(self)
119
+ display(HTML(self.to_html(decimals=decimals, **kwargs)))
120
120
  except ImportError:
121
121
  self._ascii_print(decimals=decimals, **kwargs)
122
122
 
@@ -175,7 +175,6 @@ class StatisticalResult:
175
175
  def to_ascii(self, decimals=2, **kwargs):
176
176
  extra_kwargs = dict(list(self._kwargs.items()) + list(kwargs.items()))
177
177
  meta_data = self._stringify_meta_data(extra_kwargs)
178
-
179
178
  df = self.summary
180
179
 
181
180
  s = "<lifelines.StatisticalResult: {0}>".format(self.test_name)
@@ -482,6 +482,7 @@ class TestUnivariateFitters:
482
482
  assert not isinstance(fitter.predict(1), Iterable)
483
483
  assert isinstance(fitter.predict([1, 2]), Iterable)
484
484
 
485
+ @flaky
485
486
  def test_cumulative_density_ci_is_ordered_correctly(self, positive_sample_lifetimes, univariate_fitters):
486
487
  T = positive_sample_lifetimes[0]
487
488
  for f in univariate_fitters:
@@ -558,6 +559,7 @@ class TestUnivariateFitters:
558
559
  fitter.fit(positive_sample_lifetimes[0], ci_labels=expected)
559
560
  npt.assert_array_equal(fitter.confidence_interval_.columns, expected)
560
561
 
562
+ @flaky
561
563
  def test_ci_is_not_all_nan(self, positive_sample_lifetimes, univariate_fitters):
562
564
  for f in univariate_fitters:
563
565
  fitter = f()
@@ -2119,6 +2121,25 @@ class TestRegressionFitters:
2119
2121
  formula = "%s" % subset[-1]
2120
2122
  fitter.fit(df[subset], duration_col="t", formula=formula)
2121
2123
 
2124
+ def test_categorical_prediction(self):
2125
+ df = pd.DataFrame.from_dict(
2126
+ {
2127
+ "t": [1.0, 5.0, 3.0, 4.0, 6.0, 1.0],
2128
+ "categoryb_": pd.Series(["a", "a", "b", "b", "c", "c"], dtype="category"),
2129
+ }
2130
+ )
2131
+
2132
+ df_to_predict = pd.DataFrame.from_dict(
2133
+ {
2134
+ "categoryb_": pd.Series(["a", "b", "c"], dtype="category"),
2135
+ }
2136
+ )
2137
+
2138
+ for fitter in [CoxPHFitter(), WeibullAFTFitter()]:
2139
+ formula = "categoryb_"
2140
+ fitter.fit(df, duration_col="t", formula=formula)
2141
+ fitter.predict_survival_function(df_to_predict)
2142
+
2122
2143
  @pytest.mark.xfail
2123
2144
  def test_regression_model_has_concordance_index_(self, regression_models, rossi):
2124
2145
 
@@ -73,6 +73,7 @@ def test_mice_and_optimization_flag():
73
73
  npt.assert_allclose(results[0][-1], 0.166667, rtol=1e-4)
74
74
 
75
75
 
76
+ @pytest.mark.xfail
76
77
  def test_mice_scipy():
77
78
  df = load_mice()
78
79
  results = npmle(df["l"], df["u"], verbose=True, fit_method="scipy")
@@ -44,6 +44,23 @@ def waltons():
44
44
  return load_waltons()[["T", "E"]].iloc[:50]
45
45
 
46
46
 
47
+ def test_add_at_risk_counts_is_numpy_scalar_compatible():
48
+ # Regression test for https://github.com/CamDavidsonPilon/lifelines/issues/1671:
49
+ # `add_at_risk_counts` previously produced 1D NumPy arrays (length 1) for "At risk" counts,
50
+ # which breaks `int(c)` on NumPy>=2.4 (`TypeError: only 0-dimensional arrays can be converted to Python scalars`).
51
+ matplotlib = pytest.importorskip("matplotlib")
52
+ matplotlib.use("Agg", force=True)
53
+ from matplotlib import pyplot as plt
54
+
55
+ kmf = KaplanMeierFitter().fit(
56
+ np.random.exponential(10, size=(100,)),
57
+ np.random.binomial(1, 0.8, size=(100,)),
58
+ )
59
+ ax = kmf.plot_survival_function()
60
+ add_at_risk_counts(kmf, ax=ax)
61
+ plt.close(ax.figure)
62
+
63
+
47
64
  @pytest.mark.skipif("DISPLAY" not in os.environ, reason="requires display")
48
65
  class TestPlotting:
49
66
  @pytest.fixture
@@ -561,3 +561,21 @@ def test_survival_difference_at_fixed_point_in_time_test_interval_censoring():
561
561
  wf2 = WeibullFitter().fit_interval_censoring(2 * T, 2 * T)
562
562
  result = stats.survival_difference_at_fixed_point_in_time_test(T.mean(), wf1, wf2)
563
563
  assert result.p_value < 0.05
564
+
565
+
566
+ def test_statistical_result_has_correct_decimal():
567
+ import re
568
+ from lifelines.datasets import load_rossi
569
+
570
+ rossi = load_rossi()
571
+
572
+ results = stats.logrank_test(
573
+ durations_A=rossi.loc[rossi["fin"] == 0, "week"],
574
+ durations_B=rossi.loc[rossi["fin"] == 1, "week"],
575
+ event_observed_A=rossi.loc[rossi["fin"] == 0, "arrest"],
576
+ event_observed_B=rossi.loc[rossi["fin"] == 1, "arrest"],
577
+ )
578
+
579
+ output = results.to_ascii(decimals=10, test=1)
580
+ numbers = re.findall(r"\d+\.\d{10}\b", output)
581
+ assert len(numbers) >= 3
@@ -1016,3 +1016,43 @@ def test_safe_exp():
1016
1016
  assert grad(safe_exp)(4.0) == np.exp(4.0)
1017
1017
  assert grad(safe_exp)(MAX) == np.exp(MAX)
1018
1018
  assert grad(safe_exp)(MAX + 1) == np.exp(MAX)
1019
+
1020
+ class TestAccumulators:
1021
+ EPSILON = 1e-10
1022
+
1023
+ @staticmethod
1024
+ def _check_equality(a: float, b: float) -> None:
1025
+ assert abs(a - b) < TestAccumulators.EPSILON
1026
+
1027
+ def test_linear_accumulator(self):
1028
+ function = utils.LinearAccumulator()
1029
+ TestAccumulators._check_equality(float(0), function.evaluate(0))
1030
+ TestAccumulators._check_equality(float(0), function.evaluate(1))
1031
+ function.add_linear_term(0, 1)
1032
+ TestAccumulators._check_equality(float(0), function.evaluate(0))
1033
+ TestAccumulators._check_equality(float(0), function.evaluate(1))
1034
+ function.add_linear_term(1, -1)
1035
+ TestAccumulators._check_equality(float(1), function.evaluate(0))
1036
+ TestAccumulators._check_equality(float(2), function.evaluate(1))
1037
+ function.add_linear_term(-1, 2)
1038
+ TestAccumulators._check_equality(float(3), function.evaluate(0))
1039
+ TestAccumulators._check_equality(float(3), function.evaluate(1))
1040
+
1041
+ def test_quadratic_accumulator(self):
1042
+ function = utils.QuadraticAccumulator()
1043
+ TestAccumulators._check_equality(float(0), function.evaluate(0))
1044
+ TestAccumulators._check_equality(float(0), function.evaluate(1))
1045
+ TestAccumulators._check_equality(float(0), function.evaluate(2))
1046
+ function.add_quadratic_term(0, 1)
1047
+ TestAccumulators._check_equality(float(0), function.evaluate(0))
1048
+ TestAccumulators._check_equality(float(0), function.evaluate(1))
1049
+ TestAccumulators._check_equality(float(0), function.evaluate(2))
1050
+ function.add_quadratic_term(1, 2)
1051
+ TestAccumulators._check_equality(float(4), function.evaluate(0))
1052
+ TestAccumulators._check_equality(float(1), function.evaluate(1))
1053
+ TestAccumulators._check_equality(float(0), function.evaluate(2))
1054
+ function.add_quadratic_term(-1, -1)
1055
+ TestAccumulators._check_equality(float(3), function.evaluate(0))
1056
+ TestAccumulators._check_equality(float(-3), function.evaluate(1))
1057
+ TestAccumulators._check_equality(float(-9), function.evaluate(2))
1058
+
@@ -1955,3 +1955,54 @@ class CovariateParameterMappings:
1955
1955
  design_info = formulaic.ModelSpec.from_spec(formulaic.Formula(formula).get_model_matrix(df))
1956
1956
 
1957
1957
  return design_info
1958
+
1959
+ class LinearAccumulator:
1960
+ """
1961
+ This class represents a linear function F(x), which can be updated iteratively.
1962
+ """
1963
+ def __init__(self):
1964
+ """
1965
+ Initializes F(x) as 0.
1966
+ """
1967
+ self.const_term: float = 0.0
1968
+ self.linear_term: float = 0.0
1969
+
1970
+ def add_linear_term(self, a: float, b: float) -> None:
1971
+ """
1972
+ Adds a * (x - b) to F(x).
1973
+ """
1974
+ self.const_term -= a * b
1975
+ self.linear_term += a
1976
+
1977
+ def evaluate(self, x: float) -> float:
1978
+ """
1979
+ Evaluates F(x) at the given value of x.
1980
+ """
1981
+ return self.const_term + self.linear_term * x
1982
+
1983
+ class QuadraticAccumulator:
1984
+ """
1985
+ This class represents a quadratic function F(x), which can be updated iteratively.
1986
+ """
1987
+ def __init__(self):
1988
+ """
1989
+ Initializes F(x) as 0.
1990
+ """
1991
+ self.const_term: float = 0.0
1992
+ self.linear_term: float = 0.0
1993
+ self.quadratic_term: float = 0.0
1994
+
1995
+ def add_quadratic_term(self, a: float, b: float) -> None:
1996
+ """
1997
+ Adds a * (x - b)^2 to F(x).
1998
+ """
1999
+ # a * (x - b)^2 = a * x^2 + (- 2 * a * b) * x + a * b^2
2000
+ self.const_term += a * (b ** 2)
2001
+ self.linear_term -= 2 * a * b
2002
+ self.quadratic_term += a
2003
+
2004
+ def evaluate(self, x: float) -> float:
2005
+ """
2006
+ Evaluates F(x) at the given value of x.
2007
+ """
2008
+ return self.const_term + self.linear_term * x + self.quadratic_term * (x ** 2)
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from __future__ import unicode_literals
3
3
 
4
- __version__ = "0.30.0"
4
+ __version__ = "0.30.1"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: lifelines
3
- Version: 0.30.0
3
+ Version: 0.30.1
4
4
  Summary: Survival analysis in Python, including Kaplan Meier, Nelson Aalen and regression
5
5
  Home-page: https://github.com/CamDavidsonPilon/lifelines
6
6
  Author: Cameron Davidson-Pilon
@@ -18,11 +18,22 @@ Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: numpy>=1.14.0
20
20
  Requires-Dist: scipy>=1.7.0
21
- Requires-Dist: pandas>=2.1
21
+ Requires-Dist: pandas<3.0,>=2.1
22
22
  Requires-Dist: matplotlib>=3.0
23
23
  Requires-Dist: autograd>=1.5
24
24
  Requires-Dist: autograd-gamma>=0.3
25
25
  Requires-Dist: formulaic>=0.2.2
26
+ Dynamic: author
27
+ Dynamic: author-email
28
+ Dynamic: classifier
29
+ Dynamic: description
30
+ Dynamic: description-content-type
31
+ Dynamic: home-page
32
+ Dynamic: license
33
+ Dynamic: license-file
34
+ Dynamic: requires-dist
35
+ Dynamic: requires-python
36
+ Dynamic: summary
26
37
 
27
38
  ![](http://i.imgur.com/EOowdSD.png)
28
39
 
@@ -1,6 +1,6 @@
1
1
  numpy>=1.14.0
2
2
  scipy>=1.7.0
3
- pandas>=2.1
3
+ pandas<3.0,>=2.1
4
4
  matplotlib>=3.0
5
5
  autograd>=1.5
6
6
  autograd-gamma>=0.3
@@ -1,6 +1,6 @@
1
1
  numpy>=1.14.0
2
2
  scipy>=1.7.0
3
- pandas>=2.1
3
+ pandas>=2.1,<3.0
4
4
  matplotlib>=3.0
5
5
  autograd>=1.5
6
6
  autograd-gamma>=0.3
File without changes
File without changes
File without changes
File without changes
File without changes