cswork / libraryOpen practice workspace

Interview guides · FogHorn Systems

FogHorn Systems Data Scientist Interview Questions & Guide 2026

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

The 60-second brief

The role of a Data Scientist at FogHorn Systems is pivotal to driving innovation and delivering actionable insights from complex data sets. As a Data Scientist, you will leverage advanced analytical techniques and machine learning algorithms to solve critical business problems and optimize solutions for FogHorn's edge computing products. This role is not only about analyzing data but also about understanding the unique challenges faced by FogHorn's clients and creating scalable models that enhance product performance and user experience.

Coding rounds for this role are usually data-manipulation shaped rather than data-structure shaped: group-bys, joins, time windows, ranking within a partition. Confirm the format before spending a week on graph traversal.

Focus areas

  • Measure churn only on renewal-eligible accounts
  • Read NRR on a fixed account cohort
  • Separate contracted seats from actively used seats

Overview

At FogHorn Systems, Data Scientists work closely with cross-functional teams, including product managers, software engineers, and domain experts, to develop and refine algorithms that power real-time analytics and predictive capabilities in various sectors. Your contributions will play a significant role in enhancing product offerings that serve industries such as manufacturing, transportation, and smart cities. Expect to engage with cutting-edge technologies and methodologies that make this position both challenging and rewarding.

The FogHorn Systems interview process

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

Initial Screening

Whoever runs this call is usually not a practitioner. They take notes, and a hiring manager skims those notes later, so the real question is whether your work survives being written down by someone outside the field. Test every project sentence against that: could a non-specialist repeat it correctly without knowing what a propensity score is? Carry a plain-language version of each project and one reason you want this particular role that you could not copy onto another application. Vagueness at this stage reads as inexperience, even when the underlying work was genuinely deep.

What to demonstrate

  • Whether a non-specialist can restate your projects accurately, since their paraphrase is what reaches the hiring manager
  • Whether your reason for wanting the role points at the work itself rather than the company's reputation
  • Whether your language signals the level being screened for: what you decided yourself versus what you were handed

How to prepare

  • Write a two-sentence, jargon-free version of each major project: the question nobody could answer, and the decision your work changed. Read it to someone outside data and have them repeat it back
  • Point your 'why this role' answer at something concrete in the job description or the product surface you would be working on, and keep it to two sentences
  • Have two questions ready about measurement: which metric the team is held to, and who acts on an analysis once it lands

Technical Assessments

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

On-site Interview

A loop is not scored one interview at a time. The people you meet compare notes afterwards, usually in a meeting you are not in, and the outcome turns on what each of them can say about you when asked. That rewards something other than survival: every room needs one specific thing worth repeating, and none of them can contradict another. The common way to lose is to tell the same project four times with different numbers in it, or to be uniformly fine in a way that leaves nobody with anything to argue for.

What to demonstrate

  • Whether your account of a project survives being told twice, with the same scale, the same metric definition and the same numbers each time
  • Whether each interviewer leaves with one concrete claim they could make on your behalf later, rather than an absence of complaints
  • Whether a question you already answered in an earlier room gets the same answer at the same depth, without visible impatience

How to prepare

  • Write a one-page fact sheet for your two or three main projects that fixes the numbers you will quote: rows of data, the metric as a single sentence, the effect you measured and how long the work took. Say them aloud from the sheet until they come out identical every time
  • For each kind of room you expect, decide the one sentence you want that interviewer repeating in a debrief, then check during the mock that you said it outright instead of implying it
  • Rehearse answering the same project question twice in one sitting, the second time as though you had not just answered it, because the thing that needs fixing is the flatness that creeps into a repeated story

Where candidates lose points

Reading consumption metrics before the metering lag window has closed

Usage pipelines land late and correct themselves, which is exactly what is_restated and restated_at record. A dashboard queried on day T sees a partially populated tail for the last several days, so the most recent points always slope downward and always look like a regression. Analysts then explain the artefact, and sometimes ship a change to fix it. Establish the empirical settling time by measuring how much a given usage_date's total moves between first_written_at and its final value, exclude that many trailing days from every reportable figure, and never compare a fresh period against a settled one.

Computing monthly churn against the entire customer base when contracts are annual

