HKUDS/Vibe-Trading · error · ValueError

{n_groups} unique groups cannot make {n_folds} folds

Error message

{n_groups} unique groups cannot make {n_folds} folds

What it means

group_purged_kfold_splits forms folds from whole groups, so it needs at least n_folds unique groups; each fold's test set is one or more groups. Fewer unique groups than folds makes the split impossible and is rejected.

Source

Thrown at agent/src/quantlib/crossvalidation.py:321

    if n_folds < MIN_FOLDS:
        raise ValueError(f"n_folds must be at least {MIN_FOLDS}, got {n_folds}")
    if not 0.0 <= embargo_fraction < 1.0:
        raise ValueError(f"embargo_fraction must be in [0, 1), got {embargo_fraction}")

    grp_array = np.asarray(groups)
    n_samples = len(grp_array)
    if n_samples == 0:
        raise ValueError("groups array cannot be empty")

    # Find unique groups preserving chronological order of appearance
    unique_groups, first_indices = np.unique(grp_array, return_index=True)
    # Sort by appearance order
    order = np.argsort(first_indices)
    unique_groups = unique_groups[order]
    n_groups = len(unique_groups)

    if n_groups < n_folds:
        raise ValueError(f"{n_groups} unique groups cannot make {n_folds} folds")

    # Map each group to its member row indices
    group_to_rows: dict[object, np.ndarray] = {}
    for idx, g in enumerate(grp_array):
        group_to_rows.setdefault(g, []).append(idx)
    for g in group_to_rows:
        group_to_rows[g] = np.array(group_to_rows[g], dtype=int)

    embargo_groups = int(round(n_groups * embargo_fraction))
    boundaries = np.linspace(0, n_groups, n_folds + 1).astype(int)

    for fold in range(n_folds):
        start_g, stop_g = int(boundaries[fold]), int(boundaries[fold + 1])
        if stop_g <= start_g:
            continue

        test_groups = set(unique_groups[start_g:stop_g])
        embargo_end_g = min(n_groups, stop_g + embargo_groups)

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Lower n_folds to at most the number of unique groups
  2. Choose a higher-cardinality grouping (e.g. per-date instead of per-ticker) if more folds are needed
  3. Fix the group key if it is accidentally constant (wrong column, dtype mismatch after merge)

Example fix

// before
splits = list(group_purged_kfold_splits(X, groups=df['asset_class'], n_folds=6))  # 4 unique classes
// after
n_groups = df['asset_class'].nunique()
splits = list(group_purged_kfold_splits(X, groups=df['asset_class'], n_folds=min(6, n_groups)))
Defensive patterns

Strategy: validation

Validate before calling

n_groups = len(set(groups))
assert n_groups >= n_folds, f'{n_groups} groups cannot make {n_folds} folds'

Prevention

When it happens

Trigger: Calling with groups containing 3 unique values but n_folds=5, e.g. groups=[1,1,2,2,3,3] with n_folds=5, or a grouping column that is constant.

Common situations: Grouping by a low-cardinality column (weekday with weekends removed -> 5 values max), a constant/buggy group key, or raising n_folds for a panel with few entities.

Related errors


AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28). Data as JSON: /api/errors/b90eea82e5543808. Report an issue: GitHub.