8  Matplotlib ile Grafik Çizimi

Bir fonksiyonun grafiğine bakmak, çoğu zaman bir sayfa hesabın söylediğini tek bakışta söyler: fonksiyon nerede artıyor, nerede sıçrıyor, bir yaklaşım hangi aralıkta iyi, bir dizi ne hızla yakınsıyor. Önceki bölümlerde NumPy ile bu sayıları üretmeyi öğrendik; bu bölümde onları görmeyi öğreneceğiz.

Python’da bilimsel çizimin standart aracı Matplotlib kütüphanesidir. Anaconda ve Colab ile hazır gelir; değilse pip install matplotlib komutuyla kurulur. Kütüphanenin iki kullanım biçimi vardır: kısa komutlarla “o anki” grafiğe çizen pyplot arayüzü ve çizimin her parçasını bir nesne olarak tutan nesne yönelimli arayüz. Biz baştan itibaren ikincisini kullanacağız, çünkü bir sayfaya birden çok grafik koyduğumuzda hangi komutun neyi değiştirdiği ancak böyle açık kalır. Önce tek bir eğri çizip grafiğin parçalarını tanıyacağız; sonra alt grafiklere, logaritmik eksenlere, parametrik ve kutupsal eğrilere, saçılım grafiklerine ve histogramlara, kontur çizimlerine ve üç boyutlu yüzeylere geçeceğiz.

Bu bölümdeki kodlar grafikleri fig.savefig(...) ile dosyaya yazar ve çizdikleri veriden birkaç sayıyı ekrana basar. Kodu Jupyter ya da Colab’de çalıştırırsanız grafik hücrenin altında kendiliğinden görünür; bir .py betiği olarak çalıştırıyorsanız grafiği bir pencerede görmek için en sona plt.show() satırını ekleyin. Sayfadaki şekiller, kodun ürettiği verinin aynısından sitenin temasına uygun renklerle yeniden çizilmiştir; bu yüzden renkleri Matplotlib’in varsayılan renklerinden farklıdır.

8.1 Figure ve Axes

Matplotlib’de her çizim iki katlı bir yapıya oturur: bir tuval ve tuvalin üzerindeki koordinat sistemleri.

Tanım 8.1 (Figure ve Axes) Figure, bir çizimin bütün tuvalidir; boyutu, çözünürlüğü ve üzerindeki her şey ona aittir. Axes ise bir Figure içindeki tek bir koordinat sistemidir; kendi yatay ve dikey eksenleri, tik işaretleri, eksen adları, başlığı ve lejantı vardır. Bir Figure bir ya da daha çok Axes taşır.

Yani Figure bir kâğıt, Axes o kâğıda çizilmiş bir koordinat sistemidir. Eğri çizmek, eksenlere ad koymak, sınırları ayarlamak gibi işlerin hepsi bir Axes nesnesinin metotlarıyla yapılır: ax.plot, ax.set_xlabel, ax.set_xlim gibi. Figure’a yalnız bütünle ilgili işler kalır: fig.savefig ile kaydetmek ya da fig.suptitle ile bütün çizime ortak bir başlık koymak gibi. Adın çoğul görünmesi yanıltmasın: bir Axes tek bir koordinat sistemidir, tek tek eksenlere ise Matplotlib Axis der.

İkisini birlikte üreten komut plt.subplots()’tur; burada plt, matplotlib.pyplot modülünün geleneksel kısaltmasıdır. fig, ax = plt.subplots() satırı bir Figure ile içinde tek bir Axes oluşturur ve döndürdüğü iki nesneyi iki değişkene açar. İnternetteki örneklerde sık görülen plt.plot(x, y) ya da plt.xlabel("x") gibi komutlar ise o anda etkin olan Axes’e çizer. Tek grafiklik kısa betiklerde bu biçim işe yarar, ama birden çok Axes olduğunda hangisinin etkin olduğunu izlemek zorlaşır; bu derste her zaman ax üzerinden çalışacağız.

İpucuDört adımda bir grafik
  1. Çizilecek veriyi NumPy dizileri olarak hazırla: x = np.linspace(a, b, n) ve y = f(x).
  2. fig, ax = plt.subplots() ile bir Figure ve bir Axes oluştur.
  3. ax.plot(x, y) ile çiz; eksen adlarını, başlığı ve lejantı ekle.
  4. fig.savefig("ad.png") ile kaydet ya da plt.show() ile ekranda göster.

Örnek 8.1 (Sinüs ve Kosinüs Aynı Eksende) \(\sin x\) ve \(\cos x\) fonksiyonlarını \([0, 2\pi]\) aralığında aynı Axes üzerinde, eğrileri bir lejantla adlandırarak çiziniz.

Çözüm

Tarifin dört adımını sırayla uyguluyoruz.

import numpy as np
import matplotlib.pyplot as plt

# 1. Veri: [0, 2π] aralığında eşit aralıklı 200 nokta
x = np.linspace(0, 2 * np.pi, 200)

# 2. Figure ve Axes
fig, ax = plt.subplots(figsize=(6, 4))

# 3. Çizim ve etiketler
ax.plot(x, np.sin(x), label=r"$\sin x$")
ax.plot(x, np.cos(x), linestyle="--", label=r"$\cos x$")
ax.set_xlabel("$x$")
ax.set_ylabel("$y$")
ax.set_title("Sinüs ve kosinüs")
ax.legend()
ax.grid(True)

# 4. Kaydetme
fig.savefig("sinus_kosinus.png", dpi=150)

print(type(fig).__name__, type(ax).__name__)
print("Çizilen eğri sayısı:", len(ax.lines))
print("Piksel boyutu:", fig.get_size_inches() * 150)

Çıktı:

Figure Axes
Çizilen eğri sayısı: 2
Piksel boyutu: [900. 600.]

print satırları neyin oluştuğunu doğruluyor: plt.subplots bir Figure ve bir Axes döndürdü, ax.lines listesinde iki eğri var. Figure 6 × 4 inç boyutunda ve inç başına 150 nokta (dpi=150) ile kaydedildiği için PNG dosyası 900 × 600 pikseldir. \(\blacksquare\)

Figure 0 1 2 3 4 5 6 −1 −0,5 0 0,5 1 Sinüs ve kosinüs x y sin x cos x ax.set_title Axes ax.plot ax.grid ax.legend ax.set_xlabel
İlk örnekteki kodun çizdiği grafik. Kesikli çerçeve bütün tuvali, yani Figure nesnesini; içteki çerçeve tek koordinat sistemini, yani Axes nesnesini gösterir. Kenardaki etiketler her parçayı hangi metodun oluşturduğunu söyler.

Koddaki her satırın bir görevi var:

  • np.linspace(0, 2 * np.pi, 200) aralığı 200 eşit aralıklı noktayla örnekler. Matplotlib bu noktaları sırayla doğru parçalarıyla birleştirir; ekranda düzgün görünen eğri aslında 199 parçalı bir kırık çizgidir.
  • figsize=(6, 4) Figure’ın boyutunu inç cinsinden verir.
  • ax.plot(x, y) bir eğri çizer. Aynı Axes’e yapılan her plot çağrısı yeni bir eğri ekler ve Matplotlib her birine sıradaki rengi verir. label= lejantta görünecek adı, linestyle="--" kesikli çizgiyi seçer.
  • r"$\sin x$" gibi dolar işaretleri arasındaki metinler, Matplotlib’in kendi TeX dizgicisiyle (mathtext) matematik olarak dizilir; bunun için bilgisayarda LaTeX kurulu olması gerekmez. Dizgideki ters bölü bir kaçış dizisi sanılmasın diye dizgiyi r önekiyle ham dizgi (raw string) olarak yazarız.
  • ax.legend() etiketli eğrilerden lejantı kurar, ax.grid(True) ızgara çizgilerini açar.

8.2 Çizgiler, Renkler ve Açıklamalar

İlk grafikte renkleri, çizgi kalınlıklarını ve eksen sınırlarını Matplotlib kendisi seçti. Bu seçimlerin her biri plot çağrısının anahtar kelimeli argümanlarıyla ya da Axes’in metotlarıyla değiştirilebilir. En sık kullanılan argümanlar şunlardır:

Argüman Örnek değerler Anlamı
color "C0", "C3", "k", "tab:red" Renk; "C0"–"C9" varsayılan renk sırası, "k" siyah
linestyle "-", "--", ":", "-." Düz, kesikli, noktalı ve kesik-noktalı çizgi
linewidth 1, 2.5 Çizgi kalınlığı (punto)
marker "o", "s", "^", "x", "." Her veri noktasına konan işaret
markersize 4, 7 İşaretin büyüklüğü

Kısa yol olarak üçüncü konumsal argümana bir biçim dizgisi yazılabilir: ax.plot(x, y, "ro--") kırmızı (r), daire işaretli (o), kesikli (--) bir çizgi çizer. Biçim dizgisinde çizgi stili yoksa, örneğin "o", noktalar birleştirilmez; yalnız işaretler çizilir.

Grafiğin geri kalanını Axes’in metotları ayarlar:

  • ax.set_xlim(a, b) ve ax.set_ylim(c, d) eksen sınırlarını sabitler. Sınırlar verilmezse Matplotlib onları veriden seçer ve iki yana veri aralığının %5’i kadar pay bırakır.
  • ax.set_aspect("equal") iki eksende birim uzunluğu eşitler; çember çember, kare kare görünür.
  • ax.axhline(c) ve ax.axvline(c) sırasıyla \(y = c\) ve \(x = c\) doğrularını çizer. Koordinat eksenlerini göstermek için kullanışlıdır.
  • ax.annotate(metin, xy=(x0, y0), xytext=(x1, y1), arrowprops=dict(arrowstyle="->")) metni \((x_1, y_1)\) noktasına yazar ve oradan \((x_0, y_0)\) noktasına bir ok çizer. Ok gerekmiyorsa ax.text(x, y, metin) yeterlidir.
  • ax.fill_between(x, y1, y2) iki eğrinin arasını boyar; where= argümanına bir boolean dizi verilirse yalnız koşulun sağlandığı yerler boyanır.

Örnek 8.2 (Yerel Ekstremumları İşaretlemek) \(f(x) = x^3 - 3x\) fonksiyonunun \([-2{,}5;\ 2{,}5]\) aralığındaki grafiğini, kritik noktaları işaretlenmiş ve yerel ekstremumları okla gösterilmiş olarak çiziniz.

Çözüm

