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.
The food aggregator company has stored the data of the different orders made by the registered customers in their online portal. They want to analyze the data to get a fair idea about the demand of different restaurants which will help them in enhancing their customer experience. Suppose you are hired as a Data Scientist in this company and the Data Science team has shared some of the key questions that need to be answered. Perform the data analysis to find answers to these questions that will help the company to improve the business.
The data contains the different data related to a food order. The detailed data dictionary is given below.
# Installing the libraries with the specified version.
!pip install numpy==1.25.2 pandas==1.5.3 matplotlib==3.7.1 seaborn==0.13.1 -q --user
Note: After running the above cell, kindly restart the notebook kernel and run all cells sequentially from the start again.
# import libraries for data manipulation
import numpy as np
import pandas as pd
# import libraries for data visualization
import matplotlib.pyplot as plt
import seaborn as sns
# Google Colab mounts personal Google Drive
from google.colab import drive
drive.mount('/content/drive')
Mounted at /content/drive
This mounts to the root of the Google Drive. In order to access a certain file, drive/file path needs to be added starting from 'My Drive' folder.
# Define the actual file path
file_path = '/content/drive/My Drive/Colab Notebooks/foodhub_order_dataset.csv'
# Read the CSV file into a DataFrame
df = pd.read_csv(file_path)
The first five rows:
# The head() function will display the first 5 rows of the dataset.
# View the first 5 rows
print(df.head())
order_id customer_id restaurant_name cuisine_type \ 0 1477147 337525 Hangawi Korean 1 1477685 358141 Blue Ribbon Sushi Izakaya Japanese 2 1477070 66393 Cafe Habana Mexican 3 1477334 106968 Blue Ribbon Fried Chicken American 4 1478249 76942 Dirty Bird to Go American cost_of_the_order day_of_the_week rating food_preparation_time \ 0 30.75 Weekend Not given 25 1 12.08 Weekend Not given 25 2 12.23 Weekday 5 23 3 29.20 Weekend 3 25 4 11.59 Weekday 4 25 delivery_time 0 20 1 23 2 28 3 15 4 24
# The .shape attribute of pandas DataFrame provides the number of rows and columns in the Dataset.
# Get the number of rows and columns
rows, columns = df.shape
# Display the result
print(f'The dataset contains {rows} rows and {columns} columns.')
The dataset contains 1898 rows and 9 columns.
The 9 columns are already given as order_id, customer_id, restaurant_name, cuisine_type, cost_of_the_order, day_of_the_week, (weekday or weekend), rating, food_preparation_time, and delivery_time. The dataset contains 1898 orders. We don't know yet if the data types are correct in the dataset or if there is any missing data at this point.
# The info() functions or .dtypes atribute gives the data type of each column
df.info()
<class 'pandas.core.frame.DataFrame'> 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
Most data types seem to be correctly defined. However, the 'rating' column could be numerical (e.g., ratings like 1-5 or 1-10) but I see that in the dataset there are 'Not given' values, so it is appropriate to use 'object' as data type.
# Use the isnull().sum() method to identify missing values and how many there are in each column
missing_values = df.isnull().sum()
print(missing_values)
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
There is no missing values in any of the columns in your dataset. This is a good data! No cleaning is necessary at this point.
# Get the statistical summary of the 'food_preparation_time' column
food_preparation_stats = df['food_preparation_time'].describe()
# Extract the minimum, average (mean), and maximum values
min_time = food_preparation_stats['min']
average_time = food_preparation_stats['mean']
max_time = food_preparation_stats['max']
print(f"Minimum food preparation time: {min_time} minutes")
print(f"Average food preparation time: {average_time} minutes")
print(f"Maximum food preparation time: {max_time} minutes")
Minimum food preparation time: 20.0 minutes Average food preparation time: 27.371970495258168 minutes Maximum food preparation time: 35.0 minutes
The food prep time range is between 20 min to 35 min. The difference is 15. It's a relatively narrow range. Indicating consistancy. The average prep time (27.37 min) is closer to the minimum value than the maximum.
# Count the number of "Not given" ratings
not_rated_count = df['rating'].value_counts().get('Not given', 0)
print(f"Number of orders not rated: {not_rated_count}")
Number of orders not rated: 736
736 out of 1898 orders are not rated, indicating that approximately 39% of orders lack a rating. Which is a lot!
# Set plot style for better visualization
sns.set(style="whitegrid")
# Plot numerical variables (histograms and boxplots)
numerical_columns = ['cost_of_the_order', 'food_preparation_time', 'delivery_time']
for col in numerical_columns:
plt.figure(figsize=(12, 5))
# Histogram
plt.subplot(1, 2, 1)
sns.histplot(df[col], kde=True)
plt.title(f'Histogram of {col}')
# Boxplot
plt.subplot(1, 2, 2)
sns.boxplot(x=df[col])
plt.title(f'Boxplot of {col}')
plt.show()
1) Cost of the Orders:
a) Histogram: Histogram above shows how the cost of the orders is distributed. Data is skewed (left-skew) as appearent in the histogram since most orders cost between ~$7 and ~$17. This indicate that the customers prefer orders in this price range.
b) Boxplot: Box and Whisker plot shows a minimum order cost of ~$4 and a maximum order cost of ~$36. Median order cost is ~$14. Lower quartile (Q1) is ~$12 and uppoer quartile (Q3) is ~$22.
2) Food Prep Time:
a) Histogram: Histogram shows a normal distribution of the data, pretty symmetrical. This indicates a consistency in the food prep times.
b) Boxplot: Normal distribution can also be observed in the boxplot. It shows a minimum prep tiem of ~20 and a maximum prep time of ~35. Median prep time is ~27. Lower quartile (Q1) is ~23 and uppoer quartile (Q3) is ~31.
3) Delivery Time:
a) Histogram: Histogram shows a right-skewed data. Most delivery times concentrate between 23 and 30 min. Fairly consistent.
b) Boxplot: Box and Whisker plot shows a minimum delivery time of ~15 and a maximum delivery time of ~33. Median delivery time is ~25. Lower quartile (Q1) is ~20 and uppoer quartile (Q3) is ~28.
# Set a compatible font
plt.rcParams['font.family'] = 'DejaVu Sans' # I think chinese characters in the data are still causing issues
# Plot categorical variables (countplots)
categorical_columns = ['restaurant_name', 'cuisine_type', 'day_of_the_week', 'rating']
for col in categorical_columns:
plt.figure(figsize=(10, 5))
sns.countplot(y=df[col], order=df[col].value_counts().index) # Sorting by count for better visualization
plt.title(f'Countplot of {col}')
plt.show()
/usr/local/lib/python3.10/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 142 (\x8e) missing from current font. fig.canvas.print_figure(bytes_io, **kw) /usr/local/lib/python3.10/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 140 (\x8c) missing from current font. fig.canvas.print_figure(bytes_io, **kw)
1) Countplot of Restaurant Names: Basically orders all restaurants by popularity. The most popular restaurant has more than 200 orders. Shake Shack is the most popular restaurant.
2) Countplot of Cuisine Types: American cuisine is the most popular, followed by Japanese and Italian cuisines. Vietnamese cuisine is the least popular one.
3) Countplot of Days: Weekends are significantly busier. Closer to 1400 orders are recorded in the weekends.
4) Countplot of Ratings: More than 700 orders are not rated, suggesting that the company should encourage customers to provide ratings more frequently. A significant number of customers have given a 5-star rating, with close to 600 orders receiving this top rating. There are no 2-star or 1-star rating so far, which is fabulous.
# Count the number of orders for each restaurant
restaurant_counts = df['restaurant_name'].value_counts()
# Get the top 5 restaurants
top_5_restaurants = restaurant_counts.head(5)
print(top_5_restaurants)
restaurant_name Shake Shack 219 The Meatball Shop 132 Blue Ribbon Sushi 119 Blue Ribbon Fried Chicken 96 Parm 68 Name: count, dtype: int64
Shake Shack leads significantly with the highest number of orders, followed by Meatball Shop. Blue Ribbon Sushi and Blue Ribbon Fried Chicken also have great order counts. This indicates a strong preference for Shake Shack among customers.
# Filter for weekend orders only
weekend_orders = df[df['day_of_the_week'] == 'Weekend']
# Check if there are any weekend orders
if not weekend_orders.empty:
# Count the number of orders for each cuisine type on weekends
cuisine_counts_weekend = weekend_orders['cuisine_type'].value_counts()
# Get the most popular cuisine on weekends
most_popular_cuisine = cuisine_counts_weekend.idxmax()
most_popular_cuisine_count = cuisine_counts_weekend.max()
print(f"The most popular cuisine on weekends is {most_popular_cuisine} with {most_popular_cuisine_count} orders.")
else:
print("No orders available for weekends.")
The most popular cuisine on weekends is American with 415 orders.
I added a if/else condition to check if there are any weekend orders first. If there is no weekend orders yet, the output will indicate that. Observation is that the American cuisine is the most popular in weekends.
# Filter for orders with cost greater than 20 dollars only
orders_above_20 = df[df['cost_of_the_order'] > 20]
# Calculate the number of such orders
num_orders_above_20 = orders_above_20.shape[0]
# Total number of orders
total_orders = df.shape[0]
# Fianlly calculate the percentage
percentage_above_20 = (num_orders_above_20 / total_orders) * 100
print(f"Percentage of orders costing more than 20 dollars: {percentage_above_20:.2f}%")
Percentage of orders costing more than 20 dollars: 29.24%
About 30% of the orders cost more than $20.
This means that most of the orders stay under $20.
# Calculate the mean delivery time using the mean() function
mean_delivery_time = df['delivery_time'].mean()
print(f"The mean order delivery time is {mean_delivery_time:.2f} minutes.")
The mean order delivery time is 24.16 minutes.
This suggests that on average, customers receive their orders within about 24 minutes. This is a very quick service time. Kudos to FoodHub!
# Count the number of orders placed by each customer
customer_order_counts = df['customer_id'].value_counts()
# Get the top 3 most frequent customers
top_3_customers = customer_order_counts.head(3)
# Display the customer IDs and the number of orders they placed
print("Top 3 most frequent customers and their number of orders:")
print(top_3_customers)
Top 3 most frequent customers and their number of orders: customer_id 52832 13 47440 10 83287 9 Name: count, dtype: int64
Topt 3 customers placed a notable number of orders. Customer 52832 placed the highest number of orders with 13 orders, followed by customer 47440 with 10 orders, and customer 83287 with 9 orders.
# Select only numerical columns
numerical_columns = ['cost_of_the_order', 'food_preparation_time', 'delivery_time']
# Compute correlation matrix
correlation_matrix = df[numerical_columns].corr()
# Plot a heatmap for the correlation matrix
plt.figure(figsize=(8, 6))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', linewidths=0.5)
plt.title('Correlation Heatmap of Numerical Variables')
plt.show()
We can check the correlation between numerical variables using a correlation matrix and visualize it using a heatmap.
# Pair plot for numerical variables
sns.pairplot(df[numerical_columns])
plt.suptitle('Pair Plot of Numerical Variables', y=1.02)
plt.show()
We can explore the pairwise relationships between multiple numerical variables. So, we can see how variables interact with each other.
# Count plot for 'cuisine_type' and 'day_of_the_week'
plt.figure(figsize=(10, 6))
sns.countplot(x='day_of_the_week', hue='cuisine_type', data=df)
plt.title('Cuisine Type Distribution by Day of the Week')
plt.xticks(rotation=90)
plt.show()
We can analyze the relationship between two categorical variable. For example, how the cuisine type changes based on the day of the week. First, second, third, and fourth most popular cuisine types remain same between weekends and weekdays. But, their quantity changes.
# Rating column is NOT numeric
# Convert the 'rating' column to numeric, treating 'Not given' as NaN
df['rating'] = pd.to_numeric(df['rating'], errors='coerce')
# Group by 'restaurant_name' to calculate the number of ratings and the average rating
restaurant_ratings = df.groupby('restaurant_name')['rating'].agg(['count', 'mean'])
# Filter for restaurants with more than 50 ratings only and an average rating greater than 4
eligible_restaurants = restaurant_ratings[(restaurant_ratings['count'] > 50) & (restaurant_ratings['mean'] > 4)]
# Display the eligible restaurants
print(eligible_restaurants)
count mean restaurant_name Blue Ribbon Fried Chicken 64 4.328125 Blue Ribbon Sushi 73 4.219178 Shake Shack 133 4.278195 The Meatball Shop 84 4.511905
Only 4 restaurants are eligible by having a mean rating greater than 4 and a rating count of more than 50. The 2 Blue Ribbon restaurants top the list for the free advetisement rewards because their rating averages are higher than the others.
# Define a function to calculate commission based on the cost of the order using an if conditional statement
def calculate_commission(cost):
if cost > 20:
return cost * 0.25 # 25% commission for orders > 20 dollars
elif cost > 5:
return cost * 0.15 # 15% commission for orders > 5 dollars
else:
return 0 # No commission for orders <= 5 dollars
# Apply the commission amount to each order
df['commission'] = df['cost_of_the_order'].apply(calculate_commission)
# Calculate the total revenue generated by FoodHub
total_revenue = df['commission'].sum()
print(f"The total revenue generated is ${total_revenue:.2f}")
The total revenue generated is $6166.30
Total revenue generated by FoodHub based on the commision rules provided is $6,166
# Calculate the total time (food preparation + delivery time)
df['total_time'] = df['food_preparation_time'] + df['delivery_time']
# Count the number of orders with total time > 60 minutes
orders_above_60 = df[df['total_time'] > 60].shape[0]
# Calculate the total number of orders
total_orders = df.shape[0]
# Calculate the percentage of orders taking more than 60 minutes
percentage_above_60 = (orders_above_60 / total_orders) * 100
print(f"Percentage of orders taking more than 60 minutes: {percentage_above_60:.2f}%")
Percentage of orders taking more than 60 minutes: 10.54%
About 10% of the total orders is taking more than 60 min to get delivered. Which is not bad. FoodHub is doing a great job!
# Convert 'day_of_the_week' to lowercase to ensure consistency (just in case)
df['day_of_the_week'] = df['day_of_the_week'].str.lower()
# Filter the data into weekdays and weekends
weekdays_df = df[df['day_of_the_week'] == 'weekday']
weekends_df = df[df['day_of_the_week'] == 'weekend']
# Calculate the mean delivery time for weekdays and weekends
mean_delivery_weekdays = weekdays_df['delivery_time'].mean()
mean_delivery_weekends = weekends_df['delivery_time'].mean()
print(f"Mean delivery time on weekdays: {mean_delivery_weekdays:.2f} minutes")
print(f"Mean delivery time on weekends: {mean_delivery_weekends:.2f} minutes")
Mean delivery time on weekdays: 28.34 minutes Mean delivery time on weekends: 22.47 minutes
Weekday deliveries are taking significantly longer, possibly due to increased city traffic congestion during weekdays. Weekend deliveries are faster, likely because there is less traffic on weekends.
From the analysis, it is clear that many customers are not leaving ratings, so encouraging more feedback could be beneficial. The top restaurants are popular and should be highlighted in promotions. Popular cuisines on weekends should be targeted in advertising. The average delivery time is ~24 minutes, with weekday deliveries taking longer due to traffic.