Skip to content

Commit a455b00

Browse files
committed
Documentation accuracy pass: 83 docstring fixes, Sphinx repairs, runnable examples
An audit compared every constructor signature against its numpydoc block, attempted all 72 example scripts, and built the Sphinx site. No runtime behavior changes: an executable-AST comparison confirms the 43 touched model modules differ only in docstrings. Docstrings (83 fixes across 43 modules): - Removed seven documented constructor parameters that do not exist and raise TypeError if passed: GMM.verbose, GMM.verbose_interval, RGraph.active_support_params, RGraph.random_state, SUOD.cost_forecast_loc_fit/_pred, and LUNAR.n_neighbors (the real keyword is the British spelling n_neighbours; the entry is renamed with a note). - Corrected documented defaults that disagreed with the signature, among them ABOD.n_neighbors 10->5, ALAD.epochs 500->200, ALAD.preprocessing True->False, AnoGAN.learning_rate_query 0.001->0.01, RGraph.transition_steps 20->10, and DIF.hidden_neurons, documented as [64,32] while the constructor substitutes [500,100]. - Documented public parameters that had no entry, and corrected labels_, typed as int in BaseDetector and every detector that copied the wording when it is a numpy array, plus XGBOD.labels_, which claimed threshold_ is applied to decision_scores_ when fit() never sets threshold_. - Disclosed CBLOF.n_jobs as accepted-but-unused (see #713). Sphinx: - Enabled sphinx.ext.napoleon, absent since numpydoc style was adopted, so every Parameters/Attributes heading was parsed as an RST section title. A full build goes from 310 warnings and 242 class="problematic" spans to 41 and 5, and the :attr: links for decision_scores_ and labels_ resolve for the first time. - Enabling it also exposed five docstrings the parser could not read, inert text until now: AnoGAN emitted 1 of 16 parameters, RGraph 5 of 16, DIF 30 fields for an 11-parameter constructor, XGBOD turned a commented-out block into four bogus parameters, and so_gaal_new.SO_GAAL had an entirely empty Parameters section. All five repaired; a sweep over all 62 detectors now parses every documented parameter with no bogus or missing entries. - Removed the dead pyod.models.auto_encoder_torch section (module deleted in 2024; a duplicated :exclude-members: made the directive raise DuplicateOptionError, which Sphinx stripped, so the page rendered a bare heading) and de-duplicated the pyod.models.base automodule. Entry points: - The README quick start called clf.fit(X_train) without defining X_train and used visualize without importing it, so the block on the GitHub landing page and the PyPI description raised NameError when pasted. - docs/install.rst documented a pytorch extra that does not exist (pip treats an unknown extra as a warning, so the command succeeded while installing none of the PyTorch stack), omitted nine real extras and pyod[all], claimed the MCP server registers seven tools when it registers ten, and advertised Claude Desktop support for a command no code path targets. Examples: fixed runtime failures in mad_example.py (two features for a univariate-only detector) and qmcd_example.py (appended ground-truth labels to the feature matrix before predict, raising and leaking test labels). mat_file_conversion.py now byte-compiles after removal of mid-file Python 2 __future__ imports. Deferred to separate issues, since each needs a runtime decision: #713 (CBLOF.n_jobs), #714 (DevNet unused parameters including random_state), #715 (pyod info infers Claude Code from a directory it creates itself). Full suite: 1532 passed, 0 failed. Reviewed via /implement-review (Codex round 6), which caught that enabling Napoleon had silently broken four docstrings -- a failure mode that produces no "problematic" markup and so was invisible to the metric used to validate the change.
1 parent 4b4fbbb commit a455b00

54 files changed

Lines changed: 285 additions & 178 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,4 @@ v<3.6.0>, <06/04/2026> -- Add audio as a first-class anomaly detection modality.
232232
v<3.6.1>, <06/16/2026> -- Maintenance and contributor PRs since v3.6.0 (no breaking API changes). PyThresh v1 support (#684, Daniel Kulik): the pyod.models.thresholds wrappers and the BaseDetector threshold path use the pythresh v1 API (.fit()/.labels_/.predict() instead of .eval()), and the dependency pin moves to pythresh>=1.0.0. EmbeddingOD air-gapped and pre-instantiated encoder support (#696, Sunny Guntuka): a pre-loaded SentenceTransformer instance can be passed directly as the encoder, and a local filesystem path is loaded with local_files_only=True (no Hub call) for offline use; this also fixes a resolver-order bug where a SentenceTransformer instance was wrapped as a CallableEncoder (calling model(X) instead of model.encode(X)). DataFrame feature-name warning fix (#692, eferhire ugbotu; closes #540): GMM, IForest, LOF, and OCSVM run check_array in decision_function, so scoring a pandas DataFrame after fitting no longer emits the scikit-learn feature-name UserWarning; the predict/predict_proba/predict_confidence paths route through decision_function, so the single-point fix covers them. Audio docs: AudioAE and the audio modality are now in the README and docs algorithm tables with a new pyod.models.audio API page (v3.6.0 shipped AudioAE without a table row). Tests: regression tests for the DataFrame-warning fix (GMM/OCSVM/LOF) and a no-download EmbeddingOD resolver test using SentenceTransformer(modules=[]). Docs: PyPI download figure refreshed to 46M+. Reviewed via /implement-review (Codex gatekeeper): #696 and #692 merged after dual review, with the gatekeeper-flagged test gaps closed in a follow-up. Buildable detector count unchanged at 61.
233233
v<3.6.2>, <07/20/2026> -- Security hardening for model persistence (CVE-2026-15529): both `pyod.utils.persistence.load()` and `pyod.utils.persistence.compat_load()` now refuse to deserialize pickle/joblib artifacts unless callers pass `trusted=True`, making the trust boundary explicit before `joblib.load()` can execute pickle reducers. This is a breaking change: existing `save()` then `load()` round trips, and any direct `compat_load()` calls, must add `trusted=True`, because both functions now refuse even artifacts you saved yourself until the caller acknowledges the source is trusted. The internal `load()` fall-through to `compat_load()` forwards the acknowledgement automatically, so `load(path, trusted=True)` still recovers legacy dtype-mismatched artifacts in a single call. `strict=True` remains a dependency-version policy and is documented as not making untrusted pickle files safe. Adds regression tests verifying that both the default `load()` path and a direct `compat_load()` call reject before any unpickling side effects occur. Updates the model persistence guide, decision tree, troubleshooting table, and save/load example to pass `trusted=True` for trusted artifacts. Closes #697.
234234
v<3.6.3>, <08/01/2026> -- DeepSVDD correctness fixes, contributor bug fixes, and project infrastructure. DeepSVDD hypersphere-collapse fix (builds on #704 by Devashish Moghe; closes #606 and #641): the training loop had `loss.backward()` commented out, so no gradient ever reached the weights and scores came from the randomly initialized network. #704 restored the backward pass, recomputing the L2 term `w_d` inside the batch loop so each step gets a fresh graph, moving the best-model bookkeeping out of the batch loop, and deep-copying `state_dict()` (which otherwise aliases the live parameters). Enabling training then exposed three pre-existing defects that the commented-out line had been masking. First, `fit()` assigned the estimator's center `self.c = 0.0` and let `_init_c()` write the computed center to the inner module, which the loss and scoring never read; for a bias-free ReLU network the all-zero weights map every input to 0, so `c = 0` is exactly the trivial solution of Ruff et al., ICML 2018, Proposition 1, and training converged to a collapsed hypersphere. On a 17-dataset ODDS benchmark (3 seeds, 30 epochs) the collapsed configuration returned a single distinct score on 10 of 17 datasets -- a detector that is silently useless while still meeting a ROC floor, because ordering can be induced by floating-point noise as small as 1e-24. The center is now taken from `_init_c()`, detached, and stored where the objective reads it. Second, the optimizer was constructed without a learning rate (Adam's default 1e-3) and passed `l2_regularizer=0.1` as `weight_decay`, five orders of magnitude above the 5e-7 used by the reference implementation; that decay drives the weights toward zero and compounds the collapse. `l2_regularizer` now defaults to 5e-7 and a new `learning_rate` parameter defaults to 1e-4 (both match the reference implementation; the `l2_regularizer` default change is API-visible). Third, `best_model_dict` was stored but never loaded, so scoring used final-epoch weights; the snapshot is now applied before scoring. Fourth, `fit()` wrote the computed center into the constructor parameter `c`, which leaked fitted state through `get_params()`, made `clone()` start pre-seeded, and made a refit train a newly built network against the previous fit's center; the fitted center now lives in `c_`, `c` stays configuration, and a user-supplied center is validated before training: a scalar is expanded to the network output width, a vector must match that width (`hidden_neurons[-1]`, or `n_features` when `use_ae=True`), non-finite values are rejected, an all-zero center is rejected with a `ValueError` because it is exactly the Proposition 1 trivial-solution condition, and the tensor is copied so a later mutation of the caller's array cannot change the fitted center. Measured effect on the 17-dataset benchmark: mean ROC AUC rises from 0.601 (collapsed) to 0.748, with score collapse eliminated on all 17 datasets. Calibration note: 0.748 is level with the 0.746 obtained by the untrained network, so these changes restore DeepSVDD to its baseline rather than improving on it. Deep SVDD assumes a clean one-class training set, while PyOD fits it on contaminated data unsupervised; in a controlled comparison, training on genuinely normal samples only reaches 0.879 and trimming the most anomalous 10% before training reaches 0.800, so a substantial part of the gap tracks the mismatch between the method's one-class assumption and unsupervised use on contaminated data. These two diagnostics do not rule out further defects or tuning gains. Tests: `test_scores_are_not_constant` asserts score diversity on both the standard and autoencoder paths and `test_center_is_valid` asserts the center is a detached, finite, non-scalar tensor; both fail on the collapsed code. `test_fit_changes_parameters` pins the original backward-pass defect only, and does not detect collapse. The LOCI and generate_data contributor PRs remain under review and are not included here. Contributor PRs: local encoder paths now fall back to the HuggingFace backend when sentence-transformers is absent (#701, Sunny Guntuka; follow-up to #696), so a `pyod[huggingface]`-only install no longer raises `ImportError` before the existing fallback can run; save/load round-trip coverage now spans 23 detectors (#708, Jayesh Suryavanshi; closes #269), up from 2, while 21 of them also gain clone assertions for unfitted equivalence and refit score reproduction, strengthening per-detector `test_model_clone` methods that previously only checked that `clone()` did not raise. Test fix (#710): `test_resolve_st_instance_no_download` built its no-download model with `SentenceTransformer(modules=[])`, which sentence-transformers 5.6 rejects; it now passes a single trivial `torch.nn.Identity` module instead, which keeps the test network-free without depending on `sentence_transformers.models` keyword arguments that have been renamed across 5.x. Project infrastructure (#705): pyod.dev website badge in the README (using an absolute image URL so it renders on the PyPI project page), brand assets under `brand/` plus Sphinx `docs/_static/` copies, and a `SECURITY.md` vulnerability-reporting policy.
235+
v<3.6.4>, <TBD> -- Documentation accuracy pass; no runtime behavior changes. A parallel audit compared every constructor signature against its numpydoc block, attempted all 72 example scripts, and built the Sphinx site; the in-scope findings were then repaired and checked again. Docstrings (83 fixes across 43 modules in `pyod/models/`): removed seven documented constructor parameters that do not exist and raise `TypeError` when passed (`GMM.verbose`, `GMM.verbose_interval`, `RGraph.active_support_params`, `RGraph.random_state`, `SUOD.cost_forecast_loc_fit`, `SUOD.cost_forecast_loc_pred`, and `LUNAR.n_neighbors`, whose real keyword is the British spelling `n_neighbours`; the entry is renamed and notes the alternative spelling); corrected documented defaults that disagreed with the signature (among them `ABOD.n_neighbors` 10 -> 5, `ALAD.epochs` 500 -> 200, `ALAD.preprocessing` True -> False, `ALAD.learning_rate_gen`/`learning_rate_disc` 0.001 -> 0.0001, `AnoGAN.learning_rate_query` 0.001 -> 0.01, `RGraph.transition_steps` 20 -> 10, `RGraph.n_nonzero` 50 -> 10, `SUOD.n_jobs` 1 -> None, and `DIF.hidden_neurons`, documented as [64, 32] while the constructor substitutes [500, 100] for the `None` default); documented public constructor parameters that had no entry (`ALAD.latent_dim`, `add_disc_zz_loss`, `spectral_normalization`; `AnoGAN.latent_dim_G`, `device`); corrected `labels_` in `BaseDetector` and every detector that copied the wording, which described an `int` where the attribute is a numpy array of shape (n_samples,); and corrected `XGBOD.labels_`, documented as `threshold_` applied to `decision_scores_` when `fit()` never sets `threshold_` and the labels come from the fitted XGBoost classifier. An executable-AST comparison against the previous commit confirms every one of those 43 files changed only docstrings. Sphinx (`docs/conf.py`, `docs/pyod.models.tabular.rst`): enabled `sphinx.ext.napoleon`, absent since the numpydoc style was adopted, so every `Parameters`/`Attributes`/`Examples` heading was parsed as an RST section title rather than a field list; a full build goes from 310 warnings and 242 `class="problematic"` spans to 41 and 5, and the `:attr:` links for `decision_scores_` and `labels_` on the landing page and API cheat sheet resolve for the first time. Removed the `pyod.models.auto_encoder_torch` section, dead since the module was deleted in 2024 (a duplicated `:exclude-members:` option made the directive raise `DuplicateOptionError`, which Sphinx stripped from the output, so the page silently rendered a heading with no body), and de-duplicated the `pyod.models.base` automodule so `api_cc.rst` is its canonical home, clearing 13 duplicate-object warnings. Enabling Napoleon also exposed five docstrings it could not parse, which had been inert text before: `AnoGAN` emitted 1 of 16 parameters and `RGraph` 5 of 16 (consecutive blank lines and `name:` headers missing the space before the colon terminated the block), `DIF` emitted 30 fields for an 11-parameter constructor (comma-separated headers such as `hidden_neurons, list` split into bogus names), `XGBOD` turned a commented-out `missing` block into four bogus parameters, and the `SO_GAAL` in `so_gaal_new.py` had an entirely empty `Parameters` section for its 12 arguments. All five are repaired; a sweep over all 62 detectors now parses every documented parameter with no bogus or missing entries. Entry points: the README quick start called `clf.fit(X_train)` without ever defining `X_train` and used `visualize` without importing it, so the block on the GitHub landing page and the PyPI description raised `NameError` when pasted; it now carries the seeded prelude and the sample output measured from it. `docs/install.rst` documented a `pytorch` extra that does not exist (pip treats an unknown extra as a warning, so the command succeeded while installing none of the PyTorch stack), omitted nine real extras and the `audio` extra entirely, and never mentioned `pip install pyod[all]`, a string that appeared nowhere in the repository; the table is now keyed on the extras defined in `pyproject.toml`. The same file claimed the MCP server registers seven tools when it registers ten, omitting the three that perform detection, and contradicted `README.rst`. Removed the claim that `pyod install skill` supports Claude Desktop, which no code path targets, and routed Desktop users to the MCP path. Examples: fixed runtime failures in `mad_example.py`, which generated two features for a detector that requires one, and `qmcd_example.py`, which appended ground-truth labels to the feature matrix before calling `predict` (raising, and leaking test labels). `examples/data/mat_file_conversion.py` now byte-compiles after removal of mid-file Python 2 `__future__` imports; end-to-end conversion still requires its optional dependencies and external source datasets. Flagged for a separate decision, not changed here: `CBLOF.n_jobs` is accepted but unused, `DevNet` has no constructor docstring and exposes arguments that are unused or overridden, and `pyod/cli.py` infers "Claude Code detected" from a directory that `pyod install skill` itself creates.

README.rst

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ Then pick the activation path that matches your agent stack:
9090

9191
.. code-block:: bash
9292
93-
# 1. Claude Code / Claude Desktop / Codex — enables the od-expert skill
94-
pyod install skill # Claude Code / Desktop: user-global (~/.claude/skills/)
93+
# 1. Claude Code / Codex — enables the od-expert skill
94+
pyod install skill # Claude Code: user-global (~/.claude/skills/)
9595
pyod install skill --project # Codex: project-local (./skills/, Codex has no user-global dir)
9696
9797
# 2. Any MCP-compatible LLM — requires the optional mcp extra
@@ -730,6 +730,16 @@ More detailed instructions for running examples can be found in `examples direct
730730
731731
732732
from pyod.models.knn import KNN # kNN detector
733+
from pyod.utils.data import generate_data
734+
735+
contamination = 0.1 # percentage of outliers
736+
n_train = 200 # number of training points
737+
n_test = 100 # number of testing points
738+
739+
# generate sample data
740+
X_train, X_test, y_train, y_test = generate_data(
741+
n_train=n_train, n_test=n_test, n_features=2,
742+
contamination=contamination, random_state=42)
733743
734744
# train kNN detector
735745
clf_name = 'KNN'
@@ -767,14 +777,16 @@ More detailed instructions for running examples can be found in `examples direct
767777
768778
769779
On Training Data:
770-
KNN ROC:1.0, precision @ rank n:1.0
780+
KNN ROC:0.9992, precision @ rank n:0.95
771781
772782
On Test Data:
773-
KNN ROC:0.9989, precision @ rank n:0.9
783+
KNN ROC:1.0, precision @ rank n:1.0
774784
775785
.. code-block:: python
776786
777787
788+
from pyod.utils.example import visualize
789+
778790
visualize(clf_name, X_train, y_train, X_test, y_test, y_train_pred,
779791
y_test_pred, show_figure=True, save_figure=False)
780792

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
# ones.
4545
extensions = [
4646
'sphinx.ext.autodoc',
47+
'sphinx.ext.napoleon',
4748
'sphinx.ext.doctest',
4849
'sphinx.ext.intersphinx',
4950
'sphinx.ext.coverage',

docs/examples/agentic.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ Activation Paths
5757

5858
PyOD 3 reaches agents through three paths. Pick whichever matches your stack:
5959

60-
**Claude Code / Claude Desktop / Codex**
60+
**Claude Code / Codex**
6161
The ``od-expert`` skill ships as package data inside the pyod wheel.
6262
Two install modes are supported:
6363

6464
.. code-block:: bash
6565
6666
pip install pyod
6767
68-
# Claude Code / Claude Desktop: user-global install
68+
# Claude Code: user-global install
6969
pyod install skill # installs to ~/.claude/skills/od-expert/
7070
7171
# Codex: project-local install (Codex reads ./skills/, has no user-global dir)

docs/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ Then pick the activation path that matches your agent stack:
9090

9191
.. code-block:: bash
9292
93-
# 1. Claude Code / Claude Desktop / Codex — enables the od-expert skill
94-
pyod install skill # Claude Code / Desktop: user-global (~/.claude/skills/)
93+
# 1. Claude Code / Codex — enables the od-expert skill
94+
pyod install skill # Claude Code: user-global (~/.claude/skills/)
9595
pyod install skill --project # Codex: project-local (./skills/, Codex has no user-global dir)
9696
9797
# 2. Any MCP-compatible LLM — requires the optional mcp extra

0 commit comments

Comments
 (0)