Interview guides · Grainger
Grainger Data Scientist Interview Questions & Guide 2026
Data scientist prep for Grainger candidates: drill A/B design, power and minimum detectable effect on original exercises.
The 60-second brief
As a Data Scientist at Grainger, you will play a pivotal role in leveraging data to drive strategic decisions and improve operational efficiencies. This position is crucial for developing analytical models that influence product offerings, enhance customer experiences, and optimize supply chain processes. Your work not only impacts the internal workings of the company but also has a direct effect on the products and services provided to millions of customers across various sectors.
Allocate prep to your weakest link rather than your favourite topic. Of the three things that usually gate the outcome (SQL that is correct under messy joins, sound reasoning about experiments, and structured framing of an open-ended problem), candidates tend to over-invest in modelling theory and under-invest in framing.
Focus areas
- Size an experiment before anyone launches it
- Pick a randomisation unit that respects interference
- Turn a vague request into a measurable question
Overview
The complexity and scale of data at Grainger present both challenges and opportunities. You will be part of a team that employs advanced statistical methods, machine learning algorithms, and data visualization techniques to extract valuable insights from large datasets. This role is not just about crunching numbers; it's about storytelling through data, translating complex analyses into actionable strategies that align with business goals. Whether you are enhancing inventory management systems or predicting customer needs, your contributions will be integral to Grainger's mission of making it easy for customers to get the products they need when they need them.
The Grainger interview process
Grainger candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Screening Call
Most candidates lose this call inside the first two minutes, during the walkthrough of their own background. The account runs chronologically, sits at the level of tools and titles, and never arrives at a decision anyone could have disagreed with. Anchor on a problem instead of a timeline: what the team could not answer, what you did about it, what happened next. Ninety seconds is enough, and stopping on time leaves room for the half of the call that belongs to you. What you ask about how work gets prioritised signals your level more reliably than the walkthrough does.
What to demonstrate
- Whether your background summary has a shape (problem, decision, consequence) or is a chronological list of tools and employers
- Whether you can account for gaps, short stints and the reason you are looking, unprompted and without hedging
- The substance of the questions you ask back, which an experienced screener reads as a level signal
How to prepare
- Time your opening walkthrough against a clock. If it runs past two minutes, compress the earliest role into a single clause and spend the recovered time on the most recent one
- Write one honest sentence for every gap or short stint visible on your resume and offer it before being asked about it
- Prepare questions about how work arrives and gets prioritised: who writes the request, how often priorities change, and what happens to an analysis after it is delivered
Technical Interview
A handful of shapes account for most of what gets asked in this format: a ranking or deduplication inside groups, a running or rolling total, a period-over-period comparison, and a cohort tracked forward over time. Recognising the shape quickly is most of the speed here; deriving it from scratch while a clock runs is where the time goes. Know that a window function keeps every row while a GROUP BY collapses them, and know which one the question needs. If the exercise is in Python instead of SQL, the same shapes arrive as groupby with transform, shift and merge, and the same grain mistakes are available.
What to demonstrate
- Whether you reach the right construct without a detour, such as ROW_NUMBER over a partition to deduplicate instead of a self-join against a MAX subquery
- Whether you know what your window frame actually is, since adding ORDER BY inside OVER changes the default frame and silently changes a running total
- Whether the thing runs. A near-miss that throws an error scores below a plainer query that returns the right rows.
How to prepare
- Write each of the four shapes once from memory against a small schema and keep the working version somewhere you will reread it: dedupe with ROW_NUMBER, a running total, a month-over-month change with LAG, and a retention table
- Compute one running total twice on data with tied timestamps, once on the default frame and once with ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, and look at where the two disagree
- If Python is on the table, rebuild the dedupe and the running total with groupby and cumsum, then assert the two implementations return identical rows
Coding Challenges
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
Senior Team Interviews
An added round often puts you in front of someone outside the core hiring team: a partner engineer, a product owner, a domain expert, sometimes a more senior manager. The question they are really asking is not whether you can do the work but whether they would trust a number that came from you. That changes what a good answer looks like. Lead with what the decision cost and what it changed, keep the method available but not central, and be plain about the limits of your evidence. Overstating a result is the fastest way to lose this round.
What to demonstrate
- Whether you can explain a technical choice to someone who will never read your code, without either flattening it into nothing or hiding inside jargon
- Honesty about evidence strength: what the analysis establishes, what it only suggests, and what it cannot say at all
- How you take disagreement, specifically whether you update on a good objection, hold your position with reasons, or fold on contact
How to prepare
- Write the two-sentence version of your most technical project for a non-specialist, then check that neither sentence needs a method name to make sense.
- For one result you are proud of, write the strongest objection someone could raise and a response that concedes the part of it that is correct.
- Prepare one decision that turned out to be wrong: how you found out, what it cost, and what you changed afterwards. A senior cross-functional interviewer asks for this more often than a technical one does.
Where candidates lose points
Watching an experiment daily and stopping when it crosses significance
A fixed-sample test controls type I error at one pre-declared look. Checking repeatedly and stopping at the first p < 0.05 inflates the false positive rate to roughly 0.15 to 0.20 for ten looks, and it rises further with more frequent checks, because the p-value takes a random walk that will eventually dip below the threshold under the null. The usual defences are a fixed horizon declared before launch, group-sequential boundaries such as O'Brien-Fleming that spend alpha across a planned number of looks, or always-valid confidence sequences that are correct under continuous monitoring. Compounding it, the effect size reported conditional on having crossed the threshold is biased away from zero, and the bias is larger the lower the power was, so an underpowered test that 'won' typically overstates the lift it found.
Reading a pooled rate that moved because the mix moved, not because any behaviour changed
A pooled conversion rate is a weighted average, and a shift in the weights can move it in the opposite direction to every one of its parts. A paid campaign that brings low-converting traffic drops overall signup conversion even if desktop, mobile web and app conversion each rose that week, which is Simpson's paradox and it is the single most common cause of an inexplicable dashboard move. The discipline is to decompose before explaining: recompute the rate holding last period's segment weights fixed, and compare that counterfactual to the actual, so the mix effect and the rate effect are separated numerically rather than argued about. Segment on the dimensions that actually reweight, which in this domain are almost always device_type, referrer_channel, country and new versus returning.
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.
Analysing at a different unit than the one randomised
Say out loud what was randomised (user, device, account, cluster) and make the analysis unit match, or account for the clustering with cluster-robust standard errors, the delta method, or aggregation up to the randomised unit. Randomising users and then running a test over sessions understates variance and inflates the false-positive rate.
Questions & practice
Write a function to calculate the mean and standard deviation of a list of numbers.
Medium · statistics and probability
Approach
- Sanity-check the answer against a simple bound or a simulated case.
- Translate the result into the decision it informs, in one plain sentence.
- Write down the assumption the method needs before you use the method.
Follow-up
- Which assumption here is most likely to be violated in practice?
- How would you explain this result to someone who does not know statistics?
What are precision and recall, and why are they important in classification problems?
Medium · machine learning and modelling
Approach
- Check what information would not exist at prediction time, and exclude it.
- Set a baseline first, so any model has something honest to beat.
- Say how the offline result would be validated online before it is trusted.
Follow-up
- Where could label leakage enter this setup?
- What would you monitor after launch to know the model is still valid?
Explain how you would optimize a machine learning model using cross-validation.
Medium · machine learning and modelling
Approach
- Say how the offline result would be validated online before it is trusted.
- Check what information would not exist at prediction time, and exclude it.
- Pick an evaluation metric that matches the cost of each error type, not a default.
Follow-up
- Where could label leakage enter this setup?
- How would you choose the decision threshold, and who owns that choice?
Rebuild per-visitor ordering without groupby convenience methods
Easy · pandas · vectorisation · window logic
You have a DataFrame of 2 million fct_event rows with visitor_id, occurred_at_utc and event_id, unsorted and containing duplicate timestamps within a visitor. Produce three new columns: event_rank, the 1-based position of the event within its visitor ordered by occurred_at_utc; seconds_since_prev, the gap to that visitor's previous event, NULL for the first; and is_first_for_visitor. You may use sort_values, shift, cumsum, numpy and boolean masking. You may not use groupby.transform, groupby.apply, groupby.cumcount, groupby.rank or merge_asof. Break timestamp ties on event_id.
Approach
- Sort once by ['visitor_id', 'occurred_at_utc', 'event_id'] and reset the index. The whole exercise reduces to row arithmetic on a sorted frame, and the tiebreak on event_id is what makes the result reproducible across runs.
- Mark visitor boundaries with is_first = df['visitor_id'].ne(df['visitor_id'].shift()). This is the single fact every other column derives from.
- Compute seconds_since_prev as the diff of the timestamp column, then overwrite it with NaT/NaN wherever is_first is True. The shift crosses the boundary between visitors and will otherwise hand the first row of each visitor the last event of the previous one.
- Build event_rank from a running counter that resets at boundaries: take a global cumulative position (np.arange(len(df))) and subtract, per row, the global position at which that visitor started. Get the start position by forward-filling the positions where is_first is True, which is a cumsum-free reset and is O(n).
- Verify against the forbidden method once, as a test rather than as the implementation, and confirm the two agree on every row.
Worked solution (20 min)
- Sort on the three-key tuple and reset_index(drop=True).
- Compute is_first via .ne(.shift()), which is True for row 0 because the shifted value is NaN.
- pos = np.arange(len(df)); start = pd.Series(np.where(is_first, pos, np.nan)).ffill(); event_rank = (pos - start + 1).astype(int).
- gap = df['occurred_at_utc'].diff().dt.total_seconds(); gap[is_first] = np.nan.
- Assert event_rank equals df.groupby('visitor_id').cumcount() + 1 on the sorted frame.
Expected result: Three columns on the sorted frame: event_rank starting at 1 for every visitor and increasing by 1 with no gaps, seconds_since_prev null exactly where is_first_for_visitor is True, and is_first_for_visitor summing to df['visitor_id'].nunique().
Verify it yourself
- is_first_for_visitor.sum() equals the number of distinct visitor_id values.
- seconds_since_prev.isna().sum() equals that same number.
- event_rank.max() per visitor equals that visitor's row count, and event_rank is non-negative everywhere.
- A visitor with two events at the identical timestamp gets ranks 1 and 2 with a gap of 0.0, ordered by event_id.
Follow-up
- The frame does not fit in memory. How does your approach change if you can only process one visitor-partitioned chunk at a time?
- occurred_at_utc is client-supplied and sometimes runs backwards within a visitor. Does your seconds_since_prev go negative, and should it?
- How would you extend this to reset the counter at every change of surface as well as visitor?
Weekly visit-to-signup conversion split by acquisition channel
Easy · funnel · conversion · aggregation
From fct_session (session_id, visitor_id, started_at_utc, referrer_channel, is_bot_flagged, consent_state) and fct_event (visitor_id, occurred_at_utc, event_name), compute visit-to-signup conversion for one ISO week, split by channel. session_id is the unique key of fct_session. Denominator: distinct visitor_id with a session starting in the week, is_bot_flagged = FALSE and consent_state <> 'denied'. Numerator: those visitors with a 'signup_completed' event in the same week. Label each visitor with the referrer_channel of their first session in the window. Return channel, visitors, signups and rate, plus one all-channel total row.
Approach
- Build a visitor spine that is one row per visitor: filter sessions to the week, drop is_bot_flagged and consent_state = 'denied', then take the first session per visitor with ROW_NUMBER() OVER (PARTITION BY visitor_id ORDER BY started_at_utc, session_id) = 1 to carry the channel label. Collapsing to one row here is what makes the channel buckets mutually exclusive and the totals additive.
- session_id is the unique key, so that ordering is total and the label is reproducible. If the table carried no unique key you would have to write an explicit tie rule instead, because two sessions on different channels at the identical timestamp would otherwise label the visitor differently between runs.
- Attach the outcome as a semi-join (EXISTS on a signup_completed event for that visitor inside the same week) rather than a join to the event table, so a visitor who fires the event twice does not count twice and inflate the numerator past the denominator.
- Aggregate with COUNT() as visitors and COUNT() FILTER (WHERE signed_up) as signups, and compute the rate as signups::numeric / NULLIF(visitors, 0) so an empty channel returns NULL rather than a division error.
- Produce the total with GROUP BY GROUPING SETS ((channel), ()), which re-sums numerator and denominator for the total row. Averaging the channel rates gives a different and wrong number whenever channel volumes differ, which they always do.
- Verify the spine before trusting the output: COUNT(*) must equal COUNT(DISTINCT visitor_id), and the per-channel visitor counts must sum to the total row.
Follow-up
- The denominator is distinct visitors. If a browser release shortens cookie lifetime, what happens to this rate, and how would you tell that apart from a genuine drop?
- A visitor's first session is direct and their signup session is paid search. Your label says direct. When is that the wrong answer for the decision being made?
- How do you roll four weeks into a month, and why is averaging the four weekly rates wrong?
Pair flow starts to completions within a thirty-minute bound
Medium · event-streams · conditional-aggregation · flow-funnel
fct_event holds event_id, flow_id, flow_instance_id, event_name, occurred_at_utc, surface, app_version. Using only rows where event_name is 'flow_started', 'flow_completed' or 'error_shown', compute the daily core-flow completion rate by surface and app_version. Numerator: distinct flow_instance_id whose flow_completed occurs within 30 minutes of its flow_started with no error_shown for the same instance in between. Denominator: distinct flow_instance_id with a flow_started that day. Report rows with a NULL flow_instance_id as a separate coverage count, not inside either side.
Approach
- Collapse the stream to one row per flow_instance_id using conditional aggregation: MIN(occurred_at_utc) FILTER (WHERE event_name = 'flow_started') AS started, MIN(...) FILTER (WHERE event_name = 'flow_completed') AS completed, MIN(...) FILTER (WHERE event_name = 'error_shown') AS first_error. This makes the 'first' semantics explicit and avoids a self-join entirely.
- Apply the predicates on that single row: completed IS NOT NULL AND completed <= started + interval '30 minutes' AND (first_error IS NULL OR first_error > completed). Note the last clause encodes 'in between' literally, so an error surfaced after a successful completion does not disqualify the attempt.
- Bucket by started::date, surface and app_version. Splitting by app_version is not decoration: a completion-rate regression is nearly always confined to one client build, and a pooled daily rate hides it behind the installed base.
- Count NULL flow_instance_id rows as a separate coverage figure. They cannot be attributed to an attempt at all, so they belong in neither numerator nor denominator, and their share tells you how much of the rate is unmeasurable.
- For any roll-up to week or to all surfaces, re-sum the numerator and denominator rather than averaging the daily rates.
Worked solution (25 min)
- Write the per-instance collapse CTE and confirm COUNT(*) equals COUNT(DISTINCT flow_instance_id).
- Add the three predicates one at a time, recording how many instances each removes.
- Aggregate by day, surface and app_version with FILTER on the clean-completion flag.
- Compute the NULL-instance coverage count as a separate select and report it alongside.
- Pick the worst day-surface-version cell and pull ten raw instances to confirm the disqualification reason.
Expected result: One row per (day, surface, app_version) with started, completed_clean and a rate bounded by 0 and 1, plus a scalar coverage count of events carrying a NULL flow_instance_id. Instances completing after 30 minutes appear in the denominator only.
Verify it yourself
- No rate exceeds 1.
- COUNT(*) equals COUNT(DISTINCT flow_instance_id) in the collapsed CTE.
- Total started across cells equals the count of distinct flow_instance_id values carrying a flow_started event in the window.
- Instances with an error strictly between start and completion are excluded from the numerator but present in the denominator.
Follow-up
- occurred_at_utc is client-supplied. What do you do with an instance whose completion timestamp precedes its start?
- flow_instance_id is missing on 4 percent of starts for one app_version. What can and cannot you conclude about that build?
- An instance starts at 23:55 and completes at 00:04. Which day owns it, and what does the alternative do to the daily series?
How do you prioritize your projects when faced with tight deadlines?
Medium · product sense and metrics
Approach
- Fix the population and the time window before naming any metric.
- Restate the decision this analysis has to support, and who acts on the answer.
- Name one primary metric, then the guardrail that stops it being gamed.
Follow-up
- What would you do if the primary metric and the guardrail moved in opposite directions?
- Which segment would you cut first, and what would that rule out?
Given a dataset with missing values, what strategies would you implement to handle them?
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- Restate the decision this analysis has to support, and who acts on the answer.
- 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?
- Which segment would you cut first, and what would that rule out?
How would you approach a project to improve sales forecasting accuracy?
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- Restate the decision this analysis has to support, and who acts on the answer.
- State what result would change your recommendation, so the answer is falsifiable.
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?
Walk us through how you would analyze customer churn data.
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- Name one primary metric, then the guardrail that stops it being gamed.
- Restate the decision this analysis has to support, and who acts on the answer.
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?
Design an experiment to test the effectiveness of a new marketing strategy.
Medium · a/b testing and experimentation
Approach
- Name the guardrails that would stop a launch even on a positive primary result.
- Decide the analysis before seeing data, including how long it runs and when you look.
- Name the randomisation unit first; it decides the variance and what the test can detect.
Follow-up
- What would you do if you could not randomise at all?
- How would you handle interference between treated and control units?
Explain the differences between supervised and unsupervised learning.
Medium
Approach
- Clarify what is being asked and what a complete answer would contain.
- Work from the decision backwards to the evidence you would need.
- Say what you would check first and why it is the highest-information step.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Metric tree for a seat-expansion push on seat-based plans
Medium · revenue · seats · leading indicators
Sales wants a push to add seats to existing accounts. Tables: dim_account (account_id, seats_licensed, seats_assigned, lifecycle_status, current_plan_tier), fct_subscription_period (subscription_id, account_id, mrr_cents_constant_fx, seats_billed, period_start_utc, period_end_utc, change_reason), and fct_event (is_core_action, user_id, account_id, occurred_at_utc). Build the metric tree from net revenue retention down to the metric the push team is graded on, name the primary, and give the guardrail that fires before the damage shows up in NRR. Deliverable: the tree, the primary metric, and the lead-time argument for the guardrail.
Approach
- Grade the team at the grain it controls. NRR is a trailing twelve-month revenue-weighted ratio that the push cannot move inside a quarter and that also moves for reasons the push does not touch, so it belongs at the top of the tree and nowhere near the target.
- Define the primary as a delta rather than a count: the sum of period-over-period mrr_cents_constant_fx changes on rows with change_reason = 'seat_change' and a positive seats_billed delta. A COUNT of seat_change rows rewards churn of seats as much as growth.
- Name the failure the target creates: seats sold and never assigned book revenue now and reappear as a seat reduction at the next renewal, six to twelve months later, comfortably outside the window the team is measured in.
- Pick licensed-seat utilisation as the guardrail precisely because of that lag — distinct user_id with an is_core_action event in the trailing 28 days over seats_licensed, MRR-weighted across lifecycle_status = 'active' accounts — and read it against each account's renewal date rather than the calendar.
- Close the horizon gap with a rule instead of a hope: the push books expansion MRR immediately but retains credit only for accounts still above the utilisation threshold at the following renewal.
Worked solution (30 min)
- Build the tree downward: NRR at twelve months, then expansion plus contraction plus churned MRR, then seat_change deltas and tier changes, then seats billed per account, then licensed-seat utilisation.
- Write the primary as a period-over-period MRR delta restricted to positive seat changes, in constant FX, on subscription_id.
- Write the guardrail with its weighting and its read points: 30 and 60 days before each account's renewal date.
- State the lead time in months: utilisation moves within one 28-day window of a seat sale, while the seat reduction it predicts reaches NRR only at the next renewal.
- Write the credit rule that ties the two horizons together.
Expected result: A tree with expansion MRR as the primary, utilisation as the guardrail with its lead time stated in months, and a credit rule that makes the push team carry the renewal-horizon consequence.
Verify it yourself
- Summing mrr_cents_constant_fx across all period rows in a month double-counts any account with a mid-period change; the monthly figure must be built from period boundaries rather than a naive SUM over rows.
- seats_assigned can exceed seats_licensed during a grace period, so a utilisation computed on assigned seats can exceed 1. The denominator is seats_licensed.
- The MRR-weighted and unweighted utilisation means should differ substantially. If they do not, single-seat accounts are probably being dropped somewhere in the aggregation.
Follow-up
- How do you separate seat expansion from price and tier expansion inside NRR without double-counting either?
- An account buys 40 seats and assigns 6 during a phased rollout. Is that expansion, and what would change your answer?
- What does the utilisation threshold do to a genuinely growing customer three weeks into a rollout?
Attribute a flow-completion drop to one client build
Medium · release-regression · instrumentation · staged-rollout
Core-flow completion rate on surface = 'ios', defined as distinct flow_instance_id reaching 'flow_completed' within 30 minutes of 'flow_started' with no 'error_shown' on the same id in between, fell from 78% to 71% over four days. A new iOS build began a staged rollout on day one. Using fct_event columns flow_id, flow_instance_id, app_version, event_name, user_id and occurred_at_utc, produce the completion rate per build per day, size the loss in absolute completions, and say whether the fix is a rollback. Treat the possibility that the instrumentation changed rather than the flow.
Approach
- Cut the rate by app_version and day, each build against its own denominator. A pooled series during a staged rollout is a weighted average whose weights move daily, so it declines in proportion to the rollout share even when the old build is perfectly flat; confirming that proportionality is itself the evidence the regression is build-specific rather than environmental.
- Test the denominator before believing the rate. Compute flow_started events per distinct user_id on each build: if the new build re-mints flow_instance_id on retry, or fails to carry the id from start to completion, the denominator inflates and the numerator falls with nothing changing for the user.
- Normalise to a user-level outcome that is immune to the id question, namely completions per distinct user_id per day on each build. This is the number that says whether anyone actually failed to finish.
- Check the error path: pull error_shown counts and their properties for flow_instance_id values on the new build. A genuine regression produces errors; an instrumentation break produces missing completions with no corresponding error volume, and the two prescribe different fixes.
- Size the real loss as the per-user completion gap multiplied by daily users on the new build, and give the rollback recommendation conditional on the error evidence rather than on the rate.
Follow-up
- The rollout is at 45% and product wants to go to 100% tomorrow. What do you say, and what would you need by when?
- If the flow_instance_id is genuinely being re-minted, what is the correct historical treatment of the four affected days?
- How would you have caught this on day one instead of day four?
Suggested practice checklist
Instead of guessing where the week should go, day one measures it under a fixed rubric and allocates the remaining hours in proportion to the gaps. The method is deliberately rigid: the allocation is written down before any studying starts and is not renegotiated when a topic turns out to be unpleasant.
Day 1 — Diagnostic, scored before you study anything
- Sit a 100-minute timed diagnostic in four blocks: 30 minutes of SQL across three prompts, 25 minutes of short-answer statistics, 25 minutes on one modelling or case prompt, and 20 minutes delivering one behavioural story aloud.
- Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct, and 0 is stuck, grading the output rather than how the attempt felt.
- Allocate the hours for days two to five roughly in proportion to 3 minus the score in each block, write the allocation down, and commit to not revising it midweek.
Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.
Day 2 — Largest gap: find the boundary rather than the subject
- Break the weakest area into five named sub-skills (for query work: grain control, window frames, date arithmetic, set logic with NULLs, and reading a query plan) and rate each one, so the rest of the week targets a sub-skill instead of a subject.
- Solve three problems chosen to sit just above where the rating drops off, and for each write the first move you failed to make.
- Re-solve one of them from memory four hours later, on paper, with nothing open.
Deliverable: A five-item sub-skill map with the two blocking sub-skills circled.
Day 3 — Largest gap: drill the blocking sub-skill
- Do eight short repetitions of the same shape rather than eight different problems, so what you practise is the pattern and not the puzzle.
- Write the rule you now hold in one sentence, then test it against a case built to break it: a ranking function over a column with ties, or a two-sample test on observations that are obviously dependent.
- Have someone else read your one-sentence rule and find the precondition you left out.
Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.
Day 4 — Second gap, plus maintenance on your strongest area
- Run the same sub-skill map and boundary protocol on the second-largest gap, compressed into half the day.
- Spend 25 timed minutes on your strongest area to stop it decaying, choosing the hardest problem you can still finish rather than an easy warm-up.
- Compare how the two areas fail: whether you lose time on recall, on setup, or on arithmetic, because the fix differs for each.
Deliverable: A second sub-skill map plus a one-line diagnosis of how each area fails you.
Day 5 — The gap that is not a skill
- Record yourself answering one technical and one behavioural prompt, then count two things in the playback: how many seconds before your first clarifying question, and how many sentences you started without knowing where they ended.
- Rewrite your three most-used stock phrases into shorter versions, and practise saying "I do not know, here is how I would find out" without softening it into a guess.
- Deliver one answer again with a hard 90-second limit to force structure before detail.
Deliverable: Two recordings with a counted improvement in time-to-first-question.
Day 6 — Retest under day-one conditions
- Sit the same 100-minute diagnostic structure with new prompts of comparable difficulty and score it on the identical rubric.
- Compare block by block, and for any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
- Write which single block you would still lose the offer on.
Deliverable: A second scored rubric placed next to the first, with one named remaining risk.
Day 7 — Full loop under interview conditions
- Run a 60-minute mock covering the two blocks that moved least, with an interviewer instructed to interrupt and change direction.
- Write your recovery script for the moment you go blank: restate the question, state your assumption, name the first thing you would check.
- Reduce the week to the rule statements you wrote, each with its preconditions attached, then say every one of them out loud without reading it and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive being interrupted.
Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.
Getting an analysis acted on
Work that nobody used is a common and unflattering pattern in data careers, and interviewers probe for it. Have a story about an analysis that changed a decision, and be specific about how you got it in front of the person who could act. Also have one about work that went nowhere, with your reading of why.
Give an example of how you have contributed to a team’s success.
Medium · behavioural and stakeholder questions
Approach
- Pick a story where you drove the decision, not one where you observed it.
- State the situation in two sentences and spend the rest on your reasoning.
- Close with what you would do differently, concretely.
Follow-up
- What would you do differently if you ran that project again?
- What did you decide not to do, and why?
Turn an ambiguous onboarding question into a measurable metric
Easy · scoping · metric definition · stakeholder
Two days before a planning review, a director asks whether onboarding is working. You have dim_user (account_created_at_utc, signup_surface, is_internal), fct_event (is_core_action, flow_id, flow_instance_id, event_name, occurred_at_utc, received_at_utc) and fct_session. No further meeting with the director is possible before you start work. Deliver three clarifying questions you would send in writing, the metric you will compute in the meantime with its numerator, denominator, window and exclusions, and one sentence naming the question you are deliberately not answering.
Approach
- Recognise what is being probed: whether you convert a goal into a computable predicate without stalling for requirements or guessing in silence. Listing clarifying questions is the generic answer; shipping a defensible default alongside them is the strong one, because the review is in two days and it will happen with or without you.
- Infer the decision behind the request. A question about whether onboarding works, arriving before a planning cycle, usually means whether to staff it next quarter. That points at a rate with visible headroom over several cohorts, not at a descriptive dashboard.
- Write the three questions so that each one changes the SQL. Which population, all signups or only self-serve from dim_user.signup_surface. What counts as working, reaching a core action or completing the onboarding flow_id. Against what bar, last quarter's cohorts or a stated target.
- Propose the default explicitly: seven-day activation on weekly signup cohorts. Numerator, users with is_core_action = TRUE events on at least two distinct UTC dates inside [account_created_at_utc, account_created_at_utc + 7 days). Denominator, the signup cohort with is_internal = FALSE. Publish with an eight-day lag, and state that the two-distinct-days threshold is a frozen choice rather than a discovery.
- Name the exclusion in the same breath as the number. The series shows whether users activate; it does not establish that onboarding caused the level, which needs a staged rollout or an experiment.
Follow-up
- The director replies that they meant the onboarding flow specifically, not activation. What changes in the query and in the caveats?
- Your cohort metric needs an eight-day lag and the review is in two days. What do you present, and how do you label it?
- Two of your three questions come back unanswered. Which one do you refuse to proceed without?
Handle a request for numbers supporting a decision already made
Hard · integrity · framing · stakeholder
A senior leader has already decided to sunset a plan tier and asks you for the analysis showing it is the right call. Accounts on that tier carry 6% of MRR at constant FX and have the highest licensed-seat utilisation in the book. The leader's support matters to your next review cycle, and the decision is being presented in four days. Deliver what you produce, what you decline to produce, and the exact sentence you will say in the meeting where the number appears on a slide.
Approach
- Recognise what is being probed: whether you can find the legitimate request inside an illegitimate framing instead of either complying or refusing on principle. The generic answer promises to push back; the strong one produces something genuinely useful and states its limits in the room, without ambushing anybody.
- Separate the decision from the justification. Sunsetting the tier may be correct for reasons the data does not hold, such as support cost, roadmap surface area or sales motion. What you decline is a one-sided document. What you produce is the case read both ways, which also happens to be more useful to the leader.
- Build the symmetric analysis: MRR at risk at constant FX, the share of affected accounts with a plausible migration path given seats_licensed and billing_term, the recovery rate assumed for that migration and where it came from, and the downside case in which high-utilisation accounts treat the sunset as a reason to re-evaluate the vendor entirely.
- Surface the inconvenient fact privately and early. The highest seat utilisation in the book is a retention signal, and the leader should hold it before the room does, so they can incorporate it rather than be caught by it.
- Agree the meeting sentence in advance with the leader, so that nobody is surprised. Something to the effect that the tier is 6% of MRR and its accounts are the most heavily used in the book, and that the case for sunsetting rests on cost and focus rather than on revenue. That is true, it supports the decision on its real grounds, and it stops the deck claiming the numbers endorse it.
- Decide your own line before you need it: what you will not put your name to, and that the route if asked anyway is your own manager rather than a confrontation in the meeting.
Follow-up
- The deck circulates with your analysis included and the downside case removed. What do you do, and by when?
- What changes if the honest analysis says the sunset is clearly the wrong call?
- How do you write the same memo when the leader is your skip-level and the meeting is tomorrow?