Projeto de Disciplina 26.1

Análise Exploratória de Dados

Author

Anabella Sgarbi Pereira

Published

April 13, 2026

1 Carregando os pacotes

library(tidyverse)
library(skimr)
library(gt)
library(summarytools)
library(ggplot2)
library(corrplot)
library(patchwork)
library(rstatix)
library(gtsummary)
library(janitor)
library(naniar)
library(visdat)
library(DataExplorer)
library(scales)
library(knitr)
library(kableExtra)
library(tidymodels)
library(effectsize)
library(broom)

2 Carregando os dados

Base de dados baixada do Kaggle: https://www.kaggle.com/datasets/allanbruno/airbnb-rio-de-janeiro

dez19 = read_csv("dados/dezembro2019.csv")
jan20 = read_csv("dados/janeiro2020.csv")
fev20 = read_csv("dados/fevereiro2020.csv")
mar20 = read_csv("dados/maro2020.csv")
abr20 = read_csv("dados/abril2020.csv")
maio20 = read_csv("dados/maio2020.csv")

2.1 Descrição das bases de dados

Reúnem informações dos imóveis disponíveis para locação pelo Airbnb no Rio de Janeiro, do período de dezembro de 2019 até maio de 2020.

Possuem 108 colunas, que descrevem desde o preço e o período de disponibilidade de locação até detalhes sobre o host (dono) e do próprio imóvel.

O dicionário dos dados pode ser encontrado no seguinte link: https://docs.google.com/spreadsheets/d/1iWCNJcSutYqpULSQHlNyGInUvHg2BoUGoNRIGa6Szc4/edit?gid=1322284596#gid=1322284596

3 Objetivo da análise

O objetivo da presente análise consiste em identificar as variáveis que mais influenciaram o preço cobrado na locação de imóveis em anúncios do Airbnb no Rio de Janeiro no período disponível na base de dados.

A escolha por este tema de estudo se deu por uma curiosidade pessoal minha acerca da cidade onde moro desde nasci.

4 Limpeza dos dados

4.1 Construção de base única

Para facilitar o trabalho de limpeza e análise, as 6 bases serão unificadas em uma, com adição de uma coluna do mês e ano correspondente.

dez19 = dez19 |>  
  mutate(mes = "2019-12")

jan20 = jan20  |> 
  mutate(mes = "2020-01")

fev20 = fev20 |> 
  mutate(mes = "2020-02")

mar20 = mar20 |> 
  mutate(mes = "2020-03")

abr20 = abr20 |> 
  mutate(mes = "2020-04")

maio20 = maio20 |> 
  mutate(mes = "2020-05")
df_raw = bind_rows(dez19, jan20, fev20, mar20, abr20, maio20)

4.2 Visão geral

# skim(df_raw)

Variável de interesse: price, que determina o preço de aluguel dos imóveis.

A base df_raw possui muitas variáveis descartáveis para a análise, como listing_url, last_scraped, host_thumbnail_url, entre outras.

Sendo assim, abaixo serão selecionadas somente as variáveis que, de fato, podem ter alguma influência no preço do aluguel de um imóvel:

df_clean = df_raw |>
  select(
    price,
    room_type,
    accommodates,
    bedrooms,
    bathrooms,
    neighbourhood_cleansed, 
    number_of_reviews, 
    review_scores_rating, 
    minimum_nights, 
    host_is_superhost, 
    property_type, 
    mes, 
    guests_included
    )

4.3 Investigação inicial

df_clean |> 
  select(where(is.numeric), where(is.factor)) |>
  tbl_summary(
    statistic = list(
      all_continuous() ~ "{median} ({p25}, {p75})",
      all_categorical() ~ "{n} ({p}%)"
    ),
    digits = all_continuous() ~ 2
  ) |>
  modify_header(label ~ "**Variável**") |>
  modify_spanning_header(all_stat_cols() ~ "**Resumo Geral**") |>
  modify_caption("**Tabela 1. Estatística descritiva das variáveis da base de dados**") |>
  bold_labels()
Tabela 1. Estatística descritiva das variáveis da base de dados
Variável
Resumo Geral
N = 213,5141
accommodates 4.00 (2.00, 5.00)
bedrooms 1.00 (1.00, 2.00)
    Unknown 389
bathrooms 1.00 (1.00, 2.00)
    Unknown 374
number_of_reviews 1.00 (0.00, 6.00)
review_scores_rating 98.00 (93.00, 100.00)
    Unknown 95,105
minimum_nights 2.00 (1.00, 4.00)
guests_included 1.00 (1.00, 2.00)
1 Median (Q1, Q3)

Há mais de 200 mil observações na nossa base. Nesta primeira descrição geral, destacam-se as seguintes medianas e IQRs de características dos imóveis colocados para locação pelo Airbnb no período de dezembro de 2019 até maio de 2020 no Rio de Janeiro:

  • Quantidade máxima de pessoas acomodadas (accommodates): 4 (IQR = 2, 5)

  • Quantidade de reviews (number_of_reviews): 1 (IQR = 0, 6)

  • Notas das reviews (review_scores_rating): 98 (IQR = 93, 100)

  • Quantidade mínima de noites a serem alugadas (minimum_nights): 2 (IQR = 1, 4)

Preço (price) não apareceu na lista, o que indica que a variável deve estar erroneamente classificada como categórica.

4.3.1 Tipagem de variáveis

Identificando os tipos de variáveis presentes na base:

df_clean |> 
  summarise(across(everything(), class)) |>
  pivot_longer(everything(),
               names_to = "variavel",
               values_to = "tipo") |> 
  count(tipo, sort = TRUE) |> 
  kbl(col.names = c("Tipo", "Quantidade"), align = "lr") |> 
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
Tipo Quantidade
numeric 7
character 5
logical 1

A base possui o total de 13 variáveis, sendo 7 numéricas, 5 categóricas e 1 lógica.

Checando quais variáveis numéricas são, de fato, numéricas:

df_clean |> 
  select(where(is.numeric)) |> 
  glimpse()
Rows: 213,514
Columns: 7
$ accommodates         <dbl> 5, 2, 3, 2, 2, 13, 1, 10, 3, 6, 12, 2, 6, 4, 6, 4…
$ bedrooms             <dbl> 2, 1, 1, 1, 1, 6, 1, 4, 1, 4, 4, 0, 4, 1, 3, 3, 0…
$ bathrooms            <dbl> 1.0, 1.0, 1.0, 1.5, 1.0, 7.0, 1.0, 3.5, 1.0, 3.0,…
$ number_of_reviews    <dbl> 246, 236, 274, 171, 320, 64, 42, 3, 79, 71, 7, 16…
$ review_scores_rating <dbl> 93, 94, 96, 94, 98, 92, 98, 80, 93, 91, 91, 95, 9…
$ minimum_nights       <dbl> 4, 7, 2, 2, 3, 2, 3, 4, 3, 3, 1, 3, 7, 5, 2, 4, 3…
$ guests_included      <dbl> 2, 2, 2, 2, 2, 7, 1, 8, 2, 4, 1, 2, 5, 2, 3, 2, 2…
  • review_scores_rating: variável de nível de qualidade percebida = ambíguo (pode ser tanto numérica quanto fator)

  • bathrooms: variável que carrega informação dupla (quantidade de banheiros completos e lavabos) = será mantida como variável numérica única, pois as informações fazem sentido quando unificadas e facilitam a análise

  • acommodates, bedrooms, number_of_reviews, minimun_nights e guests_included: variáveis de contagem = numéricas inteiras

Tratando as variáveis:

df_clean = df_clean |>
  mutate(
    across(
      c(accommodates, bedrooms, number_of_reviews,
        minimum_nights, guests_included),
      as.integer
    )
  )

OBS: Optou-se por manter a variável review_scores_rating como numérica (dbl), por representar uma escala contínua de qualidade, preservando maior nível de informação.

Checando quais variáveis categóricas são, de fato, categóricas:

df_clean |> 
  select(where(is.character)) |> 
  glimpse()
Rows: 213,514
Columns: 5
$ price                  <chr> "$323.00", "$159.00", "$266.00", "$368.00", "$1…
$ room_type              <chr> "Entire home/apt", "Entire home/apt", "Entire h…
$ neighbourhood_cleansed <chr> "Copacabana", "Copacabana", "Ipanema", "Ipanema…
$ property_type          <chr> "Condominium", "Apartment", "Apartment", "Apart…
$ mes                    <chr> "2019-12", "2019-12", "2019-12", "2019-12", "20…
  • price: variável de preço = numérica

  • mes: variável de mês = data

