2025, Oct 17 16:18
Управление порядком легенд в plotnine через guides
Показываем, как в plotnine задать порядок двух легенд (fill и color) через guides и guide_legend без правки данных и геомов. Воспроизводимый пример на Python.
Когда в plotnine сочетаются несколько эстетик, нередко появляется больше одной легенды. Если легенды идут в неправильном порядке, график воспринимается странно. Ниже — простой способ управлять их порядком без перестройки данных и без замены геомов.
Цель
Воссоздать столбчато‑ступенчатый график с двумя отдельными легендами — одна для столбцов (2040), другая для линий (2015) — и поменять их местами в области легенды.
Воспроизводимый пример с нежелательным порядком легенд
# импорт
import polars as po
import polars.selectors as sel
from plotnine import *
# данные
payload = {
    "Category": ['0-1', '1-5', '5-10', '10-15', '15-20', '20-25', '25-30', '30-35', '35-40', '40-45', '45-50', '50-55', '55-60', '60-65', '65-70', '70-75', '75-80', '80-85', '85-90', '90-95', '95 en ouder'],
    "Mannen 2040": [-540, -910, -1530, -1990, -2120, -2300, -2270, -2340, -2540, -2690, -3140, -3370, -3750, -4200, -5310, -7250, -8310, -8170, -6970, -4610, -1480],
    "Vrouwen 2040": [460, 670, 1050, 1740, 2030, 2450, 3040, 3280, 3370, 3420, 3990, 4390, 4510, 4880, 6170, 8050, 9530, 11080, 11470, 8900, 3350],
    "Mannen 2015": [-420, -660, -980, -1310, -1380, -1550, -1490, -1410, -1460, -1810, -2360, -2690, -2910, -3030, -3430, -3050, -2920, -2540, -1800, -840, -210],
    "Vrouwen 2015": [360, 500, 710, 1180, 1350, 1720, 2070, 2060, 1930, 2060, 2570, 2850, 2870, 2840, 3300, 3070, 3540, 4240, 4400, 2950, 1020]
}
wide_tbl = po.DataFrame(payload)
long_tbl = wide_tbl.unpivot(sel.numeric(), index="Category")
# график
(
    ggplot() +
    geom_bar(
        data=long_tbl.filter(po.col('variable').is_in(['Mannen 2040', 'Vrouwen 2040'])),
        mapping=aes(x='Category', y='value', fill='variable'),
        stat='identity'
    ) +
    scale_fill_manual(values=['#007BC7', '#CA005D']) +
    geom_step(
        data=long_tbl.filter(po.col('variable').is_in(['Mannen 2015', 'Vrouwen 2015'])),
        mapping=aes(x='Category', y='value', color='variable', group='variable'),
        stat='identity',
        size=1.5,
        direction='mid'
    ) +
    scale_color_manual(values=['#0C163F', '#FFB612']) +
    scale_x_discrete(limits=payload['Category']) +
    scale_y_continuous(
        name="Euro's (x miljoen)",
        limits=[-10_000, 12_500],
        breaks=range(-10_000, 12_500, 2_000),
        labels=list(map(abs, range(-10_000, 12_500, 2_000)))
    ) +
    coord_flip() +
    theme(
        figure_size=(8, 6),
        plot_title=element_text(color="#01689B", size=16, hjust=0.5),
        legend_position="bottom",
        legend_title=element_blank(),
        legend_key=element_blank(),
        panel_background=element_rect(fill="white"),
        panel_grid_major_x=element_line(colour="black", size=0.25),
        axis_ticks_length=5,
        axis_ticks_y=element_line(color="black", size=1.5),
        axis_line_y=element_line(color="black", size=1.5, linetype="solid"),
        axis_title_y=element_blank(),
        axis_title_x=element_text(colour="#007BC7", size=10, hjust=1)
    ) +
    ggtitle("De uitgaven aan zorg voor vrouwen zijn hoger dan\nvoor mannen, en dit verschil neemt toe in de toekomst")
)
Что происходит
Столбцы используют эстетику fill, а линии — color, поэтому plotnine создаёт две легенды. По умолчанию их порядок задаётся тем, как гайды строятся на основе шкал. Если этот порядок не подходит, его нужно указать явно.
Решение: поменять порядок легенд через guides
Задайте порядок каждой легенды с помощью guides и guide_legend. Изменение ниже аккуратно меняет их местами, не трогая исходные данные.
import polars as po
import polars.selectors as sel
from plotnine import *
# данные
payload = {
    "Category": ['0-1', '1-5', '5-10', '10-15', '15-20', '20-25', '25-30', '30-35', '35-40', '40-45', '45-50', '50-55', '55-60', '60-65', '65-70', '70-75', '75-80', '80-85', '85-90', '90-95', '95 en ouder'],
    "Mannen 2040": [-540, -910, -1530, -1990, -2120, -2300, -2270, -2340, -2540, -2690, -3140, -3370, -3750, -4200, -5310, -7250, -8310, -8170, -6970, -4610, -1480],
    "Vrouwen 2040": [460, 670, 1050, 1740, 2030, 2450, 3040, 3280, 3370, 3420, 3990, 4390, 4510, 4880, 6170, 8050, 9530, 11080, 11470, 8900, 3350],
    "Mannen 2015": [-420, -660, -980, -1310, -1380, -1550, -1490, -1410, -1460, -1810, -2360, -2690, -2910, -3030, -3430, -3050, -2920, -2540, -1800, -840, -210],
    "Vrouwen 2015": [360, 500, 710, 1180, 1350, 1720, 2070, 2060, 1930, 2060, 2570, 2850, 2870, 2840, 3300, 3070, 3540, 4240, 4400, 2950, 1020]
}
long_tbl = po.DataFrame(payload).unpivot(sel.numeric(), index="Category")
(
    ggplot() +
    geom_bar(
        data=long_tbl.filter(po.col('variable').is_in(['Mannen 2040', 'Vrouwen 2040'])),
        mapping=aes(x='Category', y='value', fill='variable'),
        stat='identity'
    ) +
    scale_fill_manual(values=['#007BC7', '#CA005D'], guide=guide_legend(order=1)) +
    geom_step(
        data=long_tbl.filter(po.col('variable').is_in(['Mannen 2015', 'Vrouwen 2015'])),
        mapping=aes(x='Category', y='value', color='variable', group='variable'),
        stat='identity',
        size=1.5,
        direction='mid'
    ) +
    scale_color_manual(values=['#0C163F', '#FFB612']) +
    scale_x_discrete(limits=payload['Category']) +
    scale_y_continuous(
        name="Euro's (x miljoen)",
        limits=[-10_000, 12_500],
        breaks=range(-10_000, 12_500, 2_000),
        labels=list(map(abs, range(-10_000, 12_500, 2_000)))
    ) +
    coord_flip() +
    theme(
        figure_size=(8, 6),
        plot_title=element_text(color="#01689B", size=16, hjust=0.5),
        legend_position="bottom",
        legend_title=element_blank(),
        legend_key=element_blank(),
        panel_background=element_rect(fill="white"),
        panel_grid_major_x=element_line(colour="black", size=0.25),
        axis_ticks_length=5,
        axis_ticks_y=element_line(color="black", size=1.5),
        axis_line_y=element_line(color="black", size=1.5, linetype="solid"),
        axis_title_y=element_blank(),
        axis_title_x=element_text(colour="#007BC7", size=10, hjust=1)
    ) +
    guides(
        color=guide_legend(order=1),  # линии 2015
        fill=guide_legend(order=2)    # столбцы 2040
    ) +
    ggtitle("De uitgaven aan zorg voor vrouwen zijn hoger dan\nvoor mannen, en dit verschil neemt toe in de toekomst")
)
Почему это важно
Понятные легенды ускоряют чтение графика. Когда на одном рисунке совмещены столбцы и линии, ожидается неизменное соответствие между визуальными каналами и блоками легенды. Управляя порядком легенд, мы избегаем неправильного толкования и сохраняем удобство поддержки многослойных визуализаций.
Итоги
Если отдельные эстетики в plotnine дают отдельные легенды, явно задайте их относительные позиции. Минимальное изменение — добавить guides с guide_legend(order=...). Данные останутся нетронутыми, геомы — прежними, а порядок будет однозначно задан. Небольшая правка, которая заметно повышает читаемость.