How to remove noise-only rows from a numpy array? - numpy

I want to remove rows from a Numpy array, where there is just noise and a random constant offset.
My data looks similar to this:
offset = np.array([0.2, 3.2])
signal = np.sin(np.arange(0, 2 * np.pi, 0.1))
null = np.zeros_like(signal)
data_block = np.array([signal, null]).T
padding = np.zeros((10, 2))
block = np.vstack((padding, data_block, padding)) + offset
# add noise
shape = block.shape
noise = np.random.rand(shape[0], shape[1]) * .01
everything = noise + block
In reality, there is no step but rather a smooth transition from the offset to the data. It is like first there is the offset, which starts to move, once the data block starts, and becomes another offset when it stops. The noise amplitude is much smaller than the data amplitude.
I would like to retrieve the rows with the data block from everything, preferably based on continuous, smooth change in the data block. How can I do that?

This is my best effort on identifying the data_block. I would be happy if it could be improved!
import numpy as np
offset = np.array([0.2, 3.2])
signal = np.sin(np.arange(0, 2 * np.pi, 0.1))
null = np.zeros_like(signal)
data_block = np.array([signal, null]).T
padding = np.zeros((10, 2))
block = np.vstack((padding, data_block, padding)) + offset
# add noise
shape = block.shape
noise = np.random.rand(shape[0], shape[1]) * .01
everything = noise + block
from matplotlib import pyplot as plt
x = np.arange(shape[0])
plt.plot(x, everything[:, 0])
plt.plot(x, everything[:, 1])
plt.show()
diff_everything = np.diff(everything, axis=0)
x = np.arange(shape[0] - 1)
plt.plot(x, diff_everything[:, 0])
plt.plot(x, diff_everything[:, 1])
plt.show()
mask = (np.linalg.norm(diff_everything[:, :], axis=1) > 0.01)
mask = np.append(mask, False)
data = everything[mask, :]
shape = data.shape
x=np.arange(0,shape[0])
plt.plot(x, data[:, 0])
plt.plot(x, data[:, 1])
plt.show()

Related

multi animation whit subplot

I got some sort of a problem with a pendulum animation, I tried to display my animation (the pendulum's movement) next to a graph in two separate axes, but when I try my code, it barely works displaying two axes that overlap on one another... Here is what I tried:
PS: best would be that the circles I was intended to add at the end of my pendulum appear on the final animation, but I really have no idea how to put them only on a particular ax
from numpy import sin, cos, pi, array
import numpy as np
import scipy.integrate
import matplotlib.pyplot as plt
import matplotlib.animation as animation
g = 10
y0 = np.array([np.pi / 2.0, 0]) # angle, vitesse
j = 0.2
def f(y, t):
return np.array([y[1], -g * np.sin(y[0])-j*y[1]])
t = np.linspace(0, 100, 10000)
y = scipy.integrate.odeint(f, y0, t)
theta, thetadot = y[:, 0], y[:, 1]
fig, axs = plt.subplots(1,2)
axs[0] = fig.add_subplot(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5))
axs[0].grid()
axs[0].set_box_aspect(1)
# anchor = plt.Circle((0, 0), 0.01, color='black')
# mass = plt.Circle((sin(y0[0]),-cos(y0[0])), 0.2, color='black')
pendulums = axs[0].plot((0, sin(y0[0])), (0, -cos(y0[0])), 'o-', color = 'black')
# plt.gca().add_patch(weight) # adding circles
# plt.gca().add_patch(attach)
phase = axs[1].plot(theta,thetadot)
def animate(i):
angle = theta[i]
x = (0, sin(angle))
y = (0, -cos(angle))
#mass.center = (x[1],y[1])
pendulums[0].set_data(x, y)
anim = animation.FuncAnimation(fig, animate, interval=10)
plt.show()

Real-time plotting of a custom turning marker

Is it somehow possible to plot a custom marker (like this) interactively, but have it turn in real-time? It seems that the scatter graph does not grant any access to the markers.
You can create a custom marker with a FancyArrowPatch. Many styles and options are possible. Such a patch is not easy to update, but you could just remove the patch and create it again to create an animation.
The easiest way to create an animation is via plt.pause(), but that doesn't work in all environments. Another way is via FuncAnimation, which involves a few more lines, but makes controlling the animation easier.
Here is some example code to show the concepts:
import matplotlib.pyplot as plt
from matplotlib import patches
from matplotlib.collections import PatchCollection
from matplotlib import animation
import numpy as np
fig, ax = plt.subplots()
N = 50
x = np.random.uniform(-20, 20, (N, 2))
dx = np.random.uniform(-1, 1, (N, 2))
dx /= np.linalg.norm(dx, axis=1, keepdims=True)
colors = plt.cm.magma(np.random.uniform(0, 1, N))
arrow_style = "Simple,head_length=2,head_width=3,tail_width=1"
ax.set_xlim(-40, 40)
ax.set_ylim(-30, 30)
ax.set_aspect('equal')
old_arrows = None
def animate(i):
global old_arrows, x, dx
if old_arrows is not None:
old_arrows.remove()
x += dx
dx += np.random.uniform(-.1, .1, (N, 2))
dx /= np.linalg.norm(dx, axis=1, keepdims=True)
arrows = [patches.FancyArrowPatch((xi, yi), (xi + dxi * 10, yi + dyi * 10), arrowstyle=arrow_style)
for (xi, yi), (dxi, dyi) in zip(x, dx)]
old_arrows = ax.add_collection(PatchCollection(arrows, facecolors=colors))
return old_arrows,
ani = animation.FuncAnimation(fig, animate, np.arange(1, 200),
interval=25, repeat=False, blit=True)
plt.show()
I solved it by remove() and static variables like this:
class pltMarker:
def __init__(self, angle=None, pathString=None):
self.angle = angle or []
self.pathString = pathString or """simply make and svg, open in a text editor and copy the path XML string in here"""
self.path = parse_path( self.pathString )
self.path.vertices -= self.path.vertices.mean( axis=0 )
self.marker = mpl.markers.MarkerStyle( marker=self.path )
self.marker._transform = self.marker.get_transform().rotate_deg(angle)
def rotate(self, angle=0):
self.marker._transform = self.marker.get_transform().rotate_deg(angle)
def animate(k):
angle = ... # new angle
myPltMarker.rotate(angle)
animate.Scatter.remove()
animate.Scatter = plt.scatter(1, 0, marker=myPltMarker.marker, s=100)
return animate.Scatter,
angle = ...
myPltMarker = pltMarker(angle=angle)
animatePlt.Scatter = plt.scatter(1, 0, marker=myPltMarker.marker, s=100)
anm = animation.FuncAnimation(fig, animate, blit=False, interval=1)
plt.show()

Matplotlib: different scale on negative side of the axis

