Привет! Меня зовут Мира и я работаю DQE (Data Quality Engineer) в крупной международной компании.
В этой статье я расскажу, как у нас получилось автоматизировать работу аналитиков DQ и разработать продукт, который генерирует тесты автоматически на любой source.
Всё началось с того, что в компании зародилась новая команда, целью которой было построить качественное хранилище данных. Хранилище, которому можно доверять «без угрызения совести». И, конечно же, без DQ здесь не обойтись.
Что такое Data Quality
Data Quality (DQ) — это набор характеристик, определяющих целостность, точность и пригодность данных для их использования в бизнес-процессах и аналитике. Высокое качество данных критически важно для достижения эффективного принятия решений, так как оно напрямую влияет на результаты анализа и позволяет организациям более точно интерпретировать информацию.
Существует множество инструментов для обеспечения качества данных (Data Quality), и они могут варьироваться по функциональности и целям, мы используем питоновский фреймворк soda. Если кратко, то soda — это open-source проект для проверки качества данных. Под капотом — собственный язык проверок SodaCL (Soda Check Language), чеки прописываются в файле YAML в интуитивно понятной форме. Более подробно о принципах работы soda можно почитать в статье.
Бизнес процесс: как мы проверяем данные
Процесс покрытия данных проверками у нас построен следующим образом: заказчик заводит заявку и описывает данные -> DQ аналитик (DQA) берет заявку в работу -> DQA анализирует данные -> DQA уточняет у заказчика детали -> DQA пишет проверки в формате yaml на синтаксисе soda -> DQA пишет даг -> DQA запускает даг -> DQA разбирает баги -> DQA выясняет причину багов (общается как с заказчиком, так и с поставщиком данных) -> DQA при необходимости передаёт информацию инженерам.
Как много DQA в этом процессе, согласны? Помимо этого есть и вторая проблема — по мере того, как быстро развивалась наша команда и как быстро росло количество заказчиков,рос запрос на покрытие данных проверками. По этой же цепочке событий росло количество штатных единиц на DQ аналитиков.Но что дальше? Нанимать людей бесконечно? Или просить ждать заказчиков по полгода, пока очередь бэклога доберется до их заявки?
Тут мы поняли, что нам необходимо как-то помочь аналитикам и автоматизировать их работу, и — вуа-ля — получилось! Наше решение помогло сократить время по написанию проверок на данные с 3 часов до 2 минут, а время нахождения в очереди заявки с 5-6 месяцев до 0 дней. Более того, заказчики сами могли генерировать проверки – всё предельно просто, нужно лишь указать таблицу, а алгоритм все сделает сам.
Итак, теперь о том, как мы к этому шли: начала был подход в лоб — rule-based алгоритм.
Автогенерация: rule-based
Rule-based подход (или подход, основанный на правилах) — методика построения систем искусственного интеллекта, при которой поведение системы определяется набором явно заданных правил. Проще говоря, с помощью if/else и порогов мы сами определяем, когда нам вставлять проверку
Архитектура выглядела следующим образом:
У пользователя на входе есть класс SODAGenerator, которому он передаёт свои креды и тип коннектора, и эти объекты передаются в некий коннектный менеджер. Менеджер запускает соответствующий скрипт по подключению. У нас в компании в основном используются 2 source, но поскольку мы старались придерживаться микро-сервисной архитектуры, то в архитектуру без труда можно добавить свой источник и написать соответствующие методы для него.
Методы rule-based
Какие методы мы реализовали в источниках:
Создание самого коннекта.
Чтение данные в polars.
Для тех, кто не знаком с библиотекой, polars — это высокопроизводительная библиотека для работы с данными на языке Python и Rust. Её основное преимущество в более высокой скорости и более низком потреблении памяти по сравнению с традиционными библиотеками, такими как Pandas. Всё это достигается за счет режима ‘lazy’. Более подробное сравнение можно почитать тут.Выгрузка оригинальных типов данных из системных таблиц источника.
Метрики профилирования
Теперь, когда у нас есть коннект и данные, мы можем сделать профилирование и посчитать метрики.
Профилирование данных — это процесс анализа и оценки данных для определения их структуры, качества и содержания. Этот процесс включает в себя сбор статистической информации о данных, выявление их закономерностей, а также обнаружение проблем, таких как отсутствующие или аномальные значения. Список метрик может быть любой, но вот, что реализовали мы:
сolumns — название столбца
Первое значение всегда 'all' — это означает, что метрика применяется ко всему набору данных.rows — количество строк
percent_dup — процентное содержание дубликатов
quantile_05_morning — процентиль 5% количества заполненных строк в час с 9:00:00 утра до 23:59:59
quantile_95_morning — процентиль 95% количества заполненных строк в час с 9:00:00 утра до 23:59:59
quantile_05_night — процентиль 5% количества заполненных строк в час с 00:00:00 утра до 8:59:59
quantile_95_night — процентиль, составляющий 95% от количества заполненных строк в час с 00:00:00 утра до 8:59:59
null_count — процент значений Null
percent_zeros — процент 0 (для числовых единиц, тип которых — numeric или string, но внутри которых находится число)
percent_empty_strings — процент пустых строк. Если в строке есть пробелы, они учитываться не будут
percent_strings_with_spaces — процентное соотношение только с пробелами
minimum — минимум для чисел и дат (которые имеют числовой или строковый тип, но находятся внутри числа или даты)
maximum — максимум для чисел и дат (которые имеют числовой или строковый тип, но находятся внутри числа или даты)
cat_dist — процент распределения (для категориальных значений менее 20). Имеет структуру: ['название распределения':процент]
Дополнительно в модуле профилирования реализован тест на определение категориальных переменных, а также сбор регулярок по маске регулярного выражения в словарь.
Для определения регулярок у нас используется порог в 60% заполненных значений подходят под маску регулярного выражения. Список регулярок:
email
uuid
card_regex
phone_number
дата: колонка будет определена, если тип данных на источнике != temporal, но в строке содержится дата
ip_address
web_site
int
float
В случаях, когда колонка подходит под несколько регулярок, алгоритм выбирает первое регулярное выражение из списка в соответствии с приоритизацией (регулярные выражения перечислены в порядке приоритизации).
Определение правил и порогов
Если визуализировать результаты модуля профилирования, то мы должны собрать словарь для проверок со следующей структурой:
Собственно — всё. Далее в ContractVariable можно прописывать свои пороги для определения проверок. Условно, установить порог в 2% для пропущенных значений и там,где % пропущенных значений <=2, вставлять соответствующую проверку. Здесь полная свобода изощрений, как и по каким критериям установить, что проверка должна присутствовать в данных.
Думаю, что минусы алгоритма понятны, тут есть 2 основные проблемы:
bug попал в конкретную выгрузку, тогда алгоритм не сможет задетектить проверку
алгоритм не умеет думать
ML-подход
ML эксперименты
Сначала мы думали в сторону классического ML, но не понимали, как строить модель на неразмеченных данных и как генерировать проверки. Была идея подавать профайлинг в качестве данных обучения модели ML, но тогда у нас бы было:
а) очень мало данных для обучения, ведь профайлинг — это метрики таблицы
б) для лечения п.а можно было бы собирать историю по профайлингу, но тогда прежде, чем запустить обучение нужно было просчитать все эти метрики, что заняло бы достаточное количество времени.
Поэтому мы стали думать как разметить as is данные. Для примера мы взяли 1 таблицу, на которую DQA уже написал тесты и в которой были баги, далее разметили строку как битая/не битая и, если битая, то по какому признаку/признакам. Скормили эти данные Random Forest и визуализировали feature importance и листья деревьев. В итоге мы смогли лучше понимать скрытые зависимости, но опять появился ряд проблем:
а) Random Forest работал на таблице, по которой аналитик уже проделал работу
б) Каждая таблица в хранилище имеет разную логику, разные колонки и модель будет строить разные деревья для каждой таблицы, иными словами нам нужен унифицированный подход, а не подход, который решает 1 конкретную задачу
в) Работа уперлась в исследовательскую деятельность, Random Forest может предсказывать, а не сгенерировать
LLM
Мы начали думать в сторону LLM (Large Language Model, крупная языковая модель) — это тип модели машинного обучения, специально предназначенный для работы с текстом. Эти модели обучаются на огромных объемах текстовых данных и способны понимать, генерировать и обрабатывать человеческий язык на естественном уровне.
Казалось, что LLM может закрыть наши потребности, поскольку:
а) LLM может думать и генерировать
б) Нам не нужно думать о разметке, соответственно мы можем запускать алгоритм на таблицах, где еще не была приложена рука DQA
в) В LLM можно реализовать ручной подход, когда заказчик пишет человеческим языком какие данные должны быть, чтобы наложить на них проверки
Автогенерация: LLM
В итоге мы решили использовать LLM, и давать модели на вход профилирование за 3 периода, чтобы можно было отследить нормальное поведение таблицы.
Архитектура верхнеуровневого выглядит след образом:
У пользователя есть 2 сценария: сгенерировать контракт или сгенерировать suite. Сценарий упрощен до минимума: все что нужно сделать — это создать файл .env со своими кредами и типом коннектора, вызвать соответствующий класс и воспользоваться методом, который необходим (выгрузить профайлинг/ распечатать контракт или suite/ выгрузить контракт или suite).
Мы используем модель Llama3, но можно использовать любую модель. Всем известно, что любая модель ограничена в контексте, поэтому мы решили для каждой проверки писать свой промт и запускать ручку LLM итеративно, так мы сможем экономить контекст, более подробно описывать логику проверки модели и фокусировать ее внимание на одну цель.
LLM: профилирование
Итак, теперь главный вопрос, а как определять период и делать запрос для профилирования? Мы не знаем насколько объемная таблица может прийти на вход. У нас в компании были таблицы, которые весили терабайты, нужно было придумать алгоритм, который определит данные для подсчета метрик и вот, как мы это сделали:
Мы собираем некие характеристики таблицы, на основе которых пишем запрос. Разберем пошагово:
Первым шагом определяется бизнес-дата, по которой будет настроена фильтрация в запросе, здесь таблица разделяется на 4 типа: бизнес-дата с типом date, timestamp, varchar (для таблиц, где дата разбита на year, month, day) и таблица без бизнес-даты.
Для определения п1 делается запрос в системные таблицы и смотрится есть ли у таблицы партиция. Если она есть, то партиция определяется, как бизнес-дата. Если нет — у таблицы отбираются все колонки с типом date, timestamp и бизнес-дата определяется по приоритизации. Если нет ни партиции, ни колонок с типом даты или даты и времени, то таблица остается без бизнес-даты и пока что «отдыхает».
Если у таблицы определилась бизнес-дата, то определяется ее гранулярность. Мы не стали ходить далеко и решили брать последние 3 месяца в профилирование, поскольку нет смысла смотреть данные глубже, ведь более старые данные не будут отражать действительности. Для определения гранулярности мы делаем запрос к источнику за период [вчера+2 месяца назад] и [вчера+3 месяца назад], считаем количество уникальных бизнес-дат в этом периоде. Если есть хотя бы 20 дат в результате -> источник старый, иначе — новый. Тут же для работы с новыми источниками мы считаем переменную diff — это разница между максимальной и минимальной датой в таблице (нам это пригодится для генерации запроса).
Дальше нам необходимо определить объем источника, мы делаем быструю оценку данных за периоды: для новых таблиц — это 3 рандомных дня (для этого мы из максимума бизнес-даты вычитаем рандомное число из diff), для старых источников — это 3 дня, которые мы определили ранее (вчера / вчера+месяц назад / вчера+2 месяца назад). Берем из 3 результатов максимум для определения «огромных» таблиц, поскольку не исключаем ту вероятность, что мы могли попасть в день бага и данные вообще могли не поступать в таблицу, тогда среднее и медиана тут сработает некорректно.
Дальше мы определяем % сэмпла для выгрузки, для больших таблиц за день, для нормальных за месяц. Тут все просто, подставляем в формулу количество строк, которых мы хотим видеть в выгрузке и рассчитываем % на основе полученного максимума из п4.
Определяем колонки для выгрузки, тут мы исключаем нераспарсенные колонки, поскольку на них проверки все равно не пишутся, а профайлинг упадет с ошибкой. Этот шаг обязателен для всех таблиц, поэтому тут «подключаются» таблицы без бизнес-дат.
И финальный шаг, где мы пишем различные запросы с соответствующим tablesample на основе характеристик таблицы. Для таблиц, которые были без бизнес-даты мы пишем запросы с limit и offset, где увеличиваем offset в цикле.
В конце мы склеиваем профайлинг и представляем каждую метрику в виде массива, где каждое значение результат за период.
LLM: входные данные
Теперь можно кормить модель, здесь дело вкуса, но мы сделали вот так:
Кратко разберем пару проверок и логику, которую мы описывали модели в промтах:
Дубликаты: мы отдаем только метрики из профилирования процент дубликатов, процент пропущенных значений и категориальные признаки. Очевидно, что % дубликатов и % пропущенных значений скоррелированы и, если % дубликатов 1%, а % пропущенных значений 97%, то проверка не нужна, поскольку все значения просто пустые. Ну и если у профайлинга есть значения в столбце категориального признака, то никогда проверка на дубликаты не будет допустима.
Минимумы и максимумы: отдаем профайлинг с соответствующими колонками и сэмпл для того, чтобы модель могла посмотреть данные и понять, является ли рассчитанный минимум/максимум выбросом. Если это выброс, то не включать его как валидный порог в проверку.
«Как художник видит» — так и можно описать логику в промтах. Ну а знание конкретных кейсов, которые характерны вашим данным, сильно упростит работу модели.
LLM: нейминг тестов
Завершающим элементом продукта является нейминг проверок, тут без LLM тоже не обошлось. У нас в компании есть каталог данных OMD, где можно найти описание таблицы и данных. Сценарий прост: запрос через апи -> получение описание-> прогон через LLM, чтобы оставить только описание колонки и убрать воду.
Result
На выходе у нас получается готовый suite с сгенерированными тестами:
Для простоты использования мы опубликовали библиотеку на pypi, поэтому скачать ее можно через pip.
Алгоритм можно продолжать развивать и считать большое количество метрик, например, добавить стандартное отклонение, дисперсию, медиану и отдельно выделять выбросы.