An annual contract has no opportunity to churn except at its renewal date, so an account that is eleven months from renewal is in the denominator while being incapable of appearing in the numerator. The resulting rate is smaller than the real one by roughly the ratio of the base to the renewal-eligible base, and it oscillates with the seasonality of when deals were originally signed rather than with anything about the customers. The corresponding trap on the other side is counting a churn on the date the record was updated rather than on term_end_date, which shifts losses into whichever month the operations team did its paperwork.

Optimising accuracy on a heavily imbalanced target

State the base rate first, then choose the metric from the relative cost of a false positive against a false negative: precision and recall at the operating threshold, PR-AUC, or expected cost. At a 1 percent positive rate, predicting the majority class for everyone scores 99 percent accuracy and is worthless.

Comparing periods without accounting for seasonality or day-of-week

Compare whole weeks against whole weeks and check whether the same swing appeared in prior cycles or prior years before attributing it to anything you changed. Weekday and weekend populations often differ enough that a Tuesday-to-Saturday comparison is meaningless.

Questions & practice

Write a function to implement a decision tree algorithm.

Medium · machine learning and modelling

Approach

  1. Frame the prediction: the label, the moment of prediction, and the action it triggers.
  2. Say how the offline result would be validated online before it is trusted.
  3. Pick an evaluation metric that matches the cost of each error type, not a default.

Follow-up

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

How would you approach a situation where your model is underperforming?

Medium · machine learning and modelling

Approach

  1. Set a baseline first, so any model has something honest to beat.
  2. Check what information would not exist at prediction time, and exclude it.
  3. Pick an evaluation metric that matches the cost of each error type, not a default.

Follow-up

  • What would you monitor after launch to know the model is still valid?
  • Where could label leakage enter this setup?

Bootstrap a confidence interval for net revenue retention

Medium · bootstrap · retention · resampling

You have one row per account with arr_start_cents (ARR twelve months ago) and arr_end_cents (ARR today, zero if churned), covering the fixed cohort of accounts that had ARR twelve months ago. Net revenue retention is sum(arr_end_cents) / sum(arr_start_cents). Write a nonparametric bootstrap from scratch, without scipy.stats.bootstrap: resample accounts with replacement, recompute the ratio of sums on each resample, and return the point estimate with a 95% percentile interval from 10,000 resamples. Also report the interval you would get from the mean of per-account ratios, and explain the difference.

Approach

  1. Resample the account, because the account is the unit the estimand is defined over. One bootstrap draw is a vector of account indices and both numerator and denominator are recomputed from that same draw; resampling the two sides independently destroys the within-account correlation that makes a ratio estimator stable.
  2. Vectorise the draws: idx = rng.integers(0, n, size=(B, n)), then end[idx].sum(axis=1) / start[idx].sum(axis=1). A 10,000 by n index matrix is usually far cheaper than a Python loop; if the matrix is too large for memory, chunk over B rather than reverting to a loop.
  3. Take the interval from np.quantile(ratios, [0.025, 0.975]). The percentile interval differs from estimate +/- 1.96 * bootstrap SE whenever the resample distribution is skewed, which it will be here, and the skew is the thing you want represented.
  4. Compute the mean-of-ratios version on the same resamples, and state the exact relationship rather than guessing which of the two is larger. With r_i = arr_end_i / arr_start_i, the ratio of sums is the arr_start-weighted mean of exactly those r_i, so sum(end)/sum(start) - mean(r) = Cov(arr_start, r) / mean(arr_start) using the population covariance. The gap is positive when larger accounts retain and expand better than smaller ones, and negative when they do not; a cohort whose small accounts churn at a higher rate has positive covariance, which puts the mean of per-account ratios BELOW the ratio of sums. Requires arr_start_i > 0 for every account, which the fixed-cohort definition guarantees; r_i is floored at 0 and unbounded above, so a handful of 4x expansions among small accounts can flip the sign. Compute the covariance and report it instead of asserting a direction.
  5. Report the interval width beside the concentration of the cohort. If the largest account is 12% of starting ARR, a narrow interval is evidence that the resampling unit is wrong rather than evidence that the estimate is precise.

