geniekit 0.0.1__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.
- geniekit/__init__.py +30 -0
- geniekit/_version.py +8 -0
- geniekit/cli.py +200 -0
- geniekit/dictionary.py +317 -0
- geniekit/io/__init__.py +36 -0
- geniekit/io/cohort.py +161 -0
- geniekit/io/local.py +275 -0
- geniekit/io/synapse.py +334 -0
- geniekit/releases.py +318 -0
- geniekit/resources/__init__.py +10 -0
- geniekit/resources/dictionary.csv.gz +0 -0
- geniekit/resources/dictionary.meta.json +13 -0
- geniekit/resources/releases.json +1880 -0
- geniekit/synthetic/__init__.py +15 -0
- geniekit/synthetic/generate.py +1272 -0
- geniekit/synthetic/spec.py +331 -0
- geniekit-0.0.1.dist-info/METADATA +230 -0
- geniekit-0.0.1.dist-info/RECORD +23 -0
- geniekit-0.0.1.dist-info/WHEEL +5 -0
- geniekit-0.0.1.dist-info/entry_points.txt +2 -0
- geniekit-0.0.1.dist-info/licenses/LICENSE +21 -0
- geniekit-0.0.1.dist-info/licenses/NOTICE.md +50 -0
- geniekit-0.0.1.dist-info/top_level.txt +1 -0
geniekit/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""geniekit: tools for the AACR Project GENIE BPC clinicogenomic cohorts.
|
|
2
|
+
|
|
3
|
+
Three capabilities, one package:
|
|
4
|
+
|
|
5
|
+
* ``geniekit.harmonize``: normalise the six public BPC cohorts (NSCLC, CRC,
|
|
6
|
+
BrCa, PANC, Prostate, BLADDER) to one schema for cross-cohort analysis.
|
|
7
|
+
* ``geniekit.lot``: derive lines of therapy from the regimen data relative to a
|
|
8
|
+
user-chosen index event.
|
|
9
|
+
* ``geniekit.aggregate``: an aggregate-only analytic layer with disclosure
|
|
10
|
+
control for users who may not see patient-level records.
|
|
11
|
+
|
|
12
|
+
Supporting modules: ``geniekit.releases`` (registry of public releases and
|
|
13
|
+
Synapse ids), ``geniekit.dictionary`` (compiled variable synopses),
|
|
14
|
+
``geniekit.io`` (download and load), ``geniekit.synthetic`` (synthetic cohorts
|
|
15
|
+
for tests and demos).
|
|
16
|
+
|
|
17
|
+
The package ships metadata only. Patient-level GENIE data must be obtained
|
|
18
|
+
from Synapse under the GENIE access requirement.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from ._version import __version__
|
|
22
|
+
from .releases import COHORTS, get_release, list_releases, normalize_cohort
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"COHORTS",
|
|
26
|
+
"__version__",
|
|
27
|
+
"get_release",
|
|
28
|
+
"list_releases",
|
|
29
|
+
"normalize_cohort",
|
|
30
|
+
]
|
geniekit/_version.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Single source of truth for the package version.
|
|
2
|
+
|
|
3
|
+
Keep in sync with ``pyproject.toml``. Bump according to semantic versioning:
|
|
4
|
+
MAJOR for breaking API changes, MINOR for backward-compatible features, PATCH
|
|
5
|
+
for backward-compatible fixes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.0.1"
|
geniekit/cli.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Command line interface: ``geniekit <command> [options]``.
|
|
2
|
+
|
|
3
|
+
Commands: ``releases``, ``dictionary``, ``access``, ``download``, ``inspect``.
|
|
4
|
+
``harmonize``, ``lot`` and ``aggregate`` are added with their modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
from . import dictionary, io, releases
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _print_frame(frame: pd.DataFrame, max_rows: int | None = None) -> None:
|
|
20
|
+
with pd.option_context("display.max_rows", max_rows, "display.max_columns", None, "display.width", 200):
|
|
21
|
+
print(frame.to_string(index=False) if not frame.empty else "(no rows)")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_releases(args: argparse.Namespace) -> int:
|
|
25
|
+
rels = releases.list_releases(args.cohort)
|
|
26
|
+
rows = [
|
|
27
|
+
{
|
|
28
|
+
"cohort": r.cohort,
|
|
29
|
+
"version": r.version,
|
|
30
|
+
"patients": r.n_patients,
|
|
31
|
+
"institutions": "/".join(r.institutions),
|
|
32
|
+
"schema_gen": r.schema_generation,
|
|
33
|
+
"release_folder": r.release_folder,
|
|
34
|
+
"clinical_files": len(r.clinical),
|
|
35
|
+
"genomic_files": len(r.genomic),
|
|
36
|
+
}
|
|
37
|
+
for r in rels
|
|
38
|
+
]
|
|
39
|
+
_print_frame(pd.DataFrame(rows))
|
|
40
|
+
if args.files:
|
|
41
|
+
frame = releases.to_frame()
|
|
42
|
+
if args.cohort:
|
|
43
|
+
frame = frame[frame["cohort"] == releases.normalize_cohort(args.cohort)]
|
|
44
|
+
print()
|
|
45
|
+
_print_frame(frame[frame["group"].isin(["clinical", "genomic", "documentation"])])
|
|
46
|
+
print(f"\nregistry generated on {releases.registry_generated_on()}")
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_dictionary(args: argparse.Namespace) -> int:
|
|
51
|
+
dd = dictionary.load()
|
|
52
|
+
if args.dict_command == "lookup":
|
|
53
|
+
rows = dd.lookup(args.variable, args.table)
|
|
54
|
+
if rows.empty:
|
|
55
|
+
print(f"{args.variable!r} is not declared in any cohort", file=sys.stderr)
|
|
56
|
+
return 1
|
|
57
|
+
out = rows[["cohort", "table", "dtype", "n_values", "label"]]
|
|
58
|
+
_print_frame(out)
|
|
59
|
+
if args.values:
|
|
60
|
+
for c, vals in zip(rows["cohort"], rows["values"], strict=True):
|
|
61
|
+
print(f"\n{c}:")
|
|
62
|
+
for v in vals:
|
|
63
|
+
print(f" - {v}")
|
|
64
|
+
return 0
|
|
65
|
+
if args.dict_command == "diff":
|
|
66
|
+
kinds = args.kind or None
|
|
67
|
+
diff = dd.differences(args.table, kinds)
|
|
68
|
+
_print_frame(diff, max_rows=None)
|
|
69
|
+
print(f"\n{len(diff)} rows")
|
|
70
|
+
return 0
|
|
71
|
+
if args.dict_command == "presence":
|
|
72
|
+
mat = dd.presence(args.table)
|
|
73
|
+
if args.only_partial:
|
|
74
|
+
mat = mat[~mat.all(axis=1)]
|
|
75
|
+
with pd.option_context("display.max_rows", None, "display.width", 200):
|
|
76
|
+
print(mat.replace({True: "x", False: "."}).to_string())
|
|
77
|
+
return 0
|
|
78
|
+
return 2
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_access(args: argparse.Namespace) -> int:
|
|
82
|
+
try:
|
|
83
|
+
session = io.SynapseSession(args.token)
|
|
84
|
+
who = session.whoami()
|
|
85
|
+
status = session.access_status()
|
|
86
|
+
except io.SynapseError as exc:
|
|
87
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
88
|
+
return 1
|
|
89
|
+
print(f"Synapse user: {who.get('userName')} (owner id {who.get('ownerId')})")
|
|
90
|
+
print(status.describe())
|
|
91
|
+
return 0 if status.approved else 2
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_download(args: argparse.Namespace) -> int:
|
|
95
|
+
try:
|
|
96
|
+
session = io.SynapseSession(args.token)
|
|
97
|
+
folder = session.download_release(
|
|
98
|
+
args.cohort,
|
|
99
|
+
version=args.version,
|
|
100
|
+
dest=args.dest,
|
|
101
|
+
groups=args.groups,
|
|
102
|
+
names=args.tables or None,
|
|
103
|
+
overwrite=args.overwrite,
|
|
104
|
+
progress=print,
|
|
105
|
+
)
|
|
106
|
+
except (io.SynapseError, KeyError, ValueError) as exc:
|
|
107
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
print(f"release folder: {folder}")
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cmd_inspect(args: argparse.Namespace) -> int:
|
|
114
|
+
try:
|
|
115
|
+
data = io.load_release(
|
|
116
|
+
args.path, cohort=args.cohort, version=args.version, genomic=not args.no_genomic
|
|
117
|
+
)
|
|
118
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
119
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
print(data)
|
|
122
|
+
_print_frame(data.summary())
|
|
123
|
+
report = data.validation.get("columns") or {}
|
|
124
|
+
if report:
|
|
125
|
+
print("\ncolumn differences versus the packaged dictionary:")
|
|
126
|
+
for table, r in report.items():
|
|
127
|
+
print(f" {table}: missing {len(r['missing'])}, unexpected {len(r['extra'])}")
|
|
128
|
+
if r["extra"]:
|
|
129
|
+
print(" unexpected: " + ", ".join(r["extra"][:15]))
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
134
|
+
p = argparse.ArgumentParser(prog="geniekit", description=__doc__.splitlines()[0])
|
|
135
|
+
p.add_argument("--version", action="version", version=f"geniekit {__version__}")
|
|
136
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
137
|
+
|
|
138
|
+
pr = sub.add_parser("releases", help="list the known public BPC releases")
|
|
139
|
+
pr.add_argument("--cohort", help="restrict to one cohort")
|
|
140
|
+
pr.add_argument(
|
|
141
|
+
"--files", action="store_true", help="also list clinical, genomic and documentation files"
|
|
142
|
+
)
|
|
143
|
+
pr.set_defaults(func=cmd_releases)
|
|
144
|
+
|
|
145
|
+
pdict = sub.add_parser("dictionary", help="query the compiled variable synopses")
|
|
146
|
+
dsub = pdict.add_subparsers(dest="dict_command", required=True)
|
|
147
|
+
lk = dsub.add_parser("lookup", help="show how a variable is declared in each cohort")
|
|
148
|
+
lk.add_argument("variable")
|
|
149
|
+
lk.add_argument("--table", choices=dictionary.TABLES)
|
|
150
|
+
lk.add_argument("--values", action="store_true", help="print permitted values per cohort")
|
|
151
|
+
df = dsub.add_parser("diff", help="list variables not declared identically across cohorts")
|
|
152
|
+
df.add_argument("--table", choices=dictionary.TABLES)
|
|
153
|
+
df.add_argument(
|
|
154
|
+
"--kind",
|
|
155
|
+
action="append",
|
|
156
|
+
choices=["missing_in_some", "dtype", "value_set", "value_text", "label"],
|
|
157
|
+
help="restrict to one or more kinds of difference (repeatable)",
|
|
158
|
+
)
|
|
159
|
+
pres = dsub.add_parser("presence", help="presence matrix of variables by cohort")
|
|
160
|
+
pres.add_argument("--table", choices=dictionary.TABLES)
|
|
161
|
+
pres.add_argument("--only-partial", action="store_true", help="hide variables present in every cohort")
|
|
162
|
+
pdict.set_defaults(func=cmd_dictionary)
|
|
163
|
+
|
|
164
|
+
pa = sub.add_parser("access", help="check the Synapse token and GENIE access status")
|
|
165
|
+
pa.add_argument("--token", help="Synapse personal access token (default: SYNAPSE_AUTH_TOKEN)")
|
|
166
|
+
pa.set_defaults(func=cmd_access)
|
|
167
|
+
|
|
168
|
+
pdl = sub.add_parser("download", help="download a public BPC release from Synapse")
|
|
169
|
+
pdl.add_argument("cohort")
|
|
170
|
+
pdl.add_argument("--version", help="release version, e.g. v2.0-public (default: latest public)")
|
|
171
|
+
pdl.add_argument("--dest", help="destination directory (default: $GENIEKIT_DATA_DIR or ./data/genie_bpc)")
|
|
172
|
+
pdl.add_argument(
|
|
173
|
+
"--groups",
|
|
174
|
+
nargs="+",
|
|
175
|
+
default=["clinical", "genomic"],
|
|
176
|
+
choices=list(releases.FILE_GROUPS),
|
|
177
|
+
help="file groups to download",
|
|
178
|
+
)
|
|
179
|
+
pdl.add_argument("--tables", nargs="+", help="restrict to these logical table names")
|
|
180
|
+
pdl.add_argument("--overwrite", action="store_true", help="re-download files that already exist")
|
|
181
|
+
pdl.add_argument("--token", help="Synapse personal access token (default: SYNAPSE_AUTH_TOKEN)")
|
|
182
|
+
pdl.set_defaults(func=cmd_download)
|
|
183
|
+
|
|
184
|
+
pi = sub.add_parser("inspect", help="load a release folder and summarise its tables")
|
|
185
|
+
pi.add_argument("path")
|
|
186
|
+
pi.add_argument("--cohort")
|
|
187
|
+
pi.add_argument("--version")
|
|
188
|
+
pi.add_argument("--no-genomic", action="store_true", help="skip genomic files")
|
|
189
|
+
pi.set_defaults(func=cmd_inspect)
|
|
190
|
+
return p
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
194
|
+
parser = build_parser()
|
|
195
|
+
args = parser.parse_args(argv)
|
|
196
|
+
return int(args.func(args))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__": # pragma: no cover
|
|
200
|
+
sys.exit(main())
|
geniekit/dictionary.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Machine-readable data dictionary for the six public GENIE BPC cohorts.
|
|
2
|
+
|
|
3
|
+
AACR publishes a Variable Synopsis workbook with every public BPC release. The
|
|
4
|
+
six workbooks are compiled by ``data-raw/build_dictionary.py`` into one long
|
|
5
|
+
table shipped as ``resources/dictionary.csv.gz``, with one row per
|
|
6
|
+
(cohort, table, variable). This module exposes that table and the
|
|
7
|
+
cross-cohort comparisons that motivate the harmonisation rules:
|
|
8
|
+
|
|
9
|
+
* which variables exist in which cohorts (``presence``),
|
|
10
|
+
* variables whose declared type differs across cohorts,
|
|
11
|
+
* variables whose permitted value sets differ across cohorts.
|
|
12
|
+
|
|
13
|
+
The dictionary contains no patient-level data.
|
|
14
|
+
|
|
15
|
+
Example
|
|
16
|
+
-------
|
|
17
|
+
>>> from geniekit import dictionary
|
|
18
|
+
>>> dd = dictionary.load()
|
|
19
|
+
>>> dd.compare("ca_cigarette", table="cancer").present_in
|
|
20
|
+
('BrCa', 'PANC', 'Prostate', 'BLADDER')
|
|
21
|
+
>>> dd.differences(table="patient").kind.value_counts() # doctest: +SKIP
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
from collections.abc import Iterable
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from functools import lru_cache
|
|
31
|
+
from importlib import resources as importlib_resources
|
|
32
|
+
|
|
33
|
+
import pandas as pd
|
|
34
|
+
|
|
35
|
+
from .releases import COHORTS, normalize_cohort
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"TABLES",
|
|
39
|
+
"DataDictionary",
|
|
40
|
+
"VariableComparison",
|
|
41
|
+
"load",
|
|
42
|
+
"normalize_level",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
#: Logical table names in the order they appear in the synopses.
|
|
46
|
+
TABLES: tuple[str, ...] = (
|
|
47
|
+
"patient",
|
|
48
|
+
"cancer",
|
|
49
|
+
"regimen",
|
|
50
|
+
"imaging",
|
|
51
|
+
"pathology",
|
|
52
|
+
"med_onc",
|
|
53
|
+
"tumor_marker",
|
|
54
|
+
"radiation",
|
|
55
|
+
"cpt",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# hyphen, non-breaking hyphen, figure dash, en dash, em dash, minus sign
|
|
59
|
+
_DASH_CHARS = "".join(chr(c) for c in (0x2010, 0x2011, 0x2012, 0x2013, 0x2014, 0x2212))
|
|
60
|
+
_DASHES = re.compile("[" + re.escape(_DASH_CHARS) + "]")
|
|
61
|
+
_WS = re.compile(r"\s+")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def normalize_level(level: str) -> str:
|
|
65
|
+
"""Cosmetic normalisation of a permitted value for set comparison.
|
|
66
|
+
|
|
67
|
+
Lower-cases, collapses whitespace, unifies dash characters and strips
|
|
68
|
+
trailing punctuation so that ``"Mixed Gliomas"`` and ``"Mixed gliomas"``
|
|
69
|
+
compare equal while genuinely different levels do not.
|
|
70
|
+
"""
|
|
71
|
+
s = _DASHES.sub("-", str(level))
|
|
72
|
+
s = _WS.sub(" ", s).strip().lower()
|
|
73
|
+
s = s.replace(" - ", "-").replace("- ", "-").replace(" -", "-")
|
|
74
|
+
return s.rstrip(".;, ")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class VariableComparison:
|
|
79
|
+
"""How one variable is declared across the cohorts."""
|
|
80
|
+
|
|
81
|
+
table: str
|
|
82
|
+
variable: str
|
|
83
|
+
present_in: tuple[str, ...]
|
|
84
|
+
missing_in: tuple[str, ...]
|
|
85
|
+
labels: dict[str, str]
|
|
86
|
+
dtypes: dict[str, str]
|
|
87
|
+
values: dict[str, list[str]]
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_universal(self) -> bool:
|
|
91
|
+
return not self.missing_in
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def dtype_conflict(self) -> bool:
|
|
95
|
+
return len(set(self.dtypes.values())) > 1
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def label_conflict(self) -> bool:
|
|
99
|
+
return len({normalize_level(v) for v in self.labels.values()}) > 1
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def value_set_conflict(self) -> bool:
|
|
103
|
+
"""True when the permitted value sets differ after cosmetic normalisation."""
|
|
104
|
+
sets = {frozenset(normalize_level(v) for v in vals) for vals in self.values.values() if vals}
|
|
105
|
+
return len(sets) > 1
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def value_text_conflict(self) -> bool:
|
|
109
|
+
"""True when value sets are equal after normalisation but differ as raw text."""
|
|
110
|
+
if self.value_set_conflict:
|
|
111
|
+
return False
|
|
112
|
+
raw = {tuple(sorted(vals)) for vals in self.values.values() if vals}
|
|
113
|
+
return len(raw) > 1
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def union_values(self) -> list[str]:
|
|
117
|
+
seen: dict[str, str] = {}
|
|
118
|
+
for vals in self.values.values():
|
|
119
|
+
for v in vals:
|
|
120
|
+
seen.setdefault(normalize_level(v), v)
|
|
121
|
+
return list(seen.values())
|
|
122
|
+
|
|
123
|
+
def summary(self) -> str:
|
|
124
|
+
parts = [f"{self.table}.{self.variable}: present in {', '.join(self.present_in) or 'none'}"]
|
|
125
|
+
if self.missing_in:
|
|
126
|
+
parts.append(f"missing in {', '.join(self.missing_in)}")
|
|
127
|
+
if self.dtype_conflict:
|
|
128
|
+
parts.append("dtype conflict " + ", ".join(f"{c}={t}" for c, t in self.dtypes.items()))
|
|
129
|
+
if self.value_set_conflict:
|
|
130
|
+
parts.append("value sets differ")
|
|
131
|
+
elif self.value_text_conflict:
|
|
132
|
+
parts.append("value text differs (cosmetic)")
|
|
133
|
+
return "; ".join(parts)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DataDictionary:
|
|
137
|
+
"""The compiled dictionary with cross-cohort comparison helpers."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, frame: pd.DataFrame) -> None:
|
|
140
|
+
required = {"cohort", "version", "schema_generation", "table", "variable", "label", "dtype", "values"}
|
|
141
|
+
missing = required - set(frame.columns)
|
|
142
|
+
if missing:
|
|
143
|
+
raise ValueError(f"dictionary frame lacks columns: {sorted(missing)}")
|
|
144
|
+
frame = frame.copy()
|
|
145
|
+
if len(frame) and not isinstance(frame["values"].iloc[0], list):
|
|
146
|
+
frame["values"] = [json.loads(v) if isinstance(v, str) and v else [] for v in frame["values"]]
|
|
147
|
+
self._frame = frame.reset_index(drop=True)
|
|
148
|
+
present = [c for c in COHORTS if c in set(self._frame["cohort"])]
|
|
149
|
+
self.cohorts: tuple[str, ...] = tuple(present)
|
|
150
|
+
|
|
151
|
+
# -- basic access --------------------------------------------------------
|
|
152
|
+
@property
|
|
153
|
+
def frame(self) -> pd.DataFrame:
|
|
154
|
+
"""The full long table (one row per cohort, table, variable)."""
|
|
155
|
+
return self._frame
|
|
156
|
+
|
|
157
|
+
def tables(self) -> list[str]:
|
|
158
|
+
return [t for t in TABLES if t in set(self._frame["table"])]
|
|
159
|
+
|
|
160
|
+
def schema_generation(self, cohort: str) -> int:
|
|
161
|
+
c = normalize_cohort(cohort)
|
|
162
|
+
gens = self._frame.loc[self._frame["cohort"] == c, "schema_generation"].unique()
|
|
163
|
+
if len(gens) != 1:
|
|
164
|
+
raise KeyError(f"cohort {c!r} not in dictionary")
|
|
165
|
+
return int(gens[0])
|
|
166
|
+
|
|
167
|
+
def variables(self, cohort: str | None = None, table: str | None = None) -> pd.DataFrame:
|
|
168
|
+
"""Rows filtered by cohort and/or table."""
|
|
169
|
+
f = self._frame
|
|
170
|
+
if cohort is not None:
|
|
171
|
+
f = f[f["cohort"] == normalize_cohort(cohort)]
|
|
172
|
+
if table is not None:
|
|
173
|
+
self._check_table(table)
|
|
174
|
+
f = f[f["table"] == table]
|
|
175
|
+
return f.reset_index(drop=True)
|
|
176
|
+
|
|
177
|
+
def lookup(self, variable: str, table: str | None = None) -> pd.DataFrame:
|
|
178
|
+
"""All declarations of ``variable`` (across cohorts and, if unspecified, tables)."""
|
|
179
|
+
f = self._frame[self._frame["variable"] == variable]
|
|
180
|
+
if table is not None:
|
|
181
|
+
self._check_table(table)
|
|
182
|
+
f = f[f["table"] == table]
|
|
183
|
+
return f.reset_index(drop=True)
|
|
184
|
+
|
|
185
|
+
def values(self, variable: str, table: str) -> dict[str, list[str]]:
|
|
186
|
+
"""Permitted values per cohort for one variable."""
|
|
187
|
+
rows = self.lookup(variable, table)
|
|
188
|
+
return {c: list(v) for c, v in zip(rows["cohort"], rows["values"], strict=True)}
|
|
189
|
+
|
|
190
|
+
# -- comparisons ---------------------------------------------------------
|
|
191
|
+
def presence(self, table: str | None = None, cohorts: Iterable[str] | None = None) -> pd.DataFrame:
|
|
192
|
+
"""Boolean matrix: rows (table, variable), columns cohorts."""
|
|
193
|
+
f = self.variables(table=table)
|
|
194
|
+
cols = tuple(normalize_cohort(c) for c in cohorts) if cohorts else self.cohorts
|
|
195
|
+
f = f[f["cohort"].isin(cols)]
|
|
196
|
+
mat = f.assign(flag=True).pivot_table(
|
|
197
|
+
index=["table", "variable"], columns="cohort", values="flag", aggfunc="any", fill_value=False
|
|
198
|
+
)
|
|
199
|
+
mat = mat.reindex(columns=list(cols), fill_value=False).astype(bool)
|
|
200
|
+
mat.columns.name = None
|
|
201
|
+
# keep synopsis order of first appearance
|
|
202
|
+
order = f.drop_duplicates(["table", "variable"])[["table", "variable"]]
|
|
203
|
+
return mat.reindex(pd.MultiIndex.from_frame(order))
|
|
204
|
+
|
|
205
|
+
def common_variables(self, table: str, cohorts: Iterable[str] | None = None) -> list[str]:
|
|
206
|
+
"""Variables present in every requested cohort for ``table``."""
|
|
207
|
+
mat = self.presence(table, cohorts)
|
|
208
|
+
return [v for (_, v), row in mat.iterrows() if bool(row.all())]
|
|
209
|
+
|
|
210
|
+
def compare(self, variable: str, table: str) -> VariableComparison:
|
|
211
|
+
rows = self.lookup(variable, table)
|
|
212
|
+
if rows.empty:
|
|
213
|
+
raise KeyError(f"{table}.{variable} is not declared in any cohort")
|
|
214
|
+
by = {c: r for c, r in zip(rows["cohort"], rows.to_dict("records"), strict=True)}
|
|
215
|
+
present = tuple(c for c in self.cohorts if c in by)
|
|
216
|
+
return VariableComparison(
|
|
217
|
+
table=table,
|
|
218
|
+
variable=variable,
|
|
219
|
+
present_in=present,
|
|
220
|
+
missing_in=tuple(c for c in self.cohorts if c not in by),
|
|
221
|
+
labels={c: str(by[c]["label"]) for c in present},
|
|
222
|
+
dtypes={c: str(by[c]["dtype"]) for c in present},
|
|
223
|
+
values={c: list(by[c]["values"]) for c in present},
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def differences(self, table: str | None = None, kinds: Iterable[str] | None = None) -> pd.DataFrame:
|
|
227
|
+
"""Every variable that is not declared identically in all cohorts.
|
|
228
|
+
|
|
229
|
+
Returns one row per (table, variable, kind) with ``kind`` in
|
|
230
|
+
``{"missing_in_some", "dtype", "value_set", "value_text", "label"}``
|
|
231
|
+
and a human-readable ``detail`` column.
|
|
232
|
+
"""
|
|
233
|
+
wanted = set(kinds) if kinds else {"missing_in_some", "dtype", "value_set", "value_text", "label"}
|
|
234
|
+
f = self.variables(table=table)
|
|
235
|
+
rows: list[dict] = []
|
|
236
|
+
for (tbl, var), _ in f.groupby(["table", "variable"], sort=False):
|
|
237
|
+
cmp = self.compare(var, tbl)
|
|
238
|
+
if "missing_in_some" in wanted and cmp.missing_in:
|
|
239
|
+
rows.append(
|
|
240
|
+
{
|
|
241
|
+
"table": tbl,
|
|
242
|
+
"variable": var,
|
|
243
|
+
"kind": "missing_in_some",
|
|
244
|
+
"present_in": ",".join(cmp.present_in),
|
|
245
|
+
"detail": f"missing in {', '.join(cmp.missing_in)}",
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
if "dtype" in wanted and cmp.dtype_conflict:
|
|
249
|
+
rows.append(
|
|
250
|
+
{
|
|
251
|
+
"table": tbl,
|
|
252
|
+
"variable": var,
|
|
253
|
+
"kind": "dtype",
|
|
254
|
+
"present_in": ",".join(cmp.present_in),
|
|
255
|
+
"detail": ", ".join(f"{c}={t}" for c, t in cmp.dtypes.items()),
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
if "value_set" in wanted and cmp.value_set_conflict:
|
|
259
|
+
union = {normalize_level(v) for vals in cmp.values.values() for v in vals}
|
|
260
|
+
lacks = {
|
|
261
|
+
c: sorted(union - {normalize_level(v) for v in vals})
|
|
262
|
+
for c, vals in cmp.values.items()
|
|
263
|
+
if vals
|
|
264
|
+
}
|
|
265
|
+
rows.append(
|
|
266
|
+
{
|
|
267
|
+
"table": tbl,
|
|
268
|
+
"variable": var,
|
|
269
|
+
"kind": "value_set",
|
|
270
|
+
"present_in": ",".join(cmp.present_in),
|
|
271
|
+
"detail": "; ".join(
|
|
272
|
+
f"{c} lacks {len(missing)}" for c, missing in lacks.items() if missing
|
|
273
|
+
),
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
if "value_text" in wanted and cmp.value_text_conflict:
|
|
277
|
+
rows.append(
|
|
278
|
+
{
|
|
279
|
+
"table": tbl,
|
|
280
|
+
"variable": var,
|
|
281
|
+
"kind": "value_text",
|
|
282
|
+
"present_in": ",".join(cmp.present_in),
|
|
283
|
+
"detail": "same levels, different spelling/case",
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
if "label" in wanted and cmp.label_conflict:
|
|
287
|
+
rows.append(
|
|
288
|
+
{
|
|
289
|
+
"table": tbl,
|
|
290
|
+
"variable": var,
|
|
291
|
+
"kind": "label",
|
|
292
|
+
"present_in": ",".join(cmp.present_in),
|
|
293
|
+
"detail": " | ".join(sorted({v for v in cmp.labels.values()}))[:200],
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
return pd.DataFrame(rows, columns=["table", "variable", "kind", "present_in", "detail"])
|
|
297
|
+
|
|
298
|
+
# -- helpers -------------------------------------------------------------
|
|
299
|
+
def _check_table(self, table: str) -> None:
|
|
300
|
+
if table not in TABLES:
|
|
301
|
+
raise ValueError(f"Unknown table {table!r}; expected one of {', '.join(TABLES)}")
|
|
302
|
+
|
|
303
|
+
def __repr__(self) -> str:
|
|
304
|
+
n_vars = self._frame.drop_duplicates(["table", "variable"]).shape[0]
|
|
305
|
+
return f"DataDictionary({len(self._frame)} rows, {n_vars} variables, cohorts={list(self.cohorts)})"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@lru_cache(maxsize=1)
|
|
309
|
+
def load() -> DataDictionary:
|
|
310
|
+
"""Load the packaged dictionary (cached)."""
|
|
311
|
+
path = importlib_resources.files("geniekit.resources").joinpath("dictionary.csv.gz")
|
|
312
|
+
with path.open("rb") as fh:
|
|
313
|
+
frame = pd.read_csv(fh, compression="gzip", dtype=str, keep_default_na=False)
|
|
314
|
+
frame["schema_generation"] = frame["schema_generation"].astype(int)
|
|
315
|
+
frame["n_values"] = frame["n_values"].astype(int)
|
|
316
|
+
frame["order"] = frame["order"].astype(int)
|
|
317
|
+
return DataDictionary(frame)
|
geniekit/io/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Loading GENIE BPC releases: from Synapse (with a personal access token) or
|
|
2
|
+
from files already on disk. Both paths produce a :class:`CohortData`.
|
|
3
|
+
|
|
4
|
+
>>> from geniekit import io
|
|
5
|
+
>>> data = io.load_release("data/genie_bpc/CRC_v2.0-public") # doctest: +SKIP
|
|
6
|
+
>>> data["patient"].shape # doctest: +SKIP
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .cohort import CLINICAL_TABLES, GENOMIC_TABLES, CohortData
|
|
10
|
+
from .local import FILENAME_TO_TABLE, find_release_files, load_release, read_table
|
|
11
|
+
from .synapse import (
|
|
12
|
+
GENIE_ACCESS_REQUIREMENT,
|
|
13
|
+
AccessDeniedError,
|
|
14
|
+
AccessStatus,
|
|
15
|
+
SynapseError,
|
|
16
|
+
SynapseSession,
|
|
17
|
+
download_release,
|
|
18
|
+
resolve_token,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CLINICAL_TABLES",
|
|
23
|
+
"FILENAME_TO_TABLE",
|
|
24
|
+
"GENIE_ACCESS_REQUIREMENT",
|
|
25
|
+
"GENOMIC_TABLES",
|
|
26
|
+
"AccessDeniedError",
|
|
27
|
+
"AccessStatus",
|
|
28
|
+
"CohortData",
|
|
29
|
+
"SynapseError",
|
|
30
|
+
"SynapseSession",
|
|
31
|
+
"download_release",
|
|
32
|
+
"find_release_files",
|
|
33
|
+
"load_release",
|
|
34
|
+
"read_table",
|
|
35
|
+
"resolve_token",
|
|
36
|
+
]
|