Primeiramente vamos importar as bibliotecas necessárias para esse script ser executado.
library(dplyr)
library(reshape2)
library(GGally)
library(ggplot2)
library(corrplot)
library(caret)
library(hydroGOF)
Este problema tem por objetivo exercitar os conceitos sobre sistema de recomendação vistos em sala. O cenário de recomendação é o seguinte:
Um aluno (e.g. do quarto período relativo) vai realizar sua matrícula. Nesse momento, o sistema realiza uma predição das notas das disciplinas disponíveis para matrícula desse aluno.
A ideia é usar filtragem colaborativa baseada no usuário para:
Para preparar e transformar os dados foram usadas as funções e variáveis auxiliares apresentadas nesta seção.
COL_DADOS = c("matricula", "ano", "periodo", "AL", "AV", "C1", "C2", "ES", "EDA",
"FC", "FM", "GI", "IC", "LEDA", "LOAC", "LP1", "LP2", "LPT", "LM",
"MD", "ME", "OAC", "PLP", "PE", "P1", "P2", "SI1", "TC", "TG")
COL_QUARTO_PER = c("EDA", "GI", "LEDA", "ME", "PE", "PLP", "TC")
# -------------------- Collaborative Filtering -------------------------------
K = 10
NEIGH = 0.7
# retorna uma matriz de correlação usando cor. de pearson
get_sim <- function(df) {
row.names(df) <- df$matricula
df <- df %>% subset(select=-c(matricula))
inv_df <- as.data.frame(t(df))
res <- cor(inv_df[sapply(inv_df, is.numeric)], use="p", method='pearson')
return(res);
}
# retorna um vector de Named num, cujo nome é a matrícula e o valor a similaridade
get_neigh <- function(df, index, corr) {
matr <- as.character(df[index, 1])
# todos os vizinhos, porém temos que "invalidar" ele mesmo
corr[matr, matr] = 0
all_neigh <- corr[matr, ]
k_neigh <- sort(all_neigh, decreasing = T)[1:K]
return(k_neigh);
}
# calcula score ignorando vizinhos com NAs
get_score <- function(df, k_neigh, item) {
notas <- subset(df, matricula %in% names(k_neigh))
# removendo vizinhos que não possuem notas
notas <- na.omit(notas)
# se todas as notas dos vizinhos forem NAs ou nenhum vizinho com
# nota tenha similaridade > NEIGH consideramos que esse aluno
# não tem vizinhos
if(nrow(notas) == 0) {
return(NA)
}
# atualizando similaridade
notas$sim <- 0
for(i in 1:length(notas$matricula)) {
notas$sim[i] <- k_neigh[as.character(notas$matricula[i])]
}
# se todas as notas dos vizinhos forem NAs ou nenhum vizinho com
# nota tenha similaridade > NEIGH consideramos que esse aluno
# não tem vizinhos
eh_valido <- notas[notas$sim > NEIGH,]
if(nrow(eh_valido) == 0) {
return(NA)
}
# print(item)
# print(notas[, item])
# print(sum(notas[, item] * notas$sim) / sum(notas$sim))
res <- sum(notas[, item] * notas$sim) / sum(notas$sim)
return(res)
}
Para preparação dos dados foi tomada a seguinte decisão: 5% dos dados serão escolhidos como dados de teste e portanto os valores das suas notas originais foram salvas para serem comparadas com a predição.
# recebe dados
dados <- read.csv('~/DataAnalysis2/lab4/data.csv')
# remove NAs
# dados <- na.omit(dados)
# renomeando colunas
colnames(dados) <- COL_DADOS
# separando em teste e treino
temp <- createDataPartition(dados$ano, p = 0.95, list = F)
# removendo colunas de ano e período
dados <- dados %>% subset(select=-c(ano, periodo))
# os dados de teste são zerados, copiados para uma nova tabela
# zerando
teste <- dados
for(i in 1:length(COL_QUARTO_PER)) {
teste[-temp,][COL_QUARTO_PER[i]] <- NA;
}
# copiando
teste_valores <- teste[-temp, ]
teste_indices <- rownames(teste_valores)
Nesta seção serão realizadas as seguintes atividades:
Encontrar os alunos mais similares, em termos de disciplinas cursadas e notas tiradas nessas disciplinas, aos usuários-alvo da recomendação;
A partir desses vizinhos realizar as predições de notas das disciplinas que os usuários-alvo se matricularam.
Utilizei K = 10 (Segundo o professor K = 5 ou K = 10 geralmente são bons valores) e considerei que os vizinhos não são “válidos” caso nenhum dos vizinhos com notas disponíveis apresentem similaridade > 0.7. Para indicar a não existência da predição foi utilizado NA.
# calcula a similaridade entre todos os alunos (de todos para todos)
corr <- teste %>% get_sim()
## Warning in cor(inv_df[sapply(inv_df, is.numeric)], use = "p", method =
## "pearson"): the standard deviation is zero
# calcula predição: média ponderada dos K vizinhos mais próximos
for(i in 1:length(teste_indices)) {
index <- teste_indices[i]
k_proximos <- get_neigh(teste, index, corr)
for(j in 1:length(COL_QUARTO_PER)) {
pred <- get_score(teste[, c("matricula", COL_QUARTO_PER[j])],
k_proximos, COL_QUARTO_PER[j])
teste_valores[index, COL_QUARTO_PER[j]] <- pred
}
}
# simplificando os dados
dados_reais <- dados[-temp, COL_QUARTO_PER]
predicao <- teste_valores[, COL_QUARTO_PER]
Para indicar a ausência de predição foi colocado o valor de NA na disciplina, segue abaixo a análise do número de exemplos sem predição.
# número de elementos em cada coluna de teste = 56
total_alunos <- sapply(predicao, function(x) length(x))
# número de NA em cada coluna, ou seja número de alunos sem predição por disciplina
sem_predicao <- sapply(predicao, function(x) sum(is.na(x)))
# porcentagem de alunos sem predição por disciplina
(sem_predicao/total_alunos) * 100
## EDA GI LEDA ME PE PLP TC
## 3.571429 0.000000 3.571429 19.642857 10.714286 17.857143 1.785714
# porcentagem total de variáveis sem predição
(sum(sem_predicao)/sum(total_alunos)) * 100
## [1] 8.163265
Portanto, apenas ~8% das variáveis de teste (total de 56 alunos) não obtiveram uma predição por falta de vizinhos próximos o suficiente ou muitos valores NAs.
Sendo que destes grande parte ficou concentrado na disciplina de Métodos estatísticos, Paradigmas de linguagens de programação e Probabilidade e Estatística, provavelmente devido ao grande número de NAs nessas disciplinas, para comprovar essa hipótese segue abaixo o número de NAs nas disciplinas nos dados de teste.
sapply(teste, function(x) sum(is.na(x)))
## matricula AL AV C1 C2 ES EDA
## 0 517 62 73 553 400 188
## FC FM GI IC LEDA LOAC LP1
## 205 292 127 33 200 382 21
## LP2 LPT LM MD ME OAC PLP
## 58 72 284 110 484 377 329
## PE P1 P2 SI1 TC TG
## 347 21 56 221 206 54
De fato, ME, PE e PLP são disciplinas que apresentam um grande número de NAs, e se destacam como as disciplinas com mais NAs no quarto período.
Para isso irei calcular o RMSE entre os dados reais e a predição. Utilizei o pacote hydroGOF que contém uma função para calcular rmse entre dois data frames.
rmse(sim=predicao, obs=dados_reais)
## EDA GI LEDA ME PE PLP TC
## 2.585149 1.263001 2.334676 2.271330 1.825156 1.229513 1.621955
O RMSE acima parece ser razoável para as disciplinas de GI, PLP e TC. O RMSE para estas disciplinas indica que nossas predições são em média ~1.2 (~1.6 para TC) acima ou abaixo os valores reais presente nas notas dos alunos, já as demais disciplinas tem média próxima de 2, que acho um resultado expressivo dada a simplicidade do modelo.
for (i in 1:length(COL_QUARTO_PER)) {
plot(predicao[, COL_QUARTO_PER[i]],
dados_reais[, COL_QUARTO_PER[i]],
xlab=paste("predicao ", COL_QUARTO_PER[i]),
ylab=paste("dado real", COL_QUARTO_PER[i]))
abline(lm(predicao[, COL_QUARTO_PER[i]]~dados_reais[, COL_QUARTO_PER[i]]), col="red")
# regression line (y~x)
}