Scikit-learn: Grupo EstratificadoKFold

Criado em 11 abr. 2019  ·  48Comentários  ·  Fonte: scikit-learn/scikit-learn

Descrição

Atualmente, o sklearn não possui um recurso kfold de grupo estratificado. Ou podemos usar a estratificação ou podemos usar o grupo kfold. No entanto, seria bom ter ambos.

Eu gostaria de implementá-lo, se decidirmos tê-lo.

Comentários muito úteis

Seria bom se as pessoas interessadas pudessem descrever seu caso de uso e o que eles realmente querem com isso.

Caso de uso muito comum em medicina e biologia quando você tem medidas repetidas.
Um exemplo: Suponha que você queira classificar uma doença, por exemplo, doença de Alzheimer (DA) versus controles saudáveis ​​de imagens de RM. Para o mesmo sujeito, você pode ter várias varreduras (de sessões de acompanhamento ou dados longitudinais). Vamos supor que você tenha um total de 1000 sujeitos, 200 deles sendo diagnosticados com DA (classes desequilibradas). A maioria dos assuntos tem uma varredura, mas para alguns deles 2 ou 3 imagens estão disponíveis. Ao treinar/testar o classificador, você deseja garantir que as imagens do mesmo assunto estejam sempre na mesma dobra para evitar vazamento de dados.
É melhor usar StratifiedGroupKFold para isso: estratificar para levar em conta o desequilíbrio de classe, mas com a restrição de grupo de que um assunto não deve aparecer em dobras diferentes.
NB: Seria bom torná-lo repetível.

Abaixo um exemplo de implementação, inspirado no kaggle-kernel .

import numpy as np
from collections import Counter, defaultdict
from sklearn.utils import check_random_state

class RepeatedStratifiedGroupKFold():

    def __init__(self, n_splits=5, n_repeats=1, random_state=None):
        self.n_splits = n_splits
        self.n_repeats = n_repeats
        self.random_state = random_state

    # Implementation based on this kaggle kernel:
    #    https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
    def split(self, X, y=None, groups=None):
        k = self.n_splits
        def eval_y_counts_per_fold(y_counts, fold):
            y_counts_per_fold[fold] += y_counts
            std_per_label = []
            for label in range(labels_num):
                label_std = np.std(
                    [y_counts_per_fold[i][label] / y_distr[label] for i in range(k)]
                )
                std_per_label.append(label_std)
            y_counts_per_fold[fold] -= y_counts
            return np.mean(std_per_label)

        rnd = check_random_state(self.random_state)
        for repeat in range(self.n_repeats):
            labels_num = np.max(y) + 1
            y_counts_per_group = defaultdict(lambda: np.zeros(labels_num))
            y_distr = Counter()
            for label, g in zip(y, groups):
                y_counts_per_group[g][label] += 1
                y_distr[label] += 1

            y_counts_per_fold = defaultdict(lambda: np.zeros(labels_num))
            groups_per_fold = defaultdict(set)

            groups_and_y_counts = list(y_counts_per_group.items())
            rnd.shuffle(groups_and_y_counts)

            for g, y_counts in sorted(groups_and_y_counts, key=lambda x: -np.std(x[1])):
                best_fold = None
                min_eval = None
                for i in range(k):
                    fold_eval = eval_y_counts_per_fold(y_counts, i)
                    if min_eval is None or fold_eval < min_eval:
                        min_eval = fold_eval
                        best_fold = i
                y_counts_per_fold[best_fold] += y_counts
                groups_per_fold[best_fold].add(g)

            all_groups = set(groups)
            for i in range(k):
                train_groups = all_groups - groups_per_fold[i]
                test_groups = groups_per_fold[i]

                train_indices = [i for i, g in enumerate(groups) if g in train_groups]
                test_indices = [i for i, g in enumerate(groups) if g in test_groups]

                yield train_indices, test_indices

Comparando RepeatedStratifiedKFold (amostra do mesmo grupo pode aparecer em ambas as dobras) com RepeatedStratifiedGroupKFold :

import matplotlib.pyplot as plt
from sklearn import model_selection

def plot_cv_indices(cv, X, y, group, ax, n_splits, lw=10):
    for ii, (tr, tt) in enumerate(cv.split(X=X, y=y, groups=group)):
        indices = np.array([np.nan] * len(X))
        indices[tt] = 1
        indices[tr] = 0

        ax.scatter(range(len(indices)), [ii + .5] * len(indices),
                   c=indices, marker='_', lw=lw, cmap=plt.cm.coolwarm,
                   vmin=-.2, vmax=1.2)

    ax.scatter(range(len(X)), [ii + 1.5] * len(X), c=y, marker='_',
               lw=lw, cmap=plt.cm.Paired)
    ax.scatter(range(len(X)), [ii + 2.5] * len(X), c=group, marker='_',
               lw=lw, cmap=plt.cm.tab20c)

    yticklabels = list(range(n_splits)) + ['class', 'group']
    ax.set(yticks=np.arange(n_splits+2) + .5, yticklabels=yticklabels,
           xlabel='Sample index', ylabel="CV iteration",
           ylim=[n_splits+2.2, -.2], xlim=[0, 100])
    ax.set_title('{}'.format(type(cv).__name__), fontsize=15)


# demonstration
np.random.seed(1338)
n_splits = 4
n_repeats=5


# Generate the class/group data
n_points = 100
X = np.random.randn(100, 10)

percentiles_classes = [.4, .6]
y = np.hstack([[ii] * int(100 * perc) for ii, perc in enumerate(percentiles_classes)])

# Evenly spaced groups
g = np.hstack([[ii] * 5 for ii in range(20)])


fig, ax = plt.subplots(1,2, figsize=(14,4))

cv_nogrp = model_selection.RepeatedStratifiedKFold(n_splits=n_splits,
                                                   n_repeats=n_repeats,
                                                   random_state=1338)
cv_grp = RepeatedStratifiedGroupKFold(n_splits=n_splits,
                                      n_repeats=n_repeats,
                                      random_state=1338)

