Convert citations exported from RefWorks into
BibTeX (.bib) files, read BibTeX into an
R data frame, save the result as an
Excel spreadsheet, and run text analytics on
the abstracts: topic modeling, document similarity (including
duplicate finding), and trends over time. The parsing and the heavy
calculations are written in C++ (via Rcpp), so it is fast even on large
libraries.
# install.packages("remotes")
remotes::install_github("cownr10r/citelens")The C++ code is compiled on your machine when you install, so you need a C++ compiler:
xcode-select --install in Terminal)build-essential (or your
distribution’s equivalent)Convert and load references
| Function | Purpose |
|---|---|
refworks_to_xlsx(input, xlsx) |
One step: RefWorks export to an Excel file (optionally saves a
.bib too) |
convert_refworks(input, output) |
RefWorks export to BibTeX entries / a .bib file |
bib_to_df(input, xlsx = NULL) |
BibTeX file to a data frame (optionally also an Excel file) |
scholar_html_to_bib(input, output) |
A saved Google Scholar results page to BibTeX |
scholar_parse_html(input) |
A saved Google Scholar results page to a data frame |
fetch_abstracts(refs) |
Look up missing abstracts in OpenAlex (by DOI or title) |
Text analytics on abstracts (these work on the data
frame from bib_to_df())
| Function | Purpose |
|---|---|
topic_model(refs, k) |
Find k topics in the abstracts (LDA) |
topic_terms(), topic_summary(),
topic_documents() |
Top words per topic, a one-row-per-topic summary, and each abstract’s topic added to your table |
plot_topic_terms(),
plot_topic_prevalence() |
Charts of a topic model |
topic_k_search(refs, k = 2:8) |
Compare numbers of topics to help choose k |
doc_similarity(refs) |
Cosine similarity between every pair of abstracts |
similar_to(), find_duplicates(),
plot_similarity() |
Closest matches to one paper, likely duplicate entries, and a heatmap with the numbers in each box |
similarity_to_df(s, xlsx = ) |
The similarities as a data frame (matrix or pairs layout), optionally saved to Excel |
word_trends(refs, words) |
Share of abstracts mentioning a word, by year or group of years |
topic_trends(model, refs) |
Each topic’s share by year or group of years |
plot_word_trends(),
plot_topic_trends() |
Line charts of the trends |
citelens_stopwords() |
The list of ignored words (add your own with
stopwords =) |
convert_refworks() and refworks_to_xlsx()
accept both RefWorks Tagged Format (lines like
RT, A1, T1) and
RIS (lines like TY -,
AU -), and detect which one you gave them.
library(citelens)
# RefWorks / RIS export -> Excel in one step
refworks_to_xlsx("~/Downloads/export.ris", "~/Desktop/references.xlsx")
# ...and keep a BibTeX copy too
refworks_to_xlsx("~/Downloads/export.ris", "~/Desktop/references.xlsx",
bib = "~/Desktop/references.bib")
# RefWorks / RIS export -> BibTeX file
convert_refworks("~/Downloads/export.ris", "~/Desktop/references.bib")
# BibTeX file -> data frame (and optionally an Excel copy)
refs <- bib_to_df("~/Desktop/references.bib")
refs <- bib_to_df("~/Desktop/references.bib", xlsx = "~/Desktop/references.xlsx")Sample files are included so you can try it right away:
ris <- system.file("extdata", "sample_refworks.ris", package = "citelens")
cat(convert_refworks(ris)[1])
# 30 synthetic abstracts for trying the text analytics
refs <- bib_to_df(system.file("extdata", "sample_abstracts.bib", package = "citelens"))@article{tinto2006research,
author = {Tinto, Vincent},
title = {{Research and practice of student retention: What next?}},
journal = {Journal of College Student Retention: Research, Theory \& Practice},
year = 2006,
volume = {8},
number = {1},
pages = {1--19},
issn = {1521-0251},
doi = {10.2190/4NWK-STAC-2PLH-PJ3D},
url = {https://example.org/tinto2006},
keywords = {retention, persistence},
abstract = {This article reviews the last forty years of research ...}
}
bib_to_df() returns one row per entry.
bibtype and bibkey come first, then one column
for every BibTeX field in the file (author,
title, journal, year,
doi, …). Fields an entry does not have are NA,
and every column is character.
Save a Google Scholar results page from your browser (File > Save Page As, “Webpage, HTML Only”), then:
scholar_parse_html("~/Downloads/scholar_page1.html") # data frame of results
scholar_html_to_bib("~/Downloads/scholar_page1.html", "scholar.bib")
# a whole folder: every .html file in it is read and combined
scholar_parse_html("~/Documents/scholar_pages")
scholar_html_to_bib("~/Documents/scholar_pages", "scholar.bib") # repeated results are removed
# or a list of files; recursive = TRUE also reads subfolders
scholar_html_to_bib(c("page1.html", "page2.html"), "scholar.bib")
scholar_parse_html("~/Documents/scholar_pages", recursive = TRUE)Files in the folder that are not Scholar results pages are skipped
with a warning naming them. The source_file column shows
which file each result came from.
This reads only the saved file; it never contacts Google Scholar,
which blocks automated requests. The results list shows title, authors,
venue, year, publisher and link, but it shortens long author lists and
venue names with “…” and has no volume, pages, DOI or abstract.
Shortened entries get a note field in the BibTeX (and
authors_truncated / venue_truncated in the
data frame) so you can find them. For complete records use Scholar’s
Cite > BibTeX (or My library >
Export), which bib_to_df() reads directly, or its
RefWorks / RefMan exports, which
convert_refworks() reads.
Scholar’s results list has no abstracts (only short search snippets), so look them up in OpenAlex, a free scholarly index with an official API. OpenAlex now requires a free API key:
Create a free account at https://openalex.org.
Copy your key from https://openalex.org/settings/api.
Save it once in R (it is picked up automatically afterwards):
Sys.setenv(OPENALEX_API_KEY = "your-key") # this session
usethis::edit_r_environ() # permanently: add OPENALEX_API_KEY=your-keyres <- scholar_parse_html("~/Downloads/scholar_page1.html")
res <- fetch_abstracts(res) # or fetch_abstracts(res, api_key = "your-key")
topic_model(res, k = 4, id = "title") # then use the text analytics
# or straight into BibTeX, abstracts included
scholar_html_to_bib("scholar_page1.html", "scholar.bib", abstracts = TRUE)A free key allows 100,000 credits a day; a title lookup costs 10, so a few thousand references fit in a day. Without a key you get only about 100 credits a day. If OpenAlex says the allowance is used up, the function stops and keeps what it found; run it again on that result and it looks up only what is still missing.
Each title (or DOI) and your key are sent to OpenAlex. A match is
accepted only when the title is very close and the year agrees within a
year; otherwise the abstract stays NA rather than risk the
wrong paper. Many but not all works have an abstract in OpenAlex, so
expect some gaps. Check abstract_match and
abstract_source to spot-check results.
fetch_abstracts() also works on a bib_to_df()
table that has DOIs.
To see why some references have no abstract, look at
abstract_status:
table(res$abstract_status)
res[res$abstract_status == "title not matched", c("title", "closest_title", "closest_score")]
# if the closest titles are the right papers, accept looser matches (only missing ones are retried)
res <- fetch_abstracts(res, min_match = 0.75)
# "year differs": the title matched but the years are far apart (re-editions, online-first dates)
res[res$abstract_status == "year differs", c("title", "year", "closest_year")]
res <- fetch_abstracts(res, max_year_diff = 10)Statuses: found, no abstract on file
(OpenAlex has the paper but no abstract),
title not matched, year differs,
no results, lookup failed.
Find the main themes in the abstracts of a reference data frame (LDA, fitted in C++):
refs <- bib_to_df("references.bib")
m <- topic_model(refs, k = 5, seed = 1) # 5 topics
m # summary: prevalence, coherence, top words
topic_terms(m, n = 10) # top words per topic (data frame)
topic_summary(m) # one row per topic
refs2 <- topic_documents(m, refs) # your table + dominant_topic, dominant_prob, topic_1..k
plot_topic_terms(m) # bar charts of each topic's top words
plot_topic_prevalence(m) # how common each topic is
ks <- topic_k_search(refs, k = 2:8, seed = 1) # which k?
plot(ks)Tips: topic models work best with dozens of abstracts or more. Add
words that carry no meaning for your field with
stopwords = c("student", "college"), and use
max_doc_prop = 0.5 to drop words found in over half the
abstracts. A sample file lives at
system.file("extdata", "sample_abstracts.bib", package = "citelens")
(synthetic text, for trying things out).
Find which abstracts are most alike (cosine similarity of TF-IDF word weights):
s <- doc_similarity(refs) # compares every pair of abstracts
s # quick summary
similar_to(s, "smith2019retention", n = 5, data = refs) # closest matches to one paper
find_duplicates(s, cutoff = 0.9, data = refs) # likely duplicate entries
plot_similarity(s) # heatmap, similar abstracts grouped togetherGet the numbers as a data frame, and optionally save them to Excel:
similarity_to_df(s, data = refs) # matrix: one row and column per reference
similarity_to_df(s, "pairs", data = refs) # pairs: one row per pair, most similar first
similarity_to_df(s, "pairs", cutoff = 0.5) # only pairs at or above 0.5
similarity_to_df(s, data = refs, xlsx = "~/Desktop/similarity.xlsx") # Excel, both layouts
as.data.frame(s) # same as the pairs layoutThe Excel workbook has two sheets, similarity_matrix and
pairs, whichever layout you asked for. Similarities are
rounded to 3 decimals (digits = NULL for no rounding).
Similarity reflects shared vocabulary, not meaning. Use
stopwords = to drop words common to your whole
collection.
See whether words or topics rise and fall across publication years:
w <- word_trends(refs, words = c("equity", "belonging", "enrollment"), bin = 5)
w # share of abstracts mentioning each word, per period
plot_word_trends(w)
m <- topic_model(refs, k = 5, seed = 1)
tt <- topic_trends(m, refs, bin = 5)
plot_topic_trends(tt)bin groups years (5 = five-year periods). Check
n_period, the number of abstracts in each period: periods
with only a few abstracts give jumpy shares.
tinto2006research), with
a letter added to keep duplicates unique.article, book chapter to
incollection, thesis to phdthesis, and so on).
Anything unrecognised becomes misc.bib_to_df()
removes grouping braces ({{Title}} becomes
Title), unescapes \&, \%,
\$, \#, \_, and collapses extra
whitespace. Use clean = FALSE for the raw values. Accent
commands such as {\'e} are left as LaTeX.abstract column
(and a year column for the trend functions). If your column
has another name, pass it with text = "..." or
year = "...". References with no abstract are skipped, and
you are told how many.@string macros, month names, # concatenation,
{...}, "..." and (...)
delimiters; @comment and @preamble blocks are
ignored.devtools::load_all() # compile and load
devtools::test() # run the tests
devtools::check() # full package checkMIT