Skip to main content

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): combos is the single winning combo, among the ones that passed, whichever has the lowest pick_metric on pick_on. Ties break on the other metric (also lowest wins).
  • If not enough passed (status=False): combos is {}, 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.


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_name is 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_n is the cleanup knob: set it once and older saves beyond that count get deleted automatically the next time train() 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

MIT

Metadata

Release files for edgepoint 4.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for edgepoint 4.0.0
File Size Uploaded Provenance
edgepoint-4.0.0.tar.gz 68.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for edgepoint 4.0.0
File Interpreter ABI Platform
edgepoint-4.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 146.7 kB

Release files / edgepoint-4.0.0.tar.gz

Download URL edgepoint-4.0.0.tar.gz
Size 68.6 kB
Tags Source
SHA-256 checksum
How to use checksums
aaa00d4e430162ef6fb078225b293316fe538939a6e3a3a2f967555f9c12d5b5
BLAKE2b-256 checksum
How to use checksums
70b0fb4fd610d1a35aab35e361c880e4602a646864c29661d217621f4f688148
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.0-py3-none-any.whl

Download URL edgepoint-4.0.0-py3-none-any.whl
Size 78.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
df4a5a90a4b4de0689196641acf03a7d9d9fe9e710b4d43df6c0589e9bbe9323
BLAKE2b-256 checksum
How to use checksums
2d39f79997a35c179f14e832694510823deb246d15e4c7109f4819979cdf2c3a
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.0

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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page