diff --git a/docs/introduction/why-use-river.md b/docs/introduction/why-use-river.md index 81efb4d41e..9c5f2744d1 100644 --- a/docs/introduction/why-use-river.md +++ b/docs/introduction/why-use-river.md @@ -14,4 +14,4 @@ River supports different machine learning tasks, including regression, classific ## User experience -River is not the only library allowing you to do online machine learning. But it might just the simplest one to use in the Python ecosystem. River plays nicely with Python dictionaries, therefore making it easy to use in the context of web applications where JSON payloads are aplenty. +River is not the only library allowing you to do online machine learning. But it might just be the simplest one to use in the Python ecosystem. River plays nicely with Python dictionaries, therefore making it easy to use in the context of web applications where JSON payloads are aplenty. diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index e1951d3bd2..dfb7dd4b19 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -1,6 +1,7 @@ # Unreleased - The units used in River have been corrected to be based on powers of 2 (KiB, MiB). This only changes the display, the behaviour is unchanged. +- The methods `learn_one`, `learn_many`, `update`, `revert`, and `append` now return `None`. ## cluster diff --git a/poetry.lock b/poetry.lock index d8bf62a520..968c69b1e4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1662,13 +1662,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.2.0" +version = "4.2.5" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.2.0-py3-none-any.whl", hash = "sha256:0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc"}, - {file = "jupyterlab-4.2.0.tar.gz", hash = "sha256:356e9205a6a2ab689c47c8fe4919dba6c076e376d03f26baadc05748c2435dd5"}, + {file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"}, + {file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"}, ] [package.dependencies] @@ -1683,6 +1683,7 @@ jupyter-server = ">=2.4.0,<3" jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2" packaging = "*" +setuptools = ">=40.1.0" tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} tornado = ">=6.2.0" traitlets = "*" @@ -1692,7 +1693,7 @@ dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] -upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] +upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] [[package]] name = "jupyterlab-pygments" @@ -2589,13 +2590,13 @@ setuptools = "*" [[package]] name = "notebook" -version = "7.2.0" +version = "7.2.2" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.2.0-py3-none-any.whl", hash = "sha256:b4752d7407d6c8872fc505df0f00d3cae46e8efb033b822adacbaa3f1f3ce8f5"}, - {file = "notebook-7.2.0.tar.gz", hash = "sha256:34a2ba4b08ad5d19ec930db7484fb79746a1784be9e1a5f8218f9af8656a141f"}, + {file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"}, + {file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"}, ] [package.dependencies] @@ -3477,6 +3478,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, diff --git a/river/anomaly/base.py b/river/anomaly/base.py index 9c8fc42c50..c52a4bfc67 100644 --- a/river/anomaly/base.py +++ b/river/anomaly/base.py @@ -15,7 +15,7 @@ def _supervised(self): return False @abc.abstractmethod - def learn_one(self, x: dict): + def learn_one(self, x: dict) -> None: """Update the model. Parameters @@ -48,7 +48,7 @@ class SupervisedAnomalyDetector(base.Estimator): """A supervised anomaly detector.""" @abc.abstractmethod - def learn_one(self, x: dict, y: base.typing.Target): + def learn_one(self, x: dict, y: base.typing.Target) -> None: """Update the model. Parameters @@ -137,7 +137,7 @@ def score_one(self, *args, **kwargs): """ return self.anomaly_detector.score_one(*args, **kwargs) - def learn_one(self, *args, **learn_kwargs): + def learn_one(self, *args, **learn_kwargs) -> None: """Update the anomaly filter and the underlying anomaly detector. Parameters diff --git a/river/anomaly/pad.py b/river/anomaly/pad.py index 4ec6304efb..0ddd3a403f 100644 --- a/river/anomaly/pad.py +++ b/river/anomaly/pad.py @@ -80,7 +80,7 @@ class PredictiveAnomalyDetection(anomaly.base.SupervisedAnomalyDetector): >>> for t, (x, y) in enumerate(datasets.AirlinePassengers()): ... score = PAD.score_one(None, y) - ... PAD = PAD.learn_one(None, y) + ... PAD.learn_one(None, y) ... scores.append(score) >>> print(scores[-1]) @@ -131,7 +131,6 @@ def learn_one(self, x: dict | None, y: base.typing.Target | float): self.predictive_model.learn_one(y=y, x=x) else: self.predictive_model.learn_one(x=x, y=y) - return self def score_one(self, x: dict, y: base.typing.Target): # Return the predicted value of x from the predictive model, first by checking whether diff --git a/river/base/classifier.py b/river/base/classifier.py index 19f01be18f..876bef4e13 100644 --- a/river/base/classifier.py +++ b/river/base/classifier.py @@ -15,7 +15,7 @@ class Classifier(estimator.Estimator): """A classifier.""" @abc.abstractmethod - def learn_one(self, x: dict, y: base.typing.ClfTarget) -> Classifier: + def learn_one(self, x: dict, y: base.typing.ClfTarget) -> None: """Update the model with a set of features `x` and a label `y`. Parameters @@ -81,7 +81,7 @@ class MiniBatchClassifier(Classifier): """A classifier that can operate on mini-batches.""" @abc.abstractmethod - def learn_many(self, X: pd.DataFrame, y: pd.Series): + def learn_many(self, X: pd.DataFrame, y: pd.Series) -> None: """Update the model with a mini-batch of features `X` and boolean targets `y`. Parameters diff --git a/river/base/clusterer.py b/river/base/clusterer.py index 07c6ac7f3d..35a73a71ad 100644 --- a/river/base/clusterer.py +++ b/river/base/clusterer.py @@ -13,7 +13,7 @@ def _supervised(self): return False @abc.abstractmethod - def learn_one(self, x: dict) -> Clusterer: + def learn_one(self, x: dict) -> None: """Update the model with a set of features `x`. Parameters diff --git a/river/base/drift_detector.py b/river/base/drift_detector.py index 9eaef0b506..a350c40d4b 100644 --- a/river/base/drift_detector.py +++ b/river/base/drift_detector.py @@ -77,7 +77,7 @@ class BinaryDriftDetector(_BaseDriftDetector): """A drift detector for binary data.""" @abc.abstractmethod - def update(self, x: bool) -> BinaryDriftDetector: + def update(self, x: bool) -> None: """Update the detector with a single boolean input. Parameters diff --git a/river/base/multi_output.py b/river/base/multi_output.py index a05904621a..078ed1a362 100644 --- a/river/base/multi_output.py +++ b/river/base/multi_output.py @@ -10,7 +10,7 @@ class MultiLabelClassifier(Estimator, abc.ABC): """Multi-label classifier.""" @abc.abstractmethod - def learn_one(self, x: dict, y: dict[FeatureName, bool]): + def learn_one(self, x: dict, y: dict[FeatureName, bool]) -> None: """Update the model with a set of features `x` and the labels `y`. Parameters @@ -68,7 +68,7 @@ class MultiTargetRegressor(Estimator, abc.ABC): """Multi-target regressor.""" @abc.abstractmethod - def learn_one(self, x: dict, y: dict[FeatureName, RegTarget], **kwargs): + def learn_one(self, x: dict, y: dict[FeatureName, RegTarget], **kwargs) -> None: """Fits to a set of features `x` and a real-valued target `y`. Parameters diff --git a/river/base/regressor.py b/river/base/regressor.py index 775a23a33e..09abacb2b6 100644 --- a/river/base/regressor.py +++ b/river/base/regressor.py @@ -15,7 +15,7 @@ class Regressor(estimator.Estimator): """A regressor.""" @abc.abstractmethod - def learn_one(self, x: dict, y: base.typing.RegTarget): + def learn_one(self, x: dict, y: base.typing.RegTarget) -> None: """Fits to a set of features `x` and a real-valued target `y`. Parameters @@ -47,7 +47,7 @@ class MiniBatchRegressor(Regressor): """A regressor that can operate on mini-batches.""" @abc.abstractmethod - def learn_many(self, X: pd.DataFrame, y: pd.Series): + def learn_many(self, X: pd.DataFrame, y: pd.Series) -> None: """Update the model with a mini-batch of features `X` and real-valued targets `y`. Parameters diff --git a/river/base/transformer.py b/river/base/transformer.py index 5313ba618d..16f9aba276 100644 --- a/river/base/transformer.py +++ b/river/base/transformer.py @@ -57,7 +57,7 @@ class Transformer(base.Estimator, BaseTransformer): def _supervised(self): return False - def learn_one(self, x: dict): + def learn_one(self, x: dict) -> None: """Update with a set of features `x`. A lot of transformers don't actually have to do anything during the `learn_one` step @@ -81,7 +81,7 @@ class SupervisedTransformer(base.Estimator, BaseTransformer): def _supervised(self): return True - def learn_one(self, x: dict, y: base.typing.Target): + def learn_one(self, x: dict, y: base.typing.Target) -> None: """Update with a set of features `x` and a target `y`. Parameters @@ -113,7 +113,7 @@ def transform_many(self, X: pd.DataFrame) -> pd.DataFrame: """ - def learn_many(self, X: pd.DataFrame): + def learn_many(self, X: pd.DataFrame) -> None: """Update with a mini-batch of features. A lot of transformers don't actually have to do anything during the `learn_many` step @@ -138,7 +138,7 @@ def _supervised(self): return True @abc.abstractmethod - def learn_many(self, X: pd.DataFrame, y: pd.Series): + def learn_many(self, X: pd.DataFrame, y: pd.Series) -> None: """Update the model with a mini-batch of features `X` and targets `y`. Parameters diff --git a/river/cluster/textclust.py b/river/cluster/textclust.py index 82e507517c..2115fd1b03 100644 --- a/river/cluster/textclust.py +++ b/river/cluster/textclust.py @@ -213,7 +213,6 @@ def learn_one(self, x, t=None, w=None): ## increment observation counter self.n += 1 - return clusterId ## predicts the cluster number. The type specifies whether this should happen on micro-cluster ## or macro-cluster level diff --git a/river/compat/sklearn_to_river.py b/river/compat/sklearn_to_river.py index bb16ad76c7..93664bd342 100644 --- a/river/compat/sklearn_to_river.py +++ b/river/compat/sklearn_to_river.py @@ -33,7 +33,7 @@ def convert_sklearn_to_river(estimator: sklearn_base.BaseEstimator, classes: lis (sklearn_base.RegressorMixin, SKL2RiverRegressor), ( sklearn_base.ClassifierMixin, - functools.partial(SKL2RiverClassifier, classes=classes), + functools.partial(SKL2RiverClassifier, classes=classes), # type:ignore[arg-type] ), ] diff --git a/river/covariance/emp.py b/river/covariance/emp.py index 255a28a726..1b2f64a80e 100644 --- a/river/covariance/emp.py +++ b/river/covariance/emp.py @@ -379,5 +379,3 @@ def update_many(self, X: pd.DataFrame): row = self._w[fi] * inv_cov[i] for j, fj in enumerate(X): self._inv_cov[min((fi, fj), (fj, fi))] = row[j] - - return self diff --git a/river/ensemble/ewa.py b/river/ensemble/ewa.py index 7a15a95738..4a3d77358b 100644 --- a/river/ensemble/ewa.py +++ b/river/ensemble/ewa.py @@ -122,7 +122,6 @@ def learn_predict_one(self, x, y): def learn_one(self, x, y): self.learn_predict_one(x, y) - return self def predict_one(self, x): return sum(model.predict_one(x) * weight for model, weight in zip(self, self.weights)) diff --git a/river/feature_extraction/vectorize.py b/river/feature_extraction/vectorize.py index f4d6aff840..5f68450208 100644 --- a/river/feature_extraction/vectorize.py +++ b/river/feature_extraction/vectorize.py @@ -203,7 +203,7 @@ def __init__( # Stop word removal if self.stop_words: self.processing_steps.append( - functools.partial(remove_stop_words, stop_words=stop_words) + functools.partial(remove_stop_words, stop_words=self.stop_words) ) # n-grams diff --git a/river/linear_model/base.py b/river/linear_model/base.py index 02846efb18..91d6ba7f70 100644 --- a/river/linear_model/base.py +++ b/river/linear_model/base.py @@ -122,8 +122,6 @@ def _fit(self, x, y, w, get_grad): self._update_weights(x) - return self - def _update_weights(self, x): # L1 cumulative penalty helper @@ -161,7 +159,7 @@ def _eval_gradient_one(self, x: dict, y: float, w: float) -> tuple[dict, float]: return (loss_gradient * utils.VectorDict(x), loss_gradient) - def learn_one(self, x, y, w=1.0): + def learn_one(self, x, y, w=1.0) -> None: with self._learn_mode(x): self._fit(x, y, w, get_grad=self._eval_gradient_one) @@ -188,7 +186,7 @@ def _eval_gradient_many( return dict(zip(X.columns, gradient)), loss_gradient.mean() - def learn_many(self, X: pd.DataFrame, y: pd.Series, w: float | pd.Series = 1): + def learn_many(self, X: pd.DataFrame, y: pd.Series, w: float | pd.Series = 1) -> None: self._y_name = y.name with self._learn_mode(set(X)): self._fit(X, y, w, get_grad=self._eval_gradient_many) diff --git a/river/metrics/base.py b/river/metrics/base.py index 0fb837848e..788de42d3f 100644 --- a/river/metrics/base.py +++ b/river/metrics/base.py @@ -22,11 +22,11 @@ class Metric(base.Base, abc.ABC): """Mother class for all metrics.""" @abc.abstractmethod - def update(self, y_true, y_pred) -> Metric: + def update(self, y_true, y_pred) -> None: """Update the metric.""" @abc.abstractmethod - def revert(self, y_true, y_pred) -> Metric: + def revert(self, y_true, y_pred) -> None: """Revert the metric.""" @abc.abstractmethod @@ -89,14 +89,14 @@ def __init__(self, cm=None): cm = metrics.ConfusionMatrix() self.cm = cm - def update(self, y_true, y_pred, w=1.0): + def update(self, y_true, y_pred, w=1.0) -> None: self.cm.update( y_true, y_pred, w=w, ) - def revert(self, y_true, y_pred, w=1.0): + def revert(self, y_true, y_pred, w=1.0) -> None: self.cm.revert( y_true, y_pred, @@ -152,7 +152,7 @@ def update( y_true: bool, y_pred: bool | float | dict[bool, float], w=1.0, - ) -> BinaryMetric: + ) -> None: if self.requires_labels: y_pred = y_pred == self.pos_val return super().update(y_true == self.pos_val, y_pred, w) @@ -162,7 +162,7 @@ def revert( y_true: bool, y_pred: bool | float | dict[bool, float], w=1.0, - ) -> BinaryMetric: + ) -> None: if self.requires_labels: y_pred = y_pred == self.pos_val return super().revert(y_true == self.pos_val, y_pred, w) @@ -190,11 +190,11 @@ class RegressionMetric(Metric): _fmt = ",.6f" # use commas to separate big numbers and show 6 decimals @abc.abstractmethod - def update(self, y_true: numbers.Number, y_pred: numbers.Number) -> RegressionMetric: + def update(self, y_true: numbers.Number, y_pred: numbers.Number) -> None: """Update the metric.""" @abc.abstractmethod - def revert(self, y_true: numbers.Number, y_pred: numbers.Number) -> RegressionMetric: + def revert(self, y_true: numbers.Number, y_pred: numbers.Number) -> None: """Revert the metric.""" @property @@ -227,7 +227,7 @@ def __init__(self, metrics, str_sep="\n"): super().__init__(metrics) self.str_sep = str_sep - def update(self, y_true, y_pred, w=1.0): + def update(self, y_true, y_pred, w=1.0) -> None: # If the metrics are classification metrics, then we have to handle the case where some # of the metrics require labels, whilst others need to be fed probabilities if hasattr(self, "requires_labels") and not self.requires_labels: @@ -241,7 +241,7 @@ def update(self, y_true, y_pred, w=1.0): for m in self: m.update(y_true, y_pred) - def revert(self, y_true, y_pred, w=1.0): + def revert(self, y_true, y_pred, w=1.0) -> None: # If the metrics are classification metrics, then we have to handle the case where some # of the metrics require labels, whilst others need to be fed probabilities if hasattr(self, "requires_labels") and not self.requires_labels: @@ -334,10 +334,10 @@ def __init__(self): def _eval(self, y_true, y_pred): pass - def update(self, y_true, y_pred, w=1.0): + def update(self, y_true, y_pred, w=1.0) -> None: self._mean.update(x=self._eval(y_true, y_pred), w=w) - def revert(self, y_true, y_pred, w=1.0): + def revert(self, y_true, y_pred, w=1.0) -> None: self._mean.revert(x=self._eval(y_true, y_pred), w=w) def get(self): @@ -353,11 +353,11 @@ class ClusteringMetric(base.Base, abc.ABC): _fmt = ",.6f" # Use commas to separate big numbers and show 6 decimals @abc.abstractmethod - def update(self, x, y_pred, centers, w=1.0) -> ClusteringMetric: + def update(self, x, y_pred, centers, w=1.0) -> None: """Update the metric.""" @abc.abstractmethod - def revert(self, x, y_pred, centers, w=1.0) -> ClusteringMetric: + def revert(self, x, y_pred, centers, w=1.0) -> None: """Revert the metric.""" @abc.abstractmethod diff --git a/river/metrics/multioutput/base.py b/river/metrics/multioutput/base.py index 4768f96ce1..eed2ab8283 100644 --- a/river/metrics/multioutput/base.py +++ b/river/metrics/multioutput/base.py @@ -38,7 +38,7 @@ def update( y_pred: dict[str | int, base.typing.ClfTarget] | dict[str | int, dict[base.typing.ClfTarget, float]], w=1.0, - ): + ) -> None: """Update the metric.""" self.cm.update(y_true, y_pred, w) @@ -48,7 +48,7 @@ def revert( y_pred: dict[str | int, base.typing.ClfTarget] | dict[str | int, dict[base.typing.ClfTarget, float]], w=1.0, - ): + ) -> None: """Revert the metric.""" self.cm.revert(y_true, y_pred, w) @@ -72,7 +72,7 @@ def update( self, y_true: dict[str | int, float | int], y_pred: dict[str | int, float | int], - ): + ) -> None: """Update the metric.""" @abc.abstractmethod @@ -80,7 +80,7 @@ def revert( self, y_true: dict[str | int, float | int], y_pred: dict[str | int, float | int], - ): + ) -> None: """Revert the metric.""" def works_with(self, model) -> bool: diff --git a/river/neighbors/base.py b/river/neighbors/base.py index 0ca8e21c50..496e69f3f7 100644 --- a/river/neighbors/base.py +++ b/river/neighbors/base.py @@ -37,7 +37,7 @@ def __init__(self, dist_func: DistanceFunc | FunctionWrapper): self.dist_func = dist_func @abc.abstractmethod - def append(self, item: typing.Any, **kwargs): + def append(self, item: typing.Any, **kwargs) -> None: pass @abc.abstractmethod diff --git a/river/neighbors/knn_classifier.py b/river/neighbors/knn_classifier.py index a3e12f3c08..45e0d3f89c 100644 --- a/river/neighbors/knn_classifier.py +++ b/river/neighbors/knn_classifier.py @@ -143,8 +143,6 @@ def _run_class_cleanup(self): self.clean_up_classes() self._cleanup_counter = self.cleanup_every - return self - def predict_proba_one(self, x, **kwargs): nearest = self._nn.search((x, None), n_neighbors=self.n_neighbors, **kwargs) diff --git a/river/proba/base.py b/river/proba/base.py index c10766749d..f49366b5b9 100644 --- a/river/proba/base.py +++ b/river/proba/base.py @@ -56,11 +56,11 @@ class DiscreteDistribution(Distribution): """ @abc.abstractmethod - def update(self, x: typing.Hashable): + def update(self, x: typing.Hashable) -> None: """Updates the parameters of the distribution given a new observation.""" @abc.abstractmethod - def revert(self, x: typing.Hashable): + def revert(self, x: typing.Hashable) -> None: """Reverts the parameters of the distribution for a given observation.""" @@ -75,11 +75,11 @@ class BinaryDistribution(Distribution): """ @abc.abstractmethod - def update(self, x: bool): + def update(self, x: bool) -> None: """Updates the parameters of the distribution given a new observation.""" @abc.abstractmethod - def revert(self, x: bool): + def revert(self, x: bool) -> None: """Reverts the parameters of the distribution for a given observation.""" @@ -94,11 +94,11 @@ class ContinuousDistribution(Distribution): """ @abc.abstractmethod - def update(self, x: float): + def update(self, x: float) -> None: """Updates the parameters of the distribution given a new observation.""" @abc.abstractmethod - def revert(self, x: float): + def revert(self, x: float) -> None: """Reverts the parameters of the distribution for a given observation.""" @abc.abstractmethod @@ -117,11 +117,11 @@ class MultivariateContinuousDistribution(Distribution): """ @abc.abstractmethod - def update(self, x: dict[str, float]): + def update(self, x: dict[str, float]) -> None: """Updates the parameters of the distribution given a new observation.""" @abc.abstractmethod - def revert(self, x: dict[str, float]): + def revert(self, x: dict[str, float]) -> None: """Reverts the parameters of the distribution for a given observation.""" @abc.abstractmethod diff --git a/river/reco/base.py b/river/reco/base.py index de746e5d1a..d2ecc29576 100644 --- a/river/reco/base.py +++ b/river/reco/base.py @@ -33,7 +33,7 @@ def is_contextual(self): return False @abc.abstractmethod - def learn_one(self, user: ID, item: ID, y: Reward, x: dict | None = None): + def learn_one(self, user: ID, item: ID, y: Reward, x: dict | None = None) -> None: """Fits a `user`-`item` pair and a real-valued target `y`. Parameters diff --git a/river/stats/base.py b/river/stats/base.py index ddb5214197..9d3302e5c2 100644 --- a/river/stats/base.py +++ b/river/stats/base.py @@ -37,8 +37,8 @@ class Univariate(Statistic): """A univariate statistic measures a property of a variable.""" @abc.abstractmethod - def update(self, x: numbers.Number): - """Update and return the called instance.""" + def update(self, x: numbers.Number) -> None: + """Update the called instance.""" raise NotImplementedError @property @@ -68,5 +68,5 @@ class Bivariate(Statistic): """A bivariate statistic measures a relationship between two variables.""" @abc.abstractmethod - def update(self, x, y): - """Update and return the called instance.""" + def update(self, x, y) -> None: + """Update the called instance.""" diff --git a/river/stats/iqr.py b/river/stats/iqr.py index c5d56c38de..583a1260a3 100644 --- a/river/stats/iqr.py +++ b/river/stats/iqr.py @@ -61,6 +61,10 @@ def update(self, x): self._is_updated = True def get(self): + # HACK: Avoid crash if get is called before update + # panicked at 'index out of bounds: the len is 0 but the index is 0' + if not self._is_updated: + return None return self._iqr.get() def __repr__(self): diff --git a/river/time_series/base.py b/river/time_series/base.py index 1486f9815b..bcfc3ed492 100644 --- a/river/time_series/base.py +++ b/river/time_series/base.py @@ -13,7 +13,7 @@ def _supervised(self): return True @abc.abstractmethod - def learn_one(self, y: float, x: dict | None = None) -> Forecaster: + def learn_one(self, y: float, x: dict | None = None) -> None: """Updates the model. Parameters diff --git a/river/time_series/metrics.py b/river/time_series/metrics.py index 587ce2e645..405ef4a6bc 100644 --- a/river/time_series/metrics.py +++ b/river/time_series/metrics.py @@ -9,7 +9,7 @@ class ForecastingMetric(base.Base, abc.ABC): @abc.abstractmethod - def update(self, y_true: list[Number], y_pred: list[Number]) -> ForecastingMetric: + def update(self, y_true: list[Number], y_pred: list[Number]) -> None: """Update the metric at each step along the horizon. Parameters @@ -83,8 +83,6 @@ def update(self, y_true, y_pred): metric.update(yt, yp) - return self - def get(self): return [metric.get() for metric in self.metrics] diff --git a/river/tree/isoup_tree_regressor.py b/river/tree/isoup_tree_regressor.py index 0393379b17..abe519154f 100644 --- a/river/tree/isoup_tree_regressor.py +++ b/river/tree/isoup_tree_regressor.py @@ -208,7 +208,7 @@ def _new_leaf(self, initial_stats=None, parent=None): return new_adaptive - def learn_one(self, x, y, *, w: float = 1.0) -> iSOUPTreeRegressor: # type: ignore + def learn_one(self, x, y, *, w: float = 1.0, **kwargs) -> None: """Incrementally train the model with one sample. Training tasks: @@ -232,7 +232,7 @@ def learn_one(self, x, y, *, w: float = 1.0) -> iSOUPTreeRegressor: # type: ign # Update target set self.targets.update(y.keys()) - super().learn_one(x, y, w=w) # type: ignore + super().learn_one(x, y, w=w) def predict_one(self, x): pred = {} diff --git a/river/tree/splitter/base.py b/river/tree/splitter/base.py index 70e4216835..f963fbafba 100644 --- a/river/tree/splitter/base.py +++ b/river/tree/splitter/base.py @@ -21,7 +21,7 @@ class Splitter(base.Estimator, abc.ABC): """ @abc.abstractmethod - def update(self, att_val, target_val: base.typing.Target, w: float): + def update(self, att_val, target_val: base.typing.Target, w: float) -> None: """Update statistics of this observer given an attribute value, its target value and the weight of the instance observed. @@ -109,7 +109,7 @@ def __len__(self): pass @abc.abstractmethod - def update(self, x_val, gh: GradHess, w: float): + def update(self, x_val, gh: GradHess, w: float) -> None: pass @abc.abstractmethod diff --git a/river/tree/splitter/random_splitter.py b/river/tree/splitter/random_splitter.py index 4fd099f379..aa6562f975 100644 --- a/river/tree/splitter/random_splitter.py +++ b/river/tree/splitter/random_splitter.py @@ -36,11 +36,11 @@ def cond_proba(self, att_val, class_val) -> float: """This attribute observer does not support probability density estimation.""" raise NotImplementedError - def update(self, att_val, target_val, w) -> Splitter: + def update(self, att_val, target_val, w) -> None: if self.threshold is None: if len(self._buffer) < self.buffer_size: self._buffer.append((att_val, target_val, w)) - return self + return mn = min(self._buffer, key=lambda t: t[0])[0] mx = max(self._buffer, key=lambda t: t[0])[0] @@ -51,12 +51,10 @@ def update(self, att_val, target_val, w) -> Splitter: self._update_stats(0 if a <= self.threshold else 1, t, w) self._buffer = None - return self + return self._update_stats(0 if att_val <= self.threshold else 1, target_val, w) - return self - def best_evaluated_split_suggestion(self, criterion, pre_split_dist, att_idx, binary_only): post_split_dist = [self.stats[0], self.stats[1]] merit = criterion.merit_of_split(pre_split_dist, post_split_dist) diff --git a/river/utils/rolling.py b/river/utils/rolling.py index 19ed551b55..f662b941b4 100644 --- a/river/utils/rolling.py +++ b/river/utils/rolling.py @@ -8,9 +8,9 @@ @typing.runtime_checkable class Rollable(typing.Protocol): - def update(self, *args, **kwargs): ... + def update(self, *args, **kwargs) -> None: ... - def revert(self, *args, **kwargs): ... + def revert(self, *args, **kwargs) -> None: ... class BaseRolling: diff --git a/river/utils/sorted_window.py b/river/utils/sorted_window.py index d718158c6a..1dcdd6eeec 100644 --- a/river/utils/sorted_window.py +++ b/river/utils/sorted_window.py @@ -20,7 +20,8 @@ class SortedWindow(collections.UserList): >>> window = utils.SortedWindow(size=3) >>> for i in reversed(range(9)): - ... print(window.append(i)) + ... window.append(i) + ... print(window) [8] [7, 8] [6, 7, 8] @@ -45,7 +46,7 @@ def __init__(self, size: int): def size(self): return self.unsorted_window.maxlen - def append(self, x): + def append(self, x) -> None: if len(self) >= self.size: # The window is sorted, and a binary search is more optimized than linear search start_deque = bisect.bisect_left(self, self.unsorted_window[0]) @@ -53,5 +54,3 @@ def append(self, x): bisect.insort_left(self, x) self.unsorted_window.append(x) - - return self