Changelog¶
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
0.8.0 - 2026-08-06¶
Proper logging. Everything this package has to say now goes through
loguru at a level per message, one setting decides how
much of it is emitted, and tqdm draws the progress bar where it is installed.
Added¶
verbosity, onThresher,SparkThresherand the command line. It names the lowest level that gets through —'debug','info','warning'(the default),'error'or'critical'— and can be set three ways, in increasing precedence:
thresher.set_verbosity('info') # until it is set again
thresher.Thresher(verbosity='debug') # this instance's runs
with thresher.verbosity('debug'): # this block
...
The instance setting applies for the duration of each optimize_threshold call and to
nothing else, so two Thresher objects in one process can differ — one verbose, one
silent, at the same time. Neither logging system offers that on its own: both hold
their level globally, which is why the level is checked at the call site here, against a
ContextVar. A message below the level is never handed to loguru at all, so it costs
nothing to have written it.
An unknown level is a ConfigurationError where it is given — when the Thresher is
built, not several seconds into a long run.
-
tqdmas an optional extra,pip install 'thresher-py[progress]'. It draws the progress bar when it is importable and the bar this package carries draws it when it is not. tqdm is given abar_formatshaped like the built-in bar, so installing the extra changes how a run looks and nothing else about it — including that a script watching stderr sees the same percentages either way. -
-v,-vv,-q,--verbosityand--progresson the command line.-vreports each stage of a run and-vveach step inside it;--verbositynames the level outright;-qis shorthand for--verbosity error, which is what silences the slow-algorithm warning. Where-qand-vdisagree,-qwins — it is the flag that takes something away, so it can only have been meant. -
thresher.propagate_to_logging(), which hands every record to the standard library'sloggingas well, under the name of the module that emitted it. That is whatlogging.getLogger('thresher')used to see. Off by default: an application with loguru andloggingwriting to a console would otherwise print every record twice.
Changed¶
-
Nothing in the package calls
print()any more. Twenty-two calls sat behindif verbose:in six solvers, the dispatcher and the interface, writing to stdout — the stream the command line reserves for its answer, and the one stream a library has no business claiming.tests/test_logging.pywalks the package's syntax trees and fails on aprintin any of it, so a solver written in the old style is caught rather than reviewed for. -
Progress bars are drawn on stderr, where the built-in one wrote to stdout before. Nothing had noticed, because the command line had no way to ask for a bar — and this release adds one. tqdm's own default is stderr, so this is also what lets the two backends be swapped without the output moving.
-
No bar is drawn while the log is at
debug. Both write to stderr, so together they produce a bar interrupted by log lines and log lines interrupted by a bar; the level wins. This rule existed before, applied by the genetic solver alone, and announced with a printedWarning! Enabling verbosity automatically disables a progress bar.It now applies to every solver and says nothing. -
The size warning is silenced with
thresher.set_verbosity('error'), notlogging.getLogger('thresher').setLevel(logging.ERROR). The message says so itself. The old form works again after one call topropagate_to_logging(). -
SparkThreshertakesverbosity, and still takesverbose. It has noprogress_baroption and gains none: the counting happens on executors, where nobody is watching a terminal, and the driver's share is a sweep over a few thousand bins. -
print_progress_barmoved tothresher.progress. It is still importable fromthresher.utils, where it has been since the first release, and delegates.
Removed¶
verboseis gone from every solver signature. The convention is nowrun(scores, actual_classes, progress_bar, alg_options), with the same two exceptions as before —linear.compute.runtakes noalg_optionsandrun_paralleltakesn_jobs, which also loses itsverbose. Verbosity is a property of the run rather than an argument threaded through eight functions, which is the point of the change. Theverboseoption onThresheris unaffected: it still works, and meansverbosity='debug'.
0.7.3 - 2026-08-05¶
Three defects in how the evolutionary solver turns one generation into the next, and into
an answer. On the fixture used to measure them, mean error against exact halved.
Fixed¶
- The answer is an agent that was actually measured (#31). Each generation scored
its population, selected from it, then bred a replacement — and after the last
generation that replacement was returned without ever being scored. One crossover and
one mutation therefore reached the answer with no selection in front of them. The
fittest agent measured across every generation is returned instead, the same rule the
sgdwalk already follows in returning its best point rather than its last.
It shows up worst when the mutation is loud. With mutation_chance=1.0:
mutation_factor |
before | now |
|---|---|---|
| 0.10 | 0.5078 | inside the data |
| 5.0 | 0.5776 | inside the data |
| 50.0 | 1.3188 — outside a [0, 1] dataset, at 53% accuracy where 90% was available |
inside the data, within 0.036 of the achievable accuracy |
Averaging was also measured against returning the fittest, since the old docstring claimed the mean was deliberate noise reduction: over 40 seeds the mean of the final evaluated survivors gives 0.0151 mean error against 0.0076 for the fittest. Averaging an unconverged population drags the answer toward the middle of its spread, which is a cost rather than a benefit.
-
Mutation moves a threshold either way (#31). The nudge was
mutation_factor * random.random(), drawn from[0, mutation_factor)and so never negative — a ratchet rather than a mutation. Measured againstexactover 40 seeds of 4,000 rows, the returned threshold sat+0.0061above the optimum with mutation off,+0.0069at the default chance and+0.0120at0.5: the more often it fired, the further up it pushed. It is now symmetric, and the bias at every rate is within±0.002. -
sus_factoris validated instead of being fed to a slice (#31).population_size - sus_factorwas used directly as a slice bound, and a slice takes a negative bound to mean "from the far end". Against the default population of 30:
sus_factor |
before | now |
|---|---|---|
| 30 | bare stdlib ValueError: Sample larger than population — not a ThresherError |
ConfigurationError |
| 35 | silently kept 25 survivors — asked to cull 35, culled 5 | ConfigurationError |
| 59 | silently kept 1 | ConfigurationError |
| 61 | bare stdlib ValueError again |
ConfigurationError |
population_size, number_of_generations and number_of_iterations are checked with
it: each must be a whole number of at least 1, since number_of_iterations=0 made
fitness the mean of no samples and population_size=0 left nothing to evolve. All are
checked before the simulation starts rather than discovered part-way through it.
Changed¶
- A generation is now bred from the previous one's survivors and then scored, rather than scored and then bred. The two orders run the same operations in the same sequence, but only one of them leaves no population unmeasured at the end - which was the whole defect above, and is now a property of the loop's shape rather than a special case in it.
- Every test now starts from the same global random state, set by an autouse fixture in
tests/conftest.py. Four solvers sample through therandommodule, so a test asserting an outcome from one of them was really asserting something about wherever the generator had been left by the tests before it — they passed or failed on their position in the run. This release changed how many values the genetic solver draws and nothing else aboutsgrid, and that alone turned ansgridassertion red. TestScoresOutsideTheUnitIntervalruns its sampling solvers atstoch_ratio=1.0. Those tests ask where a solver looks — whether its candidates are laid over the data or over a hardcoded[0, 1]— andsgridwas answering through 5 of 100 rows, or a single row on the three-row inputs, so the assertions were being decided by the draw as much as by the grid. It missed exact accuracy for 43 of 200 starting random states; it now misses for none of them, and the property the test names is what decides it.
0.7.2 - 2026-08-05¶
numpy and pandas input stops being second-class: it is no longer copied, and no longer comes back with a different type than a list would.
Fixed¶
- A
numpy.ndarrayand apandas.Seriesare read where they lie (#30).optimize_thresholdkept its argument only when it was acollections.abc.Sequence, and neither of those is one — they have noindexorcount— so both were copied to lists on the way in. That is theO(n)allocation 0.5.3 removed, reintroduced for the two types this library is built around, and it madehist's bounded memory unreachable through the public API. Optimizing 200,000 rows, peak allocation measured withtracemalloc:
| input | before | now |
|---|---|---|
list |
18 KiB | 18 KiB |
numpy.ndarray |
12,520 KiB | 17 KiB |
pandas.Series |
7,832 KiB | 18 KiB |
What is handed over is now decided by what the solvers use — len(), more than one
pass, and integer indexing — rather than by a protocol wider than any of them. A
generator or a set still becomes a list, because one pass is not enough.
A Series is handed over as the array underneath it rather than as itself. series[0]
is a label lookup, so on a frame that has been filtered — where the surviving rows
keep the labels they had — passing it straight through would make the sampling solvers
read the wrong row, or none. to_numpy() is a view for the dtypes a score column has,
so this costs nothing and the index cannot reach the answer.
Not copying does cost some CPU: iterating an array boxes each element where a list
hands over a reference, which measures about 1.3× on hist at 500,000 rows — against
roughly 700× less memory, which is the trade worth making for the algorithm chosen for
its memory.
-
The result is always a plain
float(#30). numpy input leaked annp.float64out ofexact,histandls, but not out ofgrid— so identical data returned a differently-typed answer depending on the algorithm, against an annotation promisingfloateither way. It subclassesfloat, so nothing broke and noisinstancecheck could see it; under numpy ≥ 2 it surfaces asnp.float64(0.35)wherever the result is printed or embedded.optimize_thresholdcoerces once, at the single point the package returns an answer. -
exact,histandgridaskedif not scores:, which raisesValueErrorfor an array of more than one element rather than answering the question. They check the length.
Changed¶
- The command line hands the score column to
optimize_thresholdas pandas holds it, instead of calling.tolist()on it. The frame is already in memory; the list was a second copy of the column at four times the bytes. The label column is still converted, because those values get named back to the user when they are wrong andFound np.int64(0)is not an improvement onFound 0.
Added¶
tests/test_array_inputs.py, which passes anndarrayand aSeries— including one whose index has gaps — through every algorithm. Nothing anywhere did before, which is why neither symptom above had surfaced. It asserts the container cannot change the answer, that the answer is afloat, and that allocation stays flat, this last one throughThresherrather than against a solver directly: the existing bounded-memory test callshistogram.runwith lists, so it passed throughout with the interface copying both arguments in full.
0.7.1 - 2026-08-05¶
Four defects that all shared a shape: a plausible number returned from input that should never have produced one.
Fixed¶
histreturns a threshold that achieves the accuracy it reports (#20). The binning floors, so a score sitting exactly on a bin edge belongs to the bin above it; the prediction rule isscore > threshold, which sends a score sitting exactly on the threshold to the class below it. Returning the edge itself made the two disagree for precisely the samples on that edge, so the sweep reported a count the threshold does not achieve — and could prefer a worse edge to a better one.
On the reported case it returned a threshold scoring 6/10 while claiming 8/10, with 8/10
available from another edge. Across 200 seeds of two-decimal scores at 100 bins, 174 lost
more than half a point of accuracy against exact; none do now.
Stepping one representable value below the edge is close but not sufficient: that
arithmetic and the (score - lowest) / span * bins used to bin are not inverses in
floating point, so a score can bin above an edge and still compare below it. The
boundary is now found with the binning function itself, which cannot disagree with the
counting by construction. Measured over 5,000 randomised shapes, the sweep never reports
more than the returned threshold delivers; before this change 9 of them did.
The same reconstruction problem reached the topmost split, which expressed "classify
everything as negative" as lowest + span. That is not always the largest score: for
scores rounded to a few decimal places - the ordinary case - 0.065 + (0.997 - 0.065)
is 0.9969999999999999, so the largest samples were classified positive while the
counting had them negative. sweep_bins now takes lowest and highest rather than
lowest and a span, deriving the span from the pair, so the range has one authoritative
definition and nothing is rebuilt from it.
The binning is likewise now one function, bin_index, used by both the counting pass
and the threshold search, so they cannot drift apart again. Over 20,000 randomised
shapes the reported count is exactly what the returned threshold delivers. The Spark
path shares sweep_bins and is fixed with it.
-
A score that is not a number is refused (#23). Only the labels were ever checked. A NaN reached the solvers and each failed its own way:
exactsorted it into place and handed it back as the answer — a "threshold" that classifies everything negative, since every comparison against NaN is false — whilehistraised a bareValueErrorout of its bin arithmetic. The command line printednanand exited 0, which reads as success.validate_scoresnow runs beside the existing guards, so all seven algorithms refuse it identically withUndefinedScoresError.Noneis refused with it; infinities are not, since a threshold can be placed relative to them. -
The Spark interface refuses the rows the in-memory path refuses (#21, #24). The class counts come from equality against the two declared labels, so a row matching neither — a null, a third class, a typo — was absent from both and landed in the negative count by omission. Six rows labelled 0/1 returned
0.25; adding three rows labelled2returned0.99, where the in-memory path raises. Null and NaN scores went the same way: Spark'sleastskips nulls, so a row with no score was filed in the top bin, and NaN sorts above everything, so it became the maximum and collapsed the sweep.
A frame whose labels match neither class was also reported as SingleClassError naming
a class that does not appear in the data at all; it is now UnexpectedLabelsError,
which names the values that do.
All the counts come from the first aggregation, so the checks cost no extra pass; only naming the offending labels reads the frame again, and only on the way out.
Added¶
UndefinedScoresError, anInvalidInputErrorand so aValueError, carrying.count.
Changed¶
histogram.compute.sweep_binstakeshighestwhere it tookspan, for the reason above. It is a solver internal rather than part of the documented API, and the only callers arehistitself and the Spark interface.
0.7.0 - 2026-08-04¶
Added¶
- A multiprocessing backend.
backend='mp'spreads the counting over the CPU cores of the machine you are already on:
from thresher import Thresher
def main():
Thresher(backend="mp").optimize_threshold(scores, actual_classes)
if __name__ == "__main__": # required; see below
main()
It is shaped exactly like the Ray backend, and shares its guarantee: the data is sharded
once, each worker counts its own shard with the same plain functions from
backends/base.py, and the driver adds the partials together. Addition is
order-independent, so the answer is identical to a local run rather than merely close -
asserted for exact, ls and grid, across several worker counts, and on data full of
duplicates and ties.
Unlike ray it needs nothing installed, multiprocessing being in the standard library.
That makes it the parallel option on macOS x86_64, where Ray publishes no wheel at all.
The same three algorithms distribute as on Ray: sgrid and gen draw a fresh random
subsample per evaluation and sgd is a sequential walk, so those three still run
in-process whatever backend is asked for.
MultiprocessingBackend(num_workers=...) configures the process count; -1 means every
processor bar one. Below min_rows_per_shard (2,000 by default) the work stays in this
process, since handing it to another costs more than the counting saves.
ParallelBootstrapError, raised when worker processes cannot start. It is aRuntimeError, which is whatBrokenProcessPool- the failure it replaces - already was.
Fixed¶
-
A parallel run in an unguarded script fails in a second instead of hanging forever (#22). Worker processes re-import
__main__on spawn platforms, so a script that parallelises at module level made every worker re-run it and start workers of its own.multiprocessing.Pool.mapthen waited on children that would never report: no error, no result, no end - the run had to be killed. The backend usesconcurrent.futures.ProcessPoolExecutor, whoseBrokenProcessPoolis translated into aParallelBootstrapErrorcarrying the__main__-guard fix. -
n_jobsvalues that name no process count are refused (#22).0and anything below-1raiseConfigurationError; they used to print a message and carry on with one process, so the warning changed nothing. Asking for more processors than the machine has is clamped to what it has, rather than opening that many -n_jobs=9999no longer tries for 9,999 processes. -
Parallelising linear search no longer changes its answer (#22).
n_jobsnow runs the ordinary search on thempbackend rather than through a second implementation. The old parallel path evaluated the raw scores as thresholds where the sequential path evaluates the midpoints between them, so the two returned different - equally valid - answers for the same data. They now agree exactly.
Changed¶
- An explicitly chosen backend takes precedence over
n_jobs, which is the older spelling of the same request; the two would otherwise contend for the same cores. This was already the behaviour, and is now tested. linear.compute.process_batchis removed. It existed only to be pickled into the pool it no longer uses;backends.base.tally_chunkdoes that counting for every backend.
0.6.4 - 2026-08-04¶
Three defects in the approximate solvers, all of which returned a plausible number while being wrong or slow rather than failing.
Fixed¶
gridandsgridlay their candidates across the data instead of over[0, 1](#25). The grid was hardcoded to the unit interval on the assumption that scores are probabilities. Given anything else - logits, margins, distances - every candidate fell outside the data, so the answer was whichever edge scored better: chance accuracy, returned in silence. On separable scores spanning[-5, -2]both returned0.0at 50% accuracy where every other solver reached 100%; they now do too.
The grid spans [min(scores), max(scores)] at the same 10**places + 1 resolution, so
the candidates are spent on the range the data occupies, plus one point below the
minimum that keeps "classify everything as positive" expressible - the one split the
old [0, 1] grid could only reach by accident. Ties still go to the leftmost candidate,
so a threshold inside the data is never given up to break one.
For scores that really are in [0, 1] the resolution now covers the observed range
rather than the whole interval, which changes results very slightly: across the
benchmark, grid moved from 99.82%/99.98%/100.00% to 99.83%/99.99%/99.92% of the exact
optimum on the separable, overlapping and imbalanced sets.
-
The
sgdstep size scales with the data (#26). The first step was a constant0.05and only ever decays, so the walk's total travel was bounded at roughly 4.3 score units however far away the boundary was. On scores spanning thousands it stopped short every run - deterministically, unlike this solver's known sampling noise. The step is now a fraction of the score range, exposed asstep_ratio(default0.05), so the same data multiplied by 1,000 returns exactly 1,000× the threshold. Probability-shaped scores span about 1, so their behaviour is unchanged. -
sgridwithreshuffle=Truesamples by index (#27). It built the full list of(score, class)pairs before sampling from it, so every candidate cost a pass over the whole input however smallstoch_ratiowas -O(c·n)against the documentedO(c·r·n), and slower than the exhaustive grid it exists to approximate. At 200,000 rows it went from 5.30 s to 0.80 s, against exhaustive grid's 1.26 s. Its memory column in the benchmark drops fromO(n)toO(r·n)for the same reason.
Added¶
step_ratioforsgd, above. Documented in the README anddocs/algorithms.md, which the parity test added in 0.6.3 insisted on before this would build.
0.6.3 - 2026-08-03¶
Removed¶
optimized_startfrom the genetic algorithm's documented parameters (#34). The solver stopped reading it long ago - the class-mean seeding it once toggled is now unconditional - but it stayed in the README, which is the only parameter documentation. Passing it did nothing, and nothing said so.
Added¶
- Each solver now declares the
algorithm_paramskeys it reads, as aknown_paramsfrozenset beside the defaults that define them, andThresher(...)rejects anything else with aConfigurationErrornaming the offending key and listing the accepted ones. A mistypedstoch_rationused to fall back to the default in silence, so the run continued with the value the caller believed they had replaced.
>>> Thresher(algorithm='sgd', algorithm_params={'stoch_ration': 0.5})
ConfigurationError: Unknown algorithm_params key(s) for sgd: 'stoch_ration'.
It reads: alpha, num_of_iters, stoch_ratio, stop_patience, stop_thresh. ...
set_algorithm re-validates, since parameters valid for one algorithm need not be
valid for the next, and SparkThresher applies the same check - a key ignored there
would have been ignored across a whole cluster run. exact accepts none at all,
and says so: being exact, it has no accuracy to trade for speed.
tests/test_internals.py::TestDocumentedParameterscompares the README's parameter lists against those sets in both directions, so neither can drift again. It is what would have caughtoptimized_startin the first place.
Fixed¶
algorithm_paramsis checked to be a mapping, rather than failing later and less clearly.- Parameters belonging to the stochastic grid search -
stoch_ratioandreshuffle- are no longer accepted for the exhaustivegrid, which shares the implementation but never reads them. - The README's
algorithm_paramsexamples, the-p/--paramhelp text andexamples/sample_parallel.pyall named a parameter without naming the algorithm that reads it, so they demonstrated a silent no-op under the default algorithm. The example in particular claimed to run linear search across several processes and ran the exact sweep in one (#35). - The command line reports these as
-p/--paramkeys rather than naming thealgorithm_paramsconstructor option, which a terminal user cannot act on.
0.6.2 - 2026-08-03¶
Fixed¶
Thresher(...)rejects option names it does not recognise, with aConfigurationErrorlisting the valid ones (#33). A mistyped name -Thresher(algoritm='gen')- was previously merged into the options dict and never read, so the run silently used the defaults the caller believed they had changed.- A malformed
labelsoption is rejected when the object is built, rather than discarded: anything that is not a two-item list or tuple raisesLabelMappingError. Previously a non-iterable value was ignored in silence - and the eventual error told the caller to declare the very option they had already declared - while a one-item mapping died later as a bareIndexErrorinsidemap_labels.labels=Nonestays accepted and means no mapping. - A non-string
algorithmraisesUnknownAlgorithmErroras the constructor documents, instead of escaping as a bareAttributeErrorfrom.lower(). NotIterableErrornames the argument that is actually at fault, and carries it as anattribute; it used to blame "scores" even whenactual_classeswas the one that could not be iterated.
0.6.1 - 2026-08-03¶
Added¶
thresher.__version__- the installed package version, importable at last (#40). It is read from the distribution metadata, sopyproject.tomlstays the single source of truth, and it always matches whatthresher --versionprints - the command was already versioned; the library now admits to one too.
Fixed¶
- The
[Unreleased]compare link at the foot of this file still pointed atv0.5.3; it now moves forward with each release again.
0.6.0 - 2026-08-03¶
Added¶
- An Apache Spark interface -
thresher.spark.SparkThresher, which takes a DataFrame and two column names rather than two in-memory sequences, and never collects the rows.
The Ray backend distributes the ordinary API, which still requires the data to be in memory first. That is the wrong shape for data living in HDFS, S3 or a Delta table: collecting a billion rows onto one machine to sort them is what having a cluster is supposed to avoid.
from thresher.spark import SparkThresher
SparkThresher().optimize_threshold(df, score_col="probability", label_col="label")
The problem reduces to a map-reduce, and this takes that reduction literally. Spark runs
one groupBy and a pair of sums - a shuffle of counts, not of rows - and returns a
summary sized by the resolution rather than by the row count. The driver then sweeps
that summary:
-
histandexactare the two algorithms offered, for the same reason only some algorithms distribute on Ray: their work is an aggregation.histis the default here and groups by bin index, so its summary isno_of_binsrows however large the input;exactgroups by distinct score and warns when it is collecting more than a million of them.ls,grid,sgrid,genandsgdraiseConfigurationErrorrather than quietly running on a sample or on the driver -lsis quadratic in candidates, and the other three draw their own random subsamples, so distributing them would change the answer and not merely where it was computed. -
pyspark>=3.4as an optional extra,pip install 'thresher-py[spark]'. It is not a runtime dependency:thresher.sparkcan be imported without it, and reports its absence asBackendDependencyErrorwhen aSparkThresheris built.
Changed¶
-
The deciding half of both supported algorithms is now a reusable function over counts -
sweep_binsinalgs/histogram/compute.pyandsweep_class_countsinalgs/exact/compute.py. The Spark path calls those same functions rather than reimplementing the sweep, which is what makes the results identical instead of merely close.tests/test_spark.pyasserts a distributed run returns the samefloatas the in-memory run, including on data full of ties, and that repartitioning to 1, 3 or 8 partitions does not move it. -
CI pins a Temurin 17 JDK for the test matrix, since PySpark 4.x needs Java 17 or newer and the runner image's JDK is not something to rely on by accident.
Fixed¶
- The documentation home page still advertised six algorithms; the histogram sweep added in 0.5.3 made it seven.
0.5.3 - 2026-07-27¶
Added¶
hist, a histogram sweep - a non-exact estimator whose memory does not follow the input. The score range is divided into a fixed number of bins, the classes falling into each are counted in one pass, and the bins are swept with the same running-total argument the exact sweep uses over distinct scores. Nothing is sorted and no row is read twice.
hist |
exact |
|
|---|---|---|
| 100,000 rows | 19 KB | 12 MB |
| 1,000,000 rows | 49 KB | 107 MB |
The cost is resolution: a threshold can only sit on a bin edge, so the answer is off by
at most one bin width. That makes it the one approximation here with a bounded error
rather than a statistical one - it does not sample, so it returns the same answer every
run. At the default 1,024 bins it captures 99.98% of the achievable accuracy, and
no_of_bins trades resolution against memory directly.
Where grid also evaluates a fixed set of candidates, it rescans every row for each one
(O(c·n)); this reads each row once whatever the resolution, at O(n + k).
Changed¶
-
optimize_thresholdno longer copies inputs that are already sequences. It always built its own lists, which costs memory proportional to the input and defeated the point of an algorithm whose own allocation is flat -histwould still have paid for two full copies to reach it. ASequencecan be measured and iterated more than once, which is all the solvers need; anything else is consumed into a list as before. End-to-end memory forhiston a million rows fell from 15.7 MB to 49 KB. -
The Documentation link in the package metadata points back at the GitHub Pages build, and the README now links to Read the Docs. PyPI can verify a URL it can tie to the publishing repository - which, under Trusted Publishing, includes that repository's GitHub Pages domain - so this returns the link to the "Verified details" section of the PyPI project page. Read the Docs, being a third-party domain, cannot be verified. The README keeps the Read the Docs link because that is the build with a copy of every release and a version switcher.
0.5.2 - 2026-07-27¶
Changed¶
- The Documentation link in the package metadata now points at
Read the Docs rather than the GitHub Pages
build, so the link shown on the
PyPI project page leads to the host that keeps a
built copy of every release. It points at
stable, which tracks the newest tag, so a reader arriving from a release on PyPI gets that release's documentation rather than whatever has been merged since.
Both sites are published and carry the same content; they are built from the same
mkdocs.yml and the same docs dependency group. The README continues to link to the
GitHub Pages build.
Package metadata is written when the distribution is built, so this only takes effect from this release onwards - the link on older versions still points at GitHub Pages.
0.5.1 - 2026-07-27¶
Added¶
- A documentation site, published at
oskar-j.github.io/thresher. Built with MkDocs and
Material, with the API reference generated by
mkdocstringsfrom the Google-style docstrings the package already carried - so the reference cannot drift from the source, because it is the source.
Guides for getting started (including using it with scikit-learn), the algorithms and how they compare, the command line, the Ray backend and the exception hierarchy, plus a generated API reference for every public class and function, and the changelog.
-
.github/workflows/docs.yml, which builds the site on every pull request and deploys it whenmainmoves. The build runs with--strict, so a dead cross-reference or a page missing from the navigation fails rather than quietly shipping a broken site. -
.readthedocs.yaml, so Read the Docs works as an alternative host. GitHub Pages needs no third-party account and is what the badge points at; importing the repository at readthedocs.org will pick this file up and build the same site from the same dependency group. -
A
docsdependency group, kept out ofdevbecause the toolchain is large and only the docs build needs it, andmake docs/make docs-serve. -
A documentation badge, and a link to the site from the top of the README.
0.5.0 - 2026-07-27¶
Removed¶
- The oracle, as announced in
0.4.0. It chose an algorithm from the size of the input, because the only exact algorithm wasO(n²)and stopped being affordable, so accuracy had to be traded against volume.exactremoved that trade-off in0.4.0by being exact at every size and cheaper than the approximations, at which point the oracle had nothing left to decide and had been returningexactunconditionally.
run_oracle() is gone, and the algorithm is settled when a Thresher is built rather
than per call.
Nothing is required of you. Callers on the default already had exact, and
algorithm='auto' - along with 'default' and 'default_heuristics' - still works as a
synonym for the default.
-
'auto'is no longer an entry inavailable_algorithms, soget_supported_algorithms()returns six real algorithms rather than five plus the oracle.Thresher().get_current_algorithm()now reportsexactinstead ofauto, which is what actually runs. -
thresher.oracleis nowthresher.dispatch. Keeping a module named for a mechanism it no longer contains would repeat the naming mismatchexceptions.pyhad before0.4.5. It holdsrun_computations, and is still the only module that imports the solvers.
Added¶
- A warning when an algorithm is asked to handle more data than it is comfortable with.
data_vol_threshwas vestigial once the oracle stopped routing on it; it is now filled in for every algorithm and drives alogging.warning:
WARNING thresher.dispatch: Linear search is likely to be slow on 12,000 rows - it is
usually comfortable up to about 10,000. The 'exact' algorithm is exact and O(n log n)...
| Algorithm | Comfortable up to | Algorithm | Comfortable up to | |
|---|---|---|---|---|
exact |
10,000,000 | grid |
1,000,000 | |
sgrid |
10,000,000 | gen |
100,000 | |
sgd |
2,000,000 | ls |
10,000 |
The figures come from the timings in examples/benchmark.py, at roughly where a run
passes ten seconds on one laptop. They are guidance rather than limits - a faster machine
moves them all up - so crossing one warns and continues rather than refusing. It goes
through logging rather than warnings precisely so it can be silenced the ordinary
way: logging.getLogger("thresher").setLevel(logging.ERROR).
- A Memory column in the README's algorithm comparison, derived from the
implementations. It shows that
exactisO(d)in distinct scores rather than rows, thatgridis the only algorithm whose memory does not grow with the input, and thatsgridallocatesO(n)despite reading only a fraction of the data - it builds the full paired list before sampling, so the subsampling buys time but not memory.
0.4.5 - 2026-07-26¶
Added¶
- An exception hierarchy.
exceptions.pyheld only message strings, so everything was raised as a builtin and callers had to catchValueError- which also swallows failures from numpy, pandas or their own code. Every error now derives fromThresherError:
ThresherError
├── ConfigurationError a name that does not exist (ValueError)
│ ├── UnknownAlgorithmError
│ └── UnknownBackendError
├── InvalidInputError the data cannot be optimized over (ValueError)
│ ├── EmptyInputError, LengthMismatchError, MissingLabelsError,
│ └── UnexpectedLabelsError, SingleClassError, InsufficientDataError
├── LabelMappingError the `labels` option cannot map (TypeError)
├── NotIterableError arguments are not iterable (AttributeError)
├── BackendDependencyError an optional dependency is missing (ImportError)
├── AlgorithmNotWiredError a bug in this package (NotImplementedError)
└── ShardMergeError a bug in this package (ValueError)
Every class also inherits the builtin it was previously raised as, shown on the
right. That is what keeps this an addition rather than a breaking change: code catching
ValueError, TypeError, AttributeError, ImportError or NotImplementedError
behaves exactly as before - including this package's own command line, which catches
ValueError and ImportError. There are tests for both directions.
- Errors carry their detail as attributes rather than only in prose:
LengthMismatchError.score_countand.class_count,MissingLabelsError.count,UnknownAlgorithmError.nameand.available,UnknownBackendError.nameand.available,UnexpectedLabelsError.unexpected,SingleClassError.only.
Changed¶
NotIterableErrorinheritsAttributeErrorrather thanTypeError, which would fit the failure better. Earlier versions raisedAttributeErrorhere, and changing it would break existingexceptclauses for no practical gain.- The message templates remain importable module constants. They define the wording, the classes format them, and they were importable before the classes existed.
0.4.4 - 2026-07-26¶
Clears the outstanding entries from CLAUDE.md's known issues, ahead of 0.5.0.
Fixed¶
- Mismatched input lengths now raise instead of being silently truncated. The solvers
pair
scoresandactual_classeswithzip, which stops at the shorter sequence, so six scores against four classes quietly discarded two scores and returned a threshold computed from the rest - a wrong answer rather than a partial one, with nothing in the result to hint at it.optimize_thresholdnow raisesValueErrornaming both counts.
This is a behaviour change: code that was relying on the truncation, knowingly or not, will now see an exception. That is the point - it was previously getting an answer derived from part of its data.
- Missing labels are now named as missing. A blank cell in a CSV arrives as NaN, which
was reported as an unrecognised label and pointed at the
labelsoption - advice that fits a differently-encoded class, not an absent one, and that sends people looking for a mapping which cannot exist. The message now says how many values are missing and that those rows need filling in or dropping. Found while checking how the length-mismatch error reads from the command line.
Added¶
stoch_ratioforsgd(default 0.05, unchanged), the one parameter the algorithm did not expose whilegenandsgridboth did. It is the documented lever against sgd's weak spot - when one class is rare, a small subsample carries little information about where the boundary lies. On 2,000 rows with 5% positives, raising it from 0.05 to 0.5 took the mean error from 0.0394 to 0.0035 and the worst case across 20 seeds from 0.302 to 0.013, at the cost of reading ten times as much data per step.
Changed¶
- The README's "Sample usage" now imports the class directly, matching the opening example since 0.3.2.
0.4.3 - 2026-07-26¶
Added¶
- Test coverage is now measured and enforced. Branch coverage is on, the floor is 90%, and
CI applies it on every supported Python version - the
testjobs are required checks onmain, so a pull request that drops below the floor cannot be merged. Coverage stands at 95%, and each job uploads itscoverage.xmlas an artefact. make covruns the tests with coverage and the same threshold CI uses.- A coverage badge. It states the enforced floor rather than a measured number, because that is what can be guaranteed: CI makes the claim true on every merge, whereas a hardcoded percentage would drift and a third-party service would read "unknown" until someone linked the repository to it.
- Tests for the parts the end-to-end suite reached around rather than through: the shared
helpers (
granularity_of_scores,calculate_range_mean,get_mean_value_for_class_pd,pairwise,get_or_default,map_labels,print_progress_bar,stochastic_processin both of its modes),process_batch- which normally runs inside a worker process where neither assertions nor coverage reach it - everyverboseandprogress_barreporting path, the CLI's value coercion and failure paths, and the dispatch guard that fires when the algorithm registry andrun_computationsdisagree.
One behaviour was pinned down in the process: calculate_range_mean emits two numpy
RuntimeWarnings and returns NaN when a class is absent. That is unreachable through
optimize_threshold, which rejects single-class input first, but reachable by calling
the helper directly.
0.4.2 - 2026-07-26¶
Added¶
- A Ray backend, so the same computation can be spread over a
Ray cluster instead of running in one process.
Install with
pip install 'thresher-py[ray]'and select withThresher(backend="ray")orthresher --backend ray. The default is unchanged and Ray is not required.
The work is a map-reduce: the data is sharded once into Ray's object store, each worker
counts its own shard, and the driver adds the partial counts together. exact, ls and
grid are distributed this way. sgrid and gen are not - each of their evaluations
reads its own random subsample, so sharding would change which samples are drawn and
therefore the result - and sgd is a sequential walk with nothing to parallelise. All
three still run under backend='ray', just in-process.
A backend changes where the work happens, never the answer. Both map steps are plain functions shared verbatim: the Ray backend ships them to workers rather than reimplementing them, and the reduce steps are addition, which is order-independent. Tests assert the two backends return identical results, not merely close ones, including on data full of duplicates and ties.
backendoption onThresher, and--backendon the CLI. Accepts a name or a configured backend instance, so sharding can be tuned withRayBackend(num_shards=16). An unknown name, or a missing Ray, is reported when the object is built rather than partway through a run.
Changed¶
exact,lsandgridnow score their candidates through the backend rather than in a local loop. Results are unchanged - the existing brute-force and parity tests cover this- but with
progress_bar=Truethe bar forlsandgridnow brackets the work instead of advancing through it, since candidates are scored in one batch. - For
ls, a non-local backend takes precedence over then_jobsmultiprocessing option, which predates backends and would only contend with a cluster for cores.
0.4.1 - 2026-07-26¶
Fixed¶
exactnow considers the "classify everything as positive" split, and is therefore optimal over every split a threshold can induce rather than only those inside the data. Expressing it needs a threshold below the smallest score, which nothing could previously return, so on data where scores and classes run contrary to each other the best answer was unreachable. Given[0.1, 0.2, 0.3]labelled[1, 1, -1]the sweep returned0.15, classifying 1 of 3 correctly, where 2 of 3 was available. The threshold used ismath.nextafter(min(scores), -inf)- the largest float that qualifies, so the answer stays as close to the data as the representation allows - and it is taken only on a strict improvement, never to break a tie. Across 4,000 randomised inputs the sweep now matches a brute force over both edge splits every time; the new split is chosen in about 10% of them.
This is the only case where a returned threshold can fall outside the range of the input. The other algorithms cannot express either edge split and are unchanged.
Added¶
- A
Makefilewithinstall,check,test,fmt,types,benchandbuildtargets.make installinstalls the git pre-commit hook alongside the dependencies, andmake checkrunsgit add --intent-to-add .before the hooks.
That last detail is the point of it. pre-commit run --all-files only sees files git
already tracks, so a newly created file is skipped in silence while the run reports
success - and CI, which checks out the committed tree, then fails on exactly what the
green local run missed. That caught out two releases in a row. make check closes it,
and the installed hook closes it again at commit time.
0.4.0 - 2026-07-26¶
Added¶
exact, an exact algorithm in O(n log n), and the new default. It returns the best threshold that exists, not an approximation, and does so faster than any of the approximations it replaces.
Linear search is O(n²) because it recomputes the whole confusion matrix for each of the n-1 candidate thresholds. That work is almost all redundant: moving the threshold past a single sample changes the number of correct predictions by exactly one, in a direction fixed by that sample's class. Sorting once and sweeping while carrying running counts therefore costs O(n log n), dominated by the sort:
This is the standard exact splitter for a decision-stump threshold, and the same linear scan used to generate an ROC curve - Fawcett, "An introduction to ROC analysis" (Pattern Recognition Letters, 2006), and Google's decision forests documentation, which gives the same O(n log n) bound "because of the sorting of the feature values". Runs of equal scores are indivisible and are stepped over whole, the tie-handling point Fawcett makes.
Measured against linear search on the same inputs:
| rows | exact |
ls |
speedup |
|---|---|---|---|
| 1,000 | 0.6 ms | 65 ms | 104× |
| 4,000 | 2.2 ms | 957 ms | 431× |
| 16,000 | 12 ms | 16,536 ms | 1,358× |
exact doubles in cost when the input doubles; ls quadruples, as O(n²) requires, so
the gap keeps widening.
It is also marginally more accurate than linear search. ls only considers midpoints
between adjacent scores, so it cannot express the "classify everything as negative"
split; the sweep reaches it at max(scores). Over 3,000 randomised inputs with
duplicates, ties and inverted labels, exact matched the brute-force optimum every time
and beat ls in 287 of them.
Deprecated¶
- The oracle mechanism, to be removed in
0.5.0. It existed to choose between algorithms trading accuracy against input size, andexactsettled that question, so there is nothing left to delegate.exactbecomes the plain default in0.5.0. Code using the default is unaffected - it already resolves toexact- andalgorithm='auto'will keep working as an alias for the default.
Changed¶
- The oracle now always selects
exact. It previously routed on input size - linear search below 1,000 rows, grid search below 50,000, stochastic gradient descent above that - because the only exact algorithm was O(n²) and stopped being affordable. That trade-off no longer exists. Callers who relied on the default choosing a particular algorithm for a particular size will see a different, and better, one; select by name to pin the old behaviour. Algorithm.data_vol_threshis now advisory. It records where each of the older algorithms stops being a sensible manual choice, and is no longer read for routing.
0.3.2 - 2026-07-26¶
Added¶
- A
threshercommand-line interface, installed with the package. It reads a delimited file (or stdin) holding one row per sample and prints the optimal threshold:
Only the bare number goes to stdout, so the result pipes into another command without
anything to strip out; progress and errors go to stderr. Flags cover the usual shape
mismatches - --labels for classes that are not -1 and 1, --sep and --no-header,
--score-column and --label-column to select by name or index, -a to pick the
algorithm and -p key=value to pass its parameters. --list-algorithms prints the
algorithms with their aliases.
Library exceptions are rewritten for a terminal audience: a mistyped algorithm points at
thresher --list-algorithms rather than at get_supported_algorithms(), and unmapped
labels point at --labels 0,1 rather than at Thresher(labels=(0, 1)). Exit codes
follow convention - 2 for a usage mistake, 1 when the data cannot be optimized.
- click as a runtime dependency, for the above. It is a small pure-Python package next to
the existing numpy and pandas requirements, so the command works from a plain
pip install thresher-py rather than needing an extra.
- Two more README badges: the supported Python versions and the licence. Both read from
PyPI metadata, so neither can drift out of date the way a hardcoded badge would.
Changed¶
- The README's opening example imports the class directly -
from thresher import Thresher - rather than reaching through the module.
0.3.1 - 2026-07-25¶
Fixed¶
- The
sgdwalk no longer stalls short of the optimum. Its step size was scaled by the relative gain of each move, which compounded: as progress slowed the step shrank, which slowed progress further, until a step so small that two consecutive subsamples scored identically drove the gain to exactly0.0and the step to0.0with it. The walk then froze wherever it stood, and the stopping rule reported that as convergence. The step now follows a fixed decay schedule and takes only its direction from the gain.
The effect is largest where the optimum sits far from the mean of the scores, which is
where the walk begins. On separable data with the boundary at 0.85, sgd returned about
0.56 and mis-classified 29% of samples while reporting success. Mean error against the
true optimum, over 8 seeds:
| data | before | after |
|---|---|---|
| boundary at 0.70 | 0.1343 | 0.0014 |
| boundary at 0.85 | 0.2910 | 0.0015 |
| boundary at 0.95 | 0.3929 | 0.0074 |
| separable | 0.0305 | 0.0037 |
| noisy, 15% overlap | 0.1762 | 0.1676 |
sgdnow returns the best threshold it visited rather than whichever one it stopped on. The walk continues past unproductive steps, so its final position was often worse than a point it had already passed through.- Dividing by the previous evaluation is gone with the old step rule, so the guard added in 0.2.2 against a zero-valued divisor is no longer needed. It used to return immediately when a subsample happened to be classified perfectly, abandoning the search on the strength of one lucky sample.
Added¶
-
stop_patience(default 3) forsgd: how many consecutive unproductive steps end the walk. Every evaluation reads a different random subsample, so a single small gain is as likely to be noise as convergence, and stopping on the first one left the walk short. Worst-case error over 20 seeds on a heavily skewed 2,000-row input fell from 0.452 to 0.302, and mean error from 0.287 to 0.039. -
examples/benchmark.py, which measures every algorithm against the exact optimum and prints the comparison table in the README. The reference optimum is computed by sweeping the sorted scores, independently of the algorithms being measured, and is checked against brute force.
Changed¶
- Rewrote the README opening. The optical-illusion image and its caption are replaced with
an explanation of what the project is for - that a
predict_probacut-off is a decision most pipelines leave at 0.5 by default rather than by measurement. - Added a table of contents to the README, and an "Algorithm scores" section comparing the
five algorithms on accuracy, runtime and complexity.
Implemented algorithmsis now a top-level heading, so the sections nest properly under it.
0.3.0 - 2026-07-25¶
No behavioural change to the public API: Thresher, its options and its results are the
same as in 0.2.3. This release is about the shape of the project.
Changed¶
- Adopted the
src/layout: the package now lives insrc/thresher/. Tests therefore run against the installed package rather than whatever happens to be onsys.path, so a packaging mistake fails the suite instead of hiding behind the working directory. - Moved the tests out of the distribution to a top-level
tests/. They were previously shipped inside the package asthresher.testsand excluded from the wheel by hand. - Fixtures are now located relative to the test file. The suite ran only from inside
thresher/tests, whileexamples/sample.pyrequired the repository root - the two were mutually exclusive. Both now run from anywhere. - Converted the suite from
unittesttopytest, using fixtures andconftest.py. Parametrisation expands the same coverage from 26 cases to 65. - Moved the README illustration to
docs/assets/. - The evolutionary algorithm's agents are a dataclass rather than a dict.
samplesandfitnessare now separate fields, which makes the 0.2.1 fitness bug - where one key held both and the aggregate overwrote the samples - impossible to express. map_labels()raisesTypeErrorfor a non-list/tuple mapping instead of asserting, so the check survivespython -O.run_oracle()likewise raisesTypeErrorrather than asserting when a routing threshold is missing from the registry, and the grid solver was restructured so it no longer needs a type-narrowing assertion at all. Noassertstatements remain insrc/.
Added¶
- Type annotations throughout, checked by
mypy --strict, plus apy.typedmarker so the annotations are visible to consumers of the published package. .pre-commit-config.yamlrunning ruff, ruff-format, mypy and file-hygiene hooks.- Ruff and mypy configuration in
pyproject.toml. - A
docs/directory with a placeholderindex.md. - Docstrings on every module, class and function in
src/, in the Google style already used byThresher, documenting arguments, return values and the exceptions raised. They record behaviour that is not evident from the signatures - thatrun_parallelevaluates the scores themselves as thresholds whilerunevaluates the midpoints between them, that grid search always spans[0, 1]regardless of the input range, that the genetic solver returns the population mean rather than its fittest agent, and that an unrecognisedalgorithm_paramskey is silently ignored. - A
pre-commitCI job. Pull requests are now gated on both it and thetestmatrix. examples/sample_data.py, replacing the loader that used to live in the test package.
Fixed¶
- The examples now guard their entry point with
if __name__ == "__main__":.examples/sample_parallel.pyasks for multiprocessing, and on platforms whose start method isspawn- macOS and Windows - every worker re-imports the script. Without the guard each worker re-ran the example and spawned workers of its own, so the example hung instead of failing. It worked only on Linux, where the start method isfork.
0.2.3 - 2026-07-25¶
Fixed¶
- An unknown algorithm name now raises
ValueErrornaming the value and listing the valid algorithms, instead of a bareStopIterationwith no message.retrieve_by_alias()ended in anext()call with no default, soThresher(algorithm='typo')surfaced an exception that read as an internal bug rather than a rejected argument. - Invalid labels now raise
ValueErrorexplaining the problem, instead of a message-lessAssertionError. Single-class input says both classes are required; labels outside(-1, 1)point at thelabelsconstructor option; empty input is named as such. The previous check was anassert, whichpython -Ostrips entirely — malformed input then reached the solvers rather than being rejected, and there is now a test that runs under-Oto keep that from returning.
Changed¶
set_algorithm()now raisesValueErrorfor an unknown name. It previously printed a message and returned successfully with the old algorithm still selected, so callers were told a switch had happened when it had not. Code that relied on that silent no-op will now see an exception.
Added¶
ThresherInputValidationTest, covering each of the above plus the alias lookups that must keep working.
0.2.2 - 2026-07-24¶
Fixed¶
sgdno longer walks outside the range of the scores it was given, and converges much closer to the optimum. Three things compounded: the gradient's sign was flipped a second time whenever a move made the error worse, cancelling the correction that would have reversed the walk; the step size was unbounded, so a large relative gain could fling the point across the whole range; and once outside the data the error curve is flat, so the gain hit exactly 0 and the stopping rule reported convergence on a divergence. The walk is now clamped to[min(scores), max(scores)], the double sign flip is gone, and the step is capped at half the data range. On separable data, mean error against linear search falls from 4.0790 to 0.0321 at 5,000 rows and from 0.8883 to 0.0077 at 2,000; on noisy data it roughly halves at every size tested.sgdno longer raisesZeroDivisionErrorwhen a stochastic evaluation mis-classifies nothing. The gradient update divided by that evaluation; it now stops and returns the point instead, since a zero mis-classification ratio cannot be improved on. This was reachable on cleanly separable data at most data volumes, and mattered most because the oracle selectssgdfor inputs above 50,000 rows.- The stochastic solvers no longer raise
ZeroDivisionErroron small inputs. Sample sizes were computed asint(ratio * N), which floors to 0 — breakinggenbelow 50 rows andsgridbelow 20. Sample sizes are now clamped to at least 1 and at most the input size. get_current_algorithm()no longer raisesTypeError. It usedwithon anAlgorithmnamedtuple, so it could never have worked.n_jobs=-1for linear search no longer raisesTypeError. The documented "use all processors except one" behaviour produced a negative chunk size, makingpool.mapreturnNoneentries. The chunk size is now derived from the resolved process count.- The multiprocessing pool used by linear search is now closed, resolving a
ResourceWarning.
Added¶
ThresherCrashRegressionTestandThresherResultRangeTest, covering each of the above. These paths — explicitly selected algorithms, small inputs, and separable data — had no coverage previously. The range test asserts every algorithm returns a threshold within the span of its input scores.- Badges in
README.mdfor the released version, build status, downloads and stars. - The README illustration is now vendored at
assets/optical-illusion.pnginstead of being hotlinked from a third-party site, so it cannot break or change underneath the project. It is W. E. Hill's "My Wife and My Mother-in-Law" (1915), public domain.
0.2.1 - 2026-07-24¶
Fixed¶
- Evolutionary algorithm: fitness is now computed from the accumulated per-iteration
mis-classification samples instead of from the agent's own threshold value. The previous
code called
np.mean()on a scalar, so it discarded every fitness sample it had gathered and selected agents by threshold value rather than by how well they performed. Results are now materially closer to the true optimum, and the intermittenttest_data_case_alt2failure (~10% of runs) is resolved.
Added¶
- Automated publishing to PyPI, using Trusted Publishing (OIDC), triggered after a GitHub Release is created.
0.2.0 - 2026-07-24¶
Added¶
pyproject.toml(PEP 621) as the single source of project metadata and dependencies.uv.lock— a fully resolved, cross-platform dependency lockfile for reproducible environments.- Support for Python 3.13 and 3.14. The test suite is verified against 3.10 through 3.14.
CHANGELOG.md(this file).CLAUDE.mdwith build/test commands and an architecture overview.- Automated GitHub Release publishing when a version bump lands on
main.
Changed¶
- Packaging migrated from
setup.pytouv, using thehatchlingbuild backend. - Minimum supported Python raised to 3.10 (3.9 reached end-of-life in October 2025).
openpyxlis now declared as a dev dependency. It is required to read the.xlsxtest fixtures and was previously an undeclared, missing requirement.- Release tags now follow the standard
v0.2.0form rather than the previousv_01_2style.
Removed¶
setup.py,setup.cfg, andrequirements.txt, all superseded bypyproject.toml.xlrddependency. It was declared as a runtime requirement but was only ever used to read test fixtures — which are excluded from the distribution — andxlrd2.x cannot read.xlsxfiles at all.
0.1.2 - 2020-10-14¶
Added¶
- Grid search algorithm, with granularity controlled by
no_of_decimal_places. - Stochastic grid search algorithm, adding the
stoch_ratioandreshuffleparameters. algorithm_paramsconstructor argument for passing per-algorithm settings.- Custom label mapping via the
labelsargument, for inputs not using(-1, 1). - Multiprocessing for linear search through the
n_jobsparameter. - Performance evaluation notebooks under
examples/performance_test/, with a 10^6-row anonymized dataset.
Changed¶
- Reworked the oracle's algorithm selection (fixes #1).
- Moved example scripts into a separate
examples/directory.
0.1.1 - 2020-10-12¶
Added¶
meta_optimizer.py, providing per-class mean helpers used to seed the evolutionary algorithm's initial population range.
Changed¶
- Algorithmic improvement to the genetic (
gen) method. - Distribution renamed to
thresher-pybecause of a PyPI name conflict.
0.1.0 - 2020-10-11¶
Added¶
- Initial release of
Thresher.optimize_threshold(). - Linear search algorithm.
- Naive 2-dimensional stochastic gradient descent algorithm.
- Evolutionary (genetic) algorithm.