192 KiB
Данные по инсультам¶
Выведем информацию о столбцах датасета:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import RandomOverSampler
from sklearn.preprocessing import StandardScaler
import featuretools as ft
import time
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
df = pd.read_csv("..//..//static//csv//healthcare-dataset-stroke-data.csv")
print(df.columns)
df.head()
Определим бизнес цели и цели технического проекта.¶
- Улучшение диагностики и профилактики инсульта.
- Бизнес-цель: повышение точности прогнозирования риска инсульта среди пациентов для более раннего лечебного вмешательства. Определение основных факторов риска для более целенаправленного подхода в медицинском обслуживании.
- Цель технического проекта: разработка статистической модели, которая решает задачу классификации и предсказывает возможность возникновения инсульта у пациентов на основе имеющихся данных (возраст, гипертония, заболевания сердца и пр.), с целью выявления групп риска. Внедрение этой модели в систему поддержки принятия медицинских решений для врачей.
- Снижение расходов на лечение инсультов.
- Бизнес-цель: снижение затрат на лечение инсульта путем более эффективного распределения медицинских ресурсов и направленных профилактических мер.
- Цель технического проекта: создание системы оценки индивидуального риска инсульта для пациентов, что позволит медучреждениям проводить профилактические меры среди целевых групп, сокращая расходы на лечение.
И теперь проверим датасет на пустые значения:¶
# Количество пустых значений признаков
print(df.isnull().sum())
print()
# Есть ли пустые значения признаков
print(df.isnull().any())
print()
# Процент пустых значений признаков
for i in df.columns:
null_rate = df[i].isnull().sum() / len(df) * 100
if null_rate > 0:
print(f"{i} процент пустых значений: %{null_rate:.2f}")
В столбце bmi можно заметить пустые значение. Заменим их на медиану:
# Замена значений
df["bmi"] = df["bmi"].fillna(df["bmi"].median())
# Проверка на пропущенные значения после замены
missing_values_after_drop = df.isnull().sum()
# Вывод результатов после замены
print("\nКоличество пустых значений в каждом столбце после замены:")
print(missing_values_after_drop)
Удалим из датафрейма столбец id, потому что нет смысла учитывать его при предсказании:
df = df.drop('id', axis=1)
print(df.columns)
Можно перейти к созданию выборок¶
# Разделение данных на признаки (X) и целевую переменную (y)
# В данном случае мы хотим предсказать 'stroke'
X = df.drop(columns=['stroke'])
y = df['stroke']
# Разбиение данных на обучающую и тестовую выборки
# Сначала разделим на обучающую и тестовую
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
# Затем разделим обучающую выборку на обучающую и контрольную
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.3)
# Проверка размеров выборок
print("Размер обучающей выборки:", X_train.shape)
print("Размер контрольной выборки:", X_val.shape)
print("Размер тестовой выборки:", X_test.shape)
Оценим сбалансированность выборок:¶
# Функция для анализа сбалансированности
def analyze_balance(y_train, y_val, y_test, y_name):
# Распределение классов
print("Распределение классов в обучающей выборке:")
print(y_train.value_counts(normalize=True))
print("\nРаспределение классов в контрольной выборке:")
print(y_val.value_counts(normalize=True))
print("\nРаспределение классов в тестовой выборке:")
print(y_test.value_counts(normalize=True))
# Создание фигуры и осей для трех столбчатых диаграмм
fig, axes = plt.subplots(1, 3, figsize=(18, 5), sharey=True)
fig.suptitle('Распределение в различных выборках')
# Обучающая выборка
sns.barplot(x=y_train.value_counts().index, y=y_train.value_counts(normalize=True), ax=axes[0])
axes[0].set_title('Обучающая выборка')
axes[0].set_xlabel(y_name)
axes[0].set_ylabel('Доля')
# Контрольная выборка
sns.barplot(x=y_val.value_counts().index, y=y_val.value_counts(normalize=True), ax=axes[1])
axes[1].set_title('Контрольная выборка')
axes[1].set_xlabel(y_name)
# Тестовая выборка
sns.barplot(x=y_test.value_counts().index, y=y_test.value_counts(normalize=True), ax=axes[2])
axes[2].set_title('Тестовая выборка')
axes[2].set_xlabel(y_name)
plt.show()
analyze_balance(y_train, y_val, y_test, 'stroke')
Легко заметить, что выборки несбалансированны. Необходимо сбалансировать обучающую и контрольную выборки, чтобы получить лучшие результаты при обучении модели. Для балансировки применим RandomOverSampler:
ros = RandomOverSampler(random_state=42)
# Применение RandomOverSampler для балансировки выборок
X_train_resampled, y_train_resampled = ros.fit_resample(X_train, y_train)
X_val_resampled, y_val_resampled = ros.fit_resample(X_val, y_val)
# Проверка сбалансированности после RandomOverSampler
analyze_balance(y_train_resampled, y_val_resampled, y_test, 'stroke')
Выборки сбалансированы.
Перейдем к конструированию признаков¶
Для начала применим унитарное кодирование категориальных признаков (one-hot encoding), переведя их в бинарные вектора:
# Определение категориальных признаков
categorical_features = ['gender', 'ever_married', 'work_type', 'Residence_type', 'smoking_status']
# Применение one-hot encoding к обучающей выборке
X_train_encoded = pd.get_dummies(X_train_resampled, columns=categorical_features, drop_first=True)
# Применение one-hot encoding к контрольной выборке
X_val_encoded = pd.get_dummies(X_val_resampled, columns=categorical_features, drop_first=True)
# Применение one-hot encoding к тестовой выборке
X_test_encoded = pd.get_dummies(X_test, columns=categorical_features, drop_first=True)
print(X_train_encoded.head())
Далее к числовым признакам, а именно к колонке age, применим дискретизацию (позволяет преобразовать данные из числового представления в категориальное):
# Определение числовых признаков для дискретизации
numerical_features = ['age']
# Функция для дискретизации числовых признаков
def discretize_features(df, features, bins, labels):
for feature in features:
df[f'{feature}_bin'] = pd.cut(df[feature], bins=bins, labels=labels)
df.drop(columns=[feature], inplace=True)
return df
# Заданные интервалы и метки
age_bins = [0, 25, 55, 100]
age_labels = ["young", "middle-aged", "old"]
# Применение дискретизации к обучающей, контрольной и тестовой выборкам
X_train_encoded = discretize_features(X_train_encoded, numerical_features, bins=age_bins, labels=age_labels)
X_val_encoded = discretize_features(X_val_encoded, numerical_features, bins=age_bins, labels=age_labels)
X_test_encoded = discretize_features(X_test_encoded, numerical_features, bins=age_bins, labels=age_labels)
print(X_train_encoded.head())
Применим ручной синтез признаков. Это создание новых признаков на основе существующих, учитывая экспертные знания и логику предметной области. К примеру, в этом случае можно создать признак, в котором вычисляется насколько уровень глюкозы отклоняется от среднего для возрастной группы пациента. Такой признак может быть полезен для выделения пациентов с нетипичными данными.
age_glucose_mean = X_train_encoded.groupby('age_bin', observed=False)['avg_glucose_level'].transform('mean')
X_train_encoded['glucose_age_deviation'] = X_train_encoded['avg_glucose_level'] - age_glucose_mean
age_glucose_mean = X_val_encoded.groupby('age_bin', observed=False)['avg_glucose_level'].transform('mean')
X_val_encoded['glucose_age_deviation'] = X_val_encoded['avg_glucose_level'] - age_glucose_mean
age_glucose_mean = X_test_encoded.groupby('age_bin', observed=False)['avg_glucose_level'].transform('mean')
X_test_encoded['glucose_age_deviation'] = X_test_encoded['avg_glucose_level'] - age_glucose_mean
print(X_train_encoded.head())
Теперь используем масштабирование признаков, что позволяет привести все числовые признаки к одинаковым или очень похожим диапазонам значений либо распределениям. По результатам многочисленных исследований масштабирование признаков позволяет получить более качественную модель за счет снижения доминирования одних признаков над другими.
# Пример масштабирования числовых признаков
numerical_features = ['avg_glucose_level', 'bmi', 'glucose_age_deviation']
scaler = StandardScaler()
X_train_encoded[numerical_features] = scaler.fit_transform(X_train_encoded[numerical_features])
X_val_encoded[numerical_features] = scaler.transform(X_val_encoded[numerical_features])
X_test_encoded[numerical_features] = scaler.transform(X_test_encoded[numerical_features])
print(X_train_encoded.head())
И также попробуем сконструировать признаки, используя фреймворк Featuretools:
data = X_train_encoded.copy() # Используем предобработанные данные
es = ft.EntitySet(id="patients")
es = es.add_dataframe(dataframe_name="strokes_data", dataframe=data, index="index", make_index=True)
feature_matrix, feature_defs = ft.dfs(
entityset=es,
target_dataframe_name="strokes_data",
max_depth=1
)
print(feature_matrix.head())
Оценим качество набора признаков.¶
Представим основные оценки качества наборов признаков:
Предсказательная способность (для задачи классификации) Метрики: Accuracy, Precision, Recall, F1-Score, ROC AUC
Методы: Обучение модели на обучающей выборке и оценка на контрольной и тестовой выборках.
Скорость вычисления
Методы: Измерение времени выполнения генерации признаков и обучения модели.
Надежность
Методы: Кросс-валидация, анализ чувствительности модели к изменениям в данных.
Корреляция
Методы: Анализ корреляционной матрицы признаков, удаление мультиколлинеарных признаков.
Цельность
Методы: Проверка логической связи между признаками и целевой переменной, интерпретация результатов модели.
X_train_encoded = pd.get_dummies(X_train_encoded, drop_first=True)
X_val_encoded = pd.get_dummies(X_val_encoded, drop_first=True)
X_test_encoded = pd.get_dummies(X_test_encoded, drop_first=True)
all_columns = X_train_encoded.columns
X_train_encoded = X_train_encoded.reindex(columns=all_columns, fill_value=0)
X_val_encoded = X_val_encoded.reindex(columns=all_columns, fill_value=0)
X_test_encoded = X_test_encoded.reindex(columns=all_columns, fill_value=0)
# Выбор модели
model = RandomForestClassifier(n_estimators=100, random_state=42)
# Начинаем отсчет времени
start_time = time.time()
model.fit(X_train_encoded, y_train_resampled)
# Время обучения модели
train_time = time.time() - start_time
print(f'Время обучения модели: {train_time:.2f} секунд')
# Получение важности признаков
importances = model.feature_importances_
feature_names = X_train_encoded.columns
# Сортировка признаков по важности
feature_importance = pd.DataFrame({'feature': feature_names, 'importance': importances})
feature_importance = feature_importance.sort_values(by='importance', ascending=False)
print("Feature Importance:")
print(feature_importance)
# Предсказание и оценка
y_pred = model.predict(X_test_encoded)
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
roc_auc = roc_auc_score(y_test, y_pred)
print(f"Accuracy: {accuracy}")
print(f"Precision: {precision}")
print(f"Recall: {recall}")
print(f"F1 Score: {f1}")
print(f"ROC AUC: {roc_auc}")
# Кросс-валидация
scores = cross_val_score(model, X_train_encoded, y_train_resampled, cv=5, scoring='accuracy')
accuracy_cv = scores.mean()
print(f"Cross-validated Accuracy: {accuracy_cv}")
# Анализ важности признаков
feature_importances = model.feature_importances_
feature_names = X_train_encoded.columns
importance_df = pd.DataFrame({'Feature': feature_names, 'Importance': feature_importances})
importance_df = importance_df.sort_values(by='Importance', ascending=False)
plt.figure(figsize=(10, 6))
sns.barplot(x='Importance', y='Feature', data=importance_df)
plt.title('Feature Importance')
plt.show()
# Проверка на переобучение
y_train_pred = model.predict(X_train_encoded)
accuracy_train = accuracy_score(y_train_resampled, y_train_pred)
precision_train = precision_score(y_train_resampled, y_train_pred)
recall_train = recall_score(y_train_resampled, y_train_pred)
f1_train = f1_score(y_train_resampled, y_train_pred)
roc_auc_train = roc_auc_score(y_train_resampled, y_train_pred)
print(f"Train Accuracy: {accuracy_train}")
print(f"Train Precision: {precision_train}")
print(f"Train Recall: {recall_train}")
print(f"Train F1 Score: {f1_train}")
print(f"Train ROC AUC: {roc_auc_train}")