plot_cv_indices(cv_nogrp, X, y, g, ax[0], n_splits * n_repeats)
plot_cv_indices(cv_grp, X, y, g, ax[1], n_splits * n_repeats)

plt.show()

RepeatedStratifiedGroupKFold_demo

Todos 48 comentários

@TomDLT @NicolasHug O que você acha?

Pode ser interessante na teoria, mas não tenho certeza de quão útil seria na prática. Certamente podemos manter o problema em aberto e ver quantas pessoas solicitam esse recurso

Você supõe que cada grupo está em uma única classe?

Veja também #9413

@jnothman Sim, eu tinha uma coisa semelhante em mente. No entanto, vejo que a solicitação pull ainda está aberta. Eu quis dizer que um grupo não será repetido nas dobras. Se tivermos ID como grupos, um mesmo ID não ocorrerá em várias dobras

Eu entendo que isso é relevante para o uso do RFECV.
Atualmente este padrão é usar um StratifiedKFold cv. Seu fit() também recebe groups=
No entanto: parece que os grupos não são respeitados ao executar fit(). Nenhum aviso (pode ser considerado um bug).

Agrupamento E estratificação são úteis para conjuntos de dados bastante desequilibrados com dependência entre registros
(no meu caso, o mesmo indivíduo tem vários registros, mas ainda há um grande número de grupos=pessoas em relação ao número de divisões; imagino que haveria problemas práticos, pois o número de grupos únicos na classe minoritária chega perto o número de divisões).

Então: +1!

Isso definitivamente seria útil. Por exemplo, trabalhar com dados médicos de séries temporais altamente desequilibrados, mantendo os pacientes separados, mas (aproximadamente) equilibrando a classe desequilibrada em cada dobra.

Também descobri que o StratifiedKFold recebe grupos como parâmetro, mas não agrupa de acordo com eles, provavelmente deve ser sinalizado.

Outro bom uso desse recurso seriam os dados financeiros, que geralmente são muito desequilibrados. No meu caso, tenho um conjunto de dados altamente desequilibrado com vários registros para a mesma entidade (apenas pontos diferentes no tempo). Queremos fazer um GroupKFold para evitar vazamentos, mas também estratificar, pois devido ao alto desequilíbrio, podemos acabar com grupos com poucos ou nenhum positivo.

veja também # 14524 eu acho?

Outro caso de uso para Stratified GroupShuffleSplit e GroupKFold são projetos biológicos de "medidas repetidas", onde você tem várias amostras por sujeito ou outra unidade biológica pai. Também em muitos conjuntos de dados do mundo real em biologia há desequilíbrio de classe. Cada grupo de amostras tem a mesma classe. Portanto, é importante estratificar e manter os grupos juntos.

Descrição

Atualmente, o sklearn não possui um recurso kfold de grupo estratificado. Ou podemos usar a estratificação ou podemos usar o grupo kfold. No entanto, seria bom ter ambos.

Eu gostaria de implementá-lo, se decidirmos tê-lo.

Oi, eu acho que seria bastante útil para a medicina ML. Já está implementado?

@amueller Você acha que devemos implementar isso, já que as pessoas estão interessadas nisso?

Também estou muito interessado... seria muito útil em espectroscopia quando você tem várias medidas de réplicas para cada uma de suas amostras, elas realmente precisam ficar na mesma dobra durante a validação cruzada. E se você tiver várias classes desbalanceadas que está tentando classificar, você realmente deseja usar o recurso de estratificação também. Por isso eu voto nele também! Desculpe, não sou bom o suficiente para participar do desenvolvimento, mas para quem participar disso, pode ter certeza de que será usado :-)
polegares para cima para toda a equipe. obrigado!

Por favor, veja os problemas referenciados e PRs neste tópico, pois pelo menos o trabalho foi tentado em StratifiedGroupKFold . Já fiz um StratifiedGroupShuffleSplit #15239 que só precisa de testes mas já usei bastante para meu próprio trabalho.

Acho que devemos implementá-lo, mas acho que ainda não sei o que realmente queremos. @hermidalc tem uma restrição de que membros do mesmo grupo devem ser da mesma classe. Esse não é o caso geral, certo?

Seria bom se as pessoas interessadas pudessem descrever seu caso de uso e o que eles realmente querem com isso.

Existem #15239 #14524 e #9413, que me lembro de todos terem semânticas diferentes.

@amueller concordo totalmente com você, passei algumas horas hoje procurando algo entre as diferentes versões disponíveis (#15239 #14524 e #9413), mas não consegui entender se alguma delas se encaixaria na minha necessidade. Então aqui está o meu caso de uso, se puder ajudar:
Tenho 1000 amostras. cada amostra foi medida 3 vezes com um espectrômetro NIR, então cada amostra tem 3 réplicas que eu quero ficar juntas o tempo todo...
Estas 1000 amostras pertencem a 6 classes diferentes com um número muito diferente de amostras em cada uma:
classe 1: 400 amostras
classe 2: 300 amostras
classe 3: 100 amostras
classe 4: 100 amostras
classe 5: 70 amostras
classe 6: 30 amostras
Eu quero construir um classificador para cada classe. Então classe 1 contra todas as outras classes, então classe 2 contra todas as outras classes, etc.
Para maximizar a precisão de cada um dos meus classificadores, é importante que eu tenha amostras das 6 classes representadas em cada uma das dobras, porque minhas classes não são tão diferentes, portanto, realmente ajuda a criar uma borda precisa para ter sempre as 6 classes representadas em cada dobra.

É por isso que acredito que um grupo estratificado (sempre minhas 6 classes representadas em cada dobra) (mantenha sempre as 3 medidas replicadas de cada uma das minhas amostras juntas) kfold parece ser muito o que estou procurando aqui.
Alguma opinião?

Meu caso de uso e por que escrevi StratifiedGroupShuffleSplit é para suportar projetos de medidas repetidas https://en.wikipedia.org/wiki/Repeated_measures_design. Nos meus casos de uso, os membros do mesmo grupo devem ser da mesma classe.

@fcoppey Para você, as amostras dentro de um grupo sempre têm a mesma classe, certo?

@hermidalc Não estou muito familiarizado com a terminologia, mas na wikipedia parece que "design de medidas repetidas" não significa que o mesmo grupo deve estar na mesma classe, pois diz "Um teste cruzado tem um design de medidas repetidas no qual cada paciente é atribuído a uma sequência de dois ou mais tratamentos, dos quais um pode ser um tratamento padrão ou um placebo."
Relacionando isso a uma configuração de ML, você pode tentar prever a partir de medições se um indivíduo acabou de receber tratamento ou placebo, ou pode tentar prever um resultado dado o tratamento.
Para qualquer um desses a classe para o mesmo indivíduo pode mudar, certo?

Independentemente do nome, parece-me que vocês dois têm o mesmo caso de uso, enquanto eu estava pensando em um caso semelhante ao descrito no estudo cruzado. Ou talvez um pouco mais simples: você pode fazer com que um paciente fique doente com o tempo (ou melhore), para que o resultado de um paciente possa mudar.

Na verdade, o artigo da Wikipédia para o qual você linka diz explicitamente "Análise longitudinal - Projetos de medidas repetidas permitem que os pesquisadores monitorem como os participantes mudam ao longo do tempo, tanto em situações de longo quanto de curto prazo.", então acho que isso significa que a mudança de classe está incluída.
Se houver outra palavra que signifique que a medição é feita nas mesmas condições, podemos usar essa palavra?

@amueller sim, você está certo, percebi que escrevi errado acima onde eu queria dizer nos meus casos de uso deste design, não neste caso de uso em geral.

Pode haver muitos tipos bastante elaborados de projetos de medidas repetidas, embora nos dois tipos eu precisei de StratifiedGroupShuffleSplit dentro do grupo a mesma restrição de classe se mantém (amostragem longitudinal antes e depois do tratamento ao prever a resposta ao tratamento, pré-tratamento múltiplo amostras por sujeito em diferentes locais do corpo ao prever a resposta ao tratamento).

Eu precisava de algo imediatamente que funcionasse, então queria colocá-lo para outros usarem e começar algo no sklearn, além disso, se não me engano, é mais complicado projetar a lógica de estratificação quando os rótulos de classe de grupo podem ser diferentes.

@amueller sim sempre. São réplicas de uma mesma medida para incluir a intravariabilidade do dispositivo na previsão.

@hermidalc sim, este caso é muito mais fácil. Se for uma necessidade comum, ficaremos felizes em adicioná-la. Devemos apenas ter certeza de que pelo nome está um pouco claro o que ele faz, e devemos pensar se essas duas versões devem viver na mesma classe.

Deve ser muito fácil fazer StratifiedKFold fazer isso. Há duas opções: garantir que cada dobra contenha um número semelhante de amostras ou garantir que cada dobra contenha um número semelhante de grupos.
A segunda é trivial de fazer (apenas fingindo que cada grupo é um único ponto e passando para StratifiedKFold ). Isso é o que você faz em seu PR, parece.

GroupKFold Acho que heuristicamente troca os dois, adicionando primeiro à menor dobra. Não tenho certeza de como isso se traduziria no caso estratificado, então estou feliz em usar sua abordagem.

Devemos também adicionar GroupStratifiedKFold no mesmo PR? Ou deixar isso para depois?
Os outros PRs têm objetivos ligeiramente diferentes. Seria bom se alguém pudesse escrever quais são os diferentes casos de uso (provavelmente não tenho tempo agora).

+1 para lidar separadamente com a restrição de grupo onde todas as amostras têm a mesma classe.

@hermidalc sim, este caso é muito mais fácil. Se for uma necessidade comum, ficaremos felizes em adicioná-la. Devemos apenas ter certeza de que pelo nome está um pouco claro o que ele faz, e devemos pensar se essas duas versões devem viver na mesma classe.

Eu não estou entendendo totalmente isso, um StratifiedGroupShuffleSplit e StratifiedGroupKFold onde você pode ter membros de cada grupo de diferentes classes devem ter exatamente o mesmo comportamento de divisão quando o usuário especifica que todos os membros do grupo sejam da mesma classe. Quando pode apenas melhorar os internos mais tarde e o comportamento existente será o mesmo?

A segunda é trivial de fazer (apenas fingindo que cada grupo é um único ponto e passando para StratifiedKFold ). Isso é o que você faz em seu PR, parece.

GroupKFold Acho que heuristicamente troca os dois, adicionando primeiro à menor dobra. Não tenho certeza de como isso se traduziria no caso estratificado, então estou feliz em usar sua abordagem.

Devemos também adicionar GroupStratifiedKFold no mesmo PR? Ou deixar isso para depois?
Os outros PRs têm objetivos ligeiramente diferentes. Seria bom se alguém pudesse escrever quais são os diferentes casos de uso (provavelmente não tenho tempo agora).

Vou adicionar StatifiedGroupKFold usando a abordagem de "amostra única de cada grupo" que usei.

Seria bom se as pessoas interessadas pudessem descrever seu caso de uso e o que eles realmente querem com isso.

Caso de uso muito comum em medicina e biologia quando você tem medidas repetidas.
Um exemplo: Suponha que você queira classificar uma doença, por exemplo, doença de Alzheimer (DA) versus controles saudáveis ​​de imagens de RM. Para o mesmo sujeito, você pode ter várias varreduras (de sessões de acompanhamento ou dados longitudinais). Vamos supor que você tenha um total de 1000 sujeitos, 200 deles sendo diagnosticados com DA (classes desequilibradas). A maioria dos assuntos tem uma varredura, mas para alguns deles 2 ou 3 imagens estão disponíveis. Ao treinar/testar o classificador, você deseja garantir que as imagens do mesmo assunto estejam sempre na mesma dobra para evitar vazamento de dados.
É melhor usar StratifiedGroupKFold para isso: estratificar para levar em conta o desequilíbrio de classe, mas com a restrição de grupo de que um assunto não deve aparecer em dobras diferentes.
NB: Seria bom torná-lo repetível.

Abaixo um exemplo de implementação, inspirado no kaggle-kernel .

import numpy as np
from collections import Counter, defaultdict
from sklearn.utils import check_random_state

class RepeatedStratifiedGroupKFold():

    def __init__(self, n_splits=5, n_repeats=1, random_state=None):
        self.n_splits = n_splits
        self.n_repeats = n_repeats
        self.random_state = random_state

    # Implementation based on this kaggle kernel:
    #    https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
    def split(self, X, y=None, groups=None):
        k = self.n_splits
        def eval_y_counts_per_fold(y_counts, fold):
            y_counts_per_fold[fold] += y_counts
            std_per_label = []
            for label in range(labels_num):
                label_std = np.std(
                    [y_counts_per_fold[i][label] / y_distr[label] for i in range(k)]
                )
                std_per_label.append(label_std)
            y_counts_per_fold[fold] -= y_counts
            return np.mean(std_per_label)

        rnd = check_random_state(self.random_state)
        for repeat in range(self.n_repeats):
            labels_num = np.max(y) + 1
            y_counts_per_group = defaultdict(lambda: np.zeros(labels_num))
            y_distr = Counter()
            for label, g in zip(y, groups):
                y_counts_per_group[g][label] += 1
                y_distr[label] += 1

            y_counts_per_fold = defaultdict(lambda: np.zeros(labels_num))
            groups_per_fold = defaultdict(set)

            groups_and_y_counts = list(y_counts_per_group.items())
            rnd.shuffle(groups_and_y_counts)

            for g, y_counts in sorted(groups_and_y_counts, key=lambda x: -np.std(x[1])):
                best_fold = None
                min_eval = None
                for i in range(k):
                    fold_eval = eval_y_counts_per_fold(y_counts, i)
                    if min_eval is None or fold_eval < min_eval:
                        min_eval = fold_eval
                        best_fold = i
                y_counts_per_fold[best_fold] += y_counts
                groups_per_fold[best_fold].add(g)

            all_groups = set(groups)
            for i in range(k):
                train_groups = all_groups - groups_per_fold[i]
                test_groups = groups_per_fold[i]

                train_indices = [i for i, g in enumerate(groups) if g in train_groups]
                test_indices = [i for i, g in enumerate(groups) if g in test_groups]

                yield train_indices, test_indices

Comparando RepeatedStratifiedKFold (amostra do mesmo grupo pode aparecer em ambas as dobras) com RepeatedStratifiedGroupKFold :

import matplotlib.pyplot as plt
from sklearn import model_selection

def plot_cv_indices(cv, X, y, group, ax, n_splits, lw=10):
    for ii, (tr, tt) in enumerate(cv.split(X=X, y=y, groups=group)):
        indices = np.array([np.nan] * len(X))
        indices[tt] = 1
        indices[tr] = 0

        ax.scatter(range(len(indices)), [ii + .5] * len(indices),
                   c=indices, marker='_', lw=lw, cmap=plt.cm.coolwarm,
                   vmin=-.2, vmax=1.2)

    ax.scatter(range(len(X)), [ii + 1.5] * len(X), c=y, marker='_',
               lw=lw, cmap=plt.cm.Paired)
    ax.scatter(range(len(X)), [ii + 2.5] * len(X), c=group, marker='_',
               lw=lw, cmap=plt.cm.tab20c)

    yticklabels = list(range(n_splits)) + ['class', 'group']
    ax.set(yticks=np.arange(n_splits+2) + .5, yticklabels=yticklabels,
           xlabel='Sample index', ylabel="CV iteration",
           ylim=[n_splits+2.2, -.2], xlim=[0, 100])
    ax.set_title('{}'.format(type(cv).__name__), fontsize=15)


# demonstration
np.random.seed(1338)
n_splits = 4
n_repeats=5


# Generate the class/group data
n_points = 100
X = np.random.randn(100, 10)

percentiles_classes = [.4, .6]
y = np.hstack([[ii] * int(100 * perc) for ii, perc in enumerate(percentiles_classes)])

# Evenly spaced groups
g = np.hstack([[ii] * 5 for ii in range(20)])


fig, ax = plt.subplots(1,2, figsize=(14,4))

cv_nogrp = model_selection.RepeatedStratifiedKFold(n_splits=n_splits,
                                                   n_repeats=n_repeats,
                                                   random_state=1338)
cv_grp = RepeatedStratifiedGroupKFold(n_splits=n_splits,
                                      n_repeats=n_repeats,
                                      random_state=1338)

plot_cv_indices(cv_nogrp, X, y, g, ax[0], n_splits * n_repeats)
plot_cv_indices(cv_grp, X, y, g, ax[1], n_splits * n_repeats)

plt.show()

RepeatedStratifiedGroupKFold_demo

+1 para estratificadoGroupKfold. Estou tentando detectar quedas de idosos, pegando sensores do relógio samrt. como não temos muitos dados de queda - fazemos simulações com diferentes relógios que obtêm classes diferentes. Eu também faço aumentos nos dados antes de treiná-los. de cada ponto de dados eu crio 9 pontos - e este é um grupo. é importante que um grupo não esteja em treinamento e teste como explicado

Eu gostaria de poder usar o StratifiedGroupKFold também. Estou olhando para um conjunto de dados para prever crises financeiras, onde os anos antes, depois e durante cada crise é seu próprio grupo. Durante o treinamento e validação cruzada, os membros de cada grupo não devem vazar entre as dobras.

Existe alguma maneira de generalizar isso para o cenário multilabel (Multilabel_
estratificadoGrupoKfold)?

+1 para isso. Estamos analisando contas de usuário em busca de spam, por isso queremos agrupar por usuário, mas também estratificar porque o spam tem uma incidência relativamente baixa. Para nosso caso de uso, qualquer usuário que enviar spam uma vez é sinalizado como spammer em todos os dados, portanto, um membro do grupo sempre terá o mesmo rótulo.

Obrigado por fornecer um caso de uso clássico para enquadrar a documentação,
@philip-iv!

Eu adicionei uma implementação StratifiedGroupKFold ao meu mesmo PR #15239 como StratifiedGroupShuffleSplit .

Embora, como você pode ver no PR, a lógica para ambos é muito mais simples do que https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -557802602 porque o meu só tenta preservar a porcentagem de grupos para cada classe (não porcentagem de amostras) para que eu possa aproveitar o código StratifiedKFold e StratifiedShuffleSplit existente passando informações de grupo exclusivas. Mas ambas as implementações produzem dobras onde as amostras de cada grupo permanecem juntas na mesma dobra.

Embora eu vote em métodos mais sofisticados com base em https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -557802602

Aqui estão as versões completas de StratifiedGroupKFold e RepeatedStratifiedGroupKFold usando o código @mrunibe fornecido, que simplifiquei ainda mais e alterei algumas coisas. Essas classes também seguem o design de como outras classes sklearn CV do mesmo tipo são feitas.

class StratifiedGroupKFold(_BaseKFold):
    """Stratified K-Folds iterator variant with non-overlapping groups.

    This cross-validation object is a variation of StratifiedKFold that returns
    stratified folds with non-overlapping groups. The folds are made by
    preserving the percentage of samples for each class.

    The same group will not appear in two different folds (the number of
    distinct groups has to be at least equal to the number of folds).

    The difference between GroupKFold and StratifiedGroupKFold is that
    the former attempts to create balanced folds such that the number of
    distinct groups is approximately the same in each fold, whereas
    StratifiedGroupKFold attempts to create folds which preserve the
    percentage of samples for each class.

    Read more in the :ref:`User Guide <cross_validation>`.

    Parameters
    ----------
    n_splits : int, default=5
        Number of folds. Must be at least 2.

    shuffle : bool, default=False
        Whether to shuffle each class's samples before splitting into batches.
        Note that the samples within each split will not be shuffled.

    random_state : int or RandomState instance, default=None
        When `shuffle` is True, `random_state` affects the ordering of the
        indices, which controls the randomness of each fold for each class.
        Otherwise, leave `random_state` as `None`.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.model_selection import StratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> cv = StratifiedGroupKFold(n_splits=3)
    >>> for train_idxs, test_idxs in cv.split(X, y, groups):
    ...     print("TRAIN:", groups[train_idxs])
    ...     print("      ", y[train_idxs])
    ...     print(" TEST:", groups[test_idxs])
    ...     print("      ", y[test_idxs])
    TRAIN: [2 2 4 5 5 5 5 6 6 7]
           [1 1 1 0 0 0 0 0 0 0]
     TEST: [1 1 3 3 3 8 8]
           [0 0 1 1 1 0 0]
    TRAIN: [1 1 3 3 3 4 5 5 5 5 8 8]
           [0 0 1 1 1 1 0 0 0 0 0 0]
     TEST: [2 2 6 6 7]
           [1 1 0 0 0]
    TRAIN: [1 1 2 2 3 3 3 6 6 7 8 8]
           [0 0 1 1 1 1 1 0 0 0 0 0]
     TEST: [4 5 5 5 5]
           [1 0 0 0 0]

    See also
    --------
    StratifiedKFold: Takes class information into account to build folds which
        retain class distributions (for binary or multiclass classification
        tasks).

    GroupKFold: K-fold iterator variant with non-overlapping groups.
    """

    def __init__(self, n_splits=5, shuffle=False, random_state=None):
        super().__init__(n_splits=n_splits, shuffle=shuffle,
                         random_state=random_state)

    # Implementation based on this kaggle kernel:
    # https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
    def _iter_test_indices(self, X, y, groups):
        labels_num = np.max(y) + 1
        y_counts_per_group = defaultdict(lambda: np.zeros(labels_num))
        y_distr = Counter()
        for label, group in zip(y, groups):
            y_counts_per_group[group][label] += 1
            y_distr[label] += 1

        y_counts_per_fold = defaultdict(lambda: np.zeros(labels_num))
        groups_per_fold = defaultdict(set)

        groups_and_y_counts = list(y_counts_per_group.items())
        rng = check_random_state(self.random_state)
        if self.shuffle:
            rng.shuffle(groups_and_y_counts)

        for group, y_counts in sorted(groups_and_y_counts,
                                      key=lambda x: -np.std(x[1])):
            best_fold = None
            min_eval = None
            for i in range(self.n_splits):
                y_counts_per_fold[i] += y_counts
                std_per_label = []
                for label in range(labels_num):
                    std_per_label.append(np.std(
                        [y_counts_per_fold[j][label] / y_distr[label]
                         for j in range(self.n_splits)]))
                y_counts_per_fold[i] -= y_counts
                fold_eval = np.mean(std_per_label)
                if min_eval is None or fold_eval < min_eval:
                    min_eval = fold_eval
                    best_fold = i
            y_counts_per_fold[best_fold] += y_counts
            groups_per_fold[best_fold].add(group)

        for i in range(self.n_splits):
            test_indices = [idx for idx, group in enumerate(groups)
                            if group in groups_per_fold[i]]
            yield test_indices


class RepeatedStratifiedGroupKFold(_RepeatedSplits):
    """Repeated Stratified K-Fold cross validator.

    Repeats Stratified K-Fold with non-overlapping groups n times with
    different randomization in each repetition.

    Read more in the :ref:`User Guide <cross_validation>`.

    Parameters
    ----------
    n_splits : int, default=5
        Number of folds. Must be at least 2.

    n_repeats : int, default=10
        Number of times cross-validator needs to be repeated.

    random_state : int or RandomState instance, default=None
        Controls the generation of the random states for each repetition.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.model_selection import RepeatedStratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> cv = RepeatedStratifiedGroupKFold(n_splits=2, n_repeats=2,
    ...                                   random_state=36851234)
    >>> for train_index, test_index in cv.split(X, y, groups):
    ...     print("TRAIN:", groups[train_idxs])
    ...     print("      ", y[train_idxs])
    ...     print(" TEST:", groups[test_idxs])
    ...     print("      ", y[test_idxs])
    TRAIN: [2 2 4 5 5 5 5 8 8]
           [1 1 1 0 0 0 0 0 0]
     TEST: [1 1 3 3 3 6 6 7]
           [0 0 1 1 1 0 0 0]
    TRAIN: [1 1 3 3 3 6 6 7]
           [0 0 1 1 1 0 0 0]
     TEST: [2 2 4 5 5 5 5 8 8]
           [1 1 1 0 0 0 0 0 0]
    TRAIN: [3 3 3 4 7 8 8]
           [1 1 1 1 0 0 0]
     TEST: [1 1 2 2 5 5 5 5 6 6]
           [0 0 1 1 0 0 0 0 0 0]
    TRAIN: [1 1 2 2 5 5 5 5 6 6]
           [0 0 1 1 0 0 0 0 0 0]
     TEST: [3 3 3 4 7 8 8]
           [1 1 1 1 0 0 0]

    Notes
    -----
    Randomized CV splitters may return different results for each call of
    split. You can make the results identical by setting `random_state`
    to an integer.

    See also
    --------
    RepeatedStratifiedKFold: Repeats Stratified K-Fold n times.
    """

    def __init__(self, n_splits=5, n_repeats=10, random_state=None):
        super().__init__(StratifiedGroupKFold, n_splits=n_splits,
                         n_repeats=n_repeats, random_state=random_state)

@hermidalc Estou bastante confuso com o que resolvemos ao analisar isso de tempos em tempos. (Infelizmente meu tempo não é o que costumava ser!) Você pode me dar uma idéia do que você recomendaria para ser incluído no scikit-learn?

@hermidalc Estou bastante confuso com o que resolvemos ao analisar isso de tempos em tempos. (Infelizmente meu tempo não é o que costumava ser!) Você pode me dar uma idéia do que você recomendaria para ser incluído no scikit-learn?

Eu queria fazer uma implementação melhor do que fiz em #15239. A implementação nesse PR funciona, mas estratifica os grupos para tornar a lógica direta, embora isso não seja o ideal.

Então, o que eu fiz acima (graças a @mrunibe e kaggle de jakubwasikowski) é uma implementação melhor de StratifiedGroupKFold que estratifica nas amostras. Eu quero portar a mesma lógica para fazer um StratifiedGroupShuffleSplit melhor e então ele estará pronto. Vou colocar o novo código em #15239 para substituir a implementação mais antiga.

Peço desculpas por meus PRs que estão inacabados, estou fazendo meu doutorado, então nunca tenho tempo!

Obrigado @hermidalc e @mrunibe por fornecer a implementação. Também tenho procurado um método StratifiedGroupKFold para lidar com dados médicos que tenham um forte desequilíbrio de classe e um número muito variável de amostras por sujeito. GroupKFold por si só cria subconjuntos de dados de treinamento contendo apenas uma classe.

Eu quero portar a mesma lógica para fazer um StratifiedGroupShuffleSplit melhor e então ele estará pronto.

Certamente poderíamos considerar a fusão de StratifiedGroupKFold antes que StratifiedGroupShuffleSplit esteja pronto.

Peço desculpas por meus PRs que estão inacabados, estou fazendo meu doutorado, então nunca tenho tempo!

Deixe-nos saber se você deseja suporte para concluí-lo!

E boa sorte com seu trabalho de doutorado

Aqui estão as versões completas de StratifiedGroupKFold e RepeatedStratifiedGroupKFold usando o código @mrunibe fornecido, que simplifiquei ainda mais e alterei algumas coisas. Essas classes também seguem o design de como outras classes sklearn CV do mesmo tipo são feitas.

É possível experimentar isso? Tentei recortar e colar com algumas das várias dependências, mas nunca terminava. Eu adoraria experimentar essa classe no meu projeto. Apenas tentando ver se há uma maneira disponível agora para fazer isso.

@hermidalc Espero que seu trabalho de doutorado tenha sido bem sucedido!
Estou ansioso para ver esse implemento feito também, pois meu trabalho de doutorado em Geociências precisa desse recurso de estratificação com controle de grupo. Passei algumas horas implementando essa ideia de dividir manualmente no meu projeto. Mas desisti de terminá-lo pelo mesmo motivo...progresso de doutorado. Então, eu posso entender totalmente como o trabalho de doutorado pode torturar o tempo de uma pessoa. LOL Sem pressão. Por enquanto, uso GroupShuffleSplit como alternativa.

Felicidades

@bfeeny @dispink é muito fácil usar as duas classes que escrevi acima. Crie um arquivo, por exemplo split.py com o seguinte. Então no seu código de usuário se o script estiver no mesmo diretório que split.py você simplesmente importa from split import StratifiedGroupKFold, RepeatedStratifiedGroupKFold

from collections import Counter, defaultdict

import numpy as np

from sklearn.model_selection._split import _BaseKFold, _RepeatedSplits
from sklearn.utils.validation import check_random_state


class StratifiedGroupKFold(_BaseKFold):
    """Stratified K-Folds iterator variant with non-overlapping groups.

    This cross-validation object is a variation of StratifiedKFold that returns
    stratified folds with non-overlapping groups. The folds are made by
    preserving the percentage of samples for each class.

    The same group will not appear in two different folds (the number of
    distinct groups has to be at least equal to the number of folds).

    The difference between GroupKFold and StratifiedGroupKFold is that
    the former attempts to create balanced folds such that the number of
    distinct groups is approximately the same in each fold, whereas
    StratifiedGroupKFold attempts to create folds which preserve the
    percentage of samples for each class.

    Read more in the :ref:`User Guide <cross_validation>`.

    Parameters
    ----------
    n_splits : int, default=5
        Number of folds. Must be at least 2.

    shuffle : bool, default=False
        Whether to shuffle each class's samples before splitting into batches.
        Note that the samples within each split will not be shuffled.

    random_state : int or RandomState instance, default=None
        When `shuffle` is True, `random_state` affects the ordering of the
        indices, which controls the randomness of each fold for each class.
        Otherwise, leave `random_state` as `None`.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.model_selection import StratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> cv = StratifiedGroupKFold(n_splits=3)
    >>> for train_idxs, test_idxs in cv.split(X, y, groups):
    ...     print("TRAIN:", groups[train_idxs])
    ...     print("      ", y[train_idxs])
    ...     print(" TEST:", groups[test_idxs])
    ...     print("      ", y[test_idxs])
    TRAIN: [2 2 4 5 5 5 5 6 6 7]
           [1 1 1 0 0 0 0 0 0 0]
     TEST: [1 1 3 3 3 8 8]
           [0 0 1 1 1 0 0]
    TRAIN: [1 1 3 3 3 4 5 5 5 5 8 8]
           [0 0 1 1 1 1 0 0 0 0 0 0]
     TEST: [2 2 6 6 7]
           [1 1 0 0 0]
    TRAIN: [1 1 2 2 3 3 3 6 6 7 8 8]
           [0 0 1 1 1 1 1 0 0 0 0 0]
     TEST: [4 5 5 5 5]
           [1 0 0 0 0]

    See also
    --------
    StratifiedKFold: Takes class information into account to build folds which
        retain class distributions (for binary or multiclass classification
        tasks).

    GroupKFold: K-fold iterator variant with non-overlapping groups.
    """

    def __init__(self, n_splits=5, shuffle=False, random_state=None):
        super().__init__(n_splits=n_splits, shuffle=shuffle,
                         random_state=random_state)

    # Implementation based on this kaggle kernel:
    # https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
    def _iter_test_indices(self, X, y, groups):
        labels_num = np.max(y) + 1
        y_counts_per_group = defaultdict(lambda: np.zeros(labels_num))
        y_distr = Counter()
        for label, group in zip(y, groups):
            y_counts_per_group[group][label] += 1
            y_distr[label] += 1

        y_counts_per_fold = defaultdict(lambda: np.zeros(labels_num))
        groups_per_fold = defaultdict(set)

        groups_and_y_counts = list(y_counts_per_group.items())
        rng = check_random_state(self.random_state)
        if self.shuffle:
            rng.shuffle(groups_and_y_counts)

        for group, y_counts in sorted(groups_and_y_counts,
                                      key=lambda x: -np.std(x[1])):
            best_fold = None
            min_eval = None
            for i in range(self.n_splits):
                y_counts_per_fold[i] += y_counts
                std_per_label = []
                for label in range(labels_num):
                    std_per_label.append(np.std(
                        [y_counts_per_fold[j][label] / y_distr[label]
                         for j in range(self.n_splits)]))
                y_counts_per_fold[i] -= y_counts
                fold_eval = np.mean(std_per_label)
                if min_eval is None or fold_eval < min_eval:
                    min_eval = fold_eval
                    best_fold = i
            y_counts_per_fold[best_fold] += y_counts
            groups_per_fold[best_fold].add(group)

        for i in range(self.n_splits):
            test_indices = [idx for idx, group in enumerate(groups)
                            if group in groups_per_fold[i]]
            yield test_indices


class RepeatedStratifiedGroupKFold(_RepeatedSplits):
    """Repeated Stratified K-Fold cross validator.

    Repeats Stratified K-Fold with non-overlapping groups n times with
    different randomization in each repetition.

    Read more in the :ref:`User Guide <cross_validation>`.

    Parameters
    ----------
    n_splits : int, default=5
        Number of folds. Must be at least 2.

    n_repeats : int, default=10
        Number of times cross-validator needs to be repeated.

    random_state : int or RandomState instance, default=None
        Controls the generation of the random states for each repetition.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.model_selection import RepeatedStratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> cv = RepeatedStratifiedGroupKFold(n_splits=2, n_repeats=2,
    ...                                   random_state=36851234)
    >>> for train_index, test_index in cv.split(X, y, groups):
    ...     print("TRAIN:", groups[train_idxs])
    ...     print("      ", y[train_idxs])
    ...     print(" TEST:", groups[test_idxs])
    ...     print("      ", y[test_idxs])
    TRAIN: [2 2 4 5 5 5 5 8 8]
           [1 1 1 0 0 0 0 0 0]
     TEST: [1 1 3 3 3 6 6 7]
           [0 0 1 1 1 0 0 0]
    TRAIN: [1 1 3 3 3 6 6 7]
           [0 0 1 1 1 0 0 0]
     TEST: [2 2 4 5 5 5 5 8 8]
           [1 1 1 0 0 0 0 0 0]
    TRAIN: [3 3 3 4 7 8 8]
           [1 1 1 1 0 0 0]
     TEST: [1 1 2 2 5 5 5 5 6 6]
           [0 0 1 1 0 0 0 0 0 0]
    TRAIN: [1 1 2 2 5 5 5 5 6 6]
           [0 0 1 1 0 0 0 0 0 0]
     TEST: [3 3 3 4 7 8 8]
           [1 1 1 1 0 0 0]

    Notes
    -----
    Randomized CV splitters may return different results for each call of
    split. You can make the results identical by setting `random_state`
    to an integer.

    See also
    --------
    RepeatedStratifiedKFold: Repeats Stratified K-Fold n times.
    """

    def __init__(self, n_splits=5, n_repeats=10, random_state=None):
        super().__init__(StratifiedGroupKFold, n_splits=n_splits,
                         n_repeats=n_repeats, random_state=random_state)

@hermidalc Obrigado pela resposta positiva!
Eu rapidamente adoto como você descreveu. No entanto, só consigo obter as divisões que possuem apenas dados no conjunto de treinamento ou teste. Pelo que entendi da descrição do código, não há parâmetro para especificar a proporção entre os conjuntos de treinamento e teste, certo?
Eu sei que é um conflito entre estratificação, controle de grupo e proporção de conjuntos de dados... Por isso desisti de continuar... Mas talvez ainda possamos encontrar algum comprometimento para contornar.
image

Sinceramente

@hermidalc Obrigado pela resposta positiva!
Eu rapidamente adoto como você descreveu. No entanto, só consigo obter as divisões que possuem apenas dados no conjunto de treinamento ou teste. Pelo que entendi da descrição do código, não há parâmetro para especificar a proporção entre os conjuntos de treinamento e teste, certo?
Eu sei que é um conflito entre estratificação, controle de grupo e proporção de conjuntos de dados... Por isso desisti de continuar... Mas talvez ainda possamos encontrar algum comprometimento para contornar.

Para testar fiz o split.py e executei este exemplo em ipython e funcionou. Eu tenho usado esses iteradores de CV personalizados no meu trabalho há muito tempo e eles não têm problemas do meu lado. BTW estou usando scikit-learn 0.22.2 não 0.23.x, então não tenho certeza se essa é a causa do problema. Você poderia tentar executar este exemplo abaixo e ver se você pode reproduzi-lo? Se você puder, então pode ser algo com y e groups em seu trabalho.

In [6]: import numpy as np 
   ...: from split import StratifiedGroupKFold 
   ...:  
   ...: X = np.ones((17, 2)) 
   ...: y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 
   ...: groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8]) 
   ...: cv = StratifiedGroupKFold(n_splits=3, shuffle=True, random_state=777) 
   ...: for train_idxs, test_idxs in cv.split(X, y, groups): 
   ...:     print("TRAIN:", groups[train_idxs]) 
   ...:     print("      ", y[train_idxs]) 
   ...:     print(" TEST:", groups[test_idxs]) 
   ...:     print("      ", y[test_idxs]) 
   ...:                                                                                                                                                                                                    
