Finest Practices for Constructing ETLs for ML

Finest Practices for Constructing ETLs for ML

An integral a part of ML Engineering is constructing dependable and scalable procedures for extracting information, reworking it, enriching it and loading it in a particular file retailer or database. This is among the elements through which the information scientist and the ML engineer collaborate probably the most. Sometimes, the information scientist comes up with a tough model of what the information set ought to appear like. Ideally, not on a Jupyter pocket book. Then, the ML engineer joins this job to assist making the code extra readable, environment friendly and dependable.

ML ETLs may be composed of a number of sub-ETLs or duties. And they are often materialized in very completely different types. Some widespread examples:

  • Scala-based Spark job studying and processing occasion log information saved in S3 as Parquet information and scheduled by Airflow on a weekly foundation.
  • Python course of executing a Redshift SQL question by a scheduled AWS Lambda perform.
  • Advanced pandas-heavy processing executed by a Sagemaker Processing Job utilizing EventBridge triggers.



We are able to establish completely different entities in these kinds of ETLs, we’ve got Sources (the place the uncooked information lives), Locations (the place the ultimate information artifact will get saved), Information Processes (how the information will get learn, processed and loaded) and Triggers (how the ETLs get initiated).


Finest Practices for Constructing ETLs for ML


  • Beneath the Sources, we will have shops resembling AWS Redshift, AWS S3, Cassandra, Redis or exterior APIs. Locations are the identical.
  • The Information Processes are sometimes run below ephemeral Docker containers. We might add one other stage of abstraction utilizing Kubernetes or another AWS managed service resembling AWS ECS or AWS Fargate. And even SageMaker Pipelines or Processing Jobs.You may run these processes in a cluster by leveraging particular information processing engines resembling Spark, Dask, Hive, Redshift SQL engine. Additionally, you need to use easy single-instance processes utilizing Python processes and Pandas for information processing. Other than that, there are another fascinating frameworks resembling Polars, Vaex, Ray or Modin which may be helpful to deal with intermediate options.
  • The most well-liked Set off software is Airflow. Others that can be utilized are Prefect, Dagster, Argo Workflows or Mage.


Best Practices for Building ETLs for ML



A framework is a set of abstractions, conventions and out-of-the-box utilities that can be utilized to create a extra uniform codebase when utilized to concrete issues. Frameworks are very handy for ETLs. As we’ve beforehand described, there are very generic entities that might probably be abstracted or encapsulated to generate complete workflows.

The development that I might take to construct an inner information processing framework is the next:

  • Begin by constructing a library of connectors to the completely different Sources and Locations. Implement them as you want them all through the completely different tasks you’re employed on. That’s one of the best ways to keep away from YAGNI.
  • Create easy and automatic improvement workflow that means that you can iterate rapidly the codebase. For instance, configure CI/CD workflows to mechanically take a look at, lint and publish your package deal.
  • Create utilities resembling studying SQL scripts, spinning up Spark classes, dates formatting features, metadata turbines, logging utilities, features for fetching credentials and connection parameters and alerting utilities amongst others.
  • Select between constructing an inner framework for writing workflows or use an current one. The complexity scope is extensive when contemplating this in-house improvement. You can begin with some easy conventions when constructing workflows and find yourself constructing some DAG-based library with generic courses resembling Luigi or Metaflow. These are widespread frameworks that you need to use.



It is a important and central a part of your information codebase. All of your processes will use this library to maneuver information round from one supply into one other vacation spot. A strong and well-though preliminary software program design is vital.


Best Practices for Building ETLs for ML


However why would we need to do that? Properly, the principle causes are:

  • Reusability: Utilizing the identical software program elements in several software program tasks permits for larger productiveness. The piece of software program must be developed solely as soon as. Then, it may be built-in into different software program tasks. However this concept will not be new. We are able to discover references again in 1968 on a convention whose goal was to unravel the so-called software program disaster. 
  • Encapsulation: Not all of the internals of the completely different connectors used by the library should be proven to end-users. Therefore, by offering an comprehensible interface, that’s sufficient. For instance, if we had a connector to a database, we wouldn’t like that the connection string acquired uncovered as a public attribute of the connector class. By utilizing a library we will make sure that safe entry to information sources is assured. Evaluate this bit
  • Increased-quality codebase: We now have to develop assessments solely as soon as. Therefore, builders can depend on the library as a result of it incorporates a take a look at suite (Ideally, with a really excessive take a look at protection). When debugging for errors or points we will ignore, at the least at first go, that the problem is throughout the library if we’re assured on our take a look at suite.
  • Standardisation / “Opinionation”: Having a library of connectors determines, in sure approach, the way in which you develop ETLs. That’s good, as a result of ETLs within the group may have the identical methods of extracting or writing information into the completely different information sources. Standardisation results in higher communication, extra productiveness and higher forecasting and planning.

When constructing one of these library, groups commit to take care of it over time and assume the chance of getting to implement advanced refactors when wanted. Some causes of getting to do these refactors is perhaps:

  • The organisation migrates to a special public cloud.
  • The info warehouse engine adjustments.
  • New dependency model breaks interfaces.
  • Extra safety permission checks should be put in place.
  • A brand new workforce is available in with completely different opinions concerning the library design.a


Interface courses


If you wish to make your ETLs agnostic of the Sources or Locations, it’s a good choice to create interface courses for base entities. Interfaces function template definitions.

For instance, you’ll be able to have summary courses for outlining required strategies and attributes of a DatabaseConnector.  Let’s present a simplified instance of how this class might appear like:

from abc import ABC

class DatabaseConnector(ABC):
    def __init__(self, connection_string: str):
        self.connection_string = connection_string

    def join(self):

    def execute(self, sql: str):


Different builders would subclass from the DatabaseConnector and create new concrete implementations. For example, a MySqlConnector or CassandraDbConnector might be carried out on this trend.  This could assist end-users to rapidly perceive the way to use any connector subclassed from the DatabaseConnector as all of them may have the identical interface (similar strategies).

mysql = MySqlConnector(connection_string)
mysql.execute("SELECT * FROM public.desk")

cassandra = CassandraDbConnector(connection_string)
cassandra.execute("SELECT * FROM public.desk")


Simples interfaces with well-named strategies are very highly effective and permit for higher productiveness. So my recommendation is to spend high quality time fascinated by it.


The proper documentation


Documentation not solely refers to docstrings and inline feedback within the code. It additionally refers back to the surrounding explanations you give concerning the library. Writing a daring assertion about what’s the top objective of the package deal and a pointy rationalization of the necessities and tips to contribute is important.

For instance:

"This utils library will likely be used throughout all of the ML information pipelines and have engineering jobs to supply easy and dependable connectors to the completely different methods within the group".



"This library incorporates a set of characteristic engineering strategies, transformations and algorithms that can be utilized out-of-the-box with a easy interface that may be chained in a scikit-learn-type of pipeline".


Having a transparent mission of the library paves the way in which for an accurate interpretation from contributors. That is why open supply libraries (E.g: pandas, scikit-learn, and so forth) have gained such an amazing reputation these final years. Contributors have embraced the objective of the library and they’re dedicated to comply with the outlined requirements. We ought to be doing one thing fairly comparable at organizations.

Proper after the mission is acknowledged, we should always develop the foundational software program structure. How do we would like our interfaces to appear like? Ought to we cowl performance by extra flexibility within the interface strategies (e.g: extra arguments that result in completely different behaviours) or extra granular strategies (e.g: every technique has a really particular perform)?

After having that, the styleguide. Define the popular modules hierarchy, the documentation depth required, the way to publish PRs, protection necessities, and so forth.

With respect to documentation within the code, docstrings should be sufficiently descriptive of the perform behaviour however we shouldn’t fall into simply copying the perform identify. Generally, the perform identify is sufficiently expressive {that a} docstring explaining its behaviour is simply redundant. Be concise and correct. Let’s present a dumb instance:


class NeptuneDbConnector:
	def shut():
	    """This perform checks if the connection to the database
             is opened. Whether it is, it closes it and if it doesn’t,
             it does nothing.



class NeptuneDbConnector:
	def shut():
	    """Closes connection to the database."""


Coming to the subject of inline feedback, I all the time like to make use of them to clarify sure issues which may appear bizarre or irregular. Additionally, if I’ve to make use of a posh logic or fancy syntax, it’s all the time higher should you write a transparent rationalization on high of that snippet.

# Getting the utmost integer of the checklist
l = [23, 49, 6, 32]
scale back((lambda x, y: x if x > y else y), l)


Other than that, you can too embody hyperlinks to Github points or Stackoverflow solutions. That is actually helpful, specifically should you needed to code a bizarre logic simply to beat a recognized dependency subject. It’s also actually handy once you needed to implement an optimisation trick that you just acquired from Stackoverflow.

These two, interface courses and clear documentation are, for my part, the most effective methods to maintain a shared library alive for a very long time. It would resist lazy and conservative new builders and likewise fully-energized, radical and extremely opinionated ones. Adjustments, enhancements or revolutionary refactors will likely be clean.



From a code perspective, ETLs ought to have 3 clearly differentiated high-level features. Every one associated to one of many following steps: Extract, Remodel, Load. This is among the easiest necessities for ETL code.

def extract(supply: str) -> pd.DataFrame:

def rework(information: pd.DataFrame) -> pd.DataFrame:

def load(transformed_data: pd.DataFrame):


Clearly, it’s not obligatory to call these features like this, however it offers you a plus on readability as they’re broadly accepted phrases.


DRY (Don’t Repeat Your self)

This is among the nice design patterns which justifies a connectors library. You write it as soon as and reuse it throughout diferent steps or tasks.

Purposeful Programming

It is a programming fashion that goals at making features “pure” or with out side-effects. Inputs have to be immutable and outputs are all the time the identical given these inputs. These features are simpler to check and debug in isolation. Subsequently, offers a greater diploma of reproducibility to information pipelines.

With practical programming utilized to ETLs, we should always be capable of present idempotency. Which means each time we run (or re-run) the pipeline, it ought to return the identical outputs. With this attribute, we’re capable of confidently function ETLs and ensure that double runs gained’t generate duplicate information. What number of instances you needed to create a bizarre SQL question to take away inserted rows from a flawed ETL run? Making certain idempotency helps avoiding these conditions. Maxime Beauchemin, creator of Apache Airflow and Superset, is one recognized advocate for Purposeful Information Engineering.





We are going to use references to courses definitions, however this part can be utilized to first-class features. We will likely be utilizing heavy object-oriented programming to clarify these ideas, however it doesn’t imply that is one of the best ways of growing an ETL. There’s not a particular consensus and every firm does it by itself approach.


Relating to the Single Accountability Precept, it’s essential to create entities which have just one purpose to vary. For instance, segregating duties amongst two objects resembling a SalesAggregator and a SalesDataCleaner class. The latter is vulnerable to comprise particular enterprise guidelines to “clear” information from gross sales, and the previous is targeted on extracting gross sales from disparate methods. Each courses code can change due to completely different causes.

For the Open-Shut Precept, entities ought to be expandable so as to add new options however not opened to be modified. Think about that the SalesAggregator acquired as elements a StoresSalesCollector which is used to extract gross sales from bodily shops. If the corporate began promoting on-line and we needed to get that information, we’d state that SalesCollector is open for extension if it could actually obtain additionally one other OnlineSalesCollector with a appropriate interface.

from abc import ABC, abstractmethod

class BaseCollector(ABC):
      def extract_sales() -> Record[Sale]:

class SalesAggregator:
      def __init__(self, collectors: Record[BaseCollector]):
		self.collectors = collectors
      def get_sales(self) -> Record[Sale]: 
		gross sales = []
		for collector in self.collectors:
			gross sales.lengthen(collector.extract_sales())
		return gross sales

class StoreSalesCollector:
	def extract_sales() -> Record[Sale]:
		# Extract gross sales information from bodily shops

class OnlineSalesCollector:
	def extract_sales() -> Record[Sale]:
		# Extract on-line gross sales information

if __name__ == "__main__":
     sales_aggregator = SalesAggregator(
            collectors = [
     gross sales = sales_aggregator.get_sales()


The Liskov substitution precept, or behavioural subtyping will not be so simple to use to ETL design, however it’s for the utilities library we talked about earlier than. This precept tries to set a rule for subtypes. In a given program that makes use of the supertype, one might potential substitute it with one subtype with out altering the behaviour of this system.

from abc import ABC, abstractmethod

class DatabaseConnector(ABC):
	def __init__(self, connection_string: str):
		self.connection_string = connection_string

	def join():

	def execute_(question: str) -> pd.DataFrame:

class RedshiftConnector(DatabaseConnector):
	def join():
	# Redshift Connection implementation

	def execute(question: str) -> pd.DataFrame:
	# Redshift Connection implementation

class BigQueryConnector(DatabaseConnector):
	def join():
	# BigQuery Connection implementation

	def execute(question: str) -> pd.DataFrame:
	# BigQuery Connection implementation

class ETLQueryManager:
	def __init__(self, connector: DatabaseConnector, connection_string: str):
		self.connector = connector(connection_string=connection_string).join()

	def run(self, sql_queries: Record[str]):
		for question in sql_queries:


We see within the instance beneath that any of the DatabaseConnector subtypes conform to the Liskov substitution precept as any of its subtypes might be used throughout the ETLManager class.

Now, let’s discuss concerning the Interface Segregation Precept. It states that purchasers shouldn’t rely upon interfaces they don’t use. This one comes very helpful for the DatabaseConnector design. If you happen to’re implementing a DatabaseConnector, don’t overload the interface class with strategies that gained’t be used within the context of an ETL. For instance, you gained’t want strategies resembling grant_permissions, or check_log_errors. These are associated to an administrative utilization of the database, which isn’t the case.

The one however not least, the Dependency Inversion precept. This one says that high-level modules shouldn’t rely upon lower-level modules, however as an alternative on abstractions. This behaviour is clearly exemplified with the SalesAggregator above. Discover that its __init__ technique doesn’t rely upon concrete implementations of both StoreSalesCollector or OnlineSalesCollector. It principally depends upon a BaseCollector interface.



We’ve closely depend on object-oriented courses within the examples above to point out methods through which we will apply SOLID rules to ETL jobs. However, there isn’t any basic consensus of what’s the most effective code format and commonplace to comply with when constructing an ETL. It may well take very completely different types and it tends to be extra an issue of getting an inner well-documented opinionated framework, as mentioned beforehand, moderately than making an attempt to provide you with a worldwide commonplace throughout the {industry}.


Best Practices for Building ETLs for ML


Therefore, on this part, I’ll attempt to deal with explaining some traits that make ETL code extra legible, safe and dependable.


Command Line Functions


All Information Processes that you can imagine are principally command line purposes. When growing your ETL in Python, all the time present a parametrized CLI interface so as to execute it from anywhere (E.g, a Docker container that may run below a Kubernetes cluster). There are a number of instruments for constructing command-line arguments parsing resembling  argparse, click on, typer, yaspin or docopt. Typer is presumably probably the most versatile, straightforward to make use of an non-invasive to your current codebase. It was constructed by the creator of the well-known Python net companies library FastApi, and its Github begins continue to grow. The documentation is nice and is changing into increasingly more industry-standard.

from typer import Typer

app = Typer()

def run_etl(
    atmosphere: str,
    start_date: str,
    end_date: str,
    threshold: int


To run the above command, you’d solely should do:

python {file_name}.py run-etl --environment dev --start-date 2023/01/01 --end-date 2023/01/31 --threshold 10


Course of vs Database Engine Compute Commerce Off


The everyday advice when constructing ETLs on high of a Information Warehouse is to push as a lot compute processing to the Information Warehouse as attainable. That’s all proper you probably have an information warehouse engine that autoscales primarily based on demand. However that’s not the case for each firm, scenario or workforce. Some ML queries may be very lengthy and overload the cluster simply. It’s typical to mixture information from very disparate tables, lookback for years of information, carry out point-in-time clauses, and so forth. Therefore, pushing every part to the cluster will not be all the time the most suitable choice. Isolating the compute into the reminiscence of the method occasion may be safer in some instances. It’s risk-free as you gained’t hit the cluster and probably break or delay business-critical queries. That is an apparent scenario for Spark customers, as all of the compute & information will get distributed throughout the executors due to the huge scale they want. However should you’re working over Redshift or BigQuery clusters all the time hold a watch into how a lot compute you’ll be able to delegate to them.


Observe Outputs


ML ETLs generate several types of output artifacts. Some are Parquet information in HDFS, CSV information in S3, tables within the information warehouse, mapping information, stories, and so forth. These information can later be used to coach fashions, enrich information in manufacturing, fetch options on-line and lots of extra choices.

That is fairly useful as you’ll be able to hyperlink dataset constructing jobs with coaching jobs utilizing the identifier of the artifacts. For instance, when utilizing Neptune track_files() technique, you’ll be able to observe these form of information. There’s a really clear instance right here that you need to use.


Implement Computerized Backfilling


Think about you’ve gotten a day by day ETL that will get final day’s information to compute a characteristic used to coach a mannequin If for any purpose your ETL fails to run for a day, the following day it runs you’d have misplaced the day prior to this information computed.

To resolve this, it’s an excellent follow to take a look at what’s the final registered timestamp within the vacation spot desk or file. Then, the ETL may be executed for these lagging two days.


Develop Loosely Coupled Parts


Code could be very vulnerable to vary, and processes that rely upon information much more. Occasions that construct up tables can evolve, columns can change, sizes can enhance, and so forth. When you’ve gotten ETLs that rely upon completely different sources of data is all the time good to isolate them within the code. It is because if at any time you need to separate each elements as two completely different duties (E.g: One wants an even bigger occasion kind to run the processing as a result of the information has grown), it’s a lot simpler to do if the code will not be spaghetti!


Make Your ETLs Idempotent


It’s typical to run the identical course of greater than as soon as in case there was a difficulty on the supply tables or throughout the course of itself. To keep away from producing duplicate information outputs or half-filled tables, ETLs ought to be idempotent. That’s, should you by chance run the identical ETL twice with the identical situations that the primary time, the output or side-effects from the primary run shouldn’t be affected (ref). You may guarantee that is imposed in your ETL by making use of the delete-write sample, the pipeline will first delete the prevailing information earlier than writing new information.


Maintain Your ETLs Code Succinct


I all the time prefer to have a transparent separation between the precise implementation code from the enterprise/logical layer. After I’m constructing an ETL, the primary layer ought to be learn as a sequence of steps (features or strategies) that clearly state what is occurring to the information. Having a number of layers of abstraction will not be unhealthy. It’s very useful you probably have have to take care of the ETL for years.

All the time isolate high-level and low-level features from one another. It is extremely bizarre to seek out one thing like:

from config import CONVERSION_FACTORS

def transform_data(information: pd.DataFrame) -> pd.DataFrame:
    information = remove_duplicates(information=information)
    information = encode_categorical_columns(information=information)
    information["price_dollars"] = information["price_euros"] * CONVERSION_FACTORS["dollar-euro"]
    information["price_pounds"] = information["price_euros"] * CONVERSION_FACTORS["pound-euro"]
    return information


Within the instance above we’re utilizing high-level features such because the “remove_duplicates” and “encode_categorical_columns” however on the similar time we’re explicitly exhibiting an implementation operation to transform the value with a conversion issue. Wouldn’t it’s nicer to take away these 2 strains of code and change them with a “convert_prices” perform?

from config import CONVERSION_FACTOR

def transform_data(information: pd.DataFrame) -> pd.DataFrame:
    information = remove_duplicates(information=information)
    information = encode_categorical_columns(information=information)
    information = convert_prices(information=information)
    return information


On this instance, readability wasn’t an issue, however think about that as an alternative, you embed a 5 strains lengthy groupby operation within the “transform_data” together with the “remove_duplicates” and “encode_categorical_columns”. In each instances, you’re mixing high-level and low-level features. It’s extremely really helpful to maintain a cohesive layered code. Generally is inevitable and over-engineered to maintain a perform or module 100% cohesively layered, however it’s a really useful objective to pursue.


Use Pure Capabilities


Don’t let side-effects or world states complicate your ETLs. Pure features return the identical outcomes if the identical arguments are handed.

❌The perform beneath will not be pure. You’re passing a dataframe that’s joined with one other features that’s learn from an outdoor supply. Which means the desk can change, therefore, returning a special dataframe, probably, every time the perform known as with the identical arguments.

def transform_data(information: pd.DataFrame) -> pd.DataFrame:
    reference_data = read_reference_data(desk="public.references")
    information = part of(reference_data, on="ref_id")
    return information


To make this perform pure, you would need to do the next:

def transform_data(information: pd.DataFrame, reference_data: pd.DataFrame) -> pd.DataFrame:
    information = part of(reference_data, on="ref_id")
    return information


Now, when passing the identical “information” and “reference_data” arguments, the perform will yield the identical outcomes.

It is a easy instance, however all of us have witnessed worse conditions. Capabilities that depend on world state variables, strategies that change the state of sophistication attributes primarily based on sure situations, probably altering the behaviour of different upcoming strategies within the ETL, and so forth.

Maximising the usage of pure features results in extra practical ETLs. As we’ve got already mentioned in factors above, it comes with nice advantages.


Paremetrize As A lot As You Can


ETLs change. That’s one thing that we’ve got to imagine. Supply desk definitions change, enterprise guidelines change, desired outcomes evolve, experiments are refined, ML fashions require extra refined options, and so forth.

As a way to have some extent of flexibility in our ETLs, we have to completely assess the place to place a lot of the effort to supply parametrised executions of the ETLs. Parametrisation is a attribute through which, simply by altering parameters by a easy interface, we will alter the behaviour of the method. The interface could be a YAML file, a category initialisation technique, perform arguments and even CLI arguments.

A easy simple parametrisation is to outline the “atmosphere”, or “stage” of the ETL. Earlier than working the ETL into manufacturing, the place it could actually have an effect on downstream processes and methods, it’s good to have a “take a look at”, “integration” or “dev” remoted environments in order that we will take a look at our ETLs. That atmosphere would possibly contain completely different ranges of isolation. It may well go from the execution infrastructure (dev cases remoted from manufacturing cases), object storage, information warehouse, information sources, and so forth.

That’s an apparent parameter and doubtless a very powerful. However we will broaden the parametrisation additionally to business-related arguments. We are able to parametrise window dates to run the ETL, columns names that may change or be refined, information varieties, filtering values, and so forth.


Simply The Proper Quantity Of Logging


This is among the most underestimated properties of an ETL. Logs are helpful to detect manufacturing executions anomalies or implicit bugs or clarify information units. It’s all the time helpful to log properties about extracted information. Other than in-code validations to make sure the completely different ETL steps run efficiently, we will additionally log:

  • References to supply tables, APIs or vacation spot paths (E.g: “Getting information from `item_clicks` desk”)
  • Adjustments in anticipated schemas (E.g: “There’s a new column in `promotion` desk”)
  • The variety of rows fetched (E.g: “Fetched 145234093 rows from `item_clicks` desk”)
  • The variety of null values in important columns (E.g: “Discovered 125 null values in Supply column”)
  • Easy statistics of information (e.g: imply, commonplace deviation, and so forth). (E.g: “CTR imply: 0.13, CTR std: 0.40)
  • Distinctive values for categorical columns (E.g: “Nation column consists of: ‘Spain’, ‘France’ and ‘Italy’”)
  • Variety of rows deduplicated (E.g: “Eliminated 1400 duplicated rows”)
  • Execution instances for compute-intensive operations (E.g: “Aggregation took 560s”)
  • Completion checkpoints for various levels of the ETL (e.g: “Enrichment course of completed efficiently”)


Manuel Martín is an Engineering Supervisor with greater than 6 years of experience in information science. He have beforehand labored as an information scientist and a machine studying engineer and now I lead the ML/AI follow at Busuu.