Введение

Я и моя команда разработали окончательное решение для конкурса под названием «Продолжительность поездки на такси в Нью-Йорке». Наша задача состояла в том, чтобы построить модель, которая предсказывает общую продолжительность поездок на такси в Нью-Йорке. Комиссия по такси и лимузинам Нью-Йорка предоставила нам первичный набор данных, который включает следующие переменные:

Работа с набором данных

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

Сравнив наш исходный набор данных с новым набором данных о погоде, мы обнаружили, что два набора данных охватывают разные периоды времени.

weather["DATE"] = pd.to_datetime(weather["DATE"])
weather['year'] = weather['DATE'].dt.year
weather_2016 = weather[weather["year"]== 2016]
weather_2016.drop(['STATION',"NAME","year"], axis = 1, inplace = True)

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

left_merge = pd.merge(left=train, right = weather_2016, on = "DATE", how="left")
left_merge_test = pd.merge(left=test, right = weather_2016, on = "DATE", how="left")
train = left_merge.loc[:, left_merge.columns != 'DATE']
test = left_merge_test.loc[:, left_merge_test.columns != 'DATE']

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

train["good_weather"] = ((train['PRCP'] == 0) & (train['SNOW'] == 0))
train.drop(['AWND', 'PRCP', 'SNOW'], axis=1, inplace=True)
train["t_mean"] = ((train['TMAX']) + (train['TMIN']))/ 2
train.drop(['TMAX', 'TMIN'], axis=1, inplace=True)

Переменная «good_weather» сообщит нам, будет ли дождь или снег, а переменная «t_mean» даст нам среднее значение максимальной и минимальной температуры. Мы еще не закончили с погодой, так как хотим применить кодировщик меток для нашей переменной «good_weather», чтобы наш столбец содержал только 1 и 0.

encoder.fit(train['good_weather'])
train['good_weather'] = encoder.transform(train['good_weather'])
test['good_weather'] = encoder.transform(test['good_weather'])

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

from math import radians, cos, sin, asin, sqrt
def haversine(row):
    lon1 = row['pickup_longitude']
    lat1 = row['pickup_latitude']
    lon2 = row['dropoff_longitude']
    lat2 = row['dropoff_latitude']
    lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
    dlon = lon2 - lon1 
    dlat = lat2 - lat1
    a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
    c = 2 * asin(sqrt(a)) 
    km = 6369 * c
    return km

Применим формулу, чтобы найти наше расстояние:

train['distance'] = train.apply(haversine, axis = 1)

Теперь, когда у нас есть расстояние и продолжительность (trip_duration), мы можем приступить к вычислению скорости:

train['speed'] = train.distance / duration * 2236.936292

Мы умножили на 2236,936292, чтобы перевести км\секунду в мили\час.

Теперь мы можем отбросить скорость, превышающую лимит:

#NYS max speed limit 65mph
train = train[(train.speed < 65)]
train.drop(['speed'], axis = 1, inplace = True)

Как я упоминал ранее, мы также решили определить часы пик в течение дня (время дня с большим трафиком):

def rush_hour_f(row):
    rhour = row['real_hour']
    if (6 <= rhour) & (rhour <= 10):
        return 1
    if (10 < rhour) & (rhour < 16):
        return 2
    if (16 <= rhour) & (rhour <= 20):
        return 3
    return 0

Мы также решили создать новую переменную с именем «is_weekend», поэтому вместо того, чтобы иметь дело с 7 разными днями, мы имеем дело только с двумя группами дней (с понедельника по четверг и с пятницы по воскресенье), что нас интересует с точки зрения трафика.

train['is_weekend'] = train['weekday'] > 4
encoder.fit(train['is_weekend'])
train['is_weekend'] = encoder.transform(train['is_weekend'])

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

Код для графика:

# Create a map
m = folium.Map(location=[40.7, -74], tiles='openstreetmap', zoom_start=4)
# Add points to the map
#Change head(n) for number of points
for idx, row in outlier_pickup.head(100).iterrows():
    Marker([row['pickup_latitude'], row['pickup_longitude']], popup='Pickup', icon=folium.Icon(color='blue')).add_to(m)
for idx, row in outlier_dropoff.head(100).iterrows():
    Marker([row['dropoff_latitude'], row['dropoff_longitude']], popup='Dropoff', icon=folium.Icon(color='orange')).add_to(m)

График:

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

train = train[(train.trip_duration < 1000000)]
train = train[train['pickup_longitude'].between(-75, -73)]
train = train[train['pickup_latitude'].between(40, 42)]
train = train[train['dropoff_longitude'].between(-75, -73)]
train = train[train['dropoff_latitude'].between(40, 42)]
duration = train['trip_duration']
train['trip_duration'] = np.log(train['trip_duration'].values)

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

Модели

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

X_train, X_testing, y_train, y_testing = train_test_split(X, Y, test_size = 0.01, random_state = 42)

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

rf = RandomForestRegressor(random_state = 42, n_estimator = 50)
rf.fit(X_train, y_train)
y_pred = rf.predict(X_testing)

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

Вот окончательный результат нашего прогноза:

Почему это хороший результат? Этот конкурс работает так, что чем ближе ваш прогноз к 0, тем он точнее. Прежде чем приступить к построению модели, мы проверили диапазон результата множества решений, и в таблице лидеров он варьируется от 0,28976 (первое место) до 6,51592 (последнее место). Итак, мы ищем низкое число в результате нашей модели.

Несмотря на то, что мы остались довольны, мы решили продолжить поиски идеальной модели. Мы пытались использовать XGBoost и LightGBM. XGBoost — это ансамблевый метод машинного обучения на основе дерева решений, в котором используется структура повышения градиента. LightGBM — это распределенная высокопроизводительная платформа повышения градиента, основанная на алгоритме дерева решений, используемом для ранжирования, классификации и многих других задач машинного обучения. Оба алгоритма работали довольно хорошо, и они были почти эквивалентны с точки зрения производительности, но LightGBM был значительно быстрее. Это не единственная причина нашего выбора, потому что мы в основном выбрали эту модель из-за всех следующих преимуществ, которые она предлагает:

· Более высокая скорость обучения и более высокая эффективность.

· Снижение использования памяти.

· Лучшая точность.

· Поддержка параллельного, распределенного и графического обучения.

· Возможность обработки больших объемов данных.

Мы импортировали LightGBM и обучили его, используя лучшие параметры.

#~250s
import lightgbm as lgb
lgb_params = {
    'learning_rate': 0.1,
    'max_depth': 25,
    'num_leaves': 1000, 
    'objective': 'regression',
    'feature_fraction': 0.9,
    'bagging_fraction': 0.5,
    'max_bin': 1000 }
#Training on all labeled data using the best parameters
lgb_df = lgb.Dataset(X_var, Y_var)
lgb_model = lgb.train(lgb_params, lgb_df, num_boost_round=1500)

Мы реализовали наш прогноз:

y_pred = lgb_model.predict(X_testing)

Мы создали отправку, используя идентификатор тестового набора данных:

submission = pd.DataFrame({'id': test.id, 'trip_duration': np.exp(y_pred)})
submission.to_csv('submission.csv', index=False)
submission.head()

И, наконец, мы получили наш лучший результат.

Заключительные соображения

Почему это наш лучший результат?

С точки зрения производительности 0,37919 — отличный результат, особенно если сравнить его с нашим предыдущим результатом 0,40658. Существует предел улучшения 0,02739.

С точки зрения времени у нас есть улучшение маржи на 8,8 минуты, потому что прогнозы выполняются за 11 минут и 15 секунд.

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

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

Наш окончательный результат 0,37919 поставил бы нашу команду на 193-е место в таблице лидеров из 1254 прогнозов.