1 Introducción y descripción del dataset

En un contexto en el que el mercado inmobiliario vuelve a ser muy volátil tras la crisis de 2008, con una subida generalizada de precios en España y el tercer mayor dato de firma de hipotecas de la serie histórica en marzo de 2022, según los datos del Instituto Nacional de Estadística (INE), a continuación se presenta un proyecto centrado en este sector. Además, cabe destacar que el mercado inmobiliario en España se ha caracterizado desde hace décadas por las diferencias en el precio de la vivienda, tanto según las zonas del territorio nacional como dentro de las propias localidades.

En concreto, el conjunto de datos objeto de análisis de este trabajo es Houses for sale in the Salamanca and Villaverde district of Madrid in April 2022, un dataset de las viviendas en venta en los portales inmobiliarios de Idealista y Fotocasa en el distrito más caro (Salamanca) y el distrito más barato (Villaverde) de Madrid obtenidos en la Práctica 1 de esta asignatura entre el 4 y 7 de abril mediante un proceso de scraping debido a que no existe un registro público de viviendas en venta actualizado.

Por tanto, la Ley por el Derecho a la Vivienda (decreto del 1 de febrero de 2022) acababa de entrar en vigor y era un momento en el que la invasión de Rusia a Ucrania había pillado por sorpresa a todos los sectores de la economía, disparando la inflación en España hasta un 9,8 %, según el INE. Del mismo modo, el Banco de España había recortado sus previsiones de crecimiento para el país y había doblado su previsión anual de inflación al 7,5 %, según los datos publicados el 5 de abril de 2022.

Teniendo en cuenta la situación descrita, el problema principal que se aborda en este proyecto es la comparación de la situación del mercado inmobiliario entre los distritos de Salamanca y Villaverde de Madrid, la ciudad más poblada de España, para descubrir qué es lo que motiva tal diferencia de valor económico. Así, a partir de los datos inmobiliarios se pretenden encontrar relaciones entre las características de las viviendas y determinar posibles relaciones de dichas características con su precio.

En este sentido, aunque en el apartado de Selección y transformación de datos de interés se hace una descripción pormenorizada de todas las variables con las que se trabajará, cabe ser mencionado que todos los registros estarán unívocamente identificados por la tupla {id,source}. Además, se trabajará con variables que contienen información sobre la localización de las viviendas, su precio, tamaño, número de habitaciones y altura (planta), así como otros atributos que no describen a las viviendas sino los anuncios que las contienen, como el número de fotografías y su descripción.

Teniendo presente esta descripción inicial del dataset y el objetivo expuesto, se va a responder a las siguientes preguntas:

  • ¿Existen diferencias significativas en el precio de la vivienda por metro cuadrado según el distrito en el que esté localizada? Para responder esta pregunta se realizará un contraste de hipótesis con un nivel de significancia \(\alpha\) = 0.05.

  • ¿Cuáles son las características (de la vivienda y del anuncio) que influyen más en el precio? Para responder esta pregunta se realizará un análisis de las correlaciones de las variables numéricas del dataset.

  • ¿Son las viviendas situadas en plantas altas más caras que las viviendas en plantas más cercanas al suelo? Para responder esta pregunta se realizará un contraste de hipótesis con un nivel de significancia \(\alpha\) = 0.05.

  • ¿Existe una relación entre el tamaño de la vivienda y el número de habitaciones que esta posee? Para responder esta pregunta se realizará un análisis de las correlaciones entre las 2 variables afectadas.

Por último, se creará un modelo de regresión lineal para predecir el precio de una vivienda dadas sus características físicas (no se tendrán en cuenta las características del anuncio).

La información que se desprenda de este estudio es relevante para agentes del mercado inmobiliario – agencias inmobiliarias, propietarios particulares que buscan vender un inmueble y personas que buscan comprar una vivienda –, así como para terceros que estén interesados en la situación del mercado inmobiliario en dichas zonas de Madrid. Por último, este proyecto también es relevante en el ámbito social, incluyendo la problemática de la repartición de la riqueza y el derecho de acceso a la vivienda.

2 Integración, selección y transformación de datos

En primer lugar, se procede a cargar los datos raw obtenidos del repositorio Zenodo, creado el 7 de abril de 2022. En dicho fichero, ya se encuentran integrados los registros obtenidos tras el proceso de scraping de Idealista y de Fotocasa, por lo que no es necesario realizar de nuevo el proceso para la fusión de los datos.

houses.raw <- read.csv('../data/real-estate-raw.csv', header=TRUE, sep=',', 
                       stringsAsFactors=FALSE, fileEncoding='UTF-8')
houses.raw.dim <- dim(houses.raw)

Se han cargado 3.544 registros con 16 campos asociados a cada uno. Así, se muestran 5 registros aleatorios para comprobar la estructura del conjunto de datos, puesto que si se seleccionan los del principio (head) o final (tail) todos serán de la misma fuente de datos:

houses.raw[sample(nrow(houses.raw), 5), ]

Además, se examina el tipo de datos con los que R ha interpretado cada variable:

Form.Basic = c("striped", "hover", "condensed", "responsive")
kbl(cbind(sapply(houses.raw, class)),
    caption="Tipo de datos de cada variable") %>%
  kable_styling(bootstrap_options = Form.Basic) %>%
  scroll_box(width = "100%")
Tipo de datos de cada variable
id character
url character
title character
location character
price character
m2 character
rooms character
floor character
num.photos character
floor.plan character
view3d character
video character
home.staging character
description character
photo_urls character
source character

2.1 Gestión de elementos duplicados

Para finalizar la fase de integración de los datos, se eliminan las viviendas duplicadas, es decir, aquellos anuncios que contengan la misma vivienda en venta. Para ello, se evalúa cuáles tienen idéntico el campo description, puesto que pueden ser anuncios diferentes (con diferente id o source) pero con el mismo contenido. Para profundizar en el asunto y poderlo analizar mejor se extraen en un nuevo dataframe las viviendas duplicadas según este criterio.

duplicated.houses <- houses.raw[duplicated(houses.raw$description), ]

Así, por ejemplo, consecuencia del proceso de integración se pueden encontrar anuncios procedentes de Fotocasa e Idealista que contienen la misma vivienda:

duplicated_integration <- duplicated.houses[duplicated.houses$id == '161630827' | 
                                        duplicated.houses$id == '97136644', 
                                      c('source', 'location', 'price', 'description')]

duplicated_integration

Por otro lado, se observa como fenómeno que también existen viviendas ofertadas varias veces en el mismo portal inmobiliario:

duplicated_source <- duplicated.houses[duplicated.houses$id == '157307581' | 
                                         duplicated.houses$id == '160123208'| 
                                         duplicated.houses$id == '162766808'|
                                         duplicated.houses$id == '96250173'|
                                         duplicated.houses$id == '91151646'|
                                         duplicated.houses$id == '96890387'|
                                         duplicated.houses$id == '91334123'|
                                         duplicated.houses$id == '96857738',
                                      c('source', 'location', 'price', 'description')]

duplicated_source

Con esta información, se deciden eliminar las viviendas duplicadas y, en caso de que se encuentre en ambos sitios web, se mantiene la vivienda de Fotocasa al contener información en el campo rooms, asunto que se trabajará más adelante. Fotocasa se selecciona por defecto porque es el primer registro que aparece en el dataframe original:

houses.unique = houses.raw[!duplicated(houses.raw$description),]

2.2 Selección y transformación de datos de interés

Sin registros duplicados, se pasa a la selección de los datos de interés, que conlleva una reducción de la dimensionalidad. Se lista a continuación cada campo, una breve descripción del mismo y si se selecciona o no, así como los motivos de la decisión:

  • id: identificador numérico para cada registro. Actúa como identificador único junto al campo source. Se decide mantener los campos identificadores.
  • url: enlace a la página de venta de la vivienda. Se descarta debido a que no se puede obtener más información de la ya obtenida en la práctica anterior y a que los anuncios pueden haber desaparecido.
  • title: título del anuncio. Se descarta por no ofrecer información adicional.
  • location: distrito en que se encuentra la vivienda anunciada. Puede ofrecer información relevante sobre la variabilidad de precios en el mercado inmobiliario y permite agrupar los anuncios por zonas. Será importante en la parte de análisis.
  • price: precio en euros. Se mantiene porque es una de las variables más importantes del conjunto de datos.
  • m2: metros cuadrados de la vivienda. Permite distinguir el tamaño de las viviendas y realizar agrupaciones, por lo que se mantiene.
  • price.m2: se creará una nueva variable que contenga el valor del precio por metro cuadrado en euros de la vivienda.
  • rooms: número de habitaciones de la vivienda. Junto a m2 permite describir sus características físicas.
  • floor: altura a la que se encuentra la vivienda. Ayuda a las descripción de sus características físicas.
  • num.photos: número de fotos que acompañan al anuncio. Se puede analizar si existe correlación entre algún tipo de vivienda (caras, baratas, con peores o mejores características) y el número de fotos, por lo que se mantiene.
  • floor.plan: valor lógico que indica si se ha adjuntado el plano de la casa al anuncio, por lo que se mantiene.
  • view3d: valor lógico que indica si el anuncio cuenta con la característica de visión en 3D, por lo que se mantiene.
  • video: valor lógico que indica si el anuncio cuenta con un vídeo de la vivienda, por lo que se mantiene.
  • home.staging: valor lógico que indica si el anuncio cuenta con la característica de home staging, por lo que se mantiene.
  • description: descripción del cuerpo del anuncio. Se mantiene por si su análisis sirviera de utilidad en el análisis futuro, aunque un análisis textual supera el contexto de la asignatura.
  • description_length: se creará un campo nuevo description_length con el número de palabras contenidas en la descripción para futuros análisis.
  • photo_urls: lista de los enlaces de las fotografías adjuntas al anuncio. Se descarta por no realizar un análisis de las fotografías.
  • source: cadena de texto que indica la fuente de la que se ha extraído la información del anuncio. Solo existen los valores “idealista” y “fotocasa”. Forma parte del identificador único de cada registro y sirve, además, para agrupar viviendas.

Como se ha observado que todos los campos se han codificado como cadenas de caracteres, se optimiza la fase de selección de datos realizando también la transformación del tipo de datos. Así, a continuación se eliminan las variables descartadas, se crean las nuevas y se transforman los datos y se muestran 5 registros al azar:

selected.houses <- houses.unique %>% dplyr::select(-url, -title, -photo_urls) %>%  
  dplyr::mutate(id = as.integer(id), 
                location = as.factor(location),
                price = as.integer(str_remove_all(price, '\\.')),
                m2 = as.integer(m2), 
                price.m2 = as.double(price/m2),
                rooms = as.integer(rooms),
                floor = as.factor(floor), 
                num.photos = as.integer(num.photos), 
                floor.plan = as.logical(as.integer(floor.plan)),
                view3d = as.logical(as.integer(view3d)), 
                video = as.logical(as.integer(video)),
                home.staging = as.logical(as.integer(home.staging)),
                description = description,
                description.length = nchar(description),
                source = as.factor(source))
selected.houses.dim <- dim(selected.houses)
houses.raw[sample(nrow(selected.houses), 5), ]

Finalmente, debido a que la cantidad de registros no es muy elevada, se decide no reducirlos mediante ningún método.

Por tanto, el conjunto de datos con el que se trabajará durante el resto del estudio está compuesto de 3.361 registros con 15 campos por cada registro. En este punto, y antes de continuar con la limpieza de datos, es interesante mostrar la estructura completa del dataframe:

