Введение

Сепсис — потенциально опасное для жизни заболевание, возникающее в результате реакции организма на инфекцию. Когда организм обнаруживает инфекцию, он выделяет химические вещества в кровоток для борьбы с вторгшимися микроорганизмами. Однако при сепсисе реакция организма сбивается, что приводит к распространенному воспалению по всему телу.

Это воспаление может вызвать повреждение различных органов и тканей, что приводит к дисфункции органов и, в тяжелых случаях, к органной недостаточности. Сепсис может быстро прогрессировать и может стать неотложной медицинской помощью. Он может поражать людей всех возрастов, включая детей и пожилых людей, и может возникать в результате инфекций, таких как бактериальные, вирусные или грибковые инфекции. Некоторые распространенные источники инфекции, которые могут привести к сепсису, включают инфекции мочевыводящих путей, респираторные инфекции, абдоминальные инфекции и кожные инфекции.

Этот проект/статья посвящен анализу распространенности сепсиса среди пациентов и определению групп пациентов, у которых, вероятно, будет диагностирован сепсис. Также создать модель машинного обучения, встроенную в веб-приложение с помощью FastAPI, для классификации пациентов с сепсисом.

Методология

Согласно IBM, CRISP-DM, что означает межотраслевой стандартный процесс интеллектуального анализа данных, является проверенным в отрасли способом управления вашими усилиями по интеллектуальному анализу данных.

  • В качестве методологии он включает описания типичных этапов проекта, задач, связанных с каждым этапом, и объяснение отношений между этими задачами.
  • Как модель процесса, CRISP-DM предоставляет обзор жизненного цикла интеллектуального анализа данных.

Шесть этапов жизненного цикла интеллектуального анализа данных CRISP-DM:

  1. Бизнес-понимание
  2. Понимание данных
  3. Подготовка данных
  4. Моделирование
  5. "Оценка"
  6. Развертывание

Понимание бизнеса

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

Проект также включает создание интерфейса прикладного программирования (API) с использованием FastAPI для интеграции разработанной модели машинного обучения в удобное для пользователя приложение.

Гипотеза и вопросы

Нулевая гипотеза: H0: нет никакой связи между возрастом и сепсисом.

Альтернативная гипотеза: H1: Существует связь между возрастом и сепсисом.

Предположения:

  1. Предполагалось, что использовалось артериальное давление диастолического типа.

Вопросы

  1. Каково распределение возрастов пациентов, охваченных данными?
  2. Каково число больных сепсисом?
  3. Сколько пациентов имеют страховку?
  4. Влияет ли артериальное давление на возраст?
  5. В каких возрастных группах больше всего случаев сепсиса?
  6. Какая связь между возрастом и индексом массы тела?
  7. Увеличивает ли отсутствие страховки шансы пациента заболеть сепсисом?
  8. Сколько больных попадает в категорию нормального, повышенного и высокого артериального давления?
  9. Сколько пациентов в каждой категории индекса массы тела имеют сепсис?

Понимание данных

Наборы данных (обучающие и тестовые) для этого проекта представлены в формате csv. Ниже описаны столбцы, присутствующие в данных.

ID – число, представляющее идентификатор пациента.

PRG — глюкоза плазмы

PL — Результат анализа крови-1 (мЕд/мл)

PR – артериальное давление (мм рт. ст.)

SK — Результат анализа крови-2 (мм)

TS — Результат анализа крови-3 (мЕд/мл)

