将字典构造器转换为Pandas MultiIndex数据框架

8
我有很多数据想要在Pandas dataframe中进行结构化。但是,我需要一个多重索引格式。Pandas MultiIndex功能总是让我感到困惑,这一次也不例外。
我按照自己的想法将结构建成了一个字典,但是由于我的实际数据要大得多,所以我想使用Pandas。下面的代码是字典变量。请注意,原始数据具有更多的标签和更多的行。
原始数据包含由索引Task_n执行的任务的行,该任务由索引Participant_n的参与者执行。每一行都是一个片段。尽管原始数据没有这种区分,但我想将其添加到我的dataframe中。换句话说:
Participant_n | Task_n | val | dur
----------------------------------
            1 |      1 |  12 |   2
            1 |      1 |   3 |   4
            1 |      1 |   4 |  12
            1 |      2 |  11 |  11
            1 |      2 |  34 |   4

上面的示例包含一个参与者,两个任务,分别具有三个两个片段(行)。

在Python中,使用dict结构可以看作是这样的:

import pandas as pd

cols = ['Participant_n', 'Task_n', 'val', 'dur']

data = [[1,1,25,83],
        [1,1,4,68],
        [1,1,9,987],
        [1,2,98,98],
        [1,2,84,4],
        [2,1,9,21],
        [2,2,15,6],
        [2,2,185,6],
        [2,2,18,4],
        [2,3,8,12],
        [3,1,7,78],
        [3,1,12,88],
        [3,2,12,48]]

d = pd.DataFrame(data, columns=cols)

part_d = {}
for row in d.itertuples():
    participant_n = row.Participant_n
    participant = "participant" + str(participant_n)
    task = "task" + str(row.Task_n)

    if participant in part_d:
        part_d[participant]['all_sum']['val'] += int(row.val)
        part_d[participant]['all_sum']['dur'] += int(row.dur)
    else:
        part_d[participant] = {
            'prof': 0 if participant_n < 20 else 1,
            'all_sum': {
                'val': int(row.val),
                'dur': int(row.dur),
            }
        }

    if task in part_d[participant]:
        # Get already existing keys
        k = list(part_d[participant][task].keys())

        k_int = []
        # Only get the ints (i.e. not all_sum etc.)
        for n in k:
            # Get digit from e.g. seg1
            n = n[3:]
            try:
                k_int.append(int(n))
            except ValueError:
                pass

        # Increment max by 1
        i = max(k_int) + 1
        part_d[participant][task][f"seg{i}"] = {
            'val': int(row.val),
            'dur': int(row.dur),
        }
        part_d[participant][task]['task_sum']['val'] += int(row.val)
        part_d[participant][task]['task_sum']['dur'] += int(row.dur)
    else:
        part_d[participant][task] = {
            'seg1': {
                'val': int(row.val),
                'dur': int(row.dur),
            },
            'task_sum': {
                'val': int(row.val),
                'dur': int(row.dur),
            }
        }

print(part_d)

最终结果中,我还有一些额外的变量,例如:task_sum(参与者任务总和),all_sum(参与者所有操作的总和)以及prof,它是一个任意的布尔标志。生成的字典看起来像这样(为节省空间未美化。如果您想检查,请在文本编辑器中打开为JSON或Python字典并美化):

{'participant1': {'prof': 0, 'all_sum': {'val': 220, 'dur': 1240}, 'task1': {'seg1': {'val': 25, 'dur': 83}, 'task_sum': {'val': 38, 'dur': 1138}, 'seg2': {'val': 4, 'dur': 68}, 'seg3': {'val': 9, 'dur': 987}}, 'task2': {'seg1': {'val': 98, 'dur': 98}, 'task_sum': {'val': 182, 'dur': 102}, 'seg2': {'val': 84, 'dur': 4}}}, 'participant2': {'prof': 0, 'all_sum': {'val': 235, 'dur': 49}, 'task1': {'seg1': {'val': 9, 'dur': 21}, 'task_sum': {'val': 9, 'dur': 21}}, 'task2': {'seg1': {'val': 15, 'dur': 6}, 'task_sum': {'val': 218, 'dur': 16}, 'seg2': {'val': 185, 'dur': 6}, 'seg3': {'val': 18, 'dur': 4}}, 'task3': {'seg1': {'val': 8, 'dur': 12}, 'task_sum': {'val': 8, 'dur': 12}}}, 'participant3': {'prof': 0, 'all_sum': {'val': 31, 'dur': 214}, 'task1': {'seg1': {'val': 7, 'dur': 78}, 'task_sum': {'val': 19, 'dur': 166}, 'seg2': {'val': 12, 'dur': 88}}, 'task2': {'seg1': {'val': 12, 'dur': 48}, 'task_sum': {'val': 12, 'dur': 48}}}}

