Skip to content

Commit

Permalink
[metrics] Adds new Louvain ambiguity mixed metric
Browse files Browse the repository at this point in the history
Details:
- Upgrades graphology-communities-louvain to get the new experimental
  version to better sample the space of acceptable partitions
- Adds new mixed metric louvainEdgeAmbiguity
- Adds new dev.json dev translations for Louvain edge ambiguity
  • Loading branch information
jacomyal committed Dec 18, 2024
1 parent 24fd520 commit db0ed2a
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 7 deletions.
11 changes: 6 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"copy-to-clipboard": "^3.3.3",
"file-saver": "^2.0.5",
"graphology": "^0.25.4",
"graphology-communities-louvain": "^2.0.1",
"graphology-communities-louvain": "^2.0.2",
"graphology-gexf": "^0.13.2",
"graphology-graphml": "^0.5.2",
"graphology-layout": "^0.6.1",
Expand Down
4 changes: 4 additions & 0 deletions src/core/metrics/collections.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { disparityMetric } from "./edges/disparityMetric";
import { edgeScript } from "./edges/edgeScript";
import { simmelianStrengthMetric } from "./edges/simmelianStrength";
import { louvainEdgeAmbiguity } from "./mixed/louvainEdgeAmbiguity";
import { betweennessCentralityMetric } from "./nodes/betweennessCentralityMetric";
import { degreeMetric } from "./nodes/degreeMetric";
import { hitsMetric } from "./nodes/hitsMetric";
Expand All @@ -21,3 +22,6 @@ export const NODE_METRICS: Metric<{ nodes: any }>[] = [

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const EDGE_METRICS: Metric<{ edges: any }>[] = [disparityMetric, simmelianStrengthMetric, edgeScript];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const MIXED_METRICS: Metric<{ edges: any; nodes: any }>[] = [louvainEdgeAmbiguity];
108 changes: 108 additions & 0 deletions src/core/metrics/mixed/louvainEdgeAmbiguity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Graph from "graphology";
import louvain from "graphology-communities-louvain/experimental/robust-randomness";
import { mapValues, mean, zipObject } from "lodash";

import { FullGraph } from "../../graph/types";
import { Metric } from "../types";
import { quantitativeOnly } from "../utils";

function computeLouvainEdgeScores(
graph: Graph,
{
runs,
getEdgeWeight,
resolution,
}: {
runs: number;
getEdgeWeight?: string;
resolution: number;
},
) {
const edgeScores: { [edge: string]: number } = {};

// Init:
graph.forEachEdge((e, _) => {
edgeScores[e] = 0;
});

// Accumulate co-membership occurrences:
for (let i = 0; i < runs; i++) {
const communities = louvain(graph, {
resolution,
getEdgeWeight: getEdgeWeight || null,
});
graph.forEachEdge((e, _, source, target) => {
if (communities[source] === communities[target]) edgeScores[e]++;
});
}

const coMembershipEdgeScores = mapValues(edgeScores, (v) => v / runs);
const ambiguityEdgeScores = mapValues(edgeScores, (v) => v * (1 - v) * 4);
const nodes = graph.nodes();
const meanAmbiguityNodeScores = zipObject(
nodes,
nodes.map((n) => mean(graph.mapEdges(n, (e) => ambiguityEdgeScores[e]))),
);

return {
coMembershipEdgeScores,
ambiguityEdgeScores,
meanAmbiguityNodeScores,
};
}

export const louvainEdgeAmbiguity: Metric<{
edges: ["coMembershipScore", "ambiguityScore"];
nodes: ["meanAmbiguityScore"];
}> = {
id: "louvainEdgeAmbiguity",
description: true,
outputs: {
edges: { coMembershipScore: quantitativeOnly, ambiguityScore: quantitativeOnly },
nodes: { meanAmbiguityScore: quantitativeOnly },
},
parameters: [
{
id: "runs",
type: "number",
defaultValue: 50,
},
{
id: "getEdgeWeight",
type: "attribute",
itemType: "edges",
restriction: "quantitative",
},
{
id: "resolution",
type: "number",
defaultValue: 1,
},
],
fn(
parameters: {
runs: number;
scoreType: "coMembership" | "ambiguity";
getEdgeWeight?: string;
fastLocalMoves: boolean;
randomWalk: boolean;
resolution: number;
},
graph: FullGraph,
) {
const { coMembershipEdgeScores, ambiguityEdgeScores, meanAmbiguityNodeScores } = computeLouvainEdgeScores(
graph,
parameters,
);

return {
edges: {
coMembershipScore: coMembershipEdgeScores,
ambiguityScore: ambiguityEdgeScores,
},
nodes: {
meanAmbiguityScore: meanAmbiguityNodeScores,
},
};
},
};
25 changes: 25 additions & 0 deletions src/locales/dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,31 @@
"custom": "Attribute name on which to store the metric"
}
}
},
"mixed": {
"louvainEdgeAmbiguity": {
"title": "Louvain edges ambiguity",
"description": "This algorithm helps exploring ambiguity in Louvain community detection. It runs the Louvain algorithm N times, and analyzes how consistent Louvain is along multiple runs, looking on each edge if its extremities are in the same cluster or not..",
"attributes": {
"coMembershipScore": "Edge co-membership attribute name (i.e. the percentage of times Louvain set the same community for both of its extremities)",
"ambiguityScore": "Edge ambiguity score name (the inverse of the consensus of all the Louvain runs, 0 when all Louvain runs agree, and 1 when they disagree the most)",
"meanAmbiguityScore": "Node average linked edges ambiguity score attribute name"
},
"parameters": {
"runs": {
"title": "Louvain runs",
"description": "The number of times to run Louvain algorithm."
},
"getEdgeWeight": {
"title": "Louvain edge weight attribute",
"description": "An edge attribute that would represent edge weights."
},
"resolution": {
"title": "Louvain resolution",
"description": "An increased resolution should produce more communities."
}
}
}
}
},
"layouts": {
Expand Down
10 changes: 9 additions & 1 deletion src/views/graphPage/StatisticsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { BooleanInput, EnumInput, NumberInput, StringInput } from "../../compone
import { useFilteredGraph, useGraphDataset, useGraphDatasetActions } from "../../core/context/dataContexts";
import { FieldModel } from "../../core/graph/types";
import { computeMetric } from "../../core/metrics";
import { EDGE_METRICS, NODE_METRICS } from "../../core/metrics/collections";
import { EDGE_METRICS, MIXED_METRICS, NODE_METRICS } from "../../core/metrics/collections";
import { Metric, MetricScriptParameter } from "../../core/metrics/types";
import { useModal } from "../../core/modals";
import { useNotifications } from "../../core/notifications";
Expand Down Expand Up @@ -367,6 +367,14 @@ export const StatisticsPanel: FC = () => {
metric,
})),
},
{
label: capitalize(t("graph.model.mixed") as string),
options: MIXED_METRICS.map((metric) => ({
value: metric.id,
label: t(`statistics.mixed.${metric.id}.title`),
metric,
})),
},
],
[t],
);
Expand Down

0 comments on commit db0ed2a

Please sign in to comment.