it-swarm.com.de

Wie man Cluster von gestapelten Bars mit Python (Pandas) hat

So sieht mein Datensatz aus:

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

Ich möchte ein Balkendiagramm für jeden Datenrahmen gestapelt haben. Da sie jedoch denselben Index haben, hätte ich gerne zwei Stapelbars pro Index.

Ich habe versucht, beide auf den gleichen Achsen zu zeichnen:

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

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

Aber es überschneidet sich.

Dann habe ich zuerst versucht, die beiden Datensätze zusammenzutragen:

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

aber hier ist alles gestapelt

Mein bester Versuch ist:

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

Was gibt:

enter image description here

Dies ist im Grunde das, was ich will, außer dass ich möchte, dass die Bar als bestellt wird

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

Ich denke es gibt einen Trick, aber ich kann ihn nicht finden!


Nach @ bgschillers Antwort bekam ich folgendes:

enter image description here

Welches ist fast was ich will. Ich möchte, dass der Balken gruppiert nach Index ist, um etwas visuelles klares zu haben.

Bonus: Da das x-Label nicht überflüssig ist, etwa: 

df1 df2    df1 df2
_______    _______ ...
   A          B

Danke fürs Helfen.

39
jrjc

So fand ich schließlich einen Trick (edit: siehe unten, um Seaborn und Longform Dataframe zu verwenden):

Lösung mit Pandas und Matplotlib

Hier ist es mit einem vollständigeren Beispiel:

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

Und es gibt das:

multiple stacked bar plot

Sie können die Farben der Leiste ändern, indem Sie ein cmap-Argument übergeben: 

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

Lösung mit Seaborn:

Wenn ich dasselbe df1, df2, df3 unten nehme, konvertiere ich sie in einer langen Form:

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)

Das Problem bei Seaborn ist, dass die Balken nicht nativ gestapelt werden. Der Trick besteht also darin, die kumulierte Summe jedes Balkens übereinander zu zeichnen:

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

Führen Sie dann eine Schleife über jede Gruppe von variable durch und zeichnen Sie die kumulierte Summe auf:

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

Ich glaube, es fehlt die Legende, die leicht hinzugefügt werden kann. Das Problem ist, dass anstelle von Schraffuren (die leicht hinzugefügt werden können), um die Datenrahmen zu unterscheiden, wir einen Helligkeitsgradienten haben, und es ist für den ersten etwas zu hell, und ich weiß nicht, wie ich das ändern kann, ohne jeden zu ändern Rechteck eins nach dem anderen (wie in der ersten Lösung).

Sagen Sie mir, wenn Sie etwas im Code nicht verstehen.

Fühlen Sie sich frei, diesen Code unter CC0 wiederzuverwenden.

53
jrjc

Ich habe es geschafft, dasselbe mit Pandas und Matplotlib-Subplots mit grundlegenden Befehlen zu tun.

Hier ist ein Beispiel:

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)

Die Dataframe-Struktur von "Subset" vor der Gruppierung sieht folgendermaßen aus:

<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

und die Handlung wie folgt:

 enter image description here

Es ist im "ggplot" -Stil mit der folgenden Kopfzeile formatiert:

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

Dies ist ein großartiger Anfang, aber ich denke, die Farben könnten aus Gründen der Klarheit etwas geändert werden. Achten Sie auch darauf, dass Sie jedes Argument in Altair importieren, da dies zu Kollisionen mit vorhandenen Objekten in Ihrem Namespace führen kann. Hier einige rekonfigurierte Codes, um beim Stapeln der Werte die korrekte Farbanzeige anzuzeigen:

 Altair Clustered Column Chart

Pakete importieren

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

Erzeugen Sie einige zufällige Daten

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

Zeichnen Sie Daten mit Altair auf

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

Die Antwort von @jrjc für die Verwendung von seaborn ist sehr klug, hat jedoch einige Probleme, wie vom Autor festgestellt:

  1. Die "leichte" Schattierung ist zu blass, wenn nur zwei oder drei Kategorien benötigt werden. Farbreihen (hellblau, blau, dunkelblau usw.) lassen sich nur schwer unterscheiden.
  2. Die Legende wird nicht erstellt, um die Bedeutung der Schattierungen zu unterscheiden ("blass" bedeutet was?)

