python - 使用具有匹配索引的 pandas .loc 时内存爆炸 + 分配给出重复轴错误
问题描述
这是大多数 pythonic way to concatenate pandas cells with conditions的观察结果 我无法理解为什么第三个解决方案比第一个解决方案占用更多内存。
如果我不采样第三个解决方案不会给出运行时错误,显然有些奇怪
为了模拟大型数据框,我尝试重新采样,但没想到会遇到这种错误
背景
非常不言自明,一行,看起来像pythonic
df['city'] + (df['city'] == 'paris')*('_' + df['arr'].astype(str))
s = """city,arr,final_target
paris,11,paris_11
paris,12,paris_12
dallas,22,dallas
miami,15,miami
paris,16,paris_16"""
import pandas as pd
import io
df = pd.read_csv(io.StringIO(s)).sample(1000000, replace=True)
df
速度
%%timeit
df['city'] + (df['city'] == 'paris')*('_' + df['arr'].astype(str))
# 877 ms ± 19.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
df['final_target'] = np.where(df['city'].eq('paris'),
df['city'] + '_' + df['arr'].astype(str),
df['city'])
# 874 ms ± 19.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
如果我不采样,则没有错误,输出也完全匹配
错误(已更新)(仅在我从数据帧中采样时发生)
%%timeit
df['final_target'] = df['city']
df.loc[df['city'] == 'paris', 'final_target'] += '_' + df['arr'].astype(str)
MemoryError: Unable to allocate 892. GiB for an array with shape (119671145392,) and data type int64
对于较小的输入(样本大小 100),我们会得到不同的错误,说明由于大小不同而导致的问题,但是内存分配和采样是怎么回事?
ValueError: cannot reindex from a duplicate axis
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-5-57c5b10090b2> in <module>
1 df['final_target'] = df['city']
----> 2 df.loc[df['city'] == 'paris', 'final_target'] += '_' + df['arr'].astype(str)
~/anaconda3/lib/python3.8/site-packages/pandas/core/ops/methods.py in f(self, other)
99 # we are updating inplace so we want to ignore is_copy
100 self._update_inplace(
--> 101 result.reindex_like(self, copy=False), verify_is_copy=False
102 )
103
我每次都从头开始重新运行它们
更新
这是我想的一部分
s = """city,arr,final_target
paris,11,paris_11
paris,12,paris_12
dallas,22,dallas
miami,15,miami
paris,16,paris_16"""
import pandas as pd
import io
df = pd.read_csv(io.StringIO(s)).sample(10, replace=True)
df
city arr final_target
1 paris 12 paris_12
0 paris 11 paris_11
2 dallas 22 dallas
2 dallas 22 dallas
3 miami 15 miami
3 miami 15 miami
2 dallas 22 dallas
1 paris 12 paris_12
0 paris 11 paris_11
3 miami 15 miami
替换采样时重复索引
因此,即使 df.arr 和 df.loc 具有本质不同的大小或替换为
df.loc[df['city'] == 'paris', 'arr'].astype(str)
将解决问题,重置索引也解决了问题。正如2e0byo指出的那样。仍然有人可以解释 .loc 是如何工作的以及当索引中有重复并且不匹配时内存爆炸?!
解决方案
@2e0byo一针见血地说,在这种情况下,熊猫的算法“效率低下”。
就 而言.loc
,它并没有真正做任何了不起的事情。它在这里的使用类似于使用相同形状的布尔数组索引 numpy 数组,并添加对特定列的类似 dict-key 的访问 - 也就是说,df['city'] == 'paris'
它本身就是一个数据帧,具有相同的行数和相同的索引为df
,具有单列布尔值。df.loc[df['city'] == 'paris']
然后给出一个数据框,该数据框仅由其中为真的行组成df['city'] == 'paris'
(在“城市”列中有“巴黎”)。添加附加参数 'final_target' 然后只返回这些行的 'final_target' 列,而不是全部三个(并且因为它只有一列,它在技术上是一个Series
对象 - 也是如此df['arr']
)。
当 pandas 实际尝试添加两个系列时,就会发生内存爆炸。正如@2e0byo 指出的那样,它必须重塑系列才能做到这一点,它通过调用第一个系列的align()
方法来做到这一点。在align
操作过程中,函数pandas.core.reshape.merge.get_join_indexers()
调用pandas._libs.join.full_outer_join()
(第 155 行)使用三个参数:left
、right
和max_groups
(澄清点:这些是函数内部full_outer_join
的名称)。left
andright
是包含两个 Series 对象的索引(索引列中的值)的整数数组,并且max_groups
是left
or中唯一元素的最大数量right
(在我们的例子中,它是五个,对应于 中的五个原始行s
)。
full_outer_join
立即转身并调用pandas._libs.algos.groupsort_indexer()
(第 194 行),一次使用left
andmax_groups
作为参数,一次使用right
and max_groups
。groupsort_indexer
返回两个数组 - 一般情况下,indexer
and counts
(对于 with 的调用left
,它们被称为left_sorter
and left_count
,并且对应地 for right
)。counts
具有长度max_groups + 1
,并且每个元素(第一个元素除外,它未使用)包含相应索引组出现在输入数组中的次数的计数。所以对于我们的例子, with max_groups = 5
,count
数组有 shape (6,)
,元素 1-5 代表 5 个唯一索引值出现在left
and中的次数right
。
构造另一个数组 ,indexer
以便用它索引原始输入数组返回按升序分组的所有元素 - 因此是“排序器”。在为两者完成此操作后,left
将right
两个full_outer_join
分拣机切碎并将它们串起来。full_outer_join
返回两个相同大小的数组,left_idx
并且right_idx
- 这些数组变得非常大并引发错误。排序器中元素的顺序决定了它们在最后两个输出数组中出现的顺序,而count
数组决定了每个元素出现的频率。由于left
先行,它的元素保持在一起 - in left_idx
,其中的第一个left_count[1]
元素每个left_sorter
都重复right_count[1]
多次(aaabbbccc ...)。在同一个地方right_idx
, 第一个right_count[1]
元素连续重复left_count[1]
(abcabcabc...)。(方便的是,由于0
行中的行s
是'paris'
一行,left_count[1]
并且right_count[1]
总是相等的,所以你会得到x重复次数x开始的次数)。然后 的下一个left_count[2]
元素left_sorter
重复right_count[2]
多次,依此类推...如果任何counts
元素为零,则idx
数组中的相应点用-1填充,稍后将被屏蔽(如,right_count[i] = 0
表示元素right_idx
为-1,反之亦然 - 对于left_count[3]
and总是如此left_count[4]
,因为 rows2
和3
ins
是非'paris'
)。
最后,_idx
数组的元素数量等于N_elements
,可以计算如下:
left_nonzero = (left_count[1:] != 0)
right_nonzero = (right_count[1:] != 0)
left_repeats = left_count[1:]*left_nonzero + np.ones(len(left_counts)-1)*(1 - left_nonzero)
right_repeats = right_count[1:]*right_nonzero + np.ones(len(right_counts)-1)*(1 - right_nonzero)
N_elements = sum(left_repeats*right_repeats)
数组的相应元素count
相乘(将所有零替换为一),然后相加得到N_elements
.
你可以看到这个数字增长很快(O(n^2)
)。对于具有 1,000,000 个采样行的原始数据帧,每个行的出现大致相同,那么count
数组看起来像:
left_count = array([0, 2e5, 2e5, 0, 0, 2e5])
right_count = array([0, 2e5, 2e5, 2e5, 2e5, 2e5])
总长度约为1.2e11
. 一般来说,对于初始样本N
( df = pd.read_csv(io.StringIO(s)).sample(N, replace=True)
),最终大小约为0.12*N**2
一个例子
看一个小例子可能会有所帮助,以了解当他们制作这些巨大的数组时要做什么full_outer_join
以及groupsort_indexer
正在尝试做什么。我们将从一个只有 10 行的小样本开始,然后按照各种数组到达最终输出,left_idx
并且right_idx
. 我们将从定义初始数据框开始:
df = pd.read_csv(io.StringIO(s)).sample(10, replace=True)
df['final_target'] = df['city'] # this line doesn't change much, but meh
看起来像:
city arr final_target
3 miami 15 miami
1 paris 11 paris
0 paris 12 paris
0 paris 12 paris
0 paris 12 paris
1 paris 11 paris
2 dallas 22 dallas
3 miami 15 miami
2 dallas 22 dallas
4 paris 16 paris
df.loc[df['city'] == 'paris', 'final_target']
好像:
1 paris
0 paris
0 paris
0 paris
1 paris
4 paris
和df['arr'].astype(str)
:
3 15
1 11
0 12
0 12
0 12
1 11
2 22
3 15
2 22
4 16
然后,在对 的调用中full_outer_join
,我们的参数看起来像:
left = array([1,0,0,0,1,4]) # indexes of df.loc[df['city'] == 'paris', 'final_target']
right = array([3,1,0,0,0,1,2,3,2,4]) # indexes of df['arr'].astype(str)
max_groups = 5 # the max number of unique elements in either left or right
函数调用groupsort_indexer(left, max_groups)
返回以下两个数组:
left_sorter = array([1, 2, 3, 0, 4, 5])
left_count = array([0, 3, 2, 0, 0, 1])
left_count
保存每个唯一值的出现次数left
- 第一个元素未使用,但随后有 3 个零、2 个一、0 个二、0 个三和 1 个四left
。
left_sorter
是这样的left[left_sorter] = array([0, 0, 0, 1, 1, 4])
- 一切都井井有条。
现在right
:groupsort_indexer(right, max_groups)
返回
right_sorter = array([2, 3, 4, 1, 5, 6, 8, 0, 7, 9])
right_count = array([0, 3, 2, 2, 2, 1])
再次,right_count
包含每个计数出现的次数:未使用的第一个元素,然后是 3 个 0、2 个 1、2 个 2、2 个 3 和 1 个 4(注意两个数组的元素 1、2 和 5count
相同: 这些是 ) 中的s
行'city' = 'paris'
。还,right[right_sorter] = array([0, 0, 0, 1, 1, 2, 2, 3, 3, 4])
通过计算两个count
数组,我们可以计算出idx
数组的大小(使用实际数字比使用上面的公式要简单一些):
N_total = 3*3 + 2*2 + 2 + 2 + 1*1 = 18
3
是两个counts
数组的元素 1,所以我们可以期待像[1,1,1,2,2,2,3,3,3]
start left_idx
,since [1,2,3]
startsleft_sorter
和[2,3,4,2,3,4,2,3,4]
to start right_idx
,sinceright_sorter
开始于[2,3,4]
。然后我们有两个,所以[0,0,4,4]
forleft_idx
和[1,5,1,5]
for right_idx
。然后left_count
有两个零和right_count
两个二,所以接下来进入 4和接下来-1
的left_idx
四个元素right_sorter
进入right_idx
: [6,8,0,7]
。两者都count
以一个结束,因此sorters
go 中的最后一个元素中的每个idx
: 5
forleft_idx
和9
for right_idx
,留下:
left_idx = array([1, 1, 1, 2, 2, 2, 3, 3, 3, 0, 0, 4, 4,-1, -1, -1, -1, 5])
right_idx = array([2, 3, 4, 2, 3, 4, 2, 3, 4, 1, 5, 1, 5, 6, 8, 0 , 7, 9])
这确实是18个元素。
由于两个索引数组的形状相同,pandas 可以从我们原来的数组中构造两个形状相同的 Series 来执行所需的任何操作,然后它可以屏蔽这些数组以获取排序后的索引。使用一个简单的布尔过滤器来查看我们刚刚排序的方式left
和right
输出,我们得到:
left[left_idx[left_idx != -1]] = array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 4])
right[right_idx[right_idx != -1]] = array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 4])
回溯所有的函数调用和模块后,此时相加的结果是:
0 paris_12
0 paris_12
0 paris_12
0 paris_12
0 paris_12
0 paris_12
0 paris_12
0 paris_12
0 paris_12
1 paris_11
1 paris_11
1 paris_11
1 paris_11
2 NaN
2 NaN
3 NaN
3 NaN
4 paris_16
它在(第 11066 行)的行中,result
以及我们要添加的之前的两个系列。result = op(self, other)
pandas.core.generic.NDFrame._inplace_method
op = pandas.core.series.Series.__add__
self
other
因此,据我所知,pandas 基本上会尝试对具有相同索引的行的每个组合执行操作(例如,1
第一个系列中具有索引的任何和所有行都应该与1
另一个系列中的所有行索引一起操作)。如果其中一个系列具有另一个没有的索引,则这些行将被屏蔽掉。在这种情况下,具有相同索引的每一行都是相同的。只要您不需要做任何事情,它就可以工作(尽管是多余的)-当 pandas 尝试将此结果重新索引回原始数据框的形状时,小数据框的麻烦就会出现df
。
分割线(较小的数据帧可以通过,但较大的数据帧不能通过的线)是result = op(self, other)
上面的那条线。稍后在同一个函数(称为,note,_inplace_method
)中,程序在 处退出self._update_inplace(result.reindex_like(self, copy=False), verify_is_copy=False)
。它尝试重新索引result
,使其看起来像self
,因此它可以替换self
为result
(self
是原始系列,添加中的第一个,df.loc[df['city'] == 'paris', 'final_target']
)。这就是较小的情况失败的地方,因为显然result
有一堆重复的索引,而 pandas 不想在删除其中一些时丢失任何信息。
最后一件事
可能值得一提的是,这种行为并不是这里的加法操作所特有的。每当您尝试对具有大量重复索引的两个大型数据帧进行算术运算时,都会发生这种情况 - 例如,尝试以与第一个数据帧完全相同的方式定义第二个数据帧df2 = pd.read_csv(io.StringIO(s)).sample(1000000, replace=True)
,然后尝试运行df.arr*df2.arr
. 你会得到同样的内存错误。
有趣的是,逻辑和比较运算符有防止这样做的保护——它们需要相同的索引,并在调用它们的运算符方法之前检查它。
我在 pandas 1.2.4、python 3.7.10 中完成了所有工作,但我提供了 pandas Github 的链接,该链接目前为 1.3.3 版。据我所知,差异不会影响结果。
推荐阅读
- rust - 用相同的值替换 Rust 中的变量会产生不同的结果
- java - 使所有 spring-boot @ConfigurationProperties 字段在应用程序启动时都是必需的,而无需注释每个字段
- html - 如何在使用 `grid-auto-flow: column` 时将 `height: auto` 设置为 CSS 网格容器?
- angular - 如何从 let i =index ( from html) 获取 app.ts 中的 i 值?#角度
- node.js - 使用 atom 在 Mac 上保存 node.js 文件的问题
- flutter - 如何制作具有屏幕尺寸的动画?
- html - 容器内文本的对齐方式
- numpy - np.where:“ValueError:操作数无法与形状一起广播(38658637,)(9456,)”
- django - 如何链接 Django 查询集并按这些查询集的不同字段对结果进行排序
- mysql - 完成mysql回调函数