Давайте сделаем черты для наших моделей по дате и времени!
Даты и время - богатые источники информации, которые можно использовать с моделями машинного обучения. Однако эти переменные datetime требуют некоторой разработки функций, чтобы превратить их в числовые данные. В этом посте я продемонстрирую, как создавать функции datetime со встроенными функциями pandas для ваших моделей машинного обучения.
Когда я преподавал курс машинного обучения в Северо-Восточном университете, некоторые из моих студентов использовали действительно интересный набор данных (неявка на прием к врачу), который я собираюсь изучить в этом сообщении в блоге. Неявка на прием к врачу - большая проблема в здравоохранении, поскольку примерно каждый пятый пациент пропускает прием (источник). Это проблема для всех участников: 1) запланированного пациента, вероятно, нужно осмотреть, иначе его бы не записали, 2) другие пациенты хотели бы иметь это место, но не могли, 3) медицинские работники должны потратить дополнительное время, чтобы связаться с пациентом и изменить его график, а также потраченное впустую время, которое они использовали для подготовки к визиту.
Определение проекта
Учитывая дату и время запланированного дня и дня приема, спрогнозируйте, пропустит ли пациент свое посещение врача.
Набор данных
Здесь мы будем использовать информацию о неявке на прием к врачу, размещенную на Kaggle (https://www.kaggle.com/joniarroba/noshowappointments). Этот набор данных состоит из более чем 110 000 обращений к врачу. Три основных столбца, которые мы будем использовать для этого проекта, - это ScheduledDay (дата и время, когда была назначена встреча), AppointmentDay (дата встречи, без указания времени), No-Show (двоичный флаг, указывающий, не показывались ли они). В целях этого поста мы проигнорируем остальные числовые функции (хотя, честно говоря, они не добавили ценности AUC).
Подготовка данных
Давайте начнем с загрузки нашего набора данных, создания выходного столбца (1 = неявка, 0 = показа) и преобразования нашего времени (в настоящее время это строки) в дату и время Python.
import pandas as pd import numpy as np import matplotlib.pyplot as plt
Здесь я предполагаю, что вы загрузили данные из Kaggle и поместили их в папку «data»:
df = pd.read_csv(‘data/KaggleV2-May-2016.csv’)
Мы можем исследовать столбец «Неявка» с помощью value_counts
Давайте определим двоичный столбец OUTPUT_LABEL, чтобы указать Да = 1, Нет = 0.
df[‘OUTPUT_LABEL’] = (df[‘No-show’] == ‘Yes’).astype(‘int’)
Мы можем проверить распространенность нашего OUTPUT_LABEL:
def calc_prevalence(y): return (sum(y)/len(y))
Это означает, что каждый пятый пациент пропустит назначенный прием.
Теперь давайте поработаем со столбцами datetime, посмотрев на первые 5 строк ScheduledDay и AppointmentDay.
Как видите, тип dtype для обоих столбцов равен object
, что означает, что pandas в настоящее время рассматривает эти значения как строки. Следует также отметить, что в ScheduledDay есть время, в котором в качестве дня встречи все время указано как 00:00:00. Нам, вероятно, следует разобраться с этим, но мне кажется странным, что они не включили время встречи в набор данных. Предположительно, время встречи тоже будет предсказуемым.
Чтобы преобразовать эти строки во внутренние даты, мы можем использовать функцию pandas to_datetime
. Мне нравится использовать параметр формата, чтобы конкретно указать формат. Если вы используете параметр формата, вы должны указать, что делать с ошибками. Здесь я буду делать любые ошибки, превращая их в не дату и время (NaT). В общем (хотя и не в данном случае) использование параметра формата ускоряет выполнение этой строки.
df[‘ScheduledDay’] = pd.to_datetime(df[‘ScheduledDay’], format = ‘%Y-%m-%dT%H:%M:%SZ’, errors = ‘coerce’) df[‘AppointmentDay’] = pd.to_datetime(df[‘AppointmentDay’], format = ‘%Y-%m-%dT%H:%M:%SZ’, errors = ‘coerce’)
Я раньше не видел T и Z в стандартных форматах, поэтому просто помещаю их в строку формата. Если кто-то знает, как с этим лучше справляться, дайте мне знать.
Каждый раз, когда я использую параметр формата и предполагаю, что все строки должны иметь даты, я хотел бы поставить утверждение, чтобы убедиться, что я не получил неправильный формат.
assert df.ScheduledDay.isnull().sum() == 0, ‘missing ScheduledDay dates’ assert df.AppointmentDay.isnull().sum() == 0, ‘missing AppointmentDay dates’
Если вы сейчас проверите dtype, вы увидите, что это datetime64, что мы и хотим, потому что он предоставляет нам все свойства datetime в pandas.
Одна вещь, которую я заметил, заключалась в том, что в настоящее время существует ~ 40 тысяч встреч, запланированных после даты и времени встречи.
Я думаю, это связано с тем, что все время встречи было установлено на самое раннее время (00:00:00), тогда как время включено в ScheduledDay. Чтобы приспособиться к этому, давайте просто перенесем все встречи на конец дня. Если бы я выполнял этот проект для работы, я бы действительно пошел и назначил время встречи.
df[‘AppointmentDay’] = df[‘AppointmentDay’] +pd.Timedelta(‘1d’) — pd.Timedelta(‘1s’)
С этим изменением есть только 5 строк, в которых время запланированного дня находится после дня встречи. Давайте просто отбросим эти строки.
Возможности Engineer Datetime
Преобразуя строки в datetime, это открывает все свойства pandas dt.
Обычно вы можете разделить дату и получить год, месяц, неделю года, день месяца, час, минуту, секунду и т. Д. Вы также можете получить день недели (понедельник = 0, воскресенье = 6). Обратите внимание, будьте осторожны с неделей в году, потому что первых нескольких дней в году может быть 53, если эта неделя начинается в предыдущем году. Давайте применим некоторые из этих свойств к обоим нашим столбцам datetime.
df[‘ScheduledDay_year’] = df[‘ScheduledDay’].dt.year df[‘ScheduledDay_month’] = df[‘ScheduledDay’].dt.month df[‘ScheduledDay_week’] = df[‘ScheduledDay’].dt.week df[‘ScheduledDay_day’] = df[‘ScheduledDay’].dt.day df[‘ScheduledDay_hour’] = df[‘ScheduledDay’].dt.hour df[‘ScheduledDay_minute’] = df[‘ScheduledDay’].dt.minute df[‘ScheduledDay_dayofweek’] = df[‘ScheduledDay’].dt.dayofweek df[‘AppointmentDay_year’] = df[‘AppointmentDay’].dt.year df[‘AppointmentDay_month’] = df[‘AppointmentDay’].dt.month df[‘AppointmentDay_week’] = df[‘AppointmentDay’].dt.week df[‘AppointmentDay_day’] = df[‘AppointmentDay’].dt.day df[‘AppointmentDay_hour’] = df[‘AppointmentDay’].dt.hour df[‘AppointmentDay_minute’] = df[‘AppointmentDay’].dt.minute df[‘AppointmentDay_dayofweek’] = df[‘AppointmentDay’].dt.dayofweek
Вы можете убедиться, что это работает:
На этом этапе было бы неплохо немного изучить наши свидания.
Как вы можете видеть здесь, встречи назначены на апрель, май и июнь 2016 года и варьируются с понедельника по субботу, в воскресенье назначений нет. Я бы никогда не использовал Год в качестве функции (но все равно показал его), потому что, по-видимому, мы хотим использовать эту прогностическую модель в будущем, и эти будущие годы не будут включены в набор данных. Однако я немного разочарован тем, что это всего лишь несколько месяцев в году. Это означает, что месяц (и, следовательно, неделя года), вероятно, также не следует использовать как функцию. Если бы я делал это для работы, я бы вернулся к базе данных и получил данные за год (или за многие годы). Я могу предположить, что определенное время года (например, около праздников) повлияет на частоту неявок.
Давайте быстро проверим, предсказывает ли dayofweek неявку:
Похоже, что все больше людей пропускают встречи в пятницу и субботу, хотя эффект скромный.
Еще одна приятная вещь с представлением даты и времени в пандах - это то, что вы можете рассчитать «время» между датами. Давайте создадим новую функцию, которая представляет собой количество дней между запланированной датой и датой встречи.
df[‘delta_days’] = (df[‘AppointmentDay’]-df[‘ScheduledDay’]).dt.total_seconds()/(60*60*24)
Обратите внимание, что здесь я использую total_seconds
. Есть функция dt.days, но я привык использовать total_seconds, потому что 1) dt.days округляется до ближайшего дня, 2) dt.days раньше занимал намного больше времени, чем total_seconds. Второй момент, похоже, был исправлен в более поздних версиях pandas.
Мы можем построить гистограмму двух наших классов по этой переменной:
plt.hist(df.loc[df.OUTPUT_LABEL == 1,’delta_days’], label = ‘Missed’,bins = range(0,60,1), normed = True) plt.hist(df.loc[df.OUTPUT_LABEL == 0,’delta_days’], label = ‘Not Missed’,bins = range(0,60,1), normed = True,alpha =0.5) plt.legend() plt.xlabel(‘days until appointment’) plt.ylabel(‘normed distribution’) plt.xlim(0,40) plt.show()
Это распределение кажется мне немного странным, поскольку большинство пациентов, которые не пропустили прием, записывались на прием в тот же день. Мне как бы интересно, включены ли в этот набор данных записи о посещении врача. Я предполагаю, что эта модель просто проведет линию через 1 день и скажет, что не пропустил, если вы запланировали ее в тот же день.
Теперь мы готовы разделить наши образцы и обучить модель!
Разделить образцы
Для простоты я просто разделю на два набора данных: поезд (70%) и проверка (30%). Важно перемешать ваши образцы, потому что вам могут быть предоставлены данные в порядке дат.
# shuffle the samples df = df.sample(n = len(df), random_state = 42) df = df.reset_index(drop = True) df_valid = df.sample(frac = 0.3, random_state = 42) df_train = df.drop(df_valid.index)
Мы можем проверить, что распространенность составляет около 20% в каждом:
print(‘Valid prevalence(n = %d):%.3f’%(len(df_valid),calc_prevalence(df_valid.OUTPUT_LABEL.values))) print(‘Train prevalence(n = %d):%.3f’%(len(df_train), calc_prevalence(df_train.OUTPUT_LABEL.values)))
Учитывая, что эти данные поступают только с апреля по июнь 2016 г., а время встреч не назначено, мы просто воспользуемся этими столбцами:
col2use = [‘ScheduledDay_day’, ‘ScheduledDay_hour’, ‘ScheduledDay_minute’, ‘ScheduledDay_dayofweek’, ‘AppointmentDay_day’, ‘AppointmentDay_dayofweek’, ‘delta_days’]
«Дневные» функции могут даже показаться подозрительными, но пока оставим их.
Его можно было бы расширить, если бы у нас были:
- встречи за весь календарный год
- время встречи
Теперь мы можем построить наши X (входы) и Y (выход) для обучения и проверки:
X_train = df_train[col2use].values X_valid = df_valid[col2use].values y_train = df_train[‘OUTPUT_LABEL’].values y_valid = df_valid[‘OUTPUT_LABEL’].values print(‘Training shapes:’,X_train.shape, y_train.shape) print(‘Validation shapes:’,X_valid.shape, y_valid.shape)
Обучите модель машинного обучения
Поскольку основное внимание в этом посте уделяется функциям datetime, мы просто обучим здесь случайную модель леса. Обратите внимание: если вы хотите использовать другие типы моделей, вам может потребоваться масштабировать или нормализовать данные. Еще одна вещь, которую вы можете захотеть сделать, - это преобразовать dayofweek в категориальную переменную с помощью однократного кодирования. Однако нам не нужно делать это для метода на основе дерева.
from sklearn.ensemble import RandomForestClassifier rf=RandomForestClassifier(max_depth = 5, n_estimators=100, random_state = 42) rf.fit(X_train, y_train)
Затем мы можем получить наши прогнозы с помощью:
y_train_preds = rf.predict_proba(X_train)[:,1] y_valid_preds = rf.predict_proba(X_valid)[:,1]
Оцените производительность
Здесь мы оценим производительность модели. Если вы плохо знакомы с классификационными метриками, я рекомендую проверить мои сообщения об этих метриках (технический пост или нетехнический пост).
from sklearn.metrics import roc_auc_score, accuracy_score, precision_score, recall_score def calc_specificity(y_actual, y_pred, thresh): # calculates specificity return sum((y_pred < thresh) & (y_actual == 0)) /sum(y_actual ==0) def print_report(y_actual, y_pred, thresh): auc = roc_auc_score(y_actual, y_pred) accuracy = accuracy_score(y_actual, (y_pred > thresh)) recall = recall_score(y_actual, (y_pred > thresh)) precision = precision_score(y_actual, (y_pred > thresh)) specificity = calc_specificity(y_actual, y_pred, thresh) print(‘AUC:%.3f’%auc) print(‘accuracy:%.3f’%accuracy) print(‘recall:%.3f’%recall) print(‘precision:%.3f’%precision) print(‘specificity:%.3f’%specificity) print(‘prevalence:%.3f’%calc_prevalence(y_actual)) print(‘ ‘) return auc, accuracy, recall, precision, specificity
Используя эту print_report
функцию, мы можем оценить производительность для обучения и проверки. Здесь я установил порог распространенности 0,201.
Мы можем построить ROC с помощью
from sklearn.metrics import roc_curve fpr_train, tpr_train, thresholds_train = roc_curve(y_train, y_train_preds) auc_train = roc_auc_score(y_train, y_train_preds) fpr_valid, tpr_valid, thresholds_valid = roc_curve(y_valid, y_valid_preds) auc_valid = roc_auc_score(y_valid, y_valid_preds) plt.plot(fpr_train, tpr_train, ‘r-’,label =’Train AUC:%.3f’%auc_train) plt.plot(fpr_valid, tpr_valid, ‘b-’,label =’Valid AUC:%.3f’%auc_valid) plt.plot([0,1],[0,1],’k — ‘) plt.xlabel(‘False Positive Rate’) plt.ylabel(‘True Positive Rate’) plt.legend() plt.show()
Это означает, что мы можем получить AUC 0,71, просто используя функции datetime. Эта кривая ROC немного странная, поскольку у нее есть локоть.
Мы можем немного разобраться в этом, посмотрев на основные функции
feature_importances = pd.DataFrame(rf.feature_importances_, index = col2use, columns=[‘importance’]).sort_values(‘importance’, ascending=False) num = min([50,len(col2use)]) ylocs = np.arange(num) # get the feature importance for top num and sort in reverse order values_to_plot = feature_importances.iloc[:num].values.ravel()[::-1] feature_labels = list(feature_importances.iloc[:num].index)[::-1] plt.figure(num=None, figsize=(6, 6), dpi=80, facecolor=’w’, edgecolor=’k’); plt.barh(ylocs, values_to_plot, align = ‘center’) plt.ylabel(‘Features’) plt.xlabel(‘Importance Score’) plt.title(‘Feature Importance Score — Random Forest’) plt.yticks(ylocs, feature_labels) plt.show()
Это показывает, что delta_days - единственные функции, используемые в модели. Это подтверждает наши вышеупомянутые подозрения, что модель, вероятно, выйдет из строя из-за назначений в тот же день.
Поскольку наши оценки за обучение и валидацию очень похожи, это означает, что мы имеем дело с высокой систематической ошибкой. Чтобы улучшить эту модель, нам потребуются дополнительные функции, поэтому я завершу этот проект на этом.
Заключение
В этом проекте мы создали много новых функций, распаковав переменные даты и времени, чтобы предсказать, не явится ли пациент. По моему опыту, функции datetime могут иметь большое влияние на модели машинного обучения в здравоохранении. Я настоятельно рекомендую попробовать это в следующем проекте. Свяжитесь с нами, если у вас есть какие-либо вопросы или сомнения по поводу этого сообщения.