Fix "ValueError: The least populated class in y has only 1 member" in scikit-learn

Fix "ValueError: The least populated class in y has only 1 member" in scikit-learn

This one usually shows up on real-world data, not toy datasets, because it needs an actual rare category to trigger: a fraud label, a churn segment, a defect type that only occurred once or twice in the whole dataset. The moment stratify=y gets added to train_test_split, the split refuses to run. This article reproduces the error, covers a related variant caused by test_size being too small rather than a class being too rare, and walks through the two real fixes: dropping the offending rows versus merging rare classes into a broader group.

Reproducing the Error

Any stratified split fails the moment one class has only a single row:

import numpy as np
from sklearn.model_selection import train_test_split

X = np.arange(20).reshape(10, 2)
y = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1])  # class 1 has exactly 1 row

train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
# ValueError: The least populated classes in y have only 1 member, which is
# too few. The minimum number of groups for any class cannot be less than 2.
# Classes with too few members are: [1]

The message itself is unusually clear for scikit-learn (it names the exact offending class), but it doesn't say what to actually do about it, and dropping stratify entirely is rarely the right answer if the split was stratified for a real reason, like preserving a rare-but-important class's proportion across train and test.

Why Stratify Needs at Least 2 Members Per Class

Stratified splitting works by taking a proportional slice of each class for the train set and a proportional slice for the test set. With only one row available for a class, that row can go to one side or the other, never both:

# without stratify, the split still runs, just without a stratification guarantee
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print(y_test)
# [0 0]

Note the single-row class didn't end up in either the printed y_test above by chance, since a non-stratified split of 10 rows into an 80/20 split can land anywhere. The point isn't where it lands, it's that scikit-learn will not silently guess for you when stratify is explicitly requested. It errors instead of quietly picking a side, because either choice would violate what "stratified" is supposed to mean.

A Related Error: test_size Too Small for the Number of Classes

A different, easily confused error shows up when every class has enough members, but test_size is too small to give each class even a single row in the test set:

y5 = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4])  # 5 classes, 2 members each

train_test_split(X, y5, test_size=0.1, stratify=y5, random_state=42)
# ValueError: The test_size = 1 should be greater or equal to the number of
# classes = 5

This isn't a rare-class problem at all, every class here has 2 members, well above the minimum. The actual constraint is that a stratified test set needs room for at least one row from each class, so test_size (in absolute row count) must be at least the number of distinct classes. The fix here is different from the rest of this article: raise test_size, not change the class distribution.

Fix 1: Drop the Rare Classes

If a class occurring once or twice is genuinely noise (a data-entry typo, a category that shouldn't exist), the fastest fix is to drop those rows before splitting:

import pandas as pd

df = pd.DataFrame({'x1': range(10), 'x2': range(10, 20), 'y': y})

counts = df['y'].value_counts()
rare_classes = counts[counts < 2].index
df_filtered = df[~df['y'].isin(rare_classes)]

X_train, X_test, y_train, y_test = train_test_split(
    df_filtered[['x1', 'x2']], df_filtered['y'],
    test_size=0.2, stratify=df_filtered['y'], random_state=42
)
# works

This is the right call for genuine noise, but it's worth being deliberate about it rather than reaching for it automatically: dropping silently removes a real category the model will then never see or be evaluated against, which matters a lot more if that rare class happens to be the one you actually care about, fraud and defect labels being the obvious examples.

Fix 2: Merge Rare Classes Into an "Other" Bucket

When the rare categories still matter and shouldn't be discarded, merging them into a broader group preserves the rows instead of dropping them:

y = np.array([0]*50 + [1]*30 + [2]*15 + [3]*4 + [4]*1)
X = np.arange(len(y) * 2).reshape(len(y), 2)
df = pd.DataFrame({'x1': X[:, 0], 'x2': X[:, 1], 'y': y})

THRESHOLD = 5
counts = df['y'].value_counts()
rare = counts[counts < THRESHOLD].index
df['y_grouped'] = df['y'].where(~df['y'].isin(rare), other=-1)

print(df['y_grouped'].value_counts())
#  0    50
#  1    30
#  2    15
# -1     5

X_train, X_test, y_train, y_test = train_test_split(
    df[['x1', 'x2']], df['y_grouped'],
    test_size=0.2, stratify=df['y_grouped'], random_state=42
)
# works, all 100 rows preserved
Merging one rare class alone isn't always enough: grouping a single 1-member class into its own "other" bucket still leaves that bucket with 1 member, and the split still fails. The fix only works if enough small classes get combined together that the resulting bucket has at least 2 rows (or, for StratifiedKFold, at least n_splits rows, see below) — that's why the threshold above is 5, not 2, in this example.

The Same Problem in StratifiedKFold

Cross-validation splitters that stratify have the identical requirement, just with a higher bar. StratifiedKFold needs at least n_splits members per class, not 2:

from sklearn.model_selection import StratifiedKFold
import warnings

y = np.array([0]*9 + [1]*1)  # class 1 has 1 member, n_splits=5 needs at least 5
X = np.arange(20).reshape(10, 2)

with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    skf = StratifiedKFold(n_splits=5)
    list(skf.split(X, y))
    print(w[0].message)
# The least populated class in y has only 1 members, which is less than
# n_splits=5.

StratifiedKFold only warns here rather than raising, since it can still produce folds, just without a guarantee that every fold's test set actually contains the rare class. The same two fixes apply, drop or merge, but the count needed to clear the bar scales with n_splits: a 5-fold split needs at least 5 rows in every class to genuinely stratify, not the 2 that a single train_test_split call needs.

Summary: Which Fix Applies

Situation                                              Fix
────────────────────────────────────────────────      ─────────────────────────────────────────────
"least populated class... 1 member"                     Class genuinely has <2 rows: drop or merge it
"test_size = N should be >= number of classes"          Raise test_size, not a class-distribution problem
Rare class is real noise                                 Drop it (fast, loses that category entirely)
Rare class matters but is too small to stratify          Merge into a broader "other"/nearest category
Using cross-validation, not a single split               StratifiedKFold needs >= n_splits per class, not 2