我希望这个结果不是一个字典,而是一个包含多个索引的pd.DataFrame,看起来像下面的表示方式或类似的方式。(为了简单起见,我只使用了索引,而没有使用task1seg1。)

Participant   Prof all_sum      Task    Task_sum     Seg   val   dur
                   val    dur           val    dur
====================================================================
participant1  0    220   1240      1     38   1138     1    25    83
                                                       2     4    68
                                                       3     9   987
                                   2    182    102     1    98    98
                                                       2    84     4
--------------------------------------------------------------------
participant2  0    235     49      1      9     21     1     9    21
                                   2    218     16     1    15     6
                                                       2   185     6
                                                       3    18     4
                                   3      8     12     1     8    12
--------------------------------------------------------------------
participant3  0     31    214      1     19    166     1     7    78
                                                       2    12    88
                                   2     12     48     1    12    48
这种结构在Pandas中是否可行?如果不行,有哪些合理的替代方案?

再次强调,在现实中还有更多的数据和可能存在更多的子级别。因此,解决方案必须是灵活的,并且高效的。如果只在一个轴上拥有多重索引,并将标题更改为可以使问题简化很多的话,我愿意尝试。

Participant  Prof  all_sum_val  all_sum_dur  Task  Task_sum_val  Task_sum_dur  Seg   

我遇到的主要问题是我不知道如何在不提前知道维度的情况下构建多索引数据框。 我不知道会有多少个任务或段落。 所以我相信我可以保留最初的字典方法中的循环结构,然后我想我必须将其附加/连接到一个初始空数据帧中,但问题是其结构必须是什么样子的。 它不能是简单的Series,因为没有考虑到多索引。 那么怎么办呢?
对于那些已经阅读到这里并想尝试的人,我认为我的原始代码大部分可以重新使用(循环和变量赋值),但它必须是DataFrame的访问器而不是字典。 一个重要的方面是:数据应该可以像常规DataFrame一样使用getter / setter轻松阅读。 例如,轻松获取参与者二,任务2,段落2等的持续时间值。但是,获取数据子集(例如,其中prof === 0)也应该没有问题。

你能详细说明一下这个语句“我事先不知道会有多少个任务或段落”吗?你是在得到解决方案之后添加的。然而,提供的答案已经涵盖了这一点,因为他们使用了groupby操作,所以我不确定还需要回答这个问题时需要额外解决什么。 - DJK
2个回答

6

我唯一的建议是摆脱你所有的字典内容。所有这些代码都可以在Pandas中重新编写,而不需要太多的努力。这可能会加快转换过程,但需要一些时间。为了帮助您完成这个过程,我已经重新编写了您提供的部分。其余部分由您决定。

import pandas as pd

cols = ['Participant_n', 'Task_n', 'val', 'dur']

data = [[1,1,25,83],
        [1,1,4,68],
        [1,1,9,987],
        [1,2,98,98],
        [1,2,84,4],
        [2,1,9,21],
        [2,2,15,6],
        [2,2,185,6],
        [2,2,18,4],
        [2,3,8,12],
        [3,1,7,78],
        [3,1,12,88],
        [3,2,12,48]]

df = pd.DataFrame(data, columns=cols)
df["Task Sum val"] = df.groupby(["Participant_n","Task_n"])["val"].transform("sum")
df["Task Sum dur"] = df.groupby(["Participant_n","Task_n"])["dur"].transform("sum")
df["seg"] =df.groupby(["Participant_n","Task_n"]).cumcount() + 1
df["All Sum val"] = df.groupby("Participant_n")["val"].transform("sum")
df["All Sum dur"] = df.groupby("Participant_n")["dur"].transform("sum")
df = df.set_index(["Participant_n","All Sum val","All Sum dur","Task_n","Task Sum val","Task Sum dur"])[["seg","val","dur"]]
df = df.sort_index()
df

输出

                                                                        seg  val  dur
Participant_n All Sum val All Sum dur Task_n Task Sum val Task Sum dur               
1             220         1240        1      38           1138            1   25   83
                                                          1138            2    4   68
                                                          1138            3    9  987
                                      2      182          102             1   98   98
                                                          102             2   84    4
2             235         49          1      9            21              1    9   21
                                      2      218          16              1   15    6
                                                          16              2  185    6
                                                          16              3   18    4
                                      3      8            12              1    8   12
3             31          214         1      19           166             1    7   78
                                                          166             2   12   88
                                      2      12           48              1   12   48

尝试运行此代码并告知我您的想法。有任何问题,请在评论中提出。

我想我明白了,因为这些值确实是多索引,意味着它们对于多个行是相同的。明白了。但是我怎么访问它们呢?比如说,我想要一个切片,其中 Task_n == 2 的行? - Bram Vanroy
我原以为可以使用 df.loc[("Task_n", 2), :],但是出现了 level type mismatch 错误。此外,这种方法无法选择一个范围。我可以使用 loc 和 idx(IndexSlice)一起使用,但是这样做很丑陋和烦人,因为似乎无法使用列名?例如,要获取参与者1的所有任务3:df.loc[idx[1, :, :, 3], :]。我希望有像 df.loc[(df.Participant_n == 1 & df.Task_n == 3] 这样的东西存在,能够在你的代码中运行吗? - Bram Vanroy
df.query() 似乎可用于获取值,但我如何轻松地以这种方式添加值呢?例如,在 Task_n == 2 下为 Participant_n == 1 添加一个值? - Bram Vanroy
https://dev59.com/zGAg5IYBdhLWcg3w1t_y - Gabriel A

2
我在数据展示方面遇到了类似的问题,并编写了以下帮助函数来进行分组并生成小计。使用这个过程,可以为任意数量的分组列生成小计,但输出数据的格式不同。每个小计都会添加一个额外的行到数据框中,而不是将小计放置在它们自己的列中。对于交互式数据探索和分析,我发现这非常有帮助,因为只需几行代码就能获取子总计。
def get_subtotals(frame, columns, aggvalues, subtotal_level):

    if subtotal_level == 0:
        return frame.groupby(columns, as_index=False).agg(aggvalues)

    elif subtotal_level == len(columns):
        return pd.DataFrame(frame.agg(aggvalues)).transpose().assign(
            **{c: np.nan  for i, c in enumerate(columns)}
        )

    return frame.groupby(
        columns[:subtotal_level],
        as_index=False
    ).agg(aggvalues).assign(
        **{c: np.nan for i, c in enumerate(columns[subtotal_level:])}
    )

def groupby_with_subtotals(frame, columns, aggvalues, grand_totals=False, totals_position='last'):
    gt = 1 if grand_totals else 0
    out = pd.concat(   
        [get_subtotals(df, columns, aggvalues, i)
         for i in range(len(columns)+gt)]
     ).sort_values(columns, na_position=totals_position)
    out[columns] = out[columns].fillna('total')
    return out.set_index(columns)

Gabriel A的回答中重用数据框创建代码

cols = ['Participant_n', 'Task_n', 'val', 'dur']

data = [[1,1,25,83],
        [1,1,4,68],
        [1,1,9,987],
        [1,2,98,98],
        [1,2,84,4],
        [2,1,9,21],
        [2,2,15,6],
        [2,2,185,6],
        [2,2,18,4],
        [2,3,8,12],
        [3,1,7,78],
        [3,1,12,88],
        [3,2,12,48]]

df = pd.DataFrame(data, columns=cols)

首先需要添加seg列。

df['seg'] = df.groupby(['Participant_n', 'Task_n']).cumcount() + 1

然后我们可以像这样使用groupby_with_subtotals。此外,请注意,您可以将小计放在顶部,并通过传递grand_totals=True, totals_position='first'来包括grand_totals。

groupby_columns = ['Participant_n', 'Task_n', 'seg']
groupby_aggs = {'val': 'sum', 'dur': 'sum'}
aggdf = groupby_with_subtotals(df, groupby_columns, groupby_aggs)
aggdf
# outputs

                             dur  val
Participant_n Task_n seg
1             1.0    1.0      83   25
                     2.0      68    4
                     3.0     987    9
                     total  1138   38
              2.0    1.0      98   98
                     2.0       4   84
                     total   102  182
              total  total  1240  220
2             1.0    1.0      21    9
                     total    21    9
              2.0    1.0       6   15
                     2.0       6  185
                     3.0       4   18
                     total    16  218
              3.0    1.0      12    8
                     total    12    8
              total  total    49  235
3             1.0    1.0      78    7
                     2.0      88   12
                     total   166   19
              2.0    1.0      48   12
                     total    48   12
              total  total   214   31

在这里,小计行标有total,最左边的total表示小计级别。
创建聚合数据框后,可以使用loc访问小计。例如:
aggdf.loc[1,'total','total']
# outputs:
dur    1240
val     220
Name: (1, total, total), dtype: int64

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接