Quick Reference · resampling & imbalanced classification

imbalanced-learn cheat sheet

Every technique attacks the skew at one of four levels: the data (over- or under-sample), the algorithm (reweight the loss), or the decision (move the threshold). imblearn owns the first two. Learn the map once — and learn where resampling is allowed to happen — and the 31 estimators stop being a list to memorize.

foundations over-sample under-sample combine & ensemble pipeline & metrics trap most common

Distilled & cross-checked against: imbalanced-learn.org (API reference · User Guide · Common Pitfalls, v0.14.x) · scikit-learn.org · the original JMLR paper · machinelearningmastery.com

The four levels & where imblearn is allowed to act
IMBLEARN'S TERRITORY THE RULE THAT MAKES IT VALID Skewed y 99 : 1 Counter(y) 1 · DATA — grow the minority SMOTE · ADASYN · Borderline · RandomOverSampler 2 · DATA — shrink the majority RandomUnderSampler · TomekLinks · ENN · NearMiss 3 · ALGORITHM — reweight the loss class_weight · BalancedRF · BalancedBagging · RUSBoost 4 · DECISION — move the threshold TunedThresholdClassifierCV  — sklearn's job, not imblearn's …and level 4 is free, invents no data, and often enough on its own but only THE GATE resample the TRAIN FOLD ONLY imblearn.pipeline.Pipeline Score on the NATURAL skew balanced_accuracy average_precision (PR-AUC) geometric_mean_score never plain accuracy skip the gate → synthetic points leak into your test fold → CV score is a fantasy, production score is not THE ONLY HONEST NUMBER

Foundations & contracts

Four things to know before you touch a sampler. Card 03 is the one everybody skips and then loses an afternoon to.

01Install & Import Surfacenine modules
  • pip install imbalanced-learn # NOT "imblearn"
    Package name has hyphens; the import name doesn't. There's a squatter named imblearn on PyPI — don't install it.
  • from imblearn.over_sampling import SMOTE
    8 classes — grow the minority.
  • from imblearn.under_sampling import RandomUnderSampler
    11 classes — shrink the majority.
  • from imblearn.combine import SMOTEENN, SMOTETomek
    2 classes — over-sample, then clean up the mess.
  • from imblearn.ensemble import BalancedRandomForestClassifier
    4 classifiers that resample internally, per estimator.
  • from imblearn.pipeline import Pipeline, make_pipeline
    The one you must not skip. See card 15.
  • from imblearn.metrics import classification_report_imbalanced
    8 metrics scikit-learn doesn't ship.
  • from imblearn.datasets import make_imbalance, fetch_datasets
    Skew a balanced set, or pull 27 real benchmarks.
  • from imblearn import FunctionSampler
    Wrap any (X,y) → (X,y) function into a sampler.
02Diagnose Firstbefore you fix
Half of "imbalanced" problems are 80:20 and need nothing but a better metric. Resampling starts earning its keep past roughly 1:20.
  • from collections import Counter
    Counter(y) # {0: 9900, 1: 100}
    The two-second diagnostic. Run it before and after every sampler.
  • IR = n_majority / n_minority # = 99
    Imbalance Ratio. Report it — it frames every decision downstream.
  • make_imbalance(X, y, sampling_strategy={0: 100, 1: 50})
    Deliberately skew a balanced dataset — for tests and demos.
  • fetch_datasets()['ecoli'] # 27 sets, IR 9 → 130
    Real benchmarks from Zenodo. Sanity-check a method before you trust it.
  • # the count matters more than the ratiothink
    1:1000 with 5,000 positives is a fine problem. 1:10 with 12 positives is not a modelling problem — it's a data-collection problem, and SMOTE will happily invent 100 fictional points from those 12.
