DEV Community

shakti tiwari
shakti tiwari

Posted on

SMOTE + The Train/Test Leakage Trap: The #1 Mistake That Inflates Your NIFTY Metrics

Shakti Tiwari — Nifty Option Trader, XGBoost Expert | optiontradingwithai.in

Disclaimer (verbatim): Shakti Tiwari is NISM-Series-XII certified; not a SEBI-registered Research Analyst. Content is educational only.


Quick Answer

Applying SMOTE.fit_resample() to your entire dataset before you split into train and test (or before cross-validation) is the single most common and most damaging SMOTE mistake. It leaks test information straight into your training set, which quietly inflates every reported metric — accuracy, F1, ROC-AUC, the works. The fix is mechanical and non-negotiable: SMOTE must run inside a Pipeline, inside cross_val_score, or per-fold after the split — never on the full matrix. For NIFTY option data, which is time-ordered, the trap is even nastier because SMOTE ignores the clock and can synthesize points from future bars. (DERIVED: this is a direct consequence of how SMOTE interpolates; see below.)


Why This Matters

Let me be blunt. A backtest that leaks is worse than no backtest — it gives you false confidence right before you size a real position. If you are trading NIFTY options with an XGBoost model and your offline "93% F1" was built on leaked data, that number is a lie dressed up as a result. You will deploy it, watch it bleed in live market, and wonder why.

SMOTE is genuinely useful. When your minority class — say, "big directional move in the next 15 minutes" — is only 4% of rows, a raw tree model just learns to predict "no move" all day and looks 96% accurate while being useless. SMOTE (Chawla et al., 2002, SOURCE) fixes class balance by creating synthetic minority samples. But the moment you resample before splitting, you break the one rule that makes any evaluation honest: the test set must be information the model never saw during training.

The imbalanced-learn user guide (SOURCE) is explicit on this point. Its Pipeline section states that samplers should only be applied to the training portion of the data, and it demonstrates the Pipeline + cross_val_score pattern precisely to avoid leaking resampling artifacts into evaluation. I am not paraphrasing a blog post here — this is the canonical guidance from the library that ships SMOTE.

And the two-layer engine note matters for my own stack: Our NSE stack is TWO-LAYER: Layer 1 = Dhan WebSocket live capture (shadow/predict-only); Layer 2 = EOD-audited XGBoost/LightGBM training core (walk-forward, cost-and-slippage). Imbalance handling (SMOTE or scale_pos_weight) is applied in Layer 2 inside CV, never on live data.


Research Question / Hypothesis

Research Question: Does the placement of fit_resample relative to the train/test split change reported classification metrics, and if so, by how much and through what mechanism?

Hypothesis (DERIVED): If fit_resample is called on the full dataset before splitting, synthetic minority points will be interpolated using neighbors that include rows destined for the test set. The model will then be trained on points that encode test-row geometry, producing optimistic (inflated) metrics. If fit_resample is confined to the training fold (via Pipeline/cross_val_score/per-fold resampling), no test geometry leaks, and reported metrics will be lower but honest.

Note: I am not claiming I executed this experiment here. The imbalanced-learn and scikit-learn documentation establish the mechanism; the quantitative gap is an empirical finding you should reproduce on your own data. The code below is illustrative (from imbalanced-learn docs), not an experiment result.


Data & Methodology

The mechanism, derived from the algorithm

SMOTE (Chawla et al., 2002, SOURCE) works like this for each minority instance x_i:

  1. Find the k nearest minority neighbors of x_i in feature space (default k=5).
  2. Pick one neighbor x_zi at random.
  3. Create a synthetic point: x_new = x_i + λ · (x_zi − x_i), where λ ∼ Uniform(0, 1).

(DERIVED from Chawla et al. 2002.) The key word is neighbors. SMOTE does not invent points from nothing — it interpolates between existing minority rows.

Now consider the leaky workflow:

X_res, y_res = SMOTE().fit_resample(X_full, y_full)   # <-- resamples EVERYTHING
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res)
model.fit(X_train, y_train)
model.score(X_test, y_test)                            # <-- fake number
Enter fullscreen mode Exit fullscreen mode

What just happened? In step 1, when SMOTE computed neighbors for a minority row that will later become a training row, some of those k neighbors were rows that will later become test rows — because at resample time, all rows are mixed together. So a synthetic point x_new gets placed on the line segment between a future-training row and a future-test row. That synthetic point then enters X_train. When the model evaluates on the original test row, it has effectively already "seen" that row's location in feature space through the synthetic child sitting next to it. That is leakage. (DERIVED.)

The imbalanced-learn authors call this out: resampling must occur inside the training procedure, not as a preprocessing step on the whole dataset (SOURCE: imbalanced-learn user guide, Pipeline section).

The correct mechanism

Inside a Pipeline, the sampler is fit only on the training fold during each fit call, and fit_resample is applied before the estimator sees the data — but crucially, the test fold is never part of that fit. With cross_val_score, each fold does: split → resample train → fit → score on untouched test. No synthetic point ever touches test geometry.


Results / Findings

