TP Final Estadistica aplicada a investigación de mercado
Author
Betsy Cohen
Objetivo
Según datos abiertos oficiales, la C.A.B.A. posee 665 alojamientos turísticos registrados de los cuales el 40% corresponden a modelo no tradicionales como hoteles boutique, hostels, aparts y B&B.
Ver código
df_alojamientos_badata %>%mutate("Tipo de alojamiento"=case_when(str_detect(tipo,"estrella")|tipo %in%c("Hotel sindical","Hospedaje") ~"Hotel tradicional", tipo %in%c("Boutique","Hotel boutique") ~"Hotel boutique",TRUE~ tipo))%>%tabyl("Tipo de alojamiento") %>%arrange(desc(n)) %>%adorn_totals() %>%adorn_pct_formatting(digits =0) %>%as.data.frame() %>%gt() %>%tab_header(title =md('**Tipos de alojamiento CABA**')) %>%opt_align_table_header('left') %>%tab_source_note(source_note ="Fuente: BA DATA")
Tipos de alojamiento CABA
Tipo de alojamiento
n
percent
Hotel tradicional
401
60%
Hotel boutique
85
13%
Hostel
80
12%
Apart Hotel
73
11%
Bed & Breakfast
26
4%
Total
665
100%
Fuente: BA DATA
“Casa Angelito” era un antiguo hotel de pasajeros que Don Angel administró durante los años 50, alquilando habitaciones a marineros, mozos y pasajeros. Tras su muerte sus nietos están poniendo en valor la propiedad para rentar las habitaciones de manera temporal por Airbnb.
Ubicado en el epicentro de San Telmo, el espacio espera abrir sus puertas al público en diciembre de 2024.
Preocupados por comenzar a recuperar la inversión en la reforma de la antigua casa, los nietos de Don Angel quieren conocer las principales características de la oferta y conocer, para su segmento en particular, cuál es el precio al cual deberían lanzarse al mercado para optimizar la ocupación.
Metodología
Para dar respuesta a esta pregunta utilizamos algunas de las técnicas aprendidas en la materia “Estadística aplicada a investigación de mercado”. Los pasos a seguir serán
Realizar limpieza y categorización de variables
Observar la correlación entre las variables utilizando v de cramer y ordenar una primera selección de las mismas en conjuntos de correlaciones por temática.
Utilizar análisis correspondencias múltiples (MCA) para comprender cómo se relacionan las categorías de estas variables y volver seleccionar las que más aporten a cada uno de los modelos.
Realizar un análisis de conglomerados utilizando utilizando el algoritmo k-modas.
Analizar los clusters obtenidos sobre todo en relación al precio y la ocupación utilizando una regresión lineal para estas variables en cada cluster.
Acerca de la base de datos
El sitio http://insideairbnb.com/. es un proyecto que recopila datos de la plataforma colaborativa de alquileres, con el propósito de realizar un seguimiento del impacto de este tipo de alquiler sobre la vivienda en las principales capitales turísticas del mundo. Si bien existen otras plataformas de alquiler, Airbnb ha logrado posicionarse como una de las principales y es por ello que creemos que es un buen punto de partida.
Insideairbnb provee un dataset con publicaciones, comentarios de publicaciones y calendario de anuncios a diciembre de 2022 y se publican en forma de archivos csv:
listing_details.csv - Datos detallados de los anuncios calendar_details.csv - Datos detallados del calendario para anuncios review_details.csv - Datos de los comentarios detallados para los anuncions review_summary.csv - Resumen de datos de comentaris e ID de anuncios neighbourhoods.csv: lista de barrios para el filtro geográfico. barrios.geojson - Archivo GeoJSON de los barrios
Nosotros vamos a trabajar con publicaciones que tuvieron al menos una publicación en el último año y con una selección inicial de variables de listing_details.csv :
Ver código
selected_cols <-c("id","calculated_host_listings_count","host_response_time", "host_is_superhost", "host_identity_verified", "neighbourhood_cleansed", "property_type","room_type", "accommodates","bedrooms", "beds", "amenities", "price_US", "bathrooms_text","review_scores_rating", "review_scores_accuracy", "review_scores_cleanliness", "review_scores_checkin", "review_scores_communication", "review_scores_location", "review_scores_value","instant_bookable", "reviews_per_month")df_airbnb <- df_airbnb %>%filter(number_of_reviews_ltm >0) %>%# hacemos una transformacion en precio para que aparezca como numérica y en dolaresmutate(price_US =as.numeric(gsub("[$,]", "",price))/350) %>% dplyr::select(selected_cols)
Tenemos 13848 publicaciones y 25 variables de las cuales 10 son factoriales y 15 numéricas. Entre las variables numéricas ya vemos que tenemos distribuciónes con casos extremos en casi todas las variables y que ninguna parece tener una distribuición normal. Se excluye las viviendas de lujo por lo que situamos el precio por debajo de los 100 dolares la noche
Ver código
df_airbnb <- df_airbnb %>%filter(price_US <=80) # precio hasta 80 dolaresskimr::skim(df_airbnb)
Data summary
Name
df_airbnb
Number of rows
13275
Number of columns
23
_______________________
Column type frequency:
character
9
numeric
14
________________________
Group variables
None
Variable type: character
skim_variable
n_missing
complete_rate
min
max
empty
n_unique
whitespace
host_response_time
0
1
3
18
0
5
0
host_is_superhost
0
1
1
1
0
2
0
host_identity_verified
0
1
1
1
0
2
0
neighbourhood_cleansed
0
1
4
17
0
45
0
property_type
0
1
7
34
0
54
0
room_type
0
1
10
15
0
4
0
amenities
0
1
25
1924
0
12896
0
bathrooms_text
0
1
0
17
8
35
0
instant_bookable
0
1
1
1
0
2
0
Variable type: numeric
skim_variable
n_missing
complete_rate
mean
sd
p0
p25
p50
p75
p100
hist
id
0
1.00
2.706916e+17
3.341409e+17
11508.00
29794879.00
48295211.00
6.60034e+17
7.878161e+17
▇▁▁▂▃
calculated_host_listings_count
0
1.00
1.322000e+01
2.561000e+01
1.00
1.00
2.00
1.10000e+01
1.350000e+02
▇▁▁▁▁
accommodates
0
1.00
2.780000e+00
1.260000e+00
1.00
2.00
2.00
4.00000e+00
1.600000e+01
▇▁▁▁▁
bedrooms
1999
0.85
1.280000e+00
7.800000e-01
1.00
1.00
1.00
1.00000e+00
1.400000e+01
▇▁▁▁▁
beds
142
0.99
1.830000e+00
1.260000e+00
1.00
1.00
1.00
2.00000e+00
1.600000e+01
▇▁▁▁▁
price_US
0
1.00
2.598000e+01
1.392000e+01
1.51
16.47
22.65
3.22200e+01
7.999000e+01
▅▇▂▁▁
review_scores_rating
0
1.00
4.770000e+00
3.500000e-01
1.00
4.69
4.86
5.00000e+00
5.000000e+00
▁▁▁▁▇
review_scores_accuracy
1
1.00
4.820000e+00
3.300000e-01
1.00
4.76
4.91
5.00000e+00
5.000000e+00
▁▁▁▁▇
review_scores_cleanliness
1
1.00
4.720000e+00
3.900000e-01
1.00
4.63
4.82
5.00000e+00
5.000000e+00
▁▁▁▁▇
review_scores_checkin
1
1.00
4.860000e+00
3.000000e-01
1.00
4.84
4.96
5.00000e+00
5.000000e+00
▁▁▁▁▇
review_scores_communication
1
1.00
4.860000e+00
3.100000e-01
1.00
4.83
4.96
5.00000e+00
5.000000e+00
▁▁▁▁▇
review_scores_location
1
1.00
4.880000e+00
2.600000e-01
1.00
4.86
4.95
5.00000e+00
5.000000e+00
▁▁▁▁▇
review_scores_value
0
1.00
4.710000e+00
3.800000e-01
1.00
4.63
4.80
4.94000e+00
5.000000e+00
▁▁▁▁▇
reviews_per_month
0
1.00
1.510000e+00
1.360000e+00
0.02
0.51
1.08
2.12000e+00
1.217000e+01
▇▂▁▁▁
Tranformación y exploración
Realizamos algunas transformaciones en las variables
host_response_time
Se llevan los NA a la categoría de menor nivel “a few days or more”
Ver código
# df_airbnb %>%# tabyl(host_response_time)df_airbnb <- df_airbnb %>%mutate(host_response_time =ifelse(host_response_time =="N/A","a few days or more",host_response_time))
neighbourhood_cleansed
Armamos una categorizacion para los barrios en función de las posibles funciones turísticas que podrían tener: Palermo, Puerto Madero y San Telmo como típicos barrios turísticos, Otros barrios del corredor norte (Belgrano, Recoleta, Nuñez y Colegiales), los barrios del Microcentro de la ciudad (San Nicolas y Retiro) y un grupo de Otros.
Ver código
df_airbnb <- df_airbnb %>%mutate(neighbourhood_cleansed =case_when(neighbourhood_cleansed =="San Telmo"~"San Telmo", neighbourhood_cleansed =="Puerto Madero"~"Puerto Madero", neighbourhood_cleansed =="Palermo"~"Palermo", neighbourhood_cleansed %in%c("Belgrano","Recoleta","Nuñez","Colegiales") ~"Other north", neighbourhood_cleansed %in%c("San Nicolas","Retiro") ~"Other downtown",TRUE~"Other not north"))plot_ly(df_airbnb, x=~neighbourhood_cleansed, color =~neighbourhood_cleansed, type ="histogram") %>%layout(title ="<b>Distribución de neighbourhood_cleansed<b>")
amenities
amenities es una variable que está como una lista de texto entre brakets. se tokeniza y se busca los tokens más frecuentes. Con unas regex se unen auqellos tokens que parecen tener sentido como definitorios de la ocupacion en tanto brinden confort.
El acceso a baño privado o no suele ser un factor importante en la búsqueda de una estadía. La cantidad la descartamos para dicotomizar las 6 categorías resultantes si es privado o no Los NA los imputamos como privados o compartidos en función del room_type.
Ver código
# PASO 1: separar el tipo y cantidad de bañosdf_airbnb <- df_airbnb %>%mutate(bathrooms_text =str_replace(bathrooms_text, "Shared half-bath", "1 Shared_half-bath")) %>%mutate(bathrooms_text =str_replace(bathrooms_text, "Private half-bath", "1 Private_half-bath")) %>%mutate(bathrooms_text =str_replace(bathrooms_text,"Half-bath","1 Private_half-bath")) %>%#se asume privateseparate(bathrooms_text, into =c("num_baths", "bath_type"), sep =" ", fill ="right", remove = F, convert = T) # PASO 2 Dicotomización de bath a privado(Private) o no privado (Shared) asumiendo que los que tienen más de un baño "baths" también son privados.df_airbnb <- df_airbnb %>%mutate(bath_type =case_when(bath_type %in%c("bath","baths","private","private","Private_half-bath","Private") ~"Private bath", bath_type %in%c("shared","Shared_half-bath","Shared") ~"Shared bath")) # Imputación de NA segun el tipo de habitacion df_airbnb <- df_airbnb %>%mutate(bath_type =case_when(is.na(bath_type) & room_type =="Entire home/apt"~"Private bath",is.na(bath_type) & room_type =="Hotel room"~"Private bath",is.na(bath_type) & room_type =="Private room"~"Shared bath",is.na(bath_type) & room_type =="Shared room"~"Shared bath",TRUE~ bath_type ))# df_airbnb %>%# tabyl(bath_type)# Eliminar bathrooms_textdf_airbnb$bathrooms_text<-NULLdf_airbnb$num_baths<-NULLplot_ly(df_airbnb, x=~bath_type, color =~bath_type, type ="histogram") %>%layout(title ="<b>Distribución de bath_type<b>")
propety_type
Dada la escaza caracterización que brinda esta variable la descartamos del análisis
Ver código
# df_airbnb <- df_airbnb %>%# mutate(property_type = case_when(# property_type %in% c("Entire condo", "Private room in condo", "Shared room in condo") ~ "Condo",# property_type %in% c("Room in bed and breakfast", "Private room in bed and breakfast", "Shared room in bed and breakfast") ~ "B&B",# property_type %in% c("Private room in hostel", "Shared room in hostel") ~ "Hostel",# property_type %in% c("Room in boutique hotel", "Private room in boutique hotel", "Shared room in boutique hotel") ~ "Bout Hot",# property_type %in% c("Room in hotel", "Entire serviced apartment", "Private room in serviced apartment", "Shared room in serviced apartment", "Room in aparthotel") ~ "Hotel",# property_type %in% c("Entire guesthouse", "Private room in guesthouse", "Shared room in guesthouse") ~ "Guesthouse",# property_type %in% c("Entire home/apt", "Private room in home", "Shared room in home") ~ "Home",# property_type %in% c("Entire townhouse", "Private room in townhouse") ~ "Townhouse",# property_type %in% c("Entire villa", "Private room in villa", "Shared room in villa") ~ "Villa",# TRUE ~ "Others"))plot_ly(df_airbnb, x=~property_type, color =~property_type, type ="histogram") %>%layout(title ="<b>Distribución de property_type<b>")
Warning in RColorBrewer::brewer.pal(N, "Set2"): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(N, "Set2"): n too large, allowed maximum for palette Set2 is 8
Returning the palette you asked for with that many colors
Ver código
df_airbnb$property_type<-NULL
review_scores_vars
Cemos todas las variables de review_scores tienden a concentrarse cerac de los 5 puntos por lo que armamos categorías Bajo, Medio y Alto (1 a 3, 4 y 5)
Warning: Using an external vector in selections was deprecated in tidyselect 1.1.0.
ℹ Please use `all_of()` or `any_of()` instead.
# Was:
data %>% select(vars_review)
# Now:
data %>% select(all_of(vars_review))
See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
Ver código
df_airbnb <- df_airbnb %>%mutate(across(vars_review, ~case_when(. <=3~"Low", . <=4~"Medium", . <=5~"Hight",TRUE~"Medium"))) # como los NA solo son un caso les pongo la categoria Medium rm(vars_review)
accommodates
La mayor parte de las habitaciones del hotel serán dobles pero habrá una de ellas que tendrá tres camas cuchetas de modo que armamos variables categóricas de acuerdo a este criterio contemplando una posible competencia con espacios donde pueda haber viajeros solitarios en parejas en grupos de hasta 3 o más pasajeros.
Ver código
df_airbnb <- df_airbnb %>%mutate( accommodates_cont = accommodates,accommodates =case_when(accommodates ==1~"1 pax", accommodates ==2~"2 pax", accommodates ==3~"3 pax",TRUE~"4 and more pax")) # plot_ly(df_airbnb, x= ~accommodates, color = ~accommodates, type = "histogram") %>%# layout(title = "<b>Distribución de accommodates<b>")
bedrooms and beds
Ver código
# analizo si se pueden recuperar los NA 1999 por room_type y beds # # df_airbnb %>% # # filter(is.na(bedrooms)) %>% # # tabyl(room_type)# # df_airbnb %>%# filter(is.na(bedrooms)) %>%# tabyl(room_type)# # df_airbnb %>%# filter(is.na(bedrooms)) %>%# tabyl(beds)df_airbnb <- df_airbnb %>%mutate(bedrooms =case_when(bedrooms ==1~"1 bedroom", bedrooms ==2~"2 bedrooms",is.na(bedrooms) & beds ==1~"1 bedroom",is.na(bedrooms) & beds %in%c(2,3) ~"2 bedrooms",TRUE~"3+ bedrooms")) plot_ly(df_airbnb, x=~bedrooms, color =~bedrooms, type ="histogram") %>%layout(title ="<b>Distribución de bedrooms<b>")
df_airbnb <- df_airbnb %>%mutate(beds =case_when(beds ==1~"1 bed", beds ==2~"2 beds", beds ==3~"3 beds",TRUE~"+4 beds"))plot_ly(df_airbnb, x=~beds, color =~beds, type ="histogram") %>%layout(title ="<b>Distribución de beds<b>")
price_US
Nuestro cliente ofrece sus habitaciones en un hotel no tradicional o petit hotel por lo que observando las distribuciones de precio por tipo de hotel armamos una serie de categorías de precio posibles.
Ver código
df_airbnb %>%plot_ly(x=~room_type, y =~price_US,type ="box", name =~room_type, color =~room_type) %>%layout(title ="<b>Precios de las publicaciones en US$ según tipo<b>",yaxis =list(title ="Precio en US$ por noche"))
La tasa de ocupación es el Número de publicaciones que el anfitrion tiene ocupadas / número total de publicaciones de ese host x 100. Este no es un dato que proporcione la base por lo que debemos estimarlo. Inside Airbnb sugiere el 55% de los viajeros deja un comentario, por lo que utilizamos la variable reviews_per_monthy la variable calculated_host_listings_count para estimarlo
Si vemos la categoría Private rooom (que es la más cercana a la oferta de nuestro cliente) tiene una media de 33.5%, valor que se encuentra bastante cercano al 36.1% y un poco más alejado del 26.4% publicado por el Sistema de Información Turística de la Argentina (SINTA) en su Encuesta de Ocupación Hotelera de diciembre 2022 (fecha correspondiente al scrapeo de la base) para los hoteles boutique y el sector para-hotelero correspondientemente.
Tomando como referencia la citada encuesta observamos la evolución y armamos hacemos cortes de deciles para las categorías “3 estrellas/boutiques/aparts”, “Para Hoteles”
Ver código
eoh <-read.csv("input/tasas-de-ocupacion-plazas-por-categoria.csv")eoh %>%mutate(indice_tiempo =ymd(as.Date(indice_tiempo))) %>%filter(categoria_del_hotel %in%c("3 estrellas/boutiques/aparts", "Para Hoteles") ) %>%mutate(tasa_de_ocupacion_plazas = tasa_de_ocupacion_plazas*100) %>%plot_ly(x =~indice_tiempo, y =~tasa_de_ocupacion_plazas, color =~categoria_del_hotel, type ='scatter', mode ='lines') %>%layout(title ="<b>Tasa de ocupacion hotelera<b>", xaxis =list(title ="", type ="date"),yaxis =list(title =""),showlegend = T, legend =list(orientation ='h')) %>%add_annotations(x=0., y=-0.1, xref ="paper",yref ="paper",text ="Elaboración propia en base a datos abiertos SINTA",showarrow = F)
Warning in RColorBrewer::brewer.pal(N, "Set2"): minimal value for n is 3, returning requested palette with 3 different levels
Warning in RColorBrewer::brewer.pal(N, "Set2"): minimal value for n is 3, returning requested palette with 3 different levels
Para guiar una primera lectura analizamos las variables con el coeficiente de V de cramer que nos sirve para medir la asociación entre variables categóricas.
Ver código
# seleccion de variables para correlacióndf_airbnb_for_cor_1 <- df_airbnb %>% dplyr::select(-c("id","calculated_host_listings_count","reviews_per_month"))# Initialize empty matrix to store coefficientsempty_m <-matrix(ncol =length(df_airbnb_for_cor_1),nrow =length(df_airbnb_for_cor_1),dimnames =list(names(df_airbnb_for_cor_1),names(df_airbnb_for_cor_1)))cor_matrix_1 <-calculate_cramer(empty_m ,df_airbnb_for_cor_1)corrplot::corrplot(cor_matrix_1, method ='number',tl.col ="darkgray",number.cex=0.60, tl.cex =0.75)
De esta primera lectura surge como primer punto a destacar que si bien las review scores se relacionan fuertemente entre sí, no parecen tener mucha relación con ninguna otra variable del set, por lo que las descartamos inicialmente de nuestro análisis .
A continuación listamo las relaciones más importantes y las agrupamos por temáticas:
Performance del anfitrión:
host_response_time y instant_bookable 0.27
host_response_time y host_identity_verified 0.19
host_response_time y host_is_superhost 0.18
Ubicación:
neighbourhood_cleansed y am_patio_balcony 0.22
Características de la habitación:
room_type y bath_type 0.80
bath_type y accommodates 0.53
bath_type y price_US 0.52
beds y bedrooms 0.51
bedrooms y accommodates 0.50
beds y accommodates 0.48
room_type y am_AC_heating 0.45
bedrooms y accommodates 0.40
bath_type y am_AC_heating 0.40
room_type y accommodates 0.35
room_type y am_tv 0.33
room_type y price_US 0.32
bedrooms y price_US 0.27
bath_type y am_tv 0.27
beds y price_US 0.19
room_type y am_elevator 0.18
Amenities:
am_bedroom_stuff y am_essentials 0.45
am_bedroom_stuff y am_bathtub 0.46
am_essentials y am_elevator 0.37
am_bedroom_stuff y am_parking 0.37
am_essentials y am_parking 0.31
am_bedroom_stuff y am_elevator 0.29
am_patio_balcony y balconyy am_parking 0.29
am_essentials y am_bath_stuff 0.29
am_patio_balcony y am_bedroom_stuff 0.28
am_bedroom_stuff y am_patio_balcony 0.28
am_essentials y am_bathtub 0.26
am_bedroom_stuff y am_bath_stuff 0.25
am_essentials y am_patio_balcony 0.24
am_elevator y am_bathtub 0.23
Ahora hagamos zoom en cada una de estos conjuntos de relaciones de variables y con una serie de Análisis de correspondencias múltiples (MCA)
Analisis de corresponecias múltiples (MCA)
Como señalan Joaquin Aldas y Ezequiel Uriel en “Análisis multivariado aplicado con R” (2017:159) el análisis de correspondencias es una técnica que muestra las distancias entre dos o más variables no métricas. Nuestro objetivo es ver si existen relaciones entre las variables que arrojen luz sobre los determinantes o drivers del mercado.En este caso vamos a alimentar el modelo directamente con las variables (raw data), pero es importante destacar que también se pueden utilizar un conjunto de tablas anidadas conocidas como matriz de burt(C) o una matriz de indicadores(Z) (los casos son filas y las columnas son todas respuestas dicotómicas)
Si analizamos todas juntas el MCA puede resultar poco explicativo por lo que cada una de las temáticas que indicamos arriba y su contribución e interacciones en una serie de MCA’s
Si analizamos las frecuencias generales sabemos que el 71% de los avisos se responden dentro de una hora y que el 89% tiene su identidad verificada pero el 68% no ofrece reservas inmediatas, alcanzando la categoría de superhost solo el 38% de los avisos.
Ver código
# df_airbnb %>% tabyl(host_response_time)# df_airbnb %>% tabyl(instant_bookable)# df_airbnb %>% tabyl(host_identity_verified)# df_airbnb %>% tabyl(host_is_superhost)# seleccion de variables para el MCA de Host performancedf_host_perfo <- df_airbnb %>% dplyr::select(host_response_time,instant_bookable,host_identity_verified,host_is_superhost)# aplicación de MCA para host perfofit_host_perfo <-MCA(df_host_perfo, ncp =4, graph = F)
En las primeras dos dimensiones del MCA ya podemos explicar el 43% de la varianza de la performance del host, y recien en la tercera dimensión tenemos un corte (tal cual se observa en este gráfico de sedimentación)
Analizando un mapa perceptual de la distribución de los casos en torno alas categorías de estas variables vemos que en la dimensión 2 la verificación de la identidad y el superhost aparece rodeada (aunque contribuyendo minoritariamente) de una respuesta dentro de los 60 minutos y sobre todo de reservas inmediatas. Sin embargo ¿Es la variable que mejor nos aporta a esta dimensión?
Ver código
# Mapa perceptual decontribución a la varianzafviz_mca_var(fit_host_perfo,col.var="contrib",gradient.cols =c("#00AFBB", "#E7B800", "#FC4E07"), repel = F) +theme_gray()
Si vemos las contribuciones a cada dimensión observamos que el tiempo de respuesta parece marcar mejor las diferencias entre las diferentes dimensiones
Ver código
# contribuciónfit_host_perfo$var$contrib
Dim 1 Dim 2 Dim 3 Dim 4
a few days or more 19.499639 8.5753363 8.007679383 23.6892776
within a day 8.566057 3.7253948 69.861584974 0.1123610
within a few hours 5.154641 22.4663351 20.924424599 14.6509988
within an hour 10.145324 4.4633331 0.331491242 0.2189696
instant_bookable_f 1.976601 14.5317713 0.008167697 3.1096440
instant_bookable_t 4.290268 31.5416140 0.017728213 6.7495688
host_identity_verified_f 23.390730 6.5431734 0.291169870 5.7350866
host_identity_verified_t 2.704903 0.7566524 0.033670875 0.6632052
host_is_superhost_f 9.291862 2.8315218 0.200632057 17.2542564
host_is_superhost_t 14.979974 4.5648678 0.323451090 27.8166318
Cuando el tiempo de respuesta es within an hour se ofrece reservas inmediatas instant_bookable
instant_bookable
host_response_time f t Total
a few days or more 5.5% 4.8% 5.3%
within a day 8.3% 1.6% 6.1%
within a few hours 22.9% 5.4% 17.4%
within an hour 63.3% 88.2% 71.2%
Total 100.0% 100.0% 100.0%
También vemos que a un mejor tiempo de respuesta tiene a ser super host en host_is_superhost
host_is_superhost
host_response_time f t Total
a few days or more 7.7% 1.4% 5.3%
within a day 7.8% 3.5% 6.1%
within a few hours 19.0% 14.9% 17.4%
within an hour 65.6% 80.2% 71.2%
Total 100.0% 100.0% 100.0%
Lo mismo sucede en relación a host_identity_verified
host_identity_verified
host_response_time f t Total
a few days or more 16.6% 4.0% 5.3%
within a day 10.3% 5.7% 6.1%
within a few hours 19.1% 17.2% 17.4%
within an hour 53.9% 73.2% 71.2%
Total 100.0% 100.0% 100.0%
Por lo tanto concluimos en que host_response_time es una buen indicador de la performance del host
En el análisis de correlaciones habíamos visto que la ubicación del hotel se relaciona con si tiene balcón y/o patio. Esto podemos verlo en la tabla de frecuencias y confirmarlo con chi cuadrado.
El valor p obtenido del chi cuadrado es importante para determinar si se debe rechazar o no la hipótesis nula. Si el valor p es menor que el nivel de significancia seleccionado (por ejemplo, 0.05), entonces se rechaza la hipótesis nula y se concluye que hay evidencia suficiente para afirmar que existe una asociación significativa entre las variables y que las diferencias que observa,os no se deben al azar.
Como podemos ver las viviendas ubicadas en Palermo ofrecen balcones y patios y que naturalmente caen cuando nos acercamos a los barrios del micro centro.
Avancemos y veamos cómo se comporta am_patio_balcony en la dimensión de comodidades y servicios para ver si vale la pena sumarla a nuestro modelo.
En bloque está compuesto por las variables room_type, bath_type, accommodates, price_US, beds, bedrooms, am_AC_heating, am_tv, am_elevator,
El 53% de los avisos tienen una cama, el 76% una habitación, 56% acepta hasta 2 pasajeros, pero mayoritariamente se alquila el espacio entero (91%) con un baño privado (94%), el aire acondicionado tampoco es un diferencial (93%) ni la tv (92%). El acceso a ascensor sí ya que hay un 36% que no lo tiene.
Al observar la relación entre estas variables en un MCA las primeras dos dimensiones explican el 29% de la varianza y las primera 4 explican el 43%
Ver código
# seleccion de variables para el MCA de Host performancedf_room_drivers <- df_airbnb %>% dplyr::select(room_type, bath_type, accommodates, price_US, beds, bedrooms, am_AC_heating, am_tv, am_elevator)# aplicación de MCA para host perfofit_room_drivers <-MCA(df_room_drivers, ncp =9 ,graph = F)#summary(fit_room_drivers, nb.des = 3, ncp = 2) # Las primeras dos dimensiones explican el 29% de la varianza y las primera 4 explican el 43%fviz_screeplot(fit_room_drivers, "variance", geom =c("bar", "line"), addlabels = T)
La primera dimensión parece representar la asociación entre habitaciones privadas con baño compartido para un pasajero y económicas (menos de 10 dolares), sin aire ni calefacción. Pareciera representar a los típicos avisos de hostels económicos que inspiraron plataformas como couchsurf y luego Airbnb.
La segunda dimensión muestra la asociación para aquellas publicaciones para muchos pasajeros (4 o más), que por lo tanto requieren de muchas camas (+4 beds) y al menos 2 habitaciones, en esto se parece un poco a la dimensión 3 (donde vemos una fuerte contribución de la cantidad de camas para su opción de 2 camas) y de 3 pasajeros (quizás una familia con un niño) y la dimensión 4 parece tener una fuerte relación con las categorías de 3 camas y 3 pasajeros (quizás tres adultos o más pero ya con habitaciones separadas)
Quizás podemos concluir que este set de variables no nos puede decir mucho acerca de lo que entendemos son la mayoría de los avisos (un departamento de dos ambientes con una cama matrimonial para 1 o 2 pasajeros) pero si permite distingue el aviso para viajeros solitarios y económicos sin muchos servicios (como aire acondicionado y calefacción) y el de avisos para más pasajeros donde hay más variabilidad de los precios lógicamente tienden a aumentar. Las amenities que seleccionamos no parecen estar contribuyendo mucho en este grupo de asociaciones vinculadas a las características de la habitación.
En las dos primeras dimensiones ya podemos explicar el 52% de la varianza
Ver código
# seleccion de variables para el MCA de amenitiesdf_amenities <- df_airbnb %>% dplyr::select(am_bedroom_stuff, am_essentials, am_bathtub, am_parking, am_elevator, am_patio_balcony, am_bath_stuff)# aplicación de MCA de amenitiesfit_amenities <-MCA(df_amenities, ncp =7 ,graph = F)# screeplot de amenitiesfviz_screeplot(fit_amenities, "variance", geom =c("bar", "line"), addlabels = T)
Como podemos ver en el biplot las amenities esenciales (que como vimos en la ETL eran elementos esenciales,perchas,secador de pelo,agua caliente y plancha) y en menor medida elementos para la habitación (como almohadas extra y ropa de cama) explican aparecen juntas con la negativa de todo el resto de las amenities que podríamos llamar “de lujo” y que como vimos al comienzo tienen una menor variabilidad.
En la dimensión 2 vemos cómo las amenities de lujo se asocian entre sí sobre todo la bañera y elementos para el baño.
Tomamos entonces am_essentials para sumarla a nuestro modelo
Conclusión de drivers de mercado
Como pudimos ver en esta sección podemos ordenar las variables en 5 temas o drivers: La performance del host (de la cual destacamos la variable host_response_time), las características de la habitación (room_drivers) donde claramente distinguimos aquellas más pequeñas y económicas con baño compartido de aquellas que apuntan a un perfil de más viajeros, donde distinguimos a su vez un tipo de habitación para tres pasajeros que necesitan una sola cama (quizás una familia con un niño pequeño) y otras destinadas 3 y más pasajeros donde se ofrecen más habitaciones y más camas. En este sentido (y para sorpresa de lo que nos imaginábamos) las variables que más contribuyen a describir la habitación no incluyen room_type sino que son beds,accommodatesy bath_type (expresando el 47% de la varianza tal cual vemos en el gráfico inferior)
Finalmente analizamos la asociación entre amenities donde pudimos distinguir como amenities esenciales y en menor medida elementos básicos de la habitación (como almohadas extra y ropa de cama) se asocian entre sí contraponiéndose a otras que podríamos llamar amenities de lujo como la presencia de ascensor, estacionamiento, balcón o bañera (otra variable por la cual nunca hubiéramos apostado y nos sorprendió).
Cluster con K-modas
Teniendo en cuenta lo que vimos hasta ahora y ya haciéndonos una idea de los posibles perfiles de tipo de aviso que conforman la oferta en la que se va a insertar “Casa Angelito” vamos a usar las variables host_response_time, accommodates,beds,bath_type y am_essentials para hacer un análisis de conglomerados o cluster.
La idea general de esta técnica de es clasificar las observaciones en grupos que sean lo más homogéneos internamente y lo más heterogéneos entre sí teniendo en cuenta la cercanía de las observaciones entre ellas o lo que llamamos medidas de proximidad o similaridad. La elección de una medida u otra depende del tipo de de variable.
Vamos a utilizar la técnica de kmodas. k-modas es similar al k-medias pero en vez de calcular los centroides, usa las modas para representar cada cluster. El objetivo es encontrar los valores más frecuentes en las variables categóricas para cada cluster y asignar cada observación al cluster con la moda más cercana. El número de modas, k, se establece de antemano al igual que en k-media
En un archivo de colab aparte estimamos la cantidad de clusters con el método del codo con la distancia de Hamming. Como no encontramos la función para hacerla en R sino en Python lo corremos en el colab y pegamos los resultados a continuación.
Después de varias pruebas elegimos trabajar con 3 clusters que corremos usando la librería klaR.
Primero buscamos una iteración y una semilla que nos parezcan estables
Ver código
# selecciono las variables para el clusterdf_airbnb_cluster<-df_airbnb %>% dplyr::select(host_response_time,accommodates,beds,bath_type,am_essentials)# Definir número de clustersk <-3# Definir número de iteracionesn_iter <-100# con 10 no era tan estable y lo subimos a 100# Definir vector de semillasseeds <-c(123, 456, 789, 321, 654, 987)# Matriz de resultadosresult_matrix <-matrix(0, nrow =length(seeds), ncol = k)# Loop sobre las semillasfor (i inseq_along(seeds)) {# Fijar la semillaset.seed(seeds[i])# Aplicar kmodes kmodes_result <- klaR::kmodes(df_airbnb_cluster, k, iter.max = n_iter)# Guardar el tamaño de los clusters result_matrix[i,] <- kmodes_result$size}# Imprimir los resultadosprint(result_matrix)
# Seleccion de la semilla que consideramos más estableset.seed(321)# aplicacion de algoritmodf_airbnb_cluster_vector <- klaR::kmodes(df_airbnb_cluster,3, iter.max =100)#Chequear el tamaño de los clustersdf_airbnb_cluster_vector$size
cluster
1 2 3
3163 2887 7225
Las variables y am_essentials, bath_type y host_response_time no parecen estar contribuyendo en la construcción de los cluster. El primer cluster (3163 publicaciones) nos recuerda a las publcaciones para grandes contingentes (4 pasajeros y 3 camas), el segundo cluster que habíamos asociados a un modelo familiar donde se acepta hasta 3 pasajeros con 2 camas y luego sí el cluster más populoso que conforma lo que podríamos llamar el airbnb base (7225) de 2 pasajeros y una cama.
Por supuesto todas estas son hipótesis que utilizamos en este examen a modo de ejemplo y para facilitar una posible lectura ya que no contamos con datos de los huèspedes y para ello se debería realizar otro tipo de estudio con otras fuentes. (por ej. encuestas, escrapeo y análisis de los comentarios de los huespedes, etc. etc.)
Ver código
df_airbnb_cluster_vector$modes
host_response_time accommodates beds bath_type am_essentials
1 within an hour 4 and more pax 3 beds Private bath t
2 within an hour 3 pax 2 beds Private bath t
3 within an hour 2 pax 1 bed Private bath t
Ahora imputamos el vector con los clusters a la base para poder empezar a analizarlos
Ver código
# extraer el clusterdf_airbnb_cluster_index <- df_airbnb_cluster_vector$cluster %>%as.data.frame() %>%rename("cluster"=".")#unirlo a la basedf_airbnb <- df_airbnb %>%cbind(df_airbnb_cluster_index)# # chequear que los tamaños esten ok#table(df_airbnb$cluster)# eliminar df intermediosrm(df_airbnb_cluster_index,df_airbnb_cluster_vector,df_airbnb_cluster)# renombramos los clustersdf_airbnb <- df_airbnb %>%mutate(cluster =case_when(cluster ==1~"cluster_1", cluster ==2~"cluster_2", cluster ==3~"cluster_3"))
Caracterización de los cluster
Bajo la hipótesis de que los clusters que surjen están vinculados estos distintos tipos de contingentes (grandes contingentes, grupo familiar reducido, y parejas/ hasta 2 pasajeros) analizamos en esta sección el comportamiento de las variables en cada cluster para ver qué más nos dicen acerca de los mismos.
Tipos de habitación y cluster
Como se había sugerido en el MCA vemos que el cluster 3 (de dos pasajeros y donde es muy probable que están contemplados los pasajeros solitarios que son un grupo minoritario) muestran diferencias en la distribución (las cuales son estadísticamente significativas).
Ver código
df_airbnb %>%tabyl(room_type,cluster) %>%adorn_totals('col') %>%adorn_percentages('col') %>%adorn_totals() %>%adorn_pct_formatting() %>%gt() %>%tab_header(title ="Tipos de habitacion por cluster")
La oferta de habitaciones privadas (que aunque es siempre minoritaria nos interesa sobre todo en el cluster 3), no tiene una distribución exactamente igual en todos los barrios: San Telmo (nuestro barrio objetivo) tiene una distribución porcentual de habitaciones privadas que se asemeja a la del conjunto de los barrios “no turísticos” de la ciudad (Other not north) para el cluster 3.
Ver código
df_airbnb %>% dplyr::group_by(cluster,neighbourhood_cleansed,room_type) %>%summarise(recuento =n_distinct(id)) %>%mutate(prop = recuento/sum(recuento)*100) %>%ggplot() +aes(x = cluster, y = prop, fill = room_type) +geom_col() +scale_fill_hue(direction =1) +labs(title ="Tipo de habitación por barrio y cluster",fill ="Room type" ) +theme_minimal() +facet_wrap(vars(neighbourhood_cleansed), scales ="free_y")
`summarise()` has grouped output by 'cluster', 'neighbourhood_cleansed'. You
can override using the `.groups` argument.
Esto es importante porque también determinará el rango de precios en que se mueva la oferta que como es de esperar cae entre las viviendas que para el cluster 3.
Ver código
df_airbnb %>%mutate(price_US =factor(price_US,levels =c("under 10","11 - 15","16 - 25","26 - 35","36 - 45","over 46"),labels =c("under 10","11 - 15","16 - 25","26 - 35","36 - 45","over 46"),ordered = T)) %>%group_by(cluster,room_type,price_US) %>%summarise(recuento =n_distinct(id)) %>%mutate(prop = recuento/sum(recuento)*100) %>%ggplot() +aes(x = cluster, y = prop, fill = price_US) +geom_col() +scale_fill_hue(direction =1) +labs(title ="Rangos de precios en US$ de las habitaciones segun tipo y cluster",fill ="Room type" ) +theme_minimal() +facet_wrap(vars(room_type), scales ="free_y")
`summarise()` has grouped output by 'cluster', 'room_type'. You can override
using the `.groups` argument.
Ninguno de los clusters parece tener una oferta diferencial respecto de las reservas inmediatas
Respecto de la ocupación resulta lógico que a medida que nos vamos acercando al cluster 3 la media de ocupación de las habitaciones privadas vaya creciendo, hasta casi igualar la proporción de ocupación para habitaciones privadas.
Ver código
df_airbnb %>% dplyr::group_by(cluster, room_type) %>%summarise(occupancy_cont = (mean(occupancy_cont))) %>%ggplot() +aes(x = room_type, y = occupancy_cont, fill = room_type) +geom_col() +scale_fill_hue(direction =1) +labs(x ="Room type",y ="Mean occupancy",title ="Ocupación calculada promedio por tipo de habitación para cada cluster",caption ="Ocupación = reviews por cantidad de publicaciones que tiene un host",fill ="Room type" ) +theme_minimal() +facet_wrap(vars(cluster))
`summarise()` has grouped output by 'cluster'. You can override using the
`.groups` argument.
Amenities
No se observan cortes de preferencia por amenities entre los clusters, lo cual nos indica que es un punto a contrinuar exploraar y refinar
Si bine los precios promedios para contingentes y familias son mayores el precio per capita es menor que para el cluster 3, de modo que el cliente deberá tener que tener en cuenta este efecto en caso de querer realizar ofertas o packs si desea competir con esos segmentos.
Ver código
df_airbnb %>%group_by(cluster) %>%summarise(precio_medio =round(mean(price_US_cont),2),precio_per_capita_medio =mean(price_US_cont/accommodates_cont)) %>%gt() %>%tab_header(title ="Precio promedio y per capita segun cluster")
Precio promedio y per capita segun cluster
cluster
precio_medio
precio_per_capita_medio
cluster_1
34.70
8.123692
cluster_2
25.66
8.874505
cluster_3
22.29
11.246528
Los precios promedio por noche en cada barrio muestran que si bien los contingentes representan un precio sustancialmente mayor en el cluster 1 y 2 la diferencia entre el cluster 2 y 3 parecen ser leves
Ver código
df_airbnb %>%group_by(cluster,neighbourhood_cleansed) %>%summarise(precio_medio =round(mean(price_US_cont),2)) %>%plot_ly(x=~cluster, y=~precio_medio, color =~neighbourhood_cleansed) %>%layout(title ="<b>Precios promedio por barrio en cada cluster<b>")
`summarise()` has grouped output by 'cluster'. You can override using the
`.groups` argument.
No trace type specified: Based on info supplied, a 'bar' trace seems
appropriate. Read more about this trace type ->
https://plotly.com/r/reference/#bar
En este gráfico podemos ver los saltos en el costo marginal de pasar de paquete twin los clusters de 2 y 3 donde es interesante destacar lo que sucede en San Telmo, donde el paso de uno a otro no está tan marcado y el precio es muy similar, por lo que en relación a otros barrios es poco competitivo recibir a un pasajero más.
Ver código
df_airbnb %>%group_by(neighbourhood_cleansed, cluster) %>%summarise(precio_medio =round(mean(price_US_cont),2)) %>%mutate(costo_marginal =round((precio_medio/lead(precio_medio, default =last(precio_medio))) -1, 2) ) %>%filter(cluster !="cluster_3") %>%plot_ly(x=~cluster, y=~costo_marginal, color =~neighbourhood_cleansed) %>%layout(title ="<b>Costo marginal sobre cluster 3 para pasar al cluster 2 y 1<b>")
`summarise()` has grouped output by 'neighbourhood_cleansed'. You can override
using the `.groups` argument.
No trace type specified: Based on info supplied, a 'bar' trace seems
appropriate. Read more about this trace type ->
https://plotly.com/r/reference/#bar
En general se observan leves diferencias en el porcentaje de ocupación a favor de los clusters 1 y 2
Ver código
df_airbnb %>%group_by(cluster) %>%summarise(ocupacion_media =round(mean(occupancy_cont),2)) %>%gt() %>%tab_header(title ="Ocupación media según cluster")
Ocupación media según cluster
cluster
ocupacion_media
cluster_1
43.22
cluster_2
41.84
cluster_3
39.90
Dentro del Cluster 3 la ocupación parece ser bastante homogéna entre los barrios entre el 40 y 45% lo cual es bastante elevado respecto de los valores oficiales que habíamos visto que reportaba SINTA para esta época del año (diciembre 2022). Resulta llamativa la ocupación en Puerto Madero, para los grandes contingentes, superando el 60%
Ver código
df_airbnb %>%group_by(cluster,neighbourhood_cleansed) %>%summarise(ocupacion_media =round(mean(occupancy_cont),2)) %>%plot_ly(x=~cluster, y=~ocupacion_media, color =~neighbourhood_cleansed) %>%layout(title ="<b>Ocupación media por barrio segun cluster <b>")
`summarise()` has grouped output by 'cluster'. You can override using the
`.groups` argument.
No trace type specified: Based on info supplied, a 'bar' trace seems
appropriate. Read more about this trace type ->
https://plotly.com/r/reference/#bar
Respondiendo a la pregunta de nuestro cliente de en qué punto interceptan el precio y la ocupación en cada cluster para la cual aplicamos una regresión lineal para cada cluster y luego se igualamos las ecuaciones par ver en que punto interceptan ambas regresiones lineales
Ver código
# lm's para el cluster 1df_cluster_1 <- df_airbnb %>%filter(cluster =="cluster_1")eq_price_cluster_1 <-lm(price_US_cont ~ occupancy_cont, data = df_cluster_1)eq_ocupp_cluster_1 <-lm(occupancy_cont ~ price_US_cont, data = df_cluster_1)# lm's para el cluster 2df_cluster_2 <- df_airbnb %>%filter(cluster =="cluster_2")eq_price_cluster_2 <-lm(price_US_cont ~ occupancy_cont, data = df_cluster_2)eq_ocupp_cluster_2 <-lm(occupancy_cont ~ price_US_cont, data = df_cluster_2)# lm's para el cluster 3df_cluster_3 <- df_airbnb %>%filter(cluster =="cluster_3")eq_price_cluster_3 <-lm(price_US_cont ~ occupancy_cont, data = df_cluster_3)eq_ocupp_cluster_3 <-lm(occupancy_cont ~ price_US_cont, data = df_cluster_3)
Ver código
intercept_cluster_1 <-lmIntx(eq_price_cluster_1,eq_ocupp_cluster_1) %>%rownames_to_column("intercept") %>%mutate(intercept ="cluster_1")intercept_cluster_2 <-lmIntx(eq_price_cluster_2,eq_ocupp_cluster_2) %>%rownames_to_column("intercept") %>%mutate(intercept ="cluster_2")intercept_cluster_3 <-lmIntx(eq_price_cluster_3,eq_ocupp_cluster_3) %>%rownames_to_column("intercept") %>%mutate(intercept ="cluster_3")rbind(intercept_cluster_1,intercept_cluster_2,intercept_cluster_3) %>%rename(occupancy_cont = x,price_US_cont = y) %>%gt() %>%tab_header("¿Cual es el precio cuando la ocupación alcanza su punto óptimo? según cluster")
¿Cual es el precio cuando la ocupación alcanza su punto óptimo? según cluster
intercept
occupancy_cont
price_US_cont
cluster_1
69.28
34.17
cluster_2
49.22
25.42
cluster_3
58.77
21.92
Como se puede observar el precio y la ocupación alcanzan su punto óptimo simultáneamente en el cluster 3 cuando la ocupación es del 58% y el precio es de US$ 22. Nos parece llamativo el valor de la ocupación para el cluster 1 pero entendemos que las reviews (en las cuales se basa el cálculo de la misma) quizás podrían estar sobre estimadas en los grandes contingentes.
Mapa de clusters
Por ultimo vamos graficar una muestra de los clusters sobre un mapa de la ciudad simplemente a modo ilustrativo donde podemos visualizar la importancia del corredor norte
Ver código
# traer la lat y longdf_airbnb_geo <-read.csv("input/listings.csv") %>% dplyr::select("id","latitude","longitude")df_airbnb <- df_airbnb %>%left_join(df_airbnb_geo)
Joining with `by = join_by(id)`
Ver código
rm(df_airbnb_geo)library(leaflet)
Warning: package 'leaflet' was built under R version 4.2.3
Ver código
# Crear df para cada clusterdf_airbnb_c1 <- df_airbnb %>%filter(cluster =="cluster_1")df_airbnb_c2 <- df_airbnb %>%filter(cluster =="cluster_2")df_airbnb_c3 <- df_airbnb %>%filter(cluster =="cluster_3")# Crear el mapamapa <-leaflet() %>%addTiles('https://wms.ign.gob.ar/geoserver/gwc/service/tms/1.0.0/capabaseargenmap@EPSG%3A3857@png/{z}/{x}/{-y}.png',attribution ="Argenmap v2 - Instituto Geográfico Nacional") %>%setView(-58.44169, -34.603954, zoom =12)# Agregar los puntos al mapamapa <- mapa %>%addCircles(data = df_airbnb_c1,lat =~latitude,lng =~longitude,fillColor ="#ec407a",radius =30,fillOpacity =0.8,stroke =FALSE,group ="Cluster 1") %>%addCircles(data = df_airbnb_c2,lat =~latitude,lng =~longitude,fillColor ="#8F83BC",radius =30,fillOpacity =0.8,stroke =FALSE,group ="Cluster 2") %>%addCircles(data = df_airbnb_c3,lat =~latitude,lng =~longitude,fillColor ="#50B7B0",radius =30,fillOpacity =0.8,stroke =FALSE,group ="Cluster 3") %>%# Agregar control de las capasaddLayersControl(overlayGroups =c("Cluster 1","Cluster 2","Cluster 3"),options =layersControlOptions(collapsed =FALSE) )# Tirar mapamapa
Conclusiones
Como vimos a lo largo del TP se pueden distinguir tres tipos de avisos en Airbnb los cuales clasificamos principalmente por la capacidad de sus contingentes, y donde mayoritariamente el grupo 3 (hasta dos pasajeros con una cama matrimonial) es el más habitual.
De los grupos de asociaciones que analizamos inicialmente en relación a las amenities, no logramos hallar distinguir ninguna entre sí siendo este un tema a continuar explorando.
Respecto de las características de la habitación observamos que San Telmo la proporción de habitaciones privadas los saltos en el costo marginal de pasar de paquete twin los clusters de 2 y 3 en San Telmo resulta poco competitivo ya que la diferencia es de 31% y 29%.
Finalmente el precio y la ocupación alcanzan su punto óptimo simultáneamente en el cluster 3 (dentro del cual se ubicaría nuestro cliente) cuando la ocupación es del 58% y el precio es de US$ 22.
Para seguir profundizando
Todavía queda mucho por explorar, entre los posibles próximos pasos continuar evaluando otras estrategias de clasificación en las que podríamos por ej. :
ajustar kmodas con otros otros parámetros
evaluar la posibilidad de buscar subsegmentos dentro del cluster 3 (clusters dentro del gran cluster que es el cluster 3)