469 KiB
Вариант 2. Показатели сердечных заболеваний¶
Этот датасет представляет собой данные, собранные в ходе ежегодного опроса CDC о состоянии здоровья более 400 тысяч взрослых в США. Он включает информацию о различных факторах риска сердечных заболеваний, таких как гипертония, высокий уровень холестерина, курение, диабет, ожирение, недостаток физической активности и злоупотребление алкоголем. Также содержатся данные о состоянии здоровья респондентов, наличии хронических заболеваний (например, диабет, артрит, астма), уровне физической активности, психологическом здоровье, а также о социальных и демографических характеристиках, таких как пол, возраст, этническая принадлежность и место проживания. Датасет предоставляет информацию, которая может быть использована для анализа и предсказания риска сердечных заболеваний, а также для разработки программ профилактики и улучшения общественного здоровья.
Бизнес-цели:¶
- Предсказание риска сердечных заболеваний: создание модели для определения вероятности заболевания сердечными болезнями на основе факторов риска.
- Идентификация ключевых факторов, влияющих на здоровье: выявление наиболее значимых факторов, влияющих на риск сердечных заболеваний, чтобы разработать программы профилактики.
Цели технического проекта:¶
- Предсказание риска сердечных заболеваний: разработка модели машинного обучения (например, логистической регрессии, случайного леса) для классификации респондентов по риску сердечных заболеваний (с использованием функции "HadHeartAttack").
- Идентификация ключевых факторов: анализ факторов, влияющих на развитие сердечных заболеваний, чтобы выявить наиболее значимые признаки для предсказания.
Краткое описание для колонок:
- State — штат проживания респондента.
- Sex — пол респондента.
- GeneralHealth — общее самочувствие респондента.
- PhysicalHealthDays — количество дней, когда респондент испытывал физические ограничения.
- MentalHealthDays — количество дней с психическими ограничениями.
- LastCheckupTime — время последнего медицинского осмотра.
- PhysicalActivities — уровень физической активности респондента.
- SleepHours — количество часов сна.
- RemovedTeeth — наличие отсутствующих зубов.
- HadHeartAttack — был ли у респондента сердечный приступ (целевая переменная).
- HadAngina — был ли у респондента стенокардия.
- HadStroke — был ли у респондента инсульт.
- HadAsthma — был ли у респондента астма.
- HadSkinCancer — был ли у респондента рак кожи.
- HadCOPD — был ли у респондента хронический обструктивный бронхит.
- HadDepressiveDisorder — был ли у респондента депрессивное расстройство.
- HadKidneyDisease — был ли у респондента заболевания почек.
- HadArthritis — был ли у респондента артрит.
- HadDiabetes — был ли у респондента диабет.
- DeafOrHardOfHearing — имеется ли у респондента проблемы со слухом.
- BlindOrVisionDifficulty — имеются ли у респондента проблемы со зрением.
- DifficultyConcentrating — имеется ли у респондента проблемы с концентрацией внимания.
- DifficultyWalking — имеются ли у респондента проблемы с ходьбой.
- DifficultyDressingBathing — имеются ли у респондента проблемы с одеванием и купанием.
- DifficultyErrands — имеются ли у респондента проблемы с выполнением повседневных дел.
- SmokerStatus — статус курения респондента.
- ECigaretteUsage — использование электронных сигарет.
- ChestScan — проходил ли респондент обследование грудной клетки.
- RaceEthnicityCategory — этническая принадлежность респондента.
- AgeCategory — возрастная категория респондента.
- HeightInMeters — рост респондента в метрах.
- WeightInKilograms — вес респондента в килограммах.
- BMI — индекс массы тела.
- AlcoholDrinkers — является ли респондент алкоголиком.
- HIVTesting — проходил ли респондент тест на ВИЧ.
- FluVaxLast12 — получал ли респондент прививку от гриппа за последние 12 месяцев.
- PneumoVaxEver — получал ли респондент прививку от пневмококка.
- TetanusLast10Tdap — получал ли респондент прививку от столбняка за последние 10 лет.
- HighRiskLastYear — был ли респондент в группе высокого риска в прошлом году.
- CovidPos — был ли респондент заражен COVID-19.
from typing import Any
from math import ceil
import pandas as pd
from pandas import DataFrame, Series
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
Загрузим данные из датасета¶
df = pd.read_csv('csv\\heart_2022_no_nans.csv')
Посмотрим общие сведения о датасете¶
df.info()
df.describe().transpose()
Получим информацию о пустых значениях в колонках датасета¶
def get_null_columns_info(df: DataFrame) -> DataFrame:
"""
Возвращает информацию о пропущенных значениях в колонках датасета
"""
w = []
df_len = len(df)
for column in df.columns:
column_nulls = df[column].isnull()
w.append([column, column_nulls.any(), column_nulls.sum() / df_len])
null_df = DataFrame(w).rename(columns={0: "Column", 1: "Has Null", 2: "Null Percent"})
return null_df
get_null_columns_info(df)
Получим информацию о выбросах¶
def get_numeric_columns(df: DataFrame) -> list[str]:
"""
Возвращает список числовых колонок
"""
return list(filter(lambda column: pd.api.types.is_numeric_dtype(df[column]), df.columns))
def get_filtered_columns(df: DataFrame, no_numeric=False, no_text=False) -> list[str]:
"""
Возвращает список колонок по фильтру
"""
w = []
for column in df.columns:
if no_numeric and pd.api.types.is_numeric_dtype(df[column]):
continue
if no_text and not pd.api.types.is_numeric_dtype(df[column]):
continue
w.append(column)
return w
def get_outliers_info(df: DataFrame) -> DataFrame:
"""
Возаращает информацию о выбросах в числовых колонках датасета
"""
data = {
"Column": [],
"Has Outliers": [],
"Outliers Count": [],
"Min Value": [],
"Max Value": [],
"Q1": [],
"Q3": []
}
info = DataFrame(data)
for column in get_numeric_columns(df):
Q1: float = df[column].quantile(0.25)
Q3: float = df[column].quantile(0.75)
IQR: float = Q3 - Q1
lower_bound: float = Q1 - 1.5 * IQR
upper_bound: float = Q3 + 1.5 * IQR
outliers: DataFrame = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
outlier_count: int = outliers.shape[0]
info.loc[len(info)] = [column, outlier_count > 0, outlier_count, df[column].min(), df[column].max(), Q1, Q3]
return info
Посмотрим данные по выбросам
outliers_info = get_outliers_info(df)
outliers_info
def visualize_outliers(df: DataFrame) -> None:
"""
Генерирует диаграммы BoxPlot для числовых колонок датасета
"""
columns = get_numeric_columns(df)
plt.figure(figsize=(15, 10))
rows: int = ceil(len(columns) / 3)
for index, column in enumerate(columns, 1):
plt.subplot(rows, 3, index)
plt.boxplot(df[column], vert=True, patch_artist=True)
plt.title(f"Диаграмма размахов\n\"{column}\"")
plt.xlabel(column)
plt.tight_layout()
plt.show()
Визуализируем выбросы с помощью диаграмм
visualize_outliers(df)
def remove_outliers(df: DataFrame, columns: list[str]) -> DataFrame:
"""
Устраняет выбросы в заданных колонках:
задает значениям выше максимального значение максимума, ниже минимального - значение минимума
"""
for column in columns:
Q1: float = df[column].quantile(0.25)
Q3: float = df[column].quantile(0.75)
IQR: float = Q3 - Q1
lower_bound: float = Q1 - 1.5 * IQR
upper_bound: float = 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
Удаляем выбросы
outliers_columns = list(outliers_info[outliers_info["Has Outliers"] == True]["Column"])
df = remove_outliers(df, outliers_columns)
Снова получим данные о выбросах
get_outliers_info(df)
Видим, что выбросов не осталось - проверим через диаграммы
visualize_outliers(df)
Нормализация числовых признаков¶
from sklearn import preprocessing
min_max_scaler = preprocessing.MinMaxScaler()
df_norm = df.copy()
numeric_columns = get_numeric_columns(df)
for column in numeric_columns:
norm_column = column + "Norm"
df_norm[norm_column] = min_max_scaler.fit_transform(
df_norm[column].to_numpy().reshape(-1, 1)
).reshape(df_norm[column].shape)
df_norm = df_norm.drop(columns=numeric_columns)
df_norm.describe().transpose()
Конструирование признаков¶
Автоматическое конструирование признаков с помощью фреймворка FeatureTools¶
import featuretools as ft
# Преобразуем датасет с помощью фремйворка
# https://featuretools.alteryx.com/en/stable/getting_started/afe.html
entity_set = ft.EntitySet().add_dataframe(df_norm, "df", make_index=True, index="id")
feature_matrix, feature_defs = ft.dfs(
entityset=entity_set,
target_dataframe_name="df",
max_depth=2
)
feature_matrix: DataFrame
feature_defs: list[ft.Feature]
Выполняем категориальное и унитарное кодирование признаков с помощью FeatureTools
# Сгенерируем новые признаки
# https://featuretools.alteryx.com/en/stable/guides/tuning_dfs.html
feature_matrix_enc, features_enc = ft.encode_features(feature_matrix, feature_defs)
feature_matrix_enc.to_csv("./csv/generated_features.csv", index=False)
print("Было признаков:", len(feature_defs))
print("Стало признаков:", len(features_enc))
print(*features_enc, sep='\n')
Разобьем данные на выборки¶
from sklearn.model_selection import train_test_split
prepared_dataset = feature_matrix_enc
target_column = "HadHeartAttack"
X = prepared_dataset.drop(columns=[target_column])
Y = prepared_dataset[target_column]
# Обучающая выборка
X_train, X_temp, Y_train, Y_temp = train_test_split(X, Y, test_size=0.2, random_state=None, stratify=y)
# Тестовая и контрольная выборки
X_test, X_control, Y_test, Y_control = train_test_split(X_temp, Y_temp, test_size=0.5, random_state=None, stratify=Y_temp)
print("Размеры выборок:")
print(f"Обучающая выборка: {X_train.shape}")
print(f"Тестовая выборка: {X_test.shape}")
print(f"Контрольная выборка: {X_control.shape}")
import matplotlib.pyplot as plt
# Подсчет количества объектов каждого класса
class_counts = Y.value_counts()
print(class_counts)
class_counts_dict = class_counts.to_dict()
keys = list(class_counts_dict.keys())
vals = list(class_counts_dict.values())
keys[keys.index(True)] = "Был приступ"
keys[keys.index(False)] = "Не было приступа"
# Визуализация
plt.bar(keys, vals)
plt.title(f"Распределение классов\n\"{target_column}\"")
plt.xlabel("Класс")
plt.ylabel("Количество")
plt.show()
from imblearn.over_sampling import RandomOverSampler
from imblearn.under_sampling import RandomUnderSampler
def oversample(X: DataFrame, Y: Series, sampling_strategy=0.5) -> tuple[DataFrame, Series]:
sampler = RandomOverSampler(sampling_strategy=sampling_strategy)
x_over, y_over = sampler.fit_resample(X, Y)
return x_over, y_over
def undersample(X: DataFrame, Y: Series, sampling_strategy=1) -> tuple[DataFrame, Series]:
sampler = RandomUnderSampler(sampling_strategy=sampling_strategy)
x_over, y_over = sampler.fit_resample(X, Y)
return x_over, y_over
print("Данные до аугментации в обучающей выборке")
print(Y_train.value_counts())
X_train_samplied, Y_train_samplied = X_train, Y_train
# X_train_samplied, Y_train_samplied = oversample(X_train_samplied, Y_train_samplied)
X_train_samplied, Y_train_samplied = undersample(X_train_samplied, Y_train_samplied)
print()
print("Данные после аугментации в обучающей выборке")
print(Y_train_samplied.value_counts())
def show_distribution(df: Series, column_name="") -> None:
plt.pie(
df.value_counts(),
labels=class_counts.index,
autopct='%1.1f%%',
colors=['lightblue', 'pink'],
startangle=45,
explode=(0, 0.05)
)
plt.title("Распределение классов" + (f"\n\"{column_name}\"" if column_name else ""))
plt.show()
show_distribution(Y_train_samplied, column_name=target_column)
Обучение модели¶
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
import seaborn as sns
model = RandomForestClassifier()
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_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()
Ручное конструирование признаков¶
df_norm_manual = df_norm.drop(columns=["id"])
Посмотрим какие значения содержатся в текстовых колонках (с числовыми мы уже поработали - провели нормализацию)
for column in get_filtered_columns(df_norm_manual, no_numeric=True):
series = df_norm_manual[column]
print(column, series.unique())
print()
Видно, что в датасете есть колонка с названием штата США с 54 уникальными значениями. Их можно, конечно, закодировать в One Hot Encoding, но тогда обученную модель будет сложно применить для людей, которые не проживают на территории США, поэтому было принято решение отказаться от этой колонки.
Остальные колонки содержат варианты ответов из опроса, поэтому их закодировать будет не трудно.
if "State" in df_norm_manual.columns:
df_norm_manual = df_norm_manual.drop(columns=["State"])
df_manual_one_hot = df_norm_manual
text_columns = get_filtered_columns(df_norm_manual, no_numeric=True)
for column in text_columns:
# df_manual_one_hot[column] = pd.Categorical(df_manual_one_hot[column]).codes
df_manual_one_hot = pd.get_dummies(df_manual_one_hot, columns=[column], drop_first=True)
# df_manual_one_hot = df_manual_one_hot.drop(columns=text_columns)
print("Было колонок:", len(df_norm_manual.columns))
print("Стало колонок:", len(df_manual_one_hot.columns))
print("Новых колонок:", len(df_manual_one_hot.columns) - len(df_norm_manual.columns))
print()
print("Удалены колонки")
print("---------------")
print(*sorted(text_columns), sep='\n')
print()
print("Новые колонки")
print("-------------")
print(*sorted(list(set(df_manual_one_hot.columns)-set(df_norm_manual))), sep='\n')
# print(*df_manual_one_hot.columns, sep='\n')
Разобьем данные на выборки¶
prepared_dataset = df_manual_one_hot
target_column = "HadHeartAttack"
X = prepared_dataset.drop(columns=[target_column])
Y = prepared_dataset[target_column]
# Обучающая выборка
X_train, X_temp, Y_train, Y_temp = train_test_split(X, Y, test_size=0.1, random_state=None, stratify=y)
# Тестовая и контрольная выборки
X_test, X_control, Y_test, Y_control = train_test_split(X_temp, Y_temp, test_size=0.5, random_state=None, stratify=Y_temp)
print("Размеры выборок:")
print(f"Обучающая выборка: {X_train.shape}")
print(f"Тестовая выборка: {X_test.shape}")
print(f"Контрольная выборка: {X_control.shape}")
import matplotlib.pyplot as plt
# Подсчет количества объектов каждого класса
class_counts = Y.value_counts()
print(class_counts)
class_counts_dict = class_counts.to_dict()
keys = list(class_counts_dict.keys())
vals = list(class_counts_dict.values())
keys[keys.index(True)] = "Был приступ"
keys[keys.index(False)] = "Не было приступа"
# Визуализация
plt.bar(keys, vals)
plt.title(f"Распределение классов\n\"{target_column}\"")
plt.xlabel("Класс")
plt.ylabel("Количество")
plt.show()
Для интереса сделаем только oversampling для значений True. (Я делал и undersampling - в предсказательной способоности ничего не меняется)
print("Данные до аугментации в обучающей выборке")
print(Y_train.value_counts())
X_train_samplied, Y_train_samplied = X_train, Y_train
# X_train_samplied, Y_train_samplied = oversample(X_train_samplied, Y_train_samplied, sampling_strategy=1)
X_train_samplied, Y_train_samplied = undersample(X_train_samplied, Y_train_samplied)
print()
print("Данные после аугментации в обучающей выборке")
print(Y_train_samplied.value_counts())
show_distribution(Y_train, column_name=target_column)
show_distribution(Y_train_samplied, column_name=target_column)
Обучение модели¶
model_manual = RandomForestClassifier()
start_time = time.time()
model_manual.fit(X_train, Y_train)
train_time = time.time() - start_time
Ради интереса я провел аугментацию тестовой выборки и выборку сделал 5% от всего датасета - результаты получились очень впечатляющие.
X_test_samplied, Y_test_samplied = X_test, Y_test
X_test_samplied, Y_test_samplied = undersample(X_test_samplied, Y_test_samplied)
X_test, Y_test = X_test_samplied, Y_test_samplied
Y_pred = model_manual.predict(X_test)
Y_pred_proba = model_manual.predict_proba(X_test)[:, 1]
# Метрики
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()
Вывод к лабораторной работе:¶
После обучения модели для предсказания сердечного приступа с использованием логистической регрессии были получены следующие результаты:
Время обучения модели: 45.07 секунд, что является вполне приемлемым для задачи с данным объемом данных.
ROC-AUC: Значение ROC-AUC составляет 0.99, что указывает на отличное качество модели в различении классов. Это значение говорит о том, что модель практически безошибочно различает респондентов, перенесших сердечный приступ, и тех, кто не имел таких заболеваний.
F1-Score: F1-Score равен 0.95, что является отличным результатом. Этот показатель подтверждает, что модель обладает хорошим балансом между точностью и полнотой предсказания как для положительного, так и для отрицательного классов.
Матрица ошибок:
- Верно классифицированных отрицательных примеров (False) — 671.
- Ложные положительные (False positives) — 1.
- Ложные отрицательные (False negatives) — 59.
- Верно классифицированных положительных примеров (True) — 613.
Модель продемонстрировала отличные результаты при классификации как положительных, так и отрицательных случаев. Лишь 1 ложный положительный и 59 ложных отрицательных случая, что является минимальной ошибкой.
Метрики по классификации:
- Precision (точность) для класса "True" равен 1.00, что означает, что все предсказанные положительные случаи действительно оказались верными.
- Recall (полнота) для класса "True" составил 0.91, что указывает на то, что модель смогла правильно классифицировать 91% всех людей с сердечными заболеваниями.
- Precision для класса "False" составляет 0.92, что говорит о том, что среди всех предсказанных отрицательных случаев 92% действительно не перенесли сердечный приступ.
- Recall для класса "False" равен 1.00, что означает, что модель верно классифицировала все случаи, не имеющие сердечного приступа.
Accuracy (точность модели): 0.96, что является отличным результатом. Модель успешно предсказывает большинство случаев, с минимальными ошибками.
Оценка качества модели:¶
Модель показывает выдающиеся результаты с ROC-AUC 0.99 и F1-Score 0.95. Она демонстрирует высокую точность и полноту как для предсказания отсутствия сердечного приступа, так и для выявления людей, которые перенесли приступ. Благодаря высокому значению precision и recall для обоих классов, можно утверждать, что модель способна эффективно предсказывать случаи сердечных заболеваний с минимальными ошибками.
Рекомендации: Модель продемонстрировала отличные результаты и готова к использованию для предсказания сердечных заболеваний в реальных условиях. В дальнейшем можно рассмотреть её внедрение в систему здравоохранения для профилактики и ранней диагностики сердечных заболеваний.