Corin1998 commited on
Commit
0a34695
·
verified ·
1 Parent(s): 92efe44

Create external_scoring.py

Browse files
Files changed (1) hide show
  1. core/external_scoring.py +379 -0
core/external_scoring.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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年)"),
21
+ ("経営者能力", "社外取締役比率(%)"),
22
+ ("経営者能力", "代表者の業界経験年数"),
23
+ ("経営者能力", "現預金(円)"),
24
+ ("経営者能力", "月商(円)"),
25
+ ("経営者能力", "担保余力評価額(円)"),
26
+ ("経営者能力", "倒産歴の有無(TRUE/FALSE)"),
27
+ ("経営者能力", "倒産からの経過年数"),
28
+ ("経営者能力", "重大事件・事故件数(過去10年)"),
29
+ # 成長率
30
+ ("成長率", "売上_期3(最新期)"),
31
+ ("成長率", "売上_期2"),
32
+ ("成長率", "売上_期1(最古期)"),
33
+ ("成長率", "営業利益_期3(最新期)"),
34
+ ("成長率", "営業利益_期2"),
35
+ ("成長率", "営業利益_期1(最古期)"),
36
+ ("成長率", "主力商品数"),
37
+ ("成長率", "成長中主力商品数"),
38
+ # 安定性
39
+ ("安定性", "自己資本比率(%)"),
40
+ ("安定性", "利益剰余金(円)"),
41
+ ("安定性", "支払遅延件数(直近12ヶ月)"),
42
+ ("安定性", "不渡り件数(直近12ヶ月)"),
43
+ ("安定性", "平均支払遅延日数"),
44
+ ("安定性", "メインバンク明確か(TRUE/FALSE)"),
45
+ ("安定性", "借入先数"),
46
+ ("安定性", "メインバンク借入シェア(%)"),
47
+ ("安定性", "コミットメントライン等の長期与信枠あり(TRUE/FALSE)"),
48
+ ("安定性", "担保余力評価額(円)"),
49
+ ("安定性", "月商(円)_再掲"),
50
+ ("安定性", "主要顧客上位1社売上比率(%)"),
51
+ ("安定性", "主要顧客上位3社売上比率(%)"),
52
+ ("安定性", "主要顧客の平均信用スコア(0-100)"),
53
+ ("安定性", "不良債権件数(直近12ヶ月)"),
54
+ ("安定性", "業歴(年)"),
55
+ # 公平性・総合世評
56
+ ("公平性・総合世評", "有価証券報告書提出企業か(TRUE/FALSE)"),
57
+ ("公平性・総合世評", "決算公告や官報での公開あり(TRUE/FALSE)"),
58
+ ("公平性・総合世評", "HP/IRサイトで財務資料公開あり(TRUE/FALSE)"),
59
+ ("公平性・総合世評", "直近更新が定め通りか(TRUE/FALSE)"),
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年)"))
193
+ indep = _to_float(ref("社外取締役比率(%)"))
194
+ exp_years = _to_float(ref("代表者の業界経験年数"))
195
+ cash = _to_float(ref("現預金(円)"))
196
+ sales_m = _to_float(ref("月商(円)"))
197
+ collat = _to_float(ref("担保余力評価額(円)"))
198
+ has_bk = _to_bool(ref("倒産歴の有無(TRUE/FALSE)"))
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)