Tratando as variáveis:

# Tratando price
df_clean = df_clean |>
  mutate(price = parse_number(price))
# Tratando mes
df_clean = df_clean |>
  mutate(mes = ymd(paste0(mes, "-01")))

Verificando tipagem final das variáveis:

df_clean |> 
  glimpse()
Rows: 213,514
Columns: 13
$ price                  <dbl> 323, 159, 266, 368, 131, 3492, 78, 851, 184, 90…
$ room_type              <chr> "Entire home/apt", "Entire home/apt", "Entire h…
$ accommodates           <int> 5, 2, 3, 2, 2, 13, 1, 10, 3, 6, 12, 2, 6, 4, 6,…
$ bedrooms               <int> 2, 1, 1, 1, 1, 6, 1, 4, 1, 4, 4, 0, 4, 1, 3, 3,…
$ bathrooms              <dbl> 1.0, 1.0, 1.0, 1.5, 1.0, 7.0, 1.0, 3.5, 1.0, 3.…
$ neighbourhood_cleansed <chr> "Copacabana", "Copacabana", "Ipanema", "Ipanema…
$ number_of_reviews      <int> 246, 236, 274, 171, 320, 64, 42, 3, 79, 71, 7, …
$ review_scores_rating   <dbl> 93, 94, 96, 94, 98, 92, 98, 80, 93, 91, 91, 95,…
$ minimum_nights         <int> 4, 7, 2, 2, 3, 2, 3, 4, 3, 3, 1, 3, 7, 5, 2, 4,…
$ host_is_superhost      <lgl> TRUE, FALSE, TRUE, TRUE, TRUE, TRUE, TRUE, FALS…
$ property_type          <chr> "Condominium", "Apartment", "Apartment", "Apart…
$ mes                    <date> 2019-12-01, 2019-12-01, 2019-12-01, 2019-12-01…
$ guests_included        <int> 2, 2, 2, 2, 2, 7, 1, 8, 2, 4, 1, 2, 5, 2, 3, 2,…

Agora possuímos 18 variáveis, sendo:

  • room_type, neighbourhood_cleansed e property_type: categóricas

  • price, accommodates, bedrooms, bathrooms, number_of_reviews, review_scores_rating, minimum_nights, guests_included: numéricas

  • host_is_superhost: lógica

  • mes: data

Tratando a variável lógica (transformando em categórica para facilitar ações futuras):

df_clean = df_clean |>
  mutate(
    host_is_superhost = factor(host_is_superhost, 
                               levels = c(TRUE, FALSE),
                               labels = c("Superhost", "Host"))
  )

Verificando tipagem final das variáveis:

df_clean |> 
  glimpse()
Rows: 213,514
Columns: 13
$ price                  <dbl> 323, 159, 266, 368, 131, 3492, 78, 851, 184, 90…
$ room_type              <chr> "Entire home/apt", "Entire home/apt", "Entire h…
$ accommodates           <int> 5, 2, 3, 2, 2, 13, 1, 10, 3, 6, 12, 2, 6, 4, 6,…
$ bedrooms               <int> 2, 1, 1, 1, 1, 6, 1, 4, 1, 4, 4, 0, 4, 1, 3, 3,…
$ bathrooms              <dbl> 1.0, 1.0, 1.0, 1.5, 1.0, 7.0, 1.0, 3.5, 1.0, 3.…
$ neighbourhood_cleansed <chr> "Copacabana", "Copacabana", "Ipanema", "Ipanema…
$ number_of_reviews      <int> 246, 236, 274, 171, 320, 64, 42, 3, 79, 71, 7, …
$ review_scores_rating   <dbl> 93, 94, 96, 94, 98, 92, 98, 80, 93, 91, 91, 95,…
$ minimum_nights         <int> 4, 7, 2, 2, 3, 2, 3, 4, 3, 3, 1, 3, 7, 5, 2, 4,…
$ host_is_superhost      <fct> Superhost, Host, Superhost, Superhost, Superhos…
$ property_type          <chr> "Condominium", "Apartment", "Apartment", "Apart…
$ mes                    <date> 2019-12-01, 2019-12-01, 2019-12-01, 2019-12-01…
$ guests_included        <int> 2, 2, 2, 2, 2, 7, 1, 8, 2, 4, 1, 2, 5, 2, 3, 2,…

Agora host_is_superhost é fator.

4.3.2 Tratamento de NAs

Identificando a proporção de NAs:

na.summary = df_clean |> 
  summarise(across(everything(), ~ mean(is.na(.)))) |> 
  pivot_longer(everything(),
               names_to = "variavel",
               values_to = "pct_na") |> 
  filter(pct_na > 0) |> 
  arrange(desc(pct_na)) |> 
  mutate(pct_na_fmt = percent(pct_na, accuracy = 0.1))

na.summary |> 
    kbl(col.names = c("Variável", "% NA", "% Formatada"), align = "lrr", caption = "Variáveis com valores ausentes") |> 
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE) |> 
  row_spec(which(na.summary$pct_na > 0.4), background = "#fa93b0")
Variáveis com valores ausentes
Variável % NA % Formatada
review_scores_rating 0.4454275 44.5%
bedrooms 0.0018219 0.2%
bathrooms 0.0017516 0.2%
host_is_superhost 0.0006510 0.1%

Visualização:

# Gerando amostragem para possibilitar a geração do gráfico
df_vis = df_clean |> 
  slice_sample(n = 5000)
# Mapa Visual de NAs
vis_miss(
  df_vis, sort_miss = TRUE
) +
  labs(title = "Valores ausentes na base do Airbnb RJ",
       subtitle = "Ordenado por proporção de missings") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7))

Observa-se que review_scores_rating é a única variável com mais de 1% de NAs (44,5%).

Analisando cada caso de NAs:

  • review_scores_rating NAs = não há reviews para o imóvel

  • bedrooms e bathrooms NAs = números não especificados no anúncio

  • host_is_superhost NAs = não especificado no anúncio

Tratando os NAs:

  • Para review_scores_rating, bedrooms e bathrooms, foi imputada a mediana:
df_clean = df_clean |>
  mutate(
    # rating
    review_scores_rating = coalesce(
      review_scores_rating,
      median(review_scores_rating, na.rm = TRUE)
    ),

    # variáveis estruturais
    across(
      c(bedrooms, bathrooms),
      ~ coalesce(., median(., na.rm = TRUE))
    )
  )
  • Para host_is_superhost, foi imputada a moda:
moda_superhost = names(sort(table(df_clean$host_is_superhost), decreasing = TRUE))[1]

moda_superhost
[1] "Host"
df_clean = df_clean |>
  mutate(
    host_is_superhost = coalesce(host_is_superhost, moda_superhost)
  )

Checagem final da proporção de NAs da base de dados:

# Gerando amostragem para possibilitar a geração do gráfico
df_vis = df_clean |> 
  slice_sample(n = 5000)
# Mapa Visual de NAs
vis_miss(
  df_vis, sort_miss = TRUE
) +
  labs(title = "Valores ausentes na base do Airbnb RJ",
       subtitle = "Ordenado por proporção de missings") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7))

NAs foram totalmente removidos da base de dados.

4.3.3 Tratamento de outliers

Identificando outliers:

df_clean |> 
  ggplot(aes(y = price)) +
  geom_boxplot(outlier.alpha = 0.3) +
  scale_y_continuous(
  labels = scales::label_dollar(
    prefix = "R$ ",
    big.mark = ".",
    decimal.mark = ","
  )
) +
  theme_minimal()

Observa-se que há outliers especialmente a partir de mais de R$ 70.000. Há outliers que ultrapassam R$ 125.000.

Identificando o IQR

q1 = quantile(df_clean$price, 0.25)
q3 = quantile(df_clean$price, 0.75)
iqr = q3 - q1
inferior = q1 - 1.5 * iqr
superior = q3 + 1.5 * iqr

Características de price:

  • Q1 = R$ 157

  • Q3 = R$ 611

  • IQR = R$ 454

  • Inferior = - R$ 524

  • Superior = R$ 1.292

Identificando quantos imóveis são muito caros:

df_clean |>
  filter(price > superior) |>
  nrow()
[1] 22297

Temos o total de 22.297 imóveis acima do IQR superior. São muitas observações para cortarmos.

