Анализ лингвистического корпуса Scratch студии: различия между версиями

Материал из Поле цифровой дидактики
Нет описания правки
 
(не показаны 4 промежуточные версии этого же участника)
Строка 1: Строка 1:
== Корпусная лингвистика на практике ==
* сборка корпуса из разнородных источников (стена студии + комментарии к проектам)
* разметка текстов: length, token count, script detection (латиница / кириллица)
* частотный словарь и Type-Token Ratio как мера лексического разнообразия
* понятие регистра и жанра на конкретном материале: чем "стена" отличается от комментария к проекту


Сетевой анализ коммуникации
* граф ответов как модель речевого взаимодействия
* центральность как лингвистическая роль (кто инициирует, кому отвечают)
* понятие плотности сети применительно к дискурсу сообщества


=== Выбор материала для анализа ===
# https://scratch.mit.edu/studios/27046820 SuperMario1
# https://scratch.mit.edu/studios/50716890/ Фан клуб Марио (на русском)
# https://scratch.mit.edu/studios/31223126/ Kids_Iot


== Скрипт R ==
== Скрипт R ==

Текущая версия от 11:00, 5 мая 2026

Корпусная лингвистика на практике

  • сборка корпуса из разнородных источников (стена студии + комментарии к проектам)
  • разметка текстов: length, token count, script detection (латиница / кириллица)
  • частотный словарь и Type-Token Ratio как мера лексического разнообразия
  • понятие регистра и жанра на конкретном материале: чем "стена" отличается от комментария к проекту

Сетевой анализ коммуникации

  • граф ответов как модель речевого взаимодействия
  • центральность как лингвистическая роль (кто инициирует, кому отвечают)
  • понятие плотности сети применительно к дискурсу сообщества

Выбор материала для анализа

  1. https://scratch.mit.edu/studios/27046820 SuperMario1
  2. https://scratch.mit.edu/studios/50716890/ Фан клуб Марио (на русском)
  3. https://scratch.mit.edu/studios/31223126/ Kids_Iot

Скрипт R

########################################################################
## scratch_lingvo_expedition.R
## "Программирование и лингвистические данные" — DigIda МГПУ
##
## Цель: собрать данные из Scratch-студии и проанализировать
##       комментарии как лингвистический корпус.
##
## Зависимости: Scratch_Api_core.R (в той же папке)
##
## Все данные сохраняются в CSV (UTF-8) — для удобного обмена,
## загрузки в SemanticMediaWiki и работы в Excel / Google Sheets.
########################################################################

library(httr)
library(jsonlite)
library(dplyr)
library(purrr)
library(tidyr)
library(lubridate)
library(stringr)
library(ggplot2)

source("Scratch_Api_core.R", encoding = "UTF-8")

## Вспомогательная обёртка write.csv: UTF-8, без row.names, BOM-free
save_csv <- function(df, path) {
  write.csv(df, path, row.names = FALSE, fileEncoding = "UTF-8")
  message("Сохранено: ", path)
}

########################################################################
## 0. ПАРАМЕТРЫ ЭКСПЕДИЦИИ
##    Замените studio_id на реальный номер вашей студии.
########################################################################

TARGET_STUDIO_ID <- 30570341      # пример: RU-студия из ScratchStudio-Lingvo-Data.md
STUDIO_LABEL     <- "ru_lingvo"   # метка — используется в именах файлов и столбце label

OUTPUT_DIR <- file.path("data_expedition", STUDIO_LABEL)
dir.create(OUTPUT_DIR, showWarnings = FALSE, recursive = TRUE)

########################################################################
## БЛОК 1. СБОР ДАННЫХ
########################################################################

## 1.1 Проекты студии --------------------------------------------------
message("=== 1.1 Получаем проекты студии ===")
studio_projects <- get_studio_projects(TARGET_STUDIO_ID, verbose = TRUE)
save_csv(studio_projects, file.path(OUTPUT_DIR, "studio_projects.csv"))

