有没有一种高效的方法来检查一个列是否包含混合数据类型?

22
考虑
np.random.seed(0)
s1 = pd.Series([1, 2, 'a', 'b', [1, 2, 3]])
s2 = np.random.randn(len(s1))
s3 = np.random.choice(list('abcd'), len(s1))


df = pd.DataFrame({'A': s1, 'B': s2, 'C': s3})
df
           A         B  C
0          1  1.764052  a
1          2  0.400157  d
2          a  0.978738  c
3          b  2.240893  a
4  [1, 2, 3]  1.867558  a

列"A"具有混合数据类型。 我希望能想出一种快速确定这一点的方法。 这并不像检查type == object那么简单,因为这会将"C"识别为误报。

我可以考虑使用

df.applymap(type).nunique() > 1

A     True
B    False
C    False
dtype: bool

但是在 applymap 上调用 type 会相当缓慢,特别是对于较大的框架。

%timeit df.applymap(type).nunique() > 1
3.95 ms ± 88 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

我们可以做得更好(也许使用NumPy)吗?如果你的论点足够有说服力,我可以接受“不行”。:-)

我们是否能够用更好的方式来完成某件事情(或许利用NumPy)?如果你的理由足够有说服力,那么我能接受“不行”的回答。:-)


不要认为一列可以有不同的数据类型。我的意思是,它可能会在行上有不同的数据类型,但是列本身只有一个数据类型。您可以使用 df.info() 进行检查。 - Herc01
你需要循环遍历所有条目并检查条目本身的类型。如果条目类型集合大于1,则存在混合类型。就像你所做的那样。 - Matthieu Brucher
你遇到的困难是因为pandas更喜欢使用object dtype而不是numpy string dtype。 - hpaulj
是的,那正是问题所在。 - cs95
提前的圣诞礼物?谢谢,伙计,我很高兴! - Paul Panzer
3个回答

21

在 Pandas 中,有一个名为 infer_dtype() 的函数,在这里可能很有用。

这个函数是用 Cython(代码链接)编写的,它返回一个字符串,总结了传递对象中的值。在 Pandas 的内部中经常使用它,因此我们可以合理地期望它已经被设计为高效。

>>> from pandas.api.types import infer_dtype

现在,列A是整数和其他一些类型的混合:

>>> infer_dtype(df.A)
'mixed-integer'

B列的值都是浮点类型:

>>> infer_dtype(df.B)
'floating'

列 C 包含字符串:

>>> infer_dtype(df.B)
'string'

混合值的通用“catchall”类型是简单的“mixed”:

>>> infer_dtype(['a string', pd.Timedelta(10)])
'mixed'

浮点数和整数的混合称为“混合整数浮点数”:

>>> infer_dtype([3.141, 99])
'mixed-integer-float'
为了创建你在问题中描述的函数,一种方法是创建一个可以捕获相关混合情况的函数:
def is_mixed(col):
    return infer_dtype(col) in ['mixed', 'mixed-integer']

那么你就有:

>>> df.apply(is_mixed)
A     True
B    False
C    False
dtype: bool

df.apply(lambda x: 'mixed' in infer_dtype(x)) 这正是我所需要的。虽然速度有点慢,但足够快,值得一试。非常感谢! - cs95
这看起来是一种干净的方法,但如果您添加一个仅包含容器s4 = pd.Series([[1],[1,2],[1,2,3],[1,2,3,4],[1,2,3,4,5]])的第四列,则会失败,将为混合类型检查返回True - Darkonaut
@Darkonaut:没错 - 它只是默认为“mixed”。如果有更有用/可用的返回值,可能值得向pandas开发人员提出问题(例如“container”之类的东西)。 - Alex Riley
我认为这只是针对任何非原始的Python对象。例如,对于object()也是如此。我不明白pandas如何有机会推断自定义类的实例类型。虽然我不熟悉pandas的内部结构。 - Darkonaut
@Darkonaut 我不知道使用自定义类(甚至是标准容器)填充pd.Series有多常见,甚至不知道是否推荐这样做,但也许infer_dtype可以为Python中的所有标准容器返回container,并为自定义类返回custom_class?虽然不完美,但我认为比仅返回“mixed”要好。 - user3471881

