Repository: online-ml/river (branch: sad-implementation)
Commit date: 2023-10-31T09:22:32Z

Workflow file: .github/workflows/code-quality.yml (code-quality)
name: code-quality

on:
  pull_request:
    branches:
      - "*"
  push:
    branches:
      - main

jobs:
  ubuntu:
    runs-on: ubuntu-latest
    services:
      pypi_wayback:
        image: ghcr.io/waleko/pypi-wayback
        ports:
          - 8629:8080
    steps:
      - uses: actions/checkout@v2

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: 3.11

      - run: curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain=nightly --profile=minimal -y && rustup show
        if: matrix.os == 'ubuntu-latest'

      - name: Cache Python dependencies
        uses: actions/cache@v2
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install Python dependencies
        run: |
          python -m pip install --upgrade pip
          pip install wheel
          pip install -e ".[dev]"

      - name: Run pre-commit on all files
        run: pre-commit run --all-files

env:
  PIP_INDEX_URL: http://localhost:8629/2023-10-31
  UV_INDEX_URL: http://localhost:8629/2023-10-31


Failed build logs:
step_name: ubuntu/7_Run pre-commit on all files.txt
log: ##[group]Run pre-commit run --all-files
[36;1mpre-commit run --all-files[0m
shell: /usr/bin/bash -e {0}
env:
  pythonLocation: /opt/hostedtoolcache/Python/3.11.6/x64
  PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.6/x64/lib/pkgconfig
  Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.6/x64
  Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.6/x64
  Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.6/x64
  LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.6/x64/lib
##[endgroup]
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Initializing environment for local.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
[INFO] Installing environment for local.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
check json...........................................(no files to check)Skipped
check yaml...............................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
mixed line ending........................................................Passed
black....................................................................Passed
ruff.....................................................................Passed
mypy.....................................................................Failed
- hook id: mypy
- exit code: 1

river/drift/kswin.py:112: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/tree/nodes/sgt_nodes.py:45: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/tree/nodes/hatc_nodes.py:140: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/tree/stochastic_gradient_tree.py:56: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/anomaly/sad.py:68: error: Incompatible types in assignment (expression has type "Quantile", variable has type "Mean")  [assignment]
river/forest/aggregated_mondrian_forest.py:173: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/forest/aggregated_mondrian_forest.py:295: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
Found 1 error in 1 file (checked 230 source files)
river/drift/kswin.py:112: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/tree/nodes/sgt_nodes.py:45: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/tree/nodes/hatc_nodes.py:140: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/tree/stochastic_gradient_tree.py:56: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/anomaly/sad.py:68: error: Incompatible types in assignment (expression has type "Quantile", variable has type "Mean")  [assignment]
river/forest/aggregated_mondrian_forest.py:173: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
river/forest/aggregated_mondrian_forest.py:295: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
Found 1 error in 1 file (checked 230 source files)

##[error]Process completed with exit code 1.


Changed files:
river/anomaly/sad.py
river/test_estimators.py

Fix diff:
diff --git a/river/anomaly/sad.py b/river/anomaly/sad.py
index 959719a589..80c272c5a2 100644
--- a/river/anomaly/sad.py
+++ b/river/anomaly/sad.py
@@ -5,7 +5,7 @@ from river import anomaly, stats
 __all__ = ["StandardAbsoluteDeviation"]
 
 
-class StandardAbsoluteDeviation(anomaly.base.AnomalyDetector):
+class StandardAbsoluteDeviation(anomaly.base.SupervisedAnomalyDetector):
     r"""Standard Absolute Deviation (SAD).
 
     SAD is the model that calculates the anomaly score by using the deviation from the mean/median, divided by the
@@ -15,8 +15,9 @@ class StandardAbsoluteDeviation(anomaly.base.AnomalyDetector):
     This implementation is adapted from the [implementation](https://github.com/selimfirat/pysad/blob/master/pysad/models/standard_absolute_deviation.py)
     within PySAD (Python Streaming Anomaly Detection) [^2].
 
-    Despite the fact that this model only works with univariate distribution, the author maintains the required input
-    to be a dictionary (with length 1) to align with other anomaly detection algorithms implemented within `River`.
+    As a univariate anomaly detection algorithm, this implementation is adapted to `River` in a similar way as that of
+    the `GaussianScorer` algorithm, with the variable taken into the account at the learning phase and scoring phase
+    under variable `y`, ignoring `x`.
 
     Parameters
     ----------
@@ -40,25 +41,25 @@ class StandardAbsoluteDeviation(anomaly.base.AnomalyDetector):
 
     >>> np.random.seed(42)
 
-    >>> X = np.random.randn(150, 1)
+    >>> X = np.random.randn(150)
 
     >>> model = anomaly.StandardAbsoluteDeviation(sub_stat="mean")
 
-    >>> for x, _ in stream.iter_array(X):
-    ...     model.learn_one(x)
+    >>> for x in X:
+    ...     model = model.learn_one(None, x)
 
-    >>> model.score_one({0: 2})
+    >>> model.score_one(None, 2)
     2.209735291993561
 
-    >>> model.score_one({0: 0})
+    >>> model.score_one(None, 0)
     0.08736408615569183
 
-    >>> model.score_one({0: -1})
-    0.9738215167632427
+    >>> model.score_one(None, 1)
+    1.1485496890746263
 
     """
 
-    def __init__(self, sub_stat: str = "mean"):
+    def __init__(self, sub_stat=None):
         self.variance = stats.Var()
         self.sub_stat = sub_stat
 
@@ -71,18 +72,14 @@ class StandardAbsoluteDeviation(anomaly.base.AnomalyDetector):
                 f"Unknown subtracted statistic {self.sub_stat}, expected one of median, mean."
             )
 
-    def learn_one(self, x):
-        assert len(x) == 1
-        ((x_key, x_value),) = x.items()
+    def learn_one(self, x, y):
+        self.variance.update(y)
+        self.subtracted_statistic_estimator.update(y)
 
-        self.variance.update(x_value)
-        self.subtracted_statistic_estimator.update(x_value)
+        return self
 
-    def score_one(self, x):
-        assert len(x) == 1
-        ((x_key, x_value),) = x.items()
-
-        score = (x_value - self.subtracted_statistic_estimator.get()) / (
+    def score_one(self, x, y):
+        score = (y - self.subtracted_statistic_estimator.get()) / (
             self.variance.get() ** 0.5 + 1e-10
         )
 
diff --git a/river/test_estimators.py b/river/test_estimators.py
index 5cc1ab499b..18aacf32f2 100644
--- a/river/test_estimators.py
+++ b/river/test_estimators.py
@@ -54,7 +54,6 @@ def iter_estimators_which_can_be_tested():
         River2SKLBase,
         SKL2RiverBase,
         anomaly.LocalOutlierFactor,  # needs warm-start to work correctly
-        anomaly.StandardAbsoluteDeviation,  # SAD only works with data points as dictionaries with length 1.
         compose.FuncTransformer,
         compose.Grouper,
         compose.Pipeline,