03The sampling_strategy Contractread it once, properly
Every sampler takes it. It's the single biggest source of confusion in the library — because 'auto' means different things for over- and under-samplers, and because the float is a target ratio, not a multiplier.
TypeMeaning & gotchas
float Desired minority : majority ratio after resampling.
Over: αos = Nrm / NM  ·  Under: αus = Nm / NrM
Binary only — raises on multi-class. Rejected by every cleaning sampler.
str 'minority' · 'not minority' · 'not majority' · 'all' · 'auto'
'auto' is two behaviours: over-samplers → 'not majority'; under-samplers → 'not minority'.
dict {class: n_samples_after} — absolute counts, not deltas. The only sane option for multi-class.
callable f(y) → dict. Recomputed per fold — the right way to say "bring every class to 25% of the majority" inside CV.
  • SMOTE(sampling_strategy=0.5) # 900/100 → 900/450
    Minority grows to half the majority. Majority untouched.
  • RandomUnderSampler(sampling_strategy=0.5) # → 200/100
    Same 0.5, opposite bar moves. Majority shrinks to 2× minority.
  • SMOTE(sampling_strategy=0.1) # data already at 0.11raises
    The float trap. A target below the current ratio is a ValueError, not a no-op. Check Counter(y) first.
  • check_sampling_strategy(strategy, y, 'over-sampling')
    Resolves any input into the exact {class: n} dict it will produce — without running the sampler. Your debugger for this card.
04The Sampler APIfit_resample, nothing else
Samplers are not transformers. They change the number of rows, so they cannot implement transform() — which is precisely why sklearn's Pipeline rejects them.
  • X_res, y_res = sampler.fit_resample(X, y)
    The only resampling entry point. len(X_res) != len(X). pandas in → pandas out.
  • sampler.sampling_strategy_ # resolved dict
    Post-fit: exactly how many samples per class were added or removed.
  • sampler.sample_indices_
    Only on selection-based samplers (RUS, Tomek, ENN, NearMiss, CNN…). Absent on generative ones (SMOTE, ADASYN, ClusterCentroids) — an invented point has no original index to point at.
  • sklearn.pipeline.Pipeline([('smote', SMOTE()), …])TypeError
    All intermediate steps should be transformers. Use imblearn.pipeline.Pipeline. Samplers also cannot live inside a ColumnTransformer.

Over-sampling — grow the minority

Keeps every row of information. Risks overfitting the minority, and corrupts your predicted probabilities. Every SMOTE variant is a k-NN, so scale your features first.

05RandomOverSamplerthe honest baseline
Duplicates minority rows at random. Crude — and it beats SMOTE more often than the literature admits. It never invents a point that doesn't exist.
  • RandomOverSampler(random_state=42)
    Pure duplication. Roughly equivalent to per-sample weighting for most learners.
  • RandomOverSampler(shrinkage=0.2)
    Smoothed bootstrap: adds Gaussian noise around each duplicate so they aren't exact clones. None (default) = exact copies. Numeric only.
  • # the only over-sampler that takes any dtypeunique
    Strings, categoricals, mixed DataFrames, sparse — all fine. Every SMOTE variant needs a distance metric; this one doesn't. When your features are messy, this is your option.
06SMOTEinterpolate, don't clone
For each minority point, pick one of its k nearest minority neighbours and drop a synthetic point on the segment between them. See the geometry diagram below.
  • SMOTE(k_neighbors=5, random_state=42)
    Lower k → tighter, more local synthesis. Raise it when the minority is sparse and scattered.
  • # needs ≥ k_neighbors + 1 minority samplesraises
    Expected n_neighbors <= n_samples. You cannot synthesise from 4 points.
  • StandardScaler() SMOTE() # in that orderorder
    It's a Euclidean k-NN. Unscaled, your largest-magnitude feature silently owns the entire distance metric.
  • SMOTE on OneHotEncoder outputnonsense
    Produces is_married = 0.41. Use SMOTENC on the raw columns — card 08.
07The SMOTE Familysame maths, different where
Vanilla SMOTE synthesises uniformly — including deep in the safe interior, where new points teach the model nothing. Every variant answers one question: where should the budget go?
  • BorderlineSMOTE(kind='borderline-1', m_neighbors=10)
    Only from minority points in danger — mostly-majority neighbourhoods, but not all majority (those are labelled noise and skipped). 'borderline-2' also interpolates toward a majority neighbour.
  • SVMSMOTE(m_neighbors=10, out_step=0.5)
    Synthesises around the minority support vectors. The one variant that can extrapolate outward when a region is sparse. Sharper boundary; slower.
  • KMeansSMOTE(cluster_balance_threshold=0.1)
    Clusters first, then over-samples only minority-dominated clusters. Fixes the "synthesise straight through a majority island" failure. Will raise if no cluster qualifies.
  • ADASYN(n_neighbors=5)
    Weights each minority point by how many majority neighbours it has — hardest points get the most children.
  • # ADASYN amplifies label noise, by designcare
    A mislabelled minority point sitting in majority territory is exactly what it targets. Never run it on data you haven't de-noised.
  • # rule of thumb
    Clean data → BorderlineSMOTE. Noisy data → plain SMOTE, or SMOTEENN to sweep up after.
08Categorical FeaturesSMOTENC · SMOTEN
Vanilla SMOTE on one-hot columns produces gender = 0.63. These two exist so you never do that.
  • SMOTENC(categorical_features=[0, 3, 7])
    Mixed continuous + categorical. Interpolates the continuous columns; takes the majority vote among the k neighbours for each categorical one.
  • SMOTENC(categorical_features="auto")
    Auto-detects category/object dtypes — DataFrame input only. Otherwise pass indices or a boolean mask.
  • SMOTEN(k_neighbors=5)
    All-categorical X. Neighbours found via the Value Difference Metric; every new value is a mode vote. No interpolation happens at all.
  • # SMOTENC needs ≥1 continuous featureraises
    All-categorical X is SMOTEN's job. And feed both the raw columns — encode after resampling, inside the pipeline.

Under-sampling — shrink the majority

Throws information away — but it's fast, it never invents data, and it never corrupts your probabilities. Card 09 is the distinction the docs bury, and it explains most "why was my sampling_strategy ignored?" confusion.

09Controlled vs Cleaningthe split that organises everything
ControlledCleaning
You name the target. It hits it exactly. A heuristic decides what dies. You cannot name a count.
RandomUnderSampler
NearMiss
ClusterCentroids
InstanceHardnessThreshold
TomekLinks
ENN / RepeatedENN / AllKNN
CondensedNearestNeighbour / OneSidedSelection
NeighbourhoodCleaningRule
Takes float, dict, str, callable. Takes str / list only — which classes are eligible for removal. A float or dict raises.
Result is balanced. Result is still imbalanced, just less noisy. Removing 94 of 9,900 rows is a normal outcome.
Cleaning samplers are not a balancing tool — they're a noise-removal tool. Their real home is the second half of SMOTEENN / SMOTETomek, or as a pre-step before a controlled sampler.
10RandomUnderSamplerbrutal · fast · hard to beat
  • RandomUnderSampler(random_state=42)
    Drops majority rows at random until balanced. O(n). Any dtype, sparse included.
  • RandomUnderSampler(replacement=True)
    Bootstrap instead of a subset. This is the sampler running inside BalancedBagging and EasyEnsemble — which is how you get RUS's speed without throwing data away.
  • # the right default when you're data-richtry first
    500k negatives, 5k positives? Dropping to 5k+5k costs you nothing you'd have learned anyway, trains 50× faster, and dodges every SMOTE pathology.
  • # destructive when the majority is smallcare
    You're discarding real signal to fix a ratio. Use a balanced ensemble (card 13) — same balanced subsets, but every majority row is seen by some estimator.
