根据切片日期,将 .loc[date] 切片传递到 altair 图表中会产生奇怪的结果

Passing a .loc[date] slice into altair chart has odd results depending on slice date

在我意识到图层规范不是问题之前是struggling with plotting a few layers of a chart,但不知何故我通过图表的切片表现得很奇怪(对我来说)。如果它没有坏,那我一定是误解了事情应该如何运作。

附上一个具体的例子来演示它是如何工作的以及我认为它不应该像这样工作的原因。

import altair as alt
alt.renderers.enable('notebook')

import pandas as pd

idx = pd.IndexSlice

history_index = pd.date_range(start="31jan2016", end="30jun2019", freq="M")
forecast_index = pd.date_range(start="31jan2019", end="31dec2019", freq="M")

history_df = pd.DataFrame([z for z in range(len(history_index))], index=history_index,columns = ['history'])
forecast_df = pd.DataFrame([z for z in range(len(forecast_index))], index=forecast_index, columns = ['forecast'])

df = history_df.join(forecast_df, how="outer")
df.index.name = "date"

第一个示例有效:

#without making it a seasonal chart,  this works
non_seasonal  = alt.Chart(df.loc[idx['20170701':],:].reset_index(), title=f"non seasonal plot").mark_line().encode(
        x='date',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
    )
non_seasonal

但是当我开始将这些变成季节性图表时,通过将 X 轴设为月份,问题就出现了。

我的第一个切片成功了,我只是切片所有现有的 forecast 数据,这些数据从 2019 年 1 月开始。

#works ok: shows all the data since 1jan2019
seasonal1 = alt.Chart(df.loc[idx['20190101':],:].reset_index(), title=f"seasonal plot").mark_line().encode(
        x='month(date)',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
    )
seasonal1

但是当我从较早的日期开始切片时(在 'forecast' 有任何数据之前),我遇到了麻烦。

#fails:  shows no data
seasonal2 = alt.Chart(df.loc[idx['20180101':],:].reset_index(), title=f"seasonal plot").mark_line().encode(
        x='month(date)',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
    )
seasonal2

如果我添加颜色编码,我可以使数据出现,但这不是最终适合我的解决方案。

#works if I add a color-encoding
seasonal3 = alt.Chart(df.loc[idx['20180101':],:].reset_index(), title=f"seasonal plot").mark_line().encode(
        x='month(date)',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
    color="year(date):N"
    )
seasonal3

此时事情开始变得非常奇怪。如果我在 2018 年的任何地方开始我的切片,切片的 "start" 似乎充当切片的 "end" ....

#fails bizarrely -- the 20180701 slice appears to be the END of the slice, not the start
seasonal4 = alt.Chart(df.loc[idx['20180701':],:].reset_index(), title=f"seasonal plot").mark_line().encode(
        x='month(date)',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
    )
seasonal4

同样,如果我给它一个颜色编码它就可以工作

#again, it works if I add a color encoding.
seasonal5 = alt.Chart(df.loc[idx['20180701':],:].reset_index(), title=f"seasonal plot").mark_line().encode(
        x='month(date)',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
        color="year(date):N"
    )
seasonal5

所以显而易见的快速解决方法是添加颜色编码。但这对我不起作用,因为我试图在此图表上分层多组数据(按年份着色的历史数据)和硬编码为红色的预测数据。

=============================================

根据下面杰克的回答,我得到了我想要的最终产品:

forecast = alt.Chart(df.loc[idx['20180101':],'forecast'].reset_index().dropna(), title=f"seasonal plot").mark_line(color="green").encode(
        x='month(date)',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
    )

history = alt.Chart(df.loc[idx['20170101':],'history'].reset_index().dropna(), title=f"seasonal plot").mark_line().encode(
        x='month(date)',
        y=alt.Y(f'history', scale=alt.Scale(zero=False)),
        color="year(date):O"
    )

forecast+history

如果将 mark_line() 更改为 mark_point(),您会看到数据确实存在,但未显示在折线图中。为什么?因为只在相邻的 non-null 点之间画了一条线。

查看 df.loc[idx['20180101':],:] 的输出:您会看到它包含许多行,其中大部分是 NaN 值。当您从索引中提取月份时,这些 NaN 值散布在具有匹配月份的定义值中,这会在行中创建中断:在某些情况下,中断太多以至于不再有任何相邻的 non-null点连接,所以没有画线。

顺便说一句,这就是为什么添加颜色编码改善情况的原因:这意味着前几年的空数据不再与定义数据属于同一组,因此相邻点是non-null并且可以画一条线。

要解决此问题,我建议您对数据切片的方式更加小心,and/or 过滤您正在创建的切片的 NaN 值。例如,在您的 seasonal2 图表中,您可以这样做:

df_sliced = df.loc[idx['20180101':],:].dropna().reset_index()
seasonal2 = alt.Chart(df_sliced, title=f"seasonal plot").mark_line().encode(
        x='month(date)',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
    )
seasonal2

另一种选择是在提取日期时使用 yearmonth 而不是 month,这样可以防止未定义的数据与已定义的数据穿插:

seasonal2 = alt.Chart(df.loc[idx['20180101':],:].reset_index(), title=f"seasonal plot").mark_line().encode(
        x='yearmonth(date)',
        y=alt.Y(f'forecast', scale=alt.Scale(zero=False)),
    )
seasonal2

其他例子可以用类似的方式修复。