Version data pipeline#

Data lineage becomes crucial when quickly experimenting with a small-scale ML project. Usually the source datasets are a mess and a preprocessing can change frequently. This is why it is important to start tracking the changes early in the project.

After reading this you will learn how to:

  • Version your data pipeline

  • Add metadata and fill data cards

  • Avoid version explosions due to volatile fields in meta

Your starting data transformation pipeline#

Here you can see a simplified version of a data pipeline that was built using Cascade. ClientProjectDataset represents an object that could make a database call or read a JSON file. Then there is a set of preprocessing functions each applied using ApplyModifier. The

from cascade.data import ApplyModifier, Dataset


class ClientProjectDataset(Dataset):
    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)

        self._tickets = [
            {"title": "Fix onboarding flow", "owner": "Ava", "effort": 3, "priority": "high"},
            {"title": "Add CSV export", "owner": "Leo", "effort": 5, "priority": "medium"},
            {"title": "Warm cache for dashboard", "owner": "Mina", "effort": 2, "priority": "high"},
        ]

    def __len__(self):
        return len(self._tickets)

    def get(self, index):
        return self._tickets[index]


def add_slug(item):
    item["slug"] = item["title"].lower().replace(" ", "-")
    return item


def score_risk(item):
    item["risk_score"] = item["effort"] + (1 if item["priority"] == "high" else 0)
    return item

RISK_THRESHOLD = 5

def pick_sprint_candidates(item):
    item["sprint_ready"] = item["risk_score"] <= RISK_THRESHOLD and item["owner"] != "Mina"
    return item


pipeline = ClientProjectDataset()
pipeline = ApplyModifier(pipeline, add_slug)
pipeline = ApplyModifier(pipeline, score_risk)
pipeline = ApplyModifier(pipeline, pick_sprint_candidates)

This pipeline can easily be tracked using DataLine.

Enable dataset versioning#

In this example we create client_project_datasets line and save our pipeline there with only_meta flag for this example, but you can always save the pipeline object itself by turning this off.

We can see that the line now knows the version of this pipeline which is 0.1.

from cascade.lines import DataLine

line = DataLine("client_project_datasets")
line.save(pipeline, only_meta=True)

line.get_version(pipeline) # -> 0.1

Add useful metadata#

If we change something in the source data or code DataLine would not notice that unless we added it in the metadata. Here we try to describe our source data using DataCard. It is a helper object that you can create and fill as a way to describe your dataset. We also use update_meta here to track risk threshold parameter and change the pipeline version if it updates.

When we save this updated dataset it will bump its minor version to 0.2.

from cascade.data import DataCard

pipeline = ClientProjectDataset(
    data_card=DataCard(
        name="tickets",
        desc="Dataset of tickets from client's project",
        source="client database",
        goal="We want to train ML model to estimate task effort",
        labeling_info=None,
    )
)
pipeline = ApplyModifier(pipeline, add_slug)
pipeline = ApplyModifier(pipeline, score_risk)
pipeline = ApplyModifier(pipeline, pick_sprint_candidates)

pipeline.tag("testing")
pipeline.update_meta(
    {
        "risk_threshold": RISK_THRESHOLD
    }
)

line.save(pipeline, only_meta=True)

line.get_version(pipeline) # -> 0.2

Track pipeline changes#

Dataset version has two components - major and minor. Minor will bump when anything in metadata changes. And Major will bump if there is a change in the pipeline structure - for example when you add/remove a pipeline stage.

In the following example we add RandomSampler which will bump the version to 1.0.

from cascade.data import RandomSampler


pipeline = RandomSampler(pipeline)

line.save(pipeline, only_meta=True)

line.get_version(pipeline) # -> 1.0

Avoid version explosions#

Minor version will always go up if there is a change in any metadata field. Meaning that if you have a field that is unique for every run your minor versions will update with each run while masking real changes.

To avoid this situation Cascade Dataset provides a method declare_volatiles which helps to mask some values from versioning.

In the following example we subclass the ClientProjectDataset and add the field that will always be different each run.

from datetime import datetime

class TimedClientProjectDataset(ClientProjectDataset):
    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)

        self.query_time = datetime.now()

    def get_meta(self):
        meta = super().get_meta()
        meta[0]["query_time"] = self.query_time
        return meta

def build_pipeline():
    pipeline = TimedClientProjectDataset(
        data_card=DataCard(
            name="timed_tickets",
            desc="Dataset of tickets from client's project but now with time of DB query",
            source="client database",
            goal="We want to train ML model to estimate task effort",
            labeling_info=None,
        )
    )
    pipeline = ApplyModifier(pipeline, add_slug)
    pipeline = ApplyModifier(pipeline, score_risk)
    pipeline = ApplyModifier(pipeline, pick_sprint_candidates)

    pipeline.tag("testing")
    pipeline.update_meta(
        {
            "risk_threshold": RISK_THRESHOLD,
        }
    )
    return pipeline

pipeline = build_pipeline()

line.get_version(pipeline) # -> 2.0

line.save(pipeline, only_meta=True)

We expected the version to change since we updated the pipeline. But now if we rebuild it without any changes the minor version will still go up.

pipeline = build_pipeline()

line.get_version(pipeline) # -> 2.1

line.save(pipeline, only_meta=True)

Let’s avoid it by using declare_volatiles. It will say DataLine to not use query_time field when versioning this dataset.

class TimedClientProjectDataset(ClientProjectDataset):
    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)

        self.query_time = datetime.now()

        self.declare_volatiles("query_time")

    def get_meta(self):
        meta = super().get_meta()
        meta[0]["query_time"] = self.query_time
        return meta

Now we also expect the version to change since we changed how this field is represented.

pipeline = build_pipeline()

line.get_version(pipeline) # -> 2.2

But from now on, when creating a new dataset DataLine will version it consistently.

pipeline = build_pipeline()

line.get_version(pipeline) # -> 2.2

See also#