Wednesday, July 31, 2013

Pandas ile Kategorik Veriyi Ikisel Hale Getirmek (One-Hot Encoding)

Yapay ogrenim algoritmalarinin cogu zaman hem kategorik hem numerik degerleri ayni anda bulunduran verilerle is yapmasi gerekebiliyor. Ayrica literature bakilinca gorulur ki cogunlukla bir algoritma ya biri, ya digeri ile calisir, ikisi ile ayni anda calismaz (calisanlar var tabii, mesela karar agaclari -decision tree-). Bu gibi durumlarda iki secenek var, ya numerik veri kategorisellestirilir (ayriksallastirilir), ya da kategorik veri numerik hale getirilir.

Mesela su veri seti

   pop   state  year
0  1.5    Ohio  2000
1  1.7    Ohio  2001
2  3.6    Ohio  2002
3  2.4  Nevada  2001
4  2.9  Nevada  2002


su hale getirilir

   pop  year  state=Ohio  state=Nevada
0  1.5  2000           1             0
1  1.7  2001           1             0
2  3.6  2002           1             0
3  2.4  2001           0             1
4  2.9  2002           0             1


Bu durumda, kategorik bir kolon eyalet icin, eyaletin Ohio olup olmamasi basli basina ayri bir kolon olarak gosteriliyor. Ayni sekilde Nevada. Bu kodlamaya literaturde One-Hot Kodlamasi adi veriliyor. KMeans, lojistik regresyon gibi metotlara girdi vermek icin bu transformasyon kullanilabilir.

import numpy as np
import pandas as pd, os
import scipy.sparse as sps
from sklearn.feature_extraction import DictVectorizer

def one_hot_dataframe(data, cols, replace=False):
    vec = DictVectorizer()
    mkdict = lambda row: dict((col, row[col]) for col in cols)
    vecData = pd.DataFrame(vec.fit_transform(data[cols].apply(mkdict, axis=1)).toarray())
    vecData.columns = vec.get_feature_names()
    vecData.index = data.index
    if replace is True:
        data = data.drop(cols, axis=1)
        data = data.join(vecData)
    return (data, vecData, vec)

data = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada'],
        'year': [2000, 2001, 2002, 2001, 2002],
        'pop': [1.5, 1.7, 3.6, 2.4, 2.9]}

df = pd.DataFrame(data)

df2, _, _ = one_hot_dataframe(df, ['state'], replace=True)
print df2


Unutmayalim, kategorik degerler bazen binleri bulabilir (hatta sayfa tiklama tahmini durumunda mesela milyonlar, hatta milyarlar), bu da binlerce yeni kolon demektir.

Fakat 1/0 kodlamasi, yani one-hot isleminden ele gecen yeni blok icinde aslinda oldukca cok sayida sifir degeri vardir (sonucta her satirda binlerce 'sey' icinde sadece bir tanesi 1 oluyor), yani bu blok bir seyrek matris olacaktir. O zaman matrisin tamamini sps.csr_matrix ya da sps.lil_matrix ile gercekten seyrek formata cevirebiliriz, ve mesela Scikit Learn adli yapay ogrenim paketi, sifirdan YO kodlamak istersek Numpy, Scipy islemleri seyrek matrisler ile hesap yapabilme yetenegine sahip. Seyreksellestirince ne elde ediyoruz? Sifirlari depolamadigimiz icin sadece sifir olmayan degerler ile islem yapiyoruz, o olcude kod hizlaniyor, daha az yer tutuyor.

Dikkat etmek gerekir ki yeni kolonlari uretince degerlerin yerleri sabitlenmis olur. Her satir bazinda bazen state=Ohio, state=Nevada, bazen sadece state=Ohio uretiyor olamayiz. Ustteki ornekte her zaman 4 tane kolon elde edilmelidir.

Bastan Seyrek Matris ile Calismak

Buyuk Veri ortaminda, eger kategorik degerler milyonlari buluyorsa, o zaman ustteki gibi normal Numpy matrisinden seyrege gecis yapmak bile kulfetli olabilir. Bu durumlarda daha en bastan seyrek matris uretiyor olmaliyiz. Mevcut tum degerleri onceden bildigimizi farz edersek,

import numpy as np
import pandas as pd, os
import scipy.sparse as sps
import itertools

def one_hot_column(df, cols, vocabs):
    mats = []; df2 = df.drop(cols,axis=1)
    mats.append(sps.lil_matrix(np.array(df2)))
    for i,col in enumerate(cols):
        mat = sps.lil_matrix((len(df), len(vocabs[i])))
        for j,val in enumerate(np.array(df[col])):
            mat[j,vocabs[i][val]] = 1.
        mats.append(mat)

    res = sps.hstack(mats)   
    return res
           
data = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada'],
        'year': ['2000', '2001', '2002', '2001', '2002'],
        'pop': [1.5, 1.7, 3.6, 2.4, 2.9]}

df = pd.DataFrame(data)
print df

vocabs = []
vals = ['Ohio','Nevada']
vocabs.append(dict(itertools.izip(vals,range(len(vals)))))
vals = ['2000','2001','2002']
vocabs.append(dict(itertools.izip(vals,range(len(vals)))))

print vocabs

print one_hot_column(df, ['state','year'], vocabs).todense()


Sonuc olarak

   pop   state  year
0  1.5    Ohio  2000
1  1.7    Ohio  2001
2  3.6    Ohio  2002
3  2.4  Nevada  2001
4  2.9  Nevada  2002


[{'Ohio': 0, 'Nevada': 1}, {'2002': 2, '2000': 0, '2001': 1}]


[[ 1.5  1.   0.   1.   0.   0. ]
 [ 1.7  1.   0.   0.   1.   0. ]
 [ 3.6  1.   0.   0.   0.   1. ]
 [ 2.4  0.   1.   0.   1.   0. ]
 [ 2.9  0.   1.   0.   0.   1. ]]


one_hot_column cagrisina bir "sozlukler listesi" verdik, sozluk her kolon icin o kolonlardaki mumkun tum degerleri bir sira sayisi ile esliyor. Sozluk listesinin sirasi kolon sirasina uyuyor olmali.

Niye sozluk verdik? Bunun sebebi eger azar azar (incremental) ortamda is yapiyorsak, ki Buyuk Veri (Big Data) ortaminda her zaman azar azar yapay ogrenim yapmaya mecburuz, o zaman bir kategorik kolonun mevcut tum degerlerine azar azar ulasamazdik (verinin basinda isek, en sonundaki bir kategorik degeri nasil gorelim ki?). Fakat onceden bu listeyi baska yollarla elde etmissek, o zaman her one-hot islemine onu parametre olarak geciyoruz.

Sozluk niye one_hot_dataframe cagrisi disinda yaratildi? Bu cagri duz bir liste alip oradaki degerleri sirayla bir sayiyla esleyerek her seferinde bir sozluk yaratabilirdi. Bunu yapmadik, cunku sozluk yaratiminin sadece bir kere, one_hot_dataframe disinda olmasini istiyoruz. Yine Buyuk Veri ortamini dusunenelim, esleme (map) icin mesela bir script yazdik, bu script icinde (basinda) hemen sozlukler yaratilirdi. Daha sonra verinin tamami icin, azar azar surekli one_hot_dataframe cagrisi yapilacaktir. O zaman arka arkaya surekli ayni veriyi (sozlukleri) sifirdan tekrar yaratmamiz gerekirdi. Bu gereksiz performans kaybi demek olacakti. Unutmayalim, Buyuk Veri ortaminda tek bir kategorik kolonun milyonlarca degisik degeri olabilir!

No comments: