cswork / libraryOpen practice workspace

Interview guides · Brady

Brady Data Scientist Interview Questions & Guide 2026

A two-week prep plan for Brady data scientist candidates: write your answer before reading the solution, revisit misses three days later.

The 60-second brief

The Data Scientist role at Brady is pivotal in harnessing data to drive informed decision-making and innovative solutions across the organization. As a Data Scientist, you will analyze complex datasets, build predictive models, and contribute to strategic initiatives that enhance products and services. This position is critical for ensuring that Brady remains competitive and responsive to market trends, ultimately impacting customer satisfaction and business performance.

When randomisation is off the table, the skill being checked is naming an identification strategy together with the assumption it rests on: parallel trends for difference-in-differences, relevance and exclusion for an instrument, overlap and conditional ignorability for matching. Say the assumption out loud and say how you would try to break it.

Focus areas

  • Separate release-calendar spikes from underlying trend
  • Measure catalogue breadth beyond head consumption
  • Define a qualified stream and defend it

Overview

In this role, you will collaborate with cross-functional teams, including marketing, engineering, and product management, to provide insights that shape product development and optimize operational efficiency. You will work on real-world problems, leveraging your analytical skills to develop data-driven solutions that affect millions of users. Expect to engage in challenging projects that not only require technical expertise but also creativity and strategic thinking.

The Brady interview process

Brady candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Initial Screening

A screening call is a matching exercise run by someone who will not evaluate your statistics. They are checking that the work described on your resume is work you personally did, and that its scope matches the level the role is written for. Logistics get settled in the same half hour so nobody spends an interviewer's afternoon on a mismatch. The answer that fails is the one narrated in the plural. If every sentence is 'we built' and 'the team decided', there is nothing specific to write down about you. Name the piece that was yours, the decision you made inside it, and what changed after.

What to demonstrate

  • Whether the ownership implied by your resume survives one round of follow-up about who actually did which part
  • Whether your described scope (data size, stakeholders, what shipped) matches the seniority the role is written at
  • Whether timeline, location and compensation expectations make the rest of the loop worth scheduling

How to prepare

  • Rewrite your top three resume bullets in the first person singular, each with the decision you made and what moved afterwards, then say them out loud once so the 'we' does not return under pressure
  • Attach one number to each project: the baseline, the change, and the window it was measured over. Where impact was never measured, say that plainly rather than inventing a figure
  • Settle your compensation range before the call and give it as a range with a reason behind it, such as current total comp or a competing timeline, instead of deflecting the question twice

Technical Assessment

Much of what gets scored here happens out loud while you type. Nobody can see your reasoning inside a half-written query, so five silent minutes read as being stuck even when they are not. State the plan in plain language first: which tables, what grain you are aggregating to, and the one filter that defines the population. Then write it. The narration doubles as insurance, because a wrong plan gets caught early and cheaply while a wrong query gets caught at the end with no time left to redo it. A timed statistics section, where one exists, is a separate test with its own clock.

What to demonstrate

  • Whether the query you write matches the plan you just described
  • What you do with a hint, meaning whether the correction gets absorbed or the first approach gets defended
  • Whether you can debug your own wrong output by reading the result set and naming which part of the query produced the anomaly

How to prepare

  • Solve three problems while screen-sharing into a recording, then watch it back and mark every stretch longer than thirty seconds where you said nothing
  • Practise compressing the plan into one sentence before typing, then check afterwards whether the finished query actually matched it
  • Time yourself on statistics questions that carry a business reading, such as what a confidence interval does and does not claim, rather than re-reading notes without a clock

Behavioral Interview

Most of the weight in this round sits on the disagreement questions. Data work routinely produces an answer someone senior did not want, and the interviewer is trying to learn what you do in that hour. Both failure modes are common: folding as soon as a director pushes back, and treating the pushback as ignorance to be corrected with a better chart. A strong answer usually contains a specific thing the other person knew that you did not, and describes how you found out whether it changed the conclusion.

What to demonstrate

  • Whether you can state the other side's argument accurately before you explain why you disagreed
  • What you treated as evidence during the disagreement, such as a rerun under their assumption or a holdout check, rather than persuasion technique
  • Whether you distinguish being overruled from being wrong, and can give an example of each