M11 — индекс массы тела (вес в кг/(рост в м)²

BD2 — Результат анализа крови-4 (мкЕд/мл)

Возраст — возраст пациентов (лет).

Страховка — если у пациента есть действующая страховая карта.

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

Подготовка данных

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

Импорт всех соответствующих библиотек и пакетов

# Data Manipuulation
import pandas as pd
import numpy as np

# Vizualisation (Matplotlib, Plotly, Seaborn, etc. )
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.graph_objects as go
import plotly.express as px

# Feature Processing (Scikit-learn processing, etc. )
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.metrics import confusion_matrix , classification_report, f1_score, accuracy_score,\
precision_score, recall_score, fbeta_score, make_scorer, roc_auc_score
from sklearn.model_selection import train_test_split, GridSearchCV
from skopt import BayesSearchCV
from sklearn.utils import class_weight

import scipy.stats as stats

# models 
from sklearn import svm
from xgboost import XGBClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier

# model interpretation
import shap

import pickle
import os
import warnings
warnings.filterwarnings("ignore")

Обзор набора данных

#load preview the dataset
sepsis_data = pd.read_csv("Paitients_Files_Train.csv")
sepsis_data.head()

Базовая проверка набора данных

# Listing the columns in the data
sepsis_data.columns.to_list()

# Checking the shape of the data
sepsis_data.shape

# Checking for more information about the data
sepsis_data.info()

# Checking for missing/null values
sepsis_data.isna().sum()

# Checking for duplicates
sepsis_data.duplicated().sum()

# Create a function to print the number of unique values
def print_unique_values(data):
    values = {}
    for col in data.columns:
        number = data[col].nunique()
        print(f'{col}: has {number} of unique values')   
print_unique_values(sepsis_data)

# Create a function to check if there are zeros in the data
def number_of_zeros(data):
    for col in data.columns:
        if col != 'Sepssis':
            count = (data[col] == 0).sum()
            print(f'Count of zeros in Column  {col} : {count}')
number_of_zeros(sepsis_data)

Несмотря на то, что в столбцах не было пустых значений, в некоторых столбцах были нули.

# checking the column headers
sepsis_data.columns

В приведенной выше ячейке отображаются имена столбцов, однако они недостаточно информативны.

Переименуйте столбцы

sepsis_data = sepsis_data.rename(columns={'PRG': 'Plasma Glucose', 'PL': 'Blood Work Result-1',
                        'PR': 'Blood Pressure','SK': 'Blood Work Result-2',
                        'TS': 'Blood Work Result-3', 'M11': 'Body Mass Index',
                        'BD2': 'Blood Work Result-4', 'Sepssis': 'Sepsis'})
# Checking the rename of columns
sepsis_data.head()

Столбцы были успешно переименованы и теперь являются описательными.

# Checking the statistics of the data
sepsis_data.describe()

Из сводной статистики мы делаем вывод, что средний возраст захваченных пациентов составляет 33,29 года. Среднее артериальное давление составляет 68,732888 мм рт.ст., что ниже 80 мм рт.ст., следовательно, указывает на то, что у пациентов было в среднем нормальное давление.

Проверьте корреляцию между различными столбцами

# Find the correlation between the columns
corr_matrix = sepsis_data.corr()
corr_matrix

# Plot a heatmap for the correlations
plt.figure(figsize=(8, 5))
sns.heatmap(corr_matrix, annot=True)
plt.show();

# get the minimum value
min_corr = corr_matrix.min().min()  
# get the columns involved
min_corr_cols = corr_matrix.unstack().idxmin()

print('The minimum correlation was:', min_corr)
print('The columns involved are:', min_corr_cols)

The minimum correlation was: -0.12155329275228288
The columns involved are: ('Blood Work Result-2', 'Age')
# get the minimum value
max_corr = (corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool_))
            .max().max())  

# get the maximum value (excluding same columns)
# get the columns involved
max_corr_cols = (corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool_))
                 .stack().idxmax()) 

print('The maximun correlation was:', max_corr)
print('The columns involved are:', max_corr_cols)
The maximun correlation was: 0.5325446086908504
The columns involved are: ('Plasma Glucose', 'Age')

Самая низкая корреляция между результатом анализа крови-2 и возрастом составляет -0,12155329275228288.

Самая высокая корреляция между уровнем глюкозы в плазме и возрастом составляет 0,5325446086908504.

Проверка гипотезы

