At Affirm, the Data Scientist role is at the intersection of financial innovation and high-scale consumer technology. You are not just building models; you are architecting the decision-making engines that power transparent financial products. Your work directly influences core business pillars, including underwriting, fraud detection, and personalized user experiences, ensuring that the company maintains its commitment to honest finance while scaling globally.
The role demands a unique blend of rigor and pragmatism. You will tackle complex problems involving large datasets, such as identifying patterns in transaction logs to mitigate risk or designing scoring systems that evaluate user trust in real-time. Because Affirm operates in a highly regulated industry, your ability to translate technical findings into actionable business insights is as critical as your ability to write clean, efficient code. You will collaborate closely with Engineering, Product, and teams to turn data into a competitive advantage.
Your technical solutions must be robust enough to handle high-frequency data streams while remaining interpretable for stakeholders who rely on your models to make critical business decisions.
Initial Phone Screen
reportedMost candidates lose this call inside the first two minutes, during the walkthrough of their own background. The account runs chronologically, sits at the level of tools and titles, and never arrives at a decision anyone could have disagreed with. Anchor on a problem instead of a timeline: what the team could not answer, what you did about it, what happened next. Ninety seconds is enough, and stopping on time leaves room for the half of the call that belongs to you. What you ask about how work gets prioritised signals your level more reliably than the walkthrough does.
What to demonstrate
- Whether your background summary has a shape (problem, decision, consequence) or is a chronological list of tools and employers
- Whether you can account for gaps, short stints and the reason you are looking, unprompted and without hedging
- The substance of the questions you ask back, which an experienced screener reads as a level signal
How to prepare
- Time your opening walkthrough against a clock. If it runs past two minutes, compress the earliest role into a single clause and spend the recovered time on the most recent one
- Write one honest sentence for every gap or short stint visible on your resume and offer it before being asked about it
- Prepare questions about how work arrives and gets prioritised: who writes the request, how often priorities change, and what happens to an analysis after it is delivered
Technical Deep Dive
reportedA 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
7 candidate reports. Individual accounts describe a particular role and hiring cycle.
Affirm Senior Data Analyst Interview Experience — A Downlevel After Passing the Take-home
In July, a kind younger alum referred me for Full Stack Analyst II. In early August, HR contacted me to say that role was gone and asked whether I'd consider another Senior Analyst role. After that, it was one step each week: HR call -> hiring manager interview -> take-home. The HM case: Analyze what could have caused the delinquency rate to jump in the past quarter. The take-home: What product a…
Read full experienceAffirm Software Engineer interview: finance-style coding and a timed technical screen
My online assessment felt like a finance-specific LeetCode problem. Afterward, I had a 30-minute recruiter screen about the role and logistics, including compensation, the actual work and cultural fit. It wasn't a particularly deep conversation, but it gave me an idea of what they wanted. Next came a technical screen where I shared my screen for a timed coding problem. I only saw the prompt when…
Read full experienceAffirm Software Engineer Interview Experience: payments design and poor recruiter communication
After an initial recruiter call, I entered a fintech-focused process. The roughly 30-minute screen covered my background, interest in fintech and BNPL, compensation, and location. A deeper hiring-manager conversation followed, covering past projects, technical decisions, and fit for the specific team. The coding portion was a 60-minute HackerRank session. Next came about an hour of system design…
Read full experienceAffirm Software Engineer Interview Experience — Fraud Detection Coding and a Payment System Design Round
Four rounds total: one hour of coding + half an hour of behavioral questions + one hour of system design + half an hour of behavioral questions. The two behavioral rounds were just the standard questions. Coding round (the prompt was long, but not hard): Part 1: Live Fraud Detector (Debug Existing Function) Description One of Affirm's competitive edges is our ability to do credit underwriting and…
Read full experienceAffirm Software Engineer Interview Experience — Five Onsite Rounds, One Shaky HM Round
View report detailsPracHub editorial advice for the preparation topics above.
Recalibrating an underwriting cutoff on approved and funded applicants only
Rejected applicants have no repayment outcome, and they were rejected because the incumbent model scored them badly, so the missingness depends directly on the outcome being modelled. Reject inference by augmentation or parcelling fills the gap using the incumbent model's own assumptions, which means it can confirm those assumptions but cannot test them. The only genuinely new information about the reject region comes from bureau performance on rejects who borrowed elsewhere, or from a deliberately randomised approval band around the cutoff.
Reading the most recent months of fraud and dispute rates as final
Consumer dispute rights commonly run around 120 days from the transaction or expected delivery date, and several reason codes run considerably longer, so the disputes belonging to a recent transaction month have simply not been filed yet. Any chart attributed by transaction date therefore slopes down at the right edge regardless of what is happening. The fix is to report only matured cohorts, or to apply development factors estimated from completed months and to show the estimate as an estimate.
Answering a product-sense question with a list of features
Answer with a decision and the measurement that would settle it: the hypothesis, the primary metric, the guardrails, and the result that would make you not ship. A feature brainstorm cannot be wrong, which is exactly why it earns no points.
Ending an analysis without a recommendation or next step
Close with what you would do and what would change your mind, stated as a condition you can check later. If the evidence is genuinely inconclusive, recommend the specific next measurement and say what it costs in time or exposure.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Build a vintage delinquency table without pivot or unstack
fct_loan_performance_monthly gives loan_id, origination_month, months_on_book, days_past_due, charge_off_flag and restructured_flag. Produce a DataFrame with one row per origination_month and columns for months_on_book 0 through 12, each cell holding the share of that vintage's funded loans that had ever reached 90 or more days past due, or charge-off, by that age. You may not use pivot, pivot_table, crosstab or unstack. Cells for ages a cohort has not yet reached must be NaN rather than zero.
Approach
- Define the per-row indicator as days_past_due >= 90 or charge_off_flag, then take a cumulative maximum of it per loan ordered by months_on_book, because the metric is reached-by-age-m, not in-that-state-at-age-m.
- Deal with restructuring before the cumulative max. Restructuring resets days_past_due, so a restructured loan re-enters at current and, without the cumulative maximum carrying its pre-restructure worst state, reads as a cure.
- Fix the denominator once as the count of distinct loan_id per origination_month across the whole cohort. Prepaid and charged-off loans stop producing rows, so a denominator recomputed at each age silently shrinks exactly where losses land.
- Aggregate with groupby(['origination_month','months_on_book'])['ever_90'].sum(), then pre-build the output frame indexed by sorted origination months with integer columns 0 to 12 and assign from the grouped Series by .loc on its index.
- Mask cells beyond each cohort's maximum observed months_on_book so an immature cell reads NaN instead of an artificially low rate.
Worked solution 30 min
- Sort by loan_id and months_on_book, build the ever_90 indicator, then apply groupby('loan_id')['ever_90'].cummax().
- Compute cohort_size as the distinct loan_id count per origination_month, before any filtering on age.
- Group the cumulative indicator by origination_month and months_on_book and sum it to get the numerator per cell.
- Create the output frame with the sorted origination months as index and range(0, 13) as columns, fill it from the grouped Series, and divide each row by its cohort_size.
- Compute each cohort's maximum observed months_on_book and set every cell to the right of it to NaN.
Follow-up
- Two adjacent vintages diverge at months_on_book 6. How would you separate seasoning, mix shift and a genuine credit-quality change?
- The three most recent vintages look best on this table. What do you check before saying so?
- How does the table change if charge-off policy moved from 180 to 120 days past due partway through the series?
Simulate false alarms in a merchant chargeback monitoring rule
Baseline matured first-chargeback rate is 12 per 10,000 settled transactions. A monitoring rule alerts when a merchant's observed monthly rate exceeds twice baseline. For monthly settled transaction counts of 500, 2,000, 10,000 and 50,000, simulate the false-alarm probability per merchant-month under the baseline, and the power to detect a merchant whose true rate is 30 per 10,000. Then, for a portfolio of 4,000 merchants split 60, 25, 10 and 5 percent across those four counts, give the expected number of false alarms per month.
Approach
- Recognise the rule is a threshold on an integer count, not on a continuous rate. At n = 500, twice baseline is 24 per 10,000, so the first observable value above it is 2 chargebacks, or 40 per 10,000. Derive the trigger count for every n before simulating anything.
- Draw binomial counts with numpy at p = 0.0012 and take the share at or above the trigger for the false-alarm rate, then repeat at p = 0.0030 for power. Use at least 200,000 draws per cell so a probability near 0.001 has a usable standard error.
- Cross-check every simulated cell against the Poisson approximation with lambda = n*p, which is tight here because p is tiny. A mismatch almost always means the trigger count is off by one.
- Weight the per-merchant false-alarm probabilities by the portfolio mix, and report the share of expected alerts contributed by each size band rather than only the total.
- Close on the operating consequence: a fixed multiplicative threshold is not a constant false-alarm rate across merchant sizes, so either the threshold scales with n or small merchants need a minimum volume before the rule applies.
Follow-up
- How would you set a threshold that holds the false-alarm rate roughly constant across merchant size?
- The rule reads the transaction month, but disputes arrive for up to 120 days afterwards. What does that do to the alert and how would you fix it?
- What does a month of these false alarms cost, and how would you decide whether it is worth paying?
Bootstrap a fraud loss rate that clusters within merchant
You have a per-transaction frame with auth_id, merchant_id, settled_amount_reporting and net_loss_reporting, both already in one reporting currency. Most rows carry zero loss, a few carry large ones, and losses cluster within merchant. Using only numpy's random generator and no resampling helper from any library, write a bootstrap that returns a 95 percent interval for net fraud loss in basis points of settled volume, resampling merchants with replacement and taking all rows belonging to each drawn merchant. Also produce the naive row-level interval and state which you would report.
Approach
- State the estimator before writing it: total net loss divided by total settled volume, times 10,000. It is a ratio of sums, so each replicate recomputes both sums. Averaging per-transaction loss rates instead would weight a five-unit transaction like a five-thousand-unit one.
- Pre-aggregate loss and volume to merchant level once. For a ratio of sums, drawing merchants and taking all their rows is arithmetically identical to drawing merchant-level (loss_sum, volume_sum) pairs, so a replicate becomes one integer draw plus two vectorised sums rather than a groupby inside the loop.
- Draw B replicates of M merchant indices with replacement, where M is the observed merchant count, compute the ratio per replicate, and take the 2.5th and 97.5th percentiles. Say explicitly that this is a percentile interval and that BCa would correct the skew-induced bias if the decision is close.
- Repeat with independent row draws for the naive interval and compare widths on the same replicate count.
- Report the clustered interval. Rows within a merchant share an acceptance profile, a category code and a fraud exposure, so they are not independent, and the row-level interval understates variance by roughly the design effect.
Follow-up
- Your clustered interval is three times wider. How do you explain that to someone who wanted a tighter number?
- One merchant accounts for 40 percent of losses. What does that do to the interval, and what would you do about it?
- How does this change if the question is whether two months differ rather than what this month's rate is?
How would you handle a stream of logs instead of static log files?
How would you handle a stream of logs instead of static log files?
Approach
- Compute rates by summing numerator and denominator separately, never by averaging rates.
- Handle the rows that do not match: a LEFT JOIN with a NULL check is usually the question.
- Say which table is the grain you start from, and join outward from it.
Follow-up
- What breaks if events arrive late or out of order?
- How does the query change if the join becomes one-to-many?
Find a bug in a given dictionary and store PII (personally identifiabl…
Find a bug in a given dictionary and store PII (personally identifiable information) in a set.
Approach
- Check whether any join is one-to-many before aggregating, or the sums inflate.
- State the window function and its partition and ordering out loud before writing it.
- Say which table is the grain you start from, and join outward from it.
Follow-up
- What breaks if events arrive late or out of order?
- How does the query change if the join becomes one-to-many?
Given two log files with a schema of <date, user id, order type, amoun…
Given two log files with a schema of <date, user id, order type, amount>, identify user IDs that appear on both days and have at least two unique order types.
Approach
- Say which table is the grain you start from, and join outward from it.
- State the window function and its partition and ordering out loud before writing it.
- 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?
- How does the query change if the join becomes one-to-many?
Attribute chargebacks to transaction month without fanning out
Report the matured first-chargeback rate. Numerator: fct_card_dispute rows whose dispute_stage is first_chargeback, representment, pre_arbitration or arbitration, attributed to the requested_at month of the linked fct_payment_authorization row rather than to opened_at. Denominator: settled authorizations in that same transaction month, meaning settled_at is not null and is_reversal is false. One authorization can carry more than one dispute case. Report only transaction months whose last day is at least 120 days old, and mark the remainder incomplete rather than showing them as low.
Approach
- Compute the denominator from fct_payment_authorization alone, grouped by the requested_at month, before any dispute table is mentioned, so the join cannot touch it.
- Compute the numerator as a separate aggregate: join disputes to their authorization only to recover requested_at, then count dispute rows per transaction month with the stage filter.
- Join the two aggregates on the month key, which keeps the grain at one row per month and makes double counting structurally impossible rather than something you have to remember.
- Apply the maturity gate on the month's last day plus 120 days against the current date, because a transaction on the final day of the month is the least mature one in the cohort.
- Emit an explicit status column of 'final' or 'incomplete' instead of filtering immature months away silently, so a reader cannot mistake absence for zero.
Worked solution 30 min
- CTE denom: SELECT date_trunc('month', requested_at) AS txn_month, COUNT(*) AS settled_txns FROM fct_payment_authorization WHERE settled_at IS NOT NULL AND NOT is_reversal GROUP BY 1.
- CTE numer: join fct_card_dispute to fct_payment_authorization on auth_id, filter dispute_stage to the four stages at or beyond first_chargeback, group by the same txn_month expression, count dispute rows.
- LEFT JOIN numer onto denom on txn_month so months with zero disputes appear as zero rather than disappearing.
- Derive the rate with a numeric cast and a status column driven by (txn_month + interval '1 month - 1 day') + interval '120 days' <= current_date.
Follow-up
- Should the numerator count dispute cases or disputed authorizations? Which one reconciles to the loss line, and which one to the operations queue?
- Some reason codes allow filing well beyond 120 days. How would you estimate the tail on a month you have decided to call final?
- How would you present the incomplete months so that a weekly reader does not read the right-hand slope as an improvement?
Given two event types, underwriting and fraud_flag, determine if an ev…
Given two event types, underwriting and fraud_flag, determine if an event is fraud.
Approach
- Clarify what is being asked and what a complete answer would contain.
- 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?
If a purchase amount falls outside a user's historical range, how do y…
If a purchase amount falls outside a user's historical range, how do you mathematically represent the penalty in your scoring system?
Approach
- Clarify what is being asked and what a complete answer would contain.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Calculate the trust score for users based on new purchases, where scor…
Calculate the trust score for users based on new purchases, where scoring depends on previously seen order types and historical purchase ranges.
Approach
- State your assumptions explicitly before working the problem.
- Work from the decision backwards to the evidence you would need.
- Say what you would check first and why it is the highest-information step.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Success metrics for loosening a fraud decline threshold
A risk team proposes lowering the risk_score cutoff that produces auth_result = 'declined_risk_rule'. Settled volume per active customer is the north star; net fraud loss in basis points of settled volume is the guardrail. The two move in opposite directions by construction. Specify the readout: primary metric, guardrail, the maturity window each is read at, and the decision rule agreed before launch. Show the expected-cost arithmetic that sets the cutoff using an average ticket of 200 units, a 1.5 percent contribution margin, 35 percent recovery on fraud losses, and 12 units of downstream value lost per false decline.
Approach
- Refuse the two-metric framing and convert both sides into one currency. Approving a fraudulent transaction costs the amount net of recovery; declining a good one costs the forgone margin plus the downstream value of the customer's reaction. Decline when p times C_FN exceeds (1 minus p) times C_FP, so the break-even probability is p* = C_FP / (C_FP + C_FN).
- Put the numbers in. C_FN = 200 times (1 minus 0.35) = 130, C_FP = 200 times 0.015 plus 12 = 15, so p* = 15 / 145 = 10.3 percent. Then show the threshold is amount-dependent: at a 2,000 ticket C_FN = 1,300 and C_FP = 42, giving p* = 3.1 percent, so a single global cutoff is already the wrong shape before any tuning starts.
- State the precondition that makes this arithmetic legal: risk_score has to be calibrated, so that a score of 0.10 corresponds to an observed 10 percent fraud rate. A score that only ranks makes p* meaningless. Check the reliability curve before quoting any cutoff to anyone.
- Set the maturity windows separately. Volume is readable within days, fraud loss is not, so the guardrail is read only on transaction months with at least 120 days of dispute maturity and the decision stays open until then, or a leading indicator is agreed in advance with its bias written down.
- Agree the stopping rule before launch in the right units: revert if matured net fraud loss per unit of incremental settled volume exceeds the figure implied by p*. Fraud loss is supposed to rise when the cutoff loosens, so a rule that triggers on any rise is a rule that was never going to allow the change.
- Report the swap set rather than portfolio totals: the transactions the new cutoff approves that the old one declined, and their realised loss rate. Portfolio aggregates dilute the change into invisibility.
Worked solution 30 min
- Compute p* at ticket sizes of 50, 200 and 2,000 with the given margin, recovery and false-decline cost, and tabulate them.
- Bucket historical declined_risk_rule authorizations by risk_score decile and, for each bucket, write down what outcome data exists and what does not.
- Write the readout spec: primary metric, guardrail, the 120-day maturity rule, the swap-set table and the numeric stopping rule.
- Write the calibration precondition in two sentences and say how you would test it.
Follow-up
- Fraud loss in basis points falls after launch. Name two ways that happens without any improvement in decisioning.
- How do you keep observing outcomes in the region the rule still declines?
- What changes if the 12 units of downstream value is a guess with no evidence behind it?
Approval rate rose in every band yet fell overall
Monthly application approval rate on fct_loan_application fell from 62 to 57 percent. Cut by bureau_score band, the rate rose in every band, including the null-bureau band. Columns: application_id, channel, submitted_at, requested_amount_minor, declared_annual_income_minor, bureau_score, model_pd_12m, model_version, policy_rule_hits, decision, decided_by, decision_at. The denominator is decision in ('approve','decline'). Explain the arithmetic, quantify how much of the five-point fall is mix versus within-band movement, and say what you would tell the team that owns acquisition.
Approach
- Confirm the paradox is real rather than a banding artefact. Rebuild the bands on fixed cutpoints taken from the earlier period, because quantile bands re-cut each month move with the population and can manufacture this pattern on their own.
- Compute the exact decomposition rather than describing it: within = sum of w_i0 * (r_i1 - r_i0), mix = sum of r_i0 * (w_i1 - w_i0), interaction = sum of (w_i1 - w_i0) * (r_i1 - r_i0). The three terms sum identically to the change in the blended rate, so the report can state the split.
- Attribute the weight change by cutting the same fixed bands by channel. Keep null bureau_score as its own band; a thin file is a population signal, not missing data to be imputed away.
- Check whether the new arrivals also changed the population inside a band, by comparing requested_amount_minor and declared_annual_income_minor distributions within one band across the two months.
- Deliver two numbers rather than one story: policy is looser in every band, and the funnel is being fed a different population. Those have different owners and different fixes.
Follow-up
- If the new channel is profitable at its own approval rate, is the blended fall a problem at all?
- How would you present this so that nobody reads the blended series unaccompanied again?
- What breaks if you fix the mix by reweighting to a frozen band distribution every month?
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.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Build 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.
Practice prompt ↗Practice prompt ↗Practice prompt ↗Worked solution ↗02Joins, 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.
Practice prompt ↗Practice prompt ↗03Window 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.
Practice prompt ↗Practice prompt ↗04The 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Write 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.
Practice prompt ↗Practice prompt ↗06One 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.
Practice prompt ↗Practice prompt ↗07Full 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Most of the questions in this section reduce to one thing: can you be handed a vague request and come back with something useful? Prepare an example where the ask was underspecified, you chose an interpretation, and you said out loud which interpretation you chose. Describing how you narrowed the question matters more than the technique you eventually used.
State honestly what your cutoff change actually contributed
Six months ago your recommendation moved a credit cutoff, using fct_loan_application and fct_loan_performance_monthly. Since then approval rate rose four points and the 12-month vintage 90-plus rate on affected cohorts is flat. In the same window the bureau changed a score attribute, marketing shifted channel mix toward broker, and the internal funding rate moved. Your performance review asks for impact in currency terms. Give the number you would stand behind, the counterfactual it rests on, and the part of the observed movement you would not claim.
Approach
- Define the counterfactual before computing anything: the claim is not what happened after the change, it is what would have happened had the old cutoff scored the same applications, which means replaying the old threshold on the post-change population.
- Build the swap set: applications the new cutoff approves that the old one declined, applications the old one approved that the new one declines, and everyone else held out as unaffected. Only the swap groups carry your effect. Note that the swap-out group has no outcome under the new rule, because those loans were never funded, so its forgone margin must be estimated from matched pre-change approvals rather than observed.
- Price each swap group at months_on_book equal to 12 on the measure the cutoff was meant to move: interest and fees collected, minus net charge-offs, minus funding cost at the internal transfer rate.
- Strip the confounders explicitly. Cohorts affected by the bureau attribute change are either recomputed on the old attribute or excluded; channel mix is held fixed by reweighting to the pre-change mix; funding cost is charged at the rate in force each month rather than one blended average.
- State the residual you will not claim, with its size, and give a range rather than a point wherever cohorts have not yet reached 12 months on book.
Follow-up
- The swap-in group is only 6 percent of applications. How does that change the way you present the number, and to whom?
- What would you have needed to set up at launch to make this attribution clean, and why was a randomised band around the cutoff not used?
Turn a one-line fraud-number request into a scoped brief
A stakeholder messages: what is our fraud rate, and is it going up? You have fct_payment_authorization, fct_card_dispute and dim_customer. At least four defensible answers exist: count-weighted or value-weighted, attributed to the transaction month or to the dispute filing month, and gross or net of recoveries and successful representments. You get one reply before someone else produces an uncaveated number. Write that reply: the clarifying questions you ask, the single default you will produce if nobody answers, and what the default excludes.
Approach
- Establish the decision behind the question first, because a risk-rule change, a board number and a merchant contract negotiation need different denominators, and asking which one is not stalling.
- Offer a short menu rather than an open question: a stakeholder can choose between two named options but cannot specify a denominator from scratch.
- Commit to a default so the reply is useful even if nobody answers, for example net fraud loss in basis points of settled volume, attributed to the requested_at month, matured months only.
- State the exclusions in the same breath as the default: non-fraud dispute categories, transaction months with less than 120 days of maturity, and first-party abuse that arrives coded as consumer_dispute.
- Give a delivery time for the default and a longer one for the fuller cut, so the choice between them carries a visible cost.
Follow-up
- They come back wanting it by merchant for a contract negotiation. What changes in the definition and in the maturity rule?
- How would you separate first-party abuse from third-party fraud in this data, and what would you refuse to conclude from the split?
Allocate one analyst-week across three competing risk requests
Three requests land in the same week and you have one analyst-week. Payments wants a merchant-level decline teardown before a contract renewal in nine days. Credit wants a swap-set analysis on a cutoff change scheduled to ship in six weeks. Insurance wants accident-quarter loss ratios at 12 months development for a reserving review with no fixed date. Each sponsor believes theirs is first, and each has escalated before. Produce the allocation, the reasoning you would say out loud to all three at once, and what you explicitly drop.
Approach
- Score each request on the decision it unblocks rather than on effort or on how loudly it arrived: what changes if it is late, and is that change reversible.
- Separate deadline from value. The nine-day renewal is a hard, irreversible date with a bounded prize; the six-week cutoff has slack but a much larger downside if it ships unmeasured; the reserving number has no date but feeds external reporting, which is its own kind of hard.
- Hunt for the cheap partial in each: a decline teardown restricted to the top merchants by declined value usually answers the contract question at a fraction of the full cut.
- Sequence by hard date first, then by largest irreversible downside, and deliver the trade-off to all three sponsors in one message rather than three, so nobody negotiates privately against a version you told someone else.
- Name what is dropped and who now owns that consequence, in writing, so the trade-off is visible rather than silently absorbed by you.
Follow-up
- The credit sponsor escalates to your manager. What do you change, and what do you refuse to change?
- How would you make this allocation reproducible so the next contested week is a rule application rather than a negotiation?
- 01
Six months ago your recommendation moved a credit cutoff, using fct_loan_application and fct_loan_performance_monthly. Since then approval rate rose four points and the 12-month vintage 90-plus rate on affected cohorts is flat. In the same window the bureau changed a score attribute, marketing shifted channel mix toward broker, and the internal funding rate moved. Your performance review asks for impact in currency terms. Give the number you would stand behind, the counterfactual it rests on, and the part of the observed movement you would not claim.
- 02
A stakeholder messages: what is our fraud rate, and is it going up? You have fct_payment_authorization, fct_card_dispute and dim_customer. At least four defensible answers exist: count-weighted or value-weighted, attributed to the transaction month or to the dispute filing month, and gross or net of recoveries and successful representments. You get one reply before someone else produces an uncaveated number. Write that reply: the clarifying questions you ask, the single default you will produce if nobody answers, and what the default excludes.
- 03
Three requests land in the same week and you have one analyst-week. Payments wants a merchant-level decline teardown before a contract renewal in nine days. Credit wants a swap-set analysis on a cutoff change scheduled to ship in six weeks. Insurance wants accident-quarter loss ratios at 12 months development for a reserving review with no fixed date. Each sponsor believes theirs is first, and each has escalated before. Produce the allocation, the reasoning you would say out loud to all three at once, and what you explicitly drop.
Is this an official Affirm interview guide?
No. It is PracHub's own research and practice material for the Data Scientist role at Affirm. Rounds and questions reflect what candidates have reported, not a process Affirm has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How much time should I spend preparing for the coding portion?
Focus on consistent practice with data manipulation tasks rather than memorizing complex algorithms. Spend at least 2–3 weeks working through problems that involve parsing logs, handling dictionaries, and designing simple scoring logic.
PracHub interview research ↗What differentiates a good candidate from a great one?
Great candidates ask clarifying questions before writing a single line of code. They demonstrate an understanding of the business impact of their code—for example, explaining why a specific fraud threshold might be chosen over another.
PracHub interview research ↗Is the culture at Affirm collaborative?
Yes, the team values cross-functional collaboration. You will be expected to work closely with engineers and product managers, so emphasize your ability to communicate and work within a team during your behavioral rounds.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-22 - 02PracHub Data Scientist practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-22