🐨

書籍『わが投資術』のネットキャッシュ比率の汎用性の検証

に公開
2025/12/17

本記事は、マケデコ Advent Calendar 2025 の記事として執筆しています。

https://qiita.com/advent-calendar/2025/market-api

ちょうど1年ほど前に、清原達郎さんの書籍『わが投資術 市場は誰に微笑むか』を参考に、ネットキャッシュ比率の有用性の分析をJQuants APIを使ってやりました。

https://zenn.dev/morim34/articles/ae053ce5b94419

今見返すと大雑把な分析だなと思うのですが、この分析の後、実際にネットキャッシュ比率+αで小型株を4銘柄選定し、昨年末余っていたNISA枠で購入してみました。
すると、約1年ですべての銘柄が値上がりし、日経平均やTOPIXもアウトパフォームする結果となりました。

(たまたまかもしれませんが)このような結果が得られたことに加え、今年もNISA枠が余っていることもあり、また今年も小型株投資を検討しています。
そこで本記事では、昨年の記事で行ったネットキャッシュ比率の分析を複数の期間で行い、その汎用性を検証しました。

この記事では、その検証結果について共有していきます。


TL;DR

  • ネットキャッシュ比率が過去複数の期間においても有用である傾向がうかがえた。
  • ただ、書籍にあるような「リターン100%以上」の銘柄を抽出するうえでは、必ずしも汎用的に使えるとは言えない結果となった。

書籍『わが投資術 市場は誰に微笑むか』について

個人資産800億円超、長者番付1位となった伝説のサラリーマン投資家・清原達郎さんの著書です。
著者の投資戦略のほか、投資に関する生々しい体験談なども語られており、とても興味深い内容でした。

https://amzn.asia/d/fw8BZE6

ネットキャッシュ比率とは

著者の主な戦略は、割安小型株への投資です。
その銘柄が割安であるかを判断する指標として、ネットキャッシュ比率が紹介されています。

ネットキャッシュ = 流動資産 + 投資有価証券 × 70% - 負債

ネットキャッシュ比率 = ネットキャッシュ / 時価総額

ネットキャッシュは、その会社の現金や換金可能な流動資産の合計と負債との差となります。
それに対し時価総額との比を取ったのがネットキャッシュ比率ですが、この値が大きければ大きいほど、割安のまま放置されている銘柄ということになります。

詳しいところは書籍をご確認ください。

検証内容と方法

簡単に言うと、
「年末にネットキャッシュ比率で銘柄を選んで、3年間持ったらどうなっていたか」
を、複数の年で繰り返し確認しています。

細かい話が多くなるので、結果だけ見たい方は以降は読み飛ばしてもらって大丈夫です。

ネットキャッシュ比率の計算

  • 財務データは、 その年に公表された通期の決算短信(例えば2021年であれば2020年度分) を使用。流動資産・投資有価証券・負債の算出方法は、本記事の末尾のコード参照のこと。
  • 時価総額は、決算短信に記載されている発行済株式数と、その年の年末終値から算出。
  • データはすべてJQuants APIから取得している。

リターンの計算

  • 年末終値で購入し、3年後の年末終値で売却したと仮定して計算(2025年についてのみ、12月12日の終値を使用)。(※ 厳密には、年末終値を使って指標を計算して、その同じ価格で買うことはできないが、この分析では影響は小さいと考えて無視している。)

対象銘柄について

  • 小型株のみを対象とするため、時価総額が 20億円〜500億円 の銘柄でフィルタリングしている。(※書籍にのっとるなら上場市場区分で絞るべきだが、2022年の市場区分見直し前後で条件を揃えたかったためやっていない。)

検証期間と見方

  • 検証期間は 2015年〜2025年までの8パターン
    • 2015年末 → 2018年末
    • 2016年末 → 2019年末
    • …
    • 2022年末 → 2025年末
  • 各期間ごとに、
    • ネットキャッシュ比率の昇順で銘柄を並べる
    • 10等分(10グループ)に分ける
    • それぞれのグループで、3年間のリターンの傾向を比較

注意点

  • ネットキャッシュ比率に用いている財務データと株価データの間には、どうしてもタイミングのラグが生じている。
  • 検証期間中に上場廃止になった銘柄は分析に含めていない。

結果

ネットキャッシュ比率によるリターンの傾向

