2

I have the following dataframe

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib 
print('matplotlib: {}'.format(matplotlib.__version__))
# 3.5.3

df=pd.DataFrame({'Type': [ 'Sentence', 'Array', 'String', '-','-', 'Sentence', 'Array', 'String', '-','-', 'Sentence'],
                 'Length': [42,21,11,6,6,42,21,11,6,6,42],
                 'label': [1,1,0,0,0,1,1,0,0,0,1],
                 })
print(df)
#       Type     Length  label
#0   Sentence      42      1
#1      Array      21      1
#2     String      11      0
#3          -       6      0
#4          -       6      0
#5   Sentence      42      1
#6      Array      21      1
#7     String      11      0
#8          -       6      0
#9          -       6      0
#10  Sentence      42      1

I want to plot stacked bar chart for the arbitrary column within dataframe (either numerical e.g. Length column or categorical e.g. Type column) and stack with respect to label column using annotations of both count/percentage, where small values of rare observations are also displayed. The following script gives me the wrong results:

ax = df.plot.bar(stacked=True)

#ax = df[["Type","label"]].plot.bar(stacked=True)
#ax = df.groupby('Type').size().plot(kind='bar', stacked=True)

ax.legend(["0: normanl", "1: Anomaly"])
for p in ax.patches:
    width, height = p.get_width(), p.get_height()
    x, y = p.get_xy() 
    ax.text(x+width/2, 
            y+height/2, 
            '{:.0f} %'.format(height), 
            horizontalalignment='center', 
            verticalalignment='center')

I can imagine that somehow I need to calculate the counts of the selected column with respect to label column:

## counts will be used for the labels
counts = df.apply(lambda x: x.value_counts())

## percents will be used to determine the height of each bar
percents = counts.div(counts.sum(axis=1), axis=0)

