Quais as associações entre as cartas de Gwent nos decks existentes?

Eu tenho jogado Gwent: the Witcher Card Game há algum tempo, e é impressionante a quantidade de combos e sinergias que podem haver entre as cartas de acordo com o deck que você monta. Neste post, eu tento identificar as combinações de cartas que aparecem com maior frequência através de uma análise das regras de associação entre elas.

true
01-08-2022

Motivação

Gwent é um jogo de cartas do universo de The Witcher no qual dois jogadores se enfrentam em busca da maior pontuação em pelo menos 2 de 3 rodadas, cada uma com no máximo uns 10 turnos para cada jogador. Esta pontuação é dada pelo poder de cada carta e, também, através da forma que elas interagem entre si. A tabela abaixo traz um exemplo disto para 4 cartas pertencentes aos decks da facção Scoia’tael - uma das 6 facções existentes no jogo.

Show code
# carrega os pacotes
library(tidyverse) # core
library(reactable) # para tabelas interativas
library(reactablefmtr) # para embedar imagens no reactable

# carrega o exemplo
read_rds(file = 'data/decks.rds') %>%
  # pegando quatro cartas de exemplo
  filter(localizedName %in% c('Malena', 'Brigada Vrihedd', 
                              'Dol Blathanna: Guarda' , 'Bruxo Gato')) %>% 
  # extraindo um exemplo único de cada carta
  distinct(small, localizedName, power, texto) %>% 
  # juntando o prefixo do link da imagem
  mutate(small = paste0('https://www.playgwent.com/', small)) %>% 
  # organiza as cartas em ordem alfabetica
  arrange(localizedName) %>% 
  # colocando os exemplos em um reactable
  reactable(
    compact = TRUE, borderless = TRUE, defaultColDef = colDef(align = 'left'), 
    style = list(fontFamily = "Fira Sans", fontSize = "12px"),
    columns = list(
      small         = colDef(name = '', cell = embed_img(height = 80, width = 60), maxWidth = 80),
      localizedName = colDef(name = 'Carta', maxWidth = 140),
      power         = colDef(name = 'Poder', maxWidth = 50),
      texto         = colDef(name = 'Descrição')
    )
  )

Se utilizarmos um exemplar de cada uma destas cartas em uma rodada, temos um poder total de 16 (i.e., cada uma das 4 cartas tem 4 de poder). Todavia, a cada turno dentro desta rodada, podemos tomar vantagem de cada uma das cartas:

O combo destas 4 cartas é capaz criar uma diferença de 5 pontos entre nós e o oponente à cada turno em uma rodada (i.e., removendo um total de 3 pontos de poder dele e adicionando 2 pontos de poder à nós mesmos). Se você contar os 10 turnos da rodada, e o fato de que precisamos de 4 turnos para baixar estas cartas, este combo deve girar por uns 6 turnos - nos dando uma vantagem de 30 pontos ao final dele, caso o oponente não tome nenhuma contra-medida. Além disso, ainda existem os pontos que vamos abrindo de vantagem enquanto baixamos o combo, e que dependem muito mais da estratégia de jogo (i.e., que cartas baixar em que turnos) do que da estratégia de montagem do deck em si (i.e., que cartas incluir em um deck). De toda forma, sem as cartas certas no deck não há estratégia de jogo que segure…portanto, acredito que é muito importante saber identificar estas cartas e combiná-las da melhor forma possível.

Uma forma de identificarmos estas combinações é olhar os próprios decks existentes e tentar mapear os padrões de co-ocorrência das cartas. Neste contexto, se duas ou mais cartas tendem a aparecer juntas com grande frequência entre os decks é porquê, possivelmente, esta combinação pode estar envolvida em um combo. Assim, se conseguirmos identificar os padrões de co-ocorrência entre as cartas, poderíamos usar esta informação para desenhar decks que cujas cartas tenham maior sinergia entre si - garantindo que as contra-medidas não anulem totalmente a nossa estratégia.

Com isto em mente, vou utilizar o scrapper da biblioteca de decks de Gwent que desenvolvi em outro post para analisar os padrões de co-ocorrência entre cartas de Gwent. Meu intuito aqui vai ser preparar a base de dados que já havíamos obtido anteriormente, fazer uma breve análise exploratória dos padrões existentes e, então, utilizar um algoritmo para minear regras de associações para: (1) identificar os conjuntos de cartas que ocorrem com uma frequência maior do que àquela esperada ao acaso, e (2) determinar quão diferentes são as regras de associação detectadas. Eu não espero que ter acesso à essas informações vá me dar uma vantagem competitiva frente aos outros jogadores, até porquê talento para estratégia de jogo eu não tenho, mas acredito que elas possam acabar me ajudando a montar decks mais efetivos do que aqueles horríveis que monto atualmente.

Antes de começar a olhar os dados e tudo o mais, vou fazer um breve resumo sobre as peculiaridades e regras-padrão que devem ser seguidas para montagem de um deck de um Gwent. Isto vai facilitar bastante o entendimento dos dados que teremos à nossa disposição, bem como o contexto da análise exploratória dos dados.

Um resumo sobre a montagem dos decks

Preparação dos dados

O scrapper da biblioteca de decks de Gwent nos fornece a lista de decks contribuídos pela comunidade no site oficial do jogo, bem como a composição de cartas associadas à cada um deles. Como existem atualizações relativamente frequentes do jogo, algumas cartas sofrem nerfs e outras podem acabar recebendo um boost, o que faz com que decks que tenham sido editados há muito tempo possam estar meio defasados com relação à sua performance. Além disso, os decks podem receber likes e dislikes da comunidade, o que funciona como um termômetro do quão bom aquele deck deve ser. Assim, resolvi focar apenas nos decks que tivessem sido editados no ano de 2021 e que receberam pelo menos um voto. Com isso, é mais provável que vamos usar decks que estejam mais atualizados e validados de alguma forma pela comunidade. O pedaço de código abaixo carrega a lista de decks disponíveis no site oficial do jogo quando escrevi este post, e os apresenta em uma tabela.

Show code
# carregando os pacotes
library(tidytext) # para ajudar a trabalhar com texto
library(ggridges) # para os ridge plots
library(plotly) # para visualizacao com interatividade
library(igraph) # para plotar grafos
library(fs) # para manipular os paths

# carregando os metadados de cada deck
metadados <- read_rds(file = 'data/lista_de_decks.rds')

# fazendo alguns ajustes à base dos metadados
metadados <- metadados %>% 
  # selecionando apenas as colunas desejadas
  select(deck = id, name, faccao = slug, ano = modified, language, votes, craftingCost) %>% 
  # implementando pequenos ajustes aos dados
  mutate(
    # ajustando a coluna de data
    ano = lubridate::as_datetime(x = ano),
    ano = lubridate::year(x = ano),
    # ajustando coluna de slug
    faccao = str_to_title(string = faccao),
    faccao = case_when(faccao == 'Northernrealms' ~ 'Northern Realms',
                       faccao == 'Scoiatael' ~ "Scoia'tael",
                       TRUE ~ faccao)
  )

# printando a tabela como um reactable
metadados %>% 
  reactable(
    sortable = TRUE, filterable = TRUE, compact = TRUE,
    highlight = TRUE, borderless = TRUE, showPageSizeOptions = TRUE,
    defaultColDef = colDef(align = 'center'), defaultPageSize = 5,
    style = list(fontFamily = "Fira Sans", fontSize = "12px"),
    columns = list(
      deck         = colDef(name = 'Deck'),
      name         = colDef(name = 'Nome'),
      ano          = colDef(name = 'Ano de Edição'),
      faccao       = colDef(name = 'Facção'),
      language     = colDef(name = 'Origem'),
      votes        = colDef(name = 'Votos'),
      craftingCost = colDef(name = 'Custo de Criação')
    )
  )

A segunda parte do scrapper usa o código identificador único de cada deck (i.e., coluna Deck na tabela acima) para obter a sua composição de cartas e os metadados das mesmas. Eu raspei todos os decks atenderam aos dois requisitos descritos acima, compilei os resultados para um único dataframe e fiz alguns pequenos ajustes à base, só para fins de entendimento e clareza mesmo. O código abaixo carrega a base de dados com as cartas encontradas em cada deck, e cria uma tabela para visualizarmos todas as informações existentes ao nível do deck, apenas para fins de entendimento da estrutura de dados1.

Show code
# carregando os dados dos decks
decks <- read_rds(file = 'data/decks.rds')

# fazendo alguns ajustes aos dados dos decks
decks <- decks %>% 
  # removendo algumas informacoes que nao precisamos
  select(-small, -big, -fluff, -ownable, -short, -categoryName, -primaryCategoryId, -name) %>% 
  # ajustando as colunas
  mutate(
    # passando o id do deck para um inteiro, para bater com os metadados
    deck = as.integer(deck),
    # ajustando coluna do slug
    slug = str_to_title(string = slug),
    slug = case_when(slug == 'Northernrealms' ~ 'Northern Realms',
                     slug == 'Scoiatael' ~ "Scoia'tael",
                     TRUE ~ slug),
    # ajustando coluna do repeat count - quantidade daquela carta no deck
    repeatCount = repeatCount + 1,
    # contando quantidade de habilidade de cada carta
    habilidades = case_when(is.na(keywords) ~ 0,
                            TRUE ~ str_count(string = keywords, pattern = ';') + 1)
  ) %>% 
  # passando os outros strings para maiusculo
  mutate(across(.cols = c(rarity, cardGroup, type), .fns = ~ str_to_title(string = .x))) %>% 
  # juntando com id da faccao
  left_join(y = select(metadados, deck, faccao), by = 'deck')

