Corin1998 commited on
Commit
eb687c0
·
verified ·
1 Parent(s): cbdf486

Update core/external_scoring.py

Browse files
Files changed (1) hide show
  1. core/external_scoring.py +102 -252
core/external_scoring.py CHANGED
@@ -1,20 +1,12 @@
1
  # core/external_scoring.py
2
  from __future__ import annotations
3
- from typing import Dict, Any, List, Tuple, Optional
4
  import pandas as pd
5
  import math
6
- import re
7
 
8
- __all__ = [
9
- "get_external_template_df",
10
- "fill_missing_with_external",
11
- "score_external_from_df",
12
- "score_external", # UI からはこれを呼べばOK(薄いラッパー)
13
- ]
14
 
15
- # ===== 入力テンプレ(外部評価で UI から埋める想定) =====
16
  _TEMPLATE_ROWS: List[Tuple[str, str]] = [
17
- # 経営者能力
18
  ("経営者能力", "予実達成率_3年平均(%)"),
19
  ("経営者能力", "監査・内部統制の重大な不備 件数(過去3年)"),
20
  ("経営者能力", "重大コンプライアンス件数(過去3年)"),
@@ -26,7 +18,7 @@ _TEMPLATE_ROWS: List[Tuple[str, str]] = [
26
  ("経営者能力", "倒産歴の有無(TRUE/FALSE)"),
27
  ("経営者能力", "倒産からの経過年数"),
28
  ("経営者能力", "重大事件・事故件数(過去10年)"),
29
- # 成長率
30
  ("成長率", "売上_期3(最新期)"),
31
  ("成長率", "売上_期2"),
32
  ("成長率", "売上_期1(最古期)"),
@@ -35,7 +27,7 @@ _TEMPLATE_ROWS: List[Tuple[str, str]] = [
35
  ("成長率", "営業利益_期1(最古期)"),
36
  ("成長率", "主力商品数"),
37
  ("成長率", "成長中主力商品数"),
38
- # 安定性
39
  ("安定性", "自己資本比率(%)"),
40
  ("安定性", "利益剰余金(円)"),
41
  ("安定性", "支払遅延件数(直近12ヶ月)"),
@@ -52,7 +44,7 @@ _TEMPLATE_ROWS: List[Tuple[str, str]] = [
52
  ("安定性", "主要顧客の平均信用スコア(0-100)"),
53
  ("安定性", "不良債権件数(直近12ヶ月)"),
54
  ("安定性", "業歴(年)"),
55
- # 公平性・総合世評
56
  ("公平性・総合世評", "有価証券報告書提出企業か(TRUE/FALSE)"),
57
  ("公平性・総合世評", "決算公告や官報での公開あり(TRUE/FALSE)"),
58
  ("公平性・総合世評", "HP/IRサイトで財務資料公開あり(TRUE/FALSE)"),
@@ -60,133 +52,77 @@ _TEMPLATE_ROWS: List[Tuple[str, str]] = [
60
  ]
61
 
62
  def get_external_template_df() -> pd.DataFrame:
63
- """UI 側で空の雛形を出すときに利用"""
64
- return pd.DataFrame([(c, i, "") for c, i in _TEMPLATE_ROWS],
65
- columns=["カテゴリー", "入力項目", "値"])
66
 
67
  def fill_missing_with_external(df: pd.DataFrame, company: str = "", country: str = "") -> pd.DataFrame:
68
- """
69
- 将来:外部DBやLLMで不足値を補完する場所。
70
- いまは何もしないでそのまま返す。
71
- """
72
  return df.copy()
73
 
74
- # ===== スコア計算(定量 & ユニット頑健化) =====
75
-
76
  _WEIGHTS = {
77
- # 経営者能力
78
  ("経営者能力", "経営姿勢"): 8,
79
  ("経営者能力", "事業経験"): 5,
80
  ("経営者能力", "資産担保力"): 6,
81
  ("経営者能力", "減点事項"): 7,
82
- # 成長率
83
  ("成長率", "売上高伸長性"): 10,
84
  ("成長率", "利益伸長性"): 10,
85
  ("成長率", "商品"): 6,
86
- # 安定性
87
  ("安定性", "自己資本"): 8,
88
  ("安定性", "決済振り"): 10,
89
  ("安定性", "金融取引"): 6,
90
  ("安定性", "資産担保余力"): 6,
91
  ("安定性", "取引先"): 6,
92
  ("安定性", "業歴"): 4,
93
- # 公平性
94
  ("公平性・総合世評", "ディスクロージャー"): 8,
95
  }
96
  _WEIGHT_NORM = 100.0 / float(sum(_WEIGHTS.values()))
97
 
98
- def _clamp(v: float, a: float, b: float) -> float:
99
- return max(a, min(b, v))
100
-
101
- def _add(items: List[Dict[str, Any]], cat: str, name: str,
102
- raw: float, weight: float, reason: str):
103
- items.append({
104
- "category": cat,
105
- "name": name,
106
- "raw": None if raw is None else round(raw, 2),
107
- "weight": round(weight * _WEIGHT_NORM, 2),
108
- "score": 0.0 if raw is None else round((raw / 10.0) * weight * _WEIGHT_NORM, 2),
109
- "reason": reason
110
- })
111
-
112
- # ---- 数値パーサ(日本語単位に強い) ----
113
- _UNIT = {"兆": 1e12, "億": 1e8, "万": 1e4}
114
- def _to_float(x) -> Optional[float]:
115
- if x is None:
116
- return None
117
- s = str(x).strip()
118
- if s == "":
119
- return None
120
 
121
- # ▲, △ は負号扱い
122
- sign = -1 if ("▲" in s or s.startswith("-")) else 1
123
-
124
- # 兆/億/万/千 の単位
125
- mul = 1.0
126
- for k, v in _UNIT.items():
127
- if k in s:
128
- mul *= v
129
- # 「千円」「3千万円」等
130
- if "千" in s:
131
- mul *= 1e3
132
-
133
- # 数字のみ抽出
134
- s_num = re.sub(r"[^\d\.]", "", s)
135
- if not s_num:
136
- return None
137
  try:
138
- return sign * float(s_num) * mul
139
  except Exception:
140
- try:
141
- return sign * float(s_num)
142
- except Exception:
143
- return None
144
-
145
- def _to_bool(x) -> Optional[bool]:
146
- if x is None:
147
  return None
 
 
 
148
  s = str(x).strip().lower()
149
- if s in ("true", "t", "1", "yes", "y", "有", "あり", "○", "◯"):
150
- return True
151
- if s in ("false", "f", "0", "no", "n", "無", "なし", "×"):
152
- return False
153
  return None
154
 
155
- def _ratio(a: Optional[float], b: Optional[float]) -> Optional[float]:
156
- if a is None or b is None or b == 0:
 
 
 
 
157
  return None
158
- return a / b
159
-
160
- def _ramp(x: Optional[float], good: float, bad: float,
161
- lo: float = 0.0, hi: float = 10.0, neutral: Optional[float] = None) -> float:
162
- """
163
- x が good 側に近いほど高得点(10)、bad 側ほど低得点(0)。
164
- 欠損は neutral(指定なければ 5)。
165
- """
166
  if x is None:
167
- return neutral if neutral is not None else (lo + hi) / 2.0
168
  if good > bad:
169
  if x <= bad: return lo
170
  if x >= good: return hi
171
- return lo + (hi - lo) * (x - bad) / (good - bad)
172
  else:
173
  if x >= bad: return lo
174
  if x <= good: return hi
175
- return lo + (hi - lo) * (x - good) / (bad - good)
176
 
177
- # ===== メイン:DataFrame からスコア作成 =====
178
  def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
179
- """
180
- df: カラム ["カテゴリー","入力項目","値"] を前提。
181
- 値は '億', '万', '千円', '▲' などを含んでもOK(自動正規化)。
182
- """
183
  def ref(label: str):
184
- m = df["入力項目"].eq(label)
185
- return df.loc[m, "値"].values[0] if m.any() else None
186
 
187
  items: List[Dict[str, Any]] = []
188
 
189
- # ---------- 経営者能力 ----------
190
  yoy3 = _to_float(ref("予実達成率_3年平均(%)"))
191
  audit_bad = _to_float(ref("監査・内部統制の重大な不備 件数(過去3年)"))
192
  comp_bad = _to_float(ref("重大コンプライアンス件数(過去3年)"))
@@ -199,181 +135,95 @@ def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
199
  bk_years = _to_float(ref("倒産からの経過年数"))
200
  incidents = _to_float(ref("重大事件・事故件数(過去10年)"))
201
 
202
- # ---------- 成長率 ----------
203
- s1 = _to_float(ref("売上_期1(最古期)"))
204
- s2 = _to_float(ref("売上_期2"))
205
- s3 = _to_float(ref("売上_期3(最新期)"))
206
- p1 = _to_float(ref("営業利益_期1(最古期)"))
207
- p2 = _to_float(ref("営業利益_期2"))
208
- p3 = _to_float(ref("営業利益_期3(最新期)"))
209
- prod_all = _to_float(ref("主力商品数"))
210
- prod_grow = _to_float(ref("成長中主力商品数"))
211
-
212
- # ---------- 安定性 ----------
213
  equity = _to_float(ref("自己資本比率(%)"))
214
- delay_cnt = _to_float(ref("支払遅延件数(直近12ヶ月)"))
215
- boun_cnt = _to_float(ref("不渡り件数(直近12ヶ月)"))
216
- delay_days = _to_float(ref("平均支払遅延日数"))
217
- mainbank = _to_bool(ref("メインバンク明確か(TRUE/FALSE)"))
218
- lenders = _to_float(ref("借入先数"))
219
- main_share = _to_float(ref("メインバンク借入シェア(%)"))
220
- has_line = _to_bool(ref("コミットメントライン等の長期与信枠あり(TRUE/FALSE)"))
221
- sales_m2 = _to_float(ref("月商(円)_再掲")) or sales_m
222
- top1 = _to_float(ref("主要顧客上位1社売上比率(%)"))
223
- top3 = _to_float(ref("主要顧客上位3社売上比率(%)"))
224
- cust_score = _to_float(ref("主要顧客の平均信用スコア(0-100)"))
225
- npl_cnt = _to_float(ref("不良債権件数(直近12ヶ月)"))
226
- years = _to_float(ref("業()"))
227
-
228
- # ---------- 平性 ----------
229
- has_sec = _to_bool(ref("有価証券報告書提出企業か(TRUE/FALSE)"))
230
- pub_off = _to_bool(ref("決算公告や官報での公開あり(TRUE/FALSE)"))
231
- pub_web = _to_bool(ref("HP/IRサイトで財務資料公開あり(TRUE/FALSE)"))
232
- upd_on = _to_bool(ref("直近更新が定め通りか(TRUE/FALSE)"))
233
-
234
- # 比率
235
  cash_to_ms = _ratio(cash, sales_m2)
236
  coll_to_ms = _ratio(collat, sales_m2)
237
 
238
- def cagr(v1: Optional[float], v3: Optional[float]) -> Optional[float]:
239
- if v1 is None or v3 is None or v1 <= 0:
240
- return None
241
  try:
242
- return (v3 / v1) ** (1 / 2) - 1.0
243
  except Exception:
244
  return None
245
 
246
- s_cagr = cagr(s1, s3)
247
- p_cagr = cagr(p1, p3)
248
 
249
- # --- 経営者能力 ---
250
- mg_att = (
251
- _ramp(yoy3, 90, 50) +
252
- _ramp(0 if not audit_bad else -audit_bad, 0, -3) +
253
- _ramp(0 if not comp_bad else -comp_bad, 0, -2) +
254
- _ramp(indep, 33, 0)
255
- ) / 4
256
- _add(items, "経営者能力", "経営姿勢", mg_att,
257
- _WEIGHTS[("経営者能力", "経営姿勢")],
258
- f"予実{yoy3 or '—'}%/監査{int(audit_bad or 0)}/違反{int(comp_bad or 0)}/社外{indep or '—'}%")
259
 
 
 
 
260
  mg_exp = _ramp(exp_years if exp_years is not None else 5.0, 15, 0)
261
- _add(items, "経営者能力", "事業経験", mg_exp,
262
- _WEIGHTS[("経営者能力", "事業経験")],
263
- f"経験{exp_years if exp_years is not None else '不明→中立'}年")
264
-
265
  mg_asset = _ramp(cash_to_ms, 1.5, 0.2)
266
- _add(items, "経営者能力", "資産担保力", mg_asset,
267
- _WEIGHTS[("経営者能力", "資産担保力")],
268
- f"現預金/月商≈{round(cash_to_ms, 2) if cash_to_ms else '—'}")
269
 
270
- if incidents and incidents > 0:
271
- pen = 0.0; rs = f"重大事故{int(incidents)}件→大幅減点"
272
  elif has_bk:
273
- pen = 6.0 if (bk_years and bk_years >= 10) else 3.0
274
- rs = f"倒産歴あり({bk_years or '不明'}年)"
275
  else:
276
- pen = 10.0; rs = "事故/倒産なし"
277
- _add(items, "経営者能力", "減点事項", pen,
278
- _WEIGHTS[("経営者能力", "減点事項")], rs)
279
-
280
- # --- 成長率 ---
281
- _add(items, "成長率", "売上高伸長性",
282
- _ramp(s_cagr, 0.08, -0.05),
283
- _WEIGHTS[("成長率", "売上高伸長性")],
284
- f"CAGR売上{round((s_cagr or 0)*100,1) if s_cagr is not None else '—'}%")
285
-
286
- _add(items, "成長率", "利益伸長性",
287
- _ramp(p_cagr, 0.08, -0.05),
288
- _WEIGHTS[("成長率", "利益伸長性")],
289
- f"CAGR営業{round((p_cagr or 0)*100,1) if p_cagr is not None else '—'}%")
290
-
291
- # 成長中/全体の比率(0〜1)→ スコアへ線形変換
292
- prod_ratio = None
293
- if prod_all and prod_all > 0 and prod_grow is not None:
294
- prod_ratio = max(0.0, min(1.0, prod_grow / prod_all))
295
- prod_score = None if prod_ratio is None else 10.0 * prod_ratio
296
- _add(items, "成長率", "商品",
297
- 5.0 if prod_score is None else prod_score,
298
- _WEIGHTS[("成長率", "商品")],
299
- f"成長中/主力 ≈ {round(prod_ratio,2) if prod_ratio is not None else '—'}")
300
-
301
- # --- 安定性 ---
302
- _add(items, "安定性", "自己資本",
303
- _ramp(equity, 40, 5),
304
- _WEIGHTS[("安定性", "自己資本")],
305
- f"自己資本比率{equity or '—'}%")
306
 
 
 
 
 
 
 
 
307
  if (delay_cnt is not None) or (boun_cnt is not None) or (delay_days is not None):
308
- sc = (
309
- _ramp(- (delay_cnt or 0), 0, -6) +
310
- _ramp(- (boun_cnt or 0), 0, -1) +
311
- _ramp(- (delay_days or 0), 0, -30)
312
- ) / 3
313
- rs = f"遅延{int(delay_cnt or 0)}/不渡{int(boun_cnt or 0)}/平均{int(delay_days or 0)}日"
314
  else:
315
- sc = _ramp(cash_to_ms, 1.0, 0.2)
316
- rs = f"代理:現預金/月商≈{round(cash_to_ms,2) if cash_to_ms else '—'}"
317
- _add(items, "安定性", "決済振り",
318
- sc, _WEIGHTS[("安定性", "決済振り")], rs)
319
 
320
  sc_mb = 5.0
321
  sc_mb += 2.0 if mainbank else (-0.5 if mainbank is False else 0)
322
- sc_mb += 1.0 if has_line else 0.0
323
- sc_mb = _clamp(sc_mb, 0, 10)
324
- _add(items, "安定性", "金融取引",
325
- sc_mb, _WEIGHTS[("安定性", "金融取引")],
326
- f"メイン{'有' if mainbank else '無' if mainbank is False else '—'}/与信枠{'有' if has_line else '無' if has_line is False else '—'}")
327
-
328
- _add(items, "安定性", "資産担保余力",
329
- _ramp(coll_to_ms, 4.0, 0.0),
330
- _WEIGHTS[("安定性", "資産担保余力")],
331
- f"担保/月商≈{round(coll_to_ms,2) if coll_to_ms else '—'}")
332
-
333
- _add(items, "安定性", "取引先",
334
- ( _ramp(- (top1 or 50), 0, -80) +
335
- _ramp(cust_score, 80, 50) +
336
- _ramp(- (npl_cnt or 1), 0, -3) ) / 3,
337
- _WEIGHTS[("安定性", "取引先")],
338
- f"上位1社{top1 or '—'}%/信用{cust_score or '—'}/不良{int(npl_cnt or 0)}")
339
-
340
- _add(items, "安定性", "業歴",
341
- _ramp(years, 20, 1),
342
- _WEIGHTS[("安定性", "業歴")],
343
- f"{years or '—'}年")
344
-
345
- # --- 公平性・総合世評 ---
346
  sc_dis = 0.0
347
  sc_dis += 10.0 if has_sec else (7.0 if (pub_off or pub_web) else 4.0)
348
- if upd_on:
349
- sc_dis += 1.0
350
- sc_dis = _clamp(sc_dis, 0, 10)
351
- _add(items, "公平性・総合世評", "ディスクロージャー",
352
- sc_dis, _WEIGHTS[("公平性・総合世評", "ディスクロージャー")],
353
- f"{'有報' if has_sec else '公開あり' if (pub_off or pub_web) else '公開乏しい'} / 更新{'◯' if upd_on else '—'}")
354
-
355
- total = round(sum(x["score"] for x in items), 1)
356
- return {
357
- "name": "企業評価(外部)",
358
- "external_total": total,
359
- "items": items,
360
- "notes": "欠損は中立、連続スコア×重み(自動正規化)/日本語単位を自動解釈"
361
- }
362
-
363
- # ===== ラッパー:UI から呼びやすい形 =====
364
- def score_external(fin: Dict[str, Any] | None = None,
365
- external_df: Optional[pd.DataFrame] = None,
366
- company: str = "",
367
- country: str = "") -> Dict[str, Any]:
368
- """
369
- UI 側では基本この関数を呼ぶ想定。
370
- - `external_df` が未指定ならテンプレを自動生成して中立値扱いで採点(ばらつきは小さくなる)
371
- - 値が入った DataFrame を渡せば、上の `score_external_from_df` で定量スコア化
372
- """
373
- if external_df is None or external_df.empty:
374
- tmpl = get_external_template_df()
375
- filled = fill_missing_with_external(tmpl, company=company, country=country)
376
- return score_external_from_df(filled)
377
- else:
378
- filled = fill_missing_with_external(external_df, company=company, country=country)
379
- return score_external_from_df(filled)
 
1
  # core/external_scoring.py
2
  from __future__ import annotations
3
+ 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年平均(%)"),
11
  ("経営者能力", "監査・内部統制の重大な不備 件数(過去3年)"),
12
  ("経営者能力", "重大コンプライアンス件数(過去3年)"),
 
18
  ("経営者能力", "倒産歴の有無(TRUE/FALSE)"),
19
  ("経営者能力", "倒産からの経過年数"),
20
  ("経営者能力", "重大事件・事故件数(過去10年)"),
21
+
22
  ("成長率", "売上_期3(最新期)"),
23
  ("成長率", "売上_期2"),
24
  ("成長率", "売上_期1(最古期)"),
 
27
  ("成長率", "営業利益_期1(最古期)"),
28
  ("成長率", "主力商品数"),
29
  ("成長率", "成長中主力商品数"),
30
+
31
  ("安定性", "自己資本比率(%)"),
32
  ("安定性", "利益剰余金(円)"),
33
  ("安定性", "支払遅延件数(直近12ヶ月)"),
 
44
  ("安定性", "主要顧客の平均信用スコア(0-100)"),
45
  ("安定性", "不良債権件数(直近12ヶ月)"),
46
  ("安定性", "業歴(年)"),
47
+
48
  ("公平性・総合世評", "有価証券報告書提出企業か(TRUE/FALSE)"),
49
  ("公平性・総合世評", "決算公告や官報での公開あり(TRUE/FALSE)"),
50
  ("公平性・総合世評", "HP/IRサイトで財務資料公開あり(TRUE/FALSE)"),
 
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,
65
  ("経営者能力", "資産担保力"): 6,
66
  ("経営者能力", "減点事項"): 7,
67
+
68
  ("成長率", "売上高伸長性"): 10,
69
  ("成長率", "利益伸長性"): 10,
70
  ("成長率", "商品"): 6,
71
+
72
  ("安定性", "自己資本"): 8,
73
  ("安定性", "決済振り"): 10,
74
  ("安定性", "金融取引"): 6,
75
  ("安定性", "資産担保余力"): 6,
76
  ("安定性", "取引先"): 6,
77
  ("安定性", "業歴"): 4,
78
+
79
  ("公平性・総合世評", "ディスクロージャー"): 8,
80
  }
81
  _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年)"))
128
  comp_bad = _to_float(ref("重大コンプライアンス件数(過去3年)"))
 
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":"欠損は中立、連続スコア×重み(自動正規化)"}