Недавно я участвовал в конкурсе Kaggle, организованном Two sigma. Речь идет об использовании рыночных и новостных данных для прогнозирования движения цены акций на 10 дней. Подробные объяснения каждого столбца хорошо написаны хостом, и их было бы полезно прочитать, чтобы понять, какую функцию я создаю. Также вы можете прочитать весь исходный код и объяснения из моего ядра Kaggle.

Первое, что следует отметить в этом соревновании, это то, что это соревнование только для ядра, поэтому существуют различные ограничения ресурсов, такие как 16 ГБ ОЗУ и отсутствие доступного графического процессора. Таким образом, невозможно использовать все данные и добавить к этому кучу новых функций. Поэтому, естественно, вы должны ограничить период данных более свежими, а не использовать слишком старые данные. Кроме того, после анализа важности функций столбцы, которые не помогают, должны исчезнуть.

Прежде всего, как и в случае с большинством данных временных рядов, он помогает создавать функции оконной (скользящей) статистики. Даже в реальной жизни многие инвесторы будут проверять такие вещи, как пиковая / минимальная цена за 1 год, выше / ниже ли текущая цена, чем ее скользящая средняя за 1 месяц и все такое (перечислим несколько скользящих средних) ',' Экспоненциальная скользящая средняя, Полоса Боллинджера, Индекс относительной силы и Скользящая средняя объема). Поэтому имеет смысл создавать оконную статистику за 1 неделю, 2 недели, 1 месяц, 1 год и так далее. Используйте общие и простые статистические данные, такие как среднее, медианное, максимальное, минимальное и экспоненциально взвешенное среднее значение. Или проявите изобретательность и придумывайте разные вещи. Сгенерируйте эти оконные элементы на основе числовых столбцов. На самом деле я генерировал функции запаздывания только на основе рыночных данных, а не данных новостей (если бы у меня было больше вычислительных ресурсов, чем я бы попытался создать их также из данных новостей).

BASE_FEATURES = [ 'returnsOpenPrevMktres10', 'returnsOpenPrevRaw10',
                  'open', 'close']

Кроме того, некоторые числа не имеют большого значения без контекста их обычного диапазона и их отношения к рыночному среднему значению (которое также меняется с течением времени). Я имею в виду, что если цена открытия акции сегодня составляет 300 долларов, то она перепродана или перекуплена? Если бы я говорил об Amazon, это было бы безумно дешево, потому что в настоящее время (по состоянию на начало 2019 года) он торгуется около 1600 долларов. Но если бы я говорил об акциях Tesla, они были бы умеренно дешевыми, поскольку акции Tesla на данный момент торгуются в диапазоне 310 ~ 350 долларов. Тот же принцип применяется ко многим другим функциям, таким как необработанная прибыль (прибыль / убыток, которая не корректируется по какому-либо контрольному показателю), (торговый) объем. Поэтому имеет смысл рассчитать соотношение между этими характеристиками и средним рыночным значением.

def add_market_mean_col(market_df):
    daily_market_mean_df = market_df.groupby('time').mean()
    daily_market_mean_df = daily_market_mean_df[['volume', 'close']]
    merged_df = market_df.merge(daily_market_mean_df, left_on='time',
                                right_index=True, suffixes=("",'_market_mean'))
    merged_df['volume/volume_market_mean'] = merged_df['volume'] / merged_df['volume_market_mean']
    merged_df['close/close_market_mean'] = merged_df['close'] / merged_df['close_market_mean']
    return merged_df.reset_index(drop = True)

BASE_FEATURES = BASE_FEATURES + ['volume', 'volume/volume_market_mean', 'close/close_market_mean']

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

def generate_open_close_ratio(df):
    df['open/close'] = df['open'] / df['close']
    
BASE_FEATURES = BASE_FEATURES + ['open/close']

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

open_raw_cols = ['returnsOpenPrevRaw1', 'returnsOpenPrevRaw10']
close_raw_cols = ['returnsClosePrevRaw1', 'returnsClosePrevRaw10']

def raw_features_to_ratio_features(df):
    for col in open_raw_cols:
        df[col + '/open' ] = df[col] / df['open']
    for col in close_raw_cols:
        df[col + '/close'] = df[col] / df['close']

BASE_FEATURES = BASE_FEATURES + ['returnsClosePrevRaw1/close', 'returnsClosePrevRaw10/close', 'returnsOpenPrevRaw1/open', 'returnsOpenPrevRaw10/open']

Ранее упомянутая функция оконной статистики генерируется на основе собранных нами BASE_FEATURES. И объединены time и assetCode (что похоже на столбец id рыночных данных)

new_df = generate_features(market_train_df)
market_train_df = pd.merge(market_train_df, new_df, how = 'left', on = ['time', 'assetCode'])

Из-за нехватки памяти я отбросил много столбцов из данных новостей. Однако, учитывая, что название этого конкурса называется «Использование новостей для прогнозирования движения акций», я подумал, что должен каким-то образом использовать новостные данные. Также думая о реальности, многие трейдеры и машины реагируют на положительные / отрицательные новости, поэтому имеет смысл, что новости влияют на цены акций. Поэтому я решил, что наиболее интересными для меня столбцами являются «sentimentClass», «sentimentNegative», «sentimentNeutral» и «sentimentPositive». Подсчитывается «sentimentClass» (сколько новостей было нейтральным, положительным, отрицательным), остальные принимаются как средние (среднее значение по всей нейтральности и т. Д.). Обратите внимание, что столбец «assetName» действует как столбец идентификатора для данных новостей. В «assetCodes» есть список кодов активов, и иногда на каждую новость приходится несколько десятков кодов. Это будет иметь смысл, если мы рассмотрим некоторые из примеров «assetCodes» и «assetName».

news_train_df.head(100)[['assetCodes', 'assetName']]

По результатам я вижу, что одна новость, относящаяся к названию актива «Microsoft Corp», связана с «кодами активов» из «{« MSFT.O »,« MSFT.F »,« MSFT.DE »,« MSFT.OQ »} '. Эти коды - просто акции Microsoft на разных фондовых биржах. Итак, теперь ясно, что если одна новость связана с названием актива, будут затронуты все связанные коды активов. Кроме того, объединение таблицы по «assetName» практически намного проще, потому что это одно имя актива как в рыночных, так и в новостных данных (напротив, у рыночных данных есть «assetCode», который имеет единый код актива, а у новостных данных есть «assetCodes», у которого есть более одного кода актива). Итак, теперь давайте соответствующим образом трансформируем столбец настроений и объединим рыночные и новостные данные.

def merge_with_news_data(market_df, news_df):
    news_df['firstCreated'] = news_df.firstCreated.dt.hour
    news_df['assetCodesLen'] = news_df['assetCodes'].map(lambda x: len(eval(x)))
    news_df['asset_sentiment_count'] = news_df.groupby(['assetName', 'sentimentClass'])['firstCreated'].transform('count')
    kcol = ['time', 'assetName']
    news_df = news_df.groupby(kcol, as_index=False).mean()
    market_df = pd.merge(market_df, news_df, how='left', on=kcol, suffixes=("", "_news"))
    return market_df

market_train_df = merge_with_news_data(market_train_df, news_train_df)

Теперь, когда у меня есть все функции, которые я хочу использовать, давайте обучим несколько моделей. Я использовал 3 модели из 3 библиотек lightgbm, catboost и XGBoost. Я просто подумал, что было бы интересно сравнить, как каждая библиотека расставляет приоритеты по-разному. Теперь осталось только тренироваться с моделями и проверять важность функций. Полезно знать, сколько функций у нас есть и каков средний вклад каждого столбца.

print("total features:", len(fcol), ", average:", 100/len(fcol))
=> total features: 270 , average: 0.37037037037037035

В среднем каждая функция составила 0,37%. Давайте рассмотрим все особенности и их важность в процентах.

def show_feature_importances(feature_importances):
    total_feature_importances = sum(feature_importances)
    assert len(feature_importances) == len(fcol) # sanity check
    for score, feature_name in sorted(zip(feature_importances, fcol), reverse=True):
        print('{}: {}'.format(feature_name, score/total_feature_importances * 100))
show_feature_importances(gbm.feature_importance(importance_type='split'))
=> assetCodeT: 2.359697858792684
close_market_mean: 2.3284849241525687
volume_market_mean: 1.879018665334915
open/close_window_20_max: 1.4332979586740746
open/close_window_20_min: 1.4114489044259941
open/close_window_10_max: 1.3533928459953806
returnsOpenPrevMktres10_window_20_min: 1.33965915475373
... 

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

def get_non_important_features(feature_importances, threshold):
    total_feature_importances = sum(feature_importances)
    assert len(feature_importances) == len(fcol) # sanity check
    return [feature_name for score, feature_name in sorted(zip(feature_importances, fcol), reverse=True) if ((score * 100) / total_feature_importances)  < threshold]

non_features = get_non_important_features(gbm.feature_importance(importance_type='split'), threshold = 0.1)
print(len(non_features))
non_features
=> 123
['open_window_5_min',
 'close/close_market_mean_window_10_median',
 'open_window_20_median',
 'close_window_5_min',
...

Я считаю, что это неплохой рабочий процесс при создании функций. В итоге,

  1. Экспериментируйте, создавая новые функции
  2. Обучайте модели с помощью функций
  3. Проверить важность функций
  4. Отфильтровать неважные особенности
  5. Повторяйте 1–4, пока не будете довольны имеющимися у вас функциями.
  6. Используйте эти функции для производства / отправки

Большое спасибо за чтение, и если вы обнаружите какие-то проблемы в моем процессе или у вас есть несколько советов, уловок и предложений, пожалуйста, дайте мне знать. Ах, еще моя ручка Kaggle - wontheone1, на случай, если вы захотите последовать ее примеру :)