5.1. 配列#

NumPy では、すべてのデータを配列array)という構造で扱います。リストや数学のベクトルのような一次元のデータは一次元配列として、表形式のデータや行列のような二次元のデータは二次元配列として表現されます。配列は、必要に応じて次元を増減させることができ、柔軟に形を変えることが可能です。また、NumPy に用意されている多くの関数は、この配列を操作対象としています。本節では、NumPy を使う上で基本となる配列について解説していきます。

5.1.1. 配列の作成#

5.1.1.1. 一次元配列#

NumPy の配列は、Python のリストを np.array 関数に渡すことで作成できます。特に、データが少ない場合や、プログラム内で一時的に配列を使いたい場合に便利です。次は、リストを NumPy 配列に変換する基本的な例です。

x = [1, 2, 3, 4, 5]
a = np.array(x)

NameError: name 'np' is not defined?察して。NumPy は、自分からは出てきません。

配列が保存されている変数をそのまま実行すると、array([...]) の形式で表示されます。なお、この方法は Jupyter Notebook 特有のものであり、他の環境では何も表示されないため注意が必要です。

a
array([1, 2, 3, 4, 5])

一方、print 関数を使って配列を表示すると、Python のリストのような見た目で表示されます。

print(a)
[1 2 3 4 5]

print 関数を使った場合、見た目はリストと NumPy 配列で同じだが、実際にはまったく異なるデータ構造です。NumPy の配列は、単なる値の集まりではなく、要素の数、形状、データ型などの情報(属性)を持ったオブジェクトです。たとえば、配列の全要素数を知りたい場合は、次のように .size 属性を使います。

a.size
5

一方で、リストには .size のような属性は存在しないため、次のように書くとエラーになります。

x.size
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[6], line 1
----> 1 x.size

AttributeError: 'list' object has no attribute 'size'

配列を作成する際に、以下のようにリストを直接 np.array 関数に渡して、1 行で配列を作成することもできます。

a = np.array([1, 2, 3, 4, 5])
a
array([1, 2, 3, 4, 5])

NumPy の配列では、すべての要素が同じデータ型である必要があります。つまり、配列内には すべて整数、またはすべて小数(浮動小数点数)といったように、統一された型のデータだけが含まれます。たとえば、以下のように整数と小数が混在するリストを np.array に渡すと、この関数はすべての要素を自動的に 64 ビット浮動小数点数型(float64 型)に変換して配列を作成します。

a = np.array([1, 2, 3, 3.14])
a
array([1.  , 2.  , 3.  , 3.14])
a.dtype
dtype('float64')

5.1.1.2. 二次元配列#

行列のような二次元配列を作成するには、二重リスト(リストのリスト)を np.array 関数に渡します。たとえば、次のコードは、以下のような 3 行 3 列の行列を作成します。

\[\begin{split} \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{pmatrix} \end{split}\]
x = [[1, 2, 3],
     [4, 5, 6],
     [7, 8, 9]]
a = np.array(x)
a
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
a = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])
a
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

三次元以上の配列も、同じ方法で作ります。つまり、多重リストを np.array 関数に渡すこと多次元配列に変換できます。

5.1.1.3. データフレームによる変換#

CSV ファイルなどに保存されたデータを配列として読み込むには、NumPy の np.loadtxt 関数を使う方法があります。ただし、NumPy は数値計算向けに設計されているため、文字データや欠損値を含む CSV ファイルでは予期せぬ動作をすることがあります。そのため、実務では Pandas を使ってファイルからデータを読み込んでから、前処理をした後に、配列に変換する方法が一般的です。

どんぐりのデータセットをデータフレームから配列に変換する一連の手順を以下に示します。まず、pd.read_csv 関数を使って CSV ファイルを Pandas のデータフレームとして読み込み、そのデータフレームの末尾に .values を付けるだけで、配列を取得できます。

