@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
256 lines • 8.8 kB
JavaScript
/**
* 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