3 検証:「三國志」シリーズの人材は年々無個性化しているのか?

ここでの処理は analysis.R に書かれている.

3.1 ggplot2による視覚化

以上で一旦名寄せ処理を切り上げて, 整形したデータから情報を読み取る. まずは大まかに集約した情報から, 徐々に個別の部分に拡大して解像度を挙げていこう.

次に, 各作品で, 新しく登録された人物と除外された人物が何人かを表してみる. 以下では登場人物が各作品で採用されたか, 逆に前作と比較して採用を見送られたかを判定した結果をdf_in_outに出力している.

# シリーズの参加回数
attend_times <- df_all %>% group_by(name_id) %>% summarise(attend_times = n()) %>% 
    ungroup
# 初登場人数・脱落人数の集計
at_first <- df_all %>% arrange(name_id, as.integer(title)) %>% group_by(name_id) %>% 
    summarise(at_first = as.integer(first(title))) %>% ungroup
df_all <- inner_join(df_all, attend_times, by = "name_id") %>% inner_join(at_first, 
    by = "name_id")
df_in_out <- df_all %>% select(title, name_id) %>% mutate(exists = T, title = as.integer(title)) %>% 
    right_join(x = ., y = expand_grid(title = unique(.$title), name_id = unique(.$name_id)), 
        by = c("title", "name_id")) %>% mutate(exists = if_else(is.na(exists), 
    F, T)) %>% arrange(name_id, title) %>% group_by(name_id) %>% mutate(join = !lag(exists) & 
    exists, out = !exists & lag(exists)) %>% ungroup

さらに, 各作品ごとに「新規採用」「不採用」「継続」の3通りの人数を集計する.

df_in_out_summary <- df_in_out %>% group_by(title) %>% summarise_if(is.logical, 
    sum) %>% ungroup %>% mutate(keep = exists - join) %>% select(-exists) %>% 
    pivot_longer(-title, names_to = "var", values_to = "number") %>% mutate(var = factor(var, 
    levels = c("out", "join", "keep"), labels = c("out", "in", "keep"))) %>% 
    arrange(title, var)

3.1は, その結果をグラフで表したものである.前作から追加された人物が in, 逆に除外された人物を out, 続投している人物を keep で表した. つまり, in + keep が各作品に登場する人数である. この図からは, 4, 12 で前作より減っているものの基本的に最近の作品ほど登場人物が増えていることがわかる. よって, 少しづつ正史三国志に記述のある人物が増えていることが分かる29.

ggplot(df_in_out_summary, aes(x = title, y = number, group = var, fill = var, 
    color = var, alpha = (var != "out"), linetype = (var == "out"))) + 
    geom_bar(stat = "identity", position = "stack", size = 1, width = 0.6) + 
    scale_x_continuous(breaks = 2:13) + scale_fill_colorblind() + scale_alpha_manual(guide = F, 
    breaks = c(F, T), values = c(0.1, 1)) + scale_linetype_manual(guide = F, 
    values = c("solid", "dashed")) + scale_color_colorblind(guide = F) + 
    labs(x = "タイトル", y = "人数") + theme_document
登録・除外フロー

図 3.1: 登録・除外フロー

[テクニカル] ggplot2の使い方

3.1を含め, 以降の画像はほぼ全てggplot2で作成している. ggplot2は自由度が高く, かつデータビジュアライゼーションでよく使われる様々な形式のグラフを簡単に作成することができる. しかし図3.1はやや変わった見せ方をしているため, コードが長大化している. 細かいレイアウトにこだわらなければ2,3行以内で表示できるが, 今回はスライドと原稿で両立するようなレイアウトにしようとしたため, 多くの設定を手動調整している.

ggplot2は自由度があるぶん, 全ての機能を説明するのは大変である. 今回は作例に使った構文で特に有用だったり使い方がわかりにくかったりするものだけを解説する.

Rで使えるggplot2以外のフレームワークとして, 例えばplotlyがある. しかし現状ではggplot2と比較して(1)変数の増加に対してスケールせず, 冗長になりがちな構文であり(tidyverseとのシナジーが得られにくい), (2)デザインの微調整の構文が煩雑または機能が限定されている, という理由で私は使っていない. これらの特徴は繰り返し変数や軸を変えてグラフを描くことの多いデータ分析では大きなデメリットである30.

