Часть 1 - Визуализация данных, предварительная обработка и базовая модель прогнозирования
Вы когда-нибудь искали квартиру? Хотите добавить машинное обучение и сделать процесс более интересным?
Сегодня я расскажу вам о применении машинного обучения для поиска оптимальной квартиры.
Прежде всего, я хочу прояснить этот момент и объяснить, что означает «оптимальная квартира». Это квартира с набором различных характеристик, таких как «площадь», «район», «количество балконов» и так далее. И за эти характеристики квартиры мы ожидаем определенной цены. Похоже на функцию, которая принимает несколько параметров и возвращает число. Или, может быть, черный ящик, дающий некоторую магию.
Но… есть большое «но», иногда вы можете столкнуться с завышенной ценой квартиры по ряду причин, например, из-за хорошего географического положения. Также есть более престижные районы в центре города и районы за городом. Или… иногда люди хотят продать свои квартиры, потому что переезжают в другую точку Земли. Другими словами, есть много факторов, которые могут повлиять на цену. Знакомо?
Маленький шаг в сторону
Прежде чем продолжить, позвольте мне сделать небольшое лирическое отступление.
Я прожил в Екатеринбурге (город между Европой и Азией, один из городов, где в 2018 году проводился чемпионат мира по футболу) 5 лет.
Я был влюблен в эти бетонные джунгли. И я ненавидел этот город за русскую зиму и общественный транспорт. Это растущий город, и каждый месяц здесь продаются тысячи и тысячи квартир.
Да, это перенаселенный, загрязненный город. В то же время - хорошее место для анализа рынка недвижимости. Я получил много объявлений о квартирах из Интернета. И я буду использовать эту информацию в дальнейшем.
Также я попытался визуализировать различные предложения на карте Екатеринбурга.
В Екатеринбурге в июле 2019 года продано более 2 тысяч однокомнатных квартир. У них была другая цена, от менее миллиона до почти 14 миллионов рублей.
Цвет точек на карте обозначает цену, чем ниже цена рядом с синим цветом, тем выше цена рядом с красным. Это можно рассматривать как аналогию с холодными и теплыми цветами, чем теплее, тем выше цена. Пожалуйста, запомните этот момент, чем краснее цвет, тем выше ценность чего-либо. Та же идея работает для синего, но в направлении самой низкой цены.
Теперь у вас есть общий обзор картины, и приближается время для анализа.
Цель
Чего я хотел, когда жил в Екатеринбурге? Я искал достаточно хорошую квартиру, или, если говорить о машинном обучении, я хотел построить модель, которая даст мне рекомендации по покупке.
С одной стороны, если цена на флэт переоценена, модель должна рекомендовать дождаться снижения цены, показывая ожидаемую цену для этого флэта.
С другой стороны - если цена достаточно хорошая, в соответствии с состоянием рынка - возможно, мне стоит рассмотреть это предложение.
Конечно, ничего идеального нет, и я был готов допустить ошибку в расчетах. Обычно для такого рода задач используют среднюю ошибку прогноза, и я был готов к 10% ошибке. Например, если у вас есть 2–3 миллиона российских рублей, вы можете проигнорировать ошибку в 200–300 тысяч, вы можете себе это позволить. Как мне показалось.
Подготовка
Как я уже говорил, квартир было много, давайте посмотрим на них повнимательнее.
import pandas as pd df = pd.read_csv(‘flats.csv’) df.shape
2310 квартир за месяц, из этого можно было извлечь что-нибудь полезное. А как насчет общего обзора данных?
df.describe()
Ничего экстраординарного нет - долгота, широта, цена квартиры (ярлык «стоимость») и так далее. Да, на тот момент я использовал «стоимость» вместо «цена», надеюсь, это не приведет к недоразумениям, пожалуйста, считайте их такими же.
Уборка
Все ли записи имеют одинаковое значение? Некоторые из них представляют собой квартиры в виде кабинки, там можно работать, но жить там не хочется. Это маленькие тесные комнаты, а не настоящая квартира. Пусть их уберут.
df = df[df.total_area >= 20]
Прогноз цены квартиры исходит из старейших вопросов экономики и смежных областей. К слову «ML» ничего не относилось, и люди пытались угадать цену на основе квадратных метров / футов.
Итак, мы смотрим на эти столбцы / метки и пытаемся получить их распределение.
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()
Ну… ничего особенного, вроде нормальная раздача. Может, нам нужно пойти глубже?
sns.pairplot(df[numerical_fields])
Упс ... что-то не так. Удалите выбросы в этих полях и попробуйте снова проанализировать наши данные.
#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])
Что ж, теперь это выглядит лучше, и распределение кажется неплохим.
Трансформация
Надпись «год», указывающая на год постройки, должна быть преобразована в нечто более информативное. Пусть это будет возраст постройки, иными словами, насколько старый конкретный дом.
df['age'] = 2019 -df['year']
Посмотрим на результат.
df.head()
Есть все виды данных, категориальные, Nan-значения, текстовое описание и некоторая геоинформация (долгота и широта). Отложим последние, потому что на этом этапе они бесполезны. Мы вернемся к ним позже.
df.drop(columns=["lon","lat","description"],inplace=True)
Категориальные данные
Обычно для категориальных данных люди используют разные виды кодирования или такие вещи, как CatBoost, которые дают возможность работать с ними, как с числовыми переменными.
Но можно ли использовать что-то более логичное и интуитивно понятное? Пришло время сделать наши данные более понятными, не теряя их смысла.
Районы
Что ж, существует более двадцати возможных округов, можем ли мы добавить в нашу модель более 20 дополнительных переменных? Конечно, могли, но… должны ли? Мы люди и можем сравнивать вещи, не так ли?
Во-первых, не каждый район эквивалентен другому. В центре города цены за квадратный метр выше, дальше от центра - становится меньше. Это звучит логично? Можем ли мы использовать это?
Да, мы можем сопоставить любой район с конкретным коэффициентом, и чем дальше район, тем дешевле будут квартиры. После сопоставления города и использования другого веб-сервиса карта изменилась и имеет аналогичный вид
Я использовал ту же идею, что и для визуализации квартиры. Самый «престижный» и «дорогой» район окрашен в красный цвет, а наименее - в синий. Цветовая температура, вы об этом помните?
Кроме того, мы должны проделать некоторые манипуляции с нашим фреймворком данных.
district_map = {'alpha': 2, 'beta': 4, ... 'delta':3, ... 'epsilon': 1} df.district = df.district.str.lower() df.replace({"district": district_map}, inplace=True)
Такой же подход будет использован для описания внутреннего качества квартиры. Иногда требуется ремонт, иногда квартира в хорошем состоянии и готова к проживанию. А в других случаях стоит потратить дополнительные деньги на то, чтобы он выглядел лучше (сменить краны, покрасить стены). Также могут быть коэффициенты использования.
repair = {'A': 1, 'B': 0.6, 'C': 0.7, 'D': 0.8} df.repair.fillna('D', inplace=True) df.replace({"repair": repair}, inplace=True)
Кстати, о стенах. Конечно, это также влияет на стоимость квартиры. Современный материал лучше старого, кирпич лучше бетона. Стены из дерева - довольно спорный момент, возможно, это хороший выбор для дачи, но не такой уж хороший для городской жизни.
Мы используем тот же подход, что и раньше, плюс делаем предположение о строках, о которых мы ничего не знаем. Да, иногда люди не предоставляют всю информацию о своей квартире. Но, исходя из истории, можно попробовать угадать материал стен. В определенный период времени (например, период правления Хрущева) - мы знаем о типовых материалах для строительства.
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)
Также есть информация о балконе. По моему скромному мнению - балкон - вещь действительно полезная, поэтому не мог удержаться от размышлений.
К сожалению, есть некоторые нулевые значения. Если бы автор рекламы проверил информацию о ней, у нас была бы более реалистичная информация.
Что ж, если информации нет, значит «балкона нет».
df.balcony.fillna(0,inplace=True)
После этого сбрасываем столбцы с информацией о году постройки (для него у нас есть хорошая альтернатива). Кроме того, мы удалили столбец с информацией о типе здания, потому что он имеет много NaN-значений, и я не нашел возможности заполнить эти пробелы. И мы отбрасываем все строки с NaN, которые у нас есть.
df.drop(columns=['type_house'],inplace=True) df = df.astype(np.float64) df.dropna(inplace=True)
Проверка
Итак ... мы использовали нестандартный подход и заменили категориальные значения на их числовое представление. И вот мы закончили преобразование наших данных.
Часть данных была удалена, но в целом это неплохой набор данных. Посмотрим, что с ней произошло.
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']))
Эээ… стало очень интересно.
Положительная корреляция
Общая площадь - балконы. Почему нет? Если наша квартира большая, будет балкон.
Отрицательная корреляция
Общая площадь - возраст. Чем новее квартира, тем больше площадь для проживания. Звучит логично, новые квартиры более просторны, чем старые.
Возраст - балкон. Чем старше квартира, тем меньше в ней балконов. Похоже на корреляцию через другую переменную. Возможно, это треугольник Возраст-Балкон-Площадь, где одна переменная неявно влияет на другую. Отложите это на время.
Возраст - район. Более старая квартира с большой вероятностью попадет в более престижные районы. Может быть, это связано с более высокой ценой возле центра?
Также мы могли видеть корреляцию между зависимыми и независимыми переменными.
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")
Вот так…
Очень сильная корреляция между площадью флета и ценой. Если вы хотите иметь более просторное жилище, на это потребуется больше денег.
Между парами «возраст / стоимость» и «район / стоимость» существует отрицательная корреляция. Квартира в новом доме менее доступна, чем в старом. А в деревне квартиры дешевле.
Во всяком случае, это кажется ясным и понятным, поэтому я решил пойти с этим.
Модель
Для задач, связанных с прогнозированием цены флэта, обычно используют линейную регрессию. В соответствии со значительной корреляцией из предыдущего этапа, мы также можем попробовать его использовать. Это рабочая лошадка, которая подходит для многих задач.
Подготовьте наши данные для следующих действий
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)
Также мы создаем несколько простых функций для прогнозирования и оценки результата. Давайте сначала попробуем предсказать цену!
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)
Что ж… 76,67% точности. Это большое число или нет? На мой взгляд, неплохо. Более того, это хорошая отправная точка. Конечно, это не идеально, и есть потенциал для улучшения.
При этом - мы пытались спрогнозировать только одну часть данных. А как насчет применения той же стратегии для других данных? Да, время для перекрестной проверки.
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)
Теперь возьмем другой результат. 73 меньше 76. Но это также хороший кандидат до того момента, когда у нас появится лучший. Кроме того, это означает, что линейная регрессия довольно стабильно работает с нашим набором данных.
И время для последнего шага. Мы рассмотрим лучшую особенность линейной регрессии - интерпретируемость.
Это семейство моделей, в отличие от более сложных, лучше понимает. Есть только некоторые числа с коэффициентами, и вы можете поместить свои числа в уравнение, выполнить простую математику и получить результат.
Попробуем интерпретировать нашу модель
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)
Картина выглядит вполне логичной. Балкон / Стены / Площадь / Ремонт дают положительный вклад в фиксированную цену .
Чем дальше флет, тем больше отрицательный вклад. Также применимо для возраста. Чем старше квартира, тем ниже будет цена.
Итак, это было увлекательное путешествие.
Мы начали с нуля, использовали нетипичный подход к преобразованию данных, основанный на человеческой точке зрения (числа вместо фиктивных переменных), проверенных переменных и их соотношении друг с другом. После этого мы построили нашу простую модель, применили кросс-валидацию для ее тестирования. И как вишенка на торте - посмотрите на внутреннее устройство модели, что вселяет в нас уверенность в правильности нашего пути.
Но! Это не конец нашего пути, а всего лишь перерыв. Мы постараемся изменить нашу модель в будущем и, возможно (только возможно), это повысит точность прогнозов.
Спасибо за чтение