library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.4
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.4.4     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.0
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(ggplot2)
library(lubridate)
library(corrplot)
## corrplot 0.92 loaded
library(caret)
## Loading required package: lattice
## 
## Attaching package: 'caret'
## 
## The following object is masked from 'package:purrr':
## 
##     lift
library(nnet)
library(naivebayes)
## naivebayes 1.0.0 loaded
## For more information please visit: 
## https://majkamichal.github.io/naivebayes/
library(lindia)
sensor_data <- read.csv("/Users/manojdannana/Desktop/Manoj/IUPUI/class work/intro to statistics/datasets/Occupancy_Estimation.csv")

About the dataset:

This is a dataset with various sensor readings over time. Each row represents a snapshot of the sensor data at a specific time. Here’s a breakdown of the columns:

  1. Date

  2. Time

  3. S1_Temp: Temperature reading from sensor 1

  4. S2_Temp: Temperature reading from sensor 2

  5. S3_Temp: Temperature reading from sensor 3

  6. S4_Temp: Temperature reading from sensor 4

  7. S1_Light: Light intensity reading from sensor 1

  8. S2_Light: Light intensity reading from sensor 2

  9. S3_Light: Light intensity reading from sensor 3

  10. S4_Light: Light intensity reading from sensor 4

  11. S1_Sound: Sound level reading from sensor 1

  12. S2_Sound: Sound level reading from sensor 2

  13. S3_Sound: Sound level reading from sensor 3

  14. S4_Sound: Sound level reading from sensor 4

  15. S5_CO2: CO2 level reading from sensor 5

  16. S5_CO2_Slope: Slope of CO2 level change over time from sensor 5

  17. S6_PIR: Passive Infrared (PIR) motion detection reading from sensor 6

  18. S7_PIR: Passive Infrared (PIR) motion detection reading from sensor 7

  19. Room_Occupancy_Count: Count of occupants in the room

Let’s do some eda on this dataset

Industry: Smart Building Management

Use Case: Predictive Room Occupancy Management System

Description: In modern smart buildings, optimizing room occupancy is crucial for efficient resource utilization, energy conservation, and enhanced occupant comfort. By leveraging sensor data from temperature, light intensity, sound level, CO2 level, and other environmental factors, we can develop a predictive room occupancy management system.

Key Features:

  1. Real-time Monitoring: Continuously monitor environmental conditions and occupancy status in various rooms of the building.

  2. Predictive Analytics: Utilize historical sensor data to predict future room occupancy counts based on current environmental factors.

  3. Occupancy Forecasting: Forecast occupancy trends for different time intervals (e.g., hourly, daily) to anticipate peak usage periods and plan resource allocation accordingly.

  4. Energy Optimization: Adjust heating, ventilation, and air conditioning (HVAC) systems based on predicted occupancy patterns to optimize energy consumption while maintaining occupant comfort.

  5. Space Utilization: Analyze room occupancy patterns to identify underutilized spaces and optimize space allocation for various activities or functions.

  6. Alerts and Notifications: Send alerts and notifications to building managers or occupants when occupancy levels deviate from expected patterns or reach predefined thresholds.

Application Workflow:

  1. Data Collection: Gather sensor data from temperature sensors, light sensors, sound sensors, CO2 sensors, and occupancy sensors installed in different rooms of the building.

  2. Data Preprocessing: Clean and preprocess the sensor data, including handling missing values, removing outliers, and engineering relevant features such as time-based features.

  3. Model Training: Train machine learning models, such as regression or classification models, using historical sensor data to predict room occupancy counts based on environmental factors.

  4. Real-time Prediction: Deploy the trained models in a real-time predictive analytics system that continuously ingests new sensor data and generates predictions for room occupancy counts.

  5. Visualization and Insights: Visualize the predicted occupancy counts and environmental factors in an intuitive dashboard, providing building managers with actionable insights for optimizing room occupancy and energy usage.

Benefits:

  • Energy Efficiency: By adjusting HVAC systems based on predicted occupancy patterns, the system can significantly reduce energy consumption and operational costs.

  • Improved Comfort: Occupants benefit from optimized room conditions tailored to their occupancy needs, leading to enhanced comfort and satisfaction.

  • Space Optimization: Building managers can identify opportunities to optimize space allocation and utilization based on occupancy trends, leading to better space management and resource allocation.

  • Proactive Maintenance: Early detection of anomalies or deviations in sensor data can facilitate proactive maintenance and address potential issues before they escalate.