棒グラフを作成するのに最低限必要な構文は以下のように最初の3行だけである. 色や軸ラベルの名称, メモリの細かさ, 凡例の位置など細かいところを調整するために以降の7行を追加している(図3.2).

ggplot(df_in_out_summary, aes(x = title, y = number, group = var, fill = var, 
    color = var)) + geom_bar(stat = "identity", position = "stack", size = 1, 
    width = 0.6)
最低限で書けるが, 見栄えが悪い

図 3.2: 最低限で書けるが, 見栄えが悪い

ggplot2でグラフを描くにあたって最低限必要なのは入力データの指定をする ggplot(), 軸を指定する aes(), そしてグラフの種類を決める, geom_で始まる各種関数である. 今回はgeom_bar()でバープロットを描画している. aes(x = title, y = number, group = var, fill = var, color = var)について, x, y, group, fill, colorはそれぞれx軸, y軸, グループ分け, 塗りつぶし色を指定する引数であり, 上記のようにグループごとに色分けした棒グラフを描くために必要な設定である. どのような設定が必要かはgeom_の関数ごとに異なるため, 慣れないうちは必要な引数をヘルプで確認する必要があるだろう. geom_bar()ではさらに, stat="identity"でy軸の計算方法を指定しているデフォルトはstat="count"で, これは aes(y=)で指定したyの件数をy軸に出力する. しかし今回は件数はすでに集計済みなので, stat="identity"を指定し, 件数ではなくyの値をそのまま出すようにしている. position="stack"は積み上げ棒グラフにする設定である (積み上げ棒グラフはデフォルトなのであえて指定する必要はない). 例えば"dodge"を指定すると, 以下の図3.3のように横並びになる.

ggplot(df_in_out_summary, aes(x = title, y = number, group = var, fill = var, 
    color = var)) + geom_bar(stat = "identity", position = "dodge", size = 1, 
    width = 0.6)
棒グラフを横並びに

図 3.3: 棒グラフを横並びに

size, widthは名前の通り棒の大きさや幅を指定するオプションである.

3.1で上記の3行のプログラムに追加している, scale_で始まる関数は全て色や目盛りと言った見やすさに関するレイアウトを設定する関数である. さらに文字の大きさや見出しの非表示など細かい設定は, theme()関数を使うことになる. 今回は使いまわすため, プリセットしてあらかじめ作成したtheme_documentを使った.

theme_document <- theme_classic(base_family = font_name) + theme(axis.ticks = element_blank(), 
    legend.position = "bottom", strip.placement = "outside", legend.key.width = unit(3, 
        "line"), legend.title = element_blank())

これはレポート用の体裁とプレゼンテーション用のスライドでレイアウトを調整する必要があったからで, これとは別にtheme_presenも作成している.

その他, よくある用例が『ggplot2逆引き集』でいくつか紹介されている

https://kazutan.github.io/ggplot2-gyakubiki-book/index.html

自由になんでもして良いとなると, ついつい自分の好みが反映されてしまいがちである. しかしここでは視聴者に情報を適切に読み取ってもらうことが第一である. 我流のグラフやこだわった配色はかえって見やすさを損ねることが多く, また設定のために書くコードも増えてしまうため, なるべく基本的な構文だけで表現できないかをまず考えることが重要である. ggthemesパッケージはggplot2用のテーマやカラーパレットを追加してくれる. その中にあるcolorblind/panderシリーズは色弱者に配慮した配色になっているため, 私はこれを常用している.

自分の作成したグラフが色弱者にも認識しやすいかどうかを確認するには, 以下のサイトが便利である.

https://asada.website/webCVS/

ggplot2に限定するならば, colorblindrパッケージを使えばより簡単に色弱者にとっての見え方を確認できる.

https://github.com/clauswilke/colorblindr

使い方は単純で, cvd_grid()にグラフオブジェクトを与えるだけである.

3.2 補足: データビジュアライゼーションの教科書について

グラフの書き方にも流儀がある. 3D 円グラフはやめよう31, ユーレイ棒グラフはやめよう32, という話は昔から喚起されているので知っている人も多いかもしれない. これだけでなく, もっと体系的なグラフ作成のルールというのがあるのだが, それをここで全て説明するのは大変だ. そのうち挑戦してみたくはあるが.

