web-dev-qa-db-fra.com

Comment avoir des grappes de barres empilées avec Python (Pandas)

Voici comment se présente mon ensemble de données:

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

Je veux avoir un tracé en barres empilées pour chaque image, mais comme elles ont le même index, j'aimerais disposer de 2 barres empilées par index.

J'ai essayé de tracer les deux sur les mêmes axes:

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

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

Mais cela se chevauche.

J'ai ensuite essayé de concaténer les deux ensembles de données

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

mais ici tout est empilé

Mon meilleur essai est:

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

Qui donne :

enter image description here

C’est fondamentalement ce que je veux, sauf que je veux que le bar soit commandé comme

(df1, a) (df2, a) (df1, b) (df2, b) etc ...

Je suppose qu'il y a un truc mais je ne le trouve pas!


Après la réponse de @ bgschiller, j'ai eu ceci:

enter image description here

Ce qui est presque ce que je veux. Je voudrais que la barre soit regroupée par index , afin que quelque chose soit clair visuellement.

Bonus: le label x n'est pas redondant, par exemple: 

df1 df2    df1 df2
_______    _______ ...
   A          B

Merci pour ton aide.

39
jrjc

J'ai donc finalement trouvé une astuce (edit: voir ci-dessous l'utilisation de dataframe seaborn et longform):

Solution avec des pandas et matplotlib

La voici avec un exemple plus complet:

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"])

Et ça donne ça:

multiple stacked bar plot

Vous pouvez changer les couleurs de la barre en passant un argument cmap

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

Solution avec seaborn:

Étant donné les mêmes df1, df2, df3, ci-dessous, je les convertis sous une forme longue:

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)

Le problème avec seaborn est qu’il n’empile pas les barres de manière native, il s’agit donc de tracer la somme cumulée de chaque barre les unes sur les autres:

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

Puis passez en boucle sur chaque groupe de variable et tracez la somme cumulée:

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 

 multiple stack bar plot seaborn

Il manque la légende qui peut être ajoutée facilement je pense. Le problème est qu'au lieu de hachures (qui peuvent être facilement ajoutées) pour différencier les images, nous avons un léger dégradé, et c'est un peu trop clair pour la première, et je ne sais pas vraiment comment changer cela sans changer chaque rectangle un par un (comme dans la première solution).

Dites-moi si vous ne comprenez pas quelque chose dans le code.

N'hésitez pas à réutiliser ce code qui est sous CC0.

53
jrjc

J'ai réussi à faire de même en utilisant des pandas et des intrigues secondaires Matplotlib avec des commandes de base.

Voici un exemple:

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 structure de données du "sous-ensemble" avant le regroupement ressemble à ceci:

<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

et l'intrigue comme ceci:

 enter image description here

Il est formaté dans le style "ggplot" avec l'en-tête suivant:

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
matplotlib.style.use('ggplot')
4
Cord Kaldemeyer

C'est un bon début, mais je pense que les couleurs pourraient être légèrement modifiées pour plus de clarté. Veillez également à importer chaque argument dans Altair, car cela pourrait entraîner des conflits avec des objets existants dans votre espace de noms. Voici un code reconfiguré pour afficher la couleur correcte lors de l’empilement des valeurs:

 Altair Clustered Column Chart

Importer des paquets

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

Générer des données aléatoires

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])

Tracer les données avec Altair

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

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

    # 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='')),

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

    # 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_facet_cell(
    # remove grid lines around column clusters
        strokeWidth=0.0)
2
Grant Langseth

La réponse de @jrjc pour l'utilisation de seaborn est très intelligente, mais elle pose quelques problèmes, comme l'a noté l'auteur:

  1. La nuance "claire" est trop pâle lorsque seulement deux ou trois catégories sont nécessaires. Cela rend les séries de couleurs (bleu pâle, bleu, bleu foncé, etc.) difficiles à distinguer.
  2. La légende n'est pas produite pour distinguer le sens des nuances ("pâle" veut dire quoi?)