Exploratory Data Analysis

Let’s analyse the data to see if we can extract some important information or patterns.

Some changes to the dataset

I would like to first see how the room_occupancy is affected by entities such as light, temperature and sound.

Let’s analyze these elements first

Let’s first create columns for means of sensors used to capture same data

sensor_data$Mean_Temp <- rowMeans(sensor_data[, c("S1_Temp", "S2_Temp", "S3_Temp", "S4_Temp")])
sensor_data$Mean_Light <- rowMeans(sensor_data[, c("S1_Light", "S2_Light", "S3_Light", "S4_Light")])
sensor_data$Mean_Sound <- rowMeans(sensor_data[, c("S1_Sound", "S2_Sound", "S3_Sound", "S4_Sound")])

The Room_Occupancy_Count may seem like a numerical column but it has only four distinct values.

unique(sensor_data$Room_Occupancy_Count)
## [1] 1 2 3 0

It would be better to consider Room_Occupancy_Count as a Categorical column rather than a numerical column. Type Converting the column into a factor would be better for our analysis.

sensor_data$Room_Occupancy_Count <- factor(sensor_data$Room_Occupancy_Count)

Summary Statistics

summary(sensor_data)
##      Date               Time              S1_Temp         S2_Temp     
##  Length:10129       Length:10129       Min.   :24.94   Min.   :24.75  
##  Class :character   Class :character   1st Qu.:25.19   1st Qu.:25.19  
##  Mode  :character   Mode  :character   Median :25.38   Median :25.38  
##                                        Mean   :25.45   Mean   :25.55  
##                                        3rd Qu.:25.63   3rd Qu.:25.63  
##                                        Max.   :26.38   Max.   :29.00  
##     S3_Temp         S4_Temp         S1_Light         S2_Light     
##  Min.   :24.44   Min.   :24.94   Min.   :  0.00   Min.   :  0.00  
##  1st Qu.:24.69   1st Qu.:25.44   1st Qu.:  0.00   1st Qu.:  0.00  
##  Median :24.94   Median :25.75   Median :  0.00   Median :  0.00  
##  Mean   :25.06   Mean   :25.75   Mean   : 25.45   Mean   : 26.02  
##  3rd Qu.:25.38   3rd Qu.:26.00   3rd Qu.: 12.00   3rd Qu.: 14.00  
##  Max.   :26.19   Max.   :26.56   Max.   :165.00   Max.   :258.00  
##     S3_Light         S4_Light        S1_Sound         S2_Sound     
##  Min.   :  0.00   Min.   : 0.00   Min.   :0.0600   Min.   :0.0400  
##  1st Qu.:  0.00   1st Qu.: 0.00   1st Qu.:0.0700   1st Qu.:0.0500  
##  Median :  0.00   Median : 0.00   Median :0.0800   Median :0.0500  
##  Mean   : 34.25   Mean   :13.22   Mean   :0.1682   Mean   :0.1201  
##  3rd Qu.: 50.00   3rd Qu.:22.00   3rd Qu.:0.0800   3rd Qu.:0.0600  
##  Max.   :280.00   Max.   :74.00   Max.   :3.8800   Max.   :3.4400  
##     S3_Sound         S4_Sound          S5_CO2        S5_CO2_Slope     
##  Min.   :0.0400   Min.   :0.0500   Min.   : 345.0   Min.   :-6.29615  
##  1st Qu.:0.0600   1st Qu.:0.0600   1st Qu.: 355.0   1st Qu.:-0.04615  
##  Median :0.0600   Median :0.0800   Median : 360.0   Median : 0.00000  
##  Mean   :0.1581   Mean   :0.1038   Mean   : 460.9   Mean   :-0.00483  
##  3rd Qu.:0.0700   3rd Qu.:0.1000   3rd Qu.: 465.0   3rd Qu.: 0.00000  
##  Max.   :3.6700   Max.   :3.4000   Max.   :1270.0   Max.   : 8.98077  
##      S6_PIR            S7_PIR        Room_Occupancy_Count   Mean_Temp    
##  Min.   :0.00000   Min.   :0.00000   0:8228               Min.   :24.89  
##  1st Qu.:0.00000   1st Qu.:0.00000   1: 459               1st Qu.:25.13  
##  Median :0.00000   Median :0.00000   2: 748               Median :25.36  
##  Mean   :0.09014   Mean   :0.07957   3: 694               Mean   :25.45  
##  3rd Qu.:0.00000   3rd Qu.:0.00000                        3rd Qu.:25.61  
##  Max.   :1.00000   Max.   :1.00000                        Max.   :26.80  
##    Mean_Light       Mean_Sound    
##  Min.   :  0.00   Min.   :0.0525  
##  1st Qu.:  0.00   1st Qu.:0.0625  
##  Median :  0.00   Median :0.0675  
##  Mean   : 24.73   Mean   :0.1376  
##  3rd Qu.: 30.50   3rd Qu.:0.0750  
##  Max.   :193.75   Max.   :3.1825

Oulier Analysis

Let’s plot box plots to check for outliers

ggplot(sensor_data, aes(x = "", y = Mean_Temp)) +
  geom_boxplot() +
  labs(x = "", y = "Mean Temperature") +
  theme_minimal() +
  ggtitle("Box Plot of Mean Temperature") +
  theme(plot.title = element_text(hjust = 0.5)) +
  coord_flip()

ggplot(sensor_data, aes(x = "", y = Mean_Light)) +
  geom_boxplot() +
  labs(x = "", y = "Mean Light") +
  theme_minimal() +
  ggtitle("Box Plot of Mean Light") +
  theme(plot.title = element_text(hjust = 0.5)) +
  coord_flip()

ggplot(sensor_data, aes(x = "", y = Mean_Sound)) +
  geom_boxplot() +
  labs(x = "", y = "Mean Sound") +
  theme_minimal() +
  ggtitle("Box Plot of Mean Sound") +
  theme(plot.title = element_text(hjust = 0.5)) +
  coord_flip()

There seem to be few ouliers in Temperature but the range of those outliers is very small (~ 0.5 degrees) and hence can be ignored as it’s very common for temperature to fluctuate that much during the day.

There seem to be a lot of outliers in Light readings, this could be because of a lot of data being captured during night time where lights are generally switched off. Hence these ouliers can be explained.

In case of sound readings there seem to be an unusual amount of outliers. I feel these are due to some unrecorded factors like a nearby construction or the geographical location of the room for which the observations are taken. Hence I feel that this is weird but this cannot be explained with the data we have.

Correlation Analysis

We are doing this in order to see which columns are relevant and which are not

colnames(sensor_data)
##  [1] "Date"                 "Time"                 "S1_Temp"             
##  [4] "S2_Temp"              "S3_Temp"              "S4_Temp"             
##  [7] "S1_Light"             "S2_Light"             "S3_Light"            
## [10] "S4_Light"             "S1_Sound"             "S2_Sound"            
## [13] "S3_Sound"             "S4_Sound"             "S5_CO2"              
## [16] "S5_CO2_Slope"         "S6_PIR"               "S7_PIR"              
## [19] "Room_Occupancy_Count" "Mean_Temp"            "Mean_Light"          
## [22] "Mean_Sound"
selected_data <- sensor_data[, c("S5_CO2", "S5_CO2_Slope", "S6_PIR", "S7_PIR", "Room_Occupancy_Count", "Mean_Temp", "Mean_Light", "Mean_Sound")]
selected_data <- sapply(selected_data, as.numeric)

selected_data <- na.omit(selected_data)

correlation_matrix <- cor(selected_data)

print(correlation_matrix)
##                          S5_CO2 S5_CO2_Slope    S6_PIR    S7_PIR
## S5_CO2               1.00000000   0.06922002 0.3952649 0.4734369
## S5_CO2_Slope         0.06922002   1.00000000 0.3683741 0.4253462
## S6_PIR               0.39526487   0.36837408 1.0000000 0.5711250
## S7_PIR               0.47343692   0.42534621 0.5711250 1.0000000
## Room_Occupancy_Count 0.66014450   0.60110500 0.6331329 0.6951375
## Mean_Temp            0.82872506   0.15395670 0.4549522 0.4742304
## Mean_Light           0.62662861   0.50745387 0.5932932 0.5897310
## Mean_Sound           0.47243812   0.40177114 0.5631196 0.6042985
##                      Room_Occupancy_Count Mean_Temp Mean_Light Mean_Sound
## S5_CO2                          0.6601445 0.8287251  0.6266286  0.4724381
## S5_CO2_Slope                    0.6011050 0.1539567  0.5074539  0.4017711
## S6_PIR                          0.6331329 0.4549522  0.5932932  0.5631196
## S7_PIR                          0.6951375 0.4742304  0.5897310  0.6042985
## Room_Occupancy_Count            1.0000000 0.6926980  0.8541599  0.6535266
## Mean_Temp                       0.6926980 1.0000000  0.6858731  0.5063958
## Mean_Light                      0.8541599 0.6858731  1.0000000  0.6468916
## Mean_Sound                      0.6535266 0.5063958  0.6468916  1.0000000
corrplot(correlation_matrix, method = "color", type = "upper", order = "hclust", addrect = 8, tl.cex = 0.7, addCoef.col = "black")

From the above correlation heatmap it’s clearly evident that Room_Occupancy_Count is having very strong positive correlation with all the other columns.

Let’s analyse for each of temperature sensors, light sensors and sound sensors

Temperature Sensors

selected_data <- sensor_data[, c("S1_Temp", "S2_Temp", "S3_Temp", "S4_Temp", "Room_Occupancy_Count")]
selected_data <- sapply(selected_data, as.numeric)

selected_data <- na.omit(selected_data)

correlation_matrix <- cor(selected_data)

print(correlation_matrix)
##                        S1_Temp   S2_Temp   S3_Temp   S4_Temp
## S1_Temp              1.0000000 0.7997073 0.9488387 0.8552793
## S2_Temp              0.7997073 1.0000000 0.7655254 0.6965813
## S3_Temp              0.9488387 0.7655254 1.0000000 0.8851858
## S4_Temp              0.8552793 0.6965813 0.8851858 1.0000000
## Room_Occupancy_Count 0.7008682 0.6712635 0.6520466 0.5265087
##                      Room_Occupancy_Count
## S1_Temp                         0.7008682
## S2_Temp                         0.6712635
## S3_Temp                         0.6520466
## S4_Temp                         0.5265087
## Room_Occupancy_Count            1.0000000
corrplot(correlation_matrix, method = "color", type = "upper", order = "hclust", addrect = 8, tl.cex = 0.7, addCoef.col = "black", col = colorRampPalette(c("blue", "white", "red"))(10))

From the above correlation heatmap it’s clearly evident that Room_Occupancy_Count is having very strong positive correlation with all the temperature sensors. But the temperature sensor S2_Temp has a smaller correlation that the other sensors. This should be noted for further analysis.

Light Sensors

selected_data <- sensor_data[, c("S1_Light", "S2_Light", "S3_Light", "S4_Light", "Room_Occupancy_Count")]

selected_data <- sapply(selected_data, as.numeric)
selected_data <- na.omit(selected_data)

correlation_matrix <- cor(selected_data)

print(correlation_matrix)
##                       S1_Light  S2_Light  S3_Light  S4_Light
## S1_Light             1.0000000 0.8420901 0.8164378 0.5108527
## S2_Light             0.8420901 1.0000000 0.7095795 0.4589136
## S3_Light             0.8164378 0.7095795 1.0000000 0.5794836
## S4_Light             0.5108527 0.4589136 0.5794836 1.0000000
## Room_Occupancy_Count 0.8490581 0.7887639 0.7930813 0.3557147
##                      Room_Occupancy_Count
## S1_Light                        0.8490581
## S2_Light                        0.7887639
## S3_Light                        0.7930813
## S4_Light                        0.3557147
## Room_Occupancy_Count            1.0000000
corrplot(correlation_matrix, method = "color", type = "upper", order = "hclust", addrect = 8, tl.cex = 0.7, addCoef.col = "black", col = colorRampPalette(c("white", "grey", "pink"))(10))

From the above correlation heatmap it’s clearly evident that Room_Occupancy_Count is having very strong positive correlation with all the light sensors except S4_Light. This should be noted for further analysis.

Sound Sensors

selected_data <- sensor_data[, c("S1_Sound", "S2_Sound", "S3_Sound", "S4_Sound", "Room_Occupancy_Count")]

selected_data <- sapply(selected_data, as.numeric)
selected_data <- na.omit(selected_data)

correlation_matrix <- cor(selected_data)

print(correlation_matrix)
##                       S1_Sound  S2_Sound  S3_Sound  S4_Sound
## S1_Sound             1.0000000 0.5600616 0.5407361 0.5577329
## S2_Sound             0.5600616 1.0000000 0.5298299 0.5786347
## S3_Sound             0.5407361 0.5298299 1.0000000 0.6966697
## S4_Sound             0.5577329 0.5786347 0.6966697 1.0000000
## Room_Occupancy_Count 0.5737483 0.5578530 0.5316849 0.4602873
##                      Room_Occupancy_Count
## S1_Sound                        0.5737483
## S2_Sound                        0.5578530
## S3_Sound                        0.5316849
## S4_Sound                        0.4602873
## Room_Occupancy_Count            1.0000000
corrplot(correlation_matrix, method = "color", type = "upper", order = "hclust", addrect = 8, tl.cex = 0.7, addCoef.col = "black", col = colorRampPalette(c("darkblue", "blue", "skyblue"))(10))

From the above correlation heatmap it’s clearly evident that Room_Occupancy_Count is having very strong positive correlation with all the sound sensors except S1_Sound. This should be noted for further analysis.

Modelling

Feature Selection

correlation_with_target <- cor(sensor_data[, c( "S1_Temp", "S2_Temp", "S3_Temp", "S4_Temp", "S1_Light", "S2_Light", "S3_Light", "S4_Light", "S1_Sound", "S2_Sound", "S3_Sound", "S4_Sound", "S5_CO2", "S5_CO2_Slope", "S6_PIR", "S7_PIR")], as.numeric(sensor_data$Room_Occupancy_Count))
abs(correlation_with_target) > 0.5
##               [,1]
## S1_Temp       TRUE
## S2_Temp       TRUE
## S3_Temp       TRUE
## S4_Temp       TRUE
## S1_Light      TRUE
## S2_Light      TRUE
## S3_Light      TRUE
## S4_Light     FALSE
## S1_Sound      TRUE
## S2_Sound      TRUE
## S3_Sound      TRUE
## S4_Sound     FALSE
## S5_CO2        TRUE
## S5_CO2_Slope  TRUE
## S6_PIR        TRUE
## S7_PIR        TRUE

From the above correlation analysis we can see that two columns aren’t good for our model, hence we select the all the others.

Features Selected for Model: S1_Temp, S2_Temp, S3_Temp, S4_Temp, S1_Light, S2_Light, S3_Light, S1_Sound, S2_Sound, S3_Sound, S5_C02, S5_C02_Slope, S6_PIR, S7_PIR.

Trying different models

Linear Regression

sensor_data$Room_Occupancy_Count <- as.numeric(sensor_data$Room_Occupancy_Count)
formula <- "Room_Occupancy_Count ~ S1_Temp + S2_Temp + S3_Temp + S4_Temp + S1_Light + S2_Light + S3_Light + S1_Sound + S2_Sound + S3_Sound + S5_CO2 + S5_CO2_Slope + S6_PIR + S7_PIR"

lm_model <- lm(formula, data = sensor_data)

summary(lm_model)
## 
## Call:
## lm(formula = formula, data = sensor_data)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.95093 -0.12862  0.00908  0.08813  2.33055 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -8.601e+00  5.193e-01 -16.563  < 2e-16 ***
## S1_Temp       4.606e-01  4.018e-02  11.465  < 2e-16 ***
## S2_Temp       5.860e-02  9.479e-03   6.183 6.55e-10 ***
## S3_Temp       6.500e-01  2.819e-02  23.061  < 2e-16 ***
## S4_Temp      -7.707e-01  2.031e-02 -37.938  < 2e-16 ***
## S1_Light      4.711e-03  1.746e-04  26.983  < 2e-16 ***
## S2_Light      1.007e-03  9.888e-05  10.181  < 2e-16 ***
## S3_Light      1.700e-03  1.134e-04  14.987  < 2e-16 ***
## S1_Sound      7.130e-02  1.314e-02   5.426 5.89e-08 ***
## S2_Sound      1.102e-01  1.557e-02   7.077 1.57e-12 ***
## S3_Sound     -8.510e-02  1.016e-02  -8.377  < 2e-16 ***
## S5_CO2        1.625e-04  3.746e-05   4.338 1.45e-05 ***
## S5_CO2_Slope  2.024e-01  3.386e-03  59.761  < 2e-16 ***
## S6_PIR        1.725e-01  1.444e-02  11.947  < 2e-16 ***
## S7_PIR        4.335e-01  1.609e-02  26.943  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2959 on 10114 degrees of freedom
## Multiple R-squared:  0.8905, Adjusted R-squared:  0.8903 
## F-statistic:  5874 on 14 and 10114 DF,  p-value: < 2.2e-16

Based on the summary of the model we can observe:

  • All features we have selected are statistically significant as the p-value for all the feature is <0.05.
  • All coefficients have relatively large absolute values, indicating strong relationships with the target variable.
  • The Adjusted R-squared value is 0.8903, suggesting that the model explains around 89.03% of the variance in the target variable.

Based on all the above points we can conclude that this is a better model for the given explanatory and response variables.

Accuracy and Predictions

Let’s partition the data into two sets, one for training and one for testing, the train test split I have considered here is 0.7 which means 70% of data to training and 30% to testing.

set.seed(123)

train_index <- createDataPartition(sensor_data$Room_Occupancy_Count, p = 0.7, list = FALSE)
train_data <- sensor_data[train_index, ]
test_data <- sensor_data[-train_index, ]

lm_model <- lm(formula, data = train_data)

predicted <- predict(lm_model, newdata = test_data)

accuracy <- cor(predicted, test_data$Room_Occupancy_Count)^2

print(accuracy)
## [1] 0.8896556

Overall, we get an accuracy of ~ 89% which is a pretty accuracy. considering all the other results from the above cell, I feel this is a good model.

DIagnosis

gg_diagnose(lm_model)
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'

Residual vs Fitted Value

gg_resfitted(lm_model) +
  geom_smooth(se=FALSE)
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

From the above plot it is clear that linear model model doesn’t really suit our use case.


Linear model doesn’t really suit our use case as the target/response variable although might look to be continuous are actually categorical.

Non-Linear Models

Since the target column Room_Occupancy_Count is categorical, it’s better to go for a classification technique rather than a linear model. Since there are four categories in the target column I will go for softmax transformation instead of a sigmoid transformation because sigmoid transformation works good only in the case of binary classification tasks.

Multi-Nomial Regression

multinom_model <- multinom(factor(Room_Occupancy_Count) ~ S1_Temp + S2_Temp + S3_Temp + S4_Temp + S1_Light + S2_Light + S3_Light + S1_Sound + S2_Sound + S3_Sound + S5_CO2 + S5_CO2_Slope + S6_PIR + S7_PIR, data = sensor_data)
## # weights:  64 (45 variable)
## initial  value 14041.775584 
## iter  10 value 2204.342652
## iter  20 value 1361.447425
## iter  30 value 670.690597
## iter  40 value 275.868263
## iter  50 value 151.700413
## iter  60 value 121.832944
## iter  70 value 116.361861
## iter  80 value 112.475785
## iter  90 value 110.722290
## iter 100 value 110.185607
## final  value 110.185607 
## stopped after 100 iterations
summary(multinom_model)
## Call:
## multinom(formula = factor(Room_Occupancy_Count) ~ S1_Temp + S2_Temp + 
##     S3_Temp + S4_Temp + S1_Light + S2_Light + S3_Light + S1_Sound + 
##     S2_Sound + S3_Sound + S5_CO2 + S5_CO2_Slope + S6_PIR + S7_PIR, 
##     data = sensor_data)
## 
## Coefficients:
##   (Intercept)   S1_Temp    S2_Temp   S3_Temp   S4_Temp   S1_Light    S2_Light
## 2    21.87952 -1.598909 -28.398829 -26.82408  53.39130  0.7576250 -0.13455900
## 3    88.28031 -1.853871  -8.531248  61.15831 -53.49753  0.5240298  0.09895107
## 4  -133.34636 17.186898  -7.458114  44.72936 -48.42625 -0.2784464  0.46533291
##      S3_Light  S1_Sound   S2_Sound   S3_Sound      S5_CO2 S5_CO2_Slope   S6_PIR
## 2 -0.35969307 -1.013038 -9.0746373 -4.7942700  0.03854205     8.521521 9.688373
## 3  0.02578905 -1.291806 -0.8935680 -0.1854925 -0.04128530     7.839662 5.195352
## 4  0.07530602 -1.997713 -0.4411527 -0.2809556 -0.02482201     8.602999 5.660996
##     S7_PIR
## 2 2.291560
## 3 4.024230
## 4 4.559914
## 
## Std. Errors:
##   (Intercept)    S1_Temp    S2_Temp    S3_Temp    S4_Temp   S1_Light  S2_Light
## 2 0.002764958 0.06874802 0.08090234 0.08229162 0.08564311 0.05775641 0.1122372
## 3 0.004106097 0.12300454 0.19817428 0.12635251 0.11160684 0.07110592 0.1112535
## 4 0.001975019 0.07986672 0.21862902 0.08662038 0.06907264 0.17300588 0.1597986
##     S3_Light  S1_Sound   S2_Sound   S3_Sound      S5_CO2 S5_CO2_Slope    S6_PIR
## 2 0.03479643 0.4806437 0.09908168 0.02923712 0.007565350    0.4034594 0.4211150
## 3 0.02721398 0.4142987 0.33682774 0.26924266 0.005240530    0.1971412 0.3119308
## 4 0.02391356 0.4161612 0.30674281 0.26540352 0.002793867    0.2298846 0.3203523
##      S7_PIR
## 2 0.5511972
## 3 0.3563774
## 4 0.3786651
## 
## Residual Deviance: 220.3712 
## AIC: 310.3712
Accuracy and Predictions
predictions <- predict(multinom_model, newdata = test_data, type = "class")

accuracy <- mean(predictions == as.factor(test_data$Room_Occupancy_Count))

print(accuracy)
## [1] 0.9967084

The accuracy of the model is 99.6% which is a great accuracy but this can also lead to over fitting where the model has memorized the data in the dataset rather than understanding the underlying patterns.

conf_mat <- confusionMatrix(predictions, factor(test_data$Room_Occupancy_Count))
conf_mat
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    1    2    3    4
##          1 2489    0    0    0
##          2    0  134    1    0
##          3    0    2  199    3
##          4    1    0    3  206
## 
## Overall Statistics
##                                          
##                Accuracy : 0.9967         
##                  95% CI : (0.994, 0.9984)
##     No Information Rate : 0.8196         
##     P-Value [Acc > NIR] : < 2.2e-16      
##                                          
##                   Kappa : 0.9896         
##                                          
##  Mcnemar's Test P-Value : NA             
## 
## Statistics by Class:
## 
##                      Class: 1 Class: 2 Class: 3 Class: 4
## Sensitivity            0.9996  0.98529  0.98030  0.98565
## Specificity            1.0000  0.99966  0.99824  0.99859
## Pos Pred Value         1.0000  0.99259  0.97549  0.98095
## Neg Pred Value         0.9982  0.99931  0.99859  0.99894
## Prevalence             0.8196  0.04477  0.06682  0.06880
## Detection Rate         0.8193  0.04411  0.06550  0.06781
## Detection Prevalence   0.8193  0.04444  0.06715  0.06912
## Balanced Accuracy      0.9998  0.99247  0.98927  0.99212
conf_table <- conf_mat$table

# Compute precision for each class
precision <- diag(conf_table) / rowSums(conf_table)
print(precision)
##         1         2         3         4 
## 1.0000000 0.9925926 0.9754902 0.9809524

from the above confusion matrix it’s clear that multi nomial classification works well with all the classes.

Let’s go for another probabilistic approach with a bayesian technique

Naive Bayes Classification

nb_model <- naive_bayes(factor(Room_Occupancy_Count) ~ S1_Temp + S2_Temp + S3_Temp + S4_Temp + S1_Light + S2_Light + S3_Light + S1_Sound + S2_Sound + S3_Sound + S5_CO2 + S5_CO2_Slope + S6_PIR + S7_PIR, data = train_data)

summary(nb_model)
## 
## ================================= Naive Bayes ================================== 
##  
## - Call: naive_bayes.formula(formula = factor(Room_Occupancy_Count) ~      S1_Temp + S2_Temp + S3_Temp + S4_Temp + S1_Light + S2_Light +          S3_Light + S1_Sound + S2_Sound + S3_Sound + S5_CO2 +          S5_CO2_Slope + S6_PIR + S7_PIR, data = train_data) 
## - Laplace: 0 
## - Classes: 4 
## - Samples: 7091 
## - Features: 14 
## - Conditional distributions: 
##     - Gaussian: 14
## - Prior probabilities: 
##     - 1: 0.8092
##     - 2: 0.0456
##     - 3: 0.0769
##     - 4: 0.0684
## 
## --------------------------------------------------------------------------------
predictions <- predict(nb_model, newdata = subset(test_data, select = c("S1_Temp", "S2_Temp", "S3_Temp", "S4_Temp", "S1_Light", "S2_Light", "S3_Light", "S1_Sound", "S2_Sound", "S3_Sound", "S5_CO2", "S5_CO2_Slope", "S6_PIR", "S7_PIR")))