\(f'(x) = 3x^2 - 3 = 3(x - 1)(x + 1)\) olduğundan kritik noktalar \(x = \pm 1\)’dir. \(f''(x) = 6x\) olduğundan \(f''(-1) < 0\) ve \(f''(1) > 0\)’dır; \(x = -1\) yerel maksimum, \(x = 1\) yerel minimum noktasıdır. Kritik noktaları ayrı bir plot çağrısında "o" işaretiyle çiziyor, okları annotate ile koyuyoruz.

import numpy as np
import matplotlib.pyplot as plt


def f(x):
    return x**3 - 3 * x


x = np.linspace(-2.5, 2.5, 300)
crit = np.array([-1.0, 1.0])      # f'(x) = 3x^2 - 3 = 0 olan noktalar

fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, f(x), color="C0", linewidth=2, label="$f(x) = x^3 - 3x$")
ax.plot(crit, f(crit), "o", color="C3", markersize=7,
        label="kritik noktalar")
ax.axhline(0, color="k", linewidth=0.8)
ax.axvline(0, color="k", linewidth=0.8)
ax.annotate("yerel maksimum", xy=(-1, 2), xytext=(-2.4, 3.6),
            arrowprops=dict(arrowstyle="->"))
ax.annotate("yerel minimum", xy=(1, -2), xytext=(-0.6, -4.2),
            arrowprops=dict(arrowstyle="->"))
ax.set_ylim(-5, 5)
ax.legend(loc="lower right")
fig.savefig("ekstremum.png", dpi=150)

for c in crit:
    print(f"x = {c:+.0f} noktasında f(x) = {f(c):+.0f}")

Çıktı:

x = -1 noktasında f(x) = +2
x = +1 noktasında f(x) = -2

f fonksiyonu NumPy işlemleriyle yazıldığı için hem tek bir sayıya hem bir diziye uygulanabilir: f(x) 300 değerlik eğriyi, f(crit) iki kritik değeri verir. Çıktı \(f(-1) = 2\) ve \(f(1) = -2\) hesabıyla uyuşur. set_ylim(-5, 5) olmasaydı dikey eksen, uç noktalardaki \(f(\pm 2{,}5) = \pm 8{,}125\) değerlerine göre seçilir ve ekstremumlar küçük kalırdı. \(\blacksquare\)

8.3 Alt Grafikler ve Logaritmik Eksenler

Birbiriyle karşılaştırılacak iki grafiği aynı Figure’a yan yana koymak çoğu zaman en açık anlatımdır. plt.subplots(nrows, ncols) çağrısı nrows satır ve ncols sütunluk bir Axes ızgarası kurar ve Axes nesnelerini bir NumPy dizisi olarak döndürür. Tek satır ya da tek sütun varsa dizi tek boyutludur (axs[0], axs[1], …); yoksa iki boyutludur (axs[1, 0] ikinci satırın ilk Axes’i). Bütün Axes’leri tek bir döngüyle dolaşmak için axs.flat kullanılır. Yararlı üç argüman daha var: sharex=True ve sharey=True Axes’lere ortak eksen sınırları verir, layout="constrained" ise başlıkların ve eksen adlarının birbirinin üstüne binmemesi için Axes’lerin yerini kendiliğinden ayarlar.

Yan yana grafiklerin sık kullanıldığı bir yer, bir yaklaşımı ve onun hatasını birlikte göstermektir. Hata ise çoğu zaman birçok mertebeye yayılır; onu okunur kılan logaritmik eksendir.

Tanım 8.2 (Logaritmik Eksen) Bir eksen logaritmik ölçekli ise üzerindeki \(v > 0\) değerinin konumu \(\log_{10} v\) ile orantılıdır. Yalnız dikey ekseni logaritmik ölçekli olan grafiğe yarı logaritmik grafik, iki ekseni de logaritmik ölçekli olan grafiğe log-log grafik denir.

Yani logaritmik eksende 1 ile 10, 10 ile 100 ve 100 ile 1000 arasındaki uzaklıklar eşittir; çok farklı büyüklükteki sayılar aynı resme sığar. İki özel durum her yerde karşımıza çıkar:

  • \(y = C a^x\) ise \(\log y = \log C + x \log a\) olur; üstel bir fonksiyon yarı logaritmik grafikte bir doğrudur.
  • \(y = C x^p\) ise \(\log y = \log C + p \log x\) olur; bir kuvvet fonksiyonu log-log grafikte eğimi \(p\) olan bir doğrudur. Örneğin bir yöntemin hatası \(h^2\) ile orantılı azalıyorsa log-log grafikte eğimi 2 olan bir doğru görürüz.

Matplotlib’de ax.semilogy(x, y) yarı logaritmik, ax.loglog(x, y) log-log grafik çizer; var olan bir Axes’in ölçeği ax.set_yscale("log") ile de değiştirilebilir. Sıfırın ve negatif sayıların logaritması olmadığından bu değerler logaritmik eksende gösterilemez. Matplotlib onları eksenin çok altına yerleştirir; bu yüzden böyle bir noktaya giden çizgi dik bir şekilde aşağı inip grafikten çıkar. ax.set_yscale("log", nonpositive="mask") yazılırsa bu noktalar atlanır ve çizgi orada kesilir.

Örnek 8.3 (Sinüsün Taylor Polinomları ve Hataları) \(\sin x\) fonksiyonunu ve \(0\) etrafındaki \(T_1, T_3, T_5, T_7\) Taylor polinomlarını \([-2\pi, 2\pi]\) aralığında bir Axes’te, \(|\sin x - T_n(x)|\) hatalarını ise yanındaki ikinci Axes’te yarı logaritmik ölçekte çiziniz.

Çözüm

\(\sin x\)’in \(0\) etrafındaki Taylor polinomu yalnız tek dereceli terimlerden oluşur (bkz. Analiz 2). \(n\) tek olmak üzere

\[ T_n(x) = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \cdots \pm \frac{x^n}{n!} = \sum_{j=0}^{(n-1)/2} (-1)^{j}\, \frac{x^{2j+1}}{(2j+1)!} \]

olur. Bu toplamı bir fonksiyona yazıyor, plt.subplots(1, 2) ile yan yana iki Axes açıyoruz. Fonksiyondaki k, tek dereceleri \(1, 3, \dots, n\) sırasıyla dolaşır; \(k = 2j + 1\) için işaret \((-1)^{(k-1)/2} = (-1)^j\)’dir.

import math

import numpy as np
import matplotlib.pyplot as plt


def taylor_sin(x, n):
    """sin x'in 0 etrafındaki n. dereceden Taylor polinomu."""
    total = np.zeros_like(x)
    for k in range(1, n + 1, 2):
        sign = (-1) ** ((k - 1) // 2)
        total += sign * x**k / math.factorial(k)
    return total


x = np.linspace(-2 * np.pi, 2 * np.pi, 400)
degrees = (1, 3, 5, 7)

fig, axs = plt.subplots(1, 2, figsize=(10, 4), layout="constrained")

axs[0].plot(x, np.sin(x), "k", linewidth=2.5, label=r"$\sin x$")
for n in degrees:
    axs[0].plot(x, taylor_sin(x, n), label=f"$T_{n}$")
axs[0].set_ylim(-3, 3)
axs[0].set_title("Taylor polinomları")
axs[0].legend()

for n in degrees:
    error = np.abs(np.sin(x) - taylor_sin(x, n))
    axs[1].semilogy(x, error, label=f"$n = {n}$")
axs[1].set_ylim(1e-16, 1e3)
axs[1].set_title(r"Hata $|\sin x - T_n(x)|$")
axs[1].legend()

fig.savefig("taylor_sinus.png", dpi=150)

print(type(axs).__name__, axs.shape)
for n in degrees:
    e = abs(np.sin(1.0) - taylor_sin(np.array([1.0]), n)[0])
    print(f"n = {n}: x = 1 noktasındaki hata {e:.2e}")

Çıktı:

ndarray (2,)
n = 1: x = 1 noktasındaki hata 1.59e-01
n = 3: x = 1 noktasındaki hata 8.14e-03
n = 5: x = 1 noktasındaki hata 1.96e-04
n = 7: x = 1 noktasındaki hata 2.73e-06

axs, iki Axes tutan tek boyutlu bir NumPy dizisidir; axs[0] soldaki, axs[1] sağdaki koordinat sistemidir. Polinomlar büyük \(|x|\) için hızla büyüdüğünden sol Axes’in dikey sınırlarını set_ylim(-3, 3) ile sabitledik; aksi hâlde Matplotlib ölçeği polinomların uçlarda 46’ya varan değerlerine göre seçer ve sinüs dümdüz görünürdü. Sağ Axes’te de alt sınırı \(10^{-16}\)’ya sabitledik, çünkü \(x = 0\) yakınında hata, çift duyarlıklı sayıların yuvarlama düzeyine iner.

Çıktıdaki hatalar her adımda sırasıyla yaklaşık 19, 42 ve 72 kat küçülür. Bu hızı Lagrange kalanlı Taylor teoremi açıklar (bkz. Analiz 2). \(n\) tek olduğundan \(n + 1\) çifttir. Sinüsün çift mertebeden türevleri \(\pm \sin\) olduğu için \(T_{n+1}\)’e eklenen \(x^{n+1}\) teriminin katsayısı \(\sin^{(n+1)}(0)/(n+1)! = 0\)’dır; yani \(T_n = T_{n+1}\)’dir. Teoremi \(T_{n+1}\)’e uygularsak \(0\) ile \(x\) arasındaki bir \(c\) için

\[ \sin x - T_n(x) = \frac{\sin^{(n+2)}(c)}{(n+2)!}\, x^{n+2} \]

olur. Sinüsün her türevi \(\pm \sin\) ya da \(\pm \cos\) olduğundan \(|\sin^{(n+2)}(c)| \le 1\)’dir; bu yüzden \(x = 1\) noktasındaki hata \(1/(n+2)!\) ile sınırlıdır. Bu sınırlar \(1/3! \approx 0{,}167\), \(1/5! \approx 0{,}0083\), \(1/7! \approx 1{,}98 \cdot 10^{-4}\) ve \(1/9! \approx 2{,}76 \cdot 10^{-6}\)’dır ve gerçek hatalar bunların hemen altında kalır. \(\blacksquare\)

−6 −4 −2 0 2 4 6 −3 −2 −1 0 1 2 3 Taylor polinomları sin x n = 1 n = 3 n = 5 n = 7 −6 −4 −2 0 2 4 6 10−15​ 10−12​ 10−9​ 10−6​ 10−3​ 100​ 103​ Hata |sin x − Tn​(x)|
Alt grafik örneğindeki kodun çizdiği iki Axes. Solda kalın açık renkli eğri sin x, ince eğriler T1, T3, T5, T7 Taylor polinomlarıdır; derece arttıkça polinom sinüse daha geniş bir aralıkta yapışır. Sağda aynı renklerle |sin x − Tn(x)| hataları logaritmik eksendedir: 0 yakınında hata 10−16 düzeyine iner, uçlarda 102 düzeyine çıkar.

Yan yana grafikler, bir çizimin yanlış olduğunu fark etmek için de iyi bir araçtır. Matplotlib noktaları sırayla birleştirdiği için, fonksiyonun süreksiz olduğu bir yerde ne olduğunu kendisi bilemez.

Örnek 8.4 (Asimptotları Olan Bir Grafik) \(\tan x\) fonksiyonunu \([-\pi, \pi]\) aralığında 1000 noktayla, asimptotlarda düzgün görünecek biçimde çiziniz.

Çözüm

Önce fonksiyonu olduğu gibi çiziyor, sonra \(|y| > 10\) olan değerleri np.nan (Not a Number, “sayı değil”) ile değiştirip yeniden çiziyoruz.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-np.pi, np.pi, 1000)
y = np.tan(x)

