2025, Oct 19 17:16
Как построить корректную CDF в Altair: mark_area без stacking
Как исправить «рваную» CDF в Altair с mark_area: transform_quantile даёт дубликаты X. Отключите stacking, агрегируйте Y или подберите шаг квантилей точнее.
Площадные диаграммы — естественный выбор для визуализации CDF, потому что кривая должна монотонно возрастать, а площадь под ней удерживает взгляд. Однако при построении CDF в Altair через transform_quantile легко получить странные многоугольники и «рваную» заливку. Причина не в математике CDF, а в том, как маркер area обращается с повторяющимися позициями по оси X после квантильного преобразования.
Минимальный пример, который выглядит неправильно
Ниже приведённый фрагмент вычисляет квантили из небольшого набора исходов и пытается отрисовать их как площадную диаграмму.
import altair as alt
import polars as pl
df_src = pl.DataFrame({"outcomes": [16950, 17050, 18750, 18750, 20950]})
(
    alt.Chart(df_src)
    .transform_quantile("outcomes", step=0.1)
    .mark_area(line=True, opacity=0.5)
    .encode(
        x=alt.X("value:Q"),
        y=alt.Y("prob:Q").title("Prob"),
    )
)
Что на самом деле происходит
Если исходных значений немного относительно выбранного шага, квантильное преобразование выдаёт несколько вероятностей, соответствующих одному и тому же value. Это легко проверить, заглянув в промежуточные данные в Vega Editor; например, вероятности 0.55, 0.65 и 0.75 могут иметь общий value. Когда площадная диаграмма сталкивается с несколькими строками с одинаковой x-координатой, она пытается сложить их (stack), что и порождает неожиданную геометрию. Увеличение числа различных исходов обычно убирает такие коллизии, но гарантии нет, если в данных есть повторы.
Решение: отключить stacking (или подправить преобразование)
Самый простой способ — отключить stacking на канале y, чтобы повторяющиеся позиции по X не накладывались друг на друга. Если это допустимо, можно увеличить размер шага, чтобы меньше вероятностей приходилось на одно и то же значение.
import altair as alt
import polars as pl
df_src = pl.DataFrame({"outcomes": [16950, 17050, 18750, 18750, 20950]})
(
    alt.Chart(df_src)
    .transform_quantile("outcomes", step=0.1)
    .mark_area(line=True, opacity=0.5)
    .encode(
        x=alt.X("value:Q"),
        y=alt.Y("prob:Q", stack=None).title("Prob")
    )
)
О базовой линии Y2 и API y2/Y2
В площадной диаграмме значение 0 используется как базовая линия для Y2 по умолчанию. Но как только несколько строк разделяют одну и ту же позицию по X, рендереру приходится решать, как сочетать эти значения y. Суммировать, брать максимум или поступить иначе? Именно эта неопределённость и делает полигон «странным» при повторяющихся значениях X. Отключение stacking снимает эту двусмысленность. Ещё один способ — агрегировать, например, взять максимальную вероятность на канале y, чтобы дубликаты по X схлопывались в единственную точку.
Что касается использования API: y2 можно задать как базовую линию через datum. Допустимы формы y2=alt.Y2Datum(0) или передача alt.Y2Datum(0) напрямую в encode. Визуально график может «заработать», но в целом лучше устранять первопричину — предотвращать stacking или агрегировать значения y, ведь дело не в базовой линии, а в дублирующихся позициях по X.
Почему это важно знать
Квантильные преобразования и маркеры area — мощные инструменты, но в связке они проявляют особенности семантики stacking. При повторяющихся позициях по X наслоение области способно исказить CDF, которая в норме должна быть монотонной и визуально простой. Понимание, как управлять stacking, когда агрегировать или менять шаг, помогает сохранить график верным исходному распределению.
Итоги
Если ваша CDF с маркером area в Altair выглядит рваной или «сломанной», проверьте промежуточный результат квантильного преобразования на повторяющиеся значения по оси X. Отключите stacking на канале y, чтобы избежать непреднамеренного накопления, либо агрегируйте вероятности, чтобы дубликаты разрешались детерминированно. Если точность позволяет, рассмотрите корректировку шага квантилей. С этими мерами CDF с маркером area будет отрисовываться как ожидаемая возрастающая кривая.
Статья основана на вопросе с StackOverflow от Juan Martinez и ответе от kgoodrick.