How to prepare

  • Write out one disagreement where you turned out to be wrong, and say what in the data misled you. Candidates prepare the story where they were right, and the follow-up asks for the other one.
  • For your main disagreement story, be ready to say what result would have made you drop your position. If no such result exists, you were not arguing from the data.
  • Practise stating the opposing position out loud in one sentence the stakeholder would accept, then continue the story.

Where candidates lose points

Treating the account as the person, or the profile as the person.

A household account carries several people, profiles frequently are not switched, and shared-screen, car and speaker playback often lands on a default profile with no user behind it. Personalisation trained on a profile therefore learns a mixture, retention regressions attribute one member's behaviour to another, and a per-account taste statistic describes a household composite. The practical consequence is that apparent personalisation wins can be device or context effects, so any identity-level claim needs a stated unit and an acknowledgement of what that unit actually aggregates.

Counting plays without a qualification threshold, or changing the threshold without restating history.

Playback arrives as heartbeats, so a play only exists once you decide what counts, and the common 30-second convention is not a neutral analytics choice: in music it is also the boundary at which a play becomes payable, which makes the warehouse definition a payout definition. The threshold interacts violently with content length, so a catalogue of three-minute tracks and one of forty-minute episodes move in opposite directions when you change it, and a skip-heavy surface can add plays while adding no hours. Any metric mixing pre-threshold and post-threshold counts, or pooling short-form and long-form on a per-stream basis, moves by double digits for reasons that have nothing to do with the product.

Explaining an aggregate move without decomposing the mix shift

Split the change in the aggregate into within-segment movement and movement in segment weights before you explain it. Every segment's rate can fall while the overall rate rises, purely because volume shifted toward segments that already had higher rates.

Interpreting a change before checking data quality and logging

Spend the first pass on row volume by day, null rates, duplicate keys, and whether the step change lands on a release or tracking-migration date. A discontinuity that coincides with a deploy is an instrumentation hypothesis before it is a behavioural one.

Questions & practice

Write a function to calculate the mean and standard deviation of a list of numbers.

Medium · statistics and probability

Approach

  1. Write down the assumption the method needs before you use the method.
  2. Sanity-check the answer against a simple bound or a simulated case.
  3. Translate the result into the decision it informs, in one plain sentence.

Follow-up

  • What sample size would you need to detect an effect half this size?
  • How would you explain this result to someone who does not know statistics?

Discuss a project where you applied machine learning algorithms.

Medium · machine learning and modelling

Approach

  1. Check what information would not exist at prediction time, and exclude it.
  2. Say how the offline result would be validated online before it is trusted.
  3. Set a baseline first, so any model has something honest to beat.

Follow-up

  • What would you monitor after launch to know the model is still valid?
  • How would you choose the decision threshold, and who owns that choice?

Attribute streams to prior impressions without merge_asof

Medium · pandas · joins · attribution

You have impressions (impression_id, profile_id, content_version_id, rendered_at, surface, slate_position, viewport_visible_ms) and streams (stream_id, profile_id, content_version_id, started_at). Attribute each stream to the most recent impression of the same profile and content version with rendered_at at or before started_at, a gap of at most 30 minutes, and viewport_visible_ms above zero; break ties on rendered_at by the smaller slate_position. pd.merge_asof and any groupby-apply over individual rows are off limits. Return streams with impression_id added, plus the attributed share by surface.

Approach

  1. Filter impressions to viewport_visible_ms above zero first. A row that never scrolled into view is not an exposure, and leaving it in lets an unseen impression win the as-of match over a seen one.
  2. Stack both frames into one long frame with a ts column and an is_stream flag, then sort by (profile_id, content_version_id, ts, is_stream, negative slate_position) so that at an identical timestamp impressions sort before the stream, and among tied impressions the smallest slate_position sorts last.
  3. Forward-fill the candidate impression_id and rendered_at with groupby(['profile_id','content_version_id']).ffill(). The grouping is what stops the fill leaking across profiles; a global ffill on a sorted frame is the usual wrong answer.
  4. Apply the 30-minute window as a post-filter on the filled candidate, and let a stream that fails it go unattributed rather than falling back to an older row. Every candidate precedes the stream, so the gap grows monotonically as you walk backwards: if the nearest in-view impression is more than 1800 seconds old, no earlier one is inside the window either, and null is the correct answer. The window cannot be pushed onto the impressions frame beforehand in any case, because "within 30 minutes" is defined against a particular stream's started_at and one impression is a candidate for many streams. Dropping impressions rendered more than 30 minutes before the earliest stream is a safe prune, but it is an optimisation, not the rule.
  5. Slice the stream rows back out and compute the attributed share by surface, stating that unattributed streams (resume, direct link, radio seed) are not a surface and belong in their own bucket.

Worked solution (30 min)

  1. imp = impressions[impressions.viewport_visible_ms > 0]; build an impression side with ts = rendered_at, is_stream = 0 and a sort helper neg_pos = -slate_position, and a stream side with ts = started_at, is_stream = 1, neg_pos = 0.
  2. Concatenate both, then sort_values(['profile_id','content_version_id','ts','is_stream','neg_pos']).
  3. Add cand_id and cand_ts columns carrying impression_id and rendered_at on impression rows and NaN on stream rows, then groupby(['profile_id','content_version_id'])[['cand_id','cand_ts']].ffill().
  4. Slice to is_stream == 1, compute gap = (ts - cand_ts) in seconds, and set cand_id to NA where the gap is null or above 1800.
  5. Merge cand_id back onto streams by stream_id, then group the attributed rows by the winning impression's surface for the share.

Expected result: streams with an impression_id column, row count unchanged, non-null only where an in-view impression of the same profile and content version exists within 1800 seconds before started_at, plus a per-surface share over a stated denominator of attributed streams rather than all streams.

Verify it yourself

  • Every attributed pair satisfies profile_id equality, content_version_id equality, and a gap between 0 and 1800 seconds. Assert all three rather than eyeballing the head.
  • On a 10,000-row sample the result matches pd.merge_asof(by=['profile_id','content_version_id'], tolerance=30 minutes, direction='backward', allow_exact_matches=True) once slate_position ties are made unique.
  • The output row count equals len(streams) exactly; a frame that grew matched several impressions to one stream.
  • Setting viewport_visible_ms to 0 on the winning impression of a known stream moves that stream to an earlier impression or to null, never leaves it unchanged.

Follow-up

  • The same item was rendered in two slates inside the window. Your tie-break picks one. What does that do to per-surface credit, and what would a fractional rule change?
  • How would you validate this against the impression_id already carried on fct_stream, and what would a systematic disagreement tell you?
  • At 2 million impressions and 500 thousand streams this fits in memory. What changes at 200 million?

How would you optimize a SQL query that is running slowly?

Medium · sql and data manipulation

Approach

  1. Say which table is the grain you start from, and join outward from it.
  2. State the window function and its partition and ordering out loud before writing it.
  3. Compute rates by summing numerator and denominator separately, never by averaging rates.

Follow-up

  • How does the query change if the join becomes one-to-many?
  • What breaks if events arrive late or out of order?

Write a Python script to clean and preprocess a dataset.

Medium · sql and data manipulation

Approach

  1. Handle the rows that do not match: a LEFT JOIN with a NULL check is usually the question.
  2. Check whether any join is one-to-many before aggregating, or the sums inflate.
  3. Compute rates by summing numerator and denominator separately, never by averaging rates.

Follow-up

  • How does the query change if the join becomes one-to-many?
  • How would you verify this result without re-running the same query?

Sessionise playback into listening sessions with an idle gap

Medium · window functions · gaps and islands · sessionisation

fct_stream has profile_id, stream_id, content_version_id, started_at, ended_at (nullable when the client stopped reporting) and played_seconds. Collapse consecutive streams by the same profile into listening sessions: a new session begins when a stream starts more than 30 minutes after the latest end reached by any of that profile's earlier streams. One profile can play on two devices at once, so streams overlap and 'the previous stream' is not well defined — the idle gap is measured against the running maximum end, not against the row immediately before. Treat a NULL ended_at as started_at plus played_seconds. Return one row per session with profile_id, session_start, session_end, stream_count, distinct content_version_id count and total played_seconds. Use window functions; a self-join on fct_stream is not an acceptable answer.

