使用 altair 的多个分组图表

Multiple grouped charts with altair

我的数据有4个属性:dataset(D1/D2),model(M1/M2),layer(L1/L2),scene(S1/S2)。我可以制作一个按场景分组的图表,然后水平和垂直合并图表(上图)。 但是,我希望通过场景和数据集获得 'double grouping',例如通过将 blue/orange 条彼此相邻放置但具有不同的不透明度或 pattern/hatch.[= 来合并 D1 和 D2 图。 13=]

基本上是这样的(假设黑色特征是填充图案)。

这是重现第一个情节的代码

import numpy as np
import itertools
import argparse
import pandas as pd
import matplotlib.pyplot as plt
import os
import altair as alt
alt.renderers.enable('altair_viewer')

np.random.seed(0)

################################################################################

model_keys = ['M1', 'M2']
data_keys = ['D1', 'D2']
scene_keys = ['S1', 'S2']
layer_keys = ['L1', 'L2']

ys = []
models = []
dataset = []
layers = []
scenes = []

for sc in scene_keys:
    for m in model_keys:
        for d in data_keys:
            for l in layer_keys:
                for s in range(10):
                    data_y = list(np.random.rand(10) / 10)
                    ys += data_y
                    scenes += [sc] * len(data_y)
                    models += [m] * len(data_y)
                    dataset += [d] * len(data_y)
                    layers += [l] * len(data_y)


# ------------------------------------------------------------------------------


df = pd.DataFrame({'Y': ys,
                   'Model': models,
                   'Dataset': dataset,
                   'Layer': layers,
                   'Scenes': scenes})

bars = alt.Chart(df, width=100, height=90).mark_bar().encode(
    # field to group columns on
    x=alt.X('Scenes:N',
        title=None,
        axis=alt.Axis(
            grid=False,
            title=None,
            labels=False,
        ),
    ),
    # field to use as Y values and how to calculate
    y=alt.Y('Y:Q',
        aggregate='mean',
        axis=alt.Axis(
            grid=True,
            title='Y',
            titleFontWeight='normal',
        ),
    ),
    # field to use for sorting
    order=alt.Order('Scenes',
        sort='ascending',
    ),
    # field to use for color segmentation
    color=alt.Color('Scenes',
        legend=alt.Legend(orient='bottom',
            padding=-10,
        ),
        title=None,
    ),
)

error_bars = alt.Chart(df).mark_errorbar(extent='ci').encode(
    x=alt.X('Scenes:N'),
    y=alt.Y('Y:Q'),
)

text = alt.Chart(df).mark_text(align='center',
    baseline='line-bottom',
    color='black',
    dy=-5 # y-shift
).encode(
    x=alt.X('Scenes:N'),
    y=alt.Y('mean(Y):Q'),
    text=alt.Text('mean(Y):Q', format='.1f'),
)

chart_base = bars + error_bars + text

chart_base = chart_base.facet(
    # field to use to use as the set of columns to be represented in each group
    column=alt.Column('Layer:N',
        # header=alt.Header(
            # labelFontStyle='bold',
        # ),
        title=None,
        sort=list(set(models)), # get unique indices
    ),
    spacing={"row": 0, "column": 15},
)

def unique(sequence):
    seen = set()
    return [x for x in sequence if not (x in seen or seen.add(x))]

for i, m in enumerate(unique(models)):
    chart_imnet = chart_base.transform_filter(
        alt.FieldEqualPredicate(field='Dataset', equal='D1'),
    ).transform_filter(
        alt.FieldEqualPredicate(field='Model', equal=m)
    )
    chart_places = chart_base.transform_filter(
        alt.FieldEqualPredicate(field='Dataset', equal='D2')
    ).transform_filter(
        alt.FieldEqualPredicate(field='Model', equal=m)
    )

    if i == 0:
        title_params = dict({'align': 'center', 'anchor': 'middle', 'dy': -10})
        chart_imnet = chart_imnet.properties(title=alt.TitleParams('D1', **title_params))
        chart_places = chart_places.properties(title=alt.TitleParams('D2', **title_params))

    chart_places = alt.concat(chart_places,
        title=alt.TitleParams(
            m,
            baseline='middle',
            orient='right',
            anchor='middle',
            angle=90,
            # dy=10,
            dx=30 if i == 0 else 0,
        ),
    )

    if i == 0:
        chart = (chart_imnet | chart_places).resolve_scale(x='shared')
    else:
        chart = (chart & (chart_imnet | chart_places).resolve_scale(x='shared'))

chart.save('test.html')

目前,我不知道一个好的答案,但是一旦 https://github.com/altair-viz/altair/pull/2528 被接受,您就可以像这样使用 xOffset 编码通道:

alt.Chart(df, height=90).mark_bar(tooltip=True).encode(
    x=alt.X("Scenes:N"),
    y=alt.Y("mean(Y):Q"),
    color=alt.Color("Scenes:N"),
    opacity=alt.Opacity("Dataset:N"),
    xOffset=alt.XOffset("Dataset:N"),
    column=alt.Column('Layer:N'),
    row=alt.Row("Model:N")
).resolve_scale(x='independent')

这将导致:

Colab Notebook or Vega Editor

编辑

要控制不透明度和图例名称,可以这样做

alt.Chart(df, height=90).mark_bar(tooltip=True).encode(
    x=alt.X("Scenes:N"),
    y=alt.Y("mean(Y):Q"),
    color=alt.Color("Scenes:N"),
    opacity=alt.Opacity("Dataset:N", 
                        scale=alt.Scale(domain=['D1', 'D2'], 
                                        range=[0.2, 1.0]), 
                        legend=alt.Legend(labelExpr="datum.label == 'D1' ? 'D1 - transparent' : 'D2 - full'")),
    xOffset=alt.XOffset("Dataset:N"),
    column=alt.Column('Layer:N'),
    row=alt.Row("Model:N")
).resolve_scale(x='independent')