Cómo tener grupos de barras apiladas

Resuelto jrjc asked hace 10 años • 10 respuestas

Así es como se ve mi conjunto de datos:

In [1]: df1=pd.DataFrame(np.random.rand(4,2),index=["A","B","C","D"],columns=["I","J"])

In [2]: df2=pd.DataFrame(np.random.rand(4,2),index=["A","B","C","D"],columns=["I","J"])

In [3]: df1
Out[3]: 
          I         J
A  0.675616  0.177597
B  0.675693  0.598682
C  0.631376  0.598966
D  0.229858  0.378817

In [4]: df2
Out[4]: 
          I         J
A  0.939620  0.984616
B  0.314818  0.456252
C  0.630907  0.656341
D  0.020994  0.538303

Quiero tener un diagrama de barras apiladas para cada marco de datos, pero como tienen el mismo índice, me gustaría tener 2 barras apiladas por índice.

Intenté trazar ambos en los mismos ejes:

In [5]: ax = df1.plot(kind="bar", stacked=True)

In [5]: ax2 = df2.plot(kind="bar", stacked=True, ax = ax)

Pero se superpone.

Luego intenté concatenar los dos conjuntos de datos primero:

pd.concat(dict(df1 = df1, df2 = df2),axis = 1).plot(kind="bar", stacked=True)

pero aquí todo está apilado

Mi mejor intento es:

 pd.concat(dict(df1 = df1, df2 = df2),axis = 0).plot(kind="bar", stacked=True)

Lo que da :

ingrese la descripción de la imagen aquí

Esto es básicamente lo que quiero, excepto que quiero que la barra esté ordenada como

(df1,A) (df2,A) (df1,B) (df2,B) etc...

¡Supongo que hay un truco pero no lo encuentro!


Después de la respuesta de @bgschiller obtuve esto:

ingrese la descripción de la imagen aquí

Que es casi lo que quiero. Me gustaría que la barra estuviera agrupada por índice para tener algo visualmente claro.

Bonificación : tener la etiqueta x no redundante, algo como:

df1 df2    df1 df2
_______    _______ ...
   A          B
jrjc avatar Apr 01 '14 20:04 jrjc
Aceptado

Finalmente encontré un truco (editar: consulte a continuación el uso de marcos de datos de formato largo y seaborn):

Solución con pandas y matplotlib

Aquí lo tenéis con un ejemplo más completo:

import pandas as pd
import matplotlib.cm as cm
import numpy as np
import matplotlib.pyplot as plt

def plot_clustered_stacked(dfall, labels=None, title="multiple stacked bar plot",  H="/", **kwargs):
    """Given a list of dataframes, with identical columns and index, create a clustered stacked bar plot. 
labels is a list of the names of the dataframe, used for the legend
title is a string for the title of the plot
H is the hatch used for identification of the different dataframe"""

    n_df = len(dfall)
    n_col = len(dfall[0].columns) 
    n_ind = len(dfall[0].index)
    axe = plt.subplot(111)

    for df in dfall : # for each data frame
        axe = df.plot(kind="bar",
                      linewidth=0,
                      stacked=True,
                      ax=axe,
                      legend=False,
                      grid=False,
                      **kwargs)  # make bar plots

    h,l = axe.get_legend_handles_labels() # get the handles we want to modify
    for i in range(0, n_df * n_col, n_col): # len(h) = n_col * n_df
        for j, pa in enumerate(h[i:i+n_col]):
            for rect in pa.patches: # for each index
                rect.set_x(rect.get_x() + 1 / float(n_df + 1) * i / float(n_col))
                rect.set_hatch(H * int(i / n_col)) #edited part     
                rect.set_width(1 / float(n_df + 1))

    axe.set_xticks((np.arange(0, 2 * n_ind, 2) + 1 / float(n_df + 1)) / 2.)
    axe.set_xticklabels(df.index, rotation = 0)
    axe.set_title(title)

    # Add invisible data to add another legend
    n=[]        
    for i in range(n_df):
        n.append(axe.bar(0, 0, color="gray", hatch=H * i))

    l1 = axe.legend(h[:n_col], l[:n_col], loc=[1.01, 0.5])
    if labels is not None:
        l2 = plt.legend(n, labels, loc=[1.01, 0.1]) 
    axe.add_artist(l1)
    return axe

# create fake dataframes
df1 = pd.DataFrame(np.random.rand(4, 5),
                   index=["A", "B", "C", "D"],
                   columns=["I", "J", "K", "L", "M"])
df2 = pd.DataFrame(np.random.rand(4, 5),
                   index=["A", "B", "C", "D"],
                   columns=["I", "J", "K", "L", "M"])
df3 = pd.DataFrame(np.random.rand(4, 5),
                   index=["A", "B", "C", "D"], 
                   columns=["I", "J", "K", "L", "M"])

# Then, just call :
plot_clustered_stacked([df1, df2, df3],["df1", "df2", "df3"])
    

Y da eso:

gráfico de barras apiladas múltiples

Puedes cambiar los colores de la barra pasando un cmapargumento:

plot_clustered_stacked([df1, df2, df3],
                       ["df1", "df2", "df3"],
                       cmap=plt.cm.viridis)

Solución con seaborn:

Dados los mismos df1, df2, df3, a continuación, los convierto en formato largo:

df1["Name"] = "df1"
df2["Name"] = "df2"
df3["Name"] = "df3"
dfall = pd.concat([pd.melt(i.reset_index(),
                           id_vars=["Name", "index"]) # transform in tidy format each df
                   for i in [df1, df2, df3]],
                   ignore_index=True)

El problema con seaborn es que no apila barras de forma nativa, por lo que el truco consiste en trazar la suma acumulada de cada barra una encima de la otra:

dfall.set_index(["Name", "index", "variable"], inplace=1)
dfall["vcs"] = dfall.groupby(level=["Name", "index"]).cumsum()
dfall.reset_index(inplace=True) 

>>> dfall.head(6)
  Name index variable     value       vcs
0  df1     A        I  0.717286  0.717286
1  df1     B        I  0.236867  0.236867
2  df1     C        I  0.952557  0.952557
3  df1     D        I  0.487995  0.487995
4  df1     A        J  0.174489  0.891775
5  df1     B        J  0.332001  0.568868

Luego recorre cada grupo variabley traza la suma acumulada:

c = ["blue", "purple", "red", "green", "pink"]
for i, g in enumerate(dfall.groupby("variable")):
    ax = sns.barplot(data=g[1],
                     x="index",
                     y="vcs",
                     hue="Name",
                     color=c[i],
                     zorder=-i, # so first bars stay on top
                     edgecolor="k")
ax.legend_.remove() # remove the redundant legends 

gráfico de barras de pila múltiple seaborn

Creo que le falta la leyenda que se puede agregar fácilmente. El problema es que en lugar de sombreados (que se pueden agregar fácilmente) para diferenciar los marcos de datos, tenemos un gradiente de luminosidad, y es demasiado claro para el primero, y realmente no sé cómo cambiar eso sin cambiar cada uno. rectángulo uno por uno (como en la primera solución).

Dime si no entiendes algo en el código.

No dude en reutilizar este código que se encuentra en CC0.

jrjc avatar Apr 03 '2014 18:04 jrjc

Este es un gran comienzo, pero creo que los colores podrían modificarse un poco para mayor claridad. También tenga cuidado al importar todos los argumentos en Altair, ya que esto puede causar colisiones con objetos existentes en su espacio de nombres. Aquí hay un código reconfigurado para mostrar el color correcto al apilar los valores:

Gráfico de columnas agrupadas de Altair

Importar paquetes

import pandas as pd
import numpy as np
import altair as alt

Generar algunos datos aleatorios.

df1=pd.DataFrame(10*np.random.rand(4,3),index=["A","B","C","D"],columns=["I","J","K"])
df2=pd.DataFrame(10*np.random.rand(4,3),index=["A","B","C","D"],columns=["I","J","K"])
df3=pd.DataFrame(10*np.random.rand(4,3),index=["A","B","C","D"],columns=["I","J","K"])

def prep_df(df, name):
    df = df.stack().reset_index()
    df.columns = ['c1', 'c2', 'values']
    df['DF'] = name
    return df

df1 = prep_df(df1, 'DF1')
df2 = prep_df(df2, 'DF2')
df3 = prep_df(df3, 'DF3')

df = pd.concat([df1, df2, df3])

Trazar datos con Altair

alt.Chart(df).mark_bar().encode(

    # tell Altair which field to group columns on
    x=alt.X('c2:N', title=None),

    # tell Altair which field to use as Y values and how to calculate
    y=alt.Y('sum(values):Q',
        axis=alt.Axis(
            grid=False,
            title=None)),

    # tell Altair which field to use to use as the set of columns to be  represented in each group
    column=alt.Column('c1:N', title=None),

    # tell Altair which field to use for color segmentation 
    color=alt.Color('DF:N',
            scale=alt.Scale(
                # make it look pretty with an enjoyable color pallet
                range=['#96ceb4', '#ffcc5c','#ff6f69'],
            ),
        ))\
    .configure_view(
        # remove grid lines around column clusters
        strokeOpacity=0    
    )
Grant Langseth avatar Oct 31 '2017 16:10 Grant Langseth

Me las arreglé para hacer lo mismo usando pandas y subtramas matplotlib con comandos básicos.

He aquí un ejemplo:

fig, axes = plt.subplots(nrows=1, ncols=3)

ax_position = 0
for concept in df.index.get_level_values('concept').unique():
    idx = pd.IndexSlice
    subset = df.loc[idx[[concept], :],
                    ['cmp_tr_neg_p_wrk', 'exp_tr_pos_p_wrk',
                     'cmp_p_spot', 'exp_p_spot']]     
    print(subset.info())
    subset = subset.groupby(
        subset.index.get_level_values('datetime').year).sum()
    subset = subset / 4  # quarter hours
    subset = subset / 100  # installed capacity
    ax = subset.plot(kind="bar", stacked=True, colormap="Blues",
                     ax=axes[ax_position])
    ax.set_title("Concept \"" + concept + "\"", fontsize=30, alpha=1.0)
    ax.set_ylabel("Hours", fontsize=30),
    ax.set_xlabel("Concept \"" + concept + "\"", fontsize=30, alpha=0.0),
    ax.set_ylim(0, 9000)
    ax.set_yticks(range(0, 9000, 1000))
    ax.set_yticklabels(labels=range(0, 9000, 1000), rotation=0,
                       minor=False, fontsize=28)
    ax.set_xticklabels(labels=['2012', '2013', '2014'], rotation=0,
                       minor=False, fontsize=28)
    handles, labels = ax.get_legend_handles_labels()
    ax.legend(['Market A', 'Market B',
               'Market C', 'Market D'],
              loc='upper right', fontsize=28)
    ax_position += 1

# look "three subplots"
#plt.tight_layout(pad=0.0, w_pad=-8.0, h_pad=0.0)

# look "one plot"
plt.tight_layout(pad=0., w_pad=-16.5, h_pad=0.0)
axes[1].set_ylabel("")
axes[2].set_ylabel("")
axes[1].set_yticklabels("")
axes[2].set_yticklabels("")
axes[0].legend().set_visible(False)
axes[1].legend().set_visible(False)
axes[2].legend(['Market A', 'Market B',
                'Market C', 'Market D'],
               loc='upper right', fontsize=28)

La estructura del marco de datos del "subconjunto" antes de la agrupación se ve así:

<class 'pandas.core.frame.DataFrame'>
MultiIndex: 105216 entries, (D_REC, 2012-01-01 00:00:00) to (D_REC, 2014-12-31 23:45:00)
Data columns (total 4 columns):
cmp_tr_neg_p_wrk    105216 non-null float64
exp_tr_pos_p_wrk    105216 non-null float64
cmp_p_spot          105216 non-null float64
exp_p_spot          105216 non-null float64
dtypes: float64(4)
memory usage: 4.0+ MB

y la trama así:

ingrese la descripción de la imagen aquí

Tiene el formato "ggplot" con el siguiente encabezado:

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
matplotlib.style.use('ggplot')
Cord Kaldemeyer avatar Jan 28 '2016 11:01 Cord Kaldemeyer

La respuesta de @jrjc para el uso de seabornes muy inteligente, pero tiene algunos problemas, como señala el autor:

  1. El sombreado "claro" es demasiado pálido cuando sólo se necesitan dos o tres categorías. Hace que las series de colores (azul pálido, azul, azul oscuro, etc.) sean difíciles de distinguir.
  2. La leyenda no se produce para distinguir el significado de los matices ("pálido" ¿qué significa?)

Sin embargo, lo más importante es que descubrí que, debido a la groupbydeclaración en el código:

  1. Esta solución sólo funciona si las columnas están ordenadas alfabéticamente. Si cambio el nombre de las columnas ["I", "J", "K", "L", "M"]con algo antialfabético ( ["zI", "yJ", "xK", "wL", "vM"]), obtengo este gráfico :

La construcción de barras apiladas falla si las columnas no están en orden alfabético


Me esforcé por resolver estos problemas con la plot_grouped_stackedbars()función de este módulo de Python de código abierto .

  1. Mantiene el sombreado dentro de un rango razonable.
  2. Autogenera una leyenda que explica el sombreado.
  3. No depende degroupby

Gráfico de barras apiladas agrupadas correctamente con leyenda y rango de sombreado estrecho

También permite

  1. varias opciones de normalización (ver más abajo normalización al 100% del valor máximo)
  2. la adición de barras de error

Ejemplo con barras de normalización y error.

Vea la demostración completa aquí . Espero que esto resulte útil y pueda responder la pregunta original.

billjoie avatar Dec 17 '2018 19:12 billjoie