Approach

  1. In a first CTE compute effective_end = COALESCE(ended_at, started_at + played_seconds * INTERVAL '1 second'). This both fills the abandoned-client case and bounds it, since played_seconds is advancing playback only and cannot run past the true end.
  2. Take prev_max_end = MAX(effective_end) OVER (PARTITION BY profile_id ORDER BY started_at, stream_id ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), and flag is_new_session = (prev_max_end IS NULL OR started_at > prev_max_end + INTERVAL '30 minutes'). LAG(effective_end) is the reflex here and it is wrong under concurrent devices: with A 10:00-12:00, B 10:05-10:10 and C starting 10:45, LAG hands C the end of B at 10:10, so C opens a new session 35 minutes into a session still running until 12:00. The NULL branch is not defensive noise either: the first row of every profile has no preceding frame and would otherwise fall outside any session.
  3. Number the sessions with SUM(is_new_session::int) OVER (PARTITION BY profile_id ORDER BY started_at, stream_id ROWS UNBOUNDED PRECEDING). A running count of boundaries is the gaps-and-islands device. Specify ROWS rather than letting the default RANGE frame apply, because RANGE lumps peer rows that share a started_at into one frame position — the same reason the ORDER BY carries stream_id as a tie-break.
  4. Group by (profile_id, session_number) and aggregate: MIN(started_at), MAX(effective_end), COUNT(*), COUNT(DISTINCT content_version_id), SUM(played_seconds).
  5. The running maximum makes the sessions disjoint in wall-clock time, but it does not make SUM(played_seconds) an elapsed-time figure: two devices playing at once contribute overlapping seconds, so the total can exceed session_end minus session_start. Report it as consumption and say which of the two the requester asked for.

Worked solution (30 min)

  1. Materialise the effective_end CTE and check for rows where effective_end < started_at, which means negative played_seconds or a clock-skewed ended_at and must be quarantined before anything else.
  2. Add the running MAX and the boundary flag; verify that exactly one row per profile has prev_max_end IS NULL and that it is flagged as a session start.
  3. Add the running SUM to number sessions; confirm the numbers are dense and begin at 1 within each profile.
  4. Aggregate and reconcile: SUM(stream_count) across all sessions must equal the input row count.
  5. Run the overlap fixture — one profile with A 10:00-12:00, B 10:05-10:10 and C at 10:45 — and confirm it collapses to one session. With LAG(effective_end) in place of the running MAX it splits into two, and the second session's start, 10:45, sits inside the first session's span.

Expected result: One row per (profile_id, session_number). Session numbers are dense from 1 within each profile, SUM(stream_count) equals the input row count exactly, and a profile's sessions are disjoint in wall-clock time: each session_start exceeds the previous session's session_end by more than 30 minutes.

Verify it yourself

  • Input row count equals SUM(stream_count); a shortfall means rows fell out of the window partition or the NULL branch of the boundary flag.
  • For any profile, each session's session_start exceeds the previous session's session_end by more than 30 minutes. This holds only because the boundary is measured against the running maximum end — under LAG(effective_end) the overlap fixture above produces a session whose session_start, 10:45, precedes the prior session_end of 12:00.
  • Changing the gap from 30 to 60 minutes reduces the session count or leaves it unchanged, and never raises it.

Follow-up

  • Two devices on one profile play simultaneously. Is that one session or two, and what changes in the query for each answer?
  • Re-run at a 10-minute and a 60-minute idle gap. How far does session count move, and which number do you publish and defend?
  • Can SUM(played_seconds) for a session ever exceed session_end minus session_start, and what does it mean when it does?

You have a dataset with sales figures across various regions. How would you analyze it to identify trends?

Medium · product sense and metrics

Approach

  1. State what result would change your recommendation, so the answer is falsifiable.
  2. Fix the population and the time window before naming any metric.
  3. Name one primary metric, then the guardrail that stops it being gamed.

Follow-up

  • Which segment would you cut first, and what would that rule out?
  • How would you detect that the metric is being gamed rather than genuinely improving?

Given a scenario where product sales are declining, what steps would you take to diagnose the issue?

Medium · product sense and metrics

Approach

  1. Decompose the metric into the rates that drive it, and say which one you would check first.
  2. Fix the population and the time window before naming any metric.
  3. Name one primary metric, then the guardrail that stops it being gamed.

