File size: 15,043 Bytes
0a34695
 
eb687c0
0a34695
 
692f002
 
 
dab75f1
692f002
 
0a34695
2fc41c7
0a34695
 
 
 
 
 
 
 
 
 
 
 
eb687c0
0a34695
 
 
 
 
 
 
 
692f002
eb687c0
0a34695
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb687c0
0a34695
 
 
 
 
 
 
692f002
 
 
 
 
 
 
 
dab75f1
 
 
692f002
 
dab75f1
2fc41c7
dab75f1
 
2fc41c7
 
dab75f1
2fc41c7
 
 
 
 
dab75f1
2fc41c7
dab75f1
 
2fc41c7
dab75f1
 
 
 
 
 
 
 
 
2fc41c7
dab75f1
 
2fc41c7
0a34695
 
 
 
 
eb687c0
0a34695
 
 
6116cd3
eb687c0
0a34695
 
 
 
 
 
eb687c0
0a34695
 
 
 
eb687c0
 
fc23dfc
0a34695
eb687c0
0a34695
 
eb687c0
fc23dfc
0a34695
fc23dfc
 
0a34695
fc23dfc
 
 
eb687c0
0a34695
fc23dfc
0a34695
 
 
fc23dfc
0a34695
 
 
fc23dfc
dab75f1
6116cd3
 
 
 
 
 
 
 
 
 
 
 
 
 
0a34695
2fc41c7
0a34695
692f002
 
0a34695
fc23dfc
0a34695
 
 
 
 
 
 
 
 
 
 
 
 
692f002
 
 
 
 
 
eb687c0
0a34695
692f002
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb687c0
0a34695
 
 
eb687c0
fc23dfc
 
 
692f002
 
0a34695
eb687c0
692f002
 
 
 
fc23dfc
692f002
 
0a34695
fc23dfc
692f002
 
0a34695
fc23dfc
 
0a34695
fc23dfc
 
0a34695
fc23dfc
0a34695
fc23dfc
 
0a34695
fc23dfc
 
 
692f002
fc23dfc
 
692f002
 
6116cd3
692f002
 
 
 
 
 
fc23dfc
692f002
fc23dfc
 
 
 
692f002
eb687c0
 
fc23dfc
 
692f002
0a34695
fc23dfc
 
 
 
0a34695
2fc41c7
fc23dfc
0a34695
 
 
eb687c0
fc23dfc
1646b86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# core/external_scoring.py
from __future__ import annotations
from typing import Dict, Any, List, Tuple
import pandas as pd

__all__ = [
    "get_external_template_df",
    "fill_missing_with_external",
    "merge_market_into_external_df",
    "score_external_from_df",
]

# ひな形(+市場成長率系の列を追加)
_TEMPLATE_ROWS: List[Tuple[str, str]] = [
    ("経営者能力", "予実達成率_3年平均(%)"),
    ("経営者能力", "監査・内部統制の重大な不備 件数(過去3年)"),
    ("経営者能力", "重大コンプライアンス件数(過去3年)"),
    ("経営者能力", "社外取締役比率(%)"),
    ("経営者能力", "代表者の業界経験年数"),
    ("経営者能力", "現預金(円)"),
    ("経営者能力", "月商(円)"),
    ("経営者能力", "担保余力評価額(円)"),
    ("経営者能力", "倒産歴の有無(TRUE/FALSE)"),
    ("経営者能力", "倒産からの経過年数"),
    ("経営者能力", "重大事件・事故件数(過去10年)"),

    ("成長率", "売上_期3(最新期)"),
    ("成長率", "売上_期2"),
    ("成長率", "売上_期1(最古期)"),
    ("成長率", "営業利益_期3(最新期)"),
    ("成長率", "営業利益_期2"),
    ("成長率", "営業利益_期1(最古期)"),
    ("成長率", "主力商品数"),
    ("成長率", "成長中主力商品数"),
    ("成長率", "市場の年成長率(%)"),

    ("安定性", "自己資本比率(%)"),
    ("安定性", "利益剰余金(円)"),
    ("安定性", "支払遅延件数(直近12ヶ月)"),
    ("安定性", "不渡り件数(直近12ヶ月)"),
    ("安定性", "平均支払遅延日数"),
    ("安定性", "メインバンク明確か(TRUE/FALSE)"),
    ("安定性", "借入先数"),
    ("安定性", "メインバンク借入シェア(%)"),
    ("安定性", "コミットメントライン等の長期与信枠あり(TRUE/FALSE)"),
    ("安定性", "担保余力評価額(円)"),
    ("安定性", "月商(円)_再掲"),
    ("安定性", "主要顧客上位1社売上比率(%)"),
    ("安定性", "主要顧客上位3社売上比率(%)"),
    ("安定性", "主要顧客の平均信用スコア(0-100)"),
    ("安定性", "不良債権件数(直近12ヶ月)"),
    ("安定性", "業歴(年)"),

    ("公平性・総合世評", "有価証券報告書提出企業か(TRUE/FALSE)"),
    ("公平性・総合世評", "決算公告や官報での公開あり(TRUE/FALSE)"),
    ("公平性・総合世評", "HP/IRサイトで財務資料公開あり(TRUE/FALSE)"),
    ("公平性・総合世評", "直近更新が定め通りか(TRUE/FALSE)"),
]

