Data Analysis for a Food Tech Startup: Swiggy
Exploratory analysis and customer segmentation on a 10,000-row Swiggy order sample using RFM quartiles, custom delivery-efficiency scoring, and k-means clustering.
- Published
- Updated
- Reading
- 5 min
- Author
- Sudhanva Narayana
Swiggy Data Science Assessment - LinkedIn, MTV Get a Job
This notebook walks through a take-home data science assessment I completed for Swiggy on a 10,000-row sample of customer order history. The goal was to audit data quality, engineer an RFM (Recency, Frequency, Monetary) score weighted by delivery speed per kilometer, and segment customers across both 7-day and 4-week windows using k-means clustering.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
from sklearn.cluster import KMeans
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
Import Dataset and Quick Inspection
dataset_path = './data/SampleAssessment.csv'
df = pd.read_csv(dataset_path)
df.head()
df.tail()
df['first_time'] = pd.to_datetime(df['first_time'])
df['recent_time'] = pd.to_datetime(df['recent_time'])
df.head()
Number of missing values (orders in 7 days and last 4 weeks)
df.isna().sum()
customer_id 0
first_time 0
recent_time 0
no_of_orders 0
orders_7_days 8077
orders_last_4_weeks 5659
amount 0
amt_last_7_days 0
amt_in_last_4_weeks 0
avg_dist_rest 0
avg_del_time 0
dtype: int64
Verify that the amount is zero when there are no orders
(df['amt_last_7_days'] == 0).sum(axis=0)
8078
(df['amt_in_last_4_weeks'] == 0).sum(axis=0)
5660
Description of the subset of dataset with negative restaurant distances
df[df['avg_dist_rest'] < 0].describe().round(2)
Description of the original dataset with negative restaurant distances
df.describe().round(2)
Description of the dataset without negative restaurant distances
df[df['avg_dist_rest'] > 0].describe().round(2)
- Dropping the rows with negative distances barely changes the mean or standard deviation, so we filter them out before running correlation and clustering.
df = df[df['avg_dist_rest'] > 0]
df.describe().round(2)
Correlation
df.corr()
- Across all numeric features, total order count (
no_of_orders) and total spend (amount) show the only strong positive correlation.
plt.figure(figsize=(20,10))
plt.title('Number of orders vs Amount spent', fontsize=26)
plt.xlabel('Number of Orders', fontsize=24)
plt.ylabel('Amount Spent', fontsize=24)
plt.scatter(df['no_of_orders'], df['amount'])
plt.savefig('./plots/orders_vs_amount.png', format='png', dpi=1000)
plt.show()
plt.figure(figsize=(20,10))
plt.title('Distribution of amount', fontsize=26)
plt.ylabel('Amount Spent', fontsize=24)
plt.hist(df['amount'], bins=[0, 10000, 20000, 30000, 40000, 50000, 60000, 70000])
plt.savefig('./plots/distribution_amount.png', format='png', dpi=300)
plt.show()
plt.figure(figsize=(20,10))
plt.title('Distribution of distances', fontsize=26)
plt.ylabel('Average Distances', fontsize=24)
plt.hist(df['avg_dist_rest'])
plt.savefig('./plots/distribution_distances.png', format='png', dpi=300)
plt.show()
plt.figure(figsize=(20,10))
plt.title('Distribution of delivery time', fontsize=26)
plt.ylabel('Average Delivery Time', fontsize=24)
plt.hist(df['avg_del_time'])
plt.savefig('./plots/distribution_delivery.png', format='png', dpi=300)
plt.show()
df.head()
Calculating customer Recency
latest_recent_time = df['recent_time'].max()
df['recency'] = df['recent_time'].apply(lambda x: (latest_recent_time - x).days)
Delivery score
- We define
delivery_scoreas average delivery time divided by average restaurant distance (avg_del_time / avg_dist_rest).
df['delivery_score'] = df['avg_del_time'] / df['avg_dist_rest']
df.head()
quantiles = df.quantile(q=[0.25,0.5,0.75])
quantiles.to_dict()
{'customer_id': {0.25: 335730.75, 0.5: 667378.0, 0.75: 1004865.75},
'no_of_orders': {0.25: 1.0, 0.5: 2.0, 0.75: 7.0},
'orders_7_days': {0.25: 1.0, 0.5: 1.0, 0.75: 2.0},
'orders_last_4_weeks': {0.25: 1.0, 0.5: 2.0, 0.75: 4.0},
'amount': {0.25: 280.0, 0.5: 691.0, 0.75: 2060.75},
'amt_last_7_days': {0.25: 0.0, 0.5: 0.0, 0.75: 0.0},
'amt_in_last_4_weeks': {0.25: 0.0, 0.5: 0.0, 0.75: 401.75},
'avg_dist_rest': {0.25: 1.7, 0.5: 2.4, 0.75: 3.1},
'avg_del_time': {0.25: 26.0, 0.5: 37.0, 0.75: 47.0},
'recency': {0.25: 39.0, 0.5: 65.0, 0.75: 111.0},
'delivery_score': {0.25: 10.74074074074074,
0.5: 15.65217391304348,
0.75: 22.5}}
# Arguments (x = value, p = recency, monetary_value, frequency, d = quartiles dict)
def RScore(x, p, d):
if x <= d[p][0.25]:
return 4
elif x <= d[p][0.50]:
return 3
elif x <= d[p][0.75]:
return 2
else:
return 1
# Arguments (x = value, p = recency, monetary_value, frequency, d = quartiles dict)
def FMScore(x, p, d):
if x <= d[p][0.25]:
return 1
elif x <= d[p][0.50]:
return 2
elif x <= d[p][0.75]:
return 3
else:
return 4
RFM Segmentation for last 7 days of customer orders
rfm_segmentation_7_days = df
rfm_segmentation_7_days['r_quartile_7_days'] = rfm_segmentation_7_days['recency'].apply(RScore, args=('recency',quantiles,))
rfm_segmentation_7_days['f_quartile_7_days'] = rfm_segmentation_7_days['orders_7_days'].apply(FMScore, args=('orders_7_days',quantiles,))
rfm_segmentation_7_days['m_quartile_7_days'] = rfm_segmentation_7_days['amt_last_7_days'].apply(FMScore, args=('amt_last_7_days',quantiles,))
rfm_segmentation_7_days.head()
RFM Segmentation for last 4 weeks of customer orders
rfm_segmentation_4_weeks = df
rfm_segmentation_4_weeks['r_quartile_4_weeks'] = rfm_segmentation_4_weeks['recency'].apply(RScore, args=('recency',quantiles,))
rfm_segmentation_4_weeks['f_quartile_4_weeks'] = rfm_segmentation_4_weeks['orders_last_4_weeks'].apply(FMScore, args=('orders_last_4_weeks',quantiles,))
rfm_segmentation_4_weeks['m_quartile_4_weeks'] = rfm_segmentation_4_weeks['amt_in_last_4_weeks'].apply(FMScore, args=('amt_in_last_4_weeks',quantiles,))
rfm_segmentation_4_weeks.head()
RFMScore = (r * f * m) * delivery_score
rfm_segmentation_7_days['RFMScore_7_days'] = rfm_segmentation_7_days.r_quartile_7_days \
* rfm_segmentation_7_days.f_quartile_7_days \
* rfm_segmentation_7_days.m_quartile_7_days \
* rfm_segmentation_7_days.delivery_score
rfm_segmentation_7_days.head()
rfm_segmentation_7_days.describe()
RFMScore for last 4 weeks = (r * f * m) * delivery_score
rfm_segmentation_4_weeks['RFMScore_4_weeks'] = rfm_segmentation_4_weeks.r_quartile_4_weeks \
* rfm_segmentation_4_weeks.f_quartile_4_weeks \
* rfm_segmentation_4_weeks.m_quartile_4_weeks \
* rfm_segmentation_4_weeks.delivery_score
rfm_segmentation_4_weeks.head()
rfm_segmentation_7_days.describe()['RFMScore_7_days']
count 9934.000000
mean 256.353922
std 395.715899
min 14.117647
25% 83.478261
50% 152.727273
75% 271.562998
max 11093.333333
Name: RFMScore_7_days, dtype: float64
rfm_segmentation_4_weeks.describe()['RFMScore_4_weeks']
count 9934.000000
mean 321.186736
std 500.580048
min 14.117647
25% 82.666667
50% 157.241379
75% 355.555556
max 12480.000000
Name: RFMScore_4_weeks, dtype: float64
K Means Clustering
rfm_segmentation_7_days_values = rfm_segmentation_7_days[['amt_last_7_days', 'RFMScore_7_days']].values
rfm_segmentation_4_weeks_values = rfm_segmentation_4_weeks[['amt_in_last_4_weeks', 'RFMScore_4_weeks']].values
wcss = []
for i in range(1, 11):
kmeans = KMeans(n_clusters=i, init='k-means++', max_iter=300, n_init=10, random_state=0)
kmeans.fit(rfm_segmentation_7_days_values)
wcss.append(kmeans.inertia_)
plt.figure(figsize=(20,10))
plt.plot(range(1, 11), wcss)
plt.title('Finding the number of clusters - Elbow curve - last 7 days', fontsize=26)
plt.xlabel('Number of clusters', fontsize=24)
plt.ylabel('WCSS')
plt.savefig('./plots/7_days_elbow.png', format='png', dpi=300)
plt.show()
The curve says that 2 would be the ideal number of clusters
wcss = []
for i in range(1, 11):
kmeans = KMeans(n_clusters=i, init='k-means++', max_iter=300, n_init=10, random_state=0)
kmeans.fit(rfm_segmentation_4_weeks_values)
wcss.append(kmeans.inertia_)
plt.figure(figsize=(20,10))
plt.plot(range(1, 11), wcss)
plt.title('Finding the number of clusters - Elbow curve - last 4 weeks', fontsize=26)
plt.xlabel('Number of clusters', fontsize=24)
plt.ylabel('WCSS')
plt.savefig('./plots/4_weeks_elbow.png', format='png', dpi=300)
plt.show()
The curve says that 3 would be the ideal number of clusters
Apply K Means to customer data from the past 7 days
kmeans = KMeans(n_clusters=2, init='k-means++', max_iter=300, n_init=10, random_state=0)
y_kmeans = kmeans.fit_predict(rfm_segmentation_7_days_values)
# Visualizing the clusters
plt.figure(figsize=(20,10))
plt.scatter(rfm_segmentation_7_days_values[y_kmeans==0, 0], rfm_segmentation_7_days_values[y_kmeans==0, 1], s=100, c='red', label='Cluster 1', alpha=0.5)
plt.scatter(rfm_segmentation_7_days_values[y_kmeans==1, 0], rfm_segmentation_7_days_values[y_kmeans==1, 1], s=100, c='blue', label='Cluster 2', alpha=0.5)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], s=300, c='yellow', label='Centroids')
plt.title('Clusters of customers for last 7 days', fontsize=26)
plt.xlabel('Amount', fontsize=24)
plt.ylabel('RFMScore', fontsize=24)
plt.legend(prop={'size': 30})
plt.savefig('./plots/k_means_7_days.png', format='png', dpi=300)
plt.show()
Apply K Means to customer data from the past 4 weeks
kmeans = KMeans(n_clusters=3, init='k-means++', max_iter=300, n_init=10, random_state=0)
y_kmeans = kmeans.fit_predict(rfm_segmentation_4_weeks_values)
# Visualizing the clusters
plt.figure(figsize=(20,10))
plt.scatter(rfm_segmentation_4_weeks_values[y_kmeans==0, 0], rfm_segmentation_4_weeks_values[y_kmeans==0, 1], s=100, c='red', label='Cluster 1', alpha=0.5)
plt.scatter(rfm_segmentation_4_weeks_values[y_kmeans==1, 0], rfm_segmentation_4_weeks_values[y_kmeans==1, 1], s=100, c='blue', label='Cluster 2', alpha=0.5)
plt.scatter(rfm_segmentation_4_weeks_values[y_kmeans==2, 0], rfm_segmentation_4_weeks_values[y_kmeans==2, 1], s=100, c='green', label='Cluster 3', alpha=0.5)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], s=300, c='yellow', label='Centroids')
plt.title('Clusters of customers for last 4 weeks', fontsize=26)
plt.xlabel('Amount', fontsize=24)
plt.ylabel('RFMScore', fontsize=24)
plt.legend(prop={'size': 30})
plt.savefig('./plots/k_means_4_weeks.png', format='png', dpi=300)
plt.show()
Conclusion
- Across the 9,934 cleaned customer rows:
- 7-day window: The elbow curve bends at 2 clusters, separating frequent small-order customers from higher-spend customers.
- 4-week window: The elbow curve bends at 3 clusters:
- Red cluster: High order count with low spend per order.
- Green cluster: High order count and high spend per order.
- Blue cluster: Fewer orders over the month, with a high average order value when they do order.
Related work
Production ML context
See how this topic connects to production ML systems, infrastructure, and inference.