openalex-metrics 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.
Files changed (44) hide show
  1. openalex_metrics-0.1.0/.github/workflows/publish.yml +25 -0
  2. openalex_metrics-0.1.0/.github/workflows/tests.yml +22 -0
  3. openalex_metrics-0.1.0/CITATION.cff +17 -0
  4. openalex_metrics-0.1.0/LICENSE +21 -0
  5. openalex_metrics-0.1.0/MANIFEST.in +5 -0
  6. openalex_metrics-0.1.0/PKG-INFO +161 -0
  7. openalex_metrics-0.1.0/README.md +130 -0
  8. openalex_metrics-0.1.0/docs/building.md +146 -0
  9. openalex_metrics-0.1.0/docs/metrics.md +111 -0
  10. openalex_metrics-0.1.0/docs/openalex-RELEASE_NOTES.txt +302 -0
  11. openalex_metrics-0.1.0/docs/openalex-parquet-manifest.json +36571 -0
  12. openalex_metrics-0.1.0/docs/openalex-works-schema.txt +259 -0
  13. openalex_metrics-0.1.0/docs/querying.md +166 -0
  14. openalex_metrics-0.1.0/docs/scope.md +116 -0
  15. openalex_metrics-0.1.0/docs/snapshots.jsonl +4 -0
  16. openalex_metrics-0.1.0/docs/sources.yaml +72 -0
  17. openalex_metrics-0.1.0/examples/quickstart.py +28 -0
  18. openalex_metrics-0.1.0/oam/LICENSE-OpenAlex.txt +121 -0
  19. openalex_metrics-0.1.0/oam/__init__.py +147 -0
  20. openalex_metrics-0.1.0/oam/arrow.py +166 -0
  21. openalex_metrics-0.1.0/oam/bundle.py +540 -0
  22. openalex_metrics-0.1.0/oam/checks.py +294 -0
  23. openalex_metrics-0.1.0/oam/config.py +292 -0
  24. openalex_metrics-0.1.0/oam/duck.py +245 -0
  25. openalex_metrics-0.1.0/oam/metrics.py +336 -0
  26. openalex_metrics-0.1.0/oam/openalex.py +75 -0
  27. openalex_metrics-0.1.0/oam/query.py +229 -0
  28. openalex_metrics-0.1.0/oam/report.py +200 -0
  29. openalex_metrics-0.1.0/oam/resolve_store.py +309 -0
  30. openalex_metrics-0.1.0/oam/resolver.py +311 -0
  31. openalex_metrics-0.1.0/oam/store.py +647 -0
  32. openalex_metrics-0.1.0/openalex_metrics.egg-info/PKG-INFO +161 -0
  33. openalex_metrics-0.1.0/openalex_metrics.egg-info/SOURCES.txt +42 -0
  34. openalex_metrics-0.1.0/openalex_metrics.egg-info/dependency_links.txt +1 -0
  35. openalex_metrics-0.1.0/openalex_metrics.egg-info/requires.txt +9 -0
  36. openalex_metrics-0.1.0/openalex_metrics.egg-info/top_level.txt +1 -0
  37. openalex_metrics-0.1.0/pyproject.toml +49 -0
  38. openalex_metrics-0.1.0/setup.cfg +4 -0
  39. openalex_metrics-0.1.0/tests/conftest.py +192 -0
  40. openalex_metrics-0.1.0/tests/test_aggregate.py +62 -0
  41. openalex_metrics-0.1.0/tests/test_bundle.py +171 -0
  42. openalex_metrics-0.1.0/tests/test_explain_resolve.py +60 -0
  43. openalex_metrics-0.1.0/tests/test_ids.py +50 -0
  44. openalex_metrics-0.1.0/tests/test_metrics.py +81 -0
