Skip to main content

Listen to this page

Mathematics

An Elbow or Nothing

elbow-helper detects the elbows of diminishing-returns curves, with quantified uncertainty. When the evidence is missing, it abstains.

The elbow-helper logo: an articulated elbow armor piece, drawn like an antique engraving. Try elbow-helper in the browser ↗

Start with an example anyone can picture. An online shop wants to sort its customers into groups that resemble each other: the bargain hunters, the Sunday regulars, the big-basket buyers. An algorithm can do that sorting, provided you tell it in advance how many groups to form. Two groups? The sorting stays coarse. Ten? Each group becomes so small it no longer means anything. Somewhere in between hides the right number.

To find it, you draw a curve. On the horizontal axis, the number of groups tried; on the vertical axis, a measure of the disorder left inside the groups. The curve drops fast at first: each added group soaks up a lot of disorder. Then it flattens out: extra groups barely help anymore. The same diminishing-returns silhouette shows up when you ask when a model should stop training or at what budget an advertising channel saturates. The point where the curve stops paying off has an anatomical name: the elbow or knee1. Before it, every extra unit earns its keep; past it, you are just insisting.

Finding that point looks easy. That is the trap. An elbow-detection algorithm always answers something, even on a perfect straight line, even on pure noise. elbow-helper asks a harder question. Is this elbow clear? Is it the only one? Does it stay in the same place when you smooth the curve a little? Does it come back if you rerun the computation? And above all: could a curve with no elbow at all produce the same thing by chance? If a single one of those answers is bad, the tool abstains and says why. All of it runs at deraison.ai/elbow-helper, inside your browser: paste a curve and you will have the answer and its evidence before you finish this article.

The inertia curve of a k-means run against the number of clusters: it drops fast then flattens, with the detected elbow marked along with its bootstrap interval.

The textbook case: the inertia of a k-means, plotted against the number of clusters. k-means sorts points into a number of clusters fixed in advance; its inertia is the sum of the distances from each point to the center of its cluster. The detected elbow, its bootstrap interval and the quantified evidence beside it.


Motivation

Existing methods, starting with the Kneedle algorithm that everyone refers to2, are very good at proposing a location. But they never say how much they believe it: a point, with no error bar, no probability, no right to stay silent. On a noisy curve, that lone point is far too easy to over-read. You pick four clusters, size a cache, stop an experiment. All of it because of an accident of noise.

Faced with a blurry scan, a radiologist does not diagnose. They write that the image does not support a conclusion, then ask for another one. A statistical program facing a noisy curve should show the same restraint: only conclude what the data allows. The comparison has a limit: the radiologist exercises judgment, while the program applies thresholds fixed once and for all. So it is more predictable, but also less subtle.

elbow-helper's priority is therefore clear: avoid false elbows, even at the cost of staying silent more often. The program that calls it has no choice: the answer is either a ClearKnee or a NoClearKnee. The first gives the elbow's position, a range and the evidence in figures. That range aims to contain the true position in roughly nine cases out of ten, but so far only the test family of curves confirms it. The second gives a code saying why the tool kept quiet, readable by a machine. Never a quiet fallback to a doubtful estimate: abstention is handled, not worked around.

The tools already available come in three groups. First, those that look for the elbow and nothing else: kneed3, kneebow4, Yellowbrick's visualizer5. They always answer with a point, and none of them can say "there is nothing here". Then the changepoint libraries, ruptures6 foremost. They are excellent, but they answer a different question: how many breaks in a signal, with no concern for returns that shrink. The third group leaves the judgment to a human, by eye or through a language model, which does not reproduce. elbow-helper's closest relative is none of the three: it is R's segmented package7, which starts from the same idea. Saying there is an elbow means saying how sure you are. But staying silent when the evidence is missing stays rare across all of them. That is elbow-helper's main contribution. The comparison comes with numbers: the ratings table and the map summarizing its eleven criteria are at the end of this article.

That leaves the question of whether the tool is really usable. The package is published on PyPI, the public index pip install pulls Python libraries from, across eight releases from v0.1.0 to v0.1.7. No code is accepted until the 85 automated tests pass on Python 3.12, nor until the style checker is happy. Every week, the same test suite is replayed across the four supported Python versions, 3.10 through 3.13. Those 85 tests execute 96% of the package's lines.

You reach the same chain through four doors. The Python library. A command to type in a terminal. A web interface other programs can put their questions to, known as an HTTP API. And a server offering the same operations to a conversational assistant, through the Model Context Protocol, shortened to MCP. These four doors are thin layers over one shared core, so none of them can say anything different from the others. Add to that the web app, which runs the chain end to end inside a browser tab. And a LaTeX note proving every formula in the package with a worked example: ELBOW.pdf, with LIKELIHOOD.pdf alongside. Both exist in English and in French, kept in step with the code.


How it works

Before anything is looked for, the curve is put in order: invalid points dropped, x values sorted and deduplicated, both scales brought back between 0 and 1. That rescaling takes one precaution. It does not anchor itself on the lowest and highest points, but on the 5th and 95th percentiles. The 5th percentile is the value below which 5% of the points fall. The 95th is the one below which 95% of them fall. One outlier alone therefore cannot squash the whole curve against an edge.

Then comes a first shape check. Does the curve rise, or fall, without ever changing direction? We then say it is monotone. A Spearman correlation8 answers that by looking only at the order of the values, never at their size. A second check, which takes the gaps into account, confirms it. A curve with no clear trend is turned away at once, before any elbow is looked for.