グラフの書き方に関する本は Tufte (2001) が古典的?である. 彼の基本的な考え方は「グラフに余計な装飾をするな」であり, 見る者になんら情報をもたらさない装飾のあるグラフを “chartjunk” と呼んだ. そして線や点や色などの使用を最小限にしてデータの特徴を表現できているかの指標として, グラフを描くのに使ったインクの量に対してどれだけのデータを示せたかを表す「データ-インク比率 (data-ink ratio)」を提唱している. つまり, データ-インク比率が少ないほど, 最小限のシンプルなグラフでより多くの情報を提示できているということになる. 3Dグラフは装飾過剰であることから, データ-インク比率の観点からも望ましいものでないことが分かる.

最近のものとして Schwabish (2014) は経済学の論文で実際に掲載された図を例に添削している. Healy (2018) はTufteの思想を受け継ぎつつ, 掲載されている全ての図に対してggplot2によるコードを公開しているため, タイトル通り “practical” である. 一方で, これらはいずれも日本語訳がない33. Tufte 流の理論に則ったという本では, 藤 and 渡部 (2019) が比較的近い. ただしグラフの例は紹介されているものの実際にどのようなソフトウェアでどうやって作成するかといったことは書かれていないため, 教科書としては物足りない. あるいはRではないが, Qlik Senseというソフトウェアでデータ-インク比率のルールに則した作例を紹介しているブログがある.

https://qlik-training.ashisuto.co.jp/data-ink-ratio/

森藤 and あんちべ (2014) もまた, グラフ作成のフレームワークとしてd3.jsの使用を前提にしているものの, 意味のあるグラフの作り方に焦点を置いている.

これらの情報はある程度有用であるが, 教科書と呼べる程度に一般的な話題を扱い, かつRの作例を載せた実践的な日本語の教科書は現時点では存在しないようだ.

3.3 skimrによる要約統計の計算

では次に, シリーズごとの能力値の傾向を見ていこう. そのために次はグラフではなく要約統計量を確認してみる. 要約統計量を計算するのに役立つのがskimrパッケージである. 要約統計量を表示する関数は, 組み込みのsummary()を始めいくつもあるが, skimr

  • summary()よりも見やすい
  • group_by() したデータを与えるとグループ別集計してくれる
  • 出力もまたデータフレームである(体裁の修正がしやすい)

といった便利さからおすすめする3435.

基本的にはskim()にデータフレームを与えるだけだが, 表示したい要約統計量を変更したい場合はskimr::skim_with()を使う. これは関数ジェネレータのように扱う. 例えば以下の例ではデフォルトの項目に加え歪度と尖度を表示するように変更した関数 my_skim() を作成している.

my_skim <- skim_with(numeric = sfl(n = length, mean = mean, skew = skewness, 
    kurto = kurtosis, hist = NULL))

これを使い, シリーズごとに能力値の要約統計量を求める(表3.1).

map(1:13, function(x) filter(df_all %>% mutate(title = as.integer(title)), 
    title == x) %>% unnest(cols = data) %>% select_if(is.numeric)) %>% 
    bind_rows %>% select(title, 知力, 武力, 政治, 魅力, 統率) %>% 
    group_by(title) %>% my_skim()
表 3.1: 作品ごとの要約統計量(一部)
title status n missing mean sd skew kurto min p25 p50 p75 max
1 知力 254 1 56.4 24.6 0.0 1.8 14 35.0 56.5 77.8 100
2 知力 312 1 58.4 22.5 -0.1 1.9 13 40.0 59.5 78.0 100
3 知力 531 1 56.1 17.5 0.0 2.8 12 45.0 57.0 67.0 100
4 知力 454 1 58.6 19.7 -0.2 2.4 13 45.0 61.0 71.0 100
5 知力 500 1 59.5 21.0 -0.1 2.3 10 42.8 61.0 75.0 100
6 知力 520 1 59.1 20.0 -0.3 2.4 15 44.0 62.0 73.0 100
7 知力 538 1 57.6 19.6 -0.2 2.2 14 42.0 61.0 73.0 97
8 知力 567 1 56.2 19.2 0.0 2.2 10 41.0 56.0 70.0 100
9 知力 663 1 59.3 20.5 -0.6 2.8 1 44.0 65.0 73.0 100
10 知力 650 1 58.5 21.3 -0.6 2.7 1 44.0 65.0 74.0 100
11 知力 671 1 58.9 20.5 -0.6 2.7 1 43.0 65.0 74.0 100
12 知力 473 1 60.7 22.0 -0.7 2.6 1 43.0 68.0 77.0 100
13 知力 803 1 59.0 20.0 -0.6 2.6 1 44.0 65.0 74.0 100
1 武力 254 1 57.4 24.6 0.0 1.8 15 36.0 57.5 78.8 100
2 武力 312 1 58.8 21.3 -0.1 2.1 11 41.0 61.0 74.0 100
3 武力 531 1 61.4 17.1 -0.5 3.0 15 52.0 64.0 71.0 100
4 武力 454 1 61.4 20.2 -0.6 2.6 13 49.2 66.0 75.0 100
5 武力 500 1 60.3 22.7 -0.6 2.5 7 44.0 67.0 76.0 100
6 武力 520 1 58.4 20.0 -0.3 2.3 16 42.8 62.5 73.0 100
7 武力 538 1 58.8 20.4 -0.5 2.3 11 45.2 63.0 74.0 98

3.4 要約統計量の視覚化による判断

今回は見るべき項目が多いため要約統計量だけでも数値が非常に多く, 数値の羅列を眺めるだけでは何が読み取れるのか分かりにくい. そこでデータの要約統計量をさらに, グラフで見やすくする必要がある.

3.4.1 作品ごとの要約統計量推移

ここまでで集計した各要約統計量の作品ごとの推移を折れ線グラフで表したのが図3.4である.

df_append <- df_all %>% group_by(title) %>% group_map(~unnest(.x, cols = data) %>% 
    rename_if(names(.) == "カリスマ", function(x) "魅力") %>% select_if(names(.) %in% 
    c("title", "name_id") | map_lgl(., is.numeric)), keep = T) %>% bind_rows %>% 
    select(title, name_id, attend_times, at_first, 身体, 知力, 武力, 
        魅力, 運勢, 義理, 野望, 相性, 政治, 統率, 陸指, 
        水指)

descriptive_status <- df_append %>% group_by(title) %>% select(title, 武力, 
    知力, 魅力, 政治) %>% my_skim() %>% as_tibble() %>% rename_all(~str_remove(.x, 
    "^numeric.")) %>% rename(missings = n_missing) %>% filter(skim_type == 
    "numeric") %>% select(-skim_type, -complete_rate) %>% rename(variable = skim_variable, 
    min = p0, max = p100, skewness = skew, kurtosis = kurto) %>% mutate(title = as.integer(title))

descriptive_status %>% mutate(range = max - min) %>% pivot_longer(-c(variable, 
    title), names_to = "stat", values_to = "value") %>% filter(stat %in% 
    c("range", "mean", "sd", "skewness", "kurtosis")) %>% mutate(stat = factor(stat, 
    levels = c("range", "mean", "sd", "skewness", "kurtosis"))) %>% ggplot(aes(x = title, 
    y = value, group = variable, color = variable)) + geom_line(size = 2) + 
    facet_wrap(~stat, scales = "free_y", ncol = 1, strip.position = "left") + 
    scale_color_colorblind() + theme_document_no_y
シリーズごとの要約統計量の推移

図 3.4: シリーズごとの要約統計量の推移

このグラフを見て特に気になるのは, レンジの変化である. 三國志シリーズのステータスは基本的に1~100の数値だが, 実際には最小値と最大値の幅が作品ごとに異なることがわかった. そのため, 値域を統一するためにシリーズごとにmin-max正規化を行う. 通常のmin-max正規化は0-1の範囲にする正規化処理だが, 今回は見やすさのため, レンジが100になるよう以下 (3.1) のようにさらに100を掛けてで調整している.

\[\begin{aligned} z:= & 100\times\frac{x-\min(x)}{\max(x)-\min(x)}\end{aligned}\tag{3.1}\]

この式で, 主要な能力値を全てシリーズごとに調整して再集計した結果をグラフにする.

scale_max_min <- function(x) {
    (x - min(x))/(max(x) - min(x))
}

df_norm <- df_append %>% mutate(title = factor(title, levels = 1:13)) %>% 
    group_by(title) %>% group_map(~mutate_if(.x, !names(.x) %in% c("attend_times", 
    "at_first") & map_lgl(.x, is.numeric), function(x) scale_max_min(x) * 
    100), keep = T) %>% bind_rows %>% rowwise %>% mutate(total = mean(c(武力, 
    知力, 魅力, 政治, 統率, 水指, 陸指), na.rm = T), total_sd = sd(c(武力, 
    知力, 魅力, 政治, 統率, 水指, 陸指), na.rm = T), total_range = max(c(武力, 
    知力, 魅力, 政治, 統率, 水指, 陸指), na.rm = T) - min(c(武力, 
    知力, 魅力, 政治, 統率, 水指, 陸指), na.rm = T)) %>% ungroup
変数のスケーリングには他に標準化という有名な方法もあるが, 標準化は分散を固定してしまう方法である. 今回のテーマでは作品ごとのばらつきを見ることも重要なので, 標準化では必要な情報が得られない.

調整後の要約統計量が図3.5である. min-max正規化はレンジを1に統一するため, rangeの値は掲載していない. 調整前と比較し, 後期作品ほど標準偏差が減少するような傾向が顕著になった. 逆に平均値は上昇傾向にあるように見える. 歪度も徐々にゼロから離れていることがわかる. 標準偏差の低下という意味では, これらから判断するに, 最近の作品ほど三国志演義で活躍の場面の少ない人物が再評価された結果, どの人物もまんべんなくそこそこの評価がされるようになり, 「没個性化」しつつあるとも読み取れる. しかし, 単純な要約統計量から得られる情報が全てではない. ため, もう少し他の観点からも見ていこう.

正規化後のシリーズごとの要約統計量

図 3.5: 正規化後のシリーズごとの要約統計量

[テクニカル] 複数の折れ線グラフの描き方

今回の折れ線グラフの作例プログラムも少し長いが, 最低限必要なのは ggplot(), aes(), geom_line(), そして facet_wrap() である. 上記のプログラムのうち, 最初の2つのブロックはグラフ描画用にデータを集計しているものである. さらに最後のブロックでも, 最初の3行はデータフレームの中身を編集しているだけである. この3行で作成したデータフレームは以下の表3.2のようになっている.

表 3.2: 折れ線グラフの入力データ
variable title stat value
武力 1 mean 57.3661417
武力 1 sd 24.6314293
武力 1 skewness -0.0038454
武力 1 kurtosis 1.7950797
武力 1 range 85.0000000
武力 10 mean 56.4276923

元のデータフレームでは「平均」「標準偏差」といった項目がそれぞれ別の変数になっていたが, ggplot2では使用する変数を一列にする必要があるため, このように数値を1つの列にまとめたlong形式に整形している36.

今回新たに使うfacet_wrap()は, groupとはさらに別のグループを設定する関数である. ただし, group=と違って画面を分割して描く. 今回は「政治」「知力」「武力」といった異なるステータス値ごとに折れ線グラフを描きたい. しかし, スケールや単位の異なる変数を同じ画面に描画するのは非常に見づらいことが多い.

このデータフレームはやや複雑なので, ここでは例としてもっとシンプルなデータフレームを利用して説明する. 図3.6は, 50~150を推移する変数xと, 0~1を推移するyの折れ線グラフを同時に描いたものである. スケールの小さなyがどう変化しているのか, 全くわからない.

set.seed(42)
tibble(n = 1:50, x = rnorm(n = 50, mean = 10, sd = 1)^2, y = runif(50, 
    0, 1)) %>% pivot_longer(cols = c("x", "y")) %>% ggplot(aes(x = n, y = value, 
    group = name, color = name)) + geom_line(size = 1.5) + scale_color_colorblind() + 
    theme_document_no_y
スケールが一致しない例

図 3.6: スケールが一致しない例

しかし, facet_wrap() によって分割することで見やすくなる(図3.7).

set.seed(42)
tibble(n = 1:50, x = rnorm(n = 50, mean = 10, sd = 1)^2, y = runif(50, 
    0, 1)) %>% pivot_longer(cols = c("x", "y")) %>% ggplot(aes(x = n, y = value, 
    group = name, color = name)) + geom_line(size = 1.5) + facet_wrap(~name, 
    scales = "free_y", ncol = 1) + scale_color_colorblind() + theme_document_no_y
スケールの違う変数ごとに分割して作図

図 3.7: スケールの違う変数ごとに分割して作図

~name は画面を分割する変数である. pivot_longer()で元のデータフレームをlong形式に変換しているため, nameに元の変数名が格納されている. scales="free_y"は, y軸のスケールを分割ごとに変動することを許可している. この設定によって, 図3.7のようにスケールが調整され変化がわかりやすくなっている. ncol=1は, 分割の並べ方である. デフォルトでは横に並べてしまうため, ncol=1 で横1列に制限している.

