Analyze Returning Borrowers Across Two Days of Logs
AffirmSoftware Engineer · Technical ScreenLast updated
Process two daily loan activity logs to identify users active on both days with diverse loan types, then score them using a configurable trust policy.
Problem Statement
At Affirm, monitoring borrower activity across consecutive days helps detect recurring engagement and credit diversity. You are given two lists of transaction logs representing loan activity on Day 1 and Day 2.
Each log entry is a dictionary with the following structure:
user_id(str): Unique identifier for the borrower.loan_id(str): Unique identifier for the loan transaction.loan_type(str): Category of the loan (e.g.,'pay_in_4','installment','split_pay').amount(float): Principal loan amount.
A user qualifies as an eligible returning borrower if and only if:
- They have at least one record in
day1_logsAND at least one record inday2_logs. - The number of distinct
loan_typevalues across their combined records from both days is at least2.
For every eligible user, construct an aggregate profile dictionary containing:
user_id(str)distinct_loan_types(set of str)total_amount(float, rounded to 2 decimal places)day1_tx_count(int)day2_tx_count(int)
Finally, evaluate each eligible user using an injected trust_policy function: trust_policy(profile) -> float. Return a list of result dictionaries sorted by trust_score in descending order. Break any ties by user_id in ascending lexicographical order.
Interface
from typing import List, Dict, Any, Callable
def evaluate_returning_borrowers(
day1_logs: List[Dict[str, Any]],
day2_logs: List[Dict[str, Any]],
trust_policy: Callable[[Dict[str, Any]], float]
) -> List[Dict[str, Any]]:
pass
Each returned dictionary must contain: user_id, trust_score, and total_amount.
Example
day1_logs = [
{"user_id": "u1", "loan_id": "L1", "loan_type": "installment", "amount": 250.0},
{"user_id": "u2", "loan_id": "L2", "loan_type": "pay_in_4", "amount": 50.0},
]
day2_logs = [
{"user_id": "u1", "loan_id": "L3", "loan_type": "pay_in_4", "amount": 100.0},
{"user_id": "u2", "loan_id": "L4", "loan_type": "pay_in_4", "amount": 75.0},
]
def sample_policy(profile: Dict[str, Any]) -> float:
# e.g., base score on loan type count and total amount
return round(len(profile["distinct_loan_types"]) * 10 + profile["total_amount"] * 0.01, 2)
result = evaluate_returning_borrowers(day1_logs, day2_logs, sample_policy)
# Output:
# [
# {"user_id": "u1", "trust_score": 23.5, "total_amount": 350.0}
# ]
Explanation: u2 appears on both days but only has one distinct loan type (pay_in_4), so u2 is disqualified. u1 appears on both days and has 2 distinct types (installment, pay_in_4), yielding trust_score = 2 * 10 + 350 * 0.01 = 23.5.
Constraints
0 <= len(day1_logs), len(day2_logs) <= 100,000- Loan amounts are positive floats:
0.01 <= amount <= 50,000.0 loan_idis unique per transaction.- The function should operate in
O(N + M)time whereNandMare the lengths ofday1_logsandday2_logsrespectively (excluding final sorting step of eligible usersK log K).
Edge Cases
- Either or both log lists are empty.
- Users with multiple transactions on the same day with duplicate
loan_type. - Floating-point precision differences when summing amounts.
Hints
Hint 1
Aggregate per-user statistics for each day separately using hash maps before identifying the intersection of users.
Hint 2
Combine stats only for users present in both daily sets, checking if `len(distinct_loan_types) >= 2`.
Hint 3
Decouple data aggregation from scoring so that any callable scoring function can be swapped in without modifying data traversal logic.
Follow-up questions
- How would you scale this implementation if the daily logs do not fit into single-machine memory (e.g., using PySpark or SQL)?
- How would you handle deduplication if the logs contain retried or duplicate `loan_id` values across calls?
- If the trust policy itself requires historical data from the past 30 days, how would you restructure the aggregate profile pipeline to keep queries performant?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.