Análise de Avaliações de Hotéis na Europa

Projeto Final — Ciência de Dados com R

Autor

Eva Carrasquel

Data de Publicação

25 de junho de 2026


1 Código no RPubs

O código completo deste projeto está disponível no RPubs: Clique aqui para acessar


2 Introdução

2.1 Escolha e Justificativa da Base de Dados

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:

  • Foi uma indicação da professora Edneide Florivalda Ramos
  • É uma base grande e rica, com muitas variáveis numéricas e categóricas
  • O tema é fácil de entender (todo mundo conhece avaliações de hotel)
  • Os dados têm valores faltantes naturais (coordenadas geográficas), o que permite exercitar a imputação
  • É relevante na prática: entender o que influencia a nota de um hóspede é importante para a indústria hoteleira

2.2 Variáveis Utilizadas

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)

2.3 Resultados Esperados

Espero encontrar que:

  • Hotéis com maior nota média recebam notas mais altas individualmente
  • Avaliações com mais palavras positivas estejam associadas a notas mais altas
  • Avaliações com mais palavras negativas estejam associadas a notas mais baixas
  • O tipo de viagem (lazer vs. negócios) influencie a nota dada
  • O modelo de regressão múltipla performe melhor que o modelo simples

3 Configuração do Ambiente

Código
install.packages(c(
  "tidyverse",    
  "lubridate",    
  "gtsummary",    
  "ggcorrplot",   
  "mice",         
  "patchwork",    
  "kableExtra",   
  "scales"        
))
Código
# 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)

4 Carregamento e Preparação dos Dados

Código
hotel <- read_csv("Hotel_Reviews.csv")

cat("Dimensões da base:\n")
Dimensões da base:
Código
cat("  Linhas  :", nrow(hotel), "\n")
  Linhas  : 515738 
Código
cat("  Colunas :", ncol(hotel), "\n\n")
  Colunas : 17 
Código
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…

4.1 Criando e limpando variáveis

Código
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…

5 Estatística Descritiva

Abaixo, a tabela de estatísticas descritivas usando o pacote gtsummary:

Código
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 (%)

6 Heatmap de Correlação

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.

Código
# 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)
  )

6.1 Interpretação

Analisando o heatmap visualmente:

  • Nota Média do Hotel é a variável com maior correlação positiva com a Nota do Avaliador. Faz sentido: avaliadores tendem a concordar com a reputação geral do hotel.
  • Palavras Negativas apresenta correlação negativa com a nota: quanto mais o hóspede escreveu de negativo, menor foi a nota que deu.
  • Palavras Positivas apresenta correlação positiva: mais palavras positivas, notas mais altas.
  • As demais variáveis (days_since_review, Avals. do Usuário) têm correlação muito fraca com a nota, o que já era esperado.

7 Normalidade das Variáveis

7.1 O que é uma distribuição normal?

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:

  • A curva é perfeitamente simétrica em torno da média
  • Média = Mediana = Moda
  • Cerca de 68% dos dados estão a 1 desvio padrão da média; 95% a 2; 99,7% a 3
  • Muitos testes estatísticos (teste t, ANOVA, regressão linear) assumem que os dados seguem essa distribuição

7.2 Histogramas + Curva de Densidade

As variáveis escolhidas foram:

  1. Reviewer_Score — variável de interesse principal
  2. Average_Score — nota média do hotel
  3. Review_Total_Positive_Word_Counts — quantidade de palavras positivas

Justificativa 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.

Código
# 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 / p3

7.3 Gráficos Q-Q (Quantil-Quantil)

O 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.

Código
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 / qq3

7.4 Teste de Shapiro-Wilk

O 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.

Código
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"))
Resultados do Teste de Shapiro-Wilk (n = 5.000)
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) |

7.5 Conclusão sobre Normalidade

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.


8 Hipóteses sobre a Variável de Interesse

Levanto 5 hipóteses/perguntas sobre o Reviewer_Score e testo cada uma com um gráfico, tabela resumo e teste estatístico adequado.


8.1 H1: Hotéis com nota média mais alta recebem notas mais altas dos avaliadores?

Código
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:

Código
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):

Código
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.


8.2 H2: Avaliações com mais palavras positivas estão associadas a notas mais altas?

Código
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")

Código
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
Código
# 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.


8.3 H3: Avaliações com mais palavras negativas têm notas menores?

Código
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"
  )

Código
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
Código
# 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.


8.4 H4: Viagens de lazer e de negócios recebem notas diferentes?

Código
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")

Código
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
Código
# 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.


8.5 H5: Avaliadores mais experientes dão notas diferentes dos iniciantes?

Código
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))

Código
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
Código
# 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.


9 Completude dos Dados

Código
# 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")
Completude dos Dados por Variável
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
Código
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 geocodificados
  • Trip_Type e Traveler_Type: criadas a partir das Tags — quando a tag não estava presente, o valor ficou como NA
  • As demais variáveis estão 100% completas

10 Imputação de Dados Faltantes

10.1 Método escolhido e justificativa

Para 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:

  • O PMM imputa valores que já existem na base (não inventa valores)
  • É robusto: não precisa que as variáveis sejam normalmente distribuídas
  • Preserva melhor a distribuição original dos dados do que simplesmente usar a média
  • É considerado um dos métodos mais seguros para variáveis numéricas em geral

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.

Código
# 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)))
Código
# 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 
Código
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 
Código
# 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.


11 Modelos de Regressão Linear

11.1 Preparação dos dados

Código
# 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
Código
cat("Tamanho do conjunto de teste :", nrow(teste),  "observações\n")
Tamanho do conjunto de teste : 103148 observações

11.2 Modelo 1 — Regressão Simples

Usamos apenas a nota média do hotel para prever a nota do avaliador:

\[\hat{Y} = \beta_0 + \beta_1 \cdot \text{Average\_Score}\]

Código
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

11.3 Modelo 2 — Regressão Múltipla

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}\]

Código
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

11.4 Comparação dos modelos

Código
# 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")
Comparação dos Modelos de Regressão Linear
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).

11.5 Diagnóstico do modelo final

Código
# Gráficos de diagnóstico do modelo 2
par(mfrow = c(2, 2))
plot(m2, col = "#3498db", pch = 20, cex = 0.5)

Código
par(mfrow = c(1, 1))

11.6 Previsões vs. Valores Reais

Código
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.


12 Conclusões

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:

  1. A nota média do hotel é o indicador mais forte da nota individual — o que faz sentido, pois hotéis bons tendem a receber boas avaliações consistentemente.
  2. Avaliadores que escrevem mais texto positivo dão notas mais altas; quem escreve mais texto negativo dá notas mais baixas.
  3. Existe diferença estatisticamente significativa entre viagens de lazer e negócios.
  4. Avaliadores mais experientes (com mais avaliações no histórico) tendem a dar notas ligeiramente diferentes dos iniciantes.
  5. O Modelo 2 (regressão múltipla com 4 preditoras) performa melhor que o modelo simples, com maior R² e menor RMSE.

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.


13 Referências

  • Kaggle Dataset: 515k Hotel Reviews Data in Europe
  • Wickham, H. et al. (2023). Tidyverse: Easily Install and Load the Tidyverse.
  • Aulas da disciplina eletiva: Ciência de Dados com R - Profesora, Edineide Florivalda Ramos.