Python
pandas
PyData
xarray

多次元データ解析ライブラリ xarray

More than 1 year has passed since last update.

多次元データ解析ライブラリ xarray

この記事では、多次元データ解析を支援するPythonのライブラリxarrayを紹介します。
さらに詳しい情報は本家の情報を参照してください。

xarrayの特徴

背景

科学計測データは往々にして多次元になります。
例えば、複数の位置に設置したセンサで時系列データを計測する場合、
計測データは 空間チャンネル方向 × 時間方向 の二次元データになります。
さらにそのデータに短時間フーリエ変換を施す場合は 空間チャンネル方向 × 時間方向 × 周波数方向 の三次元データになったりします。

一般的にこういうデータを扱う場合、numpy の np.ndarray を使うことが多いと思います。
しかし、np.ndarray は単純な行列(もしくはテンソル)なので他の情報は別途置いておく必要があります。
上の例だと、
+ 次元の順番:二次元データのうち1次元目が空間チャンネル、二次元目が時間に対応すること
+ 各次元の座標

などがここで言う「他の情報」にあたります。

そのため、例えばその中のデータからある時間範囲の一部切り出して使う、というような場合は
切り出したデータの他に、時間軸のデータも同時に切り出しておく必要があります。

素の np.ndarray を使ってそれをきっちりやっていく、ってことももちろんできるんですが、
複雑なプログラムだとこのような煩雑な操作はミスの元になります。

xarray はそういった操作を簡単にするためのライブラリです。

(ちなみに内部では np.ndarray を用いているので、np.ndarrayの高速な演算性能をほとんど犠牲にしません)
なお、一次元のデータを扱うライブラリとしてpandasがあります。
pandasは多次元のデータを(簡単には)扱えません。xarrayはそれを補間するライブラリになっています。

特徴としては上記の他に
+ __str__ メソッドがオーバーロードされていて、print したときにその概要を表示してくれる。
+ 位置インデクシング・スライシング(例えば、ある時刻に最も近いときのデータを探すなど)が可能。
その結果もxarrayオブジェクトになり、軸に関する情報も正しく保有してくれる。
+ 簡単な統計処理(移動平均など)が可能。軸に関する情報も正しく保有してくれる。
+ pandas との相互変換が可能
+ メモリに載り切らない巨大なデータにも対応している(らしい)

などがあります。

ちなみに、

import numpy as np
import xarray as xr
xr.__version__
'0.9.1'

と言う風に、xrと略すことが一般的なようです。

データタイプ

主には xr.DataArray, xr.Dataset の2つのデータタイプをサポートしています。

xr.DataArray

xr.DataArray は上記で述べた多次元のデータです。
内部には軸の値とラベルのペアである順序付き辞書型 coords および、その他の情報を格納するattrsという順序付き辞書型を有しています。

__get_item__ メソッドをオーバーロードしているので、da[i,j]というようにnp.ndarrayと同じようにアクセスすることができます。
ただし戻り値も xr.DataArray オブジェクトになるので、軸情報などを受け継いでくれます。

xr.Dataset

xr.DataArray を複数保持するオブジェクトです。
複数の軸を有することができて、各データがどの軸に対応するかの情報を保有しておいてくれます。

辞書オブジェクトのようにアクセスすることができます。
例えば、内部に温度 T と密度 N の情報を有するxr.Datasetでは
data['T'] とすると、温度 T がxr.DataArrayとして戻ってきます。

xr.DataArray の使い方

これはpandasにおけるDataSeriesに似た役割のものです。
データの値自体と、軸のデータを有しています。

インスタンス化

data = xr.DataArray(np.random.randn(2, 3))

とすると、軸情報のない2x3の xr.DataArray オブジェクトを作成できます。

print メソッドで作成したオブジェクトの概要を表示できます。

print(data)
<xarray.DataArray (dim_0: 2, dim_1: 3)>
array([[ 0.32853 , -1.010702,  1.220686],
       [ 0.877681,  1.180265, -0.963936]])
Dimensions without coordinates: dim_0, dim_1

今回のように軸を明示的に指定しなかった場合、dim_0, dim_1が自動的に割り振られます。

例えば、あるデータdata_npの一次元目が空間位置x、二次元目が時刻tに対応する場合を考えます。

#例データ
data_np = np.random.randn(5,4)
x_axis = np.linspace(0.0, 1.0, 5)
t_axis = np.linspace(0.0, 2.0, 4)
data = xr.DataArray(data_np, dims=['x','t'], 
                    coords={'x':x_axis, 't':t_axis}, 
                    name='some_measurement')

というように、
+ dims にdata_npの各次元に対応するラベルをリスト(もしくはタプル)で、
+ coords に軸ラベルとそれに対応するデータを辞書型で与えます。

