Corin1998 commited on
Commit
692f002
·
verified ·
1 Parent(s): c37aeda

Update core/external_scoring.py

Browse files
Files changed (1) hide show
  1. core/external_scoring.py +179 -84
core/external_scoring.py CHANGED
@@ -4,7 +4,11 @@ from typing import Dict, Any, List, Tuple
4
  import pandas as pd
5
  import math
6
 
7
- __all__ = ["get_external_template_df", "fill_missing_with_external", "score_external_from_df"]
 
 
 
 
8
 
9
  _TEMPLATE_ROWS: List[Tuple[str, str]] = [
10
  ("経営者能力", "予実達成率_3年平均(%)"),
@@ -27,6 +31,7 @@ _TEMPLATE_ROWS: List[Tuple[str, str]] = [
27
  ("成長率", "営業利益_期1(最古期)"),
28
  ("成長率", "主力商品数"),
29
  ("成長率", "成長中主力商品数"),
 
30
 
31
  ("安定性", "自己資本比率(%)"),
32
  ("安定性", "利益剰余金(円)"),
@@ -52,13 +57,23 @@ _TEMPLATE_ROWS: List[Tuple[str, str]] = [
52
  ]
53
 
54
  def get_external_template_df() -> pd.DataFrame:
55
- return pd.DataFrame([(c, i, "") for c, i in _TEMPLATE_ROWS], columns=["カテゴリー", "入力項目", "値"])
56
-
57
- def fill_missing_with_external(df: pd.DataFrame, company: str = "", country: str = "") -> pd.DataFrame:
58
- # 将来: 外部DBと突合。今はスルー。
59
- return df.copy()
60
-
61
- # ===== スコア計算(堅牢化) =====
 
 
 
 
 
 
 
 
 
 
62
  _WEIGHTS = {
63
  ("経営者能力", "経営姿勢"): 8,
64
  ("経営者能力", "事業経験"): 5,
@@ -68,6 +83,7 @@ _WEIGHTS = {
68
  ("成長率", "売上高伸長性"): 10,
69
  ("成長率", "利益伸長性"): 10,
70
  ("成長率", "商品"): 6,
 
71
 
72
  ("安定性", "自己資本"): 8,
73
  ("安定性", "決済振り"): 10,
@@ -82,46 +98,64 @@ _WEIGHT_NORM = 100.0 / float(sum(_WEIGHTS.values()))
82
 
83
  def _clamp(v, a, b): return max(a, min(b, v))
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def _to_float(x):
86
- if x is None: return None
 
87
  try:
88
  return float(str(x).replace(",", "").replace("▲", "-").replace("△", "-"))
89
  except Exception:
90
  return None
91
 
92
  def _to_bool(x):
93
- if x is None: return None
 
94
  s = str(x).strip().lower()
95
- if s in ("true","t","1","yes","y","有","あり"): return True
96
- if s in ("false","f","0","no","n","無","なし"): return False
 
 
97
  return None
98
 
99
- def _ratio(a,b):
100
- a = _to_float(a); b = _to_float(b)
101
- if a is None or b in (None, 0): return None
102
- try:
103
- return a/b
104
- except Exception:
105
  return None
 
106
 
107
  def _ramp(x, good, bad, lo=0.0, hi=10.0, neutral=None):
108
  if x is None:
109
- return neutral if neutral is not None else (lo+hi)/2.0
110
  if good > bad:
111
  if x <= bad: return lo
112
  if x >= good: return hi
113
- return lo + (hi-lo) * (x-bad)/(good-bad)
114
  else:
115
  if x >= bad: return lo
116
  if x <= good: return hi
117
- return lo + (hi-lo) * (x-good)/(bad-good)
118
 
119
  def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
 
 
 
 
120
  def ref(label: str):
121
- m = df["item"].eq(label) if "item" in df.columns else df["入力項目"].eq(label)
122
- return df.loc[m, "value" if "value" in df.columns else "値"].values[0] if m.any() else None
123
 
124
- items: List[Dict[str, Any]] = []
125
 
126
  yoy3 = _to_float(ref("予実達成率_3年平均(%)"))
127
  audit_bad = _to_float(ref("監査・内部統制の重大な不備 件数(過去3年)"))
@@ -135,95 +169,156 @@ def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
135
  bk_years = _to_float(ref("倒産からの経過年数"))
136
  incidents = _to_float(ref("重大事件・事故件数(過去10年)"))
137
 
138
- s1=_to_float(ref("売上_期1(最古期)")); s2=_to_float(ref("売上_期2")); s3=_to_float(ref("売上_期3(最新期)"))
139
- p1=_to_float(ref("営業利益_期1(最古期)")); p2=_to_float(ref("営業利益_期2")); p3=_to_float(ref("営業利益_期3(最新期)"))
 
 
 
 
140
 
141
  equity = _to_float(ref("自己資本比率(%)"))
142
- delay_cnt=_to_float(ref("支払遅延件数(直近12ヶ月)"))
143
- boun_cnt=_to_float(ref("不渡り件数(直近12ヶ月)"))
144
- delay_days=_to_float(ref("平均支払遅延日数"))
145
- mainbank=_to_bool(ref("メインバンク明確か(TRUE/FALSE)"))
146
- lenders=_to_float(ref("借入先数"))
147
- main_share=_to_float(ref("メインバンク借入シェア(%)"))
148
- has_line=_to_bool(ref("コミットメントライン等の長期与信枠あり(TRUE/FALSE)"))
149
- sales_m2=_to_float(ref("月商(円)_再掲")) or sales_m
150
- top1=_to_float(ref("主要顧客上位1社売上比率(%)"))
151
- cust_score=_to_float(ref("主要顧客の平均信用スコア(0-100)"))
152
- npl_cnt=_to_float(ref("不良債権件数(直近12ヶ月)"))
153
- years=_to_float(ref("業歴()"))
154
- has_sec=_to_bool(ref("有価証券報告書提出企業か(TRUE/FALSE)"))
155
- pub_off=_to_bool(ref("決算公告や官報での公開あり(TRUE/FALSE)"))
156
- pub_web=_to_bool(ref("HP/IRサイトで財務資料公開あり(TRUE/FALSE)"))
157
- upd_on=_to_bool(ref("直近更新が定め通りか(TRUE/FALSE)"))
 
158
 
159
  cash_to_ms = _ratio(cash, sales_m2)
160
  coll_to_ms = _ratio(collat, sales_m2)
161
 
162
  def cagr(v1, v3):
163
- if v1 is None or v3 is None or v1 <= 0: return None
 
164
  try:
165
- return (v3/v1)**(1/2) - 1.0
166
  except Exception:
167
  return None
168
 
169
- s_cagr = cagr(s1, s3); p_cagr = cagr(p1, p3)
170
-
171
- def _add(cat, name, raw, weight, reason):
172
- items.append({
173
- "category": cat,
174
- "name": name,
175
- "raw": round(raw,2) if raw is not None else None,
176
- "weight": round(weight*_WEIGHT_NORM,2),
177
- "score": round(((raw if raw is not None else 5.0)/10.0)*weight*_WEIGHT_NORM,2),
178
- "reason": reason
179
- })
180
 
181
  # 経営者能力
182
- mg_att = (_ramp(yoy3, 90,50)+_ramp(0 if not audit_bad else -audit_bad,0,-3)+_ramp(0 if not comp_bad else -comp_bad,0,-2)+_ramp(indep,33,0))/4
183
- _add("経営者能力", "経営姿勢", mg_att, _WEIGHTS[("経営者能力","経営姿勢")], f"予実{yoy3 or '—'}%/監査{audit_bad or 0}/違反{comp_bad or 0}/社外{indep or '—'}%")
 
 
 
 
 
184
  mg_exp = _ramp(exp_years if exp_years is not None else 5.0, 15, 0)
185
- _add("経営者能力", "事業経験", mg_exp, _WEIGHTS[("経営者能力","事業経験")], f"経験{exp_years if exp_years is not None else '不明→中立'}年")
 
 
186
  mg_asset = _ramp(cash_to_ms, 1.5, 0.2)
187
- _add("経営者能力", "資産担保力", mg_asset, _WEIGHTS[("経営者能力","資産担保力")], f"現預金/月商≈{round(cash_to_ms,2) if cash_to_ms else '—'}")
 
188
 
189
- if incidents and incidents>0:
190
- pen=0.0; rs=f"重大事故{int(incidents)}件→大幅減点"
191
  elif has_bk:
192
- pen=6.0 if (bk_years and bk_years>=10) else 3.0; rs=f"倒産歴あり({bk_years or '不明'}年)"
193
  else:
194
- pen=10.0; rs="事故/倒産なし"
195
- _add("経営者能力","減点事項",pen,_WEIGHTS[("経営者能力","減点事項")],rs)
196
 
197
- # 成長率
198
- _add("成長率","売上高伸長性", _ramp(s_cagr,0.08,-0.05), _WEIGHTS[("成長率","売上高伸長性")], f"CAGR売上{round((s_cagr or 0)*100,1) if s_cagr is not None else '—'}%")
199
- _add("成長率","利益伸長性", _ramp(p_cagr,0.08,-0.05), _WEIGHTS[("成長率","利益伸長性")], f"CAGR営業{round((p_cagr or 0)*100,1) if p_cagr is not None else '—'}%")
200
- _add("成長率","商品", 5.0, _WEIGHTS[("成長率","商品")], "不明→中立")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
  # 安定性
203
- _add("安定性","自己資本", _ramp(equity,40,5), _WEIGHTS[("安定性","自己資本")], f"自己資本比率{equity or '—'}%")
 
 
204
  if (delay_cnt is not None) or (boun_cnt is not None) or (delay_days is not None):
205
- sc=( _ramp(- (delay_cnt or 0),0,-6) + _ramp(- (boun_cnt or 0),0,-1) + _ramp(- (delay_days or 0),0,-30) )/3
206
- rs=f"遅延{int(delay_cnt or 0)}/不渡{int(boun_cnt or 0)}/平均{int(delay_days or 0)}日"
 
 
207
  else:
208
- sc=_ramp(cash_to_ms,1.0,0.2); rs=f"代理:現預金/月商≈{round(cash_to_ms,2) if cash_to_ms else '—'}"
209
- _add("安定性","決済振り", sc, _WEIGHTS[("安定性","決済振り")], rs)
 
210
 
211
  sc_mb = 5.0
212
  sc_mb += 2.0 if mainbank else (-0.5 if mainbank is False else 0)
213
  sc_mb += 1.0 if has_line else 0
214
- sc_mb = _clamp(sc_mb,0,10)
215
- _add("安定性","金融取引", sc_mb, _WEIGHTS[("安定性","金融取引")], f"メイン{'有' if mainbank else '無' if mainbank is False else '—'}/与信枠{'有' if has_line else '無' if has_line is False else '—'}")
 
 
 
 
216
 
217
- _add("安定性","資産担保余力", _ramp(coll_to_ms,4.0,0.0), _WEIGHTS[("安定性","資産担保余力")], f"担保/月商≈{round(coll_to_ms,2) if coll_to_ms else '—'}")
218
- _add("安定性","取引先", ( _ramp(- (top1 or 50),0,-80) + _ramp(cust_score,80,50) + _ramp(- (npl_cnt or 1),0,-3) )/3, _WEIGHTS[("安定性","取引先")], f"上位1社{top1 or '—'}%/信用{cust_score or '—'}/不良{int(npl_cnt or 0)}")
219
- _add("安定性","業歴", _ramp(years,20,1), _WEIGHTS[("安定性","業歴")], f"{years or '—'}年")
 
 
 
 
 
 
220
 
221
  # 公平性
222
  sc_dis = 0.0
223
- sc_dis += 10.0 if has_sec else (7.0 if (pub_off or pub_web) else 4.0)
 
 
 
 
 
224
  if upd_on: sc_dis += 1.0
225
- sc_dis = _clamp(sc_dis,0,10)
226
- _add("公平性・総合世評","ディスクロージャー", sc_dis, _WEIGHTS[("公平性・総合世評","ディスクロージャー")], f"{'有報' if has_sec else '公開あり' if (pub_off or pub_web) else '公開乏しい'} / 更新{'◯' if upd_on else '—'}")
227
-
228
- total = round(sum(x["score"] for x in items),1)
229
- return {"name":"企業評価(外部・定量化)","external_total": total, "items": items, "notes":"欠損は中立、連続スコア×重み(自動正規化)"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import pandas as pd
5
  import math
6
 
7
+ __all__ = [
8
+ "get_external_template_df",
9
+ "fill_missing_with_external",
10
+ "score_external_from_df",
11
+ ]
12
 
13
  _TEMPLATE_ROWS: List[Tuple[str, str]] = [
14
  ("経営者能力", "予実達成率_3年平均(%)"),
 
31
  ("成長率", "営業利益_期1(最古期)"),
32
  ("成長率", "主力商品数"),
33
  ("成長率", "成長中主力商品数"),
34
+ ("成長率", "市場の年成長率(%)"),
35
 
36
  ("安定性", "自己資本比率(%)"),
37
  ("安定性", "利益剰余金(円)"),
 
57
  ]
58
 
59
  def get_external_template_df() -> pd.DataFrame:
60
+ return pd.DataFrame([(c, i, "") for c, i in _TEMPLATE_ROWS],
61
+ columns=["カテゴリー", "入力項目", "値"])
62
+
63
+ def fill_missing_with_external(df: pd.DataFrame, suggestions: Dict[str, Any] | None = None) -> pd.DataFrame:
64
+ """
65
+ LLM等からの suggestions を {入力項目: 値} で受け取り、空欄のみ埋める
66
+ """
67
+ if not suggestions:
68
+ return df.copy()
69
+ df2 = df.copy()
70
+ for idx, row in df2.iterrows():
71
+ key = row["入力項目"]
72
+ if (row["値"] in (None, "", "—")) and (key in suggestions):
73
+ df2.at[idx, "値"] = suggestions[key]
74
+ return df2
75
+
76
+ # ===== スコア計算(定量的・ルールベース) =====
77
  _WEIGHTS = {
78
  ("経営者能力", "経営姿勢"): 8,
79
  ("経営者能力", "事業経験"): 5,
 
83
  ("成長率", "売上高伸長性"): 10,
84
  ("成長率", "利益伸長性"): 10,
85
  ("成長率", "商品"): 6,
86
+ ("成長率", "市場成長調整"): 6, # ★ 追加:市場成長率を反映
87
 
88
  ("安定性", "自己資本"): 8,
89
  ("安定性", "決済振り"): 10,
 
98
 
99
  def _clamp(v, a, b): return max(a, min(b, v))
100
 
101
+ def _add(items, cat, name, raw, weight, reason):
102
+ raw_s = None if raw is None else round(raw, 2)
103
+ w = round(weight * _WEIGHT_NORM, 2)
104
+ sc = 0.0 if raw is None else round((raw / 10.0) * w, 2)
105
+ items.append({
106
+ "category": cat,
107
+ "name": name,
108
+ "raw": raw_s,
109
+ "weight": w,
110
+ "score": sc,
111
+ "reason": reason
112
+ })
113
+
114
  def _to_float(x):
115
+ if x is None:
116
+ return None
117
  try:
118
  return float(str(x).replace(",", "").replace("▲", "-").replace("△", "-"))
119
  except Exception:
120
  return None
121
 
122
  def _to_bool(x):
123
+ if x is None:
124
+ return None
125
  s = str(x).strip().lower()
126
+ if s in ("true", "t", "1", "yes", "y", "有", "あり"):
127
+ return True
128
+ if s in ("false", "f", "0", "no", "n", "無", "なし"):
129
+ return False
130
  return None
131
 
132
+ def _ratio(a, b):
133
+ if a is None or b is None or b == 0:
 
 
 
 
134
  return None
135
+ return a / b
136
 
137
  def _ramp(x, good, bad, lo=0.0, hi=10.0, neutral=None):
138
  if x is None:
139
+ return neutral if neutral is not None else (lo + hi) / 2.0
140
  if good > bad:
141
  if x <= bad: return lo
142
  if x >= good: return hi
143
+ return lo + (hi - lo) * (x - bad) / (good - bad)
144
  else:
145
  if x >= bad: return lo
146
  if x <= good: return hi
147
+ return lo + (hi - lo) * (x - good) / (bad - good)
148
 
149
  def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
150
+ """
151
+ 入力DF(カテゴリー/入力項目/値)を定量スコア化。
152
+ 欠損は中立。市場の年成長率(%) を「市場成長調整」に反映し、成長率評価の過剰/過小を補正。
153
+ """
154
  def ref(label: str):
155
+ m = df["入力項目"].eq(label)
156
+ return df.loc[m, "値"].values[0] if m.any() else None
157
 
158
+ items = []
159
 
160
  yoy3 = _to_float(ref("予実達成率_3年平均(%)"))
161
  audit_bad = _to_float(ref("監査・内部統制の重大な不備 件数(過去3年)"))
 
169
  bk_years = _to_float(ref("倒産からの経過年数"))
170
  incidents = _to_float(ref("重大事件・事故件数(過去10年)"))
171
 
172
+ s1 = _to_float(ref("売上_期1(最古期)"))
173
+ s2 = _to_float(ref("売上_期2"))
174
+ s3 = _to_float(ref("売上_期3(最新期)"))
175
+ p1 = _to_float(ref("営業利益_期1(最古期)"))
176
+ p2 = _to_float(ref("営業利益_期2"))
177
+ p3 = _to_float(ref("営業利益_期3(最新期)"))
178
 
179
  equity = _to_float(ref("自己資本比率(%)"))
180
+ delay_cnt = _to_float(ref("支払遅延件数(直近12ヶ月)"))
181
+ boun_cnt = _to_float(ref("不渡り件数(直近12ヶ月)"))
182
+ delay_days = _to_float(ref("平均支払遅延日数"))
183
+ mainbank = _to_bool(ref("メインバンク明確か(TRUE/FALSE)"))
184
+ lenders = _to_float(ref("借入先数"))
185
+ main_share = _to_float(ref("メインバンク借入シェア(%)"))
186
+ has_line = _to_bool(ref("コミットメントライン等の長期与信枠あり(TRUE/FALSE)"))
187
+ sales_m2 = _to_float(ref("月商(円)_再掲")) or sales_m
188
+ top1 = _to_float(ref("主要顧客上位1社売上比率(%)"))
189
+ top3 = _to_float(ref("主要顧客上位3社売上比率(%)"))
190
+ cust_score = _to_float(ref("主要顧客の平均信用スコア(0-100)"))
191
+ npl_cnt = _to_float(ref("不良債権件数(直近12ヶ月)"))
192
+ years = _to_float(ref("業歴()"))
193
+
194
+ prod_total = _to_float(ref("主力商品数"))
195
+ prod_growing = _to_float(ref("成長中主力商品数"))
196
+ market_growth = _to_float(ref("市場の年成長率(%)"))
197
 
198
  cash_to_ms = _ratio(cash, sales_m2)
199
  coll_to_ms = _ratio(collat, sales_m2)
200
 
201
  def cagr(v1, v3):
202
+ if v1 is None or v3 is None or v1 <= 0:
203
+ return None
204
  try:
205
+ return (v3 / v1) ** (1 / 2) - 1.0
206
  except Exception:
207
  return None
208
 
209
+ s_cagr = cagr(s1, s3)
210
+ p_cagr = cagr(p1, p3)
 
 
 
 
 
 
 
 
 
211
 
212
  # 経営者能力
213
+ mg_att = (_ramp(yoy3, 90, 50) +
214
+ _ramp(0 if not audit_bad else -audit_bad, 0, -3) +
215
+ _ramp(0 if not comp_bad else -comp_bad, 0, -2) +
216
+ _ramp(indep, 33, 0)) / 4
217
+ _add(items, "経営者能力", "経営姿勢", mg_att, _WEIGHTS[("経営者能力", "経営姿勢")],
218
+ f"予実{yoy3 or '—'}%/監査{audit_bad or 0}/違反{comp_bad or 0}/社外{indep or '—'}%")
219
+
220
  mg_exp = _ramp(exp_years if exp_years is not None else 5.0, 15, 0)
221
+ _add(items, "経営者能力", "事業経験", mg_exp, _WEIGHTS[("経営者能力", "事業経験")],
222
+ f"経験{exp_years if exp_years is not None else '不明→中立'}年")
223
+
224
  mg_asset = _ramp(cash_to_ms, 1.5, 0.2)
225
+ _add(items, "経営者能力", "資産担保力", mg_asset, _WEIGHTS[("経営者能力", "資産担保力")],
226
+ f"現預金/月商≈{round(cash_to_ms, 2) if cash_to_ms else '—'}")
227
 
228
+ if incidents and incidents > 0:
229
+ pen = 0.0; rs = f"重大事故{int(incidents)}件→大幅減点"
230
  elif has_bk:
231
+ pen = 6.0 if (bk_years and bk_years >= 10) else 3.0; rs = f"倒産歴あり({bk_years or '不明'}年)"
232
  else:
233
+ pen = 10.0; rs = "事故/倒産なし"
234
+ _add(items, "経営者能力", "減点事項", pen, _WEIGHTS[("経営者能力", "減点事項")], rs)
235
 
236
+ # 成長率(市場成長で調整)
237
+ _add(items, "成長率", "売上高伸長性", _ramp(s_cagr, 0.08, -0.05),
238
+ _WEIGHTS[("成長率", "売上高伸長性")],
239
+ f"CAGR売上{round((s_cagr or 0)*100,1) if s_cagr is not None else '—'}%")
240
+
241
+ _add(items, "成長率", "利益伸長性", _ramp(p_cagr, 0.08, -0.05),
242
+ _WEIGHTS[("成長率", "利益伸長性")],
243
+ f"CAGR営業{round((p_cagr or 0)*100,1) if p_cagr is not None else '—'}%")
244
+
245
+ # 商品スコア:総数と成長中の比率で
246
+ if prod_total is None or prod_total <= 0:
247
+ pr_sc = 5.0; rs = "不明→中立"
248
+ else:
249
+ ratio = _ratio(prod_growing, prod_total) or 0.0
250
+ pr_sc = ( _ramp(prod_total, 3, 0) + _ramp(ratio, 0.7, 0.1) ) / 2
251
+ rs = f"主力{int(prod_total)}/成長中比{round(ratio*100,1)}%"
252
+ _add(items, "成長率", "商品", pr_sc, _WEIGHTS[("成長率", "商品")], rs)
253
+
254
+ # 市場成長調整:市場 >10% なら高評価、マイナス成長は減点
255
+ _add(items, "成長率", "市場成長調整",
256
+ _ramp(market_growth, 15, -5),
257
+ _WEIGHTS[("成長率", "市場成長調整")],
258
+ f"市場年成長{market_growth or '—'}%")
259
 
260
  # 安定性
261
+ _add(items, "安定性", "自己資本", _ramp(equity, 40, 5),
262
+ _WEIGHTS[("安定性", "自己資本")], f"自己資本比率{equity or '—'}%")
263
+
264
  if (delay_cnt is not None) or (boun_cnt is not None) or (delay_days is not None):
265
+ sc = ( _ramp(-(delay_cnt or 0), 0, -6) +
266
+ _ramp(-(boun_cnt or 0), 0, -1) +
267
+ _ramp(-(delay_days or 0), 0, -30) ) / 3
268
+ rs = f"遅延{int(delay_cnt or 0)}/不渡{int(boun_cnt or 0)}/平均{int(delay_days or 0)}日"
269
  else:
270
+ sc = _ramp(cash_to_ms, 1.0, 0.2)
271
+ rs = f"代理:現預金/月商≈{round(cash_to_ms,2) if cash_to_ms else '—'}"
272
+ _add(items, "安定性", "決済振り", sc, _WEIGHTS[("安定性", "決済振り")], rs)
273
 
274
  sc_mb = 5.0
275
  sc_mb += 2.0 if mainbank else (-0.5 if mainbank is False else 0)
276
  sc_mb += 1.0 if has_line else 0
277
+ sc_mb = _clamp(sc_mb, 0, 10)
278
+ _add(items, "安定性", "金融取引", sc_mb, _WEIGHTS[("安定性", "金融取引")],
279
+ f"メイン{'有' if mainbank else '無' if mainbank is False else '—'}/与信枠{'有' if has_line else '無' if has_line is False else '—'}")
280
+
281
+ _add(items, "安定性", "資産担保余力", _ramp(coll_to_ms, 4.0, 0.0),
282
+ _WEIGHTS[("安定性", "資産担保余力")], f"担保/月商≈{round(coll_to_ms,2) if coll_to_ms else '—'}")
283
 
284
+ _add(items, "安定性", "取引先",
285
+ ( _ramp(-(top1 or 50), 0, -80) +
286
+ _ramp(cust_score, 80, 50) +
287
+ _ramp(-(npl_cnt or 1), 0, -3) ) / 3,
288
+ _WEIGHTS[("安定性", "取引先")],
289
+ f"上位1社{top1 or '—'}%/信用{cust_score or '—'}/不良{int(npl_cnt or 0)}")
290
+
291
+ _add(items, "安定性", "業歴", _ramp(years, 20, 1),
292
+ _WEIGHTS[("安定性", "業歴")], f"{years or '—'}年")
293
 
294
  # 公平性
295
  sc_dis = 0.0
296
+ sc_dis += 10.0 if has_sec := _to_bool(ref("有価証券報告書提出企業か(TRUE/FALSE)")) else 0.0
297
+ if sc_dis == 0.0:
298
+ pub_off = _to_bool(ref("決算公告や官報での公開あり(TRUE/FALSE)"))
299
+ pub_web = _to_bool(ref("HP/IRサイトで財務資料公開あり(TRUE/FALSE)"))
300
+ sc_dis += 7.0 if (pub_off or pub_web) else 4.0
301
+ upd_on = _to_bool(ref("直近更新が定め通りか(TRUE/FALSE)"))
302
  if upd_on: sc_dis += 1.0
303
+ sc_dis = _clamp(sc_dis, 0, 10)
304
+ _add(items, "公平性・総合世評", "ディスクロージャー", sc_dis,
305
+ _WEIGHTS[("公平性・総合世評", "ディスクロージャー")],
306
+ f"{'有報' if has_sec else '公開あり' if sc_dis>=7.0 else '公開乏しい'} / 更新{'◯' if upd_on else '—'}")
307
+
308
+ total = round(sum(x["score"] for x in items), 1)
309
+
310
+ # レーダ用にカテゴリ集計(重み付き平均→0-100)
311
+ from collections import defaultdict
312
+ cat_sum, cat_w = defaultdict(float), defaultdict(float)
313
+ for it in items:
314
+ cat_sum[it["category"]] += it["score"]
315
+ cat_w[it["category"]] += it["weight"]
316
+ cat_scores = {c: round((cat_sum[c] / cat_w[c]) * 100.0 if cat_w[c] > 0 else 0.0, 1) for c in cat_sum}
317
+
318
+ return {
319
+ "name": "企業評価(外部・定量)",
320
+ "external_total": total,
321
+ "items": items,
322
+ "category_scores": cat_scores,
323
+ "notes": "欠損は中立。市場成長率を成長評価に加味(過熱/低迷の補正)。",
324
+ }