Comment diviser les données en 3 ensembles (train, validation et test)?

146

J'ai un dataframe pandas et je souhaite le diviser en 3 ensembles séparés. Je sais qu'en utilisant train_test_split from sklearn.cross_validation, on peut diviser les données en deux ensembles (train et test). Cependant, je n'ai trouvé aucune solution pour diviser les données en trois ensembles. De préférence, j'aimerais avoir les indices des données d'origine.

Je sais qu'une solution de contournement serait d'utiliser train_test_splitdeux fois et d'ajuster en quelque sorte les indices. Mais existe-t-il un moyen plus standard / intégré de diviser les données en 3 ensembles au lieu de 2?

CentAu
la source
5
Cela ne répond pas à votre question spécifique, mais je pense que l'approche la plus standard pour cela consisterait à diviser en deux ensembles, former et tester, et exécuter une validation croisée sur l'ensemble de formation, éliminant ainsi le besoin d'un ensemble de «développement» autonome .
David
1
Cela s'est produit avant, et pour autant que je sache, il n'y a pas encore de méthode intégrée pour cela.
ayhan
5
Je suggère The Elements of Statistical Learning de Hastie et al.pour une discussion sur les raisons d'utiliser trois ensembles au lieu de deux ( web.stanford.edu/~hastie/local.ftp/Springer/OLD/… Chapitre sur l' évaluation et la sélection du modèle)
ayhan
2
@David Dans certains modèles, pour éviter le surajustement, il faut 3 ensembles au lieu de 2. Parce que dans vos choix de conception, vous ajustez en quelque sorte les paramètres pour améliorer les performances sur l'ensemble de test. Pour éviter cela, un ensemble de développement est nécessaire. Ainsi, l'utilisation de la validation croisée ne sera pas suffisante.
CentAu
6
@ayhan, une URL corrigée pour ce livre est statweb.stanford.edu/~tibs/ElemStatLearn/printings/… , chapitre 7 (p. 219).
Camille Goudeseune

Réponses:

161

Solution numpy. Nous allons d'abord mélanger l'ensemble de données entier (df.sample (frac = 1)), puis diviser notre ensemble de données dans les parties suivantes:

  • 60% - coffret,
  • 20% - jeu de validation,
  • 20% - ensemble de test

In [305]: train, validate, test = np.split(df.sample(frac=1), [int(.6*len(df)), int(.8*len(df))])

In [306]: train
Out[306]:
          A         B         C         D         E
0  0.046919  0.792216  0.206294  0.440346  0.038960
2  0.301010  0.625697  0.604724  0.936968  0.870064
1  0.642237  0.690403  0.813658  0.525379  0.396053
9  0.488484  0.389640  0.599637  0.122919  0.106505
8  0.842717  0.793315  0.554084  0.100361  0.367465
7  0.185214  0.603661  0.217677  0.281780  0.938540

In [307]: validate
Out[307]:
          A         B         C         D         E
5  0.806176  0.008896  0.362878  0.058903  0.026328
6  0.145777  0.485765  0.589272  0.806329  0.703479

In [308]: test
Out[308]:
          A         B         C         D         E
4  0.521640  0.332210  0.370177  0.859169  0.401087
3  0.333348  0.964011  0.083498  0.670386  0.169619

[int(.6*len(df)), int(.8*len(df))]- est un indices_or_sectionstableau pour numpy.split () .

Voici une petite démonstration d' np.split()utilisation - divisons le tableau de 20 éléments en les parties suivantes: 80%, 10%, 10%:

In [45]: a = np.arange(1, 21)

In [46]: a
Out[46]: array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])

In [47]: np.split(a, [int(.8 * len(a)), int(.9 * len(a))])
Out[47]:
[array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16]),
 array([17, 18]),
 array([19, 20])]
MaxU
la source
@root Que fait exactement le paramètre frac = 1?
SpiderWasp42
1
@ SpiderWasp42, frac=1demande à la sample()fonction de renvoyer toutes les lignes ( 100%ou fraction = 1.0)
MaxU
12
Merci @MaxU. J'aimerais mentionner 2 choses pour simplifier les choses. Commencez par utiliser np.random.seed(any_number)avant la ligne de division pour obtenir le même résultat à chaque exécution. Deuxièmement, pour faire des rapports inégaux comme l' train:test:val::50:40:10utilisation [int(.5*len(dfn)), int(.9*len(dfn))]. Ici, le premier élément indique la taille pour train(0,5%), le deuxième élément indique la taille pour val(1-0,9 = 0,1%) et la différence entre les deux indique la taille pour test(0,9-0,5 = 0,4%). Corrigez-moi si je me trompe :)
dataLeo
hrmm est-ce une erreur quand vous dites "Voici une petite démo pour l'utilisation de np.split () - divisons le tableau de 20 éléments en les parties suivantes: 90%, 10%, 10%:" Je suis presque sûr que vous voulez dire 80 %, 10%, 10%
Kevin
Hé, @MaxU j'ai eu un cas, quelque chose d'un peu similaire. Je me demandais si vous pouviez le regarder pour moi pour voir si c'est le cas et m'aider là-bas. Voici ma question stackoverflow.com/questions/54847668/…
Deepak M
55

Remarque:

La fonction a été écrite pour gérer l'amorçage de la création d'ensemble aléatoire. Vous ne devez pas vous fier à un fractionnement d'ensemble qui ne randomise pas les ensembles.

import numpy as np
import pandas as pd

def train_validate_test_split(df, train_percent=.6, validate_percent=.2, seed=None):
    np.random.seed(seed)
    perm = np.random.permutation(df.index)
    m = len(df.index)
    train_end = int(train_percent * m)
    validate_end = int(validate_percent * m) + train_end
    train = df.iloc[perm[:train_end]]
    validate = df.iloc[perm[train_end:validate_end]]
    test = df.iloc[perm[validate_end:]]
    return train, validate, test

Manifestation

np.random.seed([3,1415])
df = pd.DataFrame(np.random.rand(10, 5), columns=list('ABCDE'))
df

entrez la description de l'image ici

train, validate, test = train_validate_test_split(df)

train

entrez la description de l'image ici

validate

entrez la description de l'image ici

test

entrez la description de l'image ici

piRSquared
la source
1
Je crois que cette fonction nécessite un df avec des valeurs d'index allant de 1 à n. Dans mon cas, j'ai modifié la fonction pour utiliser df.loc car mes valeurs d'index n'étaient pas nécessairement dans cette plage.
iOSBeginner
32

Cependant, une approche pour diviser l'ensemble de données en train, test, cvavec 0.6, 0.2, 0.2serait d'utiliser la train_test_splitméthode deux fois.

from sklearn.model_selection import train_test_split

x, x_test, y, y_test = train_test_split(xtrain,labels,test_size=0.2,train_size=0.8)
x_train, x_cv, y_train, y_cv = train_test_split(x,y,test_size = 0.25,train_size =0.75)
blitu12345
la source
Sous-optimal pour les grands ensembles de données
Maksym Ganenko
@MaksymGanenko Pouvez-vous préciser?
blitu12345
Vous suggérez de fractionner les données en deux opérations distinctes. Chaque fractionnement de données implique la copie de données. Ainsi, lorsque vous suggérez d'utiliser deux opérations séparées au lieu d'une, vous créez artificiellement une charge sur la RAM et le processeur. Votre solution est donc sous-optimale. Le fractionnement des données doit être effectué en une seule opération comme np.split(). En outre, il ne nécessite pas de dépendance supplémentaire sur sklearn.
Maksym Ganenko
@MaksymGanenko a convenu de la charge supplémentaire sur la mémoire, et pour la même chose, nous pouvons supprimer les données d'origine de la mémoire, c'est-à-dire (xtrain & labels)! Et à propos de votre suggestion d'utilisation de numpy est quelque peu limitée aux seuls types de données entiers, qu'en est-il des autres types de données?
blitu12345 le
1
un autre avantage de cette approche est que vous pouvez utiliser les paramètres de stratification.
Ami Tavory
7

Voici une fonction Python qui divise une trame de données Pandas en trames de données d'entraînement, de validation et de test avec un échantillonnage stratifié. Il effectue cette division en appelant train_test_split()deux fois la fonction de scikit-learn .

import pandas as pd
from sklearn.model_selection import train_test_split

def split_stratified_into_train_val_test(df_input, stratify_colname='y',
                                         frac_train=0.6, frac_val=0.15, frac_test=0.25,
                                         random_state=None):
    '''
    Splits a Pandas dataframe into three subsets (train, val, and test)
    following fractional ratios provided by the user, where each subset is
    stratified by the values in a specific column (that is, each subset has
    the same relative frequency of the values in the column). It performs this
    splitting by running train_test_split() twice.

    Parameters
    ----------
    df_input : Pandas dataframe
        Input dataframe to be split.
    stratify_colname : str
        The name of the column that will be used for stratification. Usually
        this column would be for the label.
    frac_train : float
    frac_val   : float
    frac_test  : float
        The ratios with which the dataframe will be split into train, val, and
        test data. The values should be expressed as float fractions and should
        sum to 1.0.
    random_state : int, None, or RandomStateInstance
        Value to be passed to train_test_split().

    Returns
    -------
    df_train, df_val, df_test :
        Dataframes containing the three splits.
    '''

    if frac_train + frac_val + frac_test != 1.0:
        raise ValueError('fractions %f, %f, %f do not add up to 1.0' % \
                         (frac_train, frac_val, frac_test))

    if stratify_colname not in df_input.columns:
        raise ValueError('%s is not a column in the dataframe' % (stratify_colname))

    X = df_input # Contains all columns.
    y = df_input[[stratify_colname]] # Dataframe of just the column on which to stratify.

    # Split original dataframe into train and temp dataframes.
    df_train, df_temp, y_train, y_temp = train_test_split(X,
                                                          y,
                                                          stratify=y,
                                                          test_size=(1.0 - frac_train),
                                                          random_state=random_state)

    # Split the temp dataframe into val and test dataframes.
    relative_frac_test = frac_test / (frac_val + frac_test)
    df_val, df_test, y_val, y_test = train_test_split(df_temp,
                                                      y_temp,
                                                      stratify=y_temp,
                                                      test_size=relative_frac_test,
                                                      random_state=random_state)

    assert len(df_input) == len(df_train) + len(df_val) + len(df_test)

    return df_train, df_val, df_test

Voici un exemple de travail complet.

Considérez un ensemble de données qui a une étiquette sur laquelle vous souhaitez effectuer la stratification. Cette étiquette a sa propre distribution dans l'ensemble de données d'origine, disons 75% foo, 15% baret 10% baz. Maintenant, divisons l'ensemble de données en train, validation et test en sous-ensembles en utilisant un ratio 60/20/20, où chaque division conserve la même distribution des étiquettes. Voir l'illustration ci-dessous:

entrez la description de l'image ici

Voici l'exemple d'ensemble de données:

df = pd.DataFrame( { 'A': list(range(0, 100)),
                     'B': list(range(100, 0, -1)),
                     'label': ['foo'] * 75 + ['bar'] * 15 + ['baz'] * 10 } )

df.head()
#    A    B label
# 0  0  100   foo
# 1  1   99   foo
# 2  2   98   foo
# 3  3   97   foo
# 4  4   96   foo

df.shape
# (100, 3)

df.label.value_counts()
# foo    75
# bar    15
# baz    10
# Name: label, dtype: int64

Maintenant, appelons la split_stratified_into_train_val_test()fonction ci-dessus pour obtenir des trames de données d'entraînement, de validation et de test suivant un rapport 60/20/20.

df_train, df_val, df_test = \
    split_stratified_into_train_val_test(df, stratify_colname='label', frac_train=0.60, frac_val=0.20, frac_test=0.20)

Les trois dataframes df_train, df_valet df_testcontiennent toutes les lignes d'origine, mais leurs tailles suivront le rapport ci-dessus.

df_train.shape
#(60, 3)

df_val.shape
#(20, 3)

df_test.shape
#(20, 3)

De plus, chacune des trois divisions aura la même répartition du label, à savoir 75% foo, 15% baret 10% baz.

df_train.label.value_counts()
# foo    45
# bar     9
# baz     6
# Name: label, dtype: int64

df_val.label.value_counts()
# foo    15
# bar     3
# baz     2
# Name: label, dtype: int64

df_test.label.value_counts()
# foo    15
# bar     3
# baz     2
# Name: label, dtype: int64
stackoverflowuser2010
la source
NameError: le nom 'df' n'est pas défini. Le 'df' dans split_stratified_into_train_val_test () doit être remplacé par 'df_input'.
Fantasy Pollock
Merci. Je l'ai corrigé. Le problème était dans un chemin de gestion des erreurs du code.
stackoverflowuser2010
1

Il est très pratique à utiliser train_test_splitsans effectuer de réindexation après avoir divisé en plusieurs ensembles et ne pas écrire de code supplémentaire. La meilleure réponse ci-dessus ne mentionne pas qu'en séparant deux fois en train_test_splitne modifiant pas la taille des partitions, cela ne donnera pas la partition initialement prévue:

x_train, x_remain = train_test_split(x, test_size=(val_size + test_size))

Ensuite, la partie des ensembles de validation et de test dans le x_remain change et pourrait être comptée comme

new_test_size = np.around(test_size / (val_size + test_size), 2)
# To preserve (new_test_size + new_val_size) = 1.0 
new_val_size = 1.0 - new_test_size

x_val, x_test = train_test_split(x_remain, test_size=new_test_size)

Dans cette occasion, toutes les partitions initiales sont enregistrées.

A.Ametov
la source