library(tidyverse)
library(janitor)
library(skimr)
library(naniar)
library(visdat)
library(DataExplorer)
library(patchwork)
library(scales)
library(knitr)
library(kableExtra)
library(gtsummary)
library(corrplot)
library(ggmosaic)Aula 04 — Análise Bivariada
MBA Data Science · Análise Exploratória de Dados com R
26/03/2026
1 Objetivo da aula
- Finalizar exercícios da aula anterior
- Fazer Análise Bivariada
2 Pacotes
3 Dados Originais
ames_raw <- read_csv("dados/AmesHousing.csv") |>
clean_names()4 Dados Limpos
# .RDS — recomendado para um objeto
# saveRDS(ames_clean, "dados_limpos.rds")
# Para carregar depois:
ames_clean <- readRDS("dados_limpos.rds")5 Exercícios Práticos
Os bairros de GrnHill e Landmrk não tem essa variável, ou seja, tem todos os valores NA.
Dos bairros que tem esse valor preenchido, temos que ClearCr, NWAmes, Sawyer, Veenker, Gilbert são os que mais tem NA, com proporção de dados faltantes acima dos 30%. No total, 28 bairros apresentam alguma quantidade de dados faltantes para a variável lot_frontage.
Podemos ainda ampliar essa resposta, mostrando as estatísticas descritivas dessa variável para os Top 10 bairros com maior valor de NA.
ames_raw |>
group_by(neighborhood) |>
summarise(
media = mean(lot_frontage, na.rm = TRUE),
mediana = median(lot_frontage, na.rm = TRUE)
) |>
ungroup() |>
arrange(desc(mediana)) |>
kbl(col.names = c("Bairro", "Média", "Mediana"),
align = "lrr",
caption = "Resumo sobre Lot Frontage por bairro") |>
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Bairro | Média | Mediana |
|---|---|---|
| NridgHt | 84.18405 | 92.0 |
| NoRidge | 91.62963 | 89.0 |
| Timber | 81.15789 | 82.0 |
| ClearCr | 88.15000 | 80.5 |
| NWAmes | 81.51765 | 80.0 |
| Veenker | 72.00000 | 80.0 |
| Mitchel | 75.14444 | 74.0 |
| NAmes | 75.21067 | 73.0 |
| Somerst | 64.54938 | 72.5 |
| Sawyer | 74.55102 | 72.0 |
| CollgCr | 71.33636 | 70.0 |
| Crawfor | 69.95181 | 70.0 |
| SawyerW | 70.66981 | 67.0 |
| Edwards | 66.91011 | 65.0 |
| Gilbert | 74.20721 | 64.0 |
| IDOTRR | 62.24138 | 60.0 |
| OldTown | 61.77729 | 60.0 |
| SWISU | 59.06818 | 60.0 |
| StoneBr | 62.17391 | 60.0 |
| BrkSide | 55.78947 | 51.0 |
| Blmngtn | 46.90000 | 43.0 |
| Greens | 41.00000 | 40.0 |
| Blueste | 27.30000 | 24.0 |
| NPkVill | 28.14286 | 24.0 |
| BrDale | 21.50000 | 21.0 |
| MeadowV | 25.60606 | 21.0 |
| GrnHill | NaN | NA |
| Landmrk | NaN | NA |
Como a média é menor que a mediana, podemos observar que a distribuição é assimétrica à esquerda, ou seja, tem casas que foram construídas há muito tempo, antes de 1900, e isso puxa a média para a esquerda.
Vamos ver as casas mais antigas:
ames_clean |>
filter(year_built < 1900) |>
View()6 Análise bivariada
8 Numérica vs. numérica
8.1 Intuição: o que é correlação?
Correlação mede a força e direção da relação linear entre duas variáveis numéricas. Mas antes de qualquer fórmula, olhe o scatter plot.
set.seed(42)
n <- 300
d_forte_pos <- tibble(x = rnorm(n), y = x * 0.9 + rnorm(n, sd = 0.4),
tipo = "Correlação forte positiva\nr ≈ +0.90")
d_fraca_pos <- tibble(x = rnorm(n), y = x * 0.4 + rnorm(n, sd = 0.9),
tipo = "Correlação fraca positiva\nr ≈ +0.40")
d_nula <- tibble(x = rnorm(n), y = rnorm(n),
tipo = "Sem correlação linear\nr ≈ 0.00")
d_nao_linear <- tibble(x = seq(-3, 3, length.out = n),
y = x^2 + rnorm(n, sd = 0.5),
tipo = "Relação não-linear\nr ≈ 0.00 (mas há padrão!)")
bind_rows(d_forte_pos, d_fraca_pos, d_nula, d_nao_linear) |>
ggplot(aes(x = x, y = y)) +
geom_point(alpha = 0.35, color = "#185FA5", size = 1.2) +
geom_smooth(method = "lm", se = FALSE, color = "#C04828",
linewidth = 0.9, linetype = "dashed") +
facet_wrap(~ tipo, scales = "free", ncol = 2) +
labs(title = "Padrões de correlação",
subtitle = "A linha vermelha é sempre a regressão linear",
x = NULL, y = NULL) +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"),
strip.text = element_text(face = "bold", size = 10),
panel.spacing = unit(1.2, "lines"))O 4° painel acima tem correlação de Pearson próxima de zero — mas claramente há uma relação quadrática. Sempre visualize antes de calcular. A correlação de Pearson só captura relações lineares.
8.2 Correlação de Pearson: teoria
\[r = \frac{\sum_{i=1}^{n}(x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum(x_i-\bar{x})^2 \cdot \sum(y_i-\bar{y})^2}}\]
Interpretação prática (regra geral — não absoluta):
| |r| | Força |
|---|---|
| 0,00 – 0,19 | Desprezível |
| 0,20 – 0,39 | Fraca |
| 0,40 – 0,59 | Moderada |
| 0,60 – 0,79 | Forte |
| 0,80 – 1,00 | Muito forte |
Pressupostos de Pearson: - Ambas as variáveis aproximadamente normais - Relação linear - Sem outliers extremos influentes
Quando esses pressupostos não valem → use Spearman (baseado em ranks).
8.2.1 Área habitável vs. preço de venda
ames_clean |>
ggplot(aes(x = gr_liv_area, y = sale_price,
color = as.integer(overall_qual))) +
geom_point(alpha = 0.4, size = 1.5) +
geom_smooth(method = "lm", se = TRUE,
color = "#C04828", fill = "#C04828",
alpha = 0.15, linewidth = 1) +
scale_color_gradient(low = "#B5D4F4", high = "#0C447C",
name = "Qualidade\ngeral") +
scale_x_continuous(labels = comma) +
scale_y_continuous(labels = dollar_format(prefix = "US$", scale = 1e-3,
suffix = "k")) +
labs(title = "Área habitável vs. Preço de venda",
subtitle = "Cor mais escura = qualidade geral mais alta",
x = "Área habitável (sqft)", y = "Preço de venda") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"))Vamos calular a correlação:
# Correlação pontual
r_pearson <- cor(ames_clean$gr_liv_area, ames_clean$sale_price,
method = "pearson")
r_spearman <- cor(ames_clean$gr_liv_area, ames_clean$sale_price,
method = "spearman")
r_pearson[1] 0.7194627
r_spearman[1] 0.7233735
Temos uma correlação positiva e forte. Então, podemos pensar que quanto maior a área habitável, maior o preço de venda da casa.
8.2.2 Heatmap de correlação geral
vars_num <- ames_clean |>
select(sale_price, gr_liv_area, total_bsmt_sf, x1st_flr_sf,
garage_area, lot_area, year_built, year_remod_add,
overall_qual_int = overall_qual) |>
mutate(overall_qual_int = as.integer(overall_qual_int))
vars_num <- ames_clean |>
select(sale_price, gr_liv_area, total_bsmt_sf, x1st_flr_sf,
garage_area, lot_area, year_built, year_remod_add,
overall_qual_int = overall_qual) |>
mutate(overall_qual_int = as.integer(overall_qual_int))
M <- cor(vars_num, use = "complete.obs")
corrplot(M,
method = "color",
type = "upper",
order = "hclust",
tl.cex = 0.85,
tl.col = "#1A1A18",
addCoef.col = "#1A1A18",
number.cex = 0.72,
col = colorRampPalette(c("#C04828", "white", "#185FA5"))(200),
diag = FALSE,
mar = c(0, 0, 1, 0),
title = "Correlações de Pearson — Ames Housing")As variáveis com maiores correlações com sales price são:
gr_liv_area
gr_liv_area
overall_qual_int
total_bsmt_sf
garage_area
year_built
x1st_flr_sf
9 Numérica × Categórica
9.1 Intuição: comparar distribuições entre grupos
Quando uma variável é numérica e a outra é categórica, a pergunta é: “A distribuição da variável numérica muda entre os grupos?”
As ferramentas principais são boxplot, violin plot e stat_summary().
9.2 Preço por tipo de construção (bldg_type)
ames_clean |>
mutate(bldg_type = fct_reorder(bldg_type,
sale_price, .fun = median)) |>
ggplot(aes(x = bldg_type, y = sale_price, fill = bldg_type)) +
geom_boxplot(alpha = 0.75, outlier.alpha = 0.4,
outlier.size = 1.2, width = 0.55,
show.legend = FALSE) +
stat_summary(fun = mean, geom = "point",
shape = 18, size = 3, color = "#C04828",
show.legend = FALSE) +
scale_fill_manual(values = c("#B5D4F4","#85B7EB","#378ADD",
"#185FA5","#0C447C")) +
scale_y_continuous(labels = dollar_format(prefix = "US$",
scale = 1e-3, suffix = "k")) +
labs(title = "Preço de venda por tipo de imóvel",
subtitle = "Losango vermelho = média · Linha horizontal = mediana · Caixas ordenadas por mediana",
x = "Tipo de imóvel", y = "Preço de venda") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"))9.3 Violin plot: ver a forma completa
ames_clean |>
ggplot(aes(x = overall_qual,
y = sale_price,
fill = overall_qual)) +
geom_violin(alpha = 0.55, color = NA, show.legend = FALSE) +
geom_boxplot(width = 0.12, outlier.shape = NA,
fill = "white", alpha = 0.7,
show.legend = FALSE) +
scale_fill_manual(values = colorRampPalette(
c("#B5D4F4", "#185FA5", "#042C53"))(10)) +
scale_y_continuous(labels = dollar_format(prefix = "US$",
scale = 1e-3, suffix = "k")) +
labs(title = "Preço de venda por qualidade geral",
subtitle = "Violin mostra a forma completa da distribuição · Boxplot interno mostra os quartis",
x = "Qualidade geral (1 = muito ruim → 10 = excelente)",
y = "Preço de venda") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"))9.4 Grouped summaries: tabela de estatísticas por grupo
ames_clean |>
group_by(bldg_type) |>
summarise(
n = n(),
mediana = median(sale_price),
media = mean(sale_price),
dp = sd(sale_price),
cv = as.character(round(dp / media, 3)),
q1 = quantile(sale_price, 0.25),
q3 = quantile(sale_price, 0.75)
) |>
arrange(desc(mediana)) |>
mutate(across(c(mediana, media, dp, q1, q3),
~ dollar(., prefix = "US$", big.mark = ","))) |>
kbl(col.names = c("Tipo", "n", "Mediana", "Média",
"DP", "CV", "Q1", "Q3"),
align = "lrrrrrrrr") |>
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = TRUE)| Tipo | n | Mediana | Média | DP | CV | Q1 | Q3 |
|---|---|---|---|---|---|---|---|
| TwnhsE | 233 | US$180,000 | US$192,312 | US$66,191.74 | 0.344 | US$145,000 | US$222,000 |
| 1Fam | 2420 | US$165,000 | US$184,356 | US$81,295.99 | 0.441 | US$130,000 | US$220,000 |
| Duplex | 109 | US$136,905 | US$139,809 | US$39,498.97 | 0.283 | US$118,858 | US$153,337 |
| Twnhs | 101 | US$130,000 | US$135,934 | US$41,938.93 | 0.309 | US$100,500 | US$170,000 |
| 2fmCon | 62 | US$122,250 | US$125,582 | US$31,089.24 | 0.248 | US$106,562 | US$140,000 |
- Outra maneira de fazer (mais resumida):
ames_clean |>
select(sale_price, bldg_type) |>
tbl_summary(by = bldg_type)| Characteristic | 1Fam N = 2,4201 |
2fmCon N = 621 |
Duplex N = 1091 |
Twnhs N = 1011 |
TwnhsE N = 2331 |
|---|---|---|---|---|---|
| sale_price | 165,000 (130,000, 220,000) | 122,250 (106,250, 140,000) | 136,905 (118,858, 153,337) | 130,000 (100,500, 170,000) | 180,000 (145,000, 222,000) |
| 1 Median (Q1, Q3) | |||||
9.5 Comparação de médias por bairro
ames_clean |>
group_by(neighborhood) |>
summarise(
mediana = median(sale_price),
n = n()
) |>
slice_max(mediana, n = 15) |>
mutate(neighborhood = fct_reorder(neighborhood, mediana)) |>
ggplot(aes(x = neighborhood, y = mediana)) +
geom_col(aes(fill = mediana), width = 0.7,
show.legend = FALSE) +
geom_text(aes(label = paste0(dollar(mediana / 1000,
prefix = "US$"), "k\nn=", n)),
hjust = -0.1, size = 3, color = "#5C5B57") +
coord_flip() +
scale_fill_gradient(low = "#85B7EB", high = "#0C447C") +
scale_y_continuous(labels = dollar_format(prefix = "US$",
scale = 1e-3, suffix = "k"),
limits = c(0, 360000),
expand = c(0, 0)) +
labs(title = "Mediana do preço por bairro — top 15",
subtitle = "Bairros ordenados por mediana de preço de venda",
x = NULL, y = "Mediana do preço de venda") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"),
panel.grid.major.y = element_blank())10 Categórica × Categórica
10.1 Intuição: há associação entre dois grupos?
Quando ambas as variáveis são categóricas, queremos saber se a distribuição de uma muda em função da outra — ou seja, se as categorias são independentes ou associadas.
As ferramentas são: tabela de contingência, gráfico de barras empilhadas e gráfico de mosaico.
10.2 Tabela de contingência
ames_clean |>
count(sale_condition) |>
arrange(desc(n))# A tibble: 6 × 2
sale_condition n
<chr> <int>
1 Normal 2412
2 Partial 242
3 Abnorml 189
4 Family 46
5 Alloca 24
6 AdjLand 12
Por simplicidade, vamos usar as 3 categorias mais frequentes:
# sale_condition × bldg_type
tab <- ames_clean |>
filter(sale_condition %in% c("Normal", "Abnorml", "Partial")) |>
count(bldg_type, sale_condition) |>
pivot_wider(names_from = sale_condition, values_from = n,
values_fill = 0) |>
column_to_rownames("bldg_type")
tab |>
as.data.frame() |>
rownames_to_column("Tipo de imóvel") |>
kbl(caption = "Contagem: tipo de imóvel × condição de venda",
align = "lrrr") |>
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE) |>
add_header_above(c(" " = 1, "Condição de venda" = 3))| Tipo de imóvel | Abnorml | Normal | Partial |
|---|---|---|---|
| 1Fam | 157 | 2001 | 204 |
| 2fmCon | 8 | 52 | 1 |
| Duplex | 10 | 78 | 0 |
| Twnhs | 6 | 93 | 1 |
| TwnhsE | 8 | 188 | 36 |
# Proporções por linha (dentro de cada tipo de imóvel)
prop.table(as.matrix(tab), margin = 2) |>
round(3) |>
as.data.frame() |>
rownames_to_column("Tipo de imóvel") |>
kbl(caption = "Proporção por linha: condição de venda dentro de cada tipo",
align = "lrrr",
digits = 3) |>
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE) |>
add_header_above(c(" " = 1, "Condição de venda" = 3))| Tipo de imóvel | Abnorml | Normal | Partial |
|---|---|---|---|
| 1Fam | 0.831 | 0.830 | 0.843 |
| 2fmCon | 0.042 | 0.022 | 0.004 |
| Duplex | 0.053 | 0.032 | 0.000 |
| Twnhs | 0.032 | 0.039 | 0.004 |
| TwnhsE | 0.042 | 0.078 | 0.149 |
- Proporção por linha (
margin = 1): “dentro de cada tipo, qual a distribuição das condições de venda?” — útil quando X é a variável explicativa - Proporção por coluna (
margin = 2): “dentro de cada condição, qual a distribuição dos tipos?” — útil quando Y é a variável explicativa - Proporção geral (
margin = NULL): cada célula como % do total — útil para comparar com o esperado sob independência
10.3 Gráfico de barras empilhadas (proporcional)
10.3.1 Condição de venda vs. tipo de imóvel
ames_clean |>
filter(sale_condition %in% c("Normal", "Abnorml", "Partial")) |>
count(bldg_type, sale_condition) |>
group_by(bldg_type) |>
mutate(pct = n / sum(n)) |>
ungroup() |>
# Reordena bldg_type pela proporção de "Normal"
mutate(bldg_type = fct_reorder(
bldg_type,
pct * (sale_condition == "Normal"), # só conta o valor de Normal
.fun = sum
)) |>
ggplot(aes(x = bldg_type, y = pct, fill = sale_condition)) +
geom_col(position = "fill", width = 0.65, alpha = 0.85) +
geom_text(aes(label = percent(pct, accuracy = 1)),
position = position_fill(vjust = 0.5),
size = 3.2, color = "white", fontface = "bold") +
scale_fill_manual(values = c("#185FA5", "#C04828", "#3B6D11"),
name = "Condição de venda") +
scale_y_continuous(labels = percent_format()) +
labs(title = "Condição de venda por tipo de imóvel",
subtitle = "Barras proporcional a 100% — permite comparar a composição entre grupos",
x = "Tipo de imóvel", y = "Proporção") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"),
legend.position = "top")10.3.2 qualidade exterior × qualidade do porão
ames_clean |>
filter(bsmt_qual != "None") |>
mutate(
exter_qual = factor(exter_qual,
levels = c("Po","Fa","TA","Gd","Ex"),
labels = c("Ruim","Regular","Médio","Bom","Excelente")),
bsmt_qual = factor(bsmt_qual,
levels = c("Po","Fa","TA","Gd","Ex"),
labels = c("Ruim","Regular","Médio","Bom","Excelente"))
) |>
count(bsmt_qual, exter_qual) |>
group_by(bsmt_qual) |>
mutate(pct = n / sum(n)) |>
ungroup() |>
ggplot(aes(x = bsmt_qual, y = pct, fill = exter_qual)) +
geom_col(position = "fill", width = 0.65, alpha = 0.85) +
# geom_text(aes(label = percent(pct, accuracy = 1)),
# position = position_fill(vjust = 0.5),
# size = 3.2, color = "white", fontface = "bold") +
scale_fill_manual(values = c("#8f8d88", "#C04828", "#3B6D11", "#facf41", "#185FA5"),
name = "Condição Externa") +
scale_y_continuous(labels = percent_format()) +
labs(title = "Condição do porão por condição externa",
x = "Qualidade do porão", y = "Proporção") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"),
legend.position = "top")# Tabela de contingencia
ames_clean |>
filter(bsmt_qual != "None") |>
mutate(
exter_qual = factor(exter_qual,
levels = c("Po","Fa","TA","Gd","Ex"),
labels = c("Ruim","Regular","Médio","Bom","Excelente")),
bsmt_qual = factor(bsmt_qual,
levels = c("Po","Fa","TA","Gd","Ex"),
labels = c("Ruim","Regular","Médio","Bom","Excelente"))
) |>
count(bsmt_qual, exter_qual) |>
group_by(bsmt_qual) |>
mutate(pct = round(100 * n / sum(n), 2)) |>
ungroup() |>
select(-n) |>
pivot_wider(names_from = bsmt_qual,
values_from = pct, values_fill = 0) |>
rename(`Qualidade externa` = exter_qual) |>
kbl(caption = "Qual. do porão vs. qual. externa",
align = "lrrrrr") |>
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE) |>
add_header_above(c(" " = 1, "Qualidade do porão" = 5))| Qualidade externa | Ruim | Regular | Médio | Bom | Excelente |
|---|---|---|---|---|---|
| Médio | 100 | 81.82 | 91.97 | 37.41 | 6.72 |
| Regular | 0 | 7.95 | 1.64 | 0.16 | 0.00 |
| Bom | 0 | 10.23 | 6.08 | 61.53 | 58.50 |
| Excelente | 0 | 0.00 | 0.31 | 0.90 | 34.78 |
Imóveis com qualidade exterior “Excelente” tendem a ter porão de qualidade “Bom” ou “Excelente” também — mas isso não quer dizer que reformar a fachada causa melhora no porão. Ambos refletem o mesmo fator latente: nível geral do imóvel (e do bairro onde está inserido).
11 Exercícios Práticos
11.1 📝 Exercício 1 — Correlação entre ano de construção e preço
Tarefa: Calcule a correlação de Pearson e Spearman entre year_built e sale_price. Em seguida, crie um scatter plot com linha de tendência. A relação é linear? Que tipo de correlação é mais adequado neste caso?
11.2 📝 Exercício 2 — Distribuição do preço por condição de venda
Tarefa: Crie um boxplot comparando a distribuição de sale_price entre as categorias de sale_condition. Ordene as categorias pela mediana. Qual condição apresenta maior variabilidade (maior IQR)?
11.3 📝 Exercício 3 — Tabela de contingência e visualização
Tarefa: Construa uma tabela de contingência entre house_style (estilo da casa) e exter_qual (qualidade exterior). Em seguida, crie um gráfico de barras empilhadas proporcional mostrando a distribuição de qualidade exterior dentro de cada estilo. Qual estilo tem maior proporção de imóveis com qualidade “Excelente”?