History of reddit_graphsage
The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.
[entries] New entry: reddit_graphsage 2ca3e0c
diff --git a/entries/reddit_graphsage/__init__.py b/entries/reddit_graphsage/__init__.py
new file mode 100644
index 0000000..b667174
--- /dev/null
+++ b/entries/reddit_graphsage/__init__.py
@@ -0,0 +1,126 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import re
+
+from .. import *
+
+title = "Reddit posts linked by common commenters (GraphSAGE, 2014)"
+description = r"""Networks of the posts made on Reddit in September 2014, two posts being connected if the same user commented on both, built by W. L. Hamilton, R. Ying and J. Leskovec as a benchmark for the inductive node classification method GraphSAGE, from the Reddit data of Pushshift. The posts are those of 50 large communities (subreddits), and the task of the benchmark is to predict the community of a post. The first 20 days of the month were used for training, and the rest for validation (30%) and testing (70%).
+
+There are two networks, both distributed with GraphSAGE:
+
+- `full`: the complete network (`reddit-G_full.json`), with all 232,965 posts and 57.3 million edges (average degree 492, as given in the paper). This is also the network of the `Reddit` dataset of the libraries DGL and PyTorch Geometric (whose copy, with the nodes in another order, has the same numbers of nodes and edges).
+- `graphsage`: the sparser network distributed as the main graph of the benchmark (`reddit-G.json`), with 231,443 posts and 11.6 million edges, which is also the network of the `Reddit2` dataset of PyTorch Geometric (from GraphSAINT). How it was obtained from the full network is not documented.
+
+The communities are given only as numbered classes (41 of them appear), not by name. Each post also has the node features of the benchmark: a 602-dimensional vector made of the average of the 300-dimensional GloVe word embeddings of the title of the post, the average of those of its comments, its score and its number of comments (the dimensions are as given by the source; their order is not documented).
+
+The networks have the following properties:
+
+- `post_id`: the identifier of the post on Reddit (in base 36, e.g. `2hgylt`).
+- `label`: the class of the community of the post (a number from 0 to 40).
+- `split`: the set of the post in the benchmark: `train`, `val` or `test`.
+- `features`: the 602-dimensional feature vector of the post."""
+tags = ['Social', 'Online', 'Projection', 'Unweighted', 'Metadata']
+url = ['https://snap.stanford.edu/graphsage/']
+citation = [('W. L. Hamilton, R. Ying, J. Leskovec, "Inductive Representation Learning on Large Graphs." Advances in Neural Information Processing Systems 30, 1024-1034 (2017)',
+ 'https://doi.org/10.48550/arXiv.1706.02216')]
+icon_hash = None
+upstream_license = None
+upstream_prefix = 'http://snap.stanford.edu'
+ZIP = 'graphsage/reddit.zip:reddit'
+COMMON = (f'{ZIP}/reddit-class_map.json', f'{ZIP}/reddit-id_map.json',
+ f'{ZIP}/reddit-feats.npy')
+files = [((f'{ZIP}/reddit-G_full.json',) + COMMON, 'full', None),
+ ((f'{ZIP}/reddit-G.json',) + COMMON, 'graphsage', None)]
+
+def fetch_upstream(force=False):
+ return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+ force, check=False)
+
+NODE = re.compile(rb'\{[^{}]*\}')
+LINK = re.compile(rb'"source": (\d+), "target": (\d+)')
+
+def read_node_link(name, fname):
+ """The nodes (list of dicts) and the links (array of pairs of node indices)
+ of the networkx node-link JSON file `fname`, read in blocks with regular
+ expressions (the file of the full network has 2.1 GB)."""
+ nodes, links = [], []
+ in_links = False
+ tail = b""
+ with open_upstream_file(name, fname, "rb") as f:
+ while True:
+ chunk = f.read(1 << 26)
+ buf = tail + chunk
+ if not in_links:
+ i = buf.find(b'"links"')
+ part = buf if i < 0 else buf[:i]
+ cut = part.rfind(b"}") + 1
+ # (the first match is the empty "graph" attribute dict)
+ nodes.extend(d for d in map(json.loads, NODE.findall(part[:cut]))
+ if "id" in d)
+ if i < 0:
+ tail = part[cut:]
+ if not chunk:
+ break
+ continue
+ in_links = True
+ buf = buf[i:]
+ cut = buf.rfind(b"}") + 1
+ m = LINK.findall(buf[:cut])
+ if m:
+ links.append(numpy.array(m, dtype="int64"))
+ tail = buf[cut:]
+ if not chunk:
+ break
+ return nodes, numpy.concatenate(links)
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+ name = __name__.split(".")[-1]
+ for fnames, alt, fmt in files:
+ if alts is not None and alt not in alts:
+ continue
+ nodes, links = read_node_link(name, fnames[0])
+ with open_upstream_file(name, fnames[1], "r") as f:
+ labels = json.load(f)
+ with open_upstream_file(name, fnames[2], "r") as f:
+ id_map = json.load(f)
+ ids = [n["id"] for n in nodes]
+ g = Graph(directed=False)
+ g.add_vertex(len(nodes))
+ g.add_edge_list(links)
+ del links
+ g.vp.post_id = g.new_vp("string", vals=ids)
+ g.vp.label = g.new_vp("int", vals=[labels[i] for i in ids])
+ g.vp.split = g.new_vp("string", vals=["test" if n["test"] else
+ "val" if n["val"] else "train"
+ for n in nodes])
+ # the features: a float64 array (number of posts x 602), in the order
+ # of id_map
+ with open_upstream_file(name, fnames[3], "rb", seekable=True) as f:
+ feats = numpy.load(f, mmap_mode=None)
+ p = g.new_vp("vector<double>")
+ for v, i in enumerate(ids):
+ p[v] = feats[id_map[i]]
+ del feats
+ g.vp.features = p
+ yield alt, g