11Tomek Links & ENNcleaning · erase the fuzz
A Tomek link is a pair of opposite-class points that are each other's nearest neighbour — something is wrong there. ENN is blunter: delete any point its neighbours disagree with.
  • TomekLinks()
    Default 'auto' removes only the majority member of each link. sampling_strategy='all' removes both — a genuinely different, more aggressive operation.
  • EditedNearestNeighbours(n_neighbors=3, kind_sel='all')
    Remove a point if any of its 3 neighbours disagrees ('all', aggressive) or if most do ('mode', gentle).
  • RepeatedEditedNearestNeighbours(max_iter=100)
    Runs ENN until nothing more is removed. Peels the boundary layer by layer.
  • AllKNN(allow_minority=False)
    ENN with k ramping 1→n. The most thorough of the three — and the most likely to over-clean.
12NearMiss & the Specialistsdistance-driven selection
  • NearMiss(version=1, n_neighbors=3)
    Keep majority points with the smallest mean distance to their k nearest minority points. Hugs the boundary — very sensitive to minority noise.
  • NearMiss(version=2)
    Distance to the k farthest minority points instead. Effectively picks majority points near the minority centroid. More stable than v1.
  • NearMiss(version=3, n_neighbors_ver3=3)
    Two-stage: shortlist each minority point's M nearest majority neighbours, then keep those farthest from the minority. High recall, low precision. The most defensible of the three.
  • ClusterCentroids(voting='auto')
    Prototype generation — replaces the majority with k-Means centroids (synthetic, not real rows). voting='hard' snaps each centroid to the nearest real sample.
  • InstanceHardnessThreshold(estimator=…, cv=5)
    Cross-validates a classifier, then drops the majority points it's least confident about. A learned, model-aware cleaner.
  • CondensedNearestNeighbour() · OneSidedSelection() · NeighbourhoodCleaningRule()
    CNN keeps only majority points a 1-NN would misclassify (noise-sensitive — noise is misclassified, so it gets kept). OSS = CNN then TomekLinks, explicitly to fix that. NCR = ENN, plus cleaning around minority points.
  • # CNN / OSS / NCR are ~O(n²)slow
    1968-era algorithms designed for hundreds of rows. Past ~50k, use RUS or a balanced ensemble.

Combine & ensemble

Where the library actually earns its keep. Card 14 is the strongest single default in imblearn — and card 16 is the free baseline you must beat before you're allowed to claim SMOTE helped.

13Balanced Ensemblesunder-sample without losing data
The elegant answer to RUS's weakness: under-sample many times, differently, and ensemble. Every majority row lands in some bag.
  • BalancedBaggingClassifier(estimator=HistGradientBoostingClassifier(), n_estimators=10)
    The general form — wrap any classifier. Swap the internal sampler with sampler=NearMiss() etc. The most flexible tool in the library.
  • EasyEnsembleClassifier(n_estimators=10)
    BalancedBagging pinned to AdaBoost learners. That's the entire difference.
  • RUSBoostClassifier(n_estimators=200)
    Boosting, not bagging: random under-samples before each boosting round. Strong at very high IR; sequential, so no n_jobs speed-up.
14BalancedRandomForeststart here
A forest where every tree grows on its own balanced bootstrap. No synthetic data, no discarded data — across 200 trees the whole majority class gets seen.
  • BalancedRandomForestClassifier(
      n_estimators=200,
      sampling_strategy="all",
      replacement=True,
      bootstrap=False,
      random_state=42)
    Write all four arguments out. This is the canonical Chen–Liaw–Breiman algorithm, and what the official docs use in every example.
  • # the default-drift trappin them
    The defaults for sampling_strategy, replacement and bootstrap were all migrated across 0.11 → 0.13 behind FutureWarnings. Code relying on defaults silently changed algorithm between versions. Pin them and the problem vanishes.
  • # why it's the best defaultstrong
    One line. No distance metric to break. Multi-class native. Gives feature_importances_. And it beats most SMOTE + classifier pipelines on tabular data.