Follow-up

  • How would you detect that the metric is being gamed rather than genuinely improving?
  • What would you do if the primary metric and the guardrail moved in opposite directions?

Discuss the trade-offs between different data structures in terms of their performance.

Medium · product sense and metrics

Approach

  1. Restate the decision this analysis has to support, and who acts on the answer.
  2. Decompose the metric into the rates that drive it, and say which one you would check first.
  3. State what result would change your recommendation, so the answer is falsifiable.

Follow-up

  • What would you do if the primary metric and the guardrail moved in opposite directions?
  • How would you detect that the metric is being gamed rather than genuinely improving?

How do you prioritize tasks when faced with multiple deadlines?

Medium · product sense and metrics

Approach

  1. Fix the population and the time window before naming any metric.
  2. State what result would change your recommendation, so the answer is falsifiable.
  3. Decompose the metric into the rates that drive it, and say which one you would check first.

Follow-up

  • Which segment would you cut first, and what would that rule out?
  • What would you do if the primary metric and the guardrail moved in opposite directions?

Explain the difference between supervised and unsupervised learning.

Medium

Approach

  1. Work from the decision backwards to the evidence you would need.
  2. Clarify what is being asked and what a complete answer would contain.
  3. State your assumptions explicitly before working the problem.

Follow-up

  • How would you know your answer was wrong?
  • What assumption would you test first?

Value a catalogue removal with synthetic control

Hard · synthetic control · difference-in-differences · causal inference · rights windows

A non-exclusive licence covering 1,400 content_version rows expires on a fixed date in six territories and is renewed in the other 48. Nothing else about the product changes that week. Using fct_stream, dim_content_version.available_territories and fct_subscription_period, estimate the removal's effect on qualified hours per active account-week and on 90-day voluntary churn. No randomisation is available and the six territories were chosen by the counterparty. Specify the estimator, the control group, the assumption you need and how you would test it.

Approach

  1. Set the panel: one row per territory-week of qualified hours per active account-week, at least 24 pre-expiry weeks, with the six affected territories treated and the 48 renewed ones as the donor pool.
  2. Reject plain difference-in-differences as the primary estimator. The six territories were selected by a counterparty, not at random, and territory-level hours differ by an order of magnitude, so an unweighted parallel-trends claim is not credible. Fit it as a secondary event study with leads and lags instead.
  3. Fit a synthetic control per treated territory, with donor weights chosen to match the pre-period outcome path plus the predictors that matter here: plan-tier mix and the pre-period share of hours coming from the affected rights_holder_id. Aggregate the six gaps afterwards rather than pooling territories first.
  4. Clean the donor pool before fitting. Exclude any donor where the same licence or another material rights window changes inside the event window; a contaminated donor is the failure mode that quietly halves the estimate.
  5. Do inference by placebo-in-space: refit the synthetic control treating each donor as if it were treated, rank the true post-to-pre RMSPE ratio within that distribution, and report the exact p-value, whose floor with 48 donors is 1/49 = 0.020.
  6. Split the answer into mechanical and behavioural. The removed versions' pre-period share of qualified hours is the gross loss, the synthetic-control gap is the net, and one minus net over gross is the substitution rate. Read churn only on monthly-billing accounts and declare the annual cohort censored, since an annual account has no renewal decision inside a 90-day window.

Worked solution (45 min)

  1. Build the territory-week panel from fct_stream using play_territory as the unit, restricted to is_qualified = true, spanning 24 weeks before and 12 weeks after the expiry date, with the active-account denominator computed identically in every territory.
  2. Compute gross exposure per treated territory: the share of pre-period qualified hours on content_version rows whose available_territories included that territory and whose rights_expires_at is the expiry date.
  3. Fit the synthetic control per treated territory on the pre-period and read the post-period gap; also fit a two-way fixed-effects event study with leads from week minus 12 to minus 1 and lags to plus 12.
  4. Run 48 placebo-in-space fits and rank the treated post-to-pre RMSPE ratios against that distribution.
  5. Repeat the whole procedure with a placebo expiry date 12 weeks earlier, where the estimated effect must be zero.
  6. Build the churn read separately on monthly-billing accounts with period_end_ts inside the 90-day window and renewal_outcome = 'cancelled_voluntary', computed only after the dunning horizon has closed.

