ValueError: The least populated class in y has only 1 member" in scikit-learnThis 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.
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.
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 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.
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.
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
StratifiedKFold, at least n_splits rows, see below) — that's why the threshold above is 5, not 2, in this example.
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.
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