//  [python]
def crossval_helper(
    nid_to_sample_pool,
    perquery,
    perdatab,
    n_need,
    n_splits=None,
    rng=None,
    rebalance=True,
):
    """
    does sampling based on some grouping (or no grouping) of annots

    perquery = 2
    perdatab = 2

    nid_to_sample_pool = {
        1: [1, 2, 3, 4],
        2: [6, 7, 8, 9],
    }
    """
    if len(nid_to_sample_pool) == 0:
        raise ValueError(
            'Names do not have enough data for %d/%d split' % (perquery, perdatab)
        )
    rng = ut.ensure_rng(rng, impl='python')

    def split_combos(pool, perquery, perdatab, rng):
        import scipy.special

        poolsize = len(pool)
        # Number of ways we can select queries
        n_qmax = int(scipy.special.comb(poolsize, perquery))
        # Number of ways we can select targets from remaining items
        n_dmax = int(scipy.special.comb(poolsize - perquery, perdatab))
        # Total number of query / data combinations
        n_combos = n_qmax * n_dmax

        # Yield random combinations until we get something we havent seen
        poolset = set(pool)
        splits = set()
        while len(splits) < n_combos:
            # combo = tuple(sorted(rng.choice(items, size, replace=False)))
            qcombo = tuple(sorted(rng.sample(pool, perquery)))
            remain = poolset - set(qcombo)
            dcombo = tuple(sorted(rng.sample(remain, perdatab)))
            # TODO: try not to use queries / databases that we've used before
            # until we've exhauseted those possibilities.
            split = (qcombo, dcombo)
            if split not in splits:
                splits.add(split)
                yield split

    if n_splits is None:
        # What is the maximum number of items in a name?
        maxsize_name = max(map(len, nid_to_sample_pool.values()))
        # This is only a heuristic
        n_splits = maxsize_name

    # Create a mapping from each name to a list of query/target splits
    nid_to_splits = ut.ddict(list)
    # Create several splits for each name
    for nid, pool in nid_to_sample_pool.items():
        # Randomly select up to `n_splits` combinations of size `n_need`.
        combo_iter = split_combos(pool, perquery, perdatab, rng)
        for count, fold_split in enumerate(combo_iter, start=1):
            # Earlier samples will be biased towards names with more annots
            nid_to_splits[nid].append(fold_split)
            if count >= n_splits:
                break

    # logger.info(ut.repr2(list(nid_to_splits.values()), strvals=True, nl=2))

    # Some names may have more splits than others
    # nid_to_nsplits = ut.map_vals(len, nid_to_splits)
    # Find the name with the most splits
    # max_nid = ut.argmax(nid_to_nsplits)
    # max_size = nid_to_nsplits[max_nid]

    new_splits = [[] for _ in range(n_splits)]
    if rebalance:
        # Rebalance by adding combos from each name in a cycle.
        # The difference between the largest and smallest split is at most one.
        for count, split in enumerate(ut.iflatten(nid_to_splits.values())):
            new_splits[count % len(new_splits)].append(split)
    else:
        # No rebalancing. The first split contains everything from the dataset
        # and subsequent splits contain less and less.
        for nid, combos in nid_to_splits.items():
            for count, split in enumerate(combos):
                new_splits[count].append(split)

    # Reshape into an expanded aids list
    # List of query / database objects per split, grouped by name.
    reshaped_splits = [ut.listT(splits) for splits in new_splits if len(splits) > 0]
    return reshaped_splits