str(selected.houses)
## 'data.frame':    3361 obs. of  15 variables:
##  $ id                : int  157512027 162925661 162588489 162239807 163071926 162965614 162532531 162696838 162659376 160304048 ...
##  $ location          : Factor w/ 3 levels "barrio-de-salamanca",..: 1 1 1 1 1 1 1 1 1 1 ...
##  $ price             : int  1090000 1400000 1490000 2150000 2550000 410000 895000 1690000 699000 1600000 ...
##  $ m2                : int  130 232 147 197 274 77 173 193 108 157 ...
##  $ rooms             : int  2 5 1 3 4 4 3 3 3 3 ...
##  $ floor             : Factor w/ 69 levels "11ª","12ª","1ª",..: 9 3 9 5 4 7 4 3 7 5 ...
##  $ num.photos        : int  29 16 50 51 40 24 41 38 47 15 ...
##  $ floor.plan        : logi  FALSE FALSE FALSE FALSE FALSE FALSE ...
##  $ view3d            : logi  FALSE FALSE TRUE FALSE FALSE FALSE ...
##  $ video             : logi  FALSE FALSE FALSE FALSE FALSE FALSE ...
##  $ home.staging      : logi  FALSE FALSE FALSE FALSE FALSE FALSE ...
##  $ description       : chr  "Maravillo ático ubicado en un edificio clásico del año 1923 recién rehabilitado, manteniendo sus originales pat"| __truncated__ "Fortuny Real Estate vende vivienda ubicada en un prestigioso y señorial edificio de mediados de siglo.\n\nSe tr"| __truncated__ "Uptown Real Estate presenta en exclusiva este espectacular ático a estrenar en la mejor ubicación de Recoletos,"| __truncated__ "CALLE VELAZQAUEZ JUNTO AL RETIRO, 3 BALCONES A LA CALLE.\nPropiedad a estrenar, situada en un edificio clásico "| __truncated__ ...
##  $ source            : Factor w/ 2 levels "fotocasa","idealista": 1 1 1 1 1 1 1 1 1 1 ...
##  $ price.m2          : num  8385 6034 10136 10914 9307 ...
##  $ description.length: int  758 1032 2605 1630 1191 659 3186 1048 1419 2317 ...

3 Limpieza de los datos

Tras haber seleccionado los datos útiles, el siguiente paso hacia el análisis consiste en la limpieza y normalización de los datos, aunque se descarta normalizar al no ser necesario en el tipo de pruebas previstas. En esta fase, es necesario gestionar los registros con valores perdidos o missing data, así como comprobar la distribución de ciertos tipos de datos (por ejemplo, el precio de la vivienda o los \(m^2\)) para inspeccionar la existencia de valores extremos o outliers. Sin embargo, tanto en la visualización de registros aleatorios del dataframe como en el estudio de la estructura, se ha detectado que hay campos que todavía deben tratarse mediante un proceso de transformación previa ya que su resultado puede afectar tanto a la gestión de missing data como de outliers.

3.1 Transformaciones previas

Varios de los campos del dataset son factores y pueden albergar valores ligeramente distintos que hagan referencia al mismo concepto. Se procede a comprobar los valores de estos campos para decidir sobre su necesidad de unificarlos:

levels(selected.houses$location)
## [1] "barrio-de-salamanca" "location"            "villaverde"
levels(selected.houses$floor)
##  [1] "11ª"                              "12ª"                             
##  [3] "1ª"                               "2ª"                              
##  [5] "3ª"                               "4ª"                              
##  [7] "5ª"                               "6ª"                              
##  [9] "7ª"                               "8ª"                              
## [11] "9ª"                               "floor"                           
## [13] "Planta -1 interior"               "Planta 10ª exterior con ascensor"
## [15] "Planta 11ª exterior con ascensor" "Planta 12ª"                      
## [17] "Planta 12ª exterior con ascensor" "Planta 13ª exterior con ascensor"
## [19] "Planta 1ª"                        "Planta 1ª con ascensor"          
## [21] "Planta 1ª exterior"               "Planta 1ª exterior con ascensor" 
## [23] "Planta 1ª exterior sin ascensor"  "Planta 1ª interior con ascensor" 
## [25] "Planta 1ª interior sin ascensor"  "Planta 1ª sin ascensor"          
## [27] "Planta 2ª"                        "Planta 2ª con ascensor"          
## [29] "Planta 2ª exterior"               "Planta 2ª exterior con ascensor" 
## [31] "Planta 2ª exterior sin ascensor"  "Planta 2ª interior"              
## [33] "Planta 2ª interior con ascensor"  "Planta 2ª interior sin ascensor" 
## [35] "Planta 2ª sin ascensor"           "Planta 3ª"                       
## [37] "Planta 3ª con ascensor"           "Planta 3ª exterior"              
## [39] "Planta 3ª exterior con ascensor"  "Planta 3ª exterior sin ascensor" 
## [41] "Planta 3ª interior con ascensor"  "Planta 3ª interior sin ascensor" 
## [43] "Planta 3ª sin ascensor"           "Planta 4ª"                       
## [45] "Planta 4ª con ascensor"           "Planta 4ª exterior"              
## [47] "Planta 4ª exterior con ascensor"  "Planta 4ª exterior sin ascensor" 
## [49] "Planta 4ª interior con ascensor"  "Planta 4ª interior sin ascensor" 
## [51] "Planta 4ª sin ascensor"           "Planta 5ª con ascensor"          
## [53] "Planta 5ª exterior"               "Planta 5ª exterior con ascensor" 
## [55] "Planta 5ª exterior sin ascensor"  "Planta 5ª interior con ascensor" 
## [57] "Planta 5ª sin ascensor"           "Planta 6ª exterior con ascensor" 
## [59] "Planta 6ª exterior sin ascensor"  "Planta 6ª interior con ascensor" 
## [61] "Planta 7ª exterior con ascensor"  "Planta 7ª exterior sin ascensor" 
## [63] "Planta 7ª interior con ascensor"  "Planta 8ª"                       
## [65] "Planta 8ª exterior con ascensor"  "Planta 8ª interior con ascensor" 
## [67] "Planta 9ª exterior con ascensor"  "Planta 9ª interior con ascensor" 
## [69] "Sin planta"
levels(selected.houses$source)
## [1] "fotocasa"  "idealista"

El campo source contiene solo los valores esperados. El campo location contiene un nivel incorrecto y el campo floor contiene información redundante en varios niveles, además de un nivel incorrecto. Se decide descartar la información de si la vivienda es interior o exterior y si posee o no ascensor contenida en floor, dejando como valor de floor su planta, cuya información será útil en futuros análisis.

Para ello, se modifica el campo floor para obtener únicamente la planta de la vivienda. Si la vivienda no tiene planta (el caso de “Sin planta” o “floor”), se le asigna la planta 0, conclusión a la que se llega tras la exploración de los datos y verificación por la información del campo description:

selected.houses <- selected.houses %>% 
  dplyr::mutate(floor=as.character(floor)) %>%
  dplyr::mutate(floor=ifelse(floor %in% c('Sin planta', 'floor'), '0', floor)) %>%
  dplyr::mutate(floor=readr::parse_number(floor)) %>% 
  dplyr::mutate(floor=as.factor(floor))
levels(selected.houses$floor)
##  [1] "-1" "0"  "1"  "2"  "3"  "4"  "5"  "6"  "7"  "8"  "9"  "10" "11" "12" "13"

3.2 Gestión de Missing Data

Se procede a realizar un análisis preliminar y superficial de los valores perdidos en todos los campos:

kable(colSums(is.na(selected.houses)),
      digits=2, 
      align='l', 
      caption="Valores nulos en cada variable") %>%
  kable_styling(bootstrap_options = Form.Basic) %>%
    scroll_box(width = "100%")
Valores nulos en cada variable
x
id 1
location 0
price 2
m2 1
rooms 2370
floor 0
num.photos 1
floor.plan 1
view3d 1
video 1
home.staging 1
description 0
source 0
price.m2 2
description.length 0

Como se puede observar, el campo rooms es el que tiene más valores perdidos, por lo que se tratará más adelante. Así, se visualizan los registros que contienen valores perdidos en las variables id, price, m2, num.photos, floor.plan, view3d, video, home.staging y price.m2:

selected.houses[is.na(selected.houses$id) | 
                  is.na(selected.houses$price) | 
                  is.na(selected.houses$m2) |
                  is.na(selected.houses$num.photos) |
                  is.na(selected.houses$floor.plan) |
                  is.na(selected.houses$view3d) |
                  is.na(selected.houses$video) |
                  is.na(selected.houses$home.staging) |
                  is.na(selected.houses$price.m2),]

Dadas las características de los registros (demasiados campos con valores perdidos y otros campos con valores incorrectos en el registro sin identificador) se decide eliminar los registros.

selected.houses <- selected.houses[!is.na(selected.houses$id) & 
                                     !is.na(selected.houses$price) &
                                     !is.na(selected.houses$m2) &
                                     !is.na(selected.houses$num.photos) &
                                     !is.na(selected.houses$floor.plan) &
                                     !is.na(selected.houses$view3d) &
                                     !is.na(selected.houses$video) &
                                     !is.na(selected.houses$home.staging) &
                                     !is.na(selected.houses$price.m2),]

Se procede ahora a analizar los registros con valores perdidos en el campo rooms y, para ello, se decide mostrar todos los registros en busca de un patrón:

selected.houses[is.na(selected.houses$rooms),]

Se trata de 2.369 registros que no tienen valor en el campo rooms, es decir, el 70% del total de registros. Sin embargo, se observa claramente que se trata de viviendas que proceden de Idealista. Se verifica dicha suposición:

if(length(which(is.na(selected.houses$rooms))) == 
   length(selected.houses$source[selected.houses$source == 'idealista'])){
  print(paste0("El número de viviendas sin valor en 'rooms' SÍ es ",
          "igual al de procedentes de Idealista."))
} else {
  print(paste0("El número de viviendas sin valor en 'rooms' NO es ",
          "igual al de procedentes de Idealista."))
}
## [1] "El número de viviendas sin valor en 'rooms' SÍ es igual al de procedentes de Idealista."

Por tanto, se decide mantener todos los registros sin valor en rooms y se tiene en cuenta para futuros análisis que si se quiere emplear esta variable solamente se podrán analizar las viviendas procedentes del portal web de Fotocasa. Otras opciones habrían contemplado eliminar el campo completamente del dataset, predecir el número de habitaciones dado el resto de características o imputarles un valor arbitrario, como 0.

Por último, se eliminan los niveles sin usar de los factores:

selected.houses <- droplevels.data.frame(selected.houses)

3.3 Gestión de outliers

Tras el trabajo que se ha realizado con los datos, se puede deducir que los valores extremos, o outliers, solo pueden encontrarse en las variables price, m2, rooms, floor (en este momento categórica), num.photos,price.m2 y description.length.

Como en la primera entrega de este proyecto (Práctica 1) ya se determinó que los distritos de Salamanca y Villaverde de Madrid se encuentran en los extremos de precios dentro de la ciudad según todas las fuentes consultadas, para no distorsionar el análisis de outliers, se opta por estudiar cada variable según la ubicación de la vivienda.

Para explorar cada uno de los casos, se realiza una aproximación gráfica a cada problema mediante boxplots o diagramas de caja interactivos que facilitan la identificación de los registros, comprensión e identificación, y se decide qué hacer con los candidatos a outliers.

En primer lugar, se analiza el precio:

plot_ly(y = ~ selected.houses$price, 
        color = ~ selected.houses$location, 
        colors = palette.binary,
        type = "box") %>%
  layout(yaxis = list(type = "log", 
                      title = "Precio en escala logarítmica"), 
         xaxis = list(title = "'Location' (distrito de la vivienda)"), 
         title = "Distribución del precio según el distrito")