You do not have to say which way the curve goes, unless you want to force it. The program reads it from the sign of the trend. To know whether the curve is hollow or domed, it compares it to the chord joining its first and last point. Above the chord, it is domed. Below, it is hollow like a bowl. All four cases are therefore covered, domed or hollow, rising or falling, without your having to name the shape. If you do give it, yours wins.

The curve is clean and its direction known. What remains is to say what we are looking for, starting with the word "elbow", which deserves an honest definition. Take a square-root curve, rising fast then settling down. Draw the straight line joining its two ends, then subtract it from the curve. What remains is a bump, zero at both ends, largest where the curve pulls furthest from that line. That peak is the elbow. Once the data is brought between 0 and 1, that difference curve reads:

$$d(x_i) \;=\; y_{\text{norm}}(x_i) - x_{\text{norm},\,i}$$

Its local maxima, the points higher than their immediate neighbors, are the candidates. A sensitivity threshold then decides whether a peak is a real elbow or a mere wiggle: the candidate must dominate its neighborhood by a margin proportional to a parameter \(S\):

$$T \;=\; d_{\max} - S \cdot \overline{|\Delta x_{\text{norm}}|}$$

where the bar denotes the mean gap between two neighboring x values. The larger \(S\), the wider the required margin. This whole search is replayed over a grid of smoothing windows and sensitivities. Only candidates that come back at the same place for every setting survive. A real elbow holds when you blur the curve a little; an accident of noise disappears.

That peak search has a name and authors. It is Kneedle2, described by Satopää, Albrecht, Irwin and Raghavan in 2011, rewritten here from scratch in NumPy. The way it walks the curve, the orientation table and the sensitivity threshold follow closely the choices of Kevin Arvai's kneed library3, BSD licensed and credited as such in the repository. elbow-helper does not need it to run. What elbow-helper adds is therefore elsewhere: in what it demands of a candidate before agreeing to call it an elbow.

Then comes the check by computation. First demand: the slope has to change, and change in a way three stray points could not manufacture. That is what a Theil-Sen estimator9 measures. The usual method, least squares, looks for the line that makes the sum of squared gaps as small as possible, so one wild value weighs very heavily. Theil-Sen goes another way. It computes the slope of every pair of points, then takes the middle value, the median. A few wild points no longer decide the result.

That leaves the model itself to write. A real elbow is a change of slope: a straight line, then a different slope past the point \(k\). To build that broken line in one piece, with no jump, we use a function that is zero before \(k\) and rises steadily after:

$$y \;=\; a + b\,x + c \cdot \max(0,\; x - k)$$

The coefficient \(c\) carries the whole story of the elbow. If \(c\) is zero, we are back to a straight line with no elbow. The further \(c\) gets from zero, the sharper the slope breaks at \(k\). This broken model then has to beat the plain line on two grounds. The first is an exam in pieces10: we hide part of the curve, then see whether the model guesses what was hidden. We take out whole stretches, never isolated points, because an isolated point is far too easy to guess from its neighbors. The second ground is a score that compares the two models while accounting for their complexity. But that score first requires saying what "fits better" means.


What "fits better" means: likelihood

A model fits the data well when, if it were true, what we observe would be no surprise. That is the idea of likelihood. To compute it, we first have to commit to a model of the randomness. Each observed point departs a little from the theoretical curve. That departure is Gaussian noise: the bell-curve randomness that many small errors produce when they add up. Each departure is drawn independently of the others, with a typical size written \(\sigma\). Every observation then gets a probability density, that is, a number saying how expected that value was. The closer the point falls to the proposed curve, the larger that number.

Textbooks multiply those numbers together, across the \(n\) points. That product raises a practical problem. Multiplying \(n\) numbers all smaller than one gives a result that collapses at speed as \(n\) grows. A curve of 80 points and a curve of 800 then stop being comparable at all. So elbow-helper takes the likelihood per observation: multiply the \(n\) numbers, then take the \(n\)-th root, which amounts to an average. The number of points therefore stops weighing in:

$$L(\theta,\sigma^2) \;:=\; \exp\big(\mathbb{E}[\ln p]\big) \;=\; L_{\text{raw}}^{\,1/n}$$

With bell-curve noise, everything simplifies in one stroke. What remains is the mean squared error, that is, the average of the squared gaps between the observed points and the fitted curve:

$$L(\theta,\sigma^2) \;=\; (2\pi\sigma^2)^{-1/2} \exp\!\left(-\frac{\mathrm{MSE}(\theta)}{2\sigma^2}\right)$$

The logarithm is not an arbitrary choice here. The Gaussian density contains an exponential, and the natural logarithm undoes it exactly.

$$\ell(\theta,\sigma^2) \;=\; -\tfrac{1}{2}\ln(2\pi\sigma^2) \;-\; \frac{\mathrm{MSE}(\theta)}{2\sigma^2}$$

That line holds a small surprise. At fixed noise, making the mean squared error as small as possible is exactly making the likelihood as large as possible. We learn least squares as a geometric recipe. We learn maximum likelihood as a statistical principle. They are the same thing. Pushing the computation a little further lands on a quantity information theory calls perplexity. The word comes from language models, where perplexity reads as the number of words the model hesitates between at each step. Here, on numbers rather than words, it measures the same thing: the spread of values the model thinks possible. The lower it is, the less the model hesitates:

$$\mathrm{PPL}(\theta) \;=\; \exp\big(-2\,\ell(\theta,\hat\sigma^2)\big) \;=\; 2\pi e \cdot \mathrm{MSE}(\theta)$$

Scaled back up to the whole sample, it gives the negative log-likelihood \(\mathrm{NLL}\), written through the residual sum of squares \(\mathrm{RSS}\), the mean squared error before dividing by \(n\):

$$\mathrm{NLL} \;=\; n \ln\!\left(\frac{\mathrm{RSS}}{n}\right) \;+\; n\,(1 + \ln 2\pi)$$

Comparing two models on their fit alone would be rigged from the start. A model with more dials always clings closer to the data, right up to hugging the noise itself. So the dials have to be paid for. That is what the BIC family of scores does11. BIC stands for Bayesian Information Criterion. These scores start from the likelihood, then add a penalty proportional to the number of dials spent. The elbow costs dials. It has to pay them back in fit. Otherwise the plain line wins.

What remains is making all of this readable for a human. The reflex would be \(R^2\), the textbook fit score. It compares the model's error to the one you would make by always answering the same value, the mean of the observations: 1 for a perfect fit, 0 for "no better than answering the mean". The problem is that the mean is exactly the fixed value that minimizes the error: it is therefore the best possible opponent among dumb answers, and an \(R^2\) can drop below zero. elbow-helper compares itself to a frankly bad opponent instead: the observed point from which it is hardest to predict all the others.

$$y_{\text{bad}} \;:=\; \operatorname*{arg\,max}_{y_j}\; \frac{1}{n}\sum_i (y_i - y_j)^2 \qquad\Longrightarrow\qquad \text{quality} \;=\; 1 - \frac{\mathrm{MSE}(\theta)}{\mathrm{MSE}_{\text{worst}}}$$

That score is 1 for a perfect fit, and 0 for a fit as bad as that worst fixed value. Since this reference is always worse than the mean, falling below zero is far harder than with an \(R^2\). That is the number printed in the figure's legend. Three others sit beside it: the detection probability, the \(p\)-value defined in the next section, and a weight comparing the two models. That last one is the odds between the elbow model and the plain line, computed from the BIC gap12. A word of caution: those odds only hold when you have many observations. Reading them as a probability would further require deciding in advance how much you trust each of the two models, which nothing in the data supplies. So it is taken for what it is: a weight comparing two models, with no claim to being a probability.


The robustness trials

A model can win that duel on the data we have, and still be a whim of the noise. Two trials guard against it. The first is called the bootstrap13. The name comes from the English expression about pulling yourself up by your own bootstraps: lacking other datasets, you make some out of the one you have. You subtract the fitted model from the curve, which leaves the gaps. You shuffle those gaps at random, then paste them back onto the model. Each copy is a curve you might have observed had chance drawn differently. The whole search is redone on each copy. To pass, the elbow must come back at the same place in at least 90% of the copies, with a tight range. The second trial is the plain-line test. Thousands of straight lines carrying the same noise level are simulated. We then count how often chance alone produces evidence as strong as the one observed.

That number, the \(p\)-value, deserves a pause, because it is the most misread quantity in all of statistics. It does not give the probability that there is no elbow. It says the opposite: supposing there is none, how often chance alone would manufacture evidence this convincing. A \(p\)-value of 0.01 means a pure straight line would manage it once in a hundred tries. That is little, so we keep the elbow. It is not zero, so we can be wrong once in a hundred. This detour through simulation is not a luxury. If there is no elbow, then its position does not exist, so the classical tests lose the reference they normally lean on14. Both bars therefore have to be cleared:

$$\widehat{p}_{\text{detect}} \;\ge\; 0.9 \qquad\text{and}\qquad p_{\text{null}} \;\le\; 0.01$$

Only a candidate that clears all those gates becomes a ClearKnee. The diagnostic figure shows that case file next to the curve. On abstention, it switches to an honest state: curve greyed out, reason displayed. Never a marker that would suggest more certainty than there is.

A lightly noisy curve whose slope changes once: the confirmed break is marked with a vertical dashed line near the middle.

Low noise. The slope change is confirmed and placed at x = 0.463, marked by the dashed line. The true break sits at 0.5: the gap is the discretization onto the supplied points.

The next figure shows the same curve, the same break underneath, with stronger noise and nothing else changed.

The same curve, far noisier: no dashed line at all, the figure states that no break is confirmed.

High noise, identical true shape. No break confirmed, so no marker drawn: the abstention reads off the figure itself.


What abstaining means

Abstaining is not failing in silence. It is an answer, and one that has a name. The function always returns one of two objects, never anything else, so the program calling it has to look at which one before going on. Here are both answers as they print, on the same curve at two noise levels:

>>> from elbow_helper import robust_knee

>>> robust_knee(x, y)            # noise of standard deviation 0.02
ClearKnee(knee_x=0.3038, ci90=(0.3038, 0.3418), detection_rate=0.98, null_p=0.00498)

>>> robust_knee(x, y_noisy)      # same elbow, ten times the noise
NoClearKnee(reason='INCOMPATIBLE_GLOBAL_SHAPE')

The first line reads without documentation. Elbow at 0.304. Range running from 0.304 to 0.342. Elbow found again in 98% of the copies. Probability that an elbow-free straight line would do as well: five in a thousand. The second gives no position at all. That is deliberate: at that noise level the curve no longer even passes the shape check, its trend being too faint.

Every refusal carries a stable code, readable by a machine as much as by a human. Those codes form a list settled in advance, fifteen reasons and not one more. A dashboard can therefore count its abstentions, and learn whether its curves are too short, too noisy, or simply ambiguous.

Returned code What it says
Data preparation
INVALID_INPUT the inputs are unusable: mismatched lengths, no finite value, an unknown shape requested.
INSUFFICIENT_DATA fewer points than the required minimum, twenty by default: too short for the later trials to mean anything.
ZERO_RANGE the x axis does not vary: every point sits at the same place.
INCOMPATIBLE_GLOBAL_SHAPE the curve does not have the shape announced or has no trend clear enough to infer one.
Candidate search
NO_KNEE_CANDIDATES the difference curve has no peak at all, at any smoothing scale.
ALL_CANDIDATES_WEAK peaks exist, but none stands out enough against the local noise.
BOUNDARY_KNEE the best candidate hugs one end of the curve, where an elbow cannot be told apart from an edge effect.
NO_PERSISTENT_CLUSTER no candidate comes back to the same place when the smoothing scale changes.
MULTIPLE_PLAUSIBLE_KNEES two elbows are equally defensible; answering would amount to a coin flip.
Statistical confirmation
WEAK_SLOPE_CHANGE the slope, once measured robustly, changes too little to deserve the word elbow.
SEGMENTED_MODEL_NOT_BETTER the broken line fails to beat the plain line, on blocked cross-validation or on the BIC.
BOOTSTRAP_UNSTABLE the elbow is recovered in only a minority of the reshuffled-noise replays.
BOOTSTRAP_MULTIMODAL it is recovered often, but at two distinct places depending on the replay.
NULL_NOT_REJECTED a straight line with no elbow, carrying the same noise, would too easily produce evidence this strong.
INTERNAL_NUMERICAL_FAILURE a computation failed; the package says so instead of returning a doubtful number.

The fifteen ways of saying no, in the order the pipeline can meet them.

That detail has a practical consequence: a refusal says what to do. INSUFFICIENT_DATA asks for more points. BOOTSTRAP_MULTIMODAL hints that there may be two elbows rather than none, and points at the plural search described next. NULL_NOT_REJECTED says the curve is pretty, but that noise alone would account for it.


Five doors, one verdict

A detector is only useful if you can call it from where you work. elbow-helper therefore offers the same chain through five entry points. The Python library, for a notebook. The terminal command, for a script or for those automatic checks replayed at every code change. The HTTP API, for a service written in another language than Python. The MCP server, for a conversational assistant. And the web app, for anyone who would rather install nothing. The first four share one core and offer the same four operations. knee returns an elbow with its uncertainty. elbow is the shortcut for the k-means case, where the curve is known in advance to be hollow and falling. diagnostics returns the figure, as SVG, a drawing format that stays sharp at any size. And locator returns the position alone, with no evidence. What one returns, the others return too.

Installing means choosing your entry point. The core depends on NumPy alone. The network entry points are optional extras, in brackets, installed only if you use them:

$ pip install elbow-helper            # library + command line
$ pip install "elbow-helper[api]"     # + the HTTP API
$ pip install "elbow-helper[mcp]"     # + the MCP server (which pulls the API in)

The library

Two functions are enough. robust_knee takes a curve and infers its shape. robust_elbow is the shortcut for the k-means case, where the shape is known in advance. The x values are optional: if you give only the y values, they will be numbered from zero to n minus one.

from elbow_helper import robust_knee, robust_elbow, RobustKneeConfig

res = robust_knee(x, y)                     # shape and direction inferred
res = robust_elbow(k_values, inertia)       # the k-means case, shape fixed
res = robust_knee(y)                        # implicit x: 0, 1, 2, ...

# More replays: slower, safer.
res = robust_knee(x, y, config=RobustKneeConfig(bootstrap_replicates=500,
                                                null_replicates=1000,
                                                random_seed=0))

if res.is_clear:
    print(res.knee_x, res.ci90, res.detection_rate, res.null_p_value)
else:
    print("no clear elbow:", res.reason)

Every threshold lives in RobustKneeConfig, a structure you do not edit: you make a copy with config.with_(...). The shipped settings aim at the second rather than at the decimal: a hundred bootstrap copies, two hundred simulated lines. The values above are validation-grade, five to ten times slower.

The command line

It offers the same four operations and answers in JSON, a text format programs read back without ambiguity, which lets you chain it to the usual tools. Data comes in as comma-separated values, as a NumPy file, or as a CSV column named by its number:

$ elbow-helper knee --y-values 0,0.1,0.3,0.6,0.85,0.9,0.92,0.93,0.94,0.95
{
  "reason": "INSUFFICIENT_DATA",
  "diagnostics": {
    "curve": "auto",
    "direction": "auto",
    "n": 10,
    "min_samples": 20
  },
  "is_clear": false
}

$ elbow-helper elbow --x-npy k.npy --y-npy inertia.npy
$ elbow-helper knee --y-csv measures.csv:2 --config-json '{"bootstrap_replicates": 500}'
$ elbow-helper diagnostics --x-npy k.npy --y-npy inertia.npy > diagnostics.svg

The first command deserves a pause. The curve it receives looks exactly right, and any classic detector would have returned a number. But ten points are not enough to claim there is an elbow. The answer is therefore a documented refusal, stating how many points were supplied and how many would be needed. No obliging estimate. The rule holds through every entry point.

The HTTP API

A server starts in one line and listens on four addresses, named after the four operations. The curve travels as JSON, and so do the settings, inside a config_overrides object. The diagnostics address returns the SVG itself, with no wrapper around it.

# uvicorn: the server that runs the Python application.
$ uvicorn elbow_helper.api:app --port 8000

$ curl -s -X POST localhost:8000/knee -H 'Content-Type: application/json' \
       -d '{"x": [0, 0.0127, ...], "y": [0.0043, 0.0361, ...],
            "config_overrides": {"random_seed": 0}}'
{"reason": "CLEAR_KNEE", "knee_x": 0.3038, "ci90": [0.3038, 0.3418],
 "detection_rate": 0.98, "null_p_value": 0.00498, "is_clear": true,
 "diagnostics": {...}}                       # response abridged

The MCP server

The MCP server is that same application, with the four operations declared as tools a conversational assistant can discover and call, served at /mcp:

$ uvicorn elbow_helper.mcp_server:app --port 8021
# The assistant connects to http://127.0.0.1:8021/mcp and finds
# four tools there: knee, elbow, diagnostics, locator.

The point goes beyond convenience. Show a curve to an assistant: it will happily produce a plausible elbow, with nothing to contradict it. Wired to this tool, it gets a computed verdict, with its range and its \(p\)-value, or a named refusal it cannot turn into certainty.

The web app

The last surface asks for nothing: deraison.ai/elbow-helper runs the whole pipeline in the browser, on your data, without uploading it anywhere. Paste a curve and you get the same verdict and the same diagnostic figure as through the other four doors, in English or in French.


Several elbows: the dynamic program

A curve sometimes changes regime several times: three pricing tiers on a demand curve, say. The question then becomes: how many breaks, and where? That is what robust_knees answers, in the plural. Changing the question forces a change of model. The curve is cut into pieces and each piece gets its own straight line, with a jump allowed at the joint. So we give up the single unbroken line from earlier. That is deliberate. As soon as several breaks are free to move, the cost of a partition no longer splits piece by piece: moving one break changes what its neighbors have to satisfy. And it is exactly that splitting into small independent problems that makes the search fast.

First ingredient: being able to compute the cost of one piece in a single step. Fitting a line through \(m\) points and measuring its error looks like it needs a pass over every point. It does not: six running totals suffice,

$$S_x = \sum_i x_i,\quad S_y = \sum_i y_i,\quad S_{xx} = \sum_i x_i^2,\quad S_{xy} = \sum_i x_i y_i,\quad S_{yy} = \sum_i y_i^2$$

from which the least-squares slope and intercept follow and then the piece's error:

$$b = \frac{m\,S_{xy} - S_x S_y}{m\,S_{xx} - S_x^2}, \qquad a = \frac{S_y - b\,S_x}{m}, \qquad \mathrm{RSS} = S_{yy} - a\,S_y - b\,S_{xy}$$

That last equality falls out of the least-squares equations. It says the cost of any stretch of the curve can be read off those totals, without ever touching the points again. Computing them once takes time proportional to the number of points. Each later piece cost is then obtained in constant time, however long the curve is.

Second ingredient: the search itself. Trying every way to place \(k\) breaks among \(n\) points is out of reach: the number of combinations runs into the billions. Dynamic programming sidesteps the obstacle: it builds the answer from smaller answers already computed. Write \(C[k][t]\) for the lowest cost of explaining the first \(t\) points with \(k+1\) pieces:

$$C[0][t] = \text{cost}(0, t), \qquad C[k][t] = \min_{s}\Big( C[k-1][s] + \text{cost}(s, t) \Big)$$

The formula reads as follows. Try every possible position \(s\) of the last cut. For each one, add two things. On one side, the best cost of explaining the first \(s\) points with one break fewer: it is already computed, so you look it up. On the other, the cost of the final piece, from \(s\) to \(t\). Then keep the minimum.

Why is looking up a smaller answer legitimate rather than recomputing everything? Take the reasoning backwards. Suppose the best partition of the first \(t\) points has its last cut at \(s^\ast\), but its beginning is not the cheapest possible partition of that beginning. We could then swap that beginning for the cheaper one, keep the final piece as it is, and get a valid partition cheaper still than the best. That is impossible. So the beginning of an optimal solution is itself optimal: that is Bellman's principle15.

Filling every cell of the table stays reasonable, where brute enumeration demanded an astronomical number of operations. One arrow kept in each cell then lets you walk back through the table to find where the cuts are. This is the classic method of changepoint detection. PELT16 is built on it and speeds it up, abandoning partitions that have become hopeless along the way. Here it is checked against full enumeration on small inputs, rather than taken on faith.

Third ingredient: choosing the number of breaks. The more you add, the closer you cling to the data, down to the absurdity of one piece per point. A modified BIC17 settles it: it adds a penalty that charges for each break, while watching the length of each piece:

$$\mathrm{mBIC}(k) \;=\; n \ln\!\left(\frac{\mathrm{RSS}(k)}{n}\right) \;+\; 3k \ln n \;-\; \sum_{j=1}^{k+1} \ln\!\left(\frac{\ell_j}{n}\right)$$