## 1.2 Комментарии студии (стена) -------------------------------------
message("=== 1.2 Комментарии студии (стена) ===")
studio_wall_comments <- get_studio_comments(TARGET_STUDIO_ID, verbose = TRUE)
if (!is.null(studio_wall_comments)) {
  save_csv(studio_wall_comments, file.path(OUTPUT_DIR, "studio_wall_comments.csv"))
}

## 1.3 Комментарии ко всем проектам студии ----------------------------
message("=== 1.3 Комментарии к проектам студии ===")
project_comments <- get_all_project_comments_for_studio(
  studio_id       = TARGET_STUDIO_ID,
  studio_projects = studio_projects,
  verbose         = TRUE
)
if (!is.null(project_comments)) {
  save_csv(project_comments, file.path(OUTPUT_DIR, "project_comments.csv"))
}

## 1.4 Объединённый корпус --------------------------------------------
## Единый датафрейм: source = "wall" | "project"
make_corpus <- function(wall_df, proj_df, studio_id, label) {

  wall_part <- NULL
  if (!is.null(wall_df) && nrow(wall_df) > 0) {
    wall_part <- wall_df |>
      transmute(
        studio_id,
        label,
        source           = "wall",
        project_id       = NA_integer_,
        comment_id,
        parent_id,
        author_username,
        content,
        datetime_created,
        reply_count
      )
  }

  proj_part <- NULL
  if (!is.null(proj_df) && nrow(proj_df) > 0) {
    proj_part <- proj_df |>
      transmute(
        studio_id,
        label,
        source           = "project",
        project_id,
        comment_id,
        parent_id,
        author_username,
        content,
        datetime_created,
        reply_count
      )
  }

  bind_rows(wall_part, proj_part)
}

corpus <- make_corpus(
  wall_df   = studio_wall_comments,
  proj_df   = project_comments,
  studio_id = TARGET_STUDIO_ID,
  label     = STUDIO_LABEL
)

## Полный корпус с текстами (основа для всех последующих шагов)
save_csv(corpus, file.path(OUTPUT_DIR, "corpus.csv"))

cat("\nВсего комментариев:", nrow(corpus), "\n")
cat("Уникальных авторов:", n_distinct(corpus$author_username), "\n")

########################################################################
## БЛОК 2. ЛИНГВИСТИЧЕСКАЯ ПРЕДОБРАБОТКА
########################################################################

## 2.1 Базовые текстовые метрики --------------------------------------
corpus_ling <- corpus |>
  mutate(
    n_chars      = nchar(content, allowNA = TRUE),
    n_words      = str_count(content, "\\S+"),           # токены через пробел
    n_tokens     = str_count(content, "[[:alpha:]]+"),   # только буквенные
    has_latin    = str_detect(content, "[A-Za-z]"),
    has_cyrillic = str_detect(content, "[А-Яа-яЁё]"),
    has_digits   = str_detect(content, "[0-9]"),
    has_url      = str_detect(content, "https?://"),
    has_at       = str_detect(content, "@"),
    is_reply     = !is.na(parent_id),
    ym           = format(floor_date(datetime_created, "month"), "%Y-%m")
  )

## Сохраняем с метриками, но БЕЗ исходного текста — удобно грузить в SMW
corpus_meta <- corpus_ling |> select(-content)
save_csv(corpus_meta, file.path(OUTPUT_DIR, "corpus_meta.csv"))

## Отдельно — только тексты (нужны для tidytext / quanteda)
corpus_texts <- corpus_ling |>
  select(studio_id, label, source, comment_id, author_username,
         datetime_created, content)
save_csv(corpus_texts, file.path(OUTPUT_DIR, "corpus_texts.csv"))

## 2.2 Профиль авторов ------------------------------------------------
author_profile <- corpus_ling |>
  group_by(author_username) |>
  summarise(
    studio_id      = first(studio_id),
    label          = first(label),
    n_comments     = n(),
    n_wall         = sum(source == "wall"),
    n_projects     = sum(source == "project"),
    n_replies      = sum(is_reply),
    mean_chars     = round(mean(n_chars,  na.rm = TRUE), 1),
    mean_words     = round(mean(n_words,  na.rm = TRUE), 2),
    share_latin    = round(mean(has_latin,    na.rm = TRUE), 3),
    share_cyrillic = round(mean(has_cyrillic, na.rm = TRUE), 3),
    first_comment  = format(min(datetime_created, na.rm = TRUE), "%Y-%m-%d"),
    last_comment   = format(max(datetime_created, na.rm = TRUE), "%Y-%m-%d"),
    .groups = "drop"
  ) |>
  arrange(desc(n_comments))

