6  ファクターモデルの導入

# パッケージの読み込み
pacman::p_load(
  tidyverse, # 便利なパッケージ群
  ggthemes, # ggplot2のテーマ集
  broom # モデルの整形
)
# グラフのスタイル設定
mystyle <- list(
  theme_economist_white(
    base_family = "HiraKakuProN-W3"
    ),
  scale_colour_economist(),
  theme(
    text = element_text(size = 12),
    axis.title = element_text(size = 12)
  )
)

6.1 ファクター構築の準備

  1. CAPMの実証的検証に必要な市場ポートフォリオの構築
  2. ある特徴に基づいて各銘柄をランキングにし,ランキングに応じたポートフォリオの構築

6.2 市場ポートフォリオの構築

市場ポートフォリオ(market portfolio)とは,市場に存在する全ての危険資産を時価総額比率で保有したポートフォリオをいいます。

厳密には,リスク資産には株式や債券に代表される金融資産の他、不動産や貴金属などの実物資産も含まれますが、実用上はTOPIXやS&P500といった株価指数と同一視されることが多いです。

入手可能な全銘柄の前年末時価総額\sum ME_{j,t-1}と個別銘柄の時価総額 ME_{i,t-1} の比率をウェイト w_{i,t}^M として市場ポートフォリオを構築します。

w_{i,t}^M = \frac{ME_{i,t-1}^{12\text{月}}}{\sum_{j = 1}^N ME_{j,t-1}^{12\text{月}}}

株価は日々変動するため、時価総額も変動します。 そのため、毎年1月に時価の変動で崩れた比率をリセットするために、市場ポートフォリオの中身を入れ替えるリバランスを行います。

では練習用データで市場ポートフォリオを構築してみましょう。

monthly_data <- read_csv("data/ch05_output1.csv")
annual_data <- read_csv("data/ch05_output2.csv")

monthly_data月次の株式データが収録されており、 annual_data年次の財務データが収録されています。 データの構造を確認しておきます。

glimpse(monthly_data)
Rows: 95,040
Columns: 24
$ year        <dbl> 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015…
$ month       <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7…
$ month_ID    <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,…
$ firm_ID     <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ stock_price <dbl> 954, 960, 1113, 1081, 1317, 1366, 1353, 1209, 1291, 1407, …
$ DPS         <dbl> 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 40, 0…
$ n_shares    <dbl> 2422000, 2422000, 2422000, 2422000, 2422000, 2422000, 2422…
$ adj_coef    <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ R_F         <dbl> 6.506826e-04, 5.834099e-04, 6.114423e-04, 6.848180e-04, 7.…
$ ME          <dbl> 2310588000, 2325120000, 2695686000, 2618182000, 3189774000…
$ R           <dbl> NA, 0.006289308, 0.159375000, -0.028751123, 0.218316374, 0…
$ Re          <dbl> NA, 0.005705898, 0.158763558, -0.029435941, 0.217579602, 0…
$ industry_ID <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 1, 1, 1, 1…
$ sales       <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 5948.96, 5…
$ OX          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 564.14, 56…
$ NFE         <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 50.66750, …
$ X           <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 513.48, 51…
$ OA          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 13865.58, …
$ FA          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 4642.16, 4…
$ OL          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 4534.22, 4…
$ FO          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 3959.70, 3…
$ BE          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 10013.82, …
$ lagged_BE   <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ ROE         <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
glimpse(annual_data)
Rows: 7,920
Columns: 20
$ year        <dbl> 2015, 2016, 2017, 2018, 2019, 2020, 2015, 2016, 2017, 2018…
$ firm_ID     <dbl> 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4…
$ ME          <dbl> 3577.294, 6883.324, 11376.990, 8694.752, 13957.518, 9708.9…
$ annual_R    <dbl> NA, 0.99727265, 0.68786382, -0.21361287, 0.64683576, -0.28…
$ annual_R_F  <dbl> 7.432089e-03, 5.649890e-04, 4.884804e-05, 5.786109e-03, -7…
$ annual_Re   <dbl> NA, 0.99670766, 0.68781497, -0.21939898, 0.64760569, -0.28…
$ industry_ID <dbl> NA, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ sales       <dbl> NA, 5948.96, 6505.06, 6846.38, 7572.24, 7537.63, 3505.75, …
$ OX          <dbl> NA, 564.14, 691.18, 751.29, 958.53, 778.37, 45.82, 51.25, …
$ NFE         <dbl> NA, 50.667498, 29.543157, 86.486500, 298.049774, -65.45877…
$ X           <dbl> NA, 513.48, 661.64, 664.80, 660.48, 843.83, 40.07, 49.37, …
$ OA          <dbl> NA, 13865.58, 13952.58, 18818.48, 18190.00, 20462.86, 2977…
$ FA          <dbl> NA, 4642.16, 7743.99, 7284.72, 9735.13, 10274.25, 2258.33,…
$ OL          <dbl> NA, 4534.22, 5111.22, 5137.28, 5487.96, 5371.38, 1840.35, …
$ FO          <dbl> NA, 3959.70, 6159.02, 10123.91, 11362.22, 13772.15, 2340.8…
$ BE          <dbl> NA, 10013.82, 10426.33, 10842.01, 11074.95, 11593.58, 1054…
$ lagged_BE   <dbl> NA, NA, 10013.82, 10426.33, 10842.01, 11074.95, NA, 1054.9…
$ ROE         <dbl> NA, NA, 0.06607269, 0.06376165, 0.06091859, 0.07619267, NA…
$ lag_ME      <dbl> NA, 3577.294, 6883.324, 11376.990, 8694.752, 13957.518, NA…
$ lagged_BEME <dbl> NA, NA, 1.4547942, 0.9164401, 1.2469602, 0.7934756, NA, 0.…

この銘柄ごとの保有比率を計算するために,前年度末の時価総額を計算し,lagged_MEに代入します。 annual_dataは2015年から2020年のデータが入っています。 lag()で前期末の時価総額をlagged_MEに代入しようとしても2015年の前年のデータは存在しないので,欠損値になることに注意しましょう。

annual_data <- annual_data |>
    arrange(firm_ID, year) |> # firm_IDとyearで並び換え
    mutate(
      .by = firm_ID, # 企業ごと
      lagged_ME = lag(ME) # 前期末時価総額
      )

この処理の結果がおおよそこんな感じになっているはずです。

firm_ID year ME lagged_ME
1 2015 3577.294 NA
1 2016 6883.324 3577.294
1 2017 11376.990 6883.324

このlagged_MEを使って保有比率を計算します。 年度ごとに時価総額を合計し、ある企業の前期末時価総額を合計時価総額で割ることで保有比率w_Mを計算します。

annual_data <- annual_data |>
    mutate(
      .by = year, # 年度ごとに
      # ウェイト
      w_M = lagged_ME / sum(lagged_ME, na.rm = TRUE)
    )

2015年のlagged_MEは欠損値なので,w_Mも欠損値になっていますが,2015年のデータはもう使わないので無視します。

次に,2016年以降の欠損値の行を削除するのではなく,保有比率w_Mをゼロに置き換えることで投資しないことを表しますmutate()replace()を用いて変数の置き換えをします。

annual_data <- annual_data |>
    mutate( # w_Mの欠損値を0に置き換える
        w_M = replace(
          w_M,
          year >= 2016 & is.na(w_M),
          0
        )
    )

この処理は、

  1. annual_dataannual_dataを代入し直す
  2. mutate()関数で変数を変換する
  3. replace()関数でw_Mの値を置き換える
    1. w_Mに対して、replace()関数を適用し、
    2. year >= 2016 かつ
    3. 欠損値かどうかを判定する関数is.na()を使って判別したw_Mが欠損値の場合に、
    4. w_Mの値を0に置き換える

replace()関数は,第1引数のデータに対して,第2引数の条件を満たす要素を,第3引数の値に置き換えます。 ここでは,w_Mに対して,yearが2016以上で,かつw_Mが欠損値の場合のw_Mを0に置き換えています。

作成した保有比率を表すウェイトw_Mの合計が1になっているかどうかを確認します。

annual_data |>
    summarise(
        weight_sum = round(sum(w_M), digits = 2),
        .by = year
    )
# A tibble: 6 × 2
   year weight_sum
  <dbl>      <dbl>
1  2015         NA
2  2016          1
3  2017          1
4  2018          1
5  2019          1
6  2020          1

確認できました。 これまでの操作で変数を追加したannual_datamonthly_dataに結合します。 完全外部結合(full outer join)を行います。 完全外部結合とは,データベースを連結する操作の1つで、2つのデータフレームからそれぞれ特定のキーとなる列を指定して,キーの値が一致する行同士は連結し、一致しない残りの行もそのまますべて抽出するものです。

ではfull_join()関数を使って,annual_datamonthly_datayearfirm_IDの2つのキーで結合し,その結果をmonthly_dataに代入します。

月次データに保有比率のデータを追加
monthly_data <- annual_data |>
  select(year, firm_ID, w_M) |> # 必要な変数のみ
  full_join(monthly_data, by = c("year", "firm_ID")) |> # 完全外部結合
  select(-w_M, w_M) # w_Mを最終列に移動

できあがった拡大データセットmonthly_dataを確認します。

glimpse(monthly_data)
Rows: 95,040
Columns: 25
$ year        <dbl> 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015, 2015…
$ firm_ID     <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ month       <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7…
$ month_ID    <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,…
$ stock_price <dbl> 954, 960, 1113, 1081, 1317, 1366, 1353, 1209, 1291, 1407, …
$ DPS         <dbl> 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 29, 0, 0, 0, 0, 0, 40, 0…
$ n_shares    <dbl> 2422000, 2422000, 2422000, 2422000, 2422000, 2422000, 2422…
$ adj_coef    <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ R_F         <dbl> 6.506826e-04, 5.834099e-04, 6.114423e-04, 6.848180e-04, 7.…
$ ME          <dbl> 2310588000, 2325120000, 2695686000, 2618182000, 3189774000…
$ R           <dbl> NA, 0.006289308, 0.159375000, -0.028751123, 0.218316374, 0…
$ Re          <dbl> NA, 0.005705898, 0.158763558, -0.029435941, 0.217579602, 0…
$ industry_ID <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 1, 1, 1, 1…
$ sales       <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 5948.96, 5…
$ OX          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 564.14, 56…
$ NFE         <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 50.66750, …
$ X           <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 513.48, 51…
$ OA          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 13865.58, …
$ FA          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 4642.16, 4…
$ OL          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 4534.22, 4…
$ FO          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 3959.70, 3…
$ BE          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 10013.82, …
$ lagged_BE   <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ ROE         <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ w_M         <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 2.233661e-…

準備が整ったので,市場ポートフォリオの月次リターンを計算します。 t時点における市場ポートフォリオのリターンR_{M,t}は、個別銘柄のリターンR_{i,t}とウェイトw_{i,t}^Mの積の合計で表されます。

R_{M,t} = \sum_{i=1}^{N} w_{i,t}^M R_{i,t}

これをRで実装します。 monthly_datamonth_IDでグループ化し,summarise()関数を用いて,R_Mを計算し,その後でmutate()関数を用いて,R_Meを計算し,その結果をfactor_dataに代入します。

市場ポートフォリオの月次リターンを計算
factor_data <- monthly_data |>
  filter(month_ID >= 13) |> # 2016以降のデータを抽出
  summarise(
    R_F = R_F[1], # 無リスク金利を抽出
    R_M = sum(w_M * R, na.rm = TRUE), # 月次リターンの加重平均
    .by = month_ID
  ) |>
  mutate(R_Me = R_M - R_F) # 月次超過リターン変数を作成

factor_dataの中身をsummary()で確認します。

summary(factor_data)
    month_ID          R_F                  R_M                 R_Me          
 Min.   :13.00   Min.   :-2.329e-04   Min.   :-0.102438   Min.   :-0.102438  
 1st Qu.:27.75   1st Qu.:-4.107e-05   1st Qu.:-0.011056   1st Qu.:-0.010890  
 Median :42.50   Median : 3.870e-05   Median : 0.006081   Median : 0.006186  
 Mean   :42.50   Mean   : 9.991e-05   Mean   : 0.004100   Mean   : 0.004000  
 3rd Qu.:57.25   3rd Qu.: 1.323e-04   3rd Qu.: 0.031698   3rd Qu.: 0.031649  
 Max.   :72.00   Max.   : 6.326e-04   Max.   : 0.111043   Max.   : 0.110819  

作成した市場ポートフォリオの超過リターンをヒストグラムにして分布を確認します。

# 市場ポートフォリオの月次超過リターンをヒストグラムで可視化
ggplot(factor_data) + aes(x = R_Me) + geom_histogram() +
  labs(x = "市場ポートフォリオの月次超過リターン", y = "度数") + mystyle

次に、市場ポートフォリオの累積リターンを計算します。 計算の仮定は以下の通りです。

  1. month_IDが13の月初から運用スタートし、バイアンドホールドで運用すると仮定する。
  2. 毎年1月にコストなしでリバランスし、リバランス前後で元本の変動はないと仮定する。

市場ポートフォリオの累積グロス・リターンを計算します。

市場ポートフォリオの累積リターンの可視化 (1)
df_g <- factor_data |>
  mutate(
    gross_R_M = 1 + R_M, # rに1足してグロスリターン
    cumulative_gross_R_M = cumprod(gross_R_M) # 累積グロスリターン
    )

作成した累積グロス・リターンを折れ線グラフで可視化します。

g <- ggplot(df_g) + aes(x = month_ID, y = cumulative_gross_R_M) + geom_line()
g <- g + labs(x = "Month ID", y = "累積グロスリターン") + mystyle
print(g)

累積リターンであることが一発で分かるように、始点を1として、折れ線グラフを描き直します。 rbind()で始点となるデータを追加し、geom_hline()で始点の水準を点線で図示します。

市場ポートフォリオの累積リターンの可視化 (2)
df_g <- factor_data |>
  mutate(
    gross_R_M = 1 + R_M,
    cumulative_gross_R_M = cumprod(gross_R_M)
  ) |>
  select(month_ID, cumulative_gross_R_M) |>
  add_row(month_ID = 12, cumulative_gross_R_M = 1, .before = 1)

# 折れ線グラフを作成
g <- ggplot(df_g) +
  geom_line(aes(x = month_ID, y = cumulative_gross_R_M)) +
  geom_hline(yintercept = 1, linetype = "dotted", color = "red") + # 元本の水準を点線で図示
  labs(x = "Month ID", y = "Cumulative Gross Return") +
  scale_x_continuous(expand = c(0, 0)) + ylim(0.5,1.5) +  mystyle
print(g)

6.3 ポートフォリオ・ソート

ある特性に基づいて株式銘柄をランキングにし、そのランキングに基づいてポートフォリオを構築することをポートフォリオ・ソートと呼びます。 ポートフォリオ・ソートは、ファクター・モデルの検証において重要な手法です。 ここでは前年度末の時価総額に基づいて、企業を10個のグループに分類して、実現リターンの比較をしてみましょう。

図6.2 前年度末の時価総額に基づくポートフォリオ・ソート

図6.2 前年度末の時価総額に基づくポートフォリオ・ソート

Rで時価総額ランキングを作成するには、ntile()関数を用います。 ntile()関数は、データを指定した数のグループに分類します。 以下のコードでは、mutate()関数でME_rank10を新たに作成しています。 ME_rank10は、lagged_ME変数をntile()関数で10個に分類し、as.factor()関数で因子型に変換したものです。

# ch06_11: 前年度末の時価総額に基づくポートフォリオ・ソート (1)
annual_data <- annual_data |>
  mutate(
    ME_rank10 = as.factor(ntile(lagged_ME, 10)),
    .by = year
  ) # ntile()関数を用いて十個のグループに分類
head(annual_data)
# A tibble: 6 × 23
   year firm_ID     ME annual_R annual_R_F annual_Re industry_ID sales    OX
  <dbl>   <dbl>  <dbl>    <dbl>      <dbl>     <dbl>       <dbl> <dbl> <dbl>
1  2015       1  3577.   NA      0.00743      NA              NA   NA    NA 
2  2016       1  6883.    0.997  0.000565      0.997           1 5949.  564.
3  2017       1 11377.    0.688  0.0000488     0.688           1 6505.  691.
4  2018       1  8695.   -0.214  0.00579      -0.219           1 6846.  751.
5  2019       1 13958.    0.647 -0.000770      0.648           1 7572.  959.
6  2020       1  9709.   -0.284  0.000380     -0.285           1 7538.  778.
# ℹ 14 more variables: NFE <dbl>, X <dbl>, OA <dbl>, FA <dbl>, OL <dbl>,
#   FO <dbl>, BE <dbl>, lagged_BE <dbl>, ROE <dbl>, lag_ME <dbl>,
#   lagged_BEME <dbl>, lagged_ME <dbl>, w_M <dbl>, ME_rank10 <fct>

ME_rank10の値と、年・ランキングごとの会社数を確認してみましょう。

summary(annual_data$ME_rank10)
   1    2    3    4    5    6    7    8    9   10 NA's 
 643  642  641  641  640  640  640  640  639  639 1515 
table(annual_data$year,  annual_data$ME_rank10)
      
         1   2   3   4   5   6   7   8   9  10
  2015   0   0   0   0   0   0   0   0   0   0
  2016 125 124 124 124 124 124 124 124 124 124
  2017 127 127 127 127 127 127 127 127 127 127
  2018 127 127 127 127 127 127 127 127 126 126
  2019 131 131 131 131 130 130 130 130 130 130
  2020 133 133 132 132 132 132 132 132 132 132

ここでは、ME_rank10の値が10の企業が時価総額ランキングの上位10%に、1の企業が時価総額ランキングの下位10%に属することを意味します。

前回と同様に、full_join()関数でmonthly_dataannual_dataを結合します。 drop_na()関数で欠損行を削除し、.by =month_IDME_rank10に関してグループ化した上で、summarize()関数で月次超過リターンReの平均値を計算して、Re変数としています。

前年度末の時価総額に基づくポートフォリオ・ソート
ME_sorted_portfolio <- annual_data |>
  select(year, firm_ID, ME_rank10) |> # 年次データから追加したい情報を抽出
  full_join(monthly_data, by = c("year", "firm_ID")) |> # yearとfirm_IDをキーに月次データと結合
  drop_na() |> # 欠損行を削除
  summarize(
    # 各グループで月次超過リターンの平均値を計算
    Re = mean(Re),
    .by = c(month_ID, ME_rank10)
    )
ME_sorted_portfolio
# A tibble: 600 × 3
   month_ID ME_rank10       Re
      <dbl> <fct>        <dbl>
 1       25 2          0.0976 
 2       26 2          0.00392
 3       27 2          0.0172 
 4       28 2          0.0773 
 5       29 2          0.00948
 6       30 2          0.0473 
 7       31 2          0.0313 
 8       32 2          0.0136 
 9       33 2          0.00911
10       34 2         -0.0962 
# ℹ 590 more rows

準備が出来たので、各ポートフォリオの平均超過リターンを可視化してみましょう。 これにより、時価総額の大きい企業のポートフォリオが、時価総額の小さい企業のポートフォリオよりも高い、あるいは低いリターンを上げているかどうかを確認することができます。

各ポートフォリオの平均超過リターンを可視化
ME_cross_sectional_return <- ME_sorted_portfolio |>
  summarize(
    mean_Re = mean(Re),
    .by = ME_rank10
  ) # 月次超過リターンの平均値を計算

g <- ggplot(ME_cross_sectional_return) +
  aes(x = ME_rank10, y = mean_Re) +
  geom_col() + # 棒グラフ
  xlab("時価総額ランク") + ylab("平均月次超過リターン") +
  scale_y_continuous(expand = c(0, 0)) +
  ylim(0,0.02) + mystyle
print(g)

小型株ほど月次超過リターンの平均が高いことが分かりました。 このように、時価総額の大きい企業のポートフォリオと小さい企業のポートフォリオのリターンの差をサイズ・プレミアムと呼びます。

先ほどは各ポートフォリオの区分を同じウェイトで保有した場合のリターンを計算しましたが,コラムでは,時価総額の大きさに応じてウェイトを変えた時価総額加重ポートフォリオを作成して,先ほどの結果を再現してみる。

まずは等加重の場合のコードを確認する。

BPRに基づくポートフォリオ・ソート(等加重の場合)
annual_data <- annual_data |>
  mutate(
    lagged_BEME = lagged_BE / lagged_ME
    ) |>
  mutate(
    # 簿価時価比率に基づいて十個のグループに分類
    BEME_rank10 = as.factor(ntile(lagged_BEME, 10)),
    .by = year
    )

BEME_sorted_portfolio <- annual_data |>
  select(year, firm_ID, BEME_rank10, lagged_ME) |>
  full_join(monthly_data, by = c("year", "firm_ID")) |>
  drop_na() |>
  summarize(
    # 月次超過リターンの平均値を計算
    Re = mean(Re),
    .by = c(month_ID, BEME_rank10)
  )

# 作図
BEME_sorted_portfolio |>
  summarize(
    mean_Re = mean(Re),
    .by = BEME_rank10
  ) |>
  ggplot() +
  geom_col(aes(x = BEME_rank10, y = mean_Re)) +
  geom_hline(yintercept = 0) + # y = 0の直線を追加
  labs(x = "BE/ME Rank", y = "Mean Monthly Excess Return") +
  scale_y_continuous(limits = c(-0.005, 0.02)) + mystyle

次に時価総額加重の場合のコードを確認します。

BPRに基づくポートフォリオ・ソート(時価総額加重の場合)
# 中盤で保有比率wと月次超過リターンReを計算している箇所を除けば,ch06_15aと全く同じ
annual_data <- annual_data |>
  mutate(
    lagged_BEME = lagged_BE / lagged_ME
    ) |>
  mutate(
    BEME_rank10 = as.factor(ntile(lagged_BEME, 10)),
    .by = year
    )

BEME_sorted_portfolio <- annual_data |>
  select(year, firm_ID, BEME_rank10, lagged_ME) |>
  full_join(monthly_data, by = c("year", "firm_ID")) |>
  drop_na() |>
  mutate(
    # 各ポートフォリオで保有比率を計算
    w = lagged_ME / sum(lagged_ME),
    .by = c(month_ID, BEME_rank10)
  ) |>
  summarize(
    # 時価総額加重の月次超過リターンを計算
    Re = sum(w * Re),
    .by = c(month_ID, BEME_rank10)
  )

BEME_sorted_portfolio |>
  summarize(
    mean_Re = mean(Re),
    .by = BEME_rank10
  ) |>
  ggplot() +
  geom_col(aes(x = BEME_rank10, y = mean_Re)) +
  geom_hline(yintercept = 0) +
  labs(x = "BE/ME Rank", y = "Mean Monthly Excess Return") +
  scale_y_continuous(limits = c(-0.005, 0.02)) + mystyle

結果が異なっていることに注意しましょう。

次節では,この現象を,資産価格モデルの1つであるCAPM(Capital Asset Pricing Model)が説明できるかどうかを検証します。

6.4 CAPMの実証的な検証

6.4.1 CAPMを検証する意義

まずはCAPMの復習から始めましょう。 CAPMは,資産の期待リターンを,市場ポートフォリオの期待リターンと市場ポートフォリオとの共分散で説明するモデルです。

NoteCAPM
  • 第1命題: 市場ポートフォリオは接点ポートフォリオと一致し,効率的フ ロンティア(資本市場線)上に位置する.
  • 第2命題: 各証券のリスクプレミアムは,その証券のマーケット・ベータ に比例する.

\mathbb{E}[R_i] - R_F = \beta_i \left ( \mathbb{E}[R_M] - R_F \right ) ただし, \beta_i = \frac{\mathrm{COV}_{R_i, R_M}}{\mathrm{Var}_M}

このCAPMを回帰式で表現すると次のようになります。

R_{i,t}^e = \beta_i \times R_{M,t}^e + \varepsilon_{i,t} ここで、R^e_{i,t} = R_{i,t} - R_{F,t}である。 つまり、t時点における証券iの実現超過リターンR_{i,t}^eは、t時点における市場ポートフォリオの実現超過リターンR_{M,t}^eと、証券iの市場ポートフォリオに対するベータ係数\beta_iの積に、誤差項\varepsilon_{i,t}を加えたものとして表現されます。

また、誤差項\varepsilon_{i,t}に関して次の仮定を置きます。

  • varepsilon _{i,t}は独立同一分布(i.i.d.)に従う
  • E[\varepsilon_{i,t}] = 0
  • E[R_{M,t}^e , \ \varepsilon_{i,t} ] = 0

こうすることで、CAPM式を線形回帰モデルで表現できるので、\beta_iの推定が可能となります。

6.4.1.1 時系列回帰

CAPM式は任意のi証券で成立するモデルのため、ポートフォリオにも応用できます。 つまりあるポートフォリオの超過リターンを、市場ポートフォリオの超過リターンと、そのポートフォリオに対するベータ係数の積で説明することができます。

R_{P,t}^e = \alpha _P + \beta_P R_{M,t}^e + \varepsilon_{P,t}

CAPMの式と比較すると、切片である\alpha _Pが追加されていることが分かります。 もし証券市場にCAPMの関係が成立しているなら、\alpha _Pはゼロとなっているはずです。 この$$を調べることで、CAPMの検証が可能となります。

ここでは、時系列回帰を使って、市場ポートフォリオの超過リターンを説明変数として、各ポートフォリオの超過リターンを説明するモデルを推定します。

# ch06_16: 市場ポートフォリオの超過リターンを追加

ME_sorted_portfolio <- factor_data |>
  select(-R_F) |> # 無リスク金利は重複するので結合前に削除
  full_join(ME_sorted_portfolio, by = "month_ID") |> # month_IDをキーに
  select(-R_Me, R_Me) # R_Meを最終列へ移動
# ch06_17: 時系列回帰 (1)

ME_sorted_portfolio |>
  filter(ME_rank10 == 1) |> # 時価総額が最小のポートフォリオを抽出
  lm(Re ~ R_Me, data = _) |> # .を使ってlm()関数の第二引数にデータを代入
  tidy() # 線形回帰の結果をtidy()関数でデータフレームに変換
# A tibble: 2 × 5
  term        estimate std.error statistic       p.value
  <chr>          <dbl>     <dbl>     <dbl>         <dbl>
1 (Intercept)   0.0121   0.00404      3.00 0.00395      
2 R_Me          0.654    0.0976       6.70 0.00000000937
# ch06_18: 時系列回帰 (2)

ME_sorted_portfolio |>
  filter(ME_rank10 == 1) |>
  ggplot(aes(x = R_Me, y = Re)) + # aes()関数はggplot()関数の中にも代入可能
  geom_point() +  # geom_point()関数と次のgeom_smooth()関数で共通のaes()関数を受け取る
  geom_smooth(method = "lm", color = "black") +
  labs(x = "Excess Return of Market Portfolio", y = "Excess Return of Small Size Portfolio") +
  theme_classic()

6.4.1.2 ポートフォリオごとの回帰

CAPMの実証的な検証 (1)
# 推定結果を保存するために空のリストを準備
CAPM_results <- vector("list", 10)

for(i in 1:10){

  CAPM_results[[i]] <- ME_sorted_portfolio |>
    filter(ME_rank10 == i) |>
    lm(Re ~ R_Me, data = _) |>
    tidy() |>
    mutate(ME_rank10 = i) |> # 推定対象のポートフォリオ名を保存
    select(ME_rank10, everything()) # ME_rank10を第一列に移動
}
CAPMの実証的な検証 (2)
# リストを一つのデータフレームにまとめる
CAPM_results <- do.call(rbind, CAPM_results)
グループごとの線形回帰 (1) lapply()関数を使う場合
ME_sorted_portfolio_splitted <- split(ME_sorted_portfolio, ME_sorted_portfolio$ME_rank10) # 元データをME_rank10の値に応じて十個のデータフレームに分割

estimate_CAPM <- function(return_data) { # リターン・データを受け取り, CAPMの推定結果をデータフレームで返す関数を準備
  lm_results <- lm(Re ~ R_Me, data = return_data)
  tidied_lm_results <- tidy(lm_results)
}

CAPM_results_by_lapply <- lapply(ME_sorted_portfolio_splitted, estimate_CAPM)
# lapply()関数は第一引数にリスト, 第二引数に関数を取る
# lapplyの返り値はリストなので,一つのデータフレームにまとめたい場合はdo.call()関数を用いる
グループごとの線形回帰 (2) map()関数を使う場合
ME_sorted_portfolio |>
  nest(.by = ME_rank10) |> # 【修正1】ここでグループ化を指定して畳み込む
  mutate(
    # map()関数を用いて各グループを線形回帰
    CAPM_regression = map(data, ~lm(Re ~ R_Me, data = .x)),
    CAPM_summary = map(CAPM_regression, tidy)
    # 【修正2】ここは既にランクごとの行になっているので .by は不要
    ) |>
  select(-c(data, CAPM_regression)) |>
  unnest(cols = CAPM_summary)
