Как изучить студию с помощью Scratch API
Материал из Поле цифровой дидактики
| Описание | Мы хотим изучить деятельность участников, которые разместили свои проекты внутри конкретной студии Scratch |
|---|---|
| Область знаний | Информатика, Статистика, Моделирование |
| Область использования (ISTE) | |
| Возрастная категория | 14
|
| Поясняющее видео | |
| Близкие рецепту понятия | Социограмма, Сетевой анализ |
| Среды и средства для приготовления рецепта: | R, Scratch API, VOSviewer, NetLogo |
История
В среде Scratch есть студии, которые объединяют проекты разных скретчеров. Мы хотим изучить отношения авторов этой студии, используя возможности Scrtatch API
Последовательность исследования
Логика сбора данных
- Perplexy.AI
- PlantUML

Логика исследования

В результате мы получаем два слоя данных — внутренний срез конкурсной студии (кто и с какими проектами участвует) и внешний слой окружения этих авторов (в какие ещё студии они выкладывают свои проекты). Это позволяет видеть не только «жизнь внутри конкурса», но и более широкие траектории участия скретчеров в сообществе.
R скрипт сбора данных
###############################################
# Анализ авторов и студий Scratch на R
# (пример на студии 4789981 — Collab Challenge)
###############################################
# Пакеты ----
library(httr)
library(jsonlite)
library(dplyr)
library(purrr)
library(tidyr)
library(igraph)
###############################################
# 1. Функция для получения проектов студии
###############################################
# Использует API:
# https://api.scratch.mit.edu/studios/<studio_id>/projects?limit=&offset=
# Возвращает data.frame:
# project_id, title, creator_id, username
get_studio_projects <- function(studio_id, limit = 40, verbose = TRUE) {
offset <- 0
all_projects <- list()
page <- 1
repeat {
url <- paste0(
"https://api.scratch.mit.edu/studios/",
studio_id,
"/projects?limit=", limit,
"&offset=", offset
)
if (verbose) message("Studio ", studio_id,
": page ", page,
" (offset = ", offset, ")")
resp <- GET(url)
if (status_code(resp) != 200) {
warning("Non-200 status code: ", status_code(resp),
" at offset ", offset)
break
}
txt <- content(resp, as = "text", encoding = "UTF-8")
dat <- fromJSON(txt, flatten = TRUE)
if (length(dat) == 0) break
all_projects[[length(all_projects) + 1]] <- dat
if (nrow(dat) < limit) break
offset <- offset + limit
page <- page + 1
}
if (length(all_projects) == 0) return(NULL)
bind_rows(all_projects) |>
transmute(
project_id = id,
title = title,
creator_id = creator_id,
username = username
) |>
distinct()
}
###############################################
# 2. Проекты пользователя
###############################################
# API:
# https://api.scratch.mit.edu/users/<username>/projects?limit=&offset=
# Возвращает data.frame:
# project_id, title, username
get_user_projects <- function(username, limit = 40, max_pages = 10, verbose = TRUE) {
offset <- 0
all <- list()
page <- 1
repeat {
if (page > max_pages) break
url <- paste0(
"https://api.scratch.mit.edu/users/",
username,
"/projects?limit=", limit,
"&offset=", offset
)
if (verbose) message("User ", username,
": page ", page,
" (offset = ", offset, ")")
resp <- GET(url)
if (status_code(resp) != 200) break
txt <- content(resp, as = "text", encoding = "UTF-8")
dat <- fromJSON(txt, flatten = TRUE)
if (length(dat) == 0) break
all[[length(all) + 1]] <- dat
if (nrow(dat) < limit) break
offset <- offset + limit
page <- page + 1
}
if (length(all) == 0) return(NULL)
res <- bind_rows(all)
res |>
transmute(
project_id = id,
title = title,
username = username # аргумент функции
) |>
distinct()
}
###############################################
# 3. Студии одного проекта пользователя
###############################################
# ВАЖНО: используем путь:
# https://api.scratch.mit.edu/users/<username>/projects/<project_id>/studios
# а не /projects/<id>/studios, потому что второй вариант
# часто не работает.
#
# Возвращает data.frame:
# studio_id, studio_name
###############################################
get_project_studios <- function(username, project_id,
limit = 40, max_pages = 10,
verbose = TRUE) {
user_name <- as.character(username)[1]
offset <- 0
all <- list()
page <- 1
repeat {
if (page > max_pages) break
url <- paste0(
"https://api.scratch.mit.edu/users/",
user_name,
"/projects/",
project_id,
"/studios?limit=", limit,
"&offset=", offset
)
if (verbose) {
message("User ", user_name,
", project ", project_id,
": page ", page,
" (offset = ", offset, ")")
}
resp <- GET(url)
if (status_code(resp) != 200) {
return(NULL)
}
txt <- content(resp, as = "text", encoding = "UTF-8")
dat <- fromJSON(txt, flatten = TRUE)
if (length(dat) == 0) break
all[[length(all) + 1]] <- dat
if (nrow(dat) < limit) break
offset <- offset + limit
page <- page + 1
}
if (length(all) == 0) return(NULL)
res <- bind_rows(all)
if (!("id" %in% names(res)) || !("title" %in% names(res))) return(NULL)
res |>
transmute(
studio_id = id,
studio_name = title
) |>
distinct()
}
###############################################
# 4. Автор → его проекты → студии этих проектов
###############################################
# Возвращает data.frame:
# username, project_id, project_title, studio_id, studio_name
get_user_project_studios <- function(username, verbose = TRUE) {
user_projects <- get_user_projects(username, verbose = verbose)
if (is.null(user_projects) || nrow(user_projects) == 0) return(NULL)
proj_studios <- user_projects |>
mutate(
studios = map(
project_id,
~ get_project_studios(username, .x, verbose = verbose)
)
)
proj_studios_nonempty <- proj_studios |>
filter(!map_lgl(studios, is.null))
if (nrow(proj_studios_nonempty) == 0) return(NULL)
proj_studios_nonempty |>
tidyr::unnest(cols = studios) |>
transmute(
username,
project_id,
project_title = title,
studio_id,
studio_name
) |>
distinct()
}
###############################################
# 5. От студии → к авторам → к студиям авторов
###############################################
# Возвращает список:
# $studio_projects — проекты внутри исходной студии
# $author_studios — студии, где встречаются проекты её авторов
get_studio_author_studios <- function(studio_id, verbose = TRUE) {
# Проекты студии
studio_projects <- get_studio_projects(studio_id, verbose = verbose)
if (is.null(studio_projects) || nrow(studio_projects) == 0) return(NULL)
# Авторы этих проектов
authors <- unique(studio_projects$username)
# Для каждого автора — его проекты и студии этих проектов
author_studios <- purrr::map_df(
authors,
~ {
if (verbose) message("Author: ", .x)
get_user_project_studios(.x, verbose = verbose)
}
)
list(
studio_projects = studio_projects,
author_studios = author_studios
)
}
###############################################
# 6. Пример: анализ студии 4789981
###############################################
# 6.1. Собираем данные
res_4789981 <- get_studio_author_studios(4789981, verbose = TRUE)
studio_projects <- res_4789981$studio_projects
author_studios <- res_4789981$author_studios
# 6.2. Активность авторов внутри студии:
authors_in_4789981 <- studio_projects |>
count(username, name = "projects_in_studio") |>
arrange(desc(projects_in_studio))
head(authors_in_4789981)
# 6.3. Пересечения студий по авторам и проектам:
studio_author_counts <- author_studios |>
group_by(studio_id, studio_name) |>
summarise(
authors_from_4789981 = n_distinct(username),
projects_from_4789981 = n_distinct(project_id),
.groups = "drop"
) |>
arrange(desc(authors_from_4789981))
head(studio_author_counts, 20)
###############################################
# 7. Построение двудольного графа автор–студия
###############################################
# Узлы: авторы и студии.
# Рёбра: автор ↔ студия (если хотя бы один проект автора в студии).
author_studios_simple <- author_studios |>
distinct(username, studio_id, studio_name)
# Узлы-авторы
author_nodes <- author_studios_simple |>
distinct(name = username) |>
mutate(
type = "author",
label = name
)
# Узлы-студии
studio_nodes <- author_studios_simple |>
distinct(
studio_id,
studio_name
) |>
transmute(
name = as.character(studio_id),
type = "studio",
label = studio_name
)
nodes <- bind_rows(author_nodes, studio_nodes)
# Рёбра: автор → студия
edges <- author_studios_simple |>
transmute(
from = username,
to = as.character(studio_id)
)
# Граф
g_bip <- graph_from_data_frame(
d = edges,
vertices = nodes,
directed = FALSE
)
# Простейший просмотр структуры:
g_bip
vcount(g_bip)
ecount(g_bip)
table(V(g_bip)$type)
###############################################
# 8. Экспорт nodes и edges в CSV
# (для VOSviewer, NetLogo, Gephi и др.)
###############################################
nodes_export <- tibble(
id = V(g_bip)$name,
type = V(g_bip)$type,
label = V(g_bip)$label
)
edges_export <- as_data_frame(g_bip, what = "edges") |>
as_tibble() |>
rename(
from = from,
to = to
)
write.csv(nodes_export,
"scratch_studios_nodes.csv",
row.names = FALSE,
fileEncoding = "UTF-8")
write.csv(edges_export,
"scratch_studios_edges.csv",
row.names = FALSE,
fileEncoding = "UTF-8")
###############################################
# 9. Простая визуализация в R
###############################################
# Быстрый "черновой" рисунок:
plot(g_bip,
vertex.size = ifelse(V(g_bip)$type == "studio", 6, 3),
vertex.color = ifelse(V(g_bip)$type == "studio", "tomato", "skyblue"),
vertex.label = NA,
edge.color = "grey80")
Данные студии
Ребра
| От кого | К кому |
|---|---|
| timur1985 | 4789981 |
| timur1985 | 33969674 |
| timur1985 | 36279833 |
Узлы
Визуализация
VOSviewer

