Прогнозирование повторной госпитализации с помощью выписки.

Эндрю Лонг был научным сотрудником Insight Health Data в Бостоне летом 2017 года. Сейчас он работает специалистом по анализу данных в Fresenius Medical Care North America, медицинской компании, предоставляющей диализ 200 000 пациентов в США. В этом руководстве он знакомит с использованием обработки естественного языка для прогнозного моделирования в здравоохранении.

Изначально опубликовано в На пути к науке о данных.

Врачи всегда составляли клинические заметки о своих пациентах - первоначально записи были на бумаге и были заперты в шкафу. К счастью для специалистов по данным, теперь врачи вносят свои записи в электронную медицинскую карту. Эти заметки представляют собой обширный объем знаний и идей, которые можно использовать для прогнозных моделей с использованием обработки естественного языка (NLP) для улучшения ухода за пациентами и рабочего процесса в больнице. В качестве примера я покажу вам, как спрогнозировать повторную госпитализацию с помощью сводок о выписке.

Эта статья предназначена для людей, интересующихся наукой о данных в сфере здравоохранения. После прохождения этого руководства вы узнаете

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

Недавно я прочитал эту замечательную статью Масштабируемое и точное глубокое обучение для электронных медицинских карт от Rajkomar et al. Авторы построили множество современных моделей глубокого обучения с данными больниц для прогнозирования внутрибольничной смертности (AUC = 0,93–0,94), 30-дневной незапланированной повторной госпитализации (AUC = 0,75–76), продолжительной продолжительности пребывания (AUC = 0,75–76). = 0,85–0,86) и диагнозы при выписке (AUC = 0,90). AUC - это показатель эффективности обработки данных (подробнее об этом ниже), где чем ближе к 1, тем лучше. Понятно, что прогнозирование повторного допуска - самая сложная задача, поскольку у него более низкий AUC. Мне было любопытно, насколько хороша модель, которую мы можем получить, если использовать краткие сводки с произвольным текстом с простой прогнозной моделью.

Если вы хотите следить за кодом Python в Jupyter Notebook, не стесняйтесь загружать код с моего github.

Определение модели

В этом сообщении блога будет описано, как построить модель классификации, чтобы предсказать, какие пациенты подвержены риску 30-дневной незапланированной повторной госпитализации, с использованием текстовых сводок выписки из больницы.

Набор данных

Мы будем использовать базу данных MIMIC-III (Medical Information Mart for Intensive Care III). Эта удивительная бесплатная база данных больниц содержит обезличенные данные о более чем 50 000 пациентов, которые были госпитализированы в Медицинский центр Бет Исраэль Дьяконисса в Бостоне, штат Массачусетс, с 2001 по 2012 год. Чтобы получить доступ к данным для этого проекта, вам необходимо запросить доступ. по этой ссылке (https://mimic.physionet.org/gettingstarted/access/).

В этом проекте мы будем использовать следующие таблицы MIMIC III

  • ADMISSIONS - таблица с датами поступления и выписки (имеет уникальный идентификатор HADM_ID для каждого приема)
  • NOTEEVENTS - содержит все примечания для каждой госпитализации (ссылки на HADM_ID)

Для сохранения анонимности все даты для каждого пациента сдвинуты далеко в будущее, но время между двумя последовательными событиями для пациента сохраняется в базе данных. Это важно, так как позволяет сохранить время между двумя госпитализациями конкретного пациента.

Поскольку это ограниченный набор данных, я не могу публиковать необработанные данные пациентов. В результате я буду показывать вам только искусственные данные об одном пациенте или агрегированные описания.

Шаг 1. Подготовьте данные для проекта машинного обучения

Мы выполним следующие шаги, чтобы подготовить данные из таблиц MIMIC ADMISSIONS и NOTEEVENTS для нашего проекта машинного обучения.

Сначала мы загружаем таблицу допуска, используя фреймы данных pandas:

# set up notebook
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# read the admissions table
df_adm = pd.read_csv('ADMISSIONS.csv')

Основные интересующие нас столбцы в этой таблице:

  • SUBJECT_ID: уникальный идентификатор для каждой темы
  • HADM_ID: уникальный идентификатор для каждой госпитализации
  • ADMITTIME: дата поступления в формате ГГГГ-ММ-ДД чч: мм: сс
  • DISCHTIME: дата выписки в том же формате
  • DEATHTIME: время смерти (если существует) в том же формате
  • ADMISSION_TYPE: включает ELECTIVE, EMERGENCY, NEWBORN, URGENT

Следующим шагом является преобразование дат из их строкового формата в формат даты и времени. Мы используем флаг errors = ‘coerce’, чтобы разрешить пропущенные даты.

# convert to dates
df_adm.ADMITTIME = pd.to_datetime(df_adm.ADMITTIME, format = '%Y-%m-%d %H:%M:%S', errors = 'coerce')
df_adm.DISCHTIME = pd.to_datetime(df_adm.DISCHTIME, format = '%Y-%m-%d %H:%M:%S', errors = 'coerce')
df_adm.DEATHTIME = pd.to_datetime(df_adm.DEATHTIME, format = '%Y-%m-%d %H:%M:%S', errors = 'coerce')

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

# sort by subject_ID and admission date
df_adm = df_adm.sort_values(['SUBJECT_ID','ADMITTIME'])
df_adm = df_adm.reset_index(drop = True)

Теперь для одного пациента фрейм данных может выглядеть так:

Мы можем использовать оператор группового сдвига, чтобы получить следующее разрешение (если оно существует) для каждого SUBJECT_ID.

# add the next admission date and type for each subject using groupby
# you have to use groupby otherwise the dates will be from different subjects
df_adm['NEXT_ADMITTIME'] = df_adm.groupby('SUBJECT_ID').ADMITTIME.shift(-1)
# get the next admission type
df_adm['NEXT_ADMISSION_TYPE'] = df_adm.groupby('SUBJECT_ID').ADMISSION_TYPE.shift(-1)

Обратите внимание, что последнее зачисление не имеет следующего зачисления.

Но мы хотим предсказать НЕПЛАНИРУЕМЫЕ повторные приемы, поэтому мы должны отфильтровать ЭЛЕКТИВНЫЕ следующие приемы.

# get rows where next admission is elective and replace with naT or nan
rows = df_adm.NEXT_ADMISSION_TYPE == 'ELECTIVE'
df_adm.loc[rows,'NEXT_ADMITTIME'] = pd.NaT
df_adm.loc[rows,'NEXT_ADMISSION_TYPE'] = np.NaN

И затем заполните значения, которые мы удалили

# sort by subject_ID and admission date
# it is safer to sort right before the fill in case something changed the order above
df_adm = df_adm.sort_values(['SUBJECT_ID','ADMITTIME'])
# back fill (this will take a little while)
df_adm[['NEXT_ADMITTIME','NEXT_ADMISSION_TYPE']] = df_adm.groupby(['SUBJECT_ID'])[['NEXT_ADMITTIME','NEXT_ADMISSION_TYPE']].fillna(method = 'bfill')

Затем мы можем рассчитать количество дней до следующего приема.

df_adm['DAYS_NEXT_ADMIT']=  (df_adm.NEXT_ADMITTIME - df_adm.DISCHTIME).dt.total_seconds()/(24*60*60)

В нашей базе данных с 58976 госпитализациями 11399 повторных госпитализаций. Для тех, у кого есть повторная госпитализация, мы можем построить гистограмму дней между поступлениями.

Теперь мы готовы работать с NOTEEVENTS.csv.

df_notes = pd.read_csv("NOTEEVENTS.csv")

Основные интересующие нас столбцы:

  • SUBJECT_ID
  • HADM_ID
  • КАТЕГОРИЯ: включает «Сводку выписки», «Эхо», «ЭКГ», «Уход», «Врач», «Реабилитационные услуги», «Ведение пациентов», «Респираторные органы», «Питание», «Общие», «Социальная работа». , "Аптека", "Консультация", "Радиология",
    "Сестринское дело / другое"
  • ТЕКСТ: наша колонка клинических примечаний

Поскольку я не могу показать отдельные заметки, я просто опишу их здесь. Набор данных содержит 2 083 180 строк, что указывает на то, что на одну госпитализацию приходится несколько заметок. В примечаниях даты и PHI (имя, врач, местонахождение) преобразованы для обеспечения конфиденциальности. Также есть специальные символы, такие как \ n (новая строка), числа и знаки препинания.

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

# filter to discharge summary
df_notes_dis_sum = df_notes.loc[df_notes.CATEGORY == 'Discharge summary']

Поскольку следующим шагом является объединение примечаний в таблице приема, у нас может быть предположение, что для каждого приема существует одна сводка о выписке, но мы, вероятно, должны это проверить. Мы можем проверить это с помощью оператора assert, который завершается ошибкой.

На этом этапе вы можете выяснить, почему существует несколько сводок, но для простоты давайте просто воспользуемся последней.

df_notes_dis_sum_last = (df_notes_dis_sum.groupby(['SUBJECT_ID','HADM_ID']).nth(-1)).reset_index()
assert df_notes_dis_sum_last.duplicated(['HADM_ID']).sum() == 0, 'Multiple discharge summaries per admission'

Теперь мы готовы объединить таблицы допуска и заметок. Я использую слияние слева, чтобы учесть отсутствие заметок. Есть много случаев, когда вы получаете несколько строк после слияния (хотя мы уже разобрались с этим выше), поэтому я люблю добавлять утверждения assert после слияния.

df_adm_notes = pd.merge(df_adm[['SUBJECT_ID','HADM_ID','ADMITTIME','DISCHTIME','DAYS_NEXT_ADMIT','NEXT_ADMITTIME','ADMISSION_TYPE','DEATHTIME']],
                        df_notes_dis_sum_last[['SUBJECT_ID','HADM_ID','TEXT']], 
                        on = ['SUBJECT_ID','HADM_ID'],
                        how = 'left')
assert len(df_adm) == len(df_adm_notes), 'Number of rows increased'

10,6% заявлений отсутствуют (df_adm_notes.TEXT.isnull().sum() / len(df_adm_notes)), поэтому я немного изучил

df_adm_notes.groupby('ADMISSION_TYPE').apply(lambda g: g.TEXT.isnull().sum())/df_adm_notes.groupby('ADMISSION_TYPE').size()

и обнаружил, что в 53% поступивших НОВОРОЖДЕННЫХ отсутствовали выписки по выписке по сравнению с ~ 4% для остальных. На этом этапе я решил удалить НОВОРОЖДЕННЫХ. Скорее всего, у этих пропавших без вести НОВОРОЖДЕННЫХ есть сводка выписки, хранящаяся вне набора данных MIMIC.

По этой проблеме мы собираемся классифицировать, будет ли пациент госпитализирован в ближайшие 30 дней. Следовательно, нам нужно создать переменную с меткой вывода (1 = повторно принято, 0 = не принято).

df_adm_notes_clean['OUTPUT_LABEL'] = (df_adm_notes_clean.DAYS_NEXT_ADMIT < 30).astype('int')

Быстрый подсчет положительных и отрицательных результатов в 3004 положительных образцах, 48109 отрицательных образцах. Это указывает на то, что у нас есть несбалансированный набор данных, что является обычным явлением в науке о данных в сфере здравоохранения.

Последний шаг в подготовке наших данных - разделить данные на наборы для обучения, проверки и тестирования. Для воспроизводимости результатов я сделал random_state всегда 42.

# shuffle the samples
df_adm_notes_clean = df_adm_notes_clean.sample(n = len(df_adm_notes_clean), random_state = 42)
df_adm_notes_clean = df_adm_notes_clean.reset_index(drop = True)
# Save 30% of the data as validation and test data 
df_valid_test=df_adm_notes_clean.sample(frac=0.30,random_state=42)
df_test = df_valid_test.sample(frac = 0.5, random_state = 42)
df_valid = df_valid_test.drop(df_test.index)
# use the rest of the data as training data
df_train_all=df_adm_notes_clean.drop(df_valid_test.index)

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

  • подвыборка негативов
  • избыточная выборка положительных результатов
  • создавать синтетические данные (например, SMOTE)

Поскольку я не налагал никаких ограничений на размер ОЗУ для вашего компьютера, мы проведем частичную выборку негативов, но я рекомендую вам попробовать другие методы, если ваш компьютер или сервер может справиться с этим, чтобы увидеть, сможете ли вы добиться улучшения. . (Отправьте комментарий ниже, если попробуете это!)

# split the training data into positive and negative
rows_pos = df_train_all.OUTPUT_LABEL == 1
df_train_pos = df_train_all.loc[rows_pos]
df_train_neg = df_train_all.loc[~rows_pos]
# merge the balanced data
df_train = pd.concat([df_train_pos, df_train_neg.sample(n = len(df_train_pos), random_state = 42)],axis = 0)
# shuffle the order of training samples 
df_train = df_train.sample(n = len(df_train), random_state = 42).reset_index(drop = True)

Шаг 2. Предварительная обработка неструктурированных заметок с использованием набора слов.

Теперь, когда мы создали наборы данных с меткой и примечаниями, нам нужно предварительно обработать наши текстовые данные, чтобы преобразовать их во что-то полезное (например, числа) для модели машинного обучения. Мы собираемся использовать подход «мешок слов» (BOW).

BOW в основном разбивает заметку на отдельные слова и считает, сколько раз встречается каждое слово. Ваши числовые данные затем становятся счетчиками для некоторого набора слов, как показано ниже. BOW - это самый простой способ провести классификацию НЛП. В большинстве сообщений в блогах, которые я читал, более изощренным методам трудно превзойти BOW в задачах классификации НЛП.

В этом процессе нужно сделать несколько вариантов.

  • как предварительно обработать слова
  • как считать слова
  • какие слова использовать

Не существует оптимального выбора для всех проектов НЛП, поэтому я рекомендую попробовать несколько вариантов при построении собственных моделей.

Вы можете выполнить предварительную обработку двумя способами

  • изменить исходный столбец TEXT фрейма данных
  • предварительная обработка как часть вашего конвейера, чтобы вы не редактировали исходные данные

Я покажу вам, как сделать и то, и другое, но я предпочитаю второй, поскольку до этого момента потребовалось много работы.

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

def preprocess_text(df):
    # This function preprocesses the text by filling not a number and replacing new lines ('\n') and carriage returns ('\r')
    df.TEXT = df.TEXT.fillna(' ')
    df.TEXT = df.TEXT.str.replace('\n',' ')
    df.TEXT = df.TEXT.str.replace('\r',' ')
    return df
# preprocess the text to deal with known issues
df_train = preprocess_text(df_train)
df_valid = preprocess_text(df_valid)
df_test = preprocess_text(df_test)

Другой вариант - предварительная обработка как часть конвейера. Этот процесс состоит из использования токенизатора и векторизатора. Токенизатор разбивает отдельную заметку на список слов, а векторизатор берет список слов и считает слова.

Мы будем использовать word_tokenize из пакета nltk для нашего токенизатора по умолчанию, который в основном разбивает примечание на основе пробелов и некоторых знаков препинания. Пример показан ниже:

import nltk
from nltk import word_tokenize
word_tokenize('This should be tokenized. 02/02/2018 sentence has stars**')

С выходом:

[‘This’, ‘should’, ‘be’, ‘tokenized’, ‘.’, ‘02/02/2018’, ‘sentence’,
 ‘has’, ‘stars**’]

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

  • заменить знаки препинания пробелами
  • заменить числа пробелами
  • строчные буквы все слова
import string
def tokenizer_better(text):
    # tokenize the text by replacing punctuation and numbers with spaces and lowercase all words
    
    punc_list = string.punctuation+'0123456789'
    t = str.maketrans(dict.fromkeys(punc_list, " "))
    text = text.lower().translate(t)
    tokens = word_tokenize(text)
    return tokens

С помощью этого токенизатора мы получаем из нашего исходного предложения

['this', 'should', 'be', 'tokenized', 'sentence', 'has', 'stars']

Дополнительные вещи, которые вы можете сделать, - это лемматизировать или сокращать слова, но это более сложный вопрос, поэтому я пропущу его.

Теперь, когда у нас есть способ конвертировать произвольный текст в токены, нам нужен способ подсчета токенов для каждой сводки разряда. Мы будем использовать встроенный пакет CountVectorizer from scikit-learn. Этот векторизатор просто считает, сколько раз каждое слово встречается в заметке. Существует также TfidfVectorizer , который учитывает, как часто слова используются во всех заметках, но для этого проекта давайте воспользуемся более простым (я получил аналогичные результаты и со вторым).

В качестве примера предположим, что у нас есть 3 заметки

sample_text = ['Data science is about the data', 'The science is amazing', 'Predictive modeling is part of data science']

По сути, вы настраиваете CountVectorizer для изучения слов в ваших данных и преобразования данных для создания счетчиков для каждого слова.

from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer(tokenizer = tokenizer_better)
vect.fit(sample_text)
# matrix is stored as a sparse matrix (since you have a lot of zeros)
X = vect.transform(sample_text)

Матрица X будет разреженной матрицей, но если вы конвертируете ее в массив (X.toarray()), вы увидите это

array([[1, 0, 2, 1, 0, 0, 0, 0, 1, 1],
       [0, 1, 0, 1, 0, 0, 0, 0, 1, 1],
       [0, 0, 1, 1, 1, 1, 1, 1, 1, 0]], dtype=int64)

Где есть 3 строки (так как у нас 3 заметки) и количество каждого слова. Вы можете увидеть имена столбцов с vect.get_feature_names()

['about', 'amazing', 'data', 'is', 'modeling', 'of', 'part', 'predictive', 'science', 'the']

Теперь мы можем разместить наш CountVectorizer в клинических заметках. Важно использовать только обучающие данные, потому что вы не хотите включать какие-либо новые слова, которые появляются в проверочных и тестовых наборах. Существует гиперпараметр max_features, который вы можете установить, чтобы ограничить количество слов, включенных в векторизатор. Это будет использовать первые N наиболее часто используемых слов. На шаге 5 мы настроим это, чтобы увидеть эффект.

# fit our vectorizer. This will take a while depending on your computer.
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer(max_features = 3000, tokenizer = tokenizer_better)
# this could take a while
vect.fit(df_train.TEXT.values)

Мы можем взглянуть на наиболее часто используемые слова и увидим, что многие из этих слов могут не иметь никакого значения для нашей модели. Эти слова называются стоп-словами, и мы можем легко удалить их (если захотим) с помощью CountVectorizer. Существуют списки общих стоп-слов для разных корпусов НЛП, но мы просто составим свои собственные, основываясь на изображении ниже.

my_stop_words = ['the','and','to','of','was','with','a','on','in','for','name',                 'is','patient','s','he','at','as','or','one','she','his','her','am',                 'were','you','pt','pm','by','be','had','your','this','date',                'from','there','an','that','p','are','have','has','h','but','o',                'namepattern','which','every','also']

Не стесняйтесь добавлять свои собственные стоп-слова, если хотите.

from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer(max_features = 3000, 
                       tokenizer = tokenizer_better, 
                       stop_words = my_stop_words)
# this could take a while
vect.fit(df_train.TEXT.values)

Теперь мы можем преобразовать наши заметки в числовые матрицы. На данный момент я буду использовать только данные обучения и проверки, поэтому у меня еще нет соблазна посмотреть, как это работает с тестовыми данными.

X_train_tf = vect.transform(df_train.TEXT.values)
X_valid_tf = vect.transform(df_valid.TEXT.values)

Нам также нужны наши выходные метки как отдельные переменные

y_train = df_train.OUTPUT_LABEL
y_valid = df_valid.OUTPUT_LABEL

Судя по расположению полосы прокрутки… как всегда, подготовка данных для прогнозной модели занимает 80% времени.

Шаг 3. Создайте простую прогностическую модель

Теперь мы можем построить простую прогностическую модель, которая принимает наш набор слов и предсказывает, будет ли пациент повторно госпитализирован через 30 дней (ДА = 1, НЕТ = 0).

Здесь мы будем использовать модель логистической регрессии. Логистическая регрессия - хорошая базовая модель для задач НЛП, поскольку она хорошо работает с разреженными матрицами и поддается интерпретации. У нас есть несколько дополнительных вариантов (называемых гиперпараметрами), включая C, который является коэффициентом регуляризации и штрафом, который сообщает, как измерить регуляризацию. Регуляризация - это, по сути, методика минимизации переобучения.

# logistic regression
from sklearn.linear_model import LogisticRegression
clf=LogisticRegression(C = 0.0001, penalty = 'l2', random_state = 42)
clf.fit(X_train_tf, y_train)

Мы можем рассчитать вероятность повторного допуска для каждой выборки с подобранной моделью.

model = clf
y_train_preds = model.predict_proba(X_train_tf)[:,1]
y_valid_preds = model.predict_proba(X_valid_tf)[:,1]

Шаг 4. Оцените качество вашей модели

На этом этапе нам нужно измерить, насколько хорошо работает наша модель. Есть несколько различных показателей эффективности обработки данных. Я написал еще один пост в блоге, подробно объясняя это, если вам интересно. Поскольку этот пост уже довольно длинный, я начну просто показывать результаты и цифры. Вы можете увидеть в аккаунте github код для получения цифр.

Для порога 0,5 для прогнозирования положительного результата мы получаем следующую производительность

При текущем выборе гиперпараметров у нас действительно есть некоторые переоснащения. Следует отметить, что основное различие между точностью двух наборов данных связано с тем, что мы сбалансировали обучающий набор, а набор для проверки - это исходное распределение. В настоящее время, если мы составляем список пациентов, которые, по прогнозам, будут повторно приняты, мы поймаем их вдвое больше, чем если бы мы выбрали пациентов случайным образом (ТОЧНОСТЬ против РАСПРОСТРАНЕНИЯ).

Другой показатель производительности, не показанный выше, - это AUC или площадь под кривой ROC. Кривая ROC для нашей текущей модели показана ниже. По сути, кривая ROC позволяет вам увидеть компромисс между истинно положительной скоростью и ложноположительной частотой, когда вы меняете порог того, что вы определяете как прогнозируемое положительное и прогнозируемое отрицательное.

Шаг 5: Следующие шаги по улучшению модели

На этом этапе у вас может возникнуть соблазн подсчитать производительность на вашем тестовом наборе и посмотреть, как вы это сделали. Но ждать! Мы сделали много вариантов (несколько ниже), которые можно было бы изменить и посмотреть, есть ли улучшения:

  • стоит ли тратить время на получение дополнительных данных?
  • как токенизировать - стоит ли использовать стемминг?
  • как векторизовать - надо ли менять количество слов?
  • как упорядочить логистическую регрессию - следует ли изменить C или штраф?
  • какую модель использовать?

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

Для проектов НЛП, использующих логистическую регрессию BOW +, мы можем нанести наиболее важные слова, чтобы увидеть, сможем ли мы получить какое-либо представление. Для этого шага я позаимствовал код из хорошей статьи о НЛП от Insight Data Science. Когда вы смотрите на самые важные слова, я сразу вижу две вещи:

  • Ой! Я забыл исключить пациентов, которые умерли после того, как "истек срок", появились в отрицательном списке. На данный момент я проигнорирую это и исправлю это ниже.
  • Есть и другие стоп-слова, которые нам, вероятно, следует удалить («следует», «если», «это», «было», «кто», «во время», «х»)

Когда мы хотим улучшить модель, мы хотим сделать это на основе данных. Вы можете потратить много времени на «догадки», которые в конечном итоге не оправдаются. Для этого рекомендуется выбрать одну метрику производительности, которую вы будете использовать при принятии решений. Для этого проекта я выберу AUC.

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

Мы видим, что у нас есть некоторые переоснащения, но добавление дополнительных данных, скорее всего, не приведет к радикальному изменению AUC в наборе для проверки. Это полезно знать, потому что это означает, что нам не нужно тратить месяцы на получение дополнительных данных.

Некоторые простые вещи, которые мы можем сделать, - это попытаться увидеть эффект некоторых из наших гиперпараметров (max_features и C). Мы могли бы запустить поиск по сетке, но поскольку здесь у нас всего 2 параметра, мы можем посмотреть на них по отдельности и увидеть эффект.

Мы видим, что увеличение C и max_features приводит к довольно быстрой переобучению модели. Я выбрал C = 0,0001 и max_features = 3000, где набор проверки начал выходить на плато.

На этом этапе вы можете попробовать еще несколько вещей.

  • изменить подвыборку на избыточную
  • добавить стемминг или лемматизацию в токенизатор
  • протестировать несколько разных моделей научного набора
  • объединить все примечания вместо последнего отчета о выписке
  • попробуйте метод глубокого обучения, такой как LSTM
  • просмотрите сводки выписки, что вы ошибаетесь

Шаг 6. Доработайте модель и протестируйте ее.

Теперь мы подгоним нашу окончательную модель с выбором гиперпараметров. Мы также исключим пациентов, которые умерли при повторной балансировке.

rows_not_death = df_adm_notes_clean.DEATHTIME.isnull()
df_adm_notes_not_death = df_adm_notes_clean.loc[rows_not_death].copy()
df_adm_notes_not_death = df_adm_notes_not_death.sample(n = len(df_adm_notes_not_death), random_state = 42)
df_adm_notes_not_death = df_adm_notes_not_death.reset_index(drop = True)
# Save 30% of the data as validation and test data 
df_valid_test=df_adm_notes_not_death.sample(frac=0.30,random_state=42)
df_test = df_valid_test.sample(frac = 0.5, random_state = 42)
df_valid = df_valid_test.drop(df_test.index)
# use the rest of the data as training data
df_train_all=df_adm_notes_not_death.drop(df_valid_test.index)
assert len(df_adm_notes_not_death) == (len(df_test)+len(df_valid)+len(df_train_all)),'math didnt work'
# split the training data into positive and negative
rows_pos = df_train_all.OUTPUT_LABEL == 1
df_train_pos = df_train_all.loc[rows_pos]
df_train_neg = df_train_all.loc[~rows_pos]
# merge the balanced data
df_train = pd.concat([df_train_pos, df_train_neg.sample(n = len(df_train_pos), random_state = 42)],axis = 0)
# shuffle the order of training samples 
df_train = df_train.sample(n = len(df_train), random_state = 42).reset_index(drop = True)
# preprocess the text to deal with known issues
df_train = preprocess_text(df_train)
df_valid = preprocess_text(df_valid)
df_test = preprocess_text(df_test)
my_new_stop_words = ['the','and','to','of','was','with','a','on','in','for','name',              'is','patient','s','he','at','as','or','one','she','his','her','am',                 'were','you','pt','pm','by','be','had','your','this','date',                'from','there','an','that','p','are','have','has','h','but','o',                'namepattern','which','every','also','should','if','it','been','who','during', 'x']
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer(lowercase = True, max_features = 3000, 
                       tokenizer = tokenizer_better,
                      stop_words = my_new_stop_words)
# fit the vectorizer
vect.fit(df_train.TEXT.values)
X_train_tf = vect.transform(df_train.TEXT.values)
X_valid_tf = vect.transform(df_valid.TEXT.values)
X_test_tf = vect.transform(df_test.TEXT.values)
y_train = df_train.OUTPUT_LABEL
y_valid = df_valid.OUTPUT_LABEL
y_test = df_test.OUTPUT_LABEL
from sklearn.linear_model import LogisticRegression
clf=LogisticRegression(C = 0.0001, penalty = 'l2', random_state = 42)
clf.fit(X_train_tf, y_train)
model = clf
y_train_preds = model.predict_proba(X_train_tf)[:,1]
y_valid_preds = model.predict_proba(X_valid_tf)[:,1]
y_test_preds = model.predict_proba(X_test_tf)[:,1]

Это дает следующие результаты и кривую ROC.

Заключение

Поздравляю! Вы построили простую модель НЛП (AUC = 0,70) для прогнозирования повторной госпитализации на основе сводных данных о выписке из больницы, которая лишь немного хуже, чем современный метод глубокого обучения, использующий все данные больницы (AUC = 0,75). Если у вас есть какие-либо отзывы, не стесняйтесь оставлять их ниже.

использованная литература

Масштабируемое и точное глубокое обучение с использованием электронных медицинских карт. Раджкомар А., Орен Э., Чен К. и др. NPJ Цифровая медицина (2018). DOI: 10.1038 / s41746–018–0029–1. Доступно на: https://www.nature.com/articles/s41746-018-0029-1

MIMIC-III, свободно доступная база данных по интенсивной терапии. Johnson AEW, Pollard TJ, Shen L, Lehman L, Feng M, Ghassemi M, Moody B, Szolovits P, Celi LA и Mark RG. Научные данные (2016). DOI: 10.1038 / sdata.2016.35. Доступно на: http://www.nature.com/articles/sdata201635

Заинтересованы в переходе к карьере в области данных? Узнайте больше о программах для стипендиатов Insight Наука о данных, Инжиниринг данных, Данные о здоровье и Искусственный интеллект в Нью-Йорке. , Бостон, Сиэтл и Кремниевая долина, подайте заявку сегодня или подпишитесь на обновления программы.