さらに軸を増やしたい場合は, facet_grid()を使うこともできる.

異なる変数のスケールを調整して無理やり1つの画面に収めたグラフがたまに見られる. グラフの左右に単位の異なる目盛りが付いているのだが, そのようなグラフはえてして見づらく, 誤解を招きやすい.

今回の作例は「ステータス値の種類」と「統計量の種類」という2つの軸が存在する. ステータス値はどの作品でも0~100に設定されているため分割する意義は少ない (むしろ比較のため分割しないほうがよい)が, 平均値や標準偏差といった要約統計量はそれぞれでスケールが全く異なる. よって, 統計量の種類ごとに facet_wrap()で分割している.

3.4.2 人物別に確認する

一旦, 個別の例に拡大して見てみよう. 4名の人物についてシリーズを通してステータスがどう変化しているかを見る. 主要人物は記述が多く, 演義と正史での評価の差異を細かく説明するのが大変である. そこで, 主要人物ではないが, 差異の分かりやすい人物をケーススタディの対象とする.

  • 華雄(カユウ)
    • 正史: 「董卓(トウタク)が派遣した胡軫(コシン)の配下として従軍したが孫堅(ソンケン)軍に討たれた」としか書かれていない(呉書孫堅伝,後漢書董卓伝)
    • 演義: 董卓配下の猛将で, 孫堅を敗走させる. しかし関羽(カンウ)に即敗北する (三国演義第五回)
  • 関興(カンコウ)
  • 曹真(ソウシン)
    • 正史: 魏の将軍として諸葛亮(ショカツリョウ)の北伐に対する防衛を指揮し, 二度退ける(魏書曹真伝)
    • 演義: 北伐では終始諸葛亮に翻弄され, 最期は罵倒され憤死した(三国演義第一百回等)
  • 李通(リツウ)

3.8では, 「演義で活躍の盛られている」代表である華雄, 関興はシリーズを通してあまり変化していない. すくなくとも低下しているようには見えない. 一方で, 「活躍の場を奪われていた」李通, 曹真は徐々に上昇しているように見える.

df_norm %>% filter(name_id %in% c("華雄", "関興", "李通", "曹真")) %>% 
    mutate(name_id = str_split(name_id, "") %>% map_chr(~paste(.x, collapse = "\n"))) %>% 
    select(title, name_id, 武力, 魅力, 知力, 政治) %>% pivot_longer(-c(title, 
    name_id), names_to = "status", values_to = "value") %>% ggplot(aes(x = title, 
    y = value, group = status, color = status, linetype = status)) + geom_line(size = 1) + 
    facet_wrap(~name_id, ncol = 1, strip.position = "left") + scale_color_colorblind() + 
    theme_document_no_y + theme(strip.text.y.left = element_text(angle = 0, 
    size = 18))
4名のシリーズを通した変化

図 3.8: 4名のシリーズを通した変化

ということは, もしこれが全体の傾向にも当てはまるのなら, 三国志演義で活躍が誇張されている人物の評価はそのままで, 同時に正史の見直しによって従来ステータスの低い武将の値が底上げされれば, 「没個性化」になりうる. 全体の傾向ならば分布にも現れるはずである. そこで, シリーズの作品ほとんどで存在するステータス項目である, 「武力」「知力」「魅力」37「政治」を確認してみる38. 分布の形状と言えばヒストグラムだが, シリーズごとの特徴をうまく表したい. 分布を表すものとして, 箱ひげ図(box plot)があるが, ここでは geom_violin() を使ってバイオリン図を作図した(図3.9).

df_norm %>% rename(主要値平均 = total) %>% select(title, 武力, 知力, 
    魅力, 政治, 主要値平均) %>% pivot_longer(cols = -title) %>% 
    ggplot(aes(x = title, y = value, fill = as.numeric(title))) + geom_violin(draw_quantiles = c(0.25, 
    0.5, 0.75)) + scale_fill_continuous_tableau(guide = F) + facet_wrap(~name, 
    ncol = 1, strip.position = "left") + theme_document_no_y + labs(x = "タイトル")
シリーズごとの主要能力値の分布

図 3.9: シリーズごとの主要能力値の分布

