Edge effects Density 2D plot with KDE - python

I'm plotting a simple 2D density map obtained with scipy.stats.gaussian_kde. There is always a plotting artifact towards the edges where the density appears to be lower:
I've tried every interpolation method in imshow() and none seems to be able to get rid of it. Is there a proper way to handle this?
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
x_data = np.random.uniform(1., 2000., 1000)
y_data = np.random.uniform(1., 2000., 1000)
xmin, xmax = np.min(x_data), np.max(x_data)
ymin, ymax = np.min(y_data), np.max(y_data)
values = np.vstack([x_data, y_data])
# Gaussian KDE.
kernel = stats.gaussian_kde(values, bw_method=.2)
# Grid density (number of points).
gd_c = complex(0, 50)
# Define x,y grid.
x_grid, y_grid = np.mgrid[xmin:xmax:gd_c, ymin:ymax:gd_c]
positions = np.vstack([x_grid.ravel(), y_grid.ravel()])
# Evaluate kernel in grid positions.
k_pos = kernel(positions)
ext_range = [xmin, xmax, ymin, ymax]
kde = np.reshape(k_pos.T, x_grid.shape)
im = plt.imshow(np.rot90(kde), cmap=plt.get_cmap('RdYlBu_r'), extent=ext_range)
plt.show()

After a while I found a way to address this issue applying a neat trick explained by Flabetvibes in this excellent answer.
I use the code shown there to mirror the data as shown in the first figure of the mentioned answer. The only modification I introduced is to trim the mirrored data to a perc padding (I set it to 10% by default) so as not to carry around a lot of unnecessary values.
The result is shown here, original non-mirrored data to the left, and mirrored data to the right:
As can be seen, the changes in the resulting density map are not trivial. I personally believe the mirrored-data KDE represents the actual density better.
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
def in_box(towers, bounding_box):
return np.logical_and(np.logical_and(bounding_box[0] <= towers[:, 0],
towers[:, 0] <= bounding_box[1]),
np.logical_and(bounding_box[2] <= towers[:, 1],
towers[:, 1] <= bounding_box[3]))
def dataMirror(towers, bounding_box, perc=.1):
# Select towers inside the bounding box
i = in_box(towers, bounding_box)
# Mirror points
points_center = towers[i, :]
points_left = np.copy(points_center)
points_left[:, 0] = bounding_box[0] - (points_left[:, 0] - bounding_box[0])
points_right = np.copy(points_center)
points_right[:, 0] = bounding_box[1] + (bounding_box[1] - points_right[:, 0])
points_down = np.copy(points_center)
points_down[:, 1] = bounding_box[2] - (points_down[:, 1] - bounding_box[2])
points_up = np.copy(points_center)
points_up[:, 1] = bounding_box[3] + (bounding_box[3] - points_up[:, 1])
points = np.append(points_center,
np.append(np.append(points_left,
points_right,
axis=0),
np.append(points_down,
points_up,
axis=0),
axis=0),
axis=0)
# Trim mirrored frame to withtin a 'perc' pad
xr, yr = np.ptp(towers.T[0]) * perc, np.ptp(towers.T[1]) * perc
xmin, xmax = bounding_box[0] - xr, bounding_box[1] + xr
ymin, ymax = bounding_box[2] - yr, bounding_box[3] + yr
msk = (points[:, 0] > xmin) & (points[:, 0] < xmax) &\
(points[:, 1] > ymin) & (points[:, 1] < ymax)
points = points[msk]
return points.T
def KDEplot(xmin, xmax, ymin, ymax, values):
# Gaussian KDE.
kernel = stats.gaussian_kde(values, bw_method=.2)
# Grid density (number of points).
gd_c = complex(0, 50)
# Define x,y grid.
x_grid, y_grid = np.mgrid[xmin:xmax:gd_c, ymin:ymax:gd_c]
positions = np.vstack([x_grid.ravel(), y_grid.ravel()])
# Evaluate kernel in grid positions.
k_pos = kernel(positions)
ext_range = [xmin, xmax, ymin, ymax]
kde = np.reshape(k_pos.T, x_grid.shape)
plt.imshow(np.rot90(kde), cmap=plt.get_cmap('RdYlBu_r'), extent=ext_range)
x_data = np.random.uniform(1., 2000., 1000)
y_data = np.random.uniform(1., 2000., 1000)
xmin, xmax = np.min(x_data), np.max(x_data)
ymin, ymax = np.min(y_data), np.max(y_data)
values = np.vstack([x_data, y_data])
# Plot non-mirrored data
plt.subplot(121)
KDEplot(xmin, xmax, ymin, ymax, values)
plt.scatter(*values, s=3, c='k')
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
# Plot mirrored data
bounding_box = (xmin, xmax, ymin, ymax)
values = dataMirror(values.T, bounding_box)
plt.subplot(122)
KDEplot(xmin, xmax, ymin, ymax, values)
plt.scatter(*values, s=3, c='k')
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
plt.show()

Related

How to make a wavy arrow that goes from A to B in matplotlib

I'm basing this question on a useful, yet closed question by Ian Roberts, and awesome answer by hayk.
How to make a wavy arrow that goes between specified points on a plt plot, with specified number of waves? Method should be compatible with many subplots to make it universal.
I give the code in the answer. It took me some time to figure out how to make it work on subplots, so you don't have to.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import matplotlib.patches as mpatches
import matplotlib as mpl
def curly_arrow(start, end, ax,arr_size=1, n=5, col='gray', linew=1., width=0.1):
xmin, ymin = start
xmax, ymax = end
dist = np.sqrt((xmin - xmax) ** 2 + (ymin - ymax) ** 2)
n0 = dist / (2 * np.pi)
x = np.linspace(0, dist, 151) + xmin
y = width * np.sin(n * x / n0) + ymin
line = plt.Line2D(x, y, color=col, lw=linew)
del_x = xmax - xmin
del_y = ymax - ymin
ang = np.arctan2(del_y, del_x)
line.set_transform(mpl.transforms.Affine2D().rotate_around(xmin, ymin, ang) + ax.transData)
ax.add_line(line)
verts = np.array([[0, 1], [0, -1], [2, 0], [0, 1]]).astype(float) * arr_size
verts[:, 1] += ymax
verts[:, 0] += xmax
path = mpath.Path(verts)
patch = mpatches.PathPatch(path, fc=col, ec=col)
patch.set_transform(mpl.transforms.Affine2D().rotate_around(xmax, ymax, ang) + ax.transData)
return patch
then you can use it like this:
fig, (ax1, ax2) = plt.subplots(1,2)
ax1.add_patch(curly_arrow((0, 0.5), (0.6, 0.5),ax1, n=4, arr_size=0.1))
ax2.add_patch(curly_arrow((0, 0.5), (0.6, 0.5),ax2, n=3, arr_size=0.1))

Visual defects in matplotlib graph

Current matplotlib graph
How it should look like
I want to change the graph color and gradient direction in the parts where graph goes below zero. Alternative image for illustration:
I have tried it using this code
def add_gradient_fill(ax: Optional[plt.Axes] = None, alpha_gradientglow: float = 1.0):
"""Add a gradient fill under each line,
i.e. faintly color the area below the line."""
if not ax:
ax = plt.gca()
lines = ax.get_lines()
for line in lines:
# don't add gradient fill for glow effect lines:
if hasattr(line, 'is_glow_line') and line.is_glow_line:
continue
fill_color = line.get_color()
zorder = line.get_zorder()
alpha = line.get_alpha()
alpha = 1.0 if alpha is None else alpha
rgb = mcolors.colorConverter.to_rgb(fill_color)
z = np.empty((100, 1, 4), dtype=float)
z[:, :, :3] = rgb
z[:, :, -1] = np.linspace(0, alpha, 100)[:, None]
x, y = line.get_data(orig=False)
x, y = np.array(x), np.array(y) # enforce x,y as numpy arrays
xmin, xmax = x.min(), x.max()
ymin, ymax = y.min(), y.max()
im = ax.imshow(z, aspect='auto',
extent=[xmin, xmax, ymin, ymax],
alpha=alpha_gradientglow,
origin='lower', zorder=zorder)
xy = np.column_stack([x, y])
xy = np.vstack([[xmin, ymin], xy, [xmax, ymin], [xmin, ymin]])
clip_path = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
ax.add_patch(clip_path)
im.set_clip_path(clip_path)
ax.autoscale(True)
This code is also a part of a matplotlib theming library called mplcyberpunk.
This provides great looks to the plot, but as mentioned earlier, I want that the sub-zero parts of the graphs be in different color with gradient direction reversed.
How can this be possibly achieved?
PS: Sincerely, my question is different from other graph gradient questions, please don't close this.
Edit
Minimal reproducible code
import matplotlib.pyplot as plt
import mplcyberpunk as mplcp
x = range(-10, 11)
y = [(i ** 2) - 50 for i in x]
plt.style.use('cyberpunk')
###### just for setting the theme, ignore these lines #########
for param in ['figure.facecolor', 'axes.facecolor', 'savefig.facecolor']:
plt.rcParams[param] = '#303030'
for param in ['text.color', 'axes.labelcolor', 'xtick.color', 'ytick.color']:
plt.rcParams[param] = '#ffffff'
plt.subplots()[1].grid(color='#404040')
##################################################################
plt.plot(x, y)
mplcp.make_lines_glow()
mplcp.add_gradient_fill()
plt.show()
Update:
Well I somehow figured it out, but there are some visual defects that need focus. Here are the functions and output:
from itertools import groupby
import numpy as np
from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.patches import Polygon
def add_glow_effects(n_glow_lines: int = 10,
diff_linewidth: float = 1.05,
alpha_line: float = 0.3,
change_line_color: bool = True,
color_positive: str = '#0000ff',
color_negative: str = '#ff0000',
alpha_gradientglow: float = 1.0, ):
make_lines_glow(n_glow_lines, diff_linewidth, alpha_line, change_line_color, color_positive, color_negative)
add_gradient_fill(alpha_gradientglow, color_positive, color_negative, )
def make_lines_glow(n_glow_lines: int = 10,
diff_linewidth: float = 1.05,
alpha_line: float = 0.3,
change_line_color: bool = True,
color_positive: str = '#0000ff',
color_negative: str = '#ff0000'):
ax = plt.gca()
lines = ax.get_lines()
alpha_value = alpha_line / n_glow_lines
for line_element in lines:
if not isinstance(line_element, Line2D):
continue
x, y = line_element.get_data(orig=False)
x, y = optimize_lines(list(x), list(y))
lines_list = list_form(x, y)
for line in lines_list:
if change_line_color:
y_avg = sum(line[1]) / len(line[1])
if y_avg >= 0:
color = color_positive
else:
color = color_negative
else:
color = line_element.get_color()
line = Line2D(line[0], line[1], linewidth=line_element.get_linewidth(), color=color)
data = list(line.get_data(orig=False))
linewidth = line.get_linewidth()
ax.plot(data[0], data[1], color=color, linewidth=linewidth)
for n in range(1, n_glow_lines + 1):
glow_line, = ax.plot(*data)
glow_line.update_from(line)
# line properties are copied as seen in this solution: https://stackoverflow.com/a/54688412/3240855
glow_line.set_alpha(alpha_value)
glow_line.set_linewidth(linewidth + (diff_linewidth * n))
# mark the glow lines, to disregard them in the underglow function.
glow_line.is_glow_line = True
# noinspection PyArgumentList
def add_gradient_fill(alpha_gradientglow: float = 1.0,
color_positive: str = '#00ff00',
color_negative: str = '#ff0000'):
"""Add a gradient fill under each line,
i.e. faintly color the area below the line."""
ax = plt.gca()
lines = ax.get_lines()
for line_element in lines:
if not isinstance(line_element, Line2D):
continue
x, y = line_element.get_data(orig=False)
x, y = optimize_lines(list(x), list(y))
lines_list = list_form(x, y)
for line in lines_list:
y_avg = sum(line[1]) / len(line[1])
# don't add gradient fill for glow effect lines:
if hasattr(line, 'is_glow_line') and line.is_glow_line:
continue
line = Line2D(line[0], line[1], linewidth=line_element.get_linewidth())
zorder = line.get_zorder()
alpha = line_element.get_alpha()
alpha = 1.0 if alpha is None else alpha
x, y = line.get_data(orig=False)
x, y = np.array(x), np.array(y) # enforce x,y as numpy arrays
xmin, xmax = x.min(), x.max()
ymin, ymax = y.min(), y.max()
xy = np.column_stack([x, y])
if y_avg >= 0:
fill_color = color_positive
linspace = np.linspace(0, alpha, 100)[:, None]
xy = np.vstack([[xmin, ymin], xy, [xmax, ymin], [xmin, ymin]])
else:
fill_color = color_negative
linspace = np.linspace(alpha, 0, 100)[:, None]
xy = np.vstack([[xmin, ymax], xy, [xmax, ymax], [xmin, ymax]])
rgb = mcolors.colorConverter.to_rgb(fill_color)
z = np.empty((100, 1, 4), dtype=float)
z[:, :, :3] = rgb
z[:, :, -1] = linspace
im = ax.imshow(z, aspect='auto',
extent=[xmin, xmax, ymin, ymax],
alpha=alpha_gradientglow,
origin='lower', zorder=zorder)
clip_path = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
ax.add_patch(clip_path)
im.set_clip_path(clip_path)
ax.autoscale(True)
def optimize_lines(x: list, y: list):
y = [list(element) for index, element in groupby(y, lambda a: a >= 0)]
indexes = [0]
for i in y:
indexes.append(len(i) + indexes[-1])
# from https://www.geeksforgeeks.org/python-group-consecutive-elements-by-sign/
x = [x[indexes[i]:indexes[i + 1]] for i, _ in enumerate(indexes) if i != len(indexes) - 1]
for i in range(len(y) - 1):
if y[i][-1] == 0 and y[i + 1][0] == 0:
continue
a = y[i][-1]
b = y[i + 1][0]
diff = abs(a) + abs(b)
a_ = (abs(0 - a)) / diff
b_ = abs(0 - b) / diff
x[i].append(x[i][-1] + a_)
x[i + 1].insert(0, x[i + 1][0] - b_)
y[i].append(0)
y[i + 1].insert(0, 0)
x = [list(i) for i in x]
y = [list(i) for i in y]
# input: x=[1,2,3,4,5], y=[1,2,-5,0,2]
# output: x=[[1, 2, 2.2857142857142856], [2.2857142857142856, 3, 4.0], [4.0, 4, 5]],
# y=[[1, 2, 0], [0, -5, 0], [0, 0, 2]]
return list(x), list(y)
def list_form(x: list[list], y: list[list]):
lst = []
for i in range(len(x)):
lst.append([x[i], y[i]])
return lst
The output is now this:
Notice how the glow from function is collected at the left side of graph. also, at the end of the graph these is a tiny purple triangle that is offset by one corner.
The title of this post has been changed to "Visual defects in matplotlib graph" from "Matplotlib graph gradient away from the x axis" for the purpose of relevance, keeping in mind the latest update to the post.
Interesting question. I have several ideas to help you there. I think the easiest solution will be to find an elegant way to "split" the data conditionally when zero-crossing occurs (but you need to detect the zero-crossings accurately for clean clipping masks).
The solution below is not yet finished, but it solves the first issue of having a two-color gradient and a compound path to get a positive/negative clipping mask. Now there is the line color that needs to be also split into + and - parts. So far, I just overlayed the line below zero on top of the existing line, and the glow of this line clearly mixes with the one of the first line.
I'll be back to it later; maybe this will help meanwhile.
import matplotlib.pyplot as plt
import mplcyberpunk as mplcp
import matplotlib.colors as mcolors
from matplotlib.path import Path
import numpy as np
from matplotlib.lines import Line2D
from matplotlib.patches import Polygon, PathPatch
def add_gradient_fill(ax=None, alpha_gradientglow=1.0, negative_color="C1"):
"""Add a gradient fill under each line,
i.e. faintly color the area below the line."""
if not ax:
ax = plt.gca()
lines = ax.get_lines()
for line in lines:
# don't add gradient fill for glow effect lines:
if hasattr(line, 'is_glow_line') and line.is_glow_line:
continue
fill_color = line.get_color()
zorder = line.get_zorder()
alpha = line.get_alpha()
alpha = 1.0 if alpha is None else alpha
rgb = mcolors.colorConverter.to_rgb(fill_color)
x, y = line.get_data(orig=False)
x, y = np.array(x), np.array(y) # enforce x,y as numpy arrays
xmin, xmax = np.nanmin(x), np.nanmax(x)
ymin, ymax = np.nanmin(y), np.nanmax(y)
z = np.empty((100, 1, 4), dtype=float)
z[:, :, :3] = rgb
# z[:, :, -1] = np.linspace(0, alpha, 100)[:, None]
ynorm = max(np.abs(ymin), np.abs(ymax))
ymin_norm = ymin / ynorm
ymax_norm = ymax / ynorm
ynorm = np.linspace(ymin_norm, ymax_norm, 100)
z[:, :, -1] = alpha * np.abs(ynorm[:, None])
rgb_neg = mcolors.colorConverter.to_rgb(negative_color)
z[ynorm < 0, :, :3] = rgb_neg
im = ax.imshow(z, aspect='auto',
extent=[xmin, xmax, ymin, ymax],
alpha=alpha_gradientglow,
origin='lower', zorder=zorder)
# Detect zero crossings
y_copy = y.copy()
y = y.clip(0, None)
xy = np.column_stack([x, y])
xy = np.vstack([[xmin, 0], xy, [xmax, 0], [xmin, 0]])
clip_path_1 = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
y = y_copy.copy()
y = y.clip(None, 0)
xy = np.column_stack([x, y])
xy = np.vstack([[xmin, 0], xy, [xmax, 0], [xmin, 0]])
clip_path_2 = Polygon(xy, facecolor='none', edgecolor='none', closed=True)
ax.add_patch(clip_path_1)
ax.add_patch(clip_path_2)
clip_paths = clip_path_2, clip_path_1
vertices = np.concatenate([i.get_path().vertices for i in clip_paths])
codes = np.concatenate([i.get_path().codes for i in clip_paths])
clip_path = PathPatch(Path(vertices, codes), transform=ax.transData)
im.set_clip_path(clip_path)
ax.autoscale(True)
y = y_copy.copy()
y[y > 0] = np.nan
ax.plot(x, y)
def make_lines_glow(
ax=None,
n_glow_lines: int = 10,
diff_linewidth: float = 1.05,
alpha_line: float = 0.3,
lines=None,
) -> None:
"""Add a glow effect to the lines in an axis object.
Each existing line is redrawn several times with increasing width and low alpha to create the glow effect.
"""
if not ax:
ax = plt.gca()
lines = ax.get_lines() if lines is None else lines
lines = [lines] if isinstance(lines, Line2D) else lines
alpha_value = alpha_line / n_glow_lines
for line in lines:
data = line.get_data(orig=False)
linewidth = line.get_linewidth()
try:
step_type = line.get_drawstyle().split('-')[1]
except:
step_type = None
for n in range(1, n_glow_lines + 1):
if step_type:
glow_line, = ax.step(*data)
else:
glow_line, = ax.plot(*data)
glow_line.update_from(line) # line properties are copied as seen in this solution: https://stackoverflow.com/a/54688412/3240855
glow_line.set_alpha(alpha_value)
glow_line.set_linewidth(linewidth + (diff_linewidth * n))
glow_line.is_glow_line = True # mark the glow lines, to disregard them in the underglow function.
x = np.arange(-10, 11)
y = np.array([(i ** 2) - 50 for i in x])
plt.style.use('cyberpunk')
for param in ['figure.facecolor', 'axes.facecolor', 'savefig.facecolor']:
plt.rcParams[param] = '#303030'
for param in ['text.color', 'axes.labelcolor', 'xtick.color', 'ytick.color']:
plt.rcParams[param] = '#ffffff'
plt.subplots()[1].grid(color='#404040')
plt.plot(x, y)
add_gradient_fill(negative_color="C1")
make_lines_glow()
plt.show()