Expected result: Per-territory synthetic-control gaps in qualified hours per active account-week, aggregated across the six treated territories, with placebo-in-space p-values whose floor is 1/49 = 0.020, and an event-study DiD as the secondary specification whose pre-expiry leads are jointly indistinguishable from zero. The reported pair is the net hours change and the substitution rate against the gross loss, with voluntary churn read on monthly-billing accounts only and the annual cohort declared censored.

Verify it yourself

  • Pre-period RMSPE must be small relative to the post-period gap. A post-to-pre RMSPE ratio below about 2 is not a result, whatever the point estimate looks like.
  • Donor weights should not concentrate on a single territory; a synthetic control that is 90 percent one donor is a matched-pair comparison with extra machinery.
  • The event-study leads from week minus 12 to minus 1 must be jointly insignificant. A visible pre-trend invalidates the DiD specification regardless of what the synthetic control reports.
  • The placebo expiry 12 weeks earlier must produce a gap sitting inside the placebo distribution; a significant placebo means the fit is chasing seasonality.

Follow-up

  • Two of the six territories account for 80 percent of the affected hours. Does aggregating the six gaps into one number still mean anything?
  • The pre-period fit is poor for one territory. Do you drop it, report it, or widen the donor pool, and what does each choice do to the placebo inference?
  • Management wants a renewal decision for next year. Which number do you hand them, and what does it assume about a different catalogue and a different competitive set?

Qualified streams drop the week a client shipped

Easy · instrumentation · release-debugging · segmentation

Qualified streams in fct_stream (is_qualified, played_seconds, max_position_seconds, app_version, device_type, started_at, ended_at, end_reason) fell 11% on phone in the seven days after version 8.4.0 reached staged rollout. Smart TV and desktop are flat. The mobile team says nothing in the release touched playback. Using fct_stream and dim_profile only, decide within one day whether engagement fell or whether the client stopped reporting playback correctly, and say what evidence settles it. Deliverable: a one-page finding with the decomposition and a recommendation on whether to halt the rollout.

Approach

  1. Split the 11% into stream count and per-stream played_seconds. Cut phone streams by app_version into 8.3.x and 8.4.0 on the same dates and compare starts per active profile-day against played_seconds per start. A real engagement fall moves starts; a telemetry fall moves played_seconds per start while starts hold.
  2. Check the accrual mechanics that change silently: share of rows with ended_at null, share with end_reason in ('unknown','app_kill'), and the played_seconds distribution in 5-second buckets from 0 to 60. A heartbeat regression piles rows up just under 30 seconds and raises the null-ended_at share, because is_qualified is a threshold on a counter rather than on the fact of playback.
  3. Use the staged rollout as the control, but use it correctly. Within phone and within the same dates, prefer profiles that have streams on both versions so each profile is its own before-and-after. Rollout order is not random — early upgraders skew toward frequent users — so report the within-profile change and treat the between-version gap as an upper bound.
  4. Compare max_position_seconds per start against played_seconds per start. max_position_seconds is the furthest playhead reached and does not depend on how time is accrued, so if the playhead still travels the same distance while played_seconds falls, playback happened and the counter is wrong. That comparison is decisive; the others are only suggestive.
  5. Recommend on the basis of which number moved. Halt the rollout for an engagement regression only if starts fell. If accrual broke, the halt is a data-integrity call, the affected days need a caveat on every series built on played_seconds, and the music payout path that reads is_qualified has to be told the same day.

Follow-up

  • If played_seconds under-reports on 8.4.0, how would you restate the qualified-stream series across the rollout window rather than blanking it?
  • Staged rollout is ordered by store and device, not randomised. What does that do to a between-version estimate, and how would you bound the bias?
  • The per-stream payout boundary is the same 30-second threshold. Who needs to know, and before what deadline?

Suggested practice checklist

For someone who has spent the last year in notebooks, dashboards or modelling work and has not written raw SQL under time pressure. The first four days rebuild query fluency against a fixture you control and can verify by hand; the last three attach that fluency to the rest of the loop.