fig, axs = plt.subplots(1, 2, figsize=(10, 4), layout="constrained")

axs[0].plot(x, y)
axs[0].set_title("Düzeltilmemiş")

y_cut = np.where(np.abs(y) > 10, np.nan, y)
axs[1].plot(x, y_cut)
axs[1].set_ylim(-10, 10)
axs[1].set_title(r"$|y| > 10$ olan noktalar NaN")

fig.savefig("tanjant.png", dpi=150)

print(f"En büyük |tan x| değeri: {np.max(np.abs(y)):.1f}")
print("NaN yapılan nokta sayısı:", np.isnan(y_cut).sum())

Çıktı:

En büyük |tan x| değeri: 636.0
NaN yapılan nokta sayısı: 64

Soldaki grafikte iki kusur var. Örnek noktalardan biri \(\pi/2\)’ye çok yakın düştüğü için \(|\tan x|\) 636’ya ulaşmış ve Matplotlib dikey ölçeği bu değere göre seçmiştir; dalların biçimi kaybolmuş, her dal yatay bir çizgi gibi görünür. Ayrıca asimptotun solundaki büyük pozitif değerle sağındaki büyük negatif değer bir doğru parçasıyla birleştirilmiş, grafikte fonksiyona ait olmayan dikey çizgiler belirmiştir. Matplotlib NaN olan noktaları atlar ve çizgiyi orada keser: sağdaki grafikte 64 nokta NaN yapıldığı için dallar birbirinden ayrılır, set_ylim(-10, 10) de ölçeği okunur bir aralığa sabitler. \(\blacksquare\)

−3 −2 −1 0 1 2 3 −600 −400 −200 0 200 400 600 Düzeltilmemiş −3 −2 −1 0 1 2 3 −10 −5 0 5 10 |y| > 10 olan noktalar NaN
tan x örneğindeki kodun çizdiği iki Axes. Solda Matplotlib, asimptotun iki yanındaki noktaları dikey çizgilerle birleştirmiş ve ölçeği ±600 düzeyine çıkarmıştır; dalların biçimi kaybolur. Sağda |y| > 10 olan değerler NaN yapıldığı için çizgi asimptotlarda kopar.
UyarıNoktalar sırayla birleştirilir

ax.plot verilen noktaları sırayla doğru parçalarıyla birleştirir; fonksiyonun bu noktalar arasında ne yaptığını bilmez. Bu yüzden süreksizlik noktalarında dikey çizgiler belirir ve nokta sayısı azsa hızlı salınımlar kaybolur. Süreksizliklerde değerleri np.nan yapın, salınan fonksiyonlarda nokta sayısını artırın ve kuşkulu bir grafiği daha sık örnekleyerek yeniden çizin.

8.4 Parametrik ve Kutupsal Eğriler

Bir eğri her zaman \(y = f(x)\) biçiminde verilmez; düzlemde dolanan ve kendini kesen eğriler bir parametreyle ya da kutupsal koordinatlarla daha doğal yazılır.

\(x = x(t)\), \(y = y(t)\), \(a \le t \le b\) parametrik eğrisini çizmek için önce t = np.linspace(a, b, n) dizisini kurar, sonra iki koordinat dizisini ax.plot(x(t), y(t)) ile çizeriz; plot için önemli olan yalnız noktaların sırasıdır. Böyle eğrilerde ax.set_aspect("equal") neredeyse her zaman gerekir, yoksa eğri bir yönde basık görünür.

Kutupsal bir eğri için Axes’i projection="polar" ile kutupsal yaparız. Böyle bir Axes’te ax.plot(theta, r) çağrısının ilk argümanı açı (radyan), ikincisi yarıçaptır. Bir Figure’a farklı türde Axes’ler koymak için Figure’ı önce plt.figure() ile boş kurar, Axes’leri fig.add_subplot(nrows, ncols, index) ile tek tek ekleriz; index soldan sağa, yukarıdan aşağı 1’den başlayarak sayılır.

Örnek 8.5 (Bir Lissajous Eğrisi ve Kardioid) \(x = \sin 3t\), \(y = \sin 2t\) (\(0 \le t \le 2\pi\)) parametrik eğrisini ve \(r = 1 + \cos\theta\) kardioidini aynı Figure’da yan yana çiziniz.

Çözüm

Sağdaki Axes kutupsal olacağı için Figure’ı boş kuruyor, iki Axes’i fig.add_subplot ile ekliyoruz. Kodun sonunda kardioidin sınırladığı bölgenin alanını da yaklaşık hesaplıyoruz. Kutupsal bir bölgenin alanı

\[ A = \frac{1}{2}\int_0^{2\pi} r(\theta)^2 \, d\theta \]

formülüyle verilir (bkz. Analiz 2). İntegrali, \(\theta\) ızgarasının sol uç noktalarındaki değerlerle kurulan bir Riemann toplamıyla yaklaşık hesaplıyoruz.

import numpy as np
import matplotlib.pyplot as plt

t = np.linspace(0, 2 * np.pi, 400)

fig = plt.figure(figsize=(10, 4.5), layout="constrained")

# Sol: parametrik eğri, eşit ölçekli eksende
ax1 = fig.add_subplot(1, 2, 1)
ax1.plot(np.sin(3 * t), np.sin(2 * t))
ax1.set_aspect("equal")
ax1.set_title(r"$x = \sin 3t,\ y = \sin 2t$")

# Sağ: kutupsal eksen; plot(θ, r) sırasıyla
ax2 = fig.add_subplot(1, 2, 2, projection="polar")
r = 1 + np.cos(t)
ax2.plot(t, r)
ax2.set_rticks([0.5, 1, 1.5, 2])
ax2.set_rlabel_position(157.5)
ax2.set_title(r"$r = 1 + \cos\theta$")

fig.savefig("parametrik_kutupsal.png", dpi=150)

# Kardioidin alanı (1/2)∫ r^2 dθ için bir Riemann toplamı
dtheta = t[1] - t[0]
area = 0.5 * np.sum(r[:-1] ** 2) * dtheta
print(type(ax2).__name__)
print(f"Riemann toplamı: {area:.6f}")
print(f"3π/2           : {3 * np.pi / 2:.6f}")

Çıktı:

PolarAxes
Riemann toplamı: 4.712389
3π/2           : 4.712389

Kardioidin alanı \(\dfrac{3\pi}{2} \approx 4{,}712389\)’dur (bkz. Analiz 2); Riemann toplamı altı ondalık basamağa kadar bu değeri verir. Toplamı r[:-1] ile kurduk, çünkü linspace dizisinin son elemanı olan \(2\pi\), ilk eleman \(0\) ile aynı açıdır. set_rticks yarıçap çemberlerinin yerlerini, set_rlabel_position(157.5) ise yarıçap etiketlerinin yazılacağı açıyı derece cinsinden seçer; etiketleri eğriden uzak tutmak için 157,5°’yi seçtik. \(\blacksquare\)

−1 −0,5 0 0,5 1 −1 −0,5 0 0,5 1 x = sin 3t, y = sin 2t 0° 45° 90° 135° 180° 225° 270° 315° 0,5 1 1,5 2 r = 1 + cos θ
Parametrik ve kutupsal örnekteki kodun çizdiği iki Axes. Solda eşit ölçekli eksende x = sin 3t, y = sin 2t Lissajous eğrisi; sağda kutupsal eksende r = 1 + cos θ kardioidi. Kutupsal eksende açılar derece, yarıçap çemberleri 0,5 aralıklıdır.
UyarıKutupsal eksende negatif yarıçap

Kutupsal koordinatlarda \(r < 0\) iken \((r, \theta)\) noktası, \((|r|, \theta + \pi)\) noktasıdır. Matplotlib’in kutupsal Axes’i bu kuralı uygulamaz: negatif yarıçaplar varsa merkeze \(r = 0\)’ı değil, eksenin en küçük \(r\) değerini koyar ve eğriyi biçimini bozarak çizer. Yarıçap negatif değer alabiliyorsa eğriyi \(x = r\cos\theta\), \(y = r\sin\theta\) dönüşümüyle sıradan bir Axes’te çizin.

8.5 Saçılım Grafikleri ve Histogramlar

Şimdiye kadar çizdiğimiz her şey, birbirine doğru parçalarıyla bağlanan noktalardı. Bazı verilerde ise noktalar arasında bir sıra yoktur; önemli olan tek tek noktaların nerede durduğu ya da sayı doğrusuna nasıl yayıldığıdır.

Tek tek noktaları ax.scatter(x, y) çizer. s= işaretin alanını (punto kare), marker= biçimini, alpha= saydamlığını, c= rengini verir. c= bir sayı dizisi olarak verilirse her nokta kendi değerine göre bir renk skalasından (cmap=) boyanır ve fig.colorbar(...) bu skalayı grafiğin yanına çizer. Bütün noktaları aynı biçimde çizmek için ax.plot(x, y, "o") de yeterlidir; scatter, boyut ya da renk noktadan noktaya değiştiğinde gereklidir.

Bir saçılım grafiğinin açığa çıkardığı bir düzeni görelim. Rastgele bir matrisin özdeğerleri nerede durur? Tek tek bakıldığında düzensiz görünen bu karmaşık sayılar, büyük matrislerde şaşırtıcı bir düzene uyar ve bunu ancak çizince fark ederiz. Özdeğerleri, NumPy ile Lineer Cebir bölümündeki np.linalg.eig’in yalnız özdeğerleri döndüren biçimi olan np.linalg.eigvals ile hesaplıyoruz. Rastgele sayıları np.random.default_rng üreteci verir; onu Rastgele Sayılar ve Monte Carlo Yöntemleri bölümünde ayrıntılı göreceğiz. Üretece sabit bir tohum (seed) verdiğimiz için kod her çalıştırmada aynı sayıları üretir.

