将成对列表转换为尽可能小的 DataFrame 表示形式?

Converting a list of pairs into smallest possible DataFrame representation?

我有一个配对项列表,我想将它们转换成 pandas DataFrame,其中每个配对项在同一列中共享相同的数字。所以像这样:

[('A', 'B'),
('A', 'C'),
('B', 'D')]

转换为...

  0  1
A 2  1
B 3  1
C 2  0
D 3  0

因此列按编码对数递减排列,并且使用尽可能少的列。

是否有算法,最好是 numpy 或 pandas 中的算法,可以做到这一点?到目前为止,我一直找不到关于 Google 的任何内容,但自从我学习线性代数以来已经有一段时间了,所以我可能只是忘记了正确使用的术语。

我创建了以下(有问题的)代码来创建 DataFrame,但出于某种原因,它创建了与对一样多的列,这不是我想要完成的。

def create_df(ps):
    df = pd.DataFrame(index=np.unique(ps))
    cnt = 1
    for p in ps:
        col = 0
        a, b = p
        while col in df.columns and (df.at[a, col] != 0 or df.at[b, col] != 0):
            col += 1
        df.loc[a, col] = cnt
        df.loc[b, col] = cnt
        cnt += 1
    return df

这样做的最终目标是将输出集成到数据管道中,这样我就可以在 pandas 中使用 groupby 来计算对的统计数据。因此,每对必须在同一列中定义,如示例中所示。

这更像是一个 pivot 问题,我们做了 melt

s=pd.DataFrame(l).reset_index().melt('index')
s=s.assign(Col=s.groupby('value').cumcount()).pivot('value','Col','index').\
    add(1).fillna(0)
s
Out[62]: 
Col      0    1
value          
A      1.0  2.0
B      3.0  1.0
C      2.0  0.0
D      3.0  0.0

这是一种使用图形的稀疏矩阵表示的 numpy/scipy 方法。

import numpy as np
from numpy.lib.stride_tricks import as_strided
from scipy import sparse
import pandas as pd

def rerepr_grph(g):
    vtx,edg = np.unique(g,return_inverse=True)
    npr,nvx = edg.size//2,vtx.size
    aux = sparse.csr_matrix(
        (np.ones(2*npr),edg,2*np.arange(npr+1)),(npr,nvx)).tocsc()
    deg = np.diff(aux.indptr)
    srt = (-deg).argsort(kind="stable")
    mxdg = deg[srt[0]]
    rlr = np.concatenate([aux.indices+1,np.zeros(mxdg,np.int32)])
    rlr = as_strided(rlr,(2*npr,mxdg),2*rlr.strides)
    szgrps = np.diff(deg.searchsorted(
        np.arange(mxdg+1),"right",sorter=srt[::-1]),axis=0)[::-1]
    triud = np.array([True,False]).repeat((mxdg,mxdg-1))
    triud = as_strided(triud,(mxdg,mxdg),2*triud.strides)
    msk = triud.repeat(szgrps,axis=0)
    res = np.where(msk,rlr[aux.indptr[srt]],0)
    return pd.DataFrame(res,index=vtx[srt])

def rerepr_pd(l):
    s=pd.DataFrame(l).reset_index().melt('index')
    s=s.assign(Col=s.groupby('value').cumcount()).pivot('value','Col','index').\
                    add(1).fillna(0)
    return s

data = [('A', 'B'),
        ('A', 'C'),
        ('B', 'D')]

print(rerepr_grph(data))
print(rerepr_pd(data))

