Approach: compute per-user total spend, most recent purchase date, and average days between consecutive purchases. For avg interval: sort by date, take diffs, average per user. For single purchase return NULL (or 0 if preferred). Ignore/null-date rows or treat as missing.Sample input:user_id | amount | occurred_at1 | 10.0 | 2025-01-011 | 20.0 | 2025-01-102 | 5.0 | 2025-02-203 | 15.0 | NULL3 | 5.0 | 2025-03-01Expected output:user_id | total_spend | last_purchase_date | avg_purchase_interval_days1 | 30.0 | 2025-01-10 | 9.02 | 5.0 | 2025-02-20 | NULL3 | 5.0 | 2025-03-01 | NULLSQL (Postgres):sql
WITH cleaned AS (
SELECT user_id, amount, occurred_at::date AS occurred_at
FROM transactions
WHERE occurred_at IS NOT NULL
),
ranked AS (
SELECT
user_id,
amount,
occurred_at,
LAG(occurred_at) OVER (PARTITION BY user_id ORDER BY occurred_at) AS prev_date
FROM cleaned
),
diffs AS (
SELECT
user_id,
amount,
occurred_at,
CASE WHEN prev_date IS NULL THEN NULL ELSE (occurred_at - prev_date) END AS interval_days
FROM ranked
)
SELECT
user_id,
SUM(amount) AS total_spend,
MAX(occurred_at) AS last_purchase_date,
CASE WHEN COUNT(interval_days) = 0 THEN NULL ELSE AVG(interval_days) END AS avg_purchase_interval_days
FROM diffs
GROUP BY user_id;
Pandas:python
import pandas as pd
df = pd.DataFrame(...) # columns: user_id, amount, occurred_at
df['occurred_at'] = pd.to_datetime(df['occurred_at'], errors='coerce')
df = df.dropna(subset=['occurred_at']) # or keep and handle separately
# total and last
agg = df.groupby('user_id').agg(
total_spend=('amount', 'sum'),
last_purchase_date=('occurred_at', 'max')
).reset_index()
# avg interval
df_sorted = df.sort_values(['user_id','occurred_at'])
df_sorted['prev'] = df_sorted.groupby('user_id')['occurred_at'].shift(1)
df_sorted['interval_days'] = (df_sorted['occurred_at'] - df_sorted['prev']).dt.days
avg_interval = df_sorted.groupby('user_id')['interval_days'].mean().reset_index()
result = agg.merge(avg_interval, on='user_id', how='left')
result['interval_days'] = result['interval_days'].where(result['interval_days'].notna(), None)
result.rename(columns={'interval_days':'avg_purchase_interval_days'}, inplace=True)
Edge cases:- Single purchase -> no interval diffs → return NULL (or 0 if business prefers).- Null occurred_at -> excluded from interval calc; include them only if you have rules (e.g., impute).- Duplicate timestamps -> interval can be zero days; included in avg.Complexity: SQL/pandas both O(N log N) dominated by sort per user; grouping linear.