Background
I am trying to show three variables on a single plot. I have connected the three points using lines of different colours based on some other variables. This is shown here
Problem
What I want to do is to have a different scale on the negative x-axis. This would help me in providing positive x_ticks, different axis label and also clear and uncluttered representation of the lines on left side of the image
Question
How to have a different positive x-axis starting from 0 towards negative direction?
Have xticks based on data plotted in that direction
Have a separate xlabel for this new axis
Additional information
I have checked other questions regarding inclusion of multiple axes e.g. this and this. However, these questions did not serve the purpose.
Code Used
font_size = 20
plt.rcParams.update({'font.size': font_size})
fig = plt.figure()
ax = fig.add_subplot(111)
#read my_data from file or create it
for case in my_data:
#Iterating over my_data
if condition1 == True:
local_linestyle = '-'
local_color = 'r'
local_line_alpha = 0.6
elif condition2 == 1:
local_linestyle = '-'
local_color = 'b'
local_line_alpha = 0.6
else:
local_linestyle = '--'
local_color = 'g'
local_line_alpha = 0.6
datapoint = [case[0], case[1], case[2]]
plt.plot(datapoint[0], 0, color=local_color)
plt.plot(-datapoint[2], 0, color=local_color)
plt.plot(0, datapoint[1], color=local_color)
plt.plot([datapoint[0], 0], [0, datapoint[1]], linestyle=local_linestyle, color=local_color)
plt.plot([-datapoint[2], 0], [0, datapoint[1]], linestyle=local_linestyle, color=local_color)
plt.show()
exit()
You can define a custom scale, where values below zero are scaled differently than those above zero.
import numpy as np
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
from matplotlib.ticker import FuncFormatter
class AsymScale(mscale.ScaleBase):
name = 'asym'
def __init__(self, axis, **kwargs):
mscale.ScaleBase.__init__(self)
self.a = kwargs.get("a", 1)
def get_transform(self):
return self.AsymTrans(self.a)
def set_default_locators_and_formatters(self, axis):
# possibly, set a different locator and formatter here.
fmt = lambda x,pos: "{}".format(np.abs(x))
axis.set_major_formatter(FuncFormatter(fmt))
class AsymTrans(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
def __init__(self, a):
mtransforms.Transform.__init__(self)
self.a = a
def transform_non_affine(self, x):
return (x >= 0)*x + (x < 0)*x*self.a
def inverted(self):
return AsymScale.InvertedAsymTrans(self.a)
class InvertedAsymTrans(AsymTrans):
def transform_non_affine(self, x):
return (x >= 0)*x + (x < 0)*x/self.a
def inverted(self):
return AsymScale.AsymTrans(self.a)
Using this you would provide a scale parameter a that scales the negative part of the axes.
# Now that the Scale class has been defined, it must be registered so
# that ``matplotlib`` can find it.
mscale.register_scale(AsymScale)
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([-2, 0, 5], [0,1,0])
ax.set_xscale("asym", a=2)
ax.annotate("negative axis", xy=(.25,0), xytext=(0,-30),
xycoords = "axes fraction", textcoords="offset points", ha="center")
ax.annotate("positive axis", xy=(.75,0), xytext=(0,-30),
xycoords = "axes fraction", textcoords="offset points", ha="center")
plt.show()
The question is not very clear about what xticks and labels are desired, so I left that out for now.
Here's how to get what you want. This solution uses two twined axes object to get different scaling to the left and right of the origin, and then hides all the evidence:
import matplotlib.pyplot as plt
import matplotlib as mpl
from numbers import Number
tickkwargs = {m+k:False for k in ('bottom','top','left','right') for m in ('','label')}
p = np.zeros((10, 3, 2))
p[:,0,0] -= np.arange(10)*.1 + .5
p[:,1,1] += np.repeat(np.arange(5), 2)*.1 + .3
p[:,2,0] += np.arange(10)*.5 + 2
fig = plt.figure(figsize=(8,6))
host = fig.add_subplot(111)
par = host.twiny()
host.set_xlim(-6, 6)
par.set_xlim(-1, 1)
for ps in p:
# mask the points with negative x values
ppos = ps[ps[:,0] >= 0].T
host.plot(*ppos)
# mask the points with positive x values
pneg = ps[ps[:,0] <= 0].T
par.plot(*pneg)
# hide all possible ticks/notation text that could be set by the second x axis
par.tick_params(axis="both", **tickkwargs)
par.xaxis.get_offset_text().set_visible(False)
# fix the x tick labels so they're all positive
host.set_xticklabels(np.abs(host.get_xticks()))
fig.show()
Output:
Here's what the set of points p I used in the code above look like when plotted normally:
fig = plt.figure(figsize=(8,6))
ax = fig.gca()
for ps in p:
ax.plot(*ps.T)
fig.show()
Output:
The method of deriving a class of mscale.ScaleBase as shown in other answers may be too complicated for your purpose.
You can pass two scale transform functions to set_xscale or set_yscale, something like the following.
def get_scale(a=1): # a is the scale of your negative axis
def forward(x):
x = (x >= 0) * x + (x < 0) * x * a
return x
def inverse(x):
x = (x >= 0) * x + (x < 0) * x / a
return x
return forward, inverse
fig, ax = plt.subplots()
forward, inverse = get_scale(a=3)
ax.set_xscale('function', functions=(forward, inverse)) # this is for setting x axis
# do plotting
More examples can be found in this doc.

contour lines from the edge of a map don't show up on basemap

I'm drawing several contour lines over a basemap projection as shown in the following figure:.
There are 3 contours that are not drawn completely (in Oregon, Washington and California) and seems like there is this line that has cut all 3 of them in the same latitude. I'm not sure how to solve this problem.
I added the number of interpolation points, didn't help. changed the ll and ur points to include more area didn't help.
The code is below (not reproducible but might help):
def visualise_bigaus(mus, sigmas, corxys , output_type='pdf', **kwargs):
lllat = 24.396308
lllon = -124.848974
urlat = 49.384358
urlon = -66.885444
fig = plt.figure(figsize=(4, 2.5))
ax = fig.add_subplot(111, axisbg='w', frame_on=False)
m = Basemap(llcrnrlat=lllat,
urcrnrlat=urlat,
llcrnrlon=lllon,
urcrnrlon=urlon,
resolution='i', projection='cyl')
m.drawmapboundary(fill_color = 'white')
#m.drawcoastlines(linewidth=0.2)
m.drawcountries(linewidth=0.2)
m.drawstates(linewidth=0.2, color='lightgray')
#m.fillcontinents(color='white', lake_color='#0000ff', zorder=2)
#m.drawrivers(color='#0000ff')
m.drawlsmask(land_color='gray',ocean_color="#b0c4de", lakes=True)
lllon, lllat = m(lllon, lllat)
urlon, urlat = m(urlon, urlat)
mlon, mlat = m(*(mus[:,1], mus[:,0]))
numcols, numrows = 1000, 1000
X = np.linspace(mlon.min(), urlon, numcols)
Y = np.linspace(lllat, urlat, numrows)
X, Y = np.meshgrid(X, Y)
m.scatter(mlon, mlat, s=0.2, c='red')
shp_info = m.readshapefile('./data/us_states_st99/st99_d00','states',drawbounds=True, zorder=0)
printed_names = []
ax = plt.gca()
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
for spine in ax.spines.itervalues():
spine.set_visible(False)
for k in xrange(mus.shape[0]):
#here x is longitude and y is latitude
#apply softplus to sigmas (to make them positive)
sigmax=np.log(1 + np.exp(sigmas[k][1]))
sigmay=np.log(1 + np.exp(sigmas[k][0]))
mux=mlon[k]
muy=mlat[k]
corxy = corxys[k]
#apply the soft sign
corxy = corxy / (1 + np.abs(corxy))
#now given corxy find sigmaxy
sigmaxy = corxy * sigmax * sigmay
#corxy = 1.0 / (1 + np.abs(sigmaxy))
Z = mlab.bivariate_normal(X, Y, sigmax=sigmax, sigmay=sigmay, mux=mux, muy=muy, sigmaxy=sigmaxy)
#Z = maskoceans(X, Y, Z)
con = m.contour(X, Y, Z, levels=[0.02], linewidths=0.5, colors='darkorange', antialiased=True)
'''
num_levels = len(con.collections)
if num_levels > 1:
for i in range(0, num_levels):
if i != (num_levels-1):
con.collections[i].set_visible(False)
'''
contour_labels = False
if contour_labels:
plt.clabel(con, [con.levels[-1]], inline=True, fontsize=10)
'''
world_shp_info = m.readshapefile('./data/CNTR_2014_10M_SH/Data/CNTR_RG_10M_2014','world',drawbounds=False, zorder=100)
for shapedict,state in zip(m.world_info, m.world):
if shapedict['CNTR_ID'] not in ['CA', 'MX']: continue
poly = MplPolygon(state,facecolor='gray',edgecolor='gray')
ax.add_patch(poly)
'''
if iter:
iter = str(iter).zfill(3)
else:
iter = ''
plt.tight_layout()
plt.savefig('./maps/video/gaus_' + iter + '.' + output_type, frameon=False, dpi=200)
The problem is the meshgrid not covering the complete map. The meshgrid simply doesn't have any points at the positions where you want to draw the gaussian contour line.
An example to reproduce this behaviour is the following, where the meshgrid in x directio starts at -1, such that points lower than that are not drawn.
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
import numpy as np
fig, ax=plt.subplots()
ax.plot([-2,2],[-2,-2], alpha=0)
X,Y = np.meshgrid(np.linspace(-1,2),np.linspace(-2,2))
Z = mlab.bivariate_normal(X, Y, sigmax=1., sigmay=1., mux=0.1, muy=0.1, sigmaxy=0)
con = ax.contour(X, Y, Z, levels=[Z.max()/3, Z.max()/2., Z.max()*0.8],colors='darkorange')
plt.show()
A similar problem occurs in the code from the question.
While in Y direction, you use the complete map, Y = np.linspace(lllat, urlat, numrows), in X direction you restrict the mesh to start at mlon.min(),
X = np.linspace(mlon.min(), urlon, numcols)
The solution would of course be not to start the mesh in Portland, but somewhere in the ocean, i.e. at the edge of the shown map.

Matplotlib axis with two scales shared origin

I need two overlay two datasets with different Y-axis scales in Matplotlib. The data contains both positive and negative values. I want the two axes to share one origin, but Matplotlib does not align the two scales by default.
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax1.bar(range(6), (2, -2, 1, 0, 0, 0))
ax2.plot(range(6), (0, 2, 8, -2, 0, 0))
plt.show()
I suppose it is possible to perform some computation with .get_ylim() and .set_ylim() two align the two scales. Is there an easier solution?
use the align_yaxis() function:
import numpy as np
import matplotlib.pyplot as plt
def align_yaxis(ax1, v1, ax2, v2):
"""adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
_, y1 = ax1.transData.transform((0, v1))
_, y2 = ax2.transData.transform((0, v2))
inv = ax2.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2))
miny, maxy = ax2.get_ylim()
ax2.set_ylim(miny+dy, maxy+dy)
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax1.bar(range(6), (2, -2, 1, 0, 0, 0))
ax2.plot(range(6), (0, 2, 8, -2, 0, 0))
align_yaxis(ax1, 0, ax2, 0)
plt.show()
In order to ensure that the y-bounds are maintained (so no data points are shifted off the plot), and to balance adjustment of both y-axes, I made some additions to #HYRY's answer:
def align_yaxis(ax1, v1, ax2, v2):
"""adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
_, y1 = ax1.transData.transform((0, v1))
_, y2 = ax2.transData.transform((0, v2))
adjust_yaxis(ax2,(y1-y2)/2,v2)
adjust_yaxis(ax1,(y2-y1)/2,v1)
def adjust_yaxis(ax,ydif,v):
"""shift axis ax by ydiff, maintaining point v at the same location"""
inv = ax.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
miny, maxy = ax.get_ylim()
miny, maxy = miny - v, maxy - v
if -miny>maxy or (-miny==maxy and dy > 0):
nminy = miny
nmaxy = miny*(maxy+dy)/(miny+dy)
else:
nmaxy = maxy
nminy = maxy*(miny+dy)/(maxy+dy)
ax.set_ylim(nminy+v, nmaxy+v)
#drevicko's answer fails for me when plotting the following two sequences of points:
l1 = [0.03, -0.6, 1, 0.05]
l2 = [0.8, 0.9, 1, 1.1]
fig, ax1 = plt.subplots()
ax1.plot(l1)
ax2 = ax1.twinx()
ax2.plot(l2, color='r')
align_yaxis(ax1, 0, ax2, 0)
... so here's my version:
def align_yaxis(ax1, ax2):
"""Align zeros of the two axes, zooming them out by same ratio"""
axes = (ax1, ax2)
extrema = [ax.get_ylim() for ax in axes]
tops = [extr[1] / (extr[1] - extr[0]) for extr in extrema]
# Ensure that plots (intervals) are ordered bottom to top:
if tops[0] > tops[1]:
axes, extrema, tops = [list(reversed(l)) for l in (axes, extrema, tops)]
# How much would the plot overflow if we kept current zoom levels?
tot_span = tops[1] + 1 - tops[0]
b_new_t = extrema[0][0] + tot_span * (extrema[0][1] - extrema[0][0])
t_new_b = extrema[1][1] - tot_span * (extrema[1][1] - extrema[1][0])
axes[0].set_ylim(extrema[0][0], b_new_t)
axes[1].set_ylim(t_new_b, extrema[1][1])
There are in principle infinite different possibilities to align the zeros (or other values, which the other provided solutions accept): wherever you place zero on the y axis, you can zoom each of the two series so that it fits. We just pick the position such that, after the transformation, the two cover a vertical interval of same height.
Or in other terms, we minimize them of a same factor compared to the non-aligned plot.
(This does not mean that 0 is at half of the plot: this will happen e.g. if one plot is all negative and the other all positive.)
Numpy version:
def align_yaxis_np(ax1, ax2):
"""Align zeros of the two axes, zooming them out by same ratio"""
axes = np.array([ax1, ax2])
extrema = np.array([ax.get_ylim() for ax in axes])
tops = extrema[:,1] / (extrema[:,1] - extrema[:,0])
# Ensure that plots (intervals) are ordered bottom to top:
if tops[0] > tops[1]:
axes, extrema, tops = [a[::-1] for a in (axes, extrema, tops)]
# How much would the plot overflow if we kept current zoom levels?
tot_span = tops[1] + 1 - tops[0]
extrema[0,1] = extrema[0,0] + tot_span * (extrema[0,1] - extrema[0,0])
extrema[1,0] = extrema[1,1] + tot_span * (extrema[1,0] - extrema[1,1])
[axes[i].set_ylim(*extrema[i]) for i in range(2)]
The other answers here seem overly complicated and don't necessarily work for all the scenarios (e.g. ax1 is all negative and ax2 is all positive). There are 2 easy methods that always work:
Always put 0 in the middle of the graph for both y axes
A bit fancy and somewhat preserves the positive-to-negative ratio, see below
def align_yaxis(ax1, ax2):
y_lims = numpy.array([ax.get_ylim() for ax in [ax1, ax2]])
# force 0 to appear on both axes, comment if don't need
y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
y_lims[:, 1] = y_lims[:, 1].clip(0, None)
# normalize both axes
y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
y_lims_normalized = y_lims / y_mags
# find combined range
y_new_lims_normalized = numpy.array([numpy.min(y_lims_normalized), numpy.max(y_lims_normalized)])
# denormalize combined range to get new axes
new_lim1, new_lim2 = y_new_lims_normalized * y_mags
ax1.set_ylim(new_lim1)
ax2.set_ylim(new_lim2)
I've cooked up a solution starting from the above that will align any number of axes:
def align_yaxis_np(axes):
"""Align zeros of the two axes, zooming them out by same ratio"""
axes = np.array(axes)
extrema = np.array([ax.get_ylim() for ax in axes])
# reset for divide by zero issues
for i in range(len(extrema)):
if np.isclose(extrema[i, 0], 0.0):
extrema[i, 0] = -1
if np.isclose(extrema[i, 1], 0.0):
extrema[i, 1] = 1
# upper and lower limits
lowers = extrema[:, 0]
uppers = extrema[:, 1]
# if all pos or all neg, don't scale
all_positive = False
all_negative = False
if lowers.min() > 0.0:
all_positive = True
if uppers.max() < 0.0:
all_negative = True
if all_negative or all_positive:
# don't scale
return
# pick "most centered" axis
res = abs(uppers+lowers)
min_index = np.argmin(res)
# scale positive or negative part
multiplier1 = abs(uppers[min_index]/lowers[min_index])
multiplier2 = abs(lowers[min_index]/uppers[min_index])
for i in range(len(extrema)):
# scale positive or negative part based on which induces valid
if i != min_index:
lower_change = extrema[i, 1] * -1*multiplier2
upper_change = extrema[i, 0] * -1*multiplier1
if upper_change < extrema[i, 1]:
extrema[i, 0] = lower_change
else:
extrema[i, 1] = upper_change
# bump by 10% for a margin
extrema[i, 0] *= 1.1
extrema[i, 1] *= 1.1
# set axes limits
[axes[i].set_ylim(*extrema[i]) for i in range(len(extrema))]
example on 4 random series (you can see the discrete ranges on the 4 separate sets of y axis labels):
#Tim's solution adapted to work for more than two axes:
import numpy as np
def align_yaxis(axes):
y_lims = np.array([ax.get_ylim() for ax in axes])
# force 0 to appear on all axes, comment if don't need
y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
y_lims[:, 1] = y_lims[:, 1].clip(0, None)
# normalize all axes
y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
y_lims_normalized = y_lims / y_mags
# find combined range
y_new_lims_normalized = np.array([np.min(y_lims_normalized), np.max(y_lims_normalized)])
# denormalize combined range to get new axes
new_lims = y_new_lims_normalized * y_mags
for i, ax in enumerate(axes):
ax.set_ylim(new_lims[i])
I needed to align two subplots but not at their zeros. And other solutions didn't quite work for me.
The main code of my program looks like this. The subplots are not aligned. Further I only change align_yaxis function and keep all other code the same.
import matplotlib.pyplot as plt
def align_yaxis(ax1, v1, ax2, v2):
return 0
x = range(10)
y1 = [3.2, 1.3, -0.3, 0.4, 2.3, -0.9, 0.2, 0.1, 1.3, -3.4]
y2, s = [], 100
for i in y1:
s *= 1 + i/100
y2.append(s)
fig = plt.figure()
ax1 = fig.add_subplot()
ax2 = ax1.twinx()
ax1.axhline(y=0, color='k', linestyle='-', linewidth=0.5)
ax1.bar(x, y1, color='tab:blue')
ax2.plot(x, y2, color='tab:red')
fig.tight_layout()
align_yaxis(ax1, 0, ax2, 100)
plt.show()
Picture of not aligned subplots
Using #HYRY's solution I get aligned subplots, but the second subplot is out of the figure. You can't see it.
def align_yaxis(ax1, v1, ax2, v2):
"""adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
_, y1 = ax1.transData.transform((0, v1))
_, y2 = ax2.transData.transform((0, v2))
inv = ax2.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2))
miny, maxy = ax2.get_ylim()
ax2.set_ylim(miny+dy, maxy+dy)
Picture without second subplot
Using #drevicko's solution I also get aligned plot. But now the first subplot is out of the picture and first Y axis is quite weird.
def align_yaxis(ax1, v1, ax2, v2):
"""adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
_, y1 = ax1.transData.transform((0, v1))
_, y2 = ax2.transData.transform((0, v2))
adjust_yaxis(ax2,(y1-y2)/2,v2)
adjust_yaxis(ax1,(y2-y1)/2,v1)
def adjust_yaxis(ax,ydif,v):
"""shift axis ax by ydiff, maintaining point v at the same location"""
inv = ax.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
miny, maxy = ax.get_ylim()
miny, maxy = miny - v, maxy - v
if -miny>maxy or (-miny==maxy and dy > 0):
nminy = miny
nmaxy = miny*(maxy+dy)/(miny+dy)
else:
nmaxy = maxy
nminy = maxy*(miny+dy)/(maxy+dy)
ax.set_ylim(nminy+v, nmaxy+v)
Picture without firstsubplot
So I've tuned #drevicko's solution a little and got what I wanted.
def align_yaxis(ax1, v1, ax2, v2):
"""adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
_, y1 = ax1.transData.transform((0, v1))
_, y2 = ax2.transData.transform((0, v2))
adjust_yaxis(ax1,(y2 - y1)/2,v1)
adjust_yaxis(ax2,(y1 - y2)/2,v2)
def adjust_yaxis(ax,ydif,v):
"""shift axis ax by ydiff, maintaining point v at the same location"""
inv = ax.transData.inverted()
_, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
miny, maxy = ax.get_ylim()
nminy = miny - v + dy - abs(dy)
nmaxy = maxy - v + dy + abs(dy)
ax.set_ylim(nminy+v, nmaxy+v)
Subplots as I've expected them to look
This might not be what you are looking for but this helped me get whole numbers to line up on two different vertical axis:
ax1.set_ylim(0,4000)
ax2.set_ylim(0,120)
ax2.set_yticks(np.linspace(ax2.get_yticks()[0], ax2.get_yticks()[-1], len(ax1.get_yticks())))