1  前処理メニュー

1.1 使用するデータセット

KH Coderのチュートリアル用のデータを使う。tutorial_data_3x.zipの中に含まれているtutorial_jp/kokoro.xlsというxlsファイルを次のように読み込んでおく。

Code
tbl <-
  readxl::read_xls("tutorial_jp/kokoro.xls",
    col_names = c("text", "section", "chapter", "label"),
    skip = 1
  ) |>
  dplyr::mutate(
    doc_id = 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)

tbl
#> # A tibble: 1,213 × 5
#>    doc_id text                                            section label  chapter
#>     <int> <chr>                                           <chr>   <chr>  <chr>  
#>  1      1 私はその人を常に先生と呼んでいた。だからここで… [1]上_… 上・一 1_01   
#>  2      2 私が先生と知り合いになったのは鎌倉である。その… [1]上_… 上・一 1_01   
#>  3      3 学校の授業が始まるにはまだ大分日数があるので鎌… [1]上_… 上・一 1_01   
#>  4      4 宿は鎌倉でも辺鄙な方角にあった。玉突きだのアイ… [1]上_… 上・一 1_01   
#>  5      5 私は毎日海へはいりに出掛けた。古い燻ぶり返った… [1]上_… 上・一 1_01   
#>  6      6 私は実に先生をこの雑沓の間に見付け出したのであ… [1]上_… 上・一 1_01   
#>  7      7 私がその掛茶屋で先生を見た時は、先生がちょうど… [1]上_… 上・二 1_02   
#>  8      8 その西洋人の優れて白い皮膚の色が、掛茶屋へ入る… [1]上_… 上・二 1_02   
#>  9      9 彼はやがて自分の傍を顧みて、そこにこごんでいる… [1]上_… 上・二 1_02   
#> 10     10 私は単に好奇心のために、並んで浜辺を下りて行く… [1]上_… 上・二 1_02   
#> # ℹ 1,203 more rows

このデータでは、夏目漱石の『こころ』が段落(doc_id)ごとにひとつのテキストとして打ち込まれている。『こころ』は上中下の3部(section)で構成されていて、それぞれの部が複数の章(label, chapter)に分かれている。

1.2 語の抽出(A.2.2)

gibasaを使って形態素解析をおこない、語を抽出する。

このデータをIPA辞書を使って形態素解析すると、延べ語数は105,000語程度になる。これくらいの語数であれば、形態素解析した結果をデータフレームとしてメモリ上に読み込んでも問題ないと思われるが、ここではより大規模なテキストデータを扱う場合を想定し、結果をDuckDBデータベースに書き込むことにする。

ここではchapterごとにグルーピングしながら、段落は文に分割せずに処理している。MeCabはバッファサイズの都合上、一度に262万字くらいまで一つの文として入力できるらしいが、極端に長い入力に対してはコスト計算ができず、エラーが出る可能性がある。また、多くの文を与えればそれだけ多くの行からなるデータフレームが返されるため、一度に処理する分量は利用している環境にあわせて適当に加減したほうがよい。

KH Coderでは、IPA辞書の品詞体系をもとに変更した品詞体系が使われている。そのため、KH Coderで前処理した結果をある程度再現するためには、一部の品詞情報を書き換える必要がある。KH Coder内で使われている品詞体系については、KH Coderのレファレンスを参照されたい。

また、このデータを使っているチュートリアルでは、強制抽出する語として「一人」「二人」という語を指定している。こうした語についてはMeCabのユーザー辞書に追加してしまったほうがよいが、簡単に処理するために、ここではgibasaの制約付き解析機能によって「タグ」として抽出している(KH Coderは強制抽出した語に対して「タグ」という品詞名を与える)。

Code
suppressPackageStartupMessages({
  library(duckdb)
})
drv <- duckdb::duckdb()

