rapidsegment 1.2.1__tar.gz → 1.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.
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/PKG-INFO +1 -1
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/pyproject.toml +1 -1
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/pyproject.toml.orig +1 -1
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/builder.py +159 -59
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/LICENSE +0 -0
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/README.md +0 -0
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/__init__.py +0 -0
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/py.typed +0 -0
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/scorer.py +0 -0
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/utils/__init__.py +0 -0
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/utils/data_loader.py +0 -0
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/utils/on_gcp_feature_selection.py +0 -0
- {rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/utils/undersampler.sql +0 -0
|
@@ -95,8 +95,7 @@ class StrategicSegmentBuilder:
|
|
|
95
95
|
feature_groups: Optional[Dict[str, List[str]]] = None,
|
|
96
96
|
ignore_features: Optional[List[str]] = None,
|
|
97
97
|
sort_priority: str = "rate_lift_count",
|
|
98
|
-
binning_method: str = "optimal",
|
|
99
|
-
naive_bins: int = 5 ,
|
|
98
|
+
binning_method: str = "optimal_cart", # "naive" | "optimal_cart" | "optimal_quantile" | "optimal" (alias of cart) naive_bins: int = 5 ,
|
|
100
99
|
max_expansion_hops: int = 0,
|
|
101
100
|
selection_metric: str = "iv",
|
|
102
101
|
expand_log_mode: str = "none",
|
|
@@ -121,8 +120,12 @@ class StrategicSegmentBuilder:
|
|
|
121
120
|
feature_groups: Mapping of business categories to columns (e.g. {'risk': ['scr', 'bal']}).
|
|
122
121
|
ignore_features: Explicit list of columns to drop prior to IV calculation.
|
|
123
122
|
sort_priority: Ranking criteria for selecting champion segments.
|
|
124
|
-
binning_method:
|
|
125
|
-
|
|
123
|
+
binning_method: Binning engine:
|
|
124
|
+
- "naive": DuckDB equal-frequency quantiles (supports adjacent-bin expansion).
|
|
125
|
+
- "optimal_cart": OptBinning with CART prebinning (target-aware cuts).
|
|
126
|
+
- "optimal_quantile": OptBinning with quantile prebinning (more stable).
|
|
127
|
+
- "optimal": alias for "optimal_cart" (backward compatible).
|
|
128
|
+
naive_bins: Number of quantile bins when binning_method is "naive".
|
|
126
129
|
selection_metric: Metric used to rank features for top_n_vars selection ("iv" or "response_rate").
|
|
127
130
|
max_expansion_hops: Adjacent-bin merging hop distance limit (0 disables expansion).
|
|
128
131
|
expand_log_mode: Controls verbosity of adjacent-bin expansion logging ("none", "summary", "champion", "full").
|
|
@@ -159,6 +162,16 @@ class StrategicSegmentBuilder:
|
|
|
159
162
|
self.diagnostics_: List[Dict[str, Any]] = []
|
|
160
163
|
self.stop_reason: Optional[str] = None
|
|
161
164
|
self.binning_method = binning_method
|
|
165
|
+
# Normalize aliases
|
|
166
|
+
_bm = (binning_method or "optimal_cart").lower().strip()
|
|
167
|
+
if _bm == "optimal":
|
|
168
|
+
_bm = "optimal_cart"
|
|
169
|
+
if _bm not in ("naive", "optimal_cart", "optimal_quantile"):
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"binning_method must be one of 'naive', 'optimal_cart', "
|
|
172
|
+
f"'optimal_quantile', or 'optimal' (alias of cart); got {_bm!r}"
|
|
173
|
+
)
|
|
174
|
+
self.binning_method = _bm
|
|
162
175
|
self.naive_bins = naive_bins
|
|
163
176
|
self.max_expansion_hops = max(0, int(max_expansion_hops))
|
|
164
177
|
self.selection_metric = selection_metric
|
|
@@ -230,35 +243,42 @@ class StrategicSegmentBuilder:
|
|
|
230
243
|
"""
|
|
231
244
|
priority = self.sort_priority
|
|
232
245
|
if priority == "lift_count_rate":
|
|
233
|
-
|
|
246
|
+
key = (rule["lift"], rule["count"], rule["rate"])
|
|
234
247
|
elif priority == "count_lift_rate":
|
|
235
|
-
|
|
248
|
+
key = (rule["count"], rule["lift"], rule["rate"])
|
|
236
249
|
elif priority == "rate_lift_count":
|
|
237
|
-
|
|
250
|
+
key = (rule["rate"], rule["lift"], rule["count"])
|
|
238
251
|
elif priority == "lift_rate_count":
|
|
239
|
-
|
|
252
|
+
key = (rule["lift"], rule["rate"], rule["count"])
|
|
240
253
|
elif priority == "count_rate_lift":
|
|
241
|
-
|
|
254
|
+
key = (rule["count"], rule["rate"], rule["lift"])
|
|
242
255
|
elif priority == "rate_count_lift":
|
|
243
|
-
|
|
256
|
+
key = (rule["rate"], rule["count"], rule["lift"])
|
|
244
257
|
elif priority == "events_lift_rate":
|
|
245
|
-
|
|
258
|
+
key = (rule["events"], rule["lift"], rule["rate"])
|
|
246
259
|
elif priority == "events_rate_lift":
|
|
247
|
-
|
|
260
|
+
key = (rule["events"], rule["rate"], rule["lift"])
|
|
248
261
|
elif priority == "lift_events_rate":
|
|
249
|
-
|
|
262
|
+
key = (rule["lift"], rule["events"], rule["rate"])
|
|
250
263
|
elif priority == "rate_events_lift":
|
|
251
|
-
|
|
264
|
+
key = (rule["rate"], rule["events"], rule["lift"])
|
|
252
265
|
elif priority == "events_count_rate":
|
|
253
|
-
|
|
266
|
+
key = (rule["events"], rule["count"], rule["rate"])
|
|
254
267
|
elif priority == "events_rate_count":
|
|
255
|
-
|
|
268
|
+
key = (rule["events"], rule["rate"], rule["count"])
|
|
256
269
|
elif priority == "count_events_rate":
|
|
257
|
-
|
|
270
|
+
key = (rule["count"], rule["events"], rule["rate"])
|
|
258
271
|
elif priority == "rate_events_count":
|
|
259
|
-
|
|
272
|
+
key = (rule["rate"], rule["events"], rule["count"])
|
|
260
273
|
else:
|
|
261
|
-
|
|
274
|
+
key = (rule["lift"], rule["rate"], rule["count"])
|
|
275
|
+
# Deterministic tie-breaker: when every configured priority dimension
|
|
276
|
+
# is exactly equal between two candidates, the winner must not depend
|
|
277
|
+
# on incoming list order (which itself can vary run-to-run due to
|
|
278
|
+
# Python's per-process string hash randomization affecting set/dict
|
|
279
|
+
# iteration order, and unordered SQL GROUP BY results). Sorting on
|
|
280
|
+
# the rule string last makes ties resolve identically every run.
|
|
281
|
+
return key + (rule.get("rule", ""),)
|
|
262
282
|
|
|
263
283
|
def compute_iv_ranking_and_bin(
|
|
264
284
|
self,
|
|
@@ -380,14 +400,25 @@ class StrategicSegmentBuilder:
|
|
|
380
400
|
# Transform bins array in DuckDB
|
|
381
401
|
transformed_bins = thread_con.execute(
|
|
382
402
|
f"""
|
|
383
|
-
SELECT
|
|
403
|
+
SELECT
|
|
404
|
+
"__rs_row_id",
|
|
405
|
+
{case_expr} AS bin_label
|
|
406
|
+
FROM current_df
|
|
407
|
+
ORDER BY "__rs_row_id"
|
|
384
408
|
"""
|
|
385
409
|
).fetchnumpy()["bin_label"].astype(str)
|
|
386
410
|
|
|
387
411
|
else:
|
|
388
412
|
# Optimal binning fallback
|
|
389
413
|
data_dict = thread_con.execute(
|
|
390
|
-
f'
|
|
414
|
+
f'''
|
|
415
|
+
SELECT
|
|
416
|
+
"__rs_row_id",
|
|
417
|
+
"{col}",
|
|
418
|
+
"{self.target}"
|
|
419
|
+
FROM current_df
|
|
420
|
+
ORDER BY "__rs_row_id"
|
|
421
|
+
'''
|
|
391
422
|
).fetchnumpy()
|
|
392
423
|
|
|
393
424
|
col_arr_raw = data_dict[col]
|
|
@@ -396,10 +427,48 @@ class StrategicSegmentBuilder:
|
|
|
396
427
|
col_arr = col_arr_raw.filled(np.nan if np.issubdtype(col_arr_raw.dtype, np.number) else None) if isinstance(col_arr_raw, np.ma.MaskedArray) else col_arr_raw
|
|
397
428
|
target_arr = target_arr_raw.filled(0) if isinstance(target_arr_raw, np.ma.MaskedArray) else target_arr_raw
|
|
398
429
|
|
|
430
|
+
# Deterministic row order before CART prebinning.
|
|
431
|
+
# DuckDB SELECT has no ORDER BY; OptBinning's default prebinning
|
|
432
|
+
# (CART) can split differently when tied samples arrive in a
|
|
433
|
+
# different order, which changes bins/IV and cascades into
|
|
434
|
+
# different segments across runs.
|
|
435
|
+
#
|
|
436
|
+
# IMPORTANT: only the *fitting* input is sorted. The transform
|
|
437
|
+
# below must run on the ORIGINAL (unsorted) arrays so the
|
|
438
|
+
# returned bin labels stay aligned with "__rs_row_id" / the
|
|
439
|
+
# row order of current_df. Otherwise binned_df pairs the wrong
|
|
440
|
+
# target values with the wrong bin labels, corrupting both the
|
|
441
|
+
# event counts of 1-way rules and the row alignment of 2/3-way
|
|
442
|
+
# rules (which is what produced spurious candidates whose SQL
|
|
443
|
+
# re-validation then failed against the real table).
|
|
444
|
+
try:
|
|
445
|
+
col_arr = np.asarray(col_arr)
|
|
446
|
+
target_arr = np.asarray(target_arr)
|
|
447
|
+
if np.issubdtype(col_arr.dtype, np.number):
|
|
448
|
+
order = np.lexsort((
|
|
449
|
+
target_arr,
|
|
450
|
+
np.nan_to_num(col_arr.astype(float, copy=False), nan=np.inf),
|
|
451
|
+
))
|
|
452
|
+
else:
|
|
453
|
+
col_as_str = col_arr.astype(str)
|
|
454
|
+
order = np.lexsort((target_arr, col_as_str))
|
|
455
|
+
col_fit = col_arr[order]
|
|
456
|
+
target_fit = target_arr[order]
|
|
457
|
+
except Exception:
|
|
458
|
+
col_fit = col_arr
|
|
459
|
+
target_fit = target_arr
|
|
460
|
+
|
|
399
461
|
# Fit an Optimal Binning model to the current feature/target pair so we can
|
|
400
462
|
# create monotonic, high-signal bins without relying on a fixed quantile grid.
|
|
401
|
-
|
|
402
|
-
|
|
463
|
+
prebinning_method = (
|
|
464
|
+
"quantile" if self.binning_method == "optimal_quantile" else "cart"
|
|
465
|
+
)
|
|
466
|
+
optb = OptimalBinning(
|
|
467
|
+
name=col,
|
|
468
|
+
dtype=dtype,
|
|
469
|
+
prebinning_method=prebinning_method,
|
|
470
|
+
)
|
|
471
|
+
optb.fit(col_fit, target_fit)
|
|
403
472
|
|
|
404
473
|
bin_table = optb.binning_table.build()
|
|
405
474
|
iv_val = float(bin_table["IV"].values[-1])
|
|
@@ -469,11 +538,12 @@ class StrategicSegmentBuilder:
|
|
|
469
538
|
ranking.append({"variable": col, "iv": iv, "max_rr": rr})
|
|
470
539
|
if bins is not None:
|
|
471
540
|
precomputed_bins[col] = bins
|
|
472
|
-
|
|
541
|
+
# Stable sort: primary metric descending, variable name ascending on ties
|
|
542
|
+
# so top_n_vars is identical across runs when scores collide.
|
|
473
543
|
if self.selection_metric == "response_rate":
|
|
474
|
-
ranking.sort(key=lambda x: x["max_rr"],
|
|
544
|
+
ranking.sort(key=lambda x: (-x["max_rr"], x["variable"]))
|
|
475
545
|
else:
|
|
476
|
-
ranking.sort(key=lambda x: x["iv"],
|
|
546
|
+
ranking.sort(key=lambda x: (-x["iv"], x["variable"]))
|
|
477
547
|
|
|
478
548
|
return ranking, precomputed_bins
|
|
479
549
|
|
|
@@ -834,6 +904,11 @@ class StrategicSegmentBuilder:
|
|
|
834
904
|
parts = [p.strip() for p in rule_str.split("&")]
|
|
835
905
|
sql_conditions: List[str] = []
|
|
836
906
|
|
|
907
|
+
def _quote_sql_ident(ident: str) -> str:
|
|
908
|
+
# Quote every emitted column identifier so DuckDB reserved words
|
|
909
|
+
# (e.g. "default") and exotic column names do not break the predicate.
|
|
910
|
+
return '"' + str(ident).replace('"', '""') + '"'
|
|
911
|
+
|
|
837
912
|
def _quote_sql_string(val: Any) -> str:
|
|
838
913
|
txt = str(val)
|
|
839
914
|
# escape single quotes for SQL and wrap in single quotes
|
|
@@ -908,10 +983,10 @@ class StrategicSegmentBuilder:
|
|
|
908
983
|
range_conds = []
|
|
909
984
|
if overall_lower.lower() != "-inf":
|
|
910
985
|
op = ">=" if overall_lower_char == "[" else ">"
|
|
911
|
-
range_conds.append(f"{col} {op} {overall_lower}")
|
|
986
|
+
range_conds.append(f"{_quote_sql_ident(col)} {op} {overall_lower}")
|
|
912
987
|
if overall_upper.lower() != "inf":
|
|
913
988
|
op = "<=" if overall_upper_char == "]" else "<"
|
|
914
|
-
range_conds.append(f"{col} {op} {overall_upper}")
|
|
989
|
+
range_conds.append(f"{_quote_sql_ident(col)} {op} {overall_upper}")
|
|
915
990
|
|
|
916
991
|
if range_conds:
|
|
917
992
|
sql_conditions.append(" AND ".join(range_conds))
|
|
@@ -932,13 +1007,13 @@ class StrategicSegmentBuilder:
|
|
|
932
1007
|
)
|
|
933
1008
|
if is_single_nested:
|
|
934
1009
|
cleaned = _strip_wrapping_quotes(interval[1:-1])
|
|
935
|
-
sql_conditions.append(f"{col} = {_quote_sql_string(cleaned)}")
|
|
1010
|
+
sql_conditions.append(f"{_quote_sql_ident(col)} = {_quote_sql_string(cleaned)}")
|
|
936
1011
|
continue
|
|
937
1012
|
if cat_tokens:
|
|
938
1013
|
cleaned_items = [_strip_wrapping_quotes(t) for t in cat_tokens]
|
|
939
1014
|
formatted_items = ", ".join(_quote_sql_string(item) for item in cleaned_items if item)
|
|
940
1015
|
if formatted_items:
|
|
941
|
-
sql_conditions.append(f"{col} IN ({formatted_items})")
|
|
1016
|
+
sql_conditions.append(f"{_quote_sql_ident(col)} IN ({formatted_items})")
|
|
942
1017
|
continue
|
|
943
1018
|
|
|
944
1019
|
# 2. Explicit Categoricals
|
|
@@ -988,12 +1063,12 @@ class StrategicSegmentBuilder:
|
|
|
988
1063
|
]
|
|
989
1064
|
)
|
|
990
1065
|
if formatted_items:
|
|
991
|
-
sql_conditions.append(f"{col} IN ({formatted_items})")
|
|
1066
|
+
sql_conditions.append(f"{_quote_sql_ident(col)} IN ({formatted_items})")
|
|
992
1067
|
continue
|
|
993
|
-
|
|
1068
|
+
|
|
994
1069
|
# 3. Special or Missing values
|
|
995
1070
|
if interval in ["Special", "Missing"]:
|
|
996
|
-
sql_conditions.append(f"{col} IS NULL")
|
|
1071
|
+
sql_conditions.append(f"{_quote_sql_ident(col)} IS NULL")
|
|
997
1072
|
continue
|
|
998
1073
|
|
|
999
1074
|
# 4. Standard Intervals [lo, hi) or Single Bracket Values [val]
|
|
@@ -1005,7 +1080,7 @@ class StrategicSegmentBuilder:
|
|
|
1005
1080
|
raw_items = [_strip_wrapping_quotes(x) for x in inner.split(",") if x.strip()]
|
|
1006
1081
|
formatted_items = ", ".join(_quote_sql_string(item) for item in raw_items)
|
|
1007
1082
|
if formatted_items:
|
|
1008
|
-
sql_conditions.append(f"{col} IN ({formatted_items})")
|
|
1083
|
+
sql_conditions.append(f"{_quote_sql_ident(col)} IN ({formatted_items})")
|
|
1009
1084
|
continue
|
|
1010
1085
|
left_char, right_char = interval[0], interval[-1]
|
|
1011
1086
|
lower_str, upper_str = [x.strip() for x in inner.split(",", 1)]
|
|
@@ -1013,10 +1088,10 @@ class StrategicSegmentBuilder:
|
|
|
1013
1088
|
range_conds = []
|
|
1014
1089
|
if lower_str.lower() != "-inf":
|
|
1015
1090
|
op = ">=" if left_char == "[" else ">"
|
|
1016
|
-
range_conds.append(f"{col} {op} {lower_str}")
|
|
1091
|
+
range_conds.append(f"{_quote_sql_ident(col)} {op} {lower_str}")
|
|
1017
1092
|
if upper_str.lower() != "inf":
|
|
1018
1093
|
op = "<=" if right_char == "]" else "<"
|
|
1019
|
-
range_conds.append(f"{col} {op} {upper_str}")
|
|
1094
|
+
range_conds.append(f"{_quote_sql_ident(col)} {op} {upper_str}")
|
|
1020
1095
|
|
|
1021
1096
|
if range_conds:
|
|
1022
1097
|
sql_conditions.append(" AND ".join(range_conds))
|
|
@@ -1025,11 +1100,11 @@ class StrategicSegmentBuilder:
|
|
|
1025
1100
|
clean_val = _strip_wrapping_quotes(inner)
|
|
1026
1101
|
|
|
1027
1102
|
if col_is_categorical:
|
|
1028
|
-
sql_conditions.append(f"{col} = {_quote_sql_string(clean_val)}")
|
|
1103
|
+
sql_conditions.append(f"{_quote_sql_ident(col)} = {_quote_sql_string(clean_val)}")
|
|
1029
1104
|
elif clean_val.replace(".", "", 1).replace("-", "", 1).isdigit():
|
|
1030
|
-
sql_conditions.append(f"{col} = {clean_val}")
|
|
1105
|
+
sql_conditions.append(f"{_quote_sql_ident(col)} = {clean_val}")
|
|
1031
1106
|
else:
|
|
1032
|
-
sql_conditions.append(f"{col} = {_quote_sql_string(clean_val)}")
|
|
1107
|
+
sql_conditions.append(f"{_quote_sql_ident(col)} = {_quote_sql_string(clean_val)}")
|
|
1033
1108
|
|
|
1034
1109
|
return " AND ".join(f"({cond})" for cond in sql_conditions)
|
|
1035
1110
|
|
|
@@ -1069,14 +1144,17 @@ class StrategicSegmentBuilder:
|
|
|
1069
1144
|
con.execute(f"SET memory_limit = '{target_memory_gb}GB';")
|
|
1070
1145
|
if db_temp_dir:
|
|
1071
1146
|
con.execute(f"PRAGMA temp_directory='{db_temp_dir}';")
|
|
1072
|
-
con.execute("SET preserve_insertion_order = false;")
|
|
1147
|
+
# con.execute("SET preserve_insertion_order = false;")
|
|
1073
1148
|
|
|
1074
1149
|
logger.info(
|
|
1075
1150
|
f"⚙️ DuckDB Configured for Disk Spilling: Threads={target_threads}/{total_cores}, "
|
|
1076
1151
|
f"MemoryLimit={target_memory_gb}GB, TempDir={self.db_temp_dir}"
|
|
1077
1152
|
)
|
|
1078
1153
|
logger.info(f"📊 Sort priority: {self.sort_priority}")
|
|
1079
|
-
logger.info(
|
|
1154
|
+
logger.info(
|
|
1155
|
+
f"📦 Binning method: {self.binning_method}"
|
|
1156
|
+
+ (f" (naive_bins={self.naive_bins})" if self.binning_method == "naive" else "")
|
|
1157
|
+
)
|
|
1080
1158
|
try:
|
|
1081
1159
|
con.register("input_data_view", data)
|
|
1082
1160
|
# Cast the target to DOUBLE up front so boolean targets work with AVG,
|
|
@@ -1089,9 +1167,15 @@ class StrategicSegmentBuilder:
|
|
|
1089
1167
|
f"Available columns: {sorted(probe_cols)}"
|
|
1090
1168
|
)
|
|
1091
1169
|
con.execute(
|
|
1092
|
-
f'
|
|
1093
|
-
|
|
1094
|
-
|
|
1170
|
+
f'''
|
|
1171
|
+
CREATE OR REPLACE TABLE current_df AS
|
|
1172
|
+
SELECT
|
|
1173
|
+
ROW_NUMBER() OVER () AS "__rs_row_id",
|
|
1174
|
+
* REPLACE (
|
|
1175
|
+
CAST("{self.target}" AS DOUBLE) AS "{self.target}"
|
|
1176
|
+
)
|
|
1177
|
+
FROM input_data_view
|
|
1178
|
+
'''
|
|
1095
1179
|
)
|
|
1096
1180
|
|
|
1097
1181
|
cols_info = con.execute("DESCRIBE current_df").fetchall()
|
|
@@ -1107,9 +1191,15 @@ class StrategicSegmentBuilder:
|
|
|
1107
1191
|
if self.enable_diversity:
|
|
1108
1192
|
self._validate_feature_groups(all_cols)
|
|
1109
1193
|
|
|
1110
|
-
eligible_cols =
|
|
1111
|
-
c
|
|
1112
|
-
|
|
1194
|
+
eligible_cols = sorted(
|
|
1195
|
+
c
|
|
1196
|
+
for c in all_cols
|
|
1197
|
+
if (
|
|
1198
|
+
c != self.target
|
|
1199
|
+
and c != "__rs_row_id"
|
|
1200
|
+
and c not in self.ignore_features
|
|
1201
|
+
)
|
|
1202
|
+
)
|
|
1113
1203
|
self.feature_usage_counts = {col: 0 for col in eligible_cols}
|
|
1114
1204
|
|
|
1115
1205
|
if self.param_grid:
|
|
@@ -1211,7 +1301,11 @@ class StrategicSegmentBuilder:
|
|
|
1211
1301
|
break
|
|
1212
1302
|
|
|
1213
1303
|
raw_target_arr = con.execute(
|
|
1214
|
-
f'
|
|
1304
|
+
f'''
|
|
1305
|
+
SELECT "{self.target}"
|
|
1306
|
+
FROM current_df
|
|
1307
|
+
ORDER BY "__rs_row_id"
|
|
1308
|
+
'''
|
|
1215
1309
|
).fetchnumpy()[self.target]
|
|
1216
1310
|
|
|
1217
1311
|
if isinstance(raw_target_arr, np.ma.MaskedArray):
|
|
@@ -1231,14 +1325,16 @@ class StrategicSegmentBuilder:
|
|
|
1231
1325
|
break
|
|
1232
1326
|
|
|
1233
1327
|
con.execute("DROP TABLE IF EXISTS binned_df")
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1328
|
+
|
|
1329
|
+
con.register("binned_data_view", binned_data)
|
|
1330
|
+
|
|
1331
|
+
con.execute("""
|
|
1332
|
+
CREATE TABLE binned_df AS
|
|
1333
|
+
SELECT *
|
|
1334
|
+
FROM binned_data_view
|
|
1335
|
+
""")
|
|
1336
|
+
|
|
1337
|
+
con.unregister("binned_data_view")
|
|
1242
1338
|
|
|
1243
1339
|
global_min_sample = (
|
|
1244
1340
|
min(exp["min_sample_size"] for exp in experiments)
|
|
@@ -1269,7 +1365,7 @@ class StrategicSegmentBuilder:
|
|
|
1269
1365
|
# Level 2 (Pairs)
|
|
1270
1366
|
valid_2way_sets = set()
|
|
1271
1367
|
if len(valid_1way_vars) >= 2 and (self.enable_2way or self.enable_3way):
|
|
1272
|
-
combos_2 = [c for c in combinations(valid_1way_vars, 2) if self.is_diverse(c)]
|
|
1368
|
+
combos_2 = [c for c in combinations(sorted(valid_1way_vars), 2) if self.is_diverse(c)]
|
|
1273
1369
|
if combos_2:
|
|
1274
1370
|
res_2 = self._agg_combinations(con, combos_2, original_base_rate)
|
|
1275
1371
|
if res_2:
|
|
@@ -1280,7 +1376,7 @@ class StrategicSegmentBuilder:
|
|
|
1280
1376
|
# Level 3 (Triplets)
|
|
1281
1377
|
if self.enable_3way and len(valid_1way_vars) >= 3 and valid_2way_sets:
|
|
1282
1378
|
combos_3 = [
|
|
1283
|
-
c for c in combinations(valid_1way_vars, 3)
|
|
1379
|
+
c for c in combinations(sorted(valid_1way_vars), 3)
|
|
1284
1380
|
if self.is_diverse(c) and all(frozenset(p) in valid_2way_sets for p in combinations(c, 2))
|
|
1285
1381
|
]
|
|
1286
1382
|
if combos_3:
|
|
@@ -1305,6 +1401,10 @@ class StrategicSegmentBuilder:
|
|
|
1305
1401
|
grid_candidates.append(top_match)
|
|
1306
1402
|
|
|
1307
1403
|
if not grid_candidates:
|
|
1404
|
+
self.diagnostics_[-1]["candidate_funnel"] = {
|
|
1405
|
+
"total_candidates_before_grid": len(all_candidate_rules),
|
|
1406
|
+
"candidates_after_grid": 0,
|
|
1407
|
+
}
|
|
1308
1408
|
self.stop_reason = "No candidate rules met the minimum grid thresholds (sample size / lift)."
|
|
1309
1409
|
logger.info("⏹️ No candidates cleared the grid. Stopping.")
|
|
1310
1410
|
break
|
|
@@ -1864,4 +1964,4 @@ class StrategicSegmentBuilder:
|
|
|
1864
1964
|
})
|
|
1865
1965
|
|
|
1866
1966
|
health_report_df = pd.DataFrame(rows)
|
|
1867
|
-
return health_report_df
|
|
1967
|
+
return health_report_df
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rapidsegment-1.2.1 → rapidsegment-1.2.2}/src/rapidsegment/utils/on_gcp_feature_selection.py
RENAMED
|
File without changes
|
|
File without changes
|