R code
library(arrow)
library(dplyr)
library(jsonlite)
fmt <- function(x) format(x, big.mark = ",", scientific = FALSE, trim = TRUE)
journals <- read_parquet(params$data_file)
crossref <- readRDS(params$crossref_cache)Meeting update · 15 September 2026
library(arrow)
library(dplyr)
library(jsonlite)
fmt <- function(x) format(x, big.mark = ",", scientific = FALSE, trim = TRUE)
journals <- read_parquet(params$data_file)
crossref <- readRDS(params$crossref_cache)We use the existing PKP V7 list enriched with OpenAlex and Crossref: 98,273 records, including 72,084 with a usable ISSN. The saved Crossref data were collected on 2026-08-03. The results below are calculated from that merged file when this report runs.
Agreed task: Compare every Crossref source field with related OpenAlex fields, using actual values from the same journal.
example <- journals |>
filter(openalex_id == "https://openalex.org/S4210240683",
openalex_match_status == "unique",
crossref_match_status == "unique")
stopifnot(nrow(example) == 1,
example$crossref_title == example$openalex_display_name,
setequal(fromJSON(example$crossref_ISSN),
fromJSON(example$openalex_issn)))The example throughout both tables is International Journal of Innovative Research and Scientific Studies (ISSNs 2790-7600 and 2617-6548). All 11 top-level fields from the saved Crossref /journals response are shown below. Nested fields contain further entries. Crossref’s article-level /works endpoint provides additional information such as authors, abstracts and references; those article records are not part of this journal table. The examples are read from this one row of the merged file. Long nested fields show selected entries; Full saved value opens the complete content. NA means the corresponding field exists but has no value for this journal.
field_map <- tibble::tribble(
~cr, ~cr_meaning, ~oa, ~oa_meaning,
"title", "Journal name.",
"display_name", "Journal name.",
"publisher", "Publisher name.",
"host_organization_name", "Publisher or hosting organisation name.",
"ISSN", "Journal ISSNs.",
"issn", "Journal ISSNs, without print/online labels.",
"issn-type", "ISSNs with print or electronic labels.",
"issn", "Same identifiers; no medium labels in this list.",
"counts", "DOI counts: total, current and backfile publications.",
"works_count", "Number of works recorded by OpenAlex.",
"breakdowns", "DOI counts by publication (issued) year.",
"counts_by_year", "Works published and citations received in each year.",
"subjects", "Legacy subject codes, retired by Crossref in April 2024.",
"topics", "OpenAlex topic labels and associated work counts.",
"coverage", "Share of records containing each kind of metadata, for current and backfile publications.",
"", "",
"coverage-type", "The same coverage measures grouped by all, current and backfile publications.",
"", "",
"flags", "Yes/no indicators for deposited content and metadata.",
"", "",
"last-status-check-time", "Crossref status-check timestamp.",
"", ""
)
cr_fields <- field_map$cr
available_cr_fields <- sort(unique(unlist(lapply(crossref$journals, names))))
stopifnot(setequal(cr_fields, available_cr_fields),
all(paste0("crossref_", cr_fields) %in% names(example)),
all(paste0("openalex_", field_map$oa[field_map$oa != ""]) %in%
names(example)))doi_counts <- fromJSON(example$crossref_counts)
cr_years <- fromJSON(example$crossref_breakdowns)[["dois-by-issued-year"]]
oa_years <- fromJSON(example$openalex_counts_by_year)
oa_topics <- fromJSON(example$openalex_topics)
cr_coverage <- fromJSON(example$crossref_coverage)
coverage <- fromJSON(example[["crossref_coverage-type"]])$all
cr_flags <- fromJSON(example$crossref_flags)
# Use the same year for the DOI and work counts.
comparison_year <- 2025
cr_year_count <- cr_years[cr_years[, 1] == comparison_year, 2]
oa_year <- oa_years[oa_years$year == comparison_year, ]
stopifnot(length(cr_year_count) == 1, nrow(oa_year) == 1)
# These excerpts are computed from the saved values; the full values remain available.
excerpts <- list(
crossref_breakdowns = list(`dois-by-issued-year` =
list(c(comparison_year, cr_year_count))),
openalex_counts_by_year = oa_year,
openalex_topics = head(oa_topics[c("display_name", "count")], 3),
crossref_coverage = cr_coverage[c("abstracts-current", "references-current",
"orcids-current")],
`crossref_coverage-type` = list(all = coverage[c("abstracts", "references",
"orcids")]),
crossref_flags = cr_flags[c("deposits-abstracts-current",
"deposits-references-current",
"deposits-orcids-current")]
)field_cell <- function(prefix, field, meaning) {
if (field == "") return("")
paste0("<code>", prefix, field, "</code><br>",
htmltools::htmlEscape(meaning))
}
value_cell <- function(column) {
if (column == "openalex_") return("")
value <- example[[column]][[1]]
if (is.na(value)) return("<code>NA</code> (not supplied)")
value <- as.character(value)
full <- if (jsonlite::validate(value)) prettify(value, indent = 2) else value
full <- paste0("<pre class='field-value'>",
htmltools::htmlEscape(full), "</pre>")
if (is.null(excerpts[[column]])) return(full)
preview <- toJSON(excerpts[[column]], auto_unbox = TRUE, pretty = TRUE)
paste0("<pre class='field-value'>", htmltools::htmlEscape(preview),
"</pre><details><summary>Full saved value</summary>",
full, "</details>")
}
comparison <- data.frame(
`Crossref column and meaning` = mapply(field_cell, "crossref_",
field_map$cr, field_map$cr_meaning),
`Crossref example` = vapply(paste0("crossref_", field_map$cr),
value_cell, character(1)),
`OpenAlex column and meaning` = mapply(field_cell, "openalex_",
field_map$oa, field_map$oa_meaning),
`OpenAlex example` = vapply(paste0("openalex_", field_map$oa),
value_cell, character(1)),
check.names = FALSE
)The OpenAlex cells are left blank where no corresponding source field exists.
knitr::kable(comparison[field_map$oa == "", ], format = "html", escape = FALSE,
row.names = FALSE)| Crossref column and meaning | Crossref example | OpenAlex column and meaning | OpenAlex example |
|---|---|---|---|
crossref_coverageShare of records containing each kind of metadata, for current and backfile publications. |
Full saved value
|
||
crossref_coverage-typeThe same coverage measures grouped by all, current and backfile publications. |
Full saved value
|
||
crossref_flagsYes/no indicators for deposited content and metadata. |
Full saved value
|
||
crossref_last-status-check-timeCrossref status-check timestamp. |
|
The title and both ISSNs agree. Crossref adds the print/electronic labels and the publisher name, Innovative Research Publishing, which is missing from the related OpenAlex field for this journal. For subject information, Crossref supplies an empty list, while OpenAlex supplies topic labels.
Both sources describe publication volume, but count their own records. Crossref has 2,551 DOI registrations and OpenAlex has 2,526 works. In 2025, the corresponding figures are 2,055 and 2,030. These figures show a difference between the databases; identifying the individual publications behind it would require a publication-level comparison. Crossref’s current category covers the snapshot year and the preceding two years of publication; backfile covers earlier publications.
Crossref’s main additional information concerns what publishers deposit. For this journal, across all years, 100% of Crossref records contain abstracts, 0% contain references and 73.7% contain ORCIDs. The zero for references means references were not deposited with Crossref; it does not mean the papers contain no references. coverage, coverage-type and flags express related deposit information as proportions or yes/no indicators. The status-check timestamp is a record of Crossref’s check, not an additional characteristic of the journal’s research.
Field meanings follow the Crossref journal schema, Crossref coverage definitions and OpenAlex Source fields.
Agreed task: Find records with Crossref information but no OpenAlex match, and show their number and examples.
We select records with one Crossref match and no OpenAlex match in the saved ISSN search. Records with several possible OpenAlex matches are counted separately.
crossref_only <- journals |>
filter(crossref_match_status == "unique",
openalex_match_status == "unmatched")
n_uncertain <- sum(journals$crossref_match_status == "unique" &
journals$openalex_match_status == "ambiguous")
examples <- crossref_only |>
transmute(Journal = crossref_title,
ISSN = gsub("|", ", ", crossref_matched_issns, fixed = TRUE),
`Registered DOIs` = vapply(crossref_counts,
function(x) as.numeric(fromJSON(x)$`total-dois`), numeric(1)))
knitr::kable(examples, format.args = list(big.mark = ","))| Journal | ISSN | Registered DOIs |
|---|---|---|
| Usrotuna Journal of Islamic Family Law | 3089-1272 | 5 |
| Saqifah Jurnal Hukum Ekonomi Syariah | 2548-4974 | 32 |
| JBMP Jurnal Bhakti Muhammadiyah Papua | 3124-1522 | 20 |
| Research Journal of Human and Social Aspects | 3006-9696, 3006-970X | 7 |
| Collection of Scientific Papers | 2720-8257 | 25 |
| Stout in Agriculture and Animal Science | 2979-2835 | 2 |
| African Journal of Multidisciplinary Research and Reviews | 3142-5917, 3142-5925 | 1 |
Crossref adds a match where OpenAlex found none for 7 of the 72,084 records with a usable ISSN (0.010%). Its additional reach is therefore small in this PKP list; its main contribution is the extra information it provides about records already found in both sources. Another 104 records have one Crossref match but several possible OpenAlex matches. They need a separate identity check. The 26,189 records without a usable ISSN were not searched.
Agreed task: Check an actual empty-list example and show how it should be treated.
Here we inspect subjects, the Crossref subject-label field, for the first record listed above.
example_issn <- strsplit(crossref_only$crossref_matched_issns[1],
"|", fixed = TRUE)[[1]][1]
raw_example <- Filter(function(x) example_issn %in% x$ISSN,
crossref$journals)[[1]]
raw_example$subjectslist()
Crossref retired its old subject codes on 23 April 2024, leaving the fields empty for compatibility. The empty list is already present in the saved API response, before merging. We represent this unavailable classification as missing in the analysis:
raw_nonempty_subjects <- sum(vapply(crossref$journals,
function(journal) length(journal$subjects) > 0, logical(1)))
matched <- journals |> filter(crossref_match_status == "unique")
matched <- matched |>
mutate(subjects_clean = na_if(crossref_subjects, "[]"))
knitr::kable(data.frame(
`Subject information` = c("Supplied", "Missing"),
Records = c(sum(!is.na(matched$subjects_clean)),
sum(is.na(matched$subjects_clean))),
check.names = FALSE), format.args = list(big.mark = ","))| Subject information | Records |
|---|---|
| Supplied | 0 |
| Missing | 52,630 |
The original cache contains 168,907 journal records, of which 0 have nonempty subject labels. All 52,630 records with one Crossref match likewise have empty subject labels in the merged data. This is consistent with the retired Crossref classification, rather than evidence that the journals have no subject or that the merge removed their labels. These legacy codes came from a Scopus-based experiment; they were not subject labels deposited by publishers. Crossref’s explanation also makes clear that the empty fields are intentional. OpenAlex’s topics is a separate field and is populated in the example above.
Agreed task: Test whether a query now runs and identify the permission needed.
query_test <- system2(params$bq_cli, c(
"--headless=true", "--project_id=insyspo", "--location=US",
"--dataset_id=insyspo:userdb_yann_jyal", "query",
"--use_legacy_sql=false", "--maximum_bytes_billed=1",
shQuote("SELECT 1 AS access_test")), stdout = TRUE, stderr = TRUE)The SELECT 1 test still fails in insyspo; the account needs BigQuery Job User (roles/bigquery.jobUser), which includes bigquery.jobs.create.
Tasks follow the meeting discussion (02:13–08:29 and 34:23–34:31), alongside the field-review request.