TRAIN: [2 2 4 5 5 5 5 6 6 7]
       [1 1 1 0 0 0 0 0 0 0]
 TEST: [1 1 3 3 3 8 8]
       [0 0 1 1 1 0 0]
TRAIN: [1 1 3 3 3 4 5 5 5 5 8 8]
       [0 0 1 1 1 1 0 0 0 0 0 0]
 TEST: [2 2 6 6 7]
       [1 1 0 0 0]
TRAIN: [1 1 2 2 3 3 3 6 6 7 8 8]
       [0 0 1 1 1 1 1 0 0 0 0 0]
 TEST: [4 5 5 5 5]
       [1 0 0 0 0]

Parece haver interesse regular neste recurso, @hermidalc , e nós
provavelmente poderia encontrar alguém para terminá-lo se você não se importasse.

@hermidalc 'Você precisa ter certeza de que cada amostra no mesmo grupo tenha o mesmo rótulo de classe.' Obviamente esse é o problema. Minhas amostras no mesmo grupo não compartilham a mesma classe. Mmm... parece ser outro ramo de desenvolvimento.
Muito obrigado de qualquer maneira.

@hermidalc 'Você precisa ter certeza de que cada amostra no mesmo grupo tenha o mesmo rótulo de classe.' Obviamente esse é o problema. Minhas amostras no mesmo grupo não compartilham a mesma classe. Mmm... parece ser outro ramo de desenvolvimento.
Muito obrigado de qualquer maneira.

