arborenum 0.2.0__tar.gz → 0.2.2__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.
- {arborenum-0.2.0/src/arborenum.egg-info → arborenum-0.2.2}/PKG-INFO +1 -1
- {arborenum-0.2.0 → arborenum-0.2.2}/pyproject.toml +1 -1
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum/__init__.py +570 -2
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum/_core.cpp +138 -6
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum/cpp/arborenum.cpp +1280 -16
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum/cpp/rid.cpp +317 -107
- {arborenum-0.2.0 → arborenum-0.2.2/src/arborenum.egg-info}/PKG-INFO +1 -1
- {arborenum-0.2.0 → arborenum-0.2.2}/MANIFEST.in +0 -0
- {arborenum-0.2.0 → arborenum-0.2.2}/README.md +0 -0
- {arborenum-0.2.0 → arborenum-0.2.2}/setup.cfg +0 -0
- {arborenum-0.2.0 → arborenum-0.2.2}/setup.py +0 -0
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum/_threshold_guessing.py +0 -0
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum.egg-info/SOURCES.txt +0 -0
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum.egg-info/dependency_links.txt +0 -0
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum.egg-info/requires.txt +0 -0
- {arborenum-0.2.0 → arborenum-0.2.2}/src/arborenum.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arborenum"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Exact, approximate, and anytime decision tree Rashomon sets over binary and continuous features"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = [
|
|
@@ -10,10 +10,35 @@ from ._core import (
|
|
|
10
10
|
)
|
|
11
11
|
from ._threshold_guessing import ThresholdGuessBinarizer
|
|
12
12
|
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from importlib.resources import files
|
|
16
|
+
|
|
13
17
|
DEFER_PREDICTION = -1
|
|
14
18
|
|
|
15
19
|
__all__ = ["ArborEnum", "ThresholdGuessBinarizer", "DEFER_PREDICTION"]
|
|
16
20
|
|
|
21
|
+
def _json_safe(x):
|
|
22
|
+
if isinstance(x, np.integer):
|
|
23
|
+
return int(x)
|
|
24
|
+
|
|
25
|
+
if isinstance(x, np.floating):
|
|
26
|
+
return float(x)
|
|
27
|
+
|
|
28
|
+
if isinstance(x, np.ndarray):
|
|
29
|
+
return x.tolist()
|
|
30
|
+
|
|
31
|
+
if isinstance(x, dict):
|
|
32
|
+
return {
|
|
33
|
+
str(k): _json_safe(v)
|
|
34
|
+
for k, v in x.items()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if isinstance(x, (list, tuple)):
|
|
38
|
+
return [_json_safe(v) for v in x]
|
|
39
|
+
|
|
40
|
+
return x
|
|
41
|
+
|
|
17
42
|
def _normalize_key(s: str) -> str:
|
|
18
43
|
# lower, trim, and make separators uniform
|
|
19
44
|
s = str(s).strip().lower()
|
|
@@ -311,6 +336,37 @@ def _as_2d_numeric_array(X, name):
|
|
|
311
336
|
|
|
312
337
|
return X
|
|
313
338
|
|
|
339
|
+
def _shift_column_values_to_midpoints(column):
|
|
340
|
+
# replace each observed value except the maximum with the midpoint
|
|
341
|
+
# between it and the next distinct observed value.
|
|
342
|
+
# used for training because splits are directly on unique values in the cpp
|
|
343
|
+
|
|
344
|
+
column = np.asarray(column, dtype=np.float64)
|
|
345
|
+
unique = np.unique(column)
|
|
346
|
+
|
|
347
|
+
if unique.size <= 1:
|
|
348
|
+
return column
|
|
349
|
+
|
|
350
|
+
left = unique[:-1]
|
|
351
|
+
right = unique[1:]
|
|
352
|
+
|
|
353
|
+
midpoints = left + 0.5 * (right - left)
|
|
354
|
+
|
|
355
|
+
valid = (
|
|
356
|
+
np.isfinite(midpoints)
|
|
357
|
+
& (midpoints > left)
|
|
358
|
+
& (midpoints < right)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
shifted_unique = unique.copy()
|
|
362
|
+
shifted_unique[:-1] = np.where(
|
|
363
|
+
valid,
|
|
364
|
+
midpoints,
|
|
365
|
+
left,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
positions = np.searchsorted(unique, column)
|
|
369
|
+
return shifted_unique[positions]
|
|
314
370
|
|
|
315
371
|
def _split_binary_and_continuous(
|
|
316
372
|
X,
|
|
@@ -331,8 +387,26 @@ def _split_binary_and_continuous(
|
|
|
331
387
|
if unique.size <= 1:
|
|
332
388
|
continue
|
|
333
389
|
|
|
390
|
+
left = unique[:-1]
|
|
391
|
+
right = unique[1:]
|
|
392
|
+
|
|
393
|
+
thresholds = left + 0.5 * (right - left)
|
|
394
|
+
|
|
395
|
+
valid = (
|
|
396
|
+
np.isfinite(thresholds)
|
|
397
|
+
& (thresholds > left)
|
|
398
|
+
& (thresholds < right)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
thresholds = np.where(
|
|
402
|
+
valid,
|
|
403
|
+
thresholds,
|
|
404
|
+
left,
|
|
405
|
+
)
|
|
406
|
+
|
|
334
407
|
if unique.size <= binary_unique_threshold:
|
|
335
|
-
|
|
408
|
+
# python directly creates the midpoint threshold columns for low cardinality features
|
|
409
|
+
for threshold in thresholds:
|
|
336
410
|
binary_columns.append(
|
|
337
411
|
(column <= threshold).astype(np.uint8)
|
|
338
412
|
)
|
|
@@ -343,9 +417,19 @@ def _split_binary_and_continuous(
|
|
|
343
417
|
"cutpoint": float(threshold),
|
|
344
418
|
}
|
|
345
419
|
)
|
|
420
|
+
|
|
346
421
|
else:
|
|
422
|
+
# shift each observed level so the existing C++ threshold
|
|
423
|
+
# generation (split on unique values) produces the midpoint cutpoints.
|
|
424
|
+
shifted_column = _shift_column_values_to_midpoints(
|
|
425
|
+
column
|
|
426
|
+
)
|
|
427
|
+
|
|
347
428
|
numerical_columns.append(
|
|
348
|
-
|
|
429
|
+
shifted_column.astype(
|
|
430
|
+
np.float64,
|
|
431
|
+
copy=False,
|
|
432
|
+
)
|
|
349
433
|
)
|
|
350
434
|
|
|
351
435
|
numerical_feature_indices.append(j)
|
|
@@ -668,6 +752,13 @@ class ArborEnum:
|
|
|
668
752
|
eta_defer=eta_defer,
|
|
669
753
|
)
|
|
670
754
|
|
|
755
|
+
self.lambda_reg_ = float(lambda_reg)
|
|
756
|
+
self.depth_budget_ = int(depth_budget)
|
|
757
|
+
self.rashomon_mult_ = float(rashomon_mult)
|
|
758
|
+
self.multiplicative_slack_ = float(multiplicative_slack)
|
|
759
|
+
self.lookahead_k_ = int(lookahead_k)
|
|
760
|
+
self.eta_defer_ = float(eta_defer)
|
|
761
|
+
|
|
671
762
|
return self.fit_prepared(
|
|
672
763
|
lambda_reg=lambda_reg,
|
|
673
764
|
depth_budget=depth_budget,
|
|
@@ -1631,6 +1722,13 @@ class ArborEnum:
|
|
|
1631
1722
|
thresh = round((1.0 + mult) * min_obj)
|
|
1632
1723
|
return sum(cnt for obj, cnt in hist if obj <= thresh)
|
|
1633
1724
|
|
|
1725
|
+
def alternating_optimization(self, max_iterations=10):
|
|
1726
|
+
return int(
|
|
1727
|
+
self._model.alternating_optimization(
|
|
1728
|
+
int(max_iterations)
|
|
1729
|
+
)
|
|
1730
|
+
)
|
|
1731
|
+
|
|
1634
1732
|
def get_continuous_starts(self):
|
|
1635
1733
|
# return the internal column index at which each continuous threshold group begins.
|
|
1636
1734
|
return list(self._model.get_continuous_starts())
|
|
@@ -1917,6 +2015,458 @@ class ArborEnum:
|
|
|
1917
2015
|
)
|
|
1918
2016
|
|
|
1919
2017
|
return names
|
|
2018
|
+
|
|
2019
|
+
def export_andor_graph(self, as_dict=True):
|
|
2020
|
+
g = self._model.export_andor_graph()
|
|
2021
|
+
|
|
2022
|
+
if not as_dict:
|
|
2023
|
+
return g
|
|
2024
|
+
|
|
2025
|
+
return {
|
|
2026
|
+
"root_trie_id": int(g.root_trie_id),
|
|
2027
|
+
|
|
2028
|
+
"trie_nodes": [
|
|
2029
|
+
{
|
|
2030
|
+
"id": int(node.id),
|
|
2031
|
+
"budget": int(node.budget),
|
|
2032
|
+
"min_objective": int(node.min_objective),
|
|
2033
|
+
"subproblem_size": int(
|
|
2034
|
+
node.subproblem_size
|
|
2035
|
+
),
|
|
2036
|
+
"leaf_ids": [
|
|
2037
|
+
int(x)
|
|
2038
|
+
for x in node.leaf_ids
|
|
2039
|
+
],
|
|
2040
|
+
"split_ids": [
|
|
2041
|
+
int(x)
|
|
2042
|
+
for x in node.split_ids
|
|
2043
|
+
],
|
|
2044
|
+
}
|
|
2045
|
+
for node in g.trie_nodes
|
|
2046
|
+
],
|
|
2047
|
+
|
|
2048
|
+
"split_nodes": [
|
|
2049
|
+
{
|
|
2050
|
+
"id": int(split.id),
|
|
2051
|
+
"parent_trie_id": int(
|
|
2052
|
+
split.parent_trie_id
|
|
2053
|
+
),
|
|
2054
|
+
"feature": int(split.feature),
|
|
2055
|
+
"left_trie_id": int(
|
|
2056
|
+
split.left_trie_id
|
|
2057
|
+
),
|
|
2058
|
+
"right_trie_id": int(
|
|
2059
|
+
split.right_trie_id
|
|
2060
|
+
),
|
|
2061
|
+
"min_objective": int(
|
|
2062
|
+
split.min_objective
|
|
2063
|
+
),
|
|
2064
|
+
}
|
|
2065
|
+
for split in g.split_nodes
|
|
2066
|
+
],
|
|
2067
|
+
|
|
2068
|
+
"leaf_nodes": [
|
|
2069
|
+
{
|
|
2070
|
+
"id": int(leaf.id),
|
|
2071
|
+
"parent_trie_id": int(
|
|
2072
|
+
leaf.parent_trie_id
|
|
2073
|
+
),
|
|
2074
|
+
"prediction": int(
|
|
2075
|
+
leaf.prediction
|
|
2076
|
+
),
|
|
2077
|
+
"loss": int(leaf.loss),
|
|
2078
|
+
"subproblem_size": int(
|
|
2079
|
+
leaf.subproblem_size
|
|
2080
|
+
),
|
|
2081
|
+
}
|
|
2082
|
+
for leaf in g.leaf_nodes
|
|
2083
|
+
],
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
def _get_builder_feature_metadata(
|
|
2087
|
+
self,
|
|
2088
|
+
feature_names=None,
|
|
2089
|
+
):
|
|
2090
|
+
if self.binary_feature_specs_ is None:
|
|
2091
|
+
raise RuntimeError(
|
|
2092
|
+
"Feature metadata is unavailable. "
|
|
2093
|
+
"Fit ArborEnum through fit(X, y, ...) first."
|
|
2094
|
+
)
|
|
2095
|
+
|
|
2096
|
+
if self.continuous_feature_indices_ is None:
|
|
2097
|
+
raise RuntimeError(
|
|
2098
|
+
"Continuous feature metadata is unavailable."
|
|
2099
|
+
)
|
|
2100
|
+
|
|
2101
|
+
if self.n_features_in_ is None:
|
|
2102
|
+
raise RuntimeError(
|
|
2103
|
+
"Original feature count is unavailable."
|
|
2104
|
+
)
|
|
2105
|
+
|
|
2106
|
+
if feature_names is None:
|
|
2107
|
+
feature_names = list(self.feature_names_in_)
|
|
2108
|
+
else:
|
|
2109
|
+
feature_names = list(feature_names)
|
|
2110
|
+
|
|
2111
|
+
if len(feature_names) != self.n_features_in_:
|
|
2112
|
+
raise ValueError(
|
|
2113
|
+
"feature_names must contain one name for "
|
|
2114
|
+
"every original input feature."
|
|
2115
|
+
)
|
|
2116
|
+
|
|
2117
|
+
# exactly aligned with our internal ids
|
|
2118
|
+
internal_feature_names = (
|
|
2119
|
+
self.get_internal_feature_names(feature_names)
|
|
2120
|
+
)
|
|
2121
|
+
|
|
2122
|
+
thresholds = {}
|
|
2123
|
+
feature_registry = []
|
|
2124
|
+
|
|
2125
|
+
# low cardinality features
|
|
2126
|
+
for internal_feature, spec in enumerate(
|
|
2127
|
+
self.binary_feature_specs_
|
|
2128
|
+
):
|
|
2129
|
+
original_feature = int(
|
|
2130
|
+
spec["original_feature"]
|
|
2131
|
+
)
|
|
2132
|
+
|
|
2133
|
+
cutpoint = float(spec["cutpoint"])
|
|
2134
|
+
|
|
2135
|
+
thresholds[internal_feature] = cutpoint
|
|
2136
|
+
|
|
2137
|
+
feature_registry.append(
|
|
2138
|
+
{
|
|
2139
|
+
"internalFeature": internal_feature,
|
|
2140
|
+
"originalFeature": original_feature,
|
|
2141
|
+
"originalName": str(
|
|
2142
|
+
feature_names[original_feature]
|
|
2143
|
+
),
|
|
2144
|
+
"threshold": cutpoint,
|
|
2145
|
+
"kind": "binary_threshold",
|
|
2146
|
+
"continuousGroup": None,
|
|
2147
|
+
}
|
|
2148
|
+
)
|
|
2149
|
+
|
|
2150
|
+
# cpp generated continuous groups
|
|
2151
|
+
continuous_groups = {}
|
|
2152
|
+
|
|
2153
|
+
starts = [
|
|
2154
|
+
int(x)
|
|
2155
|
+
for x in self._model.get_continuous_starts()
|
|
2156
|
+
]
|
|
2157
|
+
|
|
2158
|
+
for group, original_feature in enumerate(
|
|
2159
|
+
self.continuous_feature_indices_
|
|
2160
|
+
):
|
|
2161
|
+
original_feature = int(original_feature)
|
|
2162
|
+
|
|
2163
|
+
cutpoints = [
|
|
2164
|
+
float(x)
|
|
2165
|
+
for x in self.get_continuous_cutpoints(group)
|
|
2166
|
+
]
|
|
2167
|
+
|
|
2168
|
+
start = starts[group]
|
|
2169
|
+
|
|
2170
|
+
internal_columns = []
|
|
2171
|
+
|
|
2172
|
+
for offset, cutpoint in enumerate(cutpoints):
|
|
2173
|
+
internal_feature = start + offset
|
|
2174
|
+
|
|
2175
|
+
thresholds[internal_feature] = cutpoint
|
|
2176
|
+
internal_columns.append(internal_feature)
|
|
2177
|
+
|
|
2178
|
+
feature_registry.append(
|
|
2179
|
+
{
|
|
2180
|
+
"internalFeature": internal_feature,
|
|
2181
|
+
"originalFeature": original_feature,
|
|
2182
|
+
"originalName": str(
|
|
2183
|
+
feature_names[original_feature]
|
|
2184
|
+
),
|
|
2185
|
+
"threshold": cutpoint,
|
|
2186
|
+
"kind": "continuous_threshold",
|
|
2187
|
+
"continuousGroup": group,
|
|
2188
|
+
}
|
|
2189
|
+
)
|
|
2190
|
+
|
|
2191
|
+
continuous_groups[
|
|
2192
|
+
str(feature_names[original_feature])
|
|
2193
|
+
] = internal_columns
|
|
2194
|
+
|
|
2195
|
+
# feature_registry[i] describes split.feature == i. this should already be the case by how we went over.
|
|
2196
|
+
feature_registry.sort(
|
|
2197
|
+
key=lambda x: x["internalFeature"]
|
|
2198
|
+
)
|
|
2199
|
+
|
|
2200
|
+
for expected, entry in enumerate(feature_registry):
|
|
2201
|
+
if entry["internalFeature"] != expected:
|
|
2202
|
+
raise RuntimeError(
|
|
2203
|
+
"Builder feature metadata is not aligned "
|
|
2204
|
+
"with ArborEnum internal feature indices: "
|
|
2205
|
+
f"expected {expected}, got "
|
|
2206
|
+
f"{entry['internalFeature']}."
|
|
2207
|
+
)
|
|
2208
|
+
|
|
2209
|
+
if len(internal_feature_names) != len(
|
|
2210
|
+
feature_registry
|
|
2211
|
+
):
|
|
2212
|
+
raise RuntimeError(
|
|
2213
|
+
"Internal feature-name count does not match "
|
|
2214
|
+
"the builder feature registry."
|
|
2215
|
+
)
|
|
2216
|
+
|
|
2217
|
+
if len(starts) != len(
|
|
2218
|
+
self.continuous_feature_indices_
|
|
2219
|
+
):
|
|
2220
|
+
raise RuntimeError(
|
|
2221
|
+
"Continuous-group metadata is inconsistent: "
|
|
2222
|
+
"the number of C++ continuous groups does not "
|
|
2223
|
+
"match continuous_feature_indices_."
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
|
|
2227
|
+
# feature_registry says for each threshold
|
|
2228
|
+
'''
|
|
2229
|
+
internalFeature: 0,
|
|
2230
|
+
originalFeature: 1,
|
|
2231
|
+
originalName: sex,
|
|
2232
|
+
threshold: 0.5,
|
|
2233
|
+
kind: binary_threshold,
|
|
2234
|
+
continuousGroup: None,
|
|
2235
|
+
'''
|
|
2236
|
+
|
|
2237
|
+
return {
|
|
2238
|
+
"featureRegistry": feature_registry,
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
|
|
2242
|
+
def save_builder_payload(
|
|
2243
|
+
self,
|
|
2244
|
+
path,
|
|
2245
|
+
*,
|
|
2246
|
+
feature_names=None,
|
|
2247
|
+
feature_descriptions=None,
|
|
2248
|
+
lambda_reg=None,
|
|
2249
|
+
depth_budget=None,
|
|
2250
|
+
rashomon_mult=None,
|
|
2251
|
+
multiplicative_slack=None,
|
|
2252
|
+
lookahead_k=None,
|
|
2253
|
+
root_n=None,
|
|
2254
|
+
gamma=None,
|
|
2255
|
+
eta_defer=None,
|
|
2256
|
+
indent=2,
|
|
2257
|
+
):
|
|
2258
|
+
graph = self.export_andor_graph(
|
|
2259
|
+
as_dict=True
|
|
2260
|
+
)
|
|
2261
|
+
|
|
2262
|
+
feature_meta = (
|
|
2263
|
+
self._get_builder_feature_metadata(
|
|
2264
|
+
feature_names=feature_names
|
|
2265
|
+
)
|
|
2266
|
+
)
|
|
2267
|
+
|
|
2268
|
+
if lambda_reg is None:
|
|
2269
|
+
lambda_reg = getattr(
|
|
2270
|
+
self,
|
|
2271
|
+
"lambda_reg_",
|
|
2272
|
+
None,
|
|
2273
|
+
)
|
|
2274
|
+
|
|
2275
|
+
if depth_budget is None:
|
|
2276
|
+
depth_budget = getattr(
|
|
2277
|
+
self,
|
|
2278
|
+
"depth_budget_",
|
|
2279
|
+
None,
|
|
2280
|
+
)
|
|
2281
|
+
|
|
2282
|
+
if rashomon_mult is None:
|
|
2283
|
+
rashomon_mult = getattr(
|
|
2284
|
+
self,
|
|
2285
|
+
"rashomon_mult_",
|
|
2286
|
+
None,
|
|
2287
|
+
)
|
|
2288
|
+
|
|
2289
|
+
if multiplicative_slack is None:
|
|
2290
|
+
multiplicative_slack = getattr(
|
|
2291
|
+
self,
|
|
2292
|
+
"multiplicative_slack_",
|
|
2293
|
+
None,
|
|
2294
|
+
)
|
|
2295
|
+
|
|
2296
|
+
if lookahead_k is None:
|
|
2297
|
+
lookahead_k = getattr(
|
|
2298
|
+
self,
|
|
2299
|
+
"lookahead_k_",
|
|
2300
|
+
None,
|
|
2301
|
+
)
|
|
2302
|
+
|
|
2303
|
+
if eta_defer is None:
|
|
2304
|
+
eta_defer = getattr(
|
|
2305
|
+
self,
|
|
2306
|
+
"eta_defer_",
|
|
2307
|
+
None,
|
|
2308
|
+
)
|
|
2309
|
+
|
|
2310
|
+
meta = {
|
|
2311
|
+
**feature_meta,
|
|
2312
|
+
|
|
2313
|
+
"featureDescriptions": feature_descriptions,
|
|
2314
|
+
|
|
2315
|
+
"lambda_reg": lambda_reg,
|
|
2316
|
+
"depth_budget": depth_budget,
|
|
2317
|
+
"rashomon_mult": rashomon_mult,
|
|
2318
|
+
"multiplicative_slack": multiplicative_slack,
|
|
2319
|
+
"lookahead_k": lookahead_k,
|
|
2320
|
+
"root_n": root_n,
|
|
2321
|
+
"gamma": gamma,
|
|
2322
|
+
"eta_defer": eta_defer,
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
root_id = int(
|
|
2326
|
+
graph.get("root_trie_id", 0)
|
|
2327
|
+
)
|
|
2328
|
+
|
|
2329
|
+
root = next(
|
|
2330
|
+
(
|
|
2331
|
+
node
|
|
2332
|
+
for node in graph.get(
|
|
2333
|
+
"trie_nodes",
|
|
2334
|
+
[],
|
|
2335
|
+
)
|
|
2336
|
+
if int(node.get("id", -1)) == root_id
|
|
2337
|
+
),
|
|
2338
|
+
None,
|
|
2339
|
+
)
|
|
2340
|
+
|
|
2341
|
+
if root is not None:
|
|
2342
|
+
meta["root_budget"] = root.get(
|
|
2343
|
+
"budget"
|
|
2344
|
+
)
|
|
2345
|
+
|
|
2346
|
+
meta["root_min_objective"] = root.get(
|
|
2347
|
+
"min_objective"
|
|
2348
|
+
)
|
|
2349
|
+
|
|
2350
|
+
meta["root_subproblem_size"] = root.get(
|
|
2351
|
+
"subproblem_size"
|
|
2352
|
+
)
|
|
2353
|
+
|
|
2354
|
+
if root_n is None:
|
|
2355
|
+
meta["root_n"] = root.get(
|
|
2356
|
+
"subproblem_size"
|
|
2357
|
+
)
|
|
2358
|
+
|
|
2359
|
+
if (
|
|
2360
|
+
gamma is None
|
|
2361
|
+
and lambda_reg is not None
|
|
2362
|
+
and meta.get("root_n") is not None
|
|
2363
|
+
):
|
|
2364
|
+
meta["gamma"] = int(
|
|
2365
|
+
round(
|
|
2366
|
+
float(lambda_reg)
|
|
2367
|
+
* int(meta["root_n"])
|
|
2368
|
+
)
|
|
2369
|
+
)
|
|
2370
|
+
|
|
2371
|
+
payload = {
|
|
2372
|
+
"graph": graph,
|
|
2373
|
+
"meta": {
|
|
2374
|
+
k: v
|
|
2375
|
+
for k, v in meta.items()
|
|
2376
|
+
if v is not None
|
|
2377
|
+
},
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
path = Path(path)
|
|
2381
|
+
|
|
2382
|
+
path.parent.mkdir(
|
|
2383
|
+
parents=True,
|
|
2384
|
+
exist_ok=True,
|
|
2385
|
+
)
|
|
2386
|
+
|
|
2387
|
+
path.write_text(
|
|
2388
|
+
json.dumps(
|
|
2389
|
+
_json_safe(payload),
|
|
2390
|
+
indent=indent,
|
|
2391
|
+
),
|
|
2392
|
+
encoding="utf-8",
|
|
2393
|
+
)
|
|
2394
|
+
|
|
2395
|
+
return path
|
|
2396
|
+
|
|
2397
|
+
def save_builder_html(
|
|
2398
|
+
self,
|
|
2399
|
+
path,
|
|
2400
|
+
**kwargs,
|
|
2401
|
+
):
|
|
2402
|
+
payload_path = (
|
|
2403
|
+
Path(path).with_suffix(
|
|
2404
|
+
".payload.json"
|
|
2405
|
+
)
|
|
2406
|
+
)
|
|
2407
|
+
|
|
2408
|
+
self.save_builder_payload(
|
|
2409
|
+
payload_path,
|
|
2410
|
+
**kwargs,
|
|
2411
|
+
)
|
|
2412
|
+
|
|
2413
|
+
payload = json.loads(
|
|
2414
|
+
payload_path.read_text(
|
|
2415
|
+
encoding="utf-8"
|
|
2416
|
+
)
|
|
2417
|
+
)
|
|
2418
|
+
|
|
2419
|
+
payload_path.unlink(
|
|
2420
|
+
missing_ok=True
|
|
2421
|
+
)
|
|
2422
|
+
|
|
2423
|
+
payload_json = json.dumps(payload)
|
|
2424
|
+
|
|
2425
|
+
# prevent accidental closing of the script tag
|
|
2426
|
+
payload_json = payload_json.replace(
|
|
2427
|
+
"</",
|
|
2428
|
+
"<\\/",
|
|
2429
|
+
)
|
|
2430
|
+
|
|
2431
|
+
html = (
|
|
2432
|
+
files(__package__)
|
|
2433
|
+
.joinpath(
|
|
2434
|
+
"builder_static/index.html"
|
|
2435
|
+
)
|
|
2436
|
+
.read_text(
|
|
2437
|
+
encoding="utf-8"
|
|
2438
|
+
)
|
|
2439
|
+
)
|
|
2440
|
+
|
|
2441
|
+
inject = f"""
|
|
2442
|
+
<script>
|
|
2443
|
+
window.ARBORENUM_BUILDER_PAYLOAD = {payload_json};
|
|
2444
|
+
</script>
|
|
2445
|
+
"""
|
|
2446
|
+
|
|
2447
|
+
if "</head>" in html:
|
|
2448
|
+
html = html.replace(
|
|
2449
|
+
"</head>",
|
|
2450
|
+
inject + "\n</head>",
|
|
2451
|
+
1,
|
|
2452
|
+
)
|
|
2453
|
+
else:
|
|
2454
|
+
html = inject + html
|
|
2455
|
+
|
|
2456
|
+
path = Path(path)
|
|
2457
|
+
|
|
2458
|
+
path.parent.mkdir(
|
|
2459
|
+
parents=True,
|
|
2460
|
+
exist_ok=True,
|
|
2461
|
+
)
|
|
2462
|
+
|
|
2463
|
+
path.write_text(
|
|
2464
|
+
html,
|
|
2465
|
+
encoding="utf-8",
|
|
2466
|
+
)
|
|
2467
|
+
|
|
2468
|
+
return path
|
|
2469
|
+
|
|
1920
2470
|
|
|
1921
2471
|
def plot_tree(self, tree_index: int, feature_names=None, figsize=(8, 6), ax=None, title=None, show=True):
|
|
1922
2472
|
paths, preds = self.get_tree_paths(tree_index)
|
|
@@ -2337,6 +2887,8 @@ class ArborEnum:
|
|
|
2337
2887
|
lookahead_k=1,
|
|
2338
2888
|
seed=0,
|
|
2339
2889
|
memory_efficient=False,
|
|
2890
|
+
key_mode="hash",
|
|
2891
|
+
trie_cache_enabled=True,
|
|
2340
2892
|
proxy_refinement="auto",
|
|
2341
2893
|
refinement_width=1,
|
|
2342
2894
|
max_refinement_rounds=-1,
|
|
@@ -2354,6 +2906,7 @@ class ArborEnum:
|
|
|
2354
2906
|
eta_defer=0.0,
|
|
2355
2907
|
bb_pred=None,
|
|
2356
2908
|
return_joint_samples=False,
|
|
2909
|
+
lossless=False,
|
|
2357
2910
|
):
|
|
2358
2911
|
|
|
2359
2912
|
if hasattr(X, "columns"):
|
|
@@ -2548,6 +3101,8 @@ class ArborEnum:
|
|
|
2548
3101
|
int(lookahead_k),
|
|
2549
3102
|
int(seed),
|
|
2550
3103
|
bool(memory_efficient),
|
|
3104
|
+
parse_key_mode(key_mode),
|
|
3105
|
+
bool(trie_cache_enabled),
|
|
2551
3106
|
bool(early_stopping),
|
|
2552
3107
|
int(refinement_width),
|
|
2553
3108
|
int(max_refinement_rounds),
|
|
@@ -2570,6 +3125,7 @@ class ArborEnum:
|
|
|
2570
3125
|
float(eta_defer),
|
|
2571
3126
|
bb_pred_vec,
|
|
2572
3127
|
bool(return_joint_samples),
|
|
3128
|
+
bool(lossless),
|
|
2573
3129
|
)
|
|
2574
3130
|
|
|
2575
3131
|
if (
|
|
@@ -2599,11 +3155,14 @@ class ArborEnum:
|
|
|
2599
3155
|
lookahead_k=1,
|
|
2600
3156
|
seed=0,
|
|
2601
3157
|
memory_efficient=False,
|
|
3158
|
+
key_mode="hash",
|
|
3159
|
+
trie_cache_enabled=True,
|
|
2602
3160
|
binning_map=None,
|
|
2603
3161
|
use_deferral=False,
|
|
2604
3162
|
eta_defer=0.0,
|
|
2605
3163
|
bb_pred=None,
|
|
2606
3164
|
return_joint_samples=False,
|
|
3165
|
+
lossless=False,
|
|
2607
3166
|
):
|
|
2608
3167
|
X = np.asarray(X, dtype=np.uint8)
|
|
2609
3168
|
y = np.asarray(y, dtype=int)
|
|
@@ -2662,11 +3221,14 @@ class ArborEnum:
|
|
|
2662
3221
|
int(lookahead_k),
|
|
2663
3222
|
int(seed),
|
|
2664
3223
|
bool(memory_efficient),
|
|
3224
|
+
parse_key_mode(key_mode),
|
|
3225
|
+
bool(trie_cache_enabled),
|
|
2665
3226
|
binning_map,
|
|
2666
3227
|
bool(use_deferral),
|
|
2667
3228
|
float(eta_defer),
|
|
2668
3229
|
bb_pred_vec,
|
|
2669
3230
|
bool(return_joint_samples),
|
|
3231
|
+
bool(lossless),
|
|
2670
3232
|
)
|
|
2671
3233
|
|
|
2672
3234
|
if (
|
|
@@ -2701,6 +3263,8 @@ class ArborEnum:
|
|
|
2701
3263
|
lookahead_k=1,
|
|
2702
3264
|
seed=0,
|
|
2703
3265
|
memory_efficient=False,
|
|
3266
|
+
key_mode="hash",
|
|
3267
|
+
trie_cache_enabled=True,
|
|
2704
3268
|
use_anytime_fit=False,
|
|
2705
3269
|
refinement_width=1,
|
|
2706
3270
|
max_refinement_rounds=-1,
|
|
@@ -2723,6 +3287,7 @@ class ArborEnum:
|
|
|
2723
3287
|
eta_defer=0.0,
|
|
2724
3288
|
bb_pred=None,
|
|
2725
3289
|
return_joint_samples=False,
|
|
3290
|
+
lossless=False,
|
|
2726
3291
|
):
|
|
2727
3292
|
X_num = np.asarray(X_num, dtype=np.float64)
|
|
2728
3293
|
y = np.asarray(y, dtype=int)
|
|
@@ -2901,6 +3466,8 @@ class ArborEnum:
|
|
|
2901
3466
|
int(lookahead_k),
|
|
2902
3467
|
int(seed),
|
|
2903
3468
|
bool(memory_efficient),
|
|
3469
|
+
parse_key_mode(key_mode),
|
|
3470
|
+
bool(trie_cache_enabled),
|
|
2904
3471
|
bool(use_anytime_fit),
|
|
2905
3472
|
int(refinement_width),
|
|
2906
3473
|
int(max_refinement_rounds),
|
|
@@ -2925,6 +3492,7 @@ class ArborEnum:
|
|
|
2925
3492
|
float(eta_defer),
|
|
2926
3493
|
bb_pred_vec,
|
|
2927
3494
|
bool(return_joint_samples),
|
|
3495
|
+
bool(lossless),
|
|
2928
3496
|
)
|
|
2929
3497
|
|
|
2930
3498
|
if (
|