Wordle y palabras en castellano

¿Se puede resolver de forma «automática»?

Javier Álvarez Liébana https://dadosdelaplace.github.io (Universidad Complutense de Madrid)
2022-01-15

Wordle: el juego de moda

00:01 (hora local). Aterrizaje efectuado sin dificultad. Propulsión convencial (ampliada). Velocidad de aterrizaje: 6:30 de la escala convencional (restringida). Velocidad en el momento del amaraje: 4 de la escala Bajo-U 109 de la escala Molina-Calvo. Cubicaje: AZ-0.3. Denominación local del lugar de aterrizaje: Sardanyola.

Así empieza uno de mis libros favoritos, «Sin noticias de Gurb», en el que Eduardo Mendoza nos contaba la historia de un extraterrestre recién aterrizado en Barcelona, con el objetivo de encontrar a un compañero perdido. Y es que si tuviéramos que elaborar un método en estas primeras semanas de 2022 para detectar si una persona acaba de llegar del espacio exterior, no habría uno mejor que preguntarle: «¿has jugado a WORDLE

Wordle, el juego de moda: <https://www.powerlanguage.co.uk/wordle/>

Figure 1: Wordle, el juego de moda: https://www.powerlanguage.co.uk/wordle/

Este sencillo juego, que imita la dinámica del famoso Master Mind, no tiene muchas reglas pero es «adictivo»: una palabra, 5 letras, y 6 intentos para adivinar el vocablo mientras la aplicación te indica en cada paso que letras están bien colocadas (o mal colocadas o si directamente no aparecen en la palabra). No solo choca su sencillez sino que además es una web distinta a las que hoy nos tiene acostumbrados la red: sin pop-ups, sin anuncios, sin cookies, sin vídeos que se reproducen solos. Nada. Solo un juego, una interfaz sencilla (pero visualmente atractiva) que no reporta ningún beneficio a su creador, el ingenierio de software Josh Wardle. Un juego que, aunque ha alcanzado la categoría de fenómeno de masas a finales de 2021 y principios de 2022, nació además de una historia de amor, como relata el autor al periodista del New York Times Daniel Victor en esta entrevistahttps://www.nytimes.com/2022/01/03/technology/wordle-word-game-creator.html

Por si alguien llega a esta entrada sin conocer el juego, lo explicamos. El objetivo consiste en adivinar una palabra de 5 letras.

Wordle, el juego de moda: <https://www.powerlanguage.co.uk/wordle/>

Figure 2: Wordle, el juego de moda: https://www.powerlanguage.co.uk/wordle/

En cada intento el juego nos indica con amarillo las letras que están en la palabra (pero mal colocadas), en verde las letras que están en la palabra y correctamente colocadas, y en gris las letras que no están en la palabra. Con esas pistas, el usuario tiene 6 intentos y solo podrá jugar una palabra al día (quizás esa sea una de las claves de las ganas de seguir jugando).

Wordle, el juego de moda: <https://www.powerlanguage.co.uk/wordle/>

Figure 3: Wordle, el juego de moda: https://www.powerlanguage.co.uk/wordle/

Desde unas semanas el juego también cuenta con su versión en castellano, https://wordle.danielfrg.com/, adaptado por Daniel Rodríguez, y su versión en catalán, https://gelozp.com/games/wordle/, adaptada por Gerard López, y no han sido pocos los medios que han dedicado sus espacios a hablar de él.

Incluso no son pocos los matemáticos y estadísticos que se han lanzado a intentar analizar el juego, las opciones de ganar y la forma en la que juegan sus usuarios. Es el caso de Esteban Moro, a quién entrevistaban hace unos días en El País contando su estrategia para el juego en inglés, el caso del investigador y divulgador Picanúmeros o yo mismo.

El castellano y sus letras

Todo lo contenido en este documento está libremente disponible en GitHub https://github.com/dadosdelaplace/blog-R

Paquetes de R que vamos a necesitar

Además para la creación de este tutorial he usado {rmarkdown} con el paquete {distill}, el paquete {tweetrmd} para incrustar enlaces de Twitter, el paquete {DT} para las tablas interactivas y {ggtext} para fuentes en las gráficas. Si quieres empezar a programar en R desde cero tienes por aquí materiales gratuitos https://dadosdelaplace.github.io/courses

CREA (Corpus de Referencia del Español Actual)

Dado que se trata de un juego de adivinar palabras en castellano, lo primero que vamos a hacer es analizar (de forma muy de «andar por casa») cómo se comportan las palabras y letras en el castellano, así que necesitamos es un conjunto de palabras con las que trabajar.

Seguramente se pueda scrappear la web oficial del juego, en castellano https://wordle.danielfrg.com/, pero ando escaso de tiempo así que no he podido extraer el historial de palabras que se han jugado hasta ahora (si alguien se anima, todo suyo/a).

Extraer un listado de palabras de la RAE tampoco es sencillo ya que la propia institución no lo pone fácil, hasta el absurdo que su listado de palabras y definiciones no son de uso libre y tiene copyright, como ha comentado en varias ocasiones Jaime Gómez Obregón

Dichos impedimentos hacen incluso difícil saber el número de palabras totales en castellano que la RAE incluye en el diccionario. Según la propia institución:

«Es imposible saber el número de palabras de una lengua. La última edición del diccionario académico (2014), registraba 93 111 artículos y 195 439 acepciones

Lo que si pone la RAE a nuestra disposición es el Corpus de Referencia del Español Actual (CREA). El CREA es un «conjunto de textos de diversa procedencia, almacenados en soporte informático, del que es posible extraer información para estudiar las palabras, sus significados y sus contextos». El corpus de referencia de la RAE cuenta con 152 560 documentos analizados, producidos en los países de habla hispana desde 1975 hasta 2004 (sesgo de selección, parte I), y seleccionados tanto de libros como de periódicos y revistas (sesgo de selección, parte II), y lo tienes en bruto en mi repositorio. Para su lectura podemos usar read_delim() del paquete stringr (cargado en el entorno {tidyverse}).

# Corpus de Referencia del Español Actual (CREA)
# https://corpus.rae.es/lfrecuencias.html
datos_brutos_CREA <- # read
  read_delim(file = "./CREA_bruto.txt", delim = "\t")

Preprocesado

Dicho fichero lo he preprocesado para hacer más fácil su lectura. El archivo preprocesado lo tienes disponible en CREA_procesado.csv y el código que he ejecutado lo tienes debajo.

📝Código

# Eliminamos columna de orden y separamos última columna en dos
datos_CREA <-
  datos_brutos_CREA[, -1] %>%
  separate(col = 2, sep = "\t",
           into = c("frec_abs", "frec_norm"))

# Renombramos columnas
names(datos_CREA) <- c("palabra", "frec_abs", "frec_norm")

# Convertimos a número que vienen como cadenas de texto
datos_CREA <- datos_CREA %>%
  mutate(frec_abs = as.numeric(gsub(",", "", frec_abs)),
         frec_norm = as.numeric(frec_norm))

# convertimos tildes
datos_CREA <-
  datos_CREA %>%
  mutate(palabra = gsub(" ", "", iconv(palabra, "latin1")))

 

La carga desde el archivo ya preprocesado puede hacerse con read_csv().

# Archivo ya preprocesado
datos_CREA <- read_csv(file = "./CREA_procesado.csv")

Tras cargarlo, dado que en el juego en castellano no se admiten tildes, pero si la letra ñ, he decidido eliminar todas las tildes, acentos y diéresis del CREA y he eliminado duplicados (por ejemplo, mi y tras quitar tildes). Tienes debajo en 📝Código un resumen numérico y el código R.

📝Código
# Quitamos tildes pero no queremos eliminar la ñ
datos_CREA <- datos_CREA %>%
  mutate(palabra =
           gsub("ö", "o",
                gsub("ä", "a",
                     gsub("ò", "o",
                          gsub("ï", "i",
                               gsub("ô", "o",
                                    gsub("â", "a",
                                         gsub("ë", "e",
                                              gsub("ê", "e",
                                                   gsub("ã", "a",
                                                        gsub("î", "i",
                                                             gsub("ù", "u",
                                                                  gsub("¢", "c",
                                                                       gsub("ì", "i",
                                                                            gsub("è", "e",
                                                                                 gsub("à", "a", gsub("ç", "c",
           gsub("á", "a",
                gsub("é", "e",
                     gsub("í", "i",
                          gsub("ó", "o",
                               gsub("ú", "u",
                                    gsub("ü", "u",
                                         as.character(palabra)))))))))))))))))))))))) %>%
  # eliminamos duplicados
  distinct(palabra, .keep_all = TRUE) %>%
  # Eliminamos palabras con '
  filter(!grepl("'", palabra) & !grepl("ø", palabra))

datos_CREA %>% skim()
Table 1: Data summary
Name Piped data
Number of rows 693402
Number of columns 3
_______________________
Column type frequency:
character 1
numeric 2
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
palabra 0 1 1 30 0 693402 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
frec_abs 0 1 217.22 19637.76 1 1 2.00 9.00 9999518.00 ▇▁▁▁▁
frec_norm 0 1 1.42 128.72 0 0 0.01 0.05 65545.55 ▇▁▁▁▁

Tras este preprocesamiento nuestro corpus se compone aproximadamente de 700 000 palabras/vocablos, de las que tenemos su frecuencia absoluta frec_abs (nº de documentos analizados en los que aparece) y frecuencia normalizada frec_norm (veces que aparece por cada 1000 documentos).

datos_CREA
# A tibble: 693,402 × 3
   palabra frec_abs frec_norm
   <chr>      <dbl>     <dbl>
 1 de       9999518    65546.
 2 la       6277560    41149.
 3 que      4681839    30689.
 4 el       4569652    29953.
 5 en       4234281    27755.
 6 y        4180279    27401.
 7 a        3260939    21375.
 8 los      2618657    17165.
 9 se       2022514    13257.
10 del      1857225    12174.
# … with 693,392 more rows

Además, he calculado los siguientes parámetros de cada una de las palabras (tienes el código colapsado debajo) por si nos son de utilidad:

📝Código

datos_CREA <- datos_CREA %>%
  mutate(# frec. relativa
         frec_relativa = frec_abs / sum(frec_abs),
         # log(frec. absolutas)
         log_frec_abs = log(frec_abs), 
         # log(frec. normalizadas)
         log_frec_rel = log_frec_abs / sum(log_frec_abs),
         # distribución de frec_norm
         int_frec_norm =
           cut(frec_norm,
               breaks = c(-Inf, 0.01, 0.05, 0.1, 0.5, 1:5,
                          10, 20, 40, 60, 80, Inf)),
         # número de letras
         nletras = nchar(palabra))

Análisis numérico

¿Cómo se distribuyen las frecuencias de las palabras? Si nos fijamos en cómo se reparten las palabras y sus repeticiones a lo largo de los más de 150 000 documentos analizados, obtenemos que el 75% de los vocablos que contiene CREA aparecen, como mucho, en 5 de cada 100 000 documentos.

quantile(datos_CREA$frec_norm)
      0%      25%      50%      75%     100% 
    0.00     0.00     0.01     0.05 65545.55 
datos_CREA %>% skim()
Table 2: Data summary
Name Piped data
Number of rows 693402
Number of columns 8
_______________________
Column type frequency:
character 1
factor 1
numeric 6
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
palabra 0 1 1 30 0 693402 0

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
int_frec_norm 0 1 FALSE 15 (-I: 423578, (0.: 99155, (0.: 71980, (0.: 38683

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
frec_abs 0 1 217.22 19637.76 1 1 2.00 9.00 9999518.00 ▇▁▁▁▁
frec_norm 0 1 1.42 128.72 0 0 0.01 0.05 65545.55 ▇▁▁▁▁
frec_relativa 0 1 0.00 0.00 0 0 0.00 0.00 0.07 ▇▁▁▁▁
log_frec_abs 0 1 1.41 1.83 0 0 0.69 2.20 16.12 ▇▁▁▁▁
log_frec_rel 0 1 0.00 0.00 0 0 0.00 0.00 0.00 ▇▁▁▁▁
nletras 0 1 8.97 2.95 1 7 9.00 11.00 30.00 ▂▇▂▁▁

Es importante advertir que el CREA contiene aproximadamente 8 veces más vocablos que palabras hay registradas en la RAE (según la propia RAE). A diferencia de un diccionario, en CREA no solo hay palabras registradas oficialmente en castellano sino que recopila todo un conjunto de vocablos que aparecen en textos, que no siempre tienen porque estar «validadas» en los diccionarios, incluidos americanismos). Por ello, vamos a hacer un filtro inicial, eliminando aquellas palabras muy poco frecuentes, definiendo como poco frecuente toda aquella palabra que aparezca con una frecuencia inferior a 1 de cada 1000 textos analizados o más (aproximadamente 45 000 vocablos).

📝Código

datos_CREA_filtrado <- datos_CREA %>% filter(frec_norm >= 1)
datos_CREA_filtrado
# A tibble: 40,655 × 8
   palabra frec_abs frec_norm frec_relativa log_frec_abs log_frec_rel
   <chr>      <dbl>     <dbl>         <dbl>        <dbl>        <dbl>
 1 de       9999518    65546.        0.0664         16.1    0.0000164
 2 la       6277560    41149.        0.0417         15.7    0.0000160
 3 que      4681839    30689.        0.0311         15.4    0.0000157
 4 el       4569652    29953.        0.0303         15.3    0.0000156
 5 en       4234281    27755.        0.0281         15.3    0.0000156
 6 y        4180279    27401.        0.0278         15.2    0.0000155
 7 a        3260939    21375.        0.0217         15.0    0.0000153
 8 los      2618657    17165.        0.0174         14.8    0.0000151
 9 se       2022514    13257.        0.0134         14.5    0.0000148
10 del      1857225    12174.        0.0123         14.4    0.0000147
# … with 40,645 more rows, and 2 more variables: int_frec_norm <fct>,
#   nletras <int>

Tras dicho filtrado, he hecho una tabla con las 10 000 palabras más repetidas en frecuencia absoluta, y la tabla con las 500 palabras menos repetidas (pero que aparecen en 1 de cada 1000 documentos analizados, o más), por si quieres curiosear algunas de ellas escribiendo en el buscador.

Palabras más repetidas en CREA (las 12 000 primeras)

Palabras menos repetidas en CREA

Entre todas esas palabras que hemos obtenido quizás sea también relevante analizar la distribución de las letras: ¿de qué número de letras son las palabras más repetidas en castellano (según el corpus de la RAE)?

Frecuencia de repetición de las palabras según número de letras

Número de palabras en CREA según número de letras

Si combinamos la tabla y los gráficos tenemos:

📝Código

theme_set(theme_void())
theme_set(theme_minimal(base_size = 35, base_family = "Roboto"))

theme_update(
  text = element_text(color = "#787c7e"),
  axis.title = element_text(family = "Be Vietnam", size = 45,
                            color = "#787c7e"),
  axis.text.x = element_text(family = "Roboto", size = 23),
  axis.text.y = element_text(family = "Roboto", size = 23),
  panel.grid.major.y = element_blank(),
  panel.grid.minor = element_blank(),
  plot.title = element_text(family = "Be Vietnam", size = 140,
                            color = "black"),
  plot.subtitle = element_text(family = "Roboto", size = 41, lineheight = 0.5),
  plot.caption =
    element_text(family = "Roboto", color = "#6baa64",
                 face = "bold", size = 33)
)

# Marcamos las de 5 palabras
datos_CREA_filtrado <-
  datos_CREA_filtrado %>%
  mutate(candidata_wordle = nletras == 5)

# Número de palabras del CREA
n_palabras_CREA <- nrow(datos_CREA_filtrado)

ggplot(datos_CREA_filtrado,
       aes(x = nletras, fill = candidata_wordle)) +
  geom_bar(alpha = 0.9) +
  scale_fill_manual(values = c("#c9b458", "#6baa64"),
                     labels = c("5 letras", "Otras")) +
  guides(fill = "none") +
  labs(y = glue("Frec. absoluta ({n_palabras_CREA} palabras)"),
       x = "Número de letras",
       title = "WORDLE",
       subtitle =
       paste0("Distribución del nº letras de CREA. Se han eliminado las que\n",
              "aparecen en menos de 1 de cada 1000 (152 560 docs analizados)"),
       caption =
         paste0("Javier Álvarez Liébana (@dadosdelaplace) | Datos: CREA"))

ggplot(datos_CREA_filtrado,
       aes(x = nletras, y = frec_norm, color = frec_norm, size = frec_norm)) +
  geom_point(alpha = 0.8) + guides(color = "none", size = "none") +
  labs(y = "Frec. normalizada (por cada 1000 documentos)",
       x = "Número de letras",
       title = "WORDLE",
       subtitle = "Distribución del nº letras vs frecuencia normalizada de CREA",
       caption =
         paste0("Javier Álvarez Liébana (@dadosdelaplace) | Datos: CREA"))

Corpus WORDLE

En los datos anteriores se han incluido todas las palabras del CREA que superan cierto número de repeticiones en los documentos (al menos aparecer en 1 de cada 1000 documentos), y vemos como de 5 letras tan solo contamos con casi 4000 palabras (daría para jugar 10 años seguidos aproximadamente, aunque recuerda que NO significa que haya esa cantidad de palabras en la RAE, simplemente estamos analizando las palabras usadas de un conjunto amplío de textos), que representa aproximadamente el 9% de nuestro corpus.

El juego WORDLE se reduce a palabras de 5 letras: ¿cuáles son las palabras más repetidas en CREA de dicho tamaño?

Frecuencia de repetición en CREA de las palabras de 5 letras

Las 10 palabras de 5 letras más repetidas en CREA son: sobre, entre, había, hasta, desde, puede, todos, parte, tiene y donde/dónde. El creador del juego, como hemos mencionado anteriormente, dispone de un repositorio abierto en Github, conteniendo, entre otros archivos, el listado de las 620 palabras que ha considerado inicialmente para el juego. Dicho listado está ya descargado en palabras_wordle.csv SPOILER: no mires el archivo si vas a seguir jugando, el objetivo no es dejar de jugar sino analizar las opciones de ganar.

palabras_wordle <- read_csv(file = "./palabras_wordle.csv")
palabras_wordle
# A tibble: 620 × 1
   palabra
   <chr>  
 1 coche  
 2 nieve  
 3 hueso  
 4 titan  
 5 flujo  
 6 disco  
 7 razon  
 8 hongo  
 9 jaula  
10 atril  
# … with 610 more rows

Nuestro corpus tiene limitaciones, en particular un sesgo de selección ya que analiza textos de un periodo concreto, por lo que palabras más usadas en los últimos años quizás no aparezcan con tanta frecuencia en dichos documentos (como kefir o tesla), amén de que pueden haber sido incluidas por el autor de la aplicación libremente.

Las palabras en el WORDLE pero no estén incluidas en el filtro de frecuencia realizado vamos a buscarlas en el corpus original, e incluiremos dichas palabras con sus frecuencias en nuestros corpus filtrado.

📝Código

setdiff(palabras_wordle %>% pull(palabra),
        datos_CREA_filtrado %>% filter(nletras == 5) %>%
          pull(palabra))
 [1] "cotar" "jurco" "sushi" "lituo" "albur" "koala" "licra" "minal"
 [9] "sauco" "mimar" "boxer" "koine" "liana" "cagon" "foton" "bollo"
[17] "kefir" "fauno" "podar" "rimar" "cutre" "sarza" "gamba" "zombi"
[25] "anime" "salmo" "titar" "zebra" "pulga" "ladra" "erizo" "finta"
[33] "tesla" "leñar" "flama" "raspa" "kilim" "misio" "bucle" "kopek"
[41] "sarro" "labia" "lemur" "dueto" "oruga" "cague" "arete" "arpon"
[49] "catar" "kurdo" "rotar" "rasta" "rublo" "rayar" "abano" "ostia"
[57] "domar" "lamer" "arepa" "hidra" "cagar" "braza"
palabras_ausentes <- 
  setdiff(palabras_wordle %>% pull(palabra),
        datos_CREA_filtrado %>% filter(nletras == 5) %>%
          pull(palabra))

datos_CREA_filtrado <-
  datos_CREA %>%
  filter(palabra %in% (palabras_wordle %>% pull(palabra)) | 
           palabra %in% (datos_CREA_filtrado %>% pull(palabra)))
datos_CREA_filtrado <-
  datos_CREA_filtrado %>%
  add_row(palabra = "cotar", frec_abs = 10,
          log_frec_abs = log(10), nletras = 5) %>%
  add_row(palabra = "titar", frec_abs = 10,
          log_frec_abs = log(10), nletras = 5) %>%
  add_row(palabra = "kopek", frec_abs = 10,
          log_frec_abs = log(10), nletras = 5)

Con las palabras candidatas en WORDLE podemos también generar su tabla de frecuencias en los documentos incluidos en el CREA.

Frecuencia en CREA de las palabras configuradas para salir en WORLDE

Las 5 palabras del WORDLE con mayor frecuencia de repetición en el conjunto de textos que componen el corpus de la RAE son entre, donde, menos, mundo, forma.

Frecuencia de letras en palabras

No solo es importante el número de veces que se repite una palabra sino cómo se distribuyen las letras en esas palabras: no es lo mismo empezar el juego con una palabra con varias vocales (para obtener información de las mismas) que empezar con una palabra que tiene z, ñ o k (ya que lo más seguro es que te quedes con la misma información que antes de jugar). ¿Cómo se distribuyen las letras en el castellano? ¿Influye el número de palabras? ¿Y su posición?

📝Código

# Matriz letras tokenizadas
matriz_letras <- function(corpus, n = 5) {
  
  if (!is.null(n)) {
    
    # Filtramos
    corpus_filtrado <- corpus %>% filter(nletras == n)
  
    # Creamos matriz de letras
    matriz_letras <-
      matrix(unlist(strsplit(corpus_filtrado$palabra, "")),
               ncol = nrow(corpus_filtrado))
    
    # Frecuencia de letras en las palabras de wordle
    frecuencia_letras <-
      as_tibble(as.character(matriz_letras)) %>%
      group_by(value) %>% count() %>%
      ungroup %>%
      mutate(porc = 100 * n / sum(n))
    
  } else {
    
    corpus_filtrado <- corpus
    
    # Creamos matriz de letras
    matriz_letras <- unlist(strsplit(corpus_filtrado$palabra, ""))
    
    # Frecuencia de letras en las palabras de wordle
    frecuencia_letras <-
      as_tibble(as.character(matriz_letras)) %>%
      group_by(value) %>% count() %>%
      ungroup %>%
      mutate(porc = 100 * n / sum(n))
  }
  
  # Output
  return(list("corpus_filtrado" = corpus_filtrado,
              "matriz_letras" = matriz_letras,
              "frecuencia_letras" = frecuencia_letras))
}
tokens <- matriz_letras(datos_CREA_filtrado, n = NULL)

Frecuencia de las letras en los vocablos de CREA

Las letras más comunes en CREA son la a, e, o, i, r, y las que menos aparecen son la k, ñ, w.

¿Se mantiene esa distribución de las letras cuando reducimos el corpus de CREA a palabras de 5 letras?

Frecuencia de las letras en los vocablos de CREA de 5 caracteres

Se mantienen las 5 primeras a, e, o, r, i, aunque las letras i,r se intercambian posicioens. ¿Se mantiene esa distribución de las letras en el conjunto de candidatas de WORDLE?

Frecuencia de las letras en los vocablos candidatos en WORDLE

En el caso de las palabras candidatas del WORDLE el top5 queda como a, o, r, e, l. Otra pregunta razonable a hacerse sería si influye el número de letras en los caracteres que aparecen. ¿La distribución de letras es similar en palabras de 3, 5 u 8 letras? Veámoslo gráficamente.

Palabras iniciales y finales

Elena Álvarez Mellado, experta en lingüística computacional, apuntaba que quizás una pista o ayuda para adivinar las palabras sea analizar qué letras suelen encabezar y terminas las palabras en castellano. El tuit es este

Entre las palabras de CREA, analizaremos todas las letras iniciales y finales de las palabras de las que disponemos, y calcularemos la proporción de veces en las que sucede.

Del conjunto total de CREA, las letras más frecuentes iniciando palabras son c,a,p,e,d, y las letras más frecuentes terminando palabras son s,a,o,e,n.

Del conjunto total de CREA con solo 5 letras, las letras más frecuentes iniciando palabras son c,a,p,m,s, y las letras más frecuentes terminando palabras son a,s,o,e,n.

Del conjunto de palabras de WORDLE las letras más frecuentes iniciando palabras son c,a,m,p,l, y las letras más frecuentes terminando palabras son o,a,r,e,l.

📝Código

letras_iniciales <-
  tibble("letras_iniciales" =
           map_chr(strsplit(datos_CREA_filtrado$palabra, ""),
                   function(x) { x[1] })) %>%
  group_by(letras_iniciales) %>% count() %>% ungroup() %>%
  mutate(porc = 100 * n / sum(n))

letras_finales <-
  tibble("letras_finales" =
           map_chr(strsplit(datos_CREA_filtrado$palabra, ""),
                   function(x) { rev(x)[1] })) %>%
  group_by(letras_finales) %>% count() %>% ungroup() %>%
  mutate(porc = 100 * n / sum(n))

fig1 <- ggplot(letras_iniciales %>%
         arrange(desc(porc)) %>%
         mutate(letras_iniciales =
                  factor(letras_iniciales, levels = letras_iniciales),
                vocal = 
                  letras_iniciales %in% c("a", "e", "i", "o", "u")),
       aes(x = letras_iniciales, y = porc, fill = vocal)) + 
  geom_col(alpha = 0.9) +
  scale_fill_manual(values = c("#c9b458", "#6baa64"),
                     labels = c("Consonante", "Vocal")) +
  labs(x = "Letras iniciales", y = "Frec. relativa (%)",
       fill = "Tipo")

fig2 <- ggplot(letras_finales %>%
         arrange(desc(porc)) %>%
         mutate(letras_finales =
                  factor(letras_finales, levels = letras_finales),
                vocal =
                  letras_finales %in% c("a", "e", "i", "o", "u")),
       aes(x = letras_finales, y = porc, fill = vocal)) +
  geom_col(alpha = 0.9) +
  scale_fill_manual(values = c("#c9b458", "#6baa64"),
                     labels = c("Consonante", "Vocal")) +
  labs(x = "Letras finales", y = "Frec. relativa (%)",
       fill = "Tipo")

library(patchwork)
(fig1 / fig2) +
  plot_annotation(
    title = "WORDLE",
       subtitle =
       paste0("Distribución de letras finales/iniciales en TODAS las palabras de CREA"),
       caption =
         paste0("Javier Álvarez Liébana (@dadosdelaplace). Datos: CREA")) +
  plot_layout(guides = "collect")
letras_iniciales <-
  tibble("letras_iniciales" =
           map_chr(strsplit(datos_CREA_filtrado %>%
                              filter(nletras == 5) %>%
                              pull(palabra), ""),
                   function(x) { x[1] })) %>%
  group_by(letras_iniciales) %>% count() %>% ungroup() %>%
  mutate(porc = 100 * n / sum(n))
letras_finales <-
  tibble("letras_finales" =
           map_chr(strsplit(datos_CREA_filtrado %>%
                              filter(nletras == 5) %>%
                              pull(palabra), ""),
                   function(x) { rev(x)[1] })) %>%
  group_by(letras_finales) %>% count() %>% ungroup() %>%
  mutate(porc = 100 * n / sum(n))

fig1 <- 
  ggplot(letras_iniciales %>%
         arrange(desc(porc)) %>%
         mutate(letras_iniciales =
                  factor(letras_iniciales, levels = letras_iniciales),
                vocal = 
                  letras_iniciales %in% c("a", "e", "i", "o", "u")),
       aes(x = letras_iniciales, y = porc, fill = vocal)) + 
  geom_col(alpha = 0.9) +
  scale_fill_manual(values = c("#c9b458", "#6baa64"),
                     labels = c("Consonante", "Vocal")) +
  labs(y = "Frec. relativa (%)", x = "Letras iniciales",
       fill = "Tipo")
fig2 <- 
  ggplot(letras_finales %>%
         arrange(desc(porc)) %>%
         mutate(letras_finales =
                  factor(letras_finales, levels = letras_finales),
                vocal = 
                  letras_finales %in% c("a", "e", "i", "o", "u")),
       aes(x = letras_finales, y = porc, fill = vocal)) + 
  geom_col(alpha = 0.9) +
  scale_fill_manual(values = c("#c9b458", "#6baa64"),
                     labels = c("Consonante", "Vocal")) +
  labs(y = "Frec. relativa (%)", x = "Letras finales",
       fill = "Tipo")

library(patchwork)
(fig1 / fig2) +
  plot_annotation(
    title = "WORDLE",
       subtitle =
       paste0("Distribución de letras finales/iniciales en las palabras de CREA de 5 letras"),
       caption =
         paste0("Javier Álvarez Liébana (@dadosdelaplace). Datos: CREA")) +
  plot_layout(guides = "collect")
letras_iniciales <-
  tibble("letras_iniciales" =
           map_chr(strsplit(palabras_wordle$palabra, ""),
                   function(x) { x[1] })) %>%
           group_by(letras_iniciales) %>% count() %>%
           ungroup() %>%
           mutate(porc = 100 * n / sum(n))
letras_finales <-
  tibble("letras_finales" =
           map_chr(strsplit(palabras_wordle$palabra, ""),
                   function(x) { rev(x)[1] })) %>%
           group_by(letras_finales) %>% count() %>%
           ungroup() %>%
           mutate(porc = 100 * n / sum(n))

fig1 <- 
  ggplot(letras_iniciales %>%
         arrange(desc(porc)) %>%
         mutate(letras_iniciales =
                  factor(letras_iniciales, levels = letras_iniciales),
                vocal = 
                  letras_iniciales %in% c("a", "e", "i", "o", "u")),
       aes(x = letras_iniciales, y = porc, fill = vocal)) + 
  geom_col(alpha = 0.9) +
  scale_fill_manual(values = c("#c9b458", "#6baa64"),
                     labels = c("Consonante", "Vocal")) +
  labs(y = "Frec. relativa (%)", x = "Letras iniciales",
       fill = "Tipo")
fig2 <- 
  ggplot(letras_finales %>%
         arrange(desc(porc)) %>%
         mutate(letras_finales =
                  factor(letras_finales, levels = letras_finales),
                vocal = 
                  letras_finales %in% c("a", "e", "i", "o", "u")),
       aes(x = letras_finales, y = porc, fill = vocal)) + 
  geom_col(alpha = 0.9) +
  scale_fill_manual(values = c("#c9b458", "#6baa64"),
                     labels = c("Consonante", "Vocal")) +
  labs(y = "Frec. relativa (%)", x = "Letras finales",
       fill = "Tipo")

(fig1 / fig2) +
  plot_annotation(
    title = "WORDLE",
       subtitle =
       paste0("Distribución de letras finales/iniciales en las palabras de WORDLE"),
       caption =
         paste0("Javier Álvarez Liébana (@dadosdelaplace). Datos: CREA y github.com/danielfrg")) +
  plot_layout(guides = "collect")

Puntuando

Puntuando letras

Hemos visto cuáles son las letras más frecuentes en las palabras, en general, y al inicio y final de las mismas, y su probabilidad (empírica) de aparecer. Sin embargo, como bien apunta Gabriel Rodríguez Alberich, hemos visto que no todas las palabras aparecerán con la misma frecuencia, tendremos una bolsa de palabras donde hay palabras más repetidas que otras, así que una opción es ponderar cada letra por las opciones que tiene cada palabra que la contiene de aparecer: la letra e en kefir no debería puntuar lo mismo que en sobre. Extraeremos cada letra pero a la hora de contarla, la multiplicaremos por las opciones que tiene la palabra de aparecer.

📝Código

puntuar_letras <- function(corpus, n = 5) {
  
  if (!is.null(n)) {
    
    # Filtramos
    corpus_filtrado <- corpus %>% filter(nletras == n)
  
    # Creamos matriz de letras
    matriz_letras <-
      matrix(unlist(strsplit(corpus_filtrado$palabra, "")),
               ncol = nrow(corpus_filtrado))
    pesos <- rep(corpus_filtrado$frec_relativa, each = n)
    matriz_letras_pesos <-
      tibble("matriz_letras" = 
               unlist(strsplit(corpus_filtrado$palabra, "")),
             pesos)
    
    # Ponderación de letras
    frecuencia_letras <-
      matriz_letras_pesos %>%
      group_by(matriz_letras) %>%
      summarise(peso_promediado = sum(pesos, na.rm = TRUE)) %>%
      ungroup() %>%
      mutate(peso_promediado_rel =
               peso_promediado / sum(peso_promediado, na.rm = TRUE))
    
  } else {
    
    corpus_filtrado <- corpus
    
    # Creamos matriz de letras
    matriz_letras <- unlist(strsplit(corpus_filtrado$palabra, ""))
    pesos <-
      unlist(mapply(corpus_filtrado$frec_relativa,
                    corpus_filtrado$nletras,
                    FUN = function(x, y) { rep(x, y)}))
    matriz_letras_pesos <- tibble(matriz_letras, pesos)
    
    # Ponderación de letras
    frecuencia_letras <-
      matriz_letras_pesos %>%
      group_by(matriz_letras) %>%
      summarise(peso_promediado = sum(pesos, na.rm = TRUE)) %>%
      ungroup() %>%
      mutate(peso_promediado_rel =
               peso_promediado /
               sum(peso_promediado, na.rm = TRUE))
  }
  
  # Output
  return(frecuencia_letras)
}
puntuacion_letras_global <-
  puntuar_letras(datos_CREA_filtrado, n = NULL)
puntuacion_letras_5 <-
  puntuar_letras(datos_CREA_filtrado, n = 5)

Ponderación de las letras basadas en CREA

Ponderación de las letras basadas en palabras de 5 letras de CREA

Puntuando palabras

Una vez que tenemos puntuadas las letras que van a formar nuestas palabras vamos a tomar los dos conjuntos de palabras de 5 letras, el conjunto extraído de CREA tras eliminar palabras poco repetidas (casi 10 000 vocablos) y el conjunto de candidatas a WORDLE (620 palabras), y puntuaremos cada palabra en función de cuatro criterios:

\[B = 1 - \sum_{i=1}^{k} f_{i}^{2}\]

donde \(k\) es el número de letras distintas y \(f_i\) es la proporción de veces que se repite cada letra distinta en la palabra. Por ejemplo, la palabra aerea tendrá un índice de \(B = 0.64\) ya que tanto la a como la e tienen una frecuencia relativa de \(2/5\) y la r \(1/5\), tal que \(B = 1 - \left[\left( \frac{2}{5} \right)^2 + \left( \frac{2}{5} \right)^2 + \left( \frac{1}{5} \right)^2 \right] = 0.64\). La máxima puntuación para 5 letras, sería que todas fueran distintas (\(k = 5\)), con un índice de \(B = \frac{k-1}{k} = \frac{4}{5} = 0.8\); la mínima puntuación sería que todas fueran iguales (\(k=1\)) con \(B = 0\). Este índice nos permite medir la probabilidad de que dos letras de la palabra tomadas al azar sean distintas. El índice será normalizado para que aquellas palabras con todas las letras repetidas tengan \(B_{norm} = 0\) y todas las palabras con las letras distintas tengan \(B_{norm} = 1\).

📝Código

# Letras iniciales/finales
letras_iniciales <-
  tibble("letras_iniciales" =
           map_chr(strsplit(datos_CREA_filtrado %>%
                              filter(nletras == 5) %>%
                              pull(palabra), ""),
                   function(x) { x[1] })) %>%
  group_by(letras_iniciales) %>% count() %>% ungroup() %>%
  mutate(porc = 100 * n / sum(n))
letras_finales <-
  tibble("letras_finales" =
           map_chr(strsplit(datos_CREA_filtrado %>%
                              filter(nletras == 5) %>%
                              pull(palabra), ""),
                   function(x) { rev(x)[1] })) %>%
  group_by(letras_finales) %>% count() %>% ungroup() %>%
  mutate(porc = 100 * n / sum(n))

# Puntuamos palabras
puntuar_palabras <-
  function(palabras, letras_puntuadas, letras_iniciales,
           letras_finales, nletras = 5) { 
    
    # Matriz letras
    matriz_letras_corpus <- matriz_letras(palabras, n = nletras)
    matriz_letras_corpus <- matriz_letras_corpus$matriz_letras
    
    # palabras  peso promediado  peso relativo
    # Puntuar palabras
    palabras_puntuadas <-
      palabras %>% 
      mutate(punt_letras =
               apply(matriz_letras_corpus, MARGIN = 2,
                     FUN = function(x) { sum(letras_puntuadas$peso_promediado_rel[
                       letras_puntuadas$matriz_letras %in% x] * 
                         c((letras_iniciales %>%
                             filter(letras_iniciales == x[1]) %>%
                              pull(porc)) / 100, 
                           1/5, 1/5, 1/5, (letras_finales %>%
                                       filter(letras_finales ==
                                                rev(x)[1]) %>%
                                       pull(porc)) / 100))}),
             ind_blau =
               apply(matriz_letras_corpus, MARGIN = 2,
                     FUN = function(x) { 1 - sum((table(x) / sum(table(x)))^2)}),
             ind_blau_norm = ind_blau / max(ind_blau),
             punt_letras_total = punt_letras * ind_blau_norm,
             punt_total_w = punt_letras_total * log_frec_abs)
    
    # Iniciales y finales
    
    # Output
    return(list("palabras_puntuadas" = palabras_puntuadas,
                "matriz_letras" = matriz_letras_corpus))
    
  }
CREA_puntuado <-
  puntuar_palabras(datos_CREA_filtrado %>%
                     filter(nletras == 5),
                   puntuacion_letras_5,
                   letras_iniciales, letras_finales)
WORDLE_puntuado <-
  puntuar_palabras(datos_palabras_wordle,
                   puntuacion_letras_5,
                   letras_iniciales, letras_finales)
CREA_puntuado$palabras_puntuadas
# A tibble: 3,529 × 13
   palabra frec_abs frec_norm frec_relativa log_frec_abs log_frec_rel
   <chr>      <dbl>     <dbl>         <dbl>        <dbl>        <dbl>
 1 sobre     289704     1899.      0.00192          12.6    0.0000128
 2 entre     267493     1753.      0.00178          12.5    0.0000127
 3 habia     223430     1465.      0.00148          12.3    0.0000126
 4 hasta     202935     1330.      0.00135          12.2    0.0000125
 5 desde     198647     1302.      0.00132          12.2    0.0000124
 6 puede     161219     1057.      0.00107          12.0    0.0000122
 7 todos     158168     1037.      0.00105          12.0    0.0000122
 8 parte     148750      975.      0.000988         11.9    0.0000121
 9 tiene     147274      965.      0.000978         11.9    0.0000121
10 donde     132077      866.      0.000877         11.8    0.0000120
# … with 3,519 more rows, and 7 more variables: int_frec_norm <fct>,
#   nletras <dbl>, punt_letras <dbl>, ind_blau <dbl>,
#   ind_blau_norm <dbl>, punt_letras_total <dbl>, punt_total_w <dbl>
WORDLE_puntuado$palabras_puntuadas
# A tibble: 620 × 13
   palabra frec_abs frec_norm frec_relativa log_frec_abs log_frec_rel
   <chr>      <dbl>     <dbl>         <dbl>        <dbl>        <dbl>
 1 entre     267493     1753.      0.00178          12.5    0.0000127
 2 donde     132077      866.      0.000877         11.8    0.0000120
 3 menos     103498      678.      0.000687         11.5    0.0000118
 4 mundo     101745      667.      0.000676         11.5    0.0000118
 5 forma      97165      637.      0.000645         11.5    0.0000117
 6 hacer      96063      630.      0.000638         11.5    0.0000117
 7 mayor      90166      591.      0.000599         11.4    0.0000116
 8 ellos      84636      555.      0.000562         11.3    0.0000116
 9 hecho      83898      550.      0.000557         11.3    0.0000116
10 lugar      78250      513.      0.000520         11.3    0.0000115
# … with 610 more rows, and 7 more variables: int_frec_norm <fct>,
#   nletras <dbl>, punt_letras <dbl>, ind_blau <dbl>,
#   ind_blau_norm <dbl>, punt_letras_total <dbl>, punt_total_w <dbl>

Puntuación de palabras del CREA

Puntuación de palabras candidatas de WORDLE

Simulando WORDLE

Una vez que tenemos un sistema para puntuar palabras, la mecánica será sencilla: vamos a simular un número de partidas de WORDLE, considerando tres casos:

Una vez tenemos puntuadas las palabras la mecánica será sencilla. Generaremos un conjunto de simulaciones, generando una palabra inicial en cada una de ellas (palabra inicial que se obtendrá aleatoriamente tomando las puntuaciones de las palabras como pesos). En cada iteración comprobaremos que letras están bien colocadas, que letras están pero mal colocadas y que letras son errores. Tras dicha comprobación, calcularemos el conjunto de palabras de entre las candidatas que cumplen dichas condiciones, y de ese conjunto «superviviente» elegiremos la palabra con mayor puntuación posible. Además, para comprobar que nuestro método mejora la metodología de hacerlo totalmente aleatorio, se compara en cada caso que pasaría si simplemente eligiéramos las palabras al azar del conjunto de candidatas que cumplen las condiciones.

Aunque el juego en inglés si parece elegir las palabras a jugar en base a su frecuencia de uso en inglés, priorizando las palabras más usadas (aquí una metodología propuesta por Esteban Moro para el juego en inglés), no tengo constancia que sea así en castellano, así que la elección de palabras a adivinar será equiprobable (todas las palabras tienen las mismas opciones de salir) y, de momento, la palabra inicial del usuario también.

📝Código

# Iteración del juego
iteracion <- function(inicial, clave) {
  
  # Jugada
  bien_colocadas <-
    unlist(map2(strsplit(inicial, ""), strsplit(clave, ""),
                function(x, y) { x == y }))
  mal_colocadas <-
    unlist(map2(strsplit(inicial, ""), strsplit(clave, ""),
                function(x, y) { x %in% y })) &
    !bien_colocadas
  errores <- !(bien_colocadas | mal_colocadas)
  
  # Output
  return(list("bien_colocadas" = bien_colocadas,
              "mal_colocadas" = mal_colocadas,
              "errores" = errores))
}

# Simulación
simular_wordle <-
  function(corpus, matriz_corpus, palabras_candidatas = corpus, 
           intentos = 1, generar_equi = TRUE, iniciar_equi = TRUE,
           dummy_random = FALSE,  inicial_fija = NULL,
           clave_fija = NULL,
           extremely_dummmy = FALSE) {
    
    
    if (is.null(clave_fija)) {
      
      # probabilidades de salir la palabra
      # * si generar_equi = TRUE --> equiprobables
      # * si generar_equi = FALSE --> en función de pesos
      if (generar_equi) {
        
        probs_gen <- rep(1 / nrow(palabras_candidatas),
                         nrow(palabras_candidatas))
        
      } else {
        
        probs_gen <- palabras_candidatas$punt_total_w /
          sum(palabras_candidatas$punt_total_w)
        
      }
    
      # Palabra a adivinar
      clave <- sample(palabras_candidatas$palabra,
                      size = 1, prob = probs_gen)
    } else {
      
      clave <- clave_fija
    }
    propiedades_clave <-
      palabras_candidatas %>% filter(palabra == clave)
    
    # Palabra inicial
    if (is.null(inicial_fija)) {
      if (iniciar_equi) {
        
        inicial <- sample(corpus$palabra, size = 1)
        
      } else {
        
        # Las 50 mejor puntuadas
        inicial <- corpus %>%
          arrange(desc(punt_total_w)) %>%
          slice(30) %>% pull(palabra)
        inicial <- sample(inicial, size = 1)
        
      }
    } else {
      
      inicial <- inicial_fija
      
    }
    propiedades_inicial <- corpus %>% filter(palabra == inicial)
    
    # Inicialización
    palabra_0 <- inicial
    candidatas <- corpus
    matriz_candidatas <- matriz_corpus
    salida <- list()
    for (i in 1:intentos) {
      
      salida[[i]] <- iteracion(palabra_0, clave)
      
      idx_palabras <-
        apply(matriz_candidatas, MARGIN = 2,
              FUN = function(x) {
                all(x[salida[[i]]$bien_colocadas] ==
                      unlist(strsplit(palabra_0, ""))[salida[[i]]$bien_colocadas]) }) &
        apply(matriz_candidatas, MARGIN = 2,
              FUN = function(x) {
                all(!(x %in% unlist(strsplit(palabra_0, ""))[salida[[i]]$errores])) })
      
      if (any(salida[[i]]$mal_colocadas)) {
        
        idx_palabras <- idx_palabras &
          apply(matriz_candidatas, MARGIN = 2,
                FUN = function(x) {
                  all(unlist(strsplit(palabra_0, ""))[salida[[i]]$mal_colocadas] %in% x) &
                    all(!mapply(x[which(salida[[i]]$mal_colocadas)],
                                unlist(strsplit(palabra_0, ""))[salida[[i]]$mal_colocadas],
                                FUN = function(x, y) { x == y})) } )
      }
      
      # Seleccionamos
      if (extremely_dummmy) {
        
        matriz_candidatas <- matriz_candidatas
        candidatas <- candidatas
        
      } else {
        if (any(idx_palabras)) {
          
          matriz_candidatas <- matriz_candidatas[, idx_palabras]
          candidatas <- candidatas[idx_palabras, ]
          
          if (!dummy_random) {
            
            palabra_0 <-
              candidatas %>% arrange(desc(punt_total_w)) %>%
              slice(1) %>% pull(palabra)
            
          } else {
            
            palabra_0 <-
              candidatas %>%
              slice_sample(n = 1) %>% pull(palabra)
          }
          
        }
      }
      
      if (nrow(candidatas) <= 1) {
        
        break
      } 
    }
    
    intentos <- ifelse(nrow(candidatas) == 1,
                       ifelse(palabra_0 == clave, i + 1,
                              intentos + 1), intentos + 1)
    
    # Output
    return(list("palabra_clave" = clave, "inicial" = inicial,
                "salida" = salida, "candidatas" = candidatas,
                "palabra_0" = palabra_0,
                "matriz_candidatas" = matriz_candidatas,
                "intentos" = intentos,
                "propiedades_clave" = propiedades_clave,
                "propiedades_inicial" = propiedades_inicial))
  }

simulacion_wordle <-
  function(corpus_puntuado,
           palabras_candidatas = corpus_puntuado,
           simulaciones = 1e3, nintentos = 6,
           generar_equi = TRUE, iniciar_equi = TRUE,
           dummy_random = FALSE, inicial_fija = NULL,
           clave_fija = NULL,
           extremely_dummmy = FALSE) {
    
    # Puntuamos palabras
    corpus_wordle_puntuado <- corpus_puntuado$palabras_puntuadas
    matriz_letras_wordle <- corpus_puntuado$matriz_letras
    palabras_candidatas <- palabras_candidatas$palabras_puntuadas
  
    # Simulación
    resultados <- 
      replicate(simulaciones,
                simular_wordle(corpus_wordle_puntuado,
                               matriz_letras_wordle,
                               palabras_candidatas,
                               intentos = nintentos,
                               generar_equi = generar_equi,
                               iniciar_equi = iniciar_equi,
                               dummy_random = dummy_random,
                               inicial_fija = inicial_fija,
                               clave_fija = clave_fija,
                               extremely_dummmy = extremely_dummmy))
    # Output
    return(list("corpus_wordle" = corpus_wordle_puntuado,
                "matriz_letras_wordle" = matriz_letras_wordle,
                "corpus_wordle_puntuado" = corpus_wordle_puntuado,
                "resultados" = resultados))
  }

Empecemos por el peor de los casos: la palabra a adivinar puede ser cualquiera de los 3529 vocablos de CREA de 5 letras.

📝Código

# * 6 intentos y 5 letras
# * con palabra inicial y clave equiprobables
simulaciones <- 2000
generar_equi <- TRUE
iniciar_equi <- FALSE
set.seed(1234567)
simulacion_CREA <-
  simulacion_wordle(CREA_puntuado,
                    simulaciones = simulaciones,
                    generar_equi = generar_equi,
                    iniciar_equi = iniciar_equi,
                    dummy_random = FALSE)

intentos_CREA <- unlist(simulacion_CREA$resultados["intentos", ])
distrib_intentos_CREA <- 100 * table(intentos_CREA) / simulaciones
media_intentos_CREA <- mean(intentos_CREA)
distrib_intentos_CREA
intentos_CREA
    2     3     4     5     6     7 
 1.00 16.60 37.20 28.45 11.10  5.65 
media_intentos_CREA
[1] 4.49
# Dummy (palabra aleatoria entre candidatas)
generar_equi <- TRUE
iniciar_equi <- TRUE
simulacion_dummy <-
  simulacion_wordle(CREA_puntuado,
                    simulaciones = simulaciones,
                    generar_equi = generar_equi,
                    iniciar_equi = iniciar_equi,
                    dummy_random = TRUE)
intentos_dummy <- unlist(simulacion_dummy$resultados["intentos", ])
distrib_intentos_dummy <- 100 * table(intentos_dummy) / simulaciones
media_intentos_dummy <- mean(intentos_dummy)
distrib_intentos_dummy
intentos_dummy
    2     3     4     5     6     7 
 0.70 10.85 33.30 31.80 15.30  8.05 
media_intentos_dummy
[1] 4.743

En este caso extremo en el que nuestras palabras candidatas podrían ser los 3529 vocablos de CREA de 5 letras, conseguimos ganar en 6 intentos o menos el 94.35% de las veces, con una media de 4.49 intentos para resolverlo y una mediana de 4 (el 50% de las veces lo resuelve en dichos intentos o menos). En el caso de decidir las palabras aleatoriamente (entre las candidatas en cada paso), obtendríamos una media de 4.74 y una mediana de 5, consiguiendo resolverlo el 91.95% de las veces.

El mejor de los casos será aquel en el que el conjunto de palabras que el usuario podría pensar y el conjunto de palabras a adivinar es el mismo, y es el conjunto reducido de palabras que el juego oficial de WORDLE en castellano tiene programadas, con 620 vocablos.

📝Código

# solo las candidatas a wordle
simulaciones <- 2000
generar_equi <- TRUE
iniciar_equi <- FALSE
set.seed(1234567)
simulacion_WORDLE <-
  simulacion_wordle(WORDLE_puntuado,
                    simulaciones = simulaciones,
                    generar_equi = generar_equi,
                    iniciar_equi = iniciar_equi)
intentos_WORDLE <- unlist(simulacion_WORDLE$resultados["intentos", ])
distrib_intentos_WORDLE <- 100 * table(intentos_WORDLE) / simulaciones
media_intentos_WORDLE <- mean(intentos_WORDLE)
distrib_intentos_WORDLE
intentos_WORDLE
    2     3     4     5     6     7 
 4.40 35.75 39.55 16.80  2.90  0.60 
media_intentos_WORDLE
[1] 3.7985
# Dummy (palabra aleatoria entre candidatas)
generar_equi <- TRUE
iniciar_equi <- TRUE
simulacion_dummy_WORDLE <-
  simulacion_wordle(WORDLE_puntuado,
                    simulaciones = simulaciones,
                    generar_equi = generar_equi,
                    iniciar_equi = iniciar_equi,
                    dummy_random = TRUE)
intentos_dummy_WORDLE <-
  unlist(simulacion_dummy_WORDLE$resultados["intentos", ])
distrib_intentos_dummy_WORDLE <-
  100 * table(intentos_dummy_WORDLE) / simulaciones
media_intentos_dummy_WORDLE <- mean(intentos_dummy_WORDLE)
distrib_intentos_dummy_WORDLE
intentos_dummy_WORDLE
    2     3     4     5     6     7 
 3.40 31.45 42.60 18.00  3.85  0.70 
media_intentos_dummy_WORDLE
[1] 3.8955

En este caso conseguimos ganar en 6 intentos o menos el 99.4% de las veces, con una media de 3.8 intentos para resolverlo y una mediana de 4 (el 50% de las veces lo resuelve en dichos intentos o menos). En el caso de decidir las palabras aleatoriamente (entre las candidatas en cada paso), obtendríamos una media de 3.9 y una mediana de 4 (el 50% de las veces lo resuelve en dichos intentos o menos)., consiguiendo resolverlo el 99.3% de las veces.

Por último el caso más realista: el conjunto de palabras que el usuario podría pensar será el conjunto de vocablos de CREA de 5 letras y con una frecuencia normalizada superior a 3 por cada 1000 documentos analizados (un total de 19818 vocablos, bastante más extenso de las palabras que una persona seguramente pueda considerar, de 1993 palabras si lo reducimos a las palabras de 5 letras). Sin embargo, el conjunto de palabras a adivinar será el conjunto reducido de palabras que el juego oficial de WORDLE en castellano tiene programadas, con 620 vocablos.

📝Código

# adivinando wordle pero con corpus
simulaciones <- 2000
generar_equi <- TRUE
iniciar_equi <- FALSE
set.seed(1234567)
simulacion_mixta <-
  simulacion_wordle(CREA_puntuado,
                    palabras_candidatas = WORDLE_puntuado,
                    simulaciones = simulaciones,
                    generar_equi = generar_equi,
                    iniciar_equi = iniciar_equi)
intentos_mixta <- unlist(simulacion_mixta$resultados["intentos", ])
distrib_intentos_mixta <-
  100 * table(intentos_mixta) / simulaciones
media_intentos_mixta <- mean(intentos_mixta)
distrib_intentos_mixta
intentos_mixta
    2     3     4     5     6     7 
 0.75 19.20 41.10 27.20  9.20  2.55 
media_intentos_mixta
[1] 4.3255
# Dummy (palabra aleatoria entre candidatas)
generar_equi <- TRUE
iniciar_equi <- TRUE
simulacion_dummy_mixta <-
  simulacion_wordle(CREA_puntuado,
                    palabras_candidatas = WORDLE_puntuado,
                    simulaciones = simulaciones,
                    generar_equi = generar_equi,
                    iniciar_equi = iniciar_equi,
                    dummy_random = TRUE)
intentos_dummy_mixta <-
  unlist(simulacion_dummy_mixta$resultados["intentos", ])
distrib_intentos_dummy_mixta <-
  100 * table(intentos_dummy_mixta) / simulaciones
media_intentos_dummy_mixta <- mean(intentos_dummy_mixta)
distrib_intentos_dummy_mixta
intentos_dummy_mixta
   2    3    4    5    6    7 
 0.6 11.6 33.4 32.7 14.9  6.8 
media_intentos_dummy_mixta
[1] 4.701

En este caso conseguimos ganar en 6 intentos o menos el 97.45% de las veces, con una media de 4.33 intentos para resolverlo y una mediana de 4 (el 50% de las veces lo resuelve en dichos intentos o menos). En el caso de decidir las palabras aleatoriamente (entre las candidatas en cada paso), obtendríamos una media de 4.7 y una mediana de 5 (el 50% de las veces lo resuelve en dichos intentos o menos), consiguiendo resolverlo el 93.2% de las veces.

 

Las palabras a adivinar en los casos en los que no se puedo completar en 6 menos eran:

botar, limar, juego, forro, kefir, rotar, jarro, rayar, ruego, calva, jamas, gatas, gallo, rasta, rimar, junta, zebra, huevo

Las palabras iniciales eran:

costa

IMPORTANTE: los resultados de elegir una palabra aleatoria tienen truco, ya que no es totalmente aleatorio, sino que estamos cribando palabras en función de los resultados de la iteración anterior. Si la palabra fuese totalmente aleatorio, sin atender a los resultados de los cuadrados, el resultado sería bastante desastrosos, pero asumimos que en el peor de los casos, la estrategia mínima de un usuario será, al menos, cuadrar una palabra en función de sus cuadrados anteriores.

Elección de la palabra inicial

Por último vamos a realizar una busqueda de las palabras que mejor funcionan como palabra inicial. Para ello vamos a considerar las palabras del CREA más repetidas (que aparezcan en más de 220 de cada 1000 documentos) amén de las palabras de WORDLE (filtrando las que se repitan en menos de 20 de cada 1000 documentos). Para cada una vamos a generar un número de simulaciones y contabilizar el número de éxitos o fracasos.

📝Código

idx_WORDLE <-
  which(WORDLE_puntuado$palabras_puntuadas$frec_norm > 20)
idx_frec <- which(CREA_puntuado$palabras_puntuadas$frec_norm > 220 &
               CREA_puntuado$palabras_puntuadas$nletras == 5 &
               !(CREA_puntuado$palabras_puntuadas$palabra %in%
                   WORDLE_puntuado$palabras_puntuadas$palabra))

datos_CREA_frecuentes <- WORDLE_puntuado
datos_CREA_frecuentes$palabras_puntuadas <-
  rbind(WORDLE_puntuado$palabras_puntuadas[idx_WORDLE, ],
        CREA_puntuado$palabras_puntuadas[idx_frec, ])
datos_CREA_frecuentes$matriz_letras <-
  cbind(WORDLE_puntuado$matriz_letras[, idx_WORDLE],
        CREA_puntuado$matriz_letras[, idx_frec])

simulaciones <- 400
generar_equi <- TRUE
iniciar_equi <- FALSE
simulacion <- intentos <- distrib_intentos <- list()
media_intentos <- mediana_intentos <- n_fallos <-
  rep(0, nrow(datos_CREA_frecuentes$palabras_puntuadas))
for (i in 1:nrow(datos_CREA_frecuentes$palabras_puntuadas)) {
  
  simulacion[[i]] <-
      simulacion_wordle(datos_CREA_frecuentes,
                        palabras_candidatas = WORDLE_puntuado,
                        simulaciones = simulaciones,
                        generar_equi = generar_equi,
                        iniciar_equi = iniciar_equi,
                        inicial_fija = datos_CREA_frecuentes$palabras_puntuadas$palabra[i],
                        dummy_random = FALSE)
  
    intentos[[i]] <-
      unlist(simulacion[[i]]$resultados["intentos", ])
    distrib_intentos[[i]] <- 100 * table(intentos[[i]]) / simulaciones
    media_intentos[i] <- mean(intentos[[i]])
    mediana_intentos[i] <- median(intentos[[i]])
    n_fallos[i] <- sum(intentos[[i]] == 7)
}

Las palabras iniciales con mejor «rendimiento» han sido:

banco, marco, clima, serio, comer, metro, gusto.

Jugar a WORDLE

Puedes simular el juego con el código en https://github.com/dadosdelaplace/blog-R-repo/blob/main/wordle/codigoR.R, con el que podrás introducir los aciertos que te devuelva la web, y la función te propondrá palabras candidatas a introducir.

La idea es hacer en el futuro un simulador visual del juego en R pero…to be continued. Aunque puedes simular el juego Tenéis un simulador en R para el juego en inglés en https://github.com/coolbutuseless/wordle

🛑 Limitaciones

Mi ignorancia

La rama de la estadística o la ciencia de datos que se dedica al análisis de texto se suele conocer como text mining o minería de textos, y es una de las ramas más complejas y difíciles (al menos en mi opinión) ya que perdemos las bondades de los números y pasamos a trabajar no solo con variables cualitativas (no son variables cuantitativas, no cuantifican ningun valor medible) sino que entra en juego un factor complejo de modelizar: las reglas del lenguaje. Al contrario que el famoso Master Mind, donde cada combinación de colores es posible, al trabajar con letras y palabras no todas las combinaciones son válidas. Incluso existe toda una rama de la computación, ciencia de datos e inteligencia artficial dedicada a conseguir que un ordenador no solo procese palabras sino que las «entienda», y sea capaz de interactuar con lo escrito (por ejemplo, GPT-3).

La principal limitación de este pequeño análisis es mi propia ignorancia: no soy experto en minería de datos ni en 1ºººº2www procesamiento natural del lenguaje (NLP), más allá de un superficial conocimiento para poder impartir docencia en el máster de minería de datos de la UCM. Así que, obviamente, la metodología tiene un mero objetivo pedagógico y lúdico, siendo ampliamente mejorable.

Para aprender de este tipo de herramientas os dejo una lista de expertas y expertos que han tratado estos temas por Twitter:

Julia Silge, experta en text mining y autora de muchos de los paquetes más útiles de R para el tratamiento de textos.

Elena Álvarez Mellado, experta en lingüística computacional, y autora de uno de los repositorios más útiles para aprender a tratar textos, donde recopila los discursos de los jefes de Estado en España desde 1937 hasta 2021 https://github.com/lirondos/discursos-de-navidad

Picanúmeros, doctor en estadística y divulgador, suele analizar los textos de los programas electorales de los partidos en España.

Barri y Mari Luz Congosto, expertos en análisis de mensajes en Twitter (y su propagación).

Dot CSV (Carlos Santana), divulgador en Inteligencia Artificial, y uno de los mayores (y mejores) divulgadores de tecnologías como GPT-3.

Sesgo de selección en el corpus

En los datos analizados del CREA hay un sesgo de selección que depende de la tipología de los textos analizados (de hecho términos relacionados con biología o ciencia aparecen en mucha menor frecuencia) y con la franja temporal a la que pertenecen dichos documentos. Es importante recordar que el conjunto de vocablos en CREA no tiene porque coincidir con las palabras registradas en el diccionario oficial de la RAE.

Hipótesis de léxico extenso

Todo lo simulado se ha realizado bajo la hipótesis de que los usuarios conocen todas las palabras posibles del conjunto de palabras candidatas, algo que seguramente no suceda, por lo que el éxito en el juego dependará fuertemente del número de palabras que se sepa la persona que juegue. Algo interesante a analizar sería cómo evolucionan los aciertos en función del número de palabras que uno conoce (y basándonos en las palabras más usadas en castellano).