Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
823d514
Relax dependency version constraints and update Python version suppor…
kulbachcedric Dec 21, 2025
1beb085
Remove python 3.13 due to pytorch
kulbachcedric Dec 21, 2025
9f34f1c
Remove python 3.13 due to pytorch
kulbachcedric Dec 21, 2025
1779456
Add "sys_platform == 'linux' and platform_machine == 'x86_64'",
kulbachcedric Dec 21, 2025
33f9286
Change river version
kulbachcedric Dec 21, 2025
f3c5589
Change river version
kulbachcedric Dec 21, 2025
d1bf0ea
Add boundaries for uv
kulbachcedric Dec 21, 2025
9dc067d
Add networkx
kulbachcedric Dec 21, 2025
266308a
add sympy
kulbachcedric Dec 21, 2025
0384ff0
add lxml
kulbachcedric Dec 21, 2025
24aad29
Refactor dependencies
kulbachcedric Dec 21, 2025
ef133f1
Restore tool config sections
kulbachcedric Feb 5, 2026
b548cfa
Use lowest-direct for minimum deps
kulbachcedric Feb 5, 2026
f731afe
Add workflow_dispatch trigger
kulbachcedric Feb 5, 2026
10df8c5
Add min max tests
kulbachcedric Feb 6, 2026
97970c3
Refactor networks version
kulbachcedric Feb 6, 2026
5143a99
add better constraints
kulbachcedric Feb 6, 2026
8210e7f
add python 313
kulbachcedric Feb 6, 2026
6898a39
Add networkxs
kulbachcedric Feb 9, 2026
3d97fed
change tensor conversion
kulbachcedric Mar 31, 2026
4b4b5ea
Merge branch 'issue-130-relax-deps' into 130-relax-dependency-version…
kulbachcedric Mar 31, 2026
0ad1de9
Merge pull request #133 from online-ml/130-relax-dependency-version-c…
kulbachcedric Mar 31, 2026
e605e5e
Ad nox tests
kulbachcedric Mar 31, 2026
14d07c0
minor changes
kulbachcedric Apr 3, 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
Empty file added 
Empty file.
23 changes: 16 additions & 7 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
pull_request:
branches:
- master
workflow_dispatch:

permissions:
contents: read
Expand All @@ -18,9 +19,10 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ['3.10', '3.11', '3.12']
python: ['3.10', '3.11', '3.12', '3.13']
os: [ubuntu-latest, macos-latest, windows-latest]
river: ['0.22.0']
deps: ['min', 'max']

