からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

gganimateパッケージで値の変化する数式を表示したい

はじめに

 素直なやり方ではできなかったのでむりくりなんとかする黒魔術シリーズです。もっといい方法があれば教えてください。

 この記事では、R言語でグラフのアニメーションを作成する際に値が変化する数式を表示する方法を解説します。

【前の内容】

www.anarchive-beta.com

【目次】

gganimateパッケージで値の変化する数式を表示したい

 gganimate パッケージを利用して、ggplot2 パッケージによるグラフのアニメーションを作成できる。また、base パッケージ(組み込み関数)の expression() を使って図に数式を描画できる。この記事では、expression記法による数式をアニメーションに表示する小細工(黒魔術)的な処理を備忘録として書いておく。
 expression記法による数式の表示については、「【R】expression関数で微妙に凝った数式を表示したい - からっぽのしょこ」を参照のこと。

 利用するパッケージを読み込む。

# 利用パッケージ
library(tidyverse)
library(gganimate)

 この記事では、基本的に パッケージ名::関数名() の記法を使うので、パッケージを読み込む必要はない。ただし、作図コードについてはパッケージ名を省略するので、ggplot2 を読み込む必要がある。
 また、ネイティブパイプ演算子 |> を使う。magrittr パッケージのパイプ演算子 %>% に置き換えられるが、その場合は magrittr を読み込む必要がある。

サブタイトルに表示

 フレームごとの数式をサブタイトルの位置に描画することを考える。
 単位円の円周上の点を例とする。円周の座標については「【R】円周の作図 - からっぽのしょこ」を参照のこと。

 円周の座標を作成する。

# 円周の座標を作成
circle_df <- tibble::tibble(
  t = seq(from = 0, to = 2*pi, length.out = 361), 
  x = cos(t), 
  y = sin(t)
)
circle_df
# A tibble: 361 × 3
        t     x      y
    <dbl> <dbl>  <dbl>
 1 0      1     0     
 2 0.0175 1.00  0.0175
 3 0.0349 0.999 0.0349
 4 0.0524 0.999 0.0523
 5 0.0698 0.998 0.0698
 6 0.0873 0.996 0.0872
 7 0.105  0.995 0.105 
 8 0.122  0.993 0.122 
 9 0.140  0.990 0.139 
10 0.157  0.988 0.156 
# ℹ 351 more rows

 単位円周上の点  (x, y) の座標は、ラジアン  0 \leq t \leq 2 \pi を用いて  (x, y) = (\cos t, \sin t) で得られる。

フレーム番号による切り替え

 フレームごとの通し番号(昇順の重複しない値)を用いてグラフを切り替える。

基本的な使い方

 まずは、gganimate パッケージの基本的な使い方を確認する。

 円周上の点の座標を作成する。

# フレーム数を指定
frame_num <- 60

# 円周上の点の座標を作成
point_df <- tibble::tibble(
  frame_i = 1:frame_num, 
  t = seq(from = 0, to = 2*pi, length.out = frame_num+1)[-(frame_num+1)], 
  x = cos(t), 
  y = sin(t)
)
point_df
# A tibble: 60 × 4
   frame_i     t     x     y
     <int> <dbl> <dbl> <dbl>
 1       1 0     1     0    
 2       2 0.105 0.995 0.105
 3       3 0.209 0.978 0.208
 4       4 0.314 0.951 0.309
 5       5 0.419 0.914 0.407
 6       6 0.524 0.866 0.5  
 7       7 0.628 0.809 0.588
 8       8 0.733 0.743 0.669
 9       9 0.838 0.669 0.743
10      10 0.942 0.588 0.809
# ℹ 50 more rows

 フレーム数を指定して、フレーム番号と単位円周上の点の座標をデータフレームに格納する。

 円周上の点のアニメーションを作成する。

# 円周上の点を作図
graph <- ggplot() + 
  geom_path(data = circle_df, 
            mapping = aes(x = x, y = y), 
            linewidth = 1) + # 円周
  geom_point(data = point_df, 
            mapping = aes(x = x, y = y), 
            size = 5) + # 円周上の点
  gganimate::transition_manual(frames = frame_i) + # フレーム制御
  coord_equal(ratio = 1) + # アスペクト比
  labs(title = "unit circle", 
       subtitle = "{current_frame}")

