6.11. Redukcje

Redukcja zwija tablicę wzdłuż jednej lub kilku osi, sumując ją, uśredniając, wyznaczając minimum i tak dalej. Każda redukcja to pojedyncze wywołanie biblioteki na całej tablicy, znacznie szybsze niż równoważna pętla w Pythonie. numpy obejmuje te codzienne operacje:

  • sum() – suma wszystkich elementów

  • mean() – średnia arytmetyczna (suma podzielona przez liczbę elementów)

  • std() – odchylenie standardowe, ddof= koryguje dzielnik (N - ddof)

  • min() / max() – najmniejszy i największy element

  • median() – wartość środkowa po posortowaniu elementów (50. percentyl)

  • argmin() / argmax()indeks elementu minimalnego lub maksymalnego

  • all() / any() – redukcje wartości logicznych na tablicach typu boolean

6.11.1. Bez słowa kluczowego axis

Wywołana bez axis=, redukcja zwraca skalar obejmujący całą tablicę:

a = np.array([1, 2, 3, 4], dtype=np.float)
np.sum(a)           # 10.0
np.mean(a)          # 2.5
np.std(a)           # 1.118...
np.median(a)        # 2.5

b = np.array([40, 10, 30, 20], dtype=np.float)
np.max(b)           # 40.0
np.argmax(b)        # 0  (index of the maximum)

6.11.2. Ze słowem kluczowym axis

axis= redukuje jedną wskazaną oś, pozostawiając pozostałe nienaruszone. Wynikiem jest tablica o rangę niższej niż dane wejściowe:

m = np.arange(12, dtype=np.float).reshape((3, 4))

np.sum(m)               # 66.0          - scalar
np.sum(m, axis=0)       # length-4      - column sums
np.sum(m, axis=1)       # length-3      - row sums

Ta sama reguła kształtu obowiązuje przy każdej redukcji: axis=0 zwija pierwszą oś, axis=1 zwija drugą i tak dalej. Na przykład średnią / odchylenie standardowe wzdłuż wiersza zapisuje się jako np.mean(m, axis=1) oraz np.std(m, axis=1). Wynik ma długość drugiej osi.

Słowo kluczowe keepdims=True zachowuje zredukowaną oś w miejscu z długością 1, zamiast ją usuwać. Rozróżnienie ma znaczenie, gdy zredukowany wynik musi z powrotem rozpropagować się względem oryginału: keepdims zachowuje rangę, co utrzymuje zgodność reguł rozpropagowania oś po osi.

Odejmowanie od każdego wiersza jego średniej to kanoniczne zastosowanie:

m = np.arange(12, dtype=np.float).reshape((3, 4))
row_means = np.mean(m, axis=1, keepdims=True)
# row_means has shape (3, 1)
centred = m - row_means
# (3, 4) - (3, 1) -> (3, 4), each row centred on its own mean

Bez keepdims np.mean(m, axis=1) zwraca jednowymiarowy wynik o kształcie (3,). Rozpropagowanie (3, 4) - (3,) ustawia (3,) jako (1, 3) po dodaniu rangi z przodu, co jest niezgodne z (3, 4): ostatnie osie się różnią (4 wobec 3) i żadna nie jest równa 1, więc numpy zgłasza ValueError. To właśnie keepdims=True utrzymuje poprawność odejmowania.

6.11.3. Układ danych ma znaczenie

W połączeniu z układem wierszowym (row-major) omówionym w Kształt i kroki, redukcja wzdłuż ostatniej osi jest przypadkiem najtańszym. Redukcja przechodzi przez blok danych w kierunku, w jakim są one zapisane, bez przeskoków z wiersza do wiersza:

m = np.arange(2000, dtype=np.float).reshape((2, 1000))
np.sum(m, axis=1)       # cheap - long axis is the inner one
np.sum(m, axis=0)       # has to jump rows on every step

Gdy aplikacja ma swobodę w doborze rozkładu bufora, umieść długą oś jako ostatnią, aby redukcje wzdłuż niej działały w szybkim kierunku.

6.11.4. Iterowalne jako dane wejściowe

Większość redukcji przyjmuje iterowalny obiekt Pythona (list, range, krotkę) zamiast ndarray. Ta wygoda kosztuje kilka mikrosekund na niejawną konwersję – co szybko narasta w pętli. Gdy te same dane są redukowane wielokrotnie, zbuduj ndarray raz i przekazuj go dalej.