if (!fs::file_exists("tutorial_jp/kokoro.duckdb")) {
  con <- duckdb::dbConnect(drv, dbdir = "tutorial_jp/kokoro.duckdb", read_only = FALSE)

  dbCreateTable(
    con, "tokens",
    data.frame(
      doc_id = integer(),
      section = character(),
      label = character(),
      token_id = integer(),
      token = character(),
      pos = character(),
      original = character(),
      stringsAsFactors = FALSE
    )
  )

  tbl |>
    dplyr::group_by(chapter) |>
    dplyr::group_walk(~ {
      df <- .x |>
        dplyr::mutate(
          text = stringi::stri_replace_all_regex(text, "(?<codes>([一二三四五六七八九]{1}人))", "\n${codes}\tタグ\n") |>
            stringi::stri_trim_both()
        ) |>
        gibasa::tokenize(text, doc_id, partial = TRUE) |>
        gibasa::prettify(
          col_select = c("POS1", "POS2", "POS3", "Original")
        ) |>
        dplyr::mutate(
          pos = dplyr::case_when(
            (POS1 == "タグ") ~ "タグ",
            (is.na(Original) & stringr::str_detect(token, "^[[:alpha:]]+$")) ~ "未知語",
            (POS1 == "感動詞") ~ "感動詞",
            (POS1 == "名詞" & POS2 == "一般" & stringr::str_detect(token, "^[\\p{Han}]{1}$")) ~ "名詞C",
            (POS1 == "名詞" & POS2 == "一般" & stringr::str_detect(token, "^[\\p{Hiragana}]+$")) ~ "名詞B",
            (POS1 == "名詞" & POS2 == "一般") ~ "名詞",
            (POS1 == "名詞" & POS2 == "固有名詞" & POS3 == "地域") ~ "地名",
            (POS1 == "名詞" & POS2 == "固有名詞" & POS3 == "人名") ~ "人名",
            (POS1 == "名詞" & POS2 == "固有名詞" & POS3 == "組織") ~ "組織名",
            (POS1 == "名詞" & POS2 == "形容動詞語幹") ~ "形容動詞",
            (POS1 == "名詞" & POS2 == "ナイ形容詞語幹") ~ "ナイ形容詞",
            (POS1 == "名詞" & POS2 == "固有名詞") ~ "固有名詞",
            (POS1 == "名詞" & POS2 == "サ変接続") ~ "サ変名詞",
            (POS1 == "名詞" & POS2 == "副詞可能") ~ "副詞可能",
            (POS1 == "動詞" & POS2 == "自立" & stringr::str_detect(token, "^[\\p{Hiragana}]+$")) ~ "動詞B",
            (POS1 == "動詞" & POS2 == "自立") ~ "動詞",
            (POS1 == "形容詞" & stringr::str_detect(token, "^[\\p{Hiragana}]+$")) ~ "形容詞B",
            (POS1 == "形容詞" & POS2 == "非自立") ~ "形容詞(非自立)",
            (POS1 == "形容詞") ~ "形容詞",
            (POS1 == "副詞" & stringr::str_detect(token, "^[\\p{Hiragana}]+$")) ~ "副詞B",
            (POS1 == "副詞") ~ "副詞",
            (POS1 == "助動詞" & Original %in% c("ない", "まい", "ぬ", "ん")) ~ "否定助動詞",
            .default = "その他"
          )
        ) |>
        dplyr::select(doc_id, section, label, token_id, token, pos, Original) |>
        dplyr::rename(original = Original)

      dbAppendTable(con, "tokens", df)
    })
} else {
  con <- duckdb::dbConnect(drv, dbdir = "tutorial_jp/kokoro.duckdb", read_only = TRUE)
}

1.3 コーディングルール(A.2.5)

KH Coderの強力な機能のひとつとして、「コーディングルール」によるトークンへのタグ付けというのがある。KH Coderのコーディングルールはかなり複雑な記法を扱うため、Rで完璧に再現するには相応の手間がかかる。一方で、コードを与えるべき抽出語を基本形とマッチングする程度であれば、次のように比較的少ないコード量で似たようなことを実現できる。

Code
rules <- list(
  "人の死" = c("死後", "死病", "死期", "死因", "死骸", "生死", "自殺", "殉死", "頓死", "変死", "亡", "死ぬ", "亡くなる", "殺す", "亡くす", "死"),
  "恋愛" = c("愛", "恋", "愛す", "愛情", "恋人", "愛人", "恋愛", "失恋", "恋しい"),
  "友情" = c("友達", "友人", "旧友", "親友", "朋友", "友", "級友"),
  "信用・不信" = c("信用", "信じる", "信ずる", "不信", "疑い", "疑惑", "疑念", "猜疑", "狐疑", "疑問", "疑い深い", "疑う", "疑る", "警戒"),
  "病気" = c("医者", "病人", "病室", "病院", "病症", "病状", "持病", "死病", "主治医", "精神病", "仮病", "病気", "看病", "大病", "病む", "病")
)
rules_chr <- purrr::flatten_chr(rules)

codes <-
  dplyr::tbl(con, "tokens") |>
  dplyr::filter(original %in% rules_chr) |>
  dplyr::collect() |>
  dplyr::mutate(
    codings = purrr::map(
      original,
      ~ purrr::imap(rules, \(.x, .y) tibble::tibble(code = .y, flag = . %in% .x)) |>
        purrr::list_rbind() |>
        dplyr::filter(flag == TRUE) |>
        dplyr::select(!flag)
    )
  ) |>
  tidyr::unnest(codings)

codes
#> # A tibble: 537 × 8
#>    doc_id section        label  token_id token    pos      original code      
#>     <int> <chr>          <chr>     <int> <chr>    <chr>    <chr>    <chr>     
#>  1      2 [1]上_先生と私 上・一       36 友達     名詞     友達     友情      
#>  2      2 [1]上_先生と私 上・一       96 友達     名詞     友達     友情      
#>  3      2 [1]上_先生と私 上・一      115 病気     サ変名詞 病気     病気      
#>  4      2 [1]上_先生と私 上・一      124 友達     名詞     友達     友情      
#>  5      2 [1]上_先生と私 上・一      128 信じ     動詞     信じる   信用・不信
#>  6      2 [1]上_先生と私 上・一      132 友達     名詞     友達     友情      
#>  7      2 [1]上_先生と私 上・一      240 病気     サ変名詞 病気     病気      
#>  8      3 [1]上_先生と私 上・一       44 友達     名詞     友達     友情      
#>  9     19 [1]上_先生と私 上・三      207 疑っ     動詞     疑う     信用・不信
#> 10     21 [1]上_先生と私 上・四      161 亡くなっ 動詞     亡くなる 人の死    
#> # ℹ 527 more rows

また、集計するだけだったらquanteda::dictionary()を使うのが早い。

Code
rules <- quanteda::dictionary(rules)

dfm <-
  dplyr::tbl(con, "tokens") |>
  dplyr::mutate(token = dplyr::if_else(is.na(original), token, original)) |>
  dplyr::count(doc_id, token) |>
  dplyr::collect() |>
  tidytext::cast_dfm(doc_id, token, n) |>
  quanteda::dfm_lookup(rules)

dfm
#> Document-feature matrix of: 1,213 documents, 5 features (94.23% sparse) and 0 docvars.
#>     features
#> docs 人の死 恋愛 友情 信用・不信 病気
#>    1      0    0    0          0    0
#>    2      0    0    4          1    2
#>    3      0    0    1          0    0
#>    4      0    0    0          0    0
#>    5      0    0    0          0    0
#>    6      0    0    0          0    0
#> [ reached max_ndoc ... 1,207 more documents ]

1.4 抽出語リスト(A.3.4)

「エクスポート」メニューから得られるような抽出語リストをデータフレームとして得る例。

