Log scales with Seaborn kdeplot - python-3.x

I am trying to make a nice free energy surface (heat map) using Seaborn's kdeplot.
I am very close but can not figure out a way to change the color bar scale. The color bar scale is important since it is supposed to represent the difference in energy at different coordinates on the map. I need to know how to scale the values of the color bar by -(0.5961573)*log(x), where x is the values of the color bar. I may also then need to normalize the color bar from there so that the max value is 0.
Here is what I currently have:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.pyplot as plt
import seaborn as sns
rs=[]
dihes=[]
with open(sys.argv[1], 'r') as f:
for line in f:
time,r,dihe = line.split()
rs.append(float(r))
dihes.append(float(dihe))
sns.set_style("white")
sns.kdeplot(rs, dihes, n_levels=25, cbar=True, cmap="Purples_d")
plt.show()
This gets me:
The arrays rs and dihes are simple one dimensional arrays.
Any suggestions on how to scale the color bar (z-axis) would be very helpful!

One way to do it is to create the graph manually and then modify the labels directly. This involves a couple more lines of code. You may have to tweak the formatting a bit but something like this should get you on the right track.
The following is adapted from this answer and this answer.
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
rs=[]
dihes=[]
with open(sys.argv[1], 'r') as f:
for line in f:
time,r,dihe = line.split()
rs.append(float(r))
dihes.append(float(dihe))
x = rs
y = dihes
kde = stats.gaussian_kde([x, y])
xx, yy = np.mgrid[min(x):max(x):(max(x)-min(x))/100, min(y):max(y):(max(y)-min(y))/100]
density = kde(np.c_[xx.flat, yy.flat].T).reshape(xx.shape)
sns.set_style("white")
fig, ax = plt.subplots()
cset = ax.contour(xx, yy, density, 25, cmap="Purples_r")
cb = fig.colorbar(cset)
cb.ax.set_yticklabels(map(lambda x: -0.5961573*np.log(float(x.get_text())),
cb.ax.get_yticklabels()))

A bit late to the party, but I ended up putting together this context manager which switches plotted density values to a logarithmic scale:
import contextlib
import seaborn as sns
#contextlib.contextmanager
def plot_kde_as_log(base=np.exp(1), support_threshold=1e-4):
"""Context manager to render density estimates on a logarithmic scale.
Usage:
with plot_kde_as_log():
sns.jointplot(x='x', y='y', data=df, kind='kde')
"""
old_stats = sns.distributions._has_statsmodels
old_univar = sns.distributions._scipy_univariate_kde
old_bivar = sns.distributions._scipy_bivariate_kde
sns.distributions._has_statsmodels = False
def log_clip_fn(v):
v = np.log(np.clip(v, support_threshold, np.inf))
v -= np.log(support_threshold)
v /= np.log(base)
return v
def new_univar(*args, **kwargs):
x, y = old_univar(*args, **kwargs)
y = log_clip_fn(y)
return x, y
def new_bivar(*args, **kwargs):
x, y, z = old_bivar(*args, **kwargs)
z = log_clip_fn(z)
return x, y, z
sns.distributions._scipy_univariate_kde = new_univar
sns.distributions._scipy_bivariate_kde = new_bivar
try:
yield
finally:
sns.distributions._has_statsmodels = old_stats
sns.distributions._scipy_univariate_kde = old_univar
sns.distributions._scipy_bivariate_kde = old_bivar
The benefit of this approach is that it keeps all of the styling and other options of sns.jointplot() without any additional effort.

I updated Walt W's context manager to work with newer versions of seaborn
#contextlib.contextmanager
def plot_kde_as_log(base=np.exp(1), support_threshold=1e-4):
"""Context manager to render density estimates on a logarithmic scale.
Usage:
with plot_kde_as_log():
sns.jointplot(x='x', y='y', data=df, kind='kde')
"""
old_call = sns._statistics.KDE.__call__
def log_clip_fn(v):
v = np.log(np.clip(v, support_threshold, np.inf))
v -= np.log(support_threshold)
v /= np.log(base)
return v
def new_call(*args, **kwargs):
density, support = old_call(*args, **kwargs)
density = log_clip_fn(density)
return density, support
sns._statistics.KDE.__call__ = new_call
try:
yield
finally:
sns._statistics.KDE.__call__ = old_call

