Doğru soruya doğru grafik
matplotlib ile görselleştirme dersinde Figure/Axes yapısını, çizgi, çubuk ve histogramı kurdun. Gerçek işte bunun bir üst katı gelir: "Sepet tutarı şehirlere göre nasıl dağılıyor?" sorusu için kutu grafiği, "Hangi değişkenler birlikte hareket ediyor?" için korelasyon ısı haritası, "Her şehirde trend aynı mı?" için küçük çoklu grafikler (small multiples), yöneticinin fareyle üzerine gelip değer okuyacağı etkileşimli bir grafik. Bu işler için Python dünyasında iki yıldız vardır: seaborn (istatistiksel grafik, matplotlib üstüne kurulu) ve plotly (etkileşimli, tarayıcıda çalışan grafik).
Ama araçtan önemlisi karardır: hangi soru, hangi grafik, hangi renk ve hangi eksen. Kötü seçilmiş bir grafik, doğru veriyle yanlış karar aldırır. Bu ders hem araçları hem de bu karar rehberini verir.
seaborn: tidy veri ile tek satırlık grafikler
seaborn'un sihri şudur: veriyi uzun (tidy) biçimde verirsin, sütun adlarını eksenlere ve renk (hue), panel (col, row) gibi görsel kanallara bağlarsın; gruplama, renk paleti ve gösterge (legend) kendiliğinden gelir.
import seaborn as sns import matplotlib.pyplot as plt sns.set_theme(style="whitegrid") # tutarlı tema # df: siparis_id, sehir, kategori, adet, tutar, segment sns.histplot(data=df, x="tutar", bins=20, kde=True) # dağılım sns.boxplot(data=df, x="sehir", y="tutar") # gruplara göre dağılım sns.violinplot(data=df, x="sehir", y="tutar", inner="quartile") # dağılımın şekli sns.scatterplot(data=df, x="adet", y="tutar", hue="segment") # ilişki + renkle grup sns.pairplot(df[["adet", "tutar", "indirim"]]) # tüm ikili ilişkiler kor = df[["adet", "tutar", "indirim"]].corr() sns.heatmap(kor, annot=True, fmt=".2f", cmap="vlag", vmin=-1, vmax=1) g = sns.FacetGrid(aylik, col="sehir", col_wrap=3, sharey=True) # küçük çoklu grafikler g.map_dataframe(sns.lineplot, x="ay", y="ciro") plt.show()
pip install seaborn komutuyla kurulur (matplotlib ve pandas'ı da getirir). Jupyter'de grafik hücrenin altında görünür; betikte plt.show() pencere açar, plt.savefig("grafik.png", dpi=150) dosyaya yazar.seaborn fonksiyonları iki türdür. Eksen düzeyi (axes-level: histplot, boxplot, scatterplot, heatmap) tek bir Axes'e çizer ve ax= parametresi alır; matplotlib'in alt grafiklerine yerleştirebilirsin. Figür düzeyi (figure-level: relplot, displot, catplot, pairplot, FacetGrid) kendi figürünü kurar ve col=/row= ile panellere böler.
Aynı grafikleri matplotlib ile kurmak
seaborn'un yaptığı işin altında matplotlib nesneleri vardır. Gruplara göre kutu ve keman grafiği (violin plot) için veriyi önce gruplara bölmek yeterlidir.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"sehir": ["Ankara"] * 6 + ["İzmir"] * 6 + ["Bursa"] * 6,
"tutar": [120, 340, 95, 410, 260, 1500, 80, 150, 210, 175, 140, 190, 300, 320, 280, 350, 310, 900],
})
sehirler = sorted(df["sehir"].unique())
gruplar = [df.loc[df["sehir"] == s, "tutar"].to_numpy() for s in sehirler]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), sharey=True)
ax1.boxplot(gruplar)
ax1.set_xticks(range(1, len(sehirler) + 1), sehirler)
ax1.set_title("Kutu grafiği")
ax1.set_ylabel("Sepet tutarı (TL)")
ax2.violinplot(gruplar, showmedians=True)
ax2.set_xticks(range(1, len(sehirler) + 1), sehirler)
ax2.set_title("Keman grafiği")
fig.savefig("dagilim.png", dpi=100, bbox_inches="tight")
plt.close(fig)
print("eksen etiketleri:", [t.get_text() for t in ax1.get_xticklabels()])
print(df.groupby("sehir")["tutar"].describe()[["min", "25%", "50%", "75%", "max"]])
Kutu grafiği beş sayıyı (min, Q1, medyan, Q3, maks) ve aykırı değerleri gösterir: Ankara'daki 1500 TL'lik sipariş ayrı bir nokta olarak görünür. Keman grafiği ise dağılımın şeklini (iki tepeli mi, çarpık mı) da gösterir.
Korelasyon ısı haritası (heatmap) da bir imshow ve her hücreye bir text'tir. Dikkat: imshow'da x ekseni sütun, y ekseni satır indeksidir; hücre yazısı ax.text(j, i, ...) ile yazılır.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"adet": [1, 3, 2, 5, 4, 6, 2, 8],
"tutar": [120, 310, 260, 480, 450, 640, 190, 820],
"indirim": [0, 5, 0, 10, 10, 15, 0, 20],
"teslim_gun": [3, 2, 4, 1, 2, 1, 5, 1],
})
kor = df.corr().round(2)
fig, ax = plt.subplots(figsize=(5, 4))
im = ax.imshow(kor.to_numpy(), cmap="RdBu_r", vmin=-1, vmax=1)
ax.set_xticks(range(len(kor.columns)), kor.columns, rotation=45)
ax.set_yticks(range(len(kor.index)), kor.index)
for i in range(len(kor.index)):
for j in range(len(kor.columns)):
ax.text(j, i, f"{kor.iloc[i, j]:.2f}", ha="center", va="center")
fig.colorbar(im, ax=ax)
ax.set_title("Korelasyon matrisi")
plt.close(fig)
print(kor)
print("hücre yazısı:", len(ax.texts), "| renk skalası:", im.get_clim())
vmin=-1, vmax=1 kullan. Sınırı vermezsen renkler verideki en küçük ve en büyük değere göre gerilir; 0.3'lük zayıf bir korelasyon koyu kırmızı görünebilir.Grafik için veri hazırlamak
Görselleştirme işinin yarısı pandas'tır. seaborn ve plotly uzun biçim ister; ayların takvim sırasında çizilmesi için sıralı kategori gerekir; şehirleri ciroya göre sıralamak okunurluğu artırır. (melt'i ileri pandas dersinde gördün.)
import pandas as pd
genis = pd.DataFrame({
"sehir": ["Ankara", "İzmir", "Bursa"],
"Oca": [1200, 800, 600], "Şub": [1500, 950, 720], "Mar": [1400, 1010, 690],
})
aylar = ["Oca", "Şub", "Mar"]
uzun = genis.melt(id_vars="sehir", var_name="ay", value_name="ciro")
uzun["ay"] = pd.Categorical(uzun["ay"], categories=aylar, ordered=True)
print(uzun.sort_values(["sehir", "ay"]).head(4))
# Çubuk grafik için: büyükten küçüğe sırala, payı hesapla
toplam = uzun.groupby("sehir")["ciro"].sum().sort_values(ascending=False)
pay = (toplam / toplam.sum() * 100).round(1)
print(pd.DataFrame({"ciro": toplam, "pay_%": pay}))
plotly express: etkileşimli grafik
plotly grafiği bir HTML/JavaScript nesnesidir: fareyle üzerine gelince değer görünür (hover), yakınlaştırılır, seriler göstergeden açılıp kapanır. Panolar (dashboard), Jupyter keşfi ve e-postayla gönderilecek tek dosyalık raporlar için idealdir. plotly.express (px) seaborn'a çok benzer bir arayüz sunar.
import plotly.express as px
fig = px.line(aylik, x="ay", y="ciro", color="sehir", markers=True,
title="Anadolu Market aylık ciro")
fig.show() # tarayıcıda / Jupyter'de etkileşimli
fig = px.scatter(df, x="adet", y="tutar", color="segment",
hover_data=["siparis_id", "sehir"], facet_col="kategori")
fig.write_html("siparisler.html") # tek dosya, her tarayıcıda açılır
fig = px.bar(toplam_df, x="sehir", y="ciro", text_auto=".2s")
fig.update_layout(yaxis_title="Ciro (TL)")
pip install plotly. Jupyter'de fig.show() hücrede etkileşimli grafik açar; PNG olarak kaydetmek için ayrıca pip install kaleido gerekir. Web panosu kurmak istersen plotly'nin Dash kütüphanesine ya da Streamlit'e bak.altair ise "grafiğin dilbilgisi" (grammar of graphics) yaklaşımını Python'a getirir: veriyi, işareti (mark) ve kodlamayı (encode) bildirirsin.
import altair as alt # pip install altair
alt.Chart(df).mark_point().encode(
x="adet", y="tutar", color="segment", tooltip=["siparis_id", "sehir"]
).interactive()
Grafik seçimi rehberi
Grafiği seçerken "hangi veriye sahibim?" diye değil "hangi soruya cevap arıyorum?" diye başla.
| Soru | Grafik | seaborn / plotly |
|---|---|---|
| Zamanla nasıl değişti? | Çizgi | lineplot / px.line |
| Hangi grup daha büyük? | Sıralı çubuk (yatay, uzun etiketlerde) | barplot / px.bar |
| Değerler nasıl dağılıyor? | Histogram, yoğunluk | histplot, kdeplot / px.histogram |
| Gruplar arasında dağılım farkı? | Kutu, keman | boxplot, violinplot / px.box |
| İki sayısal değişken ilişkili mi? | Saçılım (dağılım) grafiği | scatterplot / px.scatter |
| Birçok değişkenin ilişkisi? | Korelasyon ısı haritası, pairplot | heatmap, pairplot / px.imshow |
| Parçanın bütüne oranı? | Yığılmış çubuk; en fazla 3-4 dilimse pasta | histplot(multiple="stack") / px.bar |
| Her grupta desen aynı mı? | Küçük çoklu grafikler | FacetGrid, relplot(col=) / facet_col |
Renk ve erişilebilirlik
Rengin üç işi vardır ve her biri farklı palet ister: kategorik (şehirler: birbirinden ayrışan renkler), sıralı (sequential: az → çok, tek renk tonu, örn. viridis), iki uçlu (diverging: negatif ↔ pozitif, ortası nötr). Erkeklerin yaklaşık %8'inde bir tür renk görme farklılığı vardır; en yaygını kırmızı-yeşil ayrımıdır. "Kırmızı kötü, yeşil iyi" grafiği bu izleyiciler için tek renktir.
- Renk körü dostu paletler kullan: viridis, cividis, seaborn'da
palette="colorblind". - Rengi tek bilgi taşıyıcısı yapma: işaret şekli, çizgi stili ya da doğrudan etiket ekle.
- Metin ile arka plan arasında yeterli kontrast bırak (WCAG: normal metin için en az 4.5:1).
- Kategori sayısı 6-7'yi geçiyorsa renk ayrımı zorlaşır; grupları birleştir ya da küçük çoklu grafiğe geç.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
aylar = ["Oca", "Şub", "Mar", "Nis"]
seriler = {"Ankara": [12, 15, 14, 18], "İzmir": [8, 9, 10, 11], "Bursa": [6, 7, 7, 9]}
renkler = ["#0072B2", "#E69F00", "#009E73"] # Okabe-Ito: renk körü dostu
isaretler = ["o", "s", "^"]
cizgiler = ["-", "--", ":"]
fig, ax = plt.subplots(figsize=(7, 4))
for (sehir, y), r, m, c in zip(seriler.items(), renkler, isaretler, cizgiler):
ax.plot(aylar, y, color=r, marker=m, linestyle=c, label=sehir)
ax.annotate(sehir, (len(aylar) - 1, y[-1]), xytext=(5, 0), textcoords="offset points")
ax.set_title("Aylık ciro (bin TL)")
ax.legend()
plt.close(fig)
print([(l.get_label(), l.get_marker(), l.get_linestyle()) for l in ax.lines])
Yanıltıcı grafikler
Çoğu yanıltıcı grafik kötü niyetle değil, varsayılan ayarla üretilir. En sık beş tuzak:
| Tuzak | Etkisi | Doğrusu |
|---|---|---|
| Çubuk grafikte y ekseni 0'dan başlamıyor | %2'lik fark 5 kat görünür | Çubukta eksen 0'dan başlar; küçük farkı göstermek için çizgi ya da fark grafiği |
| Çift y ekseni | Ölçek seçimiyle her iki seri "birlikte" gösterilebilir | İki ayrı panel ya da endekslenmiş (başlangıç=100) seri |
| Seçilmiş zaman aralığı | Düşüşün ortasından başlayan grafik "toparlanma" anlatır | Tam dönem ya da yıllık kıyas |
| 3B ve çok dilimli pasta | Açı ve hacim yanlış algılanır | Sıralı yatay çubuk |
| Farklı genişlikte histogram kutuları | Dağılımın şekli değişir | Eşit kutu, birkaç kutu sayısını dene |
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
sube = ["Kadıköy", "Çankaya"]
ciro = [1000, 1020] # yalnızca %2 fark
fig, (a, b) = plt.subplots(1, 2, figsize=(8, 3))
a.bar(sube, ciro); a.set_ylim(990, 1025); a.set_title("Yanıltıcı")
b.bar(sube, ciro); b.set_ylim(0, 1100); b.set_title("Dürüst")
plt.close(fig)
def gorunen_oran(ax):
alt = ax.get_ylim()[0]
return round((ciro[1] - alt) / (ciro[0] - alt), 2)
print("gerçek oran:", round(ciro[1] / ciro[0], 2))
print("yanıltıcı grafikte görünen:", gorunen_oran(a))
print("dürüst grafikte görünen:", gorunen_oran(b))
sns.set_theme ya da matplotlib rcParams) kullanır. Keşif aşamasında seaborn ve pairplot, sunumda sade matplotlib/seaborn PNG'leri, paylaşılan panoda plotly tercih edilir.seaborn ve plotly'yi çalıştır
İki kütüphane de tarayıcıda çalışır. İlk kullanımda inerler: seaborn yaklaşık 295 KB, plotly yaklaşık 9,7 MB. plotly büyüktür; mobil veride uzun sürebilir. fig.show() grafiği çıktının içinde etkileşimli gösterir, ayrıca 4,5 MB'lık bir çizim kitaplığını taşır.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
d = pd.DataFrame({"sehir": ["Van", "İzmir", "Van", "İzmir"], "tutar": [10, 40, 30, 20]})
sns.barplot(data=d, x="sehir", y="tutar", estimator="sum", errorbar=None)
plt.show()
| Soru | Grafik |
|---|---|
| Dağılım nasıl? | histplot, boxplot |
| Grup karşılaştırması | barplot, countplot |
| Zaman içinde | lineplot |
| İki sayı ilişkisi | scatterplot, regplot |
| Gruba göre küçük panolar | FacetGrid, catplot |
| Etkileşim gerekiyor | plotly |
Derinleş: grafikten panoya ve rapora
Tek grafik bir soruyu cevaplar; pano (dashboard) ise bir ekibin her sabah baktığı soruları tek ekranda toplar. Bu bölümde Python'la pano kurmanın iki popüler yolunu (Streamlit ve Dash), iyi pano tasarımının ilkelerini, veri hikâyesinin akışını ve e-postayla gönderilebilen tek dosyalık HTML rapor üretmeyi göreceksin. Rakamlar Kesit Power BI'ın kesit_bi.Satislar tablosundan (1620 satış satırı) geliyor; aynı panoyu orada Power BI ile kuruyorsun.
Önce rakamlar: KPI hesabı
import veri
import pandas as pd
s = veri.oku("kesit_bi", "Satislar")
s = s[s["Durum"] != "İptal"].copy()
s["YilAy"] = s["Tarih"].str[:7]
def kpi(df):
ciro = df["Tutar"].sum()
siparis = df["SiparisNo"].nunique()
return {"ciro": round(ciro), "siparis": siparis, "sepet": round(ciro / siparis),
"marj": round(1 - df["Maliyet"].sum() / ciro, 3)}
bu = kpi(s[s["YilAy"] == "2024-12"])
onceki = kpi(s[s["YilAy"] == "2024-11"])
for ad in bu:
degisim = (bu[ad] - onceki[ad]) / onceki[ad]
print(f"{ad:<8} {bu[ad]:>10,} ({degisim:+.1%} geçen aya göre)")Streamlit: betikten uygulamaya
Streamlit'te uygulama yukarıdan aşağı çalışan bir betiktir; kullanıcı bir filtreyi her değiştirdiğinde betik baştan çalışır. Bu yüzden veriyi okuyan fonksiyon @st.cache_data ile önbelleğe alınır. pip install streamlit ve streamlit run pano.py ile kendi bilgisayarında dene:
# pano.py
import pandas as pd
import plotly.express as px
import streamlit as st
st.set_page_config(page_title="Kesit Satış Panosu", layout="wide")
@st.cache_data
def yukle():
s = pd.read_csv("Satislar.csv")
return s[s["Durum"] != "İptal"].assign(YilAy=lambda d: d["Tarih"].str[:7])
s = yukle()
kanallar = st.sidebar.multiselect("Kanal", sorted(s["Kanal"].unique()), default=sorted(s["Kanal"].unique()))
ay = st.sidebar.selectbox("Ay", sorted(s["YilAy"].unique(), reverse=True))
f = s[s["Kanal"].isin(kanallar)]
k1, k2, k3 = st.columns(3)
bu, onceki = f[f["YilAy"] == ay], f[f["YilAy"] < ay]
k1.metric("Ciro", f"{bu['Tutar'].sum():,.0f} TL")
k2.metric("Sipariş", bu["SiparisNo"].nunique())
k3.metric("Ortalama sepet", f"{bu['Tutar'].sum() / max(bu['SiparisNo'].nunique(), 1):,.0f} TL")
aylik = f.groupby("YilAy", as_index=False)["Tutar"].sum()
st.plotly_chart(px.line(aylik, x="YilAy", y="Tutar", title="Aylık ciro"), use_container_width=True)
st.dataframe(bu.groupby("Kanal")["Tutar"].sum().sort_values(ascending=False))Dash: bileşen ve geri çağırma
Dash (Plotly'nin çatısı) farklı bir model kullanır: sayfa düzeni bileşen ağacıdır, etkileşim ise girdi ve çıktıları açıkça bağlayan geri çağırma (callback) fonksiyonlarıyla kurulur. Yalnız etkilenen bileşen yeniden hesaplanır; daha çok kod ister ama büyük, çok sayfalı panolarda daha kontrollüdür (pip install dash).
from dash import Dash, Input, Output, dcc, html
import plotly.express as px
app = Dash(__name__)
app.layout = html.Div([
dcc.Dropdown(sorted(s["Kanal"].unique()), "Mobil", id="kanal"),
dcc.Graph(id="grafik"),
])
@app.callback(Output("grafik", "figure"), Input("kanal", "value"))
def guncelle(kanal):
aylik = s[s["Kanal"] == kanal].groupby("YilAy", as_index=False)["Tutar"].sum()
return px.bar(aylik, x="YilAy", y="Tutar")
app.run(debug=True)| Streamlit | Dash | Power BI | |
|---|---|---|---|
| Öğrenme | En kolay | Orta (callback) | Tıkla-sürükle + DAX |
| Ne zaman? | Hızlı prototip, ML demosu, iç araç | Özelleştirilmiş, büyük web panosu | Kurumsal raporlama, yetki, paylaşım |
| Yayın | Streamlit Community Cloud, sunucu | Herhangi bir Python web sunucusu | Power BI Hizmeti |
Pano tasarım ilkeleri
- Üstte KPI kartları: 3–5 ana sayı, her birinin yanında karşılaştırma (geçen ay, hedef) ve yönü. Karşılaştırmasız sayı anlam taşımaz.
- Soldan sağa, yukarıdan aşağı ayrıntıya: özet → eğilim (zaman çizgisi) → kırılım (kanal, kategori) → ayrıntı tablosu.
- Filtreler tek yerde (kenar çubuğu ya da üst şerit) ve hepsini etkiler; etkin filtre ekranda görünsün.
- Ayrıntıya inme (drill-down): bölge → şehir → müşteri gibi hiyerarşide tıklayarak inmek; her seviyede toplamlar tutarlı olmalı.
- Az renk: gri temel, vurgu yalnız dikkat çekilecek yerde; kırmızı/yeşil yerine erişilebilir palet (bu dersteki renk bölümü).
Veri hikâyesi akışı panodan farklıdır; tek bir mesajı iletir: bağlam ("Aralık'ta ciro geçen yıla göre arttı"), gerilim ("ama iade oranı Mağaza kanalında iki katına çıktı"), çözüm ("Mağaza iade sürecinin incelenmesini öneriyoruz"). Her grafiğin başlığı bir bulgu cümlesi olsun: "Aylık ciro" değil, "Ciro Aralık'ta yılın zirvesine çıktı".
Tek dosyalık HTML rapor
Herkesin tarayıcısı vardır; sunucu kurmadan paylaşılabilecek en pratik çıktı tek bir HTML dosyasıdır. Grafikleri base64 ile gömersen dosya dışa bağımlılık olmadan açılır. plotly ile etkileşimli rapor için fig.to_html(full_html=False, include_plotlyjs="cdn") parçalarını birleştirirsin (ya da çevrimdışı için include_plotlyjs=True).
import base64, html, io
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
aylik = s[s["YilAy"].str.startswith("2024")].groupby("YilAy")["Tutar"].sum()
fig, ax = plt.subplots(figsize=(6, 2.5))
ax.plot(aylik.index.str[5:], aylik.values, color="#2a6f97")
ax.set_title("2024'te ciro yıl sonuna doğru yükseldi")
tampon = io.BytesIO()
fig.savefig(tampon, format="png", dpi=100, bbox_inches="tight")
plt.close(fig)
png = base64.b64encode(tampon.getvalue()).decode()
rapor = f"""<!doctype html><html lang="tr"><meta charset="utf-8">
<title>{html.escape("Satış Raporu — Aralık 2024")}</title>
<h1>Aralık 2024</h1><p>Ciro: {bu['ciro']:,} TL</p>
<img alt="Aylık ciro" src="data:image/png;base64,{png}"></html>"""
print(len(rapor), "karakter; görsel gömülü:", "data:image/png;base64," in rapor)Grafiğin dilbilgisi
seaborn, plotly express ve altair aynı fikre dayanır: veri + görsel kanal eşlemesi (x, y, renk, boyut, şekil, panel) + işaret (nokta, çizgi, çubuk). Bu dili öğrenince araç değiştirmek kolaylaşır; R'daki ggplot2, Power BI ve Tableau da aynı mantıkla çalışır. Kanalların algı gücü sıralıdır: ortak eksende konum en doğru okunur, sonra uzunluk, açı, alan ve en son renk tonu. Önemli karşılaştırmayı konuma, ikincil bilgiyi renge ver.
Spagetti yerine küçük çoklu grafikler
On şehrin trendini tek grafikte on çizgiyle göstermek okunmaz. Ortak y ekseniyle (sharey=True) on küçük panel, hem her şehrin desenini hem de şehirler arası düzeyi gösterir. Gerekirse her panelde diğer şehirleri soluk gri arka plan olarak çiz.
Çıktı kalitesi
Sunum için dpi=200 PNG ya da ölçeklenebilir SVG/PDF kaydet; bbox_inches="tight" etiketlerin kesilmesini önler. Rapor üretimini fonksiyonlaştır: veri hazırlama, çizim ve kaydetme ayrı fonksiyonlarda olsun ki aynı grafiği her ay tek komutla yeniden üretebilesin ve testleyebilesin.