gephi-ai 1.18.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gephi_ai-1.18.0/.gitignore +27 -0
- gephi_ai-1.18.0/PKG-INFO +89 -0
- gephi_ai-1.18.0/README.md +55 -0
- gephi_ai-1.18.0/bipartite.py +116 -0
- gephi_ai-1.18.0/caveats.json +189 -0
- gephi_ai-1.18.0/community_stability.py +120 -0
- gephi_ai-1.18.0/figure.py +375 -0
- gephi_ai-1.18.0/gephi_mcp.py +3139 -0
- gephi_ai-1.18.0/gephi_mcp_viewer/__init__.py +371 -0
- gephi_ai-1.18.0/gephi_mcp_viewer/assets/VENDORED.md +11 -0
- gephi_ai-1.18.0/gephi_mcp_viewer/assets/graphology.umd.min.js +2 -0
- gephi_ai-1.18.0/gephi_mcp_viewer/assets/sigma.min.js +1369 -0
- gephi_ai-1.18.0/gephi_mcp_viewer/community_layout.py +215 -0
- gephi_ai-1.18.0/gephi_mcp_viewer/profile.py +191 -0
- gephi_ai-1.18.0/gephi_mcp_viewer/similarity.py +116 -0
- gephi_ai-1.18.0/gephi_mcp_viewer/template.html +526 -0
- gephi_ai-1.18.0/graph_diff.py +105 -0
- gephi_ai-1.18.0/legend.py +133 -0
- gephi_ai-1.18.0/probe_verdicts.py +127 -0
- gephi_ai-1.18.0/pyproject.toml +92 -0
- gephi_ai-1.18.0/requirements.txt +4 -0
- gephi_ai-1.18.0/session_ledger.py +91 -0
- gephi_ai-1.18.0/stats_integrity.py +257 -0
- gephi_ai-1.18.0/tests/live_scale_test.py +133 -0
- gephi_ai-1.18.0/tests/live_smoke_test.py +422 -0
- gephi_ai-1.18.0/tests/probes/record.py +105 -0
- gephi_ai-1.18.0/tests/probes/test_probes.py +268 -0
- gephi_ai-1.18.0/tests/test_bipartite.py +136 -0
- gephi_ai-1.18.0/tests/test_centralization.py +95 -0
- gephi_ai-1.18.0/tests/test_claim_record.py +140 -0
- gephi_ai-1.18.0/tests/test_community_stability.py +140 -0
- gephi_ai-1.18.0/tests/test_community_stability_tool.py +183 -0
- gephi_ai-1.18.0/tests/test_edge_cases.py +313 -0
- gephi_ai-1.18.0/tests/test_enrichment.py +318 -0
- gephi_ai-1.18.0/tests/test_figure.py +188 -0
- gephi_ai-1.18.0/tests/test_filter_disclosure.py +140 -0
- gephi_ai-1.18.0/tests/test_graph_diff.py +127 -0
- gephi_ai-1.18.0/tests/test_group_c_tools.py +128 -0
- gephi_ai-1.18.0/tests/test_legend.py +121 -0
- gephi_ai-1.18.0/tests/test_legend_tool.py +229 -0
- gephi_ai-1.18.0/tests/test_probe_recording.py +149 -0
- gephi_ai-1.18.0/tests/test_register_is_shipped_pristine.py +76 -0
- gephi_ai-1.18.0/tests/test_repo_docs.py +159 -0
- gephi_ai-1.18.0/tests/test_session_ledger.py +135 -0
- gephi_ai-1.18.0/tests/test_stat_caveat_attachment.py +306 -0
- gephi_ai-1.18.0/tests/test_stats_integrity.py +220 -0
- gephi_ai-1.18.0/tests/test_text_network.py +495 -0
- gephi_ai-1.18.0/tests/test_tools.py +642 -0
- gephi_ai-1.18.0/tests/test_undo.py +296 -0
- gephi_ai-1.18.0/tests/test_viewer.py +680 -0
- gephi_ai-1.18.0/text_network.py +674 -0
- gephi_ai-1.18.0/uv.lock +1994 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
gephi-ai-plugin/target/
|
|
2
|
+
|
|
3
|
+
# Personal Claude Code config (machine-specific hooks, not for other contributors)
|
|
4
|
+
.claude/settings.local.json
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.pyc
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.venv/
|
|
14
|
+
mcp-server/dist/
|
|
15
|
+
|
|
16
|
+
# MCPB build artifacts
|
|
17
|
+
mcpb/server/lib/
|
|
18
|
+
*.mcpb
|
|
19
|
+
|
|
20
|
+
# Local drafts and internal notes — never publish (LinkedIn posts, papers, scratch)
|
|
21
|
+
docs/
|
|
22
|
+
*-draft.md
|
|
23
|
+
*-draft.*
|
|
24
|
+
|
|
25
|
+
# Probe verdicts are per-install and must never ship or be committed
|
|
26
|
+
mcp-server/caveats.local.json
|
|
27
|
+
.artifact-verified
|
gephi_ai-1.18.0/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: gephi-ai
|
|
3
|
+
Version: 1.18.0
|
|
4
|
+
Summary: MCP Server for controlling Gephi Desktop via remote API
|
|
5
|
+
Project-URL: Homepage, https://www.mattartz.me
|
|
6
|
+
Project-URL: Repository, https://github.com/MattArtzAnthro/gephi-ai
|
|
7
|
+
Author: Matt Artz
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: claude,gephi,graph,mcp,network,visualization
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: defusedxml>=0.7.1
|
|
21
|
+
Requires-Dist: httpx>=0.25.0
|
|
22
|
+
Requires-Dist: mcp<3,>=2.1
|
|
23
|
+
Requires-Dist: nltk>=3.8
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: pillow>=10.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: scipy>=1.10
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# gephi-ai
|
|
36
|
+
|
|
37
|
+
MCP server that bridges any [Model Context Protocol](https://modelcontextprotocol.io) client
|
|
38
|
+
to a running [Gephi Desktop](https://gephi.org) instance, exposing **113 tools** for graph
|
|
39
|
+
construction, statistics, community detection, layout, styling, filtering, and
|
|
40
|
+
publication-ready export.
|
|
41
|
+
|
|
42
|
+
It translates MCP tool calls into HTTP requests against the Gephi AI plugin's local API
|
|
43
|
+
(`http://127.0.0.1:8080`). Each tool has a typed signature, so clients receive a precise
|
|
44
|
+
per-field JSON schema rather than an opaque blob.
|
|
45
|
+
|
|
46
|
+
This is the **MCP server** component of [gephi-ai](https://github.com/MattArtzAnthro/gephi-ai);
|
|
47
|
+
see the top-level repository for the Gephi plugin, the Claude Code plugin, and full docs.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
No install needed with [uv](https://docs.astral.sh/uv/) — point your MCP client at:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uvx gephi-ai
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`uvx` fetches [`gephi-ai` from PyPI](https://pypi.org/project/gephi-ai/) on first run
|
|
58
|
+
and caches it. For a persistent `gephi-ai` command on your `PATH` instead, use
|
|
59
|
+
`pipx install gephi-ai` (or `pipx install .` from this directory). Avoid plain
|
|
60
|
+
`pip install -e .` inside a virtual environment: the command is then only visible on
|
|
61
|
+
that venv's `PATH`, and MCP clients launched outside your shell won't find it.
|
|
62
|
+
|
|
63
|
+
## Use
|
|
64
|
+
|
|
65
|
+
The Gephi AI plugin must be installed and Gephi Desktop running first. Then point any MCP
|
|
66
|
+
client at the `gephi-ai` command, e.g. for Claude Desktop:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{ "mcpServers": { "gephi-mcp": { "command": "uvx", "args": ["gephi-ai"] } } }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
| Env var | Default | Purpose |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `GEPHI_API_URL` | `http://127.0.0.1:8080` | Gephi plugin HTTP API base URL |
|
|
77
|
+
| `GEPHI_REQUEST_TIMEOUT` | `60.0` | Per-request timeout (seconds) |
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install -e . pytest pytest-asyncio ruff
|
|
83
|
+
ruff check .
|
|
84
|
+
pytest -q
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
Apache-2.0 — see the [repository LICENSE](https://github.com/MattArtzAnthro/gephi-ai/blob/main/LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# gephi-ai
|
|
2
|
+
|
|
3
|
+
MCP server that bridges any [Model Context Protocol](https://modelcontextprotocol.io) client
|
|
4
|
+
to a running [Gephi Desktop](https://gephi.org) instance, exposing **113 tools** for graph
|
|
5
|
+
construction, statistics, community detection, layout, styling, filtering, and
|
|
6
|
+
publication-ready export.
|
|
7
|
+
|
|
8
|
+
It translates MCP tool calls into HTTP requests against the Gephi AI plugin's local API
|
|
9
|
+
(`http://127.0.0.1:8080`). Each tool has a typed signature, so clients receive a precise
|
|
10
|
+
per-field JSON schema rather than an opaque blob.
|
|
11
|
+
|
|
12
|
+
This is the **MCP server** component of [gephi-ai](https://github.com/MattArtzAnthro/gephi-ai);
|
|
13
|
+
see the top-level repository for the Gephi plugin, the Claude Code plugin, and full docs.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
No install needed with [uv](https://docs.astral.sh/uv/) — point your MCP client at:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uvx gephi-ai
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`uvx` fetches [`gephi-ai` from PyPI](https://pypi.org/project/gephi-ai/) on first run
|
|
24
|
+
and caches it. For a persistent `gephi-ai` command on your `PATH` instead, use
|
|
25
|
+
`pipx install gephi-ai` (or `pipx install .` from this directory). Avoid plain
|
|
26
|
+
`pip install -e .` inside a virtual environment: the command is then only visible on
|
|
27
|
+
that venv's `PATH`, and MCP clients launched outside your shell won't find it.
|
|
28
|
+
|
|
29
|
+
## Use
|
|
30
|
+
|
|
31
|
+
The Gephi AI plugin must be installed and Gephi Desktop running first. Then point any MCP
|
|
32
|
+
client at the `gephi-ai` command, e.g. for Claude Desktop:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{ "mcpServers": { "gephi-mcp": { "command": "uvx", "args": ["gephi-ai"] } } }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
| Env var | Default | Purpose |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `GEPHI_API_URL` | `http://127.0.0.1:8080` | Gephi plugin HTTP API base URL |
|
|
43
|
+
| `GEPHI_REQUEST_TIMEOUT` | `60.0` | Per-request timeout (seconds) |
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install -e . pytest pytest-asyncio ruff
|
|
49
|
+
ruff check .
|
|
50
|
+
pytest -q
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
Apache-2.0 — see the [repository LICENSE](https://github.com/MattArtzAnthro/gephi-ai/blob/main/LICENSE).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Two-mode networks: people by events, authors by concepts, informants by sites.
|
|
2
|
+
|
|
3
|
+
Gephi draws a two-mode network as though every node were the same kind of thing, which
|
|
4
|
+
misrepresents the data at a glance and has no fix in the application: gephi/gephi#3131 asked for a
|
|
5
|
+
bipartite layout and notes the only plugin offering one was removed from the current release, and
|
|
6
|
+
gephi-plugins#130 asked for multimode support in 2016. Projection, the operation that collapses a
|
|
7
|
+
two-mode network into a one-mode one, does not exist in Gephi at all.
|
|
8
|
+
|
|
9
|
+
Both are computed here in Python. The layout is pushed as coordinates and the projection as a new
|
|
10
|
+
graph, so neither needs a Gephi layout plugin or any change to the Java side.
|
|
11
|
+
|
|
12
|
+
Gephi has no concept of a node's mode, so the mode is whatever column the researcher names. That
|
|
13
|
+
is a convention rather than a property of the data, which is why a column carrying more than two
|
|
14
|
+
values is refused rather than guessed at.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from itertools import combinations
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
Graph = dict[str, Any]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _value(attributes: dict[str, Any], column: str) -> Any:
|
|
26
|
+
wanted = column.replace("_", "").replace(" ", "").lower()
|
|
27
|
+
for key, value in (attributes or {}).items():
|
|
28
|
+
if key.replace("_", "").replace(" ", "").lower() == wanted:
|
|
29
|
+
return value
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def split_modes(graph: Graph, column: str) -> tuple[list[str], list[str]]:
|
|
34
|
+
"""The two groups of nodes named by `column`, in the order the values first appear."""
|
|
35
|
+
seen: dict[Any, list[str]] = {}
|
|
36
|
+
for node in graph.get("nodes", []):
|
|
37
|
+
value = _value(node.get("attributes"), column)
|
|
38
|
+
if value is None:
|
|
39
|
+
continue
|
|
40
|
+
seen.setdefault(value, []).append(node["key"])
|
|
41
|
+
if not seen:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"No node carries a value in {column!r}, so the two modes cannot be told apart.")
|
|
44
|
+
if len(seen) != 2:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"{column!r} holds {len(seen)} distinct values ({sorted(map(str, seen))}). A bipartite "
|
|
47
|
+
"graph has exactly two modes; name a column that separates them.")
|
|
48
|
+
left, right = seen.values()
|
|
49
|
+
return left, right
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def project_bipartite(graph: Graph, column: str, keep: str) -> dict[str, Any]:
|
|
53
|
+
"""Collapse a two-mode network onto one mode, joining nodes that share a partner.
|
|
54
|
+
|
|
55
|
+
Two people who attended the same event become connected, weighted by how many events they
|
|
56
|
+
shared. Nodes sharing nothing are kept with no edges: dropping them would silently delete
|
|
57
|
+
people from the network, which is a different graph rather than a tidier one.
|
|
58
|
+
"""
|
|
59
|
+
left, right = split_modes(graph, column)
|
|
60
|
+
modes = {}
|
|
61
|
+
for node in graph.get("nodes", []):
|
|
62
|
+
modes[node["key"]] = _value(node.get("attributes"), column)
|
|
63
|
+
|
|
64
|
+
kept = [k for k in (left + right) if str(modes.get(k)) == str(keep)]
|
|
65
|
+
if not kept:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"No node has {column}={keep!r}. Present values: "
|
|
68
|
+
f"{sorted({str(v) for v in modes.values() if v is not None})}.")
|
|
69
|
+
|
|
70
|
+
partners: dict[str, set[str]] = {k: set() for k in kept}
|
|
71
|
+
within = 0
|
|
72
|
+
for edge in graph.get("edges", []):
|
|
73
|
+
s, t = edge["source"], edge["target"]
|
|
74
|
+
if modes.get(s) == modes.get(t):
|
|
75
|
+
within += 1
|
|
76
|
+
continue
|
|
77
|
+
for node, other in ((s, t), (t, s)):
|
|
78
|
+
if node in partners:
|
|
79
|
+
partners[node].add(other)
|
|
80
|
+
|
|
81
|
+
edges = []
|
|
82
|
+
for a, b in combinations(sorted(kept), 2):
|
|
83
|
+
shared = len(partners[a] & partners[b])
|
|
84
|
+
if shared:
|
|
85
|
+
edges.append({"source": a, "target": b, "weight": shared})
|
|
86
|
+
|
|
87
|
+
result: dict[str, Any] = {
|
|
88
|
+
"nodes": sorted(kept),
|
|
89
|
+
"edges": edges,
|
|
90
|
+
"kept_mode": keep,
|
|
91
|
+
"within_mode_edges": within,
|
|
92
|
+
}
|
|
93
|
+
if within:
|
|
94
|
+
result["warning"] = (
|
|
95
|
+
f"{within} edge(s) join two nodes of the same mode, so this graph is not bipartite. "
|
|
96
|
+
"Those edges were ignored in the projection; check the mode column before relying on "
|
|
97
|
+
"the result.")
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def bipartite_positions(graph: Graph, column: str, *,
|
|
102
|
+
separation: float = 600.0, spacing: float = 60.0) -> list[dict[str, Any]]:
|
|
103
|
+
"""Coordinates placing each mode in its own column, which is what makes the shape legible.
|
|
104
|
+
|
|
105
|
+
Returned in the shape gephi_batch_set_positions accepts, so the layout is pushed rather than
|
|
106
|
+
implemented as a Gephi layout plugin.
|
|
107
|
+
"""
|
|
108
|
+
left, right = split_modes(graph, column)
|
|
109
|
+
positions = []
|
|
110
|
+
for index, members in enumerate((left, right)):
|
|
111
|
+
x = -separation / 2 if index == 0 else separation / 2
|
|
112
|
+
span = (len(members) - 1) * spacing
|
|
113
|
+
for row, key in enumerate(members):
|
|
114
|
+
positions.append({"id": key, "x": float(x),
|
|
115
|
+
"y": float(row * spacing - span / 2)})
|
|
116
|
+
return positions
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"note": "Known defects in Gephi's own statistics, surfaced alongside the numbers they affect. An entry is only asserted as live once a probe has reproduced it against a running Gephi; until then its status is 'unverified' and its text says so. Run the probe suite (tests/probes/) to update the verification blocks.",
|
|
4
|
+
"caveats": [
|
|
5
|
+
{
|
|
6
|
+
"id": "gephi-2034",
|
|
7
|
+
"issues": [
|
|
8
|
+
"https://github.com/gephi/gephi/issues/2034"
|
|
9
|
+
],
|
|
10
|
+
"metrics": [
|
|
11
|
+
"modularity"
|
|
12
|
+
],
|
|
13
|
+
"applies_when": {
|
|
14
|
+
"param_not_default": {
|
|
15
|
+
"name": "resolution",
|
|
16
|
+
"default": 1.0
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"severity": "reporting",
|
|
20
|
+
"says": "Gephi applies the modularity resolution parameter as the RECIPROCAL of the convention used in Blondel et al., the paper its own dialogue cites. A resolution reported from Gephi does not mean what a reader of that literature will take it to mean, and higher values here yield FEWER communities rather than more. The issue has been open since 2018 and concludes it cannot be changed without invalidating every value users have already recorded, so treat this as permanent. Not verified against your Gephi: run the probe suite to confirm it still holds.",
|
|
21
|
+
"reported_open_since": "2018-10-03",
|
|
22
|
+
"verification": {
|
|
23
|
+
"status": "unverified",
|
|
24
|
+
"probe": "probe_gephi_2034"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "gephi-1872",
|
|
29
|
+
"issues": [
|
|
30
|
+
"https://github.com/gephi/gephi/issues/1872"
|
|
31
|
+
],
|
|
32
|
+
"metrics": [
|
|
33
|
+
"closeness",
|
|
34
|
+
"harmonic_closeness",
|
|
35
|
+
"avg_path_length"
|
|
36
|
+
],
|
|
37
|
+
"applies_when": {
|
|
38
|
+
"always": true
|
|
39
|
+
},
|
|
40
|
+
"severity": "reporting",
|
|
41
|
+
"says": "Closeness centrality is normalised whether or not the normalisation checkbox was ticked, while betweenness respects the setting. Two measures from the same dialogue therefore behave differently, and a closeness value cannot be assumed to be on the scale you asked for. This one cannot be checked automatically from here, so it is reported as filed rather than confirmed.",
|
|
42
|
+
"reported_open_since": "2017-12-09",
|
|
43
|
+
"verification": {
|
|
44
|
+
"status": "not_probeable",
|
|
45
|
+
"why": "Cannot be probed through this API: confirming it needs the normalisation toggle off, and the statistics endpoint exposes no such parameter."
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "gephi-2191",
|
|
50
|
+
"issues": [
|
|
51
|
+
"https://github.com/gephi/gephi/issues/2191"
|
|
52
|
+
],
|
|
53
|
+
"metrics": [
|
|
54
|
+
"pagerank"
|
|
55
|
+
],
|
|
56
|
+
"applies_when": {
|
|
57
|
+
"undirected": true
|
|
58
|
+
},
|
|
59
|
+
"severity": "wrong",
|
|
60
|
+
"says": "PageRank is reported to be computed incorrectly for undirected graphs, and this graph is undirected. Treat the values as unreliable for ranking until checked against an independent implementation. Not verified against your Gephi: run the probe suite to confirm it still holds.",
|
|
61
|
+
"reported_open_since": "2020-05-20",
|
|
62
|
+
"verification": {
|
|
63
|
+
"status": "unverified",
|
|
64
|
+
"why": "Deciding this needs an independent implementation of the measure to compare against, and a mismatch of conventions between the two would look identical to a fix. No probe is written rather than one that could give a false all-clear."
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "gephi-557",
|
|
69
|
+
"issues": [
|
|
70
|
+
"https://github.com/gephi/gephi/issues/557",
|
|
71
|
+
"https://github.com/gephi/gephi/issues/1817"
|
|
72
|
+
],
|
|
73
|
+
"metrics": [
|
|
74
|
+
"betweenness",
|
|
75
|
+
"closeness",
|
|
76
|
+
"harmonic_closeness",
|
|
77
|
+
"eigenvector"
|
|
78
|
+
],
|
|
79
|
+
"applies_when": {
|
|
80
|
+
"weights_vary": true
|
|
81
|
+
},
|
|
82
|
+
"severity": "wrong",
|
|
83
|
+
"says": "This graph carries edge weights that vary, but Gephi's centrality measures are reported not to use them, so the result describes an unweighted version of your network. If the weights carry meaning, this number is not measuring what you think it measures. Open since 2012. Not verified against your Gephi: run the probe suite to confirm it still holds.",
|
|
84
|
+
"reported_open_since": "2012-03-14",
|
|
85
|
+
"verification": {
|
|
86
|
+
"status": "unverified",
|
|
87
|
+
"probe": "probe_gephi_557"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "gephi-modularity-unstable",
|
|
92
|
+
"issues": [
|
|
93
|
+
"https://github.com/gephi/gephi/issues/2002",
|
|
94
|
+
"https://github.com/gephi/gephi/issues/2735",
|
|
95
|
+
"https://github.com/gephi/gephi/issues/2888"
|
|
96
|
+
],
|
|
97
|
+
"metrics": [
|
|
98
|
+
"modularity"
|
|
99
|
+
],
|
|
100
|
+
"applies_when": {
|
|
101
|
+
"always": true
|
|
102
|
+
},
|
|
103
|
+
"severity": "unstable",
|
|
104
|
+
"says": "This partition is one draw, not the answer. Gephi's community detection is reported to give different results between runs with randomisation switched off, to change with the order the node and edge tables were imported, and to change after a layout has been run, which should not touch a partition at all. Do not describe these communities as findings until you know they are stable: call gephi_community_stability to run detection repeatedly and report which groups hold up. Not verified against your Gephi: run the probe suite to confirm it still holds.",
|
|
105
|
+
"reported_open_since": "2018-08-01",
|
|
106
|
+
"verification": {
|
|
107
|
+
"status": "unverified",
|
|
108
|
+
"probe": "probe_modularity_unstable"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "gephi-2951",
|
|
113
|
+
"issues": [
|
|
114
|
+
"https://github.com/gephi/gephi/issues/2951"
|
|
115
|
+
],
|
|
116
|
+
"metrics": [
|
|
117
|
+
"closeness",
|
|
118
|
+
"harmonic_closeness"
|
|
119
|
+
],
|
|
120
|
+
"applies_when": {
|
|
121
|
+
"always": true
|
|
122
|
+
},
|
|
123
|
+
"severity": "wrong",
|
|
124
|
+
"says": "Closeness centrality is reported to produce wrong results. The report is untriaged, so the conditions under which it goes wrong are not established. Not verified against your Gephi: run the probe suite to confirm it still holds.",
|
|
125
|
+
"reported_open_since": "2024-10-11",
|
|
126
|
+
"verification": {
|
|
127
|
+
"status": "unverified",
|
|
128
|
+
"why": "Deciding this needs an independent implementation of the measure to compare against, and a mismatch of conventions between the two would look identical to a fix. No probe is written rather than one that could give a false all-clear."
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "gephi-2145",
|
|
133
|
+
"issues": [
|
|
134
|
+
"https://github.com/gephi/gephi/issues/2145"
|
|
135
|
+
],
|
|
136
|
+
"metrics": [
|
|
137
|
+
"eigenvector"
|
|
138
|
+
],
|
|
139
|
+
"applies_when": {
|
|
140
|
+
"always": true
|
|
141
|
+
},
|
|
142
|
+
"severity": "wrong",
|
|
143
|
+
"says": "Eigenvector centrality is reported not to be calculated properly. The report is untriaged. Check against an independent implementation before ranking on it. Not verified against your Gephi: run the probe suite to confirm it still holds.",
|
|
144
|
+
"reported_open_since": "2019-10-10",
|
|
145
|
+
"verification": {
|
|
146
|
+
"status": "unverified",
|
|
147
|
+
"why": "Deciding this needs an independent implementation of the measure to compare against, and a mismatch of conventions between the two would look identical to a fix. No probe is written rather than one that could give a false all-clear."
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "gephi-858",
|
|
152
|
+
"issues": [
|
|
153
|
+
"https://github.com/gephi/gephi/issues/858"
|
|
154
|
+
],
|
|
155
|
+
"metrics": [
|
|
156
|
+
"clustering_coefficient"
|
|
157
|
+
],
|
|
158
|
+
"applies_when": {
|
|
159
|
+
"always": true
|
|
160
|
+
},
|
|
161
|
+
"severity": "wrong",
|
|
162
|
+
"says": "The average clustering coefficient is reported to be wrong. Open and untriaged since 2013. Not verified against your Gephi: run the probe suite to confirm it still holds.",
|
|
163
|
+
"reported_open_since": "2013-11-22",
|
|
164
|
+
"verification": {
|
|
165
|
+
"status": "unverified",
|
|
166
|
+
"probe": "probe_gephi_858"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "gephi-1784",
|
|
171
|
+
"issues": [
|
|
172
|
+
"https://github.com/gephi/gephi/issues/1784"
|
|
173
|
+
],
|
|
174
|
+
"metrics": [
|
|
175
|
+
"betweenness"
|
|
176
|
+
],
|
|
177
|
+
"applies_when": {
|
|
178
|
+
"always": true
|
|
179
|
+
},
|
|
180
|
+
"severity": "reporting",
|
|
181
|
+
"says": "Betweenness centrality from Gephi is reported to differ from NodeXL on the same graph, with no explanation recorded on the issue. If you are comparing figures with work done in another tool, the difference may be the tool rather than the data. This one cannot be checked automatically from here, so it is reported as filed rather than confirmed.",
|
|
182
|
+
"reported_open_since": "2017-09-16",
|
|
183
|
+
"verification": {
|
|
184
|
+
"status": "not_probeable",
|
|
185
|
+
"why": "Cannot be probed through this API: confirming it needs NodeXL, or another independent implementation, to compare against."
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Consensus analysis over repeated community detection.
|
|
2
|
+
|
|
3
|
+
Gephi runs community detection once and reports a partition as though it were the answer. It is
|
|
4
|
+
one draw. The same graph run again can give a different partition (gephi#2002), and the result
|
|
5
|
+
changes with the order the tables were imported (gephi#2888) and even after a layout has run
|
|
6
|
+
(gephi#2735), which should not touch a partition at all.
|
|
7
|
+
|
|
8
|
+
gephi#2968 asked Gephi for exactly this analysis and was closed as not planned, so nothing in this
|
|
9
|
+
ecosystem can currently answer the first question anyone should ask of a community result: are
|
|
10
|
+
these groups real, or are they an artefact of one run?
|
|
11
|
+
|
|
12
|
+
The measure used here is co-assignment. Across N runs, every pair of nodes has a rate at which the
|
|
13
|
+
two landed in the same community. A rate of 1.0 or 0.0 is decisive; 0.5 is a coin flip. A node's
|
|
14
|
+
stability is the average decisiveness of its relations with every other node, so 1.0 means every
|
|
15
|
+
relation came out the same way every time, and 0.5 means the node's membership is undetermined.
|
|
16
|
+
|
|
17
|
+
The consensus partition keeps the pairs that agreed more often than not, which leaves a node that
|
|
18
|
+
agrees with nobody standing on its own rather than being forced into a group it only half joined.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from itertools import combinations
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
Partition = dict[str, Any]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _canonical(partition: Partition) -> frozenset[frozenset[str]]:
|
|
30
|
+
"""A partition as its set of groups, so arbitrary community labels stop mattering.
|
|
31
|
+
|
|
32
|
+
Gephi numbers communities differently between runs. {a,b}|{c,d} is the same partition whether
|
|
33
|
+
the groups are called 1 and 2 or 7 and 3, and counting relabellings as different outcomes
|
|
34
|
+
would report instability that is not there.
|
|
35
|
+
"""
|
|
36
|
+
groups: dict[Any, set[str]] = {}
|
|
37
|
+
for node, community in partition.items():
|
|
38
|
+
groups.setdefault(community, set()).add(node)
|
|
39
|
+
return frozenset(frozenset(g) for g in groups.values())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _co_assignment(runs: list[Partition]) -> tuple[dict[tuple[str, str], float], list[str]]:
|
|
43
|
+
"""For every pair of nodes, the fraction of the runs containing both that grouped them."""
|
|
44
|
+
nodes = sorted({n for run in runs for n in run})
|
|
45
|
+
rates: dict[tuple[str, str], float] = {}
|
|
46
|
+
for a, b in combinations(nodes, 2):
|
|
47
|
+
shared = [r for r in runs if a in r and b in r]
|
|
48
|
+
if not shared:
|
|
49
|
+
continue
|
|
50
|
+
together = sum(1 for r in shared if r[a] == r[b])
|
|
51
|
+
rates[(a, b)] = together / len(shared)
|
|
52
|
+
return rates, nodes
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _components(nodes: list[str], edges: set[tuple[str, str]]) -> list[list[str]]:
|
|
56
|
+
parent = {n: n for n in nodes}
|
|
57
|
+
|
|
58
|
+
def find(x: str) -> str:
|
|
59
|
+
while parent[x] != x:
|
|
60
|
+
parent[x] = parent[parent[x]]
|
|
61
|
+
x = parent[x]
|
|
62
|
+
return x
|
|
63
|
+
|
|
64
|
+
for a, b in edges:
|
|
65
|
+
ra, rb = find(a), find(b)
|
|
66
|
+
if ra != rb:
|
|
67
|
+
parent[ra] = rb
|
|
68
|
+
|
|
69
|
+
groups: dict[str, list[str]] = {}
|
|
70
|
+
for n in nodes:
|
|
71
|
+
groups.setdefault(find(n), []).append(n)
|
|
72
|
+
return [sorted(g) for g in groups.values()]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def consensus(runs: list[Partition]) -> dict[str, Any]:
|
|
76
|
+
"""Summarise how reproducible a set of community-detection runs was.
|
|
77
|
+
|
|
78
|
+
Returns the number of genuinely distinct partitions seen, a stability score per node, the
|
|
79
|
+
least stable nodes, and a consensus partition built from the pairs that agreed more often
|
|
80
|
+
than not.
|
|
81
|
+
|
|
82
|
+
A single run returns `mean_stability: None` and a warning rather than a score. One draw
|
|
83
|
+
carries no information about reproducibility, and reporting 1.0 there would assert exactly
|
|
84
|
+
the thing the caller asked us to check.
|
|
85
|
+
"""
|
|
86
|
+
runs = [r for r in (runs or []) if r]
|
|
87
|
+
if not runs:
|
|
88
|
+
return {"runs": 0, "distinct_partitions": 0, "node_stability": {},
|
|
89
|
+
"mean_stability": None, "unstable_nodes": [], "consensus_groups": [],
|
|
90
|
+
"warning": "No runs were recorded, so nothing can be said about stability."}
|
|
91
|
+
|
|
92
|
+
distinct = len({_canonical(r) for r in runs})
|
|
93
|
+
|
|
94
|
+
if len(runs) == 1:
|
|
95
|
+
return {"runs": 1, "distinct_partitions": distinct, "node_stability": {},
|
|
96
|
+
"mean_stability": None, "unstable_nodes": [],
|
|
97
|
+
"consensus_groups": [sorted(g) for g in _canonical(runs[0])],
|
|
98
|
+
"warning": ("Only one run was recorded. One draw says nothing about whether the "
|
|
99
|
+
"partition is reproducible; run it several times to find out.")}
|
|
100
|
+
|
|
101
|
+
rates, nodes = _co_assignment(runs)
|
|
102
|
+
|
|
103
|
+
stability: dict[str, float] = {}
|
|
104
|
+
for node in nodes:
|
|
105
|
+
decisiveness = [max(p, 1.0 - p)
|
|
106
|
+
for (a, b), p in rates.items() if node in (a, b)]
|
|
107
|
+
stability[node] = round(sum(decisiveness) / len(decisiveness), 4) if decisiveness else 1.0
|
|
108
|
+
|
|
109
|
+
agreed = {pair for pair, p in rates.items() if p > 0.5}
|
|
110
|
+
groups = _components(nodes, agreed)
|
|
111
|
+
|
|
112
|
+
ranked = sorted(stability.items(), key=lambda kv: (kv[1], kv[0]))
|
|
113
|
+
return {
|
|
114
|
+
"runs": len(runs),
|
|
115
|
+
"distinct_partitions": distinct,
|
|
116
|
+
"node_stability": stability,
|
|
117
|
+
"mean_stability": round(sum(stability.values()) / len(stability), 4),
|
|
118
|
+
"unstable_nodes": [{"node": n, "stability": s} for n, s in ranked[:10]],
|
|
119
|
+
"consensus_groups": sorted(groups, key=lambda g: (-len(g), g[0])),
|
|
120
|
+
}
|