Related

Pyvista surface plot?

I need a way to make a 3-dimensional surface plot using millions of datapoints, so I began checking into pyvista which is supposed to do this well.
However, pyvista is a bit difficult for me to grasp.
I have x,y,z data where x is time, y is different measurements, and z is the values for those measurements.
All I want is for pyvista to show me a surface plot with this information.
For example, if I use this array in matplotlib or other libraries with surface plots:
X = np.array([1,2,3,4,5,6,7,8,9])
Y = np.array([1,2,3,4,5,6,7,8,9])
X, Y = np.meshgrid(X, Y)
Z = X*Y
I get this output:
But if I use the same data on any of the pyvista plots, I get something like this:
import sys
# Setting the Qt bindings for QtPy
import os
os.environ["QT_API"] = "pyqt5"
from qtpy import QtWidgets
from qtpy.QtWidgets import QMainWindow
import numpy as np
import pyvista as pv
from pyvistaqt import QtInteractor
import pandas as pd
class MainWindow(QMainWindow):
def __init__(self, parent=None, show=True):
QtWidgets.QMainWindow.__init__(self, parent)
# create the frame
self.frame = QtWidgets.QFrame()
vlayout = QtWidgets.QVBoxLayout()
# add the pyvista interactor object
self.plotter = QtInteractor(self.frame)
vlayout.addWidget(self.plotter.interactor)
self.frame.setLayout(vlayout)
self.setCentralWidget(self.frame)
# simple menu to demo functions
mainMenu = self.menuBar()
fileMenu = mainMenu.addMenu('File')
exitButton = QtWidgets.QAction('Exit', self)
exitButton.setShortcut('Ctrl+Q')
exitButton.triggered.connect(self.close)
fileMenu.addAction(exitButton)
# allow adding a sphere
meshMenu = mainMenu.addMenu('Mesh')
self.add_sphere_action = QtWidgets.QAction('Add Sphere', self)
self.add_sphere_action.triggered.connect(self.add_sphere)
meshMenu.addAction(self.add_sphere_action)
x = np.array([9,8,7,6,5,4,3,2,1])
y = np.array([9,8,7,6,5,4,3,2,1])
x, y = np.meshgrid(x, y)
z = x*y
# z[z < -10] = np.nan # get rid of missing data. pyvista needs you to do this
i_res = 2 # display every nth point
j_res = 2 # display every nth point
self.grid = pv.StructuredGrid(x[::i_res, ::j_res], y[::i_res, ::j_res], z[::i_res, ::j_res])
self.z = z
self.x = x
self.y = y
self.plotter.add_mesh(self.grid, scalars=self.grid.points[:, 2], lighting=True, specular=0.5, smooth_shading=True,
show_scalar_bar=True)
if show:
self.show()
def add_sphere(self): #changing resolution, not adding a sphere
i_res = 5 # display every nth point
j_res = 5 # display every nth point
self.grid = pv.StructuredGrid(self.x[::i_res, ::j_res], self.y[::i_res, ::j_res], self.z[::i_res, ::j_res])
self.plotter.update()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())
import pyvista as pv
import numpy as np
# Define a simple Gaussian surface
x = np.array([1,2,3,4,5,6,7,8,9])
y = np.array([1,2,3,4,5,6,7,8,9])
x, y = np.meshgrid(x, y)
z = x*y
# Get the points as a 2D NumPy array (N by 3)
points = np.c_[x.reshape(-1), y.reshape(-1), z.reshape(-1)]
points[0:5, :]
# simply pass the numpy points to the PolyData constructor
cloud = pv.PolyData(points)
cloud.plot(point_size=15)
I managed to get "something" different using this bit of code:
import pandas as pd
import pyvista as pv
import numpy as np
# Load Excel sheet using Pandas
# Note - you may need to `pip install xlrd`
# x = np.array([1,2,3,4,5,6,7,8,9])
# y = np.array([1,2,3,4,5,6,7,8,9])
x = np.array([[1],[2],[3],[4],[5],[6],[7],[8],[9]])
y = np.array([[1],[2],[3],[4],[5],[6],[7],[8],[9]])
# # x, y = np.meshgrid(x, y)
z = x*y
coords = np.hstack((x,y,z))
# Make the structured surface manually
structured = pv.StructuredGrid()
# Set coordinates
structured.points = coords
# Set the dimensions of the structured grid
structured.dimensions = [1, 1, 9]
# Apply an Elevation filter
elevation = structured.elevation()
elevation.plot(show_edges=True, show_grid=True, notebook=False)
But it only provides a single string of data. I haven't been able to get anything else work properly.
Does anyone know why the x,y,z data is doing weird things in pyvista and how I can provide just a normal surface plot? It would be much appreciated, as I am pretty stumped.
Your first version is correct.
PyVista has excellent documentation, part of which is an extensive collection of examples. You need the one that's called Creating a Structured Surface. This ends up being pretty much the same code as what you originally showed:
import pyvista as pv
import numpy as np
# Define a simple linear surface
x = np.array([1,2,3,4,5,6,7,8,9])
y = np.array([1,2,3,4,5,6,7,8,9])
x, y = np.meshgrid(x, y)
z = x*y
# Create and plot structured grid
grid = pv.StructuredGrid(x, y, z)
plotter = pv.Plotter()
plotter.add_mesh(grid, scalars=grid.points[:, -1], show_edges=True,
scalar_bar_args={'vertical': True})
plotter.show_grid()
plotter.show()
Here is the (correct!) output:
The reason why this looks different is that matplotlib isn't a 3d visualization tool (in fact its 3d tooling infamously uses a 2d renderer that leads to weird quirks). PyVista on the other hand is designed to visualize spatially referenced data. If your x goes from 1 to 9 and your z goes from 1 to 81 then why would it squash the z axis? What PyVista shows is the truth if you set a 1:1:1 aspect ratio along each coordinate axis.
If you don't want this, you can mess with scaling yourself:
import pyvista as pv
import numpy as np
# Define a simple linear surface
x = np.array([1,2,3,4,5,6,7,8,9])
y = np.array([1,2,3,4,5,6,7,8,9])
x, y = np.meshgrid(x, y)
z = x*y
# Create and plot structured grid
grid = pv.StructuredGrid(x, y, z)
plotter = pv.Plotter()
plotter.add_mesh(grid, scalars=grid.points[:, -1], show_edges=True,
scalar_bar_args={'vertical': True})
plotter.show_grid()
# scale plot to enforce 1:1:1 aspect ratio
plotter.set_scale(xscale=1, yscale=x.ptp()/y.ptp(), zscale=x.ptp()/z.ptp())
plotter.show()
If you want PyVista to lie about your data, you have to tell it to do so.

Coordinate conversion problem of a FITS file

I have loaded and plotted a FITS file in python.
With the help of a previous post, I have managed to get the conversion of the axis from pixels to celestial coordinates. But I can't manage to get them in milliarcseconds (mas) correctly.
The code is the following
import numpy as np
import matplotlib.pyplot as plt
import astropy.units as u
from astropy.wcs import WCS
from astropy.io import fits
from astropy.utils.data import get_pkg_data_filename
filename = get_pkg_data_filename('hallo.fits')
hdu = fits.open(filename)[0]
wcs = WCS(hdu.header).celestial
wcs.wcs.crval = [0,0]
plt.subplot(projection=wcs)
plt.imshow(hdu.data[0][0], origin='lower')
plt.xlim(200,800)
plt.ylim(200,800)
plt.xlabel('Relative R.A ()')
plt.ylabel('Relative Dec ()')
plt.colorbar()
The output looks like
The y-label is cut for some reason, I do not know.
As it was shown in another post, one could use
wcs.wcs.ctype = [ 'XOFFSET' , 'YOFFSET' ]
to switch it to milliarcsecond, and I get
but the scale is incorrect!.
For instance, 0deg00min00.02sec should be 20 mas and not 0.000002!
Did I miss something here?
Looks like a spectral index map. Nice!
I think the issue might be that FITS implicitly uses degrees for values like CDELT. And they should be converted to mas explicitly for the plot.
The most straightforward way is to multiply CDELT values by 3.6e6 to convert from degrees to mas.
However, there is a more general approach which could be useful if you want to convert to different units at some point:
import astropy.units as u
w.wcs.cdelt = (w.wcs.cdelt * u.deg).to(u.mas)
So it basically says first that the units of CDELT are degrees and then converts them to mas.
The whole workflow is like this:
def make_transform(f):
'''use already read-in FITS file object f to build pixel-to-mas transformation'''
print("Making a transformation out of a FITS header")
w = WCS(f[0].header)
w = w.celestial
w.wcs.crval = [0, 0]
w.wcs.ctype = [ 'XOFFSET' , 'YOFFSET' ]
w.wcs.cunit = ['mas' , 'mas']
w.wcs.cdelt = (w.wcs.cdelt * u.deg).to(u.mas)
print(w.world_axis_units)
return w
def read_fits(file):
'''read fits file into object'''
try:
res = fits.open(file)
return res
except:
return None
def start_plot(i,df=None, w=None, xlim = [None, None], ylim=[None, None]):
'''starts a plot and returns fig,ax .
xlim, ylim - axes limits in mas
'''
# make a transformation
# Using a dataframe
if df is not None:
w = make_transform_df(df)
# using a header
if w is not None:
pass
# not making but using one from the arg list
else:
w = make_transform(i)
# print('In start_plot using the following transformation:\n {}'.format(w))
fig = plt.figure()
if w.naxis == 4:
ax = plt.subplot(projection = w, slices = ('x', 'y', 0 ,0 ))
elif w.naxis == 2:
ax = plt.subplot(projection = w)
# convert xlim, ylim to coordinates of BLC and TRC, perform transformation, then return back to xlim, ylim in pixels
if any(xlim) and any(ylim):
xlim_pix, ylim_pix = limits_mas2pix(xlim, ylim, w)
ax.set_xlim(xlim_pix)
ax.set_ylim(ylim_pix)
fig.add_axes(ax) # note that the axes have to be explicitly added to the figure
return fig, ax
rm = read_fits(file)
wr = make_transform(rm)
fig, ax = start_plot(RM, w=wr, xlim = xlim, ylim = ylim)
Then just plot to the axes ax with imshow or contours or whatever.
Of course, this piece of code could be reduced to meet your particular needs.

