私は2018年の後半、NumPy や pandas をよく使うようになりました。
とても有用なライブラリですが、ある程度不自由なく使えるようになるまでには少し時間がかかったなというのが個人的な感覚です。
ライブラリの機能が豊富であるがゆえに、使い方を覚えるのに時間がかかった側面はあります。それはある程度仕方ないとは思います。
しかし、実現したい処理内容をもとに、使うべき機能を探し出し、理解するまでの過程がやや大変な印象があるのは事実です。
また、実現したい処理内容によっては、複数の機能を組み合わせる必要があったりもします。覚えた知識の量が少ないうちは、特にそういった場面でかなり苦戦するのではないかと思います。私自身そうでした。
そこで、「実現したい処理」の具体例をいくつか出しながら、それに対応する実装方法を紹介するような記事を書こうと思い立ちました。
この手のライブラリは、「こういう処理をしたければ、これこれの機能をこう使えばいい」という引き出しをどれだけ頭の中に持てるかだと思います。
そうした「引き出し」を作るための助けになるような記事を目指して書いていきたいと思います。
※ 本記事では、NumPy と pandas の概要を説明した後、NumPy の配列である “ndarray” の生成と “ndarray” の演算までを説明します。
大量の数値データの扱いに適している
最初に、NumPy と pandas とはどういうものかを軽く説明します。
NumPy は多次元配列の計算を高速に実行できるライブラリで、pandas は統計処理(例:合計や平均、標準偏差の算出)に強いライブラリです。
pandas は、 DataFrame という2次元の表のような形式のデータ構造を備えています。これは、数値の配列に列名のラベルを付けたものだと思っておけばよいと思います。(各行にラベルを付けることもできます)
Python によるデータ分析や機械学習の情報を探していると、NumPy や pandas の名前はよく目にしますが、これは、データ分析であれば統計処理が必要になるのと、機械学習ならベクトル(= 数値を複数並べたもの)が必ず出てくるのが理由だと思います。
一言で表すなら、大量の数値データを扱うのに適したライブラリといったところです。
NumPy と pandas でできること
NumPy は、Python がデフォルトで持っている配列よりも高速に処理を行うことができます。内部はC言語で実装されているそうです。
Python そのものの動作は遅いですが、NumPy はその短所が問題にならないような仕組みになっています。
NumPy であれば、ループ処理を書かずに配列の全要素に対して処理を行うことができたり、簡単な記述で条件を満たす要素を抽出できたりもするので、ソースコードの量も少なく済みます。
一方、pandas は NumPy をベースに作られています。pandas のデータから NumPy の配列( “ndarray” )を取り出して使うこともできたりします。
しかし、わざわざ NumPy の配列を取り出さずとも、全要素に対する処理や、条件を満たす要素の抽出などの基本的な NumPy の機能はそのまま使えます。
NumPy と pandas の両方に共通していえるのは、行方向の操作も列方向の操作も、ほぼ同じ感覚で実行できるということです。
代表的な例としては、行と列の両方に対して自在にスライシングできる機能が挙げられます。
また、基本統計量(最大値・最小値、平均値、中央値、合計、標準偏差など)であれば簡単に算出できますし、共分散や相関係数の算出機能なども備えています。
列ごとに基本統計量を求めるだけでなく、SQL の GROUP BY と同様の処理や、「ピボットテーブル」のような複雑な集計処理も可能です。
他にも、CSVから DataFrame の形式でデータを読み込んだり、CSV にデータを書き出したりする機能も備えています。
環境
私が現在使っている環境ですが、次の通りです。
Python: 3.6.6
NumPy: 1.15.3
pandas: 0.23.4
使いこなすための練習方法
NumPy や pandas を使いこなすための練習としては、簡単な機能を試す程度であれば、私はとにかく Python の対話型シェルを使って実験するのがよいと思っています。
Python がインストールされていれば、python コマンドを打つだけで、すぐに練習をはじめられます。例えば次のように:
1 2 3 4 5 6 |
>>> import numpy as np >>> >>> arr = np.arange(0, 10, 2) >>> arr # array([0, 2, 4, 6, 8]) |
もちろん ipython を使ってもよいと思います。
おこなった実験の記録を残したいとか、なおかついつでも実行し直せるようにしたいという場合は、Jupyter Notebook が適しているかもしれません。
必要なコードの量が多い場合は、Python シェルではなく、Python スクリプト(.py)を書いて print() で出力させるなどの方法でもよいかと思います。
NumPy の機能(1) – ndarray の生成
NumPy を使うにあたって、NumPy の配列 “ndarray” (※1)の生成方法は最初におさえておく必要があります。
といっても、ここは難しい部分ではないでしょう。
まずは次の2つを知っておけば、一応先に進むことができます。
- np.array() による方法
- np.arange() による方法
np.array() については、引数として Python のリストを渡すだけで良いです。多次元配列を渡せば、そのデータ構造は ndarray にそのまま反映されます。
np.arange() は、引数の仕様が Python の range() や xrange() と同じになっています。(綴りについてですが、arrange ではありません。”r” は1つです。私は最初の頃、何度か間違えました。。)
[リンク] SciPy の公式ドキュメント上の ndarray の説明
それ以外の生成方法としては、あらかじめ決まった値で配列を初期化する関数を使うものが考えられます。次の3つを知っていればよいでしょう。
- np.zeros() (0 で初期化)
- np.ones() (1で初期化)
- np.full() (初期値を引数で指定できる)
np.zeros() と np.ones() は、関数名そのものが初期値を表しており覚えやすいと思います。
0 や 1 ではなく、それ以外の値を初期値として指定したければ、np.full() が使えます。
np.zeros() や np.ones() にタプルを渡せば、多次元配列を生成することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import numpy as np np.zeros(8) # array([0., 0., 0., 0., 0., 0., 0., 0.]) np.ones(8) # array([1., 1., 1., 1., 1., 1., 1., 1.]) np.zeros((3, 4)) """ array([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]]) """ np.ones((3, 4)) """ array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]]) """ |
引数として渡すタプルは、リストでも可です。
続いて、np.full() についても動きを確認してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
np.full(8, 5.0) # array([5., 5., 5., 5., 5., 5., 5., 5.]) fives = np.full((3, 2, 4), 5.0) """ array([[[5., 5., 5., 5.], [5., 5., 5., 5.]], [[5., 5., 5., 5.], [5., 5., 5., 5.]], [[5., 5., 5., 5.], [5., 5., 5., 5.]]]) """ |
引数のタプルの要素を3つにしていますが、この場合、2×4 の2次元配列を3つ含んだ3次元配列が出来上がります。
なお、ndarray のデータ構造は、”shape” という属性を参照すれば、後から参照できます。
これを使うと配列全体を出力せずとも構造を確認できるので、巨大なデータに対しては特に便利です。ログに情報を書き出してデバッグする必要がある場合に使えるかもしれません。
1 2 3 4 |
fives = np.full((3, 2, 4), 5.0) fives.shape # (3, 2, 4) |
さて、ここまでに示したもの以外にも ndarray の生成方法は存在します。
例えば、次のものがあります。
- np.linspace()
- np.random.randn()
np.linspace() は、第1・第2 引数で示した数値の範囲を等間隔で区切った配列を生成します。その間隔の幅は、第3引数で指定します。
第1・第2 引数はそれぞれ、始点と終点の値となります。始点の値を終点より大きい値にすると、値が減少していく配列が得られます。
1 2 3 4 5 6 |
np.linspace(1.0, 5.0, 9) # array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ]) np.linspace(2. , -1. , 7) # array([ 2. , 1.5, 1. , 0.5, 0. , -0.5, -1. ]) |
np.linspace() は、Matplotlib (データ可視化ライブラリ)でグラフを描くときに使えそうな関数です。
np.random.randn() は、乱数を生成して ndarray に詰めてくれる関数です。
多次元配列を得たい場合は、次のようにします。
1 2 3 4 5 6 7 |
np.random.randn(3, 4) """ array([[-1.18500569, -1.26786467, 0.3368098 , 0.32900955], [-1.22201542, -1.32482763, -0.15786995, -1.55695229], [ 1.37352127, -0.15011026, -0.41889954, 0.7646363 ]]) """ |
上の例では2次元配列を取得していますが、引数を増やせば、3次元、4次元・・・と次元を増やすことができます。
(※ この関数の場合、タプルによる shape の指定はできないようです。)
この np.random.randn() は、機械学習を軽く試してみたい時などに使えるでしょう。ばらつきをもった、ちょっとしたデータが実験用に欲しい時に使えます。
Numpy の機能(2) – 全要素に対する処理
この項で示す内容は単純で、分かってしまえば何ということはないものです。
言葉で説明するより、例を見たほうが早いでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
test1 = np.array([[0, 1, 2], [3, 4, 5]]) test1 """ array([[0, 1, 2], [3, 4, 5]]) """ test2 = test1 + 10 test2 """ array([[10, 11, 12], [13, 14, 15]]) """ |
例を見てしまえば、もう余計な説明は必要ないかと思います。
ただ “+ 10” と書くだけで、全部の要素に10が加算されるのです。
足し算以外の四則演算でも、同じように全部の要素に対して処理が行われます。
ループ処理(for 文など)を書かなくても、こういうコードを書くだけで全要素に対して処理ができるのです。要素が 1000 個だろうと、10000 個だろうと、同じ方法で書いて問題ありません。
また、ndarray どうしの四則演算は、同じ位置の要素どうしの四則演算が一挙に実行されます。
足し算や引き算であれば、「行列の和」「行列の差」ということになるでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
arr1 = np.arange(0, 6).reshape((2, 3)) arr1 """ array([[0, 1, 2], [3, 4, 5]]) """ arr2 = np.arange(0, 12, 2).reshape((2, 3)) """ array([[ 0, 2, 4], [ 6, 8, 10]]) """ result = arr1 + arr2 result """ array([[ 0, 3, 6], [ 9, 12, 15]]) """ |
ndarray の変形
さて、上の例ではさり気なく ndarray.reshape() というメソッドを使っていますが、これは、ndarray の構造を変化させる機能です。
上の例の “result” について言えば、”shape” が (2, 3) 、すなわち 2×3 の構造になっています。
この “result” に対して reshape((3, 2)) を実行すれば、”shape” は (3, 2) に変化するといった具合です。
また、flatten() というメソッドを呼ぶことで、1次元配列に変化させることもできます。ここでは、上の例の “result” に対して、flatten() してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
result.flatten() # array([ 0, 3, 6, 9, 12, 15]) for x in result.flatten(): print( x ) """ output: 0 3 6 9 12 15 """ |
(result.flatten() は ndarray です。そして上の例のように、ndarray は for … in でループさせることができます。)
多次元配列を1次元配列に直す処理が必要な場面は、たまに出てきます。例えば次のような場合があるかと思います。
- RDB から、1列だけのデータを読み取る場合
- OpenCV を Python から使用し、画像データを読み込み機械学習アルゴリズムの入力データとして使う場合
まず1つ目の RDB のほうですが、実際に私は SQLite3 を使うプログラムでこのケースに遭遇しました。後で調べて分かったのですが、この問題は MySQL でも当てはまるようです。
DB のレコードを読み取ったとき、1レコード分の情報はタプルになり、取得したデータ集合全体はそのタプルを要素とするリストになります。1列だけ読み取った場合でもこの2次元の構造になりますが、そもそも1列分しかないデータならば1次元のほうが扱いやすいので、1次元に直したくなります。
2つ目の画像と機械学習のケースについては、機械学習の処理の入力は基本的に1次元のベクトル(列ベクトル)ですから、配列を1次元に直す必要があるということです。
OpenCV を使用して “imread” メソッドで画像データを取得した場合、画像データは ndarray になるので、reshape() や flatten() で 1次元に直すことができます。
続きは次回以降
こうして書いてみると、NumPy のさわりだけで結構な量になってしまいました。少しは pandas の機能にも具体例を通じて触れられるかなと思っていたのですが、NumPy の分量は予想以上でした。。
非常に重要なスライシングやインデックス参照も説明したかったのですが、記事がすでに長くなってしまっているので、それは次回にしようと思います。
スライシング・インデックス参照まわりは紙一重(?)でビューになったりコピーになったりするので、地味ですが注意が必要です。
pandas については、データの結合や部分的な削除、欠損値が出現するケース、ピボットテーブル、階層型インデックスなど話題は色々ありますが、文章のまとめ方はよくよく考える必要がありそうです。
それでは今回は、ここまでにしたいと思います。
参考書籍
今回記事を書くにあたり、『Python によるデータ分析入門』を参考にしました。これは O’REILLY の本で、著者は Wes McKinney。pandas を開発した方のようです。
追記
本当はこの記事、12月のうちに書いてしまいたかったのですが・・・当初の目論見通りにはいかず。そもそも作業の分量が見えていませんでしたね。
執筆の仕方については、少しずつ学んでいければと思います。