Анализ медицинских врачебных заключений¶
В этом демо-примере будет проведен анализ медицинских данных с помощью инструментов аналитики
Во время анализа датасета было вычеслено несколько метрик по согласованности, точности работы врачей.
Необходимо установить библиотеки
Необходимо вручную прописать пути, где находится проект, чтобы перейти в рабочую директорию, а также установить необходимые зависимости
!cd /user/Demo_public/biomedical/analis_medical_result
!pip install -r /user/Demo_public/biomedical/analis_medical_result/requirements.txt
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import cohen_kappa_score
from statsmodels.stats.inter_rater import fleiss_kappa
import numpy as np
from sklearn.metrics import accuracy_score, recall_score, confusion_matrix, balanced_accuracy_score, mean_squared_error
from sklearn.metrics import precision_recall_curve, average_precision_score, f1_score, fbeta_score, roc_auc_score,ConfusionMatrixDisplay, roc_curve, auc
from sklearn.preprocessing import MinMaxScaler
from scipy.stats import sem, t
Прочитаем данные и посмотрим что содержится в первых 5 строках датафрейма
focus = pd.read_csv('/user/Demo_public/biomedical/analis_medical_result/Дата.csv')
focus.head()
focus = focus.drop('ID Файла', axis=1)
В файлах содержится разметка изображений. Каждый из 15 врачей на 476 фотографий сделал заключение в отсутствии или наличии патологии.
Можно оценить количество врачей, уверенных на патологию, на каждый снимок и предварительно оценить, где действительно патология, где нет. Также можно поискать сильные расхождения в результатах врачей. Можно определить, насколько врачи согласны друг с другом в определении патологии.
Выполним оценку качества разметки каждого врача по патологии¶
focus_corr = focus.corr(method='spearman')
filtered = focus_corr.where(focus_corr > 0.5).abs()
Как видно из таблицы корреляции не так много пар врачей, у которых решения по поводу потологий совпадают хотя бы на 50 процентов. Наблюдается слабая коррелция.
Однако есть две пары врачей(3 и 13)(12 и 10) у которых средня сила корреляции. Это дает понять, что именно эти 2 пары склонны делать одинаковую разметку для большинства снимков. Это может свидетельствовать о высоком уровне согласованности между ними в отношении того, есть ли патология на снимке или нет.
focus_corr_ = focus.corr(method='spearman')
filtered_ = focus_corr_.where(focus_corr < 0.1)
Также есть пары врачей, которые дают совершенно противоположные друг другу ответы, это значит что их мнения почти на 100 процентов разнятся.
focus_corr_mean = focus_corr.mean(axis=1).sort_values(ascending=False)
focus_corr_mean
В табличке выше видна средняя корреляция каждого врача с другими врачами, эта информация дает понять, насколько мнение каждого врача сходится с другими
plt.figure(figsize=(8,8))
sns.heatmap(focus_corr.round(2), annot=True, center=0, cmap='inferno')
plt.title('Корреляция между разметками врачей')
plt.figure(figsize=(8,8))
sns.heatmap(filtered.round(2), annot=True, center=0, cmap='inferno')
plt.title('Корреляция между разметками врачей с средней по силе корреляции')
Посмотрим, сколько врачей на каждый снимок уверены с патологией в процентном соотношении
confidence = focus.sum(axis=1).sort_values(ascending=False) / focus.shape[1] * 100
confidence.head(20)
Таким образом в табличке выше можно наблюдать вероятность в процентах на снимке соответствующего ID патологию с учетом решения врачей.
average_marking = focus.mean(axis=1)
average_doctor_deviation = focus.sub(average_marking, axis=0).abs().mean().sort_values(ascending=False)
average_doctor_deviation
Врач №15 имеет самое высокое среднее отклонение (0.2457), что говорит о том, что его разметки значительно отличаются от усредненной разметки по всем врачам. Это может указывать на возможные ошибки в разметке.
Врач №3 имеет самое низкое среднее отклонение (0.1069), что указывает на высокую согласованность его разметок с усредненной разметкой по всем врачам. Этот врач чаще всего разметил снимки так же, как и большинство других врачей.
Таким образом тут было получено среднее отклонение от усредненной по всем врачам разметке
Далее рассчитваем ковариационную матрицу для оценок врачей
covariance_matrix = focus.cov()
covariance_matrix
covariance_matrix_mean = covariance_matrix.mean(axis=1).sort_values(ascending=False)
covariance_matrix_mean
Врач №15 имеет самую высокую среднюю ковариацию (0.0525). Это говорит о том, что разметка Врача №15 в среднем более согласованна с разметками других врачей. Этот врач чаще всего отмечает наличие или отсутствие патологии так же, как и другие врачи.
Врач №8 имеет самую низкую среднюю ковариацию (0.0101). Это указывает на то, что разметка Врача №8 в среднем менее согласованна с разметками других врачей. Этот врач чаще всего оценивает наличие или отсутствие патологии иначе, чем другие врачи.
Видно, что у врача № 15 самая высокая средняя ковариация и самое высокое среднее отклонение. Это может быть связано с тем, что мнение насчет патологии на снимке у врача сходится с мнением большинства, однако он чаще чем другие может замечать скрытые особенности, немного завышать оценки.
doctor_13 = focus['Врач№13']
doctor_3 = focus['Врач№3']
doctor_12 = focus['Врач№12']
doctor_10 = focus['Врач№10']
cohen_kappa_score(doctor_13, doctor_3), cohen_kappa_score(doctor_12, doctor_10)
Что подтверждает корреляционную таблицу о самых высокосогласованных парах по оценкам снимков.
counts = np.zeros((focus.shape[0], 2), dtype=int)
for idx, row in focus.iterrows():
counts[idx, 0] = (row == 0).sum()
counts[idx, 1] = (row == 1).sum()
fleiss_kappa(counts)
Этот тест показывает согласованность по аннотациям всех врачей. Как видно из результата, согласованность невысокая, у врачей возникают разногласия в наличии патологии.
kappa_scores = pd.DataFrame(focus.columns, focus.columns)
for col in focus.columns:
for col_ in focus.columns:
kappa_scores.loc[col, col_] = cohen_kappa_score(focus[col], focus[col_])
kappa_scores = kappa_scores.drop([0], axis=1)
Благодаря данной табличке можно узнать какие врачи больше согласны друг с другом
kappa_scores_mean = kappa_scores.mean(1).sort_values(ascending=False)
kappa_scores_mean
Из данной таблички видно, что врач номер 15 имеет одну из самых низких средних согласованносетй по врачам.
Коэффициент Каппа Коэна в отличии от таблицы корреляции регулирует случайные совпадения
Это может быть связано с тем, что врач часто соглашается с другими врачами, но он ставит больше положительных или отрицательных диагнозов
Рассчитаем среднеквадратическое отклонение результатов врача от усредненной по врачам
standard_deviation_mean=focus.sub(focus.mean(axis=1), axis=0).pow(2).mean().sort_values(ascending=False)
standard_deviation_mean
В данном случае MSE от усредненной по врачам помогает понять у каких врачей самое высокое отклонение, кто чаще всего дает несогласованные с другими врачами оценки для конкретного снимка
std_mean=focus.std().sort_values(ascending=False)
std_mean
СКО помогает видеть врачей, у которых наблюдается самый высокий разброс
Далее получим "Эталонную оценку" на каждый снимок, которая будет рассчитываться по сумме голосов, то есть, если большая часть врачей за то, что есть патология, то ставим 1, в противном случае - 0.
diagnosis_major = pd.Series([0 if x > y else 1 for x, y in zip(counts[:, 0], counts[:, 1])])
accuracy_df = focus.apply(lambda x: accuracy_score(diagnosis_major, x)).sort_values(ascending=False)
accuracy_df
В табличке выше получили точность оценков снимков врачей в сравнений с эталонными оценками, которые были получены на основе мажоритарного концепта, то есть чем больше у конкретного снимка подтверждений диагнозов врачей, тем выше вероятность того, что это действительно патология
recall_df = focus.apply(lambda x: recall_score(diagnosis_major, x)).sort_values(ascending=False)
recall_df
С помощью полноты(recall) можно оценить, насколько часто врач правильно идентифицирует наличие патологий на снимках.
columns = focus.columns
confusion_matrices = {}
for column in columns:
confusion_matrix_ = confusion_matrix(diagnosis_major, focus[column])
confusion_matrices[column] = confusion_matrix_.sum() - np.diag(confusion_matrix_).sum()
errors_df = pd.DataFrame(list(confusion_matrices.items()), columns=['Врач', 'Количество ошибок']).sort_values(ascending=False, by='Количество ошибок')
errors_df
Посчитав FP + FN можем видеть, у кого из врачей больше всего ошибок
counts[:, 0].sum(), counts[:, 1].sum()
(6316, 839) - сумарное количество диагнозов врачей. 6316 - количество голосов врачей за отстутствие патологии, 839 - наличие. Как видно, имеется небольшой дисбаланс классов, попробуем применить accuracy_balanced из sklearn
balanced_df = focus.apply(lambda x: balanced_accuracy_score(diagnosis_major, x)).sort_values(ascending=False)
balanced_df
Построим графики, наглядно отображающие информацию о том, какой врач лучше всего справлялся с диагностикой. Всего было рассчитано 10 метрик, построем 10 графиков
colors = ['blue', 'green', 'red', 'purple', 'orange', 'yellow', 'brown', 'pink', 'gray', 'cyan', 'magenta', 'lime', 'teal', 'navy']
fig, axes = plt.subplots(nrows=5, ncols=2,figsize=(13, 24))
axes = axes.flatten()
axes[0].bar(focus_corr_mean.index.tolist(), focus_corr_mean.values.tolist(), color=colors[0])
axes[0].set_title('Средняя корреляция по врачам')
axes[0].set_xlabel('Врач')
axes[0].set_ylabel('Средняя корреляция')
axes[0].tick_params(axis='x', rotation=45)
axes[1].bar(average_doctor_deviation.index.tolist(), average_doctor_deviation.values.tolist(), color=colors[1])
axes[1].set_title('Среднее отклонение по врачам')
axes[1].set_xlabel('Врач')
axes[1].set_ylabel('Среднее отклонение')
axes[1].tick_params(axis='x', rotation=45)
axes[2].bar(covariance_matrix_mean.index.tolist(), covariance_matrix_mean.values.tolist(), color=colors[2])
axes[2].set_title('Средняя ковариация по врачам')
axes[2].set_xlabel('Врач')
axes[2].set_ylabel('Средняя ковариация')
axes[2].tick_params(axis='x', rotation=45)
axes[3].bar(kappa_scores_mean.index.tolist(), kappa_scores_mean.values.tolist(), color=colors[3])
axes[3].set_title('Средняя оценка по Капо-Коэно по врачам')
axes[3].set_xlabel('Врач')
axes[3].set_ylabel('Средняя оценка по Капо-Коэно')
axes[3].tick_params(axis='x', rotation=45)
axes[4].bar(standard_deviation_mean.index.tolist(), standard_deviation_mean.values.tolist(), color=colors[4])
axes[4].set_title('Среднеквадратическое отклонение по врачам')
axes[4].set_xlabel('Врач')
axes[4].set_ylabel('Среднеквадратическое отклонение')
axes[4].tick_params(axis='x', rotation=45)
axes[5].bar(std_mean.index.tolist(), std_mean.values.tolist(), color=colors[5])
axes[5].set_title('Среднее СКО по врачам')
axes[5].set_xlabel('Врач')
axes[5].set_ylabel('Среднее СКО')
axes[5].tick_params(axis='x', rotation=45)
axes[6].bar(accuracy_df.index.tolist(), accuracy_df.values.tolist(), color=colors[6])
axes[6].set_title('Мажоритарная точность по врачам')
axes[6].set_xlabel('Врач')
axes[6].set_ylabel('Мажоритарная точность')
axes[6].tick_params(axis='x', rotation=45)
axes[7].bar(recall_df.index.tolist(), recall_df.values.tolist(), color=colors[7])
axes[7].set_title('Мажоритарная полнота по врачам')
axes[7].set_xlabel('Врач')
axes[7].set_ylabel('Мажоритарная полнота')
axes[7].tick_params(axis='x', rotation=45)
axes[8].bar(errors_df['Врач'], errors_df['Количество ошибок'], color=colors[8])
axes[8].set_title('Ошибки по врачам')
axes[8].set_xlabel('Врач')
axes[8].set_ylabel('ошибки по полнота')
axes[8].tick_params(axis='x', rotation=45)
axes[9].bar(balanced_df.index.tolist(), balanced_df.values.tolist(), color=colors[9])
axes[9].set_title('Мажоритарная сбалансированная точность по врачам')
axes[9].set_xlabel('Врач')
axes[9].set_ylabel('Мажоритарная сбалансированная точность')
axes[9].tick_params(axis='x', rotation=45)
plt.subplots_adjust(hspace=0.6)
plt.tight_layout()
Проведем анализ графиков: врачи, которые имеют высокую среднюю корреляцию по врачам чаще всего имеют такое же мнение насчет снимка, как и другие врачи. Высокая ковариация отвечает за изменчиваость, то есть чем выше этот параметр, тем сильнее отличаются результаты конкретного врача от мнения всех врачей по снимку. Оценка по Копа-Коэно помогает уловить согнасность врача с решениями других врачей, эта оценка, в отличии от средней корреляции, учитывает случайность врачебных решений.
Среднее отклонение, среднеквадратическое отклонение помогает видеть, насколько сильно от среднего мнения врачей отклоняется мнение конкретного врача, а вот СКО дает понять численно разброс по средним оценкам врачам, то есть грубо говоря насколько далеко мнение врача от среднего.
Была посчитанна мажоритарная оценка каждого снимка, то есть на основе мнения большиснва оценивалась патология. Будем считать это эталоном. Точность показывает, насколько хорошо врач может правильно заключать диагноз(отсутсивие и наличие патологии), а вот полнота показывает, насколько врач хорошо находит патологию в случаях, когда она действительно есть. Сбалансированная точность рассчитывалась в условиях, что предсказанных врачами случаев, когда нет патологии в несколько раз больше, чем когда она есть, то есть небольшой дисбаланс
fig, axes = plt.subplots(figsize=(7, 7))
axes.bar(accuracy_df.index.tolist(), accuracy_df.values.tolist(), color=colors[6], alpha=1, label='Мажоритарная точность')
axes.set_title('Мажоритарная точность и сбалансированная точность по врачам')
axes.set_xlabel('Врач')
axes.set_ylabel('Мажоритарная точность, сбалансированная точность')
axes.tick_params(axis='x', rotation=45)
axes.bar(balanced_df.index.tolist(), balanced_df.values.tolist(), color=colors[9], alpha=0.5, label='Мажоритарная сбалансированная точность')
axes.legend()
axes.set_ylim(0, 1.25)
plt.tight_layout()
plt.figure(figsize=(8, 6))
sns.boxplot(data=confidence, orient='v', color='lightblue')
plt.ylabel('Вероятность наличия патологии (%)')
plt.title('Распределение вероятностей наличия патологии')
plt.tight_layout()
plt.show()
Как видно из графика выше, большая часть снимков не имеет 100 процентой гарантии того, что там есть патология. Только на одном снимке все врачи сказали, что есть патология
Попробуем создать единую метрику, которая объединяет все вышеперечисленные
Отберем худших врачей по каждой из патологий.¶
metrics_df = pd.concat([focus_corr_mean, average_doctor_deviation, covariance_matrix_mean, kappa_scores_mean, standard_deviation_mean, std_mean, accuracy_df, recall_df, balanced_df, errors_df.set_index('Врач')['Количество ошибок']], axis=1)
metrics_df.columns=['focus_corr_mean', 'average_doctor_deviation', 'covariance_matrix_mean', 'kappa_scores_mean', 'standard_deviation_mean', 'std_mean', 'accuracy_df', 'recall_df', 'balanced_df', 'errors_df']
metrics_df
Отнормируем метрики
scaler = MinMaxScaler()
normalized_df = pd.DataFrame(scaler.fit_transform(metrics_df), index=metrics_df.index, columns=metrics_df.columns)
normalized_df
Зададим весокой коэффициент каждой из метрик
weights = {
'kappa_scores_mean': 0.3,
'covariance_matrix_mean': -0.2,
'average_doctor_deviation': -0.25,
'focus_corr_mean': 0.25,
'balanced_accuracy': 0.25,
'recall': 0.25,
'accuracy': 0.25,
'std_mean': -0.15,
'standard_deviation_mean': -0.10,
'errors_df': -0.2
}
Посчитаем комбинированную метрику, как перемножение весовых коэффициентов на нормализованные метрики, просумировах их
normalized_df['final_score'] = (normalized_df * pd.Series(weights)).sum(axis=1)
normalized_df['final_score'].sort_values(ascending=False)
Таким образом, на основе существующих метрик, написали свою метрику, рассчитали ее и получили "рейтинг" врачей
plt.figure(figsize=(10, 6))
sns.barplot(x=normalized_df['final_score'].sort_values(ascending=False).values, y=normalized_df['final_score'].sort_values(ascending=False).index, palette='coolwarm')
plt.title('Рейтинг врачей')
plt.xlabel('Рейтинг')
plt.ylabel('Врач')
plt.show()
Как видно из графика выше, самые худшие врачи - 15, 2, 8, 6, 7
Сформируем итоговую разметку и посчитаем уверенность врачей¶
Можно учитывать суммарный рейтинг врачей, то есть если 14 из 15 врачей поставили 1, то патология есть и т.д. Однако можно учитывать рейтинг врачей.
Рассчитаем итоговые диагнозв
votes = focus.sum(axis=1)
f = focus * normalized_df['final_score']
final_votes = (f.sum(1) > 0.4).astype(int)
confidence__ = focus.sum(axis=1)
weights_ = {
'doctor' : 0.4,
'confidence': 0.6
}
Diagnosis_weights = ((confidence__ / focus.shape[1]) * weights_['confidence'] + (f.sum(1) > 0.4).astype(int) * weights_['doctor'])
Diagnosis = (Diagnosis_weights >= 0.36).astype(int)
Diagnosis.sum()
Вычислим уверенность врачей
normalized_diagnosis_weights = (Diagnosis_weights - np.min(Diagnosis_weights)) / (np.max(Diagnosis_weights) - np.min(Diagnosis_weights))
confidence = normalized_diagnosis_weights * 100
confidence.sort_values(ascending=False)
Посчитаем доверительный интервал
mean = np.mean(Diagnosis_weights)
std_err = sem(Diagnosis_weights)
confidence_level = 0.95
n = len(Diagnosis_weights)
h = std_err * t.ppf((1 + confidence_level) / 2, n - 1)
lower_bound = mean - h
upper_bound = mean + h
# Вывод доверительного интервала
print(f"Доверительный интервал: [{lower_bound}, {upper_bound}]")
Проанализируем полученные данные.¶
В pd.Series Diagnosis содержатся итоговые метки о том, какой точно диагноз у конкретного снимка. Диагноз вычислялся на основе того, какой рейтинг у врачей, также учитывалась вероятность того, есть ли патология. Эта вероятность для конкретного снимка рассчитывалась как среднее меток врачей на снимке.
Итоговая вероятность, есть ли патология на снимке, на основе приведенных данных выше, рассчитывалась как взвешенная сумма из приведенных выше составляющих Рейтинг врачей при расчете учитывался с весом 0.4, вес вероятности на основе среднего - 0.6. Такие веса были выбраны в связи с тем, что если 14 из 15 врачей подтвердят что нет патологии, а 1 врач с самым высоких рейтингом, например 0.4 скажет что есть патология, то взвешенная сумма будет высокая и будет ложноположительный результат. normalized_diagnosis_weights - взвешенная сумма.
normalized_diagnosis_weights - уверенность того, есть ли на снимке патология. Она рассчитывалась. На основе этой уверенности выбиралась итоговая метка для снимка.
Я взял порог 0.4, то есть если вес диагноза больше чем 0.4, то патология есть. Итог: выявлено 25 снимков с патологией.
Также посчитан доверительный интервал для уверенностей диагнозов, с 95% вероятностью большенство вероятностей находится в [0.07206422989620075, 0.1021915353029607], то есть итоговый диагноз для большинства снимков - 0
df = pd.concat([Diagnosis, confidence], axis=1)
df.columns = ['Diagnosis', 'Probability, %']
df['Probability, %'] = df.apply(lambda row: 100 - row['Probability, %'] if row['Diagnosis'] == 0 else row['Probability, %'], axis=1)
df
В датафрейме выше содержится итоговый диагноз и вероятность в процентах того, что диагноз действительно верно выбран. Для каждого снимка прописан диагноз и вероятность того, что диагноз верен.
Оценим итоговое качество разметки по каждой из патологий.¶
mean_probability = np.mean(Diagnosis_weights)
print(f"Средняя вероятность: {mean_probability}")
# Стандартное отклонение вероятностей
std_deviation = np.std(Diagnosis_weights)
print(f"Стандартное отклонение вероятностей: {std_deviation}")
# Максимальная и минимальная вероятность
max_probability = np.max(Diagnosis_weights)
min_probability = np.min(Diagnosis_weights)
print(f"Максимальная вероятность: {max_probability}")
print(f"Минимальная вероятность: {min_probability}")
Как видно, большая часть диагнозов - отрицательная, то есть нет патологии.
accuracy_d = accuracy_score(diagnosis_major, Diagnosis)
recall_d = recall_score(diagnosis_major, Diagnosis)
balanced_accuracy_d = balanced_accuracy_score(diagnosis_major, Diagnosis)
F1_d = f1_score(diagnosis_major, Diagnosis)
fbeta_d = fbeta_score(diagnosis_major, Diagnosis, beta=0.5)
roc_auc_d = roc_auc_score(diagnosis_major, Diagnosis)
print(f"Точность: {accuracy_d}")
print(f"Полнота: {recall_d}")
print(f"Сбалансированная Точность: {balanced_accuracy_d}")
print(f"F1: {F1_d}")
print(f"Fbeta: {fbeta_d}")
print(f"roc-auc: {roc_auc_d}")
Высокая точность (96.2%) указывает на то, что большинство предсказаний правильны.
Полнота (62.1%) указывает на то, что пропускаются некоторые положительные случаи.
Сбалансированная точность (80.3%) и ROC-AUC (80.3%) - хорошая способность различать классы.
F1-score (66.7%) и Fbeta-score (69.8%) указывают на сбалансированное качество итоговых диагнозов
confusion_matrix_ = confusion_matrix(diagnosis_major, Diagnosis)
disp = ConfusionMatrixDisplay(confusion_matrix_)
disp.plot(cmap=plt.cm.Blues)
plt.title('Confusion Matrix')
матрица ошибок дает понять, что не так много неверно определенных диагнозов.
precision, recall, _ = precision_recall_curve(diagnosis_major, Diagnosis)
average_precision = average_precision_score(diagnosis_major, Diagnosis)
plt.figure()
plt.step(recall, precision, where='post', color='b', alpha=0.2, linestyle='-', linewidth=2, label='Precision-Recall curve')
plt.fill_between(recall, precision, step='post', alpha=0.2, color='b')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall curve: AP={0:0.2f}'.format(average_precision))
plt.legend(loc="lower left")
Как видно из графика выше, точность совсем немного преобладает над полнотой, однако как видно из графика precision немного ниже accuracy, что говорит о неточных положительных диагнозах.
Как видно из оценивания качества разметки выше, что разметка достаточно неплохо выполнена, есть небольшие пробелы. Однако стоит учиытвать тот факт, что "Эталонные метки", так сказать targetы, вычесленные как средняя оценка по врачам, не совсем точно могут отражать действительное. Это связано с тем, что некоторые врачи имеют низкий рейтинг, и даже среднее значение может быть неточным. Однако стоит учитывать тот факт, что у врачей хоть и невысокая, но согласованность наблюдается, что может говорить о правильности согласованности итоговой разметки.
Выводы¶
В данном примере был проведен анализ заключений врачей с помощью методов аналитики данных