cswork / libraryOpen practice workspace

Interview guides · Activision

Activision Data Scientist Interview Questions & Guide 2026

Practice explaining analysis before a Activision data scientist interview: the recommendation, the uncertainty, the follow-ups.

The 60-second brief

A Data Scientist at Activision plays a critical role in shaping the experiences of millions of players worldwide. By leveraging massive datasets generated by blockbuster franchises like Call of Duty, Crash Bandicoot, and Spyro, you will turn raw telemetry into actionable insights that directly influence game design, live-ops, player retention, and monetization strategies. This is not a purely theoretical research role; it is a highly collaborative, product-oriented position where your models and analyses directly impact real-time gaming ecosystems.

Seniority shifts the scope more than the words in the title do. Earlier-career loops mostly check that you execute a well-posed analysis correctly; senior loops check that you can decide which question is worth answering and defend what you chose not to do.

Focus areas

  • Design matchmaking tests that survive interference
  • Diagnose churn by progression stage, not calendar day
  • Audit faucet and sink flows per currency

Overview

At its core, data science at Activision is about understanding player behavior. You will build machine learning models to predict player churn, optimize matchmaking algorithms, detect toxic behavior, and personalize in-game content. The sheer scale of the data—consisting of billions of daily events—requires a deep understanding of scalable computing, robust statistical modeling, and efficient engineering practices.

What makes this role uniquely challenging and rewarding is the bridge you must build between complex data and creative execution. You will regularly collaborate with game producers, product managers, and software engineers. Your ability to translate a complex neural network or a survival analysis model into a clear, strategic recommendation for a game design team is just as important as your technical execution.

The Activision interview process

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

Recruiter Screen

A screening call is a matching exercise run by someone who will not evaluate your statistics. They are checking that the work described on your resume is work you personally did, and that its scope matches the level the role is written for. Logistics get settled in the same half hour so nobody spends an interviewer's afternoon on a mismatch. The answer that fails is the one narrated in the plural. If every sentence is 'we built' and 'the team decided', there is nothing specific to write down about you. Name the piece that was yours, the decision you made inside it, and what changed after.

What to demonstrate

  • Whether the ownership implied by your resume survives one round of follow-up about who actually did which part
  • Whether your described scope (data size, stakeholders, what shipped) matches the seniority the role is written at
  • Whether timeline, location and compensation expectations make the rest of the loop worth scheduling

How to prepare

  • Rewrite your top three resume bullets in the first person singular, each with the decision you made and what moved afterwards, then say them out loud once so the 'we' does not return under pressure
  • Attach one number to each project: the baseline, the change, and the window it was measured over. Where impact was never measured, say that plainly rather than inventing a figure
  • Settle your compensation range before the call and give it as a range with a reason behind it, such as current total comp or a competing timeline, instead of deflecting the question twice

Hiring Manager Conversation

Much of this round runs on your own history, but the manager is not collecting a project list. They are working out what it is like when something goes wrong on your watch: how late the bad news tends to arrive, and whether a number you hand over has been checked by anyone including you. That is why the strongest material is a project where you can describe the part that did not work and what it cost. A result you cannot take full responsibility for, however clean, gives them nothing to trust you with afterwards.

What to demonstrate

  • Whether you volunteer the limits of a result you are proud of, or wait to be pushed onto them
  • How errors surfaced in your past work, and whether you or somebody else found them
  • Whether the scope you claim matches the level of detail you can still produce about it
  • What you did the first time a stakeholder acted on something of yours that turned out to be wrong

How to prepare

  • Rebuild one headline figure from memory down to the join and the filter, so a question about the denominator does not stall the conversation
  • For each project you raise, write the sentence you would say to someone who had already acted on a number that later turned out wrong
  • Mark which parts of a project were yours and which belonged to other people, and state that boundary yourself before anyone asks

Technical Screening

A handful of shapes account for most of what gets asked in this format: a ranking or deduplication inside groups, a running or rolling total, a period-over-period comparison, and a cohort tracked forward over time. Recognising the shape quickly is most of the speed here; deriving it from scratch while a clock runs is where the time goes. Know that a window function keeps every row while a GROUP BY collapses them, and know which one the question needs. If the exercise is in Python instead of SQL, the same shapes arrive as groupby with transform, shift and merge, and the same grain mistakes are available.

