library(tidyverse)
library(ROSE)
library(randomForest)
library(caret)
library(e1071)
library(rpart)
library(rpart.plot)
Este projeto tem como objetivo criar um modelo de classificação que preveja se uma máquina irá falhar ou não e posteriormente fazer um dashboard com os gráficos produzidos e a predição. O conjunto de dados foi fornecido S. Matzka, 2020 e está disponível no UCI. Neste conjunto, temos 10000 linhas e 10 variáveis. São elas:
set.seed(1981)
maquinas <- read.csv("predictive_maintenance.csv")
summary(maquinas)
## UDI Product.ID Type Air.temperature..K.
## Min. : 1 Length:10000 Length:10000 Min. :295.3
## 1st Qu.: 2501 Class :character Class :character 1st Qu.:298.3
## Median : 5000 Mode :character Mode :character Median :300.1
## Mean : 5000 Mean :300.0
## 3rd Qu.: 7500 3rd Qu.:301.5
## Max. :10000 Max. :304.5
## Process.temperature..K. Rotational.speed..rpm. Torque..Nm. Tool.wear..min.
## Min. :305.7 Min. :1168 Min. : 3.80 Min. : 0
## 1st Qu.:308.8 1st Qu.:1423 1st Qu.:33.20 1st Qu.: 53
## Median :310.1 Median :1503 Median :40.10 Median :108
## Mean :310.0 Mean :1539 Mean :39.99 Mean :108
## 3rd Qu.:311.1 3rd Qu.:1612 3rd Qu.:46.80 3rd Qu.:162
## Max. :313.8 Max. :2886 Max. :76.60 Max. :253
## Target Failure.Type
## Min. :0.0000 Length:10000
## 1st Qu.:0.0000 Class :character
## Median :0.0000 Mode :character
## Mean :0.0339
## 3rd Qu.:0.0000
## Max. :1.0000
É possível notar que não há dados faltantes, mas algumas colunas não estão com o tipo de variável adequadas.
# Mudando as variáveis tipo e tipo de defeito para factor
maquinas <- maquinas |> mutate(Type = as.factor(Type),
Target = as.factor(Target),
Failure.Type = as.factor(Failure.Type))
# As colunas Product ID e UDI não interessam
maquinas <- maquinas |> select(-c(UDI, Product.ID))
# Convertendo K em C
maquinas[c(2,3)] <- maquinas[c(2,3)] - 273.15
summary(maquinas)
## Type Air.temperature..K. Process.temperature..K. Rotational.speed..rpm.
## H:1003 Min. :22.15 Min. :32.55 Min. :1168
## L:6000 1st Qu.:25.15 1st Qu.:35.65 1st Qu.:1423
## M:2997 Median :26.95 Median :36.95 Median :1503
## Mean :26.85 Mean :36.86 Mean :1539
## 3rd Qu.:28.35 3rd Qu.:37.95 3rd Qu.:1612
## Max. :31.35 Max. :40.65 Max. :2886
## Torque..Nm. Tool.wear..min. Target Failure.Type
## Min. : 3.80 Min. : 0 0:9661 Heat Dissipation Failure: 112
## 1st Qu.:33.20 1st Qu.: 53 1: 339 No Failure :9652
## Median :40.10 Median :108 Overstrain Failure : 78
## Mean :39.99 Mean :108 Power Failure : 95
## 3rd Qu.:46.80 3rd Qu.:162 Random Failures : 18
## Max. :76.60 Max. :253 Tool Wear Failure : 45
É importante verificar se não há incongruências entre as variáveis Target e Failure Type. Além disso, nota-se que no conjunto de dados há muito mais casos de não falha.
maquinas <- maquinas |> filter(!(Target == 0 & Failure.Type != 'No Failure'| Target == 1 & Failure.Type == 'No Failure') )
nrow(maquinas)
## [1] 9973
Como não é possível identificar o erro, as observações não-conformes foram retiradas.
cores <- c('#704C5E', '#825C6E', '#946C7E', '#B88C9E', '#F1C8DB')
cor <- '#704C5E'
maquinas |> filter(Failure.Type != 'No Failure') |>
ggplot(aes(x = '', fill = fct_infreq(Failure.Type) )) +
geom_bar(position = 'fill') +
scale_fill_manual(values = cores) +
labs(fill = '', x = "", y = "Percentual") +
ggtitle('Percentual das falhas apresentadas')+
coord_flip()+
scale_y_continuous(labels = scales::percent) +
annotate(geom = 'text', label = c( '13%',' 24%',' 29%','34%'), x = c( 1, 1, 1, 1), y = c (0.07, 0.25, 0.52, 0.85), color = 'white')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
maquinas |> filter(Target == 1) |> count(Type) |>
ggplot(aes(x =fct_rev(fct_reorder(Type, n)) , y = n)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= n %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Numero de falhas por tipo") +
labs(x = 'Tipo de maquina', y = 'Numero de ocorrencias')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
Existem mais falhas em máquinas com baixa qualidade de produção, seguida por média e alta qualidade.
### Dispersão Rotação vs Torque
maquinas |> filter(Target==1) |>
ggplot(aes(x=Rotational.speed..rpm., y=Torque..Nm.,color = Failure.Type)) +
geom_point() +
labs(fill = '', x = "Velocidade de Rotação (rpm)", y = "Torque (Nm)") +
ggtitle('Tipos de Falhas por Velocidade de Rotação vs Torque ')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
Podemos obsevar que nos maiores torques ocorrem as falhas por calor, esforço e energia. Enquanto nas maiores velocidades de rotação (e menores torques) acontecem as falhas de energia. Já as falhas por desgaste de ferramenta aparecem numa faixa intermediária de torque e velocidade de rotação.
matrizcor <- maquinas |> select_if(is.numeric) |> cor()
corrplot::corrplot(matrizcor, method = 'color', addCoef.col = 'black',tl.srt = 45)
maquinas |> select_if(is.numeric) |> select(- Rotational.speed..rpm.)|> stack() |>
ggplot(aes(x = ind, y = values)) +
geom_boxplot(fill = '#B88C9E') +
labs(x = '', y = '') +
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
maquinas |> select(Rotational.speed..rpm.) |> stack() |>
ggplot(aes(x = ind, y = values)) +
geom_boxplot(fill = '#B88C9E') +
labs(x = '', y = '') +
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
Pelo gráfico de correlação podemos observar duas correlações esperadas empiricamente que são entre as temperaturas do ar e do processo e entre torque e velocidade de rotação, esta negativa. Ambas as correlações são bastante fortes. Já a variável desgaste de ferramenta não está correlacionada a nenhuma das outras colunas numéricas.
Já pelos boxplots podemos observar que a temperatura do processo é em média maior que a temperatura do ar e que o torque tem outliers à direita e à esquerda, enquanto a velocidade de rotação tem outliers apenas à direita.
## Temp. do Ar
maquinas |> group_by(Target) |> summarise(media = mean(Air.temperature..K.)) |>
ggplot(aes(x = Target, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Temperatura do Ar") +
labs(x = 'Falha', y = 'Média de Temperatura (°C)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
maquinas |> group_by(Type) |> summarise(media = mean(Air.temperature..K.)) |>
ggplot(aes(x = Type, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Temperatura do Ar por Tipo de Máquina") +
labs(x = 'Tipo', y = 'Média de Temperatura (°C)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
## Temp. do Processo
maquinas |> group_by(Target) |> summarise(media = mean(Process.temperature..K.)) |>
ggplot(aes(x = Target, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Temperatura do Processo") +
labs(x = 'Falha', y = 'Média de Temperatura (°C)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
maquinas |> group_by(Type) |> summarise(media = mean(Process.temperature..K.)) |>
ggplot(aes(x = Type, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Temperatura do Processo por Tipo de Máquina") +
labs(x = 'Tipo', y = 'Média de Temperatura (°C)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
# Velocidade de rotação
maquinas |> group_by(Target) |> summarise(media = mean(Rotational.speed..rpm.)) |>
ggplot(aes(x = Target, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Velocidade de Rotação do Processo") +
labs(x = 'Falha', y = 'Média de Velocidade (rpm)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
maquinas |> group_by(Type) |> summarise(media = mean(Rotational.speed..rpm.)) |>
ggplot(aes(x = Type, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Velocidade de Rotação do Processo por Tipo de Máquina") +
labs(x = 'Tipo', y = 'Média de Velocidade (rpm)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
# Torque
maquinas |> group_by(Target) |> summarise(media = mean(Torque..Nm.)) |>
ggplot(aes(x = Target, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Torque do Processo") +
labs(x = 'Falha', y = 'Média de Torque (Nm)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
maquinas |> group_by(Type) |> summarise(media = mean(Torque..Nm.)) |>
ggplot(aes(x = Type, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Torque do Processo por Tipo de Máquina") +
labs(x = 'Tipo', y = 'Média de Torque (Nm)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
# Tool wear
maquinas |> group_by(Target) |> summarise(media = mean(Tool.wear..min.)) |>
ggplot(aes(x = Target, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Desgaste de Ferramenta do Processo") +
labs(x = 'Falha', y = 'Média de Desgaste (min)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
maquinas |> group_by(Type) |> summarise(media = mean(Tool.wear..min.)) |>
ggplot(aes(x = Type, y = media)) +
geom_bar(stat = 'identity', fill = cor) +
geom_text(aes(label= media %>% round(digits = 2)), vjust=1.6, color="white", size=3.5) +
ggtitle("Médias de Desgaste de Ferramenta do Processo por Tipo de Máquina") +
labs(x = 'Tipo', y = 'Média de Desgaste (min)')+
theme_bw() +
theme(axis.line = element_line(colour = "black"),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
De modo geral, podemos obsevar que o tipo de máquina não influencia nas variáveis numéricas. Já as máquinas que falharam tiveram maior desgaste de ferramenta, maior torque e menor velocidade rotação, enquanto a temperatura não parece influenciar tanto se há ou não falha.
maquinas <- maquinas |> mutate_if(is.numeric, scale)
maquinas <- maquinas |> select(-Failure.Type)
ind <- sample(2, nrow(maquinas), replace = TRUE, prob = c(0.7, 0.3))
train <- maquinas[ind==1,]
test <- maquinas[ind==2,]
maquinas |> group_by(Target) |>
summarise(n = n()) |>
mutate(freq = n/sum(n))
## # A tibble: 2 × 3
## Target n freq
## <fct> <int> <dbl>
## 1 0 9643 0.967
## 2 1 330 0.0331
train |> group_by(Target) |>
summarise(n = n()) |>
mutate(freq = n/sum(n))
## # A tibble: 2 × 3
## Target n freq
## <fct> <int> <dbl>
## 1 0 6699 0.967
## 2 1 226 0.0326
test |> group_by(Target) |>
summarise(n = n()) |>
mutate(freq = n/sum(n))
## # A tibble: 2 × 3
## Target n freq
## <fct> <int> <dbl>
## 1 0 2944 0.966
## 2 1 104 0.0341
Temos um problema de desbalanceamento dos dados. No entanto, tanto os dados de treino quanto os dados de teste seguiram uma proporção próxima aos dados totais. No entanto, será utilizada a técnica de undersampling para comparação entre os modelos. Além disso, a variável tipo de falha foi retirada, pois um dos levels era justamente ’Sem Falha”, o que facilitaria muito para os modelos, deixando a análise sem sentido.
under <- ovun.sample(Target~., data=maquinas[-8], method = "under")$data
sample_data <- createDataPartition(under$Target, p = 0.70, list = F)
train_data <- under[sample_data,]
test_data <- under[-sample_data,]
under |> group_by(Target) |>
summarise(n = n()) |>
mutate(freq = n/sum(n))
## # A tibble: 2 × 3
## Target n freq
## <fct> <int> <dbl>
## 1 0 322 0.494
## 2 1 330 0.506
train_data |> group_by(Target) |>
summarise(n = n()) |>
mutate(freq = n/sum(n))
## # A tibble: 2 × 3
## Target n freq
## <fct> <int> <dbl>
## 1 0 226 0.495
## 2 1 231 0.505
test_data |> group_by(Target) |>
summarise(n = n()) |>
mutate(freq = n/sum(n))
## # A tibble: 2 × 3
## Target n freq
## <fct> <int> <dbl>
## 1 0 96 0.492
## 2 1 99 0.508
Assim como os dados desbalanceados, os dados de treino e teste mantém a mesma proporção.
tree <- rpart(Target ~., data = train)
p <- predict(tree, test, type = 'class')
confusionMatrix(p, test$Target, positive='1', mode = 'everything')
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2924 29
## 1 20 75
##
## Accuracy : 0.9839
## 95% CI : (0.9788, 0.9881)
## No Information Rate : 0.9659
## P-Value [Acc > NIR] : 8.491e-10
##
## Kappa : 0.7455
##
## Mcnemar's Test P-Value : 0.2531
##
## Sensitivity : 0.72115
## Specificity : 0.99321
## Pos Pred Value : 0.78947
## Neg Pred Value : 0.99018
## Precision : 0.78947
## Recall : 0.72115
## F1 : 0.75377
## Prevalence : 0.03412
## Detection Rate : 0.02461
## Detection Prevalence : 0.03117
## Balanced Accuracy : 0.85718
##
## 'Positive' Class : 1
##
tree <- rpart(Target ~., data = train_data)
p <- predict(tree, test_data, type = 'class')
confusionMatrix(p, test_data$Target, positive='1', mode = 'everything')
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 84 1
## 1 12 98
##
## Accuracy : 0.9333
## 95% CI : (0.8887, 0.964)
## No Information Rate : 0.5077
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.8664
##
## Mcnemar's Test P-Value : 0.005546
##
## Sensitivity : 0.9899
## Specificity : 0.8750
## Pos Pred Value : 0.8909
## Neg Pred Value : 0.9882
## Precision : 0.8909
## Recall : 0.9899
## F1 : 0.9378
## Prevalence : 0.5077
## Detection Rate : 0.5026
## Detection Prevalence : 0.5641
## Balanced Accuracy : 0.9324
##
## 'Positive' Class : 1
##
nbmodel1 <- naivebayes::naive_bayes(Target ~., data = train_data)
p <- predict(nbmodel1, test, type = 'class')
## Warning: predict.naive_bayes(): more features in the newdata are provided as
## there are probability tables in the object. Calculation is performed based on
## features to be found in the tables.
confusionMatrix(p, test$Target, positive='1', mode = 'everything')
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2553 18
## 1 391 86
##
## Accuracy : 0.8658
## 95% CI : (0.8532, 0.8777)
## No Information Rate : 0.9659
## P-Value [Acc > NIR] : 1
##
## Kappa : 0.2543
##
## Mcnemar's Test P-Value : <2e-16
##
## Sensitivity : 0.82692
## Specificity : 0.86719
## Pos Pred Value : 0.18029
## Neg Pred Value : 0.99300
## Precision : 0.18029
## Recall : 0.82692
## F1 : 0.29604
## Prevalence : 0.03412
## Detection Rate : 0.02822
## Detection Prevalence : 0.15650
## Balanced Accuracy : 0.84706
##
## 'Positive' Class : 1
##
nbmodel2 <- naivebayes::naive_bayes(Target ~., data = train_data)
p <- predict(nbmodel2, test_data, type = 'class')
## Warning: predict.naive_bayes(): more features in the newdata are provided as
## there are probability tables in the object. Calculation is performed based on
## features to be found in the tables.
confusionMatrix(p, test_data$Target, positive='1', mode = 'everything')
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 82 16
## 1 14 83
##
## Accuracy : 0.8462
## 95% CI : (0.7877, 0.8937)
## No Information Rate : 0.5077
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.6923
##
## Mcnemar's Test P-Value : 0.8551
##
## Sensitivity : 0.8384
## Specificity : 0.8542
## Pos Pred Value : 0.8557
## Neg Pred Value : 0.8367
## Precision : 0.8557
## Recall : 0.8384
## F1 : 0.8469
## Prevalence : 0.5077
## Detection Rate : 0.4256
## Detection Prevalence : 0.4974
## Balanced Accuracy : 0.8463
##
## 'Positive' Class : 1
##
modelorl1 <- glm(Target ~.,family=binomial(link='logit'),data=train)
p <- predict(modelorl1, test)
p <- ifelse(p > 0.51, 1, 0) |> as.factor()
confusionMatrix(p, as.factor(test$Target) , positive = '1', mode = 'everything')
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2941 89
## 1 3 15
##
## Accuracy : 0.9698
## 95% CI : (0.9631, 0.9756)
## No Information Rate : 0.9659
## P-Value [Acc > NIR] : 0.1245
##
## Kappa : 0.2382
##
## Mcnemar's Test P-Value : <2e-16
##
## Sensitivity : 0.144231
## Specificity : 0.998981
## Pos Pred Value : 0.833333
## Neg Pred Value : 0.970627
## Precision : 0.833333
## Recall : 0.144231
## F1 : 0.245902
## Prevalence : 0.034121
## Detection Rate : 0.004921
## Detection Prevalence : 0.005906
## Balanced Accuracy : 0.571606
##
## 'Positive' Class : 1
##
modelorl2 <- glm(Target ~.,family=binomial(link='logit'),data=train_data)
p <- predict(modelorl2, test_data)
p <- ifelse(p > 0.51, 1, 0) |> as.factor()
confusionMatrix(p, as.factor(test_data$Target) , positive = '1', mode = 'everything')
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 85 26
## 1 11 73
##
## Accuracy : 0.8103
## 95% CI : (0.7481, 0.8627)
## No Information Rate : 0.5077
## P-Value [Acc > NIR] : < 2e-16
##
## Kappa : 0.6213
##
## Mcnemar's Test P-Value : 0.02136
##
## Sensitivity : 0.7374
## Specificity : 0.8854
## Pos Pred Value : 0.8690
## Neg Pred Value : 0.7658
## Precision : 0.8690
## Recall : 0.7374
## F1 : 0.7978
## Prevalence : 0.5077
## Detection Rate : 0.3744
## Detection Prevalence : 0.4308
## Balanced Accuracy : 0.8114
##
## 'Positive' Class : 1
##
Se formos considerar apenas acurácia, os modelos desbalanceados tiveram resultados um pouco superiores aos com undersampling. No entanto, as demais métricas como Kappa, TNI, F1-score e p-valor do Teste de Mcnemar mostram que faz sentido utilizar os modelos com undersampling. Assim, considerando as métricas avaliadas, o modelo escolhido é o da Árvore de Decisões com undersampling.
Este projeto apresentou de forma satisfatória um modelo de classificação de falhas em máquinas. Os modelos com undersampling mostraram-se de métricas superiores aos desbalanceados. Para um próximo projeto seria ideal testar modelos com oversampling também. A árvore de decisão teve uma performance boa em acurácia, apesar de tentar melhorar com um ajuste fino, não foi possível atingir tal resultado. O projeto em si não é complexo, mas mais uma vez o deploy foi importante no aprendizado. Aliás ele encontra-se aqui. Ficam pro futuro, usar modelos mais complexos, mostrar os ajustes de hiperparâmetros e melhorar o dashboard.