# printando a tabela
decks %>% 
  # selecionando apenas as colunas cujas informações estejam no nível do deck
  select(deck, faccao, card_in_seq, localizedName, repeatCount, slug, type) %>% 
  # passando para o reactable
  reactable(
    sortable = TRUE, filterable = TRUE, compact = TRUE,
    highlight = TRUE, borderless = TRUE, showPageSizeOptions = TRUE,
    defaultColDef = colDef(align = 'center'), defaultPageSize = 5,
    style = list(fontFamily = "Fira Sans", fontSize = "12px"),
    columns = list(
      deck          = colDef(name = 'Deck'),
      faccao        = colDef(name = 'Facção'),
      card_in_seq   = colDef(name = 'Sequência'),
      localizedName = colDef(name = 'Carta'),
      repeatCount   = colDef(name = 'Unidades'),
      slug          = colDef(name = 'Facção da Carta'),
      type          = colDef(name = 'Tipo')
    )
  )

Para finalizar, separei os metadados das cartas em uma outra tabela, de forma a termos acesso mais fácil às informações sobre cada uma delas.

Show code
# criando a base de-para de decks
cartas <- decks %>% 
  # removendo colunas que nao tem muito haver com as cartas individualmente
  select(-c(deck, card_in_seq, repeatCount, id, habilidades, faccao)) %>% 
  # pegando as cartas distintas
  distinct() %>% 
  # colocando o nome da carta na frente de tudo
  relocate(localizedName, .before = craftingCost) %>% 
  # colocando em ordem alfabetica por slug
  arrange(slug, localizedName)

# printando a tabela como um reactable
cartas %>% 
  reactable(
    sortable = TRUE, filterable = TRUE, compact = TRUE,
    highlight = TRUE, borderless = TRUE, showPageSizeOptions = TRUE,
    defaultColDef = colDef(align = 'left'), defaultPageSize = 5,
    style = list(fontFamily = "Fira Sans", fontSize = "12px"),
    columns = list(
      localizedName  = colDef(name = 'Carta'),
      craftingCost   = colDef(name = 'Custo de Criação'),
      rarity         = colDef(name = 'Raridade'),
      slug           = colDef(name = 'Facção'),
      cardGroup      = colDef(name = 'Grupo'),
      power          = colDef(name = 'Poder'),
      provisionsCost = colDef(name = 'Custo de Provisão'),
      type           = colDef(name = 'Tipo'),
      armour         = colDef(name = 'Armadura'),
      keywords       = colDef(name = 'Habilidades'),
      texto          = colDef(name = 'Descrição', minWidth = 200)
    )
  )

Com isso, acredito que já dê para ter noção do tipo de informação que temos à nossa disposição.

Análise exploratória dos dados

O primeiro padrão que vamos explorar é quantos decks temos disponíveis para cada uma das 6 facções. De acordo com a figura abaixo, parece haver algum tipo de favoritismo por algumas facções, com a maior parte dos decks pertencendo às facções Nilfgaard e Monsters, e a minoria à facção Syndicate. Acredito que este padrão esteja relacionado com a flexibilidade que seus decks têm em combinarem estratégias de contra-medidas, combos de pontuação e, no caso do Syndicate, a necessidade de quase toda ação depender de um custo em moedas (que é um aspecto único da mecânica de jogo aplicável apenas à esta facção). De toda forma, o fato de haver um diferença na quantidade de decks disponíveis entre as facções sugere que qualquer análise combinando todas elas pode acabar sendo enviesada pelos decks das facções mais comuns. Por conta disso, acredito que o ideal seja realizar todas as análises por facção.

Show code
# criando figura da quantidade de decks na base
decks %>% 
  # pegando os decks distintos
  distinct(deck, faccao) %>% 
  # contando quantos decks existem por faccao
  count(faccao, name = 'observacoes') %>% 
  # plotando a figura
  ggplot(mapping = aes(x = observacoes, y = faccao, fill = faccao)) +
  geom_col(color = 'black', size = 0.2, show.legend = FALSE) +
  geom_text(
    mapping = aes(label = format(x = observacoes, big.mark = '.', decimal.mark = ',')),
    nudge_x = 100,
    size = 3,
    fontface = 'bold'
  ) +
  scale_x_continuous(
    breaks = seq(from = 0, to = 2600, by = 300),
    limits = c(0, 2200)
  ) +
  scale_fill_manual(values = cores_por_faccao) +
  labs(
    title    = 'Quantidade de decks por facção na base de dados',
    subtitle = 'A maior parte dos decks pertence à facção de Nilfgaard, enquanto a minoria a do Sindicato',
    x        = 'Quantidade de decks'
  ) +
  theme(axis.title.y = element_blank())

Estrutura dos decks

Em linha com as regras de montagem dos decks, podemos ver que o tamanho mínimo dos decks é de 25 cartas, bem como a mediana do tamanho dos decks. Apesar disso, é possível ver que existem casos extremos nos quais os decks chegam a ter quase 40 cartas. A similaridade entre o mínimo e a mediana do tamanhos dos decks é bastante plausível: começamos uma partida com 10 cartas na mão e compramos 3 cartas em cada uma das rodadas subsequentes. Assim, cada partida envolve o uso de, pelo menos, 13 (i.e., quando o mesmo jogador vence a primeira e a segunda rodada) à 16 cartas (i.e., quando ocorre o empate nas duas primeiras rodadas e um dos jogadores vence a terceira rodada). Assim, ter 25 cartas no deck aumenta as chances das cartas que precisamos acabarem rapidamente em nossa mão, quando comparado à decks maiores.

Show code
# criando a tabela
decks %>%
  # agrupando por deck
  group_by(faccao, deck) %>% 
  # contando o total de cartas existentes em cada deck, e tirando as cartas obrigatorias
  # correspondente à habilidade do lider e estrategia
  summarise(
    n_cartas = sum(repeatCount, na.rm = TRUE) - 2
  ) %>% 
  # pegando a mediana, o minimo e maximo por faccao
  summarise(
    minimo  = min(n_cartas),
    mediana = median(x = n_cartas),
    maximo  = max(n_cartas)
  )
# A tibble: 6 × 4
  faccao          minimo mediana maximo
  <chr>            <dbl>   <dbl>  <dbl>
1 Monsters            25      25     35
2 Nilfgaard           25      25     38
3 Northern Realms     25      25     36
4 Scoia'tael          25      25     33
5 Skellige            25      25     31
6 Syndicate           25      25     29

Outro padrão importante é que os decks de cada facção tendem a ter 20 à 22 cartas distintas - contando com as cartas de habilidade do líder e de estratégia. Essa diferença para o mínimo de 25 cartas ocorre por conta das cópias das cartas de bronze que podem ocorrer em cada deck. De toda forma, esta informação é relevante pois ela indica que mais vale em focar na diversidade de cartas no deck do que na repetição de cartas. No entanto, ainda não fica claro se a melhor estratégia entre os decks é ter mais cartas do grupo de bronze ou de ouro.

Show code
decks %>%
  # agrupando por deck
  group_by(faccao, deck) %>% 
  # contando o total de cartas existentes em cada deck
  summarise(
    n_cartas = n(),
    .groups = 'drop'
  ) %>% 
  # contando quantas vezes ocorrem cada combinacao
  count(faccao, n_cartas, name = 'observacoes') %>% 
  # criando a figura
  ggplot(mapping = aes(x = n_cartas, y = observacoes, fill = faccao)) +
  facet_wrap(~ faccao, scales = 'free') +
  geom_col(color = 'black', size = 0.3, show.legend = FALSE) +
  scale_x_continuous(breaks = seq(from = 10, to = 40, by = 2)) +
  scale_y_continuous(breaks = seq(from = 0, to = 1000, by = 100)) +
  scale_fill_manual(values = cores_por_faccao) +
  labs(
    title    = 'Distribuição da quantidade de cartas distintas no deck',
    subtitle = 'A maior parte dos decks é composta por 20 à 22 cartas distintas',
    x        = 'Quantidade de cartas distintas',
    y        = 'Observações'
  )

Uma forma de entender qual o melhor mix de cartas é analisando a raridade das cartas e a frequência com a qual elas ocorrem. A figura abaixo revela que as cartas lendárias tendem a representar 30% à 40% das cartas em um deck, padrão comum à todas as facções. Por outro lado, a diferença entre facções está na forma como o mix é balanceado nos demais níveis de raridade. Por exemplo, é bastante claro que as cartas lendárias são as mais frequentes nos decks das facções Scoia'tael e Syndicate, enquanto que os demais níveis de raridade parecem ter representatividades similares (i.e., 60:20:20:20% - cartas lendárias:épicas:raras:comuns, respectivamente). Já os decks da facção Nilfgaard têm uma proporção de cartas em cada nível de raridade que remete a algo como 35:25:23:17%. Do que eu já pude perceber, esta diferença no mix de cartas comuns à épicas parece estar relacionada a onde estão as cartas entre elas que abrigam o restante do motor que faz o deck girar (e.g., muitas das cartas necessárias para fazer os combos da facção Nilfgaard funcionarem dependem das cartas em todos os outros níveis de raridade, enquanto a de Scoia'tael dependem mais das cartas lendárias mesmo).

Show code
decks %>% 
  # removendo as cartas de habilidade de lider e estrategia
  filter(!type %in% c('Leader', 'Stratagem')) %>% 
  # contando o status de raridade das cartas em cada deck de cada faccao
  count(faccao, deck, rarity, name = 'observacoes') %>% 
  # agrupando pelo deck
  group_by(deck) %>% 
  # calculando a proporcao de cartas de cada raridade por deck 
  mutate(
    prop   = observacoes / sum(observacoes)
  ) %>% 
  # desagrupando o tibble
  ungroup %>% 
  # colocando os niveis de raridade em ordem de custo
  mutate(
    rarity = fct_relevel(.f = rarity, 'Common', 'Rare', 'Epic')
  ) %>% 
  # criando a figura
  ggplot(mapping = aes(x = prop, y = rarity, fill = faccao)) +
  facet_wrap(~ faccao, scales = 'free') +
  geom_boxplot(size = 0.3, show.legend = FALSE) +
  scale_fill_manual(values = cores_por_faccao) +
  scale_x_continuous(
    breaks = seq(from = 0, to = 1, by = 0.2),
    labels = scales::percent_format(accuracy = 1)
  ) +
  labs(
    title    = 'Proporção de cartas em cada nível de raridade nos decks de cada facção',
    subtitle = 'Cartas lendárias parecem ser as mais frequentes nos decks para todas as facções',
    caption  = 'Cartas comuns e raras pertencem ao grupo de bronze, cartas épicas e lendárias ao de ouro',
    y        = 'Raridade ou custo de criação da carta',
    x        = 'Proporção de cartas no deck'
  )