Worked solution (30 min)

  1. start = df.arr_start_cents.to_numpy(float); end = df.arr_end_cents.to_numpy(float); n = len(start); point = end.sum() / start.sum()
  2. rng = np.random.default_rng(7); idx = rng.integers(0, n, size=(10_000, n)); ratios = end[idx].sum(1) / start[idx].sum(1)
  3. lo, hi = np.quantile(ratios, [0.025, 0.975]); return point, lo, hi
  4. per_acct = end / start; mean_point = per_acct.mean(); mean_boot = per_acct[idx].mean(1); compare np.quantile(mean_boot, [0.025, 0.975]) against (lo, hi), and report np.cov(start, per_acct, ddof=0)[0,1] / start.mean() as the quantity that accounts for the gap between the two centres.

Expected result: A point estimate equal to sum(end)/sum(start) on the unresampled data — the bootstrap mean is close but must not be substituted for it — and a percentile interval that is asymmetric around the point when expansion is skewed. The mean-of-ratios interval is centred on a different estimand, and which side it falls on is decided by Cov(arr_start, r): it sits below the ratio of sums in the common case where small accounts churn at a higher rate than large ones, and above it when the large accounts are the ones shrinking. Its width is governed by the spread of per-account growth, while the ratio-of-sums width is governed by how concentrated starting ARR is, so neither interval is reliably the wider one.

Verify it yourself

  • point equals sum(end)/sum(start) computed directly on the original arrays, and point - per_acct.mean() equals np.cov(start, per_acct, ddof=0)[0,1] / start.mean() to floating-point tolerance. That is an algebraic identity, not an empirical claim: a mismatch means the per-account ratios or the weights are wrong, and the sign of it tells you which of the two numbers is larger before you look.
  • np.median(ratios) sits within a small fraction of a point of the point estimate; a large gap indicates a wrong resampling unit or a transposed index matrix.
  • Raising B from 10,000 to 20,000 moves the endpoints by far less than the interval width.
  • Setting every arr_end equal to its arr_start gives point == 1.0, per_acct.mean() == 1.0, a zero covariance term and a zero-width interval.

Follow-up

  • The cohort has 800 accounts and the largest is 12% of starting ARR. How much do you trust a percentile interval here?
  • How would you extend this to an interval on the year-over-year change in NRR?
  • Two accounts merged mid-window and one contract was co-termed into the other. How do you keep the cohort fixed?

Solve a coding challenge related to data manipulation.

Medium · sql and data manipulation

Approach

  1. State the window function and its partition and ordering out loud before writing it.
  2. Handle the rows that do not match: a LEFT JOIN with a NULL check is usually the question.
  3. Check whether any join is one-to-many before aggregating, or the sums inflate.

Follow-up

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

How would you select the appropriate algorithm for a given 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. Say which table is the grain you start from, and join outward from it.
  3. Check whether any join is one-to-many before aggregating, or the sums inflate.

Follow-up

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

Sessionise interactive API traffic with a thirty-minute inactivity gap

Medium · window functions · sessionisation · gaps and islands

fct_api_request carries account_id, user_id (null for service accounts), request_at, traffic_class and http_status. Using only traffic_class = 'interactive' rows with a non-null user_id, group each seat's requests into sessions on a 30-minute inactivity threshold: a request more than 30 minutes after the previous request from the same (account_id, user_id) opens a new session. For one ISO week return, per account, the session count, the median session duration in minutes and the median requests per session. A single-request session has a duration of zero.

Approach

  1. Filter first: traffic_class = 'interactive' and user_id IS NOT NULL. Machine traffic has no sessions in any useful sense, and leaving CI or batch rows in produces sessions that are really cron schedules.
  2. Get the previous timestamp with LAG(request_at) OVER (PARTITION BY account_id, user_id ORDER BY request_at). Partitioning by user_id alone stitches one person's work across two different accounts into one fabricated session, because a human holds memberships in several accounts.
  3. Flag a boundary where the lag is NULL or request_at - lag > interval '30 minutes'. Decide and state whether exactly 30 minutes continues the session; strictly greater is the conventional choice and needs to be written down either way.
  4. Assign session ids with SUM(boundary::int) OVER (PARTITION BY account_id, user_id ORDER BY request_at ROWS UNBOUNDED PRECEDING), the standard running-count construction for islands.
  5. Roll up to sessions with min(request_at), max(request_at) and count(*), then to accounts with percentile_cont(0.5) WITHIN GROUP (ORDER BY ...). Use medians, not means: session length is strongly right-skewed and one long-running client dominates the average.

