Ana içeriğe geç
Kesit Python
Orta · Veri analizine giriş

matplotlib ile görselleştirme

Figure/Axes modeli; çizgi, çubuk, histogram ve dağılım grafikleri; başlık, eksen, legend; savefig ile dosyaya yazma; pandas .plot kısayolu.

Uygulamada aç, 27 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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.

Bu ortamdaTarayıcıdaki Python (Pyodide) görüntü penceresi açamaz; bu yüzden örneklerde 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()))
savefig ve closeHer figürü dosyaya yazdıktan sonra 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

SoruGrafikmatplotlibpandas
Zaman içinde nasıl değişti?Çizgiax.plots.plot()
Kategoriler arası karşılaştırmaÇubukax.bar / ax.barhs.plot.bar()
Bir değişkenin dağılımıHistogramax.hists.plot.hist()
İki değişken ilişkisiDağılımax.scatterdf.plot.scatter(x, y)
Parça-bütün (az kategori)Pasta (dikkatli)ax.pies.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

Pasta grafiğiDörtten fazla dilimde insan gözü oranları karşılaştıramaz; sıralı yatay çubuk neredeyse her zaman daha iyidir.
Derinleş

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
BelirtiNedenÇözüm
plt.show() hiçbir şey göstermiyorGörüntüsüz arka uç (Agg) seçili ya da sunucuda çalışıyorsunDosyaya 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ıyorHer turda plt.close(fig)
Eksen etiketleri kesikUzun şehir adları tuvale sığmıyorfig.tight_layout() ya da bbox_inches="tight"
Sunucuda (Linux, ekransız) TclError: no displayPencereli 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)
Stil bir kere tanımlanırKurumsal renkleri ve yazı boyutlarını 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.
pyplot ile Axes'i karıştırmaplt.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.
Gerçek işteAnalist ekiplerinde haftalık raporlar çoğunlukla bir betikle üretilir: veri çekilir, her grafik için ayrı bir fonksiyon PNG ya da base64 üretir, bir HTML şablonu bunları birleştirir ve rapor e-postayla gider. Bu düzende en sık iki sorun çıkar: kapatılmayan figürlerin sunucuda belleği doldurması ve şehirler arası grafiklerin ortak eksen kullanmaması yüzünden yanlış karşılaştırma. İkisini de bu bölümdeki alışkanlıklar çözer.
Derinleş

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.

SoruGrafikNeden bu tür?
Gecikmeler nasıl dağılıyor?Histogram + eşik çizgisiTek sayısal değişkenin şekli; çarpıklık ve kuyruk görünür
Hangi neden en çok dakika yiyor?Sıralı yatay çubukKategorik karşılaştırma; uzun etiketler okunur
Dakiklik zamanla nasıl değişiyor?İşaretli çizgi + hedef çizgisiZaman 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)
Grafiği test etmekGrafik üreten fonksiyonları da test edebilirsin: 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.
Gerçek işteOperasyon panolarında "aylık dakiklik + hedef çizgisi" en çok bakılan grafiktir. Ortalama gecikmeyi değil, eşiğin altındaki uçuş yüzdesini çiz: yönetim hedefi (ör. %80) bu birimle koyar. Kötüleşen bir trendi gördüğünde bir sonraki grafik her zaman kırılımdır: hangi rota, hangi saat, hangi neden?
Uzmanlaş

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.

Alıştırmalara geç (27)