Interview guides · Davidson Consulting España
Davidson Consulting España Data Scientist Interview Questions & Guide 2026
Diagnose a metric move for Davidson Consulting España data scientist prep: split by segment, rule out a logging change, separate mix shift from behaviour.
The 60-second brief
A Data Scientist at Davidson Consulting España plays a pivotal role as a strategic consultant, bridging the gap between complex analytical models and actionable business value for diverse clients. Unlike internal roles, this position demands a high degree of adaptability, as you will often be embedded within client teams to solve specific, high-stakes problems ranging from predictive maintenance to customer behavior modeling.
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
- Cluster inference at the account, not the engagement
- Correct late-entered timesheets before trending recent weeks
- Separate bookings, recognised revenue and collected cash
Overview
Your impact is measured by your ability to translate raw data into clear, technical, and commercial recommendations. Because Davidson Consulting España positions its consultants as experts who bring both technical rigor and professional autonomy, you are expected to navigate ambiguous client environments, manage stakeholder expectations, and deliver robust, scalable data solutions that drive real-world performance.
Where to focus your preparation
Treat the sections below as preparation areas and confirm the format with your recruiter.
Preparation focus
No round sequence has been reported for this company, so work the categories below and confirm the format with your recruiter.
What to demonstrate
- Breadth across SQL, experimentation and product reasoning
- Ability to state assumptions before choosing a method
How to prepare
- Drill the practice exercises below and time yourself
- Prepare three quantified stories about decisions you drove
Where candidates lose points
Using calendar hours, or a current rate card, as the denominator
Available hours is not days x 8. It must subtract approved leave and region-specific public holidays, scale by fte_fraction, and prorate for hire_date and termination_date inside the period; skipping this produces cross-region utilisation gaps that are purely holiday-calendar artifacts and a fake dip every time a cohort of new hires starts. Equally, bill and cost rates change over time, so joining today's dim_consultant row to last year's hours restates history. Use the rate snapshotted on the time entry, or join dim_consultant on work_date BETWEEN effective_from AND COALESCE(effective_to, 'infinity').
Computing days-to-pay or proposal cycle time over completed records only
At any snapshot date, invoices that have already been paid are disproportionately the fast ones, and proposals that already have a decision are disproportionately the quick ones. Averaging over the completed set alone biases both numbers downward, and the bias grows exactly when the business is deteriorating, because the slow cases are the ones still open. Unpaid and undecided records are right-censored; use Kaplan-Meier or a restricted mean up to a fixed horizon, and never fill paid_at with a placeholder.
Averaging per-user rates to produce a population rate
Decide which quantity you want: the mean of per-user ratios and the ratio of summed numerator to summed denominator are different estimands, and heavy users dominate one but not the other. For a ratio metric, aggregate numerator and denominator separately and use the delta method for its variance.
Reporting a mean for a heavy-tailed metric without saying what it hides
For spend, session length or items per order, a small fraction of units carries most of the total, so the mean has a wide standard error and one account can move it. Fix the handling before you see the result: cap or winsorise at a pre-declared percentile, and report the median or the share above a threshold next to the mean. Capping changes the estimand, so say which question the capped number answers, and check how much of any difference comes from the top 0.1 percent of units.
Questions & practice
Collapse time entries into contiguous staffing spells
Medium · sessionisation · gap and island · vectorisation
From approved delivery time entries (consultant_id, engagement_id, work_date, hours, charge_code, status, with engagement_id not null), build staffing spells. Per consultant and engagement, collapse weeks containing any logged hours into contiguous runs, where three or more consecutive zero-hour weeks end a spell. Output one row per spell with consultant_id, engagement_id, start_week, end_week, active_weeks, gap_weeks and total_hours. Consultants sit on several engagements at once, so spells from different engagements may overlap in time and must not be merged. No Python loop over rows.
Approach
- Aggregate to (consultant_id, engagement_id, week_start) with summed hours and keep only weeks with positive hours. The absent weeks are the signal, so materialising zeros here would destroy the thing you are detecting.
- Convert week_start into an integer week index, ((week_start - epoch_monday).dt.days // 7), so gap detection is integer subtraction rather than calendar arithmetic that breaks over month and year boundaries.
- Sort by [consultant_id, engagement_id, week_index], diff the week index inside each pair, and mark a spell start where the diff is null (first row of the pair) or greater than 3. A diff of 1 is adjacent weeks and a diff of 3 is two empty weeks, which the tolerance permits.
- Take spell_id = the cumulative sum of that boolean over the whole frame so ids are globally unique, then a single groupby on [consultant_id, engagement_id, spell_id] yields min and max week, active week count and summed hours; gap_weeks = (end - start + 1) - active_weeks.
- Do not deduplicate overlapping spells across engagements. A consultant on two engagements in the same week is the normal case, and that overlap is the fact any capacity or context-switching question needs.
Follow-up
- Re-run with a one-week and a four-week tolerance. What happens to the spell count, and which tolerance would you defend to a staffing lead?
- Using these spells, how would you measure how many engagements a consultant is split across in a given week, and why is that not just a count of rows?
Build a dense consultant-week hours spine with zero weeks
Easy · calendar spine · groupby · missing rows
You are given time_entries (time_entry_id, consultant_id, engagement_id, charge_code, work_date as datetime64, hours, is_billable, status) and a list of consultant_ids in scope plus a start and end date. Produce a DataFrame with exactly one row per consultant per Monday-anchored week in that range, with columns consultant_id, week_start, billable_hours, total_hours. Weeks in which a consultant logged nothing must appear as zeros rather than be absent. Count only rows with status == 'approved'. Do not loop over consultants and do not use resample.
Approach
- Anchor weeks arithmetically: week_start = work_date - pd.to_timedelta(work_date.dt.weekday, unit='D'). Avoid dt.isocalendar().week, whose numbers restart each January and do not sort across a year boundary.
- Construct the spine from the inputs, not from the data: pd.date_range(start, end, freq='W-MON') crossed with the in-scope consultant_ids via pd.MultiIndex.from_product, so its size is fixed before any aggregation runs.
- Aggregate approved entries once with a single groupby on [consultant_id, week_start], producing total hours and billable hours (hours.where(is_billable, 0).sum()) in the same pass.
- Left-merge the aggregate onto the spine, fillna(0.0) on both hour columns, and keep float dtypes so later ratios do not integer-divide.
- Assert the shape before returning: len(out) == n_consultants * n_weeks, and out.total_hours.sum() equals the sum of hours over the filtered input inside the window.
Follow-up
- Utilisation gets computed on this spine. What breaks for a consultant hired mid-window or terminated mid-window, and where would you fix it?
- How would you extend this to consultant x engagement x week without the spine exploding to a row count nobody can hold in memory?
Implement billable utilisation against a defended availability denominator
Medium · metric implementation · denominators · proration
Implement billable utilisation for one calendar month at consultant grain, then roll it up to practice_area. Inputs: time_entries (consultant_id, work_date, hours, is_billable, charge_code, status), consultants (consultant_id, practice_area, fte_fraction, home_region, is_billable_role, hire_date, termination_date) and holidays (region, holiday_date). Numerator is approved hours with is_billable true. Denominator is scheduled workdays in the month, clipped to [hire_date, termination_date] and net of that region's holidays, times 8 times fte_fraction, minus approved leave hours. Roll up as sum(numerator) / sum(denominator).
Approach
- Build the workday set per region first: business days in the month minus that region's holiday_dates. State the Monday-to-Friday assumption out loud, because a region with a different working week makes np.busday_count wrong rather than approximate.
- Prorate by clipping each consultant's interval to [max(month_start, hire_date), min(month_end, termination_date or month_end)] and counting workdays inside the clipped interval, so a mid-month start produces a smaller denominator instead of a fake dip in the ratio.
- Subtract approved leave hours (charge_code in leave_paid, leave_unpaid) only for leave falling on days already counted as workdays; leave logged on a public holiday would otherwise be subtracted twice and can push a denominator negative.
- Filter to is_billable_role, then drop consultant-months whose denominator is zero or negative (full-month leave, a hire on the last day) rather than emitting inf or NaN, and return the dropped count as part of the result.
- Roll up by summing numerators and denominators separately. Averaging per-person ratios gives a 0.2 FTE consultant the same weight as a full-time one and produces a practice number that no individual reconciles to.
Worked solution (35 min)
- Per region, workdays = np.busday_count(month_start, month_end + 1 day, holidays=region_holidays); per consultant, recompute on the clipped [start, end] interval so proration and holidays compose.
- leave = time_entries[status=='approved' & charge_code.isin(['leave_paid','leave_unpaid']) & work_date.isin(region workdays)].groupby('consultant_id').hours.sum().
- denominator = workdays_clipped * 8 * fte_fraction - leave, reindexed to the billable-role consultant list and filled with 0 leave where absent.
- numerator = time_entries[status=='approved' & is_billable].groupby('consultant_id').hours.sum(), reindexed and zero-filled.
- Drop rows with denominator <= 0 (counting them), compute utilisation_pct = 100 * numerator / denominator, then groupby practice_area and divide summed numerator by summed denominator.
Expected result: A consultant-month frame carrying numerator, denominator and utilisation_pct, a practice-area frame whose ratio equals sum(numerator)/sum(denominator), and an explicit count of consultant-months excluded for a non-positive denominator.
Verify it yourself
- A full-time consultant with no leave in a 21-workday month has denominator exactly 168.0.
- Every consultant's denominator is at most workdays * 8 * fte_fraction, with equality only when leave is zero.
- The practice-area ratio equals the denominator-weighted mean of the per-consultant ratios, and sits between their min and max.
- Summed numerators across practice areas equal total approved billable hours for in-scope consultants.
Follow-up
- Utilisation rose two points this month while realisation fell. Reconcile those two movements with one mechanism.
- A regional lead says their team looks four points below another region. What do you check before you answer, and in what order?
Build a client cohort revenue table from invoice lines
Easy · cohorts · date truncation · revenue recognition
dim_client holds client_id and first_engagement_date. fct_engagement maps engagement_id to client_id. fct_invoice_line holds engagement_id, line_type, period_start_date, amount_usd and status; recognised fees are lines with line_type IN ('fees','milestone','credit_note') and status <> 'draft', and credit notes carry negative amount_usd. Return one row per (cohort_month, months_since_first) giving distinct clients billed and net recognised fees. cohort_month is the month of first_engagement_date; months_since_first is the whole-month gap between period_start_date and first_engagement_date. Long format, no pivot. Decide explicitly what to do with clients whose first_engagement_date is NULL.
Approach
- Filter fct_invoice_line to recognised lines first (line_type, status <> 'draft'), and key recognition on period_start_date rather than issued_at or paid_at. Billing period, issue date and payment date are three different clocks, and mixing them produces a cohort table nobody can reconcile against finance.
- Join invoice lines to fct_engagement to reach client_id, since invoice lines carry engagement_id only, then to dim_client for first_engagement_date.
- Truncate both dates to month, then compute months_since_first as 12 * (year(bill_month) - year(cohort_month)) + (month(bill_month) - month(cohort_month)). Doing it on truncated months is what keeps a client billed on day 2 of the third month in bucket 2 instead of bucket 1.
- Aggregate to (cohort_month, months_since_first) with SUM(amount_usd) and COUNT(DISTINCT client_id), keeping credit_note lines inside the sum so the figure is net rather than gross.
- Handle NULL first_engagement_date as an explicit exclusion and report how many clients and how much revenue that removes, rather than letting the join drop them silently. That exclusion is why the output's total is smaller than the filtered line set's total, so carry the excluded figure as a named number rather than discovering the gap later.
Follow-up
- How would you turn this table into a 12-month net revenue retention ratio, and what exactly becomes the denominator?
- One account contributes 40 percent of a cohort's month-13 fees. How do you present the cohort so a reader sees that before they act on the ratio?
- If subsidiaries roll up to parent_client_id, which first_engagement_date defines the cohort, and does the rollup change the cohort a subsidiary lands in?
Find signed engagements with no approved billable hour
Medium · anti-join · null semantics · not exists
fct_engagement holds engagement_id, start_date and status. fct_time_entry holds time_entry_id, consultant_id, engagement_id, charge_code, work_date, hours, is_billable and status, and engagement_id is NULL on bench, leave, training and internal rows. Return engagements with start_date in the trailing quarter and status IN ('active','closed_delivered','closed_early') that have no approved billable time entry with work_date within 45 days of start_date, giving engagement_id, start_date and days elapsed. Then explain why NOT IN over a subquery selecting fct_time_entry.engagement_id is the wrong instrument here.
Approach
- Express the exclusion as NOT EXISTS with a correlated predicate covering all four conditions at once: te.engagement_id = e.engagement_id AND te.is_billable AND te.status = 'approved' AND te.work_date < e.start_date + INTERVAL '45 days'. NOT IN can only carry a single column, so a multi-condition anti-join forces a contorted subquery even before the NULL problem.
- State the NULL semantics precisely: NOT IN (SELECT engagement_id FROM fct_time_entry) evaluates to NULL rather than TRUE for every candidate as soon as one NULL is present in the list, because x <> NULL is unknown. The WHERE clause keeps only TRUE, so the query returns zero rows and reads as a clean bill of health. This table guarantees NULLs, since bench and leave rows carry no engagement.
- Note the equivalent LEFT JOIN ... WHERE te.engagement_id IS NULL form. It is correct, but it materialises a join only to discard most of it; NOT EXISTS states the intent and most planners execute both as the same anti-join.
- Guard the window against truncation: an engagement that started 10 days ago cannot yet have failed a 45-day test, so require start_date <= current_date - 45 or report the too-early set separately rather than counting them as failures.
- Decide on the late-timesheet risk explicitly. A missing entry may mean unstaffed work or an unsubmitted timesheet, so either hold the cutoff back past the measured submission lag or label the recent tail as provisional.
Worked solution (20 min)
- Count engagements matching the status and start_date predicates; this is the denominator.
- Write the NOT EXISTS version and record its row count.
- Write the NOT IN version on the same data and observe zero rows, then confirm with SELECT COUNT(*) FROM fct_time_entry WHERE engagement_id IS NULL that NULLs are present.
- Cross-check by counting engagements that DO have a qualifying entry and verifying the two counts sum to the denominator.
- Open one engagement from the result and look at its raw time entries to see whether the cause is no staffing or unapproved timesheets.
Expected result: A short list of engagement_id, start_date and days_since_start for engagements that started at least 45 days ago and carry no approved billable hour in their first 45 days. The NOT IN variant returns zero rows on the same data.
Verify it yourself
- Count of flagged engagements plus count of engagements with at least one qualifying entry equals the count of engagements matching the status and date filters.
- Every returned engagement has start_date at or before current_date - 45.
- Spot-checking one flagged engagement in fct_time_entry shows either no rows or only unapproved or non-billable rows.
- Replacing NOT EXISTS with LEFT JOIN ... IS NULL returns an identical set.
Follow-up
- Turn this into a time-to-first-billable-hour distribution. What do you do with engagements that never recorded one?
- Should client_pursuit hours count as the engagement being staffed, and how does that choice change the list?
- How would you prove the empty result from the NOT IN version is a bug rather than good news?
Build the driver tree beneath gross margin per engagement-week
Medium · metric tree · decomposition · margin · pricing models
Gross margin per delivered engagement-week is the north-star. Using fct_invoice_line (engagement_id, line_type, amount_usd, period_start_date, period_end_date) and fct_time_entry (engagement_id, work_date, hours, is_billable, status, bill_rate_usd, cost_rate_usd), decompose it into drivers a delivery lead can act on, such that the drivers recombine to the parent metric exactly rather than approximately. Show the algebra. Then say which driver a staffing decision moves, which a pricing decision moves, and which a scope-control decision moves.
Approach
- Start from the identity margin = 1 - cost / fees, then introduce standard value SV = SUM(hours x bill_rate_usd) over approved billable entries. That gives cost / fees = (cost / SV) / (fees / SV), and fees / SV is realisation rate.
- Split cost / SV into two readable ratios. With H_total as all approved hours on the engagement-week and H_bill as approved billable hours, c as the hours-weighted mean cost rate over H_total and b as the billable-hours-weighted mean bill rate, cost / SV = (H_total / H_bill) x (c / b). So margin = 1 - (H_total / H_bill) x (c / b) / realisation, exact by construction.
- Verify exactness numerically before presenting it. Recompute margin from the three drivers on a real engagement-week and diff it against the direct (fees - cost) / fees. A tree that only holds approximately cannot attribute a share of a change to any driver.
- Map each driver to a decision. Staffing mix moves c / b, because seniority moves cost and bill rates together but not at the same ratio. Rate card and discount policy move realisation and b. Scope control and rework move H_total / H_bill, since every non-billable delivery hour lands in the numerator only.
- State the scope limit: the tree is interpretable only within a pricing_model. On fixed_fee, weekly fees come from milestone recognition and do not respond to hours at all, so realisation absorbs the entire hour overrun and c / b carries no information about whether the work is on budget.
Follow-up
- H_total / H_bill moved from 1.08 to 1.40 on one engagement. Name three causes and the column that separates them.
- How does this tree behave on a retainer, where contracted_hours is NULL and fees are flat regardless of hours worked?
Resolve a real conflict between margin and account retention
Medium · guardrails · leverage · trade-offs · decision rules
A leverage push will staff more analysts and fewer managers on delivery. Engagement gross margin is the primary metric and twelve-month repeat purchase on client_id is the guardrail. Using fct_time_entry (consultant_id, engagement_id, hours, cost_rate_usd, bill_rate_usd, written_off_hours), dim_consultant (consultant_id, level, standard_cost_rate_usd) and fct_invoice_line (engagement_id, line_type, amount_usd, status), explain why the two genuinely move in opposite directions here rather than one being a measurement artifact, then specify the decision rule you would write before rollout, including the threshold at which the guardrail stops the change.
Approach
- Write the per-hour arithmetic before quoting any number. Replacing one manager hour with k analyst hours changes fixed_fee margin by c_m - k x c_a, which is positive only while k < c_m / c_a. On uncapped time_and_materials it changes contribution by k x (b_a - c_a) - (b_m - c_m). Both inequalities turn on k, the efficiency ratio, and none of these tables record it.
- Bound k from the one place it leaks: hours per comparable deliverable on past engagements with different level mixes. Concede the confound immediately, since the engagements that got junior teams were selected for being simpler.
- Establish the guardrail's mechanism rather than asserting a correlation. More analyst hours per manager hour raises review and rework load, which appears as written_off_hours and disputed invoice lines within weeks, and in repeat purchase only after twelve months.
- Substitute a leading guardrail for the lagging one deliberately and say so. Gate on write-off rate and disputed-line share, and state the residual risk: a client who pays every invoice in full and quietly does not return is invisible to both.
- Write the rule with a price attached. Proceed while the within-pricing-model margin gain exceeds X basis points and write-off rate rises by less than Y points on the same engagements; the guardrail stops the change regardless of the margin gain. Fix the unit as engagement within pricing_model, clustered at client_id, because a leverage change lands unevenly across accounts and engagements inside one account are not independent.
Worked solution (35 min)
- Pull current bill and cost rates by level from recent fct_time_entry rows and dim_consultant, and compute the break-even k for fixed_fee (c_m / c_a) and the break-even k for uncapped time_and_materials from k x (b_a - c_a) = b_m - c_m.
- Estimate a plausible range for k from historical hours per deliverable across differing level mixes, and state the selection confound in the same sentence as the estimate.
- Compute write-off rate and disputed-line share by engagement for the trailing four quarters, stratified by pricing_model, to fix the pre-period baseline the threshold is measured against.
- Write the decision rule with both numeric thresholds, the override direction, and the clustering unit named.
- Evaluate the rule against one historical engagement cohort that already ran junior-heavy, and record whether it would have fired.
Expected result: A decision rule carrying two numeric thresholds and an explicit override direction, built on the per-hour inequalities (fixed_fee improves while k < c_m / c_a; uncapped time_and_materials improves while k x (b_a - c_a) > b_m - c_m), with k named as unmeasured in this schema and bounded rather than assumed to equal 1. Leading guardrails identified as substitutes for twelve-month repeat purchase, with the substitution risk stated.
Verify it yourself
- Setting k = 1 makes the fixed_fee case trivially favourable. If your rule still produces a hard decision at k = 1, you have assumed away the quantity that decides it.
- The two pricing models yield different break-even k on the same rate card, so a single firm-wide threshold is wrong by construction.
- The stated threshold is expressed in the same units as the metric it gates, so a reader can confirm from the scorecard alone whether the rule fired correctly.
Follow-up
- Margin is up 300 basis points and write-off rate is up 1.5 points. Does your rule fire? Show the arithmetic that decides it.
- What would convince you the write-off rise is a change in concession policy rather than a change in delivery quality?
Two-year-old engagement margins moved after a dimension join change
Hard · type-2 scd · restatement · effective dating
After a warehouse release, gross margin on engagements closed two years ago changed by up to six points, upward on some and downward on others. The cost model now joins dim_consultant on consultant_id AND is_current = TRUE to read standard_cost_rate_usd, instead of using the cost_rate_usd already snapshotted on each fct_time_entry row. Available columns include consultant_sk, consultant_id, level, standard_cost_rate_usd, effective_from, effective_to, is_current, termination_date. Explain the restatement, quantify who it favours, and specify the join you would ship.
Approach
- State the mechanism in one line: joining a type-2 dimension on is_current pastes today's cost rate onto historical hours, so every consultant promoted or re-rated since restates their own history in the direction their rate moved. Because promotions differ per person, engagement margins move in both directions, which is exactly the observed pattern.
- Quantify with three parallel cost computations per engagement: (a) the snapshotted fct_time_entry.cost_rate_usd, (b) an effective-dated join using work_date BETWEEN effective_from AND COALESCE(effective_to, 'infinity'), and (c) the shipped is_current join. Report the distribution of (c) minus (a) and correlate it with the share of hours delivered by staff who have since been promoted.
- Check cardinality as a separate defect: an is_current join can drop consultants whose current row is missing or whose termination retired the row, and can fan out if consultant_id carries more than one current row. Compare hour totals before and after the join; they must be identical.
- Decide the canonical source and justify it: the snapshotted rate on the time entry is what was booked and is authoritative for financial restatement. The effective-dated join is the reconciliation, and a systematic disagreement between (a) and (b) is its own defect worth raising rather than papering over.
- Reissue the affected periods with an explicit changeover note, and pin the rule in a test that fails if is_current appears in any join from a fact table to a type-2 dimension on a historical grain.
Follow-up
- Some entries have cost_rate_usd NULL or zero. Which source wins then, and what does that do to your reconciliation?
- The same pattern exists for bill_rate_usd. What does it do to realisation, and in which direction?
- How would you sweep every model in the warehouse for this class of bug without reading each one?
Suggested practice checklist
For someone who can already write the query and train the model but stalls when asked what to measure or whether a change is worth making. Metric definition and case structure come first; the technical work is kept as maintenance rather than the centre of the week.
Day 1 — Metric anatomy
- For three products you use daily, write one primary metric, two input metrics that plausibly move it, and one guardrail that would catch a cheap way of moving the primary at the cost of the product.
- For one of them, specify the metric precisely enough that two analysts would return the same number: numerator, denominator, unit of observation, time window, and how returning and deleted accounts are treated.
- Pick a ratio metric and write what happens to it when the denominator shrinks for reasons unrelated to the numerator, with a concrete example of that happening.
Deliverable: A one-page metric tree for one product, with the primary metric written as an unambiguous spec.
Day 2 — Diagnosing a drop without guessing
- Take the prompt "weekly active users fell 8 percent week over week" and write the segmentation plan before proposing any cause: platform, region, tenure cohort, acquisition channel, and whether the movement sits in the numerator or in a changed denominator.
- List the instrumentation failures that manufacture fake drops (a client release that stopped firing an event, a bot filter change, a shifted date boundary or timezone) and write the query that rules out each one.
- Rehearse stating the boring explanations first, seasonality and day-of-week composition, before reaching for a product cause.
Deliverable: A drop-diagnosis checklist short enough to recite from memory in under a minute.
Day 3 — Should we build it
- Take a feature idea and write it as a bet: what you believe is true, what would have to be true for it to pay off, the metric that would confirm it, and the effect size that would justify the engineering cost.
- Size the opportunity top-down and bottom-up, then reconcile the two numbers in writing instead of quoting whichever is friendlier.
- Write the counter-metric that would make you kill the feature even if it wins on the primary metric.
Deliverable: A one-page product memo ending in a decision rather than a list of considerations.
Day 4 — The places aggregate numbers lie
- Construct a Simpson's paradox numerically: two segments where the treatment wins within each segment yet loses overall, and identify the shift in segment weights that causes it.
- Take a heavy right-tailed quantity such as revenue per user and write why the mean is the wrong summary, which percentile you would report instead, and what a moving mean with a stable median tells you.
- Write your definition of a session for the product from day one, then name two real behaviours it misclassifies.
Deliverable: One page holding a worked Simpson's paradox table and a session definition with its two known failure cases.
Day 5 — Technical maintenance, aimed at metrics
- Solve four timed SQL prompts that all end in a ratio metric, so the question of grain stays live in every answer.
- Compute a 95 percent confidence interval for a proportion on a small sample, and state why the normal approximation is unreliable when either np or n(1 minus p) falls below roughly 10, along with which interval you would use instead.
- Take one metric from your day-one tree, write the query that computes it correctly, then write the query that computes it wrong in the most plausible way and explain how you would notice.
Deliverable: Four solved prompts plus a matched correct and plausible-wrong query for one metric.
Day 6 — Turning engineering work into data science stories
- Write three project stories as situation, decision, trade-off, outcome, each carrying one number and one thing you got wrong.
- For the story you will lead with, prepare an answer to "what would you do differently" that names a decision you made, not a constraint you were handed.
- Practise the sentence that reframes a systems project as a question project: the question the work answered, ahead of the pipeline it shipped.
Deliverable: Three written stories with the lead story delivered aloud and timed under four minutes.
Day 7 — Mock case and gap list
- Run a 40-minute mock case with someone playing a product manager who pushes back on your metric choice, and record it.
- Listen back and mark every moment you proposed a solution before the success metric existed.
- Rewrite those moments as the question you should have asked, and rehearse the first 90 seconds of the case until scoping comes before solving.
Deliverable: A recorded case plus a rewritten opening 90 seconds.
Bring numbers, including the ones you are unsure of
An answer without a quantity is hard to interrogate, so interviewers keep probing until they find one. Come with the baseline, the change, the window it was measured over, and how confident you were. If the effect never got measured, say so and say what you would have measured. Fabricated precision is worse than an honest gap.
What is your experience with the current data science stack (Python, SQL, R)?
Medium · behavioural and stakeholder questions
Approach
- Close with what you would do differently, concretely.
- 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.
Follow-up
- How did you know the outcome was caused by your change?
- What did you decide not to do, and why?
How do you handle situations where you have limited guidance from a client?
Medium · behavioural and stakeholder questions
Approach
- State the situation in two sentences and spend the rest on your reasoning.
- Quantify the outcome, including what you would not claim credit for.
- Pick a story where you drove the decision, not one where you observed it.
Follow-up
- How did you know the outcome was caused by your change?
- What would you do differently if you ran that project again?
Describe a time you had to explain a complex technical concept to a non-technical stakeholder.
Medium · behavioural and stakeholder questions
Approach
- State the situation in two sentences and spend the rest on your reasoning.
- Quantify the outcome, including what you would not claim credit for.
- Pick a story where you drove the decision, not one where you observed it.
Follow-up
- What would you do differently if you ran that project again?
- How did you know the outcome was caused by your change?
What are your long-term career ambitions, and how do they align with a consulting career?
Medium · behavioural and stakeholder questions
Approach
- Quantify the outcome, including what you would not claim credit for.
- State the situation in two sentences and spend the rest on your reasoning.
- Name the disagreement or constraint, and how you resolved it with evidence.
Follow-up
- How did you know the outcome was caused by your change?
- What would you do differently if you ran that project again?