O risco de fraude está em toda parte, mas para as empresas que anunciam online, a fraude de cliques pode acontecer em um volume avassalador, resultando em dados de cliques enganosos e dinheiro desperdiçado. Os canais de anúncios podem aumentar os custos simplesmente quando pessoas ou bots clicam nos anúncios em grande escala, o que na prática não gera o resultado esperado. Com mais de 1 bilhão de dispositivos móveis em uso todos os meses, a China é o maior mercado móvel do mundo e, portanto, sofre com grandes volumes de tráfego fraudulento.
A TalkingData (https://www.talkingdata.com), a maior plataforma de Big Data independente da China, cobre mais de 70% dos dispositivos móveis ativos em todo o país. Eles lidam com 3 bilhões de cliques por dia, dos quais 90% são potencialmente fraudulentos. Sua abordagem atual para impedir fraudes de cliques para desenvolvedores de aplicativos é medir a jornada do clique de um usuário em todo o portfólio e sinalizar endereços IP que produzem muitos cliques, mas nunca acabam instalando aplicativos. Com essas informações, eles criaram uma lista negra de IPs e uma lista negra de dispositivos.
Embora bem-sucedidos, eles querem estar sempre um passo à frente dos fraudadores e pediram a sua ajuda para desenvolver ainda mais a solução. Você está desafiado a criar um algoritmo que possa prever se um usuário fará o download de um aplicativo depois de clicar em um anúncio de aplicativo para dispositivos móveis.
Construir um modelo de machine learning cujo preveja se um clique é fraudulento ou nao.
O dataset utilizado aqui, faz parte de uma das competições presentes no kaggle: https://www.kaggle.com/c/talkingdata-adtracking-fraud-detection/data
setwd("E:/projetos/concluidos/deteccao_de_fraudes_no_trafego_de_cliques")
pacman::p_load(tidyverse, caTools, corrplot, caret, data.table,knitr, gridExtra,gmodels, class,e1071,ROCR)
list.files()
## [1] "deteccao_de_fraudes.html" "deteccao_de_fraudes.R"
## [3] "deteccao_de_fraudes.Rmd" "Projeto01.pdf"
## [5] "sample_submission.csv" "test.csv"
## [7] "test_supplement.csv" "teste.csv"
## [9] "train.csv" "train_sample.csv"
## [11] "treino.csv"
df <- fread("train.csv");dim(df);table(df$is_attributed)
## [1] 184903890 8
##
## 0 1
## 184447044 456846
Para mantermos o balanceamento da nossa amostra, vamos subsetar outro dataset com 137k de observacoes considerando is_attibruted == 0. Em seguida vamos pré-visualizar nossos dados.
df2 <- df %>%
filter(is_attributed == 1) %>%
sample_frac(0.3);dim(df2)
## [1] 137054 8
df <- df %>% filter(is_attributed == 0);dim(df)
## [1] 184447044 8
df <- df[1:137054,];dim(df)
## [1] 137054 8
df <- rbind(df,df2);kable(head(df));kable(tail(df));glimpse(df)
| ip | app | device | os | channel | click_time | attributed_time | is_attributed |
|---|---|---|---|---|---|---|---|
| 83230 | 3 | 1 | 13 | 379 | 2017-11-06 14:32:21 | 0 | |
| 17357 | 3 | 1 | 19 | 379 | 2017-11-06 14:33:34 | 0 | |
| 35810 | 3 | 1 | 13 | 379 | 2017-11-06 14:34:12 | 0 | |
| 45745 | 14 | 1 | 13 | 478 | 2017-11-06 14:34:52 | 0 | |
| 161007 | 3 | 1 | 13 | 379 | 2017-11-06 14:35:08 | 0 | |
| 18787 | 3 | 1 | 16 | 379 | 2017-11-06 14:36:26 | 0 |
| ip | app | device | os | channel | click_time | attributed_time | is_attributed |
|---|---|---|---|---|---|---|---|
| 194316 | 51 | 0 | 24 | 203 | 2017-11-07 11:27:48 | 2017-11-07 11:30:00 | 1 |
| 332494 | 148 | 1 | 19 | 213 | 2017-11-08 17:42:31 | 2017-11-08 17:45:09 | 1 |
| 106223 | 107 | 1 | 19 | 171 | 2017-11-08 00:32:19 | 2017-11-08 04:14:33 | 1 |
| 355151 | 29 | 1 | 19 | 213 | 2017-11-08 23:13:03 | 2017-11-08 23:17:05 | 1 |
| 146712 | 10 | 1 | 37 | 113 | 2017-11-07 05:07:58 | 2017-11-07 05:08:29 | 1 |
| 291965 | 19 | 78 | 0 | 333 | 2017-11-09 13:03:49 | 2017-11-09 13:04:29 | 1 |
## Rows: 274,108
## Columns: 8
## $ ip <int> 83230, 17357, 35810, 45745, 161007, 18787, 103022, ...
## $ app <int> 3, 3, 3, 14, 3, 3, 3, 3, 3, 64, 3, 3, 3, 3, 3, 3, 3...
## $ device <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...
## $ os <int> 13, 19, 13, 13, 13, 16, 23, 19, 13, 22, 25, 13, 19,...
## $ channel <int> 379, 379, 379, 478, 379, 379, 379, 379, 379, 459, 3...
## $ click_time <chr> "2017-11-06 14:32:21", "2017-11-06 14:33:34", "2017...
## $ attributed_time <chr> "", "", "", "", "", "", "", "", "", "", "", "", "",...
## $ is_attributed <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
Verificando se há valores NA no dataset:
colSums(is.na(df))
## ip app device os channel
## 0 0 0 0 0
## click_time attributed_time is_attributed
## 0 0 0
Dropando colunas que nao iremos utilizar, e criando novas variáveis.
df <- df %>%
mutate_if(is.integer,as.factor) %>%
select(c(-attributed_time)) %>%
mutate_if(is.character,as.POSIXct) %>%
mutate(hora_click = as.factor(hour(click_time)),
dia_click = as.factor(weekdays(click_time))) %>%
select(c(-click_time));kable(head(df));glimpse(df)
| ip | app | device | os | channel | is_attributed | hora_click | dia_click |
|---|---|---|---|---|---|---|---|
| 83230 | 3 | 1 | 13 | 379 | 0 | 14 | segunda-feira |
| 17357 | 3 | 1 | 19 | 379 | 0 | 14 | segunda-feira |
| 35810 | 3 | 1 | 13 | 379 | 0 | 14 | segunda-feira |
| 45745 | 14 | 1 | 13 | 478 | 0 | 14 | segunda-feira |
| 161007 | 3 | 1 | 13 | 379 | 0 | 14 | segunda-feira |
| 18787 | 3 | 1 | 16 | 379 | 0 | 14 | segunda-feira |
## Rows: 274,108
## Columns: 8
## $ ip <fct> 83230, 17357, 35810, 45745, 161007, 18787, 103022, 11...
## $ app <fct> 3, 3, 3, 14, 3, 3, 3, 3, 3, 64, 3, 3, 3, 3, 3, 3, 3, ...
## $ device <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,...
## $ os <fct> 13, 19, 13, 13, 13, 16, 23, 19, 13, 22, 25, 13, 19, 1...
## $ channel <fct> 379, 379, 379, 478, 379, 379, 379, 379, 379, 459, 379...
## $ is_attributed <fct> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ hora_click <fct> 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 1...
## $ dia_click <fct> segunda-feira, segunda-feira, segunda-feira, segunda-...
df3 <- df2 %>%
mutate_if(is.integer,as.factor) %>%
mutate_if(is.character,as.POSIXct) %>%
mutate(hora_click = as.factor(hour(click_time)),
dia_click = as.factor(weekdays(click_time)),
hora_attributed = as.factor(hour(attributed_time)),
dia_attributed = as.factor(weekdays(attributed_time))) %>%
select(ip,app,device,os,channel,dia_click,hora_click,dia_attributed,hora_attributed,is_attributed);kable(head(df3));glimpse(df3)
| ip | app | device | os | channel | dia_click | hora_click | dia_attributed | hora_attributed | is_attributed |
|---|---|---|---|---|---|---|---|---|---|
| 268216 | 22 | 1 | 19 | 116 | quinta-feira | 2 | quinta-feira | 2 | 1 |
| 49573 | 9 | 1 | 22 | 134 | terça-feira | 3 | terça-feira | 4 | 1 |
| 304621 | 3 | 1 | 13 | 137 | quarta-feira | 21 | quinta-feira | 14 | 1 |
| 344851 | 5 | 1 | 43 | 113 | quinta-feira | 7 | quinta-feira | 7 | 1 |
| 355265 | 191 | 33 | 29 | 414 | quinta-feira | 2 | quinta-feira | 2 | 1 |
| 110300 | 45 | 1 | 77 | 419 | quinta-feira | 10 | quinta-feira | 10 | 1 |
## Rows: 137,054
## Columns: 10
## $ ip <fct> 268216, 49573, 304621, 344851, 355265, 110300, 1303...
## $ app <fct> 22, 9, 3, 5, 191, 45, 9, 108, 45, 35, 35, 19, 29, 4...
## $ device <fct> 1, 1, 1, 1, 33, 1, 1, 1, 1, 1, 1, 60, 1, 1, 1, 1, 0...
## $ os <fct> 19, 22, 13, 43, 29, 77, 19, 18, 61, 14, 22, 38, 8, ...
## $ channel <fct> 116, 134, 137, 113, 414, 419, 466, 243, 419, 274, 2...
## $ dia_click <fct> quinta-feira, terça-feira, quarta-feira, quinta-fei...
## $ hora_click <fct> 2, 3, 21, 7, 2, 10, 12, 2, 12, 9, 5, 10, 21, 17, 7,...
## $ dia_attributed <fct> quinta-feira, terça-feira, quinta-feira, quinta-fei...
## $ hora_attributed <fct> 2, 4, 14, 7, 2, 10, 13, 3, 12, 9, 6, 10, 22, 14, 10...
## $ is_attributed <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...
p1 <- ggplot(df3, aes(x=dia_click))+
geom_bar(fill = "lightblue")+theme_minimal()+labs(x = "Dia",title="Click por dia")
p2 <- ggplot(df3, aes(x=dia_attributed))+
geom_bar(fill = "lightgreen")+theme_minimal()+labs(x = "Dia",title = "Download por dia")
grid.arrange(p1,p2,nrow=2)
Segunda-feira é o pior dia, tanto quando falamos em downloads quando falamos em clicks.
É possível notar também que existe um padrão quanto ao comportamento de ambas as amostras.
Vamos analisar quais são os números de “os” dos usuários que mais efetuam downloads.
df_os <- df3 %>% group_by(os) %>% summarize(n = n()) %>% arrange(desc(n));df_os <- df_os[1:10,]
## `summarise()` ungrouping output (override with `.groups` argument)
df_os_na <- df3 %>% group_by(os) %>% summarize(n = n()) %>% arrange(desc(n));df_os <- df_os[1:10,]
## `summarise()` ungrouping output (override with `.groups` argument)
p4 <- ggplot(df_os, aes(x = reorder(os,n),y=n))+
geom_col(fill = "steelblue")+theme_minimal()+labs(x = "os",title = "Numero de dowloads por tipo de os (top 10)")
grid.arrange(p4,nrow=1)
Os usuário que possuem o os numero 19 e 13 são os que mais efetuam downloads.
Vamos ver também como fica a relação entre a hora do click e a hora em que foram realizados os downloads.
A ideia aqui, é tentar identificar se há algum padrão.
p5 <- ggplot(df3, aes(x = hora_click))+
geom_histogram(stat = "count",fill = "steelblue")+theme_minimal()+labs(x = "Hora",title = "Click por hora")
## Warning: Ignoring unknown parameters: binwidth, bins, pad
p6 <- ggplot(df3, aes(x = hora_attributed))+
geom_histogram(stat = "count",fill = "lightgreen")+theme_minimal()+labs(x = "Hora",title = "Download por hora")
## Warning: Ignoring unknown parameters: binwidth, bins, pad
grid.arrange(p5,p6,nrow=1)
Embora existam algumas diferencas entre o horário de click e de download (especialmente entre a faixa de 0 a 4), pode-se dizer que as amostras possuem o mesmo padrao.
glimpse(df)
## Rows: 274,108
## Columns: 8
## $ ip <fct> 83230, 17357, 35810, 45745, 161007, 18787, 103022, 11...
## $ app <fct> 3, 3, 3, 14, 3, 3, 3, 3, 3, 64, 3, 3, 3, 3, 3, 3, 3, ...
## $ device <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,...
## $ os <fct> 13, 19, 13, 13, 13, 16, 23, 19, 13, 22, 25, 13, 19, 1...
## $ channel <fct> 379, 379, 379, 478, 379, 379, 379, 379, 379, 459, 379...
## $ is_attributed <fct> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ hora_click <fct> 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 1...
## $ dia_click <fct> segunda-feira, segunda-feira, segunda-feira, segunda-...
df_final <- df %>% select(1:5,7,8,6) %>%
mutate(ip = as.numeric(ip),
hora_click = as.numeric(hora_click),
device = as.numeric(device),
os = as.numeric(os),
channel = as.numeric(channel),
is_attributed = as.factor(is_attributed),
dia_click = as.factor(dia_click),
app = as.numeric(app))
sample <- sample.split(df_final$ip, SplitRatio = 0.7)
treino <- subset(df_final,sample == TRUE)
teste <- subset(df_final,sample == FALSE)
kable(head(treino));kable(head(teste))
| ip | app | device | os | channel | hora_click | dia_click | is_attributed |
|---|---|---|---|---|---|---|---|
| 16323 | 4 | 2 | 14 | 114 | 15 | segunda-feira | 0 |
| 6925 | 4 | 2 | 14 | 114 | 15 | segunda-feira | 0 |
| 3717 | 4 | 2 | 17 | 114 | 15 | segunda-feira | 0 |
| 22430 | 4 | 2 | 20 | 114 | 15 | segunda-feira | 0 |
| 38575 | 4 | 2 | 14 | 114 | 15 | segunda-feira | 0 |
| 14604 | 57 | 2 | 23 | 148 | 15 | segunda-feira | 0 |
| ip | app | device | os | channel | hora_click | dia_click | is_attributed |
|---|---|---|---|---|---|---|---|
| 3392 | 4 | 2 | 20 | 114 | 15 | segunda-feira | 0 |
| 8903 | 15 | 2 | 14 | 156 | 15 | segunda-feira | 0 |
| 36881 | 4 | 2 | 14 | 114 | 15 | segunda-feira | 0 |
| 20141 | 4 | 2 | 24 | 114 | 15 | segunda-feira | 0 |
| 20718 | 4 | 2 | 14 | 114 | 15 | segunda-feira | 0 |
| 54258 | 4 | 2 | 20 | 114 | 15 | segunda-feira | 0 |
teste.att <- data.frame(teste[,-8])
teste.class <- data.frame(teste[,8])
Vamos utilizar o algoritimo “trainControl” para nos indicar as variáveis mais relevantes para a construção do modelo preditivo.
formula <- "is_attributed~."
formula <- as.formula(formula)
control <- trainControl(method = "repeatedcv", number = 10, repeats = 2 )
model <- train(formula, data = treino, method = "glm", trControl = control)
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
importance <- varImp(model,scale = FALSE)
plot(importance)
formula.new <- "is_attributed~app+ip+channel+os"
formula.new <- as.formula(formula.new)
modelo <- glm(formula=formula.new,data=treino,family = "binomial");summary(modelo)
##
## Call:
## glm(formula = formula.new, family = "binomial", data = treino)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -6.3638 -0.7556 0.0845 0.6915 2.4287
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -2.685e+00 1.929e-02 -139.175 < 2e-16 ***
## app 9.188e-02 5.933e-04 154.857 < 2e-16 ***
## ip 4.868e-05 2.842e-07 171.334 < 2e-16 ***
## channel -3.398e-03 1.480e-04 -22.965 < 2e-16 ***
## os 2.368e-03 3.954e-04 5.989 2.11e-09 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 290981 on 211128 degrees of freedom
## Residual deviance: 193505 on 211124 degrees of freedom
## AIC: 193515
##
## Number of Fisher Scoring iterations: 6
previsoes <- round(predict(modelo, teste, "response"))
previsoes_new <- round(as.data.frame(predict(modelo, teste, "response")))
colnames(previsoes_new) <- "previsoes";previsoes_new$previsoes <- as.factor(previsoes_new$previsoes)
confusionMatrix(table(data = previsoes_new$previsoes,reference = teste.class$is_attributed), positive = "1")
## Confusion Matrix and Statistics
##
## reference
## data 0 1
## 0 33804 8840
## 1 7167 13168
##
## Accuracy : 0.7458
## 95% CI : (0.7424, 0.7492)
## No Information Rate : 0.6506
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.431
##
## Mcnemar's Test P-Value : < 2.2e-16
##
## Sensitivity : 0.5983
## Specificity : 0.8251
## Pos Pred Value : 0.6476
## Neg Pred Value : 0.7927
## Prevalence : 0.3494
## Detection Rate : 0.2091
## Detection Prevalence : 0.3229
## Balanced Accuracy : 0.7117
##
## 'Positive' Class : 1
##
Analisando pela confusion matrix, pode-se dizer que nosso modelo teve um bom indice de acurácia (0.7434).
O número de falsos negativos e falsos posivos também foram baixos.
previsoes <- predict(modelo,teste.att,"response")
avaliacao <- prediction(previsoes, teste.class)
plot.roc.curve <- function(predictions, title.text){
perf <- performance(predictions, "tpr", "fpr")
plot(perf,col = "black",lty = 1, lwd = 2,
main = title.text, cex.main = 0.6, cex.lab = 0.8,xaxs = "i", yaxs = "i")
abline(0,1, col = "red")
auc <- performance(predictions,"auc")
auc <- unlist(slot(auc, "y.values"))
auc <- round(auc,2)
legend(0.4,0.4,legend = c(paste0("AUC: ",auc)), cex = 0.6, bty = "n", box.col = "white")
}
par(mfrow = c(1,1))
plot.roc.curve(avaliacao,title.text = "Curva Roc")
A curva ROC, valida nossa afirmação anterior, apresentando AUC = 0.78.
O que é muito bom.
linkedin/in/sandropenha
github.com/sandropenha