Investigando os outliers por bairro:

top_bairros = df_clean |>
  count(neighbourhood_cleansed, sort = TRUE) |>
  slice_head(n = 15) |>
  pull(neighbourhood_cleansed)

df_clean |>
  filter(neighbourhood_cleansed %in% top_bairros) |>
  ggplot(aes(x = neighbourhood_cleansed, y = price)) +
  geom_boxplot() +
  scale_y_continuous(
  labels = scales::label_dollar(
    prefix = "R$ ",
    big.mark = ".",
    decimal.mark = ","
  )
) +
  labs(
    title = "Distribuição de preço por bairro",
    subtitle = "Bairros selecionados: top 15",
    x = "Bairro",
    y = "Preço"
  ) +
  coord_flip() +
  theme_minimal()

Observa-se que Copacana é o bairro com 2 observações de extrema discrepância em relação ao restante.

Investigando estes outliers:

df_clean |>
  filter(neighbourhood_cleansed == "Copacabana") |>
  arrange(desc(price)) |>
  select(price, accommodates, bedrooms, bathrooms, room_type, property_type) |>
  head(5) |> 
  gt()
price accommodates bedrooms bathrooms room_type property_type
138262 2 1 1 Private room Bed and breakfast
131727 2 1 1 Private room Bed and breakfast
57517 4 2 2 Entire home/apt Apartment
55305 4 2 3 Entire home/apt Apartment
54798 4 2 2 Entire home/apt Apartment

Os dois valores mais altos (R$ 138.262 e R$ 131.727) são relativos a quartos privativos em propriedades de categoria “Bed and breakfast”, com acomodação para 2 pessoas, 1 banheiro e 1 quarto. Há fortes indicativos de erro, uma vez que nem mesmo em hotéis de luxo encontramos esses valores para quartos.

Logo, estes outliers serão removidos, pois também destoam muito da concentração geral de valores.

df = df_clean |>
  filter(price <= 60000)

Checagem final de outliers:

df |> 
  ggplot(aes(y = price)) +
  geom_boxplot(outlier.alpha = 0.3) +
  scale_y_continuous(
  labels = scales::label_dollar(
    prefix = "R$ ",
    big.mark = ".",
    decimal.mark = ","
  )
) +
  theme_minimal()

Agora todas as observações concentram-se em valores até R$ 60.000.

5 Análise Univariada

5.1 Variáveis numéricas

Descrição estatística da variável numérica price, após remoção de outliers:

df |> 
  select(price) |> 
  descr(stats = c("mean", "sd", "min", "med", "max", "iqr"),
    transpose = TRUE)
Descriptive Statistics  
df$price  
N: 213510  

                Mean   Std.Dev    Min   Median        Max      IQR
----------- -------- --------- ------ -------- ---------- --------
      price   701.89   1930.32   0.00   298.00   57517.00   454.00

Trata-se de uma variável com mediana de R$ 298 e média de aproximadamente R$ 702, o que indica que pode se tratar de uma variável com assimetria à direita.

# Criando função para calcular normalidade
plot_normalidade = function(dados, var, bins = 30, xlim = NULL){
  
  var_enquo = rlang::enquo(var)
  
  vetor = dados |> dplyr::pull(!!var_enquo)
  
  media = mean(vetor, na.rm = TRUE)
  mediana = median(vetor, na.rm = TRUE)
  
  p1 = dados |> 
    ggplot(aes(x = !!var_enquo)) +
    geom_histogram(
      aes(y = after_stat(density)),
      bins = bins,
      fill = "lightblue",
      color = "black"
    ) +
    geom_density(color = "darkblue", linewidth = 0.8, alpha = 0.3) +
    theme_minimal() +
    labs(
      title = paste0("Distribuição - ", rlang::as_name(var_enquo)),
      x = rlang::as_name(var_enquo),
      y = "Densidade"
    )
  
  # aplicar limite do eixo x se for especificado
  if(!is.null(xlim)){
    p1 = p1 + coord_cartesian(xlim = xlim)
  }
  
  p2 = dados |> 
    ggplot(aes(sample = !!var_enquo)) +
    stat_qq(size = 2, alpha = 0.6) +
    stat_qq_line(color = "red", linewidth = 1) +
    theme_minimal() +
    labs(
      title = paste0("QQ plot - ", rlang::as_name(var_enquo)),
      x = "Quantis teóricos",
      y = "Quantis amostrais"
    )
  
  p1 + p2
}
df |> 
plot_normalidade(price, bins = 300, xlim = c(0, 10000))

OBS: A quantidade de bins escolhida para o histograma foi o valor encontrado que melhor permite a visualização da distribuição das observações.

Os gráficos acima indicam que, como esperado, a variável price não segue distribuição normal. Trata-se de uma variável assimétrica à direita. Ou seja, a maioria dos preços se concentram em valores abaixo de R$ 1.000, tendo poucas observações com valores extremos. O IQR de R$ 454 (R$ 157 - R$ 611) descreve a variabilidade dos preços mais comuns sem grande influência de valores extremos.

Descrição estatística da variável review_scores_rating:

df |> 
  select(review_scores_rating) |> 
  descr(stats = c("mean", "sd", "min", "med", "max", "iqr"),
    transpose = TRUE)
Descriptive Statistics  
df$review_scores_rating  
N: 213510  

                              Mean   Std.Dev     Min   Median      Max    IQR
-------------------------- ------- --------- ------- -------- -------- ------
      review_scores_rating   96.11      7.28   20.00    98.00   100.00   2.00

Trata-se de uma variável com mediana de 98 e média de aproximadamente 96, o que indica que pode se tratar de uma variável com assimetria à esquerda.

df |> 
plot_normalidade(review_scores_rating, bins = 100)

OBS: A quantidade de bins escolhida para o histograma foi o valor encontrado que melhor permite a visualização da distribuição das observações.

Os gráficos acima indicam que, como esperado, a variável review_scores_rating não segue distribuição normal. Trata-se de uma variável assimétrica à esquerda. Ou seja, a maioria das notas se concentram em valores acima de 90, tendo poucas observações com notas baixas. O IQR de 2 (97, 99) comprova a baixa variabilidade de notas.

Descrição estatística da variável number_of_reviews:

df |> 
  select(number_of_reviews) |> 
  descr(stats = c("mean", "sd", "min", "med", "max", "iqr"),
    transpose = TRUE)
Descriptive Statistics  
df$number_of_reviews  
N: 213510  

                          Mean   Std.Dev    Min   Median      Max    IQR
----------------------- ------ --------- ------ -------- -------- ------
      number_of_reviews   9.69     25.55   0.00     1.00   401.00   6.00

Trata-se de uma variável com mediana de 1 e média de aproximadamente 10, o que indica que pode se tratar de uma variável com assimetria à direita.

df |> 
plot_normalidade(number_of_reviews, bins = 50, xlim = c(0, 100))

OBS: A quantidade de bins escolhida para o histograma foi o valor encontrado que melhor permite a visualização da distribuição das observações.

Os gráficos acima indicam que, como esperado, a variável number_of_reviews não segue distribuição normal. Trata-se de uma variável assimétrica à direita. Ou seja, a maioria dos anúncios possuem menos de 1 review, sendo poucos os anúncios com maior quantidade de avaliações. O IQR de 6 (0, 6) descreve a variabilidade das quantidades de reviews por anúncio mais comuns sem grande influência de valores extremos.

5.2 Variáveis categóricas

Descrição estatística da variável room_type:

df |> 
  select(room_type) |> 
  tbl_summary() |> 
  modify_header(label ~ "**Variável**") |>
  modify_spanning_header(all_stat_cols() ~ "**Proporção**") |>
  modify_caption("**Tabela 3. Proporção de tipos de imóveis**") |>
  bold_labels()
Tabela 3. Proporção de tipos de imóveis
Variável
Proporção
N = 213,5101
room_type
    Entire home/apt 153,028 (72%)
    Hotel room 1,278 (0.6%)
    Private room 54,591 (26%)
    Shared room 4,613 (2.2%)
1 n (%)
df |>
  filter(!is.na(room_type)) |>
  ggplot(aes(x = reorder(room_type, room_type, function(x) length(x)), fill = room_type)) +
  geom_bar() +
  theme_minimal() +
  labs (
    x = "Tipo de imóvel",
    y = "Frequência",
    title = "Distribuição dos tipos de imóveis",
    subtitle = "Base de dados: df_clean"
  ) +
  theme(legend.position = "none")

