Off-by-one on range boundaries
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Move from brute-force thinking to an efficient approach using core interview patterns strategy.
Table: subscription_events
+------------------+---------+ | Column Name | Type | +------------------+---------+ | event_id | int | | user_id | int | | event_date | date | | event_type | varchar | | plan_name | varchar | | monthly_amount | decimal | +------------------+---------+ event_id is the unique identifier for this table. event_type can be start, upgrade, downgrade, or cancel. plan_name can be basic, standard, premium, or NULL (when event_type is cancel). monthly_amount represents the monthly subscription cost after this event. For cancel events, monthly_amount is 0.
Write a solution to Find Churn Risk Customers - users who show warning signs before churning. A user is considered churn risk customer if they meet ALL the following criteria:
50% of their historical maximum plan revenue.60 days.Return the result table ordered by days_as_subscriber in descending order, then by user_id in ascending order.
The result format is in the following example.
Example:
Input:
subscription_events table:
+----------+---------+------------+------------+-----------+----------------+ | event_id | user_id | event_date | event_type | plan_name | monthly_amount | +----------+---------+------------+------------+-----------+----------------+ | 1 | 501 | 2024-01-01 | start | premium | 29.99 | | 2 | 501 | 2024-02-15 | downgrade | standard | 19.99 | | 3 | 501 | 2024-03-20 | downgrade | basic | 9.99 | | 4 | 502 | 2024-01-05 | start | standard | 19.99 | | 5 | 502 | 2024-02-10 | upgrade | premium | 29.99 | | 6 | 502 | 2024-03-15 | downgrade | basic | 9.99 | | 7 | 503 | 2024-01-10 | start | basic | 9.99 | | 8 | 503 | 2024-02-20 | upgrade | standard | 19.99 | | 9 | 503 | 2024-03-25 | upgrade | premium | 29.99 | | 10 | 504 | 2024-01-15 | start | premium | 29.99 | | 11 | 504 | 2024-03-01 | downgrade | standard | 19.99 | | 12 | 504 | 2024-03-30 | cancel | NULL | 0.00 | | 13 | 505 | 2024-02-01 | start | basic | 9.99 | | 14 | 505 | 2024-02-28 | upgrade | standard | 19.99 | | 15 | 506 | 2024-01-20 | start | premium | 29.99 | | 16 | 506 | 2024-03-10 | downgrade | basic | 9.99 | +----------+---------+------------+------------+-----------+----------------+
Output:
+----------+--------------+------------------------+-----------------------+--------------------+ | user_id | current_plan | current_monthly_amount | max_historical_amount | days_as_subscriber | +----------+--------------+------------------------+-----------------------+--------------------+ | 501 | basic | 9.99 | 29.99 | 79 | | 502 | basic | 9.99 | 29.99 | 69 | +----------+--------------+------------------------+-----------------------+--------------------+
Explanation:
Result table is ordered by days_as_subscriber DESC, then user_id ASC.
Note: days_as_subscriber is calculated from the first event date to the last event date for each user.
Problem summary: Table: subscription_events +------------------+---------+ | Column Name | Type | +------------------+---------+ | event_id | int | | user_id | int | | event_date | date | | event_type | varchar | | plan_name | varchar | | monthly_amount | decimal | +------------------+---------+ event_id is the unique identifier for this table. event_type can be start, upgrade, downgrade, or cancel. plan_name can be basic, standard, premium, or NULL (when event_type is cancel). monthly_amount represents the monthly subscription cost after this event. For cancel events, monthly_amount is 0. Write a solution to Find Churn Risk Customers - users who show warning signs before churning. A user is considered churn risk customer if they meet ALL the following criteria: Currently have an active subscription (their last event is not cancel). Have performed at least one downgrade in their subscription history.
Start with the most direct exhaustive search. That gives a correctness anchor before optimizing.
Pattern signal: General problem-solving
{"headers":{"subscription_events":["event_id","user_id","event_date","event_type","plan_name","monthly_amount"]},"rows":{"subscription_events":[[1,501,"2024-01-01","start","premium",29.99],[2,501,"2024-02-15","downgrade","standard",19.99],[3,501,"2024-03-20","downgrade","basic",9.99],[4,502,"2024-01-05","start","standard",19.99],[5,502,"2024-02-10","upgrade","premium",29.99],[6,502,"2024-03-15","downgrade","basic",9.99],[7,503,"2024-01-10","start","basic",9.99],[8,503,"2024-02-20","upgrade","standard",19.99],[9,503,"2024-03-25","upgrade","premium",29.99],[10,504,"2024-01-15","start","premium",29.99],[11,504,"2024-03-01","downgrade","standard",19.99],[12,504,"2024-03-30","cancel",null,0.00],[13,505,"2024-02-01","start","basic",9.99],[14,505,"2024-02-28","upgrade","standard",19.99],[15,506,"2024-01-20","start","premium",29.99],[16,506,"2024-03-10","downgrade","basic",9.99]]}}Source-backed implementations are provided below for direct study and interview prep.
// Accepted solution for LeetCode #3716: Find Churn Risk Customers
// Auto-generated Java example from py.
class Solution {
public void exampleSolution() {
}
}
// Reference (py):
// # Accepted solution for LeetCode #3716: Find Churn Risk Customers
// import pandas as pd
//
//
// def find_churn_risk_customers(subscription_events: pd.DataFrame) -> pd.DataFrame:
// subscription_events["event_date"] = pd.to_datetime(
// subscription_events["event_date"]
// )
// subscription_events = subscription_events.sort_values(
// ["user_id", "event_date", "event_id"]
// )
// last_events = (
// subscription_events.groupby("user_id")
// .tail(1)[["user_id", "event_type", "plan_name", "monthly_amount"]]
// .rename(
// columns={
// "event_type": "last_event_type",
// "plan_name": "current_plan",
// "monthly_amount": "current_monthly_amount",
// }
// )
// )
//
// agg_df = (
// subscription_events.groupby("user_id")
// .agg(
// start_date=("event_date", "min"),
// last_event_date=("event_date", "max"),
// max_historical_amount=("monthly_amount", "max"),
// downgrade_count=("event_type", lambda x: (x == "downgrade").sum()),
// )
// .reset_index()
// )
//
// merged = pd.merge(agg_df, last_events, on="user_id", how="inner")
// merged["days_as_subscriber"] = (
// merged["last_event_date"] - merged["start_date"]
// ).dt.days
//
// result = merged[
// (merged["last_event_type"] != "cancel")
// & (merged["downgrade_count"] >= 1)
// & (merged["current_monthly_amount"] < 0.5 * merged["max_historical_amount"])
// & (merged["days_as_subscriber"] >= 60)
// ][
// [
// "user_id",
// "current_plan",
// "current_monthly_amount",
// "max_historical_amount",
// "days_as_subscriber",
// ]
// ]
//
// result = result.sort_values(
// ["days_as_subscriber", "user_id"], ascending=[False, True]
// ).reset_index(drop=True)
// return result
// Accepted solution for LeetCode #3716: Find Churn Risk Customers
// Auto-generated Go example from py.
func exampleSolution() {
}
// Reference (py):
// # Accepted solution for LeetCode #3716: Find Churn Risk Customers
// import pandas as pd
//
//
// def find_churn_risk_customers(subscription_events: pd.DataFrame) -> pd.DataFrame:
// subscription_events["event_date"] = pd.to_datetime(
// subscription_events["event_date"]
// )
// subscription_events = subscription_events.sort_values(
// ["user_id", "event_date", "event_id"]
// )
// last_events = (
// subscription_events.groupby("user_id")
// .tail(1)[["user_id", "event_type", "plan_name", "monthly_amount"]]
// .rename(
// columns={
// "event_type": "last_event_type",
// "plan_name": "current_plan",
// "monthly_amount": "current_monthly_amount",
// }
// )
// )
//
// agg_df = (
// subscription_events.groupby("user_id")
// .agg(
// start_date=("event_date", "min"),
// last_event_date=("event_date", "max"),
// max_historical_amount=("monthly_amount", "max"),
// downgrade_count=("event_type", lambda x: (x == "downgrade").sum()),
// )
// .reset_index()
// )
//
// merged = pd.merge(agg_df, last_events, on="user_id", how="inner")
// merged["days_as_subscriber"] = (
// merged["last_event_date"] - merged["start_date"]
// ).dt.days
//
// result = merged[
// (merged["last_event_type"] != "cancel")
// & (merged["downgrade_count"] >= 1)
// & (merged["current_monthly_amount"] < 0.5 * merged["max_historical_amount"])
// & (merged["days_as_subscriber"] >= 60)
// ][
// [
// "user_id",
// "current_plan",
// "current_monthly_amount",
// "max_historical_amount",
// "days_as_subscriber",
// ]
// ]
//
// result = result.sort_values(
// ["days_as_subscriber", "user_id"], ascending=[False, True]
// ).reset_index(drop=True)
// return result
# Accepted solution for LeetCode #3716: Find Churn Risk Customers
import pandas as pd
def find_churn_risk_customers(subscription_events: pd.DataFrame) -> pd.DataFrame:
subscription_events["event_date"] = pd.to_datetime(
subscription_events["event_date"]
)
subscription_events = subscription_events.sort_values(
["user_id", "event_date", "event_id"]
)
last_events = (
subscription_events.groupby("user_id")
.tail(1)[["user_id", "event_type", "plan_name", "monthly_amount"]]
.rename(
columns={
"event_type": "last_event_type",
"plan_name": "current_plan",
"monthly_amount": "current_monthly_amount",
}
)
)
agg_df = (
subscription_events.groupby("user_id")
.agg(
start_date=("event_date", "min"),
last_event_date=("event_date", "max"),
max_historical_amount=("monthly_amount", "max"),
downgrade_count=("event_type", lambda x: (x == "downgrade").sum()),
)
.reset_index()
)
merged = pd.merge(agg_df, last_events, on="user_id", how="inner")
merged["days_as_subscriber"] = (
merged["last_event_date"] - merged["start_date"]
).dt.days
result = merged[
(merged["last_event_type"] != "cancel")
& (merged["downgrade_count"] >= 1)
& (merged["current_monthly_amount"] < 0.5 * merged["max_historical_amount"])
& (merged["days_as_subscriber"] >= 60)
][
[
"user_id",
"current_plan",
"current_monthly_amount",
"max_historical_amount",
"days_as_subscriber",
]
]
result = result.sort_values(
["days_as_subscriber", "user_id"], ascending=[False, True]
).reset_index(drop=True)
return result
// Accepted solution for LeetCode #3716: Find Churn Risk Customers
// Rust example auto-generated from py reference.
// Replace the signature and local types with the exact LeetCode harness for this problem.
impl Solution {
pub fn rust_example() {
// Port the logic from the reference block below.
}
}
// Reference (py):
// # Accepted solution for LeetCode #3716: Find Churn Risk Customers
// import pandas as pd
//
//
// def find_churn_risk_customers(subscription_events: pd.DataFrame) -> pd.DataFrame:
// subscription_events["event_date"] = pd.to_datetime(
// subscription_events["event_date"]
// )
// subscription_events = subscription_events.sort_values(
// ["user_id", "event_date", "event_id"]
// )
// last_events = (
// subscription_events.groupby("user_id")
// .tail(1)[["user_id", "event_type", "plan_name", "monthly_amount"]]
// .rename(
// columns={
// "event_type": "last_event_type",
// "plan_name": "current_plan",
// "monthly_amount": "current_monthly_amount",
// }
// )
// )
//
// agg_df = (
// subscription_events.groupby("user_id")
// .agg(
// start_date=("event_date", "min"),
// last_event_date=("event_date", "max"),
// max_historical_amount=("monthly_amount", "max"),
// downgrade_count=("event_type", lambda x: (x == "downgrade").sum()),
// )
// .reset_index()
// )
//
// merged = pd.merge(agg_df, last_events, on="user_id", how="inner")
// merged["days_as_subscriber"] = (
// merged["last_event_date"] - merged["start_date"]
// ).dt.days
//
// result = merged[
// (merged["last_event_type"] != "cancel")
// & (merged["downgrade_count"] >= 1)
// & (merged["current_monthly_amount"] < 0.5 * merged["max_historical_amount"])
// & (merged["days_as_subscriber"] >= 60)
// ][
// [
// "user_id",
// "current_plan",
// "current_monthly_amount",
// "max_historical_amount",
// "days_as_subscriber",
// ]
// ]
//
// result = result.sort_values(
// ["days_as_subscriber", "user_id"], ascending=[False, True]
// ).reset_index(drop=True)
// return result
// Accepted solution for LeetCode #3716: Find Churn Risk Customers
// Auto-generated TypeScript example from py.
function exampleSolution(): void {
}
// Reference (py):
// # Accepted solution for LeetCode #3716: Find Churn Risk Customers
// import pandas as pd
//
//
// def find_churn_risk_customers(subscription_events: pd.DataFrame) -> pd.DataFrame:
// subscription_events["event_date"] = pd.to_datetime(
// subscription_events["event_date"]
// )
// subscription_events = subscription_events.sort_values(
// ["user_id", "event_date", "event_id"]
// )
// last_events = (
// subscription_events.groupby("user_id")
// .tail(1)[["user_id", "event_type", "plan_name", "monthly_amount"]]
// .rename(
// columns={
// "event_type": "last_event_type",
// "plan_name": "current_plan",
// "monthly_amount": "current_monthly_amount",
// }
// )
// )
//
// agg_df = (
// subscription_events.groupby("user_id")
// .agg(
// start_date=("event_date", "min"),
// last_event_date=("event_date", "max"),
// max_historical_amount=("monthly_amount", "max"),
// downgrade_count=("event_type", lambda x: (x == "downgrade").sum()),
// )
// .reset_index()
// )
//
// merged = pd.merge(agg_df, last_events, on="user_id", how="inner")
// merged["days_as_subscriber"] = (
// merged["last_event_date"] - merged["start_date"]
// ).dt.days
//
// result = merged[
// (merged["last_event_type"] != "cancel")
// & (merged["downgrade_count"] >= 1)
// & (merged["current_monthly_amount"] < 0.5 * merged["max_historical_amount"])
// & (merged["days_as_subscriber"] >= 60)
// ][
// [
// "user_id",
// "current_plan",
// "current_monthly_amount",
// "max_historical_amount",
// "days_as_subscriber",
// ]
// ]
//
// result = result.sort_values(
// ["days_as_subscriber", "user_id"], ascending=[False, True]
// ).reset_index(drop=True)
// return result
Use this to step through a reusable interview workflow for this problem.
Two nested loops check every pair or subarray. The outer loop fixes a starting point, the inner loop extends or searches. For n elements this gives up to n²/2 operations. No extra space, but the quadratic time is prohibitive for large inputs.
Most array problems have an O(n²) brute force (nested loops) and an O(n) optimal (single pass with clever state tracking). The key is identifying what information to maintain as you scan: a running max, a prefix sum, a hash map of seen values, or two pointers.
Review these before coding to avoid predictable interview regressions.
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.