Que haya viviendas caras hasta tal punto que su precio parezca un posible valor extremo equivocado no debe sorprender en este proyecto debido a que el mercado inmobiliario se caracteriza precisamente por ello. En cualquier caso, para verificar que estos precios son compatibles con la realidad, se opta por extraer los registros de las viviendas sospechosas:

villaverde.price <- selected.houses[selected.houses$price %in% 
                  boxplot.stats(selected.houses$price[selected.houses$location == 
                                                        'villaverde'])$out & 
                  selected.houses$location == 'villaverde' ,]

salamanca.price <- selected.houses[selected.houses$price %in% 
                  boxplot.stats(selected.houses$price[selected.houses$location == 
                                                        'barrio-de-salamanca'])$out & 
                  selected.houses$location == 'barrio-de-salamanca' ,]

Tras estudiar los 165 registros en profundidad, se descubre que en ambos distritos los viviendas consideradas atípicamente caras también son muy grandes para la zona en la que se encuentran. A modo de ejemplo, se muestran los metros cuadrados de las viviendas más pequeñas que son consideradas posibles outliers en cuanto a su precio:

min(villaverde.price$m2)
## [1] 88
min(salamanca.price$m2)
## [1] 209

Por tanto se decide no tratar dichos registros al encontrarse dentro del rango de lo posible en este contexto, como se puede deducir del tamaño mínimo de estas viviendas.

A continuación se analiza el precio por metro cuadrado:

plot_ly(y = ~ selected.houses$price.m2, 
        color = ~ selected.houses$location, 
        colors = palette.binary,
        type = "box") %>%
  layout(yaxis = list(type = "log", 
                      title = "Precio por metro cuadrado en escala logarítmica"), 
         xaxis = list(title = "'Location' (distrito de la vivienda)"), 
         title = "Distribución del precio por metro cuadrado según el distrito")

Como en el caso anterior, se extraen los registros de las viviendas que tienen un precio atípicamente alto o bajo en relación con sus metros cuadradados para cada uno de los distritos de Madrid analizados:

villaverde.price.m2 <- selected.houses[selected.houses$price.m2 %in% 
                  boxplot.stats(selected.houses$price.m2[selected.houses$location == 
                                                           'villaverde'])$out & 
                  selected.houses$location == 'villaverde' ,]

salamanca.price.m2 <- selected.houses[selected.houses$price.m2 %in% 
                  boxplot.stats(selected.houses$price.m2[selected.houses$location == 
                                                           'barrio-de-salamanca'])$out & 
                  selected.houses$location == 'barrio-de-salamanca' ,]

Tras estudiar los 51 registros en profundidad, no se encuentra un patrón que pudiera explicar esta situación y los valores del resto de campos son coherentes con el ámbito de este proyecto. Si embargo, sí que se detecta que las tres viviendas con un precio más bajo por metro cuadradado en Villaverde no son tal, sino que se trata de parcelas según se explica en el campo description, por lo que se procede a su eliminación:

selected.houses[(selected.houses$id == "162471798") | 
                  (selected.houses$id == "159999857") | 
                  (selected.houses$id == "162471885"),]
selected.houses <- subset(selected.houses, 
                          id != "162471798" & 
                            id != "159999857" & 
                            id !="162471885")

Acabado el análisis sobre el precio, se pasa a estudiar el tamaño:

plot_ly(y = ~ selected.houses$m2, 
        color = ~ selected.houses$location, 
        colors = palette.binary,
        type = "box") %>%
  layout(yaxis = list( title = "Metros cuadrados"), 
         xaxis = list(title = "'Location' (distrito de la vivienda)"), 
         title = "Distribución de los metros cuadrados según el distrito")

También se extraen los registros de las viviendas que tienen un tamaño atípicamente alto para cada uno de los distritos:

villaverde.m2 <- selected.houses[selected.houses$m2 %in% 
                  boxplot.stats(selected.houses$m2[selected.houses$location == 
                                                     'villaverde'])$out & 
                  selected.houses$location == 'villaverde' ,]

salamanca.m2 <- selected.houses[selected.houses$m2 %in% 
                  boxplot.stats(selected.houses$m2[selected.houses$location == 
                                                     'barrio-de-salamanca'])$out & 
                  selected.houses$location == 'barrio-de-salamanca' ,]

Tras analizar los 122 registros no se encuentra ninguna anomalía que haga sospechar de un error en los datos y el tamaño por encima de lo esperado en la zona se debe simplemente a que se trata de viviendas grandes. De hecho, en las descripciones de varias de ellas se detalla que se pueden dividir en varias viviendas o el número de plantas que tiene dentro.

El siguiente paso es analizar el número de habitaciones teniendo en cuenta que este dato procede únicamente de las viviendas anunciadas en Fotocasa:

plot_ly(y = ~ selected.houses$rooms, 
        color = ~ selected.houses$location, 
        colors = palette.binary,
        type = "box") %>%
  layout(yaxis = list( title = "Número de habitaciones"), 
         xaxis = list(title = "'Location' (distrito de la vivienda)"), 
         title = "Distribución del número de habitaciones según el distrito")

Se sigue el mismo procedimiento que antes, analizando por separado los posibles outliers pero, como era de esperar, las 4 viviendas con más habitaciones también son más grandes, por lo que se decide no tratar estos registros.

villaverde.rooms <- selected.houses[selected.houses$rooms %in% 
                  boxplot.stats(selected.houses$rooms[selected.houses$location == 
                                                        'villaverde'])$out & 
                  selected.houses$location == 'villaverde' ,]

salamanca.rooms <- selected.houses[selected.houses$rooms %in% 
                  boxplot.stats(selected.houses$rooms[selected.houses$location == 
                                                        'barrio-de-salamanca'])$out & 
                  selected.houses$location == 'barrio-de-salamanca' ,]

A continuación se visualizan los candidatos a outliers según la planta en la que se encuentra la vivienda:

plot_ly(y = ~ selected.houses$floor, 
        color = ~ selected.houses$location, 
        colors = palette.binary,
        type = "box") %>%
  layout(yaxis = list(title = "Planta"), 
         xaxis = list(title = "'Location' (distrito de la vivienda)"), 
         title = "Distribución de la planta según el distrito")

Según los datos del Catastro en el distrito de Salamanca de Madrid existen edificios de 12 alturas y en el de Villaverde de 13, e incluso más en ambos casos, aunque no es lo más común. Por tanto, cabe la posibilidad de que las viviendas en venta se encuentren en dichas plantas, por lo que no es necesario profundizar en el análisis de estos datos o los registros completos.

En cuanto al número de fotografías, los boxplots resultantes son los siguientes:

plot_ly(y = ~ selected.houses$num.photos, 
        color = ~ selected.houses$location, 
        colors = palette.binary,
        type = "box") %>%
  layout(yaxis = list(title = "Número de fotografías"), 
         xaxis = list(title = "'Location' (distrito de la vivienda)"), 
         title = "Distribución del número de fotografías según el distrito")

Si bien es cierto que el número máximo de fotografías en Fotocasa es de 30, este límite asciende hasta 200 en Idealista, por lo que nada hace determinar que los valores atípicos sean indicativos de errores en el proceso de captura de los anuncios de las viviendas. De hecho, el anuncio con 194 imágenes corresponde al de una vivienda de casi 3 millones de euros, por lo que tiene sentido que sea el más completo de todos en este aspecto:

selected.houses[(selected.houses$num.photos = 194),]

Por último, se evalúa la variable que recoge la longitud de la descripción en caracteres:

plot_ly(y = ~ selected.houses$description.length, 
        color = ~ selected.houses$location, 
        colors = palette.binary,
        type = "box") %>%
  layout(yaxis = list(title = "Longitud de la descripción"), 
         xaxis = list(title = "'Location' (distrito de la vivienda)"), 
         title = "Distribución de la longitud de la descripción según el distrito")

Como según la normativa de Fotocasa el máximo de caracteres es de 4.000 y la de Idealista sube este máximo hasta los 5.000 caracteres, los marcados como posibles outliers se consideran registros dentro de lo posible, por lo que se decide mantenerlos sin actuar sobre ellos.

Este apartado ha sido útil para descartar la eliminación de posibles outliers y verificar la presunta corrección de los datos, excepto tres viviendas que en realidad no lo eran. Además, ha servido para tener una primera aproximación sobre las características de las viviendas en función de su localización. Así, aquellas situadas en el distrito de Salamanca de Madrid tienden a ser más caras en valores absolutos y relativos, más grandes, con más habitaciones y con anuncios más completos (en número de caracteres y fotografías) que las ubicadas en el distrito de Villaverde de la capital. En cualquier caso, en el apartado de Pruebas estadísticas se llegará a conclusiones más precisas.

3.4 Conjunto de datos final

Tras este proceso de data cleaning o limpieza de datos, el conjunto de datos final tiene las siguientes características:

options(knitr.kable.NA = '')
kable(summary(selected.houses),
      digits=2, 
      align='l', 
      caption="Datos resumen de cada variable") %>%
  kable_styling(bootstrap_options = Form.Basic) %>%
    scroll_box(width = "100%")
Datos resumen de cada variable
id location price m2 rooms floor num.photos floor.plan view3d video home.staging description source price.m2 description.length
Min. : 305727 barrio-de-salamanca:2523 Min. : 43000 Min. : 16 Min. : 1 1 :696 Min. :194 Mode :logical Mode :logical Mode :logical Mode :logical Length:3356 fotocasa : 987 Min. : 758 Min. : 12
1st Qu.: 95621531 villaverde : 833 1st Qu.: 244750 1st Qu.: 70 1st Qu.: 2 2 :628 1st Qu.:194 FALSE:2367 FALSE:3079 FALSE:2827 FALSE:3323 Class :character idealista:2369 1st Qu.: 3071 1st Qu.: 658
Median : 96925385 Median : 650000 Median :106 Median : 3 3 :560 Median :194 TRUE :989 TRUE :277 TRUE :529 TRUE :33 Mode :character Median : 6158 Median : 987
Mean :113824515 Mean : 1003208 Mean :144 Mean : 3 0 :451 Mean :194 Mean : 6021 Mean :1177
3rd Qu.:160123187 3rd Qu.: 1395000 3rd Qu.:185 3rd Qu.: 4 4 :362 3rd Qu.:194 3rd Qu.: 8365 3rd Qu.:1523
Max. :163163399 Max. :15000000 Max. :885 Max. :14 5 :339 Max. :194 Max. :25000 Max. :4632
NA’s :2369 (Other):320

Por último, se guarda el conjunto de datos final en un nuevo fichero, que también estará disponible en el repositorio público:

write.csv(selected.houses, 
          file = '../data/real-estate.csv',
          row.names = FALSE,
          sep=',')

4 Análisis de los datos

Tras haber seleccionado y limpiado los datos útiles, el siguiente paso consiste en la selección de los grupos de datos y el propio análisis estadístico. En esta fase se particiona el conjunto de datos final según diferentes filtros, ya sea por distrito, precio, altura de la planta en que se encuentran, etcétera.

Después de seleccionar los grupos de datos relevantes y antes de realizar las pruebas estadísticas, es necesario comprobar la normalidad y homocedasticidad de dichos datos, con el objetivo de escoger adecuadamente los tests estadísticos que aplicar en cada prueba. Por último, se ha de proceder a escoger las pruebas estadísticas que respondan a las preguntas planteadas.

4.1 Selección de grupos de datos

Teniendo en cuenta los objetivos marcados en la introducción y el resultado de las exploraciones realizadas en apartados anteriores, a continuación se agrupan las viviendas según el distrito:

villaverde.houses <- selected.houses[selected.houses$location == "villaverde",]
salamanca.houses <- selected.houses[selected.houses$location == "barrio-de-salamanca",]

Además, se crean nuevas variables que se emplearán en nuevas agrupaciones para facilitar la realización de pruebas estadísticas que puedan llevar a conclusiones interesantes. En este proyecto no se emplearán todas las agrupaciones necesariamente, pero quedará preparado para ampliar su alcance en el futuro.

Así, se dividen los datos según si el precio por metro cuadrado (price.m2) de las viviendas es caro o barato teniendo en cuenta si está por encima o debajo de la media general o del distrito, según el caso:

selected.houses$price.clasification <- as.factor(
  case_when(selected.houses$price.m2 <= mean(selected.houses$price.m2) ~ "barato",    
            selected.houses$price.m2 > mean(selected.houses$price.m2)  ~ "caro")) 

villaverde.houses$price.clasification <- as.factor(
  case_when(villaverde.houses$price.m2 <= mean(villaverde.houses$price.m2) ~ "barato",    
            villaverde.houses$price.m2 > mean(villaverde.houses$price.m2)  ~ "caro")) 

salamanca.houses$price.clasification <- as.factor(
  case_when(salamanca.houses$price.m2 <= mean(salamanca.houses$price.m2) ~ "barato",
            salamanca.houses$price.m2 > mean(salamanca.houses$price.m2)  ~ "caro")) 

Se dividen los datos según si el el tamaño (m2) de las viviendas es pequeño o grande atendiendo a si está por encima o debajo de la media general o del distrito, según el caso:

selected.houses$m2.clasification <- as.factor(case_when(
                          selected.houses$m2 <= mean(selected.houses$m2) ~ "pequeño",    
                          selected.houses$m2 > mean(selected.houses$m2)  ~ "grande"
                          )) 

villaverde.houses$m2.clasification <- as.factor(case_when(
                          villaverde.houses$m2 <= mean(villaverde.houses$m2) ~ "pequeño",    
                          villaverde.houses$m2 > mean(villaverde.houses$m2)  ~ "grande"
                          )) 

salamanca.houses$m2.clasification <- as.factor(case_when(
                          salamanca.houses$m2 <= mean(salamanca.houses$m2) ~ "pequeño",    
                          salamanca.houses$m2 > mean(salamanca.houses$m2)  ~ "grande"
                          )) 

Se dividen los datos según si la altura (floor) puede considerarse alta o baja. Aunque la Ley de Propiedad Horizontal no deja claro una altura a partir la cual el ascensor sí que es obligatorio, del texto emana que hasta un cuarto piso puede ser tolerable su ausencia mientras que en otros países, como Perú, es obligatorio a partir de la quinta planta incluida. Por tanto, se decide que el cuarto sea el umbral para que un piso sea considerado “bajo” a efectos de este proyecto:

selected.houses$height.clasification <- as.factor(case_when(
                          as.numeric(selected.houses$floor) <= 4 ~ "bajo",    
                          as.numeric(selected.houses$floor) > 4  ~ "alto"
                          )) 

villaverde.houses$height.clasification <- as.factor(case_when(
                          as.numeric(villaverde.houses$floor) <= 4 ~ "bajo",    
                          as.numeric(villaverde.houses$floor) > 4  ~ "alto"
                          )) 

salamanca.houses$height.clasification <- as.factor(case_when(
                          as.numeric(salamanca.houses$floor) <= 4 ~ "bajo",    
                          as.numeric(salamanca.houses$floor) > 4 ~ "alto"
                          )) 

Gracias a las variables creadas, se realizan nuevas agrupaciones de manera que las viviendas se agrupan además de por su ubicación, en caras o baratas, grandes o pequeñas y altas o bajas teniendo como referencia el distrito en el que se encuentren. Así, las viviendas con precio por metro cuadrado por encima de su media del distrito de Salamanca y los de Villaverde estarán agrupados juntos, por ejemplo, de manera que se evitan sesgos:

# Viviendas caras y baratas
cheap.houses <- bind_rows(
  villaverde.houses[villaverde.houses$price.clasification == "barato",],
  salamanca.houses[salamanca.houses$price.clasification == "barato",])

expensive.houses <- bind_rows(
  villaverde.houses[villaverde.houses$price.clasification == "caro",],
  salamanca.houses[salamanca.houses$price.clasification == "caro",])

# Viviendas pequeñas y grandes
small.houses <- bind_rows(
  villaverde.houses[villaverde.houses$m2.clasification == "pequeño",], 
  salamanca.houses[salamanca.houses$m2.clasification == "pequeño",])

big.houses <- bind_rows(
  villaverde.houses[villaverde.houses$m2.clasification == "grande",], 
  salamanca.houses[salamanca.houses$m2.clasification == "grande",])

# Viviendas altas y bajas
lower.floors.villaverde.houses <- 
  villaverde.houses[villaverde.houses$height.clasification == "bajo",]
upper.floors.villaverde.houses <- 
  villaverde.houses[villaverde.houses$height.clasification == "alto",]

lower.floors.salamanca.houses <- 
  salamanca.houses[salamanca.houses$height.clasification == "bajo",]
upper.floors.salamanca.houses <- 
  salamanca.houses[salamanca.houses$height.clasification == "alto",]

lower.floors.houses <- bind_rows(lower.floors.villaverde.houses, 
                                 lower.floors.salamanca.houses)
upper.floors.houses <- bind_rows(upper.floors.villaverde.houses, 
                                 upper.floors.salamanca.houses)

4.2 Comprobación de normalidad y homocedasticidad

Se procede a comprobar si las variables cuantitativas price, m2 y price.m2 cumplen los supuestos de normalidad. A su vez, también se comprobará si el precio por \(m^2\) cumple el supuesto de homocedasticidad para las viviendas agrupadas por distrito y las viviendas agrupadas por distrito y altura.

4.2.1 Normalidad de price

Se procede a realizar las comprobaciones de normalidad en la variable price, utilizando el test de Lilliefors y la ayuda de gráficas auxiliares.

price.n <- length(selected.houses$price)
price.mean <- mean(selected.houses$price)
price.sd <- sd(selected.houses$price)

ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.75),
          ggplot(selected.houses, aes(x=price)) +
            geom_density(mapping=aes(y=..density..), fill=default.color.main) +
            geom_vline(xintercept=price.mean, size=1.05,
                       linetype='dashed', color='gray50') +
            stat_function(fun=dnorm, args=c(mean=price.mean,
                                            sd=price.sd),
                          color=default.color.secondary, size=1.15) +
            no.axis.y + xlab('Precio') + ylab('') + title.centered +
            ggtitle('Distribución de los precios de las viviendas', 
                    subtitle='Respecto a una distribución normal'),
          
          ggqqplot(selected.houses$price, color=default.color.main, 
                   ggtheme = theme_gray(), xlab='Cuantiles teóricos', 
                   ylab='Cuantiles de la muestra', title='Gráfico Q-Q', 
                   shape=16) + title.centered + xlab('') + ylab('')
          )

lillie.test(selected.houses$price)
## 
##  Lilliefors (Kolmogorov-Smirnov) normality test
## 
## data:  selected.houses$price
## D = 0.2, p-value <0.0000000000000002

Las gráficas muestran que el precio de las viviendas no sigue una distribución normal; con el test de normalidad de Lilliefors se confirma que la distribución de la variable price (precio) no es normal (p-value < \(\alpha\) = 0.05). Sin embargo, la variable precio parece seguir una distribución log-normal; es decir, el logaritmo del precio puede seguir una distribución normal. Se procede a comprobar dicha hipótesis:

selected.houses <- selected.houses %>% plyr::mutate(price.log = log(price))
price.log.n <- length(selected.houses$price.log)
price.log.mean <- mean(selected.houses$price.log)
price.log.sd <- sd(selected.houses$price.log)

ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.75),
          ggplot(selected.houses, aes(x=price.log)) +
            geom_density(mapping=aes(y=..density..), fill=default.color.main) +
            geom_vline(xintercept=price.log.mean, size=1.05,
                       linetype='dashed', color='gray50') +
            stat_function(fun=dnorm, args=c(mean=price.log.mean,
                                            sd=price.log.sd),
                          color=default.color.secondary, size=1.15) +
            no.axis.y + xlab('Log(Precio)') + ylab('') + title.centered +
            ggtitle(expression('Distribución de los precios de las viviendas'), 
                    subtitle='Respecto a una distribución normal'),
          
          ggqqplot(selected.houses$price.log, color=default.color.main, 
                   ggtheme = theme_gray(), xlab='Cuantiles teóricos', 
                   ylab='Cuantiles de la muestra', title = expression('Gráfico Q-Q')) + 
                   title.centered + xlab('') + ylab('')
          )

lillie.test(selected.houses$price.log)
## 
##  Lilliefors (Kolmogorov-Smirnov) normality test
## 
## data:  selected.houses$price.log
## D = 0.06, p-value <0.0000000000000002

La variable precio tampoco sigue una distribución log-normal, según se aprecia tanto en los gráficos (colas muy pesadas en el qqplot) como en el test de Lilliefors (p-value < \(\alpha\) = 0.05). Se concluye por tanto que la variable precio no sigue una distribución normal.

4.2.2 Normalidad de m2

Se procede a realizar las comprobaciones de normalidad en la variable m2, utilizando el test de Lilliefors y la ayuda de gráficas auxiliares.

m2.n <- length(selected.houses$m2)
m2.mean <- mean(selected.houses$m2)
m2.sd <- sd(selected.houses$m2)

ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.75),
          ggplot(selected.houses, aes(x=m2)) +
            geom_density(mapping=aes(y=..density..), fill=default.color.main) +
            geom_vline(xintercept=m2.mean, size=1.05,
                       linetype='dashed', color='gray50') +
            stat_function(fun=dnorm, args=c(mean=m2.mean,
                                            sd=m2.sd),
                          color=default.color.secondary, size=1.15) +
            no.axis.y + xlab(expression('m'^2)) + ylab('') + title.centered +
            ggtitle(expression('Distribución de las viviendas según m'^2), 
                    subtitle='Respecto a una distribución normal'),
          
          ggqqplot(selected.houses$m2, color=default.color.main, 
                   ggtheme = theme_gray(), xlab='Cuantiles teóricos', 
                   ylab='Cuantiles de la muestra', title='Gráfico Q-Q', 
                   shape=16) + title.centered + xlab('') + ylab('')
          )

lillie.test(selected.houses$m2)
## 
##  Lilliefors (Kolmogorov-Smirnov) normality test
## 
## data:  selected.houses$m2
## D = 0.2, p-value <0.0000000000000002

Las gráficas muestran que la superficie (\(m^2\)) de las viviendas no sigue una distribución normal; con el test de normalidad de Lilliefors se confirma que la distribución de la variable m2 no es normal (p-value < \(\alpha\) = 0.05). Se concluye por tanto que la variable m2 no sigue una distribución normal.

4.2.3 Normalidad de price.m2

Se procede a realizar las comprobaciones de normalidad en la variable price.m2, utilizando el test de Lilliefors y la ayuda de gráficas auxiliares.

price.m2.n <- length(selected.houses$price.m2)
price.m2.mean <- mean(selected.houses$price.m2)
price.m2.sd <- sd(selected.houses$price.m2)

ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.75),
          ggplot(selected.houses, aes(x=price.m2)) +
            geom_density(mapping=aes(y=..density..), fill=default.color.main) +
            geom_vline(xintercept=price.m2.mean, size=1.05,
                       linetype='dashed', color='gray50') +
            stat_function(fun=dnorm, args=c(mean=price.m2.mean,
                                            sd=price.m2.sd),
                          color=default.color.secondary, size=1.15) +
            no.axis.y + xlab(expression('Precio/m'^2)) + ylab('') + title.centered +
            ggtitle(expression('Distribución de los precios de las viviendas'), 
                    subtitle='Respecto a una distribución normal'),
          
          ggqqplot(selected.houses$price.m2, color=default.color.main, 
                   ggtheme = theme_gray(), xlab='Cuantiles teóricos', 
                   ylab='Cuantiles de la muestra', title=(expression('Gráfico Q-Q'))) + 
            title.centered + xlab('') + ylab('')
          )

lillie.test(selected.houses$price.m2)
## 
##  Lilliefors (Kolmogorov-Smirnov) normality test
## 
## data:  selected.houses$price.m2
## D = 0.09, p-value <0.0000000000000002

Las gráficas muestran que el precio por metro cuadrado de las viviendas no sigue una distribución normal; con el test de normalidad de Lilliefors se confirma que la distribución de la variable price.m2 no es normal (p-value < \(\alpha\) = 0.05). Se concluye por tanto que la variable price.m2 no sigue una distribución normal.

4.2.4 Homocedasticidad de price.m2 por distritos

Se procede a comprobar la homocedasticidad (también llamada homogeneidad de la varianza) del precio por metro cuadrado entre los distritos de Salamanca y Villaverde. Como los datos no siguen una distribución normal, se ha de utilizar el test de Fligner-Killeen, la alternativa no paramétrica al test de Levene.

fligner.test(price.m2 ~ location, data=selected.houses)
## 
##  Fligner-Killeen test of homogeneity of variances
## 
## data:  price.m2 by location
## Fligner-Killeen:med chi-squared = 842, df = 1, p-value
## <0.0000000000000002

El test de Fligner-Killeen produce un p-value < \(\alpha\) = 0.05; por tanto, se rechaza la hipótesis nula de homocedasticidad y se concluye que el precio por metro cuadrado tiene varianzas estadísticamente diferentes para ambos distritos.

4.2.5 Homocedasticidad de price.m2 por distritos y alturas

Se procede a comprobar la homocedasticidad (también llamada homogeneidad de la varianza) del precio por metro cuadrado entre los distritos de Salamanca y Villaverde, según la altura a la que está situada la vivienda. Como los datos no siguen una distribución normal, se ha de utilizar el test de Fligner-Killeen, la alternativa no paramétrica al test de Levene.

selected.houses.location.height <- selected.houses %>%
  dplyr::mutate(location.height.clasification=as.factor(
    paste0(as.character(location),'-',as.character(height.clasification))))

fligner.test(price.m2 ~ location.height.clasification, data=selected.houses.location.height)
## 
##  Fligner-Killeen test of homogeneity of variances
## 
## data:  price.m2 by location.height.clasification
## Fligner-Killeen:med chi-squared = 875, df = 3, p-value
## <0.0000000000000002

El test de Fligner-Killeen produce un p-value < \(\alpha\) = 0.05; por tanto, se rechaza la hipótesis nula de homocedasticidad y se concluye que el precio por metro cuadrado tiene varianzas estadísticamente diferentes para ambos distritos y criterios de altura.

4.3 Pruebas estadísticas

Tras comprobar que los datos no siguen una distribución normal y no tienen varianzas homogéneas, se concluye que se ha de elegir el test no paramétrico para las pruebas estadísticas correspondientes. Sin embargo, dado que el conjunto de datos tiene un tamaño considerable y las muestras son grandes (n > 30), también se ha de considerar la aplicación del Teorema del Límite Central (TLC), que asume una distribución aproximadamente normal y permite el uso de tests paramétricos en las pruebas estadísticas.

4.3.1 ¿Existen diferencias en el precio de la vivienda según el distrito?

Desde la concepción de este proyecto se trabajó sobre la premisa de que la mayor amplitud de diferencia de precios en la ciudad de Madrid se daba entre el distrito de Salamanca y el de Villaverde. Sin embargo, todas las fuentes consultadas evalúan los precios en valores absolutos y, en ese sentido, cabe preguntarse si existe tal diferencia en valores relativos respecto al tamaño de las viviendas.

Por tanto, la pregunta de investigación es si existe una diferencia significativa en el precio por metro cuadrado entre las viviendas de ambos distritos. En consecuencia, se plantean las siguientes hipótesis nula y alternativa:

  • H\(_0\) : \(\mu\) precio por m\(^2\) B. Salamanca = \(\mu\) precio m\(^2\) B. Villaverde
  • H\(_1\) : \(\mu\) precio por m\(^2\) B. Salamanca > \(\mu\) precio m\(^2\) B. Villaverde

En primer lugar se observa la distribución de los datos por grupos:

GGally::ggpairs(dplyr::select(selected.houses, price.m2, location), 
                mapping=aes(fill=factor(location)),
                columns=c('price.m2', 'location'), proportions=c(1.5,1),
                title='Distribución del precio/m2 por distrito', legend=4,
                diag=list(continuous = wrap("densityDiag", alpha = 0.75))) +
  scale_colour_manual(name='Distrito', values=palette) +
  scale_fill_manual(name='Distrito', values=palette) +
  theme(legend.position='bottom') + title.centered

El histograma, el gráfico de densidad y el boxplot (cuyo interactivo se puede explorar en Gestión de outliers) muestran que el precio por metro cuadrado en el distrito de Salamanca es superior al precio por metro cuadrado del distrito de Villaverde.

Como ya se ha estudiado anteriormente, la variable de price.m2 no sigue una distribución normal y se da heterocedasticidad. Sin embargo, como el tamaño de las muestras es grande, y aplicando el teorema del límite central (TLC), se puede asumir en ambos casos una distribución aproximadamente normal.

En consecuencia, se puede realizar un test de hipótesis de dos muestras independientes sobre la media, con distribución normal, unilateral por la derecha y teniendo en cuenta que las varianzas poblacionales son desconocidas y diferentes. Por ello, se opta en primer lugar por la prueba t de Student:

t.test(salamanca.houses$price.m2,
       villaverde.houses$price.m2,
       alternative = "greater")
## 
##  Welch Two Sample t-test
## 
## data:  salamanca.houses$price.m2 and villaverde.houses$price.m2
## t = 110, df = 3075, p-value <0.0000000000000002
## alternative hypothesis: true difference in means is greater than 0
## 95 percent confidence interval:
##  5437  Inf
## sample estimates:
## mean of x mean of y 
##      7391      1871

Como era previsible el valor del p-value es prácticamente 0, muy por debajo de \(\alpha\) = 0.05, por lo que se rechaza la hipótesis nula en favor de la hipótesis alternativa.

No obstante, si no se hubiera aplicado el teorema del límite central y se hubiera atendido al resultado del test de Lilliefors, se habría tenido que aplicar un test no paramétrico. Para verificar la calidad del resultado anterior se realiza el test de Mann-Whitney:

wilcox.test(salamanca.houses$price.m2, 
            villaverde.houses$price.m2,
            alternative = "greater")
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  salamanca.houses$price.m2 and villaverde.houses$price.m2
## W = 2096431, p-value <0.0000000000000002
## alternative hypothesis: true location shift is greater than 0

El resultado del p-value es exactamente el mismo, por lo que se corrobora la decisión de descartar la hipótesis nula y aceptar la hipótesis alternativa.

En conclusión, se descarta que el precio por metro cuadrado de las viviendas en ambos distritos sea el mismo y se determina que este valor es significativamente mayor en el distrito de Salamanca que en el de Villaverde, con un nivel de confianza del 95% en el método.

4.3.2 ¿Qué características influyen más en el precio de la vivienda?

Se desea saber cuáles de todas las características influye más en el precio fijado para la vivienda en el anuncio. Estas características pueden ser tanto de la propia vivienda (m\(^2\) o altura) como las características del anuncio (el número de fotos o la longitud de la descripción).

Se procede a realizar un estudio de las correlaciones entre la variable price y el resto de variables numéricas (m2, price.m2, description.length). No se utiliza num.photos dado que no todos los registros tienen valores para este campo; sin embargo, se utilizará más adelante para otros propósitos.

Se utiliza el coeficiente de correlación de Spearman, dado que se trata de un test no paramétrico y es más adecuado para los datos a analizar debido a que no siguen una distribución normal. Sin embargo, también se van a realizar las pruebas con el coeficiente de correlación de Pearson, ya que se puede aplicar el teorema del límite central (TLC) sobre la muestra al tener un tamaño suficientemente grande.

corr.houses <- selected.houses %>% dplyr::select(price, m2, price.m2, description.length) %>%
  dplyr::rename(desc.len=description.length)

# Tabla de correlaciones
# as.data.frame(cor(corr.houses, use='complete.obs', method='spearman'))
# as.data.frame(cor(corr.houses, use='complete.obs', method='pearson'))

# Test
price.m2.spearman <- cor.test(corr.houses$price, corr.houses$m2, method='spearman')
price.m2.pearson <- cor.test(corr.houses$price, corr.houses$m2, method='pearson')
price.desc.len.spearman <-cor.test(corr.houses$price, corr.houses$desc.len, method='spearman')
price.desc.len.pearson <- cor.test(corr.houses$price, corr.houses$desc.len, method='pearson')
price.price.m2.spearman <-cor.test(corr.houses$price, corr.houses$price.m2, method='spearman')
price.price.m2.pearson <- cor.test(corr.houses$price, corr.houses$price.m2, method='pearson')
price.m2.m2.spearman <-cor.test(corr.houses$price.m2, corr.houses$m2, method='spearman')
price.m2.m2.pearson <- cor.test(corr.houses$price.m2, corr.houses$m2, method='pearson')

# Tabla de resultados
results.table <- data.frame(Variables = c("Price y m2", 
                                           "Price y description length", 
                                           "Price y price.m2",
                                           "Price.m2 y m2"), 
                                  Correlación.Pearson = c(price.m2.pearson$estimate,
                                            price.desc.len.pearson$estimate,
                                            price.price.m2.pearson$estimate,
                                            price.m2.m2.pearson$estimate), 
                            Pvalue.Pearson = c(format(price.m2.pearson$p.value, scientific = TRUE),
                                            format(price.desc.len.pearson$p.value,scientific = TRUE),
                                            format(price.price.m2.pearson$p.value,scientific = TRUE),
                                            format(price.m2.m2.pearson$p.value,scientific = TRUE)),
                                  Correlación.Spearman= c(price.m2.spearman$estimate,
                                            price.desc.len.spearman$estimate,
                                            price.price.m2.spearman$estimate,
                                            price.m2.m2.spearman$estimate), 
                            Pvalue.Spearman = c(format(price.m2.spearman$p.value,scientific = TRUE),
                                            format(price.desc.len.spearman$p.value, scientific = TRUE),
                                            format(price.price.m2.spearman$p.value,scientific = TRUE),
                                            format(price.m2.m2.spearman$p.value,scientific = TRUE)))
results.table %>% 
  kable() %>% 
    kable_styling(bootstrap_options = Form.Basic) %>%
  kableExtra::scroll_box(width = "100%")
Variables Correlación.Pearson Pvalue.Pearson Correlación.Spearman Pvalue.Spearman
Price y m2 0.89 0e+00 0.85 0e+00
Price y description length 0.28 1.2e-59 0.36 5e-103
Price y price.m2 0.65 0e+00 0.80 0e+00
Price.m2 y m2 0.40 2.7e-130 0.41 4.6e-134
# Correlograma
spearman <- ggcorr(corr.houses, 
                   method=c('complete.obs', 'spearman'), 
                   name='Corr. coef.',
                   label=TRUE, 
                   label_alpha=TRUE, 
                   legend.position='left') +
  title.centered + ggtitle('Matriz de correlaciones (Spearman)') +
  theme(legend.title=element_text(face='bold', size=10))

