Pandas: 没有完全复制就无法构建混合dtype DataFrame,建议的解决方案

创建于 2015-01-09  ·  58评论  ·  资料来源: pandas-dev/pandas

经过几个小时的撕扯,我得出的结论是,如果不将所有数据复制进去,就不可能创建一个混合 dtype DataFrame。也就是说,无论你做什么,如果你想创建一个混合 dtype DataFrame ,您将不可避免地创建数据的临时版本(例如,使用 np.empty),并且各种 DataFrame 构造函数将始终创建此临时版本的副本。 这个问题已经在一年前提出了: https :

这对于与其他编程语言的互操作性来说尤其糟糕。 如果您打算通过调用 C 来填充 DataFrame 中的数据,那么到目前为止,最简单的方法是在 python 中创建 DataFrame,获取指向底层数据的指针,即 np.arrays,并传递这些 np .arrays 以便它们可以被填充。 在这种情况下,您根本不关心 DataFrame 以什么数据开始,目标只是分配内存,以便您知道要复制到的内容。

这通常也令人沮丧,因为它意味着原则上(可能取决于具体情况和实现细节等)很难保证您最终不会使用真正应该使用的内存的两倍。

这有一个非常简单的解决方案,它已经基于定量 p​​ython 堆栈:有一个类似于 numpy 的空的方法。 这分配了空间,但实际上并没有浪费任何时间来编写或复制任何东西。 由于已经占用了空,我建议调用方法 from_empty。 它将接受索引(强制性,最常见的用例是传递 np.arange(N))、列(强制性,通常是字符串列表)、类型(可接受的列类型列表,与列的长度相同)。 类型列表应包括对所有 numpy 数字类型(整数、浮点数)以及特殊 Pandas 列(如 DatetimeIndex 和 Categorical)的支持。

作为一个额外的好处,由于实现是在一个完全独立的方法中,它根本不会干扰现有的 API。

API Design Constructors Dtypes

最有用的评论

SO 上有许多线程要求此功能。

在我看来,所有这些问题都源于 BlockManager 将单独的列合并为单个内存块(“块”)。
当指定 copy=False 时,不将数据合并到块中不是最简单的解决方法。

我有一个非整合的猴子补丁 BlockManager:
https://stackoverflow.com/questions/45943160/can-memmap-pandas-series-what-about-a-dataframe
我曾经解决过这个问题。

所有58条评论

您可以简单地创建一个带有索引和列的空框架
然后分配 ndarrays - 这些不会复制你一次分配所有特定的 dtype

如果你愿意,你可以用 np.empty 创建这些

df = pd.DataFrame(index=range(2), columns=["dude", "wheres"])

df
Out[12]:
  dude wheres
0  NaN    NaN
1  NaN    NaN

x = np.empty(2, np.int32)

x
Out[14]: array([6, 0], dtype=int32)

df.dude = x

df
Out[16]:
   dude wheres
0     6    NaN
1     0    NaN

x[0] = 0

x
Out[18]: array([0, 0], dtype=int32)

df
Out[19]:
   dude wheres
0     6    NaN
1     0    NaN

好像是抄袭给我的。 除非我写的代码不是你的意思,或者发生的复制不是你认为我试图删除的副本。

你改变了dtype
这就是为什么它用浮点数复制 try

y = np.empty(2, np.float64)

df
Out[21]:
   dude wheres
0     6    NaN
1     0    NaN

df.wheres = y

y
Out[23]: array([  2.96439388e-323,   2.96439388e-323])

y[0] = 0

df
Out[25]:
   dude         wheres
0     6  2.964394e-323
1     0  2.964394e-323

df = pd.DataFrame(index=range(2), columns=["dude", "wheres"])

df.dtypes
Out[27]:
dude      object
wheres    object
dtype: object

dtype 是对象,因此无论我使用浮点数还是整数,它都会发生变化。

In [25]: arr = np.ones((2,3))

In [26]: df = DataFrame(arr,columns=['a','b','c'])

In [27]: arr[0,1] = 5

In [28]: df
Out[28]: 
   a  b  c
0  1  5  1
1  1  1  1

可以在混合类型上构建 w/oa 副本,但非常棘手。 问题是某些类型需要副本(例如,避免内存争用问题的对象)。 并且内部结构整合了不同的类型,所以添加一个新的类型需要一个副本。 在大多数情况下,避免复制是非常困难的。

您应该只创建您需要的内容,获取指向数据的指针,然后覆盖它。 为什么这是一个问题?

问题是,为了创建我需要的东西,我必须复制正确 dtype 的东西,我无意使用其中的数据。 即使假设您创建空 DataFrame 的建议不使用大量 RAM,这也不会降低复制成本。 如果我想创建一个 1 GB 的 DataFrame 并将其填充到其他地方,我将不得不支付在内存中复制 1 GB 垃圾的成本,这是完全没有必要的。 你不认为这是一个问题吗?

是的,我知道内部结构整合了不同的类型。 我不确定你所说的内存争用问题到底是什么意思,但无论如何,对象并不是真正感兴趣的东西。

实际上,虽然一般来说避免复制是一个难题,但按照我建议的方式避免复制相当容易,因为我从一开始就提供了所有必要的信息。 它与从数据构造相同,除了不是从数据推断 dtypes 和行数并复制数据,而是直接指定 dtypes 和行数,并按照减去副本的方式执行其他所有操作。

每个受支持的列类型都需要一个“空”构造函数。 对于 numpy 数字类型,这是显而易见的,它需要对 Categorical 进行非零工作,不确定 DatetimeIndex。

将 dict 传递给构造函数,并且 copy=False 应该可以工作

所以这会起作用。 但是您必须确保您传递的数组是不同的 dtype。 一旦你对此采取任何措施,它就可以复制底层数据。 所以YMMV。 你当然可以传入np.empty而不是我的 1/0。

