Tablo mu grafik mi?
Anadolu Market'in haftalık ciro tablosunu yöneticiye gönderirsen 12 sayıya bakar; aynı veriyi çizgi grafik yaparsan düşüşü saniyede görür. Grafik bir bulgu iletişim aracıdır. Python'da temel kütüphane matplotlib; pandas'ın .plot() kısayolu da altında onu kullanır.
matplotlib.use("Agg") ile görüntüsüz (headless) arka uç kullanıyor, figürü dosyaya yazıyor ve grafiğin yapısını (başlık, çizgi sayısı, çubuk sayısı) print ile gösteriyoruz. Kendi bilgisayarında aynı kod plt.show() ile pencere açar.Figure ve Axes
Bir Figure tuvaldir; içinde bir ya da daha çok Axes (grafik alanı) bulunur. Modern kullanım: fig, ax = plt.subplots() ile ikisini al, çizimi ax üzerinden yap. plt.plot() gibi "durum makinesi" çağrıları da çalışır ama birden çok grafikte karışır.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
haftalar = ["H1", "H2", "H3", "H4", "H5", "H6"]
ciro = [1213, 1480, 1390, 1725, 1610, 1950]
hedef = [1300, 1400, 1500, 1600, 1700, 1800]
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(haftalar, ciro, marker="o", label="Ciro")
ax.plot(haftalar, hedef, linestyle="--", color="gray", label="Hedef")
ax.set_title("Anadolu Market haftalık ciro")
ax.set_xlabel("Hafta")
ax.set_ylabel("TL")
ax.legend()
ax.grid(True, alpha=0.3)
fig.savefig("ciro.png", dpi=120, bbox_inches="tight")
plt.close(fig)
print("başlık:", ax.get_title())
print("çizgi sayısı:", len(ax.lines))
print("ilk çizginin y verisi:", list(ax.lines[0].get_ydata()))
plt.close(fig) çağır. Döngüde yüz grafik üretirken kapatmazsan bellek şişer ve matplotlib uyarı verir.Grafik türü seçimi
| Soru | Grafik | matplotlib | pandas |
|---|---|---|---|
| Zaman içinde nasıl değişti? | Çizgi | ax.plot | s.plot() |
| Kategoriler arası karşılaştırma | Çubuk | ax.bar / ax.barh | s.plot.bar() |
| Bir değişkenin dağılımı | Histogram | ax.hist | s.plot.hist() |
| İki değişken ilişkisi | Dağılım | ax.scatter | df.plot.scatter(x, y) |
| Parça-bütün (az kategori) | Pasta (dikkatli) | ax.pie | s.plot.pie() |
Çubuk ve histogram
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
sehirler = ["İstanbul", "Ankara", "İzmir", "Bursa"]
ciro = [4210, 2980, 2450, 1120]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3.5))
cubuklar = ax1.bar(sehirler, ciro, color="steelblue")
ax1.set_title("Şehir bazında ciro")
ax1.bar_label(cubuklar, fmt="%d") # çubuk üstüne değer
ax1.tick_params(axis="x", rotation=20)
tutarlar = [45, 120, 80, 95, 310, 60, 75, 899, 130, 55, 210, 48, 1250, 90, 70]
ax2.hist(tutarlar, bins=6, color="darkorange", edgecolor="white")
ax2.set_title("Sipariş tutarı dağılımı")
ax2.set_xlabel("TL")
fig.tight_layout()
fig.savefig("sehir_hist.png")
plt.close(fig)
print("çubuk sayısı:", len(ax1.patches), "histogram kutusu:", len(ax2.patches))
print("en yüksek çubuk:", max(p.get_height() for p in ax1.patches))
ax.patches çubukları ve histogram kutularını dikdörtgen nesneler olarak tutar; get_height() yüksekliğini verir. Testlerde grafiğin doğru veriyle çizildiğini bu şekilde kontrol ederiz.
Dağılım grafiği ve pandas .plot
Bir DataFrame'den hızlı keşif grafiği için df.plot(...): döndürdüğü nesne yine bir Axes'tir, üstüne matplotlib ile başlık ve etiket ekleyebilirsin. ax= parametresiyle kendi Axes'ine çizdirirsin.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"sehir": ["İstanbul", "Ankara", "İzmir", "Bursa", "Antalya"],
"musteri": [820, 540, 460, 210, 330],
"ciro": [4210, 2980, 2450, 1120, 1890],
})
fig, ax = plt.subplots(figsize=(5, 4))
df.plot.scatter(x="musteri", y="ciro", ax=ax, s=60, color="seagreen")
for _, r in df.iterrows():
ax.annotate(r["sehir"], (r["musteri"], r["ciro"]), textcoords="offset points", xytext=(5, 5))
ax.set_title("Müşteri sayısı ve ciro")
fig.savefig("scatter.png")
plt.close(fig)
print("başlık:", ax.get_title(), "| nokta koleksiyonu:", len(ax.collections))
ax2 = df.set_index("sehir")["ciro"].sort_values().plot.barh(title="Ciro (TL)")
print("barh çubuk sayısı:", len(ax2.patches))
plt.close("all")
Okunur grafik için kontrol listesi
- Başlık bulguyu söylesin: "Ciro" değil, "Haftalık ciro hedefin altında kaldı".
- Eksen etiketleri birim içersin (TL, adet, %).
- Birden çok seri varsa legend; tek seriyse legend gereksiz.
- Çubuk grafiği sıralı çiz (
sort_values), y ekseni sıfırdan başlasın. - Renk az ve anlamlı olsun: vurgulanacak çubuk farklı, gerisi gri.
fig.tight_layout()ya dabbox_inches="tight"ile kesilen etiketleri düzelt.
Derinleş: kurulum, çoklu grafik, vurgu ve rapora gömme
Tek bir çizgi, çubuk ya da histogram çizebiliyorsun. Gerçek bir yönetim raporunda ise daha fazlası istenir: aynı figürde birden çok grafik, iki farklı birimi (TL ve adet) aynı grafikte göstermek, hedef çizgisi ve not eklemek, eksende "1.250.000 TL" gibi Türkçe sayı biçimi, yığılmış çubuk ve grafiği bir HTML rapora gömmek. Bu bölüm bunları tamamlıyor. İstatistiksel grafikler (seaborn) ve etkileşimli grafikler (plotly) "İleri görselleştirme" dersinde.
Kurulum ve arka uç (backend)
python -m pip install matplotlib python -c "import matplotlib; print(matplotlib.__version__, matplotlib.get_backend())" # Jupyter'de grafik hücre altında görünsün: %matplotlib inline
| Belirti | Neden | Çözüm |
|---|---|---|
plt.show() hiçbir şey göstermiyor | Görüntüsüz arka uç (Agg) seçili ya da sunucuda çalışıyorsun | Dosyaya kaydet (fig.savefig); masaüstünde matplotlib.use("TkAgg") |
| Kaydedilen PNG boş | savefig, plt.show()'dan sonra çağrılmış; show figürü kapatır | Önce savefig, sonra show |
More than 20 figures have been opened uyarısı | Döngüde figürler kapatılmıyor | Her turda plt.close(fig) |
| Eksen etiketleri kesik | Uzun şehir adları tuvale sığmıyor | fig.tight_layout() ya da bbox_inches="tight" |
Sunucuda (Linux, ekransız) TclError: no display | Pencereli arka uç seçilmiş | pyplot'u içe aktarmadan önce matplotlib.use("Agg") |
Izgara düzeni: subplots(satır, sütun)
plt.subplots(2, 2) bir figür ve 2×2 numpy dizisi halinde Axes döndürür; axes[0, 1] sağ üst grafiktir. Hepsini dolaşmak için axes.flat kullanılır. sharey=True tüm grafiklerin y eksenini ortak yapar; şehirleri karşılaştırırken bu şarttır, yoksa her grafik kendi ölçeğini seçer ve küçük şehir büyük görünür. Kullanılmayan alanı ax.remove() kaldırır.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
satis = {"İstanbul": [420, 450, 480], "Ankara": [300, 280, 310], "İzmir": [250, 260, 300]}
fig, axes = plt.subplots(2, 2, figsize=(8, 6), sharey=True)
print(type(axes).__name__, axes.shape)
for ax, (sehir, degerler) in zip(axes.flat, satis.items()):
ax.plot(["Oca", "Şub", "Mar"], degerler, marker="o")
ax.set_title(sehir)
axes[1, 1].remove() # 4. alan boş kalmasın
fig.suptitle("Şehir bazında aylık satış")
fig.tight_layout()
print("Axes sayısı:", len(fig.axes))
print("ortak y sınırı:", axes[0, 0].get_ylim() == axes[1, 0].get_ylim())
plt.close(fig)
İki birim tek grafikte: twinx
Ciro (TL) ve sipariş adedi aynı eksende çizilirse adet çizgisi sıfıra yapışır. ax.twinx() aynı x eksenini paylaşan, sağda kendi y ekseni olan ikinci bir Axes verir. Okuyucunun hangi serinin hangi eksene ait olduğunu anlaması için eksen etiketlerini serinin rengiyle boyamak iyi bir alışkanlıktır.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
aylar = ["Oca", "Şub", "Mar", "Nis"]
ciro = [125000, 98000, 143000, 151000]
siparis = [410, 350, 470, 455]
fig, ax = plt.subplots(figsize=(7, 4))
ax.bar(aylar, ciro, color="lightsteelblue")
ax.set_ylabel("Ciro (TL)")
ax2 = ax.twinx()
ax2.plot(aylar, siparis, color="darkred", marker="o")
ax2.set_ylabel("Sipariş (adet)", color="darkred")
print("sol çubuk:", len(ax.patches), "| sağ çizgi:", len(ax2.lines))
print(ax.get_ylabel(), "/", ax2.get_ylabel())
plt.close(fig)
Vurgu: hedef çizgisi, bölge ve not
Grafiğin mesajını okuyucuya söyletmek için üç araç yeter: ax.axhline(y) yatay referans çizgisi (hedef, ortalama), ax.axvspan(x1, x2) bir aralığı gölgeler (kampanya haftaları), ax.annotate(metin, xy=..., xytext=..., arrowprops=...) bir noktaya oklu not koyar. Vurgulanacak çubuğu farklı renkle boyamak için renk listesini koşulla kur.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
haftalar = ["H1", "H2", "H3", "H4", "H5"]
ciro = [1210, 1480, 990, 1725, 1610]
hedef = 1300
fig, ax = plt.subplots()
renk = ["firebrick" if c < hedef else "lightgray" for c in ciro]
ax.bar(haftalar, ciro, color=renk)
ax.axhline(hedef, linestyle="--", color="black", label="Hedef")
ax.axvspan(2.5, 4.5, alpha=0.15, color="green") # kampanya haftaları
ax.annotate("Stok sorunu", xy=(2, 990), xytext=(2.2, 1550),
arrowprops={"arrowstyle": "->"})
ax.set_title("İki hafta hedefin altında kaldı")
print("hedef altı hafta:", sum(c < hedef for c in ciro))
print("çizgi:", len(ax.lines), "| not:", len(ax.texts))
plt.close(fig)
Türkçe sayı biçimi ve yığılmış çubuk
matplotlib eksende 1e6 gibi bilimsel gösterim ya da 1,250,000 gibi İngilizce ayraç kullanır. matplotlib.ticker.FuncFormatter her eksen değerini senin fonksiyonundan geçirir; fonksiyon (değer, konum) alır ve metin döndürür. Yığılmış (stacked) çubukta her kategori, bir önceki katmanların toplamı üzerine bottom= parametresiyle oturtulur; pandas'ta aynı iş pivot.plot.bar(stacked=True) ile yapılır.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
import numpy as np
def tl(x, pos):
return f"{x:,.0f} TL".replace(",", ".")
sehirler = ["İstanbul", "Ankara", "İzmir"]
katmanlar = {"Gıda": [500000, 320000, 210000], "Elektronik": [900000, 410000, 380000]}
fig, ax = plt.subplots()
alt = np.zeros(len(sehirler))
for kategori, degerler in katmanlar.items():
ax.bar(sehirler, degerler, bottom=alt, label=kategori)
alt += degerler
ax.yaxis.set_major_formatter(FuncFormatter(tl))
ax.legend()
print(tl(1250000, 0))
print("çubuk parçası:", len(ax.patches), "| İstanbul toplam:", int(alt[0]))
plt.close(fig)
Dışa aktarma ve HTML rapora gömme
savefig uzantıya göre biçim seçer: PNG (ekran, e-posta; dpi=150 baskıda net), SVG ve PDF (vektör, büyütünce bozulmaz). Grafiği dosya yerine belleğe yazmak için io.BytesIO kullanılır; base64 ile metne çevrilen PNG, tek dosyalık bir HTML rapora <img src="data:image/png;base64,..."> olarak gömülür. E-posta ile giden otomatik raporlar genellikle böyle üretilir.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import io, base64
fig, ax = plt.subplots(figsize=(4, 3))
ax.bar(["A", "B"], [3, 5])
tampon = io.BytesIO()
fig.savefig(tampon, format="png", dpi=100, bbox_inches="tight")
plt.close(fig)
ham = tampon.getvalue()
print("PNG imzası:", ham[:4] == b"\x89PNG")
kod = base64.b64encode(ham).decode("ascii")
html = f'<img alt="Ciro" src="data:image/png;base64,{kod[:20]}...">'
print(html)
plt.rcParams.update({"axes.spines.top": False, "axes.spines.right": False, "font.size": 10}) ile betiğin başında bir kez ayarla. Üst ve sağ çerçeveyi kaldırmak grafiği hemen sadeleştirir.plt.title(), plt.xlabel() "geçerli" Axes'e, yani en son oluşturulana yazar. subplots(1, 2) sonrası plt.title sağdaki grafiğe gider. Birden çok grafikte daima ax.set_title kullan.Derinleş: gerçek veriyle dakiklik panosu
Grafik kurallarını küçük listelerle öğrendin; şimdi bunları gerçek bir operasyon sorusuna uygulayalım. Kanat Havayolları'nın Eylül–Aralık 2024 arasındaki 3.173 uçuşu (kanat_havayollari/hv_flights) ve 1.350 rötar nedeni kaydı (hv_delays) elimizde. Operasyon direktörü üç soru soruyor: Gecikmeler nasıl dağılıyor? En çok dakikayı hangi neden yiyor? Dakiklik (on-time performance) zamanla kötüleşiyor mu? Her soru için doğru grafik türünü seçip okunur hale getireceğiz.
| Soru | Grafik | Neden bu tür? |
|---|---|---|
| Gecikmeler nasıl dağılıyor? | Histogram + eşik çizgisi | Tek sayısal değişkenin şekli; çarpıklık ve kuyruk görünür |
| Hangi neden en çok dakika yiyor? | Sıralı yatay çubuk | Kategorik karşılaştırma; uzun etiketler okunur |
| Dakiklik zamanla nasıl değişiyor? | İşaretli çizgi + hedef çizgisi | Zaman ekseni; hedefe uzaklık tek bakışta |
| Hangi rota sorunlu? | Küçük çoklu grafik (aynı y ölçeği) | Birden çok seriyi karmaşaya boğmadan kıyaslar |
Önce sayıyı üret
Kalkış gecikmesi = gerçek kalkış − planlanan kalkış (dakika). İptal edilen uçuşlarda gerçek kalkış yoktur; onları dağılımdan çıkar ama ayrı bir sayı olarak raporla. Havacılıkta yaygın tanım: 15 dakikadan az gecikme "zamanında" sayılır.
import veri
f = veri.oku("kanat_havayollari", "hv_flights", parse_dates=["sched_dep", "actual_dep"])
print(f["status"].value_counts().to_dict())
u = f[f["status"] != "cancelled"].copy()
u["gecikme"] = (u["actual_dep"] - u["sched_dep"]).dt.total_seconds() / 60
print(u["gecikme"].describe().round(1).to_dict())
print("zamanında (%):", round((u["gecikme"] < 15).mean() * 100, 1))
Ortalama gecikme 25 dakika ama medyan 7 dakika: dağılım sağa çarpık, birkaç uzun rötar ortalamayı yukarı çekiyor. Bu, histogramı tek başına gösterilmesi gereken bir dağılım yapar.
Çarpık dağılım: kuyruğu kırp, eşiği çiz
Uzun kuyruk (289 dakikaya kadar) eksenini genişletir ve asıl kitleyi sola sıkıştırır. Değerleri 180'de clip ile kırpıp son kutuyu "180+" diye açıkla; 15 dakika eşiğini dikey çizgiyle göster. Kutu sınırlarını 15'in katı seçmek, eşiğin bir kutu sınırına denk gelmesini sağlar.
import veri
import matplotlib.pyplot as plt
f = veri.oku("kanat_havayollari", "hv_flights", parse_dates=["sched_dep", "actual_dep"])
u = f[f["status"] != "cancelled"]
gecikme = (u["actual_dep"] - u["sched_dep"]).dt.total_seconds() / 60
fig, ax = plt.subplots(figsize=(8, 4))
sayilar, kenarlar, _ = ax.hist(gecikme.clip(upper=180), bins=range(-15, 181, 15), color="#6b7c93")
ax.axvline(15, color="#c0392b", linestyle="--", label="15 dk eşiği")
ax.set_title("Kalkış gecikmesi dağılımı (180+ son kutuda)")
ax.set_xlabel("Dakika")
ax.set_ylabel("Uçuş sayısı")
ax.legend()
print(sayilar[:4].astype(int).tolist(), "...", int(sayilar[-1]))
plt.close(fig)
Nedenler: sıralı yatay çubuk
Kategorik ve uzun adlı nedenleri karşılaştırmak için yatay çubuk en okunur türdür. Çubukları değere göre sırala; matplotlib yatay çubukta ilk değeri en alta koyduğu için artan sıralarsan en büyük neden en üstte görünür. Pasta grafik beş kategoride bile farkları okutmaz.
import veri
import matplotlib.pyplot as plt
d = veri.oku("kanat_havayollari", "hv_delays")
toplam = d.groupby("reason")["minutes"].sum().sort_values()
pay = (toplam / toplam.sum() * 100).round(1)
fig, ax = plt.subplots(figsize=(7, 3.5))
ax.barh(toplam.index, toplam.values, color="#6b7c93")
for y, (deger, yuzde) in enumerate(zip(toplam.values, pay.values)):
ax.text(deger, y, f" %{yuzde}", va="center")
ax.set_title("Rötar dakikası nedene göre")
ax.set_xlabel("Toplam dakika")
print(pay.to_dict())
plt.close(fig)
ax.patches çubukları, ax.lines çizgileri, ax.get_title() başlığı verir. [p.get_height() for p in ax.patches] ile çizilen değerlerin veriyle tuttuğunu denetlersin. Bu dersin alıştırmaları tam olarak böyle denetleniyor; gece çalışan rapor işlerinde de aynı yöntem kullanılır.Rapor üretimini fonksiyonlaştır
Her grafik için def ciz_ciro(df, yol: str) -> None gibi bir fonksiyon yaz: veri alır, figür kurar, dosyaya yazar, kapatır. Raporlama betiği bu fonksiyonları sırayla çağırır ve PNG'leri bir HTML ya da PDF şablonuna gömer. Fonksiyonun test edilebilir olması için ax döndürmek de yaygındır; test başlığı ve çizgi sayısını kontrol eder.
Stil ve tutarlılık
plt.rcParams ya da plt.style.use("ggplot") ile tüm grafiklerde aynı yazı tipi, renk ve ızgara. Kurumsal renkler için bir liste tanımla ve color= ile sırayla kullan. Türkçe karakter sorunu yaşarsan DejaVu Sans gibi Unicode destekli bir yazı tipi seç.
Ötesi
İstatistiksel grafikler için seaborn (matplotlib üstüne), etkileşimli panolar için plotly ya da Power BI. Ama hepsinin temelinde aynı sorular: hangi soru, hangi grafik, hangi vurgu.