第3回 配列の取り扱い

目次

  • はじめに
  • NumPyの基本
    • 定数
    • 初等関数
    • 数を扱うその他の関数
    • 統計
  • NumPyの配列
    • 配列の生成
    • ベクトル計算(forループを使わない方法)
    • メソッド
  • 実例:KEOデータの解析
    • データ入出力
    • データの概観
    • マスク
    • 2次量の算出
    • 積分と条件付き積分
  • その他の便利な関数

はじめに

前回は描画ライブラリmatplotlibの力を借りて、時系列データの折れ線グラフを描きました。 描画のために1年間・日ごとの気温データなどをCSV形式から読み込み、配列として保存しましたが、その際に用いたのがNumPyモジュールのloadtxt関数でした。

NumPyは、上記のような多数の数値データを扱うのに長けた機能が集まったライブラリで、Pythonを用いた科学計算の中核をなすものです。中でも、ndarrayと呼ばれる配列の型(リストを機能拡張したものと思えばよいでしょう)は1次元的・多次元的に敷き詰められた数値データを高速に・簡易に扱う機能が詰まっています。 今回は、配列を中心としたNumPyの機能の基本を学びましょう。

慣習として、NumPyはnpという名前でインポートすることが多いです。後で使うので、前回と同様にmatplotlibもインポートしておきます。

In [1]:
import numpy as np
import matplotlib.pyplot as plt

NumPyの基本

定数

matplotlibと同じく、NumPyの中には様々な関数や定数が入っています。 例として、重要な定数や初等関数を見てみましょう。 おさらいとして、ライブラリの中の定数や関数にアクセスするときにはnp.○○という形で表記します。

数値計算の中で、無限大が出てきたり、付随して不定形が出てきたりする場合があります。そのような状況を表現するためのひろい意味での数も定義されています。

In [2]:
print(np.pi) # 円周率
print(np.e) # ネイピア数
print(np.inf) # 正の無限大
print(np.nan) # Not a number = 数ではない
3.141592653589793
2.718281828459045
inf
nan

初等関数

三角関数や指数関数、それらの逆関数などの初等関数ももちろん入っています。 非常によく使いますので、np.とセットで列記します。

  • 三角関数: np.sin, np.cos, np.tan
  • 指数・対数関数: np.exp (exponential=指数関数), np.log (自然対数), np.log10 (底10), np.log2 (底2)
  • 逆三角関数: np.arcsin, np.arccos, np.arctan, np.arctan2 (y/xではなくy,xを引数にとり、180度の不定性を除去する)
  • 双曲線関数・逆双曲線関数: np.sinh, np.cosh, np.tanh, np.arcsinh, np.arccosh, np.arctanh
  • 平方根: np.sqrt (square root=平方根)、ほかの冪関数はa**bまたはnp.power(a,b)で表記する
In [3]:
print(np.sin(np.pi/4)) # sin, cos, tanはラジアン表記
print(np.sqrt(1/2)) # ルート2分の1, sin(pi/4)と等しいはず
print(np.exp(2))
print(np.sqrt(-1)) # マイナスの数の平方根は、虚数ではなくnanになります
print(np.log(-1)) # マイナスの数の対数も、nan
print(np.exp(1j*np.pi)) # 複素数を引数にもできる。e^(i*pi)=?
0.7071067811865476
0.7071067811865476
7.38905609893065
nan
nan
(-1+1.2246467991473532e-16j)
C:\Users\yfuji\AppData\Local\Temp\ipykernel_18696\1972208716.py:4: RuntimeWarning: invalid value encountered in sqrt
  print(np.sqrt(-1)) # マイナスの数の平方根は、虚数ではなくnanになります
C:\Users\yfuji\AppData\Local\Temp\ipykernel_18696\1972208716.py:5: RuntimeWarning: invalid value encountered in log
  print(np.log(-1)) # マイナスの数の対数も、nan

数を扱うその他の関数

  • 絶対値: np.abs(実数・複素数どちらにも使える)
  • その数以下の最大整数: np.floor
  • その数より大きい最小整数: np.ceil
  • 複素数の実部・虚部・角度(ラジアン)・複素共役: np.real, np.imag, np.angle, np.conj
In [4]:
print(np.abs(4))
print(np.abs(-3))
print(np.floor(1.5), np.ceil(1.5))
print(np.floor(-1.5), np.ceil(-1.5))
4
3
1.0 2.0
-2.0 -1.0

統計

リストや(次に説明する)ndarrayなどの配列を対象にとって、合計・平均・分散・中央値などの統計量を計算する関数を紹介します。

In [5]:
# 統計の関数の例
import numpy as np
x = [6, 70, 39, 16, 56, 36, 19, 9, 24, 40, 22, 91, 80, 78, 84, 7, 29, 90, 86, 
     12, 93, 77, 32, 71, 95, 69, 80, 54, 14, 27, 74, 28, 21, 30, 92, 83, 96, 
     58, 51, 22, 22, 75, 81, 98, 57, 41, 13, 1, 48, 90]

print(np.sum(x))  # 合計
print(np.mean(x)) # 平均
print(np.var(x))  # 分散: 自由度Nだがオプション"ddof=1"で自由度N-1にもできる
print(np.std(x))  # 標準偏差: 自由度は分散と同様
print(np.median(x)) # 中央値
print(np.max(x))  # 最大値
print(np.min(x))  # 最小値
2587
51.74
916.2724
30.26999174099656
52.5
98
1

配列にNaNが混じっている場合、統計値はNaNを返します。NaNを除いた統計値を返す関数として、np.nansum, np.nanmean, np.nanmax, np.nanmin...などがあります。

NumPyの配列

Numpyの最大の長所が、配列の型であるndarrayオブジェクトです。 C言語やFortranの「配列」に相当する型で、Python固有のリストにも似ています。

ndarrayとリストの特徴を比べると、以下のようにまとめられるでしょう。

  • ndarray
    • 予め大きさ(要素数)を決めておく
    • 要素の型は統一(浮動小数点数、整数、複素数、論理型など)
    • まとめて計算する際のベクトル演算が非常に高速
    • 多次元データの操作性に長ける
  • リスト
    • 要素数を付け足したり減らしたり柔軟に変更できる
    • 異なる型の要素も含めることができる
    • まとめて計算する際にはforループなどを使用する必要があり、遅い
    • データは原則1次元構造

このように、時系列データやシミュレーション結果など、膨大な量の数値の羅列を扱う際には極めて強力な機能が実装されています。ここでは、配列の生成方法やベクトル演算・メソッドなどの配列操作の基本を解説します。

配列の生成

配列を作るには、以下のような方法があります。

1. リストなど、元々あるデータにnp.array関数を作用させる

In [6]:
datalist = [1,3,5,7,9]
print(type(datalist))
print(datalist)

dataarray = np.array(datalist)
print(type(dataarray))
print(dataarray)
<class 'list'>
[1, 3, 5, 7, 9]
<class 'numpy.ndarray'>
[1 3 5 7 9]

2. 外部データを読み込む

前回KEOの時系列データをnp.loadtxt関数で読み込んだように、CSVファイルなどをプログラム内に読み込みます。 後に、「データ入出力」で解説します。

3. 生成関数を用いる

NumPyには、ある決まった規則で配列を生成する関数があります。ここでは、よく用いるものを紹介します。 細かい使い方は、関数名?と入力するか、公式リファレンスを確認しましょう。

In [7]:
a = np.zeros(5) # 要素数5の、0ばかりの配列をつくる
print(a)
[0. 0. 0. 0. 0.]
In [8]:
b = np.ones(10) # 要素数10の、1ばかりの配列をつくる
print(b)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
In [9]:
c = np.arange(50) # rangeと同じく、start=0, end=50, step=1の等差数列
print(c)
d = np.arange(1, 50, 3) # start=1, end=50, step=3
print(d)
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
 48 49]
[ 1  4  7 10 13 16 19 22 25 28 31 34 37 40 43 46 49]
In [10]:
e = np.linspace(-2, 2, 11) # -2から2までを**両端点を含めて**等分した等差数列
print(e)
[-2.  -1.6 -1.2 -0.8 -0.4  0.   0.4  0.8  1.2  1.6  2. ]
In [11]:
# d = [1, 4, 7, ..., 49], 17要素
f = np.zeros_like(d) # dと同じ大きさの、0ばかりの配列
print(f)

g = np.ones_like(d) # dと同じ大きさの、1ばかりの配列
print(g)
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]

練習3-1

以下の配列を生成しなさい。生成関数を用いること。

  1. [0, 1, 2, 3, 4, 5]
  2. [0, 0, 0, 0, 0, 0]
  3. $[0, 0.2\pi, 0.4\pi, ..., 1.8\pi, 2\pi]$

要素の取り出し・スライス

ndarrayはリストと同様に、配列名[要素番号]配列名[start:end]配列名[start:end:interval]のような形で要素を取り出せます。

In [12]:
print(d)
print(d[2]) # 0から数え始めて2番目
print(d[-2]) # 後ろから2番目
print(d[5:8]) # 5から7番目
print(d[1::2]) # 1番目から最後まで、2個間隔で
[ 1  4  7 10 13 16 19 22 25 28 31 34 37 40 43 46 49]
7
46
[16 19 22]
[ 4 10 16 22 28 34 40 46]

ベクトル演算

forループ vs ベクトル演算

例えば100要素の配列同士の足し算や掛け算を行いたいとき、forループで1要素ずつ行う方法があります。

$c_i=a_i+b_i (i=0, 1, ..., 99)$のようなイメージです。

In [13]:
a = np.arange(100) # [0, 1, 2, ..., 99]
b = np.arange(100, 0, -1) # [100, 99, 98, ..., 1]

c = np.zeros(100) # 入れ物を先に用意する
for i in range(100):
    c[i] = a[i] + b[i] 
print(c)
[100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.
 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.
 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.
 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.
 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.
 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.
 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100. 100.
 100. 100.]

上と同じ計算を次のように一気に計算できます。 ベクトルの成分ごとの足し算$c_i=a_i+b_i (i=0,...,99)$に対して、ベクトル丸ごとの足し算$\textbf{c}=\textbf{a}+\textbf{b}$を行うイメージで、ベクトル演算と呼ばれます。

In [14]:
a = np.arange(100) # [0, 1, 2, ..., 99]
b = np.arange(100, 0, -1) # [100, 99, 98, ..., 1]

c = a + b # [100, 100, ..., 100]のndarray

Pythonのforループは処理が非常に遅いので、可能な限りベクトル演算を使うことが重要です。

In [15]:
a = np.arange(1000000) # 非常に大きい配列
b = np.roll(a, 500000) # aと同じ大きさの、別の配列
print(a)
print(b)
[     0      1      2 ... 999997 999998 999999]
[500000 500001 500002 ... 499997 499998 499999]
In [16]:
%%timeit
# 時間計測
c = np.zeros(1000000)
for i in range(1000000):
    c[i] = a[i] + b[i]
358 ms ± 12.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [17]:
%%timeit
d = a + b
1.22 ms ± 61.3 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

関数を作用させる

NumPy配列は配列丸ごとの四則演算が行えるほか、np.sinnp.expといった関数を作用させることも可能です。 実行したい計算をできるだけループを使わずにベクトル演算の形式で表記できるかが、Pythonプログラムの効率化に重要です。

In [18]:
# ベクトル演算の例
a = np.linspace(0., 1, 11) # 0 ~ 1..., 10等分した端点11個
b = a + 1 # 成分それぞれに1を足す

print(a)
print(b)

print(a * b) # 成分ごとに掛け算
print(2 ** b) # 第i成分について、2のb[i]乗

print(np.cos(2*np.pi*a)) # 成分ごとにコサインを計算する
print(np.exp(a)) # eのa[i]乗
print(np.log(b)) # log(b[i])
print(np.sqrt(a+b))
[0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]
[1.  1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2. ]
[0.   0.11 0.24 0.39 0.56 0.75 0.96 1.19 1.44 1.71 2.  ]
[2.         2.14354693 2.29739671 2.46228883 2.63901582 2.82842712
 3.03143313 3.24900959 3.48220225 3.73213197 4.        ]
[ 1.          0.80901699  0.30901699 -0.30901699 -0.80901699 -1.
 -0.80901699 -0.30901699  0.30901699  0.80901699  1.        ]
[1.         1.10517092 1.22140276 1.34985881 1.4918247  1.64872127
 1.8221188  2.01375271 2.22554093 2.45960311 2.71828183]
[0.         0.09531018 0.18232156 0.26236426 0.33647224 0.40546511
 0.47000363 0.53062825 0.58778666 0.64185389 0.69314718]
[1.         1.09544512 1.18321596 1.26491106 1.34164079 1.41421356
 1.4832397  1.54919334 1.61245155 1.67332005 1.73205081]

練習3-2

以下の配列を作りなさい。2.以降ではベクトル演算形式を用いること。

  1. -2, -1.9, ..., 1.9, 2の41要素をもつ配列(xの名前で定義すること)
  2. xの2乗
  3. 2のx
  4. $e^{-x^2}$

属性・メソッド

属性

ndarray型の変数には、格納している数値そのものだけでなく配列のサイズ、数値の型といった情報も含まれています。そういった変数に付属した情報を属性と呼びます。属性には、変数名.属性名でアクセスできます。

In [19]:
y = np.linspace(-10, 10, 101) # -10から10までの100等分
print(y.size) # 配列のサイズ
print(y.dtype) # 格納されている数値の型: 今はfloat64=8バイトの浮動小数点数
101
float64

メソッド

ndarray型の変数には属性に加えて、その配列を対象とする操作を行うための関数も含まれています。 そういった、変数に付属した関数をメソッドと呼びます。属性と似た形で、変数名.メソッド名(引数)でアクセスできます。

ndarrayには、合計・平均・分散・標準偏差・最大最小といった統計値を計算するメソッドがあるのが非常に便利です。 勿論np.meanなどの関数を呼んでもよいのですが、関数の対象を省略できるのでコードの短縮につながります。 np.median, np.nanmeanなどの関数に対応するメソッドはないので、その場合はおとなしく関数を呼びましょう。

In [20]:
print(x) # 上の「統計」で定義した、ランダムな長いリスト
x = np.array(x) # リストをndarrayに変換

print(x.mean()) # 平均
print(x.max()) # 最大値
print(x - x.mean()) # xからxの平均を引いた配列
[6, 70, 39, 16, 56, 36, 19, 9, 24, 40, 22, 91, 80, 78, 84, 7, 29, 90, 86, 12, 93, 77, 32, 71, 95, 69, 80, 54, 14, 27, 74, 28, 21, 30, 92, 83, 96, 58, 51, 22, 22, 75, 81, 98, 57, 41, 13, 1, 48, 90]
51.74
98
[-45.74  18.26 -12.74 -35.74   4.26 -15.74 -32.74 -42.74 -27.74 -11.74
 -29.74  39.26  28.26  26.26  32.26 -44.74 -22.74  38.26  34.26 -39.74
  41.26  25.26 -19.74  19.26  43.26  17.26  28.26   2.26 -37.74 -24.74
  22.26 -23.74 -30.74 -21.74  40.26  31.26  44.26   6.26  -0.74 -29.74
 -29.74  23.26  29.26  46.26   5.26 -10.74 -38.74 -50.74  -3.74  38.26]

実例:KEOデータの解析

配列の基本を学んだところで、実例形式でより多くの使い方を学んでいきましょう。

データ入出力

第2回でも実演した通り、NumPyにはファイルからデータを読み込むのに優れた関数がいろいろ入っています。

CSVファイルの読み込み

CSVファイルを読み込むならnp.loadtxt関数が使えます。前回と同じく、KEOの気温・水温・風速を読み込みましょう。

In [21]:
airt = np.loadtxt(r"C:\Users\yfuji\Desktop\lesson\data\data_KEO\KEOairt_2019daily.csv")
sst = np.loadtxt(r"C:\Users\yfuji\Desktop\lesson\data\data_KEO\KEOsst_2019daily.csv")
wind = np.loadtxt(r"C:\Users\yfuji\Desktop\lesson\data\data_KEO\KEOwind_2019daily.csv")

print(airt)
[15.1 15.8 13.  13.8 17.3 13.4 13.5 16.8 13.  15.8 15.  15.  14.1 12.8
 15.7 17.1 15.9 13.3 14.4 17.6 15.  13.9 15.6 13.7 13.6 12.2 10.7 14.4
 12.3 13.5 17.3 12.4 15.6 17.2 18.  15.9 17.7 17.9 15.2 16.1 12.8 15.9
 14.  15.3 12.4 15.6 16.4 13.3 14.3 17.3 18.5 14.8 15.5 13.9 14.2 17.2
 15.6 15.  18.6 15.6 14.9 16.3 18.2 14.6 17.  17.3 12.5 14.  17.2 18.7
 17.8 16.8 14.6 15.9 15.4 15.6 14.5 15.7 17.4 18.8 18.8 15.8 14.2 16.9
 16.9 16.6 16.8 16.2 17.2 17.7 14.8 12.8 12.  15.3 18.  18.4 18.7 16.3
 14.  15.5 15.8 15.  14.6 16.5 17.9 16.8 16.2 17.1 18.  17.2 18.2 19.
 18.3 18.7 19.7 19.8 17.9 15.8 16.8 17.7 20.5 18.7 17.8 17.6 18.6 18.5
 18.9 18.2 19.1 20.2 18.2 19.2 17.4 18.1 18.7 19.2 19.2 19.  19.1 20.
 20.4 20.8 20.4 20.3 20.7 20.9 21.4 21.7 21.2 20.9 21.4 20.8 20.3 21.1
 20.6 20.8 21.3 22.1 22.8 21.8 22.4 21.3 21.1 20.8 21.  22.7 23.2 22.4
 22.5 21.5 22.7 22.  23.8 24.2 24.  22.6 23.1 24.2 24.4 24.3 24.2 24.1
 24.2 24.6 25.  25.2 25.5 25.7 24.4 24.  24.1 23.8 24.9 25.1 25.8 26.
 26.  26.3 27.4 27.2 27.1 25.6 24.9 25.9 26.9 27.4 27.3 27.3 27.5 27.8
 27.9 28.1 28.  28.1 28.2 28.1 27.9 27.7 28.3 28.5 28.6 28.7 28.8 28.8
 28.8 28.7 28.6 28.5 28.4 28.6 28.7 28.4 29.1 29.3 29.3 29.2 27.8 27.6
 28.4 28.9 28.9 28.9 29.1 29.1 29.2 29.1 29.  29.  28.9 29.1 29.2 28.9
 28.7 29.  27.7 27.3 27.8 28.  28.7 28.5 28.4 27.2 25.9 27.2 28.1 28.1
 28.2 27.4 26.7 26.1 26.3 26.1 26.4 25.9 25.2 25.6 26.5 26.8 26.7 26.6
 26.9 25.9 27.  27.6 27.7 26.6 26.8 25.7 24.7 24.6 24.9 26.5 26.2 25.2
 26.7 24.2 23.5 25.8 25.5 23.6 23.6 25.5 24.2 24.  23.5 24.4 24.4 21.8
 20.6 21.1 23.6 22.6 21.2 20.8 23.2 22.3 22.2 23.4 22.2 21.7 20.9 22.8
 23.1 19.6 19.  20.5 21.1 22.9 23.1 20.8 22.6 20.  16.4 16.3 19.1 21.8
 19.8 19.  18.7 18.6 18.2 17.4 18.4 21.2 21.8 20.6 19.  19.5 17.3 17.3
 19.9 21.1 21.  19.8 18.9 20.  17.6 17.1 17.6 17.9 17.5 14.4 15.3 17.8
 19.2]

バイナリファイルの入出力

CSVファイルはテキストファイル(人間が読める、文字列の羅列)なので、数字一桁(0~9の10種類)を記録するのに1バイト必要です。 しかし、本来1バイト=8ビットの情報量では、2の8乗=256種類の数値が記録できるはずなので、テキストファイルは記録効率が悪いと言えるでしょう。

このように、(テキストとしては読めないものの)高効率なデータ保存形式をバイナリファイルと呼びます。大気・海洋の大規模データは記録容量の効率化のためバイナリファイルが用いられることがほとんどです。バイナリの中にも様々な保存形式があり、中でもNetCDF形式が良く使われますが、これは別の機会に説明することにします。

NumPyにはいろいろなバイナリ形式を読む関数が含まれているほか、NumPy独自の形式で配列をバイナリファイルとして出力・読み込みできる関数np.save, np.loadを持っています。これについて紹介します。

In [22]:
# 保存するファイルのパス文字列。拡張子".npy"を用いる
filepath = r"C:\Users\yfuji\Desktop\lesson\data\data_KEO\KEOairt_2019daily.npy" 

np.save(filepath, airt) # 配列airtをfilepathに保存
In [23]:
%ls C:\Users\yfuji\Desktop\lesson\data\data_KEO\KEOairt_2019daily.*
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は B23F-B8CB です

 C:\Users\yfuji\Desktop\lesson\data\data_KEO のディレクトリ

2022/10/24  09:32             2,190 KEOairt_2019daily.csv
2022/10/25  18:23             3,048 KEOairt_2019daily.npy
               2 個のファイル               5,238 バイト
               0 個のディレクトリ  169,215,111,168 バイトの空き領域

上ではJupyterlabの機能で、KEOairt_2019daily.○○というファイルのサイズを羅列しています。 2190, 3048というのがファイルサイズで、一見.npyのほうがファイルが大きくなっているように見えますが、CSVのほうは十の位~小数点以下1桁までの有効数字3桁で記録しているのに対し、.npyのデフォルトが8バイト実数のバイナリ形式なので有効数字約16桁でデータが記録されています。このように、データ量に対する容量の効率が飛躍的に向上しています。 実際の気温測定精度を考えると有効数字16桁で記録するのは無駄なので、4バイト実数(有効数字8桁)・2バイト実数(同4桁)あたりを使うのが普通です。

では、この.npyファイルを読み込んでみましょう。 保存前のairtと同じ内容が読み込めているか、論理演算をベクトル形式で行って確かめてみます。