In [75]: arr = np.ones((2,3))

In [76]: arr2 = np.zeros((2,2),dtype='int32')

In [77]: df = DataFrame(arr,columns=list('abc'))

In [78]: df2 = DataFrame(arr2,columns=list('de'))

In [79]: result = pd.concat([df,df2],axis=1,copy=False)

In [80]: arr2[0,1] = 20

In [81]: arr[0,1] = 10

In [82]: result
Out[82]: 
   a   b  c  d   e
0  1  10  1  0  20
1  1   1  1  0   0

In [83]: result._data
Out[83]: 
BlockManager
Items: Index([u'a', u'b', u'c', u'd', u'e'], dtype='object')
Axis 1: Int64Index([0, 1], dtype='int64')
FloatBlock: slice(0, 3, 1), 3 x 2, dtype: float64
IntBlock: slice(3, 5, 1), 2 x 2, dtype: int32

In [84]: result._data.blocks[0].values.base
Out[84]: 
array([[  1.,  10.,   1.],
       [  1.,   1.,   1.]])

In [85]: result._data.blocks[1].values.base
Out[85]: 
array([[ 0, 20],
       [ 0,  0]], dtype=int32)

_Initial 尝试被删除,因为reindex强制转换不起作用,这是一个奇怪的“功能”。_

必须使用“方法”,这使得这种尝试不太令人满意:

arr = np.empty(1, dtype=[('x', np.float), ('y', np.int)])
df = pd.DataFrame.from_records(arr).reindex(np.arange(100))

如果您真的很担心性能,我不确定为什么不尽可能多地使用 numpy,因为它在概念上要简单得多。

jreback,感谢您的解决方案。 这似乎有效,即使对于 Categoricals(这让我感到惊讶)。 如果我遇到问题,我会让你知道。 我不确定你的意思:如果你对此做任何事情,它可以复制。 你什么意思? 除非有 COW 语义,否则我认为您在构建时看到的就是深拷贝与浅拷贝的结果。

我仍然认为应该实现一个 from_empty 构造函数,我认为这不会那么困难,虽然这种技术有效,但它确实涉及很多代码开销。 原则上,这可以通过指定单个复合 dtype 和多行来完成。

bashtage,这些解决方案仍然写入整个DataFrame。 由于写入通常比读取慢,这意味着最多可以节省不到一半的开销。

显然,如果我没有去使用 numpy,那是因为 Pandas 有许多我喜欢的很棒的特性和功能,我不想放弃这些。 您是真的在问,还是只是暗示如果我不想受到这种性能影响,我应该使用 numpy?

请抓紧这一点,用户错误,我深表歉意。 带有 copy=False 的 reindex_axis 工作得很好。

bashtage,这些解决方案仍然写入整个DataFrame。 由于写入通常比读取慢,这意味着最多可以节省不到一半的开销。

是的,但是您需要为reindex一个新的method ,它不会填充任何内容,然后您可以分配具有任意列类型的类型化数组,而无需写入/复制。

显然,如果我没有去使用 numpy,那是因为 Pandas 有许多我喜欢的很棒的特性和功能,我不想放弃这些。 您是真的在问,还是只是暗示如果我不想受到这种性能影响,我应该使用 numpy?

这有点夸夸其谈 - 尽管从性能的角度来看也是一个严肃的建议,因为 numpy 可以更容易地接近 data-as-a-blob-of-memory 访问,如果您尝试编写非常重要的高性能代码。 当代码简单性比性能更重要时,您始终可以从 numpy 转换为 Pandas。

我明白你在说什么。 我仍然认为它应该更干净地成为界面的一部分,而不是一种变通方法,但随着变通方法的发展,它是一个很好且易于实现的方法。

Pandas 仍然强调性能是其主要目标之一。 显然,与 numpy 相比,它具有更高级别的功能,并且必须为此付费。 我们所谈论的与那些更高级别的功能无关,并且没有理由应该在不需要它们的地方为大量副本付费。 如果有人对设置列、索引等的成本感到厌恶,您的建议将是合适的,这与本次讨论完全不同。

我认为您高估了编写与在 Python 中分配内存的代码的成本——昂贵的部分是内存分配。 对象创建也很昂贵。

两者都分配了 1GB 的内存,一个空一个零。

%timeit np.empty(1, dtype=[('x', float), ('y', int), ('z', float)])
100000 loops, best of 3: 2.44 µs per loop

%timeit np.zeros(1, dtype=[('x', float), ('y', int), ('z', float)])
100000 loops, best of 3: 2.47 µs per loop

%timeit np.zeros(50000000, dtype=[('x', float), ('y', int), ('z', float)])
100000 loops, best of 3: 11.7 µs per loop

%timeit np.empty(50000000, dtype=[('x', float), ('y', int), ('z', float)])
100000 loops, best of 3: 11.4 µs per loop

3µs 将 150,000,000 个值归零。

现在将它们与一个简单的 DataFrame 进行比较。

%timeit pd.DataFrame([[0]])
1000 loops, best of 3: 426 µs per loop

微不足道的速度大约慢 200 倍。 但对于较大的阵列,情况要糟糕得多。

%timeit pd.DataFrame(np.empty((50000000, 3)),copy=False)
1 loops, best of 3: 275 ms per loop

现在只需要275米秒-请注意,这不是抄袭任何东西。 成本在于设置索引等,当数组非常大时,这显然非常慢。

这对我来说感觉像是过早的优化,因为 Pandas 中的其他开销非常大,以至于 malloc + 填充组件的成本接近于 0。

似乎如果你想在一个紧密循环中分配任何东西,出于性能原因,它必须是一个 numpy 数组。