def get_external_template_df() -> pd.DataFrame:
    return pd.DataFrame([(c, i, "") for c, i in _TEMPLATE_ROWS],
                        columns=["カテゴリー", "入力項目", "値"])

def fill_missing_with_external(df: pd.DataFrame, suggestions: Dict[str, Any] | None = None) -> pd.DataFrame:
    if not suggestions:
        return df.copy()
    df2 = df.copy()
    for idx, row in df2.iterrows():
        k = row["入力項目"]
        if (row["値"] in (None, "", "—")) and (k in suggestions):
            df2.at[idx, "値"] = suggestions[k]
    return df2

def merge_market_into_external_df(ext_df: pd.DataFrame, market: Dict[str, Any], products: List[str]) -> pd.DataFrame:
    """市場推定結果と商品リストをext_dfへ反映(必ずDataFrameを返す)"""
    df = ext_df.copy()

    def _set(df_: pd.DataFrame, label: str, val: Any, cat_hint: str = "成長率") -> pd.DataFrame:
        m = df_["入力項目"].eq(label)
        if m.any():
            df_.loc[m, "値"] = val
            return df_
        # 行がない場合は追加
        return pd.concat([df_, pd.DataFrame([[cat_hint, label, val]], columns=df_.columns)], ignore_index=True)

    if market.get("市場の年成長率(%)") is not None:
        df = _set(df, "市場の年成長率(%)", float(market["市場の年成長率(%)"]), "成長率")

    prods = [p for p in products if str(p).strip()]
    df = _set(df, "主力商品数", len(prods), "成長率")

    growing = 0
    prod_growth: Dict[str, float] = market.get("製品別年成長率(%)") or {}
    for p in prods:
        try:
            if float(prod_growth.get(p, 0.0)) > 10.0:
                growing += 1
        except Exception:
            pass
    df = _set(df, "成長中主力商品数", growing, "成長率")
    return df

# ===== スコア計算(定量化+ばらつきストレッチ) =====
_WEIGHTS = {
    ("経営者能力", "経営姿勢"): 8,
    ("経営者能力", "事業経験"): 5,
    ("経営者能力", "資産担保力"): 6,
    ("経営者能力", "減点事項"): 7,

    ("成長率", "売上高伸長性"): 10,
    ("成長率", "利益伸長性"): 10,
    ("成長率", "商品"): 6,
    ("成長率", "市場成長調整"): 6,

    ("安定性", "自己資本"): 8,
    ("安定性", "決済振り"): 10,
    ("安定性", "金融取引"): 6,
    ("安定性", "資産担保余力"): 6,
    ("安定性", "取引先"): 6,
    ("安定性", "業歴"): 4,

    ("公平性・総合世評", "ディスクロージャー"): 8,
}
_WEIGHT_NORM = 100.0 / float(sum(_WEIGHTS.values()))

def _clamp(v, a, b): return max(a, min(b, v))
def _to_float(x):
    if x is None: return None
    try:
        return float(str(x).replace(",", "").replace("▲", "-").replace("△", "-"))
    except Exception:
        return None
def _to_bool(x):
    if x is None: return None
    s = str(x).strip().lower()
    if s in ("true","t","1","yes","y","有","あり"): return True
    if s in ("false","f","0","no","n","無","なし"): return False
    return None
def _ratio(a,b):
    if a is None or b is None or b == 0: return None
    return a/b
def _ramp(x, good, bad, lo=0.0, hi=10.0, neutral=None):
    if x is None:
        return neutral if neutral is not None else (lo+hi)/2.0
    if good > bad:
        if x <= bad: return lo
        if x >= good: return hi
        return lo + (hi-lo) * (x-bad)/(good-bad)
    else:
        if x >= bad: return lo
        if x <= good: return hi
        return lo + (hi-lo) * (x-good)/(bad-good)
def _stretch_0_10(x: float, k: float = 1.25) -> float:
    if x is None: return None
    t = (x/10.0)
    t = t**(1.0/k) if t >= 0.5 else (t**k)
    return _clamp(t*10.0, 0.0, 10.0)
