Movie Ratings Explorer: A Full EDA on a Public Dataset

Data Science & ML Projects (Beginner)

Chapter 5 · Movie Ratings Explorer: A Full EDA on a Public Dataset

Deliberately no numbered checklist this time
ds1-9 walked through EDA as seven explicit, numbered steps — shape, summary stats, univariate, bivariate, multivariate, anomalies, hypotheses. Every one of those concerns is still here in this chapter, but presented the way an analyst actually uses them once the checklist has done its job: as instincts guiding a real investigation, not a list to tick off in order.

What We're Building

An exploration of a MovieLens-style ratings dataset — a real, well-known public research dataset of user ratings for movies, freely available and widely used for exactly this kind of practice. Two files: movies.csv (movie ID, title, genre, release year) and ratings.csv (user ID, movie ID, rating, timestamp). The question driving the whole exploration: what actually makes a movie "highly rated," and does the obvious answer hold up once the data gets a real look?

Loading and Merging

import pandas as pd

movies = pd.read_csv("movies.csv")
ratings = pd.read_csv("ratings.csv")

df = ratings.merge(movies, on="movie_id")
print(df.shape)
print(df.head())
print(df["rating"].describe())

The merge itself is ds1-5's own join material put to real use — a ratings file alone has no genre or title, and a movies file alone has no ratings; the interesting questions only exist once both are joined. .shape and .describe() here aren't a separate "Step 1" — they're just the first thing worth checking before trusting anything built on top of this data.

What Does a Rating Actually Look Like?

import seaborn as sns
import matplotlib.pyplot as plt

sns.histplot(df["rating"], bins=10, color="#15803D")
plt.title("Distribution of Individual Ratings")
plt.savefig("rating_distribution.png")

Ratings cluster heavily around 3-4 out of 5, with genuinely low ratings comparatively rare — a real, common pattern in rating data worth noticing before trusting any average built from it: the scale isn't used symmetrically, so "average rating" already means something skewed toward the positive end before any other analysis happens.

Which Genres Rate Highest?

genre_avg = df.groupby("genre")["rating"].mean().sort_values(ascending=False)
print(genre_avg)

genre_avg.plot(kind="barh", color="#EAB308")
plt.title("Average Rating by Genre")
plt.savefig("genre_ratings.png")

A one-line groupby, but already worth a second look before drawing conclusions — some genres will naturally have far fewer total ratings than others, which is exactly the kind of thing that turns into a genuine trap two sections from now.

Popular vs. Highly Rated — Not the Same Question

title_stats = df.groupby("title").agg(
    avg_rating=("rating", "mean"),
    num_ratings=("rating", "count"),
)

print("Top 10 by average rating:")
print(title_stats.sort_values("avg_rating", ascending=False).head(10))

print("\nTop 10 by number of ratings:")
print(title_stats.sort_values("num_ratings", ascending=False).head(10))
A real, common finding
Sorted purely by average rating, the top of the list is dominated by obscure titles with only one or two ratings — a single perfect score from one viewer outranks a genuinely acclaimed film with thousands of ratings averaging 4.3. Sorted by number of ratings instead, a completely different, far more recognizable list appears. "Highest rated" and "most popular" are answering two different questions, and naively trusting the first without checking num_ratings produces a genuinely misleading result.

A Statistical Trap: Small-Sample Extremes

Why does a one-rating movie so easily top the list? With only one or two data points, the average has nothing to pull it toward a typical value — a single 5 or a single 1 is the entire average. As the number of ratings grows, the law of large numbers (ds1-6) pulls the average toward the movie's own genuine, underlying quality, since the extremes of individual opinion start to cancel out. This is the same small-sample-size caution ds1-6 raised in the abstract, now caught red-handed in a real dataset.

reliable = title_stats[title_stats["num_ratings"] >= 20]
print(reliable.sort_values("avg_rating", ascending=False).head(10))
A real-world parallel
Filtering to a minimum rating count before ranking is a simplified version of exactly what real rating platforms (IMDb among them) do — a raw average alone is known to be unreliable at low sample sizes, so production systems use some form of minimum-count threshold or weighted formula rather than trusting a naive .mean() directly.

Forming a Real Hypothesis: Do Older Movies Rate Higher?

df["decade"] = (df["year"] // 10) * 10
decade_avg = df.groupby("decade")["rating"].mean()

decade_avg.plot(kind="line", marker="o", color="#15803D")
plt.title("Average Rating by Decade")
plt.savefig("decade_trend.png")
A pattern isn't a conclusion
If older decades show a slightly higher average, resist the urge to conclude "movies used to be better." ds1-6's own correlation-vs-causation warning applies directly here: only movies good enough to still be watched and rated today tend to survive into a modern ratings dataset at all — a real selection-bias effect, not evidence about film quality across all eras equally. The honest conclusion is a genuine, testable hypothesis worth investigating further, not a settled finding.

Merging before exploring

The interesting questions only exist once both files are joined (ds1-5).

groupby + agg with named aggregations

Computing average and count together, side by side, in one call.

Small-sample extremes

Low-count groups produce unreliable, easily-extreme averages — filter before ranking.

Selection bias in survivorship

What's still in the dataset today isn't a random sample of everything that ever existed.

Extend This Project

Try these on your own:

  • Try a few different minimum-num_ratings thresholds (5, 20, 50) and see how much the top-10 list changes at each one.
  • Compute each genre's own average and total rating count together, the same way this chapter did for titles, before trusting the genre bar chart's own ranking.
  • Look up whether a Bayesian-average or weighted-rating formula (search "IMDb weighted rating formula" for a real, documented example) produces a meaningfully different ranking than the simple threshold used here.
  • Plot ratings-per-year instead of average-rating-per-year — does the dataset simply contain more recent ratings, which could itself explain part of the decade pattern?

What's Next

Chapter 6: Multi-Source Data Aggregator — combining two independent public APIs into one clean, merged dataset, extending Chapter 3's own single-API pattern to genuine multi-source integration.

Chapter 5 Quick Reference

  • Applies ds1-9's/ds1-10's own seven EDA concerns as a flowing investigation, not a numbered checklist — the shape/stats/univariate/bivariate/anomaly/hypothesis concerns are all still present
  • Merging (ds1-5) two files first, since the interesting questions only exist once both are joined
  • "Highest rated" and "most popular" are genuinely different questions — a naive average alone can be misleading
  • Small-sample extremes are a real statistical trap (ds1-6), directly caught in this chapter's own top-10 list
  • A visible pattern (older movies rating higher) is a hypothesis worth investigating, not a conclusion — selection/survivorship bias is a real, honest alternative explanation