好的,这就是我认为我们应该做的, @quicknir如果您想进行一些改进。 2 题。

  • #4464 - 这本质上是在DataFrame构造函数中允许复合数据类型,然后转身调用from_records() ,如果传入的数组是一个rec/结构化数组,也可以调用它 - 这个基本上会使from_records成为记录/结构化数组处理路径
  • copy=关键字传递给from_records
  • from_records然后可以使用我上面显示的concat ,而不是将 rec-array 拆分,消毒它们(作为系列)然后将它们重新组合在一起(放入 dtype 块;这部分在内部完成)。

这有点不平凡,但可以很容易地传入一个已经创建的具有混合类型的 ndarray(可能是空的)。 请注意,这可能(在第一遍实现中)仅处理(int/float/string)。 因为 datetime/timedelta 需要特殊的清理,并且会使这变得更加复杂。

所以@bashtage从性能的角度来看是正确的。 简单地根据需要构建框架然后修改 ndarrays 是很有意义的(但你必须通过抓取块来做到这一点,否则你会得到副本)。

我上面的意思是这个。 Pandas 将任何类似的数据类型(例如 int64、int32 不同)分组为一个“块”(帧中的 2-d)。 这些是一个连续的内存 ndarray(即新分配的,除非它只是简单地传入其中当前仅适用于单个 dtype)。 如果你然后做一个 setitem,例如df['new_columns'] = 5并且你已经有一个 int64 块,那么这个新列将最终连接到它(导致该 dtype 的新内存分配)。 如果您使用参考作为对此的看法,它将不再有效。 这就是为什么这不是您可以在没有对等数据帧内部结构的情况下采用的策略。

@bashtage是的,正如您所指出的,最大的成本是索引。 RangeIndex (见#939)将完全解决这个问题。 (它实际上几乎是在一个侧枝中完成的,只需要一些除尘)。

即使使用优化的RangeIndex它仍然比构建 NumPy 数组慢 2 个数量级,考虑到DataFrame更重的重量性质和额外的功能,这已经足够

我认为这只能被认为是一个便利函数,而不是性能问题。初始化混合类型DataFramePanel类的可能很有用。

dtype=np.dtype([('GDP', np.float64), ('Population', np.int64)])
pd.Panel(items=['AU','AT'],
         major_axis=['1972','1973'],
         minor_axis=['GDP','Population'], 
         dtype=[np.float, np.int64])

这只是一个 API/便利问题

同意性能确实是一个附带问题(而不是驱动程序)

@bashtage

%timeit pd.DataFrame(np.empty((100, 1000000)))
100 个循环,最好的 3 个:每个循环 15.6 毫秒

%timeit pd.DataFrame(np.empty((100, 1000000)), copy=True)
1 个循环,最好的 3 个:每个循环 302 毫秒

因此复制到数据帧似乎比创建数据帧所涉及的所有其他工作长 20 倍,即复制(和额外分配)是 95% 的时间。 您所做的基准测试并未对正确的事物进行基准测试。 无论是复制本身还是分配所花费的时间并不重要,关键是如果我可以避免多个 dtype DataFrame 的副本,就像我可以为单个 dtype DataFrame 一样,我可以节省大量时间。

你的两个数量级的推理也是骗人的。 这不是正在执行的唯一操作,还有其他需要执行的操作,例如磁盘读取。 现在,我创建 DataFrame 所需的额外副本在我的简单程序中花费了大约一半的时间,该程序只是将数据从磁盘读取到 DataFrame 中。 如果它花费 1/20 的时间,那么磁盘读取将占主导地位(应该如此),进一步的改进几乎没有效果。

所以我想再次向你们俩强调:这是一个真正的性能问题。

jreback,鉴于串联策略不适用于分类,不要认为您上面建议的改进会起作用。 我认为更好的起点是重新索引。 现在的问题是 reindex 做了很多额外的事情。 但原则上,零行的 DataFrame 具有允许创建具有正确行数的 DataFrame 所需的所有信息,而无需做任何不必要的工作。 顺便说一句,这让我真的觉得熊猫需要一个模式对象,但那是改天再讨论。

我认为我们必须同意不同意。 IMO DataFrames 不是数值生态系统中的极端性能对象,如基本 numpy 数组和 DataFrame 创建之间的数量级差异所示。

%timeit np.empty((1000000, 100))
1000 loops, best of 3: 1.61 ms per loop

%timeit pd.DataFrame(np.empty((1000000,100)))
100 loops, best of 3: 15.3 ms per loop

现在,我创建 DataFrame 所需的额外副本在我的简单程序中花费了大约一半的时间,该程序只是将数据从磁盘读取到 DataFrame 中。 如果它花费 1/20 的时间,那么磁盘读取将占主导地位(应该如此),进一步的改进几乎没有效果。

我认为这更不是关心 DataFrame 性能的理由——即使你可以让它 100% 免费,总的程序时间也只会减少 50%。

我同意您可以在此处进行 PR 来解决此问题,无论您是想将其视为性能问题还是便利性问题。 从我的 POV 来看,我将其视为后者,因为当我关心性能时,我将始终使用 numpy 数组。 Numpy 还做其他事情,比如不使用块管理器,这对某些事情来说相对有效(比如通过添加列来增加数组)。 但从其他角度来看很糟糕。

可能有两种选择。 第一个,一个空的构造函数,如我上面给出的例子。 这不会复制任何内容,但可能会使用 Null-fill 以与 Pandas 中的其他内容保持一致。 空值填充非常便宜,并不是 IMO 问题的根源。

另一种方法是使用DataFrame.from_blocks将预先形成的块直接传递给块管理器。 就像是

DataFrame.from_blocks([np.empty((100,2)), 
                       np.empty((100,3), dtype=np.float32), 
                       np.empty((100,1), dtype=np.int8)],
                     columns=['f8_0','f8_1','f4_0','f4_1','f4_2','i1_0'],
                     index=np.arange(100))

这种类型的方法将强制块具有兼容的形状,所有块都有唯一的类型,以及对索引和列的形状的通常检查。 这种类型的方法不会对数据做任何事情,而是会在 BlockManger 中使用它。

@quicknir你正试图结合非常复杂的东西。 numpy 中不存在分类,而是它们是一种复合 dtype,就像熊猫构造一样。 您必须单独构造和分配(这实际上非常便宜 - 这些不像其他单一的 dtypes 那样组合成块)。

@bashtage soln 似乎很合理。 这可以提供一些简单的检查并简单地传递数据(并由其他内部例程调用)。 通常用户不需要关心内部代表。 既然你真的很想,那么你就需要意识到这一点。

综上所述,我仍然不确定您为什么不完全按照自己的意愿创建框架。 然后抓取块指针并更改值。 它消耗相同的内存,正如@bashtage指出的那样,创建一个本质上已经设置的空框架(具有所有 dtype、索引、列)非常便宜。

不确定空构造函数是什么意思,但如果你的意思是构造一个没有行和所需架构的数据帧并调用 reindex,这与使用 copy=True 创建的时间相同。

你的第二个建议是合理的,但前提是你能弄清楚如何做分类。 在这个主题上,我正在浏览代码,我意识到分类是不可合并的。 因此,凭直觉,我创建了一个整数数组和两个分类序列,然后创建了三个 DataFrame,并将所有三个连接起来。 果然,即使两个 DataFrame 具有相同的 dtype,它也没有执行复制。 我将尝试查看如何使其适用于日期时间索引。

@jreback我仍然没有按照您的意思创建完全像您想要的框架。

@quicknir为什么不显示您实际尝试执行的操作的代码/伪代码示例。

def read_dataframe(filename, ....):
   f = my_library.open(filename)
   schema = f.schema()
   row_count = f.row_count()
   df = pd.DataFrame.from_empty(schema, row_count)
   dict_of_np_arrays = get_np_arrays_from_DataFrame(df)
   f.read(dict_of_np_arrays)
   return df

前面的代码首先构造了一个 numpy 数组字典,然后从中构造了一个 DataFrame,因为它正在复制所有内容。 大约一半的时间都花在了这上面。 所以我想把它改成这个方案。 问题是,即使您不关心内容,如上所述构建 df 也是非常昂贵的。

np 数组的@quicknir dict 需要大量复制。

你应该简单地这样做:

# construct your biggest block type (e.g. say you have mostly floats)
df = DataFrame(np.empty((....)),index=....,columns=....)

# then add in other things you need (say strings)
df['foo'] = np.empty(.....)

# say ints
df['foo2'] = np.empty(...)

如果您通过 dtype 执行此操作,它将很便宜

然后。

for dtype, block in df.as_blocks():
    # fill the values
    block.values[0,0] = 1

因为这些块值是 numpy 数组的视图

类型的组成一般事先并不知道,在最常见的用例中,浮点数和整数是健康的组合。 我想我不知道这将如何便宜,如果我有 30 个浮点列和 10 个 int 列,那么是的,浮点数将非常便宜。 但是,当您执行 int 时,除非有某种方法可以一次性完成所有我所缺少的操作,否则每次添加一列 int 时,都会导致整个 int 块被重新分配。

您之前给我的解决方案接近工作,我似乎无法使其适用于 DatetimeIndex。

不确定空构造函数是什么意思,但如果你的意思是构造一个没有行和所需架构的数据帧并调用 reindex,这与使用 copy=True 创建的时间相同。

一个空的构造函数看起来像

dtype=np.dtype([('a', np.float64), ('b', np.int64), ('c', np.float32)])
df = pd.DataFrame(columns='abc',index=np.arange(100),dtype=dtype)

这将产生与

dtype=np.dtype([('a', np.float64), ('b', np.int64), ('c', np.float32)])
arr = np.empty(100, dtype=dtype)
df = pd.DataFrame.from_records(arr, index=np.arange(100))

只有它不会复制数据。

基本上,构造函数允许为以下调用使用混合数据类型,但只能使用一个基本数据类型。

df = pd.DataFrame(columns=['a','b','c'],index=np.arange(100), dtype=np.float32)

唯一的其他 _feature_ 是防止它对 int 数组进行空填充,这具有将它们转换为对象 dtype 的副作用,因为 int 没有缺失值。

你的第二个建议是合理的,但前提是你能弄清楚如何做分类。 在这个主题上,我正在浏览代码,我意识到分类是不可合并的。 因此,凭直觉,我创建了一个整数数组和两个分类序列,然后创建了三个 DataFrame,并将所有三个连接起来。 果然,即使两个 DataFrame 具有相同的 dtype,它也没有执行复制。 我将尝试查看如何使其适用于日期时间索引。

from_block方法必须知道合并规则,以便它允许多个分类,但只允许其他基本类型之一。

是的......这并不难做到......寻找想要对内部结构进行温和介绍的人......hint.hint.hint...... :)

哈哈,我愿意做一些实施工作,不要误会我的意思。 本周末我将尝试查看内部结构,并了解哪个构造函数更容易实现。 首先,虽然我需要处理我在一个单独的线程中遇到的一些 DatetimeIndex 问题。

@quicknir你找到解决办法了吗?

我正在寻找一种廉价地分配(但不填充)混合 dtype 数据帧的方法,以允许从 cython 库中对列进行无副本填充。

如果您愿意分享您拥有的任何代码(甚至是半工作的)来帮助我入门,那就太好了。

以下是明智的做法吗? 我通过使用原型数据帧来回避重新创建阻塞逻辑。

除了分类之外,哪些 dtype 需要特殊处理?

当然,在填充之前使用创建的数据框是不安全的......

