Skip to main content

Find the point in a numeric column above which a binary outcome becomes meaningfully and reliably better.

Project description

edgepoint

Find the point in a numeric column above which a binary outcome becomes meaningfully and reliably better, then check whether that finding actually holds up on data it never saw.

Built for data analysts who aren't deep into data science and don't want to be, who just want a quick, trustworthy answer to a data question without writing a model, tuning hyperparameters, or learning a new toolbox to get there.

A note on terms: throughout this README, "threshold", "edge point", "metric", "variable", and "column" are all used interchangeably. They all just mean a numeric column in your DataFrame.

edgepoint splits your data into train and test, searches train for the best single-column threshold(s) and the best AND-combination(s) of those thresholds, then replays every one of those exact picks against test with no re-optimization. What you get back isn't just "the best split we could find", it's that, plus an honest readout of how it held up on data it never touched.

from edgepoint import search

edgepoints_df, top_combos = search(df, outcome_col="hit")

Correlation is not causation

search() finds correlations, not causes. A great-looking threshold doesn't mean crossing it causes the outcome to improve, it may be riding along with something else entirely, or just noise. This risk is worse on small data: an 80% hit rate on 5 rows is far more likely to be a fluke than the same 80% on 500 rows.

So supervise what you pass in. Only include columns with a plausible reason to relate to the outcome, treat thin n results as leads to investigate rather than decisions to act on, and re-run as more data arrives rather than locking in a threshold found on a thin slice.


Where this applies

Any continuous metric (or several) plus a binary outcome pair, for example:

  • Marketing: at what engagement score does churn risk drop off
  • Credit: at what score does default rate become acceptable
  • Healthcare: at what biomarker level does an outcome rate jump
  • Product: at what usage count does upgrade to paid rate spike
  • Manufacturing: at what sensor reading does defect rate climb
  • Trading / betting: at what signal strength value (or combination of signals) does a pick's hit rate become reliable

outcome_col: telling edgepoint what "good" means

outcome_col is your 0/1 (or True/False) judgment of what counts as a win on each row. edgepoint has no idea what a good sale, patient outcome, or bet looks like in your business. Everything else it computes (hit_rate, gap, coverage, score) is just counting, over and over, against the column you point it at.

  • It must already exist. Derive it (e.g. "closed within 7 days") before calling search().
  • One outcome per run. Two definitions of success means two calls.
  • Rare outcomes (almost all 0s or almost all 1s) leave little contrast to find an edge across, and columns are likely to get skipped for failing min_coverage.

Quick start

import pandas as pd
from edgepoint import search

df = pd.DataFrame({
    "score": [
        72.68, 58.92, 62.15, 8.13, 33.3, 66.19, 73.79, 35.43,
        65.06, 63.13, 61.47, 45.13, 39.16, 38.15, 29.41, 59.91,
        54.12, 86.35, 76.02, 63.13, 51.63, 0, 77.4, 81.66,
        50.13, 52.4, 52.82, 86.12, 57.58, 13.66, 37.29, 32.91,
        73.66, 96.2, 36.3, 22.74, 65.54, 23.98, 61.59, 32.27
    ],
    "age": [
        33.9, 41.9, 30.8, 56.5, 45.8, 45.1, 43.3, 54.3,
        52.5, 54.2, 43.9, 18, 31.3, 38, 48.3, 55.8,
        35.4, 56, 20.2, 27.7, 28.1, 39.4, 44.3, 39.9,
        53.6, 26.1, 63.1, 42.3, 46.3, 20.2, 39.2, 35.3,
        27.7, 31.8, 26.5, 32.2, 35.1, 43.5, 45.8, 43.8
    ],
    "hit": [
        1, 1, 0, 0, 0, 1, 1, 0,
        0, 1, 1, 0, 0, 0, 0, 1,
        1, 1, 1, 1, 0, 0, 0, 1,
        0, 1, 1, 1, 0, 0, 0, 0,
        1, 1, 0, 0, 1, 0, 1, 0
    ]
})

edgepoints_df, top_combos = search(
    df,
    outcome_col="hit",
    mode="quick",
    min_coverage=10,
    show_progress=True,
    top_combo_n=3,
)

With show_progress=True, search() prints a 6 stage log (data sanity/splitting, train search, test validation, merged edges, merged combos, top combo summary) as it runs. With show_progress=False it runs silently and just returns the two values below.


Auto split & auto fold

There's no train_split or k parameter to set. Both are worked out for you from the data itself:

  • Split: search() takes df exactly as ordered and treats the oldest rows as train, the newest rows as test (a walk forward split; train never sees the future relative to test). The train/test ratio is picked automatically from row count. If your rows aren't already in chronological order, or you don't want a chronological split at all, shuffle df yourself before passing it in. search() never reorders or randomizes rows on its own.
  • Folds: within train and again within test, the number of k-folds used for validation is auto-sized off row count and min_coverage, so each fold still has enough rows to mean something. Folds themselves are randomly stratified, rows are shuffled and split so each fold keeps roughly the same positive rate as the whole set, rather than plain random slicing that could hand one fold almost all 0s or almost all 1s.

How the pipeline works

  1. Data sanity / splitting: df is cleaned first. Invalid outcome rows dropped, True/False normalized to 1/0, non-numeric, constant, or near-unique predictor columns dropped, remaining NaN/inf filled. The cleaned data is then split into train/test automatically, chronologically, based on row count, with no split ratio to configure.
  2. Train section: every numeric column is searched for its best single-column threshold (percentile-based candidate edges, scored on a shrinkage-discounted gap/coverage blend), then qualifying columns are combined into AND-combinations (size capped by mode) and k-fold validated on train itself.
  3. Test section: every train-side edge and surviving combo is replayed against test, unchanged, with its own k-fold validation.
  4. Merge edges: train and test side stats for each column are merged side by side (_tr/_te suffixes) into edgepoints_df, ranked by a blended final_score.
  5. Merge combos: same idea, but only combos that qualified on both train and test survive.
  6. Top combos: the best top_combo_n combos are printed and returned as top_combos.

