aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2023-11-22 23:45:17 -0800
committerFuwn <[email protected]>2023-11-22 23:45:17 -0800
commitffde08d759becc928f2e4bfd283665b2d6be86d4 (patch)
tree673ce9c018714665b9a78f67c09b730bb91688c4 /src
parentfeat(wrapped): adjust window length (diff)
downloaddue.moe-ffde08d759becc928f2e4bfd283665b2d6be86d4.tar.xz
due.moe-ffde08d759becc928f2e4bfd283665b2d6be86d4.zip
feat(manga): new estimation methods
Diffstat (limited to 'src')
-rw-r--r--src/lib/AniList/media.ts71
-rw-r--r--src/routes/settings/+page.svelte7
-rw-r--r--src/stores/settings.ts4
3 files changed, 58 insertions, 24 deletions
diff --git a/src/lib/AniList/media.ts b/src/lib/AniList/media.ts
index 8cc9c9c8..1ae33f7c 100644
--- a/src/lib/AniList/media.ts
+++ b/src/lib/AniList/media.ts
@@ -196,10 +196,45 @@ export const publicMediaListCollection = async (userId: number, type: Type): Pro
return flattenLists(userIdResponse['data']['MediaListCollection']['lists']);
};
+const countMedian = (guesses: number[]) => {
+ guesses.sort((a: number, b: number) => a - b);
+
+ const mid = Math.floor(guesses.length / 2);
+
+ return guesses.length % 2 !== 0 ? guesses[mid] : (guesses[mid - 1] + guesses[mid]) / 2;
+};
+
+const countIQR = (guesses: number[]) => {
+ guesses.sort((a: number, b: number) => a - b);
+
+ const q1 = guesses[Math.floor(guesses.length / 4)];
+ const q3 = guesses[Math.ceil(guesses.length * (3 / 4))];
+ const iqr = q3 - q1;
+
+ return guesses.filter((guess: number) => guess >= q1 - 1.5 * iqr && guess <= q3 + 1.5 * iqr);
+};
+
+const countMode = (guesses: number[]) => {
+ const frequency: { [key: number]: number } = {};
+ let maximumFrequency = 0;
+ let mode = 0;
+
+ for (const guess of guesses) {
+ frequency[guess] = (frequency[guess] || 0) + 1;
+
+ if (frequency[guess] > maximumFrequency) {
+ maximumFrequency = frequency[guess];
+ mode = guess;
+ }
+ }
+
+ return mode;
+};
+
export const recentMediaActivities = async (
userIdentity: UserIdentity,
media: Media,
- method: 'median' | 'trimmed_mean' | 'weighted_average' = 'trimmed_mean'
+ method: 'median' | 'iqr_median' | 'iqr_mode' | 'mode'
): Promise<number | null> => {
const activities = await (
await fetch('https://graphql.anilist.co', {
@@ -235,34 +270,32 @@ export const recentMediaActivities = async (
guesses.sort((a, b) => b - a);
if (guesses.length) {
- let bestGuess = guesses[0];
+ let bestGuess;
switch (method) {
case 'median':
{
- const sortedGuesses = guesses.slice().sort((a, b) => a - b);
-
- bestGuess = sortedGuesses[Math.floor(sortedGuesses.length / 2)];
+ bestGuess = countMedian(guesses);
+ }
+ break;
+ case 'iqr_median':
+ {
+ bestGuess = countMedian(countIQR(guesses));
+ }
+ break;
+ case 'iqr_mode':
+ {
+ bestGuess = countMode(countIQR(guesses));
}
break;
- case 'trimmed_mean':
+ case 'mode':
{
- const sortedGuesses = guesses.slice().sort((a, b) => a - b);
- const trimmedGuesses = sortedGuesses.slice(1, -1);
- bestGuess = trimmedGuesses.reduce((a, b) => a + b) / trimmedGuesses.length;
+ bestGuess = countMode(guesses);
}
break;
- case 'weighted_average':
+ default:
{
- const countMap: { [key: number]: number } = {};
- guesses.forEach((g) => {
- countMap[g] = (countMap[g] || 0) + 1;
- });
-
- bestGuess =
- Object.entries(countMap).reduce((acc: number, [val, count]: [string, number]) => {
- return acc + Number(val) * Math.log(count);
- }, 0) / Object.values(countMap).reduce((a: number, b: number) => a + Math.log(b), 0);
+ bestGuess = countMedian(guesses);
}
break;
}
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte
index e015bde8..1a78aaa2 100644
--- a/src/routes/settings/+page.svelte
+++ b/src/routes/settings/+page.svelte
@@ -129,9 +129,10 @@
<br />
<select bind:value={$settings.guessMethod} on:change={pruneAllManga}>
- <option value="median">Median (fastest, reasonably accurate)</option>
- <option value="trimmed_median">Trimmed Median (fast, very accurate, recommended)</option>
- <option value="weighted_average">Weighted Average (acceptably fast, most accurate)</option>
+ <option value="mode">Mode (fast, moderate to low accuracy)</option>
+ <option value="median">Median (moderate speed, high accuracy, recommended)</option>
+ <option value="iqr_median">Interquartile Range with Median (slower, high accuracy)</option>
+ <option value="iqr_mode">Interquartile Range with Mode (slower, high accuracy)</option>
</select>
<SettingHint lineBreak>
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index 874f9c4c..aa0350d9 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -18,7 +18,7 @@ export interface Settings {
disableGuessing: boolean;
hoverNavigation: boolean;
displayNativeTitles: boolean;
- guessMethod: 'median' | 'trimmed_mean' | 'weighted_average';
+ guessMethod: 'median' | 'iqr_median' | 'iqr_mode' | 'mode';
disableOutOfDateVolumeWarning: boolean;
displayPlannedAnime: boolean;
}
@@ -40,7 +40,7 @@ const defaultSettings: Settings = {
disableGuessing: false,
hoverNavigation: false,
displayNativeTitles: false,
- guessMethod: 'trimmed_mean',
+ guessMethod: 'median',
disableOutOfDateVolumeWarning: false,
displayPlannedAnime: true
};