検索
連載

[NumPy超入門]異なる型のデータを格納する構造化配列を使ってみようPythonデータ処理入門

NumPyの多次元配列の要素は全て同じ型である必要があります。が、異なる型のデータを1つの配列に格納したいこともあるはずです。それを可能にする構造化配列を紹介します。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
「Pythonデータ処理入門」のインデックス

連載目次

連載概要

 本連載はPythonについての知識を既にある程度は身に付けている方を対象として、Pythonでデータ処理を行う上で必須ともいえるNumPyやpandas、Matplotlibなどの各種ライブラリの基本的な使い方を学んでいくものです。そして、それらの使い方をある程度覚えた上で、それらを活用してデータ処理を行うための第一歩を踏み出すことを目的としています。


 前回はNumPyが提供するnumpy.datetime64クラスとnumpy.timedelta64クラスを紹介し、NumPyにおける日付の取り扱いについて話しました。その最後に、日付を含むCSVファイルをnumpy.loadtxt関数で読み込もうとすると例外が発生するのを確認しました。

from pathlib import Path

print(Path('test.csv').read_text())
# 出力結果:
#date,value0,value1
#2024-01-01,5.,2.
#2024-01-02,8.,7.
#2024-01-03,1.,10.
#2024-01-04,6.,2.

data = np.loadtxt('test.csv', delimiter=',', skiprows=1# ValueError

日付を含むCSVファイルを読み込もうとするとValueError例外が発生した

 このとき発生した例外には「could not convert string '2024-01-01' to float64 at row 0, column 1.」というメッセージが付記されていました。つまり、日付を浮動小数点数値に変換できないので読み込みに失敗したということです。

Visual Studio Codeで上記コードを実行したところ
Visual Studio Codeで上記コードを実行したところ

 このようなCSVファイルを読み込むには実際には以下のようなコードを書くことになります。

dtype = np.dtype([('date', 'M8[D]'), ('value0', 'float64'),
                  ('value1', 'float64')])
data = np.loadtxt('test.csv', delimiter=',', skiprows=1, dtype=dtype)

日付を含むCSVファイルを読み込めるコード

 上のコードで行っていることを簡単に説明すると、次のようになります。

  • 1行目:構造化データ型の定義
  • 2行目:上で定義した構造化データ型を配列の要素のデータ型として指定して、構造化配列としてCSVファイルの内容を読み込む

 というわけで、以下では構造化データ型と構造化配列について見ていくことにしましょう。

 構造化データ型や構造化配列はC言語の構造体的な使い方やC言語で書かれたコードとのやり取りを考えて作られたもので、データ分析やデータ処理という立場から見ると使い勝手が良いとはいいにくいものです。異なる種類のデータを格納している表形式データを扱うのであれば、pandasなどの方がうまく扱えるはずです。

 ではありますが、今回例に挙げる日付と数値データをひとまとめにして扱う話の他にも名前(文字列)と何らかの数値データが並んだ表形式データを扱うことだって考えられます。NumPyでそうした処理を考えるのであれば、構造化配列の知識はあっても損はないでしょう。


構造化配列とは

 構造化配列(Structured Array)とはいってしまえば、異なる種類のデータ型をまとめて1つのデータ型と見なし、それを多次元配列のデータ型としたものです。このひとまとめにしたデータ型のことをNumPyでは「構造化データ型」(Structured Datatype)と呼び、それを用いて作成された多次元配列のことを「構造化配列」と呼ぶのは既に述べた通りです。

 構造化データ型を構成するデータ型(subtype)を「フィールド」と呼びます。フィールドはそれにアクセスするための名前やそこに格納するデータの型などの情報を含んでいます。

 先ほどの例であれば、作成する構造化配列の構造化データ型は「numpy.datetime64クラスの日付(M8[D])、64bitの浮動小数点数値(float64)、同じく64bitの浮動小数点数値(float64)」を組み合わせたものとなります。そして、それらのフィールドの名前は、'date'、'value0'、'value1'です。

構造化データ型の定義

 構造化データ型はnumpy.dtypeクラスのインスタンスとして作成することもありますが、単にリストを使って「[(フィールド名, データ型), (フィールド名, データ型), ……]」のように指定することもあります。以下に例を示します。

dtype = np.dtype([('field0', np.int32), ('field1', np.float64)])
print(dtype)  # [('field0', '<i4'), ('field1', '<f8')]

構造化データ型の定義

 この例では、32bit整数値と64bit浮動小数点数値で構成される構造化データ型を定義しています。リストの要素となっているタプルではフィールド名とそのフィールドのデータ型を「('field0', np.int32)」「('field1', np.float64)」のように指定しています。タプルの第0要素である'field0'と'field1'がフィールドの名前、第1要素であるnp.int32とnp.float64が2つのフィールドに格納されるデータの型です(今回紹介するのはこれら2つだけですが、より多くの情報を含めることも可能です)。

 ちなみにフィールド名を空文字にすると、'f0'、'f1'、……のような名前が自動的に付けられます。以下はその例です。

dtype = np.dtype([('', np.int32), ('', np.float64)])
print(dtype)  # [('f0', '<i4'), ('f1', '<f8')]

フィールド名を省略した場合

 構造化データ型だけではなく、NumPyではデータ型が以下のような方法で指定可能です。

  • numpy.int32やnumpy.float64のようなNumPyの型(あるいはPythonに組み込みのデータ型)をそのまま指定する
  • 'int32'や'float64'のようにNumPyの型(あるいはPythonに組み込みのデータ型)を文字列として表現したものを使用する
  • 'i4'や'f8'のようにデータ型を表す「型コード」を使用する

 「print(dtype)」行の出力結果は最後の型コードを使用したものになっています。このときには複数バイトで構成される1個のデータをリトルエンディアン形式でメモリに最下位バイトから順に格納するか('<')、ビッグエンディアン形式でメモリに最上位バイトから順に格納するか('>')を示す文字も指定できます(データ型を出力したときに'<'や'>'が付加されることもあります)。通常はエンディアンを指定しない、つまりデフォルトのエンディアンを使えば問題ありません。エンディアンの指定は、異機種間でバイナリデータの互換性を保つための機能であり、一般的なデータ分析や機械学習には不要な機能です。

 また、'i'や'f'の後にはそのデータサイズをバイト数で指定できます。'i4'なら「4バイトの整数型」を、'f8'なら「8バイトの浮動小数点数型」ということです(以下の表の'b'、'B'、'?'のようにバイトサイズが固定なものもあり、それらについてはサイズを指定できません)。

 このような型コードとしては以下のものがあります。

型コード データ型
'?' ブーリアン型
'b' 符号付きの1バイト整数型
'B' 符号なしの1バイト整数型
'i' 符号付き整数型
'u' 符号なし整数型
'f' 浮動小数点数型
'c' 複素数型
'm' numpy.timedelta64型
'M' numpy.datetime64型
'O' Pythonのobject型
'S'、'a' 0終端するバイト列
'U' Unicode文字列
'V' 生データ
型コード

 前回も見たように'M'はnumpy.datetime64クラスの型コード、'm'はnumpy.timedelta64クラスの型コードですが、これらはさらに'M[D]'や'm[s]'のような日付単位や時刻単位を指定可能です。

 実際に型コードを使ってnumpy.dtypeオブジェクトを作成する例を見てみましょう(ここではシンプルにするために構造化データ型は作成しません)。

dt = np.dtype('i8')
print(dt)  # int64
dt = np.dtype('f8')
print(dt)  # float64
dt = np.dtype('U8')
print(dt)  # '<U8'
dt = np.dtype('?')
print(dt)  # bool
dt = np.dtype('M8[D]')
print(dt)  # datetime64[D]

型コードを使ったnumpy.dtypeオブジェクトの作成

 これらの要素を使って構造化データ型を定義したのが最初に見てもらったコードです。

dtype = np.dtype([('date', 'M8[D]'), ('value0', 'float64'),
                  ('value1', 'float64')])

構造化データ型を定義するコード(再掲)

 ここでは3つのフィールドからなる構造化データ型を定義しています。最初のフィールドの名前は'date'でdatetime64[D]型のデータを格納し、次のフィールドの名前は'value0'で64bitの浮動小数点数値を格納し、最後のフィールドの名前は'value1'で同じく64bitの浮動小数点数値を格納することが分かります。

 この構造化データ型のデータ1個が実際にどれだけのメモリを消費するかはitemsize属性で調べられます。

print(dtype.itemsize)  # 24

上で定義した構造化データ型のメモリサイズは24B

 この例では、上で定義した構造化データ型のデータ1つが24Bのメモリを必要とすることが分かりました。これはnumpy.datetime64クラスのインスタンスが8B、2つあるfloat64のデータがそれぞれ8Bを必要とするからです。

 そして、この構造化データ型は日付、数値、数値という3カラムで構成されているサンプルのCSVファイルのフォーマットに一致しているので、numpy.loadtxt関数でdtypeパラメーターにこれを渡してやれば、うまくCSVファイルの内容を読み込めるというわけです。

data = np.loadtxt('test.csv', delimiter=',', skiprows=1, dtype=dtype)

作成した構造化データ型を用いてCSVファイルを読み込む

 読み込んだデータを表示してみたのが以下です。

読み込んだデータを表示したところ
読み込んだデータを表示したところ

 なお、構造化データ型は多種多様な方法で作成できるため、本稿ではその全てを紹介し切れません。詳細については、NumPyのドキュメント「Structured arrays」「Data type objects (dtype)」などを参照してください。

構造化配列の作成と操作

 とまあ、日付データを含むCSVを読み込めるようにはなりましたが、それで終わりというわけにもいきません。CSVファイルの読み込みではなく、先ほど作成した構造化データ型を基に構造化配列(多次元配列)を作成して、そこにデータを格納できるようにするにはどうすればいいかまで知りたいとは思いませんか。

 というわけで、以下のコードで日付データとそれ以外のデータを読み込んでおきましょう。構造化配列を作成したら、これらのデータをそれに代入してみます。

data = np.loadtxt('test.csv', delimiter=',', usecols=[1, 2], skiprows=1)
print(data)
# 出力結果:
#[[ 5.  2.]
# [ 8.  7.]
# [ 1. 10.]
# [ 6.  2.]]

dates = np.loadtxt('test.csv', delimiter=',', usecols=[0], skiprows=1, dtype='M8[D]')
print(dates)  # ['2024-01-01' '2024-01-02' '2024-01-03' '2024-01-04']

日付データとそれ以外のデータを別々に読み込んでおく

 変数dataにはCSVファイルの日付データ以外の部分が、変数datesには日付データ部分が読み込まれています(日付データの読み込みではdtypeパラメーターに先ほども見た'M8[D]'を渡している点にも注目しましょう)。

構造化配列の作成

 では、構造化配列を作成します。といっても、これはnumpy.array関数でdtypeパラメーターに先ほど作成した構造化データ型(dtype)を渡すだけです。このときに、[0, 0, 0, 0]を渡していますが、これは構造化配列の要素の初期化に使われます(CSVファイルは4行のデータを格納しているので、ここでは明示的に4個の0を要素とするリストを渡しています)。

sa = np.array([0, 0, 0, 0], dtype=dtype)
sa
# 出力結果:
#array([('1970-01-01', 0., 0.), ('1970-01-01', 0., 0.),
#       ('1970-01-01', 0., 0.), ('1970-01-01', 0., 0.)],
#      dtype=[('date', '<M8[D]'), ('value0', '<f8'), ('value1', '<f8')])

構造化配列の作成

 これにより、日付が「'1970-01-01'」で他の要素の値が0である構造化配列が作成されました。この日付なのは、前回も述べましたが、numpy.datetime64クラスでは0が1970年1月1日を意味するようになっているからです。また、[0, 0, 0, 0]という4要素のリストを渡したところ、構造化配列の各フィールドの値が全て0に初期化されました。興味のある方はリストを例えば[0, 1, 2, 3]のようにすることで、第0要素から第3要素までの値がどうなるかを確認してください。

 また、次の行にも注目です。

print(sa.shape)  # (4,)

構造化配列の形状を調べる

 「(4,)」という出力結果が教えてくれるのは、この構造化配列は複数のデータを格納する構造化データ型の値を要素とする1次元配列だということです。4行3列の2次元配列ではないことには注意しましょう。

 あるいはnumpy.ndarrayクラスのインスタンスを直接作成する方法も考えられますが、ここでは取り上げません。

構造化配列への代入

 作成した構造化配列へ読み込んだデータを代入していきましょう。まずは日付からです(環境によっては’value0’フィールドと'value1'フィールドの値が異なっているかもしれません)。

sa['date'] = dates
sa
# 出力結果:
#array([('2024-01-01', 0., 0.), ('2024-01-02', 0., 0.),
#       ('2024-01-03', 0., 0.), ('2024-01-04', 0., 0.)],
#      dtype=[('date', '<M8[D]'), ('value0', '<f8'), ('value1', '<f8')])

構造化配列への値の代入

 出力結果を見ると、日付が変わっていることが確認できますね。

 先述のように、構造化配列のフィールドにはその名前を使ってアクセスします。この例では「sa['date']」とすることで、構造化配列のdateフィールドへ代入(書き込みアクセス)をしています。もちろん、参照(読み取りアクセス)も可能です。

print(sa['date'])  # ['2024-01-01' '2024-01-02' '2024-01-03' '2024-01-04']

構造化配列のdateフィールドの値を参照

 ここでsa['date']フィールドにPythonの整数値(スカラー値)を1つだけ代入してみましょう。

sa['date'] = 3
sa['date']
# 出力結果:
#array(['1970-01-04', '1970-01-04', '1970-01-04', '1970-01-04'],
#      dtype='datetime64[D]')

スカラー値はフィールドの全ての要素に代入される

 この場合、構造化配列の対応するフィールドの全ての要素に値が代入されます。ただし、「sa = 3」のようにすると、これは変数saに3を代入するだけになります。実験が終わったら「sa['date'] = dates」を実行して、次のフィールドに進みましょう。

sa['value0'] = data[:, 0]
sa
# 出力結果:
#array([('2024-01-01', 5., 0.), ('2024-01-02', 8., 0.),
#       ('2024-01-03', 1., 0.), ('2024-01-04', 6., 0.)],
#      dtype=[('date', '<M8[D]'), ('value0', '<f8'), ('value1', '<f8')])

'value0'フィールドへの代入

 'value0'フィールドでもやっていることは上と同様です。ただし、変数dataに読み込んだデータは4行2列の多次元配列なので、その第0列を代入するようにここでは「data[:, 0]」のような表記をしています。出力結果を見ると、'value0'フィールドの値が変わっていることが確認できました。

 最後のフィールドも同様です。

sa['value1'] = data[:, 1]
sa
# 出力結果:
#array([('2024-01-01', 5.,  2.), ('2024-01-02', 8.,  7.),
#       ('2024-01-03', 1., 10.), ('2024-01-04', 6.,  2.)],
#      dtype=[('date', '<M8[D]'), ('value0', '<f8'), ('value1', '<f8')])

'value1'フィールドへの代入

 ここで元のCSVファイルの内容を確認しておきましょう。

print(Path('test.csv').read_text())
# 出力結果:
#date,value0,value1
#2024-01-01,5.,2.
#2024-01-02,8.,7.
#2024-01-03,1.,10.
#2024-01-04,6.,2.

元のCSVファイル

 各行各列の値を見る限りは、自分で構造化配列を作成して代入していくことで、numpy.loadtxt関数で読み込んだものと同じ結果が得られました。

複数フィールドや行データの参照と代入

 複数のフィールドの値を同時に参照するときには、角かっこ「[]」の内側にさらに角かっこを置き、その中に参照したいフィールドの名前を記述します。

print(sa[['date', 'value1']])
# 出力結果:
#[('2024-01-01',  2.) ('2024-01-02',  7.) ('2024-01-03', 10.)
# ('2024-01-04',  2.)]

複数のフィールドの参照

 この方法でフィールドを指定して、そこに値を代入することも可能です。以下に例を示します。

sa[['date', 'value1']] = [('2025-01-01', 20.), ('2025-02-01', 15.), ('2025-03-01', 10.), ('2025-04-01', 5)]
print(sa)
# 出力結果:
#[('2025-01-01', 5., 20.) ('2025-02-01', 8., 15.) ('2025-03-01', 1., 10.)
# ('2025-04-01', 6.,  5.)]

複数フィールドへの代入

 この例では4行分の'date'フィールドと'value1'フィールドの値をリストの要素として代入しています。一方、以下のような代入も可能です。

sa[['value0', 'value1']] = (1, 2)
print(sa)
# 出力結果:
#[('2025-01-01', 1., 2.) ('2025-02-01', 1., 2.) ('2025-03-01', 1., 2.)
# ('2025-04-01', 1., 2.)]

複数フィールドへの代入その2

 こちらでは要素を2つ持つタプルを1個だけ代入していますが、このときには全ての要素へその値が代入されます。

 行データを参照するときには、整数値のインデックスを使用します。

print(sa[0])  # ('2025-01-01', 5., 20.)

第0行のデータを参照

 行を示す整数値のインデックスとフィールド名を同時に使用することも可能です。

print(sa[0]['date'])  # 2025-01-01
print(sa[0][['date', 'value0']])  # ('2025-01-01', 5.)

整数値のインデックスとフィールド名を両方指定

 この方法で特定の要素に値を代入することもできます(ただし、整数値のインデックスに加えてフィールドを複数指定した場合は代入できないようです)。

sa[0]['date'] = '2023-12-31'
print(sa)
# 出力結果:
#[('2023-12-31', 5.,  2.) ('2024-01-02', 8.,  7.) ('2024-01-03', 1., 10.)
# ('2024-01-04', 6.,  2.)]

整数値のインデックスとフィールド名を指定して代入


 今回の話はあくまでも構造化配列とは何かを知るほんの一歩でしかありません。興味の湧いた方はNumPyの公式ドキュメントなどを参考にしていろいろと触ってみてください。

 さて、今回まではNumPyに焦点を当ててきましたが、次回からはpandasの基本的な知識をみなさんと一緒に見ていくことにしましょう。

「Pythonデータ処理入門」のインデックス

Pythonデータ処理入門

Copyright© Digital Advantage Corp. All Rights Reserved.

[an error occurred while processing this directive]
ページトップに戻る