import numpy as np
from pandas.core.index import _ensure_index
from pandas.core.internals import BlockManager
from pandas.core.generic import NDFrame
from pandas.core.frame import DataFrame
from pandas.core.common import CategoricalDtype
from pandas.core.categorical import Categorical
from pandas.core.index import Index

def allocate_like(df, size, keep_categories=False):
    # define axes (waiting for #939 (RangeIndex))
    axes = [df.columns.values.tolist(), Index(np.arange(size))]

    # allocate and create blocks
    blocks = []
    for block in df._data.blocks:
        # special treatment for non-ordinary block types
        if isinstance(block.dtype, CategoricalDtype):
            if keep_categories:
                categories = block.values.categories
            else:
                categories = Index([])
            values = Categorical(values=np.empty(shape=block.values.shape,
                                                 dtype=block.values.codes.dtype),
                                 categories=categories,
                                 fastpath=True)
        # ordinary block types
        else:
            new_shape = (block.values.shape[0], size)
            values = np.empty(shape=new_shape, dtype=block.dtype)

        new_block = block.make_block_same_class(values=values,
                                                placement=block.mgr_locs.as_array)
        blocks.append(new_block)

    # create block manager
    mgr = BlockManager(blocks, axes)

    # create dataframe
    return DataFrame(mgr)


# create a prototype dataframe
import pandas as pd
a = np.empty(0, dtype=('i4,i4,f4,f4,f4,a10'))
df = pd.DataFrame(a)
df['cat_col'] = pd.Series(list('abcabcdeff'), dtype="category")

# allocate an alike dataframe
df1 = allocate_like(df, size=10)

@ARF1不太确定最终目标是什么
你能提供一个简单的例子吗

使用 copy=False 进一步连接通常会绕过这个

@jreback我想使用 cython 库从压缩数据存储中逐列读取大量数据,出于性能原因,我想直接将其解压缩到数据帧中,而无需进行中间复制。

在这种情况下,借用通常的 numpy 解决方案,我想为数据帧预先分配内存,以便我可以将指向这些分配的内存区域的指针传递给我的 cython 库,然后可以使用对应于的普通 c 指针/c 数组那些直接填充数据帧的内存区域,无需中间复制步骤(或中间 python 对象的生成)。 使用多个 cython 线程与已发布的 gil 并行填充数据帧的选项将是一个附带好处。

在(简化的)伪代码中,idom 类似于:

df = fn_to_allocate_memory()
colums = df.columns.values
column_indexes = []
for i in xrange(len(df._data.blocks)):
    column_indexes.extend(df._data.blocks[i].mgr_locs.as_array)
block_arrays = [df._data.blocks[i].values for i in len(df._data.blocks)]

some_cython_library.fill_dataframe_with_content(columns, column_indexes, block_arrays)

这对你有意义吗?

据我所知concatcopy=False不会有相同dtypes成块,但操作的路线COALESCE列将触发此-导致我试图避免复制。 还是我误解了pandas的内部操作?

虽然我在大型(非填充)数据帧(因子 ~6.7)的实例化方面取得了一些进展,但我离麻木速度还很远。 只剩下大约 90 的另一个因素......

In [157]: a = np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4,a10'))

In [158]: df = pd.DataFrame(a)

In [162]: %timeit np.empty(int(1e6), dtype=('i8,i4,i4,f4,f4,f4,a10'))
1000 loops, best of 3: 247 µs per loop

In [163]: %timeit allocate_like(df, size=int(1e6))
10 loops, best of 3: 22.4 ms per loop

In [164]: %timeit pd.DataFrame(np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4,a10')))

10 loops, best of 3: 150 ms per loop

另一个希望是,当频繁读取小容量数据时,这种方法还可以允许更快地重复实例化相同形状的 DataFrame。 到目前为止,这并不是主要目标,但无意中我在这方面取得了更好的进展:只有约 4.8 倍的系数才能达到麻木速度。

In [157]: a = np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4,a10'))

In [158]: df = pd.DataFrame(a)

In [159]: %timeit np.empty(0, dtype=('i8,i4,i4,f4,f4,f4,a10'))
10000 loops, best of 3: 79.9 µs per loop

In [160]: %timeit allocate_like(df, size=0)
1000 loops, best of 3: 379 µs per loop

In [161]: %timeit pd.DataFrame(np.empty(0, dtype=('i4,i4,f4,f4,f4,a10')))
1000 loops, best of 3: 983 µs per loop

编辑

上述时间将苹果与橙子进行比较时描绘了一幅过于悲观的图景:虽然 numpy 字符串列被创建为固定长度的原生字符串,但 Pandas 中的等效列将被创建为 Python 对象数组。 比较相似将 DataFrame 实例化推到 numpy 速度,但索引生成除外,它负责大约 92% 的实例化时间。

@ARF1如果您想要 numpy 速度,那么只需使用 numpy。 我不确定你实际上在做什么或你在 cython 中做什么。 通常的解决方法是将您的计算分块,将单个 dtypes 传递给 cython 或只是获得更大的机器。

DataFrames 在描述和操作数据方面做得比 numpy 多得多。 这不是你实际对他们做的事情。

几乎所有的熊猫操作都复制。 (就像大多数 numpy 操作一样),所以不确定你在追求什么。

@jreback我目前正在使用 numpy,但我混合了 dtypes,它们只能(方便地)用结构化数组处理。 然而,结构化数组本质上是行优先排序的,这与我的典型分析维度相冲突,导致性能不佳。 由于其以列为主的排序,Pandas 看起来是一种自然的选择——如果我能以良好的速度将数据放入数据帧。

当然,另一种选择是使用不同类型的 numpy 数组的字典,但这使得分析变得痛苦,因为切片等不再可能。

通常的解决方法是将您的计算分块,将单个 dtypes 传递给 cython。

这就是我在示例中对block_arrays变量所做的事情。

或者换一台更大的机器。

快 100 倍以上对我来说是一个财务挑战。 ;-)

