cehrbert-data 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.
- __init__.py +0 -0
- cehrbert_data/__init__.py +0 -0
- cehrbert_data/apps/__init__.py +0 -0
- cehrbert_data/apps/generate_concept_similarity_table.py +423 -0
- cehrbert_data/apps/generate_hierarchical_bert_training_data.py +238 -0
- cehrbert_data/apps/generate_included_concept_list.py +116 -0
- cehrbert_data/apps/generate_information_content.py +131 -0
- cehrbert_data/apps/generate_required_labs.py +112 -0
- cehrbert_data/apps/generate_training_data.py +337 -0
- cehrbert_data/cohorts/__init__.py +0 -0
- cehrbert_data/cohorts/atrial_fibrillation.py +45 -0
- cehrbert_data/cohorts/cabg.py +72 -0
- cehrbert_data/cohorts/coronary_artery_disease.py +84 -0
- cehrbert_data/cohorts/covid.py +43 -0
- cehrbert_data/cohorts/covid_inpatient.py +84 -0
- cehrbert_data/cohorts/death.py +46 -0
- cehrbert_data/cohorts/heart_failure.py +423 -0
- cehrbert_data/cohorts/ischemic_stroke.py +45 -0
- cehrbert_data/cohorts/last_visit_discharged_home.py +35 -0
- cehrbert_data/cohorts/query_builder.py +153 -0
- cehrbert_data/cohorts/spark_app_base.py +745 -0
- cehrbert_data/cohorts/type_two_diabietes.py +166 -0
- cehrbert_data/cohorts/ventilation.py +22 -0
- cehrbert_data/config/__init__.py +0 -0
- cehrbert_data/config/output_names.py +9 -0
- cehrbert_data/const/__init__.py +0 -0
- cehrbert_data/const/__pycache__/__init__.cpython-311.pyc +0 -0
- cehrbert_data/const/__pycache__/common.cpython-311.pyc +0 -0
- cehrbert_data/const/common.py +28 -0
- cehrbert_data/decorators/__init__.py +0 -0
- cehrbert_data/decorators/__pycache__/__init__.cpython-311.pyc +0 -0
- cehrbert_data/decorators/__pycache__/patient_event_decorator.cpython-311.pyc +0 -0
- cehrbert_data/decorators/patient_event_decorator.py +759 -0
- cehrbert_data/prediction_cohorts/__init__.py +0 -0
- cehrbert_data/prediction_cohorts/afib_ischemic_stroke.py +14 -0
- cehrbert_data/prediction_cohorts/cad_cabg_cohort.py +19 -0
- cehrbert_data/prediction_cohorts/cad_hf_cohort.py +14 -0
- cehrbert_data/prediction_cohorts/copd_readmission.py +76 -0
- cehrbert_data/prediction_cohorts/covid_death.py +14 -0
- cehrbert_data/prediction_cohorts/covid_ventilation.py +14 -0
- cehrbert_data/prediction_cohorts/discharge_home_death.py +18 -0
- cehrbert_data/prediction_cohorts/hf_readmission.py +82 -0
- cehrbert_data/prediction_cohorts/hospitalization.py +100 -0
- cehrbert_data/prediction_cohorts/hospitalization_mortality.py +77 -0
- cehrbert_data/prediction_cohorts/t2dm_hf_cohort.py +14 -0
- cehrbert_data/queries/__init__.py +0 -0
- cehrbert_data/queries/measurement_unit_stats_query.py +42 -0
- cehrbert_data/tools/__init__.py +0 -0
- cehrbert_data/tools/download_omop_tables.py +141 -0
- cehrbert_data/utils/__init__.py +0 -0
- cehrbert_data/utils/spark_parse_args.py +350 -0
- cehrbert_data/utils/spark_utils.py +1405 -0
- cehrbert_data-0.0.1.dist-info/LICENSE +21 -0
- cehrbert_data-0.0.1.dist-info/METADATA +116 -0
- cehrbert_data-0.0.1.dist-info/RECORD +57 -0
- cehrbert_data-0.0.1.dist-info/WHEEL +5 -0
- cehrbert_data-0.0.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from pyspark.sql import DataFrame
|
|
8
|
+
from pyspark.sql import Window as W
|
|
9
|
+
from pyspark.sql import functions as F
|
|
10
|
+
from pyspark.sql import types as T
|
|
11
|
+
|
|
12
|
+
from cehrbert_data.const.common import CATEGORICAL_MEASUREMENT, MEASUREMENT
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AttType(Enum):
|
|
16
|
+
DAY = "day"
|
|
17
|
+
WEEK = "week"
|
|
18
|
+
MONTH = "month"
|
|
19
|
+
CEHR_BERT = "cehr_bert"
|
|
20
|
+
MIX = "mix"
|
|
21
|
+
NONE = "none"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PatientEventDecorator(ABC):
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def _decorate(self, patient_events):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def decorate(self, patient_events):
|
|
30
|
+
decorated_patient_events = self._decorate(patient_events)
|
|
31
|
+
self.validate(decorated_patient_events)
|
|
32
|
+
return decorated_patient_events
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_required_columns(cls):
|
|
36
|
+
return set(
|
|
37
|
+
[
|
|
38
|
+
"cohort_member_id",
|
|
39
|
+
"person_id",
|
|
40
|
+
"standard_concept_id",
|
|
41
|
+
"date",
|
|
42
|
+
"datetime",
|
|
43
|
+
"visit_occurrence_id",
|
|
44
|
+
"domain",
|
|
45
|
+
"concept_value",
|
|
46
|
+
"visit_rank_order",
|
|
47
|
+
"visit_segment",
|
|
48
|
+
"priority",
|
|
49
|
+
"date_in_week",
|
|
50
|
+
"concept_value_mask",
|
|
51
|
+
"mlm_skip_value",
|
|
52
|
+
"age",
|
|
53
|
+
"visit_concept_id",
|
|
54
|
+
"visit_start_date",
|
|
55
|
+
"visit_start_datetime",
|
|
56
|
+
"visit_concept_order",
|
|
57
|
+
"concept_order",
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def validate(self, patient_events: DataFrame):
|
|
62
|
+
actual_column_set = set(patient_events.columns)
|
|
63
|
+
expected_column_set = set(self.get_required_columns())
|
|
64
|
+
if actual_column_set != expected_column_set:
|
|
65
|
+
diff_left = actual_column_set - expected_column_set
|
|
66
|
+
diff_right = expected_column_set - actual_column_set
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"{self}\n"
|
|
69
|
+
f"actual_column_set - expected_column_set: {diff_left}\n"
|
|
70
|
+
f"expected_column_set - actual_column_set: {diff_right}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PatientEventBaseDecorator(PatientEventDecorator):
|
|
75
|
+
# output_columns = [
|
|
76
|
+
# 'cohort_member_id', 'person_id', 'concept_ids', 'visit_segments', 'orders',
|
|
77
|
+
# 'dates', 'ages', 'visit_concept_orders', 'num_of_visits', 'num_of_concepts',
|
|
78
|
+
# 'concept_value_masks', 'concept_values', 'mlm_skip_values',
|
|
79
|
+
# 'visit_concept_ids'
|
|
80
|
+
# ]
|
|
81
|
+
def __init__(self, visit_occurrence):
|
|
82
|
+
self._visit_occurrence = visit_occurrence
|
|
83
|
+
|
|
84
|
+
def _decorate(self, patient_events: DataFrame):
|
|
85
|
+
"""
|
|
86
|
+
Patient_events contains the following columns (cohort_member_id, person_id,.
|
|
87
|
+
|
|
88
|
+
standard_concept_id, date, visit_occurrence_id, domain, concept_value)
|
|
89
|
+
|
|
90
|
+
:param patient_events:
|
|
91
|
+
:return:
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# todo: create an assertion the dataframe contains the above columns
|
|
95
|
+
|
|
96
|
+
valid_visit_ids = patient_events.select("visit_occurrence_id", "cohort_member_id").distinct()
|
|
97
|
+
|
|
98
|
+
# Add visit_start_date to the patient_events dataframe and create the visit rank
|
|
99
|
+
visit_rank_udf = F.row_number().over(
|
|
100
|
+
W.partitionBy("person_id", "cohort_member_id").orderBy(
|
|
101
|
+
"visit_start_datetime", "is_inpatient", "expired", "visit_occurrence_id"
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
visit_segment_udf = F.col("visit_rank_order") % F.lit(2) + 1
|
|
105
|
+
|
|
106
|
+
# The visit records are joined to the cohort members (there could be multiple entries for the same patient)
|
|
107
|
+
# if multiple entries are present, we duplicate the visit records for those.
|
|
108
|
+
visits = (
|
|
109
|
+
self._visit_occurrence.join(valid_visit_ids, "visit_occurrence_id")
|
|
110
|
+
.select(
|
|
111
|
+
"person_id",
|
|
112
|
+
"cohort_member_id",
|
|
113
|
+
"visit_occurrence_id",
|
|
114
|
+
"visit_end_date",
|
|
115
|
+
F.col("visit_start_date").cast(T.DateType()).alias("visit_start_date"),
|
|
116
|
+
F.to_timestamp("visit_start_datetime").alias("visit_start_datetime"),
|
|
117
|
+
F.col("visit_concept_id").cast("int").isin([9201, 262, 8971, 8920]).cast("int").alias("is_inpatient"),
|
|
118
|
+
F.when(F.col("discharged_to_concept_id").cast("int") == 4216643, F.lit(1))
|
|
119
|
+
.otherwise(F.lit(0))
|
|
120
|
+
.alias("expired"),
|
|
121
|
+
)
|
|
122
|
+
.withColumn("visit_rank_order", visit_rank_udf)
|
|
123
|
+
.withColumn("visit_segment", visit_segment_udf)
|
|
124
|
+
.drop("person_id", "expired")
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Determine the concept order depending on the visit type. For outpatient visits, we assume the concepts to
|
|
128
|
+
# have the same order, whereas for inpatient visits, the concept order is determined by the time stamp.
|
|
129
|
+
# the concept order needs to be generated per each cohort member because the same visit could be used
|
|
130
|
+
# in multiple cohort's histories of the same patient
|
|
131
|
+
concept_order_udf = F.when(
|
|
132
|
+
F.col("is_inpatient") == 1,
|
|
133
|
+
F.dense_rank().over(W.partitionBy("cohort_member_id", "visit_occurrence_id").orderBy("datetime")),
|
|
134
|
+
).otherwise(F.lit(1))
|
|
135
|
+
|
|
136
|
+
# Determine the global visit concept order for each patient, this takes both visit_rank_order and concept_order
|
|
137
|
+
# into account when assigning this new order.
|
|
138
|
+
# e.g. visit_rank_order = [1, 1, 2, 2], concept_order = [1, 1, 1, 2] -> visit_concept_order = [1, 1, 2, 3]
|
|
139
|
+
visit_concept_order_udf = F.dense_rank().over(
|
|
140
|
+
W.partitionBy("person_id", "cohort_member_id").orderBy("visit_rank_order", "concept_order")
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# We need to set the visit_end_date as the visit_start_date for outpatient visits
|
|
144
|
+
# For inpatient visits, we use the original visit_end_date if available, otherwise
|
|
145
|
+
# we will infer the visit_end_date using the max(date) of the current visit
|
|
146
|
+
visit_end_date_udf = F.when(
|
|
147
|
+
F.col("is_inpatient") == 1,
|
|
148
|
+
F.coalesce(
|
|
149
|
+
F.col("visit_end_date"),
|
|
150
|
+
F.max("date").over(W.partitionBy("cohort_member_id", "visit_occurrence_id")),
|
|
151
|
+
),
|
|
152
|
+
).otherwise(F.col("visit_start_date"))
|
|
153
|
+
|
|
154
|
+
# We need to bound the medical event dates between visit_start_date and visit_end_date
|
|
155
|
+
bound_medical_event_date = F.when(
|
|
156
|
+
F.col("date") < F.col("visit_start_date"), F.col("visit_start_date")
|
|
157
|
+
).otherwise(F.when(F.col("date") > F.col("visit_end_date"), F.col("visit_end_date")).otherwise(F.col("date")))
|
|
158
|
+
|
|
159
|
+
# We need to bound the medical event dates between visit_start_date and visit_end_date
|
|
160
|
+
bound_medical_event_datetime = F.when(
|
|
161
|
+
F.col("datetime") < F.col("visit_start_datetime"),
|
|
162
|
+
F.col("visit_start_datetime"),
|
|
163
|
+
).otherwise(
|
|
164
|
+
F.when(
|
|
165
|
+
F.col("datetime") > F.col("visit_end_datetime"),
|
|
166
|
+
F.col("visit_end_datetime"),
|
|
167
|
+
).otherwise(F.col("datetime"))
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
patient_events = (
|
|
171
|
+
patient_events.join(visits, ["cohort_member_id", "visit_occurrence_id"])
|
|
172
|
+
.withColumn("visit_end_date", visit_end_date_udf)
|
|
173
|
+
.withColumn("visit_end_datetime", F.date_add("visit_end_date", 1))
|
|
174
|
+
.withColumn("visit_end_datetime", F.expr("visit_end_datetime - INTERVAL 1 MINUTE"))
|
|
175
|
+
.withColumn("date", bound_medical_event_date)
|
|
176
|
+
.withColumn("datetime", bound_medical_event_datetime)
|
|
177
|
+
.withColumn("concept_order", concept_order_udf)
|
|
178
|
+
.withColumn("visit_concept_order", visit_concept_order_udf)
|
|
179
|
+
.drop("is_inpatient", "visit_end_date", "visit_end_datetime")
|
|
180
|
+
.distinct()
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Set the priority for the events.
|
|
184
|
+
# Create the week since epoch UDF
|
|
185
|
+
weeks_since_epoch_udf = (F.unix_timestamp("date") / F.lit(24 * 60 * 60 * 7)).cast("int")
|
|
186
|
+
patient_events = patient_events.withColumn("priority", F.lit(0)).withColumn(
|
|
187
|
+
"date_in_week", weeks_since_epoch_udf
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Create the concept_value_mask field to indicate whether domain values should be skipped
|
|
191
|
+
# As of now only measurement has values, so other domains would be skipped.
|
|
192
|
+
patient_events = patient_events.withColumn(
|
|
193
|
+
"concept_value_mask", (F.col("domain") == MEASUREMENT).cast("int")
|
|
194
|
+
).withColumn(
|
|
195
|
+
"mlm_skip_value",
|
|
196
|
+
(F.col("domain").isin([MEASUREMENT, CATEGORICAL_MEASUREMENT])).cast("int"),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if "concept_value" not in patient_events.schema.fieldNames():
|
|
200
|
+
patient_events = patient_events.withColumn("concept_value", F.lit(0.0))
|
|
201
|
+
|
|
202
|
+
# (cohort_member_id, person_id, standard_concept_id, date, datetime, visit_occurrence_id, domain,
|
|
203
|
+
# concept_value, visit_rank_order, visit_segment, priority, date_in_week,
|
|
204
|
+
# concept_value_mask, mlm_skip_value, age)
|
|
205
|
+
return patient_events
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class PatientEventAttDecorator(PatientEventDecorator):
|
|
209
|
+
def __init__(
|
|
210
|
+
self,
|
|
211
|
+
visit_occurrence,
|
|
212
|
+
include_visit_type,
|
|
213
|
+
exclude_visit_tokens,
|
|
214
|
+
att_type: AttType,
|
|
215
|
+
include_inpatient_hour_token: bool = False,
|
|
216
|
+
):
|
|
217
|
+
self._visit_occurrence = visit_occurrence
|
|
218
|
+
self._include_visit_type = include_visit_type
|
|
219
|
+
self._exclude_visit_tokens = exclude_visit_tokens
|
|
220
|
+
self._att_type = att_type
|
|
221
|
+
self._include_inpatient_hour_token = include_inpatient_hour_token
|
|
222
|
+
|
|
223
|
+
def _decorate(self, patient_events: DataFrame):
|
|
224
|
+
if self._att_type == AttType.NONE:
|
|
225
|
+
return patient_events
|
|
226
|
+
|
|
227
|
+
# visits should the following columns (person_id,
|
|
228
|
+
# visit_concept_id, visit_start_date, visit_occurrence_id, domain, concept_value)
|
|
229
|
+
cohort_member_person_pair = patient_events.select("person_id", "cohort_member_id").distinct()
|
|
230
|
+
valid_visit_ids = patient_events.groupby(
|
|
231
|
+
"cohort_member_id",
|
|
232
|
+
"visit_occurrence_id",
|
|
233
|
+
"visit_segment",
|
|
234
|
+
"visit_rank_order",
|
|
235
|
+
).agg(
|
|
236
|
+
F.min("visit_concept_order").alias("min_visit_concept_order"),
|
|
237
|
+
F.max("visit_concept_order").alias("max_visit_concept_order"),
|
|
238
|
+
F.min("concept_order").alias("min_concept_order"),
|
|
239
|
+
F.max("concept_order").alias("max_concept_order"),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
visit_occurrence = (
|
|
243
|
+
self._visit_occurrence.select(
|
|
244
|
+
"person_id",
|
|
245
|
+
F.col("visit_start_date").cast(T.DateType()).alias("date"),
|
|
246
|
+
F.col("visit_start_date").cast(T.DateType()).alias("visit_start_date"),
|
|
247
|
+
F.col("visit_start_datetime").cast(T.TimestampType()).alias("visit_start_datetime"),
|
|
248
|
+
F.coalesce("visit_end_date", "visit_start_date").cast(T.DateType()).alias("visit_end_date"),
|
|
249
|
+
"visit_concept_id",
|
|
250
|
+
"visit_occurrence_id",
|
|
251
|
+
F.lit("visit").alias("domain"),
|
|
252
|
+
F.lit(0.0).alias("concept_value"),
|
|
253
|
+
F.lit(0).alias("concept_value_mask"),
|
|
254
|
+
F.lit(0).alias("mlm_skip_value"),
|
|
255
|
+
"age",
|
|
256
|
+
"discharged_to_concept_id",
|
|
257
|
+
)
|
|
258
|
+
.join(valid_visit_ids, "visit_occurrence_id")
|
|
259
|
+
.join(cohort_member_person_pair, ["person_id", "cohort_member_id"])
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# We assume outpatient visits end on the same day, therefore we start visit_end_date to visit_start_date due
|
|
263
|
+
# to bad date
|
|
264
|
+
visit_occurrence = visit_occurrence.withColumn(
|
|
265
|
+
"visit_end_date",
|
|
266
|
+
F.when(
|
|
267
|
+
F.col("visit_concept_id").isin([9201, 262, 8971, 8920]),
|
|
268
|
+
F.col("visit_end_date"),
|
|
269
|
+
).otherwise(F.col("visit_start_date")),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
weeks_since_epoch_udf = (F.unix_timestamp("date") / F.lit(24 * 60 * 60 * 7)).cast("int")
|
|
273
|
+
visit_occurrence = visit_occurrence.withColumn("date_in_week", weeks_since_epoch_udf)
|
|
274
|
+
|
|
275
|
+
# Cache visit for faster processing
|
|
276
|
+
visit_occurrence.cache()
|
|
277
|
+
|
|
278
|
+
visits = visit_occurrence.drop("discharged_to_concept_id")
|
|
279
|
+
|
|
280
|
+
# (cohort_member_id, person_id, standard_concept_id, date, visit_occurrence_id, domain,
|
|
281
|
+
# concept_value, visit_rank_order, visit_segment, priority, date_in_week,
|
|
282
|
+
# concept_value_mask, mlm_skip_value, visit_end_date)
|
|
283
|
+
visit_start_events = (
|
|
284
|
+
visits.withColumn("date", F.col("visit_start_date"))
|
|
285
|
+
.withColumn("datetime", F.to_timestamp("visit_start_date"))
|
|
286
|
+
.withColumn("standard_concept_id", F.lit("VS"))
|
|
287
|
+
.withColumn("visit_concept_order", F.col("min_visit_concept_order"))
|
|
288
|
+
.withColumn("concept_order", F.col("min_concept_order") - 1)
|
|
289
|
+
.withColumn("priority", F.lit(-2))
|
|
290
|
+
.drop("min_visit_concept_order", "max_visit_concept_order")
|
|
291
|
+
.drop("min_concept_order", "max_concept_order")
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
visit_end_events = (
|
|
295
|
+
visits.withColumn("date", F.col("visit_end_date"))
|
|
296
|
+
.withColumn("datetime", F.date_add(F.to_timestamp("visit_end_date"), 1))
|
|
297
|
+
.withColumn("datetime", F.expr("datetime - INTERVAL 1 MINUTE"))
|
|
298
|
+
.withColumn("standard_concept_id", F.lit("VE"))
|
|
299
|
+
.withColumn("visit_concept_order", F.col("max_visit_concept_order"))
|
|
300
|
+
.withColumn("concept_order", F.col("max_concept_order") + 1)
|
|
301
|
+
.withColumn("priority", F.lit(200))
|
|
302
|
+
.drop("min_visit_concept_order", "max_visit_concept_order")
|
|
303
|
+
.drop("min_concept_order", "max_concept_order")
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Get the prev days_since_epoch
|
|
307
|
+
prev_visit_end_date_udf = F.lag("visit_end_date").over(
|
|
308
|
+
W.partitionBy("person_id", "cohort_member_id").orderBy("visit_rank_order")
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Compute the time difference between the current record and the previous record
|
|
312
|
+
time_delta_udf = F.when(F.col("prev_visit_end_date").isNull(), 0).otherwise(
|
|
313
|
+
F.datediff("visit_start_date", "prev_visit_end_date")
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Udf for calculating the time token
|
|
317
|
+
if self._att_type == AttType.DAY:
|
|
318
|
+
att_func = time_day_token
|
|
319
|
+
elif self._att_type == AttType.WEEK:
|
|
320
|
+
att_func = time_week_token
|
|
321
|
+
elif self._att_type == AttType.MONTH:
|
|
322
|
+
att_func = time_month_token
|
|
323
|
+
elif self._att_type == AttType.MIX:
|
|
324
|
+
att_func = time_mix_token
|
|
325
|
+
else:
|
|
326
|
+
att_func = time_token_func
|
|
327
|
+
|
|
328
|
+
time_token_udf = F.udf(att_func, T.StringType())
|
|
329
|
+
|
|
330
|
+
att_tokens = (
|
|
331
|
+
visits.withColumn("datetime", F.to_timestamp("date"))
|
|
332
|
+
.withColumn("prev_visit_end_date", prev_visit_end_date_udf)
|
|
333
|
+
.where(F.col("prev_visit_end_date").isNotNull())
|
|
334
|
+
.withColumn("time_delta", time_delta_udf)
|
|
335
|
+
.withColumn(
|
|
336
|
+
"time_delta",
|
|
337
|
+
F.when(F.col("time_delta") < 0, F.lit(0)).otherwise(F.col("time_delta")),
|
|
338
|
+
)
|
|
339
|
+
.withColumn("standard_concept_id", time_token_udf("time_delta"))
|
|
340
|
+
.withColumn("priority", F.lit(-3))
|
|
341
|
+
.withColumn("visit_rank_order", F.col("visit_rank_order"))
|
|
342
|
+
.withColumn("visit_concept_order", F.col("min_visit_concept_order"))
|
|
343
|
+
.withColumn("concept_order", F.lit(0))
|
|
344
|
+
.drop("prev_visit_end_date", "time_delta")
|
|
345
|
+
.drop("min_visit_concept_order", "max_visit_concept_order")
|
|
346
|
+
.drop("min_concept_order", "max_concept_order")
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if self._exclude_visit_tokens:
|
|
350
|
+
artificial_tokens = att_tokens
|
|
351
|
+
else:
|
|
352
|
+
artificial_tokens = visit_start_events.unionByName(att_tokens).unionByName(visit_end_events)
|
|
353
|
+
|
|
354
|
+
if self._include_visit_type:
|
|
355
|
+
# insert visit type after the VS token
|
|
356
|
+
visit_type_tokens = (
|
|
357
|
+
visits.withColumn("standard_concept_id", F.col("visit_concept_id"))
|
|
358
|
+
.withColumn("datetime", F.to_timestamp("date"))
|
|
359
|
+
.withColumn("visit_concept_order", F.col("min_visit_concept_order"))
|
|
360
|
+
.withColumn("concept_order", F.lit(0))
|
|
361
|
+
.withColumn("priority", F.lit(-1))
|
|
362
|
+
.drop("min_visit_concept_order", "max_visit_concept_order")
|
|
363
|
+
.drop("min_concept_order", "max_concept_order")
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
artificial_tokens = artificial_tokens.unionByName(visit_type_tokens)
|
|
367
|
+
|
|
368
|
+
artificial_tokens = artificial_tokens.drop("visit_end_date")
|
|
369
|
+
|
|
370
|
+
# Retrieving the events that are ONLY linked to inpatient visits
|
|
371
|
+
inpatient_visits = visit_occurrence.where(F.col("visit_concept_id").isin([9201, 262, 8971, 8920])).select(
|
|
372
|
+
"visit_occurrence_id", "visit_end_date", "cohort_member_id"
|
|
373
|
+
)
|
|
374
|
+
inpatient_events = patient_events.join(inpatient_visits, ["visit_occurrence_id", "cohort_member_id"])
|
|
375
|
+
|
|
376
|
+
# Fill in the visit_end_date if null (because some visits are still ongoing at the time of data extraction)
|
|
377
|
+
# Bound the event dates within visit_start_date and visit_end_date
|
|
378
|
+
# Generate a span rank to indicate the position of the group of events
|
|
379
|
+
# Update the priority for each span
|
|
380
|
+
inpatient_events = (
|
|
381
|
+
inpatient_events.withColumn(
|
|
382
|
+
"visit_end_date",
|
|
383
|
+
F.coalesce(
|
|
384
|
+
"visit_end_date",
|
|
385
|
+
F.max("date").over(W.partitionBy("cohort_member_id", "visit_occurrence_id")),
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
.withColumn(
|
|
389
|
+
"date",
|
|
390
|
+
F.when(F.col("date") < F.col("visit_start_date"), F.col("visit_start_date")).otherwise(
|
|
391
|
+
F.when(F.col("date") > F.col("visit_end_date"), F.col("visit_end_date")).otherwise(F.col("date"))
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
.withColumn("priority", F.col("priority") + F.col("concept_order") * 0.1)
|
|
395
|
+
.drop("visit_end_date")
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
discharge_events = (
|
|
399
|
+
visit_occurrence.where(F.col("visit_concept_id").isin([9201, 262, 8971, 8920]))
|
|
400
|
+
.withColumn(
|
|
401
|
+
"standard_concept_id",
|
|
402
|
+
F.coalesce(F.col("discharged_to_concept_id"), F.lit(0)),
|
|
403
|
+
)
|
|
404
|
+
.withColumn("visit_concept_order", F.col("max_visit_concept_order"))
|
|
405
|
+
.withColumn("concept_order", F.col("max_concept_order") + 1)
|
|
406
|
+
.withColumn("date", F.col("visit_end_date"))
|
|
407
|
+
.withColumn("datetime", F.date_add(F.to_timestamp("visit_end_date"), 1))
|
|
408
|
+
.withColumn("datetime", F.expr("datetime - INTERVAL 1 MINUTE"))
|
|
409
|
+
.withColumn("priority", F.lit(100))
|
|
410
|
+
.drop("discharged_to_concept_id", "visit_end_date")
|
|
411
|
+
.drop("min_visit_concept_order", "max_visit_concept_order")
|
|
412
|
+
.drop("min_concept_order", "max_concept_order")
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Add discharge events to the inpatient visits
|
|
416
|
+
inpatient_events = inpatient_events.unionByName(discharge_events)
|
|
417
|
+
|
|
418
|
+
# Get the prev days_since_epoch
|
|
419
|
+
inpatient_prev_date_udf = F.lag("date").over(
|
|
420
|
+
W.partitionBy("cohort_member_id", "visit_occurrence_id").orderBy("concept_order")
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Compute the time difference between the current record and the previous record
|
|
424
|
+
inpatient_time_delta_udf = F.when(F.col("prev_date").isNull(), 0).otherwise(F.datediff("date", "prev_date"))
|
|
425
|
+
|
|
426
|
+
if self._include_inpatient_hour_token:
|
|
427
|
+
# Create ATT tokens within the inpatient visits
|
|
428
|
+
inpatient_prev_datetime_udf = F.lag("datetime").over(
|
|
429
|
+
W.partitionBy("cohort_member_id", "visit_occurrence_id").orderBy("concept_order")
|
|
430
|
+
)
|
|
431
|
+
# Compute the time difference between the current record and the previous record
|
|
432
|
+
inpatient_hour_delta_udf = F.when(F.col("prev_datetime").isNull(), 0).otherwise(
|
|
433
|
+
F.floor((F.unix_timestamp("datetime") - F.unix_timestamp("prev_datetime")) / 3600)
|
|
434
|
+
)
|
|
435
|
+
inpatient_att_token = F.when(
|
|
436
|
+
F.col("hour_delta") < 24, F.concat(F.lit("i-H"), F.col("hour_delta"))
|
|
437
|
+
).otherwise(F.concat(F.lit("i-"), time_token_udf("time_delta")))
|
|
438
|
+
# Create ATT tokens within the inpatient visits
|
|
439
|
+
inpatient_att_events = (
|
|
440
|
+
inpatient_events.withColumn(
|
|
441
|
+
"is_span_boundary",
|
|
442
|
+
F.row_number().over(
|
|
443
|
+
W.partitionBy("cohort_member_id", "visit_occurrence_id", "concept_order").orderBy("priority")
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
.where(F.col("is_span_boundary") == 1)
|
|
447
|
+
.withColumn("prev_date", inpatient_prev_date_udf)
|
|
448
|
+
.withColumn("time_delta", inpatient_time_delta_udf)
|
|
449
|
+
.withColumn("prev_datetime", inpatient_prev_datetime_udf)
|
|
450
|
+
.withColumn("hour_delta", inpatient_hour_delta_udf)
|
|
451
|
+
.where(F.col("prev_date").isNotNull())
|
|
452
|
+
.where(F.col("hour_delta") > 0)
|
|
453
|
+
.withColumn("standard_concept_id", inpatient_att_token)
|
|
454
|
+
.withColumn("visit_concept_order", F.col("visit_concept_order"))
|
|
455
|
+
.withColumn("priority", F.col("priority") - 0.01)
|
|
456
|
+
.withColumn("concept_value_mask", F.lit(0))
|
|
457
|
+
.withColumn("concept_value", F.lit(0.0))
|
|
458
|
+
.drop("prev_date", "time_delta", "is_span_boundary")
|
|
459
|
+
.drop("prev_datetime", "hour_delta")
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
# Create ATT tokens within the inpatient visits
|
|
463
|
+
inpatient_att_events = (
|
|
464
|
+
inpatient_events.withColumn(
|
|
465
|
+
"is_span_boundary",
|
|
466
|
+
F.row_number().over(
|
|
467
|
+
W.partitionBy("cohort_member_id", "visit_occurrence_id", "concept_order").orderBy("priority")
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
.where(F.col("is_span_boundary") == 1)
|
|
471
|
+
.withColumn("prev_date", inpatient_prev_date_udf)
|
|
472
|
+
.withColumn("time_delta", inpatient_time_delta_udf)
|
|
473
|
+
.where(F.col("time_delta") != 0)
|
|
474
|
+
.where(F.col("prev_date").isNotNull())
|
|
475
|
+
.withColumn(
|
|
476
|
+
"standard_concept_id",
|
|
477
|
+
F.concat(F.lit("i-"), time_token_udf("time_delta")),
|
|
478
|
+
)
|
|
479
|
+
.withColumn("visit_concept_order", F.col("visit_concept_order"))
|
|
480
|
+
.withColumn("priority", F.col("priority") - 0.01)
|
|
481
|
+
.withColumn("concept_value_mask", F.lit(0))
|
|
482
|
+
.withColumn("concept_value", F.lit(0.0))
|
|
483
|
+
.drop("prev_date", "time_delta", "is_span_boundary")
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
self.validate(inpatient_events)
|
|
487
|
+
self.validate(inpatient_att_events)
|
|
488
|
+
|
|
489
|
+
# Retrieving the events that are NOT linked to inpatient visits
|
|
490
|
+
other_events = patient_events.join(
|
|
491
|
+
inpatient_visits.select("visit_occurrence_id", "cohort_member_id"),
|
|
492
|
+
["visit_occurrence_id", "cohort_member_id"],
|
|
493
|
+
how="left_anti",
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
patient_events = inpatient_events.unionByName(inpatient_att_events).unionByName(other_events)
|
|
497
|
+
|
|
498
|
+
self.validate(patient_events)
|
|
499
|
+
self.validate(artificial_tokens)
|
|
500
|
+
|
|
501
|
+
# artificial_tokens = artificial_tokens.select(sorted(artificial_tokens.columns))
|
|
502
|
+
return patient_events.unionByName(artificial_tokens)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class DemographicPromptDecorator(PatientEventDecorator):
|
|
506
|
+
def __init__(self, patient_demographic, use_age_group: bool = False):
|
|
507
|
+
self._patient_demographic = patient_demographic
|
|
508
|
+
self._use_age_group = use_age_group
|
|
509
|
+
|
|
510
|
+
def _decorate(self, patient_events: DataFrame):
|
|
511
|
+
if self._patient_demographic is None:
|
|
512
|
+
return patient_events
|
|
513
|
+
|
|
514
|
+
# set(['cohort_member_id', 'person_id', 'standard_concept_id', 'date',
|
|
515
|
+
# 'visit_occurrence_id', 'domain', 'concept_value', 'visit_rank_order',
|
|
516
|
+
# 'visit_segment', 'priority', 'date_in_week', 'concept_value_mask',
|
|
517
|
+
# 'mlm_skip_value', 'age', 'visit_concept_id'])
|
|
518
|
+
|
|
519
|
+
# Get the first token of the patient history
|
|
520
|
+
first_token_udf = F.row_number().over(
|
|
521
|
+
W.partitionBy("cohort_member_id", "person_id").orderBy(
|
|
522
|
+
"visit_start_datetime",
|
|
523
|
+
"visit_occurrence_id",
|
|
524
|
+
"priority",
|
|
525
|
+
"standard_concept_id",
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Identify the first token of each patient history
|
|
530
|
+
patient_first_token = (
|
|
531
|
+
patient_events.withColumn("token_order", first_token_udf)
|
|
532
|
+
.withColumn("concept_value_mask", F.lit(0))
|
|
533
|
+
.withColumn("concept_value", F.lit(0.0))
|
|
534
|
+
.where("token_order = 1")
|
|
535
|
+
.drop("token_order")
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Udf for identifying the earliest date associated with a visit_occurrence_id
|
|
539
|
+
sequence_start_year_token = (
|
|
540
|
+
patient_first_token.withColumn(
|
|
541
|
+
"standard_concept_id",
|
|
542
|
+
F.concat(F.lit("year:"), F.year("date").cast(T.StringType())),
|
|
543
|
+
)
|
|
544
|
+
.withColumn("priority", F.lit(-10))
|
|
545
|
+
.withColumn("visit_segment", F.lit(0))
|
|
546
|
+
.withColumn("date_in_week", F.lit(0))
|
|
547
|
+
.withColumn("age", F.lit(-1))
|
|
548
|
+
.withColumn("visit_rank_order", F.lit(0))
|
|
549
|
+
.withColumn("visit_concept_order", F.lit(0))
|
|
550
|
+
.withColumn("concept_order", F.lit(0))
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
sequence_start_year_token.cache()
|
|
554
|
+
|
|
555
|
+
if self._use_age_group:
|
|
556
|
+
calculate_age_group_at_first_visit_udf = F.ceil(
|
|
557
|
+
F.floor(F.months_between(F.col("date"), F.col("birth_datetime")) / F.lit(12) / 10)
|
|
558
|
+
)
|
|
559
|
+
age_at_first_visit_udf = F.concat(
|
|
560
|
+
F.lit("age:"),
|
|
561
|
+
(calculate_age_group_at_first_visit_udf * 10).cast(T.StringType()),
|
|
562
|
+
F.lit("-"),
|
|
563
|
+
((calculate_age_group_at_first_visit_udf + 1) * 10).cast(T.StringType()),
|
|
564
|
+
)
|
|
565
|
+
else:
|
|
566
|
+
calculate_age_at_first_visit_udf = F.ceil(
|
|
567
|
+
F.months_between(F.col("date"), F.col("birth_datetime")) / F.lit(12)
|
|
568
|
+
)
|
|
569
|
+
age_at_first_visit_udf = F.concat(F.lit("age:"), calculate_age_at_first_visit_udf.cast(T.StringType()))
|
|
570
|
+
|
|
571
|
+
sequence_age_token = (
|
|
572
|
+
self._patient_demographic.select(F.col("person_id"), F.col("birth_datetime"))
|
|
573
|
+
.join(sequence_start_year_token, "person_id")
|
|
574
|
+
.withColumn("standard_concept_id", age_at_first_visit_udf)
|
|
575
|
+
.withColumn("priority", F.lit(-9))
|
|
576
|
+
.drop("birth_datetime")
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
sequence_gender_token = (
|
|
580
|
+
self._patient_demographic.select(F.col("person_id"), F.col("gender_concept_id"))
|
|
581
|
+
.join(sequence_start_year_token, "person_id")
|
|
582
|
+
.withColumn("standard_concept_id", F.col("gender_concept_id").cast(T.StringType()))
|
|
583
|
+
.withColumn("priority", F.lit(-8))
|
|
584
|
+
.drop("gender_concept_id")
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
sequence_race_token = (
|
|
588
|
+
self._patient_demographic.select(F.col("person_id"), F.col("race_concept_id"))
|
|
589
|
+
.join(sequence_start_year_token, "person_id")
|
|
590
|
+
.withColumn("standard_concept_id", F.col("race_concept_id").cast(T.StringType()))
|
|
591
|
+
.withColumn("priority", F.lit(-7))
|
|
592
|
+
.drop("race_concept_id")
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
patient_events = patient_events.unionByName(sequence_start_year_token)
|
|
596
|
+
patient_events = patient_events.unionByName(sequence_age_token)
|
|
597
|
+
patient_events = patient_events.unionByName(sequence_gender_token)
|
|
598
|
+
patient_events = patient_events.unionByName(sequence_race_token)
|
|
599
|
+
|
|
600
|
+
return patient_events
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class DeathEventDecorator(PatientEventDecorator):
|
|
604
|
+
def __init__(self, death, att_type):
|
|
605
|
+
self._death = death
|
|
606
|
+
self._att_type = att_type
|
|
607
|
+
|
|
608
|
+
def _decorate(self, patient_events: DataFrame):
|
|
609
|
+
if self._death is None:
|
|
610
|
+
return patient_events
|
|
611
|
+
|
|
612
|
+
death_records = patient_events.join(self._death.select("person_id", "death_date"), "person_id")
|
|
613
|
+
|
|
614
|
+
max_visit_occurrence_id = death_records.select(F.max("visit_occurrence_id").alias("max_visit_occurrence_id"))
|
|
615
|
+
|
|
616
|
+
last_ve_record = (
|
|
617
|
+
death_records.where(F.col("standard_concept_id") == "VE")
|
|
618
|
+
.withColumn(
|
|
619
|
+
"record_rank",
|
|
620
|
+
F.row_number().over(W.partitionBy("person_id", "cohort_member_id").orderBy(F.desc("date"))),
|
|
621
|
+
)
|
|
622
|
+
.where(F.col("record_rank") == 1)
|
|
623
|
+
.drop("record_rank")
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
last_ve_record.cache()
|
|
627
|
+
last_ve_record.show()
|
|
628
|
+
# set(['cohort_member_id', 'person_id', 'standard_concept_id', 'date',
|
|
629
|
+
# 'visit_occurrence_id', 'domain', 'concept_value', 'visit_rank_order',
|
|
630
|
+
# 'visit_segment', 'priority', 'date_in_week', 'concept_value_mask',
|
|
631
|
+
# 'mlm_skip_value', 'age', 'visit_concept_id'])
|
|
632
|
+
|
|
633
|
+
artificial_visit_id = F.row_number().over(
|
|
634
|
+
W.partitionBy(F.lit(0)).orderBy("person_id", "cohort_member_id")
|
|
635
|
+
) + F.col("max_visit_occurrence_id")
|
|
636
|
+
death_records = (
|
|
637
|
+
last_ve_record.crossJoin(max_visit_occurrence_id)
|
|
638
|
+
.withColumn("visit_occurrence_id", artificial_visit_id)
|
|
639
|
+
.withColumn("standard_concept_id", F.lit("[DEATH]"))
|
|
640
|
+
.withColumn("domain", F.lit("death"))
|
|
641
|
+
.withColumn("visit_rank_order", F.lit(1) + F.col("visit_rank_order"))
|
|
642
|
+
.withColumn("priority", F.lit(20))
|
|
643
|
+
.drop("max_visit_occurrence_id")
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
vs_records = death_records.withColumn("standard_concept_id", F.lit("VS")).withColumn("priority", F.lit(15))
|
|
647
|
+
|
|
648
|
+
ve_records = death_records.withColumn("standard_concept_id", F.lit("VE")).withColumn("priority", F.lit(30))
|
|
649
|
+
|
|
650
|
+
# Udf for calculating the time token
|
|
651
|
+
if self._att_type == AttType.DAY:
|
|
652
|
+
att_func = time_day_token
|
|
653
|
+
elif self._att_type == AttType.WEEK:
|
|
654
|
+
att_func = time_week_token
|
|
655
|
+
elif self._att_type == AttType.MONTH:
|
|
656
|
+
att_func = time_month_token
|
|
657
|
+
elif self._att_type == AttType.MIX:
|
|
658
|
+
att_func = time_mix_token
|
|
659
|
+
else:
|
|
660
|
+
att_func = time_token_func
|
|
661
|
+
|
|
662
|
+
time_token_udf = F.udf(att_func, T.StringType())
|
|
663
|
+
|
|
664
|
+
att_records = death_records.withColumn(
|
|
665
|
+
"death_date",
|
|
666
|
+
F.when(F.col("death_date") < F.col("date"), F.col("date")).otherwise(F.col("death_date")),
|
|
667
|
+
)
|
|
668
|
+
att_records = (
|
|
669
|
+
att_records.withColumn("time_delta", F.datediff("death_date", "date"))
|
|
670
|
+
.withColumn("standard_concept_id", time_token_udf("time_delta"))
|
|
671
|
+
.withColumn("priority", F.lit(10))
|
|
672
|
+
.drop("time_delta")
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
new_tokens = att_records.unionByName(vs_records).unionByName(death_records).unionByName(ve_records)
|
|
676
|
+
new_tokens = new_tokens.drop("death_date")
|
|
677
|
+
self.validate(new_tokens)
|
|
678
|
+
|
|
679
|
+
return patient_events.unionByName(new_tokens)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def time_token_func(time_delta) -> Optional[str]:
|
|
683
|
+
if time_delta is None or np.isnan(time_delta):
|
|
684
|
+
return None
|
|
685
|
+
if time_delta < 0:
|
|
686
|
+
return "W-1"
|
|
687
|
+
if time_delta < 28:
|
|
688
|
+
return f"W{str(math.floor(time_delta / 7))}"
|
|
689
|
+
if time_delta < 360:
|
|
690
|
+
return f"M{str(math.floor(time_delta / 30))}"
|
|
691
|
+
return "LT"
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def time_day_token(time_delta):
|
|
695
|
+
if time_delta is None or np.isnan(time_delta):
|
|
696
|
+
return None
|
|
697
|
+
if time_delta < 1080:
|
|
698
|
+
return f"D{str(time_delta)}"
|
|
699
|
+
return "LT"
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def time_week_token(time_delta):
|
|
703
|
+
if time_delta is None or np.isnan(time_delta):
|
|
704
|
+
return None
|
|
705
|
+
if time_delta < 1080:
|
|
706
|
+
return f"W{str(math.floor(time_delta / 7))}"
|
|
707
|
+
return "LT"
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def time_month_token(time_delta):
|
|
711
|
+
if time_delta is None or np.isnan(time_delta):
|
|
712
|
+
return None
|
|
713
|
+
if time_delta < 1080:
|
|
714
|
+
return f"M{str(math.floor(time_delta / 30))}"
|
|
715
|
+
return "LT"
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def time_mix_token(time_delta):
|
|
719
|
+
# WHEN day_diff <= 7 THEN CONCAT('D', day_diff)
|
|
720
|
+
# WHEN day_diff <= 30 THEN CONCAT('W', ceil(day_diff / 7))
|
|
721
|
+
# WHEN day_diff <= 360 THEN CONCAT('M', ceil(day_diff / 30))
|
|
722
|
+
# WHEN day_diff <= 720 THEN CONCAT('Q', ceil(day_diff / 90))
|
|
723
|
+
# WHEN day_diff <= 1440 THEN CONCAT('Y', ceil(day_diff / 360))
|
|
724
|
+
# ELSE 'LT'
|
|
725
|
+
if time_delta is None or np.isnan(time_delta):
|
|
726
|
+
return None
|
|
727
|
+
if time_delta <= 7:
|
|
728
|
+
return f"D{str(time_delta)}"
|
|
729
|
+
if time_delta <= 30:
|
|
730
|
+
# e.g. 8 -> W2
|
|
731
|
+
return f"W{str(math.ceil(time_delta / 7))}"
|
|
732
|
+
if time_delta <= 360:
|
|
733
|
+
# e.g. 31 -> M2
|
|
734
|
+
return f"M{str(math.ceil(time_delta / 30))}"
|
|
735
|
+
# if time_delta <= 720:
|
|
736
|
+
# # e.g. 361 -> Q5
|
|
737
|
+
# return f'Q{str(math.ceil(time_delta / 90))}'
|
|
738
|
+
# if time_delta <= 1080:
|
|
739
|
+
# # e.g. 1081 -> Y2
|
|
740
|
+
# return f'Y{str(math.ceil(time_delta / 360))}'
|
|
741
|
+
return "LT"
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def get_att_function(att_type: Union[AttType, str]):
|
|
745
|
+
# Convert the att_type str to the corresponding enum type
|
|
746
|
+
if isinstance(att_type, str):
|
|
747
|
+
att_type = AttType(att_type)
|
|
748
|
+
|
|
749
|
+
if att_type == AttType.DAY:
|
|
750
|
+
return time_day_token
|
|
751
|
+
elif att_type == AttType.WEEK:
|
|
752
|
+
return time_week_token
|
|
753
|
+
elif att_type == AttType.MONTH:
|
|
754
|
+
return time_month_token
|
|
755
|
+
elif att_type == AttType.MIX:
|
|
756
|
+
return time_mix_token
|
|
757
|
+
elif att_type == AttType.CEHR_BERT:
|
|
758
|
+
return time_token_func
|
|
759
|
+
return None
|