diff --git a/arutunyan_dmitry_lab_3/FullParam.png b/arutunyan_dmitry_lab_3/FullParam.png new file mode 100644 index 0000000..1209694 Binary files /dev/null and b/arutunyan_dmitry_lab_3/FullParam.png differ diff --git a/arutunyan_dmitry_lab_3/ImpParam.png b/arutunyan_dmitry_lab_3/ImpParam.png new file mode 100644 index 0000000..7d0e704 Binary files /dev/null and b/arutunyan_dmitry_lab_3/ImpParam.png differ diff --git a/arutunyan_dmitry_lab_3/README.md b/arutunyan_dmitry_lab_3/README.md new file mode 100644 index 0000000..f4df9be --- /dev/null +++ b/arutunyan_dmitry_lab_3/README.md @@ -0,0 +1,170 @@ + +## Лабораторная работа 3. Вариант 4. +### Задание +Выполнить ранжирование признаков и решить с помощью библиотечной реализации дерева решений +задачу классификации на 99% данных из курсовой работы. Проверить +работу модели на оставшемся проценте, сделать вывод. + +Модель: + +- Дерево решений `DecisionTreeClassifier`. + +### Как запустить +Для запуска программы необходимо с помощью командной строки в корневой директории файлов прокета прописать: +``` +python main.py +``` +После этого в папке `static` сгенерируются 2 графика, по которым оценивается результат выполнения программы. + +### Используемые технологии +- Библиотека `numpy`, используемая для обработки массивов данных и вычислений +- Библиотека `pyplot`, используемая для построения графиков. +- Библиотека `pandas`, используемая для работы с данными для анализа scv формата. +- Библиотека `sklearn` - большой набор функционала для анализа данных. Из неё были использованы инструменты: + - `DecisionTreeClassifier` - инструмент работы с моделью "Дерево решений" + - `metrics` - набор инструменов для оценки моделей + - `MinMaxScaler` - инструмент масштабирования значений в заданный диапазон + +### Описание работы +#### Описание набора данных +Набор данных - набор для определения возможности наличия ССЗ заболеваний у челоека + +Названия столбцов набора данных и их описание: + + * HeartDisease - Имеет ли человек ССЗ (No / Yes), + * BMI - Индекс массы тела человека (float), + * Smoking - Выкурил ли человек хотя бы 5 пачек сигарет за всю жизнь (No / Yes), + * AlcoholDrinking - Сильно ли человек употребляет алкоголь (No / Yes), + * Stroke - Был ли у человека инсульт (No / Yes), + * PhysicalHealth - Сколько дней за последний месяц человек чувствовал себя плохо (0-30), + * MentalHealth - Сколько дней за последний месяц человек чувствовал себя удручённо (0-30), + * DiffWalking - Ииспытывает ли человек трудности при ходьбе (No / Yes), + * Sex - Пол (female, male), + * AgeCategory - Возрастная категория (18-24, 25-29, 30-34, 35-39, 40-44, 45-49, 50-54, 55-59, 60-64, 65-69, 70-74, 75-79, 80 or older), + * Race - Национальная принадлежность человека (White, Black, Hispanic, American Indian/Alaskan Native, Asian, Other), + * Diabetic - Был ли у человека диабет (No / Yes), + * PhysicalActivity - Занимался ли человек спротом за последний месяц (No / Yes), + * GenHealth - Общее самочувствие человека (Excellent, Very good, Good, Fair, Poor), + * SleepTime - Сколько человек в среднем спит за 24 часа (0-24), + * Asthma - Была ли у человека астма (No / Yes), + * KidneyDisease - Было ли у человека заболевание почек (No / Yes), + * SkinCancer - Был ли у человека рак кожи (No / Yes). + +Ссылка на страницу набора на kuggle: [Indicators of Heart Disease](https://www.kaggle.com/datasets/kamilpytlak/personal-key-indicators-of-heart-disease/data) + +#### Оцифровка и нормализация данных +Для нормальной работы с данными, необходимо исключить из них все нечисловые значения. После этого, представить все строковые значения параметров как числовые и очистить датасет от "мусора". Для удаления нечисловых значений воспользуемся функцией `.dropna()`. Мы исключаем строки с нечисловыми значениями, поскольку данные предварительно были очищены (указано в описании датасета) и строк данных достаточно с избытком для обучение модели: `400.000`. + +После этого, переведём все строковые значения данных в числовые методами прямой оцифровки, разделения на группы, ранжирования. + +Процесс оцифровки данных столбцов со строковыми значениями: + +- Имеет ли человек ССЗ (0 / 1) +- Выкурил ли человек хотя бы 5 пачек сигарет за всю жизнь (0 / 1) +- Сильно ли человек употребляет алкоголь (0 / 1) +- Был ли у человека инсульт (0 / 1) +- Ииспытывает ли человек трудности при ходьбе (0 / 1) +- Пол (Ж - 0 / М - 1) +- Возрастная категория (средний возраст каждого диапазона) +- Национальная принадлежность человека + - White - Европиойды - 0 + - Black - Негройды - 1 + - Hispanic - Испанцы - 2 + - American Indian/Alaskan Native - Индусы - 3 + - Asian - Азиаты - 4 + - Other - Другие - 5 +- Был ли у человека диабет (0 / 1) +- Занимался ли человек спротом за последний месяц (0 / 1) +- Общее самочувствие человека + - Excellent - Отлично - 4 + - Very good - Очень хорошо - 3 + - Good - Хорошо - 2 + - Fair - Нормально - 1 + - "Poor" / "Other..." - Плохое или другое - 0 +- Была ли у человека астма (0 / 1) +- Было ли у человека заболевание почек (0 /1) +- Был ли у человека рак кожи (0 / 1) + +После оцифровки значений необходимо избавиться от строк с возможными остаточнымии данными ("мусором"). Для этого переведём автоматически все значения датасета в числовые функцией `to_numeric` и непереводимые отметим как `NaN` (параметр `errors='coerce'`). После снова сотрём строки содержащие нечисловые значения методом `.dropna()` и сохраним нормализованный датасет в новый csv файл: +```python +df = df.applymap(pd.to_numeric, errors='coerce').dropna() +df.to_csv(fileout, index=False) +``` + +#### Выявление значимых параметров +В выбранном датасете параметром предсказания `y` выступает столбец данных `HeartDisease`. Остальные столбцы считаются параметрами для решения задачи предсказания `x`, которые необходимо проранжировать по важности. Чтобы разделить выборку данных на обучаемую и тестовую, воспользуемся функцией `.iloc`. +```python +x_train = df[["BMI", "Smoking", "AlcoholDrinking", "Stroke", "PhysicalHealth", + "MentalHealth", "DiffWalking", "Sex", "AgeCategory", "Race", "Diabetic", + "PhysicalActivity", "GenHealth", "SleepTime", "Asthma", "KidneyDisease", "SkinCancer"]].iloc[ + 0:round(len(df) / 100 * 99)] +y_train = df["HeartDisease"].iloc[0:round(len(df) / 100 * 99)] +x_test = df[["BMI", "Smoking", "AlcoholDrinking", "Stroke", "PhysicalHealth", + "MentalHealth", "DiffWalking", "Sex", "AgeCategory", "Race", "Diabetic", + "PhysicalActivity", "GenHealth", "SleepTime", "Asthma", "KidneyDisease", "SkinCancer"]].iloc[ + round(len(df) / 100 * 99):len(df)] +y_test = df["HeartDisease"].iloc[round(len(df) / 100 * 99):len(df)] +``` +Где `round(len(df) / 100 * 99)` - 99ти процентная строка в датасете. + +Теперь, обучим модель на данных `x_train` и `y_train` и получим значимость каждого признака в модели с помощью метода `.feature_importances_`. После отмасштабируем значения важности признаков. +```python +ranks = np.abs(dtc.feature_importances_) +minmax = MinMaxScaler() +ranks = minmax.fit_transform(np.array(ranks).reshape(len(x_train.columns), 1)).ravel() +ranks = map(lambda x: round(x, 2), ranks) +ranks = dict(zip(x_train.columns, ranks)) +ranks = dict(sorted(ranks.items(), key=lambda x: x[1], reverse=True)) +``` + +Чтобы отсеять значимые параметры от незначимых, условимся, что параметры, с оценкой значимости меньше `0.05` будут считаться незначимыми. Выведем список параметров с пометками: +``` +X ranging results: + * BMI: 1.0 - Approved + * SleepTime: 0.26 - Approved + * PhysicalHealth: 0.18 - Approved + * GenHealth: 0.16 - Approved + * MentalHealth: 0.15 - Approved + * AgeCategory: 0.14 - Approved + * Race: 0.07 - Approved + * PhysicalActivity: 0.06 - Approved + * Stroke: 0.04 - Eliminated + * Smoking: 0.03 - Eliminated + * Asthma: 0.03 - Eliminated + * SkinCancer: 0.03 - Eliminated + * DiffWalking: 0.02 - Eliminated + * Sex: 0.02 - Eliminated + * AlcoholDrinking: 0.0 - Eliminated + * Diabetic: 0.0 - Eliminated + * KidneyDisease: 0.0 - Eliminated +``` + +Где `Approved` - параметр значим и будет использоваться в предсказании, а `Eliminated` - параметр незначим и будет исключён. + +#### Решение задачи кластеризации на полном наборе признаков +Чтобы решить задачу кластеризации моделью `DecisionTreeClassifier`, воспользуемся методом `.predict()`. Оценку качества решения и графики будем строить теми же методами, что в 1й лабораторной работе. + +График решения задачи классификации на полном наборе признаков: + +![](FullParam.png "") + +#### Решение задачи кластеризации, используя только значимые признаки +Согласно предыдущему пункту, значимыми признаками модели были выявлены: + * BMI + * SleepTime + * PhysicalHealth + * GenHealth + * MentalHealth + * AgeCategory + * Race + * PhysicalActivity +Обучим модель только с их использованием, решим задачу классификации и построим график. + +График решения задачи классификации, используя только значимые признаки: + +![](ImpParam.png "") + +### Вывод +Согласно среднеквадратической ошибке и коэфициенту детерминации, модель, обученная только на значимых признаков отработала точнее, чем модель, обученная на полном наборе признаков. Это значит, что ранжирование было проведено верно и дало полезный результат. О логической оценке исключённых данных сказать ничего не получится, поскольку действительную зависимость результата от параметров значет только медицинский эксперт. + +Исходя их общих значений точности, обе модели показали хорошие результаты и могут быть применимы к решению задачи классификации на данном наборе данных. \ No newline at end of file diff --git a/arutunyan_dmitry_lab_3/main.py b/arutunyan_dmitry_lab_3/main.py new file mode 100644 index 0000000..62b4510 --- /dev/null +++ b/arutunyan_dmitry_lab_3/main.py @@ -0,0 +1,221 @@ +import pandas as pd +import numpy as np +from matplotlib import pyplot as plt +from sklearn import metrics +from sklearn.preprocessing import MinMaxScaler +from sklearn.tree import DecisionTreeClassifier + +''' +Названия столбцов набора данных и их описание: + + * HeartDisease - Имеет ли человек ССЗ (No / Yes), + * BMI - Индекс массы тела человека (float), + * Smoking - Выкурил ли человек хотя бы 5 пачек сигарет за всю жизнь (No / Yes), + * AlcoholDrinking - Сильно ли человек употребляет алкоголь (No / Yes), + * Stroke - Был ли у человека инсульт (No / Yes), + * PhysicalHealth - Сколько дней за последний месяц человек чувствовал себя плохо (0-30), + * MentalHealth - Сколько дней за последний месяц человек чувствовал себя удручённо (0-30), + * DiffWalking - Ииспытывает ли человек трудности при ходьбе (No / Yes), + * Sex - Пол (female, male), + * AgeCategory - Возрастная категория (18-24, 25-29, 30-34, 35-39, 40-44, 45-49, 50-54, 55-59, 60-64, 65-69, 70-74, 75-79, 80 or older), + * Race - Национальная принадлежность человека (White, Black, Hispanic, American Indian/Alaskan Native, Asian, Other), + * Diabetic - Был ли у человека диабет (No / Yes), + * PhysicalActivity - Занимался ли человек спротом за последний месяц (No / Yes), + * GenHealth - Общее самочувствие человека (Excellent, Very good, Good, Fair, Poor), + * SleepTime - Сколько человек в среднем спит за 24 часа (0-24), + * Asthma - Была ли у человека астма (No / Yes), + * KidneyDisease - Было ли у человека заболевание почек (No / Yes), + * SkinCancer - Был ли у человека рак кожи (No / Yes). +''' + + +# Метод оцифровки и нормализации данных +def normalisation(filename): + fileout = "P:\\ULSTU\\ИИС\\Datasets\\heart_2020_norm.csv" + df = pd.read_csv(filename, sep=',').dropna() # Считываем данные с csv файла и удаляем строки, содержащие NaN + + for index, row in df.iterrows(): + if index % 10000 == 0: + print("normalisation running . . . " + str(round((index / len(df) * 100), 2)) +'%') + if "Yes" in row["HeartDisease"]: # Имеет ли человек ССЗ (0 / 1) + df.at[index, "HeartDisease"] = 1 + else: + df.at[index, "HeartDisease"] = 0 + if "Yes" in row["Smoking"]: # Выкурил ли человек хотя бы 5 пачек сигарет за всю жизнь (0 / 1) + df.at[index, "Smoking"] = 1 + else: + df.at[index, "Smoking"] = 0 + if "Yes" in row["AlcoholDrinking"]: # Сильно ли человек употребляет алкоголь (0 / 1) + df.at[index, "AlcoholDrinking"] = 1 + else: + df.at[index, "AlcoholDrinking"] = 0 + if "Yes" in row["Stroke"]: # Был ли у человека инсульт (0 / 1) + df.at[index, "Stroke"] = 1 + else: + df.at[index, "Stroke"] = 0 + if "Yes" in row["DiffWalking"]: # Ииспытывает ли человек трудности при ходьбе (0 / 1) + df.at[index, "DiffWalking"] = 1 + else: + df.at[index, "DiffWalking"] = 0 + if "Female" in row["Sex"]: # Пол (Ж - 0 / М - 1) + df.at[index, "Sex"] = 0 + else: + df.at[index, "Sex"] = 1 + if "18-24" in row["AgeCategory"]: # Возрастная категория (средний возраст каждого диапазона) + df.at[index, "AgeCategory"] = (18 + 24) / 2 + elif "25-29" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (25 + 29) / 2 + elif "30-34" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (30 + 34) / 2 + elif "35-39" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (35 + 39) / 2 + elif "40-44" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (40 + 44) / 2 + elif "45-49" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (45 + 49) / 2 + elif "50-54" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (50 + 54) / 2 + elif "55-59" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (55 + 59) / 2 + elif "60-64" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (60 + 64) / 2 + elif "65-69" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (65 + 69) / 2 + elif "70-74" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (70 + 74) / 2 + elif "75-79" in row["AgeCategory"]: + df.at[index, "AgeCategory"] = (75 + 79) / 2 + else: + df.at[index, "AgeCategory"] = (25 + 29) / 2 + if "White" in row["Race"]: # Национальная принадлежность человека + df.at[index, "Race"] = 0 # White - Европиойды - 0 + elif "Black" in row["Race"]: # Black - Негройды - 1 + df.at[index, "Race"] = 1 # Hispanic - Испанцы - 2 + elif "Hispanic" in row["Race"]: # American Indian/Alaskan Native - Индусы - 3 + df.at[index, "Race"] = 2 # Asian - Азиаты - 4 + elif "American Indian/Alaskan Native" in row["Race"]: # Other - Другие - 5 + df.at[index, "Race"] = 3 + elif "Asian" in row["Race"]: + df.at[index, "Race"] = 4 + else: + df.at[index, "Race"] = 5 + if "Yes" in row["Diabetic"]: # Был ли у человека диабет (0 / 1) + df.at[index, "Diabetic"] = 1 + else: + df.at[index, "Diabetic"] = 0 + if "Yes" in row["PhysicalActivity"]: # Занимался ли человек спротом за последний месяц (0 / 1) + df.at[index, "PhysicalActivity"] = 1 + else: + df.at[index, "PhysicalActivity"] = 0 + if "Excellent" in row["GenHealth"]: # Общее самочувствие человека + df.at[index, "GenHealth"] = 4 # Excellent - Отлично - 4 + elif "Very good" in row["GenHealth"]: # Very good - Очень хорошо - 3 + df.at[index, "GenHealth"] = 3 # Good - Хорошо - 2 + elif "Good" in row["GenHealth"]: # Fair - Нормально - 1 + df.at[index, "GenHealth"] = 2 # "Poor" / "Other..." - Плохое или другое - 0 + elif "Fair" in row["GenHealth"]: + df.at[index, "GenHealth"] = 1 + else: + df.at[index, "GenHealth"] = 0 + if "Yes" in row["Asthma"]: # Была ли у человека астма (0 / 1) + df.at[index, "Asthma"] = 1 + else: + df.at[index, "Asthma"] = 0 + if "Yes" in row["KidneyDisease"]: # Было ли у человека заболевание почек (0 /1) + df.at[index, "KidneyDisease"] = 1 + else: + df.at[index, "KidneyDisease"] = 0 + if "Yes" in row["SkinCancer"]: # Был ли у человека рак кожи (0 / 1) + df.at[index, "SkinCancer"] = 1 + else: + df.at[index, "SkinCancer"] = 0 + + df = df.applymap(pd.to_numeric, errors='coerce').dropna() # Гарантированно убираем все нечисловые значения из датасета + df.to_csv(fileout, index=False) # Сохраняем нормализованный датасет для дальнейшей работы + return fileout + + +# Метод ранжирования параметров по степени важности +def param_range(filename, elim_kp): + df = pd.read_csv(filename, sep=',') # Считываем нормализованные данные и разделяем их на выборки + x_train = df[["BMI", "Smoking", "AlcoholDrinking", "Stroke", "PhysicalHealth", + "MentalHealth", "DiffWalking", "Sex", "AgeCategory", "Race", "Diabetic", + "PhysicalActivity", "GenHealth", "SleepTime", "Asthma", "KidneyDisease", "SkinCancer"]].iloc[ + 0:round(len(df) / 100 * 99)] + y_train = df["HeartDisease"].iloc[0:round(len(df) / 100 * 99)] + x_test = df[["BMI", "Smoking", "AlcoholDrinking", "Stroke", "PhysicalHealth", + "MentalHealth", "DiffWalking", "Sex", "AgeCategory", "Race", "Diabetic", + "PhysicalActivity", "GenHealth", "SleepTime", "Asthma", "KidneyDisease", "SkinCancer"]].iloc[ + round(len(df) / 100 * 99):len(df)] + y_test = df["HeartDisease"].iloc[round(len(df) / 100 * 99):len(df)] + + dtc = DecisionTreeClassifier(random_state=241) # Создаём модель дерева решений + dtc.fit(x_train.values, y_train.values) # Обучаем модель на данных + y_predict = dtc.predict(x_test.values) # Решаем задачу классификации на полном наборе признаков + err = pred_errors(y_predict, y_test.values) # Рассчитываем ошибки предсказания + make_plots(y_test.values, y_predict, err[0], err[1], "Полный набор данных") # Строим графики + + ranks = np.abs(dtc.feature_importances_) # Получаем значимость каждого признака в модели + minmax = MinMaxScaler() # Шкалируем и нормализуем значимость + ranks = minmax.fit_transform(np.array(ranks).reshape(len(x_train.columns), 1)).ravel() + ranks = map(lambda x: round(x, 2), ranks) + ranks = dict(zip(x_train.columns, ranks)) + ranks = dict(sorted(ranks.items(), key=lambda x: x[1], reverse=True)) # Сортируем оценки по максимуму и записываем в словарь + + print("X ranging results: \n") + del_keys = [] # Исключаем параметры, важность которых меньше elim_kp + for key, value in ranks.items(): + if value >= elim_kp: + print(" * " + key + ": " + str(value) + " - Approved") + else: + print(" * " + key + ": " + str(value) + " - Eliminated") + del_keys.append(key) + + for key in del_keys: + ranks.pop(key) + + return filename, ranks.keys() + + +# Метод решения задачи классификации, основанный только на значимых параметрах +def most_valuable_prediction(params): + filename = params[0] + val_p = params[1] + df = pd.read_csv(filename, sep=',') + x_train = df[val_p].iloc[0:round(len(df) / 100 * 99)] + y_train = df["HeartDisease"].iloc[0:round(len(df) / 100 * 99)] + x_test = df[val_p].iloc[round(len(df) / 100 * 99):len(df)] + y_test = df["HeartDisease"].iloc[round(len(df) / 100 * 99):len(df)] + + dtc = DecisionTreeClassifier(random_state=241) + dtc.fit(x_train.values, y_train.values) + y_predict = dtc.predict(x_test.values) + err = pred_errors(y_predict, y_test.values) + make_plots(y_test.values, y_predict, err[0], err[1], "Только важные параметры") + + +# Метод рассчёта ошибок +def pred_errors(y_predict, y_test): + mid_square = np.round(np.sqrt(metrics.mean_squared_error(y_test, y_predict)),3) # Рассчёт среднеквадратичной ошибки модели + det_kp = np.round(metrics.accuracy_score (y_test, y_predict), 2) # Рассчёт коэфициента детерминации модели + return mid_square, det_kp + + +# Метод отрисовки графиков +def make_plots(y_test, y_predict, mid_sqrt, det_kp, title): + plt.plot(y_test, c="red", label="\"y\" исходная") # Создание графика исходной функции + plt.plot(y_predict, c="green", label="\"y\" предсказанная \n" + "Ср^2 = " + str(mid_sqrt) + "\n" + "Кд = " + str(det_kp)) # Создание графика предсказанной функции + plt.legend(loc='lower left') + plt.title(title) + plt.savefig('static/' + title + '.png') + plt.close() + + +if __name__ == '__main__': + # Работа системы в комплексе + # Здесь elim_kp - значение пороговой значимости параметра (выбран эмпирически) + most_valuable_prediction(param_range(normalisation("P:\\ULSTU\\ИИС\\Datasets\\heart_2020_cleaned.csv"), 0.05)) + +