mostlyai-engine 1.0.3__tar.gz → 1.1.0__tar.gz

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 (51) hide show
  1. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/PKG-INFO +3 -3
  2. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/__init__.py +1 -1
  3. mostlyai_engine-1.1.0/mostlyai/engine/_encoding_types/language/categorical.py +75 -0
  4. mostlyai_engine-1.1.0/mostlyai/engine/_encoding_types/language/datetime.py +143 -0
  5. mostlyai_engine-1.1.0/mostlyai/engine/_encoding_types/language/numeric.py +136 -0
  6. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/language/text.py +5 -1
  7. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/numeric.py +1 -1
  8. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/encoding.py +36 -14
  9. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/hf_engine.py +2 -5
  10. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/vllm_engine.py +6 -0
  11. mostlyai_engine-1.1.0/mostlyai/engine/_language/formatron_utils.py +258 -0
  12. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/generation.py +48 -18
  13. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/tokenizer_utils.py +23 -1
  14. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/training.py +2 -3
  15. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/analysis.py +84 -36
  16. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/domain.py +7 -1
  17. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/generation.py +3 -7
  18. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/pyproject.toml +7 -7
  19. mostlyai_engine-1.0.3/mostlyai/engine/_language/formatron_utils.py +0 -114
  20. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/.gitignore +0 -0
  21. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/LICENSE +0 -0
  22. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/README.md +0 -0
  23. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_common.py +0 -0
  24. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_dtypes.py +0 -0
  25. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/__init__.py +0 -0
  26. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/language/__init__.py +0 -0
  27. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/__init__.py +0 -0
  28. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/categorical.py +0 -0
  29. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/character.py +0 -0
  30. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/datetime.py +0 -0
  31. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/itt.py +0 -0
  32. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_encoding_types/tabular/lat_long.py +0 -0
  33. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/__init__.py +0 -0
  34. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/common.py +0 -0
  35. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/__init__.py +0 -0
  36. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/engine/base.py +0 -0
  37. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_language/lstm.py +0 -0
  38. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_memory.py +0 -0
  39. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/__init__.py +0 -0
  40. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/argn.py +0 -0
  41. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/common.py +0 -0
  42. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/encoding.py +0 -0
  43. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/fairness.py +0 -0
  44. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/generation.py +0 -0
  45. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_tabular/training.py +0 -0
  46. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_training_utils.py +0 -0
  47. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/_workspace.py +0 -0
  48. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/encoding.py +0 -0
  49. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/logging.py +0 -0
  50. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/splitting.py +0 -0
  51. {mostlyai_engine-1.0.3 → mostlyai_engine-1.1.0}/mostlyai/engine/training.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyai-engine
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: Synthetic Data Engine
5
5
  Project-URL: homepage, https://github.com/mostly-ai/mostlyai-engine
6
6
  Project-URL: repository, https://github.com/mostly-ai/mostlyai-engine
@@ -37,7 +37,7 @@ Requires-Dist: psutil<6,>=5.9.5
37
37
  Requires-Dist: pyarrow>=16.0.0
38
38
  Requires-Dist: rich<14,>=13.9.4
39
39
  Requires-Dist: setuptools<76,>=75.0.0
40
- Requires-Dist: tokenizers<0.21,>=0.20.1
40
+ Requires-Dist: tokenizers<0.22,>=0.21.0
41
41
  Requires-Dist: torch>=2.5.1; sys_platform == 'darwin'
42
42
  Requires-Dist: transformers<5,>=4.45.2
43
43
  Provides-Extra: cpu
@@ -46,7 +46,7 @@ Requires-Dist: torch>=2.5.1; (sys_platform != 'linux') and extra == 'cpu'
46
46
  Provides-Extra: gpu
47
47
  Requires-Dist: bitsandbytes<0.44,>=0.43.3; extra == 'gpu'
48
48
  Requires-Dist: torch>=2.5.1; extra == 'gpu'
49
- Requires-Dist: vllm<0.7,>=0.6.4; extra == 'gpu'
49
+ Requires-Dist: vllm<0.8,>=0.7.2; extra == 'gpu'
50
50
  Description-Content-Type: text/markdown
51
51
 
52
52
  # Synthetic Data Engine 💎
@@ -22,7 +22,7 @@ from mostlyai.engine.generation import generate
22
22
 
23
23
 
24
24
  __all__ = ["split", "analyze", "encode", "train", "generate", "init_logging"]
25
- __version__ = "1.0.3"
25
+ __version__ = "1.1.0"
26
26
 
27
27
  # suppress specific warning related to os.fork() in multi-threaded processes
28
28
  warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*multi-threaded.*fork.*")
@@ -0,0 +1,75 @@
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
+ Categorical encoding for language models.
17
+ """
18
+
19
+ import numpy as np
20
+ import pandas as pd
21
+
22
+ from mostlyai.engine._common import safe_convert_string, STRING
23
+
24
+ CATEGORICAL_UNKNOWN_TOKEN = "_RARE_"
25
+
26
+
27
+ def analyze_language_categorical(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
28
+ values = safe_convert_string(values)
29
+ # count distinct root_keys per categorical value for rare-category protection
30
+ df = pd.concat([root_keys, values], axis=1)
31
+ cnt_values = df.groupby(values.name)[root_keys.name].nunique().to_dict()
32
+ stats = {"has_nan": sum(values.isna()) > 0, "cnt_values": cnt_values}
33
+ return stats
34
+
35
+
36
+ def analyze_reduce_language_categorical(stats_list: list[dict], value_protection: bool = True) -> dict:
37
+ # sum up all counts for each categorical value
38
+ cnt_values: dict[str, int] = {}
39
+ for item in stats_list:
40
+ for value, count in item["cnt_values"].items():
41
+ cnt_values[value] = cnt_values.get(value, 0) + count
42
+ # create alphabetically sorted list of non-rare categories
43
+ known_categories = [k for k in sorted(cnt_values.keys())]
44
+ if value_protection:
45
+ # stochastic threshold for rare categories
46
+ rare_min = 5 + int(3 * np.random.uniform())
47
+ else:
48
+ rare_min = 0
49
+ categories = [k for k in known_categories if cnt_values[k] >= rare_min]
50
+ no_of_rare_categories = len(known_categories) - len(categories)
51
+ # add None to categories, if any are present
52
+ if any([j["has_nan"] for j in stats_list]):
53
+ categories = [None] + categories
54
+ # add special token for UNKNOWN categories at first position
55
+ if no_of_rare_categories > 0:
56
+ categories = [CATEGORICAL_UNKNOWN_TOKEN] + categories
57
+ stats = {"no_of_rare_categories": no_of_rare_categories, "categories": categories}
58
+ return stats
59
+
60
+
61
+ def encode_language_categorical(values: pd.Series, stats: dict) -> pd.Series:
62
+ values = safe_convert_string(values)
63
+ values = values.copy()
64
+ known_categories = stats["categories"]
65
+ mask = ~values.isin(known_categories)
66
+ if None in known_categories:
67
+ mask &= ~pd.isna(values)
68
+ values[mask] = CATEGORICAL_UNKNOWN_TOKEN
69
+ return values
70
+
71
+
72
+ def decode_language_categorical(x: pd.Series, col_stats: dict[str, str]) -> pd.Series:
73
+ x = x.astype(STRING)
74
+ allowed_categories = col_stats.get("categories", [])
75
+ return x.where(x.isin(allowed_categories), other=None)
@@ -0,0 +1,143 @@
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
+ import calendar
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+
19
+ from mostlyai.engine._common import safe_convert_datetime
20
+
21
+
22
+ def analyze_language_datetime(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
23
+ values = safe_convert_datetime(values)
24
+ df = pd.concat([root_keys, values], axis=1)
25
+ # determine lowest/highest values by root ID, and return Top 10
26
+ min_dates = df.groupby(root_keys.name)[values.name].min().dropna()
27
+ min11 = min_dates.sort_values(ascending=True).head(11).astype(str).tolist()
28
+ max_dates = df.groupby(root_keys.name)[values.name].max().dropna()
29
+ max11 = max_dates.sort_values(ascending=False).head(11).astype(str).tolist()
30
+ # determine if there are any NaN values
31
+ has_nan = bool(values.isna().any())
32
+ # return stats
33
+ stats = {
34
+ "has_nan": has_nan,
35
+ "min11": min11,
36
+ "max11": max11,
37
+ }
38
+ return stats
39
+
40
+
41
+ def analyze_reduce_language_datetime(stats_list: list[dict], value_protection: bool = True) -> dict:
42
+ # check if there are missing values
43
+ has_nan = any([j["has_nan"] for j in stats_list])
44
+ # determine min / max 5 values to map too low / too high values to
45
+ min11 = sorted([v for min11 in [j["min11"] for j in stats_list] for v in min11], reverse=False)[:11]
46
+ max11 = sorted([v for max11 in [j["max11"] for j in stats_list] for v in max11], reverse=True)[:11]
47
+ if value_protection:
48
+ # extreme value protection - discard lowest/highest 5 values
49
+ if len(min11) < 11 or len(max11) < 11:
50
+ # less than 11 subjects with non-NULL values; we need to protect all
51
+ min5 = []
52
+ max5 = []
53
+ else:
54
+ min5 = [str(v) for v in min11[5:10]] # drop 1 to 5th lowest; keep 6th to 10th lowest
55
+ max5 = [str(v) for v in max11[5:10]] # drop 1 to 5th highest; keep 6th to 10th highest
56
+ else:
57
+ min5 = min11[0:4]
58
+ max5 = max11[0:4]
59
+ stats = {
60
+ "has_nan": has_nan,
61
+ "min5": min5,
62
+ "max5": max5,
63
+ }
64
+ return stats
65
+
66
+
67
+ def encode_language_datetime(values: pd.Series, stats: dict, _: pd.Series | None = None) -> pd.Series:
68
+ # convert
69
+ values = safe_convert_datetime(values)
70
+ values = values.copy()
71
+ # reset index, as `values.mask` can throw errors for misaligned indices
72
+ values.reset_index(drop=True, inplace=True)
73
+ # replace extreme values with randomly sampled 5-th to 10-th largest/smallest values
74
+ min5 = stats["min5"] if len(stats["min5"]) > 0 else [0]
75
+ max5 = stats["max5"] if len(stats["max5"]) > 0 else [0]
76
+ min5 = pd.Series(min5, dtype=values.dtype)
77
+ max5 = pd.Series(max5, dtype=values.dtype)
78
+ values.mask(
79
+ values < min5[0],
80
+ min5.sample(n=len(values), replace=True, ignore_index=True),
81
+ inplace=True,
82
+ )
83
+ values.mask(
84
+ values > max5[0],
85
+ max5.sample(n=len(values), replace=True, ignore_index=True),
86
+ inplace=True,
87
+ )
88
+ return values
89
+
90
+
91
+ def _clip_datetime(x: pd.Series, min5: list, max5: list) -> pd.Series:
92
+ x_dt = pd.to_datetime(x, errors="coerce")
93
+ min_arr = pd.to_datetime(min5).to_numpy(dtype="datetime64[ns]")
94
+ max_arr = pd.to_datetime(max5).to_numpy(dtype="datetime64[ns]")
95
+ n = len(x_dt)
96
+ random_mins = np.random.choice(min_arr, size=n)
97
+ random_maxs = np.random.choice(max_arr, size=n)
98
+ clipped = np.minimum(np.maximum(x_dt.to_numpy(dtype="datetime64[ns]"), random_mins), random_maxs)
99
+ return pd.Series(clipped, index=x.index)
100
+
101
+
102
+ def decode_language_datetime(x: pd.Series, col_stats: dict[str, str]) -> pd.Series:
103
+ x = x.where(~x.isin(["", "_INVALID_"]), np.nan)
104
+
105
+ valid_mask = (
106
+ x.str.len().ge(10)
107
+ & x.str.slice(0, 4).str.isdigit()
108
+ & x.str.slice(5, 7).str.isdigit()
109
+ & x.str.slice(8, 10).str.isdigit()
110
+ )
111
+ if valid_mask.sum() > 0: # expected "YYYY-MM-DD" prefix
112
+ # handle the date portion, ensuring validity
113
+ years = x[valid_mask].str.slice(0, 4).astype(int)
114
+ months = x[valid_mask].str.slice(5, 7).astype(int)
115
+ days = x[valid_mask].str.slice(8, 10).astype(int)
116
+
117
+ # clamp days according to maximum possible day of the month of a given year
118
+ last_days = np.array([calendar.monthrange(y, m)[1] for y, m in zip(years, months)])
119
+ clamped_days = np.minimum(days, last_days)
120
+
121
+ # rebuild the date portion
122
+ new_date = (
123
+ years.astype(str).str.zfill(4)
124
+ + "-"
125
+ + months.astype(str).str.zfill(2)
126
+ + "-"
127
+ + pd.Series(clamped_days, index=years.index).astype(str).str.zfill(2)
128
+ )
129
+
130
+ # handle the time portion, ensuring validity
131
+ remainder = x[valid_mask].str.slice(10)
132
+
133
+ time_regex = r"^[ T]?(\d{2}:\d{2}:\d{2}(?:\.\d+)?)"
134
+ valid_time = remainder.str.extract(time_regex, expand=False)
135
+ valid_time = valid_time.fillna("00:00:00")
136
+ valid_time = " " + valid_time
137
+
138
+ new_date = new_date + valid_time
139
+ x.loc[valid_mask] = new_date
140
+
141
+ x = pd.to_datetime(x, errors="coerce")
142
+ x = _clip_datetime(x, col_stats["min5"], col_stats["max5"])
143
+ return x.astype("datetime64[ns]")
@@ -0,0 +1,136 @@
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
+ import numpy as np
15
+ import pandas as pd
16
+
17
+ from mostlyai.engine._common import safe_convert_numeric
18
+ from mostlyai.engine._encoding_types.tabular.numeric import _type_safe_numeric_series
19
+ from mostlyai.engine.domain import ModelEncodingType
20
+
21
+
22
+ def analyze_language_numeric(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
23
+ values = safe_convert_numeric(values)
24
+
25
+ # determine lowest/highest values by root ID, and return top 11
26
+ df = pd.concat([root_keys, values], axis=1)
27
+ min_values = df.groupby(root_keys.name)[values.name].min().dropna()
28
+ min11 = min_values.sort_values(ascending=True).head(11).tolist()
29
+ max_values = df.groupby(root_keys.name)[values.name].max().dropna()
30
+ max11 = max_values.sort_values(ascending=False).head(11).tolist()
31
+
32
+ # determine if there are any NaN values
33
+ has_nan = bool(values.isna().any())
34
+
35
+ # determine max scale
36
+ def count_scale(num: float) -> int:
37
+ # represent number as fixed point string, remove trailing zeros and decimal point
38
+ num = format(num, "f").rstrip("0").rstrip(".")
39
+ if "." in num:
40
+ # in case of decimal, return number of digits after decimal point
41
+ return len(num.split(".")[1])
42
+ # in case of integer, return 0
43
+ return 0
44
+
45
+ max_scale = int(values.apply(count_scale).max())
46
+
47
+ stats = {
48
+ "has_nan": has_nan,
49
+ "max_scale": max_scale,
50
+ "min11": min11,
51
+ "max11": max11,
52
+ }
53
+ return stats
54
+
55
+
56
+ def analyze_reduce_language_numeric(stats_list: list[dict], value_protection: bool = True) -> dict:
57
+ # check for occurrence of NaN values
58
+ has_nan = any([j["has_nan"] for j in stats_list])
59
+
60
+ # determine max scale
61
+ max_scale = max([j["max_scale"] for j in stats_list])
62
+
63
+ # determine min / max 5 values to map too low / too high values to
64
+ min11 = sorted([v for min11 in [j["min11"] for j in stats_list] for v in min11], reverse=False)[:11]
65
+ max11 = sorted([v for max11 in [j["max11"] for j in stats_list] for v in max11], reverse=True)[:11]
66
+ if value_protection:
67
+ # extreme value protection - discard lowest/highest 5 values
68
+ if len(min11) < 11 or len(max11) < 11:
69
+ # less than 11 subjects with non-NULL values; we need to protect all
70
+ min5 = []
71
+ max5 = []
72
+ else:
73
+ min5 = min11[5:10] # drop 1 to 5th lowest; keep 6th to 10th lowest
74
+ max5 = max11[5:10] # drop 1 to 5th highest; keep 6th to 10th highest
75
+ else:
76
+ min5 = min11[0:5]
77
+ max5 = max11[0:5]
78
+
79
+ stats = {
80
+ "encoding_type": ModelEncodingType.language_numeric.value,
81
+ "has_nan": has_nan,
82
+ "max_scale": max_scale,
83
+ "min5": min5,
84
+ "max5": max5,
85
+ }
86
+
87
+ return stats
88
+
89
+
90
+ def encode_language_numeric(values: pd.Series, stats: dict, _: pd.Series | None = None) -> pd.DataFrame:
91
+ values = safe_convert_numeric(values)
92
+ # try to convert to int, if possible
93
+ dtype = "Int64" if stats["max_scale"] == 0 else "Float64"
94
+ if dtype == "Int64":
95
+ values = values.round()
96
+ try:
97
+ values = values.astype(dtype)
98
+ except TypeError:
99
+ if dtype == "Int64": # if couldn't safely convert to int, stick to float
100
+ dtype = "Float64"
101
+ values = values.astype(dtype)
102
+ # reset index, as `values.mask` can throw errors for misaligned indices
103
+ values.reset_index(drop=True, inplace=True)
104
+ # replace extreme values with randomly sampled 5-th to 10-th largest/smallest values
105
+ min5 = _type_safe_numeric_series(stats["min5"] or [0], dtype)
106
+ max5 = _type_safe_numeric_series(stats["max5"] or [0], dtype)
107
+ values.mask(
108
+ values < min5[0],
109
+ min5.sample(n=len(values), replace=True, ignore_index=True),
110
+ inplace=True,
111
+ )
112
+ values.mask(
113
+ values > max5[0],
114
+ max5.sample(n=len(values), replace=True, ignore_index=True),
115
+ inplace=True,
116
+ )
117
+ return values
118
+
119
+
120
+ def _clip_numeric(x: pd.Series, min5: list, max5: list) -> pd.Series:
121
+ x_numeric = pd.to_numeric(x, errors="coerce")
122
+ min_arr = np.array(min5, dtype=x_numeric.dtype)
123
+ max_arr = np.array(max5, dtype=x_numeric.dtype)
124
+ n = len(x_numeric)
125
+ random_mins = np.random.choice(min_arr, size=n)
126
+ random_maxs = np.random.choice(max_arr, size=n)
127
+ clipped = np.minimum(np.maximum(x_numeric.to_numpy(), random_mins), random_maxs)
128
+ return pd.Series(clipped, index=x.index)
129
+
130
+
131
+ def decode_language_numeric(x: pd.Series, col_stats: dict[str, str]) -> pd.Series:
132
+ x = pd.to_numeric(x, errors="coerce")
133
+ x = x.round(col_stats["max_scale"])
134
+ x = _clip_numeric(x, col_stats["min5"], col_stats["max5"])
135
+ dtype = "Int64" if col_stats["max_scale"] == 0 else float
136
+ return x.astype(dtype)
@@ -14,7 +14,7 @@
14
14
 
15
15
  import pandas as pd
16
16
 
17
- from mostlyai.engine._common import safe_convert_string
17
+ from mostlyai.engine._common import safe_convert_string, STRING
18
18
 
19
19
 
20
20
  def analyze_text(values: pd.Series, root_keys: pd.Series, _: pd.Series | None = None) -> dict:
@@ -39,3 +39,7 @@ def analyze_reduce_text(stats_list: list[dict], _: bool = True) -> dict:
39
39
  "nchar_max": nchar_max,
40
40
  }
41
41
  return stats
42
+
43
+
44
+ def decode_text(x: pd.Series, col_stats: dict[str, str]) -> pd.Series:
45
+ return x.astype(STRING)
@@ -165,7 +165,7 @@ def analyze_numeric(
165
165
  # do not count values, if there are too many
166
166
  cnt_values = None
167
167
 
168
- # determine lowest/highest values by root ID, and return Top 10
168
+ # determine lowest/highest values by root ID, and return top 11
169
169
  df = pd.concat([root_keys, values], axis=1)
170
170
  min_values = df.groupby(root_keys.name)[values.name].min().dropna()
171
171
  min11 = min_values.sort_values(ascending=True).head(11).astype("float").tolist()
@@ -24,17 +24,36 @@ from tokenizers.pre_tokenizers import ByteLevel
24
24
 
25
25
  from mostlyai.engine._common import is_sequential, ProgressCallback, ProgressCallbackWrapper, TABLE_COLUMN_INFIX
26
26
  from mostlyai.engine._workspace import ensure_workspace_dir, Workspace, reset_dir
27
+ from mostlyai.engine._encoding_types.language.categorical import encode_language_categorical
28
+ from mostlyai.engine._encoding_types.language.numeric import encode_language_numeric
29
+ from mostlyai.engine._encoding_types.language.datetime import encode_language_datetime
27
30
 
28
31
  _LOG = logging.getLogger(__name__)
29
32
 
30
33
 
31
- def format_df(df: pd.DataFrame, columns: list[str], is_target: bool = False) -> pd.DataFrame:
32
- df = df[columns].copy()
34
+ def apply_encoding_types(df: pd.DataFrame, stats: dict) -> pd.DataFrame:
35
+ for col, col_stats in stats["columns"].items():
36
+ if col_stats["encoding_type"] == "LANGUAGE_CATEGORICAL":
37
+ df[col] = encode_language_categorical(df[col], col_stats)
38
+ elif col_stats["encoding_type"] == "LANGUAGE_NUMERIC":
39
+ df[col] = encode_language_numeric(df[col], col_stats)
40
+ elif col_stats["encoding_type"] == "LANGUAGE_DATETIME":
41
+ df[col] = encode_language_datetime(df[col], col_stats)
42
+ return df
43
+
44
+
45
+ def drop_sequential_columns(df: pd.DataFrame) -> pd.DataFrame:
33
46
  # Some columns (e.g., SCP columns) may contain np.ndarray, which are not JSON serializable
34
47
  # We need to drop them before converting the DataFrame to JSON
35
48
  sequential_columns = [col for col in df.columns if is_sequential(df[col])]
36
49
  df = df.drop(columns=sequential_columns)
37
- _LOG.info(f"Formatting {'target' if is_target else 'context'} columns {df.columns.tolist()} to JSON")
50
+ return df
51
+
52
+
53
+ def format_df(df: pd.DataFrame, stats: dict, is_target: bool = False) -> pd.DataFrame:
54
+ columns = list(stats["columns"].keys())
55
+ df = df[columns].copy()
56
+ _LOG.info(f"Formatting {'target' if is_target else 'context'} columns {columns} to JSON")
38
57
  # convert date format to ISO so that it's JSON serializable
39
58
  for col in df.columns:
40
59
  if is_datetime64_any_dtype(df[col]):
@@ -76,15 +95,21 @@ def row_to_json(row: pd.Series, is_target: bool = False) -> str:
76
95
 
77
96
  def encode_df(
78
97
  ctx_df: pd.DataFrame,
79
- ctx_columns: list[str],
98
+ ctx_stats: dict | None = None,
80
99
  tgt_df: pd.DataFrame | None = None,
81
- tgt_columns: list[str] | None = None,
100
+ tgt_stats: dict | None = None,
82
101
  ) -> pd.DataFrame:
83
- assert (tgt_df is None) == (tgt_columns is None), "tgt_df and tgt_columns must be both None or both not None"
102
+ assert (tgt_df is None) == (tgt_stats is None), "tgt_df and tgt_stats must be both None or both not None"
103
+ if ctx_stats is None:
104
+ ctx_stats = {"columns": {}}
84
105
  df = pd.DataFrame()
85
- df["ctx"] = format_df(ctx_df, columns=ctx_columns, is_target=False)
86
- if tgt_df is not None and tgt_columns is not None:
87
- df["tgt"] = format_df(tgt_df, columns=tgt_columns, is_target=True)
106
+ ctx_df = drop_sequential_columns(ctx_df)
107
+ ctx_df = apply_encoding_types(ctx_df, stats=ctx_stats)
108
+ df["ctx"] = format_df(ctx_df, stats=ctx_stats, is_target=False)
109
+ if tgt_df is not None and tgt_stats is not None:
110
+ tgt_df = drop_sequential_columns(tgt_df)
111
+ tgt_df = apply_encoding_types(tgt_df, stats=tgt_stats)
112
+ df["tgt"] = format_df(tgt_df, stats=tgt_stats, is_target=True)
88
113
 
89
114
  # log the bounds of n_tokens in this partition
90
115
  content = df["ctx"] + df["tgt"] if "tgt" in df.columns else df["ctx"]
@@ -107,19 +132,16 @@ def _encode_partition(
107
132
  ctx_stats: dict | None = None,
108
133
  ) -> None:
109
134
  tgt_df = pd.read_parquet(tgt_partition_file)
110
- tgt_columns = list(tgt_stats.get("columns", {}).keys())
111
135
  if ctx_partition_file:
112
136
  ctx_df = pd.read_parquet(ctx_partition_file)
113
- ctx_columns = list(ctx_stats.get("columns", {}).keys())
114
137
  else:
115
138
  # create on-the-fly context
116
139
  ctx_df = pd.DataFrame(index=range(len(tgt_df)))
117
- ctx_columns = []
118
140
  df = encode_df(
119
141
  ctx_df=ctx_df,
120
- ctx_columns=ctx_columns,
142
+ ctx_stats=ctx_stats,
121
143
  tgt_df=tgt_df,
122
- tgt_columns=tgt_columns,
144
+ tgt_stats=tgt_stats,
123
145
  )
124
146
  # shuffle and persist to disk as parquet files
125
147
  df = df.sample(frac=1)
@@ -23,8 +23,8 @@ from peft import PeftModel
23
23
 
24
24
  from transformers import AutoTokenizer
25
25
  from mostlyai.engine._language.common import load_base_model_and_config
26
- from mostlyai.engine._language.tokenizer_utils import tokenize_fn
27
26
  from mostlyai.engine._language.formatron_utils import monkey_patch_formatron
27
+ from mostlyai.engine._language.tokenizer_utils import tokenize_fn
28
28
 
29
29
  from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
30
30
  from formatron.formatter import FormatterBuilder
@@ -66,11 +66,8 @@ class HuggingFaceEngine(LanguageEngine):
66
66
  self.tokenizer.special_tokens_map
67
67
  )
68
68
  self._json_enforcing_possible = is_peft_adapter or is_trained_lstm_tokenizer
69
-
70
- # apply all necessary monkey patches to the formatron library
71
- if self._json_enforcing_possible:
69
+ if self.supports_json_enforcing():
72
70
  monkey_patch_formatron()
73
-
74
71
  self._logits_processors = None
75
72
 
76
73
  def get_default_batch_size(self) -> int:
@@ -16,6 +16,8 @@ import contextlib
16
16
  from os import PathLike
17
17
  import time
18
18
  import typing
19
+
20
+ from mostlyai.engine._language.common import is_bf16_supported
19
21
  from mostlyai.engine._language.engine.base import EngineMetrics, LanguageEngine
20
22
  import torch
21
23
  from formatron.config import EngineGenerationConfig
@@ -24,6 +26,7 @@ from formatron.formatter import FormatterBuilder
24
26
  from peft import PeftConfig
25
27
 
26
28
  from transformers import AutoTokenizer, AutoConfig, PreTrainedTokenizerBase
29
+ from mostlyai.engine._language.formatron_utils import monkey_patch_formatron
27
30
  from vllm import LLM, SamplingParams
28
31
  from vllm.lora.request import LoRARequest
29
32
  from vllm.config import _get_and_verify_max_len
@@ -105,8 +108,10 @@ class VLLMEngine(LanguageEngine):
105
108
  device=device.type,
106
109
  max_model_len=min(config_max_model_len, self.tokenizer_max_length + max_new_tokens),
107
110
  enable_lora=True,
111
+ dtype=torch.bfloat16 if is_bf16_supported(device) else torch.float16,
108
112
  # enforce_eager=True, # results in big slowdown, but is needed when running pytest locally
109
113
  swap_space=0,
114
+ disable_log_stats=True,
110
115
  )
111
116
  self._base_logits_processors = [MaskInvalidIndicesLogitsProcessor(self.llm.get_tokenizer())]
112
117
  self.tokenizer = AutoTokenizer.from_pretrained(
@@ -119,6 +124,7 @@ class VLLMEngine(LanguageEngine):
119
124
  add_eos_token=False,
120
125
  )
121
126
  self._logits_processors = None
127
+ monkey_patch_formatron()
122
128
 
123
129
  def get_default_batch_size(self) -> int:
124
130
  return 192