バイオリン図で書くことで, シリーズごとに各ステータスの分布形状が変動していることがわかった. そして興味深いことに, 最近の作品ほど分布が二極化している. 平均より大きな値域で大きめの峰が, 平均より小さい値域でも小さい峰が発生している39. 図3.5の歪度の推移から, なんらかの形で分布が歪んでいることは予想できたが, 具体的な形状は実際にグラフにしないと分からない.

「能力値平均」の項目では小さい方の峰は存在しない変わりに, 50より少し上のあたりに峰のピークがあり, 逆に50未満の裾野はかなり薄くなっている. この異なる傾向から以下の2つのことが読み取れる.

  1. 最近の作品ほど, ステータスの平均が50より少し上になる人物ばかりになっている.
  2. しかし同時に, 特定の能力値だけが低い or 高い設定の人物が増えている.

なお, 箱ひげ図は geom_violin()geom_boxplot()に置き換えるだけで作成できる(図3.10).

df_norm %>% rename(主要値平均 = total) %>% select(title, 武力, 知力, 
    魅力, 政治, 主要値平均) %>% pivot_longer(cols = -title) %>% 
    ggplot(aes(x = title, y = value, fill = as.numeric(title))) + geom_boxplot() + 
    scale_fill_continuous_tableau(guide = F) + facet_wrap(~name, ncol = 1, 
    strip.position = "left") + theme_document_no_y + labs(x = "タイトル")
箱ひげ図で表した場合

図 3.10: 箱ひげ図で表した場合

[テクニカル] 異なるグラフを1つにまとめる

もっと情報を必要最小限のものに縮約できないだろうか? 図3.9の「主要能力平均値」さえ提示すれば, 最近の作品ほど50より少し上野あたりに能力値平均のボリュームゾーンが存在し, それ以外の頻度が減っていることを示せる. 後は作品ごとの標準偏差, 尖度, 歪度の傾向を見せれば主張したいことに必要最小限の情報を提示できるので, そのような複合的な図を作成できないだろうか? もちろん,それぞれの図を個別に作成するだけのほうが簡単であるが, 作成できれば資料作成のときに何かと便利である.

しかし, 既に紹介した facet_wrap()/facet_grid()は, 同じグラフを分割するだけで, 異なるグラフに分割する機能はない. 異なるグラフを1つの画像にまとめるには, patchworkパッケージを利用すると簡単である.

geom_point()geom_line()が描く点や線は入力データの点に対して1対1に対応している. しかし今は集計した結果を点や線で描画したい. このような場合, 事前にデータフレームに集計処理を加えたものを与えることもできるが, stat_summary()を使うことでggplot内で集計した結果を表示することもできる.

g_concentrate <- ggplot(df_norm, aes(x = title, y = total, fill = as.integer(title))) + 
    geom_violin(draw_quantiles = c(0.25, 0.5, 0.75)) + scale_fill_continuous_tableau(guide = F, 
    "Classic Blue") + labs(title = "主要能力値平均") + theme_document_no_y + 
    theme(axis.title.x = element_blank())
g_concentrate_sd <- ggplot(df_norm, aes(x = as.integer(title), y = total)) + 
    stat_summary(fun = sd, geom = "line", size = 2) + stat_summary(fun = sd, 
    geom = "point", size = 4) + labs(title = "標準偏差") + theme_document_no_y + 
    theme(axis.title = element_blank())
g_concentrate_kurto <- ggplot(df_norm, aes(x = as.integer(title), y = total)) + 
    stat_summary(fun = kurtosis, geom = "line", size = 2) + stat_summary(fun = kurtosis, 
    geom = "point", size = 4) + labs(title = "尖度") + theme_document_no_y + 
    theme(axis.title.x = element_text(size = 15))

g_concentrate_sd は標準偏差の傾向を表すグラフである. stat_summary(fun = sd)は入力データのy軸に指定した変数に対してsd()つまり標準偏差を計算する関数を適用するということである. g_concentrate_kurtoは同様に尖度を計算したものである.

この方法はグラフを描くたびに集計処理をする必要があるので, サイズの大きなデータを扱うときは非効率であることに注意する.

最後に作成した3種類のグラフを / で垂直に連結する. これは patchworkパッケージ40による機能である. これまでggplot2で作成した複数のグラフを連結して1つの画像にするにはgridExtra が必要だった41. しかしこれは構文がやや複雑だ. そもそも複数のグラフを結合すると言っても10も20も連結したいときは稀である. 手軽にグラフを連結したい42場合はpatchworkパッケージのほうが向いている. このパッケージは2年ほど前から存在したが, 最近CRANに登録された.

