Jak podzielić dane na 3 zbiory (trenowanie, walidacja i testowanie)?

146

Mam ramkę danych pandy i chcę ją podzielić na 3 oddzielne zestawy. Wiem, że używając train_test_split z sklearn.cross_validation, można podzielić dane na dwa zestawy (pociąg i test). Nie mogłem jednak znaleźć żadnego rozwiązania dotyczącego podziału danych na trzy zestawy. Najlepiej, jeśli posiadam indeksy oryginalnych danych.

Wiem, że obejściem byłoby train_test_splitdwukrotne użycie i jakoś skorygowanie indeksów. Ale czy istnieje bardziej standardowy / wbudowany sposób dzielenia danych na 3 zestawy zamiast 2?

CentAu
źródło
5
To nie odpowiada na Twoje konkretne pytanie, ale myślę, że bardziej standardowym podejściem byłoby podzielenie na dwa zestawy, trenowanie i testowanie oraz przeprowadzenie weryfikacji krzyżowej na zestawie szkoleniowym, eliminując w ten sposób potrzebę samodzielnego zestawu „rozwoju” .
David,
1
To pojawiło się wcześniej i o ile wiem, nie ma jeszcze wbudowanej metody.
ayhan
5
Proponuję Hastie i wsp. The Elements of Statistical Learning do dyskusji na temat tego, dlaczego używać trzech zestawów zamiast dwóch ( web.stanford.edu/~hastie/local.ftp/Springer/OLD/ ... rozdział Ocena i wybór modelu)
ayhan
2
@David W niektórych modelach, aby zapobiec nadmiernemu dopasowaniu, potrzebne są 3 zestawy zamiast 2. Ponieważ przy wyborze projektu w jakiś sposób dostrajasz parametry, aby poprawić wydajność zestawu testowego. Aby temu zapobiec, wymagany jest zestaw deweloperski. Tak więc użycie walidacji krzyżowej nie będzie wystarczające.
CentAu
6
@ayhan, poprawiony adres URL tej książki to statweb.stanford.edu/~tibs/ElemStatLearn/printings/… , rozdział 7 (s. 219).
Camille Goudeseune

Odpowiedzi:

161

Odrętwiałe rozwiązanie. Najpierw przetasujemy cały zestaw danych (df.sample (frac = 1)), a następnie podzielimy nasz zestaw danych na następujące części:

  • 60% - skład pociągu,
  • 20% - zestaw walidacyjny,
  • 20% - zestaw testowy

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))]- jest indices_or_sectionstablicą dla numpy.split () .

Oto małe demo do np.split()użycia - podzielmy 20-elementową tablicę na następujące części: 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
źródło
@root, co dokładnie robi parametr frac = 1?
SpiderWasp42
1
@ SpiderWasp42, frac=1instruuje sample()funkcję, aby zwróciła wszystkie ( 100%lub fraction = 1.0) wiersze
MaxU
12
Dzięki @MaxU. Chciałbym wspomnieć o 2 rzeczach, aby uprościć sprawę. Po pierwsze, użyj np.random.seed(any_number)przed linią podziału, aby uzyskać ten sam wynik przy każdym przebiegu. Po drugie, aby uzyskać nierówne proporcje, takie jak train:test:val::50:40:10użycie [int(.5*len(dfn)), int(.9*len(dfn))]. Tutaj pierwszy element oznacza rozmiar dla train(0,5%), drugi element oznacza rozmiar dla val(1-0,9 = 0,1%), a różnica między nimi oznacza rozmiar dla test(0,9-0,5 = 0,4%). Popraw mnie, jeśli się mylę :)
dataLeo
hrmm czy to pomyłka, kiedy mówisz "Oto małe demo do użycia np.split () - podzielmy 20-elementową tablicę na następujące części: 90%, 10%, 10%:" Jestem pewien, że masz na myśli 80 %, 10%, 10%
Kevin,
Hej, @MaxU miałem przypadek, coś podobnego. Zastanawiałem się, czy mógłbyś na to spojrzeć, żeby zobaczyć, czy tak jest i pomóc mi w tym. Oto moje pytanie stackoverflow.com/questions/54847668/ ...
Deepak M
55

Uwaga:

Funkcja została napisana, aby obsłużyć tworzenie losowych zestawów. Nie powinieneś polegać na podziale zestawów, który nie powoduje ich losowania.

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

Demonstracja

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

wprowadź opis obrazu tutaj

train, validate, test = train_validate_test_split(df)

train

wprowadź opis obrazu tutaj

validate

wprowadź opis obrazu tutaj

test

wprowadź opis obrazu tutaj

piRSquared
źródło
1
Uważam, że ta funkcja wymaga df z wartościami indeksu od 1 do n. W moim przypadku zmodyfikowałem funkcję, aby używała df.loc, ponieważ moje wartości indeksu niekoniecznie znajdowały się w tym zakresie.
iOSBeginner
32

Jednakże, jednym z podejść do rozdzielenia zestawu danych w train, test, cvo 0.6, 0.2, 0.2byłoby użyć train_test_splitmetody dwukrotnie.

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
źródło
Nieoptymalne dla dużych zbiorów danych
Maksym Ganenko
@MaksymGanenko Czy możesz to rozwinąć?
blitu12345
Sugerujesz podzielenie danych za pomocą dwóch oddzielnych operacji. Każdy podział danych obejmuje kopiowanie danych. Kiedy więc sugerujesz użycie dwóch oddzielnych operacji podziału zamiast jednej, sztucznie obciążasz zarówno pamięć RAM, jak i procesor. Twoje rozwiązanie jest więc nieoptymalne. Podział danych należy wykonać za pomocą jednej operacji, takiej jak np.split(). Ponadto nie wymaga dodatkowej zależności od sklearn.
Maksym Ganenko
@MaksymGanenko zgodził się na dodatkowe obciążenie pamięci i tym samym możemy usunąć oryginalne dane z pamięci tj. (Xtrain & labels)! A jeśli chodzi o twoją sugestię dotyczącą używania numpy, jest nieco ograniczona tylko do typów danych całkowitych, a co z innymi typami danych?
blitu12345
1
Inną zaletą tego podejścia jest to, że można użyć parametrów stratyfikacji.
Ami Tavory
7

Oto funkcja Pythona, która dzieli ramkę danych Pandas na pociąg, walidację i testowe ramki danych z próbkowaniem warstwowym. Dokonuje tego podziału, dwukrotnie wywołując funkcję scikit-learn train_test_split().

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

Poniżej znajduje się pełny przykład roboczy.

Rozważ zbiór danych z etykietą, na podstawie której chcesz przeprowadzić stratyfikację. Ta etykieta ma swoją własną dystrybucję w oryginalnym zbiorze danych, powiedzmy 75% foo, 15% bari 10% baz. Teraz podzielmy zestaw danych na pociąg, walidację i test na podzbiory przy użyciu stosunku 60/20/20, gdzie każdy podział zachowuje ten sam rozkład etykiet. Zobacz poniższą ilustrację:

wprowadź opis obrazu tutaj

Oto przykładowy zbiór danych:

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

Teraz split_stratified_into_train_val_test()wywołajmy funkcję z góry, aby uzyskać pociąg, walidację i przetestować ramki danych zgodnie ze stosunkiem 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)

Trzy ramki danych df_train, df_vali df_testzawierają wszystkie oryginalne wiersze, ale ich rozmiary będą zgodne z powyższym współczynnikiem.

df_train.shape
#(60, 3)

df_val.shape
#(20, 3)

df_test.shape
#(20, 3)

Ponadto każdy z trzech podziałów będzie miał taką samą dystrybucję etykiety, a mianowicie 75% foo, 15% bari 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
źródło
NameError: nazwa „df” nie jest zdefiniowana. „Df” w split_stratified_into_train_val_test () należy zastąpić „df_input”.
Fantasy Pollock
Dzięki. Naprawiłem to. Problem dotyczy ścieżki obsługi błędów kodu.
stackoverflowuser2010
1

Jest bardzo wygodny w użyciu train_test_splitbez wykonywania ponownego indeksowania po podzieleniu na kilka zestawów i bez pisania dodatkowego kodu. Najlepsza odpowiedź powyżej nie wspomina, że ​​dwukrotne oddzielenie przy użyciu train_test_splitniezmieniających się rozmiarów partycji nie da początkowo zamierzonej partycji:

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

Następnie część zestawów walidacyjnych i testowych w x_remain zmienia się i można ją policzyć jako

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)

W tym przypadku wszystkie partycje początkowe są zapisywane.

A.Ametov
źródło