1. Node, Edge, and Graph Features
Node features $\mathbf{x}_i$
- Raw attributes: age, price, atom symbol one-hot
- Text embeddings: BERT/LLM encoding of node description (common in LLM+graph systems)
- Structural features: degree, clustering coefficient, PageRank score
- Positional: Laplacian eigenvector PE (used in graph transformers)
Edge features $\mathbf{e}_{ij}$
- Relation type (one-hot), bond order, distance, timestamp, edge weight
Graph-level features
For graph classification, you either:
- Provide a global feature vector per graph, or
- Learn a graph embedding via readout (sum/mean/max over node embeddings)
2. Building Graphs from Raw Data
| Source | Nodes | Edges | Features |
|---|---|---|---|
| Social network | Users | Follow/friend | Profile, posts embedding |
| Citations | Papers | Cites | TF-IDF, title embedding |
| E-commerce | Users + items | Purchase/click | User/item embeddings (bipartite) |
| Molecules | Atoms | Bonds | Atom type, charge |
| Knowledge base | Entities | Relations | Text description, type |
| Code repo | Functions/classes | Call/import | Code 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
- Homogeneous: one node type, one edge type (Cora citations)
- Heterogeneous: multiple node/edge types (Amazon: users, products, brands)
- Bipartite: two node sets, edges only across sets (users ↔ items)
- Hypergraph: edges connect more than two nodes (co-authorship of 3+ authors)
- Multigraph: multiple edges between same pair
Heterogeneous graphs need special models - see Heterogeneous Graphs.
4. Train/Val/Test Splits - The Right Way
Node classification on one graph
- Random split: sample labeled nodes. OK for homophily + transductive (Cora-style).
- Stratified: keep class balance per split.
- Temporal split: train on past, test on future edges/nodes (critical for dynamic data).
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
- Transductive training: test nodes are in the graph during message passing - this is valid for transductive setting but must be stated. Inductive evaluation must mask test nodes entirely.
- Link prediction: if you include test edges in the graph before prediction, you cheated.
- Normalization: computing global statistics (mean/std of features) using test nodes - compute on train only.
- Duplicate nodes: same entity appearing twice bridges train and test.
- Future information in temporal graphs: using tomorrow's edge to predict today.
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
- Remove self-loops if model does not expect them (or add them if GCN)
- Make undirected edges bidirectional in
edge_index(two directed entries) - Handle isolated nodes (degree 0) - add self-loop or remove
- Normalize features: standardize or L2-normalize
- For large graphs: use
NeighborLoaderfor mini-batch sampling - Store masks:
train_mask,val_mask,test_maskas boolean tensors
8. Common Benchmark Datasets
| Dataset | Task | Nodes/Edges/Graphs | Notes |
|---|---|---|---|
| Cora | Node cls | 2.7K / 5.4K | Citation, 7 classes, homophily |
| PubMed | Node cls | 19K / 44K | Biomedical citations |
| OGBN-Arxiv | Node cls | 169K / 1.1M | Large, temporal split |
| OGBN-Products | Node cls | 2.4M / 61M | Amazon co-purchase |
| OGBG-MolHIV | Graph cls | 41K graphs | Molecule property |
| ZINC | Graph reg | 12K graphs | Molecular 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()