0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZENITH Codeディープ解析③ 政策保有株から取引関係の地図を作る――保有先の数・売り切った保有先・持ち合いを競合で並べる

0
Last updated at Posted at 2026-11-04

取引先や競合が、どの会社の株を取引関係の維持のために持っているかを調べるとき、有価証券報告書の「株式の保有状況」を1社ずつ開いていないでしょうか。明細は数十行あり、前期の株数と見比べないと、どの保有先の株数が減ったのか、売り切ったのかは分かりません。

前回のディープ解析②では、大株主の上位10を名前でそろえ、比べる会社どうしの持ち合いを上位10の範囲で見ました。今回は同じ「開示リーダー」の「政策保有」の画面で、保有先の数・保有中の明細と前期からの増減・期中に売り切った保有先・持ち合いを、自社と競合の数社で並べて読みます。

開示リーダーの「政策保有」。5社の保有先の数の表、トヨタ自動車とマツダの持ち合いの行、デンソーの保有中の明細と期中に売り切った保有先

画面の内容は、2026年11月4日にZENITH Codeの実データを取得し直したデモです。大株主の数字は②と同じです。トヨタ自動車(7203)・デンソー(6902)・本田技研工業(7267)・マツダ(7261)・テイン(7217)の、2026年3月期の有価証券報告書を並べています。テインは政策保有の開示がなく、画面では「—」と出ます。

私たち株式会社Crabgrassは、企業の財務データなどをAPIで提供するZENITH Codeを開発しています。本記事は自社サービスのデータを使うためのテンプレートを題材にした実装例です。

この記事のコードは、公開サンプル集のdisclosure-readerテンプレート(執筆時点の版)に対応しています。画面の仕組みの基本(ローカルのPythonサーバーとブラウザの役割分担)は、実践編で詳しく扱いました。

1. この回で読む画面と操作

画面の部分 分かること
保有先の数(純投資目的以外) 上場・非上場の保有先の数と、明細の行数
比べる会社どうしの持ち合い 比べる会社の株を互いに持っているか
会社ごとの明細 保有中の行の当期・前期の株数と増減、相手の保有の有無、保有の目的と理由

2. 登録不要で試す

必要なものはPython 3.9以上とブラウザです。追加のパッケージはありません。

git clone https://github.com/Crab-grass/zenith-code-examples.git
cd zenith-code-examples
python3 templates/disclosure-reader/server.py

ブラウザで http://127.0.0.1:8790 を開き、左の「政策保有」(上から3番目)を押します。上の「比べる会社」で2〜5社を選べます。デモではAPIを呼ばないので、会員登録もAPIキーも要りません。終了するときは端末で Ctrl+C を押します。

3. 政策保有のデータは1つのAPIから受け取る

政策保有の画面は、1社につき /stable/cross-shareholdings を1回だけ呼びます。有報の「株式の保有状況」のうち、純投資目的以外の保有(政策保有株式)を返すAPIです。

ローカルのサーバーは、受け取った明細の行を server.py の clean_cross で整えてからブラウザに渡します。

    for h in _list(row.get("holdings") or [], 600):
        item = _pick(h, CROSS_HOLDING)
        item["rationale"] = _scalar(h.get("rationale") if isinstance(h, dict) else None, 1200)
        holdings.append(item)  # carryingAmount and priorCarryingAmount are never copied.

渡す項目は許可リスト(CROSS_HOLDING など)で決まっています。APIは保有先ごとの貸借対照表計上額(carryingAmount・priorCarryingAmount)も返しますが、テンプレートはブラウザに渡しません。上場株式は時価で計上されるので、計上額は株価から決まる値だからです(server.py の冒頭のコメントとREADMEに書いてあります)。この記事でも、出すのは保有先の名前・株数とその増減・保有の目的と理由・相互保有の有無・保有先の数だけです。保有の目的と理由(rationale)は、長い文を1,200字で切ってから渡します。

4. 保有先の数を並べる

画面の最初の表は、純投資目的以外の保有先の数です。crossSummary が、APIの集計(summary)から上場(listed)と非上場(unlisted)の行を取り出します。

会社 上場 非上場 明細(保有中) 期中に売却し保有なし みなし保有株式
トヨタ自動車 34 80 34 3 5
デンソー 8 79 8 4 0
本田技研工業 21 58 21 12 0
マツダ 3 24 3 1 0
テイン — — — — —

前期の数の列はありません。画面の注記のとおり、前期の数は、有報では純投資目的の区分にだけ記載があります。前期と比べるときは、明細の行で見ます(5節)。

右端の「純投資目的(上場)」の列は、デモではすべて「—」です。APIの集計に純投資目的の行がなく、たとえばトヨタ自動車の有報も、純投資目的である投資株式は「該当事項はありません」としています。保有先の数は事業の形や取引先の構成でも変わるので、この表だけでは多い少ないを比べられません。

5. 明細と前期からの増減

明細(有報の「特定投資株式」)は、保有先ごとに当期と前期の株数を並べた表です。crossSummary は、提出会社が持つ行を3つに分けます。

  const split = (holder) => {
    const rows = (report.holdings ?? []).filter((h) => h.holder === holder).map((h) => ({ ...h, change: shareChange(h) }));
    const specified = rows.filter((h) => h.type !== "deemed");
    return {
      held: specified.filter((h) => finite(h.shares) !== null),
      soldOut: specified.filter((h) => finite(h.shares) === null && finite(h.priorShares) !== null),
      deemed: rows.filter((h) => h.type === "deemed"),
    };
  };
  • held:当期の株数がある行(保有中)。
  • soldOut:当期の株数がなく、前期だけある行(期中に売り切った保有先)。
  • deemed:みなし保有株式(退職給付信託に出した株式など)。

増減(change)は、当期と前期の株数がどちらもあるときだけ「当期−前期」で計算します。保有中の行を増減で数えると、次のとおりです。

会社 保有中 増えた 変わらない 減った 前期が空
トヨタ自動車 34 4 26 1 3
デンソー 8 1 5 2 0
本田技研工業 21 0 14 7 0
マツダ 3 0 2 1 0

トヨタ自動車の「前期が空」の3行(㈱ブリヂストン・Gold Hydrogen Limited・岡谷鋼機㈱)は、増減が「—」です。原文でも前期の株数の欄は「―」で、株式数が増加した理由の欄に、ブリヂストンと岡谷鋼機は「退職給付信託からの返還のため」、Gold Hydrogenは取得と書かれています。

株数の増減には、株式分割の分も入ります。トヨタ自動車のKDDI㈱は363,366千株(前期203,295千株、+160,071千株)で、目的と理由の欄には「株式分割、一部売却のため」と書かれています。株数が増えた行が、取得した行とは限りません。

デンソーの㈱モルフォは、原文では名前の後に「(注2)」の注記が付いています。APIは注記を名前から外して返すので、画面には「㈱モルフォ」と出ます。注記の中身は原文で読みます。

明細の行数が保有先の数より少ないとき

有報の明細は、計上額が資本金の1%を超える保有先を載せ、それが60に満たないときは大きい順に60まで載せる決まりです。保有先が多い会社では、明細の行数が上場の保有先の数より少なくなりますが、誤りではありません。テンプレートは detailPartial で判定して行数に「*」を付けます。デモの4社は、どれも行数が一致します。

6. 期中に売り切った保有先

当期の株数がなく前期だけある行は、画面の「期中に売却し保有なし」に別に並びます。デモの4社で計20行です。

会社 行数 保有先(前期の株数、千株)
トヨタ自動車 3 パナソニックホールディングス㈱ 8,228、カヤバ㈱ 2,939、第一交通産業㈱ 1,078
デンソー 4 ㈱豊田自動織機 14,824、愛三工業㈱ 5,500、Blaize Holdings, Inc. 4,674、澤藤電機㈱ 400
本田技研工業 12 日本特殊陶業㈱ 1,542、大同特殊鋼㈱ 4,569、NOK㈱ 1,717、住友ゴム工業㈱ 1,401ほか8社(13節の画像)
マツダ 1 ダイナミックマッププラットフォーム㈱ 10

売り切った行の「相手の保有」の欄は、本田技研工業では12行のうち7行が「有」、デンソーでは㈱豊田自動織機の1行が「有」です。

トヨタ自動車には、みなし保有株式の表に5行が並び、どれも当期の株数がなく前期だけです(原文の当期の欄も「―」)。このうち㈱ブリヂストン・㈱御園座・岡谷鋼機㈱は、保有中の明細の目的と理由の欄に「退職給付信託からの返還のため」とあります。みなし保有株式の表で消えた保有先が、特定投資株式の表に移って出る形です。2つの表は分けて開示されるので、保有先の動きを追うときは両方を見ます。

7. 複数の会社が持つ保有先を並べる

同じ保有先が何社の明細に出るかを、テンプレートの関数で数えます(画面にはない表です)。リポジトリの直下で実行します。

node --input-type=module <<'EOF'
import fs from "node:fs";
import * as M from "./templates/disclosure-reader/model.mjs";
const raw = JSON.parse(fs.readFileSync("templates/disclosure-reader/demo.json", "utf8"));
const { companies } = M.normalizeBundle(raw, raw.companies.map((c) => c.symbol));
const seen = new Map();
for (const c of companies) {
  const s = M.crossSummary(c.crossShareholdings);
  if (!s) continue;
  for (const r of [...s.held, ...s.soldOut]) {
    const key = M.companyKey(r.name);
    if (!seen.has(key)) seen.set(key, []);
    seen.get(key).push(c.profile.nameJa + (r.shares === null ? "(売り切り)" : ""));
  }
}
for (const [key, names] of seen) if (names.length > 1) console.log(key, names.join("・"));
EOF
ルネサスエレクトロニクス トヨタ自動車・デンソー・本田技研工業
住友電気工業 トヨタ自動車・本田技研工業(売り切り)
ジーエスユアサコーポレーション トヨタ自動車・本田技研工業
大同特殊鋼 トヨタ自動車・本田技研工業(売り切り)
ELECTREONWIRELESSLTD トヨタ自動車・デンソー
御園座 トヨタ自動車・デンソー
ダイナミックマッププラットフォーム トヨタ自動車・本田技研工業・マツダ(売り切り)
澤藤電機 デンソー(売り切り)・本田技研工業(売り切り)

「㈱」と「株式会社」、空白の違いは companyKey でそろえます(8節の「マツダ㈱」と社名の「マツダ」も同じです)。保有先の名前の空白はAPIの側でも整えてあり、上の8組はどの会社でも同じ表記です。

8. 比べる会社どうしの持ち合い

比べる会社が、別の比べる会社の株を明細に載せているかは crossLinks で調べます。

      const key = companyKey(b.profile.nameJa);
      const row = (a.crossShareholdings.holdings ?? []).find((h) => h.holder === "reporting" && companyKey(h.name) === key);

比べる会社の社名を companyKey でそろえ、明細の行から同じ名前を探して「AがBを持つ」として集めます。

  const mutual = [];
  for (const l of links) {
    const back = links.find((m) => m.from === l.to && m.to === l.from);
    if (l.current && back?.current && l.from < l.to) mutual.push([l, back]);
  }

両方向に当期の株数がある組を mutual(持ち合い)にします。デモでは、トヨタ自動車がマツダ㈱を31,929千株、マツダがトヨタ自動車㈱を41,467千株持つ1組で、画面には「トヨタ自動車 ⇄ マツダ」と出ます。どちらも前期から株数は変わらず、相手の保有は「有」です。目的と理由の欄は、マツダ側が「自動車関連業務提携の維持強化」、トヨタ自動車側が「業務提携(米国での合弁会社(完成車生産)の設立・運営、共同開発、技術連携、商品補完)の維持・発展」です。

「相手の保有」の列は、有報の「当社の株式の保有の有無」の欄です。保有中の行では、トヨタ自動車は34行のうち20行、本田技研工業は21行のうち14行、マツダは3行のうち2行が「有」、デンソーは8行すべてが「無」です。比べる会社の外の保有先でも、相互の保有はこの欄で読めます。

9. ②の大株主の表とつなぐ

②の「比べる会社が大株主に入っている」一覧と、今回の明細を並べます。

株主 発行会社 大株主の表(②) 株主側の政策保有の明細
デンソー トヨタ自動車 7位 3.45% 449,576千株 トヨタ自動車の行はない
トヨタ自動車 デンソー 1位 22.24% 598,927千株 デンソーの行はない
トヨタ自動車 マツダ 3位 5.06% 31,928千株 マツダ㈱ 31,929千株

大株主の上位10に出ても、その株主側の政策保有の明細に出ないことがあります。株式の保有状況で開示する区分の外で持っている場合があるので、原文で確かめます。 明細に出ないことだけで、持っていないとは読めません。

10. 保有の目的と理由を読む

明細の右端の列は、保有の目的と理由です。画面では2行まで出し、押すと全文を開きます。

書き方は会社ごとに違います。本田技研工業は「原材料等の調達取引の安定化」のような短い句、トヨタ自動車は「自動車関連(調達)取引の維持・発展」のように括弧で取引の種類を示す形です。デンソーは「〜を目的としています。」で終わる文です。目的の分類(調達・販売・技術・提携・金融など)で並べ直すと、取引関係の形が見えやすくなります。

売り切った行は、目的と理由の欄が空のことが多く、デモの20行で記載があるのはマツダのダイナミックマッププラットフォーム㈱の1行(「自動車関連情報取引の関係維持」)だけです。

