Código
install.packages(c(
"tidyverse",
"lubridate",
"gtsummary",
"ggcorrplot",
"mice",
"patchwork",
"kableExtra",
"scales"
))Projeto Final — Ciência de Dados com R
O código completo deste projeto está disponível no RPubs: Clique aqui para acessar
A base de dados escolhida foi a “515k Hotel Reviews Data in Europe”, disponível gratuitamente no Kaggle.
Ela contém mais de 515.000 avaliações de hotéis em 6 países europeus (Reino Unido, França, Países Baixos, Áustria, Espanha e Itália), coletadas da plataforma Booking.com.
Motivos da escolha:
Variável de interesse (numérica): Reviewer_Score — nota dada pelo avaliador (de 2,5 a 10)
Outras variáveis numéricas:
| Variável | Descrição |
|---|---|
Average_Score |
Nota média do hotel no período |
Review_Total_Positive_Word_Counts |
Nº de palavras na parte positiva da avaliação |
Review_Total_Negative_Word_Counts |
Nº de palavras na parte negativa da avaliação |
Total_Number_of_Reviews_Reviewer_Has_Given |
Quantas avaliações o usuário já fez |
days_since_review |
Dias passados desde a avaliação |
Variáveis categóricas (criadas a partir dos dados):
| Variável | Descrição |
|---|---|
Trip_Type |
Tipo de viagem: Lazer ou Negócios (extraída de Tags) |
Traveler_Type |
Tipo de viajante: Casal, Solo, Família, etc. (extraída de Tags) |
Hotel_Country |
País do hotel (extraído de Hotel_Address) |
Espero encontrar que:
install.packages(c(
"tidyverse",
"lubridate",
"gtsummary",
"ggcorrplot",
"mice",
"patchwork",
"kableExtra",
"scales"
))# Carregando os pacotes
library(tidyverse)
library(lubridate)
library(gtsummary)
library(ggcorrplot)
library(mice)
library(patchwork)
library(kableExtra)
library(scales)
# Tema visual padrão para todos os gráficos
theme_set(theme_minimal(base_size = 12))
# Semente aleatória para reprodutibilidade
set.seed(42)hotel <- read_csv("Hotel_Reviews.csv")
cat("Dimensões da base:\n")Dimensões da base:
cat(" Linhas :", nrow(hotel), "\n") Linhas : 515738
cat(" Colunas :", ncol(hotel), "\n\n") Colunas : 17
glimpse(hotel)Rows: 515,738
Columns: 17
$ Hotel_Address <chr> "s Gravesandestraat 55 Oost…
$ Additional_Number_of_Scoring <dbl> 194, 194, 194, 194, 194, 19…
$ Review_Date <chr> "8/3/2017", "8/3/2017", "7/…
$ Average_Score <dbl> 7.7, 7.7, 7.7, 7.7, 7.7, 7.…
$ Hotel_Name <chr> "Hotel Arena", "Hotel Arena…
$ Reviewer_Nationality <chr> "Russia", "Ireland", "Austr…
$ Negative_Review <chr> "I am so angry that i made …
$ Review_Total_Negative_Word_Counts <dbl> 397, 0, 42, 210, 140, 17, 3…
$ Total_Number_of_Reviews <dbl> 1403, 1403, 1403, 1403, 140…
$ Positive_Review <chr> "Only the park outside of t…
$ Review_Total_Positive_Word_Counts <dbl> 11, 105, 21, 26, 8, 20, 18,…
$ Total_Number_of_Reviews_Reviewer_Has_Given <dbl> 7, 7, 9, 1, 3, 1, 6, 1, 3, …
$ Reviewer_Score <dbl> 2.9, 7.5, 7.1, 3.8, 6.7, 6.…
$ Tags <chr> "[' Leisure trip ', ' Coupl…
$ days_since_review <chr> "0 days", "0 days", "3 days…
$ lat <dbl> 52.36058, 52.36058, 52.3605…
$ lng <dbl> 4.915968, 4.915968, 4.91596…
hotel <- hotel |>
mutate(
# País do hotel
Hotel_Country = word(Hotel_Address, -1),
# Tipo de viagem
Trip_Type = case_when(
str_detect(Tags, "Leisure trip") ~ "Lazer",
str_detect(Tags, "Business trip") ~ "Negócios",
TRUE ~ NA_character_
),
# Tipo de viajante
Traveler_Type = case_when(
str_detect(Tags, "Solo traveller") ~ "Solo",
str_detect(Tags, "Couple") ~ "Casal",
str_detect(Tags, "Family with young children") ~ "Família",
str_detect(Tags, "Group") ~ "Grupo",
str_detect(Tags, "People with friends") ~ "Amigos",
TRUE ~ NA_character_
),
# Convertendo os "days" para número
days_since_review = suppressWarnings(
as.numeric(str_extract(as.character(days_since_review), "\\d+"))
),
# Data da avaliação
Review_Date = mdy(Review_Date)
)
# Variaveis selecionadas
hotel_clean <- hotel |>
select(
Reviewer_Score,
Average_Score,
Review_Total_Positive_Word_Counts,
Review_Total_Negative_Word_Counts,
Total_Number_of_Reviews_Reviewer_Has_Given,
days_since_review,
Hotel_Country,
Trip_Type,
Traveler_Type,
lat,
lng
)
glimpse(hotel_clean)Rows: 515,738
Columns: 11
$ Reviewer_Score <dbl> 2.9, 7.5, 7.1, 3.8, 6.7, 6.…
$ Average_Score <dbl> 7.7, 7.7, 7.7, 7.7, 7.7, 7.…
$ Review_Total_Positive_Word_Counts <dbl> 11, 105, 21, 26, 8, 20, 18,…
$ Review_Total_Negative_Word_Counts <dbl> 397, 0, 42, 210, 140, 17, 3…
$ Total_Number_of_Reviews_Reviewer_Has_Given <dbl> 7, 7, 9, 1, 3, 1, 6, 1, 3, …
$ days_since_review <dbl> 0, 0, 3, 3, 10, 10, 17, 17,…
$ Hotel_Country <chr> "Netherlands", "Netherlands…
$ Trip_Type <chr> "Lazer", "Lazer", "Lazer", …
$ Traveler_Type <chr> "Casal", "Casal", "Família"…
$ lat <dbl> 52.36058, 52.36058, 52.3605…
$ lng <dbl> 4.915968, 4.915968, 4.91596…
Abaixo, a tabela de estatísticas descritivas usando o pacote gtsummary:
hotel_clean |>
select(-lat, -lng) |> # removendo coordenadas (presença de muitos NAs)
tbl_summary(
label = list(
Reviewer_Score ~ "Nota do Avaliador",
Average_Score ~ "Nota Média do Hotel",
Review_Total_Positive_Word_Counts ~ "Palavras Positivas (nº)",
Review_Total_Negative_Word_Counts ~ "Palavras Negativas (nº)",
Total_Number_of_Reviews_Reviewer_Has_Given ~ "Avaliações Feitas pelo Usuário",
days_since_review ~ "Dias desde a Avaliação",
Hotel_Country ~ "País do Hotel",
Trip_Type ~ "Tipo de Viagem",
Traveler_Type ~ "Tipo de Viajante"
),
statistic = list(
all_continuous() ~ "{mean} ± {sd} [mín: {min} / máx: {max}]",
all_categorical() ~ "{n} ({p}%)"
),
digits = all_continuous() ~ 2,
missing = "ifany",
missing_text = "(Dados faltantes)"
) |>
modify_header(label = "**Variável**") |>
bold_labels() |>
italicize_levels()| Variável | N = 515,7381 |
|---|---|
| Nota do Avaliador | 8.40 ± 1.64 [mín: 2.50 / máx: 10.00] |
| Nota Média do Hotel | 8.40 ± 0.55 [mín: 5.20 / máx: 9.80] |
| Palavras Positivas (nº) | 17.78 ± 21.80 [mín: 0.00 / máx: 395.00] |
| Palavras Negativas (nº) | 18.54 ± 29.69 [mín: 0.00 / máx: 408.00] |
| Avaliações Feitas pelo Usuário | 7.17 ± 11.04 [mín: 1.00 / máx: 355.00] |
| Dias desde a Avaliação | 354.44 ± 208.93 [mín: 0.00 / máx: 730.00] |
| País do Hotel | |
| Austria | 38,939 (7.6%) |
| France | 59,928 (12%) |
| Italy | 37,207 (7.2%) |
| Kingdom | 262,301 (51%) |
| Netherlands | 57,214 (11%) |
| Spain | 60,149 (12%) |
| Tipo de Viagem | |
| Lazer | 417,778 (83%) |
| Negócios | 82,939 (17%) |
| (Dados faltantes) | 15,021 |
| Tipo de Viajante | |
| Casal | 252,294 (67%) |
| Família | 61,015 (16%) |
| Grupo | 65,392 (17%) |
| (Dados faltantes) | 137,037 |
| 1 Mean ± SD [mín: Min / máx: Max]; n (%) | |
Aqui construímos um mapa de calor para visualizar as correlações entre a variável de interesse e as demais variáveis numéricas.
# Selecionar somente variáveis numéricas principais
numericas <- hotel_clean |>
select(
Reviewer_Score,
Average_Score,
Review_Total_Positive_Word_Counts,
Review_Total_Negative_Word_Counts,
Total_Number_of_Reviews_Reviewer_Has_Given,
days_since_review
) |>
drop_na()
# Matriz de correlação
mat_cor <- cor(numericas)
# Rótulos mais legíveis
rownames(mat_cor) <- colnames(mat_cor) <- c(
"Nota\nAvaliador",
"Nota Média\nHotel",
"Palavras\nPositivas",
"Palavras\nNegativas",
"Avals.\ndo Usuário",
"Dias desde\nAvaliação"
)
# Gráfico
ggcorrplot(
mat_cor,
method = "square",
type = "lower",
lab = TRUE,
lab_size = 4,
colors = c("#c0392b", "#ffffff", "#27ae60"),
title = "Heatmap de Correlação entre Variáveis Numéricas",
ggtheme = theme_minimal(base_size = 11)
) +
theme(
plot.title = element_text(hjust = 0.5, face = "bold", size = 13)
)Analisando o heatmap visualmente:
days_since_review, Avals. do Usuário) têm correlação muito fraca com a nota, o que já era esperado.A distribuição normal (ou gaussiana) é uma das distribuições de probabilidade mais importantes em estatística. Ela tem formato de sino simétrico, definida por dois parâmetros: a média (μ) e o desvio padrão (σ).
Características principais:
As variáveis escolhidas foram:
Reviewer_Score — variável de interesse principalAverage_Score — nota média do hotelReview_Total_Positive_Word_Counts — quantidade de palavras positivasJustificativa do número de bins: Usei 30 bins, que é um valor intermediário razoável para amostras de milhares de observações. A regra de Sturges sugere k = 1 + log₂(n) ≈ 14 bins para n = 10.000, mas com 30 bins conseguimos enxergar melhor a forma da distribuição sem perder detalhes importantes.
# Amostra para visualização (10k pontos é suficiente e mais rápido)
amostra <- hotel_clean |> sample_n(10000)
# Função auxiliar para criar histograma + densidade
cria_hist <- function(dados, var, titulo, cor_barras, cor_linha) {
media_var <- mean(dados[[var]], na.rm = TRUE)
dp_var <- sd(dados[[var]], na.rm = TRUE)
ggplot(dados, aes(x = .data[[var]])) +
geom_histogram(
aes(y = after_stat(density)),
bins = 30,
fill = cor_barras,
color = "white",
alpha = 0.75
) +
geom_density(color = cor_linha, linewidth = 1.3) +
stat_function(
fun = dnorm,
args = list(mean = media_var, sd = dp_var),
color = "red",
linetype = "dashed",
linewidth = 1
) +
labs(
title = titulo,
x = var,
y = "Densidade",
caption = "Linha vermelha tracejada = distribuição normal teórica"
) +
theme(plot.caption = element_text(size = 8, color = "gray50"))
}
p1 <- cria_hist(amostra, "Reviewer_Score",
"① Nota do Avaliador", "#3498db", "#1a5fa3")
p2 <- cria_hist(amostra, "Average_Score",
"② Nota Média do Hotel", "#2ecc71", "#1a7d43")
p3 <- cria_hist(
amostra |> filter(Review_Total_Positive_Word_Counts <= 200),
"Review_Total_Positive_Word_Counts",
"③ Palavras Positivas (valores ≤ 200, sem outliers extremos)",
"#e67e22", "#a04000"
)
# Empilhando os gráficos
p1 / p2 / p3O gráfico Q-Q compara os quantis da variável observada com os quantis de uma distribuição normal teórica. Se os pontos seguirem a linha diagonal vermelha, a variável é aproximadamente normal. Desvios nas extremidades indicam caudas mais pesadas ou leves.
qq1 <- ggplot(amostra, aes(sample = Reviewer_Score)) +
stat_qq(color = "#3498db", alpha = 0.3, size = 0.8) +
stat_qq_line(color = "red", linewidth = 1) +
labs(title = "Q-Q: Nota do Avaliador",
x = "Quantis Teóricos (Normal)", y = "Quantis Observados")
qq2 <- ggplot(amostra, aes(sample = Average_Score)) +
stat_qq(color = "#2ecc71", alpha = 0.3, size = 0.8) +
stat_qq_line(color = "red", linewidth = 1) +
labs(title = "Q-Q: Nota Média do Hotel",
x = "Quantis Teóricos (Normal)", y = "Quantis Observados")
qq3 <- ggplot(amostra, aes(sample = Review_Total_Positive_Word_Counts)) +
stat_qq(color = "#e67e22", alpha = 0.3, size = 0.8) +
stat_qq_line(color = "red", linewidth = 1) +
labs(title = "Q-Q: Palavras Positivas",
x = "Quantis Teóricos (Normal)", y = "Quantis Observados")
qq1 / qq2 / qq3O teste de Shapiro-Wilk é o teste formal de normalidade mais usado para amostras pequenas e médias. Ele testa a hipótese nula H₀ de que os dados seguem distribuição normal. Se p < 0,05, rejeitamos H₀ e concluímos que os dados não são normais.
Limitação importante: o teste Shapiro-Wilk aceita no máximo 5.000 observações. Por isso, usamos uma amostra aleatória da base.
set.seed(42)
sw_amostra <- hotel_clean |>
drop_na(Reviewer_Score, Average_Score, Review_Total_Positive_Word_Counts) |>
sample_n(5000)
sw1 <- shapiro.test(sw_amostra$Reviewer_Score)
sw2 <- shapiro.test(sw_amostra$Average_Score)
sw3 <- shapiro.test(sw_amostra$Review_Total_Positive_Word_Counts)
tibble(
Variável = c("Nota do Avaliador", "Nota Média do Hotel", "Palavras Positivas"),
`Estatística W` = round(c(sw1$statistic, sw2$statistic, sw3$statistic), 5),
`p-valor` = formatC(c(sw1$p.value, sw2$p.value, sw3$p.value),
format = "e", digits = 3),
`Conclusão` = case_when(
c(sw1$p.value, sw2$p.value, sw3$p.value) < 0.05 ~
"❌ Rejeita normalidade (p < 0,05)",
TRUE ~ "✅ Não rejeita normalidade (p ≥ 0,05)"
)
) |>
kable(caption = "Resultados do Teste de Shapiro-Wilk (n = 5.000)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))| Variável | Estatística W | p-valor | Conclusão |
|---|---|---|---|
| Nota do Avaliador | 0.86558 | 2.561e-54 | ❌ Rejeita normalidade (p < 0,05) | |
| Nota Média do Hotel | 0.97329 | 1.377e-29 | ❌ Rejeita normalidade (p < 0,05) | |
| Palavras Positivas | 0.69666 | 1.025e-69 | ❌ Rejeita normalidade (p < 0,05) | |
Com base nos três instrumentos de análise (histograma, Q-Q e Shapiro-Wilk):
Nota do Avaliador: distribuição assimétrica negativa (a maioria das notas é alta, entre 7 e 10). O Q-Q mostra desvios claros nos extremos. Não é normal.
Nota Média do Hotel: visualmente é a mais parecida com uma distribuição normal, mas o teste formal rejeita. Com amostras grandes, até pequenos desvios são detectados pelo Shapiro-Wilk. Formalmente não é normal, mas é a mais próxima.
Palavras Positivas: fortemente assimétrica à direita. A maioria das avaliações tem poucas palavras. Claramente não é normal.
Observação: em bases muito grandes (como esta, com 515k linhas), o Shapiro-Wilk quase sempre rejeita a normalidade, mesmo para distribuições bem comportadas. Por isso, a análise visual dos gráficos é igualmente importante.
Levanto 5 hipóteses/perguntas sobre o Reviewer_Score e testo cada uma com um gráfico, tabela resumo e teste estatístico adequado.
hotel_clean |>
sample_n(8000) |>
ggplot(aes(x = Average_Score, y = Reviewer_Score)) +
geom_point(alpha = 0.2, color = "#3498db", size = 0.9) +
geom_smooth(method = "lm", color = "red", se = TRUE, linewidth = 1) +
labs(
title = "H1: Nota Média do Hotel vs. Nota do Avaliador",
subtitle = "Cada ponto = 1 avaliação | Linha = regressão linear",
x = "Nota Média do Hotel (Average Score)",
y = "Nota do Avaliador (Reviewer Score)"
)Tabela Resumo:
hotel_clean |>
mutate(
Cat_Media = cut(Average_Score,
breaks = c(0, 7, 8, 9, 10.1),
labels = c("Baixa (≤7)", "Média (7–8)", "Alta (8–9)", "Excelente (9+)"),
right = FALSE)
) |>
select(Cat_Media, Reviewer_Score) |>
drop_na() |>
tbl_summary(
by = Cat_Media,
statistic = list(all_continuous() ~ "{mean} ± {sd}")
) |>
add_p() |>
bold_p()| Characteristic | Baixa (≤7) N = 5,6651 |
Média (7–8) N = 88,9821 |
Alta (8–9) N = 341,5781 |
Excelente (9+) N = 79,5131 |
p-value2 |
|---|---|---|---|---|---|
| Reviewer_Score | 6.54 ± 1.98 | 7.52 ± 1.86 | 8.46 ± 1.54 | 9.22 ± 1.09 | <0.001 |
| 1 Mean ± SD | |||||
| 2 Kruskal-Wallis rank sum test | |||||
Teste estatístico (correlação de Pearson):
cor.test(hotel_clean$Average_Score, hotel_clean$Reviewer_Score)
Pearson's product-moment correlation
data: hotel_clean$Average_Score and hotel_clean$Reviewer_Score
t = 280.97, df = 515736, p-value < 2.2e-16
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
0.3619816 0.3667154
sample estimates:
cor
0.3643508
Conclusão: Há correlação positiva significativa (p < 0,05). Hotéis com melhor reputação geral tendem a receber notas individuais mais altas. Isso confirma a hipótese.
hotel_h2 <- hotel_clean |>
mutate(
Faixa_Pos = cut(Review_Total_Positive_Word_Counts,
breaks = c(-1, 0, 15, 50, Inf),
labels = c("Nenhuma", "Poucas (1–15)",
"Média (16–50)", "Muitas (50+)"))
) |>
drop_na(Faixa_Pos)
ggplot(hotel_h2, aes(x = Faixa_Pos, y = Reviewer_Score, fill = Faixa_Pos)) +
geom_boxplot(alpha = 0.7, outlier.alpha = 0.05) +
scale_fill_brewer(palette = "Blues") +
labs(
title = "H2: Quantidade de Palavras Positivas vs. Nota",
x = "Faixa de Palavras Positivas",
y = "Nota do Avaliador"
) +
theme(legend.position = "none")hotel_h2 |>
select(Faixa_Pos, Reviewer_Score) |>
tbl_summary(
by = Faixa_Pos,
statistic = list(all_continuous() ~ "{mean} ± {sd}")
) |>
add_p() |>
bold_p()| Characteristic | Nenhuma N = 35,9461 |
Poucas (1–15) N = 286,4191 |
Média (16–50) N = 161,3881 |
Muitas (50+) N = 31,9851 |
p-value2 |
|---|---|---|---|---|---|
| Reviewer_Score | 6.89 ± 1.94 | 8.24 ± 1.67 | 8.86 ± 1.28 | 9.10 ± 1.18 | <0.001 |
| 1 Mean ± SD | |||||
| 2 Kruskal-Wallis rank sum test | |||||
# ANOVA (comparação de mais de 2 grupos)
aov_h2 <- aov(Reviewer_Score ~ Faixa_Pos, data = hotel_h2)
summary(aov_h2) Df Sum Sq Mean Sq F value Pr(>F)
Faixa_Pos 3 139245 46415 19239 <2e-16 ***
Residuals 515734 1244257 2
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Conclusão: A ANOVA indica diferença significativa entre os grupos (p < 0,05). Avaliações sem palavras positivas têm notas menores. Contudo, a relação não é perfeitamente linear: escrever mais não garante nota muito mais alta.
hotel_clean |>
sample_n(8000) |>
filter(Review_Total_Negative_Word_Counts <= 200) |>
ggplot(aes(x = Review_Total_Negative_Word_Counts, y = Reviewer_Score)) +
geom_point(alpha = 0.15, color = "#e74c3c", size = 0.8) +
geom_smooth(method = "lm", color = "darkred", se = TRUE, linewidth = 1) +
labs(
title = "H3: Palavras Negativas vs. Nota do Avaliador",
subtitle = "Limitado a avaliações com ≤ 200 palavras negativas (sem outliers extremos)",
x = "Quantidade de Palavras Negativas",
y = "Nota do Avaliador"
)hotel_clean |>
mutate(
Faixa_Neg = cut(Review_Total_Negative_Word_Counts,
breaks = c(-1, 0, 15, 50, Inf),
labels = c("Nenhuma", "Poucas (1–15)",
"Média (16–50)", "Muitas (50+)"))
) |>
drop_na(Faixa_Neg) |>
select(Faixa_Neg, Reviewer_Score) |>
tbl_summary(
by = Faixa_Neg,
statistic = list(all_continuous() ~ "{mean} ± {sd}")
) |>
add_p() |>
bold_p()| Characteristic | Nenhuma N = 127,8901 |
Poucas (1–15) N = 201,4781 |
Média (16–50) N = 141,7261 |
Muitas (50+) N = 44,6441 |
p-value2 |
|---|---|---|---|---|---|
| Reviewer_Score | 9.34 ± 0.92 | 8.50 ± 1.52 | 7.88 ± 1.67 | 6.87 ± 1.85 | <0.001 |
| 1 Mean ± SD | |||||
| 2 Kruskal-Wallis rank sum test | |||||
# Correlação de Spearman (mais robusta pois a variável não é normal)
cor.test(
hotel_clean$Review_Total_Negative_Word_Counts,
hotel_clean$Reviewer_Score,
method = "spearman",
exact = FALSE
)
Spearman's rank correlation rho
data: hotel_clean$Review_Total_Negative_Word_Counts and hotel_clean$Reviewer_Score
S = 3.3617e+16, p-value < 2.2e-16
alternative hypothesis: true rho is not equal to 0
sample estimates:
rho
-0.4703603
Conclusão: Correlação negativa significativa (p < 0,05). Quanto mais palavras negativas o hóspede escreve, menor tende a ser a nota. Intuitivo: quem tem muito a reclamar dá nota baixa.
hotel_h4 <- hotel_clean |> filter(!is.na(Trip_Type))
ggplot(hotel_h4, aes(x = Trip_Type, y = Reviewer_Score, fill = Trip_Type)) +
geom_violin(alpha = 0.5, trim = FALSE) +
geom_boxplot(width = 0.15, fill = "white", outlier.shape = NA) +
scale_fill_manual(values = c("Lazer" = "#3498db", "Negócios" = "#e67e22")) +
labs(
title = "H4: Tipo de Viagem vs. Nota do Avaliador",
x = "Tipo de Viagem",
y = "Nota do Avaliador"
) +
theme(legend.position = "none")hotel_h4 |>
select(Trip_Type, Reviewer_Score) |>
tbl_summary(
by = Trip_Type,
statistic = list(all_continuous() ~ "{mean} ± {sd}")
) |>
add_p() |>
bold_p()| Characteristic | Lazer N = 417,7781 |
Negócios N = 82,9391 |
p-value2 |
|---|---|---|---|
| Reviewer_Score | 8.49 ± 1.59 | 7.97 ± 1.78 | <0.001 |
| 1 Mean ± SD | |||
| 2 Wilcoxon rank sum test | |||
# Teste t de Welch (não assume variâncias iguais entre grupos)
t.test(Reviewer_Score ~ Trip_Type, data = hotel_h4)
Welch Two Sample t-test
data: Reviewer_Score by Trip_Type
t = 77.378, df = 110465, p-value < 2.2e-16
alternative hypothesis: true difference in means between group Lazer and group Negócios is not equal to 0
95 percent confidence interval:
0.5025061 0.5286246
sample estimates:
mean in group Lazer mean in group Negócios
8.488296 7.972731
Conclusão: O teste t indica diferença estatisticamente significativa entre os dois tipos de viagem (p < 0,05). Viajantes a lazer tendem a dar notas ligeiramente diferentes dos viajantes a negócios. Uma possível explicação: viagens de negócios têm expectativas mais objetivas (localização, wi-fi), enquanto viagens de lazer envolvem mais fatores emocionais.
hotel_h5 <- hotel_clean |>
mutate(
Experiencia = case_when(
Total_Number_of_Reviews_Reviewer_Has_Given == 1 ~ "1 avaliação",
Total_Number_of_Reviews_Reviewer_Has_Given <= 5 ~ "2–5 avaliações",
Total_Number_of_Reviews_Reviewer_Has_Given <= 20 ~ "6–20 avaliações",
TRUE ~ "21+ avaliações"
),
Experiencia = factor(
Experiencia,
levels = c("1 avaliação", "2–5 avaliações",
"6–20 avaliações", "21+ avaliações")
)
)
hotel_h5 |>
sample_n(12000) |>
ggplot(aes(x = Experiencia, y = Reviewer_Score, fill = Experiencia)) +
geom_boxplot(alpha = 0.7, outlier.alpha = 0.05) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "H5: Experiência do Avaliador vs. Nota Dada",
x = "Nível de Experiência",
y = "Nota do Avaliador"
) +
theme(legend.position = "none",
axis.text.x = element_text(angle = 15, hjust = 1))hotel_h5 |>
select(Experiencia, Reviewer_Score) |>
tbl_summary(
by = Experiencia,
statistic = list(all_continuous() ~ "{mean} ± {sd}")
) |>
add_p() |>
bold_p()| Characteristic | 1 avaliação N = 154,6401 |
2–5 avaliações N = 176,5771 |
6–20 avaliações N = 143,9751 |
21+ avaliações N = 40,5461 |
p-value2 |
|---|---|---|---|---|---|
| Reviewer_Score | 8.38 ± 1.72 | 8.41 ± 1.66 | 8.39 ± 1.57 | 8.40 ± 1.47 | <0.001 |
| 1 Mean ± SD | |||||
| 2 Kruskal-Wallis rank sum test | |||||
# ANOVA (4 grupos)
aov_h5 <- aov(Reviewer_Score ~ Experiencia, data = hotel_h5)
summary(aov_h5) Df Sum Sq Mean Sq F value Pr(>F)
Experiencia 3 71 23.619 8.805 7.81e-06 ***
Residuals 515734 1383431 2.682
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Conclusão: A ANOVA indica diferença significativa entre os grupos (p < 0,05). Avaliadores mais experientes (com mais avaliações no histórico) tendem a dar notas ligeiramente diferentes dos iniciantes. Uma hipótese é que usuários experientes são mais criteriosos ou têm parâmetros mais calibrados.
# Calculando % de dados completos por variável
completude <- hotel_clean |>
summarise(across(everything(), ~ mean(!is.na(.)) * 100)) |>
pivot_longer(everything(),
names_to = "variavel",
values_to = "pct_completo") |>
mutate(
pct_faltante = round(100 - pct_completo, 2),
pct_completo = round(pct_completo, 2),
n_faltante = round((pct_faltante / 100) * nrow(hotel_clean))
) |>
arrange(pct_completo)
# Tabela
completude |>
select(variavel, pct_completo, pct_faltante, n_faltante) |>
kable(
col.names = c("Variável", "% Completo", "% Faltante", "Nº de Faltantes"),
caption = "Completude dos Dados por Variável"
) |>
kable_styling(bootstrap_options = c("striped", "hover")) |>
row_spec(which(completude$pct_completo < 100), background = "#fff3cd")| Variável | % Completo | % Faltante | Nº de Faltantes |
|---|---|---|---|
| Traveler_Type | 73.43 | 26.57 | 137032 |
| Trip_Type | 97.09 | 2.91 | 15008 |
| lat | 99.37 | 0.63 | 3249 |
| lng | 99.37 | 0.63 | 3249 |
| Reviewer_Score | 100.00 | 0.00 | 0 |
| Average_Score | 100.00 | 0.00 | 0 |
| Review_Total_Positive_Word_Counts | 100.00 | 0.00 | 0 |
| Review_Total_Negative_Word_Counts | 100.00 | 0.00 | 0 |
| Total_Number_of_Reviews_Reviewer_Has_Given | 100.00 | 0.00 | 0 |
| days_since_review | 100.00 | 0.00 | 0 |
| Hotel_Country | 100.00 | 0.00 | 0 |
ggplot(completude,
aes(x = reorder(variavel, pct_completo),
y = pct_completo,
fill = pct_completo < 99)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = paste0(pct_completo, "%")),
hjust = -0.1, size = 3.5) +
scale_fill_manual(values = c("FALSE" = "#2ecc71", "TRUE" = "#e74c3c")) +
coord_flip() +
ylim(0, 112) +
labs(
title = "Completude das Variáveis",
x = NULL,
y = "% de Dados Completos"
)Interpretação: As variáveis com dados faltantes são:
lat e lng: coordenadas geográficas ausentes para alguns hotéis que não puderam ser geocodificadosTrip_Type e Traveler_Type: criadas a partir das Tags — quando a tag não estava presente, o valor ficou como NAPara as variáveis numéricas lat e lng, usamos o método MICE (Multivariate Imputation by Chained Equations) com o algoritmo PMM (Predictive Mean Matching).
Justificativa para o PMM:
Para as variáveis categóricas (Trip_Type, Traveler_Type), usamos a imputação pela moda (categoria mais frequente), que é simples e funciona bem para variáveis com poucas categorias.
Nota prática: o MICE é computacionalmente pesado para 515k linhas. Por isso, a demonstração abaixo usa uma amostra de 10.000 registros.
# Amostra para demonstrar a imputação
hotel_imp_antes <- hotel_clean |>
sample_n(10000) |>
select(Reviewer_Score, Average_Score,
Review_Total_Positive_Word_Counts,
Review_Total_Negative_Word_Counts,
lat, lng)
# Verificando NAs antes
cat("=== NAs ANTES da imputação ===\n")
print(colSums(is.na(hotel_imp_antes)))
# Rodando o MICE (m=1 = 1 conjunto imputado; maxit=10 iterações)
imp_mice <- mice(hotel_imp_antes,
m = 1,
method = "pmm",
maxit = 10,
seed = 42,
printFlag = FALSE)
hotel_imp_depois <- complete(imp_mice, 1)
cat("\n=== NAs DEPOIS da imputação ===\n")
print(colSums(is.na(hotel_imp_depois)))# Imputação categórica pela moda
moda <- function(x) {
x <- x[!is.na(x)]
ux <- unique(x)
ux[which.max(tabulate(match(x, ux)))]
}
hotel_cat_imp <- hotel_clean |>
sample_n(10000) |>
mutate(
Trip_Type = ifelse(is.na(Trip_Type), moda(Trip_Type), Trip_Type),
Traveler_Type = ifelse(is.na(Traveler_Type), moda(Traveler_Type), Traveler_Type)
)
cat("NAs em Trip_Type após imputação:", sum(is.na(hotel_cat_imp$Trip_Type)), "\n")NAs em Trip_Type após imputação: 0
cat("NAs em Traveler_Type após imputação:", sum(is.na(hotel_cat_imp$Traveler_Type)), "\n")NAs em Traveler_Type após imputação: 0
# Comparando distribuição de latitude antes e depois
bind_rows(
hotel_imp_antes |> select(lat) |> mutate(Momento = "Antes da Imputação"),
hotel_imp_depois |> select(lat) |> mutate(Momento = "Após Imputação")
) |>
filter(!is.na(lat)) |>
ggplot(aes(x = lat, fill = Momento)) +
geom_density(alpha = 0.5) +
scale_fill_manual(values = c("Antes da Imputação" = "#e74c3c",
"Após Imputação" = "#2ecc71")) +
labs(
title = "Distribuição de Latitude: Antes e Depois da Imputação (MICE/PMM)",
subtitle = "Distribuições muito similares indicam que o método não distorceu os dados",
x = "Latitude",
y = "Densidade",
fill = ""
)As distribuições antes e depois da imputação são muito parecidas, o que mostra que o MICE/PMM funcionou bem e não distorceu a variável.
# Dataset para modelagem (apenas variáveis sem NAs naturais)
hotel_modelo <- hotel_clean |>
select(
Reviewer_Score,
Average_Score,
Review_Total_Positive_Word_Counts,
Review_Total_Negative_Word_Counts,
Total_Number_of_Reviews_Reviewer_Has_Given
) |>
drop_na()
# Divisão treino (80%) e teste (20%)
set.seed(42)
n_total <- nrow(hotel_modelo)
idx_tr <- sample(1:n_total, size = floor(0.8 * n_total))
treino <- hotel_modelo[ idx_tr, ]
teste <- hotel_modelo[-idx_tr, ]
cat("Tamanho do conjunto de treino:", nrow(treino), "observações\n")Tamanho do conjunto de treino: 412590 observações
cat("Tamanho do conjunto de teste :", nrow(teste), "observações\n")Tamanho do conjunto de teste : 103148 observações
Usamos apenas a nota média do hotel para prever a nota do avaliador:
\[\hat{Y} = \beta_0 + \beta_1 \cdot \text{Average\_Score}\]
m1 <- lm(Reviewer_Score ~ Average_Score, data = treino)
summary(m1)
Call:
lm(formula = Reviewer_Score ~ Average_Score, data = treino)
Residuals:
Min 1Q Median 3Q Max
-7.0912 -0.7892 0.3683 1.0598 3.8745
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -0.716745 0.036448 -19.66 <2e-16 ***
Average_Score 1.085052 0.004331 250.52 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 1.526 on 412588 degrees of freedom
Multiple R-squared: 0.132, Adjusted R-squared: 0.132
F-statistic: 6.276e+04 on 1 and 412588 DF, p-value: < 2.2e-16
Adicionamos mais três variáveis preditoras:
\[\hat{Y} = \beta_0 + \beta_1 \cdot \text{Average\_Score} + \beta_2 \cdot \text{Palavras+} + \beta_3 \cdot \text{Palavras-} + \beta_4 \cdot \text{Experiência}\]
m2 <- lm(
Reviewer_Score ~
Average_Score +
Review_Total_Positive_Word_Counts +
Review_Total_Negative_Word_Counts +
Total_Number_of_Reviews_Reviewer_Has_Given,
data = treino
)
summary(m2)
Call:
lm(formula = Reviewer_Score ~ Average_Score + Review_Total_Positive_Word_Counts +
Review_Total_Negative_Word_Counts + Total_Number_of_Reviews_Reviewer_Has_Given,
data = treino)
Residuals:
Min 1Q Median 3Q Max
-12.5365 -0.6865 0.2546 0.9308 8.8193
Coefficients:
Estimate Std. Error t value
(Intercept) 1.035e+00 3.313e-02 31.239
Average_Score 8.864e-01 3.937e-03 225.133
Review_Total_Positive_Word_Counts 1.763e-02 9.912e-05 177.841
Review_Total_Negative_Word_Counts -2.072e-02 7.278e-05 -284.737
Total_Number_of_Reviews_Reviewer_Has_Given -1.850e-03 1.929e-04 -9.592
Pr(>|t|)
(Intercept) <2e-16 ***
Average_Score <2e-16 ***
Review_Total_Positive_Word_Counts <2e-16 ***
Review_Total_Negative_Word_Counts <2e-16 ***
Total_Number_of_Reviews_Reviewer_Has_Given <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 1.367 on 412585 degrees of freedom
Multiple R-squared: 0.303, Adjusted R-squared: 0.303
F-statistic: 4.483e+04 on 4 and 412585 DF, p-value: < 2.2e-16
# Funções auxiliares de métrica
rmse_fn <- function(real, pred) sqrt(mean((real - pred)^2))
r2_fn <- function(real, pred) {
1 - sum((real - pred)^2) / sum((real - mean(real))^2)
}
pred1 <- predict(m1, newdata = teste)
pred2 <- predict(m2, newdata = teste)
tibble(
Modelo = c("Modelo 1 (Simples)", "Modelo 2 (Múltiplo)"),
Preditoras = c("1 variável", "4 variáveis"),
`R² Treino` = round(c(summary(m1)$r.squared, summary(m2)$r.squared), 4),
`R² Adj` = round(c(summary(m1)$adj.r.squared, summary(m2)$adj.r.squared), 4),
`RMSE (Teste)` = round(c(rmse_fn(teste$Reviewer_Score, pred1),
rmse_fn(teste$Reviewer_Score, pred2)), 4),
`R² (Teste)` = round(c(r2_fn(teste$Reviewer_Score, pred1),
r2_fn(teste$Reviewer_Score, pred2)), 4),
`AIC` = round(c(AIC(m1), AIC(m2)), 1)
) |>
kable(caption = "Comparação dos Modelos de Regressão Linear") |>
kable_styling(bootstrap_options = c("striped", "hover")) |>
row_spec(2, bold = TRUE, background = "#d5f5e3")| Modelo | Preditoras | R² Treino | R² Adj | RMSE (Teste) | R² (Teste) | AIC |
|---|---|---|---|---|---|---|
| Modelo 1 (Simples) | 1 variável | 0.132 | 0.132 | 1.5235 | 0.1356 | 1519489 |
| Modelo 2 (Múltiplo) | 4 variáveis | 0.303 | 0.303 | 1.3614 | 0.3098 | 1429007 |
Justificativa da escolha: O Modelo 2 é melhor em todas as métricas: maior R² ajustado (explica mais variância), menor RMSE no teste (erra menos) e menor AIC (melhor equilíbrio entre ajuste e complexidade do modelo).
# Gráficos de diagnóstico do modelo 2
par(mfrow = c(2, 2))
plot(m2, col = "#3498db", pch = 20, cex = 0.5)par(mfrow = c(1, 1))tibble(
Real = teste$Reviewer_Score,
Previsto = pred2
) |>
sample_n(5000) |>
ggplot(aes(x = Real, y = Previsto)) +
geom_point(alpha = 0.15, color = "#3498db", size = 0.8) +
geom_abline(intercept = 0, slope = 1,
color = "red", linewidth = 1, linetype = "dashed") +
labs(
title = "Valores Reais vs. Previstos — Modelo 2 (Conjunto de Teste)",
subtitle = paste0(
"RMSE = ", round(rmse_fn(teste$Reviewer_Score, pred2), 3),
" | R² = ", round(r2_fn(teste$Reviewer_Score, pred2), 3)
),
x = "Valores Reais (Reviewer Score)",
y = "Valores Previstos pelo Modelo"
)Os pontos próximos da linha diagonal vermelha indicam boa previsão. O modelo captura a tendência geral, mas com variância residual — esperado, pois a nota de um hóspede é subjetiva e influenciada por muitos fatores não medidos.
Neste projeto, analisei uma base com mais de 515 mil avaliações de hotéis na Europa para entender o que influencia a nota dada pelos hóspedes (Reviewer_Score).
Principais achados:
Limitações: o R² do modelo é moderado, o que indica que existem fatores não capturados (experiência do hotel, sazonalidade, expectativas do hóspede) que também influenciam a nota.