Search code examples
pythonmatplotliberrorbar

Upper limit with upper error


Is it possibile to have an upper limit (with the down arrow) with the point centered in the the best value and, at the same time, the upper error?

Something like this:

enter image description here

I'm trying with:

import numpy as np
import matplotlib.pyplot as plt

x = np.array([10, 15, 20, 25, 30, 35])
x_el = np.array([1, 1, 2, 25, 1, 2, 1])
x_eu = np.array([1, 1, 2, 1, 1, 2, 1])
y = np.array([29, 15, 9, 10, 25, 14])
y_el = np.array([1, 1, 2, 1, 1, 2, 1])
y_eu = np.array([11,1,2,1,1,2,1])

fig, ax = plt.subplots()

for i in range(len(x)):
    if (x[i] - x_el[i]) == 0:
        el = 0
        ax.errorbar(x[i], y[i], yerr=[[y_el[i]], [y_eu[i]]], xerr=[[el],[x_eu[i]]],
                    c='b', capsize=2, elinewidth=1, marker='o',
                    xuplims=True)
    else:
        ax.errorbar(x[i], y[i], yerr=[[y_el[i]], [y_eu[i]]], xerr=[[x_el[i]], [x_eu[i]]],
                    c='b', capsize=2, elinewidth=1, marker='o')

But this is the result:

enter image description here

The point number 4 has neither the uplim nor the upper error.


Solution

  • The short answer is yes, but you have to plot the upper limits and the error bars separately. Let's start by plotting your normal error bars properly. You can do this without looping if your data is in a numpy array already:

    import numpy as np
    import matplotlib.pyplot as plt
    
    x = np.array([10, 15, 20, 25, 30, 35])
    x_el = np.array([1, 1, 2, 25, 1, 2])
    x_eu = np.array([1, 1, 2, 1, 1, 2])
    y = np.array([29, 15, 9, 10, 25, 14])
    y_el = np.array([1, 1, 2, 1, 1, 2])
    y_eu = np.array([11, 1, 2, 1, 1, 2])
    
    fig, ax = plt.subplots()
    
    mask = (x != x_el)
    
    ax.errorbar(x, y, yerr=[y_el, y_eu], xerr=[x_el * mask, x_eu],
                c='b', capsize=2, elinewidth=1, marker='o', linestyle='none')
    

    Notice that I trimmed the error bar arrays down to the same size as x, which allows me to compute the mask using the != operator. Since you are interested in having all the error bars besides the one in x_el, I multiply by the mask. The mask is a boolean, and any error bar that is masked out will just be set to zero that way. All the other bars get plotted properly at this point:

    enter image description here

    Now you can use the same mask (but inverted) to plot the upper limits:

    ax.errorbar(x[~mask], y[~mask], xerr=x_el[~mask],
                c='b', capsize=2, elinewidth=1, marker='o', linestyle='none',
                xuplims=True)
    

    The result is

    enter image description here

    If you were not interested in having an obscenely long arrow that stretches to zero, you can shorten it to whatever size you like:

    ax.errorbar(x[~mask], y[~mask], xerr=1,
                c='b', capsize=2, elinewidth=1, marker='o', linestyle='none',
                xuplims=True)
    

    enter image description here

    Alternative

    You could even get pretty close with a single plotting call, since xuplims accepts an array of booleans. However, anywhere it is True will eliminate the right bar:

    mask = (x == x_el)
    ax.errorbar(x, y, yerr=[y_el, y_eu], xerr=[x_el, x_eu],
                c='b', capsize=2, elinewidth=1, marker='o', linestyle='none',
                xuplims=mask)
    

    enter image description here

    You end up having to fill in the right bars in this case:

    ax.errorbar(x[mask], y[mask], xerr=[np.zeros_like(x_eu)[mask], x_eu[mask]],
                c='b', capsize=2, elinewidth=1, marker='o', linestyle='none')
    

    enter image description here