Привет, Хабр! Меня зовут Иван, я Data Science специалист SimbirSoft. Я расскажу, как на одном из проектов мы занимались прогнозированием на месяц вперед с помощью методов NLP, Catboost и тематического моделирования на новостном потоке.
Один из способов достижения экономического превосходства над конкурентами — это получение инструмента, оценивающего ситуацию на рынке сейчас и позволяющего делать точные достоверные прогнозы на будущее. И с помощью искусственного интеллекта, анализа данных, работы с временными рядами можно создать достаточно точный инструмент прогнозирования цен.
Подобное моделирование можно использовать в различных областях — от прогнозирования цен на стройматериалы до цен на сельхозпродукцию или спички. Но важно определиться – каким источникам вы доверяете больше всего. На мой взгляд, речь идет не только про доверие, но еще и про поиск информационных площадок, которые коррелируют со спросом.
К нам обратился заказчик, который занимается продажами удобрений. Ему необходимо было протестировать различные подходы к прогнозированию цен на удобрения. Нашей задачей стало построить бейзлайн-модель и оценить, насколько перспективен подход с использованием новостей. Заказчик хотел, чтобы мы оценили предсказательную силу именно новостей, поэтому мы не использовали признаки из самого временного ряда цен (лаги/сезонность/тренд) и признаки, связанные с рынком.
Планировалось, что если эта модель хорошо покажет себя, мы сможем объединить ее с другими подходами (с использованием ряда, данных рынка и признаков, связанных с сельским хозяйством).
Горизонт прогнозирования был установлен в один месяц по требованию заказчика. Его интересовали и прогнозы на 1-2-3 недели, но решено было оценить перспективность подхода на месячных прогнозах, так как эта задача казалась самой сложной.
Далее мы разберем, откуда брать датасет, и как решать задачу с определенными условиями и целью.
Датасет
Датасет — это данные, которым мы доверяем и которые являются основой для построения любой модели. Некачественные или нерелевантные данные могут дать результаты, на которые впоследствии будет опасно опираться, и которые могут дать нам неверную оценку. Датасет цен нам предоставил заказчик.
В качестве таргета (того, что будем предсказывать) выступила средняя за неделю спотовая цена на карбамид на бразильском рынке — он наиболее перспективен для нашего примера. Спотовая цена — это цена, по которой продается реальный товар на условиях немедленной поставки.
Для прогнозирования цены мы составили корпус новостей. Корпус состоит из англоязычных статей, взятых с специализированного новостного сайта worldfertilizer.com. При анализе мы определяли, как та или иная новость повлияла на цену, и как подобная новость в будущем может цену изменить.
В теории хватит использования одного источника, на практике же рекомендуется использовать до пяти источников новостей. Так данные будут более релевантными и будут опираться не на один критерий влияния на цену, а на несколько.
У нас использовался датасет от июля 2018 года до сентября 2023 года.
График цены:
Синяя линия — трейн (226 недель), оранжевая линия — валидация (17 недель), зеленая — тест (24 недели)
Тематическое моделирование
Тематическое моделирование (topic modeling) представляет из себя задачу кластеризации текстов. Мы пытаемся сгруппировать схожие тексты, и таким образом выделить темы, которые присутствуют в корпусе. Для моделирования топиков мы попробовали несколько моделей (LSI, LDA, NMF, TOP2VEC), но лучше всего себя показала LSI, поэтому тут рассмотрим только её. Эта модель использует метод векторизации TF-IDF, который представляет каждый документ как «мешок слов». Это означает, что при рассмотрении документа модель никак не учитывает порядок слов, только частоту их появления в каждом документе и в корпусе в целом. Учитывая это, необходимо провести предобработку новостей.
Предобработка новостей
Посмотрим на вид датасета
import pandas as pd
articles_eng = pd.read_csv("merged_news_from_world_fertilizers.csv")
articles_eng.set_index("DateTime",drop=True,inplace=True)
print(articles_eng.shape)
articles_eng.tail(2)
Из новостей нужно исключить стоп-слова (артикли, предлоги, местоимения и тому подобные слова, не несущие смысловой нагрузки отдельно от целого текста), разбить текст на отдельные слова (токены) и для каждого слова выполнить стемминг. Стемминг — это процесс нахождения основы слова (основа не всегда совпадает с корнем слова).
Воспользуемся для этого библиотекой nltk.
from nltk.tokenize import RegexpTokenizer
from nltk.stem.porter import PorterStemmer
from nltk.corpus import stopwords
tokenizer = RegexpTokenizer(r'\w+')
en_stop = set(stopwords.words('english'))
p_stemmer = PorterStemmer()
doc_processed = []
for i in articles_eng['Article Text'].to_list():
raw = i.lower()
tokens = tokenizer.tokenize(raw)
stopped_tokens = [i for i in tokens if not i in en_stop]
stemmed_tokens = [p_stemmer.stem(i) for i in stopped_tokens]
doc_processed.append(stemmed_tokens)
Новость до предобработки:
The partnership will enable farmers to increase yields and optimise fertilizer use…
И новость после предобработки:
['partnership', 'enabl', 'farmer', 'increas', 'yield', 'optimis', 'fertil', ‘use’, …
Теперь необходимо сформировать словарь и doc-term матрицу для модели.
from gensim import corpora
documents = articles_eng['Article Text'].to_list()
doc_processed = preprocess_data(documents)
dictionary = corpora.Dictionary(doc_processed)
dictionary.filter_extremes()
doc_term_matrix = [dictionary.doc2bow(doc) for doc in doc_processed]
Метод filter_extremes()
исключает из словаря слова, которые встречаются слишком редко (менее чем в пяти текстах) и те, которые встречаются слишком часто (более чем в половине текстов). Слова, которые встречаются слишком редко, не будут давать интересной информации, но увеличат нам словарь, поэтому мы их исключаем. Слова, которые встречаются слишком часто, попадут в doc-term матрицу и будут использоваться при кластеризации, хотя для кластеризации они не особо полезны. Мы разбиваем на темы, используя частоту слов. Если слово встречается в каждом втором документе, скорее всего, оно не связано с какими-то отдельными темами внутри корпуса, а просто является типичным словом для корпуса в целом.
Подобный подход позволяет отфильтровать поток текста и сделать из него качественный набор ключевых, опорных элементов новостей, которые впоследствии будут помогать строить модель для оценки рынка и прогнозирования.
Подбор количества кластеров
Для подбора количества тем мы использовали coherence score. Это метрика, которая показывает, насколько похожи самые частые слова в топике. Под похожестью подразумевается, насколько часто эти слова встречаются в одном контексте в корпусе. В целом считают, что coherence score показывает то, насколько топики осмысленные и интерпретируемые с точки зрения человека.
Возможно, число кластеров, подобранное такой метрикой, будет не оптимально с точки зрения кластеризации векторов (если забыть о том, что эти векторы были текстами, и просто измерять качество кластеризации, например, по silhouette score). Но зато кластеры должны лучше отображать темы, о которых говорят в новостях. А так как мы надеемся уловить темы, которые обсуждаются на рынке удобрений, такая метрика кажется лучшим выбором.
from gensim.models import LsiModel
from gensim.models.coherencemodel import CoherenceModel
coherence_values = []
best_model = None
best_coherence = -np.inf
best_number_of_topics = None
for num_topics in range(2, 15, 1):
model = LsiModel(doc_term_matrix, num_topics=num_topics, id2word = dictionary) # train LSI model
coherencemodel = CoherenceModel(model=model, texts=doc_clean, dictionary=dictionary, coherence='c_v')
coherence_values.append(coherencemodel.get_coherence())
if coherence_values[-1] > best_coherence:
best_coherence = coherence_values[-1]
best_model = model
best_number_of_topics = num_topics
Наилучшее по coherence количество кластеров — 4. На графике видно, что после 4 coherence в общем снижается.
Как превратить кластеризованные новости в ряды фичей?
Для того чтобы построить предиктивную модель, необходимо агрегировать кластеризованные новости и привести их к недельному таймфрейму (как у нашего таргета). Очевидное решение — посчитать количество новостей по каждой теме за N прошедших дней и использовать эти количества как фичи.
Проблема в том, что разные новости могут иметь разные по продолжительности эффекты на покупателей и производителей. К тому же в ходе наших экспериментов мы заметили, что на этапе валидации можно подобрать количество дней N с очень хорошими метриками, но это никак не коррелирует с метриками на тесте. В идеале хотелось бы сделать плавное угасание новостей, и дать возможность модели самой оценивать, насколько старые новости важны. Для этого мы формируем фичи следующим образом:
Для каждой даты рассчитывается взвешенная сумма количества новостей по топику за весь период до даты. Веса показывают, насколько новость старая, и определяются экспоненциальной функцией w = exp(-d/rf), где w — вес новости, d — сколько дней прошло с новости, r — параметр, который определяется экспериментально.
Пример:
Рассмотрим некоторую дату 2022-01-06. Для примера возьмем параметр rf=7. Посчитаем значение признака, отвечающего топику 1. Пусть по топику 1 до 2022-01-06 опубликовано три новости. Одна опубликована в этот же день 2022-01-06, другая опубликована 7 дней назад 2021-12-31, третья новость очень старая — опубликована 37 дней назад 2021-12-01. Тогда значение признака n на дату 2022-01-06 будет равно n(2022-01-06) = exp(-0/7) + exp(-7/7) + exp(-31/7) = 1 + 0.37 + 0.005 = 1.375.
После формирования признаков по каждому топику значения признаков сдвигаются на четыре недели (так как мы прогнозируем на месяц вперед).
В итоге наши фичи выглядят следующим образом (при параметре rf=7).
На графике мы видим, как новостные топики влияют на цену, и на сколько тот или иной топик в n раз поднимает или опускает цену.
Преобразования таргета
Перейдем от цены к относительному изменению цены (делим разность цен на изначальную) и получаем следующий ряд, показывающий изменение цены в долях:
Упростим задачу для модели, и вместо регрессии будем проводить классификацию. Разобьем цену на несколько классов «силы» изменения цены. Часто берут либо два класса — up и down, либо три — up, down и flat (цена почти не изменилась). Но в таком случае прогноз будет не слишком точный. Допустимой для нас по точности стала разбивка на 13 классов. Разбивка проводилась по квантилям. Квантили брались из распределения изменения цен по модулю, после чего формировались интервалы между квантилями для положительных и отрицательных изменений цен. Разбивка выглядит так:
Получившийся результат
Для прогнозирования использовалась библиотека CatBoost. Для эффективного использования нейронных сетей взятый датасет слишком маленький, поэтому было решено использовать градиентный бустинг, так как он как правило дает наилучшие результаты на задачах прогнозирования с признаками на «дефолтных» параметрах без тюнинга. Мы подобрали значение rf на валидационном множестве и получили модель с f1-score = 0.25, accuracy = 0.23.
На тесте результат получился даже лучше — f1-score=0.26, accuracy=0.29.
На графике показаны факт и прогноз, по оси Y идут классы изменения цены:
В целом график выглядит неплохо, модель примерно угадывает динамику цен (за исключением загадочного выброса в мае). Возможно, оказала влияние иная новость, не вошедшая в датасет — к примеру, продажа крупного объема на другом рынке, поэтому важно использовать не один источник, а несколько. Если перевести прогноз из классов в изменения цены (взяв среднее значение в каждом классе), то получим следующий график с предсказанным изменением цены и ожидаемым разбросом.
Тут прогноз выглядит хуже, хотя динамика более-менее совпадает. Для этого прогноза MAE = 0.04, r2=0.35. То есть модель объясняет ~35% дисперсии данных.
Другие модели кластеризации
Кроме LSI, мы также использовали другие модели кластеризации, но они все показали себя сильно хуже.
LSI | LDA | NMF | TOP2VEC | |
r2 | 0.35 | -4.9 | -0.66 | 0.19 |
Как мы подозреваем, причина в том, что даже если кластеризация получилась неплохая с точки зрения silhouette score или coherence score, полученные кластеры могут не отлавливать важные события/темы. Вероятно LSI, top2vec показали себя лучше, просто потому что один или несколько кластеров «случайно» вобрали в себя новости, которые влияют на рынок. Планируется подробнее исследовать и проинтерпретировать полученные кластеры в дальнейшем исследовании, или использовать несколько источников для датасета.
Заключение
Итогом стало то, что модель примерно угадывает динамику ряда на одних только новостях без прочих эндогенных или экзогенных признаков. Хотя мы пока не добились желаемой точности, которая была бы оптимальной для бизнеса, подход можно считать перспективным.
В дальнейшем планируется рассмотреть новости из получившихся кластеров и попытаться их интерпретировать — чтобы понять, чем вызван майский выброс в прогнозе. Если посчитать метрики без этого выброса, то r2 увеличивается с 0.35 до 0.53, а МАЕ улучшается с 0.04 до 0.035. Также в планах расширить датасет новостей с использованием большего количества специализированных источников.
Данный подход можно использовать как стартовый для построения первичной модели и затем дополнять его различными новыми источниками данных и расширять модель. Использование новостей как источника прогнозирования не является чем-то новым, но сам подход дает достаточно точное прогнозирование того, что позволит в будущем достичь большей точности.
Подобный метод можно использовать для прогнозов в различных областях и отраслях, и он не привязан к одному направлению, что позволяет масштабировать метод. Простота модели позволяет в течении 1-2 месяцев внедрения найти необходимые рекомендации для развития бизнеса.
Спасибо за внимание!
Больше авторских материалов для специалистов Data Science от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.