Excel向けの出力は見やすいようにカラムを分けているが、Rのデータフレームとして扱うならtidyな縦長のデータにしたほうがよい。

1.4.1 品詞別・上位15語

Code
dplyr::tbl(con, "tokens") |>
  dplyr::filter(
    !pos %in% c("その他", "名詞B", "動詞B", "形容詞B", "副詞B", "否定助動詞", "形容詞(非自立)")
  ) |>
  dplyr::mutate(token = dplyr::if_else(is.na(original), token, original)) |>
  dplyr::count(token, pos) |>
  dplyr::slice_max(n, n = 15, by = pos) |>
  dplyr::collect()
#> # A tibble: 232 × 3
#>    token  pos          n
#>    <chr>  <chr>    <dbl>
#>  1 二人   タグ       115
#>  2 一人   タグ        73
#>  3 三人   タグ        10
#>  4 前     副詞可能   169
#>  5 今     副詞可能   140
#>  6 場合   副詞可能    38
#>  7 過去   副詞可能    34
#>  8 結果   副詞可能    30
#>  9 すべて 副詞可能    28
#> 10 平生   副詞可能    26
#> # ℹ 222 more rows

1.4.2 頻出150語

Code
dplyr::tbl(con, "tokens") |>
  dplyr::filter(
    !pos %in% c("その他", "名詞B", "動詞B", "形容詞B", "副詞B", "否定助動詞", "形容詞(非自立)")
  ) |>
  dplyr::mutate(token = dplyr::if_else(is.na(original), token, original)) |>
  dplyr::count(token, pos) |>
  dplyr::slice_max(n, n = 150) |>
  dplyr::collect()
#> # A tibble: 152 × 3
#>    token  pos        n
#>    <chr>  <chr>  <dbl>
#>  1 先生   名詞     595
#>  2 K      未知語   411
#>  3 奥さん 名詞     388
#>  4 思う   動詞     293
#>  5 父     名詞C    269
#>  6 自分   名詞     264
#>  7 見る   動詞     225
#>  8 聞く   動詞     218
#>  9 出る   動詞     179
#> 10 人     名詞C    176
#> # ℹ 142 more rows

1.5 「文書・抽出語」表(A.3.5)

いわゆる文書単語行列の例。dplyr::collectした後にtidyr::pivot_wider()などで横に展開してもよいが、多くの場合、疎行列のオブジェクトにしてしまったほうが、この後にRでの解析に用いる上では扱いやすいと思われる。quantedaのdfmオブジェクトをふつうの密な行列にしたいときは、as.matrix(dfm)すればよい。

Code
dfm <-
  dplyr::tbl(con, "tokens") |>
  dplyr::filter(
    !pos %in% c("その他", "名詞B", "動詞B", "形容詞B", "副詞B", "否定助動詞", "形容詞(非自立)")
  ) |>
  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")

quanteda::docvars(dfm, "section") <-
  dplyr::filter(tbl, doc_id %in% quanteda::docnames(dfm)) |>
  dplyr::pull("section")

dfm
#> Document-feature matrix of: 1,189 documents, 76 features (92.93% sparse) and 1 docvar.
#>     features
#> docs 先生/名詞 取る/動詞 思う/動詞 女/名詞C 一人/タグ 知れる/動詞 立つ/動詞
#>    1         3         0         0        0         0           0         0
#>    2         1         0         0        0         1           0         0
#>    3         0         0         0        0         1           0         0
#>    4         0         1         0        0         0           0         0
#>    5         0         0         1        1         1           0         0
#>    6         1         0         0        0         0           0         0
#>     features
#> docs 来る/動詞 見る/動詞 頭/名詞C
#>    1         0         0        0
#>    2         2         0        0
#>    3         0         0        0
#>    4         0         0        0
#>    5         1         0        1
#>    6         0         0        0
#> [ reached max_ndoc ... 1,183 more documents, reached max_nfeat ... 66 more features ]

