Corin1998 commited on
Commit
412434a
·
verified ·
1 Parent(s): 1646b86

Update core/ai_judgement.py

Browse files
Files changed (1) hide show
  1. core/ai_judgement.py +84 -141
core/ai_judgement.py CHANGED
@@ -1,174 +1,117 @@
1
  # core/ai_judgement.py
2
  from __future__ import annotations
 
3
  import os, json
4
- from typing import Dict, Any, List
5
  from openai import OpenAI
6
 
7
  OPENAI_MODEL_TEXT = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
8
 
9
- def _client() -> OpenAI:
10
- key = os.environ.get("OPENAI_API_KEY")
11
- if not key:
12
- raise RuntimeError("OPENAI_API_KEYが未設定です")
13
- return OpenAI(api_key=key, timeout=30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  def suggest_external_with_llm(fin: Dict[str, Any], company: str = "") -> Dict[str, Any]:
16
  """
17
- 外部入力DFの不足をLLMが推定して埋めるための候補を返す。
18
- 失敗時は空dictで戻す(安全)
19
  """
 
20
  try:
21
- prompt = f"""
22
- あなたは企業アナリストです。以下の財務データから、可能な範囲で数値を推定して埋めてください。
23
- 返答は必ず JSON オブジェクトのみで、存在しない項目は省略可。単位は以下のキーに合わせてください。
24
-
25
- 必須候補キー例:
26
- - "市場の年成長率(%)"
27
- - "主力商品数"
28
- - "成長中主力商品数"
29
-
30
- [財務データ]
31
- {json.dumps(fin, ensure_ascii=False)}
32
- """
33
- cli = _client()
34
- resp = cli.chat.completions.create(
35
- model=OPENAI_MODEL_TEXT,
36
- messages=[
37
- {"role": "system", "content": "出力は厳密なJSON。説明不要。"},
38
- {"role": "user", "content": prompt},
39
- ],
40
- response_format={"type": "json_object"},
41
- temperature=0.2,
42
- )
43
- data = json.loads(resp.choices[0].message.content)
44
- # 型の安全化
45
- out = {}
46
- for k in ["市場の年成長率(%)", "主力商品数", "成長中主力商品数"]:
47
- v = data.get(k, None)
48
- if v is None: continue
49
- try:
50
- out[k] = float(str(v).replace(",",""))
51
- except Exception:
52
- continue
53
- # 整合性
54
- if ("主力商品数" in out) and ("成長中主力商品数" in out):
55
- if out["成長中主力商品数"] > out["主力商品数"]:
56
- out["成長中主力商品数"] = out["主力商品数"]
57
- return out
58
  except Exception:
59
- return {}
 
60
 
61
- def ai_evaluate(fin: Dict[str, Any], ext_df_like: Dict[str, float] | None = None) -> Dict[str, Any]:
62
  """
63
- AI評点(外部評価と基準を分離): 将来リスク・効率性・成長余地・資金繰り耐性・資本政策。
64
- LLMは短評のみ使用(数値はルールベース)。ext_df_likeの値(市場成長率・商品情報等)があれば加味。
65
  """
66
  bs = fin.get("balance_sheet", {}) or {}
67
  is_ = fin.get("income_statement", {}) or {}
68
  cf = fin.get("cash_flows", {}) or {}
69
 
70
- def _to_f(x):
71
- if x in (None, "", "null"): return None
72
- try: return float(str(x).replace(",","").replace("▲","-").replace("△","-"))
73
- except Exception: return None
74
-
75
  sales = _to_f(is_.get("sales"))
76
  op = _to_f(is_.get("operating_income"))
77
- ni = _to_f(is_.get("net_income"))
78
- assets = _to_f(bs.get("total_assets"))
79
- liab = _to_f(bs.get("total_liabilities"))
80
- equity = _to_f(bs.get("total_equity"))
81
  ca = _to_f(bs.get("current_assets"))
82
  cl = _to_f(bs.get("current_liabilities"))
 
 
 
83
  ocf = _to_f(cf.get("operating_cash_flow"))
84
 
85
- def ratio(a,b):
86
- if a is None or b is None or b==0: return None
87
- return a/b
88
- def ramp(x, good, bad, lo=0, hi=10):
89
- if x is None: return 5.0
90
- if good>bad:
91
- if x<=bad: return lo
92
- if x>=good: return hi
93
- return lo+(hi-lo)*(x-bad)/(good-bad)
94
- else:
95
- if x>=bad: return lo
96
- if x<=good: return hi
97
- return lo+(hi-lo)*(x-good)/(bad-good)
98
- def clamp01(x): return max(0.0, min(10.0, x))
99
-
100
- # ①効率性: 回転・収益性
101
- margin = ratio(op, sales) # 営業利益率
102
- at = ratio(sales, assets) # 総資産回転率
103
- eff = (ramp(margin, 0.12, 0.01) + ramp(at, 1.3, 0.3)) / 2
104
-
105
- # ②資金繰り耐性: 流動比率+営業CF/売上
106
- cr = ratio(ca, cl) # 流動比率
107
- ocf_ms = ratio(ocf, sales)
108
- cashbuf = (ramp(cr, 1.5, 0.7) + ramp(ocf_ms, 0.1, -0.05)) / 2
109
-
110
- # ③資本政策・希薄化リスク: 負債比率+純利益/自己資本
111
- lev = ratio(liab, assets)
112
- roe_proxy = ratio(ni, equity)
113
- cap = (ramp(lev, 0.3, 0.9) + ramp(roe_proxy, 0.10, -0.05)) / 2
114
-
115
- # ④成長余地: 市場成長+現在の収益性レベル
116
- market_g = None
117
- prod_factor = 5.0
118
- if ext_df_like:
119
- market_g = ext_df_like.get("市場の年成長率(%)")
120
- grow_items = ext_df_like.get("成長中主力商品数")
121
- prod_total = ext_df_like.get("主力商品数")
122
- if prod_total:
123
- r = (grow_items or 0)/prod_total
124
- prod_factor = clamp01(10.0*r) # 成長中比率を0-10に
125
 
126
- growth = (ramp(None if market_g is None else market_g/100.0, 0.15, -0.05) + prod_factor)/2
127
-
128
- # ⑤将来リスク: 利益率の低さやレバレッジ上昇をマイナス評価
129
- risk = 10.0 - ((ramp(margin, 0.12, 0.01) + ramp(lev, 0.3, 0.9))/2)
130
-
131
- # 重み(外部評価と差別化)
132
- weights = {
133
- "効率性": 0.25,
134
- "資金繰り耐性": 0.20,
135
- "資本政策・希薄化": 0.20,
136
- "成長余地": 0.20,
137
- "将来リスク": 0.15,
138
- }
139
  cats = {
140
- "効率性": eff, "資金繰り耐性": cashbuf, "資本政策・希薄化": cap,
141
- "成長余地": growth, "将来リスク": risk
 
 
 
142
  }
143
- total_0_10 = sum(cats[k]*w for k,w in weights.items())
144
- total = round(total_0_10*10.0, 1) # 0-100換算
145
-
146
- grade = "S" if total>=85 else "A" if total>=75 else "B" if total>=65 else "C" if total>=50 else "D"
147
-
148
- # 短評(LLM)
149
- memo = ""
150
- try:
151
- cli = _client()
152
- memo_prompt = f"""以下の財務数値(推定含む)を踏まえ、「良い点3つ」「懸念3つ」「総評(100字以内)」を簡潔に日本語で:
153
- [主要比率] 営業利益率={margin or '—'} / 総資産回転率={at or '—'} / 流動比率={cr or '—'}
154
- [市場成長率(%)]={market_g or '—'} / 成長中商品比 ~ {prod_factor*10:.0f}% (概算)
155
- """
156
- resp = cli.chat.completions.create(
157
- model=OPENAI_MODEL_TEXT,
158
- messages=[
159
- {"role": "system", "content": "簡潔・中立な財務アナリスト。数値に即した短評を出力。"},
160
- {"role": "user", "content": memo_prompt},
161
- ],
162
- temperature=0.3,
163
- )
164
- memo = resp.choices[0].message.content.strip()
165
- except Exception:
166
- memo = "短評生成に失敗しました(ネットワーク/鍵設定をご確認ください)。"
167
 
168
  return {
 
 
169
  "ai_total": total,
170
- "grade": grade,
171
- "category_scores": {k: round(v*10.0,1) for k,v in cats.items()},
172
- "weights": weights,
173
- "memo": memo,
174
  }
 
1
  # core/ai_judgement.py
2
  from __future__ import annotations
3
+ from typing import Dict, Any, Optional
4
  import os, json
 
5
  from openai import OpenAI
6
 
7
  OPENAI_MODEL_TEXT = os.environ.get("OPENAI_TEXT_MODEL", "gpt-4o-mini")
8
 
9
+ def _to_f(x):
10
+ if x is None: return None
11
+ try: return float(str(x).replace(",","").replace("▲","-").replace("△","-"))
12
+ except Exception: return None
13
+
14
+ def _safe_div(a,b):
15
+ a=_to_f(a); b=_to_f(b)
16
+ if a is None or b is None or b==0: return None
17
+ return a/b
18
+
19
+ def _clamp01(x):
20
+ if x is None: return 0.5
21
+ return max(0.0, min(1.0, x))
22
+
23
+ def _score01(x, good, bad):
24
+ # good側=1.0, bad側=0.0 の線形
25
+ if x is None: return 0.5
26
+ if good > bad:
27
+ if x <= bad: return 0.0
28
+ if x >= good: return 1.0
29
+ return (x-bad)/(good-bad)
30
+ else:
31
+ if x >= bad: return 0.0
32
+ if x <= good: return 1.0
33
+ return (bad-x)/(bad-good)
34
 
35
  def suggest_external_with_llm(fin: Dict[str, Any], company: str = "") -> Dict[str, Any]:
36
  """
37
+ 外部入力の埋め草候補。LLM不要で作れる範囲はローカルで。
38
+ (例)月商(円)_再掲 を 売上/12 で推定、自己資本比率 を BS から計算 等
39
  """
40
+ out: Dict[str, Any] = {}
41
  try:
42
+ bs = fin.get("balance_sheet", {}) or {}
43
+ is_ = fin.get("income_statement", {}) or {}
44
+ sales = _to_f(is_.get("sales"))
45
+ if sales is not None:
46
+ out["月商(円)_再掲"] = sales / 12.0
47
+ ta = _to_f(bs.get("total_assets")); te = _to_f(bs.get("total_equity"))
48
+ if ta and te is not None and ta>0:
49
+ out["自己資本比率(%)"] = te/ta*100.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  except Exception:
51
+ pass
52
+ return out
53
 
54
+ def ai_evaluate(fin: Dict[str, Any], ext_like: Dict[str, Any]) -> Dict[str, Any]:
55
  """
56
+ LLMに依存せず数式でAI評点を作る(別軸:効率・流動性・資本・成長余地・リスク)。
57
+ 0-100に正規化、カテゴリ平均で合計点。
58
  """
59
  bs = fin.get("balance_sheet", {}) or {}
60
  is_ = fin.get("income_statement", {}) or {}
61
  cf = fin.get("cash_flows", {}) or {}
62
 
 
 
 
 
 
63
  sales = _to_f(is_.get("sales"))
64
  op = _to_f(is_.get("operating_income"))
65
+ net = _to_f(is_.get("net_income"))
 
 
 
66
  ca = _to_f(bs.get("current_assets"))
67
  cl = _to_f(bs.get("current_liabilities"))
68
+ ta = _to_f(bs.get("total_assets"))
69
+ tl = _to_f(bs.get("total_liabilities"))
70
+ te = _to_f(bs.get("total_equity"))
71
  ocf = _to_f(cf.get("operating_cash_flow"))
72
 
73
+ # 1) 効率性(営業利益率・最終利益率)
74
+ opm = _safe_div(op, sales) # 0.15良 / -0.05悪
75
+ npm = _safe_div(net, sales) # 0.10良 / -0.05悪
76
+ eff = 0.6 * _score01(opm, 0.15, -0.05) + 0.4 * _score01(npm, 0.10, -0.05)
77
+
78
+ # 2) 流動性(当座/流動比率+OCF/Sales)
79
+ cur = _safe_div(ca, cl) # 2.0良 / 0.7悪
80
+ ocf_ratio = _safe_div(ocf, sales) # 0.12良 / -0.02悪
81
+ liq = 0.6 * _score01(cur, 2.0, 0.7) + 0.4 * _score01(ocf_ratio, 0.12, -0.02)
82
+
83
+ # 3) 資本(自己資本比率・D/E)
84
+ eq_ratio = _safe_div(te, ta) # 0.40良 / 0.05悪
85
+ de = _safe_div(tl, te) # 0.5良 / 4.0悪(逆評価)
86
+ cap = 0.6 * _score01(eq_ratio, 0.40, 0.05) + 0.4 * _score01(de, 0.5, 4.0)
87
+
88
+ # 4) 成長余地(外部の市場成長&製品の数/成長中比の“軽ウェイト”)
89
+ mg = _to_f(ext_like.get("市場の年成長率(%)"))
90
+ prods = _to_f(ext_like.get("主力商品数")) or 0.0
91
+ prods_g = _to_f(ext_like.get("成長中主力商品数")) or 0.0
92
+ grow = 0.5 * _score01(None if mg is None else mg/100.0, 0.15, -0.05) \
93
+ + 0.25 * _score01(prods, 4, 0) \
94
+ + 0.25 * _score01((prods_g/(prods or 1.0)), 0.7, 0.0)
95
+
96
+ # 5) リスク(赤字/債務過多/OCFマイナス)
97
+ risk = 0.0
98
+ risk += 0.4 * (1.0 - _score01(npm, 0.10, -0.05))
99
+ risk += 0.3 * (1.0 - _score01(de, 0.5, 4.0))
100
+ risk += 0.3 * (1.0 - _score01(ocf_ratio, 0.12, -0.02))
101
+ risk = 1.0 - _clamp01(risk) # 高いほど良い
 
 
 
 
 
 
 
 
 
 
 
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  cats = {
104
+ "効率性": round(eff*100, 1),
105
+ "流動性": round(liq*100, 1),
106
+ "資本": round(cap*100, 1),
107
+ "成長余地": round(grow*100, 1),
108
+ "リスク耐性": round(risk*100, 1),
109
  }
110
+ total = round(sum(cats.values())/len(cats), 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  return {
113
+ "name": "AI評点(数式・中立)",
114
+ "category_scores": cats,
115
  "ai_total": total,
116
+ "notes": "LLM非依存。外部評価とは別軸(効率・流動性・資本・成長余地・リスク)で定量化。",
 
 
 
117
  }