Interview guides · Huuuge Games
Huuuge Games Data Scientist Interview Questions & Guide 2026
Timed data scientist practice for Huuuge Games: short SQL and probability sets run end to end, then scored against a rubric.
The 60-second brief
As a Data Scientist at Huuuge Games, you operate at the intersection of high-stakes gaming analytics and player behavior. Your work influences how millions of users experience Huuuge Games titles, which requires translating complex datasets into actionable product strategies. You will not just be building models; you will be the architect of insights that drive retention, monetization, and feature development across a fast-paced, global gaming portfolio.
Ask early whether the loop includes an asynchronous take-home or a timed live case, because the two are graded on different things. A take-home is read as an artifact: the question you decided to answer, what you did about missing or malformed records, and a conclusion stated plainly enough for someone to act on. A reviewer who cannot rerun your notebook discounts the result whatever score is printed in it. Hold to the stated time box and write down what you would have done with more of it, since the follow-up round is usually a live defence of the same work.
Focus areas
- Audit faucet and sink flows per currency
- Price cosmetics without cannibalising existing sinks
- Design matchmaking tests that survive interference
Overview
The role is highly technical and demands a rigorous, product-oriented mindset. You will collaborate with product managers, marketing teams, and engineers to design experiments, optimize game economies, and diagnose performance shifts in real-time. Whether you are investigating a sudden drop in a core engagement metric or designing the next A/B test for a live-ops event, your contributions feed directly into the company's data-driven decision-making.
The compensation data provided reflects the market competitiveness for Data Scientist roles at Huuuge Games. Candidates should view these ranges as a baseline that accounts for varying levels of seniority and geographic location. Use this information to calibrate your expectations and prepare for transparent discussions about your total rewards package.
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
Randomising players individually for a change that acts on a shared pool.
Matchmaking parameters, queue populations, tradeable-item supply and event leaderboards are shared resources: a treated player is matched against a control player, and treated supply lands in the control arm's market. The treatment leaks across arms, so the measured difference understates or reverses the true effect, and the control arm is no longer a clean counterfactual. Cluster by region and mode, or switchback on time slices with a burn-in long enough to clear carryover in the shared state.
Treating the account as the person, and the calendar week as neutral time.
Reinstalls, multi-platform play and shared accounts split or merge humans relative to player_id, which inflates install counts and deflates retention denominators in ways that correlate with platform and campaign. Meanwhile daily actives spike on release day and decay over the following week, so a week-over-week read aliases the content calendar. Anchor comparisons to time since release or to matched days in the release cycle, and report identity-sensitive metrics with an explicit statement of which identity key was used.
Extrapolating a first-week lift inflated by novelty effects
Plot the treatment effect by days since first exposure instead of quoting one pooled average. A lift that decays toward zero across the test window is behaviour that will not persist, and annualising it produces a forecast that misses by an order of magnitude.
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.
Questions & practice
Sink to faucet ratio per currency from a signed ledger
Easy · ledger accounting · groupby · metric definition
You are given ledger, a pandas DataFrame with one row per signed currency movement: ledger_id, player_id, occurred_at (UTC timestamp), currency_code, delta_amount (signed int64), flow_type in {faucet, sink, transfer, correction}, source_system, is_reversal (bool). Implement sink_to_faucet(ledger) returning one row per (currency_code, week_start) with faucet_units, sink_units and ratio = sink_units / faucet_units. Sink units are reported as positive magnitudes. Rows with flow_type in {transfer, correction} and rows with is_reversal true are excluded from both sides. A currency-week with zero faucet volume must return a null ratio rather than raise or return infinity.
Approach
- Filter first, aggregate second: build one boolean mask for flow_type in ('faucet','sink') and is_reversal == False, apply it once, so the exclusion cannot be applied to one side of the ratio and forgotten on the other.
- Derive week_start by flooring occurred_at to the Monday of its ISO week: occurred_at.dt.normalize() minus pd.to_timedelta(occurred_at.dt.weekday, unit='D'). Avoid dt.isocalendar().week on its own, which drops the year and collapses week 1 of two different years into one bucket.
- Split the aggregate by sign rather than by abs(): faucet_units = sum of delta_amount where flow_type == 'faucet', sink_units = sum of -delta_amount where flow_type == 'sink'. Taking abs() of the whole column hides a faucet row carrying a negative delta, which is a real data bug you want surfaced.
- Pivot to one row per (currency_code, week_start) and divide under a guard so faucet_units == 0 yields NaN, not inf; downstream charts silently drop NaN and silently rescale on inf.
- Run the same aggregate once with the is_reversal filter removed and diff the two results. Any currency whose ratio moves materially is telling you that clawbacks or customer-service corrections are being read as genuine economy flow.
Follow-up
- The definition drops the reversal row but leaves the original entry it undid inside the aggregate. Defend that choice or change it, and say which direction the ratio moves either way.
- The soft currency has held a ratio near 0.8 for six weeks. What do you look at next, and what evidence would let you say the economy is nevertheless fine?
- How would you report this when a single live-ops event contributed 60 percent of the week's faucet volume?
Exact sign-flip test for a paired switchback readout
Hard · switchback · permutation test · cluster randomisation
A queue-tolerance change ran as a switchback: 12 consecutive days, each split into two 12-hour slices, with a fair coin choosing which slice of each day was treated. slices carries day (date), slot in {am, pm}, arm in {treat, control}, and the underlying match rows carry started_at, player_id and a slice key. Drop the first 30 minutes of every slice as burn-in, aggregate matches per active player at slice level, form the 12 within-day differences, and write an exact randomisation test by enumerating every sign pattern. Report the two-sided p-value and the smallest p this design can produce.
Approach
- Apply the burn-in at row level, before any aggregation: drop match rows whose started_at is within 30 minutes of their slice's start. Aggregating first and subtracting the burn-in afterwards gives a different number, because the active-player denominator changes too, not only the match count.
- Collapse to one metric per slice, matches_started / active_players, then pivot to 12 rows with a treat column and a control column and form d_j = treat_j - control_j. The analysis unit must be the day-pair, n = 12, because the day-pair is the thing the coin was flipped on. Testing at match level gives millions of rows and a standard error wrong by more than an order of magnitude.
- Build the null distribution from the design's own randomisation rather than from a distributional assumption. Under the sharp null of no effect on any unit, swapping the labels within a day flips the sign of d_j and changes nothing else, so enumerate all 2^12 = 4096 sign vectors with itertools.product([-1, 1], repeat=12), compute mean(s * d) for each, and take p as the share of patterns whose absolute mean is at least the observed absolute mean.
- Report the resolution alongside the result. The two-sided p cannot fall below 2/4096 = 0.00049, because the observed pattern and its exact mirror always tie for the maximum. A reported p below that is arithmetic, not evidence.
- Name the residual risk honestly. A systematic am-versus-pm difference does not bias this test, since which slot got treated was random, but it inflates the variance of d and costs power. If the observed am-pm gap is large next to the effect, block on slot or extend the run rather than pretending the variance is not there.
Worked solution (35 min)
- Join each match row to its slice, drop rows inside the first 30 minutes of the slice, and aggregate matches and distinct active players per slice.
- Pivot to 12 day rows, compute d_j = treat_j - control_j, and record the observed mean(d).
- Enumerate all 4096 sign vectors, compute mean(s * d) for each, and count how many have absolute value at least the observed one.
- Divide by 4096 for the two-sided p-value and print it next to the design floor of 2/4096.
Expected result: A two-sided p-value computed over all 4096 sign patterns, reported alongside the design's resolution floor of 2/4096 = 0.00049, and an effect estimate stated as the mean of the 12 within-day differences with its slice-level standard error.
Verify it yourself
- The all-plus-one sign vector reproduces the observed mean exactly.
- The enumerated null distribution is symmetric about zero, so the count above any absolute threshold is always even.
- Replacing d with pure noise and repeating returns p-values roughly uniform across draws.
- Swapping the treat and control labels leaves the two-sided p-value unchanged.
Follow-up
- A content patch shipped on day 7 and lifted matches per player in both arms. Does the test survive it, and what would you change about the analysis?
- The point estimate looks real but p = 0.08 across 12 pairs. What is the cheapest way to buy power short of doubling the run length?
- How would you measure whether 30 minutes of burn-in is actually enough, rather than asserting it?
Three integrity checks over a ledger and purchase join
Medium · data quality · join fan-out · reversals
Given ledger (ledger_id, player_id, occurred_at, currency_code, delta_amount, balance_after, is_reversal, reversed_ledger_id, transaction_id) and txns (transaction_id, player_id, purchase_ts, price_usd_net, status), write check_ledger(ledger, txns) returning violations as rule_code, player_id, ledger_id, detail. Rule 1: balance_after that does not equal the prior balance_after plus delta_amount within (player_id, currency_code) ordered by (occurred_at, ledger_id). Rule 2: is_reversal rows whose reversed_ledger_id is missing, points at a different player or currency, or whose delta_amount is not the exact negative of its target. Rule 3: transaction_ids carried on more than one ledger row. For rule 3 also report completed net revenue computed before and after an inner merge of txns onto ledger.
Approach
- Rule 1 is a shifted comparison inside the sorted group: take balance_after.shift() within (player_id, currency_code) ordered by (occurred_at, ledger_id) and flag where balance_after minus the prior value does not equal delta_amount. Compare the first row of each group against delta_amount alone. Keep the arithmetic in int64; a float cast makes large balances differ at the eighth significant digit and floods the rule with phantom violations.
- Rule 2 is a self-merge of the reversal rows back onto the ledger by reversed_ledger_id. Use a left merge so a dangling pointer survives as a null and becomes a violation. An inner merge deletes precisely the rows you were asked to find.
- Split rule 2 into three codes rather than one: missing target, mismatched player or currency, and a delta that is not the exact negative. Collapsing them throws away the diagnostic that tells you whether this is a bad backfill or a bad writer.
- Rule 3 is ledger.groupby('transaction_id').size() over non-null transaction_ids, kept where the count exceeds one. Then compute completed net revenue two ways: from txns alone, and from the same rows after an inner merge onto ledger. The merged figure is inflated by exactly the fan-out multiplicity, and it is the figure that ends up in a revenue deck when someone joins for an item name.
- Return one long frame with a rule_code column rather than three separate frames. A monitor wants counts per rule per day, and a wide shape makes that awkward to chart and awkward to alert on.
Follow-up
- Rule 1 fires on 0.02 percent of rows, all on one currency, all inside one hour. Incident or backfill, and what distinguishes them in the data?
- How do you run rule 1 incrementally each day without re-reading the full history, given that the check is defined over adjacent rows?
- Rule 3 flags a transaction with three grant rows that turns out to be legitimate. What produces several ledger entries per purchase, and how do you whitelist it without blinding the rule?
Explain how you would join user event logs with purchase history to identify high-value players.
Medium · sql and data manipulation
Approach
- Handle the rows that do not match: a LEFT JOIN with a NULL check is usually the question.
- Check whether any join is one-to-many before aggregating, or the sums inflate.
- Compute rates by summing numerator and denominator separately, never by averaging rates.
Follow-up
- How would you verify this result without re-running the same query?
- What breaks if events arrive late or out of order?
How do you use SQL window functions to calculate rolling retention rates?
Medium · sql and data manipulation
Approach
- State the window function and its partition and ordering out loud before writing it.
- Check whether any join is one-to-many before aggregating, or the sums inflate.
- Compute rates by summing numerator and denominator separately, never by averaging rates.
Follow-up
- How would you verify this result without re-running the same query?
- What breaks if events arrive late or out of order?
Find cosmetic items with no recorded spend in ninety days
Medium · anti-join · null semantics · catalog
dim_catalog_item has item_id, config_version, item_class, rarity, available_from, is_current. fct_currency_ledger has item_id (nullable, and most rows are match rewards and daily logins carrying no item), flow_type, occurred_at, is_reversal. Return every current cosmetic that became available more than 90 days ago and has no sink ledger row referencing it in the last 90 days, with its rarity and available_from. Your first draft will probably return zero rows on this data. Explain exactly why, then fix it.
Approach
- Build the candidate set from dim_catalog_item where is_current = true, item_class = 'cosmetic' and available_from < now() - interval '90 days'. Confirm is_current really yields one row per item_id, because the primary key is (item_id, config_version) and two current versions of one item would duplicate the output.
- Build the spend set separately: ledger rows with flow_type = 'sink', occurred_at in the last 90 days, is_reversal = false.
- Diagnose the empty result. Most ledger rows carry a NULL item_id, and x NOT IN (a, b, NULL) expands to x <> a AND x <> b AND x <> NULL, whose last conjunct is NULL. The predicate can therefore be FALSE or NULL but never TRUE, so the query returns nothing no matter what the data says.
- Fix it and say which form you prefer: NOT EXISTS with a correlated predicate, which is NULL-safe by construction and usually plans as an anti-join; a LEFT JOIN with WHERE spend.item_id IS NULL; or NOT IN with an explicit WHERE item_id IS NOT NULL inside the subquery.
- Sense-check the output against the catalog. If almost every cosmetic appears, the sinks are probably recorded against a bundle or container item_id rather than the granted item, and the question needs a different join path through drop_table_id.
Worked solution (25 min)
- Count ledger rows with item_id IS NULL over the window first. That number is the whole explanation for the empty result and is worth having before you start debugging.
- Write the candidate CTE and confirm its row count equals the count of distinct current cosmetic item_ids older than 90 days.
- Write the naive NOT IN version, observe zero rows, then rewrite it as NOT EXISTS.
- Join the survivors back to dim_catalog_item for rarity and available_from, ordering by available_from so the oldest dead stock surfaces first.
Expected result: A non-empty list of current cosmetic item_ids, one row each, with rarity and available_from, covering items live for more than 90 days with no sink ledger entry in that window. The naive NOT IN version of the identical query returns exactly zero rows.
Verify it yourself
- Row count must be strictly less than the count of current cosmetics older than 90 days. Equality means the anti-join predicate never matches anything.
- Spot-check one returned item_id with a direct count over fct_currency_ledger for that item_id in the window and confirm zero sink rows.
- Swapping NOT EXISTS for LEFT JOIN with IS NULL must return an identical set. A difference means a join condition leaked from ON into WHERE.
Follow-up
- The same NOT IN written against fct_iap_transaction.sku_id returns rows normally. What is different about that column, and would you still reach for NOT EXISTS?
- How would you separate an item nobody wants from an item nobody can see, using available_from, available_to and store impression data?
- An item shows zero sinks but non-zero grants from source_system = 'container_open'. Is it unsold, or is it simply not directly purchasable?
If we observe a sudden 5% drop in daily active users, what is your systematic approach to diagnosing the root cause?
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- Name one primary metric, then the guardrail that stops it being gamed.
- Fix the population and the time window before naming any metric.
Follow-up
- Which segment would you cut first, and what would that rule out?
- How would you detect that the metric is being gamed rather than genuinely improving?
How would you design a metric to measure the long-term health of our game economy?
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- State what result would change your recommendation, so the answer is falsifiable.
- Restate the decision this analysis has to support, and who acts on the answer.
Follow-up
- Which segment would you cut first, and what would that rule out?
- How would you detect that the metric is being gamed rather than genuinely improving?
What are the risks of using conversion rate as your primary success metric for a new monetization feature?
Medium · product sense and metrics
Approach
- Restate the decision this analysis has to support, and who acts on the answer.
- Decompose the metric into the rates that drive it, and say which one you would check first.
- Name one primary metric, then the guardrail that stops it being gamed.
Follow-up
- Which segment would you cut first, and what would that rule out?
- How would you detect that the metric is being gamed rather than genuinely improving?
How would you measure the success of a new in-game feature, such as a limited-time tournament?
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- State what result would change your recommendation, so the answer is falsifiable.
- Fix the population and the time window before naming any metric.
Follow-up
- Which segment would you cut first, and what would that rule out?
- How would you detect that the metric is being gamed rather than genuinely improving?
Find a proxy for perceived fairness in a randomised reward table
Medium · proxy metrics · selection bias · drop tables · survivorship
A container SKU (dim_catalog_item.item_class = 'container') grants items from a drop table with a 1.5% legendary rate and a hard guarantee at the 60th open. Leadership asks you to measure whether players find the system fair. Fairness is not in any table. Using fct_currency_ledger (source_system = 'container_open'), fct_session and fct_match_participant, propose two proxy metrics, state the direction and likely size of the bias in each, and say what decision you would refuse to make from proxies alone.
Approach
- Say what fairness is not. It is not the drop rate matching its configuration, and it is not the mean number of opens. Both can be exactly as designed while the experience is resented.
- Build the exposure variable first: per player, the run length of consecutive container_open ledger entries since their last legendary grant. Ground it in the reference distribution, because under a 1.5% rate with a guarantee at the 60th open, expected opens is (1 - 0.985^60)/0.015, about 39.7, the median is 46, and roughly 41% of players reach the guarantee. Reaching pity therefore describes a plurality of the population and is useless as a frustration flag. Use the upper tail of the dry streak instead.
- Proxy one, behavioural: next-24-hour session count and matches played after an open, bucketed by dry-streak length and compared against that same player's trailing 14-day baseline. Bias: it is computed only over players who kept opening containers, so the most alienated players are missing from every bucket and the estimate is attenuated toward zero, with the attenuation growing as streak length grows, which is exactly where you wanted the signal.
- Proxy two, economic: the share of players who stop spending premium currency on containers while continuing to spend it on direct-sale items, within 7 days of a long dry streak. Bias: it cannot separate disappointment from a rational switch to a deterministic purchase, and it is defined only on players who spend at all, a small share of actives, so it reports the sentiment of the group with the least reason to be angry.
- Triangulate with a source that is not behavioural. A sampled survey or a support-contact rate keyed to the same dry-streak buckets has different biases from both proxies, so report all three together rather than choosing the one that agrees with you.
- Draw the line explicitly. These proxies can support flagging a configuration or region whose behavioural response is far worse than the rest, and they cannot support a claim that the system is or is not perceived as fair, nor a change to the drop rate.
Worked solution (35 min)
- Compute the reference distribution for the current table: expected opens, median opens, and the share of players who reach the guarantee, so you know which streak lengths are actually rare.
- Build the dry-streak variable from ordered container_open ledger rows per player.
- Define proxy one as a within-player comparison of the 24 hours after an open against that player's trailing 14-day baseline, bucketed by dry-streak length.
- Define proxy two on premium spend composition within 7 days, containers against direct-sale items.
- Write one sentence per proxy naming the population it silently conditions on and the direction that pushes the estimate.
- Write the sentence that refuses the decision the proxies cannot support.
Expected result: Two proxies with explicit definitions and explicit biases: a within-player post-streak activity change, attenuated because it conditions on players who kept opening; and a premium-spend composition shift, defined only on spenders and unable to separate disappointment from a preference for deterministic purchases. Plus a non-behavioural triangulating source and a stated refusal to tune the drop rate on proxy evidence alone.
Verify it yourself
- Under a 1.5% rate with a guarantee at the 60th open, expected opens is (1 - 0.985^60)/0.015, about 39.7, while the median is 46. The mean sits below the median because the guarantee truncates the long right tail of the geometric. If your reference numbers do not show that ordering, recheck them.
- About 41% of players reach the guarantee, so any proxy that treats reaching pity as the frustration event is describing a plurality of your players.
- Each proxy in your answer names the population it is conditioned on.
Follow-up
- Next-day play falls 6% after a 50-open dry streak. How much of that is the streak, and how much is that a long streak means a long and expensive session that may simply have exhausted a budget?
- The guarantee moves from 60 opens to 40. Which of your proxies can read that change within a week, and what happens to your dry-streak exposure variable when it does?
Recent install cohorts look richer than mature ones
Medium · cohort maturity · refund recognition · revenue definition
Net revenue per install at cohort day 30 reads about 6% higher for install cohorts aged 30 to 45 days than for cohorts aged 120 days or more. Revenue is refund-adjusted using refunds recognised to date. Tables: dim_player (player_id, install_ts, acquisition_channel) and fct_iap_transaction (player_id, purchase_ts, price_usd_net, status, status_changed_at, refund_ts). Marketing wants to credit a creative refresh. Decompose the 6% and say what you would change about the chart.
Approach
- Separate the two clocks the chart conflates. Purchase accrual is capped at cohort day 30 by the metric definition, so it is the same for every cohort. Refund recognition is not capped: a refund lands weeks after its purchase, so a young cohort has observed only part of the refunds its day-30 purchases will eventually attract. The metric is therefore not comparable across cohort ages even though the accrual window is fixed.
- Measure the recognition curve from cohorts old enough to be complete. Using purchases at least 60 days old, compute the share of eventual 60-day refund and chargeback value that had been recognised by each day since purchase. That curve is the instrument for everything that follows.
- Apply the curve to size the bias. If r is the eventual share of net value refunded within 60 days and f is the share of that already recognised for a given cohort, measured net over matured net is (1 minus r times f) divided by (1 minus r). Compute f as a weighted average over the cohort's purchase-age distribution, not at the cohort's own age, because a cohort aged 40 days contains purchases from day 0 to day 30.
- Subtract the maturity term and decompose what is left by acquisition_channel using the standard mix and within-segment split, since a creative refresh usually moves channel mix as well as channel performance.
- Confirm nothing else changed in the pipeline over the comparison span: check store_channel availability, whether any price book or FX conversion changed, and whether platform_fee_usd rates moved, since all three alter price_usd_net without any player behaving differently.
- Fix the chart rather than the number: restrict it to cohorts that have passed a fixed maturity, or hold refunds at a fixed observation lag so every cohort is measured with the same refund window.
Follow-up
- How long must a cohort be to appear on this chart, and how do you answer the executive who wants a read on last week's cohort?
- Would you rather fix the maturity by dropping young cohorts or by using the recognition curve to project them, and what does each choice cost you?
- If the creative refresh genuinely raised revenue per install, how would that look different from what you found here?
Suggested practice checklist
For someone who has spent the last year in notebooks, dashboards or modelling work and has not written raw SQL under time pressure. The first four days rebuild query fluency against a fixture you control and can verify by hand; the last three attach that fluency to the rest of the loop.
Day 1 — Build a fixture you can check answers against
- Create a local Postgres or SQLite database with four tables (users, sessions, events, orders) holding roughly 200 rows you generated yourself, so you know the contents well enough to predict every result.
- Deliberately seed the cases that break queries: a user with no sessions, a session with no events, two orders sharing a timestamp, a NULL in one join key, and one duplicated user row.
- Before writing any SQL, hand-compute five answers on paper (how many users placed at least one order, median orders per ordering user, and three others) and save them as the ground truth for the week.
Deliverable: A one-command seed script plus a text file of five hand-computed answers to grade every later query against.
Day 2 — Joins, filters and NULL semantics
- Answer "which users have no orders" three ways (LEFT JOIN with IS NULL, NOT EXISTS, NOT IN) and confirm that the NOT IN version returns zero rows once the subquery contains a NULL, because the comparison is never TRUE.
- Reproduce the LEFT JOIN that silently collapses to an inner join by putting a right-table predicate in WHERE, then fix it by moving the predicate into the ON clause, and record both row counts.
- Create a fan-out bug on purpose by joining orders to order_items and summing the order total, then correct it with a pre-aggregated subquery and explain in one line which table changed the grain.
Deliverable: One annotated .sql file holding the three join traps, each with the wrong result and the corrected result side by side.
Day 3 — Window functions and frames
- Write three window queries against the fixture: a running order total per user, the rank of each order within its user by value, and the day gap to that user's previous order, then check each against the day-one ground truth.
- Run ROW_NUMBER, RANK and DENSE_RANK over a column containing ties, print all three side by side, and write one sentence on when each is the correct choice.
- Switch one query from the default frame (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which is what you get when ORDER BY is present and no frame is written) to ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, and explain why the output differs only when the ORDER BY column has duplicates.
Deliverable: Three verified window queries plus a short note explaining the RANGE versus ROWS difference in your own words.
Day 4 — The four analytical query patterns
- Write a monthly retention grid: first order month per user, then months-since-first as the column, and verify that month zero equals the cohort size exactly.
- Sessionize the events table under a 30-minute inactivity rule using LAG plus a cumulative sum over a new-session flag.
- Build a four-step funnel that counts distinct users rather than events at each step, and state the rule you applied to a user who reaches step three without ever logging step two.
Deliverable: One file with the retention, sessionization and funnel patterns, each carrying a one-line note on the assumption it bakes in.
Day 5 — Write SQL the way you will have to write it live
- Set a 12-minute timer and solve three medium prompts in a plain editor with no execution and no autocomplete, then run them and tally syntax errors separately from logic errors.
- Narrate one solution aloud while writing it, stating the grain of each intermediate result (one row per user, one row per user-day) before you type its body.
- Rewrite your slowest solution as a CTE chain where every CTE name states its grain, and time yourself re-solving it from blank.
Deliverable: A recording of one narrated solution plus an error tally that separates syntax from logic.
Day 6 — One day for everything that is not SQL
- Write the preconditions of the two-sample t-test from memory, then check them: independent observations, and a difference in means whose sampling distribution is approximately normal, which at large sample sizes follows from the central limit theorem rather than from normality of the raw values.
- Write the difference between an odds ratio from logistic regression and a relative risk, and state the condition under which the two are close (low outcome prevalence).
- Prepare a 90-second answer to "how would you know this model is any good" that names the metric, the baseline you would beat, and the cost of the errors you care about.
Deliverable: One page of notes covering test preconditions, the odds-ratio caveat and the model-quality answer.
Day 7 — Full loop rehearsal
- Run a 45-minute mock with someone willing to interrupt: 20 minutes of SQL, 15 minutes defining a metric, 10 minutes on a past project.
- Re-solve from blank the two queries you were slowest on this week and compare the times against day five.
- Write a five-line answer to "walk me through a project" that puts a number in the first sentence and names the decision the work changed.
Deliverable: Mock feedback notes plus a timed project narrative you can deliver without reading it.
Being wrong in public
A number you shipped turned out to be wrong, and someone had already acted on it. That is one of the most useful stories a data person can carry. What is being scored is how fast you noticed, who you told first, and what you changed in the process so the same class of error could not repeat quietly.
Explain a wide revenue interval to a non-technical executive
Easy · uncertainty communication · heavy tails · bootstrap · decision framing
A 14-day test of a new currency-pack price is done. Net ARPDAU is up 4.1% with a bootstrap 95% interval of -1.8% to +10.3%. Paying conversion is up 0.6 percentage points with a much tighter interval. In both arms the top 1% of payers hold 42% of net revenue. An executive with no statistics background asks whether this is a win, yes or no, and has ten minutes. Give the explanation, the recommendation, and the claim you refuse to make.
Approach
- Answer the decision first, in one sentence, then explain. An executive who waits ninety seconds for a recommendation stops listening to the reasoning.
- Convert the interval into stakes in their units. Translate -1.8% to +10.3% of net ARPDAU into a monthly or annual dollar range at current active player-days, and say which end you would budget against.
- Explain the width with a fact they already believe rather than with sampling theory: 42% of net revenue sits on 1% of accounts, so a handful of large purchases move the average. That is a property of the business, not a defect of the test, and it is why a conversion number of the same length is far tighter.
- Offer the conversion result as the part of the decomposition that does have enough sample, and be explicit that it is a different claim: more people paid, and the size of the average payment is where the uncertainty lives.
- Price more data honestly. The interval narrows roughly with the square root of exposure, so halving its width needs about four times the runtime. Give the calendar date that implies and say whether the decision is worth waiting for.
- Name the claim you will not make, so nobody reports the point estimate as fact after you leave the room.
Follow-up
- The executive suggests capping the top payers and rerunning the numbers to tighten the interval. What do you say?
- Bootstrap intervals can undercover on a very heavy tail. How would you check whether yours is trustworthy at this sample size?
- If the interval had been -0.4% to +1.2%, would your recommendation change, and why?
Defend an unpopular readout on a live-ops event
Medium · stakeholder pushback · revenue maturity · pull-forward · economy health
A two-week live-ops event closed with gross bookings up 18% against the prior week. Your readout says it added no net revenue: summed price_usd_net in fct_iap_transaction over the 28 days spanning the event is flat against a matched window one release cycle earlier, and fct_currency_ledger shows premium-currency sinks falling while faucets from source_system = 'pass_tier' rose. The event owner disputes the readout in a review with their director present. Give the argument you would make in that room, and name the one piece of evidence you would concede.
Approach
- Separate the disputed claim from yours. Gross bookings up 18% is true and you are not contesting it. Restate your finding precisely: net of platform fee and of refunds recognised to date, over a window long enough to contain the pull-forward, the number does not move.
- Present the decomposition rather than the aggregate, because the aggregate is what the room is already arguing about. Split the delta into active player-days, paying conversion, and net spend per converting player-day. If conversion is flat and spend per payer spiked inside the event window then fell below baseline after, that is re-timing, not new demand.
- Evidence the re-timing at player grain: take the player_ids who purchased during the event and chart their purchase timing in the four weeks before and after. A pulled-forward purchase shows as a deficit on the same accounts, not as a smaller cohort.
- Bring the ledger in as the second, independent signal. Premium sinks fell while pass_tier faucets rose, so granted currency was banked rather than spent. Report the sink-to-faucet ratio per currency and the balance percentile curve, and exclude is_reversal rows and cs_grant so a correction cannot masquerade as a faucet.
- State the limit of the design out loud before the owner does. There was no holdout, and a matched window still aliases the release calendar. Say which part of your conclusion is robust to that and which is not.
- Close on a decision and a fix: run the next instance with a region-clustered holdout or a staggered start, so the argument is settled by design rather than by seniority.
Follow-up
- The event owner says the banked currency will be spent next month, so the sink drop is timing too. How would you test that claim rather than argue about it?
- If you could add exactly one instrumentation change before the next event, what would it be and what question does it close?
- What would you have said if the net number had been up 3% with an interval spanning zero?
Disagree with a queue-tolerance change using guardrail data
Medium · guardrails · conditioning on success · switchback · disagreement
A product manager proposes widening matchmaking skill tolerance in one region because p95 matchmaking wait there is 71 seconds against 34 seconds elsewhere. You have fct_match_participant with matchmaking_wait_seconds, skill_rating_before, result, disconnected, server_region and game_mode. Your concern is that the wait percentile is computed only over matches that actually formed, and that wider tolerance raises mismatch and abandon rate. The PM has a deadline and reads your concern as blocking. Make the disagreement in a way that keeps the decision moving, and state the evidence that would change your mind.
Approach
- Agree on the problem in the first sentence. The wait gap is real and worth fixing; the disagreement is about the price of this particular fix, which is a far easier conversation than one that sounds like a veto.
- Name the measurement defect concretely rather than generally. p95 matchmaking wait is an order statistic over matches that formed, so players who gave up in queue are absent from it entirely. A queue that fails outright reports an excellent p95. Pair it with a queue-abandonment rate measured on queue entries, and with match abandon rate defined over result = 'abandon' or disconnected = true.
- Bring a quantity instead of an objection. From current data, bucket matches by absolute skill gap between teams using skill_rating_before and plot abandon rate and disconnect rate per bucket, cut by game_mode and server_region. That converts 'mismatch is bad' into a rate per unit of tolerance, which the PM can trade against seconds of wait.
- Propose the design that settles it, and explain why the obvious one is invalid: queue population is shared, so a treated player is matched against a control player and the treatment leaks across arms. Randomise by region and mode cluster, or switchback on time slices with a burn-in long enough for the queue to clear, and analyse at the randomisation unit.
- State your falsifier explicitly, for example that if abandon rate is flat across the top two skill-gap buckets in this region, you withdraw the objection.
- Give the PM something shippable inside the deadline, such as a bounded tolerance increase in the worst hours only, with the guardrail thresholds pre-registered before launch.
Follow-up
- How long a burn-in would you use for a switchback here, and what in the data tells you it is long enough?
- Queue-abandonment is not in the tables listed. What would you instrument, and what would you do for this decision in the meantime?
- The PM ships it globally without a test. What do you measure afterwards, and what can that measurement honestly support?