1.6 「文書・コード」表(A.3.6)

「文書・コード」行列の例。コードの出現頻度ではなく「コードの有無をあらわす2値変数」を出力する。

Code
dfm <- codes |>
  dplyr::count(doc_id, code) |>
  tidytext::cast_dfm(doc_id, code, n) |>
  quanteda::dfm_weight(scheme = "boolean")

quanteda::docvars(dfm, "section") <-
  dplyr::filter(tbl, doc_id %in% quanteda::docnames(dfm)) |>
  dplyr::pull("section")

dfm
#> Document-feature matrix of: 294 documents, 5 features (76.19% sparse) and 1 docvar.
#>     features
#> docs 信用・不信 友情 病気 人の死 恋愛
#>   2           1    1    1      0    0
#>   3           0    1    0      0    0
#>   19          1    0    0      0    0
#>   21          0    0    0      1    0
#>   37          0    0    0      1    0
#>   49          0    1    0      0    0
#> [ reached max_ndoc ... 288 more documents ]

1.7 「抽出語・文脈ベクトル」表(A.3.7)

1.7.1 word2vec🍳

Caution

以下で扱っているベクトルは、KH Coderにおける「抽出語・文脈ベクトル」とは異なるものです

KH Coderにおける「文脈ベクトル」は、これを使って抽出語メニューからおこなえるような分析をすることによって、「似たような使われ方をする語を調べる」使い方をするためのものだと思われる。

「似たような使われ方をする語を調べる」ためであれば、単語埋め込みを使ってもよさそう。ただし、KH Coderの「文脈ベクトル」を使う場合の「似たような使われ方をする」が、あくまで分析対象とする文書のなかでという意味であるのに対して、単語埋め込みを使う場合では、埋め込みの学習に使われた文書のなかでという意味になってしまう点には注意。

試しに、既存のword2vecモデルから単語ベクトルを読み込んでみる。ここでは、Wikipedia2Vecの100次元のものを使う。だいぶ古いモデルだが、MeCab+IPA辞書で分かち書きされた語彙を使っていることが確認できる単語埋め込みとなると(参考)、これくらいの時期につくられたものになりそう。

Code
# word2vecのテキスト形式のファイルは、先頭行に埋め込みの「行数 列数」が書かれている
readr::read_lines("tutorial_jp/jawiki_20180420_100d.txt.bz2", n_max = 1)
#> [1] "1593143 100"

# 下のほうは低頻度語で、全部読み込む必要はないと思われるので、ここでは先頭から1e5行だけ読み込む
embeddings <-
  readr::read_delim(
    "tutorial_jp/jawiki_20180420_100d.txt.bz2",
    delim = " ",
    col_names = c("token", paste0("dim_", seq_len(100))),
    skip = 1,
    n_max = 1e5,
    show_col_types = FALSE
  )

# メモリ上でのサイズ
lobstr::obj_size(embeddings)
#> 117.94 MB

このうち、分析対象の文書に含まれる語彙のベクトルだけを適当に取り出しておく。

Code
embeddings <-
  dplyr::tbl(con, "tokens") |>
  dplyr::filter(
    !pos %in% c("その他", "名詞B", "動詞B", "形容詞B", "副詞B", "否定助動詞", "形容詞(非自立)")
  ) |>
  dplyr::transmute(
    doc_id = doc_id,
    token = token,
    label = paste(token, pos, sep = "/")
  ) |>
  dplyr::distinct(doc_id, token, .keep_all = TRUE) |>
  dplyr::collect() |>
  dplyr::inner_join(embeddings, by = dplyr::join_by(token == token))

embeddings
#> # A tibble: 10,578 × 103
#>    doc_id token    label    dim_1   dim_2  dim_3   dim_4   dim_5   dim_6   dim_7
#>     <int> <chr>    <chr>    <dbl>   <dbl>  <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
#>  1      1 起す     起す/…  0.195  -0.222  -0.537 -0.410   0.0108 -0.0568 -0.233 
#>  2      2 利用     利用/…  0.748  -0.0368  0.112  0.328  -0.112  -0.410   0.267 
#>  3      2 勧       勧/人…  1.00   -0.378  -0.202  0.621   0.344  -1.16    0.444 
#>  4      3 変り     変り/… -0.0866 -0.638   0.234  0.258  -0.0035 -0.183   0.259 
#>  5      4 別荘     別荘/…  0.842   0.0458  0.517  0.537   0.0167  0.142   0.186 
#>  6      5 寝そべっ 寝そ…   0.241   0.255   0.496 -0.0384 -0.158   0.777   0.443 
#>  7      5 賑やか   賑や…   0.295   0.451   0.182  0.248   0.497   0.092  -0.538 
#>  8      5 辺       辺/名…  0.559  -0.506   0.316 -0.0812  0.110   0.0038 -0.179 
#>  9      5 銭湯     銭湯/…  0.657  -0.165  -0.613  0.507   0.109  -0.115  -0.0007
#> 10      6 飲み     飲み/…  0.642  -0.475   0.452  0.005   0.231  -0.358  -0.0187
#> # ℹ 10,568 more rows
#> # ℹ 93 more variables: dim_8 <dbl>, dim_9 <dbl>, dim_10 <dbl>, dim_11 <dbl>,
#> #   dim_12 <dbl>, dim_13 <dbl>, dim_14 <dbl>, dim_15 <dbl>, dim_16 <dbl>,
#> #   dim_17 <dbl>, dim_18 <dbl>, dim_19 <dbl>, dim_20 <dbl>, dim_21 <dbl>,
#> #   dim_22 <dbl>, dim_23 <dbl>, dim_24 <dbl>, dim_25 <dbl>, dim_26 <dbl>,
#> #   dim_27 <dbl>, dim_28 <dbl>, dim_29 <dbl>, dim_30 <dbl>, dim_31 <dbl>,
#> #   dim_32 <dbl>, dim_33 <dbl>, dim_34 <dbl>, dim_35 <dbl>, dim_36 <dbl>, …

1.7.2 独立成分分析(ICA)🍳

word2vecを含む埋め込み表現は、「独立成分分析(ICA)で次元削減することで、人間にとって解釈性の高い成分を取り出すことができる」ことが知られている(参考)。これを応用すると、ICAで取り出した成分をもとにして、コーディングルールにするとよさそうなカテゴリや語彙を探索できるかもしれない。

Code
ica <- embeddings |>
  dplyr::select(label, dplyr::starts_with("dim")) |>
  dplyr::distinct() |>
  tibble::column_to_rownames("label") |>
  as.matrix() |>
  fastICA::fastICA(n.comp = 20)

dat <- ica$S |>
  rlang::as_function(~ {
    . * sign(moments::skewness(.)) # 正方向のスコアだけを扱うため、歪度が負の成分を変換する
  })() |>
  dplyr::as_tibble(
    .name_repair = ~ paste("dim", stringr::str_pad(seq_along(.), width = 2, pad = "0"), sep = "_"),
    rownames = "label"
  ) |>
  tidyr::pivot_longer(cols = !label, names_to = "dim", values_to = "score")

ここでは正方向のスコアだけを扱うため、歪度が負の成分を正方向に変換する処理をしている。もちろん、実際には負の方向のスコアが大きい語彙とあわせて解釈したほうがわかりやすい成分もありそうなことからも、そのあたりも含めてそれぞれの成分を探索し、ほかの分析とも組み合わせながらコーディングルールを考えるべきだろう。

各成分の正方向のスコアが大きい語彙を図示すると次のような感じになる。

Code
library(ggplot2)

dat |>
  dplyr::slice_max(score, n = 8, by = dim) |>
  ggplot(aes(x = reorder(label, score), y = score)) +
  geom_col() +
  coord_flip() +
  facet_wrap(vars(dim), ncol = 4, scales = "free_y") +
  theme_minimal() +
  labs(x = NULL, y = NULL)

