cocoa-tokenizer 26.6.0__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.
- cocoa/__init__.py +0 -0
- cocoa/cli.py +285 -0
- cocoa/collator.py +318 -0
- cocoa/config/__init__.py +1 -0
- cocoa/config/collation.yaml +388 -0
- cocoa/config/tokenization.yaml +56 -0
- cocoa/config/winnowing.yaml +21 -0
- cocoa/configurable.py +37 -0
- cocoa/logger.py +184 -0
- cocoa/tokenizer.py +394 -0
- cocoa/util.py +93 -0
- cocoa/winnower.py +167 -0
- cocoa_tokenizer-26.6.0.dist-info/METADATA +674 -0
- cocoa_tokenizer-26.6.0.dist-info/RECORD +18 -0
- cocoa_tokenizer-26.6.0.dist-info/WHEEL +5 -0
- cocoa_tokenizer-26.6.0.dist-info/entry_points.txt +2 -0
- cocoa_tokenizer-26.6.0.dist-info/licenses/LICENSE.md +21 -0
- cocoa_tokenizer-26.6.0.dist-info/top_level.txt +1 -0
cocoa/__init__.py
ADDED
|
File without changes
|
cocoa/cli.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
CLI for cocoa - configurable collation and tokenization
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pathlib
|
|
8
|
+
import time
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
from typing import Annotated, Optional
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich import print
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from cocoa.collator import Collator
|
|
17
|
+
from cocoa.tokenizer import Tokenizer
|
|
18
|
+
from cocoa.util import combine_processed_data
|
|
19
|
+
from cocoa.winnower import Winnower
|
|
20
|
+
|
|
21
|
+
__version__ = version("cocoa")
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
name="cocoa",
|
|
25
|
+
help=f"Configurable collation and tokenization (v{__version__})",
|
|
26
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
27
|
+
)
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def collate(
|
|
33
|
+
collation_config: Annotated[
|
|
34
|
+
Optional[pathlib.Path],
|
|
35
|
+
typer.Option(
|
|
36
|
+
"--collation-config",
|
|
37
|
+
"-c",
|
|
38
|
+
help="Collation configuration file (overrides default)",
|
|
39
|
+
show_default=False,
|
|
40
|
+
),
|
|
41
|
+
] = None,
|
|
42
|
+
raw_data_home: Annotated[
|
|
43
|
+
str, typer.Option("--raw-data-home", "-r", help="Raw data directory")
|
|
44
|
+
] = ...,
|
|
45
|
+
processed_data_home: Annotated[
|
|
46
|
+
str,
|
|
47
|
+
typer.Option("--processed-data-home", "-p", help="Processed data directory"),
|
|
48
|
+
] = ...,
|
|
49
|
+
verbose: Annotated[
|
|
50
|
+
bool,
|
|
51
|
+
typer.Option(
|
|
52
|
+
"--verbose",
|
|
53
|
+
"-v",
|
|
54
|
+
help="Verbose logging for collate; this may cause "
|
|
55
|
+
"memory issues with large datasets",
|
|
56
|
+
is_flag=True,
|
|
57
|
+
),
|
|
58
|
+
] = False,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Collate raw data into a denormalized format.
|
|
62
|
+
|
|
63
|
+
Reads collation configuration and produces a MEDS-like parquet file
|
|
64
|
+
with collated events.
|
|
65
|
+
"""
|
|
66
|
+
with console.status("[bold green]Collating data..."):
|
|
67
|
+
t0 = time.perf_counter()
|
|
68
|
+
collator = Collator(
|
|
69
|
+
collation_cfg=collation_config,
|
|
70
|
+
raw_data_home=raw_data_home,
|
|
71
|
+
processed_data_home=processed_data_home,
|
|
72
|
+
)
|
|
73
|
+
collator.save_all(verbose=verbose)
|
|
74
|
+
t1 = time.perf_counter()
|
|
75
|
+
print(f"\n[green]✓[/green] Collation completed in {t1 - t0:.2f}s.")
|
|
76
|
+
out_path = collator.processed_data_home
|
|
77
|
+
print(f" Output: [cyan]{out_path}/meds.parquet[/cyan]")
|
|
78
|
+
print(f" Output: [cyan]{out_path}/subject_splits.parquet[/cyan]")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command()
|
|
82
|
+
def tokenize(
|
|
83
|
+
tokenization_config: Annotated[
|
|
84
|
+
Optional[pathlib.Path],
|
|
85
|
+
typer.Option(
|
|
86
|
+
"--tokenization-config",
|
|
87
|
+
"-c",
|
|
88
|
+
help="Tokenization configuration file (overrides default)",
|
|
89
|
+
show_default=False,
|
|
90
|
+
),
|
|
91
|
+
] = None,
|
|
92
|
+
processed_data_home: Annotated[
|
|
93
|
+
str,
|
|
94
|
+
typer.Option("--processed-data-home", "-p", help="Processed data directory"),
|
|
95
|
+
] = ...,
|
|
96
|
+
tokenizer_home: Annotated[
|
|
97
|
+
Optional[str],
|
|
98
|
+
typer.Option(
|
|
99
|
+
"--tokenizer-home",
|
|
100
|
+
"-t",
|
|
101
|
+
help="Use a pretrained tokenizer at this path",
|
|
102
|
+
show_default=False,
|
|
103
|
+
),
|
|
104
|
+
] = None,
|
|
105
|
+
verbose: Annotated[
|
|
106
|
+
bool,
|
|
107
|
+
typer.Option(
|
|
108
|
+
"--verbose",
|
|
109
|
+
"-v",
|
|
110
|
+
help="Verbose logging for collate; this may cause "
|
|
111
|
+
"memory issues with large datasets",
|
|
112
|
+
is_flag=True,
|
|
113
|
+
),
|
|
114
|
+
] = False,
|
|
115
|
+
):
|
|
116
|
+
"""
|
|
117
|
+
Tokenize collated data into integer sequences.
|
|
118
|
+
|
|
119
|
+
Reads collated parquet files and produces tokenized timelines with
|
|
120
|
+
vocabulary and bin information.
|
|
121
|
+
"""
|
|
122
|
+
with console.status("[bold green]Tokenizing data..."):
|
|
123
|
+
t0 = time.perf_counter()
|
|
124
|
+
if tokenizer_home is not None:
|
|
125
|
+
print(f"Using pretrained tokenizer from [cyan]{tokenizer_home}[/cyan]...")
|
|
126
|
+
tokenizer = Tokenizer(
|
|
127
|
+
tokenization_cfg=tokenization_config,
|
|
128
|
+
processed_data_home=processed_data_home,
|
|
129
|
+
).load(tokenizer_home)
|
|
130
|
+
else:
|
|
131
|
+
tokenizer = Tokenizer(
|
|
132
|
+
tokenization_cfg=tokenization_config,
|
|
133
|
+
processed_data_home=processed_data_home,
|
|
134
|
+
)
|
|
135
|
+
tokenizer.save_all(verbose=verbose)
|
|
136
|
+
t1 = time.perf_counter()
|
|
137
|
+
print(f"\n[green]✓[/green] Tokenization completed in {t1 - t0:.2f}s.")
|
|
138
|
+
out_path = tokenizer.processed_data_home
|
|
139
|
+
print(f" Output: [cyan]{out_path}/tokens_times.parquet[/cyan]")
|
|
140
|
+
print(f" Output: [cyan]{out_path}/tokenizer.yaml[/cyan]")
|
|
141
|
+
print(f" Vocabulary size: [cyan]{len(tokenizer)}[/cyan] tokens")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.command()
|
|
145
|
+
def winnow(
|
|
146
|
+
winnowing_config: Annotated[
|
|
147
|
+
Optional[pathlib.Path],
|
|
148
|
+
typer.Option(
|
|
149
|
+
"--winnowing-config",
|
|
150
|
+
"-c",
|
|
151
|
+
help="Winnowing configuration file (overrides default)",
|
|
152
|
+
show_default=False,
|
|
153
|
+
),
|
|
154
|
+
] = None,
|
|
155
|
+
processed_data_home: Annotated[
|
|
156
|
+
str,
|
|
157
|
+
typer.Option("--processed-data-home", "-p", help="Processed data directory"),
|
|
158
|
+
] = ...,
|
|
159
|
+
verbose: Annotated[
|
|
160
|
+
bool,
|
|
161
|
+
typer.Option(
|
|
162
|
+
"--verbose",
|
|
163
|
+
"-v",
|
|
164
|
+
help="Verbose logging for winnow; prints summary statistics",
|
|
165
|
+
is_flag=True,
|
|
166
|
+
),
|
|
167
|
+
] = False,
|
|
168
|
+
):
|
|
169
|
+
"""
|
|
170
|
+
Winnow held-out data for evaluation.
|
|
171
|
+
|
|
172
|
+
Filters held-out timelines and assigns flags to disqualify certain subjects
|
|
173
|
+
from evaluation based on the configured criteria.
|
|
174
|
+
"""
|
|
175
|
+
with console.status("[bold green]Winnowing data..."):
|
|
176
|
+
t0 = time.perf_counter()
|
|
177
|
+
winnower = Winnower(
|
|
178
|
+
winnowing_cfg=winnowing_config, processed_data_home=processed_data_home
|
|
179
|
+
)
|
|
180
|
+
winnower.save_all(verbose=verbose)
|
|
181
|
+
t1 = time.perf_counter()
|
|
182
|
+
print(f"\n[green]✓[/green] Winnowing completed in {t1 - t0:.2f}s.")
|
|
183
|
+
out_path = winnower.processed_data_home
|
|
184
|
+
for s in winnower.cfg.get("splits", ["held_out"]):
|
|
185
|
+
print(f" Output: [cyan]{out_path}/{s}_for_inference.parquet[/cyan]")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@app.command()
|
|
189
|
+
def pipeline(
|
|
190
|
+
collation_config: Annotated[
|
|
191
|
+
Optional[pathlib.Path],
|
|
192
|
+
typer.Option(
|
|
193
|
+
"--collation-config",
|
|
194
|
+
help="Collation configuration file (overrides default)",
|
|
195
|
+
show_default=False,
|
|
196
|
+
),
|
|
197
|
+
] = None,
|
|
198
|
+
tokenization_config: Annotated[
|
|
199
|
+
Optional[pathlib.Path],
|
|
200
|
+
typer.Option(
|
|
201
|
+
"--tokenization-config",
|
|
202
|
+
help="Tokenization configuration file (overrides default)",
|
|
203
|
+
show_default=False,
|
|
204
|
+
),
|
|
205
|
+
] = None,
|
|
206
|
+
winnowing_config: Annotated[
|
|
207
|
+
Optional[pathlib.Path],
|
|
208
|
+
typer.Option(
|
|
209
|
+
"--winnowing-config",
|
|
210
|
+
help="Winnowing configuration file (overrides default)",
|
|
211
|
+
show_default=False,
|
|
212
|
+
),
|
|
213
|
+
] = None,
|
|
214
|
+
raw_data_home: Annotated[
|
|
215
|
+
str, typer.Option("--raw-data-home", "-r", help="Raw data directory")
|
|
216
|
+
] = ...,
|
|
217
|
+
processed_data_home: Annotated[
|
|
218
|
+
str,
|
|
219
|
+
typer.Option("--processed-data-home", "-p", help="Processed data directory"),
|
|
220
|
+
] = ...,
|
|
221
|
+
verbose: Annotated[
|
|
222
|
+
bool,
|
|
223
|
+
typer.Option(
|
|
224
|
+
"--verbose", "-v", help="Verbose logging for pipeline steps", is_flag=True
|
|
225
|
+
),
|
|
226
|
+
] = False,
|
|
227
|
+
):
|
|
228
|
+
"""
|
|
229
|
+
Run the full pipeline: collate, tokenize, & winnow.
|
|
230
|
+
"""
|
|
231
|
+
print("[bold]Running full pipeline[/bold]\n")
|
|
232
|
+
t0 = time.perf_counter()
|
|
233
|
+
collate(
|
|
234
|
+
collation_config=collation_config,
|
|
235
|
+
raw_data_home=raw_data_home,
|
|
236
|
+
processed_data_home=processed_data_home,
|
|
237
|
+
verbose=verbose,
|
|
238
|
+
)
|
|
239
|
+
tokenize(
|
|
240
|
+
tokenization_config=tokenization_config,
|
|
241
|
+
processed_data_home=processed_data_home,
|
|
242
|
+
verbose=verbose,
|
|
243
|
+
)
|
|
244
|
+
winnow(
|
|
245
|
+
winnowing_config=winnowing_config,
|
|
246
|
+
processed_data_home=processed_data_home,
|
|
247
|
+
verbose=verbose,
|
|
248
|
+
)
|
|
249
|
+
t1 = time.perf_counter()
|
|
250
|
+
print(f"\n[bold green]Pipeline completed in {t1 - t0:.2f}s.[/bold green]")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@app.command()
|
|
254
|
+
def combine_datasets(
|
|
255
|
+
input_data_dirs: list[str],
|
|
256
|
+
output_data_dir: Annotated[
|
|
257
|
+
str,
|
|
258
|
+
typer.Option(
|
|
259
|
+
"--output-data-dir", "-o", help="Output directory for combined data"
|
|
260
|
+
),
|
|
261
|
+
] = ...,
|
|
262
|
+
):
|
|
263
|
+
"""
|
|
264
|
+
Combine multiple processed datasets into one.
|
|
265
|
+
|
|
266
|
+
Merges parquet files and validates that tokenizer configurations match
|
|
267
|
+
across all input directories.
|
|
268
|
+
"""
|
|
269
|
+
with console.status("[bold green]Combining datasets..."):
|
|
270
|
+
t0 = time.perf_counter()
|
|
271
|
+
output = combine_processed_data(
|
|
272
|
+
processed_data_homes=input_data_dirs, processed_data_home=output_data_dir
|
|
273
|
+
)
|
|
274
|
+
t1 = time.perf_counter()
|
|
275
|
+
print(f"\n[green]✓[/green] Combine completed in {t1 - t0:.2f}s.")
|
|
276
|
+
print(f" Output at: [cyan]{output}[/cyan]")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def main():
|
|
280
|
+
app()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
# pipeline()
|
|
285
|
+
main()
|
cocoa/collator.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
collects and collates different dataframes into a denormalized format
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pathlib
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
from cocoa.configurable import Configurable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Collator(Configurable):
|
|
16
|
+
default_file = "collation.yaml"
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
collation_cfg: pathlib.Path | str = None,
|
|
21
|
+
raw_data_home: pathlib.Path | str = None,
|
|
22
|
+
processed_data_home: pathlib.Path | str = None,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(collation_cfg, **kwargs)
|
|
26
|
+
|
|
27
|
+
self.raw_data_home, self.processed_data_home = map(
|
|
28
|
+
lambda p: pathlib.Path(p).expanduser().resolve(),
|
|
29
|
+
[raw_data_home, processed_data_home],
|
|
30
|
+
)
|
|
31
|
+
self.processed_data_home.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
self.reference_frame = None
|
|
33
|
+
self.splits: tuple = ("train", "tuning", "held_out")
|
|
34
|
+
self._tz_warned: set = set()
|
|
35
|
+
|
|
36
|
+
self.logger.info("Collator initialized...")
|
|
37
|
+
self.logger.info(f"{self.raw_data_home=}")
|
|
38
|
+
self.logger.info(f"{self.processed_data_home=}")
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def slightly_safer_eval(expr):
|
|
42
|
+
"""
|
|
43
|
+
performs eval on polars expressions;
|
|
44
|
+
prevents some forms of accidental usage
|
|
45
|
+
but is not secure against malicious input
|
|
46
|
+
"""
|
|
47
|
+
return eval(
|
|
48
|
+
expr,
|
|
49
|
+
{"__builtins__": {"str": str, "int": int, "float": float, "bool": bool}},
|
|
50
|
+
{"pl": pl},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def to_utc_naive(
|
|
54
|
+
self, df: pl.LazyFrame, column: str, *, table: str = None
|
|
55
|
+
) -> pl.Expr:
|
|
56
|
+
"""
|
|
57
|
+
expression converting `column` to a timezone-naive UTC datetime;
|
|
58
|
+
tz-aware columns are converted to UTC (instant-preserving),
|
|
59
|
+
tz-naive columns are assumed to already be UTC
|
|
60
|
+
"""
|
|
61
|
+
dtype = df.collect_schema().get(column)
|
|
62
|
+
if isinstance(dtype, pl.Datetime) and dtype.time_zone not in (None, "UTC"):
|
|
63
|
+
if (key := (table, column, dtype.time_zone)) not in self._tz_warned:
|
|
64
|
+
self._tz_warned.add(key)
|
|
65
|
+
self.logger.warning(
|
|
66
|
+
f"{table or '<frame>'}.{column}: "
|
|
67
|
+
f"converting {dtype.time_zone} -> UTC"
|
|
68
|
+
)
|
|
69
|
+
return (
|
|
70
|
+
pl.col(column)
|
|
71
|
+
.dt.convert_time_zone("UTC")
|
|
72
|
+
.dt.replace_time_zone(time_zone=None)
|
|
73
|
+
)
|
|
74
|
+
return pl.col(column).cast(pl.Datetime).dt.replace_time_zone(time_zone=None)
|
|
75
|
+
|
|
76
|
+
def load_table(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
table: str = None,
|
|
80
|
+
filter_expr: str | list = None,
|
|
81
|
+
agg_expr: str | list = None,
|
|
82
|
+
with_col_expr: str | list = None,
|
|
83
|
+
key: str = None,
|
|
84
|
+
subject_id_str: str = None,
|
|
85
|
+
time: str = None,
|
|
86
|
+
**kwargs,
|
|
87
|
+
) -> pl.LazyFrame:
|
|
88
|
+
"""
|
|
89
|
+
lazy-load the table `table`.parquet and perform some standard ETL
|
|
90
|
+
tasks in the following order if specified:
|
|
91
|
+
1. fix subject_id to `subject_id_str`
|
|
92
|
+
2. perform a filter operation
|
|
93
|
+
3. add columns
|
|
94
|
+
4. perform an aggregation by `key` or self.cfg["subfject_id"]
|
|
95
|
+
"""
|
|
96
|
+
if (f := self.raw_data_home / f"{table}.parquet").exists():
|
|
97
|
+
df = pl.scan_parquet(f)
|
|
98
|
+
elif (f := self.raw_data_home / f"{table}.csv").exists():
|
|
99
|
+
df = pl.scan_csv(f)
|
|
100
|
+
else:
|
|
101
|
+
raise FileNotFoundError(
|
|
102
|
+
f"No parquet / csv file found for {table=} in {self.raw_data_home}"
|
|
103
|
+
)
|
|
104
|
+
if subject_id_str is not None:
|
|
105
|
+
df = df.with_columns(pl.col(subject_id_str).alias(self.cfg["subject_id"]))
|
|
106
|
+
if filter_expr is not None:
|
|
107
|
+
df = df.filter(
|
|
108
|
+
self.slightly_safer_eval(filter_expr)
|
|
109
|
+
if isinstance(filter_expr, str)
|
|
110
|
+
else [self.slightly_safer_eval(c) for c in filter_expr]
|
|
111
|
+
)
|
|
112
|
+
if with_col_expr is not None:
|
|
113
|
+
df = df.with_columns(
|
|
114
|
+
self.slightly_safer_eval(with_col_expr)
|
|
115
|
+
if isinstance(with_col_expr, str)
|
|
116
|
+
else [self.slightly_safer_eval(c) for c in with_col_expr]
|
|
117
|
+
)
|
|
118
|
+
if agg_expr is not None:
|
|
119
|
+
df = (
|
|
120
|
+
df.sort(time)
|
|
121
|
+
.group_by(key if key is not None else self.cfg["subject_id"])
|
|
122
|
+
.agg(
|
|
123
|
+
self.slightly_safer_eval(agg_expr)
|
|
124
|
+
if isinstance(agg_expr, str)
|
|
125
|
+
else [self.slightly_safer_eval(c) for c in agg_expr]
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return df
|
|
129
|
+
|
|
130
|
+
def get_reference_frame(self) -> pl.LazyFrame:
|
|
131
|
+
"""create the static reference frame as configured"""
|
|
132
|
+
cfg = self.cfg["reference"]
|
|
133
|
+
if self.reference_frame is not None: # pull from cache if available
|
|
134
|
+
return self.reference_frame
|
|
135
|
+
df = self.load_table(**cfg)
|
|
136
|
+
for tkv in cfg.get("augmentation_tables", []):
|
|
137
|
+
df = df.join(
|
|
138
|
+
self.load_table(**tkv),
|
|
139
|
+
on=tkv["key"],
|
|
140
|
+
validate=tkv["validation"],
|
|
141
|
+
how="left",
|
|
142
|
+
maintain_order="left",
|
|
143
|
+
)
|
|
144
|
+
for col in (cfg["start_time"], cfg["end_time"]):
|
|
145
|
+
df = df.with_columns(
|
|
146
|
+
self.to_utc_naive(df, col, table=cfg["table"]).alias(col)
|
|
147
|
+
)
|
|
148
|
+
self.reference_frame = df # cache result
|
|
149
|
+
return self.reference_frame
|
|
150
|
+
|
|
151
|
+
def get_entry(
|
|
152
|
+
self,
|
|
153
|
+
table: str,
|
|
154
|
+
time: str,
|
|
155
|
+
code: str,
|
|
156
|
+
*,
|
|
157
|
+
prefix: str = None,
|
|
158
|
+
numeric_value: str = None,
|
|
159
|
+
text_value: str = None,
|
|
160
|
+
filter_expr: str = None,
|
|
161
|
+
with_col_expr: str = None,
|
|
162
|
+
reference_key: str = None,
|
|
163
|
+
subject_id_str: str = None,
|
|
164
|
+
fix_date_to_time: bool = None,
|
|
165
|
+
agg_expr: str | list = None,
|
|
166
|
+
key: str = None,
|
|
167
|
+
) -> pl.LazyFrame:
|
|
168
|
+
"""create tokens corresponding to a configured event"""
|
|
169
|
+
df = (
|
|
170
|
+
self.get_reference_frame()
|
|
171
|
+
if table == "REFERENCE"
|
|
172
|
+
else self.load_table(
|
|
173
|
+
table=table,
|
|
174
|
+
time=time,
|
|
175
|
+
filter_expr=filter_expr,
|
|
176
|
+
with_col_expr=with_col_expr,
|
|
177
|
+
subject_id_str=subject_id_str,
|
|
178
|
+
agg_expr=agg_expr,
|
|
179
|
+
key=key,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
if time in df.collect_schema().names():
|
|
183
|
+
df = df.with_columns(self.to_utc_naive(df, time, table=table).alias(time))
|
|
184
|
+
# otherwise `time` arrives normalized via the reference_key join below
|
|
185
|
+
if fix_date_to_time:
|
|
186
|
+
# if a date was cast to a time,
|
|
187
|
+
# the default of 00:00:00 should be replaced with 23:59:59
|
|
188
|
+
df = df.with_columns(
|
|
189
|
+
pl.when(pl.col(time).cast(pl.Datetime).dt.time() == pl.time(0, 0, 0))
|
|
190
|
+
.then(
|
|
191
|
+
pl.col(time)
|
|
192
|
+
.cast(pl.Datetime)
|
|
193
|
+
.dt.replace(hour=23, minute=59, second=59)
|
|
194
|
+
)
|
|
195
|
+
.otherwise(pl.col(time).cast(pl.Datetime))
|
|
196
|
+
.alias(time)
|
|
197
|
+
)
|
|
198
|
+
if reference_key is not None:
|
|
199
|
+
df = df.join(self.reference_frame, on=reference_key, how="inner").filter(
|
|
200
|
+
pl.col(time)
|
|
201
|
+
.cast(pl.Datetime)
|
|
202
|
+
.is_between(
|
|
203
|
+
pl.col(self.cfg["reference"]["start_time"]).cast(pl.Datetime),
|
|
204
|
+
pl.col(self.cfg["reference"]["end_time"]).cast(pl.Datetime),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return df.select(
|
|
209
|
+
pl.col(self.cfg["subject_id"]).cast(pl.String).alias("subject_id"),
|
|
210
|
+
# `time` was normalized to naive UTC when the frame was loaded above
|
|
211
|
+
pl.col(time).alias("time"),
|
|
212
|
+
pl.when(pl.col(code).is_not_null())
|
|
213
|
+
.then(
|
|
214
|
+
pl.concat_str(
|
|
215
|
+
[
|
|
216
|
+
pl.lit(prefix),
|
|
217
|
+
pl.col(code)
|
|
218
|
+
.cast(pl.String)
|
|
219
|
+
.str.to_lowercase()
|
|
220
|
+
.str.replace_all(r"\s+", "_"),
|
|
221
|
+
],
|
|
222
|
+
separator="//",
|
|
223
|
+
ignore_nulls=True, # prefix is optional
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
.alias("code"),
|
|
227
|
+
(pl.col(numeric_value) if numeric_value else pl.lit(None))
|
|
228
|
+
.cast(pl.Float32) # dumb
|
|
229
|
+
.alias("numeric_value"),
|
|
230
|
+
(
|
|
231
|
+
pl.col(text_value).str.to_lowercase().str.replace_all(r"\s+", "_")
|
|
232
|
+
if text_value
|
|
233
|
+
else pl.lit(None)
|
|
234
|
+
)
|
|
235
|
+
.cast(pl.String)
|
|
236
|
+
.alias("text_value"),
|
|
237
|
+
).drop_nulls(subset=["subject_id", "time", "code"])
|
|
238
|
+
|
|
239
|
+
def get_all(self) -> pl.LazyFrame:
|
|
240
|
+
"""get all tokens for all events as configured"""
|
|
241
|
+
return pl.concat(
|
|
242
|
+
(self.get_entry(**entry) for entry in self.cfg.get("entries", []))
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def get_subject_splits(self) -> pl.DataFrame:
|
|
246
|
+
"""get the subject splits as configured"""
|
|
247
|
+
partition = (
|
|
248
|
+
self.get_reference_frame()
|
|
249
|
+
.group_by(self.cfg.get("group_id", self.cfg["subject_id"]))
|
|
250
|
+
.agg(pl.col(self.cfg.reference.start_time).min().alias("first_time"))
|
|
251
|
+
.sort("first_time")
|
|
252
|
+
.with_row_index()
|
|
253
|
+
).collect()
|
|
254
|
+
split_idx = (
|
|
255
|
+
(
|
|
256
|
+
len(partition)
|
|
257
|
+
* np.cumsum(
|
|
258
|
+
[
|
|
259
|
+
self.cfg.subject_splits.train_frac,
|
|
260
|
+
self.cfg.subject_splits.tuning_frac,
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
.astype(int)
|
|
265
|
+
.tolist()
|
|
266
|
+
)
|
|
267
|
+
partition = partition.with_columns(
|
|
268
|
+
split=pl.when(pl.col("index") < split_idx[0])
|
|
269
|
+
.then(pl.lit(self.splits[0]))
|
|
270
|
+
.when(pl.col("index") < split_idx[1])
|
|
271
|
+
.then(pl.lit(self.splits[1]))
|
|
272
|
+
.otherwise(pl.lit(self.splits[2]))
|
|
273
|
+
)
|
|
274
|
+
return (
|
|
275
|
+
partition
|
|
276
|
+
if "group_id" not in self.cfg
|
|
277
|
+
else self.get_reference_frame()
|
|
278
|
+
.collect()
|
|
279
|
+
.join(partition, on=self.cfg["group_id"])
|
|
280
|
+
).select(pl.col(self.cfg["subject_id"]).alias("subject_id"), "split")
|
|
281
|
+
|
|
282
|
+
def save_all(self, verbose: bool = False):
|
|
283
|
+
"""save collated data and subject splits to disc, optionally w/ summary stats"""
|
|
284
|
+
meds_path = self.processed_data_home / "meds.parquet"
|
|
285
|
+
meds_path.unlink(missing_ok=True)
|
|
286
|
+
df_all = self.get_all()
|
|
287
|
+
try:
|
|
288
|
+
df_all.sink_parquet(meds_path, engine="streaming")
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self.logger.warning(f"Streaming write failed: {e}")
|
|
291
|
+
df_all.sink_parquet(meds_path, engine="in-memory", mkdir=True)
|
|
292
|
+
df_splits = self.get_subject_splits()
|
|
293
|
+
if "pass_through_columns" in self.cfg:
|
|
294
|
+
df_splits = df_splits.join(
|
|
295
|
+
self.reference_frame.select(
|
|
296
|
+
pl.col(self.cfg["subject_id"]).alias("subject_id"),
|
|
297
|
+
*self.cfg.pass_through_columns,
|
|
298
|
+
).collect(),
|
|
299
|
+
on="subject_id",
|
|
300
|
+
how="inner",
|
|
301
|
+
validate="1:1",
|
|
302
|
+
)
|
|
303
|
+
df_splits.write_parquet(
|
|
304
|
+
self.processed_data_home / "subject_splits.parquet", mkdir=True
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if verbose:
|
|
308
|
+
self.logger.summarize_meds_like(df_all, df_splits)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
self = Collator(
|
|
313
|
+
raw_data_home="./raw-data/raw-mimic/dev/",
|
|
314
|
+
processed_data_home="./processed/mimic/",
|
|
315
|
+
)
|
|
316
|
+
self.save_all(verbose=True)
|
|
317
|
+
print(self.get_subject_splits())
|
|
318
|
+
# breakpoint()
|
cocoa/config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Packaged default configuration resources for cocoa."""
|