What to demonstrate

  • Whether you reach the right construct without a detour, such as ROW_NUMBER over a partition to deduplicate instead of a self-join against a MAX subquery
  • Whether you know what your window frame actually is, since adding ORDER BY inside OVER changes the default frame and silently changes a running total
  • Whether the thing runs. A near-miss that throws an error scores below a plainer query that returns the right rows.

How to prepare

  • Write each of the four shapes once from memory against a small schema and keep the working version somewhere you will reread it: dedupe with ROW_NUMBER, a running total, a month-over-month change with LAG, and a retention table
  • Compute one running total twice on data with tied timestamps, once on the default frame and once with ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, and look at where the two disagree
  • If Python is on the table, rebuild the dedupe and the running total with groupby and cumsum, then assert the two implementations return identical rows

Virtual Onsite Interview

Where a loop ends with a senior leader, that conversation is rarely another skills test. The technical signal already exists by then, so the questions tend to open up: what you would look at first, where a metric you have heard about could mislead, what you would push back on. The decision being made is scope, which in practice means level and how much you would be trusted to own unsupervised. Treating it as a formality is the usual mistake. An open question late in the day is still being scored, and a vague answer reads as someone who has not run anything themselves.

What to demonstrate

  • Whether your view of the business has anything specific behind it, given that you are working only from what is public and are expected to say so
  • Whether the scope of work you describe owning matches the scope of the role, instead of sitting a level below it
  • Whether you can disagree with something concrete and stay useful about it, rather than agreeing with everything said in the room
  • Whether your questions are ones only this person could answer, as opposed to ones the recruiter already covered

How to prepare

  • Build one view you could defend for two minutes using only public information: what the funnel probably looks like, which metric likely drives decisions, and where that metric could mislead. Being wrong for a stated reason survives this round; having no view does not
  • Write down the largest piece of work you have owned from question to decision, who else touched it, and what you decided alone, then check that it reads at the level you are interviewing for
  • Prepare one thing you would want changed if you joined and phrase it as a question rather than a verdict, so it opens a conversation instead of closing one

Where candidates lose points

Reading economy health from average balances.

A faucet change can leave the mean balance flat while the top decile accumulates a stock it has no reason to spend and the median player stays starved, which is the state in which a premium shortcut stops selling. The diagnostic pair is the sink-to-faucet ratio per currency and the full balance percentile curve over time, not the mean. Ledger reversals and customer-service grants must be excluded from both sides or a single large correction will look like a genuine faucet.

Testing revenue with a difference in means on a heavy-tailed spend distribution.

Player spend is approximately log-normal with a long Pareto-like tail, so the variance of the sample mean is dominated by a few accounts and the central limit approximation converges slowly at realistic sample sizes. A t-test on raw ARPDAU can flip sign when one account buys a large currency pack. Winsorising or capping at a pre-registered percentile, or testing conversion and conditional spend separately, fixes the variance problem, but note the cost: capping biases toward zero exactly when the true effect lives in the tail, so state the cap before you look.

Generalising beyond the population the sample actually supports

State the frame the sample was drawn from and where it diverges from the population you want to talk about: time window, platform, geography, opt-in. If a group is excluded from the frame, either weight to known margins or narrow the claim rather than quietly extending it.

Averaging per-user rates to produce a population rate

Decide which quantity you want: the mean of per-user ratios and the ratio of summed numerator to summed denominator are different estimands, and heavy users dominate one but not the other. For a ratio metric, aggregate numerator and denominator separately and use the delta method for its variance.

Questions & practice

What are the advantages and disadvantages of using a random forest versus a gradient boosted decision tree for predicting in-game purchases?

Medium · machine learning and modelling

Approach

  1. Check what information would not exist at prediction time, and exclude it.
  2. Frame the prediction: the label, the moment of prediction, and the action it triggers.
  3. Set a baseline first, so any model has something honest to beat.

Follow-up

  • Where could label leakage enter this setup?
  • How would you choose the decision threshold, and who owns that choice?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Worked solution (20 min)

  1. Apply the single inclusion mask and assert that the filtered frame contains only the two flow types you intend.
  2. Add the week_start column by weekday subtraction and confirm every value is a Monday at 00:00 UTC.
  3. Aggregate faucet and sink magnitudes separately with a groupby on (currency_code, week_start), then join the two results on that key with an outer join so a currency-week with sinks but no faucets survives.
  4. Divide under a where() guard and return the frame sorted by currency_code then week_start.