@@ -0,0 +1,25 @@
1
+ name: publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ pypi:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+ - name: Build
21
+ run: |
22
+ pip install build
23
+ python -m build
24
+ - name: Publish
25
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,22 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ python: ["3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python }}
19
+ - name: Install
20
+ run: pip install ".[test,build]"
21
+ - name: Test
22
+ run: pytest
@@ -0,0 +1,17 @@
1
+ cff-version: 1.2.0
2
+ title: "OAM: OpenAlex Metrics"
3
+ message: If you use OAM, please cite it using the metadata below.
4
+ type: software
5
+ authors:
6
+ - family-names: Bernard
7
+ given-names: Gael
8
+ affiliation: EPFL
9
+ email: gael.bernard@epfl.ch
10
+ repository-code: https://github.com/epfl-p-data/OpenAlexImpactMetrics
11
+ url: https://github.com/epfl-p-data/OpenAlexImpactMetrics
12
+ license: MIT
13
+ version: 0.1.0
14
+ identifiers:
15
+ - type: doi
16
+ value: 10.5281/zenodo.23493154
17
+ description: "Data release on Zenodo, all versions. Each snapshot has its own version DOI."
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OAM contributors
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,5 @@
1
+ include CITATION.cff
2
+ recursive-include tests *.py
3
+ recursive-include docs *
4
+ recursive-include examples *.py
5
+ recursive-include .github *.yml
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: openalex-metrics
3
+ Version: 0.1.0
4
+ Summary: Field-normalised citation impact (MNCS, PP top 10%) 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/OpenAlexImpactMetrics
8
+ Project-URL: Repository, https://github.com/epfl-p-data/OpenAlexImpactMetrics
9
+ Project-URL: Issues, https://github.com/epfl-p-data/OpenAlexImpactMetrics/issues
10
+ Keywords: bibliometrics,citation impact,OpenAlex,MNCS
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>=1.2
24
+ Requires-Dist: pandas>=2.0
25
+ Requires-Dist: requests>=2.31
26
+ Provides-Extra: build
27
+ Requires-Dist: pyarrow>=14; extra == "build"
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest>=8; extra == "test"
30
+ Dynamic: license-file
31
+
32
+ # OAM — citation impact from OpenAlex
33
+
34
+ OAM tells you whether a set of papers was cited more or less than comparable papers. You can score a university, a researcher, a country, a funder's portfolio or a list of DOIs. It runs on your own machine, and the data comes from [OpenAlex](https://openalex.org).
35
+
36
+ ## Get started
37
+
38
+ Python 3.11 or newer.
39
+
40
+ ```bash
41
+ pip install openalex-metrics
42
+ ```
43
+
44
+ Download the data once. This fetches the latest release from [Zenodo](https://doi.org/10.5281/zenodo.23493154), about 12 GB, and checks every file:
45
+
46
+ ```python
47
+ import oam
48
+
49
+ oam.download()
50
+ ```
51
+
52
+ The files go to `~/oam-data`. To put them somewhere else, call `oam.set_data_dir(path)` before downloading, or set the `OAM_DATA_DIR` environment variable. You need about 25 GB free while the archive is unpacked.
53
+
54
+ Then score a set of papers:
55
+
56
+ ```python
57
+ ids = oam.resolve(institution="I5124864", year=(2020, 2024)) # EPFL
58
+ oam.aggregate(ids).round(2)
59
+ ```
60
+
61
+ `oam.resolve` finds the papers. `oam.aggregate` scores the ones that can be compared with similar papers. To find an institution's id, search by name:
62
+
63
+ ```python
64
+ oam.dimension("institutions", search="Lausanne")
65
+ ```
66
+
67
+ ## Three examples
68
+
69
+ These numbers come from the OpenAlex snapshot of 14 October 2026.
70
+
71
+ ### A university, over five years
72
+
73
+ EPFL, 2020 to 2024. Of the 36,475 works with EPFL's name on them, 22,207 are scored.
74
+
75
+ ```python
76
+ ids = oam.resolve(institution="I5124864", year=(2020, 2024))
77
+ oam.aggregate(ids).round(2)
78
+ ```
79
+
80
+ ```
81
+ publication_year P P_weighted TCS MCS MNCS PP_top10 PP_top1
82
+ 2020 4433 4433.0 200062.0 45.13 1.95 21.52 3.41
83
+ 2021 4498 4498.0 150841.0 33.54 1.80 20.08 3.11
84
+ 2022 4349 4349.0 120403.0 27.69 1.89 20.74 2.90
85
+ 2023 4470 4470.0 85462.0 19.12 1.74 20.25 3.02
86
+ 2024 4457 4457.0 58542.0 13.13 1.83 21.11 3.43
87
+ ```
88
+
89
+ ### A researcher
90
+
91
+ Jennifer Doudna, from her [ORCID](https://orcid.org/0000-0001-9161-999X). Of 755 works, 391 are scored. `by=None` gives one row for the whole set.
92
+
93
+ ```python
94
+ ids = oam.resolve(orcid="0000-0001-9161-999X")
95
+ oam.aggregate(ids, by=None).round(2)
96
+ ```
97
+
98
+ ```
99
+ P P_weighted TCS MCS MNCS PP_top10 PP_top1
100
+ 391 391.0 91886.0 235.0 5.97 59.34 14.83
101
+ ```
102
+
103
+ ### A country
104
+
105
+ Switzerland, 2020 to 2024. Of 490,707 works, 265,954 are scored.
106
+
107
+ ```python
108
+ ids = oam.resolve(country="CH", year=(2020, 2024))
109
+ oam.aggregate(ids).round(2)
110
+ ```
111
+
112
+ ```
113
+ publication_year P P_weighted TCS MCS MNCS PP_top10 PP_top1
114
+ 2020 52142 52142.0 2108683.0 40.44 1.80 18.78 2.87
115
+ 2021 55231 55231.0 1744205.0 31.58 1.78 18.79 3.02
116
+ 2022 53311 53311.0 1206510.0 22.63 1.71 18.77 2.88
117
+ 2023 52502 52502.0 887357.0 16.90 1.71 19.19 3.08
118
+ 2024 52768 52768.0 605921.0 11.48 1.75 19.27 2.99
119
+ ```
120
+
121
+ You can select papers the same way by author, funder, journal, field or DOI. [Querying](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/docs/querying.md) lists every option.
122
+
123
+ ## Reading the numbers
124
+
125
+ - **P** is the number of papers that received a score.
126
+ - **MNCS** compares those papers with similar papers worldwide. The world average is 1. A value of 2 means cited twice as much.
127
+ - **PP_top10** is the share of papers among the 10% most cited of their group. For the world, that share is about 10%. **PP_top1** is the same for the top 1%.
128
+
129
+ [How the scores work](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/docs/metrics.md) explains the other columns, and full and fractional counting.
130
+
131
+ ## How this differs from OpenAlex
132
+
133
+ OpenAlex already gives most works a citation score, the FWCI. It compares each work with everything OpenAlex indexes, and it counts citations from anything, including preprints, datasets and theses. OAM compares a paper only with peer-reviewed research papers like it, and counts only the citations that come from such papers.
134
+
135
+ ## Which papers are scored
136
+
137
+ A paper is scored when it passes all seven rules. On the snapshot of 14 October 2026, 62,493,369 of 464,765,485 works pass, which is 13.45%.
138
+
139
+ 1. It is in a core journal or a conference proceedings.
140
+ 2. It has a reference list.
141
+ 3. It has at least one institutional affiliation.
142
+ 4. It is in English.
143
+ 5. It has an OpenAlex subfield.
144
+ 6. It is not paratext, such as an editorial, erratum or table of contents.
145
+ 7. It is not retracted.
146
+
147
+ Each paper is then compared with papers of the same subfield, publication year and venue type (journal or conference).
148
+
149
+ These rules are fixed for the data you download. To score a different set of papers, for example to keep papers in every language, edit the rules and rebuild the data. [Scope](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/docs/scope.md) explains each rule, and [Building the data](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/docs/building.md) explains the rebuild.
150
+
151
+ ## Data and licence
152
+
153
+ The data is built from the public OpenAlex snapshot and released under CC0. `oam.download()` installs the latest version. To keep one snapshot, pass its Zenodo record id, listed under Versions on the [Zenodo page](https://doi.org/10.5281/zenodo.23493154):
154
+
155
+ ```python
156
+ oam.download(record=23493155) # always the 2026-10-14 snapshot
157
+ ```
158
+
159
+ If you publish results, cite the version you used. Each version has its own DOI, and `oam.status()` shows which snapshot is installed.
160
+
161
+ OAM is released under the MIT licence. See [LICENSE](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/LICENSE).
@@ -0,0 +1,130 @@
1
+ # OAM — citation impact from OpenAlex
2
+
3
+ OAM tells you whether a set of papers was cited more or less than comparable papers. You can score a university, a researcher, a country, a funder's portfolio or a list of DOIs. It runs on your own machine, and the data comes from [OpenAlex](https://openalex.org).
4
+
5
+ ## Get started
6
+
7
+ Python 3.11 or newer.
8
+
9
+ ```bash
10
+ pip install openalex-metrics
11
+ ```
12
+
13
+ Download the data once. This fetches the latest release from [Zenodo](https://doi.org/10.5281/zenodo.23493154), about 12 GB, and checks every file:
14
+
15
+ ```python
16
+ import oam
17
+
18
+ oam.download()
19
+ ```
20
+
21
+ The files go to `~/oam-data`. To put them somewhere else, call `oam.set_data_dir(path)` before downloading, or set the `OAM_DATA_DIR` environment variable. You need about 25 GB free while the archive is unpacked.
22
+
23
+ Then score a set of papers:
24
+
25
+ ```python
26
+ ids = oam.resolve(institution="I5124864", year=(2020, 2024)) # EPFL
27
+ oam.aggregate(ids).round(2)
28
+ ```
29
+
30
+ `oam.resolve` finds the papers. `oam.aggregate` scores the ones that can be compared with similar papers. To find an institution's id, search by name:
31
+
32
+ ```python
33
+ oam.dimension("institutions", search="Lausanne")
34
+ ```
35
+
36
+ ## Three examples
37
+
38
+ These numbers come from the OpenAlex snapshot of 14 October 2026.
39
+
40
+ ### A university, over five years
41
+
42
+ EPFL, 2020 to 2024. Of the 36,475 works with EPFL's name on them, 22,207 are scored.
43
+
44
+ ```python
45
+ ids = oam.resolve(institution="I5124864", year=(2020, 2024))
46
+ oam.aggregate(ids).round(2)
47
+ ```
48
+
49
+ ```
50
+ publication_year P P_weighted TCS MCS MNCS PP_top10 PP_top1
51
+ 2020 4433 4433.0 200062.0 45.13 1.95 21.52 3.41
52
+ 2021 4498 4498.0 150841.0 33.54 1.80 20.08 3.11
53
+ 2022 4349 4349.0 120403.0 27.69 1.89 20.74 2.90
54
+ 2023 4470 4470.0 85462.0 19.12 1.74 20.25 3.02
55
+ 2024 4457 4457.0 58542.0 13.13 1.83 21.11 3.43
56
+ ```
57
+
58
+ ### A researcher
59
+
60
+ Jennifer Doudna, from her [ORCID](https://orcid.org/0000-0001-9161-999X). Of 755 works, 391 are scored. `by=None` gives one row for the whole set.
61
+
62
+ ```python
63
+ ids = oam.resolve(orcid="0000-0001-9161-999X")
64
+ oam.aggregate(ids, by=None).round(2)
65
+ ```
66
+
67
+ ```
68
+ P P_weighted TCS MCS MNCS PP_top10 PP_top1
69
+ 391 391.0 91886.0 235.0 5.97 59.34 14.83
70
+ ```
71
+
72
+ ### A country
73
+
74
+ Switzerland, 2020 to 2024. Of 490,707 works, 265,954 are scored.
75
+
76
+ ```python
77
+ ids = oam.resolve(country="CH", year=(2020, 2024))
78
+ oam.aggregate(ids).round(2)
79
+ ```
80
+
81
+ ```
82
+ publication_year P P_weighted TCS MCS MNCS PP_top10 PP_top1
83
+ 2020 52142 52142.0 2108683.0 40.44 1.80 18.78 2.87
84
+ 2021 55231 55231.0 1744205.0 31.58 1.78 18.79 3.02
85
+ 2022 53311 53311.0 1206510.0 22.63 1.71 18.77 2.88
86
+ 2023 52502 52502.0 887357.0 16.90 1.71 19.19 3.08
87
+ 2024 52768 52768.0 605921.0 11.48 1.75 19.27 2.99
88
+ ```
89
+
90
+ You can select papers the same way by author, funder, journal, field or DOI. [Querying](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/docs/querying.md) lists every option.
91
+
92
+ ## Reading the numbers
93
+
94
+ - **P** is the number of papers that received a score.
95
+ - **MNCS** compares those papers with similar papers worldwide. The world average is 1. A value of 2 means cited twice as much.
96
+ - **PP_top10** is the share of papers among the 10% most cited of their group. For the world, that share is about 10%. **PP_top1** is the same for the top 1%.
97
+
98
+ [How the scores work](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/docs/metrics.md) explains the other columns, and full and fractional counting.
99
+
100
+ ## How this differs from OpenAlex
101
+
102
+ OpenAlex already gives most works a citation score, the FWCI. It compares each work with everything OpenAlex indexes, and it counts citations from anything, including preprints, datasets and theses. OAM compares a paper only with peer-reviewed research papers like it, and counts only the citations that come from such papers.
103
+
104
+ ## Which papers are scored
105
+
106
+ A paper is scored when it passes all seven rules. On the snapshot of 14 October 2026, 62,493,369 of 464,765,485 works pass, which is 13.45%.
107
+
108
+ 1. It is in a core journal or a conference proceedings.
109
+ 2. It has a reference list.
110
+ 3. It has at least one institutional affiliation.
111
+ 4. It is in English.
112
+ 5. It has an OpenAlex subfield.
113
+ 6. It is not paratext, such as an editorial, erratum or table of contents.
114
+ 7. It is not retracted.
115
+
116
+ Each paper is then compared with papers of the same subfield, publication year and venue type (journal or conference).
117
+
118
+ These rules are fixed for the data you download. To score a different set of papers, for example to keep papers in every language, edit the rules and rebuild the data. [Scope](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/docs/scope.md) explains each rule, and [Building the data](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/docs/building.md) explains the rebuild.
119
+
120
+ ## Data and licence
121
+
122
+ The data is built from the public OpenAlex snapshot and released under CC0. `oam.download()` installs the latest version. To keep one snapshot, pass its Zenodo record id, listed under Versions on the [Zenodo page](https://doi.org/10.5281/zenodo.23493154):
123
+
124
+ ```python
125
+ oam.download(record=23493155) # always the 2026-10-14 snapshot
126
+ ```
127
+
128
+ If you publish results, cite the version you used. Each version has its own DOI, and `oam.status()` shows which snapshot is installed.
129
+
130
+ OAM is released under the MIT licence. See [LICENSE](https://github.com/epfl-p-data/OpenAlexImpactMetrics/blob/main/LICENSE).
@@ -0,0 +1,146 @@
1
+ # Building the data
2
+
3
+ Most users download the data with `oam.download()` and never build it. Build it yourself when you want:
4
+
5
+ - a different scope, for example keeping non-English papers
6
+ - a newer OpenAlex snapshot than the published release
7
+ - to publish a release for others to download
8
+
9
+ ## What you need
10
+
11
+ - `pip install "openalex-metrics[build]"`. The `build` extra installs PyArrow, which step 1 uses to read the snapshot. Downloading the data does not need it.
12
+ - About 60 GB of free disk space during the build, and about 23 GB once it is finished
13
+ - A few hours. The first step downloads about 67 GB from the public OpenAlex snapshot, so its duration is set by your bandwidth: about 1 hour 40 minutes at 100 Mbit/s
14
+ - No account or API key. The snapshot is public
15
+
16
+ ## Build
17
+
18
+ ```python
19
+ import oam
20
+
21
+ oam.set_data_dir("/Volumes/data/oam") # optional: somewhere with enough space
22
+ oam.build()
23
+ oam.status()
24
+ ```
25
+
26
+ `oam.build()` can be interrupted and called again; it continues where it stopped. Everything it prints is also saved under `logs/` in the data directory.
27
+
28
+ It runs three steps in order:
29
+
30
+ | Step | What it does | Time |
31
+ |---|---|---|
32
+ | `"store"` | Reads the OpenAlex snapshot, applies the scope, keeps the citations between in-scope papers | Your bandwidth |
33
+ | `"metrics"` | Computes citation counts, bin averages and every paper's scores | About 2 minutes |
34
+ | `"resolve"` | Builds the lookup tables `oam.resolve` uses | About 25 minutes |
35
+
36
+ To run one step on its own, pass `steps="metrics"`, or a list such as `steps=["metrics", "resolve"]`.
37
+
38
+ The download uses 8 parallel connections. If your connection is fast, raise it until the speed in the log stops improving; each connection uses about 1 GB of memory:
39
+
40
+ ```python
41
+ oam.build(workers=16)
42
+ ```
43
+
44
+ ## Check the result
45
+
46
+ ```python
47
+ oam.check() # consistency checks, a few seconds
48
+ oam.profile() # what the data contains: scope funnel, citations, world averages
49
+ ```
50
+
51
+ `oam.profile()` must show a world MNCS of **1.0000**. That confirms each paper was divided by its own bin's average.
52
+
53
+ ## Change the scope
54
+
55
+ 1. Edit `SCOPE` in [`oam/config.py`](../oam/config.py). Each rule is a line of SQL. [Scope](scope.md) documents the defaults and how to write a rule.
56
+ 2. Rebuild from scratch:
57
+
58
+ ```python
59
+ oam.build(reset=True)
60
+ ```
61
+
62
+ A scope cannot be changed on existing data: the build stored only the papers and citations the old scope kept. `oam.build()` detects an edited scope and stops with a message rather than mixing the two.
63
+
64
+ ## Move to a new snapshot
65
+
66
+ OpenAlex publishes a new snapshot about once a quarter. To use it:
67
+
68
+ ```python
69
+ oam.build(reset=True, refresh_manifest=True)
70
+ ```
71
+
72
+ `refresh_manifest=True` fetches the list of files of the current snapshot. `reset=True` deletes the data built from the previous one. Data from two snapshots cannot be merged.
73
+
74
+ ## Publish a release
75
+
76
+ From a finished build, write the archive `oam.download()` installs:
77
+
78
+ ```python
79
+ oam.export("dist") # dist/oam-<snapshot>.tar, 11.9 GiB for the 2026-10-14 snapshot
80
+ ```
81
+
82
+ Then upload the file to [Zenodo](https://zenodo.org) under the CC0 licence, as a new version of the [existing record](https://doi.org/10.5281/zenodo.23493154). Set the version to the snapshot date. Keep the file name `oam.export` wrote: `oam.download()` finds the file whose name starts with `oam-` and ends with `.tar`, and reads the snapshot date from it.
83
+
84
+ Nothing in OAM needs to change. `ZENODO_RECORD` in [`oam/config.py`](../oam/config.py) holds the record's all-versions id, `23493154`, which Zenodo resolves to the newest version, so every install of OAM downloads the new snapshot from then on.
85
+
86
+ The archive records its snapshot date, its scope and a checksum of every file. `oam.download()` checks all three. It refuses to mix two snapshots in one data directory unless called with `replace=True`.
87
+
88
+ To share a build without Zenodo, send the archive file itself. The recipient installs it with:
89
+
90
+ ```python
91
+ oam.download(source="oam-2026-10-14.tar")
92
+ ```
93
+
94
+ To publish under a record of your own, set `OAM_ZENODO_RECORD` to its all-versions id, or pass `record=` to `oam.download()`.
95
+
96
+ ## Other settings
97
+
98
+ | Environment variable | Default | Effect |
99
+ |---|---|---|
100
+ | `OAM_DATA_DIR` | `~/oam-data`, or `./data` in a clone | Where the data is stored |
101
+ | `OAM_ZENODO_RECORD` | `23493154`, the latest version | The Zenodo record `oam.download()` fetches. A version's own id pins that snapshot |
102
+ | `OAM_MEMORY_LIMIT` | `20GB` | Memory DuckDB may use in the metrics and resolve steps |
103
+ | `OAM_THREADS` | `8` | Threads DuckDB may use |
104
+
105
+ Set them in the environment before starting Python.
106
+
107
+ To try the whole build on a few files first. The result is not usable for scoring:
108
+
109
+ ```python
110
+ oam.set_data_dir("/tmp/oam-trial")
111
+ oam.build(steps="store", limit=3, smallest=True)
112
+ ```
113
+
114
+ To keep the xpack records, a third of OpenAlex that the default build leaves out of the comparison, use `oam.build(include_xpack=True)`. See [Scope](scope.md#what-is-not-configurable).
115
+
116
+ ## Release checklist
117
+
118
+ The uploads themselves are done by hand.
119
+
120
+ 1. `oam.export("dist")` from a finished build, then install that archive into an empty directory and re-run the examples in the README.
121
+ 2. On the [Zenodo record](https://doi.org/10.5281/zenodo.23493154), click New version. Set the version and the collected date to the snapshot date, the publication date to today, and update the snapshot figures in the description.
122
+ 3. Upload `dist/oam-<snapshot>.tar`, keeping the file name `oam.export` wrote. The browser upload often stalls on a file this size; the API is more reliable. Create a token with the `deposit:write` and `deposit:actions` scopes, and run these with the draft's id from the address bar:
123
+
124
+ ```bash
125
+ API=https://zenodo.org/api/records/<draft id>/draft/files
126
+ NAME=oam-<snapshot>.tar
127
+ curl -f -X POST -H "Authorization: Bearer $ZENODO_TOKEN" -H "Content-Type: application/json" -d "[{\"key\":\"$NAME\"}]" "$API"
128
+ curl -f -X PUT -H "Authorization: Bearer $ZENODO_TOKEN" -H "Content-Type: application/octet-stream" --upload-file "dist/$NAME" "$API/$NAME/content"
129
+ curl -f -X POST -H "Authorization: Bearer $ZENODO_TOKEN" "$API/$NAME/commit"
130
+ ```
131
+
132
+ The checksum Zenodo shows must equal `md5 dist/oam-<snapshot>.tar`. Then publish.
133
+ 4. Add the snapshot to the README figures. `ZENODO_RECORD` does not change: it already points at the newest version.
134
+
135
+ A new version of the package is only needed when the code changes. A new snapshot does not need one: `oam.download()` always fetches the latest version on Zenodo.
136
+
137
+ ## Release the package
138
+
139
+ Publishing happens from GitHub, with no token stored in the repository. The first time, on [PyPI](https://pypi.org), add a pending publisher for the project `openalex-metrics`: owner `epfl-p-data`, repository `OpenAlexImpactMetrics`, workflow `publish.yml`, environment `pypi`.
140
+
141
+ Then, for each release:
142
+
143
+ 1. Set `version` in [`pyproject.toml`](../pyproject.toml) and in [`CITATION.cff`](../CITATION.cff), and push.
144
+ 2. On GitHub, create a release named `v` followed by that version, for example `v0.1.0`.
145
+
146
+ The [publish workflow](../.github/workflows/publish.yml) builds the package and uploads it to PyPI.
@@ -0,0 +1,111 @@
1
+ # How the scores work
2
+
3
+ OAM compares every paper with papers like it, then summarises those comparisons for a set of papers. This page starts with one paper, then moves to a set.
4
+
5
+ ## Comparable papers: the bin
6
+
7
+ A paper is compared with the papers that share three things with it:
8
+
9
+ - its **subfield**, as assigned by OpenAlex (for example Artificial Intelligence)
10
+ - its **publication year**
11
+ - its **venue type**: journal article or conference paper
12
+
13
+ That group is the paper's **bin**. Citation habits differ a lot between fields and years, so comparing within the bin is what makes a mathematics paper from 2014 comparable with a biology paper from 2022.
14
+
15
+ ## One paper
16
+
17
+ Take `W2103094658`, a 2014 conference paper in Artificial Intelligence. It has 37 citations. Its bin holds 14,122 papers, which have about 18 citations on average.
18
+
19
+ ```python
20
+ oam.get_metrics(["W2103094658"])
21
+ ```
22
+
23
+ ```
24
+ citations ncs percentile decile is_top10 is_top1
25
+ 37 2.05 0.94 1 True False
26
+ ```
27
+
28
+ | Column | Meaning | This paper |
29
+ |---|---|---|
30
+ | `citations` | Citations received so far | 37 |
31
+ | `ncs` | Citations divided by the bin's average. 1 means exactly average | 2.05: twice the average |
32
+ | `percentile` | Share of the bin with as many citations or fewer, from 0 to 1 | 0.94: ahead of about 94% of its bin |
33
+ | `decile` | Rank in tenths of the bin: 1 is the top tenth, 10 the bottom | 1: top tenth |
34
+ | `is_top10` | Among the 10% most cited papers of the bin | yes |
35
+ | `is_top1` | Among the 1% most cited papers of the bin | no |
36
+
37
+ `ncs` stands for *normalised citation score*. It is the number the set-level MNCS is built from.
38
+
39
+ ## A set of papers
40
+
41
+ For a set of papers — an institution, a lab, a funder's portfolio — OAM summarises the per-paper scores:
42
+
43
+ | Column | Meaning |
44
+ |---|---|
45
+ | `P` | Number of papers scored |
46
+ | `MNCS` | Average `ncs`. 1 is the world average |
47
+ | `PP_top10` | Percentage of papers with `is_top10`. About 10 for the world |
48
+ | `PP_top1` | Percentage of papers with `is_top1`. About 1 for the world |
49
+ | `TCS` | Total citations |
50
+ | `MCS` | Average citations per paper, not normalised |
51
+ | `P_weighted` | Sum of the paper weights. Equals `P` unless you use fractional counting (below) |
52
+
53
+ EPFL in 2014:
54
+
55
+ ```python
56
+ ids = oam.resolve(institution="I5124864", year=2014)
57
+ oam.aggregate(ids)
58
+ ```
59
+
60
+ ```
61
+ publication_year P P_weighted TCS MCS MNCS PP_top10 PP_top1
62
+ 2014 4439 4439.0 264771 59.65 1.962 21.83 3.18
63
+ ```
64
+
65
+ `aggregate` returns one row per publication year by default. Pass `by=None` for a single row covering the whole set.
66
+
67
+ - MNCS 1.96: on average, an EPFL paper from 2014 was cited 1.96 times as much as a typical paper of its bin.
68
+ - PP(top 10%) 21.8%: more than twice the world share.
69
+ - PP(top 1%) 3.2%: about three times the world share.
70
+
71
+ The percentile and the decile are per-paper columns. The summary does not average them; PP(top 10%) and PP(top 1%) are how the ranking shows up at the set level.
72
+
73
+ The world values, measured on the 2014–2023 papers: MNCS 1.00, PP(top 10%) 10.27%, PP(top 1%) 1.01%. The top-10% share is slightly above 10 because papers tied at the threshold are all counted in.
74
+
75
+ ## Full and fractional counting
76
+
77
+ By default every paper counts once, whatever the number of institutions on it. This is **full counting**, and it is what the EPFL numbers above use.
78
+
79
+ With **fractional counting**, a paper counts as `1 / number of institutions`. A paper written with nine other institutions counts as a tenth of a paper for EPFL. Use it when you compare institutions and do not want a shared paper to be counted in full by everyone who shares it.
80
+
81
+ ```python
82
+ oam.aggregate(ids, counting="full") # the default
83
+ oam.aggregate(ids, counting="frac")
84
+ ```
85
+
86
+ EPFL in 2014, fractional counting:
87
+
88
+ ```
89
+ publication_year P P_weighted TCS MCS MNCS PP_top10 PP_top1
90
+ 2014 4439 2118.3 108728 51.33 1.738 19.15 2.72
91
+ ```
92
+
93
+ `P` is still 4,439 papers. `P_weighted` is the sum of the fractions, 2,118. Every other column is a weighted average, so papers shared with many institutions weigh less. Here MNCS falls from 1.96 to 1.74, because EPFL's papers with five or more institutions are also its most cited: their average `ncs` is 2.67, against 1.75 for the others.
94
+
95
+ ## Which papers are scored
96
+
97
+ EPFL's name is on 6,223 works from 2014, and 4,439 are scored. Two things remove the others.
98
+
99
+ **Scope.** Only papers inside the scope get a score: by default, English-language articles in a core journal or a conference proceedings, with an institutional affiliation, a subfield and a reference list, and not retracted or front matter. 4,511 of the 6,223 pass. The rest are preprints, datasets, book chapters and similar. Scope also decides which citations count: a citation is only counted when the citing paper is itself in scope. See [Scope](scope.md) for the rules, and [Querying](querying.md#which-papers-are-scored) to see which rule removed which paper.
100
+
101
+ **Small bins.** A bin with fewer than 500 papers has an unstable average. Its papers still get an `ncs`, with `bin_is_reliable` set to false, but `aggregate` leaves them out. 72 of the 4,511 are in such bins. To keep them, pass `reliable_only=False`.
102
+
103
+ ## Citation window
104
+
105
+ By default a paper's citations are everything it has received up to the snapshot. A paper from 2014 has had twelve years to collect citations and a paper from 2023 only three, although both are compared within their own year, so the scores stay comparable.
106
+
107
+ To count only the citations received within three years of publication, pass `window=3`. This is the usual choice when you compare recent years with older ones.
108
+
109
+ ```python
110
+ oam.aggregate(ids, window=3)
111
+ ```