@ARF1你有一个非常奇怪的工作方式模型。 通常,您会创建少量数据框,然后对其进行处理。 创建速度只是任何实际计算或操作的一小部分。

@jreback :这不是一个奇怪的模型。 如果您从纯 Python 的角度看待事物,这可能是一个奇怪的模型。 如果您正在使用 C++ 代码,将数据读入 python 对象的最简单方法是将指针传递给预先存在的 python 对象。 如果您是在性能敏感的上下文中执行此操作,您需要一种廉价且稳定(就内存位置而言)的方式来创建 python 对象。

老实说,我不确定为什么这种态度在熊猫板上很常见。 我认为这是不幸的,就我所知,pandas 是比 numpy 更高级别的构造,但人们仍然可以更容易地在 pandas 的“之上”进行开发。 如果您的 C 代码想要将表格数据输出到 python 中,pandas DataFrame 是迄今为止最理想的类型,所以这看起来真的是一个重要的用例。

请不要对我写的东西持消极态度,如果我不认为 Pandas DataFrames 如此棒,我只会使用 numpy 记录或类似的东西并完成它。

@ARF1 :最终,我不记得原因了,但我能做的最好的事情是从带有 Copy=False 的 numpy 数组中为每个数字类型创建一个 DataFrame,然后再次使用带有 Copy=False 的 pandas.concat连接它们。 当您从 numpy 数组创建单一类型的 DataFrame 时,请非常注意 numpy 数组的方向。 如果方向错误,那么每一列对应的 numpy 数组将被非平凡地跨越,pandas 不喜欢这样,会第一时间复制一份。 您可以在最后添加分类,因为它们不会被合并,并且不应触发帧其余部分的任何副本。

我建议编写一些单元测试来逐步执行此操作并不断获取指向底层数据的指针(通过底层 numpy 数组的array_interface )并验证它们是否相同以确保副本实际上被省略。 Pandas 做出了一个非常不幸的决定,即不必遵守复制/就地参数。 也就是说,即使您为 DataFrame 构造函数设置了例如 copy=False,如果 Pandas 决定为了构造 DataFrame 需要这样做,它仍然会执行复制。 当参数不能被接受时,pandas 会这样做而不是抛出,这使得可靠地编写省略副本的代码非常令人筋疲力尽,并且需要非常有条理。 如果您不编写单元测试来验证,您可能会在稍后不小心调整一些导致复制的内容,并且它会悄悄发生并破坏您的性能。

@quicknir如果你这么说的话。 我认为在尝试优化事物之前,您应该简单地进行概要分析。 正如我之前所说,概率会再次出现。 施工时间不应占主导地位。 如果是这样,那么您只是使用 DataFrame 来保存东西,那么首先使用它有什么意义呢? 如果它不占主导地位,那有什么问题呢?

@jreback你写的,假设我还没有分析过。 事实上,我有。 我们有 c++ 和 python 代码,它们都从相同的数据格式反序列化表格数据。 虽然我预计 python 代码会有一些开销,但我认为差异应该很小,因为磁盘读取时间应该占主导地位。 情况并非如此,在我开始极其仔细地重新设计以最小化副本之前,与 C++ 代码相比,python 版本花费的时间是原来的两倍甚至更糟,而且几乎所有的开销都只是在创建 DataFrame 上。 换句话说,简单地创建一个我根本不关心其内容的某个非常大的 DataFrame 所花费的时间大约与读取、解压缩并将我关心的数据写入该 DataFrame 的时间一样长。 这是极其糟糕的表现。

如果我是此代码的最终用户并考虑到了特定的操作,那么您所说的不占主导地位的构造可能是有效的。 实际上,我是一名开发人员,这段代码的最终用户是其他人。 我不确切知道他们将用 DataFrame 做什么,DataFrame 是获取磁盘上数据的内存表示的一种方法。 如果他们想对磁盘上的数据做一些非常简单的事情,他们仍然必须通过 DataFrame 格式。

显然,我可以支持更多获取数据的方法(例如 numpy 构造),但这会大大增加代码中的分支,并使我作为开发人员的工作变得更加困难。 如果 DataFrames 需要这么慢有什么根本原因,我会理解,并决定是否支持 DataFrame、numpy 或两者。 但是没有真正的理由为什么它需要这么慢。 可以编写一个 DataFrame.empty 方法,该方法采用一组元组,其中每个元组包含列名称和类型以及行数。

这就是我的意思是支持用户和图书馆作者之间的区别。 编写自己的代码比编写库更容易。 让你的图书馆只支持用户而不是其他图书馆作者会更容易。 我只是认为在这种情况下,DataFrame 的空分配在 Pandas 中将是一个容易实现的目标,这将使像我和@ARF1这样的人的生活更轻松。

好吧,如果你想有一个合理的经过测试的记录解决方案,所有的耳朵。 pandas 有相当多的用户/开发人员。 这就是 DataFrame 如此通用的原因,也是它需要大量错误检查和推理的原因。 欢迎您按照上述方式查看您可以做什么。

我愿意花一些时间来实现这一点,但前提是一些 Pandas 开发人员对设计有一些合理的共识。 如果我提交了一个拉取请求并且人们想要改变某些事情,那很酷。 或者,如果我在花了十个小时之后意识到没有办法干净利落地做某事,而唯一的方法可能涉及人们认为令人反感的事情,那也很酷。 但是我对花费 X 小时并被告知这不是那么有用,实现很混乱,我们认为它无法真正清理,使代码库复杂化等并不是很酷。我不知道我对这种情绪很不理解,我之前没有对 OSS 项目做出重大贡献,所以我不知道它是如何工作的。 只是在我最初的帖子中,我开始提出这件事,然后坦率地说,我从你那里得到的印象是,这对 Pandas 来说有点“超出范围”。