Uma vez que o nível de raridade das cartas está relacionado ao seu custo, podemos esperar de antemão que decks compostos principalmente por cartas lendárias e épicas devem ser mais ‘caros’ de serem construídos do que aqueles onde o mix de cartas lendárias é mais balanceado com mais cartas comuns. Seguindo esta expectativa, os decks da facção de Nilfgaard aparentemente parecem ter e/ou atingir os maiores custos de fabricação do jogo. Os decks desta facção são também aqueles que mais aparecem em nossa base de dados e um dos quais mais recebem votos pela comunidade - esta análise está aqui. Ou seja, são decks que aparentemente valem o seu custo.

Show code
decks %>% 
  # agrupando pela faccao e pelo deck
  group_by(faccao, deck) %>% 
  # calculando o custo total de cada deck como o custo de cada carta multiplicado
  # pela quantidade de vezes que aquela carta aparece no deck
  summarise(
    custo = sum(craftingCost * repeatCount),
    .groups = 'drop'
  ) %>% 
  # criando a figura
  ggplot(mapping = aes(x = custo, y = faccao, fill = faccao)) +
  geom_boxplot(size = 0.3, show.legend = FALSE) +
  scale_x_continuous(breaks = seq(from = 0, to = 12000, by = 2000)) +
  scale_fill_manual(values = cores_por_faccao) +
  labs(
    title    = 'Distribuição do custo de fabricação dos decks por facção',
    subtitle = 'Existe alguma similaridade entre as facções em termos do custo de fabricação dos decks',
    x        = 'Custo de fabricação (restos)'
  ) +
  theme(axis.title.y = element_blank())

Outro mix que seria interessante de entender é o das cartas de escolha livre: do mínimo de 23 cartas de escolha livre que precisam precisam estar no deck, pelo menos 13 delas precisam ser do tipo unidade. Assim, para entender como varia o mix de cartas de unidade para não unidade entre os decks, calculei a razão entre a quantidade total de cartas dos dois tipos, de forma que quanto maior o valor desta métrica, mais frequentes são as cartas de unidade em cada deck. Além disso, essa razão nos informa sobre quantas cartas de unidade devemos colocar em um deck para cada outra carta que não pertença a esse tipo (e.g., uma razão de 3 nos diz que para cada 3 cartas de unidade, deve existir 1 carta especial ou de artefato no deck).

Para a minha surpresa, a maior parte dos decks têm de 2 à 6 cartas de Unidade para cada outra carta que não seja deste tipo, a depender da facção. Eu não esperava por isso pois acreditava que cartas como e.g. as especiais não faziam tanta diferença assim para ajudar a alavancar a pontuação, e que seus efeitos eram mais para contra-medidas do que qualquer outra coisa. Todavia, depois que vi esse resultado e parei para pensar, ficou claro o porquê disso: eu não preciso alavancar minha pontuação quando eu posso evitar que a pessoa oponente alavanque a dela. Isso já é algo que vou colocar para dentro dos meus decks.

Show code
decks %>% 
  # removendo as cartas de habilidade de lider e estrategia
  filter(!type %in% c('Leader', 'Stratagem')) %>% 
  # agrupando pela identidade do deck e faccao
  group_by(deck, faccao) %>% 
  # sumarizando a contagem dos tipos de cartas
  summarise(
    # somando as cartas do tipo unidade
    unidades     = sum(type == 'Unit'),
    # somando as cartas cujo tipo é diferente de unidade
    nao_unidades = n() - unidades,
    # calculando a razao de cartas de unidades e nao unidade em cada deck
    razao        = unidades / nao_unidades,
    # desagrupando o dataframe
    .groups = 'drop'
  ) %>% 
  # substituindo os infinitos 
  mutate(
    razao = ifelse(test = is.infinite(x = razao), yes = unidades, no = razao)
  ) %>% 
  # criando a figura
  ggplot(mapping = aes(x = razao, y = faccao, fill = faccao)) +
  geom_boxplot(size = 0.3, show.legend = FALSE) +
  scale_x_continuous(breaks = seq(from = 0, to = 40, by = 2), trans = 'sqrt') +
  scale_fill_manual(values = cores_por_faccao) +
  labs(
    title     = 'Proporção de unidades para outros tipos de carta no deck por facção',
    subtitle  = 'Os valores abaixo representam a quantidade de cartas do tipo unidade para cada 1 carta que não seja deste tipo nos decks de cada\nfacção. Quanto maiores forem estes valores, maior a proporção de unidades no deck, em comparação aos outros tipos de carta.',
    x         = 'Razão entre a quantidade de unidades e outros tipos de carta no deck'
  ) +
  theme(axis.title.y = element_blank())

Por fim, vamos olhar de que forma é dado o mix de cartas por facção entre cada uma delas. A figura abaixo mostra que quase 80% das cartas entre todos os decks de cada facção tendem a ser da própria facção, e que quando consideramos apenas a facção do Syndicate este número sobe para quase 95% (o que faz sentido, já que os decks desta facção só podem ser compostos por cartas dela própria ou neutras). Cartas do Syndicate tendem a ser bem raras nos decks das demais facções, com as cartas neutras normalmente complementando os 15% à 20% do restante de cartas nos decks. Ou seja, os decks parecem não parar de pé por si só com base nas cartas das suas próprias facções.

Show code
decks %>% 
  # removendo a carta de habilidade de lider
  filter(type != 'Leader') %>% 
  # contando quantas vezes as cartas de cada faccao aparecem 
  # nos decks de cada outra faccao
  count(faccao, slug, name = 'ocorrencias') %>% 
  # organizando os niveis do fator slug
  mutate(
    slug = fct_relevel(.f = slug, 'Neutral', 'Syndicate')
  ) %>% 
  # criando a figura
  ggplot(mapping = aes(x = ocorrencias, y = faccao, fill = slug)) +
  geom_bar(position = 'fill', stat = 'identity', color = 'black', size = 0.3, show.legend = FALSE) +
  scale_fill_manual(values = c(cores_por_faccao, 'Neutral' = 'burlywood4')) +
  scale_x_continuous(
    breaks = seq(from = 0, to = 1, by = 0.2),
    labels = scales::percent_format(accuracy = 1)
  ) +
  labs(
    title    = 'Proporção de cartas de cada facção entre os decks de cada outra facção',
    subtitle = "Cartas neutras (marrom) são igualmente frequentes entre os decks de praticamente todas as facções,
e cartas do Sindicato (laranja) aparentam ser um pouco mais frequentes nos decks da facção Scoia'tael",
    caption  = 'Cada barra representa os decks de uma dada facção, enquanto cada porção preenchida de uma barra representa
a proporção de cartas de cada outra facção encontradas nos decks daquela facção. O mapa de cores das
facções segue aquele utilizado até aqui.',
    x        = 'Proporção de cartas'
  ) +
  theme(axis.title.y = element_blank())

Acredito que quanto à estrutura dos decks já temos informações suficientes para começar a mudar um pouco a estratégia de jogo. Vamos olhar agora a composição de cartas nos decks.

Composição dos decks

Seguindo a lógica da montagem dos decks de Gwent, a primeira coisa que vamos olhar são as habilidades do líder mais frequentemente utilizadas nos decks de cada facção. A figura abaixo sumariza a quantidade de decks que têm cada uma das habilidades do líder para cada facção. É possível perceber que existem preferências claras por certas habilidades do líder para a maior parte das facções, excetuando-se Scoia'tael e Skellige. Não tenho muito o que falar sobre esses padrões, a não ser que ver aquele para a facção Northern Realms me fez mudar da Inspiração Real para Zelo Inspirado pois, de fato, o primeiro não fazia muito sentido para as cartas que tinha no meu deck.

Show code
## criando a figura
fig <- decks %>% 
  # filtrando apenas as cartas de habilidade de lider
  filter(type == 'Leader') %>% 
  # contando em quantos decks cada carta aparece
  count(faccao, localizedName, name = 'n_decks') %>% 
  # agrupando pela faccao
  group_by(faccao) %>% 
  # juntando a quantidade de decks
  left_join(
    y = distinct(decks, deck, faccao) %>% 
      count(faccao, name = 'total'),
    by = 'faccao'
  ) %>% 
  # ajustando algumas informacoes
  mutate(
    # calculando a proporcao de decks que contem cada carta
    proporcao     = n_decks / total,
    # colocando o nome original em um string
    original = localizedName,
    # reordenando os nomes das cartas
    localizedName = reorder_within(x = localizedName, by = proporcao, within = faccao)
  ) %>% 
  # juntando texto das habilidades
  left_join(y = select(cartas, localizedName, texto), by = c('original' = 'localizedName')) %>% 
  # criando a figura
  ggplot(mapping = aes(x = proporcao, y = localizedName, fill = faccao, 
                       text = paste0('<b>Habilidade do líder:</b> ', original, '<br>',
                                     '<b>Decks: </b>', n_decks, '<br>',
                                     '<b>Proporção dos decks:</b> ', scales::percent(x = proporcao, accuracy = 0.1), '<br>',
                                     '<b>Descrição:</b> ', str_wrap(string = texto, width = 40)))
  ) +
  facet_wrap(~ faccao, scales = 'free') +
  geom_col(color = 'black', size = 0.3, show.legend = FALSE) +
  scale_fill_manual(values = cores_por_faccao) +
  scale_y_reordered() +
  scale_x_continuous(
    breaks = seq(from = 0, to = 0.8, by = 0.1),
    labels = scales::percent_format(accuracy = 1)
  ) +
  labs(
    title    = 'Habilidades do líder mais populares entre os decks de cada facção',
    x        = 'Proporção dos decks com a habilidade do líder'
  ) +
  theme(legend.position = 'none',
        axis.title.y = element_blank())
