History of openlegaldata_citations
The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.
[entries] Add the date when each entry was added, and remove icon_hash beff7c9
The dates are those of the commits that first added the entries' files (2020-08-11 for the entries of the initial commit); renamed entries keep the date of their first name. Changed: none
diff --git a/entries/openlegaldata_citations/__init__.py b/entries/openlegaldata_citations/__init__.py
index f9e0990..42537a4 100644
--- a/entries/openlegaldata_citations/__init__.py
+++ b/entries/openlegaldata_citations/__init__.py
@@ -48,7 +48,7 @@ url = ['https://huggingface.co/datasets/openlegaldata/legal-citation-graph-germa
'https://huggingface.co/datasets/openlegaldata/legal-citation-graph']
citation = [('M. Ostendorff, T. Blume, S. Ostendorff, "Towards an Open Platform for Legal Information." Proceedings of the ACM/IEEE Joint Conference on Digital Libraries in 2020 (JCDL \'20), 385-388 (2020)',
'https://doi.org/10.1145/3383583.3398616')]
-icon_hash = None
+date_added = '2026-10-31'
upstream_license = ("Open Database License (ODbL) v1.0 (2026); CC BY 4.0 (2019)",
"https://opendatacommons.org/licenses/odbl/1-0/")
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) bc3dbbd
The lists of properties at the end of the descriptions marked edge properties with "Edge" but left node properties unmarked, or grouped them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property was often unclear. Every item now starts with (Node), (Edge) or (Graph), followed by the property names, a colon and the explanation, one item per property or group of properties; qualifications (kinds of nodes, networks) follow the names in parentheses. Properties that were only named got a short explanation, and a few items that named the wrong kind of property were corrected.
diff --git a/entries/openlegaldata_citations/__init__.py b/entries/openlegaldata_citations/__init__.py
index 621c335..f9e0990 100644
--- a/entries/openlegaldata_citations/__init__.py
+++ b/entries/openlegaldata_citations/__init__.py
@@ -32,17 +32,17 @@ The decisions are described by their court and the attributes of the decision wh
The networks have the following properties:
-- `is_law`: whether the node is a law section (otherwise it is a court decision).
-- `oldp_id` (`2019`): the id of the decision or the law section in Open Legal Data.
-- `slug` (`2026`): the slug of the decision, or `<book slug>/<section slug>` for a law section.
-- `court`, `chamber`, `city`, `state`, `jurisdiction`, `level_of_appeal` (decisions): the court (e.g. `Bundesgerichtshof`), its chamber or senate (e.g. `2. Strafsenat`), its city, its state (or `Bundesrepublik Deutschland` for federal courts), its branch of jurisdiction (e.g. `Ordentliche Gerichtsbarkeit`, `Arbeitsgerichtsbarkeit`) and its level of appeal (e.g. `Bundesgericht`, `Landgericht`).
-- `court_id` (`2019`, decisions): the id of the court in Open Legal Data (-1 if unknown).
-- `date`, `file_number`, `decision_type` (decisions): the date of the decision (`YYYY-MM-DD`), its file number (`Aktenzeichen`, e.g. `2 StR 454/17`) and its type (e.g. `Urteil`, `Beschluss`).
-- `review_status` (`2026`, decisions): the review status of the decision in Open Legal Data (e.g. `accepted`).
-- `source`, `private` (`2019`, decisions): the source from which the decision was collected, and whether it was marked as private (1, 0 or -1 if unknown).
-- `book_code`, `section`, `law_title` (law sections): the abbreviation of the law (e.g. `BGB`, `ZPO`), the section (e.g. `§ 433`) and its title.
-- `book_slug` (`2026`, law sections): the slug of the law book (e.g. `bgb`).
-- Edge `count`: the number of times the citation was extracted."""
+- (Node) `is_law`: whether the node is a law section (otherwise it is a court decision).
+- (Node) `oldp_id` (`2019`): the id of the decision or the law section in Open Legal Data.
+- (Node) `slug` (`2026`): the slug of the decision, or `<book slug>/<section slug>` for a law section.
+- (Node) `court`, `chamber`, `city`, `state`, `jurisdiction`, `level_of_appeal` (decisions): the court (e.g. `Bundesgerichtshof`), its chamber or senate (e.g. `2. Strafsenat`), its city, its state (or `Bundesrepublik Deutschland` for federal courts), its branch of jurisdiction (e.g. `Ordentliche Gerichtsbarkeit`, `Arbeitsgerichtsbarkeit`) and its level of appeal (e.g. `Bundesgericht`, `Landgericht`).
+- (Node) `court_id` (`2019`, decisions): the id of the court in Open Legal Data (-1 if unknown).
+- (Node) `date`, `file_number`, `decision_type` (decisions): the date of the decision (`YYYY-MM-DD`), its file number (`Aktenzeichen`, e.g. `2 StR 454/17`) and its type (e.g. `Urteil`, `Beschluss`).
+- (Node) `review_status` (`2026`, decisions): the review status of the decision in Open Legal Data (e.g. `accepted`).
+- (Node) `source`, `private` (`2019`, decisions): the source from which the decision was collected, and whether it was marked as private (1, 0 or -1 if unknown).
+- (Node) `book_code`, `section`, `law_title` (law sections): the abbreviation of the law (e.g. `BGB`, `ZPO`), the section (e.g. `§ 433`) and its title.
+- (Node) `book_slug` (`2026`, law sections): the slug of the law book (e.g. `bgb`).
+- (Edge) `count`: the number of times the citation was extracted."""
tags = ['Informational', 'Citation', 'Legal', 'Weighted', 'Metadata']
url = ['https://huggingface.co/datasets/openlegaldata/legal-citation-graph-germany',
'https://huggingface.co/datasets/openlegaldata/legal-citation-graph']
[entries] New entry: openlegaldata_citations 4d08d9b
diff --git a/entries/openlegaldata_citations/__init__.py b/entries/openlegaldata_citations/__init__.py
new file mode 100644
index 0000000..621c335
--- /dev/null
+++ b/entries/openlegaldata_citations/__init__.py
@@ -0,0 +1,201 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from .. import *
+import pyarrow.parquet as pq
+
+title = "German court decision citations (Open Legal Data, 2019, 2026)"
+description = r"""Directed networks of the citations in German court decisions, to other decisions and to the sections of laws, extracted by Open Legal Data (the Open Legal Data Platform, de.openlegaldata.io), a project that collects German court decisions and laws from the websites of the courts and of the federal and state governments, and extracts their references automatically (from the citations in the text of the decisions, such as file numbers of decisions and "§ 433 BGB" for laws).
+
+Each node is a court decision (a case) or a section of a law, and a directed edge from A to B means that A cites B: a decision cites another decision or a law section, or a law section refers to another section of the same law. The edges are weighted by the number of times the citation was extracted (a decision citing the same law section in several places of its text gives as many rows in the data). There are two networks:
+
+- `2019`: the citation graph of the first dataset of court decisions of Open Legal Data (published in 2019, with about 100,000 decisions), with decisions and laws identified by the numeric ids of the database of Open Legal Data (decisions and laws numbered separately); it contains only citations from decisions (442,236 rows).
+- `2026`: the dump of 20 May 2026 (7,438,459 rows), with decisions identified by their slug in Open Legal Data (e.g. `bgh-2018-09-05-2-str-45417`, the page `https://de.openlegaldata.io/case/<slug>/`) and law sections by the slug of their law book and their own (e.g. `bgb/433`); it also contains the references between the sections of the same law (from the law texts).
+
+The decisions are described by their court and the attributes of the decision when they cite something (those cited without citing anything only have the attributes of their court in `2026`, and none in `2019`); the law sections by their law book, section number and title (when they are cited). The extraction is automatic, and the references to decisions cover only those that could be matched to a decision in the database.
+
+The networks have the following properties:
+
+- `is_law`: whether the node is a law section (otherwise it is a court decision).
+- `oldp_id` (`2019`): the id of the decision or the law section in Open Legal Data.
+- `slug` (`2026`): the slug of the decision, or `<book slug>/<section slug>` for a law section.
+- `court`, `chamber`, `city`, `state`, `jurisdiction`, `level_of_appeal` (decisions): the court (e.g. `Bundesgerichtshof`), its chamber or senate (e.g. `2. Strafsenat`), its city, its state (or `Bundesrepublik Deutschland` for federal courts), its branch of jurisdiction (e.g. `Ordentliche Gerichtsbarkeit`, `Arbeitsgerichtsbarkeit`) and its level of appeal (e.g. `Bundesgericht`, `Landgericht`).
+- `court_id` (`2019`, decisions): the id of the court in Open Legal Data (-1 if unknown).
+- `date`, `file_number`, `decision_type` (decisions): the date of the decision (`YYYY-MM-DD`), its file number (`Aktenzeichen`, e.g. `2 StR 454/17`) and its type (e.g. `Urteil`, `Beschluss`).
+- `review_status` (`2026`, decisions): the review status of the decision in Open Legal Data (e.g. `accepted`).
+- `source`, `private` (`2019`, decisions): the source from which the decision was collected, and whether it was marked as private (1, 0 or -1 if unknown).
+- `book_code`, `section`, `law_title` (law sections): the abbreviation of the law (e.g. `BGB`, `ZPO`), the section (e.g. `§ 433`) and its title.
+- `book_slug` (`2026`, law sections): the slug of the law book (e.g. `bgb`).
+- Edge `count`: the number of times the citation was extracted."""
+tags = ['Informational', 'Citation', 'Legal', 'Weighted', 'Metadata']
+url = ['https://huggingface.co/datasets/openlegaldata/legal-citation-graph-germany',
+ 'https://huggingface.co/datasets/openlegaldata/legal-citation-graph']
+citation = [('M. Ostendorff, T. Blume, S. Ostendorff, "Towards an Open Platform for Legal Information." Proceedings of the ACM/IEEE Joint Conference on Digital Libraries in 2020 (JCDL \'20), 385-388 (2020)',
+ 'https://doi.org/10.1145/3383583.3398616')]
+icon_hash = None
+upstream_license = ("Open Database License (ODbL) v1.0 (2026); CC BY 4.0 (2019)",
+ "https://opendatacommons.org/licenses/odbl/1-0/")
+
+# pinned commits of the two datasets
+_prefix_2026 = 'https://huggingface.co/datasets/openlegaldata/legal-citation-graph-germany/resolve/061d0bda82a630de65fbf944a8fb65e184495650'
+_prefix_2019 = 'https://huggingface.co/datasets/openlegaldata/legal-citation-graph/resolve/936a283a6c3ad0a2e349ab60766f37b4c3cee7cf'
+_files_2026 = [(tuple(f"dump-20260520/train-0000{i}-of-00005.parquet" for i in range(5)),
+ "2026", None)]
+_files_2019 = [("data/de/2019/refs.csv.gz", "2019", None)]
+files = _files_2019 + _files_2026
+
+def fetch_upstream(force=False):
+ name = __name__.split(".")[-1]
+ fetch_upstream_files(name, _prefix_2019, _files_2019, force)
+ fetch_upstream_files(name, _prefix_2026, _files_2026, force)
+
+# For checks only: if set, only (about) this many rows are read
+_check_rows = None
+
+_case_cols = {"court_name": "court", "court_chamber": "chamber",
+ "court_city": "city", "court_state": "state",
+ "court_jurisdiction": "jurisdiction",
+ "court_level_of_appeal": "level_of_appeal", "date": "date",
+ "file_number": "file_number", "type": "decision_type",
+ "review_status": "review_status", "source_name": "source",
+ "private": "private", "court_id": "court_id"}
+_law_cols = {"book_code": "book_code", "book_slug": "book_slug",
+ "section": "section", "title": "law_title"}
+
+def _read(name, fnames):
+ import polars as pl
+ if fnames[0].endswith(".csv.gz"):
+ with open_upstream_file(name, fnames[0], "rb") as f:
+ df = pl.read_csv(f.read(), infer_schema=False, n_rows=_check_rows)
+ # the numeric ids of the database (cases and laws are numbered
+ # separately)
+ key = lambda side: (pl.when(pl.col(f"{side}_type") == "Case")
+ .then(pl.lit("c:")).otherwise(pl.lit("l:")) +
+ pl.col(f"{side}_id"))
+ else:
+ # the slugs of the decisions, and those of the law sections, which
+ # are unique within their law book
+ key = lambda side: (pl.when(pl.col(f"{side}_type") == "Case")
+ .then(pl.lit("c:") + pl.col(f"{side}_slug"))
+ .otherwise(pl.lit("l:") +
+ pl.col(f"{side}_law_book_slug") + "/" +
+ pl.col(f"{side}_slug")))
+ tabs = []
+ for fn in fnames:
+ with open_upstream_file(name, fn, "rb", seekable=True) as f:
+ schema = pq.read_schema(f)
+ # the attributes are read as categorical, to save memory
+ cols = [c for c in schema.names
+ if c not in ("from_case_source_name",)]
+ attr = [c for c in cols if c not in ("from_slug", "to_slug")]
+ t = pl.from_arrow(pq.read_table(f, columns=cols,
+ read_dictionary=attr))
+ t = t.with_columns(pl.col(c).cast(pl.String) for c in
+ ["from_type", "to_type", "from_law_book_slug",
+ "to_law_book_slug"])
+ t = t.with_columns(src=key("from"), dst=key("to")).drop(
+ "from_slug", "to_slug")
+ t = t.with_columns(pl.col(c).cast(pl.Categorical) for c in
+ ["from_type", "to_type", "from_law_book_slug",
+ "to_law_book_slug"])
+ tabs.append(t)
+ if _check_rows is not None:
+ break
+ df = pl.concat(tabs, how="diagonal_relaxed")
+ del tabs
+ return df.with_columns(pl.col("from_case_date").cast(pl.String))
+ return df.with_columns(src=key("from"), dst=key("to"))
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+ import polars as pl
+ name = __name__.split(".")[-1]
+ for fnames, alt, fmt in files:
+ if alts is not None and alt not in alts:
+ continue
+ if isinstance(fnames, str):
+ fnames = [fnames]
+ df = _read(name, fnames)
+
+ nodes = (pl.concat([df["src"], df["dst"]]).unique(maintain_order=True)
+ .to_frame("key").with_row_index("v"))
+ edges = (df.select("src", "dst").group_by("src", "dst", maintain_order=True)
+ .len("count")
+ .join(nodes.rename({"key": "src", "v": "s"}), on="src")
+ .join(nodes.rename({"key": "dst", "v": "t"}), on="dst"))
+
+ # the attributes of the decisions: those of the citing ones, and
+ # those of the court of the cited ones
+ ccols = [c for c in _case_cols if f"from_case_{c}" in df.columns]
+ df = df.with_columns(pl.col("from_type", "to_type").cast(pl.String))
+ cases = (df.filter(pl.col("from_type") == "Case")
+ .select(pl.col("src").alias("key"),
+ *[pl.col(f"from_case_{c}").alias(c) for c in ccols])
+ .unique(subset="key", keep="first", maintain_order=True))
+ tcols = [c for c in ccols if f"to_case_{c}" in df.columns]
+ cited = (df.filter(pl.col("to_type") == "Case")
+ .select(pl.col("dst").alias("key"),
+ *[pl.col(f"to_case_{c}").alias(c) for c in tcols])
+ .group_by("key").agg(pl.all().drop_nulls().first())
+ .join(cases.select("key"), on="key", how="anti"))
+ cases = pl.concat([cases, cited], how="diagonal_relaxed")
+ lcols = [c for c in _law_cols if f"to_law_{c}" in df.columns]
+ laws = (df.filter(pl.col("to_type") == "Law")
+ .select(pl.col("dst").alias("key"),
+ *[pl.col(f"to_law_{c}").alias(c) for c in lcols])
+ .group_by("key").agg(pl.all().drop_nulls().first()))
+ if "from_law_book_slug" in df.columns:
+ # the citing law sections (of the same law book as the cited)
+ laws = pl.concat([laws,
+ df.filter(pl.col("from_type") == "Law")
+ .select(pl.col("src").alias("key"),
+ pl.col("from_law_book_slug").cast(pl.String)
+ .alias("book_slug"))
+ .unique(subset="key")
+ .join(laws.select("key"), on="key", how="anti")],
+ how="diagonal_relaxed")
+ del df
+ attrs = (nodes.join(pl.concat([cases, laws], how="diagonal_relaxed"),
+ on="key", how="left").sort("v"))
+ del cases, laws, cited
+
+ g = Graph(directed=True)
+ g.add_vertex(len(nodes))
+ g.add_edge_list(numpy.column_stack((edges["s"].to_numpy(),
+ edges["t"].to_numpy())))
+ keys = attrs["key"]
+ g.vp.is_law = g.new_vp("bool", vals=keys.str.starts_with("l:").to_numpy())
+ if alt == "2019":
+ g.vp.oldp_id = g.new_vp("int64_t", vals=keys.str.slice(2).cast(pl.Int64).to_numpy())
+ else:
+ g.vp.slug = g.new_vp("string", vals=keys.str.slice(2).to_list())
+ for c, k in list(_case_cols.items()) + list(_law_cols.items()):
+ if c not in attrs.columns:
+ continue
+ x = attrs[c].cast(pl.String)
+ if c in ("private", "court_id"):
+ g.vp[k] = g.new_vp("int", vals=x.cast(pl.Int64).fill_null(-1).to_numpy())
+ else:
+ g.vp[k] = g.new_vp("string", vals=[" ".join(v.split()) for v in
+ x.fill_null("").to_list()])
+ g.ep.count = g.new_ep("int", vals=edges["count"].to_numpy())
+ del edges, attrs, nodes
+ yield alt, g