River: online machine learning in Python, one sample at a time
🌊 Online machine learning in Python
At a glance
- What is it?
- River is a BSD-3-Clause Python library that merges creme and scikit-multiflow into a single streaming ML toolkit. It is the right tool when your model must update on every event, and the wrong tool when batch retraining already works.
- Who is it for?
- Adopt River if your model must consume events one at a time and adapt without revisiting stored history, and if you can accept a library that states it prioritises clarity and user experience over raw performance. Do not adopt it if periodic batch retraining in scikit-learn is already sufficient, or if you need a pandas-first workflow, since the core learn_one/predict_one path deliberately avoids pandas and the many-style API is an opt-in extra.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem River addresses: models that must learn from the event in front of them
Batch learning assumes you can collect a dataset, fit a model, and serve it until the next retraining cycle. River targets the case where that loop is too slow or too storage-heavy. The README frames the question directly: you should ask yourself if you need online machine learning, and the answer is likely no. It then lists three situations where an online approach fits. You want a model that learns from new data without revisiting past data. You want a model that is robust to concept drift. You want to develop the model closer to how production actually behaves, which the README describes as usually event-based. The audience is therefore narrow and specific: engineers running scoring services where each request or message carries a label, or a delayed label, and where the distribution moves. River is the merger of creme and scikit-multiflow, so it inherits two earlier attempts at the same problem rather than starting from a blank page. That history matters for adopters, because it means the API surface is broad: linear models, trees and forests, approximate nearest neighbours, anomaly detection, drift detection, recommenders, time series forecasting, bandits, factorization machines, imbalanced learning, clustering, ensembling, and active learning.
learn_one and predict_one: the interleaved update loop
The mechanism is visible in the quickstart. River's core interface is two methods, learn_one and predict_one, plus metrics that update one observation at a time. The README example builds a pipeline and runs it over the Phishing dataset:
model = compose.Pipeline(preprocessing.StandardScaler(), linear_model.LogisticRegression()) metric = metrics.Accuracy() for x, y in dataset: y_pred = model.predict_one(x) metric.update(y, y_pred) model.learn_one(x, y)
Three things are worth noting in that loop. First, the order is prediction, metric update, then learning. The model does not see the label before it predicts, which is what makes the reported accuracy an honest streaming estimate rather than a resubstitution score. Second, x is a plain dictionary of feature names to values in the dataset example, not a dataframe row. Third, the pipeline is a compose.Pipeline of a scaler and a linear model, both of which expose the same one-at-a-time methods, so preprocessing is incremental too. The scaler does not fit on a training set and freeze; it updates its statistics as samples arrive. The README reports Accuracy: 89.28% for that loop but explicitly calls it a contrived example, so treat the number as a demonstration of the API, not as a benchmark of the algorithm. The architectural consequence is that River models hold state and mutate it. There is no separate fit and predict phase, and no artifact that represents a frozen model unless you serialise the object yourself.
Installation and the pandas boundary
River requires Python 3.11 and above. Installation is pip install river, and the README states there are wheels for Linux, MacOS, and Windows, so building from source is usually unnecessary. Installing the development version from GitHub with pip install git+https://github.com/online-ml/river --upgrade requires Cython and Rust on the machine. The more interesting packaging decision is the pandas boundary. The README states that the core online interface, learn_one and predict_one, has no pandas dependency, while the mini-batch interface (learn_many, predict_many, predict_proba_many, transform_many) is built on pandas and is opt-in via pip install "river[pandas]". That is a deliberate split. If you are processing a Kafka topic or a websocket feed, you install the base package and never pull pandas into your dependency tree. If you have micro-batches and want to pass dataframes, you take the extra. The cost is that the two paths are separate code, and a pipeline you write against learn_one is not automatically the same object you call learn_many on. Check the API overview before assuming a given transformer or model supports both.
What the feature list does not settle for you
The README lists families of algorithms rather than specific classes, and that gap is where adoption risk lives. Drift detection is listed as a capability, but the README does not say which detectors ship, what statistic each one monitors, or how you are expected to react when a detector fires. In practice a drift detector is a signal, not a remedy: you still have to decide whether to reset the model, increase the learning rate, or swap in a new one, and River's documentation is the place to look for that, not the README. The same applies to time series forecasting and recommenders. River is broad, and breadth in a merged codebase usually means uneven depth. The README's own framing supports this reading: it says River focuses on clarity and user experience, more so than performance, and that it is very fast at processing one sample at a time. Those two statements are a trade-off, not a slogan. If your workload is a large batch scored nightly, River's per-sample design is the wrong shape for it. If your workload is a high-throughput stream where per-sample Python overhead dominates, the library's own stated priority tells you to measure before you commit.
River versus scikit-learn: different lifecycle, not different accuracy
The obvious alternative is scikit-learn, and the difference is not which one predicts better on a static split. scikit-learn's estimators implement fit and predict over an array or dataframe. You call fit once on a training set, then predict on new rows, and the model does not change until you call fit again. River's estimators implement learn_one and predict_one, and the model changes after every call. That changes four things downstream. Retraining becomes continuous rather than scheduled. Memory becomes bounded by the model's parameters rather than by the training set, because you never need to keep history to refit. Concept drift becomes something the model absorbs rather than something a monitoring job detects and then triggers a retrain for. And reproducibility becomes harder, because the model's state depends on the exact order and content of the stream it saw, not on a fixed dataset you can archive and rerun. If you need to explain a prediction months later, a batch model with a versioned training set is easier to defend than a model that has been updating since deployment. River does not remove that problem; it moves it. Progressive model validation, listed in the README's utilities, is the part of the library aimed at it.
Maintenance, releases, and the BSD-3-Clause licence
The repository is not archived, and recent releases are close together: 0.26.1 on 2026-08-21, 0.26.0 earlier the same day, and 0.25.0 on 2026-05-31. The 0.26.1 patch landing hours after 0.26.0 is a normal signal that a minor release needed a quick fix, and it also means you should pin versions rather than tracking the latest tag in production. The version numbers are still in the 0.x range, which by convention means the maintainers reserve the right to make breaking changes in minor releases; the README does not promise API stability, so read the release notes before upgrading a minor version. On licensing, River is BSD-3-Clause, a permissive licence that allows commercial and closed-source use and requires retaining the copyright notice and licence text. That is a description of the licence identifier, not legal advice; if you are embedding River in a distributed product, have your own counsel confirm the notice requirements. Maintenance effort for an adopter is mostly the cost of keeping up with 0.x releases and re-validating your pipeline against each one, plus the operational work of persisting model state, since a continuously updating model has no artifact unless you create one.
Who should adopt River, and what to check first
Adopt River when the event stream is the system of record and you cannot afford to store and revisit history, or when the distribution shifts fast enough that a scheduled retrain is always behind. The interleaved predict, measure, learn loop in the quickstart is the whole idea, and it maps cleanly onto a service that scores a request and then learns from the outcome when it arrives. Do not adopt it as a general replacement for scikit-learn. If nightly retraining on a stored dataset meets your accuracy target, River adds operational complexity, a stateful model you must persist, and a 0.x upgrade treadmill for no gain. Do not adopt it if you need a pandas-native pipeline end to end, because the core interface is deliberately pandas-free and the many-style methods are a separate, opt-in surface. Before you commit, run three checks against your own problem. Confirm Python 3.11 or above in your runtime. Open the API overview and verify that the exact estimator you need exists, rather than trusting the family-level feature list. And decide in advance what your drift detector should trigger, because River gives you the signal and leaves the response to you.
Editorial conclusion
Adopt River if your model must consume events one at a time and adapt without revisiting stored history, and if you can accept a library that states it prioritises clarity and user experience over raw performance. Do not adopt it if periodic batch retraining in scikit-learn is already sufficient, or if you need a pandas-first workflow, since the core learn_one/predict_one path deliberately avoids pandas and the many-style API is an opt-in extra. Before committing, verify three things against your own data: that Python 3.11 or above is available in your runtime, that the specific algorithm you need exists in the API overview rather than only in the feature list, and that your drift assumptions match what the drift detectors in the library actually measure.
Community notes