# Select the Age and Sepsis columns from the dataset
age = sepsis_data['Age']
sepsis = (sepsis_data['Sepsis'] == 'Positive').astype(bool).astype(int)

# Perform correlation analysis
correlation, p_value = stats.pearsonr(age, sepsis)

# Print the correlation coefficient and p-value
print("Correlation coefficient:", correlation)
print("P-value:", p_value)

if p_value > 0.05:
    print('Fail to reject the null hypothesis. ')
else:
    print('Reject the null hypothesis')
Correlation coefficient: 0.2102342858235142
P-value: 2.0718778891887102e-07
Reject the null hypothesis

Одномерный анализ

  1. Каково распределение пациентов по возрасту в данных?
plt.figure(figsize=(10, 6))
sns.histplot(sepsis_data['Age'], bins=20, kde=True)
plt.title("Distribution of Ages for Patients")
plt.xlabel("Age")
plt.ylabel("Count")
plt.show()

На приведенном выше графике показано распределение пациентов по возрасту. Было замечено, что в возрастной группе от 20 до 30 лет больше пациентов, а в возрастной группе от 70 до 80 – меньше.

2. Каково число больных сепсисом?

# Calculate the sepsis counts
sepsis_counts = sepsis_data['Sepsis'].value_counts()

# Set custom colors
colors = ['#fc4f30', '#008fd5']

# Calculate percentages
percentages = round(sepsis_counts / sepsis_counts.sum() * 100, 2).astype(str) + '%'

# Create a bar plot for sepsis distribution
plt.figure(figsize=(8, 5))
ax = sepsis_counts.plot(kind='bar', color=colors, edgecolor='black', linewidth=1.2, figsize=(8, 6), rot=0)
ax.set_title('Sepsis Distribution', fontsize=14)
ax.set_xlabel('Sepsis', fontsize=12)
ax.set_ylabel('Count', fontsize=12)
ax.set_ylim(0, 500)

# Add count values and percentages on top of each bar
for i, p in enumerate(ax.patches):
    width, height = p.get_width(), p.get_height()
    x, y = p.get_xy() 
    ax.annotate(f'{height}\n{percentages[i]}', (x + 0.15, y + height + 10))

ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Show the plot
plt.show()

Распределение сепсиса показывает, что пациентов с отрицательным результатом больше, чем с положительным. В общей сложности 208 пациентов оказались положительными, а 391 – отрицательными.

3. Какова доля пациентов со страховкой?

# Calculate the count of patients with insurance
insurance_counts = sepsis_data['Insurance'].value_counts()

# Plot the proportion of  patients with insurance_counts using a pie chart
labels = insurance_counts.index
colors = ['#008fd5', '#fc4f30']
explode = (0, 0.1)
plt.figure(figsize=(6, 6))
plt.pie(insurance_counts, labels=labels, explode=explode, colors=colors,
        autopct='%1.1f%%', shadow=True, startangle=90)
plt.axis('equal')
plt.title('Proportion of Patients With Insurance')
plt.legend(loc='upper right', bbox_to_anchor=(1.3, 1))
plt.ylabel('')
plt.show()

На круговой диаграмме 1 представляет пациентов со страховкой, а 0 представляет пациентов без страховки. На диаграмме видно, что пациентов со страховкой больше, чем без страховки. Количество пациентов со страховкой составляет 68,6%, что более чем в два раза превышает количество пациентов, не имеющих страховки. Учитывая, что это также наша целевая переменная, мы можем заключить, что набор данных несбалансирован. По сути, переход к моделированию потребует некоторой предварительной обработки, чтобы убедиться, что мы не вносим предвзятость в нашу модель.

Двумерный анализ

4. Влияет ли возраст на давление крови?

plt.figure(figsize=(8, 5))
sns.regplot(x='Blood Pressure', y='Age', data=sepsis_data)
plt.title('The correlation between the Age and Blood Pressure')
plt.show()

