Beginner Intermediate 26 min read

Chapter 28: The Data Science Lifecycle

Data science projects fail far more often from poor problem framing than from inadequate algorithms. A team can spend weeks building a sophisticated gradient-boosted model only to discover that the original business question was never precisely defined, the success metric was chosen arbitrarily, or the data collected does not actually reflect the phenomenon being studied. The Data Science Lifecycle is the set of disciplined, iterative practices that prevent these failures — it turns an ambiguous organizational need into a well-scoped, measurable, and ethically grounded data problem.

This chapter treats the lifecycle as a first-class engineering concern. We examine the most widely adopted framework — CRISP-DM — alongside its modern variants, and we develop practical tools for translating vague business questions into operationalized hypotheses with quantifiable success criteria. We also distinguish carefully between the overlapping roles of data analyst, data scientist, and machine learning engineer, because role confusion inside a team is one of the most common sources of scope creep and missed deliverables.

Ethics is woven throughout rather than treated as an afterthought. Decisions made in the earliest phases — which data to collect, whose outcomes to optimize, what baseline to compare against — carry consequences that no post-hoc fairness audit can fully undo. By the end of this chapter you will have a repeatable process for scoping a project from first conversation to signed-off project charter, grounded in the practical realities of stakeholder communication, resource constraints, and organizational politics.

Runnable companion notebook for this chapter. Download the Jupyter notebook (.ipynb)

Learning Objectives

  • Describe the phases of CRISP-DM and explain how the lifecycle is iterative rather than linear.
  • Translate an ambiguous business question into a precise, measurable data problem with defined success metrics.
  • Distinguish the responsibilities of data analysts, data scientists, and ML engineers and identify which role owns each lifecycle phase.
  • Apply structured stakeholder communication techniques — including the problem statement template and the pre-mortem — to scope a project before writing any code.
  • Recognize common ethical risks (proxy discrimination, feedback loops, survivorship bias) and embed mitigation checkpoints into project planning.
  • Construct a project charter that captures objectives, constraints, success criteria, and ethical considerations in a single auditable document.
  • Evaluate whether a given business problem is better addressed by descriptive analytics, predictive modeling, or causal inference.

28.1 CRISP-DM and the Anatomy of a Data Science Project Beginner

What Is the Data Science Lifecycle?

Every data science project — whether it ends in a one-page report or a production ML system serving millions of requests — passes through a recognizable sequence of activities: understanding the problem, acquiring and preparing data, exploring and modeling that data, and communicating results. The Cross-Industry Standard Process for Data Mining (CRISP-DM), first published in 1999 by a consortium of industrial practitioners, codifies this sequence into six named phases.

CRISP-DM is deliberately domain-agnostic. The same framework applies to a hospital predicting readmissions, a retailer forecasting inventory, and a streaming service recommending content. Its longevity — over two decades — is testament to how well it captures the universal shape of applied data work.

The Six Phases

1. Business Understanding

The project begins not with data but with questions. What decision will this analysis inform? Who makes that decision? What does success look like numerically? These questions must be answered before a single dataset is opened.

Key outputs of this phase: a written problem statement, a list of stakeholders and their objectives, preliminary success criteria, and a rough assessment of feasibility.

2. Data Understanding

Once the problem is framed, the team inventories available data sources and performs initial exploration. The goal is not to model anything yet — it is to discover what you actually have versus what you assumed you had. Common findings at this stage include missing historical periods, population shift between training and production environments, and proxies for protected attributes hiding in innocuous-sounding columns.

3. Data Preparation

This phase consistently consumes 60–80 % of calendar time on real projects. It encompasses cleaning, joining, filtering, transforming, and encoding data into a form suitable for analysis. The output is an analytic base table (ABT) — a single, flat, labeled dataset aligned to the unit of analysis (e.g., one row per customer-week).

4. Modeling

With a clean ABT in hand, the team selects candidate algorithms, engineers features, and evaluates models against held-out data. This phase is the most visible part of data science in popular culture yet typically represents a minority of total project effort.

5. Evaluation

Technical model performance is necessary but not sufficient. The evaluation phase asks: does the model actually address the original business objective? A classifier with 94 % accuracy on a 95 %-negative dataset may be entirely useless. The evaluation phase forces alignment between model metrics and business metrics.

6. Deployment

Results must reach decision-makers. Deployment ranges from a static PDF report to a real-time REST API. Monitoring, retraining schedules, and rollback procedures are all deployment concerns, even for low-stakes analytical projects.

The Cycle Is Iterative, Not Waterfall

CRISP-DM is frequently drawn as a circle, not a flowchart, for a reason. New information discovered during modeling (e.g., a key variable is defined inconsistently across data sources) routinely sends the team back to Business Understanding to renegotiate the problem scope. This is expected and healthy. Treating CRISP-DM as a strict waterfall — where phases are signed off and closed — is one of the most common ways mature-sounding projects still fail.

A practical habit: after every phase, write a one-paragraph "phase retrospective" noting what you learned that surprised you and what you would change about earlier phases if you were starting over. This single practice surfaces misalignment early and creates an audit trail for stakeholders.

Modern Variants

CRISP-DM is not the only lifecycle framework in use:

  • TDSP (Team Data Science Process), developed at Microsoft, adds explicit roles, sprint-based cadences, and standardized artifact templates suited to enterprise teams.
  • OSEMN (Obtain, Scrub, Explore, Model, iNterpret), popular in academic courses, is a lighter mnemonic useful for solo exploratory projects.
  • MLOps lifecycles extend CRISP-DM with continuous training, feature stores, model registries, and A/B testing infrastructure suited to production ML systems.

For most practitioners, CRISP-DM is the right conceptual anchor and MLOps practices fill in the deployment phase as the project matures.

PYTHON
# A simple project-phase tracker — lightweight but surprisingly useful
import datetime
from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class PhaseLog:
    phase: str
    start_date: datetime.date
    end_date: Optional[datetime.date] = None
    surprises: List[str] = field(default_factory=list)
    back_to: Optional[str] = None  # which earlier phase did this send you to?

crisp_log = [
    PhaseLog(
        phase="Business Understanding",
        start_date=datetime.date(2026, 1, 6),
        end_date=datetime.date(2026, 1, 10),
        surprises=["Stakeholders disagree on KPI definition"],
    ),
    PhaseLog(
        phase="Data Understanding",
        start_date=datetime.date(2026, 1, 11),
        surprises=["CRM data only goes back 18 months, not 3 years"],
        back_to="Business Understanding",
    ),
]

