2024-11-01 23:33:34 +04:00

115 KiB
Raw Blame History

Данные по инсультам

Выведем информацию о столбцах датасета:

In [441]:
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
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
import time
from sklearn.metrics import root_mean_squared_error, r2_score, mean_absolute_error
from sklearn.ensemble import RandomForestRegressor

df = pd.read_csv("..//..//static//csv//healthcare-dataset-stroke-data.csv")

print(df.columns)
df.head()
Index(['id', 'gender', 'age', 'hypertension', 'heart_disease', 'ever_married',
       'work_type', 'Residence_type', 'avg_glucose_level', 'bmi',
       'smoking_status', 'stroke'],
      dtype='object')
Out[441]:
id gender age hypertension heart_disease ever_married work_type Residence_type avg_glucose_level bmi smoking_status stroke
0 9046 Male 67.0 0 1 Yes Private Urban 228.69 36.6 formerly smoked 1
1 51676 Female 61.0 0 0 Yes Self-employed Rural 202.21 NaN never smoked 1
2 31112 Male 80.0 0 1 Yes Private Rural 105.92 32.5 never smoked 1
3 60182 Female 49.0 0 0 Yes Private Urban 171.23 34.4 smokes 1
4 1665 Female 79.0 1 0 Yes Self-employed Rural 174.12 24.0 never smoked 1

Определим бизнес цели и цели технического проекта.

  1. Улучшение диагностики и профилактики инсульта.
    • Бизнес-цель: повышение точности прогнозирования риска инсульта среди пациентов для более раннего лечебного вмешательства. Определение основных факторов риска для более целенаправленного подхода в медицинском обслуживании.
    • Цель технического проекта: разработка статистической модели, которая решает задачу классификации и предсказывает возможность возникновения инсульта у пациентов на основе имеющихся данных (возраст, гипертония, заболевания сердца и пр.), с целью выявления групп риска. Внедрение этой модели в систему поддержки принятия медицинских решений для врачей.
  2. Снижение расходов на лечение инсультов.
    • Бизнес-цель: снижение затрат на лечение инсульта путем более эффективного распределения медицинских ресурсов и направленных профилактических мер.
    • Цель технического проекта: создание системы оценки индивидуального риска инсульта для пациентов, что позволит медучреждениям проводить профилактические меры среди целевых групп, сокращая расходы на лечение.

И теперь проверим датасет на пустые значения:

In [442]:
# Количество пустых значений признаков
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}")
id                     0
gender                 0
age                    0
hypertension           0
heart_disease          0
ever_married           0
work_type              0
Residence_type         0
avg_glucose_level      0
bmi                  201
smoking_status         0
stroke                 0
dtype: int64

id                   False
gender               False
age                  False
hypertension         False
heart_disease        False
ever_married         False
work_type            False
Residence_type       False
avg_glucose_level    False
bmi                   True
smoking_status       False
stroke               False
dtype: bool

bmi процент пустых значений: %3.93

В столбце bmi можно заметить пустые значение. Заменим их на медиану:

In [443]:
# Замена значений
df["bmi"] = df["bmi"].fillna(df["bmi"].median())

# Проверка на пропущенные значения после замены
missing_values_after_drop = df.isnull().sum()

# Вывод результатов после замены
print("\nКоличество пустых значений в каждом столбце после замены:")
print(missing_values_after_drop)
Количество пустых значений в каждом столбце после замены:
id                   0
gender               0
age                  0
hypertension         0
heart_disease        0
ever_married         0
work_type            0
Residence_type       0
avg_glucose_level    0
bmi                  0
smoking_status       0
stroke               0
dtype: int64

Можно перейти к созданию выборок

In [444]:
# Разделение данных на признаки (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)
Размер обучающей выборки: (2503, 11)
Размер контрольной выборки: (1074, 11)
Размер тестовой выборки: (1533, 11)

Оценим сбалансированность выборок:

In [445]:
# Функция для анализа сбалансированности
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')
Распределение классов в обучающей выборке:
stroke
0    0.955653
1    0.044347
Name: proportion, dtype: float64

Распределение классов в контрольной выборке:
stroke
0    0.954376
1    0.045624
Name: proportion, dtype: float64

Распределение классов в тестовой выборке:
stroke
0    0.941944
1    0.058056
Name: proportion, dtype: float64
No description has been provided for this image

Легко заметить, что выборки несбалансированны. Необходимо сбалансировать обучающую и контрольную выборки, чтобы получить лучшие результаты при обучении модели. Для балансировки применим RandomOverSampler:

In [446]:
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')
Распределение классов в обучающей выборке:
stroke
0    0.5
1    0.5
Name: proportion, dtype: float64

Распределение классов в контрольной выборке:
stroke
0    0.5
1    0.5
Name: proportion, dtype: float64

Распределение классов в тестовой выборке:
stroke
0    0.941944
1    0.058056
Name: proportion, dtype: float64
No description has been provided for this image

Выборки сбалансированы.

Перейдем к конструированию признаков

