4
$\begingroup$

I have a DataFrame like this:

import pandas as pd
pd.DataFrame({"x": [10, 15, 0, 5, 21, 2, 3, 99],
              "y": [123, 321, 25, 85, 721, 1232, 32, 994],
              "country": ["a", "a", "b", "a", "a", "b", "c", "c"],
              "age_group": ["e", "r", "t", "t", "r", "r", "e", "t"]})

I would like to cross-validate a model, I want to not have the same groups in test and training. E.g. a fold were country "c" and "b" and age_group "e" are in the test would lead to the instance 0, 2 and the last three instances to be in the test set.

I can't find a library for this and I do not have an idea for a simple implementation. Is there something like sklearn.model_selection.GroupKFold for more than one group? Is there an easy way to solve this problem? (I do not want "leave one group out" since in reality, my two group columns have high cardinality.)

$\endgroup$

3 Answers 3

1
$\begingroup$

You can create a new column (country_age) with values that concat the value of country and age_group by _. For example,

df['country_age'] = df['country'].astype(str) + '_' + df['age_group'].astype(str)

a_e, a_r, b_t, ...

Then use the GroupKFold (maybe StratifiedKFold) for this new column.

$\endgroup$
2
  • $\begingroup$ Thanks, I don't know why I didn't come up with this simple solution back then :D $\endgroup$
    – PascalIv
    Commented Dec 10, 2021 at 9:21
  • $\begingroup$ This doesn't work, unfortunately: if country "c" appears in the test set through group "c_e" (like in the example of the question), then GroupKFold might well put group "a_e" in the training set (since it's a different group), which is not what the original question asks for ("a_e" should also be in the training set, since it shares the same age group). Also, @hvtruong, I guess you meant "maybe StratifiedGroupKFold", at the end (not "StratifiedKFold")? $\endgroup$ Commented Dec 8, 2023 at 16:33
1
$\begingroup$

You want to group elements that have at least one common group value ("country" or "age_group", but this applies to any number of grouping values). This can be done for instance with the help of a class that makes building such groups:

class MultiLabelGroup:
    """
    Group of indexes pointing to data that belongs to the same group.

    Two data samples belong to the same group if they share any value
    among multiple labeled values (typically in some columns of a
    Pandas DataFrame).

    Notable attributes:
    - indexes: set with the indexes of the element in the group.
    """
    
    def __init__(self, monitored_indexes):
        """
        monitored_indexes -- sequence with the Pandas Series index to monitor
        for building the group.
        """
        # Sample indexes in the group:
        self.indexes = set()
        # Values of the columns in the samples of the group:
        self._values = {monitored_index: set() for monitored_index in monitored_indexes}

    def __repr__(self):
        return repr(self.indexes)
        
    def belongs_to_group(self, series: pd.Series) -> bool:
        """
        Return whether the sample in the given series belongs to the group.

        series -- series with the monitored names (indexes) in its index. The values
        of the series at these positions determine whether the sample 
        belongs to the group.
        """
        return any(series[index] in self._values[index] for index in self._values)

    def add_to_group(self, index, series: pd.Series):
        """
        Add the given index and sample to the group.

        index -- index value (user-defined) of the given series (not of a value in the series!).
        It must be hashable.
        """
        self.indexes.add(index)
        for (name, values) in self._values.items():
            values.add(series[name])

    def merge(self, other_group):
        """
        Merge the current group with another group.

        other_group -- another MultiLabelGroup, with the same monitored
        indexes.
        """
        self.indexes |= other_group.indexes
        for name in self._values:
            self._values[name] |= other_group._values[name]

This group class can be used like so:

groups = []
for index, row in df.iterrows():
    groups_to_merge = []
    other_groups = []
    for group in groups:
        (groups_to_merge if group.belongs_to_group(row) else other_groups).append(group)
    if groups_to_merge:
        # All the matching groups are linked
        # by the new data, which must be in all these
        # groups. Therefore they must be merged into
        # a single new group:
        new_group = groups_to_merge.pop()
        for group in groups_to_merge:
            new_group.merge(group)
        groups = other_groups
    else:  # No existing group
        new_group = MultiLabelGroup(["country", "age_group"])
    groups.append(new_group)
    # The new data must be registered:
    groups[-1].add_to_group(index, row)

print(groups)

You get the following single group:

[{0, 1, 2, 3, 4, 5, 6, 7}]

where you see that indeed the element you quoted (element 6 with country "c" and age group "e") is in the same (evaluation/test) set as element 0 (because it has the same age group "e").

The result shows that the data in the original question cannot be split between training and evaluation ("test") sets that do not share either a country or an age group.

This may work for some other, larger datasets, though.

$\endgroup$
-1
$\begingroup$

Here is a kernel containing the a Group Stratified CV function:

https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation

This should allow to separate the folds while keeping constant proportion of the classes specified in some groups.

$\endgroup$
3
  • $\begingroup$ Thanks, but I have two group columns, not one. The point is not the stratification, but having two group columns $\endgroup$
    – PascalIv
    Commented Dec 10, 2019 at 10:04
  • $\begingroup$ The function allows for multiple groups. But then again, I realize you wrote you want NOT to have the same groups in test and training, so it's not really clear what the goal is. Could you please clarify? $\endgroup$
    – Davide ND
    Commented Dec 10, 2019 at 10:32
  • $\begingroup$ sklearn.model_selection.GroupKFold also allows for multiple groups, but I have two columns, each with multiple groups. Imagine you have data for humans, with different sex and eye colors. One fold, for example, should train on "male" and "green eyes" so in the test set there should be no males and no people with green eyes. $\endgroup$
    – PascalIv
    Commented Dec 10, 2019 at 11:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.