for entry in crisp_log:
    status = "ongoing" if entry.end_date is None else f"ended {entry.end_date}"
    print(f"[{entry.phase}] {status}")
    for s in entry.surprises:
        print(f"  SURPRISE: {s}")
    if entry.back_to:
        print(f"  --> Revisiting: {entry.back_to}")

Code Examples

Visualizing CRISP-DM Phase Time Distribution

Reconstruct a typical project's time allocation across CRISP-DM phases to build intuition about where effort actually goes.

PYTHON
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

phases = [
    "Business\nUnderstanding",
    "Data\nUnderstanding",
    "Data\nPreparation",
    "Modeling",
    "Evaluation",
    "Deployment",
]
# Approximate industry-reported percentages (Kaggle surveys, practitioner blogs)
pct = [5, 15, 60, 10, 5, 5]
colors = ["#4C72B0", "#55A868", "#C44E52", "#8172B2", "#CCB974", "#64B5CD"]

fig, ax = plt.subplots(figsize=(9, 5))
bars = ax.barh(phases, pct, color=colors, edgecolor="white", height=0.6)
ax.set_xlabel("Typical % of project calendar time", fontsize=12)
ax.set_title("CRISP-DM Phase Time Allocation (industry average)", fontsize=13, fontweight="bold")
for bar, p in zip(bars, pct):
    ax.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height() / 2,
            f"{p}%", va="center", fontsize=11)
ax.set_xlim(0, 72)
ax.invert_yaxis()
plt.tight_layout()
plt.savefig("crisp_dm_time.png", dpi=150)
plt.show()
print("Figure saved to crisp_dm_time.png")
Output
Figure saved to crisp_dm_time.png

28.2 From Business Question to Measurable Problem Beginner

The Translation Problem

The most dangerous sentence in data science is: "We want to use AI/ML to improve our business." It sounds like a mandate but it is not a problem statement. Without a precise, measurable formulation, a team cannot choose the right analytical approach, cannot determine when they are done, and cannot tell stakeholders whether the project succeeded.

The translation from business language to data-science language requires extracting three things:

  1. The unit of analysis — the entity about which a prediction or decision is being made (a customer, a transaction, a sensor reading, a hospital visit).
  2. The target variable — the specific, operationally defined outcome to predict or understand.
  3. The success metric — the number that must improve, and by how much, for the project to be considered successful.
  4. A Structured Problem Statement Template

A useful template forces precision:

"For each [unit of analysis], we want to [predict / describe / segment / detect] [target variable] using [available data], so that [decision-maker] can [take action], which we expect to improve [business KPI] by [quantified goal]."

Applying this to a vague request:

  • Vague: "We want to reduce customer churn."
  • Precise: "For each active subscriber at the start of each calendar month, we want to predict whether they will cancel their subscription within the next 30 days, using usage logs and support ticket history, so that the retention team can prioritize outreach calls, which we expect to reduce monthly churn rate from 3.2 % to 2.8 % (a 12.5 % relative reduction)."

The precise version immediately surfaces several questions that must be answered before modeling begins: How is "active" defined? What counts as a cancellation versus a pause? Is the retention team capacity-constrained (i.e., do we need a ranked list, not just a binary label)?

Distinguishing Analytical Tasks

Not every business question requires a predictive model. A critical early decision is choosing the right analytical mode:

  • Descriptive analytics answers "what happened?" — dashboards, aggregations, cohort analysis. No model required.
  • Diagnostic analytics answers "why did it happen?" — funnel analysis, attribution, exploratory drill-down.
  • Predictive analytics answers "what will happen?" — regression, classification, time-series forecasting.
  • Prescriptive analytics answers "what should we do?" — optimization, causal inference, reinforcement learning.

A common mistake is to jump to predictive modeling when the actual need is descriptive. If the retention team wants to know which regions have the highest churn, a simple groupby and bar chart is the right tool. Deploying a churn model to answer that question adds cost and latency without adding value.

Connecting the Target Variable to Ground Truth

The target variable must be operationally observable in the data. Consider a healthcare example: "We want to predict which patients are at risk." At risk of what, exactly, and how will that outcome be recorded? A patient discharged with "unplanned readmission" as the target might be mislabeled if the hospital uses different readmission definitions across facilities.

This is the label quality problem, and it is worth a dedicated discussion during Business Understanding. Poor labels are statistically indistinguishable from high variance at small sample sizes — they only become visible as the dataset grows, often after months of wasted modeling effort.

PYTHON
import pandas as pd

# Simulating the label quality problem:
# two coders labeling the same customer-cancellation events
import numpy as np
rng = np.random.default_rng(42)

n = 500
df = pd.DataFrame({
    "customer_id": range(n),
    "coder_A": rng.integers(0, 2, n),  # 0=retained, 1=churned
})
# Coder B agrees 85% of the time (15% label noise)
agreement_mask = rng.random(n) < 0.85
df["coder_B"] = np.where(agreement_mask, df["coder_A"], 1 - df["coder_A"])

kappa_num = (df["coder_A"] == df["coder_B"]).mean()
pe = (
    (df["coder_A"] == 1).mean() * (df["coder_B"] == 1).mean()
    + (df["coder_A"] == 0).mean() * (df["coder_B"] == 0).mean()
)
kappa = (kappa_num - pe) / (1 - pe)

print(f"Raw agreement:   {kappa_num:.3f}")
print(f"Cohen's kappa:   {kappa:.3f}  (> 0.8 = strong agreement)")
print(f"Disagreements:   {(df['coder_A'] != df['coder_B']).sum()} / {n}")

Quantifying the Success Metric

Success metrics must be tied to the decision, not just the model. The classic mistake is reporting AUC-ROC as the project outcome to a business audience that cares about dollar revenue. The mapping from model metric to business metric must be made explicit:

$$\text{Expected Benefit} = (\text{Lift over baseline}) \times (\text{Action conversion rate}) \times (\text{Value per converted action})$$

For the churn example, if the model identifies 1,000 at-risk customers per month that the baseline rule-set misses, and the retention team converts 15 % of those with a 30-dollar retention credit that saves a 200-dollar annual subscription, the expected monthly benefit is:

$$1000 \times 0.15 \times (200 - 30) = \$25{,}500 / \text{month}$$

This number gives the project a budget: spending more than $25,500/month on data infrastructure, engineering, or analyst time to achieve this improvement is uneconomical.

Code Examples

Problem Statement Validator

A small utility that checks whether a problem statement contains all required components and flags missing elements.

PYTHON
import re
from dataclasses import dataclass
from typing import Dict, List

