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-10-30

経営会議や事業計画の資料を作るたびに、競合数社の決算短信や有価証券報告書を開き、売上高や利益をExcelに書き写していないでしょうか。転記には時間がかかり、決算期や会計基準の違いを見落とすと、比べてはいけない数字を並べてしまいます。

この連載「分析編」では、ZENITH CodeのAPIで、競合調査や比較分析に使える自社用の画面を作ります。初回は、自社と競合最大5社の収益性と効率を1枚に並べ、さらに同じ業種の上場会社全体の分布の中で、どのあたりにいるかを見られる「比較ボード」を読みます。

比較ボードの「収益性と効率」。トヨタ自動車と同業5社の売上高・営業利益率・純利益率・ROE・ROA・売上高3年成長率の表と、ROE・ROA・成長率を業種の分布帯と並べたグラフ

画面の数値は、2026年10月29日にZENITH Codeの実データを取得して保存したデモです。トヨタ自動車(7203)を自社に見立て、同業5社と並べています。「自社」の札はデモ上の見本です。

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

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

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

操作 画面で変わるもの 今回読むコード
自社のコードを入れて「競合候補を表示」を押す 同じ業種で規模の近い会社が候補に並ぶ server.py の fetch_peers と peer_rows
「比較データを取得」を押す 収益性と効率の表、業種の分布帯 fetch_bundle、model.mjs の roe・roa・revenueCagr3y
表の数字を確かめる 会社名の下の決算期と会計基準 companyMetrics
「印刷・PDF」「CSVで保存」を押す A4横の資料、比較表のファイル style.css の印刷設定、comparisonCsv

2. 登録不要で試す

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

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

ブラウザで http://127.0.0.1:8790 を開くと、保存済みのデモが表示されます。デモではAPIを呼ばないので、会員登録もAPIキーも要りません。終了するときは端末で Ctrl+C を押します。

3. 比較先の候補は、名前とコードだけ受け取る

比較先を毎回自分で探すのは手間です。ZENITH Codeの /stable/peers は、指定した会社と同じ東証33業種で、規模の近い会社を返します。

このAPIの応答には、時価総額やPER、配当利回りのような株価から計算する値も含まれます。比較ボードは財務諸表と開示の数値だけで作る画面なので、ローカルのサーバーで必要な項目だけを残してから、ブラウザへ渡しています。

# Whitelists: anything not listed here never reaches the browser. /peers and
# /industry-benchmarks also return price-derived fields (price, marketCap, PER,
# PBR, dividend yield, ...); this template must not display them.
PEER_FIELDS = ("role", "symbol", "nameJa", "nameEn", "sector")
BENCHMARK_FIELDS = ("sector", "sectorJa", "sectorEn", "count") + tuple(
    metric + suffix for metric in ("roe", "roa", "revenueCagr3y", "epsCagr3y")
    for suffix in ("Count", "Q1", "Median", "Q3"))

「使わない項目を消す」のではなく「使う項目だけを書き出す」形にしているのは、APIに項目が増えたときも、意図しない値が画面に出ないようにするためです。

候補の行では、対象の会社そのものと、業種の中央値の行も取り除きます。

peers = [r for r in picked if r["role"] == "peer" and isinstance(r["symbol"], str)
         and SYMBOL.fullmatch(r["symbol"]) and not _same(r["symbol"], expected)]
# The sector-median row carries market-based figures only; it is dropped.
return {"target": target, "peers": peers[:5]}

候補は出発点です。実際の競合は、事業の重なりで決まります。画面ではチェックを外したり、別の証券コードを足したりして、合計6社まで選べます。

4. 各社の最新通期を並べる

「比較データを取得」を押すと、サーバーは1社ずつ順番に、損益計算書・貸借対照表・キャッシュフロー計算書などを取得します。

company["income"] = statement_rows(requester("/income-statement", {
    "symbol": symbol, "period": "annual", "limit": 4}, api_key), symbol, INCOME_FIELDS, 4)
company["balance"] = statement_rows(requester("/balance-sheet-statement", {
    "symbol": symbol, "period": "annual", "limit": 1}, api_key), symbol, BALANCE_FIELDS, 1)

表に並ぶのは、各社の**最新の通期(FY)**です。会社名の下に、決算期と会計基準を必ず出しています。

トヨタ自動車
7203 · FY2026-03 · IFRS

今回の6社はどれも3月決算ですが、比較先に12月決算の会社を入れると、同じ列に9か月ずれた期間の数字が並びます。会計基準がIFRSと日本基準で違えば、営業利益の範囲も同じではありません。表の数字だけを見て判断しないよう、会社名の下を毎回確かめてください。

5. ROEとROAは、この画面で期末の値から計算する

ROE(自己資本利益率)とROA(総資産利益率)は、取得した損益計算書と貸借対照表から、model.mjs で計算しています。

const samePeriod = (a, b) => Boolean(a && b && a.fiscalPeriod === b.fiscalPeriod);

// Period-end values, not averages. Zero or negative denominators give no ratio.
export function roe(income, balance) {
  if (!samePeriod(income, balance)) return null;
  const top = finite(income.netIncomeAttributableToOwners);
  const equity = finite(balance.ownersEquity) ?? finite(balance.shareholdersEquity);
  return top === null || equity === null || equity <= 0 ? null : top / equity;
}
export function roa(income, balance) {
  if (!samePeriod(income, balance)) return null;
  const top = finite(income.netIncome), assets = finite(balance.totalAssets);
  return top === null || assets === null || assets <= 0 ? null : top / assets;
}

ポイントは3つです。

  • 分母は期末の値です。期首と期末の平均を使う方法もありますが、そのためには貸借対照表を2期分取得する必要があります。この画面はリクエスト数を抑えるため1期分だけ取得し、表の見出しに「期末値で計算」と書いています。
  • 損益計算書と貸借対照表の決算期が違えば計算しません。片方だけ新しい決算が反映されている時期に、違う期の数字で割り算をしないためです。
  • 分母が0以下なら計算しません。債務超過の会社でROEを計算すると、赤字なのにプラスの値が出ることがあるためです。

計算できない場合は null を返し、画面には「—」と表示します。0とは扱いません。

6. 3年成長率は、条件がそろうときだけ出す

売上高の3年成長率(年平均)は、4通期分の売上高から計算します。ただし、次の条件がすべてそろうときだけです。

// Three-year CAGR needs four consecutive ordinary fiscal years with the same
// fiscal month and the same accounting standard at both ends.
export function revenueCagr3y(years) {
  if (!Array.isArray(years) || years.length < 4) return null;
  const rows = years.slice(-4);
  for (let i = 0; i < rows.length; i++) {
    const days = finite(rows[i].fiscalPeriodDays);
    if (days === null || days < 360 || days > 366) return null;
    if (i && (rows[i].fiscalPeriod.slice(5) !== rows[0].fiscalPeriod.slice(5) ||
        Number(rows[i].fiscalPeriod.slice(0, 4)) - Number(rows[i - 1].fiscalPeriod.slice(0, 4)) !== 1)) return null;
  }
  const [base, , , last] = rows;
  if (!base.accountingStandard || base.accountingStandard !== last.accountingStandard) return null;
  const a = finite(last.revenue), b = finite(base.revenue);
  return a === null || b === null || b <= 0 || a < 0 ? null : (a / b) ** (1 / 3) - 1;
}
条件 外れる例
連続する4通期がある 上場して間もない、途中の年が欠けている
各期が通常の1年(360〜366日) 決算月を変えた年の変則決算(9か月など)
決算月が同じ 3月決算から12月決算に変えた
両端の会計基準が同じ 日本基準からIFRSに移った

デモでは、スズキの成長率が「—」になっています。取得した4通期のうち、2023年3月期と2024年3月期が日本基準、2025年3月期からがIFRSで、最初と最後の年の会計基準が違うためです。基準が変わると売上高の範囲が変わることがあり、その差まで「成長」に含めてしまいます。

表に空欄が出ると見栄えは悪くなりますが、条件が違う数字を年平均の成長率として並べるより、「比べられない」と示すほうが会議では安全です。

7. 業種の分布の中で見る

競合5社と比べるだけでは、「業界全体の中でどうか」が分かりません。6社とも低ければ、その中で1位でも業界では下のほうかもしれません。

そこで、/stable/industry-benchmarks から、自社と同じ業種の上場会社全体の四分位(Q1・中央値・Q3)を取得し、帯として重ねています。

用語 意味
Q1(第1四分位) 小さいほうから25%の位置の値
中央値 ちょうど真ん中の値
Q3(第3四分位) 小さいほうから75%の位置の値
帯(Q1〜Q3) 業種の真ん中の半分の会社が入る範囲

APIから受け取った四分位は、そのまま描かずに確かめます。

const BENCHMARK_METRICS = ["roe", "roa", "revenueCagr3y"];
export function normalizeBenchmark(row, sector) {
  if (!row) return null;
  if (row.sector !== sector) fail();
  const out = { sector: row.sector, sectorJa: row.sectorJa ?? null, sectorEn: row.sectorEn ?? null,
    count: finite(row.count) };
  for (const metric of BENCHMARK_METRICS) {
    const q1 = finite(row[metric + "Q1"]), median = finite(row[metric + "Median"]), q3 = finite(row[metric + "Q3"]);
    // A band is drawn only when the quartiles are complete and ordered.
    out[metric] = q1 !== null && median !== null && q3 !== null && q1 <= median && median <= q3
      ? { q1, median, q3, count: finite(row[metric + "Count"]) } : null;
  }
  return out;
}

別の業種の行が返ってきたら全体を中止し、3つの値のどれかが欠けている、または Q1 ≤ 中央値 ≤ Q3 の順になっていない指標は、帯を描きません。帯の下には、何社の分布かも出しています(デモの輸送用機器では、ROEとROAが79社、成長率が78社)。

帯の読み方

デモのROEの帯は、Q1 3.3%・中央値5.7%・Q3 9.9%です。トヨタ自動車のROEは9.6%で、帯の右端の近く、つまり業種の中では上位寄りにいます。売上高の3年成長率は10.9%で、帯(Q3 8.5%)の右の外側です。

読むときは、次の2点に注意してください。

  • 算定方法が同じとは限りません。帯はZENITH Codeが業種の全社について計算した値で、ROE・ROAが100%を超える会社などは外れ値として除いています。この画面の点は、前の節のとおり期末の値で計算しています。帯は「業界の水準」をつかむための目安として使います。
  • 帯は自社の業種で取得します。比較先に別の業種の会社を入れた場合、その会社にとっての業種の分布ではありません。

8. 会議資料にする

「印刷・PDF」を押すと、ボタン類を除いたA4横のレイアウトで印刷します。画面をダーク表示にしていても、印刷は常に明るい配色です。

「CSVで保存」では、比較表をファイルにします。CSVは表示用に丸めた文字ではなく、元の値を書き出します。

  • 金額は円単位、比率は小数(0.096 = 9.6%)
  • 値がないところは空欄(0にしない)
  • 業種のQ1・中央値・Q3も行として書き出す

表計算ソフトで開いたときに、= や - で始まる文字が数式として実行されないよう、先頭にアポストロフィを付けています。

// CSV cells are always quoted; text that a spreadsheet would treat as a formula
// gets a leading apostrophe. Missing values are blank cells, never zero.
export function csvCell(value) {
  if (value === null || value === undefined || (typeof value === "number" && !Number.isFinite(value))) return '""';
  const text = typeof value === "string" ? value.replace(/^[=+\-@\t\r]/, "'$&") : String(value);
  return '"' + text.replaceAll('"', '""') + '"';
}

社内の資料に使う場合も、出典の表示が必要です。画面には取得日時と各社の出典(書類ID)を、CSVには書類IDを入れています。共有するときはデータの表示ルールを確認してください。

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

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

python3 templates/peer-benchmark/server.py --live

キーは端末の非表示入力で渡します。キーはローカルのPythonプロセスだけが持ち、ブラウザには送りません。

画面で「実データ」を選び、自社の証券コードを入れて「競合候補を表示」を押します。比較先を選んで「比較データを取得」を押すと、1社あたり6リクエスト、業種の分布に1リクエストを使います。6社なら37リクエストです。

動き 理由
1社ずつ順番に取得する(並列にしない) 利用枠を一度に使い切らないため
1件でも失敗したら比較全体を中止する 一部の会社だけ古い、欠けた状態の表を会議に出さないため
自動の再試行・定期更新はしない 押した回数以上にリクエストしないため

定期的に自動で更新する仕組みは、連載の後の「運用編」で扱います。

10. 小さな変更:業種の上位25%・下位25%に印を付ける

帯の中の点の位置は、グラフをよく見ないと分かりません。会議の資料では、数字の横に印があると一目で伝わります。値がQ3以上なら「▲」、Q1未満なら「▼」を付けてみます。

変更後の業種の分布帯。Q3以上の値に緑の▲、Q1未満の値に赤の▼が付いている

