How we calculate your results
This page is a human-readable guide to the numbers in your dashboard: what each metric means, and where it comes from in the code. If you’re curious about a specific card or chart, search for its name below.
← Back to dashboard

How your dashboard numbers are calculated#

Everything on this page maps to analytics computed in src/lib/analytics.ts, via computeMoviePersonality(ratings).

This is intentionally focused on result math only: what each metric means, which inputs it uses, and the exact formulas behind the numbers you see.

Shared math building blocks#

Before we get into each card, these helpers appear throughout the analytics code:

  • average(values) = sum(values) / n (returns 0 when empty)
  • variance(values) = average((x - mean)^2) (returns 0 when empty)
  • clamp(x, min, max) keeps values inside a range
  • linearRegressionSlope(points) estimates trend direction and strength
  • linearRegression(points) returns { slope, intercept }
  • toDecade(year) groups years like 1994 -> "1990s"

Totals (how much data is usable)#Dashboard

These are the simplest counts, but they control confidence everywhere else.

  • ratingsCount = total imported ratings
  • matchedCount = number of ratings where tmdbId exists
  • unresolvedCount = ratingsCount - matchedCount

If matchedCount is low, metrics that depend on TMDB attributes (genres, vote averages, cast/directors, popularity) become less representative.

Harshness calibration (your baseline vs public baseline)#Dashboard

This is computed in buildCalibration.

For every movie with a TMDB average:

  • x = voteAverage / 2 (TMDB's 10-point scale mapped to your 5-point scale)
  • y = yourRating

From those points, we compute:

  • strictnessOffset = average(y - x)
  • slope and intercept from linear regression (y = slope * x + intercept)
  • calibration buckets where baseline is rounded to half-stars: bucket = round(x * 2) / 2

Interpretation:

  • negative strictnessOffset: you rate lower than baseline on average
  • positive strictnessOffset: you rate higher than baseline on average
  • slope shows whether your scaling changes as movie quality increases

Profile scores (average, pickiness, mainstream, favorites)#Dashboard

This comes from buildProfile(ratings, strictnessOffset).

Average rating and variance#

  • averageRating = average of your ratings
  • variance = spread of your ratings around your own mean

Higher variance means you use more of the rating scale (more "picky"/spread out).

Harshness label#

Label thresholds are based on strictnessOffset:

  • harsh if < -0.25
  • generous if > 0.25
  • otherwise balanced

Mainstream score and niche score#

Per movie:

  • voteCountNorm = voteCount / maxVoteCount
  • popularityNorm = popularity / maxPopularity
  • ratingWeight = yourRating / 5
  • perMovie = (voteCountNorm * 0.7 + popularityNorm * 0.3) * ratingWeight

Overall:

  • mainstreamScore = clamp(average(perMovie) * 100, 0, 100)
  • nicheScore = 100 - mainstreamScore

So "mainstream" is not just popularity; it is popularity weighted by how positively you rated those movies.

Favorite genres (top 5)#

For each genre:

  • collect all your ratings in that genre
  • rawAvg = average(genreRatings)
  • count = number of ratings in that genre
  • minimum support required: count >= max(5, ceil(0.02 * N))

Ranking uses shrinkage (to avoid tiny-sample spikes):

  • shrinkageScore = (priorStrength * globalMean + count * rawAvg) / (priorStrength + count)
  • here priorStrength = 10 and globalMean = your overall average

Displayed score remains rawAvg; shrinkage is used only for ranking fairness.

Favorite eras (top 5 decades)#

The same idea as favorite genres, but grouped by decade:

  • decade from toDecade(year)
  • minimum support: count >= max(3, ceil(0.015 * N))
  • ranked by the same shrinkage formula
  • displayed score is still the raw average for that decade

Genre affinity (you vs baseline per genre)#Dashboard

This is computed in buildGenreAffinity.

For each genre, we build two averages:

  • userScore = your average rating in that genre
  • baselineScore = average of (voteAverage / 2) in that genre

Two outputs are produced:

  • Per-genre match percent

matchPercent = clamp((1 - abs(userScore - baselineScore) / 5) * 100, 0, 100)

  • Overall cosine similarity percent between the user and baseline genre vectors

cosineSimilarityPercent = clamp(cosine(userVector, baselineVector) * 100, 0, 100)

Per-genre rows are sorted by matchPercent descending.

Temporal mood drift (how genre taste changes over review time)#Dashboard

This is computed in buildTemporalTrends.

For each genre:

  • group ratings by review month (YYYY-MM)
  • compute monthly mean ratings
  • turn months into indexed points (x = monthIndex, y = monthlyAverage)
  • compute slopePerMonth via linear regression slope

Direction is bucketed as:

  • up if slope > 0.03
  • down if slope < -0.03
  • flat otherwise

Output is limited to the top 8 genres by absolute slope magnitude.

Franchise fatigue#Dashboard

This is computed in buildFranchiseTrends.

For each franchise (collectionName):

  • sort installments by release year
  • assign x-axis by installment order (1, 2, 3, ...)
  • regress installment index vs your rating to get slope

Rules:

  • franchises need at least 3 installments
  • the "strongest fatigue" entry is the first franchise with slope < 0 after sorting by slope ascending

Director and actor affinity scores#Dashboard

This comes from buildAffinityScores.

For each person:

  • rawAverage = average of your ratings where they appear
  • count = number of appearances
  • score uses shrinkage to stabilize small samples:

score = (priorStrength * globalMean + count * rawAverage) / (priorStrength + count) with priorStrength = 5

Confidence bands:

  • high if count >= 8
  • medium if count >= 4
  • low otherwise

Additional rules:

  • Directors use the full director list per movie
  • Actors use only top-3 cast per movie
  • Keep people with count >= 2, then take top 10 by score

Most viewed actors (from watched movies)#Dashboard

This is computed from parseWatchedCsv, computeMostViewedActorsFromWatched, and rankMostViewedActorsFromMovies.

For each watched movie:

  • Build a dedupe key: normalizedTitle + "::" + year
  • Keep only one row per unique key (rewatches and duplicate watched rows do not increase counts)
  • Resolve the movie on TMDB and take top-3 billed cast

For each actor:

  • count = number of unique watched movies where they appear (top-3 cast only)
  • examples = up to 3 watched movie titles for tooltip context

Rules:

  • Watched-only tracker uses watched.csv, not ratings
  • Keep actors with count >= 2, then take top 10 by count descending
  • Tie-breaker is actor name ascending for stable ordering

Era x genre heatmap#Dashboard

This is computed in buildEraHeatmap.

Each cell is (decade, genre). For each cell:

  • averageRating = your raw mean rating in that cell
  • count = number of ratings in that cell
  • ranking score uses shrinkage:

score = (priorStrength * globalMean + count * rawAverage) / (priorStrength + count) with priorStrength = 3

Only cells with count >= 2 are kept, sorted by score descending.

Year-on-year review stats#Dashboard

This is computed in buildYearOnYear.

Review year is taken from rating.date.slice(0, 4). For each year:

  • count = number of ratings logged that year
  • averageRating = average of ratings that year
  • topGenres = top 5 genres by frequency in that year

The narrative text compares the latest year against the previous year:

  • review count difference (more, fewer, or same)
  • average rating trend (similar, slightly higher, or slightly lower)
  • plus a short mention of the latest year's top genres when available