15SMOTEENN & SMOTETomeksynthesise, then sweep up
SMOTE happily interpolates a new point straight into majority territory. These two over-sample, then run a cleaner over the result to delete exactly those mistakes.
  • SMOTEENN(random_state=42)
    SMOTE → ENN. The aggressive one: ENN's default kind_sel='all' removes misclassified points from every class, so it deletes a lot — including some synthetic points it just made. Usually the better performer.
  • SMOTETomek(random_state=42)
    SMOTE → TomekLinks. Gentler; only deletes mutually-nearest opposite pairs.
  • SMOTEENN(smote=BorderlineSMOTE(),
             enn=EditedNearestNeighbours(sampling_strategy='majority'))
    Both stages are swappable. Restricting ENN to 'majority' stops it eating your fresh synthetic points — a very common tweak.
  • Counter(y_res) # still uneven — correct!
    The cleaning stage runs after balancing and removes rows from both classes. Not a bug.
16class_weightnot imblearn — but try it first
Reweight the loss instead of the data. Same effect as duplicating minority rows — but nothing is added, nothing is invented, and it costs nothing.
  • LogisticRegression(class_weight="balanced")
    Weight = n_samples / (n_classes × bincount(y)). Works on LogReg, SVC, RandomForest, DecisionTree, SGD, HistGradientBoosting.
  • XGBClassifier(scale_pos_weight=n_neg/n_pos)
    XGBoost's binary equivalent. LightGBM: is_unbalance=True, or the same scale_pos_weight.
  • RandomForestClassifier(class_weight="balanced_subsample")
    Recomputes weights per bootstrap. Closer in spirit to BalancedRF — and often nearly as good.
  • # benchmark this before writing any imblearnbaseline
    A large share of published "SMOTE improved my F1" results evaporate when compared against class_weight='balanced' plus a tuned threshold. Skip this baseline and you don't actually know whether resampling helped.

The one mistake that invalidates everything

Resample the whole dataset, then split, and your test fold contains synthetic points interpolated from rows in your training fold — the model has effectively already seen the answers. Under-sampling is largely immune; over-sampling is where the bias is severe (published radiology work measured AUC inflated by ~0.10 per step of imbalance). This is the single reason imblearn.pipeline exists.

✗ resample → split

Leakage. SMOTE sees every row, including the ones about to become your test fold. It interpolates across the split that hasn't happened yet.

Full data 99 : 1 SMOTE on everything 50 : 50 synthetic mixed in train test ✗ a synthetic point in the TEST fold was interpolated from a row in the TRAIN fold …and the test set is now 50:50, a distribution that will never exist

✓ split → resample inside the fold

Correct. The Pipeline calls fit_resample() during fit and acts as a pass-through during predict. Each fold resamples independently; the test fold keeps its natural skew.

Full data 99 : 1 train · 99:1 test · 99:1 SMOTE (fit only) clf.fit() sampler SKIPPED at predict clf.score() pipe = make_pipeline(StandardScaler(), SMOTE(), clf) cross_val_score(pipe, X, y, cv=StratifiedKFold(5)) The CV number you get is the number you'll actually see in production.

Pipeline, CV & metrics

The part everybody gets wrong. Two rules: resampling only ever happens inside a fold, and accuracy is the enemy — at 99:1, a model that predicts zero forever scores 99%.

17imblearn Pipelinethe correct pattern
  • from imblearn.pipeline import Pipeline # NOT sklearn's
    This single import is the difference between an honest CV score and a fantasy.
  • pipe = Pipeline([
      ("scale", StandardScaler()), # SMOTE is a k-NN
      ("smote", SMOTE(random_state=42)),
      ("clf", LogisticRegression()),
    ])
    Order matters: scale → resample → fit. Encode categoricals after SMOTENC, before SMOTE.
  • cross_validate(pipe, X, y, cv=StratifiedKFold(5),
                  scoring="balanced_accuracy")
    Stratify, always. With a 1% minority, plain KFold can hand you a fold with zero positives and a silent nan.
  • GridSearchCV(pipe, {
      "smote__k_neighbors": [3, 5, 7],
      "smote__sampling_strategy": [0.3, 0.5, 1.0]})
    Tune the sampler like any other hyper-parameter. sampling_strategy is a knob, not a constant — full 1:1 balance is rarely the optimum.
  • # sample_weight is NOT forwarded through a samplerlimit
    The rows change, so the weights no longer line up. Need sample weights and resampling? Use class_weight or a balanced ensemble instead.
18Metrics That Don't Lieaccuracy is the enemy
At 99:1, always_predict_zero scores 99% accuracy. Every metric below is designed so that model scores ~0.
  • classification_report_imbalanced(y_true, y_pred)
    The one-shot overview. Columns: pre rec spe f1 geo iba sup — precision, recall, specificity, F1, geometric mean, index-balanced accuracy, support.
  • balanced_accuracy_score(y_true, y_pred) # sklearn
    Mean recall across classes. The safest default scoring= string — chance level is 0.5 regardless of skew.
  • average_precision_score(y_true, y_proba) # sklearn
    PR-AUC. Use this, not ROC-AUC. ROC's x-axis (FPR) has a huge denominator when negatives dominate, so thousands of false positives barely move the curve.
  • geometric_mean_score(y_true, y_pred)
    √(sensitivity × specificity). Drops to 0 the moment either class is ignored. Brutally honest.
  • sensitivity_score · specificity_score · sensitivity_specificity_support
    The individual pieces, sklearn-style API.
  • iba = make_index_balanced_accuracy(alpha=0.1)(geometric_mean_score)
    A decorator factory — wraps any metric to penalise a gap between per-class performance.
  • macro_averaged_mean_absolute_error(y_true, y_pred)
    For imbalanced ordinal targets (ratings, severity grades) where class distance matters.
19Threshold Tuningoften the whole fix
predict() hard-codes a 0.5 cut-off. On imbalanced data that number is arbitrary and usually wrong. Moving it costs zero training time and invents zero data.
  • TunedThresholdClassifierCV(clf, scoring="balanced_accuracy") # sklearn ≥1.5
    Cross-validates the cut-off itself. Exposes .best_threshold_.
  • TunedThresholdClassifierCV(clf,
      scoring=make_scorer(cost_fn, greater_is_better=False))
    The real answer for business problems. Encode the actual cost (a missed fraud costs ₹40,000; a false alert costs ₹50) and optimise the cut-off against that.
  • y_pred = (clf.predict_proba(X)[:, 1] >= t).astype(int)
    The manual version. Sweep t along the PR curve and pick an operating point you can defend.
  • # resampling corrupts predict_probacare
    After SMOTE the model is calibrated to a 50:50 world that does not exist, so it systematically over-predicts the minority. Need real probabilities (pricing, risk, expected value)? Prefer class_weight + threshold tuning — or recalibrate afterwards on an untouched, naturally-skewed set.
20The Rest of the Surfaceescape hatches
  • FunctionSampler(func=my_fn, kw_args={…}, validate=False)
    Turn any (X, y) → (X_res, y_res) function into a pipeline-safe sampler. The escape hatch for outlier rejection, business rules, domain filters that must still run per-fold.
  • from imblearn.model_selection import InstanceHardnessCV
    A CV splitter that spreads hard instances evenly across folds, so fold scores stop swinging wildly on tiny minority classes. Drop-in for cv=.
  • from imblearn.keras import BalancedBatchGenerator
    Yields class-balanced mini-batches to Keras fit(). The deep-learning-native answer: rebalance per batch, never touch the dataset. imblearn.tensorflow.balanced_batch_generator is the TF equivalent.
  • from imblearn.utils.estimator_checks import parametrize_with_checks
    pytest decorator to validate a sampler you wrote yourself against the full contract.

How the samplers actually work

