
You've made it past the resume screen and the first recruiter call. Now you're staring at an email with a ZIP file attached and instructions that say something like: "Here's a dataset of customer transactions. Explore it, find insights, and present your findings. You have 72 hours."
This is the take-home assignment — and it's where most candidates lose the job they were otherwise qualified for. Not because they can't do the analysis, but because they treat it like a homework assignment rather than a professional deliverable. They produce a Jupyter notebook full of uncommented cells, a couple of bar charts with default color palettes, and a conclusion that basically restates what the charts already showed. Hiring managers see hundreds of these. You can do better, and this lesson will show you exactly how.
By the end of this lesson, you'll know how to approach a take-home assignment like a senior analyst — from the first read-through of the brief to the final slide or notebook you hand in. You'll understand how to structure your thinking, write production-quality code, communicate findings with clarity, and leave reviewers with the impression that you already think like someone on their team.
What you'll learn:
This lesson assumes you're comfortable with Python (pandas, matplotlib/seaborn or plotly), have worked with real-world tabular datasets before, and understand basic statistics well enough to interpret distributions, correlation, and summary metrics. If you've built even one end-to-end analysis project, you're ready for this.
The single most important thing you do in this assignment happens before you open a single file. It's reading the prompt — carefully, critically, and more than once.
Most candidates skim the brief and start coding within ten minutes. They treat it like a Kaggle problem: load data, run EDA, make charts, submit. But take-home assignments aren't Kaggle problems. They're miniature consulting engagements. The company is trying to see how you think, how you communicate, and whether you understand the difference between an interesting finding and an actionable insight.
Here's a realistic example prompt:
"Attached is six months of transaction data from our e-commerce platform. We're particularly interested in understanding customer retention and identifying which product categories are driving repeat purchases. Please share your analysis in whatever format you think is most appropriate. We'll review your submission before a 30-minute discussion."
There are several things to extract from this:
The explicit questions: Customer retention and repeat purchase categories. These are your primary deliverables. Everything else is supporting context.
The implied questions: What's the overall health of the business? Are there any anomalies or data quality issues worth flagging? What would you recommend based on what you found?
The signal in "whatever format you think is most appropriate": This isn't permission to be lazy. It's a test of judgment. A well-structured notebook with a summary section is usually right. A separate slide deck is better if the role is more stakeholder-facing. A Python script with a README is appropriate if it's an engineering-adjacent role.
The phrase "we'll review your submission before a 30-minute discussion": This means your submission is also a conversation starter. You're not just answering the question — you're setting up a dialogue. Leave thoughtful threads open. Note where you'd go next with more time or data.
Write down your interpretation of what "success" looks like before you open the dataset. A single paragraph is enough. This becomes your north star when you're three hours deep and tempted to go down an interesting but irrelevant rabbit hole.
Now you can open the files. But don't start with df.head() and free-association from there. Structure your exploration like a medical intake exam — systematic, with a clear purpose for each check.
Your first notebook section should answer: What do I actually have?
import pandas as pd
import numpy as np
df = pd.read_csv('transactions.csv', parse_dates=['transaction_date'])
# Shape and basic types
print(f"Rows: {df.shape[0]:,} | Columns: {df.shape[1]}")
print("\n--- Column Types ---")
print(df.dtypes)
# Date range
print(f"\nDate range: {df['transaction_date'].min()} to {df['transaction_date'].max()}")
# Unique counts for key dimensions
for col in ['customer_id', 'product_category', 'order_id']:
print(f"Unique {col}: {df[col].nunique():,}")
This isn't glamorous, but it's critical. You need to know whether you have 5,000 rows or 5 million (which affects every method choice you'll make), how many customers are in the data, whether dates are actually being parsed as dates, and whether that order_id column is truly unique or hiding duplicate records.
Before you build a single metric, you need to understand the data's trustworthiness.
# Missing values — both count and percentage
missing = pd.DataFrame({
'count': df.isnull().sum(),
'pct': (df.isnull().sum() / len(df) * 100).round(2)
}).query('count > 0').sort_values('pct', ascending=False)
print("--- Missing Values ---")
print(missing)
# Duplicate order IDs (these shouldn't exist in clean transaction data)
duplicate_orders = df[df.duplicated(subset='order_id', keep=False)]
print(f"\nDuplicate order_id rows: {len(duplicate_orders):,}")
# Negative or zero revenue (almost always a data issue)
bad_revenue = df[df['revenue'] <= 0]
print(f"Rows with revenue ≤ 0: {len(bad_revenue):,}")
# Sanity check: are there transactions outside the expected date range?
expected_start = pd.Timestamp('2023-07-01')
expected_end = pd.Timestamp('2024-01-01')
out_of_range = df[~df['transaction_date'].between(expected_start, expected_end)]
print(f"Out-of-range dates: {len(out_of_range):,}")
This matters for two reasons. First, your analysis will be wrong if you don't handle data quality issues. Second, flagging data quality issues is itself impressive. A candidate who says "I noticed 3% of revenue values were negative, which I excluded after confirming these were likely refunds — here's how that affected the cohort counts" immediately signals production experience.
Now you're doing actual exploration, but with the brief's questions in mind. You're not cataloging every column. You're building toward an answer.
For the retention question, you need to know: how many customers made more than one purchase?
# Customer purchase frequency distribution
purchase_counts = df.groupby('customer_id')['order_id'].nunique().reset_index()
purchase_counts.columns = ['customer_id', 'num_orders']
freq_dist = purchase_counts['num_orders'].value_counts().sort_index()
repeat_rate = (purchase_counts['num_orders'] > 1).mean()
print(f"Repeat purchase rate: {repeat_rate:.1%}")
print("\nPurchase frequency distribution:")
print(freq_dist.head(10))
For the category question, you're looking at what categories appear in the second purchase for customers who returned:
# Identify repeat customers
repeat_customers = purchase_counts[purchase_counts['num_orders'] > 1]['customer_id']
# Get their second purchase category
repeat_txns = df[df['customer_id'].isin(repeat_customers)].copy()
repeat_txns = repeat_txns.sort_values(['customer_id', 'transaction_date'])
repeat_txns['purchase_rank'] = repeat_txns.groupby('customer_id').cumcount() + 1
second_purchase_categories = (
repeat_txns[repeat_txns['purchase_rank'] == 2]
['product_category']
.value_counts(normalize=True)
.mul(100)
.round(1)
)
print("Categories driving second purchases:")
print(second_purchase_categories)
Tip: Notice that we're not trying to find everything interesting in the data — we're investigating the specific questions the prompt asked. Keeping that discipline is what separates a focused, readable submission from a data dump.
Once your EDA has given you directional answers, you build the actual analysis — the thing that goes in your final submission. This is where you need to think carefully about what to include, not just what you found.
Every analysis section should connect to the next. You're building an argument, not a tour of the dataset. A good structure for a retention analysis looks like this:
Each section's conclusion should set up the next section's question. That's what makes it feel like analysis rather than a list of observations.
If you're analyzing retention and you're not doing cohort analysis, you're leaving the most important signal on the table. A single overall repeat rate number hides whether things are improving or degrading over time.
# Build a monthly cohort table
df['cohort_month'] = df.groupby('customer_id')['transaction_date'].transform('min').dt.to_period('M')
df['transaction_month'] = df['transaction_date'].dt.to_period('M')
# Calculate cohort period index
df['cohort_period'] = (
df['transaction_month'].astype(int) - df['cohort_month'].astype(int)
)
# Cohort size (unique customers acquired each month)
cohort_sizes = (
df[df['cohort_period'] == 0]
.groupby('cohort_month')['customer_id']
.nunique()
.rename('cohort_size')
)
# Active customers per cohort per period
cohort_data = (
df.groupby(['cohort_month', 'cohort_period'])['customer_id']
.nunique()
.reset_index()
)
# Build retention matrix
retention_matrix = cohort_data.pivot_table(
index='cohort_month',
columns='cohort_period',
values='customer_id'
)
# Convert to percentages relative to cohort size
retention_pct = retention_matrix.divide(cohort_sizes, axis=0).round(3)
print(retention_pct.to_string())
This gives you a matrix where each cell tells you what percentage of customers from a given acquisition month were still active in subsequent months. If Period 1 retention is consistently around 20-25% but the August cohort shows 35%, that's a real finding worth highlighting.
Warning: Cohort analysis only makes sense if your date range is long enough to observe multiple periods per cohort. If you only have six weeks of data, a monthly cohort analysis won't show you much. Choose your time granularity based on the data you have.
How you write your code is almost as important as what you find. The person reviewing your submission is often a senior analyst or data scientist. They will read your code. They will notice things.
1. Use meaningful intermediate variables, not chains.
Instead of this:
# Hard to debug, impossible to explain
result = df[df['status'] == 'completed'].groupby('customer_id')['revenue'].sum().reset_index().rename(columns={'revenue': 'total_revenue'}).sort_values('total_revenue', ascending=False).head(20)
Write this:
completed_orders = df[df['status'] == 'completed']
customer_revenue = (
completed_orders
.groupby('customer_id', as_index=False)['revenue']
.sum()
.rename(columns={'revenue': 'total_revenue'})
)
top_customers = customer_revenue.sort_values('total_revenue', ascending=False).head(20)
The second version takes three more lines. It is infinitely more readable and debuggable.
2. Comment on the why, not the what.
# Bad comment: explains what the code does (which you can already see)
# Filter for completed orders
completed_orders = df[df['status'] == 'completed']
# Good comment: explains why this decision was made
# Exclude cancelled and refunded orders — these inflate customer counts
# but don't represent genuine purchasing behavior
completed_orders = df[df['status'] == 'completed']
3. Write a data quality summary at the top of your notebook.
Before your analysis begins, include a markdown cell that summarizes what you found in data quality checks and how you handled it. This is the professional equivalent of a methodology section. It shows you're aware that data is never perfect and that you made deliberate decisions.
## Data Quality Notes
- **Missing values:** 2.3% of `product_category` values are null. These were excluded
from category-level analysis but retained for overall revenue and customer metrics.
- **Duplicate order IDs:** 47 duplicate rows found. Investigation showed these
represent split shipments of a single order. Deduplicated to one row per order_id
using first occurrence.
- **Negative revenue:** 312 rows with revenue < 0, representing approximately 1.1%
of transactions. Treated as refunds and excluded from purchase analysis but tracked
separately as a potential retention signal.
- **Date range:** Data spans July 1, 2023 through January 2, 2024 (183 days).
Note that the January 2 transactions may represent a partial day — the final
cohort (December) has limited observation window.
4. Use functions for repeated logic.
If you're computing the same metric three different ways in three different cells, that's a signal that it should be a function:
def compute_retention_rate(dataframe, customer_col='customer_id', order_col='order_id'):
"""
Calculate the repeat purchase rate for a customer population.
Returns the fraction of customers with more than one unique order.
"""
purchase_counts = dataframe.groupby(customer_col)[order_col].nunique()
return (purchase_counts > 1).mean()
# Now you can use it for different segments
overall_retention = compute_retention_rate(df)
mobile_retention = compute_retention_rate(df[df['acquisition_channel'] == 'mobile'])
email_retention = compute_retention_rate(df[df['acquisition_channel'] == 'email'])
print(f"Overall: {overall_retention:.1%}")
print(f"Mobile: {mobile_retention:.1%}")
print(f"Email: {email_retention:.1%}")
This is not over-engineering — it's the minimum standard for code you're sharing with someone who might have to maintain it.
Default matplotlib charts look like homework. Overdesigned charts with six colors, dual axes, and a legend that requires a magnifying glass look like you're compensating for thin analysis. The goal is clarity with competence.
Every chart in your submission should have: a title that states the finding (not the variable name), labeled axes with units, and a brief annotation or takeaway either in the chart or immediately below it.
Here's the difference between a chart that looks like homework and one that looks like work:
import matplotlib.pyplot as plt
import seaborn as sns
# Set a clean, professional style once at the top
plt.rcParams.update({
'figure.facecolor': 'white',
'axes.facecolor': '#f8f8f8',
'axes.spines.top': False,
'axes.spines.right': False,
'font.family': 'sans-serif',
'axes.titlesize': 14,
'axes.titleweight': 'bold',
'axes.titlepad': 12
})
fig, ax = plt.subplots(figsize=(10, 5))
# Data: monthly retention rates
months = ['Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
period_1_retention = [0.21, 0.35, 0.24, 0.22, 0.20, 0.19]
bars = ax.bar(months, period_1_retention, color='#2E86AB', width=0.6, edgecolor='white')
# Highlight the anomalous month
bars[1].set_color('#E84855')
# Add value labels
for bar, val in zip(bars, period_1_retention):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.005,
f'{val:.0%}', ha='center', va='bottom', fontsize=11)
# Annotation explaining the anomaly
ax.annotate(
'August cohort shows unusually\nhigh retention — coincides\nwith summer promo campaign',
xy=(1, 0.35), xytext=(2.5, 0.38),
arrowprops=dict(arrowstyle='->', color='#555'),
fontsize=10, color='#333'
)
ax.set_ylim(0, 0.50)
ax.set_ylabel('Month-1 Retention Rate', fontsize=11)
ax.set_xlabel('Acquisition Cohort', fontsize=11)
ax.set_title("August Cohort Retained at 67% Above Average — Driven by Summer Campaign",
fontsize=13)
plt.tight_layout()
plt.savefig('retention_by_cohort.png', dpi=150, bbox_inches='tight')
plt.show()
Notice the title says "August Cohort Retained at 67% Above Average — Driven by Summer Campaign." Not "Monthly Retention Rates by Cohort." The title is the insight, not the description.
Tip: Three to five high-quality, annotated charts beats ten default charts every time. Each chart should make exactly one point. If you're struggling to write a one-sentence title that states a finding, the chart probably shouldn't be in your submission.
Now you have the analysis. You need to package it in a way that a hiring manager can consume in under ten minutes and a senior data person can dig into for thirty.
Take-home assignments are reviewed by at least two types of people: the hiring manager (who wants to know what you found and whether you can communicate it) and a technical peer (who wants to know how you found it and whether you can code). Your submission needs to serve both.
The cleanest solution is a structured Jupyter notebook with a clearly delineated executive summary section at the top, followed by the full analysis. Some teams prefer a separate slide deck. If the job description mentions stakeholder communication or business-facing work, lean toward a deck. If it's more internally focused, a clean notebook is fine.
This is the first thing reviewers see, and it's the thing most candidates skip entirely. It should be a markdown section — not code — that covers:
# Executive Summary
## What I Was Asked to Do
Analyze six months of transaction data to understand customer retention patterns
and identify which product categories drive repeat purchases.
## Key Findings
1. **Overall repeat purchase rate is 23%**, meaning roughly 1 in 4 customers
makes a second purchase within the observation window. This is below typical
e-commerce benchmarks of 27–32%.
2. **The August acquisition cohort shows 35% Month-1 retention**, significantly
above average. This cohort was acquired during the summer promotion and
represents a natural experiment worth studying further.
3. **Home & Garden and Electronics are the strongest drivers of second purchases**,
accounting for 41% of all repeat transactions despite making up only 28% of
first purchases. Apparel, despite high first-purchase volume, shows the lowest
return rate.
## What I'd Investigate Next
- Whether the August promotion can be replicated — and which specific mechanics
drove the retention lift
- Why Apparel customers don't return — price point, sizing issues, or a product
quality signal worth flagging to the merchandising team
- Channel-level retention breakdown to see if acquisition source predicts LTV
## Methodology Notes
Full analysis in sections below. Key data quality decisions noted in Section 1.
This section demonstrates something that raw analysis never can: that you can synthesize, prioritize, and communicate. Those are the skills companies actually struggle to hire for.
Here's the most common weakness in take-home submissions: candidates describe what they found but don't say what to do about it. Analysis without a recommendation is journalism, not consulting. Even if you're hedged and conditional, say something:
"Based on the August cohort data, I'd recommend running a structured A/B test on the summer promotion mechanics — specifically the 15% discount versus the free shipping offer — to isolate which lever drove the retention lift. If free shipping was the driver, there's likely a permanent CPA threshold worth testing."
You don't have to be right. You have to demonstrate that you think past the chart.
Apply this framework to a real dataset you can download in the next hour.
Dataset: Use the Online Retail II UCI dataset — it's freely available and contains ~1M transactions with customer IDs, invoice dates, product descriptions, quantities, and prices from a UK-based retailer.
Your task:
Load the data and complete a full data quality inventory. Document your findings in a markdown cell before doing any analysis.
Compute the monthly cohort retention matrix for 2010 customers. How does Month-1 retention compare across acquisition cohorts?
Identify the top five product categories (you'll need to clean/group the Description column) most associated with repeat purchases. A customer "returned" if they have a transaction in a later month.
Build three publication-quality charts:
Write a 300-word executive summary as if you were handing this to a Head of E-commerce. What did you find? What would you recommend? What questions remain?
Stretch goal: Segment customers by their first-purchase category and compare their retention rates. Do customers who start with high-margin categories show different retention patterns?
Time yourself. A polished submission for a real assignment of this scope should take 8–12 focused hours. If you're consistently going over 15, identify where you're losing time — that's usually either unfocused EDA or perfectionism on visualizations.
What it looks like: A notebook with 25 charts, one for every column in the dataset. Distribution of revenue. Distribution of quantity. Distribution of unit price. A histogram for every numeric field.
Why it fails: It signals that you don't know which questions matter. It also makes the submission exhausting to review.
The fix: At the start of your analysis, write down the three to five questions you're trying to answer. Every chart and table should map to one of those questions. If you can't identify which question a chart answers, cut it.
What it looks like: "Customers who buy from the Electronics category have a 40% higher repeat purchase rate. Therefore, we should push customers toward Electronics."
Why it fails: Customers who buy Electronics might be inherently more engaged customers. You're measuring customer quality, not category quality.
The fix: Acknowledge the limitation explicitly. "This is an association, not a causal relationship. To test whether nudging customers toward Electronics increases retention, you'd need an experiment — for example, a promotional email test." This signals statistical maturity, which is rare and impressive.
What it looks like:
# Hardcoded — breaks if the data changes
high_value_customers = df[df['revenue'] > 500]
Why it fails: The reviewer might run your code on a different cut of the data. Or they might ask "how did you decide on $500?" and you have no answer.
The fix: Use percentile-based thresholds with a comment explaining your reasoning:
# Define "high value" as top quartile of customer lifetime revenue
# Percentile-based threshold adapts if the data changes
revenue_threshold = df.groupby('customer_id')['revenue'].sum().quantile(0.75)
high_value_customers = df[df['customer_id'].isin(
df.groupby('customer_id')['revenue'].sum()[
lambda x: x >= revenue_threshold
].index
)]
Hiring managers know you had a time limit. They respect candidates who acknowledge the constraints and say what they'd do with more time. A brief "given more time, I'd also look at..." section shows intellectual honesty and continued curiosity.
Run through your notebook top to bottom in a fresh kernel before submitting. Kernel → Restart & Run All is the minimum bar. If a cell errors out, that's an instant negative impression. Also re-read every markdown cell as if you've never seen it. Remove jargon, fix typos, and cut sentences that don't add meaning.
The take-home assignment is the highest-leverage moment in your job search. It's the one part of the process where you have full control — time, tools, structure, and narrative. Most candidates squander it by treating it as a technical task when it's actually a communication task that happens to involve technical work.
Here's the framework in compressed form:
Next steps to deepen this skill:
The candidate who gets the offer isn't always the one who found the most insights. It's usually the one who told the clearest story about the right ones.
Learning Path: Landing Your First Data Role