На этом графике показано, что у большинства пациентов нормальное кровяное давление находится в диапазоне от 60 до 80, а у некоторых пациентов наблюдается высокое кровяное давление в диапазоне от 80 до 90, что считается стадией гипертензии 1. Присутствуют некоторые выбросы с низким кровяным давлением ниже 20 и очень высоким кровяным давлением выше 100, что указывает на 2-ю стадию артериальной гипертензии.

5. В какой возрастной группе больше всего случаев сепсиса

# Filter data for patients with sepsis
sepsis_age_group = sepsis_data[sepsis_data['Sepsis'] == 'Positive']

# Create an interactive histogram using Plotly
fig = px.histogram(sepsis_age_group, x='Age', nbins=20,
                   labels={'Age': 'Age', 'count': 'Count'},
                   title='Distribution of Ages for Patients With Highest Cases of Sepsis',
                   color_discrete_sequence=['violet'])

# Update the layout for better visualization
fig.update_layout(
    xaxis_title="Age",
    yaxis_title="Count",
)

# Show the interactive plot
fig.show()

Гистограмма показывает, что возрастная группа с наибольшим количеством случаев сепсиса находится в возрасте от 25 до 29, в то время как возрастная группа с наименьшим количеством случаев составляет 60 лет и старше.

6. Какая связь между возрастом и артериальным давлением?

# Sample data (replace this with your actual data)
age_bmi = {
    'Blood Pressure': ['Normal', 'Elevated', 'High', 'Normal', 'Elevated'],
    'Age': [45, 55, 65, 50, 40]
}
age_bmi = pd.DataFrame(age_bmi)

# Plot the relationship between Age and Blood Pressure using Plotly box plot
fig = px.box(age_bmi, x='Blood Pressure', y='Age', color='Blood Pressure',
             labels={'Blood Pressure': 'Blood Pressure Category', 'Age': 'Age'},
             title='Relationship between Age and Blood Pressure')

# Update the layout for better visualization
fig.update_layout(
    xaxis_title="Blood Pressure Category",
    yaxis_title="Age",
)

# Show the interactive plot
fig.show()

На этой диаграмме показано сравнение между различными категориями крови и их соответствующими возрастами. Пациенты с нормальным и повышенным артериальным давлением имеют средний возраст 47 лет, однако нормальная категория включает пациентов в возрасте от 45 до 50 лет. strong>, в то время как пациенты в повышенной категории находятся в возрасте от 40 до 55 лет. Пациенты с высоким кровяным давлением старше 60 лет.

7. Увеличивает ли отсутствие страховки шансы пациента заболеть сепсисом?

# Plot the impact of having insurance on sepsis occurrence
fig = px.histogram(sepsis_data, x='Insurance', color='Sepsis', barmode='group',
                   labels={'Insurance': 'Insurance', 'Sepsis': 'Sepsis'},
                   color_discrete_map={"Negative": "lightcoral", "Positive": "lightgreen"})

fig.update_layout(
    title="Impact of Having Insurance on Sepsis Occurrence",
    xaxis_title="Insurance",
    yaxis_title="Count",
)

# Show the interactive plot
fig.show()

1. Наблюдая за графиком, 0 представляет пациентов без страховки, а 1 представляет пациентов с сепсисом. Количество пациентов со страховкой больше, чем без страховки.

2. Для пациентов без страховки отрицательный случай составляет 131, а положительный сепсис - около 57.

3. Пациенты со страховкой также показывают, что отрицательные случаи составляют 260, а положительные случаи сепсиса - 151. Когда мы смотрим на пациентов без страховки, ясно, что отсутствие страховки не влияет на возникновение сепсиса.

4. Однако это может быть результатом других параметров в данных, таких как дисбаланс целевой переменной «сепсис».

Многомерный анализ

8. Сколько пациентов имеют недостаточный вес, нормальный вес, избыточный вес, ожирение и тяжелое ожирение?

