今から始めるPython その6 numpyの基本 スライシングの続き
前回は、J.R. Johanssonさんという方が作ったiPython notebookを一緒に読みながらNumpyアレイのスライシングの基本について見ていきましたが、長くなったので次回に続くといいつつ、長めの記事を書くのは時間がかかるので、なかなか更新できませんでした。
さて、今回はLinear algebraのところから見ていきます。
CとかJavaから来た人はForループでもって計算するのに慣れているので、Forループの代わりにできるだけ行列の計算に問題を置き換えて行くのが、パフォーマンスを得るコツです。
MATLABでもループばっかり使っている人は結構いるものです。しかしNumpyやMATLABは行列の演算に最適化されていることを思い出して、前回に覚えたスライシングをうまく使うことで、Forループが行列の演算に置き換えられる場合が多いので、NumpyではいかにForループを使わないかを心がけるといいと思います。それには上級者のコードを眺めて理解するのが近道ですね。MATLABならMATLAB Centralにいろいろとありますが、NumpyだとGitHubですね。
Scalar-array operations
さて、スカラーをアレイに対して足したり、掛けたりするとどうなるかについてですね。スカラーとはつまり数字が一つということで、配列じゃないということです。配列どうしの計算では、配列の次元が大事になってきます。
まあ、例にある通り、配列のすべての要素に同じ計算が行われています。つまり、Forループで各要素についていちいち計算しなくともいいわけですね。
掛け算、割り算、足し算、引き算すべて同じです。
Element-wise array-array operations
さて、上にも書きましたが、配列どうしの計算では、配列の次元が大事になってきます。
3x3の行列なら3x3の行列を用意してやれば、配列同士の掛け算、割り算、足し算、引き算が行われ、これはElement-wiseの計算になります。Element-wiseとは同じ位置にある数字同士で計算が行われることです。つまり、行列Aの右上の数字とは、行列Bの右上の数字とで計算するということです。
ここで、なんとなく高校・大学生時代の代数を思い出して、「n × m 行列 A と m × p 行列 B を掛け算すると結果はn × p 行列になるはずだし、結果の各要素は沢山の掛け算の足し算になるんじゃなかったっけ?」と変な感じがしたかもしれません。
こういう計算は、行列の乗法というやつで、次の項で説明されているMatrix multiplicationのことですね。
Matrix multiplication
上のn × p 行列はいわゆるドットプロダクトとも言います。なので、Numpyでn × m 行列 A と m × p 行列 BのMatrix multiplicationをするときは
np.dot(A, B)
のようにします。
実は、Numpyにはnp.arrayというよく使われるアレイの他にnp.matrixというのも用意されています。np.matrixを使うと行列の掛け算は代数のようにドットプロダクトがデフォルトの動作になって、np.dotと書かなくとも良くなります。
ぶっちゃけnp.matrixはあまり使われていないと思いますが、高度な代数を駆使したアルゴリズムをかくような場合は便利かもしれません。
私はnp.matrixを使ったことがありません。まあ、そんなのもあるんだという感じでいいと思います。
In [96]のところで、.Tを使っていますね。このピリオドは、英語ではこういう文脈では普通ドットと発音しますが、ドットを使うことでNumpyアレイが持っているメソッド(またはプロパティー、アトリビュートと言ったりもする)にアクセスすることが出来ます。
このTというのはメソッドで、transposeの略で、縦長な行列が横長になったりするアレです。同様のことはMATLABではアポストロフィーでもってA'としますね。ちなみにアポストロフィーも普通はプライムと発音します。アポストロフィーは多分通じません。
試しに
a = np.array([0])
としてNumpyアレイを作ります。これで、aはNumpyアレイのオブジェクトになります。
dir(a)
として、このオブジェクトが持っているメソッドを表示してみましょう。
アンダースコアがついたプライベートメソッドの他に、アンダースコアがないメソッドが沢山表示されたと思います。
'all', 'any', 'argmax', 'argmin', 'argpartition', 'argsort', 'astype', 'base', 'byteswap', 'choose', 'clip', 'compress', 'conj', 'conjugate', 'copy', 'ctypes', 'cumprod', 'cumsum', 'data', 'diagonal', 'dot', 'dtype', 'dump', 'dumps', 'fill', 'flags', 'flat', 'flatten', 'getfield', 'imag', 'item', 'itemset', 'itemsize', 'max', 'mean', 'min', 'nbytes', 'ndim', 'newbyteorder', 'nonzero', 'partition', 'prod', 'ptp', 'put', 'ravel', 'real', 'repeat', 'reshape', 'resize', 'round', 'searchsorted', 'setfield', 'setflags', 'shape', 'size', 'sort', 'squeeze', 'std', 'strides', 'sum', 'swapaxes', 'take', 'tofile', 'tolist', 'tostring', 'trace', 'transpose', 'var', 'view'
これらのメソッドは便利なのでよく使うものが多いです。たとえばこの中のsumはnp.sum(A)としてもA.sum()としても同じ動作になります。これは多分の全部のメソッドについて言えると思いますが、それぞれ対応するnumpyのファンクションがあり、ドットでアクセスできるメソッドはこれらに対するショートカットみたいなものです。
transposeのドットTについては
np.transpose(A)
という関数がA.Tという操作に対応します。
以下では、上でみたメソッドの中から代表的なものが説明されていきます。
Array/Matrix transformations
realとかimagとかabsとか、見たままですね。
Matrix computations
ここも、問題無いと思います。
Data processing
shape(data)
としていますが、ここでのdataはNumpyアレイです。これで、アレイの次元を調べることが出来ます。ここではpylab環境なのでnp.shapeの代わりにshapeとしています。
ただ、このshapeに関しては、
data.shape
のようにして、メソッドとしてアクセスする場合がほとんどだとおもいます。実は、これを見るまでnp.shapeという関数も用意されているのを知らなかったくらいです。
その後のmin、max、sum、stdなどもメソッドとして使う場合が多いです。
Computations on subsets of arrays
ここは、スライシングの基本をやったので余裕ですね。
面白いのは127行目の
monthly_mean = [mean(data[data[:,1] == month, 3]) for month in months]
の部分ですね。前回にちょっとだけ触れたリストコンプリヘンションをつかっているので、ちょっと複雑かもしれません。上の方でmonthsをnp.arangeでもって定義していますが、arange(1,13)の結果は1から12までの整数のベクターになります。13は最初のいらない数字なので入りません。なのでmonths (複数形に注意)は12の要素があります。これが、リストコンプリヘンションで一回目は1,二回目は2とForループのようにループして、12個の要素が入ったリストが結果です。
さて、それぞれのリストの要素はmean(data[data[:,1] == month, 3])になりますが、一回目のループを想定するとmonth(単数形に注意、monthsとは別の変数です)は1ですので、
mean(data[data[:,1] == 1, 3])
となります。meanはnp.meanのことです。np.meanにはaxisというオプションのパラメータがあって、平均を取るときの次元を指定できますが、ここでは省略されているので、全要素の平均になり、結果はひとつの数字になります。
ちょっと蛇足ですが、axisを使った場合の例を追加しておきます。
toy_data = np.arange(10).reshape(2,-1) toy_data.mean(axis=1) >>> array([ 2., 7.]) toy_data.mean(axis=0) >>> array([ 2.5, 3.5, 4.5, 5.5, 6.5])
np.arange(10).reshape(2,-1)で最初に0から9までの10要素のベクターを作って、すぐさまreshapeでもって最初の次元を2にして、次の次元は-1で自動で振り分けることで、2x5の2次元配列に変換しています。その上で、.mean(axis=1)とすることで、2つ目の次元(横方向)に対して平均をとっています。同様にmean(axis=0)では最初の次元(縦方向)に平均をとっています。出てくる結果の次元が異なるのに注意です。
次にデータのフォーマットを確認すると
The dataformat is: year, month, day, daily average temperature, low, high, location.
とありますので、data[:,1]の部分はmonthが入ったベクターになるようにスライシングしています。
で、スライシング(カギカッコの中)をよくみると
[data[:,1] == 1, 3]
ですが、スライシングの最初の次元はdata[:,1] == 1の条件を満たすベクターとなります。次の3はスライシングの2つ目の次元です。ベクターとスカラーがごっちゃなので混乱するかもしれませんが、スカラーは自動的に同じサイズのベクターとして解釈されます。
dataは二次元配列ですし、インデックスが3ということは4つ目のカラムなので(Numpyはゼロインデックスです)はdaily average temperatureですので、要は、1月の気温データをdataから抜き出して、平均気温を計算しています。
というわけでmonthly_meanには1月から順に12月までの平均気温が計算されています。短くエレガントに書いてあるので初心者にはちょっとキツいかもしれません。でも、慣れるとこういう風にかくようにすぐになると思いますよ。
その後はpylabを使ってプロットしています。
fig, ax = subplots()
に関しては、この場合プロットは一個しかないので省略してsubplots()にして、その後のax.barからaxの部分を省略して、bar、set_xlabelなどとしてもオーケーです。
でも、fig, ax = subplots()のようにすると複数個プロットがある場合、
fig2, ax2 = subplots()
のようにして、区別できるのでよい習慣だと思います。
subplots(複数形に注意)というのはsubplotとfigureを合わせたもので、あんまり使われているのを見ない気がします。普通はfig = figure()とやってからax = subplot(111)とかやって2行つかって書きます。
いやいやまた長くなってきました。全部終わるまで待っているとなかなか記事が進まないので、今日はここまでにして、次回Numpyの基本最終回として、Calculations with higher-dimensional dataから見ていきます。
次回からは公式サイトのPythonチュートリアルにもどって見ていくのがいいかなぁと予定しています。