У меня есть следующий фрейм данных, 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
.
Первоначально я решил, что можно использовать вложенный цикл для разделения каждого z
на каждый person
. Сначала я попробовал протестировать это только для одного person
, вот так:
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.
Затем я попробовал аналогичную стратегию, вложив лямбда-функцию в другое условие 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
для каждого person
?
Кроме того, как я могу добавить нижнюю границу в последней строке каждого person
, не имея возможности индивидуально оформлять группы и объединять их?
Примечание. Да, цвета на colors
не будут соответствовать изображению, и это нормально.
Я бы использовал пользовательскую функцию и 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)
Вариант, который должен быть более эффективным, с использованием слияния :
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)
Выход:
Я добавил вариант, который должен быть более эффективным. Не знаю насчет границ, но использование style.apply
с axis=None
дает большую гибкость в настройке стиля. Вам просто нужно создать DataFrame со стилем для каждой ячейки.
@mozway, мне нужна твоя рука помощи. если inp = [0, 0, 0, 1, 1, 2, 3, 3, 3, 3]
, как чередовать 0/1 для четных значений и 2/3 для нечетных? результат должен быть [0, 1, 0, 2, 3, 0, 2, 3, 2, 3]
.
@Timeless один вариант: inp = pd.Series([0, 0, 0, 1, 1, 2, 3, 3, 3, 3]) ; inp.groupby(inp).cumcount()%2+inp%2*2
Другая возможность — создать background_gradient с пользовательской ListedColormap :
from operator import itemgetter
from matplotlib.colors import ListedColormap
colors = {"y": ["#fff2cc", "#ffe59a"], "b": ["#d0e1e2", "#a2c4c9"]}
cmap = ListedColormap(colors["y"] + colors["b"])
ng = df.groupby(["person", "z"], sort=False).ngroup()
gmap = ng.groupby(ng).cumcount() % 2 + ng % 2 * 2 # in a mozway
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,
).set_properties(**{"text-align": "right", "width": "110px"})
.hide(axis=0)
)
Очень хорошо. Принято и отмечено как полезное. Как я могу реализовать черную нижнюю границу в последней строке каждого
person
(5,11,17), используя этот метод?