Давайте сделаем черты для наших моделей по дате и времени!

Даты и время - богатые источники информации, которые можно использовать с моделями машинного обучения. Однако эти переменные 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 могут иметь большое влияние на модели машинного обучения в здравоохранении. Я настоятельно рекомендую попробовать это в следующем проекте. Свяжитесь с нами, если у вас есть какие-либо вопросы или сомнения по поводу этого сообщения.