1 Introduction

Kita selaku tim data dari perusahaan pembuat alat pendeteksi asap (Smoke Detection) perlu membuat sistem/model yang dapat mengklasifikasikan apakah terdapat api atau tidak dengan efisien dan efektif.

Kita menyadari sering kali fenomena yang terjadi dilapangan (realitas) sulit ditemuakan polanya, bagaimana membedakan apakah terdapat api atau tidak, terutama ketika informasi yang menjadi pertimbangan (prediktor) sangat banyak. Oleh karena itu, projek Machine Learning ini dibuat untuk mencoba menyelesaikan kesulitan tersebut dengan efektif (karena memiliki ukuran kualitas yang jelas) dan efisien (karena dengan bantuan komputasi bukan manual). Projek ini menggunakan dataset dari Kaggle.

2 Data Preparation and Wrangling

2.1 Libraries

library(tidyverse)
library(caret)

library(keras)
library(rsample)
library(tensorflow)

library(plotly)

2.2 Read Dataset

smoke <- read.csv("smoke_detection_iot.csv")
smoke

Deskripsi kolom :

Prediktor : Kolom yang menjadi pertimbangan dalam menentukan Target (input).

  • X : Index yang menjadi identitas setiap baris.
  • UTC : Waktu UTC Timestamp dalam detik (data konversi dari waktu biasa menjadi UTC timestamp).
  • Temperature_C : Tingkat Suhu dalam satuan celcius.
  • Humidity_percent : Tingkat Kelembaban dalam persen.
  • TVOC_ppb : Total Senyawa Organik Volatil; diukur dalam bagian per miliar dalam ppb.
  • eCO2_ppm : Besaran Konsentrasi setara co2; dihitung dari nilai yang berbeda seperti TVCO dalam ppm.
  • Raw_H2 : Besaran Hidrogen molekuler mentah; tidak dikompensasi (Bias, suhu, dll.).
  • Raw_Ethanol : Besaran gas etanol mentah.
  • Pressure_hPa : Besaran tekanan udara dalam hPa.
  • PM1.0 : Besaran partikel dengan diameter hingga 1 mikron.
  • PM2.5 : Besaran partikel dengan diameter hingga 2.5 mikron.
  • NC0.5 : Number Concentration, Ini berbeda dari PM karena NC memberikan jumlah sebenarnya dari partikel (ukuran <= 0.5 µm) di udara.
  • NC1.0 : Number Concentration, Ini berbeda dari PM karena NC memberikan jumlah sebenarnya dari partikel (ukuran 0.5 µm <= 1 µm) di udara.
  • NC2.5 : Number Concentration, Ini berbeda dari PM karena NC memberikan jumlah sebenarnya dari partikel (ukuran 1 µm < 2.5µm) di udara.
  • CNT : Penghitung sampel.

Target : Kolom yang menjadi hasil dari pertimbangan prediktor (output).

  • Fire_Alarm : Menunjukan apakah ada api atau tidak; 1 = terdapat api, 0 = tidak/

2.3 Checking Missing Value

anyNA(smoke)
#> [1] FALSE

Tidak terdapat nilai hilang sehingga dapat lanjut ke tahap selanjutnya.

2.4 Checking Data Duplication

anyDuplicated(smoke)
#> [1] 0

Tidak terdapat data yang terduplikasi sehingga dapat lanjut ke tahap selanjutnya.

2.5 Adjusment Type Data

Struktur data sebelum Wrangling Data

glimpse(smoke)
#> Rows: 62,630
#> Columns: 16
#> $ X                <int> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,…
#> $ UTC              <int> 1654733331, 1654733332, 1654733333, 1654733334, 16547…
#> $ Temperature_C    <dbl> 20.000, 20.015, 20.029, 20.044, 20.059, 20.073, 20.08…
#> $ Humidity_percent <dbl> 57.36, 56.67, 55.96, 55.28, 54.69, 54.12, 53.61, 53.2…
#> $ TVOC_ppb         <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
#> $ eCO2_ppm         <int> 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400…
#> $ Raw_H2           <int> 12306, 12345, 12374, 12390, 12403, 12419, 12432, 1243…
#> $ Raw_Ethanol      <int> 18520, 18651, 18764, 18849, 18921, 18998, 19058, 1911…
#> $ Pressure_hPa     <dbl> 939.735, 939.744, 939.738, 939.736, 939.744, 939.725,…
#> $ PM1.0            <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00,…
#> $ PM2.5            <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00,…
#> $ NC0.5            <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
#> $ NC1.0            <dbl> 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.00…
#> $ NC2.5            <dbl> 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.00…
#> $ CNT              <int> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,…
#> $ Fire_Alarm       <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…

Proses Wrangling Data

rownames(smoke) <- smoke$X

smoke_clean <- smoke %>% 
  # Membuang kolom X karena sudah menjadi nama kolom
  select(-X) %>% 
  mutate(Fire_Alarm = as.factor(Fire_Alarm))

Struktur data sesudah Wrangling Data

glimpse(smoke_clean)
#> Rows: 62,630
#> Columns: 15
#> $ UTC              <int> 1654733331, 1654733332, 1654733333, 1654733334, 16547…
#> $ Temperature_C    <dbl> 20.000, 20.015, 20.029, 20.044, 20.059, 20.073, 20.08…
#> $ Humidity_percent <dbl> 57.36, 56.67, 55.96, 55.28, 54.69, 54.12, 53.61, 53.2…
#> $ TVOC_ppb         <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
#> $ eCO2_ppm         <int> 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400…
#> $ Raw_H2           <int> 12306, 12345, 12374, 12390, 12403, 12419, 12432, 1243…
#> $ Raw_Ethanol      <int> 18520, 18651, 18764, 18849, 18921, 18998, 19058, 1911…
#> $ Pressure_hPa     <dbl> 939.735, 939.744, 939.738, 939.736, 939.744, 939.725,…
#> $ PM1.0            <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00,…
#> $ PM2.5            <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00,…
#> $ NC0.5            <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
#> $ NC1.0            <dbl> 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.00…
#> $ NC2.5            <dbl> 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.00…
#> $ CNT              <int> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,…
#> $ Fire_Alarm       <fct> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…

3 Eksploratory Data Analysis

3.1 Data Summary

summary(smoke_clean)
#>       UTC             Temperature_C    Humidity_percent    TVOC_ppb    
#>  Min.   :1654712187   Min.   :-22.01   Min.   :10.74    Min.   :    0  
#>  1st Qu.:1654743244   1st Qu.: 10.99   1st Qu.:47.53    1st Qu.:  130  
#>  Median :1654761920   Median : 20.13   Median :50.15    Median :  981  
#>  Mean   :1654792066   Mean   : 15.97   Mean   :48.54    Mean   : 1942  
#>  3rd Qu.:1654777577   3rd Qu.: 25.41   3rd Qu.:53.24    3rd Qu.: 1189  
#>  Max.   :1655130051   Max.   : 59.93   Max.   :75.20    Max.   :60000  
#>     eCO2_ppm         Raw_H2       Raw_Ethanol     Pressure_hPa  
#>  Min.   :  400   Min.   :10668   Min.   :15317   Min.   :930.9  
#>  1st Qu.:  400   1st Qu.:12830   1st Qu.:19435   1st Qu.:938.7  
#>  Median :  400   Median :12924   Median :19501   Median :938.8  
#>  Mean   :  670   Mean   :12942   Mean   :19754   Mean   :938.6  
#>  3rd Qu.:  438   3rd Qu.:13109   3rd Qu.:20078   3rd Qu.:939.4  
#>  Max.   :60000   Max.   :13803   Max.   :21410   Max.   :939.9  
#>      PM1.0              PM2.5              NC0.5              NC1.0         
#>  Min.   :    0.00   Min.   :    0.00   Min.   :    0.00   Min.   :    0.00  
#>  1st Qu.:    1.28   1st Qu.:    1.34   1st Qu.:    8.82   1st Qu.:    1.38  
#>  Median :    1.81   Median :    1.88   Median :   12.45   Median :    1.94  
#>  Mean   :  100.59   Mean   :  184.47   Mean   :  491.46   Mean   :  203.59  
#>  3rd Qu.:    2.09   3rd Qu.:    2.18   3rd Qu.:   14.42   3rd Qu.:    2.25  
#>  Max.   :14333.69   Max.   :45432.26   Max.   :61482.03   Max.   :51914.68  
#>      NC2.5                CNT        Fire_Alarm
#>  Min.   :    0.000   Min.   :    0   0:17873   
#>  1st Qu.:    0.033   1st Qu.: 3625   1:44757   
#>  Median :    0.044   Median : 9336             
#>  Mean   :   80.049   Mean   :10511             
#>  3rd Qu.:    0.051   3rd Qu.:17165             
#>  Max.   :30026.438   Max.   :24993