まずは、ネットキャッシュ比率の昇順で10分割した各ビンのリターンの傾向を比較します。
下図は、8パターンの時間軸での検証結果で、例えば左上のグラフは、2015年年末から2018年年末まで株式を保有した場合のリターンの傾向を示しています。
横軸は各ビンになっており、右に行くほどネットキャッシュ比率が大きい銘柄群となります。
各期間で全1800銘柄ほどあるため、各ビンは180前後あることになります。縦軸は対数をとったリターンです。

昨年書いた記事では箱ひげ図で描画しましたが、外れ値の影響で分かりにくかったので、表現を変えています。
青色の折れ線が各ビンの対数リターンの中央値、水色で塗られた部分が四分位範囲、オレンジが中央値を線形近似したものです。

説明が長くなりましたが、要は青色の折れ線や水色の帯が右肩上がりになっているほど、ネットキャッシュ比率とリターンの関連が大きいことを示しています。


図1:ネットキャッシュ比率で10分割した各ビンの3年リターンの中央値と四分位範囲

このグラフを見ると、いずれの期間でもネットキャッシュ比率に応じてリターンが大きくなる傾向を示しており、特に2020年以降はその傾向が顕著となっています。
また、昨年の記事でも示した、ネットキャッシュ比率が一番小さい群が比較的リターンが大きくなるという謎の傾向は、2020年以降から確認されるのがわかります。

リターン100%以上の銘柄数の比較

書籍では、「価格が3割上がって売るのはもったいない」、「最低2倍は狙うべき」という風に言及されています。
そこで、先ほどのネットキャッシュ比率で分けた各群の銘柄のうち、何%が3年間で100%以上のリターンになったかを棒グラフで示したものが下図となります。

見方としては、横軸が(2015年~2018年などの)各期間、縦軸が100%を超えた銘柄の割合となっており、棒の集まりは左から右にネットキャッシュ比率が大きい群となっています。


図2:各ビンにおけるリターンが100%を超えた割合

これを見ると、2020年以降の期間においては、ネットキャッシュ比率が大きいほどリターン100%以上の銘柄数が多くなるように見えますが、それ以前の期間では逆にネットキャッシュ比率が中程度の場合のほうがリターンが100%を超えることが多い結果となりました。

このグラフを見る限り、リターン100%を超える銘柄を選定するうえで、ネットキャッシュ比率はここ数年であれば有用であるが、必ずしも汎用的ではないということが伺えます。

考察(ネットキャッシュ比率が低いのにリターンが大きいケース)

※考察はChatGPTに書いてもらいました。

結果でも言及した、ネットキャッシュ比率が極端に低い銘柄群で、逆にリターンが大きく見えるケースがあった点についてです。

直感的には、ネットキャッシュ比率が低い=財務的に厳しい会社が多く、リターンも低くなりそうに思えます。
それにもかかわらず、このような結果になった背景として、 上場廃止銘柄を分析に含めていない影響(生存者バイアス) が考えられます。

生存者バイアスの可能性

ネットキャッシュ比率が極端に低い会社は、

  • 倒産
  • 民事再生
  • MBOなどによる上場廃止

といった形で、市場から退場する確率が相対的に高いと考えられます。

しかし本分析では、
検証期間中に上場廃止となった銘柄は結果に含めていません。

そのため、ネットキャッシュ比率が極端に低い銘柄群の中でも、

  • 大きく失敗して市場から消えた銘柄 → 分析に含まれない
  • 何らかの理由で持ち直し、大きく株価が上昇した銘柄 → 分析に残る

という構造になっている可能性があります。

結果として、本来であれば
「大きく負ける銘柄」と「大きく勝つ銘柄」が混在しているはずのゾーンで、
勝ち残った銘柄だけが観測され、平均的なリターンが過度に高く見えている
という解釈です。

特に2020年以降で顕著になっている理由

※ここは眉唾な気がします。

2020年は、新型コロナウイルスの拡大をきっかけに、各国で大規模な金融緩和が行われた年でもあります。
この結果、従来であれば財務的に厳しく市場から退場していた可能性のある企業も、資金調達環境の改善により生き残るケースが増えました。

その影響として、ネットキャッシュ比率が極端に低い銘柄群においても、
「生き残った企業の株価が大きく上昇する」という事例が観測されやすくなり、
本分析の結果にも影響を与えている可能性があります。

実運用への示唆

以上を踏まえると、

  • ネットキャッシュ比率が極端に低い銘柄群は
    リターンが高く見えても、リスクも非常に高いゾーン
  • 実際の銘柄選定では、
    ネットキャッシュ比率が一定以上ある銘柄に絞った方が再現性は高そう

と考えられます。

まとめ

