Qualidade do Ar na Cidade: Modelos EDA e ML
1 Introdução
Com o crescimento acelerado das cidades e o aumento das atividades industriais e veiculares, a qualidade do ar urbano tem se tornado uma preocupação crítica para a saúde pública e o meio ambiente. Altos níveis de poluentes atmosféricos como material particulado (PM2.5, PM10), monóxido de carbono (CO) e dióxidos de nitrogênio (NO₂) estão diretamente associados a doenças respiratórias, cardiovasculares e a uma menor qualidade de vida.
Diante desse cenário, este projeto tem como objetivo analisar dados de qualidade do ar coletados em diferentes cidades, utilizando técnicas de análise exploratória de dados (EDA) para identificar padrões e tendências, além de aplicar modelos de machine learning capazes de prever a condição do ar com base nos poluentes medidos. Essa abordagem permite não apenas compreender melhor os fatores que influenciam a poluição urbana, mas também fornecer uma ferramenta preditiva que pode auxiliar autoridades e cidadãos na tomada de decisões mais informadas e preventivas.
2 Importando bibliotecas importantes e dados
library(data.table)
library(dplyr)
library(ggplot2)
library(caret)
library(randomForest)
library(tidyr)
library(lubridate)
library(gridExtra)
library(corrplot)
library(GGally) df = read.csv(file = path)
head(df) Date City CO NO2 SO2 O3 PM2.5 PM10 Type
1 2024-01-01 00:00:00+00:00 Moscow 208 15.9 13.2 44 8.6 9.4 Industrial
2 2024-01-01 01:00:00+00:00 Moscow 207 17.4 13.7 44 8.6 10.5 Industrial
3 2024-01-01 02:00:00+00:00 Moscow 217 19.0 15.5 43 10.4 12.9 Industrial
4 2024-01-01 03:00:00+00:00 Moscow 231 21.0 20.7 36 12.3 15.3 Industrial
5 2024-01-01 04:00:00+00:00 Moscow 263 34.5 27.2 27 13.6 20.0 Industrial
6 2024-01-01 05:00:00+00:00 Moscow 352 49.5 34.2 12 17.6 25.8 Industrial
3 Limpeza e preparação dos dados
Nesta etapa, realizamos o pré-processamento básico dos dados para garantir a qualidade e a consistência da análise. Primeiramente, convertemos a coluna de datas para um formato adequado de data/hora, permitindo manipulações temporais precisas. Em seguida, verificamos a presença de valores faltantes em cada coluna para identificar possíveis lacunas nos dados que possam afetar os resultados. Também conferimos se existem linhas duplicadas no conjunto, uma vez que registros repetidos podem distorcer as análises e modelagens futuras. Por fim, removemos as duplicatas para garantir que cada observação seja única, preservando a integridade dos dados para as próximas etapas do projeto.
# Converter a coluna 'Date' para Date ou POSIXct
df$Date <- as.POSIXct(df$Date, format = "%Y-%m-%d") # ajuste o formato conforme sua data
# Verificar valores faltantes em cada coluna
missing_values <- sapply(df, function(x) sum(is.na(x)))
print("Missing values in each column:")[1] "Missing values in each column:"
print(missing_values) Date City CO NO2 SO2 O3 PM2.5 PM10 Type
0 0 0 0 0 0 0 0 0
# Verificar número de linhas duplicadas
num_duplicates <- sum(duplicated(df))
cat("\nNumber of duplicate rows:", num_duplicates, "\n")
Number of duplicate rows: 1
# Remover linhas duplicadas (mantendo a primeira ocorrência)
df <- df[!duplicated(df), ]4 Estatísticas descritivas
# Sumário estatístico geral para todas as variáveis numéricas
cat("\nEstatísticas resumidas para todos os poluentes:\n")
Estatísticas resumidas para todos os poluentes:
summary(df) Date City CO
Min. :2024-01-01 00:00:00.00 Length:52703 Min. : 0
1st Qu.:2024-04-01 00:00:00.00 Class :character 1st Qu.: 187
Median :2024-07-02 00:00:00.00 Mode :character Median : 268
Mean :2024-07-01 12:02:28.35 Mean : 508
3rd Qu.:2024-10-01 00:00:00.00 3rd Qu.: 519
Max. :2024-12-31 00:00:00.00 Max. :12876
NO2 SO2 O3 PM2.5
Min. : 0.90 Min. : 0.00 Min. : 0.00 Min. : 0.30
1st Qu.: 11.00 1st Qu.: 0.70 1st Qu.: 26.00 1st Qu.: 6.40
Median : 23.30 Median : 10.50 Median : 48.00 Median : 14.80
Mean : 29.62 Mean : 22.39 Mean : 53.42 Mean : 32.93
3rd Qu.: 42.20 3rd Qu.: 30.20 3rd Qu.: 69.00 3rd Qu.: 42.60
Max. :218.00 Max. :497.80 Max. :342.00 Max. :459.10
PM10 Type
Min. : 0.40 Length:52703
1st Qu.: 9.40 Class :character
Median : 19.80 Mode :character
Mean : 50.65
3rd Qu.: 68.40
Max. :661.20
# Sumário estatístico agrupado por 'Type' (supondo que seja um fator ou caractere)
cat("\nEstatísticas resumidas agrupadas por tipo de cidade:\n")
Estatísticas resumidas agrupadas por tipo de cidade:
library(dplyr)
df %>%
group_by(Type) %>%
summarise(across(where(is.numeric), list(
min = min,
q1 = ~quantile(.x, 0.25),
median = median,
mean = mean,
q3 = ~quantile(.x, 0.75),
max = max,
sd = sd,
n = ~sum(!is.na(.x))
), .names = "{col}_{fn}"))# A tibble: 2 × 49
Type CO_min CO_q1 CO_median CO_mean CO_q3 CO_max CO_sd CO_n NO2_min NO2_q1
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <dbl> <dbl>
1 Indust… 0 304 496 795. 911 12876 884. 26352 0.9 23.1
2 Reside… 103 162 189 221. 242 1602 110. 26351 1 6.9
# ℹ 38 more variables: NO2_median <dbl>, NO2_mean <dbl>, NO2_q3 <dbl>,
# NO2_max <dbl>, NO2_sd <dbl>, NO2_n <int>, SO2_min <dbl>, SO2_q1 <dbl>,
# SO2_median <dbl>, SO2_mean <dbl>, SO2_q3 <dbl>, SO2_max <dbl>,
# SO2_sd <dbl>, SO2_n <int>, O3_min <dbl>, O3_q1 <dbl>, O3_median <dbl>,
# O3_mean <dbl>, O3_q3 <dbl>, O3_max <dbl>, O3_sd <dbl>, O3_n <int>,
# PM2.5_min <dbl>, PM2.5_q1 <dbl>, PM2.5_median <dbl>, PM2.5_mean <dbl>,
# PM2.5_q3 <dbl>, PM2.5_max <dbl>, PM2.5_sd <dbl>, PM2.5_n <int>, …
# Sumário estatístico agrupado por 'City'
cat("\nEstatísticas resumidas agrupadas por cidade:\n")
Estatísticas resumidas agrupadas por cidade:
df %>%
group_by(City) %>%
summarise(across(where(is.numeric), list(
min = min,
q1 = ~quantile(.x, 0.25),
median = median,
mean = mean,
q3 = ~quantile(.x, 0.75),
max = max,
sd = sd,
n = ~sum(!is.na(.x))
), .names = "{col}_{fn}"))# A tibble: 6 × 49
City CO_min CO_q1 CO_median CO_mean CO_q3 CO_max CO_sd CO_n NO2_min NO2_q1
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <dbl> <dbl>
1 Beiji… 0 481 750 1203. 1421. 12876 1246. 8784 3.4 36.3
2 Delhi 138 397 625 837. 1076 4567 622. 8784 0.9 15
3 Moscow 141 231 286 346. 392 2267 189. 8784 6.2 25
4 Stock… 104 153 173 177. 193 492 37.8 8783 1 5.3
5 Vanco… 103 191 241 285. 323 1602 156. 8784 1.2 13.3
6 Zurich 109 158 183 201. 227 606 62.5 8784 1 6.1
# ℹ 38 more variables: NO2_median <dbl>, NO2_mean <dbl>, NO2_q3 <dbl>,
# NO2_max <dbl>, NO2_sd <dbl>, NO2_n <int>, SO2_min <dbl>, SO2_q1 <dbl>,
# SO2_median <dbl>, SO2_mean <dbl>, SO2_q3 <dbl>, SO2_max <dbl>,
# SO2_sd <dbl>, SO2_n <int>, O3_min <dbl>, O3_q1 <dbl>, O3_median <dbl>,
# O3_mean <dbl>, O3_q3 <dbl>, O3_max <dbl>, O3_sd <dbl>, O3_n <int>,
# PM2.5_min <dbl>, PM2.5_q1 <dbl>, PM2.5_median <dbl>, PM2.5_mean <dbl>,
# PM2.5_q3 <dbl>, PM2.5_max <dbl>, PM2.5_sd <dbl>, PM2.5_n <int>, …
5 Análise exploratória dos dados
# Estilo de gráfico
theme_set(theme_minimal())Análise Univariada (Distribuição de cada poluente)
pollutants <- c("CO", "NO2", "SO2", "O3", "PM2.5", "PM10") # Histograma com densidade plot_list <- lapply(pollutants, function(p) { ggplot(df, aes_string(x = p)) + geom_histogram(aes(y = ..density..), fill = "skyblue", bins = 30, color = "black") + geom_density(color = "red") + ggtitle(paste("Distribuição de", p)) }) # Exibir os 6 plots em grid grid.arrange(grobs = plot_list, ncol = 3)Análise Bivariada (Níveis por tipo de cidade)
plot_list2 <- lapply(pollutants, function(p) { ggplot(df, aes_string(x = "Type", y = p)) + geom_boxplot(fill = "orange", alpha = 0.7) + ggtitle(paste(p, "por Tipo de Cidade")) }) grid.arrange(grobs = plot_list2, ncol = 3)Análise de Correlação
# Matriz de correlação cor_matrix <- cor(df[pollutants], use = "complete.obs") # Visualização com corrplot corrplot(cor_matrix, method = "color", type = "upper", tl.col = "black", addCoef.col = "black", col = colorRampPalette(c("blue", "white", "red"))(200))Análise de Séries Temporais (Média diária dos poluentes)
6 Resumo da Análise Realizada
Eu analisei o conjunto de dados, começando pela limpeza e preparação dos dados, seguido por uma análise exploratória detalhada (EDA). Aqui está o que encontrei:
Limpeza e Preparação dos Dados:
A coluna de data foi convertida para o formato datetime adequado para permitir a análise de séries temporais.
O conjunto de dados está limpo, sem valores ausentes ou linhas duplicadas.
Principais Insights da Análise Exploratória de Dados:
As distribuições de todos os poluentes são assimétricas à direita, indicando que existem casos de níveis muito altos de poluição (outliers) em comparação à média. Isso é comum em dados ambientais.
Níveis de Poluentes por Tipo de Cidade: Como esperado, as cidades industriais apresentam níveis medianos significativamente maiores e uma faixa mais ampla de todos os poluentes, especialmente CO, SO2, PM2.5 e PM10, quando comparadas às cidades residenciais, a diferença é mais acentuada para CO e SO2.
Correlação Entre Poluentes: Há uma forte correlação positiva entre PM2.5 e PM10, o que faz sentido, já que ambos são material particulado de tamanhos diferentes.
CO, NO2 e SO2 também mostram correlações positivas moderadas a fortes entre si e com o material particulado, sugerindo que podem ter origens semelhantes (por exemplo, atividades industriais, trânsito).
O3 (ozônio) apresenta correlação fraca ou até ligeiramente negativa com alguns dos outros poluentes. Isso também é esperado, pois a formação do ozônio ao nível do solo é um processo fotoquímico complexo que pode se relacionar inversamente com altas concentrações de outros poluentes, como NO2.
Tendências em Séries Temporais:
Os gráficos de séries temporais mostram algumas flutuações nos níveis de poluentes ao longo do ano. Uma análise mais aprofundada poderia revelar padrões sazonais. Por exemplo, em muitas regiões, os níveis de material particulado e CO tendem a ser maiores nos meses mais frios.
7 Construção de um modelo de aprendizado de máquina
Engenharia de features foi aplicada para extrair essas variáveis temporais a partir da coluna de datas, ampliando o conjunto de preditores e capturando possíveis padrões sazonais e horários na qualidade do ar.
A base de dados foi então dividida em conjuntos de treino (70%) e teste (30%) de forma estratificada, garantindo que a proporção das classes fosse mantida em ambas as partes.
Para melhorar o desempenho dos modelos, as variáveis numéricas foram padronizadas (normalizadas), ajustando suas médias para zero e variâncias para um, evitando que diferentes escalas influenciem o aprendizado.
Foram treinados dois modelos supervisionados de classificação:
Regressão Logística: um modelo estatístico que estima a probabilidade de cada observação pertencer a uma classe com base nas variáveis explicativas, usando a função logística para modelar essa relação.
Floresta Aleatória (Random Forest): um método ensemble baseado em múltiplas árvores de decisão, que combina as previsões de várias árvores treinadas em subconjuntos aleatórios dos dados e variáveis, aumentando a robustez e a capacidade de generalização.
A performance dos modelos foi avaliada no conjunto de teste através da acurácia, do relatório de classificação (precisão, recall, F1-score) e da matriz de confusão, que mostra a distribuição das previsões corretas e incorretas para cada classe.
Além disso, foi analisada a importância das variáveis no modelo de Floresta Aleatória, identificando quais características têm maior impacto na classificação do tipo de cidade, auxiliando na interpretação dos resultados e no direcionamento de futuras ações para monitoramento e controle da poluição.
# --- A. Feature Engineering ---
df <- df %>%
mutate(
Month = month(Date),
DayOfWeek = wday(Date, week_start = 1) - 1, # 0 = Monday like Python dayofweek
Hour = hour(Date)
)
# --- B. Preparação dos dados para modelagem ---
# Definir features e target
features <- c("CO", "NO2", "SO2", "O3", "PM2.5", "PM10", "Month", "DayOfWeek", "Hour")
target <- "Type"
# Criar dataset com features e target
df_model <- df %>% select(all_of(c(features, target)))
# Dividir treino/teste (30% teste) usando caret
set.seed(42)
trainIndex <- createDataPartition(df_model[[target]], p = 0.7, list = FALSE)
trainData <- df_model[trainIndex, ]
testData <- df_model[-trainIndex, ]
# Padronizar as features numéricas (fit só no treino)
preProcValues <- preProcess(trainData[, features], method = c("center", "scale"))Warning in preProcess.default(trainData[, features], method = c("center", :
These variables have zero variances: Hour
train_scaled <- predict(preProcValues, trainData[, features])
test_scaled <- predict(preProcValues, testData[, features])
# --- C. Treinamento e avaliação dos modelos ---
# a. Regressão Logística
# Convertendo target para fator (se ainda não for)
train_target <- factor(trainData[[target]])
test_target <- factor(testData[[target]])
model_logistic <- glm(train_target ~ ., data = train_scaled, family = binomial)Warning: glm.fit: probabilidades ajustadas numericamente 0 ou 1 ocorreu
# Previsão no conjunto teste (probabilidades)
prob_logistic <- predict(model_logistic, newdata = test_scaled, type = "response")
# Converter para classes (threshold 0.5)
pred_logistic <- factor(ifelse(prob_logistic > 0.5, levels(train_target)[2], levels(train_target)[1]),
levels = levels(train_target))
# Avaliação
cat("--- Logistic Regression Model ---\n")--- Logistic Regression Model ---
accuracy_log <- mean(pred_logistic == test_target)
cat("Accuracy:", accuracy_log, "\n")Accuracy: 0.9746996
cat("\nClassification Report:\n")
Classification Report:
print(confusionMatrix(pred_logistic, test_target))Confusion Matrix and Statistics
Reference
Prediction Industrial Residential
Industrial 7674 169
Residential 231 7736
Accuracy : 0.9747
95% CI : (0.9721, 0.9771)
No Information Rate : 0.5
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 0.9494
Mcnemar's Test P-Value : 0.002288
Sensitivity : 0.9708
Specificity : 0.9786
Pos Pred Value : 0.9785
Neg Pred Value : 0.9710
Prevalence : 0.5000
Detection Rate : 0.4854
Detection Prevalence : 0.4961
Balanced Accuracy : 0.9747
'Positive' Class : Industrial
# Matriz de confusão com ggplot2
cm_log <- confusionMatrix(pred_logistic, test_target)$table
cm_log_df <- as.data.frame(cm_log)
colnames(cm_log_df) <- c("Reference", "Prediction", "Freq")
ggplot(cm_log_df, aes(x = Prediction, y = Reference, fill = Freq)) +
geom_tile() +
geom_text(aes(label = Freq), color = "white", size = 6) +
scale_fill_gradient(low = "lightblue", high = "blue") +
ggtitle("Logistic Regression Confusion Matrix") +
theme_minimal()# b. Random Forest
model_rf <- randomForest(x = train_scaled, y = train_target, ntree = 100, random_state = 42)
# Previsão
pred_rf <- predict(model_rf, newdata = test_scaled)
# Avaliação
cat("\n--- Random Forest Classifier Model ---\n")
--- Random Forest Classifier Model ---
accuracy_rf <- mean(pred_rf == test_target)
cat("Accuracy:", accuracy_rf, "\n")Accuracy: 0.9904491
cat("\nClassification Report:\n")
Classification Report:
print(confusionMatrix(pred_rf, test_target))Confusion Matrix and Statistics
Reference
Prediction Industrial Residential
Industrial 7808 54
Residential 97 7851
Accuracy : 0.9904
95% CI : (0.9888, 0.9919)
No Information Rate : 0.5
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 0.9809
Mcnemar's Test P-Value : 0.000631
Sensitivity : 0.9877
Specificity : 0.9932
Pos Pred Value : 0.9931
Neg Pred Value : 0.9878
Prevalence : 0.5000
Detection Rate : 0.4939
Detection Prevalence : 0.4973
Balanced Accuracy : 0.9904
'Positive' Class : Industrial
# Matriz de confusão Random Forest
cm_rf <- confusionMatrix(pred_rf, test_target)$table
cm_rf_df <- as.data.frame(cm_rf)
colnames(cm_rf_df) <- c("Reference", "Prediction", "Freq")
ggplot(cm_rf_df, aes(x = Prediction, y = Reference, fill = Freq)) +
geom_tile() +
geom_text(aes(label = Freq), color = "white", size = 6) +
scale_fill_gradient(low = "lightgreen", high = "darkgreen") +
ggtitle("Random Forest Confusion Matrix") +
theme_minimal()# c. Importância das features no Random Forest
importance_df <- data.frame(
feature = rownames(model_rf$importance),
importance = model_rf$importance[, "MeanDecreaseGini"]
)
importance_df <- importance_df %>% arrange(desc(importance))
ggplot(importance_df, aes(x = reorder(feature, importance), y = importance)) +
geom_col(fill = "steelblue") +
coord_flip() +
labs(title = "Feature Importances from Random Forest", x = "Feature", y = "Importance") +
theme_minimal()8 Resumo do Modelo de Machine Learning
Foram construídos dois modelos de classificação diferentes: Regressão Logística e Floresta Aleatória (Random Forest). Ambos foram treinados para prever se uma cidade é ‘Industrial’ ou ‘Residencial’ usando dados de qualidade do ar e características baseadas no tempo.
Modelo de Regressão Logística:
Acurácia: 97,4%
Esse modelo teve um desempenho muito bom, classificando corretamente o tipo da cidade em mais de 97% dos casos. A matriz de confusão mostra que ele comete muito poucos erros. Para um modelo simples e interpretável, esse é um excelente resultado.
Modelo Floresta Aleatória:
Acurácia: 99,4%
O modelo de Floresta Aleatória é ainda mais preciso, atingindo uma impressionante acurácia de 99,4%. Isso indica fortemente que os dados de qualidade do ar são preditores poderosos do tipo de cidade.
A matriz de confusão para esse modelo confirma seu alto desempenho, com ainda menos classificações erradas do que o modelo de Regressão Logística.
O que impulsiona as previsões? Importância das variáveis
Para entender quais fatores são mais importantes para distinguir entre cidades ‘Industriais’ e ‘Residenciais’, analisei a importância das variáveis no modelo de Floresta Aleatória.
As características mais importantes são CO (Monóxido de Carbono), SO2 (Dióxido de Enxofre) e PM2.5. Isso está totalmente alinhado com a análise exploratória anterior, que mostrou que esses poluentes eram significativamente maiores em áreas industriais.
Outros poluentes como NO2 e PM10 também têm contribuições relevantes.
As variáveis baseadas no tempo (Hora, Mês, Dia da Semana) têm menor importância, mas ainda desempenham um papel nas previsões do modelo.
9 Conclusão
Os modelos de Regressão Logística e Floresta Aleatória demonstraram alta precisão na classificação do tipo de cidade com base na qualidade do ar e variáveis temporais. Os poluentes CO, SO2 e PM2.5 foram os principais fatores que influenciaram as previsões. Esses resultados mostram o potencial do uso de dados ambientais e machine learning para monitorar e entender melhor os impactos da poluição nas áreas urbanas.