Algorithms¶
The registry¶
thresher.algorithm ¶
The registry of selectable algorithms, and lookup by name.
Algorithm ¶
Bases: NamedTuple
A selectable algorithm.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
the canonical short name, and the key in |
full_name |
str
|
human-readable name, used in verbose output. |
synonyms |
list[str]
|
alternative names accepted by |
data_vol_thresh |
int
|
input size beyond which this algorithm is slow enough to be worth
warning about. Each value is roughly where a run passes ten seconds, extrapolated from the
timings in |
retrieve_by_alias ¶
retrieve_by_alias(name: str) -> Algorithm
Resolve an algorithm by its id or by one of its synonyms.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
an algorithm id such as |
required |
Returns:
| Type | Description |
|---|---|
Algorithm
|
The matching |
Raises:
| Type | Description |
|---|---|
UnknownAlgorithmError
|
if the name matches nothing - including anything that is
not a string at all, which previously escaped as a bare |
Source code in src/thresher/algorithm.py
Exact sweep¶
thresher.algs.exact.compute.run ¶
run(
scores: Sequence[float],
actual_classes: Sequence[int],
progress_bar: bool,
alg_options: Mapping[str, Any],
backend: Backend | None = None,
) -> float
Find the threshold with the highest accuracy, exactly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
progress_bar
|
bool
|
draw a progress bar on stderr. |
required |
alg_options
|
Mapping[str, Any]
|
accepted for signature compatibility with the other solvers. This algorithm has nothing to tune - it is exact, so there is no accuracy to trade against speed. |
required |
backend
|
Backend | None
|
where the counting happens. Defaults to in-process. Only the counting is distributed; the sweep over distinct scores is trivial by comparison and stays on the driver. |
None
|
Returns:
| Type | Description |
|---|---|
float
|
A threshold yielding the highest achievable fraction of correctly classified |
float
|
samples - the best that exists, over every split a threshold can induce. |
float
|
Interior results are the midpoint between the two scores they separate, matching |
float
|
linear search. Two results sit at the edges: |
float
|
negative, and a value just below |
float
|
The latter is the only result that can fall outside the span of the input, and it |
float
|
is returned only when it beats every threshold inside it, which needs data where |
float
|
score and class run contrary to each other. |
Raises:
| Type | Description |
|---|---|
InsufficientDataError
|
if no scores were given. It is a |
Source code in src/thresher/algs/exact/compute.py
Histogram sweep¶
thresher.algs.histogram.compute.run ¶
run(
scores: Sequence[float],
actual_classes: Sequence[int],
progress_bar: bool,
alg_options: Mapping[str, Any],
) -> float
Find a near-optimal threshold from binned counts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
progress_bar
|
bool
|
draw a progress bar on stderr. |
required |
alg_options
|
Mapping[str, Any]
|
recognised keys, falling back to the module-level default:
|
required |
Returns:
| Type | Description |
|---|---|
float
|
The best threshold the binning can express: a bin edge, or a value just below the |
float
|
smallest score where classifying everything positive wins. Within one bin width of |
float
|
what |
Raises:
| Type | Description |
|---|---|
InsufficientDataError
|
if no scores were given, or |
Source code in src/thresher/algs/histogram/compute.py
Linear search¶
thresher.algs.linear.compute.run ¶
run(
scores: Sequence[float],
actual_classes: Sequence[int],
progress_bar: bool,
backend: Backend | None = None,
) -> float
Evaluate the midpoint between every pair of adjacent scores, exactly.
Unlike the other solvers this one takes no alg_options; its only parameter, n_jobs,
selects run_parallel instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
progress_bar
|
bool
|
draw a progress bar on stderr. Since 0.4.2 the candidates are scored in one batch, so this brackets the work rather than advancing through it. |
required |
backend
|
Backend | None
|
where the counting happens. Defaults to in-process. |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The midpoint threshold with the highest accuracy. Where several tie, the first one |
float
|
found wins. |
Raises:
| Type | Description |
|---|---|
InsufficientDataError
|
if fewer than two scores were given, leaving no midpoint
to evaluate. It is a |
Source code in src/thresher/algs/linear/compute.py
thresher.algs.linear.compute.run_parallel ¶
Run the linear search across several processes.
Selected by run_computations when allow_parallel is set and n_jobs != 1.
Since 0.7.0 this is the ordinary search running on the mp backend, rather than a
second implementation of it. Two things follow. The answer no longer depends on
whether the search was parallelised: this used to evaluate the raw scores as
thresholds where the sequential path evaluates the midpoints between them, so the two
returned different - though equally valid - answers for the same data. And the
__main__ guard that separate processes need is now enforced with an explanation
instead of hanging.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
n_jobs
|
int
|
number of worker processes, or -1 for every available processor bar one. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The threshold with the highest accuracy - the same one the sequential path finds. |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
if |
ParallelBootstrapError
|
if the workers could not start, which usually means a
missing |
Source code in src/thresher/algs/linear/compute.py
Grid search¶
thresher.algs.grid.compute.run ¶
run(
scores: Sequence[float],
actual_classes: Sequence[int],
progress_bar: bool,
alg_options: Mapping[str, Any],
stochastic: bool = False,
backend: Backend | None = None,
) -> float
Evaluate every point on a grid spanning the data and keep the best.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
progress_bar
|
bool
|
draw a progress bar on stderr. |
required |
alg_options
|
Mapping[str, Any]
|
recognised keys, each falling back to its module-level default:
|
required |
stochastic
|
bool
|
score each candidate against a subsample rather than all the data. |
False
|
backend
|
Backend | None
|
where the counting happens. Defaults to in-process, and is used only for the exhaustive path - the stochastic one draws its own subsamples, which sharding would change. |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The grid point with the highest measured accuracy. The grid spans |
float
|
|
float
|
actually occupies whatever its scale; one further candidate below the minimum |
float
|
expresses "classify everything as positive". Ties go to the leftmost candidate, |
float
|
which keeps the answer inside the data unless the edge is strictly better. |
Raises:
| Type | Description |
|---|---|
InsufficientDataError
|
if the grid yielded no candidates at all. It is a
|
Source code in src/thresher/algs/grid/compute.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
thresher.algs.grid.compute.run_stoch ¶
run_stoch(
scores: Sequence[float],
actual_classes: Sequence[int],
progress_bar: bool,
alg_options: Mapping[str, Any],
) -> float
Run the grid search against a random subsample rather than the full dataset.
This is the sgrid algorithm - a thin wrapper that passes stochastic=True into
run, since the two share an implementation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
progress_bar
|
bool
|
draw a progress bar on stderr. |
required |
alg_options
|
Mapping[str, Any]
|
may hold |
required |
Returns:
| Type | Description |
|---|---|
float
|
The grid point with the highest measured accuracy. |
Source code in src/thresher/algs/grid/compute.py
Evolutionary algorithm¶
thresher.algs.genetic.compute.run ¶
run(
scores: Sequence[float],
actual_classes: Sequence[int],
progress_bar: bool,
alg_options: Mapping[str, Any],
) -> float
Evolve a population of candidate thresholds and return the fittest one measured.
The initial population is seeded across the range between the mean score of the negative class and that of the positive class, so the search starts where the boundary is likely to lie.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
progress_bar
|
bool
|
draw a progress bar on stderr, one step per generation. It is not
drawn while the log is at DEBUG, which is where the per-generation detail
goes: the two write to the same stream. That rule used to live here and now
applies to every solver - see |
required |
alg_options
|
Mapping[str, Any]
|
recognised keys, each falling back to its module-level default:
|
required |
Returns:
| Type | Description |
|---|---|
float
|
The trait of the fittest agent that was actually measured, across every |
float
|
generation. Until 0.7.3 this was the mean of the final population - which is bred |
float
|
after the last round of scoring and so never evaluated at all, letting one |
float
|
crossover and one mutation reach the answer with no selection in front of them. |
float
|
With |
float
|
spanning [0, 1]. |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
if any of the four counts is below 1, or if |
Source code in src/thresher/algs/genetic/compute.py
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | |
thresher.algs.genetic.compute.Agent
dataclass
¶
A candidate threshold and its measured fitness.
'samples' and 'fitness' are deliberately separate fields. They were once a single key that started as a list of samples and was overwritten with the aggregate, which is how the fitness ended up being computed from the wrong value entirely (fixed in 0.2.1). Keeping them apart makes that class of mistake impossible to express.
Stochastic gradient descent¶
thresher.algs.sgd.compute.run ¶
run(
scores: Sequence[float],
actual_classes: Sequence[int],
progress_bar: bool,
alg_options: Mapping[str, Any],
) -> float
Find a threshold by walking down the error curve from the mean of the scores.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
progress_bar
|
bool
|
accepted for signature compatibility with the other solvers; this one reports at DEBUG only and never draws a bar. It stops when it stops improving rather than after a known number of steps, so there is no proportion of the job done for a bar to show. |
required |
alg_options
|
Mapping[str, Any]
|
recognised keys, each falling back to its module-level default:
|
required |
Returns:
| Type | Description |
|---|---|
float
|
The best threshold the walk visited, always within |
float
|
This remains the least accurate solver - expect it near the optimum rather than |
float
|
on it, and least reliable when one class is rare, where the subsamples carry |
float
|
little signal about where the boundary lies. Raising |
float
|
for a stronger signal, and |
Source code in src/thresher/algs/sgd/compute.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | |
thresher.algs.sgd.compute.sgd_solver ¶
sgd_solver(
eval_func: EvalFunc,
starting_point: float,
gradient: float,
num_of_iters: int,
stop_thresh: float,
alpha: float,
lower_bound: float,
upper_bound: float,
stop_patience: int = stop_patience_default,
) -> float
Walk the error curve downhill from a starting point, returning the best point seen.
Each step moves by the current step size and then decays it by alpha; only the
direction comes from the measured gain, reversing when a move made things worse.
The walk is clamped to [lower_bound, upper_bound] and each step is capped at half
that range, so it cannot escape the data - outside it the error curve is flat, and the
stopping rule would read that as convergence.
Because every evaluation samples the data afresh, the walk is noisy: it keeps going
through stop_patience unproductive steps before giving up, and returns the best
point it visited rather than its last.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
eval_func
|
EvalFunc
|
scores a candidate threshold. Called as
|
required |
starting_point
|
float
|
threshold to start from, normally the mean of the scores. |
required |
gradient
|
float
|
initial step size and direction. |
required |
num_of_iters
|
int
|
maximum number of steps before giving up and returning anyway. |
required |
stop_thresh
|
float
|
the absolute gain below which a step counts as making no progress. |
required |
alpha
|
float
|
per-step decay applied to the gradient, damping the walk as it proceeds. |
required |
lower_bound
|
float
|
lowest threshold the walk may reach, normally |
required |
upper_bound
|
float
|
highest threshold the walk may reach, normally |
required |
stop_patience
|
int
|
how many consecutive steps must make no progress before the walk gives up. Each evaluation reads a different random subsample, so a single small gain is as likely to be sampling noise as real convergence - stopping on the first one leaves the walk short of the optimum on skewed data. |
stop_patience_default
|
Returns:
| Type | Description |
|---|---|
float
|
The best threshold visited, meaning the one whose sampled mis-classification |
float
|
ratio was lowest - not wherever the walk happened to stop. The two differ |
float
|
whenever the last step was a step backwards. |
Source code in src/thresher/algs/sgd/compute.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
Shared helpers¶
thresher.algs.common.stochastic.stochastic_process ¶
stochastic_process(
evaluated: float,
scores: Sequence[float],
actual_classes: Sequence[int],
random_factor: float,
miss_class: bool = True,
) -> float
Evaluate a candidate threshold against a random subsample of the data.
This is the shared basis for the speed of the sgd and genetic solvers on large
inputs: neither ever reads the whole dataset to score a candidate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
evaluated
|
float
|
the candidate threshold to score. |
required |
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
random_factor
|
float
|
fraction of the data to sample, between 0 and 1. The sample is floored at one item, so small inputs still produce a usable ratio. |
required |
miss_class
|
bool
|
return the mis-classification ratio when True, the accuracy when False. |
True
|
Returns:
| Type | Description |
|---|---|
float
|
The ratio of mis-classified samples in the subsample - so lower is fitter - or |
float
|
the fraction classified correctly if |
float
|
repeated calls with the same threshold return slightly different values. |
Source code in src/thresher/algs/common/stochastic.py
thresher.algs.common.meta_optimizer ¶
Summary statistics used to seed a search before it starts.
calculate_range_mean ¶
calculate_range_mean(
scores: Sequence[float],
actual_classes: Sequence[int],
label: int,
) -> float
Average the scores belonging to one class.
The genetic solver takes the negative-class and positive-class means as the bounds of its initial population, so the search starts around where the boundary should lie.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching ground-truth classes, as -1 and 1. |
required |
label
|
int
|
the class to average over, -1 or 1. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The mean of the scores whose class equals |
float
|
absent, since numpy averages an empty selection. |
Source code in src/thresher/algs/common/meta_optimizer.py
get_mean_value_for_class_pd ¶
get_mean_value_for_class_pd(
label: Any,
label_column: str,
data: DataFrame,
data_column: str,
) -> float
Average one column of a DataFrame over the rows belonging to one class.
The pandas equivalent of calculate_range_mean, for callers holding a frame rather
than parallel sequences. Nothing in the package calls this today.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
Any
|
the class to select on. |
required |
label_column
|
str
|
name of the column holding the class labels. |
required |
data
|
DataFrame
|
the frame to read. |
required |
data_column
|
str
|
name of the column to average. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The mean of |
Source code in src/thresher/algs/common/meta_optimizer.py
thresher.algs.common.tools ¶
Small utilities shared by the solvers.
granularity_of_scores ¶
granularity_of_scores(
scores: Iterable[float],
number_of_decimal_places: int = 2,
) -> Iterator[float]
Round scores down to a coarser granularity.
Reduces a set of scores to the distinct candidate thresholds worth evaluating.
Nothing in the package calls this today - grid search builds its candidates with
numpy.linspace instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Iterable[float]
|
the values to round. |
required |
number_of_decimal_places
|
int
|
how many decimal places to keep. |
2
|
Yields:
| Type | Description |
|---|---|
float
|
Each score rounded to |