Worked solution (30 min)

  1. Build the filtered week: interactive rows with user_id IS NOT NULL inside [week_start, week_start + 7 days).
  2. Add prev_at via LAG(request_at) OVER (PARTITION BY account_id, user_id ORDER BY request_at) and derive is_new_session = (prev_at IS NULL OR request_at - prev_at > interval '30 minutes').
  3. Add session_seq = SUM(is_new_session::int) OVER (PARTITION BY account_id, user_id ORDER BY request_at ROWS UNBOUNDED PRECEDING).
  4. Aggregate to sessions by (account_id, user_id, session_seq) taking min, max and count, with duration_minutes = EXTRACT(EPOCH FROM max - min) / 60.
  5. Aggregate to accounts with count(*) AS sessions, percentile_cont(0.5) WITHIN GROUP (ORDER BY duration_minutes) and percentile_cont(0.5) WITHIN GROUP (ORDER BY request_count).

Expected result: One row per account with session_count, median_session_minutes and median_requests_per_session, computed over interactive human traffic only, where a lone request yields a session of duration zero and request count one.

Verify it yourself

  • Summing request_count across all sessions equals the filtered row count for the week exactly.
  • Every session duration is non-negative, and no two consecutive requests inside one session are more than 30 minutes apart when you re-derive the LAG within sessions.
  • Recomputing with the partition on user_id alone yields strictly fewer sessions and longer medians for any account sharing users with another account, which confirms the partition key matters.

Follow-up

  • Sessions belonging to colleagues in one account are correlated. What does that do to a t-test on session length across an experiment arm?
  • A long-poll or streaming endpoint keeps a connection open for hours. How do you stop it reading as one twelve-hour session?

What are some emerging trends in data science that you find exciting?

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. State what result would change your recommendation, so the answer is falsifiable.

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?

How do you prioritize competing tasks when working on multiple projects?

Medium · product sense and metrics

Approach

  1. State what result would change your recommendation, so the answer is falsifiable.
  2. Decompose the metric into the rates that drive it, and say which one you would check first.
  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?
  • What would you do if the primary metric and the guardrail moved in opposite directions?

How would you design a data pipeline for real-time analytics?

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

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

Stop metering retried requests: design the metric that decides it

Medium · guardrail conflict · billing integrity · retries · expected value

The platform meters accepted requests. A proposal carries three clauses: stop metering fct_api_request rows where is_retry = true, stop metering rows with a 4xx status, and stop metering rows with http_status >= 500. Its author has attached one figure to all three together, roughly 4% of requests_thousands volume. You have fct_api_request (account_id, is_retry, idempotency_key, http_status, traffic_class, billable_units, request_at) and fct_usage_daily (billable_quantity, net_amount_cents, cogs_cents). Size each clause separately before arguing about any of them, then define the primary metric, the guardrail that genuinely conflicts with it, and how you resolve that conflict for a decision that has to be made this quarter. Revenue falls this quarter with certainty; any benefit appears at renewals up to twelve months out.

Approach

  1. Size the three clauses before accepting the headline 4%, because one of them is a no-op. billable_units is defined as zero for requests that failed with a 5xx, so the third clause removes no metered volume at all. Confirm that in the data rather than trusting the column comment: if sum(billable_units) over rows with http_status >= 500 is not zero, the metering pipeline contradicts its own definition and that is a billing defect to file before any pricing conversation happens. The two live clauses are 4xx failures, which are metered in full, and retries that did not themselves end in a 5xx.
  2. Size the two live clauses as a union, not a sum. A retry can return 4xx, so the clauses overlap and adding their volumes counts that intersection twice. Partition the trailing 90 days into four mutually exclusive buckets instead: clean (is_retry = false, http_status < 400), non-retry 4xx, retry with http_status < 500, and http_status >= 500. Report the removable share per account as a distribution; if the mass sits in a handful of accounts this is a commercial conversation with those accounts rather than a platform-wide pricing change.
  3. State the conflict rather than dissolving it. The primary metric, net metered revenue per paying account, and the integrity guardrail, the share of metered volume that is retried or failed traffic, move in opposite directions by construction. No redefinition removes that. The job is to price the trade-off, not to make it disappear.
  4. Show the perverse coupling with data, and be exact about its mechanism. Because a 5xx already carries zero billable_units, the platform is not paid directly for its own failures; it is paid for the retries and the client-side 4xx traffic those failures provoke, which is one step removed and therefore easy to miss. Cross-tab each account's trailing 28-day 5xx rate against its metered volume in the same window. If metered volume rises with error rate, that indirect coupling is the actual argument for the change.
  5. Resolve on expected value with the uncertainty stated. The revenue loss is computable and near-certain; the renewal benefit is not, so invert it and state the break-even: how many basis points of gross logo retention on the renewal-eligible base would offset the loss. That converts an argument about values into an argument about one number. Then propose the measurement that would settle it instead of claiming a readout you do not have: stage the rollout by renewal cohort so accounts whose terms end soonest are treated first, read out on gross logo retention on the renewal-eligible base, and say honestly whether the number of annual renewals in the window can support that estimate at all.