Для начала применим унитарное кодирование категориальных признаков (one-hot encoding), переведя их в бинарные вектора:

In [447]:
# Определение категориальных признаков
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())
      id   age  hypertension  heart_disease  avg_glucose_level   bmi  \
0  16605  57.0             0              0             106.24  32.3   
1  12015  14.0             0              0              99.87  25.2   
2  26474  44.0             0              0              97.16  33.1   
3  31143  22.0             0              0             107.52  41.6   
4   2447  63.0             0              0              85.04  29.7   

   gender_Male  gender_Other  ever_married_Yes  work_type_Never_worked  \
0         True         False              True                   False   
1         True         False             False                   False   
2        False         False              True                   False   
3        False         False             False                   False   
4        False         False              True                   False   

   work_type_Private  work_type_Self-employed  work_type_children  \
0               True                    False               False   
1              False                    False                True   
2              False                    False               False   
3               True                    False               False   
4               True                    False               False   

   Residence_type_Urban  smoking_status_formerly smoked  \
0                  True                           False   
1                  True                           False   
2                  True                           False   
3                 False                           False   
4                  True                            True   

   smoking_status_never smoked  smoking_status_smokes  
0                         True                  False  
1                        False                  False  
2                        False                  False  
3                        False                  False  
4                        False                  False  

Далее к числовым признакам, а именно к колонке age, применим дискретизацию (позволяет преобразовать данные из числового представления в категориальное):

In [448]:
# Определение числовых признаков для дискретизации
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())
      id  hypertension  heart_disease  avg_glucose_level   bmi  gender_Male  \
0  16605             0              0             106.24  32.3         True   
1  12015             0              0              99.87  25.2         True   
2  26474             0              0              97.16  33.1        False   
3  31143             0              0             107.52  41.6        False   
4   2447             0              0              85.04  29.7        False   

   gender_Other  ever_married_Yes  work_type_Never_worked  work_type_Private  \
0         False              True                   False               True   
1         False             False                   False              False   
2         False              True                   False              False   
3         False             False                   False               True   
4         False              True                   False               True   

   work_type_Self-employed  work_type_children  Residence_type_Urban  \
0                    False               False                  True   
1                    False                True                  True   
2                    False               False                  True   
3                    False               False                 False   
4                    False               False                  True   

   smoking_status_formerly smoked  smoking_status_never smoked  \
0                           False                         True   
1                           False                        False   
2                           False                        False   
3                           False                        False   
4                            True                        False   

   smoking_status_smokes      age_bin  
0                  False          old  
1                  False        young  
2                  False  middle-aged  
3                  False        young  
4                  False          old  

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

In [449]:
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())
      id  hypertension  heart_disease  avg_glucose_level   bmi  gender_Male  \
0  16605             0              0             106.24  32.3         True   
1  12015             0              0              99.87  25.2         True   
2  26474             0              0              97.16  33.1        False   
3  31143             0              0             107.52  41.6        False   
4   2447             0              0              85.04  29.7        False   

   gender_Other  ever_married_Yes  work_type_Never_worked  work_type_Private  \
0         False              True                   False               True   
1         False             False                   False              False   
2         False              True                   False              False   
3         False             False                   False               True   
4         False              True                   False               True   

   work_type_Self-employed  work_type_children  Residence_type_Urban  \
0                    False               False                  True   
1                    False                True                  True   
2                    False               False                  True   
3                    False               False                 False   
4                    False               False                  True   

   smoking_status_formerly smoked  smoking_status_never smoked  \
0                           False                         True   
1                           False                        False   
2                           False                        False   
3                           False                        False   
4                            True                        False   

   smoking_status_smokes      age_bin  glucose_age_deviation  
0                  False          old             -27.642870  
1                  False        young               6.088032  
2                  False  middle-aged              -6.217053  
3                  False        young              13.738032  
4                  False          old             -48.842870  

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

In [450]:
# Пример масштабирования числовых признаков
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())
      id  hypertension  heart_disease  avg_glucose_level       bmi  \
0  16605             0              0          -0.244097  0.426328   
1  12015             0              0          -0.360110 -0.596170   
2  26474             0              0          -0.409465  0.541539   
3  31143             0              0          -0.220785  1.765656   
4   2447             0              0          -0.630199  0.051892   

   gender_Male  gender_Other  ever_married_Yes  work_type_Never_worked  \
0         True         False              True                   False   
1         True         False             False                   False   
2        False         False              True                   False   
3        False         False             False                   False   
4        False         False              True                   False   

   work_type_Private  work_type_Self-employed  work_type_children  \
0               True                    False               False   
1              False                    False                True   
2              False                    False               False   
3               True                    False               False   
4               True                    False               False   

   Residence_type_Urban  smoking_status_formerly smoked  \
0                  True                           False   
1                  True                           False   
2                  True                           False   
3                 False                           False   
4                  True                            True   

   smoking_status_never smoked  smoking_status_smokes      age_bin  \