Wichtiger, ich fand jedoch heraus, dass aufgrund der groupby-Anweisung im Code:

  1. Diese Lösung funktioniert nur, wenn die Spalten alphabetisch angeordnet sind. Wenn ich Spalten ["I", "J", "K", "L", "M"] durch etwas Anti-Alphabetisches (["zI", "yJ", "xK", "wL", "vM"]) umbenenne, ich bekomme stattdessen diese Grafik :

Stacked bar construction fails if columns are not in alphabetical order


Ich bemühte mich, diese Probleme mit der Funktion plot_grouped_stackedbars() in diesem Open-Source-Python-Modul zu lösen.

  1. Es hält die Schattierung in einem vernünftigen Bereich
  2. Es generiert automatisch eine Legende, die die Schattierung erklärt
  3. Es ist nicht abhängig von groupby

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

Es erlaubt auch

  1. verschiedene Normalisierungsoptionen (siehe unten Normalisierung auf 100% des Maximalwertes)
  2. das Hinzufügen von Fehlerbalken

Example with normalization and error bars

Siehe vollständige Demo hier . Ich hoffe, dass sich dies als nützlich erweist und die ursprüngliche Frage beantworten kann.

1
billjoie

Du bist auf dem richtigen Weg! Um die Reihenfolge der Balken zu ändern, sollten Sie die Reihenfolge im Index ändern. 

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]

Also wollen wir die Achsen tauschen und neu ordnen. Hier ist ein einfacher Weg, dies zu tun

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]

Wenn es wichtig ist, dass Ihre horizontalen Beschriftungen in der alten Reihenfolge (df1, A) anstatt (A, df1) angezeigt werden, können wir einfach wieder swaplevels und nicht 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

Ich mochte die Lösung von Cord Kaldemeyer, aber sie ist überhaupt nicht robust (und enthält einige nutzlose Zeilen). Hier ist eine modifizierte Version. Die Idee ist, so viel Breite wie nötig für die Grundstücke zu reservieren. Dann erhält jeder Cluster eine Unterzeichnung mit der erforderlichen Länge.

# Data and imports

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MaxNLocator
import matplotlib.gridspec as gridspec
import matplotlib

matplotlib.style.use('ggplot')

np.random.seed(0)

df = pd.DataFrame(np.asarray(1+5*np.random.random((10,4)), dtype=int),columns=["Cluster", "Bar", "Bar_part", "Count"])
df = df.groupby(["Cluster", "Bar", "Bar_part"])["Count"].sum().unstack(fill_value=0)
display(df)

# plotting

clusters = df.index.levels[0]
inter_graph = 0
maxi = np.max(np.sum(df, axis=1))
total_width = len(df)+inter_graph*(len(clusters)-1)

fig = plt.figure(figsize=(total_width,10))
gridspec.GridSpec(1, total_width)
axes=[]

ax_position = 0
for cluster in clusters:
    subset = df.loc[cluster]
    ax = subset.plot(kind="bar", stacked=True, width=0.8, ax=plt.subplot2grid((1,total_width), (0,ax_position), colspan=len(subset.index)))
    axes.append(ax)
    ax.set_title(cluster)
    ax.set_xlabel("")
    ax.set_ylim(0,maxi+1)
    ax.yaxis.set_major_locator(MaxNLocator(integer=True))
    ax_position += len(subset.index)+inter_graph

for i in range(1,len(clusters)):
    axes[i].set_yticklabels("")
    axes[i-1].legend().set_visible(False)
axes[0].set_ylabel("y_label")

fig.suptitle('Big Title', fontsize="x-large")
legend = axes[-1].legend(loc='upper right', fontsize=16, framealpha=1).get_frame()
legend.set_linewidth(3)
legend.set_edgecolor("black")

plt.show()

Das Ergebnis ist das Folgende:

(kann noch kein Bild direkt auf der Seite posten)

0
Simoons

Altair kann hier hilfreich sein. Hier ist die produzierte Handlung.

 enter image description here

Importe

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

Erstellung von Datensätzen

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

Datensatz vorbereiten

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

Altair-Plot

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