【J-Quants】pandasとplotnineを用いて投資部門別売買状況データから投資部門ごとの売買代金の推移を見る

（修正履歴）2023/10/2: 億円単位にすべきところを誤って10億円単位にしてしまっていた点を修正しました。

## はじめに

こんにちは！J-Quants運営チームです。

運営チームでは、個人向けに金融データをAPIで配信するサブスクリプションサービスである[J-Quants](https://jpx-jquants.com/)を活用した、金融データの分析例などの技術記事を投稿しております。

この記事では、東証が公表している投資部門別売買状況データを用いて、個人投資家や海外投資家などの投資家別の売買状況をPythonで可視化してみます。今回の記事で使用するデータは、J-Quantsにご加入いただきライトプラン以上を契約するとご利用いただけるものとはなりますが、pandasとplotnineを用いたデータハンドリングや可視化の一例にもなるかと思いますので、ぜひ参考にしていただければ幸いです。

J-Quantsについては巻末の[J-Quantsとは](#j-quantsとは)をご参照ください。

## 投資部門別売買状況とは

投資部門別売買状況とは、個人投資家や海外投資家などの売買主体（＝投資家のカテゴリ）ごとに、買い・売りそれぞれ売買高（株数）と売買代金がどの程度あったかというデータです。投資部門別売買状況は投資家のカテゴリ別の需給を示すことから、株価の推移を説明するデータとして使用されます。

ニュースなどで聞いたことがある方もいらっしゃるかと思いますが、一般的には、TOPIXや日経平均株価が上昇する局面では海外投資家が買い越すことが多いとされます。東証の売買代金の半分以上は海外投資家の売買であるため海外投資家の売買動向が市場全体の需給に大きな影響を与えますが、海外投資家は安くなったら売り、高くなったら買う行動が多いとされるからです。反対に、同じ局面では個人投資家や年金基金が売り越す傾向にあると言われることがあります。安くなったら買い、高くなったら売る行動が多いとされるためです。一定期間の買い越し額・売り越し額だけではなく、金額が増加・減少傾向にあるかどうか（二階微分的なイメージ）や、ある時点からの累計の買い越し額・売り越し額も需給へ影響を与えると考えられます。

投資部門別売買状況データは東証が[Webサイト](https://www.jpx.co.jp/markets/statistics-equities/investor-type/00-00-archives-00.html)にて毎週公表しておりますが、データ分析に適したフォーマットで提供するため、その中でも現物株の週次の売買代金データをJ-QuantsのAPI経由で配信しております。

以上に述べた投資部門別売買状況のデータの見方については、こちらの日経新聞の記事が分かりやすく解説されています。（[株価動向、需給手掛かり - 日本経済新聞](https://www.nikkei.com/article/DGKKZO73137120Y3A720C2PPM000/)）

## 環境

記事執筆時の環境は以下の通りです。

- MacOS (Ventura 13.5.2)
- Python 3.11.5
- numpy 1.26.0
- pandas 2.1.1
- requests 2.31.0
- plotnine 0.12.3
- patchworklib 0.6.2

使うライブラリをimportしておきます。

``` python
import datetime
import json

import numpy as np
import pandas as pd
import pandas.tseries.offsets as offsets
import patchworklib as pw
import requests
from plotnine import *
```

インストールされていなければ先に以下でインストールしてください。

``` {bash}
$ pip install numpy pandas patchworklib requests plotnine
```

## データの取得

J-Quantsからデータを取得するステップは以下の通りです。

- ユーザ認証
  - J-Quantsに登録したメールアドレスとパスワードをリクエストボディに入れて`/token/auth_user`にPOSTしてリフレッシュトークンを得る
  - リフレッシュトークンをクエリストリングとして`/token/auth_refresh`にPOSTしてIDトークンを得る
- データ取得
  - Bearer認証でIDトークンをヘッダーに含めて`/markets/trades_spec`にGETしてデータを得る

今回の分析で使用する投資部門別売買状況データおよびTOPIX四本値データのAPIは、ライトプラン以上でご利用が可能なAPIです。

取得できるデータ期間はご契約のプランにより異なります。ライトユーザでは過去5年分のデータが取得できます。記事執筆時には2008年以降のデータが取得できるプレミアムユーザを利用します。

<details>
<summary>
実装と解説はこちら
</summary>

まずは認証を行います。

``` python
BASE_URI = "https://api.jquants.com/v1"

resp = requests.post(
    f"{BASE_URI}/token/auth_user",
    data=json.dumps({"mailaddress": MAIL_ADDRESS, "password": PASSWORD})
)
REFRESH_TOKEN = resp.json()["refreshToken"]

resp = requests.post(
    f"{BASE_URI}/token/auth_refresh",
    params={"refreshtoken": REFRESH_TOKEN}
)
ID_TOKEN = resp.json()["idToken"]
```

次に投資部門別データを取得します。

J-QuantsのAPIでは、取得するデータサイズが一定以上になるとページネーションを設定します。この場合、レスポンスに`pagination_key`というkey名のvalueが含まれます。`pagination_key`をクエリストリングに含めて再度GETすることで続きのデータを取得します。`pagination_key`がレスポンスに含まれなくなるまでwhileループで回して結果をつなげるように実装すればよいということになります。

`requests.get`の引数`params`にはクエリストリングのkeyとvalueをdictで渡しますが、valueが`None`の場合はクエリストリングに含まれません。

``` python
pagination_key = None
trades_spec = []

while True:
    resp = requests.get(
        f"{BASE_URI}/markets/trades_spec",
        params={"pagination_key": pagination_key},
        headers={"Authorization": f"Bearer {ID_TOKEN}"}
    )
    data = resp.json()
    trades_spec = trades_spec + data["trades_spec"]
    if "pagination_key" in data:
        pagination_key = data["pagination_key"]
    else:
        break
```

</details>

## データを眺めてみる

最後の1レコードを見てみます。

<details>
<summary>
データの中身（長いので折り畳む）
</summary>

``` python
sorted(
    trades_spec,
    key=lambda x: (x["PublishedDate"], x["StartDate"], x["EndDate"], x["Section"]),
    reverse=True
)[0]
```

    {'PublishedDate': '2023-09-28',
     'StartDate': '2023-09-19',
     'EndDate': '2023-09-22',
     'Section': 'TokyoNagoya',
     'ProprietarySales': 1689740533.0,
     'ProprietaryPurchases': 2239117039.0,
     'ProprietaryTotal': 3928857572.0,
     'ProprietaryBalance': 549376506.0,
     'BrokerageSales': 16997640509.0,
     'BrokeragePurchases': 16431306738.0,
     'BrokerageTotal': 33428947247.0,
     'BrokerageBalance': -566333771.0,
     'TotalSales': 18687381042.0,
     'TotalPurchases': 18670423777.0,
     'TotalTotal': 37357804819.0,
     'TotalBalance': -16957265.0,
     'IndividualsSales': 3587097503.0,
     'IndividualsPurchases': 4248398912.0,
     'IndividualsTotal': 7835496415.0,
     'IndividualsBalance': 661301409.0,
     'ForeignersSales': 12068168029.0,
     'ForeignersPurchases': 11154994974.0,
     'ForeignersTotal': 23223163003.0,
     'ForeignersBalance': -913173055.0,
     'SecuritiesCosSales': 92243237.0,
     'SecuritiesCosPurchases': 97207669.0,
     'SecuritiesCosTotal': 189450906.0,
     'SecuritiesCosBalance': 4964432.0,
     'InvestmentTrustsSales': 440424055.0,
     'InvestmentTrustsPurchases': 298225556.0,
     'InvestmentTrustsTotal': 738649611.0,
     'InvestmentTrustsBalance': -142198499.0,
     'BusinessCosSales': 152069462.0,
     'BusinessCosPurchases': 303902857.0,
     'BusinessCosTotal': 455972319.0,
     'BusinessCosBalance': 151833395.0,
     'OtherCosSales': 24556862.0,
     'OtherCosPurchases': 39466258.0,
     'OtherCosTotal': 64023120.0,
     'OtherCosBalance': 14909396.0,
     'InsuranceCosSales': 13296800.0,
     'InsuranceCosPurchases': 14851481.0,
     'InsuranceCosTotal': 28148281.0,
     'InsuranceCosBalance': 1554681.0,
     'CityBKsRegionalBKsEtcSales': 27499042.0,
     'CityBKsRegionalBKsEtcPurchases': 22459412.0,
     'CityBKsRegionalBKsEtcTotal': 49958454.0,
     'CityBKsRegionalBKsEtcBalance': -5039630.0,
     'TrustBanksSales': 579963394.0,
     'TrustBanksPurchases': 239854409.0,
     'TrustBanksTotal': 819817803.0,
     'TrustBanksBalance': -340108985.0,
     'OtherFinancialInstitutionsSales': 12322125.0,
     'OtherFinancialInstitutionsPurchases': 11945210.0,
     'OtherFinancialInstitutionsTotal': 24267335.0,
     'OtherFinancialInstitutionsBalance': -376915.0}

</details>

### データの詳説

株式の週次の売買代金です。PublishedDate（公表日）に公表された、StartDate（開始日）からEndDate（終了日）までの期間における、Section（市場区分）の売買代金（単位は千円）を表します。なお、対象は株式であり、ETFやREIT[^1]は含みません[^2]。

各売買主体について、Purchases（買い）, Sales（売り）, Total（買い+売り）, Balance（買い-売り）の4つの項目があります。

各売買主体は以下のように分けられます。

- Total（合計） = Proprietary（自己勘定） + Brokerage（委託勘定）…（1）
- Brokerage = Individual（個人投資家） + Foreigners（海外投資家） +
  SecuritiesCos（証券会社） + InvestmentTrusts（投資信託） +
  BusinessCos（事業法人） + OtherCos（その他法人等） +
  InsuranceCos（生保・損保） + CityBKsRegionalBKsEtc（都銀・地銀等） +
  TrustBanks（信託銀行） +
  OtherFinancialInstituions（その他金融機関）…（2）

例えば、レスポンスの中のProprietaryPurchasesは自己勘定の買いの金額を表します。

東証を含む証券取引所に注文を発注できるのは、各取引所の取引参加者資格を持つ業者（証券会社）に限られます[^3]。例えば個人投資家が株を買いたいと思ったら、証券会社に口座を開設してその証券会社から発注しますが、この証券会社は取引参加者資格を持っています。このような証券会社経由で発注された売買が「委託勘定」に含まれます。一方、取引参加者資格を持つ業者は、自社自身の注文も発注します[^4]。この注文の約定分（成立した注文）が「自己勘定」です。

（2）の箇条書きの通り、委託勘定は、誰が注文を出したかによって分けられます。（2）に「証券会社」とあるのは、取引参加者資格を持たない証券会社から、取引参加者資格を持つ業者経由で出された注文の約定となります。

Sectionは市場区分を表します。2022/4/4に[東証の市場再編](https://www.jpx.co.jp/equities/market-restructure/index.html)が行われ、東証一部、二部、マザーズ、ジャスダックの4市場がプライム、スタンダード、グロースに組み替えられました。そのため、TSE1st（東証一部）, TSE2nd（東証二部）, TSEJASDAQ（ジャスダック）, TSEMothers（マザーズ）はStartDateが2022/4/3まで、TSEPrime（プライム）, TSEStandard（スタンダード）, TSEGrowth（グロース）はStartDateが2022/4/4以降のレコードにのみ存在します。TokyoNagoyaは全期間で存在し、東証・名証（名古屋証券取引所）の全市場の合計を表します。

## DataFrameの加工

取得したデータをプロットして眺める前にデータを加工します。

まずは取得したデータ（`list[dict]`）を`pd.DataFrame`に変換します。

``` python
df_trades_spec_before_excluding_error_correction = (
    pd.DataFrame(trades_spec)
    .sort_values(["PublishedDate", "StartDate", "Section"])
    .reset_index(drop=True)
    .assign(
        PublishedDate=lambda d: pd.to_datetime(d["PublishedDate"], format="%Y-%m-%d"),
        StartDate=lambda d: pd.to_datetime(d["StartDate"], format="%Y-%m-%d"),
        EndDate=lambda d: pd.to_datetime(d["EndDate"], format="%Y-%m-%d")
    )
)
print(f"{df_trades_spec_before_excluding_error_correction.shape[0]} rows x {df_trades_spec_before_excluding_error_correction.shape[1]} cols")
```

    3734 rows x 56 cols

公表された投資部門別売買代金データに誤りがあった場合は、過誤訂正のレコードが改めて配信されます。過誤訂正前の誤ったレコードを残しておくと、そのレコードと過誤訂正後の正しいレコードが二重にカウントされてしまいます。そのため、各集計期間・市場（StartDate,
Section。StartDateが決まればEndDateは一意に定まるため、groupbyにEndDateは不要です）の中で最も新しい公表日（PublishedDate）のレコードのみを使用することで、過誤訂正が反映されたレコードを用いるようにします。データ分析的にはこういう細かいチェックが大事だったりしますね。

``` python
df_trades_spec = (
    df_trades_spec_before_excluding_error_correction
    .groupby(["StartDate", "Section"])
    .apply(lambda d: d.sort_values(["PublishedDate"]).tail(1))
)
print(f"{df_trades_spec.shape[0]} rows x {df_trades_spec.shape[1]} cols")
```

    3734 rows x 56 cols

先のprint文と行数が変わっていないので過誤訂正レコードは存在しなかったということになります。実際のところ、以下の`shape[0]`が0であるため、元々重複レコードがないことが分かります。

``` python
df_trades_spec_before_excluding_error_correction[
    df_trades_spec_before_excluding_error_correction.duplicated(subset=["StartDate", "Section"])
].shape
```

    (0, 56)

なお、先のコードは、3730個程度x1行のgroupbyされたDataFrame（DataFrameGroupBy）全てで`sort_values(["PublishedDate"])`しているので遅いです。今回の3730個程度であれば筆者の環境では1～2秒で終わりましたので実行時間のパフォーマンスは気にしなくてもよいですが、大規模なDataFrameでは別の方法を検討した方がよいかと思います。

次に、各売買主体の値が個々のカラムに入っている横持ちのデータなので、`pd.DataFrame.melt`で縦持ちに変えます。複数系列のカラムを持つデータのハンドリングの王道パターンですが、そうしておくことで後でカラムを操作しやすくなりますし、グラフを書く際も系列指定で書きやすいです。

``` python
df_trades_spec = (
    df_trades_spec
    .melt(
        id_vars=["PublishedDate", "StartDate", "EndDate", "Section"],
        var_name="InvestorType",
        value_name="Amount"
    )
    .sort_values(["PublishedDate", "StartDate", "Section", "InvestorType"])
    # 億円単位に変換する
    .assign(Amount=lambda d: d["Amount"] * 1000 / 100_000_000)
    # TotalとBrokerageはその内訳が別のInvestorTypeに含まれており、
    # ダブルカウントになるのでカラムを削除する
    .loc[lambda d: ~d["InvestorType"].str.startswith(("Total", "Brokerage"))]
)
```

売買主体の種類が多いため、グラフの描画時に系列が多く見づらくなるので、今回取り上げない比較的値が小さいカテゴリは”\_Other”カテゴリにまとめます。

後ほど売買主体ごとの割合も見たいので、割合を計算するために、groupbyからのtransformによって全売買主体の売買金額の合計をカラムとして追加します。SQLで言うところのwindow関数とかpartition集計とか呼ばれるものですね。

``` python
df_trades_spec_1 = (
    df_trades_spec
    # Totalで終わる値、つまり合計の項目を残す
    .loc[lambda d: d["InvestorType"].str.endswith("Total")]
    .assign(
        InvestorType=lambda d: np.where(
            d["InvestorType"].isin([
                "BusinessCosTotal",
                "CityBKsRegionalBKsEtcTotal",
                "InsuranceCosTotal",
                "OtherCosTotal",
                "OtherFinancialInstitutionsTotal",
                "SecuritiesCosTotal"
            ]),
            "_OthersTotal",
            d["InvestorType"]
        )
    )
    # 上のassignにより、"_OthersTotal"が複数行あるのでgroupby -> aggによって合算する
    .groupby(["PublishedDate", "StartDate", "EndDate", "Section", "InvestorType"])
    .agg(Total=("Amount", "sum"))
    .reset_index()
    .assign(
        TotalSum=lambda d: d.groupby(["PublishedDate", "StartDate", "Section"])["Total"].transform(sum),
        Prop=lambda d: d["Total"] / d["TotalSum"]
    )
)
```

これで加工したDataFrameを得ることができました。DataFrameの行の先頭をprintしてみます。

    |    | PublishedDate       | StartDate           | EndDate             | Section   | InvestorType          |       Total |   TotalSum |       Prop |
    |---:|:--------------------|:--------------------|:--------------------|:----------|:----------------------|------------:|-----------:|-----------:|
    |  0 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE1st    | ForeignersTotal       | 19029.9     | 33417.4    | 0.569462   |
    |  1 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE1st    | IndividualsTotal      |  4642.38    | 33417.4    | 0.138921   |
    |  2 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE1st    | InvestmentTrustsTotal |   290.464   | 33417.4    | 0.00869199 |
    |  3 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE1st    | ProprietaryTotal      |  8339.89    | 33417.4    | 0.249567   |
    |  4 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE1st    | TrustBanksTotal       |   343.478   | 33417.4    | 0.0102784  |
    |  5 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE1st    | _OthersTotal          |   771.249   | 33417.4    | 0.0230792  |
    |  6 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE2nd    | ForeignersTotal       |    21.5992  |    84.8274 | 0.254625   |
    |  7 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE2nd    | IndividualsTotal      |    49.3336  |    84.8274 | 0.581577   |
    |  8 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE2nd    | InvestmentTrustsTotal |     0.70151 |    84.8274 | 0.00826985 |
    |  9 | 2008-01-16 00:00:00 | 2008-01-04 00:00:00 | 2008-01-04 00:00:00 | TSE2nd    | ProprietaryTotal      |     3.92143 |    84.8274 | 0.0462283  |

## プロット

### 売買代金合計（東証・名証全体）

東証・名証全体の売買代金（売り買い合計）をプロットしてみます。単位は億円です。

なお、以下のデータは全て2008/1/4〜2023/9/22のものとなります。

``` python
# plotnineのthemeのデフォルト値を設定する
theme_set(
    theme_minimal()
    +theme(
        legend_position="bottom",
        figure_size=(6.4, 4.8),
        dpi=200
    )
)

(
    ggplot(
        df_trades_spec_1.loc[lambda d: d["Section"] == "TokyoNagoya"],
        aes("EndDate", "Total", fill="InvestorType")
    )
    +geom_area(alpha=0.6, size=0.1, color="black")
    +scale_x_datetime(breaks="1 year", minor_breaks="3 month", date_labels="%Y")
    +labs(
        x="Date",
        y="Total (100M JPY)",
        title="Trading Volume",
        subtitle="(Sum of Purchases and Sells, Weekly, Tokyo + Nagoya)"
    )
).draw()
```

![cell-13-output-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3515935/31fe775e-6741-ddf2-8594-8db7b5373494.png)

2023年では、売買代金は週におよそ40〜45兆円ですね。ただし、ある売買主体の買いの約定はある売買主体の売りの約定となることから、全売買主体の売買代金の合算であるy軸の値は、実際の売買代金の2倍となることに注意が必要です。なので、実際の売買代金は週に20兆円程度となります。

好きなグラフ描画ライブラリを使えばよいですが、記事を書いている中の人がRのtidyverseの中のグラフ描画パッケージであるggplot2に慣れているため、ggplot2に非常に近い[^5][plotnine](https://plotnine.readthedocs.io/en/stable/)を使うことにします。ggplot2ユーザであればすぐ使い始められると思います。

上のコードで何となく伝わるかと思いますが、データ定義、キャンバスのスタイル指定、グラフのスタイル指定、軸の制御とレイヤーを足していくのが描きやすいです。

### 売買代金の構成比率（東証・名証全体）

売買主体ごとの構成比率も見てみましょう。

<details>
<summary>plotするコード</summary>

``` python
(
    ggplot(
        df_trades_spec_1.loc[lambda d: d["Section"] == "TokyoNagoya"],
        aes("EndDate", "Prop", fill="InvestorType")
    )
    +geom_area(color="black", alpha=0.6, size=0.1)
    +scale_x_datetime(breaks="1 year", minor_breaks="3 month", date_labels="%Y")
    +scale_y_continuous(
        breaks=[0, 0.2, 0.4, 0.6, 0.8, 1],
        labels=lambda x: [f'{"{:.0%}".format(i)}' for i in x]
    )
    +labs(
        x="Date",
        y="Proportion",
        title="Proportion of Trading Volume",
        subtitle="(Sum of Purchases and Sells, Weekly, Tokyo + Nagoya)"
    )
).draw()
```

</details>

![cell-14-output-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3515935/3b31c771-2b07-be61-9249-076edd9b1ceb.png)

以下のことが分かります。

- 海外投資家の割合は、最も小さかった2009年頃は40%程度であったが、2010年代前半に増加し続け、直近では60%程度ある。
- 個人投資家の割合は20%程度。
- 自己勘定の割合は、2009年頃は30%程度であったが、直近では20%程度に減った。

### 売買代金合計・売買代金の構成比率（市場別）

さて、これまでは東証と名証の合計をプロットしましたが、売買主体ごとの動向は市場ごとに異なると思われますから、市場ごとにプロットしてみましょう。plotnineであれば`facet_wrap("~Section")`でSectionごとに分割してプロットできます[^6]。

<details>
<summary>plotするコード</summary>

``` python
(
    ggplot(
        df_trades_spec_1,
        aes("EndDate", "Total", fill="InvestorType")
    )
    +geom_area(color="black", alpha=0.6, size=0.05)
    +scale_x_datetime(breaks="5 year", minor_breaks="1 year", date_labels="%Y")
    +labs(
        x="Date",
        y="Total (100M JPY)",
        title="Trading Volume",
        subtitle="(Sum of Purchases and Sells, Weekly, by section)"
    )
    +facet_wrap("~Section", scales="free_y")
).draw()
```

</details>

![cell-15-output-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3515935/ba4807e5-7000-40a7-5daa-1910f7d61435.png)

<details>
<summary>plotするコード</summary>

``` python
(
    ggplot(
        df_trades_spec_1,
        aes("EndDate", "Prop", fill="InvestorType")
    )
    +geom_area(color="black", alpha=0.6, size=0.05)
    +scale_x_datetime(breaks="5 year", minor_breaks="1 year", date_labels="%Y")
    +scale_y_continuous(
        breaks=[0, 0.5, 1],
        minor_breaks=4,
        labels=lambda l: [f'{"{:.0%}".format(i)}' for i in l]
    )
    +labs(
        x="Date",
        y="Proportion",
        title="Proportion of Trading Volume",
        subtitle="(Sum of Purchases and Sells, Weekly, by section)"
    )
    +facet_wrap("~Section", scales="fixed")
).draw()
```

</details>

![cell-16-output-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3515935/c72ebb29-6f27-fb80-44e2-59a828436226.png)

上のグラフは売買主体別売買代金、下のグラフはその構成比です。

TokyoNagoyaの売買代金のほとんどはTSE1st, TSEPrimeですね。TSE1st, TSEPrimeは海外投資家が最も大きい売買主体であり自己勘定も一定程度あることが分かる一方、TSE2nd, TSEJASDAQ, TSEStandard, TSEGrowthは個人投資家が最も大きいか海外投資家と拮抗していることと自己勘定はほとんど見られないことが分かります。

理由はいくつか推測されます。

1.  東証一部とプライムの銘柄は時価総額が大きく流動性が高いため、大きな資金を運用する海外投資家や自己勘定などの国内機関投資家が投資対象（ユニバースと呼びます）としやすい。
2.  海外投資家や国内機関投資家は、日経平均株価やTOPIXの指数に連動する売買を行うから、東証一部やプライムの上場銘柄に売買が集まる[^7]。
3.  海外投資家へのIRに積極的な銘柄は東証一部やプライムの銘柄に多いから、海外投資家が投資しやすい。

### ネットの売買代金（東証・名証全体）

次に、東証・名証合計のネット（純額。買いの売買代金から売りの売買代金を引いたもの）の売買代金を見てみましょう。

ネットの売買代金は週次では正と負の値を行ったり来たりするため、傾向を把握しやすくするために年次単位に丸めてみます。東証の投資部門別売買代金は、例えばStartDateが2023年9月のレコードを2023年9月1週、2週…と扱っていますので、それにならってStartDateの年単位で合算します。

<details>
<summary>データの集計 + plotするコード</summary>

``` python
# まずは週次単位で加工
df_trades_spec_2 = (
    df_trades_spec
    .loc[lambda d: d["InvestorType"].str.endswith("Balance")]
    .assign(
        InvestorType=lambda d: np.where(
            d["InvestorType"].isin([
                "BusinessCosBalance",
                "CityBKsRegionalBKsEtcBalance",
                "InsuranceCosBalance",
                "OtherCosBalance",
                "OtherFinancialInstitutionsBalance",
                "SecuritiesCosBalance"
            ]),
            "_OthersBalance",
            d["InvestorType"]
        )
    )
    .groupby(["PublishedDate", "StartDate", "EndDate", "Section", "InvestorType"])
    .agg(Balance=("Amount", "sum"))
    .reset_index()
)
# 年次単位で合算する
df_trades_spec_3 = (
    df_trades_spec_2
    .assign(
        StartDateYear=lambda d: (d["StartDate"] + offsets.YearEnd()).dt.to_period("Y")
    )
    .groupby(["StartDateYear", "Section", "InvestorType"])
    .agg(Balance=("Balance", "sum"))
    .reset_index()
)

(
    ggplot(
        df_trades_spec_3
        .loc[lambda d: d["Section"] == "TokyoNagoya"],
        aes("StartDateYear", "Balance", fill="InvestorType")
    )
    +geom_bar(stat="identity", alpha=0.6, color="black", size=0.3)
    +scale_y_continuous(minor_breaks=4)
    +labs(
        x="Date",
        y="Balance (100M JPY)",
        title="Trading Volume",
        subtitle="(Net of Purchases and Sells, Yearly, Tokyo + Nagoya)"
    )
).draw()
```

</details>

![cell-17-output-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3515935/777f4907-7b5d-ecd7-ce4d-a7e13992e4b2.png)

2013年に海外投資家が大きく買い越す一方個人投資家が売り越していることが分かりますね。また、海外投資家は2014年以降は必ずしも買い越しているわけではない一方、自己勘定は買い越しが続き、個人投資家は売り越しが続いていることが分かります。

日銀は金融政策の一環として2010年からETFを買い入れています。日銀のETF買い入れは自己勘定の買いに反映されるとされています[^8]。

2015年頃からは[ETFの買い入れ額が大きく](https://money-bu-jpx.com/news/article030529/)なっていますが、この時期に自己勘定の買い越し額も大きくなっていますね。

### ネットの売買代金（東証・名証全体、アベノミクス初期）

最後に、2012/1/1～2013/12/31の期間に絞り、東証・名証全体のネットの売買代金（週次）をTOPIXの終値と並べてみましょう。TOPIX四本値のAPIエンドポイントからTOPIXのデータも取得しておきます。

<details>
<summary>
TOPIXのデータ取得
</summary>

投資部門別売買状況と同様に、APIからのレスポンスに`pagination_key`が含まれなくなるまでwhileループを回します。

``` python
pagination_key = None
topix = []

while True:
    resp = requests.get(
        f"{BASE_URI}/indices/topix",
        params={"pagination_key": pagination_key},
        headers={"Authorization": f"Bearer {ID_TOKEN}"}
    )
    data = resp.json()
    topix = topix + data["topix"]
    if "pagination_key" in data:
        pagination_key = data["pagination_key"]
    else:
        break

df_topix = (
    pd.DataFrame(topix)
    .assign(Date=lambda d: pd.to_datetime(d["Date"], format="%Y-%m-%d"))
    .sort_values(["Date"])
    .reset_index(drop=True)
)
```

</details>

複数のplotを並べるのにpatchworklibというライブラリを使用しています。Rのggplot2を並べるのに使うpatchworkと似ています[^9]。

plotnineではない他のライブラリでもplotを並べるのに使えます。こちらの記事が分かりやすかったです（[Patchworklibの紹介: MatplotlibのSubplotを簡単に。](https://zenn.dev/ponnhide/articles/40ac7dbe0aa4ab)）。

<details>
<summary>plotするコード</summary>

``` python
p1 = (
    ggplot(
        df_trades_spec_2
        .loc[lambda d: d["Section"] == "TokyoNagoya"]
        .loc[
            lambda d: (d["EndDate"] >= datetime.datetime(2012, 1, 1)) &
            (d["EndDate"] <= datetime.datetime(2013, 12, 31))
        ],
        aes("EndDate", "Balance", fill="InvestorType"))
    +geom_bar(stat="identity")
    +scale_x_datetime(breaks="1 year", minor_breaks="1 month", date_labels="%Y/%m")
    +labs(
        x="Date",
        y="Balance (100M JPY)",
        title="Trading Volume\n(Net of Purchases and Sells, Weekly, Tokyo + Nagoya)"
    )
)
p2 = (
    ggplot(
        df_topix
        .loc[
            lambda d: (d["Date"] >= datetime.datetime(2012, 1, 1)) &
            (d["Date"] <= datetime.datetime(2013, 12, 31))
        ],
        aes("Date", "Close")
    )
    +geom_line()
    +scale_x_datetime(breaks="1 year", minor_breaks="1 month", date_labels="%Y/%m")
    +labs(
        x="Date",
        y="TOPIX (Close)",
        title="TOPIX Close"
    )
)

pw.load_ggplot(p1, figsize=(6.4, 2.0)) / pw.load_ggplot(p2, figsize=(6.4, 2.0))
```

</details>

![cell-19-output-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3515935/5ebfa5d5-423d-59a7-79df-888411710bf9.png)

2012年末からのTOPIXの上昇局面では、海外投資家が毎週のように継続して買い越す一方、信託銀行が売り越しを続けていることが分かります。信託銀行とは、主にGPIFなどの年金基金の売買を指すとされます（年金基金は、信託銀行経由で売買しているため）。ちょうどこの時期は、2012年末の「アベノミクス」が掲げられた時期や、2013年4月の金融政策決定会合で物価上昇率2%を政策目標とする量的・質的金融緩和が導入された時期と重なります。

## おわりに + Future Work

投資部門別売買状況データから売買主体別の売買動向をプロットしてみました。一通りのデータハンドリングや可視化のご紹介にもなったかと思います。

今回の記事では取り扱えませんでしたが、売買主体別の売買代金や買い越し額の増加率などを計算してTOPIXの騰落率と相関を調べてみたり、特徴量としてモデルに入れてみたりしてもよいでしょう。また、可視化の際はPlotlyやBokehなどを用いてWebブラウザ上でインタラクティブなグラフを作ってみると面白いかと思います。

## JPX総研について

JPX総研ではJ-Quantsなどの新しいプロダクトを内製開発しています。キャリア採用を通年で行っておりますので、ご興味がある方は以下のリンクをご覧ください！

[JPX総研キャリア採用ページ](https://jpx-mid-career-recruitment.com/)

## J-Quantsとは

J-Quantsとは、ヒストリカル株価データ・財務データなどの金融データを取得できる、個人投資家向けのAPIデータ配信サービスです。投資にまつわるデータを分析しやすい形式で提供し、個人投資家の皆様がデータを活用しやすくなることを目的としております。

個人投資家が投資などのために金融データを分析する際の大きな障壁は、整形された金融データの取得が難しいことであるという理由から、2022年7月にベータ版をリリースいたしました。ご好評の声を頂いたこともあり、2023年4月より正式にリリースしております。

## ディスクレーマー

当記事は、J-Quantsを利用した金融データ分析に関する技術的事項の共有を目的としたものであり、株式などの投資商品への投資の推奨を目的とするものではありません。また、本記事の内容をもとに投資等を行った結果損害等が生じた場合においても、一切の責任を負わないものとします。

## 参考リンク

- [J-Quants](https://jpx-jquants.com/)
- [jquants-api-client-python](https://github.com/J-Quants/jquants-api-client-python)（J-QuantsのPythonクライアントライブラリ）
  - 記事ではrequestsを用いてデータを取得しましたが、ライブラリを使うとより簡単に取得できます

[^1]: ETFは日経平均株価やTOPIXなどの指数に連動して価格が変動する上場投資信託です。REITは不動産を対象とする投資信託です。ETFおよび、REITのうち株式市場に上場しているものは株式と同じように市場で売買できます。

[^2]: 厳密な定義は、[投資部門別売買状況](https://www.jpx.co.jp/markets/statistics-equities/investor-type/07.html)の「対象銘柄」や「対象取引」の項目をご覧ください。

[^3]: [東証の取引参加者一覧](https://www.jpx.co.jp/rules-participants/participants/list/index.html)

[^4]: 例えば、相場変動から自社が利益を得るための注文（プロップと呼びます）や、顧客から受けた注文をリスクヘッジするために出す注文です。

[^5]: [plotnine公式のドキュメント](https://plotnine.readthedocs.io/en/stable/)には、“A Grammar of Graphics for Python; plotnine is an implementation of a grammar of graphics in Python based on ggplot2.”とあります。Grammer of Graphicsはグラフ描画の文法に関する考え方であり、ggplot2はこの考え方をもとに作られたとされます。plotnineはggplot2と関数がかなり似通っているため、Grammer of GraphicsのPython実装というよりは、ggplot2のPython版という方が近いかもしれません。

[^6]: `facet_wrap("~Section")`という書き方に違和感を覚えられるかもしれませんが、これはplotnineの元となっているRのggplot2では`facet_wrap(~Section)`と書くところから来ていますね（そのまんま）。Rですと非標準評価（Non-Standard Evaluation）という仕組みにより、カラムをクオーテーションで囲って文字列にしなくても値を評価できます。チルダはRではformulaというオブジェクトを作るという意味を持つ演算子です。

[^7]: 日経平均株価は市場再編前は東証一部、再編後はプライム上場銘柄のうち225銘柄が選ばれます。TOPIXについては、市場再編前は東証一部上場銘柄が対象、市場再編後は再編後の市場にかかわらず再編前と同じ銘柄が選ばれます。ただし、流通株式時価総額が一定値を下回る銘柄は段階的にTOPIXの算出対象から除外されます。TOPIXの指数計算上のウエイトは浮動株時価総額で決まるため、指数連動型の売買はいずれにせよ浮動株時価総額が大きいプライム上場銘柄に集まることになります。

[^8]: 日銀がETFを買い入れるとき、実際には、取引参加者がETFの構成銘柄（現物株）を株式市場から買い入れてETFを設定し、日銀はETFを信託銀行経由で取引参加者から購入します。そのため、投資部門別売買状況のデータでは取引参加者の買い、すなわち自己勘定の買いに計上されます。詳しくはこちらの記事をご参照ください。[日銀のETF買い入れについて【深堀りETF⑮】\| NEXT FUNDS](https://nextfunds.jp/semi/article1-3.html)

[^9]: ライブラリの作者自身、[patchworklibのGitHubリポジトリ](https://github.com/ponnhide/patchworklib)において、“This library is inspired by patchwork for ggplot2”と書いています。