Aprendizaje automático para su caza plana. Parte 1

¿Alguna vez has buscado un piso? ¿Le gustaría agregar algo de aprendizaje automático y hacer que un proceso sea más interesante?


Apartamentos en Ekaterimburgo

Hoy consideraremos aplicar Machine Learning para encontrar un piso óptimo.


Introduccion


En primer lugar, quiero aclarar este momento y explicar qué significa "un piso óptimo". Es un piso con un conjunto de características diferentes como "área", "distrito", "número de balcones", etc. Y para estas características del piso, esperamos un precio específico. Parece una función que toma varios parámetros y devuelve un número. O tal vez una caja negra que proporciona algo de magia.


Pero ... hay un gran "pero", a veces puedes enfrentar un piso que es demasiado caro debido a una serie de razones, como una buena posición geográfica. Además, hay distritos más prestigiosos en el centro de una ciudad y distritos fuera de la ciudad. O ... a veces la gente quiere vender sus apartamentos porque se mudan a otro punto de la Tierra. En otras palabras, hay muchos factores que pueden afectar el precio. ¿Te suena familiar?


Pequeño paso a un lado


Antes de continuar, déjame hacer una pequeña digresión lírica.
Viví en Ekaterimburgo (la ciudad entre Europa y Asia, una de las ciudades que había celebrado el Campeonato Mundial de Fútbol en 2018) durante 5 años.


Estaba enamorado de estas selvas de hormigón. Y odiaba esa ciudad por el invierno y el transporte público. Es una ciudad en crecimiento y cada mes se venden miles y miles de pisos.


Sí, es una ciudad superpoblada y contaminada. Al mismo tiempo, es un buen lugar para analizar un mercado inmobiliario. Recibí muchos anuncios de pisos, de Internet. Y utilizaré esa información en mayor medida.


Además, traté de visualizar diferentes ofertas en el mapa de Ekaterimburgo. Sí, es la imagen llamativa de habracut, hecha en Kepler.gl


imagen


Hay más de 2 mil apartamentos de 1 habitación que se vendieron en julio de 2019 en Ekaterimburgo. Tenían un precio diferente, de menos de un millón a casi 14 millones de rublos.


Estos puntos se refieren a su posición geográfica. El color de los puntos en el mapa representa el precio, cuanto menor es el precio cercano al color azul, mayor es el precio cercano al rojo. Puede considerarlo como una analogía con colores fríos y cálidos, el color más cálido es el precio más grande.
Por favor, recuerda ese momento, cuanto más rojo es el color, más alto es el valor de algo. La misma idea funciona para el azul pero en la dirección del precio más bajo.


Ahora tiene una visión general de la imagen y se acerca el momento de analizar.


Gol


¿Qué quería cuando vivía en Ekaterimburgo? Busqué un piso lo suficientemente bueno, o si hablamos en términos de ML, quería construir un modelo que me diera una recomendación sobre la compra.


Por un lado, si un piso tiene un precio excesivo, el modelo debería recomendar esperar a que disminuya el precio mostrando el precio esperado para ese piso.
Por otro lado, si un precio es lo suficientemente bueno, según el estado del mercado, tal vez debería considerar esa oferta.


Por supuesto, no hay nada ideal y estaba listo para aceptar un error en los cálculos. Por lo general, para este tipo de tarea, use el error medio de predicción y estaba listo para un error del 10%. Por ejemplo, si tiene 2-3 millones de rublos rusos, puede ignorar el error en 200-300 mil, puede permitírselo. Como me pareció a mí.


Preparar


Como mencioné antes, había muchos apartamentos, echemos un vistazo de cerca.
importar pandas como pd


df = pd.read_csv('flats.csv') df.shape 

imagen


2310 pisos por un mes, podríamos extraer algo útil de eso. ¿Qué pasa con una descripción general de datos?


 df.describe() 

imagen
No hay algo extraordinario: longitud, latitud, precio de un piso (la etiqueta " costo "), etc. Sí, para ese momento usé " costo " en lugar de " precio ", espero que no conduzca a malentendidos, considérelos como iguales.


Limpieza


¿Todos los registros tienen el mismo significado? Algunos de ellos son pisos representados como un cubículo, puedes trabajar allí, pero no te gustaría vivir allí. Son habitaciones pequeñas y estrechas, no un piso real. Deja eliminarlos.


 df = df[df.total_area >= 20] 

El precio de predicción de piso proviene de los problemas más antiguos en economía y campos relacionados. No había nada relacionado con el término "ML" y la gente trató de adivinar el precio basado en metros cuadrados / pies.
Entonces, miramos estas columnas / etiquetas e intentamos obtener la distribución de ellas.


 numerical_fields = ['total_area','cost'] for col in numerical_fields: mask = ~np.isnan(df[col]) sns.distplot(df[col][mask], color="r",label=col) plot.show() 

imagen


Bueno ... no hay nada especial, parece una distribución normal. Tal vez tenemos que ir más profundo?


 sns.pairplot(df[numerical_fields]) 

imagen


Vaya ... hay algo mal ahí. Limpie los valores atípicos en estos campos e intente analizar nuestros datos nuevamente.


 #Remove outliers df = df[abs(df.total_area - df.total_area.mean()) <= (3 * df.total_area.std())] df = df[abs(df.cost - df.cost.mean()) <= (3 * df.cost.std())] #Redraw our data sns.pairplot(df[numerical_fields]) 

imagen


Los valores atípicos se han ido, y ahora se ve mejor.


Transformación


La etiqueta "año", que apunta a un año de construcción, debe transformarse en algo más informativo. Que sea la edad de la construcción, en otras palabras, la antigüedad de una casa específica.


 df['age'] = 2019 -df['year'] 

Echemos un vistazo al resultado.


 df.head() 

imagen


Hay todo tipo de datos, categóricos, valores nanométricos, descripciones de texto y cierta información geográfica (longitud y latitud). Dejemos a un lado los últimos porque en ese escenario son inútiles. Volveremos a ellos más tarde.


 df.drop(columns=["lon","lat","description"],inplace=True) 

Datos categóricos


Por lo general, para los datos categóricos, las personas usan diferentes tipos de codificación o cosas como CatBoost que brindan la oportunidad de trabajar con ellos como con variables numéricas.
Pero, ¿podríamos usar algo más lógico y más intuitivo? Ahora es el momento de hacer que nuestros datos sean más comprensibles sin perder el significado de ellos.


Distritos


Bueno, hay más de veinte distritos posibles, ¿podríamos agregar más de 20 variables adicionales en nuestro modelo? Por supuesto, podríamos, pero ... ¿deberíamos? Somos personas y podríamos comparar cosas, ¿no?
En primer lugar, no todos los distritos son equivalentes a otros. En el centro de la ciudad, los precios de un metro cuadrado son más altos, más lejos del centro de la ciudad, se vuelve a bajar. ¿Suena lógico? ¿Podríamos usar eso?
Sí, definitivamente podríamos igualar cualquier distrito con un coeficiente específico y el distrito adicional es el de los pisos más baratos.


Después de hacer coincidir la ciudad y usar otro mapa de servicios web (ArcGIS Online) cambió y tiene una vista similar
imagen


Usé la misma idea que para la visualización de flat. El distrito más "prestigioso" y "caro" coloreado en rojo y el menos azul. Una temperatura de color, ¿lo recuerdas?
Además, deberíamos hacer alguna manipulación sobre nuestro marco de datos.


 district_map = {'alpha': 2, 'beta': 4, ... 'delta':3, ... 'epsilon': 1} df.district = df.district.str.lower() df.replace({"district": district_map}, inplace=True) 

Se utilizará el mismo enfoque para describir la calidad interna del piso. A veces necesita alguna reparación, a veces el piso está bastante bien y listo para vivir. Y en otros casos, debe gastar dinero adicional para que se vea mejor (cambiar grifos, pintar paredes). También podría haber coeficientes de uso.


 repair = {'A': 1, 'B': 0.6, 'C': 0.7, 'D': 0.8} df.repair.fillna('D', inplace=True) df.replace({"repair": repair}, inplace=True) 

Por cierto, sobre muros. Por supuesto, también influye en el precio del piso. El material moderno es mejor que el anterior, el ladrillo es mejor que el concreto. Las paredes de madera son un momento bastante controvertido, quizás sea una buena opción para el campo, pero no tan bueno para la vida urbana.


Usamos el mismo enfoque que antes, además de hacer una sugerencia sobre filas que no sabemos nada. Sí, a veces las personas no proporcionan toda la información sobre su piso. Además, según la historia, podemos intentar adivinar el material de las paredes. En un período de tiempo específico (por ejemplo, el período de liderazgo de Jruschov), conocemos el material típico para la construcción.


 walls_map = {'brick': 1.0, ... 'concrete': 0.8, 'block': 0.8, ... 'monolith': 0.9, 'wood': 0.4} mask = df[df['walls'].isna()][df.year >= 2010].index df.loc[mask, 'walls'] = 'monolith' mask = df[df['walls'].isna()][df.year >= 2000].index df.loc[mask, 'walls'] = 'concrete' mask = df[df['walls'].isna()][df.year >= 1990].index df.loc[mask, 'walls'] = 'block' mask = df[df['walls'].isna()].index df.loc[mask, 'walls'] = 'block' df.replace({"walls": walls_map}, inplace=True) df.drop(columns=['year'],inplace=True) 

Además, hay información sobre el balcón. En mi humilde opinión, el balcón es algo realmente útil, por lo que no pude evitar considerarlo.
Desafortunadamente, hay algunos valores nulos. Si el autor de un anuncio hubiera verificado la información al respecto, tendríamos información más realista.
Bueno, si no hay información significará "no hay un balcón".


 df.balcony.fillna(0,inplace=True) 

Después de eso, soltamos columnas con información sobre el año de construcción (tenemos una buena alternativa para ello). Además, eliminamos la columna con información sobre el tipo de edificio porque tiene muchos valores de NaN y no he encontrado ninguna oportunidad para llenar estos vacíos. Y soltamos todas las filas con NaN que tenemos.


 df.drop(columns=['type_house'],inplace=True) df = df.astype(np.float64) df.dropna(inplace=True) 

Comprobando


Entonces ... usamos un enfoque no estándar y reemplazamos los valores categóricos a su representación numérica. Y ahora terminamos con una transformación de nuestros datos.
Se ha descartado una parte de los datos, pero en general es un conjunto de datos bastante bueno. Veamos la correlación entre variables independientes.


 def show_correlation(df): sns.set(style="whitegrid") corr = df.corr() * 100 # Select upper triangle of correlation matrix mask = np.zeros_like(corr, dtype=np.bool) mask[np.triu_indices_from(mask)] = True # Set up the matplotlib figure f, ax = plt.subplots(figsize=(15, 11)) # Generate a custom diverging colormap cmap = sns.diverging_palette(220, 10) # Draw the heatmap with the mask and correct aspect ratio sns.heatmap(corr, mask=mask, cmap=cmap, center=0, linewidths=1, cbar_kws={"shrink": .7}, annot=True, fmt=".2f") plot.show() # df[columns] = scale(df[columns]) return df df1 = show_correlation(df.drop(columns=['cost'])) 

imagen


Erm ... se volvió muy interesante.
Correlación positiva
Área total - balcones . Por que no Si nuestro piso es grande habrá un balcón.
Correlación negativa
Área total - edad . Cuanto más nuevo es plano, más grande es un área para vivir. Suena lógico, los nuevos son planos más espaciosos que los antiguos.
Edad - balcón . Cuanto más viejo es plano, menos balcones tiene. Parece una correlación a través de otra variable. Quizás es un triángulo Edad-Balcón-Área donde una variable tiene una influencia implícita sobre otra. Poner eso en espera por un tiempo.
Edad - distrito. El piso más antiguo es la gran probabilidad que se colocará en los distritos más prestigiosos. ¿Podría estar relacionado con un precio más alto cerca del centro?


Además, pudimos ver la correlación con la variable dependiente


 plt.figure(figsize=(6,6)) corr = df.corr()*100.0 sns.heatmap(corr[['cost']], cmap= sns.diverging_palette(220, 10), center=0, linewidths=1, cbar_kws={"shrink": .7}, annot=True, fmt=".2f") 

imagen


Aquí vamos ...


La correlación muy fuerte entre el área de piso y el precio. Si desea tener un lugar más grande para vivir, necesitará más dinero.
Existe una correlación negativa entre los pares " edad / costo " y " distrito / costo ". Un piso en una casa más nueva menos accesible que la anterior. Y en el campo los pisos son más baratos.
De todos modos, parece claro y comprensible, así que decidí ir con eso.


Modelo


Para tareas relacionadas con el precio de la predicción plana, usualmente, use la regresión lineal. Según una correlación significativa de una etapa anterior, podríamos intentar usarla también. Es un caballo de batalla adecuado para muchas tareas.
Prepare nuestros datos para las próximas acciones.


 from sklearn.model_selection import train_test_split y = df.cost X = df.drop(columns=['cost']) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) 

Además, creamos algunas funciones simples para la predicción y evaluación del resultado. ¡Hagamos nuestro primer intento de predecir el precio!


 def predict(X, y_test, model): y = model.predict(X) score = round((r2_score(y_test, y) * 100), 2) print(f'Score on {model.__class__.__name__} is {score}') return score def train_model(X, y, regressor): model = regressor.fit(X, y) return model 

 from sklearn.linear_model import LinearRegression regressor = LinearRegression() model = train_model(X_train, y_train, regressor) predict(X_test, y_test, model) 

imagen


Bueno ... 76,67% de precisión. ¿Es un gran número o no? Según mi punto de vista, no está mal. Además, es un buen punto de partida. Por supuesto, no es lo ideal, y existe un potencial de mejora.


Al mismo tiempo, tratamos de predecir solo una parte de los datos. ¿Qué pasa con la aplicación de la misma estrategia para otros datos? Sí, hora de validación cruzada.


 def do_cross_validation(X, y, model): from sklearn.model_selection import KFold, cross_val_score regressor_name = model.__class__.__name__ fold = KFold(n_splits=10, shuffle=True, random_state=0) scores_on_this_split = cross_val_score(estimator=model, X=X, y=y, cv=fold, scoring='r2') scores_on_this_split = np.round(scores_on_this_split * 100, 2) mean_accuracy = scores_on_this_split.mean() print(f'Crossvaladaion accuracy on {model.__class__.__name__} is {mean_accuracy}') return mean_accuracy do_cross_validation(X, y, model) 

imagen


El resultado de la validación cruzada Ahora tomamos otro resultado. 73 es menor que 76. Pero también es un buen candidato hasta el momento en que tengamos uno mejor. Además, significa que una regresión lineal funciona bastante estable en nuestro conjunto de datos.


Y ahora es el momento del último paso.


Veremos la mejor característica de la regresión lineal: la interpretabilidad .
Esta familia de modelos, a diferencia de los más complejos, tiene una mejor capacidad de comprensión. Solo hay algunos números con coeficientes y puedes poner tus números en la ecuación, hacer algunas matemáticas simples y obtener un resultado.


Tratemos de interpretar nuestro modelo.


 def estimate_model(model): sns.set(style="white", context="talk") f, ax = plot.subplots(1, 1, figsize=(10, 10), sharex=True) sns.barplot(x=model.coef_, y=X.columns, palette="vlag", ax=ax) for i, v in enumerate(model.coef_.astype(int)): ax.text(v + 3, i + .25, str(v), color='black') ax.set_title(f"Coefficients") estimate_model(regressor) 

Los coeficientes de nuestro modelo


La imagen se ve bastante lógica. Balcón / Paredes / Área / Reparación dan una contribución positiva a un precio fijo.
Cuanto más plano es, mayor es una contribución negativa . También aplicable para la edad. El piso más antiguo es el precio más bajo.


Entonces, fue un viaje fascinante.
Comenzamos desde cero, usamos el enfoque atípico para la transformación de datos basado en el punto de vista humano (números en lugar de variables ficticias), variables verificadas y su relación entre sí. Después de eso, creamos nuestro modelo simple, utilizamos la validación cruzada para probarlo. Y como guinda del pastel: mire las partes internas del modelo, lo que nos da confianza sobre nuestro camino.


Pero! No es el final de nuestro viaje, sino solo un descanso. Intentaremos cambiar nuestro modelo en el futuro y tal vez (solo tal vez) aumente la precisión de la predicción.


Gracias por leer!


La segunda parte esta

PD Los datos de origen y el portátil Ipython se encuentran allí.

Source: https://habr.com/ru/post/468053/


All Articles