1

Given the following dataframe and I would like to add an error between the expected and the actual that shows the rate_off

  Month  Expected  Actual  rate_off
0   Jan        30      40     25.00
1   Feb        25      23     -8.70
2   Mar        50      51      1.96
3   Apr        20      17    -17.65

so in my following code I get this graph:

import matplotlib.pyplot as plt
import numpy as np 
import pandas as pd

df = pd.DataFrame({"Month":['Jan','Feb','Mar','Apr'], 'Expected':[30, 25, 50, 20],'Actual':[40, 23, 51, 17]})     
#df['rate_off'] = np.round(100-np.abs(df['Expected']/df['Actual']*100),2) 
 
fig, ax = plt.subplots(1,1)
df.plot(kind='bar', ax=ax)
ax.legend()
plt.show()

enter image description here

What I wish to add is some annotation that describe the difference (rate_off) to help the end user to get the data instantly.

So a desired result would be:

enter image description here

1 Answer 1

3

You can use matplotlib annotations to draw each of the arrows as well as add corresponding text. Your problem is less straightforward because the annotations are conditional, depending on whether expected < actual or expected > actual.

To access the heights and widths of the bars so that you can tell matplotlib where to place the annotations, you can create an object to store the pandas barplot using plots = df.plot(kind='bar', ax=ax) and iterate over plots.patches, as described in this article.

However, since you have a grouped bar plot with left and right bars, iterating over the list plot.patches will return the left bar objects, followed by the right bar objects, so it's best that we iterate over the first half of the plot.patches list and the last half of the plot.patches list simultaneously, choosing whether to annotate the left or right bar, which I have done by applying zip() to the first half of plot.patches and the second half of plot.patches.

The rest is determining the starting and ending x-coordinates and y-coordinates of each arrow annotation depending on whether expected < actual or expected > actual, as well as the color of your arrow, and the location of the text relative to the bars to get as close as possible to your desired plot.

import matplotlib.pyplot as plt
import numpy as np 
import pandas as pd

df = pd.DataFrame({"Month":['Jan','Feb','Mar','Apr'], 'Expected':[30, 25, 50, 20],'Actual':[40, 23, 51, 17]})     
rate_off = np.round(100-np.abs(df['Expected']/df['Actual']*100),2) 
 
fig, ax = plt.subplots(1,1)

## store the plot in an object
plots = df.plot(kind='bar', ax=ax)


## iterating over plots.patches will return the objects for left bars, then the objects for the right bars
## so it is best to iterate over the left and right bar objects simultaneously 
middle_index = len(plots.patches)//2
for change, left_bar, right_bar in zip(rate_off, plots.patches[:middle_index], plots.patches[middle_index:]):
    
    ## for expected less than actual, the annotation starts from the left bar
    if change > 0:
        # print(change, left_bar.get_x())
        plots.annotate(f"", 
            xy=(left_bar.get_x() + left_bar.get_width() / 2, right_bar.get_height()), 
            ha='center', va='center', size=10, 
            xytext=(left_bar.get_x() + left_bar.get_width() / 2, left_bar.get_height()),
            arrowprops=dict(arrowstyle="simple", color='green', facecolor='green'))
        ## annotate the text to the left of the bar
        plots.annotate(f"{change}%", 
            xy=(left_bar.get_x() + left_bar.get_width() / 2, (left_bar.get_height() + right_bar.get_height()) / 2),
            size=10, color='red', ha='right', va='center')

    ## for expected greater than actual, the annotation starts from the right bar
    else:
        # print(change, right_bar.get_x())
        plots.annotate(f"", 
            xy=(right_bar.get_x() + right_bar.get_width() / 2, right_bar.get_height()), 
            ha='center', va='center', size=10, 
            xytext=(right_bar.get_x() + right_bar.get_width() / 2, left_bar.get_height()),
            arrowprops=dict(arrowstyle="simple", color='red', facecolor='red'))
        ## annotate the text to the right of the bar
        plots.annotate(f"{change}%", 
            xy=(right_bar.get_x() + right_bar.get_width() / 2, (left_bar.get_height() + right_bar.get_height()) / 2),
            size=10, color='red', ha='left', va='center')
plt.show()

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

great idea with zip(rate_off, plots.patches[:middle_index], plots.patches[middle_index:])
Glad to hear my answer was helpful!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.