Вы не можете выбрать более 25 тем
Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
718 строки
28 KiB
Python
718 строки
28 KiB
Python
# ---
|
|
# jupyter:
|
|
# jupytext:
|
|
# formats: ipynb,py:percent
|
|
# text_representation:
|
|
# extension: .py
|
|
# format_name: percent
|
|
# format_version: '1.3'
|
|
# jupytext_version: 1.17.3
|
|
# kernelspec:
|
|
# display_name: mpei-iis-project-99040779
|
|
# language: python
|
|
# name: mpei-iis-project-99040779
|
|
# ---
|
|
|
|
# %% [markdown]
|
|
# # Очистка и первичный анализ данных о подержанных автомобилях
|
|
|
|
# %% [markdown]
|
|
# **ОСТОРОЖНО**: Исполнение этого блокнота может тихо (пере)записать (и по умолчанию (пере)записывает) файлы очищенных/аугментированных
|
|
# данных. См. ниже параметры блокнота для papermill.
|
|
|
|
# %% [markdown]
|
|
# Блокнот использует файл сырых данных датасета о подержанных автомобилях ([источник](https://www.kaggle.com/datasets/vijayaadithyanvg/car-price-predictionused-cars)). Блокнот записывает очищенные и аугментированные данные. См. ниже параметры блокнота для papermill.
|
|
|
|
# %%
|
|
from typing import Optional
|
|
|
|
# %% [markdown]
|
|
# Параметры блокнота для papermill:
|
|
|
|
# %% [markdown]
|
|
# (Ячейка с параметрами блокнота для papermill требует осторожного обращения; её содержимое парсится как текст очень простым парсером.)
|
|
|
|
# %% tags=["parameters"]
|
|
data_path: Optional[str] = None
|
|
# Полный путь к файлу (CSV) с исходным датасетом. Если не установлен, ищется файл в `data/<data_relpath>`.
|
|
data_relpath: str = 'cars.csv'
|
|
# Путь к файлу (CSV) с исходным датасетом относительно директории данных `data`. Игнорируется, если установлен data_path.
|
|
|
|
data_clean_csv: bool = True
|
|
# Сохранить ли очищенный датасет как CSV.
|
|
data_clean_csv_path: Optional[str] = None
|
|
# Полный путь к файлу (CSV) для сохранения очищенного датасета. Если не установлен, используется `data/<data_clean_csv_relpath>`.
|
|
data_clean_csv_relpath: str = 'cars.clean.csv'
|
|
# Путь к файлу (CSV) для сохранения очищенного датасета относительно директории данных `data`. Игнорируется, если установлен data_clean_csv_path.
|
|
|
|
data_clean_pickle: bool = False
|
|
# Сохранить ли очищенный датасет как pandas.DataFrame через pickle.
|
|
data_clean_pickle_path: Optional[str] = None
|
|
# Полный путь к файлу (pickle) для сохранения очищенного датасета. Если не установлен, используется `data/<data_clean_pickle_relpath>`.
|
|
data_clean_pickle_relpath: str = 'cars.clean.pickle'
|
|
# Путь к файлу (pickle) для сохранения очищенного датасета относительно директории данных `data`. Игнорируется, если установлен data_clean_pickle_path.
|
|
|
|
data_aug_csv: bool = False
|
|
# Сохранить ли аугментированный датасет как CSV.
|
|
data_aug_csv_path: Optional[str] = None
|
|
# Полный путь к файлу (CSV) для сохранения аугментированного датасета. Если не установлен, используется `data/<data_aug_csv_relpath>`.
|
|
data_aug_csv_relpath: str = 'cars.aug.csv'
|
|
# Путь к файлу (CSV) для сохранения аугментированного датасета относительно директории данных `data`. Игнорируется, если установлен data_aug_csv_path.
|
|
|
|
data_aug_pickle: bool = True
|
|
# Сохранить ли очищенный датасет как pandas.DataFrame через pickle.
|
|
data_aug_pickle_path: Optional[str] = None
|
|
# Полный путь к файлу (pickle) для сохранения очищенного датасета. Если не установлен, используется `data/<data_aug_pickle_relpath>`.
|
|
data_aug_pickle_relpath: str = 'cars.aug.pickle'
|
|
# Путь к файлу (pickle) для сохранения очищенного датасета относительно директории данных `data`. Игнорируется, если установлен data_aug_pickle_path.
|
|
|
|
# %%
|
|
# #%matplotlib ipympl
|
|
|
|
# %%
|
|
import os.path
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
# %%
|
|
import bokeh.io
|
|
import bokeh.models
|
|
import bokeh.plotting
|
|
import bokeh.transform
|
|
import matplotlib.pyplot
|
|
import matplotlib.ticker
|
|
import numpy
|
|
import pandas
|
|
import seaborn
|
|
|
|
# %%
|
|
BASE_PATH = pathlib.Path('..')
|
|
|
|
# %%
|
|
CODE_PATH = BASE_PATH
|
|
sys.path.insert(0, str(CODE_PATH.resolve()))
|
|
|
|
# %%
|
|
import iis_project.plotting_utils
|
|
|
|
# %%
|
|
bokeh.io.output_notebook()
|
|
|
|
# %%
|
|
DATA_PATH = (
|
|
pathlib.Path(os.path.dirname(data_path))
|
|
if data_path is not None
|
|
else (BASE_PATH / 'data')
|
|
)
|
|
|
|
# %% [markdown] jp-MarkdownHeadingCollapsed=true
|
|
# ## Загрузка и обзор данных
|
|
|
|
# %%
|
|
df_orig = pandas.read_csv(data_path if data_path is not None else (DATA_PATH / data_relpath))
|
|
|
|
df_orig = df_orig.rename(columns=lambda s: re.sub(r'\s', '_', s.lower().replace(' ', '_')))
|
|
|
|
# %% [markdown]
|
|
# Обзор строк сырого датасета:
|
|
|
|
# %%
|
|
df_orig.head(0x10)
|
|
|
|
# %% [markdown]
|
|
# Размер сырого датасета:
|
|
|
|
# %%
|
|
len(df_orig)
|
|
|
|
# %% [markdown]
|
|
# Количество непустых значений и тип каждого столбца:
|
|
|
|
# %%
|
|
df_orig.info()
|
|
|
|
# %% [markdown]
|
|
# ## Первичный поиск аномалий и очистка данных
|
|
|
|
# %% [markdown]
|
|
# Подтверждение, что в датасете нет пустых значений:
|
|
|
|
# %%
|
|
all((len(s) == len(df_orig)) for _, s in df_orig.items())
|
|
|
|
# %%
|
|
quantitative_columns_orig = ['selling_price', 'present_price', 'driven_kms', 'year']
|
|
categorical_columns_orig = ['car_name', 'fuel_type', 'selling_type', 'transmission', 'owner']
|
|
|
|
# %%
|
|
for column in filter(lambda s: s not in ('car_name',), categorical_columns_orig):
|
|
# XXX: по идее, переименования категорий стоило бы делать после преобразования к типу категории,
|
|
# Series.cat.rename_categories. Однако... оно просто не работает.
|
|
if pandas.api.types.is_object_dtype(df_orig[column].dtype):
|
|
df_orig[column] = df_orig[column].map(
|
|
lambda s: ' '.join(map(lambda s2: s2.lower(), s.split()))
|
|
)
|
|
df_orig[column] = df_orig[column].astype('category')
|
|
|
|
|
|
# %%
|
|
def normalize_car_name(s):
|
|
return ' '.join(map(lambda s: s.lower(), s.split()))
|
|
|
|
|
|
# %% [markdown]
|
|
# Нормализация текстовых названий моделей автомобилей:
|
|
|
|
# %%
|
|
df_orig['car_name'] = df_orig['car_name'].apply(normalize_car_name)
|
|
|
|
# %% [markdown]
|
|
# Первичные статистики по количественным признакам:
|
|
|
|
# %%
|
|
df_orig[list(quantitative_columns_orig)].describe()
|
|
|
|
# %% [markdown]
|
|
# Категориальные признаки:
|
|
|
|
# %%
|
|
categorical_values_for_columns_orig = {
|
|
column: series.unique()
|
|
for column, series in df_orig[list(categorical_columns_orig)].items()
|
|
}
|
|
|
|
for column, values in categorical_values_for_columns_orig.items():
|
|
if len(values) <= 0x10:
|
|
values_str = ', '.join(map(repr, values))
|
|
else:
|
|
values_str = f'({len(values)} values)'
|
|
print(f'{column!r}: {values_str}')
|
|
|
|
# %% [markdown] raw_mimetype=""
|
|
# Просмотр распределений по отдельным признакам на предмет аномалий:
|
|
|
|
# %%
|
|
_fig, _axis = matplotlib.pyplot.subplots(2, 1, squeeze=True)
|
|
for i, (column, series) in enumerate(df_orig[['selling_price', 'present_price']].items()):
|
|
_ax = _axis[i]
|
|
_ax.set_title(str(column))
|
|
#_ax.set_xscale('symlog')
|
|
_ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
_ = _ax.hist(series, bins=iis_project.plotting_utils.suggest_bins_num(len(series)))
|
|
_fig.tight_layout()
|
|
|
|
# %% [markdown]
|
|
# Есть 1 объект, вероятно, аномальный по `present_price`, но это неоднозначно.
|
|
|
|
# %%
|
|
for column, series in df_orig[['driven_kms']].items():
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ax.set_title(str(column))
|
|
#_ax.set_xscale('symlog')
|
|
_ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
_ = _ax.hist(series, bins=iis_project.plotting_utils.suggest_bins_num(len(series)))
|
|
|
|
# %% [markdown]
|
|
# Есть 1 аномальный объект по `driven_kms` (аномально большой пробег).
|
|
|
|
# %%
|
|
for column, series in df_orig[['year']].items():
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ax.set_title(str(column))
|
|
_ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
_ = _ax.hist(series, bins=iis_project.plotting_utils.suggest_bins_num(len(series)))
|
|
|
|
# %%
|
|
for column, series in df_orig[
|
|
list(filter(lambda s: s not in ('car_name',), categorical_columns_orig))
|
|
].items():
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ax.set_title(str(column))
|
|
_ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
value_counts = series.value_counts()
|
|
_ = _ax.bar(tuple(map(str, value_counts.index)), value_counts)
|
|
|
|
# %% [markdown]
|
|
# Есть 2 исключительных объекта по топливу `fuel_type` (автомобиль на природном газе
|
|
# (CNG)) — и 1 исключительный объект по типу владельца `owner` (3).
|
|
|
|
# %% [markdown]
|
|
# Внимательное рассмотрение аномальных объектов:
|
|
|
|
# %%
|
|
labels_to_drop_from_orig = []
|
|
|
|
# %%
|
|
df_orig.loc[df_orig['owner'].isin((3,))]
|
|
|
|
# %% [markdown]
|
|
# Объект, исключительный по `owner` (3), исключается из датасета, не в последнюю очередь
|
|
# из-за неясности смысла значений `owner`.
|
|
|
|
# %%
|
|
labels_to_drop_from_orig.extend(df_orig.loc[df_orig['owner'].isin((3,))].index)
|
|
|
|
# %%
|
|
df_orig.loc[(df_orig['present_price'] >= 60.) | (df_orig['driven_kms'] >= 400000) | (df_orig['fuel_type'].isin(('CNG',)))]
|
|
|
|
# %% [markdown]
|
|
# Аномально большой пробег у автомобиля #196 ещё и является очень круглым числом. Этот объект
|
|
# исключается из датасета как вероятно недостоверный.
|
|
#
|
|
# 2 автомобиля на природном газе решено пока не исключать, т.к. с учётом и так малого размера
|
|
# датасета возможность хоть как-то предсказывать цены для автомобилей на природном газе сочтена
|
|
# сравнительно ценной.
|
|
#
|
|
# Автомобиль с исключительно высокой `present_price` не показывает других аномалий (у него и `selling_price` высокая в датасете), поэтому он оставлен.
|
|
|
|
# %%
|
|
labels_to_drop_from_orig.extend((196,))
|
|
|
|
# %%
|
|
df_clean = df_orig.drop(labels_to_drop_from_orig)
|
|
|
|
# %%
|
|
#bokeh_source_df_clean = bokeh.models.ColumnDataSource(df_clean)
|
|
|
|
# %% [markdown]
|
|
# По названиям и распределениям признаков `selling_price` и `present_price` есть большое подозрение,
|
|
# что это изначальная цена и цена продажи с пробегом, но перепутанные местами.
|
|
|
|
# %% [raw] raw_mimetype=""
|
|
# present_price_ratio = df_clean['selling_price'] / df_clean['present_price']
|
|
#
|
|
# _fig, _ax = matplotlib.pyplot.subplots()
|
|
# _ = _ax.set_xlabel('driven_kms')
|
|
# _ = _ax.set_ylabel('selling_price / present_price')
|
|
# _ax.set_xscale('log')
|
|
# _ax.grid(True)
|
|
# _ = _ax.scatter(df_clean['driven_kms'], present_price_ratio, alpha=0.5)
|
|
# _ = _ax.set_ylim((0., None))
|
|
|
|
# %%
|
|
_src = bokeh.models.ColumnDataSource({
|
|
**dict(df_clean[['car_name', 'driven_kms', 'year', 'selling_price', 'present_price']].items()),
|
|
'present_price_ratio': (df_clean['selling_price'] / df_clean['present_price']),
|
|
})
|
|
_fig = bokeh.plotting.figure(
|
|
x_axis_type='log',
|
|
x_axis_label='driven_kms', y_axis_label='selling_price / present_price',
|
|
)
|
|
_ = _fig.scatter(
|
|
'driven_kms', 'present_price_ratio',
|
|
source=_src,
|
|
size=12., alpha=0.5,
|
|
)
|
|
_fig.y_range.start = 0
|
|
_fig.add_tools(
|
|
bokeh.models.HoverTool(
|
|
tooltips=[
|
|
('present price', '@present_price'),
|
|
('selling price ', '@selling_price'),
|
|
('name', '@car_name'),
|
|
('year', '@year',),
|
|
('index', '$index',)
|
|
],
|
|
),
|
|
)
|
|
bokeh.plotting.show(_fig)
|
|
|
|
# %% [markdown]
|
|
# Указанное выше подозрение подтверждается, `selling_price` и `present_price` нужно поменять местами.
|
|
|
|
# %%
|
|
df_clean[['selling_price', 'present_price']] = df_clean[['present_price', 'selling_price']]
|
|
|
|
# %% [markdown] jp-MarkdownHeadingCollapsed=true
|
|
# ## Сохранение очищенных данных
|
|
|
|
# %%
|
|
if data_clean_csv:
|
|
df_clean.to_csv(
|
|
(
|
|
data_clean_csv_path
|
|
if data_clean_csv_path is not None
|
|
else (DATA_PATH / data_clean_csv_relpath)
|
|
),
|
|
index=False,
|
|
)
|
|
|
|
# %%
|
|
if data_clean_pickle:
|
|
import pickle
|
|
with open(
|
|
(
|
|
data_clean_pickle_path
|
|
if data_clean_pickle_path is not None
|
|
else (DATA_PATH / data_clean_pickle_relpath)
|
|
),
|
|
'wb',
|
|
) as out_file:
|
|
pickle.dump(df_clean, out_file)
|
|
|
|
# %%
|
|
df = df_clean.copy(deep=False)
|
|
|
|
# %% [markdown]
|
|
# TODO: Разделить блокнот на два — очистка данных и анализ.
|
|
|
|
# %% [markdown]
|
|
# ## Анализ и аугментация данных
|
|
|
|
# %%
|
|
quantitative_columns = ['selling_price', 'present_price', 'driven_kms', 'year']
|
|
categorical_columns = ['car_name', 'fuel_type', 'selling_type', 'transmission', 'owner']
|
|
|
|
# %% [markdown]
|
|
# Количество объектов:
|
|
|
|
# %%
|
|
len(df)
|
|
|
|
# %% [markdown]
|
|
# Количество непустых значений и тип каждого столбца:
|
|
|
|
# %%
|
|
df.info()
|
|
|
|
# %% [markdown]
|
|
# Добавим синтетический признак возраста. Для отсчёта предполагается актуальность датасета на 2019 год;
|
|
# учитывая, что год в датасете задан целым числом, это даёт возраст минимум 1 год для каждого объекта.
|
|
|
|
# %%
|
|
df['age'] = ((max(df['year']) + 1) - df['year']).astype(float)
|
|
quantitative_columns.append('age')
|
|
|
|
# %% [markdown]
|
|
# Вообще говоря, отношение `present_price / selling_price` может быть полезно при изучении данных
|
|
# или обучении моделей. Добавим такой признак `present_price_ratio`:
|
|
|
|
# %%
|
|
df['present_price_ratio'] = df['present_price'] / df['selling_price']
|
|
quantitative_columns.append('present_price_ratio')
|
|
|
|
# %% [markdown]
|
|
# Добавим некоторые потенциально полезные синтетические признаки - логарифмы некоторых количественных признаков:
|
|
|
|
# %%
|
|
for column in ('selling_price', 'present_price', 'driven_kms'):
|
|
df[f'log_{column}'] = numpy.log10(df[column])
|
|
|
|
df['log_age'] = numpy.log10(numpy.maximum(1., df['age']))
|
|
|
|
# %%
|
|
bokeh_source_df = bokeh.models.ColumnDataSource(df)
|
|
|
|
# %% [markdown]
|
|
# Первичные статистики по количественным признакам:
|
|
|
|
# %%
|
|
df[list(quantitative_columns)].describe()
|
|
|
|
# %% [markdown]
|
|
# Категориальные признаки:
|
|
|
|
# %%
|
|
categorical_values_for_columns = {
|
|
column: series.unique()
|
|
for column, series in df[list(categorical_columns)].items()
|
|
}
|
|
|
|
for column, values in categorical_values_for_columns.items():
|
|
if len(values) <= 0x10:
|
|
values_str = ', '.join(map(repr, values))
|
|
else:
|
|
values_str = f'({len(values)} values)'
|
|
print(f'{column!r}: {values_str}')
|
|
|
|
# %% [markdown]
|
|
# Матрица корреляций количественных признаков:
|
|
|
|
# %%
|
|
df_corr = df[[
|
|
'present_price', 'log_present_price',
|
|
'selling_price', 'log_selling_price',
|
|
'present_price_ratio',
|
|
'driven_kms', 'log_driven_kms',
|
|
'age', 'log_age',
|
|
]].corr('pearson', min_periods=2)
|
|
|
|
# %% [markdown]
|
|
# Из неочевидных выводов по матрице корреляций, существует слабая, но заметная прямая корреляция
|
|
# между изначальной ценой автомобиля и пробегом к дате последующей продажи.
|
|
|
|
# %%
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_im = _ax.imshow(df_corr.to_numpy(), cmap='RdBu', vmin=-1., vmax=+1., origin='upper')
|
|
_ax.tick_params(top=True, labeltop=True, bottom=False, labelbottom=False)
|
|
_ = _ax.set_xticks(range(len(df_corr.columns)), labels=df_corr.columns, rotation=90.)
|
|
_ = _ax.set_yticks(range(len(df_corr.index)), labels=df_corr.index)
|
|
if len(df_corr.columns) <= 10:
|
|
for i in range(len(df_corr.index)):
|
|
for j in range(len(df_corr.columns)):
|
|
if i == j:
|
|
continue
|
|
val = df_corr.iloc[(i, j)]
|
|
_ = _ax.text(
|
|
j, i, '{:+.2f}'.format(val),
|
|
ha='center', va='center',
|
|
color=('white' if abs(val) >= 0.5 else 'black'),
|
|
fontsize='small',
|
|
)
|
|
else:
|
|
_ = _fig.colorbar(_im)
|
|
|
|
# %% [markdown]
|
|
# Совместное распределение изначальной цены продажи и цены с пробегом:
|
|
|
|
# %% [raw] raw_mimetype=""
|
|
# _fig, _ax = matplotlib.pyplot.subplots()
|
|
# _ = _ax.set_xlabel('selling_price')
|
|
# _ = _ax.set_ylabel('present_price')
|
|
# _ax.grid(True)
|
|
# _ = seaborn.histplot(
|
|
# x=df['selling_price'], y=df['present_price'],
|
|
# bins=tuple(
|
|
# numpy.geomspace(
|
|
# min(series), max(series),
|
|
# (iis_project.plotting_utils.suggest_bins_num(len(series)) + 1),
|
|
# )
|
|
# for _, series in df[['selling_price', 'present_price']].items()
|
|
# ),
|
|
# #thresh=None, palette='Blues',
|
|
# cbar=True,
|
|
# #hue_norm=matplotlib.colors.LogNorm(0, 64),
|
|
# ax=_ax,
|
|
# )
|
|
# _ax.set_xscale('log')
|
|
# _ax.set_yscale('log')
|
|
|
|
# %%
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ = _ax.set_xlabel('selling_price')
|
|
_ = _ax.set_ylabel('present_price')
|
|
_ax.set_xscale('log')
|
|
_ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
_scatter = _ax.scatter(
|
|
df['selling_price'], df['present_price'], c=df['selling_type'].cat.codes,
|
|
alpha=0.5,
|
|
)
|
|
_ = _ax.legend(
|
|
*_scatter.legend_elements(
|
|
fmt=matplotlib.ticker.FuncFormatter(lambda i, _: df['selling_type'].cat.categories[int(i)]),
|
|
),
|
|
)
|
|
|
|
# %% [markdown]
|
|
# Существует очень сильное разделение по ценам при продаже подержанных автомобилей дилерами
|
|
# и частными лицами; частные лица обычно продают автомобили намного дешевле, чем дилеры (хотя этот
|
|
# вывод не учитывает возможное влияние других переменных).
|
|
|
|
# %% [markdown]
|
|
# Совместные распределения отношения цены с пробегом к изначальной, пробега, года выпуска:
|
|
|
|
# %%
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ = _ax.set_xlabel('driven_kms')
|
|
_ = _ax.set_ylabel('present_price_ratio')
|
|
_ax.grid(True)
|
|
_ = seaborn.histplot(
|
|
x=df['driven_kms'], y=df['present_price_ratio'],
|
|
bins=(
|
|
numpy.geomspace(
|
|
min(df['driven_kms']), max(df['driven_kms']),
|
|
(iis_project.plotting_utils.suggest_bins_num(len(df['driven_kms'])) + 1),
|
|
),
|
|
iis_project.plotting_utils.suggest_bins_num(len(df['present_price_ratio'])),
|
|
),
|
|
#thresh=None, palette='Blues',
|
|
cbar=True,
|
|
#hue_norm=matplotlib.colors.LogNorm(0, 64),
|
|
ax=_ax,
|
|
)
|
|
_ax.set_xscale('log')
|
|
_ = _ax.set_ylim((0., None))
|
|
|
|
# %%
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ = _ax.set_xlabel('age')
|
|
_ = _ax.set_ylabel('present_price / selling_price')
|
|
_ax.grid(True)
|
|
_ = seaborn.histplot(
|
|
x=df['age'], y=df['present_price_ratio'],
|
|
bins=(
|
|
iis_project.plotting_utils.suggest_bins_num(len(df['age'])),
|
|
iis_project.plotting_utils.suggest_bins_num(len(df['present_price_ratio'])),
|
|
),
|
|
#thresh=None, palette='Blues',
|
|
cbar=True,
|
|
#hue_norm=matplotlib.colors.LogNorm(0, 64),
|
|
ax=_ax,
|
|
)
|
|
#_ax.set_xscale('log')
|
|
_ = _ax.set_ylim((0., None))
|
|
|
|
# %% [raw] raw_mimetype=""
|
|
# _fig, _ax = matplotlib.pyplot.subplots()
|
|
# _ = _ax.set_xlabel('age')
|
|
# _ = _ax.set_ylabel('driven_kms')
|
|
# _ax.grid(True)
|
|
# _ = seaborn.histplot(
|
|
# x=df['age'], y=df['driven_kms'],
|
|
# bins=(
|
|
# iis_project.plotting_utils.suggest_bins_num(len(df['age'])),
|
|
# numpy.geomspace(
|
|
# min(df['driven_kms']), max(df['driven_kms']),
|
|
# (iis_project.plotting_utils.suggest_bins_num(len(df['driven_kms'])) + 1),
|
|
# ),
|
|
# ),
|
|
# #thresh=None, palette='Blues',
|
|
# cbar=True,
|
|
# #hue_norm=matplotlib.colors.LogNorm(0, 64),
|
|
# ax=_ax,
|
|
# )
|
|
# #_ax.set_xscale('log')
|
|
# _ax.set_yscale('log')
|
|
|
|
# %%
|
|
_fig = bokeh.plotting.figure(
|
|
x_axis_type='linear', y_axis_type='log',
|
|
x_axis_label='age', y_axis_label='driven_kms',
|
|
)
|
|
_mapper = bokeh.models.LinearColorMapper(
|
|
palette='Viridis256',
|
|
low=min(0., df['present_price_ratio'].min()), high=max(1., df['present_price_ratio'].max()),
|
|
)
|
|
_ = _fig.scatter(
|
|
'age', 'driven_kms',
|
|
color=bokeh.transform.transform('present_price_ratio', _mapper),
|
|
source=bokeh_source_df,
|
|
size=12., alpha=0.75,
|
|
)
|
|
_fig.add_tools(
|
|
bokeh.models.HoverTool(
|
|
tooltips=[
|
|
('present price', '@present_price'),
|
|
('selling price ', '@selling_price'),
|
|
('year', '@year',),
|
|
('index', '$index',),
|
|
],
|
|
),
|
|
)
|
|
_fig.add_layout(bokeh.models.ColorBar(color_mapper=_mapper), 'right')
|
|
bokeh.plotting.show(_fig)
|
|
|
|
# %% [markdown]
|
|
# В частности, существует сильная связь между и возрастом автомобиля и отношением цены с пробегом
|
|
# к изначальной. Довольно много объектов, но далеко не все, сосредоточены около одной точки
|
|
# в пространстве данных.
|
|
|
|
# %% [markdown]
|
|
# Распределения цен с пробегом по категориям:
|
|
|
|
# %%
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ = _ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
_ = df.boxplot('present_price', by='fuel_type', ax=_ax, patch_artist=False)
|
|
#_ax.set_ylim((0, None))
|
|
|
|
# %% [markdown]
|
|
# Автомобили на дизеле дороже автомобилей на бензине, а также реже продаются
|
|
# за крайне низкие цены.
|
|
|
|
# %%
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ = _ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
_ = df.boxplot('present_price', by='selling_type', ax=_ax, patch_artist=False)
|
|
#_ax.set_ylim((0, None))
|
|
|
|
# %% [markdown]
|
|
# Частные лица обычно продают автомобили намного дешевле, чем дилеры (хотя этот вывод не учитывает
|
|
# возможное влияние других переменных). Отмечены множество автомобилей, продаваемых дилерами
|
|
# по особо высоким ценам.
|
|
|
|
# %% [markdown]
|
|
# Проверка возможной систематической разницы в пробеге между категориями:
|
|
|
|
# %%
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ = _ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
_ = df.boxplot('driven_kms', by='fuel_type', ax=_ax, patch_artist=False)
|
|
#_ax.set_ylim((0, None))
|
|
|
|
# %%
|
|
_fig, _ax = matplotlib.pyplot.subplots()
|
|
_ = _ax.set_yscale('log')
|
|
_ax.grid(True)
|
|
_ = df.boxplot('driven_kms', by='selling_type', ax=_ax, patch_artist=False)
|
|
#_ax.set_ylim((0, None))
|
|
|
|
# %% [markdown]
|
|
# Большой систематической разницы в пробеге между категориями нет.
|
|
|
|
# %% [markdown]
|
|
# ## Сохранение аугментированных данных
|
|
|
|
# %%
|
|
if data_aug_csv:
|
|
df.to_csv(
|
|
(data_aug_csv_path if data_aug_csv_path is not None else (DATA_PATH / data_aug_csv_relpath)),
|
|
index=False,
|
|
)
|
|
|
|
# %%
|
|
if data_aug_pickle:
|
|
import pickle
|
|
with open(
|
|
(
|
|
data_aug_pickle_path
|
|
if data_aug_pickle_path is not None
|
|
else (DATA_PATH / data_aug_pickle_relpath)
|
|
),
|
|
'wb',
|
|
) as out_file:
|
|
pickle.dump(df, out_file)
|
|
|
|
# %% [markdown]
|
|
# ## Выводы по исследованию
|
|
|
|
# %% [markdown]
|
|
# Выполнена очистка датасета: удалены несколько аномальных объектов, переименованы некоторые
|
|
# ошибочно названные признаки. (Пропущенных значений в датасете нет.)
|
|
|
|
# %% [markdown]
|
|
# Датасет дополнен (аугментирован) потенциально полезными синтетическими признаками:
|
|
# отношение цены с пробегом к изначальной цене, возраст (предполагаемый на основе года выпуска
|
|
# автомобиля и распределения этих годов выпуска в датасете), логарифмы количественных величин.
|
|
# Аугментированная версия сохраняется отдельно.
|
|
|
|
# %% [markdown]
|
|
# Предварительно подтверждена возможность определения рыночной цены автомобиля с пробегом
|
|
# по использованным признакам, **в особенности** по следующим: исходная цена, возраст и пробег
|
|
# автомобиля, тип продающего лица (дилер или частное лицо), топливо (автомобили на дизельном
|
|
# топливе редко бывают дешёвыми).
|
|
#
|
|
# * Цена продажи с пробегом сильно линейно коррелирует с изначальной ценой.
|
|
#
|
|
# * Интересно, что возраст автомобиля является заметно лучшим предиктором снижения стоимости,
|
|
# чем пробег, при этом корреляция между возрастом и пробегом существенная, но не определяющая.
|
|
#
|
|
# * Существует огромная разница в ценах у дилеров и частных лиц (у частных лиц дешевле в разы).
|
|
#
|
|
# * Существует слабая, но заметная прямая корреляция между изначальной ценой автомобиля и пробегом
|
|
# к дате последующей продажи.
|
|
#
|
|
# * Датасет не очень однороден (у него есть "тяжёлый центр"), и с малым количеством объектов
|
|
# это может создать проблемы с устойчивостью предсказания цен. Рекомендуется применение
|
|
# робастных методов ограниченной сложности; однако прямая линейная регрессия для предсказания
|
|
# цены проодажи может всё-таки оказаться не лучшим методом.
|