plotnineで非営業日を軸から除いたプロットを描く
Pythonのプロット描画ライブラリであるplotnineにおいて、土日祝日などの任意の非営業日を除外して営業日ベースでプロットを描く方法です。
結論からいうと、日付からint型の連番列を作ってこの連番列をx軸に取り、plotnine.scale_x_continuous
のlabels
引数に軸ラベルにするstr型の日付の文字列を渡せばよいです1。
バッドノウハウに近いと思いますが、株価のようにデータによっては非営業日を除外して軸を描くケースが頻出なんですよね…よく使うのでメモしておきます。
環境は以下のとおりです。
- Python=3.13.7
- polars=1.33.1
- plotnine=0.15.0
- mizani=0.14.2
こういうサンプルデータを考えます。
import polars as pl
import plotnine as p9
from mizani.breaks import breaks_width, breaks_date_width
df = (
# 2025/1/11-12は土日、1/13は祝日、1/18-19は土日
pl.DataFrame({
"date": [
"2025-01-06", "2025-01-07", "2025-01-08", "2025-01-09", "2025-01-10",
"2025-01-14", "2025-01-15", "2025-01-16", "2025-01-17",
"2025-01-20", "2025-01-21", "2025-01-22", "2025-01-23", "2025-01-24"
],
"a": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
"b": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
})
.with_columns(
date=pl.col("date").str.strptime(pl.Date, "%Y-%m-%d"),
)
)
df = (
df
.unpivot(
on=["a", "b"],
index="date",
variable_name="stock_name",
value_name="price"
)
.sort("date", "stock_name")
)
print(df)
shape: (28, 3)
┌────────────┬────────────┬───────┐
│ date ┆ stock_name ┆ price │
│ --- ┆ --- ┆ --- │
│ date ┆ str ┆ i64 │
╞════════════╪════════════╪═══════╡
│ 2025-01-06 ┆ a ┆ 1 │
│ 2025-01-06 ┆ b ┆ 2 │
│ 2025-01-07 ┆ a ┆ 2 │
│ 2025-01-07 ┆ b ┆ 3 │
│ 2025-01-08 ┆ a ┆ 3 │
│ … ┆ … ┆ … │
│ 2025-01-22 ┆ b ┆ 13 │
│ 2025-01-23 ┆ a ┆ 13 │
│ 2025-01-23 ┆ b ┆ 14 │
│ 2025-01-24 ┆ a ┆ 14 │
│ 2025-01-24 ┆ b ┆ 15 │
└────────────┴────────────┴───────┘
a
とb
という株の株価が記録されたpolars.DataFrame
です。
p9.aes
のx
にdate
を指定してふつうに折れ線グラフを描くと、レコードがない日、すなわち株式市場が空いていない日が直線で繋がってしまいます2。そうではなく、例えば2025/1/10の一つ隣は2025/1/14が来るようにプロットしたいです。
(
p9.ggplot(
df,
p9.aes(x="date", y="price", color="stock_name")
) +
p9.geom_line() +
p9.geom_point() +
p9.scale_x_date(breaks=breaks_date_width("1 days"), minor_breaks=None) +
p9.scale_y_continuous(breaks=breaks_width(1), minor_breaks=None) +
p9.theme(axis_text_x=p9.element_text(rotation=90))
)
これを解決するには、まずx軸に取りたい日付のdate
をintの連番にしたindex列を用意します。
df = (
df
# 縦持ちのデータでは同一日が複数行あるので、rankのmethodはdenseを使う
.with_columns(
date_idx=pl.col("date").rank("dense").cast(pl.Int32) - 1
)
)
そしてindex列をx軸に取り、scale_x_continuous
のbreaks
とminor_breaks
でそれぞれ軸ラベルと目盛りを振りたい連番を指定してあげます。軸ラベルにする日付の文字列として、breaks
の引数と同じ長さのlist[str]
をlabels
に与えます。
date_labels = [d.strftime("%Y-%m-%d") for d in sorted(df["date"].unique().to_list())]
date_idx = sorted(df["date_idx"].unique().to_list())
# 軸ラベルは全部の日付に振り、軸ラベルの間のminor_breaksは振らないとする
major_breaks_idx = date_idx
minor_breaks_idx = None
major_labels = [date_labels[i] for i in major_breaks_idx]
(
p9.ggplot(
df,
p9.aes(x="date_idx", y="price", color="stock_name")
) +
p9.geom_line() +
p9.geom_point() +
p9.scale_x_continuous(breaks=major_breaks_idx, minor_breaks=minor_breaks_idx, labels=major_labels) +
p9.scale_y_continuous(breaks=breaks_width(1), minor_breaks=None) +
p9.theme(axis_text_x=p9.element_text(rotation=90)) +
p9.labs(x="date")
)
5日おきに軸ラベルを振るということもできます。
major_breaks_idx = [i for i in date_idx if i % 5 == 0]
minor_breaks_idx = date_idx
major_labels = [date_labels[i] for i in major_breaks_idx]
(
p9.ggplot(
df,
p9.aes(x="date_idx", y="price", color="stock_name")
) +
p9.geom_line() +
p9.geom_point() +
p9.scale_x_continuous(breaks=major_breaks_idx, minor_breaks=minor_breaks_idx, labels=major_labels) +
p9.scale_y_continuous(breaks=breaks_width(1), minor_breaks=None) +
p9.theme(axis_text_x=p9.element_text(rotation=90)) +
p9.labs(x="date")
)
例えば月曜日を週始まりとして、週の最初の日付だけに軸ラベルを振りたいというケースもよくあります。
この場合は、polarsで各週の最初の曜日の日付のindexを取り出し、それをscale_x_continuous
のbreaks
引数に渡せばよいですね。
idx_first_business_day_of_week = (
df
.with_columns(
week=pl.col("date").dt.week(),
# pl.Expr.dt.weekday()は月曜日が1
# 月曜日始まりで週判定をするために月曜日を0にする
weekday=pl.col("date").dt.weekday() - 1
)
.group_by("week")
.agg(
first_business_day_of_week=pl.col("date").min(),
first_business_day_of_week_idx=pl.col("date_idx").min()
)
.sort("week")
.get_column("first_business_day_of_week_idx")
.to_list()
)
major_breaks_idx = idx_first_business_day_of_week
minor_breaks_idx = date_idx
major_labels = [date_labels[i] for i in major_breaks_idx]
(
p9.ggplot(
df,
p9.aes(x="date_idx", y="price", color="stock_name")
) +
p9.geom_line() +
p9.geom_point() +
p9.scale_x_continuous(breaks=major_breaks_idx, minor_breaks=minor_breaks_idx, labels=major_labels) +
p9.scale_y_continuous(breaks=breaks_width(1), minor_breaks=None) +
p9.theme(axis_text_x=p9.element_text(rotation=90)) +
p9.labs(x="date")
)
同様に月の最初の日だけ振るようなこともできます。この場合はweek
をmonth
に、weekday
をday
に読み替えればOKです。