def _add(items, cat, name, raw, weight, reason):
    raw2 = _stretch_0_10(raw, k=1.25) if raw is not None else None
    w = round(weight * _WEIGHT_NORM, 2)
    sc = 0.0 if raw2 is None else round((raw2 / 10.0) * w, 2)
    items.append({
        "category": cat, "name": name, "raw": None if raw is None else round(raw,2),
        "raw_stretched": None if raw2 is None else round(raw2,2),
        "weight": w, "score": sc, "reason": reason
    })

def score_external_from_df(df: pd.DataFrame) -> Dict[str, Any]:
    # 必ず dict を返す。途中で例外にならないよう to_x で吸収。
    def ref(label: str):
        m = df["入力項目"].eq(label)
        return df.loc[m, "値"].values[0] if m.any() else None

    items: List[Dict[str, Any]] = []

    yoy3 = _to_float(ref("予実達成率_3年平均(%)"))
    audit_bad = _to_float(ref("監査・内部統制の重大な不備 件数(過去3年)"))
    comp_bad = _to_float(ref("重大コンプライアンス件数(過去3年)"))
    indep = _to_float(ref("社外取締役比率(%)"))
    exp_years = _to_float(ref("代表者の業界経験年数"))
    cash = _to_float(ref("現預金(円)"))
    sales_m = _to_float(ref("月商(円)"))
    collat = _to_float(ref("担保余力評価額(円)"))
    has_bk = _to_bool(ref("倒産歴の有無(TRUE/FALSE)"))
    bk_years = _to_float(ref("倒産からの経過年数"))
    incidents = _to_float(ref("重大事件・事故件数(過去10年)"))

    s1 = _to_float(ref("売上_期1(最古期)"))
    s2 = _to_float(ref("売上_期2"))
    s3 = _to_float(ref("売上_期3(最新期)"))
    p1 = _to_float(ref("営業利益_期1(最古期)"))
    p2 = _to_float(ref("営業利益_期2"))
    p3 = _to_float(ref("営業利益_期3(最新期)"))

    equity = _to_float(ref("自己資本比率(%)"))
    delay_cnt = _to_float(ref("支払遅延件数(直近12ヶ月)"))
    boun_cnt = _to_float(ref("不渡り件数(直近12ヶ月)"))
    delay_days = _to_float(ref("平均支払遅延日数"))
    mainbank = _to_bool(ref("メインバンク明確か(TRUE/FALSE)"))
    lenders = _to_float(ref("借入先数"))
    main_share = _to_float(ref("メインバンク借入シェア(%)"))
    has_line = _to_bool(ref("コミットメントライン等の長期与信枠あり(TRUE/FALSE)"))
    sales_m2 = _to_float(ref("月商(円)_再掲")) or sales_m
    top1 = _to_float(ref("主要顧客上位1社売上比率(%)"))
    top3 = _to_float(ref("主要顧客上位3社売上比率(%)"))
    cust_score = _to_float(ref("主要顧客の平均信用スコア(0-100)"))
    npl_cnt = _to_float(ref("不良債権件数(直近12ヶ月)"))
    years = _to_float(ref("業歴(年)"))

    prod_total = _to_float(ref("主力商品数"))
    prod_growing = _to_float(ref("成長中主力商品数"))
    market_growth = _to_float(ref("市場の年成長率(%)"))

    cash_to_ms = _ratio(cash, sales_m2)
    coll_to_ms = _ratio(collat, sales_m2)

    def cagr(v1, v3):
        if v1 is None or v3 is None or v1 <= 0: return None
        try: return (v3/v1)**(1/2) - 1.0
        except Exception: return None
    s_cagr = cagr(s1, s3)
    p_cagr = cagr(p1, p3)

    # 経営者能力
    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
    _add(items, "経営者能力", "経営姿勢", mg_att, _WEIGHTS[("経営者能力","経営姿勢")],
         f"予実{yoy3 or '—'}%/監査{audit_bad or 0}/違反{comp_bad or 0}/社外{indep or '—'}%")

    mg_exp = _ramp(exp_years if exp_years is not None else 5.0, 15, 0)
    _add(items, "経営者能力", "事業経験", mg_exp, _WEIGHTS[("経営者能力","事業経験")],
         f"経験{exp_years if exp_years is not None else '不明→中立'}年")

    mg_asset = _ramp(cash_to_ms, 1.5, 0.2)
    _add(items, "経営者能力", "資産担保力", mg_asset, _WEIGHTS[("経営者能力","資産担保力")],
         f"現預金/月商≈{round(cash_to_ms,2) if cash_to_ms else '—'}")

    if incidents and incidents>0:
        pen=0.0; rs=f"重大事故{int(incidents)}件→大幅減点"
    elif has_bk:
        pen=6.0 if (bk_years and bk_years>=10) else 3.0; rs=f"倒産歴あり({bk_years or '不明'}年)"
    else:
        pen=10.0; rs="事故/倒産なし"
    _add(items,"経営者能力","減点事項",pen,_WEIGHTS[("経営者能力","減点事項")],rs)

    # 成長率
    _add(items,"成長率","売上高伸長性", _ramp(s_cagr,0.08,-0.05),
         _WEIGHTS[("成長率","売上高伸長性")],
         f"CAGR売上{round((s_cagr or 0)*100,1) if s_cagr is not None else '—'}%")
    _add(items,"成長率","利益伸長性", _ramp(p_cagr,0.08,-0.05),
         _WEIGHTS[("成長率","利益伸長性")],
         f"CAGR営業{round((p_cagr or 0)*100,1) if p_cagr is not None else '—'}%")

    # 商品
    if prod_total is None or prod_total <= 0:
        pr_sc = 5.0; rs = "不明→中立"
    else:
        ratio = _ratio(prod_growing, prod_total) or 0.0
        pr_sc = ( _ramp(prod_total, 3, 0) + _ramp(ratio, 0.7, 0.1) ) / 2
        rs = f"主力{int(prod_total)}/成長中比{round(ratio*100,1)}%"
    _add(items,"成長率","商品", pr_sc, _WEIGHTS[("成長率","商品")], rs)

    # 市場成長調整
    _add(items,"成長率","市場成長調整",
         _ramp(market_growth,15,-5),
         _WEIGHTS[("成長率","市場成長調整")],
         f"市場年成長{market_growth or '—'}%")

    # 安定性
    _add(items,"安定性","自己資本", _ramp(equity,40,5),
         _WEIGHTS[("安定性","自己資本")], f"自己資本比率{equity or '—'}%")

    if (delay_cnt is not None) or (boun_cnt is not None) or (delay_days is not None):
        sc=( _ramp(-(delay_cnt or 0),0,-6) +
             _ramp(-(boun_cnt or 0),0,-1) +
             _ramp(-(delay_days or 0),0,-30) )/3
        rs=f"遅延{int(delay_cnt or 0)}/不渡{int(boun_cnt or 0)}/平均{int(delay_days or 0)}日"
    else:
        sc=_ramp(_ratio(cash, sales_m2),1.0,0.2); rs=f"代理:現預金/月商≈—"
    _add(items,"安定性","決済振り", sc, _WEIGHTS[("安定性","決済振り")], rs)

    sc_mb = 5.0
    sc_mb += 2.0 if mainbank else (-0.5 if mainbank is False else 0)
    sc_mb += 1.0 if has_line else 0
    sc_mb = _clamp(sc_mb,0,10)
    _add(items,"安定性","金融取引", sc_mb, _WEIGHTS[("安定性","金融取引")],
         f"メイン{'有' if mainbank else '無' if mainbank is False else '—'}/与信枠{'有' if has_line else '無' if has_line is False else '—'}")

    _add(items,"安定性","資産担保余力", _ramp(_ratio(collat, sales_m2),4.0,0.0),
         _WEIGHTS[("安定性","資産担保余力")], f"担保/月商≈—")

    _add(items,"安定性","取引先",
         ( _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)}")

    _add(items,"安定性","業歴", _ramp(years,20,1),
         _WEIGHTS[("安定性","業歴")], f"{years or '—'}年")

    # 公平性
    sc_dis = 0.0
    has_sec = _to_bool(ref("有価証券報告書提出企業か(TRUE/FALSE)"))
    sc_dis += 10.0 if has_sec else 0.0
    if sc_dis == 0.0:
        pub_off = _to_bool(ref("決算公告や官報での公開あり(TRUE/FALSE)"))
        pub_web = _to_bool(ref("HP/IRサイトで財務資料公開あり(TRUE/FALSE)"))
        sc_dis += 7.0 if (pub_off or pub_web) else 4.0
    upd_on = _to_bool(ref("直近更新が定め通りか(TRUE/FALSE)"))
    if upd_on: sc_dis += 1.0
    sc_dis = _clamp(sc_dis,0,10)
    _add(items,"公平性・総合世評","ディスクロージャー", sc_dis,
         _WEIGHTS[("公平性・総合世評","ディスクロージャー")],
         f"{'有報' if has_sec else '公開あり' if sc_dis>=7.0 else '公開乏しい'} / 更新{'◯' if upd_on else '—'}")

    total = round(sum(x["score"] for x in items),1)

    from collections import defaultdict
    cat_sum, cat_w = defaultdict(float), defaultdict(float)
    for it in items:
        cat_sum[it["category"]] += it["score"]
        cat_w[it["category"]] += it["weight"]
    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}

    return {
        "name": "企業評価(外部・定量)",
        "external_total": total,
        "items": items,
        "category_scores": cat_scores,
        "notes": "欠損は中立+市場成長/商品構成を反映。ストレッチでばらつきを拡大。",
    }