AnomalyDetector¶
Unsupervised anomaly detection over PyOD — an auto mode that profiles the data
and runs a consensus of the best-fit detectors, or a single named detector from
a curated roster. See the unsupervised guide.
In short
AnomalyConfig.modepicks the path:"auto"(default) or"single".autoruns up to 3 detectors from PyOD's routing table and serves their z-score mean;singlefits the one detector named bydetector_name(default"iforest").contamination(default0.05) sets the share of rows labelled anomalous.to_pipeline()returns the deployableUnsupervisedPipeline.
AnomalyDetector ¶
AnomalyDetector(
config: Optional[AnomalyConfig] = None,
unsupervised_config: Optional[
UnsupervisedConfig
] = None,
preprocessor: Optional[
UnsupervisedPreprocessor
] = DEFAULT_PREPROCESSOR,
*,
outputs_dir: Optional[str] = None,
setup_file_logger: bool = True
)
Bases: BaseEstimator
Unsupervised anomaly detector.
See the class-module docstring for the design.
Parameters¶
config : AnomalyConfig, optional
Task configuration. Defaults to AnomalyConfig().
unsupervised_config : UnsupervisedConfig, optional
Preprocessor + embedding configuration. Defaults to
UnsupervisedConfig().
preprocessor : UnsupervisedPreprocessor, optional
Override the default preprocessor (pass None to skip
preprocessing entirely if your input is already a numeric
matrix).
outputs_dir : str, optional, keyword-only
When provided, opt into the outputs_dir discipline: fit()
writes <outputs_dir>/{events.ndjson, manifest.json, CANCEL,
ml4.log} and persists the deployable
:class:UnsupervisedPipeline to <outputs_dir>/deploy/
under :data:~parleml_ml4.run_layout.UNSUPERVISED_ARTEFACT_NAME. When
None (default), the fit runs in memory with no telemetry and no
artefact.
setup_file_logger : bool, keyword-only, default True
When True and outputs_dir is provided and the root
logger has no handlers, install a rotating file handler at
<outputs_dir>/ml4.log. Mirrors
:class:AutoSupervised's gating.
DEPLOY_ARTIFACT_NAME
class-attribute
instance-attribute
¶
Filename inside <outputs_dir>/deploy/ that the saved
:class:UnsupervisedPipeline lands in.
Deliberately the same name the platform's enriched re-save uses — see
:data:parleml_ml4.run_layout.UNSUPERVISED_ARTEFACT_NAME for why one deploy/
directory must not end up holding two same-versioned artefacts that
disagree.
outputs_dir
property
¶
The outputs_dir passed to the constructor, or None.
operating_threshold
property
¶
The active operating-threshold override, or None.
None means :meth:predict / :meth:explain_rows use the detector's
own fit-time threshold_.
fit ¶
Fit the detector.
Wraps the logical stages (preprocess /
fit_single or fit_auto / build_result, plus
persist when outputs_dir was provided) with
stage_* events. When outputs_dir is None the runtime
is identical to the legacy synchronous path: no telemetry, no
on-disk artefact.
set_operating_threshold ¶
Override the operating cutoff used by :meth:predict / :meth:explain_rows.
When set, labels are derived as decision_function(X) >= value
(higher == more anomalous — see :meth:_anomaly_mask), bypassing the
fit-time threshold_. Pass None to revert to the fitted threshold.
Persisted with the detector via joblib.
predict ¶
Anomaly labels (0/1) for X — the novelty labelling.
Answers "are these rows anomalous relative to what I was fitted on?",
which is the right question for serving new data and the one the
deployable :class:~parleml_ml4.unsupervised.pipeline.UnsupervisedPipeline
asks. Passing the training frame back in is a different question
from the one result_.labels answered, and for LOF / KNN the two
legitimately disagree on those rows — see
:class:~parleml_ml4.unsupervised.result.AnomalyResult. Not a bug, and not
something :meth:set_operating_threshold reconciles.
decision_function ¶
Raw anomaly scores for X - higher is more anomalous.
normalized_score ¶
Min-max normalised anomaly score in [0, 1] - not a probability.
PyOD's predict_proba(X, method='linear'), whose entire body is::
scaler = MinMaxScaler().fit(train_scores.reshape(-1, 1))
probs[:, 1] = scaler.transform(test_scores...).ravel().clip(0, 1)
probs[:, 0] = 1 - probs[:, 1]
- an affine rescale of an uncalibrated score by the two most extreme
training rows (
pyod/models/base.py). It is not calibrated against any observed frequency, it is not comparable across detectors, it moves if one outlier is added to the training set, and any served row beyond the training range is clipped to exactly 0 or 1 — so 0.87 is not an "87 % chance this row is an anomaly", and the name says so. PyOD's alternativemethod='unify'is not offered: it assumes Gaussian scores, which no detector here guarantees.
:returns: (n_samples, 2) of [1 - s, s] where s is the
normalised score - shape preserved so a caller reading column 1
keeps working; the meaning is a rank surrogate, not a class
posterior.
:raises NotImplementedError: for detectors that expose no such rescale.
explain_rows ¶
Per-row reason codes for arbitrary (out-of-sample) rows.
The serving twin of the training findings, through the same
:mod:~parleml_ml4.unsupervised.anomaly.findings producer: each entry carries
the anomaly label / score plus the top contributing features,
z-scored against the training feature statistics so attributions are
stable across calls and comparable with the fitted run's.
The same caveats apply and are stated in the narrative: the z-scores are
computed on the engineered matrix (RobustScaler units, one-hot
axes), each contribution names its source_column, and below
:data:~parleml_ml4.unsupervised.anomaly.findings.MATERIAL_Z the narrative says
that no feature deviates materially instead of naming the three largest
of a set of near-zero numbers.
contamination_diagnostics ¶
contamination_diagnostics(
threshold_sweep: Optional[Sequence[float]] = None,
) -> dict[float, dict[str, Any]]
Read-only threshold sweep over the served scores.
Synthesised from result_.scores in both modes, so the curve is
derived from the very score vector whose labels it is drawn beside, and
every threshold it reports is in the units of
result_.decision_threshold (and of :meth:set_operating_threshold).
ADEngine's own contamination_diagnostics sweeps its rank-normalised
consensus instead, a different model in different units, so it is not used.
The sweep must reproduce PyOD's own labelling rule,
because the curve is read next to result_.n_anomalies: PyOD sets
threshold_ = percentile(decision_scores_, 100 * (1 - c)) and then
labels_ = decision_scores_ > threshold_ (pyod/models/base.py).
np.quantile(scores, 1 - c) is the identical cut point (PyOD imports
NumPy's own percentile), so the only thing that has to match is
the comparison — and it is strict. A non-strict >= over-counted
by exactly the number of scores sitting on the cut, making the sweep
contradict the result it annotates: on this repo's 220-row anomaly
fixture, single-mode LOF at c=0.1 reported 25 flagged while
result_.labels held 16. Ties are not a synthetic-data curiosity —
integer / low-cardinality columns and rank- or histogram-based
detectors produce them routinely.
:param threshold_sweep: Optional iterable of contamination
fractions in (0, 1). Defaults to
:data:~parleml_ml4.unsupervised.result.DEFAULT_CONTAMINATION_GRID — the
same 40-point 0.005 .. 0.20 grid mli's plot fallback sweeps, so the
two agree by construction rather than by coincidence.
:returns: {contamination: {"n_anomalies": int,
"threshold": float, "anomaly_ratio": float}} for every
contamination probed, with threshold in the units of
result_.scores in both modes. n_anomalies equals result_.labels.sum() at
the contamination the fitted detector was actually thresholded at —
config.contamination, which now reaches the detector in auto
too (it is written into ADEngine's plans by
:func:apply_contamination_to_plans).
engine_report ¶
Return ADEngine.report(..., format='text') when this
detector ran in mode='auto'; None otherwise.
Public accessor so callers (the pipeline's MLflow logger, the
viewer panel) don't have to reach into self._engine /
self._state.
explain ¶
Return a dict summarising what was served + why.
Carries score_degeneracy when the fit produced no usable separation
and caveats whenever the numbers need qualifying, because
this dict is the natural place a caller asks "is this run worth
reading?" and the honest answer is sometimes "yes, but".
In mode='auto' it also carries scoring_rule - how the served
z-score mean combines every candidate rather than picking one - and the
per-detector rows carry the router's own reason /
routing_confidence / evidence. engine_report is ADEngine's
own text, whose detector section is the candidate ADEngine would pick,
not the served z-score mean.
to_pipeline ¶
to_pipeline(
*,
compute_embedding: bool = False,
X_for_embedding: Optional[Any] = None,
metadata: Optional[dict[str, Any]] = None
) -> Any
Wrap this fitted detector into a self-contained
:class:parleml_ml4.unsupervised.UnsupervisedPipeline.
Sugar over UnsupervisedPipeline.from_anomaly_detector(self).