ggplotly(p = fig, tooltip = 'text', height = 500, width = 1000)

O próximo grupo de cartas que olharemos serão as cartas de estratégia. Os padrões apresentados na figura abaixo sugerem que a carta de estratégia Crânio de Cristal parece ser uma boa escolha, dada a frequência com a qual esta carta aparece entre os decks de todas as facções. Entretanto, quando falamos da facção do Syndicate, parece que a melhor escolha é a carta de estratégia da própria facção, Olho de Tigre. O principal motivo para isso é que a facção do Syndicate é a única que tem as moedas como parte da mecânica de jogo, e àquela carta de estratégia é a única que conversa com ela. Aqui já tenho mais uma mudança para fazer nos meus decks: adquirir e colocar a carta de estratégia Crânio de Cristal nos meus decks.

Show code
## criando a figura
fig <- decks %>% 
  # filtrando apenas as cartas de estrategia
  filter(type == 'Stratagem') %>% 
  # contando em quantos decks cada carta aparece
  count(faccao, localizedName, name = 'n_decks') %>% 
  # agrupando pela faccao
  group_by(faccao) %>% 
  # juntando a quantidade de decks
  left_join(
    y = distinct(decks, deck, faccao) %>% 
      count(faccao, name = 'total'),
    by = 'faccao'
  ) %>% 
  # ajustando algumas informacoes
  mutate(
    # calculando a proporcao de decks que contem cada carta
    proporcao     = n_decks / total,
    # colocando o nome original em um string
    original = localizedName,
    # reordenando os nomes das cartas
    localizedName = reorder_within(x = localizedName, by = proporcao, within = faccao)
  ) %>% 
  # juntando texto da estrategia
  left_join(y = select(cartas, localizedName, texto, slug), by = c('original' = 'localizedName')) %>% 
  # criando a figura
  ggplot(mapping = aes(x = proporcao, y = localizedName, fill = faccao, 
                       text = paste0('<b>Estratégia:</b> ', original, '<br>',
                                     '<b>Facção da estratégia: </b>', slug, '<br>',
                                     '<b>Decks: </b>', n_decks, '<br>',
                                     '<b>Proporção dos decks:</b> ', scales::percent(x = proporcao, accuracy = 0.1), '<br>',
                                     '<b>Descrição:</b> ', str_wrap(string = texto, width = 40)))
  ) +
  facet_wrap(~ faccao, scales = 'free') +
  geom_col(color = 'black', size = 0.3, show.legend = FALSE) +
  scale_fill_manual(values = cores_por_faccao) +
  scale_y_reordered() +
  scale_x_continuous(
    breaks = seq(from = 0, to = 0.8, by = 0.1),
    labels = scales::percent_format(accuracy = 1)
  ) +
  labs(
    title    = 'Cartas de estratégia mais populares entre os decks de cada facção',
    x        = 'Proporção dos decks que usam a estratégia'
  ) +
  theme(legend.position = 'none',
        axis.title.y = element_blank())
ggplotly(p = fig, tooltip = 'text', height = 500, width = 1000)

Passando agora para as cartas de escolha livre de cada uma das facções, podemos observar dois padrões interessantes. O primeiro deles é que existem muitas cartas especiais entre elas, o que reforça o padrão observado anteriormente que todo deck precisa ter uma determinada quantidade de cartas que não sejam do tipo unidade. Outro padrão interessante é que existem muitas cartas comuns e raras neste ranking, com a minoria pertencendo ao nível lendário. Achei isso bastante inesperado, dado o padrão de que as cartas lendárias tendem a ser as mais frequentes entre os decks. Isto parece sugerir que deve existir uma diferença maior na composição das cartas lendárias usadas em cada deck do que na de cartas comuns e raras (i.e., temos sempre o mesmo conjunto das últimas, mas diferentes conjuntos de cartas lendárias entre os decks de cada facção). De toda forma, dois insights que já me são úteis aqui: (1) este ranking me indica as cartas que preciso ter de cada facção, de forma a conseguir (possivelmente) reproduzir a maior parte dos decks existentes e (2) eu preciso adicionar a carta Assaltante Sagaz da facção Northern Realms aos meus decks (eu já tinha essa carta, mas ignorava ela totalmente).

Show code
## criando a figura
fig <- decks %>% 
  # pegando apenas as cartas que pertençam ao deck da propria faccao
  filter(slug == faccao) %>% 
  # removendo as cartas de habilidade de lider e estrategia
  filter(!type %in% c('Leader', 'Stratagem')) %>% 
  # contando em quantos decks cada carta aparece
  count(faccao, localizedName, name = 'n_decks') %>% 
  # agrupando pela faccao
  group_by(faccao) %>% 
  # pegando as 15 cartas mais comuns por faccao
  top_n(n = 15, wt = n_decks) %>% 
  # juntando a quantidade de decks
  left_join(
    y = distinct(decks, deck, faccao) %>% 
      count(faccao, name = 'total'),
    by = 'faccao'
  ) %>% 
  # ajustando algumas informacoes
  mutate(
    # calculando a proporcao de decks que contem cada carta
    proporcao     = n_decks / total,
    # colocando o nome original em um string
    original = localizedName,
    # reordenando os nomes das cartas
    localizedName = reorder_within(x = localizedName, by = proporcao, within = faccao)
  ) %>% 
  # juntando texto das cartas
  left_join(y = select(cartas, localizedName, texto, rarity, type), 
            by = c('original' = 'localizedName')) %>% 
  # criando a figura
  ggplot(mapping = aes(x = proporcao, y = localizedName, fill = faccao, 
                       text = paste0('<b>Carta:</b> ', original, '<br>',
                                     '<b>Raridade:</b> ', rarity, '<br>',
                                     '<b>Tipo:</b> ', type, '<br>',
                                     '<b>Decks: </b>', n_decks, '<br>',
                                     '<b>Proporção dos decks:</b> ', scales::percent(x = proporcao, accuracy = 0.1), '<br>',
                                     '<b>Descrição:</b> ', str_wrap(string = texto, width = 40)))
  ) +
  facet_wrap(~ faccao, scales = 'free') +
  geom_col(color = 'black', size = 0.3, show.legend = FALSE) +
  scale_fill_manual(values = cores_por_faccao) +
  scale_y_reordered() +
  scale_x_continuous(
    breaks = seq(from = 0, to = 0.8, by = 0.2),
    labels = scales::percent_format(accuracy = 1)
  ) +
  labs(
    title    = 'As 15 cartas mais frequentes entre os decks de cada facção',
    subtitle = 'Cada painel apresenta as 15 cartas que aparecem com mais frequência entre os decks das respectivas facções',
    x        = 'Proporção de decks com a carta'
  ) +
  theme(legend.position = 'none',
        axis.title.y = element_blank())
ggplotly(p = fig, tooltip = 'text', height = 500, width = 1000)

Eu não considerei as cartas neutras utilizadas junto de cada deck na análise acima, uma vez que a minha ideia foi olhar as cartas de escolha livre das facções. Além disso, como as cartas neutras podem ser usadas nos decks de qualquer facção, optei por olhá-las em separado, a fim de identificar as cartas neutras que seriam mais coringas e àquelas que parecem ter um ajuste melhor à cada facção. Os resultados desta análise mostram que as cartas Oniromancia e Korathi Chamardente parecem ser as escolhas coringa: elas estão entre as cartas mais frequentes nos decks de todas as facções. Existem outras cartas que aparecem com alguma frequência entre as facções, apesar de não serem tão coringas, como Maxii Van Dekkar e Decreto Real. Finalmente, notei que existem muitas cartas neutras que são bruxos no deck da facção Northern Realms, possivelmente pelo ajuste que estas cartas têm à mecânica dessa facção. Esse padrão também é interessante, pois existem modos de jogo sazonais em que ter muitos bruxos no deck é vantajoso.

Show code
## criando a figura
fig <- decks %>% 
  # pegando apenas as cartas que pertençam ao deck da propria faccao
  filter(slug == 'Neutral') %>% 
  # removendo as cartas de habilidade de lider e estrategia
  filter(!type %in% c('Leader', 'Stratagem')) %>% 
  # contando em quantos decks cada carta aparece
  count(faccao, localizedName, name = 'n_decks') %>% 
  # agrupando pela faccao
  group_by(faccao) %>% 
  # pegando as 15 cartas mais comuns por faccao
  top_n(n = 15, wt = n_decks) %>% 
  # juntando a quantidade de decks
  left_join(
    y = distinct(decks, deck, faccao) %>% 
      count(faccao, name = 'total'),
    by = 'faccao'
  ) %>% 
  # ajustando algumas informacoes
  mutate(
    # calculando a proporcao de decks que contem cada carta
    proporcao     = n_decks / total,
    # colocando o nome original em um string
    original = localizedName,
    # reordenando os nomes das cartas
    localizedName = reorder_within(x = localizedName, by = proporcao, within = faccao)
  ) %>% 
  # juntando texto das cartas
  left_join(y = select(cartas, localizedName, texto, rarity, type), 
            by = c('original' = 'localizedName')) %>% 
  # criando a figura
  ggplot(mapping = aes(x = proporcao, y = localizedName, fill = faccao, 
                       text = paste0('<b>Carta:</b> ', original, '<br>',
                                     '<b>Raridade:</b> ', rarity, '<br>',
                                     '<b>Tipo:</b> ', type, '<br>',
                                     '<b>Decks: </b>', n_decks, '<br>',
                                     '<b>Proporção dos decks:</b> ', scales::percent(x = proporcao, accuracy = 0.1), '<br>',
                                     '<b>Descrição:</b> ', str_wrap(string = texto, width = 40)))
  ) +
  facet_wrap(~ faccao, scales = 'free') +
  geom_col(color = 'black', size = 0.3, show.legend = FALSE) +
  scale_fill_manual(values = cores_por_faccao) +
  scale_y_reordered() +
  scale_x_continuous(
    breaks = seq(from = 0, to = 0.8, by = 0.1),
    labels = scales::percent_format(accuracy = 1)
  ) +
  labs(
    title    = 'As 15 cartas neutras mais frequentes entre os decks de cada facção',
    subtitle = 'Cada painel apresenta as 15 cartas neutras que aparecem com mais frequência entre os decks de cada facção',
    x        = 'Proporção de decks com a carta'
  ) +
  theme(legend.position = 'none',
        axis.title.y = element_blank())
