Project Python Foundations: FoodHub Data 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¶

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.

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_of_the_order: 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¶

In [1]:
# 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.

In [2]:
# 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

Understanding the structure of the data¶

In [3]:
# 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.

In [4]:
# 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:

In [5]:
# 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  

Question 1: How many rows and columns are present in the data? [0.5 mark]¶

In [6]:
# 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.

Observations:¶

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.

Question 2: What are the datatypes of the different columns in the dataset? (The info() function can be used) [0.5 mark]¶

In [7]:
# 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

Observations:¶

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.

Question 3: Are there any missing values in the data? If yes, treat them using an appropriate method. [1 mark]¶

In [8]:
# 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

Observations:¶

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.

Question 4: Check the statistical summary of the data. What is the minimum, average, and maximum time it takes for food to be prepared once an order is placed? [2 marks]¶

In [9]:
# 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

Observations:¶

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.

Question 5: How many orders are not rated? [1 mark]¶

In [10]:
# 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

Observations:¶

736 out of 1898 orders are not rated, indicating that approximately 39% of orders lack a rating. Which is a lot!

Exploratory Data Analysis (EDA)¶

Univariate Analysis¶

Question 6: Explore all the variables and provide observations on their distributions. (Generally, histograms, boxplots, countplots, etc. are used for univariate exploration.) [9 marks]¶

In [13]:
# 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()

Observations:¶

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.
In [25]:
# 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)

Observation:¶

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.

Question 7: Which are the top 5 restaurants in terms of the number of orders received? [1 mark]¶

In [26]:
# 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

Observations:¶

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.

Question 8: Which is the most popular cuisine on weekends? [1 mark]¶

In [29]:
# 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.

Observations:¶

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.

Question 9: What percentage of the orders cost more than 20 dollars? [2 marks]¶

In [30]:
# 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%

Observations:¶

About 30% of the orders cost more than $20.

This means that most of the orders stay under $20.

Question 10: What is the mean order delivery time? [1 mark]¶

In [31]:
# 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.

Observations:¶

This suggests that on average, customers receive their orders within about 24 minutes. This is a very quick service time. Kudos to FoodHub!

Question 11: The company has decided to give 20% discount vouchers to the top 3 most frequent customers. Find the IDs of these customers and the number of orders they placed. [1 mark]¶

In [33]:
# 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

Observations:¶

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.

Multivariate Analysis¶

Question 12: Perform a multivariate analysis to explore relationships between the important variables in the dataset. (It is a good idea to explore relations between numerical variables as well as relations between numerical and categorical variables) [10 marks]¶

In [34]:
# 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()

Numerical vs. Numerical Analysis (Correlation Matrix and Heatmap)¶

We can check the correlation between numerical variables using a correlation matrix and visualize it using a heatmap.

In [36]:
# Pair plot for numerical variables
sns.pairplot(df[numerical_columns])
plt.suptitle('Pair Plot of Numerical Variables', y=1.02)
plt.show()

Pair Plot (Relationship Between Numerical Variables)¶

We can explore the pairwise relationships between multiple numerical variables. So, we can see how variables interact with each other.

In [37]:
# 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()

Categorical vs. Categorical Analysis (Count Plot)¶

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.

Question 13: The company wants to provide a promotional offer in the advertisement of the restaurants. The condition to get the offer is that the restaurants must have a rating count of more than 50 and the average rating should be greater than 4. Find the restaurants fulfilling the criteria to get the promotional offer. [3 marks]¶

In [38]:
# 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

Observations:¶

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.

Question 14: The company charges the restaurant 25% on the orders having cost greater than 20 dollars and 15% on the orders having cost greater than 5 dollars. Find the net revenue generated by the company across all orders. [3 marks]¶

In [39]:
# 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

Observations:¶

Total revenue generated by FoodHub based on the commision rules provided is $6,166

Question 15: The company wants to analyze the total time required to deliver the food. What percentage of orders take more than 60 minutes to get delivered from the time the order is placed? (The food has to be prepared and then delivered.) [2 marks]¶

In [40]:
# 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%

Observations:¶

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!

Question 16: The company wants to analyze the delivery time of the orders on weekdays and weekends. How does the mean delivery time vary during weekdays and weekends? [2 marks]¶

In [44]:
# 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

Observations:¶

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.

Conclusion and Recommendations¶

Question 17: What are your conclusions from the analysis? What recommendations would you like to share to help improve the business? (You can use cuisine type and feedback ratings to drive your business recommendations.) [6 marks]¶

Conclusions:¶

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.

Recommendations:¶

  • Offering discounts to frequent customers and improving delivery efficiency could enhance loyalty and satisfaction.
  • Using feedback ratings, focus on maintaining high-quality service, and address any issues highlighted by lower ratings.
  • Offering discounts to frequent customers and leveraging insights from both cuisine preferences and ratings can enhance customer loyalty and satisfaction.
  • Overall, focusing on these areas can help boost customer engagement, optimize operations, and increase revenue.