---
title: "Research-management analytics: coverage, attribution, and policy evaluation"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Research-management analytics: coverage, attribution, and policy evaluation}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
resource_files:
- figures/logo.svg
- figures/logo-dark.svg
---
[](https://github.com/CTTIR/scimapR/actions/workflows/R-CMD-check.yaml)
[](https://cttir.github.io/scimapR/)
[](https://CRAN.R-project.org/package=scimapR)
[](https://app.codecov.io/gh/CTTIR/scimapR?branch=main)
[](https://cran.r-project.org/package=scimapR)
[](https://cran.r-project.org/package=scimapR)
[](https://opensource.org/licenses/MIT)
[](https://lifecycle.r-lib.org/articles/stages.html#experimental)
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.width = 6,
fig.height = 4,
message = FALSE
)
library(scimapR)
```
This article walks an end-to-end research-management study: auditing an
institution's publication output against a ground-truth tracker, capturing
affiliations robustly, evaluating whether an intervention changed output, and
summarising impact defensibly. Everything runs on synthetic example data.
## A reproducible starting corpus
For arbitrary tabular sources, `sm_corpus_from_tables()` is the recommended
ingestion path: it validates required columns, coerces types, fills missing
optional tables, and returns a valid `sm_corpus`.
```{r from-tables}
works <- data.frame(
work_id = paste0("W", 1:4),
title = c("Spatial transcriptomics in cancer",
"Immune checkpoint resistance",
"A trial of biomarker discovery",
"Single-cell atlas of the tumour"),
year = c("2018", "2019", "2020", "2021"), # character -> coerced to integer
doi = paste0("10.1234/example.", 1:4),
cited_by_count = c(40, 12, 5, 1)
)
corpus <- sm_corpus_from_tables(list(works = works))
corpus
```
For the rest of the article we use the larger built-in synthetic corpus.
```{r corpus}
corpus <- sm_example_corpus(n_works = 200, seed = 1)
```
## 1. Coverage auditing
Suppose a manual tracker lists the works the institution *should* have. We can
measure recall and precision of the corpus against it, and see which years are
worst covered.
```{r coverage}
reference <- corpus$works[1:150, c("work_id", "doi", "title", "year")]
names(reference)[1] <- "id"
cov <- sm_coverage_audit(corpus, reference, by = "year", match = "doi")
cov
summary(cov)
```
```{r coverage-plot}
ggplot2::autoplot(cov)
```
Breakdowns are returned as a single flat tibble; use `sm_coverage_breakdowns()`
to access or filter them:
```{r coverage-breakdowns}
sm_coverage_breakdowns(cov, dimension = "year")
```
Source coverage can be checked against a journal master list by ISSN
(`sm_journal_in_index()`), and two corpora reconciled by content with
`sm_reconcile()`.
```{r journal-index}
ref_index <- utils::read.csv(
system.file("extdata", "example_journal_index.csv", package = "scimapR"),
stringsAsFactors = FALSE
)
sm_journal_in_index(c("1078-8956", "9999-9999"), index = "doaj",
reference_list = ref_index)
```
Passing the same index to `sm_coverage_audit(index_table = )` assesses *record
capture* and *journal indexability* together -- "did we capture this paper" and
"is its journal indexed" in one pass:
```{r coverage-indexability}
cov_idx <- sm_coverage_audit(corpus, reference, match = "doi",
index_table = ref_index)
cov_idx$indexability
```
## 2. Affiliation capture and attribution
Hand-rolled affiliation regex is brittle. `sm_affiliation_match()` uses a
maintained, extensible dictionary (with multilingual variants and an
email-domain fallback), and `sm_attribute_institution()` rolls matches up to a
controlled vocabulary.
```{r affiliation}
corpus$authorships$raw_affiliation[1:3] <- c(
"Bundeswehrkrankenhaus Berlin",
"Charite - Universitatsmedizin Berlin",
"Walter Reed Army Institute of Research"
)
corpus <- sm_affiliation_match(corpus)
ror <- utils::read.csv(
system.file("extdata", "example_ror.csv", package = "scimapR"),
stringsAsFactors = FALSE
)
corpus <- sm_attribute_institution(corpus, vocabulary = "ror", ror_table = ror)
```
`match_signal` is a factor with an exported, stable level set
(`sm_affiliation_signals()`), so you can filter reliably --- for example to
keep only name-token matches and inspect the evidence that triggered them:
```{r affiliation-vocab}
sm_affiliation_signals()
summary_tbl <- sm_affiliation_summary(corpus)
subset(summary_tbl, match_signal == "name_token",
select = c(institution, match_signal, n_works, example_evidence))
```
The `example_evidence` column (and the per-authorship `match_evidence` column)
give an audit trail: which signal matched, on what string.
> **Stability.** Accessors like `sm_affiliation_summary()` and
> `sm_coverage_breakdowns(tidy = TRUE)` follow scimapR's accessor return-type
> contract (`?scimapR-stability`): their documented columns change only via a
> `lifecycle` deprecation, so pipelines built on them do not break across
> releases.
## 3. Policy evaluation
Did an intervention in 2020 change output? An interrupted time series fits a
level shift and slope change with a counterfactual.
```{r its}
its <- sm_its(corpus, intervention_year = 2020, outcome = "count")
its
ggplot2::autoplot(its)
```
For citation-based outcomes, citation-immature recent years are excluded
automatically (`sm_citation_maturity()` exposes the same flags on `works`).
A treated-vs-control comparison uses difference-in-differences. Here we tag two
illustrative institution groups, giving each work to one of them.
```{r did}
group <- rep(c("Treated Inst", "Control Inst"),
length.out = nrow(corpus$works))
corpus$authorships$institution_name <-
group[match(corpus$authorships$work_id, corpus$works$work_id)]
did <- sm_did(corpus, treated = "Treated Inst", control = "Control Inst",
intervention_year = 2020, outcome = "count")
did
```
A work co-authored by both groups belongs to both series and pulls them
together. `sm_did()` warns about such works and lists them in
`did$shared_works`; `shared = "exclude"` leaves them out of both series and
`shared = "lead"` gives each to the group of its lead author.
For `outcome = "leadership"`, `institutions` (in `sm_its()`) or
`institution_aware = TRUE` (in `sm_did()` and `sm_synth()`) count only the
works whose corresponding author, or the author in another `lead_role` such
as `"first"`, belongs to the focal institutions.
## 4. Counting and robust impact
Fractional counting attributes a multi-author paper's single unit of credit
across its contributors.
```{r counting}
head(sm_count(corpus, method = "fractional", level = "author"), 5)
```
Heavy-tailed metrics are summarised robustly with medians, bootstrap CIs, and
a citation percentile share. This example has no reference set: its
`pp_top10` equals `top_pct` by construction. Supply `top_reference` with
a comparable external distribution or OpenAlex flags for an informative share.
```{r robust}
sm_metric_summary(corpus, metric = "citations", seed = 1, n_boot = 500)
```
## 5. Reproducible reporting
Finally, `sm_figure_manifest()` turns a directory of exported figures into a
captions / alt-text manifest for the manuscript.
```{r manifest}
dir <- withr::local_tempdir()
gg <- ggplot2::autoplot(its)
ggplot2::ggsave(file.path(dir, "fig_its.png"), gg, width = 6, height = 4,
dpi = 150)
sm_figure_manifest(dir)
```
Together these functions cover the recurring tasks of a research-coverage and
impact study — audit, affiliation capture, policy evaluation, robust
summarisation, and reporting — on top of the reproducible `sm_corpus`
foundation.