Expected result: A frame keyed by (currency_code, week_start) carrying faucet_units, sink_units and ratio, where ratio is NaN rather than inf for any week with zero faucet volume, and rows never disappear because one side of the ratio was empty.

Verify it yourself

  • Recompute one currency-week by hand from a filtered slice and confirm both aggregates match to the unit.
  • The row count equals the number of (currency_code, week_start) pairs present after filtering, with no week silently dropped by an inner join.
  • Running with and without the reversal exclusion changes the answer only for currencies that actually carry is_reversal rows.

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?

Sessionise a raw client event stream with idle timeouts

Medium · sessionisation · event streams · vectorisation

You have events: player_id, event_ts (UTC), device_id, app_version, event_name. A session is a run of consecutive events from one player on one device with no gap exceeding 1800 seconds; a device change starts a new session regardless of gap. Produce one row per session with session_id, player_id, device_id, session_start_ts, session_end_ts, duration_seconds, event_count, the app_version observed at session start, and ended_clean, true when the final event_name is 'app_background' or 'app_exit'. Sessions may span midnight. Detect boundaries vectorised rather than looping per player.

Approach

  1. Sort by (player_id, device_id, event_ts), then compute the gap with groupby(['player_id','device_id'])['event_ts'].diff().dt.total_seconds(). The groupby is the whole point: it stops the diff carrying the last event of one player into the first event of the next.
  2. Flag boundaries as is_new = gap.isna() | (gap > 1800). Strictly greater means a gap of exactly 1800 stays inside the session. Either convention is defensible; write down which one you picked, because it moves the session count by a percent or two and nobody notices until two teams disagree.
  3. Assign session_id = is_new.cumsum() over the sorted frame. Because the frame is sorted and is_new is true at every group's first row, one global cumsum already produces ids that never span a player or a device, so no per-group loop is needed.
  4. Aggregate with a single named-agg groupby on session_id: min and max of event_ts, size, first app_version, last event_name. Take first app_version rather than mode, since an in-session update is a real event you want visible, not averaged away.
  5. Compute duration as (session_end_ts - session_start_ts).total_seconds() and state the caveat rather than hiding it: this is the span between observed events, so a crashed client understates the true session by the unobserved tail. ended_clean is what carries that information; do not patch the duration by adding the timeout.

Follow-up

  • Mobile clients buffer events and upload them late and out of order. What breaks in this job, and how do you make a daily re-run idempotent?
  • A player has two devices live at once, so your definition yields two overlapping sessions. Is that the right answer for a concurrency metric, and is it the right answer for sessions per active day?
  • What happens to the session count and to mean duration if the idle timeout drops from 30 minutes to 5?

Explain the difference in time complexity between looking up an element in a Python list versus a Python dictionary.

Medium · sql and data manipulation

Approach

  1. Say which table is the grain you start from, and join outward from it.
  2. Check whether any join is one-to-many before aggregating, or the sums inflate.
  3. Compute rates by summing numerator and denominator separately, never by averaging rates.

Follow-up

  • What breaks if events arrive late or out of order?
  • How would you verify this result without re-running the same query?

Write a SQL query to calculate the rolling 7-day active user count (DAU) from a raw table of player login events.

Medium · sql and data manipulation

Approach

  1. Say which table is the grain you start from, and join outward from it.
  2. Compute rates by summing numerator and denominator separately, never by averaging rates.
  3. Handle the rows that do not match: a LEFT JOIN with a NULL check is usually the question.

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?

Build a churn hazard curve by match ordinal with censoring

Hard · window functions · survival analysis · censoring

dim_player has player_id, install_ts, is_test_account. fct_match_participant has player_id, match_id, started_at, result, is_bot_opponent_match. fct_session has player_id, session_start_ts. Number each player's non-abandoned, non-bot matches by started_at. For ordinals 1 through 20, report the risk set and the hazard: among players whose nth match is at least 14 days old, the share who never started an (n+1)th match and have no session in the 14 days following that nth match. Players whose nth match is more recent than 14 days are censored at n.