ggplotly(p = fig, tooltip = 'text', height = 500, width = 1000)

Acho que já temos bastante informação o suficiente até aqui. Vamos passar à parte principal deste post: entender os padrões de co-ocorrência das cartas entre os decks através de uma análise de regras de associação.

Explorando as regras de associação

Vou utilizar um algoritmo de mineração de regras de associação para ajudar a identificar os padrões de co-ocorrência das cartas entre os decks de cada facção. Estes são algoritmos de aprendizado não-supervisionado, frequentemente utilizados para encontrar padrões de associação entre items a partir de grandes bases de dados de compras (i.e., uma prática conhecida como market basket analysis - ou, em português, análise de cesta de compras). De uma forma geral, estes algoritmos não são utilizados para a previsão per se, mas para extrair insights quando não temos nenhum tipo de conhecimento prévio dos padrões que estamos buscando.

A família de algoritmos de regras de associação utilizam os dados de cada transação (i.e., um conjunto de itens vindo de uma interação) para gerar uma coleção de regras de associação com a forma {LHS}{RHS}. Nesta notação o LHS descreve as condições que precisam ser atendidas pela regra, e o RHS é o resultado observado quando ela é atendida. Uma regra é então dada pela combinação de cada LHS com seu respectivo RHS, de forma que o LHS pode ser representado por um ou mais itens, enquanto o RHS é representado por apenas um item.

No contexto deste post, vamos considerar que o conjunto de cartas encontradas em cada um dos decks representa uma transação diferente, e buscaremos aprender quais são as combinações de cartas mais frequentemente encontradas entre os decks. Portanto, uma das limitações desta análise é que estaremos restritos à encontrar associações já conhecidas entre cartas, mas não àquelas que poderiam ser criadas de acordo com algum aspecto ainda não explorado da interação entre elas. Apesar disso, acredito que a abordagem que vamos utilizar já é um bom pontapé inicial para nos ajudar a montar decks de forma um pouco mais direcionada pelos dados.

Preparação dos dados

O pacote arules (Hahsler et al. (2021)) implementa diversos algoritmos para a mineração de regras de associação, e vamos utilizá-lo para esta tarefa aqui. Depois de carregar este pacote, a etapa seguinte é modificar a estrutura tidy dos dados para àquela característica das transações - transactions, a estrutura de dados utilizada pelo pacote. Esta estrutura de dados pode ser parseada através de uma de duas formas:

Dentre as duas opções, escolhi trabalhar com a lista, uma vez que acredito que ela ocupape menos espaço do que àquela da matriz esparsa. Além disso, é preciso lembrar que vamos determinar as regras de associação separadamente para cada facção, uma vez que: (a) parte do funcionamento do algoritmo depende da frequência com a qual cada regra ocorre, e temos diferentes quantidade de deck por facção (logo, isto acabaria enviesando a descoberta das regras para uma ou outra facção em específico) e (b) as cartas neutras podem ser usadas nos decks de todas as facções, e pode ser que hajam regras específicas entre estas cartas para uma facção e não outra. Assim, o pedaço de código abaixo cria uma lista com as cartas em cada deck para cada facção, e as organiza como uma list-column dentro de um tibble. Alguns exemplos do formato desta lista para os decks da facção de Nilfgaard é apresentado abaixo.

# carregando mais pacotes
library(arules) # para as analises

# criando a estrutura de dados necessarias para criar os dados das transacoes
regras_por_faccao <- decks %>% 
  # pegando apenas as colunas alvo
  select(faccao, deck, localizedName) %>% 
  # colocando cartas em ordem alfabetica por deck
  arrange(deck, localizedName) %>% 
  # aninhando os dados por faccao
  nest(data = -faccao) %>% 
  # criando named list column para cada deck
  mutate(
    data = map(.x = data, 
               # pega cada um dos conjuntos de dados de cada faccao e agrupa pelo deck id
               .f = ~ group_by(.x, deck) %>% 
                 # transforma a coluna com o nome das cartas por deck em uma lista
                 summarise(cartas = list(localizedName)) %>% 
                 # seta o nome de cada uma das listas de cartas para o deck id
                 mutate(cartas = setNames(object = cartas, nm = deck)) %>% 
                 # extrai a named list column
                 pull(cartas)
    )
  ) 

# exemplo da estrutura de dados
regras_por_faccao %>% 
  slice(3) %>% 
  pull(data) %>% 
  pluck(1) %>% 
  .[1:3]
$`61930`
 [1] "Besteiro Ard Feainn"      "Caçador de escravos"     
 [3] "Caçador de Van Moorlehem" "Diplomacia Imperial"     
 [5] "Falcatrua"                "Informante da Duquesa"   
 [7] "Lança obra-prima"         "Nilfgaard: Cavaleiro"    
 [9] "Piqueiro Alba"            "Remédio Experimental"    
[11] "Sargento Nauzicaa"        "Tartaruga Ard Feainn"    
[13] "Urso Ancião"              "Vanguarda da Caravana"   
[15] "Vantagem Tática"         

$`66256`
 [1] "Aprisionamento"            "Caçador de Van Moorlehem" 
 [3] "Cantarella"                "Damien de la Tour"        
 [5] "Decreto Real"              "Divisão Magne"            
 [7] "Esporos"                   "Ffion var Gaernel"        
 [9] "Golem Imperial"            "Invocação de Yennefer"    
[11] "Isbel de Hagge"            "Letho: Regicida"          
[13] "Matta Hu'uri"              "Peleja do torneio"        
[15] "Regicida"                  "Stregobor"                
[17] "Tibor Eggebracht"          "Traheaern var Vdyffir"    
[19] "Trovador: Falcão de Ferro" "Vantagem Tática"          
[21] "Vilgefortz"               

$`81517`
 [1] "Artorius Viggo"           "Caçador de escravos"     
 [3] "Caçador de Van Moorlehem" "Cahir Dyffryn"           
 [5] "Cavalaria Pesada de Ard"  "Colar"                   
 [7] "Cynthia"                  "Decreto Real"            
 [9] "Ffion var Gaernel"        "Glynnis aep Loernach"    
[11] "Guarda Ducal"             "Impostor"                
[13] "Informante da Duquesa"    "Joachim de Wett"         
[15] "Matta Hu'uri"             "Menno Coehoorn"          
[17] "Renovar"                  "Roderick de Dun Tynne"   
[19] "Suborno"                  "Vanhemar"                
[21] "Vidente Imperial"        

Uma vez que tenhamos a estrutura de dados neste formato para cada facção, podemos parseá-la para um transaction e colocar o resultado disto em outra coluna do tibble. Após fazer isto, apresento mais uma vez o resultado do objeto criado para a facção de Nilfgaard. O output de um objeto da classe transaction traz algumas informações relevantes, tais como: a esparsidade dos dados, os itens mais frequentes, a distribuição de frequência do tamanho das transações (i.e., decks) e, quando existente, as informações sobre os metadados dos itens e/ou transações.

# parser das listas para as transacoes
regras_por_faccao <- regras_por_faccao %>% 
  # cria as transacoes a partir de cada uma das listas
  mutate(
    transacoes = map(.x = data, .f = transactions)
  )

# visualiza o resultado como a transacao para uma das faccoes
regras_por_faccao %>% 
  slice(3) %>% 
  pull(transacoes) %>% 
  pluck(1) %>% 
  summary()
transactions as itemMatrix in sparse format with
 2087 rows (elements/itemsets/transactions) and
 363 columns (items) and a density of 0.06011502 

most frequent items:
        Coup de Grace Roderick de Dun Tynne Informante da Duquesa 
                 1087                   979                   963 
Invocação de Yennefer             Braathens               (Other) 
                  914                   879                 40720 

element (itemset/transaction) length distribution:
sizes
 15  17  18  19  20  21  22  23  24  25  26  27 
  2   3   6  32 232 628 708 306  82  15  11  62 

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  15.00   21.00   22.00   21.82   22.00   27.00 

includes extended item information - examples:
                   labels
1 A Terra das Mil Fábulas
2         A Trufa Carnuda
3        Adaga Cerimonial

includes extended transaction information - examples:
  transactionID
1         61930
2         66256
3         81517

Com os dados prontos, vamos agora ao ajuste do algoritmo.

Ajuste do algoritmo

Vou utilizar o algoritmo apriori para identificar as regras de associação entre as cartas para os decks de cada facção (Agrawal and Srikant (1994)). Este algoritmo funciona em duas etapas: na primeira, ele identifica todos os itens e combinações de itens que atendem a um critério mínimo de inclusão e, na segunda etapa, ele cria as regras a partir destes conjuntos. A primeira parte do funcionamento deste algoritmo baseia-se na premissa definida a priori (daí o nome do algoritmo) de que todas as combinações frequentes de itens o são porquê os itens que os compõem também devem ser frequentes (i.e. se {LHS, RHS} é frequente, então {LHS} e {RHS} também devem ser). Isto é importante porque muitas das potenciais combinações entre os itens examinados não ocorrem ou são muito raras, de forma que examinar todas elas simultaneamente acabaria adicionando muito pouco valor em comparação à focar apenas as combinações mais frequentes. Desta forma, o algoritmo limita o escopo da busca pelas regras sobre um conjunto mais tangível dos itens, que (possivelmente) trará regras mais gerais e úteis do que àquelas que seriam dadas pelos itens menos frequentes (e, portanto, seriam mais as exceções do que regras).

