numpy 数组中非唯一行的快速组合,映射到列(即快速数据透视 table 问题,没有 Pandas)

Fast combination of non-unique rows in numpy array, mapped to columns (i.e. fast pivot table problem, without Pandas)

我想知道是否有人可以就以下编码问题提供任何想法或建议,我对快速 Python 实施特别感兴趣(即避免 Pandas)。

我有一组(虚拟示例)数据,例如:

|   User   |   Day   |   Place   |   Foo   |   Bar   |
      1         10        5          True     False
      1         11        8          True     False
      1         11        9          True     False
      2         11        9          True     False
      2         12        1          False    True
      1         12        2          False    True

包含 2 个用户("user1" 和 "user2")在给定 day/place 的数据,其中有 2 个感兴趣的布尔值(此处称为 foo 和 bar)。

我只对在同一天同一地点为两个用户记录数据的情况感兴趣。有了这些相关的数据行,然后我想为 day/place 条目创建新列,将用户和 foo/bar 描述为布尔值。例如

|   Day   |   Place   |   User 1 Foo   |   User 1 Bar   |   User 2 Foo   |   User 2 Bar   |
    11           9          True            False              True           False

每列数据都存储在numpy数组中。我很欣赏这是 pandas 的理想问题,使用枢轴 table 功能(例如 Pandas 解决方案是:

user = np.array([1, 1, 1, 2, 2, 1], dtype=int)
day = np.array([10, 11, 11, 11, 12, 12], dtype=int)
place = np.array([5,8,9,9,1,2], dtype=int)
foo = np.array([1, 1, 1, 1, 0, 0], dtype=bool)
bar = np.array([0, 0, 0, 0, 1, 1], dtype=bool) 

df = pd.DataFrame({
'user': user,
'day': day,
'place': place,
'foo': foo,
'bar': bar,
})
df2 = df.set_index(['day','place']).pivot(columns='user')

df2.columns = ["User1_foo", "User2_foo", "User1_bar", "User2_bar"]
df2 = df2.reset_index()
df2.dropna(inplace=True)   

但在我的实际使用中,我有数百万行数据,分析表明数据帧的使用和数据透视操作是性能瓶颈。

因此,我怎样才能实现相同的输出,即日期、地点的 numpy 数组和 user1_foo、user1_bar、user2_foo、user2_bar 仅用于案例哪里有两个用户在同一天的数据并放在原始输入数组中?

我想知道以某种方式从 np.unique 中找到索引然后反转它们是否是一个可能的解决方案,但无法使其工作。因此,任何解决方案(最好是快速执行)都将非常感谢!

这确实用到了 pandas,但它仍然可能有用。首先,也许首先进行搜索并删除所有没有重复日期和地点值的行可以加快速度。例如,运行ning df2=df[df.duplicated(['day','place'],keep=False)] 将删除具有唯一日期和地点对的每一行。我不确定您的数据是什么样的,但这可能会显着减少您拥有的数据量。对于你给出的例子,这行代码输出

   user  day  place   foo    bar
2     1   11      9  True  False
3     2   11      9  True  False

在此 p运行ing 之后,可以进行简化的数据提取。现在,只有当我们知道一个用户不会有任何重复的地点和日期条目并且用户一总是排在第一位时,下面的代码才有效。

def every_other_row(df): 
    first=df.iloc[::2, :]
    second=df.iloc[1::2, :]
    first['foo user 2']=second['foo'].astype(bool)
    first['bar user 2']=second['bar'].astype(bool)

    return first

条件非常具体,但我包含了这个选项,因为当我 运行 具有一百万行的 DataFrame 上的这段代码花费了 .289 秒

现在,对于更广泛的情况,您可以运行像这样

df_user1=df.loc[df['user'] == 1] 
df_user2=df.loc[df['user'] == 2] 
df_user2=df_user2.rename(index=str, columns={"foo": "foo user 2", "bar": "bar user 2"})

new=df_user1.merge(df_user2,on=['day','place'])

运行 这对 450 万行花费了 3.8 秒,但这取决于有多少行是唯一的并且需要合并。我使用 运行dom 数字生成我的 DataFrame,所以可能要合并的数据较少。

方法 #1

这是一个基于 dimensionality-reduction 的 memory-efficiency 和 np.searchsorted 用于追溯并寻找两个用户数据之间的匹配项 -

# Extract array data for efficiency, as we will work NumPy tools
a = df.to_numpy(copy=False) #Pandas >= 0.24, use df.values otherwise
i = a[:,:3].astype(int)
j = a[:,3:].astype(bool)
# Test out without astype(int),astype(bool) conversions and see how they perform

# Get grouped scalars for Day and place headers combined
# This assumes that Day and Place data are positive integers
g = i[:,2]*(i[:,1].max()+1) + i[:,1]

# Get groups for user1,2 for original and grouped-scalar items
m1 = i[:,0]==1
uj1,uj2 = j[m1],j[~m1]
ui1 = i[m1]
u1,u2 = g[m1],g[~m1]

# Use searchsorted to look for matching ones between user-1,2 grouped scalars
su1 = u1.argsort()
ssu1_idx = np.searchsorted(u1,u2,sorter=su1)
ssu1_idx[ssu1_idx==len(u1)] = 0
ssu1_idxc = su1[ssu1_idx]

match_mask = u1[ssu1_idxc]==u2
match_idx = ssu1_idxc[match_mask]

# Select matching items off original table
p1,p2 = uj1[match_idx],uj2[match_mask]

# Setup output arrays
day_place = ui1[match_idx,1:]
user1_bools = p1
user2_bools = p2

方法 #1-扩展:通用 DayPlace dtype 数据

DayPlace 数据不一定是正整数时,我们可以扩展到一般情况。那样的话,我们可以利用dtype-combinedview-based的方法来执行data-redcution。因此,唯一需要的改变就是以不同的方式获得 g,这将是一个 view-based 数组类型,并且会像这样获得 -

#  @Divakar
def view1D(a): # a is array
    a = np.ascontiguousarray(a)
    void_dt = np.dtype((np.void, a.dtype.itemsize * a.shape[1]))
    return a.view(void_dt).ravel()

# Get grouped scalars for Day and place headers combined with dtype combined view
g = view1D(i[:,1:])

方法 #2

我们将使用 lex-sorting 以这样一种方式对数据进行分组,即在连续行中查找相同的元素会告诉我们两个用户之间是否存在匹配的元素。我们将从 Approach#1 re-use a,i,j。实施将是 -

# Lexsort the i table
sidx = np.lexsort(i.T)
# OR sidx = i.dot(np.r_[1,i[:,:-1].max(0)+1].cumprod()).argsort()

b = i[sidx]

# Get matching conditions on consecutive rows
m = (np.diff(b,axis=0)==[1,0,0]).all(1)
# Or m = (b[:-1,1] == b[1:,1]) & (b[:-1,2] == b[1:,2]) & (np.diff(b[:,0])==1)

# Trace back to original order by using sidx
match1_idx,match2_idx = sidx[:-1][m],sidx[1:][m]

# Index into relevant table and get desired array outputs
day_place,user1_bools,user2_bools = i[match1_idx,1:],j[match1_idx],j[match2_idx]

或者,我们可以使用 m 的扩展掩码来索引 sidx 并生成 match1_idx,match2_idx。其余代码保持不变。因此,我们可以做 -

from scipy.ndimage import binary_dilation

# Binary extend the mask to have the same length as the input.
# Index into sidx with it. Use one-off offset and stepsize of 2 to get
# user1,2 matching indices
m_ext = binary_dilation(np.r_[m,False],np.ones(2,dtype=bool),origin=-1)
match_idxs = sidx[m_ext]
match1_idx,match2_idx = match_idxs[::2],match_idxs[1::2]

方法 #3

这是另一个基于 Approach #2 并移植到 numba 的内存和性能。效率,我们将从 approach #1 -

re-use a,i,j
from numba import njit

@njit
def find_groups_numba(i_s,j_s,user_data,bools):
    n = len(i_s)
    found_iterID = 0
    for iterID in range(n-1):
        if i_s[iterID,1] == i_s[iterID+1,1] and i_s[iterID,2] == i_s[iterID+1,2]:
            bools[found_iterID,0] = j_s[iterID,0]
            bools[found_iterID,1] = j_s[iterID,1]
            bools[found_iterID,2] = j_s[iterID+1,0]
            bools[found_iterID,3] = j_s[iterID+1,1]
            user_data[found_iterID,0] = i_s[iterID,1]
            user_data[found_iterID,1] = i_s[iterID,2]        
            found_iterID += 1
    return found_iterID

# Lexsort the i table
sidx = np.lexsort(i.T)
# OR sidx = i.dot(np.r_[1,i[:,:-1].max(0)+1].cumprod()).argsort()

i_s = i[sidx]
j_s = j[sidx]

n = len(i_s)
user_data = np.empty((n//2,2),dtype=i.dtype)
bools = np.empty((n//2,4),dtype=j.dtype)    
found_iterID = find_groups_numba(i_s,j_s,user_data,bools)    
out_bools = bools[:found_iterID] # Output bool
out_userd = user_data[:found_iterID] # Output user-Day, Place data

如果输出必须有自己的内存空间,则在最后 2 步附加 .copy()。

或者,我们可以将索引操作卸载回 NumPy 端以获得更清洁的解决方案 -

@njit
def find_consec_matching_group_indices(i_s,idx):
    n = len(i_s)
    found_iterID = 0
    for iterID in range(n-1):
        if i_s[iterID,1] == i_s[iterID+1,1] and i_s[iterID,2] == i_s[iterID+1,2]:
            idx[found_iterID] = iterID
            found_iterID += 1            
    return found_iterID

# Lexsort the i table
sidx = np.lexsort(i.T)
# OR sidx = i.dot(np.r_[1,i[:,:-1].max(0)+1].cumprod()).argsort()

i_s = i[sidx]
j_s = j[sidx]

idx = np.empty(len(i_s)//2,dtype=np.uint64)
found_iterID = find_consec_matching_group_indices(i_s,idx)
fidx = idx[:found_iterID]
day_place,user1_bools,user2_bools = i_s[fidx,1:],j_s[fidx],j_s[fidx+1]

这是一个带有 set 交集的普通 pythonic 解决方案:

import numpy as np
import pandas as pd

user = np.array([1, 1, 1, 2, 2, 1], dtype=int)
day = np.array([10, 11, 11, 11, 12, 12], dtype=int)
place = np.array([5,8,9,9,1,2], dtype=int)
foo = np.array([1, 1, 1, 1, 0, 0], dtype=bool)
bar = np.array([0, 0, 0, 0, 1, 1], dtype=bool) 

# create a set of day/paces for user1
user1_dayplaces = { 
   (day[row_id], place[row_id])
   for row_id, user_id in enumerate(user)
   if user_id == 1
}

# create a set of day/paces for user2
user2_dayplaces = { 
   (day[row_id], place[row_id])
   for row_id, user_id in enumerate(user)
   if user_id == 2
}

# intersecting two sets to get the intended day/places
shared_dayplaces = user1_dayplaces & user2_dayplaces

# use day/places as a filter to get the intended row number
final_row_ids = [
   row_id
   for row_id, user_id in enumerate(user)
   if (day[row_id], place[row_id]) in shared_dayplaces
]

# filter the data with finalised row numbers to create the intended dataframe:
df = pd.DataFrame({
   'user':  user[final_row_ids],
   'day':   day[final_row_ids],
   'place': place[final_row_ids],
   'foo':   foo[final_row_ids],
   'bar':   bar[final_row_ids],
}, final_row_ids) # setting the index in this like is only for keeping the original index numbers.

结果df是:

   user  day  place   foo    bar
2     1   11      9  True  False
3     2   11      9  True  False

替代方法 - 通过['day','place']查找重复行,这将仅过滤常见的行。然后通过 'user' 做枢轴。更改列名并重建索引。

代码:

import pandas as pd
import numpy as np
user = np.array([1, 1, 1, 2, 2, 1], dtype=int)
day = np.array([10, 11, 11, 11, 12, 12], dtype=int)
place = np.array([5,8,9,9,1,2], dtype=int)
foo = np.array([1, 1, 1, 1, 0, 0], dtype=bool)
bar = np.array([0, 0, 0, 0, 1, 1], dtype=bool)

df = pd.DataFrame({
'user': user,
'day': day,
'place': place,
'foo': foo,
'bar': bar,
})

df1=df[df.duplicated(['day','place'],keep=False)]\
    .set_index(['day','place']).pivot(columns='user')
name = df1.columns.names[1]
df1.columns = ['{}{}_{}'.format(name, col[1], col[0]) for col in df1.columns.values]
df1 = df1.reset_index()

输出:

   day  place  user1_foo  user2_foo  user1_bar  user2_bar
0   11      9       True       True      False      False