Add style details to a barplot - python

I have prepared the following plot:
x = [1, 2, 3, 4, 5, 6]
y = [-0.015, 0.386, -0.273, -0.091, 0.955, 1.727]
errors = [0.744, 0.954, 0.737, 0.969, 0.848, 0.460]
plt.figure(figsize=(8, 4))
plt.bar(x, y, yerr=errors, align='center', alpha=0.5, color='grey')
plt.xticks((0, 1, 2, 3, 4, 5, 6, 7), ('', 'I1', 'I2', 'I3', 'I4', 'I5', 'I6', ''))
plt.ylim((-3, 3))
plt.show()
I have a couple of questions:
How do i add the bottom and top dashes in the error segment?
I would like to color the background green if y > 0.8, yellow if -0.8 <= y <= 0.8, red if y < -0.8. How can I do it?

To set the horizontal lines, you need to use capsize=n. Larger the n, wider the horizontal lines. You can set the background color using axvspan() and select color based on the value of y. Below is the updated code. I changed y value of one to show the red color...
x = [1, 2, 3, 4, 5, 6]
y = [-0.015, 0.386, -0.273, -0.091, -0.955, 1.727]
errors = [0.744, 0.954, 0.737, 0.969, 0.848, 0.460]
plt.figure(figsize=(8, 4))
ax=plt.bar(x, y, yerr=errors, align='center', alpha=0.5, color='grey', capsize=5)
plt.xticks((0, 1, 2, 3, 4, 5, 6, 7), ('', 'I1', 'I2', 'I3', 'I4', 'I5', 'I6', ''))
plt.ylim((-3, 3))
for i in range(1,7):
if y[i-1] > 0.8:
color = 'green'
elif y[i-1] > -0.8:
color = 'yellow'
else:
color = 'red'
plt.axvspan(i-0.5, i+.5, facecolor=color, alpha=0.3)
plt.show()
EDIT:
As requested, you are looking for horizontal ranges. So, you would need to use axhspan(). To bring the gray to solid, change alpha to 1 and you will need to bring the bars to the front using zorder(). I also added edgecolor='gray' to keep the look of a fully solid bar. Code is below...
plt.figure(figsize=(8, 4))
ax=plt.bar(x, y, yerr=errors, align='center', alpha=1, color='grey', edgecolor='gray',capsize=10, zorder=2)
plt.xticks((0, 1, 2, 3, 4, 5, 6, 7), ('', 'I1', 'I2', 'I3', 'I4', 'I5', 'I6', ''))
plt.ylim((-3, 3))
plt.axhspan(0.8, plt.gca().get_ylim()[1], facecolor='green', alpha=0.3, zorder=1)
plt.axhspan(-0.8, 0.8, facecolor='yellow', alpha=0.3, zorder=1)
plt.axhspan(plt.gca().get_ylim()[0], -0.8, facecolor='red', alpha=0.3, zorder=1)
plt.show()

Related

adjust_text: set label distance to a line

I have the following dataframe:
d = {'a': [2, 3, 4.5], 'b': [3, 2, 5]}
df = pd.DataFrame(data=d, index=["val1", "val2","val3"])
df.head()
a b
val1 2.0 3
val2 3.0 2
val3 4.5 5
I plotted this dataframe with the following code:
fig, ax=plt.subplots(figsize=(10,10))
ax.scatter(df["a"], df["b"],s=1)
x1=[0, 2512]
y1=[0, 2512]
ax.plot(x1,y1, 'r-')
#set limits:
ax = plt.gca()
ax.set_xlim([0, 10])
ax.set_ylim([0, 10])
#add labels:
TEXTS = []
for idx, names in enumerate(df.index.values):
x, y = df["a"].iloc[idx], df["b"].iloc[idx]
TEXTS.append(ax.text(x, y, names, fontsize=12));
# Adjust text position and add lines
adjust_text(
TEXTS,
expand_points=(2.5, 2.5),
expand_text=(2.5,2),
autoalign="xy",
arrowprops=dict(arrowstyle="-", lw=1),
ax=ax
);
However, I can not find a way to push the labels away from the red diagonal line, in order to get this result:
You can use the regular matplotlib annotate function and change the direction of the offset depending on the position of the data point relative to the red line:
ax = df.plot.scatter('a', 'b')
ax.set_aspect(1)
ax.plot((0,10), (0,10), 'r-')
offset = np.array([-1, 1])
for s, xy in df.iterrows():
xy = xy.to_numpy()
direction = 1 if xy[1] > xy[0] else -1
ax.annotate(s, xy, xy + direction * offset, ha='center', va='center', arrowprops=dict(arrowstyle='-', lw=1))