A definição do quão frequente é um item (ou combinações de itens) é uma escolha que nós devemos fazer antes de ajustar o algoritmo aos dados, através da métrica de suporte. Esta métrica descreve a proporção de vezes que o LHS e o RHS ocorrem juntos no conjunto de dados, ou seja, \(P(LHS \bigcap RHS)\). Por exemplo, se temos um par de cartas que ocorrem juntas em 10 de 20 decks examinados, então o suporte para a regra entre as duas é de 0.5 - além disso, note que o suporte terá o mesmo valor para este par independentemente da ordem examinada i.e. \(P(LHS \bigcap RHS)\) = \(P(RHS \bigcap LHS)\). Usando esta lógica, se definirmos um suporte mínimo de 0.1 para a mineração de regras, então apenas as regras que envolvam itens que apareçam em pelo menos 10% das transações é que serão consideradas. Este é um dos motivos pelos quais não podemos ajustar este algoritmo para os decks de todas as facções simultaneamente: com cerca de 9 mil decks, se utilizássemos um suporte mínimo de 0.1, uma regra só seria considerada se ela aparecesse em no mínimo 900 decks, aproximadamente; enquanto isto seria fácil para as regras vindas dos decks da facção e.g. Nilfgaard, dificilmente seria atendido pelos decks da facção Syndicate2.

Ainda que utilizemos um ponto de corte para definir o escopo de busca pelo algoritmo, isto ainda assim pode acabar gerando muitas regras para analisarmos. Assim, existem três maneiras complementares de podarmos o resultado da busca feita pelo algoritmo. A primeira delas é definindo um valor mínimo que cada regra deve atingir para a métrica de confiança (i.e., confidence), que descreve a proporção de vezes que a regra {LHS} → {RHS} ocorre condicionada às vezes que observamos {LHS} (i.e., \(\frac{P(LHS \bigcap RHS)}{P(LHS)}\)). A segunda forma é aumentando o número mínimo de itens nas regras que vamos buscar: por padrão, o algoritmo busca regras que contenham pelo menos 1 item i.e. {} → {A}, o que pode não fazer sentido na maior parte dos casos. Finalmente, a terceira forma de reduzir os resultados da busca, é definindo um limite superior para o número de itens que podem estar nas regras. Aumentar o valor da confiança ou do mínimo de itens nas regras, bem como reduzir o limite máximo de itens, faz com que tenhamos um conjunto de regras mais concisas, além de fazer com que o algoritmo rode mais rápido.

No nosso caso, vou utilizar um patamar mínimo para o suporte de 0.1 (i.e., as regras devem aparecer em pelo menos 10% dos decks de cada facção) e para a confiança de 0.3 (i.e., a regra deve valer para pelo menos 30% das vezes que o LHS ocorre). Além disso, como estou interessado nas regras que envolvam pelo menos pares de cartas, vou definir que o número mínimo de itens nas regras é de 2. Por fim, dado que a maior parte dos decks entre todas as facções tem 20 à 22 cartas únicas, vou limitar o máximo de cartas nas regras à 10. O código abaixo cuida de ajustar o algoritmo apriori para as transações de cada facção, armazenando o resultado como mais uma coluna no tibble.

# ajustando o algoritmo a priori aos dados de cada faccao
regras_por_faccao <- regras_por_faccao %>% 
  mutate(
    regras = map(.x = transacoes, .f = apriori,
                 # pelo menos 2 cartas e no maximo 10 cartas em cada regra 
                 # cada regra deve cobrir no minimo 10% dos casos que ocorrem
                 # na base de dados, e regras detectadas devem representar
                 # no minimo 30% dos casos em que a condicao do LHS ocorre
                 parameter = list(minlen = 2, maxlen = 10, support = 0.1, 
                                  conf = 0.3, target = 'rules'), 
                 # desligando a verbosidade do algoritmo
                 control  = list(verbose = FALSE)
    )
  )
regras_por_faccao
# A tibble: 6 × 4
  faccao          data                 transacoes       regras 
  <chr>           <list>               <list>           <list> 
1 Scoia'tael      <named list [1,618]> <trnsctns[,356]> <rules>
2 Monsters        <named list [1,835]> <trnsctns[,348]> <rules>
3 Nilfgaard       <named list [2,087]> <trnsctns[,363]> <rules>
4 Northern Realms <named list [1,444]> <trnsctns[,360]> <rules>
5 Skellige        <named list [1,721]> <trnsctns[,338]> <rules>
6 Syndicate       <named list [970]>   <trnsctns[,302]> <rules>

Extraindo os resultados

Algoritmo ajustado, agora vou tratar os resultados para facilitar a análise dos padrões encontrados. A primeira coisa que vou fazer vai ser calcular a dissimilaridade entre as regras, de forma à entender o quão diferentes elas são. Para isso, vou utilizar a função dissimilarity do pacote arules, usando como métrica de distância àquela proposta por Gupta, Strehl, and Ghosh (1999). Esta métrica quantifica a diferença entre regras de acordo com a quantidade de transações em que ambas ocorrem: se duas regras ocorrem juntas em diversas transações, então a distância entre elas tende a zero; por outro lado, se duas regras raramente ocorrem juntas na mesma transação, então a distância entre elas tende a 1. Desta forma, regras que estejam mais próximas entre si em um espaço multidimensional seriam àquelas que mais frequentemente co-ocorrem entre as transações.

# calculando a dissimilaridade entre as regras para cada faccao
regras_por_faccao <- regras_por_faccao %>% 
  mutate(
    dissimilaridade = map2(.x = regras, 
                           .y = transacoes, 
                           .f = ~ dissimilarity(x = .x, 
                                                method = 'gupta',
                                                args = list(transactions = .y)
                           )
    )
  )

Uma vez que tenhamos a matriz de distância calculada, podemos utilizar uma PCoA (Principal Coordinate Analysis) para projetar esta matriz em um espaço 2D, e visualizar a diferença entre as regras. A PCoA é muito parecida com a PCA (Principal Component Analysis), sendo muito útil quando queremos utilizar uma distância diferente da euclidiana para analisar um conjunto de dados no espaço multivariado. Para ajustar este algoritmo de redução de dimensionalidade, vou carregar o pacote vegan e utilizar a função cmdscale. O resultado desta função, por padrão, é uma matriz com as coordenadas x e y de cada regra no espaço 2D.

# carregando mais pacotes
library(vegan) # para a ordenacao

# ajusta a PCoA e extrai os escores para a projecao
regras_por_faccao <- regras_por_faccao %>% 
  mutate(
    # ajustando a PCoA
    pcoa = map(.x = dissimilaridade, .f = cmdscale),
    # setando o nome das colunas com os escores da PCoA
    pcoa = map(.x = pcoa, .f = ~ `colnames<-`(x = .x, value = c('x', 'y')))
  )

A próxima coisa que faremos será extrair uma flag que nos diz se cada regra é um conjunto frequente máximo ou não (maximal frequent itemset, MFI). Esta é uma propriedade das regras compostas por conjuntos de itens frequentes cujas combinações com outros itens não venham a ser frequentes, e é uma característica que pode ser utilizada para identificar o tamanho máximo de regras únicas. Por exemplo, se {A, B} e {B, C} são regras frequentes, mas {A, B, C} não é uma regra frequente, então {A, B} e {B, C} são tidas como um conjunto frequente máximo.

Finalmente, o passo final do tratamento dos dados será criar uma tabela tidy com as regras e os metadados associados à cada uma das facções. Para isso, vou extrair o dataframe que contém as regras, adicionar a flag indicando se a regra é uma conjunto frequente máximo e, também, as coordenadas x e y da regra no espaço 2D conforme obtidas através da PCoA. Note que faremos separadamente para cada facção, resultando um uma coluna com 6 linhas, cada uma das quais contendo um tibble com as regras para cada facção.

# extraindo informacoes das regras
regras_por_faccao <- regras_por_faccao %>% 
  # extraindo informacoes a partir das regras
  mutate(
    # definindo se cada uma das regras eh um maximal frequent itemset
    maximal_itemset = map(.x = regras, .f = is.maximal),
    # extraindo as regras como um dataframe
    regras_df       = map(.x = regras, .f = DATAFRAME,
                          setStart = '', setEnd = '', itemSep = ';'),
    # adicionando ao data frame das regras a informacao se cada linha eh um
    # maximal frequent itemset out nao
    regras_df       = map2(.x = regras_df, .y = maximal_itemset, 
                           .f = ~ mutate(.x, maximal = .y)
    ),
    # adicionando x e y de cada regra na PCoA 
    regras_df       = map2(.x = regras_df, .y = pcoa, .f = cbind)
  )
regras_por_faccao
# A tibble: 6 × 8
  faccao          data  transacoes regras dissimilaridade pcoa  maximal_itemset
  <chr>           <lis> <list>     <list> <list>          <lis> <list>         
1 Scoia'tael      <nam… <trnsctns… <rule… <dist [19,124,… <dbl… <lgl [6,185]>  
2 Monsters        <nam… <trnsctns… <rule… <dist [921,403… <dbl… <lgl [1,358]>  
3 Nilfgaard       <nam… <trnsctns… <rule… <dist [4,114,1… <dbl… <lgl [2,869]>  
4 Northern Realms <nam… <trnsctns… <rule… <dist [2,807,2… <dbl… <lgl [2,370]>  
5 Skellige        <nam… <trnsctns… <rule… <dist [1,185,0… <dbl… <lgl [1,540]>  
6 Syndicate       <nam… <trnsctns… <rule… <dist [29,525,… <dbl… <lgl [7,685]>  
# … with 1 more variable: regras_df <list>

Uma vez que chegamos naquela estrutura de dados, podemos criar uma tabela totalmente tidy com as regras para todas as facções, o que faz com que a nossa vida fique mais fácil para entender os padrões encontrados entre todas elas.

# colocando as regras encontradas em um dataframe
df_regras <- regras_por_faccao %>% 
  # selecionando apenas as colunas necessarias
  select(faccao, regras_df) %>% 
  # desaninhando a list column com o dataframe das regras
  unnest(regras_df) %>% 
  # ajustando o dataframe
  mutate(
    # parseando as colunas de fator para caractere
    LHS        = as.character(LHS),
    RHS        = as.character(RHS),
    # definindo o tamanho de cada regra: cada ';' separa duas cartas dentro de uma
    # regra no LHS, e ainda temos a regra no RHS. Portanto, o tamanho de cada regra
    # são 2 acrescido da quantidade de separadores existentes no LHS
    tamanho    = str_count(string = LHS, pattern = ';') + 2,
    # criando identificador unico para cada deck
    combinacao = str_split(string = paste0(LHS, ';', RHS), pattern = ';'),
    combinacao = map_chr(.x = combinacao, .f = ~ paste0(sort(.x), collapse = ' + '))
  )

Pronto!

Resultados

Visão geral

A figura abaixo demonstra que grande parte das regras encontradas envolvem 3 ou 4 cartas, apesar de existirem algumas regras que chegam a incorporar 7 ou 8 cartas. De toda forma, parece existir diferenças entre as facções em torno do tamanho mais frequente das regras: 2 cartas são as mais frequentes para a facção Monsters, 3 cartas para as facções Northern Realms e Skellige, e 4 cartas para Scoia'tael e Syndicate. A facção de Nilfgaard foi a única em que parece haver uma proporção similar de regras com 3 e 4 quartas.

Show code
# criando figura para entender quantas regras foram detectadas 
df_regras %>% 
  ggplot(mapping = aes(x = tamanho, y = faccao, fill = faccao)) +
  geom_density_ridges(stat = 'binline', scale = 0.90, bins = 15, draw_baseline = FALSE,
                      show.legend = FALSE) +
  scale_fill_manual(values = cores_por_faccao) +
  scale_x_continuous(breaks = seq(from = 0, to = 10, by = 1)) +
  labs(
    title    = 'Quantidade de regras identificadas por facção',
    subtitle = 'A maior parte das regras identificadas envolvem 3 ou 4 cartas',
    x        = 'Quantidade de regras'
  ) +
  theme(axis.title.y = element_blank())

Existe uma terceira métrica que pode ser útil para nos ajudar a identificar as regras que têm uma relevância potencial maior, o lift. Esta métrica descreve o quão mais provável é que dois ou mais itens ocorram juntos do que quando comparado à hipótese de que eles são independentes entre si, e pode ser calculada como \(\frac{P(LHS \bigcap RHS)}{P(LHS).P(RHS)}\). Desta forma, quanto maior e mais distante de 1 for esta métrica, mais forte é a evidência de há uma relação de co-dependência entre os itens dentro daquela regra. A figura abaixo mostra que existe uma variação nos valores de lift entre as facções, de forma que a co-dependência entre as regras parece ser pouco comum para algumas delas (e.g., Nilfgaard onde com 20% das regras já obtemos um lift de 2) mas mais comuns para outras (e.g., Monsters e Northern Realms onde com 20% das regras temos um lift maior que 5). Isto sugere que a qualidade das regras varia entre as facções, sendo que parece valer mais a pena olhar as regras que existem até que haja um cotovelo nas curvas abaixo.

Show code
# examinando a variação no lift entre as regrass
df_regras %>% 
  # ordenando as linhas do dataframe por faccao e em ordem decrescente de lift
  arrange(faccao, -lift) %>% 
  # agrupando pela faccao
  group_by(faccao) %>% 
  # adicionando a sequencia de observacoes
  mutate(sequencia = 1:n()) %>% 
  # criando a figura
  ggplot(mapping = aes(x = sequencia, y = lift, color = faccao)) +
  facet_wrap(~ faccao, scales = 'free') +
  geom_line(show.legend = FALSE) +
  scale_color_manual(values = cores_por_faccao) +
  scale_y_continuous(breaks = seq(from = 0, to = 10, by = 1)) +
  labs(
    title    = 'Variação do lift entre as regras para cada facção',
    subtitle = 'O lift descreve quão mais provável é que os itens dentro de uma regra co-ocorram do que quando comparado à hipótese de que eles são independentes',
    x        = '# Regra',
    y        = 'Lift'
  )

Existem outros padrões que poderíamos explorar a partir destes resultados como, por exemplo, a relação entre o suporte e a confiança entre as regras para cada facção. Todavia, para não nos extendermos demais aqui, vou deixar a tabela abaixo com todas as regras encontradas para cada facção, bem como os valores das métricas associadas à qualidade de cada uma delas. Minha ideia é que esta tabela possa servir de referência rápida para buscar uma carta e ver quais as outras cartas que normalmente são usadas junto delas.

Show code
df_regras %>% 
  # pegando apenas as colunas de interesse
  mutate(across(.cols = c(support, confidence, coverage, lift, x, y), round, digits = 3)) %>% 
  # reposicionando a coluna com a regra completa
  relocate(combinacao, .before = LHS) %>% 
  # criando a tabela interativa
  reactable(
    sortable = TRUE, filterable = TRUE, searchable = TRUE, compact = TRUE,
    highlight = TRUE, borderless = TRUE, showPageSizeOptions = TRUE, 
    defaultColDef = colDef(align = 'center'), defaultPageSize = 5,
    style = list(fontFamily = "Fira Sans", fontSize = "12px"),
    columns = list(
      faccao     = colDef(name = 'Facção'),
      combinacao = colDef(name = 'Combinação'),
      support    = colDef(name = 'Suporte'),
      confidence = colDef(name = 'Confiança'),
      coverage   = colDef(name = 'Cobertura'),
      lift       = colDef(name = 'Lift'),
      count      = colDef(name = 'Ocorrências'),
      tamanho    = colDef(name = 'Regras'),
      maximal    = colDef(name = 'MFI'),
      x          = colDef(name = 'PCoA #1'),
      y          = colDef(name = 'PCoA #2')
    )
  )

Para finalizar este post, vamos voltar aos objetivos iniciais e analisar os padrões de co-ocorrência dos pares de cartas e as diferenças entre as regras.

Relação entre cartas

Um dos usos que estes resultados podem ter é saber quais cartas devem acompanhar cada outra carta quando montamos um deck. Isto é, uma vez que escolhi a carta {A}, ela pede que eu coloque junto a carta {B}, e abre a possibilidade de eu colocar as cartas {C}, {D} e {E}. De forma a poder explorar esta ideia visualmente, resolvi utilizar uma rede direcionada para analisar os padrões de ocorrência entre cada par de cartas.

Na rede que montaremos, cada nó será uma carta, cada aresta é a existência de uma relação entre elas e a espessura das arestras será o valor de lift. Para construir esta rede, filtrei todas as regras que envolvessem apenas 2 cartas, eliminando àquelas com valores de lift menores que 1. Como podem haver regras similares com o mesmo valor de lift (e.g., {A} → {B} = {B} → {A}), eu resolvi ordenar as regras restantes em ordem decrescente de confiança e retive apenas a regra duplicada que tivesse o maior valor para esta métrica. Isto é possível de ser feito com a confiança de uma regra porque ela é medida condicionada à quantidade de vezes que que apenas um dos itens aparece na base de dados, isto é \(\frac{P(A \bigcap B)}{P(A)} \neq \frac{P(A \bigcap B)}{P(B)}\). Desta forma, aumentamos as chances de manter as regras que tenham maior consistência e assumimos que a direcionalidade observada para àquela regra é a mais provável entre o par de cartas. Sei que esta premissa é bastante discutível, mas para as finalidades que tenho aqui, acredito que ela me dê uma boa resposta do que eu estava procurando.

Vou plotar a rede para a facção Nilfgaard, uma vez que tenho focado em melhorar meu deck para ela. Além disso, resolvi remover todas as regras que façam menção às cartas Oniromancia e Korathi Charmandente pois acabei achando que as regras que envolvem elas parecem ser mais um artefato do quão frequente elas são entre os decks do que uma regra acionável em si. Em todos os casos, utilizei a função forceplot do pacote networkD3 para plotar as redes (Allaire et al. (2017)).

A rede mostra padrões de associação muito razoáveis entre as cartas, alguns dos quais fazem total sentido. Um exemplo mais fácil de visualizar é aquele entre as cartas Bruxo Víbora, Cynthia, Armas Envenenadas e Kolgrim: as três primeiras cartas fazem surgir muitas cópias de outras cartas ou de si mesmo no topo do deck da pessoa oponente, no intuito de aumentar a diferença na quantidade de cartas entre o deck da pessoa e o seu; quando chega o último turno da rodada, quando esta diferença na quantidade de cartas já é grande, a carta Kolgrim entra em ação, ganhando tantos pontos quanto for a diferença na quantidade de cartas entre os dois baralhos.

Show code
# carregando pacotes
library(networkD3) # para plotar o grafo

## filtrando o dataframe de acordo com o input
df_filtered <- df_regras %>% 
  # pegando todas as regras com duas cartas só, pertencentes à facção desejada
  filter(tamanho == 2, faccao == 'Nilfgaard') %>% 
  # removendo as duas cartas neutras mais frequentes
  filter(!combinacao %in% c('Oniromancia', 'Korathi Chamardente')) %>% 
  # selecionando apenas as colunas que usaremos
  select(LHS, RHS, lift, confidence, combinacao) %>% 
  # retendo apenas o que tem lift maior que um
  filter(lift > 1) %>% 
  # agrupando pelo identificador unico da regra
  group_by(combinacao) %>% 
  # organizando a base pela regra e em ordem decrescente de lift
  arrange(combinacao, desc(confidence)) %>% 
  # pegando apenas a primeira ocorrencia de cada regra
  filter(row_number() == 1) %>% 
  # desagrupando a tabela
  ungroup() %>% 
  # removendo a coluna do identificador unico da tabela
  select(-combinacao, -confidence)

## criando o dataframe dos nos
df_nodes <- tibble(localizedName = unique(x = c(df_filtered$LHS, df_filtered$RHS))) %>% 
  # juntando metadados de cada carta
  left_join(y = cartas, by = 'localizedName') %>% 
  # adicionando um identificador unico para cada no
  mutate(node_id = row_number() - 1,
         grupo   = 1) %>% 
  # colocando o identificador unico na frente de tudo
  relocate(node_id, .before = localizedName) %>% 
  # convertendo para um dataframe
  data.frame

## criando o dataframe dos links
df_links <- df_filtered %>% 
  # juntando o identificador unico de cada carta na origem
  left_join(y = select(df_nodes, node_id, localizedName), by = c('LHS' = 'localizedName')) %>% 
  # juntando o identificador unico de cada carta no destino
  left_join(y = select(df_nodes, node_id, localizedName), by = c('RHS' = 'localizedName')) %>% 
  # renomeando as colunas de origem e destino
  rename(source = node_id.x, target = node_id.y) %>% 
  # transformando lift
  mutate(log_lift = log(lift)) %>% 
  # convertendo para um dataframe
  data.frame

## criando o grafo
forceNetwork(Links = df_links, Nodes = df_nodes, Source = 'source', 
             Target = 'target',  Value = 'lift',  NodeID = 'localizedName', 
             Group = 'grupo', zoom = TRUE, arrows = TRUE, 
             fontFamily = 'Fira Sans', opacityNoHover = 1, opacity = 1,
             fontSize = 10, charge = -100)

Conhecendo das cartas, acho que a rede ficou muito legal para entender a relação entre elas e bolar estratégias de montagem de decks

Diferença entre regras

Minha ideia em tentar entender a diferença entre as regras está mais relacionada à buscar uma compreensão do quão redundantes elas parecem ser. Com isto em mente, filtrei os dados para a facção Nilfgaard e plotei as coordenadas x e y da projeção da matriz de dissimilaridade de acordo com a PCoA. Podemos ver que existe claramente um grupinho de regras à esquerda da figura, alguns pontos perdido ao longo do eixo x e, no quadrante superior direito, mais um conjunto de pontos. Olhando as regras que estamos ao longo do gradiente, me parece que existe muito claramente um gradiente no qual temos um conjunto de regras que giram em torno da carta Baile de Máscaras à esquerda e um outro conjunto que gira em torno das cartas Braathens e Informante da Duquesa à direita. A parte à esquerda me parece remeter à mecânica de ganhar pontos conferindo algum tipo de status às cartas do oponente (e.g., espionagem, envenenamento, bloqueio,…) enquanto a parte à direita me parece com uma mecânica mais orientada à assimilação (i.e., mecânica onde ganhamos pontos conforme vamos copiando as cartas de unidade do adversário).

Show code
df_regras %>% 
  # filtrando apenas a facção alvo
  filter(faccao == 'Nilfgaard') %>% 
  # criando a figura
  plot_ly(x = ~ x, y = ~ y, type = 'scatter', mode = 'markers', 
          marker = list(color = ~ log10(lift), colorscale = 'RdBu', reversescale = TRUE),
          hoverinfo = 'text', 
          hovertext = ~ paste0('<b>', str_wrap(string = combinacao, width = 50), '</b><br>',
                               'LHS: ', str_wrap(string = LHS, width = 50), '<br>',
                               'RHS: ', str_wrap(string = RHS, width = 50), '<br>',
                               'Regras: ', tamanho, '<br>',
                               'Lift: ', round(x = lift, digits = 3))) %>% 
  layout(
    title = '<b>Projeção das diferenças entre as regras para os decks da facção de Nilfgaard</b>',
    xaxis = list(title = '', showgrid = FALSE, showticklabels = FALSE),
    yaxis = list(title = '', showgrid = FALSE, showticklabels = FALSE)
  )

Apesar de ter conseguido tirar alguma interpretação acionável à partir dessa figura, a forma como a ordenação ficou não me agradou muito. Isto foi principalmente pelo fato de haverem muitos pontos perdidos ao longo do gradiente do eixo x e do eixo y. Nesta figura, a distância entre os pontos está associada à similaridade entre as regras em termos da quantidade de transações que elas têm em comum, o que parece fazer com que a ordenação fique muito enviesada pelas regras que são subconjuntos de outras. Por exemplo, {A, B} e {A, C} são subconjuntos de {A, B, C}, e elas vão aparecer sempre nas mesmas transações, fazendo com que a similaridade entre elas seja muito alto. Todavia, bastava que olhássemos apenas para o maior subconjunto {A, B, C} para saber quão diferente esta regra é de e.g. {C, D, E}, mas a métrica de Gupta, Strehl, and Ghosh (1999) me parece não olhar isso. Na realidade, tentei a distância do coseno para definir as diferenças entre as regras, e a ordenação chegou a dois extremos muito bem definidos, similares aos que descrevi acima. Como minha ideia era seguir a documentação do pacote arules, resolvi seguir com a métrica que está aqui, mas certamente usaria outra métrica em uma outra ocasião.

Salvando os outputs

Uma coisa que vou passar a fazer a partir deste post é salvar uma cópia do objeto do modelos treinado e outros outputs que possam vir a ser úteis para outras finalidades (e.g., para a construção de um Shiny app). O código abaixo cria as pastas para armazenar estas informações e, na sequência, salva cada um dos objetos como um arquivo rds.

# checando se o diretorio para guardar os modelos existe, e criando caso necessario
if(!dir_exists(path = 'models')){
  dir_create(path = 'models')
}

# salvando o dataframe com os modelos
write_rds(x = select(regras_por_faccao, faccao, transacoes, regras),
          file = 'models/regras_por_faccao.rds')

# checando se o diretorio para guardar outros outputs existe, e criando caso necessario
if(!dir_exists(path = 'outputs')){
  dir_create(path = 'outputs')
}

# salvando o dataframe com as regras
write_rds(x = df_regras, file = 'outputs/regras.rds')

Conclusões

Neste post eu tentei entender as regras de associação entre as cartas de Gwent olhando os padrões de co-ocorrência entre elas através dos decks de cada facção. Meu intuito com isso é utilizar o output desta análise e a análise exploratória sobre a estrutura e composição dos decks de forma a me ajudar a definir estratégias de montagem dos mesmos e de jogo mais efetivas do que àquelas que uso atualmente. Novamente, eu não espero que isso vá me dar uma vantagem competitiva, mas eu realmente acredito que isso vá melhorar a forma como busco jogar.

Uma limitação da abordagem utilizada é que ela só vai minar as regras de associação que já são conhecidas pela comunidade. Desta forma, caso hajam sinergias entre as cartas que ainda não tenham sido exploradas, esta solução também não as enxergará. Ainda assim, acredito que esta abordagem possa servir de uma primeira aproximação para a tarefa de melhorar a experiência de montagem de um deck, além dela servir como um benchmark para outras possíveis soluções que eu penso serem possíveis de ser criadas.

Como vimos muitos padrões e resultados interessantes, sumarizo abaixo alguns que foram os mais importantes de acordo com a minha visão:

Dúvidas, sugestões ou críticas? É só me procurar que a gente conversa!

Possíveis extensões

Acredito que este foi um dos posts que me deu mais trabalho até agora, mas que também me ajudou a pensar em muitas ideias.

Agrawal, Rakesh, and Ramakrishnan Srikant. 1994. “Fast Algorithms for Mining Association Rules in Large Databases.” Proceedings of the 20th International Conference on Very Large Databases, 487–99.
Allaire, J. J., Christopher Gandrud, Kenton Russell, and CJ Yetman. 2017. networkD3: D3 JavaScript Network Graphs from r. https://CRAN.R-project.org/package=networkD3.
Gupta, Gunjan, Alexander Strehl, and Joydeep Ghosh. 1999. “Distance Based Clustering of Association Rules.” In Intelligent Engineering Systems Through Artificial Neural Networks, 9:759–64.
Hahsler, Michael, Christian Buchta, Bettina Gruen, and Kurt Hornik. 2021. Arules: Mining Association Rules and Frequent Itemsets. https://CRAN.R-project.org/package=arules.

  1. O objeto decks contém todas as colunas apresentadas aqui e àquelas que estão no objeto cartas, abaixo. Apenas separei a apresentação destes dados em duas tabelas para fins ilustrativos, uma vez que algumas informações dizem respeito à características das cartas e, portanto, apareciam todas as vezes que cada carta estava em um deck.↩︎

  2. Existem 2.087 decks da facção Nilfgaard e 970 decks para a do Syndicate.↩︎

References

Corrections

If you see mistakes or want to suggest changes, please create an issue on the source repository.

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY 4.0. Source code is available at https://github.com/nacmarino/codex/, unless otherwise noted. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".

Citation

For attribution, please cite this work as

Marino (2022, Jan. 8). Codex: Quais as associações entre as cartas de Gwent nos decks existentes?. Retrieved from https://nacmarino.github.io/codex/posts/2022-01-08-quais-as-associacoes-existentes-entre-as-cartas-dos-decks-de-gwent/

BibTeX citation

@misc{marino2022quais,
  author = {Marino, Nicholas},
  title = {Codex: Quais as associações entre as cartas de Gwent nos decks existentes?},
  url = {https://nacmarino.github.io/codex/posts/2022-01-08-quais-as-associacoes-existentes-entre-as-cartas-dos-decks-de-gwent/},
  year = {2022}
}