Source code for ts2net.sklearn.rolling_extractor
"""
Rolling window network features for sklearn pipelines.
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
import numpy as np
from numpy.typing import NDArray
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils.validation import check_array, check_is_fitted
from ts2net.api_windows import build_windows
_AGG_FUNCS = ("mean", "std", "min", "max")
[docs]
class RollingNetworkFeatureExtractor(BaseEstimator, TransformerMixin):
"""
Extract network features from rolling windows, aggregated per series.
Each input row is one time series. Windows are built along the series,
graph statistics are computed per window, then aggregated (mean, std, etc.)
into a fixed-length feature vector suitable for sklearn.
Parameters
----------
method : str, default "hvg"
Network method: ``hvg``, ``nvg``, ``recurrence``, ``transition``.
window : int, default 64
Window width in time points.
step : int, default 32
Step between consecutive windows.
aggregates : list of str, default ("mean", "std")
Aggregations applied to each window-level statistic.
features : list of str, optional
Window stats to include (default: core graph stats).
prefix : str, optional
Feature name prefix.
**builder_kwargs
Extra arguments for the network builder.
Examples
--------
>>> import numpy as np
>>> from ts2net.sklearn import RollingNetworkFeatureExtractor
>>> X = np.random.randn(20, 300)
>>> ext = RollingNetworkFeatureExtractor(window=50, step=25)
>>> ext.fit(X).transform(X).shape[0] == 20
True
"""
def __init__(
self,
method: str = "hvg",
window: int = 64,
step: int = 32,
aggregates: Sequence[str] = ("mean", "std"),
features: Sequence[str] | None = None,
prefix: str | None = None,
**builder_kwargs: Any,
) -> None:
self.method = method
self.window = window
self.step = step
self.aggregates = tuple(aggregates)
self.features = features
self.prefix = prefix
self.builder_kwargs = builder_kwargs
[docs]
def fit(
self,
X: NDArray[np.float64],
y: NDArray[Any] | None = None,
) -> RollingNetworkFeatureExtractor:
X = self._validate_X(X)
for agg in self.aggregates:
if agg not in _AGG_FUNCS:
raise ValueError(f"aggregate must be one of {_AGG_FUNCS}, got {agg!r}")
probe = self._series_features(X[0])
self.feature_names_ = list(probe.keys())
self.n_features_in_ = X.shape[1]
self.n_features_out_ = len(self.feature_names_)
return self
[docs]
def transform(self, X: NDArray[np.float64]) -> NDArray[np.float64]:
check_is_fitted(self, "feature_names_")
X = self._validate_X(X)
return np.vstack([self._series_to_vector(x) for x in X])
[docs]
def get_feature_names_out(
self, input_features: Sequence[str] | None = None
) -> np.ndarray:
check_is_fitted(self, "feature_names_")
return np.asarray(self.feature_names_, dtype=object)
def _validate_X(self, X: NDArray[np.float64]) -> NDArray[np.float64]:
try:
X = check_array(
X, dtype="numeric", ensure_2d=True, ensure_all_finite=False
)
except TypeError:
X = check_array(
X, dtype="numeric", ensure_2d=True, force_all_finite=False
)
if X.shape[1] < self.window:
raise ValueError(
f"Series length {X.shape[1]} must be >= window={self.window}"
)
return X
def _stat_names(self) -> list[str]:
if self.features is not None:
return list(self.features)
return ["n_nodes", "n_edges", "avg_degree", "std_degree", "density"]
def _series_features(self, x: NDArray[np.float64]) -> dict[str, float]:
x = np.asarray(x, dtype=np.float64)
x = x[np.isfinite(x)]
window_stats = build_windows(
x,
window=self.window,
step=self.step,
method=self.method,
output="stats",
**self.builder_kwargs,
)
stat_names = self._stat_names()
prefix = self.prefix or f"{self.method}_roll_"
out: dict[str, float] = {}
for stat in stat_names:
if stat not in window_stats:
continue
arr = np.asarray(window_stats[stat], dtype=np.float64)
for agg in self.aggregates:
if agg == "mean":
val = float(np.mean(arr))
elif agg == "std":
val = float(np.std(arr)) if len(arr) > 1 else 0.0
elif agg == "min":
val = float(np.min(arr))
else:
val = float(np.max(arr))
out[f"{prefix}{stat}_{agg}"] = val
return out
def _series_to_vector(self, x: NDArray[np.float64]) -> NDArray[np.float64]:
feats = self._series_features(x)
return np.array(
[feats[name] for name in self.feature_names_], dtype=np.float64
)