Creando gráficos vectoriales con Python

Cuando tratamos con datos y queremos representarlos en gráficos, en ocasiones necesitamos generar esos gráficos en un formato que permita escalar a cualquier resolución y sin perder calidad. Si además necesitamos cierta interactividad, poder crear representaciones gráficas en el formato SVG es una muy buena opción.

Los gráficos vectoriales están compuestos por texto en XML, lo que permite que cualquier navegador que respete los estándares W3C los represente sin problema.

Pandas y Pygal para gráficos SVG en Python

Para la demostración, vamos a utilizar un dataset que nos permita mostrar distintos tipos de representaciones gráficas del mismo. En este caso, utilizaré un dataset de Pokémon, donde podremos ver las características de 800 pokémon de 6 generaciones distintas.

Utilizaremos Pandas para crear un dataframe con esta estructura, con el que trabajaremos en los siguientes pasos.

Dataframe de Pandas representando la estructura de datos que vamos a utilizar.

Para generar los gráficos en SVG utilizaremos la librería de Python Pygal.

Empecemos con algo sencillo, vamos a realizar una gráfica simple para ver cuántos pokémon hay de cada generación.

Para ello, haremos un agrupado por generación y sacaremos el número de pokémon de cada una de ellas con este simple código.

pie_chart = pygal.Pie()
pie_chart.title = 'Pokemon by Generation'
for id, row in data.groupby(["Generation"]).size().reset_index().iterrows():
  pie_chart.add("Generation %s" % row["Generation"], row[0])
pie_chart.render_to_file('pie_chart_by_generation.svg')
Pokemon by Generation166391.4928844728903171.48743594049068106437.4147128241523284.8629929327153160362.8301650050641378.7126447794734121238.79006958495955356.81312547254106165203.08996784921055233.9325693087051882280.5752923931324153.02568661574566Pokemon by GenerationGeneration 1Generation 2Generation 3Generation 4Generation 5Generation 6

Bien, ahora podemos profundizar un poco más y realizar una representación que represente un grupo mayor de datos. En ella, mostraremos la distribución de los principales atributos agrupados por tipo de Pokémon. Para ello, generaremos una gráfica del tipo Box Plot que nos permitirá ver de forma sencilla todos estos datos agrupados con el siguiente código:

for this_attribute in ["HP", "Attack", "Defense"]:
  box_plot = pygal.Box(box_mode="tukey")
  box_plot.title = 'Pokemon Attribute: %s' % this_attribute

  for this_type in data["Type 1"].unique():
    box_plot.add(this_type, data[data["Type 1"]==this_type]["HP"].values)
  
  box_plot.render_to_file('box_plot_attribute_%s.svg' % this_attribute)

Lo que nos permite ver las gráficas de los tres atributos por separado.

Pokemon Attribute: HP002020404060608080100100120120140140160160180180200200220220240240Min: 30 Lower Whisker: 30 Q1: 50 Q2: 65.5 Q3: 75 Upper Whisker: 100 Max: 12320.342307692307692320.7541478129714Min: 38 Lower Whisker: 38 Q1: 58 Q2: 70 Q3: 80 Upper Whisker: 110 Max: 11543.996153846153845309.1349924585219Min: 20 Lower Whisker: 20 Q1: 51.5 Q2: 70 Q3: 90.5 Upper Whisker: 130 Max: 17067.65307.17119155354453Min: 1 Lower Whisker: 20 Q1: 45 Q2: 60 Q3: 70 Upper Whisker: 86 Max: 8691.30384615384614333.68250377073906Min: 30 Lower Whisker: 30 Q1: 55 Q2: 70 Q3: 90 Upper Whisker: 140 Max: 255114.9576923076923299.6432880844646Min: 35 Lower Whisker: 35 Q1: 50 Q2: 67.5 Q3: 80.5 Upper Whisker: 105 Max: 105138.6115384615385315.02639517345403Min: 20 Lower Whisker: 20 Q1: 50 Q2: 60 Q3: 70 Upper Whisker: 90 Max: 90162.26538461538462330.73680241327304Min: 10 Lower Whisker: 10 Q1: 54.5 Q2: 75 Q3: 92.5 Upper Whisker: 115 Max: 115185.91923076923078312.08069381598796Min: 35 Lower Whisker: 35 Q1: 54.75 Q2: 78 Q3: 91.25 Upper Whisker: 126 Max: 126209.57307692307694299.6432880844646Min: 30 Lower Whisker: 30 Q1: 50 Q2: 70 Q3: 79.5 Upper Whisker: 120 Max: 144233.2269230769231311.26244343891403Min: 20 Lower Whisker: 20 Q1: 50 Q2: 68 Q3: 80 Upper Whisker: 120 Max: 190256.88076923076926315.02639517345403Min: 30 Lower Whisker: 30 Q1: 50 Q2: 68.5 Q3: 80 Upper Whisker: 123 Max: 123280.53461538461534310.6078431372549Min: 20 Lower Whisker: 20 Q1: 45 Q2: 59.5 Q3: 64.5 Upper Whisker: 90 Max: 150304.18846153846147334.33710407239823Min: 36 Lower Whisker: 36 Q1: 53 Q2: 70 Q3: 90 Upper Whisker: 110 Max: 110327.84230769230766308.1530920060332Min: 41 Lower Whisker: 41 Q1: 67 Q2: 80 Q3: 102.5 Upper Whisker: 125 Max: 125351.4961538461538289.66063348416293Min: 35 Lower Whisker: 35 Q1: 50.5 Q2: 65 Q3: 74.25 Upper Whisker: 100 Max: 126375.15319.36312217194575Min: 40 Lower Whisker: 40 Q1: 57 Q2: 60 Q3: 75 Upper Whisker: 100 Max: 100398.8038461538461316.9901960784314Min: 40 Lower Whisker: 40 Q1: 59.5 Q2: 79 Q3: 82 Upper Whisker: 85 Max: 85422.45769230769224312.5716440422323Pokemon Attribute: HPGrassFireWaterBugNormalPoisonElectricGroundFairyFightingPsychicRockGhostIceDragonDarkSteelFlying Pokemon Attribute: Attack002020404060608080100100120120140140160160180180Min: 27 Lower Whisker: 27 Q1: 55 Q2: 70 Q3: 94 Upper Whisker: 132 Max: 13220.342307692307692259.6093117408907Min: 30 Lower Whisker: 30 Q1: 61.5 Q2: 84.5 Q3: 102 Upper Whisker: 160 Max: 16043.996153846153845233.253036437247Min: 10 Lower Whisker: 10 Q1: 53 Q2: 72 Q3: 92 Upper Whisker: 150 Max: 15567.65260.0485829959514Min: 10 Lower Whisker: 10 Q1: 45 Q2: 65 Q3: 90 Upper Whisker: 155 Max: 18591.30384615384614265.31983805668017Min: 5 Lower Whisker: 10 Q1: 55 Q2: 70.5 Q3: 85 Upper Whisker: 130 Max: 160114.9576923076923271.68927125506076Min: 43 Lower Whisker: 43 Q1: 60 Q2: 74 Q3: 91 Upper Whisker: 106 Max: 106138.6115384615385261.3663967611336Min: 30 Lower Whisker: 30 Q1: 52.5 Q2: 65 Q3: 85 Upper Whisker: 123 Max: 123162.26538461538462269.49291497975713Min: 40 Lower Whisker: 40 Q1: 72 Q2: 85 Q3: 122 Upper Whisker: 180 Max: 180185.91923076923078206.45748987854256Min: 20 Lower Whisker: 20 Q1: 43.75 Q2: 52 Q3: 74 Upper Whisker: 80 Max: 131209.57307692307694307.16042510121457Min: 35 Lower Whisker: 35 Q1: 80 Q2: 100 Q3: 120 Upper Whisker: 145 Max: 145233.2269230769231214.8036437246964Min: 20 Lower Whisker: 20 Q1: 45 Q2: 57 Q3: 96.25 Upper Whisker: 165 Max: 190256.88076923076926257.3031376518219Min: 40 Lower Whisker: 40 Q1: 59.5 Q2: 95 Q3: 120.5 Upper Whisker: 165 Max: 165280.53461538461534214.8036437246964Min: 30 Lower Whisker: 30 Q1: 52.5 Q2: 66 Q3: 93.5 Upper Whisker: 120 Max: 165304.18846153846147266.6376518218624Min: 30 Lower Whisker: 30 Q1: 50 Q2: 67 Q3: 90 Upper Whisker: 130 Max: 130327.84230769230766264.4412955465587Min: 50 Lower Whisker: 50 Q1: 85.5 Q2: 113.5 Q3: 134.5 Upper Whisker: 180 Max: 180351.4961538461538178.12449392712554Min: 50 Lower Whisker: 50 Q1: 65 Q2: 88 Q3: 102.5 Upper Whisker: 150 Max: 150375.15225.56578947368425Min: 24 Lower Whisker: 50 Q1: 76.25 Q2: 89 Q3: 110 Upper Whisker: 150 Max: 150398.8038461538461216.89018218623488Min: 30 Lower Whisker: 30 Q1: 50 Q2: 85 Q3: 107.5 Upper Whisker: 115 Max: 115422.45769230769224255.4362348178138Pokemon Attribute: AttackGrassFireWaterBugNormalPoisonElectricGroundFairyFightingPsychicRockGhostIceDragonDarkSteelFlying Pokemon Attribute: Defense002020404060608080100100120120140140160160180180200200220220Min: 30 Lower Whisker: 30 Q1: 50 Q2: 66 Q3: 85 Upper Whisker: 131 Max: 13120.342307692307692294.2926421404682Min: 37 Lower Whisker: 37 Q1: 50 Q2: 64 Q3: 78 Upper Whisker: 120 Max: 14043.996153846153845299.01003344481603Min: 20 Lower Whisker: 20 Q1: 54 Q2: 70 Q3: 89 Upper Whisker: 133 Max: 18067.65292.84113712374585Min: 30 Lower Whisker: 30 Q1: 48.75 Q2: 60 Q3: 90 Upper Whisker: 140 Max: 23091.30384615384614291.8432274247492Min: 5 Lower Whisker: 5 Q1: 43 Q2: 60 Q3: 75 Upper Whisker: 120 Max: 126114.9576923076923315.7023411371237Min: 35 Lower Whisker: 35 Q1: 52.5 Q2: 67 Q3: 82.5 Upper Whisker: 120 Max: 120138.6115384615385296.10702341137124Min: 15 Lower Whisker: 15 Q1: 49.5 Q2: 65 Q3: 80 Upper Whisker: 115 Max: 115162.26538461538462307.9005016722408Min: 25 Lower Whisker: 25 Q1: 52.5 Q2: 84.5 Q3: 110 Upper Whisker: 160 Max: 160185.91923076923078268.8913043478261Min: 28 Lower Whisker: 28 Q1: 49.5 Q2: 66 Q3: 77.5 Upper Whisker: 95 Max: 95209.57307692307694310.9849498327759Min: 30 Lower Whisker: 30 Q1: 53.5 Q2: 70 Q3: 79.75 Upper Whisker: 95 Max: 95233.2269230769231306.53971571906357Min: 15 Lower Whisker: 15 Q1: 48 Q2: 65 Q3: 81.25 Upper Whisker: 130 Max: 160256.88076923076926302.5480769230769Min: 40 Lower Whisker: 40 Q1: 71 Q2: 100 Q3: 122 Upper Whisker: 168 Max: 200280.53461538461534243.85284280936455Min: 30 Lower Whisker: 30 Q1: 60 Q2: 72.5 Q3: 114 Upper Whisker: 145 Max: 145304.18846153846147272.7015050167224Min: 15 Lower Whisker: 15 Q1: 47.5 Q2: 75 Q3: 85 Upper Whisker: 110 Max: 184327.84230769230766304.99749163879596Min: 35 Lower Whisker: 35 Q1: 67.5 Q2: 90 Q3: 100 Upper Whisker: 130 Max: 130351.4961538461538272.3386287625418Min: 30 Lower Whisker: 30 Q1: 50.5 Q2: 70 Q3: 90 Upper Whisker: 125 Max: 125375.15293.0225752508361Min: 50 Lower Whisker: 50 Q1: 96.25 Q2: 120 Q3: 150 Upper Whisker: 230 Max: 230398.8038461538461191.14506688963212Min: 35 Lower Whisker: 35 Q1: 52.5 Q2: 75 Q3: 80 Upper Whisker: 80 Max: 80422.45769230769224308.626254180602Pokemon Attribute: DefenseGrassFireWaterBugNormalPoisonElectricGroundFairyFightingPsychicRockGhostIceDragonDarkSteelFlying

Si lo que necesitamos es poder comparar los atributos principales entre ciertos Pokémon, crearemos un gráfico del tipo radar que nos permita ver estos datos de una forma sencilla, de la siguiente manera:

x_labels = ['HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed']

radar_chart = pygal.Radar(width=600, height=500)
radar_chart.title = 'Pokemon Attributes'
radar_chart.x_labels = x_labels
radar_chart.add('Bulbasaur', data[data["Name"]=="Bulbasaur"][x_labels].values[0])
radar_chart.render_to_file('radar_plot_attribute_Bulbasaur.svg') 
display(SVG('radar_plot_attribute_Bulbasaur.svg'))
Pokemon Attributes60605050404030302020101000HP1.570796327Attack2.617993878Defense3.665191429Sp. Atk4.71238898Sp. Def5.759586532Speed6.806784083Pokemon Attributes45220.274.30769230769232HP4987.091155245679134.75641025641022Attack4987.09115524567893279.2435897435897Defense65220.19999999999996398.6666666666667Sp. Atk65396.77295732716044302.8333333333334Sp. Def45342.4428166111112140.65384615384625SpeedBulbasaur

Este tipo de gráficos nos permite hacer comparaciones de datos de una forma sencilla y comprensible, mostrando un grupo de datos comparativos amplios a la vez. Por ejemplo, podemos comparar los atributos de estos cuatro Pokémon legendarios para ver sus diferencias de un vistazo.

