15 Pandas ile Veri Analizi
Şimdiye kadar verilerimiz hep aynı türden sayılardan oluşan dizilerdi: bir fonksiyonun ızgaradaki değerleri, bir matrisin elemanları, bir simülasyonun sonuçları. Gerçek veriler çoğu zaman böyle gelmez. Bir hava istasyonunun ölçüm kaydında her satır bir gündür ve satırda bir tarih, istasyonun adı, sıcaklık ve yağış yan yana durur. Sütunların türleri farklıdır, bazı hücreler boştur ve sorduğumuz sorular “Erzurum’un ortalama sıcaklığı nedir?” ya da “hangi ay en yağışlıdır?” gibi gruplara ve zamana bağlı sorulardır. NumPy dizisi bütün elemanların aynı türden olmasını ister ve satırlara yalnız \(0, 1, 2, \ldots\) konumlarıyla ulaşır; böyle bir tabloyu taşımak için tek başına elverişli değildir.
pandas bu boşluğu dolduran kütüphanedir. NumPy dizilerinin üzerine iki yapı kurar: etiketli bir boyutlu dizi olan Series ve sütunları adlandırılmış iki boyutlu tablo olan DataFrame. Satırlara ve sütunlara adlarıyla ulaşılır, eksik değerler tanınır, gruplama, birleştirme ve zamana göre özetleme gibi işlemler tek satırda yapılır. Kütüphane geleneksel kısaltmasıyla import pandas as pd diye yüklenir.
Bu bölümde önce iki temel yapıyı tanıyacak, sonra bölümün içinde ürettiğimiz bir veri kümesini bir CSV dosyasına yazıp geri okuyacağız: Ankara, İzmir ve Erzurum’daki üç istasyonun 2025 yılına ait günlük sıcaklık ve yağış değerleri. Bu sayılar gerçek ölçüm değildir; bir kosinüs dalgasına rastgele gürültü eklenerek üretilmiştir, böylece her sonucu bildiğimiz modelle karşılaştırabiliriz. Ortalama, medyan, varyans ve çeyreklik gibi kavramların kuramı Matematiksel İstatistik notlarındadır; burada onları pandas ile hesaplamayı öğreniyoruz. Önce kısa bir tadımlık:
import pandas as pd
df = pd.DataFrame({
"istasyon": ["Ankara", "İzmir", "Ankara", "Erzurum", "İzmir",
"Erzurum"],
"ay": ["Ocak", "Ocak", "Temmuz", "Ocak", "Temmuz", "Temmuz"],
"sicaklik": [0.4, 9.1, 23.6, -8.9, 27.8, 19.7],
})
print(df)
print(df.groupby("istasyon")["sicaklik"].mean())Çıktı:
istasyon ay sicaklik
0 Ankara Ocak 0.4
1 İzmir Ocak 9.1
2 Ankara Temmuz 23.6
3 Erzurum Ocak -8.9
4 İzmir Temmuz 27.8
5 Erzurum Temmuz 19.7
istasyon
Ankara 12.00
Erzurum 5.40
İzmir 18.45
Name: sicaklik, dtype: float64
Altı satırlık tabloda her istasyonun ortalamasını tek satırla bulduk. Aynı işi NumPy ile yapmak için istasyon adlarını ayrı bir dizide tutmak, her istasyon için bir maske kurmak ve sonuçları elle bir araya getirmek gerekirdi. Bu satırın nasıl çalıştığını gruplama başlığında ayrıntılı göreceğiz.
15.1 Series: Etiketli Bir Boyutlu Dizi
pandas’ın en basit yapısı, her elemanına bir etiket iliştirilmiş bir boyutlu dizidir. Asal sayı sayma fonksiyonu \(\pi(x)\)’in, yani \(x\)’e kadar olan asal sayıların sayısının birkaç değeriyle başlayalım:
import pandas as pd
# pi(x): x'e kadar olan asal sayıların sayısı
pi_x = pd.Series([4, 25, 168, 1229, 9592, 78498],
index=[10, 100, 10**3, 10**4, 10**5, 10**6],
name="pi(x)")
print(pi_x)
print(pi_x.index)
print(pi_x.to_numpy(), pi_x.dtype)Çıktı:
10 4
100 25
1000 168
10000 1229
100000 9592
1000000 78498
Name: pi(x), dtype: int64
Index([10, 100, 1000, 10000, 100000, 1000000], dtype='int64')
[ 4 25 168 1229 9592 78498] int64
Çıktının solundaki sütun etiketleri, sağdaki sütun değerleri gösterir. index özelliği etiketleri, to_numpy() yöntemi değerleri bir NumPy dizisi olarak verir; değerlerin türü int64’tür. name ile verdiğimiz ad çıktının en altında görünür.
Tanım 15.1 (Series) Bir Series, aynı türden \(n\) değer \(v_0, v_1, \ldots, v_{n-1}\) ile bu değerlere birer etiket veren bir indeksin (index) \(\ell_0, \ell_1, \ldots, \ell_{n-1}\) birleşimidir. \(k\) numaralı konumdaki değer \(v_k\), onun etiketi \(\ell_k\)’dir. Değerlerin ortak türüne Series’in dtype’ı denir; Series’e isteğe bağlı bir ad (name) da verilebilir.
Yani bir Series, bir NumPy dizisinin yanına bir etiketler sütunu eklenmiş hâlidir. Etiketler birbirinden farklıysa Series, etiket kümesinden değerlere giden \(\ell_k \mapsto v_k\) fonksiyonu gibi okunabilir. Her değere iki yoldan ulaşılır: \(k\) konumuyla ya da \(\ell_k\) etiketiyle. Etiket verilmezse pandas \(0, 1, \ldots, n-1\) etiketlerini kendisi koyar; o zaman konum ile etiket aynı sayıdır.
Bir sözlükten kurulan Series’te anahtarlar etiket, değerler değer olur:
import pandas as pd
squares = pd.Series({"bir": 1, "iki": 4, "üç": 9, "dört": 16})
print(squares)
print(squares.index.tolist(), squares.size)Çıktı:
bir 1
iki 4
üç 9
dört 16
dtype: int64
['bir', 'iki', 'üç', 'dört'] 4
Konumla ve etiketle ulaşmanın iki ayrı aracı vardır: loc etiketle, iloc konumla seçer. İkisi dilimlerde de farklı davranır:
import pandas as pd
pi_x = pd.Series([4, 25, 168, 1229, 9592, 78498],
index=[10, 100, 10**3, 10**4, 10**5, 10**6])
print(pi_x.loc[1000]) # etiketi 1000 olan eleman
print(pi_x.iloc[2]) # 2 numaralı konumdaki eleman
print(pi_x.iloc[-1]) # son eleman
print(pi_x.loc[100:10**4]) # etiket dilimi: iki uç da dahil
print(pi_x.iloc[1:3]) # konum dilimi: sağ uç hariçÇıktı:
168
168
78498
100 25
1000 168
10000 1229
dtype: int64
100 25
1000 168
dtype: int64
pi_x.loc[1000] etiketi 1000 olan elemanı, pi_x.iloc[2] ise sıfırdan saymaya başlayınca 2 numaralı konumdaki elemanı verir; burada ikisi de 168’dir. Dilimlerdeki fark önemlidir: etiket dilimi loc[100:10**4] iki ucu da içerir, konum dilimi iloc[1:3] ise Python’daki bütün dilimler gibi sağ ucu dışarıda bırakır. Etiketler sıralı olmak zorunda olmadığından “son etiketten bir önceki etiket” diye bir kavram yoktur; bu yüzden etiket dilimleri iki ucu da kapsar.
Series üzerindeki aritmetik, NumPy dizilerinde olduğu gibi eleman eleman yapılır ve etiketler korunur. Asal sayı teoremi, \(\pi(x)\)’in \(x / \ln x\) ile aynı hızda büyüdüğünü, yani \(\pi(x) \ln x / x\) oranının \(x \to \infty\) iken \(1\)’e gittiğini söyler. Oranı tablomuzdaki değerler için hesaplayalım:
import numpy as np
import pandas as pd
pi_x = pd.Series([4, 25, 168, 1229, 9592, 78498],
index=[10, 100, 10**3, 10**4, 10**5, 10**6])
x = pi_x.index.to_numpy()
ratio = pi_x / (x / np.log(x)) # asal sayı teoremindeki oran
print(ratio.round(4))
print(ratio[ratio > 1.12]) # boolean maskeyle seçim
print(ratio.idxmax(), round(ratio.max(), 4))Çıktı:
10 0.9210
100 1.1513
1000 1.1605
10000 1.1320
100000 1.1043
1000000 1.0845
dtype: float64
100 1.151293
1000 1.160503
10000 1.131951
dtype: float64
1000 1.1605
ratio yine bir Series’tir ve etiketleri hâlâ \(x\) değerleridir. ratio > 1.12 bir boolean Series verir; onu köşeli parantez içine yazınca yalnız True olan satırlar kalır. idxmax en büyük değerin konumunu değil etiketini döndürür. Tablodaki noktalar arasında oran en büyük değerine (\(1{,}16\)) \(x = 10^3\)’te ulaşır, sonra yavaş yavaş \(1\)’e doğru iner: \(x = 10^6\)’da bile \(1{,}08\)’dir. Teoremdeki yakınsama oldukça yavaştır.
İki Series arasındaki işlemlerde pandas’ın NumPy’dan en önemli farkı ortaya çıkar: değerler konumlarına göre değil, etiketlerine göre eşlenir.
import pandas as pd
a = pd.Series([1, 2, 3], index=["x", "y", "z"])
b = pd.Series([10, 20, 30], index=["y", "z", "w"])
print(a + b)
print(a.add(b, fill_value=0))Çıktı:
w NaN
x NaN
y 12.0
z 23.0
dtype: float64
w 30.0
x 1.0
y 12.0
z 23.0
dtype: float64
Tanım 15.2 (İndeks Hizalaması) \(a\) ve \(b\) iki Series olsun. \(a + b\) toplamının (ve öteki aritmetik işlemlerin) indeksi, iki indeksin birleşimidir. Bu birleşimdeki her \(\ell\) etiketi için \(\ell\) hem \(a\)’da hem \(b\)’de varsa sonuç \(a[\ell] + b[\ell]\) olur; \(\ell\) yalnız birinde varsa sonuç eksik değer NaN olur.
Yani pandas iki Series’i önce ortak bir etiket kümesine hizalar, sonra işlemi satır satır yapar. Etiketi olmayan tarafa NaN (“not a number”) yazılır ve NaN içeren her toplam yine NaN olur. Eksik tarafın \(0\) sayılmasını istiyorsak + yerine add yöntemini fill_value=0 parametresiyle çağırırız; sub, mul ve div yöntemleri de aynı parametreyi alır. Sonucun etiketleri sıralanmıştır ve değerleri float64 türündedir, çünkü NaN bir ondalıklı sayı değeridir.
a + b işleminin iki adımı. Önce iki Series etiketlerin birleşimi olan {w, x, y, z} indeksine hizalanır; etiketi olmayan taraf NaN ile doldurulur. Sonra değerler satır satır toplanır. Yalnız iki tarafta da bulunan y ve z etiketlerinde sayı çıkar. a.add(b, fill_value=0) ise eksik tarafı 0 sayar ve w ile x satırlarında da sonuç verir.Hizalama, aynı etiketin aynı şeyi anlattığı verilerde büyük kolaylıktır: farklı sırada kaydedilmiş iki ölçüm listesini toplarken satırların karışmasından korkmayız. Beklenmedik NaN değerleri görürseniz ilk bakmanız gereken yer, iki tarafın etiketleridir.
Bir Series’te s[...] yazımı etiketle seçer. Etiketler tamsayıysa bu kafa karıştırır: pi_x[0] “ilk eleman” değil “etiketi 0 olan eleman” demektir. pandas 3.0’dan itibaren metin etiketli bir Series’te de s[0] konum olarak yorumlanmaz:
import pandas as pd
pi_x = pd.Series([4, 25, 168], index=[10, 100, 1000])
print(pi_x[100]) # köşeli parantez: etiket 100
try:
pi_x[0] # 0 diye bir etiket yok
except KeyError as err:
print("KeyError:", err)
s = pd.Series([3, 5, 7], index=["a", "b", "c"])
try:
s[0] # metin indekste de konum aranmaz
except KeyError as err:
print("KeyError:", err)
print(s.iloc[0], pi_x.iloc[0])Çıktı:
25
KeyError: 0
KeyError: 0
3 4
Kural basittir: etiketle seçerken loc, konumla seçerken iloc yazın.
Bir Series’teki farklı değerlerin kaçar kez geçtiğini value_counts sayar. Sonuç, etiketleri bu farklı değerler olan bir Series’tir ve en sık değerden başlayarak sıralanır. sort_index() sonucu etiketlere göre sıralar, normalize=True parametresi ise sayımlar yerine oranları verir.
Örnek 15.1 (Asal Sayıların Son Basamakları) \(10^6\)’dan küçük asal sayıları Eratosthenes kalburunun NumPy sürümüyle bulun ve son basamaklarının dağılımını value_counts ile çıkarın.
Çözüm
Vektörizasyon ve Broadcasting bölümündeki maskeli kalburu kullanırız: \(0, 1, \ldots, N\) sayıları için bir boolean dizi tutulur ve her asal \(p\)’nin \(p^2\)’den başlayan katları tek bir dilim atamasıyla silinir. np.flatnonzero kalan True değerlerinin konumlarını, yani asalları verir. Bunları bir Series’e koyup \(10\)’a bölümden kalanlarını sayarız. Son satırdaki to_dict() bir Series’i etiketlerden değerlere giden bir sözlüğe çevirir; kısa sonuçları tek satırda yazdırmak için bölüm boyunca bunu kullanacağız.
import numpy as np
import pandas as pd
N = 10**6
is_prime = np.ones(N + 1, dtype=bool)
is_prime[:2] = False
for p in range(2, int(N**0.5) + 1):
if is_prime[p]:
is_prime[p * p::p] = False
primes = pd.Series(np.flatnonzero(is_prime))
last = primes % 10 # son basamaklar
counts = last.value_counts().sort_index()
print(len(primes))
print(counts)
print((counts / len(primes)).round(4).to_dict())Çıktı:
78498
1 19617
2 1
3 19665
5 1
7 19621
9 19593
Name: count, dtype: int64
{1: 0.2499, 2: 0.0, 3: 0.2505, 5: 0.0, 7: 0.25, 9: 0.2496}
\(78498\) asalın son basamağı yalnız bir kez \(2\) (asal \(2\)) ve bir kez \(5\)’tir (asal \(5\)). Geri kalan her asal \(10\) ile aralarında asal olduğundan son basamağı \(1\), \(3\), \(7\) ya da \(9\)’dur ve bu dört basamağın her biri asalların yaklaşık dörtte birinde görülür. Bu bir rastlantı değildir: asal sayı teoreminin aritmetik dizilerdeki karşılığına göre, \(\gcd(a, 10) = 1\) olan her \(a\) için son basamağı \(a\) olan asalların payı \(x \to \infty\) iken \(1/4\)’e yaklaşır. \(\blacksquare\)
15.2 DataFrame: Sütunları Adlandırılmış Tablo
Series tek bir sütundur; birden çok sütunu aynı satır etiketleriyle yan yana koyunca bir DataFrame elde ederiz. İlk yedi asal sayıyı, ikilik gösterimlerini, kareköklerini ve \(p + 2\)’nin de asal olup olmadığını bir tabloda toplayalım:
import math
import pandas as pd
def is_prime(m):
return m > 1 and all(m % d for d in range(2, math.isqrt(m) + 1))
p = [2, 3, 5, 7, 11, 13, 17]
df = pd.DataFrame({
"p": p,
"ikilik": [bin(q)[2:] for q in p], # ikilik gösterim
"kok": [math.sqrt(q) for q in p],
"ikiz": [is_prime(q + 2) for q in p], # p + 2 de asal mı?
})
print(df)
print(df.shape)
print(df.columns.tolist())
print(df.dtypes)Çıktı:
p ikilik kok ikiz
0 2 10 1.414214 False
1 3 11 1.732051 True
2 5 101 2.236068 True
3 7 111 2.645751 False
4 11 1011 3.316625 True
5 13 1101 3.605551 False
6 17 10001 4.123106 True
(7, 4)
['p', 'ikilik', 'kok', 'ikiz']
p int64
ikilik str
kok float64
ikiz bool
dtype: object
pd.DataFrame’e verdiğimiz sözlüğün her anahtarı bir sütunun adı, her değeri o sütunun değerleridir. shape satır ve sütun sayısını, columns sütun adlarını, dtypes her sütunun türünü verir. Dört sütunun dört farklı türü vardır: tamsayı (int64), metin (str), ondalıklı sayı (float64) ve mantıksal değer (bool). Bir NumPy dizisinde bu mümkün değildir. Sütun adlarında Türkçe harf kullanmak serbesttir; biz bu bölümde yazması kolay olsun diye ASCII harflerle yetiniyoruz.
Tanım 15.3 (DataFrame) Bir DataFrame, aynı indeksi paylaşan Series’lerin sütun sütun yan yana konmasıyla oluşan iki boyutlu bir tablodur. Satır etiketlerine indeks (index), sütun etiketlerine sütunlar (columns) denir ve her sütunun kendi dtype’ı vardır.
Yani DataFrame’in her sütunu türdeş bir dizidir, ama tablonun bütünü türdeş olmak zorunda değildir. Bir satır etiketi ve bir sütun adı verildiğinde hücre tek türlü belirlenir; bu yüzden DataFrame, satır etiketleri ile sütun adlarının çarpım kümesinden değerlere giden bir fonksiyon gibi düşünülebilir. Bir sütunu seçmek bir Series verir; bir satırı seçmek de bir Series verir, bu kez indeksi sütun adlarıdır.
df'nin parçaları. Soldaki gri sütun satır etiketleri (index), üstteki satır sütun etiketleridir (columns). Her sütun kendi türünde bir Series'tir; türler en altta df.dtypes satırında yazılıdır. Çerçeveli sütun df["kok"], çerçeveli satır df.loc[4] seçimidir; ikisi de birer Series olarak döner.import math
import pandas as pd
p = [2, 3, 5, 7, 11, 13, 17]
df = pd.DataFrame({"p": p, "kok": [math.sqrt(q) for q in p]})
col = df["kok"] # bir sütun: Series
print(type(col).__name__, col.name)
row = df.loc[4] # bir satır: yine Series
print(row)
df["p2"] = df["p"] ** 2 # yeni sütun, vektörel hesap
df["mod4"] = df["p"] % 4
print(df.head(4))
print(df["p2"].sum(), df["kok"].max())Çıktı:
Series kok
p 11.000000
kok 3.316625
Name: 4, dtype: float64
p kok p2 mod4
0 2 1.414214 4 2
1 3 1.732051 9 3
2 5 2.236068 25 1
3 7 2.645751 49 3
666 4.123105625617661
df["kok"], adı kok olan bir Series’tir. df.loc[4] ise etiketi 4 olan satırdır. Satırdaki p değeri 11.000000 diye yazıldı, çünkü bir Series’in bütün değerleri tek bir türde olmalıdır ve tamsayı ile ondalıklı sayı ortak tür olan float64’te buluşur. Yeni bir sütun, sözlüğe yeni bir anahtar ekler gibi atamayla eklenir; sağ taraftaki ifade bütün sütun üzerinde vektörel olarak hesaplanır. head(n) ilk \(n\) satırı, tail(n) son \(n\) satırı gösterir. mod4 sütununda \(2\)’den sonraki asalların \(4\)’e bölümünden kalanların \(1\) ya da \(3\) olduğunu görüyoruz.
Örnek 15.2 (Sinüs İçin Taylor Polinomlarının Hata Tablosu) \(x = 0;\ 0{,}25;\ \ldots;\ 1{,}5\) noktalarında \(\sin x\) değerlerini, \(P_3(x) = x - x^3/6\) ve \(P_5(x) = P_3(x) + x^5/120\) Taylor polinomlarını ve hatalarını bir DataFrame’in sütunlarında toplayarak \(P_3\)’ün hatasının \(10^{-2}\)’yi aştığı en küçük tablo noktasını bulun.
Çözüm
Önce x ve sin sütunlarıyla tabloyu kurar, sonra her yeni sütunu var olan sütunlardan hesaplarız. abs() mutlak değeri alır. Son satırda t.loc[maske, "x"] hatası \(10^{-2}\)’yi aşan satırların x değerlerini seçer, min() bunların en küçüğünü verir.
import numpy as np
import pandas as pd
x = np.arange(0, 1.75, 0.25)
t = pd.DataFrame({"x": x, "sin": np.sin(x)})
t["P3"] = t["x"] - t["x"]**3 / 6
t["P5"] = t["P3"] + t["x"]**5 / 120
t["hata3"] = (t["sin"] - t["P3"]).abs()
t["hata5"] = (t["sin"] - t["P5"]).abs()
print(t.round(6))
print(t.loc[t["hata3"] > 1e-2, "x"].min())Çıktı:
x sin P3 P5 hata3 hata5
0 0.00 0.000000 0.000000 0.000000 0.000000 0.000000
1 0.25 0.247404 0.247396 0.247404 0.000008 0.000000
2 0.50 0.479426 0.479167 0.479427 0.000259 0.000002
3 0.75 0.681639 0.679688 0.681665 0.001951 0.000026
4 1.00 0.841471 0.833333 0.841667 0.008138 0.000196
5 1.25 0.948985 0.924479 0.949910 0.024505 0.000926
6 1.50 0.997495 0.937500 1.000781 0.059995 0.003286
1.25
\(P_3\)’ün hatası \(x = 1\)’de \(0{,}0081\), \(x = 1{,}25\)’te \(0{,}0245\)’tir; aranan nokta \(x = 1{,}25\)’tir. \(\sin\) fonksiyonunda \(P_3 = P_4\) olduğundan Taylor teoreminin kalan terimi (Nümerik Analiz) hatayı \(x^5/120\) ile sınırlar. \(x = 1{,}25\) için bu sınır \(0{,}0254\)’tür ve tablodaki hatanın hemen üstündedir. \(P_5\)’in hatası aynı noktada \(0{,}0009\)’a iner. \(\blacksquare\)
15.3 CSV Dosyaları
Gerçek veri çoğu zaman bir dosyadan gelir ve tablo verisi için en yaygın biçim CSV’dir (comma-separated values): her satır bir kayıt, satırdaki alanlar virgülle ayrılmış, ilk satır da sütun adlarıdır. Sınıflar, Hata Yakalama ve Dosyalar bölümünde csv modülüyle satır satır okuduğumuz bu dosyaları pandas tek bir fonksiyonla okur ve yazar.
Bölümün geri kalanında kullanacağımız veri kümesini üretip olcumler.csv dosyasına yazalım. Model şudur: \(t = 0, 1, \ldots, 364\) yılın günü olmak üzere bir istasyonun sıcaklığı
\[ T(t) = m - a\cos\frac{2\pi (t - 15)}{365} + \varepsilon_t, \qquad \varepsilon_t \sim N(0;\ 2{,}5^2) \]
olsun; burada \(m\) yıllık ortalama, \(a\) genliktir. En soğuk gün \(t = 15\) (16 Ocak), en sıcak gün ise ondan yarım yıl sonrasıdır. Her gün istasyona özgü bir \(p\) olasılığıyla yağışlıdır ve yağış miktarı ortalaması \(r\) olan üstel dağılımdan gelir. Rastgele seçilen 30 sıcaklık ölçümünü de bozuk bir sensörü taklit etmek için sileriz. Rastgele sayı üretecini Rastgele Sayılar ve Monte Carlo Yöntemleri bölümünde tanımıştık.
import numpy as np
import pandas as pd
rng = np.random.default_rng(2025)
days = pd.date_range("2025-01-01", "2025-12-31") # 365 gün
t = np.arange(days.size)
# istasyon: (yıllık ortalama, genlik, yağış olasılığı, ortalama yağış)
params = {"Ankara": (12.0, 11.5, 0.25, 4.0),
"İzmir": (18.0, 8.5, 0.20, 8.0),
"Erzurum": (6.0, 14.0, 0.30, 3.5)}
names, temps, rains = [], [], []
for name, (m, a, p, r) in params.items():
wave = m - a * np.cos(2 * np.pi * (t - 15) / 365)
temps.append(wave + rng.normal(0, 2.5, t.size))
wet = rng.random(t.size) < p # yağışlı gün mü?
rains.append(np.where(wet, rng.exponential(r, t.size), 0.0))
names += [name] * t.size
temp = np.concatenate(temps).round(1) + 0.0 # -0.0 yerine 0.0
lost = rng.choice(temp.size, size=30, replace=False)
temp[lost] = np.nan # arızalı sensör
df = pd.DataFrame({"tarih": np.tile(days, 3), "istasyon": names,
"sicaklik": temp,
"yagis": np.concatenate(rains).round(1)})
df.to_csv("olcumler.csv", index=False)
print(df.shape)
with open("olcumler.csv", encoding="utf-8") as f:
for _ in range(6): # dosyanın ilk altı satırı
print(f.readline(), end="")Çıktı:
(1095, 4)
tarih,istasyon,sicaklik,yagis
2025-01-01,Ankara,-4.7,0.0
2025-01-02,Ankara,0.9,0.0
2025-01-03,Ankara,-0.6,0.0
2025-01-04,Ankara,-2.1,1.3
2025-01-05,Ankara,,0.0
pd.date_range iki tarih arasındaki bütün günleri bir tarih dizisi olarak üretir; zaman serileri başlığında ona döneceğiz. Tablo \(3 \cdot 365 = 1095\) satırlıdır: önce Ankara’nın 365 günü, sonra İzmir’in, en son Erzurum’unki. Bu yüzden np.tile(days, 3) tarih dizisini uç uca üç kez yazar (her istasyon için bir kopya); names listesi de her istasyonun adını 365 kez içerir. to_csv tabloyu dosyaya yazar; index=False satır etiketlerinin (\(0, 1, \ldots, 1094\)) dosyaya ayrı bir sütun olarak yazılmasını engeller. Dosyanın ilk satırlarında 5 Ocak’ın sıcaklık hücresinin boş kaldığını görüyoruz: silinen ölçümlerden biri. Bölümün bundan sonraki kodları bu dosyanın çalışma klasöründe durduğunu varsayar.
Dosyayı geri okumak için pd.read_csv yeterlidir. Tarih sütununun metin olarak değil tarih olarak okunması için parse_dates parametresini veririz:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
print(df.shape)
print(df.dtypes)
print(df.head())
print(df.tail(3))Çıktı:
(1095, 4)
tarih datetime64[us]
istasyon str
sicaklik float64
yagis float64
dtype: object
tarih istasyon sicaklik yagis
0 2025-01-01 Ankara -4.7 0.0
1 2025-01-02 Ankara 0.9 0.0
2 2025-01-03 Ankara -0.6 0.0
3 2025-01-04 Ankara -2.1 1.3
4 2025-01-05 Ankara NaN 0.0
tarih istasyon sicaklik yagis
1092 2025-12-29 Erzurum -7.5 0.0
1093 2025-12-30 Erzurum -4.7 0.0
1094 2025-12-31 Erzurum -7.9 0.0
Boş hücreler kendiliğinden NaN olarak okundu. tarih sütununun türü datetime64[us]’dir (mikrosaniye çözünürlüklü tarih), istasyon sütunu metindir. parse_dates verilmeseydi tarihler de düz metin (str) olarak kalırdı. read_csv’nin en çok kullanılan parametreleri şunlardır:
| Parametre | Anlamı |
|---|---|
sep |
alan ayırıcı, varsayılanı "," |
decimal |
ondalık işareti, varsayılanı "." |
na_values |
eksik değer sayılacak ek metinler, örneğin "-" |
parse_dates |
tarihe çevrilecek sütunlar |
date_format |
tarihlerin biçimi, örneğin "%d.%m.%Y" |
index_col |
indeks yapılacak sütun |
usecols |
okunacak sütunlar |
encoding |
karakter kodlaması, varsayılanı "utf-8" |
to_csv de sep ve decimal parametrelerini alır: df.to_csv("dosya.csv", sep=";", decimal=",", index=False) Türkçe ayarlı bir hesap tablosu programının doğrudan açabileceği bir dosya yazar. Elimizdeki metin bir dosyada değil de bir değişkendeyse read_csv(io.StringIO(metin)) biçiminde okunabilir.
- Dosyanın ilk birkaç satırına bakın: alan ayırıcı (
,mı,;mı), ondalık işareti (.mı,,mı), eksik değerlerin yazılışı (boş,NA,-) ve tarihlerin biçimi nedir? pd.read_csv’yi bu bilgilere karşılık gelensep,decimal,na_values,parse_datesvedate_formatparametreleriyle çağırın.- Sonucu denetleyin:
shapebeklenen boyutta mı, sayı olması gereken sütunlar sayı türünde mi,isna().sum()kaç eksik değer gösteriyor? Sayı olması gereken bir sütunstrgeldiyse bir parametre eksiktir.
Örnek 15.3 (Türkçe Biçimli Bir Dosyadan Yerçekimi İvmesi) Bir sarkaç deneyinde farklı ip uzunlukları \(L\) (metre) için ölçülen periyotlar \(T\) (saniye), noktalı virgülle ayrılmış, ondalıkları virgüllü ve tarihleri gün.ay.yıl biçiminde olan bir dosyaya yazılmıştır; ölçülemeyen bir periyot - ile gösterilmiştir. Dosyayı okuyup küçük salınımlarda geçerli \(T = 2\pi\sqrt{L/g}\) formülünden her ölçüm için \(g = 4\pi^2 L / T^2\) değerini ve bunların ortalamasını hesaplayın.
Çözüm
Adım 1. Alan ayırıcı ;, ondalık işareti ,, eksik değer -, tarih biçimi %d.%m.%Y’dir.
Adım 2. Bu dört bilgiyi parametre olarak veririz. Kodun kendi başına çalışması için dosyayı da Python ile yazıyoruz.
Adım 3. Türleri ve eksik değer sayılarını yazdırırız.
from pathlib import Path
import numpy as np
import pandas as pd
text = """tarih;uzunluk;periyot
03.03.2025;0,25;1,004
03.03.2025;0,50;1,417
04.03.2025;0,75;1,739
04.03.2025;1,00;-
05.03.2025;1,25;2,241
"""
Path("sarkac.csv").write_text(text, encoding="utf-8")
df = pd.read_csv("sarkac.csv", sep=";", decimal=",", na_values="-",
parse_dates=["tarih"], date_format="%d.%m.%Y")
print(df.dtypes)
print(df.isna().sum().to_dict())
df["g"] = 4 * np.pi**2 * df["uzunluk"] / df["periyot"]**2
print(df.round({"g": 3}))
print(round(df["g"].mean(), 3))Çıktı:
tarih datetime64[us]
uzunluk float64
periyot float64
dtype: object
{'tarih': 0, 'uzunluk': 0, 'periyot': 1}
tarih uzunluk periyot g
0 2025-03-03 0.25 1.004 9.791
1 2025-03-03 0.50 1.417 9.831
2 2025-03-04 0.75 1.739 9.791
3 2025-03-04 1.00 NaN NaN
4 2025-03-05 1.25 2.241 9.826
9.81
Bütün sütunlar doğru türde geldi; tek eksik değer periyot sütunundadır. g sütunu vektörel olarak hesaplandı; periyodu eksik satırda \(g\) de NaN oldu ve mean bu satırı atlayarak dört değerin ortalamasını aldı: \(g \approx 9{,}81\) m/s². Adım 1’in neden gerekli olduğunu görmek için aynı dosyayı parametresiz okuyalım:
import pandas as pd
df = pd.read_csv("sarkac.csv") # parametresiz okuma
print(df.shape)
print(df.head(2))Çıktı:
(5, 1)
tarih;uzunluk;periyot
03.03.2025;0 25;1 4.0
50;1 417.0
Varsayılan ayırıcı virgül olduğundan başlık satırının tamamı tek bir sütun adı oldu. Veri satırlarındaki ondalık virgüller ise alan ayırıcı sanıldı; her satırdan fazladan alan çıkınca pandas bunları satır etiketi yaptı ve örneğin 1,004 sayısının 004 parçası 4.0 diye okundu. Hata vermeden okunan ama anlamsız bir tablo: Adım 3’teki denetim bu yüzden gereklidir. \(\blacksquare\)
15.4 Satır ve Sütun Seçmek
Veri kümemiz elimizde; şimdi ondan istediğimiz parçaları alalım. DataFrame’de seçimin üç aracı vardır: sütun adlarıyla köşeli parantez, etiketlerle loc ve konumlarla iloc.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
print(df["sicaklik"].head(3)) # tek sütun: Series
print(df[["istasyon", "sicaklik"]].head(3)) # liste: DataFrame
print(df.loc[365:367, ["tarih", "istasyon"]]) # etiketlerle
print(df.iloc[730, 1:3]) # konumlarlaÇıktı:
0 -4.7
1 0.9
2 -0.6
Name: sicaklik, dtype: float64
istasyon sicaklik
0 Ankara -4.7
1 Ankara 0.9
2 Ankara -0.6
tarih istasyon
365 2025-01-01 İzmir
366 2025-01-02 İzmir
367 2025-01-03 İzmir
istasyon Erzurum
sicaklik -3.4
Name: 730, dtype: object
Tek bir sütun adı bir Series, sütun adlarından oluşan bir liste ise bir DataFrame verir. loc[365:367, ["tarih", "istasyon"]] etiketi 365’ten 367’ye kadar olan satırları (iki uç dahil) ve iki sütunu seçer; 365 etiketli satır İzmir’in ilk günüdür. iloc[730, 1:3] ise 730 numaralı konumdaki satırın 1 ve 2 numaralı sütunlarını verir: Erzurum’un ilk günü.
Tanım 15.4 (loc ve iloc ile Seçim) df.loc[satırlar, sütunlar] satırları ve sütunları etiketleriyle, df.iloc[satırlar, sütunlar] ise konumlarıyla seçer. Her iki yere tek bir etiket (ya da konum), bir liste, bir dilim ya da bir boolean dizi yazılabilir; boolean dizinin uzunluğu satır yerinde satır sayısı, sütun yerinde sütun sayısı olmalıdır. loc dilimleri iki ucu da içerir, iloc dilimleri sağ ucu içermez.
Yani loc tabloya “adı şu olan satır ve sütun”, iloc ise “şu sıradaki satır ve sütun” diye sorar. İki yere de tek bir değer yazılırsa sonuç tek bir hücredir, biri liste ya da dilim olursa bir Series, ikisi de liste ya da dilim olursa bir DataFrame döner. Varsayılan indeksli bir tabloda etiket ile konum aynı sayılardır; ama tablo süzüldükten ya da sıralandıktan sonra etiketler karışır ve iki araç farklı satırları gösterir.
Çoğu zaman satırları konumlarıyla değil bir koşulla seçeriz. Bir sütunu bir sayıyla karşılaştırmak boolean bir Series verir; onu köşeli parantez içine ya da loc’un satır yerine yazınca yalnız koşulu sağlayan satırlar kalır. Birden çok koşul, Vektörizasyon ve Broadcasting bölümündeki maskelerde olduğu gibi & (ve), | (veya), ~ (değil) ile ve her koşul parantez içinde olmak üzere birleştirilir:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
cold = df[df["sicaklik"] < -12]
print(len(cold), cold["istasyon"].unique().tolist())
hot_dry = df[(df["sicaklik"] > 30) & (df["yagis"] == 0)]
print(hot_dry["istasyon"].value_counts())
print(df[df["istasyon"].isin(["Ankara", "İzmir"])].shape)
print(df.query("istasyon == 'Ankara' and yagis > 15"))Çıktı:
3 ['Erzurum']
istasyon
İzmir 3
Ankara 2
Name: count, dtype: int64
(730, 4)
tarih istasyon sicaklik yagis
38 2025-02-08 Ankara -1.4 33.3
Sıcaklığın \(-12\) °C’nin altına indiği üç günün hepsi Erzurum’dadır. Sıcaklığın \(30\) °C’yi aştığı ve yağışın olmadığı beş günün üçü İzmir’de, ikisi Ankara’dadır. isin bir sütundaki değerin verilen listede olup olmadığını sorar. query aynı süzmeyi bir metin ifadesiyle yazar: sütun adları değişken gibi kullanılır, metin sabitleri tırnak içine alınır. Ankara’da \(15\) mm’den fazla yağışın düştüğü tek gün 8 Şubat’tır.
sort_values tabloyu bir sütuna göre sıralar; nlargest(k, sütun) ve nsmallest(k, sütun) bir sütunun en büyük ya da en küçük \(k\) değerini taşıyan satırları verir:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
print(df.nlargest(3, "sicaklik"))
print(df.sort_values("yagis", ascending=False).head(3))Çıktı:
tarih istasyon sicaklik yagis
203 2025-07-23 Ankara 31.5 0.0
598 2025-08-22 İzmir 31.4 0.0
189 2025-07-09 Ankara 31.0 0.0
tarih istasyon sicaklik yagis
521 2025-06-06 İzmir 21.0 33.7
38 2025-02-08 Ankara -1.4 33.3
629 2025-09-22 İzmir 22.3 24.6
Yılın en sıcak üç ölçümünün ikisi Ankara’da, biri İzmir’dedir; en yağışlı üç günün ikisi İzmir’de, biri Ankara’dadır. Soldaki 203, 598, 189 sayıları satırların özgün tablodaki etiketleridir; satırlar sıralanırken etiketlerini yanlarında taşır. Bu küçük tabloda iloc[0] 203 etiketli satırı verir, loc[0] ise hata verir, çünkü 0 etiketi artık tabloda yoktur.
Bir koşulu sağlayan satırlarda bir sütunu değiştirmek isterken df[maske]["y"] = 0 yazmak doğal görünür. Ama df[maske] yeni bir tablodur; atama o geçici tabloya yapılır ve df değişmez. pandas 3.0 bu durumda ChainedAssignmentError uyarısı verir. Doğrusu, satırları ve sütunu tek bir loc içinde vermektir:
import warnings
import pandas as pd
df = pd.DataFrame({"x": [1, -2, 3, -4], "y": [10, 20, 30, 40]})
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
df[df["x"] < 0]["y"] = 0 # zincirleme atama: etkisiz
print(caught[0].category.__name__)
print(df["y"].tolist())
df.loc[df["x"] < 0, "y"] = 0 # doğru yol: tek bir loc
print(df["y"].tolist())Çıktı:
ChainedAssignmentError
[10, 20, 30, 40]
[10, 0, 30, 0]
Kodda uyarıyı ekrana basmak yerine yakalayıp türünü yazdırdık. pandas 3.0’da bir tablodan seçilen her parça bir kopya gibi davranır; bir tabloyu değiştirmenin yolu ona doğrudan df.loc[...] = ... ya da df[sütun] = ... biçiminde atama yapmaktır.
Örnek 15.4 (Erzurum’da Don Günlerinin Aylara Dağılımı) olcumler.csv dosyasında Erzurum’da sıcaklığın \(0\) °C’nin altında ölçüldüğü günleri aylara göre sayın.
Çözüm
Önce Erzurum satırlarını, sonra bunların arasından sıcaklığı negatif olanları süzeriz. Tarih sütunlarının .dt erişimcisi tarihlerin parçalarını verir: .dt.month ay numarasını, .dt.day ayın gününü, .dt.dayofyear yılın kaçıncı günü olduğunu. Ay numaralarını value_counts ile sayıp aya göre sıralarız.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
erz = df[df["istasyon"] == "Erzurum"]
frost = erz[erz["sicaklik"] < 0]
months = frost["tarih"].dt.month # tarihten ay numarası
print(len(frost))
print(months.value_counts().sort_index().to_dict())Çıktı:
120
{1: 31, 2: 27, 3: 16, 11: 15, 12: 31}
Ölçülmüş 120 don günü vardır: ocakta ve aralıkta her gün, şubatta 27, martta 16, kasımda 15 gün. Nisandan ekime kadar hiç don yoktur. Modelde Erzurum’un ocak ortası sıcaklığı \(6 - 14 = -8\) °C olduğundan ocakta her günün donlu çıkması beklenir. Sıcaklığı eksik olan günler sayılmadı, çünkü NaN < 0 karşılaştırması False verir. \(\blacksquare\)
15.5 Eksik Değerler
Veri kümemizde 30 sıcaklık ölçümü eksik. pandas eksik değerleri ondalıklı sayı sütunlarında NaN ile gösterir ve onları bulmak, saymak, atmak ya da doldurmak için araçlar sunar.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
print(df.isna().sum())
print(df["sicaklik"].count(), df["sicaklik"].size)
print(df[df["sicaklik"].isna()].head(4))Çıktı:
tarih 0
istasyon 0
sicaklik 30
yagis 0
dtype: int64
1065 1095
tarih istasyon sicaklik yagis
4 2025-01-05 Ankara NaN 0.0
8 2025-01-09 Ankara NaN 0.0
45 2025-02-15 Ankara NaN 0.0
49 2025-02-19 Ankara NaN 0.0
isna() her hücre için eksik olup olmadığını söyleyen boolean bir tablo verir; sum() her sütundaki True değerlerini sayar. count() bir sütundaki eksik olmayan değerleri, size ise bütün değerleri sayar: \(1095 - 30 = 1065\). Eksik satırları görmek için isna() maskesini süzmede kullanırız.
Tanım 15.5 (Eksik Değer) Bir hücrede değer bulunmadığını gösteren özel değere eksik değer denir. pandas ondalıklı sayı sütunlarında eksik değeri IEEE 754 standardındaki NaN ile gösterir. isna() eksik değerleri, notna() eksik olmayan değerleri True ile işaretler. Toplam, ortalama ve varyans gibi indirgeme işlemleri eksik değerleri varsayılan olarak atlar; eleman eleman aritmetik ise eksik değeri sonuca taşır.
Yani eksik değerli bir sütunun ortalaması, yalnız mevcut değerlerin ortalamasıdır: toplam, satır sayısına değil count() kadar değere bölünür. Buna karşılık NaN + 1 yine NaN’dır; bir değer bilinmiyorsa ona \(1\) eklenmiş hâli de bilinmez. NaN kendisine bile eşit olmadığı için eksik değerler == ile değil isna() ile aranır:
import numpy as np
import pandas as pd
s = pd.Series([1.0, np.nan, 3.0])
print(s.sum(), s.mean(), s.count()) # toplamalarda NaN atlanır
print(s.sum(skipna=False)) # atlanmasın istenirse
print((s + 1).tolist()) # aritmetikte NaN yayılır
print(np.nan == np.nan, s.isna().tolist())Çıktı:
4.0 2.0 2
nan
[2.0, nan, 4.0]
False [False, True, False]
Toplam \(4\), ortalama \(4/2 = 2\)’dir: NaN hem toplamdan hem paydadan çıkarıldı. skipna=False verilince toplam da NaN olur.
Eksik değerlerle üç şey yapılabilir: satırı atmak (dropna), sabit bir değerle doldurmak (fillna) ya da değeri komşularından kestirmek. Kestirmenin en basit iki yolu, son bilinen değeri ileri taşımak (ffill, forward fill) ve iki komşu bilinen değer arasında doğrusal interpolasyon yapmaktır (interpolate). Hangisinin daha iyi olduğunu görmek için doğru cevabı bildiğimiz bir tablo kuralım: \(\sin x\)’in \([0, \pi]\) aralığındaki dokuz eşit aralıklı değerinden üçünü silelim.
import numpy as np
import pandas as pd
x = np.linspace(0, np.pi, 9)
s = pd.Series(np.sin(x), index=x.round(3))
s.iloc[[2, 3, 6]] = np.nan # üç değer kayıp
print(s.dropna().size)
filled = pd.DataFrame({"kayipli": s,
"ffill": s.ffill(),
"ortalama": s.fillna(s.mean()),
"dogrusal": s.interpolate()})
print(filled.round(4))
true = pd.Series(np.sin(x), index=s.index)
err = filled.drop(columns="kayipli").sub(true, axis=0).abs().max()
print(err.round(4).to_dict())Çıktı:
6
kayipli ffill ortalama dogrusal
0.000 0.0000 0.0000 0.0000 0.0000
0.393 0.3827 0.3827 0.3827 0.3827
0.785 NaN 0.3827 0.4482 0.5885
1.178 NaN 0.3827 0.4482 0.7942
1.571 1.0000 1.0000 1.0000 1.0000
1.963 0.9239 0.9239 0.9239 0.9239
2.356 NaN 0.9239 0.4482 0.6533
2.749 0.3827 0.3827 0.3827 0.3827
3.142 0.0000 0.0000 0.0000 0.0000
{'ffill': 0.5412, 'ortalama': 0.4757, 'dogrusal': 0.1297}
dropna() eksikleri atınca 6 değer kaldı. Hataları hesaplayan satırda drop(columns="kayipli") kayıplı sütunu tablodan çıkarır. sub(true, axis=0) kalan her sütundan true serisini çıkarır; axis=0, serinin etiketlerinin tablonun satır etiketleriyle eşlenmesini söyler (varsayılan eşleme sütun adlarıyladır). Son olarak abs().max() her sütunun en büyük mutlak hatasını verir. Doldurma yöntemlerinin en büyük hataları çıktının son satırındadır: son değeri taşımak \(0{,}54\), ortalamayla doldurmak \(0{,}48\), doğrusal interpolasyon \(0{,}13\) hata yapar. Doğrusal interpolasyonun hatası Nümerik Analiz notlarındaki sınıra göre en çok \(M H^2/8\)’dir; burada \(M\) ikinci türevin mutlak değeri için bir üst sınır, \(H\) de iki bilinen komşu arasındaki uzaklıktır. İki ardışık değerin eksik olduğu aralıkta \(H = 3\pi/8\) ve \(M = 1\) olduğundan sınır \(0{,}17\)’dir ve bulunan \(0{,}13\) bunun altındadır.
ffill() son bilinen değeri ileri taşır (boş daireler), en büyük hatası 0,54'tür. interpolate() iki komşu bilinen değeri doğru parçasıyla birleştirir (dolu daireler), en büyük hatası 0,13'e iner. Kesikli eğri gerçek sin x'tir.Hangi yöntemin uygun olduğu verinin doğasına bağlıdır. Sıcaklık gibi zamanla yavaş değişen bir büyüklükte interpolasyon iyi bir tahmindir. Bir günün yağışı gibi günden güne sıçrayan bir büyüklükte ise eksik bir günü komşularından kestirmek anlamsız olabilir; o zaman satırı atmak daha dürüsttür.
Örnek 15.5 (Eksik Ölçümleri Doldurmanın Ortalamaya Etkisi) olcumler.csv dosyasında her istasyon için eksik sıcaklıkları doğrusal interpolasyonla doldurmanın yıllık ortalama sıcaklığı ne kadar değiştirdiğini hesaplayın.
Çözüm
İstasyon adlarını unique() ile alıp her biri için o istasyonun sıcaklık sütununu süzeriz. Bir istasyonun satırları tarih sırasında ve ardışık olduğundan interpolate() her eksik günü önceki ve sonraki ölçüm arasında doğrusal olarak doldurur.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
for name in df["istasyon"].unique():
temp = df.loc[df["istasyon"] == name, "sicaklik"]
filled = temp.interpolate()
print(f"{name:8} eksik: {temp.isna().sum():2d} ortalama: "
f"{temp.mean():.3f} -> {filled.mean():.3f}")Çıktı:
Ankara eksik: 8 ortalama: 12.143 -> 12.016
İzmir eksik: 9 ortalama: 17.848 -> 17.786
Erzurum eksik: 13 ortalama: 5.790 -> 5.880
Eksik ölçüm sayıları 8, 9 ve 13’tür; toplamları 30’dur. Doldurmak ortalamaları en çok \(0{,}13\) °C değiştirir (Ankara: \(12{,}143 \to 12{,}016\)). Eksikler yılın rastgele günlerine dağıldığı için mevcut değerlerin ortalaması da iyi bir tahmindir ve doldurmak onu pek değiştirmez. Eksikler belirli bir mevsimde toplansaydı, örneğin sensör yalnız kışın bozulsaydı, mevcut değerlerin ortalaması sistematik olarak yukarı kayardı ve interpolasyon bu kaymayı azaltırdı. \(\blacksquare\)
NaN bir ondalıklı sayı değeri olduğu için tamsayı bir sütuna eksik değer girince sütunun türü float64 olur; tabloları birleştirirken bunun bir örneğini göreceğiz. Tamsayı kalması gereken bir sütun için pandas’ın eksik değeri <NA> ile gösteren "Int64" türü (büyük I ile) kullanılabilir:
import pandas as pd
print(pd.Series([1, 2, None]).dtype) # tamsayılar float64 olur
s = pd.Series([1, 2, None], dtype="Int64") # tamsayı kalan sütun
print(s.tolist(), s.dtype, s.sum())Çıktı:
float64
[1, 2, <NA>] Int64 3
15.6 Betimsel İstatistikler
Veri okunup temizlendikten sonra ilk iş onu birkaç sayıyla özetlemektir. Ortalama, medyan, varyans ve çeyreklik gibi betimsel istatistiklerin tanımları Matematiksel İstatistik notlarındadır; pandas bunların çoğunu tek bir çağrıyla verir:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
print(df[["sicaklik", "yagis"]].describe().round(2))Çıktı:
sicaklik yagis
count 1065.00 1095.00
mean 11.95 1.03
std 9.78 3.01
min -13.60 0.00
25% 5.00 0.00
50% 13.00 0.00
75% 19.70 0.00
max 31.50 33.70
describe her sayısal sütun için gözlem sayısını (count, eksikler hariç), ortalamayı, standart sapmayı, en küçük değeri, üç çeyrekliği (25%, 50%, 75%) ve en büyük değeri verir. Yağış sütununun üç çeyrekliği de \(0\)’dır: günlerin dörtte üçünden fazlası kurudur. Aynı sayılar tek tek de alınabilir:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
t = df["sicaklik"]
print(round(t.mean(), 3), t.median())
print(round(t.var(), 3), round(t.std(), 3))
print(t.max() - t.min(), t.quantile([0.1, 0.9]).round(2).tolist())
print(round((df["yagis"] > 0).mean(), 3)) # yağışlı günlerin oranı
print(df["istasyon"].value_counts().to_dict())Çıktı:
11.95 13.0
95.721 9.784
45.1 [-2.96, 23.66]
0.223
{'Ankara': 365, 'İzmir': 365, 'Erzurum': 365}
Yağışlı günlerin oranını (df["yagis"] > 0).mean() ile bulduk: True değerleri \(1\), False değerleri \(0\) sayıldığından boolean bir sütunun ortalaması True değerlerinin oranıdır. pandas yöntemlerinin karşılık geldiği istatistikler şunlardır:
| Yöntem | İstatistik |
|---|---|
mean() |
aritmetik ortalama |
median() |
medyan |
mode() |
mod |
var(), std() |
örneklem varyansı ve standart sapması |
max() - min() |
açıklık |
quantile(p) |
yüzdelik |
cov(), corr() |
kovaryans ve korelasyon |
value_counts() |
frekans tablosu |
var ve std örneklem varyansını, yani \(n - 1\) paydalı \(s^2\)’yi ve onun karekökünü hesaplar. NumPy’ın np.var ve np.std fonksiyonları ise varsayılan olarak \(n\)’ye böler.
Aynı veriye pd.Series(x).std() ile np.std(x) farklı sonuç verir. Fark paydadadır ve ddof (“delta degrees of freedom”) parametresiyle seçilir: payda \(n - \text{ddof}\)’tur. pandas’ta varsayılan ddof=1, NumPy’da ddof=0’dır:
import numpy as np
import pandas as pd
x = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]
print(pd.Series(x).std(), np.std(x))
print(pd.Series(x).std(ddof=0), np.std(x, ddof=1))Çıktı:
2.138089935299395 2.0
2.0 2.138089935299395
Bu sekiz sayının ortalaması \(5\), sapma kareleri toplamı \(32\)’dir: \(\sqrt{32/8} = 2\) ve \(\sqrt{32/7} \approx 2{,}138\). Hangi paydayı kullandığınızı her zaman belirtin.
Yüzdelikler için de bir kural seçmek gerekir. Matematiksel İstatistik notları \(P\). yüzdeliğin sıralı verideki konumunu \((n+1)P/100\) ile belirler. pandas’ın quantile yöntemi ise varsayılan olarak başka bir konum kuralı kullanır.
Tanım 15.6 (Doğrusal Yüzdelik Kuralı) \(x_{(1)} \le x_{(2)} \le \cdots \le x_{(n)}\) sıralı veri ve \(0 \le p \le 1\) olsun. \(h = (n - 1)p + 1\) konumunu \(h = k + d\) (\(k\) tam sayı, \(0 \le d < 1\)) biçiminde yazalım. pandas’ın quantile(p) değeri
\[ Q(p) = x_{(k)} + d\,\big(x_{(k+1)} - x_{(k)}\big) \]
sayısıdır (\(d = 0\) ise \(Q(p) = x_{(k)}\)).
Yani pandas en küçük gözleme \(p = 0\), en büyüğüne \(p = 1\) karşılığını verir ve aradaki \(p\) değerleri için iki komşu sıralı gözlem arasında doğrusal interpolasyon yapar. Matematiksel İstatistik notlarındaki kuralla tek fark konum formülüdür: orada \(h = (n + 1)p\)’dir. NumPy’ın np.quantile fonksiyonu method="weibull" parametresiyle o kuralı uygular. Büyük veride iki kuralın farkı önemsizdir, küçük veride ise görünür.
Örnek 15.6 (İki Yüzdelik Kuralıyla Çeyreklikler) Matematiksel İstatistik notlarındaki \(12, 15, 17, 20, 22, 25, 28, 30, 34, 41\) verisinin çeyrekliklerini doğrusal yüzdelik kuralıyla ve \((n+1)p\) kuralıyla hesaplayıp karşılaştırın.
Çözüm
\(n = 10\)’dur. Doğrusal kuralda \(Q_1\) için \(h = 9 \cdot 0{,}25 + 1 = 3{,}25\), \(Q_2\) için \(h = 5{,}5\), \(Q_3\) için \(h = 7{,}75\)’tir:
\[ \begin{aligned} Q_1 &= x_{(3)} + 0{,}25\,\big(x_{(4)} - x_{(3)}\big) = 17 + 0{,}25 \cdot 3 = 17{,}75,\\[1mm] Q_2 &= x_{(5)} + 0{,}5\,\big(x_{(6)} - x_{(5)}\big) = 22 + 0{,}5 \cdot 3 = 23{,}5,\\[1mm] Q_3 &= x_{(7)} + 0{,}75\,\big(x_{(8)} - x_{(7)}\big) = 28 + 0{,}75 \cdot 2 = 29{,}5. \end{aligned} \]
Kodla doğrulayıp \((n+1)p\) kuralıyla karşılaştıralım:
import numpy as np
import pandas as pd
x = pd.Series([12, 15, 17, 20, 22, 25, 28, 30, 34, 41])
q = [0.25, 0.5, 0.75]
print(x.quantile(q).tolist()) # pandas kuralı
print(np.quantile(x, q, method="weibull").tolist()) # (n + 1)p kuralıÇıktı:
[17.75, 23.5, 29.5]
[16.5, 23.5, 31.0]
method="weibull" ile bulunan \(16{,}5\), \(23{,}5\) ve \(31\) değerleri notlardaki sonuçların aynısıdır. Medyan iki kuralda da aynıdır, çeyreklikler ise farklıdır: doğrusal kural çeyreklikleri ortaya doğru, \((n+1)p\) kuralı uçlara doğru yerleştirir. Çeyrekler arası açıklık birinde \(29{,}5 - 17{,}75 = 11{,}75\), ötekinde \(31 - 16{,}5 = 14{,}5\)’tir. Yüzdelik içeren bir sonucu başka bir hesapla karşılaştırırken hangi kuralın kullanıldığını kontrol edin. \(\blacksquare\)
Bir ortalamanın ne kadar güvenilir olduğunu, onu oluşturan gözlemlerin sayısı belirler. Bölümün geri kalanında birkaç kez kullanacağımız şu sonucu hatırlayalım.
Önerme 15.1 (Bağımsız Gözlemlerin Ortalaması) \(X_1, \ldots, X_n\) bağımsız ve her biri \(\mu\) beklenen değerli, \(\sigma^2\) varyanslı rastgele değişkenler olsun. \(\bar X = (X_1 + \cdots + X_n)/n\) için \(E(\bar X) = \mu\) ve \(\operatorname{Var}(\bar X) = \sigma^2 / n\)’dir; yani \(\bar X\)’in standart sapması \(\sigma/\sqrt{n}\)’dir. İspatı için bkz. Matematiksel İstatistik.
Örnek 15.7 (Zar Atışlarının Frekans Tablosu) default_rng(7) üreteciyle atılan 6000 zarın göreli frekanslarını, ortalamasını ve varyansını bir zarın kuramsal değerleriyle karşılaştırın.
Çözüm
rng.integers(1, 7, size=6000) \(\{1, \ldots, 6\}\) kümesinden eşit olasılıkla seçilmiş 6000 tamsayı verir (üst sınır dahil değildir). Tek bir atış \(X\) için \(E(X) = (1 + \cdots + 6)/6 = 7/2\) ve \(E(X^2) = 91/6\)’dır. Buradan varyans \(\operatorname{Var}(X) = 91/6 - 49/4\) olarak bulunur; bu sayı \(35/12 \approx 2{,}9167\)’dir.
import numpy as np
import pandas as pd
rng = np.random.default_rng(7)
rolls = pd.Series(rng.integers(1, 7, size=6000))
freq = rolls.value_counts(normalize=True).sort_index()
print(freq.round(4))
print(round(rolls.mean(), 4), round(rolls.var(), 4))
print(3.5, round(35 / 12, 4))Çıktı:
1 0.1590
2 0.1740
3 0.1685
4 0.1578
5 0.1735
6 0.1672
Name: proportion, dtype: float64
3.5143 2.9023
3.5 2.9167
Göreli frekanslar \(0{,}158\) ile \(0{,}174\) arasında, \(1/6 \approx 0{,}1667\) çevresindedir. Ortalama \(3{,}5143\), varyans \(2{,}9023\)’tür. Önerme 15.1 gereği 6000 atışın ortalamasının standart sapması \(\sqrt{(35/12)/6000} \approx 0{,}022\) olduğundan ortalamadaki \(0{,}014\)’lük sapma beklenen büyüklüktedir; aynı soru Matematiksel İstatistik notlarında büyük sayılar yasasıyla ele alınır. \(\blacksquare\)
15.7 Gruplama: groupby
Bölümün başındaki küçük örnekte istasyonların ortalamasını groupby ile bulmuştuk. Şimdi bu işlemin ne yaptığına yakından bakalım.
Tanım 15.7 (Ayır, Uygula, Birleştir) Bir DataFrame’in satır etiketleri kümesi \(I\), bir anahtar sütunu da \(k\) olsun ve \(k\) sütununun aldığı farklı değerler \(g_1, \ldots, g_m\) olsun. Satırlar
\[ G_j = \{\, i \in I : k_i = g_j \,\}, \qquad j = 1, \ldots, m \]
gruplarına ayrılır. Bir indirgeme fonksiyonu (ortalama, toplam, en büyük değer, …) her grubun değerlerine uygulanır ve \(m\) sonuç, etiketleri \(g_1, \ldots, g_m\) olan bir Series’te birleştirilir. df.groupby(k)[sütun].mean() gibi bir ifade bu üç adımı yapar.
Yani groupby, satırlar kümesini “anahtarı aynı” denklik bağıntısının denklik sınıflarına ayırır ve her sınıfı tek bir sayıya indirger. Sonuçta grup sayısı kadar satır olur ve satırlar anahtarın sıralanmış değerleriyle etiketlenir.
df.groupby("istasyon")["sicaklik"].mean() işleminin üç adımı. Ayır: satırlar istasyon adına göre üç gruba bölünür. Uygula: her grubun sıcaklıklarına mean uygulanır, örneğin Ankara için (0,4 + 23,6)/2 = 12. Birleştir: üç sonuç, indeksi grup adları olan tek bir Series'te toplanır.Şimdi aynı işlemi bütün veri kümesine uygulayalım:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
g = df.groupby("istasyon")
print(g["sicaklik"].mean().round(2))
print(g.size())
stats = g["sicaklik"].agg(["count", "mean", "std", "min", "max"])
print(stats.round(2))Çıktı:
istasyon
Ankara 12.14
Erzurum 5.79
İzmir 17.85
Name: sicaklik, dtype: float64
istasyon
Ankara 365
Erzurum 365
İzmir 365
dtype: int64
count mean std min max
istasyon
Ankara 357 12.14 8.40 -4.7 31.5
Erzurum 352 5.79 10.27 -13.6 25.6
İzmir 356 17.85 6.27 6.4 31.4
g = df.groupby("istasyon") henüz bir şey hesaplamaz, yalnız grupları belirler. g["sicaklik"].mean() her grubun ortalama sıcaklığını, g.size() her grubun satır sayısını verir. agg birden çok indirgemeyi birlikte uygular ve her biri için bir sütun açar. count sütunu eksikleri saymaz: Erzurum’un 365 günün 352’sinde ölçümü vardır.
Sonuçlar modelle uyumludur: ortalamalar \(m = 12\), \(6\) ve \(18\) değerlerine yakındır. Standart sapmalar da önceden kestirilebilir. \(a\cos\theta\) dalgasının bir tam periyottaki varyansı \(a^2/2\)’dir ve gürültü bundan bağımsız olarak \(2{,}5^2\) ekler. Ankara için \(\sqrt{11{,}5^2/2 + 2{,}5^2} \approx 8{,}51\), Erzurum için \(10{,}21\), İzmir için \(6{,}51\) çıkar; tablodaki değerler \(8{,}40\), \(10{,}27\) ve \(6{,}27\)’dir.
agg’ye yeni_ad=(sütun, fonksiyon) biçiminde adlandırılmış argümanlar verilirse her sonuç sütununun adını ve hangi sütundan hesaplanacağını biz seçeriz:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
summary = df.groupby("istasyon").agg(
en_dusuk=("sicaklik", "min"),
en_yuksek=("sicaklik", "max"),
toplam_yagis=("yagis", "sum"),
)
summary["aciklik"] = summary["en_yuksek"] - summary["en_dusuk"]
print(summary.round(1))Çıktı:
en_dusuk en_yuksek toplam_yagis aciklik
istasyon
Ankara -4.7 31.5 349.2 36.2
Erzurum -13.6 25.6 373.7 39.2
İzmir 6.4 31.4 403.9 25.0
Erzurum’un yıllık sıcaklık açıklığı \(39{,}2\) °C ile en büyük, İzmir’inki \(25{,}0\) °C ile en küçüktür.
Gruplama birden çok sütuna göre de yapılabilir. Tarihten ay numarasını yeni bir sütun olarak çıkarıp istasyon ve ay çiftlerine göre gruplayalım:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
df["ay"] = df["tarih"].dt.month
monthly = df.groupby(["istasyon", "ay"])["sicaklik"].mean()
print(monthly.head(3).round(2))
table = monthly.unstack("istasyon")
print(table.round(1))
pt = df.pivot_table(index="ay", columns="istasyon", values="sicaklik",
aggfunc="mean")
print(pt.equals(table))Çıktı:
istasyon ay
Ankara 1 0.51
2 2.16
3 6.83
Name: sicaklik, dtype: float64
istasyon Ankara Erzurum İzmir
ay
1 0.5 -8.0 9.9
2 2.2 -6.4 10.1
3 6.8 -1.0 13.9
4 11.0 5.5 17.1
5 17.0 12.6 21.5
6 21.0 17.4 24.7
7 24.2 19.4 26.3
8 21.9 18.0 25.0
9 17.4 13.5 21.7
10 12.1 6.2 17.9
11 6.8 -0.9 13.9
12 2.5 -6.3 11.2
True
İki anahtarla gruplamanın sonucu, iki düzeyli bir indeksle (MultiIndex) etiketlenmiş 36 elemanlı bir Series’tir. unstack("istasyon") istasyon düzeyini sütunlara taşıyıp okunaklı bir \(12 \times 3\) tablo kurar. Aynı tablo pivot_table ile tek adımda da kurulur; equals iki tablonun aynı olduğunu doğruluyor. Tabloda mevsim dalgası açıkça görülür: her istasyonda en soğuk ay ocak, en sıcak ay temmuzdur.
Gruplara quantile de uygulanabilir. Her istasyonun beş sayılı özetini, yani en küçük değerini, üç çeyrekliğini ve en büyük değerini çıkaralım:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
q = [0, 0.25, 0.5, 0.75, 1]
five = df.groupby("istasyon")["sicaklik"].quantile(q).unstack()
print(five)Çıktı:
0.00 0.25 0.50 0.75 1.00
istasyon
Ankara -4.7 4.8 11.6 19.800 31.5
Erzurum -13.6 -3.9 5.9 15.250 25.6
İzmir 6.4 12.2 18.0 23.025 31.4
Bu beş sayı, Matematiksel İstatistik notlarında tanıtılan kutu grafiğiyle çizilir. Yaklaşık 350 gözlemde iki yüzdelik kuralının konumları yalnız yarım sıra farklıdır; fark ihmal edilebilir.
quantile([0, 0.25, 0.5, 0.75, 1]) tablosundan. Kutu birinci çeyreklikten üçüncüye uzanır, kalın çizgi medyandır, bıyıklar en küçük ve en büyük ölçüme gider. İzmir'in kutusu sağda ve dar, Erzurum'unki solda ve geniştir: İzmir hem daha sıcak hem de yıl içinde daha az değişkendir.agg her gruptan tek bir sayı döndürür. Bazen ise her satırın yanına kendi grubunun bir özetini yazmak isteriz; bunu transform yapar: grup sonucunu grubun her satırına yayar ve özgün tabloyla aynı uzunlukta bir Series döndürür.
Örnek 15.8 (Aylık Ortalamadan Sapmalar) Bir ölçümün anomalisi, ölçümden ait olduğu istasyonun o ayki ortalamasının çıkarılmasıyla bulunur. En büyük pozitif anomalinin hangi gün ve hangi istasyonda görüldüğünü bulun.
Çözüm
Gruplar istasyon ve ay çiftleridir. transform("mean") her satıra kendi grubunun ortalamasını yazar; anomali, ölçüm ile bu ortalamanın farkıdır. idxmax en büyük anomalinin satır etiketini verir, loc o satırı getirir.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
df["ay"] = df["tarih"].dt.month
keys = ["istasyon", "ay"]
df["normal"] = df.groupby(keys)["sicaklik"].transform("mean")
df["anomali"] = df["sicaklik"] - df["normal"]
cols = ["tarih", "istasyon", "sicaklik", "normal", "anomali"]
print(df[cols].head(3).round({"normal": 2, "anomali": 2}))
print(df.loc[df["anomali"].idxmax(), cols])
print(df.groupby("istasyon")["anomali"].std().round(2))Çıktı:
tarih istasyon sicaklik normal anomali
0 2025-01-01 Ankara -4.7 0.51 -5.21
1 2025-01-02 Ankara 0.9 0.51 0.39
2 2025-01-03 Ankara -0.6 0.51 -1.11
tarih 2025-10-01 00:00:00
istasyon İzmir
sicaklik 25.4
normal 17.903226
anomali 7.496774
Name: 638, dtype: object
istasyon
Ankara 2.81
Erzurum 2.92
İzmir 2.50
Name: anomali, dtype: float64
normal sütunu her satıra kendi istasyonunun o ayki ortalamasını yazdı: Ankara’nın ocak ortalaması \(0{,}51\)’dir. En büyük pozitif anomali 1 Ekim’de İzmir’dedir: o günkü \(25{,}4\) °C, İzmir’in ekim ortalamasının \(7{,}5\) °C üstündedir. Bu, gürültünün standart sapmasının (\(2{,}5\)) üç katıdır; 1065 ölçüm arasında böyle bir uç değerin bulunması şaşırtıcı değildir. Anomalilerin standart sapması gürültününkine yakındır. Ankara ve Erzurum’da biraz daha büyüktür, çünkü genliği büyük istasyonlarda aylık ortalama, sıcaklığın ay içindeki yükselişini ya da düşüşünü izleyemez ve bu eğilim de anomaliye karışır. \(\blacksquare\)
pivot uzun biçimli bir tabloyu (her satırda bir tarih ve istasyon çifti) geniş biçime (her satırda bir tarih, her istasyon ayrı bir sütunda) çevirir. Geniş tabloda corr, sütunlar arasındaki korelasyon katsayılarının tablosunu verir:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
wide = df.pivot(index="tarih", columns="istasyon", values="sicaklik")
print(wide.head(3))
print(wide.corr().round(3))Çıktı:
istasyon Ankara Erzurum İzmir
tarih
2025-01-01 -4.7 -3.4 9.5
2025-01-02 0.9 -5.8 12.1
2025-01-03 -0.6 -7.6 10.1
istasyon Ankara Erzurum İzmir
istasyon
Ankara 1.000 0.920 0.876
Erzurum 0.920 1.000 0.901
İzmir 0.876 0.901 1.000
Korelasyonlar \(0{,}88\) ile \(0{,}92\) arasındadır. Bu yüksek değerler istasyonların birbirini etkilediğini göstermez: üçü de aynı mevsim dalgasını izlediği için sıcaklıkları birlikte yükselip alçalır. Modelde günlük gürültüler bağımsızdır; mevsim etkisi çıkarılınca korelasyonun neredeyse sıfıra indiğini bir alıştırmada göreceğiz. corr eksik değerli satırları her sütun çifti için ayrı ayrı atar.
15.8 Tabloları Birleştirmek
Veriler çoğu zaman birden çok tabloya dağılmıştır: ölçümler bir tabloda, istasyonların bilgileri başka bir tabloda durur. İki tabloyu ortak bir sütun üzerinden yan yana birleştirmek merge yönteminin işidir. Aşağıda istasyonların ortalama sıcaklıklarını plaka ve bölge bilgilerini taşıyan küçük bir tabloyla birleştiriyoruz. groupby’a verilen as_index=False, grup anahtarını indekse taşımak yerine sıradan bir sütun olarak bırakır; böylece mean_t, istasyon ve sicaklik sütunlu bir DataFrame olur ve merge anahtarı iki tablonun da istasyon sütununda bulur.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
mean_t = df.groupby("istasyon", as_index=False)["sicaklik"].mean()
mean_t["sicaklik"] = mean_t["sicaklik"].round(2)
info = pd.DataFrame({"istasyon": ["Ankara", "İzmir", "Trabzon"],
"plaka": [6, 35, 61],
"bolge": ["İç Anadolu", "Ege", "Karadeniz"]})
print(mean_t)
print(info)
for how in ["inner", "left", "outer"]:
print(f"how={how!r}")
print(mean_t.merge(info, on="istasyon", how=how))Çıktı:
istasyon sicaklik
0 Ankara 12.14
1 Erzurum 5.79
2 İzmir 17.85
istasyon plaka bolge
0 Ankara 6 İç Anadolu
1 İzmir 35 Ege
2 Trabzon 61 Karadeniz
how='inner'
istasyon sicaklik plaka bolge
0 Ankara 12.14 6 İç Anadolu
1 İzmir 17.85 35 Ege
how='left'
istasyon sicaklik plaka bolge
0 Ankara 12.14 6.0 İç Anadolu
1 Erzurum 5.79 NaN NaN
2 İzmir 17.85 35.0 Ege
how='outer'
istasyon sicaklik plaka bolge
0 Ankara 12.14 6.0 İç Anadolu
1 Erzurum 5.79 NaN NaN
2 Trabzon NaN 61.0 Karadeniz
3 İzmir 17.85 35.0 Ege
Tanım 15.8 (Anahtar Sütunuyla Birleştirme) \(L\) ve \(R\) iki DataFrame, \(k\) ikisinde de bulunan bir sütun olsun. L.merge(R, on=k) sonucu, \(k\) değerleri eşit olan her \((\ell, r) \in L \times R\) satır çifti için \(\ell\) ile \(r\)’nin sütunlarını yan yana koyan bir satırdan oluşur. how parametresi karşılığı olmayan anahtarlara ne olacağını belirler: "inner" yalnız iki tabloda da bulunan anahtarları tutar; "left" \(L\)’nin, "right" \(R\)’nin, "outer" iki tablonun bütün anahtarlarını tutar ve karşılığı olmayan hücrelere NaN yazar.
Yani birleştirme, iki tablonun satır çiftlerinden anahtarları eşleşenleri seçer; ilişkisel veritabanlarındaki join işleminin aynısıdır. Anahtar kümeleri \(A_L\) ve \(A_R\) ise inner sonucunun anahtarları \(A_L \cap A_R\), outer sonucununkiler \(A_L \cup A_R\), left sonucununkiler \(A_L\)’dir. Örnekte Erzurum yalnız solda, Trabzon yalnız sağdadır.
mean_t.merge(info, on="istasyon", how=...) sonuçları (bölge sütunu yer kazanmak için çizilmedi). inner yalnız iki tabloda da bulunan anahtarları, left sol tablonun bütün anahtarlarını, outer iki tablonun bütün anahtarlarını tutar. Karşılığı olmayan hücreler NaN olur; bu yüzden left ve outer sonuçlarında plaka sütunu ondalıklı sayıya döner.left ve outer sonuçlarında plaka sütunu 6.0 gibi yazıldı: sütuna NaN girdiği için türü float64 oldu.
Bir anahtar sağdaki tabloda iki kez geçiyorsa soldaki tablonun o anahtarlı her satırı iki kez eşleşir ve sonuç sessizce büyür. İstasyon tablosuna Ankara’yı yanlışlıkla iki kez yazdığımızı düşünelim:
import pandas as pd
from pandas.errors import MergeError
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
info = pd.DataFrame({"istasyon": ["Ankara", "Ankara", "İzmir", "Erzurum"],
"plaka": [6, 6, 35, 25]}) # Ankara iki kez yazılmış
m = df.merge(info, on="istasyon")
print(len(df), len(m))
try:
df.merge(info, on="istasyon", validate="m:1")
except MergeError as err:
print("MergeError:", str(err).splitlines()[0])Çıktı:
1095 1460
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
1095 ölçüm satırı 1460 satıra çıktı: Ankara’nın 365 günü iki kez sayıldı. validate="m:1" (“çoktan bire”) sağdaki tablonun anahtarlarının tekrarsız olmasını şart koşar ve koşul bozulursa MergeError hatası verir. Bir ölçüm tablosunu bir bilgi tablosuyla birleştirirken bu denetimi eklemek iyi bir alışkanlıktır.
merge tabloları yan yana birleştirir. Aynı sütunlara sahip tabloları alt alta eklemek için pd.concat kullanılır; ignore_index=True satırları \(0\)’dan yeniden numaralar. Eski pandas sürümlerindeki DataFrame.append yöntemi kaldırılmıştır.
import pandas as pd
a = pd.DataFrame({"n": [1, 2], "kare": [1, 4]})
b = pd.DataFrame({"n": [3, 4], "kare": [9, 16]})
print(pd.concat([a, b], ignore_index=True))Çıktı:
n kare
0 1 1
1 2 4
2 3 9
3 4 16
Örnek 15.9 (Gözlenen Aylık Ortalamalar ile Modelin Karşılaştırılması) Her istasyon ve ay için gözlenen ortalama sıcaklığı, modelin o ayın 15’indeki değeriyle iki anahtarlı bir merge kullanarak karşılaştırın.
Çözüm
Gözlenen ortalamaları groupby ile, model değerlerini bir liste comprehension ile 36 satırlık bir tabloda toplarız. as_index=False sayesinde istasyon ve ay anahtarları obs tablosunda sıradan sütunlar olarak kalır; model tablosunda da aynı iki sütun vardır. Her ayın 15’inin yılın kaçıncı günü olduğunu ay başlarından buluruz: freq="MS" (“month start”) ay başlarını üretir, iki tarihin farkının .days özelliği aradaki gün sayısıdır. Birleştirmede iki anahtar vardır: on=["istasyon", "ay"]. Her istasyon ve ay çifti iki tabloda da bir kez geçtiğinden validate="1:1" istenir.
import numpy as np
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
df["ay"] = df["tarih"].dt.month
obs = df.groupby(["istasyon", "ay"], as_index=False)["sicaklik"].mean()
# Modelin her ayın 15'indeki değeri: m - a cos(2 pi (t - 15) / 365)
params = {"Ankara": (12.0, 11.5), "İzmir": (18.0, 8.5),
"Erzurum": (6.0, 14.0)}
starts = pd.date_range("2025-01-01", periods=12, freq="MS") # ay başları
t = (starts - starts[0]).days.to_numpy() + 14 # ayın 15'i
rows = [(name, k + 1, m - a * np.cos(2 * np.pi * (tk - 15) / 365))
for name, (m, a) in params.items() for k, tk in enumerate(t)]
model = pd.DataFrame(rows, columns=["istasyon", "ay", "model"])
both = obs.merge(model, on=["istasyon", "ay"], validate="1:1")
both["fark"] = both["sicaklik"] - both["model"]
print(both.shape)
print(both.head(3).round(2))
print(round(both["fark"].abs().max(), 2), round(both["fark"].std(), 2))Çıktı:
(36, 5)
istasyon ay sicaklik model fark
0 Ankara 1 0.51 0.50 0.01
1 Ankara 2 2.16 2.00 0.16
2 Ankara 3 6.83 5.77 1.06
1.06 0.43
36 satırın hepsi eşleşti. En büyük fark \(1{,}06\) °C’dir (Ankara, mart), farkların standart sapması \(0{,}43\)’tür. Bu beklenen büyüklüktür: bir ayın ortalaması yaklaşık 30 bağımsız gürültü teriminin ortalamasını içerir ve Önerme 15.1 gereği bu ortalamanın standart sapması \(2{,}5/\sqrt{30} \approx 0{,}46\)’dır. \(\blacksquare\)
15.9 Zaman Serileri
Ölçümlerimizin her biri bir tarihe bağlıdır. Tarihleri indeks yapınca pandas zamanla ilgili soruları kolaylaştırır: belirli bir dönemi seçmek, günlük veriyi aylığa çevirmek, hareketli ortalama almak.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
ank = df[df["istasyon"] == "Ankara"].set_index("tarih")["sicaklik"]
print(type(ank.index).__name__, ank.index.dtype)
print(ank.index[0], ank.index[-1])
print(ank.loc["2025-07-01":"2025-07-04"])
print(round(ank.loc["2025-02"].mean(), 2)) # yalnız şubat
print(ank.idxmax().date(), ank.max())Çıktı:
DatetimeIndex datetime64[us]
2025-01-01 00:00:00 2025-12-31 00:00:00
tarih
2025-07-01 23.9
2025-07-02 22.9
2025-07-03 23.2
2025-07-04 24.6
Name: sicaklik, dtype: float64
2.16
2025-07-23 31.5
set_index("tarih") tarih sütununu indekse taşır; Ankara satırlarından sıcaklık sütununu seçince indeksi tarihler olan bir Series elde ederiz. Böyle bir indekse DatetimeIndex denir. Tarih indeksinde etiket dilimleri tarih metinleriyle yazılabilir ve iki uç dahildir. Yalnız ayı yazmak ("2025-02") o ayın bütün günlerini seçer: Ankara’nın şubat ortalaması \(2{,}16\) °C’dir. En sıcak gün 23 Temmuz’dur.
Günlük veriyi aylığa çevirmek de bir gruplamadır: her gün ait olduğu aya göre gruplanır ve her gruba bir indirgeme uygulanır. Zaman indeksli bir Series’te bunu resample yapar:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
ank = df[df["istasyon"] == "Ankara"].set_index("tarih")["sicaklik"]
monthly = ank.resample("ME").mean()
print(monthly.round(2))Çıktı:
tarih
2025-01-31 0.51
2025-02-28 2.16
2025-03-31 6.83
2025-04-30 10.97
2025-05-31 17.00
2025-06-30 20.99
2025-07-31 24.15
2025-08-31 21.93
2025-09-30 17.39
2025-10-31 12.12
2025-11-30 6.76
2025-12-31 2.45
Freq: ME, Name: sicaklik, dtype: float64
"ME" kodu (“month end”) günleri aylara göre gruplar ve her ayı son günüyle etiketler. Haftalık gruplama için "W", yıllık için "YE" yazılır. Sonuç, gruplama başlığında groupby ile bulduğumuz aylık tablonun Ankara sütunuyla aynıdır.
Aylık ortalama yılı on iki parçaya böler ve her parçayı tek bir sayıyla temsil eder. Her gün için o günün çevresindeki bir pencerenin ortalamasını almak ise günlük çözünürlüğü korur.
Tanım 15.9 (Merkezî Hareketli Ortalama) \(x_0, x_1, \ldots, x_{N-1}\) bir zaman serisi ve \(w = 2q + 1\) tek bir pencere uzunluğu olsun. \(q \le t \le N - 1 - q\) için
\[ \bar x_t = \frac{1}{w}\sum_{j=-q}^{q} x_{t+j} \]
sayılarına serinin \(w\) uzunluklu merkezî hareketli ortalaması denir. Pencerenin serinin dışına taştığı ilk ve son \(q\) günde \(\bar x_t\) tanımlı değildir. pandas’ta s.rolling(w, center=True).mean() ile hesaplanır.
Yani her gün, kendisinin ve iki yanındaki \(q\)’şar günün ortalamasıyla değiştirilir. Günden güne bağımsız olan gürültü bu ortalamada büyük ölçüde birbirini götürür: Önerme 15.1 gereği \(w\) bağımsız gürültü teriminin ortalamasının standart sapması \(\sigma/\sqrt{w}\)’dir ve \(w = 31\), \(\sigma = 2{,}5\) için bu \(0{,}45\)’tir. Yavaş değişen mevsim dalgası ise neredeyse olduğu gibi kalır. center=True verilmezse pencere \(t - w + 1, \ldots, t\) günlerinden, yani yalnız o güne kadarki günlerden oluşur.
rolling(...).mean() varsayılan olarak yalnız penceredeki değerlerin hepsi mevcutsa bir sonuç üretir. Ankara serisinde yalnız 8 gün eksik olduğu hâlde 31 günlük hareketli ortalamanın 124 değeri NaN çıkar, çünkü her eksik gün kendisini içeren 31 pencerenin hepsini bozar. Önce eksikleri interpolate() ile doldurmak ya da min_periods parametresiyle daha az değerle yetinmek gerekir:
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
ank = df[df["istasyon"] == "Ankara"].set_index("tarih")["sicaklik"]
r1 = ank.rolling(31, center=True).mean()
r2 = ank.interpolate().rolling(31, center=True).mean()
print(ank.isna().sum(), r1.isna().sum(), r2.isna().sum())
print(r2.loc["2025-07-13":"2025-07-17"].round(2))
print(round((ank - r2).std(), 3))Çıktı:
8 124 30
tarih
2025-07-13 24.30
2025-07-14 24.17
2025-07-15 24.17
2025-07-16 24.15
2025-07-17 23.99
Name: sicaklik, dtype: float64
2.635
Eksikler doldurulunca yalnız baştaki ve sondaki 15’er gün, toplam 30 değer tanımsız kalır. Günlük değerlerin hareketli ortalamadan sapmalarının standart sapması \(2{,}635\)’tir ve gürültünün standart sapması olan \(2{,}5\)’e yakındır.
Zaman serilerini en iyi grafik anlatır. Matplotlib ile Grafik Çizimi bölümündeki nesne yönelimli arayüzle günlük değerleri, hareketli ortalamayı ve aylık ortalamaları aynı eksenlere çizelim. ax.plot bir Series’in indeksini ve değerlerini doğrudan kabul eder; tarih ekseni kendiliğinden biçimlenir.
import matplotlib.pyplot as plt
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
raw = df[df["istasyon"] == "Ankara"].set_index("tarih")["sicaklik"]
ank = raw.interpolate()
smooth = ank.rolling(31, center=True).mean()
monthly = raw.resample("ME").mean()
mid = monthly.index - pd.Timedelta(days=15) # ay ortaları
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ank.index, ank, lw=0.7, alpha=0.6, label="günlük")
ax.plot(smooth.index, smooth, lw=2.2, label="31 günlük ortalama")
ax.plot(mid, monthly, "o", label="aylık ortalama")
ax.set_ylabel("sıcaklık (°C)")
ax.legend()
fig.savefig("ankara.png", dpi=150)
print(len(ax.lines), monthly.idxmax().month, monthly.idxmin().month)Çıktı:
3 7 1
Kod üç çizgi çizip grafiği ankara.png dosyasına kaydetti; aylık ortalamaların en büyüğü 7. ayda, en küçüğü 1. aydadır. Aylık ortalamalar ayın son günüyle etiketlendiği için 15 gün geri kaydırılarak ay ortalarına yerleştirildi. Aşağıdaki şekil aynı grafiğin aynı veriden bu sitenin çizim aracıyla yeniden çizilmiş hâlidir. pandas’ın kendi plot yöntemi de vardır: ank.plot(ax=ax) aynı çizgiyi Matplotlib ile çizer.
resample("ME").mean() ile bulunan aylık ortalamalardır ve ay ortalarına yerleştirilmiştir. Hareketli ortalama günlük gürültüyü bastırır, mevsim dalgası kalır.Zaman serilerinde sık kullanılan birkaç işlem daha vardır: shift(k) seriyi \(k\) adım kaydırır, yani her güne \(k\) gün önceki değeri yazar; diff() ardışık farkları \(x_t - x_{t-1}\) verir; cumsum() birikimli toplamı, expanding().mean() ise başlangıçtan o güne kadarki ortalamayı hesaplar. Aşağıdaki örnek shift ile ardışık donlu günlerden oluşan dizileri buluyor.
Örnek 15.10 (Erzurum’da En Uzun Don Dizisi) Eksik günleri doğrusal interpolasyonla doldurarak Erzurum’da sıcaklığın üst üste \(0\) °C’nin altında kaldığı en uzun dönemi bulun.
Çözüm
Önce her günün donlu olup olmadığını gösteren boolean bir frost serisi kurarız. Bir dizi, frost değerinin bir önceki günden farklı olduğu günde başlar; frost != frost.shift() bu günlerde True olur. İlk gün de True olur, çünkü shift ilk güne NaN yazar. Bu boolean serinin birikimli toplamı, her güne kaçıncı dizide olduğunu gösteren bir numara verir. İki seriyi bir tabloda yan yana koyduğumuzda tarihler indekste durur; set_index’in tersi olan reset_index() onları yeniden tarih sütununa taşır, çünkü her dizinin tarihlerini bu sütundan alacağız. Son olarak yalnız donlu günleri alıp dizi numarasına göre gruplar, her dizinin ilk ve son tarihini ve uzunluğunu buluruz.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
erz = df[df["istasyon"] == "Erzurum"].set_index("tarih")["sicaklik"]
frost = erz.interpolate() < 0 # don günü mü?
run_id = (frost != frost.shift()).cumsum() # değişince yeni dizi
days = pd.DataFrame({"don": frost, "dizi": run_id}).reset_index()
runs = days[days["don"]].groupby("dizi")["tarih"].agg(
["min", "max", "size"])
print(len(runs))
print(runs.nlargest(3, "size"))Çıktı:
10
min max size
dizi
1 2025-01-01 2025-03-01 60
19 2025-11-20 2025-12-31 42
3 2025-03-03 2025-03-11 9
Yıl içinde 10 ayrı don dizisi vardır. En uzunu 1 Ocak’tan 1 Mart’a kadar \(31 + 28 + 1 = 60\) gün sürmüştür. İkinci en uzun dizi 20 Kasım’da başlar ve yılın son gününe kadar 42 gün sürer; veri yıl sonunda kesildiği için bu dizinin gerçek uzunluğunu bilmiyoruz. \(\blacksquare\)
15.10 Alıştırmalar
Alıştırmaların çoğu, bölümde ürettiğimiz olcumler.csv dosyasını kullanır.
Alıştırma 15.1 (İki Zarın Toplamının Dağılımı) default_rng(12) üreteciyle iki zarı 36000 kez atın. Toplamın göreli frekanslarını \(s = 2, \ldots, 12\) için kuramsal olasılıklar \((6 - |s - 7|)/36\) ile bir DataFrame’de yan yana koyarak en büyük mutlak farkı bulun.
Çözüm
rng.integers(1, 7, size=(36000, 2)) her satırı bir atış olan iki sütunlu bir tamsayı dizisi verir; satır toplamları zarların toplamıdır. value_counts(normalize=True).sort_index() göreli frekansları \(2\)’den \(12\)’ye sıralar. Kuramsal olasılıkları aynı indeksle bir Series olarak kurarız ki DataFrame’de satırlar etiketlerine göre hizalansın.
import numpy as np
import pandas as pd
rng = np.random.default_rng(12)
dice = rng.integers(1, 7, size=(36000, 2))
total = pd.Series(dice.sum(axis=1))
freq = total.value_counts(normalize=True).sort_index()
s = freq.index.to_numpy()
exact = pd.Series((6 - np.abs(s - 7)) / 36, index=freq.index)
table = pd.DataFrame({"goreli": freq, "kuramsal": exact})
table["fark"] = table["goreli"] - table["kuramsal"]
print(table.round(4))
print(round(table["fark"].abs().max(), 4))Çıktı:
goreli kuramsal fark
2 0.0259 0.0278 -0.0019
3 0.0566 0.0556 0.0010
4 0.0826 0.0833 -0.0007
5 0.1130 0.1111 0.0019
6 0.1379 0.1389 -0.0010
7 0.1668 0.1667 0.0002
8 0.1402 0.1389 0.0013
9 0.1127 0.1111 0.0016
10 0.0816 0.0833 -0.0018
11 0.0569 0.0556 0.0014
12 0.0258 0.0278 -0.0020
0.002
Göreli frekanslar kuramsal olasılıklardan en çok \(0{,}002\) uzaktadır. Örneğin \(P(S = 7) = 6/36 \approx 0{,}1667\), gözlenen oran \(0{,}1668\)’dir. 36000 atışta bir oranın standart sapması en çok \(\sqrt{0{,}25/36000} \approx 0{,}0026\) olduğundan farklar beklenen büyüklüktedir. \(\blacksquare\)
Alıştırma 15.2 (Collatz Adım Sayıları) Kontrol Yapıları ve Fonksiyonlar bölümündeki Collatz dizisinde \(n\)’den \(1\)’e ulaşmak için gereken adım sayısını \(n = 1, \ldots, 9999\) için indeksi \(n\) olan bir Series’te toplayın ve en çok adım gerektiren üç sayıyı bulun.
Çözüm
Adım sayısını bir fonksiyonla hesaplayıp sonuçları bir liste comprehension ile toplarız. index=n ile etiketler \(1, \ldots, 9999\) olur; böylece idxmax ve nlargest doğrudan sayıları verir.
import pandas as pd
def collatz_steps(n):
steps = 0
while n != 1:
n = n // 2 if n % 2 == 0 else 3 * n + 1
steps += 1
return steps
n = range(1, 10**4)
steps = pd.Series([collatz_steps(k) for k in n], index=n)
print(steps.idxmax(), steps.max())
print(steps.nlargest(3))
print(round(steps.mean(), 2), steps.median())Çıktı:
6171 261
6171 261
9257 259
6943 256
dtype: int64
84.97 73.0
En çok adımı \(6171\) gerektirir: \(261\) adım. Onu \(9257\) (\(259\) adım) ve \(6943\) (\(256\) adım) izler. Ortalama adım sayısı yaklaşık \(85\), medyan \(73\)’tür; ortalamanın medyandan büyük olması, az sayıdaki çok uzun dizinin dağılımı sağa çektiğini gösterir. \(\blacksquare\)
Alıştırma 15.3 (Ankara’da Sıcak Temmuz Günleri) olcumler.csv dosyasında Ankara’da temmuz ayında sıcaklığın \(28\) °C’yi aştığı günleri bulun.
Çözüm
Koşulları art arda uygularız: önce Ankara satırları, sonra bunların arasından ay numarası 7 olanlar, en son sıcaklığı \(28\)’i aşanlar. count() temmuzda eksik ölçüm olup olmadığını da gösterir.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
ank = df[df["istasyon"] == "Ankara"]
july = ank[ank["tarih"].dt.month == 7]
hot = july[july["sicaklik"] > 28]
print(len(july), july["sicaklik"].count(), len(hot))
print(hot["tarih"].dt.day.tolist())
print(hot["sicaklik"].tolist())Çıktı:
31 31 3
[9, 14, 23]
[31.0, 28.1, 31.5]
Temmuzun 31 gününün hepsinde ölçüm vardır. Sıcaklık yalnız 3 günde \(28\) °C’yi aşmıştır: 9 Temmuz (\(31{,}0\)), 14 Temmuz (\(28{,}1\)) ve 23 Temmuz (\(31{,}5\)). Modelde temmuz ortası sıcaklığı \(12 + 11{,}5 = 23{,}5\) °C olduğundan \(28\) °C, ortalamanın \(1{,}8\) standart sapma üstündedir. \(\blacksquare\)
Alıştırma 15.4 (Doğrusal, Karesel ve Kübik İnterpolasyon) \([0, 2]\) aralığında \(0{,}2\) adımlı ızgaradaki \(e^x\) değerlerinden 3, 4 ve 8 numaralı konumdakileri silin. Eksikleri interpolate yönteminin "linear", "quadratic" ve "cubic" seçenekleriyle doldurup hataları karşılaştırın.
Çözüm
"quadratic" ve "cubic" seçenekleri SciPy’ın ikinci ve üçüncü dereceden spline interpolasyonunu kullanır ve indeksin değerlerini \(x\) koordinatı olarak alır. Spline’ları Polinomlar, İnterpolasyon ve Eğri Uydurma bölümünde ayrıntılı ele alıyoruz. "linear" değerleri eşit aralıklı sayar; bizim ızgaramız eşit aralıklı olduğu için bu bir şey değiştirmez.
import numpy as np
import pandas as pd
x = np.linspace(0, 2, 11)
s = pd.Series(np.exp(x), index=x)
s.iloc[[3, 4, 8]] = np.nan
true = np.exp(x[[3, 4, 8]])
for method in ["linear", "quadratic", "cubic"]:
filled = s.interpolate(method=method)
err = np.abs(filled.iloc[[3, 4, 8]].to_numpy() - true)
print(f"{method:9}", err.round(5), f"{err.max():.1e}")Çıktı:
linear [0.07852 0.08392 0.09939] 9.9e-02
quadratic [0.00103 0.00332 0.00199] 3.3e-03
cubic [0.00086 0.00087 0.0009 ] 9.0e-04
Doğrusal interpolasyonun en büyük hatası \(0{,}099\), karesel spline’ınki \(0{,}0033\), kübik spline’ınki \(0{,}0009\)’dur. \(e^x\) dışbükey olduğundan doğru parçaları eğrinin hep üstünde kalır ve hata \(f''\) ile orantılıdır; yüksek dereceli spline’lar eğriliği de izlediği için çok daha isabetlidir. \(\blacksquare\)
Alıştırma 15.5 (Çeyrekler Arası Açıklık Kuralıyla Aykırı Değerler) Bir veride çeyrekler arası açıklık \(\text{ÇAA} = Q_3 - Q_1\) olmak üzere
\[ \big[\,Q_1 - 1{,}5\,\text{ÇAA},\ \ Q_3 + 1{,}5\,\text{ÇAA}\,\big] \]
aralığının dışındaki gözlemlere aykırı değer diyelim. Ankara’nın yağışlı günlerindeki yağış miktarlarında doğrusal yüzdelik kuralına göre kaç aykırı değer vardır?
Çözüm
Yağışlı Ankara günlerini iki koşulla süzeriz. quantile([0.25, 0.75]) iki değerli bir Series döndürür; onu iki değişkene açabiliriz. Alt sınır negatif çıkacağından (yağış negatif olamaz) aykırı değerlerin hepsi üst taraftadır.
import numpy as np
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
wet = (df["istasyon"] == "Ankara") & (df["yagis"] > 0)
rain = df.loc[wet, "yagis"]
q1, q3 = rain.quantile([0.25, 0.75])
iqr = q3 - q1
low, high = q1 - 1.5 * iqr, q3 + 1.5 * iqr
out = rain[(rain < low) | (rain > high)]
print(len(rain), q1, q3, high)
print(len(out), round(len(out) / len(rain), 3))
print(round(np.exp(-(np.log(4) + 1.5 * np.log(3))), 3))Çıktı:
88 1.275 5.4 11.5875
3 0.034
0.048
88 yağışlı günde \(Q_1 = 1{,}275\) mm, \(Q_3 = 5{,}4\) mm, çeyrekler arası açıklık \(4{,}125\) mm ve üst sınır \(11{,}5875\) mm’dir. Bu sınırı 3 gün, yani günlerin \(\%3{,}4\)’ü aşmıştır. Veride hiçbir hata olmadığı hâlde kural birkaç günü aykırı saydı. Bunun nedeni dağılımın biçimidir: ortalaması \(r\) olan üstel dağılımda \(Q_1 = r\ln(4/3)\) ve \(Q_3 = r\ln 4\) olduğundan üst sınır \(r(\ln 4 + 1{,}5\ln 3)\)’tür ve bir gözlemin bu sınırı aşma olasılığı
\[ e^{-(\ln 4 + 1{,}5\ln 3)} = \frac{1}{4 \cdot 3^{1{,}5}} \approx 0{,}048 \]
olur. Çarpık dağılımlarda bu kural hatalı ölçümlerden çok dağılımın uzun kuyruğunu yakalar. \(\blacksquare\)
Alıştırma 15.6 (Grup İçinde Standartlaştırma) Her sıcaklık ölçümünü kendi istasyonunun ortalaması \(\bar x_g\) ve standart sapması \(s_g\) ile \(z = (x - \bar x_g)/s_g\) biçiminde standartlaştırın ve mutlak değerce en büyük \(z\) değerinin hangi ölçüme ait olduğunu bulun.
Çözüm
Vektörizasyon ve Broadcasting bölümünde bir veri matrisini sütun sütun standartlaştırmıştık; burada sütunların yerini istasyonlar alır. transform("mean") ve transform("std") her satıra kendi istasyonunun ortalamasını ve standart sapmasını yazar, böylece formül bütün sütuna tek ifadeyle uygulanır. Doğrulama için her istasyonda \(z\)’nin ortalamasına ve standart sapmasına da bakarız.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
g = df.groupby("istasyon")["sicaklik"]
df["z"] = (df["sicaklik"] - g.transform("mean")) / g.transform("std")
check = df.groupby("istasyon")["z"].agg(["mean", "std"])
print(check.abs().round(10))
row = df.loc[df["z"].abs().idxmax()]
print(row["tarih"].date(), row["istasyon"], row["sicaklik"],
round(row["z"], 3))Çıktı:
mean std
istasyon
Ankara 0.0 1.0
Erzurum 0.0 1.0
İzmir 0.0 1.0
2025-07-23 Ankara 31.5 2.304
Standartlaştırılmış değerlerin ortalaması her istasyonda \(0\), standart sapması \(1\)’dir (yuvarlama hataları \(10^{-10}\)’un altında). Mutlak değerce en büyük \(z\) değeri \(2{,}304\)’tür ve Ankara’nın 23 Temmuz’daki \(31{,}5\) °C ölçümüne aittir. Bir kosinüs dalgası ile küçük bir gürültünün toplamında \(|z|\) değerleri \(3\)’e pek yaklaşmaz, çünkü \(a\cos\theta\) dalgasının tepesi ortalamasından yalnız \(\sqrt 2\) standart sapma uzaktadır. \(\blacksquare\)
Alıştırma 15.7 (Yağışlı Günlerin Oranı ve Ortalaması) Her istasyon için yağışlı günlerin yıl içindeki oranını ve yağışlı günlerdeki ortalama yağışı hesaplayıp veri üretiminde kullanılan \(p\) ve \(r\) değerleriyle karşılaştırın.
Çözüm
Önce yağışlı günleri süzer, sonra istasyona göre gruplayıp agg ile gün sayısını, ortalamayı ve en büyük değeri alırız. Oran, gün sayısının 365’e bölümüdür.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
wet = df[df["yagis"] > 0]
table = wet.groupby("istasyon")["yagis"].agg(["size", "mean", "max"])
table["oran"] = table["size"] / 365
print(table.round(3))Çıktı:
size mean max oran
istasyon
Ankara 88 3.968 33.3 0.241
Erzurum 101 3.700 22.0 0.277
İzmir 55 7.344 33.7 0.151
Oranlar Ankara için \(0{,}241\) (\(p = 0{,}25\)), Erzurum için \(0{,}277\) (\(p = 0{,}30\)), İzmir için \(0{,}151\)’dir (\(p = 0{,}20\)). Yağışlı gün sayısı \(n = 365\) ve \(p\) parametreli binom dağılımına uyar. İzmir için beklenen sayı \(73\) gün, standart sapma \(\sqrt{365 \cdot 0{,}2 \cdot 0{,}8} \approx 7{,}6\) gündür; gözlenen \(55\) gün beklenenin yaklaşık \(2{,}4\) standart sapma altındadır. Bu seyrek ama imkânsız olmayan bir sapmadır. Yağışlı günlerdeki ortalamalar \(3{,}97\), \(3{,}70\) ve \(7{,}34\) mm’dir ve \(r = 4\), \(3{,}5\) ve \(8\) değerlerine yakındır. \(\blacksquare\)
Alıştırma 15.8 (Yalnız Bir Tabloda Geçen Anahtarlar) a tablosunda Ankara, Erzurum ve İzmir’in ortalama sıcaklıkları, b tablosunda Ankara, İzmir, Trabzon ve Van’ın plaka numaraları var. outer birleştirme ve indicator=True parametresiyle yalnız bir tabloda geçen istasyonları bulun.
Çözüm
indicator=True sonuca _merge adlı bir sütun ekler. Bu sütunun değeri, anahtarın iki tabloda da (both), yalnız solda (left_only) ya da yalnız sağda (right_only) bulunduğunu söyler.
import pandas as pd
a = pd.DataFrame({"istasyon": ["Ankara", "Erzurum", "İzmir"],
"sicaklik": [12.14, 5.79, 17.85]})
b = pd.DataFrame({"istasyon": ["Ankara", "İzmir", "Trabzon", "Van"],
"plaka": [6, 35, 61, 65]})
m = a.merge(b, on="istasyon", how="outer", indicator=True)
print(m)
print(m["_merge"].value_counts().to_dict())
print(m.loc[m["_merge"] != "both", "istasyon"].tolist())Çıktı:
istasyon sicaklik plaka _merge
0 Ankara 12.14 6.0 both
1 Erzurum 5.79 NaN left_only
2 Trabzon NaN 61.0 right_only
3 Van NaN 65.0 right_only
4 İzmir 17.85 35.0 both
{'right_only': 2, 'both': 2, 'left_only': 1}
['Erzurum', 'Trabzon', 'Van']
Ankara ve İzmir iki tabloda da vardır; Erzurum yalnız solda, Trabzon ve Van yalnız sağdadır. Anahtar kümeleriyle söylersek aradığımız küme simetrik farktır: \((A_L \cup A_R) \setminus (A_L \cap A_R)\). \(\blacksquare\)
Alıştırma 15.9 (Aylık Ortalamaların Yarı Açıklığı) pivot_table ile kurduğunuz ay ve istasyon tablosundan her istasyonun aylık ortalamalarının yarı açıklığını, yani en büyük ile en küçük aylık ortalamanın farkının yarısını hesaplayıp modeldeki \(a\) genliğiyle karşılaştırın.
Çözüm
Tablonun her sütunu bir istasyondur. idxmax() ve idxmin() her sütunda en büyük ve en küçük değerin etiketini, yani en sıcak ve en soğuk ayın numarasını verir; max() - min() de sütun sütun hesaplanır.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
df["ay"] = df["tarih"].dt.month
pt = df.pivot_table(index="ay", columns="istasyon", values="sicaklik",
aggfunc="mean")
print(pt.idxmax().to_dict(), pt.idxmin().to_dict())
print(((pt.max() - pt.min()) / 2).round(2).to_dict())Çıktı:
{'Ankara': 7, 'Erzurum': 7, 'İzmir': 7} {'Ankara': 1, 'Erzurum': 1, 'İzmir': 1}
{'Ankara': 11.82, 'Erzurum': 13.7, 'İzmir': 8.23}
Üç istasyonda da en sıcak ay temmuz, en soğuk ay ocaktır. Yarı açıklıklar \(11{,}82\), \(13{,}70\) ve \(8{,}23\)’tür; model genlikleri \(11{,}5\), \(14\) ve \(8{,}5\)’tir. Aylık ortalama dalganın tepesini biraz düzleştirir: tepe çevresindeki 31 günlük bir pencerede kosinüs dalgasının ortalaması, tepe değerinin yaklaşık \(0{,}988\) katıdır. Kalan farklar ay ortalamalarına giren gürültüden gelir; Önerme 15.1 gereği bir ay ortalamasındaki gürültünün standart sapması yaklaşık \(2{,}5/\sqrt{30} \approx 0{,}46\)’dır. \(\blacksquare\)
Alıştırma 15.10 (Mevsim Dalgasını En Küçük Karelerle Kestirmek) \(\omega = 2\pi/365\) olmak üzere Ankara’nın eksik olmayan ölçümlerine
\[ T(t) \approx c_0 + c_1\cos\omega t + c_2\sin\omega t \]
modelini en küçük kareler yöntemiyle uydurun ve buradan yıllık ortalama \(m\)’yi, genlik \(a\)’yı ve en soğuk günü veren \(t_0\)’ı kestirin.
Çözüm
Model katsayılarda doğrusaldır; dolayısıyla bu bir doğrusal en küçük kareler problemidir. Sütunları \(1\), \(\cos\omega t\) ve \(\sin\omega t\) olan \(A\) matrisiyle \(Ac \approx y\) sistemini NumPy ile Lineer Cebir bölümündeki np.linalg.lstsq ile çözeriz. Gün numarası \(t\), tarihten 1 Ocak çıkarılarak bulunur. Katsayılardan parametrelere geçmek için
\[ m - a\cos\omega(t - t_0) = m - a\cos\omega t_0\,\cos\omega t - a\sin\omega t_0\,\sin\omega t \]
özdeşliğini kullanırız: \(c_0 = m\), \(c_1 = -a\cos\omega t_0\) ve \(c_2 = -a\sin\omega t_0\). Buradan \(a = \sqrt{c_1^2 + c_2^2}\) ve \(\omega t_0 = \operatorname{atan2}(-c_2, -c_1)\) bulunur.
import numpy as np
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
ank = df[df["istasyon"] == "Ankara"].dropna(subset=["sicaklik"])
t = (ank["tarih"] - pd.Timestamp("2025-01-01")).dt.days.to_numpy()
y = ank["sicaklik"].to_numpy()
w = 2 * np.pi / 365
A = np.column_stack([np.ones(t.size), np.cos(w * t), np.sin(w * t)])
coef, *_ = np.linalg.lstsq(A, y, rcond=None)
c0, c1, c2 = coef
amp = np.hypot(c1, c2)
t0 = np.arctan2(-c2, -c1) / w
resid = y - A @ coef
print(len(t), np.round(coef, 3))
print(round(c0, 2), round(amp, 2), round(t0, 1))
print(round(resid.std(ddof=3), 3))Çıktı:
357 [ 12.005 -10.924 -2.94 ]
12.01 11.31 15.3
2.65
357 ölçümden \(m \approx 12{,}01\), \(a \approx 11{,}31\) ve \(t_0 \approx 15{,}3\) gün kestirilir; gerçek değerler \(12\), \(11{,}5\) ve \(15\)’tir. Artıkların standart sapması \(2{,}65\)’tir ve gürültünün \(2{,}5\)’lik standart sapmasına yakındır; ddof=3, üç parametre kestirdiğimiz için paydayı \(n - 3\) yapar. \(\blacksquare\)
Alıştırma 15.11 (Mevsim Etkisi Çıkarılınca Korelasyon) İstasyonların sıcaklıkları arasındaki korelasyonu, her istasyonun 31 günlük merkezî hareketli ortalamasını çıkardıktan sonra yeniden hesaplayın.
Çözüm
pivot ile geniş tabloyu kurarız. DataFrame’in interpolate ve rolling yöntemleri her sütuna ayrı ayrı uygulanır; böylece üç istasyonun hareketli ortalaması tek satırda bulunur. Ölçülen değerden hareketli ortalamayı çıkarınca mevsim dalgası gider, gürültü kalır. Eksik günler ile baştaki ve sondaki 15’er gün NaN olur ve corr bunları atar; count() her sütunda kullanılabilen değer sayısını gösterir.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
wide = df.pivot(index="tarih", columns="istasyon", values="sicaklik")
trend = wide.interpolate().rolling(31, center=True).mean()
anom = wide - trend
print(wide.corr().round(3))
print(anom.corr().round(3))
print(anom.count().to_dict())Çıktı:
istasyon Ankara Erzurum İzmir
istasyon
Ankara 1.000 0.920 0.876
Erzurum 0.920 1.000 0.901
İzmir 0.876 0.901 1.000
istasyon Ankara Erzurum İzmir
istasyon
Ankara 1.000 0.057 -0.062
Erzurum 0.057 1.000 -0.016
İzmir -0.062 -0.016 1.000
{'Ankara': 330, 'Erzurum': 322, 'İzmir': 330}
Ham sıcaklıklar arasındaki \(0{,}88\)–\(0{,}92\) korelasyonlar, mevsim etkisi çıkarılınca \(-0{,}06\) ile \(0{,}06\) arasına iner. Modelde istasyonların günlük gürültüleri bağımsızdır; yüksek korelasyonun tek kaynağı ortak mevsim dalgasıydı. Gerçek verilerde komşu istasyonların sapmaları da birlikte hareket eder, çünkü aynı hava sistemlerinden etkilenirler; ama o ilişkiyi görmek için de önce ortak eğilimi çıkarmak gerekir. Korelasyonun \(0\) olmasının bağımsızlık anlamına gelmediğini Olasılık Teorisi notlarındaki örnek gösterir. \(\blacksquare\)
Alıştırma 15.12 (Yıllık Yağışın Yarısı) İzmir’de 2025 yılı boyunca düşen toplam yağışın yarısı hangi gün tamamlanmıştır?
Çözüm
cumsum() her gün için yılbaşından o güne kadar düşen toplam yağışı verir. cum >= total / 2 boolean serisi o günden itibaren hep True’dur. Boolean bir serinin idxmax() değeri ilk True değerin etiketi olduğundan aradığımız günü verir. Son satır, aylık toplamların en büyüğünün hangi ayda olduğunu da yazdırır.
import pandas as pd
df = pd.read_csv("olcumler.csv", parse_dates=["tarih"])
izm = df[df["istasyon"] == "İzmir"].set_index("tarih")["yagis"]
cum = izm.cumsum()
total = cum.iloc[-1]
reached = cum >= total / 2
day = reached.idxmax() # ilk True değerin etiketi
print(round(total, 1), day.date(), round(cum.loc[day], 1))
print(izm.resample("ME").sum().idxmax().month)Çıktı:
403.9 2025-06-06 203.3
6
Yıllık toplam \(403{,}9\) mm’dir ve yarısı (\(201{,}95\) mm) 6 Haziran’da aşılmıştır; o günün sonunda birikimli toplam \(203{,}3\) mm’dir. Yılın ortası 2 Temmuz olduğu hâlde yarıya daha erken ulaşıldı: yılın en yağışlı günü (\(33{,}7\) mm) 6 Haziran’dır ve en yağışlı ay da hazirandır. Modelde yağış olasılığı mevsime bağlı olmadığından bu erkenlik yalnız bir rastlantıdır. \(\blacksquare\)
Bu bölümde pandas’ın iki temel yapısını, Series ve DataFrame’i tanıdık. Etiketlerin işlemlerde nasıl hizalandığını, CSV dosyalarını okuyup yazmayı, loc ve iloc ile seçimi, eksik değerleri, betimsel istatistikleri, groupby ile ayır, uygula, birleştir düzenini, merge ile tablo birleştirmeyi ve zaman serilerini gördük. Bu, dersin son bölümüdür: Python’ın temel yapılarından başlayıp NumPy, Matplotlib, SymPy ve SciPy’dan geçerek bir veri tablosunun analizine geldik. Burada hesapladığımız ortalama, varyans, korelasyon ve yüzdeliklerin arkasındaki kuram için Matematiksel İstatistik notlarına geçebilir, dersin genel planı için Müfredat ve Giriş sayfasına dönebilirsiniz.