How to annotate a regression line with the proper text rotation

I have the following snippet of code to draw a best-fit line through a collections of points on a graph, and annotate it with the corresponding R2 value:
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats
x = 50 * np.random.rand(20) + 50
y = 200 * np.random.rand(20)
plt.plot(x, y, 'o')
# k, n = np.polyfit(x, y, 1)
k, n, r, _, _ = scipy.stats.linregress(x, y)
line = plt.axline((0, n), slope=k, color='blue')
xy = line.get_xydata()
plt.annotate(
f'$R^2={r**2:.3f}$',
(xy[0] + xy[-1]) // 2,
xycoords='axes fraction',
ha='center', va='center_baseline',
rotation=k, rotation_mode='anchor',
)
plt.show()
I have tried various different (x,y) pairs, different xycoords and other keyword parameters in annotate but I haven't been able to get the annotation to properly appear where I want it. How do I get the text annotation to appear above the line with proper rotation, located either at the middle point of the line, or at either end?
1. Annotation coordinates
We cannot compute the coordinates using xydata here, as axline() just returns dummy xydata (probably due to the way matplotlib internally plots infinite lines):
print(line.get_xydata())
# array([[0., 0.],
# [1., 1.]])
Instead we can compute the text coordinates based on the xlim():
xmin, xmax = plt.xlim()
xtext = (xmin + xmax) // 2
ytext = k*xtext + n
Note that these are data coordinates, so they should be used with xycoords='data' instead of 'axes fraction'.
2. Annotation angle
We cannot compute the angle purely from the line points, as the angle will also depend on the axis limits and figure dimensions (e.g., imagine the required rotation angle in a 6x4 figure vs 2x8 figure).
Instead we should normalize the calculation to both scales to get the proper visual rotation:
rs = np.random.RandomState(0)
x = 50 * rs.rand(20) + 50
y = 200 * rs.rand(20)
plt.plot(x, y, 'o')
# save ax and fig scales
xmin, xmax = plt.xlim()
ymin, ymax = plt.ylim()
xfig, yfig = plt.gcf().get_size_inches()
k, n, r, _, _ = scipy.stats.linregress(x, y)
plt.axline((0, n), slope=k, color='blue')
# restore x and y limits after axline
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
# find text coordinates at midpoint of regression line
xtext = (xmin + xmax) // 2
ytext = k*xtext + n
# find run and rise of (xtext, ytext) vs (0, n)
dx = xtext
dy = ytext - n
# normalize to ax and fig scales
xnorm = dx * xfig / (xmax - xmin)
ynorm = dy * yfig / (ymax - ymin)
# find normalized annotation angle in radians
rotation = np.rad2deg(np.arctan2(ynorm, xnorm))
plt.annotate(
f'$R^2={r**2:.3f}$',
(xtext, ytext), xycoords='data',
ha='center', va='bottom',
rotation=rotation, rotation_mode='anchor',
)

