Approach: if you need the test set to contain at least N positive examples, do a controlled split by (1) checking there are >= N positives, (2) sampling N positives into the test set, (3) sampling negatives (or additional positives if you want a larger test set) to achieve desired test size or class balance, and (4) putting the rest into train. This gives deterministic control while preserving stratification for the required positive count.python
import pandas as pd
from math import ceil
def stratified_split_min_positive(df, label_col='label', test_frac=0.2, min_positives=50, random_state=42):
"""
Returns train_df, test_df such that test_df contains at least min_positives
examples with label==1. If not enough positives in dataset, raises ValueError.
test_frac is target fraction of total rows for test set (approx).
"""
rng = df.sample(frac=1, random_state=random_state) # shuffle
positives = rng[rng[label_col] == 1]
negatives = rng[rng[label_col] == 0]
if len(positives) < min_positives:
raise ValueError(f"Not enough positive examples: found {len(positives)}, need {min_positives}")
total = len(df)
target_test_size = max(ceil(test_frac * total), min_positives) # ensure at least min_positives overall
# take min_positives positives first
test_pos = positives.sample(n=min_positives, random_state=random_state)
remaining_pos = positives.drop(test_pos.index)
# fill remaining slots in test with negatives (or more positives if negatives are scarce)
remaining_slots = target_test_size - len(test_pos)
if remaining_slots <= 0:
test_df = test_pos
else:
take_neg = min(len(negatives), remaining_slots)
test_neg = negatives.sample(n=take_neg, random_state=random_state)
remaining_slots -= take_neg
# if still need slots, take more positives
extra_pos = remaining_pos.sample(n=remaining_slots, random_state=random_state) if remaining_slots > 0 else pd.DataFrame(columns=df.columns)
test_df = pd.concat([test_pos, test_neg, extra_pos])
train_df = df.drop(test_df.index)
return train_df.reset_index(drop=True), test_df.reset_index(drop=True)
# Example usage:
# train, test = stratified_split_min_positive(df, label_col='label', test_frac=0.2, min_positives=100)
Key points:- This guarantees at least min_positives in test without repeated random splits.- If you care about exact class ratio in test (stratification), you can adapt the number of negatives/positives sampled to match desired proportion; here we prioritize the min positives constraint.- Complexity: O(n) time and O(n) space (shuffling and sampling).Edge cases:- Less than min_positives available -> raise error.- Very small dataset may force test fraction to exceed intended; we set target to at least min_positives.- If labels are not binary, filter/select appropriate class values.