Эффективно находите количество разных одноклассников на основе данных уровня курса

Я застрял в эффективном вычислении количества одноклассников для каждого студента из базы данных уровня курса.

Рассмотрим этот data.frame, где каждая строка представляет курс, который студент прошел в течение данного семестра:

dat <- 
  data.frame(
  student = c(1, 1, 2, 2, 2, 3, 4, 5),
  semester = c(1, 2, 1, 2, 2, 2, 1, 2),
  course = c(2, 4, 2, 3, 4, 3, 2, 4)
)

#   student semester course
# 1       1        1      2
# 2       1        2      4
# 3       2        1      2
# 4       2        2      3
# 5       2        2      4
# 6       3        2      3
# 7       4        1      2
# 8       5        2      4

Студенты собираются на курсы в данном семестре. Их одноклассниками являются другие студенты, посещающие тот же курс в том же семестре. Например, в течение обоих семестров у студента 1 было 3 одноклассника (студенты 2, 4 и 5).

Как я могу получить количество уникальных одноклассников, которые есть у каждого студента за оба семестра? Желаемый результат будет:

  student n
1       1 3
2       2 4
3       3 1
4       4 2
5       5 2

где n — количество разных одноклассников, которые были у ученика в течение учебного года.

Я чувствую, что решение igraph могло бы сработать (отсюда и тег), но мои знания об этом пакете слишком ограничены. Мне также кажется, что использование joins может помочь, но опять же, я не знаю, как это сделать.

Важно отметить, что я бы хотел, чтобы это работало для больших наборов данных (у меня около 17 миллионов строк). Вот пример набора данных:

set.seed(1)
big_dat <- 
  data.frame(
    student = sample(1e4, 1e6, TRUE),
    semester = sample(2, 1e6, TRUE),
    course = sample(1e3, 1e6, TRUE)
  )

На каком основании Студент 1 имеет в качестве однокурсников Студентов 2, 4 и 5 в течение обоих семестров? Я не сразу вижу эту связь в наборе данных.

Ifeanyi Idiaye 09.07.2024 19:10

Два ученика являются одноклассниками, если у них хотя бы один раз одинаковое значение semester и course. Итак, Студент 1 является одноклассником Студента 2 и Студента 4, потому что они вместе ходили на курс 2 в течение первого семестра. Студент 1 также является одноклассником Студента 5, поскольку они вместе ходили на курс 4 во втором семестре.

Maël 09.07.2024 19:12
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
2
273
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Это может потребовать много памяти, но, надеюсь, это шаг в правильном направлении.

library(data.table)
library(tictoc)

tic()

# Assume 1-n ids for the students
dat <- big_dat
n <- max(dat$student)
classmate <- matrix(FALSE, nrow=n, ncol=n)
setDT(dat)
grps <- dat[, .(list(student)), .(semester,course)][, V1]
for (g in grps) {
  for (i in g) {
    classmate[i, g] <- TRUE
  }
}
# if -1 student not present / id missing
data.table(student = seq_len(n), n = rowSums(classmate)-1)

toc()
# 6.48 sec elapsed
Ответ принят как подходящий

Сначала попробуйте с igraph:

library(data.table)
library(igraph)

setDT(dat)
i <- max(dat$student)
g <- graph_from_data_frame(
  dat[,.(student, class = .GRP + i), .(semester, course)][,-1:-2]
)
v <- V(g)[1:uniqueN(dat$student)]
data.frame(student = as.integer(names(v)),
           n = ego_size(g, 2, v, mindist = 2))
#>   student n
#> 1       1 3
#> 2       2 4
#> 3       4 2
#> 4       5 2
#> 5       3 1

Обратите внимание: если student не является целым числом, вам нужно будет создать временный целочисленный идентификатор с match в уникальном значении, а затем индексировать его в конечном результате.

С tcrossprod:

library(data.table)
library(Matrix)

