rapidsegment 1.2.7__tar.gz → 1.2.8__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.7 → rapidsegment-1.2.8}/PKG-INFO +118 -41
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/README.md +117 -40
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/pyproject.toml +1 -1
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/pyproject.toml.orig +1 -1
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/builder.py +270 -73
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/scorer.py +36 -8
- rapidsegment-1.2.8/src/rapidsegment/ui/.streamlit/config.toml +3 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/1_Data_Loader.py +106 -33
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/2_Workbench.py +964 -143
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/3_Execution_Console.py +23 -27
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/4_Results_Dashboard.py +64 -44
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/5_Leaderboard.py +1 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/6_Arena.py +5 -2
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/utils/data_loader.py +57 -1
- rapidsegment-1.2.7/src/rapidsegment/ui/.streamlit/config.toml +0 -3
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/LICENSE +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/__init__.py +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/py.typed +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/__init__.py +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/_exit.py +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/_theme.py +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/app.py +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/__init__.py +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/utils/__init__.py +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/utils/on_gcp_feature_selection.py +0 -0
- {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/utils/undersampler.sql +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rapidsegment
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.8
|
|
4
4
|
Summary: A fast, multi-segment population scoring and combinatorial heuristic rule extraction engine.
|
|
5
5
|
Author: Bishwarup Biswas
|
|
6
6
|
Author-email: Bishwarup Biswas <bishwarup1429@gmail.com>
|
|
@@ -47,7 +47,7 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
# 🚀 RapidSegment – Strategic Segmentation & Scorecard Engine
|
|
48
48
|
|
|
49
49
|
[](https://pypi.org/project/rapidsegment/)
|
|
50
|
-
[](https://www.python.org/downloads/)
|
|
51
51
|
[](https://opensource.org/licenses/MIT)
|
|
52
52
|
|
|
53
53
|
**RapidSegment** is an industrial‑grade, combinatorial heuristic engine for discovering high‑lift predictive segments and compiling them into transparent, production‑ready scorecards. It bridges the gap between black‑box ML and legacy SQL rules engines.
|
|
@@ -64,6 +64,7 @@ Description-Content-Type: text/markdown
|
|
|
64
64
|
- [⚙️ How It Works – Step by Step](#️-how-it-works--step-by-step)
|
|
65
65
|
- [📊 Statistical Foundations](#-statistical-foundations)
|
|
66
66
|
- [🔧 Configuration Reference](#-configuration-reference)
|
|
67
|
+
- [🔋 Single Data Artifact & Memory Efficiency](#-single-data-artifact--memory-efficiency)
|
|
67
68
|
- [🤔 FAQs & Troubleshooting](#-faqs--troubleshooting)
|
|
68
69
|
- [🤝 Contributing](#-contributing)
|
|
69
70
|
- [📄 License](#-license)
|
|
@@ -72,14 +73,16 @@ Description-Content-Type: text/markdown
|
|
|
72
73
|
## ✨ Features
|
|
73
74
|
|
|
74
75
|
- **🔎 Automated Rule Discovery** – Uses Optimal Binning (or fast naive quantile binning) + Apriori pruning to find multi‑way (1‑, 2‑, 3‑way) conditions that maximise lift and volume.
|
|
75
|
-
- **🧩 Hierarchical Segments** – Extracts mutually exclusive rules sequentially on a shrinking residual
|
|
76
|
+
- **🧩 Hierarchical Segments** – Extracts mutually exclusive rules sequentially on a shrinking residual population, ensuring clean portfolio decomposition.
|
|
76
77
|
- **🔀 Adjacent-Bin Expansion** – Optionally merges neighbouring bins (`max_expansion_hops`) to recover higher-event rules that pure single-bin candidates miss.
|
|
77
78
|
- **⚡ Hyper‑Efficient & Out-of-Core** – Leverages **DuckDB** (disk-backed by default) for vectorised SQL aggregations; spills to disk so large datasets fit in limited RAM.
|
|
79
|
+
- **🔋 Single Data Artifact (opt-in)** – With `persist_db=True`, extraction keeps one DuckDB file for evaluate / health / score reuse; residual rows are flagged in place (`__rs_excluded`) instead of rewriting the full table each iteration.
|
|
80
|
+
- **📉 Two-Phase Binning** – Fits IV ranking across all eligible features, then materialises full-length bin-label arrays only for `top_n_vars`, bounding peak Python memory.
|
|
81
|
+
- **📁 Zero-Copy File Path** – Pass a path to an existing DuckDB file (table `udl_data` for the builder, `df` for the scorer) so data never has to be pulled into a Python frame.
|
|
78
82
|
- **☁️ BigQuery Ready** – Optional feature screening runs natively inside Google BigQuery, downloading only the most predictive columns.
|
|
79
83
|
- **📦 Production‑Ready Outputs** – Exports pure ANSI SQL filters and a JSON scorecard with decile thresholds, ready for deployment.
|
|
80
84
|
- **📊 Transparent Weighting** – Uses the segment response rate to compute intuitive integer weights, while retaining lift, response rate, and capture rate for each segment.
|
|
81
85
|
- **🔬 Full Audit Trail** – `explain_feature_journey`, `explain_no_segments`, and `generate_feature_health_report` for complete diagnostics.
|
|
82
|
-
|
|
83
86
|
---
|
|
84
87
|
|
|
85
88
|
## 🌳 Decision Trees vs RapidSegment
|
|
@@ -101,6 +104,21 @@ Decision trees and RapidSegment both aim to create interpretable segments, but t
|
|
|
101
104
|
- The output is already aligned with SQL and scorecard workflows, reducing the gap between modeling and deployment.
|
|
102
105
|
- It is easier to reason about when you want stable, reusable segmentation logic rather than a branching tree structure.
|
|
103
106
|
- It is well suited for scenarios where you want a fixed set of business rules, consistent segment definitions, and explainable weights.
|
|
107
|
+
|
|
108
|
+
#### Empirical 1v1: Decision Tree vs RapidSegment
|
|
109
|
+
|
|
110
|
+
See the full showdown notebook:
|
|
111
|
+
**[DT vs RS comparison](https://github.com/B2BDA/RapidSegment/blob/main/Examples/compare_segmenters_general.ipynb)**
|
|
112
|
+
|
|
113
|
+
On matched runs with similar population and event capture:
|
|
114
|
+
|
|
115
|
+
| Pattern | Decision tree | RapidSegment |
|
|
116
|
+
|--------|----------------|--------------|
|
|
117
|
+
| Lift / response rate | Highly variable across segments | More stable segment to segment |
|
|
118
|
+
| Segment size | Often large (many customers per rule) | Typically tighter (on the order of ~1K rows per segment in this study) |
|
|
119
|
+
| Trade-off | High volume can inflate false positives | Lower volume per segment, steadier response rates |
|
|
120
|
+
|
|
121
|
+
**Takeaway:** Similar overall capture does not imply similar segment quality. Trees tend to grow broad leaves that pull in more non-events; RapidSegment favors smaller, higher-signal rules with more consistent lift and response rate.
|
|
104
122
|
|
|
105
123
|
In short, a decision tree is great when you want a predictive model structure; RapidSegment is better when you want transparent, deployable segments that are easy to understand and operationalize.
|
|
106
124
|
|
|
@@ -284,47 +302,47 @@ config:
|
|
|
284
302
|
theme: dark
|
|
285
303
|
---
|
|
286
304
|
flowchart TD
|
|
287
|
-
A["Original Data
|
|
288
|
-
--> B["Create
|
|
305
|
+
A["Original Data - input to extract_segments"]
|
|
306
|
+
--> B["Create current_df_base + __rs_row_id, target to DOUBLE, __rs_excluded = FALSE"]
|
|
307
|
+
|
|
308
|
+
B --> V["current_df = VIEW WHERE __rs_excluded IS NOT TRUE"]
|
|
289
309
|
|
|
290
|
-
|
|
310
|
+
V --> C{"For each segment i = 1 to max_segments"}
|
|
291
311
|
|
|
292
|
-
C --> D[Compute base_rate
|
|
312
|
+
C --> D["Compute base_rate and volume on residual current_df"]
|
|
293
313
|
|
|
294
|
-
D --> E[IV Ranking +
|
|
314
|
+
D --> E["IV Ranking + Binning: Phase 1 fit all eligible, Phase 2 transform top_n_vars only"]
|
|
295
315
|
|
|
296
|
-
E --> F[Build binned_df
|
|
316
|
+
E --> F["Build binned_df from top_n bin labels"]
|
|
297
317
|
|
|
298
|
-
F --> G[Generate
|
|
318
|
+
F --> G["Generate candidates 1-way, 2-way, 3-way on binned_df"]
|
|
299
319
|
|
|
300
|
-
G --> H[Select best candidate
|
|
320
|
+
G --> H["Select best candidate by sort_priority"]
|
|
301
321
|
|
|
302
|
-
H --> I["Parse rule
|
|
322
|
+
H --> I["Parse rule to SQL filter via parse_rule_to_sql"]
|
|
303
323
|
|
|
304
|
-
I --> J[Validate on
|
|
324
|
+
I --> J["Validate on residual current_df: COUNT + SUM WHERE sql_filter"]
|
|
305
325
|
|
|
306
|
-
J --> K{Meets min_sample_size
|
|
326
|
+
J --> K{"Meets min_sample_size, min_events, and min_lift?"}
|
|
307
327
|
|
|
308
|
-
K -- No --> L[Reject
|
|
328
|
+
K -- No --> L["Reject and try next candidate, or stop"]
|
|
309
329
|
L --> C
|
|
310
330
|
|
|
311
|
-
K -- Yes --> M[Store
|
|
331
|
+
K -- Yes --> M["Store segment with actual counts from residual"]
|
|
312
332
|
|
|
313
|
-
M --> N[Update
|
|
333
|
+
M --> N["Update feature usage tracker"]
|
|
314
334
|
|
|
315
|
-
N --> O["
|
|
335
|
+
N --> O["In-place residual update: SET __rs_excluded = TRUE WHERE sql_filter IS TRUE"]
|
|
316
336
|
|
|
317
|
-
O --> P[current_df
|
|
337
|
+
O --> P["current_df view shrinks - no full-table rewrite"]
|
|
318
338
|
|
|
319
339
|
P --> C
|
|
320
340
|
|
|
321
|
-
C --> Q[End
|
|
341
|
+
C --> Q["End loop"]
|
|
322
342
|
|
|
323
|
-
Q --> R[Return self.segments
|
|
343
|
+
Q --> R["Return self.segments hierarchical rules"]
|
|
324
344
|
|
|
325
|
-
|
|
326
|
-
R --> S[evaluate_final_coverage\nCASE WHEN on Original Data]
|
|
327
|
-
end
|
|
345
|
+
R --> S["evaluate_final_coverage: CASE WHEN on original population"]
|
|
328
346
|
```
|
|
329
347
|
## ⚙️ How It Works – Step by Step
|
|
330
348
|
|
|
@@ -429,12 +447,15 @@ For each iteration, the engine sweeps over a user‑defined grid of `(min_sample
|
|
|
429
447
|
### 4. Champion Validation & Extraction
|
|
430
448
|
The champion’s SQL filter is validated against the **raw residual** to ensure it meets the absolute hard constraints. Only then is it accepted.
|
|
431
449
|
|
|
432
|
-
### 5. Residual Update (NULL‑safe)
|
|
433
|
-
|
|
450
|
+
### 5. Residual Update (NULL‑safe, in-place)
|
|
451
|
+
|
|
452
|
+
Matched residual rows are **excluded in place** on the base table — the engine does **not** rewrite a new residual table each iteration:
|
|
453
|
+
This guarantees that the residual dataset exactly matches the `CASE`‑based hierarchical segmentation used in `evaluate_final_coverage`.
|
|
434
454
|
```sql
|
|
435
|
-
|
|
455
|
+
UPDATE current_df_base
|
|
456
|
+
SET "__rs_excluded" = TRUE
|
|
457
|
+
WHERE ({rule}) IS TRUE
|
|
436
458
|
```
|
|
437
|
-
This guarantees that the residual dataset exactly matches the `CASE`‑based hierarchical segmentation used in `evaluate_final_coverage`.
|
|
438
459
|
|
|
439
460
|
### 6. Loop
|
|
440
461
|
Steps 1‑5 repeat until either `max_segments` is reached or no more rules can be found.
|
|
@@ -486,7 +507,7 @@ Scores are computed as the sum of weights for all segments a customer triggers.
|
|
|
486
507
|
| `min_sample_size` | `int` | `1000` | Absolute minimum rows for a valid rule. |
|
|
487
508
|
| `min_lift` | `float` | `1.5` | Absolute minimum lift (hard constraint). |
|
|
488
509
|
| `min_events` | `int` | `100` | Minimum positive events for a valid rule. |
|
|
489
|
-
| `top_n_vars` | `int` | `15` | Number of top features passed to the Apriori engine. |
|
|
510
|
+
| `top_n_vars` | `int` | `15` | Number of top features passed to the Apriori engine (and for which full bin-label arrays are materialised). |
|
|
490
511
|
| `max_segments` | `int` | `10` | Maximum segments to extract. |
|
|
491
512
|
| `max_feature_reuse` | `int` | `1` | Max times any single feature may appear across segments. |
|
|
492
513
|
| `param_grid` | `dict` | `{}` | Optional grid of `{min_sample_size, min_lift}` to sweep. |
|
|
@@ -495,32 +516,85 @@ Scores are computed as the sum of weights for all segments a customer triggers.
|
|
|
495
516
|
| `feature_groups` | `dict` | `{}` | Business-category → column list (used by diversity). |
|
|
496
517
|
| `ignore_features` | `list` | `[]` | Columns to exclude before IV calculation. |
|
|
497
518
|
| `sort_priority` | `str` | `"rate_lift_count"` | Ranking key for champion selection (many variants supported). |
|
|
498
|
-
| `binning_method` | `str` | `"
|
|
519
|
+
| `binning_method` | `str` | `"optimal_cart"` | `"optimal_cart"` (or alias `"optimal"`), `"optimal_quantile"`, or `"naive"`. |
|
|
499
520
|
| `naive_bins` | `int` | `5` | Number of quantile bins when `binning_method="naive"`. |
|
|
500
521
|
| `max_expansion_hops` | `int` | `0` | Adjacent-bin merge distance (0 = disabled). |
|
|
501
522
|
| `selection_metric` | `str` | `"iv"` | Rank features by `"iv"` or `"response_rate"`. |
|
|
502
523
|
| `expand_log_mode` | `str` | `"none"` | Expansion logging: `"none"` \| `"summary"` \| `"champion"` \| `"full"`. |
|
|
524
|
+
| `memory_limit_gb` | `float` | `None` | DuckDB RAM buffer cap in GB. `None` → ~80% of host RAM. |
|
|
525
|
+
| `engine_threads` | `int` | `None` | DuckDB execution threads. `None` → all-but-two cores (or all cores if ≤4). |
|
|
503
526
|
| `db_path` / `db_temp_dir` | `str` | `None` | Optional explicit DuckDB file + temp dir (auto-created otherwise). |
|
|
527
|
+
| `persist_db` | `bool` | `False` | Keep the auto-created DuckDB artifact after `extract_segments` so evaluate / health / score can share the same file. Requires `close()` or a `with` block to clean up. |
|
|
504
528
|
|
|
505
529
|
**Output** – list of dicts with keys: `segment_id`, `rule_string`, `sql_filter`, `count`, `rate`, `lift`, `meta_applied_sample_size`, `meta_applied_min_lift`.
|
|
506
530
|
|
|
507
531
|
### `StrategicSegmentScore`
|
|
508
532
|
|
|
509
|
-
| Parameter | Type | Description |
|
|
510
|
-
|
|
511
|
-
| `target_col` | `str` | Binary target column. |
|
|
512
|
-
| `primary_key` | `str` | Unique row identifier. |
|
|
513
|
-
| `segment_cols` | `list` | List of binary segment flag columns. |
|
|
533
|
+
| Parameter | Type | Default | Description |
|
|
534
|
+
|-----------|------|---------|-------------|
|
|
535
|
+
| `target_col` | `str` | **Required** | Binary target column. |
|
|
536
|
+
| `primary_key` | `str` | **Required** | Unique row identifier. |
|
|
537
|
+
| `segment_cols` | `list` | **Required** | List of binary segment flag columns. |
|
|
538
|
+
|
|
539
|
+
`calculate_and_export_weights(data, export_path=..., db_path=None)`:
|
|
540
|
+
|
|
541
|
+
| Argument | Type | Default | Description |
|
|
542
|
+
|----------|------|---------|-------------|
|
|
543
|
+
| `data` | any / `str` | **Required** | Frame/table, or path to a DuckDB file containing table `df` (zero-copy attach). |
|
|
544
|
+
| `export_path` | `str` | timestamped JSON | Path for the model artifact. |
|
|
545
|
+
| `db_path` | `str` | `None` | Optional shared DuckDB file (e.g. the builder’s `db_path`). If omitted, a unique temp DB under the system temp dir is created and removed after export. |
|
|
514
546
|
|
|
515
547
|
**Export** – JSON artifact with `model_metadata`, `segment_weights`, and `decile_min_thresholds`.
|
|
516
548
|
|
|
517
549
|
---
|
|
518
550
|
|
|
551
|
+
## 🔋 Single Data Artifact & Memory Efficiency
|
|
552
|
+
|
|
553
|
+
By default RapidSegment materialises the residual workspace into a DuckDB file during `extract_segments` and deletes it when the run finishes. If you chain `extract_segments → evaluate_final_coverage → generate_feature_health_report → StrategicSegmentScore`, set **`persist_db=True`** so the **same DuckDB file** stays alive and later steps can share it instead of each opening a fresh, isolated DB.
|
|
554
|
+
|
|
555
|
+
**What is reused**
|
|
556
|
+
|
|
557
|
+
| Stage | Behaviour with `persist_db=True` |
|
|
558
|
+
|-------|----------------------------------|
|
|
559
|
+
| Residual inside extraction | One base table (`current_df_base`) + in-place `__rs_excluded` flag; `current_df` is a filtered **view** (no full-table rewrite per segment). |
|
|
560
|
+
| Binning memory | Phase 1 fits all eligible features; Phase 2 materialises full-length bin-label arrays only for `top_n_vars`. |
|
|
561
|
+
| Evaluate / health | Connect to the same `db_path`. If `original_df` is not already present in that file, it is materialised **once** from the data you pass (or attached zero-copy when you pass a DuckDB file path). Subsequent calls on the same file can reuse `original_df`. |
|
|
562
|
+
| Scorer | Pass `db_path=b.db_path` (or a path to a file that already contains table `df`) so scoring does not create a separate CWD `score_experiment_*.db`. |
|
|
563
|
+
|
|
564
|
+
**How to use** — prefer the context manager so cleanup is automatic:
|
|
565
|
+
|
|
566
|
+
```python
|
|
567
|
+
from rapidsegment import StrategicSegmentBuilder, StrategicSegmentScore
|
|
568
|
+
|
|
569
|
+
with StrategicSegmentBuilder(target="default_flag", persist_db=True) as b:
|
|
570
|
+
segments = b.extract_segments(data)
|
|
571
|
+
coverage = b.evaluate_final_coverage(data) # same DB file
|
|
572
|
+
health = b.generate_feature_health_report(data, ["age", "balance"])
|
|
573
|
+
|
|
574
|
+
# build seg_N flag columns on your scoring frame, then:
|
|
575
|
+
scorer = StrategicSegmentScore("default_flag", "cust_id", segment_cols)
|
|
576
|
+
scorer.calculate_and_export_weights(scored_df, "model.json", db_path=b.db_path)
|
|
577
|
+
# b.close() runs here → temp DuckDB file + temp dir removed
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Or manage it manually — but you **must** call `b.close()`:
|
|
581
|
+
|
|
582
|
+
```python
|
|
583
|
+
b = StrategicSegmentBuilder(target="default_flag", persist_db=True)
|
|
584
|
+
segments = b.extract_segments(data)
|
|
585
|
+
# ... evaluate / health / score ...
|
|
586
|
+
b.close() # required, otherwise the file lingers
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Leave `persist_db=False` (the default) if you only call `extract_segments` — there is no benefit and you avoid having to clean up.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
519
593
|
## 🤔 FAQs & Troubleshooting
|
|
520
594
|
|
|
521
595
|
**Q: Why are later segments sometimes stronger in lift than earlier ones?**
|
|
522
|
-
A:
|
|
523
|
-
Because of this cascading extraction:
|
|
596
|
+
A: Extraction is sequential and operates on a shrinking residual population. Once a champion rule is discovered, matching residual rows are **flagged out** (`__rs_excluded = TRUE`) before the next iteration.
|
|
597
|
+
Because of this cascading extraction:
|
|
524
598
|
**`Local Optimization`**: The engine optimizes parameters and evaluates candidates based purely on the residual portfolio left behind by previous segments. A rule that yields massive lift on a specific, purified subset of data might look less dominant if it had been evaluated against the noisy baseline of the entire original population.
|
|
525
599
|
**`Changing Base Rates`**: As high-risk or high-performing records are stripped away in early rounds, the baseline event rate of the remaining pool shifts dynamically. This shifting baseline changes the mathematical benchmark for what constitutes a "high-lift" rule during that specific loop. Consequently, when evaluate_final_coverage maps all rules simultaneously back over the original, unfiltered dataset, the global KPIs can naturally surface instances where a later segment outperforms an earlier one.
|
|
526
600
|
|
|
@@ -536,14 +610,17 @@ A: Yes – just pass a DuckDB‑compatible table (e.g., a Pandas DataFrame) dire
|
|
|
536
610
|
**Q: Does the engine handle missing values (NULLs) correctly?**
|
|
537
611
|
A: Yes. Both extraction and evaluation treat NULLs consistently – NULL conditions do not match the rule and are carried forward to later segments (or the `ELSE 0` bucket).
|
|
538
612
|
|
|
539
|
-
**Q: My dataset may contain target
|
|
540
|
-
A: No.
|
|
613
|
+
**Q: My dataset may contain a target-leaked feature (100% correlation with the target). Will it be treated as an important feature?**
|
|
614
|
+
A: No. OptBinning drops it during segment creation. If you use `BigQueryFeatureSelector`, that feature’s IV is marked 0 and it is not considered.
|
|
615
|
+
|
|
616
|
+
**Q: I want to showcase/introduce to my tream for adoption! Are there any deck that I can use?**
|
|
617
|
+
A: Yes. Please refer to the [business deck](https://github.com/B2BDA/RapidSegment/blob/main/docs/RapidSegment_Business_Deck.pdf)
|
|
541
618
|
|
|
542
619
|
---
|
|
543
620
|
|
|
544
621
|
## 🤝 Contributing
|
|
545
622
|
|
|
546
|
-
We welcome contributions! Please open an issue or pull request on [GitHub](https://github.com/
|
|
623
|
+
We welcome contributions! Please open an issue or pull request on [GitHub](https://github.com/B2BDA/RapidSegment).
|
|
547
624
|
For major changes, please discuss them first via an issue.
|
|
548
625
|
|
|
549
626
|
---
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# 🚀 RapidSegment – Strategic Segmentation & Scorecard Engine
|
|
10
10
|
|
|
11
11
|
[](https://pypi.org/project/rapidsegment/)
|
|
12
|
-
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
15
|
**RapidSegment** is an industrial‑grade, combinatorial heuristic engine for discovering high‑lift predictive segments and compiling them into transparent, production‑ready scorecards. It bridges the gap between black‑box ML and legacy SQL rules engines.
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- [⚙️ How It Works – Step by Step](#️-how-it-works--step-by-step)
|
|
27
27
|
- [📊 Statistical Foundations](#-statistical-foundations)
|
|
28
28
|
- [🔧 Configuration Reference](#-configuration-reference)
|
|
29
|
+
- [🔋 Single Data Artifact & Memory Efficiency](#-single-data-artifact--memory-efficiency)
|
|
29
30
|
- [🤔 FAQs & Troubleshooting](#-faqs--troubleshooting)
|
|
30
31
|
- [🤝 Contributing](#-contributing)
|
|
31
32
|
- [📄 License](#-license)
|
|
@@ -34,14 +35,16 @@
|
|
|
34
35
|
## ✨ Features
|
|
35
36
|
|
|
36
37
|
- **🔎 Automated Rule Discovery** – Uses Optimal Binning (or fast naive quantile binning) + Apriori pruning to find multi‑way (1‑, 2‑, 3‑way) conditions that maximise lift and volume.
|
|
37
|
-
- **🧩 Hierarchical Segments** – Extracts mutually exclusive rules sequentially on a shrinking residual
|
|
38
|
+
- **🧩 Hierarchical Segments** – Extracts mutually exclusive rules sequentially on a shrinking residual population, ensuring clean portfolio decomposition.
|
|
38
39
|
- **🔀 Adjacent-Bin Expansion** – Optionally merges neighbouring bins (`max_expansion_hops`) to recover higher-event rules that pure single-bin candidates miss.
|
|
39
40
|
- **⚡ Hyper‑Efficient & Out-of-Core** – Leverages **DuckDB** (disk-backed by default) for vectorised SQL aggregations; spills to disk so large datasets fit in limited RAM.
|
|
41
|
+
- **🔋 Single Data Artifact (opt-in)** – With `persist_db=True`, extraction keeps one DuckDB file for evaluate / health / score reuse; residual rows are flagged in place (`__rs_excluded`) instead of rewriting the full table each iteration.
|
|
42
|
+
- **📉 Two-Phase Binning** – Fits IV ranking across all eligible features, then materialises full-length bin-label arrays only for `top_n_vars`, bounding peak Python memory.
|
|
43
|
+
- **📁 Zero-Copy File Path** – Pass a path to an existing DuckDB file (table `udl_data` for the builder, `df` for the scorer) so data never has to be pulled into a Python frame.
|
|
40
44
|
- **☁️ BigQuery Ready** – Optional feature screening runs natively inside Google BigQuery, downloading only the most predictive columns.
|
|
41
45
|
- **📦 Production‑Ready Outputs** – Exports pure ANSI SQL filters and a JSON scorecard with decile thresholds, ready for deployment.
|
|
42
46
|
- **📊 Transparent Weighting** – Uses the segment response rate to compute intuitive integer weights, while retaining lift, response rate, and capture rate for each segment.
|
|
43
47
|
- **🔬 Full Audit Trail** – `explain_feature_journey`, `explain_no_segments`, and `generate_feature_health_report` for complete diagnostics.
|
|
44
|
-
|
|
45
48
|
---
|
|
46
49
|
|
|
47
50
|
## 🌳 Decision Trees vs RapidSegment
|
|
@@ -63,6 +66,21 @@ Decision trees and RapidSegment both aim to create interpretable segments, but t
|
|
|
63
66
|
- The output is already aligned with SQL and scorecard workflows, reducing the gap between modeling and deployment.
|
|
64
67
|
- It is easier to reason about when you want stable, reusable segmentation logic rather than a branching tree structure.
|
|
65
68
|
- It is well suited for scenarios where you want a fixed set of business rules, consistent segment definitions, and explainable weights.
|
|
69
|
+
|
|
70
|
+
#### Empirical 1v1: Decision Tree vs RapidSegment
|
|
71
|
+
|
|
72
|
+
See the full showdown notebook:
|
|
73
|
+
**[DT vs RS comparison](https://github.com/B2BDA/RapidSegment/blob/main/Examples/compare_segmenters_general.ipynb)**
|
|
74
|
+
|
|
75
|
+
On matched runs with similar population and event capture:
|
|
76
|
+
|
|
77
|
+
| Pattern | Decision tree | RapidSegment |
|
|
78
|
+
|--------|----------------|--------------|
|
|
79
|
+
| Lift / response rate | Highly variable across segments | More stable segment to segment |
|
|
80
|
+
| Segment size | Often large (many customers per rule) | Typically tighter (on the order of ~1K rows per segment in this study) |
|
|
81
|
+
| Trade-off | High volume can inflate false positives | Lower volume per segment, steadier response rates |
|
|
82
|
+
|
|
83
|
+
**Takeaway:** Similar overall capture does not imply similar segment quality. Trees tend to grow broad leaves that pull in more non-events; RapidSegment favors smaller, higher-signal rules with more consistent lift and response rate.
|
|
66
84
|
|
|
67
85
|
In short, a decision tree is great when you want a predictive model structure; RapidSegment is better when you want transparent, deployable segments that are easy to understand and operationalize.
|
|
68
86
|
|
|
@@ -246,47 +264,47 @@ config:
|
|
|
246
264
|
theme: dark
|
|
247
265
|
---
|
|
248
266
|
flowchart TD
|
|
249
|
-
A["Original Data
|
|
250
|
-
--> B["Create
|
|
267
|
+
A["Original Data - input to extract_segments"]
|
|
268
|
+
--> B["Create current_df_base + __rs_row_id, target to DOUBLE, __rs_excluded = FALSE"]
|
|
269
|
+
|
|
270
|
+
B --> V["current_df = VIEW WHERE __rs_excluded IS NOT TRUE"]
|
|
251
271
|
|
|
252
|
-
|
|
272
|
+
V --> C{"For each segment i = 1 to max_segments"}
|
|
253
273
|
|
|
254
|
-
C --> D[Compute base_rate
|
|
274
|
+
C --> D["Compute base_rate and volume on residual current_df"]
|
|
255
275
|
|
|
256
|
-
D --> E[IV Ranking +
|
|
276
|
+
D --> E["IV Ranking + Binning: Phase 1 fit all eligible, Phase 2 transform top_n_vars only"]
|
|
257
277
|
|
|
258
|
-
E --> F[Build binned_df
|
|
278
|
+
E --> F["Build binned_df from top_n bin labels"]
|
|
259
279
|
|
|
260
|
-
F --> G[Generate
|
|
280
|
+
F --> G["Generate candidates 1-way, 2-way, 3-way on binned_df"]
|
|
261
281
|
|
|
262
|
-
G --> H[Select best candidate
|
|
282
|
+
G --> H["Select best candidate by sort_priority"]
|
|
263
283
|
|
|
264
|
-
H --> I["Parse rule
|
|
284
|
+
H --> I["Parse rule to SQL filter via parse_rule_to_sql"]
|
|
265
285
|
|
|
266
|
-
I --> J[Validate on
|
|
286
|
+
I --> J["Validate on residual current_df: COUNT + SUM WHERE sql_filter"]
|
|
267
287
|
|
|
268
|
-
J --> K{Meets min_sample_size
|
|
288
|
+
J --> K{"Meets min_sample_size, min_events, and min_lift?"}
|
|
269
289
|
|
|
270
|
-
K -- No --> L[Reject
|
|
290
|
+
K -- No --> L["Reject and try next candidate, or stop"]
|
|
271
291
|
L --> C
|
|
272
292
|
|
|
273
|
-
K -- Yes --> M[Store
|
|
293
|
+
K -- Yes --> M["Store segment with actual counts from residual"]
|
|
274
294
|
|
|
275
|
-
M --> N[Update
|
|
295
|
+
M --> N["Update feature usage tracker"]
|
|
276
296
|
|
|
277
|
-
N --> O["
|
|
297
|
+
N --> O["In-place residual update: SET __rs_excluded = TRUE WHERE sql_filter IS TRUE"]
|
|
278
298
|
|
|
279
|
-
O --> P[current_df
|
|
299
|
+
O --> P["current_df view shrinks - no full-table rewrite"]
|
|
280
300
|
|
|
281
301
|
P --> C
|
|
282
302
|
|
|
283
|
-
C --> Q[End
|
|
303
|
+
C --> Q["End loop"]
|
|
284
304
|
|
|
285
|
-
Q --> R[Return self.segments
|
|
305
|
+
Q --> R["Return self.segments hierarchical rules"]
|
|
286
306
|
|
|
287
|
-
|
|
288
|
-
R --> S[evaluate_final_coverage\nCASE WHEN on Original Data]
|
|
289
|
-
end
|
|
307
|
+
R --> S["evaluate_final_coverage: CASE WHEN on original population"]
|
|
290
308
|
```
|
|
291
309
|
## ⚙️ How It Works – Step by Step
|
|
292
310
|
|
|
@@ -391,12 +409,15 @@ For each iteration, the engine sweeps over a user‑defined grid of `(min_sample
|
|
|
391
409
|
### 4. Champion Validation & Extraction
|
|
392
410
|
The champion’s SQL filter is validated against the **raw residual** to ensure it meets the absolute hard constraints. Only then is it accepted.
|
|
393
411
|
|
|
394
|
-
### 5. Residual Update (NULL‑safe)
|
|
395
|
-
|
|
412
|
+
### 5. Residual Update (NULL‑safe, in-place)
|
|
413
|
+
|
|
414
|
+
Matched residual rows are **excluded in place** on the base table — the engine does **not** rewrite a new residual table each iteration:
|
|
415
|
+
This guarantees that the residual dataset exactly matches the `CASE`‑based hierarchical segmentation used in `evaluate_final_coverage`.
|
|
396
416
|
```sql
|
|
397
|
-
|
|
417
|
+
UPDATE current_df_base
|
|
418
|
+
SET "__rs_excluded" = TRUE
|
|
419
|
+
WHERE ({rule}) IS TRUE
|
|
398
420
|
```
|
|
399
|
-
This guarantees that the residual dataset exactly matches the `CASE`‑based hierarchical segmentation used in `evaluate_final_coverage`.
|
|
400
421
|
|
|
401
422
|
### 6. Loop
|
|
402
423
|
Steps 1‑5 repeat until either `max_segments` is reached or no more rules can be found.
|
|
@@ -448,7 +469,7 @@ Scores are computed as the sum of weights for all segments a customer triggers.
|
|
|
448
469
|
| `min_sample_size` | `int` | `1000` | Absolute minimum rows for a valid rule. |
|
|
449
470
|
| `min_lift` | `float` | `1.5` | Absolute minimum lift (hard constraint). |
|
|
450
471
|
| `min_events` | `int` | `100` | Minimum positive events for a valid rule. |
|
|
451
|
-
| `top_n_vars` | `int` | `15` | Number of top features passed to the Apriori engine. |
|
|
472
|
+
| `top_n_vars` | `int` | `15` | Number of top features passed to the Apriori engine (and for which full bin-label arrays are materialised). |
|
|
452
473
|
| `max_segments` | `int` | `10` | Maximum segments to extract. |
|
|
453
474
|
| `max_feature_reuse` | `int` | `1` | Max times any single feature may appear across segments. |
|
|
454
475
|
| `param_grid` | `dict` | `{}` | Optional grid of `{min_sample_size, min_lift}` to sweep. |
|
|
@@ -457,32 +478,85 @@ Scores are computed as the sum of weights for all segments a customer triggers.
|
|
|
457
478
|
| `feature_groups` | `dict` | `{}` | Business-category → column list (used by diversity). |
|
|
458
479
|
| `ignore_features` | `list` | `[]` | Columns to exclude before IV calculation. |
|
|
459
480
|
| `sort_priority` | `str` | `"rate_lift_count"` | Ranking key for champion selection (many variants supported). |
|
|
460
|
-
| `binning_method` | `str` | `"
|
|
481
|
+
| `binning_method` | `str` | `"optimal_cart"` | `"optimal_cart"` (or alias `"optimal"`), `"optimal_quantile"`, or `"naive"`. |
|
|
461
482
|
| `naive_bins` | `int` | `5` | Number of quantile bins when `binning_method="naive"`. |
|
|
462
483
|
| `max_expansion_hops` | `int` | `0` | Adjacent-bin merge distance (0 = disabled). |
|
|
463
484
|
| `selection_metric` | `str` | `"iv"` | Rank features by `"iv"` or `"response_rate"`. |
|
|
464
485
|
| `expand_log_mode` | `str` | `"none"` | Expansion logging: `"none"` \| `"summary"` \| `"champion"` \| `"full"`. |
|
|
486
|
+
| `memory_limit_gb` | `float` | `None` | DuckDB RAM buffer cap in GB. `None` → ~80% of host RAM. |
|
|
487
|
+
| `engine_threads` | `int` | `None` | DuckDB execution threads. `None` → all-but-two cores (or all cores if ≤4). |
|
|
465
488
|
| `db_path` / `db_temp_dir` | `str` | `None` | Optional explicit DuckDB file + temp dir (auto-created otherwise). |
|
|
489
|
+
| `persist_db` | `bool` | `False` | Keep the auto-created DuckDB artifact after `extract_segments` so evaluate / health / score can share the same file. Requires `close()` or a `with` block to clean up. |
|
|
466
490
|
|
|
467
491
|
**Output** – list of dicts with keys: `segment_id`, `rule_string`, `sql_filter`, `count`, `rate`, `lift`, `meta_applied_sample_size`, `meta_applied_min_lift`.
|
|
468
492
|
|
|
469
493
|
### `StrategicSegmentScore`
|
|
470
494
|
|
|
471
|
-
| Parameter | Type | Description |
|
|
472
|
-
|
|
473
|
-
| `target_col` | `str` | Binary target column. |
|
|
474
|
-
| `primary_key` | `str` | Unique row identifier. |
|
|
475
|
-
| `segment_cols` | `list` | List of binary segment flag columns. |
|
|
495
|
+
| Parameter | Type | Default | Description |
|
|
496
|
+
|-----------|------|---------|-------------|
|
|
497
|
+
| `target_col` | `str` | **Required** | Binary target column. |
|
|
498
|
+
| `primary_key` | `str` | **Required** | Unique row identifier. |
|
|
499
|
+
| `segment_cols` | `list` | **Required** | List of binary segment flag columns. |
|
|
500
|
+
|
|
501
|
+
`calculate_and_export_weights(data, export_path=..., db_path=None)`:
|
|
502
|
+
|
|
503
|
+
| Argument | Type | Default | Description |
|
|
504
|
+
|----------|------|---------|-------------|
|
|
505
|
+
| `data` | any / `str` | **Required** | Frame/table, or path to a DuckDB file containing table `df` (zero-copy attach). |
|
|
506
|
+
| `export_path` | `str` | timestamped JSON | Path for the model artifact. |
|
|
507
|
+
| `db_path` | `str` | `None` | Optional shared DuckDB file (e.g. the builder’s `db_path`). If omitted, a unique temp DB under the system temp dir is created and removed after export. |
|
|
476
508
|
|
|
477
509
|
**Export** – JSON artifact with `model_metadata`, `segment_weights`, and `decile_min_thresholds`.
|
|
478
510
|
|
|
479
511
|
---
|
|
480
512
|
|
|
513
|
+
## 🔋 Single Data Artifact & Memory Efficiency
|
|
514
|
+
|
|
515
|
+
By default RapidSegment materialises the residual workspace into a DuckDB file during `extract_segments` and deletes it when the run finishes. If you chain `extract_segments → evaluate_final_coverage → generate_feature_health_report → StrategicSegmentScore`, set **`persist_db=True`** so the **same DuckDB file** stays alive and later steps can share it instead of each opening a fresh, isolated DB.
|
|
516
|
+
|
|
517
|
+
**What is reused**
|
|
518
|
+
|
|
519
|
+
| Stage | Behaviour with `persist_db=True` |
|
|
520
|
+
|-------|----------------------------------|
|
|
521
|
+
| Residual inside extraction | One base table (`current_df_base`) + in-place `__rs_excluded` flag; `current_df` is a filtered **view** (no full-table rewrite per segment). |
|
|
522
|
+
| Binning memory | Phase 1 fits all eligible features; Phase 2 materialises full-length bin-label arrays only for `top_n_vars`. |
|
|
523
|
+
| Evaluate / health | Connect to the same `db_path`. If `original_df` is not already present in that file, it is materialised **once** from the data you pass (or attached zero-copy when you pass a DuckDB file path). Subsequent calls on the same file can reuse `original_df`. |
|
|
524
|
+
| Scorer | Pass `db_path=b.db_path` (or a path to a file that already contains table `df`) so scoring does not create a separate CWD `score_experiment_*.db`. |
|
|
525
|
+
|
|
526
|
+
**How to use** — prefer the context manager so cleanup is automatic:
|
|
527
|
+
|
|
528
|
+
```python
|
|
529
|
+
from rapidsegment import StrategicSegmentBuilder, StrategicSegmentScore
|
|
530
|
+
|
|
531
|
+
with StrategicSegmentBuilder(target="default_flag", persist_db=True) as b:
|
|
532
|
+
segments = b.extract_segments(data)
|
|
533
|
+
coverage = b.evaluate_final_coverage(data) # same DB file
|
|
534
|
+
health = b.generate_feature_health_report(data, ["age", "balance"])
|
|
535
|
+
|
|
536
|
+
# build seg_N flag columns on your scoring frame, then:
|
|
537
|
+
scorer = StrategicSegmentScore("default_flag", "cust_id", segment_cols)
|
|
538
|
+
scorer.calculate_and_export_weights(scored_df, "model.json", db_path=b.db_path)
|
|
539
|
+
# b.close() runs here → temp DuckDB file + temp dir removed
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Or manage it manually — but you **must** call `b.close()`:
|
|
543
|
+
|
|
544
|
+
```python
|
|
545
|
+
b = StrategicSegmentBuilder(target="default_flag", persist_db=True)
|
|
546
|
+
segments = b.extract_segments(data)
|
|
547
|
+
# ... evaluate / health / score ...
|
|
548
|
+
b.close() # required, otherwise the file lingers
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Leave `persist_db=False` (the default) if you only call `extract_segments` — there is no benefit and you avoid having to clean up.
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
481
555
|
## 🤔 FAQs & Troubleshooting
|
|
482
556
|
|
|
483
557
|
**Q: Why are later segments sometimes stronger in lift than earlier ones?**
|
|
484
|
-
A:
|
|
485
|
-
Because of this cascading extraction:
|
|
558
|
+
A: Extraction is sequential and operates on a shrinking residual population. Once a champion rule is discovered, matching residual rows are **flagged out** (`__rs_excluded = TRUE`) before the next iteration.
|
|
559
|
+
Because of this cascading extraction:
|
|
486
560
|
**`Local Optimization`**: The engine optimizes parameters and evaluates candidates based purely on the residual portfolio left behind by previous segments. A rule that yields massive lift on a specific, purified subset of data might look less dominant if it had been evaluated against the noisy baseline of the entire original population.
|
|
487
561
|
**`Changing Base Rates`**: As high-risk or high-performing records are stripped away in early rounds, the baseline event rate of the remaining pool shifts dynamically. This shifting baseline changes the mathematical benchmark for what constitutes a "high-lift" rule during that specific loop. Consequently, when evaluate_final_coverage maps all rules simultaneously back over the original, unfiltered dataset, the global KPIs can naturally surface instances where a later segment outperforms an earlier one.
|
|
488
562
|
|
|
@@ -498,14 +572,17 @@ A: Yes – just pass a DuckDB‑compatible table (e.g., a Pandas DataFrame) dire
|
|
|
498
572
|
**Q: Does the engine handle missing values (NULLs) correctly?**
|
|
499
573
|
A: Yes. Both extraction and evaluation treat NULLs consistently – NULL conditions do not match the rule and are carried forward to later segments (or the `ELSE 0` bucket).
|
|
500
574
|
|
|
501
|
-
**Q: My dataset may contain target
|
|
502
|
-
A: No.
|
|
575
|
+
**Q: My dataset may contain a target-leaked feature (100% correlation with the target). Will it be treated as an important feature?**
|
|
576
|
+
A: No. OptBinning drops it during segment creation. If you use `BigQueryFeatureSelector`, that feature’s IV is marked 0 and it is not considered.
|
|
577
|
+
|
|
578
|
+
**Q: I want to showcase/introduce to my tream for adoption! Are there any deck that I can use?**
|
|
579
|
+
A: Yes. Please refer to the [business deck](https://github.com/B2BDA/RapidSegment/blob/main/docs/RapidSegment_Business_Deck.pdf)
|
|
503
580
|
|
|
504
581
|
---
|
|
505
582
|
|
|
506
583
|
## 🤝 Contributing
|
|
507
584
|
|
|
508
|
-
We welcome contributions! Please open an issue or pull request on [GitHub](https://github.com/
|
|
585
|
+
We welcome contributions! Please open an issue or pull request on [GitHub](https://github.com/B2BDA/RapidSegment).
|
|
509
586
|
For major changes, please discuss them first via an issue.
|
|
510
587
|
|
|
511
588
|
---
|