The previous lesson's loop left CineClick with a system that watches itself, retrains itself, and deploys with safeguards. This lesson answers the questions no dashboard answers: why did the system decide what it decided about a specific customer? Does it treat all segments equally well? What do we document, how long do we keep what, and what does regulation expect from us? The good news — and the lesson's thesis — is that governance is not a layer of bureaucracy bolted on at the end: the system we've built over six modules is already auditable by design, and here we're only going to make that explicit, fill in the gaps (model card, ADRs, subgroup analysis, a privacy policy for the prediction table), and place it on the regulatory map with honesty about its limits.
Contents
- Why governance is not bureaucracy
- End-to-end traceability: the system is already auditable
- Model cards: the model's fact sheet
- Datasheets for datasets, briefly
- Documenting decisions: lightweight ADRs
- Fairness and bias: subgroup metrics
- Privacy in CineClick's system
- The regulatory landscape, in honest broad strokes
- Governance checklist
Why governance is not bureaucracy
The churn model makes decisions about people: who gets offered a retention discount and who doesn't. That makes questions like "why was this customer offered retention while their neighbor, with an almost identical profile, wasn't?" legitimate — and sooner or later mandatory. The customer themselves may ask, or the legal team, an auditor, or a regulator. If the answer is "we'd have to check which model was deployed back then… we think the March one", the problem isn't paperwork: it's that you don't control your own system.
Governance is, simply, being able to answer three questions in minutes and with evidence:
- What happened? — what prediction was made, when, with what input.
- With what? — which exact model, trained on what data and what code.
- Why is it there? — who approved that model, with what evidence, following what process.
And the pleasant surprise: almost all of that already exists at CineClick, because good MLOps practices and audit requirements are largely the same thing seen from two different offices.
End-to-end traceability: the system is already auditable
Let's make the inventory explicit. Every audit question already has an answer with a concrete address:
| Audit question | Where the answer lives | Built in |
|---|---|---|
| What prediction did customer X receive on day D? | Prediction table: timestamp, features, probability, model_version, group |
06-01 |
| Which model was serving that day? | model_version on every log row + the registry's alias history |
04-02, 03-02 |
| What exact data was that model trained on? | dvc_data_md5 tag in the registry → dvc pull of that version from the storage remote |
06-03, 02-03 |
| With what code? | The MLflow run stores the git commit; the ghcr.io/cineclick/churn-api image is tagged per version |
03-01, 04-03 |
| What metrics did it have and how was it compared to the previous one? | MLflow run (experiment churn-cineclick) + the retraining flow's double evaluation |
03-01, 06-03 |
| Who approved putting it in production and with what evidence? | Promotion PR (reviewer, discussion, docs/model-promotion.md checklist) + shadow and canary results |
05-02, 05-04 |
| Did it behave well after deployment? | 06-01 dashboard, weekly real metrics, alert history in #ml-alerts |
06-01 |
| Why was it retrained on such-and-such date? | reason and flow_run of the churn-retraining flow + lineage tags |
06-03 |
Read it twice: we added nothing for auditing. Versioning data, tracking experiments, requiring promotion PRs, and logging every prediction with its version were engineering decisions; auditability came for free. That's the section's deep lesson: cheap governance is designed, not attached. The team working with loose notebooks and manual deployments cannot buy this table after the fact at any price.
What is missing are the narrative pieces: the documents that explain the system to someone who doesn't live inside it. That's where we're headed.
Model cards: the model's fact sheet
A model card is a model's technical fact sheet: a short, standardized document explaining what it does, what data it was built on, how it performs (globally and by subgroup), what it is NOT for, and what ethical considerations it raises. Google proposed it in 2019 and it has become the most widespread piece of model documentation. Its audience is not whoever trained the model: it's the new teammate, the legal team, the auditor.
The full model card of CineClick's champion, as it lives in docs/model-card-churn-v2.md:
# Model Card: churn-cineclick v2 (@champion) ## Model details - **Model**: RandomForestClassifier (n_estimators=300, max_depth=20, class_weight="balanced"). Registry: `churn-cineclick` v2, alias @champion. - **Training / promotion date**: 2026-05 / 2026-06 (PR #241). - **Owners**: CineClick ML team (Laura, DS; ML engineering). - **Lineage**: MLflow run in experiment `churn-cineclick`; data `dvc_data_md5: a3f9…`; code at commit `git: 7c21…`. ## Intended use - **Purpose**: estimate a subscriber's probability of cancelling in the next 30 days, to prioritize the weekly retention campaign. - **Users**: marketing team (weekly batch) and internal systems (API). - **Final decision**: marketing configures the retention offer over the prioritized list; the model does NOT execute actions on customers by itself. - **Out of scope**: individualized pricing decisions, denial of service, credit scoring, or any use with legal effects on the customer. Not validated for markets other than the current one. ## Training data - Dataset derived from the customer master and event tables, with a point-in-time cutoff (features prior to the cutoff; label = cancellation within 30 days). - Features: tenure_months, weekly_hours, support_tickets, plan, payment_method, active_discount, ratio_tickets. No direct personal data (see Privacy). - Churn prevalence ~15%. Evaluation labels taken from the permanent control group (unaffected by the campaign). ## Metrics - **Global (frozen test)**: accuracy 0.842, precision 0.55, recall 0.68, F1 0.61. Operating threshold 0.5 (env CHURN_THRESHOLD). - **Acceptance criterion (business)**: recall >= 0.60 and precision >= 0.50. - **By subgroup**: see table in docs/ (recall by plan: basic 0.71, standard 0.67, premium 0.58; see Limitations). - **In production**: weekly real recall/precision on the monitoring dashboard; offline reference 0.68/0.55. ## Limitations - Lower performance on the premium segment (recall 0.58) and on customers with tenure < 3 months (little history for the aggregate features). - Does not model seasonality or catalog events (releases, price increases): the associated concept drift requires retraining. - Probabilities not finely calibrated: useful for ranking and thresholding, not as an exact cancellation probability. ## Ethical considerations - Effect of the decision: receiving or not receiving a discount offer. Main risk: systematically unequal distribution of offers across segments (see subgroup analysis and quarterly review). - No protected attributes as inputs; indirect proxies not ruled out (e.g. plan as a socioeconomic correlate) — watched via subgroups. - Human in the loop for model promotion and campaign design.
Maintenance rules: the model card accompanies the version (v3 will have its own, generated as part of 05-02's promotion checklist), it's written in the repo (versioned with git, reviewable in the PR), and the limitations section is the most valuable one — a model card without honest limitations is marketing, not documentation.
Datasheets for datasets, briefly
The sibling idea, proposed by Gebru et al.: just as electronic components ship with a datasheet, every dataset should carry its own — motivation (why it exists), composition (what it contains, what's missing, what collection biases it has), collection process, recommended and discouraged uses, and maintenance policy. For CineClick, a one-page docs/datasheet-churn-customers.md documenting the training dataset: the origin of each column, the detail that the evaluation labels come from the control group, the temporal window covered, and the warning that the spring promo cohort is underrepresented before 2026-04. We won't develop it in full: the pattern is the same as the model card, applied to the data.
Documenting decisions: lightweight ADRs
The system's important decisions were made throughout the course: mandatory two-week shadow, 10–25% positive-rate band, monthly retraining, PSI threshold of 0.25, 30-day window. A year from now, someone will ask "why two weeks and not one?" — and the answer cannot live in the team's memory.
A lightweight ADR (Architecture Decision Record) is a half-page markdown file: context, decision, alternatives considered, consequences. The repo already has the embryo of the pattern (docs/model-promotion.md documents the promotion process); we generalize it with a docs/decisions/ directory:
# ADR-007: Two-week shadow window for every challenger - **Date**: 2026-06 · **Status**: accepted · **Deciders**: ML team + product ## Context After the v2 canary incident (05-04) it was agreed to require a shadow phase for every challenger. Its duration must be set. ## Decision 2 calendar weeks, no exceptions, including routine retraining models. ## Alternatives considered - 1 week: doesn't cover two full cycles of the weekly usage pattern (weekend + weekdays x2) and doesn't let enough volume accumulate for small segments (premium). - Variable depending on the change: introduces case-by-case judgment exactly where we want a non-negotiable rule. ## Consequences Every promotion takes >= 2 weeks from the challenger's registration. Accepted: churn doesn't demand reaction within days. Revisit if we ever need a model hotfix (exception procedure: deliberately doesn't exist; see ADR-008 if it gets created).
The convention: one ADR per decision, numbered, immutable (if the decision changes, another ADR is written that "supersedes ADR-007" — the decision history is as valuable as the decision in force). The cost is fifteen minutes per decision; the return, every time someone new asks "and why is this the way it is?".
Fairness and bias: subgroup metrics
Global metrics can hide that the model works systematically worse for a segment. At CineClick the effect is tangible: low recall in a subgroup means that its at-risk customers don't receive the retention offer — the system's benefit gets distributed unevenly. The analysis is straightforward with sklearn on the versioned test set:
# scripts/evaluate_subgroups.py
import pandas as pd
import mlflow
from sklearn.metrics import recall_score, precision_score
test = pd.read_csv("data/processed/test.csv")
model = mlflow.sklearn.load_model("models:/churn-cineclick@champion")
X = test.drop(columns=["churned"])
test["prediction"] = (model.predict_proba(X)[:, 1] >= 0.5).astype(int)
def subgroup_metrics(df: pd.DataFrame, column: str) -> pd.DataFrame:
rows = []
for value, g in df.groupby(column):
rows.append({
column: value,
"n": len(g),
"prevalence": g["churned"].mean(), # the group's real churn
"recall": recall_score(g["churned"], g["prediction"], zero_division=0),
"precision": precision_score(g["churned"], g["prediction"], zero_division=0),
"positive_rate": g["prediction"].mean(), # % flagged at risk
})
return pd.DataFrame(rows).round(3)
print(subgroup_metrics(test, "plan"))
# Tenure segments: the cuts are defined BEFORE looking at the results.
test["tenure_bracket"] = pd.cut(test["tenure_months"], bins=[0, 6, 24, 999],
labels=["0-6m", "6-24m", ">24m"])
print(subgroup_metrics(test, "tenure_bracket"))Illustrative output for plan:
| plan | n | prevalence | recall | precision | positive_rate |
|---|---|---|---|---|---|
| basic | 1240 | 0.19 | 0.71 | 0.56 | 0.24 |
| standard | 1410 | 0.14 | 0.67 | 0.55 | 0.17 |
| premium | 610 | 0.09 | 0.58 | 0.49 | 0.11 |
How to read it without fooling yourself:
- Compare
recallacross groups, but always look atnandprevalence: premium has fewer customers and less churn — its recall is noisier and its task is objectively harder (fewer positive examples to learn from). positive_ratemeasures the distribution of the "benefit" (the offer): if a group with similar prevalence systematically receives fewer offers, there's a fairness problem even if the recall adds up.
And if a subgroup consistently comes out worse? The menu of responses, from least to most intervention: (1) document it in the model card (done: premium 0.58) and watch it as a dashboard panel; (2) more data or better features for that segment — premium's low recall is often lack of signal, not algorithmic bias; (3) per-segment thresholds or reweighting in training — with an ADR, because it introduces explicit differentiated treatment that must be justifiable; (4) if the subgroup is defined by a protected characteristic or its proxy, escalate to legal/compliance before touching anything. And a cross-cutting rule: this analysis runs on every candidate evaluation (it gets added to 06-03's flow), not once a year.
Privacy in CineClick's system
Let's re-examine the system through data-protection glasses, because several decisions we made for engineering reasons turn out to also be privacy decisions:
- Minimization: the model uses no direct personal data — no name, no email, no address, no payment data; only
customer_id(an internal identifier) and behavioral features. The best way not to leak a piece of data is never to have given it to the system. - The log's
input_hash(04-02): the prediction table stores no raw payloads; the hash allows detecting duplicates and verifying integrity without storing the literal input. Auditing without hoarding. - Retention: the prediction table can't grow forever. CineClick defines an explicit policy — full detail (with features) for 13 months (covering one annual seasonality cycle + audit margin); afterwards, anonymous aggregates for the dashboard's historical series. Legal sets the exact figure; what matters is that it exists and is automated (a purge job, not an intention).
- Right to erasure and versioned data — the delicate point. If a customer exercises their right to erasure, what happens to the immutable DVC datasets where they appear? Deleting a row changes the hash and breaks the reproducibility of every model trained on that dataset. The strategies, in order of preference:
- Don't version PII: if the dataset only contains the internal
customer_idand behavior, erasure is resolved in the master table mapping identity↔id (which is NOT in DVC). That's CineClick's strategy, and another win for minimization. - Pseudonymization before versioning: the data enters DVC already pseudonymized; the pseudonym key lives outside, and deleting it unlinks the person from their rows.
- Purge the raw + re-derive: if a true deletion were required, the data is removed from the raw, the dataset is regenerated (new hash), and an ADR documents that the old versions are no longer reproducible and why. Reproducibility broken with justified cause and a trail, rather than silently.
- Don't version PII: if the dataset only contains the internal
The regulatory landscape, in honest broad strokes
Two European norms frame a system like CineClick's. Broad strokes, not a treatise:
GDPR, Article 22: people have the right not to be subject to decisions based solely on automated processing that produce legal effects or similarly significantly affect them. Does offering (or not offering) a retention discount cross that bar? It's debatable and case-dependent — but notice that CineClick's design already incorporates the natural safeguard: there are humans in the loop both in model promotion and in campaign design (marketing decides the offer over the prioritized list; the model doesn't execute actions on customers by itself, as the model card declares). The architecture we chose out of operational prudence is also the one that reduces the Article 22 risk.
The EU AI Act: it classifies systems by risk (unacceptable / high / limited / minimal) with proportional obligations. A churn model for retention campaigns points to limited or minimal risk — it's not on the high-risk lists (credit, employment, essential services…). But the valuable observation is another one: the requirements the regulation imposes on high-risk systems — risk management, data governance, technical documentation, event logging, human oversight, monitored accuracy and robustness — are exactly the content of this module. The traceability table, the model card, the prediction table, the human in the loop, and continuous monitoring are that list. Building with good MLOps practices is, to a large extent, building regulation-ready.
Important warning: the above is technical engineering guidance, not legal advice. The regulatory classification of a specific system, the applicability of Article 22, retention periods, and the exact obligations depend on each organization's context and must be determined by its legal and compliance professionals. The ML engineer's job is to build a system capable of complying — traceable, documented, with human oversight — and to arrive at that conversation with the technical answers ready; the legal qualification is not theirs to make.
Governance checklist
The lesson, condensed into a table you can carry to any project:
| # | Item | At CineClick | Status |
|---|---|---|---|
| 1 | Every prediction logged with model version, features, and timestamp | Prediction table (06-01) | Done |
| 2 | Every model version traceable to data (hash), code (commit), and run | Lineage tags in the registry (06-03) | Done |
| 3 | Promotions with documented human approval and evidence | Promotion PR + shadow/canary (05-02, 05-04) | Done |
| 4 | Model card per champion version, with honest limitations | docs/model-card-churn-v2.md |
Done |
| 5 | Datasheet for the training dataset | docs/datasheet-churn-customers.md |
Done |
| 6 | Design decisions recorded as ADRs | docs/decisions/ |
Done |
| 7 | Subgroup evaluation on every candidate, with an escalation threshold | evaluate_subgroups.py in the flow (06-03) |
Done |
| 8 | Minimization: no PII in features or logs (hash, internal ids) | 04-02 design + PII-free datasets | Done |
| 9 | Prediction retention policy, automated | Purge job: 13 months → aggregates | Done |
| 10 | Erasure procedure compatible with versioned data | "No PII in DVC" strategy + exceptions ADR | Done |
| 11 | Regulatory classification reviewed with legal/compliance | Annual meeting + whenever the model's use changes | Pending scheduling |
| 12 | Periodic review of this checklist | Quarterly, alongside the subgroup analysis | Done |
Common Mistakes and Tips
- Treating governance as a final sprint before the audit. The traceability in section 2's table cannot be reconstructed after the fact: either the system generates it as it operates, or it doesn't exist. Design it from module 2, as we did (even if back then we didn't call it governance).
- Model cards written once and never updated. A v1 model card describing v3 is worse than none: it documents something false with authority. Tie it to the promotion process: no updated model card, no PR approval.
- Fairness analysis only with the favorite subgroup's global metrics. Define the subgroups and the cuts before looking at the results, and repeat it on every candidate — a model can be fair in v2 and stop being fair in v3.
- Confusing "I don't use protected attributes" with "there is no bias". Proxies exist (plan and payment method correlate with socioeconomic level). That's why the outcome is measured by subgroup instead of trusting the variable's absence.
- Keeping everything forever "just in case". Indefinite retention is a privacy and cost liability. Explicit policy, justified period, automated purge.
- Writing ADRs only when the decision is controversial. Today's "obvious" decisions are the mysteries of two years from now. If it took more than a ten-minute discussion, it deserves an ADR.
- Playing lawyer. Prepare impeccable technical evidence and take the regulatory questions to legal/compliance. Engineering answers to legal questions age badly.
Exercises
-
Audit drill. An auditor asks: "On April 14, 2026, customer 88213 was included in the retention campaign. Justify that decision: which model made it, with what input, what evidence there was that the model was adequate, and who approved it". Write the answer step by step, citing, for each element, the exact system component where the evidence lives (use section 2's table).
-
The subgroup that gets worse. In the evaluation of v3 (the retraining's challenger), recall for the
0-6mtenure bracket falls from 0.61 to 0.44, while the global figure rises from 0.68 to 0.71. With what you've learned in this module: (a) give a plausible hypothesis of the cause related to the promo scenario; (b) say what you'd do about v3's promotion; (c) indicate which documents would need touching whatever the decision. -
Erasure with DVC. In a new project, CineClick decides to version a dataset with DVC that includes customers' email addresses "because it simplifies the joins". Write the objection paragraph you'd post on the PR, mentioning: the conflict with the right to erasure, the two preferable strategies, and the cost of fixing it later.
Solutions
-
(i) What prediction: customer 88213's row in the prediction table with timestamp 2026-04-13/14, its input features, probability ≥ 0.5, and a
groupother than control — origin: the weekly batchweekly-churn-scoringof Monday the 13th. (ii) Which model: that row'smodel_versionfield (v2); cross-checked against the registry's alias history, which confirms v2 = @champion on that date. (iii) The model's adequacy: v2's MLflow run (recall 0.68 / precision 0.55 on the frozen test, above the 0.60/0.50 business criterion), lineage tags pointing to the exact DVC dataset, the v2 model card with subgroup metrics, and the results of its two weeks of shadow and canary prior to promotion. (iv) Who approved: v2's promotion PR (identified reviewer,docs/model-promotion.mdchecklist completed) and the associatedpromote_champion.pyexecution. (v) The final decision to include them in the campaign: the prioritized list was handed to marketing, which configured the offer — human in the loop, documented in the model card. Estimated response time with the current system: minutes. -
(a) Hypothesis: v3 was trained on the window that includes the promo cohort — many new customers (0-6m) with still-immature labels or atypical behavior (their 30-day horizon hasn't elapsed yet, or their initial usage pattern doesn't represent their stable behavior), so the model learned noisy or outright wrong signal for that bracket; the global figure rises because the large brackets improve. (b) Don't promote yet: the global criterion is met, but a 17-point recall drop in a segment means de facto withdrawing retention from new customers — a business decision that can't be made by omission. Options: delay v3 until the cohort's labels mature, reweight/adjust the cohort's inclusion cutoff in the dataset, or promote with a per-segment threshold — any of them with an ADR and in agreement with product. (c) Documents: the subgroup evaluation attached to the promotion PR (which in this case blocks it), an ADR with the decision taken and its alternatives, and — if some variant does get promoted — v3's model card with the 0-6m bracket limitation described honestly.
-
A model paragraph: "Objection to versioning email in DVC: DVC datasets are immutable by design (the hash is the data's identity), so a future erasure request would force us to choose between missing the deadline or purging the raw and re-deriving, breaking the reproducibility of every model trained on that dataset and forcing an exception ADR. I propose: (1) doing the joins with the internal
customer_idand keeping the identity↔id mapping outside DVC, where erasure is aDELETEwith no effect on the versioning; or (2) if the email carries signal (domain, age), deriving those features and versioning only the pseudonymized derivative. Fixing it later will cost purging and re-deriving N historical versions, invalidating the comparability of the models trained on them, and documenting the exception — all avoidable today with a one-line change in the join."
Conclusion
Governance has turned out to be, above all, a shift of perspective: the system built over six modules already answered the audit questions — prediction table, lineage in the registry, promotion PRs — and this lesson has made that explicit and added the narrative and accountability layer: v2's model card with its honest limitations, the dataset's datasheet, the ADRs in docs/decisions/, subgroup evaluation inside the candidate flow, the prediction table's privacy policy (minimization, hashing, retention, DVC-compatible erasure), and the regulatory map with its warning: engineering prepares the evidence; the legal qualification belongs to legal and compliance. The twelve-point checklist condenses the module into something portable. And with it, construction closes: there is no piece left to add to CineClick's platform. The course's final lesson builds nothing — it integrates: the complete map of the system, an end-to-end incident that exercises everything, and the project through which you'll make everything you've learned your own, in another domain.
MLOps Course
Module 1: MLOps Fundamentals
- What MLOps is and why models die in the notebook
- The lifecycle of an ML model in production
- MLOps maturity levels and team roles
- The course project: from notebook to production
Module 2: From Notebook to Reproducible Code
- Structuring an ML project: from notebook to package
- Reproducible environments and dependency management
- Data versioning with DVC
- Reproducible training pipelines
Module 3: Experiments and Model Registry
- Experiment tracking with MLflow
- Model registry: versioning and promoting models
- Feature stores: when and what for
Module 4: Serving Models in Production
- Deployment patterns: batch, online and streaming
- A prediction service with FastAPI
- Packaging with Docker
- Scaling and deployment: Kubernetes and serverless
- Inference optimization: latency and cost
Module 5: Automation: CI/CD and Orchestration
- CI for ML: testing code, data and models
- CD: automating model deployment
- Orchestrating ML pipelines
- Release strategies: shadow, canary and A/B
