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!

Friday, July 26, 2013

Veriyi Ayriksallastirmak (Discretization)

Pandas ile eldeki veriyi ayriksallastirmak icin herhangi bir sayisal kolon uzerinde qcut kullanilabilir. Bu cagri istatistiksel bolgeler (quartile) uzerinden hesap yapan bir cagridir, belli bir kolon icin numerik degerler belirlenen araliklara bolunur ve eslemesi geri rapor edilir.

import pandas as pd
import numpy as np

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)

cats = pd.qcut(df['pop'], 3)
df['pop_cat'] = cats
df['pop_cat'] = "pop_" + df['pop_cat']


Bu cagridan sonra her satirin belli bir araliga eslendigini gorecegiz (cikti altta). Cagri qcuts'dan gelen objeye bakmak ilginc olabilir, icinde bir dizin (array) bir de "seviyeler (levels)" adinda 2. bir dizin var. Ana dizin icinde her nokta icin (veriyle ayni sirada) hangi araligin secildigi gosteriliyor. Siranin veri ile ayni olmasi sayesinde zaten pop_cat adli yeni bir kolona atama yaptigimiz anda Pandas o kolonu ana veriyle hemen esleyebiliyor. Bu iyi bir ozellik bu arada, elde mesela bir Numpy vektoru var ise, ve bu vektor DataFrame'imiz satir sayisi ile esdeger tane veri iceriyorsa, o zaman o vektoru alip cat diye aninda DataFrame'e yeni bir kolonmus gibi verebiliriz (ya da eskisinin uzerine yazabiliriz).

Levels ise o araliklari teker teker listeliyor, ustteki ornek icin 3 tane.

Biz ayrica ufak bir ek yaparak araligin hangi kolona ait oldugu gozuksun diye onu da veri icine ekledik. DataFrame icinde zaten belli oluyor ama duz CSV'ye versek ve mesela fpgrowth gibi bir algoritma onun uzerinde islese sonuclari rapor ederken aralik ve veri baglantisi daha rahat belli olur.


   pop   state  year             pop_cat
0  1.5    Ohio  2000    pop_[1.5, 1.933]
1  1.7    Ohio  2001    pop_[1.5, 1.933]
2  3.6    Ohio  2002    pop_(2.733, 3.6]
3  2.4  Nevada  2001  pop_(1.933, 2.733]
4  2.9  Nevada  2002    pop_(2.733, 3.6]


Istatistiksel bolgeler bu arada verinin frekansina hassastir (ki bu iyi), median ve mode arasindaki fark gibi, quartile islemi mode gibi hesaplanir, mode bilindigi gibi veri noktalarini siraya dizip ortadaki noktayi dondurur. Ortalama gibi toplama bolme yapmaz.

Thursday, July 4, 2013

Dekoratorler, Onbellek Kodlamasi, Fonksiyon Degistirmek

Bir fonksiyona ya da kod parcasina onbellek kullanimi eklemek kolaydir fakat bu tur ekler kodun okunabilirligini de azaltabilirler. Mesela kare almakla yukumlu bir fonksiyonumuz var diyelim (altta ise yaramaz bir degisken dummy ekledik, bazi puf noktalari gosterebilmek icin),

def kare(dummy, a):
    return a*a


Bu fonksiyonu

kare("filan", 3)

diye cagiriyoruz ve sonuc olarak 9 gelmesini bekliyoruz.

Onbellekleme icin, diyelim ki, eger a degeri onceden gorulmusse, kare islemi sonucunun tekrar hesaplanmasini istemiyoruz, onu onbellekten bulup hizli bir sekilde geri dondurmek tercihimiz (tabii carpim islemi de cok hizli isler, ama bu ornek icin yavas olabilecegini hayal edelim).

Bu kod uzerinde onbelleklemeyi eski usulle yapsaydik, kod suna benzerdi:

cache = {}
def kare(dummy, a):
    if not a in cache: cache[a] = a*a
    return cache[a]


Degisken cache bir Python dictionary'dir ve onbellegimiz onun uzerinde  duruyor. Goruldugu gibi kod biraz kalabaliklasti. Onbellek objesi alanen ortada, ayrica if gibi cok ciddi bir ibareyi koda sokusturmak zorunda kaldik :) Genellikle bu ifade onemli bir islem mantigi var ise kullanilir - en azindan kod okunabilirligi acisindan boyle olmasi daha iyidir.

