代码之家  ›  专栏  ›  技术社区  ›  bismo

如何使用Pandas Styler根据列的值对行组进行不同的样式设置

  •  1
  • bismo  · 技术社区  · 2 年前

    我试图实现的目标:

    我有以下数据帧, df :

    data = {'person': {0: 'a',
      1: 'a',
      2: 'a',
      3: 'a',
      4: 'a',
      5: 'a',
      6: 'b',
      7: 'b',
      8: 'b',
      9: 'b',
      10: 'b',
      11: 'b',
      12: 'c',
      13: 'c',
      14: 'c',
      15: 'c',
      16: 'c',
      17: 'c'},
     'x': {0: 1,
      1: 1,
      2: 1,
      3: 1,
      4: 1,
      5: 1,
      6: 1,
      7: 1,
      8: 1,
      9: 1,
      10: 1,
      11: 1,
      12: 1,
      13: 1,
      14: 1,
      15: 1,
      16: 1,
      17: 1},
     'y': {0: 2,
      1: 2,
      2: 2,
      3: 2,
      4: 2,
      5: 2,
      6: 2,
      7: 2,
      8: 2,
      9: 2,
      10: 2,
      11: 2,
      12: 2,
      13: 2,
      14: 2,
      15: 2,
      16: 2,
      17: 2},
     'z': {0: 'foo',
      1: 'foo',
      2: 'foo',
      3: 'bar',
      4: 'bar',
      5: 'bar',
      6: 'foo',
      7: 'foo',
      8: 'foo',
      9: 'bar',
      10: 'bar',
      11: 'bar',
      12: 'foo',
      13: 'foo',
      14: 'foo',
      15: 'bar',
      16: 'bar',
      17: 'bar'}}
    
    df = pd.DataFrame.from_dict(data, orient='columns')
    

    我想根据以下值对行组进行不同的样式设置(使用不同的交替颜色集) z 对于每个值 person .


    我想要的输出:

    enter image description here


    我所尝试的:

    最初,我想我可以使用一个嵌套循环来分解每个 z 对于每一个 。我最初试着只测试一个 ,就像这样:

    
    COLORS = {
         'foo':['red','green'],
         'bar':['blue','yellow']
    }
    
    test = df.loc[df.person=='a'].copy()
    
    sub_person = pd.DataFrame()
    
    for val in test.z.unique():
         i_test = test.loc[test.z==val].copy()
         c1 = COLORS[val][0]
         c2 = COLORS[val][-1]
         css_alt_rows = f'background-color: {c1}; color: {c2};'
    
         i_test = (i_test.style.apply(lambda col: np.where(col.index % 2, css_alt_rows,None)))
    
         sub_person = pd.concat([sub_person,i_test])
    

    我认为这是一个单独处理不同样式的聪明解决方案,但我遇到了错误:

    TypeError: cannot concatenate object of type '<class 'pandas.io.formats.style.Styler'>'; only Series and DataFrame objs are valid
    

    因此,事实证明,这段代码无法工作,因为您无法连接Styler对象。

    接下来,我尝试了一种类似的策略,将lambda函数嵌套在另一个函数中 np.where() 有条件的:

    COLORS = {
         'foo':['red','green'],
         'bar':['blue','yellow']
    }
    
    test = df.loc[df.person=='a'].copy()
    
    for val in test.z.unique():
    
         c1 = COLORS[val][0]
         c2 = COLORS[val][-1]
         css_alt_rows = f'background-color: {c1}; color: {c2};'
    
         test = (test.style.apply(lambda col: np.where(np.where(col.index % 2, css_alt_rows,None),None)))
    
    

    但我得到以下错误:

    AttributeError: 'Styler' object has no attribute 'style'
    

    这是有道理的,因为在循环的第一次迭代之后, test 是一个样式器对象,其结果为 test.style 在下一次迭代中产生错误。


    那么,我该如何为每个应用这些样式呢 z 对于每一个 ?

    此外,我如何在每行的最后一行添加底部边框 无法单独设置分组样式并将其连接起来?

    注:是的,颜色 colors 与图像不匹配,这很好。

    1 回复  |  直到 2 年前
        1
  •  2
  •   mozway    2 年前

    我会使用自定义函数 style.apply axis=None 和帮助 groupby.transform :

    COLORS = {
         'foo':['red','green'],
         'bar':['blue','yellow']
    }
    
    def color(s):
        # convert chunk to color array
        a = np.asarray(COLORS.get(s.name[1], ['']))
        # index colors with modulo
        return a[np.arange(len(s))%len(a)]
    
    def highlight(df):
        # apply color per group
        c = 'background-color: ' + df.groupby(['person', 'z'])['z'].transform(color)
        # expand as DataFrame
        return pd.DataFrame(dict.fromkeys(df, c), index=df.index)
    
    df.style.apply(highlight, axis=None)
    

    应使用更高效的变体 merge :

    sizes = pd.Series({k: len(v) for k,v in COLORS.items()})
    
    colors = (pd.concat({k: pd.Series(v) for k, v in COLORS.items()},
                        names=['z', 'n'])
                .radd('background-color: ')
                .reset_index(name='color')
             )
    
    def highlight(df):
        c = (df.assign(n=df.groupby(['person', 'z']).cumcount()
                        %df['z'].map(sizes))
               .merge(colors, how='left')['color']
            )
        return pd.DataFrame(dict.fromkeys(df, c), index=df.index)
    
    df.style.apply(highlight, axis=None)
    

    输出:

    enter image description here

        2
  •  0
  •   Timeless    2 年前

    另一种可能性是做 background_gradient 用一个 习俗 ListedColormap :

    from itertools import cycle
    from operator import itemgetter
    from matplotlib.colors import ListedColormap
    
    colors = {"y": ["#fff2cc", "#ffe59a"], "b": ["#d0e1e2", "#a2c4c9"]}
    c1, c2 = cycle([0, 1]), cycle([2, 3]) # has to be generic..
    cmap = ListedColormap(colors["y"] + colors["b"])
    
    gmap = [
        next(c1 if ng % 2 == 0 else c2)
        for ng in df.groupby(["person", "z"], sort=False).ngroup()
    ]
    
    border_css = {"selector": "td", "props": "border-bottom: 3px solid black;"}
    
    st = (
        df.style.background_gradient(gmap=gmap, cmap=cmap)
        .set_table_styles(
            {
                idx: [border_css]
                for idx in map(
                    itemgetter(-1),
                    df.groupby("person").indices.values(),
                )
            }, axis=1,
        ).hide(axis=0)
    )
    

    enter image description here