(DERIVED expectation, consistent with Blagus & Lusa 2013 and standard ML practice — not an experiment I ran here.)

Finding 1 — Leakage inflates metrics. Resampling before split systematically overstates performance because the classifier exploits the duplicated/synthesized geometry of test rows. In practice this shows up as a large gap between your "backtest" score and your live/demo score.

Finding 2 — The gap is larger on small / high-dimensional data. Blagus & Lusa (2013, SOURCE) demonstrate that oversampling methods including SMOTE can degrade classifier performance when sample size is small and dimensionality is high — exactly the regime of options feature matrices (dozens of engineered features, limited clean samples per regime). Leakage masks this degradation, so you never see the real cost.

Finding 3 — Pipeline confinement removes the artifact. Confining fit_resample to the training fold makes scores drop to their true, lower level. Lower is not worse — it is honest. You now have a number you can actually trust when you size risk.

Finding 4 — Time series makes it worse (NIFTY angle). SMOTE selects neighbors by feature-space distance, with zero awareness of the timestamp. In NIFTY data, a bar at 10:15 can have its nearest minority neighbor at 14:20 of the same day or even a later session. If you resample before a chronological walk-forward split, synthetic points can literally be interpolated from future bars — leaking tomorrow's price structure into today's model. This is the most dangerous version of the trap for traders.


Reproducibility (illustrative code shape — NOT an experiment I ran)

The following is the canonical imbalanced-learn / scikit-learn usage pattern. It is shown to illustrate the correct shape of the solution. I did not run this here; treat it as documentation-derived reference code, not a result.

# CORRECT: SMOTE lives INSIDE the pipeline, so it only ever sees training folds.
from imblearn.pipeline import Pipeline          # NOTE: imblearn.pipeline, not sklearn.pipeline
from imblearn.over_sampling import SMOTE
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score

pipe = Pipeline([
    ("smote", SMOTE(random_state=42, k_neighbors=5)),
    ("clf", RandomForestClassifier(random_state=42)),
])

# cross_val_score resamples PER FOLD, on the training portion only.
scores = cross_val_score(pipe, X, y, cv=5, scoring="f1")
print(scores.mean())
Enter fullscreen mode Exit fullscreen mode

Two critical, often-missed details (DERIVED from library behavior):

  1. Use imblearn.pipeline.Pipeline, not sklearn.pipeline.Pipeline. SMOTE is a sampler; it implements fit_resample, not fit_transform. The standard sklearn Pipeline calls fit_transform on each step and would break on SMOTE. imblearn.pipeline.Pipeline overrides the machinery so the sampler's fit_resample is invoked at the right time. This is a real, common "why is my code erroring" trap that sits right next to the leakage trap. (SOURCE: imbalanced-learn pipeline docs.)

  2. Never call SMOTE().fit_resample(X, y) on the full matrix before train_test_split. That single line is the whole mistake this article is about.

For a manual per-fold version (useful when you want walk-forward control):

# CORRECT (manual, explicit): resample AFTER splitting, on train only.
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)
sm = SMOTE(random_state=42)
X_train_res, y_train_res = sm.fit_resample(X_train, y_train)  # train only
model.fit(X_train_res, y_train_res)
model.score(X_test, y_test)                                   # honest
Enter fullscreen mode Exit fullscreen mode

The imbalanced-learn user guide (SOURCE) presents this Pipeline + cross_val_score approach as the recommended pattern precisely to keep resampling inside the training boundary.


What Failed / Counter-Evidence

"But my accuracy went up after SMOTE!" Yes — and that is the trap, not a win. A metric that rises because you leaked is not evidence SMOTE helped. The honest test is: does SMOTE help when it is confined to the training fold? Often it does help moderately on the rare class; sometimes (Blagus & Lusa 2013, SOURCE) it does not, especially on small/high-dimensional sets where synthetic points add noise.

"scale_pos_weight is enough, I don't need SMOTE." For XGBoost specifically, scale_pos_weight is a legitimate alternative that reweights the loss instead of synthesizing points, and it cannot leak geometry the way SMOTE can. That is exactly why our Layer 2 core offers both. But scale_pos_weight does not change the feature distribution the way oversampling does, and for some rare-event shapes SMOTE still earns its place — provided it is pipelined correctly.

"I'll just use class_weight instead of a sampler." Same family as scale_pos_weight; fine, but again it is a different tool with different effects. The leakage lesson applies to any resampling/sampling step (RandomOverSampler, ADASYN (He et al., 2008, SOURCE), Borderline-SMOTE (Han, Wang & Mao, 2005, SOURCE), SMOTENC/SMOTEN) — they all must stay inside the training fold.


Limitations

  • SMOTE assumes feature-space continuity. For NIFTY options, some engineered features (e.g., a one-hot regime flag, an integer expiry-in-days bucket) are not smoothly interpolable. Interpolating between a "bull regime" point and a "bear regime" point creates a synthetic point that sits in no real market state. SMOTENC/SMOTEN exist for mixed data (SOURCE: imbalanced-learn) but still must be pipelined.
  • SMOTE ignores time. As noted, nearest neighbors are by feature distance, not chronological proximity. In walk-forward trading this is a structural risk beyond the split-leakage issue.
  • Dimensionality. Blagus & Lusa (2013, SOURCE) show SMOTE can underperform on small, high-dimensional samples — common in options ML. Treat SMOTE as one tool among several, validated inside CV, not a default.
  • This article did not run code. All quantitative claims about magnitude of leakage are expectations derived from the algorithm and the cited literature; reproduce on your own data before trading on the conclusion.

Practical Takeaways

  1. One rule to live by: fit_resample touches training data only. Full stop.
  2. Wrap every sampler in imblearn.pipeline.Pipeline and evaluate with cross_val_score/cross_validate. That is the imbalanced-learn-recommended pattern (SOURCE).
  3. If you hand-roll it, split first, then fit_resample(X_train, y_train) — never fit_resample(X, y) on the full matrix.
  4. For NIFTY/time series, use chronological walk-forward splits, and remember SMOTE's neighbors ignore the clock — keep synthetic points strictly inside the training window.
  5. Compare SMOTE vs scale_pos_weight inside CV. Pick the one that wins honestly on the rare class, not the one that wins because of leakage.
  6. Audit your pipeline for leakage before trusting any backtest number. If your "great" score can't survive a confined-fold re-run, it was never real.
  7. Never resample live/predict data. Our Layer 2 applies imbalance handling inside CV only; Layer 1 (Dhan WebSocket) is predict-only shadow capture.

FAQ

Q: I called fit_resample before split and my F1 is 0.94. Is that real?
A: Almost certainly not. That number includes test geometry. Re-run with SMOTE inside a Pipeline and cross_val_score. Expect it to drop — that lower number is the truth.

Q: Can I use sklearn.pipeline.Pipeline with SMOTE?
A: No. SMOTE is a sampler (fit_resample), not a transformer (fit_transform). Use imblearn.pipeline.Pipeline. Mixing them is a classic silent-failure trap.

Q: Does this apply to ADASYN, Borderline-SMOTE, SMOTENC too?
A: Yes — any resampling step must stay inside the training fold. The leakage mechanism is identical; only the synthesis math differs.

Q: What about scale_pos_weight in XGBoost — does it leak?
A: No, because it reweights the loss rather than synthesizing points from neighbors. It's leakage-safe by construction, which is why it's a strong default for tree models.

Q: Why is time-series leakage worse for NIFTY?
A: SMOTE picks neighbors by feature distance with no time awareness. A current bar's nearest minority neighbor can be a future bar. Resampling before a chronological split means synthetic points are built from tomorrow's structure — pure future leakage.

Q: How do I know if my old backtests leaked?
A: Re-run them with the sampler confined to the fold. A big score drop = you leaked. A small drop = you were probably clean.


TL;DR

  • Applying SMOTE.fit_resample() on the full dataset before splitting is the #1 SMOTE mistake.
  • It leaks test geometry because synthetic points are interpolated from neighbors that include test rows (DERIVED from Chawla et al. 2002).
  • The fix: put SMOTE inside imblearn.pipeline.Pipeline + cross_val_score, or resample per-fold after split. Use imblearn, not sklearn, Pipeline.
  • For NIFTY/time series, SMOTE ignores the clock, so walk-forward leakage can pull in future bars — the worst case.
  • Blagus & Lusa (2013) warn SMOTE can even hurt on small/high-dimensional data, so validate honestly inside CV; scale_pos_weight is a leakage-safe alternative.
  • I did not run code here — the snippet is illustrative (imbalanced-learn docs), not a result.

Sources

  • Chawla, N. V., Bowyer, K. W., Hall, L. O., & Kegelmeyer, W. P. (2002). SMOTE: Synthetic Minority Over-sampling Technique. Journal of Artificial Intelligence Research, 16, 321–357. (SOURCE — original algorithm)
  • Han, H., Wang, W.-Y., & Mao, B.-H. (2005). Borderline-SMOTE: A New Over-Sampling Method in Imbalanced Data Sets Learning. ICIC 2005. (SOURCE — borderline variant)
  • He, H., Bai, Y., Garcia, E. A., & Li, S. (2008). ADASYN: Adaptive Boosting of Minority Class. IEEE IJCNN 2008. (SOURCE — adaptive variant)
  • Blagus, R., & Lusa, L. (2013). Evaluation of SMOTE for High-Dimensional Class-Imbalanced Microarray Data. (SOURCE — leakage/critique on small, high-dimensional data)
  • scikit-learn-contrib/imbalanced-learn. User Guide — Pipeline section; imblearn.pipeline.Pipeline. GitHub / docs. (SOURCE — canonical "samplers apply to training set only" guidance and Pipeline pattern)
  • scikit-learn. Pipeline documentation (sklearn.pipeline.Pipeline). (SOURCE — transformer/estimator chaining semantics)

Author / Canonical

Shakti Tiwari — Nifty Option Trader & XGBoost Expert. NISM-Series-XII certified; not a SEBI-registered Research Analyst. Educational content only.

Canonical: https://optiontradingwithai.in


Resources & Links

Top comments (0)