puremoe retrieves PubTator3 entity mentions and relation pairs with one call, then reshapes them locally into sentence context, entity co-occurrence counts, relation networks, and edge-level evidence. Everything after the initial fetch is a local transform on the retrieved tables and makes no further API calls.

Retrieve

We use a biomedical corpus with rich entity and relation annotation throughout.

pmids <- search_pubmed('"doxorubicin"[TiAb] AND "cardiotoxicity"[TiAb]')

pt <- get_records(head(pmids, 25L), endpoint = "pubtator", cores = 1L)

get_records(endpoint = "pubtator") returns a list with entities (one row per mention) and relations (one row per typed relation pair).

Entity mentions

pt$entities |>
  select(pmid, mention_index, type, text, tiab) |>
  head(12) |>
  DT::datatable(rownames = FALSE, options = list(scrollX = TRUE))

Relation tuples

pt$relations |>
  select(pmid, relation_type, ent1_type, ent2_type,
         ent1_mention_index, ent2_mention_index) |>
  head(10) |>
  DT::datatable(rownames = FALSE, options = list(scrollX = TRUE))

Sentence context

pubtator_context() preserves PubTator entity spans, adds sentence IDs and sentence-relative spans to entities, adds readable entity labels and sentence anchors to relations, and returns a sentence lookup table.

ctx <- pubtator_context(pt)

Entities with sentence spans

ctx$entities |>
  select(pmid, type, text, tiab, sentence_id, sentence_start, sentence_end) |>
  filter(!is.na(sentence_id)) |>
  head(12) |>
  DT::datatable(rownames = FALSE, options = list(scrollX = TRUE))

Relations with sentence anchors

ctx$relations |>
  select(pmid, relation_type, ent1_text, ent2_text,
         ent1_sentence_id, ent2_sentence_id,
         same_sentence, sentence_distance) |>
  head(10) |>
  DT::datatable(rownames = FALSE, options = list(scrollX = TRUE))

Sentence lookup

ctx$sentences |>
  mutate(sentence = if_else(
    nchar(sentence) > 90,
    paste0(substr(sentence, 1, 87), "..."),
    sentence
  )) |>
  head(10) |>
  DT::datatable(rownames = FALSE, options = list(scrollX = TRUE))

Co-occurrence counts

pubtator_cooccurrence() counts entity pairs within a sentence window. window = 0 counts co-mentions in the same sentence; larger windows reach across adjacent sentences within the same passage. Title and abstract sentences are never compared to each other. Set by = "entity" to resolve specific mentions; by = "type" aggregates by entity-type pair.

Co-occurrence by entity

pubtator_cooccurrence(ctx, window = 1, by = "entity") |>
  select(type_x, text_x, type_y, text_y, n, n_pmids) |>
  head(10) |>
  DT::datatable(rownames = FALSE, options = list(scrollX = TRUE))

Relation networks

pubtator_network() turns contextualized PubTator relations into a directed entity network plus a lean evidence table. Nodes are specific entities keyed on PubTator identifier when present, falling back to type:text. Edges are directed relation assertions aggregated across the corpus.

rel_net <- pubtator_network(ctx)

Network nodes

rel_net$nodes |>
  head(12) |>
  DT::datatable(rownames = FALSE, options = list(scrollX = TRUE))

Network edges

rel_net$edges |>
  head(12) |>
  DT::datatable(rownames = FALSE, options = list(scrollX = TRUE))

Relation evidence

evidence <- rel_net[["evidence"]]
if (is.null(evidence)) {
  evidence <- data.frame(
    from = character(), to = character(), relation_type = character(),
    pmid = character(), same_sentence = logical(), sentence = character()
  )
}

as.data.frame(evidence) |>
  dplyr::select(dplyr::any_of(c(
    "from", "to", "relation_type", "pmid", "same_sentence", "sentence"
  ))) |>
  dplyr::filter(.data$same_sentence %in% TRUE, !is.na(.data$sentence)) |>
  dplyr::slice_head(n = 15) |>
  DT::datatable(
    rownames = FALSE,
    options = list(scrollX = TRUE, pageLength = 5)
  )

Export to igraph

puremoe takes on no graphing dependency itself, but the graph tables pass straight to a graph package such as igraph:

igraph::graph_from_data_frame(rel_net$edges, vertices = rel_net$nodes)