Práctica del lenguaje R: uso de rWCVP para hacer coincidir nombres en WCVP

cargar biblioteca

La Lista mundial de plantas vasculares proporciona una visión de consenso global de todas las especies de plantas vasculares conocidas, incluido el estado taxonómico y los sinónimos. Por lo tanto, puede usarse para estandarizar y armonizar listas de nombres botánicos de otras fuentes.

Se ha implementado una función de coincidencia de nombres en rWCVP para que sea más fácil para los usuarios estandarizar las listas de nombres botánicos con WCVP.

En este artículo, le mostraremos un ejemplo de cómo la coincidencia de nombres en rWCVP encaja en un flujo de trabajo real.

Además de rWCVP, también usaremos una colección de paquetes tidyverse para la manipulación de datos, gt para formatear tablas y ggallaul para visualizar el proceso de coincidencia.

library(rWCVP)
library(tidyverse)
library(gt)
library(ggalluvial)

flujo de trabajo

1. Datos de muestra: Lista Roja de la UICN

El ejemplo que usamos aquí es para hacer coincidir las evaluaciones en la Lista Roja de Especies Amenazadas de la UICN (Lista Roja) con nombres reconocidos en WCVP.

Usaremos las descargas de evaluación de plantas de la edición 2022-1 de la Lista Roja

redlist = read_csv("D:/ALL_Softwares/R-4.2.0/library/rWCVP/extdata/redlist-summary_2022-1.csv",
                   col_types = cols(.default = col_character()))
glimpse(redlist)
Rows: 61,015
Columns: 3
$ scientific_name <chr> "Cotoneaster granatensis", "Juniperus drup…
$ authority       <chr> "Boiss.", "Labill.", "(Pancic) Purk.", "Bo…
$ category        <chr> "LR/cd", "LC", "EN", "VU", "LC", "EN", "VU…

Ahora que hemos cargado los nombres de la Lista Roja, podemos emparejarlos con WCVP usando la función wcvp_match_names .

Esta función toma un marco de datos de nombres para hacer coincidir, el nombre de la columna en el marco de datos que almacena los nombres y (opcionalmente) el nombre de la columna que almacena el autor de cada nombre, si se va a incluir en el proceso de emparejamiento.

La función primero intentará encontrar cualquier nombre que coincida exactamente con los nombres en WCVP. Si se proporciona una columna de autor, el primer paso incluirá al autor y el segundo se ejecutará para hacer coincidir completamente los nombres restantes sin la cadena de autor. Todos los nombres no coincidentes restantes luego pasan por un proceso de coincidencia aproximada que intenta hacer coincidir fonéticamente los nombres y luego encuentra los nombres más similares por distancia de Levenshtein.

matches = wcvp_match_names(redlist,
                           name_col = "scientific_name",
                           author_col = "authority",
                           fuzzy = TRUE,
                           progress_bar = FALSE)
── Matching names to WCVP ──────────────────────────────────────────
ℹ Using the `scientific_name` column                      
                                                          
── Exact matching 61015 names ──                          
                                                          
✔ Found 60165 of 61015 names                              
                                                          
── Fuzzy matching 850 names ──                            
                                                          
✔ Found 826 of 850 names                                  
                                                          
── Matching complete! ──                                  
                                                          
✔ Matched 60928 of 61015 names                            
ℹ Exact (with author): 43268                              
ℹ Exact (without author): 16897                           
ℹ Fuzzy (edit distance): 398                              
ℹ Fuzzy (phonetic): 365                                   
! Names with multiple matches: 391                        
Matching ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■  100% | ETA:  4s

No hemos pasado explícitamente un marco de datos de nombres de WCVP y recibimos advertencias porque estamos usando una versión obsoleta a través del paquete rWCVPdata. Podemos ignorar esta advertencia por ahora porque el paquete (y este tutorial) se desarrollaron utilizando una versión de los datos que actualmente no está disponible en el sitio web.

También recibimos una serie de mensajes que nos informaban la proporción de coincidencias exactas y cuánto tardaban las coincidencias parciales.

Una vez completado, tendremos un resumen completo de la cantidad de nombres que coincidieron, el número que usó la coincidencia aproximada y el número que coincidió con varios nombres en WCVP.

