1 - Introduccion

Volver al Inicio



El siguiente trabajo consiste en utilizar los datos de la inmobiliaria Properatti de Argentina para programar utilizando el lenguaje Python un modelo de regresión lineal múltiple que mejor estime los precios en dólares por metro cuadrado.

Los datos vienen con faltantes e incluso valores atípicos que perjudicarían a la estimación de los parámetros del modelo, por lo tanto, en este Notebook se realiza un análisis exploratorio y un data wrangling a los efectos de obtener los datos deseados para estimar el modelo.



2 - Librerias

Volver al Inicio



Librerías

library(reticulate)

Sys.setenv(RETICULATE_PYTHON = 'C:/ProgramData/Anaconda3/python.exe')

use_python('C:/ProgramData/Anaconda3/python.exe')


import pandas as pd
import numpy as np
import plotly.io as pio
import plotly.express as px
import plotly.figure_factory as ff
import matplotlib.pyplot as plt
import matplotlib.patches
import warnings
from time import sleep
from IPython.display import clear_output
warnings.filterwarnings('ignore')



data_location = "properatti.csv"
data = pd.read_csv(data_location, sep=",")
print('El data frame contiene: ',data.shape[0], ' filas y ', data.shape[1], ' columnas.')
## El data frame contiene:  121220  filas y  26  columnas.


3 - Visualizaciones Preliminares

Volver al Inicio



state = data.groupby('state_name')['price_usd_per_m2'].mean().sort_values(ascending = False).index
values = data.groupby('state_name')['price_usd_per_m2'].mean().sort_values(ascending = False).values

datas = {'state': state, 'price_mean': values}
df = pd.DataFrame(datas)


def getPlot(df, x, y, title , x_descripy, y_descripy,  number_color):
    fig = px.bar(df, x=x, y=y,
                title= title,
                color_discrete_sequence=[px.colors.qualitative.Alphabet[number_color]],
                width=800, height=600,
                labels={x:x_descripy, y:y_descripy},
                template="simple_white"
                )
    return fig.show()
zona       = ((data.groupby("state_name").price_usd_per_m2.count()/data.shape[0])*100).sort_values(ascending = False).index
porcentaje = ((data.groupby("state_name").price_usd_per_m2.count()/data.shape[0])*100).sort_values(ascending = False).values

datos  = {'zona': zona, 'porcentaje': porcentaje}
df_dos = pd.DataFrame(datos)

El siguiente gráfico nos muestra el porcentaje de datos por Zona que tiene el data frame. Capital Federal tiene el 20% de los datos.

getPlot(df_dos, 'zona', 'porcentaje', 'Datos de Precio en Usd por M2' , 'zona', 'porcentaje',  2)
getPlot(df, 'state', 'price_mean', 'Precio Promedio por Metro Cuadrado por Zona' , 'Zona', 'Precio' , 15)
def getPlotGroup(data, filtro,number_color):
    data = data.loc[data['state_name'] == filtro, ]
    x = data.groupby('place_name')['price_usd_per_m2'].std().sort_values(ascending = False).index
    y = data.groupby('place_name')['price_usd_per_m2'].std().sort_values(ascending = False).values
   
    df = pd.DataFrame({'x': x, 'y': y})
    fig = px.bar(df, x=x, y=y,
                title= 'Desvio Standard' + ' m2 ' + filtro,
                color_discrete_sequence=[px.colors.qualitative.Alphabet[number_color]],
                width=800, height=600,
                template="simple_white"
                )
    return fig.show()

El gráfico de barras con desvío standard en el precio por metro cuadrado en dólares nos sirve para visualizar la dispersión de los datos respecto al promedio de cada uno de los barrios.

getPlotGroup(data, 'Capital Federal', 7)

df_caba = data[data['state_name'] == 'Capital Federal']
x = df_caba.groupby('place_name')['price_usd_per_m2'].std().sort_values(ascending = False).index
y = df_caba.groupby('place_name')['price_usd_per_m2'].std().sort_values(ascending = False).values

df_caba_original = data[data['state_name'] == 'Capital Federal']
df_caba = data[data['state_name'] == 'Capital Federal']

En el siguiente gráfico podemos ver pocos puntos ya que a simple vista encontramos valores atípicos que deberíamos ver si es un error en los datos o efectivamente son verdaderos.


def custom_log10(x):
  return np.log10(x) if x != 0 else x

x = df_caba.price_aprox_usd.apply(custom_log10)
y = df_caba.surface_total_in_m2.apply(custom_log10)

fig = px.scatter(df_caba, x=x, y=y, color="property_type",
                  hover_data=['property_type'])
fig.show()


4 - Valores Atipicos

Volver al Inicio



Se agrupan por Zona y tipo de inmueble. Luego se sacan los percentiles 75 y 25, el rango intercuartil y, por ultimo, se extraen los indices de los precios por metro cuadrado en dolares que están por encima y debajo de los siguientes rangos:


import itertools


lista_index   = []
for i in range(len(df_caba.place_name.unique())):
    for tipo in list(df_caba.property_type.unique()):
        try:
            q75,q25 = np.nanpercentile(df_caba.loc[(df_caba.place_name == df_caba.place_name.unique()[i]) &  (df_caba.property_type == tipo), ['price_usd_per_m2']], [75, 25])
            rango_max = q75 + 1.5 * (q75 - q25)   
            rango_min = q25 - 1.5 * (q75 - q25) 
            index_maximos = list(df_caba.loc[(df_caba.place_name == df_caba.place_name.unique()[i]) &  (df_caba.property_type == tipo), ['price_usd_per_m2']][df_caba['price_usd_per_m2'] > rango_max].index)
            index_minimos = list(df_caba.loc[(df_caba.place_name == df_caba.place_name.unique()[i]) &  (df_caba.property_type == tipo), ['price_usd_per_m2']][df_caba['price_usd_per_m2'] < rango_min].index)
            index_total = index_maximos + index_minimos
            if len(index_total) >= 1:
                lista_index.append(index_total)
        except Exception as error:
            print(df_caba.place_name.unique()[i], ' No tiene: ', tipo)
## Puerto Madero  No tiene:  PH
## Villa Pueyrredón  No tiene:  store
## Parque Centenario  No tiene:  house
## Parque Avellaneda  No tiene:  store
## Versalles  No tiene:  store
## Retiro  No tiene:  PH
## Centro / Microcentro  No tiene:  PH
## Once  No tiene:  house
## Tribunales  No tiene:  house
## Catalinas  No tiene:  PH
## Catalinas  No tiene:  house
## Villa General Mitre  No tiene:  store
## Villa Riachuelo  No tiene:  store
lista_filtro = list(itertools.chain.from_iterable(lista_index))
print('Total de indices a eliminar: ',len(lista_filtro))
## Total de indices a eliminar:  1036
df_caba.drop(lista_filtro, axis=0, inplace=True)
df_caba.reset_index(drop=True, inplace=True)

En el siguiente gráfico observamos un boxplot de Boedo antes y después de la limpieza de los valores atípicos. Si bien los mas extremos se eliminaron todavía queda por ver los que faltan.

zona="Boedo"
x        = df_caba_original.loc[df_caba_original['place_name'] == zona, ]['place_name']
y        = df_caba_original.loc[df_caba_original['place_name'] == zona, ]['price_usd_per_m2']
color    = df_caba_original.loc[df_caba_original['place_name'] == zona, ]['property_type']
x_dos    = df_caba.loc[df_caba['place_name'] == zona, ]['place_name']
y_dos    = df_caba.loc[df_caba['place_name'] == zona, ]['price_usd_per_m2']
color_dos= df_caba.loc[df_caba['place_name'] == zona, ]['property_type']
fig      = px.box(df_caba_original, x= x, y=y, points="all", color = color, title="Zona por Tipo de departamento Con Outliers")
figdos   = px.box(df_caba, x= x_dos, y=y_dos, points="all", color = color_dos, title="Zona por Tipo de departamento Sin Outliers")
fig.show()
#figdos.show()

