History of gleif_ownership
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] New entry: gleif_ownership 8ee393c
diff --git a/entries/gleif_ownership/__init__.py b/entries/gleif_ownership/__init__.py
new file mode 100644
index 0000000..1930848
--- /dev/null
+++ b/entries/gleif_ownership/__init__.py
@@ -0,0 +1,170 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2020-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 pandas
+
+title = "Corporate parent and fund relationships of legal entities (GLEIF, 2026)"
+description = """Networks of the corporate parent relationships, and of other relationships (fund management, subfunds, feeder funds, branches), among legal entities registered in the Global Legal Entity Identifier (LEI) System, from the Golden Copy of the Global Legal Entity Identifier Foundation (GLEIF) of 1 October 2026 (00:00 UTC). Every legal entity that takes part in financial transactions (companies, funds, branches, government bodies, etc.) can obtain an LEI, a 20-character identifier, and must report to the LEI issuing organization its direct accounting consolidating parent and its ultimate accounting consolidating parent (the parents that consolidate it in their financial statements), if these have an LEI themselves; the issuing organizations validate the data (to different degrees, see the GLEIF documentation), which the GLEIF publishes three times per day as the "Golden Copy".
+
+The entry has two networks. In `parents`, nodes are the legal entities that appear in at least one parent relationship, and a directed edge $(i,j)$ means that entity $j$ is the direct or the ultimate consolidating parent of entity $i$ (from child to parent). The two kinds of relationship between the same pair of entities are merged into a single edge, whose properties `is_direct` and `is_ultimate` say which of them apply (a direct parent that is not itself consolidated by another entity is also the ultimate parent, and both relationships are then usually reported: this is the case of 89,676 of the 170,510 edges). The relationship records of other kinds in the Golden Copy, which are not parent relationships (fund management, subfunds, feeder funds and international branches, about 229,000 records), are left out of `parents`. The network `all` contains all the relationship records of the Golden Copy (489,614), as they are: nodes are the entities that appear in at least one of them, and each record is a directed edge $(i,j)$ from its start node to its end node (e.g. from a fund to its manager), with its type (property `type`); there can be several edges between two entities. The "reporting exceptions", in which entities state the reason why they do not report a parent (e.g. a parent without an LEI, or a natural person), are not included in either network. Relationship records whose registration has lapsed (i.e., has not been renewed) are included, as are the few marked as inactive (see the property `status`). Four entities are reported as their own parents, and appear with self-loops in `parents` (in `all`, 3,223 records have the same start and end node). Four LEIs that appear in relationships have no LEI record in the Golden Copy, and their entity properties are empty. The percentages of ownership are given only for some relationships, as reported (20,579 edges of `parents`, 32,815 of `all`), and a few of them are larger than 100.
+
+The entity data (name, country, legal form, etc.) are those of the LEI records of the same Golden Copy.
+
+The networks have the following properties:
+
+- `name`: the legal name of the entity.
+- `lei`: the Legal Entity Identifier (LEI) of the entity.
+- `country`: the country of the legal address of the entity (ISO 3166-1 alpha-2 code).
+- `hq_country`: the country of the headquarters address of the entity.
+- `jurisdiction`: the legal jurisdiction of the entity (ISO 3166-1 country or subdivision code).
+- `category`: the entity category (`GENERAL`, `FUND`, `BRANCH`, `SOLE_PROPRIETOR`, `RESIDENT_GOVERNMENT_ENTITY` or `INTERNATIONAL_ORGANIZATION`).
+- `legal_form`: the legal form of the entity, as a code of the ISO 20275 Entity Legal Forms (ELF) code list, or `8888` if the legal form is not in the list (then given by `other_legal_form`).
+- `other_legal_form`: the legal form of the entity, as text, if it has no ELF code.
+- `entity_status`: `ACTIVE`, `INACTIVE` (e.g. dissolved or merged entities), or `NULL` if not known.
+- `registration_status`: the status of the LEI registration (`ISSUED`, `LAPSED` if it has not been renewed, `RETIRED`, `DUPLICATE`, `ANNULLED`, `PENDING_TRANSFER` or `PENDING_ARCHIVAL`).
+- Edge `type` (only in `all`): the type of the relationship: `IS_DIRECTLY_CONSOLIDATED_BY`, `IS_ULTIMATELY_CONSOLIDATED_BY`, `IS_FUND-MANAGED_BY`, `IS_SUBFUND_OF`, `IS_FEEDER_TO` or `IS_INTERNATIONAL_BRANCH_OF`.
+- Edge `is_direct` (only in `parents`): whether $j$ is the direct consolidating parent of $i$.
+- Edge `is_ultimate` (only in `parents`): whether $j$ is the ultimate consolidating parent of $i$.
+- Edge `percentage`: the percentage of $i$ held by $j$, if reported (in `parents`, from the direct relationship if both are reported; NaN otherwise).
+- Edge `status`: the status of the relationship (`ACTIVE`, `INACTIVE`, or `NULL` if not known; in `parents`, from the direct relationship if both are reported).
+- Edge `registration_status`: the status of the registration of the relationship (`PUBLISHED`, `LAPSED` if it has not been renewed, and in a few records of `all`, `PENDING_TRANSFER` or `PENDING_ARCHIVAL`; in `parents`, from the direct relationship if both are reported)."""
+tags = ['Economic', 'Commerce', 'Unweighted', 'Metadata']
+url = ['https://www.gleif.org/en/lei-data/gleif-golden-copy-and-delta-files',
+ 'https://goldencopy.gleif.org/']
+citation = [('Global Legal Entity Identifier Foundation (GLEIF), "GLEIF Golden Copy and Delta Files." (2026)',
+ 'https://www.gleif.org/en/lei-data/gleif-golden-copy-and-delta-files')]
+icon_hash = None
+upstream_license = ("Creative Commons Zero v1.0 Universal (CC0 1.0)",
+ "https://www.gleif.org/en/meta/lei-data-terms-of-use")
+
+# The Golden Copy published on 2026-10-01 at 00:00 UTC (the files of past
+# publications stay available; see
+# https://goldencopy.gleif.org/api/v2/golden-copies/publishes): the LEI
+# records (level 1) and the relationship records (level 2), in CSV
+upstream_prefix = 'https://goldencopy.gleif.org/storage/golden-copy-files/2026/10/01'
+LEI2 = '1282970/20261001-0000-gleif-goldencopy-lei2-golden-copy.csv'
+RR = '1283015/20261001-0000-gleif-goldencopy-rr-golden-copy.csv'
+FILES = (f'{LEI2}.zip:{os.path.basename(LEI2)}', f'{RR}.zip:{os.path.basename(RR)}')
+files = [(FILES, "parents", None), (FILES, "all", None)]
+
+PARENT_TYPES = {"IS_DIRECTLY_CONSOLIDATED_BY": "direct",
+ "IS_ULTIMATELY_CONSOLIDATED_BY": "ultimate"}
+
+ENTITY_COLS = {"Entity.LegalName": "name",
+ "Entity.LegalAddress.Country": "country",
+ "Entity.HeadquartersAddress.Country": "hq_country",
+ "Entity.LegalJurisdiction": "jurisdiction",
+ "Entity.EntityCategory": "category",
+ "Entity.LegalForm.EntityLegalFormCode": "legal_form",
+ "Entity.LegalForm.OtherLegalForm": "other_legal_form",
+ "Entity.EntityStatus": "entity_status",
+ "Registration.RegistrationStatus": "registration_status"}
+
+S, T = "Relationship.StartNode.NodeID", "Relationship.EndNode.NodeID"
+TYPE = "Relationship.RelationshipType"
+STATUS = "Relationship.RelationshipStatus"
+AMOUNT = "Relationship.Quantifiers.1.QuantifierAmount"
+UNITS = "Relationship.Quantifiers.1.QuantifierUnits"
+RSTATUS = "Registration.RegistrationStatus"
+
+def percentage(amount, units):
+ return float(amount) if units == "PERCENTAGE" and amount != "" else numpy.nan
+
+def parents_edges(rr, index):
+ """The parent relationships, with the direct and ultimate relationships
+ between the same entities merged into one edge."""
+ rr = rr[rr[TYPE].isin(PARENT_TYPES.keys())]
+ # the direct relationships first, so that their attributes are kept when
+ # merged with the ultimate ones
+ rr = rr.assign(kind=rr[TYPE].map(PARENT_TYPES))
+ rr = rr.sort_values("kind", kind="stable")
+ edges = {}
+ for s, t, kind, status, amount, units, rstatus in \
+ zip(rr[S], rr[T], rr.kind, rr[STATUS], rr[AMOUNT], rr[UNITS], rr[RSTATUS]):
+ u = index.setdefault(s, len(index))
+ v = index.setdefault(t, len(index))
+ e = edges.get((u, v))
+ if e is None:
+ e = edges[(u, v)] = [False, False, percentage(amount, units), status,
+ rstatus]
+ elif numpy.isnan(e[2]):
+ e[2] = percentage(amount, units)
+ e[0 if kind == "direct" else 1] = True
+ return edges
+
+def entity_props(g, index, f):
+ vals = {k: [""] * len(index) for k in ENTITY_COLS.values()}
+ for chunk in pandas.read_csv(f, dtype=str, keep_default_na=False,
+ usecols=["LEI"] + list(ENTITY_COLS.keys()),
+ chunksize=1 << 18):
+ for row in zip(chunk["LEI"], *[chunk[c] for c in ENTITY_COLS.keys()]):
+ i = index.get(row[0])
+ if i is None:
+ continue
+ for k, x in zip(ENTITY_COLS.values(), row[1:]):
+ vals[k][i] = x
+ for k, x in vals.items():
+ g.vp[k] = g.new_vp("string", vals=x)
+
+def fetch_upstream(force=False):
+ return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+ force)
+
+@cache_network()
+@coerce_props(vforce={"name": "string"}, eforce={"percentage": "double"})
+@annotate()
+def parse(alts=None):
+ global files
+ name = __name__.split(".")[-1]
+ for (f_lei, f_rr), alt, fmt in files:
+ if alts is not None and alt not in alts:
+ continue
+ with open_upstream_file(name, f_rr, "rb") as f:
+ rr = pandas.read_csv(f, dtype=str, keep_default_na=False,
+ usecols=[S, T, TYPE, STATUS, AMOUNT, UNITS, RSTATUS])
+ index = {}
+ g = Graph(directed=True)
+ if alt == "parents":
+ edges = parents_edges(rr, index)
+ del rr
+ g.add_vertex(len(index))
+ g.ep.is_direct = g.new_ep("bool")
+ g.ep.is_ultimate = g.new_ep("bool")
+ g.ep.percentage = g.new_ep("double")
+ g.add_edge_list([(u, v, d, ul, p) for (u, v), (d, ul, p, st, rs) in edges.items()],
+ eprops=[g.ep.is_direct, g.ep.is_ultimate, g.ep.percentage])
+ g.ep.status = g.new_ep("string", vals=[e[3] for e in edges.values()])
+ g.ep.registration_status = g.new_ep("string", vals=[e[4] for e in edges.values()])
+ del edges
+ else:
+ us = [index.setdefault(x, len(index)) for x in rr[S]]
+ vs = [index.setdefault(x, len(index)) for x in rr[T]]
+ g.add_vertex(len(index))
+ g.add_edge_list(numpy.column_stack((us, vs)))
+ g.ep.type = g.new_ep("string", vals=rr[TYPE])
+ g.ep.percentage = g.new_ep("double", vals=[percentage(a, u) for a, u in
+ zip(rr[AMOUNT], rr[UNITS])])
+ g.ep.status = g.new_ep("string", vals=rr[STATUS])
+ g.ep.registration_status = g.new_ep("string", vals=rr[RSTATUS])
+ del rr
+ # the entities in the order of their indices (dicts keep the order)
+ g.vp.lei = g.new_vp("string", vals=list(index.keys()))
+ with open_upstream_file(name, f_lei, "rb") as f:
+ entity_props(g, index, f)
+ yield alt, g