Pokemon Attributes140140120120100100808060604040202000HP1.570796327Attack2.617993878Defense3.665191429Sp. Atk4.71238898Sp. Def5.759586532Speed6.806784083Pokemon Attributes90221.494.98701298701297HP100106.11739754817539144.77056277056272Attack90117.64565779335781263.0064935064935Defense125221.39999999999995362.5735930735931Sp. Atk85319.3902120840509259.89502164502164Sp. Def90325.1543422066422150.99350649350657Speed90221.494.98701298701297HP85123.40978791594908154.1049783549783Attack100106.11739754817535269.22943722943717Defense95221.39999999999995325.23593073593076Sp. Atk125365.5032530647807284.7867965367966Sp. Def85319.390212084051154.10497835497847Speed106221.475.07359307359309HP11094.58913730299294138.54761904761898Attack90117.64565779335781263.0064935064935Defense154221.39999999999995398.6666666666667Sp. Atk90325.1543422066422263.0064935064936Sp. Def130371.2673831873721126.10173160173173Speed90221.494.98701298701297HP90117.64565779335786150.99350649350646Attack85123.40978791594904259.89502164502164Defense125221.39999999999995362.5735930735931Sp. Atk90325.1543422066422263.0064935064936Sp. Def100336.6826024518247144.77056277056283SpeedMoltresArticunoMewtwoZapdos

Si lo que queremos es ver la media de cada uno de los atributos de los Pokémon, pero esta vez agrupados por tipo, de forma que nos facilite, por ejemplo, encontrar qué tipo tiene, de media, el ataque o la defensa más altos, podemos mostrarlo de la siguiente forma:

x_labels = ['HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed']
dot_chart = pygal.Dot(x_label_rotation=30, width=600, height=600)
dot_chart.title = 'Pokemon attributes by Type'
dot_chart.x_labels = x_labels
for id, row in data.groupby(["Type 1"])[x_labels].mean().reset_index().iterrows():
  dot_chart.add(row["Type 1"], row[x_labels])

