лабораторная работа 1; установить и записать порядки установки и работы с Jupyter; удалить некоторый ненужный код

lab_1/master lab_1_1
syropiatovvv 1 месяц назад
Родитель 8bbd3c8da5
Сommit e01bc467b0
Подписано: syropiatovvv
Идентификатор GPG ключа: 297380B8143A31BD

4
.gitignore поставляемый

@ -1,6 +1,10 @@
### Python
*.pyc
### Jupyter
.ipynb_checkpoints/
*.ipynb
### Project
# virtual environments
.venv/

@ -1,3 +1,66 @@
# Лабораторный проект по курсу "Интеллектуальные информационные системы"
**Выполняет**: **Сыропятов В.В.** (А-01м-24)
## Данные
Используемый датасет: [Car price prediction(used cars)
](https://www.kaggle.com/datasets/vijayaadithyanvg/car-price-predictionused-cars/data) —
продажа подержанных автомобилей на рынке в Индии.
## Установка
### Общий порядок
**Внимание**: Здесь описан только общий порядок установки. Определённые части проекта могут требовать установки по отдельным инструкциям.
1. Проект разработан для Python 3.10–3.12. Установите совместимую версию Python ([Download Python](https://www.python.org/downloads/)).
2. Скопируйте/склонируйте репозиторий в выделенную директорию и перейдите в неё.
```sh
tar -xzf iis-project.tar.xz
cd iis-project
```
3. Создайте виртуальное окружение Python.
```sh
python -m venv .venv
```
Активируйте созданное виртуальное окружение Python.
* **Linux**:
```sh
source .venv/bin/activate
```
* **Windows**:
```ps
.\.venv\Scripts\activate
```
4. Установите зависимости для необходимых частей проекта. См. **Зависимости**.
5. **При необходимости** скачайте данные. Каноническое расположение для данных проекта: `data/`.
### Зависимости
#### Общие зависимости
Зависимости — пакеты Python — записаны в файле `requirements.txt` (см. **Пакеты Python**).
#### Пакеты Python
Установка/обновление пакетов Python в активное окружение из файла `requirements.txt`:
```sh
pip install -U -r requirements.txt
```
## Разведочный анализ данных (EDA)
См. `eda/README.md`.

@ -0,0 +1,73 @@
# Обоснование порядка работы с блокнотами Jupyter
[Архитектура](https://docs.jupyter.org/en/latest/projects/architecture/content-architecture.html) экосистемы Jupyter очень сложна, особенно если учитывать сторонние дополнения; данное обоснование может содержать неточности.
Концептуально, важные для темы компоненты в архитектуре Jupyter:
* *Ядро* ([kernel](https://docs.jupyter.org/en/latest/projects/kernels.html#kernels)) — независимый процесс, непосредственно исполняющий код на конкретном языке программирования (основываясь на конкретном экземпляре интерпретатора) и предоставляющий API для передачи исполняемого кода и получения результатов. (Самое популярное ядро — `ipykernel` — использует [IPython](https://ipython.org/), программный пакет, реализующий Python с расширенными возможностями интерактивной работы.)
* *Сервер* (server) и *приложение* (application) — компоненты, обеспечивающие выполнение прикладных задач. Так, стандартный `jupyter_server` и использующие его популярные [веб-приложения](https://jupyter.org/) (Notebook, JupyterLab) реализуют редактирование и исполнение блокнотов и других файлов (делегируя только непосредственное исполнение кода ядрам) и все пользовательские интерфейсы.
Чтобы пользователь могу выбрать ядро для использования (а значит, выбрать язык программирования и конкретный экземпляр интерпретатора), сервер Jupyter выполняет поиск доступных *спецификаций ядер* (kernelspec).
В случае Python, выбор конкретного экземпляра интерпретатора важен в частности потому, что это означает и выбор конкретного виртуального окружения. И здесь начинаются проблемы.
* Сервер Jupyter реализует только примитивный механизм поиска спецификаций ядер. "**Ядра [устанавливается](https://ipython.readthedocs.io/en/stable/install/kernel_install.html#installing-the-ipython-kernel)**" (install) глобально в систему либо для отдельного пользователя, что означает размещение спецификаций ядер под глобально уникальными именами (с учётом приоритета ядер, установленных для конкретного пользователя). Механизма локального размещения спецификаций ядер просто не предусмотрено. (Сервер Jupyter, возможно, реализует и другой механизм поиска *активных* ядер, который здесь нерелевантен.)
* Расширение [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) для Visual Studio Code самостоятельно реализует ограниченные функции сервера и приложения. Однако оно имеет [продвинутый механизм](https://code.visualstudio.com/docs/datascience/jupyter-kernel-management#_python-environments) поиска спецификаций ядер. Спецификации ядер, размещённые в виртуальном окружении (в директории данных (например, `share`), далее `jupyter/kernels`), также подбираются и могут быть использованы пользователем. Они должны иметь *локально* (в отношении виртуального окружения) уникальные имена, **однако** они делят пространство имён с глобально установленными ядрами, просто с более высоким приоритетом.
* Блокнот Jupyter сохраняет имя нужного для его исполнения ядра в своих метаданных (`kernelspec.name`) (даже при использовании Jupytext).
Таким образом, отдельный блокнот требует ядро с определённым именем, это имя ищется в системе глобально, и при использовании оригинальной системы Jupyter нет возможности даже определить это имя локально для отдельного виртуального окружения с приоритетом выше, чем у ядер, установленных глобально. Т.е. блокнот существенно привязывается к окружению конкретной системы, что мешает переносимости и контролю версий, а также захламляет систему глобально установленными ядрами.
Найденные варианты смягчения проблемы:
1. Разворачивать Jupyter целиком внутри виртуального окружения.
При этом подходе проблемы с выбором ядра, вероятно, нет вообще.
**\*** Нет полной уверенности, что Jupyter в виртуальном окружении действительно будет использовать его экземпляр интерпретатора для ядра по умолчанию.
**+** Простота работы, по крайней мере в пределах одного виртуального окружения.
**−** Повышенный расход ресурсов хранилища; пакет `notebook` версии 7.4 со всеми зависимостями занимает порядка 264 МБ на диске 12 тысячами файлов.
**−** Возможные проблемы из-за изоляции среды Jupyter от других проектов.
2. Использовать только Visual Studio Code с расширением Jupyter и пакетом `ipykernel` в каждом проекте; не использовать оригинальную систему (сервер Jupyter и веб-приложения) вообще.
В каждом виртуальном окружении также нужен пакет `ipykernel` для работы с ядром.
**+** Довольно надёжная локальная связь ядер с виртуальными окружениями.
**−** Большая проблема с переносимостью блокнотов из-за необходимости использовать конкретную IDE с расширением вместо оригинальной системы.
**−** Ограниченный функционал и нестабильность расширения Jupyter для Visual Studio Code.
**−** Потенциальная необходимость ручных хаков в случае использования более одного виртуального окружения в одном проекте.
3. Использовать единственное определённое и зафиксированное для проекта имя ядра; устанавливать ядро с указанным именем глобально либо, для использования нескольких ядер (например, для нескольких виртуальных окружений для одного проекта), править каждый блокнот локально.
**+** Относительно простой порядок работы в случае с единственным виртуальным окружением для каждого проекта.
**−** Необходимо обеспечить уникальность имени ядра для проекта; имя ядра может стать нечитаемым.
**−** Если появляется необходимость иметь несколько ядер для одного проекта (например, для нескольких виртуальных окружений) или переименовать единственное ядро, необходимо применять локальные правки к каждому релевантному блокноту, что существенно усложняет контроль версий.
**−** Установленные спецификации ядер остаются в системе глобально и захламляют систему.
4. Запускать ядро самостоятельно из виртуального окружения и использовать механизм поиска активных ядер.
Есть интерфейсы, намекающие на возможность этого варианта (см. пакет `ipykernel_launcher`), но неясно, насколько это было бы практично.
**\*** Непонятно, возможно ли это.
**\*** Непонятно, практично ли это.
**+** Довольно надёжная локальная связь ядер с виртуальными окружениями.
**−** Довольно много телодвижений для обычной работы с Jupyter.
**−** Очень сложно в реализации, может потребовать разработки собственного ПО.
Вариант **3** выбран на данный момент для данного проекта из-за экономии ресурсов, переносимости и относительной простоты порядка работы с блокнотами в тривиальном рабочем процессе.

@ -0,0 +1,154 @@
# Разведочный анализ данных (EDA)
## Блокноты Jupyter
* `cars_eda` — Очистка и первичный анализ данных о подержанных автомобилях.
Использует CSV-файл сырых данных из [датасета](https://www.kaggle.com/datasets/vijayaadithyanvg/car-price-predictionused-cars/data). Каноническое расположение файла данных: `data/cars.csv`.
Создаёт файлы очищенных данных (по умолчанию &mdash; CSV) и аугментированных данных (по умолчанию &mdash; pickle). Канонические расположения: соответственно `data/cars.clean.<ext>` и `data/cars.aug.<ext>`, с заменой `<ext>` на расширение в зависимости от формата: `csv` для CSV, `pickle` для pickle.
**Выводы по исследованию**:
* Выполнена очистка датасета: удалены несколько аномальных объектов, переименованы некоторые ошибочно названные признаки. (Пропущенных значений в датасете нет.)
* Датасет дополнен (аугментирован) потенциально полезными синтетическими признаками: отношение цены с пробегом к изначальной цене, возраст (предполагаемый на основе года выпуска автомобиля и распределения этих годов выпуска в датасете), логарифмы количественных величин. Аугментированная версия сохраняется отдельно.
* Предварительно подтверждена возможность определения рыночной цены автомобиля с пробегом по использованным признакам, **в особенности** по следующим: исходная цена, возраст и пробег автомобиля, тип продающего лица (дилер или частное лицо), топливо (автомобили на дизельном топливе редко бывают дешёвыми).
* Цена продажи с пробегом сильно линейно коррелирует с изначальной ценой.
* Интересно, что возраст автомобиля является заметно лучшим предиктором снижения стоимости, чем пробег, при этом корреляция между возрастом и пробегом существенная, но не определяющая.
* Существует огромная разница в ценах у дилеров и частных лиц (у частных лиц дешевле в разы).
* Существует слабая, но заметная прямая корреляция между изначальной ценой автомобиля и пробегом к дате последующей продажи.
* Датасет не очень однороден (у него есть "тяжёлый центр"), и с малым количеством объектов это может создать проблемы с устойчивостью предсказания цен. Рекомендуется применение робастных методов ограниченной сложности; однако прямая линейная регрессия для предсказания цены проодажи может всё-таки оказаться не лучшим методом.
Графические артефакты исследования сохранены в директории `./cars_eda_figures/`.
## Установка
Для EDA необходимы общие зависимости, см. **Общие зависимости** в `README.md`.
Для EDA используется среда [Jupyter](https://jupyter.org/). Т.к. блокноты хранятся в текстовом формате под контролем версий, нужно также дополнение [Jupytext](https://jupytext.readthedocs.io/en/latest/) (как минимум для ручной конвертации блокнотов; см. ниже).
Опционально можно использовать дополнение [papermill](https://papermill.readthedocs.io/en/latest/) для простого параметризованного исполнения блокнотов.
### Общий порядок
**Внимание**: Оптимальный порядок установки и конфигурации Jupyter для работы с проектом неоднозначен. См. обоснование выбранного здесь порядка работы с блокнотами Jupyter и возможные альтернативные варианты в `eda/docs/jupyter_workflow_motivation.md`.
1. Выполните установку общих зависимостей, если это ещё не выполнено, см. **Общие зависимости** в `README.md`.
2. Jupyter и дополнения должны быть установлены в систему, а НЕ в виртуальное окружение. При необходимости деактивируйте виртуальное окружение.
```sh
deactivate
```
3. [Установите Jupyter](https://jupyter.org/install) и Jupytext в систему (НЕ в виртуальное окружение).
```sh
pip install -U notebook
```
4. Установите Jupytext в систему (НЕ в виртуальное окружение).
```sh
pip install -U jupytext
```
Полная инструкция по установке: [Installation &#8212; Jupytext documentation](https://jupytext.readthedocs.io/en/latest/install.html).
5. **Опционально**, установите papermill в систему (НЕ в виртуальное окружение).
```sh
pip install -U papermill
```
Полная инструкция по установке: [Installation - papermill 2.4.0 documentation](https://papermill.readthedocs.io/en/stable/installation.html).
**Шаги 6&ndash;7** **необходимо** выполнить **только** если Вы желаете использовать что-то кроме расширения Jupyter для Visual Studio Code для работы с блокнотами **или собираетесь** коммитить писать блокноты под контроль версий.
6. Активируйте виртуальное окружение вновь.
7. Установите ядро Jupyter, связанное с данным виртуальным окружением, в систему. Используйте следующее имя для ядра: `mpei-iis-99040779`. (*Да, это странно, но это признано лучшим подходом на данный момент; см. обоснование в `docs/dev/jupyter_workflow_motivation.md`.*)
```sh
ipython kernel install --user --name=mpei-iis-99040779
```
(Эта команда устанавливает виртуальное окружение глобально для текущего пользователя; для установки глобально в систему, уберите флаг `--user`).
**Внимание**: На данном этапе могут отсутствовать пригодные для прямого редактирования блокноты `.ipynb` (например, если проект развёртывается с нуля). Об использовании спаренных блокнотов и конвертации форматов см. **Использование Jupytext**.
#### Удаление ядра из системы
Во избежание захламления системы ядро можно удалить из системы **позднее**, вызвав глобально установленный Jupyter (НЕ в виртуальном окружении) и передав имя ядра.
```sh
jupyter kernelspec uninstall mpei-iis-99040779
```
**Примечание**: Установленное ядро НЕ занимает значительные ресурсы хранилища. На самом деле в систему устанавливается крайне лёгкая *спецификация ядра*, ограниченная коротким текстовым файлом и иконками.
### Зависимости
Используемые непосредственно кодом проекта зависимости для разведочного анализа данных (EDA) (директория `eda/`) &mdash; пакеты Python &mdash; на данный момент включены в общие зависимости (см. выше).
### Использование Jupytext
Описанные ниже команды `jupytext` используют глобальной установленный экземпляр Jupytext (однако его можно запускать и изнутри виртуального окружения).
Для автоматической синхронизации связанных блокнотов (включая создание блокнотов отсутствующих, но ожидаемых форматов):
```sh
jupytext --sync eda/cars_eda.py
```
Jupytext довольно удобно работает в оригинальной среде Jupyter, синхронизируя изменения связанных файлов на лету при работе в Jupyter, **ориентируясь на метки времени на файлах**. См. документацию [Jupytext](https://jupytext.readthedocs.io/en/latest/index.html).
**Внимание**: С расширением Jupyter для Visual Studio Code Jupytext **не работает напрямую**. Для использования блокнотов `.ipynb` с расширением Jupyter для VS Code нужно синхронизировать текстовый файл под контролем версий и файл `.ipynb` вручную указанными выше командами. Однако заметьте, что это же расширение может исполнять блокнот в текстовом формате самостоятельно, посредством автоматизированного ведения временного блокнота; и оно даже автоматически создаёт/подхватывает локальное ядро Jupyter в виртуальном окружении.
## Работа с блокнотами Jupyter
### Jupyter
1. Запустите глобальный глобально установленный сервер Jupyter и приложение (НЕ из виртуального окружения).
* Например, запустите сервер Jupyter и веб-приложение Notebook в браузере:
```sh
jupyter notebook
```
Веб-приложение Notebook должно открыться в веб-браузере автоматически. Если этого не произошло, найдите в сообщениях сервера Jupyter строку примерно следующего содержания:
[I 08:58:24.417 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/
Откройте веб-браузер и перейдите по ссылке, выведенной в конце указанного сообщения.
См. также [документацию Jupyter](https://docs.jupyter.org/en/stable/running.html).
2. Используйте приложение для навигации по файловой системе (в частности, по каталогу `eda/`), редактирования и исполнения кода в блокнотах.
3. Если приложение Jupyter запрашивает **выбор ядра** Jupyter (**kernel**) или Вы сталкиваетесь с необъяснимыми **ошибками импортов**, выберите для текущего блокнота ядро с именем `mpei-iis-99040779` (которое Вы установили в систему раньше).
* **Notebook**: Может понадобиться выбор вручную; кнопка для выбора ядра для открытого блокнота находится в верхнем правом углу веб-страницы.
### Расширение Jupyter для Visual Studio Code
1. Запустите Visual Studio Code.
2. **Если** Вы НЕ установили ядро Jupyter, связанное с виртуальным окружением для проекта, в систему, обязательно откройте корневую директорию проекта в VS Code (*File* -> *Open Folder...*). **Иначе** это необязательный, но удобный шаг.
3. Если Вы открыли директорию проекта и VS Code запрашивает выбор автоматически обнаруженного виртуального окружения, согласитесь.
4. **При открытии любого блокнота** убедитесь, что выбрано корректное ядро Jupyter. (Кнопка для выбора ядра для открытого блокнота находится в верхнем правом углу области содержимого вкладки; если ядро не выборано, на кнопке написано *Select Kernel*.)
* **Если** Вы установили ядро Jupyter в систему, рекомендуется выбрать установленное в систему ядро с именем `mpei-iis-99040779`.
* **Если** Вы совсем **не собираетесь** использовать Jupyter для работы с проектом **и не собираетесь** записывать блокноты под контроль версий, можно выбрать локальное ядро, связанное с виртуальным окружением (по умолчанию имеет название виртуального окружения &mdash; `.venv`).
5. Используйте IDE с расширением для навигации по файловой системе (в частности, по каталогу `eda/`), редактирования и исполнения кода в блокнотах.

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

@ -0,0 +1 @@
Эта директория существует по указанию преподавателя.

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 16 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 16 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 17 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 16 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 63 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 77 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 99 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 45 KiB

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

@ -0,0 +1,8 @@
from functools import wraps
from math import log
from numpy import logspace as numpy_logspace
@wraps(numpy_logspace, assigned=('__annotations__', '__type_params__'))
def logspace(start, stop, *args_rest, **kwargs):
return numpy_logspace(log(start), log(stop), *args_rest, **kwargs)

@ -1,8 +0,0 @@
import pandas
from .common import zip_n
def describe_df(df: pandas.DataFrame) -> pandas.DataFrame:
df_items = df.items()
names, series = zip_n(*df_items, n=2)
return pandas.DataFrame(({'length': len(s), 'dtype': s.dtype} for s in series), index=names)

@ -1,5 +1,8 @@
bokeh >=3.7.2,<4
ipykernel >=6.30.1,<7
ipympl ~=0.9.6
matplotlib >=3.10.1,<4
numpy >=2.3.1,<3
pandas >=2.3.1,<3
scipy >=1.16.1,<2
seaborn ~=0.13.2

Загрузка…
Отмена
Сохранить