3.2 Predictor Exploration

  • Nilai kolom UTC berada dikisaran 1,655,000,000
  • Nilai kolom Temparature_C didominasi (75%-nya) dari rentang -22.01 C hingga 15.97 C dengan tertingggi di angka 59.93 C.
  • Nilai kolom Humidity_percent didominasi (75%-nya) dari rentang 10.74% hingga 53.24% dengan tertingggi di angka 75.20%.
  • Nilai kolom TVOC_ppb didominasi (75%-nya) dari rentang 0 ppb hingga 1,189 ppb dengan tertingggi di angka 60,000 ppb.
  • Nilai kolom eCO2_ppm didominasi (75%-nya) dari rentang 400 ppm hingga 438 ppm dengan tertingggi di angka 60,000 ppm.
  • Nilai kolom Raw_H2 didominasi (75%-nya) dari rentang 10,668 hingga 13109 dengan tertingggi di angka 13,803.
  • Nilai kolom Raw_Ethanol didominasi (75%-nya) dari rentang 15,317 hingga 20078 dengan tertingggi di angka 21,410.
  • Nilai kolom Pressure_hPa didominasi (75%-nya) dari rentang 930.9 hingga 939.4 dengan tertingggi di angka 939.9.
  • Nilai kolom PM1.0 didominasi (75%-nya) dari rentang 0.00 hingga 2.09 dengan tertingggi di angka 14,333.69 (Sangat tidak merata).
  • Nilai kolom PM2.5 didominasi (75%-nya) dari rentang 0.00 hingga 2.18 dengan tertingggi di angka 45,432.26.
  • Nilai kolom NC0.5 didominasi (75%-nya) dari rentang 0.00 hingga 14.42 dengan tertingggi di angka 61,482.03.
  • Nilai kolom NC1.0 didominasi (75%-nya) dari rentang 0.00 hingga 2.25 dengan tertingggi di angka 51,914.68.
  • Nilai kolom NC2.5 didominasi (75%-nya) dari rentang 0.000 hingga 0.051 dengan tertingggi di angka 30,026.438.
  • Nilai kolom CNT didominasi (75%-nya) dari rentang 0 hingga 17,165 dengan tertingggi di angka 24,993.

3.3 Target Exploration

vis_data <- as.data.frame(table(smoke_clean$Fire_Alarm))

