有不同版本的 None 的合理方法?

Reasonable way to have different versions of None?

在 Python3 工作。

假设您有一百万只甲虫,您的任务是对它们斑点的大小进行编目。所以你会制作一个table,每一行是一只甲虫,行中的数字代表斑点的大小;

 [[.3, 1.2, 0.5],
  [.6, .7],
  [1.4, .9, .5, .7],
  [.2, .3, .1, .7, .1]]

此外,您决定将其存储在一个 numpy 数组中,为此您用 None 填充列表(numpy 会将其转换为 np.nan)。

 [[.3, 1.2, 0.5, None, None],
  [.6, .7, None, None, None],
  [1.4, .9, .5, .7, None],
  [.2, .3, .1, .7, .1]]

但是有一个问题,表示为 None 的值可以是 None,原因有 3 个;

  1. 甲虫的斑点不多;该数量不存在。

  2. 甲虫不会静止不动,你无法测量位置。

  3. 您还没来得及测量那只甲虫,所以该值未分配。

我的问题其实不涉及甲虫,但是原理是一样的。 我想要 3 个不同的 None 值,这样我就可以区分这些缺失值的原因。我目前的解决方案是使用一个大到物理上不可能的值,但这不是一个非常安全的解决方案。

假设你不能使用负数——实际上我测量的数量可能是负数。

数据量大,读取速度很重要

编辑;评论正确地指出,说速度很重要而不说什么操作有点毫无意义。主成分分析可能用于变量去相关,聚类算法的欧几里德距离平方计算(但该变量中的数据稀疏)可能用于一些插值。最终是一个递归神经网络,但它将来自一个库,所以我只需要将数据转换为输入形式。所以也许没有比线性代数更糟糕的了,如果我小心的话,它应该都适合 RAM。

什么是好的策略?

如果您只想要一个不是任何已知值且也不是 None 的对象,只需创建一个新对象:

NOT_APPLICABLE = object()
NOT_MEASURED = object()
UNKNOWN = object()

现在您可以像使用 None:

一样使用这些值
[1.4, .9, .5, .7, UNKNOWN]

...

if value is UNKNOWN:
    # do something

等等

如果您需要一个可以表示为 float 的值(例如在 numpy 数组中),您可以使用尾数中编码的 "extra" 数据创建一个 NaN 值.但是,这样做可能 不安全 ,因为不能保证通过对值的各种操作保留这些位。

最简单的方法是使用字符串:'not counted'、'unknown' 和 'N/A'。但是,如果您想在 numpy 中快速处理,混合 numbers/objects 的数组不是您的朋友。

我的建议是添加几个与您的数据形状相同的数组,由 0 和 1 组成。因此数组 missing = 1,其中 spot 缺失,其他为 0,依此类推,与数组 not_measured, 等等..

然后您可以在任何地方使用 NaN,然后​​使用 np.where(missing == 1) 来屏蔽您的数据,以便轻松找到您需要的特定 NaN。

在问题下方的评论中,我问为什么不使用 np.inf-np.infnp.nan,作者回应说这正是他需要的。

所以我添加 post,因为人们更常看回复,而不是评论。

建议为每个案例创建三个不同的 object 实例。

由于您希望这些对象具有 NaN 的属性,您可以尝试创建三个不同的 NaN 实例。

NOT_APPLICABLE = float("nan")
NOT_MEASURED = float("nan")
UNKNOWN = float("nan")

这是黑客攻击的极限,因此使用风险自负,但我不相信任何 Python 实现都会优化 NaN 以始终重用同一对象。尽管如此,您仍然可以在 运行.

之前添加一个标记条件来检查它
if NOT_APPLICABLE is NOT_MEASURED or NOT_MEASURED is UNKNOWN or UNKNOWN is NOT_APPLICABLE :
    raise ValueError # or try something else

如果这可行,它的优点是允许您比较 NaN id 以检查其含义。

row = [1.0, 2.4, UNKNOWN]

...

if value is UNKNOWN:
    ...

同时,它保留了 numpy 可能对其数组进行的任何优化。

披露:这是一个骇人听闻的建议,我很想听听其他人的意见。

这是一个解决方案(免责声明:HACK!),它避免了减速带,例如对象数据类型或单独的掩码:

nan:

的 fp 表示周围似乎有相当多的 "dead space"
>>> nan_as_int = np.array(np.nan).view(int)[()]
>>> nan_as_int
9221120237041090560

>>> custom_nan = np.arange(nan_as_int, nan_as_int+10).view(float)
>>> custom_nan
array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan])

我们创建了十个不同的 nan。请注意,这与使用 float("nan") 创建多个实例不同。这些实例都将映射到 numpy 中的相同值,因此一旦放入非对象数组中就无法区分。

即使我们的十个 nan 具有不同的表示,但在浮点级别上它们很难区分(因为根据定义 nan != nan 即使对于唯一 nan)。所以我们需要一个小帮手:

>>> def which_nan(a):
...     some_nan = np.isnan(a)
...     return np.where(some_nan, np.subtract(a.view(int), nan_as_int, where=some_nan), -1)

示例:

>>> exmpl = np.array([0.1, 1.2, custom_nan[3], custom_nan[0]])
>>> exmpl
array([0.1, 1.2, nan, nan])
>>> which_nan(exmpl)
array([-1, -1,  3,  0], dtype=int64)

也许令人惊讶的是,这似乎至少在一些基本的 numpy 操作中幸存下来:

>>> which_nan(np.sin(exmpl))
array([-1, -1,  3,  0], dtype=int64)