In [0]:
# Import potrebných balíčkov
import pandas as pd
from sklearn.preprocessing import OrdinalEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.impute import SimpleImputer
In [25]:
# Uistíme sa, že máme všetky potrebné dáta
!mkdir -p data
!wget -nc -O data/iris.csv https://www.dropbox.com/s/v3ptdkv5fvmx5zk/iris.csv?dl=1
File ‘data/iris.csv’ already there; not retrieving.

Predspracovanie dát, tréning a nové dáta

Ako treba postupovať, ak chceme v Python-e natrénovať jednoduchý model – napríklad rozhodovací strom – už vieme:

  • Načítame dátovú množinu.
  • Vyčístíme a predspracujeme ju.
    • Napr. preškálujeme numerické dáta do vhodných rozsahov.
    • Prekódujeme kategorické premenné z textovej do číselnej reprezentácie.
    • Ošetríme chýbajúce hodnoty.
  • Oddelíme tréningové a testovacie dáta (ak ešte nie sú rozdelené).
  • Natrénujeme model na tréningových dátach.
  • Otestujeme zovšeobecnenie na testovacích dátach.

Príklad preškálovania a prekódovania

Pozrime sa na jednoduchý príklad preškálovania a prekódovania. Povedzme, že máme nasledujúcu dátovú množinu:

In [6]:
df = pd.DataFrame(
    [
        ["Vendelín Lopota", "male", 15],
        ["Múdroslav Kucharčík", "male", 32],
        ["Pamfília Hamižná", "female", 12],
        ["Klotylda Berevčáková", "female", 89]
    ],
    columns = ["name", "gender", "age"]
)

df
Out[6]:
name gender age
0 Vendelín Lopota male 15
1 Múdroslav Kucharčík male 32
2 Pamfília Hamižná female 12
3 Klotylda Berevčáková female 89

Stĺpec gender

Stĺpec gender obsahuje kategorickú premennú – textové hodnoty budeme chcieť prekódovať na číselné:

In [7]:
ordenc = OrdinalEncoder()
df['gender_num'] = ordenc.fit_transform(df[['gender']])
df[['gender', 'gender_num']]
Out[7]:
gender gender_num
0 male 1.0
1 male 1.0
2 female 0.0
3 female 0.0

Ako vidno, pohlavie female bolo prekódované ako 0 a pohlavie male ako 1. Overiť si to môžeme aj podľa poradia jednotlivých označení v nasledujúcom zozname:

In [8]:
ordenc.categories_
Out[8]:
[array(['female', 'male'], dtype=object)]

Stĺpec age

Stĺpec age obsahuje číselné hodnoty – štandardizujeme ich (stredná hodnota 0 a rozptyl 1):

In [9]:
scaler = StandardScaler()
df['age_scaled'] = scaler.fit_transform(df[['age']])
df[['age', 'age_scaled']]
Out[9]:
age age_scaled
0 15 -0.710232
1 32 -0.161416
2 12 -0.807082
3 89 1.678730

Predspracovanie nových dát

Dajme tomu, že sme vyššie uvedený kód použili na predspracovanie dátovej množiny, na ktorej sme natrénovali model. Teraz dostaneme nové dáta a aj pre ne chceme vykonať predikciu. Ako ich predspracujeme?

In [0]:
df_new = pd.DataFrame(
    [
        ["Polykarp Chrapota", "male", 32],
        ["Lucián Chrasta", "male", 45]
    ],
    columns = ["name", "gender", "age"]
)

Prirodzene, že nechceme celý kód znovu zopakovať. Povedzme, že budeme teda postupovať naivne: kód si obalíme do funkcie preprocess a tú istú funkciu zavoláme najprv pre pôvodné a potom pre nové dáta.

In [0]:
def preprocess(df):
    ordenc = OrdinalEncoder()
    df['gender_num'] = ordenc.fit_transform(df[['gender']])
    
    scaler = StandardScaler()
    df['age_scaled'] = scaler.fit_transform(df[['age']])

    return df

Funkcia je hotová – čo sa teda stane, keď ju aplikujeme na pôvodné a na nové dáta?

In [12]:
preprocess(df)
Out[12]:
name gender age gender_num age_scaled
0 Vendelín Lopota male 15 1.0 -0.710232
1 Múdroslav Kucharčík male 32 1.0 -0.161416
2 Pamfília Hamižná female 12 0.0 -0.807082
3 Klotylda Berevčáková female 89 0.0 1.678730
In [13]:
preprocess(df_new)
Out[13]:
name gender age gender_num age_scaled
0 Polykarp Chrapota male 32 0.0 -1.0
1 Lucián Chrasta male 45 0.0 1.0

Na prvý pohľad sa zdá, že všetko funguje – ale len kým sa na výsledky nepozrieme pozornejšie. Potom zistíme, že zatiaľ čo na pôvodných dátach bol textový reťazec male prekódovaný ako 1, v nových dátach je prekódovaný ako 0. Nekonzistentne je prekódovaný aj vek.

Prečo vznikol taký rozdiel? Samozrejme preto, lebo sme pri predspracovaní vnútri funkcie preprocess vytvorili nové transformátory, ktoré nevedia nič o pôvodnej dátovej množine. Keďže v novej dátovej množine sa vyskytuje len hodnota male, samozrejme je prekódovaná ako 0 a nie ako 1. Vekový rozsah je menší – 32 je v novej dátovej množine minimálny vek, preto je tiež prekódovaný inak.

Ako to pohodlne riešiť: Pipelines

Aby sme vyššie uvedený kód opravili, museli byť sme zabezpečiť, že si odložíme transformátory ordenc a scaler a tie isté znovu použijeme aj pri spracovaní nových dát. Pri zložitejších dátových množinách, kde je predspracovanie netriviálne, je to však prácne a ľahko môže dôjsť ku chybám. Existuje preto nástroj, ktorý to uľahčuje: sklearn pipelines.

Ukážme si teda, ako by sa vyššie uvedený príklad implementoval pomocou pipelines:

In [0]:
from sklearn.compose import make_column_transformer
from sklearn.pipeline import make_pipeline

Použijeme funkciu make_column_transformer, ktorá nám umožní na každý stĺpec (alebo skupinu stĺpcov) aplikovať inú transformáciu:

In [0]:
categorical_inputs = ['gender']
numeric_inputs = ['age']
In [0]:
preproc_pipeline = make_column_transformer(
    (OrdinalEncoder(), categorical_inputs),
    (StandardScaler(), numeric_inputs)
)

Najprv pomocou funkcie fit_transform vytvorený pipeline objekt nastavíme a zároveň aj predspracujeme pôvodnú dátovú množinu:

In [17]:
preproc_pipeline.fit_transform(df)
Out[17]:
array([[ 1.        , -0.71023193],
       [ 1.        , -0.16141635],
       [ 0.        , -0.80708174],
       [ 0.        ,  1.67873001]])

Následne môžeme to isté predspracovanie jednoducho aplikovať aj na nové dáta pomocou funkcie transform:

In [18]:
preproc_pipeline.transform(df_new)
Out[18]:
array([[ 1.        , -0.16141635],
       [ 1.        ,  0.25826616]])

Ako vidno, v tomto prípade sa už hodnoty transformujú konzistentne.

Pipelines na dátovej množine Iris

Do pipeline objektu sa dá zaradiť aj samotný klasifikátor. Aby sme videli aj kompletný príklad, môžeme si teraz ukázať použitie pipelines na dátovej množine Iris.

Načítanie dát a nastavenie predspracovania

In [26]:
df = pd.read_csv("data/iris.csv")
df.head()
Out[26]:
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) species
0 5.1 3.5 1.4 0.2 setosa
1 4.9 3.0 1.4 0.2 setosa
2 4.7 3.2 1.3 0.2 setosa
3 4.6 3.1 1.5 0.2 setosa
4 5.0 3.6 1.4 0.2 setosa

Na tréningové a testovacie dáta je najlepšie deliť hneď na začiatku – aby sme v rámci predspracovania predsa len do tréningových dát nepreniesli nejakú informáciu z testovacej množiny. V dátových súťažiach bývajú dáta niekedy už predrozdelené. Parameter random_state špecifikuje jadro pseudonáhodného generátora. Za normálnych okolností sa dátová množina rozdelí zakaždým inak. My budeme jadro niekedy fixovať, aby sme v každom behu dostali podľa možnosti podobné výsledky.

In [0]:
df_train, df_test = train_test_split(df, test_size=0.25,
                                     stratify=df['species'],
                                     random_state=4)

Prvé štyri stĺpce obsahujú vstupy, posledný stĺpec triedu:

In [0]:
categorical_inputs = []
numeric_inputs = ['sepal length (cm)', 'sepal width (cm)',
                  'petal length (cm)', 'petal width (cm)']
output = ['species']
In [0]:
X_train = df_train[categorical_inputs + numeric_inputs]
Y_train_l = df_train[output]

X_test = df_test[categorical_inputs + numeric_inputs]
Y_test_l = df_test[output]

V prípade dátovej množiny Iris máme len numerické vstupy. Aplikujme na ne škálovanie. Kategorické vstupy nie sú žiadne, takže príslušný riadok by sme správne mali úplne vynechať – uvádzame ho len preto, aby sa tie isté kúsky kódu dali ľahko použiť pri ďalších príkladoch. Takisto kvôli všeobecnosti pridávame aj transformátor SimpleImputer – triviálny spôsob ako ošetriť chýbajúce dáta, ak by v dátovej množine náhodou boli.

In [0]:
input_preproc = make_column_transformer(
    (make_pipeline(
        SimpleImputer(strategy='constant', fill_value='MISSING'),
        OrdinalEncoder()
    ),
    categorical_inputs),
    
    (make_pipeline(
        SimpleImputer(),
        StandardScaler()
    ),
    numeric_inputs)
)

Prekódujeme výstupný stĺpec:

In [0]:
output_preproc = OrdinalEncoder()
Y_train = output_preproc.fit_transform(Y_train_l)
Y_test = output_preproc.transform(Y_test_l)

Tréning modelu ako súčasť pipeline

Po predspracovaní budeme na dáta aplikovať rozhodovací strom. Pipeline na predspracovanie zreťazíme s pipeline pre učenie:

In [0]:
model = make_pipeline(
    input_preproc,
    DecisionTreeClassifier()
)

Ďalej môžeme už trénovať celý pipeline objekt – fázu predspracovania aj rozhodovací strom:

In [0]:
model = model.fit(X_train, Y_train)

Otestujeme zovšeobecnenie:

In [0]:
y_test = model.predict(X_test)
y_test_l = output_preproc.inverse_transform(y_test.reshape(-1, 1))
In [45]:
cm = pd.crosstab(Y_test_l.values.reshape(-1),
                 y_test_l.reshape(-1),
                 rownames=['actual'],
                 colnames=['predicted'])
print(cm)
predicted   setosa  versicolor  virginica
actual                                   
setosa          13           0          0
versicolor       0          11          1
virginica        0           1         12
In [46]:
print("Accuracy = {}".format(accuracy_score(Y_test, y_test)))
Accuracy = 0.9473684210526315
In [0]: