前章では、ファイルの読み込みと基本的な描画を紹介した。 続いて本章では、長期時系列データセットを用いた基礎的な時系列解析・統計解析を実習する。
本演習では、主にpandasのメソッドを解析に用いることにする。
ただし、インデックスがタグ付けされていないデータ配列(ndarray)を用いた解析例(Numpyの関数を用いた手法)についても 参考までに併記している。なお、Numpy関数を用いる場合にでもpandasのオブジェクトを引数としてそのまま渡せる場合が多い。
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
import pandas as pd
import datetime
import scipy.stats as stats
from scipy.optimize import curve_fit
yst = 1980; yen = 2018
dlist = []
for yy in range(yst,yen+1,1):
cyr = str(yy) # 文字列化
file = "../../data/Kyoto/Kyoto_" + cyr +".csv" #<---(注)"data/AMeDAS/Kyoto/Kyoto_"に変更のこと
df = pd.read_csv(file,encoding = "shift-jis",header=2,index_col=0,parse_dates=[0])
df = df.iloc[2:,:] # ヘッダーの余計な部分を除く
dlist.append(df) # listに要素(df)を追加していく
# pandas.concatで結合する
dfm = pd.concat(dlist,axis=0) # axis=0 (時間軸方向)に結合
temp = dfm['平均気温(℃)']
Series、DataFrame型のデータについてdescribe()
メソッドを使うと、基本的な統計量を一覧にして示してくれる。
pandasでは個々の統計量を計算するメソッドも多種用意されている。下表に代表例を示す:
pandasメソッド | 説明 |
---|---|
要素の数 | count |
sum | 合計 |
mean | 平均 |
median | 中央値 |
quantile | 分位値(0から1の範囲) |
min, max | 最小値,最大値 |
idxmin, idxmax | 最小値、最大値が得られた要素のラベル(注1) |
var(ddof=) | 分散(ddof = 0: 標本分散, 1: 不偏分散,...; デフォルトは1) |
std(ddof=) | 標準偏差(ddof = 0, 1,..) |
cov(other) | other との共分散 |
(注) 複数のインデックス(日時)で同じ最小(大)値を示す場合には、"最も小さい"ラベル(日付で言えば"若い"ラベル)しか
結果を返さないので注意が必要である。これを確かめるには、data[data == data.min()]
などとして(「1.2.1 条件を指定したデータの抜き出し」を参照)、最小値を取るデータを抜き出して調べる必要がある。
(参考) [Numpy]
Numpy においても、一部については同様のメソッドが用意されている(
sum
,mean
,min
,max
,var
,std
など)。 その他に関しては、Numpyの関数を用いる:
Numpy関数 説明 np.median(array) 中央値 np.quantile(array,q) データのパーセント点を0から1の範囲(q)で求める なお統計量の計算にあたっては、pandasとは異なり欠損値は考慮されない。欠損値を考慮するには
nansum
、nanmean
などのメソッドを別途用いる。参考) ndarrayのメソッド
# 一括表示
print("describeの結果")
print(temp.describe())
# 各統計量を個別に計算
# 平均
mean = temp.mean()
print(mean)
.corr(other)
: 各行同士の相関係数を計算し、DataFrame形式で戻してくれる。Series形式の場合は引数に相手(other)が必要np.corrcoef(x,y)
: x, yの相関係数を計算。結果は、相関係数を要素に持つ行列として返される。相関係数の定義¶
\begin{align} R(x:y) = \frac{\sum (x_i - \bar x)(y_i - \bar y)}{ \sqrt{\sum (x_i-\bar x)^2} \sqrt{\sum (y_i - \bar y)^2} } \end{align}
precip = dfm['降水量の合計(mm)']
corr_tp = temp.corr(precip)
corr_tp_np = np.corrcoef(temp,precip)
print('R(pandas) ',corr_tp)
print('R(np)', corr_tp_np)
(ヒント)分位値メソッド
(Tips1)異なるヒストグラムを重ねる際には、ヒストグラム描画のオプションでalpha=0.5
(グラフの透過度)などとすると見やすい。
(Tips2)特定の月のデータの取り出しは2.3節も参照のこと
気象場の変動は、様々な時間スケールの変動が重なって生じている(2.5節「スペクトル解析」も参照)。 データ解析では、予め着目する時間スケールの変動を抽出して解析を行うことも多い。
例えば年々〜十年規模変動を見たい場合には年平均のデータがあれば十分だし、 逆に低気圧・高気圧の通過に伴うような数日周期の変動を見たい場合には長い時間スケールの変動を除いた方が見やすい。
以下では、(2.2.1) より低い頻度(時間間隔)のデータの作成、 および、(2.2.2)移動平均を用いた時間フィルターの作成、を演習する。
[pandas メソッド] .resample(freq,axis=,closed=)
:
freq
: 時系列の基準頻度を表す("A-DEC", "M", "3D"など)。下表参照。基準頻度の前に整数を書けば、倍数が作られる(e.g.,"3D": 3日)。axis=
: 対象とする軸。デフォルトはaxis=0
closed=
: ダウンサンプリングのとき、どちらの端のデータを含めるか。right
(left
):右端(左端)を含める。 文字 | 説明 |
---|---|
A-JAN, A-FEB,... | 一年に1度、指定した月の月末ごと |
M | 月末ごと |
D | 日ごと |
H | 毎時 |
T (or min) | 毎分 |
S | 毎秒 |
(参考)[Numpy]
pandasを用いずに同様の処理を行うには工夫が必要である。以下の例のように年平均のデータを作る場合には、 各年の日数分(閏年を考慮)を平均するという操作が必要になる。
いっそのこと、用いるデータが何であれ、pandasの型(1次元:Series, 2次元: DateFrame)にしてしまうのが 一番楽なのかも知れない。 次章では時間軸のみpandasのindex(DatetimeIndex)にして処理をする方法を紹介している。
使い方: popt, pcov = scipy.optimize.curvefit(f, xdata, ydata, ....)
:
関数 ydata = f(xdata)
をフィッティングする。関数f
は予め自分で定義する(下記の例を参照)
出力値: popt: フィッティングパラメター と pcov: 共分散
## (1) [pandas] resampleメソッド
t_annual = temp.resample('A-DEC').mean()
# 時刻を積算時間へ変換(後の説明参照)--------
x = t_annual.index # 時刻インデックスを取得
print(x)
x0 = x[0] # 基準時刻
dt_index = (x - x0).days # 日数に変換
dt = dt_index.values #日数の値を取得->x_iの配列
#--------------------------------------
# 関数を定義する
def f_trend(x, a, b):
return a*x + b
popt,pcov = curve_fit(f_trend,dt,t_annual)
# popt[0]:a(回帰直線の傾き)、popt[1]:b(y切片)
#fitting = popt[0]*dt + popt[1]
fitting = f_trend(dt,popt[0],popt[1])
# トレンド(10年あたりで表す)
trend10 = popt[0]*10*365
print('Trend = ',trend10,'K/decade')
# 描画
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot(111)
ax.plot(x,t_annual,label='Annual-mean T')
ax.plot(x,fitting,label='Regression')
ax.legend()
plt.show()
◆ 時刻型のインデックスから、積算時間への変換¶
予めx軸の座標($x_i$)を数値の配列(データの開始時刻からの積算時間)とする必要がある。 すなわち、これまではx軸の座標(インデックス)を時刻型(DateTime型;例,"2017-01-01")として扱ってきたがこれでは都合が悪い。 pandasでは便利なことに時刻の差分を計算できる(
Timedelta型
という)ので、これを用いて変換する。 具体的には、以下の手順を踏んでいる:
時刻(Datetime型)の差分を取る。これにより、Timedelta型のオブジェクト(dt_index)を作成。
dt_indexの属性である
.day(日数)
、.seconds(秒数)
などを取り出す。このままでは(pandasの)indexとして見なされているので、最後に
.values
メソッドを使って値の配列(dt)にする(これが$x_i$の配列)。(参考)TimedeltaIndex型の属性・メソッド一覧はこちら
◆ 信頼区間の推定(詳細はこちらを参照)¶
本演習では省略するが、実際には求めたトレンドの信頼区間(エラーバー)を推定する必要がある。その幅(誤差範囲)を考慮した上でもトレンドが正で あれば、「統計的に有意な上昇トレンドがある」と(自信を持って)言えることになる。
$$ \beta = \frac{\sum_{i=1}^N (x_i-\bar x)(y_i - \bar y)}{\sum_{i=1}^N (x_i - \bar x)^2} $$
ある条件を満たすデータのみを抽出し、その条件下での平均的な特徴を求めることをコンポジット(合成図)解析という(「重ね合わせる」というニュアンス)。気象データ解析においては、ある事象の平均的な構造を調べたり、事象の振る舞いが 外部条件によってどのように変わるかを調べる際によく使われる手法である。
簡単な例としては:
1)1月の平均気温を求める(条件:「月が1月であること」)
2)気圧が高い日と低い日で気温は有意に異なるか調べる(条件:「気圧がXX以上」「気圧がXX以下」でそれぞれ平均を取って比べる)
などが挙げられる。
ある条件・基準(「キー」と呼ぶ)にしたがってデータをグループ化し、それぞれのグループごとに関数(sum, meanなど)を適用した 結果を返すことができる。
キーとしては
軸のインデックス、あるいは、各インデックスのラベルに対して呼び出される関数
などを用いることができる。
ここでは、インデックス配列がdatetime
型(日付・時刻)であるので、その属性を利用して
「月」のインデックス配列を作成し、それをキーとして与えることでグループ化する(上の例の一番目)。
DatetimeIndex
の属性としては、年・月・日などがある(以下、一例):
属性 | 意味 |
---|---|
year | 年 |
month | 月 |
day | 日 |
hour | 時間 |
dayofweek | 曜日(0:月曜日...6:日曜日) |
dayofyear | 一年の何日目か |
(参考)DatetimeIndexの属性・メソッドの一覧はこちら
ここではmonth
属性を取得して、各インデックスの月を求める。
# 時間情報を元に条件を決めるのであるから、まずは時間の情報を抜き出す。
date = temp.index #ndex列が時間情報なので
#print(date)
# "月"の配列を作る
key = date.month
#print(key)
# これをキーとしてグループ化
group = temp.groupby(key)
# 各グループで平均を計算
temp_monthly = group.mean()
#以下、描画
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot(111)
temp_monthly.plot(ax=ax,label="Monthly mean")
ax.set_ylabel('Temperature ($^\circ$C)')
ax.set_xlabel('Month')
ax.legend()
plt.show()
#真偽値配列の作成
mask_mon = (temp.index.month == 1)
#1月データを抜き出して平均
temp_jan = temp[mask_mon].mean()
print(temp_jan)
各月において上記と同様の処理を繰り返せば良い。
mon_ave = np.zeros(12) #大きさ12(12か月分)の配列を用意しておく
for mm in range(1,13):
mon_ave[mm-1] = temp[temp.index.month == mm].mean()
# mm-1 としたのは配列の要素が0から始まるため
x = np.arange(1,13) #x軸(1-12の月を表す整数配列)
## ここから描画
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot(111)
ax.plot(x,mon_ave,label='Monthly Average')
ax.set_ylabel('Temperature ($^\circ$C)')
ax.set_xlabel('Month')
ax.legend()
plt.show()
$\ \ \ $ヒント:三角関数はnp.sin()
、np.cos()
で与えられる。また引数の単位はラジアンであることに注意。
$\ \ $参考:1時間毎のAMeDASデータの読み込みはこちら(の最初のセル)を参考にしてください
移動平均 (Running-mean)とは、一言で言えばデータを均(なら)す処理である。式で書けば、 \begin{equation} y_j = \frac{1}{2K+1}\sum_{k=-K}^{K} x_{j+k} \end{equation} ここで、$x_j$は元の時系列、$y_j$は移動平均した時系列。すなわち、各点$j$において、その周り$\pm K$点の平均を計算することを意味する。
長周期変動を抜き出すという意味で、移動平均は簡単なローパスフィルターとしても使われる(さらに、これを元の時系列から差し引けば短周期成分を抜き出すハイパスフィルターとなる)。但し、そのフィルター特性は必ずしも良くないので、実際の研究で用いる際には注意を要する。大まかに言って『$n$日移動平均は、ほぼ$2n$日以下の周期を落とすフィルターである』(伊藤・見延, 2010)。(より正確には、移動平均した時系列と元の時系列のパワー(振幅の二乗)を比べると、周期$2n$でパワーが半分になる(周期$2n$以下ではそれ以下))。
(補足)より特性の良いフィルターとしては、「Lanczos(ランチョス)フィルター」などが良く用いられる。
.rolling(wd, center=, win_type =,...)
: 移動する窓関数。窓の中にあるデータをまとめてしまう。.mean()
などと一緒に用いる。デフォルトではデータの端($<$wd/2, n-wd/2$<$)は欠損(NAN)となる。wd
: 窓の長さ(要素数;上の式での$2K+1$)center
: True なら各indexの前後合わせて wd 個の要素を使う(デフォルトはFalse)win_type
:窓の種類(デフォルトは等重み;他にhammingなど)\begin{align} (a*v)[n] = \sum_{m=-\infty}^{\infty} a[m] v[n-m] \end{align}
- (参考)[Numpy]
np.convolve(a,v, mode=)
: 畳み込み積分(定義は以下)。a
: 時系列データ、b
:重み関数。実際には、長い方の配列の要素1つ1つに対して短い方の配列の要素全てを掛け合わせたものを足し合わせるという操作を行っている。mode
:valid
(端っこは計算しない)、same
(長い方の配列と同じ)例) $v=[0.33,0.33,0.33]$の時(三点移動平均)のときは($0 \le n-m \le 2$のみ計算することに注意すると)、
\begin{align} (a*v)[0] &= a[0]*v[0] \\ (a*v)[1] &= a[1]*v[0] + a[0]*v[1] \\ (a*v)[2] &= a[2]*v[0] + a[1]*v[1] + a[0]*v[2] \\ (a*v)[3] &= a[3]*v[0] + a[2]*v[1] + a[1]*v[2] \\ \cdots \end{align}このうち、
mode=valid
では、$(a*v)[2]$からの値のみを返す(すなわち元の配列とは大きさが異なる)。 したがって図示する際には、x軸(時間)も最初と最後をトリミングしたものを用いる必要があるので注意。
a = [1,2,3,4,5]
v = [0.33,0.33,0.33]
print(np.convolve(a,v,mode="valid"))
# (1) [pandas] rolling を使った方法 -----
wd = 31
t_mv1 = temp.rolling(wd,center=True).mean()
x1 = t_mv1.index # t_mv1のインデックスをそのまま使う
#print(x1)
# ------------------------------
# (2) [Nump] convolveを使った方法 -----
temp_v = temp.values # 値を取り出す
#print(type(temp_v))
# 重み関数の定義
w = np.ones(wd)/wd
t_mv2 = np.convolve(temp_v,w,mode='valid')
# x軸(時間軸)をトリミング
wd2 = int((wd-1)/2)
x2 = temp.index[wd2:-wd2]
print(x2)
#----------------------------
# 図示
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot(111)
#
ax.plot(x1[:90], temp[:90],label='(0) Original')
ax.plot(x1[:90], t_mv1[:90],label='(1) 31-day moving average (rolling)')
#ax.plot(x2[:90], t_mv2[:90],label='(2) Convolution')
ax.legend()
plt.show()
これを元の時系列から差し引けば、ハイパスフィルターとなる。
### High-pass filter
# オリジナル時系列から低周波成分を差し引く
t_high = temp - t_mv1
#図示
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot(111)
ax.plot(t_high[:90],label='High-pass filtered')
ax.legend()
plt.show()