Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1c691cf
FIX: Prevent false-positive Claude Code detection in pyod info (#715)
busysofa15 Aug 6, 2026
6635e76
test: mock shutil.which in tests for hermetic execution
busysofa15 Aug 6, 2026
ad39845
Merge pull request #723 from busysofa15/fix-claude-detection-715
yzhao062 Aug 17, 2026
dc9fef9
Merge approved master configuration updates into development
yzhao062 Sep 16, 2026
8313009
Respect verbose in DeepSVDD training output (#735)
VenishPaneliya Sep 16, 2026
c8af992
Drop the sample_weight entry from GMM.fit's docstring (#741)
VenishPaneliya Sep 16, 2026
6eef29a
docs(utility): correct score_to_label return description (#733)
godarrenw Sep 16, 2026
4958473
docs: fix dead reference links in KNN/LOF/LUNAR docstrings (#732)
godarrenw Sep 16, 2026
109bdfd
docs: clarify ECOD/COPOD relationship in docstrings (#655) (#725)
dishasharma23-prog Sep 16, 2026
19fa982
Return self from BaseDeepLearningDetector.fit (#740)
VenishPaneliya Sep 16, 2026
fc5bbf2
Keep logvar_clip as given so VAE can be cloned (#738)
VenishPaneliya Sep 16, 2026
59f195b
Stop Sampling.fit from overwriting subset_size (#737)
VenishPaneliya Sep 16, 2026
033b964
Forward n_components in PyODKernelPCA (#730)
VenishPaneliya Sep 16, 2026
8d2eae3
fix: support cosine distance in KNN (#729)
Iams4kura Sep 16, 2026
e357c5b
Make LOCI honor contamination and keep k as a parameter (#707)
Mohit-Ak Sep 16, 2026
87c9499
Iterate over rows in the unweighted branch of pearsonr_mat (#731)
VenishPaneliya Sep 16, 2026
97fa2b4
Prepare v3.6.6 maintenance release
yzhao062 Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.txt

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyod/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from __future__ import annotations

import argparse
import shutil
import importlib.util
import sys
from collections import Counter
Expand Down Expand Up @@ -101,7 +102,7 @@ def _cmd_info(args: argparse.Namespace) -> int:
user_installed = user_skill_path.is_file()
project_installed = project_skill_path.is_file()
agents_detected: list[str] = []
if claude_dir.is_dir():
if shutil.which("claude") is not None or (Path.home() / ".claude.json").exists():
agents_detected.append("Claude Code")
if codex_dir.is_dir():
agents_detected.append("Codex")
Expand Down
7 changes: 7 additions & 0 deletions pyod/models/base_dl.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ def fit(self, X, y=None):

y : numpy array of shape (n_samples,), optional (default=None)
The ground truth of input samples. Not used in unsupervised methods.

Returns
-------
self : object
Fitted estimator.
"""
# validate inputs X and y (optional)
X = check_array(X)
Expand Down Expand Up @@ -202,6 +207,8 @@ def fit(self, X, y=None):
self.decision_scores_ = self.decision_function(X)
self._process_decision_scores()

return self

def training_prepare(self):
self.model = self.model.to(self.device)

Expand Down
12 changes: 10 additions & 2 deletions pyod/models/copod.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,19 @@ def _parallel_ecdf(n_dims, X):


class COPOD(BaseDetector):
"""COPOD class for Copula Based Outlier Detector.
COPOD is a parameter-free, highly interpretable outlier detection algorithm
"""COPOD is a parameter-free, highly interpretable outlier detection algorithm
based on empirical copula models.
See :cite:`li2020copod` for details.

.. note::
COPOD and :class:`~pyod.models.ecod.ECOD` are closely related: ECOD is
an extended, renamed formulation of COPOD, developed by the same
authors with a more refined mathematical presentation (see the
discussion in `GitHub issue #655
<https://github.com/yzhao062/pyod/issues/655>`_). We generally
recommend using ECOD, which is better documented and explained.
COPOD is retained for backward compatibility and availability.

Parameters
----------
contamination : float in (0., 0.5), optional (default=0.1)
Expand Down
3 changes: 2 additions & 1 deletion pyod/models/deep_svdd.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,8 @@ def fit(self, X, y=None):
# place and leave the final weights here instead of the best
# ones. Copy it.
best_model_dict = copy.deepcopy(self.model_.state_dict())
print(f"Epoch {epoch + 1}/{self.epochs}, Loss: {epoch_loss}")
if self.verbose >= 1:
print(f"Epoch {epoch + 1}/{self.epochs}, Loss: {epoch_loss}")
self.best_model_dict = best_model_dict
if self.best_model_dict is not None:
self.model_.load_state_dict(self.best_model_dict)
Expand Down
14 changes: 11 additions & 3 deletions pyod/models/ecod.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,20 @@ def _parallel_ecdf(n_dims, X):


class ECOD(BaseDetector):
"""ECOD class for Unsupervised Outlier Detection Using Empirical
Cumulative Distribution Functions (ECOD)
ECOD is a parameter-free, highly interpretable outlier detection algorithm
"""ECOD is a parameter-free, highly interpretable outlier detection algorithm
based on empirical CDF functions.
See :cite:`li2021ecod` for details.

.. note::
ECOD is an extended, renamed version of :class:`~pyod.models.copod.COPOD`,
developed by the same authors with a more refined presentation (see
the discussion in `GitHub issue #655
<https://github.com/yzhao062/pyod/issues/655>`_). ECOD is the
recommended choice of the two; COPOD is kept in the library mainly
for availability and backward compatibility.



Parameters
----------
contamination : float in (0., 0.5), optional (default=0.1)
Expand Down
4 changes: 0 additions & 4 deletions pyod/models/gmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,6 @@ def fit(self, X, y=None):
y : Ignored
Not used, present for API consistency by convention.

sample_weight : array-like, shape (n_samples,)
Per-sample weights. Rescale C per sample. Higher weights
force the classifier to put more emphasis on these points.

Returns
-------
self : object
Expand Down
18 changes: 5 additions & 13 deletions pyod/models/knn.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from warnings import warn

import numpy as np
from sklearn.neighbors import BallTree
from sklearn.neighbors import NearestNeighbors
from sklearn.utils import check_array
from sklearn.utils.validation import check_is_fitted
Expand Down Expand Up @@ -108,7 +107,7 @@ class KNN(BaseDetector):
sklearn.metrics.pairwise.pairwise_distances. When p = 1, this is
equivalent to using manhattan_distance (l1), and euclidean_distance
(l2) for p = 2. For arbitrary p, minkowski_distance (l_p) is used.
See http://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.pairwise_distances
See https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise_distances.html

metric_params : dict, optional (default = None)
Additional keyword arguments for the metric function.
Expand Down Expand Up @@ -189,20 +188,13 @@ def fit(self, X, y=None):

self.neigh_.fit(X)

# In certain cases, _tree does not exist for NearestNeighbors
# See Issue #158 (https://github.com/yzhao062/pyod/issues/158)
# n_neighbors = 100
# Brute-force backends do not expose an internal tree. Reuse the
# fitted estimator in that case instead of rebuilding a BallTree,
# which rejects otherwise supported metrics such as cosine.
if self.neigh_._tree is not None:
self.tree_ = self.neigh_._tree

else:
if self.metric_params is not None:
self.tree_ = BallTree(X, leaf_size=self.leaf_size,
metric=self.metric,
**self.metric_params)
else:
self.tree_ = BallTree(X, leaf_size=self.leaf_size,
metric=self.metric)
self.tree_ = self.neigh_

dist_arr, _ = self.neigh_.kneighbors(n_neighbors=self.n_neighbors,
return_distance=True)
Expand Down
1 change: 1 addition & 0 deletions pyod/models/kpca.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(
random_state=None,
):
super().__init__(
n_components=n_components,
kernel=kernel,
gamma=gamma,
degree=degree,
Expand Down
18 changes: 7 additions & 11 deletions pyod/models/loci.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ class LOCI(BaseDetector):
should be considered "local".

k: int, default = 3
An outlier cutoff threshold for determine whether or not a point
should be considered an outlier.
Sensitivity of the radius-scan early stop. Scanning stops when MDEF
exceeds k times sigma_MDEF. Binary labels use the threshold derived
from ``contamination``, not k.

Attributes
----------
Expand Down Expand Up @@ -140,13 +141,13 @@ class LOCI(BaseDetector):
... contamination=contamination, random_state=42)
>>> clf = LOCI()
>>> clf.fit(X_train)
LOCI(alpha=0.5, contamination=0.1, k=None)
LOCI(alpha=0.5, contamination=0.1, k=3)
"""

def __init__(self, contamination=0.1, alpha=0.5, k=3):
super(LOCI, self).__init__(contamination=contamination)
self.alpha = alpha
self.threshold_ = k
self.k = k

def _get_alpha_n(self, dist_matrix, indices, r):
"""Computes the alpha neighbourhood points.
Expand Down Expand Up @@ -208,7 +209,7 @@ def _calculate_decision_score(self, X):
sigma_mdef = np.std(n_values) / n_hat
if n_hat >= 20:
outlier_scores[p_ix] = mdef / sigma_mdef
if mdef > (self.threshold_ * sigma_mdef):
if mdef > (self.k * sigma_mdef):
break
return np.asarray(outlier_scores)

Expand All @@ -231,13 +232,8 @@ def fit(self, X, y=None):
X = check_array(X)
self._set_n_classes(y)
self.decision_scores_ = self._calculate_decision_score(X)
self.labels_ = (self.decision_scores_ > self.threshold_).astype(
'int').ravel()
self._process_decision_scores()

# calculate for predict_proba()

self._mu = np.mean(self.decision_scores_)
self._sigma = np.std(self.decision_scores_)
return self

def decision_function(self, X):
Expand Down
2 changes: 1 addition & 1 deletion pyod/models/lof.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class LOF(BaseDetector):
sklearn.metrics.pairwise.pairwise_distances. When p = 1, this is
equivalent to using manhattan_distance (l1), and euclidean_distance
(l2) for p = 2. For arbitrary p, minkowski_distance (l_p) is used.
See http://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.pairwise_distances
See https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise_distances.html

metric_params : dict, optional (default = None)
Additional keyword arguments for the metric function.
Expand Down
2 changes: 1 addition & 1 deletion pyod/models/lunar.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def forward(self, x):

class LUNAR(BaseDetector):
"""
LUNAR class for outlier detection. See https://www.aaai.org/AAAI22Papers/AAAI-51.GoodgeA.pdf for details.
LUNAR class for outlier detection. See https://ojs.aaai.org/index.php/AAAI/article/view/20629 for details.
For an observation, its ordered list of distances to its k nearest neighbours is input to a neural network,
with one of the following outputs:

Expand Down
11 changes: 8 additions & 3 deletions pyod/models/sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,18 @@ def fit(self, X, y=None):
)
if isinstance(self.subset_size, float) is True:
if 0.0 < self.subset_size <= 1.0:
self.subset_size = int(self.subset_size * n_samples)
subset_size = int(self.subset_size * n_samples)
else:
raise ValueError("subset_size=%r must be between 0.0 and 1.0")
raise ValueError(
"subset_size=%r must be between 0.0 and 1.0"
% self.subset_size
)
else:
subset_size = self.subset_size

random_indices = self.random_state.choice(
n_samples,
size=self.subset_size,
size=subset_size,
replace=False,
)
self.subset = X[random_indices, :]
Expand Down
11 changes: 8 additions & 3 deletions pyod/models/vae.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,14 @@ def __init__(self, contamination=0.1, preprocessing=True,
self.output_activation_name = output_activation_name
self.batch_norm = batch_norm
self.dropout_rate = dropout_rate
self.logvar_clip = self._validate_logvar_clip(logvar_clip)
# Validate eagerly, but keep the argument exactly as it was given:
# sklearn's clone() requires __init__ to store its parameters
# unmodified.
self._validate_logvar_clip(logvar_clip)
self.logvar_clip = logvar_clip

def build_model(self):
self.logvar_clip_ = self._validate_logvar_clip(self.logvar_clip)
self.model = VAEModel(self.feature_size,
encoder_neuron_list=self.encoder_neuron_list,
decoder_neuron_list=self.decoder_neuron_list,
Expand All @@ -262,7 +267,7 @@ def build_model(self):
output_activation_name=self.output_activation_name,
batch_norm=self.batch_norm,
dropout_rate=self.dropout_rate,
logvar_clip=self.logvar_clip)
logvar_clip=self.logvar_clip_)

def training_forward(self, batch_data):
x = batch_data
Expand All @@ -271,7 +276,7 @@ def training_forward(self, batch_data):
x_recon, z_mu, z_logvar = self.model(x)
loss = self.criterion(x, x_recon, z_mu, z_logvar,
beta=self.beta, capacity=self.capacity,
logvar_clip=self.logvar_clip)
logvar_clip=self.logvar_clip_)
loss.backward()
self.optimizer.step()
return loss.item()
Expand Down
12 changes: 12 additions & 0 deletions pyod/test/test_base_dl.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@ def test_fit_decision_function(self):
# dummy_clf.fit(self.X_train)
# self.assertEqual(dummy_clf.decision_scores_.all(), zero_scores.all())

def test_fit_returns_self(self):
# BaseDetector.fit documents "Returns: self : object", but the deep
# learning base returned None, so `clf.fit(X).predict(X)` raised
# AttributeError for every detector built on it.
dummy_clf = DummyDetector()
returned = dummy_clf.fit(self.X_train)

self.assertIs(returned, dummy_clf)
# the chained form users expect from an sklearn-style estimator
labels = DummyDetector().fit(self.X_train).predict(self.X_test)
self.assertEqual(labels.shape, (self.n_test,))

def test_fit_with_y_is_ignored(self):
zero_scores = np.zeros(self.n_train)

Expand Down
Loading
Loading