"С вашим датасетом нужно проделать примерно то же ...

創建於:2025年10月17日

使用 GPT-5 Thinking 回答 Chat01

問題

"С вашим датасетом нужно проделать примерно то же самое, что мы делали на лекции. Обратите внимание, что у вас может быть задача классификации, а мы решали задачу регрессии (также можно посмотреть 19-02. Adult.ipynb, где решается задача классификации) (https://github.com/NikolaiZolotykh/MachineLearningCourse). Все должно быть выложено на ваш гит.
А именно:
0. Описать задачу словами

  1. Прочитать данные
  2. Разбить данные на обучающую и тестовую выборки
  3. Визуализировать данные и вычислить основные характеристики (среднее, разброс, корреляционную матрицу и т.д.). Интерпретировать.
  4. Обработать пропущенные значения (или убедиться, что их нет)
  5. Обработать категориальные признаки
  6. Провести нормализацию (или объяснить, почему в вашем случае она не нужна)
  7. Запустить классификатор (ререссию) ближайших соседей или другой метод (аргументируйте выбор!)
  8. Вычислить ошибки на обучающей и тестовой выборках. Напечатайте матрицы рассогласования (для задачи классификации). Прокомментируйте. Выбрать оптимальное значение гиперпараметра (к-ва ближайших соседей)
  9. По желанию: запустить другие классификаторы. Сравнить результаты
  10. По желанию: как-то побороться с несбалансированностью классов (если она есть)
  11. По желанию: исключить коррелированные переменные (объяснить зачем)
  12. Сделать общие выводы" так выглядит моё домашнее задание по машинному обучению, вот так выглядят мои данные: Дерябин Максим Сергеевич ПМиИ 3822Б1ПМоп3 https://drive.google.com/drive/folders/1KyT6HDLf36rx0SOO9asAMlO6D0919tZU?usp=sharing https://github.com/BrunoBucCcellati/MachineLearningUNN2025 https://huggingface.co/datasets/forwins/Drug-Review-Dataset Drug Review Dataset 184622 8 регрессия 0% 31/3 нет 1 4 3 0 да FALSE TRUE TRUE FALSE TRUE TRUE TRUE - это заполненная в гугл таблице строка, где были заполнены следующие графы: Фамилия Имя Отчество Направление Группа "теоретические" домашки "практические" домашки Ссылка на данные (если у вас свои данные, то пишем - "свои данные") Название датасета N (число объектов) d (число признаков) K (количество классов) Наличие пропущенных значений в процентах (одно число) Несбалансированные классы (указать соотношение, например, 5/95) Большое количество выбросов (указать примерный процент) Количество числовых признаков Количество категориальных признаков Количество текстовых признаков Количество признаков других типов (написать каких) Поле для общения с преподавателем ) (пишем "да", если вы сдаете текущую работу на проверку) Наличие пропущенных значений Разнородные признаки Несбалансированные классы Большое количество выбросов Наличие текстовых признаков Соотвествие датасета требованиям Датасет одобрен преподавателем, теперь для примера: Баранов Алексей Вадимович ФИИТ 3822Б1ФИ1 https://disk.yandex.ru/d/qFt2NvO8ayYvpQ https://github.com/Torcna/ML-practice-unn https://www.kaggle.com/datasets/nabihazahid/spotify-dataset-for-churn-analysis Spotify Analysis Dataset 2025 8000 12 2 нет 5929/2071 0.17 7 4 0 0 да FALSE TRUE TRUE TRUE FALSE TRUE TRUE вот например ещё одна строка в таблице одного из студентов, обрати внимание на его датасет, и вот, что он написал в своём репозитории по этой домашней работе: EDA
    0 Описание Датасета
    В данном датасете собрана информация о 8000 пользователях популярной стриминговой платформы Spotify за 2025г. О каждом из них была собрана индивидуальная статистика, характеризующая как их личную информацию (Страна, возраст, пол и др), так и их музыкальные привычки (Среднее время прослушивания музыки на платформе в день, тип устройства, тип подписки на сервис и др).

0 Постановка задачи
Для этого набора данных предстоит решить задачу классификации пользователей по ключевом параметру - покинул ли пользователь площадку или остается активным пользователем. Необходимо по вышеуказанному набору параметров принять решение и спрогнозировать будет ли пользователь продолжать использовать платформу, или в скором времени покинет ее.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
%matplotlib inline

1 Читаем данные

url = "https://raw.githubusercontent.com/Torcna/ML-practice-unn/main/spotify_churn_dataset.csv"
df = pd.read_csv(url, encoding="utf-8")
df.head(15)

user_id gender age country subscription_type listening_time songs_played_per_day skip_rate device_type ads_listened_per_week offline_listening is_churned
0 1 Female 54 CA Free 26 23 0.20 Desktop 31 0 1
1 2 Other 33 DE Family 141 62 0.34 Web 0 1 0
2 3 Male 38 AU Premium 199 38 0.04 Mobile 0 1 1
3 4 Female 22 CA Student 36 2 0.31 Mobile 0 1 0
4 5 Other 29 US Family 250 57 0.36 Mobile 0 1 1
5 6 Female 17 AU Free 219 35 0.46 Desktop 13 0 0
6 7 Female 39 UK Premium 289 44 0.38 Desktop 0 1 0
7 8 Female 41 UK Free 210 68 0.11 Mobile 5 0 0
8 9 Other 55 DE Student 50 66 0.29 Web 0 1 0
9 10 Male 44 DE Free 278 94 0.38 Mobile 44 0 0
10 11 Other 33 DE Free 86 44 0.11 Web 37 0 0
11 12 Other 24 CA Free 113 24 0.56 Desktop 39 0 1
12 13 Female 37 IN Family 24 86 0.11 Desktop 0 1 0
13 14 Female 53 US Premium 45 47 0.60 Web 0 1 0
14 15 Other 37 IN Family 114 13 0.44 Web 0 1 1
Проверим из каких столбцов состоит наш датасет

print('Shape of dataset:', df.shape)
print('Columns in dataset:', df.columns.tolist())
df.info()

Shape of dataset: (8000, 12)
Columns in dataset: ['user_id', 'gender', 'age', 'country', 'subscription_type', 'listening_time', 'songs_played_per_day', 'skip_rate', 'device_type', 'ads_listened_per_week', 'offline_listening', 'is_churned']
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8000 entries, 0 to 7999
Data columns (total 12 columns):

Column Non-Null Count Dtype


0 user_id 8000 non-null int64
1 gender 8000 non-null object
2 age 8000 non-null int64
3 country 8000 non-null object
4 subscription_type 8000 non-null object
5 listening_time 8000 non-null int64
6 songs_played_per_day 8000 non-null int64
7 skip_rate 8000 non-null float64
8 device_type 8000 non-null object
9 ads_listened_per_week 8000 non-null int64
10 offline_listening 8000 non-null int64
11 is_churned 8000 non-null int64
dtypes: float64(1), int64(7), object(4)
memory usage: 750.1+ KB
Мы видим, что столбцы (признаки) имеют имена

user_id - уникальный id пользователя (его можно сразу дропнуть)
gender - пол - категориальный признак (мужчина, женщина или другой (мы такое не приветствуем))
age - возраст
country - страна - категориальный признак
subscription_type - тип подписки - категориальный признак (студенческая, семейная, премиум или бесплатная)
listening_time - время прослушивания в день в минутах
songs_played_per_day - количество песен прослушанных за день
skip_rate - частота скипа треков
device_type - тип клиента с которого прослушивались треки (веб-клиент, мобильное устройство, приложение для PC или мобильное приложение)
ads_listened_per_week - число рекламных интеграций, показанных пользователю за неделлю
offline_listening - Изпользовал ли пользователь возможность оффлайн прослушивания песен
is_churned - ключевой параметр - является ли пользователь активным
2 Разделение на обучающую и тестовую выборку

y = df['is_churned'].astype(int)
X = df.drop(columns=['is_churned', 'user_id'])

num_cols = ['age','listening_time','songs_played_per_day','skip_rate','ads_listened_per_week']
cat_cols = ['gender','country','subscription_type','device_type']
bin_cols = ['offline_listening']

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)

3 Определим основные метрики нашего датасета

X_train.describe()

