survscope 0.4.3__py3-none-any.whl

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.
survscope/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ """SurvScope public Python API."""
2
+
3
+ from .analysis import analyze
4
+ from .data import DataStore
5
+ from .grouping import GroupingSpec
6
+ from .models import EndpointResult, SurvivalAnalysis
7
+ from .plotting import plot
8
+
9
+ __all__ = [
10
+ "DataStore",
11
+ "GroupingSpec",
12
+ "EndpointResult",
13
+ "SurvivalAnalysis",
14
+ "analyze",
15
+ "available_cohorts",
16
+ "plot",
17
+ "search_genes",
18
+ ]
19
+
20
+ __version__ = "0.4.3"
21
+
22
+
23
+ def available_cohorts(store: DataStore | None = None) -> list[dict]:
24
+ """Return cohort records from the selected data release."""
25
+ return (store or DataStore()).available_cohorts()
26
+
27
+
28
+ def search_genes(
29
+ query: str,
30
+ cohort: str | None = None,
31
+ store: DataStore | None = None,
32
+ ) -> list[dict]:
33
+ """Search supported gene symbols or Ensembl identifiers."""
34
+ return (store or DataStore()).search_genes(query, cohort=cohort)
survscope/analysis.py ADDED
@@ -0,0 +1,152 @@
1
+ """Survival analysis over compact SurvScope data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import numpy as np
8
+
9
+ from .constants import ENDPOINTS, MONTH_DAYS
10
+ from .data import DataStore, GeneData
11
+ from .grouping import GroupingSpec, assign_groups, grouping_label, normalize_grouping
12
+ from .models import Curve, EndpointResult, SurvivalAnalysis
13
+ from .statistics import COX_MESSAGES, bh_fdr, cox_fit, kaplan_meier, km_timeline, logrank_test
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Sequence
17
+
18
+
19
+ def _number_array(values: Sequence[float | int | None], dtype=float) -> np.ndarray:
20
+ return np.asarray([np.nan if value is None else value for value in values], dtype=dtype)
21
+
22
+
23
+ def analyze(
24
+ gene: str,
25
+ cohort: str,
26
+ cutoff: str | float = "median",
27
+ *,
28
+ store: DataStore | None = None,
29
+ grouping: GroupingSpec | dict | None = None,
30
+ ) -> SurvivalAnalysis:
31
+ """Analyze one gene in one TCGA or CPTAC cohort.
32
+
33
+ A numeric cutoff is interpreted as TPM. The special value ``"median"``
34
+ applies the exact endpoint-specific median grouping recorded during the
35
+ data build.
36
+ """
37
+ data_store = store or DataStore()
38
+ gene_data = data_store.load_gene(gene, cohort)
39
+ return analyze_gene_data(gene_data, cutoff=cutoff, grouping=grouping)
40
+
41
+
42
+ def analyze_gene_data(
43
+ data: GeneData,
44
+ cutoff: str | float = "median",
45
+ *,
46
+ grouping: GroupingSpec | dict | None = None,
47
+ ) -> SurvivalAnalysis:
48
+ spec = normalize_grouping(cutoff, grouping)
49
+
50
+ tpm = data.expression_tpm
51
+ results: dict[str, EndpointResult] = {}
52
+ pvalues: list[float] = []
53
+ for endpoint in ENDPOINTS:
54
+ clinical = data.clinical["endpoints"][endpoint]
55
+ time = _number_array(clinical["time"], dtype=float)
56
+ event = _number_array(clinical["event"], dtype=float)
57
+ valid = np.isfinite(tpm) & np.isfinite(time) & np.isin(event, [0, 1]) & (time > 0)
58
+ indices = np.flatnonzero(valid)
59
+ endpoint_time = time[valid]
60
+ endpoint_event = event[valid].astype(int)
61
+ endpoint_tpm = tpm[valid]
62
+
63
+ low_mask, high_mask, lower_threshold, upper_threshold = assign_groups(
64
+ endpoint_tpm,
65
+ indices,
66
+ data.medians.get(endpoint, {}),
67
+ spec,
68
+ )
69
+ included = low_mask | high_mask
70
+ eligible_n = len(endpoint_time)
71
+ excluded_middle = int(np.sum(~included))
72
+ endpoint_time = endpoint_time[included]
73
+ endpoint_event = endpoint_event[included]
74
+ high = high_mask[included]
75
+ low = ~high
76
+ endpoint_cutoff = lower_threshold if lower_threshold == upper_threshold else np.nan
77
+
78
+ warning = ""
79
+ if len(endpoint_time) == 0:
80
+ warning = "No endpoint-valid samples."
81
+ elif not bool(np.any(low)) or not bool(np.any(high)):
82
+ warning = "The cutoff leaves one expression group empty."
83
+ elif int(np.sum(endpoint_event[low])) == 0 or int(np.sum(endpoint_event[high])) == 0:
84
+ warning = "At least one group has no observed events; inferential statistics may be NA."
85
+
86
+ chi2, pvalue = logrank_test(endpoint_time, endpoint_event, high.astype(int))
87
+ hazard_ratio, cox_p, cox_status = cox_fit(
88
+ endpoint_time,
89
+ endpoint_event,
90
+ high.astype(int),
91
+ )
92
+ if cox_status != "ok" and len(endpoint_time):
93
+ warning = COX_MESSAGES[cox_status]
94
+ low_x, low_y = kaplan_meier(endpoint_time[low], endpoint_event[low])
95
+ high_x, high_y = kaplan_meier(endpoint_time[high], endpoint_event[high])
96
+ quality = clinical.get("quality", "caution")
97
+ quality_note = clinical.get("quality_note", "")
98
+ result = EndpointResult(
99
+ endpoint=endpoint,
100
+ quality=quality,
101
+ quality_note=quality_note,
102
+ n=len(endpoint_time),
103
+ n_low=int(np.sum(low)),
104
+ n_high=int(np.sum(high)),
105
+ events=int(np.sum(endpoint_event)),
106
+ events_low=int(np.sum(endpoint_event[low])),
107
+ events_high=int(np.sum(endpoint_event[high])),
108
+ cutoff_tpm=endpoint_cutoff,
109
+ logrank_chi2=chi2,
110
+ logrank_p=pvalue,
111
+ cox_hr=hazard_ratio,
112
+ cox_p=cox_p,
113
+ low=Curve(
114
+ x_months=low_x / MONTH_DAYS,
115
+ survival=low_y,
116
+ n=int(np.sum(low)),
117
+ events=int(np.sum(endpoint_event[low])),
118
+ timeline=km_timeline(endpoint_time[low], endpoint_event[low]),
119
+ ),
120
+ high=Curve(
121
+ x_months=high_x / MONTH_DAYS,
122
+ survival=high_y,
123
+ n=int(np.sum(high)),
124
+ events=int(np.sum(endpoint_event[high])),
125
+ timeline=km_timeline(endpoint_time[high], endpoint_event[high]),
126
+ ),
127
+ warning=warning,
128
+ eligible_n=eligible_n,
129
+ excluded_middle=excluded_middle,
130
+ lower_threshold=lower_threshold,
131
+ upper_threshold=upper_threshold,
132
+ cox_status=cox_status,
133
+ )
134
+ results[endpoint] = result
135
+ pvalues.append(pvalue)
136
+
137
+ for endpoint, qvalue in zip(ENDPOINTS, bh_fdr(pvalues), strict=True):
138
+ results[endpoint].logrank_q = qvalue
139
+
140
+ return SurvivalAnalysis(
141
+ gene=data.symbol,
142
+ ensembl=data.ensembl,
143
+ cohort=data.cohort,
144
+ cohort_label=data.cohort_label,
145
+ cutoff="median" if spec.kind == "median" else spec.threshold,
146
+ data_version=data.data_version,
147
+ source_expression=data.sources["expression"]["label"],
148
+ source_survival=data.sources["survival"]["label"],
149
+ endpoints=results,
150
+ grouping=spec.to_dict(),
151
+ grouping_label=grouping_label(spec),
152
+ )