UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

155 lines 5.31 kB
/** * Calculate common neighbors score for a pair of nodes */ export function commonNeighborsScore(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))); // Count common neighbors let commonCount = 0; for (const neighbor of sourceNeighbors) { if (targetNeighbors.has(neighbor)) { commonCount++; } } return commonCount; } /** * Calculate common neighbors scores for all possible node pairs */ export function commonNeighborsPrediction(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 = commonNeighborsScore(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 common neighbors scores for specific node pairs */ export function commonNeighborsForPairs(graph, pairs, options = {}) { return pairs.map(([source, target]) => ({ source, target, score: commonNeighborsScore(graph, source, target, options), })); } /** * Get top candidates for link prediction for a specific node */ export function getTopCandidatesForNode(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 = commonNeighborsScore(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 link prediction evaluation */ export function evaluateCommonNeighbors(trainingGraph, testEdges, nonEdges, options = {}) { // Get scores for test edges and non-edges const testScores = commonNeighborsForPairs(trainingGraph, testEdges, options); const nonEdgeScores = commonNeighborsForPairs(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, }; } //# sourceMappingURL=common-neighbors.js.map