Plotting a dot that moves along side a dispersive wave?

How would I go on about plotting a dot that moves along a wave pack/superposition. I saw this on the website and wanted to try for myself.https://blog.soton.ac.uk/soundwaves/further-concepts/2-dispersive-waves/. So I know how to animate a superpositon of two sine waves. But how would I plot a dot that moves along it? I won't post my entire code, but it looks somewhat like this
import matplotlib.pyplot as plt
import numpy as np
N = 1000
x = np.linspace(0,100,N)
wave1 = np.sin(2*x)
wave2 = np.sin(3*x)
sWave = wave1+wave2
plt.plot(x,sWave)
plt.ion()
for t in np.arange(0,400):
sWave.set_ydata(sWave)
plt.draw()
plt.pause(.1)
plt.ioff()
plt.show()
Note that this is just a quick draft of my original code.
You can add a scatter and update its data in a loop by using .set_offsets().
import matplotlib.pyplot as plt
import numpy as np
N = 1000
x = np.linspace(0, 100, N)
wave1 = np.sin(2*x)
wave2 = np.sin(3*x)
sWave = wave1 + wave2
fig, ax = plt.subplots()
ax.plot(x, sWave)
scatter = ax.scatter([], [], facecolor="red") # Initialize an empty scatter.
for t in range(N):
scatter.set_offsets((x[t], sWave[t])) # Modify that scatter's data.
fig.canvas.draw()
plt.pause(.001)

Axis label missing

