11  Phân loại theo giá trị và hai biến

Trong chương này, chúng ta mở rộng phân tích danh mục đơn biến của chương Phân loại danh mục đơn biến sang phân loại danh mục hai biến, trong đó cổ phiếu được gán vào các danh mục dựa trên hai đặc điểm. Phân loại hai biến thường được dùng trong tài liệu định giá tài sản học thuật và là nền tảng của các nhân tố trong mô hình ba nhân tố Fama-French. Tuy nhiên, một số học giả cũng dùng phân loại với ba biến nhóm. Về mặt khái niệm, phân loại danh mục dễ dàng áp dụng được trong các chiều cao hơn.

Chúng ta hình thành các danh mục theo quy mô doanh nghiệp và tỷ số giá trị sổ sách trên giá trị thị trường (B/M). Việc tính tỷ số B/M đòi hỏi dữ liệu kế toán, kéo theo các bước bổ sung trong quá trình hình thành danh mục. Cuối cùng, chúng ta trình bày cách hình thành danh mục theo hai biến phân loại bằng cái gọi là phân loại danh mục độc lập và phân loại danh mục phụ thuộc.

Hiện mã
import pandas as pd
import numpy as np
import datetime as dt
import sqlite3

11.1 Chuẩn bị dữ liệu

Trước hết, chúng ta tải dữ liệu cần thiết từ cơ sở dữ liệu SQLite đã được giới thiệu trong chương Truy cập và quản lý dữ liệu tài chính và Dữ liệu DataCore. Chúng ta thực hiện phân loại danh mục dựa trên mẫu của mình nhưng chỉ giữ các cột cần thiết trong bộ nhớ. Chúng ta dùng cùng các nguồn dữ liệu cho quy mô doanh nghiệp như trong chương Phân loại theo quy mô và P-Hacking.

Hiện mã
tidy_finance = sqlite3.connect(database="data/tidy_finance_python.sqlite")

prices_monthly = (pd.read_sql_query(
    sql=("SELECT symbol, date, ret_excess, mktcap, " 
         "mktcap_lag, exchange FROM prices_monthly"),
    con=tidy_finance,
    parse_dates={"date"})
  .dropna()
)

Ngoài ra, chúng ta sử dụng dữ liệu kế toán. Trong ứng dụng này, chúng ta chỉ cần dữ liệu giá trị sổ sách của vốn chủ sở hữu, được chọn từ cơ sở dữ liệu của chúng ta. Bên cạnh đó, chúng ta chuyển biến datadate sang giá trị theo tháng, vì ở đây chúng ta chỉ xét lợi nhuận theo tháng và không cần quan tâm đến ngày chính xác.

Hiện mã
book_equity = (pd.read_sql_query(
    sql="SELECT symbol, datadate, be FROM datacore",
    con=tidy_finance, 
    parse_dates={"datadate"})
  .dropna()
  .assign(
    date=lambda x: (
      pd.to_datetime(x["datadate"]).dt.to_period("M").dt.to_timestamp()
    )
  )
)

11.2 Tỷ số giá trị sổ sách trên giá trị thị trường

Một vấn đề cơ bản khi xử lý dữ liệu kế toán là thiên lệch nhìn trước; chúng ta không được đưa vào việc hình thành danh mục những dữ liệu mà tại thời điểm đó chưa được biết. Tất nhiên, các nhà nghiên cứu có nhiều thông tin hơn khi nhìn lại quá khứ so với những gì các tác nhân thực sự có vào thời điểm đó. Tuy nhiên, lợi nhuận vượt mức bất thường từ một chiến lược giao dịch không nên dựa trên lợi thế thông tin, vì phần chênh lệch đó không thể là kết quả của các giao dịch của những tác nhân có thông tin. Do đó, chúng ta phải làm trễ thông tin kế toán.

Như ở chương trước, chúng ta tiếp tục làm trễ quy mô doanh nghiệp một tháng. Sau đó, chúng ta tính tỷ số B/M, tức tỷ số liên hệ giá trị sổ sách của vốn chủ sở hữu với vốn chủ sở hữu theo giá thị trường của doanh nghiệp. Các doanh nghiệp có tỷ số B/M cao (thấp) được gọi là doanh nghiệp giá trị (tăng trưởng). Sau khi ghép thông tin kế toán và thông tin vốn chủ sở hữu thị trường của cùng một tháng, chúng ta làm trễ B/M sáu tháng. Đây là cách tiếp cận đủ thận trọng vì thông tin kế toán thường được công bố rất sớm trước khi sáu tháng trôi qua. Tuy nhiên, trong tài liệu định giá tài sản, người ta cũng dùng các độ trễ dài hơn nữa.

Khi đã có cả hai biến, tức quy mô doanh nghiệp trễ một tháng và B/M trễ sáu tháng, chúng ta ghép các biến phân loại này vào dữ liệu lợi nhuận bằng cột sorting_date được tạo cho mục đích này. Bước cuối cùng trong chuẩn bị dữ liệu xử lý sự khác biệt về tần suất của các biến. Lợi nhuận và quy mô doanh nghiệp được ghi nhận theo tháng. Tuy nhiên, thông tin kế toán chỉ được công bố theo năm. Do đó, chúng ta chỉ ghép B/M vào một tháng mỗi năm và còn mười một quan sát trống. Để giải quyết vấn đề tần suất này, chúng ta mang tỷ số B/M mới nhất của mỗi doanh nghiệp sang các tháng tiếp theo, tức là điền các quan sát thiếu bằng báo cáo gần đây nhất. Việc này được thực hiện qua hàm fillna(method="ffill") sau khi sắp xếp theo ngày và doanh nghiệp (mà chúng ta xác định bằng symbol và symbol) và trên cơ sở từng doanh nghiệp (mà chúng ta thực hiện bằng .groupby() như thường lệ). Chúng ta loại bỏ mọi quan sát có dữ liệu kế toán cũ hơn một năm. Ở bước cuối, chúng ta xóa mọi dòng có giá trị thiếu vì lợi nhuận không thể ghép với bất kỳ báo cáo thường niên nào.

Hiện mã
size = (prices_monthly
  .assign(sorting_date=lambda x: x["date"]+pd.DateOffset(months=1))
  .rename(columns={"mktcap": "size"})
  .get(["symbol", "sorting_date", "size"])
)

bm = (book_equity
  .merge(prices_monthly, how="inner", on=["symbol", "date"])
  .assign(bm=lambda x: x["be"]/x["mktcap"],
          sorting_date=lambda x: x["date"]+pd.DateOffset(months=6))
  .assign(accounting_date=lambda x: x["sorting_date"])
  .get(["symbol", "symbol", "sorting_date", "accounting_date", "bm"])
)

data_for_sorts = (prices_monthly
  .merge(bm, 
         how="left", 
         left_on=["symbol", "symbol", "date"], 
         right_on=["symbol", "symbol", "sorting_date"])
  .merge(size, 
         how="left", 
         left_on=["symbol", "date"], 
         right_on=["symbol", "sorting_date"])
  .get(["symbol", "symbol", "date", "ret_excess", 
        "mktcap_lag", "size", "bm", "exchange", "accounting_date"])
)

data_for_sorts = (data_for_sorts
  .sort_values(by=["symbol", "symbol", "date"])
  .groupby(["symbol", "symbol"])
  .apply(lambda x: x.assign(
      bm=x["bm"].fillna(method="ffill"), 
      accounting_date=x["accounting_date"].fillna(method="ffill")
    )
  )
  .reset_index(drop=True)
  .assign(threshold_date = lambda x: (x["date"]-pd.DateOffset(months=12)))
  .query("accounting_date > threshold_date")
  .drop(columns=["accounting_date", "threshold_date"])
  .dropna()
)

Bước chuẩn bị cuối cùng cho việc phân loại danh mục là tính các điểm cắt. Chúng ta tiếp tục dùng cùng một hàm, cho phép chỉ định các sàn dùng để tính điểm cắt. Ngoài ra, chúng ta đưa lại đối số sorting_variable vào hàm để xác định các biến phân loại khác nhau.

Hiện mã
def assign_portfolio(data, exchanges, sorting_variable, n_portfolios):
    """Assign portfolio for a given sorting variable."""
    
    breakpoints = (data
      .query(f"exchange in {exchanges}")
      .get(sorting_variable)
      .quantile(np.linspace(0, 1, num=n_portfolios+1), 
                interpolation="linear")
      .drop_duplicates()
    )
    breakpoints.iloc[0] = -np.inf
    breakpoints.iloc[breakpoints.size-1] = np.inf
    
    assigned_portfolios = pd.cut(
      data[sorting_variable],
      bins=breakpoints,
      labels=range(1, breakpoints.size),
      include_lowest=True,
      right=False
    )
    
    return assigned_portfolios

Sau các bước chuẩn bị dữ liệu này, chúng ta trình bày phân loại danh mục hai biến theo cách độc lập và phụ thuộc.

11.3 Phân loại độc lập

Phân loại hai biến tạo ra các danh mục trong không gian hai chiều được căng bởi hai biến phân loại. Khi đó, có thể đánh giá tác động lên lợi nhuận của từng biến phân loại qua chênh lệch lợi nhuận của một chiến lược giao dịch đầu tư vào các danh mục ở hai đầu của phổ giá trị của biến tương ứng. Trong ví dụ dưới đây, chúng ta tạo ma trận năm nhân năm dùng B/M và quy mô doanh nghiệp làm biến phân loại. Kết quả là 25 danh mục. Vì chúng ta quan tâm đến phần bù giá trị (tức là chênh lệch lợi nhuận giữa các doanh nghiệp có B/M cao và thấp), chúng ta mua năm danh mục gồm các doanh nghiệp có B/M cao nhất và bán khống năm danh mục gồm các doanh nghiệp có B/M thấp nhất. Có năm danh mục ở mỗi đầu là do các lần chia theo quy mô mà chúng ta áp dụng song song với các lần chia theo B/M.

Để triển khai phân loại hai biến độc lập, chúng ta gán các danh mục theo tháng cho từng biến phân loại một cách riêng biệt để tạo các biến portfolio_bm và portfolio_size. Sau đó, các danh mục riêng biệt này được kết hợp thành phân loại cuối cùng lưu trong portfolio_combined. Sau khi gán danh mục, chúng ta tính lợi nhuận trung bình trong mỗi danh mục cho từng tháng. Ngoài ra, chúng ta giữ lại danh mục B/M vì điều này giúp việc tính phần bù giá trị dễ hơn. Phương án thay thế là tách danh mục kết hợp ở một bước riêng. Lưu ý rằng chúng ta đặt trọng số cho các cổ phiếu trong mỗi danh mục theo vốn hóa thị trường của chúng, tức là chúng ta quyết định dùng trọng số theo vốn hóa cho lợi nhuận.

Hiện mã
value_portfolios = (data_for_sorts
  .groupby("date")
  .apply(lambda x: x.assign(
      portfolio_bm=assign_portfolio(
        data=x, sorting_variable="bm", n_portfolios=5, exchanges=["HOSE"]
      ),
      portfolio_size=assign_portfolio(
        data=x, sorting_variable="size", n_portfolios=5, exchanges=["HOSE"]
      )
    )
  )
  .reset_index(drop=True)
  .groupby(["date", "portfolio_bm", "portfolio_size"])
  .apply(lambda x: pd.Series({
      "ret": np.average(x["ret_excess"], weights=x["mktcap_lag"])
    })
  )
  .reset_index()
)

Khi đã có lợi nhuận danh mục theo tháng, chúng ta sẵn sàng tính phần bù giá trị. Tuy nhiên, chúng ta vẫn phải quyết định cách đầu tư vào năm danh mục B/M cao và năm danh mục B/M thấp. Cách phổ biến nhất là đặt trọng số bằng nhau cho các danh mục này, nhưng đây lại là một lựa chọn nữa của nhà nghiên cứu. Sau đó, chúng ta tính chênh lệch lợi nhuận giữa các danh mục B/M cao và thấp và trình bày phần bù giá trị trung bình.

Hiện mã
value_premium = (value_portfolios
  .groupby(["date", "portfolio_bm"])
  .aggregate({"ret": "mean"})
  .reset_index()
  .groupby("date")
  .apply(lambda x: pd.Series({
    "value_premium": (
        x.loc[x["portfolio_bm"] == x["portfolio_bm"].max(), "ret"].mean() - 
          x.loc[x["portfolio_bm"] == x["portfolio_bm"].min(), "ret"].mean()
      )
    })
  )
  .aggregate({"value_premium": "mean"})
)

11.4 Phân loại phụ thuộc

Trong bài tập trước, chúng ta gán các danh mục mà không xét đến biến thứ hai trong việc gán. Quy ước này được gọi là phân loại danh mục độc lập. Phương án thay thế, tức phân loại phụ thuộc, tạo các danh mục cho biến phân loại thứ hai trong từng nhóm của biến phân loại thứ nhất. Trong ví dụ dưới đây, chúng ta chia doanh nghiệp thành năm nhóm quy mô, và trong mỗi nhóm đó, chúng ta gán doanh nghiệp vào năm danh mục B/M. Do đó, chúng ta có các điểm cắt theo tháng riêng cho từng nhóm quy mô. Việc chọn giữa phân loại danh mục độc lập và phụ thuộc là một lựa chọn nữa của nhà nghiên cứu. Lưu ý rằng phân loại phụ thuộc bảo đảm các danh mục có số cổ phiếu xấp xỉ bằng nhau khi các điểm cắt được tính từ tất cả các sàn. Tuy nhiên, nếu các điểm cắt chỉ dựa trên cổ phiếu HOSE, số lượng cổ phiếu trong danh mục nhìn chung sẽ không đồng đều, phản ánh sự hiện diện lớn của cổ phiếu vốn hóa nhỏ trên HNX và UPCoM (xem Bài tập bên dưới).

Để triển khai phân loại phụ thuộc, trước hết chúng ta tạo các danh mục quy mô bằng cách gọi assign_portfolio() với sorting_variable="me". Sau đó, chúng ta nhóm dữ liệu một lần nữa theo tháng và theo danh mục quy mô trước khi gán danh mục B/M. Phần còn lại của việc triển khai giống như trước. Cuối cùng, chúng ta tính phần bù giá trị.

Hiện mã
value_portfolios = (data_for_sorts
  .groupby("date")
  .apply(lambda x: x.assign(
      portfolio_size=assign_portfolio(
        data=x, sorting_variable="size", n_portfolios=5, exchanges=["HOSE"]
      )
    )
  )
  .reset_index(drop=True)
  .groupby(["date", "portfolio_size"])
  .apply(lambda x: x.assign(
      portfolio_bm=assign_portfolio(
        data=x, sorting_variable="bm", n_portfolios=5, exchanges=["HOSE"]
      )
    )
  )
  .reset_index(drop=True)
  .groupby(["date", "portfolio_bm", "portfolio_size"])
  .apply(lambda x: pd.Series({
      "ret": np.average(x["ret_excess"], weights=x["mktcap_lag"])
    })
  )
  .reset_index()
)

value_premium = (value_portfolios
  .groupby(["date", "portfolio_bm"])
  .aggregate({"ret": "mean"})
  .reset_index()
  .groupby("date")
  .apply(lambda x: pd.Series({
    "value_premium": (
        x.loc[x["portfolio_bm"] == x["portfolio_bm"].max(), "ret"].mean() -
          x.loc[x["portfolio_bm"] == x["portfolio_bm"].min(), "ret"].mean()
      )
    })
  )
  .aggregate({"value_premium": "mean"})
)

Nhìn chung, chúng ta đã trình bày cách thực hiện phân loại danh mục hai biến trong chương này. Ở một trường hợp, chúng ta phân loại các danh mục độc lập với nhau. Tuy nhiên, chúng ta cũng thảo luận cách tạo phân loại danh mục phụ thuộc. Tương tự chương Phân loại theo quy mô, chúng ta thấy nhà nghiên cứu phải đưa ra bao nhiêu lựa chọn để triển khai phân loại danh mục, và phân loại hai biến làm tăng số lượng lựa chọn.

11.5 Những điểm chính

  • Phân loại danh mục hai biến gán cổ phiếu dựa trên hai đặc điểm, như quy mô doanh nghiệp và tỷ số B/M, để nắm bắt tốt hơn các quy luật lợi nhuận trong định giá tài sản.
  • Phân loại độc lập xử lý từng biến một cách riêng biệt, trong khi phân loại phụ thuộc thực hiện lần phân loại thứ hai có điều kiện theo kết quả của lần thứ nhất.
  • Xử lý đúng dữ liệu kế toán, đặc biệt là làm trễ tỷ số B/M, là thiết yếu để tránh thiên lệch nhìn trước và bảo đảm kiểm định ngược (backtesting) hợp lệ.
  • Phần bù giá trị được tính bằng cách so sánh lợi nhuận của các danh mục B/M cao với B/M thấp, với kết quả nhạy cảm với các lựa chọn phân loại và cách đặt trọng số.