Last active
January 8, 2025 14:41
-
-
Save MadameMinty/af5829330caf131b5477c2ec7c34aef8 to your computer and use it in GitHub Desktop.
IMDoBetter - UserScript calculating a gaussian fitted score instead of a weighted average
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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 = ` ${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