# ---
# 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)) &mdash; и 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: Разделить блокнот на два &mdash; очистка данных и анализ.

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