Cómo tener grupos de barras apiladas
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 :
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:
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
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:
Puedes cambiar los colores de la barra pasando un cmap
argumento:
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 variable
y 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
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.
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:
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
)
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í:
Tiene el formato "ggplot" con el siguiente encabezado:
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
matplotlib.style.use('ggplot')
La respuesta de @jrjc para el uso de seaborn
es muy inteligente, pero tiene algunos problemas, como señala el autor:
- 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.
- 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 groupby
declaración en el código:
- 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 :
Me esforcé por resolver estos problemas con la plot_grouped_stackedbars()
función de este módulo de Python de código abierto .
- Mantiene el sombreado dentro de un rango razonable.
- Autogenera una leyenda que explica el sombreado.
- No depende de
groupby
También permite
- varias opciones de normalización (ver más abajo normalización al 100% del valor máximo)
- la adición de barras de error
Vea la demostración completa aquí . Espero que esto resulte útil y pueda responder la pregunta original.