282 KiB
бизнес-цели и 2 задачи, которые нужно решить:
Снижение вероятности инсульта у пациентов с высоким риском путем раннего выявления предрасположенности.
Оптимизация медицинских услуг, предоставляемых пациентам, с учетом их риска инсульта.
Разработать модель, которая прогнозирует вероятность инсульта у пациента.
Определить значимые признаки для анализа риска инсульта, чтобы направить усилия медицинских работников на важные факторы.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Загрузка данных
data = pd.read_csv('./csv/option4.csv')
# Обзор данных
print("Количество колонок:", data.columns.size)
print("Колонки:", data.columns)
print("\nНаличие пропущенных значений:")
print(data.isnull().sum())
print("\n\n")
print(data.describe)
Возьмем и заменим нулевые значения в столбце bmi на средние значения по столбцу
data['bmi'] = data['bmi'].fillna(data['bmi'].median())
print("\nНаличие пропущенных значений:")
print(data.isnull().sum())
Взглянем на выбросы:
def plot_numeric_boxplots(dataframe):
# Фильтрация числовых столбцов
numeric_columns = ['age', 'avg_glucose_level', 'bmi']
# Построение графиков
if numeric_columns:
plt.figure(figsize=(15, 5))
for i, col in enumerate(numeric_columns):
if col != 'id':
plt.subplot(1, len(numeric_columns), i + 1)
sns.boxplot(y=dataframe[col])
plt.title(f'{col}')
plt.ylabel('')
plt.xlabel(col)
plt.tight_layout()
plt.show()
else:
print("Нет подходящих числовых столбцов для построения графиков.")
plot_numeric_boxplots(data)
Видим выбросы в столбцах со средним уровнем глюкозы и в столбце bmi (индекс массы тела). устраним выбросы - поставим верхние и нижние границы
def remove_outliers(df):
numeric_columns = ['age', 'avg_glucose_level', 'bmi']
for column in numeric_columns:
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df[column] = df[column].apply(lambda x: lower_bound if x < lower_bound else upper_bound if x > upper_bound else x)
return df
data = remove_outliers(data)
plot_numeric_boxplots(data)
Так, от выбросов избавились, теперь разобьем на выборки
from sklearn.model_selection import train_test_split
# Определение признаков и целевой переменной
X = data.drop(columns=['id', 'stroke'])
y = data['stroke']
# Обучающая выборка
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# Тестовая и контрольная выборки
X_test, X_control, y_test, y_control = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp)
print("\nРазмеры выборок:")
print(f"Обучающая выборка: {X_train.shape}")
print(f"Тестовая выборка: {X_test.shape}")
print(f"Контрольная выборка: {X_control.shape}")
import seaborn as sns
import matplotlib.pyplot as plt
# Подсчет количества объектов каждого класса
class_counts = y.value_counts()
print(class_counts)
# Визуализация
sns.barplot(x=class_counts.index, y=class_counts.values)
plt.title("Распределение классов (stroke)")
plt.xlabel("Класс")
plt.ylabel("Количество")
plt.show()
Напишем функцию и сделаем аугментацию данных
from imblearn.over_sampling import RandomOverSampler
from imblearn.under_sampling import RandomUnderSampler
def over_under_sampling(x_selection, y_selection):
# сначала увеличение меньшинства
oversampler = RandomOverSampler(sampling_strategy=0.5, random_state=42)
x_over, y_over = oversampler.fit_resample(x_selection, y_selection)
print("\nПосле оверсемплинга\n")
print(y_over.value_counts())
# потом уменьшение большинства
undersampler = RandomUnderSampler(sampling_strategy=1.0, random_state=42)
x_balanced, y_balanced = undersampler.fit_resample(x_over, y_over)
print("\nПосле балансировки данных (андерсемплинга)\n")
print(y_balanced.value_counts())
plt.pie(
y_balanced.value_counts(),
labels=class_counts.index, # Метки классов (0 и 1)
autopct='%1.1f%%', # Отображение процентов
colors=['lightgreen', 'lightcoral'], # Цвета для классов
startangle=45, # Поворот диаграммы
explode=(0, 0.05) # Небольшое смещение для класса 1
)
plt.title("Распределение классов (stroke)")
plt.show()
print("Данные ДО аугментации в ОБУЧАЮЩЕЙ ВЫБОРКЕ (60-80% данных)\n")
print(y_train.value_counts())
over_under_sampling(X_train, y_train)
print("Данные ДО аугментации в ТЕСТОВОЙ ВЫБОРКЕ (10-20% данных)\n")
print(y_test.value_counts())
over_under_sampling(X_test, y_test)
Теперь можно и к конструированию признаков приступить) данные ведь сбалансированы (в выборках)
Унитарное кодирование категориальных признаков
Применяем к категориальным (НЕ числовым) признакам: 'gender', 'ever_married', 'work_type', 'Residence_type', 'smoking_status'
# One-Hot Encoding
categorical_columns = ['gender', 'ever_married', 'work_type', 'Residence_type', 'smoking_status']
X_encoded = pd.get_dummies(X_train, columns=categorical_columns, drop_first=True)
print("Данные после унитарного кодирования:")
X_encoded.head()
Дискретизация числовых признаков
Числовые признаки, такие как 'age', 'avg_glucose_level', 'bmi', можно разделить на категории (биннинг).
X_encoded['age_bins'] = pd.cut(X_encoded['age'], bins=[0, 18, 30, 50, 80], labels=['ребенок', 'молодой', 'средний', 'пожилой'])
X_encoded['bmi_bins'] = pd.cut(X_encoded['bmi'], bins=[0, 18.5, 25, 30, 50], labels=['низкий', 'норма', 'избыток', 'ожирение'])
print("Данные после дискретизации:")
X_encoded[['age_bins', 'bmi_bins']].head(10)
Ручной синтез новых признаков
X_encoded['age_glucose_index'] = X_encoded['age'] * X_encoded['avg_glucose_level']
X_encoded['bmi_glucose_ratio'] = X_encoded['bmi'] / X_encoded['avg_glucose_level']
print("Данные после синтеза новых признаков:")
X_encoded[['age_glucose_index', 'bmi_glucose_ratio']].head(10)
Масштабирование признаков
Применяем нормализацию (для сжатия в диапазон [0, 1]) и стандартизацию (для приведения к среднему 0 и стандартному отклонению 1)
import pandas as pd
from sklearn.preprocessing import MinMaxScaler, StandardScaler
scaler = MinMaxScaler()
standardizer = StandardScaler()
# Нормализация
X_encoded[['age', 'avg_glucose_level', 'bmi']] = scaler.fit_transform(X_encoded[['age', 'avg_glucose_level', 'bmi']])
print("Данные после нормализации:\n", X_encoded.head(10))
# # Стандартизация
# X_encoded[['age', 'avg_glucose_level', 'bmi']] = standardizer.fit_transform(X_encoded[['age', 'avg_glucose_level', 'bmi']])
# print("Данные после стандартизации:\n", X_encoded.head(10))
Конструирование признаков с применением фреймворка Featuretools
import featuretools as ft
print("Столбцы в data:", X_encoded.columns.tolist())
print(X_encoded.isnull().sum())
# Создание EntitySet (основная структура для Featuretools)
entity = ft.EntitySet(id="stroke_prediction")
entity = entity.add_dataframe(
dataframe_name="data",
dataframe=X_encoded,
index="id",
)
# Генерация новых признаков
feature_matrix, feature_defs = ft.dfs(
entityset=entity,
target_dataframe_name="data", # Основная таблица
max_depth=2 # Уровень вложенности
)
print("Сгенерированные признаки:")
print(feature_matrix.head())
# Сохранение результатов
feature_matrix.to_csv("./csv/generated_features.csv", index=False)
feature_matrix
Самое время оценить качество работы модели
import time
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, f1_score, confusion_matrix, classification_report
# Разделение данных на обучающую и тестовую выборки
X = data.drop(columns=['id', 'stroke']) # Признаки
y = data['stroke'] # Целевая переменная
# Преобразование категориальных признаков с помощью One-Hot Encoding
categorical_columns = ['gender', 'ever_married', 'work_type', 'Residence_type', 'smoking_status']
X = pd.get_dummies(X, columns=categorical_columns, drop_first=True)
# Заполнение пропущенных значений (например, медианой для числовых данных)
X.fillna(X.median(), inplace=True)
# Разделение данных на обучающую и тестовую выборки
# Обучающая выборка
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# Тестовая и контрольная выборки
X_test, X_control, y_test, y_control = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp)
# Обучение модели
model = RandomForestClassifier(random_state=42)
# Начинаем отсчет времени
start_time = time.time()
model.fit(X_train, y_train)
# Время обучения модели
train_time = time.time() - start_time
# Предсказания и оценка модели
y_pred = model.predict(X_test)
y_pred_proba = model.predict_proba(X_test)[:, 1] # Вероятности для ROC-AUC
# Метрики
roc_auc = roc_auc_score(y_test, y_pred_proba)
f1 = f1_score(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred)
class_report = classification_report(y_test, y_pred)
# Вывод результатов
print(f'Время обучения модели: {train_time:.2f} секунд')
print(f'ROC-AUC: {roc_auc:.2f}')
print(f'F1-Score: {f1:.2f}')
print('Матрица ошибок:')
print(conf_matrix)
print('Отчет по классификации:')
print(class_report)
# Визуализация матрицы ошибок
plt.figure(figsize=(7, 7))
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues', xticklabels=['Нет инсульта', 'Инсульт'], yticklabels=['Нет инсульта', 'Инсульт'])
plt.title('Матрица ошибок')
plt.xlabel('Предсказанный класс')
plt.ylabel('Истинный класс')
plt.show()
plt.figure(figsize=(10, 6))
plt.scatter(y_test, y_pred, alpha=0.5, color='blue', label='Прогнозы модели')
plt.plot([0, 1], [0, 1], 'k--', lw=2, label='Идеальное совпадение')
plt.xlabel('Фактический статус инсульта')
plt.ylabel('Прогнозируемый статус инсульта')
plt.title('Фактический статус инсульта по сравнению с прогнозируемым')
plt.legend()
plt.show()
в общем, вышло так, что пока что моя модель может предсказать ОТСУТСТВИЕ инсульта с высокой точностью, но вообще не может предсказать его наличие... целей пока не достигаем, задачи не решаем(