MAI_PIbd-33_Volkov_N.A./lab3/lab3.ipynb
2024-11-08 21:48:40 +04:00

191 KiB
Raw Blame History

Вариант 4. Данные по инсультам

In [3]:
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("../data/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[3]:
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. Снижение затрат на лечение: Предупреждение инсультов у пациентов позволит снизить затраты на лечение и реабилитацию. Это также поможет улучшить качество медицинских услуг и повысить удовлетворенность пациентов.

3. Повышение эффективности профилактики: Выявление факторов риска инсульта на ранней стадии может способствовать более эффективному проведению профилактических мероприятий.

Цели технического проекта:

1. Создание и обучение модели машинного обучения: Разработка модели, способной предсказать вероятность инсульта на основе данных о пациентах (например, возраст, уровень глюкозы, наличие сердечно-сосудистых заболеваний, тип работы, индекс массы тела и т.д.).

2. Анализ и обработка данных: Провести предобработку данных (очистка, заполнение пропущенных значений, кодирование категориальных признаков), чтобы улучшить качество и надежность модели.

3. Оценка модели: Использовать метрики, такие как точность, полнота и F1-мера, чтобы оценить эффективность модели и минимизировать риск ложных положительных и ложных отрицательных предсказаний.

In [5]:
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 [6]:
df["bmi"] = df["bmi"].fillna(df["bmi"].median())

missing_values = df.isnull().sum()

print("Количество пустых значений в каждом столбце после замены:")
print(missing_values)
Количество пустых значений в каждом столбце после замены:
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 [7]:
df = df.drop('id', axis = 1)
print(df.columns)
Index(['gender', 'age', 'hypertension', 'heart_disease', 'ever_married',
       'work_type', 'Residence_type', 'avg_glucose_level', 'bmi',
       'smoking_status', 'stroke'],
      dtype='object')

Создаем выборки

In [8]:
# Разделим данные на признак (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, 10)
Размер контрольной выборки:  (1074, 10)
Размер тестовой выборки:  (1533, 10)

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

In [9]:
from locale import normalize


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.951658
1    0.048342
Name: proportion, dtype: float64

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

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

Заметим, что выборки не сбалансированы. Для балансировки будем использовать RandomOverSampler

In [10]:
randoversamp = RandomOverSampler(random_state=42)

# Применение RandomOverSampler для балансировки выборок
X_train_resampled, y_train_resampled = randoversamp.fit_resample(X_train, y_train)
X_val_resampled, y_val_resampled = randoversamp.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.953033
1    0.046967
Name: proportion, dtype: float64
No description has been provided for this image

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

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

In [11]:
# Определение категориальных признаков
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  hypertension  heart_disease  avg_glucose_level   bmi  gender_Male  \
0  31.0             0              0              80.88  29.3        False   
1  63.0             1              0              81.54  24.2        False   
2  33.0             0              0              86.97  42.2        False   
3   7.0             0              0              61.42  20.8        False   
4  62.0             0              0             163.17  25.6        False   

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

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

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

   smoking_status_smokes  
0                  False  
1                  False  
2                  False  
3                  False  
4                  False  

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

In [12]:
# Определение числовых признаков для дискретизации
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())
   hypertension  heart_disease  avg_glucose_level   bmi  gender_Male  \
0             0              0              80.88  29.3        False   
1             1              0              81.54  24.2        False   
2             0              0              86.97  42.2        False   
3             0              0              61.42  20.8        False   
4             0              0             163.17  25.6        False   

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

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

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

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

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

In [13]:
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())
   hypertension  heart_disease  avg_glucose_level   bmi  gender_Male  \
0             0              0              80.88  29.3        False   
1             1              0              81.54  24.2        False   
2             0              0              86.97  42.2        False   
3             0              0              61.42  20.8        False   
4             0              0             163.17  25.6        False   

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

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

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

   smoking_status_smokes      age_bin  glucose_age_deviation  
0                  False  middle-aged             -22.997954  
1                  False          old             -54.147764  
2                  False  middle-aged             -16.907954  
3                  False        young             -32.619531  
4                  False          old              27.482236  

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

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

In [14]:
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())
   hypertension  heart_disease  avg_glucose_level       bmi  gender_Male  \
0             0              0          -0.696288 -0.031658        False   
1             1              0          -0.684615 -0.785297        False   
2             0              0          -0.588575  1.874608        False   
3             0              0          -1.040476 -1.287724        False   
4             0              0           0.759172 -0.578416        False   

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

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

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

   smoking_status_smokes      age_bin  glucose_age_deviation  
0                  False  middle-aged              -0.428019  
1                  False          old              -1.007754  
2                  False  middle-aged              -0.314677  
3                  False        young              -0.607088  
4                  False          old               0.511477  

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

In [15]:
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())
       hypertension  heart_disease  avg_glucose_level       bmi  gender_Male  \
index                                                                          
0                 0              0          -0.696288 -0.031658        False   
1                 1              0          -0.684615 -0.785297        False   
2                 0              0          -0.588575  1.874608        False   
3                 0              0          -1.040476 -1.287724        False   
4                 0              0           0.759172 -0.578416        False   

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

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

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

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

       glucose_age_deviation  
index                         
0                  -0.428019  
1                  -1.007754  
2                  -0.314677  
3                  -0.607088  
4                   0.511477  
c:\Users\bocchanskyy\source\repos\MAI_PIbd-33_Volkov_NA\.venv\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(

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

  1. Предсказательная способность (для задачи классификации)
  • Метрики: Accuracy, Precision, Recall, F1-Score, ROC AUC
  • Методы: Обучение модели на обучающей выборке и оценка на валидационной и тестовой выборках.
  1. Вычислительная эффективность
  • Методы: Измерение времени, затраченного на генерацию признаков и обучение модели.
  1. Надежность
  • Методы: Кросс-валидация и анализ чувствительности модели к изменениям в данных.
  1. Корреляция
  • Методы: Анализ корреляционной матрицы признаков и исключение мультиколлинеарных признаков.
  1. Логическая согласованность
  • Методы: Проверка логической связи признаков с целевой переменной и интерпретация результатов модели.
In [16]:
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} секунд")
Время обучения модели: 0.33 секунд
In [17]:
# Получение важности признаков
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)
Feature Importance:
                           feature    importance
3                              bmi  1.937437e-01
2                avg_glucose_level  1.902965e-01
15           glucose_age_deviation  1.742016e-01
17                     age_bin_old  1.676844e-01
0                     hypertension  3.892436e-02
6                 ever_married_Yes  3.224085e-02
4                      gender_Male  2.826243e-02
16             age_bin_middle-aged  2.826115e-02
11            Residence_type_Urban  2.391783e-02
13     smoking_status_never smoked  2.307375e-02
8                work_type_Private  2.125517e-02
9          work_type_Self-employed  1.917397e-02
1                    heart_disease  1.690222e-02
12  smoking_status_formerly smoked  1.681537e-02
14           smoking_status_smokes  1.665569e-02
10              work_type_children  8.476756e-03
7           work_type_Never_worked  1.141679e-04
5                     gender_Other  5.572087e-08
In [18]:
# Предсказание и оценка
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}")
Accuracy: 0.9406392694063926
Precision: 0.047619047619047616
Recall: 0.013888888888888888
F1 Score: 0.021505376344086023
ROC AUC: 0.5000998174766141
Cross-validated Accuracy: 0.9930740606840847
No description has been provided for this image
Train Accuracy: 1.0
Train Precision: 1.0
Train Recall: 1.0
Train F1 Score: 1.0
Train ROC AUC: 1.0