Rでデータ解析を始めよう021 Rで変化点検出をやってみよう

Rでデータ解析を始めよう021 Rで変化点検出をやってみよう

統計ソフトR言語を使ったデータ解析の練習です。

モモノキ&ナノネと一緒にR言語でchangepointパッケージを使った変化点検出をやってみよう




changepointパッケージを使った簡単なデータの変化点検出

ナノネ、今回はR言語で変化点検出をやってみよう。

データが変化する箇所を検出するの?

うん。changepointパッケージを使うとデータの変化点を簡単に検出できるから試してみよう。

モモノキ、データは何に使う?いつものirisは向いて無さそうかも?

そうだね...まずは、乱数を使った疑似データで変化点検出ができるか試してみよう。

In [1]:
set.seed(1)
# テストデータ1
data1 <- c(rnorm(100, mean=0, sd=1),rnorm(100, mean=5, sd=1))

plot(data1, type='l')
title('data1')

前半100個は平均0で標準偏差1(分散1)。後半100個が平均5で標準偏差1(分散1)のデータだね。

テストデータが準備できたので、実際にchangepointパッケージを使ってみよう。まずはパッケージのインポート(changepointは必要に応じてインストールしてね)。

In [2]:
# install.packages('changepoint')
library(changepoint)
Loading required package: zoo

Attaching package: 'zoo'

The following objects are masked from 'package:base':

    as.Date, as.Date.numeric

Successfully loaded changepoint package version 2.2.2
 NOTE: Predefined penalty values changed in version 2.2.  Previous penalty values with a postfix 1 i.e. SIC1 are now without i.e. SIC and previous penalties without a postfix i.e. SIC are now with a postfix 0 i.e. SIC0. See NEWS and help files for further details.

changekpointパッケージで変化点検出を実行にするには、主に3つの関数あるよ。

・cpt.mean() 平均だけの変化を検出
・cpt.var() 分散だけの変化を検出
・cpt.menavar() 平均と分散の変化を検出

用途が関数の名前通りだから、覚えられそう。

作成したテストデータ1は、平均が途中で変わっているだけだから、cpt.mean()が使えそうだね。もし、変化する点が1個だけと分かっている場合は、関数の引数にデータを渡すだけで変化点検出ができるよ。

In [3]:
cpt_mean_data1 <- cpt.mean(data1)
In [4]:
cpt_mean_data1
Class 'cpt' : Changepoint Object
       ~~   : S4 class containing 12 slots with names
              cpttype date version data.set method test.stat pen.type pen.value minseglen cpts ncpts.max param.est 

Created on  : Tue Dec 18 22:05:36 2018 

summary(.)  :
----------
Created Using changepoint version 2.2.2 
Changepoint type      : Change in mean 
Method of analysis    : AMOC 
Test Statistic  : Normal 
Type of penalty       : MBIC with value, 15.89495 
Minimum Segment Length : 1 
Maximum no. of cpts   : 1 
Changepoint Locations : 100 

なんかデータが出力されたけど...うまく検出できたの?

関数の実行で返ってきたオブジェクトをplot関数に渡すと、変化点付きのグラフを自動で描いてくれるよ。

In [5]:
plot(cpt_mean_data1)
title('cpt_mean(data1)')

うまく検出できてる!しかも簡単。

検出しやすい疑似データだからね。実際のデータだとオプションパラメータを少しいじるケースも多いかも。設定できるパラメータが沢山あるので、詳しくはドキュメントなどを調べてみてね。

In [6]:
# テストデータ1をcpt.var()で試しに実行
cpt_var_data1 <- cpt.var(data1)
plot(cpt_var_data1)
title('cpt_var(data1)')

同じデータで、cpt.var()を実行してみた。期待通りに変化点検出されずOK。

In [7]:
# テストデータ1をcpt.meanvar()で試しに実行
cpt_meanvar_data1 <- cpt.meanvar(data1)
plot(cpt_meanvar_data1)
title('cpt_meanvar(data1)')

cpt.meanvar()なら、平均の変化もうまく検出できてる。

分散が途中で変化するテストデータ2を作ったから、同じように確認してみて。

In [8]:
set.seed(1)
# テストデータ2
data2 <- c(rnorm(100, mean=0, sd=1),rnorm(100, mean=0, sd=5))

plot(data2, type='l')
title('data2')

前半100個は平均0で標準偏差1(分散1)。後半100個は平均0で標準偏差5(分散25)のデータだね。

In [9]:
par(mfrow=c(3, 1))

# cpt.mean()
cpt_mean_data2 <- cpt.mean(data2)
plot(cpt_mean_data2, cpt.col='blue')
title('cpt.mean(data2)')

# cpt.var()
cpt_var_data2 <- cpt.var(data2)
plot(cpt_var_data2, cpt.col='red', cpt.width=3)
title('cpt.var(data2)')

# cpt.meanvar()
cpt_meanvar_data2 <- cpt.meanvar(data2)
plot(cpt_meanvar_data2, cpt.col='green')
title('cpt.meanvar(data2)')

par(mfrow=c(1, 1))

テストデータ2は分散だけ変化させているから、cpt.var()を使うと変化点をうまく検出できるみたい。

検出された変化点の場所を数値で知りたいときは?

変化点が何番目のデータか知りたいときは、cpts()関数に検出結果オブジェクトを渡せばOK。また、同じようにparam.est()関数を使うと変化点で分けたときの平均や分散も確認できるよ。

In [10]:
# 変化点の確認(データn番目)
cpts(cpt_var_data2)
100
In [11]:
# 平均、分散の確認
param.est(cpt_var_data2)
$variance
  1. 0.798694468796771
  2. 22.7089247209009
$mean
-0.0400765079574995

変化点が複数ある場合はどうするの?必ずしも1点だけとは限らないよね。

変化点が複数ある場合も使う関数は同じでいいんだけど、オプション引数を少し変更する必要があるよ。

まず最初に、変化点が複数あるデータを適当に準備しておこう。

In [12]:
set.seed(1)
# テストデータ3
data3 <- c(
    rnorm(100, mean=0, sd=1),
    rnorm(100, mean=5, sd=2),
    rnorm(100, mean=9, sd=1),
    rnorm(100, mean=3, sd=1)
)

plot(data3, type='l')
title('data3')

複数の変化点を検出したい場合は、関数の引数methodに"PELT"を指定すると複数検出にも対応できるよ。(methodは計算方法の指定で、PELT以外にもいくつか選択できるんだけど、詳しい内容はドキュメントを調べてね)

In [13]:
cpt_meanvar_data3 <- cpt.meanvar(data3, method='PELT')

plot(cpt_meanvar_data3, cpt.width=3)
title("cpt.meanvar(data3, method='PELET')")

複数の変化点も検出できてる!分散だけ変更したデータも作って、変化点検出を試してみる。

In [14]:
set.seed(1)
# テストデータ4
data4 <- c(
    rnorm(100, mean=0, sd=1),
    rnorm(100, mean=0, sd=2),
    rnorm(100, mean=0, sd=7),
    rnorm(100, mean=0, sd=9)
)

par(mfrow=c(2, 1))

plot(data4, type='l')
title('data4')

cpt_var_data4 <- cpt.var(data4, method='PELT')
plot(cpt_var_data4, cpt.width=3)
title("cpt.var(data4, method='PELET')")

par(mfrow=c(1, 1))

あれ、変化点の検出箇所が1個足りない?分散の差が小さすぎると検出は無理みたい。見た目でも違いが分かりずらいし。

検出力にも限界があるよ。データによっては、パラメータをうまく設定することで対処できるケースもあるので、機会があったらいろいろ試してみてね。

次はデータセット使って、changepointを試してみよう。今回使うのは時系列データで有名なNile。

たしかナイル川の年別水位だっけ?

In [15]:
Nile
Time Series:
Start = 1871 
End = 1970 
Frequency = 1 
  [1] 1120 1160  963 1210 1160 1160  813 1230 1370 1140  995  935 1110  994 1020
 [16]  960 1180  799  958 1140 1100 1210 1150 1250 1260 1220 1030 1100  774  840
 [31]  874  694  940  833  701  916  692 1020 1050  969  831  726  456  824  702
 [46] 1120 1100  832  764  821  768  845  864  862  698  845  744  796 1040  759
 [61]  781  865  845  944  984  897  822 1010  771  676  649  846  812  742  801
 [76] 1040  860  874  848  890  744  749  838 1050  918  986  797  923  975  815
 [91] 1020  906  901 1170  912  746  919  718  714  740

Nileデータで変化点検出をやってみる。

In [16]:
cpt_nile <- cpt.meanvar(Nile, method='PELT')
plot(cpt_nile)
title('Nile')
In [17]:
# 変化点の確認(データn番目)
print(cpts(cpt_nile)) # インデックス
print((1871:1970)[cpts(cpt_nile)]) # 年
[1]  4  6 28
[1] 1874 1876 1898
In [18]:
# 平均、分散の確認
param.est(cpt_nile)
$mean
  1. 1113.25
  2. 1160
  3. 1089.27272727273
  4. 849.972222222222
$variance
  1. 8541.6875
  2. 0
  3. 20344.9256198347
  4. 15352.9158950617

meanvar(method='PELT')関数を実行したら、変化点を3つ自動検出したみたい。見た目だと1個でもよさそうなんだけどなぁ。

変化点が1つだけと決まっているなら、methodにamoc(または指定を省略)を指定してもできるけど...。

In [19]:
cpt_nile <- cpt.meanvar(Nile, method='AMOC')
# cpt_nile <- cpt.meanvar(Nile)
plot(cpt_nile)

参考までにパラメータのいじり方も少し練習しておこうか。

ラジャー。

変化点の数は、ペナルティの設定を変更すると変化するよ。具体的には引数penaltyにManualを指定して、引数pen.valueの値を変えることで変化点の数が変わるよ。pen.valueは小さな値にすると変化点が増えて、大きな値にすると変化点が減るよ。

In [20]:
par(mfrow=c(3, 2))

cpt_nile_pv1 <- cpt.meanvar(Nile, method='PELT', penalty='Manual', pen.value=1)
plot(cpt_nile_pv1, main='pen.value=1')

cpt_nile_pv5 <- cpt.meanvar(Nile, method='PELT', penalty='Manual', pen.value=5)
plot(cpt_nile_pv5, main='pen.value=5')

cpt_nile_pv10 <- cpt.meanvar(Nile, method='PELT', penalty='Manual', pen.value=10)
plot(cpt_nile_pv10, main='pen.value=10')

cpt_nile_pv20 <- cpt.meanvar(Nile, method='PELT', penalty='Manual', pen.value=20)
plot(cpt_nile_pv20, main='pen.value=20')

cpt_nile_pv50 <- cpt.meanvar(Nile, method='PELT', penalty='Manual', pen.value=50)
plot(cpt_nile_pv50, main='pen.value=50')

cpt_nile_pv100 <- cpt.meanvar(Nile, method='PELT', penalty='Manual', pen.value=100)
plot(cpt_nile_pv100, main='pen.value=100')

par(mfrow=c(1, 1))

なるほど。でも、よさげなpen.valの値はどうやって見つけるの?

完璧な方法は無さそうだけど、自動探索機能と探索したプロット結果を見れば、ある程度妥当なpen.val値を求めることができるみたい。

pen.valを自動探索するには、penaltyにCROPSを指定、pen.valはベクトルで検索範囲(上下限値)を指定すればOKだよ。

In [21]:
# 妥当な変化点数、ペナルティを求めるための自動探索
cpt_nile_search <- cpt.meanvar(Nile, method='PELT', penalty='CROPS', pen.value=c(10, 500))
[1] "Maximum number of runs of algorithm = 13"
[1] "Completed runs = 2"
[1] "Completed runs = 3"
[1] "Completed runs = 5"
[1] "Completed runs = 7"
[1] "Completed runs = 9"
In [22]:
# 検出された変化点
print(cpts.full(cpt_nile_search))
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11]
[1,]    4    6   28   45   47   52   54   76   80    82    97
[2,]    4    6   28   45   47   52   54   97   NA    NA    NA
[3,]    4    6   28   97   NA   NA   NA   NA   NA    NA    NA
[4,]    4    6   28   NA   NA   NA   NA   NA   NA    NA    NA
[5,]   28   NA   NA   NA   NA   NA   NA   NA   NA    NA    NA
[6,]   NA   NA   NA   NA   NA   NA   NA   NA   NA    NA    NA
In [23]:
# 対応するペナルティ(pen.valの値)
print(pen.value.full(cpt_nile_search))
[1] 10.00000 11.11861 11.72615 14.56093 34.93431 57.55588