The minus sign in front of that sum deserves an anecdote, because it says how this project works. The literature writes that term with a plus. Tested as written, it produces the opposite of its intended effect: the term being always negative, adding it rewards uneven partitions instead of punishing them. With a minus, short and uneven pieces are properly penalized, as announced. The convention kept here is therefore the one the tests confirm. The discrepancy is documented rather than hidden. What is not settled should be said too. A sign that looks flipped can have three causes. A different definition of the likelihood. A score you minimize instead of maximize. Or a length normalized another way. Matching it term by term with the original formulation would take an appendix the repository has yet to write. So this is a measured disagreement, not a correction to the literature.

The winner is finally confirmed by a permutation test. The curve is shuffled thousands of times: none of those shuffled versions carries a real break, so almost all of them must do worse than the real fit. And since running more tests means more chances to get lucky, a Bonferroni correction tightens each test's bar in proportion to how many are run18.

One nuance separates the singular from the plural. It matters. In the plural, an empty answer is not an abstention: it is a conclusion. This curve has no real break. That verdict cleared exactly the same gates as a non-empty answer. Only a preparation problem, an invalid input, too little data or a zero range, returns an error instead of a list of breaks.

Those choices, dynamic programming over the greedy method, the minus sign, the permutation confirmation, were settled by measurement. Ten combinations were put in competition. On one hand, two ways of searching: dynamic programming, which examines every partition, and the greedy method, which lays its breaks down one at a time and never revisits them. On the other, five ways of choosing how many breaks there are. Two times five. Each method handled three hundred curves built for the test: a hundred points each, twenty-five copies for every combination of four break counts, zero to three, and three noise levels. The labels on the two figures below read as follows. DP is dynamic programming; Greedy is the greedy method. BIC is the criterion alone; mBIC_add and mBIC_sub are the two signs of the length term. ICL is a neighboring criterion that also charges for the uncertainty of the partition19. FWER is the permutation test with the Bonferroni correction.

A bar chart ranking ten methods by the probability of recovering the true number of breaks, from 0.85 for DP+mBIC_sub down to 0.49 for Greedy+BIC.

Probability of recovering the true number of breaks. The top five methods all rest on the dynamic program; the bottom five are their greedy twins.

Two lessons read straight off that ranking. First, the full search beats the greedy method every time, and by a wide margin: 0.85 against 0.60 for the same criterion. The mechanism is known20: the greedy method commits early to an imperfect cut. The cuts that follow then go into repairing that error instead of describing the curve. Second, the sign of the length term, the anecdote told above, comes with a number too. With the minus, the score is 0.85; with the literature's plus, 0.77.

That leaves the flaw this project cares about most: inventing a break where there is none. The second figure measures exactly that, on curves drawn deliberately flat.

A bar chart of false-positive rates on curves with no break at all: 0.00 for DP+FWER and DP+mBIC_sub, up to 0.27 for DP+BIC.

Probability of announcing at least one break on a curve that has none. The bare BIC is wrong one time in four, the drift theory has long ascribed to it21; the subtractive form and the permutation gate are never wrong across these three hundred curves.

One limit is shared by every method. It deserves stating. At high noise, with three real breaks, none of them recovers all three: the best announces 1.8 on average. So the error runs toward fewer elbows, never toward more. That is exactly the direction this package accepts being wrong in. These numbers hold for that family of curves and for it alone. The full protocol is published in the repository, with the detailed table by noise level22.

The whole thing stands on a single computational dependency, NumPy. The search algorithm is rewritten from scratch. The figure is hand-written SVG, with no drawing library. Every formula in the chain, from the rescaling to the permutation test, is proved step by step in the repository's mathematical note, ELBOW.pdf23. That note is written intuition-first, with a worked example before every formula. The part on likelihood is treated separately, in LIKELIHOOD.pdf24, because it owes nothing to curve fitting.


What it does not do

A project that claims caution has to state its own limits. Otherwise it commits the overconfidence it holds against the others.

The smoother assumes x values that are roughly regularly spaced: a curve sampled any which way would defeat it. The position returned is one of the supplied points, not a value in between. With few points, the elbow can therefore land a few samples away from the truth. On the family of curves used for testing, the error stays under 5% of the curve's width half the time. The simulated line and the bootstrap make another assumption: they assume noise of constant size from one end of the curve to the other, and one point that does not depend on the next. The variants that drop that assumption remain to be written.

Finally, the thresholds are values calibrated on a documented family of curves, not universal constants. 90% redetections and a \(p\)-value at 1% are defensible choices, not laws of nature. The package is at version 0.1. Its author says so: if your decision depends on it, recalibrate on your own curves. No method working on limited data is infallible. This one merely makes its assumptions checkable.


Conclusion

A detector that knows when to stay silent serves you better than one that always answers. "No clear elbow" is real information: it keeps you from sizing a system, stopping an experiment or settling a budget on a piece of chance. That displayed confidence is paid for in abstentions. It is a trade I stand by.

elbow-helper installs in one line, pip install elbow-helper. It can also be tried with nothing installed, in the online sandbox, where the full pipeline runs in your browser without uploading your data. The code, the examples, the comparison with neighboring tools and the mathematical notes all live in the GitHub repository. Bring a curve; you will leave with a defensible elbow or with a good reason not to believe in one.


The landscape, tool by tool

