This article is not yet available in English. You are reading the Traditional Chinese original. The English edition will appear here once it is translated.
Browse articles that do have an English edition可比公司分析自動化:一次跑完一整個板塊
「把整個板塊的可比公司跑一遍」聽起來是個體力活,於是很多人把希望寄託在 AI 上:丟一串股票代碼進去,讓它吐出倍數表。結果往往是一張看起來專業、實際上每家公司口徑都不一樣的表格——A 家用的是上一個財政年度的數據,B 家幣別沒換,C 家的 EBITDA 含著一筆處置收益。這種表格比沒有表格更危險,因為它把「不可比」偽裝成了「可比」。
這篇文章的目標是把可比公司分析(Comparable Company Analysis,簡稱 comps)改造成一條可重跑、可審計、口徑統一的自動化流水線。讀完你會得到:同業挑選的準入與排除標準、六類必須統一的口徑問題與處理規則、倍數選擇的適用情境與陷阱、五段式流水線的 schema 設計與 Python 骨架,以及一套「AI 會在哪裡污染這張表」的機械化驗證清單。文中出現的任何數字都是教學用假設值,公司均為虛構,與真實市場數據無關。
一、先把問題定義清楚:難的是分母,不是算術
倍數的算術只有一行:
EV = 市值 + 淨負債(口徑見第二節)
P/E = 每股股價 ÷ 每股盈餘
P/B = 每股股價 ÷ 每股淨值
EV/EBITDA = 企業價值 ÷ EBITDA
EV/Sales = 企業價值 ÷ 收入
真正的工作量與風險全在分母的口徑上:EBITDA 是哪個期間的?調整過一次性項目沒有?租賃怎麼處理?幣別是什麼?淨負債含不含非控股權益?這些問題不解決,倍數之間的比較就是噪音。所以自動化的第一性原則不是「算得快」,而是:每一個進入表格的數字,都帶著期間、幣別、調整記錄與來源指針。沒有這四樣 metadata 的數字,禁止進入比較表。
「一次跑完一整個板塊」的正確含義也因此改變了:不是讓 AI 一口氣生成 20 家公司的倍數,而是把單家公司的「抽取→標準化→計算→驗證」固化成可批量重跑的技能,再對板塊內每家公司獨立執行、獨立驗證,最後才彙總。批量是結果,標準化才是原因。
二、同業挑選:五條準入標準與一張負面清單
挑選同業的錯誤通常是「用行業分類代碼代替業務判斷」。兩家公司掛同一個行業代碼,可能一家是品牌商、一家是代工廠,利潤結構完全不同。準入標準建議固定為五條,每條都要能在數據中找到證據:
| 標準 | 具體問題 | 證據來源 |
|---|---|---|
| 業務重疊 | 收入構成是否相近?主要產品線、客戶類型、商業模式(品牌/代工/平台)是否同類? | 年報分部資料、收入構成附註 |
| 規模帶 | 收入、資產、市值是否落在可比區間? | 損益表、資產負債表、市場數據 |
| 地區與監管 | 主要營運地、適用的會計準則與監管環境是否相近? | 年報公司資料、會計政策附註 |
| 成長與盈利階段 | 成長期、成熟期還是衰退期?毛利率與利潤率量級是否相近? | 多年損益表趨勢 |
| 會計口徑 | 財政年度截止日、報告幣別、會計準則(IFRS/US GAAP/當地準則)是否可統一? | 年報基本資料 |
同樣重要的是負面清單——以下情況直接排除或降級,不進入主比較表:
□ 僅因行業代碼相同、但收入構成明顯不同的公司
□ 財報期間或幣別無法可靠統一的公司(缺披露、缺匯率口徑)
□ 處於重大特殊狀態的公司:停牌、破產程序、剛完成重大重組、借殼
(不是永遠排除,而是「本期不可比」,留檔並註明原因)
□ 樣本不足時的硬湊:可比公司湊不夠,寧可縮小結論範圍,
也不要放寬標準把不可比的公司塞進來充數
挑選結果本身要產出一張「樣本表」:入選公司、入選理由(對應五條標準)、排除公司、排除理由。這張表是整份 comps 可被挑戰的第一層——挑戰樣本比挑戰倍數容易得多,先把樣本論證做扎實,後面的工作才有意義。
三、六類必須先統一的口徑問題
以下是批次比較中最常見的六種污染,每一種都對應一條明確的統一規則。規則要寫成流水線中的代碼,而不是寫在備忘錄裡。
| 口徑問題 | 不統一時的症狀 | 統一規則 |
|---|---|---|
| 財政年度不同 | A 家是 12 月結、B 家是 3 月結,直接拿「年度數據」相比 | 統一滾算至最近可得的 LTM(最近四季加總);無法滾算的標記為「期間不對齊」並降級 |
| 幣別不同 | 報告幣別、上市地幣別、收入幣別三者混用 | 明確登記每家的報告幣別;換算統一用同一時點或同一期間平均匯率,匯率來源與日期入檔 |
| 一次性項目 | 某家 EBITDA 含大額處置收益或減值,倍數突然「很便宜」 | 建立調整規則:處置損益、減值、重大訴訟和解等非經常項目逐項調整,每筆調整記錄金額與附註出處 |
| EV 構成差異 | 淨負債定義各算各的 | 固定 EV 公式並逐項登記口徑:是否含租賃負債、非控股權益、權益法投資、退休金缺口;受限現金不得計入可動用現金 |
| 股數口徑 | 基本股數與攤薄股數混用,市值差出一截 | 統一用攤薄股數(庫藏股法處理期權與認股權證),註明截止日期;可轉債的處理方式入檔 |
| 市場數據時點 | 股價是今天的、財報是半年前的 | 全表統一 as-of 日期;股價與財報期間的錯配寫進輸出備註 |
其中最容易出事的是租賃。現行租賃會計準則(如 IFRS 16)把多數經營租賃資本化上表,EBITDA 與淨負債同時被墊高;若樣本中有公司仍按舊準則或不同準則編製,EV/EBITDA 就系統性失真。處理方式不是「都調整」或「都不調整」二選一,而是登記每家的租賃口徑,調整到統一口徑,並把調整過程留檔。
統一後的 EV 公式建議固定成這樣(逐項可開關、口徑入檔):
EV = 市值(攤薄股數 × as-of 股價)
+ 總債務(含/不含租賃負債:登記選擇)
+ 非控股權益(按帳面或公允:登記選擇)
− 現金及約當現金(排除受限現金)
+ 退休金淨缺口(是否計入:登記選擇)
− 權益法投資(是否扣除:登記選擇,需與 EBITDA 口徑一致——
若 EBITDA 不含聯營損益,EV 也應扣除對應投資)
分子分母口徑必須成對一致:EV/Sales 的分母若含合併收入,分子的 EV 也應含對應子公司;EBITDA 若剔除了某分部,EV 也要剔除該分部對應的價值。這條「成對原則」是 comps 自動化裡最值得寫成斷言的規則。
四、倍數選擇:適用情境與各自的陷阱
沒有一個倍數適用於所有公司。選擇邏輯是「分母必須為正、必須與分子口徑匹配、必須反映你真正想比較的經濟變量」:
| 倍數 | 適用情境 | 主要陷阱 |
|---|---|---|
| P/E | 盈利穩定、資本結構相近的公司 | 虧損時失效;盈餘可被會計政策影響;高槓桿公司 P/E 偏低是風險不是便宜 |
| P/B | 金融業、重資產行業 | 負權益時失效;商譽與無形資產佔比高時淨值失真;不同會計準則下帳面不可比 |
| EV/EBITDA | 資本結構差異大、折舊攤提重的業務 | EBITDA 忽略維持業務所需的資本支出;租賃口徑污染(見上節);一次性項目未調整 |
| EV/Sales | 尚未盈利的成長期公司 | 完全忽略利潤率差異——高毛利與低毛利業務的 Sales 不可直接比 |
| 行業特定倍數 | 有明確計量單位的行業(每用戶、每單位產能、每儲量等) | 計量單位定義本身可能不統一;需行業知識才能審計 |
統計口徑上還有三條紀律:
1. 樣本含極端值時用中位數,不用平均數;同時報告四分位距
2. 樣本數太少時(例如個位數),不報「平均倍數」,逐家列示
3. 任何「行業平均倍數」都必須是當場從樣本算出來的,
禁止引用記憶中或模型輸出的「行業慣例值」——那是幻覺的高發區
第三條值得展開:LLM 被問「這個行業的 EV/EBITDA 通常是多少倍」時,幾乎總會給出一個流暢的數字,而且語氣篤定。這個數字沒有任何可追溯來源,是典型的編造。流水線層面的對策是:倍數只能由「抽取的原始數據 ÷ 計算」產生,模型不允許直接輸出倍數。這條規則的架構含義見 AI 在金融場景的幻覺風險。
五、自動化流水線:從抽取到輸出的五段設計
整條流水線分五段,每段有明確的輸入輸出契約,段與段之間用結構化 JSON 傳遞:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ ┌────────────┐
│ 1 樣本與 │→ │ 2 資料抽取 │→ │ 3 口徑標準化│→ │ 4 倍數計算│→ │ 5 異常處理 │
│ metadata │ │(帶來源) │ │(帶調整記錄)│ │(帶斷言)│ │ 與輸出 │
└────────────┘ └────────────┘ └────────────┘ └──────────┘ └────────────┘
每家公司獨立走 2→4,互不共享狀態;任何一段失敗只標記該公司,不污染全表
第 1 段:樣本與 metadata。 輸出樣本表(入選/排除與理由),以及每家的靜態 metadata:代碼、上市地、報告幣別、財政年度截止日、會計準則。
第 2 段:資料抽取。 每個數值都是「值+來源指針+as-of」三元組。來源優先級:結構化數據 API > 年報 PDF 抽取 > 網頁資訊(需人工複核)。年報抽取的做法見 300 頁年報轉結構化 JSON 與 PaddleOCR 抽取港股年報;API 資訊源的選擇見 金融數據 API 調研。抽取層的輸出 schema 示例:
{
"target_id": "FAKE-001",
"target_name": "甲公司(虛構,教學用)",
"as_of": "2026-06-30",
"reporting_currency": "HKD",
"fiscal_year_end": "12-31",
"accounting_standard": "HKFRS",
"facts": [
{
"field": "ebitda_ltm",
"value": 850.0,
"unit": "million",
"period": "LTM_2026Q2",
"source": {"type": "filing", "doc": "2025 annual report", "page": 68},
"adjustments": []
},
{
"field": "net_debt",
"value": 1200.0,
"unit": "million",
"period": "2026-06-30",
"source": {"type": "filing", "doc": "2026 interim report", "page": 12},
"adjustments": [
{"rule": "exclude_restricted_cash", "amount": -150.0,
"reason": "受限現金不得計入可動用現金", "source_page": 45}
]
}
]
}
第 3 段:口徑標準化。 規則引擎逐條套用第三節的統一規則,每筆調整寫入 adjustments 陣列。關鍵紀律:調整只能由規則觸發,不能由模型「覺得」觸發;規則本身有版本號,改了規則就要全表重跑。
第 4 段:倍數計算。 純函數計算,帶斷言:
def ev_from_facts(facts: dict) -> float:
"""EV = 市值 + 總債務 + 非控股權益 - 可動用現金(口徑由 policy 固定)"""
ev = facts["market_cap"] + facts["total_debt"] + facts["minority_interest"] \
- facts["cash_and_equivalents_unrestricted"]
assert ev > 0, "EV 為負:口徑錯誤或數據異常,標記人工裁決,不得靜默輸出"
return ev
def multiples(facts: dict, ev: float) -> dict:
out = {}
if facts.get("ebitda_ltm_adjusted", 0) > 0:
out["ev_ebitda"] = ev / facts["ebitda_ltm_adjusted"]
if facts.get("net_profit_ltm_adjusted", 0) > 0:
out["pe"] = facts["market_cap"] / facts["net_profit_ltm_adjusted"]
if facts.get("net_assets", 0) > 0:
out["pb"] = facts["market_cap"] / facts["net_assets"]
# 分母為負或缺失 → 該倍數輸出 null + 原因,而不是硬算或留空不解釋
for key in ("ev_ebitda", "pe", "pb"):
out.setdefault(key, None)
return out
第 5 段:異常值處理與輸出。 異常值的紀律是「標記→查因→裁決→留檔」,永遠不要靜默刪除。落在四分位距之外的倍數,先跑查因清單:一次性項目沒調乾淨?期間沒對齊?業務真的不同類?公司處於特殊狀態?查因結果與裁決(保留/剔除/降級)全部入檔。統計時剔除的樣本,仍要出現在附錄裡。
批量執行:「跑完一整個板塊」的正確打開方式
板塊級掃描不是把二十幾家公司塞進同一次對話,而是把單公司流水線當作原子任務批量調度。三條工程紀律:
1. 每家公司獨立上下文執行,互不共享工作記憶
2. 增量重跑:以(target_id, as_of, 規則版本)為快取鍵,
只有新財報發布或標準化規則升版時才重抽重算,其餘復用存檔
3. 全表彙總發生在所有單公司任務通過驗證之後;
彙總層只做統計與排序,禁止在彙總時「順手」修改任何一家的數字
第 1 條不是紙上談兵:連續處理多家公司時,上一家的數據洩漏進下一家,是本站實測記錄過的事故模式,完整復盤與 Clean Context 設計見 子代理隔離架構。
批量輸出的檔頭固定三行 metadata:全表 as-of 日期、標準化規則版本號、樣本構成(A/B/C 級家數與剔除名單)。任何人拿到這張表,先讀這三行,再讀倍數。
六、「不可比」是輸出而不是失敗:可比性分級
流水線的最終輸出表格,每家虛擬公司(教學假設值,均為虛構)都帶可比性等級與口徑備註:
| 公司(虛構) | EV/EBITDA (LTM) | P/E (LTM) | P/B | 可比性 | 口徑備註 |
|---|---|---|---|---|---|
| 甲公司 | 7.2 | 11.5 | 1.4 | A | 12 月結;LTM 已剔除處置收益 |
| 乙公司 | 9.8 | 15.2 | 2.1 | A | 12 月結;租賃已統一資本化口徑 |
| 丙公司 | 6.1 | n/a | 0.9 | B | 3 月結,LTM 滾算;本期虧損故無 P/E |
| 丁公司 | 14.5 | 28.0 | 3.8 | B | 含高毛利新業務分部,業務重疊度僅部分達標 |
| 戊公司 | — | — | — | C(剔除) | 重大重組進行中,本期不可比,留檔待下期重評 |
| 中位數(A+B 級) | 8.5 | 15.2 | 1.8 | 樣本 4 家,逐家列示如上 |
分級定義:
A 級:五條準入標準全達標,口徑全部統一,無需重大調整
B 級:準入達標但存在口徑調整或期間滾算,調整已留檔
C 級:本期不可比(特殊狀態/無法統一),剔除出統計、保留在附錄
規則:統計量只用 A+B;C 級必須附原因;A 級不足 3 家時,
結論降級為「逐家比較」,不輸出板塊統計量
這個設計的好處是:「不可比」從流水線的失敗,變成了流水線的正式產出之一。整張表自帶論證結構——讀者可以逐家檢查調整記錄,而不是只能選擇相信中位數。
七、AI 會在這裡出錯:七種污染與機械驗證
把這條流水線交給 AI Agent 時,可預測的失敗模式與對應驗證:
| AI 的典型錯誤 | 症狀 | 機械化驗證 |
|---|---|---|
| 憑記憶給數字 | 倍數「看起來合理」但無來源指針 | schema 強制:無 source 欄位的 fact 一律拒收 |
| LTM/NTM/FY 混用 | 同表內期間口徑不一致 | period 欄位枚舉校驗;全表 as-of 一致性斷言 |
| 幣別與單位錯 | 百萬/億、HKD/USD 混用 | unit 與 currency 必填;跨公司匯算必須引用同一匯率記錄 |
| 一次性項目未剔除 | 某家倍數異常「便宜」 | 調整規則清單逐條核對;異常倍數強制觸發查因流程 |
| 股數口徑錯 | 市值系統性偏低或偏高 | 登記基本/攤薄口徑;市值=攤薄股數×股價的重算斷言 |
| 把異常值直接刪掉 | 中位數「變漂亮」但無記錄 | 剔除必須有裁決記錄;統計樣本與附錄樣本對帳 |
| 編造「行業平均」 | 輸出記憶中的倍數當共識 | 架構規則:倍數只能由計算產生,模型輸出中出現直接倍數即判失敗 |
在此之上加兩道整體性驗證。第一道是重算抽檢:隨機抽 2–3 家,從來源文件重新抽取同一字段,與流水線結果比對;不一致即整批凍結排查。第二道是雙源交叉:關鍵字段(收入、淨負債)若同時有 API 與年報兩個來源,自動比對差異,超過容差就標記人工裁決——兩個獨立來源不一致,本身就是最有價值的警報。
這套「假設產出會被污染、所以每段都設卡」的思路,與 多 Agent 投研流水線 的失敗隔離設計同源;把驗證升級成系統級門控與評測集,則見 自建投研系統。
下一步
- DCF 假設與敏感度分析:comps 給出市場口徑的區間後,用內在價值視角交叉檢查,並把兩者的假設分歧寫成可挑戰的論證
- 300 頁年報轉結構化 JSON:抽取層的具體實作,本篇流水線第 2 段的上游
- 三表聯動與現金流品質檢查:在把一家公司納入樣本之前,先確認它的賬面數字經得起現金流檢驗
- 多 Agent 投研流水線:當板塊掃描要並行處理幾十家公司時,怎麼分工與隔離失敗
More in Tools
- PaddleOCR in Practice: Extracting Hong Kong Stock Annual Report Financial Data in 83 Seconds
- Webb-Site: The Essential Hidden Treasure for Hong Kong Stock Research, a One-Click Tool to Get Annual Report PDFs for All Listed Companies
- Academic Research Skills Deep Technical Breakdown: How 45+ Agents Collaborate to Complete the Full Workflow from Literature Review to Peer Review
- AI Engineering from Scratch Deep Dive: 435 Lessons × 20 Stages