save_csv(author_profile, file.path(OUTPUT_DIR, "author_profile.csv"))
print(author_profile, n = 20)

## 2.3 Частотный словарь (TTR) ----------------------------------------
## Базовый подсчёт без стоп-слов — для первого знакомства с корпусом.
## Для углублённого анализа: подключите tidytext или quanteda.

word_freq <- corpus_ling |>
  filter(!is.na(content)) |>
  mutate(word = str_extract_all(content, "[[:alpha:]]{2,}")) |>
  unnest(word) |>
  mutate(word = str_to_lower(word)) |>
  count(word, sort = TRUE)

save_csv(word_freq, file.path(OUTPUT_DIR, "word_freq.csv"))

cat("\nТоп-30 слов:\n")
print(head(word_freq, 30))

TTR <- n_distinct(word_freq$word) / sum(word_freq$n)
cat("\nType-Token Ratio (TTR):", round(TTR, 4), "\n")

## Сохраняем сводные метрики корпуса одной строкой — удобно для SMW
corpus_summary <- tibble(
  studio_id       = TARGET_STUDIO_ID,
  label           = STUDIO_LABEL,
  n_comments      = nrow(corpus),
  n_authors       = n_distinct(corpus$author_username),
  n_wall          = sum(corpus$source == "wall"),
  n_project       = sum(corpus$source == "project"),
  mean_chars      = round(mean(corpus_ling$n_chars, na.rm = TRUE), 1),
  median_words    = round(median(corpus_ling$n_words, na.rm = TRUE), 1),
  TTR             = round(TTR, 4),
  share_reply     = round(mean(corpus_ling$is_reply, na.rm = TRUE), 3),
  share_latin     = round(mean(corpus_ling$has_latin, na.rm = TRUE), 3),
  share_cyrillic  = round(mean(corpus_ling$has_cyrillic, na.rm = TRUE), 3),
  date_first      = format(min(corpus$datetime_created, na.rm = TRUE), "%Y-%m-%d"),
  date_last       = format(max(corpus$datetime_created, na.rm = TRUE), "%Y-%m-%d"),
  collected_at    = format(Sys.time(), "%Y-%m-%d %H:%M")
)

save_csv(corpus_summary, file.path(OUTPUT_DIR, "corpus_summary.csv"))
print(corpus_summary)

########################################################################
## БЛОК 3. АНАЛИЗ ВЗАИМОДЕЙСТВИЯ (сеть ответов)
########################################################################

## Граф ответов: ребро author_from → author_to
## Экспортируем edge list и node list как CSV — грузятся в Gephi,
## NetworkX, а также встраиваются в SMW-страницы через cargo-таблицы.

if (requireNamespace("igraph", quietly = TRUE)) {
  library(igraph)

  reply_edges <- corpus_ling |>
    filter(is_reply) |>
    left_join(
      corpus_ling |> select(comment_id, reply_to_author = author_username),
      by = c("parent_id" = "comment_id")
    ) |>
    filter(!is.na(reply_to_author)) |>
    count(from = author_username, to = reply_to_author, name = "weight")

  save_csv(reply_edges, file.path(OUTPUT_DIR, "reply_edges.csv"))

  g <- graph_from_data_frame(reply_edges, directed = TRUE)

  cat("\n--- Сеть ответов ---\n")
  cat("Вершины (авторы):", vcount(g), "\n")
  cat("Рёбра (взаимодействия):", ecount(g), "\n")
  cat("Плотность сети:", round(graph.density(g), 4), "\n")

  ## Node list с метриками центральности
  nodes <- tibble(
    author_username = V(g)$name,
    in_degree       = degree(g, mode = "in"),
    out_degree      = degree(g, mode = "out"),
    betweenness     = round(betweenness(g, directed = TRUE), 2)
  ) |>
    arrange(desc(in_degree))

  save_csv(nodes, file.path(OUTPUT_DIR, "reply_nodes.csv"))
  cat("\nТоп-10 по входящим ответам:\n")
  print(head(nodes, 10))

} else {
  message("Пакет igraph не установлен. Пропускаем сетевой анализ.")
  message("Установите: install.packages('igraph')")
}

########################################################################
## БЛОК 4. БАЗОВЫЕ ВИЗУАЛИЗАЦИИ
########################################################################

## 4.1 Динамика комментариев по месяцам --------------------------------
p_time <- corpus_meta |>
  count(ym, source) |>
  ggplot(aes(x = ym, y = n, fill = source)) +
  geom_col(position = "stack") +
  labs(
    title = paste("Динамика комментариев —", STUDIO_LABEL),
    x = NULL, y = "Комментарии", fill = "Источник"
  ) +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

ggsave(file.path(OUTPUT_DIR, "plot_time.png"), p_time, width = 10, height = 5)
print(p_time)

## 4.2 Распределение длины комментариев (log-шкала) -------------------
p_len <- corpus_meta |>
  filter(n_chars > 0) |>
  ggplot(aes(x = n_chars, fill = source)) +
  geom_histogram(bins = 40, alpha = 0.7, position = "identity") +
  scale_x_log10() +
  labs(
    title = "Распределение длины комментариев (символы, log)",
    x = "Длина (log)", y = "Частота", fill = "Источник"
  ) +
  theme_minimal(base_size = 13)

ggsave(file.path(OUTPUT_DIR, "plot_length.png"), p_len, width = 9, height = 5)
print(p_len)

## 4.3 Топ-15 авторов (цвет = доля латиницы) -------------------------
p_authors <- author_profile |>
  head(15) |>
  ggplot(aes(
    x    = reorder(author_username, n_comments),
    y    = n_comments,
    fill = share_latin
  )) +
  geom_col() +
  coord_flip() +
  scale_fill_gradient(low = "steelblue", high = "tomato",
                      name = "Доля\nлатиницы") +
  labs(title = "Топ-15 авторов", x = NULL, y = "Комментарии") +
  theme_minimal(base_size = 13)

ggsave(file.path(OUTPUT_DIR, "plot_authors.png"), p_authors, width = 9, height = 6)
print(p_authors)

## 4.4 Топ-30 слов ----------------------------------------------------
p_words <- word_freq |>
  head(30) |>
  ggplot(aes(x = reorder(word, n), y = n)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = "Топ-30 слов корпуса", x = NULL, y = "Частота") +
  theme_minimal(base_size = 13)

ggsave(file.path(OUTPUT_DIR, "plot_top_words.png"), p_words, width = 9, height = 7)
print(p_words)

########################################################################
## БЛОК 5. СРАВНЕНИЕ ДВУХ И БОЛЕЕ СТУДИЙ
##
## Добавьте строки в studios_meta — и запустите блок заново.
## Каждая студия собирается независимо; итог — единая таблица
## comparison_table.csv в корне data_expedition/.
########################################################################

studios_meta <- tibble::tibble(
  studio_id = c(
    30570341   # ru — добавьте свои студии ниже
    # 26107498,  # en
    # 34175413   # de
  ),
  label = c(
    "ru_lingvo"
    # "en_lingvo",
    # "de_lingvo"
  )
)