setDT(dat)
u <- unique(dat$student)
data.frame(
  student = u,
  n = colSums(
    tcrossprod(
      dat[,id := match(student, u)][
        ,.(i = id, j = .GRP), .(semester, course)
      ][,sparseMatrix(i, j)]
    )
  ) - 1L
)
#>   student n
#> 1       1 3
#> 2       2 4
#> 3       3 1
#> 4       4 2
#> 5       5 2

Ваш вариант tcrossprod работает отлично и единственный, который правильно работает в высших измерениях. Спасибо!

Maël 10.07.2024 10:18

на самом деле вы можете использовать ego_size(g, 2, v, mindist = 2) вместо того, чтобы дважды звонить ego_size, что должно немного ускориться

ThomasIsCoding 10.07.2024 15:59

Спасибо! Мне казалось, что я помню эту опцию, но я не заметил ее, просматривая список аргументов.

jblood94 10.07.2024 17:14

Вот подход, аналогичный подходу Эррина. Я создаю список всех учеников в каждом классе, объединяю их для каждого ученика и подсчитываю уникальных.
Подробные инструкции смотрите в комментариях.

dat <- 
   data.frame(
      student = c(1, 1, 2, 2, 2, 3, 4, 5),
      semester = c(1, 2, 1, 2, 2, 2, 1, 2),
      course = c(2, 4, 2, 3, 4, 3, 2, 4)
   )

#create a data frame with a list of the students in each class
classes <- dat %>% group_by(semester, course) %>% summarize(otherstudents = n(), s=list(student))

#join the student information onto each class (one to many join)
newdat <- left_join(classes, dat, join_by(semester, course))
#loop through each student
classmates <-sapply(sort(unique(newdat$student)), function(i){
   #find the classes the student is taking, merge together the students list 
   #find the unqiue students
   #count the list and substrate 1 for the original student
   unlist(newdat$s[newdat$student ==i]) %>% unique() %>% length()- 1
})

answer <- data.frame(student= sort(unique(newdat$student)), n=classmates)

Эту проблему можно решить с помощью igraph, но я не думаю, что вам это действительно нужно, например.

dat %>%
    mutate(gid = cur_group_id(), .by = -student) %>%
    select(student, gid) %>%
    {
        rev(stack(rowSums(tcrossprod(table(.)) > 0) - 1))
    } %>%
    setNames(c("student", "n"))

и вы получите

  student n
1       1 3
2       2 4
3       3 1
4       4 2
5       5 2

но я не уверен в его эффективности, возможно, потребуются дополнительные эксперименты.

Вот решение igraph с использованием bipartite_projection() и degree(), если вам интересно.

# simplify the raw dataset
df <- dat %>%
    distinct() %>%
    mutate(gid = paste0(semester, ",", course)) %>%
    select(student, gid)

# create a bipartite graph
g <- df %>%
    graph_from_data_frame() %>%
    set_vertex_attr("type", value = names(V(.)) %in% unique(df$student))

# bipartite projection
out <- g %>%
    bipartite_projection(which = "true") %>%
    degree() %>%
    {
        data.frame(
            student = as.integer(names(.)),
            n = .
        )
    }

который дает

  student n
1       1 3
2       2 4
3       3 1
4       4 2
5       5 2

Объяснение

  1. Построить двудольный граф
g <- dat %>%
    distinct() %>%
    mutate(gid = paste0(semester, ",", course)) %>%
    select(student, gid) %>%
    graph_from_data_frame() %>%
    set_vertex_attr("type", value = names(V(.)) %in% unique(dat$student))

и его визуализация выглядит так

g %>%
    plot(layout = layout_as_bipartite, vertex.color = V(g)$type)

  1. Получите двудольные проекции
g %>%
    bipartite_projection(which = "true") %>%
    plot()

такой, что

  1. Связь между вершинами характеризуется degree вершин. Например, вершина 1 (студент 1) имеет степень 3 (связана с вершинами 2, 4 и 5) и так далее.

