5  NumPy Dizileri

Matematikte en çok kullandığımız nesneler vektörler, matrisler ve bir aralığa yayılmış nokta kümeleridir. Bir fonksiyonu bin noktada değerlendirmek, bir lineer sistemin katsayılarını saklamak ya da bir ısı dağılımını bir ızgara üzerinde tutmak aynı işe çıkar: çok sayıda sayıyı düzenli bir tabloda tutup onlarla toplu hesap yapmak. Python’ın listeleri bunun için tasarlanmamıştır. İki listeyi + ile “topladığımızda” uç uca eklenirler, bir listeyi 2 ile çarptığımızda iki kez tekrarlanır ve her eleman bellekte ayrı bir Python nesnesi olarak durur.

NumPy (Numerical Python) bu boşluğu dolduran kütüphanedir. Temel nesnesi olan ndarray, aynı türden sayıları bellekte tek bir blokta tutar ve onları vektör, matris ya da daha yüksek boyutlu bir tablo olarak okur. SciPy, Matplotlib ve pandas başta olmak üzere bu dersin geri kalanında kullanacağımız hemen her kütüphane bu nesnenin üzerine kuruludur.

Bu bölümde dizileri kurmayı, şekillerini ve veri türlerini okumayı, parçalarını seçmeyi ve dizileri birleştirmeyi öğreniyoruz. Diziler üzerinde döngü yazmadan toplu hesap yapmak bir sonraki bölümün konusudur.

5.1 Neden NumPy?

Farkı görmek için aynı iki vektörü önce liste, sonra NumPy dizisi olarak toplayalım.

import numpy as np

u = [1, 2, 3]
v = [10, 20, 30]
print("liste + liste:", u + v)
print("2 * liste    :", 2 * u)

x = np.array(u)
y = np.array(v)
print("dizi + dizi  :", x + y)
print("2 * dizi     :", 2 * x)
print("dizi * dizi  :", x * y)

Çıktı:

liste + liste: [1, 2, 3, 10, 20, 30]
2 * liste    : [1, 2, 3, 1, 2, 3]
dizi + dizi  : [11 22 33]
2 * dizi     : [2 4 6]
dizi * dizi  : [10 40 90]

import numpy as np satırı NumPy’ı yükler ve ona kısa np adını verir; bu kısaltma bütün Python dünyasında ortak bir alışkanlıktır. np.array bir listeyi diziye çevirir. Listelerde + birleştirme, 2 * tekrar anlamına gelirken diziler bu işlemleri bileşen bileşen (elementwise) yapar: x + y vektör toplamı, 2 * x skalerle çarpımdır. x * y ise matris çarpımı değil, karşılıklı bileşenlerin çarpımıdır; matris çarpımı için kullanılan @ işlecini NumPy ile Lineer Cebir bölümünde göreceğiz.

Tanım 5.1 (NumPy Dizisi) Bir NumPy dizisi (ndarray, \(n\)-dimensional array), aynı veri türündeki sabit sayıda elemanın bellekteki tek bir veri bloğunda tutulduğu ve bir şekil (shape) yardımıyla bir, iki ya da daha çok indeksle okunduğu nesnedir.

Yani bir NumPy dizisini listeden üç özellik ayırır: bütün elemanları aynı türdendir, eleman sayısı dizi kurulurken belirlenir ve elemanlar bellekte tek bir blokta durur; yeni kurulan bir dizide yan yana dizilmişlerdir. Liste ise bellekte dağınık duran nesnelerin adreslerini tutan bir tablodur. Aşağıdaki şekil iki düzeni aynı beş sayı için karşılaştırıyor.

Python listesi adres tablosu tür 0,0 tür 0,5 tür 1,0 tür 1,5 tür 2,0 her eleman ayrı bir float nesnesi NumPy dizisi dtype: float64 shape: (5,) strides: (8,) 0,0 0,5 1,0 1,5 2,0 0 8 16 24 32 40 bayt
Aynı beş sayı (0,0; 0,5; 1,0; 1,5 ve 2,0) iki biçimde. Üstte Python listesi: her eleman bellekte başka bir yerde duran, kendi tür ve sayaç bilgisini taşıyan bir float nesnesidir; liste yalnız bu nesnelerin adreslerini tutar. Altta float64 dizisi: sayılar 8'er baytlık bitişik bir blokta yan yana durur; dtype, shape ve strides bu bloğun nasıl okunacağını söyler.

Bu düzenin somut bir sonucu bellek kullanımıdır. Bin ondalık sayıyı liste ve dizi olarak saklayıp kapladıkları yeri ölçelim. sys.getsizeof bir nesnenin bayt cinsinden boyutunu verir; listenin toplam boyutu, adres tablosu ile tek tek float nesnelerinin boyutlarının toplamıdır.

import sys
import numpy as np

n = 1000
lst = [k / 2 for k in range(n)]
arr = np.array(lst)

# liste: adres tablosu + her bir float nesnesi
total = sys.getsizeof(lst) + sum(sys.getsizeof(t) for t in lst)
print("liste:", total, "bayt")
print("dizi :", arr.nbytes, "bayt")
print("eleman başına:", arr.itemsize, "bayt")

Çıktı:

liste: 32856 bayt
dizi : 8000 bayt
eleman başına: 8 bayt

Liste aynı sayılar için dört kattan fazla yer kaplar: her float nesnesi 8 baytlık değerin yanında tür ve sayaç bilgisi de taşır, tablo da her nesne için ayrıca bir adres tutar. Dizinin nbytes niteliği veriye ayrılan bayt sayısını, itemsize niteliği de bir elemanın boyutunu verir. Bitişik düzenin asıl kazancı ise hızdır: NumPy işlemlerini derlenmiş C döngüleriyle, işlemciye yan yana duran sayıları besleyerek yapar. Bu farkı Vektörizasyon ve Broadcasting bölümünde ölçeceğiz.

5.2 Dizi Kurmak

NumPy’da dizi kurmanın iki yolu vardır: var olan bir Python listesini dönüştürmek ya da diziyi doğrudan bir kurala göre üretmek.

np.array bir listeyi bir boyutlu, iç içe bir listeyi iki boyutlu diziye çevirir; iç içe listede her iç liste matrisin bir satırı olur. Her dizinin ndim (boyut sayısı), shape (şekil) ve size (eleman sayısı) nitelikleri vardır.

import numpy as np

x = np.array([3, 1, 4, 1, 5])
A = np.array([[1, 2, 3],
              [4, 5, 6]])
print(x)
print(A)
print("x:", x.ndim, x.shape, x.size)
print("A:", A.ndim, A.shape, A.size)

Çıktı:

[3 1 4 1 5]
[[1 2 3]
 [4 5 6]]
x: 1 (5,) 5
A: 2 (2, 3) 6

Tanım 5.2 (Dizinin Şekli ve Eksenleri) Bir dizinin şekli (shape), her indeksin kaç değer alabileceğini sırayla veren \((n_0, n_1, \dots, n_{d-1})\) demetidir. \(d\) sayısı dizinin boyutu (ndim), \(k\)-ıncı indeksin değiştiği yön de dizinin \(k\) numaralı eksenidir (axis). Dizinin eleman sayısı (size) \(n_0 n_1 \cdots n_{d-1}\) çarpımıdır.

Yani \(m \times n\) bir matris, şekli (m, n) olan iki boyutlu bir dizidir: 0 ekseni satır numarasını, 1 ekseni sütun numarasını sayar. Bir boyutlu bir dizinin şekli (5,) gibi tek elemanlı bir demettir; sondaki virgül, tek elemanlı demeti parantez içindeki bir sayıdan ayırır. İndeksler Python’ın her yerinde olduğu gibi 0’dan başlar: matematikteki \(a_{11}\) bileşeni NumPy’da A[0, 0], genel olarak \(a_{ij}\) bileşeni A[i-1, j-1] olur.

Sık kullanılan diziler için hazır fonksiyonlar vardır. np.zeros ve np.ones istenen şekilde sıfırlardan ve birlerden, np.full tek bir sabitten oluşan bir dizi kurar. np.eye(n) \(n \times n\) birim matrisi (bkz. Lineer Cebir), np.diag ise verilen köşegenle bir köşegen matris (bkz. Lineer Cebir) üretir. np.zeros_like(B), B ile aynı şekil ve türde bir sıfır dizisi verir.

import numpy as np

print(np.zeros(4))
print(np.ones((2, 3)))
print(np.full((2, 2), 7))
print(np.eye(3))
print(np.diag([1, 2, 3]))
print(np.zeros_like(np.diag([1, 2, 3])))

Çıktı:

[0. 0. 0. 0.]
[[1. 1. 1.]
 [1. 1. 1.]]
[[7 7]
 [7 7]]
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
[[1 0 0]
 [0 2 0]
 [0 0 3]]
[[0 0 0]
 [0 0 0]
 [0 0 0]]

zeros, ones ve eye kendiliğinden ondalık (float64) dizi kurar; çıktıdaki 0. ve 1. yazımı bunun işaretidir. Şekil tek bir demet olarak verilir: np.zeros((2, 3)) doğrudur, np.zeros(2, 3) ise hata verir, çünkü ikinci argüman veri türüne ayrılmıştır. Bir de np.empty vardır: belleği ayırır ama doldurmaz, bu yüzden o bellek bölgesinde önceden ne varsa onu gösterir. Yalnız bütün elemanların hemen ardından yazılacağı durumlarda kullanılır.

5.3 Eşit Aralıklı Noktalar

Sayısal hesapta en sık kurduğumuz dizi, bir aralığa eşit adımlarla yayılmış noktalardır: sayısal integral, sayısal türev ve grafik çizimi hep böyle bir ızgara üzerinde yapılır. NumPy bunun için iki fonksiyon sunar; biri adımı, öteki nokta sayısını esas alır.

Tanım 5.3 (Adımla Verilen Izgara) np.arange(a, b, h), \(a\)’dan başlayıp \(h\) adımıyla ilerleyen ve \(b\)’ye varmadan duran

\[a,\ \ a + h,\ \ a + 2h,\ \ \dots,\ \ a + (N-1)h, \qquad N = \left\lceil \frac{b - a}{h} \right\rceil\]

dizisini verir. Bitiş noktası \(b\) dahil değildir. Tek argümanla yazılan np.arange(n) ise \(0, 1, \dots, n - 1\) dizisidir.

Yani arange, Python’ın range fonksiyonunun dizi döndüren ve ondalık adım da kabul eden biçimidir. Eleman sayısı \(N\), aralığın uzunluğunun adıma oranı yukarı yuvarlanarak bulunur.

Tanım 5.4 (Nokta Sayısıyla Verilen Izgara) np.linspace(a, b, N), \([a, b]\) aralığını \(N - 1\) eşit parçaya bölen

\[x_k = a + k\,h, \qquad h = \frac{b - a}{N - 1}, \qquad k = 0, 1, \dots, N - 1\]

noktalarını verir. İki uç nokta da dahildir.

Yani linspace ile adımı değil nokta sayısını seçeriz; adım kendiliğinden hesaplanır ve istenirse retstep=True argümanıyla geri alınır. Sayısal integralde ve grafik çiziminde uç noktaların da hesaba girmesi gerektiğinden çoğu zaman tercih edilen budur.

import numpy as np

print(np.arange(5))
print(np.arange(2, 11, 3))
print(np.arange(0, 1, 0.25))
print(np.linspace(0, 1, 5))
x, h = np.linspace(0, 1, 5, retstep=True)
print("adım:", h)

Çıktı:

[0 1 2 3 4]
[2 5 8]
[0.   0.25 0.5  0.75]
[0.   0.25 0.5  0.75 1.  ]
adım: 0.25

np.arange(2, 11, 3) 2’den başlayıp 3’er artar ve 11’e varmadan durur. Aynı \([0, 1]\) aralığı ve aynı \(0{,}25\) adımı için linspace beş, arange dört nokta verir; fark, bitiş noktasının alınıp alınmamasıdır. Aşağıdaki şekil bu iki ızgarayı ve hemen ardından ele alacağımız bir tuzağı gösteriyor.

np.linspace(0, 1, 5) 5 nokta, 1 dahil 0 0,25 0,5 0,75 1 np.arange(0, 1, 0.25) 4 nokta, 1 dahil değil 0 0,25 0,5 0,75 1 np.arange(1, 1.3, 0.1) 4 nokta, sonuncusu 1,3000000000000003 1 1,1 1,2 1,3
Üstte linspace nokta sayısıyla çalışır ve iki ucu da alır. Ortada arange aynı adımla 1'e varmadan durur. Altta ondalık adımlı arange: eleman sayısı (1,3 − 1)/0,1 oranının yukarı yuvarlanmasıyla belirlenir; bu oran yuvarlama hatası yüzünden 3 değil 3,0000000000000004 çıktığı için dördüncü bir nokta eklenir ve o nokta 1,3'ün biraz sağındadır.
UyarıOndalık adımla arange kullanmayın

arange eleman sayısını \((b - a)/h\) oranından hesaplar. \(0{,}1\) gibi ondalık adımlar ikili sistemde tam temsil edilemediği için (bkz. Nümerik Analiz) bu oran bir tamsayıya çok yakın ama ondan biraz büyük çıkabilir; o zaman yukarı yuvarlama fazladan bir nokta ekler.

import numpy as np

x = np.arange(1, 1.3, 0.1)
print(x)
print(x[-1])                 # son eleman 1.3'ten büyük
print((1.3 - 1) / 0.1)       # eleman sayısını belirleyen oran
print(np.linspace(1, 1.2, 3))

Çıktı:

[1.  1.1 1.2 1.3]
1.3000000000000003
3.0000000000000004
[1.  1.1 1.2]

\((1{,}3 - 1)/0{,}1\) oranı \(3\) yerine \(3{,}0000000000000004\) çıktığı için dizi dört elemanlıdır ve son eleman \(b = 1{,}3\) sınırının biraz sağındadır. Ondalık adım gerektiğinde linspace kullanıp nokta sayısını kendiniz verin; arange fonksiyonunu tamsayı adımlara saklayın.

Örnek 5.1 (Bir Polinomun Değer Tablosu) \(f(x) = x^3 - 2x\) polinomunun \([-2, 2]\) aralığındaki eşit aralıklı \(9\) noktada değer tablosunu çıkarınız ve tablodan \(f\)’nin köklerinin nerede olduğunu okuyunuz.

Çözüm

Dokuz nokta istendiği için np.linspace(-2, 2, 9) kullanırız; adım \(h = 4/8 = 0{,}5\)’tir. Bir dizi üzerine yazılan x**3 - 2 * x ifadesi her elemana ayrı ayrı uygulanır; bunun ayrıntılarını bir sonraki bölümde göreceğiz. zip iki diziyi eleman eleman eşler, f-string de sayıları hizalı basar.

import numpy as np

x = np.linspace(-2, 2, 9)
y = x**3 - 2 * x
for xk, yk in zip(x, y):
    print(f"{xk:6.2f} {yk:8.3f}")

Çıktı:

 -2.00   -4.000
 -1.50   -0.375
 -1.00    1.000
 -0.50    0.875
  0.00    0.000
  0.50   -0.875
  1.00   -1.000
  1.50    0.375
  2.00    4.000

\(f\) sürekli olduğundan işaret değiştirdiği her aralıkta bir kökü vardır (bkz. Nümerik Analiz). Tablo \(f(0) = 0\) değerini doğrudan gösteriyor. Ayrıca \(f\), \([-1{,}5;\ -1]\) aralığında \(-0{,}375\)’ten \(1\)’e, \([1;\ 1{,}5]\) aralığında da \(-1\)’den \(0{,}375\)’e geçerek işaret değiştiriyor. Gerçekten \(x^3 - 2x = x\,(x^2 - 2)\) olduğundan kökler \(0\) ve \(\pm\sqrt{2} \approx \pm 1{,}414\)’tür. Böyle bir tablo, ikiye bölme metoduna başlangıç aralığı seçmenin en basit yoludur. \(\blacksquare\)

5.4 Veri Türü

Bir dizinin bütün elemanları aynı türden olduğundan tür, tek tek elemanların değil dizinin bir özelliğidir ve dtype niteliğinde saklanır.

Tanım 5.5 (Veri Türü) Bir dizinin veri türü (dtype), elemanlarının bellekte nasıl kodlandığını ve her birinin kaç bayt tuttuğunu belirler. En sık kullanılanlar int64 (64 bitlik işaretli tamsayı), float64 (IEEE 754 çift duyarlıklı kayan noktalı sayı), complex128 (gerçel ve sanal kısmı birer float64 olan karmaşık sayı) ve bool (True ya da False) türleridir.

Yani float64 dizisinin her elemanı, Python’ın float türüyle aynı 64 bitlik makine sayısıdır (bkz. Nümerik Analiz). int64 ise Python’ın sınırsız int türünden farklıdır: yalnız \(-2^{63}\) ile \(2^{63} - 1\) arasındaki tamsayıları tutabilir.

np.array türü elemanlara bakarak kendisi seçer. Elemanlar karışık türdeyse hepsini taşıyabilen ortak türe yükseltir: tamsayılarla ondalık sayılar birlikte float64, bunlarla karmaşık sayılar birlikte complex128 olur. Tamsayıdan float64 türüne geçiş ancak mutlak değeri \(2^{53}\)’ü aşmayan tamsayılar için kesindir; daha büyük tamsayıların son basamakları yuvarlanabilir (bkz. Python ile İlk Adımlar). Türü dtype argümanıyla kendimiz de verebilir, var olan bir diziyi astype ile başka bir türe çevirebiliriz.

import numpy as np

a = np.array([1, 2, 3])
b = np.array([1, 2.5, 3])
c = np.array([1, 2 + 1j])
print(a, a.dtype)
print(b, b.dtype)
print(c, c.dtype)

d = np.array([1, 2, 3], dtype=float)
print(d, d.dtype)
print(b.astype(int))     # ondalık kısım atılır

Çıktı:

[1 2 3] int64
[1.  2.5 3. ] float64
[1.+0.j 2.+1.j] complex128
[1. 2. 3.] float64
[1 2 3]

astype yeni türde bir kopya üretir. Ondalıktan tamsayıya geçerken ondalık kısım yuvarlanmaz, atılır: \(2{,}5\) değeri \(2\) olur. Türlerin sınırlarını np.iinfo (tamsayılar) ve np.finfo (ondalık sayılar) verir.

import numpy as np

print(np.iinfo(np.int64).max)
print(np.finfo(np.float64).eps)
print(np.finfo(np.float64).max)

Çıktı:

9223372036854775807
2.220446049250313e-16
1.7976931348623157e+308

İlk sayı \(2^{63} - 1\), ikincisi float64 için makine epsilonu olan \(2^{-52}\) (bkz. Python ile İlk Adımlar), üçüncüsü de temsil edilebilen en büyük sayıdır (bkz. Nümerik Analiz).

UyarıTamsayı dizilerinin iki tuzağı

Taşma sessizdir. Python’ın int türü istendiği kadar büyüyebilir; ama int64 dizisindeki bir sonuç \(2^{63} - 1\)’i aşarsa NumPy hata vermez. Sonuç \(2^{64}\) modülüne göre sarılır ve anlamsız, hatta negatif bir sayı çıkar (bkz. Nümerik Analiz).

import numpy as np

a = np.array([10])
print(a**18, a**19)      # 10**19 int64'e sığmaz
print(10**19)            # Python int'i sınırsızdır

Çıktı:

[1000000000000000000] [-8446744073709551616]
10000000000000000000

Tamsayı dizisine yazılan ondalık sayı kesilir. Bir dizinin türü kurulurken belirlenir ve sonradan değişmez. Tamsayılarla kurulmuş bir diziye ondalık bir değer atanırsa değer sessizce tamsayıya kesilir.

import numpy as np

p = np.array([1, 2, 3])        # int64 dizisi
p[0] = 0.75                    # 0.75 sessizce 0'a kesilir
print(p)
q = np.array([1.0, 2, 3])      # float64 dizisi
q[0] = 0.75
print(q)

Çıktı:

[0 2 3]
[0.75 2.   3.  ]

Bir iterasyonun ara değerlerini saklayacağınız diziyi bu yüzden baştan ondalık kurun: np.zeros(n) ya da np.array([1.0, 2, 3]) gibi.

5.5 Şekil ve Yeniden Şekillendirme

Aynı elemanlar farklı şekillerde okunabilir: on iki sayı bir satır, \(3 \times 4\) bir matris ya da \(2 \times 2 \times 3\) bir tablo olabilir. Dizinin verisine dokunmadan şeklini değiştirmek NumPy’da çok sık başvurulan bir işlemdir.

Tanım 5.6 (Yeniden Şekillendirme) a.reshape(s), a dizisinin elemanlarını değiştirmeden onları yeni s şekliyle okuyan diziyi verir; bunun için s şeklindeki sayıların çarpımı a.size değerine eşit olmalıdır. Elemanlar indeks demetlerinin sözlük sırasıyla, yani satır öncelikli (row-major, C order) sırayla yerleşir: son indeks en hızlı, ilk indeks en yavaş değişir.

Yani np.arange(12).reshape(3, 4) önce ilk satırı 0, 1, 2, 3 ile, sonra ikinci satırı 4, 5, 6, 7 ile doldurur; tıpkı bir metni satır satır okur gibi. Şekil demetindeki sayılardan biri -1 yazılabilir; NumPy onu eleman sayısından kendisi hesaplar.

import numpy as np

a = np.arange(12)
A = a.reshape(3, 4)
print(A)
print(a.reshape(2, -1))     # -1 yerine 12 / 2 = 6 hesaplanır
T = a.reshape(2, 2, 3)
print(T.shape, T[1, 0, 2])
print(A.T)                  # transpoze, şekli (4, 3)
print(A.ravel())            # yeniden tek boyut

Çıktı:

[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]]
(2, 2, 3) 8
[[ 0  4  8]
 [ 1  5  9]
 [ 2  6 10]
 [ 3  7 11]]
[ 0  1  2  3  4  5  6  7  8  9 10 11]

A.T, \(A\) matrisinin transpozesidir (bkz. Lineer Cebir); ravel ise diziyi satır öncelikli sırayla yeniden tek boyuta indirir. Üç boyutlu T dizisinde T[1, 0, 2] elemanının 8 çıkmasının nedeni aşağıdaki önermedir.

Önerme 5.1 (Satır Öncelikli Sırada Konum) Şekli \((n_0, n_1, \dots, n_{d-1})\) olan bir dizinin \((i_0, i_1, \dots, i_{d-1})\) indeksli elemanı, satır öncelikli sırada (sayma 0’dan başlar)

\[ \begin{aligned} p &= i_0\, n_1 n_2 \cdots n_{d-1} + i_1\, n_2 \cdots n_{d-1} \\[1mm] &\quad + \cdots + i_{d-2}\, n_{d-1} + i_{d-1} \end{aligned} \]

konumundadır. Özel olarak \(m \times n\) bir matrisin \((i, j)\) elemanı \(p = i\,n + j\) konumundadır.

İspat

Bir elemanın konumu, sözlük sırasında ondan önce gelen indeks demetlerinin sayısıdır.

Önce matris durumunu görelim. \((i, j)\) demetinden önce, ilk indeksi \(0, 1, \dots, i - 1\) olan \(i\) tam satır gelir; her satırda \(n\) eleman olduğundan bunlar \(i\,n\) tanedir. İlk indeksi \(i\) olanlar arasında da \((i, 0), \dots, (i, j - 1)\) olmak üzere \(j\) demet \((i, j)\)’den önce gelir. Toplam \(p = i\,n + j\) eder.

Genel durumu \(d\) üzerinden tümevarımla kuralım. \(d = 1\) için \(p = i_0\)’dır. \(d \ge 2\) olsun. İlk indeksi \(i_0\)’dan küçük olan demetler \(i_0\) tane tam alt dizi oluşturur ve her biri \(n_1 n_2 \cdots n_{d-1}\) demet içerir. İlk indeksi \(i_0\) olan demetler ise şekli \((n_1, \dots, n_{d-1})\) olan bir alt diziyi oluşturur ve kendi aralarında yine sözlük sırasıyla dizilir. Tümevarım hipotezine göre \((i_1, \dots, i_{d-1})\) demetinden önce bu alt dizide

\[i_1\, n_2 \cdots n_{d-1} + \cdots + i_{d-2}\, n_{d-1} + i_{d-1}\]

demet vardır. İki sayının toplamı iddiadaki \(p\)’dir. \(\blacksquare\)

Bu konum bilgisi bellekte de aynen geçerlidir. NumPy her eksen için bir adım (stride) tutar: o eksende indeksi bir artırmak için bellekte kaç bayt ilerleneceği. 8 baytlık elemanlardan oluşan \(3 \times 4\) bir matriste bir sütun sağa gitmek 8, bir satır aşağı inmek \(4 \cdot 8 = 32\) bayttır. Böylece A[i, j] elemanı bloğun başından \(32\,i + 8\,j = 8\,(4i + j)\) bayt ötededir; bu, Önerme 5.1 ile bulunan konumun tam 8 katıdır.

import numpy as np

A = np.arange(12).reshape(3, 4)
print("strides:", A.strides)
i, j = 2, 1
print("konum:", i * 4 + j, "eleman:", A[i, j])
offset = i * A.strides[0] + j * A.strides[1]
print("bayt:", offset, "=", offset // A.itemsize, "eleman")

Çıktı:

strides: (32, 8)
konum: 9 eleman: 9
bayt: 72 = 9 eleman
bellekteki blok: 12 × 8 bayt 0 0 1 8 2 16 3 24 4 32 5 40 6 48 7 56 8 64 9 72 10 80 11 88 satır 0 satır 1 satır 2 A = np.arange(12).reshape(3, 4) 0 1 2 3 4 5 6 7 8 9 10 11 eksen 0 +32 bayt eksen 1: +8 bayt A[2, 1] = 9 2·32 + 1·8 = 72 bayt
np.arange(12).reshape(3, 4) dizisinin verisi bellekte tek bir bloktur; şekil yalnız bu bloğun nasıl okunacağını belirler. Satır öncelikli sırada önce 0. satırın dört elemanı, sonra 1. ve 2. satırınkiler gelir. Bir sütun sağa gitmek 8, bir satır aşağı inmek 4 · 8 = 32 bayt ilerlemektir; bu iki sayı dizinin strides değeridir ve A[2, 1] elemanı bloğun başından 2 · 32 + 1 · 8 = 72 bayt ötede başlar.

Bir boyutlu bir dizi ne satır ne de sütun matrisidir; şekli yalnızca (3,) olur ve transpozesi kendisidir. Lineer cebirdeki satır ve sütun matrisleri (bkz. Lineer Cebir) gerektiğinde reshape ile açıkça kurulur.

import numpy as np

x = np.array([5, 6, 0])
row = x.reshape(1, -1)
col = x.reshape(-1, 1)
print(x.shape, row.shape, col.shape)
print(col)
print(x.T.shape)        # 1B dizide transpoze bir şey değiştirmez

Çıktı:

(3,) (1, 3) (3, 1)
[[5]
 [6]
 [0]]
(3,)

Örnek 5.2 (Üç Boyutlu Bir Dizide Konum) T = np.arange(24).reshape(2, 3, 4) dizisinin T[1, 0, 2] elemanını, diziyi yazdırmadan, Önerme 5.1 yardımıyla bulunuz.

Çözüm

Şekil \((n_0, n_1, n_2) = (2, 3, 4)\), indeks de \((i_0, i_1, i_2) = (1, 0, 2)\)’dir. Önermeye göre

\[p = i_0\, n_1 n_2 + i_1\, n_2 + i_2 = 1 \cdot 12 + 0 \cdot 4 + 2 = 14\]

olur. np.arange(24) dizisinin 14 numaralı konumunda 14 sayısı durduğundan T[1, 0, 2] elemanı 14’tür. Kodla doğrulayalım; T[1], ilk indeksi 1 olan \(3 \times 4\) alt diziyi verir.

import numpy as np

T = np.arange(24).reshape(2, 3, 4)
n0, n1, n2 = T.shape
i0, i1, i2 = 1, 0, 2
p = i0 * n1 * n2 + i1 * n2 + i2
print("formül:", p, " dizi:", T[i0, i1, i2])
print(T[1])

Çıktı:

formül: 14  dizi: 14
[[12 13 14 15]
 [16 17 18 19]
 [20 21 22 23]]

14, bu alt dizinin ilk satırının üçüncü elemanıdır. \(\blacksquare\)

5.6 İndeksleme ve Dilimleme

Listelerde bir elemana x[i] ile, bir parçaya x[başla:dur:adım] ile ulaşıyorduk (Veri Yapıları ve Comprehension). NumPy aynı sözdizimini her eksen için ayrı ayrı, virgülle ayırarak kullanır.

import numpy as np

A = np.arange(20).reshape(4, 5)
print(A)
print("A[2, 3]  =", A[2, 3])
print("A[-1, 0] =", A[-1, 0])    # son satır, ilk sütun
print("A[1]     =", A[1])        # ikinci satırın tamamı
print("A[:, 2]  =", A[:, 2])     # üçüncü sütunun tamamı

Çıktı:

[[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]
 [15 16 17 18 19]]
A[2, 3]  = 13
A[-1, 0] = 15
A[1]     = [5 6 7 8 9]
A[:, 2]  = [ 2  7 12 17]

A[2, 3] üçüncü satırın dördüncü elemanıdır, yani matematikteki \(a_{34}\). Negatif indeksler sondan sayar. Tek bir indeks verilirse ilk eksen seçilir: A[1] ikinci satırın tamamıdır. A[:, 2] yazımındaki : işareti “bu eksende hepsi” demektir. A[2][3] yazımı da aynı elemanı verir, ama önce bütün satırı ayırıp sonra içinden seçtiği için A[2, 3] tercih edilir.

Tanım 5.7 (Çok Boyutlu Dilimleme) \(d\) boyutlu bir dizi, köşeli parantez içinde virgülle ayrılmış en çok \(d\) indeksle seçilir. \(k\)-ıncı yerdeki indeks ya bir tamsayı ya da başla:dur:adım biçiminde bir dilimdir (slice). Tamsayı, \(k\) ekseninde tek bir konumu seçer ve bu ekseni sonuçtan kaldırır. Dilim, \(k\) ekseninde başla, başla + adım, … konumlarını dur değerine varmadan seçer ve ekseni korur. Yazılmayan sondaki eksenler için : (hepsi) varsayılır.

Yani tamsayı indeks boyutu düşürür, dilim düşürmez: A[1, :] (kısaca A[1]) ikinci satırı şekli (5,) olan bir boyutlu dizi olarak, A[1:2, :] ise aynı satırı şekli (1, 5) olan bir matris olarak verir. Dilimin sınırları listelerdeki gibidir: başla dahil, dur hariçtir; yazılmayan başla 0, dur eksenin sonu, adım 1 demektir. İki boyutlu bir dilim, satırları ve sütunları eşit aralıklarla seçilmiş bir alt matristir (bkz. Lineer Cebir).

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 A[1] şekil (5,) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 A[:, 2] şekil (4,) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 A[1:3, 1:4] şekil (2, 3) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 A[::2, ::2] şekil (2, 3)
A = np.arange(20).reshape(4, 5) dizisinden dört seçim. Tek bir tamsayı indeks (A[1], A[:, 2]) o ekseni ortadan kaldırır ve bir boyutlu bir dizi verir; dilimler eksenleri korur. A[::2, ::2] her iki eksende ikişer atlayarak seçer.
import numpy as np

A = np.arange(20).reshape(4, 5)
print(A[1:3, 1:4])
print(A[::2, ::2])
print(A[1, :].shape, A[1:2, :].shape)
print(A[::-1, 0])          # ilk sütun, ters sırada

Çıktı:

[[ 6  7  8]
 [11 12 13]]
[[ 0  2  4]
 [10 12 14]]
(5,) (1, 5)
[15 10  5  0]

Negatif adım sırayı ters çevirir: A[::-1, 0] ilk sütunu aşağıdan yukarıya okur. Dilimlere atama da yapılabilir: A[0, :] = 0 ilk satırı sıfırlar, A[:, -1] = [1, 2, 3, 4] son sütunu verilen değerlerle doldurur. Atanan tek bir sayıysa seçilen her konuma yazılır.

Örnek 5.3 (Satranç Tahtası Deseni) \(8 \times 8\) bir tamsayı matrisinde \(i + j\) toplamı tek olan \((i, j)\) konumlarına 1, ötekilere 0 yazınız (indeksler 0’dan başlar). Matrisi eleman eleman dolaşan bir döngüyle değil, iki dilim atamasıyla kurunuz.

Çözüm

\(i + j\) toplamının tek olması için indekslerden biri çift, öteki tek olmalıdır. Çift indeksler ::2 dilimiyle (0, 2, 4, 6), tek indeksler 1::2 dilimiyle (1, 3, 5, 7) seçilir. Dolayısıyla 1 yazılacak konumlar iki parçadır: çift satırların tek sütunları ve tek satırların çift sütunları.

import numpy as np

Z = np.zeros((8, 8), dtype=int)
Z[::2, 1::2] = 1     # çift satır, tek sütun
Z[1::2, ::2] = 1     # tek satır, çift sütun
print(Z)

Çıktı:

[[0 1 0 1 0 1 0 1]
 [1 0 1 0 1 0 1 0]
 [0 1 0 1 0 1 0 1]
 [1 0 1 0 1 0 1 0]
 [0 1 0 1 0 1 0 1]
 [1 0 1 0 1 0 1 0]
 [0 1 0 1 0 1 0 1]
 [1 0 1 0 1 0 1 0]]

Sol üst köşedeki \((0, 0)\) konumunda \(0 + 0 = 0\) çift olduğundan 0, onun sağındaki \((0, 1)\) konumunda 1 durur. \(\blacksquare\)

Örnek 5.4 (Bir Matrisi Bloklara Ayırmak) \(M\), satır satır \(1, 2, \dots, 16\) sayılarıyla doldurulmuş \(4 \times 4\) matris olsun. \(M\) matrisini dört tane \(2 \times 2\) bloğa ayırınız:

\[M = \begin{pmatrix} M_{11} & M_{12} \\ M_{21} & M_{22} \end{pmatrix}\]

Çözüm

Blok matris kavramı için bkz. Lineer Cebir. Satırlar :2 (0 ve 1) ile 2: (2 ve 3) dilimlerine, sütunlar da aynı biçimde ikiye ayrılır; her blok bir satır diliminin ve bir sütun diliminin kesişimidir.

import numpy as np

M = np.arange(1, 17).reshape(4, 4)
M11, M12 = M[:2, :2], M[:2, 2:]
M21, M22 = M[2:, :2], M[2:, 2:]
print(M)
print(M11)
print(M12)
print(M21)
print(M22)

Çıktı:

[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]
 [13 14 15 16]]
[[1 2]
 [5 6]]
[[3 4]
 [7 8]]
[[ 9 10]
 [13 14]]
[[11 12]
 [15 16]]

Örneğin \(M_{12}\), ilk iki satırın son iki sütunudur: \(\begin{pmatrix} 3 & 4 \\ 7 & 8 \end{pmatrix}\). Bu bloklar kurulurken hiçbir sayı kopyalanmaz; bunun ne demek olduğunu bir sonraki kısımda görüyoruz. \(\blacksquare\)

5.7 Görünüm ve Kopya

Dilimleme hızlıdır, çünkü NumPy seçilen elemanları kopyalamaz; yeni dizi eski dizinin belleğinin bir bölümüne bakar. Bu, listelerden alıştığımız davranışın tersidir ve dikkat ister.

import numpy as np

a = np.arange(6)
s = a[1:4]          # dizi dilimi: görünüm
s[0] = 100
print("a   =", a)

lst = list(range(6))
t = lst[1:4]        # liste dilimi: kopya
t[0] = 100
print("lst =", lst)

Çıktı:

a   = [  0 100   2   3   4   5]
lst = [0, 1, 2, 3, 4, 5]

Liste dilimi yeni bir liste kurduğu için lst değişmedi. Dizi dilimi s ise a ile aynı belleği paylaşır; bu yüzden s[0] = 100 ataması doğrudan a[1] elemanını değiştirdi.

Tanım 5.8 (Görünüm ve Kopya) Bir dizinin görünümü (view), kendi verisi olmayan ve başka bir dizinin belleğinin bir bölümünü kendi şekli ve adımlarıyla okuyan dizidir. Kopya (copy) ise verisi yeni bir bellek bloğuna yazılmış, öncekinden bağımsız dizidir.

Yani bir görünüm üzerinden yapılan atama asıl diziyi de değiştirir, bir kopya üzerinden yapılan değiştirmez. Temel dilimleme, .T ve mümkün olduğunda reshape ile ravel görünüm verir; copy(), astype ve aşağıda göreceğimiz fancy ve boolean indeksleme ise kopya üretir.

a = np.arange(6) 0 1 2 3 4 5 0 1 2 3 4 5 s = a[1:4]: görünüm, aynı bellek 1 2 3 c = a[1:4].copy() kopya: yeni bellek
s = a[1:4] kendi verisi olmayan bir görünümdür: a'nın 1, 2 ve 3 numaralı hücrelerine bakar, bu yüzden s[0] = 100 ataması a[1]'i değiştirir. c = a[1:4].copy() ise aynı değerleri yeni bir bloğa yazar; c üzerinde yapılan değişiklik a'ya ulaşmaz.

İki dizinin bellek paylaşıp paylaşmadığını np.shares_memory söyler. Bir görünümün base niteliği de verisini ödünç aldığı diziyi gösterir; kendi verisi olan bir dizide bu nitelik None olur.

import numpy as np