Peki bu isi daha temiz bir sekilde yapamaz miydik?

Python dekorator fonksiyonlari iste tam burada ise yarar. Bir dekorator bir fonsiyonu "sarmalayabilir (wrap)", ve o fonksiyona giren cikan tum degerler uzerinde islem yapabilir, ve onlari istedigi gibi degistirebilir, bu sayede o fonksiyona "caktirmadan" ek ozellikler verebilir. Sozdizim acisindan da temiz dururlar, cunku dekorator fonksiyon uzerinde '@' ile tanimlanan bir seydir, baska bir eke ihtiyac yoktur. O zaman (once dekoratorun kendisi)

def cache(function):
  memo = {}
  def wrapper(*args):
    if args[1] in memo:
      print "cache hit"
      return memo[args[1]]
    else:
      print "cache miss"
      rv = function(*args)
      memo[args[1]] = rv
      return rv
  return wrapper


Ustteki kod ana kodunuzdan ayri bir yerde, bir dosyada durabilir mesela, sadece bir kere yazilir zaten, ve kullanilmasi gerektigi zaman su ibare yeterlidir,

@cache
def kare(dummy, a):
    return a*a


Goruldugu gibi gayet temiz. Onbellek kodu hic etrafta gozukmuyor, bu da kod bakimini daha rahatlastiran bir ozellik. Boylece kare fonksiyonunu normalde olmasi gerektigi gibi yaziyoruz, kod onbellek olsa da olmasa da ayni sekilde yaziliyor, sadece carpim icin gereken islem mantigini iceriyor.

Not: dummy degiskenini dekorator icinde istedigimiz herhangi fonksiyon argumani ile is yapabilecegimizi gostermek icin kullandik, args[1] ile sadece ikinci argumana baktik mesela.

Koda Ekler Enjekte Etmek

Diyelim ki mevcut bir kod parcasi var,


import random

class Foo:
    def f(self,x):
        x = random.random()
        return x

o = Foo()   


Biz bu kodun f() cagrisini "yakalayip" ona ek bir seyler yaptirtmak istiyoruz, ve mevcut koda hic dokunmadan bunu yapmak istiyoruz. Belki f() cagrisi bir baska yazilim paketi icinde, vs. Bu fonsiyonu dekore ederek bunu yapabiliriz, fakat mevcut fonksiyon koduna dokunmak istemedigimiz icin metot ustunde @birdekorator gibi bir kullanim yapamayiz. Bu durumda baska bir secenek sudur,

def decorated_f(fn):
    def new_f(*args, **kwargs):
        res = fn(*args, **kwargs)
        setattr(args[0], "was", res)
        return res
    return new_f

Foo.f = decorated_f(Foo.f)


Simdi

print "o", o.f(0)
print "was", o.was


Yeni ekleri de isletecek, bu ek Foo uzerinde yeni bir oge yaratti, ve bu ogeye o.was diye erisiyoruz.


Monday, July 1, 2013

Oracle, Pandas, HDF5

Daha onceki yazilarda SQL*Plus uzerinden CSV dosyalarina veri aktarimini gorduk. SQLplus'in bazi eksikleri var ne yazik ki, bu program metin bazli terminaller caginda ekranda birseyler gostermek icin yazilmis, hizli, verimli veri aktarimini pek beceremiyor. Disaridan parametre gecmek bile buyuk problem. Bunlarin yerine Oracle Python baglanti kutuphanesi kurulup direk Pandas icinden baglanti yapilabilir.

Diger yandan CSV ciktisi almak yerine daha az yer tutan HDF formati secilebilir.

Simdi bunlarin hepsini yanyana koyalim:

import pandas as pd
import pandas.io.sql as sql
import cx_Oracle as odb

conn = odb.connect("kullanici/sifre@DB")

query = """
select ...

from ...
"""

df = sql.read_frame(query, conn)               
df.to_hdf("dosya.h5" % i,"table")

Direk Python icinden baglandik, sorguyu islettik, Pandas DataFrame'i direk sorgu verisinden yarattik, ve ciktiyi HDF formatinda diske yazdik. Bu formati kullanmak icin PyTables adli bir paket lazim, kurmak icin

sudo apt-get install python-tables