Вы не можете выбрать более 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: python3_venv
 | 
						|
#     language: python
 | 
						|
#     name: python3_venv
 | 
						|
# ---
 | 
						|
 | 
						|
# %% [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]
 | 
						|
# Предварительно подтверждена возможность определения рыночной цены автомобиля с пробегом
 | 
						|
# по использованным признакам, **в особенности** по следующим: исходная цена, возраст и пробег
 | 
						|
# автомобиля, тип продающего лица (дилер или частное лицо), топливо (автомобили на дизельном
 | 
						|
# топливе редко бывают дешёвыми).
 | 
						|
#
 | 
						|
# * Цена продажи с пробегом сильно линейно коррелирует с изначальной ценой.
 | 
						|
#
 | 
						|
# * Интересно, что возраст автомобиля является заметно лучшим предиктором снижения стоимости,
 | 
						|
#   чем пробег, при этом корреляция между возрастом и пробегом существенная, но не определяющая.
 | 
						|
#
 | 
						|
# * Существует огромная разница в ценах у дилеров и частных лиц (у частных лиц дешевле в разы).
 | 
						|
#
 | 
						|
# * Существует слабая, но заметная прямая корреляция между изначальной ценой автомобиля и пробегом
 | 
						|
#   к дате последующей продажи.
 | 
						|
#
 | 
						|
# * Датасет не очень однороден (у него есть "тяжёлый центр"), и с малым количеством объектов
 | 
						|
#   это может создать проблемы с устойчивостью предсказания цен. Рекомендуется применение
 | 
						|
#   робастных методов ограниченной сложности; однако прямая линейная регрессия для предсказания
 | 
						|
#   цены проодажи может всё-таки оказаться не лучшим методом.
 |