suppressPackageStartupMessages({
library(ggplot2)
library(duckdb)
library(arules)
library(arulesViz)
library(ca)
})<- duckdb::duckdb()
drv <- duckdb::dbConnect(drv, dbdir = "tutorial_jp/kokoro.duckdb", read_only = TRUE)
con
<-
tbl ::read_xls("tutorial_jp/kokoro.xls",
readxlcol_names = c("text", "section", "chapter", "label"),
skip = 1
|>
) ::mutate(
dplyrdoc_id = factor(dplyr::row_number()),
::across(where(is.character), ~ audubon::strj_normalize(.))
dplyr|>
) ::filter(!gibasa::is_blank(text)) |>
dplyr::relocate(doc_id, text, section, label, chapter) dplyr
Appendix C — 抽出語メニュー2
C.1 関連語検索(A.5.6)
C.1.1 関連語のリスト
「確率差」や「確率比」については、いちおう計算はできた気がしますが、あっているのかよくわからないです。また、このやり方はそれなりの数の共起について計算をしなければならず、共起行列が大きくなると大変そうなため、あまりおすすめしません。
<-
dfm ::tbl(con, "tokens") |>
dplyr::filter(
dplyr== "[1]上_先生と私",
section %in% c(
pos "名詞", # "名詞B", "名詞C",
"地名", "人名", "組織名", "固有名詞",
"動詞", "未知語", "タグ"
)|>
) ::mutate(
dplyrtoken = dplyr::if_else(is.na(original), token, original),
token = paste(token, pos, sep = "/")
|>
) ::count(doc_id, token) |>
dplyr::collect() |>
dplyr::cast_dfm(doc_id, token, n) |>
tidytext::dfm_weight(scheme = "boolean")
quanteda
<- dfm |>
dat ::fcm() |>
quanteda::tidy() |>
tidytext::rename(target = document, co_occur = count) |>
dplyr::as_function(~ {
rlang<- quanteda::colSums(dfm)
col_sums ::reframe(.,
dplyrterm = term,
target_occur = col_sums[target],
term_occur = col_sums[term],
co_occur = co_occur,
.by = target
)|>
})() ::mutate(
dplyrp_x = target_occur / quanteda::ndoc(dfm),
p_y = term_occur / quanteda::ndoc(dfm),
p_xy = (co_occur / quanteda::ndoc(dfm)) / p_x,
differential = p_xy - p_y, # 確率差
lift = p_xy / p_y, # 確率比(リフト),
jaccard = co_occur / (target_occur + term_occur - co_occur),
dice = 2 * co_occur / (target_occur + term_occur)
|>
) ::select(target, term, differential, lift, jaccard, dice)
dplyr
dat#> # A tibble: 26,820 × 6
#> target term differential lift jaccard dice
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 先生/名詞 知り合い/名詞 0.00790 2.10 0.0151 0.0297
#> 2 先生/名詞 海水浴/名詞 0.000176 1.05 0.00376 0.00749
#> 3 先生/名詞 帰る/動詞 0.0150 1.20 0.0848 0.156
#> 4 先生/名詞 断る/動詞 0.000529 1.05 0.0112 0.0221
#> 5 先生/名詞 一人/タグ 0.0129 1.40 0.0443 0.0848
#> 6 先生/名詞 大分/地名 -0.00145 0.839 0.00746 0.0148
#> 7 先生/名詞 鎌倉/地名 0.00430 1.40 0.0150 0.0295
#> 8 先生/名詞 探す/動詞 -0.00162 0.699 0.00375 0.00746
#> 9 先生/名詞 建てる/動詞 0.000176 1.05 0.00376 0.00749
#> 10 先生/名詞 通り抜ける/動詞 -0.00342 0.525 0.00373 0.00743
#> # ℹ 26,810 more rows
C.1.2 共起ネットワーク
「先生/名詞」と関連の強そうな語の共起を図示した例です。
「先生/名詞」と共起している語のうち、出現回数が上位20位以内である語がtarget
である共起を抽出したうえで、それらのなかからJaccard係数が大きい順に75個だけ残しています。「先生/名詞」という語そのものは図に含めていません。
|>
dat ::inner_join(
dplyr::filter(dat, target == "先生/名詞") |> dplyr::select(term),
dplyrby = dplyr::join_by(target == term)
|>
) ::filter(target %in% names(quanteda::topfeatures(dfm, 20))) |>
dplyr::slice_max(jaccard, n = 75) |>
dplyr::as_tbl_graph(directed = FALSE) |>
tidygraph::to_minimum_spanning_tree() |>
tidygraph::pluck("mst") |>
purrr::mutate(
dplyrcommunity = factor(tidygraph::group_leading_eigen())
|>
) ::ggraph(layout = "fr") +
ggraph::geom_edge_link(aes(width = sqrt(lift), alpha = jaccard)) +
ggraph::geom_node_point(aes(colour = community), show.legend = FALSE) +
ggraph::geom_node_text(aes(label = name, colour = community), repel = TRUE, show.legend = FALSE) +
ggraph::theme_graph() ggraph
C.1.3 アソシエーション分析🍳
英語だとこのメニューの名前は「Word Association」となっているので、ふつうにアソシエーション分析すればいいと思いました。
arulesのtransactions
オブジェクトをつくるには、quantedaのfcm
オブジェクトから変換すればOKです(arulesをアタッチしている必要があります)。
library(arules)
library(arulesViz)
<-
dat ::tbl(con, "tokens") |>
dplyr::filter(
dplyr%in% c(
pos "名詞", # "名詞B", "名詞C",
"地名", "人名", "組織名", "固有名詞",
"動詞", "未知語", "タグ"
)|>
) ::mutate(
dplyrtoken = dplyr::if_else(is.na(original), token, original),
token = paste(token, pos, sep = "/")
|>
) ::count(doc_id, token) |>
dplyr::collect() |>
dplyr::cast_dfm(doc_id, token, n) |>
tidytext::dfm_weight(scheme = "boolean") |>
quanteda::fcm() |>
quantedaas("nMatrix") |>
as("transactions")
arules::apriori()
でアソシエーションルールを抽出します。
<-
rules ::apriori(
arules
dat,parameter = list(
support = 0.075,
confidence = 0.8,
minlen = 2,
maxlen = 2, # LHS+RHSの長さ。変えないほうがよい
maxtime = 5
),control = list(verbose = FALSE)
)
この形式のオブジェクトはas(rules, "data.frame")
のようにしてデータフレームに変換できます。tibble
にしたい場合には次のようにすればよいです。
as(rules, "data.frame") |>
::mutate(across(where(is.numeric), ~ signif(., digits = 3))) |>
dplyr::separate_wider_delim(rules, delim = " => ", names = c("lhs", "rhs")) |>
tidyr::arrange(desc(lift))
dplyr#> # A tibble: 49 × 7
#> lhs rhs support confidence coverage lift count
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 {黙る/動詞} {聞く/動詞} 0.102 0.829 0.123 2.05 301
#> 2 {付ける/動詞} {聞く/動詞} 0.11 0.822 0.133 2.03 323
#> 3 {過ぎる/動詞} {自分/名詞} 0.0887 0.906 0.0978 1.84 261
#> 4 {感じ/名詞} {自分/名詞} 0.0938 0.879 0.107 1.79 276
#> 5 {分る/動詞} {出る/動詞} 0.105 0.812 0.13 1.78 310
#> 6 {打ち明ける/動詞} {自分/名詞} 0.0887 0.861 0.103 1.75 261
#> 7 {人間/名詞} {自分/名詞} 0.17 0.858 0.198 1.75 501
#> 8 {繰り返す/動詞} {自分/名詞} 0.0917 0.852 0.108 1.73 270
#> 9 {信じる/動詞} {思う/動詞} 0.0781 0.927 0.0842 1.71 230
#> 10 {疑う/動詞} {自分/名詞} 0.0856 0.824 0.104 1.68 252
#> # ℹ 39 more rows
C.1.4 散布図🍳
plot(rules, engine = "html")
#> To reduce overplotting, jitter is added! Use jitter = 0 to prevent jitter.
C.1.5 バルーンプロット🍳
plot(rules, method = "grouped", engine = "html")
C.1.6 ネットワーク図🍳
plot(rules, method = "graph", engine = "html")
C.2 対応分析(A.5.7)
C.2.1 コレスポンデンス分析
段落(doc_id
)内の頻度で語彙を削ってから部(section
)ごとに集計するために、ややめんどうなことをしています。
<-
dfm ::tbl(con, "tokens") |>
dplyr::filter(
dplyr%in% c(
pos "名詞", "名詞B", "名詞C",
"地名", "人名", "組織名", "固有名詞",
"動詞", "未知語", "タグ"
)|>
) ::mutate(
dplyrtoken = dplyr::if_else(is.na(original), token, original),
token = paste(token, pos, sep = "/")
|>
) ::count(doc_id, token) |>
dplyr::collect() |>
dplyr::cast_dfm(doc_id, token, n) |>
tidytext::dfm_trim(
quantedamin_termfreq = 75,
termfreq_type = "rank",
min_docfreq = 30,
docfreq_type = "count"
)
こうしてdoc_id
ごとに集計したdfm
オブジェクトを一度tidytext::tidy()
して3つ組のデータフレームに戻し、section
のラベルを結合します。このデータフレームをもう一度tidytext::cast_dfm()
で疎行列に変換して、quanteda.textmodels::textmodel_ca()
を使って対応分析にかけます。
<- dfm |>
ca_fit ::tidy() |>
tidytext::left_join(
dplyr::select(tbl, doc_id, section),
dplyrby = dplyr::join_by(document == doc_id)
|>
) ::cast_dfm(section, term, count) |>
tidytext::textmodel_ca(nd = 2, sparse = TRUE) quanteda.textmodels
この関数は疎行列に対して計算をおこなえるため、比較的大きな行列を渡しても大丈夫そうです。
C.2.2 バイプロット
caパッケージを読み込んでいるとplot()
でバイプロットを描けます。factoextra::fviz_ca_biplot()
でも描けますが、見た目はplot()
のとあまり変わらないです。
library(ca)
<- plot(ca_fit) dat
C.2.3 バイプロット(バブルプロット)
ggplot2でバイプロットを描画するには、たとえば次のようにします。ggrepel::geom_text_repel()
でラベルを出す語彙の選択の仕方は、もうすこし工夫したほうがよいかもしれないです。
なお、このコードはCorrespondence Analysis visualization using ggplot | R-bloggersを参考にしました。
<- dfm |>
tf ::tidy() |>
tidytext::left_join(
dplyr::select(tbl, doc_id, section),
dplyrby = dplyr::join_by(document == doc_id)
|>
) ::summarise(tf = sum(count), .by = term) |>
dplyr::pull(tf, term)
dplyr
# modified from https://www.r-bloggers.com/2019/08/correspondence-analysis-visualization-using-ggplot/
<- function(ca.plot.obj, row.lab = "Rows", col.lab = "Columns") {
make_ca_plot_df ::tibble(
tibbleLabel = c(
rownames(ca.plot.obj$rows),
rownames(ca.plot.obj$cols)
),Dim1 = c(
$rows[, 1],
ca.plot.obj$cols[, 1]
ca.plot.obj
),Dim2 = c(
$rows[, 2],
ca.plot.obj$cols[, 2]
ca.plot.obj
),Variable = c(
rep(row.lab, nrow(ca.plot.obj$rows)),
rep(col.lab, nrow(ca.plot.obj$cols))
)
)
}<- dat |>
dat make_ca_plot_df(row.lab = "Construction", col.lab = "Medium") |>
::mutate(
dplyrSize = dplyr::if_else(Variable == "Construction", mean(tf), tf[Label])
)# 非ASCII文字のラベルに対してwarningを出さないようにする
suppressWarnings({
<- summary(ca_fit)
ca_sum <- ca_sum$scree[, "values2"]
dim_var_percs
})
|>
dat ggplot(aes(x = Dim1, y = Dim2, col = Variable, label = Label)) +
geom_vline(xintercept = 0, lty = "dashed", alpha = .5) +
geom_hline(yintercept = 0, lty = "dashed", alpha = .5) +
geom_jitter(aes(size = Size), alpha = .3, show.legend = FALSE) +
::geom_label_repel(
ggrepeldata = \(x) dplyr::filter(x, Variable == "Construction"),
show.legend = FALSE
+
) ::geom_text_repel(
ggrepeldata = \(x) dplyr::filter(x, Variable == "Medium", sqrt(Dim1^2 + Dim2^2) > 0.25),
show.legend = FALSE
+
) scale_x_continuous(
limits = range(dat$Dim1) +
c(diff(range(dat$Dim1)) * -0.2, diff(range(dat$Dim1)) * 0.2)
+
) scale_y_continuous(
limits = range(dat$Dim2) +
c(diff(range(dat$Dim2)) * -0.2, diff(range(dat$Dim2)) * 0.2)
+
) scale_size_area(max_size = 16) +
labs(
x = paste0("Dimension 1 (", signif(dim_var_percs[1], 3), "%)"),
y = paste0("Dimension 2 (", signif(dim_var_percs[2], 3), "%)")
+
) theme_classic()
#> Warning: ggrepel: 22 unlabeled data points (too many overlaps). Consider
#> increasing max.overlaps
C.3 多次元尺度構成法(A.5.8)
C.3.1 MDS・バブルプロット
MASS::isoMDS()
よりMASS::sammon()
のほうがたぶん見やすいです。
<- dfm |>
simil ::dfm_weight(scheme = "boolean") |>
quanteda::simil(margin = 2, method = "jaccard")
proxyC
<- MASS::sammon(1 - simil, k = 2) |>
dat ::pluck("points")
purrr#> Initial stress : 0.62279
#> stress after 0 iters: 0.62279
<- dat |>
dat ::as_tibble(
dplyrrownames = "label",
.name_repair = ~ c("Dim1", "Dim2")
|>
) ::mutate(
dplyrsize = tf[label],
clust = (hclust(
::dist(dat, method = "euclidean") |> as.dist(),
proxyCmethod = "ward.D2"
|> cutree(k = 6))[label]
)
)
|>
dat ggplot(aes(x = Dim1, y = Dim2, label = label, col = factor(clust))) +
geom_point(aes(size = size), alpha = .3, show.legend = FALSE) +
::geom_text_repel(show.legend = FALSE) +
ggrepelscale_size_area(max_size = 16) +
theme_classic()
#> Warning: ggrepel: 22 unlabeled data points (too many overlaps). Consider
#> increasing max.overlaps
::dbDisconnect(con)
duckdb::duckdb_shutdown(drv)
duckdb
::session_info(info = "packages")
sessioninfo#> ═ Session info ═══════════════════════════════════════════════════════════════
#> ─ Packages ───────────────────────────────────────────────────────────────────
#> package * version date (UTC) lib source
#> arules * 1.7-11 2025-05-29 [1] RSPM (R 4.5.0)
#> arulesViz * 1.5.4 2025-08-21 [1] RSPM (R 4.5.0)
#> audubon 0.5.2 2024-04-27 [1] RSPM (R 4.5.0)
#> blob 1.2.4 2023-03-17 [1] RSPM
#> ca * 0.71.1 2020-01-24 [1] RSPM (R 4.5.0)
#> cachem 1.1.0 2024-05-16 [1] RSPM
#> cellranger 1.1.0 2016-07-27 [1] RSPM (R 4.5.0)
#> cli 3.6.5 2025-04-23 [1] RSPM
#> codetools 0.2-20 2024-03-31 [2] CRAN (R 4.5.1)
#> crosstalk 1.2.2 2025-08-26 [1] RSPM (R 4.5.0)
#> curl 7.0.0 2025-08-19 [1] RSPM
#> data.table 1.17.8 2025-07-10 [1] RSPM (R 4.5.0)
#> DBI * 1.2.3 2024-06-02 [1] RSPM (R 4.5.0)
#> dbplyr 2.5.1 2025-09-10 [1] RSPM
#> digest 0.6.37 2024-08-19 [1] RSPM
#> dplyr 1.1.4 2023-11-17 [1] RSPM (R 4.5.0)
#> duckdb * 1.4.0 2025-09-18 [1] RSPM (R 4.5.0)
#> evaluate 1.0.5 2025-08-27 [1] RSPM
#> farver 2.1.2 2024-05-13 [1] RSPM (R 4.5.0)
#> fastmap 1.2.0 2024-05-15 [1] RSPM
#> fastmatch 1.1-6 2024-12-23 [1] RSPM (R 4.5.0)
#> foreach 1.5.2 2022-02-02 [1] RSPM (R 4.5.0)
#> generics 0.1.4 2025-05-09 [1] RSPM (R 4.5.0)
#> ggforce 0.5.0 2025-06-18 [1] RSPM (R 4.5.0)
#> ggplot2 * 4.0.0 2025-09-11 [1] RSPM (R 4.5.0)
#> ggraph 2.2.2 2025-08-24 [1] RSPM (R 4.5.0)
#> ggrepel 0.9.6 2024-09-07 [1] RSPM (R 4.5.0)
#> gibasa 1.1.2 2025-02-16 [1] RSPM (R 4.5.0)
#> glmnet 4.1-10 2025-07-17 [1] RSPM (R 4.5.0)
#> glue 1.8.0 2024-09-30 [1] RSPM
#> graphlayouts 1.2.2 2025-01-23 [1] RSPM (R 4.5.0)
#> gridExtra 2.3 2017-09-09 [1] RSPM (R 4.5.0)
#> gtable 0.3.6 2024-10-25 [1] RSPM (R 4.5.0)
#> htmltools 0.5.8.1 2024-04-04 [1] RSPM
#> htmlwidgets 1.6.4 2023-12-06 [1] RSPM
#> httr 1.4.7 2023-08-15 [1] RSPM (R 4.5.0)
#> igraph 2.1.4 2025-01-23 [1] RSPM (R 4.5.0)
#> iterators 1.0.14 2022-02-05 [1] RSPM (R 4.5.0)
#> janeaustenr 1.0.0 2022-08-26 [1] RSPM (R 4.5.0)
#> jsonlite 2.0.0 2025-03-27 [1] RSPM
#> knitr 1.50 2025-03-16 [1] RSPM
#> labeling 0.4.3 2023-08-29 [1] RSPM (R 4.5.0)
#> lattice 0.22-7 2025-04-02 [2] CRAN (R 4.5.1)
#> lazyeval 0.2.2 2019-03-15 [1] RSPM
#> lifecycle 1.0.4 2023-11-07 [1] RSPM
#> magrittr 2.0.4 2025-09-12 [1] RSPM
#> MASS 7.3-65 2025-02-28 [2] CRAN (R 4.5.1)
#> Matrix * 1.7-3 2025-03-11 [2] CRAN (R 4.5.1)
#> memoise 2.0.1 2021-11-26 [1] RSPM
#> pillar 1.11.1 2025-09-17 [1] RSPM
#> pkgconfig 2.0.3 2019-09-22 [1] RSPM
#> plotly 4.11.0 2025-06-19 [1] RSPM (R 4.5.0)
#> polyclip 1.10-7 2024-07-23 [1] RSPM (R 4.5.0)
#> proxyC 0.5.2 2025-04-25 [1] RSPM (R 4.5.0)
#> purrr 1.1.0 2025-07-10 [1] RSPM
#> quanteda 4.3.1 2025-07-10 [1] RSPM (R 4.5.0)
#> quanteda.textmodels 0.9.10 2025-02-10 [1] RSPM (R 4.5.0)
#> R.cache 0.17.0 2025-05-02 [1] RSPM
#> R.methodsS3 1.8.2 2022-06-13 [1] RSPM
#> R.oo 1.27.1 2025-05-02 [1] RSPM
#> R.utils 2.13.0 2025-02-24 [1] RSPM
#> R6 2.6.1 2025-02-15 [1] RSPM
#> RColorBrewer 1.1-3 2022-04-03 [1] RSPM (R 4.5.0)
#> Rcpp 1.1.0 2025-07-02 [1] RSPM
#> RcppParallel 5.1.11-1 2025-08-27 [1] RSPM (R 4.5.0)
#> readxl 1.4.5 2025-03-07 [1] RSPM (R 4.5.0)
#> registry 0.5-1 2019-03-05 [1] RSPM (R 4.5.0)
#> rlang 1.1.6 2025-04-11 [1] RSPM
#> rmarkdown 2.30 2025-09-28 [1] RSPM (R 4.5.0)
#> RSpectra 0.16-2 2024-07-18 [1] RSPM (R 4.5.0)
#> S7 0.2.0 2024-11-07 [1] RSPM (R 4.5.0)
#> scales 1.4.0 2025-04-24 [1] RSPM (R 4.5.0)
#> seriation 1.5.8 2025-08-20 [1] RSPM (R 4.5.0)
#> sessioninfo 1.2.3 2025-02-05 [1] RSPM
#> shape 1.4.6.1 2024-02-23 [1] RSPM (R 4.5.0)
#> SnowballC 0.7.1 2023-04-25 [1] RSPM (R 4.5.0)
#> stopwords 2.3 2021-10-28 [1] RSPM (R 4.5.0)
#> stringi 1.8.7 2025-03-27 [1] RSPM
#> stringr 1.5.2 2025-09-08 [1] RSPM
#> styler 1.10.3 2024-04-07 [1] RSPM
#> survival 3.8-3 2024-12-17 [2] CRAN (R 4.5.1)
#> tibble 3.3.0 2025-06-08 [1] RSPM
#> tidygraph 1.3.1 2024-01-30 [1] RSPM (R 4.5.0)
#> tidyr 1.3.1 2024-01-24 [1] RSPM (R 4.5.0)
#> tidyselect 1.2.1 2024-03-11 [1] RSPM (R 4.5.0)
#> tidytext 0.4.3 2025-07-25 [1] RSPM (R 4.5.0)
#> tokenizers 0.3.0 2022-12-22 [1] RSPM (R 4.5.0)
#> TSP 1.2-5 2025-05-27 [1] RSPM (R 4.5.0)
#> tweenr 2.0.3 2024-02-26 [1] RSPM (R 4.5.0)
#> utf8 1.2.6 2025-06-08 [1] RSPM
#> V8 8.0.0 2025-09-27 [1] RSPM (R 4.5.0)
#> vctrs 0.6.5 2023-12-01 [1] RSPM
#> viridis 0.6.5 2024-01-29 [1] RSPM (R 4.5.0)
#> viridisLite 0.4.2 2023-05-02 [1] RSPM (R 4.5.0)
#> visNetwork 2.1.4 2025-09-04 [1] RSPM (R 4.5.0)
#> withr 3.0.2 2024-10-28 [1] RSPM
#> xfun 0.53 2025-08-19 [1] RSPM
#> yaml 2.3.10 2024-07-26 [1] RSPM
#>
#> [1] /usr/local/lib/R/site-library
#> [2] /usr/local/lib/R/library
#> * ── Packages attached to the search path.
#>
#> ──────────────────────────────────────────────────────────────────────────────