"С вашим датасетом нужно проделать примерно то же ...
تم الإنشاء في: ١٧ أكتوبر ٢٠٢٥
تمت الإجابة باستخدام GPT-5 Thinking بواسطة Chat01
تم الإنشاء في: ١٧ أكتوبر ٢٠٢٥
تمت الإجابة باستخدام GPT-5 Thinking بواسطة Chat01
"С вашим датасетом нужно проделать примерно то же самое, что мы делали на лекции. Обратите внимание, что у вас может быть задача классификации, а мы решали задачу регрессии (также можно посмотреть 19-02. Adult.ipynb, где решается задача классификации) (https://github.com/NikolaiZolotykh/MachineLearningCourse). Все должно быть выложено на ваш гит.
А именно:
0. Описать задачу словами
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):
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
text0 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
text0 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).
Целью задачи является определение принадлежности гриба к одному из классов по внешним признакам. В ходе решения задачи будут использованы различные методы машинного обучения, будут сделаны выводы о целесообразности их использования в зависимости от аккуратности предсказания.
! 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
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
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
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):
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" присутствует только одно уникальное значение, следовательно, этот столбец не несёт полезной нагрузки для нашей задачи и в дальнейшем может быть удалён.
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. Наиболее равномерно по видам популяции распределены грибы, обитающие в траве, в то время как грибы, обитающие в мусоре, растут только группами.
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)
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
Пропущенных значений больше нет.
Такой подход позволяет моделям машинного обучения работать с данными в числовом формате. В данной задаче это допустимо, поскольку используются модели, нечувствительные к порядку категорий.
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".
В этом наборе данных все признаки категориальные, они описывают качественные характеристики грибов — форму шляпки, запах, цвет, место обитания и т.д., такие признаки не обладают метрическим смыслом, поэтому нормализация не требуется.
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)
textaccuracy_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мб возможно мне стоит сделать также, дай мне решение как это сделать - хотелось бы просто чтобы ты дал мне код который я смогу вставить в гугл коллаб и чтобы я получил сразу свой один такой файл который смогу добавить в репозиторий
отлично — давай сделаем тебе «один такой» полноценный ноутбук под твой датасет Drug Review и разложим repo аккуратно, как у одногруппника. Я дам тебе:
MachineLearningUNN2025;forwins/Drug-Review-Dataset),k, посчитает MAE/MSE/R²,data/drug_reviews_sample_50k.csv.gz (<100 МБ), чтобы было что положить в git, но сырой огромный CSV не нужен — ноутбук качает данные на лету.В корне MachineLearningUNN2025 (как у примера):
textMachineLearningUNN2025/ └─ 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.gz).README.md опиши 0–12 шаги (ниже будет текст-рыба, который можно вставить).MachineLearningUNN2025 и путь ML-practice-unn/Task2DeryabinM_DrugReviews_Regression.ipynb.data/drug_reviews_sample_50k.csv.gz из панели файлов 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")
Задача: регрессия — предсказать rating по отзывам о лекарствах и метаданным.
Датасет: HuggingFace forwins/Drug-Review-Dataset (~184 622 объектов).
Признаки:
review;drugName, condition;usefulCount, а также инженерные (review_len_*, year, month).Пайплайн:
0) Описание задачи.
datasets.load_dataset.SimpleImputer (median/most_frequent), текст — пустая строка.OneHotEncoder(handle_unknown='ignore').StandardScaler (важно для KNN).k.Ridge с TF-IDF (текст+таблица), RandomForest (табличные).Почему KNN и почему без текста?
KNN плохо масштабируется на огромных sparse-матрицах TF-IDF. Поэтому базовый KNN делаем на табличных фичах (быстро и стабильно), а для текста добавляем линейную модель Ridge, которая лучше подходит к большим разреженным пространствам.
Если не через меню Colab, то локально:
bashgit 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 по табличным должен бегать шустро даже на всём объёме.