log1p 是 "correct" 对图表进行对数刻度转换的方法吗?

is log1p the "correct" way of doing log scale transformation of charts?

当出于制图目的将数据转换为对数刻度时,使用np.log1pnp.log 相比,它是否打破了任何普通用户的期望?

我正在构建一个具有对数刻度功能的图表软件,想知道在转换数据时是否应该使用 np.lognp.log1p 作为默认选择。

这是一个大大简化的代码示例:

import matplotlib.pyplot as plt
def chart_with_log_scale(x,y):
  ylog = np.log(y) # should I be using np.log1p here instead?
  plt.scatter(x,ylog )

或者对此有不同的看法,matplotlib 在这样的代码中进行日志转换时使用 log1p 还是 log

def chart_with_log_scale2(x,y):
  plt.scatter(x,y)
  ax = plt.gca()
  ax.set_yscale("log")

没有。 log1p(x) 不计算 x 的对数,如果您需要对数刻度,则不应使用。

相反,它计算 log(x+1),但精度更高。这不是你想要的。

when transforming data to log scale for charting purposes, is it more "correct" in some way to always transform using np.log1p than with np.log and does it break any common user expectations?

如果您的目标是计算 log(),那么使用 np.log1p 而不是 np.log 几乎是不正确的。

这里是 y 轴为对数刻度的图示例,β 分布的概率密度函数 = 2 和 = 5:

这里是 log1p 比例的 y 轴的相同函数:

如果我作为研究生试图将其作为 Beta(2,5) PDF 的对数比例图传递,我的导师可能会当场开枪打死我。

(例外:如果您的输入在使用 IEEE 754 binary64 算法的机器上总是大于 253,那么这两个 will 函数很可能会重合。但这只是因为 log(1 + ) 在此类输入上与 log() 相比具有如此低的相对误差——即 |log(1 + ) − log()|/|log()| = |log(⋅(1/ + 1) ) − log()|/log() = log(1 + 1/)/log() < 1/ < 2−53 所以 log(1 + ) 在最坏情况下是四舍五入远离 log() 的错误。)


在评论中,您提出了以下问题:

log1p could be what I want if the values are very close to 0, as it will have better numerical stability than log, right?

函数 log1p 和 log 只是数学函数。 两者都没有比另一个“更好的数值稳定性”: “数值稳定性”甚至不是一个明确定义的概念,当然也不是数学函数。 用于计算数学函数的 算法 可以表现出前向或后向稳定性; 属性 的含义与它要计算的函数有关。 但 log 和 log1p 只是数学函数,不是计算函数的算法,因此前向和后向稳定性不适用。

log1p 的重要性在于 函数 log(1 + ) 条件良好 接近于零,并且经常出现在数值算法或其他函数的代数重排中。 Well-conditioned 意味着如果你在点 ⋅(1 + ) 评估它,而你实际上想在点评估它,那么答案 log(1 + ⋅(1 + )) 是等于 log(1 + )⋅(1 + ) 其中 是一个相当小的倍数,只要 是相当小的。 这是输入 ⋅(1 + ) 来自 的相对误差, 是输出 log(1 + )⋅(1 + ) 来自 log() 的相对误差。

相比之下,函数 log() 在 1 附近 病态 计算 log(⋅(1 + )) 当你想要 log() 接近 1 的某个点,你得到的结果可能是 log()⋅(1 + ) for an arbitrarily bad error ,even if输入错误很小。 例如,假设您要计算 log(1.000000000000001) ≈ 9.999999999999995 × 10−16。 如果您在 Python 程序中编写 np.log(1.000000000000001),十进制常量 1.000000000000001 将四舍五入为最接近的二进制 64 浮点数,因此您实际上将计算 log(fl(1.000000000000001)) = log(1.0000000000000011102230246251565404236316680908203125) ≈ 1.110223024625156 × 10−15.

Although 1.0000000000000011102230246251565404236316680908203125 is a good approximation to 1.000000000000001, with relative error < 10−15, log(1.0000000000000011102230246251565404236316680908203125) is a terrible approximation记录 (1.000000000000001),相对误差 > 11%。 这不是 np.log 的错,np.log 在将正确舍入的结果返回给我们提出的 问题 方面做得非常出色。 这是因为 数学函数 log 在 1 附近是病态的,所以它放大了我们从我们想要询问的输入——不仅放大了,而且放大了万亿倍!

因此,如果您发现自己拥有一个小实数,并且发现自己想知道 log(1 + ) 是什么,那么您应该使用 np.log1p(x) 来回答这个问题问题。 (或者您可能希望根据 log(…) 重新安排计算,以便它使用 log(1 + …) 代替;例如,以计算 logit() = log(/( 1 − )) 对于给定的接近 1/2 的值,您最好将其重写为 log(1 + (1 − 2)/)。) 如果你写 np.log(1 + x) 而不是 np.log1p(x),那么子表达式 1 + x 可能会出现舍入错误,给出 1 ⊕ = fl(1 + ) = (1 + )⋅(1 + )。 尽管舍入误差很小(在 binary64 算法中,可以保证 || ≤ 2−53),但 log function 可能会放大为输出中任意大的错误。

但是如果你已经有了一个数字,即使它接近于零,并且发现自己想要 log(),那么 np.log(y) 将给出 log() 的一个很好的近似值,并且np.log1p(y) 会很糟糕(除非非常大)。 这就是您所处的场景。

np.log1p 能否与在对数刻度上绘制数据相关? 也许,如果你 compute 是并且你希望 plot 是 1 + 对数尺度。 但这种情况的组合——计算 ,并以对数刻度绘制 1 +——不太可能一起有意义:

  • 如果您有充分的理由将计算作为 1 + 的代理,您很可能主要关注接近零的值——否则对表示没有太大好处——因此您很可能正在绘制值1 + 接近 1.
  • 但是如果你绘制的值是 1 + 接近 1,那么就没有理由使用对数刻度,因为你的数据点越接近 1,对数刻度和对数刻度之间的差异就越小线性刻度!

对数刻度 gnuplot

set terminal pngcairo
set output "logscale.png"
set title 'log scale'
set xrange [0:1]
set logscale y
plot x**(2 - 1) * (1 - x)**(5 - 1) notitle

log1p 比例 gnuplot

set terminal pngcairo                            
set output "log1pscale.png"
set title 'log1p scale'
set xrange [0:1]
set yrange [1:1.1]
set logscale y 2
set ytics 1.1**(1/4.0)
plot 1 + x**(2 - 1) * (1 - x)**(5 - 1) notitle