部品会社や取引先が、売上のどれだけを特定の顧客に頼っているかを調べるとき、有価証券報告書の「主要な顧客」の欄を1社ずつ開いていないでしょうか。顧客の名前と金額は載っていても、売上高に対する割合は書かれていないことが多く、顧客の範囲(その会社だけか、グループ会社まで含めるか)も会社ごとに違います。
前回のディープ解析③では、政策保有株式の明細から、保有先の増減・売り切った保有先・持ち合いを並べました。今回は同じ「開示リーダー」の「監査と販売先」の画面で、売上の10%以上を占める販売先と、売上高に対する割合を、自動車部品の5社で並べて読みます。
画面の内容は、2026年11月5日にZENITH Codeの実データを取得したデモです。デンソー(6902)・アイシン(7259)・トヨタ紡織(3116)・武蔵精密工業(7220)・エフ・シー・シー(7296)の、2026年3月期の有価証券報告書を並べています。
私たち株式会社Crabgrassは、企業の財務データなどをAPIで提供するZENITH Codeを開発しています。本記事は自社サービスのデータを使うためのテンプレートを題材にした実装例です。
この記事のコードは、公開サンプル集のdisclosure-readerテンプレート(執筆時点の版)に対応しています。画面の仕組みの基本(ローカルのPythonサーバーとブラウザの役割分担)は、実践編で詳しく扱いました。
1. この回で読む画面と操作
| 画面の部分 | 分かること |
|---|---|
| 主要な販売先(「監査と販売先」の下半分) | 売上の10%以上を占める販売先の名前・関連するセグメント・当期と前期の売上・売上高に対する割合・どの表から読んだか |
| 事業等のリスク(「本文の読み比べ」) | 特定の顧客への依存を、会社がリスクとしてどう書いているか |
2. 登録不要で試す
必要なものはPython 3.9以上とブラウザです。追加のパッケージはありません。①〜③の自動車会社5社とは別の保存データを使うので、--demo suppliers を付けて起動します。
git clone https://github.com/Crab-grass/zenith-code-examples.git
cd zenith-code-examples
python3 templates/disclosure-reader/server.py --demo suppliers
ブラウザで http://127.0.0.1:8790 を開き、左の「監査と販売先」(上から4番目)を押します。デモではAPIを呼ばないので、会員登録もAPIキーも要りません。終了するときは端末で Ctrl+C を押します。--demo を付けなければ、①〜③の5社で開きます。
3. 販売先のデータは2つのAPIから受け取る
主要な販売先の表は、1社につき /stable/main-customers を1回呼びます。有報の注記(「主要な顧客ごとの情報」、IFRSの会社は「主要な顧客に関する情報」)から、売上の10%以上を占める顧客を、顧客名・その顧客への売上(円)・関連するセグメントとともに、当期と前期で返すAPIです。注記に顧客の表がない会社は、「主な相手先別の販売実績」の表から読みます。どちらから読んだかは sourceTag で分かります。
割合を出すには売上高が要るので、販売先がある会社だけ /stable/income-statement(年次・3期)を1回足します。
company["mainCustomers"] = clean_customers(optional(requester, "/main-customers", {"symbol": symbol}, api_key), symbol)
company["revenue"] = []
if company["mainCustomers"] and company["mainCustomers"]["customers"]:
# Only needed to express each customer as a share of annual revenue.
company["revenue"] = clean_revenue(requester("/income-statement", {
"symbol": symbol, "period": "annual", "limit": 3}, api_key), symbol)
ブラウザに渡すのは、販売先の行の period・customer・revenue・segment・notes と、損益計算書の売上高と期、それに読んだ表の種類だけです。clean_customers は sourceTag を customerSource(notes か sales)に置き換えて渡します。
out["customerSource"] = CUSTOMER_SOURCES.get(row.get("sourceTag")) # "notes", "sales" or None
顧客名を書いていない会社もあります。「売上の10%以上を占める顧客グループが1グループ」と金額だけ書く会社や、2つの顧客グループの合計額だけを書く会社です。この場合も、APIは金額・期・セグメントの行を返し、customer を null、理由を notes に入れます(例:TDK「顧客名の記載なし(原文: 1グループ)」466,405百万円)。テンプレートは名前の欄に「顧客名の記載なし」か「複数の顧客グループの合計」と理由を出します。
404 は、エラーではなく「販売先の行がない」として扱います。売上の10%以上の販売先の記載がない会社のほか、有価証券報告書がまだない会社(上場したばかりの会社・TOKYO PRO Market など)や、金額が幅でしか書かれていない会社(例:「4,701~9,403百万円」)も 404 です。404 の本文の reason に、幅だけの会社は amount_range_only が入ります。依存度を判断する前に、原文の「主要な顧客」の欄を確かめます。
4. 売上高に対する割合を計算する
割合は model.mjs の customerRows で計算します。
const target = { current: report.fiscalPeriod, prior: addMonths(report.fiscalPeriod, -12) };
const rows = report.customers.map((c) => {
const period = target[c.period] ?? null;
const revenueRow = period ? (revenueRows ?? []).find((r) => r.period === "FY" && r.fiscalPeriod === period) : null;
const revenue = finite(revenueRow?.revenue);
const amount = finite(c.revenue);
const share = amount !== null && revenue > 0 ? amount / revenue : null;
- 当期の行は、同じ決算期(2026年3月期)の年間の売上高で割ります。
- 前期の行は、12か月前の期(2025年3月期)の売上高で割ります。決算期を変えた会社などで、その期の売上高がなければ割りません。
- 割合が100%を超える行は、単位が原文と合っていない可能性があるので、金額も割合も出しません。
5. 5社の販売先と割合
画面のカードの数字を表にまとめます。売上は百万円、割合は売上高(IFRSの会社は売上収益)に対する割合です。
| 会社 | 販売先(原文の表記) | 前期 | 当期 | 割合(前期→当期) |
|---|---|---|---|---|
| デンソー | トヨタグループ | 3,903,121 | 4,134,582 | 54.5% → 54.8% |
| アイシン | トヨタ自動車㈱及びそのグループ会社 | 2,870,025 | 3,112,449 | 58.6% → 60.8% |
| トヨタ紡織 | トヨタ自動車(株)グループ | 1,422,366 | 1,489,792 | 72.8% → 73.1% |
| 武蔵精密工業 | 本田技研工業株式会社 | — | 21,931 | — → 6.3% |
| エフ・シー・シー | 本田技研工業㈱及び同グループ | 94,933 | 92,676 | 37.0% → 35.5% |
| エフ・シー・シー | Ford及び同グループ | 51,659 | 52,574 | 20.1% → 20.2% |
| エフ・シー・シー | ゼネラルモーターズ | 23,747 | 24,089 | 9.3% → 9.2% |
5社とも、読んだ表は注記(カードの「出どころ:注記の主要な顧客の情報」)です。武蔵精密工業は当期の行だけが開示されています。エフ・シー・シーのゼネラルモーターズは、売上高で割ると10%を下回りますが、注記には載っています。APIは注記にある行をそのまま返し、10%で切りません。
エフ・シー・シーのホンダは、原文では関連するセグメントごとに2行(二輪事業 57,273・四輪事業 35,403)です。APIは同じ顧客の行を足して1行にし、セグメントを「二輪事業、四輪事業」とつないで返します。
この表だけを見ると、武蔵精密工業はホンダへの依存が小さい会社に見えます。次の節で、そう読めない理由を見ます。
6. 販売先の範囲は、会社と表で違う
販売先の名前を見比べると、範囲が3通りあります。
- グループ会社を含める:デンソー「トヨタグループ」、アイシン「…及びそのグループ会社」、トヨタ紡織「…グループ」、エフ・シー・シー「…及び同グループ」。
- その会社だけ:武蔵精密工業「本田技研工業株式会社」(関連するセグメントは「日本」)。グループ会社を含めた割合は、7節のとおり事業等のリスクに書かれています。
- 同じ会社でも、読む表によって範囲が変わる:有報の「主な相手先別の販売実績」の表は、会社ごとに1行です。
3つめは、同じデンソーの有報で確かめられます。①〜③の保存データ(--demo なしで起動、2026年11月4日取得)のデンソーのカードは、販売実績の表から読んだ「トヨタ自動車㈱ 1,991,604百万円」で、売上高の26.4%です。今回の保存データは注記の「トヨタグループ 4,134,582百万円」で、54.8%です。同じ有報、同じ期で、割合がほぼ倍違います。
ZENITH Codeは、注記に顧客の情報があればそちらを先に返します(IFRSの会社も同じ)。テンプレートは、カードの上に読んだ表を出します。
-
notes:「出どころ:注記の主要な顧客の情報(グループ会社を含めて1行に書く会社があります)」 -
sales:「出どころ:主な相手先別の販売実績の表(会社ごとの行)」
比べる会社の出どころがそろっていないときは、割合をそのまま並べないでください。
名前の照合では、同じ顧客にまとまらない
③で使った companyKey(㈱・株式会社・空白の違いをそろえる関数)にかけても、トヨタ側の4つの表記は別のままです。
トヨタグループ → トヨタグループ
トヨタ自動車㈱及びそのグループ会社 → トヨタ自動車及びそのグループ会社
トヨタ自動車(株)グループ → トヨタ自動車グループ
トヨタ自動車株式会社 → トヨタ自動車
APIは顧客名を提出者の書いたとおりに返します(「(株)」もそのままです)。比べる会社の販売先を顧客ごとに集計するときは、名前の自動照合ではなく、自分で決めた対応表を使います。
7. 事業等のリスクで、会社自身の書き方を読む
販売先の範囲を確かめるには、同じ有報の「事業等のリスク」が役に立ちます。「本文の読み比べ」で節を「事業等のリスク」にし、キーワードに「依存」と入れると、5社とも「依存」を含む見出しが出ます(デンソー「顧客企業の業績への依存」、アイシン「得意先への依存」、トヨタ紡織と武蔵精密工業「特定の取引先への依存」、エフ・シー・シー「特定の産業や取引先への依存について」)。トヨタ紡織と武蔵精密工業は、仕入先への依存の見出し(「原材料、部品供給元への依存」「特定のサプライヤーへの依存」)にも当たります。
見出しの下の文に、会社自身が書いた割合があります。注記の割合と並べます。
| 会社 | 注記(API)の割合 | 事業等のリスクの記載 |
|---|---|---|
| デンソー | 54.8%(トヨタグループ) | トヨタグループ向けが売上の約半分 |
| アイシン | 60.8%(トヨタ自動車㈱及びそのグループ会社) | トヨタグループの割合 69.7% |
| トヨタ紡織 | 73.1%(トヨタ自動車(株)グループ) | トヨタ自動車㈱への割合 23.5% |
| 武蔵精密工業 | 6.3%(本田技研工業株式会社) | ホンダグループへの割合 約51% |
| エフ・シー・シー | 35.5%(本田技研工業㈱及び同グループ) | ホンダグループの割合 約36% |
- トヨタ紡織は、注記がグループ(73.1%)、リスクの記載がトヨタ自動車1社(23.5%)です。
- 武蔵精密工業は逆で、注記が1社(6.3%)、リスクの記載がグループ(約51%)です。
- アイシンは、どちらも「グループ」ですが、割合が違います(60.8%と69.7%)。グループに入れる会社の範囲が、欄ごとに同じとは限りません。
- エフ・シー・シーは、2つの欄がほぼ同じです。
依存度を比べるときは、注記の数字だけで決めず、事業等のリスクの記載と範囲をそろえてから読みます。 画面の割合は入口で、どの範囲の数字かは原文で確かめます。
8. 大株主の表とつなぐ
②の「株主」の画面で、同じ5社の大株主の上位を見ると、5社とも販売先の自動車会社が1位です。
| 会社 | 大株主の1位 | 割合 |
|---|---|---|
| デンソー | トヨタ自動車株式会社 | 22.24% |
| アイシン | トヨタ自動車株式会社 | 22.34% |
| トヨタ紡織 | トヨタ自動車株式会社 | 32.43% |
| 武蔵精密工業 | 本田技研工業株式会社 | 24.96% |
| エフ・シー・シー | 本田技研工業株式会社 | 22.47% |
トヨタ紡織は、事業等のリスクの同じ段落に、トヨタ自動車による議決権の所有割合(直接所有32.4%)も書いています。販売先と大株主が同じ会社であることは、取引関係を読むときの手がかりになりますが、この表から取引の条件や方針は分かりません。
9. AIに頼むときは、出典と原文だけを渡す
範囲の違う数字を並べたまま要約させると、AIは「依存度が高い・低い」と数字だけで決めがちです。出典(書類ID)と欄の原文を渡し、範囲を書き出させます。
次の文は、有価証券報告書の「主要な顧客」の注記と、「事業等のリスク」の取引先への依存の段落の原文です。出典は各社の書類IDです。
- 会社ごとに、2つの欄それぞれについて「販売先の範囲(その会社だけ/グループ会社を含む)」「割合または金額」を表にしてください。
- 範囲が欄で違う会社には印を付けてください。
- 書かれていない割合を計算したり推測したりしないでください。欄にないことは「原文で確認が必要」と書いてください。
- 取引関係の良し悪しや今後についての意見は書かないでください。
## 武蔵精密工業(7220) 2026-03期 書類ID S100YGX6
(主要な顧客ごとの情報の原文)
(事業等のリスクの「特定の取引先への依存」の段落の原文)
(以下、同じ形で各社を続ける)
10. 自社のコードで動かす
会員登録をしてAPIキーを作成し、--live を付けて起動し直します。
python3 templates/disclosure-reader/server.py --live
キーは端末の非表示入力で渡し、ローカルのPythonプロセスだけが持ちます。主要な販売先の分は、1社あたり1〜2リクエスト(/main-customers と、販売先がある会社だけ /income-statement)です。ほかの画面の分も含めると1社あたり12〜13リクエスト、5社なら最大65リクエストです。
保存データを自分の会社の組み合わせで作り直すときは、build_demo.py に証券コードを渡します。
python3 templates/disclosure-reader/build_demo.py --set suppliers # 今回の5社を取り直す
python3 templates/disclosure-reader/build_demo.py --set suppliers 7282 6995 # 別の組み合わせ
11. 数字の検算
記事の割合はテンプレートの関数で計算したものです。テンプレートを使わずに、保存データから計算し直せます。リポジトリの直下で実行します。
python3 - <<'EOF'
import json
d = json.load(open("templates/disclosure-reader/demo-suppliers.json", encoding="utf-8"))
for c in d["companies"]:
m = c["mainCustomers"]
fy = m["fiscalPeriod"]
prior = str(int(fy[:4]) - 1) + fy[4:]
rev = {r["fiscalPeriod"]: r["revenue"] for r in c["revenue"] if r["period"] == "FY"}
for x in m["customers"]:
base = rev.get(fy if x["period"] == "current" else prior)
share = x["revenue"] / base * 100 if base else None
print(c["symbol"], x["period"], x["customer"], round(x["revenue"] / 1e6), "—" if share is None else f"{share:.1f}%")
EOF
6902 prior トヨタグループ 3903121 54.5%
6902 current トヨタグループ 4134582 54.8%
7259 prior トヨタ自動車㈱及びそのグループ会社 2870025 58.6%
7259 current トヨタ自動車㈱及びそのグループ会社 3112449 60.8%
3116 prior トヨタ自動車(株)グループ 1422366 72.8%
3116 current トヨタ自動車(株)グループ 1489792 73.1%
7220 current 本田技研工業株式会社 21931 6.3%
7296 prior 本田技研工業㈱及び同グループ 94933 37.0%
7296 current 本田技研工業㈱及び同グループ 92676 35.5%
7296 prior Ford及び同グループ 51659 20.1%
7296 current Ford及び同グループ 52574 20.2%
7296 prior ゼネラルモーターズ 23747 9.3%
7296 current ゼネラルモーターズ 24089 9.2%
画面と5節の表と一致します。筆者は次も確かめました。
- 原文と照合する。 画面の下の「出典」の書類ID(デンソー S100Y9T1・アイシン S100YAWO・トヨタ紡織 S100Y9AH・武蔵精密工業 S100YGX6・エフ・シー・シー S100YFKQ)で有報を開き、「主要な顧客」の注記の金額と、事業等のリスクの割合(7節)を見比べました。エフ・シー・シーのホンダは、注記の2行の和(57,273+35,403=92,676)です。
-
ほかの会社でも確かめる。 執筆の前に、上場会社から選んだ1,000社で
/main-customersを取り、売上高を超える行と、同じ顧客・同じ金額の重複がないことを確かめました。
12. 小さな変更:いちばん大きい販売先を1つの表に並べる
カードは会社ごとに分かれているので、比べる会社の当期でいちばん大きい販売先を1つの表に並べます。
計算(model.mjs)とサーバー、APIの呼び出し回数は変えません。変えるのは app.mjs と i18n.mjs の2つです。
1. 表を足す(app.mjs の renderAudit()、const blocks = … の行の前)。
// This year's largest customer of each company, side by side (display only).
const tops = companies.map((c) => {
const result = M.customerRows(c.mainCustomers, c.revenue);
const top = result.rows.filter((r) => r.period === "current" && r.share !== null).sort((a, b) => b.share - a.share)[0] ?? null;
return { company: c, top, source: result.source, none: result.status === "none" };
});
host.append(panel(t("topTitle"), t("topNote"), table([
{ label: "", render: (r) => companyTitle(r.company) },
{ label: t("customer"), cls: "name", render: (r) => (r.top ? (r.top.named ? r.top.customer : t("unnamedCustomer"))
: r.none ? t("noCustomers") : t("topNoShare")) },
{ label: t("shareOfRevenue"), render: (r) => (r.top ? ratioCell(r.top.share) : "") },
{ label: t("topSource"), cls: "nowrap", render: (r) => (r.source ? t("topSource_" + r.source) : "—") },
], tops)));
customerRows が返す割合(share)と出どころ(source)をそのまま使います。割合が出ない行(期が合わない・売上高が空・単位の疑い)は選びません。販売先の行はあるのに当期の割合が出ない会社(当期は10%未満で前期の行だけの会社や、売上高が空の会社)は、「記載なし」と区別して出します。いちばん大きい行が顧客名のない行なら「顧客名の記載なし」と出します(テンプレートの文言をそのまま使います)。
2. 文言を足す(i18n.mjs の ja と en)。
topTitle: "いちばん大きい販売先(当期)", topNote: "販売先の名前に「グループ」があるかと、出どころを見てから比べます。", topSource: "出どころ",
topSource_notes: "注記", topSource_sales: "販売実績の表", topNoShare: "当期の割合を出せる行なし(表で確かめます)",
topTitle: "Largest customer (this year)", topNote: "Check whether the customer name includes its group and where it was read before comparing.", topSource: "Source",
topSource_notes: "Notes", topSource_sales: "Sales table", topNoShare: "No row with a share for this year (see the table)",
保存して再読み込みし、「監査と販売先」に「いちばん大きい販売先(当期)」の表が出れば完成です。トヨタ紡織 73.1%、武蔵精密工業 6.3% のように並びますが、6節と7節のとおり、範囲の違う数字です。表の「販売先」の列で範囲を確かめてから読みます。
テストはそのまま通ります(node --test tests/disclosure-reader.test.mjs)。文言を日本語と英語の片方だけに足すと失敗します。
13. 確認箇所
| 症状 | 確認箇所 |
|---|---|
| 主要な販売先が404になる | 売上の10%以上の販売先の記載がない会社のほか、有価証券報告書がない会社・金額が幅でしか書かれていない会社(reason が amount_range_only)も 404 になる。「依存先なし」と決めず、原文の「主要な顧客」の欄を確かめる |
| 販売先の名前が「顧客名の記載なし」「複数の顧客グループの合計」 | 原文が顧客名を書かず金額だけを書いた会社、2つ以上の顧客グループの合計額だけを書いた会社。APIは customer を null にし、理由を notes に入れる。金額と割合は原文どおり |
| 割合が出ない(「同じ期の売上高がない」) | 上場前の期の行(前期の売上高がない)、損益計算書の売上高が空の会社(売上を事業収益などで開示する会社)。金額は原文どおり出る |
| 当期の行がなく、前期の行だけある | 当期は10%未満になった会社。原文の当期の欄は「-」や「該当なし」。「いちばん大きい販売先」の表には「当期の割合を出せる行なし」と出る |
| 銀行の販売先の金額が「経常収益」 | 銀行は売上高の代わりに経常収益で書く。割合は損益計算書の経常収益で割る |
| 同じ会社で、以前の数字と割合が大きく違う | 読んだ表が違う。注記はグループ会社を含めることがあり、販売実績の表は会社ごと。カードの「出どころ」を見る |
| 割合が10%を下回る販売先がある | 注記に載っている行はそのまま返す。会社が10%未満の顧客も載せることがある |
| 原文では2行の顧客が1行になっている | 同じ顧客のセグメント別の行は、APIが足して1行にし、セグメントを「、」でつなぐ |
| 顧客名の「(株)」「㈱」「株式会社」がそろっていない | 提出者の書いたまま返す。顧客ごとに集計するときは自分の対応表を使う |
| 前期の行に割合が出ない | 12か月前の期の売上高がない(決算期の変更など)。割らずに金額だけ出す |
14. 次回は監査報酬を並べる
今回は、主要な販売先の割合を5社で並べ、販売先の範囲が会社と表で違うこと、事業等のリスクの記載と並べて読むことを見ました。画面は入口で、判断は原文で行います。
次回のディープ解析⑤では、同じ「監査と販売先」の画面の上半分で、監査報酬(監査証明業務と非監査業務)と、監査法人のネットワークへの報酬を並べます。