Approach

  1. Filter the match stream before numbering it. Onboarding bot matches and abandons otherwise occupy the first few ordinals for most players, which shifts every spike leftward and makes ordinal 1 mean different things for different install cohorts.
  2. Assign the ordinal with ROW_NUMBER() OVER (PARTITION BY player_id ORDER BY started_at, match_id). The match_id tiebreak keeps numbering stable across reruns when two matches share a start timestamp.
  3. Form the risk set at each n as players with match_ordinal = n whose started_at <= now() - interval '14 days'. This censoring filter is the whole exercise. Without it, every player's most recent match is classified as churn purely because the follow-up window has not elapsed, which inflates the hazard, worst at the ordinals where the mass of last-observed matches sits.
  4. Define the event without a second scan of the match table by adding MAX(match_ordinal) OVER (PARTITION BY player_id) as final_ordinal: a player churned at n when n equals final_ordinal. Then add NOT EXISTS against fct_session for a session_start_ts in (nth_match.started_at, +14 days], so a player who kept opening the app but stopped playing matches is not counted as churned.
  5. Aggregate to hazard = churned / risk_set per ordinal, report both columns rather than the ratio alone, and read the curve: expect ordinal 1 to dominate, then look for local maxima where a difficulty step or a progression gate is likely to sit.
  6. Before proposing a cause for a spike, join the churning players at that ordinal to dim_player.current_level and check whether the spike coincides with a specific progression gate rather than with elapsed time.

Worked solution (45 min)

  1. Build the filtered match CTE (non-test players, result <> 'abandon', is_bot_opponent_match = false) and add both ROW_NUMBER as match_ordinal and MAX(match_ordinal) OVER (PARTITION BY player_id) as final_ordinal.
  2. Restrict to match_ordinal <= 20 and started_at <= now() - interval '14 days' to form the risk sets.
  3. Mark the event: match_ordinal = final_ordinal AND NOT EXISTS a session in (started_at, started_at + interval '14 days'].
  4. Group by match_ordinal and output risk_set = COUNT(*) and hazard = AVG(event::int).
  5. Verify the risk set shrinks monotonically, then read the curve for local maxima before writing any interpretation.

Expected result: Twenty rows, ordinals 1 through 20, each with risk_set and hazard in [0,1]. risk_set decreases monotonically in ordinal. Hazard is highest at ordinal 1, since first-match drop-off dominates in this shape of data, then falls away with one or two local spikes.

Verify it yourself

  • risk_set(n+1) must be less than or equal to risk_set(n) * (1 - hazard(n)); a violation means the censoring filter is applied inconsistently across ordinals.
  • The churned counts summed over all ordinals must not exceed the number of distinct players appearing in any risk set, because a player can be an event at most once.
  • If hazard(1) is not the maximum, suspect that abandons or bot matches are still inside the ordinal and are absorbing the real first-match churn.

Follow-up

  • A spike sits at ordinal 8. Name two explanations the curve alone cannot distinguish, and the query that separates them.
  • How does the curve change if you index by calendar days since install instead of by matches played, and which framing do you defend to a designer?
  • Your hazard uses a 14-day inactivity window. What happens to the curve at 7 days and at 30, and how do you choose between them?

How would you define and measure "player toxicity" using telemetry data, and what steps would you recommend to mitigate it?

Medium · product sense and metrics

Approach

  1. Fix the population and the time window before naming any metric.
  2. Name one primary metric, then the guardrail that stops it being gamed.
  3. Decompose the metric into the rates that drive it, and say which one you would check first.

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?

Imagine a game producer wants to know why a specific in-game event underperformed. How would you structure your analysis to answer this question?

Medium · product sense and metrics

Approach

  1. Name one primary metric, then the guardrail that stops it being gamed.
  2. Fix the population and the time window before naming any metric.
  3. 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?

How would you design a model to predict player churn? What metrics would you use to evaluate its performance if the classes are highly imbalanced?

Medium · product sense and metrics

Approach

  1. Fix the population and the time window before naming any metric.
  2. Name one primary metric, then the guardrail that stops it being gamed.
  3. State what result would change your recommendation, so the answer is falsifiable.

Follow-up

  • What would you do if the primary metric and the guardrail moved in opposite directions?
  • How would you detect that the metric is being gamed rather than genuinely improving?