@dataclass
class ProblemStatement:
    unit_of_analysis: str
    task: str           # predict / describe / segment / detect
    target_variable: str
    available_data: List[str]
    decision_maker: str
    action: str
    business_kpi: str
    quantified_goal: str

    def validate(self) -> Dict[str, bool]:
        checks = {
            "unit_of_analysis is specific": len(self.unit_of_analysis.split()) >= 2,
            "task is one of the four modes": self.task in
                {"predict", "describe", "segment", "detect", "explain"},
            "target is observable": len(self.target_variable) > 0,
            "at least one data source named": len(self.available_data) >= 1,
            "decision_maker identified": len(self.decision_maker) > 0,
            "action is concrete": len(self.action.split()) >= 3,
            "KPI contains a number": bool(re.search(r"\d", self.quantified_goal)),
        }
        return checks

    def report(self):
        checks = self.validate()
        print("Problem Statement Quality Check")
        print("=" * 40)
        for criterion, passed in checks.items():
            icon = "PASS" if passed else "FAIL"
            print(f"  [{icon}] {criterion}")
        score = sum(checks.values()) / len(checks)
        print(f"\nScore: {score:.0%} ({sum(checks.values())}/{len(checks)} checks passed)")

ps = ProblemStatement(
    unit_of_analysis="active subscriber at month start",
    task="predict",
    target_variable="cancellation within next 30 days",
    available_data=["usage_logs", "support_tickets", "billing_history"],
    decision_maker="retention team manager",
    action="prioritize outreach calls to top 200 at-risk customers",
    business_kpi="monthly churn rate",
    quantified_goal="reduce monthly churn from 3.2% to 2.8%",
)
ps.report()
Output
Problem Statement Quality Check
========================================
  [PASS] unit_of_analysis is specific
  [PASS] task is one of the four modes
  [PASS] target is observable
  [PASS] at least one data source named
  [PASS] decision_maker identified
  [PASS] action is concrete
  [PASS] KPI contains a number

Score: 100% (7/7 checks passed)

Business Value Estimation Model

Compute expected monthly business value of a churn model under varying lift and conversion assumptions.

PYTHON
import numpy as np
import matplotlib.pyplot as plt

# Parameters
at_risk_identified = 1000          # extra customers caught vs. baseline
conversion_rates = np.linspace(0.05, 0.30, 50)
retention_credit = 30              # cost of the retention incentive ($)
annual_subscription_value = 200    # LTV of a retained subscription ($)

benefit_per_customer = annual_subscription_value - retention_credit  # $170

monthly_benefits = at_risk_identified * conversion_rates * benefit_per_customer

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(conversion_rates * 100, monthly_benefits, linewidth=2, color="#2E86AB")
ax.axhline(25500, color="#E84855", linestyle="--", label="Break-even @ 15% conversion")
ax.fill_between(conversion_rates * 100, monthly_benefits, alpha=0.15, color="#2E86AB")
ax.set_xlabel("Retention Conversion Rate (%)", fontsize=11)
ax.set_ylabel("Expected Monthly Benefit ($)", fontsize=11)
ax.set_title("Churn Model Business Value by Conversion Rate", fontsize=12, fontweight="bold")
ax.legend()
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"${x:,.0f}"))
plt.tight_layout()
plt.savefig("churn_value.png", dpi=150)
plt.show()
print("Saved churn_value.png")
Output
Saved churn_value.png

28.3 Roles, Responsibilities, and Team Structure Beginner

The Role Confusion Problem

Organizations routinely hire a single person under a title like "Data Scientist" and expect them to simultaneously own a Tableau dashboard, build a production recommendation engine, maintain a Spark pipeline, and brief the C-suite. This is not an unreasonable expectation for a startup with two engineers; it is an unreasonable expectation for a 500-person organization that could staff a cross-functional team. Understanding where the boundaries of roles lie — and where they legitimately blur — is a project management skill as important as any technical skill.

Core Data Roles

Data Analyst

The data analyst's primary output is insight for decision-making. Their work is predominantly retrospective: what happened, why did it happen, how does it compare to benchmarks? Core tools include SQL, spreadsheets, and business intelligence platforms (Tableau, Looker, Power BI). Analysts produce dashboards, reports, and ad-hoc analyses.

A skilled data analyst is the most direct lever on business outcomes in organizations that have more data than they have interpretive capacity. Their work does not require a deployed model — it requires a clear question, clean data, and good visualization.

Data Scientist

The data scientist's primary output is models and statistical analyses that answer questions the data alone cannot answer by inspection. Their work spans the full CRISP-DM lifecycle and is predominantly forward-looking: what will happen, what should we do? Core tools include Python/R, scikit-learn, statistical modeling libraries, and Jupyter notebooks. They interface directly with stakeholders to frame problems and communicate results.

A useful heuristic: if the output is a number with a confidence interval, or a ranked list with an expected lift, the data scientist owns it.

Machine Learning Engineer

The ML engineer's primary output is production ML systems — the infrastructure that takes a data scientist's model and serves predictions reliably at scale. Core tools include REST APIs, container orchestration (Docker, Kubernetes), feature stores, model monitoring platforms, and CI/CD pipelines. They work primarily with software engineering concerns: latency, throughput, reliability, and reproducibility.

A useful heuristic: if the output is a deployed endpoint with an SLA, the ML engineer owns it.

Data Engineer

The data engineer's primary output is reliable, well-modeled data — pipelines that ingest, transform, and serve data to analysts and scientists. Core tools include orchestration frameworks (Airflow, Prefect), cloud data warehouses (BigQuery, Snowflake), and streaming platforms (Kafka, Flink). Without data engineers, data scientists spend their CRISP-DM "Data Preparation" phase rebuilding the same Pandas munging script every sprint.

How Roles Map to CRISP-DM Phases

Phase ownership varies by organization but the following assignment is common in mid-sized teams:

  • Business Understanding — Data Scientist (led), Data Analyst (contributing), product/domain stakeholders.
  • Data Understanding — Data Scientist and Data Analyst jointly; Data Engineer consulted on availability.
  • Data Preparation — Data Engineer (pipelines), Data Scientist (feature engineering and ABT construction).
  • Modeling — Data Scientist (owns), ML Engineer (consulted on productionization constraints).
  • Evaluation — Data Scientist and Analyst jointly; stakeholder sign-off required.
  • Deployment — ML Engineer (owns infrastructure), Data Engineer (owns data feeds), Data Scientist (owns model retraining policy).
  • Cross-Functional Communication Norms

