dersblog

Pandas

Pandas'ı ilk kez bir CSV dosyasını okumak ve işlemek için kullanmıştım; numpy bu amacla yazılmamış -rivayete göre ticari ürün Matlab hala CSV dosyalarını basit bir şekilde okuyamıyor-, ve bazı belgelere göre Pandas bu işi R'ye benzer şekilde yapabiliyordu. Pandas kurduktan sonra, alttaki gibi bir kod Pandas noktalı virgül ile ayrılmış dosyayı söylendiği gibi güzelce okudu,

import pandas as pd, io
pd.set_option('display.max_columns', None)

s1 = """
c;d;a;b
one;0;0;7
one;1;1;6
one;2;2;5
two;0;3;4
two;1;4;3
two;2;5;2
two;3;6;1
"""

df1 = pd.read_csv(io.StringIO(s1),sep=';')
print (df1)
     c  d  a  b
0  one  0  0  7
1  one  1  1  6
2  one  2  2  5
3  two  0  3  4
4  two  1  4  3
5  two  2  5  2
6  two  3  6  1

Not: CSV'yi yazıda gösterebilmek için StringIO uzerinden okuduk, fakat aslında read_csv ile direk dosyadan da okuyabilirdik.

Güzel... Ama sol tarafta bir takım sayılar var, 0,1,..,6 diye gidiyor, bunlar nedir? CSV okuyup direk numpy matrisi elde etsek olmaz mıydı? Bu "ekstra" "gereksiz" şeyleri ne yapacağız? Baştaki amacımız Pandas'i numpy için bir önyüz gibi kullanmaktı (gerçi np.array(df1)) ile hemen çevrim yapılabilirdi ama, şimdi, bu ek aşamaya ne gerek var?), fakat Pandas belgelerini takip ettikçe, ve örnekleri gördükçe bu rakamların, yanı indisin gerekliği anlaşılmaya başladı.

İndis

Pandas'in en temel iki objesi Series ve DataFrame'in muhakkak bir indisi vardır. Öyle ki herhangi bir Series, DataFrame için indis tanımlanmamissa, Pandas otomatik olarak bir indis kendisi yaratır. Bu indis cok temel, birer birer artan düz sayılar olacaktır (üstteki gibi) ama muhakkak bir indis olur.

Pandas ile bir kolona erismek istersem bu cok basittir; mesela

print (df1['b'])
0    7
1    6
2    5
3    4
4    3
5    2
6    1
Name: b, dtype: int64

Dikkat, kolona erişince indis de onunla "beraber geldi". Üstte elde ettiğimiz bir Series objesi, DataFrame'in kolonları Series objeleridir. Series tek bir kolonu temsil eden bir objedir.

İndis pek çok türlü tipte olabilir. Bir tarih, bir string bile olabilir. Onu mevcut bir kolon üzerinden kendimiz tanımlayabiliriz,

s2 = """
c;d;a;b
2016-01-02;one;0;0;7
2016-01-03;one;1;1;6
2016-01-04;one;2;2;5
2016-01-05;two;0;3;4
2016-01-06;two;1;4;3
2016-01-07;two;2;5;2
2016-01-08;two;3;6;1
"""

df2 = pd.read_csv(io.StringIO(s2),sep=';', parse_dates=True,index_col=0)
print (df2)
              c  d  a  b
2016-01-02  one  0  0  7
2016-01-03  one  1  1  6
2016-01-04  one  2  2  5
2016-01-05  two  0  3  4
2016-01-06  two  1  4  3
2016-01-07  two  2  5  2
2016-01-08  two  3  6  1

Sıfırıncı, ilk kolonu indis olarak tanımladık, Pandas'a ayrıca parse_dates ile bu kolonun içinde "tarihimsi" veriler olduğunu söyledik ki onları otomatik olarak DateTime objesi haline getirsin. Böylece tarihsel olarak büyüktür, küçüktür işlemlerini kullanabiliriz,

print (df2[df2.index > '2016-01-06'])
              c  d  a  b
2016-01-07  two  2  5  2
2016-01-08  two  3  6  1

İndisi sonradan değiştirmek mümkün. Mesela b kolonunu indisi yapalım,

print (df2.reset_index().set_index('b'))
       index    c  d  a
b                      
7 2016-01-02  one  0  0
6 2016-01-03  one  1  1
5 2016-01-04  one  2  2
4 2016-01-05  two  0  3
3 2016-01-06  two  1  4
2 2016-01-07  two  2  5
1 2016-01-08  two  3  6

Dikkat reset_index() ile mevcut indisi iptal ettik, o indis normal bir kolon haline geldi. Tabii bir isme sahip olmasi gerekiyordu, Pandas da ona "index" diye bir isim verdi. Bu isim degistirilebilir muhakkak.

İndisin esas değeri DataFrame'de yeni bir kolon yaratmak istediğimiz zaman ortaya çıkar. Diyelim ki bir şekilde elimizde şöyle bir Series var,

s1 = pd.Series(['x','y','z'], index=[1,2,3])
print (s1)
1    x
2    y
3    z
dtype: object

Bu seriyi tamamen kendimiz, yan bir tarafta apayrı bir şekilde yarattık. Şimdi bu seriyi bir kolon olarak df1'e ekleyelim. Ne olacak acaba?

df1['s1'] = s1
print (df1)
     c  d  a  b   s1
0  one  0  0  7  NaN
1  one  1  1  6    x
2  one  2  2  5    y
3  two  0  3  4    z
4  two  1  4  3  NaN
5  two  2  5  2  NaN
6  two  3  6  1  NaN

Pandas seriyi aldı, indisine baktı, ve o indisi df1 ile eşledi, uyan öğeleri yeni kolon öğesi olarak ekledi, diğerlerini boş bıraktı!

Pandas'in bu özelliği sayesinde zaten formülsel hesaplar çok rahat yapılabiliyor. Mesela x = d + a + b gibi bir hesabı direk DataFrame üzerinde yapabiliriz,

df1['x'] = df1.d + df1.a + df1.b
print df1
     c  d  a  b   s1   x
0  one  0  0  7  NaN   7
1  one  1  1  6    x   8
2  one  2  2  5    y   9
3  two  0  3  4    z   7
4  two  1  4  3  NaN   8
5  two  2  5  2  NaN   9
6  two  3  6  1  NaN  10

Bu ifadenin çok rahat bir şekilde işleyebilmesinin arkasında yatan sır kolon erişiminin, toplama işlemlerinin sonucunun hepsinin içinde indis olan sonuçlar üretmeleri - bu sayede Pandas bu sonucu alıp pat diye geri DataFrame içine yazabiliyor.

İndis uyumu üzerinden akla gelebilecek her türlü operasyon mümkün; mesela bir dizin içinde Series objeleri var, onları yanyana yapıştırıp bir DataFrame oluşturabilirim, Pandas indisleri uyan hücreleri aynı satıra koyar.

s1 = pd.Series(['x','y','z'], index=[1,2,3])
s2 = pd.Series(['a','b','c'], index=[1,2,3])
s3 = pd.Series(['aa','bb','cc'], index=[1,2,3])
df3 = pd.concat([s1,s2,s3],axis=1)
df3.columns = ['bir','iki','uc']
print df3
  bir iki  uc

1   x   a  aa

2   y   b  bb

3   z   c  cc

Aynı şekilde DataFrame'ler de yanyana yapıştırılabilir.

Bu arada Pandas aynen SQL tabanları gibi birleştirme operasyonu yapabiliyor, yani iki DataFrame'i alıyorum, indis uyumu üzerinden, ya da sadece bazı kolonların ismini verip kolon uyumu üzerinden iki DataFrame birleştirilebilir. İşlem oldukça hızlı; bir projede her biri 1 gigabaytlık iki DataFrame'i birleştirip üçüncü bir devasa DataFrame yaratmıştım bir kez, tamamen hafızada!

Kordinat bazlı (kordinat derken indeks ve kolon, ki indeks hangi tipte ise o) kalıcı değişimler için

df2.loc['2016-01-05','d'] = 1000
print df2
              c     d  a  b
2016-01-02  one     0  0  7
2016-01-03  one     1  1  6
2016-01-04  one     2  2  5
2016-01-05  two  1000  3  4
2016-01-06  two     1  4  3
2016-01-07  two     2  5  2
2016-01-08  two     3  6  1

Sadece tek bir hücre değiştirdik.

Fonksiyonlar

Üstte gördüğümüz formülsel erişim her türlü satırsal işlem için yeterli olmayabilir. Belki bir Series'in tüm öğleri, ya da DataFrame'in tüm satırları üzerinde bir fonksiyon işlemesi gerekir... Burada map ve apply fonksiyonları var; Python'un fonksiyonel ruhuna uygun bir şekilde satır satır gezinen kod yazmıyoruz, genel bir geziciye bir fonksiyon geçiyoruz, ve gezici her satıra / öğeye verilen fonksiyonu uyguluyor.

Diyelim ki bir kolondaki her ögeyi string haline getirip yanına "XX" ekliyoruz,

def f(x): return str(x)+"XX"

print df1.d.map(f)
0    0XX

1    1XX

2    2XX

3    0XX

4    1XX

5    2XX

6    3XX

Name: d, dtype: object

Çok basit fonksiyonlar için Python'un lambda kullanımı var,

print df1.d.map(lambda x: str(x)+"XX")

Aynı sonucu verir. Elde edilen sonucun bir Series olduğuna dikkat, bir indisi var, ve alıp bu Series'i bir DataFrame içine yazabilirdik.

Eğer fonksiyon içinde tüm DataFrame satırına erişim gerekiyorsa, apply kullanımı var, apply ona geçilen fonksiyona satır geçer; yani apply satırları teker teker gezer ve satırlar sırasıyla bizim verdiğimiz fonksiyonun ilk parametresine "düşer".

print df1.apply(lambda x: str(x.c) + ":" + str(x.d), axis=1)
0    one:0

1    one:1

2    one:2

3    two:0

4    two:1

5    two:2

6    two:3

Sözlük

Bazen bir DataFrame'in indis değerlerini baz alan bir sözlük yaratmak isteyebiliriz. Bunun için to_dict çağrısı var. Test verisi yaratalım,

df = pd.DataFrame(np.array(range(20)).reshape(10,2))
df.columns = ['a','b']
df = df.set_index('a')
print (df)
     b
a     
0    1
2    3
4    5
6    7
8    9
10  11
12  13
14  15
16  17
18  19

Eğer a bazlı bir sözlük yaratmak istersem,

d = df.to_dict('index')
d
Out[1]: 
{0: {'b': 1},
 2: {'b': 3},
 4: {'b': 5},
 6: {'b': 7},
 8: {'b': 9},
 10: {'b': 11},
 12: {'b': 13},
 14: {'b': 15},
 16: {'b': 17},
 18: {'b': 19}}

Artık mesela d[2]['b'] ile 2 indis değerindeki b kolon değerine erişmiş oluyorum bir bakıma, eğer daha fazla kolon olsaydı onlara da benzer şekilde erişim sağlanacaktı.

Pivot

Satır olarak tekrar eden iki kolondaki verileri bir tür kordinat olarak alıp, üçüncü bir kolondaki değere göre bir tablo hücresinde değer atamak istiyorsak, Pandas paketinin pivot özelliği ise yarar. Pivot genel bir veri prezentasyon yaklaşımı, Postgres içinde crosstab diye bir fonksiyon var, fakat kullanımı pek kolay değil.

import numpy as np
from pandas import DataFrame
df=DataFrame({
       'foo': ['one',  'one',  'one',  'two',  'two',  'two'],
       'bar':  [ 'A' ,   'B' ,   'C' ,   'A' ,   'B' ,   'C' ] ,
       'baz':  [ '1',   '2',   '3',   '4',   '5',   '6']
       })
print df
pv = df.pivot('bar', 'foo')
print pv.to_string()

Sonuc

foo one two
bar       
A     1   4
B     2   5
C     3   6

Kolonda Liste

DataFrame pek cok Python tipini kabul edebilir, liste de bunlardan biri,

import numpy as np
from pandas import DataFrame
df=DataFrame({
       'foo': ['one',  'one',  'one',  'two',  'two',  'two'],
       'bar':  [ '[2,3]',   '[4,5]', '[6,7]', '[8,9]', '[10,11]', '[12,13]'],
       'baz':  [ [2,3],   [4,5],   [6,7],   [8,9],   [10,11],   [12,13]]
       })
print (df)
   foo      bar       baz
0  one    [2,3]    [2, 3]
1  one    [4,5]    [4, 5]
2  one    [6,7]    [6, 7]
3  two    [8,9]    [8, 9]
4  two  [10,11]  [10, 11]
5  two  [12,13]  [12, 13]

Artık bar ve baz kolonları içinde liste var, biri metin bazli digeri duz Python objesi halinde. Listeleri nasıl geri okuruz?

print (df.iloc[0].baz)
print (type(df.iloc[0].baz))
print (df.iloc[0].bar)
print (type(df.iloc[0].bar))
[2, 3]
<class 'list'>
[2,3]
<class 'str'>

İlk durum kolay, liste doğru Python tipinde direk erişip kullanırız. Eğer liste bir string olarak geliyorsa, onu Python tipine çevirmek lazım. Burada eval kullanımı önerenler olabilir fakat bu kullanım yavaştır, ayrıca verilen parametreyi bir Python kodu kabul edip işlettiği için tehlikeli olabilir. Bu durumda en hızlı ve güvenli çözüm json kullanmak.

import json
res = json.loads(df.iloc[0].bar)
print (res)
print (type(res))
print (res[0])
[2, 3]
<class 'list'>
2

Bu yaklaşım işledi çünkü liste kabaca zaten bir JSON formatına sahiptir, sözlük (dictionary) yok ama liste var. Bu sebeple loads listeye çevrimi yapabildi.

Dosyalardan Okumak

Üstteki örneklerde StringIO kullandık, ama en basit Pandas kullanımı aslında df = pd.read_csv('[dosya]') ile direk diskten okumak.

import pandas as pd

df = pd.read_csv('data.csv')

Burada ilginç bazı ek numaralar da yapılabiliyor, örnek zip içinde olan csv direk zip üzerinden okunabilir! Mesela test.zip içinde olan test.csv dosyasını okumak için

import pandas as pd, zipfile
with zipfile.ZipFile('test.zip', 'r') as z:
      df =  pd.read_csv(z.open('test.csv'))
      ... 

Eğer CSV dosyası dosyası direk, basit ekranda gösterilebilen türden bir bağlantıysa, o zaman https://... diye giden bağlantıyı read_csv çağrısına geçmek yeterlidir.

df = pd.read_csv('https://.../data.csv')

Fakat bazen URL bağlantısı bir 'dosya indirme' aksiyonu tetikler, bu durumda o indirilen dosyanın 'yakalanması' ve okunması gerekir. Bu durum için ek hareketler gerekir, mesela Yahoo Finance üzerinde görelim,

import pandas as pd, urllib.request as urllib2, io

url = "https://query1.finance.yahoo.com/v7/finance/download/AAPL?period1=1492524105&period2=1495116105&interval=1d&events=history"
r = urllib2.urlopen(url).read()
file = io.BytesIO(r)
df = pd.read_csv(file,index_col='Date')
print (df['Adj Close'].tail())
Date
2017-05-12    37.154709
2017-05-15    37.059498
2017-05-16    37.004768
2017-05-17    35.762302
2017-05-18    36.307369
Name: Adj Close, dtype: float64

Bunun bir ileri noktasi İnternet üzerinden indirme tetikleyen bir ZIP bağlantısını hiç diske indirmeden hem yakalamak, hem açmak, sonra içindeki dosyayı Pandas'a okutmak!

import pandas as pd, datetime
import urllib.request as urllib2
r = urllib2.urlopen("https://www.filanca.com/test.zip").read()
file = ZipFile(BytesIO(r))
csv = file.open("test.csv")
df = pd.read_csv(csv,sep='\t',header=None)

Bazi Ayarlar

Ekranda print ile dataframe basınca bazen tüm kolonlar gözükmeyebilir,

import pandas as pd
pd.set_option('display.max_columns', None)

Kolonun kendisini en büyük halde görmek için

pd.set_option('display.max_colwidth',-1)

Excel'den okuma teknikleri icin [2]

Kaynaklar

[1] http://pandas.pydata.org/pandas-docs/stable/

[2] [Yazi](../../2012/05/python-pandas-excel.html)


Yukarı