# アニメーションを作成
anim <- gganimate::animate(
  plot = graph, nframes = frame_num, fps = 10, 
  width = 500, height = 500
)

# ファイルを書き出し
gganimate::anim_save(filename = "frame_index.gif", animation = anim, path = "folder")

フレーム番号を表示

 gganimate パッケージの transition_manual() にフレーム順を示す列を指定する。指定したフレームごとの値を "{current_frame}" で扱える。
 各フレームのグラフをフレーム番号で制御して、サブタイトルにフレーム番号を表示している。

 フレーム番号に文字列を追加して表示する。

# 円周上の点を作図
graph <- ggplot() + 
  geom_path(data = circle_df, 
            mapping = aes(x = x, y = y), 
            linewidth = 1) + # 円周
  geom_point(data = point_df, 
            mapping = aes(x = x, y = y), 
            size = 5) + # 円周上の点
  gganimate::transition_manual(frames = frame_i) + # フレーム制御
  coord_equal(ratio = 1) + # アスペクト比
  labs(title = "unit circle", 
       subtitle = "frame: {current_frame}")

# アニメーションを作成
anim <- gganimate::animate(
  plot = graph, nframes = frame_num, fps = 10, 
  width = 500, height = 500
)

# ファイルを書き出し
gganimate::anim_save(filename = "add_string.gif", animation = anim, path = "folder")

フレーム番号と文字列を表示

 "{current_frame}" の波括弧 {} の外に文字列を追加できる。

ベクトルによるラベルの切り替え

 ここからは、サブタイトルに数式を表示する方法を確認していく。

 フレームラベルを作成する。

# 円周上の点の座標を作成
point_df <- point_df |> 
  dplyr::mutate(
    frame_label = paste0(
      "(x, y) = (", round(x, digits = 2), ", ", round(y, digits = 2), ")"
    )
  )
point_df
# A tibble: 60 × 5
   frame_i     t     x     y frame_label          
     <int> <dbl> <dbl> <dbl> <chr>                
 1       1 0     1     0     (x, y) = (1, 0)      
 2       2 0.105 0.995 0.105 (x, y) = (0.99, 0.1) 
 3       3 0.209 0.978 0.208 (x, y) = (0.98, 0.21)
 4       4 0.314 0.951 0.309 (x, y) = (0.95, 0.31)
 5       5 0.419 0.914 0.407 (x, y) = (0.91, 0.41)
 6       6 0.524 0.866 0.5   (x, y) = (0.87, 0.5) 
 7       7 0.628 0.809 0.588 (x, y) = (0.81, 0.59)
 8       8 0.733 0.743 0.669 (x, y) = (0.74, 0.67)
 9       9 0.838 0.669 0.743 (x, y) = (0.67, 0.74)
10      10 0.942 0.588 0.809 (x, y) = (0.59, 0.81)
# ℹ 50 more rows

 フレーム番号に応じて座標とラベルをデータフレームに格納する。
 この例では、各フレームのラベルとして座標の値を用いる。

 フレームごとにラベルを表示する。

# 円周上の点を作図
graph <- ggplot() + 
  geom_path(data = circle_df, 
            mapping = aes(x = x, y = y), 
            linewidth = 1) + # 円周
  geom_point(data = point_df, 
            mapping = aes(x = x, y = y), 
            size = 5) + # 円周上の点
  gganimate::transition_manual(frames = frame_i) + # フレーム制御
  coord_equal(ratio = 1) + # アスペクト比
  labs(title = "unit circle", 
       subtitle = "{point_df[['frame_label']][as.integer(current_frame)]}")

# アニメーションを作成
anim <- gganimate::animate(
  plot = graph, nframes = frame_num, fps = 10, 
  width = 500, height = 500
)

# ファイルを書き出し
gganimate::anim_save(filename = "as_index.gif", animation = anim, path = "folder")

フレームラベルを表示:丸め込み

 "{current_frame}" の波括弧 {} の内で current_frame を変数として扱える。
 フレーム番号をインデックスとして用いて、フレームごとのラベルをベクトルから取り出して表示する。

 フレームラベルを作成する。