El resultado de wcvp_match_names es un conjunto de datos de nuestros nombres originales, con qué nombres en WCVP coincidieron e información sobre cómo coincidieron y qué tan cerca fueron las coincidencias aproximadas.

2. Resuelva el nombre coincidente con un nombre aceptado

Ahora que hemos hecho coincidir nuestros nombres, podemos resolver las coincidencias ambiguas y los nombres que coinciden con varias entradas en WCVP y asegurarnos de que las especies que estamos evaluando estén asociadas con nombres aceptados en WCVP.

La forma en que elija qué coincidencias aproximadas son válidas y cómo resuelva las coincidencias múltiples dependerá en última instancia de por qué está haciendo la coincidencia. Por ejemplo, hacemos coincidir las evaluaciones de la UICN con los nombres aceptados en WCVP. Para esta aplicación, la evaluación solo es válida para conceptos taxonómicos específicos, por lo que probablemente no nos importe analizar nada que coincida con un no sinónimo.

En este tutorial, primero tratamos de resolver tantas coincidencias ambiguas y múltiples como sea posible antes de filtrar las coincidencias que no son adecuadas para nuestra aplicación.

2.1 Coincidencia aproximada

No hay muchas coincidencias aproximadas (alrededor de 1000), pero todavía hay muchas para verificar manualmente, así que hagamos algunas comprobaciones previas con las siguientes reglas:

  1. Verifique manualmente todas las coincidencias con una similitud inferior al 90%
  2. Si la coincidencia parcial tiene la misma cadena de autor y la similitud es ≥90 %, la conservaremos.
  3. Si la coincidencia aproximada está a solo una letra de distancia (es decir, la distancia de edición es 1) y la similitud es ≥ 90 %, la mantenemos.
fuzzy_matchs = matches %>%
  filter(str_detect(match_type, "Fuzzy")) %>%
  mutate(
    keep = case_when(
      match_similarity < 0.9 ~ NA_real_,
      wcvp_author_edit_distance == 0~1,
      match_edit_distance == 1 ~ 1
    )
  )
table(fuzzy_matchs$keep, useNA = "always")

inserte la descripción de la imagen aquí

   1 <NA> 
 534  317 

Más de la mitad de las coincidencias aproximadas se pueden resolver sin mirar.
¿Qué tal una distancia de edición de 2, seguramente eso todavía está cerca?

fuzzy_matchs %>%
  filter(match_edit_distance == 2,
         !multiple_matches,
         match_similarity > 0.9
         ) %>%
  arrange(desc(wcvp_author_edit_distance)) %>% 
  select(scientific_name, authority, match_type, wcvp_name, wcvp_authors) %>%
  head()
# A tibble: 6 × 5
  scientific_name        authority match_type wcvp_name wcvp_authors
  <chr>                  <chr>     <chr>      <chr>     <chr>       
