Однажды томным вечером, сидя напротив мелькающей ленты tjournal и попивая ромашковый чай, внезапно обнаружил себя за чтением статьи про советскую лампочку, которая освещала чей-то подъезд уже 80 лет. Да, весьма интересно, но все же я предпочитаю статьи про политику достижения ИИ в игре дум, приключения ракет SpaceX и, в конце концов, — с наибольшим кол-вом просмотров. А какие вообще статьи набирают внушительные рейтинги? Посты размером с твит про какую-то политическую акцию или же талмуды с детальным анализом российской киноиндустрии? Ну что же, тогда самое время расчехлять свой Jupyter notebook и выводить формулу идеальной статьи.   Для начала работы стоит раздобыть данные. Поскольку TJ не имеет своего API, то и собирать статьи будем старым добрым скрапингом голого html с помощью requests и вытаскиванием из него полезных данных с помощью BeautifulSoup. На выходе мы получаем 39116 статей с 2014 года. Однако стоит отметить, что сама статья записывается в виде html блока со всеми присущими ей кучей тэгов, а не как простой текст. Сам скрипт тут. После часа работы скрипта у нас на руках есть почти 39000 статей, и пришло время вытаскивать из них фичи всех степеней важности. Но сначала импортируем все необходимое: Читаем наш датасет и смотрим его содержимое:  Поскольку из доступных данных для тренировки мы имеем только контент поста и дату публикации, то и большая часть инженеринга фич будет крутиться вокруг текстовых данных. Из самого поста можно выудить два типа фич: структурные и смысловые (да-да, названия придумал сам). К структурным относятся те, которые, не поверите, определяют структуру поста: кол-во ссылок на ютуб, на твиттер, кол-во слов в названии поста, среднее кол-во слов в текстовых блоках и т.п. Все значения, которые не удалось выловить (обычно из-за малого кол-ва текстовых данных), просто заменям на -9999, градиентный бустинг нас поймет. Начнем с временных фич. И тут все как нельзя просто — вытаскиваем год, месяц, день, часы и минуты, из инженеринга фич тут только день недели: А вот теперь началась самая творческая часть работы, где можно дать волю фантазии и собрать из текстов все, что душе угодно. Для начала стоит вытащить из html блока голые куски текста, токенизировать их и пропустить через стеммер полученные слова: Недолго думая, к списку фич добавляем кол-во слов в посте и заголовке, кол-во html тэгов, среднее и среднеквадратичное отколнение кол-ва слов в блоках. Посты на tj пестрят картинками, видео, референсами на твиттер и ссылками на другие ресурсы, что также можно использовать: Вот с семантическими фичами дело обстоит сложнее. И первое, что приходит на ум: tf-idf — классика в обработке текстовых данных. Но один лишь tf-idf не внушает доверия, так что подключим еще Latent Dirihlet Allocation (LDA) для уменьшения размерности поста до 10-50-и элементного вектора. На всякий случай попробуем взять три LDA по 10, 20 и 50 топиков соответственно. Можно ли что-то еще интересное добавить? Разумеется, word2vec: На выходе у нас есть куча векторов слов по 300 элементов в каждом. Теперь нужно их как-то использовать для векторизации поста. Для этого можно использовать 2 техники: вычисление среднего вектора среди всех векторов слов и более сложная, с использованием кластеров. С первой все понятно — есть матрица c векторами слов, просто берем среднее значение по первой оси, алгоритм обработки пустых постов примерно такой же, как выше : Со второй нужно построить некоторое кол-во кластеров по всем векторам слов и затем присвоить каждому слову в тексте принадлежность к тому или иному кластеру, т.е. получится что-то вроде CountVectorizer-а на стероидах. Так же, как и в случае с LDA, попробуем взять несколько KMeans с разным кол-вом кластеров:  Все фичи собрали, осталось только скомпоновать их вместе в одну большую матрицу: Поскольку у нас здесь есть кол-во просмотров и рейтинг, то тут прям напрашивается задача регрессии. С каждым месяцем количество подписчиков на tj возрастает, а это значит, что возрастает и кол-во просмотров. Т.е. нужно как-то нормализовать кол-во просмотров, при этом минимизируя влияние выбросов. Для нормализации одного поста можно взять среднее и среднеквадратичное отклонение из предыдущих 200 постов, скалировать текущее значение, при этом убрав все, что находится за первой и 99-ой перцентилями: Глянем на распределение наших рейтингов:  Вполне ожидаемый скос налево. Лечится такое обычно логарифмом:  Уже намного лучше. Самое сложное позади, осталось только выбрать модель, поставить ее на обучение и молиться на правильный выбор гиперпараметров. Обычно над выбором модели долго не заморачиваются и сразу берут xgboost, что имеет смысл, ибо он уже завоевал свою популярность на kaggle и повсеместно используется как новичками, так и гуру. Но однажды, случайно наткнувшись на сравнение бенчмарков двух реализаций градиентного бустинга, меня привлек внимание LigthGBM от Microsoft своими аппетитными результатами тестов, которые и заставили отложить старичка xgboost как запасной вариант. В целом, тренировка проходит так же, как и на xgboost — тыкаем туда-сюда max_depth и регуляризацию для избежания оверфита, подбираем наилучшее кол-во деревьев под допустимо минимальный learning_rate. Датасет для LigthGBM необходимо для начала привести в местный формат: Параметры градиентного бустинга (получены после долгих часов тренировок): Непосредственно сама тренировка: Важной особенностью деревьев является то, что есть возможность оценить важность фич:  И вот наши результаты: C такими параметрами и фичами точность по r2 score получилась 0.317, а по absolute mean error — 0.42. Хороший ли это результат? Вполне неплохо даже с учетом того, что существуют еще пути для улучшения модели. Например, попробовать нейросети (как реккурентные, так и сверточные, с Embedding слоем и весами word2vec), к LDA добавить еще PCA, NMF и прочую декомпозицию, в конце концов, попробовать выделить другие фичи из разряда "отношение кол-ва пунктуации к кол-ву переносов строк". Но дальнейшие изыскания уже оставляю вам. UPD.
Тут пользователь ben_yazi указал в комментах ссылку на описание апи для TJ. Data scientist, Python developer