Как изучить студию с помощью Scratch API: различия между версиями

Материал из Поле цифровой дидактики
Строка 7: Строка 7:
}}
}}
== История ==
== История ==
В среде Scratch есть студии, которые объединяют проекты разных скретчеров. Мы хотим изучить отношения авторов этой студии, используя возможности [[Scrtatch API]]
В среде Scratch есть студии, которые объединяют проекты разных скретчеров. Мы хотим изучить отношения авторов этой студии, используя возможности [[Scratch API]]


* https://scratch.mit.edu/studios/4789981
* https://scratch.mit.edu/studios/4789981

Версия от 11:29, 20 марта 2026

Описание Мы хотим изучить деятельность участников, которые разместили свои проекты внутри конкретной студии Scratch
Область знаний Информатика, Статистика, Моделирование
Область использования (ISTE)
Возрастная категория 14


Поясняющее видео
Близкие рецепту понятия Социограмма, Сетевой анализ
Среды и средства для приготовления рецепта: R, Scratch API, VOSviewer, NetLogo

История

В среде Scratch есть студии, которые объединяют проекты разных скретчеров. Мы хотим изучить отношения авторов этой студии, используя возможности Scratch API


Последовательность исследования

Логика сбора данных

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

В результате мы получаем два слоя данных — внутренний срез конкурсной студии (кто и с какими проектами участвует) и внешний слой окружения этих авторов (в какие ещё студии они выкладывают свои проекты). Это позволяет видеть не только «жизнь внутри конкурса», но и более широкие траектории участия скретчеров в сообществе.

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

 Description
БиграфБиграф. Двудо́льный граф или бигра́ф или bipartite graph — это математический термин теории графов, обозначающий граф, множество вершин которого можно разбить на две части таким образом, что каждое ребро графа соединяет какую-то вершину из одной части с какой-то вершиной другой части, то есть не существует ребра, соединяющего две вершины из одной и той же части.
СоциограммаСоциограмма — способ представления, межличностных и межгрупповых отношений в виде системы связей (графа) между индивидами или социальными группами. Анализ социограммы начинается с отыскания центральных, наиболее влиятельных членов, затем взаимных пар и группировок.

Модели на собранных данных

 Description
DigidaCollab 02 2026Датасет включающий только совместные редактирования статей авторами на площадке Digida
Lens Psych CollabLens Psych Collab - This NetLogo model visualizes the evolution of scientific authorship networks using real publication data from Lens.org. The model tracks the dynamics of collaboration patterns among psychologists from major Russian academic institutions: Moscow City University (MCU), Lomonosov Moscow State University (MSU), and the Russian Academy of Sciences. The model simulates how scientific collaboration networks grow and evolve over time by processing publications chronologically, creating author agents, establishing co-authorship links, and updating network metrics in real-time. It helps researchers understand how new authors enter the scientific community, how experienced researchers maintain collaborations, and how network structure emerges from individual collaboration decisions.
Preferential AttachmentМодель предпочтительного присоединения - Preferential Attachment - Процесс предпочтительного присоединения - это любой из классов процессов, в которых некоторое количество, обычно некоторое форма богатства или кредита распределяется между несколькими людьми или объектами в зависимости от того, сколько они уже имеют, так что те, кто уже богат, получают больше, чем те, кто не богат. «Предпочтительная привязанность» - это лишь последнее из многих названий, которые были даны таким процессам. Они также упоминаются как «богатые становятся богаче». Процесс предпочтительного присоединения генерирует распределение «с длинным хвостом » после распределения Парето или степенной закон в его хвосте. Это основная причина исторического интереса к предпочтительной привязанности: распределение видов и многие другие явления наблюдаются эмпирически, следуя степенным законам, и процесс предпочтительной привязанности является ведущим механизмом для объяснения этого поведения. Предпочтительное прикрепление считается возможным основанием для распределения размеров городов, богатства чрезвычайно богатых людей, количества цитирований, полученных научными публикациями, и количества ссылок на страницы во всемирной паутине.
  • 120px-Pref_attachm.png