Örnek 8.6 (Rastgele Bir Matrisin Özdeğerleri) Girdileri bağımsız standart normal sayıların \(\sqrt{300}\)’e bölümü olan \(300 \times 300\)’lük bir \(A\) matrisinin özdeğerlerini, reel özdeğerler ayrı bir işaretle gösterilmek üzere karmaşık düzlemde bir saçılım grafiğiyle çiziniz.

Çözüm

Reel özdeğerleri, sanal kısmının mutlak değeri çok küçük olanlar olarak bir boolean maskeyle ayırıyoruz.

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(2024)
n = 300
A = rng.standard_normal((n, n)) / np.sqrt(n)
lam = np.linalg.eigvals(A)
is_real = np.abs(lam.imag) < 1e-12

t = np.linspace(0, 2 * np.pi, 400)
fig, ax = plt.subplots(figsize=(5, 5))
ax.plot(np.cos(t), np.sin(t), "k--", linewidth=1, label="birim çember")
ax.scatter(lam[~is_real].real, lam[~is_real].imag, s=12,
           label="reel olmayan")
ax.scatter(lam[is_real].real, lam[is_real].imag, s=30, marker="s",
           label="reel")
ax.set_aspect("equal")
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.5, 1.5)
ax.set_xlabel(r"Re $\lambda$")
ax.set_ylabel(r"Im $\lambda$")
ax.legend(loc="upper right")
fig.savefig("ozdegerler.png", dpi=150)

print("Özdeğer sayısı:", lam.size)
print("Reel özdeğer sayısı:", is_real.sum())
print(f"En büyük |λ|: {np.abs(lam).max():.4f}")
print(f"|λ| ≤ 1 olanların oranı: {np.mean(np.abs(lam) <= 1):.3f}")

Çıktı:

Özdeğer sayısı: 300
Reel özdeğer sayısı: 12
En büyük |λ|: 1.0453
|λ| ≤ 1 olanların oranı: 0.970

is_real bir boolean dizidir; lam[is_real] reel özdeğerleri, lam[~is_real] geri kalanları seçer (~ maskeyi tersine çevirir). İki ayrı scatter çağrısı lejantta iki ayrı girdi verir. Resim belirgin bir düzen gösteriyor: özdeğerler birim diske neredeyse düzgün dağılmış, %97’si birim çemberin içinde kalmıştır. \(A\) reel bir matris olduğundan karakteristik polinomunun katsayıları reeldir; bu yüzden reel olmayan özdeğerler eşlenik çiftler hâlinde gelir ve resim reel eksene göre simetriktir. Gördüğümüz düzen, rastgele matris kuramının dairesel yasa (circular law) adlı sonucunun \(n = 300\) için bir görüntüsüdür: \(n \to \infty\) iken böyle bir matrisin özdeğerleri birim diskte düzgün dağılıma yaklaşır. \(\blacksquare\)

−1,5 −1 −0,5 0 0,5 1 1,5 −1,5 −1 −0,5 0 0,5 1 1,5 Re λ Im λ birim çember reel olmayan reel
Saçılım örneğindeki kodun çizdiği grafik: girdileri bağımsız standart normal sayıların √300'e bölümü olan 300 × 300'lük bir matrisin 300 özdeğeri. Daireler reel olmayan, kareler 12 reel özdeğerdir; noktalar birim diske neredeyse düzgün dağılır.

Noktaların tek tek konumu yerine sayı doğrusuna nasıl yayıldıklarını görmek istediğimizde histogram çizeriz.

Tanım 8.3 (Histogram) \(x_1, \dots, x_N\) gerçel sayıları ve \(b_0 < b_1 < \cdots < b_m\) kutu sınırları, verilerin hepsi \([b_0, b_m]\) aralığında kalacak biçimde verilsin. \(j\)-inci kutuya, yani \([b_{j-1}, b_j)\) aralığına düşen veri sayısı \(n_j\) olsun; son kutu \([b_{m-1}, b_m]\) kapalı aralığıdır. Her kutunun üstüne yüksekliği \(n_j\) olan bir dikdörtgen dikilerek elde edilen grafiğe histogram denir. Yükseklik olarak

\[ \frac{n_j}{N\,(b_j - b_{j-1})} \]

alınırsa dikdörtgenlerin alanları toplamı 1 olur; bu grafiğe yoğunluk ölçeğinde histogram denir.

Yani histogram, verinin sayı doğrusuna nasıl yayıldığının resmidir. Yoğunluk ölçeğinde bir dikdörtgenin alanı, verinin o kutuya düşen oranıdır; bu yüzden böyle bir histogram bir olasılık yoğunluk fonksiyonuyla aynı eksende karşılaştırılabilir (bkz. Olasılık Teorisi). Matplotlib’de counts, edges, patches = ax.hist(data, bins=40, density=True) çağrısı histogramı çizer; kutu yüksekliklerini (counts), kutu sınırlarını (edges) ve çizilen dikdörtgenleri (patches) döndürür. bins bir tam sayıysa veri aralığı o kadar eşit kutuya bölünür, bir diziyse kutu sınırları doğrudan o dizi olur.

Örnek 8.7 (Simetrik Rastgele Bir Matrisin Özdeğer Histogramı) \(G\), girdileri bağımsız standart normal sayılar olan \(1000 \times 1000\)’lik bir matris olsun. \(H = (G + G^{T})/\sqrt{2000}\) simetrik matrisinin özdeğerlerinin 40 kutulu, yoğunluk ölçeğinde histogramını

\[ \rho(x) = \frac{\sqrt{4 - x^2}}{2\pi}, \qquad -2 \le x \le 2 \]

yarım çember yoğunluğuyla aynı eksende çiziniz.

Çözüm

\(H\) simetrik olduğundan özdeğerleri reeldir. Onları np.linalg.eigh’in yalnız özdeğerleri döndüren biçimi olan np.linalg.eigvalsh ile hesaplıyoruz; bu fonksiyon özdeğerleri artan sırada verir.

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(7)
n = 1000
G = rng.standard_normal((n, n))
H = (G + G.T) / np.sqrt(2 * n)          # simetrik rastgele matris
lam = np.linalg.eigvalsh(H)             # artan sırada reel özdeğerler

x = np.linspace(-2, 2, 400)
semicircle = np.sqrt(4 - x**2) / (2 * np.pi)

fig, ax = plt.subplots(figsize=(6, 4))
counts, edges, _ = ax.hist(lam, bins=40, density=True, alpha=0.6,
                           label="özdeğerler")
ax.plot(x, semicircle, "k", linewidth=2, label="yarım çember")
ax.set_xlabel(r"$\lambda$")
ax.set_ylim(0, 0.45)
ax.legend()
fig.savefig("yarim_cember.png", dpi=150)

width = edges[1] - edges[0]
print(f"En küçük ve en büyük özdeğer: {lam[0]:.3f}, {lam[-1]:.3f}")
print(f"Kutu genişliği: {width:.4f}")
print(f"Dikdörtgenlerin alanları toplamı: {np.sum(counts * width):.6f}")
print(f"En yüksek kutu: {counts.max():.3f}, "
      f"yarım çemberin tepesi: {1 / np.pi:.3f}")

Çıktı:

En küçük ve en büyük özdeğer: -2.016, 1.975
Kutu genişliği: 0.0998
Dikdörtgenlerin alanları toplamı: 1.000000
En yüksek kutu: 0.341, yarım çemberin tepesi: 0.318

ax.hist histogramı çizerken kutu yüksekliklerini de döndürdü. Kutular eşit genişlikte olduğundan alanların toplamı np.sum(counts * width) ile bulunur ve çıktıdaki gibi 1’dir. Özdeğerler \([-2, 2]\) aralığına yayılmış ve histogram yarım çemberi yakından izliyor. Bu, Wigner’in yarım çember yasası (semicircle law) denen sonucunun bir görüntüsüdür: \(n \to \infty\) iken böyle matrislerin özdeğer dağılımı \(\rho\) yoğunluğuna yaklaşır. \(\rho\) gerçekten bir olasılık yoğunluğudur: \(\sqrt{4 - x^2}\) eğrisinin altı yarıçapı 2 olan yarım dairedir, alanı \(2\pi\)’dir ve \(2\pi\)’ye bölününce 1 kalır. Yoğunluğun tepe değeri \(\rho(0) = 1/\pi \approx 0{,}318\)’dir; en yüksek kutunun \(0{,}341\) çıkması rastgele dalgalanmadır. \(\blacksquare\)

−2 −1,5 −1 −0,5 0 0,5 1 1,5 2 0 0,1 0,2 0,3 0,4 λ özdeğerler yarım çember
Histogram örneğindeki kodun çizdiği grafik: 1000 × 1000'lik simetrik rastgele bir matrisin özdeğerleri 40 kutuda, yoğunluk ölçeğinde. Siyah eğri √(4 − x²)/(2π) yarım çember yoğunluğudur; dikdörtgenler onu yakından izler.

8.6 Kontur Çizimleri

İki değişkenli bir \(f(x, y)\) fonksiyonunu düzlemde görmenin en yaygın yolu, haritalardaki eş yükselti eğrileri gibi, \(f\)’nin sabit bir değer aldığı eğrileri çizmektir. Bu eğriler \(f\)’nin seviye kümeleridir (bkz. Diferansiyel Geometri) ve Matplotlib’de onları contour çizer. Kontur çizmek için \(f\)’nin değerlerine dikdörtgen bir ızgaranın her noktasında ihtiyaç vardır; ızgarayı NumPy’ın meshgrid fonksiyonu kurar.

Tanım 8.4 (Izgara Dizileri) \(x\), \(m\) elemanlı ve \(y\), \(n\) elemanlı iki dizi olsun. X, Y = np.meshgrid(x, y) çağrısı \(n \times m\) biçimli iki dizi döndürür:

\[ X_{ij} = x_j, \qquad Y_{ij} = y_i . \]

\((X_{ij}, Y_{ij})\) çiftleri, \(x\) ve \(y\) değerlerinden kurulabilecek bütün noktaları, yani dikdörtgen bir ızgarayı dolaşır.

Yani X’in her satırı \(x\)’in bir kopyası, Y’nin her sütunu \(y\)’nin bir kopyasıdır. Satır indisi \(y\)’yi, sütun indisi \(x\)’i sayar; matris kâğıda yazıldığında sütunların soldan sağa dizilmesi \(x\) ekseniyle uyumludur. Küçük bir örnek:

import numpy as np

x = np.array([0, 1, 2])
y = np.array([10, 20])
X, Y = np.meshgrid(x, y)
print(X)
print(Y)
print(X + Y)

Çıktı:

[[0 1 2]
 [0 1 2]]
[[10 10 10]
 [20 20 20]]
[[10 11 12]
 [20 21 22]]

