← Pinterest Interview Insights

Pinterest·Data Scientist·Technical Phone Screen·Senior

Senior
Jul 2026

Summary

Pinterest data scientist interview that was basically a pandas deep-dive. Four tasks, all coding, all interconnected, and the nested dict stuff at the end was where I started to feel the pressure.

Questions Asked (4)

Q1

Given a DataFrame where each row has a list of event dicts and a nested attrs dict, explode the events into a long-format table with one row per event, extracting fields like event_type, event_ts, amount, and nested attrs fields. No row-level for-loops allowed.

Data ModelingAlgorithms & Data StructuresTechnical Trade-offs
Author's notes

The explode part I got pretty fast.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Use pandas' built-in vectorized operations — specifically pd.DataFrame.explode() to unnest the events list, followed by pd.json_normalize() to flatten both the event dicts and the nested attrs dict into columns. Chain these transformations together to avoid any row-level Python loops, keeping the solution performant and idiomatic.

Pro tip: Mention that json_normalize() accepts a 'meta' parameter to carry along parent-level columns, and that at Pinterest scale you'd benchmark this against an alternative using pd.DataFrame(df['events'].tolist()) + concat, since json_normalize can be slower on very wide nested structures.

1. Understand & Sketch the Schema

Verbally describe the input shape — a DataFrame with a list-of-dicts column (events) and a nested dict column (attrs) — and sketch the desired output schema with columns like event_type, event_ts, amount, and flattened attrs fields. This shows clarity before writing any code.

2. Explode the Events List

Use df.explode('events').reset_index(drop=True) to convert each element of the events list into its own row, preserving all other columns including attrs. Emphasize this is fully vectorized with no Python-level iteration.

3. Normalize the Event Dicts

Apply pd.json_normalize() on the exploded 'events' column to expand each event dict into flat columns (event_type, event_ts, amount, etc.), then pd.concat() the result back with the remaining columns from the exploded DataFrame.

4. Flatten the Nested attrs Dict

Similarly apply pd.json_normalize() on the 'attrs' column, using a separator like '_' to name nested keys (e.g., attrs.location.city → attrs_location_city), and join these columns into the working DataFrame.

5. Handle Edge Cases & Validate

Address nulls/missing keys (e.g., events lists that are None or empty, missing dict fields) using errors='ignore' in json_normalize and fillna strategies. Validate output row count and column dtypes, and discuss memory/performance trade-offs at scale.

Key Points to Mention

  • pd.DataFrame.explode() for vectorized list unnesting without for-loops
  • pd.json_normalize() with 'sep' parameter for flattening nested dicts and the 'meta' parameter for preserving parent columns
  • Handling null/empty lists and missing dict keys gracefully using dropna or fillna before exploding
  • Performance trade-offs: json_normalize vs. manual pd.DataFrame(col.tolist()) + concat at large scale
  • Memory implications of wide DataFrames after flattening deeply nested structures, and when to use lazy evaluation (e.g., Dask or Spark) at Pinterest's data volume
  • Resetting the index after explode to avoid duplicate index values causing subtle bugs in downstream joins

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

Q2

Aggregate the exploded event table into a per-user features table using groupby with named aggregations. For fields like version and beta flag, take the value associated with the user's most recent event.

Product Analytics & MetricsData Modeling
Author's notes

Named aggregations with groupby I use all the time so this felt okay.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by identifying which aggregations are straightforward (counts, sums, means) versus which require special handling for 'most recent' values (version, beta flag). Use pandas groupby with named aggregations (the dictionary-style agg syntax) to cleanly express multiple aggregation functions in a single pass. For recency-based fields, leverage a sort + last or idxmax pattern to capture the value associated with the most recent timestamp.

Pro tip: Using named aggregations (the pd.NamedAgg or dictionary syntax introduced in pandas 0.25+) not only makes your code more readable and maintainable but also signals to interviewers that you write production-quality, self-documenting data pipelines — a key expectation at Pinterest scale.

1. Sort by Timestamp

Before grouping, sort the exploded event table by user_id and event timestamp in ascending order. This ensures that when you use 'last' as an aggregation function, it correctly captures the most recent event's values.