In [24]:
airt_new = np.load(filepath) # filepathは上で定義したパス文字列
print(airt_new)
print(airt_new == airt)
[15.1 15.8 13.  13.8 17.3 13.4 13.5 16.8 13.  15.8 15.  15.  14.1 12.8
 15.7 17.1 15.9 13.3 14.4 17.6 15.  13.9 15.6 13.7 13.6 12.2 10.7 14.4
 12.3 13.5 17.3 12.4 15.6 17.2 18.  15.9 17.7 17.9 15.2 16.1 12.8 15.9
 14.  15.3 12.4 15.6 16.4 13.3 14.3 17.3 18.5 14.8 15.5 13.9 14.2 17.2
 15.6 15.  18.6 15.6 14.9 16.3 18.2 14.6 17.  17.3 12.5 14.  17.2 18.7
 17.8 16.8 14.6 15.9 15.4 15.6 14.5 15.7 17.4 18.8 18.8 15.8 14.2 16.9
 16.9 16.6 16.8 16.2 17.2 17.7 14.8 12.8 12.  15.3 18.  18.4 18.7 16.3
 14.  15.5 15.8 15.  14.6 16.5 17.9 16.8 16.2 17.1 18.  17.2 18.2 19.
 18.3 18.7 19.7 19.8 17.9 15.8 16.8 17.7 20.5 18.7 17.8 17.6 18.6 18.5
 18.9 18.2 19.1 20.2 18.2 19.2 17.4 18.1 18.7 19.2 19.2 19.  19.1 20.
 20.4 20.8 20.4 20.3 20.7 20.9 21.4 21.7 21.2 20.9 21.4 20.8 20.3 21.1
 20.6 20.8 21.3 22.1 22.8 21.8 22.4 21.3 21.1 20.8 21.  22.7 23.2 22.4
 22.5 21.5 22.7 22.  23.8 24.2 24.  22.6 23.1 24.2 24.4 24.3 24.2 24.1
 24.2 24.6 25.  25.2 25.5 25.7 24.4 24.  24.1 23.8 24.9 25.1 25.8 26.
 26.  26.3 27.4 27.2 27.1 25.6 24.9 25.9 26.9 27.4 27.3 27.3 27.5 27.8
 27.9 28.1 28.  28.1 28.2 28.1 27.9 27.7 28.3 28.5 28.6 28.7 28.8 28.8
 28.8 28.7 28.6 28.5 28.4 28.6 28.7 28.4 29.1 29.3 29.3 29.2 27.8 27.6
 28.4 28.9 28.9 28.9 29.1 29.1 29.2 29.1 29.  29.  28.9 29.1 29.2 28.9
 28.7 29.  27.7 27.3 27.8 28.  28.7 28.5 28.4 27.2 25.9 27.2 28.1 28.1
 28.2 27.4 26.7 26.1 26.3 26.1 26.4 25.9 25.2 25.6 26.5 26.8 26.7 26.6
 26.9 25.9 27.  27.6 27.7 26.6 26.8 25.7 24.7 24.6 24.9 26.5 26.2 25.2
 26.7 24.2 23.5 25.8 25.5 23.6 23.6 25.5 24.2 24.  23.5 24.4 24.4 21.8
 20.6 21.1 23.6 22.6 21.2 20.8 23.2 22.3 22.2 23.4 22.2 21.7 20.9 22.8
 23.1 19.6 19.  20.5 21.1 22.9 23.1 20.8 22.6 20.  16.4 16.3 19.1 21.8
 19.8 19.  18.7 18.6 18.2 17.4 18.4 21.2 21.8 20.6 19.  19.5 17.3 17.3
 19.9 21.1 21.  19.8 18.9 20.  17.6 17.1 17.6 17.9 17.5 14.4 15.3 17.8
 19.2]
[ True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True]

また、np.saveで生成される.npyファイルは一つの配列を保存しますが、np.savez関数を用いると複数の配列を.npzという拡張子の1つのファイルに保存することができます。自分で調べて、使ってみましょう。

データの概観

今回のように大きなデータでは、個々の数字を見ていてもその全体像がよくわかりません。ざっとその全体像をつかむには、統計量を見たりグラフを描くのが良いでしょう。matplotlibとndarrayのおさらいです。

In [25]:
time = np.arange(len(airt))

plt.plot(time, airt, label="airt")
plt.plot(time, sst, label="SST")
plt.legend()
Out[25]:
<matplotlib.legend.Legend at 0x20867d81c10>
In [26]:
plt.plot(time, wind, label="wind speed")
plt.legend()
Out[26]:
<matplotlib.legend.Legend at 0x20867cf84f0>
In [27]:
print("Air temp:", airt.mean(), airt.max(), airt.min()) # 平均、最大、最小
print("SST:", sst.mean(), sst.max(), sst.min()) # 平均、最大、最小
print("Wind speed:", wind.mean(), wind.max(), wind.min()) # 平均、最大、最小
Air temp: 21.16849315068493 29.3 10.7
SST: 22.72361643835616 30.0 17.98
Wind speed: 6.577534246575343 15.6 0.3

マスク

ここまでは1年間を通したデータを見てきましたが、夏季のみ・冬季のみや、風速が一定値以上の場合のみなど特定の条件下のデータを取り出して、その条件下での平均的描像を調べることを考えます。そうした解析手法をコンポジット(合成)解析と呼びます。

そういったときに便利なのがマスクです。 ndarrayは自身と同じ大きさで、論理型 (True/False) が入った配列を要素番号の代わりに用いることで、Trueの要素だけを抜き出すことができるのです。

In [28]:
# 例
a = np.array([1,2,3,4,5])
mask = np.array([True, False, False, True, True])

print(a)
print(mask)
print(a[mask]) # Trueの要素だけが抜き出されている!
[1 2 3 4 5]
[ True False False  True  True]
[1 4 5]

上の例ではマスクを手動で作りましたが、マスクはベクトル形式の論理演算で容易に作成できます。 ただし、「かつ」「または」「否定」はand or notではなく、以下のような書き方をする必要があります。

  • xかつynp.logical_and(x, y)x & yx * y
  • xまたはynp.logical_or(x, y)x | yx + y
  • xではない:np.logical_not(x)~x

また、要件を満たしてTrueになった個数を数えたいときにはmask.sum()のようにすれば、Trueを1, Falseを0として単純に足し算をしてくれます。