# 円周上の点の座標を作成
point_df <- point_df |> 
  dplyr::mutate(
    frame_label = paste0(
      "(x, y) = (", sprintf(x, fmt = "%.2f"), ", ", sprintf(y, fmt = "%.2f"), ")"
    )
  )
point_df
# A tibble: 60 × 5
   frame_i     t     x     y frame_label          
     <int> <dbl> <dbl> <dbl> <chr>                
 1       1 0     1     0     (x, y) = (1.00, 0.00)
 2       2 0.105 0.995 0.105 (x, y) = (0.99, 0.10)
 3       3 0.209 0.978 0.208 (x, y) = (0.98, 0.21)
 4       4 0.314 0.951 0.309 (x, y) = (0.95, 0.31)
 5       5 0.419 0.914 0.407 (x, y) = (0.91, 0.41)
 6       6 0.524 0.866 0.5   (x, y) = (0.87, 0.50)
 7       7 0.628 0.809 0.588 (x, y) = (0.81, 0.59)
 8       8 0.733 0.743 0.669 (x, y) = (0.74, 0.67)
 9       9 0.838 0.669 0.743 (x, y) = (0.67, 0.74)
10      10 0.942 0.588 0.809 (x, y) = (0.59, 0.81)
# ℹ 50 more rows

 小数点以下の桁数が変化しないように 0 で埋める場合は、sprintf() を使って丸め込みを行う。

 フレームごとにラベルを表示する。

# 円周上の点を作図
graph <- ggplot() + 
  geom_path(data = circle_df, 
            mapping = aes(x = x, y = y), 
            linewidth = 1) + # 円周
  geom_point(data = point_df, 
            mapping = aes(x = x, y = y), 
            size = 5) + # 円周上の点
  gganimate::transition_manual(frames = frame_i) + # フレーム制御
  coord_equal(ratio = 1) + # アスペクト比
  labs(title = "unit circle", 
       subtitle = "{point_df[['frame_label']][as.integer(current_frame)]}")

# アニメーションを作成
anim <- gganimate::animate(
  plot = graph, nframes = frame_num, fps = 10, 
  width = 500, height = 500
)

# ファイルを書き出し
gganimate::anim_save(filename = "zero_padding.gif", animation = anim, path = "folder")

フレームラベルを表示:ゼロ埋め

 先ほどの作図コードで描画する。

フレームラベルによる切り替え

 フレームごとのラベル(因子レベルを設定した重複しない文字列)を用いてグラフを切り替える。

character型のラベル

 フレームラベルに因子レベルを設定する。

# 円周上の点の座標を作成
point_df <- point_df |> 
  dplyr::mutate(
    frame_label = paste0(
      "(x, y) = (", sprintf(x, fmt = "%.2f"), ", ", sprintf(y, fmt = "%.2f"), ")"
    ) |> 
      (\(vec) {factor(vec, levels = vec)})()
  )
point_df
# A tibble: 60 × 5
   frame_i     t     x     y frame_label          
     <int> <dbl> <dbl> <dbl> <fct>                
 1       1 0     1     0     (x, y) = (1.00, 0.00)
 2       2 0.105 0.995 0.105 (x, y) = (0.99, 0.10)
 3       3 0.209 0.978 0.208 (x, y) = (0.98, 0.21)
 4       4 0.314 0.951 0.309 (x, y) = (0.95, 0.31)
 5       5 0.419 0.914 0.407 (x, y) = (0.91, 0.41)
 6       6 0.524 0.866 0.5   (x, y) = (0.87, 0.50)
 7       7 0.628 0.809 0.588 (x, y) = (0.81, 0.59)
 8       8 0.733 0.743 0.669 (x, y) = (0.74, 0.67)
 9       9 0.838 0.669 0.743 (x, y) = (0.67, 0.74)
10      10 0.942 0.588 0.809 (x, y) = (0.59, 0.81)
# ℹ 50 more rows

 各フレームのラベルを因子型に変換して、フレーム順に因子レベルを設定する。
 \() {} は自作関数の簡易的な記法であり、この例の処理をbaseパイプを使って行う場合には必要になる。magrittrパイプを使う場合は factor(., levels = .) で処理できる。

 因子レベルを確認する。

# 因子レベルを確認
head(point_df[["frame_label"]])
[1] (x, y) = (1.00, 0.00) (x, y) = (0.99, 0.10) (x, y) = (0.98, 0.21)
[4] (x, y) = (0.95, 0.31) (x, y) = (0.91, 0.41) (x, y) = (0.87, 0.50)
60 Levels: (x, y) = (1.00, 0.00) ... (x, y) = (0.99, -0.10)

 因子レベルの順番にグラフが表示される。

 フレームラベルによりフレームを制御する。

# 円周上の点を作図
graph <- ggplot() + 
  geom_path(data = circle_df, 
            mapping = aes(x = x, y = y), 
            linewidth = 1) + # 円周
  geom_point(data = point_df, 
            mapping = aes(x = x, y = y), 
            size = 5) + # 円周上の点
  gganimate::transition_manual(frames = frame_label) + # フレーム制御
  coord_equal(ratio = 1) + # アスペクト比
  labs(title = "unit circle", 
       subtitle = "{current_frame}")

# アニメーションを作成
anim <- gganimate::animate(
  plot = graph, nframes = frame_num, fps = 10, 
  width = 500, height = 500
)

# ファイルを書き出し
gganimate::anim_save(filename = "frame_label.gif", animation = anim, path = "folder")

フレームラベルを表示:文字列

 各フレームのグラフをラベルの因子レベルで制御して、サブタイトルにラベルを表示している。

expression型のラベル

 expression記法のラベルを作成する。

# 円周上の点の座標を作成
point_df <- point_df |> 
  dplyr::mutate(
    frame_label = paste0(
      "(list(cos~theta, sin~theta)) == (list('", sprintf(x, fmt = "%.2f"), "', '", sprintf(y, fmt = "%.2f"), "'))"
    ) |> 
      (\(vec) {factor(vec, levels = vec)})()
  )
point_df
# A tibble: 60 × 5
   frame_i     t     x     y frame_label                                        
     <int> <dbl> <dbl> <dbl> <fct>                                              
 1       1 0     1     0     (list(cos~theta, sin~theta)) == (list('1.00', '0.0…
 2       2 0.105 0.995 0.105 (list(cos~theta, sin~theta)) == (list('0.99', '0.1…
 3       3 0.209 0.978 0.208 (list(cos~theta, sin~theta)) == (list('0.98', '0.2…
 4       4 0.314 0.951 0.309 (list(cos~theta, sin~theta)) == (list('0.95', '0.3…
 5       5 0.419 0.914 0.407 (list(cos~theta, sin~theta)) == (list('0.91', '0.4…
 6       6 0.524 0.866 0.5   (list(cos~theta, sin~theta)) == (list('0.87', '0.5…
 7       7 0.628 0.809 0.588 (list(cos~theta, sin~theta)) == (list('0.81', '0.5…
 8       8 0.733 0.743 0.669 (list(cos~theta, sin~theta)) == (list('0.74', '0.6…
 9       9 0.838 0.669 0.743 (list(cos~theta, sin~theta)) == (list('0.67', '0.7…
10      10 0.942 0.588 0.809 (list(cos~theta, sin~theta)) == (list('0.59', '0.8…
# ℹ 50 more rows

 expression記法では、等号は ==、複数の(数式上の)変数を並べる場合は list(変数1, 変数2) で表示できる。( (list(変数1, 変数2)) をより簡単に {}(変数1, 変数2) とも書けるが、資料作成の都合上(次の図で {} の間に改行の記号 \n が含まれてしまうため) list() の方を使っている。)

 意図通りにならない例を確認する。

# 円周上の点を作図
graph <- ggplot() + 
  geom_path(data = circle_df, 
            mapping = aes(x = x, y = y), 
            linewidth = 1) + # 円周
  geom_point(data = point_df, 
            mapping = aes(x = x, y = y), 
            size = 5) + # 円周上の点
  gganimate::transition_manual(frames = frame_label) + # フレーム制御
  coord_equal(ratio = 1) + # アスペクト比
  labs(title = "unit circle", 
       subtitle = "{parse(text = as.character(current_frame))}")

# アニメーションを作成
anim <- gganimate::animate(
  plot = graph, nframes = frame_num, fps = 10, 
  width = 500, height = 500
)

# ファイルを書き出し
gganimate::anim_save(filename = "expression_fnc.gif", animation = anim, path = "folder")

意図しない例

 (文字列型に変換されるからなのか(?)、) "{current_frame}" を用いてexpression記法を使えないらしい。

 ラベル関数を使ってサブタイトルを代用する。

# 円周上の点を作図
graph <- ggplot() + 
  geom_path(data = circle_df, 
            mapping = aes(x = x, y = y), 
            linewidth = 1) + # 円周
  geom_point(data = point_df, 
            mapping = aes(x = x, y = y), 
            size = 5) + # 円周上の点
  geom_text(data = point_df, 
            mapping = aes(x = -Inf, y = Inf, label = frame_label), 
            parse = TRUE, hjust = 0, vjust = -0.5, size = 4) + # サブタイトルの代用
  gganimate::transition_manual(frames = frame_i) + # フレーム制御
  coord_equal(ratio = 1, clip = "off") + # アスペクト比
  theme(
    plot.title = element_text(size = 12), # タイトル
    plot.subtitle = element_text(size = 60) # サブタイトル
  ) + # 図の体裁
  labs(
    title = "unit circle", 
    subtitle = "" # ラベル用の空行
  )

# アニメーションを作成
anim <- gganimate::animate(
  plot = graph, nframes = frame_num, fps = 10, 
  width = 500, height = 500
)

# ファイルを書き出し
gganimate::anim_save(filename = "text_fnc.gif", animation = anim, path = "folder")

フレームラベルを表示:数式

 geom_text() で文字列を描画できるので、サブタイトルの位置に各フレームのラベルを表示する。parse = TRUE を指定するとexpression記法で数式を表示できる。x = -Inf で描画領域の左端、y = Inf で描画領域の最上部にプロットする。また、hjust = 0 で左揃え、vjust 引数で上下方向の表示位置を調整する。
 coord_***()clip = "off" を指定して、描画領域の外側にラベル(やデータ点など)を描画できるように設定する。
 labs()subtitle 引数に空白を指定して、ラベルの表示できる空間を開けておく。
 サブタイトルなどの図の体裁は theme() で設定できる。plot.subtitle 引数に element_text() を使って調整する。

 (サブタイトルの位置に拘らず)描画領域の内側であればシンプルに処理できる。

# 円周上の点を作図
graph <- ggplot() + 
  geom_path(data = circle_df, 
            mapping = aes(x = x, y = y), 
            linewidth = 1) + # 円周
  geom_point(data = point_df, 
            mapping = aes(x = x, y = y), 
            size = 5) + # 円周上の点
  geom_label(data = point_df, 
             mapping = aes(x = -Inf, y = Inf, label = frame_label), 
             parse = TRUE, hjust = 0, vjust = 1, size = 4, alpha = 0.8) + # フレームラベル
  gganimate::transition_manual(frames = frame_i) + # フレーム制御
  coord_equal(ratio = 1, clip = "off") + # アスペクト比
  labs(title = "unit circle")

# アニメーションを作成
anim <- gganimate::animate(
  plot = graph, nframes = frame_num, fps = 10, 
  width = 500, height = 500
)

# ファイルを書き出し
gganimate::anim_save(filename = "label_fnc.gif", animation = anim, path = "folder")

簡易的な例

 geom_label() で枠付きの文字列を描画できる。`geom_text() と同様に処理する。

 以上で、フレームごとの数式をサブタイトルの位置に描画できた。

 この記事では、gganimateパッケージの利用時に値が変化する数式の表示方法を確認した。

参考文献

 gganimateパッケージについては扱っていませんが、ggplot2パッケージについて解説されています。

おわりに

 1つ前の記事と同様に、備忘録としていつか書いておきたいけど今書かなくてもいいよなと放置していた内容を、アドカレの季節ということで棚卸的に記事にしました。
 特に拘らなければこんな手間をかけなくていいのですが、私は拘る性分なので、、このブログの図はこんな感じで手間と暇をかけて作られています。

 2024年12月8日は、AMEFURASSHIの愛来さんの22歳のお誕生日です!

 ライブ映像の最初に映る方です。ヴィジュアル良しキャラクター良しスキル良しの3拍子揃ったザ・スタプラアイドルって感じの方(新規スタプラオタクの感想)です。チャンネル内にはバラエティ動画もたくさんあるので、ぜひ合わせて見てください。