如果你愿意,我可以打开一个新问题,尽可能地创建一个具体的设计方案,一旦有反馈/初步批准,我会在有能力的时候进行处理。

@quicknir关键是它必须通过整个测试套件,这是非常全面的。

这并不超出 Pandas 的范围,但 API 必须对用户友好。

我不知道你为什么不喜欢

concat(list_of_arrays,axis=1,copy=False)我相信这正是你想要的(如果没有,那就不清楚你真正想要什么)。

我最终使用了类似的技术,但使用了从单个 numpy 数组创建的 DataFrame 列表,每个数组都有不同的类型。

首先,我想我在做这项技术时仍然遇到了一些副本。 正如我所说,pandas 并不总是尊重 copy=False,因此查看您的代码是否真的在复制非常令人筋疲力尽。 我真的希望对于 Pandas 17,开发人员会考虑将 copy=True 设为默认值,然后在无法删除副本时抛出 copy=False。 但无论如何。

其次,另一个问题是之后必须对列重新排序。 这令人惊讶地尴尬,我能找到的唯一方法是在不制作副本的情况下做到这一点,最初将列名按所需的最终顺序排列为整数。 然后我就地进行了索引排序。 然后我更改了列名。

第三,我发现对于时间戳类型(numpy datetime64)来说,副本是不可避免的。

我不久前写了这段代码,所以在我脑海中并不新鲜。 有可能我犯了错误,但我非常仔细地完成了它,这就是我当时想出的结果。

您上面提供的代码甚至不适用于 numpy 数组。 它失败了: TypeError: cannot concatenate a non-NDFrame object。 您必须首先使它们成为 DataFrame。

并不是我不喜欢你在这里或上面给出的解决方案。 我只是还没有看到一个简单的工作。

@quicknir很好,我上面的例子有效。 请准确提供您在做什么,我可以尝试帮助您。

pd.concat([np.zeros((2,2))],axis=1,copy=False)

我在熊猫 0.15.2 上,所以也许这在 0.16 开始工作?

请阅读pd.concat的文档字符串。 你需要传递一个DataFrame

顺便说一句copy=True是默认值

没错,我就是这么写的。 你上面写的代码片段有 list_of_arrays,而不是 list_of_dataframes。 不管怎样,我想我们彼此了解。 我确实最终使用了 pd.concat 方法,但它非常重要,有一大堆问题可以绊倒人们:

1) 您必须创建一个 DataFrame 列表。 每个 DataFrame 必须恰好有一个不同的 dtype。 因此,您必须在开始之前收集所有不同的 dtype。

2) 每个 DataFrame 必须从所需 dtype、相同行数、所需列数和 order ='F' 标志的单个 numpy 数组创建; 如果 order='C' (默认),那么大熊猫通常会复制,否则它不会复制。

3) 忽略 1) 对于 Categoricals,它们不会合并成一个块,因此您可以稍后添加它们。

4) 当您创建所有单独的 DataFrame 时,应使用代表您希望它们的顺序的整数命名列。否则可能无法在不触发副本的情况下更改列顺序。

5) 创建数据帧列表后,使用 concat。 您必须煞费苦心地确认您没有搞砸任何事情,因为如果无法删除副本,则 copy=False 不会抛出,而是静默复制。

6) 对列索引进行排序以达到您想要的排序,然后重命名列。

我严格地应用了这个程序。 它不是单行,有很多地方会犯错误,我很确定它仍然不适用于时间戳,并且有很多不必要的开销可以通过不使用接口来避免。 如果您愿意,我可以仅使用公共 API 编写此函数的外观草稿,也许会结合一些测试来查看它是否真的消除了副本,以及针对哪些 dtype。

此外,copy=False 是例如 DataFrame 构造函数的默认值。 我的主要观点更多的是,一个不能遵守其参数的函数应该抛出而不是“做一些合理的事情”。 也就是说,如果无法遵守 copy=False,则应抛出异常,以便用户知道他们要么必须更改其他输入以便可以进行复制省略,要么必须将 copy 更改为 True。 当 copy=False 时,副本永远不应该静默发生,这更令人惊讶并且不利于注重性能的用户发现错误。

你在这里有很多不必要的步骤
请像我上面那样展示一个实际的例子

您了解 numpy 视图可以通过非常简单的整形操作(有时)而不是其他操作返回副本

只有在复制时才会有软保证,因为它通常不可能在没有大量内省的情况下保证这一点,这根据定义违背了简单强大的高性能代码的目的

copy=False在 DataFrame 构造中的行为与 numpy 的np.array函数一致(例如,如果您提供一个数组列表,则最终数据将始终复制)。

这似乎是 Pandas 中的一个不幸的功能差距。 恕我直言,对于当前的熊猫内部模型(整合块),我们永远不会有令人满意的解决方案。 不幸的是,这对于 Pandas 来说并不是一个真正的选择,因为 Pandas 仍然需要为那些使用大量列制作 DataFrame 的人工作。

我们需要的是一个备用的 DataFrame 实现,专门设计用于处理整洁的数据,这些数据将每一列独立存储为一维 numpy 数组。 这实际上有点类似于xray 中的数据模型,除了我们允许列是 N 维数组。

我相信,考虑到它需要支持的各种不同的列类型,只分配空间的通用高性能 Pandas 数据帧构造函数是非常重要的。

话虽如此,对于想要使用 Pandas 数据帧作为高性能数据容器来实现仅限于他们需要的列类型的仅分配数据帧构造函数的库作者来说,这似乎相当简单。

以下代码段可以作为灵感。 它允许以接近 numpy 的速度实例化仅分配的、未填充的数据帧。 注意代码需要 PR #9977:

import numpy as np
from pandas.core.index import _ensure_index
from pandas.core.internals import BlockManager
from pandas.core.generic import NDFrame
from pandas.core.frame import DataFrame
from pandas.core.common import CategoricalDtype
from pandas.core.categorical import Categorical
from pandas.core.index import RangeIndex

def allocate_like(df, size, keep_categories=False):
    # define axes (uses PR #9977)
    axes = [df.columns.values.tolist(), RangeIndex(size)]

    # allocate and create blocks
    blocks = []
    for block in df._data.blocks:
        # special treatment for non-ordinary block types
        if isinstance(block.dtype, CategoricalDtype):
            if keep_categories:
                categories = block.values.categories
            else:
                categories = Index([])
            values = Categorical(values=np.empty(shape=block.values.shape,
                                                 dtype=block.values.codes.dtype),
                                 categories=categories,
                                 fastpath=True)
        # ordinary block types
        else:
            new_shape = (block.values.shape[0], size)
            values = np.empty(shape=new_shape, dtype=block.dtype)

        new_block = block.make_block_same_class(values=values,
                                                placement=block.mgr_locs.as_array)
        blocks.append(new_block)

    # create block manager
    mgr = BlockManager(blocks, axes)

    # create dataframe
    return DataFrame(mgr)

使用示例构造函数allocate_like() ,大型数组的性能损失 cf numpy 仅为 x2.3(通常为 x333),零大小数组的性能损失仅为 x3.3(通常为 x8.9):

In [2]: import numpy as np

In [3]: import pandas as pd

In [4]: a = np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4'))

# create template-dataframe
In [5]: df = pd.DataFrame(a)

# large dataframe timings
In [6]: %timeit np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4'))
1000 loops, best of 3: 212 µs per loop

In [7]: %timeit allocate_like(df, size=int(1e6))
1000 loops, best of 3: 496 µs per loop

In [8]: %timeit pd.DataFrame(np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4')))
10 loops, best of 3: 70.6 ms per loop

# zero-size dataframe timing
In [9]: %timeit np.empty(0, dtype=('i4,i4,f4,f4,f4'))
10000 loops, best of 3: 108 µs per loop

In [10]: %timeit allocate_like(df, size=0)
1000 loops, best of 3: 360 µs per loop

In [11]: %timeit pd.DataFrame(np.empty(0, dtype=('i4,i4,f4,f4,f4')))
1000 loops, best of 3: 959 µs per loop

抱歉,我暂时忘记了这一点。 @ARF1 ,非常感谢上面的代码示例。 非常好,还有性能指标。

我真的觉得创建一个对应于 DataFrame 布局的类,没有任何数据,会使上面的代码更自然,也可能更高效。 此类也可以重用,例如在重新索引行时。

我的基本建议是这样的:一个名为 DataFrameLayout 的类,它包含 dtypes、列名和列顺序。 例如,它可以存储从 dtype 到列号的 dict(用于排序),以及一个包含所有名称的单独数组。 从这个布局中,您可以看到对 dict 的简单、优雅的迭代将允许快速创建块管理器。 然后可以在诸如空构造函数之类的地方或在重新索引操作中使用此类。

我认为这种抽象对于更复杂的数据是必要的。 在某种意义上,DataFrame 是一种复合数据类型,DataFrameLayout 将指定组合的确切性质。

顺便说一下,我认为 Categoricals 需要类似的东西; 也就是说,需要一个 CategoricalType 抽象来存储类别,无论它们是否有序,支持数组类型等。也就是说,除了实际数据之外的所有内容。 实际上,当您考虑 DataFrameLayout 时,您会意识到所有列都必须具有完全指定的类型,而目前这对于 Categoricals 来说是有问题的。

人们如何看待这两个类?

@quicknir我们已经有一个CategoricalDtype类——我同意它可以扩展到你描述的完整的CategoricalType中。

我不完全确定DataFrameLayout类。 从根本上说,我认为我们可以为数据帧使用另一种更简单的数据模型(更类似于它们在 R 或 Julia 中的做法)。 人们对这种事情有一些兴趣,我怀疑它最终会以某种形式发生,但可能不会很快发生(也许永远不会作为熊猫项目的一部分)。

@quicknir 是的DataFrameLayout在这里重新发明轮子。 我们已经有一个 dtype 规范,例如

In [14]: tm.makeMixedDataFrame().to_records().dtype
Out[14]: dtype([('index', '<i8'), ('A', '<f8'), ('B', '<f8'), ('C', 'O'), ('D', '<M8[ns]')])

@jreback这不是在重新发明轮子,因为 dtype 规范有几个主要问题:

1) 据我所知, to_records() 将执行整个 DataFrame 的深度复制。 获取 DataFrame 的规范(从现在开始我将只使用这个术语)应该既便宜又容易。

2) to_records 的输出是 numpy 类型。 这样做的一个含义是,我看不到如何扩展它以正确支持分类。

3) 这种内部存储规范的方法与数据存储在 DataFrame 内部的方式(即在类似 dtype 的块中)不容易兼容。 从这样的规范创建块涉及许多额外的工作,可以通过以我建议的方式存储规范,使用从 dtype 到列号的 dict 来省略这些工作。 当您有一个包含 2000 列的 DataFrame 时,这将是昂贵的。

简而言之,记录表示的 dtype 更像是缺乏适当规范的解决方法。 它缺乏几个关键功能,而且性能要差得多。

SO 上有许多线程要求此功能。

在我看来,所有这些问题都源于 BlockManager 将单独的列合并为单个内存块(“块”)。
当指定 copy=False 时,不将数据合并到块中不是最简单的解决方法。

我有一个非整合的猴子补丁 BlockManager:
https://stackoverflow.com/questions/45943160/can-memmap-pandas-series-what-about-a-dataframe
我曾经解决过这个问题。

此页面是否有帮助?
0 / 5 - 0 等级