Data-Driven Insights for FoodHub’s Restaurant Demand Analysis¶
Context
The number of restaurants in New York is increasing day by day. Lots of
students and busy professionals rely on those restaurants due to their hectic
lifestyles. Online food delivery service is a great option for them. It provides
them with good food from their favorite restaurants. A food aggregator
company FoodHub offers access to multiple restaurants through a single
smartphone app.
The app allows the restaurants to receive a direct online order from a
customer. The app assigns a delivery person from the company to pick up
the order after it is confirmed by the restaurant. The delivery person then
uses the map to reach the restaurant and waits for the food package. Once
the food package is handed over to the delivery person, he/she confirms the
pick-up in the app and travels to the customer's location to deliver the food.
The delivery person confirms the drop-off in the app after delivering the food
package to the customer. The customer can rate the order in the app. The
food aggregator earns money by collecting a fixed margin of the delivery
order from the restaurants.
Objective
As a Data Scientist for a food aggregator company, tasked with enhancing
customer experience, the objective is to analyze the stored data of customer
orders on the online portal. The company aims to understand restaurant
demand for improved service. Specific tasks include identifying restaurants
meeting criteria for a promotional offer based on rating count (more than 50)
and average rating (greater than 4). Additionally, calculating net revenue by
applying appropriate charges on orders (25% on the orders having cost
greater than 20 dollars and 15% on the orders having cost greater than 5
dollars), analyzing the percentage of orders taking more than 60 minutes for
delivery, and assessing mean delivery times on weekdays and weekends.
Finally, the company plans to reward its top 3 frequent customers with 20%
discount vouchers, requiring the identification of these customers and the
number of orders they've placed.
Data Description
The data contains the different data related to a food order. The detailed
data dictionary is given below.
Data Dictionary
order_id: Unique ID of the order
customer_id: ID of the customer who ordered the food
restaurant_name: Name of the restaurant
cuisine_type: Cuisine ordered by the customer
cost: Cost of the order
day_of_the_week: Indicates whether the order is placed on a weekday
or weekend (The weekday is from Monday to Friday and the weekend is
Saturday and Sunday)
rating: Rating given by the customer out of 5
food_preparation_time: Time (in minutes) taken by the restaurant to
prepare the food. This is calculated by taking the difference between
the timestamps of the restaurant's order confirmation and the delivery
person's pick-up confirmation.
delivery_time: Time (in minutes) taken by the delivery person to
deliver the food package. This is calculated by taking the difference
between the timestamps of the delivery person's pick-up confirmation
and drop-off information
Let us start by importing the required libraries
# import libraries for data manipulation
import numpy as np
import pandas as pd
# import libraries for data visualization
import [Link] as plt
import seaborn as sns
# to suppress warnings
import warnings
[Link]('ignore')
Understanding the structure of the data
# read the data
df = pd.read_csv('/kaggle/input/food-ordering-and-delivery-app-dataset/
food_order.csv')
# returns the first 5 rows
[Link]()
ra
ord custo restaur cuisi cost_of_ day_of_ food_prep deliv
ti
er_i mer_i ant_na ne_ty the_ord the_we aration_ti ery_ti
n
d d me pe er ek me me
g
N
147 ot
3375 Hanga Kore Weeke
0 714 30.75 gi 25 20
25 wi an nd
7 ve
n
N
Blue
147 ot
3581 Ribbon Japan Weeke
1 768 12.08 gi 25 23
41 Sushi ese nd
5 ve
Izakaya
n
147 Cafe
6639 Mexi Weekd
2 707 Haban 12.23 5 23 28
3 can ay
0 a
Blue
147 Ribbon
1069 Amer Weeke
3 733 Fried 29.20 3 25 15
68 ican nd
4 Chicke
n
147 Dirty
7694 Amer Weekd
4 824 Bird to 11.59 4 25 24
2 ican ay
9 Go
Observations:¶
The DataFrame has 9 columns as mentioned in the Data Dictionary. Data in
each row corresponds to the order placed by a customer.
# The shape attribute returns a tuple with the number of rows and the
number of columns
[Link]
(1898, 9)
Observations:
There are 1898 rows, i.e. 1898 orders.
There are 9 columns, i.e. 9 data for each registered order.
# Use info() to print a concise summary of the DataFrame
[Link]()
<class '[Link]'>
RangeIndex: 1898 entries, 0 to 1897
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 1898 non-null int64
1 customer_id 1898 non-null int64
2 restaurant_name 1898 non-null object
3 cuisine_type 1898 non-null object
4 cost_of_the_order 1898 non-null float64
5 day_of_the_week 1898 non-null object
6 rating 1898 non-null object
7 food_preparation_time 1898 non-null int64
8 delivery_time 1898 non-null int64
dtypes: float64(1), int64(4), object(4)
memory usage: 133.6+ KB
Observations:
There are three different datatypes: int64 (represents discrete
numerical values), float64 (represents continuous numerical values)
and object (represents categorial variables).
order_id, customer_id, food_preparation_time and delivery_time have
int64 datatype.
cost_of_the_order has float64 datatype.
restaurant_name, cuisine_type, day_of_the_week and rating are of
datatype object.
Note 1: Time is normally a continous variable therefore it should be
defined as float64. However, it is indicated in the problem statement
that time for food_preparation_time and delivery_time is measured in
minutes thus it is detected as int64.
Note 2: rating is detected as object but it should be int64 since the
value is defined between 0 and 5. This indicates that there are missing
values or non-numerical entries in the data.
# Checking missing values
[Link]().sum()
order_id 0
customer_id 0
restaurant_name 0
cuisine_type 0
cost_of_the_order 0
day_of_the_week 0
rating 0
food_preparation_time 0
delivery_time 0
dtype: int64
# Count the occurences for each category
[Link].value_counts()
rating
Not given 736
5 588
4 386
3 188
Name: count, dtype: int64
Observations:
From previous observations, there are either missing values or non-numerical
entries in the rating column.
There are 736 not rated orders, that is almost 39% of the orders.
From the analyses above, ratings were not given for almost 39% of the
orders. We'll treat 'Not given' as another categorical variable.
# Descriptive statistics for the numerical variables
[Link]().T
co
mean std min 25% 50% 75% max
unt
18 1.4774 1476 1477 1477 1.4779 1478
548.04
order_id 98. 96e+0 547.0 021.2 495.5 70e+0 444.0
9724
0 6 0 5 0 6 0
18 1.7116 113698 2.7052
customer_i 1311. 7778 1286 4053
98. 85e+0 .13974 50e+0
d 00 7.75 00.00 34.00
0 5 3 5
18 1.6498 2.2297
cost_of_th 7.4838
98. 85e+0 4.47 12.08 14.14 50e+0 35.41
e_order 12
0 1 1
food_prepa 18 2.7371 3.1000
4.6324
ration_tim 98. 97e+0 20.00 23.00 27.00 00e+0 35.00
81
e 0 1 1
delivery_ti 18 2.4161 4.9726 15.00 20.00 25.00 2.8000 33.00
co
mean std min 25% 50% 75% max
unt
98. 75e+0 00e+0
me 37
0 1 1
Observations:
The minimum time it takes for food to be prepared is 20 minutes whereas
the maximum is 35 minutes. Aditionally, the average is 27.37 minutes.
Question 5: How many orders are not rated?
Exploratory Data Analysis (EDA)
Univariate Analysis
# Boxplot for the order_id variable
[Link](data=df, x='order_id');
Observations:
The maximum and minimum of order_id is 1898. This is exactly, the
number of rows of the data set thus this colum is just a numbering of
the orders.
# Histogram for the customer_id variable
[Link](data=df, x='customer_id', kde=True); # kde to see the
distribution
[Link](rotation=45) # rotate the ticks of x axis to better visualization
[Link]()
Observations:
The distribution ranges between 0 and 400000.
The user with id between 3000 and 16000 order food most often than
the other users.
There are two peaks in the distribution. One around 8000 and the
second around 36000 thousand. This is a bimode distribution.
# Bar plot for the cuisine_type variable
[Link](data=df, x='cuisine_type');
[Link](rotation=90) # rotate the ticks of x axis to better visualization
[Link]()
Observations:
The three most order foods are American, Japanese and Italian in this
order.
The three least order foods are Vietnamese, Korean and Spanish in this
order.
# Histogram for the cost_of_the_order variable
[Link](data=df, x='cost_of_the_order', kde=True);
[Link]()
# Boxplot for cost_of_the_order variable
[Link](data=df, x='cost_of_the_order')
[Link]()
Observations:
The data for cost_of_the_order ranges between 5 and 35.
50% of the data lies between 12 and 22, aproximately.
There are no outliers.
There are three peaks on the distribution. One of them is around 13.5
which almost coincide with the median of 14 (aproximately).
# Bar plot for day_of_the_week variable
[Link](data=df, x='day_of_the_week');
[Link]()
Observations:
Most of the orders are made during the weekend. That is around 1350
orders.
# Bar plot for rating variable
[Link](data=df, x='rating')
[Link]()
Observtions:
Almost 39% of the orders have no rating.
~31% of the orders have 5 as rating.
A rating of 4 was given to ~20% of the orders.
Finally, ~9% of the orders were rated with 3.
# Bar plot for food_preparation_time variable
[Link](data=df, x='food_preparation_time');
[Link]()
Observations:
Since we treat time as a discreate variable, we use a bar graph.
It seems that the distribution behaves as a uniform distribution.
# Histogram for delivery_time variable
[Link](data=df, x='delivery_time', kde=True);
[Link]()
# Boxplot for delivery_time variable
[Link](data=df, x='delivery_time')
[Link]()
Observations:
The median of the data is 25.
There's a single peak at 26.
50% of the data is between 20 and 28.
The data seems to follow a normal distribution around 25.
# Counts the number of ocurrences for category in restaurant_name variable
df.restaurant_name.value_counts()
restaurant_name
Shake Shack 219
The Meatball Shop 132
Blue Ribbon Sushi 119
Blue Ribbon Fried Chicken 96
Parm 68
...
Sushi Choshi 1
Dos Caminos Soho 1
La Follia 1
Philippe Chow 1
'wichcraft 1
Name: count, Length: 178, dtype: int64
Observations:
The top five restaurants are Shake Shack with 219 orders, The Meatball Shop
with 132 orders, Blue Ribbon Sushi with 119 orders, Blue Ribbon Fried
Chicken with 96 orders and Parm with 68 orders.
# Filter the data by 'Weekend' and count cuisine_type occurences
df[df['day_of_the_week'] == 'Weekend'].cuisine_type.value_counts()
cuisine_type
American 415
Japanese 335
Italian 207
Chinese 163
Mexican 53
Indian 49
Mediterranean 32
Middle Eastern 32
Thai 15
French 13
Korean 11
Southern 11
Spanish 11
Vietnamese 4
Name: count, dtype: int64
Observations:
The most popular cuisine on weekens is the American cuisine with 415
orders.
# Filter the data by order with cost greater than 20
# then divide by the total number of orders times 100
percent_orders = df[df['cost_of_the_order'] > 20].shape[0] / [Link][0] *
100
print(percent_orders)
29.24130663856691
Observations:
29.24% of the orders cost more than 20 dollars.
# Calculate the mean of the delivery_time variable
df['delivery_time'].mean()
24.161749209694417
Observations:
The mean order deliver time is 24.16
# Count the ocurrences for each category in the custimer_id variable
df.customer_id.value_counts()
customer_id
52832 13
47440 10
83287 9
250494 8
259341 7
..
385426 1
254913 1
289597 1
74412 1
397537 1
Name: count, Length: 1200, dtype: int64
Observations:
The users that will obtain the 20% discount vouchers are customer 52832
with 13 orders, customer 47440 with 10 orders and 83287 with 9 orders.
Multivariate Analysis
# Heatmap of the three main numerical variables
[Link](data=df[['cost_of_the_order', 'food_preparation_time',
'delivery_time']].corr(numeric_only=True), annot=True, cmap='YlGnBu');
Observations:
We can observe that there is no correaltion among the
variables cost_of_the_order, food_preparation_time and delivery_time s
ince all the correlation coefficients are near zero.
# Boxplots of the cost_of_the_order by cuisine_type
[Link](data=df, x='cuisine_type', y='cost_of_the_order') ;
[Link](rotation=90)
[Link]()
Observations:
The most expensive cuisines are French, Spanish and Southern.
The least expensive cuisines are Vietnamese and Korean.
# Bar plot comparing the food_preparation_time by cuisine_type and
day_of_the_week
[Link](data=df, x="cuisine_type", y="food_preparation_time",
hue="day_of_the_week", kind='bar', palette='pastel');
[Link](rotation=90)
[Link]()
Observations:
Generally, the preparation time is relatively similiar for every cuisine
either in weekend or weekdays
It seems to take longer to prepare Spanish food on weedays.
# Boxplots of the delivery_time by day_of_the_week
[Link](data=df, x='day_of_the_week', y='delivery_time') ;
[Link]()
Observations:
It is clear that there are higher delivery times during weekdays than in
weeknds
The minimum wating time during weekdays is of around 24, almost
two thirds more compared to 15 minutes from weekends.
Here, we find the restaurants fulfilling the criteria to get the promotional
offer
df3 = df[df['rating'] != 'Not given'] # filtering the data
df3['rating'] = df3['rating'].astype(int) # change datatype of the rating
column
means = [Link]("restaurant_name")['rating'].mean() # calculate the
mean for each group
means = means[means > 4] # filtering the data
counts = [Link]("restaurant_name")['rating'].sum() # aggregation per
group
counts = counts.sort_values(ascending = False) # sorting the values
counts = counts[counts > 50] # filtering the data
for res, val in [Link]():
if means[res]: # checking if the restaurant meets both conditions
print(res)
Shake Shack
The Meatball Shop
Blue Ribbon Sushi
Blue Ribbon Fried Chicken
RedFarm Broadway
Parm
RedFarm Hudson
TAO
Han Dynasty
Blue Ribbon Sushi Bar & Grill
Nobu Next Door
Rubirosa
Momoya
Tamarind TriBeCa
Five Guys Burgers and Fries
Jack's Wife Freda
Blue Ribbon Sushi Izakaya
Sushi of Gari 46
Bareburger
Chipotle Mexican Grill $1.99 Delivery
Sushi of Gari Tribeca
ilili Restaurant
Cafe Mogador
J. G. Melon
The Smile
Observations:
These resutrants get the promotionl offer: Shake Shack, The Meatball Shop,
Blue Ribbon Sushi, Blue Ribbon Fried Chicken, RedFarm Broadway, Parm,
RedFarm Hudson, TAO, Han Dynasty, Blue Ribbon Sushi Bar & Grill, Nobu
Next Door, Rubirosa, Momoya, Tamarind TriBeCa, Five Guys Burgers and
Fries, Jack's Wife Freda, Blue Ribbon Sushi Izakaya, Sushi of Gari 46,
Bareburger, Chipotle Mexican Grill $1.99 Delivery, Sushi of Gari Tribeca, ilili
Restaurant, Cafe Mogador, J. G. Melon, The Smile.
Next, we find the net revenue generated by the company across all orders
# revenue for orders with cost greater than 20
revenue1 = df[df['cost_of_the_order'] > 20]['cost_of_the_order'].sum()
#revenue for orders with cost between 5 and 20
revenue2 = df[(df['cost_of_the_order'] <= 20) & (df['cost_of_the_order'] > 5)]
['cost_of_the_order'].sum()
revenue = revenue1*0.25 + revenue2*0.15 # calculating the revenue foe
the company
revenue
6166.303
Observations:
The net revenue for the company is of 6166.303 dollars.
The percentage of orders taking more than 60 minutes to get delivered is
calculated as follows.
# creating new column with the total time preparation + delivery
df['total_time'] = df['delivery_time'] + df['food_preparation_time']
# calculating the percentage
orders_more_than_60 = df[df['total_time'] >
60]['total_time'].shape[0]/[Link][0] *100
orders_more_than_60
10.537407797681771
Observations:
10.53% of orders take more than 60 minutes to get delivered.
The mean delivery time vary during weekdays and weekends as follows.
# grouping by day of the week and calculating the mean per category
weekdays_mean = [Link]('day_of_the_week')['delivery_time'].mean()
weekdays_mean
day_of_the_week
Weekday 28.340037
Weekend 22.470022
Name: delivery_time, dtype: float64
Observations:
There is a difference of ~6 minutes of delivery time between weekdays and
weekends. Weekdays having the largest delivery time.
Conclusion and Recommendations
Conclusions:
A great amount of orders do not have a rating, ~39% of them.
The delivery time has a percentage increase of ~26% on weekdays
compared to weekends.
From around 1900 orders, only 6166$ were accumulated as revenue.
There's no correlation between the continouos variables.
Not many (less than 11%) of the deliveries take longer than one hour.
People order more food on weekends.
Recommendations:
Incentivize the customers to leave a rating to the restaurant such as a
discount on future orders. In this way, more clients will be attracted to
new restaurants.
Change the charges to the restaurant, for example 15% for orders
betweem 5 and 15, 20% for orders between 15 and 25 and 25% for
orders greater than 25. This will incurr in higher revenues.
The delivery time is higher on weekdays maybe because there is not a
large amount of courier. The company should incetivize the courier to
work during weekdays.
Set a higher fee for restaurants during weekends.