mostlyai-engine 1.0.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.
- mostlyai/engine/__init__.py +27 -0
- mostlyai/engine/_common.py +612 -0
- mostlyai/engine/_dtypes.py +58 -0
- mostlyai/engine/_encoding_types/__init__.py +13 -0
- mostlyai/engine/_encoding_types/language/__init__.py +13 -0
- mostlyai/engine/_encoding_types/language/text.py +41 -0
- mostlyai/engine/_encoding_types/tabular/__init__.py +13 -0
- mostlyai/engine/_encoding_types/tabular/categorical.py +115 -0
- mostlyai/engine/_encoding_types/tabular/character.py +155 -0
- mostlyai/engine/_encoding_types/tabular/datetime.py +254 -0
- mostlyai/engine/_encoding_types/tabular/itt.py +352 -0
- mostlyai/engine/_encoding_types/tabular/lat_long.py +353 -0
- mostlyai/engine/_encoding_types/tabular/numeric.py +548 -0
- mostlyai/engine/_language/__init__.py +17 -0
- mostlyai/engine/_language/common.py +110 -0
- mostlyai/engine/_language/encoding.py +162 -0
- mostlyai/engine/_language/engine/__init__.py +13 -0
- mostlyai/engine/_language/engine/base.py +51 -0
- mostlyai/engine/_language/engine/hf_engine.py +136 -0
- mostlyai/engine/_language/engine/vllm_engine.py +187 -0
- mostlyai/engine/_language/formatron_utils.py +114 -0
- mostlyai/engine/_language/generation.py +309 -0
- mostlyai/engine/_language/lstm.py +139 -0
- mostlyai/engine/_language/tokenizer_utils.py +144 -0
- mostlyai/engine/_language/training.py +718 -0
- mostlyai/engine/_memory.py +69 -0
- mostlyai/engine/_tabular/__init__.py +13 -0
- mostlyai/engine/_tabular/argn.py +1382 -0
- mostlyai/engine/_tabular/common.py +29 -0
- mostlyai/engine/_tabular/encoding.py +406 -0
- mostlyai/engine/_tabular/fairness.py +245 -0
- mostlyai/engine/_tabular/generation.py +1157 -0
- mostlyai/engine/_tabular/training.py +856 -0
- mostlyai/engine/_training_utils.py +159 -0
- mostlyai/engine/_workspace.py +225 -0
- mostlyai/engine/analysis.py +571 -0
- mostlyai/engine/domain.py +171 -0
- mostlyai/engine/encoding.py +42 -0
- mostlyai/engine/generation.py +110 -0
- mostlyai/engine/splitting.py +257 -0
- mostlyai/engine/training.py +104 -0
- mostlyai_engine-1.0.0.dist-info/METADATA +179 -0
- mostlyai_engine-1.0.0.dist-info/RECORD +45 -0
- mostlyai_engine-1.0.0.dist-info/WHEEL +4 -0
- mostlyai_engine-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Copyright 2025 MOSTLY AI
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
ITT encoding is only applicable for sequential data, with a context_key being present. For each context_key, the
|
|
17
|
+
datetime values are transformed into ITTs, and the first non-missing date is determined. That start_date is then split
|
|
18
|
+
into its datetime parts, and the ITTs are split into its own separate components. For each part its
|
|
19
|
+
minimum and maximum values are determined. Any part is then encoded as `x - min_value`, resulting in integers
|
|
20
|
+
ranging from 0 to `max_value - min_value`. Thus, the corresponding cardinality is `max_value + 1 - min_value`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import calendar
|
|
24
|
+
from datetime import timedelta
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
import pandas as pd
|
|
28
|
+
|
|
29
|
+
from mostlyai.engine._common import safe_convert_datetime
|
|
30
|
+
from mostlyai.engine._dtypes import is_date_dtype, is_timestamp_dtype
|
|
31
|
+
from mostlyai.engine._encoding_types.tabular.datetime import split_sub_columns_datetime
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def analyze_itt(
|
|
35
|
+
values: pd.Series,
|
|
36
|
+
root_keys: pd.Series,
|
|
37
|
+
context_keys: pd.Series,
|
|
38
|
+
) -> dict:
|
|
39
|
+
values = safe_convert_datetime(values)
|
|
40
|
+
df = pd.concat([root_keys, context_keys, values], axis=1)
|
|
41
|
+
# calculate min/max values for start dates
|
|
42
|
+
start_dates = df.dropna().groupby(root_keys.name)[values.name].nth(0)
|
|
43
|
+
min11 = start_dates.sort_values(ascending=True).head(11).astype(str).tolist()
|
|
44
|
+
start_dates = df.dropna().groupby(root_keys.name)[values.name].nth(0)
|
|
45
|
+
max11 = start_dates.sort_values(ascending=False).head(11).astype(str).tolist()
|
|
46
|
+
# split into datetime/ITT parts
|
|
47
|
+
df_split = split_sub_columns_itt(values, context_keys)
|
|
48
|
+
is_not_nan = df_split["nan"] == 0
|
|
49
|
+
has_nan = any(df_split["nan"] == 1)
|
|
50
|
+
has_neg = sum(df_split["itt_neg"]) > 0
|
|
51
|
+
# extract min/max value for each part to determine valid value range;
|
|
52
|
+
# for ITT parts we need to discard first entry per sequence as these have dummy `0` inserted
|
|
53
|
+
keys = [k for k in df_split if k not in ["nan", "itt_neg"]]
|
|
54
|
+
keys_start = [k for k in keys if k.startswith("start_")]
|
|
55
|
+
keys_itt = [k for k in keys if k.startswith("itt_")]
|
|
56
|
+
if any(is_not_nan):
|
|
57
|
+
# extract min/max for start_date parts
|
|
58
|
+
min_values_start = {k: int(df_split[k][is_not_nan].min()) for k in keys_start}
|
|
59
|
+
max_values_start = {k: int(df_split[k][is_not_nan].max()) for k in keys_start}
|
|
60
|
+
# extract min/max for ITT parts
|
|
61
|
+
df_split = pd.concat([context_keys, df_split], axis=1)
|
|
62
|
+
df_split = df_split.loc[df_split.groupby(context_keys.name).cumcount() > 0, :]
|
|
63
|
+
if not df_split.empty:
|
|
64
|
+
min_values_itt = {k: int(df_split[k][is_not_nan].min()) for k in keys_itt}
|
|
65
|
+
max_values_itt = {k: int(df_split[k][is_not_nan].max()) for k in keys_itt}
|
|
66
|
+
else:
|
|
67
|
+
min_values_itt = {k: 0 for k in keys_itt}
|
|
68
|
+
max_values_itt = {k: 0 for k in keys_itt}
|
|
69
|
+
# merge together
|
|
70
|
+
min_values = min_values_start | min_values_itt
|
|
71
|
+
max_values = max_values_start | max_values_itt
|
|
72
|
+
else:
|
|
73
|
+
def_values = {"start_year": 2022, "start_month": 1, "start_day": 1}
|
|
74
|
+
min_values = {k: 0 for k in keys} | def_values
|
|
75
|
+
max_values = {k: 0 for k in keys} | def_values
|
|
76
|
+
# return stats
|
|
77
|
+
stats = {
|
|
78
|
+
"has_nan": has_nan,
|
|
79
|
+
"has_neg": has_neg,
|
|
80
|
+
"min_values": min_values,
|
|
81
|
+
"max_values": max_values,
|
|
82
|
+
"min11": min11,
|
|
83
|
+
"max11": max11,
|
|
84
|
+
}
|
|
85
|
+
return stats
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def analyze_reduce_itt(stats_list: list[dict], value_protection: bool = True) -> dict:
|
|
89
|
+
# check if there are missing values
|
|
90
|
+
has_nan = any([j["has_nan"] for j in stats_list])
|
|
91
|
+
# check if there are negative values
|
|
92
|
+
has_neg = any([j["has_neg"] for j in stats_list])
|
|
93
|
+
# determine min/max values for each part
|
|
94
|
+
keys = stats_list[0]["min_values"].keys()
|
|
95
|
+
min_values = {k: min([j["min_values"][k] for j in stats_list]) for k in keys}
|
|
96
|
+
max_values = {k: max([j["max_values"][k] for j in stats_list]) for k in keys}
|
|
97
|
+
# check if any record has non-zero timestamp information
|
|
98
|
+
has_time = max_values["start_hour"] > 0 or max_values["start_minute"] > 0 or max_values["start_second"] > 0
|
|
99
|
+
# determine min / max 5 values to map too low / too high values to
|
|
100
|
+
min11 = sorted([v for min11 in [j["min11"] for j in stats_list] for v in min11], reverse=False)[:11]
|
|
101
|
+
max11 = sorted([v for max11 in [j["max11"] for j in stats_list] for v in max11], reverse=True)[:11]
|
|
102
|
+
if value_protection:
|
|
103
|
+
# extreme value protection - discard lowest/highest 5 values
|
|
104
|
+
if len(min11) < 11 or len(max11) < 11:
|
|
105
|
+
# less than 11 subjects with non-NULL values; we need to protect all
|
|
106
|
+
min5 = []
|
|
107
|
+
max5 = []
|
|
108
|
+
has_time = False
|
|
109
|
+
else:
|
|
110
|
+
min5 = [str(v) for v in min11[5:10]] # drop 1 to 5th lowest; keep 6th to 10th lowest
|
|
111
|
+
max5 = [str(v) for v in max11[5:10]] # drop 1 to 5th highest; keep 6th to 10th highest
|
|
112
|
+
# update min/max year based on first four letters of protected min/max dates
|
|
113
|
+
max_values["start_year"] = int(max5[0][0:4])
|
|
114
|
+
min_values["start_year"] = int(min5[0][0:4])
|
|
115
|
+
else:
|
|
116
|
+
min5 = min11[0:4]
|
|
117
|
+
max5 = max11[0:4]
|
|
118
|
+
|
|
119
|
+
# determine cardinalities
|
|
120
|
+
cardinalities = {}
|
|
121
|
+
if has_nan:
|
|
122
|
+
cardinalities["nan"] = 2 # binary
|
|
123
|
+
# start date
|
|
124
|
+
cardinalities["start_year"] = max_values["start_year"] + 1 - min_values["start_year"]
|
|
125
|
+
cardinalities["start_month"] = max_values["start_month"] + 1 - min_values["start_month"]
|
|
126
|
+
cardinalities["start_day"] = max_values["start_day"] + 1 - min_values["start_day"]
|
|
127
|
+
if has_time:
|
|
128
|
+
cardinalities["start_hour"] = max_values["start_hour"] + 1 - min_values["start_hour"]
|
|
129
|
+
cardinalities["start_minute"] = max_values["start_minute"] + 1 - min_values["start_minute"]
|
|
130
|
+
cardinalities["start_second"] = max_values["start_second"] + 1 - min_values["start_second"]
|
|
131
|
+
# ITT
|
|
132
|
+
if has_neg:
|
|
133
|
+
cardinalities["itt_neg"] = 2
|
|
134
|
+
cardinalities["itt_week"] = max_values["itt_week"] + 1 - min_values["itt_week"]
|
|
135
|
+
cardinalities["itt_day"] = max_values["itt_day"] + 1 - min_values["itt_day"]
|
|
136
|
+
if has_time:
|
|
137
|
+
cardinalities["itt_hour"] = max_values["itt_hour"] + 1 - min_values["itt_hour"]
|
|
138
|
+
cardinalities["itt_minute"] = max_values["itt_minute"] + 1 - min_values["itt_minute"]
|
|
139
|
+
cardinalities["itt_second"] = max_values["itt_second"] + 1 - min_values["itt_second"]
|
|
140
|
+
|
|
141
|
+
stats = {
|
|
142
|
+
"cardinalities": cardinalities,
|
|
143
|
+
"has_nan": has_nan,
|
|
144
|
+
"has_neg": has_neg,
|
|
145
|
+
"has_time": has_time,
|
|
146
|
+
"min_values": min_values,
|
|
147
|
+
"max_values": max_values,
|
|
148
|
+
"min5": min5,
|
|
149
|
+
"max5": max5,
|
|
150
|
+
}
|
|
151
|
+
return stats
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def encode_itt(
|
|
155
|
+
values: pd.Series,
|
|
156
|
+
stats: dict,
|
|
157
|
+
context_keys: pd.Series,
|
|
158
|
+
) -> pd.DataFrame:
|
|
159
|
+
# convert
|
|
160
|
+
values = safe_convert_datetime(values)
|
|
161
|
+
# split to sub_columns
|
|
162
|
+
df = split_sub_columns_itt(values, context_keys)
|
|
163
|
+
# encode values so that each datetime part ranges from 0 to `max_value-min_value`
|
|
164
|
+
start_parts = [k for k in df.columns if k.startswith("start_")]
|
|
165
|
+
itt_parts = [k for k in df.columns if k.startswith("itt_") if k != "itt_neg"]
|
|
166
|
+
for key in start_parts + itt_parts:
|
|
167
|
+
# subtract minimum value
|
|
168
|
+
df[key] = df[key] - stats["min_values"][key]
|
|
169
|
+
# ensure that any value is mapped onto valid value range
|
|
170
|
+
df[key] = np.minimum(df[key], stats["max_values"][key] - stats["min_values"][key])
|
|
171
|
+
df[key] = np.maximum(df[key], 0)
|
|
172
|
+
# remove unused columns
|
|
173
|
+
if not stats["has_nan"]:
|
|
174
|
+
df.drop(["nan"], inplace=True, axis=1)
|
|
175
|
+
if not stats["has_time"]:
|
|
176
|
+
df.drop(["start_hour", "start_minute", "start_second"], inplace=True, axis=1)
|
|
177
|
+
df.drop(["itt_hour", "itt_minute", "itt_second"], inplace=True, axis=1)
|
|
178
|
+
if not stats["has_neg"]:
|
|
179
|
+
df.drop(["itt_neg"], inplace=True, axis=1)
|
|
180
|
+
return df
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def split_sub_columns_itt(
|
|
184
|
+
values: pd.Series,
|
|
185
|
+
context_keys: pd.Series,
|
|
186
|
+
) -> pd.DataFrame:
|
|
187
|
+
if not is_date_dtype(values) and not is_timestamp_dtype(values):
|
|
188
|
+
raise ValueError("expected to be datetime")
|
|
189
|
+
values = values.astype("datetime64[us]")
|
|
190
|
+
|
|
191
|
+
sub_columns = {
|
|
192
|
+
"nan": values.isna(),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# fill NAs with valid dates; use minimum date as fallback, if a group has no dates at all;
|
|
196
|
+
df = pd.concat([values, context_keys], axis=1)
|
|
197
|
+
df[values.name] = df.groupby(df[context_keys.name])[values.name].ffill().bfill().fillna(values.dropna().min())
|
|
198
|
+
|
|
199
|
+
# first convert itt column to datetime delta in [ms] stored as float64
|
|
200
|
+
if not df.empty:
|
|
201
|
+
itt_in_ms = df.groupby(context_keys.name)[values.name].diff().astype("timedelta64[ms]").fillna(timedelta(0))
|
|
202
|
+
else: # df.empty is True
|
|
203
|
+
itt_in_ms = pd.Series(name=values.name, dtype="float64")
|
|
204
|
+
|
|
205
|
+
itt_sub_columns = {
|
|
206
|
+
"itt_neg": itt_in_ms < timedelta(0),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# convert positive number the negative raw differences to get the right differences
|
|
210
|
+
itt_in_ms = np.abs(itt_in_ms)
|
|
211
|
+
|
|
212
|
+
# calculate the ITT parts
|
|
213
|
+
s_in_min = 60
|
|
214
|
+
s_in_hour = 60 * 60
|
|
215
|
+
s_in_day = 60 * 60 * 24
|
|
216
|
+
s_in_week = 60 * 60 * 24 * 7
|
|
217
|
+
itt_sub_columns["itt_week"] = (itt_in_ms / 1000) / s_in_week
|
|
218
|
+
itt_sub_columns["itt_day"] = ((itt_in_ms / 1000) % s_in_week) / s_in_day
|
|
219
|
+
itt_sub_columns["itt_hour"] = ((itt_in_ms / 1000) % s_in_day) / s_in_hour
|
|
220
|
+
itt_sub_columns["itt_minute"] = ((itt_in_ms / 1000) % s_in_hour) / s_in_min
|
|
221
|
+
itt_sub_columns["itt_second"] = (itt_in_ms / 1000) % s_in_min
|
|
222
|
+
|
|
223
|
+
# copy the first non-null date as `start` to all events within the sequence
|
|
224
|
+
if any(df[values.name].notna()):
|
|
225
|
+
df[values.name] = df.groupby(df[context_keys.name])[values.name].transform(
|
|
226
|
+
lambda series: series.loc[series.first_valid_index()]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# extract datetime parts from start_date
|
|
230
|
+
date_sub_columns_df_split = split_sub_columns_datetime(df[values.name])
|
|
231
|
+
date_sub_columns = {
|
|
232
|
+
"start_year": date_sub_columns_df_split["year"],
|
|
233
|
+
"start_month": date_sub_columns_df_split["month"],
|
|
234
|
+
"start_day": date_sub_columns_df_split["day"],
|
|
235
|
+
"start_hour": date_sub_columns_df_split["hour"],
|
|
236
|
+
"start_minute": date_sub_columns_df_split["minute"],
|
|
237
|
+
"start_second": date_sub_columns_df_split["second"],
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
df = pd.DataFrame(sub_columns | date_sub_columns | itt_sub_columns)
|
|
241
|
+
df = df.fillna(0)
|
|
242
|
+
df = df.astype("int")
|
|
243
|
+
return df
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def decode_itt(
|
|
247
|
+
df_encoded: pd.DataFrame,
|
|
248
|
+
stats: dict,
|
|
249
|
+
context_keys: pd.Series,
|
|
250
|
+
prev_steps: dict | None = None,
|
|
251
|
+
):
|
|
252
|
+
min_values = stats["min_values"]
|
|
253
|
+
prev_dts = (prev_steps or {}).get("prev_dts")
|
|
254
|
+
|
|
255
|
+
## decode `start` parts
|
|
256
|
+
def decode_initial_starts():
|
|
257
|
+
# decode y/m/d components
|
|
258
|
+
y = df_encoded["start_year"] + min_values["start_year"]
|
|
259
|
+
m = df_encoded["start_month"] + min_values["start_month"]
|
|
260
|
+
d = df_encoded["start_day"] + min_values["start_day"]
|
|
261
|
+
# fix invalid dates by setting these to last day of month
|
|
262
|
+
is_leap = y.apply(lambda x: calendar.isleap(x))
|
|
263
|
+
d[is_leap & (m == 2) & (d > 29)] = 29
|
|
264
|
+
d[~is_leap & (m == 2) & (d > 28)] = 28
|
|
265
|
+
d[((m == 4) | (m == 6) | (m == 9) | (m == 11)) & (d > 30)] = 30
|
|
266
|
+
# concatenate to datetime string
|
|
267
|
+
y = y.astype(str)
|
|
268
|
+
m = m.astype(str).str.zfill(2)
|
|
269
|
+
d = d.astype(str).str.zfill(2)
|
|
270
|
+
starts = y + "-" + m + "-" + d
|
|
271
|
+
if stats["has_time"]:
|
|
272
|
+
hh = (df_encoded["start_hour"] + min_values["start_hour"]).astype(str).str.zfill(2)
|
|
273
|
+
mm = (df_encoded["start_minute"] + min_values["start_minute"]).astype(str).str.zfill(2)
|
|
274
|
+
ss = (df_encoded["start_second"] + min_values["start_second"]).astype(str).str.zfill(2)
|
|
275
|
+
starts = starts + " " + hh + ":" + mm + ":" + ss
|
|
276
|
+
# propagate the first non-null date per group to all events within that group
|
|
277
|
+
starts.name = "__STARTS"
|
|
278
|
+
starts = starts.loc[
|
|
279
|
+
pd.concat([context_keys, starts], axis=1)
|
|
280
|
+
.groupby(context_keys.name)[starts.name]
|
|
281
|
+
.transform(pd.Series.first_valid_index)
|
|
282
|
+
]
|
|
283
|
+
starts = starts.reset_index(drop=True)
|
|
284
|
+
starts = safe_convert_datetime(starts)
|
|
285
|
+
# clip start values to privacy-safe value range;
|
|
286
|
+
# note, that final date values may fall out of original date range, as we prioritize retaining ITT properties
|
|
287
|
+
if len(stats["min5"]) > 0 and len(stats["max5"]) > 0:
|
|
288
|
+
min5 = stats["min5"]
|
|
289
|
+
max5 = stats["max5"]
|
|
290
|
+
starts.loc[starts > max5[0]] = max5[0]
|
|
291
|
+
starts.loc[starts < min5[0]] = min5[0]
|
|
292
|
+
return starts
|
|
293
|
+
|
|
294
|
+
def continue_starts():
|
|
295
|
+
# select start for each context key
|
|
296
|
+
starts = pd.merge(context_keys, prev_dts, left_on=context_keys.name, right_on="__CONTEXT_KEYS", how="left")
|
|
297
|
+
starts = starts.reset_index(drop=True)
|
|
298
|
+
starts = starts["__STARTS"]
|
|
299
|
+
return starts
|
|
300
|
+
|
|
301
|
+
starts = decode_initial_starts() if prev_dts is None else continue_starts()
|
|
302
|
+
|
|
303
|
+
## decode `itt` parts
|
|
304
|
+
def decode_itts():
|
|
305
|
+
itts = pd.Series(np.repeat(0, df_encoded.shape[0]), dtype="int")
|
|
306
|
+
itts = itts + (df_encoded["itt_week"] + min_values["itt_week"]) * 60 * 60 * 24 * 7
|
|
307
|
+
itts = itts + (df_encoded["itt_day"] + min_values["itt_day"]) * 60 * 60 * 24
|
|
308
|
+
if stats["has_time"]:
|
|
309
|
+
itts = itts + (df_encoded["itt_hour"] + min_values["itt_hour"]) * 60 * 60
|
|
310
|
+
itts = itts + (df_encoded["itt_minute"] + min_values["itt_minute"]) * 60
|
|
311
|
+
itts = itts + (df_encoded["itt_second"] + min_values["itt_second"])
|
|
312
|
+
if stats["has_neg"]:
|
|
313
|
+
itts = itts * pd.Series(np.where(df_encoded["itt_neg"] == 1, -1, 1))
|
|
314
|
+
itts.name = "__ITTS"
|
|
315
|
+
return itts
|
|
316
|
+
|
|
317
|
+
itts = decode_itts()
|
|
318
|
+
itts_df = pd.concat([context_keys, itts], axis=1)
|
|
319
|
+
if prev_dts is None:
|
|
320
|
+
# we cumulate ITTs for each context key, to then add these to start_date;
|
|
321
|
+
# by definition, the first event of each sequence represents start_date, thus we set ITT = 0
|
|
322
|
+
itts_df["__ITTS"] = itts_df["__ITTS"].mask(itts_df.groupby(context_keys.name).cumcount() == 0, 0)
|
|
323
|
+
itts = itts_df.groupby(context_keys.name)["__ITTS"].cumsum()
|
|
324
|
+
|
|
325
|
+
# add cumulative ITTs to start_dates
|
|
326
|
+
values = starts.astype(np.int64) // 10**3 + itts * 10**3
|
|
327
|
+
values = np.clip(
|
|
328
|
+
a=values,
|
|
329
|
+
a_min=pd.Timestamp.min.value // 10**6 + 1,
|
|
330
|
+
a_max=pd.Timestamp.max.value // 10**6,
|
|
331
|
+
)
|
|
332
|
+
values = pd.to_datetime(values, unit="ms")
|
|
333
|
+
|
|
334
|
+
# convert to datetime
|
|
335
|
+
values = safe_convert_datetime(values, date_only=not stats["has_time"])
|
|
336
|
+
|
|
337
|
+
# keep track of last decoded values for next iteration
|
|
338
|
+
if prev_steps is not None:
|
|
339
|
+
prev_steps["prev_dts"] = (
|
|
340
|
+
pd.concat(
|
|
341
|
+
[context_keys.rename("__CONTEXT_KEYS"), values.rename("__STARTS")],
|
|
342
|
+
axis=1,
|
|
343
|
+
)
|
|
344
|
+
.groupby("__CONTEXT_KEYS")
|
|
345
|
+
.last()
|
|
346
|
+
.reset_index()
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# add nan values
|
|
350
|
+
if "nan" in df_encoded:
|
|
351
|
+
values[df_encoded["nan"] == 1] = pd.NA
|
|
352
|
+
return values
|