Introducción a la Visualización con Python (y 2)

Francisco Palm
qu4nt
Published in
18 min readJan 6, 2019
Fotografía cortesía de Pexels.com

En la entrega anterior conversé sobre cómo utilizar cuadernos de Júpiter para trabajar con visualización de datos. Aunque haya quien no le parezca muy científico el ejemplo que estoy utilizando, lo cierto es que resulta sumamente útil para poder explicar la potencialidades de esta herramienta en la visualización con Python. En esta entrega hablaremos de lo que nos dicen los datos.

La narrativa de los datos

Uno de los aspectos fundamentales de la visualización es dejar que los datos nos cuenten una historia. El conjunto de datos está diseñado para intentar predecir los resultados de un combate partir de las características de los tipos de pokemons involucrados y los resultados almacenados.

Pero hay muchas cosas que nos pueden interesar, comparar los pokemons de distintos tipos, estudiar las relaciones entre sus características, y realizar comparaciones a lo largo de las distintas generaciones. Podemos relacionar los factores anteriores con su porcentaje de victorias en el registro histórico de combates.

En general, vamos a:

  1. Indagar en los criterios de los diseñadores del videojuego para mantener el balance y el interés.
  2. Todo esto con un objetivo adicional: demostrar los tipos fundamentales de gráficos que podemos generar con las herramientas básicas de Python.

Comparación entre categorías

Empecemos por un poco de demografía del universo pokemon, ¿cuáles son los tipos de Pokemon que tienen la mayor variedad de especies conocidas?. Simplemente contemos cuantas veces se repite cada tipo de Pokemon y hagamos un gráfico de barras.

Para poner colores acordes a los distintos tipos de pokemons utilicemos una paleta de colores adecuada, cortesía de la Bulbapedia:

pk_colors = ['#A8B820',  # Bug,
'#705848', # Dark,
'#7038F8', # Dragon
'#F8D030', # Electric
'#EE99AC', # Fairy
'#C03028', # Fighting
'#F08030', # Fire
'#A890F0', # Flying
'#705898', # Ghost
'#78C850', # Grass
'#E0C068', # Ground
'#98D8D8', # Ice
'#A8A878', # Normal
'#A040A0', # Poison
'#F85888', # Psychic
'#B8A038', # Rock
'#B8B8D0', # Steel
'#6890F0', # Water
]

¡Vamos con nuestro primer gráfico!, un gráfico de barras horizontales que sirve para comparar el número de especies conocidas hasta la generación 6 para cada tipo de Pokemon.

pkt_cnt = pokemon["Type 1"].value_counts(sort=False).sort_index()
pkt_cnt = pd.concat([pkt_cnt, pd.DataFrame(pk_colors,
index=pkt_cnt.index,
columns=["Colors"])], axis=1)
pkt_cnt.sort_values("Type 1", inplace=True)
pkt_cnt_bar = pkt_cnt.plot(kind='barh', y="Type 1", color=pkt_cnt.Colors,
legend=False, figsize=(8, 8))
pkt_cnt_bar.set_title("Number of known species\nfor each Pokemon main type",
fontsize=16, weight="bold")
pkt_cnt_bar.set_xlabel("Number of species")
Text(0.5,0,'Number of species')

Para el gráfico anterios se realiza un conteo del número de especies por cada tipo usando el método value_counts() y se reordena por el índice para que coincida con la paleta de colores. En la línea 2 se añade la paleta como una columna del dataframe para que se reordene con el dataframe.

Esta información no es nada nueva para los fanáticos de la serie, los tipos de Pokemon con el mayor número de especies son “Agua”, “Normal” y “Planta”. Curiosamente el “Agua” es más normal que “Normal”. Los que tienen como tipo principal “Volador” son los más escasos.

Entonces, ¿queremos comparar entre los distintos tipos de pokemon?. Empecemos por definir un data frame donde podamos compararlos. Para esto utilicemos el método pivot_table() de pandas:

pkt = pokemon.pivot_table(index="Type 1",
values=["Attack", "Defense", "HP",
"Sp. Atk", "Sp. Def", "Speed"],
aggfunc='mean')
pkt.head()
Salida de Jupiter

Para comparar el poder total medio de los distintos tipos de Pokemons podemos generar un gráfico similar al anterior:

pkt.sort_values("Total_Power", ascending=True, inplace=True)
pkt_bar = pkt.plot(kind="barh", y="Total_Power", figsize=(8, 8),
color=pkt.Color, legend=False)
pkt_bar.set_xlim((350, 575))
pkt_bar.set_xlabel("Mean Total Power")
pkt_bar.set_title("Mean Total Power for each Pokemon Main Type",
fontsize=16, weight="bold")
Text(0.5,1,'Mean Total Power for each Pokemon Main Type')

Ordenar las barras es bastante útil porque facilita la comparación de valores y apreciar las diferencias por mínimas que sean. La disposición horizontal es conveniente en este caso para facilitar la colocación de las etiquetas para cada columna.

De aquí se puede ver que con diferencia los pokemon tipo Dragón tienen en promedio el mayor poder total, y los pokemon tipo Bicho los que menos. Es de esperar ya que los pokemon de tipo Dragón son unos de los que tienen la mayor proporción de pokemon legendarios como podremos ver a continuación.

Relación de una parte en el total

¿Qué proporción de los pokemon son legendarios?, ¿qué tan frecuentes son?, ¿qué tan frecuentes de acuerdo al tipo de Pokemon?. Empecemos construyendo una tabla pivote similar al caso anterior pero en base a la columna Legendary. Y a continuación contamos el total de Pokemons legendarios por cada tipo de Pokemon, y que fracción representan del total.

pkl = pokemon.pivot_table(index="Type 1",
values=["Legendary"],
aggfunc="sum")
pkl.sort_index()
pkl.Legendary = pkl.Legendary.astype(int)
pkl["Total"] = pokemon["Type 1"].value_counts()
pkl["Other"] = pkl.Total - pkl.Legendary
pkl["Ratio"] = pokemon.pivot_table(index="Type 1",
values=["Legendary"],
aggfunc="mean")
pkl["Percent"] = pkl["Ratio"] * 100
pkl["Color"] = pk_colors
pkl.head()
Salida de Jupiter

Para tener una rápida idea de la proporción de pokemon legendarios dentro del total de pokemons, podemos hacer lo siguiente:

ax = pokemon.Legendary.value_counts().plot.pie(startangle=45,
autopct='%.1f %%',
figsize=(8, 6))
ax.set_title("Proportion of Legendary Pokemons",
fontsize=16, weight="bold")
Text(0.5,1,'Proportion of Legendary Pokemons')

Dos cosas interesantes que notar:

Gracias a la evaluación perezosa podemos encadenar métodos como Legendary.value_counts().plot.pie(), y

El método plot() del DataFrame de Pandas se corresponde con el método plot() de matplotlib.pyplot, por lo que para acceder a la documentación y ejemplos debemos investigar en ambos paquetes. Tal como sugiere la forma de trabajar con matplotlib, podemos obtener el objeto ax (de la clase Axes) para realizar configuraciones sobre los ejes, en este caso para añadir el título al grafico.

De igual manera, se genera el gráfico que muestre la distribución de los legendarios por tipo de Pokemon.

Empezamos por seleccionar solamente los tipos de pokemon que tienen al menos un legendario, para que no aparezcan las etiquetas correspondientes en el gráfico. Como hemos añadido la paleta de colores como columna esta se modificará junto con los datos.

pkl_notnull = pkl[pkl.Legendary != 0]
pkl_notnull.head()
Salida de Jupiter
l_total = sum(pkl_notnull.Legendary)
ax = pkl_notnull.Legendary.plot(kind="pie", label="",
colors=pkl_notnull.Color,
autopct='%.1f%%', pctdistance=0.8,
explode=pkl_notnull.Legendary / l_total,
figsize=(8, 6))
ax.set_title("Legendary Proportions by Pokemon Type",
fontsize=16, weight="bold")
Text(0.5,1,'Legendary Proportions by Pokemon Type')

En este caso, la amplitud de la “explosión” de cada sector es proporcional a la fracción de pokemon legendarios que pertenecen a cada tipo.

Se debe asignar "" al parámetro label para que no imprima una etiqueta que colisiona con los etiquetas de los sectores luego de la "explosión". Este tipo de ajustes "finos" con frecuencia son necesarios.

Se puede notar que los tipos Dragon y Psíquico son a los que pertenecen el mayor número de pokemon legendarios. Esto respondería a la pregunta, “dado que un pokemon es legendario qué probabilidad hay de que sea de un tipo determinado”.

Ahora bien, si se quiere responder a la pregunta inversa: “dado el tipo de un pokemon qué probabilidad hay que sea legendario”, lo que es equivalente a decir ¿qué fracción de especies legendarias hay en cada tipo de Pokemon?

cols = ["Legendary", "Other"]
fig, axes = plt.subplots(6, 3, figsize=(10, 10))
for i, idx in enumerate(pkl.index):
ax = axes[i // 3, i % 3]
ax.pie(pkl[cols].T[idx], labels=cols,
startangle=30, autopct='%.1f %%')
ax.set_title(idx, fontdict={"size":"large",
"weight":"bold"})

fig.subplots_adjust(wspace=.5, hspace=.5)
plt.suptitle("Proportion of Legendary for each Pokemon Type",
fontsize=16, weight="bold")
Text(0.5,0.98,'Proportion of Legendary for each Pokemon Type')

Matplotlib permite generar este tipo de gráficos múltiples de forma “manual”. A través del método subplots obtenemos las referencias a la figura fig y a los ejes axes. A través de axes se controla la ubicación de los gráficos individuales y a través de fig los parámetros "internos" del gráfico como la separación entre gráficos. Con la referencia a pyplot plt se controlan los parámetros "externos" del gráfico como el título superior.

Esta forma de trabajar es engorrosa. Los gráficos se van colocando manualmente en el arreglo de subplots, los índices [i // 3, i % 3] son un buen ejemplo de por qué los índices de las secuencias de Python (y la mayoría de los lenguajes de programación) comienzan en 0 y no en 1. Si el arreglo de subplots tiene 3 columnas, la división entera i // 3 devuelve la fila y el resto i % 3 devuelve la columna.

Como hablamos de responder “la pregunta inversa”: ¿qué fracción de especies legendarias hay en cada tipo de Pokemon?; los gráficos se generan usando la tabla transpuesta pk_legendary[cols].T[idx]

Los tipos de Pokemon que cuentan con la mayor proporción de legendarios son los de tipo “Volador” (como tipo principal), seguido de tipo Dragón y tipo Psíquico.

Mostrando cambios en el tiempo

En el presente conjunto de datos la única referencia temporal que tenemos son las generaciones. Puede ser de interés estudiar como han cambiado las estadísticas base de los distintos Pokemon a lo largo de las generaciones, con esta finalidad vamos a generar una tabla pivote de acuerdo al campo Generation.

pkg = pokemon.pivot_table(index="Generation",
values=["Attack", "Defense", "HP",
"Sp. Atk", "Sp. Def", "Speed"],
aggfunc='mean')
pkg.head()
Salida de Jupiter

Ahora vamos a probar algo diferente, en lugar de distribuir manualmente los gráficos como subplots de matplotlib, vamos a utilizar objetos FacetGrid de seaborn que facilitan la generación de gráficos de relaciones entre variables condicionados por una o más variables adicionales.

Este es un concepto frecuente utilizado en sistemas de gráficos como Trellis o ggplot2 en el lenguaje R.

Para lograrlo es conveniente convertir los datos al formato “largo” de forma que los valores de interés se encuentren en una sola columna con otra columna indicando la pertenencia a los distintos campos mediante variables categóricas. Esto lo logramos con relativa facilidad mediante el métdodo melt() de Pandas.

pkg_long = pkg.reset_index().melt(id_vars="Generation")
pkg_long.head(10)
Salida de Jupiter

Por defecto, melt() crea un campo variable donde coloca las variables (columnas) de la tabla original como categorías (filas), y un campo value donde coloca los valores correspondientes en la tabla. Las variables que se desea dejar intactas se indican en id_vars.

En el caso código anterior es conveniente quitar Generation como índice del DataFrame y añadirlo como id_var de melt() para que genere una columna donde se repita la secuencia de las generaciones para cada categoría en variable.

Como se puede ver, pasamos de una matriz de # generacionesfilas x #camposcolumnas a otra de (# generaciones * # campos)filas x 1columna.

pkg_long.info()
Salida Jupiter

Ahora simplemente le decimos a Seaborn que cree una malla de gráficos en los que cada fila se corresponderá con las categorías de variable y luego indicamos que cada fila será un gráfico de círculos marker='o' unidos por líneas de guiones linestyle='dashed', donde el eje x corresponde a Generation y el eje y a value.

g = sns.FacetGrid(pkg_long, row="variable", size=1.5, aspect=6)
g = g.map(plt.plot, "Generation", "value",
linestyle='dashed', marker='o')
g.fig.suptitle("Mean Pokemon Stats along Generations",
fontsize=16, weight="bold", y=1.05)
Text(0.5,1.05,'Mean Pokemon Stats along Generations')

Se puede observar que la mayoría de las estadísticas aumentaron para la generación 4 y hay una disminución hacia la 6ta generación. Esto puede definir un “arco” en la saga.

Para simplificarlo, podemos reducir todas estas estadísticas a un único valor, llamémoslo Total_Power, veamos como ha evolucionado a lo largo de las distintas generaciones.

pkg["Total_Power"] = pkg.sum(axis=1)

Considerando que los creadores de la serie tienen dos formas de introducir especies llamativas, ya sea por su poder o por el número de pokemons legendarios, revisemos también el número de legendarios introducidos a lo largo de las generaciones.

ax1 = pkg.Total_Power.plot(kind="line", linestyle="dashed",
marker="o", figsize=(10, 4))
pkg["Legendary"] = pokemon.groupby("Generation")[["Legendary"]].sum()
ax2 = pkg.Legendary.plot(kind="line", linestyle="dashed",
marker="o", secondary_y=True,
ax=ax1)
ax1.set_ylabel("Total Power")
ax2.set_ylabel("# of Legendary")
ax1.legend(loc="upper left")
ax2.legend(loc="upper right")
ax1.set_title("Mean Pokemon Stats along Generations",
fontsize=16, weight="bold", y=1.05)
Text(0.5,1.05,'Mean Pokemon Stats along Generations')

Podemos ver como para añadir interés en la serie se ha intercambiado el énfasis en la introducción de especies de Pokemon poderosas con la introducción de especies de Pokemon tipo Legendario.

Este gráfico además es un ejemplo de como superponer gráficos con distintos ejes Y mediante la opción secondary_y=True, y cómo manejar los gráficos independientemente mediante los objetos Axes: ax1 y ax2.

Conexiones y Relaciones

Vamos a revisar como podemos generar gráficos que expresen relaciones entre variables cuantitativas. Siendo el más utilizado el diagrama de dispersión (scatter en inglés). Simplemente se especifica el tipo de gráfico kind='scatter', y las variables correspondientes a cada eje.

Como ejemplo inicial revisemos la relación entre el nivel de ataque y el nivel de defensa para todas las especies de Pokemon conocidas.

ax = pokemon.plot(kind='scatter',
x='Attack', y='Defense',
alpha = 0.33, color = 'red',
figsize=(8, 8))
ax.set_xlabel('Attack')
ax.set_ylabel('Defense')
ax.set_title('Attack vs Defense for every known Pokemon species',
fontsize=16, weight="bold")
Text(0.5,1,'Attack vs Defense for every known Pokemon species')

Nótese como es posible utilizar el objeto plt (alias de Matplotlib.Pyplot) para introducir las configuraciones globales del gráfico. Como hay una gran densidad de puntos se configuran con transparencia para distinguir mejor los patrones de valores más frecuentes.

Es fácil ver que en general los pokemons con mayor ataque también tienen mayor defensa. Los pokemon se hacen más poderosos de forma integral. Hay unos pocos pokemon especializados, como uno con muy alta defensa y poco ataque y otro caso a la inversa.

pokemon[(pokemon.Attack < 25) & (pokemon.Defense > 200)]
Salida Jupiter
pokemon[(pokemon.Attack > 175) & (pokemon.Defense < 50)]
Salida Jupiter

¡Vamos a ponerlos como anotaciones sobre el gráfico!

ax = pokemon.plot(kind='scatter',
x='Attack', y='Defense',
alpha = 0.33, color = 'red',
figsize=(8, 8))
ax.set_xlabel('Attack')
ax.set_ylabel('Defense')
ax.set_title('Attack vs Defense for every known Pokemon species',
fontsize=16, weight="bold")
pk1 = pokemon[(pokemon.Attack < 25) & (pokemon.Defense > 200)]
ax.annotate(pk1.Name.iloc[0],
xy=(pk1.Attack, pk1.Defense), xycoords='data',
xytext=(30, -10), textcoords='offset points',
arrowprops=dict(arrowstyle="->", lw=2))
pk2 = pokemon[(pokemon.Attack > 175) & (pokemon.Defense < 50)]
ax.annotate("\n".join(pk2.Name.iloc[0].split()),
xy=(pk2.Attack, pk2.Defense), xycoords='data',
xytext=(-100, 0), textcoords='offset points',
arrowprops=dict(arrowstyle="->", lw=2))
Text(-100,0,'DeoxysAttack\nForme')

Mediante Seaborn podemos generar con facilidad diagramas de dispersión con modelos de ajuste. Veamos un ejemplo utilizando el método regplot() (gráfico de regresión).

fig, ax = plt.subplots()
fig.set_size_inches(8, 8)
sns.regplot(x="Attack", y="Speed", data=pokemon,
scatter_kws={'color':'green', 'alpha':0.3},
line_kws={'color':'red'})
ax.set_title('Attack vs Speed for every known Pokemon species\n+ Regression Line',
fontsize=16, weight="bold")
Text(0.5,1,'Attack vs Speed for every known Pokemon species\n+ Regression Line')

El parámetro scatter_kws (scatter keywords) aplica a las propiedades de los puntos de la dispersión, y line_kws (line keywords) se utiliza para introducir parámetros específicos para la línea del ajuste.

También el crecimiento del Ataque viene acompañado con un incremento en la Velocidad.

Seaborn cuenta con una buena cantidad de gráficos predefinidos sumamente útiles y atractivos para el análisis estadístico. Uno de estos es jointplot().

g = sns.jointplot(x="Defense", y="Speed",
data=pokemon, kind="reg",
line_kws={'color':'green'},
scatter_kws={'alpha': 0.33})
g.fig.set_size_inches(8, 8)
g.fig.suptitle("Defense vs Speed joinplot\nfor every known Pokemon species",
fontsize=16, weight="bold", y=1.05)
Text(0.5,1.05,'Defense vs Speed joinplot\nfor every known Pokemon species')

Si bien en general, en nuestro conjunto de datos, el crecimiento de una de las variables implica crecimiento de las otras, lo que se conoce como correlación positiva. En el caso de “Defense”, un aumento de esta implica un incremento muy reducido de “Speed”, lo cual tiene sentido porque mejorar la defensa requiere del uso de armaduras y otros dispositivos pesados. Esto nos habla del esfuerzo que han puesto los diseñadores del juego para darle coherencia y equilibrio.

El gráfico ofrece una vista conveniente de la distribución de las variables en cada eje, y muestra valor del coeficiente de regresión de Pearson pearsonr =0.015 y el nivel de significancia p=0.67, lo que indica que este coeficiente es despreciable.

Un gráfico equivalente basado en hexágonos es más adecuado para apreciar la distribución de los valores.

g = sns.jointplot(x="Defense", y="Speed",
data=pokemon, kind="hex")
g.fig.set_size_inches(8, 8)
g.fig.suptitle("Defense vs Speed hexagon joinplot\nfor every known Pokemon species",
fontsize=16, weight="bold", y=1.05)

Para relacionar variables numéricas con categóricas podemos utilizar otro tipo de gráficos como el de caja. Por ejemplo, para comparar el poder total entre Pokemon legendarios y no legendarios.

pokemon["Total_Power"] = pokemon.iloc[:,range(3, 9)].sum(axis=1)
pokemon.head()
Salida de Jupiter

El siguiente es un ejemplo utilizando MatPlotlib a través de Pandas.

ax = pokemon.boxplot(column='Total_Power',
by='Legendary',
figsize=(10, 5))
ax.set_ylabel("Total Power")
ax.get_figure().gca().set_title("")
plt.suptitle('Legendary vs non Legendary Total Power',
fontsize=16, weight="bold")
Text(0.5,0.98,'Legendary vs non Legendary Total Power')

Todos los Pokemon legendarios están muy por encima de la media, ¡ya lo sabíamos!

Una comparación similar entre generaciones, pero utilizando Seaborn.

fig, ax = plt.subplots()
fig.set_size_inches(10, 5)
sns.boxplot(x='Generation', y='Total_Power', data=pokemon)
ax.set_title('Total Power boxplot by Generations',
fontsize=16, weight="bold")
Text(0.5,1,'Total Power boxplot by Generations')

En general hay bastante equilibrio entre las distintas generaciones, siendo la 3era generación la más variada y la 4ta con los Pokemon más poderosos en promedio.

Relación con los resultados de los combates

Jugando con los datos podríamos estudiar varias hipótesis. Por ejemplo, decir que los pokemon que atacan primero tienen mayor probabilidad de ganar. Parece tener sentido, ya que se tiene oportunidad de disminuir antes los Hit Points. Veamos:

fig, ax = plt.subplots()
fig.set_size_inches(10, 5)
first_win = sum(combats.First_pokemon == combats.Winner)
second_win = sum(combats.Second_pokemon == combats.Winner)
sns.barplot(x=["First Wins", "Second Wins"],
y=[first_win, second_win])
ax.set_title('Battle winners count by first attacker order',
fontsize=16, weight="bold")
Text(0.5,1,'Battle winners count by first attacker order')

Encontramos que la hipótesis no se corrobora con la evidencia. De hecho, parece ser al contrario. Esto podría explicarse por el carácter estratégico del juego, el segundo jugador tiene oportunidad de ver “con qué viene” su atacante.

Vamos a convertir los datos de los combates en una tasa de victorias para luego relacionarla con las estadísticas de los Pokemon.

first_combats = combats['First_pokemon'].value_counts().sort_index()
second_combats = combats['Second_pokemon'].value_counts().sort_index()
winner_combats = combats['Winner'].value_counts().sort_index()
winner_combats = winner_combats.reindex(first_combats.index, fill_value=0)
battle_stats = pd.concat([first_combats, second_combats, winner_combats], axis=1)
battle_stats["total_combats"] = total_combats = first_combats + second_combats
battle_stats["win_pct"] = winner_combats / total_combats
battle_stats.head()
Salida de Jupiter
pokemon["win_pct"] = battle_stats.win_pct
pokemon.head()
Salida de Jupiter

Veamos, ¿qué relación hay entre el poder total y el porcentaje de victorias?

g = sns.jointplot(x="Total_Power", y="win_pct",
data=pokemon, kind="reg",
line_kws={'color':'green'})
g.fig.set_size_inches(8, 8)
g.fig.suptitle("Total Power vs Winning Percentage joinplot\nfor every known Pokemon species",
fontsize=16, weight="bold", y=1.05)
Text(0.5,1.05,'Total Power vs Winning Percentage joinplot\nfor every known Pokemon species')

Como era previsible, los Pokemon más poderosos tienden a ganar más. Sin embargo, la correlación y la variabilidad en el gráfico muestran que hay bastantes casos que se escapan de la norma. Vale la pena hilar más fino.

Una forma de investigar rápidamente la relación cruzada entre todas las variables de un conjunto de datos es un gráfico “de parejas” o pairplot(). Dado que es un gráfico estadístico sofísticado es de esperar encontrarlo en un paquete como Seaborn.

fig, axes = plt.subplots(2, 3, figsize=(10, 10))

for i, col in enumerate(pokemon.columns[3:9]):
ax = axes[i // 3, i % 3]
sns.regplot(data=pokemon, ax=ax,
y="win_pct", x=col,
line_kws={"color": "g"},
scatter_kws={"alpha": 0.2, "color": "r"})
if (i % 3 > 0):
ax.set_ylabel("")
ax.set_yticklabels([])
ax.set_ylim((0, 1))
fig.suptitle("Winning Percentage vs Pokemon Stats pairgrid\nfor every known Pokemon species",
fontsize=16, weight="bold", y=0.95)
Text(0.5,0.95,'Winning Percentage vs Pokemon Stats pairgrid\nfor every known Pokemon species')

Este gráfico muestra que los factores más influyentes en la victoria son la Velocidad, el Ataque y la Velocidad de Ataque. Pero en particular la Velocidad es de lejos el factor más determinante, en la medida que aumenta la velocidad del Pokemon el porcentaje de victorias aumenta prácticamente en la misma proporción.

Quizás con un mapa de calor podemos ver esta información más fácilmente y con mayor detalle.

f,ax = plt.subplots(figsize=(10, 6))
g = sns.heatmap(pokemon[["HP", "Attack", "Defense", "Sp. Atk",
"Sp. Def", "Speed", "Total_Power", "win_pct"]].corr(),
annot=True, linewidths=.5, fmt= '.1f', ax=ax)
plt.suptitle("Pokemon Mean Stats and Wining Ratio\nCorrelation Heatmap",
fontsize=16, weight="bold", y=1)
Text(0.5,1,'Pokemon Mean Stats and Wining Ratio\nCorrelation Heatmap')

En este mapa de calor se verifica que la Velocidad es con mucho el factor más determinante en la tasa de victorias de los pokemons. La correlación de 0.9 indica que un aumento en la Velocidad implica un aumento proporcional directo determinante en las probabilidades de victoria.

Este gráfico muestra de una forma muy concisa y útil las relaciones entre todas las magnitudes de interés en nuestro conjunto de datos.

Finalmente, veamos como utilizar un gráfico de tipo swarmplot() para tener una visión general de como se distribuye el porcentaje de victorias de las especies para los distintos tipos de Pokemon.

fig, axes = plt.subplots(3, 6, figsize=(10, 12))
pkt_win = pokemon[["Type 1", "win_pct"]]
pkt_win = pkt_win.sort_values("Type 1")

grouped = pkt_win.groupby('Type 1')
for i, (key, group) in enumerate(grouped):
ax = axes[i // 6, i % 6]
sns.boxplot(x="Type 1", y="win_pct",
color="white",
data=group, ax=ax)
sns.swarmplot(y='win_pct', color=pk_colors[i],
data=group, ax=ax)
if (i % 6 > 0):
ax.set_ylabel("")
ax.set_yticklabels([])
ax.set_xlabel(key)
ax.set_ylim((0, 1))

plt.suptitle("Winning Percentage Distribution by Main Pokemon Type",
fontsize=16, weight="bold", y=0.92)
Text(0.5,0.92,'Winning Percentage Distribution by Main Pokemon Type')

En este gráfico final podemos ver la distribución del Porcentaje Ganador Promedio para cada especie de Pokemon clasificado por Tipo. Se puede observar como los Pokemons muy númerosos tienen una distribución más o menos uniforme a lo largo de todo el rango. Hay tipos de Pokemon especialmente “perdedores” como Hada, Roca y Acero, y otros Pokemon “ganadores” como Volador, Dragón y Eléctrico.

Llama la atención que siendo los Pokemon de tipo Hada entre los últimos que se han introducido a la franquicia sean los más perdedores, sería de esperar que fuesen más atractivos. De todos modos, no tenemos información de las circunstancias en las que fueron adquiridos los datos.

Como curiosidad final podemos hacer una consulta sobre cuáles son los pokemon más ganadores:

pokemon[pokemon.win_pct >= 0.95]

Como se ha dicho antes, ya que no conocemos las condiciones ni los métodos con los que se recogieron los datos de los combates es difícil llegar a conclusiones. Sin embargo, uno esperaría que entre los Pokemon más ganadores hubiesen más Legendarios, o hubiese al menos uno de tipo Eléctrico o Dragón. Sabemos que las batallas Pokemon son estratégicamente complejas, depende de la selección de los ataques y de la interacción entre los tipos de Pokemon entre otros factores.

Observaciones Finales

En este Cuaderno de Jupyter se trabajó en la visualización en base a los datos del desafío de Kaggle: Pokemon’s — Weedle’s Cave. Se siguieron las pautas básicas de un análisis exploratorio de datos que sirvieran como ejemplo, sin dar muchas explicaciones, sobre cuales son los tipos de gráficos más convenientes según determinados propósitos.

Igualmente, sin explicar todos los detalles, se muestran ejemplos de codificación que contienen muchos tips y elementos útiles para los que empiecen a trabajar con la visualización en Python.

  • Se dan varios ejemplos del manejo de una paleta de colores cualitativa que debe mantenerse relacionada correctamente aunque se alteren el orden o se filtren los datos originales.
  • Igualmente se muestran varios ejemplos de manipulaciones de índices, tablas pivote, conversión a formato largo y agrupamiento de datos sobre DataFrames de Pandas.
  • Se muestran ejemplos de la graficación directa con Matplotlib, del uso de Matplotlib a través de Pandas y de Seaborn como una interfaz de Matplotlib de alto nivel.
  • Se muestran ejemplos de la generación de gráficos multivariantes por facetas, utilizando las estructuras manuales de Matplotlib, la forma de alto nivel que ofrece Seaborn mediante funciones como facetgrid(), habiendo hacia el final un ejemplo de combinación de estas dos estrategias.
  • Aunque la códificación no es del todo coherente, ni se pretendía una gran optimización, hay aquí y allá ejemplos que se consideran valiosos para cuestiones como manejo de títulos y etiquetas en los ejes. También analizando las diferencias en que se implementan estas personalizaciones es posible comprender mejor la relación jerárquica entre los componentes de Matplotlib y Seaborn.

Hay muchas otras herramientas de graficación en Python. En especial, no se incluyeron las herramientas para la generación de gráficos interactivos. De todas maneras, estas herramientas son las fundamentales y es relativamente sencillo extrapolar lo visto a alternativas como mpld3 que siguiendo la misma filosofía permite generar gráficos para D3.js, y también para otras herramientas que han sido influenciadas por Matplotlib como Bokeh o Plotly.

No puedo trabajar con Matplotlib sin recordarme de John Hunter

--

--

Francisco Palm
qu4nt
Writer for

geomática poética, geomancia matemática, hacktivista, Python & R, Pop & Rock Indie, Otaku & Geek, Zen & K, Utopía & Emancipación