0                         True                  False          old   
1                        False                  False        young   
2                        False                  False  middle-aged   
3                        False                  False        young   
4                        False                  False          old   

   glucose_age_deviation  
0              -0.528807  
1               0.116464  
2              -0.118932  
3               0.262808  
4              -0.934362  

И также попробуем сконструировать признаки, используя фреймворк Featuretools:

In [451]:
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())
          id  hypertension  heart_disease  avg_glucose_level       bmi  \
index                                                                    
0      16605             0              0          -0.244097  0.426328   
1      12015             0              0          -0.360110 -0.596170   
2      26474             0              0          -0.409465  0.541539   
3      31143             0              0          -0.220785  1.765656   
4       2447             0              0          -0.630199  0.051892   

       gender_Male  gender_Other  ever_married_Yes  work_type_Never_worked  \
index                                                                        
0             True         False              True                   False   
1             True         False             False                   False   
2            False         False              True                   False   
3            False         False             False                   False   
4            False         False              True                   False   

       work_type_Private  work_type_Self-employed  work_type_children  \
index                                                                   
0                   True                    False               False   
1                  False                    False                True   
2                  False                    False               False   
3                   True                    False               False   
4                   True                    False               False   

       Residence_type_Urban  smoking_status_formerly smoked  \
index                                                         
0                      True                           False   
1                      True                           False   
2                      True                           False   
3                     False                           False   
4                      True                            True   

       smoking_status_never smoked  smoking_status_smokes      age_bin  \
index                                                                    
0                             True                  False          old   
1                            False                  False        young   
2                            False                  False  middle-aged   
3                            False                  False        young   
4                            False                  False          old   

       glucose_age_deviation  
index                         
0                  -0.528807  
1                   0.116464  
2                  -0.118932  
3                   0.262808  
4                  -0.934362  
c:\Users\Ilya\Desktop\AIM\aimenv\Lib\site-packages\woodwork\type_sys\utils.py:33: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
  pd.to_datetime(

Оценим качество набора признаков.

Представим основные оценки качества наборов признаков:

  • Предсказательная способность Метрики: RMSE, MAE, R²

    Методы: Обучение модели на обучающей выборке и оценка на контрольной и тестовой выборках.

  • Скорость вычисления

    Методы: Измерение времени выполнения генерации признаков и обучения модели.

  • Надежность

    Методы: Кросс-валидация, анализ чувствительности модели к изменениям в данных.

  • Корреляция

    Методы: Анализ корреляционной матрицы признаков, удаление мультиколлинеарных признаков.

  • Цельность

    Методы: Проверка логической связи между признаками и целевой переменной, интерпретация результатов модели.

In [452]:
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 = LinearRegression()

# Начинаем отсчет времени
start_time = time.time()
model.fit(X_train_encoded, y_train_resampled)

# Время обучения модели
train_time = time.time() - start_time

# Предсказания и оценка модели и вычисляем среднеквадратичную ошибку
predictions = model.predict(X_val_encoded)
mse = root_mean_squared_error(y_val_resampled, predictions)

print(f'Время обучения модели: {train_time:.2f} секунд')
print(f'Среднеквадратичная ошибка: {mse:.2f}')
Время обучения модели: 0.01 секунд
Среднеквадратичная ошибка: 0.41
In [453]:
# Выбор модели
model = RandomForestRegressor(random_state=42)

# Обучение модели
model.fit(X_train_encoded, y_train_resampled)

# Предсказание и оценка
y_pred = model.predict(X_test_encoded)

rmse = root_mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
mae = mean_absolute_error(y_test, y_pred)

print()
print(f"RMSE: {rmse}")
print(f"R²: {r2}")
print(f"MAE: {mae} \n")

# Кросс-валидация
scores = cross_val_score(model, X_train_encoded, y_train_resampled, cv=5, scoring='neg_mean_squared_error')
rmse_cv = (-scores.mean())**0.5
print(f"Кросс-валидация RMSE: {rmse_cv} \n")

# Проверка на переобучение
y_train_pred = model.predict(X_train_encoded)

rmse_train = root_mean_squared_error(y_train_resampled, y_train_pred)
r2_train = r2_score(y_train_resampled, y_train_pred)
mae_train = mean_absolute_error(y_train_resampled, y_train_pred)

print(f"Train RMSE: {rmse_train}")
print(f"Train R²: {r2_train}")
print(f"Train MAE: {mae_train}")
print()
RMSE: 0.24109840514907446
R²: -0.06295721700021817
MAE: 0.10402478799739073 

Кросс-валидация RMSE: 0.1197518340742331 

Train RMSE: 0.037396456827854585
Train R²: 0.9944060200668896
Train MAE: 0.010727424749163881

Можно заметить, что модель хорошо подстроилась под тренировочные данные (Низкий Train RMSE и высокое значение Train R²). Однако высокий RMSE и отрицательный R² на тестовом наборе свидетельствуют о том, что модель не обобщила зависимости и плохо предсказывает новые данные, поэтому можно сделать вывод о том, что получившийся набор признаков, к сожалению, далек от идеала.