fastforest
Fast approximate random-forest regression in Rust, with Python bindings.
Across six numeric and mixed-data regressions, fastforest is fastest to fit and predict in every completed comparison while retaining competitive accuracy. Both forests use 50 trees and otherwise use their default hyperparameters:
| Dataset | Model | RMSE ↓ | R² ↑ | Fit (s) ↓ | Predict (s) ↓ |
|---|---|---|---|---|---|
| SGEMM GPU 241,600 rows · 14 features 80/20 split |
fastforest | 0.05 | 1.00 | 0.17 | 0.021 |
| sklearn RF | 0.03 | 1.00 | 1.03 | 0.073 | |
| sklearn HistGBM | 0.20 | 0.97 | 1.23 | 0.022 | |
| California Housing 20,640 rows · 8 features 80/20 split |
fastforest | 0.49 | 0.81 | 0.08 | 0.003 |
| sklearn RF | 0.51 | 0.80 | 0.26 | 0.013 | |
| sklearn HistGBM | 0.47 | 0.83 | 0.96 | 0.006 | |
| Concrete Strength 1,030 rows · 8 features 80/20 split |
fastforest | 5.41 | 0.89 | 0.00 | 0.000 |
| sklearn RF | 5.52 | 0.88 | 0.05 | 0.014 | |
| sklearn HistGBM | 4.65 | 0.92 | 0.78 | 0.005 | |
| Diamonds 53,940 rows · 9 features 80/20 split |
fastforest | 551 | 0.98 | 0.17 | 0.008 |
| sklearn RF | 553 | 0.98 | 0.53 | 0.020 | |
| sklearn HistGBM | 541 | 0.98 | 1.02 | 0.018 | |
| Allstate Claims 188,318 rows · 130 features 80/20 split |
fastforest | 1,937 | 0.54 | 1.09 | 0.055 |
| sklearn RF | timed out at 180s | ||||
| sklearn HistGBM | 1,861 | 0.58 | 3.75 | 0.389 | |
| Diabetes 130-US Hospitals 101,766 rows · 46 features 80/20 split |
fastforest | 2.22 | 0.43 | 0.73 | 0.088 |
| sklearn RF | 2.21 | 0.44 | 2.98 | 0.132 | |
| sklearn HistGBM | 2.13 | 0.48 | 2.05 | 0.141 | |
Bold is best for that dataset and metric. Results use one fixed 80/20 split on a 16-core Apple M4 Max; fit includes preprocessing and FastForest's adaptive pilot. See Benchmarking for details and reproduction instructions.
Benchmarking
The table compares 50-tree FastForest and sklearn random forests with default sklearn HistGBM. Each dataset uses the same reproducible 80/20 split. FastForest's adaptive default selected 90% of features for SGEMM and Diamonds and 60% for California Housing, Allstate, and Diabetes. Concrete retained the 75% fallback because its training split has fewer than 8,000 rows.
For mixed data, sklearn RF uses median imputation plus missing indicators for numeric columns, one-hot encoding through 20 categorical levels, and target encoding above 20. HistGBM uses native categoricals through its 255-level limit and target encoding above that. Fit timing includes model construction, schema inspection, preprocessing, and fitting, but excludes process startup and inter-process transfer. Prediction timing includes input transformation. Every model/dataset combination has a 180-second limit; sklearn RF reached it on Allstate. The SGEMM target is the log-transformed mean runtime.
Install the development dependencies and release build, then reproduce one dataset with:
pip install -e '.[dev]'
maturin develop --release
python tools/accuracy.py --dataset california
Available datasets are sgemm, california, concrete, diamonds, allstate, and diabetes. Run FastForest alone with --ff_only, or reproduce the complete table with:
for dataset in sgemm california concrete diamonds allstate diabetes; do
python tools/accuracy.py --dataset "$dataset"
done
Install
pip install fastforest
Usage
import numpy as np
from fastforest import FastForest
rng = np.random.default_rng(42)
X = rng.random((1_000, 6))
y = 4*X[:, 0] - 2*X[:, 1] + X[:, 5]
model = FastForest(seed=42, oob=True).fit(X, y)
predictions = model.predict(X[:5])
oob_predictions = model.oob_prediction_
oob_counts = model.oob_counts_
X may contain numeric values, numeric strings, ordinary strings, and configured missing values. y is converted to contiguous float32; it must have one finite value per row.
Data preparation
FastForest fits a deterministic schema for every input column:
- Non-missing values are parsed as
float32when every value can be parsed and are otherwise treated as strings. Numeric columns sort numerically and other columns sort lexically. Numeric columns whose values are all integral retain that metadata so analysis displays them with no decimal places. - A column with more than
max_dummy_cardinalitydistinct values is replaced during training by its zero-based rank in that sort order. A column with cardinalityc <= max_dummy_cardinalitybecomesc-1boolean dummy features; the least-common value is omitted as the all-zero case. Frequency ties are resolved deterministically by sort order.max_dummy_cardinalitydefaults to 4. - The default missing value is the empty value. Override it per column with
missing_values, using column names or indexes. When training contains a missing value, FastForest adds<column>_missingand fills the value feature with the observed median. A column containing no training missing values rejects missing values during prediction rather than silently inventing an imputation rule. Entirely missing columns are discarded.
X = np.array([
["18", "red", ""],
["42", "blue", "3.5"],
["31", "green", "2.0"],
], dtype=object)
model = FastForest(missing_values={2: ""}).fit(X, [1, 4, 3])
Ranking is a compact training representation, not a prediction-time requirement for numeric columns. After fitting, rank cutoffs are converted back to native numeric boundaries, so seen and unseen numeric values are compared directly without a rank lookup. Nonnumeric values are mapped through their fitted lexical ordering. An unseen low-cardinality value naturally receives all-zero dummies; an unseen high-cardinality value receives its insertion rank. Missing checks and median routing are retained only for columns that contained missing training values.
Schema fitting and inference transformation run natively in Rust and parallelize independent columns with Rayon. Python only adapts NumPy and data-frame column buffers and retains display metadata for the analysis API. Pandas categorical columns pass their integer codes and vocabulary directly rather than being expanded into Python object arrays.
Generated ranks, dummies, and missing indicators remain internal. Feature importance, explanations, and partial-dependence results aggregate them back to the original column and display its original values. Fitted interpretations are available in model.column_info_.
For reproducible sklearn comparisons on the same raw dataframe, sklearn_preprocessor implements the policy used by the benchmark: numeric median imputation with missing indicators, one-hot encoding through 20 categorical levels, target encoding above 20, and removal of empty columns.
from sklearn.ensemble import RandomForestRegressor
from sklearn.pipeline import make_pipeline
from fastforest import sklearn_preprocessor
preprocess = sklearn_preprocessor(X_train, missing_values={"age":"?"})
model = make_pipeline(preprocess, RandomForestRegressor(n_estimators=50, n_jobs=-1))
model.fit(X_train, y_train)
Install the optional dependencies with pip install 'fastforest[sklearn]'.
Algorithm
Each tree draws min(floor(bootstrap_fraction * n_rows), bootstrap_max) training rows, with replacement when replacement=True and otherwise without it. When bootstrap_fraction=None, it resolves to 0.8 with OOB enabled and 1 otherwise. Fractions above 1 are supported with replacement; without replacement the maximum is 1. Pass bootstrap_max=None to disable the cap. At each node, the default histogram splitter:
- A node with fewer than
min_node_sizerows, or whose firstmax_node_samplessampled targets are equal, becomes a leaf. - A random contiguous window containing at most
max_node_samplesof the node's shuffled rows is selected. - The tree randomly selects
floor(max_features * n_features)feature units, with a minimum of one. Encoded features are independent units except that every numeric value feature and its missingness indicator form one atomic unit. - For each selected feature, the sampled rows are sorted by their encoded rank and every distinct observed boundary is evaluated. The split that most improves size-weighted negative sample standard deviation is selected, subject to the sampled child-size minimum.
- Every terminal leaf predicts the mean target of all training rows that reached it, including leaves where candidate evaluation found no useful split.
The defaults are 50 trees, minimum node size 4, all rows capped at 40,000 without replacement, histogram splitting over 75% of feature units, at most 320 evaluated rows per node, and unregularized leaf means. Enabling OOB changes the default sampling fraction to 0.8 so every row can receive held-out predictions. Preprocessing and trees build in parallel over columns and trees respectively, while batch predictions run in parallel over rows. Supplying seed makes the fitted forest deterministic regardless of parallel scheduling.
For more than 8,000 training rows, adaptive=True selects a feature fraction of 0.6 or 0.9 while keeping max_node_samples=320. It draws one fixed 8,000-row pilot sample and fits max(2*n_threads, 32) trees per candidate, sampling 50% of the pilot rows without replacement in each tree. Candidates use matching seeds and in-bag rows and are compared by mean squared OOB error; an exact tie favors 0.6. The selected pair is available as adaptive_choice_, and both (max_features, max_node_samples, oob_mse) results are in adaptive_scores_. Set adaptive=False to use workbench.max_features directly.
Tree-building workbench
Workbench keeps interchangeable tree-building choices separate from the forest parameters. Its defaults reproduce the histogram algorithm above; the original random-cutoff search remains available for experiments and comparisons:
from fastforest import FastForest,Workbench
alternate = Workbench(
splitter="random",
max_features="sqrt",
leaf_regularization=0,
)
model = FastForest(workbench=alternate, seed=42).fit(X, y)
splitter="histogram" is the production default. It randomly selects max_features, builds sparse target-statistic histograms from the node evaluation window, and checks every observed boundary for those features. splitter="random" proposes random (feature, value) cutoffs, deduplicates them, and evaluates them on the same kind of node window. Its candidate count is controlled by min_candidate_rows, candidate_attempt_factor, and cutoff_divisor. max_features accepts "sqrt", "all", a fraction in (0, 1], or a positive feature count and is ignored by the random splitter.
leaf_regularization shrinks each terminal full-node mean towards its parent node mean, treating the value as a number of parent pseudo-rows. Zero selects the unregularized production mean. Split search and leaf regularization are independent, so every splitter, feature selection, and regularization combination can be tested without adding branches to the forest API.
The focused sweep tool takes comma-separated workbench grids. Random splitting ignores the histogram-only max_features grid rather than running duplicate configurations:
python tools/sweep.py --dataset california \
--splitters random,histogram --max_features sqrt,0.5,all \
--leaf_regularizations 0,2,8,32
Out-of-bag predictions
OOB calculation is opt-in with oob=True. After fitting:
oob_prediction_contains each training row's mean prediction from trees that did not sample that row.oob_counts_contains the number of contributing trees.- A row with no contributing tree has count zero and prediction
NaN. - Sampling without replacement at
bootstrap_fraction=1.0leaves no OOB rows, so all counts are zero and predictions areNaN.
Both attributes are None when OOB is disabled.
Model analysis
FastForest includes NumPy-only analysis tools. Data frames are accepted and supply feature names automatically; arrays use x0, x1, and so on. Plot methods import matplotlib only when called.
Importance
Use validation-set permutation importance by default. It measures the drop in model score after shuffling a feature without retraining:
importance = model.feature_importance(X_valid, y_valid)
importance.sorted()
importance.plot()
Correlated features can substitute for one another and therefore look individually unimportant. Permute them together to measure their joint importance:
importance = model.feature_importance(X_valid, y_valid,
features={"location": ["latitude", "longitude"]})
model.drop_column_importance(X_train, y_train, X_valid, y_valid) performs the slower complementary analysis: it refits the forest without each feature. It accepts the same features groups. model.split_importance() returns the nearly free, normalized training-time split-gain measure, but permutation or grouped permutation is preferable because split importance is biased by the available cutoffs and correlated predictors.
Individual predictions and uncertainty
explanation = model.explain(X_valid[:3])
explanation.row(0) # (feature, observed value, contribution), strongest first
explanation.plot(0)
tree_predictions = model.predict_trees(X_valid)
prediction_std = model.predict_std(X_valid)
For every row, prediction = bias + contributions.sum(). Contributions telescope through each tree's decision path and are then averaged across trees. They explain this forest's computation, not causality; correlated features can redistribute contributions between themselves.
Partial dependence and ICE
year = model.partial_dependence(X_train, "year_made")
year.plot() # average PDP plus individual conditional-expectation lines
year.plot(centered=True)
year.plot(clusters=5) # representative centered ICE curves
interaction = model.partial_dependence(X_train, ["year_made", "sale_year"])
interaction.plot()
enclosure = model.partial_dependence(X_train,
{"enclosure": ["enclosure_ac", "enclosure_erops", "enclosure_orops"]})
Partial dependence repeatedly replaces the selected feature values and averages the resulting predictions. ICE retains the individual prediction lines. These plots describe the fitted model rather than a causal intervention, and highly correlated features can produce unrealistic synthetic rows.
Collinearity and redundancy
from fastforest import feature_dependence,feature_relations
relations = feature_relations(X_train)
relations.groups(threshold=0.2)
relations.plot()
relations.plot_dendrogram()
dependence = feature_dependence(X_train)
dependence.predictability # validation R² for predicting each feature from the others
dependence.plot() # which other features provide that predictive information
feature_relations uses tie-aware Spearman correlation and average linkage implemented directly with NumPy. feature_dependence detects nonlinear redundancy by treating each feature in turn as a target, fitting a small forest from the remaining features, and measuring grouped prediction and permutation dependence.
Development
The project is locally installed with maturin until it joins the aai-ws workspace:
maturin develop
cargo test
pytest -q
For performance work, build the extension in release mode and run the benchmark:
maturin develop --release
python tools/bench.py
Compare accuracy and timings against sklearn's random forest and histogram GBM on one fixed California Housing split:
python tools/accuracy.py
Use --dataset concrete for the smaller Concrete Compressive Strength regression dataset, or --dataset sgemm for the 241,600-row SGEMM GPU Kernel Performance dataset. Every dataset uses one reproducible 80/20 split. Each model/dataset combination runs in an isolated process with a three-minute timeout; process startup and input transfer are excluded from reported timings.
Use --ff_only with --min_node_size, --bootstrap_fraction, --bootstrap_max, --replacement, --max_node_samples, and --cutoff_divisor for focused FastForest experiments. These spellings come directly from the call_parse function parameters.
Release files for fastforest 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastforest-0.1.0.tar.gz | 61.2 kB | Details |
Built distributions (wheels)
Total release size: 3.8 MB
Release files / fastforest-0.1.0.tar.gz
| Download URL | fastforest-0.1.0.tar.gz |
|---|---|
| Size | 61.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b700918530370e6f6c39607324e3f851b42097dfd0c9ff1acf553958c0b85fbd
|
|
BLAKE2b-256 checksum How to use checksums |
cb595783ebad3a2c58687facb5c90590c2f0a00edc6ae9c8e63717f33ca66394
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / fastforest-0.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | fastforest-0.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 492.4 kB |
| Tags | CPython 3.13 Linux glibc 2.17+ x86-64 |
|
SHA-256 checksum How to use checksums |
ad1b7233735718997afd87e3328d263d02b62daf750fe69943d577b3fa7f1a36
|
|
BLAKE2b-256 checksum How to use checksums |
52648be3d6e37c4453c4fd29052baf004e5b9289724dd5050159b26a2052ba29
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / fastforest-0.1.0-cp313-cp313-macosx_11_0_arm64.whl
| Download URL | fastforest-0.1.0-cp313-cp313-macosx_11_0_arm64.whl |
|---|---|
| Size | 448.4 kB |
| Tags | CPython 3.13 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
2a3fe8a476a5d4ba7bcbfefef57e2ddf64d8eeb3a0b9beb8e9a648df07eee06c
|
|
BLAKE2b-256 checksum How to use checksums |
043f2dc875bc81deae0c38152c77fb06c53fe2d1aad1f43df1d8e6e38c1999d1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / fastforest-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | fastforest-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 492.6 kB |
| Tags | CPython 3.12 Linux glibc 2.17+ x86-64 |
|
SHA-256 checksum How to use checksums |
1e7dc6acc7e1adcb4ebade485ef03afec4d337a9416b4d5aad7b76b216f3d156
|
|
BLAKE2b-256 checksum How to use checksums |
e1fc590607244734a8c3132cc8c5d928e304d9c48b96fee071409fe2bb0e3433
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / fastforest-0.1.0-cp312-cp312-macosx_11_0_arm64.whl
| Download URL | fastforest-0.1.0-cp312-cp312-macosx_11_0_arm64.whl |
|---|---|
| Size | 448.5 kB |
| Tags | CPython 3.12 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
295b5821e192a905c821d42da9aaa90728acfd693951acc5c3de7b537431df09
|
|
BLAKE2b-256 checksum How to use checksums |
fca80e7e43e6829f2013f7678f8fdbc621821fcd798dfbd93b28e72e9d30e968
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / fastforest-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | fastforest-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 493.5 kB |
| Tags | CPython 3.11 Linux glibc 2.17+ x86-64 |
|
SHA-256 checksum How to use checksums |
54ed1eced0c6dee8268a973bc4864bc3155e9ae63b83001f8998d21ce7fbf98d
|
|
BLAKE2b-256 checksum How to use checksums |
139b119668f83a3739793885f489d2e4dc920fe98809b9a4a5586c70dd0ef5c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / fastforest-0.1.0-cp311-cp311-macosx_11_0_arm64.whl
| Download URL | fastforest-0.1.0-cp311-cp311-macosx_11_0_arm64.whl |
|---|---|
| Size | 452.0 kB |
| Tags | CPython 3.11 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
b84195bbdbe06aeee05897faf9660a00a8d873da50f4683cf97d5a27e330ce8b
|
|
BLAKE2b-256 checksum How to use checksums |
97ca50464f855e65f6f8a471c3eadfd18beba449a5830e5b7a49ce5fa409f44e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / fastforest-0.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | fastforest-0.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 493.7 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 |
|
SHA-256 checksum How to use checksums |
1f7c4d9812ddbc8a2a020bef7f050722e56dfb9391560e2a5bdd6fb130a25a4f
|
|
BLAKE2b-256 checksum How to use checksums |
07e2098d09c099e42e7cb3fcb2ffa51d94f644b7590b87ea4d491dbedc086317
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency logRelease files / fastforest-0.1.0-cp310-cp310-macosx_11_0_arm64.whl
| Download URL | fastforest-0.1.0-cp310-cp310-macosx_11_0_arm64.whl |
|---|---|
| Size | 452.2 kB |
| Tags | CPython 3.10 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
0e27f0c8784834dc80c0b8d4806a909481a1251301cdc5e890a937c9381b755d
|
|
BLAKE2b-256 checksum How to use checksums |
780f9bc759fe4dc7d6e054e377bee64988354b966b21fda977f638a42e335e6a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 15, 2026.
Transparency log