age listening_time songs_played_per_day skip_rate ads_listened_per_week offline_listening
count 6400.000000 6400.000000 6400.000000 6400.000000 6400.000000 6400.000000
mean 37.702344 153.704062 49.962656 0.300339 6.975000 0.746563
std 12.700611 83.948879 28.399716 0.173761 13.657625 0.435013
min 16.000000 10.000000 1.000000 0.000000 0.000000 0.000000
25% 27.000000 81.000000 25.000000 0.150000 0.000000 0.000000
50% 38.000000 153.000000 50.000000 0.300000 0.000000 1.000000
75% 49.000000 226.000000 75.000000 0.450000 5.000000 1.000000
max 59.000000 299.000000 99.000000 0.600000 49.000000 1.000000
Посмотрим на категориальные признаки

for col in cat_cols:
X_train[col] = X_train[col].astype('category')
X_train.describe(include=['category'])

gender country subscription_type device_type
count 6400 6400 6400 6400
unique 3 8 4 3
top Male IN Premium Desktop
freq 2160 823 1680 2207
Визуализируем основные характеристики

import matplotlib.pyplot as plt

import seaborn as sns

features_to_hist = ['age', 'listening_time', 'skip_rate']
for feature in features_to_hist:
plt.figure(figsize=(8, 4))
sns.histplot(X_train[feature], kde=True, bins=40)
plt.title(f'Histogram of {feature}')
plt.show()

Для категориальных признаков построим отдельные диаграммы

for col in cat_cols:
plt.figure(figsize=(10, 4))
sns.countplot(data=X_train, x=col, order=X_train[col].value_counts().index)
plt.title(f'Count Plot of {col}')
plt.xticks(rotation=45)
plt.show()

надо добить скаттер матрицу и пару дианграмм и написать вывод все

Построим несколько диаграмм рассеивания

X_train.plot(kind='scatter', x='age', y='listening_time')
pass

X_train.plot(kind='scatter', x='age', y='skip_rate')
pass

X_train['age'].quantile([0.005,.01,.05,.1,.5,.9,.95,.99,.995])

age
0.005 16.0
0.010 16.0
0.050 18.0
0.100 20.0
0.500 38.0
0.900 55.0
0.950 57.0
0.990 59.0
0.995 59.0

dtype: float64

X_train_numeric_features = X_train[num_cols]
pd.plotting.scatter_matrix(X_train_numeric_features, figsize = (10, 10))
pass

Построим для некоторых параметров диаграммы "ящик с усами" для того, чтобы удостовериться, что данные сбалансированы и нормализованы, значимых выбросов, которые могут повлиять на работу классификатора, нет.

X_train['age'].plot(kind='box')
pass

X_train['listening_time'].plot(kind='box')
pass

X_train['songs_played_per_day'].plot(kind='box')
pass

sns.countplot(y='is_churned', data=df)
churn_rate = df["is_churned"].value_counts(normalize=True) * 100
print("Churn Rate (%):")
print(churn_rate)

Churn Rate (%):
is_churned
0 74.1125
1 25.8875
Name: proportion, dtype: float64

sns.pairplot(X_train.sample(4000), hue='subscription_type', diag_kind='hist')
pass

sns.pairplot(X_train.sample(4000), hue='device_type', diag_kind='hist')
pass

Для числовых параметров построим матрицу корреляции, чтобы установить связь между ними

corr_ff = X_train[num_cols].corr(method='spearman')
plt.figure(figsize=(7, 6))
sns.heatmap(corr_ff, annot=True, fmt='.2f', cmap='coolwarm', vmin=-1, vmax=1)
plt.title('Feature–Feature Correlation (continuous only, Spearman)')
plt.show()

Дополнительно проверим связь каждой из них с нашим таргетом напрямую - is_churned

from scipy.stats import spearmanr

rows = []
for col in num_cols:
r, p = spearmanr(X_train[col], y_train)
rows.append({'feature': col, 'spearman_to_target': r, 'p_value': p})

corr_to_target = pd.DataFrame(rows).sort_values('spearman_to_target', key=np.abs, ascending=False)
corr_to_target

feature spearman_to_target p_value
2 songs_played_per_day 0.023801 0.056912
3 skip_rate 0.015004 0.230071
1 listening_time -0.013461 0.281620
4 ads_listened_per_week -0.007909 0.526984
0 age 0.007419 0.552917
Вывод и интерпретация
На основе построенных диаграмм и собранных метрик можно сделать вывод, что представленный датасет является довольно сбалансированным. по матрице корреляции можно увидеть, что нет конкретного параметра, который действительно влияет на поле is_churned, в таких задачах о покупательском поведении довольно часто на конечный результат влияет именно совокупность характеристик, а не какой-то конкретный параметр.

Выбросы и аномальные значения практически не встречаются, данные о пользователях различных возрастов, полов и стран подобраны равномерно и теперь можно приступить к обработке категориальных и пропущенных признаков.

4 Пропущенные данные
Можем убедиться, что пропущенных данных в нашем датасете нет

missing_values = df.isnull().sum()
print('Missing values in each column:')
print(missing_values)

Missing values in each column:
user_id 0
gender 0
age 0
country 0
subscription_type 0
listening_time 0
songs_played_per_day 0
skip_rate 0
device_type 0
ads_listened_per_week 0
offline_listening 0
is_churned 0
dtype: int64
5 Категориальные значения
Векторизируем категориальные значения для столбцов gender, subscription_type, device_type, country

X_train_ = pd.get_dummies(X_train, columns=cat_cols, drop_first=True, dtype=int)
X_test_ = pd.get_dummies(X_test, columns=cat_cols, drop_first=True, dtype=int)

X_train_.head(5)

age listening_time songs_played_per_day skip_rate ads_listened_per_week offline_listening gender_Male gender_Other country_CA country_DE country_FR country_IN country_PK country_UK country_US subscription_type_Free subscription_type_Premium subscription_type_Student device_type_Mobile device_type_Web
4661 48 138 38 0.59 0 1 0 1 0 0 0 0 0 0 1 0 0 1 0 0
5195 44 78 36 0.16 0 1 1 0 0 0 0 0 0 0 1 0 1 0 0 1
7123 29 90 75 0.52 16 0 1 0 0 0 0 0 1 0 0 1 0 0 0 1
3764 56 115 41 0.20 0 1 1 0 0 0 0 0 1 0 0 0 0 1 1 0
6824 32 105 17 0.17 0 1 0 1 0 0 0 0 0 1 0 0 0 0 0 1
6 Нормализация
Проведем нормализацию числовых параметров

scaler = StandardScaler()
scaler.fit(X_train_[num_cols])

X_train_[num_cols] = scaler.transform(X_train_[num_cols])
X_test_[num_cols] = scaler.transform(X_test_[num_cols])

X_train_.tail(5)

age listening_time songs_played_per_day skip_rate ads_listened_per_week offline_listening gender_Male gender_Other country_CA country_DE country_FR country_IN country_PK country_UK country_US subscription_type_Free subscription_type_Premium subscription_type_Student device_type_Mobile device_type_Web
5178 1.125834 -0.484905 -0.526901 -0.404835 -0.510744 1 0 1 0 0 1 0 0 0 0 0 0 1 0 0
384 1.204576 0.396653 -0.808616 -1.095492 -0.510744 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0
1468 -1.078957 0.742128 -0.350829 0.573596 -0.510744 1 0 1 0 0 0 1 0 0 0 0 1 0 0 1
4335 -0.763987 -1.080552 1.128175 -0.750163 -0.510744 1 0 1 0 0 0 0 0 1 0 0 0 1 0 0
4640 -0.763987 -1.521331 -0.491686 0.976480 2.052128 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0
7 Запуск классификатора

knn = KNeighborsClassifier(n_neighbors=5)

knn.fit(X_train_, y_train)

y_pred_knn = knn.predict(X_test_)

print("Accuracy:", accuracy_score(y_test, y_pred_knn))
print("\nConfusion Matrix:\n", confusion_matrix(y_test, y_pred_knn))
print("\nClassification Report:\n", classification_report(y_test, y_pred_knn))

Accuracy: 0.68625

Confusion Matrix:
[[1040 146]
[ 356 58]]

Classification Report:
precision recall f1-score support

text
0 0.74 0.88 0.81 1186 1 0.28 0.14 0.19 414 accuracy 0.69 1600

macro avg 0.51 0.51 0.50 1600
weighted avg 0.63 0.69 0.65 1600