pearson <- ggcorr(corr.houses, 
                  method=c('complete.obs', 'pearson'), 
                  name='Corr. coef.',
                  label=TRUE, label_alpha=TRUE, 
                  legend.position='left') +
  title.centered + ggtitle('Matriz de correlaciones (Pearson)') +
  theme(legend.title=element_text(face='bold', size=10))

ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.5),
          pearson, spearman)

Todos los resultados se han contrastado mediante un contraste de hipótesis para comprobar su nivel de significancia; con un \(\alpha = 0.05\) se puede afirmar que los resultados son estadísticamente significativos tanto con tests paramétricos (coeficiente de correlación de Pearson) como con tests no paramétricos (coeficiente de correlación de Spearman).

Se observa que los m\(^2\) de la vivienda son los que más correlación tienen con el precio de la vivienda. La longitud de la descripción no tiene una relación lineal con el precio; mientras que el precio por m\(^2\) sí que tiene una fuerte correlación con el precio. Este último resultado era esperado, dado que price.m2 es una variable derivada de price y m2.

Otras conclusiones interesantes que arroja la matriz de correlaciones son las relaciones entre el precio por m\(^2\), el precio y los m\(^2\). Mientras que se esperaba que la correlación entre price y price.m2 fuera fuertemente positiva, sorprende la débil correlación positiva entre price.m2 y m2, que viene a significar que cuantos más metros cuadrados tenga la vivienda, mayor será el precio por m\(^2\). Esto quiere decir que el precio/m\(^2\) de una vivienda también se ve afectado por los m\(^2\) de la misma de forma positiva; es decir, cuanto más grande sea la vivienda, más caro será el m\(^2\) de la misma.

Sin embargo, estos son resultados generales y, puesto que ya se ha determinado que existen diferencias entre ambos distritos y, de hecho, ese es el interés principal de este proyecto, se procede a realizar el mismo estudio por separado. No se muestra el código por razones de claridad y espacio, pero sí los resultados. En primer lugar se trabaja sobre el distrito de Salamanca:

corr.salamanca.houses <- salamanca.houses %>% dplyr::select(price, m2, price.m2, description.length) %>%
  dplyr::rename(desc.len=description.length)

# Tabla de correlaciones
# as.data.frame(cor(corr.salamanca.houses, use='complete.obs', method='spearman'))
# as.data.frame(cor(corr.salamanca.houses, use='complete.obs', method='pearson'))

# Test
price.m2.spearman <- cor.test(corr.salamanca.houses$price, corr.salamanca.houses$m2, method='spearman')
price.m2.pearson <- cor.test(corr.salamanca.houses$price, corr.salamanca.houses$m2, method='pearson')
price.desc.len.spearman <-cor.test(corr.salamanca.houses$price, corr.salamanca.houses$desc.len, method='spearman')
price.desc.len.pearson <- cor.test(corr.salamanca.houses$price, corr.salamanca.houses$desc.len, method='pearson')
price.price.m2.spearman <-cor.test(corr.salamanca.houses$price, corr.salamanca.houses$price.m2, method='spearman')
price.price.m2.pearson <- cor.test(corr.salamanca.houses$price, corr.salamanca.houses$price.m2, method='pearson')
price.m2.m2.spearman <-cor.test(corr.salamanca.houses$price.m2, corr.salamanca.houses$m2, method='spearman')
price.m2.m2.pearson <- cor.test(corr.salamanca.houses$price.m2, corr.salamanca.houses$m2, method='pearson')

# Tabla de resultados
results.table.salamanca <- data.frame(Variables = c("Price y m2", 
                                           "Price y description length", 
                                           "Price y price.m2",
                                           "Price.m2 y m2"), 
                                  Correlación.Pearson = c(price.m2.pearson$estimate,
                                            price.desc.len.pearson$estimate,
                                            price.price.m2.pearson$estimate,
                                            price.m2.m2.pearson$estimate), 
                            Pvalue.Pearson = c(format(price.m2.pearson$p.value, scientific = TRUE),
                                            format(price.desc.len.pearson$p.value,scientific = TRUE),
                                            format(price.price.m2.pearson$p.value,scientific = TRUE),
                                            format(price.m2.m2.pearson$p.value,scientific = TRUE)),
                                  Correlación.Spearman= c(price.m2.spearman$estimate,
                                            price.desc.len.spearman$estimate,
                                            price.price.m2.spearman$estimate,
                                            price.m2.m2.spearman$estimate), 
                            Pvalue.Spearman = c(format(price.m2.spearman$p.value,scientific = TRUE),
                                            format(price.desc.len.spearman$p.value, scientific = TRUE),
                                            format(price.price.m2.spearman$p.value,scientific = TRUE),
                                            format(price.m2.m2.spearman$p.value,scientific = TRUE)))
results.table.salamanca %>% 
  kable(caption="Test de correlación en el distrito de Salamanca") %>% 
    kable_styling(bootstrap_options = Form.Basic) %>%
  kableExtra::scroll_box(width = "100%")
Test de correlación en el distrito de Salamanca
Variables Correlación.Pearson Pvalue.Pearson Correlación.Spearman Pvalue.Spearman
Price y m2 0.89 0e+00 0.91 0e+00
Price y description length 0.20 1.9e-23 0.23 4.6e-32
Price y price.m2 0.56 8.1e-210 0.56 2.2e-206
Price.m2 y m2 0.23 3.7e-32 0.19 5.7e-22
# Correlograma
spearman.salamanca <- ggcorr(corr.salamanca.houses, 
                             method=c('complete.obs', 'spearman'), 
                             name='Corr. coef.',
                             label=TRUE, 
                             label_alpha=TRUE, 
                             legend.position='left') +
  title.centered + ggtitle('Distrito Salamanca (Spearman)') +
  theme(legend.title=element_text(face='bold', size=10))

pearson.salamanca <- ggcorr(corr.salamanca.houses, 
                            method=c('complete.obs', 'pearson'), 
                            name='Corr. coef.',
                            label=TRUE, 
                            label_alpha=TRUE, 
                            legend.position='left') +
  title.centered + ggtitle('Distrito Salamanca (Pearson)') +
  theme(legend.title=element_text(face='bold', size=8))

ggarrange(nrow=1, 
          ncol=2, 
          align='hv', 
          heights=c(1, 0.5),
          pearson.salamanca,
          spearman.salamanca)

En segundo lugar, se realiza el análisis sobre el distrito de Villaverde:

corr.villaverde.houses <- villaverde.houses %>% dplyr::select(price, m2, price.m2, description.length) %>%
  dplyr::rename(desc.len=description.length)

# Tabla de correlaciones
# as.data.frame(cor(corr.villaverde.houses, use='complete.obs', method='spearman'))
# as.data.frame(cor(corr.villaverde.houses, use='complete.obs', method='pearson'))

# Test
price.m2.spearman <- cor.test(corr.villaverde.houses$price, corr.villaverde.houses$m2, method='spearman')
price.m2.pearson <- cor.test(corr.villaverde.houses$price, corr.villaverde.houses$m2, method='pearson')
price.desc.len.spearman <-cor.test(corr.villaverde.houses$price, corr.villaverde.houses$desc.len, method='spearman')
price.desc.len.pearson <- cor.test(corr.villaverde.houses$price, corr.villaverde.houses$desc.len, method='pearson')
price.price.m2.spearman <-cor.test(corr.villaverde.houses$price, corr.villaverde.houses$price.m2, method='spearman')
price.price.m2.pearson <- cor.test(corr.villaverde.houses$price, corr.villaverde.houses$price.m2, method='pearson')
price.m2.m2.spearman <-cor.test(corr.villaverde.houses$price.m2, corr.villaverde.houses$m2, method='spearman')
price.m2.m2.pearson <- cor.test(corr.villaverde.houses$price.m2, corr.villaverde.houses$m2, method='pearson')

# Tabla de resultados
results.table.villaverde <- data.frame(Variables = c("Price y m2", 
                                           "Price y description length", 
                                           "Price y price.m2",
                                           "Price.m2 y m2"), 
                                  Correlación.Pearson = c(price.m2.pearson$estimate,
                                            price.desc.len.pearson$estimate,
                                            price.price.m2.pearson$estimate,
                                            price.m2.m2.pearson$estimate), 
                            Pvalue.Pearson = c(format(price.m2.pearson$p.value, scientific = TRUE),
                                            format(price.desc.len.pearson$p.value,scientific = TRUE),
                                            format(price.price.m2.pearson$p.value,scientific = TRUE),
                                            format(price.m2.m2.pearson$p.value,scientific = TRUE)),
                                  Correlación.Spearman= c(price.m2.spearman$estimate,
                                            price.desc.len.spearman$estimate,
                                            price.price.m2.spearman$estimate,
                                            price.m2.m2.spearman$estimate), 
                            Pvalue.Spearman = c(format(price.m2.spearman$p.value,scientific = TRUE),
                                            format(price.desc.len.spearman$p.value, scientific = TRUE),
                                            format(price.price.m2.spearman$p.value,scientific = TRUE),
                                            format(price.m2.m2.spearman$p.value,scientific = TRUE)))
results.table.villaverde %>% 
  kable(caption="Test de correlación en el distrito de Villaverde") %>% 
    kable_styling(bootstrap_options = Form.Basic) %>%
  kableExtra::scroll_box(width = "100%")
Test de correlación en el distrito de Villaverde
Variables Correlación.Pearson Pvalue.Pearson Correlación.Spearman Pvalue.Spearman
Price y m2 0.81 1.4e-193 0.73 1.3e-138
Price y description length 0.24 5.2e-12 0.23 1.3e-11
Price y price.m2 0.52 2.5e-58 0.62 2.8e-88
Price.m2 y m2 -0.02 5e-01 -0.02 5.8e-01
# Correlograma
spearman.villaverde <- ggcorr(corr.villaverde.houses, 
                             method=c('complete.obs', 'spearman'), 
                             name='Corr. coef.',
                             label=TRUE, 
                             label_alpha=TRUE, 
                             legend.position='left') +
  title.centered + ggtitle('Distrito Villaverde (Spearman)') +
  theme(legend.title=element_text(face='bold', size=10))

pearson.villaverde <- ggcorr(corr.villaverde.houses, 
                            method=c('complete.obs', 'pearson'), 
                            name='Corr. coef.',
                            label=TRUE, 
                            label_alpha=TRUE, 
                            legend.position='left') +
  title.centered + ggtitle('Distrito Villaverde (Pearson)') +
  theme(legend.title=element_text(face='bold', size=10))

ggarrange(nrow=1, 
          ncol=2, 
          align='hv', 
          heights=c(1, 0.5),
          pearson.villaverde,
          spearman.villaverde)

Como se puede observar, los resultados son algo diferentes atendiendo a la ubicación de las viviendas en venta, aunque se mantiene la conclusión de que todos los resultados son estadísticamente significativos independientemente del distrito y test empleado.

Aunque se puede apreciar en los correlogramas, lo que más destaca respecto a la matriz de correlaciones general es que en el caso del distito de Salamanca el tamaño de la vivienda en m2 tiene una correlación con price de entre el 89 % y 91 %, dependiendo del método empleado, mientras que este porcentaje se reduce a entre 81 % y 73 % en el distrito de Villaverde.

4.3.3 ¿A mayor cantidad de metros cuadrados mayor número de habitaciones?

Por otro lado, en cuanto a las características de las viviendas en venta en el distrito de Salamanca y de Villaverde, y todas las viviendas en general, existe la intuición de que a mayor tamaño en metros cuadrados, mayor número de habitaciones, pero se quiere verificar esta hipótesis que mucha gente tiene en el imaginario a partir de los datos descargados de Fotocasa.

