3  抽出語メニュー2

Code
suppressPackageStartupMessages({
  library(ggplot2)
  library(duckdb)
  library(arules)
  library(arulesViz)
  library(ca)
})
drv <- duckdb::duckdb()
con <- duckdb::dbConnect(drv, dbdir = "tutorial_jp/kokoro.duckdb", read_only = TRUE)

tbl <-
  readxl::read_xls("tutorial_jp/kokoro.xls",
    col_names = c("text", "section", "chapter", "label"),
    skip = 1
  ) |>
  dplyr::mutate(
    doc_id = factor(dplyr::row_number()),
    dplyr::across(where(is.character), ~ audubon::strj_normalize(.))
  ) |>
  dplyr::filter(!gibasa::is_blank(text)) |>
  dplyr::relocate(doc_id, text, section, label, chapter)

3.1 関連語検索(A.5.6)

3.1.1 関連語のリスト

「確率差」や「確率比」については、いちおう計算はできた気がするが、あっているのかよくわからない。また、このやり方はそれなりの数の共起について計算をしなければならず、共起行列が大きくなると大変そう。

Code
dfm <-
  dplyr::tbl(con, "tokens") |>
  dplyr::filter(
    section == "[1]上_先生と私",
    pos %in% c(
      "名詞", # "名詞B", "名詞C",
      "地名", "人名", "組織名", "固有名詞",
      "動詞", "未知語", "タグ"
    )
  ) |>
  dplyr::mutate(
    token = dplyr::if_else(is.na(original), token, original),
    token = paste(token, pos, sep = "/")
  ) |>
  dplyr::count(doc_id, token) |>
  dplyr::collect() |>
  tidytext::cast_dfm(doc_id, token, n) |>
  quanteda::dfm_weight(scheme = "boolean")

dat <- dfm |>
  quanteda::fcm() |>
  tidytext::tidy() |>
  dplyr::rename(target = document, co_occur = count) |>
  rlang::as_function(~ {
    col_sums <- quanteda::colSums(dfm)
    dplyr::reframe(.,
      term = term,
      target_occur = col_sums[target],
      term_occur = col_sums[term],
      co_occur = co_occur,
      .by = target
    )
  })() |>
  dplyr::mutate(
    p_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)
  ) |>
  dplyr::select(target, term, differential, lift, jaccard, dice)

dat
#> # A tibble: 26,820 × 6
#>    target    term        differential   lift jaccard   dice
#>    <chr>     <chr>              <dbl>  <dbl>   <dbl>  <dbl>
#>  1 書く/動詞 心持/名詞         0.0553  3.56   0.0417 0.08  
#>  2 書く/動詞 建てる/動詞       0.0733 21.4    0.0714 0.133 
#>  3 書く/動詞 地位/名詞         0.0733 21.4    0.0714 0.133 
#>  4 書く/動詞 来る/動詞         0.0819  2.14   0.0392 0.0755
#>  5 書く/動詞 一言/名詞         0.0733 21.4    0.0714 0.133 
#>  6 書く/動詞 見る/動詞         0.121   2.10   0.0423 0.0811
#>  7 書く/動詞 思う/動詞         0.182   2.44   0.0506 0.0964
#>  8 書く/動詞 出る/動詞        -0.0166  0.822  0.0156 0.0308
#>  9 書く/動詞 先生/名詞         0.0618  1.13   0.0258 0.0504
#> 10 書く/動詞 帰る/動詞         0.309   5.09   0.1    0.182 
#> # ℹ 26,810 more rows

3.1.2 共起ネットワーク

「先生/名詞」と関連の強そうな語の共起を図示した例。

「先生/名詞」と共起している語のうち、出現回数が上位20位以内である語がtargetである共起を抽出したうえで、それらのなかからJaccard係数が大きい順に75個だけ残している。「先生/名詞」という語そのものは図に含めていない。

Code
dat |>
  dplyr::inner_join(
    dplyr::filter(dat, target == "先生/名詞") |> dplyr::select(term),
    by = dplyr::join_by(target == term)
  ) |>
  dplyr::filter(target %in% names(quanteda::topfeatures(dfm, 20))) |>
  dplyr::slice_max(jaccard, n = 75) |>
  tidygraph::as_tbl_graph(directed = FALSE) |>
  tidygraph::to_minimum_spanning_tree() |>
  purrr::pluck("mst") |>
  dplyr::mutate(
    community = factor(tidygraph::group_leading_eigen())
  ) |>
  ggraph::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()

3.1.3 アソシエーション分析🍳