The most reliable way to align a cross-functional team is to make assumptions explicit and shared. A few concrete practices:

  • Weekly one-pager: at the start of each week, the data scientist circulates a half-page status that includes (1) what was learned, (2) what the next milestone is, and (3) what is blocking progress. This takes 10 minutes to write and prevents the most common failure mode: a team working for a month before a stakeholder learns the project has been blocked since week two.
  • Metric review cadence: agree up front how often model metrics will be reviewed with stakeholders, and which metrics stakeholders care about directly versus which are internal engineering concerns.
  • Escalation path: define who makes the call when a modeling decision requires a business tradeoff (e.g., precision vs. recall in a fraud context). This decision belongs to the business owner, not the data scientist.

PYTHON
# Simple RACI matrix for a data science project, encoded as a dictionary
# R=Responsible, A=Accountable, C=Consulted, I=Informed

raci = {
    "Business Understanding":  {"DS": "R", "DA": "C", "DE": "I", "MLE": "I",  "Product": "A"},
    "Data Understanding":      {"DS": "R", "DA": "R", "DE": "C", "MLE": "I",  "Product": "I"},
    "Data Preparation":        {"DS": "C", "DA": "I", "DE": "R", "MLE": "I",  "Product": "I"},
    "Modeling":                {"DS": "R", "DA": "I", "DE": "I", "MLE": "C",  "Product": "A"},
    "Evaluation":              {"DS": "R", "DA": "C", "DE": "I", "MLE": "I",  "Product": "A"},
    "Deployment":              {"DS": "C", "DA": "I", "DE": "R", "MLE": "R",  "Product": "A"},
}

roles = ["DS", "DA", "DE", "MLE", "Product"]
header = f"{'Phase':<25}" + "".join(f"{r:<10}" for r in roles)
print(header)
print("-" * len(header))
for phase, assignments in raci.items():
    row = f"{phase:<25}" + "".join(f"{assignments[r]:<10}" for r in roles)
    print(row)

Code Examples

Skill Gap Assessment for a DS Project

Enumerate the skills required for a project and flag which are unmet, prompting a staffing conversation before the project starts.

PYTHON
from dataclasses import dataclass, field
from typing import List

@dataclass
class SkillRequirement:
    skill: str
    phase: str
    required_level: str   # "basic" | "proficient" | "expert"
    covered_by: List[str] = field(default_factory=list)

requirements = [
    SkillRequirement("SQL", "Data Understanding", "proficient", ["Alice (DA)"]),
    SkillRequirement("Python/Pandas", "Data Preparation", "proficient", ["Bob (DS)"]),
    SkillRequirement("Statistical testing", "Evaluation", "expert", []),
    SkillRequirement("Airflow pipelines", "Data Preparation", "proficient", []),
    SkillRequirement("scikit-learn", "Modeling", "proficient", ["Bob (DS)"]),
    SkillRequirement("Docker / API deployment", "Deployment", "proficient", []),
    SkillRequirement("Stakeholder presentation", "Evaluation", "basic", ["Alice (DA)", "Bob (DS)"]),
]

print("Skill Gap Report")
print("=" * 50)
gaps = []
for req in requirements:
    covered = ", ".join(req.covered_by) if req.covered_by else "UNCOVERED"
    flag = "  [GAP]" if not req.covered_by else ""
    print(f"  {req.skill:<35} [{req.required_level}]  {covered}{flag}")
    if not req.covered_by:
        gaps.append(req.skill)
print()
if gaps:
    print(f"Action required: hire or contract for: {', '.join(gaps)}")
else:
    print("All skills covered.")
Output
Skill Gap Report
==================================================
  SQL                                 [proficient]  Alice (DA)
  Python/Pandas                       [proficient]  Bob (DS)
  Statistical testing                 [expert]  UNCOVERED  [GAP]
  Airflow pipelines                   [proficient]  UNCOVERED  [GAP]
  scikit-learn                        [proficient]  Bob (DS)
  Docker / API deployment             [proficient]  UNCOVERED  [GAP]
  Stakeholder presentation            [basic]  Alice (DA), Bob (DS)

Action required: hire or contract for: Statistical testing, Airflow pipelines, Docker / API deployment

28.4 Ethics Up Front: Embedding Responsible Practices into the Lifecycle Intermediate

Why Ethics Cannot Be an Afterthought

It is tempting to treat data ethics as a compliance step that happens after a model is built — a fairness audit tacked onto the end of the project before deployment. This approach is structurally flawed. The decisions with the largest ethical consequences are made in the earliest phases of the lifecycle: which population to collect data from, which outcome to optimize, which historical labels to trust. By the time a model exists, many of these choices are baked in and expensive or impossible to reverse.

This section catalogs the most important ethical risk categories, explains how they arise in practice, and provides concrete checkpoints to embed into the CRISP-DM workflow.

Key Risk Categories

Proxy Discrimination

A protected attribute (race, gender, age, disability status) may be absent from the training data yet still be predictable from seemingly neutral features. Zip code is the canonical example: it is a strong proxy for race in jurisdictions with a history of residential segregation. A model trained to predict credit default using zip code may produce discriminatory outcomes even without ever seeing the applicant's race.

The mitigation is not simply to remove protected attributes — it is to audit for correlation between features and protected attributes and to evaluate model outcomes by protected group, even when the protected attribute is not a model input.

Feedback Loops

Many datasets used in ML are themselves the product of prior decisions made by (often biased) humans or systems. Predictive policing is the prototypical example: if police patrol is concentrated in certain neighborhoods, more arrests occur there, the training data over-represents crime in those neighborhoods, the model recommends more patrol there, and the cycle reinforces itself.

Feedback loops are insidious because the model can achieve high predictive accuracy while systematically amplifying existing disparities. The key diagnostic question is: is the label a reflection of the world, or a reflection of prior decisions about the world?

Survivorship and Selection Bias

If data is only collected from entities that "survived" some filter, the model trained on that data will not generalize to the full population. A model trained on loan repayment data from approved applicants cannot reliably predict risk for applicants that would historically have been denied — the most relevant population is the one you have the least data about.

Data collected for one purpose should not be used freely for another. A patient's medical records, collected for treatment purposes, have different consent implications when used to train a readmission model that flags patients to case managers. GDPR, HIPAA, and sector-specific regulations impose legal constraints, but the ethical bar is often higher than the legal minimum.

Embedding Ethics Checkpoints into CRISP-DM

Each phase has natural ethics checkpoints:

  • Business Understanding: Who benefits from this model? Who bears the risk of errors? Are false positives and false negatives equally costly across demographic groups?
  • Data Understanding: Is the data a representative sample of the deployment population? Does the label reflect actual outcomes or prior decisions? Are protected attributes present directly or via proxies?
  • Data Preparation: Is the pre-processing consistent across demographic groups? Are missing values handled in ways that could introduce differential treatment?
  • Modeling: Are fairness constraints being considered alongside accuracy? Which error type is worse in this context (false positive vs. false negative)?
  • Evaluation: Are metrics disaggregated by protected group? Has the model been tested on subpopulations that might be underrepresented in the training set?
  • Deployment: Is there a human review step for high-stakes decisions? Is there a process to flag, investigate, and remediate disparate impact post-deployment?
  • A Minimal Fairness Audit in Code

