Dungeons and Dragons Spells

Author
Affiliation
Published

December 20, 2024

Looking at the Dungeons and Dragons spells data from TidyTuesday

EDA of the dataset

I used the SummaryTable() function from Observable to visualize the particulars of the dataset.

Code
spells = await FileAttachment("spells.csv").csv()
import { SummaryTable } from "@observablehq/summary-table"
viewof summary_data = SummaryTable(spells, {label: "D & D Spells"})
dd_classes = ["bard", "cleric", "druid", "paladin", "ranger", "sorcerer", "warlock", "wizard"]

Transforming the data (R)

I decided to use R to transform the data, because I am faster at the tidyverse than I am at arquero (the JS data cleaning library).

The first thing I did was subset the data to only the spellcasting classes, leaving me with an 3 column data.frame.

Code
library(tidyverse)

spells <- read.csv("spells.csv")
colnames(spells)
 [1] "name"                       "level"                     
 [3] "school"                     "bard"                      
 [5] "cleric"                     "druid"                     
 [7] "paladin"                    "ranger"                    
 [9] "sorcerer"                   "warlock"                   
[11] "wizard"                     "casting_time"              
[13] "action"                     "bonus_action"              
[15] "reaction"                   "ritual"                    
[17] "casting_time_long"          "trigger"                   
[19] "range"                      "range_type"                
[21] "verbal_component"           "somatic_component"         
[23] "material_component"         "material_component_details"
[25] "duration"                   "concentration"             
[27] "description"               
Code
dd_classes <- c("bard", "cleric", "druid", "paladin", "ranger", "sorcerer", "warlock", "wizard")

spells_class <- spells |>
  select(all_of(dd_classes))

spells_long <- spells |>
  tidyr::pivot_longer(cols = all_of(dd_classes), names_to = "class", values_to = "has_spell")

spells_long

Then I counted the class and school:

Code
spell_count <- spells_long |>
  filter(has_spell == TRUE) |>
  count(class, school)

spell_count

Clustering the Spellcasters

One natural thing to do is look at the number of spells each class has in common with another class. Here I calculate the intersection of spells between each each class. There is probably more elegant code to do this, but this was the fastest way I could figure to do this.

Code
sim_matrix2 <- matrix(data=NA, nrow = length(dd_classes), length(dd_classes),dimnames=list(dd_classes, dd_classes) )

inter <- function(x, y){
  sim_out <- sum(as.numeric(x & y))
  sim_out
}

for(cl in dd_classes){
  for(c2 in dd_classes){
    sim_matrix2[cl, c2] <- inter(spells_class[[cl]], spells_class[[c2]])
  }
}

sim_matrix2 <- as.data.frame.table(sim_matrix2)

One way to transform this is to transform the intersection into a similarity metric. I normalize by the total number of spells so that the numbers range from 0 to 1. I then need to transform the similarity matrix into a dissimilarity matrix. I subtract the matrix from 1 to do this, using the as.dist() function to transform the matrix into a distance matrix.

Code
sim <- function(x, y){
  len <- length(x)
  sim_out <- sum(as.numeric(x & y)) / len
  sim_out
}

sim_matrix <- matrix(data=NA, nrow = length(dd_classes), length(dd_classes),dimnames=list(dd_classes, dd_classes) )

for(cl in dd_classes){
  for(c2 in dd_classes){
    sim_matrix[cl, c2] <- sim(spells_class[[cl]], spells_class[[c2]])
  }
}

sim_matrix <- 1 - as.dist(sim_matrix)

sim_matrix
              bard    cleric     druid   paladin    ranger  sorcerer   warlock
cleric   0.8694268                                                            
druid    0.8630573 0.8407643                                                  
paladin  0.9585987 0.9012739 0.9522293                                        
ranger   0.9394904 0.9394904 0.8662420 0.9585987                              
sorcerer 0.8248408 0.9299363 0.8662420 0.9840764 0.9554140                    
warlock  0.8726115 0.9458599 0.9554140 0.9808917 0.9904459 0.8630573          
wizard   0.7229299 0.8662420 0.8216561 0.9649682 0.9490446 0.6019108 0.8025478

Here is the hierarchical clustering using this similarity metric. As you can see, Sorcerer and Wizard are the closest in terms of spells in common. There are two distinct clusters of similarity: [sorcerer, wizard, warlock, bard] and [druid, cleric, ranger, paladin].

Code
plot(hclust(sim_matrix))

Finally, we pass our calculated data.frames and matrices onto Observable by using the ojs_define() function:

Code
ojs_define(sim_matrix2, spells_long, spell_count)

Visualizing Spells (in Observable)

The first thing I need to do is transpose the data objects from R. This is because Observable expects the data to be in this transposed format.

Code
sim_t = transpose(sim_matrix2)
spell_c = transpose(spell_count)

Using the breakdown of spell counts, I plot the number of spells in each spellcasting school for each spellcasting class.

Code
Plot.plot({
  title: "Spells for each class by school",
  subtitle: "Wizards can learn the most spells",
  color: {scheme: "Spectral", legend: true, width: 340, label: "School"},
  marks: [
  Plot.barY(spell_c,  
    {x: "class", 
     y: "n", 
     fill:"school",
     sort: {color: null, x: "y"}})
  ]
})

Finally, using the intersection of spells between classes, I build an intersection matrix in Observable.

Code
Plot.plot({
  title: "Spellcasting Classes compared by spells in common",
  marginLeft: 100,
  label: null,
  color: { scheme: "RdYlBu", pivot: 0, legend: false, label: "Num Common Spells", domain: [0,200] },
  marks: [
    Plot.cell(sim_t, { x: "Var1", y: "Var2", fill: "Freq" }),
    Plot.text(sim_t, {
      x: "Var1",
      y: "Var2",
      text: "Freq",
      fill: (d) => (Math.abs(d.Freq) > 80 ? "white" : "black")
    })
  ]
})

Citation

BibTeX citation:
@online{laderas2024,
  author = {Laderas, Ted},
  title = {Dungeons and {Dragons} {Spells}},
  date = {2024-12-20},
  url = {https://laderast.github.io/articles/spells/},
  langid = {en}
}
For attribution, please cite this work as:
Laderas, Ted. 2024. “Dungeons and Dragons Spells.” December 20, 2024. https://laderast.github.io/articles/spells/.