今回得られた結論は、以下の通りです。

  • ネットキャッシュ比率が過去の複数の期間においても汎用的に機能していることが示唆される結果となった。
  • 一方で、リターン100%以上の銘柄を選定するうえでは、必ずしも汎用的であるとは言えない結果となった。

ネットキャッシュ比率はいい感じの結果が得られましたが、やはりこれだけでは不十分かなという印象です。
今後はほかの指標と組み合わせつつ、より的確に値上がりする銘柄を抽出する方法を見つけられればと考えています。
また気が向いたら記事を書こうと思います。

ネットキャッシュ比率はどこで見られる?

自分が認識する限り、ネットキャッシュ比率を確認するには次の2つの方法があります。

JQuantsAPIを使って計算する

自分がこの記事でやったように、JQuants APIからデータを引っ張ってきて分析するという方法です。
ただ、ネットキャッシュ比率の計算に用いる流動資産や有価証券、負債のデータを取ってくるには、プレミアムプラン(16,500円/月)への登録が必要で、そこそこ負担は大きいかもしれません。
かつ、ある程度プログラミングによるデータ分析の経験が無いと難しいかと思います。

四季報オンライン

以下の記事にあるように、四季報オンラインの「My項目」という機能を使えばネットキャッシュ比率を確認することができます。

https://shikiho.toyokeizai.net/news/0/738599

My項目の機能を使うにはベーシックプラン(月額1,100円)以上に入る必要がありますが、お手軽に確認したいという方はこちらがおすすめです。

流動資産、有価証券、負債の算出のコード

def process_current_assets(df: pd.DataFrame) -> pd.DataFrame:
    """
    流動資産を抽出して処理(IFRS / 非IFRS 両対応)

    ルール:
    - IFRS      → Current assets (IFRS) を優先
    - 非IFRS   → Current assets を使用
    - それぞれの候補内で nanmax(≒ coalesce)
    - 混在して nanmax はしない
    """

    print("\n流動資産を処理中...")

    base_cols = ["DisclosedDate", "DisclosureNumber", "LocalCode", "TypeOfDocument"]
    std_col = "FinancialStatement.Accounting standards, DEI"

    if std_col not in df.columns:
        raise ValueError(f"列が見つかりません: {std_col}")

    # 候補列
    col_ifrs = "FinancialStatement.Current assets (IFRS)"
    col_non_ifrs = "FinancialStatement.Current assets"

    cols_exist = [c for c in [col_ifrs, col_non_ifrs] if c in df.columns]

    df_result = df.loc[:, base_cols + [std_col] + cols_exist].copy()

    # 数値化
    for c in cols_exist:
        df_result[c] = pd.to_numeric(df_result[c], errors="coerce")

    is_ifrs = df_result[std_col].eq("IFRS")

    current_asset = pd.Series(np.nan, index=df_result.index, dtype="float64")

    # IFRS企業:IFRS列のみを見る
    if col_ifrs in df_result.columns:
        current_asset.loc[is_ifrs] = df_result.loc[is_ifrs, col_ifrs]

    # 非IFRS企業:非IFRS列のみを見る
    if col_non_ifrs in df_result.columns:
        current_asset.loc[~is_ifrs] = df_result.loc[~is_ifrs, col_non_ifrs]

    df_out = df_result[base_cols].copy()
    df_out["CurrentAsset"] = current_asset

    print(f"処理済みデータ件数: {len(df_out)} 件")
    print(f"CurrentAssetが有効なデータ数: {df_out['CurrentAsset'].notna().sum()} 件")

    return df_out