# A tibble: 20 × 6
   ME_rank10 term         estimate std.error statistic  p.value
   <fct>     <chr>           <dbl>     <dbl>     <dbl>    <dbl>
 1 2         (Intercept)  0.0106     0.00375     2.83  6.44e- 3
 2 2         R_Me         0.711      0.0908      7.83  1.19e-10
 3 8         (Intercept)  0.00122    0.00168     0.723 4.73e- 1
 4 8         R_Me         0.956      0.0407     23.5   2.59e-31
 5 4         (Intercept)  0.00957    0.00289     3.31  1.62e- 3
 6 4         R_Me         0.848      0.0699     12.1   1.54e-17
 7 7         (Intercept)  0.00284    0.00173     1.64  1.06e- 1
 8 7         R_Me         0.943      0.0418     22.6   2.16e-30
 9 1         (Intercept)  0.0121     0.00404     3.00  3.95e- 3
10 1         R_Me         0.654      0.0976      6.70  9.37e- 9
11 9         (Intercept)  0.000406   0.00144     0.282 7.79e- 1
12 9         R_Me         1.03       0.0349     29.5   1.33e-36
13 3         (Intercept)  0.0120     0.00312     3.86  2.90e- 4
14 3         R_Me         0.770      0.0754     10.2   1.42e-14
15 6         (Intercept)  0.00653    0.00195     3.34  1.45e- 3
16 6         R_Me         0.904      0.0472     19.2   9.39e-27
17 5         (Intercept)  0.00728    0.00234     3.11  2.86e- 3
18 5         R_Me         0.896      0.0565     15.9   9.35e-23
19 10        (Intercept) -0.000659   0.00113    -0.582 5.63e- 1
20 10        R_Me         1.06       0.0273     38.9   2.93e-43

6.4.1.3 CAPMアルファ

CAPMアルファの可視化
CAPM_results |>
  filter(term == "(Intercept)") |> # 定数項に関する推定結果のみを抽出
  mutate(
    # ME_rank10を整数型からファクター型に
    ME_rank10 = as.factor(ME_rank10)
  ) |>
  # 横軸をME_rank10, 縦軸をCAPM_alphaとする棒グラフ
  ggplot() + aes(x = ME_rank10, y = estimate) +
  geom_col() + # 棒グラフ
  geom_hline(yintercept = 0) + # y = 0の直線を追加
  labs(x = "ME Rank", y = "CAPM alpha") +
  scale_y_continuous(limits = c(-0.003, 0.013)) +
  mystyle

CAPMアルファの統計的な有意性を評価
CAPM_results |>
  filter(term == "(Intercept)") |> # 定数項を抽出
  rename(
    # 列名を変更
    CAPM_alpha = estimate,
    p_value = p.value
  ) |>
  mutate(
    # 統計的に有意な結果を*で強調
    significance = cut(p_value,
                        breaks = c(0, 0.01, 0.05, 0.1, 1),
                        labels = c("***", "**", "*", ""),
                        include.lowest = TRUE)
  ) |>
  select(# 出力したい列を指定
    ME_rank10, CAPM_alpha, p_value, significance
  )
# A tibble: 10 × 4
   ME_rank10 CAPM_alpha  p_value significance
       <int>      <dbl>    <dbl> <fct>       
 1         1   0.0121   0.00395  "***"       
 2         2   0.0106   0.00644  "***"       
 3         3   0.0120   0.000290 "***"       
 4         4   0.00957  0.00162  "***"       
 5         5   0.00728  0.00286  "***"       
 6         6   0.00653  0.00145  "***"       
 7         7   0.00284  0.106    ""          
 8         8   0.00122  0.473    ""          
 9         9   0.000406 0.779    ""          
10        10  -0.000659 0.563    ""          
証券市場線の推定
ME_cross_sectional_return <- CAPM_results |>
  filter(term == "R_Me") |> # R_Meの係数を抽出
  rename(CAPM_beta = estimate) |> # 名称変更
  select(ME_rank10, CAPM_beta) |> # 変数を選択
  mutate( # ファクター型に変換
    ME_rank10 = as.factor(ME_rank10)
  ) |>
  # 超過リターンのデータと結合
  full_join(
    ME_cross_sectional_return, ., by = "ME_rank10"
  )

# 平均超過リターンと平均市場ポートフォリオ超過リターンを計算
mean_R_Me <- mean(factor_data$R_Me)

ggplot(ME_cross_sectional_return) +
  aes(x = CAPM_beta, y = mean_Re) +
  geom_point() + # 散布図
  geom_abline(intercept = 0, slope = mean_R_Me) + # 証券市場線
  labs(x = "市場beta", y = "平均超過リターン") + #
  scale_x_continuous(limits = c(0, 1.2), expand = c(0, 0)) +
  scale_y_continuous(limits = c(0, 0.02)) + mystyle

6.4.2 Fama-Frenchの3ファクター・モデル

6.4.2.1 銘柄のランク付け

前年度の時価総額に基づくランク付け
annual_data <- annual_data |>
  mutate(
    # lagged_BEMEが欠損している場合は欠損扱いに
    lagged_ME = replace(lagged_ME, is.na(lagged_BEME), NA)
  ) |>
  mutate(
    .by = year,
    ME_rank2 = as.factor(ntile(lagged_ME, 2))
  )
簿価時価比率に基づくランク付け (1)
annual_data |>
  mutate(
    # 年度ごとに簿価時価比率のパーセンタイル順位を計算
    BEME_percent_rank = percent_rank(lagged_BEME),
    .by = year
  )
