#!/usr/bin/env python
# coding: utf-8
# In[1]:
from datetime import datetime
print(f'Päivitetty {datetime.now()}')
# # Datan valmistelu koneoppimista varten
#
# Koneoppimisen malleissa käytettävän datan osalta on huomioitava:
#
# * Data ei saa sisältää puuttuvia arvoja.
# * Kategoriset muuttujat pitää muuntaa dummy-muuttujiksi.
# * Datan standardointi voi auttaa parempien mallien luomisessa.
# * Paljon muista poikkeavat arvot kannattaa joissain tapauksissa poistaa.
# * Sopivat muuttujien muunnokset saattavat tuottaa parempia malleja.
# * Jos ennustettava muuttuja on kategorinen, jonka jokin luokka on opetusdatassa aliedustettuna, niin datan tasapainottamisen jälkeen saadaan yleensä parempi malli.
#
# Tämä muistio sisältää esimerkkejä yllä mainittujen seikkojen hoitamiseen.
# In[2]:
import numpy as np
import pandas as pd
# In[3]:
# Avaan esimerkeissä käytettävän datan
df = pd.read_excel('https://taanila.fi/data1.xlsx')
# Kaikki rivit ja sarakkeet näytetään
pd.options.display.max_rows = None
pd.options.display.max_columns = None
# Datan muuttujat listana
df.columns
# In[4]:
# Tietoa muuttujista
df.info()
# ## Puuttuvat arvot
#
# Puuttuvat arvot voin hoitaa kahdella tavalla:
# * poistamalla puuttuvia arvoja sisältävät rivit tai
# * täydentämällä puuttuvat arvot tilanteeseen sopivilla arvoilla.
#
# Puuttuvia arvoja sisältävät rivit voin poistaa dropna()-toiminnolla:
#
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.dropna.html
#
# Puuttuvia arvoja voin täydentää fillna()-toiminnolla:
#
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.fillna.html
#
# Katson ensin dataa värjäämällä puuttuvat arvot:
# In[5]:
df.style.highlight_null()
# Esimerkkidatassa neljän viimeisen sarakkeen osalta puuttuvia arvoja sisältävien rivien poistaminen ei tule kyseeseen, koska dataa ei tämän jälkeen jäisi jäljelle.
#
# Seuraavassa poistan rivit, joilla on puuttuvia arvoja muuttujissa 'koulutus', 'työtov' ja 'palveluv' sekä täydennän neljän viimeisen muuttujan puuttuvat arvot nolliksi. Näin toimimalla menetän datasta 3 riviä.
# In[6]:
df1 = df.dropna(subset=['koulutus', 'työtov', 'palveluv'])
df1 = df1.fillna({'työterv':0, 'lomaosa':0, 'kuntosa':0, 'hieroja':0})
# Katson kuinka monta riviä jäi jäljelle
df1.shape[0]
# Seuraavassa täydennän kaikki puuttuvat arvot, jolloin dataan jää alkuperäinen määrä rivejä. Eri muuttujille käytän erilaisia korvaamismenetelmiä (mediaani, keskiarvo, 0).
# In[7]:
df2 = df.fillna({'koulutus': df['koulutus'].median(),
'työtov': df['työtov'].mean(),
'palveluv': df['palveluv'].mean(),
'työterv':0, 'lomaosa':0, 'kuntosa':0, 'hieroja':0})
# Katson kuinka monta riviä jäi jäljelle
df2.shape[0]
# ## Kategoristen muuttujien muuntaminen dummy-muuttujiksi
#
# Pandas-kirjaston get_dummies-toiminto muuntaa kategoriset muuttujat dummy-muuttujiksi.
#
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.get_dummies.html
#
# Esimerkiksi sukup-muuttuja saa arvoja 1 (mies) ja 2 (nainen). get_dummies-toiminto tekee sukup-muuttujasta muuttujat sukup_1 ja sukup_2. Jos kyseessä on mies, niin sukup_1-muuttujan arvo on 1. Jos kyseessä on nainen, niin sukup_2-muuttujan arvo on 1.
#
# Seuraavassa muunnan sukup-, perhe- ja koulutus-muuttujat dummy-muuttujiksi:
# In[8]:
df_dummies = pd.get_dummies(data=df2, columns=['sukup', 'perhe', 'koulutus'])
# Katson datan muuttujat
df_dummies.info()
# ## Standardointi
#
# Jos muuttujat ovat suuruusluokaltaan erilaisia, niin muuttujien skaalauksella voidaan joissain tapauksissa päästä parempiin malleihin. Standardointi on paljon käytetty skaalausmenetelmä. Standardoinnissa muuttujan arvot muunnetaan normaalijakauman z-pisteiksi. Z-piste ilmoittaa kuinka monen keskihajonnan päässä muuttujan arvo on kaikkien arvojen keskiarvosta.
#
# Standardoinnin voin toteuttaa sklearn.preprocessing-kirjastosta tuodulla StandardScaler-toiminnolla.
#
# https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html
# In[9]:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
# Tässä standardoin iän, palkan ja palveluvuodet
df2[['ikä', 'palkka', 'palveluv']] = pd.DataFrame(scaler.fit_transform(df2[['ikä', 'palkka', 'palveluv']]))
df2
# ## Poikkeavat arvot
#
# Poikkeavina arvoina voidaan pitää arvoja, jotka olisivat normaalijakaumassa epätodennäköisiä. Tällaisia arvoja sisältävien rivien poistaminen voi joissain tapauksissa parantaa mallia. Poikkeavien arvojen poistamisen mielekkyys riippuu monista seikoista ja on harkittava kussakin tapauksessa erikseen.
#
# Poistaminen voidaan tehdä z-pisteiden (standardoitujen arvojen) perusteella. Z-piste ilmoittaa kuinka monen keskihajonnan päässä arvo on kaikkien arvojen keskiarvosta. Usein rajana käytetään arvoa 3: jos muuttujan arvo on yli kolmen keskihajonnan päässä keskiarvosta, niin se poistetaan.
#
# Lisätietoa poikkeavien arvojen poistamisesta:
#
# https://stackoverflow.com/questions/23199796/detect-and-exclude-outliers-in-pandas-data-frame
#
# Seuraavassa lasken normaalijakauman todennäköisyyden itseisarvoltaan yli kolmen (3) suuruisille z-pisteille:
# In[10]:
from scipy import stats
print('Todennäköisyys sille, että arvo on yli kolmen keskihajonnan päässä keskiarvostaan',
2*stats.norm.cdf(-3))
# Edellä standardoin df2:n palkan. Katsotaan viisi suurinta ja pienintä z-pistettä:
# In[11]:
df2['palkka'].nlargest(n=5)
# In[12]:
df2['palkka'].nsmallest(n=5)
# Yllä olevan mukaisesti poistettavaksi joutuisivat kaksi suurinta palkkaa, joiden z-pisteet ovat suurempia kuin 3.
#
# Poistaminen sujuu yhdellä koodirivillä. Seuraava koodi toimii vaikka z-pisteitä ei olisi dataan ennestään laskettukaan:
# In[13]:
df3 = df2[(np.abs(stats.zscore(df2))<3).all(axis=1)]
# Katson kuinka monta riviä jäi jäljelle
df3.shape[0]
# Tässä tapauksessa poikkeavien arvojen poistaminen johti ainoastaan kahden rivin poistamiseen.
#
# Jos olet tottunut käyttämään lambdaa, niin edellisen voi tehdä myös seuraavasti (tässä lasken z-pisteet ilman stats.zscore()-toimintoa):
# In[14]:
df4 = df2[df2.apply(lambda x: np.abs(x-x.mean())/x.std()<3).all(axis = 1)]
# Katson kuinka monta riviä jäi jäljelle
df4.shape[0]
# ## Logaritmimuunnos
#
# Muuttujien normaalijakaumasta poikkeavia jakaumia on mahdollista korjata lähemmäksi normaalijakaumaa muuttujien muunnoksilla. Paljon käytetty muunnos vinon jakauman korjaamiseen on logaritmien ottaminen.
#
# Seuraavassa muunnan palkka-muuttujan arvot logaritmeikseen. Histogrammilla voin nopeasti tarkistaa korjaantuiko jakauma lähemmäksi normaalijakaumaa.
# In[15]:
df['palkka_log'] = np.log(df['palkka'])
# Katsotaan muuuttujien histogrammit
df[['palkka', 'palkka_log']].hist()
# Logaritmia ei voi ottaa nollasta. Tämän vuoksi esimerkkidatan palveluvuosiin en voi käyttää logaritmimuunnosta. Jos lisään palveluvuosiin yhden vuoden (jolloin muuttuja ilmoittaa kuinka monetta vuotta henkilö palvelee yrityksessä), niin logaritmimuunnos onnistuu:
# In[16]:
df['palveluv_log'] = np.log(df['palveluv']+1)
df[['palveluv', 'palveluv_log']].hist()
# Muuttujien muunnoksia käytetään eri tarkoituksiin ja muunnokset ovat laaja ja monitahoinen aihe. Logaritmin ohella paljon käytettyjä muunnoksia ovat neliöjuuri, toiseen potenssiin korottaminen, käänteisluku jne.
# ## Datan tasapainottaminen
#
# Luokittelumalleja käytettäessä opetusdata kannattaa tasapainottaa jos jokin luokista on selvästi aliedustettuna. Tasapainottamisen voi tehdä monella tavalla. Katso https://towardsdatascience.com/5-techniques-to-work-with-imbalanced-data-in-machine-learning-80836d45d30c
#
# Helppo tapa tasapainoltukseen on käyttää **imbalanced-learn** -kirjastoa: https://imbalanced-learn.org/stable/.
# Kirjaston voi asentaa Anacondaan komentoriviltä (Anaconda prompt) komennolla:
#
# `conda install -c conda-forge imbalanced-learn`
# In[17]:
# Jos df2:n kuntosalin käyttöä mittaava muuttuja olisi kohdemuuttujana
# niin käyttäjien määrä on aliedustettuna
df2['kuntosa'].value_counts()
# In[18]:
# Tuodaan RandomOverSampler
from imblearn.over_sampling import RandomOverSampler
# Selittävät muuttujat
X = df2.drop('kuntosa', axis=1)
# Kohdemuuttuja
y = df2['kuntosa']
# Tasapainotus
ros = RandomOverSampler(random_state=2)
X, y = ros.fit_resample(X, y)
# Tarkistetaan kohdemuuttujan jakauma
pd.DataFrame(y).value_counts()
# ## Lisätietoa
#
# Data-analytiikka Pythonilla: https://tilastoapu.wordpress.com/python/