Observa-se que mais de 70% dos imóveis para locação são do tipo entire home/apt (casa/apartamento inteiro). A segunda categoria mais comum é private room (quarto privado). As demais categorias (quarto de hotel e quarto compartilhado) representam menos de 3% das observações, mesmo somadas.

Descrição estatística da variável neighbourhood_cleansed:

df |> 
  select(neighbourhood_cleansed) |> 
  mutate(neighbourhood_cleansed = fct_infreq(neighbourhood_cleansed)) |> 
  tbl_summary() |> 
  modify_header(label ~ "**Variável**") |>
  modify_spanning_header(all_stat_cols() ~ "**Proporção**") |>
  modify_caption("**Tabela 4. Proporção de bairros**") |>
  bold_labels()
Tabela 4. Proporção de bairros
Variável
Proporção
N = 213,5101
neighbourhood_cleansed
    Copacabana 55,910 (26%)
    Barra da Tijuca 23,989 (11%)
    Ipanema 18,385 (8.6%)
    Jacarepaguá 12,033 (5.6%)
    Botafogo 10,476 (4.9%)
    Recreio dos Bandeirantes 10,053 (4.7%)
    Leblon 9,764 (4.6%)
    Santa Teresa 6,920 (3.2%)
    Centro 5,922 (2.8%)
    Flamengo 5,518 (2.6%)
    Tijuca 4,344 (2.0%)
    Laranjeiras 4,278 (2.0%)
    Leme 3,722 (1.7%)
    Lagoa 2,416 (1.1%)
    Glória 2,125 (1.0%)
    Catete 2,034 (1.0%)
    Jardim Botânico 1,909 (0.9%)
    Gávea 1,851 (0.9%)
    Humaitá 1,777 (0.8%)
    Camorim 1,775 (0.8%)
    Maracanã 1,754 (0.8%)
    Vila Isabel 1,589 (0.7%)
    São Conrado 1,465 (0.7%)
    Freguesia (Jacarepaguá) 1,313 (0.6%)
    Vargem Pequena 1,149 (0.5%)
    Taquara 1,067 (0.5%)
    Itanhangá 1,028 (0.5%)
    Vidigal 964 (0.5%)
    Urca 875 (0.4%)
    Vargem Grande 764 (0.4%)
    Cosme Velho 693 (0.3%)
    Rio Comprido 681 (0.3%)
    São Cristóvão 631 (0.3%)
    Grajaú 618 (0.3%)
    Estácio 555 (0.3%)
    Joá 520 (0.2%)
    Curicica 516 (0.2%)
    Andaraí 504 (0.2%)
    Campo Grande 482 (0.2%)
    Praça da Bandeira 467 (0.2%)
    Engenho de Dentro 460 (0.2%)
    Pechincha 437 (0.2%)
    Guaratiba 416 (0.2%)
    Barra de Guaratiba 405 (0.2%)
    Anil 356 (0.2%)
    Jardim Guanabara 339 (0.2%)
    Alto da Boa Vista 336 (0.2%)
    Méier 330 (0.2%)
    Engenho Novo 307 (0.1%)
    Todos os Santos 242 (0.1%)
    Cachambi 225 (0.1%)
    Irajá 216 (0.1%)
    Praça Seca 206 (<0.1%)
    São Francisco Xavier 200 (<0.1%)
    Paquetá 188 (<0.1%)
    Portuguesa 177 (<0.1%)
    Tanque 162 (<0.1%)
    Cidade Nova 161 (<0.1%)
    Saúde 158 (<0.1%)
    Del Castilho 155 (<0.1%)
    Riachuelo 154 (<0.1%)
    Gardênia Azul 153 (<0.1%)
    Rocha 151 (<0.1%)
    Brás de Pina 149 (<0.1%)
    Gamboa 144 (<0.1%)
    Bangu 137 (<0.1%)
    Rocinha 134 (<0.1%)
    Bonsucesso 128 (<0.1%)
    Santa Cruz 128 (<0.1%)
    Encantado 126 (<0.1%)
    Mangueira 118 (<0.1%)
    Penha 116 (<0.1%)
    Lins de Vasconcelos 109 (<0.1%)
    Santo Cristo 104 (<0.1%)
    Vila Valqueire 96 (<0.1%)
    Pedra de Guaratiba 90 (<0.1%)
    Piedade 90 (<0.1%)
    Tauá 90 (<0.1%)
    Maria da Graça 83 (<0.1%)
    Cidade de Deus 82 (<0.1%)
    Campinho 79 (<0.1%)
    Marechal Hermes 79 (<0.1%)
    Realengo 78 (<0.1%)
    Guadalupe 77 (<0.1%)
    Quintino Bocaiúva 75 (<0.1%)
    Jardim Sulacap 73 (<0.1%)
    Vila da Penha 72 (<0.1%)
    Cosmos 68 (<0.1%)
    Jardim Carioca 68 (<0.1%)
    Parque Anchieta 68 (<0.1%)
    Bento Ribeiro 67 (<0.1%)
    Madureira 63 (<0.1%)
    Padre Miguel 63 (<0.1%)
    Olaria 61 (<0.1%)
    Sampaio 61 (<0.1%)
    Higienópolis 60 (<0.1%)
    Ramos 60 (<0.1%)
    Vasco da Gama 60 (<0.1%)
    Benfica 55 (<0.1%)
    Cascadura 55 (<0.1%)
    Osvaldo Cruz 54 (<0.1%)
    Penha Circular 54 (<0.1%)
    Cacuia 51 (<0.1%)
    Catumbi 51 (<0.1%)
    Moneró 51 (<0.1%)
    Paciência 51 (<0.1%)
    Vicente de Carvalho 50 (<0.1%)
    Anchieta 45 (<0.1%)
    Inhaúma 45 (<0.1%)
    Pilares 45 (<0.1%)
    Santíssimo 42 (<0.1%)
    Engenho da Rainha 41 (<0.1%)
    Freguesia (Ilha) 41 (<0.1%)
    Tomás Coelho 41 (<0.1%)
    Senador Vasconcelos 40 (<0.1%)
    Sepetiba 40 (<0.1%)
    Pavuna 39 (<0.1%)
    Magalhães Bastos 36 (<0.1%)
    Jacaré 34 (<0.1%)
    Ricardo de Albuquerque 34 (<0.1%)
    Abolição 33 (<0.1%)
    Parada de Lucas 33 (<0.1%)
    Barros Filho 32 (<0.1%)
    Cocotá 31 (<0.1%)
    Galeão 31 (<0.1%)
    Senador Camará 28 (<0.1%)
    Cordovil 26 (<0.1%)
    Inhoaíba 26 (<0.1%)
    Água Santa 24 (<0.1%)
    Pitangueiras 24 (<0.1%)
    Praia da Bandeira 22 (<0.1%)
    Complexo do Alemão 20 (<0.1%)
    Vila Kosmos 19 (<0.1%)
    Bancários 18 (<0.1%)
    Grumari 18 (<0.1%)
    Rocha Miranda 18 (<0.1%)
    Coelho Neto 16 (<0.1%)
    Colégio 16 (<0.1%)
    Ribeira 16 (<0.1%)
    Vaz Lobo 16 (<0.1%)
    Vila Militar 16 (<0.1%)
    Vigário Geral 14 (<0.1%)
    Deodoro 12 (<0.1%)
    Manguinhos 12 (<0.1%)
    Engenheiro Leal 10 (<0.1%)
    Caju 7 (<0.1%)
    Honório Gurgel 7 (<0.1%)
    Vista Alegre 7 (<0.1%)
    Campo dos Afonsos 6 (<0.1%)
    Cavalcanti 6 (<0.1%)
    Gericinó 6 (<0.1%)
    Parque Colúmbia 6 (<0.1%)
    Jardim América 5 (<0.1%)
    Maré 5 (<0.1%)
    Cidade Universitária 3 (<0.1%)
1 n (%)

Observa-se que a partir do 16º bairro, a proporção de anúncios cai para menos de 1%. Por isso, eles serão cortados da visualização do gráfico abaixo:

df |>
  filter(!is.na(neighbourhood_cleansed)) |>
  count(neighbourhood_cleansed, sort = TRUE) |>
  slice_head(n = 15) |>
  ggplot(aes(
    x = reorder(neighbourhood_cleansed, n),
    y = n,
    fill = neighbourhood_cleansed
  )) +
  geom_col() +
  coord_flip() +
  theme_minimal() +
  labs(
    x = "Bairro",
    y = "Frequência",
    title = "Distribuição dos bairros",
    subtitle = "Top 15 bairros com mais anúncios"
  ) +
  theme(legend.position = "none")

Mais de 20% dos anúncios de imóveis para locação são em Copacabana. Leblon, o metro quadrado mais caro do Rio de Janeiro há alguns anos, é o 7º bairro com mais anúncios, ficando com menos de 5% deles. A maior concentração de bairros com anúncios fica na zona sul da cidade, seguida pela zona oeste e pelo Centro.

Descrição estatística da variável property_type:

df |> 
  select(property_type) |> 
  mutate(property_type = fct_infreq(property_type)) |> 
  tbl_summary() |> 
  modify_header(label ~ "**Variável**") |>
  modify_spanning_header(all_stat_cols() ~ "**Proporção**") |>
  modify_caption("**Tabela 5. Proporção de tipos de construção**") |>
  bold_labels()
Tabela 5. Proporção de tipos de construção
Variável
Proporção
N = 213,5101
property_type
    Apartment 163,788 (77%)
    House 21,495 (10%)
    Condominium 12,563 (5.9%)
    Loft 4,186 (2.0%)
    Serviced apartment 3,986 (1.9%)
    Guest suite 1,323 (0.6%)
    Bed and breakfast 861 (0.4%)
    Guesthouse 812 (0.4%)
    Villa 660 (0.3%)
    Hostel 651 (0.3%)
    Hotel 621 (0.3%)
    Other 480 (0.2%)
    Townhouse 381 (0.2%)
    Aparthotel 242 (0.1%)
    Cottage 231 (0.1%)
    Tiny house 226 (0.1%)
    Earth house 183 (<0.1%)
    Chalet 181 (<0.1%)
    Boutique hotel 162 (<0.1%)
    Boat 120 (<0.1%)
    Cabin 60 (<0.1%)
    Nature lodge 57 (<0.1%)
    Bungalow 51 (<0.1%)
    Casa particular (Cuba) 42 (<0.1%)
    Island 29 (<0.1%)
    Houseboat 15 (<0.1%)
    Castle 12 (<0.1%)
    Farm stay 12 (<0.1%)
    Tent 12 (<0.1%)
    Treehouse 12 (<0.1%)
    Campsite 11 (<0.1%)
    Dorm 10 (<0.1%)
    Hut 10 (<0.1%)
    Camper/RV 8 (<0.1%)
    Barn 6 (<0.1%)
    Yurt 6 (<0.1%)
    Vacation home 4 (<0.1%)
    Igloo 1 (<0.1%)
1 n (%)

Observa-se que a partir do 5º tipo, as categorias passam a representar menos de 1% do total. Logo, a visualização abaixo será focada no top 5:

df |>
  filter(!is.na(property_type)) |>
  count(property_type, sort = TRUE) |>
  slice_head(n = 5) |>
  ggplot(aes(
    x = reorder(property_type, n),
    y = n,
    fill = property_type
  )) +
  geom_col() +
  coord_flip() +
  theme_minimal() +
  labs(
    x = "Tipo de construção",
    y = "Frequência",
    title = "Distribuição dos tipos de construção",
    subtitle = "Top 5 tipos de construção mais anunciados"
  ) +
  theme(legend.position = "none")

Observa-se que mais de 70% dos imóveis para locação são do tipo Apartment. A segunda categoria mais comum é House, seguido por Condominium. As demais categorias (Loft e Serviced apartment) representam apenas cerca de 4% das observações, mesmo somadas.

6 Análise Bivariada e Testes de Hipóteses

A análise bivariada e os testes de hipóteses serão realizados com base nas 4 variáveis consideradas de maior potencial explicativo para a variável de interesse (preço de aluguel):

  • A variável de capacidade de hóspedes (accommodates) foi escolhida para representar características estruturais do imóvel, permitindo avaliar a relação entre tamanho/capacidade e preço, o que possui interpretação econômica direta.

  • A variável de status de superhost (host_is_superhost) foi incluída por refletir aspectos de reputação do anfitrião, possibilitando investigar se fatores qualitativos influenciam o preço praticado.

  • A variável de bairro (neighbourhood_cleansed) foi selecionada por representar a localização do imóvel, um dos principais determinantes do valor no mercado imobiliário, permitindo analisar diferenças de preço entre regiões.

  • Por fim, a variável de mês (mes) foi utilizada para capturar possíveis efeitos sazonais ao longo do tempo, avaliando variações no preço associadas a períodos específicos (algo de extrema importância para turismo).

6.1 Price (numérica) x Categóricas

6.1.1 Price vs. neighbourhood_cleansed

Para analisar o impacto da localização no preço dos imóveis, foi realizada uma comparação entre os bairros com maior número de observações.

Hipóteses:

\(H_0\): Não há diferença de preço entre bairros
\(H_1\): Há diferença de preço entre pelo menos dois bairros

  • Analisando os top 15 bairros:
df |> 
  filter(neighbourhood_cleansed %in% top_bairros) |> 
  mutate(neighbourhood_cleansed = fct_reorder(neighbourhood_cleansed, price, .fun = median)) |> 
  ggplot(aes(x = neighbourhood_cleansed, y = price, fill = neighbourhood_cleansed)) +
  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 = colorRampPalette(c("#B5D4F4", "#0C447C"))(15)) +
  scale_y_continuous(labels = dollar_format(prefix = "R$")) +
  coord_flip(ylim = c(0, 1000)) +
  labs(title = "Preço de aluguel nos 15 bairros com mais anúncios",
       subtitle = "Ordenado pela mediana | Losango vermelho = média",
       x = "Bairro",
       y = "Preço (Zoom até R$ 1000)") +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        panel.grid.minor = element_blank())

df |> 
  filter(neighbourhood_cleansed %in% top_bairros) |> 
  group_by(neighbourhood_cleansed) |> 
  summarise(
    n = n(),
    min = min(price, na.rm = TRUE),
    q1 = quantile(price, 0.25, na.rm = TRUE),
    mediana = median(price, na.rm = TRUE),
    q3 = quantile(price, 0.75, na.rm = TRUE),
    media = mean(price, na.rm = TRUE),
    max = max(price, na.rm = TRUE),
    dp = sd(price, na.rm = TRUE)
  ) |> 
  arrange(desc(mediana)) |> 
  mutate(
    across(c(min, q1, mediana, q3, media, max, dp),
           ~ dollar(., prefix = "R$"))
  ) |> 
  kbl(col.names = c("Bairro", "n", "Mínimo", "Q1", "Mediana", "Q3", "Média", "Máximo", "Desvio Padrão"),
      align = "lrrrrrrrr") |> 
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE)
Bairro n Mínimo Q1 Mediana Q3 Média Máximo Desvio Padrão
Lagoa 2416 R$49 R$238.75 R$498.50 R$1,097.50 R$1,018.11 R$31,402 R$2,025.27
Leblon 9764 R$48 R$288.00 R$451.00 R$852.00 R$864.45 R$25,002 R$1,472.80
Barra da Tijuca 23989 R$41 R$247.00 R$450.00 R$1,002.00 R$1,117.86 R$54,981 R$2,704.87
Jacarepaguá 12033 R$0 R$189.00 R$413.00 R$979.00 R$801.42 R$49,784 R$2,203.05
Ipanema 18385 R$30 R$237.00 R$399.00 R$789.00 R$790.46 R$45,697 R$1,729.51
Recreio dos Bandeirantes 10053 R$28 R$198.00 R$393.00 R$900.00 R$796.92 R$37,598 R$1,516.67
Leme 3722 R$38 R$173.25 R$298.00 R$550.75 R$604.23 R$35,002 R$1,842.64
Copacabana 55910 R$0 R$159.00 R$250.00 R$498.00 R$562.04 R$57,517 R$1,580.41
Botafogo 10476 R$37 R$130.00 R$220.00 R$449.00 R$435.19 R$35,002 R$1,276.05
Flamengo 5518 R$35 R$122.00 R$218.00 R$401.00 R$485.68 R$35,002 R$1,445.48
Laranjeiras 4278 R$37 R$121.00 R$218.00 R$478.00 R$480.65 R$54,981 R$1,862.18
Tijuca 4344 R$31 R$100.00 R$201.00 R$548.00 R$500.00 R$32,944 R$1,251.79
Santa Teresa 6920 R$35 R$101.00 R$179.00 R$327.00 R$550.04 R$55,315 R$2,328.70
Glória 2125 R$29 R$101.00 R$176.00 R$323.00 R$433.42 R$30,002 R$1,762.49
Centro 5922 R$30 R$100.00 R$151.00 R$298.00 R$425.81 R$25,000 R$1,306.30

Em primeira análise, pode-se supor que existe diferença entre os preços dos 15 bairros mais populares.

  • Teste para confirmar a diferença de valor:
df_top = df |>
  filter(neighbourhood_cleansed %in% top_bairros)

kruskal.test(price ~ neighbourhood_cleansed, data = df_top) |> 
  tidy() |> 
  gt()
statistic p.value parameter method
18016.2 0 14 Kruskal-Wallis rank sum test

A análise da relação entre o preço dos imóveis e o bairro por meio do teste de Kruskal-Wallis indicou diferença estatisticamente significativa entre os bairros (p < 0,05), levando à rejeição da hipótese nula (\(H_0\)) em favor da hipótese alternativa (\(H_1\)), ou seja, há evidências de que pelo menos dois bairros apresentam diferenças nos preços dos imóveis.

  • Teste post hoc: analisar se os pares são diferentes estatisticamente
dunn_bairros = df_clean |> 
  filter(neighbourhood_cleansed %in% top_bairros) |> 
  dunn_test(price ~ neighbourhood_cleansed, p.adjust.method = "bonferroni") |> 
  filter(p.adj < 0.05) |> 
  select(group1, group2, statistic, p.adj) |> 
  mutate(
    statistic = round(statistic, 2), 
    p.adj = round(p.adj, 4)
  ) |> 
  arrange(p.adj)
dunn_bairros |> 
  kbl(col.names = c("Bairro 1", "Bairro 2", "Estatística Z", "p-valor ajustado"), caption = "Pares de bairros com diferença significativa (p Bonferroni < 0.05)", align = "llrr") |> 
  kable_styling(bootstrap_options = c("Stripped", "hover"), full_width = TRUE)
Pares de bairros com diferença significativa (p Bonferroni < 0.05)
Bairro 1 Bairro 2 Estatística Z p-valor ajustado
Barra da Tijuca Botafogo -61.10 0.0000
Barra da Tijuca Centro -72.98 0.0000
Barra da Tijuca Copacabana -70.61 0.0000
Barra da Tijuca Flamengo -49.72 0.0000
Barra da Tijuca Glória -42.77 0.0000
Barra da Tijuca Ipanema -11.03 0.0000
Barra da Tijuca Jacarepaguá -15.52 0.0000
Barra da Tijuca Laranjeiras -41.97 0.0000
Barra da Tijuca Leme -24.26 0.0000
Barra da Tijuca Recreio dos Bandeirantes -14.97 0.0000
Barra da Tijuca Santa Teresa -70.03 0.0000
Barra da Tijuca Tijuca -44.91 0.0000
Botafogo Centro -21.12 0.0000
Botafogo Copacabana 16.02 0.0000
Botafogo Glória -10.61 0.0000
Botafogo Ipanema 49.63 0.0000
Botafogo Jacarepaguá 40.57 0.0000
Botafogo Lagoa 32.59 0.0000
Botafogo Leblon 54.03 0.0000
Botafogo Leme 15.10 0.0000
Botafogo Recreio dos Bandeirantes 38.52 0.0000
Botafogo Santa Teresa -15.49 0.0000
Centro Copacabana 37.61 0.0000
Centro Flamengo 16.92 0.0000
Centro Ipanema 63.64 0.0000
Centro Jacarepaguá 55.79 0.0000
Centro Lagoa 44.70 0.0000
Centro Laranjeiras 18.06 0.0000
Centro Leblon 66.99 0.0000
Centro Leme 30.19 0.0000
Centro Recreio dos Bandeirantes 53.79 0.0000
Centro Santa Teresa 5.84 0.0000
Centro Tijuca 15.94 0.0000
Copacabana Flamengo -13.98 0.0000
Copacabana Glória -19.14 0.0000
Copacabana Ipanema 51.39 0.0000
Copacabana Jacarepaguá 36.98 0.0000
Copacabana Lagoa 27.19 0.0000
Copacabana Laranjeiras -9.55 0.0000
Copacabana Leblon 53.75 0.0000
Copacabana Leme 6.94 0.0000
Copacabana Recreio dos Bandeirantes 33.89 0.0000
Copacabana Santa Teresa -32.21 0.0000
Copacabana Tijuca -12.42 0.0000
Flamengo Glória -8.84 0.0000
Flamengo Ipanema 41.32 0.0000
Flamengo Jacarepaguá 34.99 0.0000
Flamengo Lagoa 31.25 0.0000
Flamengo Leblon 46.72 0.0000
Flamengo Leme 14.84 0.0000
Flamengo Recreio dos Bandeirantes 33.69 0.0000
Flamengo Santa Teresa -11.81 0.0000
Glória Ipanema 37.53 0.0000
Glória Jacarepaguá 33.77 0.0000
Glória Lagoa 33.22 0.0000
Glória Laranjeiras 10.23 0.0000
Glória Leblon 42.30 0.0000
Glória Leme 19.88 0.0000
Glória Recreio dos Bandeirantes 33.09 0.0000
Glória Tijuca 8.59 0.0000
Ipanema Jacarepaguá -5.57 0.0000
Ipanema Lagoa 5.92 0.0000
Ipanema Laranjeiras -34.66 0.0000
Ipanema Leblon 12.19 0.0000
Ipanema Leme -17.77 0.0000
Ipanema Recreio dos Bandeirantes -5.62 0.0000
Ipanema Santa Teresa -60.09 0.0000
Ipanema Tijuca -37.49 0.0000
Jacarepaguá Lagoa 8.68 0.0000
Jacarepaguá Laranjeiras -29.39 0.0000
Jacarepaguá Leblon 16.00 0.0000
Jacarepaguá Leme -13.55 0.0000
Jacarepaguá Santa Teresa -51.84 0.0000
Jacarepaguá Tijuca -32.04 0.0000
Lagoa Laranjeiras -28.16 0.0000
Lagoa Leme -17.13 0.0000
Lagoa Recreio dos Bandeirantes -8.73 0.0000
Lagoa Santa Teresa -41.28 0.0000
Lagoa Tijuca -29.97 0.0000
Laranjeiras Leblon 40.41 0.0000
Laranjeiras Leme 12.00 0.0000
Laranjeiras Recreio dos Bandeirantes 28.41 0.0000
Laranjeiras Santa Teresa -13.32 0.0000
Leblon Leme -24.50 0.0000
Leblon Recreio dos Bandeirantes -15.65 0.0000
Leblon Santa Teresa -63.64 0.0000
Leblon Tijuca -43.05 0.0000
Leme Recreio dos Bandeirantes 13.01 0.0000
Leme Santa Teresa -25.98 0.0000
Leme Tijuca -14.02 0.0000
Recreio dos Bandeirantes Santa Teresa -49.79 0.0000
Recreio dos Bandeirantes Tijuca -30.99 0.0000
Santa Teresa Tijuca 11.10 0.0000
Barra da Tijuca Leblon 3.71 0.0221
Centro Glória 3.60 0.0338

O teste de Dunn com ajuste de Bonferroni, para identificar quais pares de bairros diferem entre si, mostra diversas comparações com diferenças estatisticamente significativas, indicando que a variação de preços entre bairros é consistente e não se limita a casos isolados.

  • Tamanho do efeito: eta quadrado (primo do r)

    • 0,01-<0.06 (pequeno),

    • 0,06-<0,14 (moderado) e

    • 0,14 (grande)

kruskal_effsize(price ~ neighbourhood_cleansed, data = df_top) |> 
  gt()
.y. n effsize method magnitude
price 175855 0.1023783 eta2[H] moderate

Adicionalmente, o tamanho de efeito estimado (0,10) foi classificado como moderado, sugerindo que, embora a localização tenha impacto relevante sobre os preços, ela não é o único fator determinante. Assim, conclui-se que o bairro exerce influência significativa sobre o preço dos imóveis, mas outros fatores também contribuem para sua variação.

df |>
  filter(neighbourhood_cleansed %in% top_bairros) |>
  select(neighbourhood_cleansed, price) |> 
  tbl_summary(by = neighbourhood_cleansed) |> 
  add_p()