Python highlight user chosen area in contourf plot

What is the best solution for highlighting an area in a contourf plot?
I want the background to be opacity of 0.5 and the user chosen area to be normal. How can I achieve this?
In How to nicely plot clipped layered artists in matplotlib? Jake Vanderplas shows a way to draw a rectangle with a rectangular hole. The code can be adapted for your situation. The following example starts from a tutorial example, and highlights the third contour:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch
def DoubleRect(xy1, width1, height1,
xy2, width2, height2, **kwargs):
base = np.array([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
verts = np.vstack([xy1 + (width1, height1) * base,
xy2 + (width2, height2) * base[::-1],
xy1])
codes = 2 * ([Path.MOVETO] + 4 * [Path.LINETO]) + [Path.CLOSEPOLY]
return PathPatch(Path(verts, codes), **kwargs)
origin = 'lower'
delta = 0.025
x = y = np.arange(-3.0, 3.01, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X ** 2 - Y ** 2)
Z2 = np.exp(-(X - 1) ** 2 - (Y - 1) ** 2)
Z = (Z1 - Z2) * 2
fig, ax = plt.subplots()
contours = ax.contourf(X, Y, Z, 10, cmap=plt.cm.turbo, origin=origin)
# contours.collections[2].set_color('deepskyblue') # mark one contour
# calculate (or get) the coordinates of the hole
bbox = contours.collections[2].get_paths()[0].get_extents()
hole_xy, hole_width, hole_height = bbox.p0, bbox.width, bbox.height
# find the coordinates of the surrounding rectangle
xmin, xmax = ax.get_xlim()
ymin, ymax = ax.get_ylim()
full_rect = plt.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, color='black', alpha=0.5)
ax.add_patch(full_rect)
# create a rectangle with a hole to clip the surrounding rectangle
mask = DoubleRect((xmin, ymin), xmax - xmin, ymax - ymin,
hole_xy, hole_width, hole_height,
facecolor='none', edgecolor='none')
ax.add_patch(mask)
full_rect.set_clip_path(mask)
plt.show()
Instead of darkening the outside region, it could also be hatched (similar to the linked post). This would set the edge color of the mask to 'black', and create the full rectangle with hatching.
full_rect = plt.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, facecolor='none', edgecolor='black', hatch='//')