PYTHON
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_score, recall_score
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(0)
n = 2000

# Synthetic dataset: loan approval prediction
# "group" is a protected attribute (e.g., racial category)
group = rng.choice(["A", "B"], size=n, p=[0.7, 0.3])
credit_score = rng.normal(650, 80, n)
# Group B has historically been denied more often (selection bias in labels)
base_approval = 0.3 + 0.0005 * (credit_score - 500)
group_penalty = np.where(group == "B", -0.10, 0.0)  # historical bias baked in
prob_approved = np.clip(base_approval + group_penalty, 0.05, 0.95)
label = rng.binomial(1, prob_approved)  # 1 = approved

df = pd.DataFrame({"credit_score": credit_score, "group": group, "approved": label})
df["group_enc"] = (df["group"] == "B").astype(int)

X = df[["credit_score", "group_enc"]]
y = df["approved"]
X_tr, X_te, y_tr, y_te, g_tr, g_te = train_test_split(
    X, y, df["group"], test_size=0.3, random_state=42
)

model = LogisticRegression()
model.fit(X_tr, y_tr)
y_pred = model.predict(X_te)

results = pd.DataFrame({"group": g_te, "y_true": y_te, "y_pred": y_pred})
print("Fairness Audit: Approval Rate by Group")
print("=" * 45)
for g in ["A", "B"]:
    mask = results["group"] == g
    prec = precision_score(results.loc[mask, "y_true"], results.loc[mask, "y_pred"], zero_division=0)
    rec  = recall_score(results.loc[mask, "y_true"], results.loc[mask, "y_pred"], zero_division=0)
    pred_rate = results.loc[mask, "y_pred"].mean()
    print(f"  Group {g}: predicted approval rate={pred_rate:.2%}, "
          f"precision={prec:.2%}, recall={rec:.2%}")

approval_A = results.loc[results["group"]=="A", "y_pred"].mean()
approval_B = results.loc[results["group"]=="B", "y_pred"].mean()
print(f"\n  Disparate Impact ratio (B/A): {approval_B/approval_A:.3f}")
print("  (< 0.8 triggers the 4/5ths rule under US EEOC guidelines)")

The Pre-Mortem

Borrowed from project management, the pre-mortem is a structured exercise conducted before work begins. The team imagines that the project has failed catastrophically — the model is withdrawn six months after deployment due to public backlash — and works backward to identify what could have caused that failure. This technique surfaces ethical risks that are too speculative to be raised in a normal planning meeting but are nonetheless real.

Code Examples

Pre-Mortem Risk Register Generator

Scaffold a pre-mortem risk register for a data science project, covering the most common failure modes.

PYTHON
from dataclasses import dataclass
from typing import List

@dataclass
class Risk:
    category: str
    description: str
    likelihood: str   # low / medium / high
    impact: str       # low / medium / high
    mitigation: str

def risk_score(r: Risk) -> int:
    scale = {"low": 1, "medium": 2, "high": 3}
    return scale[r.likelihood] * scale[r.impact]

risks: List[Risk] = [
    Risk("Data Quality",   "Training labels are based on biased historical approvals",
         "high", "high",   "Audit label distribution by protected group; consult domain expert"),
    Risk("Proxy Discrimination", "Zip code proxies for race in the feature set",
         "high", "high",   "Compute correlation of zip code with protected attributes; consider dropping"),
    Risk("Scope Creep",    "Stakeholders expand model use to new populations post-launch",
         "medium", "medium", "Define intended use in deployment documentation; governance review required for expansion"),
    Risk("Feedback Loop",  "Model outputs become inputs to future training data",
         "medium", "high",  "Log model decisions separately from ground-truth outcomes; schedule annual data audit"),
    Risk("Regulatory",     "GDPR data deletion requests invalidate training set",
         "low", "medium",   "Implement data lineage tracking; consult legal before training on PII"),
]

risks_sorted = sorted(risks, key=risk_score, reverse=True)
print(f"{'#':<3} {'Category':<25} {'L':<8} {'I':<8} {'Score':<7} {'Mitigation (truncated)'}")
print("-" * 90)
for i, r in enumerate(risks_sorted, 1):
    print(f"{i:<3} {r.category:<25} {r.likelihood:<8} {r.impact:<8} {risk_score(r):<7} {r.mitigation[:45]}...")
Output
#   Category                  L        I        Score   Mitigation (truncated)
------------------------------------------------------------------------------------------
1   Data Quality              high     high     9       Audit label distribution by protected group; co...
2   Proxy Discrimination      high     high     9       Compute correlation of zip code with protected ...
3   Feedback Loop             medium   high     6       Log model decisions separately from ground-trut...
4   Scope Creep               medium   medium   4       Define intended use in deployment documentation...
5   Regulatory                low      medium   2       Implement data lineage tracking; consult legal ...

28.5 Stakeholder Communication and the Project Charter Intermediate

The Communication Gap in Data Science

Data scientists are trained to optimize models. They are rarely trained to negotiate scope with product managers, manage upward expectations with an executive sponsor, or communicate uncertainty to an audience that will interpret "the model is 85 % accurate" in whatever way confirms their prior beliefs. Yet communication failures are the primary reason technically successful data science projects have no organizational impact.

This section develops a set of concrete communication artifacts and techniques, culminating in the project charter — a single document that captures everything a team needs to align before writing code.

The Stakeholder Map

Before the first project meeting, identify every person or group whose interests touch this project. For each, capture:

  • Their role (decision-maker, data provider, end-user of model output, compliance reviewer).
  • Their primary concern (accuracy, explainability, latency, regulatory risk, cost).
  • Their veto power — can they stop or materially change the project?

Stakeholder maps are deliberately informal — a whiteboard sketch is fine — but they must be made explicit. The most common stakeholder failure mode is a data scientist who builds for the technical champion (the director of data science who approved the project) while ignoring the operational users (the call-center agents who will actually use the predictions), whose adoption is required for the project to have any effect.

Communicating Uncertainty

Confidence intervals, p-values, and AUC scores mean nothing to most business audiences. The data scientist's job is to translate statistical uncertainty into decision-relevant terms.

Consider a forecasting model with a 90 % prediction interval. The useful communication is not "our 90 % prediction interval for Q3 revenue is $42M–$51M" but rather: "our model is highly confident revenue will exceed $40M (our fixed-cost break-even), but the upside above $50M is genuinely uncertain — we would not recommend committing to variable-cost expansion before month two of Q3."