2. Define Standard Aggregations

Identify columns that need simple aggregations (e.g., event_count=('event_id', 'count'), total_sessions=('session_id', 'nunique')) and list them using the named aggregation syntax: df.groupby('user_id').agg(feature_name=('column', 'func')).

3. Handle Recency-Based Fields

For fields like 'version' and 'beta_flag', use 'last' as the aggregation function after sorting by timestamp, e.g., version=('version', 'last'), beta_flag=('beta_flag', 'last'). This picks the value from the user's most recent event row.

4. Combine into a Single groupby Call

Consolidate all named aggregations into one groupby().agg() call to maximize efficiency and avoid multiple passes over the data. This produces a clean, flat per-user features DataFrame with descriptive column names.

5. Validate and Sanity Check

After aggregation, verify row count equals the number of unique users, check for nulls in recency fields, and spot-check a few users against the raw event table to confirm correctness of the 'most recent' logic.

Key Points to Mention

  • Named aggregation syntax (pd.NamedAgg or keyword argument style) for readable, explicit column naming in a single agg() call
  • Pre-sorting the DataFrame by timestamp before groupby to enable correct use of 'last' for recency-based fields
  • Distinction between aggregation strategies: statistical aggregations (count, sum, mean, nunique) vs. recency-based lookups (last, idxmax)
  • Alternative approach for recency fields: using idxmax on the timestamp column and then indexing back into the original DataFrame for more complex scenarios
  • Efficiency considerations — performing aggregation in a single groupby pass rather than multiple merges to handle Pinterest-scale data
  • Post-aggregation validation steps such as checking unique user counts, null rates, and spot-checking recency values against raw events

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

Q3

From the per-user features table, compute a conversion rate by user segment defined by version, beta flag, and dark flag. Return one row per segment with columns for user count, purchaser count, and conversion rate.

Product Analytics & MetricsA/B Testing & Experimentation
Author's notes

Pretty straightforward once the features table exists.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by identifying the grouping dimensions (version, beta_flag, dark_flag) and the key metrics needed (user count, purchaser count, conversion rate) from the per-user features table. Use a GROUP BY on all three segment dimensions and aggregate with COUNT and conditional SUM or COUNT DISTINCT to derive purchasers. Finally, compute conversion rate as the ratio of purchasers to total users per segment.

Pro tip: At Pinterest scale, mention that you'd also want to check for NULL values in the flag columns before grouping, as NULLs can create unexpected segments — and consider whether to COALESCE them or treat them as a distinct segment depending on business context.

1. Understand the Table Schema

Identify the relevant columns: user_id (or equivalent unique identifier), version, beta_flag, dark_flag, and a column indicating purchase behavior (e.g., is_purchaser or a purchase event flag). Clarify what constitutes a 'purchaser' — a binary flag, a count of purchases, or a join to another table.

2. Define the Segmentation Logic

Group rows by the three segment dimensions — version, beta_flag, and dark_flag — to create one row per unique combination. Be explicit that each unique triplet (version, beta_flag, dark_flag) defines a distinct user segment.

3. Aggregate the Metrics

Use COUNT(user_id) for total user count per segment and SUM(is_purchaser) or COUNT(CASE WHEN is_purchaser = 1 THEN user_id END) for purchaser count. Ensure you are counting distinct users if the table could have duplicate user rows.

4. Compute Conversion Rate

Calculate conversion rate as ROUND(purchaser_count * 1.0 / user_count, 4) or as a percentage. Use 1.0 multiplication or CAST to avoid integer division, and handle potential division-by-zero with a NULLIF or CASE guard.

5. Validate and Interpret Results

Sanity-check that segment user counts sum to the total table population and that conversion rates fall within a plausible range (0–1). Briefly interpret which segments show notably higher or lower conversion rates, connecting findings to A/B testing or product decisions.

Key Points to Mention

  • GROUP BY on all three dimensions (version, beta_flag, dark_flag) to define mutually exclusive segments
  • Handling NULL values in flag columns — either COALESCE to a default or treat as a separate segment
  • Avoiding integer division when computing conversion rate (casting to FLOAT or multiplying by 1.0)
  • Using NULLIF(user_count, 0) to guard against division-by-zero in edge-case empty segments
  • Considering COUNT(DISTINCT user_id) if the per-user features table might not be deduplicated at the user level
  • Connecting the conversion rate analysis to experimentation context — e.g., beta and dark flags suggest feature rollout testing, so results feed into A/B test evaluation

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

Q4

Write a robust helper function that safely extracts nested keys from the attrs dict column using vectorized operations. Handle missing keys and None values, and explain in comments why your approach avoids SettingWithCopy issues and preserves vectorization.

Technical Trade-offsData ModelingAlgorithms & Data Structures
Author's notes

This is where I slowed down the most.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by explaining the core challenge: safely navigating nested dictionary structures in a pandas Series/column without triggering SettingWithCopyWarning or breaking vectorization. Design a helper that uses `.apply()` with a robust inner function or leverages `pd.json_normalize` for true vectorization, explicitly handling None, missing keys, and type mismatches at each nesting level. Walk through the implementation with inline comments that justify each design decision.

Pro tip: Mention that at Pinterest scale, even O(n) `.apply()` can be a bottleneck on hundreds of millions of rows — briefly note when you'd escalate to a Spark UDF or use `pd.json_normalize` + column selection as a faster alternative, showing you think beyond the single-machine pandas context.

1. Define the Problem & Constraints

Clarify that `attrs` is a column of dicts (possibly None or malformed), and the goal is to extract a value at an arbitrary key path like `['user', 'location', 'city']`. State upfront that you'll avoid chained indexing and operate on the original DataFrame to prevent SettingWithCopyWarning.

2. Write the Safe Key Extractor

Implement a pure Python helper `safe_get(d, *keys, default=None)` that iterates through the key path using `isinstance` checks and `.get()` at each level, returning the default on any None or missing key. This isolates the null-safety logic and keeps the pandas layer clean.

3. Apply Vectorized Extraction & Avoid SettingWithCopy

Use `df['new_col'] = df['attrs'].apply(lambda x: safe_get(x, 'key1', 'key2'))` directly on the original DataFrame (not a slice), and add a comment explaining that assigning to `df[col]` on the source object avoids the copy ambiguity that triggers the warning. Alternatively, show `df.assign()` for a fully immutable, copy-safe pattern.

4. Discuss True Vectorization Trade-offs

Acknowledge that `.apply()` is row-wise Python and not truly vectorized; explain when `pd.json_normalize(df['attrs'])` followed by column selection is preferable for flat or semi-structured dicts, and why it's faster at scale. Note the trade-off: `json_normalize` requires uniform schema assumptions.

5. Add Robustness & Testing Notes

Mention edge cases to unit-test: None values, empty dicts, wrong types at intermediate keys, and deeply nested missing paths. Briefly note that type annotations and a docstring on `safe_get` make the helper production-ready and reviewable by teammates.

Key Points to Mention

  • SettingWithCopyWarning root cause: chained indexing on a DataFrame slice creates ambiguity about whether you're modifying a copy or the original — using `df.assign()` or direct assignment on the source DataFrame eliminates this.
  • `.apply()` vs true vectorization: `.apply()` iterates in Python and is O(n) with high overhead; `pd.json_normalize` or vectorized string/struct operations (e.g., in Spark or Arrow) are preferable at Pinterest's data scale.
  • Defensive key traversal: use `.get(key, None)` at each nesting level rather than direct `[]` indexing to avoid KeyError and handle None mid-path gracefully.
  • Immutability pattern with `df.assign()`: returns a new DataFrame without mutating the original, making pipelines easier to reason about and avoiding copy-related bugs.
  • Schema variability handling: real-world `attrs` dicts at Pinterest may have inconsistent schemas across rows — the helper should degrade gracefully to a default rather than raising exceptions.
  • Scalability consideration: for very large datasets, mention migrating the logic to a PySpark UDF or using `polars` struct extraction, which are natively vectorized over nested types.

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