Mathew K Analytics

Lesson 6 · Pandas Projects

Restaurant Reviews & Ratings Analysis with Python Pandas

A complete, standalone tutorial: build a synthetic reviews dataset, clean out duplicates, then rank restaurants and cuisines with pandas. No prior pandas…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Pandas for Restaurant Reviews: Ratings, Cuisines, and Duplicates#

  • A complete, standalone tutorial: build a synthetic reviews dataset, clean out duplicates, then rank restaurants and cuisines with pandas.
  • No prior pandas experience needed. Let's jump straight in.

Before You Start#

  • Open a new Jupyter Notebook in VS Code and select your Python interpreter as the kernel.
  • If pandas isn't installed yet, open a terminal in VS Code and run: pip install pandas

Part 1: Building the Dataset#

import pandas as pd
import numpy as np
print(pd.__version__)
2.3.0

Building a Restaurant List#

rng = np.random.default_rng(seed=99)

cuisines = ['Italian', 'Japanese', 'Mexican', 'Indian', 'Thai']
cities = ['Portside', 'Elmfield', 'Brookhaven']
price_ranges = ['Budget', 'Mid-Range', 'Upscale']

restaurants = pd.DataFrame({
    'restaurant_id': range(1, 26),
    'restaurant_name': [f'Restaurant {i}' for i in range(1, 26)],
    'cuisine': rng.choice(cuisines, size=25),
    'city': rng.choice(cities, size=25),
    'price_range': rng.choice(price_ranges, size=25, p=[0.35, 0.45, 0.2])
})
restaurants.head()
restaurant_id restaurant_name cuisine city price_range
0 1 Restaurant 1 Thai Portside Mid-Range
1 2 Restaurant 2 Mexican Elmfield Mid-Range
2 3 Restaurant 3 Indian Elmfield Mid-Range
3 4 Restaurant 4 Mexican Brookhaven Mid-Range
4 5 Restaurant 5 Italian Portside Mid-Range

Generating Reviews#

n_reviews = 600
review_rows = []
review_dates = pd.date_range('2026-01-01', periods=210, freq='D')

for review_id in range(1, n_reviews + 1):
    restaurant_id = int(rng.choice(restaurants['restaurant_id']))
    rating = int(rng.choice([1, 2, 3, 4, 5], p=[0.05, 0.1, 0.2, 0.35, 0.3]))
    review_date = rng.choice(review_dates)
    review_rows.append([review_id, restaurant_id, rating, review_date])

reviews = pd.DataFrame(review_rows, columns=['review_id', 'restaurant_id', 'rating', 'review_date'])
print(reviews.shape)
(600, 4)

Injecting Duplicate Reviews#

duplicate_rows = reviews.sample(n=20, random_state=5)
reviews = pd.concat([reviews, duplicate_rows], ignore_index=True)
reviews.to_csv('restaurant_reviews.csv', index=False)
reviews = pd.read_csv('restaurant_reviews.csv', parse_dates=['review_date'])
print(reviews.shape)
(620, 4)

Part 2: First Look and Deduplication#

print(reviews.duplicated().sum())
reviews[reviews.duplicated()].head()
20
review_id restaurant_id rating review_date
600 196 3 4 2026-05-17
601 156 15 5 2026-03-23
602 41 1 4 2026-01-05
603 213 19 4 2026-01-06
604 53 24 4 2026-07-28
reviews = reviews.drop_duplicates().reset_index(drop=True)
print(reviews.shape)
print(reviews.duplicated().sum())
(600, 4)
0

Part 3: Merging and Ranking Restaurants#

full_reviews = reviews.merge(restaurants, on='restaurant_id', how='left')
full_reviews[['restaurant_name', 'cuisine', 'city', 'rating']].head()
restaurant_name cuisine city rating
0 Restaurant 21 Japanese Elmfield 5
1 Restaurant 9 Thai Brookhaven 4
2 Restaurant 16 Mexican Portside 4
3 Restaurant 13 Indian Brookhaven 2
4 Restaurant 4 Mexican Brookhaven 4
restaurant_stats = full_reviews.groupby('restaurant_name').agg(
    review_count=('rating', 'count'),
    avg_rating=('rating', 'mean')
).round(2)
top_rated = restaurant_stats[restaurant_stats['review_count'] >= 10].sort_values('avg_rating', ascending=False)
top_rated.head(5)
review_count avg_rating
restaurant_name
Restaurant 1 21 4.10
Restaurant 10 22 4.09
Restaurant 11 29 4.07
Restaurant 7 19 4.05
Restaurant 17 33 4.03

Part 4: Cuisine Analysis#

cuisine_summary = full_reviews.groupby('cuisine').agg(
    review_count=('rating', 'count'),
    avg_rating=('rating', 'mean')
).round(2).sort_values('avg_rating', ascending=False)
cuisine_summary
review_count avg_rating
cuisine
Japanese 141 3.89
Thai 129 3.89
Mexican 159 3.82
Indian 109 3.79
Italian 62 3.45
full_reviews['rating'].value_counts().sort_index()
rating
1     25
2     54
3    119
4    216
5    186
Name: count, dtype: int64

Part 5: Pivot Table and Cross-Tab#

cuisine_price_pivot = pd.pivot_table(full_reviews, values='rating', index='cuisine', columns='price_range', aggfunc='mean').round(2)
cuisine_price_pivot = cuisine_price_pivot[['Budget', 'Mid-Range', 'Upscale']]
cuisine_price_pivot
price_range Budget Mid-Range Upscale
cuisine
Indian 3.85 3.74 NaN
Italian NaN 3.45 NaN
Japanese 4.05 3.68 4.00
Mexican 3.94 3.76 NaN
Thai 3.88 4.10 3.73
pd.crosstab(full_reviews['cuisine'], full_reviews['city'])
city Brookhaven Elmfield Portside
cuisine
Indian 48 37 24
Italian 36 0 26
Japanese 36 50 55
Mexican 76 52 31
Thai 67 19 43

Part 6: Visualizing the Results#

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 5))
cuisine_summary['avg_rating'].plot(kind='bar', ax=ax, color='darkorange', ylim=(0, 5))
ax.set_title('Average Rating by Cuisine')
ax.set_ylabel('Average Rating (out of 5)')
plt.tight_layout()
plt.savefig('avg_rating_by_cuisine.png', dpi=150)
plt.close(fig)
print('Saved avg_rating_by_cuisine.png')
Saved avg_rating_by_cuisine.png
fig, ax = plt.subplots(figsize=(8, 5))
cuisine_price_pivot.plot(kind='bar', ax=ax, ylim=(0, 5))
ax.set_title('Average Rating by Cuisine and Price Range')
ax.set_ylabel('Average Rating (out of 5)')
ax.legend(title='Price Range')
plt.tight_layout()
plt.savefig('rating_by_cuisine_and_price.png', dpi=150)
plt.close(fig)
print('Saved rating_by_cuisine_and_price.png')
Saved rating_by_cuisine_and_price.png

Wrap-Up: What You Learned#

  • Generating a realistic synthetic reviews dataset with deliberately injected duplicates, then saving and reloading with to_csv and read_csv.
  • Detecting duplicate rows with duplicated, and removing them with drop_duplicates.
  • Merging reviews onto restaurant details, then ranking restaurants with a minimum-review-count filter.
  • Cuisine-level summaries with groupby, and a full rating distribution with value_counts.
  • A cuisine-by-price pivot_table, and a cuisine-by-city crosstab.
  • Grouped bar charts, including one plotted directly from a pivoted DataFrame, with matplotlib.
  • You went from a duplicate-riddled synthetic reviews feed to a fully cleaned, ranked restaurant report. If you want the next dataset in this series to land in your feed automatically, subscribing is the move see you in the next one.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.