NumPyの多次元配列ndarrayには、データ型を表現するdtypeというプロパティがあります。このdtypeを指定することで、要素のデータ型を指定して確保するメモリ量を調節することができます。

本記事では、dtypeに焦点を当てながらNumPyで指定できるdtypeの種類と指定方法、そして内部でdtypeが活用される仕組みについて解説していきます。

dtypeが必要な理由

NumPyは基本的には、大量のデータ操作を高速に実行できるように内部ではCで実装されています。Python自体はそれほど高速な言語ではないため、行列演算の操作やデータの扱いはCから行われます。

つまり、正しくNumPy配列のデータ型を指定することでPythonからでもメモリ効率と実行効率の良いコードを実装することができます。

データ型

まずは、NumPyで使うことのできるデータ型についてまとめます。NumPyのデータ型を型で分類すると整数のint、浮動小数のfloat、複素数のcomplex、符号なしの整数のuint、真偽値のboolになります。さらに、型ごとに1要素あたりに確保するデータサイズをビットで指定することができます。

データ型ごとに分類すると以下の通りになります。

int (符号付きの整数)

データ型 概要
int8 8ビットの符号付き整数
int16 16ビットの符号付き整数
int32 32ビットの符号付き整数
int64 64ビットの符号付き整数

uint (符号なしの整数)

データ型 概要
uint8 8ビットの符号なし整数
uint16 16ビットの符号なし整数
uint32 2ビットの符号なし整数
uint64 64ビットの符号なし整数

float (浮動小数点数) [1]

データ型 概要
float16 16ビットの浮動小数点数
float32 32ビットの浮動小数点数
float64 64ビットの浮動小数点数
float128 128ビットの浮動小数点数

bool –真偽値–

データ型 概要
bool TrueかFalseで表される、真偽値。データ量としては8ビット

まずは、それぞれのデータ型でどのような表記がされるのかを確かめてみましょう。NumPyのndarrayのdtypeは、arr.dtypeのようにして知ることができます。

  
In [1]: import numpy as np

In [2]: a = np.array([0, 1, 2]) # まずは何も指定しない状態で配列を生成。

In [3]: a.dtype # データ型を確かめる。
Out[3]: dtype('int64')

In [4]: b = np.array([0, 1, 2], dtype = 'int32') # ビット数を下げてみる。

In [5]: b.dtype
Out[5]: dtype('int32')

In [6]: b
Out[6]: array([0, 1, 2], dtype=int32)

In [7]: c = np.array([0, 1, 2], dtype = 'float') # floatやintのデフォルトのビット数は64。

In [8]: c # cの要素の表記のされかたが'int'の時と異なっていて、小数点がついている。
Out[8]: array([ 0.,  1.,  2.])

In [9]: d = np.array([3e50, 4e35], dtype = 'int64') # 桁数を非常に大きくしてみると、'int'では表示しきれなくなってエラーが返ってくる。
---------------------------------------------------------------------------
(エラーメッセージが表示される)

OverflowError: Python int too large to convert to C long

In [10]: d = np.array([3e50, 4e35], dtype = 'float64') # floatにすれば問題なく配列を生成できる。

In [11]: e = np.array([3.5, 4.2, -4.3], dtype = 'int') # 小数の形で表した配列も、データ型を'int'に設定すると整数部分だけが残る。

In [12]: e
Out[12]: array([ 3,  4, -4])

中身の要素をintfloatの値を指定しても、データ型がboolだった場合は、勝手にbool値へとキャストされた要素が入ります。

In [13]: f = np.array([0, 3, 0, -1], dtype = 'bool') # bool値は0ならばFalse、0以外ならばTrueとなる。

In [14]: f
Out[14]: array([False,  True, False,  True], dtype=bool)

原則として、配列が生成された後でデータ型の変換はおすすめしません。

データ型で指定できるビット数は変換される前のデータ型のビット列を何ビットごとに区切って読み取るか、という風に読み替えることもでき、ビット数を変換してしまうとそもそも返ってくる数字列が異なることがあります。

同様に、データ型を変換させてもデータの読み取り方を変えているだけなので、データ型を元に戻すと元の配列を再現することができます。サンプルコードを見ながらそれを確かめていきましょう。

In [15]: g = np.array([0., 1., 2.], dtype = 'int64')

In [16]: g
Out[16]: array([0, 1, 2])

In [17]: g.dtype = 'int32' # データ型を'int32'に変換。

In [18]: g
Out[18]: array([0, 0, 1, 0, 2, 0], dtype=int32)

In [19]: g.dtype = 'float64' # floatに変換してみる。

In [20]: g
Out[20]: array([  0.00000000e+000,   4.94065646e-324,   9.88131292e-324])

In [21]: g.dtype = 'float32' # ビット数を変える。

In [22]: g
Out[22]:
array([  0.00000000e+00,   0.00000000e+00,   1.40129846e-45,
         0.00000000e+00,   2.80259693e-45,   0.00000000e+00], dtype=float32)

In [23]: g.dtype = 'int64' # データ型を元に戻すと値も元に戻る。

In [24]: g
Out[24]: array([0, 1, 2])

これらのビット数の違いを確かめます。8ビット(bit) = 1バイト(byte)です。

In [27]: h = np.random.randint(10, size = 100, dtype = 'int8') # 0~9までの整数を100個生成。データ型はint8

In [28]: i = np.random.randint(10, size = 100, dtype = 'int64') # こちらはビット数を64に設定。

In [29]: h.nbytes # バイト数を見てみる。
Out[29]: 100

In [30]: i.nbytes # hの8倍になっている。
Out[30]: 800

参考

[1] ビットで表す数字の世界~浮動小数点編~ FPGA 2.0