colors are not consistently applied to categories in subplots

Viewed 112

Having the following code i can't understand why is not showing correctly the information (check the column C and F) bot are show as the same value but are different

What i need is plot some of the columns in the df and share the legend between all subplots (all columns have the same values ["SI","NO"] (this is just a sample code )

colnames=['a','b','c','d','e','f','g']
values=[
    ['SI','SI','NO','SI','SI','SI','NO'],
    ['SI','NO','NO','SI','NO','SI','NO'],
    ['SI','SI','NO','SI','SI','SI','NO'],
    ['SI','NO','NO','NO','NO','SI','SI'],
    ['SI','NO','NO','NO','NO','SI','NO']]

df=pd.DataFrame(values, columns=colnames)

def pieplotstest(df):
    fig,ax =plt.subplots(2,3,facecolor=(1, 1, 1),figsize=(7.2,4.3))
    plt.style.use('fivethirtyeight')

    colors=["#172a3d","#e33e31"]
    textprops=dict(color="w",weight='bold',size=5)
    labels=['NO','SI']

    ax[0,0].pie(df['b'].value_counts(),
            colors=colors,
            autopct = '%1.1f%%',
            textprops=textprops,
            wedgeprops=dict(width=0.5),
            pctdistance=0.7
        )
    ax[0,1].pie(df['c'].value_counts(),
            colors=colors,
            autopct = '%1.1f%%',
            textprops=textprops,
            wedgeprops=dict(width=0.5),
            pctdistance=0.75

        )
    ax[0,2].pie(df['d'].value_counts(),
            colors=colors,
            autopct = '%1.1f%%',
            textprops=textprops,
            wedgeprops=dict(width=0.5),
            pctdistance=0.75
        )
    ax[1,0].pie(df['e'].value_counts(),
            colors=colors,
            autopct = '%1.1f%%',
            textprops=textprops,
            wedgeprops=dict(width=0.5),
            pctdistance=0.75
        )
    ax[1,1].pie(df['f'].value_counts(),
            colors=colors,
            autopct = '%1.1f%%',
            textprops=textprops,
            wedgeprops=dict(width=0.5),
            pctdistance=0.75
        )
    ax[1,2].pie(df['g'].value_counts(),
            colors=colors,
            autopct = '%1.1f%%',
            textprops=textprops,
            wedgeprops=dict(width=0.5),
            pctdistance=0.75
        )

    ax[0,0].set_title('b',fontsize=10)
    ax[0,1].set_title('c',fontsize=10)
    ax[0,2].set_title('d',fontsize=10)
    ax[1,0].set_title('e',fontsize=10)
    ax[1,1].set_title('f',fontsize=10)
    ax[1,2].set_title('g',fontsize=10)
    fig.legend(labels,
            loc=4,                
            fontsize=7
            )
    plt.suptitle('Como estan distribuidas tus ventas')
    fig.tight_layout()
    plt.savefig(f'orders3.png',dpi=600,transparent=True)

the result is : enter image description here

2 Answers

So the commenter was correct, when you have only one value with value_counts() you run into issues.

So I transformed the DF with:

df = df.T.apply(pd.Series.value_counts, axis=1).fillna(0).reset_index()
df.columns = ('question', 'no', 'si')

Created a list of subplot indices we need to make the code cleaner:

subplot_list = []
for i in range(2):
    for j in range(3):
        subplot_list.append([i,j])

and added a loop to go through each question (question being 'a', 'b', etc.):

    for index, row in new_df.iterrows():
        if row['question'] == 'a':
            pass
        else:
            current_sub = subplot_list[index-1]
            row_num = current_sub[0]
            column = current_sub[1]

            ax[row_num,column].pie(
                [row['no'], row['si']],
                colors=colors,
                autopct = '%1.1f%%',
                textprops=textprops,
                wedgeprops=dict(width=0.5),
                pctdistance=0.7
            )

            ax[row_num,column].set_title(row['question'],fontsize=10)

And the full code with output is below:

import pandas as pd
import matplotlib.pyplot as plt


colnames=['a','b','c','d','e','f','g']
values=[
    ['SI','SI','NO','SI','SI','SI','NO'],
    ['SI','NO','NO','SI','NO','SI','NO'],
    ['SI','SI','NO','SI','SI','SI','NO'],
    ['SI','NO','NO','NO','NO','SI','SI'],
    ['SI','NO','NO','NO','NO','SI','NO']]

df=pd.DataFrame(values,columns=colnames)

df = df.T.apply(pd.Series.value_counts, axis=1).fillna(0).reset_index()
df.columns = ('question', 'no', 'si')

subplot_list = []
for i in range(2):
    for j in range(3):
        subplot_list.append([i,j])

def pieplotstest(df):
    fig,ax =plt.subplots(2,3,facecolor=(1, 1, 1),figsize=(7.2,4.3))
    plt.style.use('fivethirtyeight')

    colors=["#172a3d","#e33e31"]
    textprops=dict(color="w",weight='bold',size=5)
    labels=['NO','SI']


    for index, row in new_df.iterrows():
        if row['question'] == 'a':
            pass
        else:
            current_sub = subplot_list[index-1]
            row_num = current_sub[0]
            column = current_sub[1]

            ax[row_num,column].pie(
                [row['no'], row['si']],
                colors=colors,
                autopct = '%1.1f%%',
                textprops=textprops,
                wedgeprops=dict(width=0.5),
                pctdistance=0.7
            )

            ax[row_num,column].set_title(row['question'],fontsize=10)

    fig.legend(labels,
            loc=4,                
            fontsize=7
            )
    plt.suptitle('Como estan distribuidas tus ventas')
    plt.savefig(f'orders3.png',dpi=600,transparent=True)

Pie chart in a loop

  • If you plot the dataframe directly with pandas.DataFrame.plot.pie, then the colors will be properly managed.
    • Select specific columns to plot: dfc[['b', 'c', 'd', 'e', 'f', 'g']].plot.pie(...)
  • Use .melt to convert the dataframe from wide to long
  • Use pd.crosstab to get the value counts for each group
    • dfc shows all values, even if 0, unlike using .value_counts(). This will ensure the colors are alway applied correctly for each group.
  • Use autopct='%1.1f%%' or autopct = lambda v: f'{v:.1f}%' if v > 0 else None to format the wedge label. The second option will not add a label unless the value is >0
# display(dfc)
variable  a  b  c  d  e  f  g
value                        
NO        0  3  5  2  3  0  4
SI        5  2  0  3  2  5  1
  • This considerably reduces your code base by removing the redundancy
  • Tested in pandas 1.3.2 and matplotlib 3.4.2
import pandas as pd
from matplotlib.patches import Patch

# used df from the OP

def pieplotstest(df):
    colors=["#172a3d", "#e33e31"]
    textprops=dict(color="w", weight='bold', size=9)
    labels=['NO','SI']

    # convert the dataframe from wide to long
    dfm = df.melt()

    # get the counts for each group
    dfc = pd.crosstab(dfm.value, dfm.variable)

    # format the percent value to be None if the value is not greater than 0
    autopct = lambda v: f'{v:.1f}%' if v > 0 else None

    # plot the dataframe directly
    axes = dfc.plot.pie(subplots=True, layout=(2, 4), colors=colors,
                        autopct=autopct, figsize=(10, 6), textprops=textprops,
                        wedgeprops=dict(width=0.5), pctdistance=0.7, legend=False)

    # flatten the array of axes
    axes = axes.ravel()

    # extract the figure object
    fig = axes[0].get_figure()

    # rotate the label
    for ax in axes:
        yl = ax.get_ylabel()
        ax.set_ylabel(yl, rotation=0, fontsize=12)

    # create the legend
    legend_elements = [Patch(fc=c, label=l) for c, l in zip(colors, labels)]
    fig.legend(handles=legend_elements, loc=4, fontsize=12, bbox_to_anchor=(.95, .2))

    fig.tight_layout()
    fig.suptitle('Como estan distribuidas tus ventas')
    fig.savefig(f'orders3.png',dpi=600,transparent=True)


pieplotstest(df)

enter image description here

Related