Day 1 — Build a fixture you can check answers against

  • Create a local Postgres or SQLite database with four tables (users, sessions, events, orders) holding roughly 200 rows you generated yourself, so you know the contents well enough to predict every result.
  • Deliberately seed the cases that break queries: a user with no sessions, a session with no events, two orders sharing a timestamp, a NULL in one join key, and one duplicated user row.
  • Before writing any SQL, hand-compute five answers on paper (how many users placed at least one order, median orders per ordering user, and three others) and save them as the ground truth for the week.

Deliverable: A one-command seed script plus a text file of five hand-computed answers to grade every later query against.

Day 2 — Joins, filters and NULL semantics

  • Answer "which users have no orders" three ways (LEFT JOIN with IS NULL, NOT EXISTS, NOT IN) and confirm that the NOT IN version returns zero rows once the subquery contains a NULL, because the comparison is never TRUE.
  • Reproduce the LEFT JOIN that silently collapses to an inner join by putting a right-table predicate in WHERE, then fix it by moving the predicate into the ON clause, and record both row counts.
  • Create a fan-out bug on purpose by joining orders to order_items and summing the order total, then correct it with a pre-aggregated subquery and explain in one line which table changed the grain.

Deliverable: One annotated .sql file holding the three join traps, each with the wrong result and the corrected result side by side.

Day 3 — Window functions and frames

  • Write three window queries against the fixture: a running order total per user, the rank of each order within its user by value, and the day gap to that user's previous order, then check each against the day-one ground truth.
  • Run ROW_NUMBER, RANK and DENSE_RANK over a column containing ties, print all three side by side, and write one sentence on when each is the correct choice.
  • Switch one query from the default frame (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which is what you get when ORDER BY is present and no frame is written) to ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, and explain why the output differs only when the ORDER BY column has duplicates.

Deliverable: Three verified window queries plus a short note explaining the RANGE versus ROWS difference in your own words.

Day 4 — The four analytical query patterns

  • Write a monthly retention grid: first order month per user, then months-since-first as the column, and verify that month zero equals the cohort size exactly.
  • Sessionize the events table under a 30-minute inactivity rule using LAG plus a cumulative sum over a new-session flag.
  • Build a four-step funnel that counts distinct users rather than events at each step, and state the rule you applied to a user who reaches step three without ever logging step two.

Deliverable: One file with the retention, sessionization and funnel patterns, each carrying a one-line note on the assumption it bakes in.

Day 5 — Write SQL the way you will have to write it live

  • Set a 12-minute timer and solve three medium prompts in a plain editor with no execution and no autocomplete, then run them and tally syntax errors separately from logic errors.
  • Narrate one solution aloud while writing it, stating the grain of each intermediate result (one row per user, one row per user-day) before you type its body.
  • Rewrite your slowest solution as a CTE chain where every CTE name states its grain, and time yourself re-solving it from blank.

Deliverable: A recording of one narrated solution plus an error tally that separates syntax from logic.

Day 6 — One day for everything that is not SQL

  • Write the preconditions of the two-sample t-test from memory, then check them: independent observations, and a difference in means whose sampling distribution is approximately normal, which at large sample sizes follows from the central limit theorem rather than from normality of the raw values.
  • Write the difference between an odds ratio from logistic regression and a relative risk, and state the condition under which the two are close (low outcome prevalence).
  • Prepare a 90-second answer to "how would you know this model is any good" that names the metric, the baseline you would beat, and the cost of the errors you care about.

Deliverable: One page of notes covering test preconditions, the odds-ratio caveat and the model-quality answer.

Day 7 — Full loop rehearsal

  • Run a 45-minute mock with someone willing to interrupt: 20 minutes of SQL, 15 minutes defining a metric, 10 minutes on a past project.
  • Re-solve from blank the two queries you were slowest on this week and compare the times against day five.
  • Write a five-line answer to "walk me through a project" that puts a number in the first sentence and names the decision the work changed.

Deliverable: Mock feedback notes plus a timed project narrative you can deliver without reading it.

Ownership when the result is inconvenient

Sometimes the honest read is that the initiative did not work, and the person who commissioned the analysis was hoping otherwise. Interviewers want to know whether you softened it. Prepare the case where you delivered an unwelcome result, how you presented the uncertainty without hiding behind it, and what the team did next.