Characteristic Barra da Tijuca
N = 23,9891
Botafogo
N = 10,4761
Centro
N = 5,9221
Copacabana
N = 55,9101
Flamengo
N = 5,5181
Glória
N = 2,1251
Ipanema
N = 18,3851
Jacarepaguá
N = 12,0331
Lagoa
N = 2,4161
Laranjeiras
N = 4,2781
Leblon
N = 9,7641
Leme
N = 3,7221
Recreio dos Bandeirantes
N = 10,0531
Santa Teresa
N = 6,9201
Tijuca
N = 4,3441
p-value2
price 450 (247, 1,002) 220 (130, 449) 151 (100, 298) 250 (159, 498) 218 (122, 401) 176 (101, 323) 399 (237, 789) 413 (189, 979) 499 (239, 1,098) 218 (121, 478) 451 (288, 852) 298 (173, 551) 393 (198, 900) 179 (101, 327) 201 (100, 548) <0.001
1 Median (Q1, Q3)
2 Kruskal-Wallis rank sum test

6.1.2 Price vs. mes

Para investigar possíveis efeitos sazonais no preço dos imóveis, foi realizada uma análise bivariada entre o preço e o mês de referência.

Hipóteses:

\(H_0\): Não há diferença de preço entre os meses
\(H_1\): Há diferença de preço entre pelo menos dois meses

  • Transformando a variável em categórica para que possamos realizar as análises necessárias:
df = df |>
  mutate(mes_cat = factor(format(mes, "%Y-%m"), levels = sort(unique(format(mes, "%Y-%m")))))
  • Analisando os meses presentes na base:
df |> 
  ggplot(aes(x = mes_cat, y = price, fill = mes_cat)) +
  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 = colorRampPalette(c("#B5D4F4", "#0C447C"))(6)) +
  scale_y_continuous(labels = dollar_format(prefix = "R$")) +
  coord_flip(ylim = c(0, 1000)) +
  labs(title = "Preço de aluguel ao longo dos meses",
       subtitle = "Ordenado pela mediana | Losango vermelho = média",
       x = "Mês",
       y = "Preço") +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        panel.grid.minor = element_blank())

df |> 
  group_by(mes_cat) |> 
  summarise(
    n = n(),
    min = min(price, na.rm = TRUE),
    q1 = quantile(price, 0.25, na.rm = TRUE),
    mediana = median(price, na.rm = TRUE),
    q3 = quantile(price, 0.75, na.rm = TRUE),
    media = mean(price, na.rm = TRUE),
    max = max(price, na.rm = TRUE),
    dp = sd(price, na.rm = TRUE)
  ) |> 
  arrange(desc(mediana)) |> 
  mutate(
    across(c(min, q1, mediana, q3, media, max, dp),
           ~ dollar(., prefix = "R$"))
  ) |> 
  kbl(col.names = c("Mês", "n", "Mínimo", "Q1", "Mediana", "Q3", "Média", "Máximo", "Desvio Padrão"),
      align = "lrrrrrrrr") |> 
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE)
Mês n Mínimo Q1 Mediana Q3 Média Máximo Desvio Padrão
2019-12 34551 R$0 R$155 R$299 R$601 R$655.39 R$42,940 R$1,703.17
2020-03 36085 R$0 R$157 R$299 R$599 R$683.30 R$52,763 R$1,922.40
2020-05 35791 R$0 R$159 R$299 R$649 R$787.94 R$57,517 R$2,227.33
2020-01 34754 R$0 R$155 R$298 R$600 R$645.69 R$41,966 R$1,674.25
2020-02 36460 R$0 R$158 R$298 R$632 R$686.07 R$46,085 R$1,854.19
2020-04 35869 R$0 R$158 R$298 R$632 R$750.03 R$55,296 R$2,119.42

Em primeira análise, pode-se afirmar que as medianas permanecem constantes ao longo do período analisado, com pequenas diferenças na dispersão. No entanto, as médias apresentam variações, sugerindo a presença de valores extremos em determinados períodos. Isso indica que, apesar do preço típico dos imóveis se manter estável, alguns meses concentram imóveis com preços mais elevados, aumentando a média.

  • Teste para confirmar a diferença de valor:
kruskal.test(price ~ mes_cat, data = df) |> 
  tidy() |> 
  gt()
statistic p.value parameter method
21.74215 0.0005861063 5 Kruskal-Wallis rank sum test

A análise da relação entre o preço dos imóveis e o bairro por meio do teste de Kruskal-Wallis indicou diferença estatisticamente significativa entre os bairros (p < 0,05). Não é possível afirmar qual hipótese é a vencedora ainda, uma vez que o resultado do teste contraria o observado em primeira análise.

  • Teste post hoc: analisar se os pares são diferentes estatisticamente (mostrando somente os pares que são)
dunn_mes = df |>
 dunn_test(price ~ mes_cat, p.adjust.method = "bonferroni") |>
  filter(p.adj < 0.05) |>
  select(group1, group2, statistic, p.adj) |>
  mutate(
    statistic = round(statistic, 2),
    p.adj = round(p.adj, 4)
  ) |>
  arrange(p.adj)
dunn_mes |> 
  kbl(col.names = c("Mês 1", "Mês 2", "Estatística Z", "p-valor ajustado"), caption = "Pares de bairros com diferença significativa (p Bonferroni < 0.05)", align = "llrr") |> 
  kable_styling(bootstrap_options = c("Stripped", "hover"), full_width = TRUE)
Pares de bairros com diferença significativa (p Bonferroni < 0.05)
Mês 1 Mês 2 Estatística Z p-valor ajustado
2020-01 2020-05 3.81 0.0021
2019-12 2020-05 3.15 0.0245
2020-01 2020-04 3.14 0.0257

O teste de Dunn com ajuste de Bonferroni identificou diferenças estatisticamente significativas entre alguns pares de meses. Em particular, observou-se que janeiro apresenta preços significativamente mais elevados em comparação a maio e abril, enquanto dezembro também apresenta preços superiores aos de maio. Esses resultados indicam que as diferenças ao longo do tempo são pontuais e concentradas em determinados meses, não caracterizando um padrão sazonal uniforme.

  • Tamanho do efeito: eta quadrado (primo do r)

    • 0,01-<0.06 (pequeno),

    • 0,06-<0,14 (moderado) e

    • 0,14 (grande)

kruskal_effsize(price ~ mes_cat, data = df) |>
  gt()
.y. n effsize method magnitude
price 213510 7.84161e-05 eta2[H] small

Embora o teste de Kruskal-Wallis tenha indicado diferença estatisticamente significativa entre os meses (p < 0,05), o tamanho de efeito estimado foi extremamente baixo (0,00008), sendo classificado como pequeno. Isso indica ausência de impacto relevante do período sobre os preços no período analisado.

Ressalta-se, contudo, que a análise foi realizada com base em um subconjunto temporal limitado (6 meses), o que pode restringir a identificação de padrões sazonais mais amplos.

df |>
  mutate(mes_cat = factor(format(mes, "%Y-%m"), levels = sort(unique(format(mes, "%Y-%m")))))|>
  select(mes_cat, price) |> 
  tbl_summary(by = mes_cat) |> 
  add_p()
Characteristic 2019-12
N = 34,5511
2020-01
N = 34,7541
2020-02
N = 36,4601
2020-03
N = 36,0851
2020-04
N = 35,8691
2020-05
N = 35,7911
p-value2
price 299 (155, 601) 298 (155, 600) 298 (158, 632) 299 (157, 599) 298 (158, 632) 299 (159, 649) <0.001
1 Median (Q1, Q3)
2 Kruskal-Wallis rank sum test

6.1.3 Price vs. host_is_superhost

Para analisar se anfitriões superhosts possuem preços diferentes dos demais, foi realizada uma comparação entre os dois grupos.

Hipóteses:

\(H_0\): Não há diferença de preço entre hosts e superhosts
\(H_1\): Há diferença de preço entre hosts e superhosts

  • Analisando os dados:
df |> 
  ggplot(aes(y = host_is_superhost, x = price, fill = host_is_superhost)) +
  geom_violin(alpha = 0.5, color = NA, show.legend = FALSE) +
  geom_boxplot(width = 0.12, outlier.shape = NA, fill = "white", alpha = 0.8, show.legend = FALSE) +
  stat_summary(fun = mean, geom = "point", shape = 18, size = 3.5, color = "#C04824", show.legend = FALSE) +
  scale_x_continuous(labels = scales::dollar_format(prefix = "R$")) +
  coord_cartesian(xlim = c(0, 1000)) +
  labs(
    x = "Preço",
    y = "Tipo de anfitrião",
    title = "Distribuição dos preços por tipo de anfitrião",
    subtitle = "Zoom até R$1000 | Losango vermelho = média"
  ) +
  theme_minimal()

