替换 Pandas DataFrame 中作为字符串列表和整数列表包含的数字 ID

Replacing numerical IDs contained as lists of strings and lists of ints in a Pandas DataFrame

为了使数据匿名,我需要用一组不同的新 ID 替换原始 ID,但在替换后仍然有相同的原始 ID 在所有字段中匹配。挑战在于在这个 Pandas DataFrame 中优雅地跨 4 种不同的 ID 表示。

我有真实世界的数据,其中数字 ID 有 4 种可能的格式:

  1. 在字符串列表中 '["38", "15", "42"]'
  2. 在数字列表中 [14, 42, 94]
  3. 作为整数 42
  4. 作为浮动 1.0

这是一个包含所有 4 种数据类型的通用小型 DataFrame。

df = pd.DataFrame([['["38", "15", "42"]', [14, 42, 94], 42, 1.0],\
 ['["8", "28"]', [1, 4], 8, 94.0], ['["12"]', [12], 12, 12.0]],\
 columns = ['CommentsID','AgentID','CaseID','TicketID'])

df
| CommentsID            | AgentID         | CaseID  | TicketID |
| --------------------- | --------------- | ------- | -------- |
| ['38', '15', '42']    | [14, 42, 94]    | 42      | 1.0      |
| ['8', '28']           | [1, 4]          | 8       | 94.0     |
| ['12']                | [12]            | 12      | 12.0     |

为了便于在通用示例中使用,我只是添加 100 以生成 'new IDs' 的列表。 但是在实际问题中,对应的新ID列表是随机生成的,所以没有全程加100来解决这个问题。

orig_ids = list(range(100))
new_ids = [x + 100 for x in orig_ids]

我想要的是找到最有效的方法来用这四种数据类型的新 ID 替换数据框中的所有原始 ID。

目前我的最佳解决方案是分成三个部分:

  1. 使用 replace() 函数处理 float 和 int 版本(这不会影响列表,即使使用 regex=True):
df = df.replace(orig_ids, new_ids)
  1. 对于整数列表,使用远离 Pythonic 的双重 for 循环来匹配 ID 列表上的索引:
def newIDnumbers(datacol):
    newlist = []
    for i in range(len(datacol)):
        numlist = [orig_ids.index(x) for x in df.AgentID[i]]
        newlistrow = []
        for idx in range(len(numlist)):
            newlistrow.append(new_ids[numlist[idx]])
        newlist.append(newlistrow)
    return newlist

df.AgentID = newIDnumbers(df.AgentID)                     
df
  1. 对于字符串列表,构建原始 ID 和新 ID 的字符串列表,然后使用远离 Pythonic 的双重 for 循环来匹配 ID 列表上的索引:
str_orig_ids = [str(x) for x in orig_ids]
str_new_ids = [str(x) for x in new_ids]

def newIDstrings(datacol):
    newlist = []
    for i in range(len(datacol)):
        numlist = [str_orig_ids.index(x) for x in datacol.str.findall(r'"(\d*)"')[i]]
        newlistrow = []
        for idx in range(len(numlist)):
            newlistrow.append(str_new_ids[numlist[idx]])
        newlist.append(newlistrow)
    return newlist

df.CommentsID = [str(x) for x in newIDstrings(df.CommentsID)]
df       

有没有人有更优雅、计算量更小的方法来实现这个输出?

df
| CommentsID            | AgentID         | CaseID  | TicketID |
| --------------------- | --------------- | ------- | -------- |
| ['138', '115', '142'] | [114, 142, 194] | 142     | 100.0    |
| ['108', '128']        | [101, 104]      | 108     | 194.0    |
| ['112']               | [112]           | 112     | 112.0    |

如果加100就够了,

import json
df['CaseID'] = df['CaseID'] + 100
df['TicketID'] = df['TicketID'] + 100
df['AgentID'] = df['AgentID'].apply(lambda x: list(map(lambda y: y+100, x)))
df['CommentsID'] = df['CommentsID'].apply(lambda x: json.dumps(list(map(lambda y: str(int(y)+100), json.loads(x)))))

对于AgentIDCommentsIDSeries,我们可以用apply来一一应用变换。

对于AgentID,这个比较简单,因为是list的列,所以我们只需要将list中的每个整数映射到加法即可。

对于CommentsID,我们需要在开始的时候多一个步骤,通过json.loadsstr中的列表转换为python list,并且最后的另一个附加步骤是使用 json.dumps.

将 python list 转换回 str

这是使用重塑和 factorize 匿名化 ID 的一种方法:

from ast import literal_eval

def df_factorize(df):
    idx = df.index
    s = df.reset_index(drop=True).stack()  # stack to ensure consistent
    s[:] = s.factorize()[0]                # factors among all columns
    return s.unstack().set_index(idx)

df2 = (df
   .assign(CommentsID=df['CommentsID'].apply(literal_eval))   # extract as integers
   .explode(['CommentsID', 'AgentID'])                        # lists to rows
   .astype({'CommentsID': int}) # change str to int
   .pipe(df_factorize)                                        # anonymize
   .groupby(level=0)                                          # below to reshape
   .agg({'CommentsID': lambda s: str(list(s.astype(str))),    # to original form
         'AgentID': list,
         'CaseID': 'first',
         'TicketID': 'first',
        })
   .astype(df.dtypes)                                         # original dtypes
)

输出:

        CommentsID    AgentID  CaseID  TicketID
0  ['0', '4', '2']  [1, 2, 5]       2       3.0
1       ['6', '7']     [3, 8]       6       5.0
2            ['9']        [9]       9       9.0

如果您想要“真正的”匿名化,您可以使用 uuid:

from uuid import uuid4

def df_uuid(df):
    idx = df.index
    s = df.reset_index(drop=True).stack()
    f = s.unique()
    s = s.map(dict(zip(f, [str(uuid4()) for _ in range(len(f))])))
    return s.unstack().set_index(idx)

输出:

                                                                                                                 CommentsID                                                                                                             AgentID                                CaseID                              TicketID
0  ['e9fa80ed-58e2-4a96-a8da-0bc0c3a87e14', 'c87ca55a-e0a4-4618-ab5a-2eaf68d5a70f', '7cba72a8-3f2a-42e7-a781-b8ed242ea2ac']  [65f95f56-877e-4a9f-be88-4302fe1f77ea, 7cba72a8-3f2a-42e7-a781-b8ed242ea2ac, 3cf48cd7-e1b0-4c7f-bbeb-41836e309511]  7cba72a8-3f2a-42e7-a781-b8ed242ea2ac  549f1768-39b5-4e74-a8dd-4edbf3bb591f
1                                          ['6e54d7d6-ae2f-4319-950e-7629128d518a', '8fef2e1c-99d1-4460-b8a4-f4dbbf213c5f']                                        [549f1768-39b5-4e74-a8dd-4edbf3bb591f, 98f82da0-fa9c-447a-943b-b11875736128]  6e54d7d6-ae2f-4319-950e-7629128d518a  3cf48cd7-e1b0-4c7f-bbeb-41836e309511
2                                                                                  ['3396c9db-1244-4b2d-bb76-14bb0221778f']                                                                              [3396c9db-1244-4b2d-bb76-14bb0221778f]  3396c9db-1244-4b2d-bb76-14bb0221778f  3396c9db-1244-4b2d-bb76-14bb0221778f