feat: tag items depending on region and gold state when bought
All checks were successful
pipeline / lint-and-format (push) Successful in 4m29s
pipeline / build-and-push-images (push) Successful in 1m28s

This commit is contained in:
2026-04-18 21:08:58 +02:00
parent 17024f91a8
commit a5728a147f
7 changed files with 231 additions and 10 deletions

View File

@@ -7,6 +7,9 @@ type PlatformCounts = {
kr: number
}
// Item tags that can be derived from purchase patterns
type ItemTag = 'ahead' | 'behind' | 'region_euw' | 'region_eun' | 'region_na' | 'region_kr'
type ItemTree = {
data: number | undefined
count: number
@@ -22,6 +25,9 @@ type ItemTree = {
// Platform tracking
platformCount: PlatformCounts
// Derived tags for display
tags: Array<ItemTag>
}
function initPlatformCounts(): PlatformCounts {
@@ -34,7 +40,8 @@ function treeInit(): ItemTree {
count: 0,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 },
platformCount: initPlatformCounts()
platformCount: initPlatformCounts(),
tags: []
}
}
@@ -73,7 +80,8 @@ function nodeMerge(itemtree: ItemTree, node: ItemTree) {
count: count,
children: [],
boughtWhen: { ...node.boughtWhen },
platformCount: { ...node.platformCount }
platformCount: { ...node.platformCount },
tags: []
}
itemtree.children.push(next)
}
@@ -107,7 +115,8 @@ function treeMerge(
eun: platformKey === 'eun1' ? 1 : 0,
na: platformKey === 'na1' ? 1 : 0,
kr: platformKey === 'kr' ? 1 : 0
}
},
tags: []
})
}
}
@@ -122,7 +131,8 @@ function treeCutBranches(itemtree: ItemTree, thresholdCount: number, thresholdPe
count: +Infinity,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 },
platformCount: initPlatformCounts()
platformCount: initPlatformCounts(),
tags: []
}
)
itemtree.children.splice(itemtree.children.indexOf(leastUsedBranch), 1)
@@ -169,7 +179,8 @@ function treeClone(tree: ItemTree): ItemTree {
eun: tree.platformCount.eun,
na: tree.platformCount.na,
kr: tree.platformCount.kr
}
},
tags: [...tree.tags]
}
}
@@ -266,14 +277,100 @@ function areTreeSimilars(t1: ItemTree, t2: ItemTree): number {
return Math.max(0, Math.min(1, similarity))
}
/*
* Derive tags for an item based on purchase patterns
* Tags are derived when a specific condition is dominant (>= 60% threshold)
* For region tags, we compare against expected distribution to find items that are
* significantly more popular in a region than expected
*/
function deriveTags(node: ItemTree, expectedRegionDistribution?: PlatformCounts): void {
const tags: Array<ItemTag> = []
// Derive gold situation tags
const totalGoldSituations =
node.boughtWhen.aheadCount + node.boughtWhen.behindCount + node.boughtWhen.evenCount
if (totalGoldSituations > 0) {
const aheadPct = node.boughtWhen.aheadCount / totalGoldSituations
const behindPct = node.boughtWhen.behindCount / totalGoldSituations
// Only tag if there's a dominant pattern (>= 60%)
if (aheadPct >= 0.6) {
tags.push('ahead')
} else if (behindPct >= 0.6) {
tags.push('behind')
}
}
// Derive region tags by comparing against expected distribution
const totalRegionCount =
node.platformCount.euw + node.platformCount.eun + node.platformCount.na + node.platformCount.kr
if (totalRegionCount > 0 && expectedRegionDistribution) {
const totalExpected =
expectedRegionDistribution.euw +
expectedRegionDistribution.eun +
expectedRegionDistribution.na +
expectedRegionDistribution.kr
if (totalExpected > 0) {
// Calculate expected percentages
const expectedEuwPct = expectedRegionDistribution.euw / totalExpected
const expectedEunPct = expectedRegionDistribution.eun / totalExpected
const expectedNaPct = expectedRegionDistribution.na / totalExpected
const expectedKrPct = expectedRegionDistribution.kr / totalExpected
// Calculate actual percentages for this item
const actualEuwPct = node.platformCount.euw / totalRegionCount
const actualEunPct = node.platformCount.eun / totalRegionCount
const actualNaPct = node.platformCount.na / totalRegionCount
const actualKrPct = node.platformCount.kr / totalRegionCount
// Tag if the item is significantly more popular in a region (>= 1.5x expected rate)
// and has a minimum absolute percentage (>= 10%)
const SIGNIFICANCE_THRESHOLD = 1.5
const MINIMUM_PCT = 0.1
if (actualEuwPct >= expectedEuwPct * SIGNIFICANCE_THRESHOLD && actualEuwPct >= MINIMUM_PCT) {
tags.push('region_euw')
}
if (actualEunPct >= expectedEunPct * SIGNIFICANCE_THRESHOLD && actualEunPct >= MINIMUM_PCT) {
tags.push('region_eun')
}
if (actualNaPct >= expectedNaPct * SIGNIFICANCE_THRESHOLD && actualNaPct >= MINIMUM_PCT) {
tags.push('region_na')
}
if (actualKrPct >= expectedKrPct * SIGNIFICANCE_THRESHOLD && actualKrPct >= MINIMUM_PCT) {
tags.push('region_kr')
}
}
}
node.tags = tags
// Recursively derive tags for children
for (const child of node.children) {
deriveTags(child, expectedRegionDistribution)
}
}
/*
* Apply tag derivation to an entire tree
* expectedRegionDistribution: the total region distribution for the champion/lane,
* used to detect items that are region-specific
*/
function treeDeriveTags(itemtree: ItemTree, expectedRegionDistribution?: PlatformCounts): void {
deriveTags(itemtree, expectedRegionDistribution)
}
export {
ItemTree,
PlatformCounts,
GoldAdvantageTag,
ItemTag,
treeMerge,
treeInit,
treeCutBranches,
treeSort,
treeMergeTree,
areTreeSimilars
areTreeSimilars,
treeDeriveTags
}