英語だとこのメニューの名前は「Word Association」となっているので、ふつうにアソシエーション分析すればいいと思った。

arulesのtransactionsオブジェクトをつくるには、quantedaのfcmオブジェクトから変換すればよい(arulesをアタッチしている必要がある)。

Code
library(arules)
library(arulesViz)

dat <-
  dplyr::tbl(con, "tokens") |>
  dplyr::filter(
    pos %in% c(
      "名詞", # "名詞B", "名詞C",
      "地名", "人名", "組織名", "固有名詞",
      "動詞", "未知語", "タグ"
    )
  ) |>
  dplyr::mutate(
    token = dplyr::if_else(is.na(original), token, original),
    token = paste(token, pos, sep = "/")
  ) |>
  dplyr::count(doc_id, token) |>
  dplyr::collect() |>
  tidytext::cast_dfm(doc_id, token, n) |>
  quanteda::dfm_weight(scheme = "boolean") |>
  quanteda::fcm() |>
  as("nMatrix") |>
  as("transactions")

arules::apriori()でアソシエーションルールを抽出する。

Code
rules <-
  arules::apriori(
    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にしたい場合には次のようにすればよい。

Code
as(rules, "data.frame") |>
  dplyr::mutate(across(where(is.numeric), ~ signif(., digits = 3))) |>
  tidyr::separate_wider_delim(rules, delim = " => ", names = c("lhs", "rhs")) |>
  dplyr::arrange(desc(lift))
#> # A tibble: 65 × 7
#>    lhs           rhs         support confidence coverage  lift count
#>    <chr>         <chr>         <dbl>      <dbl>    <dbl> <dbl> <dbl>
#>  1 {手紙/名詞}   {書く/動詞}  0.127       0.821   0.155   4.14   375
#>  2 {向う/動詞}   {K/未知語}   0.0839      0.832   0.101   2.43   247
#>  3 {掛ける/動詞} {聞く/動詞}  0.0754      0.835   0.0904  2.13   222
#>  4 {黙る/動詞}   {聞く/動詞}  0.103       0.83    0.124   2.12   303
#>  5 {始める/動詞} {聞く/動詞}  0.0788      0.806   0.0978  2.05   232
#>  6 {掛ける/動詞} {出る/動詞}  0.0768      0.85    0.0904  1.88   226
#>  7 {過ぎる/動詞} {自分/名詞}  0.0778      0.898   0.0866  1.87   229
#>  8 {考え/名詞}   {自分/名詞}  0.0798      0.88    0.0907  1.83   235
#>  9 {ぎり/未知語} {出る/動詞}  0.0761      0.827   0.0921  1.83   224
#> 10 {始める/動詞} {出る/動詞}  0.0791      0.809   0.0978  1.79   233
#> # ℹ 55 more rows

3.1.4 散布図🍳

Code
plot(rules, engine = "html")
#> To reduce overplotting, jitter is added! Use jitter = 0 to prevent jitter.

3.1.5 バルーンプロット🍳

Code
plot(rules, method = "grouped", engine = "html")

3.1.6 ネットワーク図🍳

Code
plot(rules, method = "graph", engine = "html")

3.2 対応分析(A.5.7)

3.2.1 コレスポンデンス分析

段落(doc_id)内の頻度で語彙を削ってから部(section)ごとに集計するために、ややめんどうなことをしている。

Code
dfm <-
  dplyr::tbl(con, "tokens") |>
  dplyr::filter(
    pos %in% c(
      "名詞", "名詞B", "名詞C",
      "地名", "人名", "組織名", "固有名詞",
      "動詞", "未知語", "タグ"
    )
  ) |>
  dplyr::mutate(
    token = dplyr::if_else(is.na(original), token, original),
    token = paste(token, pos, sep = "/")
  ) |>
  dplyr::count(doc_id, token) |>
  dplyr::collect() |>
  tidytext::cast_dfm(doc_id, token, n) |>
  quanteda::dfm_trim(
    min_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()を使って対応分析にかける。

Code
ca_fit <- dfm |>
  tidytext::tidy() |>
  dplyr::left_join(
    dplyr::select(tbl, doc_id, section),
    by = dplyr::join_by(document == doc_id)
  ) |>
  tidytext::cast_dfm(section, term, count) |>
  quanteda.textmodels::textmodel_ca(nd = 2, sparse = TRUE)

この関数は疎行列に対して計算をおこなえるため、比較的大きな行列を渡しても大丈夫。

3.2.2 バイプロット

caパッケージを読み込んでいるとplot()でバイプロットを描ける。factoextra::fviz_ca_biplot()でも描けるが、見た目はplot()のとあまり変わらない。

Code
library(ca)
dat <- plot(ca_fit)

3.2.3 バイプロット(バブルプロット)

ggplot2でバイプロットを描画するには、たとえば次のようにする。ggrepel::geom_text_repel()でラベルを出す語彙の選択の仕方はもうすこし工夫したほうがよいかもしれない。

なお、このコードはCorrespondence Analysis visualization using ggplot | R-bloggersを参考にした。

Code
tf <- dfm |>
  tidytext::tidy() |>
  dplyr::left_join(
    dplyr::select(tbl, doc_id, section),
    by = dplyr::join_by(document == doc_id)
  ) |>
  dplyr::summarise(tf = sum(count), .by = term) |>
  dplyr::pull(tf, term)

# modified from https://www.r-bloggers.com/2019/08/correspondence-analysis-visualization-using-ggplot/
make_ca_plot_df <- function(ca.plot.obj, row.lab = "Rows", col.lab = "Columns") {
  tibble::tibble(
    Label = c(
      rownames(ca.plot.obj$rows),
      rownames(ca.plot.obj$cols)
    ),
    Dim1 = c(
      ca.plot.obj$rows[, 1],
      ca.plot.obj$cols[, 1]
    ),
    Dim2 = c(
      ca.plot.obj$rows[, 2],
      ca.plot.obj$cols[, 2]
    ),
    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") |>
  dplyr::mutate(
    Size = dplyr::if_else(Variable == "Construction", mean(tf), tf[Label])
  )
# 非ASCII文字のラベルに対してwarningを出さないようにする
suppressWarnings({
  ca_sum <- summary(ca_fit)
  dim_var_percs <- ca_sum$scree[, "values2"]
})

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) +
  ggrepel::geom_label_repel(
    data = \(x) dplyr::filter(x, Variable == "Construction"),
    show.legend = FALSE
  ) +
  ggrepel::geom_text_repel(
    data = \(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: 24 unlabeled data points (too many overlaps). Consider
#> increasing max.overlaps

3.3 多次元尺度構成法(A.5.8)

3.3.1 MDS・バブルプロット

MASS::isoMDS()よりMASS::sammon()のほうがたぶん見やすい。

Code
simil <- dfm |>
  quanteda::dfm_weight(scheme = "boolean") |>
  proxyC::simil(margin = 2, method = "jaccard")

dat <- MASS::sammon(1 - simil, k = 2) |>
  purrr::pluck("points")
#> Initial stress        : 0.62279
#> stress after   0 iters: 0.62279
Code
dat <- dat |>
  dplyr::as_tibble(
    rownames = "label",
    .name_repair = ~ c("Dim1", "Dim2")
  ) |>
  dplyr::mutate(
    size = tf[label],
    clust = (hclust(
      proxyC::dist(dat, method = "euclidean") |> as.dist(),
      method = "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) +
  ggrepel::geom_text_repel(show.legend = FALSE) +
  scale_size_area(max_size = 16) +
  theme_classic()
#> Warning: ggrepel: 23 unlabeled data points (too many overlaps). Consider
#> increasing max.overlaps

3.3.2 MDS・3次元プロット

scatterplot3dではなくplotlyで試してみたが、とくに見やすいということはなかったかもしれない。

Code
dat <- MASS::sammon(1 - simil, k = 3) |>
  purrr::pluck("points") |>
  dplyr::as_tibble(
    .name_repair = ~ paste0("V", seq_along(.)),
    rownames = "label"
  ) |>
  dplyr::rename(Dim1 = V1, Dim2 = V2, Dim3 = V3) |>
  dplyr::mutate(term_freq = tf[label])
#> Initial stress        : 0.53925
#> stress after  10 iters: 0.18929, magic = 0.092
#> stress after  20 iters: 0.14417, magic = 0.043
#> stress after  21 iters: 0.13794
Code
dat |>
  plotly::plot_ly(x = ~Dim1, y = ~Dim2, z = ~Dim3, text = ~label, color = ~term_freq) |>
  plotly::add_markers(opacity = .5)

Code
duckdb::dbDisconnect(con)
duckdb::duckdb_shutdown(drv)

sessioninfo::session_info(info = "packages")
#> ═ Session info ═══════════════════════════════════════════════════════════════
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package      * version    date (UTC) lib source
#>  arules       * 1.7-7      2023-11-29 [1] RSPM (R 4.4.0)
#>  arulesViz    * 1.5.3      2024-04-26 [1] RSPM (R 4.4.0)
#>  audubon        0.5.2      2024-04-27 [1] https://paithiov909.r-universe.dev (R 4.4.0)
#>  blob           1.2.4      2023-03-17 [1] RSPM (R 4.4.0)
#>  ca           * 0.71.1     2020-01-24 [1] RSPM (R 4.4.0)
#>  cachem         1.1.0      2024-05-16 [1] CRAN (R 4.4.0)
#>  cellranger     1.1.0      2016-07-27 [1] RSPM (R 4.4.0)
#>  cli            3.6.3      2024-06-21 [1] CRAN (R 4.4.1)
#>  codetools      0.2-19     2023-02-01 [4] CRAN (R 4.2.2)
#>  colorspace     2.1-0      2023-01-23 [1] RSPM (R 4.4.0)
#>  crosstalk      1.2.1      2023-11-23 [1] RSPM (R 4.4.0)
#>  curl           5.2.1      2024-03-01 [1] RSPM (R 4.4.0)
#>  data.table     1.15.4     2024-03-30 [1] RSPM (R 4.4.0)
#>  DBI          * 1.2.3      2024-06-02 [1] RSPM (R 4.4.0)
#>  dbplyr         2.5.0      2024-03-19 [1] RSPM (R 4.4.0)
#>  digest         0.6.36     2024-06-23 [1] RSPM (R 4.4.0)
#>  dplyr          1.1.4      2023-11-17 [1] RSPM (R 4.4.0)
#>  duckdb       * 1.0.0      2024-06-13 [1] CRAN (R 4.4.0)
#>  evaluate       0.24.0     2024-06-10 [1] RSPM (R 4.4.0)
#>  fansi          1.0.6      2023-12-08 [1] RSPM (R 4.4.0)
#>  farver         2.1.2      2024-05-13 [1] CRAN (R 4.4.0)
#>  fastmap        1.2.0      2024-05-15 [1] RSPM (R 4.4.0)
#>  fastmatch      1.1-4      2023-08-18 [1] RSPM (R 4.4.0)
#>  foreach        1.5.2      2022-02-02 [1] RSPM (R 4.4.0)
#>  generics       0.1.3      2022-07-05 [1] RSPM (R 4.4.0)
#>  ggforce        0.4.2      2024-02-19 [1] RSPM (R 4.4.0)
#>  ggplot2      * 3.5.1      2024-04-23 [1] RSPM (R 4.4.0)
#>  ggraph         2.2.1      2024-03-07 [1] RSPM (R 4.4.0)
#>  ggrepel        0.9.5      2024-01-10 [1] RSPM (R 4.4.0)
#>  gibasa         1.1.0.9004 2024-04-25 [1] https://paithiov909.r-universe.dev (R 4.3.3)
#>  glue           1.7.0      2024-01-09 [1] RSPM (R 4.4.0)
#>  graphlayouts   1.1.1      2024-03-09 [1] RSPM (R 4.4.0)
#>  gridExtra      2.3        2017-09-09 [1] RSPM (R 4.4.0)
#>  gtable         0.3.5      2024-04-22 [1] RSPM (R 4.4.0)
#>  htmltools      0.5.8.1    2024-04-04 [1] RSPM (R 4.4.0)
#>  htmlwidgets    1.6.4      2023-12-06 [1] RSPM (R 4.4.0)
#>  httr           1.4.7      2023-08-15 [1] RSPM (R 4.4.0)
#>  igraph         2.0.3      2024-03-13 [1] RSPM (R 4.4.0)
#>  iterators      1.0.14     2022-02-05 [1] RSPM (R 4.4.0)
#>  janeaustenr    1.0.0      2022-08-26 [1] RSPM (R 4.4.0)
#>  jsonlite       1.8.8      2023-12-04 [1] RSPM (R 4.4.0)
#>  knitr          1.47       2024-05-29 [1] CRAN (R 4.4.0)
#>  labeling       0.4.3      2023-08-29 [1] RSPM (R 4.4.0)
#>  lattice        0.22-5     2023-10-24 [4] CRAN (R 4.3.1)
#>  lazyeval       0.2.2      2019-03-15 [1] RSPM (R 4.4.0)
#>  lifecycle      1.0.4      2023-11-07 [1] RSPM (R 4.4.0)
#>  magrittr       2.0.3      2022-03-30 [1] RSPM (R 4.4.0)
#>  MASS           7.3-60     2023-05-04 [4] CRAN (R 4.3.1)
#>  Matrix       * 1.6-5      2024-01-11 [4] CRAN (R 4.3.3)
#>  memoise        2.0.1      2021-11-26 [1] RSPM (R 4.4.0)
#>  munsell        0.5.1      2024-04-01 [1] RSPM (R 4.4.0)
#>  pillar         1.9.0      2023-03-22 [1] RSPM (R 4.4.0)
#>  pkgconfig      2.0.3      2019-09-22 [1] RSPM (R 4.4.0)
#>  plotly         4.10.4     2024-01-13 [1] RSPM (R 4.4.0)
#>  polyclip       1.10-6     2023-09-27 [1] RSPM (R 4.4.0)
#>  proxyC         0.4.1      2024-04-07 [1] CRAN (R 4.4.0)
#>  purrr          1.0.2      2023-08-10 [1] RSPM (R 4.4.0)
#>  quanteda       4.0.2      2024-04-24 [1] CRAN (R 4.4.0)
#>  R.cache        0.16.0     2022-07-21 [1] RSPM (R 4.4.0)
#>  R.methodsS3    1.8.2      2022-06-13 [1] RSPM (R 4.4.0)
#>  R.oo           1.26.0     2024-01-24 [1] RSPM (R 4.4.0)
#>  R.utils        2.12.3     2023-11-18 [1] RSPM (R 4.4.0)
#>  R6             2.5.1      2021-08-19 [1] RSPM (R 4.4.0)
#>  Rcpp           1.0.12     2024-01-09 [1] RSPM (R 4.4.0)
#>  RcppParallel   5.1.7      2023-02-27 [1] RSPM (R 4.4.0)
#>  readxl         1.4.3      2023-07-06 [1] RSPM (R 4.4.0)
#>  registry       0.5-1      2019-03-05 [1] RSPM (R 4.4.0)
#>  rlang          1.1.4      2024-06-04 [1] RSPM (R 4.4.0)
#>  rmarkdown      2.27       2024-05-17 [1] CRAN (R 4.4.0)
#>  scales         1.3.0      2023-11-28 [1] RSPM (R 4.4.0)
#>  seriation      1.5.5      2024-04-17 [1] RSPM (R 4.4.0)
#>  sessioninfo    1.2.2      2021-12-06 [1] RSPM (R 4.4.0)
#>  SnowballC      0.7.1      2023-04-25 [1] RSPM (R 4.4.0)
#>  stopwords      2.3        2021-10-28 [1] RSPM (R 4.4.0)
#>  stringi        1.8.4      2024-05-06 [1] CRAN (R 4.4.0)
#>  stringr        1.5.1      2023-11-14 [1] RSPM (R 4.4.0)
#>  styler         1.10.3     2024-04-07 [1] RSPM (R 4.4.0)
#>  tibble         3.2.1      2023-03-20 [1] RSPM (R 4.4.0)
#>  tidygraph      1.3.1      2024-01-30 [1] RSPM (R 4.4.0)
#>  tidyr          1.3.1      2024-01-24 [1] RSPM (R 4.4.0)
#>  tidyselect     1.2.1      2024-03-11 [1] RSPM (R 4.4.0)
#>  tidytext       0.4.2      2024-04-10 [1] RSPM (R 4.4.0)
#>  tokenizers     0.3.0      2022-12-22 [1] RSPM (R 4.4.0)
#>  TSP            1.2-4      2023-04-04 [1] RSPM (R 4.4.0)
#>  tweenr         2.0.3      2024-02-26 [1] RSPM (R 4.4.0)
#>  utf8           1.2.4      2023-10-22 [1] RSPM (R 4.4.0)
#>  V8             4.4.2      2024-02-15 [1] RSPM (R 4.4.0)
#>  vctrs          0.6.5      2023-12-01 [1] RSPM (R 4.4.0)
#>  viridis        0.6.5      2024-01-29 [1] RSPM (R 4.4.0)
#>  viridisLite    0.4.2      2023-05-02 [1] RSPM (R 4.4.0)
#>  visNetwork     2.1.2      2022-09-29 [1] RSPM (R 4.4.0)
#>  withr          3.0.0      2024-01-16 [1] RSPM (R 4.4.0)
#>  xfun           0.45       2024-06-16 [1] RSPM (R 4.4.0)
#>  yaml           2.3.8      2023-12-11 [1] RSPM (R 4.4.0)
#> 
#>  [1] /home/paithiov909/R/x86_64-pc-linux-gnu-library/4.4
#>  [2] /usr/local/lib/R/site-library
#>  [3] /usr/lib/R/site-library
#>  [4] /usr/lib/R/library
#> 
#> ──────────────────────────────────────────────────────────────────────────────