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.
Files changed (45) hide show
  1. mostlyai/engine/__init__.py +27 -0
  2. mostlyai/engine/_common.py +612 -0
  3. mostlyai/engine/_dtypes.py +58 -0
  4. mostlyai/engine/_encoding_types/__init__.py +13 -0
  5. mostlyai/engine/_encoding_types/language/__init__.py +13 -0
  6. mostlyai/engine/_encoding_types/language/text.py +41 -0
  7. mostlyai/engine/_encoding_types/tabular/__init__.py +13 -0
  8. mostlyai/engine/_encoding_types/tabular/categorical.py +115 -0
  9. mostlyai/engine/_encoding_types/tabular/character.py +155 -0
  10. mostlyai/engine/_encoding_types/tabular/datetime.py +254 -0
  11. mostlyai/engine/_encoding_types/tabular/itt.py +352 -0
  12. mostlyai/engine/_encoding_types/tabular/lat_long.py +353 -0
  13. mostlyai/engine/_encoding_types/tabular/numeric.py +548 -0
  14. mostlyai/engine/_language/__init__.py +17 -0
  15. mostlyai/engine/_language/common.py +110 -0
  16. mostlyai/engine/_language/encoding.py +162 -0
  17. mostlyai/engine/_language/engine/__init__.py +13 -0
  18. mostlyai/engine/_language/engine/base.py +51 -0
  19. mostlyai/engine/_language/engine/hf_engine.py +136 -0
  20. mostlyai/engine/_language/engine/vllm_engine.py +187 -0
  21. mostlyai/engine/_language/formatron_utils.py +114 -0
  22. mostlyai/engine/_language/generation.py +309 -0
  23. mostlyai/engine/_language/lstm.py +139 -0
  24. mostlyai/engine/_language/tokenizer_utils.py +144 -0
  25. mostlyai/engine/_language/training.py +718 -0
  26. mostlyai/engine/_memory.py +69 -0
  27. mostlyai/engine/_tabular/__init__.py +13 -0
  28. mostlyai/engine/_tabular/argn.py +1382 -0
  29. mostlyai/engine/_tabular/common.py +29 -0
  30. mostlyai/engine/_tabular/encoding.py +406 -0
  31. mostlyai/engine/_tabular/fairness.py +245 -0
  32. mostlyai/engine/_tabular/generation.py +1157 -0
  33. mostlyai/engine/_tabular/training.py +856 -0
  34. mostlyai/engine/_training_utils.py +159 -0
  35. mostlyai/engine/_workspace.py +225 -0
  36. mostlyai/engine/analysis.py +571 -0
  37. mostlyai/engine/domain.py +171 -0
  38. mostlyai/engine/encoding.py +42 -0
  39. mostlyai/engine/generation.py +110 -0
  40. mostlyai/engine/splitting.py +257 -0
  41. mostlyai/engine/training.py +104 -0
  42. mostlyai_engine-1.0.0.dist-info/METADATA +179 -0
  43. mostlyai_engine-1.0.0.dist-info/RECORD +45 -0
  44. mostlyai_engine-1.0.0.dist-info/WHEEL +4 -0
  45. 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