edgepoint
Built for data analysts who aren't data scientists, people who know a little Python but don't want to build or babysit a modeling toolbox, and who need a quick, accurate read to make a decision now.
Finds the point in a numeric column where a binary outcome (a hit/0-1 column) starts performing meaningfully better - above it, below it, or inside a range, whichever direction the data actually supports - plus the best combination of columns for that. Splits your data into train/test, searches train, replays the exact result on test, so what you get back is checked against data it never saw, not just fit once and trusted.
Where this applies: any numeric metric + binary outcome pair:
- Marketing: what engagement score is where churn drops off
- Credit: what score is where default rate becomes acceptable
- Healthcare: what biomarker level is where an outcome rate jumps
- Product: what usage count is where upgrade-to-paid spikes
- Trading / betting: what signal strength is where a pick's hit rate gets reliable (the original itch this scratched)
Who's this for: anyone who wants a plain "here's exactly where the line is, and proof it held up on data it hadn't seen" instead of a black-box model, and doesn't want to babysit a modeling toolbox to get there.
Two things changed in this version. It's no longer one-directional; it used to only check "does it get better above this point," now it checks every direction (above, below, or a range) and picks whichever one actually holds. And it's no longer a plain function (edgepoint.search(df, ...)), it's wrapped in a class called Engine, which also saves/loads results to disk for you. If you're used to the old style, read on, both are different now.
Install
pip install edgepoint
from edgepoint import Engine
Quick start
from edgepoint import Engine
engine = Engine(dir_name="my_dataset") # dir_name = where results get saved
result = engine.train(df, outcome_col="hit")
result.edgepoints # per-column thresholds (DataFrame)
result.combos # best combo(s) of thresholds (dict or list of dicts)
Everything below is a method on Engine. Create one Engine per dataset (or reuse one and pass dir_name= per call if you're juggling several).
outcome_col: telling edgepoint what "good" means
Before train() scans a single edge, it needs one thing from you that matters more than any parameter: outcome_col. This is you, the analyst, labeling every row as a win or a loss, a pass or a fail. edgepoint doesn't decide what "success" looks like in your data, it has no idea what a good sale, a good patient outcome, or a good bet even is. You decide that, once, by pointing at a column, and every number the library produces after that is downstream of that one judgment.
Concretely: outcome_col must be a column of 1/0, True/False, or a mix of both, one value per row, 1 (or True) meaning "this row counts as a success," 0 (or False) meaning "this row doesn't." No other values are allowed: edgepoint raises a ValueError before doing any work if it finds anything else in that column, rather than silently guessing which values you meant.
Creating an Engine
engine = Engine(
dir_name=None, # dataset folder name; None = don't save to disk
range_bins=15, # number of candidate edges checked per column
min_coverage=33, # min % of rows a threshold must cover, 5-100
gap_weight=100, # 0-100, how much weight gap gets vs coverage
shrinkage_k=30, # small-sample discount strength
show_progress=True, # print progress as it runs
date_match="closest", # how read_file/read_combo fall back if a date is missing
max_lookback_n=30, # how many days to look back/forward for date_match
retain_saves_n=None, # cap on saved dates kept per dataset; None = keep all
)
Every one of these is checked when you set it, pass the wrong type (like a bool where a number's expected) and you'll get a clear error immediately, not a confusing crash later.
Overfitting: gap_weight and min_coverage are a pair
They're not two separate knobs. The defaults (gap_weight=100, min_coverage=33) put full weight on the gap itself, backed by a coverage floor that's already reasonably high, a safe, trustworthy starting point. If you lower min_coverage to let thinner slices of data qualify, bring gap_weight down with it: 60, 70, or 80 is a good range. Leaving gap_weight=100 while dropping min_coverage low lets an impressive-looking gap on just a handful of rows win purely on gap size, the exact overfitting risk this pairing is there to guard against.
update(): change settings without rebuilding
engine.update(min_coverage=25, show_progress=False)
Same checks as the constructor. Only touches the settings listed above, nothing else.
train(): run the search, optionally save it
result = engine.train(
df,
outcome_col="hit",
top_combo_n=3, # how many top combos to keep
dir_name=None, # overrides the Engine's dir_name for this call
date=None, # defaults to today; used as the save's date stamp
show_progress=None, # overrides the Engine's default for this call
verbose=False, # passed through to the underlying search
overwrite=False, # False = reuse an existing save for that date instead of rerunning
)
Returns a TrainResult with .edgepoints and .combos (also unpacks as edgepoints, combos = engine.train(df) if you prefer that).
If dir_name resolves to a real name, this also saves the results to disk, dated with today (or whatever date= you gave). Calling train() again for the same date does nothing but read back the old save, unless you pass overwrite=True.
read_file(): read back a saved run's thresholds
df = engine.read_file(
dir_name=None,
date=None, # defaults to today
show_progress=None,
max_lookback_n=None,
date_match=None, # "exact", "backward_first", "forward_only", "closest", "newest"
)
Returns a DataFrame (empty if nothing was found). If the exact date isn't saved, it falls back using date_match, e.g. "closest" checks nearby dates on both sides and takes the nearest one.
read_combo(): read back a saved run's best combo(s)
combos = engine.read_combo(
dir_name=None,
date=None,
show_progress=None,
max_lookback_n=None,
date_match=None,
)
Same date-fallback behavior as read_file(). Returns a dict (single combo) or list of dicts, or {} if nothing found.
predict(): check one row of data against a saved combo
result = engine.predict(
row, # dict of {column: value}, or a single-row DataFrame
dir_name=None,
date=None,
check_n=3, # how many saved top combos to check
strict_n=2, # how many of those must pass for an overall pass
pick_metric="hit_rate", # "hit_rate" or "coverage" - what to pick the winner by
pick_on="train", # "train" or "test" - which side of that metric to use
show_progress=None,
max_lookback_n=None,
)
Returns {"status": bool, "combos": dict}.
- If enough combos passed (
status=True):combosis the single winning combo, among the ones that passed, whichever has the lowestpick_metriconpick_on. Ties break on the other metric (also lowest wins). - If not enough passed (
status=False):combosis{}, empty.
delete_combos(): remove a saved date
engine.delete_combos(date, dir_name=None, show_progress=None)
date is required (no "today" default here, you have to name what you're deleting). Deletes both the saved results file and combo file for that exact date.
list_dir(): list every saved date for a dataset
engine.list_dir(dir_name=None, show_progress=None)
Returns a list of {"name": ..., "days_ago": ...}, oldest first.
Parameter reference
A quick lookup for every parameter across every method. Anything not listed here behaves exactly as its plain-English name suggests.
Engine(...) / update(...)
| Param | Default | What it does |
|---|---|---|
dir_name |
None |
Dataset namespace. None means nothing saves to disk unless you pass dir_name= on a specific call. |
range_bins |
15 |
Number of candidate edge points checked per column. Higher = finer-grained search, slower. |
min_coverage |
33 |
Minimum % of rows a threshold must cover to qualify, 5-100. |
gap_weight |
100 |
0-100, how much the scoring favors gap size vs coverage. See the overfitting note below. |
shrinkage_k |
30 |
How hard small samples get discounted, higher = more skeptical of thin slices. |
show_progress |
True |
Print progress/results as it runs. |
date_match |
"closest" |
How read_file/read_combo (and anything that calls them, like predict) fall back when the exact date isn't saved. Five modes, see below. |
max_lookback_n |
30 |
How many days to search outward (back and/or forward) when date_match needs to fall back. |
retain_saves_n |
None |
Cap on saved dates kept per dataset. None = keep everything forever; a number = oldest saves beyond that count get auto-deleted the next time train() writes a new one. |
update() takes the same params (any subset) and applies the same validation, it never touches anything outside this list.
date_match: the five fallback modes
Used whenever a requested date isn't saved exactly. In every mode, an exact match on the date you asked for always wins immediately, these five only kick in when there's nothing there.
| Mode | Behavior |
|---|---|
"exact" |
No fallback at all. If the exact date isn't saved, that's a miss. |
"backward_first" |
Walks backward day by day up to max_lookback_n. Only if the entire backward sweep finds nothing does it then walk forward. |
"forward_only" |
Walks forward day by day up to max_lookback_n. Never falls back to backward. |
"closest" (default) |
Interleaves outward by distance, 1 day forward, 1 day back, 2 forward, 2 back, and so on. On a tie at the same distance, forward wins. |
"newest" |
Scans the whole window in both directions and returns whichever saved date is the single most recent one found, not necessarily the closest to what you asked for. |
train(df, ...)
| Param | Default | What it does |
|---|---|---|
outcome_col |
"hit" |
Your win/loss column. See the dedicated section above. |
top_combo_n |
3 |
How many top-ranked combos to keep. |
dir_name |
None |
Overrides the Engine's dir_name for this call only. |
date |
None |
Defaults to today. Used as the save's date stamp and what overwrite checks against. |
show_progress |
None |
Overrides the Engine's default for this call only. |
verbose |
False |
Passed straight through to the underlying search for extra detail. |
overwrite |
False |
False = if a save already exists for this dir_name+date, skip recomputing and read the old one back. True = always recompute. |
read_file(...) / read_combo(...)
| Param | Default | What it does |
|---|---|---|
dir_name |
None |
Falls back to the Engine's stored dir_name. |
date |
None |
Defaults to today. |
show_progress |
None |
Falls back to the Engine's default. |
max_lookback_n |
None |
Falls back to the Engine's default. |
date_match |
None |
Falls back to the Engine's default. |
predict(row, ...)
| Param | Default | What it does |
|---|---|---|
row |
required | dict of {column: value}, or a single-row DataFrame. |
check_n |
3 |
How many of the saved top combos to check row against. Fewer than check_n saved = automatic reject. |
strict_n |
2 |
How many of those check_n combos must pass for an overall pass. Must be <= check_n. |
pick_metric |
"hit_rate" |
"hit_rate" or "coverage", which stat picks the winner among combos that passed (lowest wins; the other metric breaks ties, also lowest). |
pick_on |
"train" |
"train" or "test", which split of pick_metric to use. |
show_progress, max_lookback_n, date_match |
None |
Same fallback behavior as read_file/read_combo, since predict reads a saved combo under the hood. |
delete_combos(date, ...)
date is the only required parameter anywhere in this library, no "today" default, since deleting needs an explicit target. Exact match only, no fallback walk.
list_dir(...)
Just dir_name and show_progress, both falling back to the Engine's stored defaults.
Correlation, not causation
edgepoint finds where an outcome rate changes along a metric, it doesn't tell you why. A threshold that looks great on train and holds up on test is still just an association, not proof that crossing that point causes the better outcome. There could be a third factor driving both. Treat what it returns as "here's where the pattern sits and how well it held up," not "here's a lever you can pull."
That ties into the same habit as the coverage/gap_weight guidance above: the result is a snapshot worth trusting as a snapshot, not a mechanism you've proven. It's also not permanent, re-run it as new data comes in, since a threshold that held today can shift as more rows accumulate.
Notes
dir_nameis just a folder name under a fixed root, it's not a file path, and results/combos live in their own subfolders under it.- Everything above validates its own inputs. Pass a bad type or an out-of-range value and you'll get a clear error naming the exact param and what's expected.
retain_saves_nis the cleanup knob: set it once and older saves beyond that count get deleted automatically the next timetrain()writes a new one.
No coefficients to decode, no black box, just "here's the line, and here's the proof it held." Point it at a metric and a hit column and let it do the counting for you.
License
Copyright (c) 2026 osas2henry@gmail.com. All rights reserved.
Metadata
Release files for edgepoint 4.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| edgepoint-4.0.1.tar.gz | 71.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| edgepoint-4.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 150.6 kB
Release files / edgepoint-4.0.1.tar.gz
| Download URL | edgepoint-4.0.1.tar.gz |
|---|---|
| Size | 71.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fbe62a4b78cb3acb9f6041e8bd7b80039845b66a29907258dd1b71cf077ffc50
|
|
BLAKE2b-256 checksum How to use checksums |
e3c737209eaa1a7bc08a80210e5d9704117d2fa19847c4087e27a1ed52cc5707
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Release files / edgepoint-4.0.1-py3-none-any.whl
| Download URL | edgepoint-4.0.1-py3-none-any.whl |
|---|---|
| Size | 79.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
321d3283f70a360693da3b85935eb5bb6780966cdde492b79fedc9eb312dc9c4
|
|
BLAKE2b-256 checksum How to use checksums |
d26e722b906b0d366d57f247cbfb89d3589e5440e82a4d76d78fbaf2892efa27
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Security for edgepoint 4.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.