Para ésta práctica, además de las funciones incluidas con R “base”, usaremos la librería tidyverse
library(tidyverse)
# Hacemos que R se absetnga de usar notación científica para representar números con muchos espacios decimales
options(scipen = 20)
Trabajaremos con datos de airbnb. Se trata de un listado de alojamientos ofrecidos en la Ciudad de Buenos Aires, recopilado en enero 2018.
airbnBA <- read.csv("https://bitsandbricks.github.io/data/Airbnb_listings_Buenos_Aires_Jan_2018.csv")
Revisando el contenido:
summary(airbnBA)
## room_id host_id room_type
## Min. : 7270 Min. : 2616 Entire home/apt:8059
## 1st Qu.: 5701972 1st Qu.: 6715024 Private room :1826
## Median :14093556 Median : 27259276 Shared room : 231
## Mean :12857197 Mean : 48017123
## 3rd Qu.:19876958 3rd Qu.: 78877917
## Max. :23219172 Max. :172636284
##
## country city neighborhood
## Mode:logical Mode:logical Mode:logical
## NA's:10116 NA's:10116 NA's:10116
##
##
##
##
##
## address
## Buenos Aires, Ciudad Autónoma de Buenos Aires, Argentina:4032
## Buenos Aires, Autonomous City of Buenos Aires, Argentina:1406
## Palermo, Buenos Aires, Argentina : 725
## Buenos Aires, Buenos Aires, Argentina : 685
## Buenos Aires, CABA, Argentina : 590
## Recoleta, Buenos Aires, Argentina : 413
## (Other) :2265
## reviews overall_satisfaction accommodates bedrooms
## Min. : 0.00 Min. :2.500 Min. : 1.000 Min. : 0.000
## 1st Qu.: 0.00 1st Qu.:4.500 1st Qu.: 2.000 1st Qu.: 1.000
## Median : 3.00 Median :5.000 Median : 2.000 Median : 1.000
## Mean : 13.86 Mean :4.745 Mean : 2.758 Mean : 1.144
## 3rd Qu.: 15.00 3rd Qu.:5.000 3rd Qu.: 4.000 3rd Qu.: 1.000
## Max. :360.00 Max. :5.000 Max. :16.000 Max. :50.000
## NA's :4694 NA's :26
## bathrooms price deleted minstay
## Min. : 0.000 Min. : 0 Min. :0 Mode:logical
## 1st Qu.: 1.000 1st Qu.: 525 1st Qu.:0 NA's:10116
## Median : 1.000 Median : 897 Median :0
## Mean : 1.276 Mean : 1199 Mean :0
## 3rd Qu.: 1.000 3rd Qu.: 1345 3rd Qu.:0
## Max. :45.000 Max. :30570 Max. :0
## NA's :64
## last_modified latitude longitude
## 2018-02-08T16:52:39Z: 18 Min. :-34.70 Min. :-58.53
## 2018-02-10T00:44:52Z: 18 1st Qu.:-34.60 1st Qu.:-58.43
## 2018-02-10T01:45:47Z: 18 Median :-34.59 Median :-58.42
## 2018-02-09T07:56:56Z: 17 Mean :-34.59 Mean :-58.42
## 2018-02-09T09:57:08Z: 17 3rd Qu.:-34.58 3rd Qu.:-58.39
## 2018-02-09T18:58:45Z: 17 Max. :-34.53 Max. :-58.35
## (Other) :10011
## survey_id location
## Min. :1 0101000020E61000006EA301BC05344DC0471FF301814A41C0: 11
## 1st Qu.:1 0101000020E610000058552FBFD3324DC0897E6DFDF44B41C0: 10
## Median :1 0101000020E61000002BFC19DEAC334DC0F3C64961DE4B41C0: 8
## Mean :1 0101000020E61000004F22C2BF08364DC098DEFE5C344C41C0: 8
## 3rd Qu.:1 0101000020E610000006D671FC50354DC0DAACFA5C6D4F41C0: 7
## Max. :1 0101000020E6100000DDEEE53E39364DC00C789961A34A41C0: 7
## (Other) :10065
## coworker_hosted extra_host_languages
## Mode:logical Mode:logical
## NA's:10116 NA's:10116
##
##
##
##
##
## name property_type currency
## Departamento en Recoleta : 15 Apartment :8319 ARS:10116
## : 14 House : 860
## Departamento en Palermo : 10 Loft : 218
## Luminoso departamento en Palermo: 6 Condominium: 205
## Apartment in Recoleta : 5 Other : 197
## Cozy Apt. Heart of Buenos Aires : 5 Dorm : 78
## (Other) :10061 (Other) : 239
## rate_type
## nightly:10116
##
##
##
##
##
##
El test de Student se utiliza para comparar el valor medio de los valores extraidos de dos muestras distintas. Por ejemplo, la concentración de alcohol en dos tandas distintas de producción de cerveza, las notas de examen entre los alumnos de dos docentes, o -en éste caso- el precio por noche de dos categorías distintas de alojamientos en Airbnb.
Se lo debemos a este señor, William Sealy Gosset…
… que lo desarrolló alrededor de 1907 mientras trabajaba para la cervercería Guinnes. Gosset usó sus conocimientos de estadística para desarrollar un método que permitiera identificar entre dos cepas de centeno aquella con mejor rinde para la cerveza. Publicó su método en un paper bajo el pseudónimo de “Student” para aplacar a Guinness, que temía que la competencia pudiera identificar uan técnica útil para sus propios procesos.
Nosotros usaremos el test de Student, también llamado t-test, para establecer si existe una diferencia estadísticamente significativa entre el valor por noche de habitaciones privadas versus compartidas.
Primero, revisamos los valores medios para cada categoría:
airbnBA %>%
filter(room_type %in% c("Private room", "Shared room"), accommodates == 1) %>%
group_by(room_type) %>%
summarise(cantidad = n(),
valor_promedio = mean(price))
## # A tibble: 2 x 3
## room_type cantidad valor_promedio
## <fct> <int> <dbl>
## 1 Private room 1048 356.
## 2 Shared room 112 303.
Como era de esperarse, las habitaciones privadas tienen un valor más alto. Pero podemos asumir que en efecto hay una dinámica distinta de precios, o los datos no son concluyentes?
privadas_vs_compartidas <- airbnBA %>%
filter(room_type %in% c("Private room", "Shared room"), accommodates == 1)
ggplot() +
geom_density(data = privadas_vs_compartidas,
aes(x = price)) +
facet_wrap(~room_type, scales = "free_y")
T-TEST time!!
En líneas generales, un t test se realiza así: t.test(valor_de_cada_observacion ~ categoría_de_cada_observacion)
En código:
t.test(privadas_vs_compartidas$price ~ privadas_vs_compartidas$room_type)
##
## Welch Two Sample t-test
##
## data: privadas_vs_compartidas$price by privadas_vs_compartidas$room_type
## t = 5.3371, df = 134.06, p-value = 0.0000003919
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
## 33.04602 71.95861
## sample estimates:
## mean in group Private room mean in group Shared room
## 355.9666 303.4643
Podemos reportar los resultados así:
Para el alojamiento de un huésped, el test de medias halló un precio promedio significativamente* menor en las habitaciones compartidas respecto a las privadas (356 vs 303 pesos por noche), con un p-valor menor a 0,001.
*: aquí hablamos de significancia estadística, que es independiente de que la diferencia hallada sea “significativa” en el sentido de “contundente”
Un test ANOVA (“Analysis of Variance”) sirve cuando queremos comparar atribtuos de múltiples grupos par determinar si existe alguna diferencia entre ellos. Por ejemplo,
Buscando un ejemplo relacionado con nuestros datos, podríamos preguntarnos…
Evaluemos ahora el precio por alojamientos en propiedades de distinta clase. Podemos echar un vistazo a las categorías encontradas en la base, la frecuencia con la que aparecen, y su valor promedio:
airbnBA %>%
group_by(property_type) %>%
summarise(cantidad = n(),
valor_promedio = mean(price)) %>%
arrange(desc(valor_promedio))
## # A tibble: 23 x 3
## property_type cantidad valor_promedio
## <fct> <int> <dbl>
## 1 Boat 3 6771.
## 2 Cabin 1 5156
## 3 Serviced apartment 18 1820.
## 4 Loft 218 1594.
## 5 Villa 1 1325
## 6 House 860 1294.
## 7 Boutique hotel 11 1238.
## 8 Timeshare 1 1223
## 9 Condominium 205 1205.
## 10 Apartment 8319 1203.
## # … with 13 more rows
Podemos aislar sólo los alojamientos con una cama, para no comparar capacidades distintos entre si (es decir, no mezclar en el promedio alojamientos para una pareja con otros donde pueden dormir muchas mas personas):
airbnBA %>%
filter(bedrooms == 1) %>%
group_by(property_type) %>%
summarise(cantidad = n(),
valor_promedio = mean(price)) %>%
arrange(desc(valor_promedio))
## # A tibble: 20 x 3
## property_type cantidad valor_promedio
## <fct> <int> <dbl>
## 1 Boat 3 6771.
## 2 Boutique hotel 4 2498
## 3 Serviced apartment 6 1874
## 4 Loft 135 1361.
## 5 Condominium 135 992.
## 6 Apartment 5207 985.
## 7 Chalet 1 836
## 8 Other 141 719.
## 9 Townhouse 9 706.
## 10 Casa particular 5 534.
## 11 Vacation home 1 525
## 12 Camper/RV 2 505
## 13 In-law 12 494.
## 14 Pension (Korea) 2 453
## 15 House 599 423.
## 16 Guest suite 44 383.
## 17 Dorm 72 376.
## 18 Bed & Breakfast 52 374.
## 19 Guesthouse 22 359.
## 20 Hostel 26 307.
Excepto para los botes, (hay 3 botes!) el promedio desciende al concentrarnos en alojamientos de una sola cama.
También deberíamos retirar las categorías que aparecen pocas veces, dado que con una muestra tan chica podrían no ser representativas. Así podemos elegir sólo aquellas categorías que tienen más de 100 representantes.
Identificamos los tipos frecuentes:
tipos_frecuentes <- airbnBA %>%
filter(bedrooms == 1) %>%
group_by(property_type) %>%
summarise(cantidad = n()) %>%
filter(cantidad > 100) %>%
pull(property_type)
tipos_frecuentes
## [1] Apartment Condominium House Loft Other
## 23 Levels: Apartment Bed & Breakfast Boat Boutique hotel ... Villa
Y los usamos para filtrar el dataset, extrayendo sólo los alojamientos que tienen un tipo dentro de la lista identificada:
propiedades_1dorm <- airbnBA %>%
filter(property_type %in% tipos_frecuentes, bedrooms == 1)
Cómo están distribuidos?
ggplot() +
geom_histogram(data = propiedades_1dorm,
aes(x = price, fill = property_type)) +
facet_wrap(~property_type, scales = "free_y") +
guides(fill = FALSE)
Aparecen claros valores extremos, “outliers” que empujan la distribución hacia la derecha (hacia valores michísimo más altos que el promedio).
En este caso particular, una forma efectiva de retirar del análisis aquellos alojamientos que figuran con precios poco creíbles es retener solamente aquellos que tengan reviews; es decir, que sepamos que hayan sido alquilados por alguien alguna vez, dado que han dejado la opinión post estadía.
propiedades_1dorm <- airbnBA %>%
filter(property_type %in% tipos_frecuentes, bedrooms == 1, reviews > 0)
ggplot() +
geom_histogram(data = propiedades_1dorm,
aes(x = price, fill = property_type)) +
facet_wrap(~property_type, scales = "free_y") +
guides(fill = FALSE)
ANOVA time!
aov(data = misdatos, variable_dependiente ~ variable_independiente)
analisis_tipo_propiedad <- aov(data = propiedades_1dorm, price ~ property_type)
analisis_tipo_propiedad
## Call:
## aov(formula = price ~ property_type, data = propiedades_1dorm)
##
## Terms:
## property_type Residuals
## Sum of Squares 109381676 1707874538
## Deg. of Freedom 4 4375
##
## Residual standard error: 624.797
## Estimated effects may be unbalanced
summary(analisis_tipo_propiedad)
## Df Sum Sq Mean Sq F value Pr(>F)
## property_type 4 109381676 27345419 70.05 <0.0000000000000002 ***
## Residuals 4375 1707874538 390371
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
De acuedo a nuestro análisis, no caben dudas de que el tipo de alojamiento afecta su precio? Pero… ¿cuánto? ¿Qué tipos de alojamiento exhiben diferencias mayores?
El test ANOVA no nos permite extraer conclusiones espécificas; sólo indica que existe una diferencia significativa entre los grupos.
Para poder extraer más detalles, podemos recurrr a…
Desarrollado por el estadístico John Tukey, que tenía todo un talento para nombrar cosas -se lo acredita también como inventor de la palabra “bit” para expresar la mínima unidad de datos en un sistema de información.
Al hacer el test HSD, asumimos tres cosas:
Para realizarlo en R podemos usar la función TukeyHSD(). Como primer argumento usamos un objeto “aov”, el resultado de un análisis ANOVA como el que hicimos en el paso anterior. El segundo argumento para la función es la variable independiente que define los grupos, en nuestro caso el tipo de propiedad:
analisis_tukey_tipo_propiedad <- TukeyHSD(analisis_tipo_propiedad, "property_type")
Veamos los resultados
analisis_tukey_tipo_propiedad
## Tukey multiple comparisons of means
## 95% family-wise confidence level
##
## Fit: aov(formula = price ~ property_type, data = propiedades_1dorm)
##
## $property_type
## diff lwr upr p adj
## Condominium-Apartment -85.94412 -264.93784 93.04959 0.6848290
## House-Apartment -529.64574 -622.35245 -436.93902 0.0000000
## Loft-Apartment 279.20131 107.26296 451.13965 0.0000938
## Other-Apartment -252.00926 -437.97454 -66.04399 0.0020504
## House-Condominium -443.70161 -641.37228 -246.03095 0.0000001
## Loft-Condominium 365.14543 120.11084 610.18002 0.0004661
## Other-Condominium -166.06514 -421.13815 89.00787 0.3873750
## Loft-House 808.84704 617.54170 1000.15239 0.0000000
## Other-House 277.63647 73.63151 481.64144 0.0019299
## Other-Loft -531.21057 -781.38308 -281.03806 0.0000001
El test compara los grupos de a pares, y estima la diferencia entre sus medias, y el grado de significancia estádistica. Por ejemplo, aquí ha establecido que la diferencia entre condominios y apartamentos es de unos 86 dólares a favor de la segunda categoría (la media de los condos es 86 dólares menor) pero con un p-valor muy por encima de 0.05 debemos aceptar la hipótesis nula: no hay una diferencia significativa. Por otro lado, diferencias entre pares como House y Apartment, o entre Loft y Condominium si muestran significancia.
En base a las preferencias y características demográficas de una persona, ¿podemos predecir si votará a un partido político u otro?
Para el ejercicio usaremos datos recopilados por el proyecto ANES, American National Election Studies. ANES realiza en forma periódica extensas encuestas individuales en EEUU, para proveer información que permita a Cientistas Sociales teorizar y explicar resultados electorales. Se pueden encontrar en https://electionstudies.org/
Comencemos por cargar un dataset con atributos a nivel persona, que incluye el partido por el cual votaron en las elecciones presidenciales de 2008 en EEUU.
votantes <- read.csv("https://bitsandbricks.github.io/data/anes_2008tr.csv")
names(votantes)
## [1] "edad" "raza_blanco_a"
## [3] "genero_varon" "educacion"
## [5] "ingresos" "ideologia_conservadora"
## [7] "ha_votado_republicano" "ha_votado"
summary(votantes)
## edad raza_blanco_a genero_varon educacion
## Min. : 0.00 Min. :0.0000 Min. :0.0000 Min. :1.000
## 1st Qu.:32.00 1st Qu.:0.0000 1st Qu.:0.0000 1st Qu.:3.000
## Median :46.00 Median :1.0000 Median :0.0000 Median :5.000
## Mean :46.45 Mean :0.5043 Mean :0.4302 Mean :4.127
## 3rd Qu.:59.00 3rd Qu.:1.0000 3rd Qu.:1.0000 3rd Qu.:5.000
## Max. :93.00 Max. :1.0000 Max. :1.0000 Max. :7.000
##
## ingresos ideologia_conservadora ha_votado_republicano
## Min. :1.000 Min. :1.000 Min. :0.000
## 1st Qu.:2.000 1st Qu.:4.000 1st Qu.:0.000
## Median :3.000 Median :4.000 Median :0.000
## Mean :2.705 Mean :4.098 Mean :0.334
## 3rd Qu.:3.000 3rd Qu.:5.000 3rd Qu.:1.000
## Max. :5.000 Max. :7.000 Max. :1.000
## NA's :783
## ha_votado
## Min. :0.0000
## 1st Qu.:0.0000
## Median :1.0000
## Mean :0.6904
## 3rd Qu.:1.0000
## Max. :1.0000
##
Exploremos algunas de las variables
ggplot() +
geom_histogram(data = votantes, aes(x = edad))
ggplot() +
geom_histogram(data = votantes, aes(x = edad)) +
facet_wrap(~ha_votado_republicano)
ggplot() +
geom_bar(data = votantes, aes(x = ideologia_conservadora))
ggplot() +
geom_bar(data = votantes, aes(x = ideologia_conservadora)) +
facet_wrap(~ha_votado_republicano)
ggplot() +
geom_bar(data = votantes, aes(x = as.factor(educacion))) +
facet_wrap(~raza_blanco_a)
¡Listos para hacer una regresión logística! En R se resuelve de manera similar a la regresión lineal, con una fórmula que expresa la variable a predecir versus las predictoras. La función a la que recurriremos es glm(), que permite utilizar varias técnicas de regresión; con family="binomial" le aclaramos que queremos una regresión logística.
Por ejemplo, para establecer el grado en que la edad, la raza, el nivel de ingresos y la inclinación ideológica predicen el voto republicano, usamos:
vota_republicano <- glm(data = votantes,
ha_votado_republicano ~ edad + raza_blanco_a + ingresos + ideologia_conservadora,
family="binomial")
summary(vota_republicano)
##
## Call:
## glm(formula = ha_votado_republicano ~ edad + raza_blanco_a +
## ingresos + ideologia_conservadora, family = "binomial", data = votantes)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.3038 -0.4875 -0.2900 0.4763 3.3359
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -8.076556 0.462932 -17.447 < 0.0000000000000002
## edad 0.004738 0.004190 1.131 0.258
## raza_blanco_a 2.436605 0.167899 14.512 < 0.0000000000000002
## ingresos 0.404587 0.074955 5.398 0.0000000675
## ideologia_conservadora 1.041977 0.066277 15.721 < 0.0000000000000002
##
## (Intercept) ***
## edad
## raza_blanco_a ***
## ingresos ***
## ideologia_conservadora ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1960.6 on 1538 degrees of freedom
## Residual deviance: 1170.3 on 1534 degrees of freedom
## (783 observations deleted due to missingness)
## AIC: 1180.3
##
## Number of Fisher Scoring iterations: 5
La regresión logística calcula un porcentaje de probabilidad (del 0 al 100%) de que cada votante haya votado republicano de acuerdo a los atributos utilizados como predictores. Una vez realizado un modelo, como el que hemos guardado en la variable vota_republicano, se puede usar la funcion predict() para obtener los valores que el modelo “predice” para cualquier conjunto de datos, por ejemplo con el resultado de futuras encuestas.
Si usamos predict() con los mismos datos con los que hicimos el modelo, lo que obtenemos es la probabilidad de votar republicano asignada por el modelo a cada uno de los votantes en el dataset.
Verifiquemos el porcentaje de votantes que es clasificado en forma correcta utilizando
Veamos esas probabilidadaes:
predicciones <- predict(vota_republicano, votantes)
head(predicciones)
## 1 2 3 4 5 6
## -2.1244712 2.1004745 2.4150400 0.3832037 2.5713932 2.1620682
Aquí hay un problema. En lugar de valores entre 0 y 1, expresando del 0 al 10o% de pobabilidad, hemos obtenido números mayores a 1, y menores a 0… negativos! ¿Qué ocurre?
Las probabilidades están expresadas en “logits”. Son el logaritmo de las chances u odds (por ejemplo “una en mil”) de que cada votante sea republicano.
La buena noticia es que es fácil pasar de logits a chances: usamos los logits como exponente de e, y obtenemos las chances. En R es tan simple como exp(predicciones)
Y luego es facil pasar de chances a probabilidad porcentual; la cuenta es:
\[ \Leftrightarrow \dfrac{chances}{1 + chances} = P(Y = 1)\]
Donde P(Y = 1) es, de acuerdo a las chances estilo 1 en 10000, la probabilidad de que la variable Y sea de la categoría que estamos analizando… en éste caso, que la persona haya votado por el partido republicano.
Calculemos entonces las probabilidades de nuestra predicción, y agreguémoslas a los datos:
votantes <- votantes %>%
mutate(prediccion.logit = predict(vota_republicano, votantes),
prediccion.odds = exp(prediccion.logit),
prediccion.probabilidad = prediccion.odds / (1 + prediccion.odds))
Y dado que sabemos si el votante optó o no por el partido Republicano, hagamos una tabla de cruce y veamos que tan buenas resultan las predicciones.
Si la probilidad es mayor a 50% (mayor a 0.5) consideramos que se predice un votante republicano. de lo contrario, que no vota por ese partido:
votantes <- votantes %>%
mutate(predice_republicano = ifelse(prediccion.probabilidad > 0.5, 1, 0))
y ahora, la tabla cruzada:
xtabs(data = votantes, ~ predice_republicano + ha_votado_republicano)
## ha_votado_republicano
## predice_republicano 0 1
## 0 921 160
## 1 104 354
Total de casos: 921 + 104 + 160 + 354 = 1539
Aciertos: 921 + 354 = 1275
Tasa de acierto: 1275 / 1539 = 0.82846
Casi 83% de acierto… resulta mejor que predecir tirando una moneda al aire.