27

I'm trying to figure out how I can automatically annotate the maximum value in a figure window. I know you can do this by manually entering in x,y coordinates to annotate whatever point you want using the .annotate() method, but I want the annotation to be automatic, or to find the maximum point by itself.

Here's my code so far:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pandas import Series, DataFrame

df = pd.read_csv('macrodata.csv') #Read csv file into dataframe
years = df['year'] #Get years column
infl = df['infl'] #Get inflation rate column

fig10 = plt.figure()
win = fig10.add_subplot(1,1,1)
fig10 = plt.plot(years, infl, lw = 2)

fig10 = plt.xlabel("Years")
fig10 = plt.ylabel("Inflation")
fig10 = plt.title("Inflation with Annotations")

Here's the figure that it generates

5 Answers 5

51

If x and y are the arrays to plot, you get the coordinates of the maximum via

xmax = x[numpy.argmax(y)]
ymax = y.max()

This can be incorporated into a function that you may simply call with your data.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-2,8, num=301)
y = np.sinc((x-2.21)*3)


fig, ax = plt.subplots()
ax.plot(x,y)

def annot_max(x,y, ax=None):
    xmax = x[np.argmax(y)]
    ymax = y.max()
    text= "x={:.3f}, y={:.3f}".format(xmax, ymax)
    if not ax:
        ax=plt.gca()
    bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
    arrowprops=dict(arrowstyle="->",connectionstyle="angle,angleA=0,angleB=60")
    kw = dict(xycoords='data',textcoords="axes fraction",
              arrowprops=arrowprops, bbox=bbox_props, ha="right", va="top")
    ax.annotate(text, xy=(xmax, ymax), xytext=(0.94,0.96), **kw)

annot_max(x,y)


ax.set_ylim(-0.3,1.5)
plt.show()

enter image description here

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

2 Comments

Just beautiful :)
Because there was some confusion about which method might work with which kind of input: The method presented in this answer works well with the input data being numpy arrays as well as pandas Series. Pure python lists will not work - in this case refer to @Anil_M's answer to this question.
26

I don't have data of macrodata.csv to go with. However, generically, assuming you have x and y axis data as an list, you can use following method to get auto positioning of max.

Working Code:

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)

x=[1,2,3,4,5,6,7,8,9,10]
y=[1,1,1,2,10,2,1,1,1,1]
line, = ax.plot(x, y)

ymax = max(y)
xpos = y.index(ymax)
xmax = x[xpos]

ax.annotate('local max', xy=(xmax, ymax), xytext=(xmax, ymax+5),
            arrowprops=dict(facecolor='black', shrink=0.05),
            )

ax.set_ylim(0,20)
plt.show()

Plot :
enter image description here

9 Comments

I get this error message when running it: TypeError: 'RangeIndex' object is not callable - I have my axis data inside DataFrame's, not array's. Is this why I get the error message? Because just from looking at your code I know it should work.
Line 12: ( xpos = y.index(ymax) )
Tried in both python 2.7 and 3.6 , did not throw any error and was able to plot the graph. Not sure whats going on at your end.
Here, y is a python list. Lists have an .index method. However, pandas Series do not have this method; it therefore fails.
@ ImportanceOfBeingErnest: Yes, Thats why I stated that x and y are assumed to be list.
|
7

The method proposed by @ImportanceOfBeingErnest in his response is really neat, but it doesn't work if the data is within a panda data-frame whose index isn't a zero based uniform index ([0,1,2,..,N]), and it is desired to plot against the index -whose values are the x's-.

I took the liberty to adapt the aforementioned solution and use it with pandas plot function. I also wrote the symmetric min function.

def annot_max(x,y, ax=None):
    maxIxVal = np.argmax(y);
    zeroBasedIx = np.argwhere(y.index==maxIxVal).flatten()[0];
    xmax = x[zeroBasedIx];
    ymax = y.max()
    text= "k={:d}, measure={:.3f}".format(xmax, ymax)
    if not ax:
        ax=plt.gca()
    bbox_props = dict(boxstyle="round,pad=0.3", fc="w", ec="k", lw=0.72)
    arrowprops=dict(arrowstyle="-",connectionstyle="arc3,rad=0.1")
    kw = dict(xycoords='data',textcoords="axes fraction",
              arrowprops=arrowprops, bbox=bbox_props, ha="right", va="top")
    ax.annotate(text, xy=(xmax, ymax), xytext=(0.94,0.90), **kw)