x = pd.read_csv('acorns.clean.csv')
a = x.values
a
array([['kunugi', 5.55, 2.27, 1.89],
       ['kunugi', 4.62, 1.98, 1.84],
       ['kunugi', 5.05, 2.08, 1.9],
       ['kunugi', 5.44, 2.18, 1.91],
       ['kunugi', 5.6, 2.2, 1.93],
       ['kunugi', 4.83, 2.19, 1.85],
       ['kunugi', 5.55, 2.16, 2.0],
       ['kunugi', 5.13, 2.2, 1.87],
       ['kunugi', 6.03, 2.26, 2.01],
       ['kunugi', 7.41, 2.35, 2.12],
       ['konara', 1.6, 2.11, 1.1],
       ['konara', 1.49, 2.04, 1.09],
       ['konara', 1.74, 2.3, 1.17],
       ['konara', 1.57, 2.11, 1.11],
       ['konara', 1.88, 2.33, 1.11],
       ['konara', 1.63, 2.12, 1.14],
       ['konara', 1.45, 2.06, 1.09],
       ['konara', 1.07, 1.91, 0.98],
       ['konara', 1.53, 2.17, 1.17],
       ['konara', 1.61, 2.0, 1.12],
       ['matebashii', 2.96, 2.75, 1.35],
       ['matebashii', 3.18, 2.8, 1.44],
       ['matebashii', 3.08, 2.73, 1.42],
       ['matebashii', 2.89, 2.78, 1.38],
       ['matebashii', 3.68, 2.83, 1.51],
       ['matebashii', 2.93, 2.62, 1.38],
       ['matebashii', 2.3, 2.5, 1.26],
       ['matebashii', 3.18, 2.68, 1.46],
       ['matebashii', 3.13, 2.76, 1.4],
       ['matebashii', 2.2, 2.54, 1.21],
       ['shirakashi', 1.76, 1.95, 1.23],
       ['shirakashi', 1.96, 2.06, 1.23],
       ['shirakashi', 1.77, 1.93, 1.24],
       ['shirakashi', 1.88, 2.07, 1.25],
       ['shirakashi', 1.7, 1.97, 1.2],
       ['shirakashi', 1.78, 2.09, 1.2],
       ['shirakashi', 1.82, 1.97, 1.24],
       ['shirakashi', 1.7, 1.95, 1.2],
       ['shirakashi', 1.56, 1.98, 1.13],
       ['shirakashi', 1.9, 2.06, 1.25],
       ['shirakashi', 1.37, 1.89, 1.09],
       ['shirakashi', 2.01, 1.94, 1.39],
       ['shirakashi', 2.12, 1.97, 1.32],
       ['shirakashi', 1.89, 2.03, 1.26],
       ['shirakashi', 1.87, 2.06, 1.22],
       ['arakashi', 1.68, 1.89, 1.23],
       ['arakashi', 1.42, 1.87, 1.12],
       ['arakashi', 1.54, 1.82, 1.15],
       ['arakashi', 1.3, 1.77, 1.19],
       ['arakashi', 1.53, 1.81, 1.18],
       ['arakashi', 1.47, 1.85, 1.13],
       ['arakashi', 1.64, 1.89, 1.26],
       ['arakashi', 1.44, 1.81, 1.14],
       ['arakashi', 1.36, 1.87, 1.09],
       ['arakashi', 1.56, 1.9, 1.17],
       ['arakashi', 1.49, 1.82, 1.17],
       ['arakashi', 1.51, 1.84, 1.16],
       ['arakashi', 1.41, 1.88, 1.12],
       ['arakashi', 1.48, 1.83, 1.17]], dtype=object)

NumPy の配列では、すべての要素が同じデータ型である必要があります。この例では、データフレームには樹種を表す文字列データが含まれいます。そのため、データフレーム全体を配列に変換すると、変換後の配列の要素が object 型となります。object 型は数値ではありません。数値計算自体は可能だが、意図しない動作を引き起こす可能性があります。そのため、計算処理に使用する際には、object 型を含まないようにデータを整えておくことが重要です。

a.dtype
dtype('O')
a[:, 1] * a[:, 2]
array([12.5985, 9.1476, 10.504, 11.859200000000001, 12.32, 10.5777,
       11.988, 11.286000000000001, 13.627799999999999, 17.413500000000003,
       3.376, 3.0396, 4.002, 3.3127, 4.3804, 3.4556, 2.987, 2.0437,
       3.3201, 3.22, 8.14, 8.904, 8.4084, 8.0342, 10.4144,
       7.6766000000000005, 5.75, 8.522400000000001, 8.6388,
       5.588000000000001, 3.432, 4.0376, 3.4161, 3.8915999999999995,
       3.3489999999999998, 3.7201999999999997, 3.5854, 3.315, 3.0888,
       3.9139999999999997, 2.5893, 3.8993999999999995, 4.1764,
       3.8366999999999996, 3.8522000000000003, 3.1752, 2.6554, 2.8028,
       2.301, 2.7693000000000003, 2.7195, 3.0995999999999997, 2.6064,
       2.5432, 2.964, 2.7118, 2.7784, 2.6508, 2.7084], dtype=object)

次の例では、樹種名などの文字列が含まれている最初の列を削除し、その後データフレームを配列に変換しています。これにより、数値のみを含む配列が得られ、数値計算などに利用しやすくなります。