Worked solution (30 min)

  1. Test the third clause first: over the trailing 90 days compute count(*) and sum(billable_units) from fct_api_request where http_status >= 500. The sum must be zero, because billable_units is defined as zero for 5xx failures. If it is zero the clause removes nothing and drops out of the analysis; if it is not, stop and raise a metering defect, because every volume figure downstream of that column is then suspect.
  2. Compute trailing-90-day metered volume per account in the four mutually exclusive buckets: clean, non-retry 4xx, retry with http_status < 500, and http_status >= 500. Roll the total up to the requests_thousands SKU and reconcile it against fct_usage_daily billable_quantity for the same window.
  3. Convert the two removable buckets to money using each account's realised rate, net_amount_cents / billable_quantity from fct_usage_daily, because list rate overstates revenue for every discounted account.
  4. Annualise the revenue at risk and divide it by the ARR of the renewal-eligible base to express the break-even as an improvement in gross logo retention, in basis points.
  5. Cross-tab account 28-day 5xx rate deciles against metered volume per account to establish whether the error-to-revenue coupling, which can only run through retries and 4xx rather than through the failed requests themselves, is real or a story.

Expected result: Three clause sizes rather than one blended figure: zero for the 5xx clause, since billable_units is already zero there, and separate volumes for 4xx failures and for retries that did not 5xx, combined as a union so the retry-and-4xx intersection is counted once. On top of that, a single break-even figure stated as the improvement in gross logo retention on the renewal-eligible base needed to offset the metered revenue actually removed, and the per-account concentration of that removable volume. The answer says that the primary metric and the integrity guardrail conflict by construction, and resolves it with the break-even number and a staged rollout rather than by moving a definition.

Verify it yourself

  • sum(billable_units) over rows with http_status >= 500 must be exactly zero; any other result contradicts the documented definition of the column and invalidates the volume figures derived from it.
  • The four buckets must be mutually exclusive and must sum to total billable_units, which must in turn reconcile to fct_usage_daily billable_quantity within the known restatement tolerance; a large gap means the request-to-SKU mapping is wrong.
  • The two removable buckets must be reported as a union; their arithmetic sum will exceed the union by exactly the retry-and-4xx intersection, and a write-up that quotes the sum has overstated the revenue at risk.
  • Requests sent with no idempotency_key cannot carry is_retry = true, so the measured retry share is a lower bound and the write-up must say so.

Follow-up

  • Suppose the two live clauses turn out to remove 2.6% of consumption revenue. How much improvement in gross logo retention on an annual-contract base pays that back, and over what horizon does the payback land?
  • A retry sent without an idempotency_key cannot be flagged as a retry. Which direction does that bias your estimate of the removable volume, and how can you bound it?

Monthly logo churn triples with no change in satisfaction

Hard · renewal-eligible base · censoring · timestamp semantics

Monthly logo churn, computed as churned accounts divided by all paying accounts, tripled last month. Contracts are annual. From fct_subscription_period (account_id, term_start_date, term_end_date, amendment_type, auto_renew, booked_at, superseded_by_id, is_current) and dim_account (account_id, churned_at, account_status, employee_band, acquisition_channel), rebuild churn on the renewal-eligible base, separate calendar effects from customer behaviour, and state whether retention actually changed. Note that churned_at is sometimes set when the record was updated rather than at term end.