Confidence intervals in Radar charts (Area is not closed)

I'm trying to plot data containing confidence intervals in radar chart.
I followed matplotlib documentation here and made changes to add the error area but it is showing an empty area between Cat.1 and Cat.9 as shown in the plot.
When I set endpoint to true in the theta calculation, I lose a data point.
How can I fix it without losing a data point.
Link to plot
here is the modified code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, RegularPolygon
from matplotlib.path import Path
from matplotlib.projections.polar import PolarAxes
from matplotlib.projections import register_projection
from matplotlib.spines import Spine
from matplotlib.transforms import Affine2D
def radar_factory(num_vars, frame='circle'):
"""
Create a radar chart with `num_vars` axes.
This function creates a RadarAxes projection and registers it.
Parameters
----------
num_vars : int
Number of variables for radar chart.
frame : {'circle', 'polygon'}
Shape of frame surrounding axes.
"""
# calculate evenly-spaced axis angles
theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False)
class RadarAxes(PolarAxes):
name = 'radar'
# use 1 line segment to connect specified points
RESOLUTION = 1
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# rotate plot such that the first axis is at the top
self.set_theta_zero_location('N')
def fill(self, *args, closed=True, **kwargs):
"""Override fill so that line is closed by default"""
return super().fill(closed=closed, *args, **kwargs)
def plot(self, *args, **kwargs):
"""Override plot so that line is closed by default"""
lines = super().plot(*args, **kwargs)
for line in lines:
self._close_line(line)
def _close_line(self, line):
x, y = line.get_data()
# FIXME: markers at x[0], y[0] get doubled-up
if x[0] != x[-1]:
x = np.append(x, x[0])
y = np.append(y, y[0])
line.set_data(x, y)
def set_varlabels(self, labels):
self.set_thetagrids(np.degrees(theta), labels)
def _gen_axes_patch(self):
# The Axes patch must be centered at (0.5, 0.5) and of radius 0.5
# in axes coordinates.
if frame == 'circle':
return Circle((0.5, 0.5), 0.5)
elif frame == 'polygon':
return RegularPolygon((0.5, 0.5), num_vars,
radius=.5, edgecolor="k")
else:
raise ValueError("Unknown value for 'frame': %s" % frame)
def _gen_axes_spines(self):
if frame == 'circle':
return super()._gen_axes_spines()
elif frame == 'polygon':
# spine_type must be 'left'/'right'/'top'/'bottom'/'circle'.
spine = Spine(axes=self,
spine_type='circle',
path=Path.unit_regular_polygon(num_vars))
# unit_regular_polygon gives a polygon of radius 1 centered at
# (0, 0) but we want a polygon of radius 0.5 centered at (0.5,
# 0.5) in axes coordinates.
spine.set_transform(Affine2D().scale(.5).translate(.5, .5)
+ self.transAxes)
return {'polar': spine}
else:
raise ValueError("Unknown value for 'frame': %s" % frame)
register_projection(RadarAxes)
return theta
def example_data():
data = [
['Cat. 1', 'Cat. 2', 'Cat. 3', 'Cat. 4', 'Cat. 5', 'Cat. 6', 'Cat. 7', 'Cat. 8', 'Cat. 9'],
('mean1', [
[5, 5, 5, 5, 5, 5, 5, 5, 5]]),
('upper1', [
[6, 6, 6, 6, 6, 6, 6, 6, 6]]),
('lower1', [
[4, 4, 4, 4, 4, 4, 4, 4, 4]]),
('mean2', [
[2, 2, 2, 2, 2, 2, 2, 2, 2]]),
('upper2', [
[3, 3, 3, 3, 3, 3, 3, 3, 3]]),
('lower2', [
[1, 1, 1, 1, 1, 1, 1, 1, 1]])
]
return data
if __name__ == '__main__':
N = 9
theta = radar_factory(N, frame='circle')
data = example_data()
labels = data.pop(0)
mean_data=[data[0][1], data[3][1]]
upper_data=[data[1][1], data[4][1]]
lower_data=[data[2][1], data[5][1]]
fig, axs = plt.subplots(figsize=(9, 9), nrows=1, ncols=1,
subplot_kw=dict(projection='radar'))
fig.subplots_adjust(wspace=0.25, hspace=0.20, top=0.85, bottom=0.05)
colors = ['b', 'r']
# Plot the two cases from the example data on separate axes
for d, u, l, color in zip(mean_data, upper_data, lower_data, colors):
axs.plot(theta, d[0], color=color)
axs.fill_between(theta, u[0], l[0], facecolor=color, alpha=0.2)
axs.set_varlabels(labels)
# add legend relative to top-left plot
labels = ('Factor 1', 'Factor 2')
legend = axs.legend(labels, loc=(0.9, .95),
labelspacing=0.1, fontsize='small')
plt.show()
I modified the code mentioned here to add the confidence intervals and it works see the plot generated.
categories =['Cat. 1', 'Cat. 2', 'Cat. 3', 'Cat. 4', 'Cat. 5', 'Cat. 6', 'Cat. 7', 'Cat. 8', 'Cat. 9']
categories = [*categories, categories[0]]
mean = [5, 5, 5, 5, 5, 5, 5, 5, 5]
upper = [6, 6, 6, 6, 6, 6, 6, 6, 6]
lower = [4, 4, 4, 4, 4, 4, 4, 4, 4]
mean = [*mean, mean[0]]
upper = [*upper, upper[0]]
lower = [*lower, lower[0]]
mean1= [2, 2, 2, 2, 2, 2, 2, 2, 2]
upper1=[3, 3, 3, 3, 3, 3, 3, 3, 3]
lower1=[1, 1, 1, 1, 1, 1, 1, 1, 1]
mean1 = [*mean1, mean1[0]]
upper1 = [*upper1, upper1[0]]
lower1 = [*lower1, lower1[0]]
label_loc = np.linspace(start=0, stop=2 * np.pi, num=len(mean))
plt.figure(figsize=(8, 8))
plt.subplot(polar=True)
plt.plot(label_loc, mean, label='Factor 1',color='b')
plt.fill_between(label_loc, upper, lower, facecolor='b', alpha=0.2)
plt.subplot(polar=True)
plt.plot(label_loc, mean1, label='Factor 2',color='r')
plt.fill_between(label_loc, upper1, lower1, facecolor='r', alpha=0.2)
lines, labels = plt.thetagrids(np.degrees(label_loc), labels=categories)
plt.legend()
plt.show()

Seaborn Barplot - Inconsistence when displaying values

For a multi-group bar plot in Seaborn, I would like to add text which is reffered from the int_txt on top each of the bar plot.
However, the text is not placed as intended.
For example, the code below
import seaborn as sns
import pandas as pd
from matplotlib import pyplot as plt
# Create an example dataframe
data = {'pdvalue': [1, 1, 1, 1, 4, 4, 4, 4, 2, 2, 2, 2, 8, 8, 8, 8],
'xval': [0, 0, 0.5, 0.5, 0.2, 0, 0.2, 0.2, 0.3, 0.3, 0.4, 0.1, 1, 1.1, 3, 1],
'int_txt': [11, 14, 4, 5.1, 1, 2, 5.1, 1, 2, 4, 1, 3, 6, 6, 2, 3],
'group': ['a', 'b', 'c', 'd', 'a', 'b', 'c', 'd', 'a', 'b', 'c', 'd', 'a', 'b', 'c', 'd']}
df = pd.DataFrame(data)
df['int_txt'] = df['int_txt'].round(0).astype(int)
df=df.sort_values(by='pdvalue', ascending=True)
g = sns.barplot (data=df,x="pdvalue",y="xval",hue="group",)
for idx,p in enumerate(g.patches):
if p.get_height()!=0:
val_me=df['int_txt'][idx]
g.annotate(format(val_me, '.1f'),
(p.get_x() + p.get_width() / 2., p.get_height()),
ha = 'center', va = 'center',
xytext = (0, 9),
textcoords = 'offset points')
plt.show()
will produced
Whereas, the expected output shall be something like
The appended text is based on the look-up table
and for any xval equal to zero, no text will be appended.
May I know where did I do wrong?
You didn't do anything wrong really. It's just sns plots the bars by hue first. To see this do:
for idx,p in enumerate(g.patches):
# annotate the enumeration
g.annotate(format(idx, '.1f'),
(p.get_x() + p.get_width() / 2., p.get_height()),
ha = 'center', va = 'center',
xytext = (0, 9),
textcoords = 'offset points')
And you see (notice the enumeration on top)
One way around is to sort your data by hue column, then access with .iloc:
# sort by group first
df=df.sort_values(by=['group','pdvalue'], ascending=True)
g = sns.barplot (data=df,x="pdvalue",y="xval",hue="group",)
for idx,p in enumerate(g.patches):
if p.get_height()!=0:
# access with `iloc`, not `loc`
val_me=df['int_txt'].iloc[idx]
g.annotate(format(val_me, '.1f'),
(p.get_x() + p.get_width() / 2., p.get_height()),
ha = 'center', va = 'center',
xytext = (0, 9),
textcoords = 'offset points')
And you would get the expected annotation:

Sharing Y-axis in a matplotlib subplots

I have been trying to create a matplotlib subplot (1 x 3) with horizontal bar plots on either side of a lineplot.
It looks like this:
The code for generating the above plot -
u_list = [2, 0, 0, 0, 1, 5, 0, 4, 0, 0]
n_list = [0, 0, 1, 0, 4, 3, 1, 1, 0, 6]
arr_ = list(np.arange(10, 11, 0.1))
data_ = pd.DataFrame({
'points': list(np.arange(0, 10, 1)),
'value': [10.4, 10.5, 10.3, 10.7, 10.9, 10.5, 10.6, 10.3, 10.2, 10.4][::-1]
})
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 8))
ax1 = plt.subplot(1, 3, 1)
sns.barplot(u_list, arr_, orient="h", ax=ax1)
ax2 = plt.subplot(1, 3, 2)
x = data_['points'].tolist()
y = data_['value'].tolist()
ax2.plot(x, y)
ax2.set_yticks(arr_)
plt.gca().invert_yaxis()
ax3 = plt.subplot(1, 3, 3, sharey=ax1, sharex=ax1)
sns.barplot(n_list, arr_, orient="h", ax=ax3)
fig.tight_layout()
plt.show()
Edit
How do I share the y-axis of the central line plot with the other horizontal bar plots?
I would set the limits of all y-axes to the same range, set the ticks in all axes and than set the ticks/tick-labels of all but the most left axis to be empty. Here is what I mean:
from matplotlib import pyplot as plt
import numpy as np
u_list = [2, 0, 0, 0, 1, 5, 0, 4, 0, 0]
n_list = [0, 0, 1, 0, 4, 3, 1, 1, 0, 6]
arr_ = list(np.arange(10, 11, 0.1))
x = list(np.arange(0, 10, 1))
y = [10.4, 10.5, 10.3, 10.7, 10.9, 10.5, 10.6, 10.3, 10.2, 10.4]
fig, axs = plt.subplots(1, 3, figsize=(20, 8))
axs[0].barh(arr_,u_list,height=0.1)
axs[0].invert_yaxis()
axs[1].plot(x, y)
axs[1].invert_yaxis()
axs[2].barh(arr_,n_list,height=0.1)
axs[2].invert_yaxis()
for i in range(1,len(axs)):
axs[i].set_ylim( axs[0].get_ylim() ) # align axes
axs[i].set_yticks([]) # set ticks to be empty (no ticks, no tick-labels)
fig.tight_layout()
plt.show()
This is a minimal example and for the sake of conciseness, I refrained from mixing matplotlib and searborn. Since seaborn uses matplotlib under the hood, you can reproduce the same output there (but with nicer bars).

