Katkat

Analyze Returning Borrowers Across Two Days of Logs

AffirmSoftware Engineer · Technical Screen

Last 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:

  1. They have at least one record in day1_logs AND at least one record in day2_logs.
  2. The number of distinct loan_type values across their combined records from both days is at least 2.

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_id is unique per transaction.
  • The function should operate in O(N + M) time where N and M are the lengths of day1_logs and day2_logs respectively (excluding final sorting step of eligible users K 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?