Representation & Data Preparation

How to turn raw data into a graph, engineer features, split correctly, and load into PyTorch Geometric - without leaking labels.

1. Node, Edge, and Graph Features

Node features $\mathbf{x}_i$

Edge features $\mathbf{e}_{ij}$

Graph-level features

For graph classification, you either:

  1. Provide a global feature vector per graph, or
  2. Learn a graph embedding via readout (sum/mean/max over node embeddings)
Intuition: Features tell the model what each node "is." Edges tell it "who talks to whom." GNNs combine both - features alone miss structure; structure alone misses content.

2. Building Graphs from Raw Data

SourceNodesEdgesFeatures
Social networkUsersFollow/friendProfile, posts embedding
CitationsPapersCitesTF-IDF, title embedding
E-commerceUsers + itemsPurchase/clickUser/item embeddings (bipartite)
MoleculesAtomsBondsAtom type, charge
Knowledge baseEntitiesRelationsText description, type
Code repoFunctions/classesCall/importCode embedding

k-NN graphs from tabular data

Sometimes you only have a feature table. Connect each point to its $k$ nearest neighbors in feature space. Warning: this invents structure - only do it if locality in feature space is meaningful.

Threshold graphs

Connect nodes if similarity $> \tau$. Sensitive to $\tau$ - too low creates noise; too high disconnects the graph.

3. Graph Types

Heterogeneous graphs need special models - see Heterogeneous Graphs.

4. Train/Val/Test Splits - The Right Way

Node classification on one graph

Inductive node split

Hold out entire subgraphs or nodes with all their edges removed from training graph. Model must generalize to unseen structure.

Link prediction split

Hide edges (positive samples). Train on remaining graph. Validate on held-out edges + sampled non-edges (negatives).

Graph classification

Split at graph level: 80% graphs train, 10% val, 10% test. Never split nodes within one molecule and put halves in different splits.

5. Data Leakage Traps

Leakage kills GNN papers. If test labels or test edges influence training embeddings, metrics are fake.

6. PyTorch Geometric Basics

PyG stores edges in edge_index shape [2, num_edges] - COO format. Row 0 = source, row 1 = target.

from torch_geometric.data import Data

# 3 nodes, undirected edges 0-1, 1-2
edge_index = torch.tensor([[0, 1, 1, 2],
                           [1, 0, 2, 1]], dtype=torch.long)
x = torch.tensor([[1,0], [0,1], [1,1]], dtype=torch.float)  # node features
y = torch.tensor([0, 1, 0], dtype=torch.long)               # labels

data = Data(x=x, edge_index=edge_index, y=y)
data.num_nodes  # 3

DGL (Deep Graph Library) is an alternative - popular in industry, strong for heterogeneous and distributed graphs. PyG is more common in research. Concepts transfer.

7. Preprocessing Checklist

  1. Remove self-loops if model does not expect them (or add them if GCN)
  2. Make undirected edges bidirectional in edge_index (two directed entries)
  3. Handle isolated nodes (degree 0) - add self-loop or remove
  4. Normalize features: standardize or L2-normalize
  5. For large graphs: use NeighborLoader for mini-batch sampling
  6. Store masks: train_mask, val_mask, test_mask as boolean tensors

8. Common Benchmark Datasets

DatasetTaskNodes/Edges/GraphsNotes
CoraNode cls2.7K / 5.4KCitation, 7 classes, homophily
PubMedNode cls19K / 44KBiomedical citations
OGBN-ArxivNode cls169K / 1.1MLarge, temporal split
OGBN-ProductsNode cls2.4M / 61MAmazon co-purchase
OGBG-MolHIVGraph cls41K graphsMolecule property
ZINCGraph reg12K graphsMolecular solubility

Use Open Graph Benchmark (OGB) for fair comparison.

9. Full Pipeline Code

from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures
import torch

dataset = Planetoid(root='./data', name='Cora', transform=NormalizeFeatures())
data = dataset[0]

# Planetoid provides fixed masks  -  use them, don't invent random splits
print(data.train_mask.sum(), data.val_mask.sum(), data.test_mask.sum())

# Simple GCN training skeleton
import torch.nn.functional as F
from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self, in_dim, hidden, out_dim):
        super().__init__()
        self.conv1 = GCNConv(in_dim, hidden)
        self.conv2 = GCNConv(hidden, out_dim)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index).relu()
        x = F.dropout(x, p=0.5, training=self.training)
        return self.conv2(x, edge_index)

model = GCN(dataset.num_features, 16, dataset.num_classes)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)

for epoch in range(200):
    model.train()
    optimizer.zero_grad()
    out = model(data.x, data.edge_index)
    loss = F.cross_entropy(out[data.train_mask], data.y[data.train_mask])
    loss.backward()
    optimizer.step()