Как дата-сайентист машину покупал

После многих лет жизни и работы в Нидерландах мне с семьёй пришло время возвращаться в Штаты и менять велосипеды на автомобили.



В Америке очень сложно жить без машины, и, так как мы наши машины продали перед переездом, теперь нам надо было купить новое семейное средство передвижения. Я решил подойти к решению этой задачи так, как подошёл бы любой хороший специалист по обработке и анализу данных. Я решил воспользоваться данными.

Сбор данных


Часто самой сложной частью проекта по изучению данных является этап сбора данных. Особенно в том случае, если речь идёт о некоем домашнем проекте. Тут весьма вероятно то, что загрузить из интернета нечто готовое к обработке, оформленное в виде CSV-файла, не получится. А без данных не удастся ни построить модель, ни сделать прогноз.

Как раздобыть данные по рынку автомобилей? Решить эту задачу нам поможет веб-скрапинг.


Инструменты разработчика Google Chrome

Веб-скрапинг — это автоматизированный процесс сбора данных с веб-сайтов. Я не эксперт в вопросах веб-скрапинга, да и статья эта не является руководством по сбору данных с сайтов. Но, если немного попрактиковаться и вникнуть в особенности работы некоторых Python-модулей, можно удовлетворить самые безумные фантазии, касающиеся сбора данных.

В этом проекте я использовал Python-пакет Selenium, который представляет собой браузер без пользовательского интерфейса. Такой браузер открывает страницы и работает с ними так же, как работал бы с ними пользователь (в отличие от чего-то вроде Python-библиотеки Beautiful Soup, которая просто читает HTML-код).

Благодаря инструментам разработчика Google Chrome процесс подготовки к сбору данных сильно упрощается. Всё сводится к щелчкам правой кнопкой мыши по интересующим нас элементам веб-страниц и к копированию их xpath, element_id, или чего угодно другого. Всё это хорошо воспринимает Selenium. Но иногда для поиска нужного элемента и текста приходится немного повозиться.

Код, который я использовал для сбора информации по сотням машин, можно найти здесь.

Очистка данных


Одна из проблем данных, собранных с помощью веб-скрапинга, заключается в том, что эти данные, весьма вероятно, окажутся довольно-таки неопрятными. Дело тут в том, что взяты они не из некоего хранилища, а собраны с веб-сайта, который создан для того, чтобы показывать информацию людям. В результате такие данные нуждаются в серьёзной очистке. Часто это означает, что для получения нужных данных потребуется парсить строки.

После того, как я сохранил результаты веб-скрапинга в CSV-файле, я смог загрузить эти данные в датафрейм Pandas и заняться их очисткой.


Данные перед очисткой

Теперь я мог, пользуясь чем-то вроде регулярных выражений, приступать к извлечению из собранных данных нужной мне информации, и к созданию признаков, которые я мог бы исследовать, и на основе которых мог бы строить модели.

Сначала я создал функцию для работы с регулярными выражениями, подходящую для многократного использования, которую я мог бы передать методу Pandas .apply.

def get_regex_item(name, pattern):
    item = re.search(pattern, name)
    if item is None:
        return None
    else:
        return item.group()

В столбце name имеется много полезных данных. На его основе я, воспользовавшись созданной мной функцией, мог создать четыре новых признака.

df['4wd'] = df.name.apply(lambda x: 1 if get_regex_item(x, pattern= r'4[a-zA-Z]{2}') else 0)
df['year'] = df.name.apply(lambda x: get_regex_item(x, pattern= r'\d{4}'))
df['type'] = df.name.apply(lambda x: x.split()[-1])
df['certified'] = df.name.apply(lambda x: 1 if get_regex_item(x.lower(), r'certified') else 0)

df['price']  = df.price.apply(lambda x: x[:6].replace(",", "")).astype('int')
df['mileage'] = df.mileage.apply(lambda x: x.split()[0].replace(",", "")).astype('int')

В результате у меня получился датафрейм, показанный на следующем рисунке.


Данные после очистки

Разведочный анализ данных


Теперь пришло время заняться кое-чем интересным. А именно — разведочным анализом данных. Это очень важный шаг в работе дата-сайентиста, так как он позволяет понять особенности данных, увидеть тренды и взаимодействия. Моей целевой переменной была цена, в результате я строил графические представления данных, ориентируясь именно на цену.

import matplotlib.pyplot as plt
import seaborn as sns

plt.gca().spines['top'].set_visible(False)
plt.gca().spines['right'].set_visible(False)

#box plots
sns.boxplot(x = df.year, y=df.price, data=df[['year', 'price']], color='purple')
sns.boxplot(x = df.type, y=df.price, data=df[['year', 'price']], color='cyan')


Зависимость цены автомобиля от года его выпуска


Зависимость цены от типа автомобиля

Эти коробчатые диаграммы, отражающие взаимосвязь цены и некоторых категориальных переменных, дают очень чёткие сигналы о том, что цена сильно зависит от года выпуска и от типа автомобиля.

А вот — диаграмма разброса.

import matplotlib.pyplot as plt
import seaborn as sns

plt.gca().spines['top'].set_visible(False)
plt.gca().spines['right'].set_visible(False)

#scatter plot
sns.regplot(x=df.mileage, y=df.price , color='grey')


Диаграмма разброса

Тут, анализируя зависимость цены от пробега, я снова увидел сильную корреляцию между независимой переменной (mileage) и ценой автомобиля. Налицо сильная отрицательная связь, так как по мере роста пробега автомобиля его цена падает. Учитывая то, что все знают об автомобилях, это вполне понятно.

Зная о взаимоотношениях между некоторыми из независимых переменных и целевой переменной, мы можем получить довольно-таки хорошее представление о том, какие признаки являются особенно важными, и о том, модель какого типа нужно использовать.

Кодирование независимых признаков


Ещё один шаг, который нужно было выполнить до построения модели, заключается в кодировании независимых признаков. Признаки 4wd и certified уже представлены в логическом виде. Они не нуждаются в дополнительной обработке. А вот признаки type и year нужно закодировать. Для решения этой задачи я воспользовался методом Pandas .get_dummies, который позволяет выполнять кодирование с одним активным состоянием.

Pandas — довольно интеллектуальная система, которая позволила мне передать ей все признаки и закодировала только независимые переменные, а также позволила мне убрать один из только что созданных столбцов для того, чтобы избавиться от полностью коррелирующих друг с другом признаков. Использование подобных данных привело бы к ошибкам в модели и в её интерпретации. Обратите внимание на то, что в итоге у меня осталось только два столбца с информацией о годе выпуска машины, так как столбец _2015 был удалён. То же самое касается и столбцов type.


Данные после кодирования независимых переменных

Теперь мои данные оказались преобразованными в формат, на основе которого я мог построить модель.

Создание модели


Основываясь на анализе вышеприведённых диаграмм, и на том, что я знаю об автомобилях, я решил, что в моём случае подойдёт линейная модель.

Я воспользовался моделью линейной регрессии OLS (Ordinary Least Squares, обычный метод наименьших квадратов). Для построения модели применён Python-пакет statsmodel.

import statsmodels.api as sm
y = model_df.price
X = model_df.drop(columns='price')

model = sm.OLS(y, X).fit()
print(model.summary())

Вот результаты моделирования.


Моделирование привело к получению среднеквадратичной ошибки в 1556.09

Полученные коэффициенты соответствуют тому, что можно было видеть на диаграммах в ходе разведочного анализа данных. Пробег оказывает отрицательное воздействие на цену. В среднем, каждая дополнительная тысяча миль пробега приводит к снижению цены на $1600. Это справедливо для автомобилей GMC Yukon, возможно, не для всех. Вероятно, самыми шокирующими результатами из всех стали те, которые указывают на разницу в цене между SLE (автомобили Yukon низшего класса) и Denali (автомобили Yukon высшего класса). Разница составляет более $9000. Это означает, что покупателю автомобиля более высокого класса приходится платить немалые деньги за все получаемые им премиальные опции. Как результат — хорошо было бы, если бы всё это ему было по-настоящему нужно.

Какую же машину стоит купить?


Итак, я собрал, очистил, распарсил, исследовал, визуализировал данные и построил на их основе модель. Но исходный вопрос о том, что же всё-таки купить, пока остался без ответа. Как узнать о том, что некое предложение достойно того, чтобы его принять?

Подход к поиску ответа на этот вопрос очень прост. Нужно использовать модель для формирования прогноза и найти разницу между спрогнозированной ценой и ценой, которую предлагают продавцы машин. Затем нужно отсортировать список разниц цен так, чтобы самые недооценённые предложения оказались бы в его верхней части. Это и даст список самых выгодных предложений.

После учёта некоторых дополнительных ограничений (мой бюджет и предпочтения жены), я смог выйти на отсортированный список предложений и начал обзванивать продавцов.


Купленная нами машина выделена синим

В итоге мы купили 2015 GMC Yukon Denali за $32000. Это было на $3000 дешевле прогноза, выданного моделью.

Хотя на рынке были и более выгодные предложения, машина, которую мы купили, находилась недалеко от нас, что значительно упростило и ускорило процесс покупки. Учитывая то, что на тот момент автомобиля у нас не было, нам важны были быстрота и удобство в решении этого вопроса.


Наша покупка выделена на диаграмме синим цветом

В конце концов, мы смогли взглянуть на одну из ранее составленных диаграмм, выведя на ней сведения о купленной машине. Это показало нам, что мы, и правда, нашли хороший вариант.

Моей жене её новая машина очень нравится, а мне нравится цена этой машины. В общем, подводя итог вышеизложенному, можно сказать, что машину мы купили удачно.

Уважаемые читатели! Пользуетесь ли вы инструментами для анализа данных при решении каких-нибудь бытовых задач?


Источник: habr.ru