refactor/match_collector: change folder structure
Some checks are pending
pipeline / lint-and-format (push) Successful in 4m51s
pipeline / build-and-push-images (push) Has started running

This commit is contained in:
2026-04-23 18:35:37 +02:00
parent 360be86c10
commit c976f340e6
9 changed files with 12 additions and 10 deletions

View File

@@ -0,0 +1,344 @@
import {
REGION_KEYS,
initPlatformCounts,
mergePlatformCounts,
singlePlatformCount
} from './platform'
import type { PlatformCounts } from './platform'
type GoldAdvantageTag = 'ahead' | 'behind' | 'even'
// 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
children: Array<ItemTree>
// Gold advantage tracking
boughtWhen: {
aheadCount: number
behindCount: number
evenCount: number
meanGold: number
}
// Platform tracking
platformCount: PlatformCounts
// Derived tags for display
tags: Array<ItemTag>
}
function treeInit(): ItemTree {
return {
data: undefined,
count: 0,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 },
platformCount: initPlatformCounts(),
tags: []
}
}
/*
* Merge a node with an item tree
*/
function nodeMerge(itemtree: ItemTree, node: ItemTree) {
const item = node.data
const count = node.count
let next: ItemTree | null = null
// Try to find an existing node in this tree level with same item
for (const child of itemtree.children) {
if (child.data == item) {
child.count += 1
child.boughtWhen.aheadCount += node.boughtWhen.aheadCount
child.boughtWhen.evenCount += node.boughtWhen.evenCount
child.boughtWhen.behindCount += node.boughtWhen.behindCount
// Merge platform counts
mergePlatformCounts(child.platformCount, node.platformCount)
next = child
break
}
}
// If not found, add item node at this level
if (next == null && item !== undefined) {
next = {
data: item,
count: count,
children: [],
boughtWhen: { ...node.boughtWhen },
platformCount: { ...node.platformCount },
tags: []
}
itemtree.children.push(next)
}
return next!
}
/*
* Merge a full build path with an existing item tree
*/
function treeMerge(
itemtree: ItemTree,
items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag; platform?: string }>
) {
let current = itemtree
for (const item of items) {
current = nodeMerge(current, {
data: item.itemId,
count: 1,
boughtWhen: {
aheadCount: item.goldAdvantage == 'ahead' ? 1 : 0,
evenCount: item.goldAdvantage == 'even' ? 1 : 0,
behindCount: item.goldAdvantage == 'behind' ? 1 : 0,
meanGold: 0
},
children: [],
platformCount: item.platform ? singlePlatformCount(item.platform) : initPlatformCounts(),
tags: []
})
}
}
function treeCutBranches(itemtree: ItemTree, thresholdCount: number, thresholdPerc: number) {
// Remove branches that are above threshold count
while (itemtree.children.length > thresholdCount) {
const leastUsedBranch = itemtree.children.reduce(
(a, b) => (Math.min(a.count, b.count) == a.count ? a : b),
{
data: undefined,
count: +Infinity,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 },
platformCount: initPlatformCounts(),
tags: []
}
)
itemtree.children.splice(itemtree.children.indexOf(leastUsedBranch), 1)
}
// Remove branches that are of too low usage
const toRemove: Array<ItemTree> = []
for (const child of itemtree.children) {
if (child.count / itemtree.count < thresholdPerc) {
toRemove.push(child)
}
}
for (const tr of toRemove) {
itemtree.children.splice(itemtree.children.indexOf(tr), 1)
}
itemtree.children.map(x => treeCutBranches(x, thresholdCount, thresholdPerc))
}
function treeSort(itemtree: ItemTree) {
itemtree.children.sort((a, b) => b.count - a.count)
for (const item of itemtree.children) {
treeSort(item)
}
}
/*
* Deep clone an ItemTree
*/
function treeClone(tree: ItemTree): ItemTree {
return {
data: tree.data,
count: tree.count,
children: tree.children.map(child => treeClone(child)),
boughtWhen: {
aheadCount: tree.boughtWhen.aheadCount,
behindCount: tree.boughtWhen.behindCount,
evenCount: tree.boughtWhen.evenCount,
meanGold: tree.boughtWhen.meanGold
},
platformCount: { ...tree.platformCount },
tags: [...tree.tags]
}
}
/*
* Merge two ItemTrees into one
*/
function treeMergeTree(t1: ItemTree, t2: ItemTree): ItemTree {
// Merge counts for the root
t1.count += t2.count
// Merge platform counts
mergePlatformCounts(t1.platformCount, t2.platformCount)
// Merge boughtWhen
t1.boughtWhen.aheadCount += t2.boughtWhen.aheadCount
t1.boughtWhen.evenCount += t2.boughtWhen.evenCount
t1.boughtWhen.behindCount += t2.boughtWhen.behindCount
// Merge children from t2 into t1
for (const child2 of t2.children) {
// Find matching child in t1 (same data value)
const matchingChild = t1.children.find(child1 => child1.data === child2.data)
if (matchingChild) {
// Recursively merge matching children
treeMergeTree(matchingChild, child2)
} else {
// Add a deep copy of child2 to t1
t1.children.push(treeClone(child2))
}
}
return t1
}
/*
* Flatten an ItemTree into a Set of item numbers
*/
function treeToSet(itemtree: ItemTree): Set<number> {
const items: Set<number> = new Set()
function traverse(node: ItemTree) {
if (node.data !== undefined) {
items.add(node.data)
}
for (const child of node.children) {
traverse(child)
}
}
traverse(itemtree)
return items
}
/*
* Calculate similarity between two trees as item sets.
* Returns a number between 0 and 1, where 1 means identical and 0 means completely different.
* Uses Jaccard similarity: |A ∩ B| / |A B|
* Sets included in one another will have similarity close to 1.
*/
function areTreeSimilars(t1: ItemTree, t2: ItemTree): number {
const set1 = treeToSet(t1)
const set2 = treeToSet(t2)
// Handle empty sets
if (set1.size === 0 && set2.size === 0) {
return 1.0
}
// Calculate intersection
const intersection = new Set<number>()
for (const item of Array.from(set1)) {
if (set2.has(item)) {
intersection.add(item)
}
}
// Calculate union
const union = new Set<number>()
for (const item of Array.from(set1)) {
union.add(item)
}
for (const item of Array.from(set2)) {
union.add(item)
}
// Jaccard similarity: |intersection| / |union|
const similarity = intersection.size / Math.min(set1.size, set2.size)
// Ensure result is between 0 and 1
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 = REGION_KEYS.reduce((sum, key) => sum + node.platformCount[key], 0)
if (totalRegionCount > 0 && expectedRegionDistribution) {
const totalExpected = REGION_KEYS.reduce((sum, key) => sum + expectedRegionDistribution[key], 0)
if (totalExpected > 0) {
// 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
// Loop through all regions to derive tags
const regionTags: Array<{ key: keyof PlatformCounts; tag: ItemTag }> = [
{ key: 'euw', tag: 'region_euw' },
{ key: 'eun', tag: 'region_eun' },
{ key: 'na', tag: 'region_na' },
{ key: 'kr', tag: 'region_kr' }
]
for (const { key, tag } of regionTags) {
const expectedPct = expectedRegionDistribution[key] / totalExpected
const actualPct = node.platformCount[key] / totalRegionCount
if (actualPct >= expectedPct * SIGNIFICANCE_THRESHOLD && actualPct >= MINIMUM_PCT) {
tags.push(tag)
}
}
}
}
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,
treeDeriveTags
}