実際にはこうして得られたスコアの大きい語をそのままコーディングルールとして採用することはしないほうがよいが、ここから次のようにして「文書・コード」表をつくることもできる。

Code
rules <- dat |>
  dplyr::slice_max(score, n = 10, by = dim) |>
  dplyr::reframe(
    label = list(label),
    .by = dim
  ) |>
  tibble::deframe()

codes <-
  embeddings |>
  dplyr::select(doc_id, label) |>
  dplyr::filter(label %in% purrr::flatten_chr(rules)) |>
  dplyr::mutate(
    codings = purrr::map(
      label,
      ~ purrr::imap(rules, \(.x, .y) tibble::tibble(code = .y, flag = . %in% .x)) |>
        purrr::list_rbind() |>
        dplyr::filter(flag == TRUE) |>
        dplyr::select(!flag)
    )
  ) |>
  tidyr::unnest(codings)

dfm <- codes |>
  dplyr::count(doc_id, code) |>
  tidytext::cast_dfm(doc_id, code, n) |>
  quanteda::dfm_weight(scheme = "boolean")

dfm
#> Document-feature matrix of: 410 documents, 20 features (92.94% sparse) and 0 docvars.
#>     features
#> docs dim_08 dim_14 dim_12 dim_11 dim_13 dim_03 dim_10 dim_01 dim_06 dim_18
#>    2      1      0      0      0      0      0      0      0      0      0
#>    3      0      1      0      0      0      0      0      0      0      0
#>    4      0      0      1      0      0      0      0      0      0      0
#>    5      0      0      0      1      1      0      0      0      0      0
#>    7      0      0      0      0      0      1      1      0      0      0
#>    8      0      0      0      0      0      0      0      1      0      0
#> [ reached max_ndoc ... 404 more documents, reached max_nfeat ... 10 more features ]

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

sessioninfo::session_info(info = "packages")
#> ═ Session info ═══════════════════════════════════════════════════════════════
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package      * version    date (UTC) lib source
#>  audubon        0.5.2      2024-04-27 [1] https://paithiov909.r-universe.dev (R 4.4.0)
#>  bit            4.0.5      2022-11-15 [1] RSPM (R 4.4.0)
#>  bit64          4.0.5      2020-08-30 [1] RSPM (R 4.4.0)
#>  blob           1.2.4      2023-03-17 [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)
#>  colorspace     2.1-0      2023-01-23 [1] RSPM (R 4.4.0)
#>  crayon         1.5.3      2024-06-20 [1] RSPM (R 4.4.0)
#>  curl           5.2.1      2024-03-01 [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)
#>  fs             1.6.4      2024-04-25 [1] RSPM (R 4.4.0)
#>  generics       0.1.3      2022-07-05 [1] RSPM (R 4.4.0)
#>  ggplot2      * 3.5.1      2024-04-23 [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)
#>  gtable         0.3.5      2024-04-22 [1] RSPM (R 4.4.0)
#>  hms            1.1.3      2023-03-21 [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)
#>  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)
#>  lifecycle      1.0.4      2023-11-07 [1] RSPM (R 4.4.0)
#>  lobstr         1.1.2      2022-06-22 [1] RSPM (R 4.4.0)
#>  magrittr       2.0.3      2022-03-30 [1] RSPM (R 4.4.0)
#>  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)
#>  prettyunits    1.2.0      2023-09-24 [1] RSPM (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)
#>  readr          2.1.5      2024-01-10 [1] RSPM (R 4.4.0)
#>  readxl         1.4.3      2023-07-06 [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)
#>  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)
#>  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)
#>  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)
#>  tzdb           0.4.0      2023-05-12 [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)
#>  vroom          1.6.5      2023-12-05 [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
#> 
#> ──────────────────────────────────────────────────────────────────────────────