I tried to solve the problem by using df.groupby(['selcted column', 'label'] unsuccessfully. I collected all possible solutions in this Google Colab Notebook nevertheless I couldn't find a straightforward way to adapt into dataframe.

So far I have tried following solution inspired by this post to solve the problem by using df.groupby(['selcted column', 'label'] unsuccessfully and I got TypeError: unsupported operand type(s) for +: 'int' and 'str' for total = sum(dff.sum()) can't figure out what is the problem? in indexing or df transformation.

BTW I collected all possible solutions in this Google Colab Notebook nevertheless I couldn't find a straightforward way to adapt into dataframe via Mathplotlib. So I'm looking for an elegant way of using Seaborn or plotly.

df = df.groupby(["Type","label"]).count()
#dfp_Type   = df.pivot_table(index='Type', columns='label', values= 'Length', aggfunc='mean') 
dfp_Type   = df.pivot_table(index='Type', columns='label', values= df.Type.size(), aggfunc='mean') 
#dfp_Length = df.pivot_table(index='Length', columns='label', values= df.Length.size(), aggfunc='mean') 

ax = dfp_Type.plot(kind='bar', stacked=True, rot=0) 

# iterate through each bar container
for c in ax.containers: labels = [v.get_height() if v.get_height() > 0 else '' for v in c]

# add the annotations
ax.bar_label(c, fmt='%0.0f%%', label_type='center')

# move the legend
ax.legend(title='Class', bbox_to_anchor=(1, 1.02), loc='upper left')

plt.show()

output:

img

Expected output:

img

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Mario
  • 1,631
  • 2
  • 21
  • 51

2 Answers2

2
import pandas as pd

# sample dataframe
df = pd.DataFrame({'Type': [ 'Sentence', 'Array', 'String', '-','-', 'Sentence', 'Array', 'String', '-','-', 'Sentence'],
                   'Length': [42, 21, 11, 6, 6, 42, 21, 11, 6, 6, 42],
                   'label': [1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0]})

# pivot the dataframe and get len
dfp = df.pivot_table(index='Type', columns='label', values='Length', aggfunc=len) 

# get the total for each row
total = dfp.sum(axis=1)

# calculate the percent for each row
per = dfp.div(total, axis=0).mul(100).round(2)

# plot the pivoted dataframe
ax = dfp.plot(kind='bar', stacked=True, figsize=(10, 8), rot=0)

# set the colors for each Class
segment_colors = {'0': 'white', '1': 'black'}

# iterate through the containers
for c in ax.containers:
    
    # get the current segment label (a string); corresponds to column / legend
    label = c.get_label()
    
    # create custom labels with the bar height and the percent from the per column
    # the column labels in per and dfp are int, so convert label to int
    labels = [f'{v.get_height()}\n({row}%)' if v.get_height() > 0 else '' for v, row in zip(c, per[int(label)])]
    
    # add the annotation
    ax.bar_label(c, labels=labels, label_type='center', fontweight='bold', color=segment_colors[label])
    
# move the legend
_ = ax.legend(title='Class', bbox_to_anchor=(1, 1.01), loc='upper left')

enter image description here


Comment Updates

  • How to always have a spot for 'Array' if it's not in the data:
    • Add 'Array' to dfp if it's not in dfp.index.
    • df.Type = pd.Categorical(df.Type, ['-', 'Array', 'Sentence', 'String'], ordered=True) does not ensure the missing categories are plotted.
  • How to have all the annotations, even if they're small:
    • Don't stack the bars, and set logy=True.
  • This uses the full-data, which was provided in a link.
# pivot the dataframe and get len
dfp = df.pivot_table(index='Type', columns='label', values='Length', aggfunc=len) 

# append Array if it's not included
if 'Array' not in dfp.index:
    dfp = pd.concat([dfp, pd.DataFrame({0: [np.nan], 1: [np.nan]}, index=['Array'])])
    
# order the index
dfp = dfp.loc[['-', 'Array', 'Sentence', 'String'], :]

# calculate the percent for each row
per = dfp.div(dfp.sum(axis=1), axis=0).mul(100).round(2)

# plot the pivoted dataframe
ax = dfp.plot(kind='bar', stacked=False, figsize=(10, 8), rot=0, logy=True, width=0.75)

# iterate through the containers
for c in ax.containers:
    
    # get the current segment label (a string); corresponds to column / legend
    label = c.get_label()
    
    # create custom labels with the bar height and the percent from the per column
    # the column labels in per and dfp are int, so convert label to int
    labels = [f'{v.get_height()}\n({row}%)' if v.get_height() > 0 else '' for v, row in zip(c, per[int(label)])]
    
    # add the annotation
    ax.bar_label(c, labels=labels, label_type='edge', fontsize=10, fontweight='bold')
    
# move the legend
ax.legend(title='Class', bbox_to_anchor=(1, 1.01), loc='upper left')

# pad the spacing between the number and the edge of the figure
_ = ax.margins(y=0.1)

enter image description here


DataFrame Views

  • Based on the sample data in the OP

df

        Type  Length  label
0   Sentence      42      1
1      Array      21      1
2     String      11      0
3          -       6      0
4          -       6      0
5   Sentence      42      1
6      Array      21      1
7     String      11      0
8          -       6      0
9          -       6      1
10  Sentence      42      0

dfp

label       0    1
Type              
-         3.0  1.0
Array     NaN  2.0
Sentence  1.0  2.0
String    2.0  NaN

total

Type
-           4.0
Array       2.0
Sentence    3.0
String      2.0
dtype: float64

per

label          0       1
Type                    
-          75.00   25.00
Array        NaN  100.00
Sentence   33.33   66.67
String    100.00     NaN
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
1

I slightly adjusted the data so the graph would look identical to yours(e.g., Type:-'s label has three 0 and one 1)

df
###
        Type  Length  label
0   Sentence      42      1
1      Array      21      1
2     String      11      0
3          -       6      0
4          -       6      0
5   Sentence      42      1
6      Array      21      1
7     String      11      0
8          -       6      0
9          -       6      1
10  Sentence      42      0



df_plot = df.groupby(['Type','label']).size().reset_index()
df_plot.columns = ['Type', 'Class', 'count']
df_plot = df_plot.astype({'Class':'str'})
df_plot['percentage'] = df.groupby(['Type','label']).size().groupby(level=0).apply(lambda x: 100*x/float(x.sum())).values.round(2).astype(str)
df_plot['percentage'] = "(" + df_plot['percentage'] + '%)'

df_plot
###
       Type Class  count percentage
0         -     0      3    (75.0%)
1         -     1      1    (25.0%)
2     Array     1      2   (100.0%)
3  Sentence     0      1   (33.33%)
4  Sentence     1      2   (66.67%)
5    String     0      2   (100.0%)
fig = px.bar(df_plot,
             x='Type',
             y='count',
             color='Class',
             text=df_plot['count'].astype(str) + "<br>" + df_plot['percentage'],
             width=550,
             height=400,
             category_orders={'Type':['-','Array','Sentence','String']},
             template='plotly_white',
             log_y=True
             )
fig.show('browser')

enter image description here





with your CSV file followed the same ELT turning into df_plot2, while Class 0 and 1 has a huge difference, A stacked bar chart(default setting) won't give you distinguishable outcome, we can use barmode='group' instead,

fig2 = px.bar(df_plot2,
              barmode='group',
              x='Type',
              y='count',
              color='Class',
              color_discrete_map={'0':'#5DA597', '1':'#FFC851'},
              text=df_plot2['count'].astype(str) + "<br>" + df_plot2['percentage'],
              width=850,
              height=800,
              category_orders={'Type': ['-', 'Array', 'Sentence', 'String']},
              template='plotly_white',
              log_y=True,
              )
fig2.update_yaxes(dtick=1)
fig2.show('browser')

enter image description here

Baron Legendre
  • 2,053
  • 3
  • 5
  • 22