elbow-helper 0.1.2__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.
- elbow_helper-0.1.2/LICENSE +28 -0
- elbow_helper-0.1.2/PKG-INFO +269 -0
- elbow_helper-0.1.2/README.md +237 -0
- elbow_helper-0.1.2/pyproject.toml +59 -0
- elbow_helper-0.1.2/setup.cfg +4 -0
- elbow_helper-0.1.2/src/elbow_helper/__init__.py +65 -0
- elbow_helper-0.1.2/src/elbow_helper/_core_cli.py +321 -0
- elbow_helper-0.1.2/src/elbow_helper/api.py +209 -0
- elbow_helper-0.1.2/src/elbow_helper/bootstrap.py +135 -0
- elbow_helper-0.1.2/src/elbow_helper/candidates.py +88 -0
- elbow_helper-0.1.2/src/elbow_helper/cli_argparse.py +308 -0
- elbow_helper-0.1.2/src/elbow_helper/cli_click.py +314 -0
- elbow_helper-0.1.2/src/elbow_helper/clustering.py +269 -0
- elbow_helper-0.1.2/src/elbow_helper/config.py +137 -0
- elbow_helper-0.1.2/src/elbow_helper/locator.py +320 -0
- elbow_helper-0.1.2/src/elbow_helper/mcp_server.py +100 -0
- elbow_helper-0.1.2/src/elbow_helper/metrics.py +102 -0
- elbow_helper-0.1.2/src/elbow_helper/multi_criteria.py +46 -0
- elbow_helper-0.1.2/src/elbow_helper/multi_fwer.py +160 -0
- elbow_helper-0.1.2/src/elbow_helper/multi_pipeline.py +141 -0
- elbow_helper-0.1.2/src/elbow_helper/multi_segmentation.py +212 -0
- elbow_helper-0.1.2/src/elbow_helper/null_test.py +93 -0
- elbow_helper-0.1.2/src/elbow_helper/numerics.py +217 -0
- elbow_helper-0.1.2/src/elbow_helper/pipeline.py +190 -0
- elbow_helper-0.1.2/src/elbow_helper/plotting.py +1032 -0
- elbow_helper-0.1.2/src/elbow_helper/preprocessing.py +312 -0
- elbow_helper-0.1.2/src/elbow_helper/search.py +104 -0
- elbow_helper-0.1.2/src/elbow_helper/segmented.py +176 -0
- elbow_helper-0.1.2/src/elbow_helper/smoothing.py +118 -0
- elbow_helper-0.1.2/src/elbow_helper/types.py +426 -0
- elbow_helper-0.1.2/src/elbow_helper.egg-info/PKG-INFO +269 -0
- elbow_helper-0.1.2/src/elbow_helper.egg-info/SOURCES.txt +47 -0
- elbow_helper-0.1.2/src/elbow_helper.egg-info/dependency_links.txt +1 -0
- elbow_helper-0.1.2/src/elbow_helper.egg-info/entry_points.txt +4 -0
- elbow_helper-0.1.2/src/elbow_helper.egg-info/requires.txt +19 -0
- elbow_helper-0.1.2/src/elbow_helper.egg-info/top_level.txt +1 -0
- elbow_helper-0.1.2/tests/test_abstention.py +72 -0
- elbow_helper-0.1.2/tests/test_api.py +144 -0
- elbow_helper-0.1.2/tests/test_auto_shape.py +72 -0
- elbow_helper-0.1.2/tests/test_cli_argparse.py +171 -0
- elbow_helper-0.1.2/tests/test_cli_click.py +156 -0
- elbow_helper-0.1.2/tests/test_locator.py +64 -0
- elbow_helper-0.1.2/tests/test_mcp_server.py +172 -0
- elbow_helper-0.1.2/tests/test_multiknee.py +265 -0
- elbow_helper-0.1.2/tests/test_numerics.py +54 -0
- elbow_helper-0.1.2/tests/test_pipeline_synthetic.py +55 -0
- elbow_helper-0.1.2/tests/test_preprocessing.py +56 -0
- elbow_helper-0.1.2/tests/test_real_world_examples.py +160 -0
- elbow_helper-0.1.2/tests/test_stages.py +62 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Warith Harchaoui
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: elbow-helper
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Noise-robust knee/elbow detection: a conservative wrapper around a from-scratch locator that either returns a knee with uncertainty or explicitly abstains.
|
|
5
|
+
Author-email: Warith Harchaoui <warithmetics@deraison.ai>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/warith-harchaoui/elbow-helper
|
|
8
|
+
Keywords: knee,elbow,model-selection,k-means,changepoint
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Requires-Dist: os-helper>=1.8
|
|
18
|
+
Provides-Extra: cli
|
|
19
|
+
Requires-Dist: click<9,>=8.1; extra == "cli"
|
|
20
|
+
Provides-Extra: api
|
|
21
|
+
Requires-Dist: fastapi<1,>=0.111; extra == "api"
|
|
22
|
+
Requires-Dist: uvicorn<1,>=0.30; extra == "api"
|
|
23
|
+
Provides-Extra: mcp
|
|
24
|
+
Requires-Dist: fastapi-mcp<1,>=0.4; extra == "mcp"
|
|
25
|
+
Requires-Dist: mcp<2,>=1.20; extra == "mcp"
|
|
26
|
+
Requires-Dist: fastapi<1,>=0.111; extra == "mcp"
|
|
27
|
+
Requires-Dist: uvicorn<1,>=0.30; extra == "mcp"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# elbow-helper
|
|
34
|
+
|
|
35
|
+
[π«π·](https://github.com/warith-harchaoui/elbow-helper/blob/main/LISEZ-MOI.md) | [π¬π§](https://github.com/warith-harchaoui/elbow-helper/blob/main/README.md)
|
|
36
|
+
|
|
37
|
+
**Noise-robust knee and elbow detection: it reports a knee with uncertainty; it abstains outright otherwise.**
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
An algorithm can answer the question "where might a knee or an elbow be?" and it will always return something, even on a straight line or on pure noise. `elbow-helper` wraps a from-scratch knee locator in a conservative decision procedure that answers a harder question instead:
|
|
42
|
+
|
|
43
|
+
> Is any candidate knee strong, unique, persistent, reproducible, and unlikely under a no-knee model? If not, say so.
|
|
44
|
+
|
|
45
|
+
The design priority is to minimise false-positive knees, even at the cost of more abstentions.
|
|
46
|
+
|
|
47
|
+
## Documentation
|
|
48
|
+
|
|
49
|
+
[π» Documentation](https://harchaoui.org/warith/ai-helpers/docs/elbow-helper-doc/)
|
|
50
|
+
|
|
51
|
+
[πΊοΈ Landscape](https://github.com/warith-harchaoui/elbow-helper/blob/main/LANDSCAPE.md)
|
|
52
|
+
|
|
53
|
+
[π Examples](https://github.com/warith-harchaoui/elbow-helper/blob/main/EXAMPLES.md)
|
|
54
|
+
|
|
55
|
+
## Why it exists
|
|
56
|
+
|
|
57
|
+
A knee, on a curve of diminishing returns, marks the point past which extra input buys little extra output: more clusters in k-means, more iterations in an optimiser, more budget on a marketing channel. Existing knee-detection heuristics are excellent at proposing where that point sits, but they carry no notion of confidence. A single point estimate on a noisy curve is easy to over-trust in practice. This package turns that point estimate into a decision backed by evidence. It refuses to answer when the evidence is weak.
|
|
58
|
+
|
|
59
|
+
## Dependencies
|
|
60
|
+
|
|
61
|
+
The whole package depends on `numpy` and [`os-helper`](https://github.com/warith-harchaoui/os-helper) only: nothing else, not even for the diagnostic figure. The knee-locating algorithm is implemented from scratch, in NumPy only, so there is no `scipy`, `scikit-learn`, `statsmodels`, or `joblib` runtime dependency. `elbow_helper.plotting` writes hand-authored SVG (see Diagnostics below) rather than reaching for matplotlib, so it needs no extra install. See Acknowledgements below for the implementation this algorithm follows.
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e . # everything: numpy + os-helper, diagnostics included
|
|
67
|
+
pip install -e ".[dev]" # + pytest
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Prefer conda? `conda env create -f environment.yaml && conda activate elbow-helper` sets up Python and pip, then installs from `requirements.txt`, the same dependency list `pyproject.toml` declares.
|
|
71
|
+
|
|
72
|
+
## Quickstart
|
|
73
|
+
|
|
74
|
+
See [`EXAMPLES.md`](https://github.com/warith-harchaoui/elbow-helper/blob/main/EXAMPLES.md) for more recipes: the k-means elbow, an explicit abstention, an exponential-saturation curve, the diagnostic figure, the standalone locator, and configuration tuning.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import numpy as np
|
|
78
|
+
from elbow_helper import robust_knee, RobustKneeConfig
|
|
79
|
+
|
|
80
|
+
x = np.linspace(0, 1, 80)
|
|
81
|
+
y = np.where(x <= 0.3, 3*x, 0.9 + 0.2*(x - 0.3))
|
|
82
|
+
y = y / y.max() + np.random.default_rng(0).normal(0, 0.02, x.size)
|
|
83
|
+
|
|
84
|
+
result = robust_knee(x, y, config=RobustKneeConfig(random_seed=0))
|
|
85
|
+
|
|
86
|
+
if result.is_clear:
|
|
87
|
+
print(result.knee_x, result.ci90, result.detection_rate, result.null_p_value)
|
|
88
|
+
else:
|
|
89
|
+
print("no clear knee:", result.reason)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`curve` and `direction` are optional: left unset, both are inferred from the data (see below) and can still be passed explicitly, e.g. `robust_knee(x, y, curve="concave", direction="increasing", ...)`. `y` is optional too: call `robust_knee(y)` alone and `x` is taken to be the implicit `0, 1, ..., n-1`.
|
|
93
|
+
|
|
94
|
+
For the classic k-means or scree "elbow" (convex, decreasing):
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from elbow_helper import robust_elbow
|
|
98
|
+
result = robust_elbow(k_values, inertia) # curve/direction fixed to convex/decreasing
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Automatic shape and direction
|
|
102
|
+
|
|
103
|
+
Leaving `curve` or `direction` unset infers them from the cleaned, normalised data. `direction` comes from the sign of the trend between `x` and `y`. `curve` comes from whether the (lightly smoothed) curve lies above or below the straight chord connecting its first and last point: above is concave, below is convex, the mathematical definition, holding for both increasing and decreasing curves. This covers all four concave/convex Γ increasing/decreasing combinations without asking the caller to name the shape up front. Passing `curve` or `direction` explicitly always overrides the inferred value.
|
|
104
|
+
|
|
105
|
+
## The contract: a tagged union
|
|
106
|
+
|
|
107
|
+
`robust_knee` always returns one of two types, both subclasses of `KneeResult` and distinguished by `.is_clear`:
|
|
108
|
+
|
|
109
|
+
- `ClearKnee`: `knee_x`, `knee_x_norm`, `knee_index`, `ci90` (a 90% bootstrap interval, in data units), `detection_rate`, `smoothing_window`, `sensitivity`, `prominence`, `slope_contrast`, `bic_improvement`, `null_p_value`, and the full `diagnostics`.
|
|
110
|
+
- `NoClearKnee`: a machine-readable `reason` code plus `diagnostics`.
|
|
111
|
+
|
|
112
|
+
You are forced to handle abstention explicitly: there is no silent fallback to a guess.
|
|
113
|
+
|
|
114
|
+
## How it decides: the pipeline
|
|
115
|
+
|
|
116
|
+
1. **Preprocess.** Clean, sort, deduplicate, and robustly normalise the data to the unit square, then screen its global shape with a Spearman correlation (a rank-based measure of how monotonic the curve is) and a magnitude-weighted monotonicity check.
|
|
117
|
+
2. **Scale-space search.** Run the from-scratch locator across a grid of Gaussian smoothing windows and sensitivities; collect every candidate it proposes.
|
|
118
|
+
3. **Basic filters.** Reject boundary knees, weak prominence, and a low prominence-to-noise ratio.
|
|
119
|
+
4. **Persistence clustering.** Keep only knees that recur at a stable location across consecutive smoothing scales and most sensitivities. Abstain if two knees look equally plausible (`MULTIPLE_PLAUSIBLE_KNEES`).
|
|
120
|
+
5. **Model confirmation.** Require a robust slope change, via a Theil-Sen estimator (a regression method that stays accurate even with outliers), plus a continuous broken-line fit that beats a single straight line on blocked cross-validation and on the BIC (Bayesian Information Criterion, a score that rewards fit while penalising extra parameters).
|
|
121
|
+
6. **Bootstrap.** Re-run the whole search on an IID residual bootstrap (independent, identically distributed resampling of the leftover noise). The knee must be redetected at least 90% of the time, with a tight, unimodal interval.
|
|
122
|
+
7. **No-knee null test.** Run a Monte Carlo test against a straight-line null model that carries the accepted model's noise scale. The observed knee must be significant at p β€ 0.01.
|
|
123
|
+
|
|
124
|
+
Only a candidate that clears every gate becomes a `ClearKnee`.
|
|
125
|
+
|
|
126
|
+
## Abstention reason codes
|
|
127
|
+
|
|
128
|
+
`INSUFFICIENT_DATA`, `INVALID_INPUT`, `ZERO_RANGE`, `INCOMPATIBLE_GLOBAL_SHAPE`,
|
|
129
|
+
`NO_KNEE_CANDIDATES`, `ALL_CANDIDATES_WEAK`, `NO_PERSISTENT_CLUSTER`,
|
|
130
|
+
`MULTIPLE_PLAUSIBLE_KNEES`, `BOUNDARY_KNEE`, `WEAK_SLOPE_CHANGE`,
|
|
131
|
+
`SEGMENTED_MODEL_NOT_BETTER`, `BOOTSTRAP_UNSTABLE`, `BOOTSTRAP_MULTIMODAL`,
|
|
132
|
+
`NULL_NOT_REJECTED`, `INTERNAL_NUMERICAL_FAILURE`.
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Every threshold lives in `RobustKneeConfig`, a frozen dataclass; call `config.with_(...)` to change one. All positional thresholds are expressed in normalised x-range units. The shipped defaults suit a first practical prototype: modest replicate counts, so a run finishes in seconds.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
RobustKneeConfig(bootstrap_replicates=100, null_replicates=200)
|
|
140
|
+
# validation-grade:
|
|
141
|
+
config.with_(bootstrap_replicates=500, null_replicates=1000)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
A note on these thresholds: they are calibrated, conservative defaults, not universal constants. `cluster_tolerance` and `max_neighbor_shift` sit slightly above the reference plan's 0.05 to absorb the locator's one- or two-sample discretisation jitter at modest sample sizes (n of about 60 to 100). Recalibrate them against your own curve and noise family if needed.
|
|
145
|
+
|
|
146
|
+
## Diagnostics
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from elbow_helper.plotting import plot_diagnostics
|
|
150
|
+
plot_diagnostics(x, y, curve="concave", direction="increasing", out="diag.svg")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+

|
|
154
|
+
|
|
155
|
+
Hand-authored SVG, no matplotlib, no extra to install: the diagnostic is a core feature. The curve and its located knee sit next to a compact evidence legend backing the point estimate: the detection probability, the null-model p-value, the slope contrast, a BIC-derived posterior probability (the odds, under Kass & Raftery's Bayes-factor approximation, that the knee model is correct), and a fit-quality score normalized against a deliberately pessimistic worst case rather than the easily-beaten sample mean (see `doc/ELBOW-en.tex` / `doc/ELBOW-fr.tex` for the derivation of both). When the evidence is too weak, the figure switches to an honest abstention state instead: a greyed, dashed curve and the reason, never a marker implying more confidence than the data supports. Pass `language="fr"` for the French chrome text.
|
|
156
|
+
|
|
157
|
+
## Limitations
|
|
158
|
+
|
|
159
|
+
- The current smoother assumes a regular or near-regular spacing in `x`.
|
|
160
|
+
- Auto-detected `curve` and `direction` need a curve with a real, unambiguous trend; on data too weak or noisy to classify confidently, the pipeline abstains with `INCOMPATIBLE_GLOBAL_SHAPE` rather than guessing, the same gate that already screens explicit `curve`/`direction` arguments.
|
|
161
|
+
- Detection is discretised to sample locations. On modest `n`, the located knee can sit within a few samples of the truth (median error at or below about 5% of the x-range, on the supported synthetic family).
|
|
162
|
+
- The straight-line null and the IID residual bootstrap suit roughly homoscedastic (constant-variance), uncorrelated noise. Wild or moving-block bootstrap variants are future work.
|
|
163
|
+
- No finite-data method is infallible. The targets above hold for the documented simulation family, not for every possible curve.
|
|
164
|
+
|
|
165
|
+
## Standalone locator
|
|
166
|
+
|
|
167
|
+
The from-scratch locator is usable on its own:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from elbow_helper import KneeLocator
|
|
171
|
+
kl = KneeLocator(x, y, S=1.0, curve="concave", direction="increasing", online=True)
|
|
172
|
+
kl.knee, kl.all_knees
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Multiple knees: `robust_knees`
|
|
176
|
+
|
|
177
|
+
`robust_knee` answers "is there one knee?". A curve with several genuine
|
|
178
|
+
regime changes, three pricing tiers on a demand curve, say, needs a
|
|
179
|
+
different question: how many breakpoints does this curve actually have and
|
|
180
|
+
where are they? `robust_knees` (plural) answers that one. It searches over every
|
|
181
|
+
possible number of segments with a dynamic program, scores each candidate
|
|
182
|
+
with a modified BIC (a fit-quality score that penalises extra breakpoints,
|
|
183
|
+
so adding one has to earn its keep), then confirms the winning count with a
|
|
184
|
+
Bonferroni-gated permutation test to hold the false-positive rate down as
|
|
185
|
+
the search space grows.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
import numpy as np
|
|
189
|
+
from elbow_helper import RobustKneesConfig, robust_knees
|
|
190
|
+
|
|
191
|
+
rng = np.random.default_rng(3)
|
|
192
|
+
x = np.linspace(0, 1, 100)
|
|
193
|
+
y = np.piecewise(
|
|
194
|
+
x,
|
|
195
|
+
[x < 0.3, (x >= 0.3) & (x < 0.65), x >= 0.65],
|
|
196
|
+
[lambda t: 3 * t, lambda t: 0.9 + 0.2 * (t - 0.3), lambda t: 0.97 + 2.2 * (t - 0.65)],
|
|
197
|
+
) + rng.normal(0, 0.02, x.size)
|
|
198
|
+
|
|
199
|
+
result = robust_knees(x, y, config=RobustKneesConfig(random_seed=0, fwer_permutations=200))
|
|
200
|
+
print(result)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
Knees(k=2, x=[0.2929, 0.6465])
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The two breakpoints land close to the curve's true regime changes at 0.3 and
|
|
208
|
+
0.65. Unlike `robust_knee`, an empty result here is not an abstention: it is
|
|
209
|
+
the pipeline's confident conclusion that the curve has no real breakpoint,
|
|
210
|
+
having survived the same search and false-positive gates a nonempty result
|
|
211
|
+
would have had to survive. Only a preprocessing failure (bad input, too
|
|
212
|
+
little data, zero range) returns `InvalidKnees` instead of `Knees`. See
|
|
213
|
+
`research/multiknee/RESULTS.md` and `doc/ELBOW-en.tex` (sections 5-20) for
|
|
214
|
+
the validation behind this design.
|
|
215
|
+
|
|
216
|
+
## Mathematics
|
|
217
|
+
|
|
218
|
+
`doc/ELBOW-en.tex` ([π«π· doc/ELBOW-fr.tex](https://github.com/warith-harchaoui/elbow-helper/blob/main/doc/ELBOW-fr.tex)) derives every formula this package runs: the single-knee pipeline's normalisation, Spearman screen, difference-curve knee search, persistence clustering, Theil-Sen slope, BIC, blocked cross-validation, bootstrap and null test, and the multi-knee research behind `robust_knees` (see also `research/multiknee/RESULTS.md`). It is written intuition-first, with a worked example before every formula, for readers anywhere from the end of high school to a Ph.D. in applied mathematics. Its Gaussian likelihood foundation, the general theory behind why `L := exp(E[log p])` rather than a raw product, and how the same construction reads on a classification model, is factored out into a companion note, `doc/LIKELIHOOD-en.tex` ([π«π· doc/LIKELIHOOD-fr.tex](https://github.com/warith-harchaoui/elbow-helper/blob/main/doc/LIKELIHOOD-fr.tex)), since that foundation doesn't depend on curve-fitting at all. Citations are in `doc/references.bib`, including a few pointers into my own [Favourite AI books](https://deraison.ai/ai-books) where a technique used here deserves a book-length treatment. Native LaTeX (not Markdown), given the audience: compile with `latexmk -pdf ELBOW-en.tex` (or `ELBOW-fr.tex`, `LIKELIHOOD-en.tex`, `LIKELIHOOD-fr.tex`) from inside `doc/`, or read the compiled copies directly, `doc/ELBOW-en.pdf` / `doc/ELBOW-fr.pdf` / `doc/LIKELIHOOD-en.pdf` / `doc/LIKELIHOOD-fr.pdf`.
|
|
219
|
+
|
|
220
|
+
## Landscape
|
|
221
|
+
|
|
222
|
+
[πΊοΈ Landscape](https://github.com/warith-harchaoui/elbow-helper/blob/main/LANDSCAPE.md) ([π«π· PAYSAGE.md](https://github.com/warith-harchaoui/elbow-helper/blob/main/PAYSAGE.md)): how `elbow-helper` compares to `kneed`, `ruptures`, `kneebow`, Yellowbrick's `KElbowVisualizer`, R's `segmented` package, manual eyeballing and asking an LLM, rated on 11 criteria and positioned on a PCA map.
|
|
223
|
+
|
|
224
|
+
## CLI / API / MCP
|
|
225
|
+
|
|
226
|
+
Beyond the Python library, `elbow-helper` exposes three more doors onto the same pipeline: an argparse CLI (always installed), a click CLI twin, and an HTTP API with an MCP server mounted on it. All four are thin adapters over the same shared core (`elbow_helper._core_cli`), so none of them can drift from what the library itself returns.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pip install -e . # library + argparse CLI
|
|
230
|
+
pip install -e ".[cli]" # + the click twin
|
|
231
|
+
pip install -e ".[api]" # + the FastAPI HTTP surface
|
|
232
|
+
pip install -e ".[mcp]" # + the MCP server (pulls in [api] too)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# argparse (always available)
|
|
237
|
+
elbow-helper knee --y-values 0,0.1,0.3,0.6,0.85,0.9,0.92,0.93,0.94,0.95
|
|
238
|
+
|
|
239
|
+
# click twin
|
|
240
|
+
elbow-helper-click knee --y-values 0,0.1,0.3,0.6,0.85,0.9,0.92,0.93,0.94,0.95
|
|
241
|
+
|
|
242
|
+
# HTTP API
|
|
243
|
+
uvicorn elbow_helper.api:app --reload
|
|
244
|
+
curl -X POST localhost:8000/knee -d '{"x": [0,0.1,0.3,0.6,0.85,0.9]}'
|
|
245
|
+
|
|
246
|
+
# MCP (fastapi-mcp mounted on the same app, at /mcp)
|
|
247
|
+
uvicorn elbow_helper.mcp_server:app --port 8021
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Every surface exposes the same four operations (`knee`, `elbow`, `diagnostics`, `locator`), matching `robust_knee`, `robust_elbow`, `plot_diagnostics`, and the standalone `KneeLocator` one-to-one. Data goes in as inline comma-separated values, a `.npy` file, or a CSV column (CLI), or a JSON body (`x`/`y` lists, HTTP). `RobustKneeConfig` overrides travel as `--config-json '{"bootstrap_replicates": 500}'` (CLI) or a `config_overrides` object (HTTP). The `diagnostics` operation returns the SVG itself, not a JSON wrapper around it.
|
|
251
|
+
|
|
252
|
+
## Author
|
|
253
|
+
|
|
254
|
+
[Warith HARCHAOUI](https://linkedin.com/in/warith-harchaoui)
|
|
255
|
+
|
|
256
|
+
## Acknowledgements
|
|
257
|
+
|
|
258
|
+
The from-scratch Kneedle implementation in `elbow_helper/locator.py` follows the algorithm described by SatopÀÀ, Albrecht, Irwin and Raghavan (ICDCSW 2011). Its traversal logic, orientation table and sensitivity threshold closely follow the implementation choices of [`kneed`](https://github.com/arvkevi/kneed) by Kevin Arvai, released under the BSD-3-Clause license:
|
|
259
|
+
|
|
260
|
+
> Copyright (c) 2017, Kevin Arvai
|
|
261
|
+
> All rights reserved.
|
|
262
|
+
>
|
|
263
|
+
> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: (1) redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer; (2) redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
264
|
+
|
|
265
|
+
`elbow-helper` has no runtime dependency on `kneed`: the algorithm is reimplemented in NumPy only, with the scipy calls replaced as documented in `locator.py`.
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# elbow-helper
|
|
2
|
+
|
|
3
|
+
[π«π·](https://github.com/warith-harchaoui/elbow-helper/blob/main/LISEZ-MOI.md) | [π¬π§](https://github.com/warith-harchaoui/elbow-helper/blob/main/README.md)
|
|
4
|
+
|
|
5
|
+
**Noise-robust knee and elbow detection: it reports a knee with uncertainty; it abstains outright otherwise.**
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
An algorithm can answer the question "where might a knee or an elbow be?" and it will always return something, even on a straight line or on pure noise. `elbow-helper` wraps a from-scratch knee locator in a conservative decision procedure that answers a harder question instead:
|
|
10
|
+
|
|
11
|
+
> Is any candidate knee strong, unique, persistent, reproducible, and unlikely under a no-knee model? If not, say so.
|
|
12
|
+
|
|
13
|
+
The design priority is to minimise false-positive knees, even at the cost of more abstentions.
|
|
14
|
+
|
|
15
|
+
## Documentation
|
|
16
|
+
|
|
17
|
+
[π» Documentation](https://harchaoui.org/warith/ai-helpers/docs/elbow-helper-doc/)
|
|
18
|
+
|
|
19
|
+
[πΊοΈ Landscape](https://github.com/warith-harchaoui/elbow-helper/blob/main/LANDSCAPE.md)
|
|
20
|
+
|
|
21
|
+
[π Examples](https://github.com/warith-harchaoui/elbow-helper/blob/main/EXAMPLES.md)
|
|
22
|
+
|
|
23
|
+
## Why it exists
|
|
24
|
+
|
|
25
|
+
A knee, on a curve of diminishing returns, marks the point past which extra input buys little extra output: more clusters in k-means, more iterations in an optimiser, more budget on a marketing channel. Existing knee-detection heuristics are excellent at proposing where that point sits, but they carry no notion of confidence. A single point estimate on a noisy curve is easy to over-trust in practice. This package turns that point estimate into a decision backed by evidence. It refuses to answer when the evidence is weak.
|
|
26
|
+
|
|
27
|
+
## Dependencies
|
|
28
|
+
|
|
29
|
+
The whole package depends on `numpy` and [`os-helper`](https://github.com/warith-harchaoui/os-helper) only: nothing else, not even for the diagnostic figure. The knee-locating algorithm is implemented from scratch, in NumPy only, so there is no `scipy`, `scikit-learn`, `statsmodels`, or `joblib` runtime dependency. `elbow_helper.plotting` writes hand-authored SVG (see Diagnostics below) rather than reaching for matplotlib, so it needs no extra install. See Acknowledgements below for the implementation this algorithm follows.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e . # everything: numpy + os-helper, diagnostics included
|
|
35
|
+
pip install -e ".[dev]" # + pytest
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Prefer conda? `conda env create -f environment.yaml && conda activate elbow-helper` sets up Python and pip, then installs from `requirements.txt`, the same dependency list `pyproject.toml` declares.
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
See [`EXAMPLES.md`](https://github.com/warith-harchaoui/elbow-helper/blob/main/EXAMPLES.md) for more recipes: the k-means elbow, an explicit abstention, an exponential-saturation curve, the diagnostic figure, the standalone locator, and configuration tuning.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import numpy as np
|
|
46
|
+
from elbow_helper import robust_knee, RobustKneeConfig
|
|
47
|
+
|
|
48
|
+
x = np.linspace(0, 1, 80)
|
|
49
|
+
y = np.where(x <= 0.3, 3*x, 0.9 + 0.2*(x - 0.3))
|
|
50
|
+
y = y / y.max() + np.random.default_rng(0).normal(0, 0.02, x.size)
|
|
51
|
+
|
|
52
|
+
result = robust_knee(x, y, config=RobustKneeConfig(random_seed=0))
|
|
53
|
+
|
|
54
|
+
if result.is_clear:
|
|
55
|
+
print(result.knee_x, result.ci90, result.detection_rate, result.null_p_value)
|
|
56
|
+
else:
|
|
57
|
+
print("no clear knee:", result.reason)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`curve` and `direction` are optional: left unset, both are inferred from the data (see below) and can still be passed explicitly, e.g. `robust_knee(x, y, curve="concave", direction="increasing", ...)`. `y` is optional too: call `robust_knee(y)` alone and `x` is taken to be the implicit `0, 1, ..., n-1`.
|
|
61
|
+
|
|
62
|
+
For the classic k-means or scree "elbow" (convex, decreasing):
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from elbow_helper import robust_elbow
|
|
66
|
+
result = robust_elbow(k_values, inertia) # curve/direction fixed to convex/decreasing
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Automatic shape and direction
|
|
70
|
+
|
|
71
|
+
Leaving `curve` or `direction` unset infers them from the cleaned, normalised data. `direction` comes from the sign of the trend between `x` and `y`. `curve` comes from whether the (lightly smoothed) curve lies above or below the straight chord connecting its first and last point: above is concave, below is convex, the mathematical definition, holding for both increasing and decreasing curves. This covers all four concave/convex Γ increasing/decreasing combinations without asking the caller to name the shape up front. Passing `curve` or `direction` explicitly always overrides the inferred value.
|
|
72
|
+
|
|
73
|
+
## The contract: a tagged union
|
|
74
|
+
|
|
75
|
+
`robust_knee` always returns one of two types, both subclasses of `KneeResult` and distinguished by `.is_clear`:
|
|
76
|
+
|
|
77
|
+
- `ClearKnee`: `knee_x`, `knee_x_norm`, `knee_index`, `ci90` (a 90% bootstrap interval, in data units), `detection_rate`, `smoothing_window`, `sensitivity`, `prominence`, `slope_contrast`, `bic_improvement`, `null_p_value`, and the full `diagnostics`.
|
|
78
|
+
- `NoClearKnee`: a machine-readable `reason` code plus `diagnostics`.
|
|
79
|
+
|
|
80
|
+
You are forced to handle abstention explicitly: there is no silent fallback to a guess.
|
|
81
|
+
|
|
82
|
+
## How it decides: the pipeline
|
|
83
|
+
|
|
84
|
+
1. **Preprocess.** Clean, sort, deduplicate, and robustly normalise the data to the unit square, then screen its global shape with a Spearman correlation (a rank-based measure of how monotonic the curve is) and a magnitude-weighted monotonicity check.
|
|
85
|
+
2. **Scale-space search.** Run the from-scratch locator across a grid of Gaussian smoothing windows and sensitivities; collect every candidate it proposes.
|
|
86
|
+
3. **Basic filters.** Reject boundary knees, weak prominence, and a low prominence-to-noise ratio.
|
|
87
|
+
4. **Persistence clustering.** Keep only knees that recur at a stable location across consecutive smoothing scales and most sensitivities. Abstain if two knees look equally plausible (`MULTIPLE_PLAUSIBLE_KNEES`).
|
|
88
|
+
5. **Model confirmation.** Require a robust slope change, via a Theil-Sen estimator (a regression method that stays accurate even with outliers), plus a continuous broken-line fit that beats a single straight line on blocked cross-validation and on the BIC (Bayesian Information Criterion, a score that rewards fit while penalising extra parameters).
|
|
89
|
+
6. **Bootstrap.** Re-run the whole search on an IID residual bootstrap (independent, identically distributed resampling of the leftover noise). The knee must be redetected at least 90% of the time, with a tight, unimodal interval.
|
|
90
|
+
7. **No-knee null test.** Run a Monte Carlo test against a straight-line null model that carries the accepted model's noise scale. The observed knee must be significant at p β€ 0.01.
|
|
91
|
+
|
|
92
|
+
Only a candidate that clears every gate becomes a `ClearKnee`.
|
|
93
|
+
|
|
94
|
+
## Abstention reason codes
|
|
95
|
+
|
|
96
|
+
`INSUFFICIENT_DATA`, `INVALID_INPUT`, `ZERO_RANGE`, `INCOMPATIBLE_GLOBAL_SHAPE`,
|
|
97
|
+
`NO_KNEE_CANDIDATES`, `ALL_CANDIDATES_WEAK`, `NO_PERSISTENT_CLUSTER`,
|
|
98
|
+
`MULTIPLE_PLAUSIBLE_KNEES`, `BOUNDARY_KNEE`, `WEAK_SLOPE_CHANGE`,
|
|
99
|
+
`SEGMENTED_MODEL_NOT_BETTER`, `BOOTSTRAP_UNSTABLE`, `BOOTSTRAP_MULTIMODAL`,
|
|
100
|
+
`NULL_NOT_REJECTED`, `INTERNAL_NUMERICAL_FAILURE`.
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
Every threshold lives in `RobustKneeConfig`, a frozen dataclass; call `config.with_(...)` to change one. All positional thresholds are expressed in normalised x-range units. The shipped defaults suit a first practical prototype: modest replicate counts, so a run finishes in seconds.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
RobustKneeConfig(bootstrap_replicates=100, null_replicates=200)
|
|
108
|
+
# validation-grade:
|
|
109
|
+
config.with_(bootstrap_replicates=500, null_replicates=1000)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A note on these thresholds: they are calibrated, conservative defaults, not universal constants. `cluster_tolerance` and `max_neighbor_shift` sit slightly above the reference plan's 0.05 to absorb the locator's one- or two-sample discretisation jitter at modest sample sizes (n of about 60 to 100). Recalibrate them against your own curve and noise family if needed.
|
|
113
|
+
|
|
114
|
+
## Diagnostics
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from elbow_helper.plotting import plot_diagnostics
|
|
118
|
+
plot_diagnostics(x, y, curve="concave", direction="increasing", out="diag.svg")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+
Hand-authored SVG, no matplotlib, no extra to install: the diagnostic is a core feature. The curve and its located knee sit next to a compact evidence legend backing the point estimate: the detection probability, the null-model p-value, the slope contrast, a BIC-derived posterior probability (the odds, under Kass & Raftery's Bayes-factor approximation, that the knee model is correct), and a fit-quality score normalized against a deliberately pessimistic worst case rather than the easily-beaten sample mean (see `doc/ELBOW-en.tex` / `doc/ELBOW-fr.tex` for the derivation of both). When the evidence is too weak, the figure switches to an honest abstention state instead: a greyed, dashed curve and the reason, never a marker implying more confidence than the data supports. Pass `language="fr"` for the French chrome text.
|
|
124
|
+
|
|
125
|
+
## Limitations
|
|
126
|
+
|
|
127
|
+
- The current smoother assumes a regular or near-regular spacing in `x`.
|
|
128
|
+
- Auto-detected `curve` and `direction` need a curve with a real, unambiguous trend; on data too weak or noisy to classify confidently, the pipeline abstains with `INCOMPATIBLE_GLOBAL_SHAPE` rather than guessing, the same gate that already screens explicit `curve`/`direction` arguments.
|
|
129
|
+
- Detection is discretised to sample locations. On modest `n`, the located knee can sit within a few samples of the truth (median error at or below about 5% of the x-range, on the supported synthetic family).
|
|
130
|
+
- The straight-line null and the IID residual bootstrap suit roughly homoscedastic (constant-variance), uncorrelated noise. Wild or moving-block bootstrap variants are future work.
|
|
131
|
+
- No finite-data method is infallible. The targets above hold for the documented simulation family, not for every possible curve.
|
|
132
|
+
|
|
133
|
+
## Standalone locator
|
|
134
|
+
|
|
135
|
+
The from-scratch locator is usable on its own:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from elbow_helper import KneeLocator
|
|
139
|
+
kl = KneeLocator(x, y, S=1.0, curve="concave", direction="increasing", online=True)
|
|
140
|
+
kl.knee, kl.all_knees
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Multiple knees: `robust_knees`
|
|
144
|
+
|
|
145
|
+
`robust_knee` answers "is there one knee?". A curve with several genuine
|
|
146
|
+
regime changes, three pricing tiers on a demand curve, say, needs a
|
|
147
|
+
different question: how many breakpoints does this curve actually have and
|
|
148
|
+
where are they? `robust_knees` (plural) answers that one. It searches over every
|
|
149
|
+
possible number of segments with a dynamic program, scores each candidate
|
|
150
|
+
with a modified BIC (a fit-quality score that penalises extra breakpoints,
|
|
151
|
+
so adding one has to earn its keep), then confirms the winning count with a
|
|
152
|
+
Bonferroni-gated permutation test to hold the false-positive rate down as
|
|
153
|
+
the search space grows.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import numpy as np
|
|
157
|
+
from elbow_helper import RobustKneesConfig, robust_knees
|
|
158
|
+
|
|
159
|
+
rng = np.random.default_rng(3)
|
|
160
|
+
x = np.linspace(0, 1, 100)
|
|
161
|
+
y = np.piecewise(
|
|
162
|
+
x,
|
|
163
|
+
[x < 0.3, (x >= 0.3) & (x < 0.65), x >= 0.65],
|
|
164
|
+
[lambda t: 3 * t, lambda t: 0.9 + 0.2 * (t - 0.3), lambda t: 0.97 + 2.2 * (t - 0.65)],
|
|
165
|
+
) + rng.normal(0, 0.02, x.size)
|
|
166
|
+
|
|
167
|
+
result = robust_knees(x, y, config=RobustKneesConfig(random_seed=0, fwer_permutations=200))
|
|
168
|
+
print(result)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
Knees(k=2, x=[0.2929, 0.6465])
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The two breakpoints land close to the curve's true regime changes at 0.3 and
|
|
176
|
+
0.65. Unlike `robust_knee`, an empty result here is not an abstention: it is
|
|
177
|
+
the pipeline's confident conclusion that the curve has no real breakpoint,
|
|
178
|
+
having survived the same search and false-positive gates a nonempty result
|
|
179
|
+
would have had to survive. Only a preprocessing failure (bad input, too
|
|
180
|
+
little data, zero range) returns `InvalidKnees` instead of `Knees`. See
|
|
181
|
+
`research/multiknee/RESULTS.md` and `doc/ELBOW-en.tex` (sections 5-20) for
|
|
182
|
+
the validation behind this design.
|
|
183
|
+
|
|
184
|
+
## Mathematics
|
|
185
|
+
|
|
186
|
+
`doc/ELBOW-en.tex` ([π«π· doc/ELBOW-fr.tex](https://github.com/warith-harchaoui/elbow-helper/blob/main/doc/ELBOW-fr.tex)) derives every formula this package runs: the single-knee pipeline's normalisation, Spearman screen, difference-curve knee search, persistence clustering, Theil-Sen slope, BIC, blocked cross-validation, bootstrap and null test, and the multi-knee research behind `robust_knees` (see also `research/multiknee/RESULTS.md`). It is written intuition-first, with a worked example before every formula, for readers anywhere from the end of high school to a Ph.D. in applied mathematics. Its Gaussian likelihood foundation, the general theory behind why `L := exp(E[log p])` rather than a raw product, and how the same construction reads on a classification model, is factored out into a companion note, `doc/LIKELIHOOD-en.tex` ([π«π· doc/LIKELIHOOD-fr.tex](https://github.com/warith-harchaoui/elbow-helper/blob/main/doc/LIKELIHOOD-fr.tex)), since that foundation doesn't depend on curve-fitting at all. Citations are in `doc/references.bib`, including a few pointers into my own [Favourite AI books](https://deraison.ai/ai-books) where a technique used here deserves a book-length treatment. Native LaTeX (not Markdown), given the audience: compile with `latexmk -pdf ELBOW-en.tex` (or `ELBOW-fr.tex`, `LIKELIHOOD-en.tex`, `LIKELIHOOD-fr.tex`) from inside `doc/`, or read the compiled copies directly, `doc/ELBOW-en.pdf` / `doc/ELBOW-fr.pdf` / `doc/LIKELIHOOD-en.pdf` / `doc/LIKELIHOOD-fr.pdf`.
|
|
187
|
+
|
|
188
|
+
## Landscape
|
|
189
|
+
|
|
190
|
+
[πΊοΈ Landscape](https://github.com/warith-harchaoui/elbow-helper/blob/main/LANDSCAPE.md) ([π«π· PAYSAGE.md](https://github.com/warith-harchaoui/elbow-helper/blob/main/PAYSAGE.md)): how `elbow-helper` compares to `kneed`, `ruptures`, `kneebow`, Yellowbrick's `KElbowVisualizer`, R's `segmented` package, manual eyeballing and asking an LLM, rated on 11 criteria and positioned on a PCA map.
|
|
191
|
+
|
|
192
|
+
## CLI / API / MCP
|
|
193
|
+
|
|
194
|
+
Beyond the Python library, `elbow-helper` exposes three more doors onto the same pipeline: an argparse CLI (always installed), a click CLI twin, and an HTTP API with an MCP server mounted on it. All four are thin adapters over the same shared core (`elbow_helper._core_cli`), so none of them can drift from what the library itself returns.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pip install -e . # library + argparse CLI
|
|
198
|
+
pip install -e ".[cli]" # + the click twin
|
|
199
|
+
pip install -e ".[api]" # + the FastAPI HTTP surface
|
|
200
|
+
pip install -e ".[mcp]" # + the MCP server (pulls in [api] too)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# argparse (always available)
|
|
205
|
+
elbow-helper knee --y-values 0,0.1,0.3,0.6,0.85,0.9,0.92,0.93,0.94,0.95
|
|
206
|
+
|
|
207
|
+
# click twin
|
|
208
|
+
elbow-helper-click knee --y-values 0,0.1,0.3,0.6,0.85,0.9,0.92,0.93,0.94,0.95
|
|
209
|
+
|
|
210
|
+
# HTTP API
|
|
211
|
+
uvicorn elbow_helper.api:app --reload
|
|
212
|
+
curl -X POST localhost:8000/knee -d '{"x": [0,0.1,0.3,0.6,0.85,0.9]}'
|
|
213
|
+
|
|
214
|
+
# MCP (fastapi-mcp mounted on the same app, at /mcp)
|
|
215
|
+
uvicorn elbow_helper.mcp_server:app --port 8021
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Every surface exposes the same four operations (`knee`, `elbow`, `diagnostics`, `locator`), matching `robust_knee`, `robust_elbow`, `plot_diagnostics`, and the standalone `KneeLocator` one-to-one. Data goes in as inline comma-separated values, a `.npy` file, or a CSV column (CLI), or a JSON body (`x`/`y` lists, HTTP). `RobustKneeConfig` overrides travel as `--config-json '{"bootstrap_replicates": 500}'` (CLI) or a `config_overrides` object (HTTP). The `diagnostics` operation returns the SVG itself, not a JSON wrapper around it.
|
|
219
|
+
|
|
220
|
+
## Author
|
|
221
|
+
|
|
222
|
+
[Warith HARCHAOUI](https://linkedin.com/in/warith-harchaoui)
|
|
223
|
+
|
|
224
|
+
## Acknowledgements
|
|
225
|
+
|
|
226
|
+
The from-scratch Kneedle implementation in `elbow_helper/locator.py` follows the algorithm described by SatopÀÀ, Albrecht, Irwin and Raghavan (ICDCSW 2011). Its traversal logic, orientation table and sensitivity threshold closely follow the implementation choices of [`kneed`](https://github.com/arvkevi/kneed) by Kevin Arvai, released under the BSD-3-Clause license:
|
|
227
|
+
|
|
228
|
+
> Copyright (c) 2017, Kevin Arvai
|
|
229
|
+
> All rights reserved.
|
|
230
|
+
>
|
|
231
|
+
> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: (1) redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer; (2) redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
232
|
+
|
|
233
|
+
`elbow-helper` has no runtime dependency on `kneed`: the algorithm is reimplemented in NumPy only, with the scipy calls replaced as documented in `locator.py`.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "elbow-helper"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Noise-robust knee/elbow detection: a conservative wrapper around a from-scratch locator that either returns a knee with uncertainty or explicitly abstains."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "BSD-3-Clause" }
|
|
12
|
+
authors = [{ name = "Warith Harchaoui", email = "warithmetics@deraison.ai" }]
|
|
13
|
+
keywords = ["knee", "elbow", "model-selection", "k-means", "changepoint"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: BSD License",
|
|
17
|
+
"Topic :: Scientific/Engineering",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# Hard dependencies: numpy + os-helper ONLY.
|
|
22
|
+
dependencies = [
|
|
23
|
+
"numpy>=1.24",
|
|
24
|
+
"os-helper>=1.8",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
# Diagnostic plotting (elbow_helper.plotting) is hand-written SVG, stdlib +
|
|
29
|
+
# numpy only β no extra to install, no matplotlib. See CHANGELOG [Unreleased].
|
|
30
|
+
# The other three "doors" beyond the library + argparse CLI.
|
|
31
|
+
cli = ["click>=8.1,<9"] # optional click twin (argparse is always on)
|
|
32
|
+
api = ["fastapi>=0.111,<1", "uvicorn>=0.30,<1"]
|
|
33
|
+
# fastapi-mcp mounts the MCP server onto a FastAPI app (see mcp_server.py), so
|
|
34
|
+
# this extra needs the api extra's own deps too. `mcp<2` is pinned because
|
|
35
|
+
# fastapi-mcp 0.4.x calls the mcp SDK's Server() with a positional signature
|
|
36
|
+
# that mcp 2.0 broke (TypeError at import time) β the same pin ann-router and
|
|
37
|
+
# other suite members carry; lift it once a fastapi-mcp release compatible
|
|
38
|
+
# with mcp>=2 ships.
|
|
39
|
+
mcp = ["fastapi-mcp>=0.4,<1", "mcp<2,>=1.20", "fastapi>=0.111,<1", "uvicorn>=0.30,<1"]
|
|
40
|
+
dev = ["pytest>=7", "httpx>=0.27"]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
# argparse CLI β no runtime dependency beyond the stdlib + core package.
|
|
44
|
+
elbow-helper = "elbow_helper.cli_argparse:main"
|
|
45
|
+
# click twin β install with the [cli] extra.
|
|
46
|
+
elbow-helper-click = "elbow_helper.cli_click:cli"
|
|
47
|
+
# HTTP API + MCP server (needs [mcp]) β fastapi-mcp mounts /mcp on the same
|
|
48
|
+
# FastAPI app api.py serves, so one entry point covers both doors.
|
|
49
|
+
elbow-helper-mcp = "elbow_helper.mcp_server:main"
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/warith-harchaoui/elbow-helper"
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
addopts = "-q"
|