Skip to main content

datalets 🧩

Partition-then-fit machine learning, scikit-learn compatible.

Many tiles, one picture. Cluster your data into datalets, run a model tournament per datalet, route every prediction to the right specialist.

Real-world datasets rarely follow one pattern. A single global model fits the average of all the patterns in your data — datalets instead discovers the subpopulations (unsupervised, on X only), crowns the best classifier for each one, and routes every prediction to the specialist that owns it.

            ┌──────────────┐
   X ─────► │  clusterer   │────► datalet 1 ──► tournament ──► 🏆 XGBoost      (t=0.31)
            │  (any of 12) │────► datalet 2 ──► tournament ──► 🏆 LogReg       (t=0.55)
            └──────────────┘────► datalet 3 ──► tournament ──► 🏆 RandomForest (t=0.48)
                                  too small ──────────────────► global fallback

Install

pip install datalets                # core (scikit-learn models only)
pip install "datalets[boosters]"    # + XGBoost, LightGBM, CatBoost
pip install -e ".[dev]"             # from a clone, for development

Named after the data slices it carves — and pip install datalets, import datalets: one name everywhere.

Quickstart

from datalets import PartitionedClassifier

model = PartitionedClassifier(
    clusterer="auto",           # searches kmeans/gmm x k (up to 12) + hdbscan,
                                #   scored by predictive utility — fully automatic;
                                #   or a list like ["kmeans", "gmm"] to pick
                                #   which algorithms compete (skip slow ones)
    candidates="default",       # or "fast", "all", or your own list of classifiers
    class_weight="balanced",    # automated imbalance handling (weights, no resampling)
    routing="soft",             # blend specialists by P(cluster | x)
)
model.fit(X_train, y_train)     # no cluster tags needed — the partition is discovered

model.predict(X_test)          # routed to each point's specialist
model.predict_cluster(X_test)  # which datalet does each point belong to?
print(model.auto_search_)      # every partition tried, scored + silhouette/BIC
print(model.report())          # per-cluster: size, base rate, champion, lift
print(model.summary())         # verdict: does the partition earn its keep?
print(model.tree())            # the finish tree: the whole fit, one picture

Prefer manual control? clusterer="kmeans", n_clusters=6 (or "gmm", "hdbscan", … or any clusterer instance) pins the partition yourself.

fit narrates itself: numbered stages, a live progress bar over every (tournament, candidate) pair in interactive sessions, and the finish tree at the end — how the data was carved up, the clustering quality (sampled silhouette), which champion serves each segment at what threshold, which algorithms dominated, and the verdict:

datalets · PartitionedClassifier · fit in 3.4s
├─ data       1,200 rows × 5 features · 2 classes · positives 42.3%
├─ clustering kmeans → 3 clusters · silhouette 0.29 · sizes 398–402 (median 400)
├─ tournament 4 slices × 2 candidates · metric f1 · 8 CV races
├─ segments
│  ├─ cluster 0 · n=402 (34%) · pos 45.0% → ★ logistic_regression · f1 0.850 · thr 0.47 · lift +0.018
│  ├─ cluster 1 · n=400 (33%) · pos 45.8% → ★ decision_tree · f1 0.667 · thr 0.02 · lift +0.002
│  └─ cluster 2 · n=398 (33%) · pos 36.2% → ★ logistic_regression · f1 0.923 · thr 0.43 · lift +0.085
├─ fallback   decision_tree · f1 0.772 · safety net only
├─ champions  logistic_regression ×2 · decision_tree ×1
└─ verdict    ✓ partition earns its keep · routed OOF f1 0.803 vs global 0.772 · lift +0.0311

Non-interactive runs (logs, CI, servers) are never silent on long fits: throttled per-task completion lines plus a heartbeat that names the task currently blocking (⏳ still working · 96/104 done · waiting on global · random_forest). progress=False or DATALETS_QUIET=1 silences everything.

What you get

  • Clusterer zoo — all 12 scikit-learn clustering families by name (kmeans, minibatch_kmeans, bisecting_kmeans, gmm, bayesian_gmm, birch, agglomerative, spectral, hdbscan, dbscan, meanshift, optics), or bring your own instance.
  • Universal routing — clusterers that cannot label new points (Agglomerative, Spectral, DBSCAN, HDBSCAN, OPTICS) are made inductive with a KNN gate. DBSCAN/HDBSCAN noise points are served by the global model.
  • Model tournament per cluster — every candidate is scored with stratified out-of-fold CV inside the cluster; a one-standard-error rule breaks ties toward the simpler model. Candidate zoo spans scikit-learn's classifiers plus XGBoost / LightGBM / CatBoost when installed.
  • Per-cluster decision thresholds — clusters with different positive rates want different cutoffs; datalets tunes each cluster's threshold for your metric (F1 by default). This alone often lifts F1 on heterogeneous data.
  • Imbalance handled, two automated modes — thresholds alone are the F1-first mode; class_weight="balanced" adds inverse-frequency weights through fitting, thresholds and scoring (no resampling) for the recall-first mode: on a 0.4%-positive benchmark it catches 97% of positives with better ranking (ROC-AUC 0.999) than tuned boosters.
  • Soft routing — predictions blend all specialists by P(cluster | x), so nothing jumps discontinuously at cluster boundaries. routing="hard" if you want one specialist per point.
  • Global safety net — a tournament-selected global model serves clusters that are too small, too class-starved, or labelled as noise.
  • The honesty report — report() shows, per cluster, whether the specialist actually beats the global model on the same rows, and summary() gives a verdict. If the partition doesn't earn its keep, datalets says so instead of letting you ship it.
  • A fit you can watch — stage-by-stage narration with a live progress bar, and tree(): the post-fit finish tree showing the partition, the clustering quality, every segment's champion and the final verdict at a glance.
  • Temporal and grouped data — cv accepts any sklearn splitter (TimeSeriesSplit, StratifiedGroupKFold, …) or an explicit list of (train, test) folds, and fit(groups=...) keeps entity leakage out of every tournament; an integer cv combined with groups upgrades to a group-aware splitter automatically.
  • Operating-point objectives — deploy on the constraint your stakeholders speak: threshold_objective=("recall_at_precision", 0.8), ("precision_at_recall", ...), ("cost", {"fp": 1, "fn": 5}), or ("flag_rate", 0.02) (rank-based, exact per batch).
  • Missing values as a policy — missing="median" or "sentinel" (for NaNs that mean something), applied once to the single view every component sees, at fit and serve time alike.
  • Preemption-proof fits — checkpoint_dir=... caches every (slice, candidate) scoring under content-hash keys; a killed 30-hour fit resumes instead of restarting. estimate_cost(X, y) prints the wall-time plan before anything expensive runs.
  • MLflow flavor — pip install "datalets[mlflow]": save/log/load via pyfunc with the report, finish tree and segment narratives logged as artifacts.
  • Multi-scale ensembles — DataletEnsembleClassifier/Regressor fit one model per partition scale (k=3, 5, 8, …) and blend the votes — the configuration the cluster-then-predict literature found strongest.

