Привет!
В этом посте я хотел поделиться опытом использования нескольких питоновых инструментов для сравнительного анализа рынка подержанных машин в Европе на примере Германии и Франции.
C точки зрения реализации все будет достаточно ванильно. Зоопарк использованных технологий несколько избыточен, потому что был самоцелью — хотелось протестировать.
Предыстория. Мысль провести такой анализ посетила меня однажды когда я листал сайт французских объявлений. У меня явно складывалось ощущение, что в представлении аборигенов Ситроен С3 — чрезвычайно убогая машина, которая дешевеет процентов на 20 в год, а вот Опель Корса — практически бессмертна, поэтому цена ее с возрастом не меняется (а то и в рост пойдет, как хороший Ролекс). Это было забавное наблюдение, с учетом того, что у немцев Корса у взрослого человека считалась признаком фиаско в профессиональной реализации. И даже ходили шутки про то, что через 10 лет любой автомобиль превращается в Опель. За неимением у западных немцев Трабанта, амплуа Запорожца в немецком фольклоре исполняла именно эта марка. C3, насколько я знаю, в России особо не представлен, но о нем полезно знать что это духовный наследник классического 2CV с заглавной картинки, легендарной машины пару лет разминувшейся с интрнетом (в производстве до 1989) и предлагавшей водителю умопомрачительные девять лошадиных сил.
Итак, беремся за дело. Для того чтобы навести статистику нужен источник с достаточной выборкой. По Германии это autoscout24.de по Франции лучшее, что мне попалось по числу объявлений — www.lacentrale.fr (французская версия скаута существует, но менее популярна).
Выборку будем делать по B- классу как самому массовому и в котором возник изначальный вопрос (с небольшим заходом в С-класс). Для сбора данных выберем те модели, по которым есть данные за последние 10 лет, и по которым на обоих сайтах есть как минимум 1000 объявлений. В противном случае выборка будет очень шумной.
Если вы хотите получить датамайнинг здорового человека — автоскаут предлагает RESTful API если зарегистрироваться и получить ключ.
Я, конечно же, этого не делал и занялся майнингом курильщика — в лоб через безголовый браузер.
Майнинг курильщика выглядел так — на основе желаемых фильтров (о которых ниже ) формируем строку запроса, передаем ее как адрес в безголовый браузер PhantomJS ( с тех пор как я его установил разработка его оказалась почему-то приостановлена).
Из неочевидного на этом этапе — я добавил ожидание загрузки элемента с характерным классом, где содержались данные о количестве вариантов соответствующем запросу.
with io.open("dump.html", "w", encoding="utf-8") as f:
f.write(html)
try:
element = WebDriverWait(browser, 20).until(
EC.presence_of_element_located((By.CLASS_NAME, "cl-filters-summary-counter"))
) # wait until element with summry statistics is present or drop after 20 sec
Сгенерированный в браузере html скармливаем парсеру BeautifulSoup и ищем в нем элемент содержащий цифру найденных результатов.
value = bsObj.findAll("span", {"class": "cl-filters-summary-counter"})[0].text
value = value.replace(u'xa0', u' ') # removes delimeter if results exceed 1000
Как нетрудно догадаться, в сгенерированный запрос входили модель, небольшой интервал цен и возраста, то есть запросов было очень много, при количестве запросов более 500 сервера рвали соединение. Если вы хотите решить вопрос красиво — обращаться к таким сервисам лучше через меняемые по ходу дела прокси (а если у вас API ключ то вам и не надо). На Хабре есть шикарная статья о том как работать через прокси. Я решил вопрос низкотехнологично — цена следует близко к линейному закону с возрастом, поэтому я прикинул рамки этой линии и ограничил запросы только зоной +.- 30-50 процентов линейной цены, и ввел 10 секундные паузы между запросами. Этого хватило чтобы меня не банили. Сбор данных на одну модель занимал примерно полчаса тихого моргания в консоли.
Несколько слов о фильтрах. Чтобы получить информативную выборку я применил следующие фильтры:
- Все машины берутся в 4-5 дверной комплектации (у Корс, Клио и еще нескольких из представленных моделей есть более дешевые 3 дверные варианты).
- Все машины берутся с 4-5 сидячими местами — это отдельная от предыдущей характеристика, потому что у Клио, С3 и 308 есть версия Société (Entreprise) и ее довольно много на рынке — это служебные машины всяких электриков и монтеров, там вместо заднего сиденья ящик для барахла и стоят они меньше на пару килоевров при прочих равных.
- Мощность двигателя ограничена 129 л.с. Потому что дальше в этом сегменте начинаются заряженные версии и цены там совсем не соответствующие их скромному статусу. Если будете повторять то ахтунг — автоскаут в адресе использует мощность не в лошадиных силах, а в киловаттах.
Двигатели и коробки не специализированы, но в 99% это механика, и в случае с Францией — там много дизеля, просто потому что его там пиарили 10 лет до дизельгейта. А сейчас хотят пересадить всех с него, от огорчения на такую переменчивость правительственных нравов французы вторую неделю предаются двум любимым национальным забавам — забастовке и поджогу машин.
Данные собраны, разложены помодельно по листам в экселевские файлы (какой датасаенс без экселя, вы что! Спасибо openpyxl — за удобство перекладывания). Для визуализации все данные по всем моделям сведены в один CSV файл.
Для визуализации мне хотелось напилить простенький web интерфейс. В принципе, текущий вариант не требует бэкэнда. Данных мало, обработка — рудиментарная, можно вывалить все длинным JSONом вместе с графикой и обрабатывать на клиентской стороне. Но мне хотелось потестить именно сервер с прицелом использовать потом для задач с не такими тривиальными вычислениями. А еще я не умею в JS, поэтому с клиентской стороной пришлось бы помучиться, а сервер можно и на питоне запилить, благо инструменты есть.
Для реализации сервера я терзался между bokeh, с которым повозился ранее и связкой Plotly+Dash. В ряде прошлых задач меня очень порадовал bokeh, особенно тем что его можно встраивать в Jupyter Notebook (с Jupyter Labs — не все так однозначно) и то что внутри блокнотов довольно легко организовать интерактивные компоненты не запуская bokeh server (). Bokeh это ворота в мир d3.js для тех кто не умеет в JS.
Для этой задачи я решил использовать связку Plotly+Dash (последний это ворота в мир React для тех кто не умеет в JS). Выбор скорее с целью попробовать. Как можно заметить из сравнения — разница не принципиальна
Переходим к реализации интерфейса.
Подтягиваем нашу CSV, распихиваем по паре датафреймов.
Для того чтобы правильно стилизовать страницу и использовать адаптивный дизайн — разрешаем локальный CSS.
app = dash.Dash(__name__, static_folder='assets') # resource folder
app.scripts.config.serve_locally = True
app.css.config.serve_locally = True
Далее создаем простейший макет используя одну таблицу из двух колонок по 6 из 12 стандартных для адаптивных макетов.
app.layout = html.Div([
# include custom local css to allow two-column responsive
html.Link(href='/assets/twocolumns_dash.css', rel='stylesheet'),
html.Div([ # row div
html.Div([ # column div
html.H3('Average'),
dcc.Graph(id='market-app', ),
html.H4('Select model'),
dcc.Dropdown(id='model_pick', options=model_options, value=None, multi=True)
], className="six columns"),
html.Div([ # column div
html.H3('Distribution'),
dcc.Graph(id='market-app2', ),
html.H4('Select year'),
dcc.Slider(
id='year-slider',
min=years.min(),
max=years.max(),
value=years.min(),
step=None,
marks={str(year): str(year) for year in years}
)
], className="six columns"),
], className="row")
])
Элементы управления реализуются весьма незатейливо, из особенностей — вводов может быть много, а выход только один (например левый график принимает данные от выпадающего меню и от слайдера года, но апдейтить может только один элемент, это фича Dash, для обхода понадобятся костыли).
@app.callback(Output('market-app2', 'figure'),
[Input('model_pick', 'value'), Input('year-slider', 'value')])
def update_figure_dist(selected_models, year_picked):
traces = []
for model in selected_models:
traces.append(go.Bar(
x=df_filtered.loc[model, year_picked, :].index.values.tolist(),
y=df_filtered.loc[model, year_picked, :]['results'].values.tolist(),
name=model
))
return {
'data': traces,
'layout': go.Layout(
xaxis={'title': 'price'},
yaxis={'title': 'offers'},
hovermode='closest',
legend=dict(orientation="h", xanchor="center", y=1.2, x=0.5)
)
}
Готовый интерфейс — http://eu-carmarket.herokuapp.com/
Для данных по Германии в скобках стоит (DE) для Франции (FR).
Слева видим цены за весь период усредненные по каждому году. Здесь и далее все цены в евро, согласно правилам размещения на сайтах — цены с НДС. Справа распределение предложений по цене в выбранный год. Распределение было для многих моделей шумновато, поэтому при построении сглаживается по ближайшим соседям с ядром в 5 элементов (именно глядя на это я решил не брать модели с менее чем 1000 объявлений)
Итак что же мы видим в данных?
Отвечая на вопрос замотивировавший исследование — нет, ситроен не обесценивается со страшной скоростью по сравнению с неподвластной времени Корсой.
Резкое расхождение в цене в 2017 это не первичное сильное обесценивание ситроенов в первый год. Это на самом деле их подорожание с переходом на новое поколение. Теперь вместо оммажа старому ситроену 2CV они больше похожи на аналог Mini, с модными закосами под кроссовер — стильно, модно молодежно.
Если взять одну модель, то разница на немецком и французском рынке потрясает тем, что ее нет, ни в значениях, ни в скорости амортизации (хотя не битая машина старше 2-х лет во Франции встречается только в случае ее хранения с запертом гараже).
Если взять рынок популярной у российских айтишников для переезда Голландии то к немецкой цене надо накинуть процентов 20. При том, что таможенных границ между странами в Европе нет, перевезти просто так купленную машину не получится, если она не проходит как личные вещи которые переезжают вместе с вами. Для этого требуется, чтобы вы в предыдущей стране жили более полугода. В противном случае всю намечающуюся выгоду с вас снимут при попытке поставить авто на регистрацию.
Если сравнить модели крупной кучей видно несколько интересных наблюдений. По прошествии 10 лет все сходится примерно к одной точке, правда обесценивание Пежо, Мазды или Сеата сильнее, чем Фольксвагена Поло, Опеля Меривы или Шкоды Фабии. Таки да, через 10 лет любая машина становится Опелем, но не Корсой, Корсой становятся только избранные вроде С3.
Скорость амортизации не зависит существенно от модели. И от страны. Небольшие отклонения от всеобщего линейного однообразия (например Renault Megane
в 2016, Ford Fiesta в 2017 ) — это просто смена поколения модели.
Поскольку амортизация в данном случае понятие не физическое, она не значит, что машины будут в одном состоянии. На французской будут мятые бока, ссадины, щербатые бамперы, зеркала примотанные на скотч, и наследие тысяч километров езды с игрой в шашечки. Но французы убеждены в том что степень износа такая же как у немцев и платят в соответствии со своими убеждениями. А вот уговорить немцев купить б.у. машину из заботливых рук французских водителей и механиков — врят ли удастся.
По поводу цен в сравнении с Россией. Тут есть некоторая проблема с тем, что многие описанные модели в России не продаются. Из немногого общего для обоих рынков можно найти на сайте фольксвагена новый Поло (хотя в России это седан, а в Европе хетчбэк). В России он стоит от 8300 новый, в Германии новый от 13500, а за 8300 будет 2012 года (в не к столу помянутой Голландии — 16200 новый). Неплохим сравнением может быть представленная везде Киа Рио: Россия — 9000, Германия — 11950 (явный демпинг против немецкого автопатриотизма), Франция — 13700, Голландия — 19950 (Ja-ja, полтора ляма за кирюшу по цене Фольксваген Тигуана / Хюндай Туссан / Ниссан X-trail: обнять, плакать и вспоминать как крутить педали на велосипеде).
Автор: Matshishkapeu