Interview guides · Maximus Tech
Maximus Tech Data Scientist Interview Questions & Guide 2026
SQL practice for Maximus Tech data scientist candidates: window functions, funnel and retention queries, joins that quietly drop rows.
The 60-second brief
A Data Scientist at Maximus Tech serves as a bridge between raw data streams and actionable business strategy. You will be responsible for building predictive models, optimizing data pipelines, and deriving insights that directly influence Maximus Tech's core product offerings. Your work is fundamental to how the company scales operations and maintains a competitive edge in an increasingly data-driven market.
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
- Measure churn only on renewal-eligible accounts
- Analyse at the account grain, cluster errors
- Power experiments for heavy-tailed account revenue
Overview
This role is both challenging and high-impact, requiring you to navigate complex datasets while maintaining a focus on business objectives. You will collaborate closely with engineering and product teams to translate ambiguous problems into structured, data-backed solutions. Maximus Tech interviews assess technical rigor alongside a clear understanding of the broader organizational impact your models create.
Be prepared to articulate how your technical work directly impacts business outcomes. Interviewers favor candidates who prioritize practical utility over theoretical complexity.
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
Reporting a mean over accounts when account revenue is heavy-tailed
When a small number of accounts hold most of the revenue, the sample mean is dominated by whichever of them happens to be in the sample, and the sample variance keeps growing as more data arrives instead of stabilising. In that regime the usual central-limit-based confidence interval understates uncertainty, and a single renewal or a single large account's batch job can flip the sign of a measured effect. The fixes are to pre-register a winsorisation or capping rule before looking at the outcome, to report account counts crossing a threshold alongside the revenue figure, or to define the estimand on a bounded transform. Choosing the cap after seeing the result is a separate and worse problem, because the cap then encodes the answer.
Randomising an experiment at the user level when users share an account
Two problems fire at once. Colleagues in one workspace see each other's work and talk to each other, so a treated user changes the behaviour of a control user in the same account, which violates the no-interference assumption and biases the estimate toward zero. Separately, outcomes within an account are strongly correlated, so the effective sample size is roughly n / (1 + (m - 1) * rho) for m users per account and intra-class correlation rho, not n. With rho around 0.3 and twenty users per account that is a design effect near 6.7, meaning a user-level confidence interval is about two and a half times narrower than it should be and results cross significance thresholds on noise alone. Randomise the account and cluster the standard errors.
Building features from data that postdates the prediction time
Check every feature against the timestamp at which the model would actually score, and drop anything computed from a window that includes or follows the label event. For a forecasting use case, split train and test by time rather than at random, and split by entity when the same entity recurs.
Sizing estimates built on unnamed, unrevisable assumptions
Write each assumption as a named number you can change, then show the arithmetic so the interviewer can challenge one input instead of the whole answer. Finish by saying which assumption the result is most sensitive to, which matters more than the point estimate.
Questions & practice
What are the trade-offs between using a Random Forest versus a Gradient Boosting machine?
Medium · machine learning and modelling
Approach
- 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.
- Frame the prediction: the label, the moment of prediction, and the action it triggers.
Follow-up
- How would you choose the decision threshold, and who owns that choice?
- Where could label leakage enter this setup?
How do you validate a model when you have limited historical data?
Medium · machine learning and modelling
Approach
- Pick an evaluation metric that matches the cost of each error type, not a default.
- Say how the offline result would be validated online before it is trusted.
- Set a baseline first, so any model has something honest to beat.
Follow-up
- What would you monitor after launch to know the model is still valid?
- Where could label leakage enter this setup?
Permutation-test a consumption experiment randomised at account level
Hard · permutation-test · heavy-tails · cuped · experiment-readout
An experiment randomised 900 accounts into two arms. You have one row per account: account_id, arm, consumption_28d (billable units after launch) and consumption_pre (the 28 days before). Consumption is heavy-tailed and the largest account is several percent of the total. Write a permutation test from scratch: winsorise at the pooled 99th percentile as a pre-registered rule, use the difference in arm means of the winsorised outcome as the statistic, and obtain a two-sided p-value from 20,000 relabellings of the account-level arm vector. Report the observed effect, the p-value, and the same test on a CUPED-adjusted outcome.
Approach
- Be precise about what the permutation test needs. Under the sharp null of no effect for any account, the outcomes are exchangeable across arm labels, and the test is valid for ANY statistic T(outcomes, labels) provided the identical function is applied to the observed labels and to all 20,000 relabellings. The pooled 99th percentile is a function of the outcome vector alone, so recomputing it inside the loop returns the same number 20,000 times: that is wasted CPU, not a bias, and hoisting it out is an optimisation rather than a correctness fix. Say plainly that capping at all changes the estimand from mean consumption to mean capped consumption; it is not a neutral cleaning step.
- The mistake that does invalidate the test is an asymmetry between the observed statistic and the permuted ones, and the easiest way to create it is to derive the cleaning rule from the observed arm labels and then freeze it — winsorise each arm at its own observed 99th percentile, hold those two caps fixed, and permute. The observed value is then computed with caps matched to its own partition while every relabelling is scored with caps belonging to a different one, so the null distribution no longer answers the question the p-value claims to answer. A per-arm cap recomputed consistently inside every permutation is a valid test, but it estimates a contrast whose two sides are capped at different thresholds, so prefer the pooled cap on estimand grounds and pre-register it.
- Permute the account-level arm vector, because the account is the randomisation unit. Relabelling anything finer — users, workspaces, requests — generates a null distribution narrower than the design actually supports and returns p-values that are anti-conservative.
- Vectorise the null: tile the treatment indicator into a (B, n) matrix and permute along axis 1 with rng.permuted(..., out=...). The statistic is a difference of means, so the treated sum alone determines it and the whole null is one matrix-vector product. Use the two-sided p-value (1 + count(|stat_perm| >= |stat_obs|)) / (B + 1); the plus-one on each side is not cosmetic, it keeps the p-value away from exactly zero and keeps the test valid at finite B.
- For CUPED, fit theta = cov(y, x) / var(x) on the pooled data and use that same theta for the observed statistic and every relabelling. Pooled theta, like the pooled cap, carries no label information, so where in the loop you compute it is again only a performance question; fitting theta within arms is what goes wrong, because the adjusted outcome then depends on the labels and an observed-label fit frozen across all 20,000 relabellings breaks the match between observed and permuted statistics. x must be measured entirely before launch, which consumption_pre is. Expected variance reduction is about 1 - corr(y, x)^2; measure the achieved reduction from the two null distributions rather than asserting it.
Worked solution (45 min)
- cap_y = np.quantile(df.consumption_28d, 0.99); y = np.minimum(df.consumption_28d.to_numpy(float), cap_y); cap_x = np.quantile(df.consumption_pre, 0.99); x = np.minimum(df.consumption_pre.to_numpy(float), cap_x)
- t = (df.arm == 'treatment').to_numpy(); n1 = int(t.sum()); n0 = len(t) - n1; obs = y[t].mean() - y[~t].mean()
- rng = np.random.default_rng(11); L = np.tile(t.astype(np.int8), (20_000, 1)); rng.permuted(L, axis=1, out=L); s1 = L @ y; stats = s1/n1 - (y.sum() - s1)/n0
- p = (1 + int(np.sum(np.abs(stats) >= abs(obs)))) / (20_000 + 1)
- theta = np.cov(y, x, ddof=1)[0,1] / np.var(x, ddof=1); y_adj = y - theta*(x - x.mean()); repeat steps 2 to 4 on y_adj and compare stats.std(ddof=1) between the two runs.
Expected result: A two-sided p-value strictly between 1/20001 and 1, an observed effect expressed in capped billable units per account, and a CUPED null whose standard deviation is smaller than the unadjusted one by roughly sqrt(1 - corr(y, x)^2). The CUPED point estimate stays close to the unadjusted one, since the adjustment removes variance rather than shifting the effect.
Verify it yourself
- The permutation null is centred near zero: |mean(stats)| is a small fraction of std(stats).
- An A/A run, relabelling arm at random before the test and repeating over seeds, gives roughly uniform p-values with about 5% below 0.05.
- std(stats_cuped) < std(stats) whenever corr(y, x) is materially non-zero, and the ratio matches sqrt(1 - r^2) to within simulation noise.
- Moving the pooled cap computation inside the permutation loop reproduces the same p-value bit for bit at the same seed — the direct demonstration that a pooled cap carries no label information — while setting the cap to infinity reproduces the uncapped test exactly, so the winsorisation switch is independently verifiable.
Follow-up
- The p-value is 0.04 with the cap and 0.31 without it. What do you report, and what did you pre-register?
- Colleagues in a shared workspace can see the treated behaviour. How does that change the design and the estimate?
- How many accounts would you need to detect a 5% lift given this outcome's distribution?
Seven-day activation rate by signup cohort week
Easy · funnel · cohorts · censoring
dim_account carries account_id, created_at, is_internal and is_current; fct_api_request carries account_id, request_at, http_status, api_key_id and traffic_class. Build a signup cohort by the ISO week of created_at over accounts with is_internal = false. An account counts as activated when it issues a request with http_status < 400, a non-null api_key_id and traffic_class <> 'synthetic_monitor' within 168 hours of its own created_at. Return cohort accounts, activated accounts and the rate per week, and exclude any week that has not yet fully elapsed its 168-hour window.
Approach
- Collapse dim_account to one row per account_id before anything else. It is a type 2 dimension, so a plan or status change gives the same account several rows; filtering to is_current = true is the cheapest correct choice here because created_at does not change across versions.
- Express the window as interval arithmetic on the timestamptz column: request_at >= created_at AND request_at < created_at + interval '168 hours'. A date-difference of 7 days is a different and wrong condition for accounts created mid-day.
- Test activation with EXISTS rather than a join to MIN(request_at). EXISTS short-circuits, keeps the cohort at one row per account, and cannot fan out.
- Aggregate by date_trunc('week', created_at AT TIME ZONE 'UTC'), counting accounts and activated accounts, and divide as a ratio of counts.
- Drop unreportable weeks: the last account in a cohort week is created just under week_start + 7 days, so the week is only complete once now() >= week_start + interval '14 days'. Without that filter the newest week always looks like a regression.
Follow-up
- The median time-to-first-successful-call is more informative than a fixed-window rate. Why can you not compute it from this query, and what estimator does it need?
- How would you separate accounts that never called from accounts that called and got only 4xx responses, and which of those is a product problem?
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
- 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.
- 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.
- 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.
- 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.
- 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)
- Build the filtered week: interactive rows with user_id IS NOT NULL inside [week_start, week_start + 7 days).
- 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').
- Add session_seq = SUM(is_new_session::int) OVER (PARTITION BY account_id, user_id ORDER BY request_at ROWS UNBOUNDED PRECEDING).
- Aggregate to sessions by (account_id, user_id, session_seq) taking min, max and count, with duration_minutes = EXTRACT(EPOCH FROM max - min) / 60.
- 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?
We are seeing a drop in user retention; how would you design a study to identify the cause?
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- Fix the population and the time window before naming any metric.
- 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?
Explain the concept of regularization and when you would apply L1 versus L2.
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- Work from the decision backwards to the evidence you would need.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Design a seat-utilisation metric before enforcing licensed seat limits
Medium · seat utilisation · denominator design · service accounts · renewal timing
Seat limits are currently unenforced. Leadership wants a metric to decide whether to enforce them at renewal. You have dim_user_membership (user_id, account_id, role, seat_type, is_service_account, activated_at, deactivated_at, last_seen_at) and fct_subscription_period (account_id, contracted_seats, pricing_model, term_end_date, arr_cents, is_current). Define licensed seat utilisation at the account grain, state every exclusion in both the numerator and the denominator, and say what a value above 1.0 means. Deliver the metric plus the guardrail that protects against enforcement destroying more ARR than it recovers.
Approach
- Numerator: distinct user_id in dim_user_membership with seat_type = 'licensed_paid', is_service_account = false, deactivated_at null and last_seen_at inside a trailing 28 days. Service accounts hold a membership and burn API keys but consume no human attention, so counting them inflates utilisation hardest on the most deeply integrated accounts, which is exactly backwards.
- Denominator: contracted_seats from the fct_subscription_period version current at the evaluation date. Exclude accounts with pricing_model = 'pure_consumption' entirely, because contracted_seats is null there and the ratio is undefined rather than zero. A null coerced to zero produces an infinite ratio that will lead the report.
- Interpret both tails and say what each means commercially. Above 1.0 means the entitlement is not enforced and humans are sharing seats, which is recoverable revenue. Persistently below roughly 0.5 is the most reliable available signal of a seat reduction at renewal, because buyers audit seat counts at renewal and at no other moment.
- Guardrail: ARR at risk from enforcement, the sum of arr_cents over accounts with utilisation above 1.0 whose term_end_date falls inside the enforcement window. Enforcing on an account that responds by cutting seats at renewal converts a recoverable overage into a permanent downgrade, and the recovered revenue can be smaller than the loss.
- Report the metric as a distribution with the two tails counted separately and the renewal calendar attached, because no account can act on enforcement until its own term ends. A fleet average of seat utilisation is a number nobody can act on.
Worked solution (20 min)
- Build the current contract per account from fct_subscription_period where is_current = true, keeping contracted_seats, pricing_model, term_end_date and arr_cents, and drop pure_consumption rows.
- Count active humans per account from dim_user_membership under the four filters, using distinct user_id per account_id, which is safe because the table is one row per membership and not one row per human.
- Compute the ratio and bucket accounts into below 0.5, 0.5 to 1.0, and above 1.0, joining arr_cents and term_end_date.
- Report ARR in each bucket and the share of that ARR renewing in the next two quarters.
Expected result: Three utilisation buckets with ARR and renewal timing attached, and a metric definition whose numerator excludes service accounts, guests, free collaborators and deactivated memberships, and whose denominator is contracted_seats from the contract version current at the evaluation date, with pure-consumption accounts excluded rather than counted as zero.
Verify it yourself
- No account with pricing_model = 'pure_consumption' may appear, with a null or an infinite ratio.
- Bucket ARR must sum to the total ARR over included accounts.
- Summing per-account active-user counts across the fleet must be greater than or equal to the fleet-wide count of distinct user_id, because one person can hold memberships in several accounts; exact equality means multi-account users were deduplicated at the wrong grain.
Follow-up
- last_seen_at is null for service accounts by definition. If the is_service_account flag is unreliable, what happens to your numerator, and how would you audit the flag?
- Why a 28-day activity window rather than 7 or 90, and what evidence would change your answer?
Error rate halves while severe support tickets double
Hard · missing data · aggregation bias · reconciliation
The fleet-wide customer-visible error rate fell from 1.8 percent to 0.9 percent, while sev1 and sev2 tickets and reopened_count rose across the same fortnight. Using fct_api_request (account_id, environment, sdk_name, traffic_class, http_status, request_at, billable_units), fct_usage_daily (account_id, sku_code, usage_date, billable_quantity) and fct_support_ticket (account_id, severity, opened_at, reopened_count, linked_incident_id), determine whether reliability improved, and if not, identify precisely which rows are missing and from when. Deliverable: a diagnosis backed by an independent corroborating source.
Approach
- Distrust an improvement that contradicts an independent operational signal. Two sources disagreeing is itself the finding; decide which one is more likely to be broken before explaining either.
- Recompute the rate as the metric tree defines it, per account first and then as the share of accounts above the reliability target. A single global average is dominated by whichever account sends the most traffic, so a fleet number can fall while a quarter of accounts get worse.
- Audit for missingness rather than for badness: count fct_api_request rows per hour split by environment, sdk_name and status class, indexed against the trailing same-hour baseline. A partial ingestion failure shows as a step drop confined to one slice, not a uniform decline.
- Reconcile against a source the request pipeline does not feed, such as implied request volume from fct_usage_daily for the same accounts and dates. If the usage table is flat while request rows fell, rows are missing rather than traffic.
- Test whether the missingness is differential by status, which is the mechanism that fakes an improvement: if 5xx rows are written on a path that stopped while 2xx rows were unaffected, the numerator falls faster than the denominator and the rate drops with nothing improving.
- Close with the affected window, the affected slice and a restated series marked unreliable across that window, rather than a silently patched number.
Follow-up
- The missing rows are unrecoverable. How do you present that fortnight in a series people compare week over week?
- What monitor would have caught this within an hour, and what is its false-positive cost on a normal quiet weekend?
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.
Rigour against a deadline
Nearly every data role forces a trade between the analysis you want and the one that fits the decision window. Prepare a case where you deliberately shipped something less rigorous, named the weakness to the person relying on it, and said what would change your answer. The naming is the part interviewers listen for.
How do you handle imbalanced datasets in a classification problem?
Medium · behavioural and stakeholder questions
Approach
- Name the disagreement or constraint, and how you resolved it with evidence.
- Quantify the outcome, including what you would not claim credit for.
- State the situation in two sentences and spend the rest on your reasoning.
Follow-up
- What did you decide not to do, and why?
- How did you know the outcome was caused by your change?
Describe a time you had to clean a messy, real-world dataset before modeling.
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.
- Close with what you would do differently, concretely.
Follow-up
- How did you know the outcome was caused by your change?
- What did you decide not to do, and why?
Scope an open-ended request to predict account churn
Medium · scoping · churn modelling · operating point
A customer success director asks for a list of accounts about to churn. You know only that the team has six people and that contracts are annual. Available data is fct_subscription_period, fct_usage_daily, fct_api_request, fct_support_ticket and dim_account. Before writing any code, produce the questions you need answered, a proposed definition of about to churn, and the shape of the artefact you would hand back, including the operating point that turns a score into a decision.
Approach
- The interviewer is probing whether you convert a vague request into a decision with a capacity constraint attached. A candidate who starts talking about model families has already failed the exercise.
- Pin the event and the horizon first. Churn is only possible at term_end_date, so the population is accounts renewing in the next 60 to 90 days, not the whole base. Ask explicitly whether contraction and downgrade count as churn or only full non-renewal, because the three have different base rates and different interventions.
- Pin the action and the capacity. Six people times a realistic number of meaningful interventions per week gives k, and k is what the list is ranked to. Evaluate on precision at k rather than a global AUC over accounts that will never be contacted.
- Audit leakage before choosing features. Every feature needs a timestamp proving it existed before the prediction date. A downgrade amendment, a churn reason code, and a ticket opened after the renewal conversation started are all leaks that will make the offline number look excellent and the live list useless.
- Ask for the counterfactual now rather than later. Coverage is assigned deliberately, so without a held-out slice agreed at the start the intervention can never be evaluated, and you will be asked for its impact in nine months regardless.
- Propose the smallest artefact that closes the loop: a weekly ranked list sized to capacity with two or three inspectable reasons per row, plus a stated policy for accounts below the line.
Follow-up
- The director insists all accounts are in scope, not only those renewing soon. How do you answer without simply refusing?
- Historical non-renewals number about 30 a year. At what point do you tell them a model is the wrong tool and a rules list is better?
- Which candidate features would you drop purely because you cannot date them?