Лучшие практики по созданию ETL для ML
Секреты создания эффективной ETL для ML
Неотъемлемой частью ML Engineering является создание надежных и масштабируемых процедур для извлечения данных, их преобразования, обогащения и загрузки в конкретное хранилище файлов или базу данных. Это один из компонентов, в которых наиболее тесно сотрудничают ученый по данным и инженер по ML. Обычно ученый по данным предлагает первоначальную версию того, как должен выглядеть набор данных. Идеально, не на блокноте Jupyter. Затем инженер по ML присоединяется к этой задаче, чтобы сделать код более читаемым, эффективным и надежным.
ML ETL-процессы могут состоять из нескольких под-ETL или задач. И они могут материализоваться в самых разных формах. Некоторые общие примеры:
- Основанный на Scala Spark-джоб, считывающий и обрабатывающий данные журналов событий, хранящиеся в S3 в формате Parquet и запускаемый по расписанию с помощью Airflow еженедельно.
- Процесс на Python, выполняющий запрос SQL в Redshift через запланированную функцию AWS Lambda.
- Сложная обработка на базе pandas, выполняемая через задачу Sagemaker Processing Job с использованием триггеров EventBridge.
Сущности в ETL-процессах
Мы можем выделить различные сущности в таких типах ETL-процессов: у нас есть Источники (где находятся сырые данные), Получатели (где хранится конечный артефакт данных), Процессы данных (как происходит чтение, обработка и загрузка данных) и Триггеры (как инициируются ETL-процессы).
- Как создать приложения LLM с использованием векторной базы данных?
- Все тонкости использования Retrieval-Augmented Generation (RAG)
- Представляем бесплатный открытый пропуск на ODSC West
- Среди источников могут быть такие хранилища, как AWS Redshift, AWS S3, Cassandra, Redis или внешние API. Получатели такие же.
- Процессы данных обычно запускаются во временных контейнерах Docker. Мы можем добавить еще один уровень абстрагирования, используя Kubernetes или любой другой управляемый сервис AWS, такой как AWS ECS или AWS Fargate. Или даже SageMaker Pipelines или Processing Jobs. Вы можете запускать эти процессы в кластере, используя определенные движки обработки данных, такие как Spark, Dask, Hive, движок Redshift SQL. Также вы можете использовать простые процессы на основе Python с использованием Pandas для обработки данных. Кроме того, существуют и другие интересные фреймворки, такие как Polars, Vaex, Ray или Modin, которые могут быть полезны для преодоления промежуточных решений.
- Самый популярный инструмент-триггер – Airflow. Можно использовать также такие инструменты, как Prefect, Dagster, Argo Workflows или Mage.
Следует ли использовать фреймворк?
Фреймворк – это набор абстракций, соглашений и готовых утилит, которые можно использовать для создания более унифицированной кодовой базы при решении конкретных задач. Фреймворки очень удобны для ETL-процессов. Как мы уже описали, есть очень общие сущности, которые можно потенциально абстрагировать или инкапсулировать для создания комплексных рабочих процессов.
Прогресс, который я бы предпринял для создания внутреннего фреймворка обработки данных, следующий:
- Начните с создания библиотеки коннекторов к различным Источникам и Получателям. Реализуйте их, когда они вам понадобятся при работе над различными проектами. Это лучший способ избежать YAGNI.
- Создайте простой и автоматизированный рабочий процесс разработки, который позволит вам быстро развивать кодовую базу. Например, настройте рабочие процессы CI/CD для автоматического тестирования, проверки кода и публикации пакета.
- Создайте утилиты, такие как чтение SQL-скриптов, запуск сеансов Spark, функции форматирования дат, генераторы метаданных, утилиты регистрации, функции для получения учетных данных и параметров соединения, а также утилиты оповещения, среди других.
- Выберите между созданием внутреннего фреймворка для написания рабочих процессов или использованием существующего. Область сложности велика, когда речь идет об этой разработке внутри компании. Вы можете начать с некоторых простых соглашений при создании рабочих процессов и в конце концов создать библиотеку на основе DAG с использованием общих классов, таких как Luigi или Metaflow. Это популярные фреймворки, которые вы можете использовать.
Создание библиотек “Utils”
Это критическая и центральная часть вашей кодовой базы данных. Все ваши процессы будут использовать эту библиотеку для перемещения данных из одного источника в другой получатель. Прочный и хорошо продуманный начальный дизайн программного обеспечения – важный аспект.
Но зачем мы хотели бы сделать это? Основные причины такие:
- Повторное использование: Использование тех же компонентов программного обеспечения в разных проектах повышает производительность. Программное обеспечение разрабатывается только один раз, а затем может быть интегрировано в другие проекты. Но эта идея не нова. Можно найти ссылки еще в 1968 году на конференции, целью которой было решение так называемого софтверного кризиса.
- Инкапсуляция: Все внутренности используемых различных коннекторов через библиотеку не обязательно должны быть видны конечным пользователям. Достаточно предоставить понятный интерфейс. Например, если у нас есть коннектор к базе данных, мы не хотели бы, чтобы строка подключения была выставлена как публичный атрибут класса коннектора. Используя библиотеку, мы можем гарантировать безопасный доступ к источникам данных. Рассмотрите этот момент
- Более высокое качество кодовой базы: Тесты разрабатываются только один раз. Разработчики могут полагаться на библиотеку, потому что она содержит тестовый набор (в идеале, с очень высоким покрытием тестами). При отладке ошибок или проблем мы можем игнорировать, по крайней мере на первом этапе, что проблема заключается в библиотеке, если мы уверены в нашем наборе тестов.
- Стандартизация / “Определение”: Наличие библиотеки коннекторов в некоторой степени определяет способ разработки ETL-процессов. Это хорошо, потому что в организации ETL-процессы будут иметь одинаковые способы извлечения или записи данных в различные источники данных. Стандартизация приводит к лучшей коммуникации, повышает производительность и улучшает прогнозирование и планирование.
При создании такой библиотеки команды обязуются поддерживать ее со временем и принимать на себя риск реализации сложных рефакторингов при необходимости. Некоторые причины, по которым приходится выполнять эти рефакторинги:
- Организация переходит на другое облачное решение.
- Изменяется движок хранилища данных.
- Новая версия зависимости ломает интерфейсы.
- Необходимо добавить больше проверок разрешений безопасности.
- В новую команду приходят участники с разными мнениями о дизайне библиотеки.
Интерфейсные классы
Если вы хотите сделать свои ETL-процессы независимыми от источников или получателей, хорошим решением будет создание интерфейсных классов для базовых сущностей. Интерфейсы служат определением шаблонов.
Например, вы можете создать абстрактные классы для определения требуемых методов и атрибутов DatabaseConnector. Вот как может выглядеть упрощенный пример этого класса:
from abc import ABCclass DatabaseConnector(ABC): def __init__(self, connection_string: str): self.connection_string = connection_string @abc.abstractmethod def connect(self): pass @abc.abstractmethod def execute(self, sql: str): pass
Другие разработчики могут создавать подклассы от DatabaseConnector и создавать новые конкретные реализации. Например, MySqlConnector или CassandraDbConnector могут быть реализованы следующим образом. Это поможет конечным пользователям быстро понять, как использовать любой коннектор, подклассный от DatabaseConnector, так как все они имеют одинаковый интерфейс (одни и те же методы).
mysql = MySqlConnector(connection_string)mysql.connect()mysql.execute("SELECT * FROM public.table")cassandra = CassandraDbConnector(connection_string)cassandra.connect()cassandra.execute("SELECT * FROM public.table")
Простые интерфейсы с хорошо именованными методами очень мощны и позволяют достичь большей производительности. Поэтому мой совет – потратить достаточно времени на обдумывание этого.
Правильная документация
Документация не ограничивается только строками документации и комментариями в коде. Она также относится к объяснениям, которые вы даете о библиотеке. Написание жирного заявления о конечной цели пакета и четких объяснений требований и руководств для внесения вклада является важным.
Например:
"Эта утилитная библиотека будет использоваться во всех ML-каналах данных и в задачах инженерии признаков для обеспечения простых и надежных соединителей с различными системами в организации".
Или
"Эта библиотека содержит набор методов, преобразований и алгоритмов, которые могут быть использованы сразу с простым интерфейсом, который может быть объединен в цепочку, как в пайплайне scikit-learn".
Когда у библиотеки есть четкая миссия, это открывает путь к правильному пониманию со стороны участников. Поэтому открытые библиотеки с открытым исходным кодом (например, pandas, scikit-learn и т. д.) за последние годы стали очень популярны. Участники приняли цель библиотеки и обязались следовать утвержденным стандартам. Нам следует делать что-то подобное в своих организациях.
Сразу после объявления миссии мы должны разработать основную архитектуру программного обеспечения. Какими должны быть наши интерфейсы? Мы должны покрыть функциональность через больше гибкости в интерфейсных методах (например, больше аргументов, которые приводят к различным поведениям) или через более детальные методы (например, каждый метод имеет очень конкретную функцию)?
После этого – стайлгайд. Определите предпочитаемую иерархию модулей, необходимую глубину документации, как публиковать PR (pull request), требования к покрытию кода и т. д.
Что касается документации в коде, докстринги должны быть достаточно описательными в отношении возможного поведения функции, но мы не должны просто копировать название функции. Иногда название функции достаточно выразительно, и докстринг, объясняющий ее поведение, просто избыточен. Будьте краткими и точными. Приведем простой пример:
❌Нет!
class NeptuneDbConnector: ... def close(): """Эта функция проверяет, открыто ли соединение с базой данных. Если да, то она закрывает его, а если нет, то ничего не делает."""
✅Да!
class NeptuneDbConnector: ... def close(): """Закрывает соединение с базой данных."""
Перейдем к теме комментариев в строке. Я всегда использую их, чтобы объяснить некоторые вещи, которые могут показаться странными или необычными. Также, если мне нужно использовать сложную логику или сложный синтаксис, всегда лучше написать четкое объяснение над этим блоком кода.
# Получаем максимальное целое число в списке l = [23, 49, 6, 32]reduce((lambda x, y: x if x > y else y), l)
Кроме того, вы также можете включить ссылки на проблемы в Github или ответы на Stackoverflow. Это действительно полезно, особенно если вам пришлось написать странную логику, чтобы преодолеть известную проблему зависимости. Это также очень удобно, когда вам пришлось реализовать трюк для оптимизации, который вы получили из Stackoverflow.
Эти два элемента: классы интерфейса и ясная документация, на мой взгляд, являются лучшими способами сохранить общую библиотеку в живых долгое время. Она будет устойчива к ленивым и консервативным новым разработчикам, а также к полностью энергичным, решительным и имеющим сильные убеждения разработчикам. Изменения, улучшения или революционные рефакторинги будут проходить гладко.
Применение шаблонов проектирования программного обеспечения к ETL-процессам
С точки зрения кода, для ETL-процессов должно существовать 3 ясно различаемых функции высокого уровня. Каждая из них связана с одним из следующих шагов: Извлечение (Extract), Преобразование (Transform), Загрузка (Load). Это одно из самых простых требований для кода ETL.
def extract(source: str) -> pd.DataFrame: ...def transform(data: pd.DataFrame) -> pd.DataFrame: ...def load(transformed_data: pd.DataFrame): ...
Очевидно, что обязательно именно так называть эти функции не обязательно, но это повышает читабельность, поскольку они являются широко принятыми терминами.
DRY (Don’t Repeat Yourself)
Это один из великих шаблонов проектирования, который обосновывает использование библиотек соединителей. Вы пишете ее один раз и используете повторно в различных шагах или проектах.
Функциональное программирование
Это стиль программирования, направленный на создание функций “чистыми” или без побочных эффектов. Входные данные должны быть неизменяемыми, и результаты всегда одинаковы при данных входах. Эти функции легче тестировать и отлаживать в изолированном виде. Таким образом, они обеспечивают более высокую степень воспроизводимости для ETL-процессов.
Применение функционального программирования к ETL позволяет достичь идемпотентности. Это означает, что каждый раз, когда мы запускаем (или повторно запускаем) конвейер, он должен возвращать те же выходные данные. Благодаря этой характеристике мы можем уверенно работать с ETL и быть уверенными, что двойной запуск не создаст дублирующиеся данные. Сколько раз вам приходилось создавать странный SQL-запрос для удаления вставленных строк из неправильного запуска ETL? Обеспечение идемпотентности помогает избежать подобных ситуаций. Максим Бошамен, создатель Apache Airflow и Superset, известный сторонник функциональной инженерии данных.
SOLID
Мы будем использовать ссылки на определения классов, но эта секция также может применяться к функциям первого класса. Мы будем использовать тяжелые объектно-ориентированные программирование для объяснения этих концепций, но это не означает, что это лучший способ разработки ETL. Здесь нет специфического консенсуса, и каждая компания делает это по-своему.
Относительно принципа единственной ответственности вы должны создавать сущности, которые имеют только одну причину для изменения. Например, выделяя ответственность между двумя объектами, такими как класс SalesAggregator и класс SalesDataCleaner. Последний подвержен содержанию специфических бизнес-правил для “очистки” данных о продажах, а первый сосредоточен на извлечении продаж из различных систем. Код обоих классов может измениться по разным причинам.
Для принципа открытости/закрытости, сущности должны быть расширяемыми для добавления новых функций, но не должны быть открытыми для модификации. Предположим, что SalesAggregator получил в качестве компонентов StoresSalesCollector, который используется для извлечения продаж из физических магазинов. Если компания начала продавать онлайн и мы хотели получить эти данные, мы бы указали, что SalesCollector открыт для расширения, если он также может получать другой компонент OnlineSalesCollector с совместимым интерфейсом.
from abc import ABC, abstractmethodclass BaseCollector(ABC):
@abstractmethod
def extract_sales() -> List[Sale]:
passclass SalesAggregator:
def __init__(self, collectors: List[BaseCollector]):
self.collectors = collectors
def get_sales(self) -> List[Sale]:
sales = []
for collector in self.collectors:
sales.extend(collector.extract_sales())
return salesclass StoreSalesCollector:
def extract_sales() -> List[Sale]:
# Extract sales data from physical storesclass OnlineSalesCollector:
def extract_sales() -> List[Sale]:
# Extract online sales dataif __name__ == "__main__":
sales_aggregator = SalesAggregator(
collectors = [
StoreSalesCollector(),
OnlineSalesCollector()
]
)
sales = sales_aggregator.get_sales()
Принцип подстановки Лисков, или поведенческая подтипизация не так просто применить к проектированию ETL, но она применима к утилитарной библиотеке, о которой мы упоминали ранее. Этот принцип пытается установить правило для подтипов. В данной программе, использующей супертип, потенциально можно заменить его одним из подтипов без изменения поведения программы.
from abc import ABC, abstractmethodclass DatabaseConnector(ABC):
def __init__(self, connection_string: str):
self.connection_string = connection_string
@abstractmethod
def connect():
pass
@abstractmethod
def execute_(query: str) -> pd.DataFrame:
passclass RedshiftConnector(DatabaseConnector):
def connect():
# Реализация подключения к Redshift
def execute(query: str) -> pd.DataFrame:
# Реализация запроса в Redshiftclass BigQueryConnector(DatabaseConnector):
def connect():
# Реализация подключения к BigQuery
def execute(query: str) -> pd.DataFrame:
# Реализация запроса в BigQueryclass ETLQueryManager:
def __init__(self, connector: DatabaseConnector, connection_string: str):
self.connector = connector(connection_string=connection_string).connect()
def run(self, sql_queries: List[str]):
for query in sql_queries:
self.connector.execute(query=query)
Мы видим на примере ниже, что любой из подтипов DatabaseConnector соответствует принципу подстановки Лисков, так как любой из его подтипов может быть использован внутри класса ETLManager.
Теперь поговорим о принципе разделения интерфейса. Он утверждает, что клиенты не должны зависеть от интерфейсов, которые они не используют. Этот принцип очень удобен для разработки DatabaseConnector. Если вы реализуете DatabaseConnector, не перегружайте класс интерфейса методами, которые не будут использоваться в контексте ETL. Например, вам не понадобятся методы grant_permissions или check_log_errors. Они относятся к административному использованию базы данных, что не соответствует случаю использования в ETL.
Не последним, но однако, принцип инверсии зависимостей. Согласно этому принципу, модули высокого уровня не должны зависеть от модулей низкого уровня, а вместо этого должны зависеть от абстракций. Это поведение хорошо проиллюстрировано на примере SalesAggregator выше. Заметьте, что его метод __init__ не зависит от конкретных реализаций StoreSalesCollector или OnlineSalesCollector. В основном он зависит от интерфейса BaseCollector.
Как должен выглядеть великий ML ETL?
Мы сильно полагаемся на объектно-ориентированные классы в приведенных выше примерах, чтобы показать способы применения принципов SOLID к работам ETL. Однако не существует общего согласия о том, какой формат кода и стандарт следует использовать при создании ETL. Он может иметь очень разные формы, и это скорее проблема наличия внутреннего, хорошо задокументированного и определенного фреймворка, как уже обсуждалось, а не попытки создания общего стандарта для всей отрасли.
В этом разделе я постараюсь сосредоточиться на объяснении некоторых характеристик ETL-кода, которые делают его более читаемым, безопасным и надежным.
Приложения командной строки
Все процессы обработки данных, о которых вы можете подумать, являются в основном приложениями командной строки. При разработке вашего ETL на Python всегда предоставляйте параметризованный интерфейс командной строки, чтобы вы могли выполнять его из любого места (например, контейнера Docker, который может работать в кластере Kubernetes). Существует множество инструментов для создания парсинга аргументов командной строки, таких как argparse, click, typer, yaspin или docopt. Typer, возможно, самый гибкий, простой в использовании и невредоносный для вашего существующего кода. Он был создан создателем известной библиотеки веб-служб Python FastApi, и количество его звезд на GitHub продолжает расти. Документация отличная, и он становится все более и более промышленным стандартом.
from typer import Typerapp = Typer()@app.command()def run_etl( environment: str, start_date: str, end_date: str, threshold: int): ...
Чтобы выполнить вышеуказанную команду, вам нужно будет выполнить:
python {file_name}.py run-etl --environment dev --start-date 2023/01/01 --end-date 2023/01/31 --threshold 10
Сравнение вычислений процесса и базы данных
Типичное рекомендуемое решение при создании ETL на основе хранилища данных – перенести как можно больше вычислительных процессов на хранилище данных. Это нормально, если у вас есть движок хранилища данных, который автоматически масштабируется в зависимости от спроса. Но это не всегда применимо для каждой компании, ситуации или команды. Некоторые запросы ML могут быть очень долгими и легко перегружать кластер. Обычно данные агрегируются из очень разнообразных таблиц, просматриваются данные за годы, выполняются операторы точки во времени и т. д. Поэтому переносить все на кластер не всегда является лучшим вариантом. Изолирование вычислений в памяти экземпляра процесса может быть безопаснее в некоторых случаях. Это безопасно, потому что вы не ударите по кластеру и потенциально не сломаете или не задержите важные для бизнеса запросы. Для пользователей Spark это очевидная ситуация, поскольку все вычисления и данные распределяются по исполнителям из-за огромного масштаба, который им необходим. Но если вы работаете с кластерами Redshift или BigQuery, всегда следите за тем, сколько вычислений вы можете делегировать им.
Отслеживание выходных данных
ML ETL генерирует разные типы выходных артефактов. Некоторые из них – это файлы Parquet в HDFS, файлы CSV в S3, таблицы в хранилище данных, файлы сопоставления, отчеты и т. д. Эти файлы могут затем использоваться для обучения моделей, обогащения данных в производстве, получения функций в режиме реального времени и многих других вариантов.
Это очень полезно, так как вы можете связывать задания по созданию наборов данных с заданиями по обучению при помощи идентификатора артефактов. Например, при использовании метода Neptune track_files(), вы можете отслеживать подобные файлы. Здесь есть очень ясный пример, которым вы можете воспользоваться.
Внедрение автоматического заполнения
Представьте, что у вас есть ежедневное ETL, которое получает данные прошлого дня, чтобы вычислить функцию, используемую для обучения модели. Если по какой-либо причине ваше ETL не выполняется в течение дня, в следующий день вы потеряете вычисленные данные предыдущего дня.
Чтобы решить эту проблему, хорошей практикой является просмотр последней зарегистрированной временной метки в таблице или файле назначения. Затем ETL может выполняться за отставшие два дня.
Разработка слабо связанных компонентов
Код очень подвержен изменениям, и процессы, зависящие от данных, еще более. События, создающие таблицы, могут развиваться, столбцы могут меняться, размеры могут увеличиваться и так далее. Когда у вас есть ETL, которые зависят от разных источников информации, всегда лучше изолировать их в коде. Это потому что, если в любой момент вам придется разделить оба компонента как две разные задачи (например, одна требует более крупного типа инстанса для выполнения обработки из-за увеличения данных), это гораздо легче сделать, если код не является спагетти!
Сделайте ваши ETL-процессы идемпотентными
Обычно один и тот же процесс запускается несколько раз в случае, если возникла проблема с исходными таблицами или самим процессом. Чтобы избежать создания дублированных данных или заполнения таблиц наполовину, ETL-процессы должны быть идемпотентными. То есть, если вы случайно запустите тот же ETL дважды с теми же условиями, что и первый раз, вывод или побочные эффекты от первого запуска не должны быть затронуты (ссылка). Вы можете обеспечить это, применив шаблон delete-write в вашем ETL, конвейер сначала удалит существующие данные, а затем запишет новые данные.
Держите ваш код ETL лаконичным
Я всегда стараюсь ясно разделять реализационный код от бизнес-логического уровня. При создании ETL первый уровень должен читаться как последовательность шагов (функций или методов), которые четко указывают, что происходит с данными. Наличие нескольких уровней абстракции — не плохо. Это очень полезно, если вы должны поддерживать ETL в течение нескольких лет.
Всегда изолируйте высокоуровневые и низкоуровневые функции друг от друга. Очень странно видеть что-то вроде:
from config import CONVERSION_FACTORSdef transform_data(data: pd.DataFrame) -> pd.DataFrame: data = remove_duplicates(data=data) data = encode_categorical_columns(data=data) data["price_dollars"] = data["price_euros"] * CONVERSION_FACTORS["dollar-euro"] data["price_pounds"] = data["price_euros"] * CONVERSION_FACTORS["pound-euro"] return data
В приведенном выше примере мы используем высокоуровневые функции, такие как «remove_duplicates» и «encode_categorical_columns», но в то же время явно показываем операцию реализации для конвертации цены с помощью коэффициента конвертации. Не хотели бы вы удалить эти 2 строки кода и заменить их функцией «convert_prices»?
from config import CONVERSION_FACTORdef transform_data(data: pd.DataFrame) -> pd.DataFrame: data = remove_duplicates(data=data) data = encode_categorical_columns(data=data) data = convert_prices(data=data) return data
В этом примере читаемость не была проблемой, но представьте, что вместо этого в «transform_data» вы внедрили операцию groupby длиной в 5 строк вместе с «remove_duplicates» и «encode_categorical_columns». В обоих случаях вы смешиваете высокоуровневые и низкоуровневые функции. Очень рекомендуется использовать связный слоеный код. Иногда невозможно сделать функцию или модуль полностью связным на 100%, но это очень полезная цель, которую стоит преследовать.
Используйте чистые функции
Не позволяйте побочным эффектам или глобальному состоянию усложнять ваши ETL-процессы. Чистые функции возвращают те же результаты при передаче тех же аргументов.
❌ Функция ниже не является чистой. Вы передаете кадр данных, который объединен с другой функцией, считываемой из внешнего источника. Это означает, что таблица может измениться и возвращать разные кадры данных, потенциально, каждый раз при вызове функции с теми же аргументами.
def transform_data(data: pd.DataFrame) -> pd.DataFrame: reference_data = read_reference_data(table="public.references") data = data.join(reference_data, on="ref_id") return data
Чтобы сделать эту функцию понятной, вам нужно сделать следующее:
def transform_data(data: pd.DataFrame, reference_data: pd.DataFrame) -> pd.DataFrame: data = data.join(reference_data, on="ref_id") return data
Теперь, передав одни и те же аргументы “data” и “reference_data”, функция будет давать одинаковые результаты.
Это простой пример, но мы все сталкивались с более худшими ситуациями. Функции, которые зависят от глобальных переменных состояния, методы, которые изменяют состояние атрибутов класса на основе определенных условий, потенциально изменяющие поведение других последующих методов в ETL и т. д.
Максимальное использование чистых функций приводит к более функциональным ETL. Как уже обсуждалось выше, это приносит большие преимущества.
Параметризуйте все, что можно
ETL-процессы меняются. Это то, с чем нам приходится иметь дело. Изменяются определения исходных таблиц, меняются бизнес-правила, эволюционируют желаемые результаты, уточняются эксперименты, требуются более сложные функции для моделей машинного обучения и т. д.
Чтобы иметь хоть какую-то гибкость в наших ETL-процессах, нам нужно тщательно оценить, где стоит приложить больше усилий для обеспечения параметризованных выполнений ETL-процессов. Параметризация – это характеристика, которая позволяет изменять поведение процесса просто изменив параметры через простой интерфейс. Интерфейсом может быть файл YAML, метод инициализации класса, аргументы функции или даже аргументы командной строки.
Простой и прямолинейный способ параметризации – это определение «окружения» или «стадии» ETL-процесса. Перед запуском ETL-процесса в рабочую среду, где он может повлиять на последующие процессы и системы, хорошо иметь изолированные «тестовые», «интеграционные» или «dev» окружения, чтобы мы могли тестировать наши ETL-процессы. Такая среда может включать различные уровни изоляции. Она может варьироваться от инфраструктуры выполнения (экземпляры dev, изолированные от экземпляров production) до хранилищ объектов, хранилищ данных, источников данных и т. д.
Это очевидный параметр и, вероятно, самый важный. Но также мы можем расширить параметризацию до бизнес-ориентированных аргументов. Мы можем параметризовать даты окон для запуска ETL-процесса, имена столбцов, которые могут измениться или быть уточнены, типы данных, значения фильтрации и т. д.
Только нужное количество журналирования
Это одно из самых недооцениваемых свойств ETL. Журналы полезны для обнаружения необычных аномалий в процессе выполнения ETL, для выявления скрытых ошибок или объяснения наборов данных. Всегда полезно регистрировать свойства извлеченных данных. Кроме встроенных проверок кода, чтобы убедиться, что различные этапы ETL выполняются успешно, мы также можем журналировать:
- Ссылки на исходные таблицы, API или пути назначения (Например: “Получение данных из таблицы `item_clicks`”)
- Изменения в ожидаемых схемах (Например: “В таблице `promotion` появился новый столбец”)
- Количество выбранных строк (Например: “Выбрано 145234093 строк из таблицы `item_clicks`”)
- Количество нулевых значений в критических столбцах (Например: “Найдено 125 нулевых значений в столбце Source”)
- Простую статистику данных (например, среднее значение, стандартное отклонение и т. д.) (Например: “Среднее значение CTR: 0,13, стандартное отклонение CTR: 0.40”)
- Уникальные значения для категориальных столбцов (Например: “Столбец Country содержит: ‘Испания’, ‘Франция’ и ‘Италия'”)
- Количество удаленных дублированных строк (Например: “Удалено 1400 дублированных строк”)
- Время выполнения вычислительных операций (Например: “Агрегация заняла 560 секунд”)
- Контрольные точки для различных этапов ETL (например, “Процесс обогащения завершился успешно”)
Manuel Martín – руководитель инженеров с более чем 6-летним опытом в области науки о данных. Ранее он работал в качестве специалиста по обработке данных и инженера машинного обучения, а сейчас он руководит практикой МЛ/ИИ в Busuu.
[Мануель Мартин](https://www.linkedin.com/in/manuelmart%C3%ADn11/) – менеджер по инженерии с более чем 6-летним опытом в области науки о данных. Ранее он работал в качестве ученого в области данных и инженера по машинному обучению, а сейчас он является руководителем практики МО/ИИ в Busuu.