steps:
- uses: actions/checkout@v4
Expand All @@ -37,16 +39,23 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ matrix.python }}-${{ matrix.river }}-${{ hashFiles('**/uv.lock') }}
key: uv-${{ runner.os }}-${{ matrix.python }}-${{ matrix.river }}-${{ hashFiles('**/uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-${{ matrix.python }}-${{ matrix.river }}-
uv-${{ runner.os }}-${{ matrix.python }}-${{ matrix.dependency-set }}-
uv-${{ runner.os }}-${{ matrix.python }}-
uv-${{ runner.os }}-

- name: Install dependencies with river ${{ matrix.river }}
shell: bash
run: |
uv add river==${{ matrix.river }}
uv sync --all-extras
if [ "${{ matrix.deps }}" = "min" ]; then
uv sync --all-extras --resolution=lowest-direct
elif [ "${{ matrix.deps }}" = "max" ]; then
uv sync --all-extras --upgrade
else
uv sync --all-extras
fi

- name: Download datasets
run: uv run python -c "from river import datasets; datasets.Bikes().download(); datasets.CreditCard().download(); datasets.Elec2().download(); datasets.Keystroke().download()"
Expand All @@ -61,18 +70,18 @@ jobs:
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-${{ matrix.os }}-py${{ matrix.python }}-river${{ matrix.river }}
name: coverage-${{ matrix.os }}-py${{ matrix.python }}-${{ matrix.dependency-set }}
path: coverage.xml
if-no-files-found: warn

- name: Upload coverage report to Codecov (aggregate project)
if: github.repository == 'online-ml/deep-river' && matrix.os == 'ubuntu-latest' && matrix.python == '3.12' && matrix.river == '0.22.0'
if: github.repository == 'online-ml/deep-river' && matrix.os == 'ubuntu-latest' && matrix.python == '3.13' && matrix.river == '0.22.0' && matrix.deps == 'default'
uses: codecov/codecov-action@v4
with:
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
name: ${{ matrix.os }}-py${{ matrix.python }}-river${{ matrix.river }}
name: ${{ matrix.os }}-py${{ matrix.python }}-${{ matrix.dependency-set }}
fail_ci_if_error: true
verbose: true
disable_search: true
Expand Down
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
COMMIT_HASH := $(shell eval git rev-parse HEAD)
WORKFLOW_FILE := .github/workflows/unit-tests.yml
PYTHON_VERSIONS := $(shell python3 -c "import pathlib,re; text=pathlib.Path('$(WORKFLOW_FILE)').read_text(); versions=[]; m=re.search(r'python:\\s*\\[([^\\]]+)\\]', text); versions += re.findall(r'\\'([^\\']+)\\'', m.group(1)) if m else []; versions += re.findall(r'python:\\s*\\'([^\\']+)\\'', text); versions=sorted(set(versions), key=lambda v: tuple(int(x) for x in v.split('.'))); print(' '.join(versions))")

# Test that uv is installed
check-uv:
Expand All @@ -13,6 +15,20 @@ format: check-uv
test: check-uv
uv run pytest

python-install: check-uv
uv python install $(PYTHON_VERSIONS)

test-min: check-uv
uv sync --all-extras --resolution=lowest-direct
uv run pytest

test-max: check-uv
uv sync --all-extras --upgrade
uv run pytest

test-matrix: check-uv
uv run nox -s tests

execute-notebooks: check-uv
uv run jupyter nbconvert --execute --to notebook --inplace docs/*/*/*.ipynb --ExecutePreprocessor.timeout=-1

Expand Down
4 changes: 2 additions & 2 deletions deep_river/anomaly/ae.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,5 @@ def score_many(self, X: pd.DataFrame) -> np.ndarray:
self.loss_func(x_pred, x_t, reduction="none"),
dim=list(range(1, x_t.dim())),
)
score = loss.cpu().detach().numpy()
return score
score = loss.detach().cpu().tolist()
return np.array(score)
22 changes: 11 additions & 11 deletions deep_river/anomaly/probability_weighted_ae.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import pandas as pd
import torch
from river import stats, utils
from scipy.special import ndtr

from deep_river.anomaly import ae

Expand Down Expand Up @@ -65,20 +64,21 @@ def learn_one(self, x: dict, y: Any = None, **kwargs) -> None:
self._apply_loss(loss)

def _apply_loss(self, loss):
losses_numpy = loss.detach().numpy()
loss_values = loss.detach().cpu().view(-1)
mean = self.rolling_mean.get()
var = self.rolling_var.get() if self.rolling_var.get() > 0 else 1
if losses_numpy.ndim == 0:
self.rolling_mean.update(losses_numpy)
self.rolling_var.update(losses_numpy)
if loss_values.numel() == 1:
value = float(loss_values.item())
self.rolling_mean.update(value)
self.rolling_var.update(value)
else:
for loss_numpy in range(len(losses_numpy)):
self.rolling_mean.update(loss_numpy)
self.rolling_var.update(loss_numpy)
for value in loss_values.tolist():
self.rolling_mean.update(value)
self.rolling_var.update(value)

loss_scaled = (losses_numpy - mean) / math.sqrt(var)
prob = ndtr(loss_scaled)
loss = torch.tensor((self.skip_threshold - prob) / self.skip_threshold) * loss
loss_scaled = (loss - mean) / math.sqrt(var)
prob = 0.5 * (1.0 + torch.erf(loss_scaled / math.sqrt(2.0)))
loss = ((self.skip_threshold - prob) / self.skip_threshold) * loss

self.optimizer.zero_grad()
loss.backward()
Expand Down
37 changes: 21 additions & 16 deletions deep_river/anomaly/rolling_ae.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from typing import Any, Callable, List, Union

import numpy as np
import pandas as pd
import torch
from river import anomaly
from torch import nn

from deep_river.base import RollingDeepEstimator
from deep_river.utils.tensor_conversion import deque2rolling_tensor


class _TestLSTMAutoencoder(nn.Module):
Expand Down Expand Up @@ -154,9 +152,10 @@ def learn_one(self, x: dict, y: Any = None, **kwargs) -> None:
Additional keyword arguments.
"""
self._update_observed_features(x)
self._x_window.append(list(x.values()))
x_row = self._dict2tensor(x).squeeze(0).tolist()
self._x_window.append(x_row)

x_t = deque2rolling_tensor(self._x_window, device=self.device)
x_t = self._deque2rolling_tensor(self._x_window)
self._learn(x=x_t)

def learn_many(self, X: pd.DataFrame, y=None) -> None:
Expand All @@ -171,9 +170,11 @@ def learn_many(self, X: pd.DataFrame, y=None) -> None:
"""
self._update_observed_features(X)

self._x_window.append(X.values.tolist())
X_t = self._df2tensor(X)
for row in X_t.tolist():
self._x_window.append(row)
if len(self._x_window) == self.window_size:
X_t = deque2rolling_tensor(self._x_window, device=self.device)
X_t = self._deque2rolling_tensor(self._x_window)
self._learn(x=X_t)

def score_one(self, x: dict) -> float:
Expand All @@ -191,18 +192,19 @@ def score_one(self, x: dict) -> float:
"""
res = 0.0
self._update_observed_features(x)
x_row = self._dict2tensor(x).squeeze(0).tolist()
if len(self._x_window) == self.window_size:
x_win = self._x_window.copy()
x_win.append(list(x.values()))
x_t = deque2rolling_tensor(x_win, device=self.device)
x_win.append(x_row)
x_t = self._deque2rolling_tensor(x_win)
self.module.eval()
with torch.inference_mode():
x_pred = self.module(x_t)
loss = self.loss_func(x_pred, x_t)
res = loss.item()

if self.append_predict:
self._x_window.append(list(x.values()))
self._x_window.append(x_row)
return res

def score_many(self, X: pd.DataFrame) -> List[Any]:
Expand All @@ -221,23 +223,26 @@ def score_many(self, X: pd.DataFrame) -> List[Any]:
List of computed anomaly scores (reconstruction errors) for each sample in X.
"""
self._update_observed_features(X)
X_t = self._df2tensor(X)
x_win = self._x_window.copy()
x_win.append(X.values.tolist())
for row in X_t.tolist():
x_win.append(row)
if self.append_predict:
self._x_window.append(X.values.tolist())
for row in X_t.tolist():
self._x_window.append(row)

if len(self._x_window) == self.window_size:
X_t = deque2rolling_tensor(x_win, device=self.device)
X_t = self._deque2rolling_tensor(x_win)
self.module.eval()
with torch.inference_mode():
x_pred = self.module(X_t)
loss = torch.mean(
self.loss_func(x_pred, x_pred, reduction="none"),
dim=list(range(1, x_pred.dim())),
)
losses = loss.detach().numpy()
losses = loss.detach().cpu().view(-1).tolist()
if len(losses) < len(X):
losses = np.pad(losses, (len(X) - len(losses), 0))
return losses.tolist()
losses = [0.0] * (len(X) - len(losses)) + losses
return losses
else:
return np.zeros(len(X)).tolist()
return [0.0] * len(X)
2 changes: 1 addition & 1 deletion deep_river/regression/multioutput.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def predict_many(self, X: pd.DataFrame) -> pd.DataFrame:
elif y_pred.shape[1] > len(cols):
extra = [f"__extra_{i}" for i in range(y_pred.shape[1] - len(cols))]
cols = cols + extra
return pd.DataFrame(y_pred.numpy(), columns=cols)
return pd.DataFrame(y_pred.detach().cpu().tolist(), columns=cols)

# ---------------------------------------------------------------------
# Internal helpers
Expand Down
2 changes: 1 addition & 1 deletion deep_river/regression/regressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def predict_many(self, X: pd.DataFrame) -> pd.DataFrame:
self.module.eval()
with torch.inference_mode():
y_preds = self.module(x_t)
return pd.DataFrame(y_preds if not y_preds.is_cuda else y_preds.cpu().numpy())
return pd.DataFrame(y_preds.detach().cpu().tolist())

@classmethod
def _unit_test_params(cls):
Expand Down
4 changes: 2 additions & 2 deletions deep_river/regression/rolling_regressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def predict_one(self, x: dict) -> base.typing.RegTarget:
x_t = self._deque2rolling_tensor(x_win)
y_pred = self.module(x_t)
if isinstance(y_pred, torch.Tensor):
y_pred = y_pred.detach().view(-1)[-1].cpu().numpy().item()
y_pred = y_pred.detach().view(-1)[-1].cpu().item()
else:
y_pred = float(y_pred)

Expand Down Expand Up @@ -250,6 +250,6 @@ def predict_many(self, X: pd.DataFrame) -> pd.DataFrame:
x_t = self._deque2rolling_tensor(x_win)
y_preds = self.module(x_t)
if isinstance(y_preds, torch.Tensor):
y_preds = y_preds.detach().cpu().view(-1).numpy().tolist()
y_preds = y_preds.detach().cpu().view(-1).tolist()

return pd.DataFrame({"y_pred": y_preds})
45 changes: 22 additions & 23 deletions deep_river/utils/tensor_conversion.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import math
from typing import Deque, Dict, Hashable, List, Optional, Union

import numpy as np
import pandas as pd
import torch
from river import base
Expand Down Expand Up @@ -202,53 +201,53 @@ def output2proba(
else:
preds = torch.sigmoid(preds)

preds_np = preds.detach().cpu().numpy()
n_outputs = preds_np.shape[-1]
preds_t = preds.detach().cpu()
n_outputs = preds_t.shape[-1]
n_classes = len(classes)

def renorm_rows(arr):
sums = arr.sum(axis=1, keepdims=True)
sums[sums == 0] = 1.0
return arr / sums
def renorm_rows(tensor):
sums = tensor.sum(dim=1, keepdim=True)
sums = torch.where(sums == 0, torch.ones_like(sums), sums)
return tensor / sums

# Boolean mode (binary classification) – always {False, True}
boolean_mode = (
all(c in (True, False) for c in classes) and n_outputs in (1, 2)
) or (n_classes == 0 and n_outputs in (1, 2))
if boolean_mode:
if n_outputs == 1:
p_true = preds_np[:, 0].astype("float64")
p_false = (1.0 - p_true).astype("float64")
probs = np.stack([p_true, p_false], axis=1)
p_true = preds_t[:, 0].to(dtype=torch.float64)
p_false = (1.0 - p_true).to(dtype=torch.float64)
probs = torch.stack([p_true, p_false], dim=1)
probs = renorm_rows(probs)
return [dict(zip([True, False], row.astype("float64"))) for row in probs]
return [dict(zip([True, False], row)) for row in probs.tolist()]
else: # n_outputs == 2
probs = preds_np.astype("float64")
probs = preds_t.to(dtype=torch.float64)
if is_probabilistic:
probs = renorm_rows(probs)
return [dict(zip([False, True], row.astype("float64"))) for row in probs]
return [dict(zip([False, True], row)) for row in probs.tolist()]

# Single-output (non-boolean) -> observed class + Unobserved0
if n_outputs == 1:
p_obs = preds_np[:, 0].astype("float64")
p_un = (1.0 - p_obs).astype("float64")
probs = np.stack([p_obs, p_un], axis=1)
p_obs = preds_t[:, 0].to(dtype=torch.float64)
p_un = (1.0 - p_obs).to(dtype=torch.float64)
probs = torch.stack([p_obs, p_un], dim=1)
if is_probabilistic:
probs = renorm_rows(probs)
if n_classes == 0:
labels: List[Hashable] = [0, 1]
else:
primary = next(iter(classes))
labels = [primary, "Unobserved0"] # mixed types intentional
return [dict(zip(labels, row.astype("float64"))) for row in probs]
return [dict(zip(labels, row)) for row in probs.tolist()]

# Multi-output handling (n_outputs > 1, non-boolean)
if n_classes == 0:
labels2: List[Hashable] = list(range(n_outputs))
rows = preds_np
rows = preds_t.to(dtype=torch.float64)
if is_probabilistic:
rows = renorm_rows(rows.astype("float64")).astype(rows.dtype)
return [dict(zip(labels2, row)) for row in rows]
rows = renorm_rows(rows)
return [dict(zip(labels2, row)) for row in rows.tolist()]

labels3: List[Hashable] = list(classes)
if len(labels3) < n_outputs:
Expand All @@ -257,7 +256,7 @@ def renorm_rows(arr):
else:
labels3 = labels3[:n_outputs]

rows = preds_np
rows = preds_t.to(dtype=torch.float64)
if is_probabilistic:
rows = renorm_rows(rows.astype("float64")).astype(rows.dtype)
return [dict(zip(labels3, row)) for row in rows]
rows = renorm_rows(rows)
return [dict(zip(labels3, row)) for row in rows.tolist()]
4 changes: 2 additions & 2 deletions deep_river/utils/test_tensor_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_output2proba():

preds = torch.tensor([[2.0, 1.0, 0.1]])
classes = SortedSet(["class1", "class2", "class3"])
softmaxed_preds = torch.softmax(preds, dim=-1).numpy().flatten()
softmaxed_preds = torch.softmax(preds, dim=-1).flatten().tolist()
expected_output = {
"class1": softmaxed_preds[0],
"class2": softmaxed_preds[1],
Expand All @@ -114,7 +114,7 @@ def test_output2proba():

preds = torch.tensor([[0.8]])
classes = SortedSet(["positive"])
sigmoid_pred = torch.sigmoid(preds).numpy().flatten()
sigmoid_pred = torch.sigmoid(preds).flatten().tolist()
expected_output = {
"positive": sigmoid_pred[0],
"Unobserved0": 1 - sigmoid_pred[0],
Expand Down
Loading
Loading