A combo (or column) only counts as a real edge if its hit rate clears the global baseline, the dataset's own overall hit rate ("doing nothing"), computed once on the full df and reused, unchanged, at every stage. Nothing is ever re-tuned on test.


Modes

mode controls how large a column combination can get during combo search:

Mode Combo size
"quick" up to 2 columns per combo
"standard" up to 3 columns per combo
"deep" auto-fit, the largest size that keeps total combos under a fixed safety ceiling

"quick"/"standard" are refused up front if they'd blow past that ceiling given how many columns qualified.


Parameters

Parameter Default Description
df required Input DataFrame
outcome_col "hit" Binary outcome column (0/1, True/False, or a mix)
mode "quick" "quick", "standard", or "deep", controls combo size, see above
min_coverage 10 Minimum % of rows a threshold/combo must cover to qualify (train and test)
show_progress True Print the full stage-by-stage log
top_combo_n 3 How many top combos to return. 1 returns a single dict; more returns a list

Returns

search() returns a 2-tuple: (edgepoints_df, top_combos).

edgepoints_df: one row per column that appeared on either side, with train (_tr) and test (_te) versions of hit_rate, miss_rate, gap, n, coverage, plus a blended final_score, sorted best first.

top_combos: a single dict if top_combo_n=1, else a list of up to top_combo_n dicts, each shaped like:

{
    "combo_num": 3,
    "combo": "score, age",
    "edges": {"score": 49.46, "age": 39.5},
    "hit_rate_tr": 80.0, "hit_rate_te": 60.0,
    "coverage_tr": 41.67, "coverage_te": 31.25,
    "n_tr": 10, "n_te": 5,
    "score_tr": 0.62, "score_te": 0.41,
    "final_score": 21.49,
}

If nothing qualified, you get a single dict with combo_num: None and empty edges.

What a combo actually tells you

Think of top_combos as the answer to "which traits, combined, actually separate my winners from everyone else, and by how much?" A single-column result might tell you customers 34+ buy at 58% vs 22% for everyone else, useful, but crude, since plenty of 22-year-olds buy too. A combo sharpens that: age >= 34 AND minutes_on_site >= 6.5 might come back at a 71% hit rate on train and 68% on test, covering 18% of visitors against a 24% baseline. That's a different, more useful claim. It's not age alone, it's that age stacked with engagement time is where the real signal was hiding, and it held up on data the search never saw. top_combos typically hands you a few of these to compare, so you can weigh, say, "age + time on site" against "past purchases + email opens," and pick whichever combo is both strong and backed by enough rows (n_tr/n_te) to actually trust.


Using the result correctly

The thresholds search() returns aren't meant to become permanent. They're the best candidates given the data available right now, checked once against a held-out slice of that same data. Re-run search() as new rows arrive. Each run rechecks the spike against a fresh split and tells you plainly if it moved, held, or disappeared. Treat it as proposing and stress-testing a threshold once, not certifying one forever.


Supported outcome values

outcome_col may contain 0/1, True/False, 0.0/1.0, or any mix of the above. Any other value (strings, 2, -1, etc.) raises a ValueError before any column is searched. A full stop, not a per-column skip.


Design notes

  • Percentile-based candidate edges: candidate edges follow the column's own quantiles, so dense clusters get finer cuts and sparse tails get wider ones.
  • Weighted score, not hard cutoffs: min_coverage is a hard gate; among what clears it, gap and coverage are blended into one score rather than requiring both to clear fixed minimums independently.
  • Shrinkage-discounted gaps: a dramatic gap on a handful of rows is discounted toward zero before scoring, so it can't outrank a smaller, better-supported gap. Raw hit rate/gap are always what's reported.
  • Sign-flip penalty: if a combo's gap direction flips between train and test, that's a concrete overfitting signal, so final_score is halved for that row.
  • One-directional search: edgepoint only asks "is there a point above which the rate is better?" It doesn't independently verify the below side against its own bar.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

edgepoint-3.0.1.tar.gz (62.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

edgepoint-3.0.1-py3-none-any.whl (67.9 kB view details)

Uploaded Python 3

File details

Details for the file edgepoint-3.0.1.tar.gz.

File metadata

  • Download URL: edgepoint-3.0.1.tar.gz
  • Upload date:
  • Size: 62.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for edgepoint-3.0.1.tar.gz
Algorithm Hash digest
SHA256 69fd1e7a1da406a6afa5b7c4199b9de029a8a3298ca06a139f0a14fabab6a939
MD5 6907d956d54313bfe79aeee4e463cacc
BLAKE2b-256 2ac91d0e3b4a9b9c13d06bce32621558e9aba211b2aae706c679416e50fa5aba

See more details on using hashes here.

File details

Details for the file edgepoint-3.0.1-py3-none-any.whl.

File metadata

  • Download URL: edgepoint-3.0.1-py3-none-any.whl
  • Upload date:
  • Size: 67.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for edgepoint-3.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8b30d27b4c7f15fa75a84d33af460ec01d17a135450fda85ad32edc208eb38a1
MD5 557ed0c8bde351569190a8e2c5765294
BLAKE2b-256 6a5dcd98fdec8848385be7518ebad66f8b7e06cc936a3722cb5fa579a1a012a6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page