openalex-reporting 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gael Bernard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: openalex-reporting
3
+ Version: 0.1.0
4
+ Summary: A bibliometric data warehouse for any set of OpenAlex works
5
+ Author-email: Gael Bernard <gael.bernard@epfl.ch>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/epfl-p-data/OpenAlexReporting
8
+ Project-URL: Repository, https://github.com/epfl-p-data/OpenAlexReporting
9
+ Project-URL: Issues, https://github.com/epfl-p-data/OpenAlexReporting/issues
10
+ Keywords: bibliometrics,OpenAlex,DuckDB,reporting
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: duckdb>=2.0
24
+ Requires-Dist: pandas>=2.0
25
+ Requires-Dist: requests>=2.31
26
+ Provides-Extra: build
27
+ Requires-Dist: openalex-metrics>=0.5; extra == "build"
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest>=8; extra == "test"
30
+ Dynamic: license-file
31
+
32
+ # OAR — bibliometric reporting from OpenAlex
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/openalex-reporting)](https://pypi.org/project/openalex-reporting/)
35
+
36
+ OAR builds a bibliometric database for any set of papers: a university, a researcher, a country, a funder's portfolio or a list of DOIs. It answers the questions a research office asks of InCites. How many papers, how often they are cited relative to their field, who the institution publishes with, and how much of the output is open access.
37
+
38
+ The citation scores come from [OAM](https://github.com/epfl-p-data/OpenAlexImpactMetrics). OAM compares each paper with papers of the same subfield, the same publication year and the same kind of venue, journal or conference, and it stores the result as ids. OAR keeps those scores and adds what they leave out: titles, authors, affiliations and the open-access copy. The result is one DuckDB file. After the build, nothing calls OAM or the network, and the file can be handed to someone who does not have either installed.
39
+
40
+ A paper is scored only when it is in OAM's scope, peer-reviewed research in English, about 13% of OpenAlex. `P_all` counts every work in the set, of every OpenAlex type, including datasets and software. `P_scored` counts the ones the impact indicators use. For EPFL the two differ by about a third, and a report should say so.
41
+
42
+ ## Get started
43
+
44
+ Python 3.11 or newer.
45
+
46
+ ```bash
47
+ pip install openalex-reporting # query a database that already exists
48
+ pip install 'openalex-reporting[build]' # also install OAM, which oar.build reads
49
+ ```
50
+
51
+ Querying needs DuckDB 2.0 or newer. The install above provides it. An older DuckDB cannot open a file this package writes.
52
+
53
+ `oar.build` reads an OAM dataset. Install it once, from the machine that builds the database. One call fetches both Zenodo files: the scores, and the citation links published beside them.
54
+
55
+ ```python
56
+ import oam
57
+ oam.download(edges=True) # 12 GB plus the citation links, 9.9 GB, into ~/oam-data
58
+ ```
59
+
60
+ `oam.download()` without `edges=True` skips the links, and `oar.build` then stops. From archives already on disk, give the folder that holds both `oam-<snapshot>.tar` and `edges-<snapshot>.tar`: `oam.download("path/to/dist", edges=True)`. OAR still reads one OAM data directory.
61
+
62
+ Then build. This one is EPFL. The same call works for an author, an ORCID, a country, a funder or a list of DOIs.
63
+
64
+ ```python
65
+ import oar
66
+
67
+ oar.build(name="epfl", institution="I5124864")
68
+ oar.indicators(by=None)
69
+ ```
70
+
71
+ The file lands in `./data` when Python imports this checkout, which it does when you run it from the repository directory, and in `~/oar-data` otherwise. `oar.set_data_dir(path)` or the environment variable `OAR_DATA_DIR` puts it somewhere else. Set `OPENALEX_API_KEY` before building. A university is a few thousand requests; without a key the daily limit is too low. Copy `.env.example` to `.env` to keep the key out of the shell history.
72
+
73
+ A second call with the same selection does nothing if that snapshot was already built. The API responses are cached per work, so the next institution reuses the papers the two share.
74
+
75
+ ## What a build stores
76
+
77
+ The file has two parts.
78
+
79
+ The **entity** is this selection, described by the API: title, abstract, authors, affiliations, funders, locations, and the citation count in each recent year. The publication year, the subfield and the journal-or-conference flag are taken from the OAM snapshot, not from the API. Those three are what a score was computed from. When the API disagrees, the API values are kept beside them and the work is flagged `cohort_changed`. The build prints how often that happens.
80
+
81
+ The **world** is every work's id, score and institutional affiliation, copied from OAM, about 8 GB. That is what a baseline is computed from: the world, a country, or another institution. It is ids and names, not the text of every paper. `world="all"` also copies authors, DOIs and ORCIDs, so a researcher can be a peer. `world=None` keeps only the selection, and baselines are then unavailable.
82
+
83
+ A paper in a cohort of fewer than 500 works keeps its row and its raw citation count, but its normalised score, percentile and top-10% and top-1% flags are NULL. An impact average skips those NULLs, so it does not need a reliability filter. `count(ncs)` is the number of scored papers.
84
+
85
+ The file stores who cited the selection, in which year, and whether the citation is a self-citation. That comes from OAM's citation links, which `oam.download(edges=True)` installs. The build stops when they are missing.
86
+
87
+ The build writes a markdown file next to the database, with the same stem: `epfl-2026-10-14.md` beside `epfl-2026-10-14.duckdb`. It describes every table and column, explains which papers are scored and which are not, gives queries, and records reference values taken from that file. The text is `oar/doc_template.md`, the same for every database; the build fills in the numbers and the table list, and `oar.doc()` rewrites the file after the template changes.
88
+
89
+ The full OpenAlex snapshot is not stored. It is more than a terabyte, and the baselines do not need it.
90
+
91
+ ## The indicators
92
+
93
+ `oar.indicators()` returns one row per group. `counting="full"` gives every paper weight 1. `counting="frac"` gives a paper shared with other institutions a weight of one over the number of institutions. Impact uses scored papers, the ones with a normalised score, which is OAM's rule of cohorts of at least 500, so `MNCS_full` matches `oam.aggregate`.
94
+
95
+ ```python
96
+ oar.indicators(by="publication_year") # this selection
97
+ oar.baseline(world=True) # every scored paper
98
+ oar.baseline(country="CH") # Switzerland
99
+ oar.baseline(institution="I35440088") # ETH Zurich
100
+ oar.partners().head(20) # who EPFL publishes with
101
+ oar.works().head(20) # one row per paper, with its scores
102
+ ```
103
+
104
+ The same queries are stored in the file for a SQL client:
105
+
106
+ ```sql
107
+ SELECT * FROM indicators_by_year();
108
+ SELECT * FROM indicators_overall();
109
+ SELECT * FROM baseline_world_by_year();
110
+ SELECT * FROM baseline_country_by_year('CH');
111
+ ```
112
+
113
+ [Indicators](docs/indicators.md) maps each of these columns to the InCites list they stand in for, and records the ones that are not computed. [Building](docs/building.md) is the build, [querying](docs/querying.md) is every function, and [schema](docs/schema.md) is every table.
114
+
115
+ ## An example
116
+
117
+ `examples/epfl.py` builds EPFL and prints it next to ETH Zurich and next to Switzerland.
118
+
119
+ ## Data and licence
120
+
121
+ The descriptive fields come from the [OpenAlex API](https://openalex.org). The scores and the world tables come from OAM's snapshot, which is released under CC0. Say which snapshot you used. `oar.status()` prints it.
122
+
123
+ OAR is released under the MIT licence. See [LICENSE](LICENSE).
@@ -0,0 +1,92 @@
1
+ # OAR — bibliometric reporting from OpenAlex
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/openalex-reporting)](https://pypi.org/project/openalex-reporting/)
4
+
5
+ OAR builds a bibliometric database for any set of papers: a university, a researcher, a country, a funder's portfolio or a list of DOIs. It answers the questions a research office asks of InCites. How many papers, how often they are cited relative to their field, who the institution publishes with, and how much of the output is open access.
6
+
7
+ The citation scores come from [OAM](https://github.com/epfl-p-data/OpenAlexImpactMetrics). OAM compares each paper with papers of the same subfield, the same publication year and the same kind of venue, journal or conference, and it stores the result as ids. OAR keeps those scores and adds what they leave out: titles, authors, affiliations and the open-access copy. The result is one DuckDB file. After the build, nothing calls OAM or the network, and the file can be handed to someone who does not have either installed.
8
+
9
+ A paper is scored only when it is in OAM's scope, peer-reviewed research in English, about 13% of OpenAlex. `P_all` counts every work in the set, of every OpenAlex type, including datasets and software. `P_scored` counts the ones the impact indicators use. For EPFL the two differ by about a third, and a report should say so.
10
+
11
+ ## Get started
12
+
13
+ Python 3.11 or newer.
14
+
15
+ ```bash
16
+ pip install openalex-reporting # query a database that already exists
17
+ pip install 'openalex-reporting[build]' # also install OAM, which oar.build reads
18
+ ```
19
+
20
+ Querying needs DuckDB 2.0 or newer. The install above provides it. An older DuckDB cannot open a file this package writes.
21
+
22
+ `oar.build` reads an OAM dataset. Install it once, from the machine that builds the database. One call fetches both Zenodo files: the scores, and the citation links published beside them.
23
+
24
+ ```python
25
+ import oam
26
+ oam.download(edges=True) # 12 GB plus the citation links, 9.9 GB, into ~/oam-data
27
+ ```
28
+
29
+ `oam.download()` without `edges=True` skips the links, and `oar.build` then stops. From archives already on disk, give the folder that holds both `oam-<snapshot>.tar` and `edges-<snapshot>.tar`: `oam.download("path/to/dist", edges=True)`. OAR still reads one OAM data directory.
30
+
31
+ Then build. This one is EPFL. The same call works for an author, an ORCID, a country, a funder or a list of DOIs.
32
+
33
+ ```python
34
+ import oar
35
+
36
+ oar.build(name="epfl", institution="I5124864")
37
+ oar.indicators(by=None)
38
+ ```
39
+
40
+ The file lands in `./data` when Python imports this checkout, which it does when you run it from the repository directory, and in `~/oar-data` otherwise. `oar.set_data_dir(path)` or the environment variable `OAR_DATA_DIR` puts it somewhere else. Set `OPENALEX_API_KEY` before building. A university is a few thousand requests; without a key the daily limit is too low. Copy `.env.example` to `.env` to keep the key out of the shell history.
41
+
42
+ A second call with the same selection does nothing if that snapshot was already built. The API responses are cached per work, so the next institution reuses the papers the two share.
43
+
44
+ ## What a build stores
45
+
46
+ The file has two parts.
47
+
48
+ The **entity** is this selection, described by the API: title, abstract, authors, affiliations, funders, locations, and the citation count in each recent year. The publication year, the subfield and the journal-or-conference flag are taken from the OAM snapshot, not from the API. Those three are what a score was computed from. When the API disagrees, the API values are kept beside them and the work is flagged `cohort_changed`. The build prints how often that happens.
49
+
50
+ The **world** is every work's id, score and institutional affiliation, copied from OAM, about 8 GB. That is what a baseline is computed from: the world, a country, or another institution. It is ids and names, not the text of every paper. `world="all"` also copies authors, DOIs and ORCIDs, so a researcher can be a peer. `world=None` keeps only the selection, and baselines are then unavailable.
51
+
52
+ A paper in a cohort of fewer than 500 works keeps its row and its raw citation count, but its normalised score, percentile and top-10% and top-1% flags are NULL. An impact average skips those NULLs, so it does not need a reliability filter. `count(ncs)` is the number of scored papers.
53
+
54
+ The file stores who cited the selection, in which year, and whether the citation is a self-citation. That comes from OAM's citation links, which `oam.download(edges=True)` installs. The build stops when they are missing.
55
+
56
+ The build writes a markdown file next to the database, with the same stem: `epfl-2026-10-14.md` beside `epfl-2026-10-14.duckdb`. It describes every table and column, explains which papers are scored and which are not, gives queries, and records reference values taken from that file. The text is `oar/doc_template.md`, the same for every database; the build fills in the numbers and the table list, and `oar.doc()` rewrites the file after the template changes.
57
+
58
+ The full OpenAlex snapshot is not stored. It is more than a terabyte, and the baselines do not need it.
59
+
60
+ ## The indicators
61
+
62
+ `oar.indicators()` returns one row per group. `counting="full"` gives every paper weight 1. `counting="frac"` gives a paper shared with other institutions a weight of one over the number of institutions. Impact uses scored papers, the ones with a normalised score, which is OAM's rule of cohorts of at least 500, so `MNCS_full` matches `oam.aggregate`.
63
+
64
+ ```python
65
+ oar.indicators(by="publication_year") # this selection
66
+ oar.baseline(world=True) # every scored paper
67
+ oar.baseline(country="CH") # Switzerland
68
+ oar.baseline(institution="I35440088") # ETH Zurich
69
+ oar.partners().head(20) # who EPFL publishes with
70
+ oar.works().head(20) # one row per paper, with its scores
71
+ ```
72
+
73
+ The same queries are stored in the file for a SQL client:
74
+
75
+ ```sql
76
+ SELECT * FROM indicators_by_year();
77
+ SELECT * FROM indicators_overall();
78
+ SELECT * FROM baseline_world_by_year();
79
+ SELECT * FROM baseline_country_by_year('CH');
80
+ ```
81
+
82
+ [Indicators](docs/indicators.md) maps each of these columns to the InCites list they stand in for, and records the ones that are not computed. [Building](docs/building.md) is the build, [querying](docs/querying.md) is every function, and [schema](docs/schema.md) is every table.
83
+
84
+ ## An example
85
+
86
+ `examples/epfl.py` builds EPFL and prints it next to ETH Zurich and next to Switzerland.
87
+
88
+ ## Data and licence
89
+
90
+ The descriptive fields come from the [OpenAlex API](https://openalex.org). The scores and the world tables come from OAM's snapshot, which is released under CC0. Say which snapshot you used. `oar.status()` prints it.
91
+
92
+ OAR is released under the MIT licence. See [LICENSE](LICENSE).
@@ -0,0 +1,48 @@
1
+ """OAR — a bibliometric warehouse for any set of OpenAlex works.
2
+
3
+ import oar
4
+
5
+ oar.build(name="epfl", institution="I5124864")
6
+ oar.indicators() # output, impact, collaboration, open access
7
+ oar.baseline(country="CH") # the same indicators for a reference set
8
+ oar.partners() # who the set publishes with
9
+
10
+ The build reads OAM for the work set and the citation scores, and the OpenAlex
11
+ API for titles, authors and affiliations. The result is one DuckDB file. After
12
+ that, nothing here needs OAM or the network.
13
+
14
+ See the README for the indicators and for what a baseline compares against.
15
+ """
16
+
17
+ import re
18
+ from importlib.metadata import PackageNotFoundError, version
19
+ from pathlib import Path
20
+
21
+ from .build import build, status
22
+ from .config import set_data_dir
23
+ from .db import connect
24
+ from .doc import doc
25
+ from .indicators import baseline, indicators, partners, works
26
+
27
+
28
+ def _package_version():
29
+ try:
30
+ return version("openalex-reporting")
31
+ except PackageNotFoundError:
32
+ text = (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text()
33
+ match = re.search(r'(?m)^version = "([^"]+)"', text)
34
+ if match is None:
35
+ raise RuntimeError("pyproject.toml has no version")
36
+ return match.group(1)
37
+
38
+
39
+ __version__ = _package_version()
40
+
41
+ __all__ = [
42
+ "baseline", "build", "connect", "doc", "indicators", "partners",
43
+ "set_data_dir", "status", "works",
44
+ ]
45
+
46
+
47
+ def __dir__():
48
+ return sorted(__all__ + ["__version__"])