Approach

  1. Rebuild the denominator as accounts whose term_end_date falls in the month. An account eleven months from renewal sits in the current denominator while being structurally incapable of entering the numerator, so on annual contracts the published rate understates the truth by roughly the reciprocal of the annual renewal fraction and moves with the signing calendar.
  2. Plot the renewal-eligible base by month across two years. A signing surge twelve months earlier reproduces itself as an eligibility surge now, and a rate whose denominator ignores that tracks the sales calendar rather than customer sentiment.
  3. Date each churn by term_end_date, never by churned_at or an update timestamp. Inspect the distribution of churned_at minus term_end_date: a backlog cleared in one batch appears as a mass at a single date and shifts losses into whichever month the operations team did its paperwork.
  4. Apply the 45-day grace for late renewal paperwork so the most recent month and a half is marked not reportable, rather than printing a number that will rise once the paperwork lands.
  5. Compare corrected gross logo retention against its own trailing distribution, and if a real change survives, cut it by employee_band, acquisition_channel and plan_tier before proposing any cause.

Follow-up

  • Some contracts in the window have not reached their renewal date. When does this require a survival estimator rather than a simple rate, and which one would you use?
  • How do you report churn to an audience that wants a monthly number when the underlying event is annual and lumpy?

Suggested practice checklist

Roughly 90 minutes a night on weekdays with one longer weekend block. The plan deliberately cuts scope rather than compressing everything, on the assumption that finishing one thing a night beats half-starting four.

Day 1 — Fix the scope and set a baseline

  • Read the role description and write the three things the loop will almost certainly test, then write an explicit not-doing list for everything else and keep it visible all week.
  • Take one 20-minute SQL prompt and one 10-minute metric question cold, and write the single sentence that says what blocked each attempt, since that sentence is what decides which two topics get the most evenings.
  • Set the week's one rule: one problem finished to completion every night, including the night you only have 40 minutes.

Deliverable: A one-page scope with an explicit not-doing list and two cold attempts, each carrying one sentence on what blocked it.

Day 2 — One query pattern, written three times

  • Choose the single pattern most likely to appear (a cohort retention grid, or a funnel counted by user) and write it three times from a blank file rather than editing the previous attempt.
  • On the third attempt, write the grain of every CTE as a comment before writing its body.
  • Stop at 90 minutes even if the third version is imperfect, and write the one thing you would fix with another hour.

Deliverable: Three independent versions of the same query plus a note on what changed between them.

Day 3 — Only the statistics you will be asked to defend

  • Write, in under 200 words, how you would decide whether a difference between two groups is real: the test, its assumptions, and what you would switch to when an assumption fails.
  • Compute a 95 percent confidence interval for a difference in proportions by hand on realistic numbers, then write in one sentence what changes if the two samples are paired rather than independent.
  • Write your answer to "what does a p-value mean", check it against a definition, and delete the version that describes it as the probability the hypothesis is true.

Deliverable: A 200-word written answer and one hand-computed interval you can reproduce under pressure.

Day 4 — One case, and the assumptions holding it up

  • Answer one product case aloud in 20 minutes with a recording running, then listen back with a pen and mark every claim you asserted without saying what it rested on: an assumed user behaviour, an assumed data source, an assumed baseline rate, an assumed grain.
  • Pick the three assumptions the recommendation actually depends on, write how you would check each one against data, and say which one being wrong would flip the recommendation rather than merely weaken it.
  • Write the four-step structure you used onto a card small enough to hold in working memory when you are nervous.

Deliverable: One recording, three load-bearing assumptions each with a written check, and a four-step structure card.

Day 5 — Your own work, timed

  • Write a 90-second version and a four-minute version of your main project, and time both out loud rather than reading them.
  • Prepare answers to the two follow-ups that always come: what you would do differently, and how you knew it worked.
  • Put one number in the first sentence and be able to say exactly where that number came from and what it excludes.

Deliverable: Two timed narratives with one defensible number in the opening line.

Day 6 — The one full rehearsal, in a longer weekend block

  • Run a 60-minute mock covering query work, a case and a behavioural question in a single sitting with no breaks, because sustained attention is the thing evenings have not trained.
  • Immediately afterwards, and before hearing any feedback, write the three moments you lost the thread.
  • Spend the rest of the block only on those three moments, and on nothing you merely feel shaky about.

