Я застрял в эффективном вычислении количества одноклассников для каждого студента из базы данных уровня курса.
Рассмотрим этот 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)
)
Два ученика являются одноклассниками, если у них хотя бы один раз одинаковое значение semester
и course
. Итак, Студент 1 является одноклассником Студента 2 и Студента 4, потому что они вместе ходили на курс 2 в течение первого семестра. Студент 1 также является одноклассником Студента 5, поскольку они вместе ходили на курс 4 во втором семестре.
Это может потребовать много памяти, но, надеюсь, это шаг в правильном направлении.
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
работает отлично и единственный, который правильно работает в высших измерениях. Спасибо!
на самом деле вы можете использовать ego_size(g, 2, v, mindist = 2)
вместо того, чтобы дважды звонить ego_size
, что должно немного ускориться
Спасибо! Мне казалось, что я помню эту опцию, но я не заметил ее, просматривая список аргументов.
Вот подход, аналогичный подходу Эррина. Я создаю список всех учеников в каждом классе, объединяю их для каждого ученика и подсчитываю уникальных.
Подробные инструкции смотрите в комментариях.
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
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)
g %>%
bipartite_projection(which = "true") %>%
plot()
такой, что
degree
вершин. Например, вершина 1
(студент 1
) имеет степень 3 (связана с вершинами 2
, 4
и 5
) и так далее.Я думаю, что найти более быстрый non-igraph
рабочий метод непросто.
@clp нет, это непросто, но, возможно, использование Rcpp
поможет
Это продолжение ответа @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 Я использовал transform
только для вызова переменной student
из data.frame(student = unique(dat$student))
. поэтому мне не нужно создавать объект для этого фрейма данных
@clp, у тебя есть student
откуда-то еще? в моем коде это от data.frame(student = unique(dat$student))
Используйте unlist(..., use.names = FALSE)
и переместите -1 за пределы сапли. Это даст значительное улучшение: со 175 до 70 секунд.
@clp да, это действительно улучшается, но лучше, чем я думал! Спасибо Вам за Ваш вклад.
На каком основании Студент 1 имеет в качестве однокурсников Студентов 2, 4 и 5 в течение обоих семестров? Я не сразу вижу эту связь в наборе данных.