In [29]:
a = np.arange(30) # 0~29
mask2 = (a % 2 == 0) # 2で割った余りがゼロ = 偶数
mask3 = (a % 3 == 0) # 3の倍数
print(mask2) # 偶数マスクの例
print(a[mask2])
print(a[mask3])
print(a[mask2 & mask3]) # 偶数かつ3の倍数=6の倍数
print(a[mask2 | mask3]) # 偶数または3の倍数
print((mask2|mask3).sum()) # 偶数または3の倍数の個数
[ True False  True False  True False  True False  True False  True False
  True False  True False  True False  True False  True False  True False
  True False  True False  True False]
[ 0  2  4  6  8 10 12 14 16 18 20 22 24 26 28]
[ 0  3  6  9 12 15 18 21 24 27]
[ 0  6 12 18 24]
[ 0  2  3  4  6  8  9 10 12 14 15 16 18 20 21 22 24 26 27 28]
20

さて、KEOの時系列に戻りましょう。 大気・海洋でよく使う定義として、夏季を6,7,8月、冬季を12,1,2月とします。英語の月名の頭文字から、前者をJJA、後者をDJFと表記します。論文でも説明なしに使われるので、覚えておきましょう。

配列timeは1月1日を0として以下1日ずつカウントしていますが、6月1日は151日目、9月1日は243日目ですので、JJAのマスクは(time>=151) & (time<243)として作れます。 同様に、3月1日は59日目、12月1日は334日目ですので、DJFのマスクは(time<59) | (time>=334)となります。 ここではJJAの平均風速、DJFの平均風速を調べてみましょう。

In [30]:
mask_jja = (time >= 151) & (time < 243)
mask_djf = (time < 59) | (time >= 334)

print(wind[mask_jja].mean())
print(wind[mask_djf].mean())
5.360869565217392
7.483333333333333

練習3-3

以下の条件のマスクを定義しなさい。

  1. 風速が4m以下の日
  2. 気温が海面水温よりも高い日

それぞれについて、データ個数・平均風速・平均気温・平均海面水温を計算しなさい。

2次量の算出

ここまで気温・水温・風速だけを見てきましたが、これらから算出される量もベクトル形式で簡単に算出できます。

例として、顕熱フラックスを計算してみましょう。フラックスとは、単位時間あたりに、ある物理量が単位面積の面を通過する量を表し、ここでは特に1平方メートルの海面を1秒間に通過する熱(単位は$\left[\frac{{\rm J}}{{\rm m}^2\ {\rm s}}\right]=[{\rm W}/{\rm m}^2]$)のことを表します。

海面の顕熱フラックスは海面付近の大気・海洋境界層乱流によって決まるため厳密な計測は困難ですが、以下のような形で大まかに近似されます(海洋分野ではバルク法と呼びます)。空気の密度を$\rho_a\approx1.3\ [{\rm kg}/{\rm m}^3]$、空気の定圧比熱を$C_p\approx1004\ [{\rm J}/({\rm K}\ {\rm kg})]$、顕熱フラックスを$H\ [{\rm W}/{\rm m}^2]$と表し、大気から海洋への熱の移動を正とします。

$H=\rho_aC_pC_hU_{10}\Delta T,\ \Delta T=T_{\rm air}-{\rm SST}$

ここで$C_h$はバルク係数と呼ばれます。バルク係数は観測やモデルに基づき様々な値が提案されており、風速に依存して変化することが示唆されていますが、ここではざっくりと$C_h=1.2\times 10^{-3}$を使うことにしましょう。

$U_{10}$は高度10mでの水平風速です。KEOの風速計は高度4mに設置されているので、風速を10m高に補正してやらないといけません。ここでは非常におおざっぱに、風速が高度の1/7乗に比例する、という経験則をもちいて補正することにしましょう。

$\left(\frac{U_{10}}{U_{4}}\right) = \left(\frac{10}{4}\right)^{1/7},\ U_{10} = (10/4)^{1/7} \times U_4$

In [31]:
rhoa = 1.3 # 空気の密度
Cp = 1004 # 空気の定圧比熱
Ch = 1.2e-3 # バルク係数

u10 = (10/4)**(1/7) * wind

H = rhoa * Cp * Ch * u10 * (airt - sst) # ベクトル形式でフラックスを算出

plt.plot(time, H, label="sensible heat flux")
plt.legend()

print("all", H.mean())
print("summer", H[mask_jja].mean())
print("winter", H[mask_djf].mean())
all -19.74347538478495
summer 1.7852434261845833
winter -45.79098179886736

これらの解析から、以下のようなことがわかります。

  • 平均的に、熱フラックスは負 = 海から大気に熱が放出
  • 夏季平均ではほとんどゼロ、わずかに大気から海に熱が渡されている
  • 冬季は海から多量の熱が大気に放出されている

積分と条件付き積分

KEOでは平均的に海から大気へと熱が放出されていることがわかりました。 第$k$日目に$1{\rm m}^2$あたり1日に放出される熱量は$H_k\Delta t\ [{\rm J}/{\rm m}^2]$($\Delta t=1{\rm day}=86400{\rm s}$)なので、年間では$\sum_{k=0}^{364}H_k\Delta t\ [{\rm J}/{\rm m}^2]$の熱量が放出されることになります。 これは、$\int_0^{365} H dt$ (時間の単位は日)の近似ですね。

In [32]:
dt = 24 * 60 * 60 # 1日の秒数
print((H * dt).sum())
-622630239.7345784

このように、2019年にKEOでは$6.23\times10^8\ [{\rm J}/{\rm m^2}]$の熱が海から大気に放出されたことになります。

さて、熱の放出は風が強いほど多くなります。前回見たように、風は低気圧や台風の通過に伴い強弱を繰り返します。 そこで「年間の熱放出のうちの強風イベントの寄与」を解析してみましょう。具体的には、「風速$U_0$[m/s]以上の日による熱放出が、全熱放出の何%を占めるか?」ということを調べます。 数式で書くと、次の通りです。

$\left.{\int_{U\geq U_0}H dt}\right/{\int_{\rm all}H dt}$

いろんな風速について調べたいので、$U_0$を引数とする関数で書きましょう。マスクを使えば、条件付き積分も簡単にできます。

In [33]:
def heatflux_fraction(u0):
    dt = 24 * 60 * 60
    num = (H[wind >= u0] * dt).sum() # 分子 (numerator)
    denom = (H * dt).sum() # 分母 (denominator)
    return num / denom

print(heatflux_fraction(8))
print(heatflux_fraction(10))
print(heatflux_fraction(12))
0.5429120908658721
0.2585239399159132
0.10834023073704405

これによって、風速8m/s以上の日による熱放出が全体の54%, 10m/s以上による寄与が26%, 12m/s以上による寄与が11%であることがわかりました。ですが、それぞれの風速がどれほどの頻度で起こるかの情報がないと、まれな強風イベントがどれほど全体に重要なのかきちんとわかったことにはなりません。そこで、上と同様に「風速$U_0$[m/s]以上の日がどれほどの頻度で起こるのか」を計算する関数を作りましょう。

In [34]:
def wind_fraction(u0):
    return (wind >= u0).sum() / len(wind) # 365分の〇日

print(wind_fraction(8))
print(wind_fraction(10))
print(wind_fraction(12))
0.3232876712328767
0.11506849315068493
0.0410958904109589

12m/s以上を例にとると、上位4%の強風の日による熱放出が全体の11%の熱放出を占めた、ということになります。 実社会では世界の1%の富裕層が全資産の37%を保有する(ソース)そうですが、それに比べると海からの熱放出は極端イベントの相対的な重要性が低いですね。

横軸に風速の下位からの割合、縦軸にそれらの日に放出された熱の全体に対する割合を描くと、次のようになります。

In [35]:
u_array = np.linspace(0., 20., 100) # 調べる風速の列
windfrac_array = np.zeros_like(u_array)
heatfrac_array = np.zeros_like(u_array)

for n in range(len(u_array)):
    # u_arrayの風速に対応する割合を計算していく
    windfrac_array[n] = 1 - wind_fraction(u_array[n]) # 与えた風速より*弱い*日の割合
    heatfrac_array[n] = 1 - heatflux_fraction(u_array[n]) # 与えた風速より*弱い*日の熱放出の寄与

plt.figure()
plt.plot(windfrac_array, heatfrac_array)
plt.plot([0,1], [0,1], 'k--') # x=yの線
plt.grid() # 格子を描く
plt.show()

このような図をQ-Qプロットと呼びます。 例えば線が(0.8, 0.6)の点を通っていますが、これは上位20%の強風が全体の熱放出の40%を担っている、と見ることができます。

練習3-4

KEOデータを読み込んで、次の解析を行いなさい。

  1. 水温と気温の差(水温-気温)と、風速の相関係数を求める。
  2. 上記の相関係数を、JJAについて求める。
  3. 同じく、DJFについて求める。

その他の便利な関数

その他NumPyの便利な関数を、ざっと紹介します。人によっては必要ない機能も多いと思いますが、必要になった時に「こんな機能あったな」と覚えていてもらえたら十分です。

  • ベクトル内積 np.dot
  • 線形連立方程式の解 np.linalg.solve
  • 最小二乗法 np.linalg.lstsq
  • 行列の固有値分解 np.linalg.eig, np.linalg.eigh
  • 乱数生成 np.random.rand, np.random.randn, np.random.randint
  • 離散フーリエ変換 np.fft.fft, np.fft.ifft, np.fft.rfft, np.fft.irfft
  • ソート・検索 np.sort, np.argwhere, np.argmax, np.argmin
  • 線形内挿 np.interp