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.
- openalex_reporting-0.1.0/LICENSE +21 -0
- openalex_reporting-0.1.0/PKG-INFO +123 -0
- openalex_reporting-0.1.0/README.md +92 -0
- openalex_reporting-0.1.0/oar/__init__.py +48 -0
- openalex_reporting-0.1.0/oar/build.py +851 -0
- openalex_reporting-0.1.0/oar/config.py +115 -0
- openalex_reporting-0.1.0/oar/db.py +15 -0
- openalex_reporting-0.1.0/oar/doc.py +274 -0
- openalex_reporting-0.1.0/oar/doc_template.md +236 -0
- openalex_reporting-0.1.0/oar/fetch.py +365 -0
- openalex_reporting-0.1.0/oar/flatten.py +275 -0
- openalex_reporting-0.1.0/oar/ids.py +71 -0
- openalex_reporting-0.1.0/oar/indicators.py +485 -0
- openalex_reporting-0.1.0/oar/oamdata.py +83 -0
- openalex_reporting-0.1.0/oar/schema.py +446 -0
- openalex_reporting-0.1.0/oar/sql/macros.sql +247 -0
- openalex_reporting-0.1.0/openalex_reporting.egg-info/PKG-INFO +123 -0
- openalex_reporting-0.1.0/openalex_reporting.egg-info/SOURCES.txt +25 -0
- openalex_reporting-0.1.0/openalex_reporting.egg-info/dependency_links.txt +1 -0
- openalex_reporting-0.1.0/openalex_reporting.egg-info/requires.txt +9 -0
- openalex_reporting-0.1.0/openalex_reporting.egg-info/top_level.txt +1 -0
- openalex_reporting-0.1.0/pyproject.toml +50 -0
- openalex_reporting-0.1.0/setup.cfg +4 -0
- openalex_reporting-0.1.0/tests/test_age.py +15 -0
- openalex_reporting-0.1.0/tests/test_build.py +187 -0
- openalex_reporting-0.1.0/tests/test_fetch.py +77 -0
- openalex_reporting-0.1.0/tests/test_flatten.py +57 -0
|
@@ -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
|
+
[](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
|
+
[](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__"])
|