Ниже приведены диапазоны ИМТ.

  • до 18,5 — это описывается как недостаточный вес.
  • между 18,5 и 24,9 — это называется «здоровый диапазон».
  • между 25 и 29,9 — это описывается как избыточный вес.
  • от 30 до 39,9 — ожирение.
  • 40 и более — это описывается как тяжелое ожирение.
# function to create a new column 'BMI

def create_bmi_range(row):
    if (row['Body Mass Index'] <= 18.5):
        return 'Under Weight'
    elif (row['Body Mass Index'] > 18.5) and (row['Body Mass Index'] <= 24.9):
        return 'Healthy Weight'
    elif (row['Body Mass Index'] > 24.9) and (row['Body Mass Index'] <= 29.9):
        return 'Overweight'
    elif (row['Body Mass Index'] > 29.9) and (row['Body Mass Index'] < 40):
        return 'Obesity'
    elif row['Body Mass Index'] >= 40:
        return 'Severe Obesity'
weight = sepsis_data.copy()
weight['BMI Ranges'] = weight.apply(create_bmi_range, axis=1)
range_count = weight['BMI Ranges'].value_counts()
# Define custom colors for each BMI range
colors = ['lightblue', 'lightgreen', 'orange', 'lightcoral']

# Plot the number of patients in each BMI range using Plotly bar plot
fig = px.bar(x=range_count.index, y=range_count.values,
             labels={'x': 'The BMI Ranges', 'y': 'Number of Patients'},
             title='The number of patients in each BMI range',
             color=range_count.index,  # Use BMI range as the color parameter to get different colors for each bar
             color_discrete_sequence=colors)  # Use custom colors defined above

# Update the layout for better visualization
fig.update_layout(
    xaxis_title="The BMI Ranges",
    yaxis_title="Number of Patients",
)

# Show the interactive plot
fig.show()

На изображении видно, что пациентов с ожирением больше, всего 290 пациентов, и меньше пациентов с недостаточным весом. Также количество пациентов с нормальной массой тела составляет 79, что составляет менее 25% от общего числа пациентов, а количество пациентов с избыточной массой тела (включает избыточную массу тела, ожирение, более тяжелую форму ожирения) составляет более 500 человек.

9. Распределение пациентов с сепсисом в разных категориях ИМТ

# Distribution of patients in different BMI categories
data = {
    'Body Mass Index': [21.5, 26.3, 19.8, 32.0, 27.5],
    'Sepsis': ['Negative', 'Positive', 'Positive', 'Negative', 'Positive']
}
sepsis_data = pd.DataFrame(sepsis_data)

# Define BMI labels and bins
bmi_labels = ['Underweight', 'Healthy weight', 'Overweight', 'Obese', 'Severely Obese']
bmi_bins = [0, 18.5, 24.9, 29.9, 34.9, np.inf]

# Create a new column for BMI categories
sepsis_data['BMI_Category'] = pd.cut(sepsis_data['Body Mass Index'], bins=bmi_bins, labels=bmi_labels)

# Plot the distribution of patients in different BMI categories using Plotly bar plot
fig = px.histogram(sepsis_data, x='BMI_Category', color='Sepsis', barmode='group',
                   category_orders={'BMI_Category': bmi_labels},
                   labels={'BMI_Category': 'BMI Category', 'count': 'Count'},
                   color_discrete_map={"Negative": "lightcoral", "Positive": "lightgreen"})

# Update the layout for better visualization
fig.update_layout(
    title="Distribution of Patients in Different BMI Categories",
    xaxis_title="BMI Category",
    yaxis_title="Count",
)

# Show the interactive plot
fig.show()

# Multivariate paiplot
sns.pairplot(sepsis_data, hue='Sepsis', diag_kind='kde', markers=["o", "s"], palette={"Positive": "lightgreen", "Negative": "lightcoral"})
plt.suptitle("Multivariate Analysis", y=1.02)
plt.show()

Моделирование