Plus important encore, cependant, j'ai découvert que, à cause de la groupby déclaration dans le code:

  1. Cette solution fonctionne uniquement si les colonnes sont classées par ordre alphabétique. Si je renomme les colonnes ["I", "J", "K", "L", "M"] par quelque chose d'anti-alphabétique (["zI", "yJ", "xK", "wL", "vM"]), je reçois ce graphique à la place :

 Stacked bar construction fails if columns are not in alphabetical order


Je me suis efforcé de résoudre ces problèmes avec la fonction plot_grouped_stackedbars() dans ce module python à code source ouvert .

  1. Il garde l'ombrage dans une plage raisonnable
  2. Il génère automatiquement une légende qui explique les ombres
  3. Il ne repose pas sur groupby

 Proper grouped stacked-bars graph with legend and narrow shading range

Cela permet aussi de

  1. diverses options de normalisation (voir ci-dessous la normalisation à 100% de la valeur maximale)
  2. l'ajout de barres d'erreur

 Example with normalization and error bars

Voir démo complète ici . J'espère que cela s'avérera utile et pourra répondre à la question initiale.

1
billjoie

Vous êtes sur la bonne voie! Afin de changer l'ordre des barres, vous devez changer l'ordre dans l'index. 

In [5]: df_both = pd.concat(dict(df1 = df1, df2 = df2),axis = 0)

In [6]: df_both
Out[6]:
              I         J
df1 A  0.423816  0.094405
    B  0.825094  0.759266
    C  0.654216  0.250606
    D  0.676110  0.495251
df2 A  0.607304  0.336233
    B  0.581771  0.436421
    C  0.233125  0.360291
    D  0.519266  0.199637

[8 rows x 2 columns]

Nous voulons donc échanger les axes, puis réorganiser. Voici un moyen facile de faire cela

In [7]: df_both.swaplevel(0,1)
Out[7]:
              I         J
A df1  0.423816  0.094405
B df1  0.825094  0.759266
C df1  0.654216  0.250606
D df1  0.676110  0.495251
A df2  0.607304  0.336233
B df2  0.581771  0.436421
C df2  0.233125  0.360291
D df2  0.519266  0.199637

[8 rows x 2 columns]

In [8]: df_both.swaplevel(0,1).sort_index()
Out[8]:
              I         J
A df1  0.423816  0.094405
  df2  0.607304  0.336233
B df1  0.825094  0.759266
  df2  0.581771  0.436421
C df1  0.654216  0.250606
  df2  0.233125  0.360291
D df1  0.676110  0.495251
  df2  0.519266  0.199637

[8 rows x 2 columns]

S'il est important que vos étiquettes horizontales apparaissent dans l'ordre ancien (df1, a) plutôt que (a, df1), nous pouvons simplement swaplevels encore et non sort_index:

In [9]: df_both.swaplevel(0,1).sort_index().swaplevel(0,1)
Out[9]:
              I         J
df1 A  0.423816  0.094405
df2 A  0.607304  0.336233
df1 B  0.825094  0.759266
df2 B  0.581771  0.436421
df1 C  0.654216  0.250606
df2 C  0.233125  0.360291
df1 D  0.676110  0.495251
df2 D  0.519266  0.199637

[8 rows x 2 columns]
1
bgschiller

Altair peut être utile ici. Voici l'intrigue produite.

 enter image description here

Importations

import pandas as pd
import numpy as np
from altair import *

Création de jeu de données

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

Préparation du jeu de données

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')

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

Parcelle Altair

Chart(df).mark_bar().encode(y=Y('values', axis=Axis(grid=False)),
                            x='c2:N', 
                            column=Column('c1:N') ,
                            color='DF:N').configure_facet_cell( strokeWidth=0.0).configure_cell(width=200, height=200)
0
Nipun Batra