df |> 
  group_by(host_is_superhost) |> 
  summarise(
    n = n(),
    min = min(price, na.rm = TRUE),
    mediana = median(price, na.rm = TRUE),
    q1 = quantile(price, 0.25),
    q3 = quantile(price, 0.75),
    media = mean(price, na.rm = TRUE),
    max = max(price, na.rm = TRUE),
    dp = sd(price)
  ) |> 
  arrange(desc(mediana)) |> 
  mutate(
    across(c(min, max, mediana, media, dp, q1, q3),
           ~ dollar(., prefix = "R$"))
    ) |> 
    kbl(col.names = c("Anfitrião", "n", "Mínimo", "Mediana", "Q1", "Q3", "Média", "Máximo", "Desvio Padrão"),
        align = "lrrrrrrrr") |> 
  kable_styling(bootstrap_options = c("stripped", "hover"), full_width = TRUE)
Anfitrião n Mínimo Mediana Q1 Q3 Média Máximo Desvio Padrão
Host 186373 R$0 R$299 R$166 R$684 R$726.15 R$57,517 R$1,951.05
Superhost 27137 R$29 R$202 R$131 R$361 R$535.28 R$46,085 R$1,772.52

Em primeira análise, pode-se supor que os superhosts cobram menos (em média) do que os hosts comuns. A mediana de preço de hosts comuns (R$ 299, IQR = R$ 166-684) é cerca de 32% maior do que de superhosts (R$ 202, IQR = R$ 131-361). Também observa-se uma maior variabilidade de preço entre os hosts comuns.

  • Teste de normalidade da variável:
shapiro.test(sample(df$price, min(nrow(df), 5000))) |> 
  tidy() |> 
  gt()
statistic p.value method
0.2648087 1.047293e-88 Shapiro-Wilk normality test

OBS: Foi necessário fazer uma amostragem da base de dados, uma vez que o teste Shapiro-Wilk suporta no máximo 5000 observações.

Confirma-se que a variável price (preço) não é normal, como observado na análise univariada.

  • Tabela por grupo:
df |> 
  group_by(host_is_superhost) |> 
  summarise(
    n = n(),
    mediana = scales::dollar(median(price, na.rm = TRUE)),
    media = scales::dollar(mean(price, na.rm = TRUE)),
    dp = scales::dollar(sd(price, na.rm = TRUE)),
    q1 = scales::dollar(quantile(price, 0.25, na.rm = TRUE)),
    p_shapiro = shapiro.test(sample(price, min(n(), 4999)))$p.value
  ) |> 
  kbl(
    col.names = c("Grupo", "N", "Mediana", "Média", "DP", "Q1", "p-valor (Shapiro)"),
    align = "lrrrrrr"
  ) |> 
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE)
Grupo N Mediana Média DP Q1 p-valor (Shapiro)
Host 186373 $299 $726.15 $1,951.05 $166 0
Superhost 27137 $202 $535.28 $1,772.52 $131 0
  • Testando as hipóteses:
wilcox.test(
  price ~ host_is_superhost,
  data = df
) |> 
  tidy() |> 
  gt()
statistic p.value method alternative
3103251280 0 Wilcoxon rank sum test with continuity correction two.sided

O teste de Wilcoxon indicou diferença estatisticamente significativa entre os preços de imóveis de superhosts e hosts comuns (p < 0,001), levando à suspeita de possível rejeição da hipótese nula.

Esse resultado sugere que o status de superhost está associado a variações nos preços dos imóveis.

  • Testando o tamanho do efeito:
rank_biserial(price ~ host_is_superhost, data = df) |> 
  tidy()
# A tibble: 4 × 13
  column     n  mean    sd median trimmed   mad   min   max range  skew kurtosis
  <chr>  <dbl> <dbl> <dbl>  <dbl>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>    <dbl>
1 r_ran…     1 0.227    NA  0.227   0.227     0 0.227 0.227     0   NaN      NaN
2 CI         1 0.95     NA  0.95    0.95      0 0.95  0.95      0   NaN      NaN
3 CI_low     1 0.220    NA  0.220   0.220     0 0.220 0.220     0   NaN      NaN
4 CI_hi…     1 0.234    NA  0.234   0.234     0 0.234 0.234     0   NaN      NaN
# ℹ 1 more variable: se <dbl>
levels(as.factor(df$host_is_superhost))
[1] "Host"      "Superhost"

O coeficiente rank biserial (r = 0,23) indicou um efeito de magnitude pequena a moderada, sugerindo que imóveis de hosts comuns tendem a apresentar preços mais elevados do que aqueles de superhosts. Apesar da significância estatística observada, a magnitude do efeito indica que essa diferença, embora consistente, não é substancial em termos práticos. Logo, a hipótese nula não foi rejeitada.

6.2 Price (numérica) x Numérica

6.2.1 Price vs. accommodates

Para analisar se imóveis com maior capacidade de hóspedes apresentam preços mais elevados, foi avaliada a relação entre o número de pessoas acomodadas e o preço.

Hipóteses:

\(H_0\): Não há correlação entre o número de hóspedes acomodados e o preço (r = 0)
\(H_1\): Há correlação entre o número de hóspedes acomodados e o preço (r > 0)

  • Analisando os dados:
p1 = df |>
  ggplot(aes(x = accommodates, y = price)) +
  geom_point(alpha = 0.25, color = "#185FA5", size = 1.2) +
  geom_smooth(method = "lm", se = FALSE, color = "#C04824", linewidth = 1, linetype = "solid") +
  scale_x_continuous(labels = comma) +
  scale_y_continuous(labels = scales::dollar_format(prefix = "R$")) +
  labs(title = "Capacidade de hóspedes x Preço",
       subtitle = "Vermelho = linear",
       x = "Número de hóspedes (accommodates)",
       y = "Preço") +
  theme_minimal() +
  theme(plot.title = element_text(face = "bold"))

p1

cor(df$accommodates, df$price, method = "pearson")
[1] 0.2464007
cor(df$accommodates, df$price, method = "spearman")
[1] 0.5191272

Segundo o coeficiente de Spearman, há uma correlação moderada (r = 0,51) entre o preço de aluguel e a quantidade de pessoas que o imóvel acomoda. Como observado pelo gráfico, trata-se de uma relação monotônica, não-linear. Por isso, optou-se pelo uso do coeficiente de Spearman para descrever sua força.

7 Conclusão

A análise exploratória da variável preço (price) evidenciou uma distribuição fortemente assimétrica à direita, com presença de outliers expressivos, indicando que poucos imóveis com valores muito elevados impactam significativamente a distribuição. Esse comportamento reforça a adequação do uso de medidas robustas, como mediana e intervalo interquartil, ao longo da análise.

Na análise bivariada, observou-se que o número de hóspedes que o imóvel comporta (accommodates) apresenta associação positiva com o preço, indicando que imóveis com maior capacidade tendem a ser mais caros. No entanto, o scatterplot evidenciou que essa relação não é estritamente linear, sugerindo que o aumento do preço não ocorre de forma constante conforme a capacidade cresce.

Além disso, a análise por bairros revelou diferenças relevantes nos preços dos imóveis, indicando variações espaciais importantes dentro da cidade do Rio de Janeiro. Essas diferenças refletem a heterogeneidade do mercado imobiliário local, em que determinadas regiões concentram imóveis com valores mais elevados.

De forma geral, os resultados mostram que o preço dos imóveis no Airbnb é influenciado principalmente pela capacidade de acomodação e pela localização, evidenciando que tanto características quantitativas quanto fatores espaciais desempenham papel central na definição dos valores.

8 App Shiny

  • Salvando a base de dados final:
df_modelo = df |> 
  select(price, neighbourhood_cleansed, mes_cat, host_is_superhost, accommodates)

saveRDS(df_modelo, "df_modelo.rds")

Link para o app: https://9ttrpl-anabella-sgarbi0pereira.shinyapps.io/PD_EDA/

9 RPubs

Link para este projeto no RPubs: https://rpubs.com/ana_sgarbi/pd_analise_exploratoria_de_dados