なぜ Python を使うのか
もう一度、原点に立って考えてみましょう。統計や機械学習について Python を使う理由は何でしょうか。筆者は次のように考えています。
- Python 自体はグルー言語として万能性がある
- 数学、統計、機械学習などの科学計算用のライブラリが非常に豊富であり他の一般的な言語の追随を許さない域にある
- C/C++ や FORTRAN などで書かれた線形代数などの数学ライブラリを呼び出すためインタプリタ言語でありながら数理計算部分が高速である
純粋に言語としての魅力を語るのであれば Haskell や Ruby など他にもたくさん種類があるでしょう。また、単純に統計数理計算をするだけであれば R 言語がよく知られています。また金融計算や統計分析用のソフトウェアについては有償のものもあります。しかしながら、計算に特化した科学アプリケーションと、一般的なシステム処理を同時に問題解決する言語としては、まず Python が候補に挙がるのではないでしょうか。
そしてこれら Python による多次元配列計算をはじめとする科学計算・各種数理計算の基礎になるライブラリが NumPy です。
今回からしばらく NumPy の配列操作に焦点を当て、その強力な機能について理解を深めていきます。
np.ndarray オブジェクト
NumPy の ndarray オブジェクトはストライドデータ (連続的なデータ) を N 次元配列として扱うためのクラスです。
一般的なプログラミング言語でこのようなライブラリの助けなく多次元配列を扱おうとすると、複雑にネストしたリスト (配列) と、それを計算するためのこれまた複雑にネストした多重のループが必要になってしまいます。これでは現実的ではありませんから、科学計算において多次元配列ライブラリの良し悪しは要とも言えるでしょう。
ndarray の長所と短所
ndarray と多重リスト (配列) において ndarray が有利な点を挙げてみます。
- ndarray は行列を対象とした多くの高度な数学的操作を多重リストより容易かつ高速に適用できます。
- 配列中の全要素やもしくは一部の要素に対してまとめて演算や関数を適用することで高速な処理が可能です。
一方、 ndarray が不利な点を挙げてみます。
- 多重リストはリスト内でその要素の型が異なっても良いです。しかし ndarray は基本的に全て同じ型の要素で構成されていなければなりません。
- ndarray は各次元ごとの要素数が等しくなければなりません。
このあたりは、数学的とくに線形代数の観点からみると、むしろ当たり前と言えるのではないかと思います。
ndarray の構成要素
ndarray はストライド (歩幅) を内部的に持っており、配列オブジェクトは dtype (データ型) 、形状、ストライドという要素を持ちます。
配列の形状は shape 関数でアクセスすることができます。またストライドは strides 関数でアクセスできます。
np.zeros((3,4))
#=> array([[ 0., 0., 0., 0.],
# [ 0., 0., 0., 0.],
# [ 0., 0., 0., 0.]])
np.zeros((3,4)).shape
#=> (3, 4)
np.zeros((3,4)).strides
#=> (32, 8)
# 各次元の方向に要素を 1 つ進めるために必要な「歩幅」を示すバイト値
インデックス参照
スライシング
2 次元以上の配列の場合、インデックスで参照した先は 1 次元以上の配列になります。
arr = np.array( [[[1,2,3], [4,5,6]], [[7,8,9],[10,11,12]]] )
arr.ndim # 次元数
#=> 3
arr[0]
#=> array([[1, 2, 3],
# [4, 5, 6]])
# 2 次元配列が返る
インデックスに配列を指定するとその分次元数を削減した配列を取り出せます。
arr[1,0]
#=> array([7, 8, 9])
arr[1,0,2]
#=> 9
ファンシーインデックス参照
多次元配列からある特定の順序で抽出をおこないたいときに、その順番を示す整数のリストまたは ndarray をインデックス参照として渡すことができます。
arr[[1,0]]
#=> array([[[ 7, 8, 9],
# [10, 11, 12]],
# [[ 1, 2, 3],
# [ 4, 5, 6]]])
arr = np.arange(48).reshape((4,3,4))
#=> array([[[ 0, 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]]])
arr[[0,0,1],[0,1,0],[0,0,2]]
#=> array([ 0, 4, 14])
# (0,0,0), (0,1,0), (1,0,2) の位置にある要素が取り出される
まとめ
まずは基本的なインデックス参照について紹介しました。これらの参照ではメモリ上でオブジェクトがコピーされておらず、すべてデータに対するストライドビューで提供されています。これが ndarray の特徴のひとつです。