Я думаю, что найти более быстрый non-igraph рабочий метод непросто.

clp 12.07.2024 22:13

@clp нет, это непросто, но, возможно, использование Rcpp поможет

ThomasIsCoding 13.07.2024 10:17

Это продолжение ответа @jblood94. Он опирается только на встроенные функции R и igraph. Об учениках ничего не предполагается ids.

dat <-
  data.frame(
  student  = c(1, 1, 2, 2, 2, 3, 4, 5),
  semester = c(1, 2, 1, 2, 2, 2, 1, 2),
  course   = c(2, 4, 2, 3, 4, 3, 2, 4)
  )

library(igraph)
# Simplify input data.
ddf <- data.frame(student=dat$student, lectures =  paste0("L", dat$course, dat$semester))

# An edge from s to l means: student s attended lecture l.
# Make sure students come first.
# Make sure names and vertex indexes match if numeric.
g <- graph_from_data_frame(ddf, vertices = c(unique(ddf$student), unique(ddf$lectures)), directed=TRUE)

n_students <- length(unique(ddf$student))
v          <- V(g)[seq_len(n_students)]

system.time(
  answers <-
    data.frame(
      student = names(v),
            n = ego_size(g, 2, v, mindist = 2)
    )
)
head(answers)

# big data
#   user  system elapsed 
#   2.59    0.00    2.58 

Отредактируйте, чтобы добавить решение с помощью собственных функций igraph.

# ---------------------------------------------------------------------
library(igraph)
# Prepare input data.
students   <- dat$student
lectures   <- paste0("L", dat$course, dat$semester)
studentIds <- unique(students)
lectureIds <- unique(lectures)
n_sl       <- length(studentIds) + length(lectureIds)

# An edge from s to l means: student s attended lecture l.
g <- 
make_empty_graph(n_sl, directed=TRUE) %>%
  set_vertex_attr(name = "name", value=c(studentIds, lectureIds))  %>%
    add_edges(rbind(students, lectures))

system.time(
answer <- setNames(ego_size(g, 2, V(g)[studentIds], mindist = 2),
                   studentIds)
)
head(answer)

Изменить, чтобы добавить двустороннюю проекцию

V(g)$type <- bipartite_mapping(g)$type
plot(g, layout=layout_as_bipartite)
system.time(bp <- bipartite_projection(g, which = "false"))
degree(bp)
#
#  big data
#  user  system elapsed 
#  52.38    4.32   56.84

Не-igraph решение

lst <- with(dat, split(student, paste(semester, course)))
transform(
    data.frame(student = unique(dat$student)),
    n = sapply(student, \(s) sum(!duplicated(unlist(Filter(\(x) s %in% x, lst), use.names = FALSE)))) - 1
)

дает

  student n
1       1 3
2       2 4
3       3 1
4       4 2
5       5 2

но применительно к большим данным это может быть медленным из-за внутренних вложенных циклов.

Действительно медленно, но не совсем непрактично. Краткое и интересное решение. Я не совсем понимаю роль transform(). На моей машине это занимает 175 секунд вместо примерно 2,5 секунд у igraph.

clp 13.07.2024 13:03

@clp Я использовал transform только для вызова переменной student из data.frame(student = unique(dat$student)). поэтому мне не нужно создавать объект для этого фрейма данных

ThomasIsCoding 13.07.2024 13:50

@clp, у тебя есть student откуда-то еще? в моем коде это от data.frame(student = unique(dat$student))

ThomasIsCoding 13.07.2024 14:35

Используйте unlist(..., use.names = FALSE) и переместите -1 за пределы сапли. Это даст значительное улучшение: со 175 до 70 секунд.

clp 13.07.2024 20:05

@clp да, это действительно улучшается, но лучше, чем я думал! Спасибо Вам за Ваш вклад.

ThomasIsCoding 13.07.2024 21:40

Другие вопросы по теме