That leaves placing all of this. The repository rates eight ways of looking for an elbow, on eleven criteria. The rating is about the job this package does, which is announcing an elbow only when the evidence supports it. No tool is punished for being excellent at another job.

Elbow Detection Tool Noise robustness Shape inference Explicit abstention Multiple breakpoints Significance test Uncertainty Model selection Light dependencies One-call ease Reproducible Published maths
elbow-helper ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
kneed ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
ruptures ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
kneebow ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
KElbowVisualizer ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
R segmented package ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
Manual eyeballing ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
Ask an LLM ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️

The repository's eleven criteria, from one to five stars. The highlighted row is the package this article is about.

Positioning map of the eight tools: elbow-helper sits alone in the precise and adaptable quadrant, while the classic locators cluster on the nimble and thorough side.

The same table seen from above: eleven columns summarized into two axes by principal component analysis, the technique that projects a table onto the two directions that best summarize it, the axes then named by hand. elbow-helper sits alone in the precise and adaptable corner. Map drawn by Standpoint, the tool that reads a ratings table and draws its map.

  • elbow-helper: the only one here that can refuse to answer. An elbow has to hold from one smoothing scale to the next, survive the bootstrap and beat the null model. The tool then returns either an elbow with its interval, or a named abstention code.
  • kneed: the reference implementation of Kneedle, deterministic and immediate. A point, always, with no error bar and no way out.
  • kneebow: the same geometric idea, obtained by rotating the curve. Very light on dependencies, but it commits on every call.
  • KElbowVisualizer: the most common way to read a k-means elbow in practice: plot it, look at it. A visualization tool built for one curve shape, inheriting scikit-learn and matplotlib.
  • ruptures: the right tool when the question really is “how many breakpoints and where”, with PELT and binary segmentation already in place. Diminishing returns mean nothing to it.
  • R segmented package: statistically the most rigorous: broken-line regression, standard errors, Davies test, multiple breakpoints. It asks for R and real statistical fluency.
  • Manual eyeballing: a careful eye can say “I see nothing clear here”, which most tools cannot. But the judgment reproduces neither across people nor at scale.
  • Ask an LLM: describes a shape in words fluently and hedges when asked, with no calibrated uncertainty, no checkable derivation, let alone two identical answers.


Bibliography

Here are the works this package depends on, ordered as the article uses them. Each note says what the source establishes and where it does its work here. The classics are cited for the standard result they ground, the repository's own notes for the derivations and measurements they carry.

  1. Thorndike, R. L. (1953). "Who belongs in the family?", Psychometrika, 18(4), 267-276. 10.1007/BF02289263 A presidential address to the Psychometric Society, commonly given as the first statement of the elbow criterion: plot a measure of spread against the number of groups and look for the bend. It sets the question this article lives on, without offering a decision rule.
  2. Satopää, V., Albrecht, J., Irwin, D. and Raghavan, B. (2011). "Finding a Kneedle in a Haystack: Detecting Knee Points in System Behavior", ICDCSW, IEEE. Defines the knee as the maximum gap to the chord after normalization, with a sensitivity threshold: the locator rewritten here. Its limit is in its own statement: it proposes a location, it does not say whether the knee exists.
  3. Arvai, K. kneed, Python library, three-clause BSD license. github.com/arvkevi/kneed The reference Python implementation of Kneedle. Its traversal logic, orientation table and sensitivity threshold guided elbow-helper's NumPy rewrite, which carries no runtime dependency on it.
  4. kneebow, Python library, MIT license. github.com/georg-un/kneebow Rotates the curve and takes the extremum of the rotated data. Representative of the camp that always answers with a point and can never keep quiet.
  5. Bengfort, B. and Bilbro, R. (2019). "Yellowbrick: Visualizing the Scikit-Learn Model Selection Process", Journal of Open Source Software, 4(35), 1075. 10.21105/joss.01075 A visual-diagnostics library for scikit-learn, including the KElbowVisualizer that plots inertia and marks an elbow. The tool aims at reading by eye, not at a quantified decision.
  6. Truong, C., Oudre, L. and Vayatis, N. (2020). "Selective review of offline change point detection methods", Signal Processing, 167, 107299. arxiv.org/abs/1801.00718 A review that sorts changepoint detection into three parts, a cost function, a search method and a constraint on the number of changes; it accompanies the ruptures library. That is the frame this article's multi-elbow part inherits, on a question adjacent to, but distinct from, diminishing returns.
  7. Muggeo, V. M. R. (2003). "Estimating regression models with unknown break-points", Statistics in Medicine, 22(19), 3055-3071. 10.1002/sim.1545 Estimates a breakpoint's position by successive linearizations and returns a standard error with it, hence an interval. It is elbow-helper's closest relative on the idea that a knee claim should carry its uncertainty; the R package segmented is its implementation.
  8. Spearman, C. (1904). "The Proof and Measurement of Association between Two Things", American Journal of Psychology, 15(1), 72-101. A correlation computed on ranks rather than on values. It serves here as a shape screen, indifferent to scale and little disturbed by a few extreme points.
  9. Theil, H. (1950). "A Rank-Invariant Method of Linear and Polynomial Regression Analysis", Proceedings of the Royal Netherlands Academy of Sciences, 53; Sen, P. K. (1968). "Estimates of the Regression Coefficient Based on Kendall's Tau", Journal of the American Statistical Association, 63(324), 1379-1389. Together they establish the slope estimator built from the median of all pairwise slopes. It is what lets the pipeline demand a slope change without letting a handful of outliers decide the verdict.
  10. Arlot, S. and Celisse, A. (2010). "A survey of cross-validation procedures for model selection", Statistics Surveys, 4, 40-79. 10.1214/09-SS054 A review that carefully separates what is proved from what is merely observed about cross-validation and shows that the shape of the held-out blocks has to match the dependence in the data. That is the justification for holding out contiguous stretches here.
  11. Schwarz, G. (1978). "Estimating the Dimension of a Model", The Annals of Statistics, 6(2), 461-464. Establishes the BIC: a penalty equal to the number of parameters times the logarithm of the number of observations, derived as an approximation to the marginal likelihood. It is the score the broken line has to beat to earn its elbow.
  12. Kass, R. E. and Raftery, A. E. (1995). "Bayes Factors", Journal of the American Statistical Association, 90(430), 773-795. Gives the approximation by which a BIC gap reads as odds between two models and the interpretation scale that goes with it. That is the posterior probability shown in the diagnostic figure's legend.
  13. Efron, B. (1979). "Bootstrap Methods: Another Look at the Jackknife", The Annals of Statistics, 7(1), 1-26. Introduces resampling as a stand-in for a sampling distribution nobody can write down. Here it is the residual noise that gets reshuffled, to measure how often and where the elbow comes back.
  14. Davies, R. B. (1977). "Hypothesis testing when a nuisance parameter is present only under the alternative", Biometrika, 64(2), 247-254. 10.1093/biomet/64.2.247 Treats exactly the elbow's difficulty: a parameter that exists only if the alternative hypothesis is true, here the location \(k\). The likelihood-ratio test loses its usual asymptotic distribution there, which is why the \(p\)-value is simulated rather than read off a table.
  15. Bellman, R. (1957). Dynamic Programming, Princeton University Press. The canonical source of the principle of optimality: the start of an optimal solution is optimal for its own sub-problem. That is what licenses the recursion used here to place several breaks.
  16. Killick, R., Fearnhead, P. and Eckley, I. A. (2012). "Optimal Detection of Changepoints With a Linear Computational Cost", Journal of the American Statistical Association, 107(500), 1590-1598. PELT: the same exact recursion, sped up by pruning that permanently discards partitions which have become hopeless, giving a linear cost under conditions on the penalty.
  17. Zhang, N. R. and Siegmund, D. O. (2007). "A Modified Bayes Information Criterion with Applications to the Analysis of Comparative Genomic Hybridization Data", Biometrics, 63(1), 22-32. Introduces the modified BIC whose extra term watches segment lengths. It is the reference whose sign, taken at face value first, had to be corrected here once its real effect was measured.
  18. Dunn, O. J. (1961). "Multiple Comparisons Among Means", Journal of the American Statistical Association, 56(293), 52-64. 10.1080/01621459.1961.10482090 Establishes and puts to work the inequality statistics calls Bonferroni: dividing the threshold by the number of tests bounds the risk of being wrong at least once. Used here as soon as several break counts are put to the test.
  19. Biernacki, C., Celeux, G. and Govaert, G. (2000). "Assessing a Mixture Model for Clustering with the Integrated Completed Likelihood", IEEE Transactions on Pattern Analysis and Machine Intelligence, 22(7), 719-725. The ICL criterion, which adds to the BIC a penalty on the uncertainty of the assignment. A competitor in the comparison, where it reaches 0.82 accuracy and a 4% false-positive rate.
  20. Fryzlewicz, P. (2014). "Wild Binary Segmentation for Multiple Change-Point Detection", The Annals of Statistics, 42(6), 2243-2281. Analyzes the flaw in greedy searches, which commit to a cut too early and propagate the error and fixes it with randomly drawn intervals. This article's comparison recovers that flaw: every greedy method stays below its exhaustive twin.
  21. Yao, Y.-C. (1988). "Estimating the number of change-points via Schwarz' criterion", Statistics and Probability Letters, 6(3), 181-189. Studies the BIC applied to the number of changepoints and documents its tendency to keep too many. The 27% false-positive rate measured here is one instance of that, on piecewise-linear curves.
  22. Harchaoui, W. (2026). Multi-knee method comparison: results, research note in the elbow-helper repository. research/multiknee/RESULTS.md The full protocol behind the comparison above: ten methods, three hundred curves each, results broken down by break count and noise level, runtimes and the six conclusions drawn from them.
  23. Harchaoui, W. (2026). ELBOW: Noise-Robust Knee and Elbow Detection, technical note in the elbow-helper repository. doc/ELBOW-en.pdf Derives every formula in the package step by step, from normalization to the permutation test, intuition and a worked example before each one. It is the long version of everything this article compresses.
  24. Harchaoui, W. (2026). LIKELIHOOD, technical note in the elbow-helper repository. doc/LIKELIHOOD-en.pdf The likelihood groundwork revisited above: why the per-observation likelihood, the geometric mean of the densities, rather than the raw product and how the same construction reads on a classification model.

To close, three readings friendlier than a journal paper. My favorite AI books supply the annotated panorama the next two come from. The Elements of Statistical Learning by Hastie, Tibshirani and Friedman covers model selection and cross-validation at large. Information Theory, Inference and Learning Algorithms by MacKay traces the link, sketched here, between likelihood, coding and perplexity.