Metadata-Version: 2.4
Name: datalets
Version: 0.5.0
Summary: Partition-then-fit machine learning: cluster your data into datalets, run a model tournament per datalet, route every prediction to the right specialist. scikit-learn compatible.
Author: Nashit
License: MIT
Project-URL: Repository, https://github.com/nashit8421/datalets
Project-URL: Documentation, https://github.com/nashit8421/datalets/blob/main/DOCUMENTATION.md
Project-URL: Changelog, https://github.com/nashit8421/datalets/blob/main/CHANGELOG.md
Keywords: machine-learning,clustering,mixture-of-experts,cluster-then-predict,clusterwise,scikit-learn
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.10
Requires-Dist: scikit-learn>=1.4
Requires-Dist: pandas>=2.0
Requires-Dist: joblib>=1.3
Provides-Extra: boosters
Requires-Dist: xgboost>=2.0; extra == "boosters"
Requires-Dist: lightgbm>=4.0; extra == "boosters"
Requires-Dist: catboost>=1.2; extra == "boosters"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file

# 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

```bash
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

```python
from datalets import PartitionedClassifier

model = PartitionedClassifier(
    clusterer="kmeans",      # or "gmm", "hdbscan", ... or any clusterer instance
    n_clusters=6,
    candidates="default",    # or "fast", "all", or your own list of classifiers
    routing="soft",          # blend specialists by P(cluster | x)
)
model.fit(X_train, y_train)

model.predict(X_test)          # routed to each point's specialist
model.predict_cluster(X_test)  # which datalet does each point belong to?
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
```

`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) get a few plain stage lines instead;
`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.
- **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.

## 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:

```python
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"`.

```python
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

`PartitionedClassifier` and `TournamentClassifier` pass scikit-learn's
`check_estimator` suite (with the same single documented exemption as
sklearn's own `TunedThresholdClassifierCV`: tuned thresholds mean `predict`
is not `argmax(predict_proba)`). Pipelines, `GridSearchCV`,
`cross_val_score`, cloning and pickling all work:

```python
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.