How to draw colored rectangles around grouped clusters in dendogram?

I try to add colored rectangle to dendrogram results like as follow:
this is my dendrogram codes:
from scipy.cluster.hierarchy import dendrogram
...
plt.figure(figsize=(250, 100))
labelsize=20
ticksize=15
plt.title(file_name.split(".")[0], fontsize=labelsize)
plt.xlabel('stock', fontsize=labelsize)
plt.ylabel('distance', fontsize=labelsize)
dendrogram(
Z,
leaf_rotation=90., # rotates the x axis labels
leaf_font_size=8., # font size for the x axis labels
labels = corr.columns
)
pylab.yticks(fontsize=ticksize)
pylab.xticks(rotation=-90, fontsize=ticksize)
However, this is only add colorful line not a rectangle like in the above image. How can I create image like this?
Thanks
You can loop through the generated path collections and draw a bounding box.
Optionally, you could set the height to the color_threshold= parameter, which defaults to Z[:, 2].max() * 0.7.
The last collection is are the unclassified lines, so the example code below loops through all earlier collections.
import matplotlib.pyplot as plt
from scipy.cluster import hierarchy
import numpy as np
N = 15
ytdist = np.random.randint(10, 1000, N * (N + 1) // 2)
Z = hierarchy.linkage(ytdist)
fig, ax = plt.subplots(1, 1, figsize=(8, 3))
dn1 = hierarchy.dendrogram(Z, ax=ax)
for coll in ax.collections[:-1]: # the last collection is the ungrouped level
xmin, xmax = np.inf, -np.inf
ymax = -np.inf
for p in coll.get_paths():
box = p.get_extents()
(x0, _), (x1, y1) = p.get_extents().get_points()
xmin = min(xmin, x0)
xmax = max(xmax, x1)
ymax = max(ymax, y1)
rec = plt.Rectangle((xmin - 4, 0), xmax - xmin + 8, ymax*1.05,
facecolor=coll.get_color()[0], alpha=0.2, edgecolor="none")
ax.add_patch(rec)
plt.show()

Categories