将 dict 构造函数转换为 Pandas MultiIndex 数据帧

Posted

技术标签:

【中文标题】将 dict 构造函数转换为 Pandas MultiIndex 数据帧【英文标题】:Convert dict constructor to Pandas MultiIndex dataframe 【发布时间】:2018-08-14 08:41:38 【问题描述】:

我想在 Pandas 数据框中构建大量数据。但是,我需要一个多索引格式。 Pandas MultiIndex 功能一直让我感到困惑,而且这次我无法理解它。

我构建了我想要的结构作为字典,但是因为我的实际数据要大得多,所以我想改用 Pandas。下面的代码是dict 变体。请注意,原始数据还有更多的标签和更多的行。

想法是原始数据包含索引为Task_n 的任务的行,该任务已由索引为Participant_n 的参与者执行。每个 是一个段。即使原始数据没有这种区别,我也想将其添加到我的数据框中。换句话说:

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"segi"] = 
            '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,它是一个任意布尔标志。生成的 dict 看起来像这样(未美化以节省空间。如果要检查,请在文本编辑器中以 JSON 或 Python dict 格式打开并美化):

'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   

我遇到的主要问题是,如果我事先不知道维度,我不明白如何构建多索引 df。我事先不知道会有多少任务或段。所以我很确定我可以保留我最初的dict 方法的循环结构,我想我必须附加/连接到一个初始的空DataFrame,但问题是结构必须看起来像什么。它不能是一个简单的系列,因为它没有考虑多索引。那怎么办?

对于那些已经读到这里并想尝试一下的人,我认为我的原始代码大部分可以重复使用(循环和变量赋值),但它必须代替 dict成为 DataFrame 的访问者。导入方面:数据应该易于使用 getter/setter 读取,就像常规 DataFrame 一样。例如。应该很容易获得参与者 2、任务 2、段 2 等的持续时间值。而且,获取数据的子集(例如 prof === 0 的位置)应该没有问题。

【问题讨论】:

你能否详细说明一下这个说法“我不知道会有多少任务或段”。您在提供解决方案后添加了此内容。但是,提供的答案涵盖了这一点,因为他们使用了 groupby 操作,所以我不确定在回答这个问题时还需要解决什么 【参考方案1】:

我唯一的建议是摆脱所有字典内容。所有这些代码都可以毫不费力地用 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? 我以为我可以做到df.loc[("Task_n", 2), :],但我收到了level type mismatch 错误。此外,这不允许我选择范围。我可以将 loc 与 idx (IndexSlice) 一起使用,但这既丑陋又烦人,因为您似乎无法使用 col 名称?例如获取参与者 1 的所有任务 3:df.loc[idx[1, :, :, 3], :]。我希望有像df.loc[(df.Participant_n == 1 &amp; df.Task_n == 3] 这样的东西。是否存在适用于您的代码的类似内容? df.query() 似乎可用于 getting 值,但我怎样才能轻松地以这种方式添加值?例如。在 Task_n == 2 下为 Participant_n == 1 添加一个值? ***.com/questions/23108889/…【参考方案2】:

我在数据表示方面遇到了类似的问题,并为 groupby 提供了以下带有小计的辅助函数。

通过此过程,可以为任意数量的 group by 列生成小计,但输出数据具有不同的格式。每个小计不会将小计放在自己的列中,而是会在数据框中添加额外的一行。

对于交互式数据探索和分析,我发现这非常有用,因为只需几行代码即可获得小计

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's answer恢复数据框创建代码

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

【讨论】:

以上是关于将 dict 构造函数转换为 Pandas MultiIndex 数据帧的主要内容,如果未能解决你的问题,请参考以下文章

将dict的dict转换为pandas中的数据框

将 pandas.groupby 转换为 dict

将dict的dict转换为pandas DataFrame - 内存问题

将dict列表转换为pandas中的行[重复]

将json dict转换为pandas df中的行

如何将Pandas中的非零条目转换为带有列表的dict?