# A tibble: 7,920 × 26
    year firm_ID     ME annual_R annual_R_F annual_Re industry_ID sales    OX
   <dbl>   <dbl>  <dbl>    <dbl>      <dbl>     <dbl>       <dbl> <dbl> <dbl>
 1  2015       1  3577.   NA      0.00743      NA              NA   NA   NA  
 2  2016       1  6883.    0.997  0.000565      0.997           1 5949. 564. 
 3  2017       1 11377.    0.688  0.0000488     0.688           1 6505. 691. 
 4  2018       1  8695.   -0.214  0.00579      -0.219           1 6846. 751. 
 5  2019       1 13958.    0.647 -0.000770      0.648           1 7572. 959. 
 6  2020       1  9709.   -0.284  0.000380     -0.285           1 7538. 778. 
 7  2015       2  4087.   NA      0.00579      NA               1 3506.  45.8
 8  2016       2  5593.    0.375 -0.000770      0.376           1 3491.  51.2
 9  2017       2  9153.    0.641  0.000380      0.640           1 3946.  83.4
10  2018       2  7104.   -0.220  0.00371      -0.223           1 4139.  93.4
# ℹ 7,910 more rows
# ℹ 17 more variables: NFE <dbl>, X <dbl>, OA <dbl>, FA <dbl>, OL <dbl>,
#   FO <dbl>, BE <dbl>, lagged_BE <dbl>, ROE <dbl>, lag_ME <dbl>,
#   lagged_BEME <dbl>, lagged_ME <dbl>, w_M <dbl>, ME_rank10 <fct>,
#   BEME_rank10 <fct>, ME_rank2 <fct>, BEME_percent_rank <dbl>
簿価時価比率に基づくランク付け (2)
annual_data <- annual_data |>
  mutate(
    # 年度ごとに簿価時価比率のパーセンタイル順位を計算
    BEME_percent_rank = percent_rank(lagged_BEME),
    .by = year
  ) |>
  mutate(
    # BEME_percent_rankの値に応じて1から3までBEME_rank3の値を定義
    BEME_rank3 = cut(BEME_percent_rank,
                      breaks = c(0, 0.3, 0.7, 1),
                      labels = c(1, 2, 3),
                      include.lowest = TRUE)
  )

6.4.2.2 時価総額とBE/MEに基づくポートフォリオ・ソート

Size-BE/MEポートフォリオへの分類 (1)
annual_data <- annual_data |>
  mutate(
    # ME_rank2とBEME_rank3の組合せで, ファクター型の変数FF_portfolio_typeを定義
    FF_portfolio_type = interaction(ME_rank2, BEME_rank3)
    )
Size-BE/MEポートフォリオへの分類 (2)
annual_data <- annual_data |>
  mutate(
    # ファクター型の変数
    FF_portfolio_type = fct_recode(FF_portfolio_type,
                                        SL = "1.1",
                                        BL = "2.1",
                                        SN = "1.2",
                                        BN = "2.2",
                                        SH = "1.3",
                                        BH = "2.3")
  )
Size-BE/MEポートフォリオへの分類 (3)
annual_data |>
  summarize(FF_portfolio_type = FF_portfolio_type[1],
            mean_BEME = mean(lagged_BEME),
            mean_ME = mean(lagged_ME),
            mean_N_stocks = n() / length(unique(year)),
            .by = c(ME_rank2, BEME_rank3)
   ) |>
  drop_na() # 欠損データを削除
# A tibble: 6 × 6
  ME_rank2 BEME_rank3 FF_portfolio_type mean_BEME mean_ME mean_N_stocks
  <fct>    <fct>      <fct>                 <dbl>   <dbl>         <dbl>
1 1        3          SH                    1.97   11023.          260.
2 1        2          SN                    0.973  11868.          226 
3 1        1          SL                    0.416  11601.          155.
4 2        2          BN                    0.960 211793.          286 
5 2        3          BH                    1.72  151135.          125.
6 2        1          BL                    0.468 414941.          230.
Size-BE/MEポートフォリオの構築 (1)
annual_data <- annual_data |>
  # yearとFF_portfolio_typeのペアでグループ化
  mutate(
    # 各ポートフォリオ内で時価総額加重の保有比率を計算
    w = lagged_ME  / sum(lagged_ME, na.rm = TRUE),
    .by = c(year, FF_portfolio_type)
  )
Size-BE/MEポートフォリオの構築 (2)
FF_portfolio <- annual_data |>
  select(
    year, firm_ID, FF_portfolio_type, ME_rank2, BEME_rank3, w
  ) |>
  full_join(
    monthly_data,
    by = c("year", "firm_ID")
  ) |> # 今までに準備したデータと月次データを結合
  summarize(
    ME_rank2 = ME_rank2[1],
    BEME_rank3 = BEME_rank3[1],
    R = sum(w * R, na.rm = TRUE), # 各ポートフォリオの月次リターンを計算
    R_F = R_F[1],
    .by = c(month_ID, FF_portfolio_type)
  ) |>
  drop_na() # 欠損データを削除
Size-BE/MEポートフォリオのリターンの可視化 (1)
FF_portfolio_mean_return <- FF_portfolio |>
  mutate(Re = R - R_F) |>
  summarize(
    ME_rank2 = ME_rank2[1],
    BEME_rank3 = BEME_rank3[1],
    mean_Re = mean(Re),
    .by = FF_portfolio_type
    ) # 各ポートフォリオの超過リターンの平均値を計算

ggplot(FF_portfolio_mean_return) +
  geom_col(aes(x = BEME_rank3, y = mean_Re, fill = ME_rank2), position = "dodge") + # x軸をBEME_rank3, y軸をmean_Reに, ME_rank2のサブグループで色分け
  scale_fill_grey() + # 棒グラフの色をモノトーンに
  labs(x = "BE/ME Rank", y = "Mean Monthly Excess Return", fill = "ME Rank") +
  scale_y_continuous(expand = c(0, 0)) + mystyle

