Анализ лингвистического корпуса Scratch студии: различия между версиями
Материал из Поле цифровой дидактики
Patarakin (обсуждение | вклад) Новая страница: « == Скрипт R == * Обратите внимание, '''Scratch_API_core.R''' размещен в статье Как оценить командность в студиях Scratch ---- Категория:lesson» |
Patarakin (обсуждение | вклад) |
||
| (не показано 5 промежуточных версий этого же участника) | |||
| Строка 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 == | ||
* Обратите внимание, '''Scratch_API_core.R''' размещен в статье [[Как оценить командность в студиях Scratch]] | * Обратите внимание, '''Scratch_API_core.R''' размещен в статье [[Как оценить командность в студиях Scratch]] | ||
<syntaxhighlight lang="R" line> | |||
######################################################################## | |||
## 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") | |||
</syntaxhighlight> | |||
---- | ---- | ||
[[Категория:lesson]] | [[Категория:lesson]] | ||
Текущая версия от 11:00, 5 мая 2026
Корпусная лингвистика на практике
- сборка корпуса из разнородных источников (стена студии + комментарии к проектам)
- разметка текстов: 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
- Обратите внимание, Scratch_API_core.R размещен в статье Как оценить командность в студиях Scratch
########################################################################
## 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")