The geometry behind the four ideas you'll reach for most. Every one of them is a nearest-neighbour rule in disguise — which is why feature scaling is not optional.

SMOTE

Pick a minority seed, pick one of its k nearest minority neighbours, drop a point on the segment. x_new = x_i + λ(x_nn − x_i), λ ~ U(0,1).

seed real minority synthetic majority never extrapolates outward

BorderlineSMOTE vs ADASYN

Same interpolation — different budget. Borderline spends it on the edge; ADASYN spends it where the minority is hardest (most majority neighbours).

BorderlineSMOTE only the "in danger" seeds fire ADASYN density ∝ majority neighbours — amplifies noise

TomekLinks (cleaning)

A pair of opposite-class points that are each other's nearest neighbour. Remove the majority member and the margin widens. Note the result is still imbalanced.

BEFORE AFTER Tomek link margin widens · only 1 row removed

Balanced bagging / EasyEnsemble

Under-sample many times, differently, and ensemble. RUS's speed — without throwing data away, because every majority row lands in some bag.

full train 1:1 1:1 1:1 RUS × n_estimators a different majority subset each time estimator₁ estimator₂ estimatorₙ majority vote

Decision map

Follow the arrows. Don't cargo-cult SMOTE — the first three boxes are free, invent no data, and settle a surprising share of real problems on their own.

