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.
Files changed (26) hide show
  1. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/PKG-INFO +118 -41
  2. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/README.md +117 -40
  3. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/pyproject.toml +1 -1
  4. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/pyproject.toml.orig +1 -1
  5. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/builder.py +270 -73
  6. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/scorer.py +36 -8
  7. rapidsegment-1.2.8/src/rapidsegment/ui/.streamlit/config.toml +3 -0
  8. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/1_Data_Loader.py +106 -33
  9. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/2_Workbench.py +964 -143
  10. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/3_Execution_Console.py +23 -27
  11. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/4_Results_Dashboard.py +64 -44
  12. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/5_Leaderboard.py +1 -0
  13. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/6_Arena.py +5 -2
  14. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/utils/data_loader.py +57 -1
  15. rapidsegment-1.2.7/src/rapidsegment/ui/.streamlit/config.toml +0 -3
  16. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/LICENSE +0 -0
  17. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/__init__.py +0 -0
  18. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/py.typed +0 -0
  19. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/__init__.py +0 -0
  20. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/_exit.py +0 -0
  21. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/_theme.py +0 -0
  22. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/app.py +0 -0
  23. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/ui/pages/__init__.py +0 -0
  24. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/utils/__init__.py +0 -0
  25. {rapidsegment-1.2.7 → rapidsegment-1.2.8}/src/rapidsegment/utils/on_gcp_feature_selection.py +0 -0
  26. {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.7
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
  [![PyPI version](https://img.shields.io/pypi/v/rapidsegment.svg)](https://pypi.org/project/rapidsegment/)
50
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
50
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
51
51
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 dataset, ensuring clean portfolio decomposition.
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\n(input to extract_segments)"]
288
- --> B["Create current_df\n(full copy)"]
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
- B --> C{For each segment\ni = 1 to max_segments}
310
+ V --> C{"For each segment i = 1 to max_segments"}
291
311
 
292
- C --> D[Compute base_rate & volume\non current residual current_df]
312
+ C --> D["Compute base_rate and volume on residual current_df"]
293
313
 
294
- D --> E[IV Ranking + Optimal Binning\non current residual]
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\nusing precomputed bins from residual]
316
+ E --> F["Build binned_df from top_n bin labels"]
297
317
 
298
- F --> G[Generate Candidates\n1-way, 2-way, 3-way on binned_df]
318
+ F --> G["Generate candidates 1-way, 2-way, 3-way on binned_df"]
299
319
 
300
- G --> H[Select best candidate\nby sort_priority]
320
+ G --> H["Select best candidate by sort_priority"]
301
321
 
302
- H --> I["Parse rule → SQL filter\nparse_rule_to_sql()"]
322
+ H --> I["Parse rule to SQL filter via parse_rule_to_sql"]
303
323
 
304
- I --> J[Validate on RAW current_df\nCOUNT + SUM WHERE sql_filter]
324
+ I --> J["Validate on residual current_df: COUNT + SUM WHERE sql_filter"]
305
325
 
306
- J --> K{Meets min_sample_size\nand min_events?}
326
+ J --> K{"Meets min_sample_size, min_events, and min_lift?"}
307
327
 
308
- K -- No --> L[Reject & Try Next Candidate\nor Stop]
328
+ K -- No --> L["Reject and try next candidate, or stop"]
309
329
  L --> C
310
330
 
311
- K -- Yes --> M[Store Segment\nwith actual counts from residual]
331
+ K -- Yes --> M["Store segment with actual counts from residual"]
312
332
 
313
- M --> N[Update Feature Usage Tracker]
333
+ M --> N["Update feature usage tracker"]
314
334
 
315
- N --> O["Delete matching rows from residual\nWHERE NOT (sql_filter) OR IS NULL"]
335
+ N --> O["In-place residual update: SET __rs_excluded = TRUE WHERE sql_filter IS TRUE"]
316
336
 
317
- O --> P[current_df ← smaller residual]
337
+ O --> P["current_df view shrinks - no full-table rewrite"]
318
338
 
319
339
  P --> C
320
340
 
321
- C --> Q[End Loop]
341
+ C --> Q["End loop"]
322
342
 
323
- Q --> R[Return self.segments\nhierarchical rules]
343
+ Q --> R["Return self.segments hierarchical rules"]
324
344
 
325
- subgraph Final Evaluation
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
- Rows matching the rule are removed using:
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
- WHERE NOT (rule) OR (rule) IS NULL
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` | `"optimal"` | `"optimal_cart"` or `optimal` (OptBinning with CART) or `"optimal_quantile"` (OptBinning with Quantile) or `"naive"` (quantile bins). |
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: The segment extraction process is entirely sequential and operates on a shrinking residual population. Once a champion rule is discovered, its matching records are deleted from the working environment before the next iteration begins.
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 leaked feature (100% correaltion with Target). Will it be taken as important feature?**
540
- A: No. The feature will be dropped by Optbinning during segment creation steps. Furthermore, if you are using BigQueryFeatureSelector the feature IV will be marked as 0 and not considered.
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/your-org/rapidsegment).
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
  [![PyPI version](https://img.shields.io/pypi/v/rapidsegment.svg)](https://pypi.org/project/rapidsegment/)
12
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
12
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 dataset, ensuring clean portfolio decomposition.
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\n(input to extract_segments)"]
250
- --> B["Create current_df\n(full copy)"]
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
- B --> C{For each segment\ni = 1 to max_segments}
272
+ V --> C{"For each segment i = 1 to max_segments"}
253
273
 
254
- C --> D[Compute base_rate & volume\non current residual current_df]
274
+ C --> D["Compute base_rate and volume on residual current_df"]
255
275
 
256
- D --> E[IV Ranking + Optimal Binning\non current residual]
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\nusing precomputed bins from residual]
278
+ E --> F["Build binned_df from top_n bin labels"]
259
279
 
260
- F --> G[Generate Candidates\n1-way, 2-way, 3-way on binned_df]
280
+ F --> G["Generate candidates 1-way, 2-way, 3-way on binned_df"]
261
281
 
262
- G --> H[Select best candidate\nby sort_priority]
282
+ G --> H["Select best candidate by sort_priority"]
263
283
 
264
- H --> I["Parse rule → SQL filter\nparse_rule_to_sql()"]
284
+ H --> I["Parse rule to SQL filter via parse_rule_to_sql"]
265
285
 
266
- I --> J[Validate on RAW current_df\nCOUNT + SUM WHERE sql_filter]
286
+ I --> J["Validate on residual current_df: COUNT + SUM WHERE sql_filter"]
267
287
 
268
- J --> K{Meets min_sample_size\nand min_events?}
288
+ J --> K{"Meets min_sample_size, min_events, and min_lift?"}
269
289
 
270
- K -- No --> L[Reject & Try Next Candidate\nor Stop]
290
+ K -- No --> L["Reject and try next candidate, or stop"]
271
291
  L --> C
272
292
 
273
- K -- Yes --> M[Store Segment\nwith actual counts from residual]
293
+ K -- Yes --> M["Store segment with actual counts from residual"]
274
294
 
275
- M --> N[Update Feature Usage Tracker]
295
+ M --> N["Update feature usage tracker"]
276
296
 
277
- N --> O["Delete matching rows from residual\nWHERE NOT (sql_filter) OR IS NULL"]
297
+ N --> O["In-place residual update: SET __rs_excluded = TRUE WHERE sql_filter IS TRUE"]
278
298
 
279
- O --> P[current_df ← smaller residual]
299
+ O --> P["current_df view shrinks - no full-table rewrite"]
280
300
 
281
301
  P --> C
282
302
 
283
- C --> Q[End Loop]
303
+ C --> Q["End loop"]
284
304
 
285
- Q --> R[Return self.segments\nhierarchical rules]
305
+ Q --> R["Return self.segments hierarchical rules"]
286
306
 
287
- subgraph Final Evaluation
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
- Rows matching the rule are removed using:
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
- WHERE NOT (rule) OR (rule) IS NULL
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` | `"optimal"` | `"optimal_cart"` or `optimal` (OptBinning with CART) or `"optimal_quantile"` (OptBinning with Quantile) or `"naive"` (quantile bins). |
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: The segment extraction process is entirely sequential and operates on a shrinking residual population. Once a champion rule is discovered, its matching records are deleted from the working environment before the next iteration begins.
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 leaked feature (100% correaltion with Target). Will it be taken as important feature?**
502
- A: No. The feature will be dropped by Optbinning during segment creation steps. Furthermore, if you are using BigQueryFeatureSelector the feature IV will be marked as 0 and not considered.
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/your-org/rapidsegment).
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
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rapidsegment"
3
- version = "1.2.7"
3
+ version = "1.2.8"
4
4
  description = "A fast, multi-segment population scoring and combinatorial heuristic rule extraction engine."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rapidsegment"
3
- version = "1.2.7"
3
+ version = "1.2.8"
4
4
  description = "A fast, multi-segment population scoring and combinatorial heuristic rule extraction engine."
5
5
  readme = "README.md"
6
6
  authors = [