print(data)
<xarray.DataArray 'some_measurement' (x: 5, t: 4)>
array([[ 1.089975,  0.343039, -0.521509,  0.02816 ],
       [ 1.117389,  0.589563, -1.030908, -0.023295],
       [ 0.403413, -0.157136, -0.175684, -0.743779],
       [ 0.814334,  0.164875, -0.489531, -0.335251],
       [ 0.009115,  0.294526,  0.693384, -1.046416]])
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0

表示された概要のうち

<xarray.DataArray 'some_measurement' (x: 5, t: 4)>

は, このDataArray はsome_measurementという名前の 5x4 の行列で、1次元目の軸ラベルが'x'に、2次元目の軸ラベルが't'に対応していることを示しています。

また、

Coordinates:

以下は、軸データの一覧を示しています。

軸情報

軸一覧にはdimsによりアクセスすることができます。

なお、ここで表示される順序が、本来のデータの何時限目の軸に対応しているかを示しています。

data.dims
('x', 't')

軸の値にアクセスするには、ラベル名を引数にします。

data['x']
<xarray.DataArray 'x' (x: 5)>
array([ 0.  ,  0.25,  0.5 ,  0.75,  1.  ])
Coordinates:
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0

インデクシング

xarray は複数の種類のインデクシングをサポートしています。pandasの仕組みを用いているのでpandasと同様に高速です。

numpy 風のアクセス

data[0,1]
<xarray.DataArray 'some_measurement' ()>
array(0.3430393695918721)
Coordinates:
    t        float64 0.6667
    x        float64 0.0

array-likeなので通常の行列のようにアクセスできます。
その時の軸情報を引き継がれます。

positional インデクシング

.locメソッドを使うことで、軸データに沿った位置を指定してアクセスできます。

data.loc[0:0.5, :1.0]
<xarray.DataArray 'some_measurement' (x: 3, t: 2)>
array([[ 1.089975,  0.343039],
       [ 1.117389,  0.589563],
       [ 0.403413, -0.157136]])
Coordinates:
  * t        (t) float64 0.0 0.6667
  * x        (x) float64 0.0 0.25 0.5

.loc[0:0.5, :1.0]
は、1次元目の軸に沿って0 < x < 0.5 の範囲、2次元目の軸に沿って t < 1.0 の範囲のデータを切り出してくる操作です。

軸ラベル名を指定したアクセス

軸ラベル名を指定したアクセスには.isel.sel メソッドを使います。

.iselは軸ラベルを指定し、そのインデックスを整数で指定します。

data.isel(t=1)
<xarray.DataArray 'some_measurement' (x: 5)>
array([ 0.343039,  0.589563, -0.157136,  0.164875,  0.294526])
Coordinates:
    t        float64 0.6667
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0

.selは軸ラベルと、その軸値を指定します。

data.sel(t=slice(0.5,2.0))
<xarray.DataArray 'some_measurement' (x: 5, t: 3)>
array([[ 0.343039, -0.521509,  0.02816 ],
       [ 0.589563, -1.030908, -0.023295],
       [-0.157136, -0.175684, -0.743779],
       [ 0.164875, -0.489531, -0.335251],
       [ 0.294526,  0.693384, -1.046416]])
Coordinates:
  * t        (t) float64 0.6667 1.333 2.0
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0

計算

np.ndarray 風の操作を数多くサポートしています。

ブロードキャストを含めた基本的な演算をサポートしています。

data+10
<xarray.DataArray 'some_measurement' (x: 5, t: 4)>
array([[ 11.089975,  10.343039,   9.478491,  10.02816 ],
       [ 11.117389,  10.589563,   8.969092,   9.976705],
       [ 10.403413,   9.842864,   9.824316,   9.256221],
       [ 10.814334,  10.164875,   9.510469,   9.664749],
       [ 10.009115,  10.294526,  10.693384,   8.953584]])
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0

要素ごとの計算では、これらの情報を引き継げます。

np.sin(data)
<xarray.DataArray 'some_measurement' (x: 5, t: 4)>
array([[ 0.886616,  0.336351, -0.498189,  0.028156],
       [ 0.89896 ,  0.555998, -0.857766, -0.023293],
       [ 0.39256 , -0.15649 , -0.174781, -0.677074],
       [ 0.727269,  0.164129, -0.470212, -0.329006],
       [ 0.009114,  0.290286,  0.639144, -0.865635]])
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0

xr.Dataset の使い方

xr.Dataset は、複数のxr.DataArrayをひとまとめにしたオブジェクトです。

特に、軸を共有するxr.DataArrayに対して、一気にインデクシングやスライシングを施すことができます。
1つの計測器が複数種類の信号を出力することもあると思いますが、
そのような多次元情報を扱うのに適したものとなっています。