flowchart LR
  A["Imbalanced
classification"] --> B{"Is accuracy
the only problem?"} B -->|"often yes"| C["Fix the METRIC
balanced_accuracy · PR-AUC"] C --> D["Tune the THRESHOLD
TunedThresholdClassifierCV"] D --> E{"Good enough?"} E -->|yes| Z(["Ship it —
you resampled nothing"]) E -->|no| F["class_weight='balanced'
scale_pos_weight"] F --> G{"Good enough?"} G -->|yes| Z G -->|no| H{"How much
majority data?"} H -->|"plenty — 100k+"| I["RandomUnderSampler
fast · invents nothing"] H -->|"scarce"| J["BalancedRandomForest
BalancedBagging"] H -->|"minority < 50 rows"| K["Get more data.
SMOTE will hallucinate."] J --> L{"Still short
on recall?"} L -->|no| Z L -->|yes| M{"Feature types?"} M -->|"numeric, clean"| N["BorderlineSMOTE"] M -->|"numeric, noisy"| O["SMOTEENN"] M -->|"mixed cat + num"| P["SMOTENC"] M -->|"all categorical"| Q["SMOTEN"] M -->|"strings / messy"| R["RandomOverSampler"] N --> Y["Wrap in imblearn Pipeline.
Always."] O --> Y P --> Y Q --> Y R --> Y I --> Y Y --> Z classDef free fill:#ecf7f2,stroke:#059669,color:#065f46,stroke-width:2px; classDef warn fill:#fdf0ef,stroke:#dc2626,color:#991b1b,stroke-width:2px; classDef gate fill:#ecf6fa,stroke:#0891b2,color:#155e75,stroke-width:2px; classDef samp fill:#fdf6ec,stroke:#d97706,color:#92400e,stroke-width:2px; classDef ship fill:#1a1d24,stroke:#1a1d24,color:#fbbf24,stroke-width:2px; class C,D,F free; class K,Y warn; class B,E,G,H,L,M gate; class I,J,N,O,P,Q,R samp; class Z ship;

Traps

Every one of these has cost somebody a production model. The first group produces wrong numbers, silently. The second just wastes your afternoon. The third is the set of conceptual limits nobody prints on the box.

21Silently Wrongthese invalidate results
  • SMOTE().fit_resample(X, y) train_test_split(…)leak
    The cardinal sin. Always resample inside imblearn.pipeline.Pipeline.
  • # resampling the TEST setnever
    You must evaluate on the natural distribution. A balanced test set answers a question nobody asked — and precision becomes meaningless once you've changed the base rate.
  • KFold(5) # on a 1% minoritynan
    A fold with zero positives. Always StratifiedKFold; with grouped data, StratifiedGroupKFold.
  • SMOTE() StandardScaler()order
    Backwards. Scale first — SMOTE is a Euclidean k-NN.
  • roc_auc on 1:1000 dataflatters
    Optimistic to the point of being decorative. Report average_precision and show the PR curve.
  • trusting predict_proba after SMOTEmiscalibrated
    Calibrated to a 50:50 world that doesn't exist.
22Loud Errorswhat the traceback means
  • TypeError: All intermediate steps should be transformers
    You used sklearn.pipeline.Pipeline. Samplers have fit_resample, not transform. Swap the import.
  • ValueError: Expected n_neighbors <= n_samples
    Fewer minority samples than k_neighbors + 1. Lower k — or accept that you can't synthesise from 4 points.
  • ValueError: … ratio required to remove samples from the minority class
    Your float target sits below the current ratio. Card 03.
  • ValueError: 'sampling_strategy' as a float is not supported for cleaning
    Cleaning samplers take str/list only. Card 09.
  • Counter(y_res) # still imbalanced?!
    You used a cleaning sampler. It was never going to balance. Working as designed.
  • # no sample_indices_ on my sampler
    Only selection samplers have it. SMOTE / ADASYN / ClusterCentroids generate — there's no original row to point at.
  • # irreproducible runs
    Set random_state on the sampler, the estimator and the CV splitter. Three places, all of them.
23Where SMOTE Quietly Failsthe conceptual limits
  • # high dimensionality
    Distances concentrate as d grows — every point becomes equidistant, "nearest neighbour" stops meaning anything, and SMOTE degenerates toward noise. Reduce dimensions first, or don't use it. This is why it routinely underperforms on text and genomics.
  • # label noise
    A mislabelled minority point in majority territory gets amplified — SMOTE (and especially ADASYN) breeds a colony of synthetic points around your error. Clean first: SMOTEENN, or ENN as a pre-step.
  • # small disjuncts
    If the minority is several tiny separated clusters, SMOTE interpolates between them — straight through majority space. KMeansSMOTE exists for exactly this. Plot your data first.
  • # it cannot extrapolate
    Every synthetic point lands inside the convex hull of the minority you already have. If the true minority region is bigger than your samples, no amount of SMOTE will find it. (SVMSMOTE is the one partial exception.)
  • # it does not create information
    The deepest point. SMOTE rearranges the density of what you already have; it adds no new signal. It shifts the decision boundary — which is often exactly what you wanted, but that's a job class_weight and a tuned threshold do more cheaply and more honestly.

Worth memorizing

imblearn.pipelinenot sklearn's — the difference between an honest CV score and a fantasy
sampling_strategy=0.5minority ends at half the majority · binary only · cleaning samplers reject it
'auto' flipsover → 'not majority'  ·  under → 'not minority'
fit_resamplethe only entry point — no transform(), which is why sklearn's Pipeline rejects it
controlled ≠ cleaningcleaning samplers don't balance — stop waiting for 50:50
scale → resample → fitSMOTE is a Euclidean k-NN; unscaled, one feature owns the metric
BalancedRFpin sampling_strategy="all", replacement=True, bootstrap=False
class_weight firstthe free baseline you must beat before claiming SMOTE helped
PR-AUC, not ROC-AUCROC's FPR denominator is enormous when negatives dominate
StratifiedKFoldalways — plain KFold can hand you a fold with zero positives
SMOTENCraw categorical columns in, one-hot encode after — never before
test set stays skeweda balanced test set answers a question nobody asked
predict_proba liesafter SMOTE it's calibrated to a 50:50 world that doesn't exist
threshold tuningfree, invents nothing, often recovers all of it — try it first
no extrapolationSMOTE stays inside the convex hull — it redistributes, it never creates
count > ratio1:1000 with 5,000 positives is fine; 1:10 with 12 is a data problem