Bu iki diziyle Z = f(X, Y) yazmak, \(f\)’yi ızgaranın her noktasında tek bir vektörel işlemle hesaplar. Aynı sonuca Vektörizasyon ve Broadcasting bölümündeki kurallarla x[np.newaxis, :] + y[:, np.newaxis] biçiminde de ulaşılabilir; meshgrid bunu açık ve okunur kılar.

Kontur komutlarının levels argümanı bir tam sayıysa Matplotlib kabaca o sayıda, yuvarlak değerli seviyeleri kendisi seçer (seviye sayısı istenenden biraz farklı çıkabilir), bir diziyse tam olarak o seviyeleri kullanır. contour çizgileri, contourf ise ardışık iki seviye arasındaki bantları boyar.

İpucuDört adımda kontur grafiği
  1. Eksenlerdeki örnek noktaları kur: x = np.linspace(a, b, m) ve y = np.linspace(c, d, n).
  2. Izgarayı kur: X, Y = np.meshgrid(x, y).
  3. Fonksiyonu ızgaranın her noktasında hesapla: Z = f(X, Y).
  4. Seviye eğrilerini cs = ax.contour(X, Y, Z, levels=...) ile ya da boyalı bantları cf = ax.contourf(X, Y, Z, levels=...) ile çiz; ax.clabel(cs) seviyelerin değerlerini çizgilere yazar, fig.colorbar(cf, ax=ax) renk skalasını ekler.

Örnek 8.8 (Bir Tepe ve Bir Çukurun Kontur Grafikleri) \(f(x, y) = x\,e^{-x^2 - y^2}\) fonksiyonunun \([-2, 2] \times [-2, 2]\) karesindeki \(-0{,}45;\ -0{,}35;\ \dots;\ 0{,}45\) seviyelerini önce çizgilerle, sonra boyanmış bantlarla çiziniz.

Çözüm

Grafiği yorumlayabilmek için önce kritik noktaları bulalım:

\[ f_x = (1 - 2x^2)\,e^{-x^2 - y^2}, \qquad f_y = -2xy\,e^{-x^2 - y^2} . \]

\(f_x = 0\) denkleminden \(x = \pm 1/\sqrt{2}\) bulunur; bu durumda \(f_y = 0\) denklemi \(y = 0\) verir. \(f\) sonsuzda \(0\)’a gider ve hem pozitif hem negatif değerler alır; bu yüzden en büyük değerini \((1/\sqrt{2}, 0)\), en küçük değerini \((-1/\sqrt{2}, 0)\) noktasında alır:

\[ f\left(\pm\frac{1}{\sqrt{2}}, 0\right) = \pm\frac{e^{-1/2}}{\sqrt{2}} = \pm\frac{1}{\sqrt{2e}} \approx \pm 0{,}4289 . \]

Kodun sonunda ızgaradaki en büyük değeri bu sayıyla karşılaştırıyoruz.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-2, 2, 200)
y = np.linspace(-2, 2, 200)
X, Y = np.meshgrid(x, y)
Z = X * np.exp(-X**2 - Y**2)
levels = np.linspace(-0.45, 0.45, 10)

fig, axs = plt.subplots(1, 2, figsize=(10, 4.2), layout="constrained")

cs = axs[0].contour(X, Y, Z, levels=levels, colors="k")
axs[0].clabel(cs, fontsize=8)
axs[0].set_title("contour")

cf = axs[1].contourf(X, Y, Z, levels=levels, cmap="RdBu_r")
fig.colorbar(cf, ax=axs[1])
axs[1].set_title("contourf")

for ax in axs:
    ax.set_aspect("equal")
fig.savefig("kontur.png", dpi=150)

i, j = np.unravel_index(np.argmax(Z), Z.shape)
print("Seviyeler:", levels.round(2))
print(f"Izgaradaki en büyük değer: {Z[i, j]:.4f}, "
      f"(x, y) = ({X[i, j]:.3f}, {Y[i, j]:.3f})")
print(f"Gerçek maksimum: {1 / np.sqrt(2 * np.e):.4f}, "
      f"(x, y) = ({1 / np.sqrt(2):.3f}, 0)")

Çıktı:

Seviyeler: [-0.45 -0.35 -0.25 -0.15 -0.05  0.05  0.15  0.25  0.35  0.45]
Izgaradaki en büyük değer: 0.4288, (x, y) = (0.714, -0.010)
Gerçek maksimum: 0.4289, (x, y) = (0.707, 0)

np.argmax(Z) iki boyutlu diziyi düzleştirip en büyük elemanın sıra numarasını verir; np.unravel_index bu sıra numarasını (satır, sütun) indis çiftine çevirir. Izgara aralığı \(4/199 \approx 0{,}02\) olduğundan ızgaradaki en büyük değer gerçek maksimumdan ancak dördüncü ondalık basamakta ayrılır ve maksimum noktasına en yakın ızgara noktasında alınır. Soldaki grafikte negatif seviyeler kesiklidir: colors="k" ile tek renk seçildiğinde Matplotlib negatif seviyeleri kendiliğinden kesikli çizer. En büyük değer \(0{,}4289 < 0{,}45\) olduğundan \(\pm 0{,}45\) seviyeleri grafikte hiç görünmez; renk çubuğu yine de bütün bantları gösterir. \(\blacksquare\)

−0,35 −0,25 −0,15 −0,05 0,05 0,15 0,25 0,35 −2 −1 0 1 2 −2 −1 0 1 2 contour −2 −1 0 1 2 −2 −1 0 1 2 contourf −0,45 −0,35 −0,25 −0,15 −0,05 0,05 0,15 0,25 0,35 0,45
Kontur örneğindeki kodun çizdiği iki Axes: z = xe−x² − y² fonksiyonunun −0,45, −0,35, …, 0,45 seviyeleri. Solda contour çizgileri (negatif seviyeler kesikli, değerler çizginin üstünde), sağda contourf ile boyanmış bantlar ve renk çubuğu. Sağdaki tepe (0,707; 0) noktasında maksimum, soldaki çukur (−0,707; 0) noktasında minimumdur.

Kontur çizimi, kapalı biçimde verilmiş bir \(F(x, y) = 0\) eğrisini çizmenin de en kolay yoludur: tek seviyesi \(0\) olan bir kontur grafiği çizmek yeter. Çizilen eğrinin noktalarına da ulaşabiliriz: cs.get_paths() her seviye için bir yol (Path) nesnesi döndürür, bu nesnenin vertices özelliği de noktaları \(N \times 2\) biçimli bir dizi olarak verir.

Örnek 8.9 (Descartes Yaprağı) \(x^3 + y^3 = 3xy\) eğrisini (Descartes yaprağı) kontur çizimiyle çizerek eğrinin döngüsü üzerinde \(x + y\) toplamını en büyük yapan noktayı bulunuz.

Çözüm

\(F(x, y) = x^3 + y^3 - 3xy\) dersek eğri, \(F\)’nin \(0\) seviye kümesidir. Döngü birinci bölgededir; döngünün noktalarını, kontur çizgisinin \(x > 0\) ve \(y > 0\) olan noktaları olarak bir maskeyle seçiyoruz.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-3, 3, 601)
X, Y = np.meshgrid(x, x)
F = X**3 + Y**3 - 3 * X * Y

fig, ax = plt.subplots(figsize=(5, 5))
cs = ax.contour(X, Y, F, levels=[0], colors="C0")
ax.plot([-3, 3], [2, -4], "k:", linewidth=1)    # asimptot y = -x - 1
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
ax.set_aspect("equal")
fig.savefig("descartes_yapragi.png", dpi=150)

path = cs.get_paths()[0]
pts = path.vertices
print("Eğri üzerindeki nokta sayısı:", len(pts))
loop = pts[(pts[:, 0] > 0) & (pts[:, 1] > 0)]
k = np.argmax(loop.sum(axis=1))
print(f"Döngünün x + y'yi en büyük yapan noktası: "
      f"({loop[k, 0]:.3f}, {loop[k, 1]:.3f})")
residual = np.abs(pts[:, 0]**3 + pts[:, 1]**3 - 3 * pts[:, 0] * pts[:, 1])
print(f"En büyük |F| değeri: {residual.max():.1e}")

Çıktı:

Eğri üzerindeki nokta sayısı: 1642
Döngünün x + y'yi en büyük yapan noktası: (1.500, 1.500)
En büyük |F| değeri: 2.1e-04

Bulduğumuz noktayı Lagrange çarpanları yöntemiyle doğrulayalım (bkz. Analiz 4). \(x + y\)’nin \(F = 0\) üzerindeki ekstremumlarında \(\nabla F\), \((1, 1)\) vektörüne paraleldir; yani \(F_x = F_y\) olur:

\[ 3x^2 - 3y = 3y^2 - 3x \iff (x - y)(x + y + 1) = 0 . \]

Döngü üzerinde \(x, y > 0\) olduğundan \(x + y + 1 > 0\)’dır, dolayısıyla \(x = y\) olur. Bunu \(F = 0\)’da yerine yazınca \(2x^3 = 3x^2\), yani \(x = 3/2\) bulunur. Kodun bulduğu \((1{,}5;\ 1{,}5)\) noktası budur. Kontur noktaları ızgara hücrelerinde doğrusal interpolasyonla hesaplandığından \(F\) bu noktalarda tam sıfır değildir; çıktıdaki en büyük \(|F|\) değeri \(2{,}1 \cdot 10^{-4}\)’tür. Grafiğe eğrinin \(y = -x - 1\) asimptotunu da noktalı çizgiyle ekledik. \(\blacksquare\)

8.7 Üç Boyutlu Yüzeyler

İki değişkenli bir fonksiyonun grafiği uzayda bir yüzeydir (bkz. Analiz 4). Matplotlib bir Axes’i projection="3d" ile üç boyutlu yapar. Böyle bir Axes’te plot_surface(X, Y, Z) yüzeyi, plot_wireframe(X, Y, Z) yalnız tel kafesini, plot(x, y, z) ise uzayda bir eğriyi çizer. Bakış doğrultusunu view_init(elev=..., azim=...) seçer: elev bakış doğrultusunun \(xy\)-düzlemiyle yaptığı açı, azim ise bu doğrultunun \(xy\)-düzlemindeki izdüşümünün \(x\) ekseniyle yaptığı açıdır; ikisi de derece cinsindendir. plt.show() ile açılan pencerede üç boyutlu bir Axes fareyle döndürülebilir.

Örnek 8.10 (Bir Tepe ve Bir Çukurun Yüzeyi) Kontur örneğindeki \(f(x, y) = x\,e^{-x^2 - y^2}\) fonksiyonunun \([-2, 2] \times [-2, 2]\) üzerindeki grafiğini \(21 \times 21\)’lik bir ızgarayla yüzey olarak çiziniz.