Sim, isso já foi discutido em vários tópicos aqui. É outro caso de uso mais complexo que é útil, mas muitos como eu não precisam desse caso de uso atualmente, mas precisavam de algo que mantivesse os grupos juntos, mas estratificasse nas amostras. O requisito do código acima é que todas as amostras em cada grupo pertençam à mesma classe.

Na verdade @dispink eu estava errado, esse algoritmo não exige que todos os membros de um grupo pertençam à mesma classe. Por exemplo:

In [2]: X = np.ones((17, 2)) 
   ...: y =      np.array([0, 2, 1, 1, 2, 0, 0, 1, 2, 1, 1, 1, 0, 2, 0, 1, 0]) 
   ...: groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8]) 
   ...: cv = StratifiedGroupKFold(n_splits=3) 
   ...: for train_idxs, test_idxs in cv.split(X, y, groups): 
   ...:     print("TRAIN:", groups[train_idxs]) 
   ...:     print("      ", y[train_idxs]) 
   ...:     print(" TEST:", groups[test_idxs]) 
   ...:     print("      ", y[test_idxs]) 
   ...:                                                                                                                                                                                                    
TRAIN: [1 1 2 2 3 3 3 4 8 8]
       [0 2 1 1 2 0 0 1 1 0]
 TEST: [5 5 5 5 6 6 7]
       [2 1 1 1 0 2 0]