1. 判定する関数を足す(model.mjs)。 roe() の下に足します。

// Where a value sits against the sector quartiles: top quarter, bottom quarter, or between.
export function bandZone(value, band) {
  value = finite(value);
  if (value === null || !band) return null;
  return value >= band.q3 ? "high" : value < band.q1 ? "low" : "mid";
}

値がない、または業種の分布がない場合は null を返し、印を付けません。

2. 読み込む名前に足す(app.mjs の先頭)。

  segmentMix, bandZone, domain, barGeometry, position, niceAxis, oku, percent, number, salary, comparisonCsv,

3. 数字の横に印を付ける(app.mjs の renderBands())。 値を表示している行を次のように変えます。

      const zone = bandZone(row[metric], band);
      const mark = zone === "high" ? " ▲" : zone === "low" ? " ▼" : "";
      line.append(node("span", nameOf(row.symbol), { class: "bn", title: nameOf(row.symbol) }), track,
        node("span", percent(row[metric], lang) + mark, { class: "bv" + (row[metric] === null ? " missing" : "") + (zone ? " " + zone : "") }));

4. 凡例を足す(app.mjs)。 texts の ja と en に文言を足し、凡例を作るループの後に2行目を足します。

    bandKeyHigh: "▲ Q3以上(業種の上位25%)", bandKeyLow: "▼ Q1未満(業種の下位25%)",
    bandKeyHigh: "▲ At or above Q3 (sector top 25%)", bandKeyLow: "▼ Below Q1 (sector bottom 25%)",
  for (const label of ["bandKeyHigh", "bandKeyLow"]) key.append(node("span", t(label), { class: "key-item" }));
  wrap.append(key);

5. 色と幅を整える(style.css)。 数字の列に印のぶんの幅を足し、色を付けます。スマホ幅(@media の中の .band-row)も同じように広げます。

.band-row { display: grid; grid-template-columns: 8.6em minmax(0, 1fr) 4.8em; gap: 10px; align-items: center; height: 27px; }
.band-row .bv.high { color: var(--ok); }
.band-row .bv.low { color: var(--neg); }

数字の列を広げたぶん、グラフの幅が少し狭くなり、ROAの目盛り(-5%〜7.5%の6本)が詰まって見えます。renderBands() の目盛りを間引く条件を、6本から間引くように変えます。

    const step = axis.ticks.length > 5 ? 2 : 1;

保存して再読み込みし、デモで本田技研工業のROEに「▼」、トヨタ自動車の成長率に「▲」が付けば完成です。色だけでなく記号でも示しているので、白黒で印刷しても区別できます。

11. 確認箇所

症状 確認箇所
ROE・ROAが「—」になる 損益計算書と貸借対照表の決算期が同じか、自己資本が0以下ではないか
成長率が「—」になる 4通期がそろっているか、変則決算・決算月の変更・会計基準の変更がないか
帯が表示されない 業種の分布の3つの値がそろい、順番どおりか(「業種の分布なし」と出る)
決算期の違う会社が並んでいる 会社名の下の決算期。月が違う会社は期間がずれている
印が付かない bandZone を import に足したか、業種の分布が取得できているか
CSVを開くと数値の桁が違う 金額は円単位、比率は小数で書き出している

表示を変える作業をAIに頼む場合も、計算やAPIの呼び出しに触れないことを明記すると、確認する範囲を絞れます。

peer-benchmarkテンプレートの業種の分布帯で、値がQ3以上なら「▲」、Q1未満なら「▼」を数字の横に付けてください。

判定は model.mjs に bandZone(value, band) として足し、Q1・Q3がない場合や値がない場合は印を付けないでください。
app.mjs の renderBands と凡例、texts の ja と en、style.css の数字の列の幅と色を変更します。
ROE・ROA・成長率の計算、server.py、APIの呼び出し回数は変更しないでください。
nullを0に変換しないでください。
変更箇所と、デモで確認する操作を説明してください。

12. 次回は事業別に比べる

今回は会社全体の収益性を、業種の分布と並べて見ました。ただ、同じ自動車メーカーでも、二輪や金融の事業を持つかどうかで、会社全体の利益率の意味は変わります。

次回のZENITH Code分析②では、各社が開示するセグメント情報を並べ、競合がどの事業で稼いでいるかを比べます。区分が会社ごとに違うセグメントを、どう並べれば誤読しないかも扱います。

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?