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.
- {edgepoint-3.0.2 → edgepoint-3.5.1}/PKG-INFO +70 -52
- {edgepoint-3.0.2 → edgepoint-3.5.1}/README.md +69 -51
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/PKG-INFO +70 -52
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/SOURCES.txt +0 -2
- {edgepoint-3.0.2 → edgepoint-3.5.1}/pyproject.toml +1 -1
- {edgepoint-3.0.2 → edgepoint-3.5.1}/setup.py +1 -1
- edgepoint-3.0.2/edgepoint/core_v1.py +0 -573
- edgepoint-3.0.2/edgepoint/main_old.py +0 -713
- {edgepoint-3.0.2 → edgepoint-3.5.1}/LICENSE +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/__init__.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/aggregate_kfold_combos.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/aggregate_kfold_edges.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/core.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/main.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/run_kfold_find.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/run_kfold_search.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/sanity_check.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/split_router.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/stratified_folds.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/test_and_combo_val.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/train_combo.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/utils.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint/validations.py +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/dependency_links.txt +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/requires.txt +0 -0
- {edgepoint-3.0.2 → edgepoint-3.5.1}/edgepoint.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
35.
|
|
122
|
-
|
|
123
|
-
|
|
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,
|
|
127
|
-
|
|
128
|
-
1, 1, 1, 1,
|
|
129
|
-
0,
|
|
130
|
-
|
|
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)
|
|
184
|
-
are combined into AND-combinations
|
|
185
|
-
k-fold validated
|
|
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
|
-
##
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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":
|
|
255
|
+
"combo_num": 2,
|
|
246
256
|
"combo": "score, age",
|
|
247
|
-
"edges": {"score":
|
|
248
|
-
"hit_rate_tr":
|
|
249
|
-
"coverage_tr":
|
|
250
|
-
"n_tr":
|
|
251
|
-
"score_tr": 0.
|
|
252
|
-
"final_score":
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
35.
|
|
94
|
-
|
|
95
|
-
|
|
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,
|
|
99
|
-
|
|
100
|
-
1, 1, 1, 1,
|
|
101
|
-
0,
|
|
102
|
-
|
|
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)
|
|
156
|
-
are combined into AND-combinations
|
|
157
|
-
k-fold validated
|
|
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
|
-
##
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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":
|
|
227
|
+
"combo_num": 2,
|
|
218
228
|
"combo": "score, age",
|
|
219
|
-
"edges": {"score":
|
|
220
|
-
"hit_rate_tr":
|
|
221
|
-
"coverage_tr":
|
|
222
|
-
"n_tr":
|
|
223
|
-
"score_tr": 0.
|
|
224
|
-
"final_score":
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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.
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
35.
|
|
122
|
-
|
|
123
|
-
|
|
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,
|
|
127
|
-
|
|
128
|
-
1, 1, 1, 1,
|
|
129
|
-
0,
|
|
130
|
-
|
|
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)
|
|
184
|
-
are combined into AND-combinations
|
|
185
|
-
k-fold validated
|
|
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
|
-
##
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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":
|
|
255
|
+
"combo_num": 2,
|
|
246
256
|
"combo": "score, age",
|
|
247
|
-
"edges": {"score":
|
|
248
|
-
"hit_rate_tr":
|
|
249
|
-
"coverage_tr":
|
|
250
|
-
"n_tr":
|
|
251
|
-
"score_tr": 0.
|
|
252
|
-
"final_score":
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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.
|
|
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.
|
|
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,
|