在pandas/python中计数嵌套类别

1 投票
2 回答
70 浏览
提问于 2025-04-12 14:55

目标: 获取一个总结表,显示在一个大类别下的小类别的数量。

例子: 我有一个数据表:

# 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_countspowerset 的时候:

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]

撰写回答