これはpandasにおけるDataFrameに似た役割のものです。

インスタンス化

ひとつ目の引数はdata_varsdict-like です。
key には格納する data の名前、要素には 2要素のタプルを渡します。
タプルのひとつ目の要素は、そのデータに対応する軸ラベルを、ふたつ目の要素にはデータ(array-like)を渡します。

軸データを格納するにはcoordsdict-like を渡します。
キーには軸のラベル、要素には軸の値を渡します。

ds = xr.Dataset({'data1': (['x','t'], np.random.randn(5,4)), 'data2': (['x','t'], np.random.randn(5,4))}, 
                coords={'x': x_axis, 't': t_axis})
ds
<xarray.Dataset>
Dimensions:  (t: 4, x: 5)
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0
Data variables:
    data1    (x, t) float64 -1.091 -1.851 0.343 2.077 1.477 0.0009389 1.358 ...
    data2    (x, t) float64 0.4852 -0.5463 -0.22 -1.357 -1.416 -0.4929 ...

中身にアクセスするためには、ラベル名を[]内に渡します。
その場合、戻り値はxr.DataArrayになります。

ds['data1']
<xarray.DataArray 'data1' (x: 5, t: 4)>
array([[ -1.091230e+00,  -1.851416e+00,   3.429677e-01,   2.077113e+00],
       [  1.476765e+00,   9.389425e-04,   1.358136e+00,  -1.627471e+00],
       [ -2.007550e-01,   1.008126e-01,   7.177067e-01,   8.893402e-01],
       [ -1.813395e-01,  -3.407015e-01,  -9.673550e-01,   1.135727e+00],
       [  2.423873e-01,  -1.198268e+00,   1.650465e+00,  -1.923102e-01]])
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0

軸にもラベルでアクセスできます。

ds['x']
<xarray.DataArray 'x' (x: 5)>
array([ 0.  ,  0.25,  0.5 ,  0.75,  1.  ])
Coordinates:
  * x        (x) float64 0.0 0.25 0.5 0.75 1.0

xr.Dataset のインデクシング

指標アクセスにはiselを用います。

x 軸に沿った1番目の要素にアクセスするには、以下のように、軸ラベル名とそれに対応するインデックスを指定します。

ds.isel(x=1)
<xarray.Dataset>
Dimensions:  (t: 4)
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
    x        float64 0.25
Data variables:
    data1    (t) float64 1.477 0.0009389 1.358 -1.627
    data2    (t) float64 -1.416 -0.4929 0.4926 -0.7186

もちろん 複数の軸も指定できて

ds.isel(x=1, t=2)
<xarray.Dataset>
Dimensions:  ()
Coordinates:
    t        float64 1.333
    x        float64 0.25
Data variables:
    data1    float64 1.358
    data2    float64 0.4926

スライスにも対応しています。

ds.isel(x=slice(0,2,1), t=2)
<xarray.Dataset>
Dimensions:  (x: 2)
Coordinates:
    t        float64 1.333
  * x        (x) float64 0.0 0.25
Data variables:
    data1    (x) float64 0.343 1.358
    data2    (x) float64 -0.22 0.4926

位置インデクシング

位置インデクシングには.selメソッドを使います。
.iselと同じように、軸ラベル名と今度は軸の値を指定します。

ds.sel(x=0.0)
<xarray.Dataset>
Dimensions:  (t: 4)
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
    x        float64 0.0
Data variables:
    data1    (t) float64 -1.091 -1.851 0.343 2.077
    data2    (t) float64 0.4852 -0.5463 -0.22 -1.357

デフォルトでは厳密に値が一致したものが帰ってきますが、methodオプションで指定できます。
最寄値が欲しい場合はmethod='nearest'とします。

# x = 0.4 に最もxが近い値を返す。
ds.sel(x=0.4, method='nearest')
<xarray.Dataset>
Dimensions:  (t: 4)
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
    x        float64 0.5
Data variables:
    data1    (t) float64 -0.2008 0.1008 0.7177 0.8893
    data2    (t) float64 -0.03163 0.6942 0.8194 -2.93

スライスオブジェクトを渡すことも可能です。

ds.sel(x=slice(0,0.5))
<xarray.Dataset>
Dimensions:  (t: 4, x: 3)
Coordinates:
  * t        (t) float64 0.0 0.6667 1.333 2.0
  * x        (x) float64 0.0 0.25 0.5
Data variables:
    data1    (x, t) float64 -1.091 -1.851 0.343 2.077 1.477 0.0009389 1.358 ...
    data2    (x, t) float64 0.4852 -0.5463 -0.22 -1.357 -1.416 -0.4929 ...