I am trying to create a 3D plot but I am having trouble with the z-axis label. It simply doesn't appear in the graph. How do I amend this? The code is as follows
# Gamma vs Current step 2
import matplotlib as mpl
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pyplot as plt
h = np.arange(0.1,5.1,0.1)
gamma = np.arange(0.1,5.1,0.1)
sigmaz_hgam = np.array([.009998,.03988,.08878,.15403
,.230769,.312854,.394358,.4708311,.539697879,.6,.6518698
,.696033486,.73345752165,.7651390123,.792,.814845635
,.8343567,.851098499,.865535727,.8780487,.8889486,.89848986
,.906881,.914295,.9208731,.9267338,.93197569,.93668129
,.9409202379,.94475138,.951383,.9542629,.956895,.959309
,.961526,.9635675,.96545144,.9671934,.968807,.97030539
,.9716983,.972995,.974206,.975337,.97639567,.977387,.978318
,.97919266,.98,.9807902])
mu = 1
sigmaz_hgam = mu*sigmaz_hgam
# creates an empty list for current values to be stored in
J1 = []
for i in range(sigmaz_hgam.size):
expec_sz = sigmaz_hgam[i]
J = 4*gamma[i]*(mu-expec_sz)
J1.append(J.real)
#print(J)
This part of the code is what is used to graph out which is where the problem lies
mpl.rcParams['legend.fontsize'] = 10
fig = plt.figure()
ax = fig.gca(projection='3d')
x = h
y = gamma
z = J1
ax.plot(x, y, z, label='Dephasing Model')
ax.legend()
ax.set_xlabel('h', fontsize=10)
ax.set_ylabel('$\gamma$')
ax.yaxis._axinfo['label']['space_factor'] = 3.0
for t in ax.zaxis.get_major_ticks(): t.label.set_fontsize(10)
# disable auto rotation
ax.zaxis.set_rotate_label(False)
ax.set_zlabel('J', fontsize=10, rotation = 0)
plt.show()
On my version of Matplotlib (2.0.2), on a Mac, I see the label (which is there – most of it is just being cropped out of the image in your case).
You could try to reduce the padding between the ticks and the label:
ax.zaxis.labelpad = 0

How to set the view_limits / range of an axis in Matplotlib

I want some space before and after the last tick location on an axis. I would just use axis.set_xlim() for example but this interferes with my (custom) locator and reruns the tick generation. I found and overwritten the view_limits() method of the locator-classes but they don't seem to be called automatically and when called manually they don't have any impact on the resulting plot. I searched the docs and the source but haven't come up with a solution. Am I missing something?
For the greater picture I want to have a locator which gives me some space before and after the ticks and chooses tick points which are a multiple of 'base' like the MultipleLocator but scale the base automatically if the number of ticks exceeds a specified value. If there is another way to achieve this without subclassing a locator I am all ears :).
Here is my example code for the subclassed locator with overwritten view_limits-method:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MaxNLocator
class MyLocator(MaxNLocator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def view_limits(self, dmin, dmax):
bins = self.bin_boundaries(dmin, dmax)
step = bins[1] - bins[0]
result = np.array([bins[0] - step, bins[-1] + step])
print(result)
return result
a = 10.0
b = 99.0
t = np.arange(a, b, 0.1)
s = np.sin(0.1*np.pi*t)*np.exp(-t*0.01)
loc = MyLocator(9)
fig, ax = plt.subplots()
plt.plot(t, s)
ax.xaxis.set_major_locator(loc)
loc.autoscale() # results in [ 0. 110.] but doesnt change the plot
plt.show()
Not sure, if I understood completely what your problem is, but if you only want to add extra space, you can still use MaxNLocator and add that space manually like here:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MaxNLocator
a = 10.0
b = 99.0
t = np.arange(a, b, 0.1)
s = np.sin(0.1*np.pi*t)*np.exp(-t*0.01)
loc = MaxNLocator(9)
fig, ax = plt.subplots()
plt.plot(t, s)
ax.xaxis.set_major_locator(loc)
ticks = ax.get_xticks()
newticks = np.zeros(len(ticks)+2)
newticks[0] = ticks[0]- (ticks[1]-ticks[0])
newticks[-1] = ticks[-1]+ (ticks[1]-ticks[0])
newticks[1:-1] = ticks
ax.set_xticks(newticks)
plt.show()
One slightly hacky solution to avoid ticks close to the plot's edges is the following:
class PaddedMaxNLocator(mp.ticker.MaxNLocator):
def __init__(self, *args, protected_width=0.25, **kwargs):
# `prune` edge ticks that might now become visible
super().__init__(*args, **kwargs, prune='both')
# Clamp to some reasonable range
self.protected_width = min(0.5, protected_width)
def tick_values(self, vmin, vmax):
diff = (vmax - vmin) * self.protected_width / 2
return super().tick_values(vmin + diff, vmax - diff)

Resources