在pandas/python中计数嵌套类别
目标: 获取一个总结表,显示在一个大类别下的小类别的数量。
例子: 我有一个数据表:
# initialize data of lists.
data = {'Name': ['Tom', 'Tom', 'Tom', 'jack', 'jack', 'Mary', 'Mary', 'Mary', 'Jim', 'Jim'],
'CEFR_level': ['A1', 'A2', 'PreA1', 'A2', 'A1','A1','B1','C1', 'A1', 'B1']}
# Create DataFrame
df = pd.DataFrame(data)
# I use this code to recode:
Easy = ["PreA1","A1", "A2"]
Medium =["B1", "B2"]
Hard = ["C1", "C2"]
data ['CEFR_categories'] = np.where(data['CEFR_level'].isin(Easy), 'Easy',
np.where(data['CEFR_level'].isin(Medium), 'Medium',
np.where(data ['CEFR_level'].isin(Hard), 'Hard', 'Other')))
我成功地创建了一个名为 data ['CEFR_categories'] 的列,并正确地将其分类为简单、中等和困难。
我现在的问题是关于分组。
任务: 完成 我想把 X、Y 和 Z 重新编码为简单、中等和困难。
然后我想通过组合类别来进行分组。例如,新的简单类别出现了 2 次(Tom 的 CEFR_level 是 'A1'、'A2' 和 'PreA1',而 Jack 的 CEFR_level 是 A1 和 A2)。简单-中等-困难(出现 1 次,Mary 的 CEFR_level 组合不同,因此被重新编码为简单、中等和困难),简单-中等出现 1 次,Jim 的情况也是如此。
我花了好几个小时尝试重新编码,我可以在另一列中重新编码,但第一行只有 1 个类别(例如)简单。(用上面的代码)
我的输出应该是这样的:
我该如何进行分组呢?
谢谢你的帮助
编辑和更新
我使用了 @Timeless 的回答,得到了以下输出:
有什么建议吗?我真实数据的前 4 行的 cat1 是:简单、简单、简单、中等。这将导致一个简单-中等的结果。
但输出却说没有。
最终答案
Timeless 的这段代码也有效。
cats = sorted(testlet_item_bank["CEFR_categories"].unique())
#status = dict(zip(cats, ["Easy", "Medium", "Hard"])) # this was mixing categories
ps = list(map("-".join, powerset(cats)))[1:]
out = (
testlet_item_bank # the first chain can be optional
.astype({"CEFR_categories": pd.CategoricalDtype(cats, ordered=True)})
.groupby("TestletID")["CEFR_categories"]
.agg(lambda x: "-".join(pd.unique(x.sort_values())))
.value_counts()
.reindex(ps, fill_value=0)
.rename_axis("Categories")
.reset_index(name="Counts")
# .replace(status, regex=True) this mixes categories
)
2 个回答
1
这个方法相对简单,但要提醒你,在处理大量数据时,性能可能会不太理想:
import pandas as pd
data = {'Name': ['Tom', 'Tom', 'Tom', 'jack', 'jack', 'Mary', 'Mary', 'Mary', 'Jim', 'Jim'],
'Cat1': ['X', 'X', 'X', 'X', 'X','X','Y','Z', 'X', 'Y']}
df = pd.DataFrame(data)
# First, sort by name then `Cat1` value to maintain the eventual ordering of `Easy`/`Medium`/`Hard`
df.sort_values(['Name', 'Cat1'], ignore_index=True, axis=0)
# De-dupe rows
df = df.drop_duplicates()
# Map X, Y, Z to Easy, Medium, Hard
df['Cat1'] = df['Cat1'].replace(['X', 'Y', 'Z'], ['Easy', 'Medium', 'Hard'])
# Roll up levels grouped by unique Name value
df = df.groupby('Name').agg({'Cat1': '-'.join})
# Rename Cat1 column to 'counts' to match spec
df = df.rename(columns={"Cat1": "counts"})
# Get value_counts() of resulting `counts` column
return_value = df.value_counts('counts')
结果:
counts
Easy 2
Easy-Medium 1
Easy-Medium-Hard 1
Name: count, dtype: int64
这里的结果不包括数量为0的类别组合,不过如果你真的需要这个,可以很容易地加上去。
4
使用 value_counts
或 powerset
的时候:
from more_itertools import powerset
mapper = {
"Easy": ["PreA1", "A1", "A2"],
"Medium": ["B1", "B2"],
"Hard": ["C1", "C2"],
}
status = {v: k for k,lst_v in mapper.items() for v in lst_v}
df["CEFR_level"] = (
df["CEFR_level"].map(status).fillna("Other")
.astype(pd.CategoricalDtype(list(mapper) + ["Other"], ordered=True))
)
ps = list(map("-".join, powerset(mapper)))[1:]
out = (
df # the first chain can be optional
.groupby("Name")["CEFR_level"]
.agg(lambda x: "-".join(pd.unique(x.sort_values())))
.value_counts()
.reindex(ps, fill_value=0)
.rename_axis("Categories")
.reset_index(name="Counts")
)
注意:如果你无法安装 more_itertools
,可以使用 文档中的这个方法。
输出结果:
Categories Counts
0 Easy 2
1 Medium 0
2 Hard 0
3 Easy-Medium 1
4 Easy-Hard 0
5 Medium-Hard 0
6 Easy-Medium-Hard 1
[7 rows x 2 columns]