以上の方法で作成した画像が図3.11である.

(g_concentrate/g_concentrate_sd/g_concentrate_kurto) + labs(x = "タイトル")
シリーズごとの能力値分布の変遷

図 3.11: シリーズごとの能力値分布の変遷

3.5 データの視覚化からわかったこと

今回取得できたもの以外にも, 各作品では登場人物に特徴を与える能力値の項目がいくつも存在する. 全ての能力値の項目を使って検証できていないという問題はあるが, 「最近の作品ほど能力値の多様性が乏しくなりつつある 」という仮説を裏付ける結果が得られた. 一方で, 個別のステータスの分布を見れば, 「二極化」が発生していることがわかる. つまりより正確には,「武力」や「知力」など一芸に特化した人物が増え, かつそのステータス値の多様性がなくなりつつある, と言うべきだろう.


  1. 今回のデータは, 各人物が三国志演義と正史三国志いずれに登場しているかをはっきり示す情報を含んでいない. 万全を期すならば詳細に調査すべきだが, 名寄せ処理と同様の理由で今回は割愛した.↩︎

  2. Pythonモジュールの例だが, 私がデータ分析で求めるグラフ作成ツールの要件について過去に書いたことがある. https://qiita.com/s_katagiri/items/26763fd39f3dd9756809↩︎

  3. 3D円グラフを使うのはやめよう | Okumura’s Blog Wonder Graph Generator, 森藤 and あんちべ (2014)↩︎

  4. ユーレイ棒グラフ? | Okumura’s Blog - 奥村研究室↩︎

  5. Tufte の名前は日本語文献でもちらほらみられるようになったが, そもそも著作は未だに翻訳されていない. だれかやりましょう?↩︎

  6. ただし日本語の情報が少ない. 私の知る限り『niszet氏のスライド』の作者が唯一言及しているのみで, しかも現在はさらに仕様が変わっている.↩︎

  7. 発表の反響を踏まえての追記: skimr以外にも見やすい表を提供するパッケージはある. 同じくniszet氏のブログに『(R) summarytoolsパッケージ、便利そう…』という記事がある. 私がskimrを挙げた理由として, プレーンテキストとして扱えるという点も大きいことを補足せねばならない. というのも, 私が資料を作成するときはLaTeXかRMarkdownなので, これらのフォーマットでの表に変換しやすい形で出力しやすいskimrを薦めた. summarytoolsもまたskimrと似た用途のパッケージである. ただし, summarytoolsはhtml出力を念頭に置いていることと, group_byによるグループ別の要約統計量の出力が見づらいという問題がある.↩︎

  8. 変数ごとに別々にgeom_line()で描画することもできるが, このやり方は色の割り当てまで手動指定する必要がある. このような使い方はggplot2のアドバンテージを失うことになるためなるべく避けたい.↩︎

  9. 三國志1のみ「カリスマ」という名称になっている↩︎

  10. 1~100の数値で表現されるステータス項目はシリーズごとに異なる. 武力, 魅力,知力, 政治, 統率, 陸指 (陸戦指揮), 水指 (水戦指揮) などがある.↩︎

  11. 初期の作品である三國志3にも瓢箪めいたくびれがある. これは「陸指」「水指」という2つのパラメータで能力の差別化がなされているからである. この2項目を除外して平均を取ると, 多峰性は消える.↩︎

  12. 公式: https://patchwork.data-imaginist.com/ 日本語しか読めないならばこのページが良い. https://qiita.com/nozma/items/4512623bea296ccb74ba↩︎

  13. gridExtraの使い方を日本語で説明したページは例えば次のようなものがある.: https://id.fnshr.info/2016/10/10/gridextra/↩︎

  14. 手軽でない場合というのもあまり想定しにくいが, 例えば散布図の周りにヒストグラムまたは推定した密度関数を付け足したグラフをたまに見かける. 私はこのようなグラフが必須という状況に詳しくない(例えば紙面が限られる学会のポスター発表?)が, こういう複雑なものを描きたいのであれば, cowplotggExtraを使うという選択肢もある. しかし, cowplotの作例, patchworkの作例, ggExtraの作例 などを見る限り, 煩雑さでは大差ないようである.↩︎