python 中的双 x 轴:相同数据,不同比例
Dual x-axis in python: same data, different scale
我想使用两个不同的 x 轴在 Python 中绘制一些数据。为了便于解释,我会说我想绘制光吸收数据,这意味着我绘制了吸光度与波长 (nm) 或能量 (eV) 的关系图。我想要一个图,其中底轴表示以 nm 为单位的波长,顶轴表示以 eV 为单位的能量。两者不是线性相关的(正如您在下面我的 MWE 中看到的那样)。
我的完整 MWE:
import numpy as np
import matplotlib.pyplot as plt
import scipy.constants as constants
# Converting wavelength (nm) to energy (eV)
def WLtoE(wl):
# E = h*c/wl
h = constants.h # Planck constant
c = constants.c # Speed of light
J_eV = constants.e # Joule-electronvolt relationship
wl_nm = wl * 10**(-9) # convert wl from nm to m
E_J = (h*c) / wl_nm # energy in units of J
E_eV = E_J / J_eV # energy in units of eV
return E_eV
x = np.arange(200,2001,5)
x_mod = WLtoE(x)
y = 2*x + 3
fig, ax1 = plt.subplots()
ax2 = ax1.twiny()
ax1.plot(x, y, color='red')
ax2.plot(x_mod, y, color = 'green')
ax1.set_xlabel('Wavelength (nm)', fontsize = 'large', color='red')
ax1.set_ylabel('Absorbance (a.u.)', fontsize = 'large')
ax1.tick_params(axis='x', colors='red')
ax2.set_xlabel('Energy (eV)', fontsize='large', color='green')
ax2.tick_params(axis='x', colors='green')
ax2.spines['top'].set_color('green')
ax2.spines['bottom'].set_color('red')
plt.tight_layout()
plt.show()
这产生:
现在这已经接近我想要的了,但我想解决以下两个问题:
- 其中一个轴需要反转 - 高波长等于低能量,但图中并非如此。例如,我尝试使用
x_mod = WLtoE(x)[::-1]
,但这并没有解决这个问题。
- 由于轴不是线性相关的,我希望顶部和底部轴“匹配”。例如,现在 1000 nm 对应于 3 eV(或多或少),但实际上 1000 nm 对应于 1.24 eV。因此其中一个轴(最好是底部的波长轴)需要 condensed/expanded 以匹配顶部的正确能量值。换句话说,我希望红色和绿色曲线重合。
我感谢所有帮助我制作精美情节的提示和技巧!提前致谢。
** 编辑 **
DeX97 的回答完美地解决了我的问题,尽管我做了一些小改动,如下所示。我只是对我绘制事物的方式做了一些改变,定义了像 DeX97 这样的函数完美地工作。
编辑绘图代码
fig, ax1 = plt.subplots()
ax1.plot(WLtoE(x), y)
ax1.set_xlabel('Energy (eV)', fontsize = 'large')
ax1.set_ylabel('Absorbance (a.u.)', fontsize = 'large')
# Create the second x-axis on which the wavelength in nm will be displayed
ax2 = ax1.secondary_xaxis('top', functions=(EtoWL, WLtoE))
ax2.set_xlabel('Wavelength (nm)', fontsize='large')
# Invert the wavelength axis
ax2.invert_xaxis()
# Get ticks from ax1 (energy)
E_ticks = ax1.get_xticks()
E_ticks = preventDivisionByZero(E_ticks)
# Make own array of wavelength ticks, so they are round numbers
# The values are not linearly spaced, but that is the idea.
wl_ticks = np.asarray([200, 250, 300, 350, 400, 500, 600, 750, 1000, 2000])
# Set the ticks for ax2 (wl)
ax2.set_xticks(wl_ticks)
# Make the values on ax2 (wavelength) integer values
ax2.xaxis.set_major_formatter(FormatStrFormatter('%i'))
plt.tight_layout()
plt.show()
在您的代码示例中,您绘制了两次相同的数据(尽管使用 E=h*c/wl
进行了转换)。我认为只绘制一次数据就足够了,但要创建两个 x 轴:一个以 nm 为单位显示波长,一个以 eV 为单位显示相应的能量。
考虑以下调整后的代码:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FormatStrFormatter
import scipy.constants as constants
from sys import float_info
# Function to prevent zero values in an array
def preventDivisionByZero(some_array):
corrected_array = some_array.copy()
for i, entry in enumerate(some_array):
# If element is zero, set to some small value
if abs(entry) < float_info.epsilon:
corrected_array[i] = float_info.epsilon
return corrected_array
# Converting wavelength (nm) to energy (eV)
def WLtoE(wl):
# Prevent division by zero error
wl = preventDivisionByZero(wl)
# E = h*c/wl
h = constants.h # Planck constant
c = constants.c # Speed of light
J_eV = constants.e # Joule-electronvolt relationship
wl_nm = wl * 10**(-9) # convert wl from nm to m
E_J = (h*c) / wl_nm # energy in units of J
E_eV = E_J / J_eV # energy in units of eV
return E_eV
# Converting energy (eV) to wavelength (nm)
def EtoWL(E):
# Prevent division by zero error
E = preventDivisionByZero(E)
# Calculates the wavelength in nm
return constants.h * constants.c / (constants.e * E) * 10**9
x = np.arange(200,2001,5)
y = 2*x + 3
fig, ax1 = plt.subplots()
ax1.plot(x, y, color='black')
ax1.set_xlabel('Wavelength (nm)', fontsize = 'large')
ax1.set_ylabel('Absorbance (a.u.)', fontsize = 'large')
# Invert the wavelength axis
ax1.invert_xaxis()
# Create the second x-axis on which the energy in eV will be displayed
ax2 = ax1.secondary_xaxis('top', functions=(WLtoE, EtoWL))
ax2.set_xlabel('Energy (eV)', fontsize='large')
# Get ticks from ax1 (wavelengths)
wl_ticks = ax1.get_xticks()
wl_ticks = preventDivisionByZero(wl_ticks)
# Based on the ticks from ax1 (wavelengths), calculate the corresponding
# energies in eV
E_ticks = WLtoE(wl_ticks)
# Set the ticks for ax2 (Energy)
ax2.set_xticks(E_ticks)
# Allow for two decimal places on ax2 (Energy)
ax2.xaxis.set_major_formatter(FormatStrFormatter('%.2f'))
plt.tight_layout()
plt.show()
首先,我定义preventDivisionByZero
效用函数。此函数将数组作为输入并检查(近似)等于零的值。随后,它将用 而不是 等于零的小数字 (sys.float_info.epsilon
) 替换这些值。这个函数将在几个地方使用,以防止被零除。稍后我会回来讨论为什么这很重要。
在这个函数之后,你的WLtoE
函数就定义好了。请注意,我在您的函数顶部添加了 preventDivisionByZero
函数。另外,我定义了一个 EtoWL
函数,它与你的 WLtoE
函数相反。
然后,您生成虚拟数据并将其绘制在 ax1
上,这是 波长 的 x 轴。设置一些标签后,ax1
被反转(正如您在原始 post 中所要求的)。
现在,我们使用 ax2 = ax1.secondary_xaxis('top', functions=(WLtoE, EtoWL))
为 energy 创建第二个轴。第一个参数表示轴应该放在图的顶部。第二个(关键字)参数被赋予一个包含两个函数的元组:第一个函数是正向变换,而第二个函数是反向变换。有关详细信息,请参阅 Axes.secondary_axis
。请注意,matplotlib 会在必要时将值传递给这两个函数。由于这些值可以等于零,因此处理这些情况很重要。因此, preventDivisionByZero
功能!创建第二个轴后,设置标签。
现在我们有两个 x 轴,但是两个轴上的刻度位于不同的位置。为此,我们将 波长 x 轴的刻度位置存储在 wl_ticks
中 'solve'。使用 preventDivisionByZero
函数确保没有零元素后,我们使用 WLtoE
函数计算相应的能量值。这些对应的能量值存储在E_ticks
中。现在我们只需使用 ax2.set_xticks(E_ticks)
.
将第二个 x 轴的刻度位置设置为等于 E_ticks
中的值
为了在第二个 x 轴(能量)上保留两位小数,我们使用 ax2.xaxis.set_major_formatter(FormatStrFormatter('%.2f'))
。当然,您可以自己选择想要的小数位数。
上面给出的代码生成了下图:
我想使用两个不同的 x 轴在 Python 中绘制一些数据。为了便于解释,我会说我想绘制光吸收数据,这意味着我绘制了吸光度与波长 (nm) 或能量 (eV) 的关系图。我想要一个图,其中底轴表示以 nm 为单位的波长,顶轴表示以 eV 为单位的能量。两者不是线性相关的(正如您在下面我的 MWE 中看到的那样)。
我的完整 MWE:
import numpy as np
import matplotlib.pyplot as plt
import scipy.constants as constants
# Converting wavelength (nm) to energy (eV)
def WLtoE(wl):
# E = h*c/wl
h = constants.h # Planck constant
c = constants.c # Speed of light
J_eV = constants.e # Joule-electronvolt relationship
wl_nm = wl * 10**(-9) # convert wl from nm to m
E_J = (h*c) / wl_nm # energy in units of J
E_eV = E_J / J_eV # energy in units of eV
return E_eV
x = np.arange(200,2001,5)
x_mod = WLtoE(x)
y = 2*x + 3
fig, ax1 = plt.subplots()
ax2 = ax1.twiny()
ax1.plot(x, y, color='red')
ax2.plot(x_mod, y, color = 'green')
ax1.set_xlabel('Wavelength (nm)', fontsize = 'large', color='red')
ax1.set_ylabel('Absorbance (a.u.)', fontsize = 'large')
ax1.tick_params(axis='x', colors='red')
ax2.set_xlabel('Energy (eV)', fontsize='large', color='green')
ax2.tick_params(axis='x', colors='green')
ax2.spines['top'].set_color('green')
ax2.spines['bottom'].set_color('red')
plt.tight_layout()
plt.show()
这产生:
现在这已经接近我想要的了,但我想解决以下两个问题:
- 其中一个轴需要反转 - 高波长等于低能量,但图中并非如此。例如,我尝试使用
x_mod = WLtoE(x)[::-1]
,但这并没有解决这个问题。 - 由于轴不是线性相关的,我希望顶部和底部轴“匹配”。例如,现在 1000 nm 对应于 3 eV(或多或少),但实际上 1000 nm 对应于 1.24 eV。因此其中一个轴(最好是底部的波长轴)需要 condensed/expanded 以匹配顶部的正确能量值。换句话说,我希望红色和绿色曲线重合。
我感谢所有帮助我制作精美情节的提示和技巧!提前致谢。
** 编辑 ** DeX97 的回答完美地解决了我的问题,尽管我做了一些小改动,如下所示。我只是对我绘制事物的方式做了一些改变,定义了像 DeX97 这样的函数完美地工作。
编辑绘图代码
fig, ax1 = plt.subplots()
ax1.plot(WLtoE(x), y)
ax1.set_xlabel('Energy (eV)', fontsize = 'large')
ax1.set_ylabel('Absorbance (a.u.)', fontsize = 'large')
# Create the second x-axis on which the wavelength in nm will be displayed
ax2 = ax1.secondary_xaxis('top', functions=(EtoWL, WLtoE))
ax2.set_xlabel('Wavelength (nm)', fontsize='large')
# Invert the wavelength axis
ax2.invert_xaxis()
# Get ticks from ax1 (energy)
E_ticks = ax1.get_xticks()
E_ticks = preventDivisionByZero(E_ticks)
# Make own array of wavelength ticks, so they are round numbers
# The values are not linearly spaced, but that is the idea.
wl_ticks = np.asarray([200, 250, 300, 350, 400, 500, 600, 750, 1000, 2000])
# Set the ticks for ax2 (wl)
ax2.set_xticks(wl_ticks)
# Make the values on ax2 (wavelength) integer values
ax2.xaxis.set_major_formatter(FormatStrFormatter('%i'))
plt.tight_layout()
plt.show()
在您的代码示例中,您绘制了两次相同的数据(尽管使用 E=h*c/wl
进行了转换)。我认为只绘制一次数据就足够了,但要创建两个 x 轴:一个以 nm 为单位显示波长,一个以 eV 为单位显示相应的能量。
考虑以下调整后的代码:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FormatStrFormatter
import scipy.constants as constants
from sys import float_info
# Function to prevent zero values in an array
def preventDivisionByZero(some_array):
corrected_array = some_array.copy()
for i, entry in enumerate(some_array):
# If element is zero, set to some small value
if abs(entry) < float_info.epsilon:
corrected_array[i] = float_info.epsilon
return corrected_array
# Converting wavelength (nm) to energy (eV)
def WLtoE(wl):
# Prevent division by zero error
wl = preventDivisionByZero(wl)
# E = h*c/wl
h = constants.h # Planck constant
c = constants.c # Speed of light
J_eV = constants.e # Joule-electronvolt relationship
wl_nm = wl * 10**(-9) # convert wl from nm to m
E_J = (h*c) / wl_nm # energy in units of J
E_eV = E_J / J_eV # energy in units of eV
return E_eV
# Converting energy (eV) to wavelength (nm)
def EtoWL(E):
# Prevent division by zero error
E = preventDivisionByZero(E)
# Calculates the wavelength in nm
return constants.h * constants.c / (constants.e * E) * 10**9
x = np.arange(200,2001,5)
y = 2*x + 3
fig, ax1 = plt.subplots()
ax1.plot(x, y, color='black')
ax1.set_xlabel('Wavelength (nm)', fontsize = 'large')
ax1.set_ylabel('Absorbance (a.u.)', fontsize = 'large')
# Invert the wavelength axis
ax1.invert_xaxis()
# Create the second x-axis on which the energy in eV will be displayed
ax2 = ax1.secondary_xaxis('top', functions=(WLtoE, EtoWL))
ax2.set_xlabel('Energy (eV)', fontsize='large')
# Get ticks from ax1 (wavelengths)
wl_ticks = ax1.get_xticks()
wl_ticks = preventDivisionByZero(wl_ticks)
# Based on the ticks from ax1 (wavelengths), calculate the corresponding
# energies in eV
E_ticks = WLtoE(wl_ticks)
# Set the ticks for ax2 (Energy)
ax2.set_xticks(E_ticks)
# Allow for two decimal places on ax2 (Energy)
ax2.xaxis.set_major_formatter(FormatStrFormatter('%.2f'))
plt.tight_layout()
plt.show()
首先,我定义preventDivisionByZero
效用函数。此函数将数组作为输入并检查(近似)等于零的值。随后,它将用 而不是 等于零的小数字 (sys.float_info.epsilon
) 替换这些值。这个函数将在几个地方使用,以防止被零除。稍后我会回来讨论为什么这很重要。
在这个函数之后,你的WLtoE
函数就定义好了。请注意,我在您的函数顶部添加了 preventDivisionByZero
函数。另外,我定义了一个 EtoWL
函数,它与你的 WLtoE
函数相反。
然后,您生成虚拟数据并将其绘制在 ax1
上,这是 波长 的 x 轴。设置一些标签后,ax1
被反转(正如您在原始 post 中所要求的)。
现在,我们使用 ax2 = ax1.secondary_xaxis('top', functions=(WLtoE, EtoWL))
为 energy 创建第二个轴。第一个参数表示轴应该放在图的顶部。第二个(关键字)参数被赋予一个包含两个函数的元组:第一个函数是正向变换,而第二个函数是反向变换。有关详细信息,请参阅 Axes.secondary_axis
。请注意,matplotlib 会在必要时将值传递给这两个函数。由于这些值可以等于零,因此处理这些情况很重要。因此, preventDivisionByZero
功能!创建第二个轴后,设置标签。
现在我们有两个 x 轴,但是两个轴上的刻度位于不同的位置。为此,我们将 波长 x 轴的刻度位置存储在 wl_ticks
中 'solve'。使用 preventDivisionByZero
函数确保没有零元素后,我们使用 WLtoE
函数计算相应的能量值。这些对应的能量值存储在E_ticks
中。现在我们只需使用 ax2.set_xticks(E_ticks)
.
E_ticks
中的值
为了在第二个 x 轴(能量)上保留两位小数,我们使用 ax2.xaxis.set_major_formatter(FormatStrFormatter('%.2f'))
。当然,您可以自己选择想要的小数位数。
上面给出的代码生成了下图: