Projeto Final do Módulo de Machine Learning I¶
Descrição:¶
Treinar os modelos KNN e RandomForest para um problema de classificação, otimizar os hiperparâmetros e comparar a performance de cada modelo.
Regras:¶
- Tente utilizar todos os tópicos aprendidos em sala de aula.
Grupo composto por:¶
- Rayssa Vilaça
Problema¶

Motivação: Encontrar o momento ideal para roubar uma casa.
Objetivo: Predizer se um cômodo está ocupado ou desocupado com base em dados do ambiente, incluindo temperatura, umidade relativa, luminosidade e concentração de dióxido de carbono.
Dados¶
Dados experimentais usados para classificação binária sobre a ocupação de um cômodo. A base de dados foi obtida através do Kaggle.
A base contém um único arquivo chamado file.csv. Este arquivo possui as seguintes colunas:
Temperature: Temperatura em grau Celsius
Humidity: Umidade relativa em porcentagem
Light: Quantidade de luz em lux
C02: Concentração de dióxido de carbono em ppm
HumidityRatio: Razão de humidade em kgwater-vapor/kg-air (Quantidade derivada da temperatura e humidade relativa)
Occupancy: Status da ocupação
- 1 - Há chances de que o cômodo esteja ocupado
- 0 - Sem chances de que o cômodo esteja ocupado
Importações¶
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import randint
import plotly.graph_objects as go
from sklearn.neighbors import KNeighborsClassifier
from mlxtend.plotting import plot_confusion_matrix
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.model_selection import RandomizedSearchCV
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, roc_curve, roc_auc_score
Configurações iniciais¶
# Semente
RANDOM_STATE = 666
# Cores
BACKGROUND_COLOR = '#191622'
COLOR = '#868686'
BRANCO = '#fff'
# Fonte
FONT_FAMILY = 'Balto'
# Estilo pandas
plt.style.use('fivethirtyeight')
# Estilo gráficos
layout_padrao = dict(
# Gerais
width=1000,
height=600,
font=dict(
color=BRANCO,
family=FONT_FAMILY
),
margin=dict(
l=60,
r=30,
b=80,
t=50,
),
paper_bgcolor=BACKGROUND_COLOR,
plot_bgcolor=BACKGROUND_COLOR,
# Titulo
title_font_size=20,
title_x=0.5,
title_xanchor='center',
# Eixo X
xaxis=dict(
gridcolor=COLOR,
gridwidth=1,
zerolinecolor=COLOR,
zerolinewidth=2,
tickfont_size=14,
title_font_size=16,
),
# Eixo Y
yaxis=dict(
gridcolor=COLOR,
gridwidth=1,
zerolinecolor=COLOR,
zerolinewidth=2,
tickfont_size=14,
title_font_size=16,
)
)
Análise do dataset¶
# Primeiramente, vamos carregar o dataset e olhar os cinco primeiros registros
ocupacao_dados = pd.read_csv("assets/file.csv")
ocupacao_dados.head()
Temperature | Humidity | Light | CO2 | HumidityRatio | Occupancy | |
---|---|---|---|---|---|---|
0 | 23.7000 | 26.272 | 585.200000 | 749.200000 | 0.004764 | 1 |
1 | 23.7180 | 26.290 | 578.400000 | 760.400000 | 0.004773 | 1 |
2 | 23.7300 | 26.230 | 572.666667 | 769.666667 | 0.004765 | 1 |
3 | 23.7225 | 26.125 | 493.750000 | 774.750000 | 0.004744 | 1 |
4 | 23.7540 | 26.200 | 488.600000 | 779.000000 | 0.004767 | 1 |
# Vamos verificar o tamanho do dataset através da quantidade de registros e atributos
linhas = ocupacao_dados.shape[0]
colunas = ocupacao_dados.shape[1]
print(f"O dataset possui {linhas} registros e {colunas} atributos.")
O dataset possui 2665 registros e 6 atributos.
# Conhecendo melhor o dataset identificando os tipos de dados de cada coluna
ocupacao_dados.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 2665 entries, 0 to 2664 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Temperature 2665 non-null float64 1 Humidity 2665 non-null float64 2 Light 2665 non-null float64 3 CO2 2665 non-null float64 4 HumidityRatio 2665 non-null float64 5 Occupancy 2665 non-null int64 dtypes: float64(5), int64(1) memory usage: 125.0 KB
# Verificando se há valores nulos para serem tratados nas colunas
ocupacao_dados.isna().sum()
Temperature 0 Humidity 0 Light 0 CO2 0 HumidityRatio 0 Occupancy 0 dtype: int64
# Vamos ter uma visão geral dos dados por meio do resumo estatístico de cada coluna
ocupacao_dados.describe()
Temperature | Humidity | Light | CO2 | HumidityRatio | Occupancy | |
---|---|---|---|---|---|---|
count | 2665.000000 | 2665.000000 | 2665.000000 | 2665.000000 | 2665.000000 | 2665.000000 |
mean | 21.433876 | 25.353937 | 193.227556 | 717.906470 | 0.004027 | 0.364728 |
std | 1.028024 | 2.436842 | 250.210906 | 292.681718 | 0.000611 | 0.481444 |
min | 20.200000 | 22.100000 | 0.000000 | 427.500000 | 0.003303 | 0.000000 |
25% | 20.650000 | 23.260000 | 0.000000 | 466.000000 | 0.003529 | 0.000000 |
50% | 20.890000 | 25.000000 | 0.000000 | 580.500000 | 0.003815 | 0.000000 |
75% | 22.356667 | 26.856667 | 442.500000 | 956.333333 | 0.004532 | 1.000000 |
max | 24.408333 | 31.472500 | 1697.250000 | 1402.250000 | 0.005378 | 1.000000 |
# Verificando a distribuição dos dados por labelb
qtd_por_ocupacao = ocupacao_dados['Occupancy'].value_counts().sort_values().reset_index()
qtd_por_ocupacao['Occupancy Label'] = qtd_por_ocupacao['Occupancy'].apply(lambda x: 'Ocupado' if x == 1 else 'Não ocupado')
fig = go.Figure(
go.Bar(
x=qtd_por_ocupacao['Occupancy Label'],
y=qtd_por_ocupacao['count']
)
)
fig.update_layout(
title_text="Frequência por ocupação",
yaxis_title='Quantidade de cômodos',
# Padrao
**layout_padrao,
# Eixo X
xaxis_showgrid=False,
yaxis_showgrid=False,
)
fig.show()
Podemos observar que o dataset possui mais dados para a classe Não ocupado ou 0.
# Antes de treinar o modelo, iremos verificar a correlação entre as variáveis
corr = ocupacao_dados.corr()
fig = go.Figure(
go.Heatmap(
z=corr,
x=corr.columns,
y=corr.columns,
colorscale='Viridis'
)
)
fig.update_layout(
title_text="Matriz de Correlação",
# Padrao
**layout_padrao,
)
fig.show()
"""
Com base na matriz de correlação, é evidente que a variável HumidityRatio, obtida a partir
das variáveis Temperature e Humidity, apresenta uma correlação significativa com essas variáveis.
Portanto, será excluída do conjunto de dados.
As variáveis serão divididas em dois conjuntos, variáveis independentes representada por X e variável dependente y.
"""
X = ocupacao_dados[['Temperature', 'Humidity', 'Light', 'CO2']]
y = ocupacao_dados['Occupancy']
# Após definirmos X e y, iremos dividir os dados em conjuntos de treinamento e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=RANDOM_STATE)
Quais métricas utilizar?¶
Para este problema, acredito que os Falsos Negativos são mais problemáticos do que os Falsos Positivos. Imagine alguém que possua mesmo os dados de ambiente do local que pretende invadir. Se o modelo prever que o cômodo está vazio, esse indíviduo pode interpretar isso como uma oportunidade para adentrar e cometer o roubo. No entanto, se for um caso de Falso Negativo, ou seja, o modelo prever incorretamente que não há ninguém quando na verdade o cômodo está ocupado, o invasor corre sérios riscos de ser pego em flagrante.
Por outro lado, no caso de um Falso Positivo, o modelo prevê erroneamente que o cômodo está ocupado quando na verdade não está. Isso significa que o indivíduo deixou uma ótima oportunidade passar. Entretanto, quando se trata de escolher entre perder uma oportunidade ou os dentes, é preferível o primeiro. Portanto, o recall é uma métrica interessante de ser utilizada, uma vez que avalia o erro do tipo II, ou seja, a quantidade de Falsos Negativos identificados pelo modelo.

KNN com hiperparâmetros default¶
def criar_df_metricas(y_train, y_train_pred, y_test, y_test_pred):
"""
Função que retorna as métricas para modelo de classificação para dados de treino e teste.
Parâmetros:
y_train (Series): labels de treino.
y_train_pred (Series): labels obtidas pelo modelo ao prever utilizando X_train.
y_test (Series): labels de teste.
y_test_pred (Series): labels obtidas pelo modelo ao prever utilizando X_test.
Retorna:
Dataframe: Dataframe com as métricas para treino e teste.
"""
metricas = pd.DataFrame(index=['train', 'test'])
metricas.loc['train', 'accuracy'] = accuracy_score(y_train, y_train_pred)
metricas.loc['train', 'precision'] = precision_score(y_train, y_train_pred)
metricas.loc['train', 'recall'] = recall_score(y_train, y_train_pred)
metricas.loc['train', 'f1'] = f1_score(y_train, y_train_pred)
metricas.loc['train', 'roc_auc'] = roc_auc_score(y_train, y_train_pred)
metricas.loc['test', 'accuracy'] = accuracy_score(y_test, y_test_pred)
metricas.loc['test', 'precision'] = precision_score(y_test, y_test_pred)
metricas.loc['test', 'recall'] = recall_score(y_test, y_test_pred)
metricas.loc['test', 'f1'] = f1_score(y_test, y_test_pred)
metricas.loc['test', 'roc_auc'] = roc_auc_score(y_test, y_test_pred)
return metricas
# Treinar o modelo KNN sem a utilização de otimização de hiperparâmetros
knn = KNeighborsClassifier()
knn.fit(X_train, y_train)
# Prever as classes utilizando o X de treino
y_train_pred = knn.predict(X_train)
# Prever as classes utilizando o X de teste
y_test_pred = knn.predict(X_test)
# Obtendo as principais métricas de desempenho para o classificador
print(criar_df_metricas(y_train, y_train_pred, y_test, y_test_pred))
accuracy precision recall f1 roc_auc train 0.989212 0.978589 0.992337 0.985415 0.989868 test 0.983114 0.963918 0.989418 0.976501 0.984535
matriz_confusao = confusion_matrix(y_test, y_test_pred)
fig = go.Figure(
go.Heatmap(
z=matriz_confusao,
text=matriz_confusao,
texttemplate="%{text}",
textfont={"size":20},
x=['False', 'True'],
y=['False', 'True'],
colorscale='Viridis'
)
)
fig.update_layout(
title_text="Matriz de Confusão",
xaxis_title='Predicted label',
yaxis_title='True label',
# Padrao
**layout_padrao,
)
fig.show()
KNN com hiperparâmetros otimizados¶
# Selecionando os parâmetros que serão testados
parametros = {
'n_neighbors': randint(1, 50),
'weights': ['uniform', 'distance'],
'p': [1, 2]
}
# Configurando a busca para o classificador KNN utilizando os parâmetros selecionados
random_search = RandomizedSearchCV(
estimator = KNeighborsClassifier(),
param_distributions = parametros,
n_iter = 200,
cv = 5,
random_state=RANDOM_STATE
)
# Ajustando a pesquisa aleatória ao conjunto de treinamento
random_search.fit(X_train, y_train)
# obtendo os melhores parâmetros
knn_melhores_parametros = random_search.best_params_
print(knn_melhores_parametros)
{'n_neighbors': 20, 'p': 1, 'weights': 'distance'}
# Instanciando um novo classificador KNN configurado com os melhores parâmetros
knn_parametros_otimizados = KNeighborsClassifier(**knn_melhores_parametros)
# Treinar o modelo
knn_parametros_otimizados.fit(X_train, y_train)
# Prever as classes utilizando o X de treino
y_train_pred = knn_parametros_otimizados.predict(X_train)
# Prever as classes utilizando o X de teste
y_test_pred = knn_parametros_otimizados.predict(X_test)
# Obtendo as principais métricas de desempenho para o classificador
print(criar_df_metricas(y_train, y_train_pred, y_test, y_test_pred))
accuracy precision recall f1 roc_auc train 1.000000 1.000000 1.000000 1.000000 1.000000 test 0.983114 0.959184 0.994709 0.976623 0.985727
matriz_confusao = confusion_matrix(y_test, y_test_pred)
fig = go.Figure(
go.Heatmap(
z=matriz_confusao,
text=matriz_confusao,
texttemplate="%{text}",
textfont={"size":20},
x=['False', 'True'],
y=['False', 'True'],
colorscale='Viridis'
)
)
fig.update_layout(
title_text="Matriz de Confusão",
xaxis_title='Predicted label',
yaxis_title='True label',
# Padrao
**layout_padrao,
)
fig.show()
Random Forest com hiperparâmetros default¶
# Treinar o modelo RandomForest sem a utilização de otimização de hiperparâmetros
random_forest = RandomForestClassifier(random_state=RANDOM_STATE)
random_forest.fit(X_train, y_train)
# Prever as classes utilizando o X de treino
y_train_pred = random_forest.predict(X_train)
# Prever as classes utilizando o X de teste
y_test_pred = random_forest.predict(X_test)
# Obtendo as principais métricas de desempenho para o classificador
print(criar_df_metricas(y_train, y_train_pred, y_test, y_test_pred))
accuracy precision recall f1 roc_auc train 1.000000 1.000000 1.000000 1.000000 1.000000 test 0.983114 0.973684 0.978836 0.976253 0.982151
matriz_confusao = confusion_matrix(y_test, y_test_pred)
fig = go.Figure(
go.Heatmap(
z=matriz_confusao,
text=matriz_confusao,
texttemplate="%{text}",
textfont={"size":20},
x=['False', 'True'],
y=['False', 'True'],
colorscale='Viridis'
)
)
fig.update_layout(
title_text="Matriz de Confusão",
xaxis_title='Predicted label',
yaxis_title='True label',
# Padrao
**layout_padrao,
)
fig.show()
# Selecionando os parâmetros que serão testados
parametros = {
'n_estimators': randint(10, 200),
'criterion': ['gini', 'entropy'],
'max_depth': [None] + list(randint(1, 20).rvs(5)),
'min_samples_split': randint(2, 20),
'min_samples_leaf': randint(1, 10),
}
random_search = RandomizedSearchCV(
estimator=RandomForestClassifier(),
param_distributions=parametros,
n_iter=200,
cv=5,
random_state=RANDOM_STATE
)
# Ajustando a pesquisa aleatória ao conjunto de treinamento
random_search.fit(X_train, y_train)
# obtendo os melhores parâmetros
random_forest_melhores_parametros = random_search.best_params_
print(random_forest_melhores_parametros)
{'criterion': 'gini', 'max_depth': 9, 'min_samples_leaf': 1, 'min_samples_split': 6, 'n_estimators': 88}
# Instanciando um novo classificador RandomForest configurado com os melhores parâmetros
random_forest_parametros_otimizados = RandomForestClassifier(**random_forest_melhores_parametros, random_state=RANDOM_STATE)
# Treinar o modelo
random_forest_parametros_otimizados.fit(X_train, y_train)
y_train_pred = random_forest_parametros_otimizados.predict(X_train)
y_test_pred = random_forest_parametros_otimizados.predict(X_test)
# Obtendo as principais métricas de desempenho para o classificador
print(criar_df_metricas(y_train, y_train_pred, y_test, y_test_pred))
accuracy precision recall f1 roc_auc train 0.996248 0.989886 1.000000 0.994917 0.997035 test 0.981238 0.968586 0.978836 0.973684 0.980697
matriz_confusao = confusion_matrix(y_test, y_test_pred)
fig = go.Figure(
go.Heatmap(
z=matriz_confusao,
text=matriz_confusao,
texttemplate="%{text}",
textfont={"size":20},
x=['False', 'True'],
y=['False', 'True'],
colorscale='Viridis'
)
)
fig.update_layout(
title_text="Matriz de Confusão",
xaxis_title='Predicted label',
yaxis_title='True label',
# Padrao
**layout_padrao,
)
fig.show()
A métrica recall para o modelo de classificação utilizando KNN é bem próximo a 1, indicando pouquíssimos casos de Falsos Negativos. Desta forma, considerando essa métrica, a performance do KNN supera a do RandomForest e, portando, deve ser preferível.