Calculates Raw Score (before MAL's hidden adjustments), Mean Absolute Deviation and custom-weighted Pruned/Liked scores with tooltips explaining the weights. Pruned Score attempts to exclude hate votes from people clearly outside the target audience (refer to Rejection Rate), and Liked Score attempts to measure the level of enthusiasm within fans. If you want different stats, just ask Gemini to edit the script for you.
// ==UserScript==
// @name MAL Alternative Statistics
// @namespace http://tampermonkey.net/
// @version 2.19
// @description Calculates Raw Score (before MAL's hidden adjustments), Mean Absolute Deviation and custom-weighted Pruned/Liked scores with tooltips explaining the weights. Pruned Score attempts to exclude hate votes from people clearly outside the target audience (refer to Rejection Rate), and Liked Score attempts to measure the level of enthusiasm within fans. If you want different stats, just ask Gemini to edit the script for you.
// @author Gemini 3 Pro/Thinking, math results third-party verified by Grok 4.1 Expert
// @match https://myanimelist.net/anime/*
// @match https://myanimelist.net/manga/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const COLORS = {
DARK_BLUE: "#2e51a2",
SHIFT_BLUE: "#647fbd",
GHOST_BG: "rgba(0, 0, 0, 0.02)",
GHOST_BORDER: "rgba(0, 0, 0, 0.08)",
TRANSPARENT: "transparent"
};
const SCALE_FACTOR = 0.7;
GM_addStyle(`
.mal-alt-wrapper { display: flex; gap: 10px; margin-bottom: 15px; align-items: stretch; }
.mal-alt-stats-box, .mal-alt-chart-box { background-color: #fff; border: 1px solid #bebebe; color: #323232; }
.mal-alt-stats-box { font-size: 11px; min-width: 380px; }
.mal-alt-chart-box { flex-grow: 1; min-width: 220px; display: flex; flex-direction: column; }
.mal-alt-stats-header { background-color: #e1e7f5; padding: 6px 10px; font-weight: bold; border-bottom: 1px solid #bebebe; color: #000; font-size: 11px; }
.mal-alt-grid { display: grid; grid-template-columns: auto auto; column-gap: 15px; padding: 0; align-items: center; }
.hover-group { display: contents; }
.hover-group:hover > .mal-alt-label, .hover-group:hover > .mal-flex-val, .hover-group:hover > .f-bold {
background-color: #f4f6ff;
}
.mal-alt-label { font-weight: normal; text-align: right; color: #555; font-size: 11px; display: flex; align-items: center; justify-content: flex-end; gap: 4px; padding: 4px 0 4px 10px; }
.mal-flex-val { display: flex; align-items: center; gap: 4px; padding: 4px 10px 4px 0; }
.f-largest { font-size: 1.6em; font-weight: bold; color: #000; }
.f-larger { font-size: 1.3em; font-weight: bold; color: #111; }
.f-bold { font-weight: bold; font-size: 1.1em; padding: 4px 10px 4px 0; }
.raw-label { font-size: 1.4em; font-weight: bold; color: #000; }
.mal-sub-value { font-size: 0.9em; color: #777; white-space: nowrap; font-weight: normal; }
.pve-val-pos { color: green; font-size: 0.9em; font-weight: normal; }
.pve-val-neg { color: #d00000; font-size: 0.9em; font-weight: normal; }
.pve-val-neu { color: #777; font-size: 0.9em; font-weight: normal; }
.mal-diff-pos { color: green; font-size: 1.3em; font-weight: normal; }
.mal-diff-neg { color: red; font-size: 1.3em; font-weight: normal; }
.mal-diff-neu { color: #888; font-size: 1.3em; font-weight: normal; }
.mal-spacer { grid-column: span 2; height: 1px; border-bottom: 1px dashed #eee; margin: 0; }
.mal-chart-container { padding: 12px; display: flex; flex-direction: column; justify-content: space-between; flex-grow: 1; }
.mal-chart-row { display: flex; align-items: center; height: 14px; margin: 1px 0; }
.mal-chart-score-num { width: 18px; font-size: 10px; color: #666; text-align: right; margin-right: 6px; }
.mal-chart-bar-bg { flex-grow: 1; height: 100%; position: relative; }
.mal-chart-bar-fill { height: 100%; width: 0%; background-color: ${COLORS.DARK_BLUE}; position: absolute; top: 0; left: 0; z-index: 2; }
.mal-chart-ghost {
height: 100%; width: 0%; position: absolute; top: 0; left: 0; z-index: 1;
background-color: ${COLORS.GHOST_BG};
border: 1px solid ${COLORS.GHOST_BORDER};
border-left: none;
box-sizing: border-box;
}
.mal-tooltip { color: #2e51a2; cursor: help; font-size: 11px; position: relative; font-weight: bold; }
.mal-tooltip-content {
visibility: hidden; width: 260px; background-color: #323232; color: #fff;
padding: 8px; border-radius: 4px; position: absolute; z-index: 999;
bottom: 135%; left: 50%; margin-left: -130px; opacity: 0;
transition: opacity 0.2s; font-weight: normal; font-size: 10px; line-height: 1.4;
pointer-events: none; text-align: left;
}
.mal-tooltip:hover .mal-tooltip-content { visibility: visible; opacity: 1; }
`);
let globalVoteData = {};
let globalTotalVotes = 0;
// --- Latent Variable Fitting Logic ---
function computeTruncStats(mu, sigma) {
let weights = [];
let totalDensity = 0;
for (let i = 1; i <= 10; i++) {
const density = Math.exp(-0.5 * Math.pow((i - mu) / sigma, 2));
weights[i] = density;
totalDensity += density;
}
let mean = 0;
for (let i = 1; i <= 10; i++) {
weights[i] /= totalDensity;
mean += i * weights[i];
}
let mad = 0;
for (let i = 1; i <= 10; i++) {
mad += Math.abs(i - mean) * weights[i];
}
return { mean, mad, weights };
}
function fitMuSigma(targetMean, targetMad) {
// --- Stage 1: Coarse Search ---
const muStart1 = Math.max(1, targetMean - 10);
const muEnd1 = targetMean + 25;
const sigmaStart1 = 0.5;
const sigmaEnd1 = 5.0;
const stepMu1 = 0.5;
const stepSigma1 = 0.25;
let bestMu = targetMean;
let bestSigma = targetMad * 1.2533;
let minError = Infinity;
for (let m = muStart1; m <= muEnd1; m += stepMu1) {
for (let s = sigmaStart1; s <= sigmaEnd1; s += stepSigma1) {
const res = computeTruncStats(m, s);
const err = Math.pow(res.mean - targetMean, 2) + Math.pow(res.mad - targetMad, 2);
if (err < minError) {
minError = err;
bestMu = m;
bestSigma = s;
}
}
}
// --- Stage 2: Fine Search ---
const rangeMu2 = 2.0;
const rangeSigma2 = 0.5;
const stepMu2 = 0.05;
const stepSigma2 = 0.05;
const muStart2 = Math.max(muStart1, bestMu - rangeMu2);
const muEnd2 = Math.min(muEnd1, bestMu + rangeMu2);
const sigmaStart2 = Math.max(sigmaStart1, bestSigma - rangeSigma2);
const sigmaEnd2 = Math.min(sigmaEnd1, bestSigma + rangeSigma2);
for (let m = muStart2; m <= muEnd2; m += stepMu2) {
for (let s = sigmaStart2; s <= sigmaEnd2; s += stepSigma2) {
const res = computeTruncStats(m, s);
const err = Math.pow(res.mean - targetMean, 2) + Math.pow(res.mad - targetMad, 2);
if (err < minError) {
minError = err;
bestMu = m;
bestSigma = s;
}
}
}
// No fallback: Always use the best found parameters
return computeTruncStats(bestMu, bestSigma);
}
function getExpectedBoost(observedMean, observedMad, mode) {
const fit = fitMuSigma(observedMean, observedMad);
const weights = fit.weights;
let simSumRaw = 0, simCountRaw = 0;
for (let i = 1; i <= 10; i++) {
simSumRaw += i * weights[i];
simCountRaw += weights[i];
}
const fittedRawMean = simSumRaw / simCountRaw;
let simSumW = 0, simCountW = 0;
for (let i = 1; i <= 10; i++) {
let w = 1.0, val = i;
if (mode === 'pruned') {
if (i <= 4) { w = 0.22; val = 5; }
else if (i === 5) { w = 0.35; val = 5; }
else if (i === 6) { w = 0.70; val = 6; }
} else {
if (i <= 4) { w = 0.22; val = 7; }
else if (i <= 6) { w = 0.35; val = 7; }
else if (i === 7) { w = 0.70; val = 7; }
}
simSumW += val * (weights[i] * w);
simCountW += (weights[i] * w);
}
return (simSumW / simCountW) - fittedRawMean;
}
// --- UI & Injection ---
function injectPlaceholder() {
const wrapper = document.createElement('div');
wrapper.className = 'mal-alt-wrapper';
let chartRowsHtml = '';
for (let i = 10; i >= 1; i--) {
chartRowsHtml += `
<div class="mal-chart-row">
<div class="mal-chart-score-num">${i}</div>
<div class="mal-chart-bar-bg">
<div id="mal-ghost-${i}" class="mal-chart-ghost"></div>
<div id="mal-bar-${i}" class="mal-chart-bar-fill"></div>
</div>
</div>`;
}
const prunedTooltip = `
Weights:<br>
Scores 7-10: 1.0x<br>
Score 6: 0.7x<br>
Score 5: 0.35x<br>
Scores 1-4: counted as 5s in 0.22x (shown in lighter blue)<br><br>
Rejection Rate = % of excluded votes.<br><br>
Expected boost from pruning is calculated based on a fitted equivalent normal distribution.
`;
const likedTooltip = `
Weights:<br>
Scores 8-10: 1.0x<br>
Score 7: 0.7x<br>
Scores 5-6: counted as 7s in 0.35x<br>
Scores 1-4: counted as 7s in 0.22x (shown in lighter blue)<br><br>
Like Ratio = % of votes counted in the Liked Score<br><br>
Expected boost from pruning is calculated based on a fitted equivalent normal distribution.
`;
wrapper.innerHTML = `
<div class="mal-alt-stats-box">
<div class="mal-alt-stats-header">MAL Alternative Statistics</div>
<div class="mal-alt-grid">
<div class="hover-group" id="zone-raw">
<div class="mal-alt-label raw-label">Raw Score:</div>
<div class="mal-flex-val">
<span id="mal-raw-val" class="f-largest">0.00</span>
<span id="mal-raw-diff" class="mal-diff-neu">(+0.00)</span>
<span id="mal-raw-mad" class="mal-sub-value">(MAD: 0.00)</span>
</div>
</div>
<div class="mal-spacer"></div>
<div class="hover-group" id="zone-pruned">
<div class="mal-alt-label">Pruned Score <div class="mal-tooltip">[?]<div class="mal-tooltip-content">${prunedTooltip}</div></div>:</div>
<div class="mal-flex-val">
<span id="mal-pruned-val" class="f-larger">0.00</span>
<span id="mal-pruned-diff" class="mal-diff-neu">(+0.00)</span>
<span id="mal-pruned-pve" class="pve-val-neu">(+0.00 vs expected boost)</span>
</div>
<div class="mal-alt-label">Rejection Rate:</div>
<div id="mal-rej-rate" class="f-bold">0.0%</div>
</div>
<div class="mal-spacer"></div>
<div class="hover-group" id="zone-liked">
<div class="mal-alt-label">Liked Score <div class="mal-tooltip">[?]<div class="mal-tooltip-content">${likedTooltip}</div></div>:</div>
<div class="mal-flex-val">
<span id="mal-liked-val" class="f-larger">0.00</span>
<span id="mal-liked-diff" class="mal-diff-neu">(+0.00)</span>
<span id="mal-liked-pve" class="pve-val-neu">(+0.00 vs expected boost)</span>
</div>
<div class="mal-alt-label">Like Ratio:</div>
<div id="mal-lik-rate" class="f-bold">0.0%</div>
</div>
</div>
</div>
<div class="mal-alt-chart-box">
<div id="chart-title" class="mal-alt-stats-header">Raw Distribution</div>
<div class="mal-chart-container">${chartRowsHtml}</div>
</div>`;
const targetHeader = Array.from(document.querySelectorAll('h2')).find(h =>
h.innerText.includes(window.location.href.includes('/stats') ? 'Score Stats' : 'Synopsis')
);
if (targetHeader) targetHeader.parentNode.insertBefore(wrapper, targetHeader);
document.getElementById('zone-raw').onmouseenter = () => updateChart('raw');
document.getElementById('zone-pruned').onmouseenter = () => updateChart('pruned');
document.getElementById('zone-liked').onmouseenter = () => updateChart('liked');
document.querySelector('.mal-alt-grid').onmouseleave = () => updateChart('raw');
}
function updateChart(mode) {
const titleEl = document.getElementById('chart-title');
let bars = Array(11).fill({ w: 0, gradient: false, split: 0 });
if (mode === 'raw') {
titleEl.innerText = "Raw Distribution";
for(let i=1; i<=10; i++) bars[i] = { w: globalVoteData[i] || 0 };
}
else if (mode === 'pruned') {
titleEl.innerText = "Pruned Distribution";
const movedWeight = ((globalVoteData[1]||0)+(globalVoteData[2]||0)+(globalVoteData[3]||0)+(globalVoteData[4]||0)) * 0.22;
const nativeWeight = (globalVoteData[5]||0) * 0.35;
bars[5] = { w: movedWeight + nativeWeight, gradient: true, split: nativeWeight / (movedWeight + nativeWeight) };
bars[6] = { w: (globalVoteData[6]||0) * 0.70 };
for(let i=7; i<=10; i++) bars[i] = { w: globalVoteData[i] || 0 };
}
else if (mode === 'liked') {
titleEl.innerText = "Liked Distribution";
const movedWeight = ((globalVoteData[1]||0)+(globalVoteData[2]||0)+(globalVoteData[3]||0)+(globalVoteData[4]||0)) * 0.22 +
((globalVoteData[5]||0)+(globalVoteData[6]||0)) * 0.35;
const nativeWeight = (globalVoteData[7]||0) * 0.70;
bars[7] = { w: movedWeight + nativeWeight, gradient: true, split: nativeWeight / (movedWeight + nativeWeight) };
for(let i=8; i<=10; i++) bars[i] = { w: globalVoteData[i] || 0 };
}
for (let i = 1; i <= 10; i++) {
const bar = document.getElementById(`mal-bar-${i}`);
const ghost = document.getElementById(`mal-ghost-${i}`);
if (!bar) continue;
const width = Math.min(100, (bars[i].w / (globalTotalVotes * SCALE_FACTOR)) * 100);
bar.style.width = `${width}%`;
const rawW = globalVoteData[i] || 0;
const ghostWidth = Math.min(100, (rawW / (globalTotalVotes * SCALE_FACTOR)) * 100);
if (mode === 'raw') {
ghost.style.display = 'none';
} else {
ghost.style.display = 'block';
ghost.style.width = `${ghostWidth}%`;
}
if (bars[i].gradient && bars[i].w > 0) {
const splitPct = (bars[i].split * 100).toFixed(2);
bar.style.background = `linear-gradient(to right, ${COLORS.DARK_BLUE} 0%, ${COLORS.DARK_BLUE} ${splitPct}%, ${COLORS.SHIFT_BLUE} ${splitPct}%, ${COLORS.SHIFT_BLUE} 100%)`;
} else {
bar.style.background = COLORS.DARK_BLUE;
}
}
}
function updateElement(id, value, className = null, prefix = "", suffix = "") {
const el = document.getElementById(id);
if (!el) return;
el.innerText = `${prefix}${value}${suffix}`;
if (className) el.className = className;
}
function runScript() {
const match = window.location.href.match(/myanimelist\.net\/(anime|manga)\/(\d+)/);
if (!match) return;
injectPlaceholder();
const statsUrl = `https://myanimelist.net/${match[1]}/${match[2]}/_/stats`;
GM_xmlhttpRequest({
method: "GET", url: statsUrl,
onload: function(response) {
if (response.status !== 200) return;
const doc = new DOMParser().parseFromString(response.responseText, "text/html");
doc.querySelectorAll('table tr').forEach(row => {
const txt = row.innerText;
const sM = txt.match(/^\s*([1-9]|10)\s+/);
const cM = txt.match(/([\d,]+)\s*votes/);
if (sM && cM) {
const s = parseInt(sM[1], 10), c = parseInt(cM[1].replace(/,/g, ''), 10);
globalVoteData[s] = c; globalTotalVotes += c;
}
});
if (globalTotalVotes === 0) return;
const officialScore = parseFloat(document.querySelector('.score-label')?.innerText) || 0;
const c14 = (globalVoteData[1]||0)+(globalVoteData[2]||0)+(globalVoteData[3]||0)+(globalVoteData[4]||0);
const c5 = globalVoteData[5]||0, c6 = globalVoteData[6]||0, c7 = globalVoteData[7]||0;
let sumRaw = 0; for (let i = 1; i <= 10; i++) sumRaw += i * (globalVoteData[i] || 0);
const meanRaw = sumRaw / globalTotalVotes;
let madSumRaw = 0; for (let i = 1; i <= 10; i++) madSumRaw += Math.abs(i - meanRaw) * (globalVoteData[i] || 0);
const madRaw = madSumRaw / globalTotalVotes;
const wM14 = c14 * 0.22, wM5 = c5 * 0.35, wM6 = c6 * 0.7;
let wCP = wM14 + wM5 + wM6, wSP = (5 * (wM14 + wM5)) + (6 * wM6);
for (let i = 7; i <= 10; i++) { wSP += i * (globalVoteData[i]||0); wCP += (globalVoteData[i]||0); }
const meanPruned = wSP / wCP;
const wM14L = c14 * 0.22, wM56L = (c5+c6) * 0.35, wM7L = c7 * 0.7;
let wCL = wM14L + wM56L + wM7L, wSL = 7 * wCL;
for (let i = 8; i <= 10; i++) { wSL += i * (globalVoteData[i]||0); wCL += (globalVoteData[i]||0); }
const meanLiked = wSL / wCL;
const diffR = meanRaw - officialScore;
updateElement('mal-raw-val', meanRaw.toFixed(2));
updateElement('mal-raw-diff', `(${diffR >= 0 ? '+' : ''}${diffR.toFixed(2)})`, diffR > 0 ? 'mal-diff-pos' : (diffR < 0 ? 'mal-diff-neg' : 'mal-diff-neu'));
updateElement('mal-raw-mad', `(MAD: ${madRaw.toFixed(2)})`);
const actualP = meanPruned - meanRaw, expP = getExpectedBoost(meanRaw, madRaw, 'pruned'), pveP = actualP - expP;
updateElement('mal-pruned-val', meanPruned.toFixed(2));
updateElement('mal-pruned-diff', `(+${actualP.toFixed(2)})`, 'mal-diff-pos');
updateElement('mal-pruned-pve', `(${pveP >= 0 ? '+' : ''}${pveP.toFixed(2)} vs expected boost)`, pveP > 0.001 ? 'pve-val-pos' : 'pve-val-neg');
const actualL = meanLiked - meanRaw, expL = getExpectedBoost(meanRaw, madRaw, 'liked'), pveL = actualL - expL;
updateElement('mal-liked-val', meanLiked.toFixed(2));
updateElement('mal-liked-diff', `(+${actualL.toFixed(2)})`, 'mal-diff-pos');
updateElement('mal-liked-pve', `(${pveL >= 0 ? '+' : ''}${pveL.toFixed(2)} vs expected boost)`, pveL > 0.001 ? 'pve-val-pos' : 'pve-val-neg');
updateElement('mal-rej-rate', (((c14 * 0.78) + (c5 * 0.65) + (c6 * 0.30)) / globalTotalVotes * 100).toFixed(1), null, "", "%");
updateElement('mal-lik-rate', (((c14 * 0.22) + ((c5+c6) * 0.35) + (c7 * 0.7) + (globalVoteData[8]||0) + (globalVoteData[9]||0) + (globalVoteData[10]||0)) / globalTotalVotes * 100).toFixed(1), null, "", "%");
updateChart('raw');
}
});
}
runScript();
})();