8
这里有一种方法,利用Python3中不同类型无法比较这一事实。思路是在数组上运行内置的max函数,这应该相对较快,并且它会短路。
def ismixed(a):
    try:
        max(a)
        return False
    except TypeError as e: # we take this to imply mixed type
        msg, fst, and_, snd = str(e).rsplit(' ', 3)
        assert msg=="'>' not supported between instances of"
        assert and_=="and"
        assert fst!=snd
        return True
    except ValueError as e: # catch empty arrays
        assert str(e)=="max() arg is an empty sequence"
        return False

它不能捕捉混合数字类型,而且不支持比较的对象也可能会导致出错。但是它运行速度相当快,如果我们剥离所有的 pandas 包装:
v = df.values

list(map(is_mixed, v.T))
# [True, False, False]
timeit(lambda: list(map(ismixed, v.T)), number=1000)
# 0.008936170022934675

作比较

timeit(lambda: list(map(infer_dtype, v.T)), number=1000)
# 0.02499613002873957

这不是一个兼容Python2的解决方案,这有点遗憾,但我仍然喜欢你在这里所做的工作!如果DataFrame的大小为len(df) * 10000,它将如何扩展?(我想测试一下,但我不在我的工作站上。) - cs95
@coldspeed 取决于情况。由于它进行短路运算,因此除非该列以一百万个相同类型元素开头并具有一个异常值,否则它应该能够很快检测到混合列。对于非混合数组,它的效果不是很好。例如,对于 np.ones(1000000, "O") 我得到了 40 毫秒,这比基于 infer_dtype 的解决方案慢了 6 倍。 - Paul Panzer
@coldspeed 哎呀!我刚发现了一个 bug。它会高兴地将 np.array([np.inf, 'a']) 分类为非混合类型,因为在看到 inf 后它就短路了。这只 Python 野兽太聪明了。 - Paul Panzer
啊,该死!我想你可以在执行此操作之前使用np.valid(如果我记得名字正确的话)来掩盖数组,但这是额外的复杂性。让我们看看它是否值得。如果DataFrame实际上具有混合类型,则可能不值得考虑,因为这只会使事情变得太慢。尽管如此,还是很感谢你的努力!这几乎完美了。 - cs95
1
@coldspeed发现这个bug其实不是bug。问题出在np.array([np.inf, 'a'])计算结果为array(['inf', 'a'], dtype='<U32')。所以我完全误读了它。在对象数组中似乎没有短路。 - Paul Panzer
我在一个包含NumPy数组的Dataframe上尝试了这个(Python3)(同一列中的类型和形状相同);由于这种类型没有定义max操作,因此max操作失败。 - MRule

3
不确定您需要什么结果,但您可以将“type”映射到“df.values.ravel()”,并创建一个字典,其中列名链接到每个“l”切片的“set”长度比1大的比较。
l = list(map(type, df.values.ravel()))
print ({df.columns[i]:len(set(l[i::df.shape[1]])) > 1 for i in range(df.shape[1])})
{'A': True, 'B': False, 'C': False}

时间:

%timeit df.applymap(type).nunique() > 1
#3.25 ms ± 516 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%%timeit 
l = list(map(type, df.values.ravel()))
{df.columns[i]:len(set(l[i::df.shape[1]])) > 1 for i in range(df.shape[1])}
#100 µs ± 5.08 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

针对较大的数据框,时间的改善效果会较不明显:

dfl = pd.concat([df]*100000,ignore_index=True)

%timeit dfl.applymap(type).nunique() > 1
#519 ms ± 61.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit
l = list(map(type, dfl.values.ravel()))
{dfl.columns[i]:len(set(l[i::dfl.shape[1]])) > 1 for i in range(dfl.shape[1])}
#254 ms ± 33.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

同样的思路,稍微快一点的解决方案:

%timeit { col: len(set(map(type, dfl[col])))>1 for col in dfl.columns}
#124 ms ± 15.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

1
这是一个有趣的方法,但加速并不足够显著,对于我的目的来说没有意义。我会再等一段时间看看是否还有其他的东西出现。感谢您的回答。 - cs95

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接