株価であれば、1つの銘柄に対して1つの数字が対応します。債券はそうなりません。同じ発行体でも、償還までの期間が違えば別の債券で、それぞれ違う利回りが付きます。2年、5年、10年、30年。これらを並べたものがです。
配信元によっては、価格と利回りの両方を返してくれます。どちらを使うかを決めずに列を選ぶと、意味の違う数字が混ざります。そして債券の世界では、価格ではなく利回りで語るのが普通です。価格と利回りは逆に動くので、「利回りが上がった」は「価格が下がった」を意味します。株価の感覚で「上がった=良かった」と読むと、逆の解釈になります。
取得の経路はのこともあれば の配布のこともあります。この回では、利回りのデータを取得するときに何を確認しておくべきかを扱います。数字そのものは単純ですが、その数字が何を指しているかの確認を怠ると、まったく違うものを比較することになります。
同じ「10年金利」でも中身が違う
10年国債の利回り、という言い方をしますが、実際に配信されている数字にはいくつか種類があります。実在する特定の銘柄の利回りなのか、残存10年ちょうどに補間した理論値なのか。前者は銘柄が入れ替わるたびに段差が出ますが、後者は滑らかに繋がります。
長期の系列を作るなら補間された値のほうが扱いやすく、実際の取引を追うなら実在銘柄のほうが正確です。ここはと同じで、揃えるべきものを揃えないまま並べると比較が成立しません。提供元がどちらを配信しているかを確認せずに繋ぐと、段差の意味が分からなくなります。
| 確認項目 | 違いが出ること |
|---|---|
| 実在銘柄か、補間値か | 銘柄入替時に段差が出るかどうか |
| 単位(% か 小数か) | 0.5 なのか 0.005 なのかで 100 倍ずれる |
| 年率の計算方法 | 複利か単利かで数字が変わる |
| 営業日の定義 | 国ごとに休日が違う |
| 公表の時点 | 終値ベースか、日中の特定時刻か |
利回りは「0.5」と書いて 0.5% を意味することも、「0.005」と書いて同じ 0.5% を意味することもあります。100 倍の違いが、パッと見では判別できません。取得直後に値の範囲を確認して、想定と桁が合っているかを見てください。
import pandas as pd
def check_yield_scale(s: pd.Series, expect: str = "percent") -> None: """利回りの単位が想定どおりかを、値の範囲で判定する。
percent : 0.5 が 0.5% を意味する(絶対値はおおむね 10 未満) decimal : 0.005 が 0.5% を意味する(絶対値はおおむね 0.2 未満) """ v = s.dropna().abs() if v.empty: raise ValueError("値が1つも無い")
hi = v.quantile(0.99) if expect == "percent" and hi < 0.2: raise ValueError(f"percent のはずが小数のようだ(99%点 {hi:.4f})") if expect == "decimal" and hi > 1.0: raise ValueError(f"decimal のはずが百分率のようだ(99%点 {hi:.4f})")曲線として持つか、時系列として持つか
イールドカーブは、日付 × 残存期間の2次元のデータです。持ち方には2通りあり、後の使い方で選びます。日付を行・期間を列にした横長の形は、ある日の曲線を見るのに向きます。日付と期間と値を1行ずつ並べた縦長の形は、期間を増やしても列が増えないので保存に向きます。
保存は縦長、分析は横長に変換して使うのが扱いやすいです。縦長なら、提供元が配信する期間の種類が変わっても、保存の形を変えずに済みます。横長で保存していると、期間が1つ増えるたびに列を足すことになります。
import pandas as pd
def to_wide(long_df: pd.DataFrame) -> pd.DataFrame: """date / tenor_years / yield の縦長を、日付 × 期間の表に変える。""" wide = long_df.pivot(index="date", columns="tenor_years", values="yield") # 列(期間)を数値として昇順に並べる。文字列のままだと 10 が 2 より前に来る wide = wide.reindex(sorted(wide.columns, key=float), axis=1) return wide.sort_index()
def curve_slope(wide: pd.DataFrame, short: float = 2, long: float = 10) -> pd.Series: """長短の差。マイナスなら逆イールド。""" if short not in wide.columns or long not in wide.columns: raise KeyError(f"必要な期間が無い: {short}, {long}") return wide[long] - wide[short]上のコードで列を数値として並べ替えているのは、文字列のまま並べると 10 が 2 より前に来るからです。期間を文字列で持っていると、曲線を描いたときに順序が狂います。地味ですが、気づきにくい部類の間違いです。
欠けている期間をどう埋めるか
利回りのデータは、期間によっては配信されていないことがあります。30年債が存在しなかった時期、発行が止まっていた時期。その期間は「値がゼロ」ではなく「存在しない」ので、ゼロで埋めると曲線が地面まで落ちます。
埋めるとしても、隣の期間から補間するのか、直前の日から持ち越すのかで意味が変わります。前者は曲線の形を保ちますが実在しない値を作り、後者は日々の変化がゼロになります。どちらも情報を作り出しているという自覚が要ります。
扱いを決めたら、埋めた場所が分かるように印を残してください。なら、値の列とは別に「補間したかどうか」の列を持たせます。後から見た人が、どこが実測でどこが推定かを判別できます。
import pandas as pd
def fill_with_flag(s: pd.Series, method: str = "ffill") -> pd.DataFrame: """埋めた値と、それが埋めたものかどうかを並べて返す。""" if method not in ("ffill", "interpolate"): raise ValueError("ffill か interpolate を指定してください")
was_missing = s.isna() filled = s.ffill() if method == "ffill" else s.interpolate(method="linear") return pd.DataFrame({"value": filled, "imputed": was_missing & filled.notna()})為替や商品のデータについては データの宝石箱 EP.16 で扱っています。次回は、ここまで何度か触れてきた識別子の問題を正面から扱います。
利回りのデータは、金額のデータほど直感が働きません。桁が合っているか、符号が想定どおりか、期間の並びが正しいか。取得のたびに機械的に確かめる仕組みを入れておくのが確実です。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。