dot_chart.render_to_file('dot_chart_attribute_by_type.svg') 
Pokemon attributes by TypeBug17.5Dark16.5Dragon15.5Electric14.5Fairy13.5Fighting12.5Fire11.5Flying10.5Ghost9.5Grass8.5Ground7.5Ice6.5Normal5.5Poison4.5Psychic3.5Rock2.5Steel1.5Water0.5HPAttackDefenseSp. AtkSp. DefSpeed56.8840579739.3885734752328923.107905982905947HP70.97101449102.9185306933504523.107905982905947Attack70.72463768166.4484879114680223.107905982905947Defense53.86956522229.978445129585623.107905982905947Sp. Atk64.79710145293.508402347703123.107905982905947Sp. Def61.68115942357.038359565820723.107905982905947Speed66.8064516139.3885734752328949.97756410256409HP88.38709677102.9185306933504549.97756410256409Attack70.22580645166.4484879114680249.97756410256409Defense74.64516129229.978445129585649.97756410256409Sp. Atk69.51612903293.508402347703149.97756410256409Sp. Def76.16129032357.038359565820749.97756410256409Speed83.312539.3885734752328976.84722222222223HP112.125102.9185306933504576.84722222222223Attack86.375166.4484879114680276.84722222222223Defense96.84375229.978445129585676.84722222222223Sp. Atk88.84375293.508402347703176.84722222222223Sp. Def83.03125357.038359565820776.84722222222223Speed59.7954545539.38857347523289103.71688034188031HP69.09090909102.91853069335045103.71688034188031Attack66.29545455166.44848791146802103.71688034188031Defense90.02272727229.9784451295856103.71688034188031Sp. Atk73.70454545293.5084023477031103.71688034188031Sp. Def84.5357.0383595658207103.71688034188031Speed74.1176470639.38857347523289130.58653846153845HP61.52941176102.91853069335045130.58653846153845Attack65.70588235166.44848791146802130.58653846153845Defense78.52941176229.9784451295856130.58653846153845Sp. Atk84.70588235293.5084023477031130.58653846153845Sp. Def48.58823529357.0383595658207130.58653846153845Speed69.8518518539.38857347523289157.45619658119654HP96.77777778102.91853069335045157.45619658119654Attack65.92592593166.44848791146802157.45619658119654Defense53.11111111229.9784451295856157.45619658119654Sp. Atk64.7037037293.5084023477031157.45619658119654Sp. Def66.07407407357.0383595658207157.45619658119654Speed69.9038461539.38857347523289184.32585470085468HP84.76923077102.91853069335045184.32585470085468Attack67.76923077166.44848791146802184.32585470085468Defense88.98076923229.9784451295856184.32585470085468Sp. Atk72.21153846293.5084023477031184.32585470085468Sp. Def74.44230769357.0383595658207184.32585470085468Speed70.7539.38857347523289211.19551282051282HP78.75102.91853069335045211.19551282051282Attack66.25166.44848791146802211.19551282051282Defense94.25229.9784451295856211.19551282051282Sp. Atk72.5293.5084023477031211.19551282051282Sp. Def102.5357.0383595658207211.19551282051282Speed64.437539.38857347523289238.0651709401709HP73.78125102.91853069335045238.0651709401709Attack81.1875166.44848791146802238.0651709401709Defense79.34375229.9784451295856238.0651709401709Sp. Atk76.46875293.5084023477031238.0651709401709Sp. Def64.34375357.0383595658207238.0651709401709Speed67.2714285739.38857347523289264.93482905982904HP73.21428571102.91853069335045264.93482905982904Attack70.8166.44848791146802264.93482905982904Defense77.5229.9784451295856264.93482905982904Sp. Atk70.42857143293.5084023477031264.93482905982904Sp. Def61.92857143357.0383595658207264.93482905982904Speed73.7812539.38857347523289291.8044871794872HP95.75102.91853069335045291.8044871794872Attack84.84375166.44848791146802291.8044871794872Defense56.46875229.9784451295856291.8044871794872Sp. Atk62.75293.5084023477031291.8044871794872Sp. Def63.90625357.0383595658207291.8044871794872Speed7239.38857347523289318.67414529914527HP72.75102.91853069335045318.67414529914527Attack71.41666667166.44848791146802318.67414529914527Defense77.54166667229.9784451295856318.67414529914527Sp. Atk76.29166667293.5084023477031318.67414529914527Sp. Def63.45833333357.0383595658207318.67414529914527Speed77.275510239.38857347523289345.54380341880335HP73.46938776102.91853069335045345.54380341880335Attack59.84693878166.44848791146802345.54380341880335Defense55.81632653229.9784451295856345.54380341880335Sp. Atk63.7244898293.5084023477031345.54380341880335Sp. Def71.55102041357.0383595658207345.54380341880335Speed67.2539.38857347523289372.41346153846155HP74.67857143102.91853069335045372.41346153846155Attack68.82142857166.44848791146802372.41346153846155Defense60.42857143229.9784451295856372.41346153846155Sp. Atk64.39285714293.5084023477031372.41346153846155Sp. Def63.57142857357.0383595658207372.41346153846155Speed70.6315789539.38857347523289399.28311965811963HP71.45614035102.91853069335045399.28311965811963Attack67.68421053166.44848791146802399.28311965811963Defense98.40350877229.9784451295856399.28311965811963Sp. Atk86.28070175293.5084023477031399.28311965811963Sp. Def81.49122807357.0383595658207399.28311965811963Speed65.3636363639.38857347523289426.15277777777777HP92.86363636102.91853069335045426.15277777777777Attack100.7954545166.44848791146802426.15277777777777Defense63.34090909229.9784451295856426.15277777777777Sp. Atk75.47727273293.5084023477031426.15277777777777Sp. Def55.90909091357.0383595658207426.15277777777777Speed65.2222222239.38857347523289453.0224358974359HP92.7037037102.91853069335045453.0224358974359Attack126.3703704166.44848791146802453.0224358974359Defense67.51851852229.9784451295856453.0224358974359Sp. Atk80.62962963293.5084023477031453.0224358974359Sp. Def55.25925926357.0383595658207453.0224358974359Speed72.062539.38857347523289479.892094017094HP74.15178571102.91853069335045479.892094017094Attack72.94642857166.44848791146802479.892094017094Defense74.8125229.9784451295856479.892094017094Sp. Atk70.51785714293.5084023477031479.892094017094Sp. Def65.96428571357.0383595658207479.892094017094SpeedPokemon attributes by TypeBugDarkDragonElectricFairyFightingFireFlyingGhostGrassGroundIceNormalPoisonPsychicRockSteelWater

Pero si lo que queremos ver es cómo son los atributos de un Pokémon concreto respecto al resto, podemos representarlo de la siguiente manera, lo que nos permitiría incluso comparar atributos entre diferentes Pokémon de forma sencilla.

pokemon_names = ["Pikachu", "Dragonite"]
x_labels = ['HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed']

for pokemon_name in pokemon_names:
  this_pokemon = data[data["Name"]==pokemon_name]

  gauge = pygal.SolidGauge(inner_radius=0.70)
  gauge.title = '%s stats vs others Pokemon' % pokemon_name
  for this_label in x_labels:
    gauge.add(this_label, [{'value': this_pokemon[this_label].values[0], 'max_value': data[this_label].max()}])

  gauge.render_to_file('gauge_%s_vs_all.svg' % pokemon_name) 
Pikachu stats vs others Pokemon35198.2568300816166727.14714196816772355563.128631582253547.1813274565239640205.17587323816252208.826854841452350558.6984820770726220.0401566240202650212.76762268943932392.185696772389690577.485445.0Pikachu stats vs others Pokemon255351905523040194502305018090HPAttackDefenseSp. AtkSp. DefSpeed Dragonite stats vs others Pokemon91231.1164456261389359.40505582406426134563.812714768591129.9246099127647795235.36022350907496248.630886835774100577.404670544551270.3063588926622100236.46094010985098431.147697351311480576.4506358638362433.17716382354706Dragonite stats vs others Pokemon255911901342309519410023010018080HPAttackDefenseSp. AtkSp. DefSpeed

Conclusión

De esta forma, con representaciones de nuestros datos en gráficos SVG, podemos representar esos datos a cualquier tamaño y escala, sin preocuparnos por tener una resolución específica y obteniendo un resultado óptimo.

Además, herramientas como Pygal, nos permiten integrar este tipo de gráficos no sólo en Dashboards o presentaciones puntuales, también en todo tipo de intranets, webapps o páginas webs donde podemos adaptarlas sin problemas al entorno visual y respetando el diseño original.

¡Esto es todo! Si este post te ha parecido interesante, te animamos a visitar la categoría Data Analytics para ver todos los posts relacionados y a compartirlo en redes. ¡Hasta pronto!

Imagen por defecto
David Martín
Artículos: 14