This framing connects the uncertainty directly to the decision at hand, which is the only form of uncertainty stakeholders can act on.

Managing Scope with the "Three-Question" Check

Scope creep is endemic to data science projects because every analysis surfaces new questions. A practical guard is the three-question check, applied whenever a new analysis or feature is requested mid-project:

  1. Does this address the original problem statement?
  2. Is it required to meet the agreed success metric?
  3. Is there budget/time to include it without delaying the primary deliverable?

If the answer to any of the three is "no", the request is logged in a backlog and deferred to a follow-on project. This is not bureaucratic rigidity — it is what protects the team from delivering a half-finished model six months after the business decision was already made without it.

The Project Charter

A project charter is a one-to-two page document that serves as the team's shared contract. It contains:

  • Problem statement (using the template from Section 2).
  • Success metrics — primary (business KPI target) and secondary (model performance floor, e.g., "AUC >= 0.78 on holdout set").
  • Out of scope — explicit enumeration of what this project will NOT address.
  • Data assets — named sources, access status, and data owner.
  • Ethical risk summary — top three risks from the pre-mortem and their mitigations.
  • Timeline and milestones — phase-by-phase calendar with review gates.
  • Stakeholder sign-off — names and dates indicating each stakeholder has read and agreed to the above.

The charter is a living document. When scope, timeline, or success criteria change — and they will — those changes are recorded in the charter with a date and the name of the person who approved the change. This creates an audit trail that protects the data scientist when stakeholders later misremember what they agreed to.

PYTHON
import json, datetime

charter = {
    "project_name": "Subscriber Churn Prediction v1",
    "version": "1.2",
    "last_updated": str(datetime.date.today()),
    "problem_statement": (
        "For each active subscriber at month start, predict cancellation "
        "within 30 days using usage logs and support ticket history, "
        "so the retention team can prioritize 200 monthly outreach calls, "
        "reducing monthly churn from 3.2% to 2.8%."
    ),
    "success_metrics": {
        "primary": "Monthly churn rate reduced from 3.2% to <= 2.8% within 90 days of launch",
        "secondary": "Model AUC-ROC >= 0.78 on holdout set; precision@200 >= 0.35",
    },
    "out_of_scope": [
        "Predicting churn for trial (non-paying) users",
        "Real-time scoring (batch nightly scoring is sufficient for v1)",
        "Causal analysis of churn drivers",
    ],
    "data_assets": [
        {"name": "usage_logs", "owner": "Platform Eng", "access": "approved"},
        {"name": "support_tickets", "owner": "CX Team", "access": "pending legal review"},
        {"name": "billing_history", "owner": "Finance", "access": "approved"},
    ],
    "ethical_risks": [
        {"risk": "Proxy discrimination via usage pattern proxying demographics",
         "mitigation": "Audit feature-group correlations; disaggregate metrics by cohort"},
        {"risk": "Feedback loop if retention calls influence future training labels",
         "mitigation": "Log intervention status; exclude intervened customers from next training cycle"},
    ],
    "milestones": [
        {"phase": "Business Understanding", "due": "2026-06-13", "deliverable": "Signed charter"},
        {"phase": "Data Understanding",    "due": "2026-06-27", "deliverable": "Data quality report"},
        {"phase": "Modeling",              "due": "2026-07-18", "deliverable": "Candidate model + eval report"},
        {"phase": "Deployment",            "due": "2026-08-01", "deliverable": "Nightly batch scoring in prod"},
    ],
    "stakeholder_signoff": [
        {"name": "Jordan Lee",  "role": "Product Owner",   "signed": "2026-06-05"},
        {"name": "Sam Rivera", "role": "Data Scientist",   "signed": "2026-06-05"},
        {"name": "Pat Chen",   "role": "Retention Manager","signed": None},  # pending
    ],
}

# Validate completeness
print("Charter Completeness Check")
print("=" * 40)
unsigned = [s["name"] for s in charter["stakeholder_signoff"] if s["signed"] is None]
pending_data = [d["name"] for d in charter["data_assets"] if d["access"] != "approved"]
if unsigned:
    print(f"  [PENDING]  Stakeholder sign-off missing: {', '.join(unsigned)}")
if pending_data:
    print(f"  [PENDING]  Data access not yet approved: {', '.join(pending_data)}")
if not unsigned and not pending_data:
    print("  [READY]    All sign-offs and data access confirmed. Project may begin.")
print(f"\n  Milestones: {len(charter['milestones'])} phases scheduled through "
      f"{charter['milestones'][-1]['due']}")
print(f"  Out-of-scope items logged: {len(charter['out_of_scope'])}")

Handling Stakeholder Disagreement

When two stakeholders have conflicting requirements (e.g., the compliance team wants full model explainability, the product team wants maximum predictive accuracy), the data scientist's job is not to decide — it is to make the tradeoff explicit and escalate to the appropriate decision-maker. A common tool is a simple tradeoff matrix presented to both parties:

  • Option A (interpretable logistic regression): AUC 0.74, fully explainable, compliant.
  • Option B (gradient-boosted trees): AUC 0.83, SHAP explanations available, compliance review required.
  • Option C (neural net): AUC 0.86, black-box, compliance likely to reject.

Presenting options with their tradeoffs, rather than advocating for a technical preference, positions the data scientist as a trusted advisor rather than an advocate — and ensures the decision is made by the person accountable for its consequences.

Code Examples

Model Tradeoff Comparison for Stakeholder Presentation

Generate a clear visualization of model options across multiple dimensions for a stakeholder decision meeting.

PYTHON
import matplotlib.pyplot as plt
import numpy as np

models = ["Logistic\nRegression", "Gradient\nBoosted Trees", "Neural\nNetwork"]

# Scores from 0-1 across dimensions (higher is better for all)
metrics = {
    "AUC-ROC":         [0.74, 0.83, 0.86],
    "Explainability":  [1.00, 0.70, 0.20],
    "Training Speed":  [1.00, 0.60, 0.30],
    "Compliance Risk": [1.00, 0.75, 0.20],  # 1=no risk, 0=high risk
}

x = np.arange(len(models))
width = 0.20
colors = ["#2E86AB", "#E84855", "#3BB273", "#F7B731"]

fig, ax = plt.subplots(figsize=(10, 5))
for i, (metric, scores) in enumerate(metrics.items()):
    offset = (i - 1.5) * width
    ax.bar(x + offset, scores, width, label=metric, color=colors[i], alpha=0.85)

ax.set_xticks(x)
ax.set_xticklabels(models, fontsize=11)
ax.set_ylabel("Score (0–1, higher is better)", fontsize=11)
ax.set_title("Model Tradeoff Matrix for Stakeholder Review", fontsize=13, fontweight="bold")
ax.legend(loc="lower right", fontsize=10)
ax.set_ylim(0, 1.15)
ax.axhline(0.80, color="grey", linestyle=":", linewidth=1, alpha=0.6)
ax.text(2.6, 0.81, "AUC target", fontsize=9, color="grey")
plt.tight_layout()
plt.savefig("model_tradeoff.png", dpi=150)
plt.show()
print("Saved model_tradeoff.png")
Output
Saved model_tradeoff.png

Automated Milestone Tracker

Parse a charter's milestone list and print a progress report showing days remaining or overdue for each phase.

PYTHON
import datetime

milestones = [
    {"phase": "Business Understanding", "due": "2026-06-13", "status": "complete"},
    {"phase": "Data Understanding",     "due": "2026-06-27", "status": "in_progress"},
    {"phase": "Modeling",               "due": "2026-07-18", "status": "not_started"},
    {"phase": "Deployment",             "due": "2026-08-01", "status": "not_started"},
]

today = datetime.date(2026, 6, 4)  # using current date from env context

print(f"Project Milestone Report  (as of {today})")
print("=" * 60)
for m in milestones:
    due = datetime.date.fromisoformat(m["due"])
    delta = (due - today).days
    if m["status"] == "complete":
        flag = "DONE "
    elif delta < 0:
        flag = f"OVERDUE ({-delta}d)"
    elif delta <= 7:
        flag = f"DUE SOON ({delta}d)"
    else:
        flag = f"{delta} days remaining"
    print(f"  {m['phase']:<28} due {m['due']}   [{flag}]")
Output
Project Milestone Report  (as of 2026-06-04)
============================================================
  Business Understanding       due 2026-06-13   [DONE ]
  Data Understanding           due 2026-06-27   [23 days remaining]
  Modeling                     due 2026-07-18   [44 days remaining]
  Deployment                   due 2026-08-01   [58 days remaining]

28.6 Project Scoping in Practice: A Worked Example Intermediate

End-to-End Problem Framing Walkthrough

The best way to internalize the lifecycle is to walk through a realistic example from initial business conversation to signed project charter. We will use a common scenario: a mid-sized e-commerce company wants to "use data to improve marketing effectiveness."

Step 1: The Discovery Interview

Before writing anything down, the data scientist conducts a structured discovery interview with the marketing director. A set of standard questions:

  • "What decision are you making today without data that you wish you could make with data?"
  • "What would you do differently if you had this prediction / analysis?"
  • "What does success look like in six months? In numbers."
  • "What have you already tried? What happened?"
  • "Who else's buy-in is needed for this to actually change anything?"

The marketing director's answers reveal: the team currently sends a weekly promotional email to all 450,000 subscribers. Budget is fixed at $0.40 per email sent. They suspect heavy users and light users should receive different offers, but they have no way to decide who gets what.

Step 2: Identifying the Task Type

Is this descriptive, predictive, or prescriptive? The director wants to target emails differently — this is a prescription problem. However, causal inference ("what offer maximizes incremental lift?") requires significantly more infrastructure (randomized experiments, uplift models) than a simpler segmentation approach.

For a v1 project, we propose behavioral segmentation (descriptive/clustering) to identify natural customer groups, followed by a purchase probability model (predictive) to rank customers within each segment by likelihood to convert. This is tractable in one sprint and delivers the targeting capability the director needs.

Step 3: Operationalizing the Target Variable

The marketing director says "convert" — but convert means different things in the database:

  • Any order placed within 7 days of email open?
  • Any order attributable to a clicked link?
  • First purchase by a previously lapsed customer?

After discussion, we agree: conversion = any order placed within 7 days of the email send date by a customer who had no order in the prior 60 days (targeting reactivation). This is recorded in the problem statement verbatim.

Step 4: Assessing Data Feasibility

A quick data audit using the company's analytics warehouse:

PYTHON
import pandas as pd
import numpy as np

# Simulate a data availability audit
np.random.seed(7)
n = 450_000

audit_summary = {
    "email_send_log": {
        "rows": 12_400_000,
        "date_range": "2023-01-01 to 2026-05-31",
        "null_rate_customer_id": 0.003,
        "issues": "None critical",
    },
    "order_history": {
        "rows": 3_100_000,
        "date_range": "2022-06-01 to 2026-05-31",
        "null_rate_customer_id": 0.000,
        "issues": "Pre-2023 orders have no email_campaign_id (cannot attribute)",
    },
    "customer_profile": {
        "rows": 480_000,
        "date_range": "N/A (snapshot)",
        "null_rate_customer_id": 0.000,
        "issues": "30,000 rows with no email_send_log match (churned before tracking)",
    },
}

print("Data Asset Audit")
print("=" * 60)
for source, meta in audit_summary.items():
    print(f"\n  [{source}]")
    for k, v in meta.items():
        print(f"    {k:<30}: {v}")

print("\nFeasibility assessment: PROCEED with constraints:")
print("  - Attribution window limited to 2023-present (3 years of data)")
print("  - 30k pre-tracking customers excluded from training set")
print("  - Null customer_id rows (<0.3%) will be dropped in ABT construction")

Step 5: Defining the ABT Structure

With the target variable operationalized and the data sources audited, we define the unit of analysis and feature types for the analytic base table:

  • Unit: one row per (customer, emailcampaign) pair.
  • Label: converted</em>7d — binary, 1 if the customer placed an order within 7 days of the campaign and had no prior order in 60 days.
  • Feature groups:
  • Recency, Frequency, Monetary (RFM) aggregates from order history.
  • Email engagement history (open rate, click rate, unsubscribe events).
  • Product category preferences from past orders.
  • Seasonal features (days to next major sale event).
  • Step 6: Stating Success Criteria

The current email strategy has a measured conversion rate of 2.1 % on the reactivation segment. A model that scores the top 20 % of customers by purchase probability should achieve at least 3.5 % conversion if it has meaningful lift — a 67 % relative improvement. This is the primary business success criterion.

The secondary (technical) criterion: precision@top20pct >= 0.035 on a held-out validation set drawn from the most recent two months (avoiding data leakage from temporal patterns).

Step 7: Ethical Checkpoints Applied

  • Proxy discrimination: email engagement rate correlates with device type (iOS vs. Android opens), which correlates with household income. Include an income-proxy audit in the evaluation phase.
  • Opt-out respect: customers who unsubscribed must not appear in the model's scored output regardless of predicted purchase probability.
  • Consent scope: marketing analytics is within the stated use case of the privacy policy; no legal review required for this specific application.