Çözüm

Kontur örneğindeki ızgarayı daha seyrek kuruyor, Axes’i projection="3d" ile ekliyoruz.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-2, 2, 21)
y = np.linspace(-2, 2, 21)
X, Y = np.meshgrid(x, y)
Z = X * np.exp(-X**2 - Y**2)

fig = plt.figure(figsize=(6, 5))
ax = fig.add_subplot(projection="3d")
ax.plot_surface(X, Y, Z, cmap="RdBu_r", edgecolor="k", linewidth=0.3)
ax.set_xlabel("$x$")
ax.set_ylabel("$y$")
ax.set_zlabel("$z$")
ax.view_init(elev=25, azim=-60)
fig.savefig("yuzey.png", dpi=150)

print(type(ax).__name__)
print("Izgara:", Z.shape, "->", (Z.shape[0] - 1) * (Z.shape[1] - 1),
      "dörtgen yüzey parçası")
print(f"z aralığı: [{Z.min():.4f}, {Z.max():.4f}]")

Çıktı:

Axes3D
Izgara: (21, 21) -> 400 dörtgen yüzey parçası
z aralığı: [-0.4218, 0.4218]

plot_surface ızgaranın her hücresini bir dörtgenle doldurur; \(21 \times 21\) noktalı ızgarada \(20 \cdot 20 = 400\) dörtgen vardır. cmap="RdBu_r" dörtgenleri yüksekliklerine göre pozitifte kırmızı, negatifte mavi tonlarla boyar; edgecolor ve linewidth dörtgenlerin kenarlarını çizer. Izgara aralığı \(0{,}2\) olduğundan ızgara \(x = 1/\sqrt{2}\) noktasından geçmez: ızgaradaki en büyük değer \(0{,}4218\), gerçek maksimum ise \(0{,}4289\)’dur ve tepe şekilde hafifçe düzleşmiş görünür. Daha sık bir ızgara daha düzgün bir yüzey verir ama çizimi yavaşlatır. \(\blacksquare\)

−0,4 −0,2 0 0,2 0,4 z −2 −2 −1 −1 0 0 1 1 2 2 x y
Yüzey örneğindeki kodun çizdiği grafik: z = xe−x² − y² yüzeyi 21 × 21'lik ızgaranın 400 dörtgeniyle, yükseklik pozitifken kırmızı, negatifken mavi tonlarla. Ön sağda tepe, arka solda çukur görünür; kaba ızgara yüzünden tepe hafifçe düzleşmiştir.

8.8 Grafikleri Kaydetmek

Bir grafiği bir rapora, sunuma ya da LaTeX belgesine koymak için dosyaya kaydederiz; bunun tek komutu fig.savefig’tir. Dosya biçimi uzantıdan anlaşılır:

  • .png bir piksel resmidir (raster). Piksel boyutu, Figure’ın inç cinsinden boyutu ile dpi (inç başına nokta) değerinin çarpımıdır.
  • .pdf ve .svg vektör biçimleridir: çizgileri ve yazıları geometrik nesneler olarak saklar, istenildiği kadar büyütülünce bile bozulmaz. LaTeX belgeleri için PDF en uygun seçimdir.

bbox_inches="tight" resmin kenarlarındaki boşluğu kırpar, transparent=True arka planı saydam yapar.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 1, 100)
fig, ax = plt.subplots(figsize=(4, 3))
ax.plot(x, x**2)

for dpi in (100, 200):
    name = f"parabol_{dpi}.png"
    fig.savefig(name, dpi=dpi)
    image = plt.imread(name)
    print(f"dpi = {dpi}: {image.shape[1]} x {image.shape[0]} piksel")

fig.savefig("parabol.pdf")
fig.savefig("parabol.svg")
print("Açık figür sayısı:", len(plt.get_fignums()))
plt.close(fig)
print("Kapattıktan sonra:", len(plt.get_fignums()))

Çıktı:

dpi = 100: 400 x 300 piksel
dpi = 200: 800 x 600 piksel
Açık figür sayısı: 1
Kapattıktan sonra: 0

\(4 \times 3\) inçlik Figure, 100 dpi ile \(400 \times 300\), 200 dpi ile \(800 \times 600\) piksel olarak kaydedilmiştir. plt.imread bir PNG dosyasını (yükseklik, genişlik, renk kanalı) biçimli bir diziye okur; genişliğin shape[1], yüksekliğin shape[0] olmasının nedeni budur. plt.get_fignums() açık Figure’ların numaralarını listeler. pyplot, plt.subplots ile oluşturulan her Figure’ı biz kapatana kadar bellekte tutar; çok sayıda grafik üreten döngülerde her Figure kaydedildikten sonra plt.close(fig) ile kapatılmalıdır. Betiklerde plt.show() penceresi kapanınca pyplot o Figure’ı da kapatır; bu yüzden önce savefig, sonra show çağrılır.

8.9 Alıştırmalar

Alıştırma 8.1 (Az Noktayla Çizilen Bir Salınım) \(\sin 20x\) fonksiyonunu \([0, \pi]\) aralığında önce 21, sonra 1000 eşit aralıklı noktayla çiziniz. 21 noktalı grafiğin neden yanlış olduğunu, çizilen \(|y|\) değerlerinin en büyüğüne bakarak açıklayınız.

Çözüm

İki grafiği ortak dikey eksenli (sharey=True) iki Axes’te çiziyor, her birinde çizilen \(|y|\) değerlerinin en büyüğünü yazdırıyoruz.

import numpy as np
import matplotlib.pyplot as plt

fig, axs = plt.subplots(1, 2, figsize=(10, 3.5), sharey=True,
                        layout="constrained")
for ax, n in zip(axs, (21, 1000)):
    x = np.linspace(0, np.pi, n)
    y = np.sin(20 * x)
    ax.plot(x, y)
    ax.set_title(f"{n} nokta")
    print(f"n = {n:4d}: en büyük |y| = {np.max(np.abs(y)):.2e}")
fig.savefig("ornekleme.png", dpi=150)

Çıktı:

n =   21: en büyük |y| = 5.88e-15
n = 1000: en büyük |y| = 1.00e+00

21 noktalı ızgarada örnek noktaları \(x_k = k\pi/20\)’dir (\(k = 0, 1, \dots, 20\)) ve bu noktalarda \(\sin 20x_k = \sin k\pi = 0\) olur. Yani örnek noktaların hepsi fonksiyonun sıfırlarına denk gelmiştir; çıktıdaki \(5{,}88 \cdot 10^{-15}\) yalnız yuvarlama hatasıdır ve grafik yatay bir doğruya dönüşür. Fonksiyonun periyodu \(2\pi/20 = \pi/10\) olduğundan \([0, \pi]\) aralığında 10 tam periyot vardır; bir periyodu düzgün göstermek için periyot başına en az birkaç düzine nokta gerekir. 1000 noktalı ızgarada her periyoda yaklaşık 100 nokta düşer ve en büyük değer beklendiği gibi 1’e çok yakındır. \(\blacksquare\)

Alıştırma 8.2 (İki Eğri Arasındaki Bölge) \([0, 2\pi]\) aralığında \(\sin x \ge \cos x\) olan bölgeyi fill_between ile tarayan bir grafik çiziniz ve aynı boolean maskeyle kurulan bir Riemann toplamıyla bu bölgenin alanını yaklaşık hesaplayınız.

Çözüm

\(\sin x \ge \cos x\) eşitsizliği \([0, 2\pi]\) aralığında tam olarak \([\pi/4, 5\pi/4]\) üzerinde sağlanır. Bölgenin alanı

\[ \int_{\pi/4}^{5\pi/4} (\sin x - \cos x)\,dx = \Big[-\cos x - \sin x\Big]_{\pi/4}^{5\pi/4} = \sqrt{2} - \left(-\sqrt{2}\right) = 2\sqrt{2} \]

olur. fill_between çağrısına where=s >= c maskesini veriyor, Riemann toplamını aynı maskeyle seçilen noktalar üzerinden kuruyoruz.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 2001)
s, c = np.sin(x), np.cos(x)

fig, ax = plt.subplots()
ax.plot(x, s, label=r"$\sin x$")
ax.plot(x, c, label=r"$\cos x$")
ax.fill_between(x, s, c, where=s >= c, alpha=0.3,
                label=r"$\sin x \geq \cos x$")
ax.legend()
fig.savefig("sin_cos_alan.png", dpi=150)

dx = x[1] - x[0]
inside = s >= c
area = np.sum((s - c)[inside]) * dx
print(f"Riemann toplamı: {area:.6f}")
print(f"2√2           : {2 * np.sqrt(2):.6f}")

Çıktı:

Riemann toplamı: 2.828425
2√2           : 2.828427

Riemann toplamı \(2\sqrt{2} \approx 2{,}828427\) değerinden yalnız \(2 \cdot 10^{-6}\) kadar ayrılır. where= argümanı aynı uzunlukta bir boolean dizi bekler; fill_between yalnız True olan ardışık noktaların arasını boyar. \(\blacksquare\)

Alıştırma 8.3 (Chebyshev Polinomları Bir Izgarada) \(k = 1, 2, 3, 4\) için \(T_k(x) = \cos(k \arccos x)\) Chebyshev polinomlarını \([-1, 1]\) aralığında \(2 \times 2\)’lik bir alt grafik ızgarasında çiziniz ve her polinomun örnek noktalar arasında kaç kez işaret değiştirdiğini sayınız.

Çözüm

\(2 \times 2\)’lik axs dizisini axs.flat ile tek bir döngüde dolaşıyoruz; enumerate(..., start=1) sayacı \(k\)’yi 1’den başlatır. İki komşu örnek noktadaki değerlerin çarpımı negatifse aralarında bir işaret değişimi vardır.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-1, 1, 401)
fig, axs = plt.subplots(2, 2, figsize=(8, 6), sharex=True, sharey=True,
                        layout="constrained")
print("axs.shape =", axs.shape)
for k, ax in enumerate(axs.flat, start=1):
    T = np.cos(k * np.arccos(x))
    ax.plot(x, T)
    ax.axhline(0, color="k", linewidth=0.8)
    ax.set_title(f"$T_{k}(x)$")
    sign_changes = np.sum(T[:-1] * T[1:] < 0)
    print(f"k = {k}: işaret değişimi sayısı {sign_changes}")
fig.savefig("chebyshev.png", dpi=150)

Çıktı:

axs.shape = (2, 2)
k = 1: işaret değişimi sayısı 1
k = 2: işaret değişimi sayısı 2
k = 3: işaret değişimi sayısı 3
k = 4: işaret değişimi sayısı 4

\(T_k(x) = 0\) eşitliği \(k \arccos x = (2j - 1)\pi/2\) demektir; yani \(T_k\), \((-1, 1)\) aralığında

\[ x_j = \cos\frac{(2j - 1)\pi}{2k}, \qquad j = 1, \dots, k \]

noktalarında sıfır olur ve her birinde işaret değiştirir. Sayım tam olarak \(k\) işaret değişimi buldu. sharex=True ve sharey=True dört Axes’e aynı eksen sınırlarını verdiği için grafikler doğrudan karşılaştırılabilir. \(\blacksquare\)

Alıştırma 8.4 (Üstel Fonksiyon Polinomu Geçer) \(n = 1, 2, \dots, 70\) için \(2^n\) ve \(n^{10}\) değerlerini yarı logaritmik bir grafikte çiziniz. \(2^n > n^{10}\) eşitsizliği hangi \(n\) değerinden başlayarak hep sağlanır?

Çözüm

\(2^{70} \approx 1{,}2 \cdot 10^{21}\) sayısı NumPy’ın 64 bitlik tam sayılarına sığmaz ve 2**n tam sayı dizisi sessizce taşar. Bu yüzden hesabı 2.0**n ve n.astype(float) ile ondalıklı sayılarla yapıyoruz. Yarı logaritmik eksende \(2^n\) bir doğru, \(n^{10}\) ise giderek yatıklaşan bir eğri olarak görünür.

import numpy as np
import matplotlib.pyplot as plt

n = np.arange(1, 71)
exp_values = 2.0**n
poly_values = n.astype(float) ** 10

fig, ax = plt.subplots()
ax.semilogy(n, exp_values, label="$2^n$")
ax.semilogy(n, poly_values, label="$n^{10}$")
ax.set_xlabel("$n$")
ax.legend()
fig.savefig("ussel_polinom.png", dpi=150)

wins = exp_values > poly_values
print("2^n > n^10 olan n değerleri:")
print(n[wins])
last = n[~wins].max()
print(f"n = {last}: 2^n = {2.0**last:.3e}, n^10 = {float(last)**10:.3e}")

Çıktı:

2^n > n^10 olan n değerleri:
[ 1 59 60 61 62 63 64 65 66 67 68 69 70]
n = 58: 2^n = 2.882e+17, n^10 = 4.308e+17

\(n = 1\) için \(2 > 1\) olur, \(n = 2, \dots, 58\) için \(n^{10}\) daha büyüktür ve \(n = 59\)’dan itibaren \(2^n\) öne geçer. Grafik yalnız \(n \le 70\)’e kadar bakar; geçişin kalıcı olduğunu görmek için

\[ g(n) = n \ln 2 - 10 \ln n \]

fonksiyonuna bakalım. \(g'(n) = \ln 2 - 10/n\) türevi \(n > 10/\ln 2 \approx 14{,}4\) için pozitiftir, yani \(g\) bu bölgede artandır. \(g(58) \approx -0{,}40 < 0\) ve \(g(59) \approx 0{,}12 > 0\) olduğundan her \(n \ge 59\) için \(g(n) > 0\), yani \(2^n > n^{10}\) olur. Yanıt \(n = 59\)’dur. \(\blacksquare\)

Alıştırma 8.5 (Bir Serinin Yakınsama Hızı) \(S_n = \sum_{k=1}^{n} 1/k^2\) kısmi toplamları \(\pi^2/6\) değerine yakınsar. \(n = 1, \dots, 10\,000\) için \(\pi^2/6 - S_n\) hatasını log-log grafikte çiziniz ve grafiğin eğimini tahmin ediniz.

Çözüm

Bütün kısmi toplamları np.cumsum (birikimli toplam) tek seferde verir. Karşılaştırma için \(1/n\) doğrusunu da çiziyoruz.

import numpy as np
import matplotlib.pyplot as plt

N = 10_000
n = np.arange(1, N + 1)
partial = np.cumsum(1 / n**2)          # S_1, S_2, ..., S_N
error = np.pi**2 / 6 - partial

fig, ax = plt.subplots()
ax.loglog(n, error, label=r"$\pi^2/6 - S_n$")
ax.loglog(n, 1 / n, "--", label="$1/n$")
ax.set_xlabel("$n$")
ax.legend()
fig.savefig("yakinsama_hizi.png", dpi=150)

for m in (10, 100, 1000, 10000):
    e = error[m - 1]
    print(f"n = {m:5d}: hata = {e:.3e}, n * hata = {m * e:.4f}")
slope = ((np.log(error[-1]) - np.log(error[9]))
         / (np.log(N) - np.log(10)))
print(f"Eğim tahmini: {slope:.4f}")

Çıktı:

n =    10: hata = 9.517e-02, n * hata = 0.9517
n =   100: hata = 9.950e-03, n * hata = 0.9950
n =  1000: hata = 9.995e-04, n * hata = 0.9995
n = 10000: hata = 1.000e-04, n * hata = 1.0000
Eğim tahmini: -0.9928

İki noktadan geçen doğrunun log-log eğimi

\[ \frac{\log E_{10\,000} - \log E_{10}}{\log 10\,000 - \log 10} \]

ile tahmin edilir (\(E_n = \pi^2/6 - S_n\)) ve \(-0{,}99\) çıkar: hata kabaca \(1/n\) gibi azalır. Bunu integralle karşılaştırma da gösterir. \(1/x^2\) azalan olduğundan

\[ \begin{aligned} \frac{\pi^2}{6} - S_n &= \sum_{k=n+1}^{\infty} \frac{1}{k^2}, \\[1mm] \frac{1}{n+1} = \int_{n+1}^{\infty} \frac{dx}{x^2} &< \sum_{k=n+1}^{\infty} \frac{1}{k^2} < \int_{n}^{\infty} \frac{dx}{x^2} = \frac{1}{n} \end{aligned} \]

olur. Çıktıdaki n * hata sütununun 1’e yaklaşması da bunu söyler. Hata \(1/n\) hızında azaldığı için \(10^{-6}\) mertebesinde bir hata bu yolla yaklaşık bir milyon terim ister. \(\blacksquare\)

Alıştırma 8.6 (Üç Yapraklı Gül) \(r = \cos 3\theta\) (\(0 \le \theta \le 2\pi\)) gülünü önce doğrudan kutupsal bir Axes’te, sonra \(x = r\cos\theta\), \(y = r\sin\theta\) dönüşümüyle sıradan bir Axes’te çiziniz. İki grafik neden farklıdır?

Çözüm

İki grafiği yan yana çiziyor, kutupsal Axes’in merkezine hangi yarıçap değerini koyduğunu get_rorigin ile soruyoruz.

import numpy as np
import matplotlib.pyplot as plt

theta = np.linspace(0, 2 * np.pi, 600)
r = np.cos(3 * theta)

fig = plt.figure(figsize=(10, 4.5), layout="constrained")
ax1 = fig.add_subplot(1, 2, 1, projection="polar")
ax1.plot(theta, r)
ax1.set_title("Kutupsal eksen, negatif r")

ax2 = fig.add_subplot(1, 2, 2)
ax2.plot(r * np.cos(theta), r * np.sin(theta))
ax2.set_aspect("equal")
ax2.set_title(r"$x = r\cos\theta,\ y = r\sin\theta$")
fig.savefig("gul.png", dpi=150)

print(f"Kutupsal eksenin merkezindeki r değeri: {ax1.get_rorigin():.2f}")
print("r < 0 olan örnek sayısı:", np.sum(r < 0), "/", r.size)

Çıktı:

Kutupsal eksenin merkezindeki r değeri: -1.10
r < 0 olan örnek sayısı: 300 / 600

Örneklerin yarısında \(r < 0\)’dır. Kutupsal koordinatlarda böyle bir \((r, \theta)\) noktası \((|r|, \theta + \pi)\) noktasıdır, ama Matplotlib’in kutupsal Axes’i yarıçap eksenini yaklaşık \([-1{,}1;\ 1{,}1]\) aralığına ölçekler ve merkeze \(r = -1{,}10\) değerini koyar. Bir noktanın merkeze uzaklığı \(r + 1{,}10\) olarak çizildiğinden soldaki eğri aslında \(\rho = \cos 3\theta + 1{,}10\) eğrisidir: yaprakları şişkin ve merkezden geçmeyen başka bir eğri. Sağdaki grafik noktaları doğru yerlere koyar: her yaprağı merkezden geçen üç yapraklı gül. \(\cos(3\theta + 3\pi) = -\cos 3\theta\), yani \(r(\theta + \pi) = -r(\theta)\) olduğundan \(\theta\) ile \(\theta + \pi\) aynı noktayı verir; gülün tamamı \(\theta \in [0, \pi]\) ile çizilir ve \([\pi, 2\pi]\) aralığı aynı eğriyi ikinci kez dolaşır. Kutupsal Axes’te kalmak istenirse ax1.plot(np.where(r < 0, theta + np.pi, theta), np.abs(r)) çağrısı noktaları kuralına uygun olarak yerleştirir. \(\blacksquare\)

Alıştırma 8.7 (Collatz Adımlarının Saçılım Grafiği) \(n\) çiftse \(n \mapsto n/2\), tekse \(n \mapsto 3n + 1\) kuralıyla \(n\)’den \(1\)’e ulaşmak için gereken adım sayısını \(n = 1, \dots, 1000\) için hesaplayıp bir saçılım grafiğiyle gösteriniz. Bu aralıkta en çok adım gerektiren \(n\) hangisidir?

Çözüm

Adımları Kontrol Yapıları ve Fonksiyonlar bölümündeki gibi bir while döngüsüyle sayıyoruz. Bu hesap kolayca vektörelleştirilemez, çünkü her \(n\)’nin yolu farklı uzunluktadır; değerleri bir list comprehension ile toplayıp diziye çeviriyoruz. int(k) NumPy tam sayısını Python tam sayısına çevirir; döngü böylece daha hızlı çalışır ve taşma riski taşımaz. Noktaları birleştirmek anlamsız bir zikzak verir; bu yüzden scatter kullanıyoruz.

import numpy as np
import matplotlib.pyplot as plt


def collatz_steps(n):
    """n'den 1'e ulaşana kadar geçen Collatz adımı sayısı."""
    steps = 0
    while n != 1:
        n = n // 2 if n % 2 == 0 else 3 * n + 1
        steps += 1
    return steps


n = np.arange(1, 1001)
steps = np.array([collatz_steps(int(k)) for k in n])

fig, ax = plt.subplots()
ax.scatter(n, steps, s=4)
ax.set_xlabel("$n$")
ax.set_ylabel("adım sayısı")
fig.savefig("collatz.png", dpi=150)

i = np.argmax(steps)
print(f"En uzun yol: n = {n[i]}, {steps[i]} adım")
print(f"Ortalama adım sayısı: {steps.mean():.2f}")

Çıktı:

En uzun yol: n = 871, 178 adım
Ortalama adım sayısı: 59.54

1000’e kadar en uzun yol \(n = 871\) içindir ve 178 adım sürer. Grafikte noktalar dağınık değil, bantlar hâlinde kümelenir: yakın sayıların yolları çoğu zaman birkaç adım sonra birleşir ve aynı kuyruğu paylaşır. \(\blacksquare\)

Alıştırma 8.8 (İki Zarın Toplamının Histogramı) İki zar 10 000 kez atılıyor (rng = np.random.default_rng(42)). Toplamların yoğunluk ölçeğinde histogramını, her kutu tek bir tam sayı içerecek biçimde çizerek göreli sıklıkları gerçek olasılıklarla karşılaştırınız.

Çözüm

Toplam \(S\), \(2\) ile \(12\) arasında bir tam sayıdır ve \(P(S = s) = \dfrac{6 - |s - 7|}{36}\)’dır. Kutu sınırlarını tam sayıların ortasına, \(1{,}5;\ 2{,}5;\ \dots;\ 12{,}5\) noktalarına koyarsak her kutu tek bir tam sayı içerir. Kutu genişliği 1 olduğundan yoğunluk ölçeğindeki yükseklik doğrudan göreli sıklıktır. rng.integers(1, 7, ...) çağrısı \(1, \dots, 6\) arasında tam sayılar üretir; üst sınır dahil değildir. ax.hist’e verilen rwidth=0.9 çubukları kutu genişliğinin %90’ı kadar çizip aralarında boşluk bırakır; ax.set_xticks(range(2, 13)) yatay eksendeki işaretleri \(2, 3, \dots, 12\) tam sayılarına koyar.

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
N = 10_000
rolls = rng.integers(1, 7, size=(N, 2))     # her satır iki zar
s = rolls.sum(axis=1)

fig, ax = plt.subplots()
counts, edges, _ = ax.hist(s, bins=np.arange(1.5, 13.5), density=True,
                           rwidth=0.9)
ax.set_xticks(range(2, 13))
fig.savefig("zar_toplami.png", dpi=150)

print("Kutu kenarları:", edges)
print(" s  göreli sıklık  olasılık")
for value, c in zip(range(2, 13), counts):
    p = (6 - abs(value - 7)) / 36
    print(f"{value:2d}  {c:13.4f}  {p:8.4f}")

Çıktı:

Kutu kenarları: [ 1.5  2.5  3.5  4.5  5.5  6.5  7.5  8.5  9.5 10.5 11.5 12.5]
 s  göreli sıklık  olasılık
 2         0.0279    0.0278
 3         0.0575    0.0556
 4         0.0906    0.0833
 5         0.1079    0.1111
 6         0.1408    0.1389
 7         0.1645    0.1667
 8         0.1351    0.1389
 9         0.1109    0.1111
10         0.0858    0.0833
11         0.0514    0.0556
12         0.0276    0.0278

En büyük fark \(s = 4\) için yaklaşık \(0{,}007\)’dir; 10 000 atışta bu büyüklükte dalgalanmalar beklenir ve atış sayısı arttıkça küçülür. Kutu sınırlarını tam sayılara koymak bir tuzaktır: bins=10 yazsaydık sınırlar \(2, 3, \dots, 12\) olurdu ve son kutu \([11, 12]\) kapalı olduğu için 11 ile 12 aynı kutuya düşerdi. \(\blacksquare\)

Alıştırma 8.9 (Himmelblau Fonksiyonunun Minimumları) Himmelblau fonksiyonu

\[ F(x, y) = (x^2 + y - 11)^2 + (x + y^2 - 7)^2 \]

olarak tanımlanır. Bu fonksiyonun \([-5, 5] \times [-5, 5]\) karesindeki kontur grafiğini logaritmik aralıklı seviyelerle çiziniz ve her açık çeyrek düzlemde \(F\)’nin ızgaradaki en küçük değerini alan noktayı grafikte işaretleyiniz.

Çözüm

\(F \ge 0\)’dır; değerleri minimumların yakınında \(0\)’a inerken köşelerde 250 ile 890 arasına çıkar. Eşit aralıklı seviyeler minimumların çevresini boş bırakacağından seviyeleri np.logspace(-1, 3, 13) ile \(10^{-1}\) ile \(10^{3}\) arasında logaritmik aralıklı seçiyoruz. Her çeyrekte en küçük değeri bulmak için çeyreğin dışındaki noktaları np.where ile \(+\infty\) yapıp argmin alıyoruz.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 401)
X, Y = np.meshgrid(x, x)
F = (X**2 + Y - 11) ** 2 + (X + Y**2 - 7) ** 2