What motivates you to excel in your work as a Data Scientist?

Medium · behavioural and stakeholder questions

Approach

  1. Name the disagreement or constraint, and how you resolved it with evidence.
  2. Quantify the outcome, including what you would not claim credit for.
  3. State the situation in two sentences and spend the rest on your reasoning.

Follow-up

  • How did you know the outcome was caused by your change?
  • What would you do differently if you ran that project again?

Disagree with a product manager about a completion metric

Medium · disagreement · metric design · influence

A product manager proposes making duration-normalised completion rate the team's primary metric for the quarter, and wants a plain global unweighted version "so it's simple enough to put on a wall." You expect the unweighted version to move several points on catalogue mix alone, with no change in how satisfying anything was. You have one meeting, they own the roadmap, and you will work with them for years. Deliverable: the argument, the evidence you bring, and the fallback you accept if they still want the simple version.

Approach

  1. The probe is whether you can lose the decision without damaging either the metric or the relationship. Bring the failure already reproduced: recompute the unweighted global rate over the last two quarters and point at the weeks it moved several points where the only input that changed was which content people played.
  2. Decompose the variance instead of objecting. Attribute the movement in the unweighted rate to between-cell mix — (content_type, duration decile) — versus within-cell movement. A decomposition is an argument; an assertion that mix matters is a preference.
  3. Concede the real cost honestly: the mix-weighted version is harder to explain and harder to recompute. Bring the mitigation rather than dismissing the objection — weights fixed from a stated reference month, published once, so anyone can reproduce the number without redoing the weighting.
  4. Offer the fallback deliberately and in advance: ship both, with the unweighted rate labelled a diagnostic and the weighted one as the decision metric, and pre-agree the one observation that would mean the simple version has misled the team.
  5. Leave the decision with them, with the consequence written down before it happens. That is what makes a later correction a shared prediction coming true rather than a retrospective argument about who was right.

Follow-up

  • They ship the simple version and it moves four points during a heavy release week. How do you raise it without saying you told them so?
  • Is there a case where the unweighted rate is genuinely the right primary metric?
  • Suppose you are wrong and the mix effect turns out to be small. What would you change about how you argued this?

Defend a flat result on a flagship discovery launch

Medium · defending findings · statistical power · experiment readout

A new ranker ran to 50 percent of accounts for four weeks. Your read: qualified hours per active account-week is +0.4 percent with a 95 percent interval of [-0.9 percent, +1.7 percent]. Meanwhile the share of qualified streams with start_source = 'algorithmic_slate' rose six points and the share with start_source = 'search' fell five. The team reads the slate shift as the win and wants to ship. The launch review is Friday. Deliverable: a five-minute verbal position and the one table you put on the screen.

Approach

  1. The probe is whether you can hold a position under social pressure without overclaiming in the other direction. Separate the two claims explicitly: the start-source shift is well measured and real; the hours effect the launch was justified on is not distinguishable from zero at this sample size. Those are different statements and only one is contested.
  2. Do the power arithmetic before the meeting, using the observed per-account variance on a right-skewed hours metric. Arrive able to say "at this n we could not have detected less than X percent", which is a fact, instead of "it didn't work", which is an opinion the room can simply disagree with.
  3. Show the substitution as arithmetic, not interpretation: decompose treatment hours by start_source and show slate hours rising by approximately what search hours fell. A start-source mix shift with no change in total is exactly what displacement looks like, and the decomposition sums to the total so there is nothing to argue about.
  4. Refuse the symmetric overclaim. The interval's upper bound is commercially meaningful, so "the ranker does nothing" is not supportable either; say that out loud, because it is what makes the rest of your position credible.
  5. Offer the falsifiable next step with a price: a longer read powered for the hours effect, or CUPED on a pre-period consumption covariate measured before assignment, with the required account-weeks stated. Put the disagreement on the pre-registered metric, not on the team's judgement.

Follow-up

  • The PM argues start-source mix is a leading indicator of retention. How would you test that claim, and how long would it take?
  • Suppose the interval had been [+0.1 percent, +1.9 percent]. Does your position change, and by how much?
  • The decision goes against your read and the ranker ships. What do you do on Monday?