How can an e-commerce business identify the customers, channels, and products responsible for its performance — and quantify where meaningful opportunities exist? This project treats itself as the analytics function supporting an e-commerce company: establish a performance baseline, find where value is being created or lost, test whether apparent patterns are real, and size what closing a real gap would actually be worth.
Architecture
TheLook eCommerce
→
BigQuery
→
SQL Data Models
↓
Python → Plotly.js
Tableau → Executive BI
↓
Business Findings
Executive Findings
Growth Is Volume-Driven, Not Economics-Driven
AOV and margin sit essentially flat across 8 years while order volume compounds, powered by both accelerating new-customer acquisition and a growing repeat-purchase base — plus real holiday seasonality once a partial-year confound is removed.
Purchase Frequency, Not Demographics, Is the Real Value Driver
The customer value ladder — Never ($0) → One-time ($85) → Repeat ($192) → Frequent ($342) — is purely behavioral. Gender, age, geography, and channel are statistically flat across every tier.
No Factor Predicts Who Becomes a Repeat Buyer — Timing Does
Gender, age, country, channel, first-order size, spend, and return status all come back null, jointly and individually. What actually differentiates outcomes is timing: the gap between purchases shrinks from a median 260 days (1st→2nd order) to 169 days (3rd→4th), and 41% of eventual repeaters take over a year to return. That's why the campaign below is broad and sustained, not narrowly targeted or a short post-purchase drip.
Discovered Optimization Opportunity
The recommended opportunity: retarget one-time buyers into repeat buyers. The population is real and fully identified — 29,721 one-time buyers who purchased in the last 24 months and haven't repeated yet — sized against a real historical baseline (27.6% natural 24-month repeat rate), not an assumption of zero. This is a scenario, not a forecast. The full analytical path behind it — performance baseline, acquisition, segmentation, the conversion funnel, statistical testing — is below, collapsed by default.
How: A Broad, Sustained Retargeting Campaign
Email (for the identified base) or paid retargeting ads, sent to the full one-time-buyer list rather than a narrowed segment — an exhaustive check found no factor (demographics, channel, first-order size, first-order return status) predicts who's more likely to come back, so targeting narrowly would only shrink reach for no accuracy gain. Run as a sustained 12-24 month lifecycle program, not a short post-purchase drip: only about 26% of eventual repeaters return within 90 days, and 41% take over a year, so a short win-back window would structurally miss most of the population it's meant to reach.
—Incremental repeaters
—Incremental revenue
—Incremental profit
Computed directly in your browser from the live target-list size and the measured per-customer economics ($106.81 revenue / $55.51 profit per converted customer) — no server round trip.
The second purchase is the hard threshold — customers accelerate once past it.
Why the campaign needs a 12-24 month horizon, not a 90-day drip.
Supporting Analysis
01 · Performance Explorer
Revenue, orders, AOV, and margin over time — switch the metric with the dropdown on the chart.
Revenue seasonality, complete years only (2020-2025) — December peaks.
Order share by day of week — a modest weekend bump.
The order mix shifting from almost-entirely-new (2019) toward a growing repeat share (2026), at unchanged per-order economics throughout.
02 · Acquisition Quality
Search dominates on raw volume, but every quality metric — conversion, AOV, revenue per user, repeat rate — sits nearly flat across all five channels. Formally tested, not just eyeballed: chi-square p=0.578, Cramér's V = 0.005.
Volume — Search wins purely on scale.
Quality — switch metrics with the dropdown. Every one is flat.
03 · Customer/Segment Explorer
The value ladder is purely a frequency effect — darker bars are higher-frequency tiers, not a different kind of customer.
Return behavior barely moves revenue within a frequency tier.
Return behavior is bimodal — customers return everything or nothing.
Demographics don't differentiate value — every cut reads flat. Switch cuts with the dropdown.
04 · Conversion Analysis
Clearly, in order to purchase, users have to be identified. But since identification is already fixed at the very start of the session, it's unlikely to be a login happening there — no login/signup event exists in this dataset's taxonomy at all. No clear information was captured about how identification actually happens, but it's likely tied somehow to the purchasing flow itself rather than a separate checkout-page action.
A caught reasoning error worth stating plainly: an earlier pass treated "anonymous sessions never convert" as a growth opportunity — get more unidentified visitors to convert — as if identification and purchase were separable events with a gap to close between them. They aren't. Identification is fixed before a session's first tracked event and never changes mid-session, and every purchase in this dataset is, by construction, already identified — so this isn't a discovered gap so much as a restatement of how conversion is defined here. There's no unidentified-to-converted lever to pull at the cart or checkout step; any real intervention would have to happen before a session even starts, not during it.
0.00%Anonymous cart → purchase
99.31%Identified cart → purchase
The same near-total separation, shown as a literal outcome comparison.
05 · Statistical Findings
Separating "statistically detectable" from "practically meaningful" — a real null, a technically-significant-but-trivial coefficient, and one real small effect.
Factor
Test
p-value
Effect size
Verdict
Acquisition channel
Chi-square (traffic_source x converted)
5.78e-1
0.0054 (Cramer's V)
null
Main conversion model (age+gender+channel+tenure)
Logistic regression, LLR test
1.86e-1
0.0001 (pseudo R-squared)
null
tenure_days (within main conversion model)
Logistic regression coefficient
3.92e-2
1.0000 (odds ratio)
significant but meaningless
Gender x purchaser revenue
Welch's t-test + bootstrap CI
1.05e-64
0.1268 (Cohen's d)
real but small
Repeat-purchase joint model (age+gender+channel+country+first-order size)
Logistic regression, LLR test
4.84e-1
0.0001 (pseudo R-squared)
null
Repeat-purchase: gender
Chi-square
3.87e-1
—
null
Repeat-purchase: age band
Range across 6 bands
—
range 32.8%-33.9%
null
Repeat-purchase: country
Range across top 10
—
range 31.2%-35.0%
null
Repeat-purchase: channel
Range across 5 sources
—
range 31.7%-33.9%
null
Repeat-purchase: first-order item count
Range
—
range 33.2%-34.2%
null
Repeat-purchase: first-order spend quartile
Range
—
range 32.7%-33.9%
null
Repeat-purchase: first-order return status
Two-group comparison
—
33.3% vs 33.7%
null
Repeat-purchase: first-purchase category
Chi-square goodness of fit
4.30e-1
—
null
Rows testing the main conversion model, the repeat-purchase joint model, the channel/gender tests, and tenure_days were re-run live against the frozen snapshot for this page. The individual-factor repeat-purchase scan (age band, country, channel, first-order size, etc.) is carried from the already-verified exhaustive null sweep documented in the project's own methodology notes.
Behind the Analysis
Two SQL examples and three Python examples — not the full pipeline, just enough to show the work.
SQL — the return-rate discipline
-- Grain: one row per calendar day. Question 1 (business performance
-- baseline). Depends on fct_orders.
CREATE OR REPLACE TABLE `...marts_20260921.mart_daily_performance` AS
SELECT
DATE(created_at) AS order_date,
COUNT(DISTINCT order_id) AS orders,
COUNT(DISTINCT user_id) AS customers,
SUM(revenue) AS revenue,
SUM(profit) AS gross_profit,
SAFE_DIVIDE(SUM(revenue), COUNT(DISTINCT order_id)) AS aov,
SAFE_DIVIDE(SUM(profit), SUM(revenue)) AS margin_pct,
SAFE_DIVIDE(COUNTIF(has_return), COUNT(*)) AS order_return_rate
FROM `...marts_20260921.fct_orders`
WHERE NOT is_cancelled
GROUP BY order_date
ORDER BY order_date;
SQL — the funnel's terminal-outcome logic
-- Grain: one row per session. funnel_outcome takes the furthest real
-- stage reached, with purchase/cancel as the two mutually exclusive
-- terminal outcomes once a session reaches cart -- confirmed from real
-- event sequences, not assumed.
SELECT
session_id,
ANY_VALUE(user_id) IS NULL AS is_anonymous,
LOGICAL_OR(event_type = 'cart') AS reached_cart,
CASE
WHEN LOGICAL_OR(event_type = 'purchase') THEN 'purchase'
WHEN LOGICAL_OR(event_type = 'cancel') THEN 'cancel'
WHEN LOGICAL_OR(event_type = 'cart') THEN 'abandoned_at_cart'
WHEN LOGICAL_OR(event_type = 'product') THEN 'abandoned_at_product'
ELSE 'abandoned_at_home'
END AS funnel_outcome
FROM `...raw_snapshot_20260921.events`
GROUP BY session_id;
Python — testing the identification hypothesis
# Is identification the real bottleneck, or is checkout friction?
by_anon = query_df("""
SELECT is_anonymous,
COUNTIF(reached_cart) AS reached_cart,
COUNTIF(reached_purchase) AS reached_purchase,
SAFE_DIVIDE(COUNTIF(reached_purchase),
COUNTIF(reached_cart)) AS cart_to_purchase_rate
FROM `marts_20260921.mart_funnel`
WHERE reached_cart
GROUP BY is_anonymous
""")
# identified: 99.31% anonymous: 0.00% -- zero exceptions, 680,987 sessions
Python — statistically real vs. practically large
# Statistically significant is not the same as practically large.
t, p = stats.ttest_ind(male_rev, female_rev, equal_var=False)
d = (male_rev.mean() - female_rev.mean()) / np.sqrt(
(male_rev.std() ** 2 + female_rev.std() ** 2) / 2)
rng = np.random.default_rng(42)
diffs = [rng.choice(male_rev.values, len(male_rev), replace=True).mean()
- rng.choice(female_rev.values, len(female_rev), replace=True).mean()
for _ in range(2000)]
# p=1.05e-64 (real), Cohen's d=0.127 (small) -- both true at once
Methodology
Dataset, snapshot, and known limitations
Dataset: Google's public bigquery-public-data.thelook_ecommerce — synthetic, not real transactions. The dataset is continuously refreshed, so every table was materialized into a dated snapshot before any analysis — all numbers on this page trace back to that fixed snapshot, not a live query.
Data model: seven raw tables roll up into ten BigQuery marts (three fact tables, two dimensions, five pre-aggregated marts) that Python and Tableau both consume, so neither can invent a slightly different version of "revenue" or "margin."
Statistical methodology: chi-square tests of independence, Kruskal-Wallis, binomial tests, Welch's t-test with bootstrap confidence intervals, Cohen's d alongside every significance test, and logistic regression with likelihood-ratio tests. Every p-value is reported with an effect size or a plain-English magnitude — a significant result without a stated magnitude was treated as an incomplete answer throughout.
Known limitations: this is synthetic data — some patterns (the exactly-one-product-view-per-session funnel, the coin-flip cancel/abandon split) read as generator artifacts, not realistic behavior, and are flagged as such rather than presented as business insight. No causal inference is claimed anywhere — the opportunity simulator above is explicitly scenario analysis (a stated assumption run through arithmetic), not a causal estimate of what a specific campaign would achieve. Return rate (12.06% of orders) is a real, sizable pattern with no identified driver anywhere checked — reported honestly as an open question.
The dataset (Google's public thelook_ecommerce) is synthetic, not a real company's transactions — stated plainly because this project demonstrates analytical methodology, engineering, and communication, not claims about a real business. Every number above traces back to a materialized, dated snapshot so the analysis stays reproducible rather than shifting under a continuously-refreshed public table.