Para poder confirmar o negar dicha creencia popular, se utiliza el coeficiente de correlación de Spearman, dado que se trata de un test no paramétrico y es más adecuado para los datos a analizar debido a que no siguen una distribución normal. No obstante, también se van a realizar las pruebas con el coeficiente de correlación de Pearson, ya que se puede aplicar el teorema del límite central (TLC) sobre la muestra al tener un tamaño suficientemente grande:

# Ambos distritos
all.spearman <- cor.test(selected.houses$rooms, selected.houses$m2, method='spearman')
all.pearson <- cor.test(selected.houses$rooms, selected.houses$m2, method='pearson')

# Distrito de Salamanca
salamanca.spearman <- cor.test(salamanca.houses$rooms, salamanca.houses$m2, method='spearman')
salamanca.pearson <- cor.test(salamanca.houses$rooms, salamanca.houses$m2, method='pearson')

# Distrito de Villaverde
villaverde.spearman <- cor.test(villaverde.houses$rooms, villaverde.houses$m2, method='spearman')
villaverde.pearson <- cor.test(villaverde.houses$rooms, villaverde.houses$m2, method='pearson')

# Tabla de resultados
results.table <- data.frame(Zona = c("Ambos distritos", 
                                           "Distrito de Salamanca", 
                                           "Distrito de Villaverde"), 
                                  Correlación.Pearson = c(all.pearson$estimate,
                                            salamanca.pearson$estimate,
                                            villaverde.pearson$estimate), 
                            Pvalue.Pearson = c(format(all.pearson$p.value, scientific = TRUE),
                                            format(salamanca.pearson$p.value,scientific = TRUE) ,
                                            format(villaverde.pearson$p.value,scientific = TRUE)),
                                  Correlación.Spearman= c(all.spearman$estimate,
                                            salamanca.spearman$estimate,
                                            villaverde.spearman$estimate), 
                            Pvalue.Spearman = c(format(all.spearman$p.value,scientific = TRUE),
                                            format(salamanca.spearman$p.value, scientific = TRUE),
                                            format(villaverde.spearman$p.value,scientific = TRUE)))
results.table %>% 
  kable(caption="Test de correlación entre metros cuadrados y número de habitaciones") %>% 
    kable_styling(bootstrap_options = Form.Basic) %>%
  kableExtra::scroll_box(width = "100%")
Test de correlación entre metros cuadrados y número de habitaciones
Zona Correlación.Pearson Pvalue.Pearson Correlación.Spearman Pvalue.Spearman
Ambos distritos 0.69 4.6e-143 0.69 2.4e-141
Distrito de Salamanca 0.72 1.8e-123 0.76 3.6e-143
Distrito de Villaverde 0.35 4.8e-08 0.35 6.1e-08

El p-value de todos los test realizados es prácticamente 0, es decir, por debajo del nivel de significancia de \(\alpha\) = 0.05 por lo que se puede concluir que en todos los casos el número de habitaciones está significativametne correlacionado con el tamaño de las viviendas, con un nivel de confianza en el método del 95%.

En cualquier caso, lo importante a la luz de los resultados es destacar las diferencias entre el distrito de Salamanca y el de Villaverde. Mientras en el primero la correlación positiva es de entre un 72 % y un 76 %, dependiendo de la prueba realizada, en el de Villaverde es de solo un 35 %.

4.3.4 ¿Las viviendas más altas son más caras?

La intuición sugiere que, para una vivienda de características idénticas a otra, al situarse en uno de los últimos pisos del edificio su precio sería mayor. Esto se puede reformular como la pregunta del título de la sección, o como: ¿influye la altura de la vivienda en su precio?

Por tanto, la pregunta de investigación es si existe una diferencia significativa en el precio por metro cuadrado entre las viviendas altas y bajas. Como ya se ha determinado anteriormente, el precio por m\(^2\) entre distritos es significativamente diferente, por lo que se prefiere realizar la investigación en cada distrito de forma separada. En consecuencia, se plantean las siguientes hipótesis nula y alternativa:

  • H\(_{00}\) : \(\mu\) precio por m\(^2\) B. Salamanca | alto = \(\mu\) precio por m\(^2\) B. Salamanca | bajo

  • H\(_{01}\) : \(\mu\) precio por m\(^2\) B. Salamanca | alto > \(\mu\) precio por m\(^2\) B. Salamanca | bajo

  • H\(_{10}\) : \(\mu\) precio por m\(^2\) B. Villaverde | alto = \(\mu\) precio por m\(^2\) B. Villaverde | bajo

  • H\(_{11}\) : \(\mu\) precio por m\(^2\) B. Villaverde | alto > \(\mu\) precio por m\(^2\) B. Villaverde | bajo

En primer lugar se observa la distribución de los datos por grupos, tanto en el distrito de Salamanca como en el de Villaverde:

GGally::ggpairs(dplyr::select(salamanca.houses, price.m2, height.clasification), 
                mapping=aes(fill=factor(height.clasification)),
                columns=c('price.m2', 'height.clasification'), proportions=c(1.5,1),
                title='Distribución del precio/m2 por altura (Salamanca)', legend=4,
                diag=list(continuous = wrap("densityDiag", alpha = 0.75))) +
  scale_colour_manual(name='Altura', values=palette) +
  scale_fill_manual(name='Altura', values=palette) +
  theme(legend.position='bottom') + title.centered

El histograma, el gráfico de densidad y el boxplot muestran que el precio por metro cuadrado en el distrito de Salamanca es aproximadamente el mismo para los inmuebles situados en plantas altas y bajas.

GGally::ggpairs(dplyr::select(villaverde.houses, price.m2, height.clasification), 
                mapping=aes(fill=factor(height.clasification)),
                columns=c('price.m2', 'height.clasification'), proportions=c(1.5,1),
                title='Distribución del precio/m2 por altura (Villaverde)', legend=4,
                diag=list(continuous = wrap("densityDiag", alpha = 0.75))) +
  scale_colour_manual(name='Altura', values=palette) +
  scale_fill_manual(name='Altura', values=palette) +
  theme(legend.position='bottom') + title.centered

El histograma, el gráfico de densidad y el boxplot muestran que el precio por metro cuadrado en el distrito de Villaverde es aproximadamente el mismo para los inmuebles situados en plantas altas y bajas.

Como ya se ha estudiado anteriormente, la variable de price.m2 no sigue una distribución normal y se da heterocedasticidad. Sin embargo, como el tamaño de las muestras es grande, y aplicando el teorema del límite central (TLC), se puede asumir en ambos casos una distribución aproximadamente normal.

En consecuencia, se puede realizar un test de hipótesis de dos muestras independientes sobre la media, con distribución normal, unilateral por la derecha y teniendo en cuenta que las varianzas poblacionales son desconocidas y diferentes. Por ello, se opta en primer lugar por la prueba t de Student:

t.test(upper.floors.salamanca.houses$price.m2,
       lower.floors.salamanca.houses$price.m2,
       alternative = "greater")
## 
##  Welch Two Sample t-test
## 
## data:  upper.floors.salamanca.houses$price.m2 and lower.floors.salamanca.houses$price.m2
## t = 8, df = 2481, p-value <0.0000000000000002
## alternative hypothesis: true difference in means is greater than 0
## 95 percent confidence interval:
##  617 Inf
## sample estimates:
## mean of x mean of y 
##      7791      7020

El valor de p-value es prácticamente 0, muy por debajo de \(\alpha\) = 0.05, por lo que se rechaza la hipótesis nula en favor de la hipótesis alternativa: el precio por m\(^2\) en el distrito de Salamanca es distinto para viviendas en últimas plantas y en viviendas bajas, siendo significativamente superior en vivienda altas.

t.test(upper.floors.villaverde.houses$price.m2,
       lower.floors.villaverde.houses$price.m2,
       alternative = "greater")
## 
##  Welch Two Sample t-test
## 
## data:  upper.floors.villaverde.houses$price.m2 and lower.floors.villaverde.houses$price.m2
## t = 2, df = 741, p-value = 0.06
## alternative hypothesis: true difference in means is greater than 0
## 95 percent confidence interval:
##  -2.4  Inf
## sample estimates:
## mean of x mean of y 
##      1902      1847

El valor de p-value es 0.06, por encima de \(\alpha\) = 0.05, por lo que se acepta la hipótesis nula y se rechaza la hipótesis alternativa: el precio por m\(^2\) en el distrito de Villaverde es similar para viviendas en últimas plantas y en viviendas bajas.

No obstante, si no se hubiera aplicado el teorema del límite central y se hubiera atendido al resultado del test de Lilliefors, se habría tenido que aplicar un test no paramétrico. Para verificar la calidad del resultado anterior se realiza el test de Mann-Whitney:

wilcox.test(upper.floors.salamanca.houses$price.m2,
            lower.floors.salamanca.houses$price.m2,
            alternative = "greater")
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  upper.floors.salamanca.houses$price.m2 and lower.floors.salamanca.houses$price.m2
## W = 934530, p-value = 0.00000000000001
## alternative hypothesis: true location shift is greater than 0

El resultado del p-value es exactamente el mismo, por lo que se corrobora la decisión de descartar la hipótesis nula y aceptar la hipótesis alternativa.

wilcox.test(upper.floors.villaverde.houses$price.m2,
            lower.floors.villaverde.houses$price.m2,
            alternative = "greater")
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  upper.floors.villaverde.houses$price.m2 and lower.floors.villaverde.houses$price.m2
## W = 88798, p-value = 0.2
## alternative hypothesis: true location shift is greater than 0

El resultado del p-value es incluso superior al obtenido por la prueba paramétrica, por lo que se corrobora la decisión de aceptar la hipótesis nula y rechazar la hipótesis alternativa.

En conclusión, la altura de la vivienda tiene influencia en el precio por m\(^2\) en el distrito de Salamanca, pero no en el de Villaverde. Por tanto, el precio por metro cuadrado de las viviendas altas en el distrito de Salamanca es superior al precio por metro cuadrado de las viviendas bajas en el mismo distrito. Sin embargo, el precio por metro cuadrado de las viviendas altas y bajas en el distrito de Villaverde no se ve influido por la altura de la vivienda. Ambas afirmaciones cuentan con un nivel de confianza del 95% en el método.

4.3.5 Predicción del precio de una vivienda

Por último, se desea crear un modelo de regresión lineal que permita predecir el precio de una vivienda dadas sus características físicas, es decir, solo aquellas pertenecientes a la vivienda y no al anuncio, puesto que las viviendas se pueden vender sin ser anunciadas en un portal web.

Para dicho modelo, se va a utilizar la variable price como variable dependiente y las variables físicas de la vivienda location, m2 y floor como variables independientes. No se utiliza la variable price.m2 por ser un campo derivado de la variable dependiente. Tampoco se utiliza la variable rooms dados los valores perdidos.

price.prediction.model <- lm(price~location+m2+floor, data=selected.houses)
summary(price.prediction.model)
## 
## Call:
## lm(formula = price ~ location + m2 + floor, data = selected.houses)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2516961  -185450    18306   195235  7193885 
## 
## Coefficients:
##                     Estimate Std. Error t value            Pr(>|t|)    
## (Intercept)        -105609.1   345823.8   -0.31               0.760    
## locationvillaverde -352919.2    21153.4  -16.68 <0.0000000000000002 ***
## m2                    8948.6       85.8  104.32 <0.0000000000000002 ***
## floor0             -239926.2   346680.8   -0.69               0.489    
## floor1             -163615.9   346367.5   -0.47               0.637    
## floor2              -36971.2   346464.3   -0.11               0.915    
## floor3              -32066.4   346566.3   -0.09               0.926    
## floor4             -101720.5   346857.1   -0.29               0.769    
## floor5               -7807.6   347014.5   -0.02               0.982    
## floor6              -21988.6   347697.3   -0.06               0.950    
## floor7              108259.9   350645.6    0.31               0.758    
## floor8              -71269.2   360029.3   -0.20               0.843    
## floor9             -691456.9   373704.0   -1.85               0.064 .  
## floor10             -62875.2   599281.6   -0.10               0.916    
## floor11            -184033.5   424057.8   -0.43               0.664    
## floor12               5372.0   446551.4    0.01               0.990    
## floor13            -293722.4   489531.8   -0.60               0.549    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 489000 on 3339 degrees of freedom
## Multiple R-squared:  0.82,   Adjusted R-squared:  0.819 
## F-statistic:  949 on 16 and 3339 DF,  p-value: <0.0000000000000002

El modelo de regresión lineal para la predicción del precio de una vivienda dadas sus características físicas es estadísticamente significativo (\(p-value < \alpha = 0.05\)) y gracias al coeficiente de determinación \(R^2\) se sabe que explica un 82% de la variación de precios de las viviendas.

Además, se puede obtener información adicional del modelo. En primer lugar, se observa que si la localización de la vivienda es el distrito de Villaverde, el precio de la misma baja drásticamente (-352.919,2€ menos que si la vivienda se encontrara en el distrito de Salamanca). Además, como ya se había visto anteriormente en el estudio de correlaciones, cuantos más m\(^2\) tenga la vivienda, mayor será su precio.

Por último, cabe destacar que tanto los m\(^2\) como el distrito donde está situada la vivienda son estadísticamente significativos para el modelo. Por otro lado, la planta en que se encuentra no es estadísticamente significativa. Se procede a crear un segundo modelo de regresión lineal sin esta última variable:

price.prediction.model.sub <- lm(price~location+m2, data=selected.houses)
summary(price.prediction.model.sub)
## 
## Call:
## lm(formula = price ~ location + m2, data = selected.houses)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2663314  -174767    34680   169643  7184614 
## 
## Coefficients:
##                     Estimate Std. Error t value            Pr(>|t|)    
## (Intercept)        -215377.2    17214.5   -12.5 <0.0000000000000002 ***
## locationvillaverde -356306.9    21146.9   -16.9 <0.0000000000000002 ***
## m2                    9074.3       85.2   106.5 <0.0000000000000002 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 496000 on 3353 degrees of freedom
## Multiple R-squared:  0.814,  Adjusted R-squared:  0.813 
## F-statistic: 7.31e+03 on 2 and 3353 DF,  p-value: <0.0000000000000002

Se observa que el modelo sigue siendo estadísticamente significativo, así como los coeficientes de la localización y m\(^2\) de la vivienda. El coeficiente de determinación \(R^2_1 = 0.813\) frente al coeficiente de determinación del modelo completo \(R^2_0 = 0.819\) indica que la planta en que se encuentra la vivienda solo captura un 0.06% de la variación de precios de las viviendas.

5 Conclusiones

En este proyecto se ha completado el ciclo de vida de los datos, desde su captura hasta su análisis y publicación de los resultados. Así, tras todo el proceso con los datos sobre las viviendas en venta en los distritos de Salamanca y Villaverde de Madrid en los portales inmobiliarios de Idealista y Fotocasa entre el 4 y 7 de abril de 2022, ya se está en disposición de exponer las conclusiones a las que se ha ido llegando en la fase de Análisis de los datos.

En este sentido, hay que tener presente que al inicio de este proyecto centrado en el mercado inmobiliario en un contexto económico y legislativo cambiante del que ya se ha hablado ampliamente, se planteaba el problema principal sobre las diferencias de precios entre ambos distritos, puesto que son los que tienen los precios de venta más extremos en la capital, ciudad más poblada de España.

Así, se han hecho varios descubrimientos, teniendo en cuenta que todas las conclusiones tienen un 95% de confianza en el método:

  • Los precios por metro cuadrado son signficativamente mayores en el distrito de Salamanca que en el de Villaverde. Esto confirma la hipótesis del proyecto, puesto que las fuentes consultadas tenían en cuenta los precios absolutos y no por metro cuadrado y era necesario verificar la suposición.

  • Se da una mayor correlación entre precio y tamaño en el distrito de Salamanca que en el de Villaverde, si bien es cierto que correlación no implica necesariamente causalidad. Además, a mayor tamaño, el precio por metro cuadrado también es más elevado en ambas zonas.

  • En cuanto al tamaño y número de habitaciones de las viviendas, se da una correlación positiva estadísticamente significativa. Sin emabargo, en el distrito de Salamanca la correlación es de entre un 72 % y un 76 %, dependiendo de la prueba realizada, mientras que en el de Villaverde es de solo un 35 %.

  • En el análisis del precio de la vivienda con respecto a la altura también se ha encontrado un hallazgo inesperado. En el distrito de Salamanca los pisos altos (por encima de la cuarta planta) son significativamente más caros, pero en el de Villaverde no.

En conclusión, y a la luz de los resultados obtenidos, se puede determinar que lo que más influye en la diferencia de precio de una vivienda entre los distritos de Salamanca y Villaverde de Madrid es, precisamente, su ubicación, lo que ha quedado confirmado en el modelo de regresión lineal generado para la predicción de precios.

Como el ciclo de vida de los datos iniciado en este proyecto con la captura es un proceso iterativo y debido a que no se han encontrado los motivos que justifiquen tal diferencia de precios entre ambos distritos de Madrid, como pasos futuros se propone un análisis más amplio de las diferentes zonas. Por ejemplo, se podrán tener en cuenta otras características de las viviendas, como si tienen ascensor, su antigüedad o la necesidad de reforma o mantenimiento. Más allá de esto, todo apunta a que las diferencias se encuentran en los distritos en sí mismos, por lo que sería interesante estudiar otros factores, como los servicios educativos públicos y privados en la zona, los servicios sanitarios (en Villaverde había protestas a finales de mayo por el cierre de las Urgencias), los servicios para le tercera edad y personas dependientes, los comercios, el tamaño y estado de las aceras, los accesos a las principales vías de comunicación, la calidad del transporte público, la cercanía con el centro, la existencia de zonas verdes (el Parque del Retiro se encuentra en el distrito de Salamanca), etc.

6 Tabla de contribuciones

A continuación se muestra cómo ambos miembros del equipo han participado en todas las tareas de este proyecto colaborativo:

Contribuciones Firma
Investigación previa AGV, PLT
Redacción de las respuestas AGV, PLT
Desarrollo del código AGV, PLT

Bibliografía

BENGOECHEA ISASA, Jose Ignacio, 2018. NBA Gap Cleaning [en línea]. 1 julio 2018. [Consulta: 8 mayo 2022]. Disponible en: https://github.com/Bengis/nba-gap-cleaning
DALGAARD, Peter, 2002. Introductory Statistics with R. Springer New York, NY. ISBN 978-0-387-22632-3X.
FOTOCASA, 2022. Portal web completo de Fotocasa [en línea]. 2022. [Consulta: mayo 2022]. Disponible en: https://www.fotocasa.es/es/
GROLEMUND, Garrett y WICKHAM, Hadley, 2016. R for Data Science. [en línea]. Sebastopol, California : O’Reilly Media. ISBN  978-1-491-91039-9. Disponible en: https://learning.oreilly.com/library/view/mastering-shiny/9781492047377
GUILLEN ESTANY, Montserrat y ALONSO ALONSO, Maria Teresa, 2021. Modelos de regresion logistica. [en línea]. Editorial UOC. Disponible en: https://materials.campus.uoc.edu/daisy/Materials/PID_00276229/pdf/PID_00276229.pdf
GUTIÉRREZ GONZÁLEZ, Teguayco, 2017. Práctica 2: Limpieza y validación de los datos. 6 diciembre 2017.
HAN, Jiawei, KAMBER, Micheline y PEI, Jian, 2012. Data Preprocessing. En : Data Mining: Concepts and Techniques. Morgan Kaufmann. pp. 83-124. ISBN 978-0-12-381479-1.
IDEALISTA, 2022. Portal web completo de Idealista [en línea]. 2022. [Consulta: mayo 2022]. Disponible en: https://www.idealista.com/
INSTITUTO NACIONAL DE ESTADÍSTICA, 2022a. Índice de precios de consumo. Base 2021 - Avance. Marzo 2022 [en línea]. marzo 2022. [Consulta: 25 mayo 2022]. Disponible en: https://www.ine.es/dyngs/INEbase/es/operacion.htm?c=Estadistica_C&cid=1254736176802&menu=ultiDatos&idp=1254735976607
INSTITUTO NACIONAL DE ESTADÍSTICA, 2022b. El número de hipotecas sobre viviendas inscritas en los registros de la propiedad es de 43.378, un 18,0% más en tasa anual [en línea]. 27 mayo 2022. [Consulta: 27 mayo 2022]. Disponible en: https://www.ine.es/daco/daco42/daco426/h0322.pdf
J. J. ALLAIRE, Garrett Grolemund y XIE, Yihui, 2018. R Markdown: The Definitive Guide. [en línea]. Boca Raton, Florida : Chapman; Hall/CRC. ISBN ISBN 9781138359338. Disponible en: https://bookdown.org/yihui/rmarkdown
LA MONCLOA, 2022. El Gobierno aprueba la Ley por el Derecho a la Vivienda [en línea]. 1 febrero 2022. [Consulta: 28 marzo 2022]. Disponible en: https://www.lamoncloa.gob.es/consejodeministros/resumenes/Paginas/2022/010222-rp_cministros.aspx
MCKINNEY, Wes, 2012. Python for Data Analysis. O’Reilly Media. ISBN 978-1449323592.
MINISTERIO DE HACIENDA Y FUNCIÓN PÚBLICA, 2022. Sede Electrónica del Catastro [en línea]. 2022. [Consulta: 25 mayo 2022]. Disponible en: https://www.sedecatastro.gob.es/
MONTOLIU COLAS, Raul, 2021a. Modelos supervisados. [en línea]. Editorial UOC. Disponible en: http://cvapp.uoc.edu/autors/MostraPDFMaterialAction.do?id=284578
MONTOLIU COLAS, Raul, 2021b. Evaluacion de modelos. [en línea]. Editorial UOC. Disponible en: http://cvapp.uoc.edu/autors/MostraPDFMaterialAction.do?id=284573
OSBORNE, Jason W., 2010. Data Cleaning Basics: Best Practices in Dealing with Extreme Scores. Newborn and Infant Nursing Reviews. 2010. Vol. 10, n° 1. DOI 10.1053/j.nainr.2009.12.009.
RDOCUMENTATION, 2022. wilcox.test: Wilcoxon Rank Sum and Signed Rank Tests [en línea]. 2022. [Consulta: 25 mayo 2022]. Disponible en: https://www.rdocumentation.org/packages/stats/versions/3.6.2/topics/wilcox.test
SQUIRE, Megan, 2015. En : Clean Data. Packt Publishing Ltd. ISBN 978-1785284014.
SUBIRATS MATÉ, Laia, PÉREZ TRENARD, Diego Oswaldo y CALVO GONZÁLEZ, Mireia, 2019. Introducción a la limpieza y análisis de los datos. [en línea]. Editorial UOC. Disponible en: https://materials.campus.uoc.edu/daisy/Materials/PID_00265704/pdf/PID_00265704.pdf
VILLALBA, Fernando, 2018. Aprendizaje supervisado en R. [en línea]. Disponible en: https://fervilber.github.io/Aprendizaje-supervisado-en-R/l
YIHUI XIE, Christophe Dervieux y RIEDERER, Emily, 2022. R Markdown Cookbook. [en línea]. Boca Raton, Florida : Chapman; Hall/CRC. ISBN  9780367563837. Disponible en: https://bookdown.org/yihui/rmarkdown-cookbook/