Spark¶
Finding a threshold over a Spark DataFrame. See Running on Spark for the guide.
Importing this module does not import PySpark, so it is safe to reference from code
that may run without the [spark] extra installed. PySpark is imported when a
SparkThresher is constructed, and its absence is reported as a
BackendDependencyError.
The entry point¶
thresher.spark.SparkThresher ¶
SparkThresher(
algorithm_name: str = "hist",
algorithm_params: dict[str, Any] | None = None,
labels: tuple[Any, Any] | None = None,
verbose: bool = False,
verbosity: str | None = None,
)
Find the optimal threshold over a Spark DataFrame.
Mirrors Thresher, but takes a DataFrame and the names of two columns instead of two
sequences, and never collects the rows.
Example
from thresher.spark import SparkThresher SparkThresher().optimize_threshold(df, "probability", "label") # doctest: +SKIP 0.4306640625
Configure the search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
algorithm_name
|
str
|
|
'hist'
|
algorithm_params
|
dict[str, Any] | None
|
passed through to the algorithm. |
None
|
labels
|
tuple[Any, Any] | None
|
your two class labels, negative first, if they are not -1 and 1 -
for example |
None
|
verbose
|
bool
|
log what is being aggregated. |
False
|
verbosity
|
str | None
|
how much this instance reports - |
None
|
Note
No progress bar is offered, deliberately. The counting happens on executors,
where there is nobody watching a terminal, and the driver's share of the work
is a sweep over a few thousand bins - see thresher.progress.
Raises:
| Type | Description |
|---|---|
UnknownAlgorithmError
|
if the name matches no algorithm at all. |
ConfigurationError
|
if it names an algorithm that cannot run as an
aggregation, or if |
BackendDependencyError
|
if PySpark is not installed. |
Source code in src/thresher/spark.py
optimize_threshold ¶
Find the threshold that classifies the most rows correctly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
the DataFrame holding the scores and their true classes. It is read once or twice depending on the algorithm, and never collected. |
required |
score_col
|
str
|
name of the column holding the scores. |
'score'
|
label_col
|
str
|
name of the column holding the ground-truth classes. |
'label'
|
Returns:
| Type | Description |
|---|---|
float
|
The threshold, computed identically to what the in-memory algorithm would |
float
|
return for the same data. |
Raises:
| Type | Description |
|---|---|
EmptyInputError
|
if the DataFrame has no rows. |
UndefinedScoresError
|
if any score is null or NaN. |
MissingLabelsError
|
if any label is null. |
UnexpectedLabelsError
|
if any label is neither of the two declared classes. |
SingleClassError
|
if only one class is present, leaving nothing to separate. |
InsufficientDataError
|
if the aggregation came back empty. |
Note
Those refusals are the same ones the in-memory path makes, deliberately. Until 0.7.1 none of them existed here: a row whose label matched neither class - a null, a third value, a typo - was counted as a negative simply because it was not a positive, and a null or NaN score was quietly filed in the top bin. Both returned a plausible threshold computed from data the in-memory path would have refused outright.
Source code in src/thresher/spark.py
The module¶
thresher.spark ¶
Find a threshold over a Spark DataFrame, without bringing the data to the driver.
The other entry points take a sequence in memory. That is the wrong shape for data living in HDFS or S3: collecting a billion rows to one machine to sort them defeats the point of having a cluster.
This does the counting where the data already is. Spark performs one aggregation - a
groupBy and a pair of sums - and returns a summary bounded by the resolution rather
than by the row count. The driver then runs exactly the same sweep the in-memory
algorithms use, over that summary.
a billion rows -> [Spark: group and count] -> ~1,024 rows -> [driver: sweep]
Two algorithms fit that shape:
hist
Groups by bin index, so the summary is no_of_bins rows however large the input.
Nothing else here is bounded that way, which makes it the one to reach for.
exact
Groups by distinct score, so the summary is one row per distinct score. Exact, and
fine when scores are rounded probabilities; a warning is logged when the distinct
count is large enough for the collect to be the expensive part.
The rest are not offered, for the same reason the Ray backend does not distribute them: a
backend may change where the work happens, never the answer. ls is O(n²) in candidates,
and sgrid, gen and sgd each draw their own random subsamples, so sharding would
change which samples are read and therefore the result.
PySpark is an optional dependency: pip install 'thresher-py[spark]'. It also needs a JVM,
which pip cannot install for you.
The sweeps it reuses¶
The deciding half of each supported algorithm is a plain function over counts, which is what makes running it on a summary from the cluster identical to running it in memory.
thresher.algs.histogram.compute.sweep_bins ¶
sweep_bins(
negatives: Sequence[int],
positives: Sequence[int],
lowest: float,
highest: float,
progress_bar: bool = False,
) -> tuple[float, int]
Find the best threshold from binned class counts.
The deciding half of the algorithm, kept separate from the counting half. It needs only the per-bin totals and the range they cover, which is a summary whose size is the bin count and nothing else - so whatever produced those counts, a single pass here or an aggregation across a cluster, this function makes the decision and the answers agree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
negatives
|
Sequence[int]
|
count of negative samples in each bin, lowest bin first. |
required |
positives
|
Sequence[int]
|
count of positive samples in each bin, in the same order. |
required |
lowest
|
float
|
the smallest score the bins cover. |
required |
highest
|
float
|
the largest. The two are taken rather than a span because |
required |
progress_bar
|
bool
|
draw a progress bar on stderr while sweeping. |
False
|
Returns:
| Type | Description |
|---|---|
float
|
The best threshold expressible on a bin edge, and how many samples it classifies |
int
|
correctly - a count the threshold is guaranteed to achieve. |
Raises:
| Type | Description |
|---|---|
InsufficientDataError
|
if there are no bins to sweep. |
Source code in src/thresher/algs/histogram/compute.py
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 | |
thresher.algs.exact.compute.sweep_class_counts ¶
sweep_class_counts(
counts: Mapping[float, tuple[int, int]],
progress_bar: bool = False,
) -> tuple[float, int]
Find the best threshold from class counts, without seeing the samples.
The whole of the exact search lives here. It takes only "how many of each class sit at each distinct score", which is a summary bounded by the number of distinct scores rather than by the number of rows - so whatever produced those counts, in this process or across a cluster, the decision is made by this one function and the answers agree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
counts
|
Mapping[float, tuple[int, int]]
|
distinct score mapped to its |
required |
progress_bar
|
bool
|
draw a progress bar on stderr while sweeping. |
False
|
Returns:
| Type | Description |
|---|---|
tuple[float, int]
|
The best threshold and how many samples it classifies correctly. |
Raises:
| Type | Description |
|---|---|
InsufficientDataError
|
if there are no counts to sweep. |