With these seven steps completed, the project charter can be written and signed, and the team has a clear, shared, and auditable understanding of what they are building, for whom, and by what standard it will be judged.

PYTHON
# Sanity check: verify the temporal train/test split avoids leakage
import pandas as pd
import numpy as np

rng = np.random.default_rng(99)
N = 50_000

campaign_dates = pd.date_range("2024-01-01", "2026-05-31", freq="W")
dates = rng.choice(campaign_dates, N)
df = pd.DataFrame({"send_date": dates})
df["converted_7d"] = rng.binomial(1, 0.021, N)

# Temporal split: last 2 months for validation
cutoff = pd.Timestamp("2026-04-01")
train = df[df["send_date"] < cutoff]
val   = df[df["send_date"] >= cutoff]

print("Temporal Train/Validation Split")
print("=" * 40)
print(f"  Training rows:    {len(train):>7,}")
print(f"  Validation rows:  {len(val):>7,}")
print(f"  Train date range: {train['send_date'].min().date()} to {train['send_date'].max().date()}")
print(f"  Val date range:   {val['send_date'].min().date()} to {val['send_date'].max().date()}")
print(f"  Train conversion: {train['converted_7d'].mean():.3%}")
print(f"  Val conversion:   {val['converted_7d'].mean():.3%}")
assert train['send_date'].max() < val['send_date'].min(), "DATA LEAKAGE DETECTED"
print("  [PASS] No temporal overlap between train and validation sets.")

Code Examples

RFM Feature Engineering for the ABT

Compute Recency, Frequency, and Monetary value features per customer from an order history table, a standard first step in e-commerce data preparation.

PYTHON
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
today = pd.Timestamp("2026-06-01")

# Synthetic order history
n_customers = 2000
n_orders = 15000
customer_ids = rng.integers(1, n_customers + 1, n_orders)
order_dates = pd.to_datetime(
    rng.choice(pd.date_range("2024-01-01", "2026-05-31"), n_orders)
)
order_values = np.round(rng.exponential(60, n_orders), 2)

orders = pd.DataFrame({
    "customer_id": customer_ids,
    "order_date": order_dates,
    "order_value": order_values,
})

rfm = orders.groupby("customer_id").agg(
    recency_days=("order_date", lambda x: (today - x.max()).days),
    frequency=("order_date", "count"),
    monetary=("order_value", "sum"),
).reset_index()

# Bin into quintile scores (5=best)
for col, ascending in [("recency_days", False), ("frequency", True), ("monetary", True)]:
    score_col = col.replace("_days", "").replace("recency", "r_score") \
                   .replace("frequency", "f_score").replace("monetary", "m_score")
    if ascending:
        rfm[score_col] = pd.qcut(rfm[col], 5, labels=[1,2,3,4,5]).astype(int)
    else:
        rfm[score_col] = pd.qcut(rfm[col], 5, labels=[5,4,3,2,1]).astype(int)

rfm["rfm_score"] = rfm["r_score"] + rfm["f_score"] + rfm["m_score"]

print("RFM Feature Summary")
print(rfm[["recency_days","frequency","monetary","r_score","f_score","m_score","rfm_score"]]
      .describe().round(1).to_string())
print(f"\nTop 5 customers by RFM score:")
print(rfm.nlargest(5, "rfm_score")[["customer_id","recency_days","frequency","monetary","rfm_score"]].to_string(index=False))
Output
RFM Feature Summary
       recency_days   frequency    monetary     r_score     f_score     m_score   rfm_score
count       2000.0    2000.0      2000.0      2000.0      2000.0      2000.0      2000.0
mean         274.6       7.5       450.3         3.0         3.0         3.0         9.0
std          145.7       2.9       316.2         1.4         1.4         1.4         3.6
min            1.0       1.0         1.1         1.0         1.0         1.0         3.0
25%          155.0       5.0       216.8         2.0         2.0         2.0         6.0
50%          277.0       7.0       369.8         3.0         3.0         3.0         9.0
75%          398.0      10.0       598.4         4.0         4.0         4.0        12.0
max          517.0      18.0      2614.2         5.0         5.0         5.0        15.0

Data Quality Report with great_expectations

Use great_expectations to define and run a minimal data quality suite on the ABT before modeling begins.

PYTHON
# Note: requires: pip install great-expectations
import pandas as pd
import numpy as np

# Build a tiny synthetic ABT
rng = np.random.default_rng(0)
n = 1000
abt = pd.DataFrame({
    "customer_id":   rng.integers(1, 500_000, n),
    "recency_days":  rng.integers(1, 730, n),
    "frequency":     rng.integers(1, 50, n),
    "monetary":      np.round(rng.exponential(60, n), 2),
    "email_open_rate": np.clip(rng.normal(0.22, 0.10, n), 0, 1),
    "converted_7d":  rng.binomial(1, 0.021, n),
})
# Introduce a few quality issues for demonstration
abt.loc[rng.choice(n, 5), "recency_days"] = -1      # invalid negative recency
abt.loc[rng.choice(n, 3), "email_open_rate"] = 1.5  # rate > 1.0 is impossible

# Manual expectation checks (GE-style, no server required)
results = []
def check(name, series, condition):
    pct_passing = condition(series).mean()
    results.append({"expectation": name,
                    "pass_rate": pct_passing,
                    "status": "PASS" if pct_passing == 1.0 else "FAIL"})

check("recency_days >= 0",           abt["recency_days"],    lambda s: s >= 0)
check("frequency >= 1",              abt["frequency"],       lambda s: s >= 1)
check("monetary > 0",                abt["monetary"],        lambda s: s > 0)
check("email_open_rate in [0, 1]",   abt["email_open_rate"], lambda s: (s >= 0) & (s <= 1))
check("converted_7d in {0, 1}",      abt["converted_7d"],    lambda s: s.isin([0, 1]))
check("no duplicate customer_ids",   abt["customer_id"],
      lambda s: pd.Series([s.duplicated().sum() == 0]).repeat(len(s)).reset_index(drop=True))

for r in results:
    print(f"  [{r['status']}] {r['expectation']:<40} pass_rate={r['pass_rate']:.4f}")
Output
  [FAIL] recency_days >= 0                          pass_rate=0.9950
  [PASS] frequency >= 1                             pass_rate=1.0000
  [PASS] monetary > 0                               pass_rate=1.0000
  [FAIL] email_open_rate in [0, 1]                  pass_rate=0.9970
  [PASS] converted_7d in {0, 1}                     pass_rate=1.0000
  [FAIL] no duplicate customer_ids                  pass_rate=0.0000