plt <- 
ggplot(data = vis_data, aes(x = Var1, y = Freq, fill = Var1,
                              text = glue::glue("Target : {Var1}.
                                           Jumlah : {Freq}"))) + 
  geom_bar(stat = "identity") +
  labs(title = "Perbandingan Target 0 (Tidak Terdapat Api) dan 1 (Terdapat Api)",
              y = "Jumlah",
              x = "Fire Alarm") +
  theme_minimal()

ggplotly(plt, tooltip =  "text" )

Pada data smoke ini di dominasi Fire_Alarm 1 atau terdapat Api.

4 Modeling

4.1 Split Data Train-Test

prop.table(table(smoke_clean$Fire_Alarm))
#> 
#>         0         1 
#> 0.2853744 0.7146256

Kita menyadari proporsi Target 0 dan 1 pada data kita cenderung tidak seimbang karena Target 0 hanya 28.53% sedangkan Target 1 sebesar 71.46%.

RNGkind(sample.kind = "Rounding")
set.seed(100)
index <- initial_split(data = smoke_clean, #data asli sebelum splitting
                       prop = 0.74,
                       strata = Fire_Alarm) 
  
train <- training(index)
test <- testing(index)


train <- upSample(x = train %>% select(-Fire_Alarm),
                  y = train$Fire_Alarm,
                  yname = "Fire_Alarm")

Oleh karena itu, kita mencoba membagi data train dan test dengan proporsi 74% untuk data train dan 26% untuk data test. Kenapa tidak 80% dan 20%? Agar setelah dilakukan UpSample (Menambah data dengan menduplikasi agar antara jenis target seimbang) pada data train perbandingan data train dan test cenderung sekitar 80% dan 20%.

Kita menyeimbangkan proporsi Target 0 dan 1 hanya pada data train saja karena fokus pada pelatihan model adalah menemukan pola data dalam membedakan Target 0 dengan 1. Sehingga diperlukan bahan belajar (data train) yang seimbang.

  • Proporsi Jenis Target Pada Data Train :
prop.table(table(train$Fire_Alarm))
#> 
#>   0   1 
#> 0.5 0.5

Kita tidak menyeimbangkan proporsi Target 0 dan 1 pada data test karena fokus pada pengetesan model adalah menguji apakah model kita sudah dapat menemukan pola yang membedakan Target 0 dengan 1. Sehingga tidak diperlukan soal ujian (data test) yang seimbang.

  • Proporsi Jenis Target Pada Data Test :

Kita menyamakan proporsi Target 0 dan 1 pada data test seperti pada data aslinya (smoke_clean). Karena kita mengharapkan model dapat melakukan prediksi yang sesuai dengan keadaan realitas. Alias pada pelatihan model telah berhasil menemukakn pola pembeda Target 0 dan 1.

prop.table(table(smoke_clean$Fire_Alarm))
#> 
#>         0         1 
#> 0.2853744 0.7146256
prop.table(table(test$Fire_Alarm))
#> 
#>         0         1 
#> 0.2853721 0.7146279

Berikut jumlah dan proporasi data train dan test setelah dilakukan UpSample.

prop_train <- nrow(train)/(nrow(train) + nrow(test))*100
prop_test <- nrow(test)/(nrow(train) + nrow(test))*100

data.frame(Data = c("Train", "Test", "Total"),
           Jumlah = c(nrow(train), nrow(test), nrow(train)+nrow(test)),
           Proporsi_persen = c(prop_train, prop_test, prop_train + prop_test)
           
           )

4.2 Scaling Data

train_scale <- train %>% 
  mutate(UTC = scale(UTC),
         Temperature_C = scale(Temperature_C),
         Humidity_percent = scale(Humidity_percent),
         TVOC_ppb = scale(TVOC_ppb),
         eCO2_ppm = scale(eCO2_ppm),
         Raw_H2 = scale(Raw_H2),
         Raw_Ethanol = scale(Raw_Ethanol),
         Pressure_hPa = scale(Pressure_hPa),
         PM1.0 = scale(PM1.0),
         PM2.5 = scale(PM2.5),
         NC0.5 = scale(NC0.5),
         NC1.0 = scale(NC1.0),
         NC2.5 = scale(NC2.5),
         CNT = scale(CNT)
        )

test_scale <- test %>% 
  mutate(UTC = scale(UTC),
         Temperature_C = scale(Temperature_C),
         Humidity_percent = scale(Humidity_percent),
         TVOC_ppb = scale(TVOC_ppb),
         eCO2_ppm = scale(eCO2_ppm),
         Raw_H2 = scale(Raw_H2),
         Raw_Ethanol = scale(Raw_Ethanol),
         Pressure_hPa = scale(Pressure_hPa),
         PM1.0 = scale(PM1.0),
         PM2.5 = scale(PM2.5),
         NC0.5 = scale(NC0.5),
         NC1.0 = scale(NC1.0),
         NC2.5 = scale(NC2.5),
         CNT = scale(CNT)
        )

4.3 Data Preprocessing for Deep Learning

4.3.1 Separation of Predictors and Targets

# Prediktor
train_x <- train_scale %>% select(-Fire_Alarm) %>% as.matrix()
train_y <- train_scale$Fire_Alarm %>% as.character()

# Target
test_x <- test_scale %>% select(-Fire_Alarm) %>% as.matrix()
test_y <- test_scale$Fire_Alarm %>% as.character()

4.3.2 Turning Predictors Into Arrays

train_x <- array_reshape(train_x, dim = dim(train_x))
test_x <- array_reshape(test_x, dim = dim(test_x))

4.3.3 One-hot Encoding Target Variable

train_y <- to_categorical(train_y, num_classes = 2)
test_y <- to_categorical(test_y, num_classes = 2)

4.4 Building Architecture

# your code here
input_dim <- ncol(train_x) # jumlah neuron yang ada di input layer
input_dim
#> [1] 14
num_class <- n_distinct(train$Fire_Alarm) # jumlah neuron yang ada di output layer 
num_class
#> [1] 2
model1 <- keras_model_sequential(name="model_keras") %>% 
  layer_dense(units = 64, activation = "relu", input_shape = input_dim, name = "hidden_1") %>%
  layer_dense(units = 16, activation = "relu", name = "hidden_2") %>% 
  layer_dense(units = num_class, activation = "sigmoid", name = "output")

model1
#> Model: "model_keras"
#> ________________________________________________________________________________
#> Layer (type)                        Output Shape                    Param #     
#> ================================================================================
#> hidden_1 (Dense)                    (None, 64)                      960         
#> ________________________________________________________________________________
#> hidden_2 (Dense)                    (None, 16)                      1040        
#> ________________________________________________________________________________
#> output (Dense)                      (None, 2)                       34          
#> ================================================================================
#> Total params: 2,034
#> Trainable params: 2,034
#> Non-trainable params: 0
#> ________________________________________________________________________________

4.5 Model Compile

# your code here
model1 %>% compile(loss = loss_binary_crossentropy(),
                   optimizer = optimizer_sgd(learning_rate = 0.01),
                   metrics = "accuracy")

4.6 Model Fitting

set.seed(100)
# your code here
history <- model1 %>% fit(x = train_x,
                          y = train_y,
                          validation_data = list(test_x, test_y),
                          batch_size = 200,
                          epoch=15)
plot(history)

4.7 Model Evaluation

# your code here
pred <- predict(model1, test_x) %>% k_argmax() %>% as.array() %>% as.factor()
head(pred)
#> [1] 0 0 0 0 0 0
#> Levels: 0 1
confusionMatrix(data = pred, reference = as.factor(test$Fire_Alarm), positive = "1")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction     0     1
#>          0  4640  1278
#>          1     7 10359
#>                                                
#>                Accuracy : 0.9211               
#>                  95% CI : (0.9168, 0.9252)     
#>     No Information Rate : 0.7146               
#>     P-Value [Acc > NIR] : < 0.00000000000000022
#>                                                
#>                   Kappa : 0.8212               
#>                                                
#>  Mcnemar's Test P-Value : < 0.00000000000000022
#>                                                
#>             Sensitivity : 0.8902               
#>             Specificity : 0.9985               
#>          Pos Pred Value : 0.9993               
#>          Neg Pred Value : 0.7840               
#>              Prevalence : 0.7146               
#>          Detection Rate : 0.6361               
#>    Detection Prevalence : 0.6366               
#>       Balanced Accuracy : 0.9443               
#>                                                
#>        'Positive' Class : 1                    
#> 

Pada umumnya kita mengevaluasi model menggunakan 5 ukuran:

  • Re-call/Sensitivity = dari semua data aktual yang 1 (terdapat api), seberapa mampu proporsi model memprediksi benar.
  • Specificity = dari semua data aktual yang 0 (tidak terdapat api), seberapa mampu proporsi model memprediksi yang benar.
  • Accuracy = seberapa mampu model memprediksi dengan benar target Target secara keseluruhan.
  • Precision (Pos Pred Value) = dari semua hasil prediksi Target 1 (terdapat api), seberapa mampu model saya dapat memprediksi benar.
  • Negative Predict Value (Neg Pred Value) : dari semua hasil prediksi Target 0 (tidak terdapat api), seberapa mampu model saya dapat memprediksi benar.

Sebaiknya pada kasus ini, kita memperhatikan metrik selain Accuracy karena data test asli memiliki ketidakseimbangan Target.

  • Sensitivity : 0.8863
  • Specificity : 0.9987
  • Accuracy : 0.9184
  • Pos Pred Value (Precision) : 0.9994
  • Neg Pred Value : 0.7782

Secara keseluruhan model Deep Learning kita sudah baik karena dapat mengklasifikasikan data dengan baik, terlihat pada 4 metrik (Sensitivity, Specificity, Accuracy, dan Precision) tetapi untuk ukuran Neg Pred Value (Kemampuan model memprediksi kelas negatif (0)) masih sedang yaitu 77,82%.

5 Conclusion

Kita telah melalui berbagai proses dalam pembuatan Machine Learning yang dapat membedakan Target 0 (Tidak Terdapat Api) dan 1 (Terdapat Api). Pada proses tersebut, kita telah mencoba Deep Learning dalam membangun Machine Learning. Selain itu, kita telah mengevaluasi Machine Learning dan menghasilkan kesimpulan bahwa algoritma Deep Learning memiliki performa baik dan memiliki catatan pada ukuran Negative Predict Value. Pada projek ini saya juga telah mencoba tuning model (mengatur learning rate, batch_size, neuron, epoch dsb) tetapi hasilnya masih seperti menyerupai di atas. Harapannya melalui projek ini dapat menjadi referensi dalam membangun Machine Learning untuk melakukan klasifikasi sehingga dapat menjadi dasar dalam mengambil keputusan secara efektif dan efisien. Sekian terima kasih banyak yang telah melihat projek ini. Saya juga menyukai masukan dan kolaborasi sehingga apabila ada masukan, saran, kritik atau untuk berkolaborasi dapat menghubungi lewat Linkedin.