Thursday, September 29, 2016

Kafadan Hesap

Tuesday, September 27, 2016

Thursday, August 25, 2016

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, StringIO

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(StringIO.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(StringIO.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' 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

Kaynaklar

http://pandas.pydata.org/pandas-docs/stable/

Thursday, June 30, 2016

datetime

Python ile bazi datetime numaralari.

String verisi ile tarih yaratmak,

import datetime
s = "10/10/11"
d = datetime.datetime.strptime(s, "%m/%d/%y")
print d

2011-10-10 00:00:00

Gun eklemek (ya da cikartmak)

print d + datetime.timedelta(days=10)

2011-10-20 00:00:00

Tekrar string'e cevirmek

print d.strftime('%Y-%m-%d')

2011-10-10

Ayri ayri yil, gun, vs vererek yaratmak,

d2 = datetime.datetime(1999, 1, 1)
print d2

1999-01-01 00:00:00

Bit tarih objesinin yil, gun, ay ogelerine bakmak,

print d2.year, d2.month, d2.day

1999 1 1

Monday, May 30, 2016

Birim Testleri, Taklitlemek (Mocking) ve Python

Birim test bir kod parçasının beklenen sonuçları vermesini test eder, eğer toplamayı test ediyorsak,

def topla(a,b): return a+b

assert topla(2,3)==5

kodu yazılabilir; assert ifadesi eğer beklenen şart oluştuysa hiçbir şey yapmaz, olmazsa hata verir. Bir kodun mantığını bu sekilde test edebiliriz, cunku eger kodda hata varsa, bekledigimiz sonuc verilmez. Eger ustteki + isareti - olsaydi, bir AssertionError verilirdi.

İhtiyacı olan her şeyi kendi içinde barındıran kodlar için bu tür testler problem değil, üstteki kod için + işleminden başkası gerekmiyor. Fakat eğer bir kod parçasının bir dış sistem ile bağlantıya geçmesi gerekiyorsa birim test sırasında bu bağlantıların yerine taklit kod (mock) geçirmemiz gerekebilir. Özellikle dikkat edilmesi gereken husus şudur; taklit, ya da gerçek kodlarından birini atıp diğerini kullanabilmeliyiz, tasarım temiz bir şekilde olmalı.

Diyelim ki o andaki sistem zamanını alıp hangi günde olduğumuza bakan bir kod var.  Kod eğer o anki gün iş günü ise (Pazartesi-Cuma arası) verilen bir değere 200 ekliyor, yoksa 100 ekliyor ve döndürüyor..

import datetime
def f(val):
    t = datetime.datetime.now()
    if t.weekday()==6 or t.weekday()==7: return val+100
    else: return val+200

print f(50)

Sonuc

150

Üstteki sonuç geldi çünkü bugün (yazının yazıldığı gün) Cumartesi. Bu kodu birim testinden geçirmek için ne yapmalı?  Bir test kodu yazarız, o f()'i çağırır sonucu kontrol ederiz bu çok basit fakat kod her işlediğine değişik bir günde olabiliriz, kodun sürekli hafta sonu işletilmesi garanti
değil, o zaman beklediğimiz cevap ile verilen cevap habire değişebilir. Birim testleri bir kere yazınca her seferinde değiştirmek istemeyiz. Ayrıca birkaç degisik durumu, senaryoyu da test etmemiz lazım, hafta sonu olması, hafta içi olması, vs. bunlari testlerimiz kontrol edebilmeli.

Taklitleme burada ise yarar. Fakat test edilen kodun tasarımını öyle yapmalıyız ki dış sisteme bizim sağladığımız bir aracı üzerinden bağlanılsın.

import datetime

def system_time():
    return datetime.datetime.now()
    
def f(val,timer=system_time):
    t = timer()
    if t.weekday()==6 or t.weekday()==7: return val+100
    else: return val+200
    
print f(50)

Sonuc

150

Dikkat, f() çağrısına parametre olarak bir fonksiyon geçiyoruz, Python ile fonksiyonları parametre gibi geçmek mümkün, ki f() çağrısı kendi içinde sistem zamanını okumak için bu dışarından verilen fonksiyonu kullanmalı. Üstteki örnekte f()'e gerçek sistem zamanını okuyan system_time geçtik, ayrıca bu fonksiyonu olağan (default) değer haline getirdik böylece eski çağrı şekli (yani aracının verilmediği hal) aynen olduğu isleyecek.

Şimdi bir taklit fonksiyonu yaratalım, ve f()'i onun üzerinden çağıralım,

def mock_1():
    return datetime.datetime(2016, 5, 26)
    
print f(50,mock_1)

Sonuc

250

Bu taklit fonksiyon her seferinde 26/5/2016 tarihi cevabı verecek şekilde kodlandı. Böylece f() içinde okunan sistem zamanının ne olacağını biz dışarıdan belirlemiş oluyoruz. Böylece girdiler ve dış bağlantılar tamamen kontrol altında oluyor ve f()'ten buna göre beklediğimiz çıktı değerlerini kontrol etmemiz mümkün oluyor. 250 bekliyorduk, 250 aldık. Testlerimiz her işlediginde bu taklit kodunu kullanacaktır, ve girdiler hiç değişmeyeceği için beklenen çıktıların kontrolü mümkün olacaktır.

Degisik senaryo demistik, simdi hafta sonu durumu için bir ayrı taklit yazarız,

def mock_2():
    return datetime.datetime(2016, 5, 29)
    
print f(50,mock_2)

Sonuc

150

Birim testlerin otomatik kontrolü için assert çağrısı kullanılır, eğer assert sonrası verilen değer True değilse, assert hata ile bize bildirir. Kontrolleri alttaki gibi yapabilirdik,

assert f(50,mock_1) == 250
assert f(50,mock_2) == 150

Hiç hata yok. Ama kod içinde bir yanlışlık olsaydı (ya da örnek amaçıyla baska bir değer bekliyormuş gibi yapalım),

assert f(50,mock_1) == 3883

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
in ()
      1 
      2 
----> 3 assert f(50,mock_1) == 38838

AssertionError: 

Sonucunu görürdük.

Taklitleme ile bu şekilde otomatik testler yazabiliriz. Üstte ardı ardına birçok kontrolü yapabilirdik, eğer hiç hata mesajı görmezsek, kodun beklediğimiz gibi işlediğini görürdük, ve içimiz rahat ederdi.

TestCase

Ben çoğunlukla bir main program içinde assert çağrıları kullanıyorum, yeterli oluyor. Ek olarak unittest modulu kullanılabilir.

import unittest

class TestBizimClass(unittest.TestCase):

 def test_metot1(self):
     ...
     self.assertFalse ( .. )
             
 def test_metot2(self):
     ...
     self.assertFalse ( .. )
             
if __name__ == '__main__':
 unittest.main()

Bu script isletilince unittest.main() cagrisi her iki test metotunu otomatik olarak cagirip, sonuclari toplar, ve Java dunyasinda JUnit, TestNG'ye benzeyen sonuclari ekranda gosterir.


Örnek

Taklitlemede ne kadar ileri gidilebilir? Tabii ki istendiği kadar; çoğu mühendislik durumlarında olduğu gibi bir bedel / getiri dengesi var, her dış sistemi taklitlemek çok uğraştırabilir, fakat eğer test edilenler kritik ise bu efora değebilir. Bizim en son bir servise bağlanıp en son finans verilerini indiren ve bir Mongo tabanına yazan kodu test etmemiz gerekti, bu durumda dış bağlantıyı taklitledik, ayrıca verilerin apayrı bir tabana yazılmasını sağladık (yani bağlantı aracı kodu, ve taban ismi hep dışarıdan fonksiyonlara veriliyordu). Dış servisin geri getirdiği veriler tabii ki her test şartına göre farklı olmalıydı, bunun için ayrı ayrı dizinler altında ayrı CSV dosyaları hazırladık, ve her taklit şekli bu farklı (ama bilinen) veri setini geri döndürüyordu, test tabanı her test başında silinip, tekrar yaratılıyor (tabii bu tür işler anahtar-değer tabanı Mongo ile çok kolay), vs.

Bu çok efor gibi gözükebilir, fakat bu koda 5 sene sonra dönsem python test.py işletip her şeyin işlediğini görebilirim, bu hakikaten bir rahatlık saglar. Birim test gurusu Kent Beck bazen kahve molası verip sonra geri masasına tekrar oturduğunda "koda devam etmeden önce birim testleri şöyle bir daha işletirim, her şey tamam mesajını görmek beni rahatlatır" mealinde bir yorum bile yapmistir. Diğer taraftan şimdiye kadar insanlığın geliştirdiği en büyük kod bazı olan Linux'ta bildiğimiz kadarıyla hiç birim test kullanılmıyor - fakat oradaki sebep herhalde Linux'un çok temel seviyede ve donanıma çok yakın işliyor olması, ayrica Linux çekirdeğinin dolaylı ve dolaysız kullanıcı tabanı o kadar büyük ki bu kullanıclar her türlü otomize test işlemini gerçekleştirmiş oluyorlar bir bakıma.