collect_studio_summary <- function(studio_id, label, verbose = FALSE) {
  message("=== Собираем студию: ", label, " (", studio_id, ") ===")

  sp  <- get_studio_projects(studio_id, verbose = verbose)
  swc <- get_studio_comments(studio_id, verbose = verbose)
  pc  <- get_all_project_comments_for_studio(studio_id, sp, verbose = verbose)

  corp <- make_corpus(swc, pc, studio_id, label)
  if (is.null(corp) || nrow(corp) == 0) return(NULL)

  corp |>
    mutate(
      n_chars      = nchar(content, allowNA = TRUE),
      n_words      = str_count(content, "\\S+"),
      has_latin    = str_detect(content, "[A-Za-z]"),
      has_cyrillic = str_detect(content, "[А-Яа-яЁё]"),
      is_reply     = !is.na(parent_id)
    ) |>
    summarise(
      studio_id                 = studio_id,
      label                     = label,
      n_comments                = n(),
      n_authors                 = n_distinct(author_username),
      n_projects_with_comments  = n_distinct(project_id, na.rm = TRUE),
      mean_chars                = round(mean(n_chars, na.rm = TRUE), 1),
      median_words              = round(median(n_words, na.rm = TRUE), 1),
      share_reply               = round(mean(is_reply, na.rm = TRUE), 3),
      share_latin               = round(mean(has_latin, na.rm = TRUE), 3),
      share_cyrillic            = round(mean(has_cyrillic, na.rm = TRUE), 3),
      date_first                = format(min(datetime_created, na.rm = TRUE), "%Y-%m-%d"),
      date_last                 = format(max(datetime_created, na.rm = TRUE), "%Y-%m-%d")
    )
}

if (nrow(studios_meta) > 1) {
  comparison_table <- purrr::map2_df(
    studios_meta$studio_id,
    studios_meta$label,
    collect_studio_summary,
    verbose = FALSE
  )

  save_csv(comparison_table, "data_expedition/comparison_table.csv")
  print(comparison_table)

  ## Визуализация: фасеты по 4 ключевым метрикам
  comp_long <- comparison_table |>
    select(label, mean_chars, share_reply, share_latin, share_cyrillic) |>
    pivot_longer(-label, names_to = "metric", values_to = "value")

  p_compare <- ggplot(comp_long, aes(x = label, y = value, fill = label)) +
    geom_col(show.legend = FALSE) +
    facet_wrap(~ metric, scales = "free_y") +
    labs(
      title = "Сравнение студий: лингвистический профиль",
      x = NULL, y = NULL
    ) +
    theme_minimal(base_size = 12) +
    theme(axis.text.x = element_text(angle = 30, hjust = 1))

  ggsave("data_expedition/plot_comparison.png", p_compare, width = 11, height = 7)
  print(p_compare)
}

########################################################################
## БЛОК 6. ШПАРГАЛКА: КАК ОПУБЛИКОВАТЬ НА digida.mgpu.ru (SMW)
##
## Все CSV-файлы совместимы с SemanticMediaWiki напрямую.
##
## Вариант A — Cargo / SMW-таблицы:
##   Загрузите corpus_summary.csv через Special:CargoTables или
##   вставьте данные вручную на wiki-страницу в виде шаблона.
##   Каждая строка author_profile.csv → отдельная wiki-страница
##   вида «Scratch/Автор/<username>» с семантическими свойствами.
##
## Вариант B — статические артефакты:
##   PNG-графики загружаются через Special:Upload и вставляются
##   на страницу курса как [[Файл:plot_time.png|...]].
##
## Вариант C — интерактив (Chart.js / D3):
##   CSV лежат на GitHub (raw.githubusercontent.com).
##   На wiki-странице подключается внешний JS через <script> или
##   расширение TemplateStyles + inline HTML (если разрешено).
##
## Вариант D — Quarto/R Markdown → HTML:
##   quarto render expedition_report.qmd --to html
##   Результирующий HTML публикуется как отдельная страница сайта
##   digida.mgpu.ru или на GitHub Pages курса.
########################################################################

cat("\n=== Экспедиция завершена ===\n")
cat("Файлы сохранены в:", OUTPUT_DIR, "\n\n")
cat("Файлы для публикации на SMW:\n")
cat("  corpus_summary.csv     — одна строка, все метрики студии\n")
cat("  author_profile.csv     — профиль каждого автора\n")
cat("  word_freq.csv          — частотный словарь\n")
cat("  reply_edges.csv        — граф ответов (edge list для Gephi)\n")
cat("  reply_nodes.csv        — центральности авторов (node list)\n")
cat("  plot_*.png             — графики для загрузки на wiki\n")