Skip to content

Instantly share code, notes, and snippets.

@MadameMinty
Last active January 8, 2025 14:41
Show Gist options
  • Save MadameMinty/af5829330caf131b5477c2ec7c34aef8 to your computer and use it in GitHub Desktop.
Save MadameMinty/af5829330caf131b5477c2ec7c34aef8 to your computer and use it in GitHub Desktop.
IMDoBetter - UserScript calculating a gaussian fitted score instead of a weighted average
// ==UserScript==
// @name IMDoBetter
// @namespace Violentmonkey Scripts
// @match https://www.imdb.com/title/*/
// @match https://www.imdb.com/title/*/ratings/
// @version 1.0
// @description Better ratings from IMDB
// @author Minty
// @grant GM_xmlhttpRequest
// @grant GM.fetch
// @connect imdb.com
// ==/UserScript==
(function () {
"use strict";
// Use a regular expression to extract the base URL (up to '/title/xxx')
function parseUrl() {
const url = window.location.href;
const isRatings = url.includes("/ratings/");
const match = url.match(/\/title\/([^/]+)\//);
if (match) {
const movieId = match[1]; // Extracts 'tt17371078'
const urlRatings = `https://www.imdb.com/title/${movieId}/ratings/`;
console.log(urlRatings);
return { ratings: urlRatings, isRatings };
}
}
// Extract the JSON data containing the ratings
function extractJsonData(element) {
if (!element) return null;
const json = JSON.parse(element.textContent);
const histogramValues =
json.props.pageProps.contentData.histogramData.histogramValues;
return histogramValues.map((rating) => rating.voteCount);
}
// Helper functions for Gaussian fitting
function gaussian(x, a, mu, sigma) {
return a * Math.exp(-((x - mu) ** 2) / (2 * sigma ** 2));
}
function leastSquaresFit(dataX, dataY, initialParams) {
// Simple implementation of least squares fitting.
const [a0, mu0, sigma0] = initialParams;
let bestParams = [a0, mu0, sigma0];
let minError = Infinity;
function errorFunction(params) {
const [a, mu, sigma] = params;
let error = 0;
for (let i = 0; i < dataX.length; i++) {
const diff = dataY[i] - gaussian(dataX[i], a, mu, sigma);
error += diff * diff;
}
return error;
}
// Gradient descent or another optimization can be used here.
// For simplicity, let's assume a direct optimization approach for now.
// You can use a library for better optimization, such as numeric.js.
// This is a simple trial and error method, you may need a more advanced optimization here.
for (let i = -100; i <= 100; i++) {
for (let j = -100; j <= 100; j++) {
for (let k = 1; k <= 100; k++) {
const params = [a0 + i * 0.01, mu0 + j * 0.1, sigma0 + k * 0.1];
const currentError = errorFunction(params);
if (currentError < minError) {
minError = currentError;
bestParams = params;
}
}
}
}
return bestParams;
}
function mean(arr) {
const sum = arr.reduce((acc, value) => acc + value, 0);
return sum / arr.length;
}
function std(arr) {
const meanVal = mean(arr);
const variance =
arr.reduce((acc, value) => acc + Math.pow(value - meanVal, 2), 0) /
arr.length;
return Math.sqrt(variance);
}
// Fit a Gaussian curve to the data
function fitGauss(data) {
// data is an array of vote counts, from rating 1 to rating 10
const ratings = Array.from({ length: 10 }, (_, i) => 10 - i); // Rating from 10 to 1
const votes = data.toReversed(); // Also from 10 to 1, like in notebook
// e.g. [830, 450, 939, 1039, 865, 560, 324, 245, 251, 615]
const initialParams = [Math.max(...votes), mean(ratings), std(ratings)];
const [a_gauss, mu_gauss, sigma_gauss] = leastSquaresFit(
ratings,
votes,
initialParams
);
// Fitted curve
const fittedCurve = ratings.map((rating) =>
gaussian(rating, a_gauss, mu_gauss, sigma_gauss)
);
// Adjusted votes
const adjustedVotes = fittedCurve.map(
(value, index) => (value / Math.max(...fittedCurve)) * votes[index]
);
// Calculate the overall rating
const gaussOverallRating =
adjustedVotes.reduce(
(sum, value, index) => sum + ratings[index] * value,
0
) / adjustedVotes.reduce((sum, value) => sum + value, 0);
const gaussRound = gaussOverallRating.toFixed(1);
console.log(`Gaussian fit: ${gaussOverallRating} ≈ ${gaussRound}`);
return gaussRound;
}
// Modify the displayed rating on either page
function modifyDisplayedRating(doc, gauss, ratingSelector, ratingClass) {
// const ratingParent = doc.querySelector('div[data-testid="hero-rating-bar__aggregate-rating__score"]');
const ratingElement = doc.querySelector(ratingSelector);
if (!ratingElement) return;
// Create a new rating element
const clonedRatingElement = ratingElement.cloneNode(true);
clonedRatingElement.innerHTML = `&nbsp;${gauss}`;
ratingElement.parentNode.insertBefore(
clonedRatingElement,
ratingElement.nextSibling
);
// Strike out the old rating
ratingElement.style.textDecoration = "line-through";
ratingElement.classList.remove(ratingClass);
}
const url = parseUrl();
if (url.isRatings) {
// Ratings page
console.log("Ratings page");
// extract local data
const element = document.getElementById("__NEXT_DATA__");
const ratings = extractJsonData(element);
const gauss = fitGauss(ratings);
console.log("Ratings ok", ratings, gauss);
// Wait or else the page will overwrite the rating
window.onload = function () {
modifyDisplayedRating(document, gauss, ".sc-40b53d-1", "kJANdR");
};
} else {
// Title page
console.log("Title page");
// fetch remote data
GM_xmlhttpRequest({
method: "GET",
url: url.ratings,
onload: function (response) {
if (response.status === 200) {
// const parser = new DOMParser();
// console.log(response);
const element = Array.from(response.responseXML.scripts).find(
(script) => script.id === "__NEXT_DATA__"
);
const ratings = extractJsonData(element);
const gauss = fitGauss(ratings);
console.log("Title ok", ratings, gauss);
// Small screen
modifyDisplayedRating(
document,
gauss,
".sc-9a2a0028-4 .sc-d541859f-0 .sc-d541859f-1",
"imUuxf"
);
// Large screen
modifyDisplayedRating(
document,
gauss,
".sc-d541859f-0 .sc-d541859f-1",
"imUuxf"
);
} else {
console.error(`Failed to fetch ratings: ${response.status}`);
}
},
onerror: function (err) {
console.error("Error fetching ratings page:", err);
},
});
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment