Data is Beautiful
A practical book on data visualisation that shows you how to create static and interactive visualisations that are engaging and beautiful.
Get the book
League of Legends - Class Combinations
Contents Chat Share Follow Download Source
Made with Plotapi
You can create beautiful, interactive, and engaging visualisations like this one with Plotapi in any programming language. Learn how to make beautiful visualisations with the book, Data is Beautiful.
Preamble¶
import numpy as np # for multi-dimensional containers
import pandas as pd # for DataFrames
import itertools
from plotapi import Chord
Chord.set_license("your username", "your license key")
Introduction¶
In previous sections, we visualised co-occurrences of Pokémon type. Whilst it was interesting to look at, the dataset only contained Pokémon from the first six geerations. In this section, we're going to use the Pokemon with stats Generation 8 dataset to visualise the co-occurrence of Pokémon types from generations one to eight.
The Dataset¶
The dataset documentation states that we can expect 13 variables per each of the 1017 Pokémon of the first eight generations.
Let's download the mirrored dataset and have a look for ourselves.
data_url = 'https://datacrayon.com/datasets/lol/champion.json'
data = pd.read_json(data_url)
data.head()
type | format | version | data | |
---|---|---|---|---|
Aatrox | champion | standAloneComplex | 10.13.1 | {'version': '10.13.1', 'id': 'Aatrox', 'key': ... |
Ahri | champion | standAloneComplex | 10.13.1 | {'version': '10.13.1', 'id': 'Ahri', 'key': '1... |
Akali | champion | standAloneComplex | 10.13.1 | {'version': '10.13.1', 'id': 'Akali', 'key': '... |
Alistar | champion | standAloneComplex | 10.13.1 | {'version': '10.13.1', 'id': 'Alistar', 'key':... |
Amumu | champion | standAloneComplex | 10.13.1 | {'version': '10.13.1', 'id': 'Amumu', 'key': '... |
It looks good so far, but let's confirm the 13 variables against 1017 samples from the documentation.
data = pd.DataFrame(data.data.tolist()).set_index(data.index)
data
version | id | key | name | title | blurb | info | image | tags | partype | stats | |
---|---|---|---|---|---|---|---|---|---|---|---|
Aatrox | 10.13.1 | Aatrox | 266 | Aatrox | the Darkin Blade | Once honored defenders of Shurima against the ... | {'attack': 8, 'defense': 4, 'magic': 3, 'diffi... | {'full': 'Aatrox.png', 'sprite': 'champion0.pn... | [Fighter, Tank] | Blood Well | {'hp': 580, 'hpperlevel': 90, 'mp': 0, 'mpperl... |
Ahri | 10.13.1 | Ahri | 103 | Ahri | the Nine-Tailed Fox | Innately connected to the latent power of Rune... | {'attack': 3, 'defense': 4, 'magic': 8, 'diffi... | {'full': 'Ahri.png', 'sprite': 'champion0.png'... | [Mage, Assassin] | Mana | {'hp': 526, 'hpperlevel': 92, 'mp': 418, 'mppe... |
Akali | 10.13.1 | Akali | 84 | Akali | the Rogue Assassin | Abandoning the Kinkou Order and her title of t... | {'attack': 5, 'defense': 3, 'magic': 8, 'diffi... | {'full': 'Akali.png', 'sprite': 'champion0.png... | [Assassin] | Energy | {'hp': 575, 'hpperlevel': 95, 'mp': 200, 'mppe... |
Alistar | 10.13.1 | Alistar | 12 | Alistar | the Minotaur | Always a mighty warrior with a fearsome reputa... | {'attack': 6, 'defense': 9, 'magic': 5, 'diffi... | {'full': 'Alistar.png', 'sprite': 'champion0.p... | [Tank, Support] | Mana | {'hp': 600, 'hpperlevel': 106, 'mp': 350, 'mpp... |
Amumu | 10.13.1 | Amumu | 32 | Amumu | the Sad Mummy | Legend claims that Amumu is a lonely and melan... | {'attack': 2, 'defense': 6, 'magic': 8, 'diffi... | {'full': 'Amumu.png', 'sprite': 'champion0.png... | [Tank, Mage] | Mana | {'hp': 613.12, 'hpperlevel': 84, 'mp': 287.2, ... |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
Zed | 10.13.1 | Zed | 238 | Zed | the Master of Shadows | Utterly ruthless and without mercy, Zed is the... | {'attack': 9, 'defense': 2, 'magic': 1, 'diffi... | {'full': 'Zed.png', 'sprite': 'champion4.png',... | [Assassin] | Energy | {'hp': 584, 'hpperlevel': 85, 'mp': 200, 'mppe... |
Ziggs | 10.13.1 | Ziggs | 115 | Ziggs | the Hexplosives Expert | With a love of big bombs and short fuses, the ... | {'attack': 2, 'defense': 4, 'magic': 9, 'diffi... | {'full': 'Ziggs.png', 'sprite': 'champion4.png... | [Mage] | Mana | {'hp': 536, 'hpperlevel': 92, 'mp': 480, 'mppe... |
Zilean | 10.13.1 | Zilean | 26 | Zilean | the Chronokeeper | Once a powerful Icathian mage, Zilean became o... | {'attack': 2, 'defense': 5, 'magic': 8, 'diffi... | {'full': 'Zilean.png', 'sprite': 'champion4.pn... | [Support, Mage] | Mana | {'hp': 504, 'hpperlevel': 82, 'mp': 452, 'mppe... |
Zoe | 10.13.1 | Zoe | 142 | Zoe | the Aspect of Twilight | As the embodiment of mischief, imagination, an... | {'attack': 1, 'defense': 7, 'magic': 8, 'diffi... | {'full': 'Zoe.png', 'sprite': 'champion4.png',... | [Mage, Support] | Mana | {'hp': 560, 'hpperlevel': 92, 'mp': 425, 'mppe... |
Zyra | 10.13.1 | Zyra | 143 | Zyra | Rise of the Thorns | Born in an ancient, sorcerous catastrophe, Zyr... | {'attack': 4, 'defense': 3, 'magic': 8, 'diffi... | {'full': 'Zyra.png', 'sprite': 'champion4.png'... | [Mage, Support] | Mana | {'hp': 504, 'hpperlevel': 79, 'mp': 418, 'mppe... |
148 rows × 11 columns
Perfect, that's exactly what we were expecting.
Data Wrangling¶
We need to do a bit of data wrangling before we can visualise our data. We can see from the columns names that the Pokémon types are split between the columns Type 1
and Type 2
.
pd.DataFrame(data.columns.values.tolist())
0 | |
---|---|
0 | version |
1 | id |
2 | key |
3 | name |
4 | title |
5 | blurb |
6 | info |
7 | image |
8 | tags |
9 | partype |
10 | stats |
So let's select just these two columns and work with a list containing only them as we move forward.
Without further investigation, we can see that we have at least a few NaN
values in the table above. We are only interested in co-occurrence of types, so we can remove all samples which contain a NaN
value.
We can also see an instance where the type Fighting
at index $1014$ is followed by \n
. We'll strip all these out before continuing.
Our chord diagram will need two inputs: the co-occurrence matrix, and a list of names to label the segments.
First we'll populate our list of type names by looking for the unique ones.
types = [item for sublist in data.tags.tolist() for item in sublist]
names = np.unique(types).tolist()
pd.DataFrame(names)
0 | |
---|---|
0 | Assassin |
1 | Fighter |
2 | Mage |
3 | Marksman |
4 | Support |
5 | Tank |
Now we can create our empty co-occurrence matrix using these type names for the row and column indeces.
matrix = pd.DataFrame(0, index=names, columns=names)
matrix
Assassin | Fighter | Mage | Marksman | Support | Tank | |
---|---|---|---|---|---|---|
Assassin | 0 | 0 | 0 | 0 | 0 | 0 |
Fighter | 0 | 0 | 0 | 0 | 0 | 0 |
Mage | 0 | 0 | 0 | 0 | 0 | 0 |
Marksman | 0 | 0 | 0 | 0 | 0 | 0 |
Support | 0 | 0 | 0 | 0 | 0 | 0 |
Tank | 0 | 0 | 0 | 0 | 0 | 0 |
We can populate a co-occurrence matrix with the following approach. We'll start by creating a list with every type pairing in its original and reversed form.
Which we can now use to create the matrix.
len(data.tags[0])
2
for x in data.tags:
if(len(x) == 2):
matrix.at[x[0], x[1]] += 1
matrix.at[x[1], x[0]] += 1
if(len(x) == 1):
matrix.at[x[0], x[0]] += 1
matrix = matrix.values.tolist()
We can list DataFrame
for better presentation.
pd.DataFrame(matrix)
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 5 | 17 | 8 | 5 | 1 | 0 |
1 | 17 | 3 | 6 | 1 | 3 | 34 |
2 | 8 | 6 | 13 | 6 | 21 | 4 |
3 | 5 | 1 | 6 | 13 | 2 | 0 |
4 | 1 | 3 | 21 | 2 | 1 | 4 |
5 | 0 | 34 | 4 | 0 | 4 | 1 |
colors = ["#80FF72","#ffce2b","#FA7E07","#ff006e","#8338ec","#3a86ff"]
Chord(matrix, names, colors=colors, curved_labels=True).show()
Chord Diagram with Names¶
It would be nice to show a list of Pokémon names and images when hovering over co-occurring Pokémon types. To do this, we can make use of the optional details
parameter.
Let's also add a column to our dataset to store URLs that point to the images.
data['URL'] = ""
for index, row in data.iterrows():
#url = f"http://127.0.0.1:8000/images/data-is-beautiful/lol/champion/{row.name}.png"
url = f"https://shahinrostami.com/images/data-is-beautiful/lol/champion/{row.name}.png"
data.at[index,'URL'] = url
data.URL
Aatrox https://shahinrostami.com/images/data-is-beaut... Ahri https://shahinrostami.com/images/data-is-beaut... Akali https://shahinrostami.com/images/data-is-beaut... Alistar https://shahinrostami.com/images/data-is-beaut... Amumu https://shahinrostami.com/images/data-is-beaut... ... Zed https://shahinrostami.com/images/data-is-beaut... Ziggs https://shahinrostami.com/images/data-is-beaut... Zilean https://shahinrostami.com/images/data-is-beaut... Zoe https://shahinrostami.com/images/data-is-beaut... Zyra https://shahinrostami.com/images/data-is-beaut... Name: URL, Length: 148, dtype: object
data.loc['Akali']
version 10.13.1 id Akali key 84 name Akali title the Rogue Assassin blurb Abandoning the Kinkou Order and her title of t... info {'attack': 5, 'defense': 3, 'magic': 8, 'diffi... image {'full': 'Akali.png', 'sprite': 'champion0.png... tags [Assassin] partype Energy stats {'hp': 575, 'hpperlevel': 95, 'mp': 200, 'mppe... URL https://shahinrostami.com/images/data-is-beaut... Name: Akali, dtype: object
names
['Assassin', 'Fighter', 'Mage', 'Marksman', 'Support', 'Tank']
Next, we'll create an empty multi-dimensional arrays with the same shape as our matrix
for our details and thumbnail images.
data[['tag_1','tag_2']] = pd.DataFrame(data.tags.tolist(), index= data.index)
data
version | id | key | name | title | blurb | info | image | tags | partype | stats | URL | tag_1 | tag_2 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Aatrox | 10.13.1 | Aatrox | 266 | Aatrox | the Darkin Blade | Once honored defenders of Shurima against the ... | {'attack': 8, 'defense': 4, 'magic': 3, 'diffi... | {'full': 'Aatrox.png', 'sprite': 'champion0.pn... | [Fighter, Tank] | Blood Well | {'hp': 580, 'hpperlevel': 90, 'mp': 0, 'mpperl... | https://shahinrostami.com/images/data-is-beaut... | Fighter | Tank |
Ahri | 10.13.1 | Ahri | 103 | Ahri | the Nine-Tailed Fox | Innately connected to the latent power of Rune... | {'attack': 3, 'defense': 4, 'magic': 8, 'diffi... | {'full': 'Ahri.png', 'sprite': 'champion0.png'... | [Mage, Assassin] | Mana | {'hp': 526, 'hpperlevel': 92, 'mp': 418, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Mage | Assassin |
Akali | 10.13.1 | Akali | 84 | Akali | the Rogue Assassin | Abandoning the Kinkou Order and her title of t... | {'attack': 5, 'defense': 3, 'magic': 8, 'diffi... | {'full': 'Akali.png', 'sprite': 'champion0.png... | [Assassin] | Energy | {'hp': 575, 'hpperlevel': 95, 'mp': 200, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Assassin | None |
Alistar | 10.13.1 | Alistar | 12 | Alistar | the Minotaur | Always a mighty warrior with a fearsome reputa... | {'attack': 6, 'defense': 9, 'magic': 5, 'diffi... | {'full': 'Alistar.png', 'sprite': 'champion0.p... | [Tank, Support] | Mana | {'hp': 600, 'hpperlevel': 106, 'mp': 350, 'mpp... | https://shahinrostami.com/images/data-is-beaut... | Tank | Support |
Amumu | 10.13.1 | Amumu | 32 | Amumu | the Sad Mummy | Legend claims that Amumu is a lonely and melan... | {'attack': 2, 'defense': 6, 'magic': 8, 'diffi... | {'full': 'Amumu.png', 'sprite': 'champion0.png... | [Tank, Mage] | Mana | {'hp': 613.12, 'hpperlevel': 84, 'mp': 287.2, ... | https://shahinrostami.com/images/data-is-beaut... | Tank | Mage |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
Zed | 10.13.1 | Zed | 238 | Zed | the Master of Shadows | Utterly ruthless and without mercy, Zed is the... | {'attack': 9, 'defense': 2, 'magic': 1, 'diffi... | {'full': 'Zed.png', 'sprite': 'champion4.png',... | [Assassin] | Energy | {'hp': 584, 'hpperlevel': 85, 'mp': 200, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Assassin | None |
Ziggs | 10.13.1 | Ziggs | 115 | Ziggs | the Hexplosives Expert | With a love of big bombs and short fuses, the ... | {'attack': 2, 'defense': 4, 'magic': 9, 'diffi... | {'full': 'Ziggs.png', 'sprite': 'champion4.png... | [Mage] | Mana | {'hp': 536, 'hpperlevel': 92, 'mp': 480, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Mage | None |
Zilean | 10.13.1 | Zilean | 26 | Zilean | the Chronokeeper | Once a powerful Icathian mage, Zilean became o... | {'attack': 2, 'defense': 5, 'magic': 8, 'diffi... | {'full': 'Zilean.png', 'sprite': 'champion4.pn... | [Support, Mage] | Mana | {'hp': 504, 'hpperlevel': 82, 'mp': 452, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Support | Mage |
Zoe | 10.13.1 | Zoe | 142 | Zoe | the Aspect of Twilight | As the embodiment of mischief, imagination, an... | {'attack': 1, 'defense': 7, 'magic': 8, 'diffi... | {'full': 'Zoe.png', 'sprite': 'champion4.png',... | [Mage, Support] | Mana | {'hp': 560, 'hpperlevel': 92, 'mp': 425, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Mage | Support |
Zyra | 10.13.1 | Zyra | 143 | Zyra | Rise of the Thorns | Born in an ancient, sorcerous catastrophe, Zyr... | {'attack': 4, 'defense': 3, 'magic': 8, 'diffi... | {'full': 'Zyra.png', 'sprite': 'champion4.png'... | [Mage, Support] | Mana | {'hp': 504, 'hpperlevel': 79, 'mp': 418, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Mage | Support |
148 rows × 14 columns
#data.loc[data.tag_2.isna(), 'tag_2'] = data[data.tag_2.isna()].tag_1
details = np.empty((len(names),len(names)),dtype=object)
details_thumbs = np.empty((len(names),len(names)),dtype=object)
Now we can populate the details
array with lists of Pokémon names in the correct positions.
data[(data['tag_1'] == "Assassin") & (data["tag_2"].isnull())]
version | id | key | name | title | blurb | info | image | tags | partype | stats | URL | tag_1 | tag_2 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Akali | 10.13.1 | Akali | 84 | Akali | the Rogue Assassin | Abandoning the Kinkou Order and her title of t... | {'attack': 5, 'defense': 3, 'magic': 8, 'diffi... | {'full': 'Akali.png', 'sprite': 'champion0.png... | [Assassin] | Energy | {'hp': 575, 'hpperlevel': 95, 'mp': 200, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Assassin | None |
Khazix | 10.13.1 | Khazix | 121 | Kha'Zix | the Voidreaver | The Void grows, and the Void adapts—in none of... | {'attack': 9, 'defense': 4, 'magic': 3, 'diffi... | {'full': 'Khazix.png', 'sprite': 'champion1.pn... | [Assassin] | Mana | {'hp': 572.8, 'hpperlevel': 85, 'mp': 327.2, '... | https://shahinrostami.com/images/data-is-beaut... | Assassin | None |
Shaco | 10.13.1 | Shaco | 35 | Shaco | the Demon Jester | Crafted long ago as a plaything for a lonely p... | {'attack': 8, 'defense': 4, 'magic': 6, 'diffi... | {'full': 'Shaco.png', 'sprite': 'champion3.png... | [Assassin] | Mana | {'hp': 587, 'hpperlevel': 89, 'mp': 297.2, 'mp... | https://shahinrostami.com/images/data-is-beaut... | Assassin | None |
Talon | 10.13.1 | Talon | 91 | Talon | the Blade's Shadow | Talon is the knife in the darkness, a merciles... | {'attack': 9, 'defense': 3, 'magic': 1, 'diffi... | {'full': 'Talon.png', 'sprite': 'champion3.png... | [Assassin] | Mana | {'hp': 588, 'hpperlevel': 95, 'mp': 377.2, 'mp... | https://shahinrostami.com/images/data-is-beaut... | Assassin | None |
Zed | 10.13.1 | Zed | 238 | Zed | the Master of Shadows | Utterly ruthless and without mercy, Zed is the... | {'attack': 9, 'defense': 2, 'magic': 1, 'diffi... | {'full': 'Zed.png', 'sprite': 'champion4.png',... | [Assassin] | Energy | {'hp': 584, 'hpperlevel': 85, 'mp': 200, 'mppe... | https://shahinrostami.com/images/data-is-beaut... | Assassin | None |
for count_x, item_x in enumerate(names):
for count_y, item_y in enumerate(names):
if(item_y == item_x):
details_urls = data[
(data['tag_1'] == item_x) & (data["tag_2"].isnull())]['URL'].to_list()
details_names = data[
(data['tag_1'] == item_x) & (data["tag_2"].isnull())]['name'].to_list()
else:
details_urls = data[
(data['tag_1'].isin([item_x, item_y])) &
(data['tag_2'].isin([item_y, item_x]))]['URL'].to_list()
details_names = data[
(data['tag_1'].isin([item_x, item_y])) &
(data['tag_2'].isin([item_y, item_x]))]['name'].to_list()
urls_names = np.column_stack((details_urls, details_names))
if(urls_names.size > 0):
details[count_x][count_y] = details_names
details_thumbs[count_x][count_y] = details_urls
else:
details[count_x][count_y] = []
details_thumbs[count_x][count_y] = []
details=pd.DataFrame(details).values.tolist()
details_thumbs=pd.DataFrame(details_thumbs).values.tolist()
pd.DataFrame(details)
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | [Akali, Kha'Zix, Shaco, Talon, Zed] | [Ekko, Fiora, Fizz, Irelia, Jax, Kayn, Lee Sin... | [Ahri, Evelynn, Kassadin, Katarina, LeBlanc, M... | [Quinn, Teemo, Tristana, Twitch, Vayne] | [Pyke] | [] |
1 | [Ekko, Fiora, Fizz, Irelia, Jax, Kayn, Lee Sin... | [Gangplank, Mordekaiser, Rek'Sai] | [Diana, Elise, Gragas, Rumble, Ryze, Swain] | [Jayce] | [Kayle, Taric, Thresh] | [Aatrox, Blitzcrank, Camille, Darius, Dr. Mund... |
2 | [Ahri, Evelynn, Kassadin, Katarina, LeBlanc, M... | [Diana, Elise, Gragas, Rumble, Ryze, Swain] | [Annie, Aurelion Sol, Brand, Cassiopeia, Karth... | [Azir, Ezreal, Jhin, Kennen, Kog'Maw, Varus] | [Anivia, Bard, Fiddlesticks, Heimerdinger, Ive... | [Amumu, Cho'Gath, Galio, Maokai] |
3 | [Quinn, Teemo, Tristana, Twitch, Vayne] | [Jayce] | [Azir, Ezreal, Jhin, Kennen, Kog'Maw, Varus] | [Aphelios, Caitlyn, Corki, Draven, Graves, Jin... | [Ashe, Senna] | [] |
4 | [Pyke] | [Kayle, Taric, Thresh] | [Anivia, Bard, Fiddlesticks, Heimerdinger, Ive... | [Ashe, Senna] | [Rakan] | [Alistar, Braum, Leona, Tahm Kench] |
5 | [] | [Aatrox, Blitzcrank, Camille, Darius, Dr. Mund... | [Amumu, Cho'Gath, Galio, Maokai] | [] | [Alistar, Braum, Leona, Tahm Kench] | [Shen] |
Finally, we can put it all together but this time with the details
matrix passed in.
Chord(
matrix,
names,
colors=colors,
details=details,
details_thumbs=details_thumbs,
noun="Champions",
thumbs_width=70,
thumbs_margin=1,
popup_width=670,
thumbs_font_size=10,
credit=True,
padding=0.05,
arc_numbers=True,
curved_labels=True,
verb="appear together in"
).show()
Chord(
matrix,
names,
colors=colors,
details=details,
details_thumbs=details_thumbs,
noun="Champions",
thumbs_width=70,
thumbs_margin=1,
popup_width=670,
thumbs_font_size=10,
credit=True,
padding=0.05,
arc_numbers=True,
curved_labels=True,
reverse_gradients=True,
verb="appear together in"
).show()
Conclusion¶
In this section, we demonstrated how to conduct some data wrangling on a downloaded dataset to prepare it for a chord diagram. Our chord diagram is interactive, so you can use your mouse or touchscreen to investigate the co-occurrences!
Made with Plotapi
You can create beautiful, interactive, and engaging visualisations like this one with Plotapi in any programming language. Learn how to make beautiful visualisations with the book, Data is Beautiful.
Support this work
Get the practical book on data visualisation that shows you how to create static and interactive visualisations that are engaging and beautiful.
Data is Beautiful
A practical book on data visualisation that shows you how to create static and interactive visualisations that are engaging and beautiful.
Get the book