Компрессирование стационарного временного ряда
Компрессирование стационарного временного ряда

Эта статья будет полезна DS специалистам, и тем, кто хоть когда-нибудь сталкивался с такой проблемой, как выбросы в данных или OOD (out of distribution), и ищет пути решения проблем, возникающих из-за них.

Появление идеи

Машинное обучение я начал изучать основательно в декабре 2024 года, где то в августе 2025 у меня возник вопрос: «А как правильно работать с выбросами, если они не являются ошибкой?». Удаление выбросов — это просто игнорирование проблемы, которое потом повлечет за собой падение качества модели в проде, оставление выбросов без внимания могут создавать сложности в достижении необходимого уровня качества, либо потребуется использовать более сложные алгоритмы.

Потратив около часа на дискуссию с ИИ чат-ботом, как стоит поступить, пришли к выводу, что на данном этапе есть такой способ, как clipping, его суть в том, чтобы просто заменять значения, которые превышают определенный порог, конкретным значением.

Примечание: вы меня спросите, а как же логарифмирование, преобразование box-cox и т. д.? А я отвечу, а что если в распределении выбросы редки или распределение не имеет длинного хвоста, тогда мы можем потерять ценную информацию в данных.

Метод clipping’a довольно топорный поэтому он мне не очень понравился, а услышав знакомое слово (я увлекаюсь звукозаписью более 10 лет) я понимаю, что это же один из методов компрессии сигнала, где по сути идет та же борьба с пиками в сигнале, которые так же сильно били аналоговое оборудование.

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

Иллюстрация компрессии аудио-сигнала
Иллюстрация компрессии аудио-сигнала

И тут я понял, что нужно делать компрессор для данных.

MVP или простая реализация алгоритма

Я начал с простой реализации алгоритма в обычной функции, где старался подобрать наиболее эффективный подход и его математическое выражение, где основным требованием был получить алгоритм, который:

  • Легко интерпретировать;

  • Легко настраиваемый;

  • Интуитивно понятный.

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

Я получил такой код:

# Импорты
import pandas as pd
import numpy as np

# Функция для компрессии данных
def compress(data, threshold, coef, method='power', dry=0):

    data = np.asarray(data, dtype=np.float64)
    mask = data > threshold
    compressed = data.copy()
    
    if method == 'power':
        compressed[mask] = threshold + np.power(data[mask] - threshold, coef)
    
    else:
        compressed[mask] = threshold + np.log1p(data[mask] - threshold) * coef
    
    return dry * data + (1 - dry) * compressed

Пример применения функции до и после:

Признак до компрессирования
Признак до компрессирования
Признак после компрессирования
Признак после компрессирования

Из плюсов:

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

Как влияет компрессирование одного признака на обучение LinearRegression (comp - скомпрессированный)
Как влияет компрессирование одного признака на обучение LinearRegression (comp - скомпрессированный)
Как влияет компрессирование одного признака на обучение LightGBM (comp - скомпрессированный)
Как влияет компрессирование одного признака на обучение LightGBM (comp - скомпрессированный)

Оценить поможет тебе компрессирование признака или нет — mutual_information на обучающей выборке, и с помощью такого рейтинга признаков самостоятельно отобрать оптимальный набор признаков.

Оценка влияния компрессии с помощью mutual_information (comp - скомпрессированный)
Оценка влияния компрессии с помощью mutual_information (comp - скомпрессированный)

Все вроде хорошо, теперь к минусам:

Было дико неудобно ее использовать, и стояли следующие вопросы:

  • А что, если выбросы с обеих сторон распределения значений?

  • А что, если нужно использовать разные виды компрессии из каждых сторон?

  • А что если в распределении есть, как положительные, так и отрицательные значение?

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

И тут я понял — пора, пора решать те вопросы, которые нависли над моей функцией, и которая в целом способна помочь хотя бы частично решить вопрос OOD, а не только выбросы в распределении.

Реализация кастомного трансформера

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

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

Решение оказалось довольное тривиальное и одно сразу на все вопросы, а именно, раз мы компрессируем значения только с одной стороны, тогда нам нужно просто последовательно обработать сигнал, с начала с одной стороны, потом с другой, просто меняя знак  у значений и прибавляя к распределению полученное минимальное значение, так мы себе гарантировано получаем нужную сторону распределения и то, что все значения положительные. А потом после преобразований возвращаем  распределение на место, откуда взяли. И теперь нам только остается в нужный момент подать нужный аргумент, для определения соответствующего метода компрессии.

Но трудности ждали впереди, основной проблемой была валидация входных признаков, и гибкость применения самого трансформера. Одно дело, когда я сам понимаю что как работает и могу не вставлять отрицательные степени, так как окончательно предугадать результат не смогу, другое дело когда это могут сделать из-за опечатки или просто решив поэкспериментировать, что будет. Поэтому пришлось прописать огромное полотно проверок задаваемых параметров. Далее хотелось бы сохранить гибкость, которая есть во всех инструментах sklearn, чтобы он лаконично вписывался к остальным и не возникало трудностей, поэтому необходимо было продумать логику, как трансформер будет интерпретировать входные параметры, которые можно подать скляром, списком или списком списков (нужно для обработки обеих сторон признака).

Заключение

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

Ссылка на GitHub