refactor/match_collector: change folder structure
This commit is contained in:
344
match_collector/src/item_tree.ts
Normal file
344
match_collector/src/item_tree.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user