はじめに
データ分析を本格的に学ぶ上で、避けて通れないのがプログラミング。まずは"やった"という具体的な実感を持ちたいと思い、「Pythonによるデータ分析入門(O’REILLY)」を購入し、学習を進めている状況です。これからは学んだことのアウトプット(振り返り)を自分なりにしていきたいと思います。(プログラミング自体全くの初心者なので、誤りがある部分もあるかもしれません)
Numpy基礎
まずはモジュールのインポート。
import numpy as np #Numpyのインポート。npにするのは慣習らしい。
Ndarray:多次元配列オブジェクト
numpyが生成するオブジェクトであるndarryは、どうやら重要らしい。ndarrayを使用すれば、スカラー値と同じようにデータ全体に算術演算を適用できるとのこと。(つまり、すごく便利)
メモ:ndarrayはN次元配列オブジェクト(N-dimensional array) に由来するらしい。
d = np.array([[1,2,3], [4,5,6]]) #ndarrayの生成
#結果
array([[1, 2, 3],
[4, 5, 6]])
このarray([[1, 2, 3],[4, 5, 6]])とかいうやつがndarrayらしい。
d*10 #配列dに対する演結果
#結果
array([[10, 20, 30],
[40, 50, 60])
算術演算。もちろん割り算とかも可能。ndarray同士の演算も。
ndarryの多次元配列は全て同じ型でないといけないらしい。また、ndarray配列にはshapeとdtypeという属性があるらしい。
d.shape
d.dtype
#結果
(2,3)
dtype('int64')
2行3列、配列のデータ型が「int64」ってこと。
ndarrayの生成
numpyのarray関数を使えば、簡単にndarrayを生成可能。加えて、array関数の引数にリストとか、シーケンスを渡すとそのデータを格納したndarrayを返してくれるらしい。
d = [10, 20, 30, 40, 50]
d_arr = np.array(d)
#結果
array([10, 20, 30, 40, 50])
ndarrayが生成できた。辞書でも、タプルでも可能。
・その他の関数
numpy.zeros:ndarrayを生成し、すべての要素を0にする
numpy.ones:ndarrayを生成し、すべての要素を1にする
zeros = np.zeros(5)
ones = np.ones((3,3)) #n行n列のndarrayを生成するときは、引数にタプル((行数),(列数))を指定
#結果
array([0., 0., 0., 0., 0.])
array([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
numpy.arange:range関数のnumpyバージョン。つまり、rangeと同じ動きをする。
nums1 = np.arange(10) #0~9の10個の数字を配列にもつ
#結果
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
numpy.astype:データ型を型変換する関数。
ints = np.array([100, 200, 300, 400, 500])
ints.astype("float64")
#結果
array([100., 200., 300., 400., 500.])
int64型だったのがfloat64型になっている。
メモ:ほかにもint32型とかfloat16型とかがある
インデックス参照とスライシング(一次元)
d = np.arange(8)
d[4]
d[2:6]
#結果
array([0, 1, 2, 3, 4, 5, 6, 7])
np.int64(4)
array([2, 3, 4, 5])
変数名[インデックス]でインデックス参照が可能。(インデックスは0から始まる)
また、変数名[始点:終点:スキップ数]でスライシングが可能。(始点、終点、スキップ数をすべて入力しない場合は全てのインデックスが出力される)
ここら辺はPythonリストと同じ。
ということは、スライシングで抽出できる数は「終点-1」までということ。
メモ:Pythonリストとの違いは、ndarrayのスライシングは元のndarrayのビューであるということ。元のndarrayのコピーではない。スライスのあらゆる変更は元のndarrayに反映される。
d_slice = d[2:6]
d_slice[2] = 100
d
#結果
array([2, 3, 4, 5])
array([ 2, 3, 100, 5])
array([ 0, 1, 2, 3, 100, 5, 6, 7])
インデックス参照とスライシング(高次元)
n3d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
n3d[2] #一次元の時と同じコード。2行目をインデックス参照している。
n3d[2,1] #2行、1列目の要素を参照。
#結果
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
array([6, 7, 8])
np.int64(7)
変数名[行数]で行が参照される。
個々の要素を参照したい場合は、変数名[(行数),(列数)]と入力。
また、変数名[行数][列数]でも参照可能。
メモ:行、列ともに0行目、0列目から始まる
ブールインデックス参照
names = np.array(["Tanaka", "Yamada", "Sato", "Yamada", "Tanaka", "Mori"])
d = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]])
d[names == "Tanaka"]
d[names == "Tanaka", 1:] #元の形状を保った2次元の配列を返す
d[names == "Tanaka", 1] #特定の1つの列のデータだけを抽出し、1次元の配列を返す
names != "Tanaka" #"Tanaka"でないものがTrueとなる
~names = "Tanaka" #上のコードと同じ。[~]は条件式の結果を反転
#結果
array([[ 1, 2],
[ 9, 10]])
array([[ 2],
[10]])
array([ 2, 10])
array([False, True, True, True, False, True])
二つ以上のndarrayが存在する場合、ブールインデックスによる参照が可能である。上の例では、namesにおいて"Tanaka"に対応する行をすべて抜き出している。(0行目と4行目)
メモ:複数の条件を用いるときは、論理演算子&(and)と|(or)を使う。andとorは使用できない。
疑似乱数の生成
numpy.randomモジュールは様々な種類の確率分布からサンプリングした値を配列として生成する関数をもつ。
n = np.random.standard_normal(size = (4,4)) #標準正規分布から値をサンプリングし、4×4の行列に変換
#結果
array([[ 0.9247485 , -0.13144897, 0.59557529, 0.64130194],
[ 1.08522785, 0.32824722, -0.76057486, -0.11978341],
[ 0.90724035, 0.89830216, -0.66480323, -0.44423393],
[ 1.2445167 , 1.2124894 , -0.81168716, -1.40325704]])
その他のNumpyの乱数生成器メソッド(重要そうなやつ)
np.random.default_rng():引数にseedを設定し、値を代入すると、固定された値が出力される。
np.random.integers():指定した最小値、最大値の範囲でランダムに整数を生成。
np.random.binomial():二項分布に基づいて乱数を生成。
np.random.normal():ガウス分布に基づいて乱数を生成。
np.random.chisquare():カイ二乗分布に基づいて乱数を生成。
ユニバーサル関数(ufunc)
ndarrayの各要素に適用され、要素ごとの捜査結果を返す関数をufunc(universal functionの略)と呼ぶとのこと。
n = np.arange(10)
np.sqrt(n) #nの各要素の二乗根を求めるufunc
#結果
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
array([0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131, 2.82842712, 3. ])
引数に一つのndarrayを取るものを単項ufunc、二つのndarrayを取るものを二項ufuncと呼ぶらしい。ufuncは種類が多かったので割愛。
条件制御
x if condition else yのndarrayバージョンとして、numpy.whereがある。
#pythonの標準機能を用いる場合
X_array = np.array([1, 2, 3, 4, 5])
Y_array = np.array([6, 7, 8, 9, 10])
Z = np.array([True, False, True, False, True])
result = [(x if z else y)
for x, y, z in zip(X_array, Y_array, Z)] #ZがTrueならX_arrayの対応するインデックスの要素を出力。そうでない場合はY_arrayの対応するインデックスの要素を出力。
#結果
[np.int64(1), np.int64(7), np.int64(3), np.int64(9), np.int64(5)]
```python
#numpy.whereを用いる。
result = np.where(Z, X_array, Y_array) #Zなら、X_arrayを、そうでないならY_arrayを代入。
result
#結果
array([1, 7, 3, 9, 5])
なんか結果の形式がちがうけど...とにかく、同じ結果であるということ。numpy.whereの方がシンプルでいいね。こっちの方が動作も楽らしい。
rng = np.random.default_rng(seed = 12345) #出力値の固定
n = rng.standard_normal((4,4)) #標準正規分布から取得した値から4×4の行列を作成
n_new = np.where(n > 0, n, n * -1) #値が正ならそのまま出力、負なら-1をかけて出力
#結果
array([[-1.42382504, 1.26372846, -0.87066174, -0.25917323],
[-0.07534331, -0.74088465, -1.3677927 , 0.6488928 ],
[ 0.36105811, -1.95286306, 2.34740965, 0.96849691],
[-0.75938718, 0.90219827, -0.46695317, -0.06068952]])
array([[1.42382504, 1.26372846, 0.87066174, 0.25917323],
[0.07534331, 0.74088465, 1.3677927 , 0.6488928 ],
[0.36105811, 1.95286306, 2.34740965, 0.96849691],
[0.75938718, 0.90219827, 0.46695317, 0.06068952]])
使ってみた。すごく便利。
数学関数、統計関数
ndarrayは統計処理を行う関数をサポートしているらしい。
mean():平均値を計算。
sum():総和を計算。
軸(axis)を設定することで、列ごと、行ごとの統計値を計算できる。
n = np.random.standard_normal((4,4))
n.mean()
np.mean(n) #上のものと同じ処理
n.mean(axis = 0) #行ごとの平均値
n.mean(axis = 1) #列ごとの平均値
#結果
array([[ 0.97654312, 1.54969136, 0.99099423, -0.70574051],
[-0.14213838, 1.98387555, -1.04292004, -0.98714845],
[-0.00958152, -0.36914602, -0.06942229, -0.94900832],
[ 1.03001945, 0.91100313, 1.82126854, -0.38277702]])
np.float64(0.28784455126038166)
np.float64(0.28784455126038166)
他にも、cumsum(累積和)、cumprod(累積積)といった関数がある。
真偽値配列関数
n = np.random.standard_normal(100)
(n > 0).sum() #0より大きい数の総和
(n <= 0).sum() #0以下の数の総和
#結果
np.int64(48)
np.int64(52)
他にも、all関数、any関数が存在する。
bools = np.array([True, True, False, False, True])
bools.any() #少なくとも一つがTrueであるか
bools.all() #すべてTrueであるか
#結果
np.True_
np.False_
ソート
ndarrayはpythonのシーケンスと同様にsort関数によって並び替えが可能である。
n = rng.standard_normal(10)
n.sort() #小さいものから順に並べ替え
m
#結果
array([-1.42382504, 1.26372846, -0.87066174, -0.25917323, -0.07534331,
-0.74088465, -1.3677927 , 0.6488928 , 0.36105811, -1.95286306])
array([-1.95286306, -1.42382504, -1.3677927 , -0.87066174, -0.74088465,
-0.25917323, -0.07534331, 0.36105811, 0.6488928 , 1.26372846])
行列の場合は、引数axisに値を設定することで行ごと、列ごとのソートが可能。
m = rng.standard_normal((4,4))
m.sort(axis = 0) #列をソート
m.sort(axis = 1) #行をソート
#結果
array([[-1.42382504, 1.26372846, -0.87066174, -0.25917323],
[-0.07534331, -0.74088465, -1.3677927 , 0.6488928 ],
[ 0.36105811, -1.95286306, 2.34740965, 0.96849691],
[-0.75938718, 0.90219827, -0.46695317, -0.06068952]])
array([[-1.42382504, -1.95286306, -1.3677927 , -0.25917323],
[-0.75938718, -0.74088465, -0.87066174, -0.06068952],
[-0.07534331, 0.90219827, -0.46695317, 0.6488928 ],
[ 0.36105811, 1.26372846, 2.34740965, 0.96849691]])
array([[-1.95286306, -1.42382504, -1.3677927 , -0.25917323],
[-0.87066174, -0.75938718, -0.74088465, -0.06068952],
[-0.46695317, -0.07534331, 0.6488928 , 0.90219827],
[ 0.36105811, 0.96849691, 1.26372846, 2.34740965]])
やってみると分かりやすい。
集合関数
なにやら難しそうな感じ。集合関数でよく使われるのがunique関数らしい。この関数は配列要素から重複を取り除いてソートした結果を返してくれる。
names = np.array(["Tanaka", "Sato", "Tanaka", "Mori", "Sato", "Yamada", "Yamada"])
ints = np.array([1, 1, 3, 2, 2, 2, 4, 4, 5, 5, 5])
np.unique(names)
np.unique(ints)
#結果
array(['Mori', 'Sato', 'Tanaka', 'Yamada'], dtype='<U6')
array([1, 2, 3, 4, 5])
見事に重複がなくなっている。この関数は引数にndarrayの名前を渡すことによって動作する。
その他Numpyの集合関数
intersectld(x,y):配列x,yの共通要素を取り出してソートする。
unionld(x,y):配列x,yのうち少なくとも一方に存在する要素を取り出してソートする。
inld(x,y):配列xの各要素に対し、配列yの要素群が含まれるか確認し、その結果を真偽値として返す。
setdiffld(x,y):配列xから配列yの要素を取り除いてソートする。
setxorld(x,y):配列xとyのうち、どちらか一方にのみ存在する要素を取り出し、ソートする。
おわりに
今回はNumpyの基礎について振り返っていきました。にしてもこの書籍、かなりわかりやすい。ただ、知らない単語が突拍子もなく出てきたりするので、そこは調べながら。
この本での学習を進めていますが、データ分析はやはり分析作業ももちろん重要なのですが、それ以上にデータの加工、準備が最重要で、時間を要する箇所であることを思い知らされています。今日はここまで。
参考書籍