Skip to content

Commit 02041a4

Browse files
lmeyerovclaude
andauthored
docs(gfql): reconcile the OLAP q9 figure and re-measure LadybugDB (#1826)
* docs(gfql): replace the OLAP q9 "14.2x" with the re-measured 1.26x The page read "q9 is 14.2x". Reconciled the same way q8 was, and the answer is the same: our own number was right and the competitor was measured running a DIFFERENT query. The published 14.2x is 65.03 ms / 920.69 ms, and both halves are in pyg-bench's 0.58.0 tag sweep. The Kuzu artifact stores its query text verbatim, and it is the old harness's OPTIONAL MATCH degree-product rewrite -- not the canonical two-hop path enumeration GFQL ran. Re-run on dgx-spark under the host perf lock (pygraphistry master 1537e46, Kuzu 0.11.3, 30 timed runs after 5 warmups, slots interleaved K G G K, one session, host load 0.96 -> 4.53), every slot value-identical at 45,562,239: GFQL engine='polars', canonical q9 66.26 / 64.22 ms (65.03 published) Kuzu 0.11.3, canonical q9 83.93 / 83.13 ms Kuzu 0.11.3, q9_degprod rewrite 878.51 / 859.49 ms (920.69 published) So our side reproduces, the rewrite reproduces, and the handicap applied to the competitor is 10.3-10.5x. The honest same-query ratio is 1.27-1.29x this session, consistent with the 1.26x the matched lane already publishes with provenance (pyg-bench published/docs-numbers.json, graphbench.100k.q9.polars_vs_kuzu) -- which is the figure the page now quotes. The 20k cell behaves identically: published 14.17 vs 60.00 ms = 4.2x, where 60.00 is again the rewrite (reproduced at 56.76-62.54) and the canonical query makes that cell a loss, not a win. Raw artifacts and the runner: pyg-bench results/graphbench-q9-reconciliation-20260728/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx * docs(gfql): re-run LadybugDB and replace the hardcoded competitor literal bench_ladybug_cypher.py carried LADYBUG = {"full_scan": 3789, "range": 7.5, "point": 0.3, "count": 3.3, "scan_rel_props": 15722, "scan_rel_rowid": 14562} with no URL, no version, no hardware and no citation, and it was the only competitor input to the published "~65x" and "~3.5-3.7x" ratios. So LadybugDB was actually run. MEASURED (dgx-spark, host perf lock held and verified released, load 1.53 -> 2.54, 2026-07-28): LadybugDB 0.18.1 embedded in a host venv against GFQL engine='polars' on pygraphistry master 1537e46 in graphistry/test-rapids-official:26.02-gfql-polars (polars 1.35.2), both arms in ONE session on ONE generated 5M-node / 20M-edge graph (nodes.parquet sha256 22c8b239..., edges.parquet 80ededc5...), 2 warmups + 5 timed runs, slots interleaved L G G L. Every op's result values are digest-identical across the two engines on all four slots -- rows AND values, not rows alone. Ladybug is timed at its FASTEST result-producing scope of four measured (Arrow, polars, pandas, Python row iterator). Choosing a slower scope for a competitor is exactly the defect being corrected here. full_scan GFQL 58.95 ms Ladybug 364.28 ms GFQL 6.18x range GFQL 5.06 ms Ladybug 7.57 ms GFQL 1.50x scan_rel_props GFQL 3256.91 ms Ladybug 1925.61 ms -> no longer claimed scan_rel_rowid GFQL 3157.46 ms Ladybug 1449.13 ms -> no longer claimed So "~65x" becomes 6.2x, and the "~3.5-3.7x" relationship-scan claim is withdrawn rather than restated -- measured fairly on one box those are not GFQL wins. The literal is RECONCILED, not merely contradicted: it is a drain-scope measurement (rows pulled through the Python iterator, the protocol the upstream LadybugDB/kuzu-ladybug-benchmark runner uses) on hardware ~1.5x slower. literal / drain-measured-here is 1.52 / 1.56 / 1.52 on the three scans and 0.98-1.06 on the three ops where the scope is irrelevant. And our own side was right again: "~65x" implies a 58.3 ms GFQL full scan; measured 58.95 ms. engines.rst also loses "query a 1.8B-edge graph in <8 GB RAM", attributed to LadybugDB. It is not on ladybugdb.com, blog.ladybugdb.com, docs.ladybugdb.com or their GitHub README; LadybugDB documents only "on-disk databases allow you to process larger-than-memory workloads", which is what the row now says. The 1.8B traces to our own commit e676d7c -- "GFQL is in-memory; single-node ceiling at ~1.8B" -- a statement about GFQL and Friendster, transposed onto the competitor. Runner, raw JSON and full provenance: pyg-bench benchmarks/ladybug_scan/ and results/ladybug-scan-20260728/. Docs build on dgx: docs/ci.sh exit 0, build succeeded, 19 pre-existing warnings, none from these pages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 7d0d0fc commit 02041a4

4 files changed

Lines changed: 53 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
4242

4343
### Documentation
4444
- **GFQL pay-as-you-go resident indexing user guide**: New :doc:`Pay-As-You-Go Resident Indexing <gfql/indexing>` page — the lifecycle guide to resident indexes (`gfql_index_all()` / `gfql_index_edges()` / `create_index()` / `show_indexes()` / `drop_index()`): what the node-id + CSR in/out adjacency sidecars are, what engages them on 0.58.0 (seeded typed-hop fast paths incl. property RETURNs and property-seeded lookups per #1768/#1770, direct `g.hop()`; the general polars chain traversal honestly noted as not yet covered), the staleness/validity contract (identity + fingerprint; rebind invalidates; declines are safe — identical results either way), engine notes (polars needs `gfql_index_all(engine='polars')` until #1767), 0.58.0-tag measured numbers, and a runnable end-to-end example. Wired into the GFQL toctree + recommended paths alongside :doc:`Seeded Traversal Indexes <gfql/index_adjacency>`.
45-
- **GFQL performance docs: 0.58.0 release-tag-verified numbers, siloed in one page**: `gfql/performance.rst` is now the canonical benchmark-numbers page (alongside `gfql/index_adjacency.rst` for the index benchmarks) — a benchmark rerun updates it alone. It carries the 0.58.0 tag sweep (DGX Spark GB10, warm medians N=30; four-engine numbers cross-engine parity-verified, competitor pairs validated against expected result rows): seeded typed-hop fast path across all four engines (e.g. pandas 29.9→2.46ms, 12.1×), native chain form, resident-index covered-shape lookups (with the `gfql_index_all(engine='polars')` caveat / PR #1767), flat seeded-hop scaling on pandas (0.159–0.164ms from 0.25M to 32M edges), the one-keyword `engine='polars'` LDBC SNB SF1 seed-lookup win (1,299.6→106.1ms, 12.3×), LDBC SNB interactive SF1 vs Neo4j 5.26 same-box pairs (GFQL 4 of 5; Neo4j wins recent-replies — reported as-is), OLAP multi-join vs embedded Kuzu (q9 14.2×) with the honest inverse (Kuzu wins single-table aggregates 2–4×, seeded property-projection lookups 2.4–64×), plus the prior Orkut/LiveJournal bulk sweep (moved from `engines.rst`, dated once) and its methodology. All other pages — `engines.rst`, `quick.rst`, `about.rst`, `overview.rst`, `index.rst` — now carry stable qualitative claims (e.g. "often an order of magnitude faster on query-heavy workloads") that link into `performance.rst` instead of inline figures, replacing the stale "up to ~38×" headlines and avoiding scattered per-claim version labels.
46-
- **Withdrew the OLAP q8 magnitude claim from `gfql/performance.rst`** (no replacement figure published): the page read "**q8 runs in 5.0 ms vs 1,004 ms for embedded Kuzu (200×)**". Reconciled rather than merely re-measured — our own side held up (5.0 ms published vs 5.06 ms re-measured), but the 1,004 ms was not canonical Kuzu: the old harness replaced q8/q9 path enumeration with an `OPTIONAL MATCH` degree-product rewrite and fed Kuzu pre-lowercased columns built *outside* the timing loop, while GFQL paid `toLower()` in-query. Re-running that same handicapped configuration reproduces **964.60 ms on the same box**, i.e. a 98.9× handicap applied to the competitor; canonical Kuzu 0.11.3 on a position-balanced, row-validated run at 100k persons / 2.4M FOLLOWS is **9.76 ms vs 5.06 ms = 1.93×**. The directional claim (`engine='polars'` wins q8) survives and is kept; the magnitude is withdrawn. The companion q9 figure is unchanged here — it is being researched separately.
45+
- **GFQL performance docs: 0.58.0 release-tag-verified numbers, siloed in one page**: `gfql/performance.rst` is now the canonical benchmark-numbers page (alongside `gfql/index_adjacency.rst` for the index benchmarks) — a benchmark rerun updates it alone. It carries the 0.58.0 tag sweep (DGX Spark GB10, warm medians N=30; four-engine numbers cross-engine parity-verified, competitor pairs validated against expected result rows): seeded typed-hop fast path across all four engines (e.g. pandas 29.9→2.46ms, 12.1×), native chain form, resident-index covered-shape lookups (with the `gfql_index_all(engine='polars')` caveat / PR #1767), flat seeded-hop scaling on pandas (0.159–0.164ms from 0.25M to 32M edges), the one-keyword `engine='polars'` LDBC SNB SF1 seed-lookup win (1,299.6→106.1ms, 12.3×), LDBC SNB interactive SF1 vs Neo4j 5.26 same-box pairs (GFQL 4 of 5; Neo4j wins recent-replies — reported as-is), OLAP multi-join vs embedded Kuzu (q9 1.26×) with the honest inverse (Kuzu wins single-table aggregates 2–4×, seeded property-projection lookups 2.4–64×), plus the prior Orkut/LiveJournal bulk sweep (moved from `engines.rst`, dated once) and its methodology. All other pages — `engines.rst`, `quick.rst`, `about.rst`, `overview.rst`, `index.rst` — now carry stable qualitative claims (e.g. "often an order of magnitude faster on query-heavy workloads") that link into `performance.rst` instead of inline figures, replacing the stale "up to ~38×" headlines and avoiding scattered per-claim version labels.
46+
- **Withdrew the OLAP q8 magnitude claim from `gfql/performance.rst`** (no replacement figure published): the page read "**q8 runs in 5.0 ms vs 1,004 ms for embedded Kuzu (200×)**". Reconciled rather than merely re-measured — our own side held up (5.0 ms published vs 5.06 ms re-measured), but the 1,004 ms was not canonical Kuzu: the old harness replaced q8/q9 path enumeration with an `OPTIONAL MATCH` degree-product rewrite and fed Kuzu pre-lowercased columns built *outside* the timing loop, while GFQL paid `toLower()` in-query. Re-running that same handicapped configuration reproduces **964.60 ms on the same box**, i.e. a 98.9× handicap applied to the competitor; canonical Kuzu 0.11.3 on a position-balanced, row-validated run at 100k persons / 2.4M FOLLOWS is **9.76 ms vs 5.06 ms = 1.93×**. The directional claim (`engine='polars'` wins q8) survives and is kept; the magnitude is withdrawn.
47+
- **Replaced the OLAP q9 figure in `gfql/performance.rst` with a re-measured value**: the page read "q9 is **14.2×**"; it now reads **66.6 ms vs 84.1 ms for embedded Kuzu 0.11.3 (1.26×)** at 100k persons / 2.4M FOLLOWS with `engine='polars'`. Reconciled, not merely contradicted, and the same way q8 was: the published 14.2× was **65.03 ms / 920.69 ms**, and the raw artifact behind it records a Kuzu query text that is *not the query GFQL ran* — Kuzu got the old harness's `OPTIONAL MATCH` degree-product rewrite while GFQL ran the canonical two-hop path enumeration. Re-run on dgx-spark under the perf lock (30 timed runs after 5 warmups, position-balanced K G G K, both arms in one session, values identical at 45,562,239 on every slot): our own side reproduces (**64.2–66.3 ms** vs 65.03 published), the rewrite reproduces (**859.5–878.5 ms** vs 920.69 published), and canonical Kuzu answers the *same* query in **83.1–83.9 ms** — a **10.3–10.5× handicap** applied to the competitor. The honest same-query ratio is **1.27–1.29×** this session, consistent with the 1.26× published by the provenance-carrying matched lane (`pyg-bench published/docs-numbers.json`, key `graphbench.100k.q9.polars_vs_kuzu`), which is the figure the page now quotes.
48+
- **LadybugDB was actually run, and the hardcoded competitor literal is now a measurement**: `benchmarks/gfql/bench_ladybug_cypher.py` carried `LADYBUG = {"full_scan": 3789, "range": 7.5, "point": 0.3, "count": 3.3, "scan_rel_props": 15722, "scan_rel_rowid": 14562}` — a bare dict with no URL, version, hardware or citation, and the only competitor input to the published `~65×` / `~3.5–3.7×` ratios. LadybugDB **0.18.1** was installed and run on dgx-spark under the perf lock, in one session, against GFQL `engine='polars'` (master `1537e467`, RAPIDS image, polars 1.35.2) on **one** generated 5M-node / 20M-edge graph read by both arms (`nodes.parquet` sha256 `22c8b239…`, `edges.parquet` `80ededc5…`), 2 warmups + 5 timed runs, slots interleaved L G G L, host load 1.53 → 2.54. Every op's result values are **digest-identical** across the two engines on all four slots. Ladybug is timed at its fastest result-producing scope of four measured (Arrow / polars / pandas / row iterator), because picking a slower scope for a competitor is the defect being corrected. Measured: full node scan **59.0 ms vs 364.3 ms = 6.2×** (published `~65×`) and the 1,001-row range scan **5.1 ms vs 7.6 ms = 1.5×**. **The `~3.5–3.7×` relationship-scan claim is withdrawn — measured fairly on one box those ops are not GFQL wins** (`scan_rel_props` 3256.9 vs 1925.6 ms; `scan_rel_rowid` 3157.5 vs 1449.1 ms), so the page no longer claims them. The old literal is **reconciled**: it is a Python-row-iterator ("drain") measurement on hardware ~1.5× slower — literal / drain-here is 1.52 / 1.56 / 1.52 on the three scans and 0.98–1.06 on the three ops where scope is irrelevant — and our own side was right again (`~65×` implies a 58.3 ms GFQL scan; measured 58.95 ms). Also removed the `1.8B-edge graph in <8 GB RAM` capacity line attributed to LadybugDB in `engines.rst`: it is not on ladybugdb.com, blog.ladybugdb.com, docs.ladybugdb.com or their GitHub README — LadybugDB documents only the qualitative *"on-disk databases allow you to process larger-than-memory workloads"*, which is what the page now says — and the `1.8B` traces to our own commit `e676d7cbe` ("GFQL is in-memory; single-node ceiling at ~1.8B"), i.e. a statement about GFQL and Friendster that was transposed onto the competitor. Runner, raw JSON and provenance: pyg-bench `benchmarks/ladybug_scan/` and `results/ladybug-scan-20260728/`.
4749

4850
## [0.58.0 - 2026-07-22]
4951

benchmarks/gfql/bench_ladybug_cypher.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
#!/usr/bin/env python3
22
"""FAIR GFQL-vs-Ladybug: run Ladybug's benchmark ops as GFQL Cypher MATCH...RETURN
3-
(the row pipeline), NOT dataframe shortcuts. Compares against LadybugDB's PUBLISHED
4-
5M/20M numbers (their figures, their hardware — a cross-machine comparison; treat
5-
ratios as indicative). Ladybug best (5M/20M, ms): full_scan 3789, range 7.5,
6-
point 0.3, count 3.3, out_degree100 59.8, scan_rel_props 15722,
7-
scan_rel_rowid 14562. Kuzu count 46.
3+
(the row pipeline), NOT dataframe shortcuts. Op shapes follow
4+
https://github.com/LadybugDB/kuzu-ladybug-benchmark.
5+
6+
The `LADYBUG` reference column below is a MEASUREMENT, not a literal: LadybugDB 0.18.1,
7+
embedded, on the same host and in the same session as the GFQL arm, on the same generated
8+
5M-node / 20M-edge graph, warm medians (2 warmups + 5 timed runs, median across two
9+
position-balanced slots), timed at Ladybug's FASTEST result-producing scope of the four
10+
measured (zero-copy Arrow / polars / pandas / row iterator) -- Arrow for every scan,
11+
the row iterator for the single-row point lookup -- because GFQL returns a materialized
12+
columnar frame and choosing a slower scope for the competitor is not a comparison. Result values were digest-identical
13+
across the two engines on every op. Runner, raw JSON and full provenance:
14+
pyg-bench `benchmarks/ladybug_scan/` and `results/ladybug-scan-20260728/`.
15+
16+
Numbers here are from dgx-spark (GB10); on another box, re-run the pyg-bench lane rather
17+
than reusing these.
818
919
FAIRNESS: each engine is benchmarked on a graph built in ITS OWN NATIVE frame type
1020
(pandas/polars/cuDF), built ONCE outside the timing loop. An earlier version built the
@@ -49,8 +59,9 @@ def build(engine):
4959
"scan_rel_props": "MATCH (a)-[o]->(b) RETURN a.id, b.id, o.since",
5060
"scan_rel_rowid": "MATCH (a)-[r]->(b) RETURN a.id, b.id",
5161
}
52-
LADYBUG = {"full_scan": 3789, "range": 7.5, "point": 0.3, "count": 3.3,
53-
"scan_rel_props": 15722, "scan_rel_rowid": 14562}
62+
# LadybugDB 0.18.1, dgx-spark, 2026-07-28, Arrow materialization (see module docstring).
63+
LADYBUG = {"full_scan": 364.28, "range": 7.57, "point": 0.30, "count": 3.08,
64+
"scan_rel_props": 1925.61, "scan_rel_rowid": 1449.13}
5465

5566
def _size(res):
5667
df = getattr(res, "_nodes", None)

docs/source/gfql/engines.rst

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,22 @@ guess. The measured pairs behind every claim live in :doc:`performance` (and
171171
Kuzu as the store; GFQL for bulk read analytics.
172172
* - **LadybugDB**
173173
- Actively-maintained **Kuzu fork** (Kuzu is archived); embedded C++, strongly-typed
174-
Cypher, opt-in ART *or* hash indexing, zero-copy Arrow/CSR scans, and **out-of-core
175-
billion-scale** (query a 1.8B-edge graph in <8 GB RAM).
176-
- Against **LadybugDB's published numbers** for their own 5M-node / 20M-edge suite
177-
(their figures, their hardware; GFQL measured separately on the same Cypher row
178-
pipeline, each engine on its **native** frames — a cross-machine comparison, so
179-
read it as indicative): GFQL **wins the scan-shaped ops** — full node scans and
180-
relationship property/rowid scans — by large margins. **Point lookups** are close
181-
in absolute terms but favor Ladybug's B-tree/hash **index seek** over a columnar
182-
scan; a resident GFQL node-id index (tracked in issue #1676) should close that.
183-
Ladybug wins the two ops backed by persistent structure: point lookups and a
174+
Cypher, opt-in ART *or* hash indexing, zero-copy Arrow/CSR scans, and an on-disk
175+
mode its docs describe as processing larger-than-memory workloads.
176+
- **Measured on one host, in one session, on the same 5M-node / 20M-edge graph** —
177+
LadybugDB 0.18.1 embedded against GFQL ``engine='polars'``, result values
178+
digest-identical on every op, Ladybug timed at its fastest (zero-copy Arrow)
179+
materialization: GFQL wins the **node-scan** shapes — a full node scan **6.2×**
180+
and a 1,001-row range scan **1.5×**. **Point lookups** are close in absolute terms
181+
but favor Ladybug's hash/ART **index seek** over a columnar scan; a resident GFQL
182+
node-id index (tracked in issue #1676) should close that. Ladybug also holds a
184183
cached relationship ``COUNT(*)`` (an O(1) count vs GFQL's O(E) endpoint-validated
185184
scan — a dataframe has no referential integrity). GFQL's angle is
186185
dataframe-native, in-process, and GPU-accelerated with no separate store to
187-
load/index. Details and reproducer in the :doc:`performance` methodology notes.
188-
- **Complement:** Ladybug is a durable embedded store with an out-of-core mode
189-
(billion-scale in <8 GB RAM); GFQL is a query engine over your dataframes. GFQL's
186+
load/index. Numbers, provenance and reproducer in the :doc:`performance`
187+
methodology notes.
188+
- **Complement:** Ladybug is a durable embedded store with an on-disk,
189+
larger-than-memory mode; GFQL is a query engine over your dataframes. GFQL's
190190
*default* is in-memory, but it is **not limited to it** — Polars streaming
191191
(``GFQL_POLARS_CPU_STREAMING=1``, disk-spill) and the cudf-polars streaming executor
192192
(``GFQL_POLARS_GPU_EXECUTOR=streaming``) are larger-than-memory paths

docs/source/gfql/performance.rst

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,10 @@ gathers (PR #1770). Neo4j still wins recent-replies — reported as-is.
136136
OLAP multi-join
137137
~~~~~~~~~~~~~~~
138138

139-
On the graph-benchmark OLAP multi-join queries at 100k-node scale with
140-
``engine='polars'``: GFQL wins q8 against embedded Kuzu; q9 is **14.2×**.
139+
On the graph-benchmark OLAP multi-join queries at 100k-node scale (100,000 persons /
140+
2.4M ``FOLLOWS``) with ``engine='polars'``, against embedded Kuzu 0.11.3 on the same host
141+
and in the same session: GFQL wins q8, and **q9 runs in 66.6 ms vs 84.1 ms (1.26×)**. Both
142+
sides run the same canonical query text and return identical values.
141143

142144
When not to use GFQL
143145
~~~~~~~~~~~~~~~~~~~~
@@ -225,14 +227,20 @@ Methodology (prior sweep)
225227
``benchmarks/gfql/pandas_vs_polars.py``, and ``benchmarks/gfql/index_vs_kuzu_prepared.py``
226228
(vs kuzu). Numbers on this page are rendered from saved runs; the page does not re-run
227229
them.
228-
- **LadybugDB comparison** (referenced qualitatively in :doc:`engines`): the Ladybug
229-
figures are **their published results on their hardware**; the GFQL side ran on the host
230-
above via ``benchmarks/gfql/bench_ladybug_cypher.py`` (5M/20M synthetic per their suite
231-
shape, native frames per engine, warm medians) — a cross-machine comparison, disclosed
232-
as such. GFQL won the scan-shaped ops by large margins (full node scan ~65×,
233-
relationship property/rowid scans ~3.5–3.7×); Ladybug won the two ops backed by
234-
persistent structure — point lookups (index seek vs columnar scan) and a cached
235-
relationship ``COUNT(*)``.
230+
- **LadybugDB comparison** (referenced qualitatively in :doc:`engines`): **both sides
231+
measured on the host above, in one session, on the same generated 5M-node / 20M-edge
232+
graph** — LadybugDB **0.18.1** embedded in a host venv against GFQL ``engine='polars'``
233+
in the container above. Op shapes are those of
234+
`LadybugDB/kuzu-ladybug-benchmark <https://github.com/LadybugDB/kuzu-ladybug-benchmark>`_
235+
via ``benchmarks/gfql/bench_ladybug_cypher.py``; warm medians (2 warmups + 5 timed runs),
236+
slots interleaved L G G L, and **every op's result values are digest-identical across the
237+
two engines**. Ladybug is timed at its **fastest** result-producing scope of the four measured — for
238+
both cells below that is zero-copy Arrow, not its Python row iterator — because GFQL
239+
returns a materialized columnar frame.
240+
GFQL wins the node-scan shapes: **full node scan 59.0 ms vs 364.3 ms (6.2×)** and the
241+
1,001-row **range scan 5.1 ms vs 7.6 ms (1.5×)**. Point lookups stay with Ladybug's
242+
index seek over a columnar scan (a resident GFQL node-id index is tracked in issue
243+
#1676), as does a cached relationship ``COUNT(*)``.
236244

237245
There is **no universal winner**: ``polars`` typically takes over from ~10K edges up
238246
(``pandas`` still wins trivial sub-millisecond operations), and the right GPU

0 commit comments

Comments
 (0)