accuracy <- mean(predictions == sensor_data$Room_Occupancy_Count)
## Warning in `==.default`(predictions, sensor_data$Room_Occupancy_Count): longer
## object length is not a multiple of shorter object length
## Warning in is.na(e1) | is.na(e2): longer object length is not a multiple of
## shorter object length
print(paste("Accuracy:", accuracy))
## [1] "Accuracy: 0.663540329746273"

With this model we get an accuracy of 66.3%. Let’s decide from multinomial and naive bayes by doing a precision check.

conf_mat <- confusionMatrix(predictions, factor(test_data$Room_Occupancy_Count))
conf_mat
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    1    2    3    4
##          1 2443    0    0    9
##          2    0  131    0    0
##          3    0    5  196   40
##          4   47    0    7  160
## 
## Overall Statistics
##                                           
##                Accuracy : 0.9645          
##                  95% CI : (0.9572, 0.9707)
##     No Information Rate : 0.8196          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.8911          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: 1 Class: 2 Class: 3 Class: 4
## Sensitivity            0.9811  0.96324  0.96552  0.76555
## Specificity            0.9836  1.00000  0.98413  0.98091
## Pos Pred Value         0.9963  1.00000  0.81328  0.74766
## Neg Pred Value         0.9198  0.99828  0.99750  0.98265
## Prevalence             0.8196  0.04477  0.06682  0.06880
## Detection Rate         0.8041  0.04312  0.06452  0.05267
## Detection Prevalence   0.8071  0.04312  0.07933  0.07044
## Balanced Accuracy      0.9824  0.98162  0.97482  0.87323
conf_table <- conf_mat$table

precision <- diag(conf_table) / rowSums(conf_table)
print(precision)
##         1         2         3         4 
## 0.9963295 1.0000000 0.8132780 0.7476636

From the above metrics we see that the model is very good at predicting Class 1 and class 2 and bad at predicting class 3 and class 4.

Model Selection

We have 2 models that look like they both might suit our use case, but we need to choose only one. Since multi-nomial classification seems to be a better fit for all the classes with high accuracy and better precision rates we can choose that.

Appendix

Time Series Analysis

sensor_data$DateTime <- as.POSIXct(paste(sensor_data$Date, sensor_data$Time), format="%d/%m/%y %H:%M:%S")

ggplot(sensor_data, aes(x = DateTime)) +
  geom_line(aes(y = Mean_Temp, color = "Mean_Temp"), linewidth = 1) +
  geom_line(aes(y = Mean_Light, color = "Mean_Light"), linewidth = 1) +
  geom_line(aes(y = Mean_Sound, color = "Mean_Sound"), linewidth = 1) +
  geom_line(aes(y = S5_CO2, color = "S5_CO2"), linewidth = 1) +
  geom_line(aes(y = as.numeric(Room_Occupancy_Count), color = "Occupancy_Count"), linewidth = 1) +  
  scale_color_manual(name = "Variables", 
                     values = c(Mean_Temp = "blue", Mean_Light = "green", Mean_Sound = "red", S5_CO2 = "purple", Occupancy_Count = "orange"),
                     labels = c("Temperature", "Light Intensity", "Sound Level", "CO2 Level", "Occupancy Count")) +
  labs(x = "Time", y = "Value") +
  theme_minimal()

From the above plot it’s there seems to be something wrong with data, it seems as if there is some data missing for some days. Let’s analyse

unique(sensor_data$Date)
## [1] "22/12/17" "23/12/17" "24/12/17" "25/12/17" "26/12/17" "10/01/18" "11/01/18"

It’s as I assumed, the dataset is missing dates from 27 DEC 2017 to 9 JAN 2018. Hence the flat line for those days.

The patterns in the plot clearly indicate the presence of day and night, columns such as Mean_Temperature, Mean_Light and Mean_Sound show a trend of becoming less in the night and more in the day.

Conclusion

The predictive room occupancy management system offers a comprehensive solution for smart building management, enabling efficient resource utilization, energy conservation, and enhanced occupant comfort. By leveraging advanced analytics and real-time monitoring, building operators can make data-driven decisions to optimize building performance and create more sustainable and comfortable environments.