NumPyには、ndarrayの永続化方法としてsaveload関数があります。これらの関数を使うことで、PythonとNumPyから利用するだけであれば、データベースやファイル形式に悩むことなく簡単にデータを保存することができます。

saveloadを使うと、picklenpynpz形式のファイルに読み書きすることでndarrayと変換することができます。しかし一方で、他のプログラミング言語やツールなどからデータを読み込むことができなくなってしまいます。

np.loadtextnp.savetxt関数を使用することで、このような問題を解決しながらNumPyとデータ互換を維持することができます。今回はこのnp.loadtextnp.savetxtを中心にデータの欠損がある箇所を適切な値で埋めてくれるgenfromtxt関数を紹介します。

それぞれの関数の特徴

まずはそれぞれの関数の特徴をまとめます。

np.loadnp.save関数の特徴

  • 配列をそのまま保存できる、3次元以上の配列も保存可能
  • 対応拡張子はpicklenpznpy
  • ただし、扱うファイル形式は他のアプリケーションなどとの互換性はほとんどない

np.loadtxtnp.savetxt関数の特徴

  • 他のアプリケーションと互換性のある.dat、.csv、.txt形式のファイルの読み書きができる
  • 保存できる配列の次元は二次元まで

今回はnp.loadtxtnp.savetxt関数について解説していきます。

np.savetxt、np.loadtxt

np.loadtext関数のAPIドキュメントは以下の通りです。

np.loadtxt関数

numpy.loadtxt(fname, dtype=’float’, comments=’#’, delimiter=None, converters=None, skiprows=0, usecols=None, unpack=False, ndmin=0)

params:

パラメータ名 概要
fname ファイル、文字列
またはファイルのパス
読み込むファイルを指定します・拡張子が.gzや.bz2のときはまず解凍します。
dtype データ型 (省略可能)初期値’float’
出力される配列における要素のデータ型を指定します。
comments 文字列またはシーケンス (省略可能)初期値’#’
ファイルの内容におけるコメント部分が何で始まっているのかを指定します。
delimiter 文字列(string) (省略可能)初期値None
値と値を区切る文字が何であるかを指定します。デフォルトではスペースで区切ります。
converters 辞書(dict) (省略可能)初期値None
ある列に対して関数を適用したり、データの欠落箇所に埋めあわせる値を指定します。
skiprows int もしくは
シーケンス
(省略可能)初期値0
何行文読み飛ばすかを指定します。
usecols intもしくは
シーケンス
(省略可能)初期値None
0を最初の行として、どの行を読み取るのかを指定します。デフォルトでは全ての列を読み取ります。
unpack bool値 (省略可能)初期値False
Trueにすると出力される配列は転置(transpose)されます。各々の列の値を別々の変数に代入したいときなどに用いられます。
ndmin int
有効なのは0,1,2
(省略可能)初期値0
返される配列の最低次元数を指定します。ここが指定されないと要素数が1の次元の軸(axes)に関しては次元の削減が行われます。

returns:

ファイルから読み込まれたデータが出力されます。

このloadtxt関数には9つもの引数が存在しています。これらを用いてどのような形でファイルを読み込んでいくかを詳細に指定することができます。引数を詳細に指定することで、様々な形式に対応することができます。

np.savetxt関数

次に、savetxt関数のAPIドキュメントを見ていきます。

numpy.savetxt(fname, X, fmt=’%.18e’, delimiter=’ ‘, newline=’\n’, header=’’, footer=’’, comments=’#’)

params:

パラメータ名 概要
fname ファイル名または
ファイル・ハンドル
保存するファイルを指定します。.gzが拡張子のときはgzip形式のファイルとして保存します。
X array_like
(配列に相当するもの)
保存データを指定します。
fmt stringまたは
stringのシーケンス
(省略可能)初期値’%.18e’
それぞれの列ごとにおける有効桁数を指定します。
delimiter string (省略可能)初期値’ ‘
列ごとを区切る文字を指定します。
newline string (省略可能)初期値’\n’
行ごとを区切る文字を指定します。
header string (省略可能)初期値’‘
ファイルの最初に何を書き込むかを指定します。
footer string (省略可能)初期値’‘
ファイルの最後に何を書き込むかを指定します。
comments string (省略可能)初期値’#’
headerやfooterを挿入する際、先頭に入力する文字を指定します。

どちらも引数が多いですが、参照しながらどのような形式で読み書きしたいのかを考えて使いこなしていきましょう。

サンプルコード

では、この2つの関数を使って実際にコードを見ながら使い方を見ていきます。まずはシンプルにファイルの書き込み、読み込みをしていきます。

In [1]: import numpy as np

In [3]: a = np.random.randn(3,4) # 標準正規分布に従った乱数を2次元配列で生成

In [5]: np.savetxt('sample.txt', a) # 特に何も指定しない状態で保存してみる。

In [6]: b = np.loadtxt('sample.txt') # 保存したデータを呼び出す。

In [7]: a
Out[7]:
array([[-0.98495064, -0.54105417,  1.05258227,  1.0710156 ],
       [ 1.31193658,  1.09343955, -1.2977759 ,  0.78322404],
       [-1.54344345, -1.46554656,  0.40285187,  0.1302892 ]])

In [8]: b
Out[8]:
array([[-0.98495064, -0.54105417,  1.05258227,  1.0710156 ],
       [ 1.31193658,  1.09343955, -1.2977759 ,  0.78322404],
       [-1.54344345, -1.46554656,  0.40285187,  0.1302892 ]])

In [9]: np.savetxt('sample.csv', a) # csv形式でも対応してる。

In [10]: c = np.loadtxt('sample.csv')

In [11]: c
Out[11]:
array([[-0.98495064, -0.54105417,  1.05258227,  1.0710156 ],
       [ 1.31193658,  1.09343955, -1.2977759 ,  0.78322404],
       [-1.54344345, -1.46554656,  0.40285187,  0.1302892 ]])

In [12]: np.savetxt('sample.dat', a) # dat形式でも大丈夫。

In [13]: d = np.loadtxt('sample.dat')

In [14]: d
Out[14]:
array([[-0.98495064, -0.54105417,  1.05258227,  1.0710156 ],
       [ 1.31193658,  1.09343955, -1.2977759 ,  0.78322404],
       [-1.54344345, -1.46554656,  0.40285187,  0.1302892 ]])

次は区切り文字を変えていきます。,に変更してみましょう。

In [15]: np.savetxt('sample2.txt', a, delimiter=',')

実際にファイルを開いて見てどうなっているか確認してみましょう。

-9.849506405118685359e-01,-5.410541661151561099e-01,1.052582274254199479e+00,1.071015596176571272e+00
1.311936581354347986e+00,1.093439550506633662e+00,-1.297775904738235564e+00,7.832240368728708990e-01
-1.543443447806609692e+00,-1.465546557520085447e+00,4.028518723924314759e-01,1.302891977491596742e-01

ちなみに、先ほど使用したsample.txtの方はどうなっているのでしょうか。

-9.849506405118685359e-01 -5.410541661151561099e-01 1.052582274254199479e+00 1.071015596176571272e+00
1.311936581354347986e+00 1.093439550506633662e+00 -1.297775904738235564e+00 7.832240368728708990e-01
-1.543443447806609692e+00 -1.465546557520085447e+00 4.028518723924314759e-01 1.302891977491596742e-01

こちらはデフォルト通り、スペースでデータが区切られていますね。では、,で区切ったテキストファイルを読み込んでみます。

In [16]: e = np.loadtxt('sample2.txt') # delimiterに何も指定しないとエラーが返ってくる
-------------------------------------------------------------------
(エラーメッセージが表示される)
ValueError: could not convert string to float: b'-9.849506405118685359e-01,-5.410541661151561099e-01,1.052582274254199479e+00,1.071015596176571272e+00'

In [17]: e = np.loadtxt('sample2.txt', delimiter=',')

In [18]: e
Out[18]:
array([[-0.98495064, -0.54105417,  1.05258227,  1.0710156 ],
       [ 1.31193658,  1.09343955, -1.2977759 ,  0.78322404],

np.savetxt関数ではデータを保存する際に有効桁数を指定することができます。デフォルトで指定されている’%.18e’というのは小数点以下18桁まで表示し、オーダーをe08e-01のような形式で表すことを示します(1番大きい桁が1の位に来るように表記されるということです)。

’%.18f’にすると小数点以下18桁まで表示されますがオーダーを表す記号が存在せず、322.248..のように表示されます。上の上限もつけることができ’%3.f’とすれば1000の位までしか表記できなくなります。

dtype = complex(複素数)のときは’%.3e + .2ej’のように表すことができます。タプルやリストの形式で複数の列に対して個々の有効数字を指定することができます。

有効数字2桁でまずはやってみましょう。

In [19]: np.savetxt('sample3.txt', a, fmt ='%.2e')

ファイルの中身は以下のようになっています。

-9.85e-01 -5.41e-01 1.05e+00 1.07e+00
1.31e+00 1.09e+00 -1.30e+00 7.83e-01
-1.54e+00 -1.47e+00 4.03e-01 1.30e-01

では次はeからfに変えて見ます。  

In [20]: np.savetxt('sample4.txt', a, fmt ='%.2f')

ファイルの中身は以下のようになります。

-0.98 -0.54 1.05 1.07
1.31 1.09 -1.30 0.78
-1.54 -1.47 0.40 0.13

eの表記が消えたのがわかるでしょうか。次は複素数を保存して見ます。

In [21]: f = np.array([[10.1+3.21j,100+32.1j],[20.0+0.2j, 22.1-1j]]) # 複素数を要素とする配列を生成


In [22]: np.savetxt('sample6.txt', f, fmt= ['%.3e + %.3ej', '%.1e + %.1ej'])

もちろん実部虚部を別々に指定せず'%.2e'のようにして両方とも同じ値を設定することもできます。ファイルの中身を見て見ましょう。

1.010e+01 + 3.210e+00j 1.0e+02 + 3.2e+01j
2.000e+01 + 2.000e-01j 2.2e+01 + -1.0e+00j

ちゃんと指定した有効桁数通りになっていますね。

次はloadtxtで抜き出す列や行を変えていきます。sample4.txtを使っていきます。

In [44]: np.loadtxt('sample4.txt', usecols = (0,2)) # 0,2列目だけを使う。
Out[44]:
array([[-0.98,  1.05],
       [ 1.31, -1.3 ],
       [-1.54,  0.4 ]])

In [45]: np.loadtxt('sample4.txt', skiprows = 1) # 0行目をとばす
Out[45]:
array([[ 1.31,  1.09, -1.3 ,  0.78],
       [-1.54, -1.47,  0.4 ,  0.13]])

次はheaderfooterを指定します。最初の乱数配列であるaを保存します。

In [46]: np.savetxt('sample7.txt', a, fmt ='%.3e', header='this is a header',footer='this is a footer')

中身を確認してみます。

# this is a header
-9.850e-01 -5.411e-01 1.053e+00 1.071e+00
1.312e+00 1.093e+00 -1.298e+00 7.832e-01
-1.543e+00 -1.466e+00 4.029e-01 1.303e-01
# this is a footer

#を頭文字としてちゃんと入力されていることがわかりますね。では、ここの#の部分を>>>に変えてみましょう。np.loadtxtを使って対象行を無視してみます。

np.loadtxt,np.savetxt共にcommentsに指定すれば対象行を無視することができます。

In [48]: np.savetxt('sample8.txt', a, fmt ='%.3e', header='this is a header',footer='this is a footer', comments='>>>')

In [49]: np.loadtxt('sample8.txt', comments='>>>') # loadtxtでちゃんと読み込めるか確かめる。
Out[49]:
array([[-0.985 , -0.5411,  1.053 ,  1.071 ],
       [ 1.312 ,  1.093 , -1.298 ,  0.7832],
       [-1.543 , -1.466 ,  0.4029,  0.1303]])

ファイルの中身も確認しておきましょう。sample8.txtの中身を見て見ます。

>>>this is a header
-9.850e-01 -5.411e-01 1.053e+00 1.071e+00
1.312e+00 1.093e+00 -1.298e+00 7.832e-01
-1.543e+00 -1.466e+00 4.029e-01 1.303e-01
>>>this is a footer

ちゃんと指定できていました。

次に、loadtxtを使ってデータ型を指定して見ます。以下のようなアンケート結果のようなcsvデータを考えて見ます。(ファイル名はfoo.csv扱います。)

# age gender tall[cm] driver's_lisense
18 female 154.1 No
21 male 172.3 Yes
22 female 160.8 Yes
23 male 180.1 Yes
25 female 145.0 No

左か年齢、性別、身長、運転免許の有無についてです。これらを列ごとにデータ型を指定しながら読み込んでいきます。

In [10]: np.loadtxt('foo.csv', dtype=[('col1', 'i8'), ('col2', 'S10'), ('col3', 'f8'), ('col4', 'S10')]) # 8バイトのint、10バイトのstr, 8バイトのfloat,10バイトのstrの順に指定している。
Out[10]:
array([(18, b'female',  154., b'No'), (21, b'male',  172., b'Yes'),
       (22, b'female',  160., b'Yes'), (23, b'male',  180., b'Yes'),
       (25, b'female',  145., b'No')],
      dtype=[('col1', '<i8'), ('col2', 'S10'), ('col3', '<f8'), ('col4', 'S10')])

unpack = Trueにすることで転置をほどこすことができ、各要素ごとに配列へ収納することができます。

In [11]: np.loadtxt('foo.csv', dtype=[('col1', 'i8'), ('col2', 'S10'), ('col3','f8'), ('col4', 'S10')], unpack =True) # unpack=Trueにすると転置が起こる。  
Out[11]:
[array([18, 21, 22, 23, 25]),
 array([b'female', b'male', b'female', b'male', b'female'],
       dtype='|S10'),
 array([ 154.,  172.,  160.,  180.,  145.]),
 array([b'No', b'Yes', b'Yes', b'Yes', b'No'],
       dtype='|S10')]

In [12]: age, gender, tall, driver_lisense = np.loadtxt('foo.csv', dtype=[('col1', 'i8'), ('col2', 'S10'), ('col3', 'f8'), ('col4', 'S10')], unpack =True)

In [13]: age
Out[13]: array([18, 21, 22, 23, 25])

In [14]: gender
Out[14]:
array([b'female', b'male', b'female', b'male', b'female'],
      dtype='|S10')

In [15]: tall
Out[15]: array([ 154.,  172.,  160.,  180.,  145.])

In [16]: driver_lisense
Out[16]:
array([b'No', b'Yes', b'Yes', b'Yes', b'No'],
      dtype='|S10')

引数convertersを用いて文字列データをあらかじめ数値に変えておくとのちの処理も楽になるかもしれません。例えば性別でmaleを1,femaleを-1とし、driver's_lisenseYes1, No-1にするとします。以下のように2つの関数を設定してから読み込みます。

In [20]: def driver_lisense(str): # 関数を2つ定義する。
    ...:     if str == b'Yes' : return 1
    ...:     else: return -1
    ...:     

In [21]: def gender(str):
    ...:     if str == b'male': return 1
    ...:     else: return -1
    ...:     

In [22]: np.loadtxt('foo.csv', converters={1: lambda s: gender(s), 3: lambda s: driver_lisense(s)}) # 文字列のデータを関数を使って数値に変換。
Out[22]:
array([[  18.,   -1.,  154.,   -1.],
       [  21.,    1.,  172.,    1.],
       [  22.,   -1.,  160.,    1.],
       [  23.,    1.,  180.,    1.],
       [  25.,   -1.,  145.,   -1.]])

このconverters引数を用いることによって空の値に対してデフォルトの値を与えることができます。では、先ほどのデータでgender欄に穴を開けて、そこの値をNaNにするよう設定して見ます。うまく認識されなかったため、ここでデータの区切りをスペースから,に変更しました。

# age gender tall[cm] driver's_lisense
18,,154,No
21,male,172,Yes
22,,160,Yes
23,,180,Yes
25,female,145,No
In [33]: def gender2(str): # 関数を改めて設定する
    ...:     if not str: return 0 # 何も入力されていないときは0を返すようにする
    ...:
    ...:     elif  str == b'male': return 1
    ...:     else: return -1
    ...:     

In [34]: np.loadtxt('foo.csv', converters={1: lambda s: gender2(s), 3: lambda s:
    ...:  driver_lisense(s)}, delimiter =',')
Out[34]:
array([[  18.,    0.,  154.,   -1.],
       [  21.,    1.,  172.,   -1.],
       [  22.,    0.,  160.,   -1.],
       [  23.,    0.,  180.,    1.],
       [  25.,   -1.,  145.,   -1.]])

np.genfromtxt

np.savetxt関数の変形版としてnp.genfromtxt関数というのも存在します。この関数を使うことで、データの欠落に対する挙動を指定することができます。

いちいち列ごとに指定する必要がなく、全ての列に対して一括で指定できます。

まずはAPIドキュメントを確認してみましょう。

np.genfromtxt(fname, dtype = ‘float’, comments=’#’, delimiter=None, skip_header=0, skip_footer=0, converters=None, missing_values = None, filling_values=None, usecols=None, names=None, excludelist=None, deletechars=None, replace_space=’_’, autostrip=False, case_sensitive=True, defaultfmt=’f%i’, unpack=None, usemask=False, loose=True, invalide_raise=True, max_rows=None)

params;

パラメータ名 概要
fname file, str
またはファイルのパス
読み込むファイルを指定します。
dtype データ型 (省略可能)初期値’float’
返される配列における要素のデータ型を指定します。
comments str (省略可能)初期値’#’
ヘッダーやフッターの目印として使われる文字を指定します。この文字が現れたあとにある文字列やデータは全て読み込まれません。
delimiter strやint
またはシーケンス
(省略可能)初期値None
データを区切る記号を指定します。デフォルトではスペース(空白)となっています。
skip_header int (省略可能)初期値0
最初に読み飛ばす行数を指定します。
skip_footer int (省略可能)初期値0
ファイルの末尾で読み飛ばす行数を指定します。
converters dict(辞書) (省略可能)初期値None
列ごとにどのような関数を適用するかを指定します。loadtxtと同じ使い方です。
missing_values variable (省略可能)初期値None
欠損データに対応する文字列の組みを指定します。
filling_values variable (省略可能)初期値None
データが欠損しているときに穴埋めする値を指定します。
usecols シーケンス (省略可能)初期値None
どの列を読み込むのか指定します。デフォルトの値では全ての列を読み込みます。
names {None, True, str, シーケンス}のいずれか (省略可能)初期値None
それぞれの列に対して名称をつけます。
exludelist シーケンス (省略可能)初期値None
除外するデータを指定します。
deletechars str (省略可能)初期値None
namesから消去する必要のある無効な文字を指定します。
defaultfmt str (省略可能)初期値’F%i’
デフォルトのフィールド名を定義します。
autostrip bool値 (省略可能)初期値False
値からスペース(空白)を自動的に取り除くかどうかを指定します。
replace_space 文字 (省略可能)初期値’_‘
スペース(空白)があった場合、そこにどんな値を入れていくかを指定します。
case_sensitive {True, False, ‘upper’, ‘lower’}のいずれか (省略可能)初期値True
各フィールドの名前を大文字にするかどうかを指定します。Trueのときはそのままで、upperFalseのときは大文字にし、lowerのときは小文字にします。
unpack bool値 (省略可能)初期値False
返す配列を転置させるかどうかを指定します。
usemask bool値 (省略可能)初期値False
マスクされた配列(無効な値などが含まれている可能性のある配列)として出力するかどうかを指定します。
loose bool値 (省略可能)初期値True
Trueの場合、無効な値を含んでいてもエラーを返しません。
invalid_raise bool値 (省略可能)初期値True
Trueの場合、列数で矛盾が発生した場合例外を発生させます。Falseの場合、警告が発せられ、行の追加が見送られます。
max_rows int (省略可能)初期値None
最大で何行読み込むかを指定します。デフォルトでは全てを読み込みます。

returns:

データを読み込んだ配列が返されます。

引数の数がnp.savetxt以上に多い上、実際に使われているものの種類が少ないため上記の表を見ながら使っていきましょう。

以下のようにデータに欠損のあるファイルを読み込ませて見ます。ファイル名はbar.txtです。

1.1,,
2.3, 5.2, -9.1
0.1,, 2.0

これを読み込ませてみると、

In [3]: np.genfromtxt('bar.txt', delimiter=',')
Out[3]:
array([[ 1.1,  nan,  nan],
       [ 2.3,  5.2, -9.1],
       [ 0.1,  nan,  2. ]])

このように、欠損部分がnanとして返されています。データの区切りが,だったのでdelimiterでそれを指定しています。データ型も変更してみます。

In [4]: np.genfromtxt('bar.txt', delimiter=',', dtype=('int', 'float', 'int')) # int, float, intと交互にした。  
Out[4]:
array([(-1,  nan, -1), (-1,  5.2, -1), (-1,  nan, -1)],
      dtype=[('f0', '<i8'), ('f1', '<f8'), ('f2', '<i8')])

このようにnp.genfromtxtは特に何も指定しなくても欠損データのところに値を入れてくれます。

参考