If we observe a sudden drop in daily active users (DAU) for a key title, what data points would you investigate first to identify the root cause?

Medium · product sense and metrics

Approach

  1. Name one primary metric, then the guardrail that stops it being gamed.
  2. Decompose the metric into the rates that drive it, and say which one you would check first.
  3. Restate the decision this analysis has to support, and who acts on the answer.

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?

We want to introduce a new matchmaking system in our multiplayer game. How would you design an A/B test to measure its impact on player engagement?

Medium · a/b testing and experimentation

Approach

  1. State the primary metric and the minimum effect worth shipping, then size the test.
  2. Decide the analysis before seeing data, including how long it runs and when you look.
  3. Name the randomisation unit first; it decides the variance and what the test can detect.

Follow-up

  • What would you conclude if the result is positive but the test is underpowered?
  • What would you do if you could not randomise at all?

Define success for a redesigned daily login reward

Easy · north star · gameability · faucets · metric tree

The team is replacing a flat daily login grant with an escalating seven-day streak that pays soft currency through fct_currency_ledger (flow_type='faucet', source_system='daily_login'). A product manager proposes daily active users as the success metric. You have dim_player, fct_session, fct_match_participant and fct_currency_ledger. Produce a metric tree from the grant to a business outcome, name one primary metric and two guardrails, and explain in writing why daily active users can be moved by this change without the game getting better.

Approach

  1. Separate the mechanism from the outcome. The reward is a faucet, so its first-order effects are an app open and an increase in soft-currency inflow, and neither is value. Write the tree as grant, app open, session, match started, match reaching a terminal result, retained the following week, currency spent on a sink.
  2. Reject daily active users explicitly, because a notification-driven collect-and-close satisfies it. A login reward pays for exactly that behaviour, and it shows up as fct_session rows with matches_started = 0.
  3. Pick daily core-loop players as primary: distinct non-test players per UTC day with at least one fct_match_participant row whose result is not 'abandon'. Collecting a reward and closing the app does not count toward it.
  4. Guardrail one is the sink-to-faucet ratio for currency_code = 'soft' over a trailing 7 days, excluding is_reversal = true and flow_type in ('transfer','correction'). A large new faucet pushes the ratio below 1, balances accumulate, and every soft-priced sink loses its bite.
  5. Guardrail two is the share of sessions with matches_started = 0, cut by platform. This is the direct measurement of the behaviour that daily active users hides.
  6. State the decision rule before launch: ship only if core-loop players rise while the soft sink-to-faucet ratio stays above its pre-launch level minus a pre-registered margin.

Worked solution (20 min)

  1. Write the tree on one line from faucet grant to retained core-loop player, marking which node each candidate metric sits on.
  2. Draft the primary metric definition in full, including the is_test_account = false filter and the exclusion of result = 'abandon'.
  3. Draft both guardrails with exact windows and exclusions: the 7-day soft sink-to-faucet ratio with reversals, transfers and corrections dropped, and the zero-match session share by platform.
  4. Write the one-paragraph gameability argument, whose core is that the cheapest route to more daily active users under this change is a push notification and a collect button.
  5. State the pre-registered ship rule and the rollback threshold.

Expected result: A primary metric of daily core-loop players, two guardrails covering economy inflation (soft sink-to-faucet ratio, trailing 7 days, reversals and corrections excluded) and empty sessions (share of fct_session rows with matches_started = 0, by platform), plus an explicit written rejection of daily active users on the grounds that a collect-and-close session satisfies it.

Verify it yourself

  • Every metric in your tree names a table and a filter. If one exists only as a phrase, it is not yet a metric.
  • Your primary metric cannot be satisfied by a session with matches_started = 0.
  • Your guardrail set contains at least one economy metric, because this change ships new currency and not just a new screen.

Follow-up

  • The streak resets at UTC midnight. What does that do to your day boundary for players whose evening sits either side of it, and which metric shows the damage first?
  • Soft-currency inflow rises 18% and the sink-to-faucet ratio falls from 1.05 to 0.86 while core-loop players are up 2%. What do you recommend?
  • How would you detect accounts that farm the streak and never enter a match?

Revenue per player-day rose while every channel got worse

Hard · mix shift · guardrails · revenue decomposition