a = np.arange(6)
s = a[1:4]
c = a[1:4].copy()
print(np.shares_memory(a, s), s.base is a)
print(np.shares_memory(a, c), c.base is None)
c[0] = -1
print("a =", a, " c =", c)

Çıktı:

True True
False True
a = [0 1 2 3 4 5]  c = [-1  2  3]
UyarıEski değeri saklamak için copy kullanın

Listelerde x[:] bir kopya verir ve eski değeri saklamanın alışılmış yoludur. NumPy dizisinde ise x[:] bütün diziye bakan bir görünümdür; dizi yerinde güncellenince “eski” değer de değişir.

import numpy as np

x = np.array([1.0, 2.0, 3.0])
x_old = x[:]           # listelerdeki gibi kopya sanılır
x[:] = x / 2           # yerinde güncelleme
print(x_old)           # x_old da değişti

y = np.array([1.0, 2.0, 3.0])
y_old = y.copy()       # gerçek kopya
y[:] = y / 2
print(y_old)

Çıktı:

[0.5 1.  1.5]
[1. 2. 3.]

İterasyonlarda bir önceki adımın değerini x_old = x.copy() ile saklayın. x = x / 2 gibi bir atama yeni bir dizi kurup x adını ona bağlar ve eski diziye dokunmaz; tehlike, x[:] = ... ya da x += ... gibi yerinde güncellemelerdedir.

Örnek 5.5 (Transpoze Bir Görünümdür) A = np.zeros((2, 3), dtype=int) ve B = A.T olsun. B[2, 0] = 5 atamasından sonra A matrisi ne olur?

Çözüm

A.T veriyi kopyalamaz; aynı belleği eksenlerin yerini değiştirerek okur. Yani B[i, j] ile A[j, i] aynı bellek hücresidir. Dolayısıyla B[2, 0] = 5 ataması A[0, 2] elemanını 5 yapar.

import numpy as np

A = np.zeros((2, 3), dtype=int)
B = A.T
B[2, 0] = 5
print(A)
print(np.shares_memory(A, B))

Çıktı:

[[0 0 5]
 [0 0 0]]
True

Matematikte \(B = A^{t}\) yazıp \(B\) matrisini değiştirmek \(A\)’yı etkilemez, çünkü \(B\) yeni bir matristir. NumPy’da bağımsız bir transpoze gerekiyorsa A.T.copy() yazılır. \(\blacksquare\)

5.8 Fancy İndeksleme

Dilimler yalnızca eşit aralıklı konumları seçebilir. Konumları istediğimiz sırada, gerekirse tekrarlayarak seçmek için indeks olarak bir tamsayı listesi ya da dizisi veririz.

Tanım 5.9 (Tamsayı Dizisiyle İndeksleme) x bir boyutlu bir dizi, idx de tamsayılardan oluşan bir dizi ya da liste olsun. x[idx], \(k\)-ıncı elemanı x[idx[k]] olan ve şekli idx ile aynı olan yeni bir dizidir. Bu seçime fancy indeksleme (fancy indexing) denir.

Yani x konumlardan değerlere, idx de konumlardan konumlara giden birer fonksiyon olarak düşünülürse x[idx] dizisi \(x \circ \mathrm{idx}\) bileşkesidir. İndeksler tekrar edebilir, sıraları serbesttir ve sonuç her zaman bir kopyadır.

x 10 20 30 40 50 0 1 2 3 4 konum x[idx] 40 10 40 20 3 0 3 1 idx
x[idx] ile idx = [3, 0, 3, 1]: sonucun k-ıncı elemanı x[idx[k]]'dir. İndeksler istenen sırada verilebilir ve tekrar edebilir; 3 numaralı konum iki kez seçilmiştir. Sonuç, x'ten bağımsız yeni bir dizidir.
import numpy as np

x = np.array([10, 20, 30, 40, 50])
idx = np.array([3, 0, 3, 1])
print(x[idx])
print(x[[0, -1]])                # ilk ve son eleman

A = np.arange(12).reshape(3, 4)
print(A[[2, 0]])                 # üçüncü ve birinci satır
print(A[[0, 1, 2], [3, 2, 1]])   # (0,3), (1,2), (2,1) elemanları

Çıktı:

[40 10 40 20]
[10 50]
[[ 8  9 10 11]
 [ 0  1  2  3]]
[3 6 9]

İki boyutlu bir dizide tek bir indeks listesi satırları seçer: A[[2, 0]] üçüncü ve birinci satırları bu sırayla dizer. İki indeks listesi verilirse NumPy onları çift çift eşler: A[[0, 1, 2], [3, 2, 1]] bir alt matris değil, \((0, 3)\), \((1, 2)\) ve \((2, 1)\) konumlarındaki üç elemandır. Seçilen satırlarla sütunların bütün kesişimlerinden oluşan alt matris için np.ix_ kullanılır.

Örnek 5.6 (Bir Alt Matrisin Seçilmesi) Aşağıdaki \(A\) matrisinde ikinci satırı ve ikinci ile dördüncü sütunları silerek elde edilen alt matrisi (bkz. Lineer Cebir) NumPy ile bulunuz.

\[A = \begin{pmatrix} 1 & 2 & 0 & 4 \\ -5 & 1 & 3 & 0 \\ 0 & 2 & 1 & 7 \end{pmatrix}\]

Çözüm

Silinmeyen satırlar birinci ve üçüncü, silinmeyen sütunlar da birinci ve üçüncüdür; 0’dan sayınca satır indeksleri [0, 2], sütun indeksleri de [0, 2] olur. np.ix_(rows, cols) seçilen satırlarla sütunların bütün kesişimlerini alır. Aynı sonuç önce satırları, sonra sütunları seçerek iki adımda da bulunur.

import numpy as np

A = np.array([[1, 2, 0, 4],
              [-5, 1, 3, 0],
              [0, 2, 1, 7]])
rows = [0, 2]           # kalan satırlar
cols = [0, 2]           # kalan sütunlar
print(A[np.ix_(rows, cols)])
print(A[rows][:, cols])     # aynı sonuç, iki adımda
print(A[rows, cols])        # dikkat: yalnız (0,0) ve (2,2)

Çıktı:

[[1 0]
 [0 1]]
[[1 0]
 [0 1]]
[1 1]

Alt matris \(\begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}\)’dir. Bu örnekte A[::2, ::2] dilimi de aynı sonucu verirdi; ama silinen satır ve sütunlar düzensiz olduğunda fancy indeksleme gerekir. Son satır tipik bir hatayı gösteriyor: A[rows, cols] indeksleri çift çift eşlediği için yalnız \((0, 0)\) ve \((2, 2)\) konumlarındaki elemanları verir. \(\blacksquare\)

5.9 Boolean İndeksleme

Çoğu zaman elemanları konumlarına göre değil bir koşula göre seçmek isteriz: pozitif olanlar, 3’e bölünenler, bir aralığa düşenler. Bir dizinin bir sayıyla karşılaştırılması her eleman için ayrı bir True ya da False değeri üretir ve bu bool dizisi bir indeks olarak kullanılabilir.

import numpy as np

x = np.arange(1, 13)
mask = x % 3 == 0
print(mask)
print(x[mask])
print(x[(x > 4) & (x % 2 == 1)])    # 4'ten büyük tekler
print(x[~mask])                     # 3'e bölünmeyenler
print("kaç tane:", mask.sum())

Çıktı:

[False False  True False False  True False False  True False False  True]
[ 3  6  9 12]
[ 5  7  9 11]
[ 1  2  4  5  7  8 10 11]
kaç tane: 4

Tanım 5.10 (Maske ve Boolean İndeksleme) x ile aynı şekilde ve bool türünde bir m dizisine maske (mask) denir. x[m], x dizisinin m maskesinin True olduğu konumlardaki elemanlarını satır öncelikli sırayla toplayan, bir boyutlu yeni bir dizidir. Maskeye atama, yani x[m] = c, yalnız bu konumları değiştirir.

Yani maske bir alt kümeyi gösterir: \(S = \{k : m_k = \text{True}\}\) ise x[m] dizisi \(x_k\) (\(k \in S\)) elemanlarını sırayla verir. Koşullar & (ve), | (veya) ve ~ (değil) işleçleriyle birleştirilir ve her koşul paranteze alınır, çünkü & ile | karşılaştırmalardan önce uygulanır. True değeri 1, False değeri 0 sayıldığından mask.sum() koşulu sağlayan eleman sayısını verir.

Uyarıand, or ve not dizilerle çalışmaz

Python’ın and, or ve not sözcükleri tek bir doğruluk değeri bekler. Bir diziye uygulandıklarında NumPy dizinin bütün olarak doğru mu yanlış mı sayılacağına karar veremez ve hata verir.

import numpy as np

x = np.arange(1, 13)
try:
    print(x[x > 4 and x < 9])
except ValueError as e:
    print("Hata:", str(e).split(".")[0])   # mesajın ilk cümlesi
print(x[(x > 4) & (x < 9)])

Çıktı:

Hata: The truth value of an array with more than one element is ambiguous
[5 6 7 8]

Dizilerde her zaman &, |, ~ işleçlerini ve parantezi kullanın.

Maskeye atama, bir koşulu sağlayan elemanları topluca değiştirmenin en kısa yoludur. Aşağıda \(-5\) ile \(5\) arasında rastgele tamsayılar üretip negatif olanları sıfırlıyoruz. np.random.default_rng(1) sabit bir tohumla (seed) bir rastgele sayı üreteci kurar; böylece kod her çalıştığında aynı sayılar çıkar. Ayrıntıları Rastgele Sayılar ve Monte Carlo Yöntemleri bölümünde göreceğiz. rng.integers(-5, 6, size=10) çağrısı, üst sınır hariç olmak üzere 10 tamsayı üretir.

import numpy as np

rng = np.random.default_rng(1)
y = rng.integers(-5, 6, size=10)
print(y)
y[y < 0] = 0          # negatifleri sıfırla
print(y)

Çıktı:

[ 0  0  3  5 -5 -4  4  5 -3 -2]
[0 0 3 5 0 0 4 5 0 0]

Örnek 5.7 (Eratosthenes Kalburu ile Asallar) Veri Yapıları ve Comprehension bölümünde listeyle kurduğumuz Eratosthenes kalburunu bu kez bir bool dizisi üzerinde kurarak \(100\)’e kadar olan asal sayıları bulunuz. Aşağıdaki şekil, çözümdeki kodun ürettiği maskeyi \(10 \times 10\) bir tablo olarak gösteriyor.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
Kalburdan sonra is_prime[1:].reshape(10, 10) maskesi: 1'den 100'e kadar sayılar satır satır dizilmiş, maskenin True olduğu 25 asal vurgulanmıştır. 2 ve 5 dışındaki asalların hepsi 1, 3, 7 ya da 9 ile biten sütunlarda durur.
Çözüm

Algoritma liste sürümündekiyle aynıdır: \(p^2 \le N\) olan her asal \(p\) için \(p^2,\ p^2 + p,\ p^2 + 2p, \dots\) konumları elenir. NumPy üç yerde işi kısaltır. Birincisi, is_prime bir liste değil, np.ones(N + 1, dtype=bool) ile kurulan ve bütün elemanları True olan bir bool dizisidir. İkincisi, dilime tek bir değer atanabildiğinden is_prime[p * p::p] = False yazmak yeter; sağ tarafta dilimle aynı uzunlukta bir liste kurmak gerekmez. Üçüncüsü, asallar bir comprehension ile toplanmaz; np.arange(N + 1) dizisinden doğrudan maskeyle seçilir.

import numpy as np

N = 100
is_prime = np.ones(N + 1, dtype=bool)
is_prime[:2] = False                 # 0 ve 1 asal değil
for p in range(2, int(N**0.5) + 1):
    if is_prime[p]:
        is_prime[p * p::p] = False   # p*p, p*p + p, ... elenir
primes = np.arange(N + 1)[is_prime]
print(primes)
print("asal sayısı:", primes.size)
print(is_prime[1:].reshape(10, 10).astype(int))

Çıktı:

[ 2  3  5  7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89
 97]
asal sayısı: 25
[[0 1 1 0 1 0 1 0 0 0]
 [1 0 1 0 0 0 1 0 1 0]
 [0 0 1 0 0 0 0 0 1 0]
 [1 0 0 0 0 0 1 0 0 0]
 [1 0 1 0 0 0 1 0 0 0]
 [0 0 1 0 0 0 0 0 1 0]
 [1 0 0 0 0 0 1 0 0 0]
 [1 0 1 0 0 0 0 0 1 0]
 [0 0 1 0 0 0 0 0 1 0]
 [0 0 0 0 0 0 1 0 0 0]]

Liste sürümüyle aynı \(25\) asal bulunur. Son çıktı, maskenin 1’den 100’e kadar olan kısmının satır satır \(10 \times 10\) okunuşudur; şekildeki tablo da budur. \(\blacksquare\)

5.10 Dizileri Birleştirme

Büyük dizileri çoğu zaman küçük parçalardan kurarız: vektörleri bir matrisin satırları ya da sütunları olarak yan yana koyar, blok matrisleri bloklarından birleştiririz.

Tanım 5.11 (Bir Eksen Boyunca Birleştirme) np.concatenate([A, B, ...], axis=k), dizileri \(k\) ekseni boyunca uç uca ekler. Dizilerin şekilleri \(k\) dışındaki bütün eksenlerde aynı olmalıdır; sonucun \(k\) eksenindeki uzunluğu, parçaların bu eksendeki uzunluklarının toplamıdır.

Yani iki \(2 \times 3\) matris axis=0 ile alt alta konup \(4 \times 3\), axis=1 ile yan yana konup \(2 \times 6\) bir matris verir. Bu iki durumun kısa yolları np.vstack (dikey yığma) ve np.hstack (yatay yığma) fonksiyonlarıdır. np.stack ise parçaları yeni bir eksen boyunca üst üste koyar: iki \(2 \times 3\) matristen şekli (2, 2, 3) olan bir dizi çıkar.

1 2 3 4 5 6 7 8 9 10 11 12 A B 1 2 3 4 5 6 7 8 9 10 11 12 np.vstack([A, B]) şekil (4, 3) eksen 0 1 2 3 7 8 9 4 5 6 10 11 12 np.hstack([A, B]) eksen 1 şekil (2, 6)
İki 2 × 3 dizi. np.vstack onları 0 ekseni boyunca alt alta koyar ve 4 × 3 bir dizi verir; np.hstack 1 ekseni boyunca yan yana koyar ve 2 × 6 bir dizi verir. Birleştirilen eksen dışındaki uzunluklar aynı olmalıdır.
import numpy as np

A = np.array([[1, 2, 3],
              [4, 5, 6]])
B = np.array([[7, 8, 9],
              [10, 11, 12]])
print(np.vstack([A, B]))
print(np.hstack([A, B]))
print(np.concatenate([A, B], axis=1).shape)
print(np.stack([A, B]).shape)       # yeni eksen: (2, 2, 3)

Çıktı:

[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]
[[ 1  2  3  7  8  9]
 [ 4  5  6 10 11 12]]
(2, 6)
(2, 2, 3)

Bir boyutlu vektörlerle çalışırken np.column_stack vektörleri sütun, np.vstack satır olarak dizer. Birleştirmenin tersi np.split fonksiyonudur: diziyi verilen konumlardan keserek parçalarının listesini verir.

import numpy as np

x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
print(np.column_stack([x, y]))
print(np.vstack([x, y]))
print(np.split(np.arange(10), [3, 7]))

Çıktı:

[[1 4]
 [2 5]
 [3 6]]
[[1 2 3]
 [4 5 6]]
[array([0, 1, 2]), array([3, 4, 5, 6]), array([7, 8, 9])]

np.split(np.arange(10), [3, 7]) diziyi 3 ve 7 numaralı konumlardan keser; parçalar [0:3], [3:7] ve [7:10] dilimleridir.

Örnek 5.8 (Vandermonde Matrisi) \(x = (1, 2, 3, 4)\) noktaları için \((i, j)\) bileşeni \(x_i^{\,j}\) olan (\(i, j = 0, 1, 2, 3\)) \(4 \times 4\) Vandermonde matrisini sütunlarını yan yana koyarak kurunuz.

Çözüm

Matrisin sütunları \(x\) vektörünün kuvvetleridir: \(x^0 = (1, 1, 1, 1)\), \(x\), \(x^2\) ve \(x^3\). Bir dizinin kuvveti her elemana ayrı uygulandığından x**2 gibi ifadeler bu sütunları doğrudan verir; np.column_stack da onları sütun olarak dizer. Sonucu NumPy’ın hazır np.vander fonksiyonuyla karşılaştırıyoruz; increasing=True kuvvetlerin artan sırada dizilmesini ister. Karşılaştırmayı yapan np.array_equal, iki dizinin şekli ve bütün elemanları aynıysa True verir.

import numpy as np

x = np.array([1, 2, 3, 4])
V = np.column_stack([x**0, x, x**2, x**3])
print(V)
print(np.array_equal(V, np.vander(x, increasing=True)))

Çıktı:

[[ 1  1  1  1]
 [ 1  2  4  8]
 [ 1  3  9 27]
 [ 1  4 16 64]]
True

Bu matris, verilen noktalardan geçen polinomu bulma probleminin katsayı matrisidir ve Polinomlar, İnterpolasyon ve Eğri Uydurma bölümünde yeniden karşımıza çıkacak. \(\blacksquare\)

Örnek 5.9 (Blok Matrisin Transpozesi) Örnek 5.4 örneğindeki \(4 \times 4\) matris \(M\) ve blokları verilsin. np.block ile

\[M^{t} = \begin{pmatrix} M_{11}^{t} & M_{21}^{t} \\ M_{12}^{t} & M_{22}^{t} \end{pmatrix}\]

eşitliğini doğrulayınız.

Çözüm

np.block, iç içe bir listeyle verilen blokları blok matris düzeninde birleştirir; iç listeler blok satırlarıdır. Önce bloklardan \(M\) matrisinin kendisinin geri geldiğini, sonra transpoze bloklarla kurulan matrisin M.T ile aynı olduğunu np.array_equal ile kontrol ediyoruz.

import numpy as np

M = np.arange(1, 17).reshape(4, 4)
M11, M12 = M[:2, :2], M[:2, 2:]
M21, M22 = M[2:, :2], M[2:, 2:]
N = np.block([[M11, M12], [M21, M22]])
print(np.array_equal(N, M))
MT = np.block([[M11.T, M21.T], [M12.T, M22.T]])
print(np.array_equal(MT, M.T))
print(MT)

Çıktı:

True
True
[[ 1  5  9 13]
 [ 2  6 10 14]
 [ 3  7 11 15]
 [ 4  8 12 16]]

İki karşılaştırma da True verir. Kuralın nedeni şudur: transpoze, \((i, j)\) konumundaki elemanı \((j, i)\) konumuna taşır. Bu, blokların yerini köşegene göre değiştirir (\(M_{12}\) ile \(M_{21}\) yer değiştirir) ve her bloğun kendi içinde de transpozesini alır. \(\blacksquare\)

5.11 Alıştırmalar

