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,353 @@
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
+ from enum import Enum
16
+ from itertools import chain
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+ from numpy.typing import NDArray
21
+
22
+ from mostlyai.engine._common import safe_convert_string
23
+ from mostlyai.engine._encoding_types.tabular.categorical import (
24
+ CATEGORICAL_UNKNOWN_TOKEN,
25
+ encode_categorical,
26
+ )
27
+ from mostlyai.engine._encoding_types.tabular.character import (
28
+ decode_character,
29
+ encode_character,
30
+ )
31
+
32
+ MAX_UNIQUE_VALUES_PER_QUAD = 10_000
33
+ RARE_CATEGORY_THRESHOLD = 20
34
+ MIN_UNIQUE_QUAD_VALUES = 5
35
+
36
+
37
+ class GeoPositionSymbols(Enum):
38
+ A = ("0", "0") # NORTH-WEST
39
+ B = ("1", "0") # NORTH-EAST
40
+ C = ("0", "1") # SOUTH-WEST
41
+ D = ("1", "1") # SOUTH-EAST
42
+
43
+
44
+ GeoPositionSymbolsReverseLookup = {s.value: s.name for s in GeoPositionSymbols}
45
+ GEOPOSITION_PRECISION = 100_000
46
+ GEOPOSITION_BINARY_REPRESENTATION_LENGTH = 28
47
+ GEOPOSITION_MAX_LATITUDE = 90 * GEOPOSITION_PRECISION
48
+ GEOPOSITION_MAX_LONGITUDE = 180 * GEOPOSITION_PRECISION
49
+ GEOPOSITION_STEP_SIZE = 2
50
+ GEOPOSITION_SIGNS_LENGTH = 2
51
+ NUMBER_OF_GEOPOSITION_QUADS = 8
52
+ GEOPOSITION_FIRST_QUAD_LENGTH = 12
53
+ GEOPOSITION_FIRST_QUAD_ONSET = GEOPOSITION_FIRST_QUAD_LENGTH + GEOPOSITION_SIGNS_LENGTH
54
+ GEOPOSITION_QUADTILE_LENGTH = GEOPOSITION_FIRST_QUAD_ONSET + NUMBER_OF_GEOPOSITION_QUADS * GEOPOSITION_STEP_SIZE
55
+ NONE_QUADTILE_ENTRY = "++AAAAAAAAAAAAAAAAAAAAAAAAAAAA"
56
+ PLACEHOLDER_QUADTILE_ENTRY = "++AAAAAAAAAAAAAAAAAAAAAAAAAAAA"
57
+ GEOPOSITION_OUTPUT_FORMAT = "{latitude:.5f}, {longitude:.5f}"
58
+ QUADTILE = "QUADTILE"
59
+ QUAD_COLUMNS = [
60
+ f"QUAD{GEOPOSITION_FIRST_QUAD_LENGTH + GEOPOSITION_STEP_SIZE * i}" for i in range(NUMBER_OF_GEOPOSITION_QUADS)
61
+ ]
62
+
63
+
64
+ def _latlong_int_to_binary(latlong: NDArray) -> NDArray:
65
+ """
66
+ Transform an array of int64 tuples representing latlong (with 10^-5 precision) to
67
+ an array of quadtile strings (28 characters for each). Note: excluding the signs.
68
+
69
+ For example: [[3306060 3300000], [3312120 3300000]] ->
70
+ ['AAAAAADDAADAADBDCADACBCABBAA' 'AAAAAADDAADABCACDACBDBDBBAAA']
71
+
72
+ :param latlong: Numpy array of (lat, long) int64 tuples (non-negative numbers)
73
+ :return: Numpy array of object (quadtile strings)
74
+ """
75
+ str_repr = np.vectorize(np.binary_repr, otypes=[str])(
76
+ latlong[:, :],
77
+ width=GEOPOSITION_BINARY_REPRESENTATION_LENGTH,
78
+ )
79
+
80
+ def _latlong_to_binary_single(latitude: str, longitude: str) -> str:
81
+ """
82
+ Transform latitude and longitude (in binary string format) to quadtile
83
+ """
84
+ quad = [GeoPositionSymbolsReverseLookup[(lat, long)] for lat, long in zip(list(latitude), list(longitude))]
85
+ return "".join(quad)
86
+
87
+ quads_transform = np.frompyfunc(_latlong_to_binary_single, nin=2, nout=1)
88
+ quads = quads_transform(str_repr[:, 0], str_repr[:, 1])
89
+
90
+ return quads
91
+
92
+
93
+ def _latlong_to_binary(geo_positions: NDArray) -> list[str]:
94
+ """
95
+ Transform geoposition values (latitude and longitude) into quadtile string representations.
96
+
97
+ For example: [[33.0606 33. ], [33.12121 33. ]] ->
98
+ ['++AAAAAADDAADAADBDCADACBCABBAA' '++AAAAAADDAADABCACDACBDBDBBAAA']
99
+
100
+ :param geo_positions: Numpy array of (lat, long) tuples
101
+ :return:
102
+ """
103
+ # operate on integer values to get rid of the values below certain precision
104
+ numeric = (geo_positions * GEOPOSITION_PRECISION).astype(int)
105
+
106
+ # get the sign and work on the absolute values
107
+ signs = np.array(["-", "+"])[(numeric >= 0).astype(int)]
108
+ numeric = np.abs(numeric)
109
+
110
+ # apply ceiling to the values
111
+ numeric[numeric[:, 0] > GEOPOSITION_MAX_LATITUDE, 0] = GEOPOSITION_MAX_LATITUDE
112
+ numeric[numeric[:, 1] > GEOPOSITION_MAX_LONGITUDE, 1] = GEOPOSITION_MAX_LONGITUDE
113
+
114
+ # transform numeric to binary (quadtile strings)
115
+ quads = _latlong_int_to_binary(numeric)
116
+
117
+ # build final strings, adding signs
118
+ combine_strings = np.frompyfunc(lambda sign1, sign2, quad: sign1 + sign2 + quad, 3, 1)
119
+ quadtiles = combine_strings(signs[:, 0], signs[:, 1], quads)
120
+
121
+ return quadtiles
122
+
123
+
124
+ def _binary_to_latlong(data: pd.Series) -> pd.Series:
125
+ """
126
+ Transform quadtile column into latlong format.
127
+ For example ("-+AAACBAACBACBCBCBCBAACBAAAAAA") -> ("-90, 180")
128
+
129
+ :param data: Quadtile as pandas series
130
+ """
131
+
132
+ def _binary_to_latlong_single(
133
+ quadtile: str,
134
+ ) -> tuple[float | None, float | None]:
135
+ """
136
+ Transform from a quadtile to its (lat, long) float representation
137
+ For example: '-+AAAAAABCCBBBDADBBCCADDCBCCBB' -> (30.87, 133.11331)
138
+ """
139
+ if not quadtile or not len(quadtile) == len(PLACEHOLDER_QUADTILE_ENTRY):
140
+ return None, None
141
+
142
+ latitude_sign = 1 if quadtile[0] == "+" else -1
143
+ longitude_sign = 1 if quadtile[1] == "+" else -1
144
+
145
+ latitude_bin = []
146
+ longitude_bin = []
147
+ for quadtile_character in quadtile[2:]:
148
+ lat, long = GeoPositionSymbols[quadtile_character].value
149
+ latitude_bin.append(lat)
150
+ longitude_bin.append(long)
151
+
152
+ latitude = (int("".join(latitude_bin), 2) * latitude_sign) / GEOPOSITION_PRECISION
153
+ longitude = (int("".join(longitude_bin), 2) * longitude_sign) / GEOPOSITION_PRECISION
154
+
155
+ return latitude, longitude
156
+
157
+ quads_reverse_transform = np.frompyfunc(_binary_to_latlong_single, nin=1, nout=2)
158
+ latitudes, longitudes = quads_reverse_transform(data.to_numpy())
159
+
160
+ latlong_to_string = np.frompyfunc(
161
+ lambda lat, long: GEOPOSITION_OUTPUT_FORMAT.format(latitude=lat, longitude=long),
162
+ nin=2,
163
+ nout=1,
164
+ )
165
+ geo_positions = latlong_to_string(latitudes, longitudes)
166
+
167
+ geo_positions_data = pd.Series(data=geo_positions, name=data.name, index=data.index)
168
+
169
+ return geo_positions_data
170
+
171
+
172
+ def split_sub_columns_latlong(
173
+ data: pd.Series,
174
+ ) -> pd.DataFrame:
175
+ latitude_longitude = split_str_to_latlong(data)
176
+ isnan = np.isnan(latitude_longitude)
177
+ latitude_longitude[isnan] = 0.0
178
+ # mark invalid rows
179
+ invalid_entry = np.any(isnan, axis=1).values
180
+
181
+ quadtile = _latlong_to_binary(latitude_longitude.to_numpy())
182
+
183
+ def _geoposition_split_quads(quadtile: str) -> tuple:
184
+ """
185
+ Split the complete quadtile (e.g. '-+AAAAAABCCBBBDADBBCCADDCBCCBB') into its
186
+ sub-quadtiles: QUAD12, ..., QUAD28
187
+ """
188
+ quads = [
189
+ quadtile[0:x]
190
+ for x in range(
191
+ GEOPOSITION_FIRST_QUAD_ONSET,
192
+ GEOPOSITION_QUADTILE_LENGTH + GEOPOSITION_STEP_SIZE,
193
+ GEOPOSITION_STEP_SIZE,
194
+ )
195
+ ]
196
+ return tuple(quads)
197
+
198
+ split_quadtile = np.frompyfunc(_geoposition_split_quads, nin=1, nout=NUMBER_OF_GEOPOSITION_QUADS + 1)
199
+ quads = split_quadtile(quadtile)
200
+
201
+ # return a dataframe containing the sub quads as columns
202
+ transformed_data = pd.DataFrame(quads).T
203
+ transformed_data.columns = QUAD_COLUMNS + [
204
+ QUADTILE,
205
+ ]
206
+
207
+ # filter (set to empty string) the invalid entries for quads
208
+ transformed_data[invalid_entry] = ""
209
+ # filter invalid entries for quadtile by setting them to special string
210
+ transformed_data.loc[invalid_entry, QUADTILE] = NONE_QUADTILE_ENTRY
211
+ # insert a binary "nan" column at the beginning of the returned DataFrame
212
+ transformed_data.insert(0, "nan", isnan.any(axis=1).astype(int))
213
+ return transformed_data
214
+
215
+
216
+ def split_str_to_latlong(data: pd.Series) -> pd.DataFrame:
217
+ """
218
+ Split the string input into two float columns
219
+ "-13.1345, 89.331" - > | -13.1345 | 89.331 |
220
+ Anything that is not a match for a number is converted into nan
221
+
222
+ :param data: stringified lat, long Series
223
+
224
+ return: a DataFrame of two (lat, long) columns, accordingly
225
+ """
226
+
227
+ latitude_longitude = (
228
+ (
229
+ pd.concat([data, pd.Series(pd.NA)]) # ensure at least one row
230
+ .reset_index(drop=True)
231
+ .fillna("")
232
+ .astype(str)
233
+ + "," # ensure at least one comma per row
234
+ )
235
+ .str.split(",", expand=True)
236
+ .iloc[:, 0:2] # select the first two columns: lat, long
237
+ .apply(lambda x: pd.to_numeric(x, downcast="float", errors="coerce"))
238
+ .head(-1) # drop previously added row
239
+ )
240
+ return latitude_longitude
241
+
242
+
243
+ def analyze_latlong(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
244
+ values = safe_convert_string(values)
245
+ df_split = split_sub_columns_latlong(values)
246
+ df = pd.concat([root_keys, df_split], axis=1)
247
+
248
+ quad_partials = [
249
+ name for name in list(df.columns) if name not in (root_keys.name, "QUADTILE", "nan")
250
+ ] # ["QUAD_12", "QUAD_14", ...]
251
+ has_nan = bool(df_split["nan"].any())
252
+
253
+ # mapping of values and their counts (per root key) for each of the quads
254
+ quad_codes = {
255
+ quad_partial: df[[quad_partial, root_keys.name]].drop_duplicates()[quad_partial].value_counts().to_dict()
256
+ for quad_partial in quad_partials
257
+ }
258
+
259
+ df_non_na_quadtiles = df_split["QUADTILE"][df_split["nan"] == 0]
260
+ if df_non_na_quadtiles.empty: # in the edge-case if all are NaN
261
+ df_non_na_quadtiles = pd.Series([PLACEHOLDER_QUADTILE_ENTRY])
262
+ chars_df = df_non_na_quadtiles.str.split("", expand=True).drop([0, GEOPOSITION_QUADTILE_LENGTH + 1], axis=1)
263
+ chars_df.columns = [f"P{idx}" for idx in range(GEOPOSITION_QUADTILE_LENGTH)]
264
+ characters = {sub_col: list(chars_df[sub_col].unique()) for sub_col in chars_df.columns}
265
+
266
+ stats = {"has_nan": has_nan, "quad_codes": quad_codes, "characters": characters}
267
+ return stats
268
+
269
+
270
+ def analyze_reduce_latlong(stats_list: list[dict], value_protection: bool = True) -> dict:
271
+ # check if there are missing values
272
+ has_nan = any([j["has_nan"] for j in stats_list])
273
+ unk_cat_aliases = [""] # na / unknown (unseen) category
274
+
275
+ quads = list(stats_list[0]["quad_codes"].keys()) if len(stats_list) else []
276
+ quad_codes = {}
277
+
278
+ for quad in quads:
279
+ keys = list(
280
+ set(chain.from_iterable([stats["quad_codes"][quad].keys() for stats in stats_list]))
281
+ ) # all the possible values for a given quad
282
+ totals = {
283
+ key: sum([stats["quad_codes"][quad].get(key, 0) for stats in stats_list]) for key in keys if key
284
+ } # mapped to total counts per each value
285
+ totals = {
286
+ val: cnt for val, cnt in totals.items() if cnt >= RARE_CATEGORY_THRESHOLD
287
+ } # keep only those, which appear at least {rare_category_threshold} times
288
+ categories = ([CATEGORICAL_UNKNOWN_TOKEN] + [cat for cat in totals.keys() if cat not in unk_cat_aliases])[
289
+ :MAX_UNIQUE_VALUES_PER_QUAD
290
+ ] # UNK + remaining possible values
291
+ if len(categories) - 1 < MIN_UNIQUE_QUAD_VALUES: # excluding unknown category
292
+ continue # do not include quads having unique values less than {min_unique_quad_values}
293
+ quad_codes[quad] = {category: i for i, category in enumerate(categories)}
294
+
295
+ # combine all the characters stats
296
+ chars_all: dict[str, list] = {f"P{idx}": [] for idx in range(GEOPOSITION_QUADTILE_LENGTH)}
297
+ for stats in stats_list:
298
+ for k, v in stats["characters"].items():
299
+ chars_all[k] += list(set(v) - set(chars_all[k]))
300
+
301
+ # create character stats
302
+ quadtile_characters = {
303
+ "max_string_length": GEOPOSITION_QUADTILE_LENGTH,
304
+ "has_nan": False,
305
+ "codes": {
306
+ f"P{idx}": {char: code for code, char in enumerate(sorted(chars_all[f"P{idx}"]))}
307
+ for idx in range(GEOPOSITION_QUADTILE_LENGTH)
308
+ },
309
+ }
310
+
311
+ # determine cardinalities
312
+ cardinalities = {quad: len(quad_codes[quad]) for quad in quads if quad in quad_codes}
313
+ cardinalities.update({k: len(v) for k, v in quadtile_characters["codes"].items()}) # type: ignore
314
+ if has_nan:
315
+ cardinalities["nan"] = 2 # binary
316
+
317
+ stats = {
318
+ "has_nan": has_nan,
319
+ "cardinalities": cardinalities,
320
+ "quad_codes": quad_codes,
321
+ "quadtile_characters": quadtile_characters,
322
+ }
323
+ return stats
324
+
325
+
326
+ def encode_latlong(
327
+ values: pd.Series,
328
+ column_stats: dict,
329
+ context_keys: pd.Series | None = None,
330
+ ) -> pd.DataFrame:
331
+ values = safe_convert_string(values)
332
+ # split to sub_columns
333
+ quads = split_sub_columns_latlong(values)
334
+ encoded_quads = pd.DataFrame() # empty DF to include all the ModelEncodingType.tabular_categorical quads
335
+ for quad, value_counts in column_stats["quad_codes"].items():
336
+ stats = {"codes": value_counts}
337
+ encoded_quads[quad] = encode_categorical(quads[quad], stats)
338
+
339
+ encoded_quadtile = encode_character(quads["QUADTILE"], column_stats["quadtile_characters"])
340
+ if column_stats["has_nan"]:
341
+ encoded_quadtile["nan"] = quads["nan"]
342
+ df = pd.concat([encoded_quads, encoded_quadtile], axis=1)
343
+ return df
344
+
345
+
346
+ def decode_latlong(df_encoded: pd.DataFrame, stats: dict) -> pd.Series:
347
+ redundant_columns = [col for col in df_encoded.columns if not col.startswith("P")]
348
+ df = df_encoded.drop(redundant_columns, axis=1)
349
+ decoded_latlong = decode_character(df, stats["quadtile_characters"])
350
+ decoded_latlong = _binary_to_latlong(decoded_latlong)
351
+ if "nan" in df_encoded.columns:
352
+ decoded_latlong[df_encoded["nan"] == 1] = pd.NA
353
+ return decoded_latlong