Net revenue per active player-day over a trailing 28 days rose from $0.1440 to $0.1656, up 15%, while net revenue per active player-day fell about 4% inside every acquisition_channel. Organic share of player-days went from 60% to 75% as a cross-promotion campaign ended. Using dim_player (player_id, acquisition_channel), fct_session (player_id, session_start_ts) and fct_iap_transaction (player_id, purchase_ts, price_usd_net, status), decompose the rise and state whether monetisation improved.

Approach

  1. Rebuild the metric from segment parts and confirm the rebuild before decomposing. For each channel and period, compute player-days, net revenue and their ratio, then check the player-day-weighted channel ratios reproduce both reported overall figures. A decomposition on a table that does not rebuild the headline attributes arithmetic error to mix.
  2. Apply the two-term split with the weighting stated. Mix effect is the sum over channels of (share_after minus share_before) times ratio_before; within-segment effect is the sum over channels of share_after times (ratio_after minus ratio_before). Name the pairing, because the alternative assigns the interaction term to the other side.
  3. Recognise that the denominator moved for a reason unrelated to monetisation. A cross-promotion campaign ending removes player-days that carried near-zero revenue, so the blended average rises mechanically while nothing about how players monetise improved. Check the absolute player-day counts, not only the shares, to confirm the shift came from the low-monetising channel shrinking rather than the high-monetising one growing.
  4. Run the guardrails attached to this metric rather than stopping at the decomposition. Compute the share of net revenue contributed by the top 1% of payers, and D28 retention of players who have never completed a purchase. A rise in the first alongside a fall in the second means the within-segment decline is a real deterioration of the free experience, not noise.
  5. Decompose the within-segment 4% one level further into paying conversion per player-day and net spend per converting player-day, so the recommendation points at a mechanism. These are different problems with different owners.
  6. State the verdict without hedging: total net revenue and the per-channel ratios are the numbers that answer whether monetisation improved, and both say it did not. The blended metric rose because its denominator shrank where revenue was thinnest.

Follow-up

  • Total net revenue in absolute dollars is the obvious cross-check. What does it do here, and why is it not sufficient on its own?
  • The cross-promotion channel was unprofitable per player-day. Does ending it make this metric a better or a worse measure of the business?
  • What would you put in place so that a denominator change of this size cannot be reported as a monetisation win again?

Suggested practice checklist

For someone who can already write the query and train the model but stalls when asked what to measure or whether a change is worth making. Metric definition and case structure come first; the technical work is kept as maintenance rather than the centre of the week.

Day 1 — Metric anatomy

  • For three products you use daily, write one primary metric, two input metrics that plausibly move it, and one guardrail that would catch a cheap way of moving the primary at the cost of the product.
  • For one of them, specify the metric precisely enough that two analysts would return the same number: numerator, denominator, unit of observation, time window, and how returning and deleted accounts are treated.
  • Pick a ratio metric and write what happens to it when the denominator shrinks for reasons unrelated to the numerator, with a concrete example of that happening.

Deliverable: A one-page metric tree for one product, with the primary metric written as an unambiguous spec.

Day 2 — Diagnosing a drop without guessing

  • Take the prompt "weekly active users fell 8 percent week over week" and write the segmentation plan before proposing any cause: platform, region, tenure cohort, acquisition channel, and whether the movement sits in the numerator or in a changed denominator.
  • List the instrumentation failures that manufacture fake drops (a client release that stopped firing an event, a bot filter change, a shifted date boundary or timezone) and write the query that rules out each one.
  • Rehearse stating the boring explanations first, seasonality and day-of-week composition, before reaching for a product cause.

Deliverable: A drop-diagnosis checklist short enough to recite from memory in under a minute.

Day 3 — Should we build it

  • Take a feature idea and write it as a bet: what you believe is true, what would have to be true for it to pay off, the metric that would confirm it, and the effect size that would justify the engineering cost.
  • Size the opportunity top-down and bottom-up, then reconcile the two numbers in writing instead of quoting whichever is friendlier.
  • Write the counter-metric that would make you kill the feature even if it wins on the primary metric.

Deliverable: A one-page product memo ending in a decision rather than a list of considerations.

