替换 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 种可能的格式:
- 在字符串列表中
'["38", "15", "42"]'
- 在数字列表中
[14, 42, 94]
- 作为整数
42
- 作为浮动
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。
目前我的最佳解决方案是分成三个部分:
- 使用
replace()
函数处理 float 和 int 版本(这不会影响列表,即使使用 regex=True
):
df = df.replace(orig_ids, new_ids)
- 对于整数列表,使用远离 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
- 对于字符串列表,构建原始 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)))))
对于AgentID
和CommentsID
Series
,我们可以用apply
来一一应用变换。
对于AgentID
,这个比较简单,因为是list
的列,所以我们只需要将list
中的每个整数映射到加法即可。
对于CommentsID
,我们需要在开始的时候多一个步骤,通过json.loads
将str
中的列表转换为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
为了使数据匿名,我需要用一组不同的新 ID 替换原始 ID,但在替换后仍然有相同的原始 ID 在所有字段中匹配。挑战在于在这个 Pandas DataFrame 中优雅地跨 4 种不同的 ID 表示。
我有真实世界的数据,其中数字 ID 有 4 种可能的格式:
- 在字符串列表中
'["38", "15", "42"]'
- 在数字列表中
[14, 42, 94]
- 作为整数
42
- 作为浮动
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。
目前我的最佳解决方案是分成三个部分:
- 使用
replace()
函数处理 float 和 int 版本(这不会影响列表,即使使用regex=True
):
df = df.replace(orig_ids, new_ids)
- 对于整数列表,使用远离 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
- 对于字符串列表,构建原始 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)))))
对于AgentID
和CommentsID
Series
,我们可以用apply
来一一应用变换。
对于AgentID
,这个比较简单,因为是list
的列,所以我们只需要将list
中的每个整数映射到加法即可。
对于CommentsID
,我们需要在开始的时候多一个步骤,通过json.loads
将str
中的列表转换为python list
,并且最后的另一个附加步骤是使用 json.dumps
.
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