UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

256 lines 8.8 kB
/** * Adamic-Adar Index link prediction implementation * * Predicts the likelihood of a link between two nodes based on their common * neighbors, weighted by the inverse logarithm of the neighbors' degrees. * This gives higher weight to rare common neighbors. * * Formula: AA(x,y) = Σ (1 / log(|Γ(z)|)) for all z ∈ Γ(x) ∩ Γ(y) * where Γ(z) is the set of neighbors of node z * * Time complexity: O(k²) where k is the average degree * Space complexity: O(k) */ /** * Calculate Adamic-Adar index for a pair of nodes */ export function adamicAdarScore(graph, source, target, options = {}) { if (!graph.hasNode(source) || !graph.hasNode(target)) { return 0; } const { directed = false } = options; // Get neighbors const sourceNeighbors = new Set(directed ? Array.from(graph.outNeighbors(source)) : Array.from(graph.neighbors(source))); const targetNeighbors = new Set(directed ? Array.from(graph.inNeighbors(target)) : Array.from(graph.neighbors(target))); // Calculate Adamic-Adar score let score = 0; for (const neighbor of sourceNeighbors) { if (targetNeighbors.has(neighbor)) { const degree = directed ? graph.outDegree(neighbor) : // Use out-degree for directed graphs graph.degree(neighbor); // Use total degree for undirected graphs if (degree > 1) { score += 1 / Math.log(degree); } else if (degree === 1) { // For degree 1, we can't use log(1) = 0, so use a small constant score += 1; // or some other reasonable value } } } return score; } /** * Calculate Adamic-Adar scores for all possible node pairs */ export function adamicAdarPrediction(graph, options = {}) { const { directed = false, includeExisting = false, topK, } = options; const scores = []; const nodes = Array.from(graph.nodes()).map((n) => n.id); for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const source = nodes[i]; const target = nodes[j]; if (!source || !target) { continue; } // Skip existing edges unless requested if (!includeExisting && graph.hasEdge(source, target)) { continue; } const score = adamicAdarScore(graph, source, target, { directed }); if (score > 0) { scores.push({ source, target, score }); // For undirected graphs, also add the reverse pair if (!directed && source !== target) { scores.push({ source: target, target: source, score }); } } } } // Sort by score in descending order scores.sort((a, b) => b.score - a.score); // Return top K if specified if (topK && topK > 0) { return scores.slice(0, topK); } return scores; } /** * Calculate Adamic-Adar scores for specific node pairs */ export function adamicAdarForPairs(graph, pairs, options = {}) { return pairs.map(([source, target]) => ({ source, target, score: adamicAdarScore(graph, source, target, options), })); } /** * Get top Adamic-Adar candidates for link prediction for a specific node */ export function getTopAdamicAdarCandidatesForNode(graph, node, options = {}) { if (!graph.hasNode(node)) { return []; } const { directed = false, includeExisting = false, topK = 10, candidates, } = options; const scores = []; const targetNodes = candidates ?? Array.from(graph.nodes()).map((n) => n.id); for (const target of targetNodes) { if (target === node) { continue; } // Skip existing edges unless requested if (!includeExisting && graph.hasEdge(node, target)) { continue; } const score = adamicAdarScore(graph, node, target, { directed }); if (score > 0) { scores.push({ source: node, target, score }); } } // Sort by score in descending order scores.sort((a, b) => b.score - a.score); return scores.slice(0, topK); } /** * Calculate precision and recall for Adamic-Adar link prediction evaluation */ export function evaluateAdamicAdar(trainingGraph, testEdges, nonEdges, options = {}) { // Get scores for test edges and non-edges const testScores = adamicAdarForPairs(trainingGraph, testEdges, options); const nonEdgeScores = adamicAdarForPairs(trainingGraph, nonEdges, options); // Combine and sort all scores const allScores = [ ...testScores.map((s) => ({ ...s, isActualEdge: true })), ...nonEdgeScores.map((s) => ({ ...s, isActualEdge: false })), ].sort((a, b) => b.score - a.score); // Calculate precision and recall at different thresholds let truePositives = 0; let falsePositives = 0; let bestF1 = 0; let bestPrecision = 0; let bestRecall = 0; const totalPositives = testEdges.length; for (const scoreItem of allScores) { if (scoreItem.isActualEdge) { truePositives++; } else { falsePositives++; } const precision = truePositives / (truePositives + falsePositives); const recall = truePositives / totalPositives; const f1 = precision + recall > 0 ? 2 * (precision * recall) / (precision + recall) : 0; if (f1 > bestF1) { bestF1 = f1; bestPrecision = precision; bestRecall = recall; } } // Calculate AUC (Area Under Curve) let auc = 0; let tpCount = 0; let fpCount = 0; for (const item of allScores) { if (item.isActualEdge) { tpCount++; } else { auc += tpCount; fpCount++; } } if (tpCount > 0 && fpCount > 0) { auc = auc / (tpCount * fpCount); } else { auc = 0.5; // Random performance } return { precision: bestPrecision, recall: bestRecall, f1Score: bestF1, auc, }; } /** * Compare Adamic-Adar with Common Neighbors for the same dataset */ export function compareAdamicAdarWithCommonNeighbors(graph, testEdges, nonEdges, options = {}) { // Import common neighbors evaluation function // Since we're using ES modules, we can't use require. Instead, we'll implement a simple version here const commonNeighborsPairs = (pairs) => pairs.map(([source, target]) => ({ source, target, score: (() => { const sourceNeighbors = new Set(Array.from(graph.neighbors(source))); const targetNeighbors = new Set(Array.from(graph.neighbors(target))); let count = 0; for (const n of sourceNeighbors) { if (targetNeighbors.has(n)) { count++; } } return count; })(), })); const testScores = commonNeighborsPairs(testEdges); const nonEdgeScores = commonNeighborsPairs(nonEdges); const allScores = [ ...testScores.map((s) => ({ ...s, isActualEdge: true })), ...nonEdgeScores.map((s) => ({ ...s, isActualEdge: false })), ].sort((a, b) => b.score - a.score); let truePositives = 0; let falsePositives = 0; let bestF1 = 0; let bestPrecision = 0; let bestRecall = 0; const totalPositives = testEdges.length; for (const scoreItem of allScores) { if (scoreItem.isActualEdge) { truePositives++; } else { falsePositives++; } const precision = truePositives / (truePositives + falsePositives); const recall = truePositives / totalPositives; const f1 = precision + recall > 0 ? 2 * (precision * recall) / (precision + recall) : 0; if (f1 > bestF1) { bestF1 = f1; bestPrecision = precision; bestRecall = recall; } } let auc = 0; let tpCount = 0; let fpCount = 0; for (const item of allScores) { if (item.isActualEdge) { tpCount++; } else { auc += tpCount; fpCount++; } } if (tpCount > 0 && fpCount > 0) { auc = auc / (tpCount * fpCount); } else { auc = 0.5; } const commonNeighborsResults = { precision: bestPrecision, recall: bestRecall, f1Score: bestF1, auc, }; const adamicAdarResults = evaluateAdamicAdar(graph, testEdges, nonEdges, options); return { adamicAdar: adamicAdarResults, commonNeighbors: commonNeighborsResults, }; } //# sourceMappingURL=adamic-adar.js.map