1 Diospyros tampolensis  H.N.Rako… Fuzzy (ed… Diospyro… H.Perrier   
2 Diospyros crassifolia  A.G.Lina… Fuzzy (ed… Diospyro… D.Don       
3 Diospyros nitidifolia  A.G.Lina… Fuzzy (ed… Diospyro… Elmer       
4 Garcinia eugeniaefolia Wall      Fuzzy (ph… Garcinia… (Choisy) Wa…
5 Hebepetalum humiriifo… (Planch.… Fuzzy (ed… Hebepeta… (Planch.) B…
6 Diospyros ambanjensis  G.E.Scha… Fuzzy (ed… Diospyro… Gürke   

¡Obviamente no! Si realmente quisiéramos proceder algorítmicamente, podríamos muestrear aleatoriamente 100 nombres y probar la precisión de varias reglas: consulte la información de apoyo en Nic Lughadha et al. (2020)
para ver un ejemplo del uso de la similitud.

En este punto, sin embargo, un vistazo rápido a cada nombre le dará los resultados más precisos y será más rápido.

write_csv(fuzzy_matchs, "redlist-fuzzy-tocheck.csv")

Me tomó menos de una hora revisarlos en Excel; marqué las buenas coincidencias con 1 y las malas con 0 en la columna de mantenimiento. Luego eliminé todos los datos coincidentes, excepto el tipo_de_coincidencia incorrecto y las coincidencias múltiples, y agregué una nueva columna tipo_de_coincidencia resuelto. Lo dejé en blanco para las buenas coincidencias y rellené "coincidencia aproximada rechazada" para las malas coincidencias.

fuzzy_checked = read_csv("D:/ALL_Softwares/R-4.2.0/library/rWCVP/extdata/redlist-fuzzy-checked.csv",
                         show_col_types = FALSE) %>%
  select(-keep) %>%
  mutate(resolved_match_type=ifelse(! is.na(resolved_match_type),
                                    resolved_match_type,
                                    match_type))
checked_matches = matches %>%
  filter(! str_detect(match_type, "Fuzzy")) %>%
  bind_rows(fuzzy_checked)

Hemos mantenido la misma cantidad de filas hasta ahora; no queremos eliminar las filas que no coinciden todavía.

2.2 Partidos múltiples

Ahora tenemos que manejar múltiples coincidencias. Nuevamente, podemos usar algunas reglas para resolver automáticamente estos problemas:

  1. Filtre las coincidencias utilizando la información del autor. Si una o más coincidencias tienen la misma cadena de autor, las mantendremos.
  2. Si se acepta una (y solo una) coincidencia, mantenemos esa coincidencia.
  3. Si una (y solo una) coincidencia es un sinónimo (a diferencia de inválido, ilegal, etc.), mantenemos esa coincidencia.

Escribiremos estas reglas en una función.

resolve_multi = function(df) {
    
    
  if (nrow(df) == 1) {
    
    
    return(df)
  }
  
  # some fuzzy matches are rejected from the previous section
  valid_matches = filter(df, !is.na(match_similarity))
  
  if (nrow(valid_matches) == 0) {
    
    
    return(head(df, 1))
  }
  
  matching_authors =
    valid_matches %>%
    filter(wcvp_author_edit_distance == 0 | ! sum(wcvp_author_edit_distance == 0,
                                                  na.rm=TRUE))
  
  if (nrow(matching_authors) == 1) {
    
    
    return(matching_authors)
  }
  
  accepted_names =
    matching_authors %>%
    filter(wcvp_status == "Accepted" | ! sum(wcvp_status == "Accepted"))
  
  if (nrow(accepted_names) == 1) {
    
    
    return(accepted_names)
  }
  
  synonym_codes = c("Synonym", "Orthographic", "Artificial Hybrid", "Unplaced")
  synonyms =
    accepted_names %>%
    filter(wcvp_status %in% synonym_codes | ! sum(wcvp_status %in% synonym_codes))
  
  if (nrow(synonyms) == 1)  {
    
    
    return(synonyms)
  }
  
  n_matches = length(unique(synonyms$wcvp_accepted_id)) / nrow(synonyms)
  final = head(synonyms, 1)
  
  if (n_matches != 1) {
    
    
    final =
      final %>%
      mutate(
        across(wcvp_id:resolved_match_type & where(is.numeric), ~NA_real_),
        across(wcvp_id:resolved_match_type & where(is.character), ~NA_character_),
        resolved_match_type="Could not resolve multiple matches"
      )
  }
  
  final
}

Ahora iteramos sobre cada nombre que tiene múltiples coincidencias y aplicamos las reglas.

auto_resolved =
  checked_matches %>%
  nest_by(scientific_name) %>%
  mutate(data=list(resolve_multi(data))) %>%
  unnest(col=data) %>%
  ungroup()

auto_resolved =
  auto_resolved %>%
  mutate(resolved_match_type=case_when(
    is.na(resolved_match_type) & is.na(match_type) ~ "No match found",
    is.na(resolved_match_type) ~ match_type,
    TRUE ~ resolved_match_type
  ))

count(auto_resolved, resolved_match_type)
# A tibble: 6 × 2
  resolved_match_type                    n
  <chr>                              <int>
1 Could not resolve multiple matches     6
2 Exact (with author)                43266
3 Exact (without author)             16893
4 Fuzzy (edit distance)                323
5 Fuzzy (phonetic)                     350
6 Fuzzy match rejected                  90

Todavía no pudimos encontrar una coincidencia para unos 90 nombres (menos del 1 % del conjunto de datos original). Esta es una pérdida aceptable para la mayoría de los conjuntos de datos, pero estos datos en particular se utilizarán en varios documentos, por lo que vale la pena analizarlos más de cerca.

auto_resolved %>%
  filter(resolved_match_type %in% c("No match found","Fuzzy match rejected")) %>%
  write_csv("redlist_tomanuallymatch.csv")
manually_resolved = read_csv("D:/ALL_Softwares/R-4.2.0/library/rWCVP/extdata/redlist-manually-matched.csv",
                              show_col_types=FALSE)
count(manually_resolved, resolved_match_type)
# A tibble: 3 × 2
  resolved_match_type         n
  <chr>                   <int>
1 Manually matched           14
2 New/undescribed species    55
3 No valid match found       21

En este ejemplo, muchos de los nombres que no coinciden son especies nuevas que no se agregaron a WCVP (a veces se enumeran como, por ejemplo, Heptapleurum sp.), que es más rápido que buscar cada nombre individualmente. Aún así, es un proceso lento con una tasa de éxito relativamente baja (aquí encontramos 95 nombres), por lo que no es adecuado para todos (o incluso para la mayoría) de los flujos de trabajo de coincidencia de nombres.

Necesitamos volver a ejecutar la coincidencia con los nombres coincidentes manualmente para obtener el resto de la información.

manually_resolved =
  manually_resolved %>%
  wcvp_match_names(name_col = "manually_entered_name", fuzzy=FALSE)
── Matching names to WCVP ───────────────────────────────────────────────
ℹ Using the `manually_entered_name` column                
! No author information supplied - matching on taxon name only
                                                          
── Exact matching  names ──                               
                                                          
✔ Found 13 of  names                                      
                                                          
── Matching complete! ──                                  
                                                          
✔ Matched 13 of 15 names                                  
ℹ Exact (without author): 13                              
ℹ No match found: 2                                       
! Names with multiple matches: 0                          
Matching ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■  100% | ETA:  3s

Esto nuevamente produjo varias coincidencias múltiples. Podemos evitar esto ingresando la ID de WCVP que queremos en lugar del nombre. Pero en este caso solo afecta a unos pocos registros. En cuanto a estos, los nombres adicionales no son legales, por lo que podemos filtrarlos rápidamente.

manually_resolved = filter(manually_resolved, wcvp_status != "Illegitimate")

Ahora los volvemos a insertar en los resultados coincidentes y eliminamos los que no coincidan.

resolved_matches =
  manually_resolved %>%
  select(-c(manually_entered_name, Notes, match_type, multiple_matches,
            match_similarity, match_edit_distance)) %>%
  rename(match_type=original_match_type) %>%
  bind_rows(
    auto_resolved %>%
      filter(! scientific_name %in% manually_resolved$scientific_name)
  )

Finalmente, veremos un resumen de la resolución de nombres en una tabla con un buen formato.

resolved_matches %>%
  count(resolved_match_type, sort=TRUE) %>%
  gt(rowname_col = "resolved_match_type") %>%
  tab_options(table_body.hlines.color = "transparent",
              column_labels.hidden = TRUE) %>%
  summary_rows(
    columns = c(n),
    fns = list(Total ="sum"),
    formatter = fmt_integer
  ) %>%
  tab_style(
    style = list(
      cell_text(align = "left")
    ),
    locations = cells_stub()
  ) %>%
  tab_header("Match summary", "(after resolution of fuzzy and multiple matches)") %>%
  opt_align_table_header(align = "left")

inserte la descripción de la imagen aquí

2.3 Asociación de evaluaciones con nombres aceptados

Hay un paso final antes de que podamos completar la coincidencia de nombres: vincular cada evaluación de la Lista Roja con un nombre de especie aceptable. Aquí es donde las cosas se ponen un poco complicadas: para los sinónimos, debemos considerar cómo el cambio de nombre afecta la evaluación. Específicamente, podemos usar nuevos nombres para la evaluación solo si se preserva la noción de clasificación.

Afortunadamente, podemos usar la columna homotypic_synonym de WCVP para filtrar fácilmente nuestras coincidencias.

Agregaremos el WCVP completo a nuestros datos, usando wcvp_accepted_id para unirnos con plant_name_id. Entonces solo queremos marcar nombres que coincidan con una especie aceptada o con un sinónimo homotípico vinculado a una especie aceptada.

accepted_matches = resolved_matches %>%
  left_join(rWCVPdata::wcvp_names, by=c("wcvp_accepted_id"="plant_name_id")) %>%
  mutate(keep=case_when(
    taxon_status == "Accepted" & (wcvp_status != "Synonym" | wcvp_homotypic) ~
      "Matched to an accepted name",
    TRUE ~ "Not matched to an accepted name"
  ))

count(accepted_matches, keep)
# A tibble: 2 × 2
  keep                                n
  <chr>                           <int>
1 Matched to an accepted name     59084
2 Not matched to an accepted name  1844

3. Visualiza el proceso de emparejamiento

Una cosa que podríamos querer hacer es ver cómo funciona el proceso de coincidencia, es decir, visualizar la proporción de nombres que experimentan cada tipo de coincidencia. Los mapas aluviales proporcionan una forma.

step1_codes = c("Exact (with author)"="Exact",
                 "Exact (without author)"="Exact",
                 "Fuzzy (edit distance)"="Fuzzy",
                 "Fuzzy (phonetic)"="Fuzzy")
step2_codes = c("Exact (without author)"="\U2713",
                 "Exact (with author)"="\U2713",
                 "Fuzzy (edit distance)"="\U2713",
                 "Fuzzy (phonetic)"="\U2713",
                 "Could not resolve multiple matches"="\U2716",
                 "Fuzzy match rejected"="\U2716",
                 "No match found"="\U2716")
plot_data =
  accepted_matches %>%
  mutate(step0="Input",
         step1=recode(match_type, !!! step1_codes),
         step2=recode(resolved_match_type, !!! step2_codes),
         step3=ifelse(keep == "Matched to an accepted name", "\U2713", "\U2716")) %>%
  replace_na(list(multiple_matches=FALSE)) %>%
  mutate(scenario=paste0(step1, step2, step3)) %>%
  count(step0, step1, step2, step3, scenario) %>%
  mutate(colour_key=step3) %>%
  to_lodes_form(axes=c(1:4), id="scenario") %>%
  group_by(x, stratum) %>%
  mutate(label=ifelse(row_number() == 1, as.character(stratum), NA_character_),
         total=sum(n)) %>%
  ungroup() %>%
  mutate(label=ifelse(total < 1500, NA_character_, label))


plot_data %>%
  ggplot(mapping=aes(x=x, y=n, stratum=stratum, alluvium=scenario, fill=colour_key)) +
  scale_fill_brewer(palette="Set2") +
  geom_flow(stat="alluvium", lode.guidance="frontback", color="darkgrey",
            aes.flow="forward") +
  geom_stratum() +
  geom_text(mapping=aes(label=label), vjust=0.75, size=4) +
  annotate("text", x=c(2, 3, 4), y=rep(61371 * 1.03, 3),
           label=c("Initial", "Resolved", "Accepted"), size=5) +
  guides(fill="none") +
  theme_void()

inserte la descripción de la imagen aquí
Obviamente, la gran mayoría de los nombres tienen una coincidencia exacta (con o sin la cadena de autor) y no se requiere análisis. Si los excluimos, haciendo un zoom efectivo en la parte inferior del gráfico, podemos ver más detalles en las coincidencias menos directas.

plot_data =
  accepted_matches %>%
  mutate(step1=str_replace(match_type, "\\(", "\n\\("),
         step2=str_replace(resolved_match_type, "\\(", "\n\\("),
         step3=ifelse(keep == "Matched to an accepted name", "\U2713", "\U2716")) %>%
  mutate(scenario=paste0(step1, step2, step3)) %>%
  count(step1, step2, step3, scenario) %>%
  mutate(colour_key=step3) %>%
  to_lodes_form(axes=c(1:3), id="scenario") %>%
  mutate(label1=ifelse(stratum %in% c("\U2713", "\U2716"),
                       as.character(stratum), NA_character_),
         label2=ifelse(! stratum %in% c("\U2713", "\U2716"),
                       as.character(stratum), NA_character_))


plot_data %>%
  #filter out the big categories
  filter(n < 4000) %>%
  ggplot(aes(x=x, y=n, stratum=stratum, alluvium=scenario, fill=colour_key)) +
  scale_fill_brewer(palette = "Set2") +
  geom_flow(stat="alluvium", lode.guidance="frontback",
            color="darkgray", aes.flow="forward") +
  geom_stratum() +
  theme_void()+
  geom_text(mapping=aes(label=label1), stat="stratum", size=8) +
  geom_text(mapping=aes(label=label2), stat = "stratum", size=4) +
  annotate("text", x=c(1, 2, 3), y=2950, label=c("Initial", "Resolved", "Accepted")) +
  annotate("text", x=3.4, y=1000,
           label="e.g. heterotypic \nsynonyms, \nunplaced names", size=4) +
  theme(legend.position = "none")

inserte la descripción de la imagen aquí

4. Obtenga el conjunto de datos final

En última instancia, queremos convertir nuestro gran marco de datos en algo más manejable filtrando todas las coincidencias fallidas y reduciendo la cantidad de columnas (y renombrándolas para que sea más intuitivo) para que podamos usar el análisis posterior.

final_matches =
  accepted_matches %>%
  filter(keep == "Matched to an accepted name") %>%
  select(scientific_name, authority, category,
         match_name=wcvp_name, match_status=wcvp_status,
         accepted_plant_name_id=wcvp_accepted_id, ipni_id,
         accepted_taxon_name=taxon_name, accepted_taxon_authors=taxon_authors)

glimpse(final_matches)
Rows: 59,084
Columns: 9
$ scientific_name        <chr> "Actinodaphne leiantha", "Camellia dongn…
$ authority              <chr> "Hook.f.", "Orel", "Nees", "Lapeyr.", "(…
$ category               <chr> "DD", "CR", "LC", "LC", "LC", "LC", "LC"…
$ match_name             <chr> "Actinodaphne leiophylla", "Camellia don…
$ match_status           <chr> "Accepted", "Accepted", "Accepted", "Acc…
$ accepted_plant_name_id <dbl> 2620909, 2694538, 225431, 228432, 278344…
$ ipni_id                <chr> "462276-1", "60443091-2", "298997-1", "3…
$ accepted_taxon_name    <chr> "Actinodaphne leiophylla", "Camellia don…
$ accepted_taxon_authors <chr> "(Kurz) Hook.f.", "Orel", "L.", "Lapeyr.…

Además, como ejemplo de análisis posterior, podríamos mostrar el número de especies aceptadas en cada categoría de la lista roja.

cat_order = c("DD", "LC or LR/lc", "NT or LR/nt", "LR/cd", "VU", "EN", "CR",
               "EW", "EX")
cat_colors = c("DD"="#D1D1C6", "LC or LR/lc"="#60C659", "NT or LR/nt"="#CCE226",
                "LR/cd"="#e4d354", "VU"="#F9E814", "EN"="#FC7F3F", "CR"="#D81E05",
                "EW"="#542344", "EX"="#000000")

final_matches %>%
  mutate(category=recode(category, "LC"="LC or LR/lc", "LR/lc"="LC or LR/lc",
                         "NT"="NT or LR/nt", "LR/nt"="NT or LR/nt"),
         category=factor(category, levels=cat_order, ordered=TRUE)) %>%
  count(category) %>%
  mutate(p=n / sum(n)) %>%
  ggplot(mapping=aes(x=category, y=n, fill=category)) +
  geom_col() +
  geom_text(mapping=aes(label=scales::percent_format(accuracy=0.1)(p)), vjust=-0.5) +
  scale_fill_manual(values=cat_colors) +
  guides(fill="none") +
  labs(x="", y="Species")

inserte la descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/whitedrogen/article/details/130896636
Recomendado
Clasificación