示例 运行(打印 numpy 和 pandas(@WeNYoBen 的代码)对 OP 示例的回答:

   0  1
A  1  2
B  1  3
C  2  0
D  3  0
Col      0    1
value          
A      1.0  2.0
B      3.0  1.0
C      2.0  0.0
D      3.0  0.0

大型 100 万对示例的速度比较:

# larger example

print()
print("creating larger (1,000,000 pairs) example",end=" ... ")

import itertools as it
from timeit import timeit
A,Z = np.uint32(ord("A")),np.uint32(ord("0"))
data = (np.stack(np.unravel_index(np.random.choice(2600**2,1_000_000,replace=False),(26,10,10,26,10,10)),axis=1).astype("u4") + (A,Z,Z,A,Z,Z)).view('U3')
print("done")
print("benchmarking")
print("numpy ",timeit(lambda:rerepr_grph(data),number=10)*100,"ms")
print("pandas",timeit(lambda:rerepr_pd(data),number=10)*100,"ms")

样本运行:

creating larger (1,000,000 pairs) example ... done
benchmarking
numpy  560.810615005903 ms
pandas 1843.7980080023408 ms
>>> df = pd.DataFrame([('A', 'B'), ('A', 'C'), ('B', 'D')])

首先找到数据框中的所有唯一值:

>>> uniqs = list(pd.unique(df.values.ravel()))
>>> uniqs
['A', 'B', 'C', 'D']

接下来获取出现这些唯一值的索引列表(按相反顺序),将它们添加到字典并从该字典构建数据框:

dict = {}
for uniq in uniqs:
    dict[uniq] = list(reversed(df[df.eq(uniq).any(1)].index + 1))
dff = pd.DataFrame({key: pd.Series(value) for key, value in dict.items()}).T

结果:

>>> dff
     0    1
A  2.0  1.0
B  3.0  1.0
C  2.0  NaN
D  3.0  NaN

这是一个疯狂的解决方案,仅供娱乐:

s = df.stack().reset_index(name='val')

s = (s.assign(level_1=s.duplicated(['level_1','val'])
                   .groupby(s['level_0'])
                   .transform('max')
                   .add(s['level_1']) % 2,
              level_0 = s.level_0 + 1
             )
      .pivot_table(index='val', 
                   columns='level_1', 
                   values='level_0',
                   fill_value=0)
      )

s.iloc[:,::-1] = np.sort(s, axis=1)

输出:

level_1  0  1
val          
A        2  1
B        3  1
C        2  0
D        3  0

我最终在场外找到了适合我的代码,这里是:

def encode_pairs_info(pair_list):
    '''
    Encode the pair information in multiple columns group_i.

    Parameters
    ----------
    pair_list: list
    - Example:
        pair_list: [('A', 'B'), ('B', 'C')]
    Returns
    -------
    df: pd.DataFrame
    - Example:
            group_1 group_2
        A      1      0
        B      1      2
        C      0      2
    '''

    temp = pd.DataFrame(columns=['pair', 'pair_num', 'num', 'group'])

    for ipair in range(len(pair_list)):
        num_valid_1 = temp[temp.pair == pair_list[ipair][0]]
        num_valid_2 = temp[temp.pair == pair_list[ipair][1]]

        if num_valid_1.shape[0] == 0:
            num_temp_1 = [0]
        else:
            num_temp_1 = num_valid_1.num

        if num_valid_2.shape[0] == 0:
            num_temp_2 = [0]
        else:
            num_temp_2 = num_valid_2.num

        num_temp = min(set(range(1, ipair + 2, 1)) - set(num_temp_1) - set(num_temp_2))
        temp = temp.append({'ticker': pair_list[ipair][0],
                        'pair': ipair + 1,
                        'num': num_temp,
                        'group': 'group_' + str(num_temp)},
                       ignore_index = True)
        temp = temp.append({'pair': pair_list[ipair][1],
                        'pair_num': ipair + 1,
                        'num': num_temp,
                        'group': 'group_' + str(num_temp)},
                       ignore_index = True)

    df = pd.DataFrame(0,
                      index=temp.ticker.unique(),
                      columns=temp.group.unique())

    for irow in range(temp.shape[0]):
        df.loc[temp.ticker[irow]][temp.group[irow]] = temp.pair_num[irow]

    return df

输入:[('A', 'B'), ('A', 'C'), ('B', 'C')] 结果:

    group_1 group_2 group_3
A   1   2   0
B   1   0   3
C   0   2   3