x = pd.read_csv('acorns.clean.csv')
a = x.iloc[:, 1:].values
a
array([[5.55, 2.27, 1.89],
       [4.62, 1.98, 1.84],
       [5.05, 2.08, 1.9 ],
       [5.44, 2.18, 1.91],
       [5.6 , 2.2 , 1.93],
       [4.83, 2.19, 1.85],
       [5.55, 2.16, 2.  ],
       [5.13, 2.2 , 1.87],
       [6.03, 2.26, 2.01],
       [7.41, 2.35, 2.12],
       [1.6 , 2.11, 1.1 ],
       [1.49, 2.04, 1.09],
       [1.74, 2.3 , 1.17],
       [1.57, 2.11, 1.11],
       [1.88, 2.33, 1.11],
       [1.63, 2.12, 1.14],
       [1.45, 2.06, 1.09],
       [1.07, 1.91, 0.98],
       [1.53, 2.17, 1.17],
       [1.61, 2.  , 1.12],
       [2.96, 2.75, 1.35],
       [3.18, 2.8 , 1.44],
       [3.08, 2.73, 1.42],
       [2.89, 2.78, 1.38],
       [3.68, 2.83, 1.51],
       [2.93, 2.62, 1.38],
       [2.3 , 2.5 , 1.26],
       [3.18, 2.68, 1.46],
       [3.13, 2.76, 1.4 ],
       [2.2 , 2.54, 1.21],
       [1.76, 1.95, 1.23],
       [1.96, 2.06, 1.23],
       [1.77, 1.93, 1.24],
       [1.88, 2.07, 1.25],
       [1.7 , 1.97, 1.2 ],
       [1.78, 2.09, 1.2 ],
       [1.82, 1.97, 1.24],
       [1.7 , 1.95, 1.2 ],
       [1.56, 1.98, 1.13],
       [1.9 , 2.06, 1.25],
       [1.37, 1.89, 1.09],
       [2.01, 1.94, 1.39],
       [2.12, 1.97, 1.32],
       [1.89, 2.03, 1.26],
       [1.87, 2.06, 1.22],
       [1.68, 1.89, 1.23],
       [1.42, 1.87, 1.12],
       [1.54, 1.82, 1.15],
       [1.3 , 1.77, 1.19],
       [1.53, 1.81, 1.18],
       [1.47, 1.85, 1.13],
       [1.64, 1.89, 1.26],
       [1.44, 1.81, 1.14],
       [1.36, 1.87, 1.09],
       [1.56, 1.9 , 1.17],
       [1.49, 1.82, 1.17],
       [1.51, 1.84, 1.16],
       [1.41, 1.88, 1.12],
       [1.48, 1.83, 1.17]])
a.dtype
dtype('float64')
a[:, 1] * a[:, 2]
array([4.2903, 3.6432, 3.952 , 4.1638, 4.246 , 4.0515, 4.32  , 4.114 ,
       4.5426, 4.982 , 2.321 , 2.2236, 2.691 , 2.3421, 2.5863, 2.4168,
       2.2454, 1.8718, 2.5389, 2.24  , 3.7125, 4.032 , 3.8766, 3.8364,
       4.2733, 3.6156, 3.15  , 3.9128, 3.864 , 3.0734, 2.3985, 2.5338,
       2.3932, 2.5875, 2.364 , 2.508 , 2.4428, 2.34  , 2.2374, 2.575 ,
       2.0601, 2.6966, 2.6004, 2.5578, 2.5132, 2.3247, 2.0944, 2.093 ,
       2.1063, 2.1358, 2.0905, 2.3814, 2.0634, 2.0383, 2.223 , 2.1294,
       2.1344, 2.1056, 2.1411])

5.1.2. 配列の属性#

NumPy の配列は、データそのものに加えて、配列の構造に関する属性も保持しています。たとえば、次のように .ndim を使えば、配列の次元数を取得できます。

x = [[0, 1, 2, 3],
     [4, 0, 5, 6],
     [7, 8, 0, 9]]
a = np.array(x)
a.ndim
2

各次元の要素数は .shape で取得できます。この例では 1 次元目の要素が 3 つであり、2 次元目の要素が 4 つです。そのため、.shape は次のように出力されます。

a.shape
(3, 4)

配列の全要素数は .size で取得できます。この全要素数は、.shape の出力値の積に一致します。

a.size
12

5.1.3. 配列の要素#

配列から個々の要素を取り出すには、Python のリストと同様に [] を使ってインデックスを指定します。一次元配列の場合、[] の中に 1 つのインデックスを指定することで、対応する要素を取得できます。

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
x[3]
np.int64(4)

ここでは np.int64(4) と表示されますが、これは「4」という数値が NumPy の 64 ビット整数型(int64)であることを示しています。計算や処理には何の支障もなく、通常の整数と同様に扱うことができます。たとえば、次のように 1 を加えてみると、正しく 5 が得られます。

x[3] + 1
np.int64(5)

なお、どうしてもこの表記が気になる場合は、print 関数を使って出力すると、型情報はなく数値だけが表示されます。

print(x[3])
4

複数の要素を取り出すには、スライス機能(:)を使います。: の前に開始位置、後ろに終了位置を指定することで、その範囲の要素を取り出すことができます。ただし、終了位置に指定したインデックスの要素は取り出されません。理由?人間の都合です[1]

x[1:4]
array([2, 3, 4])

スライスで開始位置を省略すると先頭から、終了位置を省略すると最後までの範囲を意味します。

x[:4]
array([1, 2, 3, 4])
x[4:]
array([5, 6, 7, 8, 9])

二次元配列でも同様に値を取得できますが、二つの次元それぞれに対してインデックスを指定する必要があります。各次元のインデックスはカンマ(,)で区切って指定します。

例えば、配列 x の 1 行 2 列目の要素を取得する場合は、次のようにします。

x = np.array([[ 0,  1,  2,  3,  4],
              [ 5,  6,  7,  8,  9],
              [10, 11, 12, 13, 14],
              [15, 16, 17, 18, 19]])
x[0, 1]
np.int64(1)

二次元配列でもスライスを使って、連続した範囲の部分配列を取り出すことができます。

x[2:4, 1:3]
array([[11, 12],
       [16, 17]])
x[:3, 2:]
array([[ 2,  3,  4],
       [ 7,  8,  9],
       [12, 13, 14]])
x[:, 1:3]
array([[ 1,  2],
       [ 6,  7],
       [11, 12],
       [16, 17]])

5.1.4. 論理インデックス#

配列から値を取得する際には、単にインデックスを指定して特定の位置の値を取り出すだけでなく、特定の条件を満たす要素だけを抽出することも可能です。そのためには、まず配列全体に対して条件判定を行い、論理インデックスを作成します。そして、この論理インデックスを元の配列に適用することで、条件を満たす要素のみを抽出できます。

次の例では、一次元配列 x に対して、論理配列 f を使って要素を選択しています。f の値が True となっている位置(インデックスが 1、3、5)に対応する要素だけが取り出されています。

x = np.array([    0,    1,     2,    3,     4,    5])
f = np.array([False, True, False, True, False, True])

x[f]
array([1, 3, 5])

論理配列は、条件式を使用して簡単に生成できます。例えば、奇数のみを抽出する場合、次のように条件式 (x % 2 == 1) を用います。

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
f = (x % 2 == 1)

x[f]
array([1, 3, 5, 7, 9])

複数の条件を組み合わせる場合は、論理積(&)や論理和(|)を使用します。例えば、「5 より大きい奇数」を取得するには、奇数を表す条件 (x % 2 == 1) と、5 より大きいことを表す条件 (x > 5)& で組み合わせます。

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
f1 = (x % 2 == 1)
f2 = (x > 5)

x[f1 & f2]
array([7, 9])

一方、「5 より大きいまたは奇数」の場合は、論理和(|)を使用します。

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
f1 = (x % 2 == 1)
f2 = (x > 5)

x[f1 | f2]
array([1, 3, 5, 6, 7, 8, 9])

なお、論理配列を事前に作らなくても、条件式をそのまま配列に適用することで、同じように条件を満たす要素を抽出することができます。

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
x[(x % 2 == 1)]
array([1, 3, 5, 7, 9])

二次元配列の場合も同様に操作ができます。例えば、1 列目の要素が 0 である行をすべて取得する場合は次のようにします。

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

f = (x[:, 0] == 0)
x[f, :]
array([[ 0,  1,  2,  3],
       [ 0, 10, 11, 12]])

1 行目の要素が奇数である列を取得するには次のようにします。

f = (x[0, :] % 2 == 1)
x[:, f]
array([[ 1,  3],
       [ 4,  6],
       [ 7,  9],
       [10, 12]])

本節で NumPy 配列の作り方や値の取り出し方について紹介しました。配列に慣れることで、データ処理は格段に効率よくなります。たぶん、使う機会はないけど。そして、そのうちさっぱり忘れてる。でも安心してください。筆者も毎年、授業前に自分の資料を見ながら「おっ、意外とわかりやすく書けてるじゃん」って思ってます。自分で。