Get the count / sum of number of bars on a floating hbar plot

Viewed 184

I am currently using the matplotlib.pyplot module to plot floating horizontal bar charts of pandas dataframe.

I would like to know how I can add an additional bar with the sum / count of bars over specific segments. In other words, from the code and plot below, I would like building a dataframe giving as output :

KP 0.0 to KP 0.1  : 1 bar
KP 0.1 to KP 0.5  : 2 bars
KP 0.5 to KP 0.55 : 3 bars
KP 0.55 to KP 0.7 : 2 bars

etc...

Regards,

Image plot

import pandas as pd
import matplotlib.pyplot as plt

#Create the pandas dataframe
d = {'KP_from' : [0.12, 0.84, 0.5, 0.7], 'KP_to' : [0.55, 0.05, 0.8, 0.75]}
df = pd.DataFrame(data = d)

#Create relavant variables for the floating hbar plot
start = df[['KP_from','KP_to']].min(axis = 1)
mid = df[['KP_from','KP_to']].mean(axis = 1)
width = abs(df.KP_from - df.KP_to)
yval = range(df.shape[0])

df['Direction'] = ['->' if df.KP_from.iloc[i] < df.KP_to.iloc[i] else '<-' for i in yval]

#Create the mpl figure : floating hbar plot
plt.figure()
plt.xlabel('KP')
plt.barh(y = yval, left = start, width = width)
#Add direction arrows at the center of the floating hbar
for i in yval:
    plt.annotate(df.Direction.iloc[i], xy = (mid[i], yval[i]), va = 'center', ha = 'center')
plt.show()
3 Answers

Here is another approach. As others have already pointed out, this is first of all an intersection problem which can be solved without the bar plot.

def is_present(i_start, i_end, start, end):
    return (
        ((start <= i_start) & (end > i_start)) |
        ((start > i_start) & (end <= i_end))
    )


# Create the pandas dataframe
d = {'KP_from': [0.12, 0.84, 0.5, 0.7], 'KP_to': [0.55, 0.05, 0.8, 0.75]}

intervals = sorted(set(d['KP_from'] + d['KP_to']))
n_bins = [
    sum([is_present(i, j, s, e) for s, e in zip(d['KP_from'], d['KP_to'])])
    for i, j in zip(intervals, intervals[1:])
]

for i, j, c in zip(intervals, intervals[1:], n_bins):
    print(f'KP {i} to KP {j} \t: {c} bars')

Your question really boils down to count how many bars are in each segment, I did it this way:

def create_final(data):

    data = [(x[0],x[1]) if x[0]<x[1] else (x[1],x[0]) for x in data]

    ans = []

    points = [(i[0], 'init') for i in data] + [(i[1], 'end') for i in data]

    points = sorted(points)

    carry = points[0]
    nums = 1
    for i in range(1, len(points)):

        ans.append((carry[0], points[i][0], nums))

        if points[i][1] == 'init':
            nums+=1

        elif points[i][1]=='end':
            nums-=1

        carry = points[i]

    return ans

data = [tuple(x) for x in df[['KP_from', 'KP_to']].values]

create_final(data)
[(0.05, 0.12, 1),
 (0.12, 0.5, 2),
 (0.5, 0.55, 3),
 (0.55, 0.7, 2),
 (0.7, 0.75, 3),
 (0.75, 0.8, 2),
 (0.8, 0.84, 1)]

I don't exactly what format you want, but from this list of tuples (start, end, number of bars) you can plot what you want.

This is more of an intersection problem:

# the bar's endpoints in correct order
data = np.sort(df.values, axis=1)

# The limits of interest -- update the limits as you want
limits = [0, 0.1, 0.5, 0.55, 0.7]
limits_data = np.array([[limits[i], limits[i+1]] for i in range(len(limits)-1)])

# intersections
intersect = (np.maximum(data[:,None, 0], limits_data[:, 0]) <
             np.minimum(data[:,1,None], limits_data[:,1]) )


# count intersections by limit points
counts = intersect.sum(0)

Output:

array([1, 2, 3, 2])

To obtain your expected printout:

for count, (x,y) in zip(counts, limits_data):
    print(f'KP {x} to KP {y}  : {count} bar(s)')

Output:

KP 0.0 to KP 0.1  : 1 bar(s)
KP 0.1 to KP 0.5  : 2 bar(s)
KP 0.5 to KP 0.55  : 3 bar(s)
KP 0.55 to KP 0.7  : 2 bar(s)
Related