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(returns0when empty)variance(values)=average((x - mean)^2)(returns0when empty)clamp(x, min, max)keeps values inside a rangelinearRegressionSlope(points)estimates trend direction and strengthlinearRegression(points)returns{ slope, intercept }toDecade(year)groups years like1994 -> "1990s"
Totals (how much data is usable)#Dashboard
These are the simplest counts, but they control confidence everywhere else.
ratingsCount= total imported ratingsmatchedCount= number of ratings wheretmdbIdexistsunresolvedCount=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)slopeandinterceptfrom 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 ratingsvariance= 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:
harshif< -0.25generousif> 0.25- otherwise
balanced
Mainstream score and niche score#
Per movie:
voteCountNorm = voteCount / maxVoteCountpopularityNorm = popularity / maxPopularityratingWeight = yourRating / 5perMovie = (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 = 10andglobalMean = 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 genrebaselineScore= 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
slopePerMonthvia linear regression slope
Direction is bucketed as:
upif slope> 0.03downif slope< -0.03flatotherwise
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 < 0after sorting by slope ascending
Director and actor affinity scores#Dashboard
This comes from buildAffinityScores.
For each person:
rawAverage= average of your ratings where they appearcount= number of appearancesscoreuses shrinkage to stabilize small samples:
score = (priorStrength * globalMean + count * rawAverage) / (priorStrength + count) with priorStrength = 5
Confidence bands:
highifcount >= 8mediumifcount >= 4lowotherwise
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 bycountdescending - 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 cellcount= number of ratings in that cell- ranking
scoreuses 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 yearaverageRating= average of ratings that yeartopGenres= top 5 genres by frequency in that year
The narrative text compares the latest year against the previous year:
- review count difference (
more,fewer, orsame) - average rating trend (
similar,slightly higher, orslightly lower) - plus a short mention of the latest year's top genres when available