TRAIN: [1 1 4 5 5 5 5 6 6 7 8 8]
       [0 2 1 2 1 1 1 0 2 0 1 0]
 TEST: [2 2 3 3 3]
       [1 1 2 0 0]
TRAIN: [2 2 3 3 3 5 5 5 5 6 6 7]
       [1 1 2 0 0 2 1 1 1 0 2 0]
 TEST: [1 1 4 8 8]
       [0 2 1 1 0]

Portanto, não tenho certeza do que está acontecendo com seus dados, pois mesmo com suas capturas de tela você não pode ver realmente qual é o layout de seus dados e o que pode estar acontecendo. Eu sugiro que você primeiro reproduza os exemplos que mostrei aqui para ter certeza de que não é um problema de versão do scikit-learn (já que estou usando 0.22.2) e se você puder reproduzi-lo, sugiro que comece a partir de pequenas partes do seu dados e testá-lo. O uso de amostras de ~104k é difícil de solucionar.

@hermidalc Obrigado pela resposta!
Na verdade, posso reproduzir o resultado acima, então estou solucionando problemas com dados menores agora.

+1

Alguém se importa se eu pegar essa questão?
Parece que #15239 junto com o https://github.com/scikit-learn/scikit-learn/issues/13621#issuecomment -600894432 já tem uma implementação e apenas testes unitários são deixados para fazer.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

naught101 picture naught101  ·  59Comentários

eric-czech picture eric-czech  ·  88Comentários

tdomhan picture tdomhan  ·  58Comentários

mikeroberts3000 picture mikeroberts3000  ·  102Comentários

amueller picture amueller  ·  64Comentários