fig, ax = plt.subplots(figsize=(5, 5))
ax.contour(X, Y, F, levels=np.logspace(-1, 3, 13), cmap="viridis")
ax.set_aspect("equal")

for sx in (1, -1):
    for sy in (1, -1):
        quadrant = (sx * X > 0) & (sy * Y > 0)
        masked = np.where(quadrant, F, np.inf)
        i, j = np.unravel_index(np.argmin(masked), F.shape)
        ax.plot(X[i, j], Y[i, j], "r*", markersize=10)
        print(f"({X[i, j]:6.3f}, {Y[i, j]:6.3f}): F = {F[i, j]:.4f}")
fig.savefig("himmelblau.png", dpi=150)

Çıktı:

( 3.000,  2.000): F = 0.0000
( 3.575, -1.850): F = 0.0048
(-2.800,  3.125): F = 0.0024
(-3.775, -3.275): F = 0.0030

\((3, 2)\) bir ızgara noktasıdır ve orada \(F = 0\) olur: \(9 + 2 - 11 = 0\) ve \(3 + 4 - 7 = 0\). Fonksiyonun öteki üç minimumu da \(F = 0\) değerini alır, ama ızgara noktalarına denk gelmez; ızgara aralığı \(0{,}025\) olduğundan bulduğumuz noktalar gerçek minimumlara \(0{,}01\)’den daha yakındır ve \(F\) değerleri sıfıra yakın ama sıfır değildir. Bu noktaları tam olarak bulmayı Kök Bulma ve Optimizasyon bölümünde göreceğiz. \(\blacksquare\)

Alıştırma 8.10 (Bir Helisin Uzunluğu) \(x = \cos t\), \(y = \sin t\), \(z = t/(2\pi)\) (\(0 \le t \le 6\pi\)) helisini üç boyutlu bir Axes’te çiziniz ve 1000 noktalı bir kırık çizgiyle bulunan uzunluğu tam uzunlukla karşılaştırınız.

Çözüm

Hız vektörü \((-\sin t, \cos t, 1/(2\pi))\)’dir ve büyüklüğü \(\sqrt{1 + 1/(4\pi^2)}\) sabittir. Bu yüzden tam uzunluk

\[ L = \int_0^{6\pi} \sqrt{1 + \frac{1}{4\pi^2}}\,dt = 6\pi \cdot \frac{\sqrt{4\pi^2 + 1}}{2\pi} = 3\sqrt{4\pi^2 + 1} \approx 19{,}086795 \]

olur. Kırık çizginin uzunluğu ardışık noktalar arasındaki uzaklıkların toplamıdır; np.diff ardışık farkları tek seferde verir.

import numpy as np
import matplotlib.pyplot as plt

t = np.linspace(0, 6 * np.pi, 1000)
x, y, z = np.cos(t), np.sin(t), t / (2 * np.pi)

fig = plt.figure()
ax = fig.add_subplot(projection="3d")
ax.plot(x, y, z)
ax.set_xlabel("$x$")
ax.set_ylabel("$y$")
ax.set_zlabel("$z$")
fig.savefig("helis.png", dpi=150)

dx, dy, dz = np.diff(x), np.diff(y), np.diff(z)
length = np.sum(np.sqrt(dx**2 + dy**2 + dz**2))
exact = 3 * np.sqrt(4 * np.pi**2 + 1)
print(f"Kırık çizginin uzunluğu: {length:.6f}")
print(f"Tam yay uzunluğu       : {exact:.6f}")

Çıktı:

Kırık çizginin uzunluğu: 19.086519
Tam yay uzunluğu       : 19.086795

Kırık çizginin köşeleri eğrinin üzerindedir ve iki nokta arasındaki en kısa yol doğru parçası olduğundan kırık çizgi eğriden biraz kısadır; fark yaklaşık \(2{,}8 \cdot 10^{-4}\)’tür. \(\blacksquare\)

Alıştırma 8.11 (Bir Döngüde Grafik Kaydetmek) \(n = 1, 2, 3\) için \(y = x^n\) fonksiyonunun \([0, 1]\) üzerindeki grafiğini ayrı birer dosyaya kaydeden ve iş bittiğinde bellekte açık Figure bırakmayan bir döngü yazınız.

Çözüm

Her turda yeni bir Figure açıyor, kaydettikten hemen sonra plt.close(fig) ile kapatıyoruz. Dosya adlarını bir f-string ile kuruyoruz.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 1, 200)
names = []
for n in range(1, 4):
    fig, ax = plt.subplots(figsize=(4, 3))
    ax.plot(x, x**n)
    ax.set_title(f"$y = x^{n}$")
    name = f"kuvvet_{n}.png"
    fig.savefig(name, dpi=100, bbox_inches="tight")
    plt.close(fig)
    names.append(name)

print("Kaydedilen dosyalar:", names)
print("Açık figür sayısı:", len(plt.get_fignums()))

Çıktı:

Kaydedilen dosyalar: ['kuvvet_1.png', 'kuvvet_2.png', 'kuvvet_3.png']
Açık figür sayısı: 0

plt.close(fig) satırı olmasaydı döngü üç Figure’ı bellekte bırakırdı ve son satır 3 basardı. Yüzlerce grafik üreten döngülerde bu bellek tüketir; Matplotlib 20’den çok Figure açık kaldığında bir uyarı verir. bbox_inches="tight" ise kaydedilen resmin kenarlarındaki boşluğu kırpar. \(\blacksquare\)

Bu bölümde sayıları görünür kılmayı öğrendik: tek bir eğriden alt grafiklere, logaritmik eksenlerden kontur çizimlerine ve yüzeylere kadar. Sonraki bölümlerde yöntemlerin davranışını hep bu araçlarla çizeceğiz. Önce hesabın sembolik yüzüne geçiyoruz: SymPy ile Sembolik Hesap bölümünde türev, integral ve limitleri sayılarla değil formüllerle hesaplayan bir kütüphane tanıyacak, sembolik ifadeleri NumPy fonksiyonlarına çevirip bu bölümdeki gibi çizmenin yolunu da göreceğiz.