AIに頼むときは、出典と原文だけを渡す

目的と理由の文をAIに分類してもらうときは、出典(書類ID)と欄の原文だけを渡し、推測させない指示を付けます。

次の文は、有価証券報告書の特定投資株式の明細にある、保有目的などの欄の原文です。出典は各社の書類IDです。

- 各行を「調達」「販売」「技術・共同開発」「業務提携」「金融」「地域」「その他」のどれかに分類し、会社ごとの件数の表にしてください。
- 売却や増減の理由、今後の方針は推測しないでください。欄にないことは「原文で確認が必要」と書いてください。
- 会社の保有や売却の良し悪しについての意見は書かないでください。

## 本田技研工業(7267) 2026-03期 書類ID S100YDD9
スタンレー電気㈱:原材料等の調達取引の安定化資本業務提携
ニッコンホールディングス㈱:物流取引等の安定化
SES AI コーポレーション:共同開発関係の安定化
(21行まで同じ形)

(以下、同じ形で各社を続ける)

11. 自社のコードで動かす

会員登録をしてAPIキーを作成し、--live を付けて起動し直します。

python3 templates/disclosure-reader/server.py --live

キーは端末の非表示入力で渡し、ローカルのPythonプロセスだけが持ちます。政策保有の画面の分は、1社あたり1リクエスト(/cross-shareholdings)です。ほかの画面の分も含めると1社あたり12〜13リクエスト、5社なら最大65リクエストです。

404(その開示がない)は「なし」として扱い(テインがこの形)、それ以外の失敗が1件でもあれば全体を中止します。13節で足すボタンでは、APIを呼びません。

12. 数字の検算

記事の数字はテンプレートの関数で計算したものです。自社や競合のデータでも、次の手順で確かめられます。筆者はデモで1と2を行いました(7節の出力も同じデータです)。

1. 明細の行を数え直す。 テンプレートの関数を使わず、リポジトリの直下で実行します。

python3 - <<'EOF'
import json
d = json.load(open("templates/disclosure-reader/demo.json", encoding="utf-8"))
for c in d["companies"]:
    x = c["crossShareholdings"]
    if not x:
        print(c["symbol"], "なし")
        continue
    listed = next(s["issues"] for s in x["summary"]
                  if s["holder"] == "reporting" and s["purpose"] == "nonPureInvestment" and s["listing"] == "listed")
    rows = [h for h in x["holdings"] if h["holder"] == "reporting" and h["type"] == "specified"]
    held = [h for h in rows if h["shares"] is not None]
    sold = [h for h in rows if h["shares"] is None and h["priorShares"] is not None]
    both = [h for h in held if h["priorShares"] is not None]
    down = [h for h in both if h["shares"] < h["priorShares"]]
    up = [h for h in both if h["shares"] > h["priorShares"]]
    print(c["symbol"], listed, len(held), len(sold), len(down), len(up), len(held) - len(both))
EOF
7203 34 34 3 1 4 3
6902 8 8 4 2 1 0
7267 21 21 12 7 0 0
7261 3 3 1 1 0 0
7217 なし
  • 2列目(上場の保有先の数)と3列目(当期の株数がある行)は、4社とも一致します。明細が上位60までの会社では、3列目が少なくてもかまいません。
  • 4列目以降は、売り切った行、前期と当期の差で数え直した「減った」「増えた」行、前期が空の行で、4節と5節の表と同じです。

2. 持ち合いを両社の明細の名前で確かめる。 トヨタ自動車の明細にマツダ㈱(31,928,500株)、マツダの明細にトヨタ自動車㈱(41,466,500株)があり、どちらも当期の株数があります。両方の「相手の保有」が「有」で、互いの記載とも合います。画面は千株に四捨五入して31,929千株、②の大株主の表は千株単位で31,928千株なので、この差が出ます。

3. 原文と目で照合する。 画面の下の「出典」の書類ID(トヨタ自動車 S100Y8NY・デンソー S100Y9T1・本田技研工業 S100YDD9・マツダ S100YDZ1)でEDINETの有報を開き、「株式の保有状況」の保有先の数と、特定投資株式・みなし保有株式の明細を、画面と見比べます。名前の照合でまとまらない表記(カタカナと英字など)も、ここで目で確かめます。

13. 小さな変更:減った・売り切った行だけに絞る

保有中の明細は、株数が変わらない行が4社の66行のうち47行です。動きのあった行を読むために、株数が減った行と売り切った行だけを出す切り替えを足します。

変更後の「政策保有」。「減った・売り切った行だけ」を押し、本田技研工業の株数が減った7行と、売り切った12行が並んでいる

計算(model.mjs)とサーバー、APIの呼び出し回数は変えません。変えるのは app.mjs・i18n.mjs・style.css の3つです。

1. 状態を1つ足す(app.mjs の state)。

  keyword: params.get("q") ?? "", active: null, prompt: null, onlyDecreases: false,

2. ボタンを置く(app.mjs の renderCross()、会社ごとのカードを作るループの前)。

  // Display-only filter: rows still held whose share count fell; sold-out rows are always shown.
  const decreased = (s) => s.held.filter((r) => r.change !== null && r.change < 0);
  const only = node("button", t("onlyDecreases"), { type: "button", class: "chip toggle", "aria-pressed": String(state.onlyDecreases) });
  only.addEventListener("click", () => { state.onlyDecreases = !state.onlyDecreases; render(); document.querySelector(".cross-bar .toggle")?.focus(); });
  const bar = node("div", undefined, { class: "cross-bar" });
  bar.append(only);
  if (state.onlyDecreases) {
    const all = [...summaries.values()].filter(Boolean);
    bar.append(node("span", fill("decreasesShown", { down: all.reduce((n, s) => n + decreased(s).length, 0), sold: all.reduce((n, s) => n + s.soldOut.length, 0) }), { class: "muted small" }));
  }
  host.append(bar);

change は crossSummary が計算済みなので、負の行を選ぶだけです。

3. 保有中の明細の行を選ぶ(同じループの中)。 元の1行を、次の3行に置き換えます。

    const held = state.onlyDecreases ? decreased(s) : s.held;
    const heldTitle = state.onlyDecreases ? fill("heldDecreased", { n: held.length, all: s.held.length }) : `${t("heldTitle")}(${s.held.length})`;
    section.append(node("h3", heldTitle), held.length ? holdingTable(held, c) : node("p", state.onlyDecreases ? t("noDecrease") : "—", { class: "muted" }));

4. 文言を足す(i18n.mjs の ja と en)。

    onlyDecreases: "減った・売り切った行だけ", decreasesShown: "株数が減った{down}行と、売り切った{sold}行を出しています",
    heldDecreased: "保有中で株数が減った行({n} / {all})", noDecrease: "保有中の行に、株数が減ったものはありません。",
    onlyDecreases: "Only decreases and sell-outs", decreasesShown: "Showing {down} rows with fewer shares and {sold} sold-out rows",
    heldDecreased: "Still held, fewer shares ({n} / {all})", noDecrease: "No row still held has fewer shares.",

5. 見た目を整える(style.css の末尾)。 押した状態は塗りつぶしで示します。

.cross-bar { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; margin: 0 0 12px; }
.chip.toggle { flex: none; padding: 3px 12px; font-size: 12.5px; }
.chip.toggle[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--sheet); }

保存して再読み込みし、「政策保有」の画面で「減った・売り切った行だけ」を押します。「株数が減った11行と、売り切った20行を出しています」と出て、本田技研工業のカードが「保有中で株数が減った行(7 / 21)」になれば完成です。もう一度押すと元に戻ります。

テストはそのまま通ります(node --test tests/disclosure-reader.test.mjs)。文言を日本語と英語の片方だけに足すと失敗します。みなし保有株式と子会社が持つ分の表は絞りません。

14. 確認箇所

症状 確認箇所
明細の行数が上場の保有先の数より少ない 明細は資本金の1%超、60に満たなければ上位60までの開示。誤りではない
大株主の表に出る株主が、その株主の明細に出ない 株式の保有状況で開示する区分の外で持っている場合がある。原文で確かめる
株数が増えているが、取得の記載がない 株式分割の分も株数に入る。目的と理由の欄の「株式数が変動した理由」を読む
原文の名前に付く注記(「(注2)」など)が画面に出ない APIが名前から外して返す。注記の中身は原文で読む
「相手の保有」に原文の注記(「(注)3」など)が出ない APIは有・無と注記を別の欄で返し、テンプレートは有・無だけを使う。注記の中身は原文で読む

15. 次回は主要な販売先から取引の依存度を読む

今回は、政策保有の明細から増減・売り切った保有先・持ち合いを並べ、②の大株主の表とつなぎました。画面は入口で、判断は原文で行います。

次回のディープ解析④では、同じ開示リーダーの「監査と販売先」の画面で、有報の「主要な販売先」から、売上の10%以上を占める販売先と、売上高に対する割合を並べます。

ZENITH Code — 日本企業の財務データを、APIで。サービスサイトを見る

ZENITH Codeを見る · APIドキュメント

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?