Quero designar alunos da coorte de 2024 (n=105, mulheres=61, homens=44) em grupos para 2025. A distribuição que espero alcançar (em ordem de importância):
- não mais do que dois alunos de um grupo de 2024 podem ser transferidos para o mesmo grupo de 2025
- Os grupos de 2025 devem ser o mais equilibrados possível em termos de gênero, algo como 7 ou 8 mulheres e 5 ou 6 homens por grupo
- Os tamanhos dos grupos de 2025 devem ser o mais uniformes possível, idealmente 7 x 13 e 1 x 14
- sempre que possível, a distribuição deve ser aleatória
Dado que uma solução pode não ser possível se os critérios 1 a 3 forem rigorosamente respeitados, há alguma flexibilidade em relação ao tamanho do grupo e à aleatoriedade.
Os tamanhos e o equilíbrio de gênero para os grupos de 2024 são relativamente desiguais:
table(df[, c("Gender", "g2024")])
# g2024
# Gender 1 2 3 4 5 6 7 8
# Female 7 7 7 7 9 7 9 8
# Male 4 4 8 6 5 7 4 6
table(df[, "g2024"])
# g2024
# 1 2 3 4 5 6 7 8
# 11 11 15 13 14 14 13 14
Usando valores pré-determinados, um exemplo rudimentar do que estou tentando alcançar:
# Create random vector with group ids for females per group, assign to g2025
set.seed(42)
grp_fem <- as.character(rep(1:8, sample(c(rep(7, 3), rep(8, 5)), 8)))
df$g2025 <- unlist(lapply(1:nrow(df), function(i) {
if (df$Gender[i] == "Female") {
x <- sample(grp_fem, 1)
grp_fem <<- grp_fem[-match(x, grp_fem)]
return(x)
} else {
return(NA)
}
}))
# Get males per group, change one group length so y sums to male count
x <- as.integer(table(df$g2025))
y <- 13 - x
z <- sample(which(y == 5), 1)
y[z] <- 6
# Create vector with group ids for males per group, assign to g2025
grp_mal <- as.character(rep(1:8, rep(y)))
df$g2025 <- unlist(lapply(1:nrow(df), function(i) {
if (df$Gender[i] == "Male") {
x <- sample(grp_mal, 1)
grp_mal <<- grp_mal[-match(x, grp_mal)]
return(x)
} else {
return(df$g2025[i])
}
}))
# Gender distribution per group
table(df[, c("Gender", "g2025")])
# g2025
# Gender 1 2 3 4 5 6 7 8
# Female 7 8 8 8 7 8 7 8
# Male 6 5 5 5 6 6 6 5
# Number of students carried from g2024 to g2025 groups
table(df[, c("g2024", "g2025")])
# g2025
# g2024 1 2 3 4 5 6 7 8
# 1 0 2 1 2 1 2 3 0
# 2 1 3 2 2 0 0 1 2
# 3 2 2 1 2 3 2 1 2
# 4 1 1 2 1 2 2 2 2
# 5 2 1 4 0 2 3 1 1
# 6 5 3 1 2 0 0 2 1
# 7 1 1 0 1 2 2 1 5
# 8 1 0 2 3 3 3 2 0
Os tamanhos de grupo predeterminados e os valores de distribuição de gênero obviamente não são ideais e não levam em conta quantos alunos de um determinado grupo de 2024 acabarão no mesmo grupo de 2025.
Tentei usar uma matriz com uma distribuição quase aleatória de alocação de grupo para grupo:
m <- structure(c(1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 1, 2,
1, 2, 2, 2, 1, 1, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2,
1, 2, 1, 2, 2, 2, 1, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2,
1, 2, 2, 1, 2), dim = c(8L, 8L))
rowSums(m)
# [1] 11 11 15 13 14 14 13 14
colSums(m)
# [1] 13 13 13 13 13 13 13 14
mas isso também requeria colSums()
valores predefinidos e alguma manipulação manual. Suspeito que o lpSolve
pacote pode ser capaz de lidar com algo assim, mas não consegui entender como usá-lo.
Dados:
df <- structure(list(Student = c("1", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18",
"19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40",
"41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51",
"52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62",
"63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73",
"74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84",
"85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95",
"96", "97", "98", "99", "100", "101", "102", "103", "104", "105"
), Gender = c("Female", "Male", "Male", "Male", "Female", "Female",
"Male", "Female", "Female", "Female", "Male", "Male", "Male",
"Female", "Male", "Female", "Male", "Female", "Female", "Female",
"Male", "Male", "Female", "Female", "Female", "Male", "Male",
"Male", "Female", "Female", "Male", "Female", "Female", "Female",
"Male", "Female", "Female", "Female", "Female", "Male", "Male",
"Female", "Female", "Female", "Female", "Male", "Female", "Male",
"Female", "Female", "Female", "Female", "Male", "Female", "Male",
"Female", "Male", "Male", "Male", "Female", "Female", "Female",
"Female", "Female", "Female", "Male", "Female", "Female", "Male",
"Male", "Female", "Female", "Male", "Female", "Male", "Female",
"Female", "Male", "Female", "Female", "Female", "Male", "Female",
"Male", "Female", "Female", "Male", "Female", "Male", "Male",
"Male", "Male", "Male", "Female", "Male", "Female", "Female",
"Male", "Female", "Male", "Female", "Female", "Male", "Male",
"Female"), g2024 = c("4", "3", "3", "8", "2", "8", "4", "5",
"8", "7", "2", "4", "4", "6", "6", "5", "1", "3", "7", "2", "6",
"8", "2", "8", "1", "5", "8", "3", "3", "1", "5", "5", "1", "3",
"8", "6", "1", "7", "5", "5", "1", "7", "4", "7", "5", "4", "8",
"6", "3", "1", "7", "8", "7", "7", "2", "4", "8", "3", "7", "1",
"6", "3", "6", "8", "2", "3", "3", "5", "7", "2", "2", "2", "8",
"6", "1", "1", "4", "1", "6", "8", "2", "6", "5", "2", "5", "3",
"3", "7", "3", "4", "3", "4", "7", "4", "6", "8", "4", "6", "6",
"6", "5", "4", "5", "5", "7"), g2025 = c(NA, NA, NA, NA, NA,
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,
NA, NA, NA, NA)), row.names = c(NA, -105L), class = c("tbl_df",
"tbl", "data.frame"))
Não acho que
lpSolve
funcionaria aqui porque requer uma função linear, enquanto acho que isso terá que ser não linear. Esta tabela de pacotes de otimização da Data Science for Production and Logistics não mostra nenhum pacote R que possa lidar com problemas de otimização discretos e não lineares, e para ser honesto, a lista mais abrangente vinculada lá me assusta.No entanto, isso me parece uma aplicação perfeita para recozimento simulado, então implementei o algoritmo simples de recozimento simulado fornecido na página da Wikipedia .
Mais fácil do que descobrir como usar um novo pacote, na minha opinião, e ainda precisaríamos fazer a próxima parte de definir a função de otimização:
Então podemos executar o recozimento simulado (com parâmetros encontrados por tentativa e erro):
E veja como foi:
A partir dessas tabelas, podemos ver que os critérios 1-3, respectivamente, são satisfeitos. Tecnicamente, essa abordagem dá maior prioridade ao #3 do que ao #1 ou #2, mas como todos são satisfeitos, isso não importa para esses dados, e seria simples mudar isso se importasse. Quanto ao critério #4, os agrupamentos são aleatórios, na medida em que o palpite inicial foi aleatório e a caminhada pela vizinhança foi aleatória (embora guiada).
E como nota de rodapé, aqui está uma versão
anneal
com uma opção para imprimir informações de diagnóstico: