← Upstart Interview Insights

Upstart·Data Scientist·Technical Phone Screen·Intermediate

Intermediate
Jul 2026

Summary

Technical phone screen for a Data Scientist role at Upstart. Two coding problems back to back, one simulation-heavy and one pure math/algorithms. Nothing too wild but the trailing zeros question tripped me up more than I expected.

Questions Asked (3)

Q1

Write a Python function to simulate radioactive decay with a half-life of one day. The function takes the number of days and number of atoms as inputs and returns the survival state of each atom. Also discuss how you'd verify the result against the theoretical distribution.

Algorithms & Data StructuresTechnical Trade-offs
Author's notes

I went with the straightforward approach: for each atom, flip a coin m times and check if it survived all of them.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by clarifying the problem: each atom independently survives each day with probability 0.5. Then implement a vectorized simulation using NumPy, returning a boolean array indicating survival. Finally, discuss verification by comparing the empirical survival fraction to the theoretical 0.5^days and using statistical tests like the binomial test.

Pro tip: Mention that for large numbers of atoms, a vectorized approach is essential for efficiency, and that you can set a random seed for reproducibility. Also, note that the survival probability is independent of the number of atoms, so you can verify with a simple proportion test.

1. Clarify the problem and assumptions

Confirm that each atom decays independently with probability 0.5 per day, and that the function should return a boolean array indicating survival after the given number of days.

2. Implement the simulation

Use NumPy to generate random numbers and compare them to the survival probability. For each atom, survival after d days means it survives all d days, so the probability is 0.5^d. Alternatively, simulate day-by-day for clarity.

3. Return the survival state

Return a boolean array where True indicates the atom survived (did not decay) after the specified number of days.

4. Verify against theoretical distribution

Compare the empirical survival fraction to the theoretical 0.5^days. Use a binomial test or compute a confidence interval to check if the observed proportion is consistent with the expected probability.

5. Discuss extensions and trade-offs

Mention how to handle large numbers of atoms efficiently, the importance of reproducibility (setting a seed), and potential alternative approaches like using the exponential distribution for continuous time.

Key Points to Mention

  • Independence of each atom's decay
  • Survival probability after d days is (0.5)^d
  • Vectorization with NumPy for performance
  • Setting a random seed for reproducibility
  • Binomial test or proportion test for verification
  • Confidence intervals for the survival fraction

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q2

Implement a factorial function in Python and describe multiple approaches you could use.

Algorithms & Data Structures
Author's notes

Pretty routine.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by clarifying requirements (e.g., input constraints, expected output type, handling of edge cases) and then present multiple approaches: iterative, recursive, and using math.factorial. For each, discuss time/space complexity and trade-offs, and finally provide a clean, efficient implementation with error handling.

Pro tip: Mention that Python's math.factorial is implemented in C and is highly optimized, so in production you'd use it unless you need to demonstrate algorithmic understanding. Also, note that recursion depth limits make recursive approaches impractical for large n.

1. Clarify requirements

Ask about input constraints (e.g., non-negative integer, maximum value), expected output type (int), and how to handle invalid inputs (e.g., negative numbers, non-integers).

2. Present iterative approach

Describe a simple loop multiplying from 1 to n, with O(n) time and O(1) space. Mention it's efficient and avoids recursion limits.

3. Present recursive approach

Explain the recursive definition (n! = n * (n-1)!) with base case 0! = 1. Note O(n) time and O(n) space due to call stack, and Python's recursion limit.

4. Discuss built-in and other methods

Mention math.factorial for production use, and optionally memoization or dynamic programming for repeated calls, though factorial is not typically memoized due to linear growth.

5. Implement and test

Write clean code for the chosen approach, include error handling for negative inputs, and test with edge cases like 0, 1, and a larger number.

Key Points to Mention

  • Time and space complexity of each approach: iterative O(n) time O(1) space, recursive O(n) time O(n) space.
  • Python's recursion limit (default ~1000) and how it affects recursive factorial for large n.
  • Use of math.factorial as the practical choice in production code.
  • Handling edge cases: 0! = 1, negative inputs raise ValueError.
  • Potential for integer overflow in other languages, but Python handles arbitrary precision integers.
  • Trade-offs: readability vs. efficiency, and when to use recursion vs. iteration.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q3

Given an integer n, compute the number of trailing zeros in n! without explicitly calculating the factorial.

Algorithms & Data Structures
Author's notes

I knew the trick involves counting factors of 5 (since 2s are always more abundant), but I second-guessed myself on the formula mid-answer.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Recognize that trailing zeros in n! come from factors of 10, which are determined by pairs of 2 and 5. Since factors of 2 are more abundant, count the number of factors of 5 in n! by summing floor(n/5) + floor(n/25) + floor(n/125) + ... until the divisor exceeds n.

Pro tip: Mention that this is Legendre's formula and that it runs in O(log n) time, which is optimal. Also, clarify that you're counting multiples of 5, 25, etc., because each contributes at least one factor of 5, and higher powers contribute additional factors.

1. Understand the problem

Explain that trailing zeros are produced by factors of 10, which require one factor of 2 and one factor of 5. Since 2s are more frequent, the number of trailing zeros equals the number of factors of 5 in n!.

2. Count factors of 5

Use the formula: sum floor(n / 5^i) for i = 1, 2, 3, ... until 5^i > n. This counts multiples of 5, 25, 125, etc., each contributing additional factors of 5.

3. Implement iteratively

Initialize count = 0 and power = 5. While power <= n, add n // power to count and multiply power by 5. Return count.

4. Analyze complexity

The loop runs O(log_5 n) times, which is very efficient. Space complexity is O(1).

5. Test with examples

Verify with small n: n=5 -> 1 zero; n=10 -> 2 zeros; n=25 -> 6 zeros (since 25 contributes two 5s).

Key Points to Mention

  • Trailing zeros are determined by the number of factors of 10, which is min(count of 2s, count of 5s).
  • Factors of 2 are always more plentiful than factors of 5 in n!, so we only need to count factors of 5.
  • Use Legendre's formula: sum_{i=1}^{∞} floor(n / 5^i).
  • The sum can be computed iteratively by dividing n by 5 repeatedly and adding the quotient.
  • Time complexity is O(log n) and space complexity is O(1).
  • Edge cases: n < 5 gives 0 trailing zeros; large n may require handling integer overflow in other languages, but Python handles big integers natively.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.