Day 4 — The places aggregate numbers lie

  • Construct a Simpson's paradox numerically: two segments where the treatment wins within each segment yet loses overall, and identify the shift in segment weights that causes it.
  • Take a heavy right-tailed quantity such as revenue per user and write why the mean is the wrong summary, which percentile you would report instead, and what a moving mean with a stable median tells you.
  • Write your definition of a session for the product from day one, then name two real behaviours it misclassifies.

Deliverable: One page holding a worked Simpson's paradox table and a session definition with its two known failure cases.

Day 5 — Technical maintenance, aimed at metrics

  • Solve four timed SQL prompts that all end in a ratio metric, so the question of grain stays live in every answer.
  • Compute a 95 percent confidence interval for a proportion on a small sample, and state why the normal approximation is unreliable when either np or n(1 minus p) falls below roughly 10, along with which interval you would use instead.
  • Take one metric from your day-one tree, write the query that computes it correctly, then write the query that computes it wrong in the most plausible way and explain how you would notice.

Deliverable: Four solved prompts plus a matched correct and plausible-wrong query for one metric.

Day 6 — Turning engineering work into data science stories

  • Write three project stories as situation, decision, trade-off, outcome, each carrying one number and one thing you got wrong.
  • For the story you will lead with, prepare an answer to "what would you do differently" that names a decision you made, not a constraint you were handed.
  • Practise the sentence that reframes a systems project as a question project: the question the work answered, ahead of the pipeline it shipped.

Deliverable: Three written stories with the lead story delivered aloud and timed under four minutes.

Day 7 — Mock case and gap list

  • Run a 40-minute mock case with someone playing a product manager who pushes back on your metric choice, and record it.
  • Listen back and mark every moment you proposed a solution before the success metric existed.
  • Rewrite those moments as the question you should have asked, and rehearse the first 90 seconds of the case until scoping comes before solving.

Deliverable: A recorded case plus a rewritten opening 90 seconds.

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.

Why are you interested in applying data science specifically to the gaming industry and Activision's portfolio?

Medium · behavioural and stakeholder questions

Approach

  1. Close with what you would do differently, concretely.
  2. Name the disagreement or constraint, and how you resolved it with evidence.
  3. State the situation in two sentences and spend the rest on your reasoning.

Follow-up

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

Report a contaminated switchback when the ship call is Friday

Hard · switchback · contamination · carryover · decision under uncertainty

A switchback test of a container drop-rate change ran Monday to Sunday on 12-hour slices, randomising the whole region-mode population per slice. On Wednesday a limited-time event went live to everyone, changing both the currency faucet and store traffic for the remaining slices. Treatment and control slices are no longer exchangeable. The change is scheduled to ship Friday and the director wants a number. Give what you say, what you can still salvage from the data, and what you ask for.

Approach

  1. Say the full-week estimate is unusable before saying anything else, and give the reason in one sentence a non-analyst can repeat: the world changed halfway through, so the two halves of the test measure different games and pooling them averages across that difference.
  2. Test salvageability rather than assuming it away. If slice assignment is balanced within the pre-event window and that window holds enough slices for a usable standard error, you have a shorter and noisier but uncontaminated estimate. Report it with the widened interval and an explicit statement of the power you lost. Do not pool across the event boundary, and do not run the post-event slices as a second test with the event as a covariate, because the event was not randomised.
  3. Check carryover independently of contamination, because they fail for different reasons. A drop-rate change leaves players holding containers and currency, so treatment effects persist into the following control slice unless the burn-in exceeds the time it takes a typical player to open what they received. State the burn-in you used and what evidence supports it.
  4. Give the decision a path rather than a blocker. Offer a cluster-randomised staged rollout by region and mode with the guardrail set pre-registered before launch, which gets the change shipped on Friday while still producing a clean estimate.
  5. Fix the cause, not the instance. The live-ops calendar was a discovery here and should be a design input, checked against the planned slice schedule before a test is launched.
  6. Decide in advance what you will do if the director insists on a single number anyway, and prefer publishing the clean partial estimate with its interval over publishing the contaminated one with a caveat.

Follow-up

  • The pre-event window has six slices. What standard error does that give you, and is it worth reporting at all?
  • How would you choose the burn-in length for the rerun, using data rather than judgement?
  • The director ships it anyway on Friday. What do you measure afterwards, and what can that measurement honestly support?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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?