def annot_min(x,y, ax=None):
    minIxVal = np.argmin(y);
    zeroBasedIx = np.argwhere(y.index==minIxVal).flatten()[0];
    xmin = x[zeroBasedIx];
    ymin = y.min()
    text= "k={:d}, measure={:.3f}".format(xmin, ymin)
    if not ax:
        ax=plt.gca()
    bbox_props = dict(boxstyle="round,pad=0.3", fc="w", ec="k", lw=0.72)
    arrowprops=dict(arrowstyle="-",connectionstyle="arc3,rad=0.1")
    kw = dict(xycoords='data',textcoords="axes fraction",
              arrowprops=arrowprops, bbox=bbox_props, ha="right", va="top")
    ax.annotate(text, xy=(xmin, ymin), xytext=(0.94,0.90), **kw)

Usage is straightforward, for example:

ax = df[Series[0]].plot(grid=True, use_index=True, \
                  title=None);
annot_max(df[Series[0]].index,df[Series[0]],ax);
plt.show();

I hope this would be of any help to anyone.

Comments

2

For a single Maxima:

Set the peak_distances arg to a large number.

annot_peaks(x,y, ax, peak_distances=10000, y_position_modifier=14)

enter image description here

More than 1 Maxima / Annotate Peaks:

Set the peak_distances arg to a smaller number.

annot_peaks(x,y, ax, peak_distances=30, y_position_modifier=14)

enter image description here

Function for Automatic Annotation of Peaks:

For more than one maxima the we can use very similar code to @ImportanceOfBeingErnest's annot_max(x,y) function; with a couple of important differences and a for-loop for each peak's annotation:

import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import find_peaks
from sklearn.preprocessing import minmax_scale


def annot_peaks(x:np.array,y:np.array, ax=None, peak_distance=30, y_position_modifier=1):
    yindices, _ = find_peaks(y, distance=peak_distance)
    xmax = x[yindices]
    ymax = y[yindices]
    ymodifier = {k:v for k,v in zip(y, y_position_modifier-minmax_scale(y, feature_range=(0,y_position_modifier)))}
    if not ax:
        ax=plt.gca()
    bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
    arrowprops=dict(arrowstyle="->", color="k",
                                connectionstyle="arc3,rad=0")
    kw = dict(xycoords='data',textcoords="data",
              arrowprops=arrowprops, bbox=bbox_props, ha="right", va="top")
    for xmx, ymx in zip(xmax,ymax):
        text= "x={:.1f},\ny={:.1f}".format(xmx, ymx)
        ax.annotate(text, xy=(xmx, ymx), xytext=(xmx*1.1, ymx*(1.5+ymodifier[ymx])), **kw)


x = np.linspace(-2,8, num=301)
y = np.sinc((x-2.21)*3)
fig, ax = plt.subplots()
ax.plot(x,y)

annot_peaks(x,y, ax, peak_distance=30, y_position_modifier=14)

ax.set_ylim(-0.3,1.5)
plt.show()

Find peaks:

Above uses the scipy.signal.find_peaks() function with a distance arg to determine the horizontal distance between each peak (i.e. smaller value gives more peaks):

from scipy.signal import find_peaks
yindices, _ = find_peaks(y, distance=10)
xmax = x[yindices]
ymax = y[yindices]

Annotation positioning:

Many peaks means careful annotation positioning. To specify an annotation's y-position, a minmax scaled value of y is inverted using a q argument. (This is later used as a text position modifier, i.e. ypos=ymx+(1.1*modifier)). The output is stored in a dictionary-lookup for convenient lookup.

from sklearn.preprocessing import minmax_scale
q = 1
ymodifier = {k:v for k,v in zip(y, q-minmax_scale(y, feature_range=(0,q)))}
ymodifier[ y[0] ]
  • To manage different data magnitudes (e.g. y=range(0,2) or y=range(0,1000)), we can modify an arg q. For the data above q=14, or the nicer nomenclature: y_position_modifier=14.

Comments

0

Something like this would work:

infl_max_index = np.where(infl == max(infl)) #get the index of the maximum inflation
infl_max = infl[infl_max_index] # get the inflation corresponding to this index
year_max = year[infl_max_index] # get the year corresponding to this index

plt.annotate('max inflation', xy=(year_max, infl_max))

2 Comments

FYI, argmax is a built-in method to get the index of the maximum.
@Alex I get this: ValueError: Can only tuple-ndex with MultiIndex

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.