edgepoint 3.0.2__tar.gz → 3.5.1__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 (27) hide show
  1. {edgepoint-3.0.2 → edgepoint-3.5.1}/PKG-INFO +70 -52
  2. {edgepoint-3.0.2 → edgepoint-3.5.1}/README.md +69 -51
  3. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/PKG-INFO +70 -52
  4. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/SOURCES.txt +0 -2
  5. {edgepoint-3.0.2 → edgepoint-3.5.1}/pyproject.toml +1 -1
  6. {edgepoint-3.0.2 → edgepoint-3.5.1}/setup.py +1 -1
  7. edgepoint-3.0.2/edgepoint/core_v1.py +0 -573
  8. edgepoint-3.0.2/edgepoint/main_old.py +0 -713
  9. {edgepoint-3.0.2 → edgepoint-3.5.1}/LICENSE +0 -0
  10. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/__init__.py +0 -0
  11. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/aggregate_kfold_combos.py +0 -0
  12. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/aggregate_kfold_edges.py +0 -0
  13. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/core.py +0 -0
  14. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/main.py +0 -0
  15. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/run_kfold_find.py +0 -0
  16. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/run_kfold_search.py +0 -0
  17. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/sanity_check.py +0 -0
  18. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/split_router.py +0 -0
  19. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/stratified_folds.py +0 -0
  20. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/test_and_combo_val.py +0 -0
  21. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/train_combo.py +0 -0
  22. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/utils.py +0 -0
  23. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/validations.py +0 -0
  24. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/dependency_links.txt +0 -0
  25. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/requires.txt +0 -0
  26. {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/top_level.txt +0 -0
  27. {edgepoint-3.0.2 → edgepoint-3.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edgepoint
3
- Version: 3.0.2
3
+ Version: 3.5.1
4
4
  Summary: Find the point in a numeric column above which a binary outcome becomes meaningfully and reliably better.
5
5
  Author: Henry
6
6
  License: MIT
@@ -109,32 +109,31 @@ from edgepoint import search
109
109
 
110
110
  df = pd.DataFrame({
111
111
  "score": [
112
- 72.68, 58.92, 62.15, 8.13, 33.3, 66.19, 73.79, 35.43,
113
- 65.06, 63.13, 61.47, 45.13, 39.16, 38.15, 29.41, 59.91,
114
- 54.12, 86.35, 76.02, 63.13, 51.63, 0, 77.4, 81.66,
115
- 50.13, 52.4, 52.82, 86.12, 57.58, 13.66, 37.29, 32.91,
116
- 73.66, 96.2, 36.3, 22.74, 65.54, 23.98, 61.59, 32.27
112
+ 63.94, 22.32, 89.22, 2.98, 2.65, 54.49, 80.94, 69.81,
113
+ 95.72, 9.67, 80.71, 97.31, 82.94, 57.74, 22.79, 23.28,
114
+ 63.57, 20.95, 64.80, 72.91, 98.95, 68.46, 22.90, 26.77,
115
+ 87.64, 39.56, 26.49, 26.27, 39.94, 50.95, 10.96, 42.22,
116
+ 99.61, 86.08, 68.17, 64.10, 45.37, 26.34, 91.26, 63.89
117
117
  ],
118
118
  "age": [
119
- 33.9, 41.9, 30.8, 56.5, 45.8, 45.1, 43.3, 54.3,
120
- 52.5, 54.2, 43.9, 18, 31.3, 38, 48.3, 55.8,
121
- 35.4, 56, 20.2, 27.7, 28.1, 39.4, 44.3, 39.9,
122
- 53.6, 26.1, 63.1, 42.3, 46.3, 20.2, 39.2, 35.3,
123
- 27.7, 31.8, 26.5, 32.2, 35.1, 43.5, 45.8, 43.8
119
+ 19.2, 52.6, 22.1, 28.3, 27.3, 28.4, 18.3, 34.0,
120
+ 33.8, 57.8, 52.3, 35.8, 47.1, 51.1, 31.6, 22.7,
121
+ 35.1, 30.5, 46.6, 25.7, 48.1, 57.6, 19.5, 27.9,
122
+ 32.8, 61.0, 29.6, 45.5, 28.3, 22.3, 47.5, 21.0,
123
+ 42.9, 18.5, 43.2, 23.2, 62.8, 41.5, 58.9, 46.6
124
124
  ],
125
125
  "hit": [
126
- 1, 1, 0, 0, 0, 1, 1, 0,
127
- 0, 1, 1, 0, 0, 0, 0, 1,
128
- 1, 1, 1, 1, 0, 0, 0, 1,
129
- 0, 1, 1, 1, 0, 0, 0, 0,
130
- 1, 1, 0, 0, 1, 0, 1, 0
126
+ 1, 0, 0, 0, 0, 0, 0, 1,
127
+ 1, 0, 1, 0, 0, 1, 1, 0,
128
+ 1, 0, 1, 1, 1, 1, 0, 0,
129
+ 0, 0, 0, 0, 0, 1, 0, 0,
130
+ 0, 0, 1, 0, 0, 1, 1, 1
131
131
  ]
132
132
  })
133
133
 
134
134
  edgepoints_df, top_combos = search(
135
135
  df,
136
136
  outcome_col="hit",
137
- mode="quick",
138
137
  min_coverage=10,
139
138
  show_progress=True,
140
139
  top_combo_n=3,
@@ -180,9 +179,10 @@ for you from the data itself:
180
179
  ratio to configure.
181
180
  2. **Train section**: every numeric column is searched for its best
182
181
  single-column threshold (percentile-based candidate edges, scored on
183
- a shrinkage-discounted gap/coverage blend), then qualifying columns
184
- are combined into AND-combinations (size capped by `mode`) and
185
- k-fold validated on train itself.
182
+ a shrinkage-discounted gap/coverage blend). Columns that clear the
183
+ baseline are then combined into AND-combinations, capped at a combo
184
+ size **worked out automatically** (see below), and k-fold validated
185
+ on train itself.
186
186
  3. **Test section**: every train-side edge and surviving combo is
187
187
  replayed against test, unchanged, with its own k-fold validation.
188
188
  4. **Merge edges**: train and test side stats for each column are
@@ -200,19 +200,30 @@ stage. Nothing is ever re-tuned on test.
200
200
 
201
201
  ---
202
202
 
203
- ## Modes
204
-
205
- `mode` controls how large a column combination can get during combo
206
- search:
207
-
208
- | Mode | Combo size |
209
- | ------------ | -------------------------------------------------------------------- |
210
- | `"quick"` | up to 2 columns per combo |
211
- | `"standard"` | up to 3 columns per combo |
212
- | `"deep"` | auto-fit, the largest size that keeps total combos under a fixed safety ceiling |
213
-
214
- `"quick"`/`"standard"` are refused up front if they'd blow past that
215
- ceiling given how many columns qualified.
203
+ ## Automatic combo-size capping (no `mode` to set)
204
+
205
+ Earlier versions asked you to pick a `mode` (`"quick"` / `"standard"` /
206
+ `"deep"`) to control how large a column combination could get. That's
207
+ gone. **The cap is now worked out automatically for every run**, from
208
+ two independent limits, and the smaller one always wins:
209
+
210
+ - **Row cap**: how big a combo can you actually *trust* given how much
211
+ data you have? ANDing four conditions together on 76 rows slices the
212
+ data down to almost nothing per combo, that's a straight overfitting
213
+ risk, not a compute one. This cap comes purely from row count.
214
+ - **Memory cap**: how big a combo can the machine *afford* to generate
215
+ before the number of combinations blows past a fixed safety ceiling?
216
+ This cap comes purely from column count, nothing statistical about it.
217
+
218
+ `search()` always uses `min(row_cap, memory_cap)`, so a combo size is
219
+ never allowed past whichever limit is more restrictive. This removes a
220
+ real footgun: a `mode="deep"` setting on a small dataset used to let
221
+ combo size grow well past what the row count could statistically
222
+ support, manufacturing an impressive-looking combo that was really just
223
+ overfitting a handful of rows. Now that ceiling is enforced automatically
224
+ on every run, there's no setting to accidentally crank up and no need to
225
+ know how many rows is "enough" for a given combo size, `search()` works
226
+ it out for you and never lets you shoot yourself in the foot.
216
227
 
217
228
  ---
218
229
 
@@ -222,7 +233,6 @@ ceiling given how many columns qualified.
222
233
  | -------------- | ------- | ----------------------------------------------------------------------------- |
223
234
  | `df` | required | Input DataFrame |
224
235
  | `outcome_col` | `"hit"` | Binary outcome column (`0`/`1`, `True`/`False`, or a mix) |
225
- | `mode` | `"quick"` | `"quick"`, `"standard"`, or `"deep"`, controls combo size, see above |
226
236
  | `min_coverage` | `10` | Minimum % of rows a threshold/combo must cover to qualify (train and test) |
227
237
  | `show_progress`| `True` | Print the full stage-by-stage log |
228
238
  | `top_combo_n` | `3` | How many top combos to return. `1` returns a single dict; more returns a list |
@@ -242,14 +252,16 @@ with train (`_tr`) and test (`_te`) versions of `hit_rate`, `miss_rate`,
242
252
 
243
253
  ```python
244
254
  {
245
- "combo_num": 3,
255
+ "combo_num": 2,
246
256
  "combo": "score, age",
247
- "edges": {"score": 49.46, "age": 39.5},
248
- "hit_rate_tr": 80.0, "hit_rate_te": 60.0,
249
- "coverage_tr": 41.67, "coverage_te": 31.25,
250
- "n_tr": 10, "n_te": 5,
251
- "score_tr": 0.62, "score_te": 0.41,
252
- "final_score": 21.49,
257
+ "edges": {"score": 55.0, "age": 40.0},
258
+ "hit_rate_tr": 83.33, "hit_rate_te": 75.0,
259
+ "coverage_tr": 20.0, "coverage_te": 40.0,
260
+ "n_tr": 6, "n_te": 4,
261
+ "score_tr": 0.71, "score_te": 0.55,
262
+ "final_score": 64.6,
263
+ "baseline_full": 40.0,
264
+ "vs_baseline": 35.0,
253
265
  }
254
266
  ```
255
267
 
@@ -260,17 +272,19 @@ empty `edges`.
260
272
 
261
273
  Think of `top_combos` as the answer to *"which traits, combined,
262
274
  actually separate my winners from everyone else, and by how much?"*
263
- A single-column result might tell you customers 34+ buy at 58% vs 22%
264
- for everyone else, useful, but crude, since plenty of 22-year-olds buy
265
- too. A combo sharpens that: `age >= 34 AND minutes_on_site >= 6.5` might
266
- come back at a 71% hit rate on train and 68% on test, covering 18% of
267
- visitors against a 24% baseline. That's a different, more useful claim.
268
- It's not age alone, it's that age *stacked with* engagement time is
269
- where the real signal was hiding, and it held up on data the search
270
- never saw. `top_combos` typically hands you a few of these to compare,
271
- so you can weigh, say, "age + time on site" against "past purchases +
272
- email opens," and pick whichever combo is both strong *and* backed by
273
- enough rows (`n_tr`/`n_te`) to actually trust.
275
+ A single-column result might tell you `score >= 55` alone gets you a
276
+ 66.67% hit rate on train (covering half the rows) and `age >= 40` alone
277
+ only gets you 50%, useful, but each is still riding along with a lot of
278
+ noise. A combo sharpens that: `score >= 55.0 AND age >= 40.0` comes back
279
+ at an 83.33% hit rate on train and 75.0% on test, covering a thinner but
280
+ much sharper 20-40% of rows, against a 40% baseline. That's the real
281
+ claim, it's not score alone or age alone, it's that the two *stacked
282
+ together* is where the actual separation was hiding, and it held up on
283
+ data the search never saw (`vs_baseline: 35.0` means even the weaker of
284
+ the two splits still beat "doing nothing" by 35 points). `top_combos`
285
+ typically hands you a few of these to compare, so you can weigh one
286
+ combo's strength against another's, and pick whichever is both strong
287
+ *and* backed by enough rows (`n_tr`/`n_te`) to actually trust.
274
288
 
275
289
  ---
276
290
 
@@ -308,6 +322,10 @@ per-column skip.
308
322
  - **Sign-flip penalty**: if a combo's gap direction flips between train
309
323
  and test, that's a concrete overfitting signal, so `final_score` is
310
324
  halved for that row.
325
+ - **Automatic combo-size capping**: combo size is capped by the
326
+ stricter of a row-count-based statistical limit and a memory-based
327
+ safety ceiling, see "Automatic combo-size capping" above, there's no
328
+ `mode` to set and no way to override this cap.
311
329
  - **One-directional search**: `edgepoint` only asks "is there a point
312
330
  above which the rate is *better*?" It doesn't independently verify the
313
331
  below side against its own bar.
@@ -81,32 +81,31 @@ from edgepoint import search
81
81
 
82
82
  df = pd.DataFrame({
83
83
  "score": [
84
- 72.68, 58.92, 62.15, 8.13, 33.3, 66.19, 73.79, 35.43,
85
- 65.06, 63.13, 61.47, 45.13, 39.16, 38.15, 29.41, 59.91,
86
- 54.12, 86.35, 76.02, 63.13, 51.63, 0, 77.4, 81.66,
87
- 50.13, 52.4, 52.82, 86.12, 57.58, 13.66, 37.29, 32.91,
88
- 73.66, 96.2, 36.3, 22.74, 65.54, 23.98, 61.59, 32.27
84
+ 63.94, 22.32, 89.22, 2.98, 2.65, 54.49, 80.94, 69.81,
85
+ 95.72, 9.67, 80.71, 97.31, 82.94, 57.74, 22.79, 23.28,
86
+ 63.57, 20.95, 64.80, 72.91, 98.95, 68.46, 22.90, 26.77,
87
+ 87.64, 39.56, 26.49, 26.27, 39.94, 50.95, 10.96, 42.22,
88
+ 99.61, 86.08, 68.17, 64.10, 45.37, 26.34, 91.26, 63.89
89
89
  ],
90
90
  "age": [
91
- 33.9, 41.9, 30.8, 56.5, 45.8, 45.1, 43.3, 54.3,
92
- 52.5, 54.2, 43.9, 18, 31.3, 38, 48.3, 55.8,
93
- 35.4, 56, 20.2, 27.7, 28.1, 39.4, 44.3, 39.9,
94
- 53.6, 26.1, 63.1, 42.3, 46.3, 20.2, 39.2, 35.3,
95
- 27.7, 31.8, 26.5, 32.2, 35.1, 43.5, 45.8, 43.8
91
+ 19.2, 52.6, 22.1, 28.3, 27.3, 28.4, 18.3, 34.0,
92
+ 33.8, 57.8, 52.3, 35.8, 47.1, 51.1, 31.6, 22.7,
93
+ 35.1, 30.5, 46.6, 25.7, 48.1, 57.6, 19.5, 27.9,
94
+ 32.8, 61.0, 29.6, 45.5, 28.3, 22.3, 47.5, 21.0,
95
+ 42.9, 18.5, 43.2, 23.2, 62.8, 41.5, 58.9, 46.6
96
96
  ],
97
97
  "hit": [
98
- 1, 1, 0, 0, 0, 1, 1, 0,
99
- 0, 1, 1, 0, 0, 0, 0, 1,
100
- 1, 1, 1, 1, 0, 0, 0, 1,
101
- 0, 1, 1, 1, 0, 0, 0, 0,
102
- 1, 1, 0, 0, 1, 0, 1, 0
98
+ 1, 0, 0, 0, 0, 0, 0, 1,
99
+ 1, 0, 1, 0, 0, 1, 1, 0,
100
+ 1, 0, 1, 1, 1, 1, 0, 0,
101
+ 0, 0, 0, 0, 0, 1, 0, 0,
102
+ 0, 0, 1, 0, 0, 1, 1, 1
103
103
  ]
104
104
  })
105
105
 
106
106
  edgepoints_df, top_combos = search(
107
107
  df,
108
108
  outcome_col="hit",
109
- mode="quick",
110
109
  min_coverage=10,
111
110
  show_progress=True,
112
111
  top_combo_n=3,
@@ -152,9 +151,10 @@ for you from the data itself:
152
151
  ratio to configure.
153
152
  2. **Train section**: every numeric column is searched for its best
154
153
  single-column threshold (percentile-based candidate edges, scored on
155
- a shrinkage-discounted gap/coverage blend), then qualifying columns
156
- are combined into AND-combinations (size capped by `mode`) and
157
- k-fold validated on train itself.
154
+ a shrinkage-discounted gap/coverage blend). Columns that clear the
155
+ baseline are then combined into AND-combinations, capped at a combo
156
+ size **worked out automatically** (see below), and k-fold validated
157
+ on train itself.
158
158
  3. **Test section**: every train-side edge and surviving combo is
159
159
  replayed against test, unchanged, with its own k-fold validation.
160
160
  4. **Merge edges**: train and test side stats for each column are
@@ -172,19 +172,30 @@ stage. Nothing is ever re-tuned on test.
172
172
 
173
173
  ---
174
174
 
175
- ## Modes
176
-
177
- `mode` controls how large a column combination can get during combo
178
- search:
179
-
180
- | Mode | Combo size |
181
- | ------------ | -------------------------------------------------------------------- |
182
- | `"quick"` | up to 2 columns per combo |
183
- | `"standard"` | up to 3 columns per combo |
184
- | `"deep"` | auto-fit, the largest size that keeps total combos under a fixed safety ceiling |
185
-
186
- `"quick"`/`"standard"` are refused up front if they'd blow past that
187
- ceiling given how many columns qualified.
175
+ ## Automatic combo-size capping (no `mode` to set)
176
+
177
+ Earlier versions asked you to pick a `mode` (`"quick"` / `"standard"` /
178
+ `"deep"`) to control how large a column combination could get. That's
179
+ gone. **The cap is now worked out automatically for every run**, from
180
+ two independent limits, and the smaller one always wins:
181
+
182
+ - **Row cap**: how big a combo can you actually *trust* given how much
183
+ data you have? ANDing four conditions together on 76 rows slices the
184
+ data down to almost nothing per combo, that's a straight overfitting
185
+ risk, not a compute one. This cap comes purely from row count.
186
+ - **Memory cap**: how big a combo can the machine *afford* to generate
187
+ before the number of combinations blows past a fixed safety ceiling?
188
+ This cap comes purely from column count, nothing statistical about it.
189
+
190
+ `search()` always uses `min(row_cap, memory_cap)`, so a combo size is
191
+ never allowed past whichever limit is more restrictive. This removes a
192
+ real footgun: a `mode="deep"` setting on a small dataset used to let
193
+ combo size grow well past what the row count could statistically
194
+ support, manufacturing an impressive-looking combo that was really just
195
+ overfitting a handful of rows. Now that ceiling is enforced automatically
196
+ on every run, there's no setting to accidentally crank up and no need to
197
+ know how many rows is "enough" for a given combo size, `search()` works
198
+ it out for you and never lets you shoot yourself in the foot.
188
199
 
189
200
  ---
190
201
 
@@ -194,7 +205,6 @@ ceiling given how many columns qualified.
194
205
  | -------------- | ------- | ----------------------------------------------------------------------------- |
195
206
  | `df` | required | Input DataFrame |
196
207
  | `outcome_col` | `"hit"` | Binary outcome column (`0`/`1`, `True`/`False`, or a mix) |
197
- | `mode` | `"quick"` | `"quick"`, `"standard"`, or `"deep"`, controls combo size, see above |
198
208
  | `min_coverage` | `10` | Minimum % of rows a threshold/combo must cover to qualify (train and test) |
199
209
  | `show_progress`| `True` | Print the full stage-by-stage log |
200
210
  | `top_combo_n` | `3` | How many top combos to return. `1` returns a single dict; more returns a list |
@@ -214,14 +224,16 @@ with train (`_tr`) and test (`_te`) versions of `hit_rate`, `miss_rate`,
214
224
 
215
225
  ```python
216
226
  {
217
- "combo_num": 3,
227
+ "combo_num": 2,
218
228
  "combo": "score, age",
219
- "edges": {"score": 49.46, "age": 39.5},
220
- "hit_rate_tr": 80.0, "hit_rate_te": 60.0,
221
- "coverage_tr": 41.67, "coverage_te": 31.25,
222
- "n_tr": 10, "n_te": 5,
223
- "score_tr": 0.62, "score_te": 0.41,
224
- "final_score": 21.49,
229
+ "edges": {"score": 55.0, "age": 40.0},
230
+ "hit_rate_tr": 83.33, "hit_rate_te": 75.0,
231
+ "coverage_tr": 20.0, "coverage_te": 40.0,
232
+ "n_tr": 6, "n_te": 4,
233
+ "score_tr": 0.71, "score_te": 0.55,
234
+ "final_score": 64.6,
235
+ "baseline_full": 40.0,
236
+ "vs_baseline": 35.0,
225
237
  }
226
238
  ```
227
239
 
@@ -232,17 +244,19 @@ empty `edges`.
232
244
 
233
245
  Think of `top_combos` as the answer to *"which traits, combined,
234
246
  actually separate my winners from everyone else, and by how much?"*
235
- A single-column result might tell you customers 34+ buy at 58% vs 22%
236
- for everyone else, useful, but crude, since plenty of 22-year-olds buy
237
- too. A combo sharpens that: `age >= 34 AND minutes_on_site >= 6.5` might
238
- come back at a 71% hit rate on train and 68% on test, covering 18% of
239
- visitors against a 24% baseline. That's a different, more useful claim.
240
- It's not age alone, it's that age *stacked with* engagement time is
241
- where the real signal was hiding, and it held up on data the search
242
- never saw. `top_combos` typically hands you a few of these to compare,
243
- so you can weigh, say, "age + time on site" against "past purchases +
244
- email opens," and pick whichever combo is both strong *and* backed by
245
- enough rows (`n_tr`/`n_te`) to actually trust.
247
+ A single-column result might tell you `score >= 55` alone gets you a
248
+ 66.67% hit rate on train (covering half the rows) and `age >= 40` alone
249
+ only gets you 50%, useful, but each is still riding along with a lot of
250
+ noise. A combo sharpens that: `score >= 55.0 AND age >= 40.0` comes back
251
+ at an 83.33% hit rate on train and 75.0% on test, covering a thinner but
252
+ much sharper 20-40% of rows, against a 40% baseline. That's the real
253
+ claim, it's not score alone or age alone, it's that the two *stacked
254
+ together* is where the actual separation was hiding, and it held up on
255
+ data the search never saw (`vs_baseline: 35.0` means even the weaker of
256
+ the two splits still beat "doing nothing" by 35 points). `top_combos`
257
+ typically hands you a few of these to compare, so you can weigh one
258
+ combo's strength against another's, and pick whichever is both strong
259
+ *and* backed by enough rows (`n_tr`/`n_te`) to actually trust.
246
260
 
247
261
  ---
248
262
 
@@ -280,6 +294,10 @@ per-column skip.
280
294
  - **Sign-flip penalty**: if a combo's gap direction flips between train
281
295
  and test, that's a concrete overfitting signal, so `final_score` is
282
296
  halved for that row.
297
+ - **Automatic combo-size capping**: combo size is capped by the
298
+ stricter of a row-count-based statistical limit and a memory-based
299
+ safety ceiling, see "Automatic combo-size capping" above, there's no
300
+ `mode` to set and no way to override this cap.
283
301
  - **One-directional search**: `edgepoint` only asks "is there a point
284
302
  above which the rate is *better*?" It doesn't independently verify the
285
303
  below side against its own bar.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edgepoint
3
- Version: 3.0.2
3
+ Version: 3.5.1
4
4
  Summary: Find the point in a numeric column above which a binary outcome becomes meaningfully and reliably better.
5
5
  Author: Henry
6
6
  License: MIT
@@ -109,32 +109,31 @@ from edgepoint import search
109
109
 
110
110
  df = pd.DataFrame({
111
111
  "score": [
112
- 72.68, 58.92, 62.15, 8.13, 33.3, 66.19, 73.79, 35.43,
113
- 65.06, 63.13, 61.47, 45.13, 39.16, 38.15, 29.41, 59.91,
114
- 54.12, 86.35, 76.02, 63.13, 51.63, 0, 77.4, 81.66,
115
- 50.13, 52.4, 52.82, 86.12, 57.58, 13.66, 37.29, 32.91,
116
- 73.66, 96.2, 36.3, 22.74, 65.54, 23.98, 61.59, 32.27
112
+ 63.94, 22.32, 89.22, 2.98, 2.65, 54.49, 80.94, 69.81,
113
+ 95.72, 9.67, 80.71, 97.31, 82.94, 57.74, 22.79, 23.28,
114
+ 63.57, 20.95, 64.80, 72.91, 98.95, 68.46, 22.90, 26.77,
115
+ 87.64, 39.56, 26.49, 26.27, 39.94, 50.95, 10.96, 42.22,
116
+ 99.61, 86.08, 68.17, 64.10, 45.37, 26.34, 91.26, 63.89
117
117
  ],
118
118
  "age": [
119
- 33.9, 41.9, 30.8, 56.5, 45.8, 45.1, 43.3, 54.3,
120
- 52.5, 54.2, 43.9, 18, 31.3, 38, 48.3, 55.8,
121
- 35.4, 56, 20.2, 27.7, 28.1, 39.4, 44.3, 39.9,
122
- 53.6, 26.1, 63.1, 42.3, 46.3, 20.2, 39.2, 35.3,
123
- 27.7, 31.8, 26.5, 32.2, 35.1, 43.5, 45.8, 43.8
119
+ 19.2, 52.6, 22.1, 28.3, 27.3, 28.4, 18.3, 34.0,
120
+ 33.8, 57.8, 52.3, 35.8, 47.1, 51.1, 31.6, 22.7,
121
+ 35.1, 30.5, 46.6, 25.7, 48.1, 57.6, 19.5, 27.9,
122
+ 32.8, 61.0, 29.6, 45.5, 28.3, 22.3, 47.5, 21.0,
123
+ 42.9, 18.5, 43.2, 23.2, 62.8, 41.5, 58.9, 46.6
124
124
  ],
125
125
  "hit": [
126
- 1, 1, 0, 0, 0, 1, 1, 0,
127
- 0, 1, 1, 0, 0, 0, 0, 1,
128
- 1, 1, 1, 1, 0, 0, 0, 1,
129
- 0, 1, 1, 1, 0, 0, 0, 0,
130
- 1, 1, 0, 0, 1, 0, 1, 0
126
+ 1, 0, 0, 0, 0, 0, 0, 1,
127
+ 1, 0, 1, 0, 0, 1, 1, 0,
128
+ 1, 0, 1, 1, 1, 1, 0, 0,
129
+ 0, 0, 0, 0, 0, 1, 0, 0,
130
+ 0, 0, 1, 0, 0, 1, 1, 1
131
131
  ]
132
132
  })
133
133
 
134
134
  edgepoints_df, top_combos = search(
135
135
  df,
136
136
  outcome_col="hit",
137
- mode="quick",
138
137
  min_coverage=10,
139
138
  show_progress=True,
140
139
  top_combo_n=3,
@@ -180,9 +179,10 @@ for you from the data itself:
180
179
  ratio to configure.
181
180
  2. **Train section**: every numeric column is searched for its best
182
181
  single-column threshold (percentile-based candidate edges, scored on
183
- a shrinkage-discounted gap/coverage blend), then qualifying columns
184
- are combined into AND-combinations (size capped by `mode`) and
185
- k-fold validated on train itself.
182
+ a shrinkage-discounted gap/coverage blend). Columns that clear the
183
+ baseline are then combined into AND-combinations, capped at a combo
184
+ size **worked out automatically** (see below), and k-fold validated
185
+ on train itself.
186
186
  3. **Test section**: every train-side edge and surviving combo is
187
187
  replayed against test, unchanged, with its own k-fold validation.
188
188
  4. **Merge edges**: train and test side stats for each column are
@@ -200,19 +200,30 @@ stage. Nothing is ever re-tuned on test.
200
200
 
201
201
  ---
202
202
 
203
- ## Modes
204
-
205
- `mode` controls how large a column combination can get during combo
206
- search:
207
-
208
- | Mode | Combo size |
209
- | ------------ | -------------------------------------------------------------------- |
210
- | `"quick"` | up to 2 columns per combo |
211
- | `"standard"` | up to 3 columns per combo |
212
- | `"deep"` | auto-fit, the largest size that keeps total combos under a fixed safety ceiling |
213
-
214
- `"quick"`/`"standard"` are refused up front if they'd blow past that
215
- ceiling given how many columns qualified.
203
+ ## Automatic combo-size capping (no `mode` to set)
204
+
205
+ Earlier versions asked you to pick a `mode` (`"quick"` / `"standard"` /
206
+ `"deep"`) to control how large a column combination could get. That's
207
+ gone. **The cap is now worked out automatically for every run**, from
208
+ two independent limits, and the smaller one always wins:
209
+
210
+ - **Row cap**: how big a combo can you actually *trust* given how much
211
+ data you have? ANDing four conditions together on 76 rows slices the
212
+ data down to almost nothing per combo, that's a straight overfitting
213
+ risk, not a compute one. This cap comes purely from row count.
214
+ - **Memory cap**: how big a combo can the machine *afford* to generate
215
+ before the number of combinations blows past a fixed safety ceiling?
216
+ This cap comes purely from column count, nothing statistical about it.
217
+
218
+ `search()` always uses `min(row_cap, memory_cap)`, so a combo size is
219
+ never allowed past whichever limit is more restrictive. This removes a
220
+ real footgun: a `mode="deep"` setting on a small dataset used to let
221
+ combo size grow well past what the row count could statistically
222
+ support, manufacturing an impressive-looking combo that was really just
223
+ overfitting a handful of rows. Now that ceiling is enforced automatically
224
+ on every run, there's no setting to accidentally crank up and no need to
225
+ know how many rows is "enough" for a given combo size, `search()` works
226
+ it out for you and never lets you shoot yourself in the foot.
216
227
 
217
228
  ---
218
229
 
@@ -222,7 +233,6 @@ ceiling given how many columns qualified.
222
233
  | -------------- | ------- | ----------------------------------------------------------------------------- |
223
234
  | `df` | required | Input DataFrame |
224
235
  | `outcome_col` | `"hit"` | Binary outcome column (`0`/`1`, `True`/`False`, or a mix) |
225
- | `mode` | `"quick"` | `"quick"`, `"standard"`, or `"deep"`, controls combo size, see above |
226
236
  | `min_coverage` | `10` | Minimum % of rows a threshold/combo must cover to qualify (train and test) |
227
237
  | `show_progress`| `True` | Print the full stage-by-stage log |
228
238
  | `top_combo_n` | `3` | How many top combos to return. `1` returns a single dict; more returns a list |
@@ -242,14 +252,16 @@ with train (`_tr`) and test (`_te`) versions of `hit_rate`, `miss_rate`,
242
252
 
243
253
  ```python
244
254
  {
245
- "combo_num": 3,
255
+ "combo_num": 2,
246
256
  "combo": "score, age",
247
- "edges": {"score": 49.46, "age": 39.5},
248
- "hit_rate_tr": 80.0, "hit_rate_te": 60.0,
249
- "coverage_tr": 41.67, "coverage_te": 31.25,
250
- "n_tr": 10, "n_te": 5,
251
- "score_tr": 0.62, "score_te": 0.41,
252
- "final_score": 21.49,
257
+ "edges": {"score": 55.0, "age": 40.0},
258
+ "hit_rate_tr": 83.33, "hit_rate_te": 75.0,
259
+ "coverage_tr": 20.0, "coverage_te": 40.0,
260
+ "n_tr": 6, "n_te": 4,
261
+ "score_tr": 0.71, "score_te": 0.55,
262
+ "final_score": 64.6,
263
+ "baseline_full": 40.0,
264
+ "vs_baseline": 35.0,
253
265
  }
254
266
  ```
255
267
 
@@ -260,17 +272,19 @@ empty `edges`.
260
272
 
261
273
  Think of `top_combos` as the answer to *"which traits, combined,
262
274
  actually separate my winners from everyone else, and by how much?"*
263
- A single-column result might tell you customers 34+ buy at 58% vs 22%
264
- for everyone else, useful, but crude, since plenty of 22-year-olds buy
265
- too. A combo sharpens that: `age >= 34 AND minutes_on_site >= 6.5` might
266
- come back at a 71% hit rate on train and 68% on test, covering 18% of
267
- visitors against a 24% baseline. That's a different, more useful claim.
268
- It's not age alone, it's that age *stacked with* engagement time is
269
- where the real signal was hiding, and it held up on data the search
270
- never saw. `top_combos` typically hands you a few of these to compare,
271
- so you can weigh, say, "age + time on site" against "past purchases +
272
- email opens," and pick whichever combo is both strong *and* backed by
273
- enough rows (`n_tr`/`n_te`) to actually trust.
275
+ A single-column result might tell you `score >= 55` alone gets you a
276
+ 66.67% hit rate on train (covering half the rows) and `age >= 40` alone
277
+ only gets you 50%, useful, but each is still riding along with a lot of
278
+ noise. A combo sharpens that: `score >= 55.0 AND age >= 40.0` comes back
279
+ at an 83.33% hit rate on train and 75.0% on test, covering a thinner but
280
+ much sharper 20-40% of rows, against a 40% baseline. That's the real
281
+ claim, it's not score alone or age alone, it's that the two *stacked
282
+ together* is where the actual separation was hiding, and it held up on
283
+ data the search never saw (`vs_baseline: 35.0` means even the weaker of
284
+ the two splits still beat "doing nothing" by 35 points). `top_combos`
285
+ typically hands you a few of these to compare, so you can weigh one
286
+ combo's strength against another's, and pick whichever is both strong
287
+ *and* backed by enough rows (`n_tr`/`n_te`) to actually trust.
274
288
 
275
289
  ---
276
290
 
@@ -308,6 +322,10 @@ per-column skip.
308
322
  - **Sign-flip penalty**: if a combo's gap direction flips between train
309
323
  and test, that's a concrete overfitting signal, so `final_score` is
310
324
  halved for that row.
325
+ - **Automatic combo-size capping**: combo size is capped by the
326
+ stricter of a row-count-based statistical limit and a memory-based
327
+ safety ceiling, see "Automatic combo-size capping" above, there's no
328
+ `mode` to set and no way to override this cap.
311
329
  - **One-directional search**: `edgepoint` only asks "is there a point
312
330
  above which the rate is *better*?" It doesn't independently verify the
313
331
  below side against its own bar.
@@ -6,9 +6,7 @@ edgepoint/__init__.py
6
6
  edgepoint/aggregate_kfold_combos.py
7
7
  edgepoint/aggregate_kfold_edges.py
8
8
  edgepoint/core.py
9
- edgepoint/core_v1.py
10
9
  edgepoint/main.py
11
- edgepoint/main_old.py
12
10
  edgepoint/run_kfold_find.py
13
11
  edgepoint/run_kfold_search.py
14
12
  edgepoint/sanity_check.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "edgepoint"
7
- version = "3.0.2"
7
+ version = "3.5.1"
8
8
  description = "Find the point in a numeric column above which a binary outcome becomes meaningfully and reliably better."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -5,7 +5,7 @@ long_description = (Path(__file__).parent / "README.md").read_text(encoding="utf
5
5
 
6
6
  setup(
7
7
  name="edgepoint",
8
- version="3.0.2",
8
+ version="3.5.1",
9
9
  author="Henry",
10
10
  description="Find where a metric, or combination of metrics, starts getting better while keeping the best coverage possible, then check if that holds on unseen data.",
11
11
  long_description=long_description,