edgepoint 3.5.2__tar.gz → 3.5.4__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 (25) hide show
  1. {edgepoint-3.5.2 → edgepoint-3.5.4}/PKG-INFO +12 -11
  2. {edgepoint-3.5.2 → edgepoint-3.5.4}/README.md +11 -10
  3. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/aggregate_kfold_combos.py +55 -62
  4. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/aggregate_kfold_edges.py +15 -13
  5. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/main.py +1 -1
  6. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/split_router.py +18 -6
  7. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/test_and_combo_val.py +9 -7
  8. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint.egg-info/PKG-INFO +12 -11
  9. {edgepoint-3.5.2 → edgepoint-3.5.4}/pyproject.toml +1 -1
  10. {edgepoint-3.5.2 → edgepoint-3.5.4}/setup.py +1 -1
  11. {edgepoint-3.5.2 → edgepoint-3.5.4}/LICENSE +0 -0
  12. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/__init__.py +0 -0
  13. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/core.py +0 -0
  14. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/run_kfold_find.py +0 -0
  15. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/run_kfold_search.py +0 -0
  16. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/sanity_check.py +0 -0
  17. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/stratified_folds.py +0 -0
  18. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/train_combo.py +0 -0
  19. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/utils.py +0 -0
  20. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint/validations.py +0 -0
  21. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint.egg-info/SOURCES.txt +0 -0
  22. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint.egg-info/dependency_links.txt +0 -0
  23. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint.egg-info/requires.txt +0 -0
  24. {edgepoint-3.5.2 → edgepoint-3.5.4}/edgepoint.egg-info/top_level.txt +0 -0
  25. {edgepoint-3.5.2 → edgepoint-3.5.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edgepoint
3
- Version: 3.5.2
3
+ Version: 3.5.4
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
@@ -57,16 +57,17 @@ edgepoints_df, top_combos = search(df, outcome_col="hit")
57
57
 
58
58
  ## Correlation is not causation
59
59
 
60
- `search()` finds correlations, not causes. A great-looking threshold
61
- doesn't mean crossing it *causes* the outcome to improve, it may be
62
- riding along with something else entirely, or just noise. This risk is
63
- worse on small data: an 80% hit rate on 5 rows is far more likely to be
64
- a fluke than the same 80% on 500 rows.
65
-
66
- So supervise what you pass in. Only include columns with a plausible
67
- reason to relate to the outcome, treat thin `n` results as leads to
68
- investigate rather than decisions to act on, and re-run as more data
69
- arrives rather than locking in a threshold found on a thin slice.
60
+ `search()` finds correlations, not causes. A great-looking threshold
61
+ doesn't mean crossing it causes the outcome to improve, it may be riding
62
+ along with something else entirely, or just noise. This risk is worse on
63
+ small data: an 80% hit rate on 5 rows is far more likely to be a fluke
64
+ than the same 80% on 500 rows.
65
+
66
+ edgepoint is built to optimize well on small data, shrinkage, k-fold
67
+ survival, and the baseline gate all exist so a thin, noisy result doesn't
68
+ get to rank well. To get the best out of it on small data, if you have
69
+ domain knowledge, supervise which columns you pass in, If you don't have
70
+ that knowledge, let edgepoint do its magic.
70
71
 
71
72
  ---
72
73
 
@@ -29,16 +29,17 @@ edgepoints_df, top_combos = search(df, outcome_col="hit")
29
29
 
30
30
  ## Correlation is not causation
31
31
 
32
- `search()` finds correlations, not causes. A great-looking threshold
33
- doesn't mean crossing it *causes* the outcome to improve, it may be
34
- riding along with something else entirely, or just noise. This risk is
35
- worse on small data: an 80% hit rate on 5 rows is far more likely to be
36
- a fluke than the same 80% on 500 rows.
37
-
38
- So supervise what you pass in. Only include columns with a plausible
39
- reason to relate to the outcome, treat thin `n` results as leads to
40
- investigate rather than decisions to act on, and re-run as more data
41
- arrives rather than locking in a threshold found on a thin slice.
32
+ `search()` finds correlations, not causes. A great-looking threshold
33
+ doesn't mean crossing it causes the outcome to improve, it may be riding
34
+ along with something else entirely, or just noise. This risk is worse on
35
+ small data: an 80% hit rate on 5 rows is far more likely to be a fluke
36
+ than the same 80% on 500 rows.
37
+
38
+ edgepoint is built to optimize well on small data, shrinkage, k-fold
39
+ survival, and the baseline gate all exist so a thin, noisy result doesn't
40
+ get to rank well. To get the best out of it on small data, if you have
41
+ domain knowledge, supervise which columns you pass in, If you don't have
42
+ that knowledge, let edgepoint do its magic.
42
43
 
43
44
  ---
44
45
 
@@ -10,60 +10,50 @@ def aggregate_kfold_combos(
10
10
  min_coverage=None,
11
11
  ):
12
12
  """
13
- Collapses k per-fold combos_df (one per validation fold, from
14
- _score_combos_on_test) into one final row per combo.
15
-
16
- A combo's edges are already fixed (decided once by fold[0]'s
17
- recommend() call), so there's nothing to re-derive like edge_point.
18
- Instead: collect (hit_rate, n, gap) votes from every fold the combo
19
- survived, then re-apply its FIXED edges to the FULL df once for an
20
- honest hit_rate/miss_rate/gap/n/coverage (not an average of
21
- per-fold percentages). mean_gap/folds_appeared/folds_positive_gap
22
- are stability diagnostics only - used in show_progress debug output,
23
- not part of the returned df.
13
+ Collapses k per-fold combos_df snapshots (one per validation fold)
14
+ into one final row per combo.
15
+
16
+ Each combo's edges are fixed (decided once upstream), so nothing is
17
+ re-derived here. For every combo that survived at least one fold,
18
+ its fixed edges are re-applied to the FULL df once, giving an honest
19
+ hit_rate/miss_rate/gap/n/coverage (not an average of per-fold
20
+ percentages). mean_gap/folds_appeared/folds_positive_gap are
21
+ stability diagnostics carried for debug printing only, not part of
22
+ the returned df.
24
23
 
25
24
  Gates (applied before scoring, so dropped rows never affect the
26
- min-max normalization or the printed summary):
27
- - min_coverage: drop combos whose AGGREGATED coverage (re-derived
28
- here, not assumed from per-fold passes) falls below this.
29
- - baseline_full: drop combos whose AGGREGATED hit_rate falls
30
- below the caller's fixed global qualify bar.
31
- Both are optional (None = skip) and independent - a combo can fail
32
- either, both, or neither. This is the same "beat the bar or you're
33
- out" rule the per-fold gates upstream already enforce, applied once
34
- more on the final aggregated number - callers no longer need to
35
- re-filter after calling this.
25
+ min-max normalization or the summary):
26
+ - min_coverage: drop combos whose aggregated coverage falls below it.
27
+ - baseline_full: drop combos whose aggregated hit_rate falls below it.
28
+ Both optional (None = skip), independent, applied once here on the
29
+ final aggregated number.
36
30
 
37
31
  Scoring: shrink the aggregated gap by sample size (_shrink_gap),
38
32
  min-max normalize alongside coverage across surviving combos
39
- (_normalize), blend with gap_weight - same recipe as
40
- aggregate_kfold_edges/find(). Sorted best-first.
33
+ (_normalize), blend with gap_weight. Sorted best-first.
34
+
35
+ show_progress gates ALL printing in this function - the per-fold
36
+ vote trail, the dropped-combo table, the top-4 summary, and the
37
+ gate's drop-count line. Pass show_progress=False for a fully silent
38
+ call.
41
39
 
42
40
  Args:
43
41
  top_combos_df: original, un-narrowed lookup table - used to
44
42
  look up each combo's fixed edges/name by combo_num. When
45
43
  include_train_hit_rate=True, its hit_rate column is also
46
44
  carried through unchanged as train_hit_rate.
47
- combo_fold_results: per-fold combos_df snapshots from the
48
- walkthrough (already coverage-filtered per fold).
45
+ combo_fold_results: per-fold combos_df snapshots (already
46
+ coverage-filtered per fold).
49
47
  include_train_hit_rate: attaches train_hit_rate (copied as-is
50
48
  from top_combos_df's hit_rate) to the summary and returned
51
- df. Test side (test_and_combo_val.py) passes True, since
52
- top_combos_df there carries the train-computed hit_rate
53
- worth showing alongside test-aggregated numbers. Train side
54
- (train_and_combo_gen.py) doesn't - it'd be redundant there.
55
-
56
- Always prints a top-4 summary (combo_num, combo, hit_rate, coverage,
57
- n[, train_hit_rate], score) plus the gate's drop counts, regardless
58
- of show_progress (which only gates the extra per-combo debug trail).
49
+ df. Test side passes True; train side doesn't (redundant
50
+ there).
59
51
 
60
52
  Returns: combo_num, combo[, train_hit_rate], hit_rate, miss_rate,
61
53
  gap, n, coverage, score - numeric fields (except score) stringified
62
54
  to 2dp, sorted best-first. score is the same 0-100 scaled value
63
- shown in the printed summary (the internal _score, ready to compare
64
- directly against a train-side score column once merged). Same shape
65
- _format_combos_df_for_display()/_build_top_combos() already expect,
66
- plus this one new column - only rows clearing the gates are included.
55
+ shown in the printed summary. Only rows clearing the gates are
56
+ included.
67
57
  """
68
58
  k = len(combo_fold_results)
69
59
 
@@ -216,32 +206,35 @@ def aggregate_kfold_combos(
216
206
  # combo, hit_rate, coverage, n[, train_hit_rate], score) rather than
217
207
  # every internal column, so it reads consistently with every other
218
208
  # stage's printout. full_df itself (with mean_gap etc.) is kept
219
- # as-is for the return step below.
220
- print(f"\n=== aggregate_kfold_combos: summary (top 4 of {len(full_df)} combo(s)) ===")
221
- diagnostic_df = full_df.rename(columns={"_score": "score"})
222
- diagnostic_cols = ["combo_num", "combo", "hit_rate", "coverage", "n"]
223
- if include_train_hit_rate:
224
- diagnostic_cols += ["train_hit_rate"]
225
- diagnostic_cols += ["score"]
226
- diagnostic_df = diagnostic_df[[c for c in diagnostic_cols if c in diagnostic_df.columns]]
227
- for col in ["hit_rate", "coverage"]:
228
- if col in diagnostic_df.columns:
229
- diagnostic_df[col] = diagnostic_df[col].map(lambda v: f"{v:.2f}" if pd.notna(v) else "n/a")
230
- if "score" in diagnostic_df.columns:
231
- diagnostic_df["score"] = (diagnostic_df["score"] * 100).round(2)
232
- diagnostic_df = diagnostic_df.head(4)
233
- with pd.option_context(
234
- "display.width", 1000, "display.max_columns", None,
235
- "display.max_colwidth", None, "display.expand_frame_repr", False,
236
- ):
237
- print(diagnostic_df)
238
-
239
- # deferred from the gate block above - printed HERE, under the
240
- # summary df, instead of above it.
241
- if baseline_gate_msg is not None:
242
- print(baseline_gate_msg)
243
-
209
+ # as-is for the return step below. Gated by show_progress - this used
210
+ # to print unconditionally, which meant show_progress=False callers
211
+ # (e.g. test_and_combo_val.py's hardcoded call) still got console
212
+ # output; now it's silent like everything else in this function.
244
213
  if show_progress:
214
+ print(f"\n=== aggregate_kfold_combos: summary (top 4 of {len(full_df)} combo(s)) ===")
215
+ diagnostic_df = full_df.rename(columns={"_score": "score"})
216
+ diagnostic_cols = ["combo_num", "combo", "hit_rate", "coverage", "n"]
217
+ if include_train_hit_rate:
218
+ diagnostic_cols += ["train_hit_rate"]
219
+ diagnostic_cols += ["score"]
220
+ diagnostic_df = diagnostic_df[[c for c in diagnostic_cols if c in diagnostic_df.columns]]
221
+ for col in ["hit_rate", "coverage"]:
222
+ if col in diagnostic_df.columns:
223
+ diagnostic_df[col] = diagnostic_df[col].map(lambda v: f"{v:.2f}" if pd.notna(v) else "n/a")
224
+ if "score" in diagnostic_df.columns:
225
+ diagnostic_df["score"] = (diagnostic_df["score"] * 100).round(2)
226
+ diagnostic_df = diagnostic_df.head(4)
227
+ with pd.option_context(
228
+ "display.width", 1000, "display.max_columns", None,
229
+ "display.max_colwidth", None, "display.expand_frame_repr", False,
230
+ ):
231
+ print(diagnostic_df)
232
+
233
+ # deferred from the gate block above - printed HERE, under the
234
+ # summary df, instead of above it.
235
+ if baseline_gate_msg is not None:
236
+ print(baseline_gate_msg)
237
+
245
238
  print(f"\n[debug] final combos_df: {len(full_df)} combo(s) returned "
246
239
  f"(out of {len(per_combo_votes)} seen across folds)")
247
240
 
@@ -25,8 +25,9 @@ def aggregate_kfold_edges(df, outcome_col, fold_results, gap_weight=60, shrinkag
25
25
  core.py, so ranking stays consistent between per-fold results and
26
26
  this final table.
27
27
 
28
- Always prints the full diagnostic table before returning (separate
29
- from show_progress, which only gates the extra per-fold trail).
28
+ show_progress gates ALL printing in this function - the per-fold
29
+ vote trail AND the full diagnostic table. Pass show_progress=False
30
+ for a fully silent call.
30
31
 
31
32
  Returns: column, edge_point, hit_rate, miss_rate, gap, n, coverage,
32
33
  score - numeric fields (except score) stringified to 2dp, same
@@ -113,18 +114,19 @@ def aggregate_kfold_edges(df, outcome_col, fold_results, gap_weight=60, shrinkag
113
114
  full_df = full_df.sort_values("_score", ascending=False).reset_index(drop=True)
114
115
 
115
116
  # full_df (with mean_gap/folds_appeared/folds_positive_gap) is the
116
- # diagnostic view - always printed here so you can see it, regardless
117
- # of what gets returned below. The printed table drops folds_appeared
118
- # and folds_positive_gap (still computed above and kept internally in
119
- # full_df, just not shown).
120
- print(f"\n=== aggregate_kfold_edges: full diagnostic view ({len(full_df)} column(s)) ===")
121
- with pd.option_context(
122
- "display.width", 1000, "display.max_columns", None,
123
- "display.max_colwidth", None, "display.expand_frame_repr", False,
124
- ):
125
- print(full_df.drop(columns=["_shrunk_gap", "_score", "folds_appeared", "folds_positive_gap"], errors="ignore"))
126
-
117
+ # diagnostic view. Gated by show_progress - this used to print
118
+ # unconditionally regardless of the flag; now it's silent when
119
+ # show_progress=False like everything else in this function. The
120
+ # printed table drops folds_appeared and folds_positive_gap (still
121
+ # computed above and kept internally in full_df, just not shown).
127
122
  if show_progress:
123
+ print(f"\n=== aggregate_kfold_edges: full diagnostic view ({len(full_df)} column(s)) ===")
124
+ with pd.option_context(
125
+ "display.width", 1000, "display.max_columns", None,
126
+ "display.max_colwidth", None, "display.expand_frame_repr", False,
127
+ ):
128
+ print(full_df.drop(columns=["_shrunk_gap", "_score", "folds_appeared", "folds_positive_gap"], errors="ignore"))
129
+
128
130
  print(f"\n[debug] final_df: {len(full_df)} column(s) returned "
129
131
  f"(out of {len(per_column_votes)} seen across folds)")
130
132
 
@@ -443,7 +443,7 @@ def search(df, outcome_col="hit", min_coverage=10, show_progress=True, top_combo
443
443
  # baseline stats, or any later stage ever touches it.
444
444
  df = sanity_check_columns(df, outcome_col=outcome_col, show_progress=show_progress)
445
445
 
446
- train_df, test_df = split_dataset(df)
446
+ train_df, test_df = split_dataset(df, show_progress=show_progress)
447
447
 
448
448
  baseline_full = _baseline_hit_rate(df, outcome_col)
449
449
  baseline_train = _baseline_hit_rate(train_df, outcome_col)
@@ -34,21 +34,33 @@ def determine_split_ratio(row_count):
34
34
  return _SPLIT_TIERS[-1][1], _SPLIT_TIERS[-1][2]
35
35
 
36
36
 
37
- def split_dataset(df):
37
+ def split_dataset(df, show_progress=True):
38
38
  """
39
39
  Splits df chronologically (oldest -> train, newest -> test) using a
40
- ratio auto-picked from row count.
40
+ ratio auto-picked from row count via determine_split_ratio().
41
41
 
42
- Returns: train_df, test_df
42
+ Chronological only, no shuffling/stratification - oldest split_idx
43
+ rows go to train, the rest to test.
44
+
45
+ Args:
46
+ df: input dataframe to split.
47
+ show_progress: if True (default), prints the resolved row count,
48
+ split label, and train_ratio. Previously this print was
49
+ unconditional regardless of caller intent - now it's gated
50
+ like every other stage in search.py, so passing
51
+ show_progress=False silences it same as everywhere else.
52
+
53
+ Returns:
54
+ (train_df, test_df) - both with reset_index(drop=True).
43
55
  """
44
56
  row_count = len(df)
45
57
  train_ratio, label = determine_split_ratio(row_count)
46
-
47
58
  split_idx = int(row_count * train_ratio)
48
59
  train_df = df.iloc[:split_idx].reset_index(drop=True)
49
60
  test_df = df.iloc[split_idx:].reset_index(drop=True)
50
61
 
51
- print(f"Row count: {row_count:,} -> split method: {label} "
52
- f"(train_ratio={train_ratio})\n")
62
+ if show_progress:
63
+ print(f"Row count: {row_count:,} -> split method: {label} "
64
+ f"(train_ratio={train_ratio})\n")
53
65
 
54
66
  return train_df, test_df
@@ -65,7 +65,7 @@ def _score_edges_on_test(fold, outcome_col, edges_df):
65
65
  )
66
66
 
67
67
 
68
- def _aggregate_edges_on_full_df(df, outcome_col, edges_df, fold_results, gap_weight, shrinkage_k):
68
+ def _aggregate_edges_on_full_df(df, outcome_col, edges_df, fold_results, gap_weight, shrinkage_k, show_progress=True):
69
69
  """
70
70
  Test-side counterpart to aggregate_kfold_edges - never re-derives
71
71
  edge_point, never drops a column. Re-applies each frozen edge_point
@@ -148,12 +148,13 @@ def _aggregate_edges_on_full_df(df, outcome_col, edges_df, fold_results, gap_wei
148
148
 
149
149
  # Always printed, full table, no head/tail cut - pandas' own default
150
150
  # row truncation handles long tables (matches aggregate_kfold_edges).
151
- print(f"\n=== test_results_df: full diagnostic view ({len(full_df)} column(s)) ===")
152
- with pd.option_context(
153
- "display.width", 1000, "display.max_columns", None,
154
- "display.max_colwidth", None, "display.expand_frame_repr", False,
155
- ):
156
- print(full_df.drop(columns=["_shrunk_gap", "_score", "folds_appeared"], errors="ignore"))
151
+ if show_progress:
152
+ print(f"\n=== test_results_df: full diagnostic view ({len(full_df)} column(s)) ===")
153
+ with pd.option_context(
154
+ "display.width", 1000, "display.max_columns", None,
155
+ "display.max_colwidth", None, "display.expand_frame_repr", False,
156
+ ):
157
+ print(full_df.drop(columns=["_shrunk_gap", "_score", "folds_appeared"], errors="ignore"))
157
158
 
158
159
  if full_df.empty:
159
160
  return pd.DataFrame(columns=["column", "edge_point", "hit_rate", "miss_rate", "gap", "n", "coverage", "score"])
@@ -226,6 +227,7 @@ def test_and_combo_val(
226
227
  test_results_df = _aggregate_edges_on_full_df(
227
228
  test_df, outcome_col, train_results_df, edge_fold_results,
228
229
  gap_weight=GAP_WEIGHT, shrinkage_k=SHRINKAGE_K,
230
+ show_progress=show_progress,
229
231
  )
230
232
 
231
233
  # =====================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edgepoint
3
- Version: 3.5.2
3
+ Version: 3.5.4
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
@@ -57,16 +57,17 @@ edgepoints_df, top_combos = search(df, outcome_col="hit")
57
57
 
58
58
  ## Correlation is not causation
59
59
 
60
- `search()` finds correlations, not causes. A great-looking threshold
61
- doesn't mean crossing it *causes* the outcome to improve, it may be
62
- riding along with something else entirely, or just noise. This risk is
63
- worse on small data: an 80% hit rate on 5 rows is far more likely to be
64
- a fluke than the same 80% on 500 rows.
65
-
66
- So supervise what you pass in. Only include columns with a plausible
67
- reason to relate to the outcome, treat thin `n` results as leads to
68
- investigate rather than decisions to act on, and re-run as more data
69
- arrives rather than locking in a threshold found on a thin slice.
60
+ `search()` finds correlations, not causes. A great-looking threshold
61
+ doesn't mean crossing it causes the outcome to improve, it may be riding
62
+ along with something else entirely, or just noise. This risk is worse on
63
+ small data: an 80% hit rate on 5 rows is far more likely to be a fluke
64
+ than the same 80% on 500 rows.
65
+
66
+ edgepoint is built to optimize well on small data, shrinkage, k-fold
67
+ survival, and the baseline gate all exist so a thin, noisy result doesn't
68
+ get to rank well. To get the best out of it on small data, if you have
69
+ domain knowledge, supervise which columns you pass in, If you don't have
70
+ that knowledge, let edgepoint do its magic.
70
71
 
71
72
  ---
72
73
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "edgepoint"
7
- version = "3.5.2"
7
+ version = "3.5.4"
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.5.2",
8
+ version="3.5.4",
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,
File without changes
File without changes
File without changes
File without changes