Frames¶
Ursa has two frame types. Both are lazy: a frame is a plan, and collect() runs the plan.
EdgeFrame: the graph¶
An EdgeFrame is a table with two designated columns: the source (src) and the destination (dst). Each row is one directed edge. The frame is the graph. There is no other graph object.
src= and dst= give the roles of two columns. They do not rename the columns. The original names stay in the data, and you can read them from edges.src_col and edges.dst_col.
An EdgeFrame can carry more columns. A weight, a timestamp, or a type column are ordinary columns. An algorithm reads one of them only when you ask for it, for example with weight=ur.col("amount").
The index¶
The first graph operation on an EdgeFrame builds a topology index (a CSR structure) and caches it on the frame. Later operations on the same frame reuse it. The index is built once per frame, not once per algorithm.
A filter on the edges makes a subgraph with its own index. See Work on a subgraph.
Rows are kept¶
Ursa does not merge parallel edges, and it does not remove self-loops. Two rows with the same (src, dst) are two edges. If you want a simple graph, remove the duplicate rows before you build the frame. See Reshaped edge sets.
NodeFrame: the attributes¶
A NodeFrame is a table with one designated column: the id. It holds attributes of nodes. It has no topology.
You get a NodeFrame in three ways:
edges.nodes()gives the distinct ids insrcanddst, with no attributes. Add at least one column before you collect it.- The
NodeFrame(...)constructor,ur.scan_nodes(...), or an interop function gives a table you supply. - A standalone algorithm call such as
ur.pagerank(edges)acts as aNodeFrameof(id, pagerank).
When you call nodes.with_columns(pagerank=ur.pagerank(edges)), Ursa joins the computed values to your table by id. The join is a left join: every row of your table stays.
Lazy plans and collect()¶
A frame method returns a new frame with one more step in its plan. The plan is a value. You can hold it, extend it, or print it with explain().
plan = edges.nodes().with_columns(pr=ur.pagerank(edges)).sort("pr", descending=True)
print(plan.explain())
collect() runs the plan once and returns a MaterializedFrame. The result holds an Arrow table. From there, call to_polars(), to_arrow(), to_dicts(), sink_parquet(), or sink_csv(). See Get the result out.
Shortcuts: frame.to_polars(), frame.to_arrow(), and frame.to_dicts() call collect() for you.
Sources¶
A frame starts from one of two kinds of source:
- In memory. Row dicts, a column dict, a
polars.DataFrame, apandas.DataFrame, apyarrow.Table, or any object with__arrow_c_stream__. - A file.
ur.scan_edges(...)andur.scan_nodes(...)read Parquet and CSV, from a local path, from object storage, or from an HTTP URL. The read happens insidecollect().
A frame behaves the same way after construction, whatever its source. See Load data.