round(df_caba.isnull().sum() / df_caba.shape[0],2).sort_values(ascending = False)
## floor                         0.90
## expenses                      0.80
## rooms                         0.53
## price_usd_per_m2              0.28
## lat-lon                       0.26
## lat                           0.26
## lon                           0.26
## surface_total_in_m2           0.19
## price_per_m2                  0.16
## price_aprox_local_currency    0.11
## price                         0.11
## currency                      0.11
## price_aprox_usd               0.11
## surface_covered_in_m2         0.07
## geonames_id                   0.04
## image_thumbnail               0.02
## operation                     0.00
## state_name                    0.00
## country_name                  0.00
## place_with_parent_names       0.00
## place_name                    0.00
## property_type                 0.00
## properati_url                 0.00
## description                   0.00
## title                         0.00
## Unnamed: 0                    0.00
## dtype: float64
for i in range(len(df_caba.columns)):
    if len(df_caba[df_caba.columns.values[i]].unique()) == 1 or round(df_caba.isnull().sum() / df_caba.shape[0],2)[i] >= 0.5:
        print('La columna :',df_caba.columns[i], 'tiene ', len(df_caba[df_caba.columns.values[i]].unique()), 'valor/es unico/s y el', 100 * round(df_caba.isnull().sum() / df_caba.shape[0],2)[i], '% de valores Nulos')
## La columna : operation tiene  1 valor/es unico/s y el 0.0 % de valores Nulos
## La columna : country_name tiene  1 valor/es unico/s y el 0.0 % de valores Nulos
## La columna : state_name tiene  1 valor/es unico/s y el 0.0 % de valores Nulos
## La columna : floor tiene  86 valor/es unico/s y el 90.0 % de valores Nulos
## La columna : rooms tiene  18 valor/es unico/s y el 53.0 % de valores Nulos
## La columna : expenses tiene  732 valor/es unico/s y el 80.0 % de valores Nulos

Se eliminan las columnas que tienen valores nulos y constantes:

df_caba.drop('floor',        inplace=True, axis=1)
df_caba.drop('expenses',     inplace=True, axis=1)
df_caba.drop('Unnamed: 0',   inplace=True, axis=1)
df_caba.drop('operation',    inplace=True, axis=1)
df_caba.drop('country_name', inplace=True, axis=1)


for i in range(len(df_caba.columns)):
    if round(df_caba.isnull().sum() / df_caba.shape[0],2)[i] == 0:
        print(df_caba.columns[i], ' tiene ', round(df_caba.isnull().sum() / df_caba.shape[0],2)[i], ' valores Nulos')
## property_type  tiene  0.0  valores Nulos
## place_name  tiene  0.0  valores Nulos
## place_with_parent_names  tiene  0.0  valores Nulos
## state_name  tiene  0.0  valores Nulos
## properati_url  tiene  0.0  valores Nulos
## description  tiene  0.0  valores Nulos
## title  tiene  0.0  valores Nulos


5 - Procesamiento de Lenguaje Natural

Volver al Inicio



El kit de herramientas de lenguaje natural, o más comúnmente NLTK, es un conjunto de bibliotecas y programas para el procesamiento del lenguaje natural (PLN) simbólico y estadísticos para el lenguaje de programación Python.

NLTK está destinado a apoyar la investigación y la enseñanza en procesamiento de lenguaje natural (PLN) o áreas muy relacionadas, que incluyen la lingüística empírica, las ciencias cognitivas, la inteligencia artificial, la recuperación de información, y el aprendizaje de la máquina.5. NLTK se ha utilizado con éxito como herramienta de enseñanza, como una herramienta de estudio individual, y como plataforma para los sistemas de investigación de prototipos y construcción.

Para extraer información de las columnas descripción y title se utiliza NLTK, se limpian los StopWords para luego sacar la mayor cantidad y calidad de datos posibles como, por ejemplo:


from nltk.corpus import stopwords
stopwords = stopwords.words('spanish')
len(stopwords)
## 313
stopwords.append('-')
stopwords.append(' ')
print('Primeros StopWords: ',stopwords[0:15])
## Primeros StopWords:  ['de', 'la', 'que', 'el', 'en', 'y', 'a', 'los', 'del', 'se', 'las', 'por', 'un', 'para', 'con']

df_caba = df_caba.reset_index()


