#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""LR + WOE 评分卡标准打分脚本（由实验 DEV-mw04ysue-3434 自动生成）。

用法:
    python score.py <输入.csv> <输出.csv>

依赖: pandas, numpy。除本目录下两个 JSON 产物外无其他依赖:
    woe_map.json       分箱边界与每箱 WOE（仅开发样本拟合）
    model_results.json LR 系数、截距与评分校准参数

打分口径: logit = intercept + Σ coef_i × WOE_i
    PD    = 1 / (1 + exp(-logit))
    分数   = offset - factor × ln(PD / (1-PD))
    (factor = PDO / ln2, offset = 标准分 - factor × ln(标准ODD))
"""
import json
import math
import os
import sys

import numpy as np
import pandas as pd

HERE = os.path.dirname(os.path.abspath(__file__))


def load_artifacts():
    with open(os.path.join(HERE, "woe_map.json")) as f:
        woe_map = json.load(f)
    with open(os.path.join(HERE, "model_results.json")) as f:
        lr = json.load(f)["lr_scorecard"]
    return woe_map, lr


def encode_woe(df, woe_map):
    """Apply bin WOE values to a raw wide table (missing/unmatched -> mean WOE)."""
    encoded = pd.DataFrame(index=df.index)
    for col, mapping in woe_map.items():
        if col not in df.columns:
            raise SystemExit(f"输入缺少建模变量列: {col}")
        series = df[col]
        if mapping.get("is_numeric", True):
            out = pd.Series(np.nan, index=df.index)
            edges = [
                (float("-inf") if b["left"] is None else b["left"],
                 float("inf") if b["right"] is None else b["right"],
                 b["woe"])
                for b in mapping["bins"]
                if "left" in b
            ]
            for i, (left, right, woe_val) in enumerate(edges):
                mask = (series >= left) & (series <= right) if i == 0 else (series > left) & (series <= right)
                out.loc[mask] = woe_val
            out = out.fillna(np.nanmean([e[2] for e in edges]) if edges else 0.0)
        else:
            woe_dict = {str(b["bin"]): b["woe"] for b in mapping["bins"]}
            out = series.astype(str).map(woe_dict)
            out = out.fillna(np.nanmean(list(woe_dict.values())) if woe_dict else 0.0)
        encoded[col] = out
    return encoded


def score(df, woe_map, lr):
    coefs = lr["coefficients"]
    intercept = lr["intercept"]
    encoded = encode_woe(df, woe_map)
    logit = intercept + sum(coefs.get(c, 0.0) * encoded[c] for c in encoded.columns)

    cal = lr.get("calibration", {"base_score": 600, "base_odds": 50, "pdo": 20})
    factor = cal["pdo"] / math.log(2)
    offset = cal["base_score"] - factor * math.log(cal["base_odds"])
    pd_hat = 1.0 / (1.0 + np.exp(-logit))
    points = np.round(offset - factor * logit)
    return points, pd_hat


def main():
    if len(sys.argv) != 3:
        print(__doc__)
        sys.exit(1)
    src, dst = sys.argv[1], sys.argv[2]
    df = pd.read_csv(src)
    woe_map, lr = load_artifacts()
    points, pd_hat = score(df, woe_map, lr)
    out = df.copy()
    out["score"] = points.astype(int)
    out["pd"] = pd_hat.round(6)
    out.to_csv(dst, index=False)
    print(f"已打分 {len(out)} 行 -> {dst}; 分数范围 [{int(out['score'].min())}, {int(out['score'].max())}]")


if __name__ == "__main__":
    main()
