Skip to contents

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:

  1. 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.
  2. 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.
  3. Cleaning (clean_ref()): anystyleR flattens the fields and corrects the errors that follow recurring patterns, recording each correction in a flag column.

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.

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>

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    97

The 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 rows

literal 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          1

Flags 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       2

These 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, and parse_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.

References

Keil, Sylvester. n.d. AnyStyle: Fast and Smart Citation Reference Parsing. Https://anystyle.io/.
Lafferty, John D., Andrew McCallum, and Fernando C. N. Pereira. 2001. “Conditional Random Fields: Probabilistic Models for Segmenting and Labeling Sequence Data.” Proceedings of the Eighteenth International Conference on Machine Learning (ICML 2001), 282–89.
Lavergne, Thomas, Olivier Cappé, and François Yvon. 2010. “Practical Very Large Scale CRFs.” Proceedings of the 48th Annual Meeting of the Association for Computational Linguistics, 504–13.