Aşağıdaki alıştırmaların her birinin çözümü, kodu ve çalıştırılmış çıktısıyla birlikte verilmiştir; önce kendiniz deneyip sonra karşılaştırın.

Alıştırma 5.1 (Esas Köşegen ve İz) A = np.arange(1, 26).reshape(5, 5) matrisinin esas köşegenini fancy indekslemeyle seçiniz ve köşegen elemanlarının toplamı olan izi hesaplayınız.

Çözüm

Adım 1. Esas köşegen (bkz. Lineer Cebir) \((0, 0), (1, 1), \dots, (4, 4)\) konumlarıdır. İki indeks dizisi çift çift eşlendiğinden satır ve sütun indeksi olarak aynı k = np.arange(5) dizisini vermek tam bu konumları seçer.

Adım 2. İz, seçilen elemanların toplamıdır; dizilerin sum metodu elemanları toplar.

Adım 3. Sonucu NumPy’ın hazır fonksiyonlarıyla karşılaştırırız: np.diag bir matrise uygulandığında köşegenini çıkarır, np.trace izi verir.

import numpy as np

A = np.arange(1, 26).reshape(5, 5)
k = np.arange(5)
d = A[k, k]                  # (0,0), (1,1), ..., (4,4)
print(d)
print("iz:", d.sum())
print(np.array_equal(d, np.diag(A)), np.trace(A))

Çıktı:

[ 1  7 13 19 25]
iz: 65
True 65

Elle de kontrol edebiliriz: köşegen elemanları \(a_{kk} = 5k + k + 1 = 6k + 1\) (\(k = 0, \dots, 4\)) olduğundan iz \(1 + 7 + 13 + 19 + 25 = 65\)’tir. \(\blacksquare\)

Alıştırma 5.2 (Çerçeve Matrisi) Kenarlarındaki elemanları 1, içindeki elemanları 0 olan \(6 \times 6\) bir tamsayı matrisini tek bir dilim atamasıyla kurunuz ve matristeki 1’lerin sayısını bulunuz.

Çözüm

Adım 1. Bütün elemanları 1 olan np.ones((6, 6), dtype=int) matrisiyle başlarız.

Adım 2. İç kısım, ilk ve son satır ile ilk ve son sütun dışında kalan \(4 \times 4\) bloktur. Her iki eksende 1:-1 dilimi ilk ve son konumu dışarıda bırakır; bu bloğa 0 atarız.

Adım 3. 1’lerin sayısını elemanların toplamı verir.

import numpy as np

Z = np.ones((6, 6), dtype=int)
Z[1:-1, 1:-1] = 0        # içteki 4 x 4 blok
print(Z)
print("birlerin sayısı:", Z.sum())

Çıktı:

[[1 1 1 1 1 1]
 [1 0 0 0 0 1]
 [1 0 0 0 0 1]
 [1 0 0 0 0 1]
 [1 0 0 0 0 1]
 [1 1 1 1 1 1]]
birlerin sayısı: 20

Sayım elle de yapılabilir: \(36\) elemandan \(4 \cdot 4 = 16\) tanesi iç kısımdadır, geriye \(36 - 16 = 20\) eleman kalır. \(\blacksquare\)

Alıştırma 5.3 (Ondalık Adımın Eleman Sayısı) np.arange(0.1, 0.4, 0.1) çağrısının kaç eleman verdiğini Tanım 5.3 tanımındaki formülle açıklayınız.

Çözüm

Adım 1. Tam aritmetikte \(N = \lceil (0{,}4 - 0{,}1)/0{,}1 \rceil = \lceil 3 \rceil = 3\) olur ve dizi \(0{,}1;\ 0{,}2;\ 0{,}3\) noktalarından oluşur; \(0{,}4\) dahil değildir.

Adım 2. Makinede ise \(0{,}1\) ve \(0{,}4\) tam temsil edilemez. Çıkarma ve bölme yuvarlama hatası taşır ve oran \(3\) değil, ondan biraz büyük bir sayı çıkar. Yukarı yuvarlama bu durumda \(N = 4\) verir.

Adım 3. Kodla görelim; aynı aralıktaki üç noktayı güvenle veren linspace çağrısını da ekliyoruz.

import numpy as np

x = np.arange(0.1, 0.4, 0.1)
print(x, x.size)
print((0.4 - 0.1) / 0.1)
print(np.linspace(0.1, 0.3, 3))

Çıktı:

[0.1 0.2 0.3 0.4] 4
3.0000000000000004
[0.1 0.2 0.3]

Oran \(3{,}0000000000000004\) çıktığı için dizi dört elemanlıdır ve dışarıda kalması gereken \(0{,}4\) de diziye girer. np.linspace(0.1, 0.3, 3) ise nokta sayısını doğrudan verdiğimiz için bu sorunu yaşamaz. \(\blacksquare\)

Alıştırma 5.4 (Üç Boyutlu Bir Dizide İndeks) T = np.arange(60).reshape(3, 4, 5) dizisinde 37 sayısı hangi \((i_0, i_1, i_2)\) indeksinde durur?

Çözüm

Adım 1. np.arange(60) dizisinde 37 sayısı 37 numaralı konumdadır. Önerme 5.1 gereği şekil \((3, 4, 5)\) iken

\[37 = 20\, i_0 + 5\, i_1 + i_2, \qquad 0 \le i_1 < 4,\quad 0 \le i_2 < 5\]

olmalıdır.

Adım 2. \(5\, i_1 + i_2 \le 5 \cdot 3 + 4 = 19 < 20\) olduğundan \(i_0\), 37’nin 20’ye bölümüdür: \(37 = 20 \cdot 1 + 17\), yani \(i_0 = 1\).

Adım 3. Kalan \(17 = 5\, i_1 + i_2\) ve \(0 \le i_2 < 5\) olduğundan \(i_1\), 17’nin 5’e bölümüdür: \(17 = 5 \cdot 3 + 2\), yani \(i_1 = 3\) ve \(i_2 = 2\).

Adım 4. Bölme adımlarını Python’ın divmod fonksiyonu (bölüm ve kalanı birlikte verir) ile yapıp sonucu doğrularız. NumPy’ın np.unravel_index fonksiyonu da tam bu dönüşümü yapar; çıktısını okunaklı olsun diye Python tamsayılarına çeviriyoruz.

import numpy as np

T = np.arange(60).reshape(3, 4, 5)
i0, r = divmod(37, 4 * 5)
i1, i2 = divmod(r, 5)
print(i0, i1, i2)
print(T[i0, i1, i2])
print([int(t) for t in np.unravel_index(37, T.shape)])

Çıktı:

1 3 2
37
[1, 3, 2]

37 sayısı T[1, 3, 2] konumundadır. \(\blacksquare\)

Alıştırma 5.5 (Görünüm mü, Kopya mı?) a = np.arange(6) dizisinden b = a[::2] ve c = a[[1, 3]] dizileri kuruluyor, ardından b[0] = 100 ve c[0] = 200 atamaları yapılıyor. Bu atamalardan sonra a, b ve c dizileri ne olur? Önce kodu çalıştırmadan tahmin ediniz.

Çözüm

Adım 1. b = a[::2] temel bir dilimdir, dolayısıyla bir görünümdür: b dizisi a dizisinin 0, 2 ve 4 numaralı hücrelerine bakar. b[0] = 100 ataması a[0] hücresine yazar.

Adım 2. c = a[[1, 3]] fancy indekslemedir, dolayısıyla bir kopyadır: c kurulurken 1 ve 3 değerleri yeni bir bloğa yazılır. c[0] = 200 ataması yalnız bu yeni bloğu değiştirir.

Adım 3. Buna göre a dizisinde yalnız ilk eleman 100 olur, b dizisi [100, 2, 4], c dizisi [200, 3] olur. Kodla doğrulayalım.

import numpy as np

a = np.arange(6)
b = a[::2]
c = a[[1, 3]]
b[0] = 100
c[0] = 200
print("a =", a)
print("b =", b)
print("c =", c)

Çıktı:

a = [100   1   2   3   4   5]
b = [100   2   4]
c = [200   3]

\(\blacksquare\)

Alıştırma 5.6 (Üçün ya da Beşin Katları) \(1000\)’den küçük olup 3’e ya da 5’e bölünen doğal sayıların toplamını bir maskeyle hesaplayınız.

Çözüm

Adım 1. np.arange(1000) ile \(0, 1, \dots, 999\) sayılarını kurarız; 0 toplamı değiştirmez.

Adım 2. “3’e bölünür” koşulu x % 3 == 0, “5’e bölünür” koşulu x % 5 == 0 maskesidir. “Ya da” bağlacı | işleciyle kurulur ve her koşul paranteze alınır.

Adım 3. Maskeyle seçilen elemanları sum ile toplarız.

Adım 4. Sonucu içerme–dışarma ilkesiyle (bkz. Veri Yapıları ve Comprehension) denetleriz. \(n\)’yi aşmayan \(k\)’nin katları \(k, 2k, \dots, mk\) (\(m = \lfloor n/k \rfloor\)) olduğundan toplamları \(k\,m(m+1)/2\)’dir. Hem 3’ün hem 5’in katı olanlar 15’in katlarıdır ve iki kez sayılmamaları için bir kez çıkarılırlar. \(n = 999\) için \(m\) sırasıyla \(\lfloor 999/3 \rfloor = 333\), \(\lfloor 999/5 \rfloor = 199\) ve \(\lfloor 999/15 \rfloor = 66\) olur:

\[ \begin{aligned} S &= 3 \cdot \frac{333 \cdot 334}{2} + 5 \cdot \frac{199 \cdot 200}{2} - 15 \cdot \frac{66 \cdot 67}{2} \\[1mm] &= 166\,833 + 99\,500 - 33\,165 = 233\,168. \end{aligned} \]

import numpy as np


def multiples_sum(k, n=999):
    """n'yi aşmayan k'nin katlarının toplamı."""
    m = n // k
    return k * m * (m + 1) // 2


x = np.arange(1000)
mask = (x % 3 == 0) | (x % 5 == 0)
print(x[mask][:10])
print("toplam:", x[mask].sum())
print("formül:", multiples_sum(3) + multiples_sum(5)
      - multiples_sum(15))

Çıktı:

[ 0  3  5  6  9 10 12 15 18 20]
toplam: 233168
formül: 233168

Maske ile formül aynı sonucu, \(233\,168\)’i verir. \(\blacksquare\)

Alıştırma 5.7 (Bir Permütasyonun Tersi) perm = np.array([2, 0, 3, 1]) dizisi \(\{0, 1, 2, 3\}\) kümesinin \(i \mapsto \mathrm{perm}[i]\) permütasyonunu (bkz. Lineer Cebir) göstersin. Ters permütasyonu tek bir fancy indeksleme atamasıyla bulunuz.

Çözüm

Adım 1. Ters permütasyon inv, her \(i\) için inv[perm[i]] = i koşuluyla belirlenir: \(\mathrm{perm}\), \(i\)’yi \(\mathrm{perm}[i]\)’ye götürüyorsa ters permütasyon \(\mathrm{perm}[i]\)’yi \(i\)’ye geri götürür.

Adım 2. Fancy indeksleme atamaya da uygulanır: inv[perm] = v ataması her \(i\) için inv[perm[i]] = v[i] yazar. v = np.arange(4) alınırsa Adım 1’deki koşulun hepsi tek satırda sağlanır.

Adım 3. Doğrulama: y = x[perm] bir diziyi permütasyona göre yeniden dizer; y[inv] ise eski sırayı geri getirmelidir. Ayrıca np.argsort(perm), perm dizisini sıralayan konumları verdiği için o da tersi bulmalıdır.

import numpy as np

perm = np.array([2, 0, 3, 1])
inv = np.zeros_like(perm)
inv[perm] = np.arange(perm.size)    # inv[perm[i]] = i
print("ters:", inv)

x = np.array([10, 20, 30, 40])
y = x[perm]
print(y, y[inv])
print(np.array_equal(inv, np.argsort(perm)))

Çıktı:

ters: [1 3 0 2]
[30 10 40 20] [10 20 30 40]
True

Elle: \(0 \mapsto 2\), \(1 \mapsto 0\), \(2 \mapsto 3\), \(3 \mapsto 1\) olduğundan ters permütasyon \(2 \mapsto 0\), \(0 \mapsto 1\), \(3 \mapsto 2\), \(1 \mapsto 3\), yani [1, 3, 0, 2] dizisidir. \(\blacksquare\)

Alıştırma 5.8 (Pascal Matrisi) \((i, j)\) bileşeni \(\binom{i}{j}\) olan (\(j > i\) için 0) \(6 \times 6\) alt üçgensel Pascal matrisini, her satırını bir önceki satırdan tek bir dilim toplamasıyla elde ederek kurunuz.

Çözüm

Adım 1. Pascal üçgenini Veri Yapıları ve Comprehension bölümünde listelerle satır satır kurmuştuk; burada aynı kuralı bir matrisin satırlarına uyguluyoruz. \(1 \le j \le i\) için Pascal özdeşliği (bkz. Analiz 1)

\[\binom{i}{j} = \binom{i-1}{j-1} + \binom{i-1}{j}\]

der. Ayrıca her \(i\) için \(\binom{i}{0} = 1\)’dir.

Adım 2. \(i\)-inci satırın \(1, \dots, i\) konumları P[i, 1:i + 1] dilimidir. Sağ taraftaki ilk terimler \(j - 1 = 0, \dots, i - 1\) için P[i - 1, 0:i], ikinci terimler \(j = 1, \dots, i\) için P[i - 1, 1:i + 1] dilimidir. Bu ikinci dilimin son elemanı \(\binom{i-1}{i} = 0\)’dır ve matris sıfırlarla başlatıldığı için zaten 0’dır.

Adım 3. İlk sütunu 1 yapıp satırları sırayla doldururuz, sonra her bileşeni math.comb ile karşılaştırırız. math.comb(i, j), \(j > i\) için 0 verir.

import math
import numpy as np

n = 6
P = np.zeros((n, n), dtype=int)
P[:, 0] = 1
for i in range(1, n):
    # C(i, j) = C(i-1, j-1) + C(i-1, j),  j = 1, ..., i
    P[i, 1:i + 1] = P[i - 1, 0:i] + P[i - 1, 1:i + 1]
print(P)
ok = all(P[i, j] == math.comb(i, j)
         for i in range(n) for j in range(n))
print("math.comb ile aynı:", ok)

Çıktı:

[[ 1  0  0  0  0  0]
 [ 1  1  0  0  0  0]
 [ 1  2  1  0  0  0]
 [ 1  3  3  1  0  0]
 [ 1  4  6  4  1  0]
 [ 1  5 10 10  5  1]]
math.comb ile aynı: True

Satırlar Pascal üçgeninin satırlarıdır; örneğin son satır \(1, 5, 10, 10, 5, 1\) sayıları, yani \((a + b)^5\) açılımının katsayılarıdır. \(\blacksquare\)

Alıştırma 5.9 (Blok Köşegen Matris) \(A = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}\) ve \(B = 5 I_3\) olmak üzere

\[M = \begin{pmatrix} A & O \\ O & B \end{pmatrix}\]

blok köşegen matrisini np.block ile kurunuz. Burada \(O\) uygun boyutlu sıfır matrislerdir.

Çözüm

Adım 1. Aynı blok satırındaki blokların satır sayıları, aynı blok sütunundaki blokların sütun sayıları eşit olmalıdır. Sağ üstteki \(O\), \(A\) ile aynı satır sayısına (2), \(B\) ile aynı sütun sayısına (3) sahiptir, yani \(2 \times 3\) tipindedir; sol alttaki \(O\) da aynı nedenle \(3 \times 2\) tipindedir.

Adım 2. \(B = 5 I_3\) skaler matrisini 5 * np.eye(3, dtype=int) verir. Sıfır blokları np.zeros ile, tamsayı türünde kurarız.

Adım 3. Sonucun şeklinin \((2 + 3) \times (2 + 3) = 5 \times 5\) olduğunu ve köşegen bloklarının dilimlerle geri okunduğunu kontrol ederiz.

import numpy as np

A = np.array([[1, 2],
              [3, 4]])
B = 5 * np.eye(3, dtype=int)
M = np.block([[A, np.zeros((2, 3), dtype=int)],
              [np.zeros((3, 2), dtype=int), B]])
print(M)
print(M.shape)
print(np.array_equal(M[:2, :2], A), np.array_equal(M[2:, 2:], B))

Çıktı:

[[1 2 0 0 0]
 [3 4 0 0 0]
 [0 0 5 0 0]
 [0 0 0 5 0]
 [0 0 0 0 5]]
(5, 5)
True True

\(\blacksquare\)

Alıştırma 5.10 (Faktöriyel ve Taşma) np.prod(np.arange(1, n + 1)) ifadesi \(n!\) değerini int64 aritmetiğiyle hesaplar (np.prod bir dizinin elemanlarını çarpar). Bu ifadenin yanlış sonuç verdiği en küçük \(n\) değerini math.factorial ile karşılaştırarak bulunuz.

Çözüm

Adım 1. int64 en çok \(2^{63} - 1 \approx 9{,}22 \cdot 10^{18}\) değerini tutabilir. Çarpım bu sınırı aştığı anda sonuç sessizce sarılır.

Adım 2. \(20! \approx 2{,}43 \cdot 10^{18}\) sınırın altında, \(21! = 21 \cdot 20! \approx 5{,}11 \cdot 10^{19}\) ise üstündedir. Bu yüzden ilk hatalı değerin \(n = 21\) olmasını bekleriz.

Adım 3. \(n = 18, \dots, 22\) için NumPy sonucunu, Python’ın sınırsız tamsayılarıyla tam hesap yapan math.factorial ile karşılaştırırız.

import math
import numpy as np

for n in range(18, 23):
    p = np.prod(np.arange(1, n + 1))
    print(n, p, p == math.factorial(n))
print("int64 üst sınırı:", np.iinfo(np.int64).max)

Çıktı:

18 6402373705728000 True
19 121645100408832000 True
20 2432902008176640000 True
21 -4249290049419214848 False
22 -1250660718674968576 False
int64 üst sınırı: 9223372036854775807

\(n = 21\) için sonuç negatif çıkıyor: tahmin doğrudur. Büyük tamsayılarla tam hesap gerektiğinde NumPy yerine Python’ın int türü ya da math modülü kullanılmalıdır. \(\blacksquare\)

Bu bölümde NumPy dizilerini kurmayı, şekillerini ve veri türlerini okumayı ve parçalarını seçmeyi gördük. Şekil yalnız verinin nasıl okunacağını belirler; dilimleme görünüm, fancy ve boolean indeksleme kopya verir. Dizilerin asıl gücü ise onlar üzerinde döngü yazmadan hesap yapmaktır. Bir sonraki bölüm Vektörizasyon ve Broadcasting, evrensel fonksiyonları, bir eksen boyunca toplamaları ve farklı şekillerdeki dizilerin birlikte nasıl işlendiğini anlatıyor.