自動探索の診断結果をプロットするには、plot()関数の引数にdiagnostic=TRUEを指定してね。

In [24]:
# 診断プロット
plot(cpt_nile_search, diagnostic=TRUE)

最適な変化の点の数は、ペナルティの変化量が小さくなるところ。曲線データの肘に相当するあたりがよさげらしいよ。

肘?がどこだか明確には分からないけど、なんとなく変化点数が4か3あたりかな?

探索結果の情報をもとにグラフを描くだけだったら、引数ncptsに変化点の数を指定してプロットすればOKだよ。ただし探索をしていない変化点の数値を指定するとエラーになっちゃうから注意ね(今回の場合は11,8,4,3,1から選択できる)。

In [25]:
par(mfrow=c(3, 2))

plot(cpt_nile_search, ncpts=11, main='ncpts=11')
plot(cpt_nile_search, ncpts=8, main='ncpts=8')
plot(cpt_nile_search, ncpts=4, main='ncpts=4')
plot(cpt_nile_search, ncpts=3, main='ncpts=3')
plot(cpt_nile_search, ncpts=1, main='ncpts=1')

plot(cpt_nile_search, diagnostic=TRUE)

par(mfrow=c(1, 1))

探索結果から求められたpen.valueの値をつかって、再計算を実行してもいいよ。たとえば、探索で見つかった変化点4個を再現したい場合はこんな感じで。

In [26]:
cpts.full(cpt_nile_search)[3,] # 探索結果の3番目が変化点を4個検出
  1. 4
  2. 6
  3. 28
  4. 97
  5. <NA>
  6. <NA>
  7. <NA>
  8. <NA>
  9. <NA>
  10. <NA>
  11. <NA>
In [27]:
pv <- pen.value.full(cpt_nile_search)[3] # 探索結果3番目のデータのpen.value値
pv
11.7261467961011
In [28]:
cpt_nile_cp4 <- cpt.meanvar(Nile, method='PELT', penalty='Manual', pen.value=pv)
plot(cpt_nile_cp4, main='pen.value=11.73')

試しに...

In [29]:
Sepal_Length <- iris[, 1] # iris Sepal Length
Sepal_Width <- iris[, 2] # iris Sepal Width
Petal_Length <- iris[, 3] # iris Petal Length
Petal_Width <- iris[, 4] # iris Petal Width

par(mfrow=c(2, 2))

cpt_Sepal_Length <- cpt.meanvar(Sepal_Length, method='PELT', penalty='Manual', pen.value=40)
plot(cpt_Sepal_Length, cpt.width=1, xlab='iris index', ylab='value')
title('Iris Sepal Length')

cpt_Sepal_Width <- cpt.meanvar(Sepal_Width, method='PELT', penalty='Manual', pen.value=50)
plot(cpt_Sepal_Width, cpt.width=1, xlab='iris index', ylab='value')
title('Iris Sepal Width')

cpt_Petal_Length <- cpt.var(Petal_Length, method='PELT')
plot(cpt_Petal_Length, cpt.width=1, xlab='iris index', ylab='value')
title('Iris Petal Length')

cpt_Petal_Width <- cpt.var(Petal_Width, method='PELT')
plot(cpt_Petal_Width, cpt.width=1, xlab='iris index', ylab='value')
title('Iris Petal Width')

par(mfrow=c(1, 1))

Irisデータでchangepointを使ってみた。3種類の花のデータが順番(setosa,versicolor,virginica)に並んでいるから、花の種類が変わるところで変化点を検出できるかもと...。

setosa(1-50)は特徴が大きく違うから検出も簡単。versicolor(51-100)とvirginica(101-150)はよく似ているから、特徴量Sepalでは変化点がうまく検出できなかったみたい。

無理くりIris?使い方としてはちょっとどうかなぁ...。まぁ練習なのでヨシとしよう。

今回のchangepointを使った変化点検出は以上で。またね!

お疲れ様でした。





スポンサーリンク

0 件のコメント :

コメントを投稿