def process_investment_securities(df: pd.DataFrame, mode: str = "narrow") -> pd.DataFrame:
    """
    投資有価証券/有価証券相当を抽出して処理(IFRS / 非IFRS 両対応)

    mode:
      - "narrow": (IFRS)側は有価証券・投資有価証券・市場性有価証券中心
      - "wide"  : narrow に加えて Other financial assets / Time deposits も含める(網羅性UP)

    非IFRS(日本基準など)側は、(IFRS)無しの
      - Securities
      - Investment securities
      - Short-term investment securities(あれば)
    を合算します。

    Returns:
      base_cols + ["InvestmentSecurities"]
    """
    print("\n投資有価証券相当を処理中...")

    base_cols = ["DisclosedDate", "DisclosureNumber", "LocalCode", "TypeOfDocument"]
    std_col = "FinancialStatement.Accounting standards, DEI"
    if std_col not in df.columns:
        raise ValueError(f"列が見つかりません: {std_col}")

    # --- IFRS側候補 ---
    narrow_ifrs = [
        "FinancialStatement.Investment securities - NCA (IFRS)",
        "FinancialStatement.Marketable securities - CA (IFRS)",
        "FinancialStatement.Securities - CA (IFRS)",
        "FinancialStatement.Securities - Assets (IFRS)",
        "FinancialStatement.Marketable securities - Assets (IFRS)",
    ]

    wide_extra_ifrs = [
        "FinancialStatement.Other financial assets - CA (IFRS)",
        "FinancialStatement.Other financial assets - NCA (IFRS)",
        "FinancialStatement.Other financial assets - Assets (IFRS)",
        "FinancialStatement.Time deposits - CA (IFRS)",
    ]

    if mode not in ("narrow", "wide"):
        raise ValueError("mode must be 'narrow' or 'wide'")

    cand_ifrs = narrow_ifrs if mode == "narrow" else (narrow_ifrs + wide_extra_ifrs)

    # --- 非IFRS側候補(日本基準など)---
    cand_non_ifrs = [
        "FinancialStatement.Securities",
        "FinancialStatement.Investment securities",
        "FinancialStatement.Short-term investment securities",
    ]

    use_ifrs = [c for c in cand_ifrs if c in df.columns]
    use_non = [c for c in cand_non_ifrs if c in df.columns]

    # 抽出
    cols_to_take = base_cols + [std_col] + sorted(set(use_ifrs + use_non))
    df_work = df.loc[:, cols_to_take].copy()

    # 数値化
    for c in sorted(set(use_ifrs + use_non)):
        df_work[c] = pd.to_numeric(df_work[c], errors="coerce")

    is_ifrs = df_work[std_col].eq("IFRS")

    inv = pd.Series(np.nan, index=df_work.index, dtype="float64")

    # IFRS行:候補列の合算(全欠損ならNaN)
    if len(use_ifrs) > 0:
        tmp_ifrs = df_work.loc[is_ifrs, use_ifrs]
        inv.loc[is_ifrs] = tmp_ifrs.sum(axis=1, min_count=1)

    # 非IFRS行:候補列の合算(全欠損ならNaN)
    if len(use_non) > 0:
        tmp_non = df_work.loc[~is_ifrs, use_non]
        inv.loc[~is_ifrs] = tmp_non.sum(axis=1, min_count=1)

    df_out = df_work[base_cols].copy()
    df_out["InvestmentSecurities"] = inv

    # --- デバッグ出力 ---
    nn = df_out["InvestmentSecurities"].notna().mean()
    nz = (df_out["InvestmentSecurities"].fillna(0) != 0).mean()
    print(f"mode={mode}")
    print(f"IFRS候補列: {use_ifrs}")
    print(f"非IFRS候補列: {use_non}")
    print(f"InvestmentSecurities non-null: {nn:.1%}, non-zero: {nz:.1%}")

    return df_out

def process_liabilities(df: pd.DataFrame) -> pd.DataFrame:
    """
    負債(合計)を抽出して処理(IFRS / 非IFRS 両対応)

    - IFRS      : FinancialStatement.Liabilities (IFRS)
    - 非IFRS    : FinancialStatement.Liabilities
    - どちらも無い場合は NaN(0埋めしない)
    """
    print("\n負債を処理中...")

    base_cols = ["DisclosedDate", "DisclosureNumber", "LocalCode", "TypeOfDocument"]
    std_col = "FinancialStatement.Accounting standards, DEI"
    if std_col not in df.columns:
        raise ValueError(f"列が見つかりません: {std_col}")

    col_ifrs = "FinancialStatement.Liabilities (IFRS)"
    col_non = "FinancialStatement.Liabilities"

    use_cols = [c for c in [col_ifrs, col_non] if c in df.columns]
    df_work = df.loc[:, base_cols + [std_col] + use_cols].copy()

    for c in use_cols:
        df_work[c] = pd.to_numeric(df_work[c], errors="coerce")

    is_ifrs = df_work[std_col].eq("IFRS")

    liabilities = pd.Series(np.nan, index=df_work.index, dtype="float64")

    if col_ifrs in df_work.columns:
        liabilities.loc[is_ifrs] = df_work.loc[is_ifrs, col_ifrs]
    if col_non in df_work.columns:
        liabilities.loc[~is_ifrs] = df_work.loc[~is_ifrs, col_non]

    df_out = df_work[base_cols].copy()
    df_out["Liabilities"] = liabilities

    print(f"処理済みデータ件数: {len(df_out)} 件")
    print(f"Liabilitiesが有効なデータ数: {df_out['Liabilities'].notna().sum()} 件")

    return df_out

Discussion