Is the partition real? Prove it.

The in-fit report reuses training folds, so treat it as a screen. For a leakage-free comparison, everything (clustering, tournaments, thresholds) is refit inside every fold:

from datalets import honest_cv
print(honest_cv(X, y, datalets=PartitionedClassifier(n_clusters=6, random_state=0)))
#                          f1_mean  f1_std  roc_auc_mean  ...
# datalets                    0.912    0.011      0.965
# tournament_global         0.887    0.014      0.951
# hist_gradient_boosting    0.879    0.012      0.949

TournamentClassifier (the model-selection engine without clustering) is also exported — it is both the library's global fallback and the honest single-model baseline.

Regression too

PartitionedRegressor is the continuous-target twin: same clusterer zoo, same per-datalet tournaments (over a regressor zoo: Ridge/Lasso, KNN, forests, boosters, SVR, MLP + XGBoost/LightGBM/CatBoost), same soft/hard routing and honesty report — no thresholds, since there is nothing to threshold. selection_metric is one of "r2" (default), "mse", "rmse", "mae".

from datalets import PartitionedRegressor, honest_cv_regression

reg = PartitionedRegressor(n_clusters=6, candidates="default").fit(X_train, y_train)
print(reg.report())            # per-cluster: champion, cv_score, lift
print(honest_cv_regression(X, y, datalets=reg))

When does datalets help?

Partition-then-fit wins when your data genuinely contains subpopulations with different X→y rules — customer segments, operating regimes, sensor types, geographies. It will not beat a tuned gradient booster on homogeneous data (boosters already partition internally); the report will tell you when that is the case. What you always keep, even at parity: interpretable segments, one readable specialist per segment, and per-segment thresholds and diagnostics.

sklearn-compatible, for real

All six estimators pass scikit-learn's check_estimator suite — TournamentClassifier, TournamentRegressor and PartitionedRegressor with no exemptions at all; the partition classifiers carry a handful of documented exemptions rooted in two deliberate design decisions (the decision rule is a tuned threshold, not argmax — the same exemption scikit-learn grants its own TunedThresholdClassifierCV — and the X-only clustering is unweighted by design). Pipelines, GridSearchCV, cross_val_score, sample_weight, cloning and pickling all work:

from sklearn.model_selection import GridSearchCV

GridSearchCV(
    PartitionedClassifier(),
    {"n_clusters": [4, 6, 8], "clusterer": ["kmeans", "gmm"]},
    scoring="f1",
)

Lineage

datalets stands on a 45-year research line: clusterwise regression (Späth 1979), mixtures of experts (Jacobs, Jordan, Nowlan & Hinton 1991), finite mixtures of regressions (DeSarbo & Cron 1988; R's flexmix), local learning (Bottou & Vapnik 1992), and the cluster-then-predict practitioner pattern. See examples/demo.py for a dataset where the approach provably shines — and the report that tells you when it doesn't.

Metadata

Release files for datalets 1.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for datalets 1.0.2
File Size Uploaded Provenance Actions
datalets-1.0.2.tar.gz 105.9 kB With provenance Details for datalets-1.0.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for datalets 1.0.2
File Interpreter ABI Platform Actions
datalets-1.0.2-py3-none-any.whl Python 3 none any Details for datalets-1.0.2-py3-none-any.whl

Total release size: 185.5 kB

Release files / datalets-1.0.2.tar.gz

Download URL datalets-1.0.2.tar.gz
Size 105.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1523bb014f95b0285778cc6439497bc70d8a7b11619e14e2da8afc51cd5691d9
BLAKE2b-256 checksum
How to use checksums
159f9cb96ac625efa0284f19e24bda9fea671a3b0547ef09360e4140b684ad48
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 Jul 30, 2026.

Transparency log

Release files / datalets-1.0.2-py3-none-any.whl

Download URL datalets-1.0.2-py3-none-any.whl
Size 79.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cac0fdf8df53e820a3a197f67f232223c723e882ae23443e15ef87378ee63be9
BLAKE2b-256 checksum
How to use checksums
49ffd02c107c57ba9f62f32753a8e77e19a2a642e5624524ba8e638656526396
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 Jul 30, 2026.

Transparency log

Security for datalets 1.0.2

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for datalets-1.0.2.tar.gz

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 Jul 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page