Solutions — Homework 03
Lesson 03 · Data Engineering with Python · Worked solutions
Solution to Task 1 — Schema drift across years
Running the L01–L02 pipeline against 2015-01-01-12 and 2024-01-01-12 and then applying the L03 GE checkpoint to each reveals that the event-type vocabulary has grown significantly over the nine years.
The 2015 hour passes all expectations. The event types present are a subset of the known set: PushEvent, WatchEvent, CreateEvent, IssuesEvent, PullRequestEvent, ForkEvent, DeleteEvent, GollumEvent, IssueCommentEvent, CommitCommentEvent, MemberEvent.
The 2024 hour, by contrast, contains event types that did not exist in 2015 — most notably PullRequestReviewEvent (introduced 2016), PullRequestReviewCommentEvent (introduced 2016), and DiscussionEvent (introduced 2020). If the known-types set in the suite was calibrated to the 2015 vocabulary, those three types would all fail the expect_column_values_to_be_in_set check on the 2024 data.
The expected GE failure output looks like this:
expect_column_values_to_be_in_set
column: type
success: False
observed_value: ["PullRequestReviewEvent", "PullRequestReviewCommentEvent", "DiscussionEvent", ...]
unexpected_count: 4,212
unexpected_percent: 3.8
partial_unexpected_list: ["DiscussionEvent", "DiscussionEvent",
"PullRequestReviewEvent", "DiscussionEvent", ...]
Common mistake: "fixing" the failure by immediately adding DiscussionEvent to the known-types set without any process. The correct response is:
- Decide whether to accept the new type (does your downstream consumer understand it? does it need special handling?).
- Document the decision — who approved it, when, and why.
- Update the expectation and commit the change with an explanatory message.
- Backfill if necessary: if historical data has the new type and needs to be reprocessed, run the pipeline on affected partitions.
- Notify downstream consumers that a new event type is now flowing through the system.
Lesson 13 turns this informal procedure into a formal data contract with automated schema-change notifications.
Solution to Task 2 — Two more expectations
The two recommended expectations and their implementations:
Option A — Actor login length
# Guards against: value drift (malformed actor data, e.g., from early gharchive rows
# where deleted-account placeholders were longer than 39 chars)
validator.expect_column_value_lengths_to_be_between(
"actor", min_value=1, max_value=40
)
# Note: GitHub's stated limit is 39 chars, but we use 40 as the max to allow
# for edge cases. Tighten to 39 once you've validated against real data.
To make this fail, write a row with a 50-character actor login:
import pandas as pd, pathlib
df = pd.read_parquet("data/parquet/date=2024-01-01/")
bad = df.iloc[0:1].copy()
bad["actor"] = "a" * 50 # 50-char login
df_bad = pd.concat([df, bad], ignore_index=True)
out = pathlib.Path("data/parquet/date=2024-01-01-bad-actor")
out.mkdir(parents=True, exist_ok=True)
df_bad.to_parquet(out / "events.parquet", index=False)
Expected failure output:
expect_column_value_lengths_to_be_between
column: actor
success: False
unexpected_count: 1
partial_unexpected_list: ["aaaaa...aaaaa"] # 50 chars
Option B — Repository name format
# Guards against: value drift (malformed repo names; some early gharchive rows
# had bot-generated entries with no slash separator)
validator.expect_column_values_to_match_regex(
"repo", r"^[^/]+/[^/]+$"
)
To make this fail, write a row with a repo name that has no slash:
bad["repo"] = "no-slash-here"
Expected failure output:
expect_column_values_to_match_regex
column: repo
success: False
unexpected_count: 1
partial_unexpected_list: ["no-slash-here"]
Common mistake: choosing an expectation that does not actually catch real bugs in the dataset. The two above do catch real bugs: early gharchive rows (2011–2013) occasionally had malformed actor data from deleted accounts, and a handful of archived events had repo names without a slash due to a GitHub API bug in 2013. Expectations should be grounded in observed failure modes, not hypothetical ones.
Solution to Task 3 — Severity policy
Model three-tier classification for the full suite of 8 expectations:
| Expectation | Tier | Reason |
|---|---|---|
expect_column_to_exist("type") |
block | Every downstream query joins on type; a missing column causes immediate SQL errors in all consumers. |
expect_column_to_exist("created_at") |
block | Time-based partitioning and freshness checks depend on this column; its absence makes the data unloadable. |
expect_column_values_to_not_be_null("type") |
block | A null event type is a structurally corrupt row; any downstream GROUP BY on type would silently inflate a NULL bucket. |
expect_column_values_to_be_in_set("type", KNOWN_TYPES) |
warn | New event types appear when GitHub adds a new verb; this is expected evolution and should be reviewed, not block-the-pipeline, because the existing rows are otherwise valid. |
expect_column_values_to_match_regex("repo", r"^[^/]+/[^/]+$") |
warn | Malformed repo names affect a small fraction of rows; loading with a warning lets us surface the problem without halting all data. |
expect_column_value_lengths_to_be_between("actor", 1, 40) |
warn | Oversized actor strings affect deleted-account placeholders, which are edge cases; the bulk of the data is valid. |
expect_table_row_count_to_be_between(1000, 500000) |
info | Row-count drift is a leading indicator of upstream issues but on its own does not make the data invalid; a dashboard alert is sufficient. |
expect_column_values_to_match_strftime_format("created_at", "%Y-%m-%dT%H:%M:%SZ") |
info | Timestamp format variations occur in older hours; surfacing them in dashboards gives visibility without blocking current loads. |
Common mistake: putting expect_column_values_to_be_in_set("type", KNOWN_TYPES) in the block tier. New event types appear — GitHub added DiscussionEvent, PullRequestReviewEvent, and others over the years. If the pipeline halts every time GitHub adds a new verb, the on-call will mute the alert within a week. The warn tier is correct: it surfaces the problem for human review without stopping the load.
Note the block tier has exactly 3 items — the minimum number of invariants that downstream consumers truly cannot tolerate. This keeps the block tier credible. When a block-the-pipeline alert fires, engineers take it seriously because false positives are rare.
Module 1 complete
Module 1 is done. You have built an ETL pipeline that extracts GitHub events data, transforms it to Parquet, and validates it with a GE suite. From here, Module 2 wraps the L01 + L02 + L03 scripts in Airflow so the pipeline runs every hour without you — scheduled, retried, and backfilled automatically.