elementos_sin_stowpords_descripcion = []
elementos_sin_stowpords_titulo = []
for i in range(df_caba.shape[0]):
    elemento_descripcion = ' '.join([word for word in df_caba['description'][i].lower().split(' ') if word not in stopwords])
    elementos_sin_stowpords_descripcion.append(elemento_descripcion)
    elemento_titulo = ' '.join([word for word in df_caba['title'][i].lower().split(' ') if word not in stopwords])
    elementos_sin_stowpords_titulo.append(elemento_titulo)

palabras_descripcion = []
for i in range(len(elementos_sin_stowpords_descripcion)):
    lista_uno = elementos_sin_stowpords_descripcion[i]
    lista_uno = lista_uno.split(' ')
    for lista in lista_uno:
        palabras_descripcion.append(lista)
        
palabras_titulo = []
for i in range(len(elementos_sin_stowpords_titulo)):
    lista_uno = elementos_sin_stowpords_titulo[i]
    lista_uno = lista_uno.split(' ')
    for lista in lista_uno:
        palabras_titulo.append(lista)
values_descripcion = pd.Series(palabras_descripcion).value_counts()

values_descripcion = values_descripcion[1:] # Eliminamos el primer elemento ya que son vacios

values_titulo = pd.Series(palabras_titulo).value_counts()

Frecuencia de Palabras:

print(values_descripcion)
## cocina                 19652
## 2                      18334
## baño                   16218
## ambientes              15214
## 3                      13877
##                        ...  
## categoría.3                1
## guardar,                   1
## intimoimpresionante        1
## baños.arriba,              1
## iap478380izrastzoff        1
## Length: 119133, dtype: int64
print(values_titulo)
## departamento                              13218
## venta                                      8579
## ambientes                                  5385
##                                            5286
## 2                                          3951
##                                           ...  
## 324m2                                         1
## a/c                                           1
## limay                                         1
## monoa.-exc.ubicac.-financia.prop./apto        1
## boe261-                                       1
## Length: 11004, dtype: int64
datos  = {'palabra': values_descripcion.index[0:30], 'frecuencia': values_descripcion.values[0:30]}
df_palabras_descripcion = pd.DataFrame(datos)

getPlot(df_palabras_descripcion, 'palabra', 'frecuencia', 'Frecuencia de Palabras Descripcion' , 'palabra', 'frecuencia',  5)
datos  = {'palabra': values_titulo.index[0:30], 'frecuencia': values_titulo.values[0:30]}
df_palabras_title = pd.DataFrame(datos)

#getPlot(df_palabras_title, 'palabra', 'frecuencia', 'Frecuencia de Palabras Title' , 'palabra', 'frecuencia',  11)
ambientes_descripcion = []
ambientes_titulo = []
parrilla_descripcion = []
parrrila_titulo = []


diccionario_replace = { 'uno' : 1, 'dos' : 2, 'tres' : 3 , 'cuatro' : 4, 'cinco' : 5 , 'seis' : 6, 'siete' : 7 , 'ocho' : 8 , 'nueve' : 9 , 'dies' : 10 , 'diez': 10,
                       '1' : 1, '2' : 2, '3' : 3 , '4' : 4, '5' : 5 ,'6' : 6, '7' : 7 ,'8' : 8 ,'9' : 9, '10' : 10,'un' : 1, 'mono' : 1}

tuppla_filtros = ( 'ambientes' , 'amb.' , 'amb' )

tuppla_monoambientes = ('monoambiente'  ,'mono-ambiente' ,'ambiente' )

diccionario_parrilla = {'parrilla' : 1}

for description in elementos_sin_stowpords_descripcion:
    element = description.split(' ')
    clean_elem = 0
    for i in range(len(element)):
        ele = element[i].lower().replace(',','').replace('.','').replace('-','').replace(';','')
        if ele.lower() == 'parrilla':
            aux = ele.lower()
            clean_elem = diccionario_parrilla.get(aux)
            if not clean_elem:
                break
    parrilla_descripcion.append(clean_elem)
    
    
for description in elementos_sin_stowpords_titulo:
    element = description.split(' ')
    clean_elem = 0
    for i in range(len(element)):
        ele = element[i].lower().replace(',','').replace('.','').replace('-','').replace(';','')
        if ele.lower() == 'parrilla':
            aux = ele.lower()
            clean_elem = diccionario_parrilla.get(aux)
            if not clean_elem:
                break
    parrrila_titulo.append(clean_elem)


for description in elementos_sin_stowpords_descripcion:
    element = description.split(' ')
    clean_elem = None
    for i in range(len(element)):
        ele = element[i].lower().replace(',','').replace('.','').replace('-','').replace(';','')
        if ele in tuppla_filtros:
            aux = element[i-1].lower()
            clean_elem = diccionario_replace.get(aux)
            if not clean_elem:
                break
        if ele in tuppla_monoambientes:
            clean_elem = 1
            break
    ambientes_descripcion.append(clean_elem)


for description in elementos_sin_stowpords_titulo:
    element = description.split(' ')
    clean_elem = None
    for i in range(len(element)):
        ele = element[i].lower().replace(',','').replace('.','').replace('-','').replace(';','')
        if ele in tuppla_filtros:
            aux = element[i-1].lower()
            clean_elem = diccionario_replace.get(aux)
            if not clean_elem:
                break
        if ele in tuppla_monoambientes:
            clean_elem = 1
            break
    ambientes_titulo.append(clean_elem)

print('La columna Descripcion para ambientes tiene: ',pd.Series(ambientes_descripcion).isnull().sum(),'nulos, mientras que la columna Title para ambientes tiene: ',pd.Series(ambientes_titulo).isnull().sum())
## La columna Descripcion para ambientes tiene:  14881 nulos, mientras que la columna Title para ambientes tiene:  19784

Unificacion:

lista_ambientes_final = []
for i in range(len(ambientes_descripcion)):
    if ambientes_descripcion[i] == None:
        lista_ambientes_final.append(ambientes_titulo[i])
    else:
        lista_ambientes_final.append(ambientes_descripcion[i])
        
print('Completamos el % ',(pd.Series(lista_ambientes_final).value_counts().sum()/df_caba.shape[0])*100, 'del data set')
## Completamos el %  65.05115089514067 del data set
lista_parrilla_final = []
for i in range(len(parrilla_descripcion)):
    if parrilla_descripcion[i] == None:
        lista_parrilla_final.append(parrilla_titulo[i])
    else:
        lista_parrilla_final.append(parrilla_descripcion[i])
df_caba['ambientes'] = pd.Series(lista_ambientes_final)

df_caba['parrilla'] = pd.Series(lista_parrilla_final)

round(df_caba.isnull().sum() / df_caba.shape[0],2).sort_values(ascending = False)
## rooms                         0.53
## ambientes                     0.35
## price_usd_per_m2              0.28
## lat-lon                       0.26
## lat                           0.26
## lon                           0.26
## surface_total_in_m2           0.19
## price_per_m2                  0.16
## currency                      0.11
## price_aprox_local_currency    0.11
## price_aprox_usd               0.11
## price                         0.11
## surface_covered_in_m2         0.07
## geonames_id                   0.04
## image_thumbnail               0.02
## properati_url                 0.00
## description                   0.00
## title                         0.00
## index                         0.00
## property_type                 0.00
## state_name                    0.00
## place_with_parent_names       0.00
## place_name                    0.00
## parrilla                      0.00
## dtype: float64
df_caba_apartment = df_caba.loc[df_caba['property_type'] == 'apartment' , :]
df_caba_apartment["ambientes"] = df_caba_apartment["ambientes"].astype(str)

x = df_caba_apartment.price_aprox_usd.apply(custom_log10)
y = df_caba_apartment.surface_total_in_m2.apply(custom_log10)

fig = px.scatter(df_caba_apartment, x=x, y=y, color="ambientes",
                  hover_data=['property_type'], title = 'Scatter Plot Precio Total y Superficie Separado por Ambientes para Apartment en Logaritmos')
fig.show()


6 - Conclusiones

Volver al Inicio



Una vez hecha la limpieza del data set se guarda el archivo para entrenar un modelo que mejor generalice nuestros datos y así poder predecir el precio en dólares por metro cuadrado dada una determinada cantidad de features.