Update core/external_scoring.py
Browse files- core/external_scoring.py +25 -21
core/external_scoring.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
from __future__ import annotations
|
| 3 |
from typing import Dict, Any, List, Tuple
|
| 4 |
import pandas as pd
|
| 5 |
-
import math
|
| 6 |
|
| 7 |
__all__ = [
|
| 8 |
"get_external_template_df",
|
|
@@ -71,7 +70,7 @@ def fill_missing_with_external(df: pd.DataFrame, suggestions: Dict[str, Any] | N
|
|
| 71 |
df2.at[idx, "値"] = suggestions[key]
|
| 72 |
return df2
|
| 73 |
|
| 74 |
-
# =====
|
| 75 |
_WEIGHTS = {
|
| 76 |
("経営者能力", "経営姿勢"): 8,
|
| 77 |
("経営者能力", "事業経験"): 5,
|
|
@@ -81,7 +80,7 @@ _WEIGHTS = {
|
|
| 81 |
("成長率", "売上高伸長性"): 10,
|
| 82 |
("成長率", "利益伸長性"): 10,
|
| 83 |
("成長率", "商品"): 6,
|
| 84 |
-
("成長率", "市場成長調整"): 6,
|
| 85 |
|
| 86 |
("安定性", "自己資本"): 8,
|
| 87 |
("安定性", "決済振り"): 10,
|
|
@@ -95,34 +94,21 @@ _WEIGHTS = {
|
|
| 95 |
_WEIGHT_NORM = 100.0 / float(sum(_WEIGHTS.values()))
|
| 96 |
|
| 97 |
def _clamp(v, a, b): return max(a, min(b, v))
|
| 98 |
-
|
| 99 |
-
def _add(items, cat, name, raw, weight, reason):
|
| 100 |
-
raw_s = None if raw is None else round(raw, 2)
|
| 101 |
-
w = round(weight * _WEIGHT_NORM, 2)
|
| 102 |
-
sc = 0.0 if raw is None else round((raw / 10.0) * w, 2)
|
| 103 |
-
items.append({
|
| 104 |
-
"category": cat, "name": name, "raw": raw_s,
|
| 105 |
-
"weight": w, "score": sc, "reason": reason
|
| 106 |
-
})
|
| 107 |
-
|
| 108 |
def _to_float(x):
|
| 109 |
if x is None: return None
|
| 110 |
try:
|
| 111 |
return float(str(x).replace(",", "").replace("▲", "-").replace("△", "-"))
|
| 112 |
except Exception:
|
| 113 |
return None
|
| 114 |
-
|
| 115 |
def _to_bool(x):
|
| 116 |
if x is None: return None
|
| 117 |
s = str(x).strip().lower()
|
| 118 |
if s in ("true","t","1","yes","y","有","あり"): return True
|
| 119 |
if s in ("false","f","0","no","n","無","なし"): return False
|
| 120 |
return None
|
| 121 |
-
|
| 122 |
def _ratio(a,b):
|
| 123 |
if a is None or b is None or b == 0: return None
|
| 124 |
return a/b
|
| 125 |
-
|
| 126 |
def _ramp(x, good, bad, lo=0.0, hi=10.0, neutral=None):
|
| 127 |
if x is None:
|
| 128 |
return neutral if neutral is not None else (lo+hi)/2.0
|
|
@@ -135,8 +121,27 @@ def _ramp(x, good, bad, lo=0.0, hi=10.0, neutral=None):
|
|
| 135 |
if x <= good: return hi
|
| 136 |
return lo + (hi-lo) * (x-good)/(bad-good)
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
|
| 139 |
-
"""入力DF(カテゴリー/入力項目/値)を定量スコア化。欠損は中立。市場成長率も反映。"""
|
| 140 |
def ref(label: str):
|
| 141 |
m = df["入力項目"].eq(label)
|
| 142 |
return df.loc[m, "値"].values[0] if m.any() else None
|
|
@@ -225,7 +230,7 @@ def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
|
|
| 225 |
_WEIGHTS[("成長率","利益伸長性")],
|
| 226 |
f"CAGR営業{round((p_cagr or 0)*100,1) if p_cagr is not None else '—'}%")
|
| 227 |
|
| 228 |
-
#
|
| 229 |
if prod_total is None or prod_total <= 0:
|
| 230 |
pr_sc = 5.0; rs = "不明→中立"
|
| 231 |
else:
|
|
@@ -273,7 +278,7 @@ def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
|
|
| 273 |
_add(items,"安定性","業歴", _ramp(years,20,1),
|
| 274 |
_WEIGHTS[("安定性","業歴")], f"{years or '—'}年")
|
| 275 |
|
| 276 |
-
#
|
| 277 |
sc_dis = 0.0
|
| 278 |
has_sec = _to_bool(ref("有価証券報告書提出企業か(TRUE/FALSE)"))
|
| 279 |
sc_dis += 10.0 if has_sec else 0.0
|
|
@@ -290,7 +295,6 @@ def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
|
|
| 290 |
|
| 291 |
total = round(sum(x["score"] for x in items),1)
|
| 292 |
|
| 293 |
-
# レーダ用カテゴリスコア(重み付き)
|
| 294 |
from collections import defaultdict
|
| 295 |
cat_sum, cat_w = defaultdict(float), defaultdict(float)
|
| 296 |
for it in items:
|
|
@@ -303,5 +307,5 @@ def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
|
|
| 303 |
"external_total": total,
|
| 304 |
"items": items,
|
| 305 |
"category_scores": cat_scores,
|
| 306 |
-
"notes": "
|
| 307 |
}
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
from typing import Dict, Any, List, Tuple
|
| 4 |
import pandas as pd
|
|
|
|
| 5 |
|
| 6 |
__all__ = [
|
| 7 |
"get_external_template_df",
|
|
|
|
| 70 |
df2.at[idx, "値"] = suggestions[key]
|
| 71 |
return df2
|
| 72 |
|
| 73 |
+
# ===== スコア計算(定量・ばらつき強化) =====
|
| 74 |
_WEIGHTS = {
|
| 75 |
("経営者能力", "経営姿勢"): 8,
|
| 76 |
("経営者能力", "事業経験"): 5,
|
|
|
|
| 80 |
("成長率", "売上高伸長性"): 10,
|
| 81 |
("成長率", "利益伸長性"): 10,
|
| 82 |
("成長率", "商品"): 6,
|
| 83 |
+
("成長率", "市場成長調整"): 6,
|
| 84 |
|
| 85 |
("安定性", "自己資本"): 8,
|
| 86 |
("安定性", "決済振り"): 10,
|
|
|
|
| 94 |
_WEIGHT_NORM = 100.0 / float(sum(_WEIGHTS.values()))
|
| 95 |
|
| 96 |
def _clamp(v, a, b): return max(a, min(b, v))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
def _to_float(x):
|
| 98 |
if x is None: return None
|
| 99 |
try:
|
| 100 |
return float(str(x).replace(",", "").replace("▲", "-").replace("△", "-"))
|
| 101 |
except Exception:
|
| 102 |
return None
|
|
|
|
| 103 |
def _to_bool(x):
|
| 104 |
if x is None: return None
|
| 105 |
s = str(x).strip().lower()
|
| 106 |
if s in ("true","t","1","yes","y","有","あり"): return True
|
| 107 |
if s in ("false","f","0","no","n","無","なし"): return False
|
| 108 |
return None
|
|
|
|
| 109 |
def _ratio(a,b):
|
| 110 |
if a is None or b is None or b == 0: return None
|
| 111 |
return a/b
|
|
|
|
| 112 |
def _ramp(x, good, bad, lo=0.0, hi=10.0, neutral=None):
|
| 113 |
if x is None:
|
| 114 |
return neutral if neutral is not None else (lo+hi)/2.0
|
|
|
|
| 121 |
if x <= good: return hi
|
| 122 |
return lo + (hi-lo) * (x-good)/(bad-good)
|
| 123 |
|
| 124 |
+
def _stretch_0_10(x: float, k: float = 1.2) -> float:
|
| 125 |
+
"""
|
| 126 |
+
0-10 を保ちつつ中央付近(4-6)の密集をほぐす単調変換。
|
| 127 |
+
k>1で両端を強調。k≈1.2~1.4 推奨。
|
| 128 |
+
"""
|
| 129 |
+
if x is None: return None
|
| 130 |
+
t = (x/10.0)
|
| 131 |
+
t = t**(1.0/k) if t >= 0.5 else (t**k)
|
| 132 |
+
return _clamp(t*10.0, 0.0, 10.0)
|
| 133 |
+
|
| 134 |
+
def _add(items, cat, name, raw, weight, reason):
|
| 135 |
+
raw2 = _stretch_0_10(raw, k=1.25) if raw is not None else None
|
| 136 |
+
w = round(weight * _WEIGHT_NORM, 2)
|
| 137 |
+
sc = 0.0 if raw2 is None else round((raw2 / 10.0) * w, 2)
|
| 138 |
+
items.append({
|
| 139 |
+
"category": cat, "name": name, "raw": None if raw is None else round(raw,2),
|
| 140 |
+
"raw_stretched": None if raw2 is None else round(raw2,2),
|
| 141 |
+
"weight": w, "score": sc, "reason": reason
|
| 142 |
+
})
|
| 143 |
+
|
| 144 |
def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
|
|
|
|
| 145 |
def ref(label: str):
|
| 146 |
m = df["入力項目"].eq(label)
|
| 147 |
return df.loc[m, "値"].values[0] if m.any() else None
|
|
|
|
| 230 |
_WEIGHTS[("成長率","利益伸長性")],
|
| 231 |
f"CAGR営業{round((p_cagr or 0)*100,1) if p_cagr is not None else '—'}%")
|
| 232 |
|
| 233 |
+
# 商品
|
| 234 |
if prod_total is None or prod_total <= 0:
|
| 235 |
pr_sc = 5.0; rs = "不明→中立"
|
| 236 |
else:
|
|
|
|
| 278 |
_add(items,"安定性","業歴", _ramp(years,20,1),
|
| 279 |
_WEIGHTS[("安定性","業歴")], f"{years or '—'}年")
|
| 280 |
|
| 281 |
+
# 公平性
|
| 282 |
sc_dis = 0.0
|
| 283 |
has_sec = _to_bool(ref("有価証券報告書提出企業か(TRUE/FALSE)"))
|
| 284 |
sc_dis += 10.0 if has_sec else 0.0
|
|
|
|
| 295 |
|
| 296 |
total = round(sum(x["score"] for x in items),1)
|
| 297 |
|
|
|
|
| 298 |
from collections import defaultdict
|
| 299 |
cat_sum, cat_w = defaultdict(float), defaultdict(float)
|
| 300 |
for it in items:
|
|
|
|
| 307 |
"external_total": total,
|
| 308 |
"items": items,
|
| 309 |
"category_scores": cat_scores,
|
| 310 |
+
"notes": "欠損は中立+市場成長補正。ストレッチでばらつきを拡大。",
|
| 311 |
}
|