library(anystyleR)
library(dplyr)
pdfs <- list.files(system.file("extdata", package = "anystyleR"), "\\.pdf$", full.names = TRUE)
references <- find_ref_to_df(pdfs)
references
#> # A tibble: 266 × 34
#> id_doc doc ref_order id_ref full_ref type author first_author n_authors authors et_al year
#> <int> <chr> <int> <chr> <chr> <chr> <list> <chr> <int> <chr> <lgl> <int>
#> 1 1 examp… 1 1_1 Arnold,… arti… <tibble> Arnold 1 Arnold… FALSE 2023
#> 2 1 examp… 2 1_2 Barro, … arti… <tibble> Barro 2 Barro,… FALSE 1983
#> 3 1 examp… 3 1_3 Béland,… arti… <tibble> Béland 1 Béland… FALSE 2009
#> 4 1 examp… 4 1_4 Berger,… arti… <tibble> Berger 2 Berger… FALSE 1999
#> 5 1 examp… 5 1_5 Best, J… arti… <tibble> Best 1 Best, … FALSE 2018
#> 6 1 examp… 6 1_6 Blei, D… arti… <tibble> Blei 3 Blei, … FALSE 2003
#> 7 1 examp… 7 1_7 Braun, … arti… <tibble> Braun 3 Braun,… FALSE 2022
#> 8 1 examp… 8 1_8 Carpent… book <tibble> Carpenter 1 Carpen… FALSE 2010
#> 9 1 examp… 9 1_9 Constân… pape… <tibble> Constâncio 1 Constâ… FALSE 2016
#> 10 1 examp… 10 1_10 Constân… pape… <tibble> Constâncio 1 Constâ… FALSE 2017
#> # ℹ 256 more rows
#> # ℹ 22 more variables: year_suffix <chr>, date <chr>, title <chr>, container_title <chr>,
#> # volume <chr>, issue <chr>, number <chr>, pages <chr>, first_page <chr>, last_page <chr>,
#> # publisher <chr>, location <chr>, edition <chr>, editors <chr>, doi <chr>, url <chr>,
#> # genre <chr>, note <chr>, collection_title <chr>, other_title <chr>, other_date <chr>,
#> # flag <chr>Many research questions about science and expertise start from bibliographies. Which economists do central bankers cite in their speeches, and did it change after the 2008 crisis? Which literature do international organisations mobilise in their reports? Which books and working papers, invisible in citation indexes, structured a field before its journals were indexed? Answering them requires turning printed reference lists into a table: one row per reference, with its authors, year, title and journal, so that references can be counted, matched across documents and linked into citation networks.
This guide walks through that path with the three example articles shipped with the package. It explains what each step does, what can go wrong, and how to check the result.
Three steps, three kinds of errors
anystyle (Keil, n.d.) and anystyleR work in three steps, and each produces its own kind of error:
-
Finding the references (
anystyle find): anystyle reads the text of the document line by line and labels each line (title, text, reference, footnote…). Consecutive reference lines are then grouped into references. Errors: a missed bibliography, a reference cut in two, two references merged, a line of text taken for a reference. -
Parsing each reference (
anystyle parse): each word of the reference gets a label (author, date, title, journal, volume, pages…). Errors: a title cut in two, a journal name taken for a publisher, initials taken for a family name. -
Cleaning (
clean_ref()): anystyleR flattens the fields and corrects the errors that follow recurring patterns, recording each correction in aflagcolumn.
Both anystyle steps rely on conditional random fields (Lafferty et al. 2001), a model that labels a sequence (of lines, or of words) by taking into account the labels of the neighbours, implemented by wapiti (Lavergne et al. 2010). The models were trained on reference lists of journal articles: they are good on recent author-date bibliographies, less good on footnote-style references, grey literature and languages other than English. The article on parsing quality gives measures.
Finding the references of PDFs
find_ref_to_df() runs both anystyle steps and the cleaning. With several documents, anystyle reads each PDF in turn, then parses all the references in one go, which is much faster than one run per document.
Each row is a reference. full_ref is the reference as anystyleR gave it to the parser, and should be your first check: the identifiers say where it comes from (id_doc, doc, ref_order and id_ref).
references |> count(doc)
#> # A tibble: 3 × 2
#> doc n
#> <chr> <int>
#> 1 example_doc_1.pdf 81
#> 2 example_doc_2.pdf 88
#> 3 example_doc_3.pdf 97The three documents have between 81 and 97 references, which you can compare with the PDFs. When the count is far from what you expect, look at what the finder saw with find_ref(format = "ttx"), which prints each line of the document with its label. Here, the lines around the start of the first bibliography:
lines <- strsplit(find_ref(pdfs[1], format = "ttx"), "\n")[[1]]
first_ref <- which(startsWith(lines, "ref"))[1]
cat(lines[(first_ref - 6):(first_ref + 5)], sep = "\n")
#> meta | 20
#> text | of legitimacy, ECB policymakers might find themselves in situations of ‘Zugzwang’ central
#> | banking in the future.
#> blank |
#> |
#> title | References
#> ref | Arnold, M. (2023). ECB must do more to tackle inflation “Monster”, says christine lagarde’.
#> | Financial Times.
#> | Barro, R. J., & Gordon, D. B. (1983). Rules, discretion and reputation in a model of monetary
#> | policy’. Journal of Monetary Economics, 12(1), 101–121.
#> | https://doi.org/10.1016/0304-3932(83)90051-X.
#> | Béland, D. (2009). Ideas, institutions, and policy change’. Journal of European Public Policy,Two settings matter for PDFs. Documents in two columns are often read with their lines interleaved: try layout = FALSE. References outside a reference section (footnotes, for instance) are ignored unless solo = TRUE.
Reading the cleaned fields
The cleaned data frame has one column per field: first_author, authors, year, title, container_title (journal, book or series), volume, issue, number (of a working paper), pages, publisher, doi… The author column holds a table per reference, which you can unnest to get one row per author:
references |>
select(id_ref, author) |>
tidyr::unnest(author) |>
select(id_ref, author_order, family, given, literal)
#> # A tibble: 413 × 5
#> id_ref author_order family given literal
#> <chr> <int> <chr> <chr> <chr>
#> 1 1_1 1 Arnold M. <NA>
#> 2 1_2 1 Barro R.J. <NA>
#> 3 1_2 2 Gordon D.B. <NA>
#> 4 1_3 1 Béland D. <NA>
#> 5 1_4 1 Berger H. <NA>
#> 6 1_4 2 Haan J. <NA>
#> 7 1_5 1 Best J. <NA>
#> 8 1_6 1 Blei D.M. <NA>
#> 9 1_6 2 Ng A.Y. <NA>
#> 10 1_6 3 Jordan M.I. <NA>
#> # ℹ 403 more rowsliteral holds the names of institutions (“Bank of England”, “ECB”), which have no given name.
The flag column lists the cleaning rules that changed the reference, and the problems they met:
references |>
tidyr::separate_longer_delim(flag, "; ") |>
filter(!is.na(flag)) |>
count(flag, sort = TRUE)
#> # A tibble: 11 × 2
#> flag n
#> <chr> <int>
#> 1 report_series 14
#> 2 institutional_author 10
#> 3 speech 6
#> 4 editor_as_first_author 3
#> 5 last_page_expanded 3
#> 6 several_container_title 1
#> 7 several_issue 1
#> 8 several_pages 1
#> 9 several_url 1
#> 10 several_volume 1
#> 11 year_from_full_ref 1Flags tell you where to look. not_a_reference marks lines without the elements of a reference (often a heading or a fragment of a reference cut by the finder); several_container_title and other_title mark references where anystyle found two titles or two journals, often a sign that it cut a title; institutional_author and family_given_swapped mark corrected names. The list of rules is in ?clean_ref.
Parsing references you already have
Reference lists often come as text: copied from a document, exported from a database, or extracted from PDFs by another pipeline. parse_ref_to_df() parses them; the doc argument says which document each reference belongs to, so that references are numbered within documents and the rules that look at the previous reference (ditto marks such as “———” for “same author”) stay within a document.
speech_refs <- tibble(
speech = c("speech_a", "speech_a", "speech_a", "speech_b"),
reference = c(
"Bernanke, B. 2008. “Housing, Mortgage Markets, and Foreclosures.” Remarks delivered at the Federal Reserve System Conference on Housing and Mortgage Markets, December 4.",
"Committee on the Global Financial System (2012), ‘Improving the BIS international banking statistics’, CGFS Paper No. 47, November.",
"Rey, H. (2015). Dilemma not trilemma: The global financial cycle and monetary policy independence. NBER Working Paper 21162.",
"Kydland, F. and E. Prescott (1977). Rules rather than discretion: the inconsistency of optimal plans. Journal of Political Economy 85(3), 473-491."
)
)
parsed <- parse_ref_to_df(speech_refs$reference, doc = speech_refs$speech)
parsed |> select(doc, first_author, year, title, container_title, number, type)
#> # A tibble: 4 × 7
#> doc first_author year title container_title number type
#> <chr> <chr> <int> <chr> <chr> <chr> <chr>
#> 1 speech_a Bernanke 2008 Housing, Mor… Remarks delive… <NA> spee…
#> 2 speech_a Committee on the Global Financial System 2012 Improving th… CGFS Paper 47 repo…
#> 3 speech_a Rey 2015 Dilemma not … NBER Working P… 21162 repo…
#> 4 speech_b Kydland 1977 Rules rather… Journal of Pol… <NA> arti…The speech, the institutional report and the working paper are typed as such (speech, report), with the number of the working paper in number: anystyle alone typed them as journal articles or conference papers.
If you want anystyle’s raw output, use clean = FALSE: you get one list-column per field, with every value anystyle found. clean_ref() can be applied later.
From references to a citation dataset
The same work is often cited in many documents, written in different styles. To count citations, or to build co-citation and bibliographic coupling networks, references must be matched. A simple and transparent key combines the first author, the year and the beginning of the title, normalised:
normalise <- function(x) {
x <- stringi::stri_trans_general(x, "Latin-ASCII")
gsub("[^a-z0-9]", "", tolower(x))
}
references <- references |>
mutate(key = paste(normalise(first_author), year, substr(normalise(title), 1, 20), sep = "_"))
references |>
count(key, sort = TRUE) |>
filter(n > 1)
#> # A tibble: 5 × 2
#> key n
#> <chr> <int>
#> 1 barro_1983_rulesdiscretionandre 2
#> 2 dietsch_2018_docentralbanksservet 2
#> 3 draghi_2017_monetarypolicyandthe 2
#> 4 kydland_1977_rulesratherthandiscr 2
#> 5 lucas_1972_expectationsandthene 2These references are cited by more than one of the example articles. On a real corpus, such a key misses references whose title was cut differently, and the threshold of the match should be checked on a sample. Matching on DOIs, when present, is safer, and matching with Crossref or OpenAlex is planned. The citation table can then go to biblionetwork to build co-citation or coupling networks, and to networkflow to follow clusters over time.
Exporting a bibliography
anystyle can write BibTeX, CSL-JSON or its tagged XML directly:
bib <- parse_ref(speech_refs$reference[4], format = "bib")
cat(bib)
#> @article{kydland1977a,
#> author = {Kydland, F. and Prescott, E.},
#> date = {1977},
#> title = {Rules rather than discretion: the inconsistency of optimal plans},
#> volume = {85},
#> pages = {473–491},
#> journal = {Journal of Political Economy},
#> number = {3}
#> }and find_ref_to_word_bibliography() formats the references of a document as a Word bibliography in any citation style (a CSL file from the Zotero style repository), which is handy to recover the bibliography of an old text or to restyle a list for a manuscript.
Limits
-
Flattened text. The finder needs line breaks: a bibliography whose text was flattened into one paragraph (OCR output, some databases) is not found.
split_ref()cuts such a block into references, andparse_ref_to_df(split = TRUE)does it before parsing (see how good is the parsing? for its precision). -
Footnote citations. In footnote styles, references are spread in the notes, abbreviated after the first citation (“Keynes, General Theory, p. 12”, “op. cit.”, “ibid.”). anystyle finds them with
solo = TRUE, but the parsing is much less reliable. - Scanned documents. The PDF must have a text layer. OCR errors propagate to every field.
- Languages. The shipped model was trained mostly on English references; check French, German or Spanish references more closely.
