餡子付゛録゛

ソフトウェア開発ツールの便利な使い方を紹介。

正規表現によるMarkdownの表の列の追加と削除

Markdown記法の文書中に表を入れることもあります。列名と文字寄せしか装飾が無い簡素な場合は、

|id|name|
|:---:|:---|
|001|noah|
|002|sophia|

と言うようにバーティカルバー(|)で区切りを入れ、:---(左寄せ):---:(中央寄せ)---:(右寄せ)を指定する行の一つ上の行が列名、下の行が列名以外の1行目以降の行となります。意図せず一行の表になったりするので*1罠な面もあるのですが、便利なときもある文法です。

編集していて厄介なのが、列の追加と削除です。CSVと相互変換できるツールもありますし、VSCodeであれば表を直接編集する事もできるのですが、一般のテキストエディターであれば正規表現で置換する方が手っ取り早いかもしれません。Perl互換正規表現が使えるとして、

操作 置換対象 置換文字
J個目の|の後に|を追加 ^((?:[^|\n]*\|){J}) $1|
J個目の|の後、|までを削除 ^((?:[^|\n]*\|){J})[^|\n]*\| $1

で複数行の置換を行えば、列の追加と削除になります。Jは任意の数字に変えて使ってください。列追加の場合は、文字寄せを追記しないと表として解釈されないかもしれません。

*1:行頭が | であるかで表なのか判別していて滅多に遭遇しませんが、利用者の意図を汲んでくれるわけではないです。なお、表ではないが行頭がバーティカルバーの場合は \| もしくは|と数値参照入力で書きます。

PlantUMLでER図を描こう

PlantUMLは総本山のドキュメントも充実していますし山のように紹介記事もあるので、生成AIもよく知っているツールで資料的に困る事はないのですが、PlantUMLでER図を描く紹介の準備をしたいと思います。

インストール

Javaアプリケーションなので、Javaのインストールが必要です。まだインストールしていない場合は、Java 11以降をインストールしましょう。安定版の最新で問題ないです。Ubuntu Linuxの場合は、

sudo apt install openjdk-25-jdk -y

でインストールできます。

PlantUMLはGPLv2版のPlantUMLをダウンロードしてきます。何かに組み込んで使う場合でない場合は、GPL版で困ることはないです。バージョンは、本稿執筆時は1.2026.6が最新でした。

cd ~
wget https://github.com/plantuml/plantuml/releases/download/v1.2026.6/plantuml-gplv2-1.2026.6.jar

続けて.bashrcにaliasを追記して、souceで読み込みなおします。

cp -pr .bashrc .bashrc.bak
grep -v plantuml .bashrc.bak > .bashrc
echo "alias plantuml='java -Xmx256m -jar ~/plantuml-gplv2-1.2026.6.jar -Playout=smetana'" >> .bashrc
source .bashrc

JVMの利用メモリー量を256MBに抑え、標準のGraphvizではなく内蔵レンダリングエンジンのsmetanaを使う指定をしています。Graphviz(dotコマンド)はこれで無くても大丈夫です。

エンティティーを記述したファイルを作成

ER図のエンティティーを記述したファイルを作成しましょう。エンティティーは説明のために使いまわしがあるので、別ファイルにしておくほうが便利です。

entities.puml

@startuml

' 主キーuser_idと、nameとユニークなemailを持つ「ユーザー」テーブルを定義し、UserというPlantUMLで使う名前をつける
entity "ユーザー" as User {
  + user_id : int
  --
  name : text
  *email : text
}

' Userの表示をカスタマイズして、マスターだとはっきりわかるようにする
User << (M, cyan) Master >>

' 主キーorder_idとorder_dateと外部キーのuser_idを持つ「注文」テーブルを定義し、Orderという名前をつける
entity "注文" as Order {
  + order_id : int
  --
  order_date : date
  # user_id : int <<FK(User,user_id)>>
}

' Orderの表示をカスタマイズして、トランザクションだとはっきりわかるようにする
Order << (T, lightgreen) Transaction >>

' removeの説明用エンティティ
entity "無用" as Useless {
  + trash_id : int
}

@enduml

@startumlと@endumlはUMLであることを示します。PlantUMLではクラス図をカスタマイズしてER図にしています。
シングルコーテーションはコメントをあらわします。/'と'/で挟むことでコメントをブロック指定できます。
--はエンティティー表示の区切りになります。==を使うと二重線になります。
要素の頭についている+は主キー、*はユニーク、#は外部キーをあらわすアイコンになります。

  1. user_id : intと、要素名 : 型名になっていますが、型名は自由に入ります。<>も<>と書いてもエラーになったりしません。

リレーションを記述したファイルを作成

エンティティーを記述したファイルをインクルードして、リレーションを記述します。

relations.puml

@startuml example_ER

/'
 entities.pumlを読み込む
 includeだと何回も読み込んで展開できる
'/
!include_once entities.puml
/' 不要なエンティティーが描画されないように削除 '/
remove Useless

/'
(E)や(C)といった目印文字を表示する
表示しない場合は hide circle
'/
show circle

/'
  UserとOrderの関係を1対0以上で右並びで表示し、リレーションの線の上に注釈を書く
'/
User ||--r--o{ Order : This is a relation.

@enduml

@startumlの後のexample_ERは図表の名前で、example_ER.pngのようなファイルがつくられることになります。省略時はファイル名が用いられますが、IDEによっては必須です。
||--r--o{の--は線種です。--は実線、..は破線になります。
エンティティーの並びは、線の間にd(上から下)r(左から右)u(下から上)l(右から左)のどれかをはさみます。省略可能です。

リレーションの種類

関係 記号
1 対 0か1 ||--o|
0か1 対 1 |o--||
1 対 1 ||--||
1 対 0以上 ||--o{
0以上 対 1 }o--||
1 対 1以上 ||--|{
1以上 対 1 }|--||
1以上 対 1以上 }|--|{

エンティティーを枠でくくる

エンティティーをpackageでまとめると、ひとまとめとして示すための枠をつけられます。

relations.pumlの!include_onceを以下のようにpackageでくくるような方法でも、使えます。

package "注文"  {
    !include_once entities.puml
    remove Useless
}

packageの代わりに、rectangleやframeという選択肢もあり、見栄えが変わります。

注釈をつける

エンティティーの周りに注釈を入れられます。

note left of User : 成人男性

packageの括弧の中に入れると枠線内に描画され、外に入れると枠線外に描画されます。

スタイルの設定

エンティティーやリレーションの色や背景色、フォントをまとめて指定する場合は、styleタグの中でスタイルを定義します(PlantUML Styles: Modern CSS-like Styling for Diagrams)。以前はskinparamで指定していたのですが、廃止になりました。

コンパイルする

以下でexample_ER.pngをつくってくれます。

plantuml relations.puml
PlantUMLで描いたER図

エイリアスを設定していなければ、

java -Xmx256m -jar ~/plantuml-gplv2-1.2026.6.jar -Playout=smetana' relations.puml

となります。

エンティティーを枠でくくって注釈をつけた場合は、以下のようになります。

エンティティーを枠でくくって注釈をつけた場合


SVGで出したい時は-tsvg、EPSで出したい場合は-tepsをオプションにつけます。

plantuml relations.puml -tsvg
plantuml relations.puml -teps

plantuml --helpで使い方が出てきますが、出力ファイル名を変えたい場合はパイプ出力を使いましょう。

plantuml -pipe < relations.puml > result.png

Dynamic Panel Data (Serial Correlation) Model

北村 (2005)で紹介されている*1のですが、あまり使われていない手法を紹介します。

\begin{align}
y_{it} &= \mu_i + X_{it} \beta + w_{it} \\
w_{it} &= \rho w_{i,t-1} + u_{it}
\end{align}

個体i、時点t、従属変数y_{it}、固定効果\mu_i、説明変数 X_{it}、係数\beta、誤差項 w_{it}として、 w_{it}が自己回帰しているモデルです。\rhoは自己回帰項の係数、u_{it}がi.i.d.の確率変数です。

これをParis-Winsten変換をかけて推定してみましょう。

DGP

データセットは生成します。DGPのβは {}^t\!(0, -1, 2)です。

DGP <- function(N = 10, T = 10){
    u <- matrix(rnorm(N*T, sd = 1), N, T)
    w <- u
    rho <- 0.5
    for(j in 2:T){
        w[, j] <- rho*w[, j - 1] + u[, j]
    }
    beta <- c(0, -1, 2)
    mu <- rep(seq(0, 100, length.out = N), T)
    t <- rep(1:T, each = N)
    i <- rep(1:N, T)
    X <- cbind(1, matrix(runif(N*T*(length(beta) - 1)), N*T, length(beta) - 1))
    y <- mu + X %*% beta + c(w)
    df <- data.frame(y, X[, -1], as.factor(i), as.factor(t))
    colnames(df) <- c("y", "x", "z", "i", "t")
    df
}

set.seed(1023)
df01 <- DGP(20, 10)
head(df01, 3)
           y         x         z i t
1 -0.5524994 0.3613191 0.2426655 1 1
2  7.0878804 0.9227929 0.5528946 2 1
3 10.6797476 0.7026300 0.5465261 3 1

推定

まず、LSDVで推定します*2。

r_lm01 <- lm(y ~ x + z + i + t, df01)
coef(summary(r_lm01))[1:3, ]
               Estimate Std. Error    t value     Pr(>|t|)
(Intercept) -0.07491713  0.4589730 -0.1632277 8.705341e-01
x           -0.39637808  0.2938260 -1.3490232 1.791342e-01
z            1.77550099  0.2677757  6.6305520 4.317182e-10

推定された係数は、DGPの係数と乖離が大きめです。

sum(tapply(w_hat, df01$i, \(x) sum(diff(x)^2)))/sum(w_hat^2)
[1] 1.08872

パネルデータ版のダービン=ワトソン比も1付近で、微妙ですが系列相関の可能性を示唆します。

 \rhoを推定します。

T <- length(levels(df01$t)) # 時点の数
N <- length(levels(df01$i)) # 個体の数
w_hat <- residuals(r_lm01) # 誤差項
lhs <- df01$t != 1 # t=1時点のデータ以外の行
rhs <- df01$t != T # t=T時点のデータ以外の行
# ρを推定
r_lm02 <- lm(w_hat[lhs] ~ w_hat[rhs] + 0)
(rho_hat <- coef(r_lm02)[1])
w_hat[rhs] 
 0.3557187 

DGPでは0.5なのでやや小さめですが、とりあえずは気にしない事にします。

Paris-Winsten変換をかけてみましょう。

# Paris-Winsten変換
y <- with(df01, y[lhs] - rho_hat*y[rhs])
x <- with(df01, x[lhs] - rho_hat*x[rhs])
z <- with(df01, z[lhs] - rho_hat*z[rhs])
# lmコマンドを使う都合で、ダミー変数(と切片項の1)はParis-Winsten変換せず、推定に使う時点の観測値をそのまま使う
i <- with(df01, i[lhs])
t <- with(df01, t[lhs])
# 差分モデルで推定
r_lm03 <- lm(y ~ x + z + i + t)
coef(summary(r_lm03))[1:3, ]
              Estimate Std. Error    t value     Pr(>|t|)
(Intercept)  0.2182374  0.3830573  0.5697253 5.697163e-01
x           -0.7630830  0.2382007 -3.2035302 1.658259e-03
z            1.9556396  0.2256916  8.6650984 6.714954e-15

切片項以外は大きく改善しました。固定効果モデルなので、切片項は個体効果ダミーと合算して評価すべきです。この変化は問題ありません。改善です。

固定効果の推定値と切片項の合計値を見ると、概ねDGPの値に近いです。0から100まで個体数で等分した値に近くなっています。

i_ptr <- 4:(2 + N)
coef(r_lm03)[1] + c(0, coef(r_lm03)[i_ptr]/(1 - rho_hat)) # Paris-Winsten(事後?)変換
                   i2         i3         i4         i5         i6         i7 
 0.2182374  5.7498597 10.7469271 15.5186148 21.4453556 26.5662748 31.7242678 
        i8         i9        i10        i11        i12        i13        i14 
37.1064273 42.5975526 45.7352265 53.0230941 57.9536065 64.6846864 68.1316753 
       i15        i16        i17        i18        i19        i20 
74.4829777 79.4217145 85.6120205 89.1234461 94.7430600 99.4748170 

ウェイト付き最小二乗法になるので、因子型を自動展開して入れたダミー変数以外の係数の標準誤差はそのまま使えます。ダミー変数の係数をParis-Winsten(事後?)変換する場合は、標準誤差もスケールしてください。

Baltagi and Li (1997)によるより良い \hat{\rho}

 \rhoの推定ですが、Baltagi and Li (1997)で以下の小標本のシミュレーションでより効率のよい手法が提案されています。

Q <- function(s, r_lm){
    df <- r_lm01$model
    N <- length(levels(df$i))
    T <- length(levels(df$t))
    u <- residuals(r_lm)
    r <- 0
    for(t in (s + 1):T){
        t0 <- t - s
        r <- r + sum(u[df$t == t] * u[df$t == t0])
    }
    r / (N*(T - s))
}
rho_hat_by_Q <- function(r_lm) (Q(1, r_lm) - Q(2, r_lm)) / (Q(0, r_lm) - Q(1, r_lm))
(rho_hat <- rho_hat_by_Q(r_lm01))
[1] 0.5679262

DGPの係数は0.5なので、確かにこちらの方がパフォーマンスが良いです。これを使った推定をすると、

              Estimate Std. Error    t value     Pr(>|t|)
(Intercept)  0.2140068  0.3794652  0.5639695 5.736171e-01
x           -0.7967706  0.2212629 -3.6010139 4.302451e-04
z            1.9605123  0.2098229  9.3436523 1.214826e-16

となります。こちらも微妙に改善されました。

*1:Baltagi (2005) "Econometric Analysis of Panel Data" のpp.84–86にもあります

*2:誤差項が出ればwith推定でも大丈夫です。

カルマンフィルターを使って時系列トレンドを時系列トレンドで回帰する例

一昨日ぐらいにカルマンフィルターを使った時系列データのトレンド変化の要因分析をかけたみたわけですが、ある系列のトレンド(状態変数)を、観察可能な外生変数ではなく、別の系列のトレンド(状態変数)で回帰するケースを考えてみたいと思います。

データ生成

2系統の状態変数ベクトルx_1とx_2にx_{1,t} = x_{1,t-1} + \beta x_{2,t-1} + \epsilon_{1, t}と言う関係があると設定して、データを生成します。 tは時点、 \epsilon_{1, t}は誤差項です。
状態変数ベクトルxは観察できず、それにノイズが乗った観測値 yのみが分析に使えます。

#
# 状態変数₂と状態変数₁に影響するDGP
#
set.seed(505)
n <- 100 # サンプルサイズはそれなり必要

Gt <- matrix(c(5, 0, 0, 0.5), 2, 2) # 誤差項の分散を決める係数

y <- x <- matrix(NA, 2, n)
x[1, 1] <- 0
x[2, ] <- 1 + sin(2*pi/n*(1:n))
y[, 1] <- 0
beta <- 0.75
for(t in 2:n){
    x[1, t] <- x[1, t - 1] + beta*x[2, t - 1] + rnorm(1)
    y[, t] <-  x[, t] + Gt %*% rnorm(2)
}

y <- t(y) # KFASパッケージにあわせて転置

2系列同時カルマンフィルター

本題に入る前の準備として、2系列を同時に同じ行列で処理してローカル線形トレンドを取り出せることを確認します。

\begin{align}
y_t &= Z_t \alpha_t + \epsilon_t \quad \mbox{(観測方程式)} \\
\alpha_{t+1} &= T_t \alpha_t + \eta_t \quad \mbox{(遷移方程式)} \\
Z_t &= \begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 
\end{pmatrix} \\
T_t &= \begin{pmatrix} 
1 & 1 & 0 & 0 \\
0 & 1 & 0  & 0 \\
0 & 0 & 1 & 1 \\
0 & 0 & 0 & 1
\end{pmatrix} \\
\alpha_t &= \begin{pmatrix}
\alpha_{t1} \\
\alpha_{t2} \\
\alpha_{t3} \\
\alpha_{t4}
\end{pmatrix}
\end{align}

#
# 2系列同時カルマンフィルター
#
library(KFAS)

Zt <- matrix(c(1, 0, 0, 0, 
0, 0, 1, 0), 2, 4, byrow = TRUE)

Tt <- matrix(c(1, 1, 0, 0,
0, 1, 0, 0,
0, 0, 1, 1,
0, 0, 0, 1), 4, 4, byrow = TRUE)

Ht <- diag(c(NA, NA))

# あとで行うTtの複雑化でヘッシアンが特異値になりやすくなるので、[1, 1]と[3, 3]を0にしてモデルを簡素化する
Qt <- diag(c(0, NA, 0, NA))

model_IND <- SSModel(y ~ -1 + SSMcustom(
        Z = Zt, T = Tt, R = diag(4), Q = Qt,
        a1 = matrix(c(0, 0, 0, 0), 4, 1), # 初期値
        P1 = diag(4) * 10 # 分散の初期値; 100ぐらいの方が良かったかも; もしくはP1inf = diag(4)にする
        ),
    H = Ht # 観測方程式の誤差項の分散は推定するパラメーター
)

m <- sum(is.na(Qt)) + sum(is.na(Ht)) # 推定するパラメーターの数
fit_IND <- fitSSM(model_IND, inits = rep(1, m), method = "L-BFGS-B")
r_IND <- KFS(fit_IND$model, filtering = c("state", "signal"), smoothing = c("state", "signal"))

plot_order <- c(3, 1, 5, 4, 2, 6)
plotted_series <- cbind(y, t(x), r_IND$att[, c(1, 3)])[, plot_order]
plot_cnames <- c("観測値y₁", "観測値y₂", "観察不能な状態変数x₁", "観察不能な状態変数x₂", "推定されたαₜ₁", "推定されたαₜ₃")[plot_order]
colnames(plotted_series) <- plot_cnames
plot(plotted_series, main = "2系列に同時にカルマンフィルターによるローカル線形トレンド抽出")

プロットの添付は省略します。

時系列トレンドを時系列トレンドで回帰

さて本題です。前節の基本的な処理をちょっといじります。遷移行列 Tの成分に \betaを入れます。

\begin{align}
y_t &= Z_t \alpha_t + \epsilon_t \quad \mbox{(観測方程式)} \\
\alpha_{t+1} &= T_t \alpha_t + \eta_t \quad \mbox{(遷移方程式)} \\
Z_t &= \begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 
\end{pmatrix} \\
T_t &= \begin{pmatrix} 
1 & 1 & \beta & 0 \\
0 & 1 & 0  & 0 \\
0 & 0 & 1 & 1 \\
0 & 0 & 0 & 1
\end{pmatrix} \\
\alpha_t &= \begin{pmatrix}
\alpha_{t1} \\
\alpha_{t2} \\
\alpha_{t3} \\
\alpha_{t4}
\end{pmatrix}
\end{align}

カルマンフィルターによって \betaは逐次推定できませんが、KFASパッケージは最尤法カルマンフィルターなので、やや変則処理になりますが推定できます。

#
# 状態変数₂と状態変数₁に影響するモデル
#
Tt_CAU <- Tt
Tt_CAU[1, 3] <- NA # 推定パラメータが1つ増える

model_CAU <- SSModel(y ~ -1 + SSMcustom(
        Z = Zt, T = Tt_CAU, R = diag(4), Q = Qt,
        a1 = matrix(c(0, 0, 0, 0), 4, 1), # 初期値
        P1 = diag(4) * 10 # 分散の初期値; 100ぐらいの方が良かったかも; もしくはP1inf = diag(4)にする
        ),
    H = Ht # 観測方程式の誤差項の分散は推定するパラメーター
)

# パラメーターを行列に反映する関数をユーザー定義のものにする
m <- sum(is.na(Qt)) + sum(is.na(Ht)) + sum(is.na(Tt_CAU)) # 最後のパラメーターが行列T内のbetaになる
fit_CAU <- fitSSM(model_CAU, inits = rep(1, m), method = "L-BFGS-B", updatefn = function(pars, model, ...){
    model$Q[is.na(model$Q)] <- exp(pars[1:(m-3)]) # 分散共分散行列は非負なので
    model$H[is.na(model$H)] <- exp(pars[(m-2):(m-1)]) # 分散共分散行列は非負なので
    model$T[is.na(model$T)] <- pars[m]
    return(model)
}, 
# 内部で呼ぶoptimのパラメーター; lmm = 10は精度向上、factr = 1e1は初期値への依存を軽減している
# Ttの要素を最適化するので、trace = 3で表示されるnorm of the final projected gradientが十分小さいか確認した
hessian = TRUE, control = list(trace = 3, maxit = 200, lmm = 10, factr = 1e1)) 
# 最尤法の推定結果
beta <- fit_CAU$optim.out$par # 7番目が行列T内のbetaになる
vcov <- -solve(fit_CAU$optim.out$hessian)
se <- sqrt(abs(diag(vcov)))
a <- 0.05
# (1-a)%信頼区間
conf <- qnorm(c(a/2, 1 - a/2), mean = beta[m], sd = se[m])
print(sprintf("β̂ : %f (%.0f%% CI %f %f)", beta[m], 100*(1-a), conf[1], conf[2]))
# 観測値と状態変数それぞれの1期先予測とフィルタリング分布を計算
r_CAU <- KFS(fit_CAU$model, filtering = c("state", "signal"), smoothing = c("state", "signal"))
plot_order <- c(3, 1, 5, 4, 2, 6)
plotted_series <- cbind(y, t(x), r_CAU$att[, c(1, 3)])[, plot_order]
colnames(plotted_series) <- plot_cnames
plot(plotted_series, main = "状態変数と状態変数の関係を推定した場合のトレンド")

状態変数と状態変数の関係を推定した場合のトレンド

トレンド自体は上手く抽出できていると言えるでしょう。観測不能な状態変数 x_1の推定量が \alpha_1に、 x_2の推定量が \alpha_3になりますが、だいたい似ています。

β̂ : 0.910916 (95% CI 0.433817 1.388015)

\betaの推定量がそれらしくなるには、サンプルサイズがもう10倍ぐらいは必要そうです。収束自体はしているのですが、ヘッシアンの値が何やらおかしいことになっている*1ので、もやもや感をもって利用してください('-' )\(--;)BAKI

疑似相関のケース

疑似相関のケースで \betaの推定量がどうなるか確認しておきましょう。

DGPを以下のように書き換えて、

set.seed(505)
n <- 100 # サンプルサイズはそれなり必要

Gt <- matrix(c(5, 0, 0, 0.5), 2, 2)

y <- x <- matrix(NA, 2, n)
x[1, 1] <- 0
x[2, ] <- 1 + sqrt(1:n) # 単調増加
y[, 1] <- 0
beta <- 0.75
for(t in 2:n){
    x[1, t] <- 1 + x[1, t - 1] + rnorm(1) # 切片項で増加傾向、x[2, t-1]の係数は0
    y[, t] <-  x[, t] + Gt %*% rnorm(2)
}

y <- t(y) # KFASパッケージにあわせて転置

(2系列同時カルマンフィルターの節のコードの変数宣言をしたあと)時系列トレンドを時系列トレンドで回帰するコードを走らせてみます。

状態変数と状態変数の関係を推定した場合のトレンド(疑似相関)

 x_1と x_2に関係があるように誤認してしまいそうですが、

β̂ : -0.030803 (95% CI -0.117191 0.055585)

と、疑似相関に騙されることなく、ほぼゼロの係数を推定できました。

まとめ

先に二つ試しましたが、これが一番、お題の解に近い気がしています。

実用上は、

  • カルマンフィルターと言う良く知られた既存手法の応用
  • 人気のRのパッケージKFASで計算できる
  • 「トレンドをトレンドで回帰しました」と口頭で説明しやすい
  • プロットが出るので可視化されている
  • プロットに対応した推定量が出る
  • 疑似相関にそれなり*2頑強

と言う利点があります。昔流行ったデータ可視化マンになって使ってみてください*3。

*1:KFSパッケージが、対数尤度関数にマイナスを乗じた関数を最小化しているため、符号が逆になっているようです。

*2:100回シミュレーションをしたところ偽陽性は9回とp値の2倍弱になったので、素のOLSよりはマシですが、当てにならないです。

*3:観測値が誤差に対して大きくなると係数βの標準誤差が大きくなる非直感的な状況もあるので、推定結果自体は自己回帰モデルなどで裏をとっておきましょう。

自己回帰モデルにおけるトレンドの要因分解

以下のお題の続きです。

tjo.hatenablog.com

カルマンフィルターでトレンドの要因分析をする方法は説明できたと思うので、自己回帰モデルにおけるトレンドの要因分解を行う方法を検討してみます。

自己回帰モデルにおけるトレンド

以下のような自己回帰モデルを考えます。

 y_{t} = c + \tau t + \phi y_{t-1} + \beta x_{t} + \epsilon_{t}

 yは内生変数、 tが時点、 cが切片項、\tauがトレンドの係数、\phiはyにかかる係数、 xは外生変数、\betaは xにかかる係数、\epsilon_tは誤差項です。

口語的にはc + \tau tを指していることが多い気がするトレンドですが、ベクトル自己回帰モデルの文脈で厳密にトレンド項と言うと\tau tのことで、 cは定数項と表現されます。カルマンフィルターや季節調整の場合は、季節とノイズの影響以外がトレンドです。

切片項で間に合わない効果があるデータなのかと言う疑念はややあるのですが、あるものとして考察を進めます。

教科書的な推定

雑な説明をするので信じないで教科書を要確認ですが、 yと xにそれぞれ定数項やトレンドがあると見せかけの相関が出てくるので、定数項とトレンド項を消します。一階の階差をとれば、定数項は完全に、トレンド項はだいたい消えます。

 y_{t} - y_{t-1} = \tau + \phi (y_{t-1} - y_{t-2}) + \beta (x_{t} - x_{t-1}) + \epsilon_{t} - \epsilon_{t-1}

この操作によって、トレンド項の係数 \tauは切片項として推定されます。

切片項 cの要因分解

もとの式の切片項 cは、 xなどの説明変数で予測不能な部分の平均です。説明変数を追加していけば、切片項による毎期一定の増減の傾向を分解していけることになります。 yに安定的な増加傾向が見られてその理由を説明したい場合は、外生変数をモデルに入れればよいです。つまり、教科書モデルで済みます。

トレンド項の要因分解

\tauがx_{t}に依存しているデータ生成プロセス(DGP)だとして、\tauとx_{t}の関係を推定できるのか考えます。\tau_t = \gamma_0 + \gamma_1 x_{t}と線形関係を仮定して、

 \begin{cases}
y_{t} &= c + \tau_t t + \phi y_{t-1} + \beta x_{t} + \epsilon_{t} \\
\tau_t &= \gamma_0 + \gamma_1 x_{t}
\end{cases}

とモデルを修正します。推定される係数  \gamma_0と \gamma_1で、 xのトレンド項への影響を見ます。

非定常モデルではないので差分を取らなくてもよいのですが、差分をとれば、

 \begin{align}
y_{t} - y_{t-1} &= \gamma_0 + \gamma_1 (x_{t} t - x_{t-1} (t-1)) \\
&+ \phi (y_{t-1} - y_{t-2}) \\
&+ \beta (x_{t} - x_{t-1}) \\
&+ \epsilon_{t} - \epsilon_{t-1}
\end{align}

となります。

この修正モデルは Cov(x_{t} t - x_{t-1} (t-1), x_{t} - x_{t-1}) / Var(x_{t} t - x_{t-1} (t-1)) / Var(x_{t} - x_{t-1}) \ne 1なので、推定できる見込みがあります。

シミュレーション

実際に推定できるかはわかりません。 t \rightarrow \inftyで共分散は1になります。サンプルサイズがすべてを解決しない匂いがぷんぷんしますね。

一致性(サンプルサイズ nを増やせば推定量がDGPの値に近づく)がありそうだったら上手く行ったと評価するとして、xの生成プロセスを変えて試してみます。

DGPのパラメーターは

係数 変数 値
\gamma_0 Const. 0.0
 \gamma_1  x_{t} t 0.1
 \phi  y_{t-1} 1.0
 \beta  x_{t} 1.0

として、これが上手く推定できるか確認しましょう。

上手くいくケース

上手くいく場合と、そうでない場合が出てきました。上手くいくケースから紹介します。

最初はOLSでも良いかなと思ったのですが*1、差分モデルに生じる内生性 Cov(y_{t-1} - y_{t-2}, \epsilon_{t} - \epsilon_{t-1}) < 0、とくに Cov(y_{t-1}, -\epsilon_{t-1}) < 0*2を無視するのは教育上よろしくないので、 y_{t-2} - y_{t-3}と x_{t-1} - x_{t-2}を操作変数に用いた操作変数法を用いています*3。

#
# データ生成
#
set.seed(619)
n <- 100
x <- y <- z <- numeric(n)
y[1] <- 0
x[1] <- 0
for(t in 2:n){
    x[t] <- runif(1, min = 0, max = 10)
    y[t] <- 0.1 * x[t] * t + y[t - 1] + x[t] + rnorm(1)
}
#
# 推定
#
setup <- function(y){
    dy <- diff(y)
    dy2 <- dy[3:length(dy)]
    dy1 <- dy[2:(length(dy)-1)]
    dy0 <- dy[1:(length(dy)-2)]
    list(l = y, d = dy, d2 = dy2, d1 = dy1, d0 = dy0)
}
y_g <- setup(y)
x_g <- setup(x)
t_g <- setup((1:n)*x)
cnames <- c("Const.", "x[t]*t - x[t-1]*(t-1)", "y[t-1] - y[t-2]", "x[t] - x[t-1]")
library(AER)
r_success <- ivreg(y_g$d2 ~ t_g$d2 + y_g$d1 + x_g$d2 | t_g$d2 + y_g$d0 + x_g$d1 + x_g$d2)
s_success <- summary(r_success, diagnostics = TRUE) # 弱相関テスト、DWHテスト、過剰識別テストの結果が見られる
coef_success <- coef(s_success)
rownames(coef_success) <- cnames
coef_success
                        Estimate  Std. Error    t value      Pr(>|t|)
Const.                0.32626101 0.287460217   1.134978  2.593013e-01
x[t]*t - x[t-1]*(t-1) 0.09826274 0.001231484  79.792117  1.900817e-87
y[t-1] - y[t-2]       0.98901846 0.007831041 126.294635 8.054211e-106
x[t] - x[t-1]         1.07895301 0.075558167  14.279767  3.575584e-25
attr(,"df")
[1] 93
attr(,"nobs")
[1] 97
失敗するケース

試したら簡単だった…とはならなかったです。証明などはしていないので勘違いかもですが、どうも nが大きくなるほど xの階差の分散が xの分散よりも小さくなる場合、推定に失敗します。上手くいくケースの100倍のサンプルサイズで、ちゃんと推定されません。

#
# データ生成
#
set.seed(619)
n <- 10000
x <- y <- z <- numeric(n)
y[1] <- 0
x[1] <- 0
for(t in 2:n){
    x[t] <- 1 + sqrt(t)
    y[t] <- 0.1 * x[t] * t + y[t - 1] + x[t] + rnorm(1)
}
#
# 推定
#
y_g <- setup(y)
x_g <- setup(x)
t_g <- setup((1:n)*x)
r_fail <- ivreg(y_g$d2 ~ t_g$d2 + y_g$d1 + x_g$d2 | t_g$d2 + y_g$d0 + x_g$d1 + x_g$d2)
s_fail <- summary(r_fail, diagnostics = TRUE) # 弱相関テスト、DWHテスト、過剰識別テストの結果が見られる
coef_fail <- coef(s_fail)
rownames(coef_fail) <- cnames
coef_fail
                        Estimate   Std. Error      t value  Pr(>|t|)
Const.                0.02103378 1.236852e-01 1.700590e-01 0.8649672
x[t]*t - x[t-1]*(t-1) 0.09976125 1.677391e-03 5.947404e+01 0.0000000
y[t-1] - y[t-2]       1.00000019 1.694461e-06 5.901582e+05 0.0000000
x[t] - x[t-1]         0.51311271 2.149609e+00 2.387004e-01 0.8113428
attr(,"df")
[1] 9993
attr(,"nobs")
[1] 9997

 nをかなり増やしても x_{t}-x_{t-1}の係数が、DGPの係数に近づきません。一方、他の変数の係数は、概ね正しく推定されています。

上手くいくケースと上手くいかないケースの説明変数の分散を確認しましょう。

cmp_var <- rbind(apply(r_success$model, 2, var), apply(r_fail$model, 2, var))
colnames(cmp_var) <- c("y[t] - y[t-1]", "x[t]*t - x[t-1]*(t-1)", "y[t-1] - y[t-2]", "x[t] - x[t-1]", "y[t-2] - y[t-3]", "x[t-1] - x[t-2]")
rownames(cmp_var) <- c("successful case", "fail case")
round(cmp_var, 5)
                y[t] - y[t-1] x[t]*t - x[t-1]*(t-1) y[t-1] - y[t-2]
successful case        669.08             78537.757    6.716605e+02
fail case        918547244.24              1247.601    9.183455e+08
                x[t] - x[t-1] y[t-2] - y[t-3] x[t-1] - x[t-2]
successful case      18.82007    6.527484e+02        18.49477
fail case             0.00011    9.181437e+08         0.00012

係数が一定のとき説明変数の変動が他の説明変数の変動よりも小さいと寄与度が小さくなって、その説明変数の係数の識別は困難になります。

まとめ

もうちょっとシミュレーションをしてみて、とくに実データに近いと想定されるもので特性を掴みたいところですが、トレンドに関心があるのであれば分析しても良さそうです。

 yと広告予算 xの関係を、この方法で推定することを考えてみましょう。無計画に広告予算を増減しているときは上手くできるのですが、計画的に広告予算を増やしている場合はトレンドの変化で説明されてしまいます。トレンドの分散が、そうでない分散よりも大きいためです。しかし、大きい方の効果は掴んでいるので、大きなミスリーディングでは無いと思います。長期効果にしか関心がないのであれば、カルマンフィルターを使う手法の方がよいかも知れませんが。

*1:説明変数と従属変数の共分散に対して、説明変数と誤差項の共分散が小さいとこのバイアスは目立たなくなります。操作変数法を用いる場合と、推定される係数はほとんど変わらないです。

*2:これによって生じるバイアスをNickellバイアスと呼びます。

*3:Anderson-Hsiao推定もしくはその拡張。

カルマンフィルターを使った時系列データのトレンド変化の要因分析

問題意識が捉えきれていないのですが、以下のお題に答えてみたいと思います。

tjo.hatenablog.com

カルマンフィルターがマイブームなのですが、これは誤差項に正規分布を仮定した線形モデルと言う強い制約がある一方、それ以外は柔軟にモデルを作ることができることが知られています。

TJO氏の言うトレンドが、私がイメージしているトレンドと合致しているのかやや不安なのですが、増加や減少の傾向を何かの変数で説明するモデルを書くこともできます。

\begin{align}
y_t &= Z_t \alpha_t + \epsilon_t \quad \mbox{(観測方程式)} \\
\alpha_{t+1} &= T_t \alpha_t + \eta_t \quad \mbox{(遷移方程式)} \\
Z_t &= \begin{pmatrix}
1 & 0 & 0
\end{pmatrix} \\
T_t &= \begin{pmatrix} 
1 & 1 & \{\mbox{説明変数}\}_t \\
0 & 1 & 0 \\
0 & 0 & 1
\end{pmatrix} \\
\alpha_t &= \begin{pmatrix}
\alpha_{t1} \\
\alpha_{t2} \\
\alpha_{t3}
\end{pmatrix}
\end{align}

 \alpha_{t1}が観測ノイズが入る前の状態で、 \alpha_{t2}と \alpha_{t3}が状態の変化を説明する変数の係数です。

数式に書けても実際に推定できるのか不安になるわけですが、シミュレーションして可能なことを確認しておきましょう。

#
# 練習用のデータを生成する
#
set.seed(957)
n <- 1000 # サンプルサイズはそれなり必要
x <- 1 + sin(2*pi/n*(1:n))
# x <- runif(n) # こちらにするとサンプルサイズが10倍ぐらいいる

T0 <- matrix(c(1, 0, 0, 1, 1, 0, NA, 0, 1), 3, 3)
T <- array(dim = c(dim(T0), n))
for(t in 1:n){
    T[,,t] <- T0
    T[,,t][is.na(T[,,t])] <- x[t]
}

y <- numeric(n)
alpha <- matrix(c(1, -1, 1), 3, 1)
Z <- matrix(c(1, 0, 0), 1, 3)
for(t in 1:n){
    y[t] <- Z %*% alpha + rnorm(1, sd = 30)
    # 真のalphaはalpha[2]とalpha[3]が不変
    # 変化しても推定はできるが、正しく推定できているか判断がつかないため
    alpha <- T[,,t] %*% alpha + c(rnorm(1), 0, 0)
}

# plot(y, type="l")をすれば雰囲気が出るカモ

#
# 練習データを推定する
#
library(KFAS)

# KFASの推定モデル用の変数
Q <- R <- diag(length(alpha)) # 誤差項にかかる行列
Q[Q == 1] <- NA # 分散共分散行列の対角成分は推定するパラメーター
# Q <- matrix(c(NA, rep(0, 8)), 3, 3) # にすると、alpha[2]とalpha[3]を変化させる誤差項の分散がゼロ、つまり時間不変に仮定にできるハズ

model_KFAS <- SSModel(y ~ -1 + SSMcustom(
        Z = Z, T = T, R = R, Q = Q,
        a1 = matrix(c(0, 0, 0), 3, 1), # 初期値
        P1 = diag(length(alpha)) * 10 # 初期値の分散
        ),
    H = NA # 観測方程式の誤差項の分散は推定するパラメーター
)

# initsには、推定するパラメーター(NAを埋めた箇所)の初期値を入れる
fit_KFAS <- fitSSM(model_KFAS, inits = c(1, 1, 1, 1), method = "L-BFGS-B")

# 状態変数(state)と予測値(signal)を計算
r_KFS <- KFS(fit_KFAS$model, filtering = c("state", "signal"), smoothing = c("state", "signal"))

# alphaのフィルタリング分布の期待値の推定結果(分散はr_KFS$Pttに入る)
# r_KFS$att[2]が-1、r_KFS$att[3]が1ら辺にいけば、だいたい上手く行っている
head(r_KFS$att)
tail(r_KFS$att)
plotted_series <- cbind(x, y, r_KFS$att)
colnames(plotted_series) <- c("外生変数x", "内生変数y", "αₜ₁", "αₜ₂", "αₜ₃")
plot(plotted_series, main = "外生変数付きローカル線形トレンド過程にカルマンフィルターをかけた例")
Time Series:
Start = 1 
End = 6 
Frequency = 1 
      custom1     custom2    custom3
1  -0.4867575  0.00000000  0.0000000
2  -0.6869353 -0.06558343 -0.0659955
3  -4.0624118 -0.77534814 -0.7825130
4  -4.7764427 -0.64169851 -0.6471203
5 -16.3684459 -1.87060621 -1.8971902
6 -31.3589590 -2.94769498 -2.9985626
Time Series:
Start = 995 
End = 1000 
Frequency = 1 
       custom1   custom2   custom3
 995 -20.04321 -1.017278 0.9956837
 996 -21.29090 -1.019186 0.9962437
 997 -21.06304 -1.018749 0.9961181
 998 -19.66620 -1.016492 0.9954825
 999 -20.40062 -1.017582 0.9957829
1000 -19.85585 -1.016699 0.9955451

外生変数付きローカル線形トレンド過程にカルマンフィルターをかけた例
概ね期待通りの係数が推定されました。

上の例では時系列トレンドと外生変数の関係をそのまま見ましたが、時系列トレンドを時系列トレンドで回帰することもできます。以下のエントリーに例をあげました。

uncorrelated.hatenablog.com

LinuxのFirefoxでYahoo!Japanにパスキーを設定する方法

最近、Yahoo!Japanにパスワードでログインしようとすると、

パスワードだけではない認証を使えというメッセージ

とメッセージが出ます。

同社はオークションやショッピングなどで金銭を扱うサービスを提供しており、セキュリティーの強化は理解できるものです。パスワードの運用は、ちょっとした不注意でフィッシング詐欺にありますからね。

しかし、Linux MintでYahoo!Japanにパスキーを設定しようとすると、以下のようなメッセージが出てできません。

パスキーに対応していないというメッセージ

ユーザーエージェントでWindowsとMacOS以外は排除しているようです。サポート外と言うことなんでしょうが、いまどきどうなんだと言う感じですね。

仕方がないのでユーザーエージェントを偽装します。

Firefoxのロケーションバーにabout:configと入れ、設定名general.useragent.overrideを指定、ラジオボックスで文字列を選択して以下の偽装ユーザーエージェントを入れて、ENTERを押します。

Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/149.0

これで、Yahoo!JapanからWindowsと判別され、パスキーの設定と利用が出来るようになりました。

なお、pixivのパスキーの利用もユーザーエージェントで制限されていて、こちらは以下のように偽装するとパスキーをログインに使うことができます。2026/5/22にユーザーエージェント偽装なしにログインできたので、何かの空目だったかも知れません。

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.3912.86