# Utilize esses comandos no terminal para instalar as bibliotecas necessƔrias para rodar todo o notebook.
# !virtualenv venv --python=python3
# !source venv/bin/activate
# !pip install -r requirements.txt
Como muito bem exemplificou o SP TV da Rede Globo, nos últimos anos os jogos de tabuleiro tem se diversificado tanto em variedade de temas quanto em complexidade de sistemas. Dos tradicionais Jogo da Vida e Banco ImobiliÔrio, passando por clÔssicos como Catan e Carcassone, até chegar a jogos extremamente complexos com partidas que duram vÔrias horas, como Gloomhaven ou War of the Rings.
Com um universo de possibilidades que cresce mais a cada dia, os "Boardgamers" se organizaram em comunidades tanto locais ao redor de luderias quanto online. No Brasil, a mais famosa Ć© a Ludopedia, que segue o formato do BoardGameGeek (BGG), a maior rede social de jogos de tabuleiro do mundo.
Dessa forma, busco com este trabalho analisar um conjunto de dados do BGG disponĆvel no Kaggle e responder as seguintes perguntas:
import sys
import sqlite3
import seaborn as sn
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
# ConfiguraƧƵes de print para melhor estudo dos dados
# np.set_printoptions(threshold=np.inf)
# pd.set_option('display.max_colwidth', None)
# pd.set_option('display.max_rows', None)
db_sql = sqlite3.connect('database.sqlite')
df = pd.read_sql_query("SELECT * FROM BoardGames", db_sql)
print(df.head())
print(df.tail())
print(df.shape)
db_sql.close()
df
row_names | game.id | game.type | details.description | details.image | details.maxplayers | details.maxplaytime | details.minage | details.minplayers | details.minplaytime | ... | stats.family.arcade.bayesaverage | stats.family.arcade.pos | stats.family.atarist.bayesaverage | stats.family.atarist.pos | stats.family.commodore64.bayesaverage | stats.family.commodore64.pos | stats.subtype.rpgitem.bayesaverage | stats.subtype.rpgitem.pos | stats.subtype.videogame.bayesaverage | stats.subtype.videogame.pos | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | boardgame | Die Macher is a game about seven sequential po... | //cf.geekdo-images.com/images/pic159509.jpg | 5.0 | 240.0 | 14.0 | 3.0 | 240.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 2 | 2 | boardgame | Dragonmaster is a trick-taking card game based... | //cf.geekdo-images.com/images/pic184174.jpg | 4.0 | 30.0 | 12.0 | 3.0 | 30.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 3 | 3 | boardgame | Part of the Knizia tile-laying trilogy, Samura... | //cf.geekdo-images.com/images/pic3211873.jpg | 4.0 | 60.0 | 10.0 | 2.0 | 30.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 | 4 | 4 | boardgame | When you see the triangular box and the luxuri... | //cf.geekdo-images.com/images/pic285299.jpg | 4.0 | 60.0 | 12.0 | 2.0 | 60.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
4 | 5 | 5 | boardgame | In Acquire, each player strategically invests ... | //cf.geekdo-images.com/images/pic342163.jpg | 6.0 | 90.0 | 12.0 | 3.0 | 90.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
90395 | 90396 | 220053 | boardgame | Soldier Ball is a tabletop skill and strategy ... | //cf.geekdo-images.com/images/pic3436079.jpg | 2.0 | 15.0 | 4.0 | 2.0 | 5.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
90396 | 90397 | 220055 | boardgame | Description from the designer: The ye... | //cf.geekdo-images.com/images/pic3529002.jpg | 4.0 | 45.0 | 14.0 | 2.0 | 30.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
90397 | 90398 | 220068 | boardgameexpansion | Cecrops founded Athens and judged a competitio... | //cf.geekdo-images.com/images/pic3503602.jpg | 4.0 | 90.0 | 14.0 | 1.0 | 45.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
90398 | 90399 | 220069 | boardgameexpansion | The famous Myrmidons are the army of Achilles,... | //cf.geekdo-images.com/images/pic3437871.jpg | 4.0 | 90.0 | 14.0 | 1.0 | 45.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
90399 | 90400 | 220070 | boardgame | A snowbound small village. It quietly approach... | None | 6.0 | 20.0 | 12.0 | 3.0 | 20.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
90400 rows Ć 81 columns
Como o objeto de estudo desse trabalho sĆ£o apenas jogos de tabuleiro vamos utilizar apenas as entradas do dataset cujo atributo game.type, que representa o tipo de jogo, Ć© igual a boardgame. Assim podemos remover as expansƵes e possĆveis erros.
print(df['game.type'].unique(), df.shape)
df = df[df['game.type']=='boardgame']
print(df['game.type'].unique(), df.shape)
['boardgame' 'boardgameexpansion'] (90400, 81) ['boardgame'] (76688, 81)
Agora, vamos excluir jogos que não possuem data de publicação, pois o Board Game Geek possibilita que jogos não lançados sejam adicionados ao site. Normalmente, estes são jogos criados de forma amadora, modificações não oficiais ou protótipos de outros jogos. Também vamos remover todos os jogos que possuem menos de 10 avaliações.
print(df.shape)
df = df.dropna(axis=0, subset=['details.yearpublished'])
df = df[df['stats.usersrated'] >= 10]
print(df.shape)
(76688, 81) (22257, 81)
O dataset disponibilizado inclui uma grande quantidade de dados que nĆ£o serĆ£o Ćŗteis para responder as perguntas guia deste trabalho. Por isso, iremos remover algumas colunas. ComeƧamos removendo as colunas do tipo polls que representam enquetes criadas nos fóruns do BGG. TambĆ©m removemos as colunas do tipo family que representam a "famĆlia" do gĆŖnero do jogo e subtype, pois contĆ©m apenas informaƧƵes secundĆ”rias e irrelevantes para este trabalho.
# Removendo colunas indesejadas
for col in df:
if 'polls' in col or 'family' in col or 'subtype' in col:
df.drop(col, axis=1, inplace=True)
df.drop('attributes.t.links.concat.2....', axis=1, inplace=True)
df.drop('details.description', axis=1, inplace=True)
df.drop('details.thumbnail', axis=1, inplace=True)
df.drop('details.image', axis=1, inplace=True)
No exemplo abaixo, podemos visualizar todos os jogos criados pelo game designer Stefan Feld, um dos mais conhecidos e renomados da indĆŗstria.
# Investigando
# df2 = df.set_index('details.name')
df2 = df.set_index('attributes.boardgamedesigner')
df2.loc['Stefan Feld']
row_names | game.id | game.type | details.maxplayers | details.maxplaytime | details.minage | details.minplayers | details.minplaytime | details.name | details.playingtime | ... | stats.bayesaverage | stats.median | stats.numcomments | stats.numweights | stats.owned | stats.stddev | stats.trading | stats.usersrated | stats.wanting | stats.wishing | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
attributes.boardgamedesigner | |||||||||||||||||||||
Stefan Feld | 15174 | 16496 | boardgame | 2.0 | 45.0 | 8.0 | 2.0 | 45.0 | Roma | 45.0 | ... | 6.55524 | 0.0 | 1327.0 | 445.0 | 4898.0 | 1.277060 | 215.0 | 4040.0 | 187.0 | 626.0 |
Stefan Feld | 18364 | 19948 | boardgame | 5.0 | 60.0 | 9.0 | 2.0 | 60.0 | Rum & Pirates | 60.0 | ... | 6.07300 | 0.0 | 615.0 | 192.0 | 2228.0 | 1.306400 | 69.0 | 1581.0 | 122.0 | 317.0 |
Stefan Feld | 23521 | 25554 | boardgame | 5.0 | 75.0 | 10.0 | 2.0 | 45.0 | Notre Dame | 75.0 | ... | 7.22571 | 0.0 | 2540.0 | 1067.0 | 9597.0 | 1.209390 | 215.0 | 9905.0 | 666.0 | 2024.0 |
Stefan Feld | 29164 | 31594 | boardgame | 5.0 | 100.0 | 12.0 | 2.0 | 75.0 | In the Year of the Dragon | 100.0 | ... | 7.21802 | 0.0 | 2153.0 | 846.0 | 6876.0 | 1.327520 | 111.0 | 8232.0 | 675.0 | 2182.0 |
Stefan Feld | 32799 | 35488 | boardgame | 5.0 | 75.0 | 0.0 | 2.0 | 75.0 | The Name of the Rose | 75.0 | ... | 6.20956 | 0.0 | 314.0 | 111.0 | 1413.0 | 1.301030 | 37.0 | 1206.0 | 200.0 | 544.0 |
Stefan Feld | 37790 | 40831 | boardgame | 2.0 | 45.0 | 10.0 | 2.0 | 45.0 | The Pillars of the Earth: Builders Duel | 45.0 | ... | 6.10638 | 0.0 | 336.0 | 95.0 | 2153.0 | 1.218110 | 87.0 | 1178.0 | 111.0 | 269.0 |
Stefan Feld | 41199 | 55670 | boardgame | 4.0 | 100.0 | 12.0 | 2.0 | 50.0 | Macao | 100.0 | ... | 7.17430 | 0.0 | 1474.0 | 480.0 | 5973.0 | 1.321800 | 88.0 | 5784.0 | 788.0 | 1980.0 |
Stefan Feld | 41460 | 56931 | boardgame | 2.0 | 45.0 | 8.0 | 2.0 | 30.0 | Arena: Roma II | 45.0 | ... | 6.44404 | 0.0 | 628.0 | 137.0 | 2488.0 | 1.249270 | 100.0 | 1737.0 | 218.0 | 616.0 |
Stefan Feld | 44403 | 66193 | boardgame | 5.0 | 30.0 | 8.0 | 2.0 | 30.0 | It Happens.. | 30.0 | ... | 5.84443 | 0.0 | 225.0 | 70.0 | 1230.0 | 1.246720 | 82.0 | 815.0 | 41.0 | 113.0 |
Stefan Feld | 44546 | 66505 | boardgame | 5.0 | 45.0 | 8.0 | 2.0 | 45.0 | The Speicherstadt | 45.0 | ... | 6.77778 | 0.0 | 1057.0 | 332.0 | 4153.0 | 1.192350 | 131.0 | 4060.0 | 182.0 | 585.0 |
Stefan Feld | 45924 | 70512 | boardgame | 4.0 | 100.0 | 12.0 | 1.0 | 60.0 | Luna | 100.0 | ... | 6.97857 | 0.0 | 820.0 | 259.0 | 4156.0 | 1.310370 | 72.0 | 3243.0 | 449.0 | 1331.0 |
Stefan Feld | 47906 | 84876 | boardgame | 4.0 | 90.0 | 12.0 | 2.0 | 30.0 | The Castles of Burgundy | 90.0 | ... | 7.99551 | 0.0 | 4743.0 | 1567.0 | 34345.0 | 1.229360 | 341.0 | 24876.0 | 1057.0 | 5468.0 |
Stefan Feld | 49549 | 91873 | boardgame | 5.0 | 60.0 | 12.0 | 3.0 | 60.0 | Strasbourg | 60.0 | ... | 6.67775 | 0.0 | 481.0 | 176.0 | 2280.0 | 1.166200 | 46.0 | 2094.0 | 260.0 | 587.0 |
Stefan Feld | 52153 | 102680 | boardgame | 4.0 | 120.0 | 12.0 | 2.0 | 60.0 | Trajan | 120.0 | ... | 7.59432 | 0.0 | 1914.0 | 659.0 | 11411.0 | 1.343930 | 186.0 | 9632.0 | 889.0 | 3319.0 |
Stefan Feld | 56313 | 119591 | boardgame | 5.0 | 45.0 | 10.0 | 2.0 | 45.0 | Rialto | 45.0 | ... | 6.62988 | 0.0 | 693.0 | 233.0 | 3805.0 | 1.169250 | 143.0 | 2955.0 | 173.0 | 648.0 |
Stefan Feld | 58665 | 127060 | boardgame | 4.0 | 120.0 | 12.0 | 2.0 | 60.0 | Bora Bora | 120.0 | ... | 7.34024 | 0.0 | 1444.0 | 492.0 | 9030.0 | 1.331640 | 208.0 | 6482.0 | 530.0 | 1817.0 |
Stefan Feld | 62390 | 136888 | boardgame | 4.0 | 60.0 | 10.0 | 2.0 | 60.0 | Bruges | 60.0 | ... | 7.25115 | 0.0 | 1506.0 | 428.0 | 8315.0 | 1.259580 | 132.0 | 7065.0 | 831.0 | 2526.0 |
Stefan Feld | 62592 | 137408 | boardgame | 4.0 | 90.0 | 10.0 | 2.0 | 90.0 | Amerigo | 90.0 | ... | 7.11283 | 0.0 | 907.0 | 272.0 | 5341.0 | 1.241560 | 119.0 | 3897.0 | 395.0 | 1287.0 |
Stefan Feld | 68382 | 154246 | boardgame | 4.0 | 60.0 | 10.0 | 2.0 | 30.0 | La Isla | 60.0 | ... | 6.65128 | 0.0 | 744.0 | 205.0 | 5529.0 | 1.122200 | 214.0 | 3315.0 | 152.0 | 570.0 |
Stefan Feld | 70249 | 159508 | boardgame | 4.0 | 100.0 | 12.0 | 2.0 | 100.0 | AquaSphere | 100.0 | ... | 7.03241 | 0.0 | 940.0 | 264.0 | 7231.0 | 1.288210 | 223.0 | 4184.0 | 207.0 | 1029.0 |
Stefan Feld | 81555 | 191977 | boardgame | 4.0 | 60.0 | 12.0 | 1.0 | 30.0 | The Castles of Burgundy: The Card Game | 60.0 | ... | 6.75652 | 0.0 | 551.0 | 57.0 | 5409.0 | 1.172040 | 120.0 | 2368.0 | 192.0 | 861.0 |
Stefan Feld | 82082 | 193558 | boardgame | 4.0 | 100.0 | 12.0 | 2.0 | 70.0 | The Oracle of Delphi | 100.0 | ... | 6.73406 | 0.0 | 359.0 | 38.0 | 2501.0 | 1.245720 | 30.0 | 1571.0 | 320.0 | 1347.0 |
Stefan Feld | 82142 | 193739 | boardgame | 5.0 | 90.0 | 10.0 | 2.0 | 45.0 | JórvĆk | 90.0 | ... | 5.97219 | 0.0 | 134.0 | 9.0 | 1126.0 | 1.325370 | 26.0 | 482.0 | 150.0 | 609.0 |
Stefan Feld | 88742 | 213984 | boardgame | 5.0 | 75.0 | 10.0 | 2.0 | 45.0 | Notre Dame: 10th Anniversary | 75.0 | ... | 0.00000 | 0.0 | 12.0 | 1.0 | 100.0 | 1.181890 | 0.0 | 20.0 | 50.0 | 223.0 |
Stefan Feld | 88746 | 214000 | boardgame | 5.0 | 100.0 | 12.0 | 2.0 | 75.0 | In the Year of the Dragon: 10th Anniversary | 100.0 | ... | 0.00000 | 0.0 | 14.0 | 0.0 | 125.0 | 0.857459 | 0.0 | 19.0 | 37.0 | 199.0 |
25 rows Ć 32 columns
Iniciando a anÔlise direcionada as perguntas guia, vamos verificar que jogos estão no top 20 do BGG. à importante ressaltar que este conjunto de dados é de 2017, ou seja, não representa o top 20 atual do site.
df2 = df.set_index('details.name')
top_games = df2['stats.average'].sort_values(ascending=False)
top_games = top_games[:20]
top_games.sort_values(ascending=True, inplace=True)
plt.figure(figsize=(15,15))
plt.title("Top 20 Games", size=20)
ay = top_games.plot.barh(x='stats.average', y='details.name')
plt.xlabel("Nota", size=20)
plt.ylabel("TĆtulo", size=20)
ay.set(xlim=(8, 10))
ay.xaxis.set_ticks(np.arange(7, 10, 0.25))
for tick in ay.xaxis.get_major_ticks():
tick.label.set_fontsize(18)
for tick in ay.yaxis.get_major_ticks():
tick.label.set_fontsize(18)
for i, v in enumerate(top_games):
ay.text(v+.05, i-.15, str(v)[:4], fontsize=16)
O grƔfico nos mostra que Tournament at Camelot Ʃ o jogo mais bem avaliado atƩ o momento. A cƩlula abaixo nos mostra que o jogo era um lancamento recente de 2017 e, na Ʃpoca em que os dados foram coletados, apenas 34 usuƔrios tinham avaliado o jogo. Hoje, o jogo conta com mais de 900 avaliaƧƵes e sua nota mƩdia Ʃ de 7.2 pontos.
df2 = df.set_index('details.name')
df2.loc[['Tournament at Camelot']][['details.yearpublished', 'stats.usersrated']]
details.yearpublished | stats.usersrated | |
---|---|---|
details.name | ||
Tournament at Camelot | 2017.0 | 34.0 |
Agora que sabemos os jogos melhor avaliados, vamos descobrir quem são os Game Designers queridinhos da comunidade do Board Game Geek. Para isso, vamos pegar a média das avaliações dos jogos de cada autor.
per_game_designer = df.groupby('attributes.boardgamedesigner')['stats.average'].mean()
per_game_designer
top_game_designers = per_game_designer.nlargest(20)
top_game_designers = top_game_designers.sort_values(ascending=True)
plt.figure(figsize=(15,15))
plt.title("Top 20 Game Designers", size=20)
ax = top_game_designers.plot.barh()
plt.xlabel("Nota", size=20)
plt.ylabel("Game Designer", size=20)
ax.set(xlim=(7, 10))
ax.xaxis.set_ticks(np.arange(7, 10, 0.25))
for tick in ax.xaxis.get_major_ticks():
tick.label.set_fontsize(18)
for tick in ax.yaxis.get_major_ticks():
tick.label.set_fontsize(18)
for i, v in enumerate(top_game_designers):
ax.text(v +.05, i-.15, str(v)[:4], fontsize=16)
Em primeiro lugar, encontramos um grupo de autores, mas vamos reparar na nota mƩdia que seus jogos receberam: 9.41. Se compararmos com a nota do primeiro lugar dentre os jogos, vemos que trata-se do mesmo jogo. Vamos confirmar isso na cƩlula a seguir
df2 = df.set_index('attributes.boardgamedesigner')
df2.loc[['Karen Boginski,Jody Boginski-Barbessi,Kenneth C. Shannon, III']][['details.name']]
details.name | |
---|---|
attributes.boardgamedesigner | |
Karen Boginski,Jody Boginski-Barbessi,Kenneth C. Shannon, III | Tournament at Camelot |
Podemos verificar a correlação entre a nota do jogo, descrita por stats.average com os outros parĆ¢metros, por exemplo, o nĆvel de dificuldade stats.averageweight, a idade mĆnima details.minage e a quantidade mĆ”xima e mĆnima de jogadores, respectivamente stats.maxplayers e stats.minplayers.
Para essa anÔlise, iremos utilizar apenas os jogos que possuem mais de 100 avaliações, dessa forma, podemos remover jogos que tiveram poucos jogadores, ou seja, que não são tão relevantes para a comunidade em si ou são muito novos e ainda não estabilizaram seu lugar dentro do ranking.
cmap = sn.choose_diverging_palette(as_cmap=False)
usersrated_100 = df[df['stats.usersrated'] >= 100]
df_corr = ['stats.average', 'stats.averageweight']
for col in usersrated_100:
if 'details' in col:
df_corr.append(col)
sn.set(font_scale=1.4)
df_corr_cols = usersrated_100[df_corr]
df_corr_cols.round(decimals=2)
corrMatrix = df_corr_cols.corr()
mask = np.tril(np.ones_like(corrMatrix, dtype=np.bool))
az = plt.figure(figsize=(10,10))
heatmap = sn.heatmap(corrMatrix.round(decimals=2), mask=mask, linecolor='white', linewidths=.5,
cmap=cmap, vmin=-1, vmax=1, square=True, annot=True)
Avaliando a matriz de correlação acima, identificamos alguns padrƵes jĆ” esperados, por exemplo, a alta correlação entre o tempo de jogo dos usuĆ”rios, representado por details.playingtime, e os tempos mĆnimos e mĆ”ximos esperados, respectivamente details.minplaytime e details.maxplaytime. Ć interessante notar tambĆ©m a alta correlação entre a nota do jogo (stats.average) e sua dificuldade (stats.averageweight). Isso indica que, de acordo com a comunidade, jogos mais difĆceis tendem ter notas mais altas.
Podemos ver que existe uma certa correlação entre a duração de uma partida e sua dificuldade. De fato, por experiĆŖncia própria, posso dizer que jogos mais difĆceis e complexos, ou seja, com valor de stats.averageweight alto, tendem a ter partidas bem mais longas que jogos mais simples, os chamados party games ou fillers. TambĆ©m podemos identificar um grau de correlação entre a dificuldade do jogo e a idade mĆnima, o que jĆ” Ć© esperado, visto que crianƧas muito novas teriam dificuldades com jogos complexos, como Twilight Imperium e Mage Knight.
df2 = df.set_index('details.name')
mk_weight = df2.loc['Mage Knight Board Game']['stats.averageweight']
gp_weight = df2.loc['Twilight Imperium']['stats.averageweight']
print("O grau de dificuldade de Mage Knight Ć© {} e o de Twilight Imperium Ć© {}, ambos em uma escala de 0 a 5".format(mk_weight.round(decimals=2), gp_weight.round(decimals=2)))
O grau de dificuldade de Mage Knight Ć© 4.21 e o de Twilight Imperium Ć© 3.49, ambos em uma escala de 0 a 5
De acordo com a matriz de correlação acima, jogos mais difĆceis sĆ£o os favoritos dos usuĆ”rios do BGG, mas, como podemos ver abaixo, eles nĆ£o sĆ£o a maioria! Na verdade, a maior parte dos jogos estĆ” no nĆvel de dificuldade considerado "Simples", entre 2 e 3.
numberxweight = usersrated_100['stats.averageweight'].round()
numberxweight.plot(kind="hist", figsize=(10,10), colormap='Accent')
plt.title("NĆŗmero de jogos publicados x Dificuldade")
plt.xlabel("Dificuldade")
plt.ylabel("NĆŗmero de Jogos")
Text(0, 0.5, 'NĆŗmero de Jogos')
Vamos verificar a relação entre dificuldade e nota em cada jogo, assim poderemos descobrir se hĆ” mesmo uma tendĆŖncia dos jogos mais difĆceis serem melhor avaliados.
averagexweight = usersrated_100[['details.name', 'stats.averageweight', 'stats.average']]
averagexweight = averagexweight.sort_values('stats.averageweight', ascending=False)
sn.set(font_scale=1.4)
sn.lmplot('stats.average', 'stats.averageweight', averagexweight,
scatter_kws={"s": 15, 'color':'darkgreen'}, line_kws={'color': 'magenta'}, height=10)
# \n
plt.title('Distribuição: Dificuldade x Nota')
plt.xlabel('Nota')
plt.ylabel('Dificuldade')
Text(26.89399999999999, 0.5, 'Dificuldade')