Deliverable: Mock notes naming three failure moments with a specific fix written under each.

Day 7 — Taper

  • Write the 20-minute warm-up you will actually do on the morning of the interview: one query you can already write from a blank file, one metric you can define out loud, and nothing you have never seen before.
  • Re-read only your own notes from this week, and open no new material.
  • Write down the logistics: the tool you will be asked to work in, whether lookups are allowed, and the sentence you will use when you do not know something.

Deliverable: A one-page card holding the case structure, the project numbers, and the logistics.

Ambiguous requests are the test

Most of the questions in this section reduce to one thing: can you be handed a vague request and come back with something useful? Prepare an example where the ask was underspecified, you chose an interpretation, and you said out loud which interpretation you chose. Describing how you narrowed the question matters more than the technique you eventually used.

How do you handle missing values in a dataset?

Medium · behavioural and stakeholder questions

Approach

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

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 situation where you had to persuade a team to adopt your approach.

Medium · behavioural and stakeholder questions

Approach

  1. Close with what you would do differently, concretely.
  2. Quantify the outcome, including what you would not claim credit for.
  3. Name the disagreement or constraint, and how you resolved it with evidence.

Follow-up

  • What did you decide not to do, and why?
  • What would you do differently if you ran that project again?

Defend a churn number twelve times the one in the board deck

Hard · stakeholder management · retention metrics · definitions

You recompute logo churn on the renewal-eligible base from fct_subscription_period, counting only accounts whose term_end_date fell in the month and allowing a 45-day grace for late paperwork. Annualised, about 14 percent of accounts that reach a renewal date do not renew. A revenue leader has been quoting 1.2 percent to the board for two quarters, computed by dividing non-renewed accounts by the entire customer base in each month and printing that monthly figure with no period attached. You have 20 minutes with that leader and the finance lead. Decide which number is reported from now on, and what happens to the two quarters already published.

Approach

  1. The interviewer is probing whether you can hold a correct definition under social pressure without turning it into a competence dispute. Open by reproducing their 1.2 percent exactly, with their denominator and their months, so the disagreement is arithmetic both sides can see rather than a claim about who was careless.
  2. Separate the two defects, because they are different in kind. The denominator is wrong: on annual contracts only about one twelfth of the base reaches a renewal date in any month, so an account eleven months from renewal sits in the denominator while being structurally incapable of entering the numerator, which suppresses the rate by a factor near twelve. The period is merely unstated: a monthly figure printed beside annual revenue targets gets read as an annual rate.
  3. Say out loud that those two defects nearly cancel in the level, before the leader finds it. Twelve times 1.2 percent is about 14 percent, which is your number. That is the strongest thing you can say in the room, because it proves both figures rest on the same non-renewal count and moves the meeting onto which denominator and which period get published rather than onto whose query is right.
  4. The level is recoverable; the series is not. Non-renewals in a month are the eligible base for that month times the churn rate, so dividing by a fixed whole base makes the published line proportional to how many contracts happen to come up that month. Where signings cluster at quarter ends, the eligible base in a quarter-end month can be several times a quiet month's, and the month-over-month moves the board has been reading as satisfaction are the signing calendar.
  5. Separate the measurement change from a business change. Nothing got worse this week; the loss rate was always this. Bring net revenue retention over the same period as a ratio of sums on a cohort frozen twelve months earlier, because logo churn concentrated in small accounts can sit beside healthy revenue retention, and that combination is the actual story.
  6. Offer a migration path rather than a correction. Report both rates for one quarter with a written bridge, restate the prior two quarters in an appendix instead of silently, and pin the definition, including the period it is stated over, somewhere finance and product both read it. Concede the limits of your own number: the 45-day grace means the most recent 45 days are not reportable, and churn must be dated on term_end_date rather than on updated_at. A strong answer volunteers this; a generic one only defends.

Follow-up

  • The leader multiplies their monthly figure by twelve, lands on your annual number, and concludes nothing was ever wrong. What do you say?
  • The leader says publishing the corrected rate costs the team its credibility with the board this quarter. What do you do?
  • Gross logo retention worsened while net revenue retention improved. Which do you lead with, and what does the combination tell you about who is leaving?