8 Анализ и подбор гиперпараметров

results = []

best = {"k": None, "acc": -1, "y_pred": None}

for k in range(1, 16):
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train_, y_train)
y_pred = knn.predict(X_test_)
acc = accuracy_score(y_test, y_pred)
results.append((k, acc))
if acc > best["acc"]:
best = {"k": k, "acc": acc, "y_pred": y_pred}

print("Per-k accuracy:", results)
print(f"\nBest k = {best['k']} | Accuracy = {best['acc']:.4f}")

print("\nConfusion Matrix (best k):\n", confusion_matrix(y_test, best["y_pred"]))
print("\nClassification Report (best k):\n", classification_report(y_test, best["y_pred"], digits=3))

Per-k accuracy: [(1, 0.629375), (2, 0.715), (3, 0.668125), (4, 0.711875), (5, 0.68625), (6, 0.728125), (7, 0.70875), (8, 0.728125), (9, 0.71875), (10, 0.73), (11, 0.725), (12, 0.73125), (13, 0.7225), (14, 0.73125), (15, 0.72875)]

Best k = 12 | Accuracy = 0.7312

Confusion Matrix (best k):
[[1162 24]
[ 406 8]]

Classification Report (best k):
precision recall f1-score support

text
0 0.741 0.980 0.844 1186 1 0.250 0.019 0.036 414 accuracy 0.731 1600

macro avg 0.496 0.500 0.440 1600
weighted avg 0.614 0.731 0.635 1600

Использовав метод k-ближайших соседей мы получили, что модель с намного большей точностью может сказать, что пользователь остался на платформе, в то вермя, как ушедших пользователей мы определяем с очень большой ошибкой. А также можно сделать вывод о том, что значение гиперпараметра K=12 является оптимальным

12 Общие выводы
При использовании классификатора k=12 Ближайших Соседей явно обнаружилась большая разница в точности предсказания для разных классов. Причиной такого разброса может быть несбалансированность target, по результату видно, что точности очень похожи на процентное соотношение is_churned по двум классам. Также стоит отметить, что метод k-Ближайших Соседей может плохо подходить для залач, где между features нет корреляции (или даже она отрицательна, как в нашем случае). В будущем предполагается поработать с другими классификаторами и попробовать исправить несбалансированность данных. - это то, что у него находится в репозитории в папке с названием: ML-practice-unn
/Task2BaranovA.ipynb, потом далее у него есть таблица user_id gender age country subscription_type listening_time songs_played_per_day skip_rate device_type ads_listened_per_week offline_listening is_churned
1 Female 54 CA Free 26 23 0.2 Desktop 31 0 1
2 Other 33 DE Family 141 62 0.34 Web 0 1 0
3 Male 38 AU Premium 199 38 0.04 Mobile 0 1 1
4 Female 22 CA Student 36 2 0.31 Mobile 0 1 0
5 Other 29 US Family 250 57 0.36 Mobile 0 1 1
6 Female 17 AU Free 219 35 0.46 Desktop 13 0 0 - и так далее до 8000 строк в таблице в репозитории в папке с названием - ML-practice-unn
/spotify_churn_dataset.csv, теперь для моих данных напиши какие у меня должны быть файлы - делая аналогичный нейминг но для меня, как мне получить аналогичные таблички и текст для своего случая, если мой репозиторий называется: MachineLearningUNN2025, меня интересует в первую очередь как мне получить аналогичные таблички файлы и т д - чтобы я мог быстро вставить это в свой репозиторий, будь внимателен и ничего не напутай - но кстати в другой работе нет .csv файла а только .ipynb файл: Задача классификации грибов на съедобные и несъедобные
0. Описание задачи
Этот набор данных описывает различные физические характеристики грибов, разделённых на съедобные (e - edible) и ядовитые (p - poisonous). Датасет насчитывает 8124 образца, соответствующих 23 видам грибов семейства Agaricus и Lepiota, а также 22 признака, включающие цвет, запах, среду обитания гриба и проч. Сведения о грибах взяты из справочника Общества Одюбона по североамериканским грибам (1981 г., G. H. Lincoff (Pres.), New York: Alfred A. Knopf).

Целью задачи является определение принадлежности гриба к одному из классов по внешним признакам. В ходе решения задачи будут использованы различные методы машинного обучения, будут сделаны выводы о целесообразности их использования в зависимости от аккуратности предсказания.

  1. Чтение данных
    Установим библиотеки, которые пригодятся далее для работы с данными.

! pip install numpy matplotlib pandas plotly seaborn scikit-learn
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0)
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1)
Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2)
Requirement already satisfied: scikit-learn in /usr/local/lib/python3.12/dist-packages (1.6.1)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.60.1)
Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.4.9)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (25.0)
Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.2.5)
Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (8.5.0)
Requirement already satisfied: scipy>=1.6.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.16.2)
Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.5.2)
Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (3.6.0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.7->matplotlib) (1.17.0)

импорт библиотек

import time
import pandas
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_openml
from sklearn.metrics import ConfusionMatrixDisplay
%matplotlib inline

импортируем датасет с официального сайта https://www.openml.org

mushroom_dataset = fetch_openml(name='mushroom', version=1, parser='auto')

Посмотрим на размеры таблицы - количество строк (каждая строка соответствует одному грибу) и количество столбцов (каждый столбец соответствует конкретному признаку)

mushroom_dataset.data.shape
(8124, 22)

Выведем содержимое датасета

mushroom_dataset = mushroom_dataset.frame
mushroom_dataset
cap-shape cap-surface cap-color bruises%3F odor gill-attachment gill-spacing gill-size gill-color stalk-shape ... stalk-color-above-ring stalk-color-below-ring veil-type veil-color ring-number ring-type spore-print-color population habitat class
0 x s n t p f c n k e ... w w p w o p k s u p
1 x s y t a f c b k e ... w w p w o p n n g e
2 b s w t l f c b n e ... w w p w o p n n m e
3 x y w t p f c n n e ... w w p w o p k s u p
4 x s g f n f w b k t ... w w p w o e n a g e
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
8119 k s n f n a c b y e ... o o p o o p b c l e
8120 x s n f n a c b y e ... o o p n o p b v l e
8121 f s n f n a c b n e ... o o p o o p b c l e
8122 k y n f y f c n b t ... w w p w o e w v l p
8123 x s n f n a c b y e ... o o p o o p o c l e
8124 rows × 23 columns

Выведем первые 15 строк датасета

mushroom_dataset.head(15)
cap-shape cap-surface cap-color bruises%3F odor gill-attachment gill-spacing gill-size gill-color stalk-shape ... stalk-color-above-ring stalk-color-below-ring veil-type veil-color ring-number ring-type spore-print-color population habitat class
0 x s n t p f c n k e ... w w p w o p k s u p
1 x s y t a f c b k e ... w w p w o p n n g e
2 b s w t l f c b n e ... w w p w o p n n m e
3 x y w t p f c n n e ... w w p w o p k s u p
4 x s g f n f w b k t ... w w p w o e n a g e
5 x y y t a f c b n e ... w w p w o p k n g e
6 b s w t a f c b g e ... w w p w o p k n m e
7 b y w t l f c b n e ... w w p w o p n s m e
8 x y w t p f c n p e ... w w p w o p k v g p
9 b s y t a f c b g e ... w w p w o p k s m e
10 x y y t l f c b g e ... w w p w o p n n g e
11 x y y t a f c b n e ... w w p w o p k s m e
12 b s y t a f c b w e ... w w p w o p n s g e
13 x y w t p f c n k e ... w w p w o p n v u p
14 x f n f n f w b n t ... w w p w o e k a g e
15 rows × 23 columns

Выведем последние 15 строк датасета

mushroom_dataset.tail(15)
cap-shape cap-surface cap-color bruises%3F odor gill-attachment gill-spacing gill-size gill-color stalk-shape ... stalk-color-above-ring stalk-color-below-ring veil-type veil-color ring-number ring-type spore-print-color population habitat class
8109 b s w f n f w b w e ... w w p w t p w n g e
8110 x s n f n a c b o e ... o o p o o p n v l e
8111 k s w f n f w b p e ... w w p w t p w n g e
8112 k s n f n a c b o e ... o o p n o p b v l e
8113 k y e f y f c n b t ... p p p w o e w v d p
8114 f y c f m a c b y e ... c c p w n n w c d p
8115 x s n f n a c b y e ... o o p o o p o v l e
8116 k y n f s f c n b t ... p w p w o e w v l p
8117 k s e f y f c n b t ... p w p w o e w v d p
8118 k y n f f f c n b t ... p w p w o e w v d p
8119 k s n f n a c b y e ... o o p o o p b c l e
8120 x s n f n a c b y e ... o o p n o p b v l e
8121 f s n f n a c b n e ... o o p o o p b c l e
8122 k y n f y f c n b t ... w w p w o e w v l p
8123 x s n f n a c b y e ... o o p o o p o c l e
15 rows × 23 columns

Выведем имена столбцов (признаков)

columns = mushroom_dataset.columns.to_list()
print(len(columns))
for column in columns:
print(column)
23
cap-shape
cap-surface
cap-color
bruises%3F
odor
gill-attachment
gill-spacing
gill-size
gill-color
stalk-shape
stalk-root
stalk-surface-above-ring
stalk-surface-below-ring
stalk-color-above-ring
stalk-color-below-ring
veil-type
veil-color
ring-number
ring-type
spore-print-color
population
habitat
class
Описание признаков и их возможных значений

cap-shape (форма шляпки): bell=b (колоколчатая), conical=c (коническая), convex=x (выпуклая), flat=f (плоская), knobbed=k (бугристая), sunken=s (впалая)

cap-surface (поверхность шляпки): fibrous=f (волокнистая), grooves=g (с выемками), scaly=y (чешуйчатая), smooth=s (гладкая)

cap-color (цвет шляпки): brown=n (коричневый), buff=b (желтовато-коричневый), cinnamon=c (светло-коричневый), gray=g (серый), green=r (зеленый), pink=p (розовый), purple=u (фиолетовый), red=e (красный), white=w (белый), yellow=y (желтый)

bruises (синяки): bruises=t (есть), no=f (нет)

odor (запах): almond=a (миндальный), anise=l (анисовый), creosote=c (креозотовый), fishy=y (рыбный), foul=f (неприятный, противный), musty=m (затхлый), none=n (никакой), pungent=p (резкий, едкий), spicy=s (острый, пряный)

gill-attachment (форма пластин): attached=a (прикрепленная), descending=d (нисходящая), free=f (свободная), notched=n (зубчатая)

gill-spacing (расположение пластин): close=c (близкое), crowded=w (плотное, тесное), distant=d (далекое)

gill-size (размер пластин): broad=b (широкий), narrow=n (узкий)

gill-color (цвет пластин): black=k (черный), brown=n (коричневый), buff=b (желтовато-коричневый), chocolate=h (шоколадный), gray=g (серый), green=r (зеленый), orange=o (оранжевый), pink=p (розовый), purple=u (фиолетовый), red=e (красный), white=w (белый), yellow=y (желтый)

stalk-shape (форма ножки): enlarging=e (расширяющаяся), tapering=t (сужающаяся)

stalk-root (корень ножки): bulbous=b (луковичный), club=c (клубчатый), cup=u (чашевидный), equal=e (равный), rhizomorphs=z (ризоморфный), rooted=r (укореняющийся), missing=? (отсутствует)

stalk-surface-above-ring (поверхность ножки выше кольца): fibrous=f (волокнистая), scaly=y (чешуйчатая), silky=k (шелковистая), smooth=s (гладкая)

stalk-surface-below-ring (поверхность ножки ниже кольца): fibrous=f (волокнистая), scaly=y (чешуйчатая), silky=k (шелковистая), smooth=s (гладкая)

stalk-color-above-ring (цвет ножки выше кольца): brown=n (коричневый), buff=b (желтовато-коричневый), cinnamon=c (светло-коричневый), gray=g (серый), orange=o (оранжевый), pink=p (розовый), red=e (красный), white=w (белый), yellow=y (желтый)

stalk-color-below-ring (цвет ножки ниже кольца): brown=n (коричневый), buff=b (желтовато-коричневый), cinnamon=c (светло-коричневый), gray=g (серый), orange=o (оранжевый), pink=p (розовый), red=e (красный), white=w (белый), yellow=y (желтый)

veil-type (тип вуали): partial=p (частичная), universal=u (полная)

veil-color (цвет вуали): brown=n (коричневая), orange=o (оранжевая), white=w (белая), yellow=y (желтая)

ring-number (количество колец): none=n (ни одного), one=o (одно), two=t (два)

ring-type (тип кольца): cobwebby=c (паутинистое), evanescent=e (еле заметное), flaring=f (расширяющееся книзу), large=l (большое), none=n (никакое), pendant=p (кулонообразное), sheathing=s (обволакивающее), zone=z (зональное)

spore-print-color (цвет спор): black=k (черный), brown=n (коричневый), buff=b (желтовато-коричневый), chocolate=h (шоколадный), green=r (зеленый), orange=o (оранжевый), purple=u (фиолетовый), white=w (белый), yellow=y (желтый)

population (тип популяции): abundant=a (обильная), clustered=c (скоплениями), numerous=n (многочисленная), scattered=s (рассеянная), several=v (несколько), solitary=y (одиночная)

habitat (среда обитания): grasses=g (трава), leaves=l (листья), meadows=m (луга), paths=p (дороги, тропинки), urban=u (город), waste=w (отходы), woods=d (леса)

class (класс): edible=e (съедобные), poisonous=p (ядовитые)

Выведем информацию по датасету

mushroom_dataset.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8124 entries, 0 to 8123
Data columns (total 23 columns):

Column Non-Null Count Dtype


0 cap-shape 8124 non-null category
1 cap-surface 8124 non-null category
2 cap-color 8124 non-null category
3 bruises%3F 8124 non-null category
4 odor 8124 non-null category
5 gill-attachment 8124 non-null category
6 gill-spacing 8124 non-null category
7 gill-size 8124 non-null category
8 gill-color 8124 non-null category
9 stalk-shape 8124 non-null category
10 stalk-root 5644 non-null category
11 stalk-surface-above-ring 8124 non-null category
12 stalk-surface-below-ring 8124 non-null category
13 stalk-color-above-ring 8124 non-null category
14 stalk-color-below-ring 8124 non-null category
15 veil-type 8124 non-null category
16 veil-color 8124 non-null category
17 ring-number 8124 non-null category
18 ring-type 8124 non-null category
19 spore-print-color 8124 non-null category
20 population 8124 non-null category
21 habitat 8124 non-null category
22 class 8124 non-null category
dtypes: category(23)
memory usage: 187.8 KB
Далее выведем описательную статистику по датасету: количество значений у каждого признака, количество уникальных значений, наиболее частое значение и количество его появлений в таблице.

mushroom_dataset.describe()
cap-shape cap-surface cap-color bruises%3F odor gill-attachment gill-spacing gill-size gill-color stalk-shape ... stalk-color-above-ring stalk-color-below-ring veil-type veil-color ring-number ring-type spore-print-color population habitat class
count 8124 8124 8124 8124 8124 8124 8124 8124 8124 8124 ... 8124 8124 8124 8124 8124 8124 8124 8124 8124 8124
unique 6 4 10 2 9 2 2 2 12 2 ... 9 9 1 4 3 5 9 6 7 2
top x y n f n f c b b t ... w w p w o p w v d e
freq 3656 3244 2284 4748 3528 7914 6812 5612 1728 4608 ... 4464 4384 8124 7924 7488 3968 2388 4040 3148 4208
4 rows × 23 columns

Проанализировав полученную таблицу, можно заметить, что в столбце "veil-type" присутствует только одно уникальное значение, следовательно, этот столбец не несёт полезной нагрузки для нашей задачи и в дальнейшем может быть удалён.

  1. Визуализация данных
    Начнём с визуализации данных по классовому признаку и построим гистограмму грибов по их съедобности.

plt.figure(figsize=(12, 7))
sns.countplot(data=mushroom_dataset, x='class', hue='class', palette='bright')
<Axes: xlabel='class', ylabel='count'>

Синим цветом обозначены съедобные грибы, оранжевым - несъедобные. Съедобных грибов в датасете немного больше, чем несъедобных

Теперь рассмотрим как съедобные и несъедобные грибы распределены по значениям некоторых фиксированных признаков, например, запах, среда обитания, популяция.

Построим гистограмму, показывающую количество съедобных и несъедобных грибов по их запаху

plt.figure(figsize=(12, 7))
sns.countplot(data=mushroom_dataset, x='odor', hue='class', palette='bright')
<Axes: xlabel='odor', ylabel='count'>

Как мы можем заметить, грибы с креозотовым, очень неприятным, затхлым, резким, пряным или рыбным запахов всегда относятся к несъедобным, в то время как грибы с миндальным или анисовым запахом всегда относятся к съедобным. Что касается грибов без запаха, то большая их часть является съедобной, но встречаются также и несъедобные грибы, которые не имеют запаха

Построим гистограмму, показывающую количество съедобных и несъедобных грибов по признаку популяции

plt.figure(figsize=(12, 7))
sns.countplot(data=mushroom_dataset, x='population', hue='class', palette='bright')
<Axes: xlabel='population', ylabel='count'>

Из гистограммы видно, что почти все растущие обильно (abundant=a) и многочисленно (numerous=n) грибы относятся к съедобным, также съедобных грибов больше среди сгруппированных (clustered=c), разбросанных (scattered=s) и одиноко растущих (solitary=y). Значительно больше несъедобных грибов только среди популяции со значением "несколько" (several=v).

Теперь посмотрим, как грибы распределены по признаку "среда обитания"

plt.figure(figsize=(12, 7))
sns.countplot(data=mushroom_dataset, x='habitat',
hue="habitat", palette='bright')
<Axes: xlabel='habitat', ylabel='count'>

Большая часть грибов из набора данных обитает в лесах или в траве. Чаще всего грибы можно встретить в лесу, а реже всего - в отходах.

Для признаков популяция и среда обитания построим таблицу сопряжённости и представим её в виде тепловой карты. Таблица сопряженности показывает частоту или долю комбинаций значений категориальных признаков в данных. Это помогает увидеть, какие категории чаще или реже встречаются вместе. На тепловой карте значения визуализируются цветами от светлых - к тёмным по увеличению частоты количества искомых комбинаций.

cross_tab = pandas.crosstab(mushroom_dataset['habitat'], mushroom_dataset['population'])
sns.heatmap(cross_tab, annot=True, fmt='d', cmap='YlGnBu')
plt.title('Частота сочетаний среды обитания и популяции')
plt.show()

Из тепловой карты таблицы сопряжённости видно, что большинство грибов, обитающих в лесу и на дорогах/тропинках принадлежат к категориям several и solitary. Наиболее равномерно по видам популяции распределены грибы, обитающие в траве, в то время как грибы, обитающие в мусоре, растут только группами.

  1. Обработка пропущенных значений

Проверим каждый столбец на наличие пропущенных значений

mushroom_dataset.isnull().sum()
0
cap-shape 0
cap-surface 0
cap-color 0
bruises%3F 0
odor 0
gill-attachment 0
gill-spacing 0
gill-size 0
gill-color 0
stalk-shape 0
stalk-root 2480
stalk-surface-above-ring 0
stalk-surface-below-ring 0
stalk-color-above-ring 0
stalk-color-below-ring 0
veil-type 0
veil-color 0
ring-number 0
ring-type 0
spore-print-color 0
population 0
habitat 0
class 0

dtype: int64
В столбце "stalk-root" обнаружилось 2480 пропущенных значений. Так как остальные признаки не содержат пропусков, а попытки заполнить пропущенные значения могут привести к искажению данных, выгоднее всего - удалить данный столбец. Здесь же удалим столбец "veil-type" из-за описанных ранее причин (единственное уникальное значение).

mushroom_dataset = mushroom_dataset.drop(columns=['stalk-root'], axis=1)
mushroom_dataset = mushroom_dataset.drop(columns=['veil-type'], axis=1)

Повторим проверку на наличие пропущенных значений, заодно убедимся, что столбцов стало на 2 меньше.

mushroom_dataset.isnull().sum()
0
cap-shape 0
cap-surface 0
cap-color 0
bruises%3F 0
odor 0
gill-attachment 0
gill-spacing 0
gill-size 0
gill-color 0
stalk-shape 0
stalk-surface-above-ring 0
stalk-surface-below-ring 0
stalk-color-above-ring 0
stalk-color-below-ring 0
veil-color 0
ring-number 0
ring-type 0
spore-print-color 0
population 0
habitat 0
class 0

dtype: int64
Пропущенных значений больше нет.

  1. Обработка категориальных признаков
    Все признаки в датасете категориальные, поэтому для подготовки данных к обучению была выполнена кодировка категориальных признаков с помощью LabelEncoder из scikit-learn. Каждый уникальный категориальный признак был преобразован в числовое значение.

Такой подход позволяет моделям машинного обучения работать с данными в числовом формате. В данной задаче это допустимо, поскольку используются модели, нечувствительные к порядку категорий.

from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
mushroom_dataset_new = mushroom_dataset.copy()
for col in mushroom_dataset_new.columns:
mushroom_dataset_new[col] = label_encoder.fit_transform(
mushroom_dataset_new[col])
mushroom_dataset_new.head()
cap-shape cap-surface cap-color bruises%3F odor gill-attachment gill-spacing gill-size gill-color stalk-shape ... stalk-surface-below-ring stalk-color-above-ring stalk-color-below-ring veil-color ring-number ring-type spore-print-color population habitat class
0 5 2 4 1 6 1 0 1 4 0 ... 2 7 7 2 1 4 2 3 5 1
1 5 2 9 1 0 1 0 0 4 0 ... 2 7 7 2 1 4 3 2 1 0
2 0 2 8 1 3 1 0 0 5 0 ... 2 7 7 2 1 4 3 2 3 0
3 5 3 8 1 6 1 0 1 5 0 ... 2 7 7 2 1 4 2 3 5 1
4 5 2 3 0 5 1 1 0 4 1 ... 2 7 7 2 1 0 3 0 1 0
5 rows × 21 columns

Теперь, поскольку все признаки стали числовыми, мы можем построить корреляционную матрицу

corr_matrix = mushroom_dataset_new.corr()
mask = np.zeros_like(corr_matrix)
mask[np.triu_indices_from(mask)] = True
with sns.axes_style("white"):
f, ax = plt.subplots(figsize=(30, 20))
sns.heatmap(corr_matrix, vmin=-1, vmax=1, cmap="YlGnBu",
annot=True, mask=mask, square=True)

Мы можем заметить, что в целом корреляция между признаками довольно слабая, однако есть признаки, между которыми относительно сильная корреляция. Самая сильная корреляция наблюдается между признаками "veil-color" и "gill-attachment".

С целевой переменной class наиболее явно коррелирует признак "gill-size", а наибольшее "противоположное" влияние (отрицательная корреляция) на class оказывает признак "gill-color".

  1. Нормализация
    Нормализация — это процесс приведения числовых признаков к единому масштабу, чтобы значения разных признаков можно было корректно сравнивать и чтобы модели, чувствительные к масштабу данных, работали стабильнее.

В этом наборе данных все признаки категориальные, они описывают качественные характеристики грибов — форму шляпки, запах, цвет, место обитания и т.д., такие признаки не обладают метрическим смыслом, поэтому нормализация не требуется.

  1. Разбиение данных на обучающую и тестовую выборки
    Для обучения и тестирования модели данные были разделены в соотношении 80% на 20%. Это обеспечивает достаточный объём данных для обучения (примерно 6500 объектов) и репрезентативную тестовую выборку (примерно 1600 объектов). Такое разделение позволяет корректно оценить качество модели без потери статистической значимости.

from sklearn.model_selection import train_test_split
X = mushroom_dataset_new.drop('class', axis=1)
y = mushroom_dataset_new['class']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42)
7-8. Модель k ближайших соседей. Выбор оптимального параметра. Матрицы рассоглсования.
Для модели k ближайших соседей (kNN) была проведена проверка зависимости точности от количества соседей k в диапазоне 1–30.

from sklearn.neighbors import KNeighborsClassifier

accuracy_list_train = []
accuracy_list_test = []
for i in range(1, 31):
KNN = KNeighborsClassifier(n_neighbors=i)
KNN.fit(X_train, y_train)

text
accuracy_train = round(KNN.score(X_train, y_train)*100, 4) accuracy_list_train.append(accuracy_train) accuracy_test = round(KNN.score(X_test, y_test)*100, 4) accuracy_list_test.append(accuracy_test)

Построим график Accuracy на обучающей выборке

k = np.arange(1, 31)
plt.figure(figsize=(12, 6))
plt.plot(k, accuracy_list_train, 'red')
plt.xlabel('k')
plt.xticks(np.arange(min(k), max(k)+1, 1.0))
plt.grid(which='major')
plt.ylabel('Accuracy on train, %')
Text(0, 0.5, 'Accuracy on train, %')

Как мы можем заметить, лучший результат достигается при k = 1

Теперь построим график Accuracy на тестовой выборке

k = np.arange(1, 31)
plt.figure(figsize=(12, 6))
plt.plot(k, accuracy_list_test, 'blue')
plt.xlabel('k')
plt.xticks(np.arange(min(k), max(k)+1, 1.0))
plt.grid(which='major')
plt.ylabel('Accuracy on test, %')
Text(0, 0.5, 'Accuracy on test, %')

При анализе зависимости точности модели k ближайших соседей от количества соседей k было выявлено, что наилучшие результаты достигаются при малых значениях k (1, 2). Для этих значений модель демонстрирует высокую точность как на обучающей, так и на тестовой выборке. При увеличении числа соседей точность постепенно снижается, что указывает на ослабление чувствительности модели и тенденцию к недообучению.

Применим алгоритм ближайщих соседей при k = 2 и посмотрим, чему равно значение ошибки

KNN_model = KNeighborsClassifier(n_neighbors=2).fit(X_train, y_train)

start_time = time.time()
error_train = 1 - KNN_model.score(X_train, y_train)
error_test = 1 - KNN_model.score(X_test, y_test)
end_time = time.time()
runtime = end_time - start_time

print('Error on train = ', error_train)
print('Error on test = ', error_test)
print('Runtime = ', runtime)
Error on train = 0.00015386982612708433
Error on test = 0.0
Runtime = 1.016373872756958
ConfusionMatrixDisplay.from_estimator(KNN_model, X_test, y_test)
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay at 0x7da3f0767890>

ConfusionMatrixDisplay.from_estimator(KNN_model, X_train, y_train)
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay at 0x7da3f03aee70>

На тестовой выборке модель сработала безошибочно, допустив одну ошибку на обучающей выборке. Из этого можно сделать вывод, что k=2 является оптимальным параметром для модели k ближайших соседей. Это связано с высокой структурированностью и чистотой исходных данных: классы грибов хорошо разделяются по признакам. поэтому учитывая что когда я пытался скачать .cvs файл но не смог так как он слишком большой получился - больше 100мб возможно мне стоит сделать также, дай мне решение как это сделать - хотелось бы просто чтобы ты дал мне код который я смогу вставить в гугл коллаб и чтобы я получил сразу свой один такой файл который смогу добавить в репозиторий

思考

Crafting response for Aaron Ross

I need to respond to Aaron, who sent detailed instructions and examples in Russian. They’ve shared the Drug Review Dataset and requested code in Google Colab. They prefer not to download a large CSV and suggested generating smaller or alternative file formats. I'll proceed with generating the most efficient code for their request, no web browsing needed.

Considering classifier/regressor code

To parse the dataset from Hugging Face, I’ll likely need to use the datasets library in Colab. Since they’ve focused on regression, it’s safe to assume we’ll predict ratings from text and meta features. I’ll structure the notebook with sections 0–12, focusing on regression, leveraging the provided numeric, categorical, and text features.

Handling regression task for rating

It looks like we'll be crafting code for regression to predict ratings based on features like review text, drugName, condition, and usefulCount. I'll write the code to load the dataset from Hugging Face, adaptively handle column names, and manage memory efficiently, whether using the dataset splits or reading directly from the hub.

Processing EDA and modeling for regression

We need to generate EDA tables, describe the data, visualize distributions, check missing values, and handle categorical data. For regression, we’ll set up normalization and a KNN regressor, compute metrics, and tune hyperparameters. Since they’ve focused on regression, classification is not relevant, though we can adjust later if needed.

Creating the .ipynb file and structure

I’ll help the user create a notebook with regression code, including KNN, Ridge, and pipelines for processing text and categorical features. The notebook will load the dataset from Hugging Face and handle caching automatically. We’ll provide code sections for the analysis in a clean, organized notebook without using large CSVs, just as they requested. To keep it under the 100MB limit, we’ll include an optional compressed sample CSV.

Building notebook structure

I’ll provide a comprehensive notebook structure the user can paste directly into Colab. It’ll cover everything from reading data, handling missing values, encoding features, scaling, to training KNN and Ridge models. I’ll also suggest sensible file naming and paths for the repo, ensuring consistency with their other work. Additionally, I’ll include a basic README outline and a list of suggested requirements. This approach lets them quickly run everything from loading data to evaluation without external downloads.

Providing notebook structure

I’ll give the user a single ready-to-paste notebook structure that includes everything, from installing packages to running the regression models. There will be clear steps with section comments and optional toggles for classification. I’ll also give instructions on saving datasets and files, while ensuring everything is straightforward and doesn’t overwhelm them with too many figures or complexity. The goal is for them to copy it into Colab and run it seamlessly!

Thought for 1m 6s

回答

отлично — давай сделаем тебе «один такой» полноценный ноутбук под твой датасет Drug Review и разложим repo аккуратно, как у одногруппника. Я дам тебе:

  1. чёткую структуру файлов и нейминг для твоего репозитория MachineLearningUNN2025;
  2. готовый Colab-ноутбук (большой кодовый блок ниже), который:
    • сам подтянет датасет с Hugging Face (forwins/Drug-Review-Dataset),
    • сделает EDA, графики, корреляции, обработает пропуски/категории/текст,
    • запустит KNN регрессию (основная задача) с подбором k, посчитает MAE/MSE/R²,
    • по желанию — доп. блок «классификация» с матрицей ошибок (как у примера),
    • при желании сохранит небольшой сэмпл данных в data/drug_reviews_sample_50k.csv.gz (<100 МБ), чтобы было что положить в git, но сырой огромный CSV не нужен — ноутбук качает данные на лету.

Что положить в репозиторий и как назвать

В корне MachineLearningUNN2025 (как у примера):

text
MachineLearningUNN2025/ └─ ML-practice-unn/ ├─ Task2DeryabinM_DrugReviews_Regression.ipynb ← основной ноутбук ├─ data/ │ └─ drug_reviews_sample_50k.csv.gz ← опционально (сэмпл <100 МБ) ├─ requirements.txt ← зависимости для локального запуска └─ README.md ← краткое описание, как запустить
  • Имя файла ноутбука: Task2DeryabinM_DrugReviews_Regression.ipynb (по аналогии с Task2BaranovA.ipynb, только под твоё ФИО и тему).
  • CSV класть необязательно. Если хочется — сохраняем сжатый сэмпл (код ниже делает csv.gz).
  • В README.md опиши 0–12 шаги (ниже будет текст-рыба, который можно вставить).

Как пользоваться этим кодом

  1. Открой Google Colab → «Файл → Новый блокнот».
  2. Скопируй весь код из блока ниже в одну ячейку и запусти.
  3. По завершении:
    • В меню Colab: «Файл → Сохранить копию в GitHub» → выбери репозиторий MachineLearningUNN2025 и путь ML-practice-unn/Task2DeryabinM_DrugReviews_Regression.ipynb.
    • (Опционально) Забери созданный файл data/drug_reviews_sample_50k.csv.gz из панели файлов Colab и добавь в репозиторий тем же путём.

Готовый ноутбук (копируй целиком в Colab)

python
# ============================================================ # Task2DeryabinM_DrugReviews_Regression.ipynb # Автор: Дерябин Максим Сергеевич # Репозиторий: MachineLearningUNN2025 # Датасет: forwins/Drug-Review-Dataset (Hugging Face) # Задача: РЕГРЕССИЯ (предсказать rating) # Дополнительно (по желанию): бинарная классификация high_rating (>=8) # ============================================================ # ----- 0. Установка и импорт библиотек !pip -q install datasets scikit-learn pandas numpy matplotlib seaborn imbalanced-learn import os, re, math, textwrap, warnings warnings.filterwarnings("ignore") import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from datasets import load_dataset from sklearn.model_selection import train_test_split from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.impute import SimpleImputer from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.neighbors import KNeighborsRegressor, KNeighborsClassifier from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import Ridge from sklearn.ensemble import RandomForestRegressor # ПОВЕДЕНИЕ (настраиваемые флаги) DO_SAVE_SAMPLE_CSV = True # сохранить сэмпл в репозиторий (<100 МБ) DO_CLASSIFICATION_EXTRA = False # доп. блок с классификацией и конф. матрицами (как у примера) N_SAMPLE_FOR_KNN_TEXT = 30000 # сэмпл для текстового KNN, чтобы не «убить» Colab RANDOM_STATE = 42 np.random.seed(RANDOM_STATE) print("Готовим данные...") # ----- 1. Чтение данных с Hugging Face # Пробуем загрузить стандартный сплит. Если структура иная — fallback к общему сплиту. try: ds = load_dataset("forwins/Drug-Review-Dataset", split="train") except Exception as e: try: ds = load_dataset("forwins/Drug-Review-Dataset")["train"] except Exception as e2: raise RuntimeError(f"Не удалось загрузить датасет: {e}\n{e2}") df = ds.to_pandas() print("Размерность исходной таблицы:", df.shape) print("Первые 5 строк:"); display(df.head()) # ----- 0. Описание задачи (словами — пойдёт в README, просто печатаем здесь) task_description = """ Задача: предсказать числовой рейтинг (rating) по отзывам о лекарствах и метаданным (регрессия). Датасет: Drug Review Dataset (Hugging Face, forwins/Drug-Review-Dataset). Основные поля (могут немного отличаться): review (текст), rating (целевое), drugName, condition, date, usefulCount. Текстовые признаки: review; Категориальные: drugName, condition; Числовые: usefulCount и инженерные признаки (длина текста, год и т.п.). """ print(textwrap.dedent(task_description)) # ----- 1b. Унифицируем имена столбцов (на случай вариаций) cols_lower = {c: c.lower() for c in df.columns} df = df.rename(columns=cols_lower) # наиболее ожидаемые имена CAND_RATING = ["rating", "score", "stars"] CAND_REVIEW = ["review", "text", "content", "reviews"] CAND_DRUG = ["drugname", "drug_name", "drug", "medicine"] CAND_CONDITION = ["condition", "diagnosis", "disease"] CAND_USEFUL = ["usefulcount", "helpful", "helpful_count", "useful_count"] CAND_DATE = ["date", "review_date", "time"] def find_col(cands): for c in cands: if c in df.columns: return c return None col_rating = find_col(CAND_RATING) col_review = find_col(CAND_REVIEW) col_drug = find_col(CAND_DRUG) col_condition = find_col(CAND_CONDITION) col_useful = find_col(CAND_USEFUL) col_date = find_col(CAND_DATE) print("Опознаны столбцы:", "\nrating =", col_rating, "\nreview =", col_review, "\ndrugName =", col_drug, "\ncondition =", col_condition, "\nusefulCount=", col_useful, "\ndate =", col_date) # Минимальные требования assert col_rating is not None, "Не найден целевой столбец rating!" assert col_review is not None, "Не найден текст отзывов (review)!" # ----- 2. Разбивка на train/test (стратификация для регрессии обычно не применяется) # Сначала лёгкий препроцесс для фичеинжиниринга. df_work = df.copy() # Обработка даты if col_date in df_work.columns: # пробуем распарсить дату df_work["_date_parsed"] = pd.to_datetime(df_work[col_date], errors="coerce") df_work["year"] = df_work["_date_parsed"].dt.year df_work["month"] = df_work["_date_parsed"].dt.month else: df_work["year"] = np.nan df_work["month"] = np.nan # Инженерные: длина текста (символы и слова) df_work["review_len_char"] = df_work[col_review].fillna("").astype(str).apply(len) df_work["review_len_word"] = df_work[col_review].fillna("").astype(str).apply(lambda s: len(s.split())) # Целевая переменная (float) y = pd.to_numeric(df_work[col_rating], errors="coerce") # базовые числовые num_cols_raw = [] if col_useful in df_work.columns: num_cols_raw.append(col_useful) num_cols_raw += ["review_len_char", "review_len_word", "year", "month"] # Категориальные cat_cols_raw = [] if col_drug in df_work.columns: cat_cols_raw.append(col_drug) if col_condition in df_work.columns: cat_cols_raw.append(col_condition) # Текстовые text_col = col_review # Выбираем подмножество признаков для EDA/моделей X = df_work[[c for c in (cat_cols_raw + num_cols_raw + [text_col]) if c in df_work.columns]].copy() # Train / Test split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=RANDOM_STATE ) print("Train shape:", X_train.shape, "| Test shape:", X_test.shape) # ----- 3. Визуализации и основные характеристики print("\nИнформация по данным:") print(X_train.info()) print("\nБазовая описательная статистика по числовым:") display(X_train[num_cols_raw].describe()) # Гистограмма таргета plt.figure(figsize=(7,4)) sns.histplot(y_train, bins=30, kde=True) plt.title("Распределение rating (train)") plt.show() # Текст: распределение длины plt.figure(figsize=(7,4)) sns.histplot(X_train["review_len_char"], bins=40, kde=True) plt.title("Длина отзыва (символы)") plt.show() # Топ-20 популярных drugName / condition (если есть) def top_bar(colname, title): if colname in X_train.columns: vc = X_train[colname].value_counts().head(20) plt.figure(figsize=(8,6)) sns.barplot(x=vc.values, y=vc.index) plt.title(title) plt.xlabel("count"); plt.ylabel(colname); plt.show() top_bar(col_drug, "Топ-20 drugName") top_bar(col_condition, "Топ-20 condition") # Корреляции числовых с таргетом (Spearman) num_present = [c for c in num_cols_raw if c in X_train.columns] if num_present: corr_spearman = pd.Series({c: X_train[c].corr(y_train, method="spearman") for c in num_present}) print("\nСпирмен-корреляция числовых фич с rating:") display(corr_spearman.sort_values(key=np.abs, ascending=False)) # Матрица корреляций между числовыми corr_ff = X_train[num_present].corr(method="spearman") plt.figure(figsize=(6,5)) sns.heatmap(corr_ff, annot=True, fmt=".2f", vmin=-1, vmax=1, cmap="coolwarm") plt.title("Корреляции числовых признаков") plt.show() # ----- 4. Пропуски missing_pct = X_train.isnull().mean().sort_values(ascending=False) * 100 print("\nПроценты пропусков (train):"); display(missing_pct) # Стратегии: # - для текста: NaN -> '' (пустая строка) # - для категориальных: NaN -> 'Unknown' # - для числовых: SimpleImputer(strategy='median') # Объяснение: медиа́на робастна к выбросам, для категорий/текста важно не терять строки. # ----- 5–6. Признаки: категории+текст, нормализация числовых numeric_features = num_present categorical_features = [c for c in cat_cols_raw if c in X_train.columns] text_feature = text_col numeric_transformer = Pipeline(steps=[ ("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler()) ]) categorical_transformer = Pipeline(steps=[ ("imputer", SimpleImputer(strategy="most_frequent")), ("ohe", OneHotEncoder(handle_unknown="ignore", sparse=True)) ]) text_transformer = Pipeline(steps=[ # min_df=5 отсечёт очень редкие токены; bigrams дают чуть больше сигналов ("tfidf", TfidfVectorizer(min_df=5, ngram_range=(1,2), max_features=50000)) ]) # ----- 7–8. Модель 1 (базовая): KNN-регрессия на ТАБЛИЧНЫХ признаках (без текста) # Аргументация: KNN чувствителен к размерности и масштабу; для большой sparse-текста неэффективен. # Поэтому базовый KNN — на числовых + OHE категорий (быстрее и на всём объёме). ct_tabular = ColumnTransformer( transformers=[ ("num", numeric_transformer, numeric_features), ("cat", categorical_transformer, categorical_features), # текст пропускаем в этой модели ], remainder="drop", sparse_threshold=1.0 ) knn_tabular = Pipeline(steps=[ ("prep", ct_tabular), ("knn", KNeighborsRegressor(n_neighbors=5, weights="distance")) ]) print("\nОбучаем базовый KNN (табличные признаки) ...") knn_tabular.fit(X_train, y_train) pred_tr = knn_tabular.predict(X_train) pred_te = knn_tabular.predict(X_test) def print_regression_metrics(y_true, y_pred, title): mae = mean_absolute_error(y_true, y_pred) rmse = mean_squared_error(y_true, y_pred, squared=False) r2 = r2_score(y_true, y_pred) print(f"{title}: MAE={mae:.4f} | RMSE={rmse:.4f} | R^2={r2:.4f}") print_regression_metrics(y_train, pred_tr, "KNN(tabular) train") print_regression_metrics(y_test, pred_te, "KNN(tabular) test") # Подбор k (n_neighbors) results = [] best = {"k": None, "r2": -np.inf, "pred": None} for k in [1,3,5,7,9,11,13,15,21]: model_k = Pipeline(steps=[ ("prep", ct_tabular), ("knn", KNeighborsRegressor(n_neighbors=k, weights="distance")) ]) model_k.fit(X_train, y_train) p = model_k.predict(X_test) r2 = r2_score(y_test, p) results.append((k, r2)) if r2 > best["r2"]: best = {"k": k, "r2": r2, "pred": p} print("\nПодбор k (табличные):", results) print(f"Лучший k = {best['k']} | R^2(test) = {best['r2']:.4f}") # ----- 9. Модель 2 (текст + табличные): Ridge (линейная), как разумная альтернатива KNN для sparse # Аргументация: для высокоразмерного TF-IDF линейные модели устойчивее и быстрее, чем KNN. # Чтобы не «класть» Colab, для этой модели можно взять весь датасет; Ridge справится. ct_text_full = ColumnTransformer( transformers=[ ("num", numeric_transformer, numeric_features), ("cat", categorical_transformer, categorical_features), ("txt", text_transformer, text_feature) ], remainder="drop", sparse_threshold=1.0 ) ridge_text = Pipeline(steps=[ ("prep", ct_text_full), ("ridge", Ridge(alpha=1.0, random_state=RANDOM_STATE)) ]) print("\nОбучаем Ridge (табличные + текст TF-IDF) ...") ridge_text.fit(X_train, y_train) pred_tr_r = ridge_text.predict(X_train) pred_te_r = ridge_text.predict(X_test) print_regression_metrics(y_train, pred_tr_r, "Ridge(text+tab) train") print_regression_metrics(y_test, pred_te_r, "Ridge(text+tab) test") # Также можно сравнить с деревьями (без текста) — устойчивы к категориальному OHE и числу фич rf = Pipeline(steps=[ ("prep", ct_tabular), ("rf", RandomForestRegressor( n_estimators=200, max_depth=None, n_jobs=-1, random_state=RANDOM_STATE)) ]) print("\nОбучаем RandomForest (табличные) ...") rf.fit(X_train, y_train) print_regression_metrics(y_test, rf.predict(X_test), "RandomForest(tabular) test") # ----- 10. (опционально) Классификация для отчёта с матрицей ошибок # Например, high_rating = 1 если rating >= 8, иначе 0 (можно менять порог) if DO_CLASSIFICATION_EXTRA: print("\n[Дополнительно] Запускаем классификацию high_rating ...") y_clf = (y >= 8).astype(int) X_train_c, X_test_c, y_train_c, y_test_c = train_test_split( X, y_clf, test_size=0.2, random_state=RANDOM_STATE, stratify=y_clf ) ct_tabular_c = ct_tabular knn_clf = Pipeline(steps=[ ("prep", ct_tabular_c), ("knn", KNeighborsClassifier(n_neighbors=5)) ]) knn_clf.fit(X_train_c, y_train_c) y_pred_c = knn_clf.predict(X_test_c) print("Accuracy:", accuracy_score(y_test_c, y_pred_c)) print("\nConfusion Matrix:\n", confusion_matrix(y_test_c, y_pred_c)) print("\nClassification Report:\n", classification_report(y_test_c, y_pred_c)) # Подбор k best_c = {"k": None, "acc": -1, "pred": None} per_k = [] for k in range(1,16): m = Pipeline(steps=[("prep", ct_tabular_c), ("knn", KNeighborsClassifier(n_neighbors=k))]) m.fit(X_train_c, y_train_c) p = m.predict(X_test_c) acc = accuracy_score(y_test_c, p) per_k.append((k,acc)) if acc > best_c["acc"]: best_c = {"k":k, "acc":acc, "pred":p} print("\nPer-k accuracy:", per_k) print(f"\nBest k = {best_c['k']} | Accuracy = {best_c['acc']:.4f}") print("\nConfusion Matrix (best k):\n", confusion_matrix(y_test_c, best_c["pred"])) print("\nClassification Report (best k):\n", classification_report(y_test_c, best_c["pred"], digits=3)) # ----- 11. Исключение сильно коррелирующих признаков (демо) # Для табличных числовых можно убрать один из пары с |corr|>0.9 (если такие есть) to_drop = [] corr_thr = 0.9 if len(num_present) >= 2: corr_mat = X_train[num_present].corr().abs() upper = corr_mat.where(np.triu(np.ones(corr_mat.shape), k=1).astype(bool)) to_drop = [column for column in upper.columns if any(upper[column] > corr_thr)] if to_drop: print("\nСильно коррелирующие числовые признаки найдены и будут исключены:", to_drop) else: print("\nСильно коррелирующие числовые признаки не найдены.") else: print("\nНедостаточно числовых признаков для корреляционного отсева.") # ----- 12. Выгрузка сэмпла данных (опционально) и requirements.txt os.makedirs("ML-practice-unn/data", exist_ok=True) if DO_SAVE_SAMPLE_CSV: sample_cols = [c for c in [col_review, col_rating, col_drug, col_condition, col_useful, col_date] if c in df.columns] # 50k строк достаточно репрезентативно и компактно sample_df = df[sample_cols].sample(n=min(50000, len(df)), random_state=RANDOM_STATE) out_path = "ML-practice-unn/data/drug_reviews_sample_50k.csv.gz" sample_df.to_csv(out_path, index=False, compression="gzip") print(f"\nСохранён сэмпл данных: {out_path}") # requirements для локального запуска with open("ML-practice-unn/requirements.txt", "w") as f: f.write("\n".join([ "pandas", "numpy", "matplotlib", "seaborn", "scikit-learn", "datasets", "imbalanced-learn" ])) print("Создан ML-practice-unn/requirements.txt") print("\nГОТОВО. Дальше: Файл → Сохранить копию в GitHub → выбери свой репозиторий и путь ML-practice-unn/Task2DeryabinM_DrugReviews_Regression.ipynb")

Короткий текст для README.md (можно вставить и подредактировать)

Задача: регрессия — предсказать rating по отзывам о лекарствах и метаданным.
Датасет: HuggingFace forwins/Drug-Review-Dataset (~184 622 объектов).
Признаки:

  • текстовые: review;
  • категориальные: drugName, condition;
  • числовые: usefulCount, а также инженерные (review_len_*, year, month).

Пайплайн:
0) Описание задачи.

  1. Чтение данных через datasets.load_dataset.
  2. Train/Test = 80/20.
  3. EDA: распределения, топ-категории, корреляции.
  4. Пропуски: SimpleImputer (median/most_frequent), текст — пустая строка.
  5. Категориальные: OneHotEncoder(handle_unknown='ignore').
  6. Нормализация числовых: StandardScaler (важно для KNN).
  7. Модель: KNNRegressor (табличные признаки).
  8. Метрики: MAE, RMSE, R² на train/test; подбор k.
  9. Доп. модели: Ridge с TF-IDF (текст+таблица), RandomForest (табличные).
  10. (Опц.) Классификация high_rating (>=8) с матрицей ошибок.
  11. (Опц.) Отсев сильно коррелирующих числовых.
  12. Выводы.

Почему KNN и почему без текста?
KNN плохо масштабируется на огромных sparse-матрицах TF-IDF. Поэтому базовый KNN делаем на табличных фичах (быстро и стабильно), а для текста добавляем линейную модель Ridge, которая лучше подходит к большим разреженным пространствам.


Мини-шпаргалка по коммиту в GitHub

Если не через меню Colab, то локально:

bash
git clone https://github.com/<ты>/MachineLearningUNN2025.git cd MachineLearningUNN2025 # помести сюда папку ML-practice-unn с ноутбуком и (опционально) data/... git add ML-practice-unn git commit -m "Task2: Drug Reviews regression (KNN + Ridge)" git push

если что-то «падает» в Colab из-за памяти — уменьши N_SAMPLE_FOR_KNN_TEXT (или оставь текст только в Ridge). Но базовый KNN по табличным должен бегать шустро даже на всём объёме.

分享這個問答