Size-BE/MEポートフォリオのリターンの可視化 (2)
ggplot(FF_portfolio_mean_return) +
  geom_col(aes(x = BEME_rank3, y = mean_Re, fill = ME_rank2), position = "dodge") +
  scale_fill_grey() +
  geom_text(aes(x = BEME_rank3, y = mean_Re, group = ME_rank2, label = FF_portfolio_type), # (x, y)座標を指定して各ポートフォリオの名前をグラフに挿入
            vjust = -0.5, # 棒グラフが重ならないよう文字ラベルを上にずらす
            position = position_dodge(width = 0.9)) + # ME_rank2のサブグループで文字ラベルが左右にずれるよう調整
  labs(x = "BE/ME Rank", y = "Mean Monthly Excess Return", fill = "ME Rank") +
  scale_y_continuous(expand = c(0, 0), limits = c(0, 0.015)) + # 文字ラベルがはみ出ないようy軸の範囲を指定
  theme_classic()

Size-BE/MEポートフォリオのリターンの可視化 (3)
initial_point <- tibble(
  month_ID = c(12, 12), # 累積リターンの起点を定義
  cumulative_gross_R = c(1, 1),
  FF_portfolio_type = c("BL", "SH")
  )

FF_portfolio_cumulative_return <- FF_portfolio |>
  mutate(# グロス・リターンを累積
    cumulative_gross_R = cumprod(1 + R),
    .by = FF_portfolio_type
  ) |>
  filter(FF_portfolio_type %in% c("BL", "SH")) |>
  select(month_ID, cumulative_gross_R, FF_portfolio_type) |>
  bind_rows(initial_point)

ggplot(FF_portfolio_cumulative_return) +
  geom_line(aes(x = month_ID, y = cumulative_gross_R, linetype = FF_portfolio_type)) +
  scale_linetype_manual(values = c("longdash", "solid")) +
  geom_hline(yintercept = 1, linetype = "dotted") +
  labs(x = "Month ID", y = "Cumulative Gross Return", linetype = "") +
  scale_x_continuous(expand = c(0, 0)) + mystyle

6.4.2.3 ファクター・リターンの計算

SMBとHMLの構築 (1)
# FF_portfolio_typeの値に基づく列を作成し, 縦長から横長のデータに変換
FF_portfolio <- FF_portfolio |>
  pivot_wider(
    id_cols = month_ID,
    names_from = FF_portfolio_type,
    values_from = R
    )
SMBとHMLの構築 (2)
factor_data <- FF_portfolio |>
  mutate(
    SMB = (SH + SN + SL) / 3 - (BH + BN + BL) / 3, # SMBとHMLを計算
    HML = (SH + BH) / 2 - (SL + BL) / 2
    ) |>
  select(month_ID, SMB, HML) |>
  full_join(factor_data, by = "month_ID") |> # 3ファクターの実現値をfactor_dataに集約
  select(-c("SMB", "HML"), c("SMB", "HML")) # SMBとHMLを最後列に移動

6.4.2.4 FF3アルファ

FF3モデルの推定
ME_sorted_portfolio <- ME_sorted_portfolio |>
  select(-c(R_Me, R_M)) |>
  # 3ファクターの実現値をME_sorted_portfolioに追加
  full_join(factor_data, by = "month_ID")

FF3_results <- list(NA)  # 推定結果を保存するために空のリストを準備

for(i in 1:10) {
  FF3_results[[i]] <- ME_sorted_portfolio |>
    filter(ME_rank10 == i) |>
    lm(Re ~ R_Me + SMB + HML, data = _) |> # 3ファクターの実現値を独立変数として重回帰
    tidy() |>
    mutate(ME_rank10 = i) |> # 推定対象のポートフォリオ名を保存
    select(ME_rank10, everything()) # ME_rank10を第一列に移動
}

FF3_results <- do.call(rbind, FF3_results) # do.call()関数を用いて複数のデータフレームから構成されるリストを一つのデータフレームに統合
FF3アルファの可視化
FF3_results |>
  filter(term == "(Intercept)") |> # 定数項に関する推定結果のみを抽出
  mutate(
    ME_rank10 = as.factor(ME_rank10)
  ) |> # ME_rank10を整数型からファクター型に
  ggplot() +
  # 横軸をME_rank10, 縦軸をFF3_alphaとする棒グラフ
  geom_col(aes(x = ME_rank10, y = estimate)) +
  geom_hline(yintercept = 0) +
  labs(x = "ME Rank", y = "FF3 alpha") +
  scale_y_continuous(limits = c(-0.003, 0.013)) + mystyle

FF3アルファの統計的な有意性を評価
FF3_results |>
  filter(term == "(Intercept)") |> # 定数項に関する推定結果のみを抽出
  rename(
    FF3_alpha = estimate,
    p_value = p.value
  ) |> # 列名を変更
  mutate(
    significance = cut(p_value,
      breaks = c(0, 0.01, 0.05, 0.1, 1),
      labels = c("***", "**", "*", ""),
      include.lowest = TRUE)
  ) |> # 統計的に有意な結果を*で強調
  select(ME_rank10, FF3_alpha, p_value, significance)
# A tibble: 10 × 4
   ME_rank10 FF3_alpha p_value significance
       <int>     <dbl>   <dbl> <fct>       
 1         1  0.00244   0.181  ""          
 2         2  0.00134   0.412  ""          
 3         3  0.00246   0.0893 "*"         
 4         4 -0.000843  0.335  ""          
 5         5 -0.000924  0.336  ""          
 6         6  0.000957  0.456  ""          
 7         7 -0.00183   0.119  ""          
 8         8  0.000842  0.555  ""          
 9         9 -0.00100   0.522  ""          
10        10 -0.00228   0.0492 "**"        
データの保存
write_csv(factor_data, "data/ch06_output.csv")
Back to top