357 KiB
Вариант 2. Показатели сердечных заболеваний¶
Этот датасет представляет собой данные, собранные в ходе ежегодного опроса CDC о состоянии здоровья более 400 тысяч взрослых в США. Он включает информацию о различных факторах риска сердечных заболеваний, таких как гипертония, высокий уровень холестерина, курение, диабет, ожирение, недостаток физической активности и злоупотребление алкоголем. Также содержатся данные о состоянии здоровья респондентов, наличии хронических заболеваний (например, диабет, артрит, астма), уровне физической активности, психологическом здоровье, а также о социальных и демографических характеристиках, таких как пол, возраст, этническая принадлежность и место проживания. Датасет предоставляет информацию, которая может быть использована для анализа и предсказания риска сердечных заболеваний, а также для разработки программ профилактики и улучшения общественного здоровья.
Бизнес-цели:¶
- Предсказание риска сердечных заболеваний: создание модели для определения вероятности заболевания сердечными болезнями на основе факторов риска.
- Идентификация ключевых факторов, влияющих на здоровье: выявление наиболее значимых факторов, влияющих на риск сердечных заболеваний, чтобы разработать программы профилактики.
Цели технического проекта:¶
- Предсказание риска сердечных заболеваний: разработка модели машинного обучения (например, логистической регрессии, случайного леса) для классификации респондентов по риску сердечных заболеваний (с использованием функции "HadHeartAttack").
- Идентификация ключевых факторов: анализ факторов, влияющих на развитие сердечных заболеваний, чтобы выявить наиболее значимые признаки для предсказания.
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
from imblearn.over_sampling import ADASYN, SMOTE
from imblearn.under_sampling import RandomUnderSampler
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_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