課題設定
market_dataテーブルには、取引日(trade_date)と株価の騰落フラグ(is_fall: 1=下落, 0=上昇)が記録されています。このデータから、各日付における連続下落日数を算出します。
入力テーブル: market_data
| trade_date | is_fall |
|---|---|
| 2023-01-16 | 1 |
| 2023-01-15 | 1 |
| 2023-01-14 | 1 |
| 2023-01-13 | 1 |
| 2023-01-12 | 0 |
| 2023-01-11 | 0 |
| 2023-01-10 | 0 |
| 2023-01-09 | 1 |
| 2023-01-08 | 0 |
期待する出力結果
| trade_date | is_fall | streak_len |
|---|---|---|
| 2023-01-16 | 1 | 4 |
| 2023-01-15 | 1 | 3 |
| 2023-01-14 | 1 | 2 |
| 2023-01-13 | 1 | 1 |
| 2023-01-12 | 0 | 0 |
| 2023-01-11 | 0 | 0 |
| 2023-01-10 | 0 | 0 |
| 2023-01-09 | 1 | 1 |
| 2023-01-08 | 0 | 0 |
アプローチ1: 行番号の差分を利用する
連続する値をグループ化する際の定石として、全体の行番号と条件ごとの行番号の差分を利用する方法があります。最終的なクエリは以下の通りです。
SELECT
trade_date,
is_fall,
CASE
WHEN is_fall = 0 THEN 0
ELSE ROW_NUMBER() OVER(PARTITION BY grp_id ORDER BY trade_date ASC)
END AS streak_len
FROM (
SELECT
trade_date,
is_fall,
ROW_NUMBER() OVER(ORDER BY trade_date DESC) -
ROW_NUMBER() OVER(PARTITION BY is_fall ORDER BY trade_date DESC) AS grp_id
FROM market_data
) sub
ORDER BY trade_date DESC;
処理のステップごとの解説です。まず、日付の降順で全体の行番号(total_rn)と、is_fallごとの行番号(fall_rn)を取得します。
SELECT
trade_date,
is_fall,
ROW_NUMBER() OVER(ORDER BY trade_date DESC) AS total_rn,
ROW_NUMBER() OVER(PARTITION BY is_fall ORDER BY trade_date DESC) AS fall_rn
FROM market_data;
結果:
| trade_date | is_fall | total_rn | fall_rn |
|---|---|---|---|
| 2023-01-16 | 1 | 1 | 1 |
| 2023-01-15 | 1 | 2 | 2 |
| 2023-01-14 | 1 | 3 | 3 |
| 2023-01-13 | 1 | 4 | 4 |
| 2023-01-12 | 0 | 5 | 1 |
| 2023-01-11 | 0 | 6 | 2 |
| 2023-01-10 | 0 | 7 | 3 |
| 2023-01-09 | 1 | 8 | 5 |
| 2023-01-08 | 0 | 9 | 4 |
is_fall = 1の行に注目すると、連続している日付のグループではtotal_rnとfall_rnの差が一定になります。
SELECT
trade_date,
is_fall,
CASE WHEN is_fall = 1 THEN
ROW_NUMBER() OVER(ORDER BY trade_date DESC) -
ROW_NUMBER() OVER(PARTITION BY is_fall ORDER BY trade_date DESC)
END AS grp_id
FROM market_data;
結果:
| trade_date | is_fall | grp_id |
|---|---|---|
| 2023-01-16 | 1 | 0 |
| 2023-01-15 | 1 | 0 |
| 2023-01-14 | 1 | 0 |
| 2023-01-13 | 1 | 0 |
| 2023-01-12 | 0 | NULL |
| 2023-01-11 | 0 | NULL |
| 2023-01-10 | 0 | NULL |
| 2023-01-09 | 1 | 3 |
| 2023-01-08 | 0 | NULL |
この差分をgrp_idとしてパーティションキーに利用し、is_fall = 1の場合のみそのグループ内で行番号を振ることで連続下落日数を求めます。
アプローチ2: 状態変化の検出と累積和を利用する
もう一つのアプローチとして、前行との比較で状態変化を検出し、その累積和でグループ化する方法があります。最終的なクエリは以下の通りです。
SELECT
trade_date,
is_fall,
CASE
WHEN is_fall = 1 THEN ROW_NUMBER() OVER(PARTITION BY group_key ORDER BY trade_date ASC)
ELSE 0
END AS streak_len
FROM (
SELECT
trade_date,
is_fall,
SUM(is_boundary) OVER(ORDER BY trade_date DESC) AS group_key
FROM (
SELECT
trade_date,
is_fall,
CASE
WHEN LAG(is_fall) OVER(ORDER BY trade_date DESC) = is_fall THEN 0
ELSE 1
END AS is_boundary
FROM market_data
) t1
) t2
ORDER BY trade_date DESC;
ステップごとの解説です。まず、LAG関数を用いて前日のis_fallと比較します。値が変化していれば1(is_boundary)、同じであれば0とフラグを立てます。
SELECT
trade_date,
is_fall,
CASE WHEN LAG(is_fall) OVER(ORDER BY trade_date DESC) = is_fall THEN 0 ELSE 1 END AS is_boundary
FROM market_data;
結果:
| trade_date | is_fall | is_boundary |
|---|---|---|
| 2023-01-16 | 1 | 1 |
| 2023-01-15 | 1 | 0 |
| 2023-01-14 | 1 | 0 |
| 2023-01-13 | 1 | 0 |
| 2023-01-12 | 0 | 1 |
| 2023-01-11 | 0 | 0 |
| 2023-01-10 | 0 | 0 |
| 2023-01-09 | 1 | 1 |
| 2023-01-08 | 0 | 1 |
次に、この変化フラグの累積和を取ります。状態が変化するたびに累積和が増加するため、これが連続区間を表すグループキー(group_key)となります。
SELECT
trade_date,
is_fall,
is_boundary,
SUM(is_boundary) OVER(ORDER BY trade_date DESC) AS group_key
FROM (
SELECT
trade_date,
is_fall,
CASE WHEN LAG(is_fall) OVER(ORDER BY trade_date DESC) = is_fall THEN 0 ELSE 1 END AS is_boundary
FROM market_data
) t1;
結果:
| trade_date | is_fall | is_boundary | group_key |
|---|---|---|---|
| 2023-01-16 | 1 | 1 | 1 |
| 2023-01-15 | 1 | 0 | 1 |
| 2023-01-14 | 1 | 0 | 1 |
| 2023-01-13 | 1 | 0 | 1 |
| 2023-01-12 | 0 | 1 | 2 |
| 2023-01-11 | 0 | 0 | 2 |
| 2023-01-10 | 0 | 0 | 2 |
| 2023-01-09 | 1 | 1 | 3 |
| 2023-01-08 | 0 | 1 | 4 |
最後に、このgroup_keyでパーティションを切り、is_fall = 1の行に対して日付順に行番号を割り当てることで連続下落日数を計算します。