Np-Urのデータ分析教室

オーブンソースデータなどWeb上から入手できるデータを用いて、RとPython両方使って分析した結果を書いていきます

add・reduce・outerなど、Numpyのユニバーサル関数(ufunc)について整理してみた

今回は、numpyを使う上での備忘録として、universal function(通称ufunc)について整理しておきたいと思います。

ufuncとは、何ぞや

universal functionとは、numpy配列に含まれる全ての要素に対して、何らかの演算を行う関数です。

ufuncの良いところとして、

  • コンパイルされたC言語で実装されているので速い
  • デフォルトで便利な関数が色々用意されている

などが挙げられます。

ということで、全要素に対して何らかの処理をする場合は、for文などを使わずに、積極的にufuncの使用を検討してみましょう。

さっそく例題

言葉で説明されても分かりづらいと思うので、実際に例を見ながら挙動を確認していきましょう。
まずはサンプルデータとして、(1×8)の1次元配列を作成します。

#coding:utf-8
import numpy as np

a = np.arange(8)
#array([0, 1, 2, 3, 4, 5, 6, 7])

例として、ufunc.square()という関数を使ってみましょう。
ufunc.square()は名前からも分かる通り、各要素を2乗した配列を返してくれる関数です。

下の結果を見て頂くと、各要素が2乗されていることが分かると思います。

np.square(a)
#array([ 0,  1,  4,  9, 16, 25, 36, 49])

このように、ndarray1つを引数にとる関数は他にもいくつかあります。

関数 内容
abs 整数、小数、複素数の絶対値を要素ごとに戻す
sqrt 平方根を要素ごとに戻す
exp 自然対数の底eのべき乗を要素ごとに戻す
isnan 要素ごとにNaN判定を行い、真偽結果をTrue or Falseで返す
sign 要素ごとの符号を返す。(正=1,負=-1,0=0)
これは関数のほんの一例ですので、詳しくは以下ドキュメントを参考にしてみてください。
Universal functions (ufunc) NumPy v1.14 Manual

2つのndarrayを使う場合

先ほどは1つのndarrayを引数に取りましたが、勿論2つのndarrayを引数に取る関数も存在します。
試しにufunc.add()を使ってみたいと思います。

a = np.arange(8)
#array([0, 1, 2, 3, 4, 5, 6, 7])

np.add(a,a)
#array([ 0,  2,  4,  6,  8, 10, 12, 14])

結果を見て頂くと、2つのndarrayの各要素同士の和が返されていることがわかります。

このように2つのndarrayを引数とする関数も多く用意されており、以下は一例ですが、加減乗除はもちろん、2つの要素を比較して最大値、最小値を返す関数もあります。

関数 内容
add 対応する要素同士の和を返す
subtract subtract(a,b)の場合、配列aから配列bの対応する要素の差を取る
multiply 各要素の積を返す
divide divide(a,b)の場合、配列aから配列bの対応する要素で割る
maximum 対応する要素同士の最大値を返す
minimum 対応する要素同士の最小値を返す

2次元配列を扱う場合の挙動を確認

2次元配列以上の場合も、基本的に挙動は同様です。

ta = np.array([[2,4,3],[3,5,7],[2,8,4]])
tb = np.array([[3,2,3],[1,1,1],[6,6,6]])
ta.shape
#(3, 3)

np.add(ta,tb)
#array([[ 5,  6,  6],[ 4,  6,  8],[ 8, 14, 10]])

(3×3)の配列を2つ作って、試しに足し算を行ってみます。

返された配列は(3×3)の配列で、ta(1,1)とtb(1,1)の要素,ta(1,2)とtb(1,2)の要素…というように、対応する要素同士の計算になっていることをご確認ください。

ufuncのちょっとだけ応用編

少し使い方がややこしいのですが、便利な関数があるのでいくつか紹介します。

reduce

まずは、ufunc.reduce()
reduce()を使うと、ufuncによる演算を連続的に適応することができます。

と言っても何がなんだか分からないので、実例を見ながら挙動を確認しましょう。

まずは1次元配列の場合

#1次元配列の場合
a = np.arange(8)
np.add.reduce(a)
#28

1次元配列の場合は、単純に配列内の要素を全て足し合わせた結果が戻ってきます。

3次元配列の場合

次に3次元配列での挙動を確認したいのですが、ここでまずは3次元配列について簡単に復習しておきます。

(2×3×2)の配列を用意します。

ta = np.array([
               [[1,2],[3,4],[5,6]],
               [[7,8],[9,10],[11,12]]
               ])
ta.shape
#(2,3,2)

一番外側の[]の中に何個の配列要素があるか数えると

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

と2個あることがわかります。

これが(2×3×2)でいう一番左側の「2」を指しています。

では[[1,2],[3,4],[5,6]]の中身に何個要素が入っているか見ると、

  • [1,2]
  • [3,4]
  • [5,6]

なので3つの配列要素が入っていることが分かります。

これが、(2×3×2)の真ん中の「3」の部分に当ります。では更に[1, 2]を確認すると

  • 1
  • 2

なので2つの要素が入っています。これが(2×3×2)における一番右の「2」を指しています。

このあたりはたまに混乱するので、ta[1,:]などで具体的にどこの部分を指しているか一度確認してみるとスッキリすると思います。

ということで前置きが長くなってしまいましたが、reduceの3次元配列時の挙動を確認します。

#3次元配列の場合
ta = np.array([
               [[1,2],[3,4],[5,6]],
               [[7,8],[9,10],[11,12]]
               ])

np.add.reduce(ta, axis=0)
#array([[ 8, 10],
#       [12, 14],
#       [16, 18]])

この、array([[ 8, 10], [12, 14], [16, 18]])はそれぞれ、

  • 8(1+7)
  • 10(2+8)
  • 12(3+9)
  • 14(4+10)
  • 16(5+11)
  • 18(6+12)

という計算をしています。3次元配列の場合、axisで指定した軸方向に対して足し算を行います。
axisもよくわからなくなりますが、axisは、shapeのインデックスを指定しており、axis=0ならshape(2×3×2)の一番左側(index=0)を指定、axis=1ならshape(2×3×2)の3(index=1)を指定していることになります。

先程の復習ですが、shape(2×3×2)の一番左の2が指してる部分は、[[1,2],[3,4],[5,6]]と[[7,8],[9,10],[11,12]]の部分でした。
つまり、axis=0で、この2つの配列の対応する各要素同士を足し合わせることを指定していることになります。

axis=1,2もやってみると以下のようになります。

ta = np.array([
               [[1,2],[3,4],[5,6]],
               [[7,8],[9,10],[11,12]]
               ])

np.add.reduce(ta, axis=1)
#array([[ 9(1+3+5), 12(2+4+6)],
#       [27(7+9+11), 30(8+10+12)]])

np.add.reduce(ta, axis=2)
#array([[ 3(1+2),  7(3+4), 11(5+6)],
#       [15(7+8), 19(9+10), 23(11+12)]])

()内の数字に、どの要素同士を足しているかメモしているので、照らし合わせてどの軸方向による計算か確認してみてください。

accumulate

次に、accumulateについて説明します。

accumulate()を使うと、途中の計算過程を含めた配列を戻してくれます。はい、こちらも言葉だけだとよく分からないので、実際に動かしてみます。

tb = np.array([[3,2,3],[1,1,1],[6,6,6]])
tb.shape
#(3,3)

tb.add.accumulate(tb, axis=1)
#array([[ 3,  5,  8],
#       [ 1,  2,  3],
#       [ 6, 12, 18]])

#reduceの場合、足し算の結果だけ配列として戻る
tb.add.reduce(tb, axis=1)
#array([ 8,  3, 18])

axis = 1指定なので [3,2,3]の足し算(=8)、[1,1,1]の足し算(=3)、[6,6,6]の足し算(=18)になります。
accumulateは足し算の過程(累積値)の結果を返すので、例えば[3,2,3]であれば[3, 3+2(=5), 3+2+3(=8)]のような配列を返すことになります。

outer

最後にouter(a,b)について。

outer(a, b)とすると、配列a, b全ての要素の組み合わせに対して、関数を適用する(add, subtractなど)ことができます。

こちらも早速例を見ていきましょう。
1~4までが入った(2×2)の配列aと5~8までが入った(2×2)の配列bを用意します。

a = np.array([[1,2],[3,4]])
b = np.array([[5,6],[7,8]])
np.add.outer(a,b)
#array([[[[ 6(1+5),  7(1+6)],
#         [ 8(1+7),  9(1+8)]],
#
#        [[ 7(2+5),  8(2+6)],
#         [ 9(2+7), 10(2+8)]]],
#
#
#       [[[ 8(3+5),  9(3+6)],
#         [10(3+7), 11(3+8)]],
#
#        [[ 9(4+5), 10(4+6)],
#         [11(4+7), 12(4+8)]]]])

各要素の組み合わせ全てに対して、足し算した結果が配列として戻ってきていることが分かります。
返ってきた配列の形は(2,2,2,2)となっており、a.shape + b.shapeの形になっていることを確認しましょう。

終わりに

私自身、ufuncをこれまであまり使ったことがなかったのですが、きっとどこかで使う機会が来るだろうということで、今回備忘録としてまとめてみました。

備忘録タグを作ってみたので、同じくメモ書きとして残しておきたいことを更新していきたいと思います。