How to place lines below markers in Python?

I have to plot multiple lines and their curve fit lines on a single plot. All these lines are plotted using a for loop. Since it is plot using loops the curve fit lines of the succeeding step is plotted over its predecessor as shown in figure.
The reproducible code:
import matplotlib.pyplot as plt
import numpy as np
x = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
y = np.array([[4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24],
[6, 5.2, 8.5, 9.1, 13.4, 15.1, 16.1, 18.3, 20.4, 22.1, 23.7]])
m, n = x.shape
figure = plt.figure(figsize=(5.15, 5.15))
figure.clf()
plot = plt.subplot(111)
for i in range(m):
poly = np.polyfit(x[i, :], y[i, :], deg =1)
plt.plot(poly[0] * x[i, :] + poly[1], linestyle = '-')
plt.plot(x[i, :], y[i, :], linestyle = '', marker = 'o', markersize = 20)
plot.set_ylabel('Y', labelpad = 6)
plot.set_xlabel('X', labelpad = 6)
plt.show()
I can fix this using another loop as:
import matplotlib.pyplot as plt
import numpy as np
x = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
y = np.array([[4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24],
[6, 5.2, 8.5, 9.1, 13.4, 15.1, 16.1, 18.3, 20.4, 22.1, 23.7]])
m, n = x.shape
figure = plt.figure(figsize=(5.15, 5.15))
figure.clf()
plot = plt.subplot(111)
for i in range(m):
poly = np.polyfit(x[i, :], y[i, :], deg =1)
plt.plot(poly[0] * x[i, :] + poly[1], linestyle = '-')
for i in range(m):
plt.plot(x[i, :], y[i, :], linestyle = '', marker = 'o', markersize = 20)
plot.set_ylabel('Y', labelpad = 6)
plot.set_xlabel('X', labelpad = 6)
plt.show()
which gives me all the fit lines below the markers.
But is there any built-in function in Python/matplotlib to do this without using two loops?
Update
Only as an example I have used n = 2, n can be greater than 2, i.e. the loop would be run multiple times.
Update 2 after answer
Can I do this for the same line also? As an example:
plt.plot(x[i, :], y[i, :], linestyle = ':', marker = 'o', markersize = 20)
Can I give the linestyle a zorder = 1 and the markers a zorder = 3?
Editing just your plotting lines:
plt.plot(poly[0] * x[i, :] + poly[1], linestyle = '-',
zorder=-1)
plt.plot(x[i, :], y[i, :], linestyle = '', marker = 'o', markersize = 20,
zorder=3)
now the markers are all in front of the lines, though within marker/line groups they're still order-of-plotting.
Update answer
No. One call to plot, one zorder argument.
If you want to match the color and style of markers and line in each pass through the loop, set up an iterator or generator for colors and get current_color on each pass, then use that as an argument for plot calls.

Categories

Resources