Ursa
Dubhe — score 0.82Merak — score 0.55Phecda — score 0.63Megrez — score 0.65Alioth — score 1.00Mizar — score 0.72Alkaid — score 0.93
Fig. 1 — generated field, 660 sources, seed 1847. Radius ∝ asinh-stretched score, β = 0.35. The seven brightest are Ursa Major.

An edge list looks like nothing. It is dense with structure.

Ursa is a dataframe API for graph analytics. PageRank, centralities, components and traversals over edges in Parquet or CSV — in memory, on one machine. Rust and Apache Arrow underneath.

pip install ursa-graphDocumentationSource

v0.2.0, on PyPI. The kernels, the composed pipeline and the relational surface execute end to end; the roadmap says what does not.


pagerank

ur.pagerank(edges, damping=0.85, max_iter=30, tol=1e-6, weight=None) → NodeFrame

Returns a lazy frame of id and pagerank. Nothing runs until .collect(), so the scan, the algorithm and the sort are planned and executed as a single DataFusion query — with the graph operator as a first-class node in that plan, not a step orchestrated around it.

The colour in the table is the only colour on this page. It is a temperature scale, and it appears because there is finally something measured to show.

import ursa as ur
import pyarrow as pa

edges = ur.from_arrow(
    pa.table({"s": [0, 1, 2, 3, 4, 5, 2, 6, 7],
              "d": [1, 2, 0, 4, 5, 3, 3, 0, 0]}),
    src="s", dst="d",
)

(ur.pagerank(edges, damping=0.85)
   .sort("pagerank", descending=True)
   .head(5)
   .collect()
   .to_polars())
idin_degreepagerank
000000320.238271
000000410.221280
000000510.206838
000000210.102843
000000110.098933

Tab. 1 — top 5 of 8 by pagerank over the 9 edges above, damping 0.85, iterated to convergence. Computed, not illustrated.

pagerankhot → cool⌀ ≈ pagerank 0.238

One plan, not two engines.

There is no Graph object. An EdgeFrame is the graph, a NodeFrame is an attribute table, and every operation returns a frame — so metrics, attributes, filters and top-k compose into one lazy pipeline instead of a script that hands results between libraries.

The topology index is built once on first use and shared by every graph operation over that frame. Property-only transformations keep it; row-changing ones drop it. That contract is the performance model, and it is written down rather than guessed at.

Core concepts ·Quickstart

nodes = ur.scan_nodes("towers.parquet", id="tower_id")
edges = ur.scan_edges("links.parquet",
                      src="tower_a", dst="tower_b")

(nodes
 .with_columns(
     pagerank  = ur.pagerank(edges, damping=0.85),
     component = ur.connected_components(edges),
     nbr_cap   = ur.neighbors(edges).agg(
                     ur.col("capacity_gbps").mean()),
 )
 .filter(ur.col("pagerank") > 0.001)
 .sort("pagerank", descending=True)
 .head(20)
 .collect())

One collect(). Three kernels over one shared topology index, joined to the node attributes, filtered and ranked inside the engine — and the node file is read for only the columns the plan proves it needs.

What the instrument does today.

Algorithms

pagerank · degree · connected_components (weak and strong) · triangle_count · clustering_coefficient · closeness · betweenness (Brandes, sampled) · label_propagation · louvain — each with a weighted variant, deterministic across thread counts.

Traversals

hop().from_() · shortest_path (BFS, or Dijkstra when weighted, with a cumulative cost column) · random_walk — each its own first-class logical node, each returning a frame.

Relational verbs

filter (a full predicate algebra) · select · with_columns · sort · head · distinct · sample · rename · group_by().agg() · join — the same frame, both vocabularies.

Sources

Parquet and CSV — local, object storage (s3:// · gs:// · az://) or https:// — with the column projection pushed into the file. Or straight from data: EdgeFrame(dict | rows | polars | pandas | pyarrow), from_edgelist, from_networkx, from_numpy, from_scipy_sparse, and ur.datasets for canonical graphs. Node ids may be int64 or strings.

Statistics

describe (one-row summary frame) · density · avg_path_length · diameter — the eager scalars are the one deliberate exception to laziness.

Egress

to_polars · to_arrow · to_dicts · sink_parquet · sink_csv. Arrow is the boundary in both directions, so leaving Ursa costs nothing.

Not yet

schema() · store= · CSV format options · neighbors(from_=) — each raises a clear error rather than mis-executing, as do the stated composition limits. Motif finding and the optimizer rules are ahead.

The visualizer inverts.

You cannot read a negative on a live instrument. So where the site is paper, the visualizer is sky: black ground, bright sources, colour throughout — because everything on screen has been measured. Same tokens, flipped.

In design, not shipped. The figure below is a specimen of the rendering conventions, generated and measured at build time like every other figure here.

limiting magnitude0.42
degreehigh → lowlog stretch
SRC-0005deg 24 · pr 0.0368
Fig. 2 — preferential-attachment graph, 148 nodes, 293 edges, seed 2311. Degree, log stretch. Spikes mark the 10 sources above the limiting magnitude; they stay legible when radius has collapsed at low zoom.

Five rules, and nothing else.

The ground is a negative

Astronomers read plates as negatives — black sources on a white sky. So the site is paper and the instrument is sky. It also puts Ursa on the opposite side of every dark dev-tool launch.

Colour only where there is data

The page is ink until something has been measured. Then the temperature scale appears, and only ever means a value. No brand accent, no coloured buttons, no highlight colour.

Magnitude is centrality

Radius, weight and spike length all encode score, on one stretch, across the mark, the hero, the docs and the app — which is why the asterism is found in the field rather than placed on it.

One caption, no chrome

Figures carry a single mono line: what it is, how big, how it was processed. No corner ticks, no metadata rails, no status bars — and no figure that cannot state real numbers.

Two faces

A text serif and a mono. No sans anywhere, which is the whole reason it reads as published rather than shipped.