# Graph Algorithms

Piggie provides 19 graph algorithms across 7 categories, all accessible as driver methods. Algorithms load the AGE graph into NetworkX (or igraph) and return pandas DataFrames.

## Overview

| Category | Algorithms | Count |
|----------|-----------|-------|
| [Centrality](#centrality) | degree, betweenness, closeness, eigenvector, harmonic | 5 |
| [Community Detection](#community-detection) | louvain, label_propagation, greedy_modularity, leiden | 4 |
| [Pathfinding](#pathfinding) | shortest_path, all_shortest_paths, dijkstra | 3 |
| [Ranking](#ranking) | pagerank | 1 |
| [Similarity](#similarity) | jaccard, adamic_adar, common_neighbors | 3 |
| [Link Prediction](#link-prediction) | preferential_attachment, resource_allocation | 2 |
| [Embeddings](#embeddings) | node2vec | 1 |

All algorithms accept optional `label` and `rel_type` filters to restrict which vertices and edges are included.

---

## Centrality

Measure how important or influential nodes are within a graph.

### `db.centrality(*, measure="degree", graph=None, label=None, rel_type=None) -> DataFrame`

Returns a DataFrame with columns `[node_id, label, centrality]`.

| Measure | Description |
|---------|-------------|
| `"degree"` | Fraction of nodes each node is connected to |
| `"betweenness"` | Fraction of shortest paths passing through each node |
| `"closeness"` | Reciprocal of the average shortest path distance to all other nodes |
| `"eigenvector"` | Influence based on connections to other high-scoring nodes (requires scipy) |
| `"harmonic"` | Sum of reciprocals of shortest path distances — handles disconnected graphs |

```python
# Degree centrality
df = db.centrality(measure="degree")

# Eigenvector centrality (requires scipy)
df = db.centrality(measure="eigenvector", label="Person")

# Harmonic centrality (works on disconnected graphs)
df = db.centrality(measure="harmonic")

# Sort by centrality score
df.sort_values("centrality", ascending=False)
```

---

## Community Detection

Identify clusters of densely connected nodes.

### `db.communities(*, method="louvain", graph=None, label=None, rel_type=None) -> DataFrame`

Returns a DataFrame with columns `[node_id, label, community_id]`. The graph is converted to undirected internally.

| Method | Description | Dependencies |
|--------|-------------|--------------|
| `"louvain"` | Modularity-based hierarchical clustering. Fast and widely used | NetworkX |
| `"label_propagation"` | Nodes adopt the most common label among neighbors. Very fast | NetworkX |
| `"greedy_modularity"` | Greedy agglomerative modularity maximization | NetworkX |
| `"leiden"` | Improved Louvain with guaranteed connected communities | `leidenalg`, `igraph` |

```python
# Louvain (default)
df = db.communities()

# Label propagation (fastest)
df = db.communities(method="label_propagation")

# Greedy modularity
df = db.communities(method="greedy_modularity")

# Leiden (requires extra packages)
# pip install piggie[igraph] leidenalg
df = db.communities(method="leiden")

# Count members per community
df.groupby("community_id").size()
```

---

## Pathfinding

Find paths between nodes in the graph.

### `db.shortest_path(source, target, *, graph=None, weight=None, label=None, rel_type=None) -> list[int]`

Returns a list of node IDs forming the shortest path. Returns an empty list if no path exists.

```python
path = db.shortest_path(alice_id, dave_id)
# [844424930131969, 844424930131970, 844424930131971]

# Weighted shortest path
path = db.shortest_path(alice_id, dave_id, weight="distance")
```

### `db.all_shortest_paths(source, target, *, graph=None, weight=None, label=None, rel_type=None) -> list[list[int]]`

Returns all paths that tie for shortest. Useful when multiple equal-length routes exist.

```python
paths = db.all_shortest_paths(alice_id, dave_id)
for p in paths:
    print(p)
```

### Dijkstra

Dijkstra's algorithm is available as a standalone function that returns both the path and its total weight.

```python
from piggie._algorithms.paths import dijkstra

df = dijkstra(db, "my_graph", alice_id, dave_id, weight="distance")
# DataFrame with columns [path, total_weight]
print(df.iloc[0]["path"])          # [node_id, node_id, ...]
print(df.iloc[0]["total_weight"])  # 42.5
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `weight` | `str` | `"weight"` | Edge property to use as weight |

---

## Ranking

### `db.pagerank(*, graph=None, label=None, rel_type=None, damping=0.85) -> DataFrame`

Compute PageRank over the graph. Returns a DataFrame with columns `[node_id, label, pagerank]`. Scores sum to 1.0.

```python
df = db.pagerank(damping=0.85)
top_nodes = df.sort_values("pagerank", ascending=False).head(10)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `damping` | `float` | `0.85` | Probability of following an edge vs. jumping to a random node |

---

## Similarity

Measure structural similarity between node pairs based on shared neighbors.

### `db.similarity(*, measure="jaccard", graph=None, node_pairs=None, label=None, rel_type=None) -> DataFrame`

Returns a DataFrame with columns `[node_u, node_v, score]`. The graph is converted to undirected internally.

| Measure | Description |
|---------|-------------|
| `"jaccard"` | Size of shared neighbors / size of union of neighbors. Range [0, 1] |
| `"adamic_adar"` | Sum of 1/log(degree) for shared neighbors. Weights rare connections higher |

```python
# Jaccard similarity between specific pairs
df = db.similarity(measure="jaccard", node_pairs=[(alice_id, bob_id)])

# Adamic-Adar for all non-adjacent pairs (can be large)
df = db.similarity(measure="adamic_adar")

# Filter to high-similarity pairs
df[df["score"] > 0.5]
```

### Common Neighbors

Find the count of shared neighbors between two nodes.

```python
from piggie._algorithms.similarity import common_neighbors

df = common_neighbors(db, "my_graph", alice_id, bob_id)
# DataFrame with columns [node_u, node_v, score]
# score = number of common neighbors
```

---

## Link Prediction

Predict which unconnected node pairs are likely to form connections.

### `db.link_prediction(*, measure="preferential_attachment", graph=None, node_pairs=None, label=None, rel_type=None) -> DataFrame`

Returns a DataFrame with columns `[node_u, node_v, score]`. The graph is converted to undirected internally.

| Measure | Description |
|---------|-------------|
| `"preferential_attachment"` | Product of the degrees of two nodes. High-degree nodes attract more connections |
| `"resource_allocation"` | Sum of 1/degree for common neighbors. Similar to Adamic-Adar but uses degree directly |

```python
# Preferential attachment
df = db.link_prediction(
    measure="preferential_attachment",
    node_pairs=[(alice_id, dave_id)],
)

# Resource allocation for all non-adjacent pairs
df = db.link_prediction(measure="resource_allocation")

# Top predicted links
df.sort_values("score", ascending=False).head(20)
```

---

## Embeddings

Generate vector representations of nodes based on graph structure.

### `db.node2vec(*, graph=None, dimensions=64, walk_length=30, num_walks=200, p=1.0, q=1.0, workers=1, label=None, rel_type=None) -> DataFrame`

Compute [node2vec](https://arxiv.org/abs/1607.00975) embeddings. Returns a DataFrame with columns `[node_id, label, embedding]` where `embedding` is a list of floats.

Requires the `node2vec` package:

```bash
pip install piggie[embeddings]
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dimensions` | `int` | `64` | Embedding vector dimensionality |
| `walk_length` | `int` | `30` | Length of each random walk |
| `num_walks` | `int` | `200` | Number of random walks per node |
| `p` | `float` | `1.0` | Return parameter (lower = more local) |
| `q` | `float` | `1.0` | In-out parameter (lower = more DFS-like) |
| `workers` | `int` | `1` | Parallel workers for word2vec training |

```python
df = db.node2vec(dimensions=128, walk_length=20, num_walks=100)

# Use embeddings for downstream ML
embeddings = df["embedding"].tolist()
node_ids = df["node_id"].tolist()

# Store in pgvector for similarity search
import numpy as np
for i, row in df.iterrows():
    db.sql(
        "INSERT INTO node_embeddings (node_id, embedding) VALUES (%s, %s)",
        (row["node_id"], row["embedding"]),
    )
```

---

## Optional Dependencies

| Feature | Install command | Used by |
|---------|----------------|---------|
| Core algorithms | `pip install piggie[networkx]` | All algorithms except node2vec |
| Eigenvector centrality, PageRank | `pip install scipy` | `centrality(measure="eigenvector")`, `pagerank()` |
| Leiden community detection | `pip install piggie[igraph] leidenalg` | `communities(method="leiden")` |
| Node2vec embeddings | `pip install piggie[embeddings]` | `node2vec()` |

Install everything:

```bash
pip install piggie[networkx,igraph,embeddings]
```

---

## Direct Function Access

All algorithms are also available as standalone functions for use outside the driver:

```python
from piggie._algorithms.centrality import centrality
from piggie._algorithms.community import community_detection
from piggie._algorithms.ranking import pagerank
from piggie._algorithms.paths import shortest_path, all_shortest_paths, dijkstra
from piggie._algorithms.similarity import jaccard_similarity, adamic_adar, common_neighbors
from piggie._algorithms.link_prediction import preferential_attachment, resource_allocation
from piggie._algorithms.embeddings import node2vec_embed
```

These functions take a `PiggieDriver` and graph name as the first two arguments, matching the pattern used internally by the driver methods.
