Rで解析:時系列の変化点を検出する「scanr」パッケージ
長期間にわたって記録した時系列データでは、ある時点を境に平均や分布そのものが変わることがあり、その位置を正確に把握することが分析の出発点になります。しかし、変化した位置を目視で見極めたり、窓幅を変えながら検定を繰り返したりする作業には手間がかかります。
「scanr」パッケージは、ノンパラメトリックな検定を用いて単変量の時系列から変化点を検出するパッケージです。複数の窓幅による走査結果を投票でまとめる関数、単一の窓幅で走査する関数、CUSUM統計量やワッサースタイン統計量で変化位置を求める関数が収録されています。また、検出した変化点と真の変化点を突き合わせて精度指標を算出することや、時系列と変化点を重ねて可視化することも可能です。
パッケージバージョンは0.1.1。Windows 11 x64 (build 26200)のR version 4.6.1で確認しています。
<おすすめのRに関する書籍です>
パッケージのインストール
下記コマンドを実行してください。作図に用いるggplot2などは依存パッケージとして自動的に導入されます。
# パッケージのインストール
install.packages("scanr")
# パッケージの読み込み
library("scanr")コマンド例
詳細はコメント、パッケージのヘルプを確認してください。
変化点とは、時系列の途中で統計的な性質(平均・分散・分布の形)が切り替わる位置を指します。scanrは、時系列の各位置について、その前後の区間を比べる統計量を計算し、値が大きく跳ね上がる位置を変化点の候補とします。区間の比較には、平均のずれに反応するCUSUM統計量と、分布の形の違いに反応するワッサースタイン統計量が用意されています。比較に使う区間の長さ(窓幅)は結果に影響するため、複数の窓幅で走査し、候補を投票でまとめる関数も用意されています。
以降の例では、京都府和束町の茶園に設置した地温センサーが、120日間にわたって記録した日ごとの平均地温(摂氏)を題材にします。61日目に畝を覆う被覆資材を切り替えたため、そこを境に地温の水準がおよそ3度上がった、という状況を想定した擬似データを次のコードで作成します。
# 乱数の種を固定して結果を再現できるようにする
set.seed(2026)
# 被覆資材の切り替え前、60日分の日平均地温
before <- rnorm(60, mean = 15, sd = 1.2)
# 切り替え後、60日分の日平均地温
after <- rnorm(60, mean = 18, sd = 1.2)
# 前後をつないで120日分の時系列にする
soil_temp <- c(before, after)[スキャン窓サイズの自動選択]:default_window_sizesコマンド
系列の長さから、複数窓での走査に使う窓幅の並びを自動的に決めます。下限と上限のあいだから、指定した個数の窓幅を等間隔に選びます。scan_cpdにwindow_sizesを渡さない場合は、内部でこの関数と同じ規則が使われます。
| オプション | 意味 | 初期値 |
|---|---|---|
| n | 系列の観測数 | なし |
| min_window | 窓幅の並びの最小値 | 15L |
| max_window | 窓幅の並びの最大値。NULLのときは floor(n^(2/3)) を用いる | NULL |
| n_windows | 下限と上限のあいだから一様に選ぶ窓幅の個数。整数の候補数より多く要求した場合は候補すべてを返す | 5L |
| seed | 再現性のための非負整数のシード。NULLのときは実行時の乱数生成器の状態を用いる | NULL |
# 120点の時系列に対する既定の窓幅の並び
default_window_sizes(120L)
[1] 15 18 19 20 22
# 窓幅の下限・上限と個数、乱数の種を指定して求める
default_window_sizes(
n = 120L,
min_window = 12L,
max_window = 48L,
n_windows = 6L,
seed = 2024L
)
[1] 13 22 28 40 43 45[単変量時系列における変化点の検出]:scan_cpdコマンド
複数の窓幅で時系列を走査し、それぞれの結果を投票でまとめて変化点を返します。本パッケージの中心となる関数です。change_typeで、平均のずれ・分散のずれ・分布全体の違いのいずれに反応させるかを選びます。
| オプション | 意味 | 初期値 |
|---|---|---|
| x | 数値ベクター | なし |
| window_sizes | 走査に使う窓幅の正の整数ベクター(省略可) | NULL |
| alpha | 有意水準。0.05のような割合、または5のような百分率で指定 | 0.05 |
| n_boot | テーパー付きブロックブートストラップの反復回数 | 400L |
| vote_threshold | 変化点として残すために必要な、正規化した投票スコアの下限 | 0.5 |
| min_window | window_sizesがNULLのときの既定窓幅の最小値 | 15L |
| max_window | window_sizesがNULLのときの既定窓幅の最大値 | NULL |
| n_windows | window_sizesがNULLのときに等間隔で選ぶ既定窓幅の個数 | 5L |
| block_length | テーパー付きブロックブートストラップのブロック長(省略可) | NULL |
| taper | テーパーの形状。”tukey” または “none” | c(“tukey”, “none”) |
| tolerance | 窓をまたいで近い候補どうしを統合する際の距離 | NULL |
| random_state | 非負整数のシード(省略可) | NULL |
| n_jobs | Rust/Rayonのワーカースレッド数(省略可)。既定は1。-1で検出した全コアを使う | NULL |
| return_all | 窓ごとの診断情報と生の出力を保持するかどうか | TRUE |
| change_type | “distribution”、”mean”、”var” のいずれか | c(“distribution”, “mean”, “var”) |
| eps | ゼロ除算を避けるための小さな正の値 | 1e-12 |
| batch_size | Rustバックエンドが用いるブートストラップのバッチサイズ | 32L |
# 3種類の窓幅でアンサンブルの走査を実行する
fit <- scan_cpd(
soil_temp,
window_sizes = c(20L, 30L, 40L),
random_state = 2026L,
change_type = "mean"
)
# 検出結果の概要を表示する
fit
scanr change-point result
observations: 120
change points: 60
# 変化点の位置だけを取り出す
fit$change_points
[1] 60被覆資材を切り替えた61日目の直前、60番目の観測が変化点として検出されました。
<おすすめのRに関する書籍です>
[単一の窓幅に対する走査の実行]:scan_single_windowコマンド
窓幅を1つだけ指定して走査します。投票をはさまないため、その窓幅での検定結果がそのまま得られます。窓幅による結果の変わり方を確かめたいときに使います。
| オプション | 意味 | 初期値 |
|---|---|---|
| x | 数値ベクター | なし |
| window_size | 走査に使う正の整数の窓幅 | なし |
| alpha | 有意水準。0.05のような割合、または5のような百分率で指定 | 0.05 |
| n_boot | テーパー付きブロックブートストラップの反復回数 | 400L |
| block_length | テーパー付きブロックブートストラップのブロック長(省略可) | NULL |
| taper | テーパーの形状。”tukey” または “none” | c(“tukey”, “none”) |
| random_state | 非負整数のシード(省略可) | NULL |
| change_type | “distribution”、”mean”、”var” のいずれか | c(“distribution”, “mean”, “var”) |
| eps | ゼロ除算を避けるための小さな正の値 | 1e-12 |
| batch_size | Rustバックエンドが用いるブートストラップのバッチサイズ | 32L |
# 窓幅を30に固定して単一窓の走査を実行する
sw <- scan_single_window(
soil_temp,
window_size = 30L,
random_state = 2026L,
change_type = "mean"
)
# 検出結果を表示する
sw
scanr single-window result
window size: 30
change points: 60[CUSUM統計量による平均変化の特定]:ts_cusumコマンド
累積和(CUSUM)にもとづく統計量が最大になる位置を返します。平均が一段階ずれる形の変化に向いた、単一の走査です。引数は数値ベクターのみです。
| オプション | 意味 | 初期値 |
|---|---|---|
| x | 数値ベクター | なし |
# CUSUM統計量で平均が変化した位置を求める
ts_cusum(soil_temp)
[1] 60[ワッサースタイン統計量による分布変化の特定]:ts_wassersteinコマンド
前後の区間の分布間のワッサースタイン距離にもとづく統計量から、変化点の位置と各位置の統計量を返します。平均だけでなく、ばらつきや分布の形の違いにも反応します。返り値はリストで、change_pointに変化点の位置が入ります。
| オプション | 意味 | 初期値 |
|---|---|---|
| x | 数値ベクター | なし |
# ワッサースタイン統計量で分布が変化した位置を求める
tw <- ts_wasserstein(soil_temp)
# 変化点の位置を取り出す
tw$change_point
[1] 60
<おすすめのRに関する書籍です>
[1次元ワッサースタイン距離の計算]:one_wasserstein_distanceコマンド
2つの数値ベクターを標本とみなし、その経験分布のあいだの1次元ワッサースタイン距離(1-Wasserstein距離)を返します。ts_wassersteinが内部で用いる、区間どうしの距離そのものを取り出す関数です。
| オプション | 意味 | 初期値 |
|---|---|---|
| left | 数値ベクター | なし |
| right | 数値ベクター | なし |
# 切り替え前後それぞれ30日分の地温を取り出す
before30 <- soil_temp[1:30]
after30 <- soil_temp[91:120]
# 2つの期間の分布間の1次元ワッサースタイン距離を求める
one_wasserstein_distance(before30, after30)
[1] 2.812299[変化点検出の精度評価]:cpd_metricsコマンド
真の変化点と推定した変化点を突き合わせ、適合率・再現率・F1値と、区間の重なりにもとづくcovering指標をまとめて返します。toleranceで、何点までのずれを一致とみなすかを決めます。ここでは、別の手法が31番目と60番目を候補として返したと仮定し、scan_cpdの検出位置を真の変化点として評価します。
| オプション | 意味 | 初期値 |
|---|---|---|
| true_cps | 真の変化点の整数ベクター | なし |
| estimated_cps | 推定した変化点の整数ベクター | なし |
| n | 系列の観測数 | なし |
| tolerance | 一致とみなす際に許容する絶対距離の上限 | 10L |
# 別の手法が返した変化点の候補(誤検出を1つ含む)
candidate <- c(31L, 60L)
# scan_cpdの検出位置を真の変化点とみなして候補を評価する
cpd_metrics(
true_cps = fit$change_points,
estimated_cps = candidate,
n = 120L,
tolerance = 5L
)
$matches
true estimated distance
1 60 60 0
$precision
[1] 0.5
$recall
[1] 1
$f1
[1] 0.6666667
$covering
[1] 0.758333360番目は許容範囲内で一致したため再現率は1、いっぽう31番目は誤検出のため適合率は0.5となっています。
[時系列データと変化点の可視化]:vis_time_seriesコマンド
時系列を折れ線で描き、指定した変化点の位置に縦線を重ねたggplotオブジェクトを返します。true_change_pointsに真の変化点を渡すと、検出位置と並べて確認できます。x_label・y_labelで軸ラベルを指定できます。
| オプション | 意味 | 初期値 |
|---|---|---|
| x | 数値ベクター | なし |
| change_points | 検出した変化点(省略可) | NULL |
| true_change_points | 真の変化点(省略可) | NULL |
| index | xと同じ長さの横軸インデックス(省略可) | NULL |
| x_label | 横軸のラベル | “Time” |
| y_label | 縦軸のラベル | “Value” |
| title | 図のタイトル | NULL |
| … | 将来の描画オプション用に予約 | なし |
# 地温の推移に、検出した変化点を重ねて描画する
vis_time_series(
soil_temp,
change_points = fit$change_points,
x_label = "経過日数",
y_label = "日平均地温"
)
単一の窓幅で走査したときと複数の窓幅で走査したときとで結果を見比べたい場合は、それぞれのchange_pointsをこの関数に渡して並べて描くと、窓幅の選び方による違いを確認できます。
<おすすめのRに関する書籍です>
この記事が誰かの役に立ちますように。