跳转至

Modeling_Tool.Core

基础设施层 —— 分箱、ODPS、工具函数、加密、JSON、斜率计算。

无跨包依赖,被其他所有子包依赖。

样本权重 — sample_weight_utils

解析 weight_col / sample_weight(及 wgt / wgt_col 别名),供 Model / Eval 层统一调用。

sample_weight_utils

Sample-weight resolution and weighted aggregation helpers.

resolve_sample_weight

resolve_sample_weight(data=None, weight_col=None, sample_weight=None, expected_len=None, wgt=None, wgt_col=None)

Resolve sample weights from a DataFrame column or array.

Accepts weight_col / wgt_col and sample_weight / wgt aliases. Returns None when no weight source is provided.

源代码位于: Modeling_Tool/Core/sample_weight_utils.py
def resolve_sample_weight(
    data=None,
    weight_col=None,
    sample_weight=None,
    expected_len=None,
    wgt=None,
    wgt_col=None,
):
    """Resolve sample weights from a DataFrame column or array.

    Accepts ``weight_col`` / ``wgt_col`` and ``sample_weight`` / ``wgt`` aliases.
    Returns ``None`` when no weight source is provided.
    """
    if weight_col is None:
        weight_col = wgt_col
    if sample_weight is None:
        sample_weight = wgt

    if sample_weight is not None and weight_col is not None:
        raise ValueError("Provide either weight_col or sample_weight, not both.")

    if weight_col is not None:
        if data is None:
            raise ValueError("data is required when weight_col is provided.")
        if weight_col not in data.columns:
            raise KeyError("weight column '{0}' not found in data".format(weight_col))
        sample_weight = data[weight_col].values

    if sample_weight is None:
        return None

    return validate_sample_weight(sample_weight, expected_len=expected_len)

validate_sample_weight

validate_sample_weight(weights, expected_len=None)

Validate and return a 1-D float numpy weight vector.

源代码位于: Modeling_Tool/Core/sample_weight_utils.py
def validate_sample_weight(weights, expected_len=None):
    """Validate and return a 1-D float numpy weight vector."""
    w = np.asarray(weights, dtype=float)
    if w.ndim != 1:
        raise ValueError("sample_weight must be 1-dimensional.")
    if expected_len is not None and len(w) != expected_len:
        raise ValueError(
            "sample_weight length {0} != expected {1}".format(len(w), expected_len)
        )
    if not np.all(np.isfinite(w)):
        raise ValueError("sample_weight must be finite (no NaN/inf).")
    if np.any(w < 0):
        raise ValueError("sample_weight must be non-negative.")
    return w

weighted_sum

weighted_sum(values, weights)

Weighted sum of values.

源代码位于: Modeling_Tool/Core/sample_weight_utils.py
def weighted_sum(values, weights):
    """Weighted sum of values."""
    v = np.asarray(values, dtype=float)
    w = np.asarray(weights, dtype=float)
    return float(np.sum(v * w))

weighted_mean

weighted_mean(values, weights)

Weighted mean; returns NaN when total weight is zero.

源代码位于: Modeling_Tool/Core/sample_weight_utils.py
def weighted_mean(values, weights):
    """Weighted mean; returns NaN when total weight is zero."""
    w = np.asarray(weights, dtype=float)
    total = float(np.sum(w))
    if total == 0:
        return np.nan
    return weighted_sum(values, weights) / total

weighted_rate

weighted_rate(mask, weights)

Weighted rate of True / 1 values in mask.

源代码位于: Modeling_Tool/Core/sample_weight_utils.py
def weighted_rate(mask, weights):
    """Weighted rate of True / 1 values in mask."""
    m = np.asarray(mask, dtype=float)
    return weighted_mean(m, weights)

分箱工具 — Binning_Tool

Binning_Tool

NumVarBinning

根据分组数和方法, 计算数值型数值变量切分点数值序列。

根据指定分组数计算切分点数值, 分组方法包括"等距分组"和"等分分组": (1) 当分组数不小于变量唯一值个数时, 切分点数值序列即为变量唯一值序列 (2) 当分组数小于变量唯一值个数时, 可选择"等距分组"或"等分分组"

若有指定需要独立成组的数值, 则: (1) 将数值加入切分点数值序列 (2) 添加数值上确界=数值+0.1**(精度+1), 加入切分点数值序列

切分点数值精度: (1) 若变量取值为整数, 则精度为保留1位小数 (2) 若变量取值为浮点数, 则精度为保留输入的位数

为了泛化性, 将切分点数值序列中的最小值替换为-inf, 最大值增加+inf

参数:

名称 类型 描述 默认
var_name str

变量名

必需
spec_values list

指定需要单独成组的特殊数值

None
spec_digit int

指定特殊的切分点上确界保留小数位数, 若变量为整数则固定为1, 若非整数则为spec_digit值

3

示例:

>>> nvb = NumVarBinning(var_name='income', spec_values=[-9999])
>>> binning = nvb.equi_binning(df, bins=10)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
class NumVarBinning:
    """
    根据分组数和方法, 计算数值型数值变量切分点数值序列。

    根据指定分组数计算切分点数值, 分组方法包括"等距分组"和"等分分组":
    (1) 当分组数不小于变量唯一值个数时, 切分点数值序列即为变量唯一值序列
    (2) 当分组数小于变量唯一值个数时, 可选择"等距分组"或"等分分组"

    若有指定需要独立成组的数值, 则:
    (1) 将数值加入切分点数值序列
    (2) 添加数值上确界=数值+0.1**(精度+1), 加入切分点数值序列

    切分点数值精度:
    (1) 若变量取值为整数, 则精度为保留1位小数
    (2) 若变量取值为浮点数, 则精度为保留输入的位数

    为了泛化性, 将切分点数值序列中的最小值替换为-inf, 最大值增加+inf

    Parameters
    ----------
    var_name : str
        变量名
    spec_values : list, optional
        指定需要单独成组的特殊数值
    spec_digit : int, default 3
        指定特殊的切分点上确界保留小数位数, 若变量为整数则固定为1, 若非整数则为spec_digit值

    Examples
    --------
    >>> nvb = NumVarBinning(var_name='income', spec_values=[-9999])
    >>> binning = nvb.equi_binning(df, bins=10)
    """
    def __init__(self, var_name, spec_values=None, spec_digit=3):
        """
        初始化数值变量分箱对象。

        Parameters
        ----------
        var_name : str
            变量名
        spec_values : list, optional
            指定需要单独成组的特殊数值
        spec_digit : int, default 3
            特殊值精度位数
        """
        self.var_name = var_name
        self.spec_values = spec_values
        self.spec_digit = spec_digit
        self.bins = None
        self.cut_points = None
        self.bin_names = None

    def calc_equi_cutpoints(self, df, bins=10, equi_method="equif"):
        """
        根据分组数和等值分组方法, 计算数值型数值变量切分点数值序列。

        1、根据指定分组数计算切分点数值, 分组方法包括"等距分组"和"等分分组"
        (1) 当分组数不小于变量唯一值个数时, 切分点数值序列即为变量唯一值序列
        (2) 当分组数小于变量唯一值个数时, 可选择"等距分组"或"等分分组"

        2、若有指定需要独立成组的数值, 则:
        (1) 将数值加入切分点数值序列;
        (2) 添加数值上确界=数值+0.1**(精度+1), 加入切分点数值序列;

        3、切分点数值精度:
        (1) 若变量取值为整数, 则精度为保留1位小数
        (2) 若变量取值为浮点数, 则精度为保留输入的位数

        Parameters
        ----------
        df : pandas.DataFrame
            数据表
        bins : int, default 10
            切分组数
        equi_method : string, default "equif"
            分组方法, 候选值{"equid"等距分组, "equif"等分分组}

        Returns
        -------
        cut_points : numpy.array
            切分点数值序列
        """
        var_series = df[self.var_name]

        if any(pd.isnull(var_series)):
            raise ValueError(f"{self.var_name}变量取值中出现NaN值")

        if pd.api.types.is_integer_dtype(var_series):
            spec_digit = 1
        else:
            spec_digit = self.spec_digit

        # 根据指定分bin数计算切分点值
        cut_points = var_series.unique()
        if len(cut_points) <= bins:
            cut_points.sort()
        elif equi_method == "equif":
            pct = np.arange(1, bins) / bins
            cut_points = var_series.quantile(pct, interpolation="higher").values
        elif equi_method == "equid":
            min_value = min(var_series)
            max_value = max(var_series)
            step = (max_value - min_value) / bins
            cut_points = np.arange(start=min_value, stop=max_value, step=step)[1:]
        else:
            raise ValueError("equi_method取值错误.")

        # 添加需要单独成组的特殊数值
        if bool(self.spec_values) and len(self.spec_values) > 0:
            self.spec_values.sort()
            spec_values_upper = [x + 0.1**spec_digit for x in self.spec_values]
            cut_points = np.append(cut_points, self.spec_values + spec_values_upper)
            cut_points.sort()

#         print("NumVarBinning Cut Points: ", cut_points)
        # 返回Info
        self.equi_method = equi_method

        return cut_points

    def modify_cutpoints(self, df, points):
        """
        修正切分点数值序列。

        1、切分点数值精度:
        (1) 若变量取值为整数, 则精度为保留1位小数
        (2) 若变量取值为浮点数, 则精度为保留输入的位数

        2、过滤不在变量值域内的切分点, 并去重

        3、为了泛化性, 将切分点数值序列中的最小值替换为-inf, 最大值增加+inf。

        Parameters
        ----------
        df : pandas.DataFrame
            数据表
        points : array-like
            切分点数值序列

        Returns
        -------
        cut_points : numpy.array
            修正后的切分点数值序列
        """
        var_series = df[self.var_name]

        if pd.api.types.is_integer_dtype(var_series):
            spec_digit = 1
        else:
            spec_digit = self.spec_digit

        # 过滤变量值域内切分点
        ## Added
        min_value_wo_spec_values = min(var_series[~var_series.isin(self.spec_values)])
        max_value_wo_spec_values = max(var_series[~var_series.isin(self.spec_values)])

        min_value = min(var_series)
        max_value = max(var_series)
#         print("(MIN, MAX): ", min_value, max_value)
        cut_points = list(filter(lambda x: min_value <= x <= max_value, points))
#         print("Modify Init Cut Points: ", cut_points)
        if bool(self.spec_values):
            spec_cut_points = list(filter(lambda x: min_value <= x <= max_value, self.spec_values))
#             print("Modify Spec Cut Points: ", spec_cut_points)

            ## Added
            spec_values_upper = [x + 0.1**spec_digit for x in self.spec_values]

            cut_points.extend(spec_cut_points)

            ## Added
            cut_points.extend(spec_values_upper)

        cut_points = np.unique(np.array(cut_points).astype("float").round(spec_digit))
        cut_points = np.insert(cut_points, 0, -np.inf)
        cut_points = np.append(cut_points, np.inf)

#         print("Final Cut Points Before Binning: ", cut_points)
        # 过滤有样本的切分点
        binning_series = pd.cut(df[self.var_name], cut_points, right=False, labels=cut_points[:-1])
        binning_cnts = binning_series.value_counts()
        cut_points = np.array(binning_cnts.index[binning_cnts > 0])
        cut_points.sort()

        # 若第一位切分点非spec_values, 则令其为-inf
        if bool(self.spec_values):
            if cut_points[0] not in self.spec_values:
                cut_points[0] = -np.inf
        else:
            cut_points[0] = -np.inf

        # 将变量的最大值增加+inf
        cut_points = np.append(cut_points, np.inf)

        return cut_points

    def apt_binning(self, df, points, modify=True):
        """
        使用指定切分点数值序列对变量进行分组。

        切分点数值精度: 
        (1) 若变量取值为整数, 则精度为保留1位小数
        (2) 若变量取值为浮点数, 则精度为保留输入的位数

        过滤不在变量值域内的切分点

        为了泛化性, 将切分点数值序列中的最小值替换为-inf, 最大值增加+inf。

        Parameters
        ----------
        df : pandas.DataFrame
            数据表
        points : array-like
            切分点数值序列
        modify : bool, default True
            是否修正切分点数值序列

        Returns
        -------
        binning_series : pandas.Categorical
            分组后序列
        """
        if modify:
            self.cut_points = self.modify_cutpoints(df, points)
        else:
            points.sort()
            self.cut_points = points
        self.bins = len(self.cut_points) - 1

        binning_series = pd.cut(df[self.var_name], self.cut_points, right=False)
        self.bin_names = binning_series.values.categories
        # self.bin_names = {i: str(c) for i, c in zip(range(self.bins), binning_series.values.categories)}
        # binning_series = binning_series.values.rename_categories(range(self.bins))

        return binning_series

    def equi_binning(self, df, bins=10, equi_method="equif"):
        """
        根据分组数和等值分组方法, 计算数值型数值变量切分点数值序列并分箱。

        Parameters
        ----------
        df : pandas.DataFrame
            数据表
        bins : int, default 10
            切分组数
        equi_method : string, default "equif"
            分组方法, 候选值{"equid"等距分组, "equif"等分分组}

        Returns
        -------
        binning_series : pandas.Categorical
            分组后序列
        """
        logging.info(f"-------- [{self.var_name} : EquiX Binning] --------")
        logging.info(f"PARAMS: bins={bins}, equi_method={equi_method}")
        cut_points = self.calc_equi_cutpoints(df=df, bins=bins, equi_method=equi_method)
        binning_series = self.apt_binning(df=df, points=cut_points, modify=True)

        return binning_series

    def apply_binning(self, df):
        """
        使用已保存的切分点数值序列对变量进行分组。

        使用之前通过equi_binning或auto_binning等方法计算的切分点对新数据进行分组。

        Parameters
        ----------
        df : pandas.DataFrame
            数据表

        Returns
        -------
        binning_series : pandas.Categorical
            分组后序列
        """
        if self.cut_points is None:
            raise ValueError("类self.cut_points变量为None。")
        binning_series = self.apt_binning(df, self.cut_points, modify=False)

        return binning_series

    def auto_binning(self, df, tgt_name, max_bins=10, min_prop_in_bin=0.05, equi_bins=200, equi_method="equif", init_points = None,  binning_criteria="chi2", chi2_p=0.95):
        """
        基于卡方检验的自动分组。

        基于分布距离计算方法, 按照规定最大组数和最小组样本数, 自动求解分组序列。
        步骤:
        1. 利用初始分组方法, 完成初始分组
        2. 基于分布距离计算方法, 计算初始分组中各组样本大小、组内分布距离, 以及合并相邻组后对原分组的距离差异增益
        3. 不断合并样本量不符合最小组样本数要求, 合并相邻组后距离差异增益较大的组, 直至满足最大组数和最小组样本数要求
        4. 根据最终分组结果, 生成分组后变量序列。

        Parameters
        ----------
        df : pandas.DataFrame
            数据表
        tgt_name : str
            目标变量名
        max_bins : int, default 10
            最大分组数量
        min_prop_in_bin : float, default 0.05
            每组最小样本数占比
        equi_bins : int, default 200
            初始分组数量
        equi_method : string, default "equif"
            初始分组方法, 候选值{"equid"等距分组, "equif"等分分组}
        init_points : array-like, default None
            初始切分点数值序列, 若非None时则equi_bins、equi_method失效
        binning_criteria : string, default "chi2"
            分组准则, 候选值{"chi2": "卡方值"}
        chi2_p : float, default 0.95
            用于计算自由度为1的卡方分布分位数的概率值. 当相邻组卡方值小于该值时, 认为不独立, 进行合并. 故越大独立性判断越严格

        Returns
        -------
        binning_series : pandas.Categorical
            分组后序列
        """
        logging.info(f"\n---------------- [{self.var_name} : Auto binning] ----------------")
        min_cnt_in_bin = np.floor(df.shape[0] * min_prop_in_bin)
        logging.info(f"PARAMS: max_bins={max_bins}, min_cnt_in_bin={min_cnt_in_bin}, criteria={binning_criteria}")

        # 初始分组
        if init_points is None:
            binning_series = self.equi_binning(df, bins=equi_bins, equi_method=equi_method)
        else:
            binning_series = self.apt_binning(df=df, points=init_points, modify=True)
        cut_points = self.cut_points

        # 初始分组字段透视表
        df_tmp = df[[self.var_name, tgt_name]].copy()
        df_tmp[self.var_name] = binning_series
        df_pvt = cre_pvt(df=df_tmp, var_name=self.var_name, tgt_name=tgt_name)
        df_pvt.index = cut_points[:-1]

        # 单独列出特殊值
        if bool(self.spec_values):
            df_pvt_spec = df_pvt.loc[[x in self.spec_values for x in df_pvt.index], ].copy()
            df_pvt = df_pvt.loc[[x not in self.spec_values for x in df_pvt.index], ].copy()
            max_bins = max_bins - df_pvt_spec.shape[0]

        # 自动分组
        if not df_pvt.empty:
            logging.info(f"-------- [{self.var_name} : Auto binning] --------")
            df_pvt = chi2_auto_binning(df_pvt=df_pvt, max_bins=max_bins, min_cnt_in_bin=min_cnt_in_bin, p=chi2_p)

            # 重新计算分组
            cut_points = df_pvt.index

            # 添加需要单独成组的特殊数值
            if bool(self.spec_values) and len(self.spec_values) > 0:
#                 spec_values_upper = [x + 0.1**spec_digit for x in self.spec_values]
                cut_points = np.append(cut_points, self.spec_values)
#                 cut_points = np.append(cut_points, self.spec_values_upper)
#                 cut_points.sort()

#             print("Cut Points After Auto Binning: ", cut_points)

        binning_series = self.apt_binning(
            df=df, 
            points=cut_points, 
            modify=True
            )

        return binning_series

calc_equi_cutpoints

calc_equi_cutpoints(df, bins=10, equi_method='equif')

根据分组数和等值分组方法, 计算数值型数值变量切分点数值序列。

1、根据指定分组数计算切分点数值, 分组方法包括"等距分组"和"等分分组" (1) 当分组数不小于变量唯一值个数时, 切分点数值序列即为变量唯一值序列 (2) 当分组数小于变量唯一值个数时, 可选择"等距分组"或"等分分组"

2、若有指定需要独立成组的数值, 则: (1) 将数值加入切分点数值序列; (2) 添加数值上确界=数值+0.1**(精度+1), 加入切分点数值序列;

3、切分点数值精度: (1) 若变量取值为整数, 则精度为保留1位小数 (2) 若变量取值为浮点数, 则精度为保留输入的位数

参数:

名称 类型 描述 默认
df DataFrame

数据表

必需
bins int

切分组数

10
equi_method string

分组方法, 候选值{"equid"等距分组, "equif"等分分组}

"equif"

返回:

名称 类型 描述
cut_points array

切分点数值序列

源代码位于: Modeling_Tool/Core/Binning_Tool.py
    def calc_equi_cutpoints(self, df, bins=10, equi_method="equif"):
        """
        根据分组数和等值分组方法, 计算数值型数值变量切分点数值序列。

        1、根据指定分组数计算切分点数值, 分组方法包括"等距分组"和"等分分组"
        (1) 当分组数不小于变量唯一值个数时, 切分点数值序列即为变量唯一值序列
        (2) 当分组数小于变量唯一值个数时, 可选择"等距分组"或"等分分组"

        2、若有指定需要独立成组的数值, 则:
        (1) 将数值加入切分点数值序列;
        (2) 添加数值上确界=数值+0.1**(精度+1), 加入切分点数值序列;

        3、切分点数值精度:
        (1) 若变量取值为整数, 则精度为保留1位小数
        (2) 若变量取值为浮点数, 则精度为保留输入的位数

        Parameters
        ----------
        df : pandas.DataFrame
            数据表
        bins : int, default 10
            切分组数
        equi_method : string, default "equif"
            分组方法, 候选值{"equid"等距分组, "equif"等分分组}

        Returns
        -------
        cut_points : numpy.array
            切分点数值序列
        """
        var_series = df[self.var_name]

        if any(pd.isnull(var_series)):
            raise ValueError(f"{self.var_name}变量取值中出现NaN值")

        if pd.api.types.is_integer_dtype(var_series):
            spec_digit = 1
        else:
            spec_digit = self.spec_digit

        # 根据指定分bin数计算切分点值
        cut_points = var_series.unique()
        if len(cut_points) <= bins:
            cut_points.sort()
        elif equi_method == "equif":
            pct = np.arange(1, bins) / bins
            cut_points = var_series.quantile(pct, interpolation="higher").values
        elif equi_method == "equid":
            min_value = min(var_series)
            max_value = max(var_series)
            step = (max_value - min_value) / bins
            cut_points = np.arange(start=min_value, stop=max_value, step=step)[1:]
        else:
            raise ValueError("equi_method取值错误.")

        # 添加需要单独成组的特殊数值
        if bool(self.spec_values) and len(self.spec_values) > 0:
            self.spec_values.sort()
            spec_values_upper = [x + 0.1**spec_digit for x in self.spec_values]
            cut_points = np.append(cut_points, self.spec_values + spec_values_upper)
            cut_points.sort()

#         print("NumVarBinning Cut Points: ", cut_points)
        # 返回Info
        self.equi_method = equi_method

        return cut_points

modify_cutpoints

modify_cutpoints(df, points)

修正切分点数值序列。

1、切分点数值精度: (1) 若变量取值为整数, 则精度为保留1位小数 (2) 若变量取值为浮点数, 则精度为保留输入的位数

2、过滤不在变量值域内的切分点, 并去重

3、为了泛化性, 将切分点数值序列中的最小值替换为-inf, 最大值增加+inf。

参数:

名称 类型 描述 默认
df DataFrame

数据表

必需
points array - like

切分点数值序列

必需

返回:

名称 类型 描述
cut_points array

修正后的切分点数值序列

源代码位于: Modeling_Tool/Core/Binning_Tool.py
    def modify_cutpoints(self, df, points):
        """
        修正切分点数值序列。

        1、切分点数值精度:
        (1) 若变量取值为整数, 则精度为保留1位小数
        (2) 若变量取值为浮点数, 则精度为保留输入的位数

        2、过滤不在变量值域内的切分点, 并去重

        3、为了泛化性, 将切分点数值序列中的最小值替换为-inf, 最大值增加+inf。

        Parameters
        ----------
        df : pandas.DataFrame
            数据表
        points : array-like
            切分点数值序列

        Returns
        -------
        cut_points : numpy.array
            修正后的切分点数值序列
        """
        var_series = df[self.var_name]

        if pd.api.types.is_integer_dtype(var_series):
            spec_digit = 1
        else:
            spec_digit = self.spec_digit

        # 过滤变量值域内切分点
        ## Added
        min_value_wo_spec_values = min(var_series[~var_series.isin(self.spec_values)])
        max_value_wo_spec_values = max(var_series[~var_series.isin(self.spec_values)])

        min_value = min(var_series)
        max_value = max(var_series)
#         print("(MIN, MAX): ", min_value, max_value)
        cut_points = list(filter(lambda x: min_value <= x <= max_value, points))
#         print("Modify Init Cut Points: ", cut_points)
        if bool(self.spec_values):
            spec_cut_points = list(filter(lambda x: min_value <= x <= max_value, self.spec_values))
#             print("Modify Spec Cut Points: ", spec_cut_points)

            ## Added
            spec_values_upper = [x + 0.1**spec_digit for x in self.spec_values]

            cut_points.extend(spec_cut_points)

            ## Added
            cut_points.extend(spec_values_upper)

        cut_points = np.unique(np.array(cut_points).astype("float").round(spec_digit))
        cut_points = np.insert(cut_points, 0, -np.inf)
        cut_points = np.append(cut_points, np.inf)

#         print("Final Cut Points Before Binning: ", cut_points)
        # 过滤有样本的切分点
        binning_series = pd.cut(df[self.var_name], cut_points, right=False, labels=cut_points[:-1])
        binning_cnts = binning_series.value_counts()
        cut_points = np.array(binning_cnts.index[binning_cnts > 0])
        cut_points.sort()

        # 若第一位切分点非spec_values, 则令其为-inf
        if bool(self.spec_values):
            if cut_points[0] not in self.spec_values:
                cut_points[0] = -np.inf
        else:
            cut_points[0] = -np.inf

        # 将变量的最大值增加+inf
        cut_points = np.append(cut_points, np.inf)

        return cut_points

apt_binning

apt_binning(df, points, modify=True)

使用指定切分点数值序列对变量进行分组。

切分点数值精度: (1) 若变量取值为整数, 则精度为保留1位小数 (2) 若变量取值为浮点数, 则精度为保留输入的位数

过滤不在变量值域内的切分点

为了泛化性, 将切分点数值序列中的最小值替换为-inf, 最大值增加+inf。

参数:

名称 类型 描述 默认
df DataFrame

数据表

必需
points array - like

切分点数值序列

必需
modify bool

是否修正切分点数值序列

True

返回:

名称 类型 描述
binning_series Categorical

分组后序列

源代码位于: Modeling_Tool/Core/Binning_Tool.py
def apt_binning(self, df, points, modify=True):
    """
    使用指定切分点数值序列对变量进行分组。

    切分点数值精度: 
    (1) 若变量取值为整数, 则精度为保留1位小数
    (2) 若变量取值为浮点数, 则精度为保留输入的位数

    过滤不在变量值域内的切分点

    为了泛化性, 将切分点数值序列中的最小值替换为-inf, 最大值增加+inf。

    Parameters
    ----------
    df : pandas.DataFrame
        数据表
    points : array-like
        切分点数值序列
    modify : bool, default True
        是否修正切分点数值序列

    Returns
    -------
    binning_series : pandas.Categorical
        分组后序列
    """
    if modify:
        self.cut_points = self.modify_cutpoints(df, points)
    else:
        points.sort()
        self.cut_points = points
    self.bins = len(self.cut_points) - 1

    binning_series = pd.cut(df[self.var_name], self.cut_points, right=False)
    self.bin_names = binning_series.values.categories
    # self.bin_names = {i: str(c) for i, c in zip(range(self.bins), binning_series.values.categories)}
    # binning_series = binning_series.values.rename_categories(range(self.bins))

    return binning_series

equi_binning

equi_binning(df, bins=10, equi_method='equif')

根据分组数和等值分组方法, 计算数值型数值变量切分点数值序列并分箱。

参数:

名称 类型 描述 默认
df DataFrame

数据表

必需
bins int

切分组数

10
equi_method string

分组方法, 候选值{"equid"等距分组, "equif"等分分组}

"equif"

返回:

名称 类型 描述
binning_series Categorical

分组后序列

源代码位于: Modeling_Tool/Core/Binning_Tool.py
def equi_binning(self, df, bins=10, equi_method="equif"):
    """
    根据分组数和等值分组方法, 计算数值型数值变量切分点数值序列并分箱。

    Parameters
    ----------
    df : pandas.DataFrame
        数据表
    bins : int, default 10
        切分组数
    equi_method : string, default "equif"
        分组方法, 候选值{"equid"等距分组, "equif"等分分组}

    Returns
    -------
    binning_series : pandas.Categorical
        分组后序列
    """
    logging.info(f"-------- [{self.var_name} : EquiX Binning] --------")
    logging.info(f"PARAMS: bins={bins}, equi_method={equi_method}")
    cut_points = self.calc_equi_cutpoints(df=df, bins=bins, equi_method=equi_method)
    binning_series = self.apt_binning(df=df, points=cut_points, modify=True)

    return binning_series

apply_binning

apply_binning(df)

使用已保存的切分点数值序列对变量进行分组。

使用之前通过equi_binning或auto_binning等方法计算的切分点对新数据进行分组。

参数:

名称 类型 描述 默认
df DataFrame

数据表

必需

返回:

名称 类型 描述
binning_series Categorical

分组后序列

源代码位于: Modeling_Tool/Core/Binning_Tool.py
def apply_binning(self, df):
    """
    使用已保存的切分点数值序列对变量进行分组。

    使用之前通过equi_binning或auto_binning等方法计算的切分点对新数据进行分组。

    Parameters
    ----------
    df : pandas.DataFrame
        数据表

    Returns
    -------
    binning_series : pandas.Categorical
        分组后序列
    """
    if self.cut_points is None:
        raise ValueError("类self.cut_points变量为None。")
    binning_series = self.apt_binning(df, self.cut_points, modify=False)

    return binning_series

auto_binning

auto_binning(df, tgt_name, max_bins=10, min_prop_in_bin=0.05, equi_bins=200, equi_method='equif', init_points=None, binning_criteria='chi2', chi2_p=0.95)

基于卡方检验的自动分组。

基于分布距离计算方法, 按照规定最大组数和最小组样本数, 自动求解分组序列。 步骤: 1. 利用初始分组方法, 完成初始分组 2. 基于分布距离计算方法, 计算初始分组中各组样本大小、组内分布距离, 以及合并相邻组后对原分组的距离差异增益 3. 不断合并样本量不符合最小组样本数要求, 合并相邻组后距离差异增益较大的组, 直至满足最大组数和最小组样本数要求 4. 根据最终分组结果, 生成分组后变量序列。

参数:

名称 类型 描述 默认
df DataFrame

数据表

必需
tgt_name str

目标变量名

必需
max_bins int

最大分组数量

10
min_prop_in_bin float

每组最小样本数占比

0.05
equi_bins int

初始分组数量

200
equi_method string

初始分组方法, 候选值{"equid"等距分组, "equif"等分分组}

"equif"
init_points array - like

初始切分点数值序列, 若非None时则equi_bins、equi_method失效

None
binning_criteria string

分组准则, 候选值{"chi2": "卡方值"}

"chi2"
chi2_p float

用于计算自由度为1的卡方分布分位数的概率值. 当相邻组卡方值小于该值时, 认为不独立, 进行合并. 故越大独立性判断越严格

0.95

返回:

名称 类型 描述
binning_series Categorical

分组后序列

源代码位于: Modeling_Tool/Core/Binning_Tool.py
    def auto_binning(self, df, tgt_name, max_bins=10, min_prop_in_bin=0.05, equi_bins=200, equi_method="equif", init_points = None,  binning_criteria="chi2", chi2_p=0.95):
        """
        基于卡方检验的自动分组。

        基于分布距离计算方法, 按照规定最大组数和最小组样本数, 自动求解分组序列。
        步骤:
        1. 利用初始分组方法, 完成初始分组
        2. 基于分布距离计算方法, 计算初始分组中各组样本大小、组内分布距离, 以及合并相邻组后对原分组的距离差异增益
        3. 不断合并样本量不符合最小组样本数要求, 合并相邻组后距离差异增益较大的组, 直至满足最大组数和最小组样本数要求
        4. 根据最终分组结果, 生成分组后变量序列。

        Parameters
        ----------
        df : pandas.DataFrame
            数据表
        tgt_name : str
            目标变量名
        max_bins : int, default 10
            最大分组数量
        min_prop_in_bin : float, default 0.05
            每组最小样本数占比
        equi_bins : int, default 200
            初始分组数量
        equi_method : string, default "equif"
            初始分组方法, 候选值{"equid"等距分组, "equif"等分分组}
        init_points : array-like, default None
            初始切分点数值序列, 若非None时则equi_bins、equi_method失效
        binning_criteria : string, default "chi2"
            分组准则, 候选值{"chi2": "卡方值"}
        chi2_p : float, default 0.95
            用于计算自由度为1的卡方分布分位数的概率值. 当相邻组卡方值小于该值时, 认为不独立, 进行合并. 故越大独立性判断越严格

        Returns
        -------
        binning_series : pandas.Categorical
            分组后序列
        """
        logging.info(f"\n---------------- [{self.var_name} : Auto binning] ----------------")
        min_cnt_in_bin = np.floor(df.shape[0] * min_prop_in_bin)
        logging.info(f"PARAMS: max_bins={max_bins}, min_cnt_in_bin={min_cnt_in_bin}, criteria={binning_criteria}")

        # 初始分组
        if init_points is None:
            binning_series = self.equi_binning(df, bins=equi_bins, equi_method=equi_method)
        else:
            binning_series = self.apt_binning(df=df, points=init_points, modify=True)
        cut_points = self.cut_points

        # 初始分组字段透视表
        df_tmp = df[[self.var_name, tgt_name]].copy()
        df_tmp[self.var_name] = binning_series
        df_pvt = cre_pvt(df=df_tmp, var_name=self.var_name, tgt_name=tgt_name)
        df_pvt.index = cut_points[:-1]

        # 单独列出特殊值
        if bool(self.spec_values):
            df_pvt_spec = df_pvt.loc[[x in self.spec_values for x in df_pvt.index], ].copy()
            df_pvt = df_pvt.loc[[x not in self.spec_values for x in df_pvt.index], ].copy()
            max_bins = max_bins - df_pvt_spec.shape[0]

        # 自动分组
        if not df_pvt.empty:
            logging.info(f"-------- [{self.var_name} : Auto binning] --------")
            df_pvt = chi2_auto_binning(df_pvt=df_pvt, max_bins=max_bins, min_cnt_in_bin=min_cnt_in_bin, p=chi2_p)

            # 重新计算分组
            cut_points = df_pvt.index

            # 添加需要单独成组的特殊数值
            if bool(self.spec_values) and len(self.spec_values) > 0:
#                 spec_values_upper = [x + 0.1**spec_digit for x in self.spec_values]
                cut_points = np.append(cut_points, self.spec_values)
#                 cut_points = np.append(cut_points, self.spec_values_upper)
#                 cut_points.sort()

#             print("Cut Points After Auto Binning: ", cut_points)

        binning_series = self.apt_binning(
            df=df, 
            points=cut_points, 
            modify=True
            )

        return binning_series

Binning

统一的分箱操作类。

整合了快速分箱、卡方分箱等多种分箱方法,提供统一的接口进行数据分箱操作。 支持等频/等距分箱、决策树分箱、卡方自动分箱等多种策略。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表(将被复制,不修改原数据)

必需
column str

需要分箱的列名

必需
tgt_name str

目标变量名(卡方分箱时必需)

None
nbins int

分箱数量

10
precision int

边界值精度(小数位数)

5
min_bin_prop float

每箱最小样本占比

0.05
include_missing bool

是否包含缺失值

True
equal_freq bool

True为等频分箱,False为等距分箱

True
bin_colnames tuple

分箱结果列名元组

("_bin_num", "_bin_range")
ascending bool

分箱顺序是否升序

True
right bool

区间是否右闭合

True
include_lowest bool

是否包含最小值

False
tree_binning bool

是否使用决策树分箱

False
chi2_method bool

是否使用卡方分箱

False
chi2_p float

卡方检验显著性水平

0.95
init_equi_bins int

初始等频分箱数量

200
fillna any

缺失值填充值

-999999
spec_values list

特殊值列表

[]
random_state int

随机种子

42

属性:

名称 类型 描述
result DataFrame

分箱结果数据

bin_edges ndarray

分箱边界数组

示例:

>>> binner = Binning(data, column='income', tgt_name='default', nbins=10)
>>> binner.run()
>>> result, edges = binner.get_result()
>>> # 使用卡方分箱
>>> binner = Binning(data, column='income', tgt_name='default', 
...                  nbins=10, chi2_method=True, chi2_p=0.95)
>>> binner.run()
源代码位于: Modeling_Tool/Core/Binning_Tool.py
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
class Binning:
    """
    统一的分箱操作类。

    整合了快速分箱、卡方分箱等多种分箱方法,提供统一的接口进行数据分箱操作。
    支持等频/等距分箱、决策树分箱、卡方自动分箱等多种策略。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表(将被复制,不修改原数据)
    column : str
        需要分箱的列名
    tgt_name : str, optional
        目标变量名(卡方分箱时必需)
    nbins : int, default 10
        分箱数量
    precision : int, default 5
        边界值精度(小数位数)
    min_bin_prop : float, default 0.05
        每箱最小样本占比
    include_missing : bool, default True
        是否包含缺失值
    equal_freq : bool, default True
        True为等频分箱,False为等距分箱
    bin_colnames : tuple, default ("_bin_num", "_bin_range")
        分箱结果列名元组
    ascending : bool, default True
        分箱顺序是否升序
    right : bool, default True
        区间是否右闭合
    include_lowest : bool, default False
        是否包含最小值
    tree_binning : bool, default False
        是否使用决策树分箱
    chi2_method : bool, default False
        是否使用卡方分箱
    chi2_p : float, default 0.95
        卡方检验显著性水平
    init_equi_bins : int, default 200
        初始等频分箱数量
    fillna : any, default -999999
        缺失值填充值
    spec_values : list, default []
        特殊值列表
    random_state : int, default 42
        随机种子

    Attributes
    ----------
    result : pandas.DataFrame
        分箱结果数据
    bin_edges : numpy.ndarray
        分箱边界数组

    Examples
    --------
    >>> binner = Binning(data, column='income', tgt_name='default', nbins=10)
    >>> binner.run()
    >>> result, edges = binner.get_result()

    >>> # 使用卡方分箱
    >>> binner = Binning(data, column='income', tgt_name='default', 
    ...                  nbins=10, chi2_method=True, chi2_p=0.95)
    >>> binner.run()
    """

    def __init__(self, data, column, tgt_name=None, nbins=10, precision=5, min_bin_prop=0.05,
                 include_missing=True, equal_freq=True, bin_colnames=("_bin_num", "_bin_range"),
                 ascending=True, right=True, include_lowest=False, tree_binning=False,
                 chi2_method=False, chi2_p=0.95, init_equi_bins=200, fillna=-999999,
                 spec_values=[], random_state=42):
        """
        初始化分箱对象。

        Parameters
        ----------
        data : pandas.DataFrame
            输入数据表
        column : str
            需要分箱的列名
        tgt_name : str, optional
            目标变量名
        nbins : int, default 10
            分箱数量
        precision : int, default 5
            边界值精度
        min_bin_prop : float, default 0.05
            每箱最小样本占比
        include_missing : bool, default True
            是否包含缺失值
        equal_freq : bool, default True
            等频分箱还是等距分箱
        bin_colnames : tuple, default ("_bin_num", "_bin_range")
            分箱结果列名
        ascending : bool, default True
            分箱顺序是否升序
        right : bool, default True
            区间是否右闭合
        include_lowest : bool, default False
            是否包含最小值
        tree_binning : bool, default False
            是否使用决策树分箱
        chi2_method : bool, default False
            是否使用卡方分箱
        chi2_p : float, default 0.95
            卡方检验显著性水平
        init_equi_bins : int, default 200
            初始等频分箱数量
        fillna : any, default -999999
            缺失值填充值
        spec_values : list, default []
            特殊值列表
        random_state : int, default 42
            随机种子
        """
        self.data = data.copy()
        self.original_data = data.copy()
        self.column = column
        self.tgt_name = tgt_name
        self.nbins = nbins
        self.precision = precision
        self.min_bin_prop = min_bin_prop
        self.include_missing = include_missing
        self.equal_freq = equal_freq
        self.bin_colnames = bin_colnames
        self.ascending = ascending
        self.right = right
        self.include_lowest = include_lowest
        self.tree_binning = tree_binning
        self.chi2_method = chi2_method
        self.chi2_p = chi2_p
        self.init_equi_bins = init_equi_bins
        self.fillna = fillna
        self.spec_values = spec_values
        self.random_state = random_state
        self.result = None
        self.bin_edges = None

    def run_quick_binning(self):
        """
        执行快速分箱。

        根据配置参数执行等频或等距分箱,并将结果添加到数据中。

        Returns
        -------
        self
            返回自身以便链式调用
        """
        if isinstance(self.nbins, int):
            self.nbins = int(get_max_nbins(data=self.data, nbins=self.nbins, 
                                            min_bin_prop=self.min_bin_prop))

        if not self.include_missing:
            self.data = self.data.dropna(subset=[self.column])

        labels = None
        binned, self.bin_edges = quick_binning(
            data=self.data, 
            column=self.column, 
            nbins=self.nbins, 
            precision=self.precision, 
            equal_freq=self.equal_freq, 
            labels=labels, 
            right=self.right, 
            include_lowest=self.include_lowest,
            tree_binning=self.tree_binning,
            target=self.tgt_name,
            ascending=self.ascending,
            include_missing=self.include_missing,
            random_state=self.random_state,
            spec_values=self.spec_values
        )

        left_sign = '[' if self.include_lowest else '('
        right_sign = ']' if self.right else ')'
        bin_range_list = get_bin_range(edges=self.bin_edges, precision=self.precision, 
                                         ascending=self.ascending, left_sign=left_sign, 
                                         right_sign=right_sign)

        rename_catlist = [i for i in range(1, len(bin_range_list) + 1)] if not self.include_missing else [i for i in range(0, len(bin_range_list))]
        binned = binned.cat.rename_categories(rename_catlist)

        bin_num_col = self.bin_colnames[0]
        bin_range_col = self.bin_colnames[1]

        self.data[bin_num_col] = binned.astype(object)
        self.data[bin_range_col] = self.data[bin_num_col].apply(
            lambda x: bin_range_list[int(x)] if self.include_missing else bin_range_list[int(x - 1)]
        )

        self.result = self.data
        return self

    def run_chi2_binning(self, init_points=None):
        """
        执行卡方分箱。

        在快速分箱的基础上,进一步使用卡方检验进行自动分箱优化。

        Parameters
        ----------
        init_points : array-like, optional
            初始分箱边界,将作为卡方分箱的起点

        Returns
        -------
        self
            返回自身以便链式调用
        """
        self.data = self.original_data.copy()
        self.data = self.data.reset_index(drop=True)

        df = self.data[[self.column, self.tgt_name]].copy()

        equi_method = "equid"
        if self.equal_freq:
            equi_method = "equif"

        if self.include_missing:
            df[self.column] = df[self.column].fillna(self.fillna)
            spec_values = [self.fillna, *self.spec_values]
        else:
            df = df.dropna()
            self.data = self.data[~pd.isnull(self.data[self.column])]

        nvb = NumVarBinning(var_name=self.column, spec_values=spec_values, 
                             spec_digit=self.precision)  
        binning_series = nvb.auto_binning(
            df=df, 
            tgt_name=self.tgt_name, 
            max_bins=self.nbins, 
            min_prop_in_bin=self.min_bin_prop, 
            equi_method=equi_method,
            equi_bins=self.init_equi_bins, 
            binning_criteria='chi2',
            chi2_p=self.chi2_p,
            init_points=init_points
        )

        bin_edges = cat_2_list(binning_series)

        left_sign = '['
        right_sign = ')'
        if not self.ascending:
            left_sign = '('
            right_sign = ']'
        bin_range_list = get_bin_range(edges=bin_edges, precision=self.precision, 
                                         ascending=self.ascending, left_sign=left_sign, 
                                         right_sign=right_sign)

        binned = binning_series
        rename_catlist = [i for i in range(1, len(bin_range_list) + 1)] if not self.include_missing else [i for i in range(0, len(bin_range_list))]
        binned = binned.cat.rename_categories(rename_catlist)

        bin_num_col = self.bin_colnames[0]
        bin_range_col = self.bin_colnames[1]

        self.data[bin_num_col] = binned.astype(object)
        self.data[bin_range_col] = self.data[bin_num_col].apply(
            lambda x: bin_range_list[int(x)] if self.include_missing else bin_range_list[int(x - 1)]
        )

        self.bin_edges = sorted([np.inf if str(x).lower() == 'inf' else -np.inf if str(x).lower() == '-inf' else x for x in bin_edges])

        self.result = self.data
        return self

    def run(self):
        """
        执行分箱操作。

        根据chi2_method参数决定执行快速分箱还是卡方分箱。

        Returns
        -------
        self
            返回自身以便链式调用
        """
        if self.chi2_method:
            # 先执行快速分箱获取初始边界
            self.run_quick_binning()
            init_points = self.bin_edges.copy()

            # 再执行卡方分箱
            self.run_chi2_binning(init_points=init_points)
        else:
            self.run_quick_binning()

        return self

    def get_result(self, return_edges=True):
        """
        获取分箱结果。

        Parameters
        ----------
        return_edges : bool, default True
            是否返回分箱边界

        Returns
        -------
        tuple or pandas.DataFrame
            如果return_edges为True,返回(result, bin_edges)元组
            否则只返回result
        """
        if return_edges:
            return self.result, self.bin_edges
        return self.result

run_quick_binning

run_quick_binning()

执行快速分箱。

根据配置参数执行等频或等距分箱,并将结果添加到数据中。

返回:

类型 描述
self

返回自身以便链式调用

源代码位于: Modeling_Tool/Core/Binning_Tool.py
def run_quick_binning(self):
    """
    执行快速分箱。

    根据配置参数执行等频或等距分箱,并将结果添加到数据中。

    Returns
    -------
    self
        返回自身以便链式调用
    """
    if isinstance(self.nbins, int):
        self.nbins = int(get_max_nbins(data=self.data, nbins=self.nbins, 
                                        min_bin_prop=self.min_bin_prop))

    if not self.include_missing:
        self.data = self.data.dropna(subset=[self.column])

    labels = None
    binned, self.bin_edges = quick_binning(
        data=self.data, 
        column=self.column, 
        nbins=self.nbins, 
        precision=self.precision, 
        equal_freq=self.equal_freq, 
        labels=labels, 
        right=self.right, 
        include_lowest=self.include_lowest,
        tree_binning=self.tree_binning,
        target=self.tgt_name,
        ascending=self.ascending,
        include_missing=self.include_missing,
        random_state=self.random_state,
        spec_values=self.spec_values
    )

    left_sign = '[' if self.include_lowest else '('
    right_sign = ']' if self.right else ')'
    bin_range_list = get_bin_range(edges=self.bin_edges, precision=self.precision, 
                                     ascending=self.ascending, left_sign=left_sign, 
                                     right_sign=right_sign)

    rename_catlist = [i for i in range(1, len(bin_range_list) + 1)] if not self.include_missing else [i for i in range(0, len(bin_range_list))]
    binned = binned.cat.rename_categories(rename_catlist)

    bin_num_col = self.bin_colnames[0]
    bin_range_col = self.bin_colnames[1]

    self.data[bin_num_col] = binned.astype(object)
    self.data[bin_range_col] = self.data[bin_num_col].apply(
        lambda x: bin_range_list[int(x)] if self.include_missing else bin_range_list[int(x - 1)]
    )

    self.result = self.data
    return self

run_chi2_binning

run_chi2_binning(init_points=None)

执行卡方分箱。

在快速分箱的基础上,进一步使用卡方检验进行自动分箱优化。

参数:

名称 类型 描述 默认
init_points array - like

初始分箱边界,将作为卡方分箱的起点

None

返回:

类型 描述
self

返回自身以便链式调用

源代码位于: Modeling_Tool/Core/Binning_Tool.py
def run_chi2_binning(self, init_points=None):
    """
    执行卡方分箱。

    在快速分箱的基础上,进一步使用卡方检验进行自动分箱优化。

    Parameters
    ----------
    init_points : array-like, optional
        初始分箱边界,将作为卡方分箱的起点

    Returns
    -------
    self
        返回自身以便链式调用
    """
    self.data = self.original_data.copy()
    self.data = self.data.reset_index(drop=True)

    df = self.data[[self.column, self.tgt_name]].copy()

    equi_method = "equid"
    if self.equal_freq:
        equi_method = "equif"

    if self.include_missing:
        df[self.column] = df[self.column].fillna(self.fillna)
        spec_values = [self.fillna, *self.spec_values]
    else:
        df = df.dropna()
        self.data = self.data[~pd.isnull(self.data[self.column])]

    nvb = NumVarBinning(var_name=self.column, spec_values=spec_values, 
                         spec_digit=self.precision)  
    binning_series = nvb.auto_binning(
        df=df, 
        tgt_name=self.tgt_name, 
        max_bins=self.nbins, 
        min_prop_in_bin=self.min_bin_prop, 
        equi_method=equi_method,
        equi_bins=self.init_equi_bins, 
        binning_criteria='chi2',
        chi2_p=self.chi2_p,
        init_points=init_points
    )

    bin_edges = cat_2_list(binning_series)

    left_sign = '['
    right_sign = ')'
    if not self.ascending:
        left_sign = '('
        right_sign = ']'
    bin_range_list = get_bin_range(edges=bin_edges, precision=self.precision, 
                                     ascending=self.ascending, left_sign=left_sign, 
                                     right_sign=right_sign)

    binned = binning_series
    rename_catlist = [i for i in range(1, len(bin_range_list) + 1)] if not self.include_missing else [i for i in range(0, len(bin_range_list))]
    binned = binned.cat.rename_categories(rename_catlist)

    bin_num_col = self.bin_colnames[0]
    bin_range_col = self.bin_colnames[1]

    self.data[bin_num_col] = binned.astype(object)
    self.data[bin_range_col] = self.data[bin_num_col].apply(
        lambda x: bin_range_list[int(x)] if self.include_missing else bin_range_list[int(x - 1)]
    )

    self.bin_edges = sorted([np.inf if str(x).lower() == 'inf' else -np.inf if str(x).lower() == '-inf' else x for x in bin_edges])

    self.result = self.data
    return self

run

run()

执行分箱操作。

根据chi2_method参数决定执行快速分箱还是卡方分箱。

返回:

类型 描述
self

返回自身以便链式调用

源代码位于: Modeling_Tool/Core/Binning_Tool.py
def run(self):
    """
    执行分箱操作。

    根据chi2_method参数决定执行快速分箱还是卡方分箱。

    Returns
    -------
    self
        返回自身以便链式调用
    """
    if self.chi2_method:
        # 先执行快速分箱获取初始边界
        self.run_quick_binning()
        init_points = self.bin_edges.copy()

        # 再执行卡方分箱
        self.run_chi2_binning(init_points=init_points)
    else:
        self.run_quick_binning()

    return self

get_result

get_result(return_edges=True)

获取分箱结果。

参数:

名称 类型 描述 默认
return_edges bool

是否返回分箱边界

True

返回:

类型 描述
tuple or DataFrame

如果return_edges为True,返回(result, bin_edges)元组 否则只返回result

源代码位于: Modeling_Tool/Core/Binning_Tool.py
def get_result(self, return_edges=True):
    """
    获取分箱结果。

    Parameters
    ----------
    return_edges : bool, default True
        是否返回分箱边界

    Returns
    -------
    tuple or pandas.DataFrame
        如果return_edges为True,返回(result, bin_edges)元组
        否则只返回result
    """
    if return_edges:
        return self.result, self.bin_edges
    return self.result

get_max_nbins

get_max_nbins(data, nbins, min_bin_prop=0.05)

根据给定的最小分箱比例计算最大分箱数。

根据数据总量和最小分箱比例,计算能够满足最小样本数要求的最大分箱数。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
nbins int

期望的分箱数量

必需
min_bin_prop float

每个分箱最小样本占比

0.05

返回:

类型 描述
int

可行的最大分箱数

示例:

>>> get_max_nbins(data, nbins=10, min_bin_prop=0.05)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def get_max_nbins(data, nbins, min_bin_prop = 0.05):
    """
    根据给定的最小分箱比例计算最大分箱数。

    根据数据总量和最小分箱比例,计算能够满足最小样本数要求的最大分箱数。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    nbins : int
        期望的分箱数量
    min_bin_prop : float, default 0.05
        每个分箱最小样本占比

    Returns
    -------
    int
        可行的最大分箱数

    Examples
    --------
    >>> get_max_nbins(data, nbins=10, min_bin_prop=0.05)
    """

    n = data.shape[0]
    if n == 0:
        return nbins   # 空数据无法分箱,返回原值

    min_bin_size = min_bin_prop * n

    if min_bin_size == 0:
            # 此时 min_bin_prop == 0,退化为最多 nbins 个箱
            return min(nbins, n)

    nbins = min(nbins, max(5, n // min_bin_size))
    return nbins

get_decision_tree_binning_edges

get_decision_tree_binning_edges(feature, target, max_leaf_nodes=5, min_samples_leaf=0.05, random_state=42, missing_ref_value=None, spec_values=[])

使用决策树对连续变量进行最优分箱。

通过决策树分类算法寻找最优的分箱边界点,适用于连续型变量的自动化分箱。 算法会自动处理缺失值和特殊值,并返回合适的分箱边界。

参数:

名称 类型 描述 默认
feature array - like

待分箱的连续特征,Pandas Series或一维数组

必需
target array - like

目标变量,二分类标签(Pandas Series或一维数组)

必需
max_leaf_nodes int

决策树最大叶节点数,即最大分箱数

5
min_samples_leaf float

叶节点最小样本比例,默认为0.05(5%)

0.05
random_state int

随机种子,确保结果可重现

42
missing_ref_value any

缺失值参考值,该值将被视为缺失

None
spec_values list

特殊数值列表,这些值将被独立分箱

[]

返回:

名称 类型 描述
bin_edges list

分箱边界列表

示例:

>>> edges = get_decision_tree_binning_edges(feature, target, max_leaf_nodes=5)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def get_decision_tree_binning_edges(feature, target, max_leaf_nodes=5, min_samples_leaf=0.05, random_state=42, missing_ref_value = None, spec_values = []):
    """
    使用决策树对连续变量进行最优分箱。

    通过决策树分类算法寻找最优的分箱边界点,适用于连续型变量的自动化分箱。
    算法会自动处理缺失值和特殊值,并返回合适的分箱边界。

    Parameters
    ----------
    feature : array-like
        待分箱的连续特征,Pandas Series或一维数组
    target : array-like
        目标变量,二分类标签(Pandas Series或一维数组)
    max_leaf_nodes : int, default 5
        决策树最大叶节点数,即最大分箱数
    min_samples_leaf : float, default 0.05
        叶节点最小样本比例,默认为0.05(5%)
    random_state : int, default 42
        随机种子,确保结果可重现
    missing_ref_value : any, optional
        缺失值参考值,该值将被视为缺失
    spec_values : list, optional
        特殊数值列表,这些值将被独立分箱

    Returns
    -------
    bin_edges : list
        分箱边界列表

    Examples
    --------
    >>> edges = get_decision_tree_binning_edges(feature, target, max_leaf_nodes=5)
    """
    from sklearn.tree import DecisionTreeClassifier, export_text
    from sklearn.utils import check_random_state

    if missing_ref_value:
        feature = np.where(feature == missing_ref_value, np.nan, feature)

    # 数据预处理:移除缺失值
    df = pd.DataFrame({
        'feature': feature,
        'target': target
    }).dropna()

    if len(spec_values) > 0:
        df = df[~df["feature"].isin(spec_values)]

    feature_clean = df['feature']
    target_clean = df['target']

    # 如果特征方差为0或几乎为0,无法分箱
    if feature_clean.nunique() <= 1:
        logger.info("警告: 特征方差为0,无法分箱")
        return [feature_clean.min(), feature_clean.max()], pd.cut(feature, bins=[feature_clean.min(), feature_clean.max()])

    # 重塑特征为2D数组以适配sklearn
    X = feature_clean.values.reshape(-1, 1)
    y = target_clean.values

    # 创建决策树分类器
    tree_model = DecisionTreeClassifier(
        max_leaf_nodes=max_leaf_nodes,
        min_samples_leaf=min_samples_leaf,
        random_state=random_state
    )

    # 拟合决策树模型
    tree_model.fit(X, y)

    # 提取分箱边界
    threshold = tree_model.tree_.threshold
    feature_min = feature_clean.min()
    feature_max = feature_clean.max()

    # 获取非叶子节点的分割阈值并排序
    bin_edges = sorted([th for th in threshold if th != -2])

    # 添加最小值和最大值作为边界
#     bin_edges = [feature_min] + bin_edges + [feature_max]

    # 移除可能重复的边界
    bin_edges = sorted(list(set(bin_edges)))
    if missing_ref_value:
        bin_edges = sorted(list(set(bin_edges + [missing_ref_value])))

#     print(tree_model.get_params())

    return bin_edges

cre_pvt

cre_pvt(df, var_name, tgt_name)

生成以变量为行,目标变量为列的数据透视表。

根据指定变量和目标变量生成分组统计透视表,包含每组的负样本数、正样本数、 总样本数以及目标转化率。

参数:

名称 类型 描述 默认
df DataFrame

数据表

必需
var_name str

变量名(分组依据)

必需
tgt_name str

目标变量名(二分类标签,0和1)

必需

返回:

名称 类型 描述
df_pvt DataFrame

变量数据透视表,包含列: - 0: 负样本数 - 1: 正样本数 - n: 总样本数 - tr: 目标转化率(正样本占比)

示例:

>>> df_pvt = cre_pvt(df, var_name='income', tgt_name='default')
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def cre_pvt(df, var_name, tgt_name):
    """
    生成以变量为行,目标变量为列的数据透视表。

    根据指定变量和目标变量生成分组统计透视表,包含每组的负样本数、正样本数、
    总样本数以及目标转化率。

    Parameters
    ----------
    df : pandas.DataFrame
        数据表
    var_name : str
        变量名(分组依据)
    tgt_name : str
        目标变量名(二分类标签,0和1)

    Returns
    -------
    df_pvt : pandas.DataFrame
        变量数据透视表,包含列:
        - 0: 负样本数
        - 1: 正样本数
        - n: 总样本数
        - tr: 目标转化率(正样本占比)

    Examples
    --------
    >>> df_pvt = cre_pvt(df, var_name='income', tgt_name='default')
    """

    df_pvt = df.groupby(var_name)[tgt_name].value_counts().unstack().fillna(0)
    df_pvt["n"] = df_pvt[0] + df_pvt[1]
    df_pvt["tr"] = df_pvt[1] / df_pvt["n"]

    return df_pvt

merge_bins

merge_bins(df_pvt, ilocs)

基于位置编号的变量组合并。

将指定位置的分组进行合并,计算合并后的统计指标(样本数、转化率等), 并更新卡方值和转化率差异。

特别注意: 不可重置df_pvt的index

参数:

名称 类型 描述 默认
df_pvt DataFrame

变量数据透视表,通过cre_pvt函数生成

必需
ilocs list

待合并的位置编号列表(如[0,1]表示合并前两个分组)

必需

返回:

名称 类型 描述
df_pvt_new DataFrame

合并后的变量数据透视表

示例:

>>> df_pvt = merge_bins(df_pvt, ilocs=[0, 1])
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def merge_bins(df_pvt, ilocs):
    """
    基于位置编号的变量组合并。

    将指定位置的分组进行合并,计算合并后的统计指标(样本数、转化率等),
    并更新卡方值和转化率差异。

    特别注意: 不可重置df_pvt的index

    Parameters
    ----------
    df_pvt : pandas.DataFrame
        变量数据透视表,通过cre_pvt函数生成
    ilocs : list
        待合并的位置编号列表(如[0,1]表示合并前两个分组)

    Returns
    -------
    df_pvt_new : pandas.DataFrame
        合并后的变量数据透视表

    Examples
    --------
    >>> df_pvt = merge_bins(df_pvt, ilocs=[0, 1])
    """
    ilocs.sort()
    df = df_pvt.copy()

    # 汇总字段值至最小位置序号
    idxes = df.index
    l_idx = idxes[ilocs[0]]
    df.loc[l_idx, [0, 1, "n"]] = np.apply_over_axes(np.sum, df.loc[idxes[ilocs], [0, 1, "n"]], axes=0)[0]
    df.loc[l_idx, "tr"] = df.loc[l_idx, 1] / df.loc[l_idx, "n"]
    df.drop(index=idxes[ilocs[1:]], inplace=True)

    # 重新计算 tr_diff 和 chisq
    idxes = df.index
    if "tr_diff" in df.columns:
        if ilocs[0]+1 < df.shape[0]:
            df.loc[idxes[ilocs[0]], "tr_diff"] = df.loc[idxes[ilocs[0]+1], "tr"] - df.loc[idxes[ilocs[0]], "tr"]
        else:
            df.loc[idxes[ilocs[0]], "tr_diff"] = 0

    if "chisq" in df.columns:
        if ilocs[0]+1 < df.shape[0]:
            df.loc[idxes[ilocs[0]], "chisq"] = chi2_contingency(observed_laplace(df.loc[idxes[[ilocs[0], ilocs[0]+1]], [0, 1]]), correction=False)[0]
        else:
            df.loc[idxes[ilocs[0]], "chisq"] = np.inf

    return df

observed_laplace

observed_laplace(observed, digit=6)

对列联表数值进行拉普拉斯修正。

在列联表的每个单元格数值上加上一个小量(0.1**digit),以避免 零计数导致的卡方检验问题。

参数:

名称 类型 描述 默认
observed array_like

列联表(二维数组或类似结构)

必需
digit int

对观测值增加小数的位数, 若为-1则增加整数1

6

返回:

名称 类型 描述
obs_laplace ndarray

拉普拉斯修正后的列联表

示例:

>>> observed = [[10, 20], [30, 40]]
>>> obs_laplace = observed_laplace(observed, digit=6)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def observed_laplace(observed, digit=6):
    """
    对列联表数值进行拉普拉斯修正。

    在列联表的每个单元格数值上加上一个小量(0.1**digit),以避免
    零计数导致的卡方检验问题。

    Parameters
    ----------
    observed : array_like
        列联表(二维数组或类似结构)
    digit : int, default 6
        对观测值增加小数的位数, 若为-1则增加整数1

    Returns
    -------
    obs_laplace : numpy.ndarray
        拉普拉斯修正后的列联表

    Examples
    --------
    >>> observed = [[10, 20], [30, 40]]
    >>> obs_laplace = observed_laplace(observed, digit=6)
    """
    obs_laplace = np.asarray(observed) + 0.1**digit

    return obs_laplace

cat_2_list

cat_2_list(bin_series)

将分箱后的Categorical序列转换为边界值列表。

从Categorical类型的数据中提取所有区间边界,返回所有左边界和右边界 组成的去重列表。

参数:

名称 类型 描述 默认
bin_series Categorical

分箱后的Categorical序列

必需

返回:

类型 描述
list

包含所有区间边界值的列表

示例:

>>> edges = cat_2_list(binning_series)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def cat_2_list(bin_series):
    """
    将分箱后的Categorical序列转换为边界值列表。

    从Categorical类型的数据中提取所有区间边界,返回所有左边界和右边界
    组成的去重列表。

    Parameters
    ----------
    bin_series : pandas.Categorical
        分箱后的Categorical序列

    Returns
    -------
    list
        包含所有区间边界值的列表

    Examples
    --------
    >>> edges = cat_2_list(binning_series)
    """

    interval_list = bin_series.cat.categories.tolist()
    edges_res = set()
    for x in interval_list:
        edges_res.add(x.left)
        edges_res.add(x.right)
    edges_res = list(edges_res)

    return edges_res

get_bin_range

get_bin_range(edges, precision=5, ascending=False, left_sign='(', right_sign=']')

根据分箱边界生成区间字符串描述列表。

将分箱边界点列表转换为带区间符号的字符串描述列表,支持自定义 区间开闭符号和精度。

参数:

名称 类型 描述 默认
edges array - like

分箱边界值列表

必需
precision int

边界值精度(小数位数)

5
ascending bool

是否升序排列

False
left_sign str

左区间符号,'['表示包含,'('表示不包含

'('
right_sign str

右区间符号,']'表示包含,')'表示不包含

']'

返回:

类型 描述
list

区间字符串描述列表

示例:

>>> edges = [0, 10, 20, 30]
>>> ranges = get_bin_range(edges, precision=0, ascending=True)
['[0, 10]', '[10, 20]', '[20, 30]']
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def get_bin_range(edges, precision = 5, ascending = False, left_sign = '(', right_sign = ']'):
    """
    根据分箱边界生成区间字符串描述列表。

    将分箱边界点列表转换为带区间符号的字符串描述列表,支持自定义
    区间开闭符号和精度。

    Parameters
    ----------
    edges : array-like
        分箱边界值列表
    precision : int, default 5
        边界值精度(小数位数)
    ascending : bool, default False
        是否升序排列
    left_sign : str, default '('
        左区间符号,'['表示包含,'('表示不包含
    right_sign : str, default ']'
        右区间符号,']'表示包含,')'表示不包含

    Returns
    -------
    list
        区间字符串描述列表

    Examples
    --------
    >>> edges = [0, 10, 20, 30]
    >>> ranges = get_bin_range(edges, precision=0, ascending=True)
    ['[0, 10]', '[10, 20]', '[20, 30]']
    """

    i = 0
    reverse = not ascending
    edges = sorted([round(x, precision) for x in edges], reverse = reverse)
    res = []
    while i < len(edges) - 1:
        left = edges[i]
        right = edges[i+1]
        res.append(f"{left_sign}{left}, {right}{right_sign}")
        left = right
        i += 1

    return res

get_bin_range_list

get_bin_range_list(data, col='_bin_range')

将分箱区间字符串列转换为边界值列表。

解析DataFrame中的分箱区间字符串列(如"[0, 10)", "(10, 20]"等), 提取所有唯一的边界值并排序返回。

参数:

名称 类型 描述 默认
data DataFrame

包含分箱区间列的DataFrame

必需
col str

分箱区间列名

"_bin_range"

返回:

类型 描述
list

去重并排序后的边界值列表

示例:

>>> unique_range = get_bin_range_list(data, col="bin_range")
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def get_bin_range_list(data, col = "_bin_range"):
    """
    将分箱区间字符串列转换为边界值列表。

    解析DataFrame中的分箱区间字符串列(如"[0, 10)", "(10, 20]"等),
    提取所有唯一的边界值并排序返回。

    Parameters
    ----------
    data : pandas.DataFrame
        包含分箱区间列的DataFrame
    col : str, default "_bin_range"
        分箱区间列名

    Returns
    -------
    list
        去重并排序后的边界值列表

    Examples
    --------
    >>> unique_range = get_bin_range_list(data, col="bin_range")
    """

    data["bin_value_list"] = data[col].apply(lambda x: x.replace("[", "")\
                                                                  .replace("]", "")\
                                                                  .replace("(", "")\
                                                                  .replace(")", "")\
                                                      .split(","))
    bin_range = [np.inf if v.strip() == 'inf' else -np.inf if v.strip() == '-inf' else float(v.strip()) for x in data["bin_value_list"].tolist() for v in x]

    unique_range = []
    for x in bin_range:
        if x not in unique_range:
            unique_range.append(x)

    unique_range.sort()

    return unique_range

chi2_auto_binning

chi2_auto_binning(df_pvt, max_bins, min_cnt_in_bin, p=0.95)

基于卡方检验的自动分箱。

通过卡方检验判断相邻分箱是否应该合并,迭代进行以下步骤: 1. 首先处理样本量不满足最小要求的头尾分箱 2. 然后处理样本量不足的中间分箱 3. 最后进行卡方检验合并不独立的相邻分箱

参数:

名称 类型 描述 默认
df_pvt DataFrame

变量数据透视表,通过cre_pvt函数生成,包含列:0, 1, n, tr

必需
max_bins int

最大分组数量

必需
min_cnt_in_bin int

每组最小样本数

必需
p float

用于计算自由度为1的卡方分布分位数的概率值. 当相邻组卡方值小于该值时, 认为不独立, 进行合并. 值越大独立性判断越严格

0.95

返回:

名称 类型 描述
df_pvt DataFrame

合并后的变量数据透视表

示例:

>>> df_pvt = chi2_auto_binning(df_pvt, max_bins=10, min_cnt_in_bin=100, p=0.95)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def chi2_auto_binning(df_pvt, max_bins, min_cnt_in_bin, p=0.95):
    """
    基于卡方检验的自动分箱。

    通过卡方检验判断相邻分箱是否应该合并,迭代进行以下步骤:
    1. 首先处理样本量不满足最小要求的头尾分箱
    2. 然后处理样本量不足的中间分箱
    3. 最后进行卡方检验合并不独立的相邻分箱

    Parameters
    ----------
    df_pvt : pandas.DataFrame
        变量数据透视表,通过cre_pvt函数生成,包含列:0, 1, n, tr
    max_bins : int
        最大分组数量
    min_cnt_in_bin : int
        每组最小样本数
    p : float, default 0.95
        用于计算自由度为1的卡方分布分位数的概率值. 当相邻组卡方值小于该值时, 
        认为不独立, 进行合并. 值越大独立性判断越严格

    Returns
    -------
    df_pvt : pandas.DataFrame
        合并后的变量数据透视表

    Examples
    --------
    >>> df_pvt = chi2_auto_binning(df_pvt, max_bins=10, min_cnt_in_bin=100, p=0.95)
    """
    # 计算样本数是否满足最小样本数要求
    if np.sum(df_pvt["n"]) <= min_cnt_in_bin:
        df_pvt = merge_bins(df_pvt=df_pvt, ilocs=list(range(df_pvt.shape[0])))
        logging.info("Merge ilocs: all")
    else:
        # 头尾分箱
        # 计算头尾两侧Bin是否满足最小样本要求
        csumn_asc = np.cumsum(df_pvt["n"])
        ilocs_head = np.min(np.where(csumn_asc >= min_cnt_in_bin))
        if ilocs_head > 0:
            ori_bins = df_pvt.shape[0]
            df_pvt = merge_bins(df_pvt=df_pvt, ilocs=list(range(ilocs_head+1)))
            logging.info(f"HeadMerge: ilocs={[0, ilocs_head+1]}, bins={ori_bins} -> {df_pvt.shape[0]}")

        csumn_desc = np.cumsum(df_pvt["n"][::-1])
        ilocs_tail = np.min(np.where(csumn_desc >= min_cnt_in_bin))
        if ilocs_tail > 0:
            ori_bins = df_pvt.shape[0]
            df_pvt = merge_bins(df_pvt=df_pvt, ilocs=list(range(ori_bins-(ilocs_tail+1), ori_bins)))
            logging.info(f"TailMerge: ilocs={[ori_bins-(ilocs_tail+1), ori_bins]}, bins={ori_bins} -> {df_pvt.shape[0]}")


        # 自动分箱
        # 计算初始分组每组卡方值、与后一组合并后的分布散度及增益值
        chisq = [chi2_contingency(observed_laplace(df_pvt.loc[df_pvt.index[[i, i+1]], [0, 1]]), correction=False)[0] for i in range(df_pvt.shape[0] - 1)]
        chisq.append(np.inf)
        tr_diff = np.diff(df_pvt["tr"])
        tr_diff = np.append(tr_diff, 0)
        df_pvt["chisq"] = chisq
        df_pvt["tr_diff"] = tr_diff

        r = 0
        ori_bins = df_pvt.shape[0]
        while df_pvt.shape[0] > max_bins or any(df_pvt["n"] < min_cnt_in_bin):
            r += 1
            # 优先处理样本量不满足最低要求的Bin, 向前或向后合并
            if any(df_pvt["n"] < min_cnt_in_bin):
                iloc = np.min(np.where(df_pvt["n"] < min_cnt_in_bin))
                # 计算变量组合并方向:位置与单调性
                if iloc == 0:
                    iloc_merge = iloc + 1
                elif iloc == df_pvt.shape[0]-1:
                    iloc_merge = iloc - 1
                else:
                    idxes = df_pvt.index
                    if np.abs(df_pvt.loc[idxes[iloc-1], "tr_diff"]) <= np.abs(df_pvt.loc[idxes[iloc], "tr_diff"]):
                        iloc_merge = iloc - 1
                    else:
                        iloc_merge = iloc + 1
            # 其次处理卡方值低的Bin, 向后合并
            else:
                chisq_list = df_pvt["chisq"].to_list()
                iloc = chisq_list.index(np.min(chisq_list))
                iloc_merge = iloc + 1

            # 变量组合并
            df_pvt = merge_bins(df_pvt=df_pvt, ilocs=[iloc, iloc_merge])
        logging.info(f"LoopMerge: round={r}, bins={ori_bins} -> {df_pvt.shape[0]}")


        # 卡方检验分箱
        r = 0
        ori_bins = df_pvt.shape[0]
        while df_pvt["chisq"].min() <= chi2.ppf(p, 1):
            r += 1
            chisq_list = df_pvt["chisq"].to_list()
            iloc = chisq_list.index(np.min(chisq_list))
            df_pvt = merge_bins(df_pvt=df_pvt, ilocs=[iloc, iloc + 1])
        logging.info(f"Chi2Merge: round={r}, bins={ori_bins} -> {df_pvt.shape[0]}")

    return df_pvt

quick_binning

quick_binning(data, column, labels=None, nbins=10, precision=5, equal_freq=True, right=True, include_lowest=False, min_bin_prop=0.05, ascending=True, include_missing=False, tree_binning=False, target=None, random_state=42, fillna=-999999, spec_values=[])

快速分箱函数。

对数据进行快速等频或等距分箱,支持多种分箱策略: - 等频分箱:按分位数切分,保证每箱样本数接近 - 等距分箱:按数值区间均匀切分 - 决策树分箱:使用决策树寻找最优切分点

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
column str

需要分箱的列名

必需
labels array - like

自定义分箱标签

None
nbins int or list / tuple

分箱数量(整数)或指定分箱边界(列表或元组)

10
precision int

边界值精度(小数位数)

5
equal_freq bool

True为等频分箱,False为等距分箱

True
right bool

区间是否右闭合

True
include_lowest bool

是否包含最小值

False
min_bin_prop float

每箱最小样本占比

0.05
ascending bool

分箱顺序是否升序

True
include_missing bool

是否包含缺失值

False
tree_binning bool

是否使用决策树分箱

False
target str

目标变量名(决策树分箱时必需)

None
random_state int

随机种子

42
fillna any

缺失值填充值

-999999
spec_values list

特殊值列表,将独立成箱

[]

返回:

名称 类型 描述
binned Categorical

分箱后的序列

bin_edges ndarray

分箱边界数组

示例:

>>> binned, edges = quick_binning(data, 'income', nbins=10, equal_freq=True)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def quick_binning(data, column, labels = None, nbins = 10, precision = 5, equal_freq = True, right = True, include_lowest = False, 
                  min_bin_prop = 0.05, ascending = True, include_missing = False, tree_binning = False, target = None, random_state=42, 
                  fillna = -999999, spec_values = []):
    """
    快速分箱函数。

    对数据进行快速等频或等距分箱,支持多种分箱策略:
    - 等频分箱:按分位数切分,保证每箱样本数接近
    - 等距分箱:按数值区间均匀切分
    - 决策树分箱:使用决策树寻找最优切分点

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    column : str
        需要分箱的列名
    labels : array-like, optional
        自定义分箱标签
    nbins : int or list/tuple, default 10
        分箱数量(整数)或指定分箱边界(列表或元组)
    precision : int, default 5
        边界值精度(小数位数)
    equal_freq : bool, default True
        True为等频分箱,False为等距分箱
    right : bool, default True
        区间是否右闭合
    include_lowest : bool, default False
        是否包含最小值
    min_bin_prop : float, default 0.05
        每箱最小样本占比
    ascending : bool, default True
        分箱顺序是否升序
    include_missing : bool, default False
        是否包含缺失值
    tree_binning : bool, default False
        是否使用决策树分箱
    target : str, optional
        目标变量名(决策树分箱时必需)
    random_state : int, default 42
        随机种子
    fillna : any, default -999999
        缺失值填充值
    spec_values : list, default []
        特殊值列表,将独立成箱

    Returns
    -------
    binned : pandas.Categorical
        分箱后的序列
    bin_edges : numpy.ndarray
        分箱边界数组

    Examples
    --------
    >>> binned, edges = quick_binning(data, 'income', nbins=10, equal_freq=True)
    """

    binning_series = data[column].round(precision)

    if include_missing:
        binning_series = binning_series.fillna(fillna)
    else:
        binning_series = binning_series.dropna()

    # Determine Binning Intervals
    if isinstance(nbins, int):

        value_no_spec_value = binning_series[~binning_series.isin(spec_values)]
#         print("Tree Binning No Spec Value: ", value_no_spec_value)

        if len(value_no_spec_value) == 0:
            # 返回一个默认的空分箱结果
            return pd.Series(), []

        if equal_freq:
            nbins = int(get_max_nbins(data, nbins, min_bin_prop))
            breakpoints = np.percentile(value_no_spec_value, [100 / nbins * i for i in range(1, nbins)])
            breakpoints = list(breakpoints) + spec_values
        else:
            nbins = int(get_max_nbins(data, nbins, min_bin_prop))
            min_value = binning_series.replace(fillna, np.nan).min() if include_missing else value_no_spec_value.min()
#             print("MIN Value: ", min_value)
            breakpoints = np.linspace(min_value, value_no_spec_value.max(), nbins + 1)[1:-1]
            breakpoints = np.sort(np.unique(list(breakpoints) + [fillna])) if include_missing else breakpoints
            breakpoints = list(breakpoints) + spec_values

        if tree_binning:
            breakpoints = get_decision_tree_binning_edges(
                                binning_series, 
                                data[target],
                                max_leaf_nodes = nbins,
                                min_samples_leaf = min_bin_prop,
                                random_state = random_state,
                                missing_ref_value = fillna if include_missing else None,
                                spec_values = spec_values
                            )
            breakpoints = list(breakpoints)
#         print("Tree Output: ", breakpoints)
        breakpoints = [round(x, precision) for x in breakpoints]
        fnl_breakpoints = np.sort(np.unique([-np.inf, *breakpoints, np.inf]))
#         print("Final Tree Output: ", fnl_breakpoints)

    if isinstance(nbins, list) or isinstance(nbins, tuple):
        fnl_breakpoints = nbins

    if len(spec_values) > 0:
        fnl_breakpoints = sorted(list(set(list(fnl_breakpoints) + spec_values)))

    binned, bin_edges = pd.cut(
        binning_series, 
        bins = fnl_breakpoints, 
        labels = labels, 
        right = right, 
        include_lowest = include_lowest, 
        retbins = True
    )

    orig_cat = [x for x in binned.cat.categories.tolist()]
    if ascending:
        binned = binned
    else:
        orig_cat.reverse()
        binned = binned.cat.reorder_categories([*orig_cat], ordered=True)

    return binned, bin_edges

chi2_binning

chi2_binning(data, column, nbins=10, precision=5, min_bin_prop=0.05, tgt_name=None, include_missing=True, equal_freq=True, bin_colnames=('_bin_num', '_bin_range'), ascending=True, chi2_p=0.95, init_equi_bins=100, fillna=-999999, spec_values=[], init_points=None)

基于卡方检验的分箱函数。

使用卡方检验自动寻找最优分箱边界,通过卡方检验判断相邻分箱是否应该合并, 最终得到统计上显著的分箱结果。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
column str

需要分箱的列名

必需
nbins int

最大分箱数量

10
precision int

边界值精度(小数位数)

5
min_bin_prop float

每箱最小样本占比

0.05
tgt_name str

目标变量名(二分类标签,0和1)

None
include_missing bool

是否包含缺失值

True
equal_freq bool

True为等频分箱,False为等距分箱

True
bin_colnames tuple

分箱结果列名元组

("_bin_num", "_bin_range")
ascending bool

分箱顺序是否升序

True
chi2_p float

卡方检验显著性水平

0.95
init_equi_bins int

初始等频分箱数量

100
fillna any

缺失值填充值

-999999
spec_values list

特殊值列表

[]
init_points array - like

初始分箱边界,将作为卡方分箱的起点

None

返回:

类型 描述
tuple

(result, bin_edges) - 分箱结果数据框和分箱边界数组

示例:

>>> result, edges = chi2_binning(data, column='income', tgt_name='default', nbins=10)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def chi2_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, tgt_name = None,
                 include_missing = True, equal_freq = True, bin_colnames = ("_bin_num", "_bin_range"), ascending = True, 
                 chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [], init_points = None):
    """
    基于卡方检验的分箱函数。

    使用卡方检验自动寻找最优分箱边界,通过卡方检验判断相邻分箱是否应该合并,
    最终得到统计上显著的分箱结果。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    column : str
        需要分箱的列名
    nbins : int, default 10
        最大分箱数量
    precision : int, default 5
        边界值精度(小数位数)
    min_bin_prop : float, default 0.05
        每箱最小样本占比
    tgt_name : str
        目标变量名(二分类标签,0和1)
    include_missing : bool, default True
        是否包含缺失值
    equal_freq : bool, default True
        True为等频分箱,False为等距分箱
    bin_colnames : tuple, default ("_bin_num", "_bin_range")
        分箱结果列名元组
    ascending : bool, default True
        分箱顺序是否升序
    chi2_p : float, default 0.95
        卡方检验显著性水平
    init_equi_bins : int, default 100
        初始等频分箱数量
    fillna : any, default -999999
        缺失值填充值
    spec_values : list, default []
        特殊值列表
    init_points : array-like, optional
        初始分箱边界,将作为卡方分箱的起点

    Returns
    -------
    tuple
        (result, bin_edges) - 分箱结果数据框和分箱边界数组

    Examples
    --------
    >>> result, edges = chi2_binning(data, column='income', tgt_name='default', nbins=10)
    """

    data = data.reset_index(drop = True)

    df = data[[column, tgt_name]].copy()

    equi_method = "equid"
    if equal_freq:
        equi_method = "equif"

    if include_missing:
        df[column] = df[column].fillna(fillna)
        spec_values = [fillna, *spec_values]
    else:
        df = df.dropna()
        data = data[~pd.isnull(data[column])]

    nvb = NumVarBinning(var_name=column, spec_values=spec_values, spec_digit=precision)  
#     print(init_points)
    binning_series = nvb.auto_binning(
        df=df, 
        tgt_name=tgt_name, 
        max_bins=nbins, 
        min_prop_in_bin=min_bin_prop, 
        equi_method=equi_method,
        equi_bins=init_equi_bins, 
        binning_criteria='chi2',
        chi2_p=chi2_p,
        init_points=init_points)

    bin_num_col = bin_colnames[0]
    bin_range_col = bin_colnames[1]

    bin_edges = cat_2_list(binning_series)

    left_sign='['
    right_sign=')'
    if not ascending:
        left_sign='('
        right_sign=']'
    bin_range_list = get_bin_range(edges = bin_edges, precision = precision, ascending = ascending, left_sign=left_sign, right_sign=right_sign)

    binned = binning_series
    rename_catlist = [i for i in range(1, len(bin_range_list) + 1)] if not include_missing else [i for i in range(0, len(bin_range_list))]
    binned = binned.cat.rename_categories(rename_catlist)
    data[bin_num_col] = binned.astype(object)
#     print(bin_range_list)
#     print(data[bin_num_col])
    data[bin_range_col] = data[bin_num_col].apply(lambda x: bin_range_list[int(x)] if include_missing else bin_range_list[int(x - 1)])

    fnl_res = data

    bin_edges = sorted([np.inf if str(x).lower() == 'inf' else -np.inf if str(x).lower() == '-inf' else x for x in bin_edges])

    return fnl_res, bin_edges

run_binning

run_binning(data, column, nbins=10, precision=5, min_bin_prop=0.05, include_missing=True, equal_freq=True, bin_colnames=('bin_num', 'bin_range'), ascending=False, right=True, include_lowest=False, tree_binning=False, target=None, random_state=42, spec_values=[])

通用分箱函数,支持等频或等距分箱。

对数值型变量进行分箱处理,支持多种配置选项,返回分箱后的数据和边界值。

参数:

名称 类型 描述 默认
data DataFrame

包含数据的DataFrame

必需
column str

需要分箱的列名

必需
nbins int

分箱数量

10
precision int

边界值精度(小数位数)

5
min_bin_prop float

每箱最小样本占比

0.05
include_missing bool

是否包含缺失值

True
equal_freq bool

True为等频分箱,False为等距分箱

True
bin_colnames tuple

分箱结果列名元组

("bin_num", "bin_range")
ascending bool

分箱顺序是否升序

False
right bool

区间是否右闭合

True
include_lowest bool

是否包含最小值

False
tree_binning bool

是否使用决策树分箱

False
target str

目标变量名(决策树分箱时必需)

None
random_state int

随机种子

42
spec_values list

特殊值列表

[]

返回:

类型 描述
tuple

(data, bin_edges) - 添加了分箱列的数据和分箱边界数组

示例:

>>> data, edges = run_binning(data, column='income', nbins=10, equal_freq=True)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def run_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, equal_freq = True, 
                bin_colnames = ("bin_num", "bin_range"), ascending = False, right = True, include_lowest = False, 
                tree_binning = False, target = None, random_state=42, spec_values = []):
    """
    通用分箱函数,支持等频或等距分箱。

    对数值型变量进行分箱处理,支持多种配置选项,返回分箱后的数据和边界值。

    Parameters
    ----------
    data : pandas.DataFrame
        包含数据的DataFrame
    column : str
        需要分箱的列名
    nbins : int, default 10
        分箱数量
    precision : int, default 5
        边界值精度(小数位数)
    min_bin_prop : float, default 0.05
        每箱最小样本占比
    include_missing : bool, default True
        是否包含缺失值
    equal_freq : bool, default True
        True为等频分箱,False为等距分箱
    bin_colnames : tuple, default ("bin_num", "bin_range")
        分箱结果列名元组
    ascending : bool, default False
        分箱顺序是否升序
    right : bool, default True
        区间是否右闭合
    include_lowest : bool, default False
        是否包含最小值
    tree_binning : bool, default False
        是否使用决策树分箱
    target : str, optional
        目标变量名(决策树分箱时必需)
    random_state : int, default 42
        随机种子
    spec_values : list, default []
        特殊值列表

    Returns
    -------
    tuple
        (data, bin_edges) - 添加了分箱列的数据和分箱边界数组

    Examples
    --------
    >>> data, edges = run_binning(data, column='income', nbins=10, equal_freq=True)

    """

    # 新增保护:如果数据为空或指定列全为缺失,直接返回占位结果
    if data.empty or data[column].isnull().all():
        # 返回一个包含缺失分箱的默认结果
        bin_num_col, bin_range_col = bin_colnames
        data = data.copy()
        data[bin_num_col] = 0 if include_missing else 1
        data[bin_range_col] = "Missing" if include_missing else "All"
        return data, []

    bin_num_col = bin_colnames[0]
    bin_range_col = bin_colnames[1]

    if isinstance(nbins, int):
        nbins = int(get_max_nbins(data = data, nbins = nbins, min_bin_prop = min_bin_prop))

    if not include_missing:
        data = data.dropna(subset = [column])

    labels = None
    binned, bin_edges = quick_binning(data = data, 
                                      column = column, 
                                      nbins = nbins, 
                                      precision = precision, 
                                      equal_freq = equal_freq, 
                                      labels = labels, 
                                      right = right, 
                                      include_lowest = include_lowest,
                                      tree_binning = tree_binning,
                                      target = target,
                                      ascending = ascending,
                                      include_missing = include_missing,
                                      random_state = random_state,
                                      spec_values = spec_values)

    left_sign='[' if include_lowest else '('
    right_sign=']' if right else ')'
    bin_range_list = get_bin_range(edges = bin_edges, precision = precision, ascending = ascending, left_sign=left_sign, right_sign=right_sign)

    rename_catlist = [i for i in range(1, len(bin_range_list) + 1)] if not include_missing else [i for i in range(0, len(bin_range_list))]
    binned = binned.cat.rename_categories(rename_catlist)
    data[bin_num_col] = binned.astype(object)
    data[bin_range_col] = data[bin_num_col].apply(lambda x: bin_range_list[int(x)] if include_missing else bin_range_list[int(x - 1)])

    return data, bin_edges

super_binning

super_binning(data, score, dep, nbins=10, precision=5, min_bin_prop=0.05, include_missing=True, equal_freq=True, chi2_method=False, chi2_p=0.95, init_equi_bins=2000, fillna=-999999, spec_values=[], tree_binning=False, random_state=42, return_edges=False, ascending=True, bin_colnames=('_bin_num', '_bin_range'))

超级分箱函数,整合多种分箱策略。

提供统一的分箱接口,支持基础分箱和卡方分箱两种模式, 可以根据参数配置自动选择合适的分箱策略。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
score str

需要分箱的分数/数值列名

必需
dep str

目标变量名(二分类标签,0和1)

必需
nbins int

最大分箱数量

10
precision int

边界值精度(小数位数)

5
min_bin_prop float

每箱最小样本占比

0.05
include_missing bool

是否包含缺失值

True
equal_freq bool

True为等频分箱,False为等距分箱

True
chi2_method bool

是否使用卡方分箱进行精细化

False
chi2_p float

卡方检验显著性水平

0.95
init_equi_bins int

初始等频分箱数量(卡方分箱前)

2000
fillna any

缺失值填充值

-999999
spec_values list

特殊值列表

[]
tree_binning bool

是否使用决策树分箱

False
random_state int

随机种子

42
return_edges bool

是否返回分箱边界

False
ascending bool

分箱顺序是否升序

True
bin_colnames tuple

分箱结果列名元组

("_bin_num", "_bin_range")

返回:

类型 描述
DataFrame or tuple

如果return_edges为False,返回分箱结果数据 如果return_edges为True,返回(result, edges)元组

示例:

>>> # 基础分箱
>>> result = super_binning(data, score='income', dep='default', nbins=10)
>>> # 卡方分箱
>>> result, edges = super_binning(data, score='income', dep='default', 
...                                nbins=10, chi2_method=True, return_edges=True)
源代码位于: Modeling_Tool/Core/Binning_Tool.py
def super_binning(data, score, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, 
                  equal_freq = True, chi2_method = False, chi2_p = 0.95, init_equi_bins = 2000, fillna = -999999, 
                  spec_values = [], tree_binning = False, random_state=42, return_edges = False, ascending = True,
                  bin_colnames = ("_bin_num", "_bin_range")):
    """
    超级分箱函数,整合多种分箱策略。

    提供统一的分箱接口,支持基础分箱和卡方分箱两种模式,
    可以根据参数配置自动选择合适的分箱策略。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    score : str
        需要分箱的分数/数值列名
    dep : str
        目标变量名(二分类标签,0和1)
    nbins : int, default 10
        最大分箱数量
    precision : int, default 5
        边界值精度(小数位数)
    min_bin_prop : float, default 0.05
        每箱最小样本占比
    include_missing : bool, default True
        是否包含缺失值
    equal_freq : bool, default True
        True为等频分箱,False为等距分箱
    chi2_method : bool, default False
        是否使用卡方分箱进行精细化
    chi2_p : float, default 0.95
        卡方检验显著性水平
    init_equi_bins : int, default 2000
        初始等频分箱数量(卡方分箱前)
    fillna : any, default -999999
        缺失值填充值
    spec_values : list, default []
        特殊值列表
    tree_binning : bool, default False
        是否使用决策树分箱
    random_state : int, default 42
        随机种子
    return_edges : bool, default False
        是否返回分箱边界
    ascending : bool, default True
        分箱顺序是否升序
    bin_colnames : tuple, default ("_bin_num", "_bin_range")
        分箱结果列名元组

    Returns
    -------
    pandas.DataFrame or tuple
        如果return_edges为False,返回分箱结果数据
        如果return_edges为True,返回(result, edges)元组

    Examples
    --------
    >>> # 基础分箱
    >>> result = super_binning(data, score='income', dep='default', nbins=10)

    >>> # 卡方分箱
    >>> result, edges = super_binning(data, score='income', dep='default', 
    ...                                nbins=10, chi2_method=True, return_edges=True)
    """

    res, output_edges = run_binning(data = data, 
                                    column = score, 
                                    nbins = nbins, 
                                    precision = precision, 
                                    min_bin_prop = min_bin_prop,
                                    include_missing = include_missing, 
                                    equal_freq = equal_freq,
                                    bin_colnames = bin_colnames,
                                    ascending = ascending,
                                    tree_binning = tree_binning,
                                    target = dep,
                                    random_state = random_state,
                                    spec_values = spec_values)

#     print("First Layer Edges: ", output_edges)

    if chi2_method:
        """ Chi2 Binning. """

#         chi2_edges = [x for x in output_edges if x not in spec_values + [-np.inf, np.inf]]
        chi2_edges = [x for x in output_edges if x not in [-np.inf, np.inf]]

#         print("Special Value: ", spec_values)
#         print("Second Layer Inputs: ", chi2_edges)
        res, output_edges = chi2_binning(data = data, 
                                         column = score, 
                                         tgt_name = dep,
                                         nbins = nbins, 
                                         precision = precision, 
                                         min_bin_prop = min_bin_prop, 
                                         include_missing = include_missing, 
                                         equal_freq = equal_freq, 
                                         bin_colnames = bin_colnames, 
                                         ascending = ascending, 
                                         chi2_p = chi2_p, 
                                         init_equi_bins = init_equi_bins, 
                                         fillna = fillna, 
                                         spec_values = spec_values,
                                         init_points = chi2_edges)


    if return_edges:
        return res, output_edges

    return res

ODPS 工具 — ODPS_Tool

ODPS_Tool

ODPSRunner

Bases: object

源代码位于: Modeling_Tool/Core/ODPS_Tool.py
class ODPSRunner(object):
    _wide_schema_patch_lock = threading.RLock()
    _wide_schema_patch_ref_count = 0
    _wide_schema_orig_build = None
    _wide_schema_patch_active = False

    """ODPS执行类
    """
    def __init__(self):
        self.o = ODPS(
            os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
            os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"],
            os.environ.get("ODPS_PROJECT", "mex_anls"),
            endpoint=os.environ.get(
                "ODPS_ENDPOINT",
                "https://service.ap-southeast-1-vpc.maxcompute.aliyun-inc.com/api",
            ),
        )

        options.retry_times = 6         # 请求重试次数
        options.pool_maxsize = 200      # 连接池最大容量
        options.connect_timeout = 3600  # 连接超时
        options.read_timeout = 3600     # 读取超时


    def run_sql(self, sql, to_df=True, n_process=1, csv_path=None):
        """运行SQL并下载结果。

        Parameters
        ----------
        sql : str
            单个 SQL 代码。
        to_df : bool, default True
            是否把结果加载到内存中作为 ``pandas.DataFrame`` 返回。
            若 ``False``, 函数返回**空 DataFrame**, 但仍会下载数据(当 ``csv_path`` 被指定时)。
        n_process : int, default 1
            ``executor.open_reader().to_pandas`` 的并行进程数。
        csv_path : str, default None
            把结果另存为本地 CSV 的路径。**与 ``to_df`` 互相独立**:
                * 只设 ``csv_path`` → 下载 + 写 CSV, 不返回数据 (返回空 DataFrame)
                * 只设 ``to_df=True`` → 下载 + 返回 DataFrame, 不写 CSV
                * 两个都设 → 下载 + 返回 + 写 CSV
                * 都不设 → 只跑 SQL 不下载 (用于 DDL/INSERT 等)

        Returns
        -------
        pandas.DataFrame
            当 ``to_df=True`` 时返回完整数据;
            当 ``to_df=False`` 时返回空 DataFrame (用于占位).

        Notes
        -----
        - **执行**阶段(execute_sql)只跑一次, 无重试.
        - **下载**阶段(to_pandas + to_csv)最多重试 6 次, 适用于网络抖动.
        - 当 SQL 返回列数 > 200 时, 线程安全的 wide-schema patch 会自动 patch ODPS Tunnel,
          防止 HTTP 414 (URI too long).

        Examples
        --------
        >>> odps = ODPSRunner()
        >>> df = odps.run_sql("SELECT * FROM dual LIMIT 10")                # 仅 DataFrame
        >>> df = odps.run_sql("SELECT * FROM dual LIMIT 10", csv_path="x.csv")  # DataFrame + CSV
        >>> _  = odps.run_sql("SELECT * FROM dual LIMIT 10", to_df=False,  # 仅 CSV
        ...                   csv_path="x.csv")
        >>> _  = odps.run_sql("CREATE TABLE t AS SELECT 1")                # 仅执行, 不下载
        """
        # 准备SQL
        sqldesc = sql[:100]+"..." if len(sql)>100 else sql
        logging.info(f"SQL: \n{sqldesc}")

        # 运行SQL(只执行一次,不重试)
        starttime = datetime.now()
        logging.info(f'  execute_sql: {starttime.strftime("%Y-%m-%d %H:%M:%S")}')
        executor = self.o.execute_sql(sql)

        # 决定是否需要下载: 至少满足 to_df=True 或 csv_path 不为空
        should_download = bool(to_df) or bool(csv_path)
        df = pd.DataFrame()

        if should_download:
            k = 6
            for i in range(k):
                try:
                    logging.info(f'  to_pandas: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
                    reader = executor.open_reader()
                    with self._wide_schema_download_patch(reader):
                        if n_process > 1:
                            df = reader.to_pandas(n_process=n_process)
                        else:
                            df = reader.to_pandas()
                    if bool(csv_path):
                        logging.info(f'  to_csv: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
                        df.to_csv(csv_path, index=False)
                    break
                except Exception as e:
                    logging.error(f'  download failed [{i+1}/{k}]: {e}')
                    if i == k - 1:
                        endtime = datetime.now()
                        duration = round((endtime - starttime).total_seconds(), 1)
                        raise SystemError(
                            f'  break: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n'
                        )

        # 当用户显式要求不要 DataFrame 时, 主动释放引用, 节省内存
        if not to_df:
            df = pd.DataFrame()

        endtime = datetime.now()
        duration = round((endtime - starttime).total_seconds(), 1)
        logging.info(f'  done: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n')
        return df

    def download_table(self, table_name, partition=None, n_process=1, csv_path=None):
        """读取表中数据至DataFrame 

        Parameters
        ----------
        table_name : str
            表名
        partition : dict
            分区, 例如: 'dt=2022-01-01,taino=0'
        n_process : int, default 1
            将查询数据转为pandas.DataFrame的进程数
        csv_path : str
            查询数据保存至csv文件路径

        Returns
        -------
        df: pandas.DataFrame
            SQL查询数据结果
        """
        logging.info(f"Table: \n{table_name} {partition}")
        starttime = datetime.now()
        logging.info(f'  to_pandas: {starttime.strftime("%Y-%m-%d %H:%M:%S")}')

        t = self.o.get_table(table_name)
        if bool(partition):
            reader = t.open_reader(partition=partition)
        else:
            reader = t.open_reader()
        if n_process > 1:
            df = reader.to_pandas(n_process=10)
        else:
            df = reader.to_pandas()

        if bool(csv_path):
            logging.info(f'  to_csv: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
            df.to_csv(csv_path, index=False)

        endtime = datetime.now()
        duration = round((endtime - starttime).total_seconds(), 3)
        logging.info(f'  done shape={df.shape}: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n')

        return df

    @classmethod
    @contextmanager
    def _wide_schema_download_patch(cls, reader, col_threshold=200):
        """Prevent HTTP 414 when a query result has many columns.

        ODPS Tunnel encodes column names as URL query params. With 200+ columns
        the URI exceeds the server limit. This context temporarily replaces the
        class-level _build_input_stream to omit the columns param. The patch is
        guarded by a lock and reference count, so concurrent threads do not
        restore the global method while another wide-schema download is active.
        """
        ds = getattr(reader, '_download_session', None)
        if ds is None:
            yield False
            return
        schema = getattr(ds, 'schema', None)
        if schema is None or len(schema.simple_columns) <= col_threshold:
            yield False
            return

        from odps.tunnel.instancetunnel import InstanceDownloadSession

        with cls._wide_schema_patch_lock:
            if not cls._wide_schema_patch_active:
                cls._wide_schema_orig_build = InstanceDownloadSession._build_input_stream

                def _build_no_column_filter(self, start, count, compress=False, columns=None, arrow=False, raw_size=None):
                    with cls._wide_schema_patch_lock:
                        orig_build = cls._wide_schema_orig_build
                    if orig_build is None:
                        raise RuntimeError("ODPS wide-schema download patch lost its original method.")
                    return orig_build(self, start, count, compress=compress, columns=None, arrow=arrow, raw_size=raw_size)

                InstanceDownloadSession._build_input_stream = _build_no_column_filter
                cls._wide_schema_patch_active = True
                logging.info(f'  wide schema ({len(schema.simple_columns)} cols): patched tunnel to omit column filter from URL')
            cls._wide_schema_patch_ref_count += 1

        try:
            yield True
        finally:
            with cls._wide_schema_patch_lock:
                cls._wide_schema_patch_ref_count -= 1
                if cls._wide_schema_patch_ref_count <= 0:
                    if cls._wide_schema_orig_build is not None:
                        InstanceDownloadSession._build_input_stream = cls._wide_schema_orig_build
                    cls._wide_schema_orig_build = None
                    cls._wide_schema_patch_ref_count = 0
                    cls._wide_schema_patch_active = False

    @staticmethod
    def cre_table_schema(df, partition_name=None):

        dtypes = df.dtypes
        isnum_dtypes = [pd.api.types.is_numeric_dtype(x) for x in dtypes]
        isint_dtypes = [pd.api.types.is_integer_dtype(x) for x in dtypes]

        table_columns = []
        table_partitions = []
        for i in range(len(df.columns)):
            col = df.columns[i]
            col_type = "string" if not isnum_dtypes[i] else "float"
            col_type = "bigint" if isint_dtypes[i] else col_type
            if col == partition_name:
                table_partitions.append(Partition(name=col, type=col_type))
            else:
                table_columns.append(Column(name=col, type=col_type))

        if bool(table_partitions):
            table_schema = Schema(columns=table_columns, partitions=table_partitions)
        else:
            table_schema = Schema(columns=table_columns)

        return table_schema

    def upload_df(self, df, table_name, table_schema=None, partition=None):
        """上传数据集至mc中创建新表

        Parameters
        ----------
        table_name: str
            表名
        table_schema: odps.models.Schema
            表Schema
        df: pandas.DataFrame
            数据集
        partition: string
            保存分区
        """
        if table_schema is None:
            df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            table_schema = self.cre_table_schema(df=df, partition_name=None)

        self.o.delete_table(table_name, if_exists=True)
        t = self.o.create_table(table_name, table_schema)

        if bool(partition):
            with t.open_writer(partition=partition, create_partition=True) as writer:
                writer.write(df.values.tolist())
        else:
            with t.open_writer() as writer:
                writer.write(df.values.tolist())
        logger.info(f'<<<< 完成数据入表{table_name}: shape={df.shape} >>>>')

    def insert_df(self, df, table_name, overwrite=True, partition=None):
        """将数据集插入至mc已存在的表中.

        Parameters
        ----------
        df: pandas.DataFrame
            数据集
        table_name: str
            表名
        overwrite: Bool, default True
            是否覆盖
        partition: string, default None
            写入分区, 默认为None即无分区
        """
        t = self.o.get_table(table_name)
        if "py_inserttime" in t.schema and "py_inserttime" not in df.columns:
            df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

        if bool(partition):
            if overwrite:
                t.delete_partition(partition, if_exists=True)
            with t.open_writer(partition=partition, create_partition=True) as writer:
                writer.write(df.values.tolist())
        else:
            if overwrite:
                t.truncate()
            with t.open_writer() as writer:
                writer.write(df.values.tolist())
        logger.info('<<<< 完成数据入表: shape={0} >>>>'.format(df.shape))

run_sql

run_sql(sql, to_df=True, n_process=1, csv_path=None)

运行SQL并下载结果。

参数:

名称 类型 描述 默认
sql str

单个 SQL 代码。

必需
to_df bool

是否把结果加载到内存中作为 pandas.DataFrame 返回。 若 False, 函数返回空 DataFrame, 但仍会下载数据(当 csv_path 被指定时)。

True
n_process int

executor.open_reader().to_pandas 的并行进程数。

1
csv_path str

把结果另存为本地 CSV 的路径。to_df 互相独立: * 只设 csv_path → 下载 + 写 CSV, 不返回数据 (返回空 DataFrame) * 只设 to_df=True → 下载 + 返回 DataFrame, 不写 CSV * 两个都设 → 下载 + 返回 + 写 CSV * 都不设 → 只跑 SQL 不下载 (用于 DDL/INSERT 等)

None

返回:

类型 描述
DataFrame

to_df=True 时返回完整数据; 当 to_df=False 时返回空 DataFrame (用于占位).

Notes
  • 执行阶段(execute_sql)只跑一次, 无重试.
  • 下载阶段(to_pandas + to_csv)最多重试 6 次, 适用于网络抖动.
  • 当 SQL 返回列数 > 200 时, 线程安全的 wide-schema patch 会自动 patch ODPS Tunnel, 防止 HTTP 414 (URI too long).

示例:

>>> odps = ODPSRunner()
>>> df = odps.run_sql("SELECT * FROM dual LIMIT 10")                # 仅 DataFrame
>>> df = odps.run_sql("SELECT * FROM dual LIMIT 10", csv_path="x.csv")  # DataFrame + CSV
>>> _  = odps.run_sql("SELECT * FROM dual LIMIT 10", to_df=False,  # 仅 CSV
...                   csv_path="x.csv")
>>> _  = odps.run_sql("CREATE TABLE t AS SELECT 1")                # 仅执行, 不下载
源代码位于: Modeling_Tool/Core/ODPS_Tool.py
def run_sql(self, sql, to_df=True, n_process=1, csv_path=None):
    """运行SQL并下载结果。

    Parameters
    ----------
    sql : str
        单个 SQL 代码。
    to_df : bool, default True
        是否把结果加载到内存中作为 ``pandas.DataFrame`` 返回。
        若 ``False``, 函数返回**空 DataFrame**, 但仍会下载数据(当 ``csv_path`` 被指定时)。
    n_process : int, default 1
        ``executor.open_reader().to_pandas`` 的并行进程数。
    csv_path : str, default None
        把结果另存为本地 CSV 的路径。**与 ``to_df`` 互相独立**:
            * 只设 ``csv_path`` → 下载 + 写 CSV, 不返回数据 (返回空 DataFrame)
            * 只设 ``to_df=True`` → 下载 + 返回 DataFrame, 不写 CSV
            * 两个都设 → 下载 + 返回 + 写 CSV
            * 都不设 → 只跑 SQL 不下载 (用于 DDL/INSERT 等)

    Returns
    -------
    pandas.DataFrame
        当 ``to_df=True`` 时返回完整数据;
        当 ``to_df=False`` 时返回空 DataFrame (用于占位).

    Notes
    -----
    - **执行**阶段(execute_sql)只跑一次, 无重试.
    - **下载**阶段(to_pandas + to_csv)最多重试 6 次, 适用于网络抖动.
    - 当 SQL 返回列数 > 200 时, 线程安全的 wide-schema patch 会自动 patch ODPS Tunnel,
      防止 HTTP 414 (URI too long).

    Examples
    --------
    >>> odps = ODPSRunner()
    >>> df = odps.run_sql("SELECT * FROM dual LIMIT 10")                # 仅 DataFrame
    >>> df = odps.run_sql("SELECT * FROM dual LIMIT 10", csv_path="x.csv")  # DataFrame + CSV
    >>> _  = odps.run_sql("SELECT * FROM dual LIMIT 10", to_df=False,  # 仅 CSV
    ...                   csv_path="x.csv")
    >>> _  = odps.run_sql("CREATE TABLE t AS SELECT 1")                # 仅执行, 不下载
    """
    # 准备SQL
    sqldesc = sql[:100]+"..." if len(sql)>100 else sql
    logging.info(f"SQL: \n{sqldesc}")

    # 运行SQL(只执行一次,不重试)
    starttime = datetime.now()
    logging.info(f'  execute_sql: {starttime.strftime("%Y-%m-%d %H:%M:%S")}')
    executor = self.o.execute_sql(sql)

    # 决定是否需要下载: 至少满足 to_df=True 或 csv_path 不为空
    should_download = bool(to_df) or bool(csv_path)
    df = pd.DataFrame()

    if should_download:
        k = 6
        for i in range(k):
            try:
                logging.info(f'  to_pandas: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
                reader = executor.open_reader()
                with self._wide_schema_download_patch(reader):
                    if n_process > 1:
                        df = reader.to_pandas(n_process=n_process)
                    else:
                        df = reader.to_pandas()
                if bool(csv_path):
                    logging.info(f'  to_csv: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
                    df.to_csv(csv_path, index=False)
                break
            except Exception as e:
                logging.error(f'  download failed [{i+1}/{k}]: {e}')
                if i == k - 1:
                    endtime = datetime.now()
                    duration = round((endtime - starttime).total_seconds(), 1)
                    raise SystemError(
                        f'  break: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n'
                    )

    # 当用户显式要求不要 DataFrame 时, 主动释放引用, 节省内存
    if not to_df:
        df = pd.DataFrame()

    endtime = datetime.now()
    duration = round((endtime - starttime).total_seconds(), 1)
    logging.info(f'  done: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n')
    return df

download_table

download_table(table_name, partition=None, n_process=1, csv_path=None)

读取表中数据至DataFrame

参数:

名称 类型 描述 默认
table_name str

表名

必需
partition dict

分区, 例如: 'dt=2022-01-01,taino=0'

None
n_process int

将查询数据转为pandas.DataFrame的进程数

1
csv_path str

查询数据保存至csv文件路径

None

返回:

名称 类型 描述
df DataFrame

SQL查询数据结果

源代码位于: Modeling_Tool/Core/ODPS_Tool.py
def download_table(self, table_name, partition=None, n_process=1, csv_path=None):
    """读取表中数据至DataFrame 

    Parameters
    ----------
    table_name : str
        表名
    partition : dict
        分区, 例如: 'dt=2022-01-01,taino=0'
    n_process : int, default 1
        将查询数据转为pandas.DataFrame的进程数
    csv_path : str
        查询数据保存至csv文件路径

    Returns
    -------
    df: pandas.DataFrame
        SQL查询数据结果
    """
    logging.info(f"Table: \n{table_name} {partition}")
    starttime = datetime.now()
    logging.info(f'  to_pandas: {starttime.strftime("%Y-%m-%d %H:%M:%S")}')

    t = self.o.get_table(table_name)
    if bool(partition):
        reader = t.open_reader(partition=partition)
    else:
        reader = t.open_reader()
    if n_process > 1:
        df = reader.to_pandas(n_process=10)
    else:
        df = reader.to_pandas()

    if bool(csv_path):
        logging.info(f'  to_csv: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
        df.to_csv(csv_path, index=False)

    endtime = datetime.now()
    duration = round((endtime - starttime).total_seconds(), 3)
    logging.info(f'  done shape={df.shape}: {endtime.strftime("%Y-%m-%d %H:%M:%S")} duration {duration}\n')

    return df

upload_df

upload_df(df, table_name, table_schema=None, partition=None)

上传数据集至mc中创建新表

参数:

名称 类型 描述 默认
table_name

表名

必需
table_schema

表Schema

None
df

数据集

必需
partition

保存分区

None
源代码位于: Modeling_Tool/Core/ODPS_Tool.py
def upload_df(self, df, table_name, table_schema=None, partition=None):
    """上传数据集至mc中创建新表

    Parameters
    ----------
    table_name: str
        表名
    table_schema: odps.models.Schema
        表Schema
    df: pandas.DataFrame
        数据集
    partition: string
        保存分区
    """
    if table_schema is None:
        df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        table_schema = self.cre_table_schema(df=df, partition_name=None)

    self.o.delete_table(table_name, if_exists=True)
    t = self.o.create_table(table_name, table_schema)

    if bool(partition):
        with t.open_writer(partition=partition, create_partition=True) as writer:
            writer.write(df.values.tolist())
    else:
        with t.open_writer() as writer:
            writer.write(df.values.tolist())
    logger.info(f'<<<< 完成数据入表{table_name}: shape={df.shape} >>>>')

insert_df

insert_df(df, table_name, overwrite=True, partition=None)

将数据集插入至mc已存在的表中.

参数:

名称 类型 描述 默认
df

数据集

必需
table_name

表名

必需
overwrite

是否覆盖

True
partition

写入分区, 默认为None即无分区

None
源代码位于: Modeling_Tool/Core/ODPS_Tool.py
def insert_df(self, df, table_name, overwrite=True, partition=None):
    """将数据集插入至mc已存在的表中.

    Parameters
    ----------
    df: pandas.DataFrame
        数据集
    table_name: str
        表名
    overwrite: Bool, default True
        是否覆盖
    partition: string, default None
        写入分区, 默认为None即无分区
    """
    t = self.o.get_table(table_name)
    if "py_inserttime" in t.schema and "py_inserttime" not in df.columns:
        df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

    if bool(partition):
        if overwrite:
            t.delete_partition(partition, if_exists=True)
        with t.open_writer(partition=partition, create_partition=True) as writer:
            writer.write(df.values.tolist())
    else:
        if overwrite:
            t.truncate()
        with t.open_writer() as writer:
            writer.write(df.values.tolist())
    logger.info('<<<< 完成数据入表: shape={0} >>>>'.format(df.shape))

ODPS 并发管理 — Parallel_ODPS_Manager

Parallel_ODPS_Manager

ParallelODPSManager

Parallel ODPS pull/push helper built on ODPSRunner and ParallelApplyEngine.

源代码位于: Modeling_Tool/Core/Parallel_ODPS_Manager.py
class ParallelODPSManager:
    """Parallel ODPS pull/push helper built on ODPSRunner and ParallelApplyEngine."""

    _VALID_BACKENDS = {"thread", "process", "sequential"}
    _VALID_WRITE_MODES = {"overwrite", "append"}

    def __init__(self, config: ParallelODPSConfig, odps_runner: ODPSRunner | None = None):
        self.config = config
        self.odps_runner = odps_runner or ODPSRunner()
        self._validate_config()

    def _validate_config(self) -> None:
        cfg = self.config
        if cfg.backend not in self._VALID_BACKENDS:
            raise ValueError(f"backend must be one of {sorted(self._VALID_BACKENDS)}")
        if cfg.chunk_size is not None and cfg.chunk_size <= 0:
            raise ValueError("chunk_size must be a positive integer.")
        if cfg.n_chunks is not None and cfg.n_chunks <= 0:
            raise ValueError("n_chunks must be a positive integer.")
        if cfg.chunk_size is not None and cfg.n_chunks is not None:
            raise ValueError("chunk_size and n_chunks cannot be used together.")
        if cfg.n_jobs <= 0:
            raise ValueError("n_jobs must be a positive integer.")
        if not cfg.chunk_filter_key:
            raise ValueError("chunk_filter_key cannot be empty.")
        if not cfg.tmp_table_prefix:
            raise ValueError("tmp_table_prefix cannot be empty.")

    def _runner_for_backend(self) -> ODPSRunner | None:
        return None if self.config.backend == "process" else self.odps_runner

    def _auto_count_query(self, sql_path: str, template_kwargs: dict[str, Any]) -> str:
        kwargs = dict(template_kwargs)
        kwargs[self.config.chunk_filter_key] = "1=1"
        rendered_sql = parse_sql_file(sql_path=sql_path, **kwargs).rstrip().rstrip(";")
        return f"SELECT COUNT(1) FROM ({rendered_sql}) __count_src;"

    def _resolve_pull_n_chunks(self, count_query: str | None, sql_path: str, template_kwargs: dict[str, Any]) -> int:
        cfg = self.config
        if not cfg.unique_key:
            raise ValueError("unique_key is required for pull().")
        if cfg.n_chunks is not None:
            return cfg.n_chunks
        if cfg.chunk_size is None:
            raise ValueError("chunk_size or n_chunks is required for pull().")
        if count_query is None:
            count_query = self._auto_count_query(sql_path, template_kwargs)
        count_df = self.odps_runner.run_sql(count_query, to_df=True)
        total_rows = int(count_df.iloc[0, 0])
        return max(1, math.ceil(total_rows / cfg.chunk_size))

    def pull(
        self,
        sql_path: str,
        out_path: str,
        count_query: str | None = None,
        **template_kwargs: Any,
    ) -> dict[str, Any]:
        cfg = self.config
        n_chunks = self._resolve_pull_n_chunks(count_query, sql_path, template_kwargs)
        cfg.tmp_dir.mkdir(parents=True, exist_ok=True)

        engine_cfg = ParallelApplyConfig(
            split_axis="chunk",
            backend=cfg.backend,
            n_jobs=cfg.n_jobs,
            combine="list",
            on_error="collect",
        )
        result = ParallelApplyEngine(engine_cfg).run(
            func=_pull_one_chunk,
            chunks=list(range(n_chunks)),
            func_args=(
                self._runner_for_backend(),
                sql_path,
                template_kwargs,
                cfg.chunk_filter_key,
                cfg.unique_key,
                n_chunks,
                str(cfg.tmp_dir),
            ),
        )

        if len(result.errors):
            raise RuntimeError(f"{len(result.errors)}/{n_chunks} ODPS pull chunks failed:\n{result.errors}")

        chunk_summaries = sorted(result.output, key=lambda item: item["chunk"])
        final_path = Path(out_path)
        final_path.parent.mkdir(parents=True, exist_ok=True)
        final_path.unlink(missing_ok=True)

        with open(final_path, "wb") as fout:
            for idx, summary in enumerate(chunk_summaries):
                chunk_file = Path(summary["path"])
                with open(chunk_file, "rb") as fin:
                    if idx > 0:
                        fin.readline()
                    shutil.copyfileobj(fin, fout)
                chunk_file.unlink()

        return {
            "n_chunks": n_chunks,
            "total_rows": sum(item["rows"] for item in chunk_summaries),
            "out_path": str(final_path),
            "per_chunk_rows": [item["rows"] for item in chunk_summaries],
        }

    def push(
        self,
        data: pd.DataFrame | str | Path,
        target_table: str,
        write_mode: WriteMode | str | None = None,
    ) -> dict[str, Any]:
        if write_mode not in self._VALID_WRITE_MODES:
            raise ValueError("write_mode is required and must be one of ['append', 'overwrite'].")
        if not target_table:
            raise ValueError("target_table cannot be empty.")

        cfg = self.config
        cfg.tmp_dir.mkdir(parents=True, exist_ok=True)
        run_id = uuid.uuid4().hex[:12]
        chunk_specs, local_temp_paths = self._build_push_chunks(data=data, run_id=run_id)
        if not chunk_specs:
            raise ValueError("push data produced no chunks.")

        tmp_tables = [spec["tmp_table"] for spec in chunk_specs]
        try:
            engine_cfg = ParallelApplyConfig(
                split_axis="chunk",
                backend=cfg.backend,
                n_jobs=cfg.n_jobs,
                combine="list",
                on_error="collect",
            )
            result = ParallelApplyEngine(engine_cfg).run(
                func=_push_one_chunk,
                chunks=chunk_specs,
                func_args=(self._runner_for_backend(),),
            )
            if len(result.errors):
                raise RuntimeError(f"{len(result.errors)}/{len(chunk_specs)} ODPS push chunks failed:\n{result.errors}")

            chunk_summaries = sorted(result.output, key=lambda item: item["chunk"])
            union_sql = self._build_union_sql(tmp_tables)
            final_sql = self._write_final_table(target_table=target_table, union_sql=union_sql, write_mode=str(write_mode))

            success = True
            return {
                "n_chunks": len(chunk_summaries),
                "total_rows": sum(item["rows"] for item in chunk_summaries),
                "target_table": target_table,
                "write_mode": write_mode,
                "tmp_tables": tmp_tables,
                "per_chunk_rows": [item["rows"] for item in chunk_summaries],
                "union_sql": union_sql,
                "final_sql": final_sql,
            }
        finally:
            self._cleanup_local_files(local_temp_paths)
            if cfg.cleanup_tmp and (success or not cfg.keep_tmp_on_error):
                self._cleanup_tmp_tables(tmp_tables)

    def _build_push_chunks(self, data: pd.DataFrame | str | Path, run_id: str) -> tuple[list[dict[str, Any]], list[Path]]:
        if isinstance(data, pd.DataFrame):
            return self._build_dataframe_push_chunks(data, run_id), []
        if isinstance(data, (str, Path)):
            return self._build_csv_push_chunks(Path(data), run_id)
        raise TypeError("data must be a pandas DataFrame or a CSV path.")

    def _build_dataframe_push_chunks(self, data: pd.DataFrame, run_id: str) -> list[dict[str, Any]]:
        if len(data.columns) == 0:
            raise ValueError("data must contain at least one column.")
        positions = self._split_positions(len(data))
        specs = []
        for chunk_id, pos in enumerate(positions):
            chunk_df = data.iloc[pos].copy() if len(pos) else data.iloc[0:0].copy()
            specs.append({
                "chunk": chunk_id,
                "data": chunk_df,
                "tmp_table": self._tmp_table_name(run_id, chunk_id),
            })
        return specs

    def _build_csv_push_chunks(self, csv_path: Path, run_id: str) -> tuple[list[dict[str, Any]], list[Path]]:
        if not csv_path.is_file():
            raise FileNotFoundError(f"CSV file not found: {csv_path}")
        chunk_size = self._resolve_csv_chunk_size(csv_path)
        specs: list[dict[str, Any]] = []
        local_paths: list[Path] = []
        for chunk_id, chunk_df in enumerate(pd.read_csv(csv_path, chunksize=chunk_size)):
            local_path = self.config.tmp_dir / f"_push_{run_id}_{chunk_id:04d}.csv"
            chunk_df.to_csv(local_path, index=False)
            local_paths.append(local_path)
            specs.append({
                "chunk": chunk_id,
                "csv_path": str(local_path),
                "tmp_table": self._tmp_table_name(run_id, chunk_id),
            })
        return specs, local_paths

    def _resolve_csv_chunk_size(self, csv_path: Path) -> int:
        cfg = self.config
        if cfg.chunk_size is not None:
            return cfg.chunk_size
        if cfg.n_chunks is None:
            raise ValueError("chunk_size or n_chunks is required for CSV push().")
        with open(csv_path, "rb") as fin:
            n_lines = sum(1 for _ in fin)
        n_rows = max(0, n_lines - 1)
        return max(1, math.ceil(n_rows / cfg.n_chunks))

    def _split_positions(self, n_rows: int) -> list[np.ndarray]:
        cfg = self.config
        if n_rows < 0:
            raise ValueError("n_rows must be non-negative.")
        if n_rows == 0:
            return [np.array([], dtype=int)]
        if cfg.chunk_size is not None:
            n_chunks = math.ceil(n_rows / cfg.chunk_size)
        elif cfg.n_chunks is not None:
            n_chunks = min(cfg.n_chunks, n_rows)
        else:
            raise ValueError("chunk_size or n_chunks is required for push().")
        return [arr for arr in np.array_split(np.arange(n_rows), n_chunks) if len(arr) > 0]

    def _tmp_table_name(self, run_id: str, chunk_id: int) -> str:
        return f"{self.config.tmp_table_prefix}_{run_id}_{chunk_id:04d}"

    @staticmethod
    def _build_union_sql(tmp_tables: list[str]) -> str:
        return "\nUNION ALL\n".join(f"SELECT * FROM {table_name}" for table_name in tmp_tables)

    def _write_final_table(self, target_table: str, union_sql: str, write_mode: str) -> str:
        if write_mode == "overwrite":
            _delete_table(self.odps_runner, target_table)
            final_sql = f"CREATE TABLE {target_table} AS\n{union_sql};"
        else:
            final_sql = f"INSERT INTO TABLE {target_table}\n{union_sql};"
        self.odps_runner.run_sql(final_sql, to_df=False)
        return final_sql

    def _cleanup_tmp_tables(self, tmp_tables: list[str]) -> None:
        for table_name in tmp_tables:
            _delete_table(self.odps_runner, table_name)

    @staticmethod
    def _cleanup_local_files(paths: list[Path]) -> None:
        for path in paths:
            path.unlink(missing_ok=True)

数据一致性对比 — Proc_Compare

Proc_Compare

ProcCompareEngine

SAS proc_compare-like consistency checker for DataFrames and CSV files.

源代码位于: Modeling_Tool/Core/Proc_Compare.py
 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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
class ProcCompareEngine:
    """SAS proc_compare-like consistency checker for DataFrames and CSV files."""

    _VALID_BACKENDS = {"sequential", "thread", "process"}
    _VALID_DETAIL_MODES = {"top", "full", "none"}
    _VALID_DUPLICATE_POLICIES = {"raise", "first", "all"}

    def __init__(self, config: ProcCompareConfig | None = None):
        self.config = config or ProcCompareConfig()
        self._validate_config()

    def run(self, left: pd.DataFrame | str | Path, right: pd.DataFrame | str | Path) -> ProcCompareResult:
        key_cols = self._resolve_key_cols()
        if self._is_csv_like(left) or self._is_csv_like(right):
            result = self._run_csv(left, right, key_cols)
        else:
            left_df = self._prepare_dataframe(left, side=self.config.left_name, key_cols=key_cols)
            right_df = self._prepare_dataframe(right, side=self.config.right_name, key_cols=key_cols)
            schema = self._build_schema_summary(left_df, right_df, key_cols)
            result = self._compare_frames(left_df, right_df, key_cols, schema_summary=schema)

        output_paths: dict[str, str] = {}
        if self.config.write_outputs:
            output_paths = self._write_outputs(result)
            result.output_paths.update(output_paths)
        if self.config.write_excel:
            result.report_path = self._write_excel(result)
        return result

    def _validate_config(self) -> None:
        cfg = self.config
        if cfg.backend not in self._VALID_BACKENDS:
            raise ValueError(f"backend must be one of {sorted(self._VALID_BACKENDS)}.")
        if cfg.detail_mode not in self._VALID_DETAIL_MODES:
            raise ValueError(f"detail_mode must be one of {sorted(self._VALID_DETAIL_MODES)}.")
        if cfg.duplicate_key_policy not in self._VALID_DUPLICATE_POLICIES:
            raise ValueError(
                f"duplicate_key_policy must be one of {sorted(self._VALID_DUPLICATE_POLICIES)}."
            )
        if cfg.chunk_size <= 0:
            raise ValueError("chunk_size must be a positive integer.")
        if cfg.n_partitions <= 0:
            raise ValueError("n_partitions must be a positive integer.")
        if cfg.numeric_tol < 0 or cfg.numeric_rtol < 0 or cfg.datetime_tol_seconds < 0:
            raise ValueError("tolerance values must be non-negative.")
        if cfg.top_n <= 0:
            raise ValueError("top_n must be a positive integer.")

    def _resolve_key_cols(self) -> list[str]:
        key_cols = list(self.config.key_cols or [])
        if key_cols:
            return key_cols
        if self.config.row_order_compare:
            return [_ROW_ID_COL]
        raise ValueError("Provide key_cols or set row_order_compare=True.")

    @staticmethod
    def _is_csv_like(value: Any) -> bool:
        return isinstance(value, (str, Path))

    def _prepare_dataframe(
        self,
        data: pd.DataFrame | str | Path,
        side: str,
        key_cols: list[str],
    ) -> pd.DataFrame:
        if isinstance(data, pd.DataFrame):
            df = data.copy()
        else:
            df = pd.read_csv(data)
        if key_cols == [_ROW_ID_COL] and _ROW_ID_COL not in df.columns:
            df = df.copy()
            df[_ROW_ID_COL] = np.arange(len(df), dtype=np.int64)
        self._validate_columns(df, key_cols, side)
        return self._normalize_missing(df)

    def _validate_columns(self, df: pd.DataFrame, key_cols: list[str], side: str) -> None:
        missing = [col for col in key_cols if col not in df.columns]
        if missing:
            raise ValueError(f"{side} dataset is missing key columns: {missing}")

    def _normalize_missing(self, df: pd.DataFrame) -> pd.DataFrame:
        if not self.config.missing_values:
            return df
        return df.replace(self.config.missing_values, pd.NA)

    def _handle_duplicates(
        self,
        df: pd.DataFrame,
        key_cols: list[str],
        side: str,
    ) -> tuple[pd.DataFrame, pd.DataFrame]:
        if key_cols == [_ROW_ID_COL]:
            return df, self._empty_duplicate_summary()
        dup_mask = df.duplicated(subset=key_cols, keep=False)
        if not dup_mask.any():
            return df, self._empty_duplicate_summary()

        dup = df.loc[dup_mask, key_cols].copy()
        grouped = dup.value_counts(key_cols).reset_index(name="duplicate_rows")
        grouped.insert(0, "side", side)

        if self.config.duplicate_key_policy == "raise":
            examples = grouped.head(5).to_dict("records")
            raise ValueError(f"{side} dataset has duplicate keys; examples={examples}")
        if self.config.duplicate_key_policy == "first":
            df = df.drop_duplicates(subset=key_cols, keep="first")
        return df, grouped

    @staticmethod
    def _empty_duplicate_summary() -> pd.DataFrame:
        return pd.DataFrame(columns=["side", "duplicate_rows"])

    def _build_schema_summary(
        self,
        left_df: pd.DataFrame,
        right_df: pd.DataFrame,
        key_cols: list[str],
    ) -> pd.DataFrame:
        cols = sorted((set(left_df.columns) | set(right_df.columns)) - {_ROW_ID_COL})
        rows = []
        ignore = set(self.config.ignore_cols)
        compare_set = set(self._resolve_compare_cols(left_df, right_df, key_cols, allow_missing=True))
        for col in cols:
            in_left = col in left_df.columns
            in_right = col in right_df.columns
            if col in key_cols:
                role = "key"
            elif col in ignore:
                role = "ignored"
            elif col in compare_set:
                role = "compare"
            else:
                role = "not_compared"
            if in_left and in_right:
                status = "both"
            elif in_left:
                status = "left_only_column"
            else:
                status = "right_only_column"
            rows.append(
                {
                    "column": col,
                    "role": role,
                    "in_left": bool(in_left),
                    "in_right": bool(in_right),
                    "dtype_left": str(left_df[col].dtype) if in_left else "",
                    "dtype_right": str(right_df[col].dtype) if in_right else "",
                    "status": status,
                    "dtype_equal": bool(in_left and in_right and str(left_df[col].dtype) == str(right_df[col].dtype)),
                }
            )
        return pd.DataFrame(rows)

    def _resolve_compare_cols(
        self,
        left_df: pd.DataFrame,
        right_df: pd.DataFrame,
        key_cols: list[str],
        allow_missing: bool = False,
    ) -> list[str]:
        ignore = set(self.config.ignore_cols) | set(key_cols) | {_ROW_ID_COL}
        if self.config.compare_cols is not None:
            candidates = [col for col in self.config.compare_cols if col not in ignore]
        else:
            candidates = sorted((set(left_df.columns) | set(right_df.columns)) - ignore)
        if allow_missing:
            return candidates
        return [col for col in candidates if col in left_df.columns and col in right_df.columns]

    def _compare_frames(
        self,
        left_df: pd.DataFrame,
        right_df: pd.DataFrame,
        key_cols: list[str],
        schema_summary: pd.DataFrame | None = None,
    ) -> ProcCompareResult:
        left_df, left_dups = self._handle_duplicates(left_df, key_cols, self.config.left_name)
        right_df, right_dups = self._handle_duplicates(right_df, key_cols, self.config.right_name)
        duplicate_key_summary = pd.concat([left_dups, right_dups], ignore_index=True)
        schema_summary = schema_summary if schema_summary is not None else self._build_schema_summary(left_df, right_df, key_cols)
        compare_cols = self._resolve_compare_cols(left_df, right_df, key_cols)

        merged = left_df.merge(
            right_df,
            on=key_cols,
            how="outer",
            suffixes=("_left", "_right"),
            indicator=True,
        )
        common_mask = merged["_merge"].eq("both")
        coverage_summary = pd.DataFrame(
            [
                {
                    "left_name": self.config.left_name,
                    "right_name": self.config.right_name,
                    "n_left_rows": int(len(left_df)),
                    "n_right_rows": int(len(right_df)),
                    "n_common_rows": int(common_mask.sum()),
                    "n_left_only_rows": int(merged["_merge"].eq("left_only").sum()),
                    "n_right_only_rows": int(merged["_merge"].eq("right_only").sum()),
                    "n_compare_columns": int(len(compare_cols)),
                }
            ]
        )

        row_summary = merged[key_cols + ["_merge"]].copy()
        row_summary = row_summary.rename(columns={"_merge": "row_status"})
        row_summary["n_cell_mismatch"] = 0
        row_summary["mismatch_columns"] = ""

        column_rows = []
        mismatch_frames = []
        mismatch_cols_by_row: dict[int, list[str]] = {}

        for col in compare_cols:
            left_col = f"{col}_left"
            right_col = f"{col}_right"
            comp = self._compare_series(col, merged[left_col], merged[right_col], common_mask)
            mismatch_mask = comp["mismatch"]
            n_mismatch = int(mismatch_mask.sum())
            if n_mismatch:
                mismatch_indexes = merged.index[mismatch_mask]
                for idx in mismatch_indexes:
                    mismatch_cols_by_row.setdefault(int(idx), []).append(col)
                if self.config.detail_mode != "none":
                    detail = merged.loc[mismatch_mask, key_cols].copy()
                    detail["column"] = col
                    detail["left_value"] = merged.loc[mismatch_mask, left_col].to_numpy()
                    detail["right_value"] = merged.loc[mismatch_mask, right_col].to_numpy()
                    detail["diff"] = comp["diff"].loc[mismatch_mask].to_numpy()
                    detail["abs_diff"] = comp["abs_diff"].loc[mismatch_mask].to_numpy()
                    mismatch_frames.append(detail)

            n_compared = int(common_mask.sum())
            n_one_null = int(comp["one_null"].sum())
            n_both_null = int(comp["both_null"].sum())
            n_equal = n_compared - n_mismatch
            column_rows.append(
                {
                    "column": col,
                    "n_compared": n_compared,
                    "n_equal": n_equal,
                    "n_mismatch": n_mismatch,
                    "pct_mismatch": round(n_mismatch / n_compared * 100, 6) if n_compared else 0.0,
                    "n_one_side_null": n_one_null,
                    "n_both_null": n_both_null,
                    "mean_diff": comp["diff"].mean(skipna=True),
                    "max_abs_diff": comp["abs_diff"].max(skipna=True),
                    "_diff_sum": comp["diff"].sum(skipna=True),
                    "_diff_count": int(comp["diff"].notna().sum()),
                }
            )

        for idx, cols in mismatch_cols_by_row.items():
            row_summary.loc[idx, "n_cell_mismatch"] = len(cols)
            row_summary.loc[idx, "mismatch_columns"] = ", ".join(cols)

        column_summary = pd.DataFrame(
            column_rows,
            columns=[
                "column",
                "n_compared",
                "n_equal",
                "n_mismatch",
                "pct_mismatch",
                "n_one_side_null",
                "n_both_null",
                "mean_diff",
                "max_abs_diff",
                "_diff_sum",
                "_diff_count",
            ],
        )
        column_summary = column_summary.sort_values(["n_mismatch", "column"], ascending=[False, True])

        cell_mismatches = (
            pd.concat(mismatch_frames, ignore_index=True)
            if mismatch_frames and self.config.detail_mode != "none"
            else self._empty_cell_mismatches(key_cols)
        )
        cell_mismatches = self._limit_mismatch_details(cell_mismatches)

        return ProcCompareResult(
            coverage_summary=coverage_summary,
            schema_summary=schema_summary,
            column_summary=self._strip_internal_columns(column_summary),
            row_summary=row_summary,
            cell_mismatches=cell_mismatches,
            duplicate_key_summary=duplicate_key_summary,
        )

    def _compare_series(
        self,
        col: str,
        left: pd.Series,
        right: pd.Series,
        common_mask: pd.Series,
    ) -> dict[str, pd.Series]:
        left = self._normalize_series_missing(left)
        right = self._normalize_series_missing(right)
        both_null = left.isna() & right.isna() & common_mask
        one_null = (left.isna() ^ right.isna()) & common_mask

        diff = pd.Series(np.nan, index=left.index, dtype="float64")
        abs_diff = pd.Series(np.nan, index=left.index, dtype="float64")

        if self._should_compare_datetime(left, right):
            left_dt = pd.to_datetime(left, errors="coerce")
            right_dt = pd.to_datetime(right, errors="coerce")
            diff = (left_dt - right_dt).dt.total_seconds()
            abs_diff = diff.abs()
            tol = self._column_datetime_tol(col)
            value_mismatch = abs_diff > tol
            one_null = (left_dt.isna() ^ right_dt.isna()) & common_mask
            both_null = left_dt.isna() & right_dt.isna() & common_mask
        elif self._should_compare_numeric(left, right):
            left_num = pd.to_numeric(left, errors="coerce")
            right_num = pd.to_numeric(right, errors="coerce")
            diff = left_num - right_num
            abs_diff = diff.abs()
            tol, rtol = self._column_numeric_tol(col)
            value_mismatch = abs_diff > (tol + rtol * right_num.abs())
            one_null = (left_num.isna() ^ right_num.isna()) & common_mask
            both_null = left_num.isna() & right_num.isna() & common_mask
        else:
            left_str = left.astype("string")
            right_str = right.astype("string")
            value_mismatch = left_str.ne(right_str)

        if self.config.both_null_equal:
            value_mismatch = value_mismatch & ~both_null
            mismatch = (value_mismatch | one_null) & common_mask
        else:
            mismatch = (value_mismatch | one_null | both_null) & common_mask
        return {
            "mismatch": mismatch.fillna(False),
            "one_null": one_null.fillna(False),
            "both_null": both_null.fillna(False),
            "diff": diff,
            "abs_diff": abs_diff,
        }

    def _normalize_series_missing(self, series: pd.Series) -> pd.Series:
        if not self.config.missing_values:
            return series
        return series.replace(self.config.missing_values, pd.NA)

    @staticmethod
    def _should_compare_numeric(left: pd.Series, right: pd.Series) -> bool:
        if pd.api.types.is_numeric_dtype(left) or pd.api.types.is_numeric_dtype(right):
            return True
        left_num = pd.to_numeric(left.dropna(), errors="coerce")
        right_num = pd.to_numeric(right.dropna(), errors="coerce")
        if len(left_num) == 0 and len(right_num) == 0:
            return False
        return bool(left_num.notna().all() and right_num.notna().all())

    @staticmethod
    def _should_compare_datetime(left: pd.Series, right: pd.Series) -> bool:
        return bool(pd.api.types.is_datetime64_any_dtype(left) or pd.api.types.is_datetime64_any_dtype(right))

    def _column_numeric_tol(self, col: str) -> tuple[float, float]:
        override = self.config.per_column_tolerance.get(col)
        if override is None:
            return float(self.config.numeric_tol), float(self.config.numeric_rtol)
        if isinstance(override, dict):
            return float(override.get("tol", self.config.numeric_tol)), float(
                override.get("rtol", self.config.numeric_rtol)
            )
        return float(override), float(self.config.numeric_rtol)

    def _column_datetime_tol(self, col: str) -> float:
        override = self.config.per_column_tolerance.get(col)
        if isinstance(override, dict):
            return float(override.get("datetime_tol_seconds", self.config.datetime_tol_seconds))
        if override is not None:
            return float(override)
        return float(self.config.datetime_tol_seconds)

    def _empty_cell_mismatches(self, key_cols: list[str]) -> pd.DataFrame:
        return pd.DataFrame(columns=key_cols + ["column", "left_value", "right_value", "diff", "abs_diff"])

    def _limit_mismatch_details(self, df: pd.DataFrame) -> pd.DataFrame:
        if self.config.detail_mode == "none":
            return df.iloc[0:0].copy()
        if self.config.detail_mode == "full" or len(df) <= self.config.top_n:
            return df
        if "abs_diff" in df.columns:
            ranked = df.copy()
            ranked["_rank_abs_diff"] = pd.to_numeric(ranked["abs_diff"], errors="coerce").fillna(-math.inf)
            ranked = ranked.sort_values("_rank_abs_diff", ascending=False).drop(columns=["_rank_abs_diff"])
            return ranked.head(self.config.top_n).reset_index(drop=True)
        return df.head(self.config.top_n).reset_index(drop=True)

    @staticmethod
    def _strip_internal_columns(df: pd.DataFrame) -> pd.DataFrame:
        return df[[col for col in df.columns if not col.startswith("_")]].copy()

    def _run_csv(
        self,
        left: pd.DataFrame | str | Path,
        right: pd.DataFrame | str | Path,
        key_cols: list[str],
    ) -> ProcCompareResult:
        left_df_for_schema = pd.read_csv(left, nrows=1000) if self._is_csv_like(left) else left.copy()
        right_df_for_schema = pd.read_csv(right, nrows=1000) if self._is_csv_like(right) else right.copy()
        if key_cols == [_ROW_ID_COL]:
            if _ROW_ID_COL not in left_df_for_schema.columns:
                left_df_for_schema[_ROW_ID_COL] = np.arange(len(left_df_for_schema), dtype=np.int64)
            if _ROW_ID_COL not in right_df_for_schema.columns:
                right_df_for_schema[_ROW_ID_COL] = np.arange(len(right_df_for_schema), dtype=np.int64)
        schema = self._build_schema_summary(left_df_for_schema, right_df_for_schema, key_cols)

        output_dir = Path(self.config.output_dir)
        temp_parent = output_dir if self.config.write_outputs or self.config.write_excel else None
        if temp_parent is not None:
            temp_parent.mkdir(parents=True, exist_ok=True)
        temp_dir = Path(tempfile.mkdtemp(prefix="proc_compare_", dir=str(temp_parent) if temp_parent else None))
        try:
            left_parts = temp_dir / "left"
            right_parts = temp_dir / "right"
            left_parts.mkdir()
            right_parts.mkdir()
            self._partition_input(left, left_parts, key_cols)
            self._partition_input(right, right_parts, key_cols)

            partition_ids = list(range(self.config.n_partitions))
            if self.config.backend == "sequential":
                partials = [
                    self._compare_partition(
                        part_id,
                        left_parts,
                        right_parts,
                        list(left_df_for_schema.columns),
                        list(right_df_for_schema.columns),
                        key_cols,
                        schema,
                    )
                    for part_id in partition_ids
                ]
            else:
                from .Parallel_Engine import ParallelApplyConfig, ParallelApplyEngine

                engine = ParallelApplyEngine(
                    ParallelApplyConfig(
                        split_axis="chunk",
                        backend=self.config.backend,
                        combine="list",
                        validate_picklable=False,
                    )
                )
                partials = engine.run(
                    chunks=partition_ids,
                    func=self._compare_partition,
                    func_args=(
                        left_parts,
                        right_parts,
                        list(left_df_for_schema.columns),
                        list(right_df_for_schema.columns),
                        key_cols,
                        schema,
                    ),
                ).output
            return self._aggregate_partials(partials, schema)
        finally:
            shutil.rmtree(temp_dir, ignore_errors=True)

    def _compare_partition(
        self,
        part_id: int,
        left_parts: Path,
        right_parts: Path,
        left_columns: list[str],
        right_columns: list[str],
        key_cols: list[str],
        schema_summary: pd.DataFrame,
    ) -> ProcCompareResult:
        left_part = left_parts / f"part_{part_id}.csv"
        right_part = right_parts / f"part_{part_id}.csv"
        left_part_df = pd.read_csv(left_part) if left_part.exists() else pd.DataFrame(columns=left_columns)
        right_part_df = pd.read_csv(right_part) if right_part.exists() else pd.DataFrame(columns=right_columns)
        left_part_df = self._normalize_missing(left_part_df)
        right_part_df = self._normalize_missing(right_part_df)
        return self._compare_frames(left_part_df, right_part_df, key_cols, schema_summary=schema_summary)

    def _partition_input(self, data: pd.DataFrame | str | Path, out_dir: Path, key_cols: list[str]) -> None:
        if isinstance(data, pd.DataFrame):
            iterator = [data.copy()]
        else:
            iterator = pd.read_csv(data, chunksize=self.config.chunk_size)

        row_offset = 0
        for chunk in iterator:
            if key_cols == [_ROW_ID_COL] and _ROW_ID_COL not in chunk.columns:
                chunk = chunk.copy()
                chunk[_ROW_ID_COL] = np.arange(row_offset, row_offset + len(chunk), dtype=np.int64)
            self._validate_columns(chunk, key_cols, str(out_dir.name))
            hashes = pd.util.hash_pandas_object(chunk[key_cols], index=False)
            partitions = (hashes % self.config.n_partitions).astype(int)
            for part_id, part_df in chunk.groupby(partitions, sort=False):
                path = out_dir / f"part_{int(part_id)}.csv"
                part_df.to_csv(path, mode="a", header=not path.exists(), index=False)
            row_offset += len(chunk)

    def _aggregate_partials(
        self,
        partials: list[ProcCompareResult],
        schema_summary: pd.DataFrame,
    ) -> ProcCompareResult:
        coverage = pd.concat([p.coverage_summary for p in partials], ignore_index=True)
        coverage_summary = pd.DataFrame(
            [
                {
                    "left_name": self.config.left_name,
                    "right_name": self.config.right_name,
                    "n_left_rows": int(coverage["n_left_rows"].sum()),
                    "n_right_rows": int(coverage["n_right_rows"].sum()),
                    "n_common_rows": int(coverage["n_common_rows"].sum()),
                    "n_left_only_rows": int(coverage["n_left_only_rows"].sum()),
                    "n_right_only_rows": int(coverage["n_right_only_rows"].sum()),
                    "n_compare_columns": int(coverage["n_compare_columns"].max()) if len(coverage) else 0,
                }
            ]
        )

        cols = pd.concat([p.column_summary.assign(_partial=idx) for idx, p in enumerate(partials)], ignore_index=True)
        if len(cols):
            cols["_diff_sum"] = pd.concat(
                [
                    p.column_summary.assign(_partial=idx).get("_diff_sum", pd.Series(dtype=float))
                    for idx, p in enumerate(partials)
                ],
                ignore_index=True,
            ) if "_diff_sum" in cols.columns else 0.0
            grouped = cols.groupby("column", dropna=False).agg(
                n_compared=("n_compared", "sum"),
                n_equal=("n_equal", "sum"),
                n_mismatch=("n_mismatch", "sum"),
                n_one_side_null=("n_one_side_null", "sum"),
                n_both_null=("n_both_null", "sum"),
                max_abs_diff=("max_abs_diff", "max"),
            ).reset_index()
            grouped["pct_mismatch"] = np.where(
                grouped["n_compared"] > 0,
                grouped["n_mismatch"] / grouped["n_compared"] * 100,
                0.0,
            )
            grouped["mean_diff"] = np.nan
            column_summary = grouped[
                [
                    "column",
                    "n_compared",
                    "n_equal",
                    "n_mismatch",
                    "pct_mismatch",
                    "n_one_side_null",
                    "n_both_null",
                    "mean_diff",
                    "max_abs_diff",
                ]
            ].sort_values(["n_mismatch", "column"], ascending=[False, True])
        else:
            column_summary = pd.DataFrame()

        row_summary = pd.concat([p.row_summary for p in partials], ignore_index=True)
        mismatch_tables = [p.cell_mismatches for p in partials if not p.cell_mismatches.empty]
        cell_mismatches = (
            pd.concat(mismatch_tables, ignore_index=True)
            if mismatch_tables
            else self._empty_cell_mismatches(partials[0].cell_mismatches.columns[:-5].tolist() if partials else [])
        )
        cell_mismatches = self._limit_mismatch_details(cell_mismatches)
        duplicate_key_summary = pd.concat([p.duplicate_key_summary for p in partials], ignore_index=True)

        return ProcCompareResult(
            coverage_summary=coverage_summary,
            schema_summary=schema_summary,
            column_summary=column_summary,
            row_summary=row_summary,
            cell_mismatches=cell_mismatches,
            duplicate_key_summary=duplicate_key_summary,
        )

    def _write_outputs(self, result: ProcCompareResult) -> dict[str, str]:
        output_dir = Path(self.config.output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)
        tables = {
            "coverage_summary": result.coverage_summary,
            "schema_summary": result.schema_summary,
            "column_summary": result.column_summary,
            "row_summary": result.row_summary,
            "cell_mismatches": result.cell_mismatches,
            "duplicate_key_summary": result.duplicate_key_summary,
        }
        paths: dict[str, str] = {}
        for name, df in tables.items():
            path = output_dir / f"{name}.csv"
            df.to_csv(path, index=False)
            paths[name] = str(path)
        return paths

    def _write_excel(self, result: ProcCompareResult) -> str:
        from ExcelMaster.ExcelMaster import ExcelMaster

        excel_path = (
            Path(self.config.excel_output_path)
            if self.config.excel_output_path
            else Path(self.config.output_dir) / "Proc_Compare_Report.xlsx"
        )
        excel_path.parent.mkdir(parents=True, exist_ok=True)
        em = ExcelMaster(str(excel_path), verbose=False)
        tables = {
            "Coverage": result.coverage_summary,
            "Schema": result.schema_summary,
            "Column_Summary": result.column_summary,
            "Row_Summary": result.row_summary,
            "Cell_Mismatches": result.cell_mismatches,
            "Duplicate_Keys": result.duplicate_key_summary,
        }
        for sheet_name, df in tables.items():
            ws = em.add_worksheet(sheet_name[:31], zoom_perc=90)
            out = df.head(self.config.max_excel_rows).copy()
            em.write_dataframe(ws, df=out, title=sheet_name, index=False)
        em.close_workbook()
        return str(excel_path)

proc_compare

proc_compare(left: DataFrame | str | Path, right: DataFrame | str | Path, **kwargs: Any) -> ProcCompareResult

Convenience wrapper around :class:ProcCompareEngine.

源代码位于: Modeling_Tool/Core/Proc_Compare.py
def proc_compare(
    left: pd.DataFrame | str | Path,
    right: pd.DataFrame | str | Path,
    **kwargs: Any,
) -> ProcCompareResult:
    """Convenience wrapper around :class:`ProcCompareEngine`."""
    return ProcCompareEngine(ProcCompareConfig(**kwargs)).run(left, right)

斜率计算 — Slope_Tool

Slope_Tool

SlopeCalculator

斜率计算器。

提供多种方法计算数据列的线性回归斜率,支持: - sklearn LinearRegression - scipy.stats.linregress - numpy.polyfit - 手动最小二乘法

参数:

名称 类型 描述 默认
data DataFrame

包含数据的DataFrame

必需
column str

数据列名

必需

属性:

名称 类型 描述
data DataFrame

输入数据

column str

列名

y ndarray

转换后的数据数组

x ndarray

x轴数组(索引)

示例:

>>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
>>> calc = SlopeCalculator(df, 'values')
>>> calc.calculate_sklearn()
1.0
>>> calc.calculate_scipy()
(1.0, 1.0, 9.999999999999996e-08, 0.0)
源代码位于: Modeling_Tool/Core/Slope_Tool.py
class SlopeCalculator:
    """
    斜率计算器。

    提供多种方法计算数据列的线性回归斜率,支持:
    - sklearn LinearRegression
    - scipy.stats.linregress
    - numpy.polyfit
    - 手动最小二乘法

    Parameters
    ----------
    data : pandas.DataFrame
        包含数据的DataFrame
    column : str
        数据列名

    Attributes
    ----------
    data : pandas.DataFrame
        输入数据
    column : str
        列名
    y : numpy.ndarray
        转换后的数据数组
    x : numpy.ndarray
        x轴数组(索引)

    Examples
    --------
    >>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
    >>> calc = SlopeCalculator(df, 'values')
    >>> calc.calculate_sklearn()
    1.0
    >>> calc.calculate_scipy()
    (1.0, 1.0, 9.999999999999996e-08, 0.0)
    """

    def __init__(self, data, column):
        """
        初始化斜率计算器。

        Parameters
        ----------
        data : pandas.DataFrame
            包含数据的DataFrame
        column : str
            数据列名
        """
        self.data = data
        self.column = column
        self.series = data[column]
        self.y = np.array(self.series)
        self.x = np.arange(len(self.series))

    def calculate_sklearn(self):
        """
        使用sklearn LinearRegression计算斜率。

        Returns
        -------
        float
            线性回归的斜率值
        """
        return calculate_slope_sklearn(self.data, self.column)

    def calculate_scipy(self):
        """
        使用scipy.stats.linregress计算斜率。

        Returns
        -------
        tuple
            (slope, r_value, p_value, std_err) 元组
        """
        return calculate_slope_scipy(self.data, self.column)

    def calculate_numpy(self):
        """
        使用numpy.polyfit计算斜率。

        Returns
        -------
        float
            线性回归的斜率值
        """
        return calculate_slope_numpy(self.data, self.column)

    def calculate_manual(self):
        """
        使用手动最小二乘法计算斜率。

        Returns
        -------
        float
            线性回归的斜率值
        """
        return calculate_slope_manual(self.data, self.column)

    def calculate_all(self):
        """
        使用所有方法计算斜率。

        Returns
        -------
        dict
            包含各种方法计算结果的字典
        """
        results = {}

        # sklearn方法
        results['sklearn'] = self.calculate_sklearn()

        # scipy方法
        scipy_result = self.calculate_scipy()
        results['scipy_slope'] = scipy_result[0]
        results['scipy_r_value'] = scipy_result[1]
        results['scipy_p_value'] = scipy_result[2]
        results['scipy_std_err'] = scipy_result[3]

        # numpy方法
        results['numpy'] = self.calculate_numpy()

        # 手动方法
        results['manual'] = self.calculate_manual()

        return results

    @staticmethod
    def calculate(data, column, method='sklearn'):
        """
        静态方法:使用指定方法计算斜率。

        Parameters
        ----------
        data : pandas.DataFrame
            包含数据的DataFrame
        column : str
            数据列名
        method : str, default 'sklearn'
            计算方法,候选值:'sklearn', 'scipy', 'numpy', 'manual'

        Returns
        -------
        float or tuple
            斜率值(scipy返回元组,其他返回浮点数)

        Examples
        --------
        >>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
        >>> SlopeCalculator.calculate(df, 'values', method='numpy')
        1.0
        """
        calc = SlopeCalculator(data, column)

        if method == 'sklearn':
            return calc.calculate_sklearn()
        elif method == 'scipy':
            return calc.calculate_scipy()
        elif method == 'numpy':
            return calc.calculate_numpy()
        elif method == 'manual':
            return calc.calculate_manual()
        else:
            raise ValueError(f"不支持的方法: {method}。请选择: 'sklearn', 'scipy', 'numpy', 'manual'")

calculate_sklearn

calculate_sklearn()

使用sklearn LinearRegression计算斜率。

返回:

类型 描述
float

线性回归的斜率值

源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_sklearn(self):
    """
    使用sklearn LinearRegression计算斜率。

    Returns
    -------
    float
        线性回归的斜率值
    """
    return calculate_slope_sklearn(self.data, self.column)

calculate_scipy

calculate_scipy()

使用scipy.stats.linregress计算斜率。

返回:

类型 描述
tuple

(slope, r_value, p_value, std_err) 元组

源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_scipy(self):
    """
    使用scipy.stats.linregress计算斜率。

    Returns
    -------
    tuple
        (slope, r_value, p_value, std_err) 元组
    """
    return calculate_slope_scipy(self.data, self.column)

calculate_numpy

calculate_numpy()

使用numpy.polyfit计算斜率。

返回:

类型 描述
float

线性回归的斜率值

源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_numpy(self):
    """
    使用numpy.polyfit计算斜率。

    Returns
    -------
    float
        线性回归的斜率值
    """
    return calculate_slope_numpy(self.data, self.column)

calculate_manual

calculate_manual()

使用手动最小二乘法计算斜率。

返回:

类型 描述
float

线性回归的斜率值

源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_manual(self):
    """
    使用手动最小二乘法计算斜率。

    Returns
    -------
    float
        线性回归的斜率值
    """
    return calculate_slope_manual(self.data, self.column)

calculate_all

calculate_all()

使用所有方法计算斜率。

返回:

类型 描述
dict

包含各种方法计算结果的字典

源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_all(self):
    """
    使用所有方法计算斜率。

    Returns
    -------
    dict
        包含各种方法计算结果的字典
    """
    results = {}

    # sklearn方法
    results['sklearn'] = self.calculate_sklearn()

    # scipy方法
    scipy_result = self.calculate_scipy()
    results['scipy_slope'] = scipy_result[0]
    results['scipy_r_value'] = scipy_result[1]
    results['scipy_p_value'] = scipy_result[2]
    results['scipy_std_err'] = scipy_result[3]

    # numpy方法
    results['numpy'] = self.calculate_numpy()

    # 手动方法
    results['manual'] = self.calculate_manual()

    return results

calculate staticmethod

calculate(data, column, method='sklearn')

静态方法:使用指定方法计算斜率。

参数:

名称 类型 描述 默认
data DataFrame

包含数据的DataFrame

必需
column str

数据列名

必需
method str

计算方法,候选值:'sklearn', 'scipy', 'numpy', 'manual'

'sklearn'

返回:

类型 描述
float or tuple

斜率值(scipy返回元组,其他返回浮点数)

示例:

>>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
>>> SlopeCalculator.calculate(df, 'values', method='numpy')
1.0
源代码位于: Modeling_Tool/Core/Slope_Tool.py
@staticmethod
def calculate(data, column, method='sklearn'):
    """
    静态方法:使用指定方法计算斜率。

    Parameters
    ----------
    data : pandas.DataFrame
        包含数据的DataFrame
    column : str
        数据列名
    method : str, default 'sklearn'
        计算方法,候选值:'sklearn', 'scipy', 'numpy', 'manual'

    Returns
    -------
    float or tuple
        斜率值(scipy返回元组,其他返回浮点数)

    Examples
    --------
    >>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
    >>> SlopeCalculator.calculate(df, 'values', method='numpy')
    1.0
    """
    calc = SlopeCalculator(data, column)

    if method == 'sklearn':
        return calc.calculate_sklearn()
    elif method == 'scipy':
        return calc.calculate_scipy()
    elif method == 'numpy':
        return calc.calculate_numpy()
    elif method == 'manual':
        return calc.calculate_manual()
    else:
        raise ValueError(f"不支持的方法: {method}。请选择: 'sklearn', 'scipy', 'numpy', 'manual'")

calculate_slope_sklearn

calculate_slope_sklearn(data, column)

使用SKlearn的LinearRegression计算数据列的斜率。

基于最小二乘法,通过LinearRegression模型拟合数据点, 返回线性回归的斜率系数。

参数:

名称 类型 描述 默认
data DataFrame

包含数据的DataFrame

必需
column str

数据列名

必需

返回:

类型 描述
float

线性回归的斜率值

示例:

>>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
>>> calculate_slope_sklearn(df, 'values')
1.0
源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_slope_sklearn(data, column):
    """
    使用SKlearn的LinearRegression计算数据列的斜率。

    基于最小二乘法,通过LinearRegression模型拟合数据点,
    返回线性回归的斜率系数。

    Parameters
    ----------
    data : pandas.DataFrame
        包含数据的DataFrame
    column : str
        数据列名

    Returns
    -------
    float
        线性回归的斜率值

    Examples
    --------
    >>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
    >>> calculate_slope_sklearn(df, 'values')
    1.0
    """

    from sklearn.linear_model import LinearRegression

    series = data[column]
    # 确保数据是NumPy数组格式
    y = np.array(series).reshape(-1, 1)

    # 创建x轴(索引)
    x = np.arange(len(series)).reshape(-1, 1)

    # 创建并拟合线性回归模型
    model = LinearRegression()
    model.fit(x, y)

    # 获取斜率
    slope = model.coef_[0][0]

    return slope

calculate_slope_scipy

calculate_slope_scipy(data, column)

使用SciPy的linregress函数计算数据列的斜率。

基于最小二乘法,通过scipy.stats.linregress函数拟合数据点, 返回斜率及更多统计信息。

参数:

名称 类型 描述 默认
data DataFrame

包含数据的DataFrame

必需
column str

数据列名

必需

返回:

类型 描述
tuple

(slope, r_value, p_value, std_err) 元组,包含: - slope: 斜率值 - r_value: 相关系数 - p_value: p值 - std_err: 标准误差

示例:

>>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
>>> slope, r, p, se = calculate_slope_scipy(df, 'values')
>>> print(f"斜率: {slope}, 相关系数: {r}")
斜率: 1.0, 相关系数: 1.0
源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_slope_scipy(data, column):
    """
    使用SciPy的linregress函数计算数据列的斜率。

    基于最小二乘法,通过scipy.stats.linregress函数拟合数据点,
    返回斜率及更多统计信息。

    Parameters
    ----------
    data : pandas.DataFrame
        包含数据的DataFrame
    column : str
        数据列名

    Returns
    -------
    tuple
        (slope, r_value, p_value, std_err) 元组,包含:
        - slope: 斜率值
        - r_value: 相关系数
        - p_value: p值
        - std_err: 标准误差

    Examples
    --------
    >>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
    >>> slope, r, p, se = calculate_slope_scipy(df, 'values')
    >>> print(f"斜率: {slope}, 相关系数: {r}")
    斜率: 1.0, 相关系数: 1.0
    """

    from scipy import stats

    series = data[column]
    # 确保数据是NumPy数组格式
    y = np.array(series)

    # 创建x轴(索引)
    x = np.arange(len(y))

    # 执行线性回归
    slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)

    return slope, r_value, p_value, std_err

calculate_slope_numpy

calculate_slope_numpy(data, column)

使用NumPy的polyfit函数计算数据列的斜率。

使用numpy.polyfit函数进行一阶多项式拟合, 返回线性回归的斜率。

参数:

名称 类型 描述 默认
data DataFrame

包含数据的DataFrame

必需
column str

数据列名

必需

返回:

类型 描述
float

线性回归的斜率值

示例:

>>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
>>> calculate_slope_numpy(df, 'values')
1.0
源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_slope_numpy(data, column):
    """
    使用NumPy的polyfit函数计算数据列的斜率。

    使用numpy.polyfit函数进行一阶多项式拟合,
    返回线性回归的斜率。

    Parameters
    ----------
    data : pandas.DataFrame
        包含数据的DataFrame
    column : str
        数据列名

    Returns
    -------
    float
        线性回归的斜率值

    Examples
    --------
    >>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
    >>> calculate_slope_numpy(df, 'values')
    1.0
    """

    import numpy as np

    series = data[column]
    # 确保数据是NumPy数组格式
    y = np.array(series)

    # 创建x轴(索引)
    x = np.arange(len(y))

    # 使用一次多项式拟合(线性回归),返回斜率和截距
    slope, intercept = np.polyfit(x, y, 1)

    return slope

calculate_slope_manual

calculate_slope_manual(data, column)

手动使用最小二乘法计算数据列的斜率。

通过手动实现最小二乘法公式,计算线性回归的斜率: slope = Σ((x - x_mean) * (y - y_mean)) / Σ((x - x_mean)²)

参数:

名称 类型 描述 默认
data DataFrame

包含数据的DataFrame

必需
column str

数据列名

必需

返回:

类型 描述
float

线性回归的斜率值

示例:

>>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
>>> calculate_slope_manual(df, 'values')
1.0
源代码位于: Modeling_Tool/Core/Slope_Tool.py
def calculate_slope_manual(data, column):
    """
    手动使用最小二乘法计算数据列的斜率。

    通过手动实现最小二乘法公式,计算线性回归的斜率:
    slope = Σ((x - x_mean) * (y - y_mean)) / Σ((x - x_mean)²)

    Parameters
    ----------
    data : pandas.DataFrame
        包含数据的DataFrame
    column : str
        数据列名

    Returns
    -------
    float
        线性回归的斜率值

    Examples
    --------
    >>> df = pd.DataFrame({'values': [1, 2, 3, 4, 5]})
    >>> calculate_slope_manual(df, 'values')
    1.0
    """

    series = data[column]

    # 确保数据是NumPy数组格式
    y = np.array(series)

    # 创建x轴(索引)
    x = np.arange(len(y))

    # 计算x和y的平均值
    x_mean = np.mean(x)
    y_mean = np.mean(y)

    # 计算斜率和截距
    numerator = np.sum((x - x_mean) * (y - y_mean))
    denominator = np.sum((x - x_mean) ** 2)

    slope = numerator / denominator

    return slope

通用工具 — utils

utils

DataFrameProcessor

DataFrame处理工具类。

提供DataFrame操作的统一接口,包括列操作、行过滤、类型转换等功能。

参数:

名称 类型 描述 默认
data DataFrame

要处理的DataFrame

必需

示例:

>>> processor = DataFrameProcessor(df)
>>> processor.move_column('col_a', 0)
>>> processor.convert_colnames('lower')
源代码位于: Modeling_Tool/Core/utils.py
class DataFrameProcessor:
    """
    DataFrame处理工具类。

    提供DataFrame操作的统一接口,包括列操作、行过滤、类型转换等功能。

    Parameters
    ----------
    data : pandas.DataFrame
        要处理的DataFrame

    Examples
    --------
    >>> processor = DataFrameProcessor(df)
    >>> processor.move_column('col_a', 0)
    >>> processor.convert_colnames('lower')
    """

    def __init__(self, data):
        """
        初始化DataFrame处理器。

        Parameters
        ----------
        data : pandas.DataFrame
            要处理的DataFrame
        """
        self.data = data

    def move_column(self, colname, idx, return_kDF=True, h2o_frame=False):
        """
        移动列到指定位置。

        Parameters
        ----------
        colname : str
            要移动的列名
        idx : int
            目标位置索引
        return_kDF : bool, default True
            是否返回kDataFrame
        h2o_frame : bool, default False
            输入是否为H2OFrame

        Returns
        -------
        DataFrame or kDataFrame
        """
        return move_column(self.data, colname, idx, return_kDF, h2o_frame)

    def convert_colnames(self, how="lowercase", return_kDF=True):
        """
        转换列名格式。

        Parameters
        ----------
        how : str, default "lowercase"
            转换方式
        return_kDF : bool, default True
            是否返回kDataFrame

        Returns
        -------
        DataFrame or kDataFrame
        """
        return convert_colnames(self.data, how, return_kDF)

    def col_filter_regex(self, regex, case_sensitive=True, h2o_frame=False, return_kDF=True):
        """
        使用正则表达式过滤列。

        Parameters
        ----------
        regex : str
            正则表达式
        case_sensitive : bool, default True
            是否区分大小写
        h2o_frame : bool, default False
            输入是否为H2OFrame
        return_kDF : bool, default True
            是否返回kDataFrame

        Returns
        -------
        DataFrame or kDataFrame
        """
        return col_filter_regex(self.data, regex, case_sensitive, h2o_frame, return_kDF)

    def row_filter_regex(self, col, regex, case_sensitive=True, as_index=False, return_kDF=True):
        """
        使用正则表达式过滤行。

        Parameters
        ----------
        col : str
            用于过滤的列名
        regex : str
            正则表达式
        case_sensitive : bool, default True
            是否区分大小写
        as_index : bool, default False
            是否将过滤列作为索引
        return_kDF : bool, default True
            是否返回kDataFrame

        Returns
        -------
        DataFrame or kDataFrame
        """
        return row_filter_regex(self.data, col, regex, case_sensitive, as_index, return_kDF)

    def get_dtypes(self, outputFile=None, ck_format=False):
        """
        获取数据类型。

        Parameters
        ----------
        outputFile : str, optional
            输出文件路径
        ck_format : bool, default False
            是否使用自定义格式

        Returns
        -------
        pandas.DataFrame
        """
        return get_dtypes_file(self.data, outputFile, ck_format)

    def drop_tmp_cols(self, drop_list=['py_inserttime']):
        """
        删除临时列。

        Parameters
        ----------
        drop_list : list, default ['py_inserttime']
            要删除的列列表

        Returns
        -------
        DataFrame
        """
        return drop_tmp_cols(self.data, drop_list)

    def to_bool_str(self):
        """
        将布尔列转换为字符串。

        Returns
        -------
        DataFrame
        """
        return bool_to_str(self.data)

move_column

move_column(colname, idx, return_kDF=True, h2o_frame=False)

移动列到指定位置。

参数:

名称 类型 描述 默认
colname str

要移动的列名

必需
idx int

目标位置索引

必需
return_kDF bool

是否返回kDataFrame

True
h2o_frame bool

输入是否为H2OFrame

False

返回:

类型 描述
DataFrame or kDataFrame
源代码位于: Modeling_Tool/Core/utils.py
def move_column(self, colname, idx, return_kDF=True, h2o_frame=False):
    """
    移动列到指定位置。

    Parameters
    ----------
    colname : str
        要移动的列名
    idx : int
        目标位置索引
    return_kDF : bool, default True
        是否返回kDataFrame
    h2o_frame : bool, default False
        输入是否为H2OFrame

    Returns
    -------
    DataFrame or kDataFrame
    """
    return move_column(self.data, colname, idx, return_kDF, h2o_frame)

convert_colnames

convert_colnames(how='lowercase', return_kDF=True)

转换列名格式。

参数:

名称 类型 描述 默认
how str

转换方式

"lowercase"
return_kDF bool

是否返回kDataFrame

True

返回:

类型 描述
DataFrame or kDataFrame
源代码位于: Modeling_Tool/Core/utils.py
def convert_colnames(self, how="lowercase", return_kDF=True):
    """
    转换列名格式。

    Parameters
    ----------
    how : str, default "lowercase"
        转换方式
    return_kDF : bool, default True
        是否返回kDataFrame

    Returns
    -------
    DataFrame or kDataFrame
    """
    return convert_colnames(self.data, how, return_kDF)

col_filter_regex

col_filter_regex(regex, case_sensitive=True, h2o_frame=False, return_kDF=True)

使用正则表达式过滤列。

参数:

名称 类型 描述 默认
regex str

正则表达式

必需
case_sensitive bool

是否区分大小写

True
h2o_frame bool

输入是否为H2OFrame

False
return_kDF bool

是否返回kDataFrame

True

返回:

类型 描述
DataFrame or kDataFrame
源代码位于: Modeling_Tool/Core/utils.py
def col_filter_regex(self, regex, case_sensitive=True, h2o_frame=False, return_kDF=True):
    """
    使用正则表达式过滤列。

    Parameters
    ----------
    regex : str
        正则表达式
    case_sensitive : bool, default True
        是否区分大小写
    h2o_frame : bool, default False
        输入是否为H2OFrame
    return_kDF : bool, default True
        是否返回kDataFrame

    Returns
    -------
    DataFrame or kDataFrame
    """
    return col_filter_regex(self.data, regex, case_sensitive, h2o_frame, return_kDF)

row_filter_regex

row_filter_regex(col, regex, case_sensitive=True, as_index=False, return_kDF=True)

使用正则表达式过滤行。

参数:

名称 类型 描述 默认
col str

用于过滤的列名

必需
regex str

正则表达式

必需
case_sensitive bool

是否区分大小写

True
as_index bool

是否将过滤列作为索引

False
return_kDF bool

是否返回kDataFrame

True

返回:

类型 描述
DataFrame or kDataFrame
源代码位于: Modeling_Tool/Core/utils.py
def row_filter_regex(self, col, regex, case_sensitive=True, as_index=False, return_kDF=True):
    """
    使用正则表达式过滤行。

    Parameters
    ----------
    col : str
        用于过滤的列名
    regex : str
        正则表达式
    case_sensitive : bool, default True
        是否区分大小写
    as_index : bool, default False
        是否将过滤列作为索引
    return_kDF : bool, default True
        是否返回kDataFrame

    Returns
    -------
    DataFrame or kDataFrame
    """
    return row_filter_regex(self.data, col, regex, case_sensitive, as_index, return_kDF)

get_dtypes

get_dtypes(outputFile=None, ck_format=False)

获取数据类型。

参数:

名称 类型 描述 默认
outputFile str

输出文件路径

None
ck_format bool

是否使用自定义格式

False

返回:

类型 描述
DataFrame
源代码位于: Modeling_Tool/Core/utils.py
def get_dtypes(self, outputFile=None, ck_format=False):
    """
    获取数据类型。

    Parameters
    ----------
    outputFile : str, optional
        输出文件路径
    ck_format : bool, default False
        是否使用自定义格式

    Returns
    -------
    pandas.DataFrame
    """
    return get_dtypes_file(self.data, outputFile, ck_format)

drop_tmp_cols

drop_tmp_cols(drop_list=['py_inserttime'])

删除临时列。

参数:

名称 类型 描述 默认
drop_list list

要删除的列列表

['py_inserttime']

返回:

类型 描述
DataFrame
源代码位于: Modeling_Tool/Core/utils.py
def drop_tmp_cols(self, drop_list=['py_inserttime']):
    """
    删除临时列。

    Parameters
    ----------
    drop_list : list, default ['py_inserttime']
        要删除的列列表

    Returns
    -------
    DataFrame
    """
    return drop_tmp_cols(self.data, drop_list)

to_bool_str

to_bool_str()

将布尔列转换为字符串。

返回:

类型 描述
DataFrame
源代码位于: Modeling_Tool/Core/utils.py
def to_bool_str(self):
    """
    将布尔列转换为字符串。

    Returns
    -------
    DataFrame
    """
    return bool_to_str(self.data)

FilePathManager

文件路径管理工具类。

提供路径操作、文件列表获取、目录创建等功能。

示例:

>>> fpm = FilePathManager('/base/path')
>>> fpm.get_filenames('.*\.csv')
>>> fpm.mkdir_if_not_exist('output')
源代码位于: Modeling_Tool/Core/utils.py
class FilePathManager:
    """
    文件路径管理工具类。

    提供路径操作、文件列表获取、目录创建等功能。

    Examples
    --------
    >>> fpm = FilePathManager('/base/path')
    >>> fpm.get_filenames('.*\\.csv')
    >>> fpm.mkdir_if_not_exist('output')
    """

    def __init__(self, base_path=None):
        """
        初始化路径管理器。

        Parameters
        ----------
        base_path : str, optional
            基础路径
        """
        self.base_path = base_path or os.getcwd()

    def get_filenames(self, path, regex):
        """
        获取匹配的文件名列表。

        Parameters
        ----------
        path : str
            目录路径
        regex : str
            正则表达式

        Returns
        -------
        list
        """
        return get_filenames(path, regex)

    def add_suffix(self, file, suffix="_cut"):
        """
        添加文件后缀。

        Parameters
        ----------
        file : str
            文件路径
        suffix : str, default "_cut"
            后缀

        Returns
        -------
        str
        """
        return add_path_suffix(file, suffix)

    def mkdir(self, folder_path, replace=False):
        """
        创建目录。

        Parameters
        ----------
        folder_path : str
            目录路径
        replace : bool, default False
            是否替换已存在目录

        Returns
        -------
        int
        """
        return mkdir_if_not_exist(folder_path, replace)

    def get_curr_abs_path(self, path):
        """
        获取绝对路径。

        Parameters
        ----------
        path : str
            相对路径

        Returns
        -------
        str
        """
        return get_curr_abs_path(path)

get_filenames

get_filenames(path, regex)

获取匹配的文件名列表。

参数:

名称 类型 描述 默认
path str

目录路径

必需
regex str

正则表达式

必需

返回:

类型 描述
list
源代码位于: Modeling_Tool/Core/utils.py
def get_filenames(self, path, regex):
    """
    获取匹配的文件名列表。

    Parameters
    ----------
    path : str
        目录路径
    regex : str
        正则表达式

    Returns
    -------
    list
    """
    return get_filenames(path, regex)

add_suffix

add_suffix(file, suffix='_cut')

添加文件后缀。

参数:

名称 类型 描述 默认
file str

文件路径

必需
suffix str

后缀

"_cut"

返回:

类型 描述
str
源代码位于: Modeling_Tool/Core/utils.py
def add_suffix(self, file, suffix="_cut"):
    """
    添加文件后缀。

    Parameters
    ----------
    file : str
        文件路径
    suffix : str, default "_cut"
        后缀

    Returns
    -------
    str
    """
    return add_path_suffix(file, suffix)

mkdir

mkdir(folder_path, replace=False)

创建目录。

参数:

名称 类型 描述 默认
folder_path str

目录路径

必需
replace bool

是否替换已存在目录

False

返回:

类型 描述
int
源代码位于: Modeling_Tool/Core/utils.py
def mkdir(self, folder_path, replace=False):
    """
    创建目录。

    Parameters
    ----------
    folder_path : str
        目录路径
    replace : bool, default False
        是否替换已存在目录

    Returns
    -------
    int
    """
    return mkdir_if_not_exist(folder_path, replace)

get_curr_abs_path

get_curr_abs_path(path)

获取绝对路径。

参数:

名称 类型 描述 默认
path str

相对路径

必需

返回:

类型 描述
str
源代码位于: Modeling_Tool/Core/utils.py
def get_curr_abs_path(self, path):
    """
    获取绝对路径。

    Parameters
    ----------
    path : str
        相对路径

    Returns
    -------
    str
    """
    return get_curr_abs_path(path)

DateTimeUtils

日期时间工具类。

提供日期时间相关的便捷方法。

示例:

>>> dt_utils = DateTimeUtils()
>>> dt_utils.get_curr_datetime('-')
'2025-03-30-143624'
>>> dt_utils.get_last_vintage()
'202502'
源代码位于: Modeling_Tool/Core/utils.py
class DateTimeUtils:
    """
    日期时间工具类。

    提供日期时间相关的便捷方法。

    Examples
    --------
    >>> dt_utils = DateTimeUtils()
    >>> dt_utils.get_curr_datetime('-')
    '2025-03-30-143624'
    >>> dt_utils.get_last_vintage()
    '202502'
    """

    def get_curr_datetime(self, sep=''):
        """
        获取当前日期时间。

        Parameters
        ----------
        sep : str, default ''
            分隔符

        Returns
        -------
        str
        """
        return get_curr_datetime(sep)

    def get_buffer_date(self, start_date):
        """
        获取缓冲日期。

        Parameters
        ----------
        start_date : str
            起始日期

        Returns
        -------
        str
        """
        return get_buffer_date(start_date)

    def get_quarter(self, strDate):
        """
        获取季度。

        Parameters
        ----------
        strDate : str
            日期字符串

        Returns
        -------
        int
        """
        return get_quarter(strDate)

    def get_last_vintage(self):
        """
        获取上一个月的vintage。

        Returns
        -------
        str
        """
        return get_last_vintage()

    def last_month_vintage(self, year, month, day):
        """
        获取上个月vintage。

        Parameters
        ----------
        year : int
        month : int
        day : int

        Returns
        -------
        int
        """
        return last_Month_Vintage(year, month, day)

    def get_valid_vintages(self, sVintage, eVintage):
        """
        获取有效vintage列表。

        Parameters
        ----------
        sVintage : int
        eVintage : int

        Returns
        -------
        list
        """
        return get_valid_vintages(sVintage, eVintage)

get_curr_datetime

get_curr_datetime(sep='')

获取当前日期时间。

参数:

名称 类型 描述 默认
sep str

分隔符

''

返回:

类型 描述
str
源代码位于: Modeling_Tool/Core/utils.py
def get_curr_datetime(self, sep=''):
    """
    获取当前日期时间。

    Parameters
    ----------
    sep : str, default ''
        分隔符

    Returns
    -------
    str
    """
    return get_curr_datetime(sep)

get_buffer_date

get_buffer_date(start_date)

获取缓冲日期。

参数:

名称 类型 描述 默认
start_date str

起始日期

必需

返回:

类型 描述
str
源代码位于: Modeling_Tool/Core/utils.py
def get_buffer_date(self, start_date):
    """
    获取缓冲日期。

    Parameters
    ----------
    start_date : str
        起始日期

    Returns
    -------
    str
    """
    return get_buffer_date(start_date)

get_quarter

get_quarter(strDate)

获取季度。

参数:

名称 类型 描述 默认
strDate str

日期字符串

必需

返回:

类型 描述
int
源代码位于: Modeling_Tool/Core/utils.py
def get_quarter(self, strDate):
    """
    获取季度。

    Parameters
    ----------
    strDate : str
        日期字符串

    Returns
    -------
    int
    """
    return get_quarter(strDate)

get_last_vintage

get_last_vintage()

获取上一个月的vintage。

返回:

类型 描述
str
源代码位于: Modeling_Tool/Core/utils.py
def get_last_vintage(self):
    """
    获取上一个月的vintage。

    Returns
    -------
    str
    """
    return get_last_vintage()

last_month_vintage

last_month_vintage(year, month, day)

获取上个月vintage。

参数:

名称 类型 描述 默认
year int
必需
month int
必需
day int
必需

返回:

类型 描述
int
源代码位于: Modeling_Tool/Core/utils.py
def last_month_vintage(self, year, month, day):
    """
    获取上个月vintage。

    Parameters
    ----------
    year : int
    month : int
    day : int

    Returns
    -------
    int
    """
    return last_Month_Vintage(year, month, day)

get_valid_vintages

get_valid_vintages(sVintage, eVintage)

获取有效vintage列表。

参数:

名称 类型 描述 默认
sVintage int
必需
eVintage int
必需

返回:

类型 描述
list
源代码位于: Modeling_Tool/Core/utils.py
def get_valid_vintages(self, sVintage, eVintage):
    """
    获取有效vintage列表。

    Parameters
    ----------
    sVintage : int
    eVintage : int

    Returns
    -------
    list
    """
    return get_valid_vintages(sVintage, eVintage)

WOEIVCalculator

WOE和IV计算工具类。

提供信用评分中常用的WOE和IV指标计算功能。

参数:

名称 类型 描述 默认
data DataFrame

包含比例的数据表

必需
bad_pct_col str

坏样本占比列名

必需
good_pct_col str

好样本占比列名

必需

示例:

>>> calc = WOEIVCalculator(df, 'bad_pct', 'good_pct')
>>> calc.woe()
>>> calc.iv()
源代码位于: Modeling_Tool/Core/utils.py
class WOEIVCalculator:
    """
    WOE和IV计算工具类。

    提供信用评分中常用的WOE和IV指标计算功能。

    Parameters
    ----------
    data : pandas.DataFrame
        包含比例的数据表
    bad_pct_col : str
        坏样本占比列名
    good_pct_col : str
        好样本占比列名

    Examples
    --------
    >>> calc = WOEIVCalculator(df, 'bad_pct', 'good_pct')
    >>> calc.woe()
    >>> calc.iv()
    """

    def __init__(self, data, bad_pct_col, good_pct_col):
        """
        初始化WOE/IV计算器。

        Parameters
        ----------
        data : pandas.DataFrame
            数据表
        bad_pct_col : str
            坏样本占比列名
        good_pct_col : str
            好样本占比列名
        """
        self.data = data
        self.bad_pct_col = bad_pct_col
        self.good_pct_col = good_pct_col

    def calc_woe(self, fillna=True):
        """
        计算WOE值。

        Parameters
        ----------
        fillna : bool, default True
            是否填充NA为0

        Returns
        -------
        float or Series
        """
        return calc_woe(self.data, self.bad_pct_col, self.good_pct_col, fillna)

    def calc_iv(self, fillna=True):
        """
        计算IV值。

        Parameters
        ----------
        fillna : bool, default True
            是否填充NA为0

        Returns
        -------
        float or Series
        """
        return calc_iv(self.data, self.bad_pct_col, self.good_pct_col, fillna)

    def calc_both(self, fillna=True):
        """
        同时计算WOE和IV。

        Parameters
        ----------
        fillna : bool, default True
            是否填充NA为0

        Returns
        -------
        tuple
        """
        woe = self.calc_woe(fillna)
        iv = self.calc_iv(fillna)
        return woe, iv

calc_woe

calc_woe(fillna=True)

计算WOE值。

参数:

名称 类型 描述 默认
fillna bool

是否填充NA为0

True

返回:

类型 描述
float or Series
源代码位于: Modeling_Tool/Core/utils.py
def calc_woe(self, fillna=True):
    """
    计算WOE值。

    Parameters
    ----------
    fillna : bool, default True
        是否填充NA为0

    Returns
    -------
    float or Series
    """
    return calc_woe(self.data, self.bad_pct_col, self.good_pct_col, fillna)

calc_iv

calc_iv(fillna=True)

计算IV值。

参数:

名称 类型 描述 默认
fillna bool

是否填充NA为0

True

返回:

类型 描述
float or Series
源代码位于: Modeling_Tool/Core/utils.py
def calc_iv(self, fillna=True):
    """
    计算IV值。

    Parameters
    ----------
    fillna : bool, default True
        是否填充NA为0

    Returns
    -------
    float or Series
    """
    return calc_iv(self.data, self.bad_pct_col, self.good_pct_col, fillna)

calc_both

calc_both(fillna=True)

同时计算WOE和IV。

参数:

名称 类型 描述 默认
fillna bool

是否填充NA为0

True

返回:

类型 描述
tuple
源代码位于: Modeling_Tool/Core/utils.py
def calc_both(self, fillna=True):
    """
    同时计算WOE和IV。

    Parameters
    ----------
    fillna : bool, default True
        是否填充NA为0

    Returns
    -------
    tuple
    """
    woe = self.calc_woe(fillna)
    iv = self.calc_iv(fillna)
    return woe, iv

bucket_by_cond

bucket_by_cond(df: DataFrame, cond_dict: dict, colname: str, drop_unmatched: bool = True, default=nan) -> DataFrame

根据 query 条件字典对数据分组打标签。

参数:

名称 类型 描述 默认
df 原始 DataFrame
必需
cond_dict 标签: query条件字符串
标签: query条件字符串
colname 新增的标签列名
必需
drop_unmatched True=丢弃未命中行(原逻辑), False=保留全量
True
default 未命中行的默认值(仅 drop_unmatched=False 时生效)
nan
源代码位于: Modeling_Tool/Core/utils.py
def bucket_by_cond(df: pd.DataFrame, cond_dict: dict, colname: str, 
                   drop_unmatched: bool = True, default=np.nan) -> pd.DataFrame:
    """
    根据 query 条件字典对数据分组打标签。

    Parameters
    ----------
    df : 原始 DataFrame
    cond_dict : {标签: query条件字符串}
    colname : 新增的标签列名
    drop_unmatched : True=丢弃未命中行(原逻辑), False=保留全量
    default : 未命中行的默认值(仅 drop_unmatched=False 时生效)
    """
    if drop_unmatched:
        res_list = []
        for label, cond in cond_dict.items():
            sub = df.query(cond).copy()
            sub[colname] = label
            res_list.append(sub)
        return pd.concat(res_list)
    else:
        result = df.copy()
        result[colname] = default
        for label, cond in cond_dict.items():
            mask = result.eval(cond)
            result.loc[mask, colname] = label
        return result

cut2pieces

cut2pieces(varlist, n=4)

将列表切分为多个子列表。

根据指定的切分数量,将列表均匀地切分为多个子列表。

参数:

名称 类型 描述 默认
varlist list

需要切分的列表

必需
n int

切分的子列表数量

4

返回:

类型 描述
list

切分后的子列表

示例:

>>> cut2pieces([1, 2, 3, 4, 5, 6, 7, 8], n=4)
[[1, 2], [3, 4], [5, 6], [7, 8]]
源代码位于: Modeling_Tool/Core/utils.py
def cut2pieces(varlist, n = 4):
    """
    将列表切分为多个子列表。

    根据指定的切分数量,将列表均匀地切分为多个子列表。

    Parameters
    ----------
    varlist : list
        需要切分的列表
    n : int, default 4
        切分的子列表数量

    Returns
    -------
    list
        切分后的子列表

    Examples
    --------
    >>> cut2pieces([1, 2, 3, 4, 5, 6, 7, 8], n=4)
    [[1, 2], [3, 4], [5, 6], [7, 8]]
    """

    cut_point = np.floor(len(varlist) / n)
    cut_range = range(0, len(varlist), int(cut_point))
    cut_points = [x for x in cut_range]
    cut_points = cut_points[1: len(cut_points) - 1]

    cut_list = []

    i = 0
    while i <= len(cut_points):
        if i == 0:
            cut_list.append(varlist[:cut_points[i]])
        elif i == len(cut_points):
            cut_list.append(varlist[cut_points[i - 1]:])
        else: 
            cut_list.append(varlist[cut_points[i - 1]:cut_points[i]])
        i += 1

    return cut_list

check_colname_exist

check_colname_exist(data, colname)

检查列名是否存在于DataFrame中。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
colname str

需要检查的列名

必需

返回:

类型 描述
bool

如果列名存在返回True,否则返回False

示例:

>>> df = pd.DataFrame({'a': [1, 2], 'b': [3, 4]})
>>> check_colname_exist(df, 'a')
True
源代码位于: Modeling_Tool/Core/utils.py
def check_colname_exist(data, colname):
    """
    检查列名是否存在于DataFrame中。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    colname : str
        需要检查的列名

    Returns
    -------
    bool
        如果列名存在返回True,否则返回False

    Examples
    --------
    >>> df = pd.DataFrame({'a': [1, 2], 'b': [3, 4]})
    >>> check_colname_exist(df, 'a')
    True
    """

    return colname in data.columns

get_curr_abs_path

get_curr_abs_path(path)

获取当前模块目录下指定路径的绝对路径。

参数:

名称 类型 描述 默认
path str

相对路径

必需

返回:

类型 描述
str

绝对路径字符串

源代码位于: Modeling_Tool/Core/utils.py
def get_curr_abs_path(path):
    """
    获取当前模块目录下指定路径的绝对路径。

    Parameters
    ----------
    path : str
        相对路径

    Returns
    -------
    str
        绝对路径字符串
    """
    return os.path.dirname(os.path.abspath(__file__)) + "/" + path

get_curr_datetime

get_curr_datetime(sep='')

获取当前日期时间字符串。

参数:

名称 类型 描述 默认
sep str

日期和时间之间的分隔符

''

返回:

类型 描述
str

格式化后的日期时间字符串,格式为YYYYMMDD{sep}HHMMSS

示例:

>>> get_curr_datetime()  # 返回类似 '20250330143624'
>>> get_curr_datetime('-')  # 返回类似 '20250330-143624'
源代码位于: Modeling_Tool/Core/utils.py
def get_curr_datetime(sep=''):
    """
    获取当前日期时间字符串。

    Parameters
    ----------
    sep : str, default ''
        日期和时间之间的分隔符

    Returns
    -------
    str
        格式化后的日期时间字符串,格式为YYYYMMDD{sep}HHMMSS

    Examples
    --------
    >>> get_curr_datetime()  # 返回类似 '20250330143624'
    >>> get_curr_datetime('-')  # 返回类似 '20250330-143624'
    """
    import datetime as dt
    return dt.datetime.now().strftime(f"%Y%m%d{sep}%H%M%S")

get_buffer_date

get_buffer_date(start_date)

获取起始日期前4周(28天)的日期。

参数:

名称 类型 描述 默认
start_date str

起始日期,格式为'YYYY-MM-DD'

必需

返回:

类型 描述
str

起始日期前4周的日期,格式为'YYYY-MM-DD'

示例:

>>> get_buffer_date('2025-03-30')
'2025-03-02'
源代码位于: Modeling_Tool/Core/utils.py
def get_buffer_date(start_date):
    """
    获取起始日期前4周(28天)的日期。

    Parameters
    ----------
    start_date : str
        起始日期,格式为'YYYY-MM-DD'

    Returns
    -------
    str
        起始日期前4周的日期,格式为'YYYY-MM-DD'

    Examples
    --------
    >>> get_buffer_date('2025-03-30')
    '2025-03-02'
    """
    import datetime
    d = datetime.date(int(start_date[0:4]),int(start_date[5:7]),int(start_date[8:10]))
    res = (d + datetime.timedelta(weeks=-4)).strftime("%Y-%m-%d")
    return res

get_quarter

get_quarter(strDate)

从日期字符串获取季度值。

参数:

名称 类型 描述 默认
strDate str

日期字符串,格式为'YYYYMM'或'YYYYMMDD'

必需

返回:

类型 描述
int

季度值(1-4)

示例:

>>> get_quarter('202501')
1
>>> get_quarter('202506')
2
源代码位于: Modeling_Tool/Core/utils.py
def get_quarter(strDate):
    """
    从日期字符串获取季度值。

    Parameters
    ----------
    strDate : str
        日期字符串,格式为'YYYYMM'或'YYYYMMDD'

    Returns
    -------
    int
        季度值(1-4)

    Examples
    --------
    >>> get_quarter('202501')
    1
    >>> get_quarter('202506')
    2
    """
    return ((int(strDate[4:6])-1)//3) + 1

get_last_vintage

get_last_vintage()

获取上一个月的年月字符串。

返回:

类型 描述
str

上一年月的字符串,格式为'YYYYMM'

示例:

>>> get_last_vintage()  # 如果当前是2025年3月,返回'202502'
源代码位于: Modeling_Tool/Core/utils.py
def get_last_vintage():
    """
    获取上一个月的年月字符串。

    Returns
    -------
    str
        上一年月的字符串,格式为'YYYYMM'

    Examples
    --------
    >>> get_last_vintage()  # 如果当前是2025年3月,返回'202502'
    """
    import datetime
    todayDate = datetime.date.today()
    lastM = todayDate.replace(day=1) - datetime.timedelta(days=1)
    return lastM.strftime("%Y%m")

read_csv

read_csv(path, *args, **kwargs)

读取CSV文件并返回kDataFrame对象。

参数:

名称 类型 描述 默认
path str

CSV文件路径

必需
*args

pandas.read_csv的其他位置参数

()
**kwargs

pandas.read_csv的其他关键字参数

{}

返回:

类型 描述
kDataFrame

包含数据的kDataFrame对象

示例:

>>> df = read_csv('data.csv')
源代码位于: Modeling_Tool/Core/utils.py
def read_csv(path, *args, **kwargs):
    """
    读取CSV文件并返回kDataFrame对象。

    Parameters
    ----------
    path : str
        CSV文件路径
    *args
        pandas.read_csv的其他位置参数
    **kwargs
        pandas.read_csv的其他关键字参数

    Returns
    -------
    kDataFrame
        包含数据的kDataFrame对象

    Examples
    --------
    >>> df = read_csv('data.csv')
    """
    data = kDataFrame(pd.read_csv(path, *args, **kwargs))
    return data

df_to_h2oframe

df_to_h2oframe(data)

将DataFrame转换为H2OFrame。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需

返回:

类型 描述
H2OFrame

H2OFrame对象

示例:

>>> hf = df_to_h2oframe(df)
源代码位于: Modeling_Tool/Core/utils.py
def df_to_h2oframe(data):
    """
    将DataFrame转换为H2OFrame。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表

    Returns
    -------
    h2o.H2OFrame
        H2OFrame对象

    Examples
    --------
    >>> hf = df_to_h2oframe(df)
    """
    import h2o
    if isinstance(data, h2o.H2OFrame):
        return data
    else:
        return h2o.H2OFrame(data)

move_column

move_column(data, colname, idx, return_kDF=True, h2o_frame=False)

将指定列移动到DataFrame的特定位置。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
colname str

需要移动的列名

必需
idx int

目标位置索引

必需
return_kDF bool

是否返回kDataFrame对象

True
h2o_frame bool

输入是否为H2OFrame

False

返回:

类型 描述
DataFrame or kDataFrame

列顺序调整后的数据表

示例:

>>> df = pd.DataFrame({'a': [1, 2], 'b': [3, 4], 'c': [5, 6]})
>>> move_column(df, 'c', 0)  # 将'c'列移到第一列
源代码位于: Modeling_Tool/Core/utils.py
def move_column(data, colname, idx, return_kDF = True, h2o_frame = False):
    """
    将指定列移动到DataFrame的特定位置。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    colname : str
        需要移动的列名
    idx : int
        目标位置索引
    return_kDF : bool, default True
        是否返回kDataFrame对象
    h2o_frame : bool, default False
        输入是否为H2OFrame

    Returns
    -------
    pandas.DataFrame or kDataFrame
        列顺序调整后的数据表

    Examples
    --------
    >>> df = pd.DataFrame({'a': [1, 2], 'b': [3, 4], 'c': [5, 6]})
    >>> move_column(df, 'c', 0)  # 将'c'列移到第一列
    """
    import h2o
    if h2o_frame:
        return_kDF = False
        colarray = data.columns 
    else:
        colarray = data.columns.tolist()
    colarray.remove(colname)
    colarray.insert(idx, colname)
    data = data[colarray]
    if return_kDF:
        return kDataFrame(data)
    return data

convert_to_vintage

convert_to_vintage(data, vintage_colname='VINTAGE', by='TRAN_TMS', return_kDF=True)

根据时间列生成Vintage列。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
vintage_colname str

生成的Vintage列名

'VINTAGE'
by str

时间列名

'TRAN_TMS'
return_kDF bool

是否返回kDataFrame对象

True

返回:

类型 描述
DataFrame or kDataFrame

添加了Vintage列的数据表

示例:

>>> df = pd.DataFrame({'TRAN_TMS': ['2025-03-15 10:00:00', '2025-03-20 11:00:00']})
>>> convert_to_vintage(df)
源代码位于: Modeling_Tool/Core/utils.py
def convert_to_vintage(data, vintage_colname = 'VINTAGE', by = 'TRAN_TMS', return_kDF = True):
    """
    根据时间列生成Vintage列。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    vintage_colname : str, default 'VINTAGE'
        生成的Vintage列名
    by : str, default 'TRAN_TMS'
        时间列名
    return_kDF : bool, default True
        是否返回kDataFrame对象

    Returns
    -------
    pandas.DataFrame or kDataFrame
        添加了Vintage列的数据表

    Examples
    --------
    >>> df = pd.DataFrame({'TRAN_TMS': ['2025-03-15 10:00:00', '2025-03-20 11:00:00']})
    >>> convert_to_vintage(df)
    """
    import re
    data[vintage_colname] = data[by].apply(lambda x: re.search("\\d{4}-\\d{2}", x).group().replace('-', ''))

    if return_kDF:
        return kDataFrame(data)
    return data

col_filter_regex

col_filter_regex(data, regex='.*?of_co_at_12m', case_sensitive=True, h2o_frame=False, return_kDF=True)

使用正则表达式过滤DataFrame的列名。

参数:

名称 类型 描述 默认
data DataFrame or H2OFrame

输入数据表

必需
regex str

正则表达式模式

".*?of_co_at_12m"
case_sensitive bool

是否区分大小写

True
h2o_frame bool

输入是否为H2OFrame

False
return_kDF bool

是否返回kDataFrame对象

True

返回:

类型 描述
DataFrame or kDataFrame

过滤后的数据表(只包含匹配的列)

示例:

>>> df = pd.DataFrame({'score_at_12m': [1, 2], 'other_col': [3, 4]})
>>> col_filter_regex(df, regex='score_at_12m')
源代码位于: Modeling_Tool/Core/utils.py
def col_filter_regex(data, regex = ".*?of_co_at_12m", case_sensitive = True, h2o_frame=False, return_kDF = True):
    """
    使用正则表达式过滤DataFrame的列名。

    Parameters
    ----------
    data : pandas.DataFrame or h2o.H2OFrame
        输入数据表
    regex : str, default ".*?of_co_at_12m"
        正则表达式模式
    case_sensitive : bool, default True
        是否区分大小写
    h2o_frame : bool, default False
        输入是否为H2OFrame
    return_kDF : bool, default True
        是否返回kDataFrame对象

    Returns
    -------
    pandas.DataFrame or kDataFrame
        过滤后的数据表(只包含匹配的列)

    Examples
    --------
    >>> df = pd.DataFrame({'score_at_12m': [1, 2], 'other_col': [3, 4]})
    >>> col_filter_regex(df, regex='score_at_12m')
    """
    if h2o_frame:
        return_kDF = False
        import re
        fltr = []
        for col in data.columns:
            if re.search(regex, col):
                fltr.append(col)
    else:
        fltr = data.columns[data.columns.str.contains(regex, regex = True, case = case_sensitive)]
    if return_kDF:
        return kDataFrame(data[fltr])
    return data[fltr]

row_filter_regex

row_filter_regex(data, col, regex, case_sensitive=True, as_index=False, return_kDF=True)

使用正则表达式过滤DataFrame的行。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
col str

用于过滤的列名

必需
regex str

正则表达式模式

必需
case_sensitive bool

是否区分大小写

True
as_index bool

是否将过滤列作为索引

False
return_kDF bool

是否返回kDataFrame对象

True

返回:

类型 描述
DataFrame or kDataFrame

过滤后的数据表

示例:

>>> df = pd.DataFrame({'name': ['apple', 'banana', 'cherry'], 'value': [1, 2, 3]})
>>> row_filter_regex(df, 'name', 'a.*')
源代码位于: Modeling_Tool/Core/utils.py
def row_filter_regex(data, col, regex, case_sensitive = True,
                     as_index = False, return_kDF = True):
    """
    使用正则表达式过滤DataFrame的行。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    col : str
        用于过滤的列名
    regex : str
        正则表达式模式
    case_sensitive : bool, default True
        是否区分大小写
    as_index : bool, default False
        是否将过滤列作为索引
    return_kDF : bool, default True
        是否返回kDataFrame对象

    Returns
    -------
    pandas.DataFrame or kDataFrame
        过滤后的数据表

    Examples
    --------
    >>> df = pd.DataFrame({'name': ['apple', 'banana', 'cherry'], 'value': [1, 2, 3]})
    >>> row_filter_regex(df, 'name', 'a.*')
    """
    fltr = data[col].astype('str').str.contains(pat = regex, regex = True, case = case_sensitive)
    if return_kDF:
        return kDataFrame(data[fltr])
    if as_index:
        return data[fltr].set_index(col)
    return data[fltr]

convert_colnames

convert_colnames(data, how='lowercase', return_kDF=True)

统一DataFrame列名的格式。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
how str

转换方式,可选值:'lower'/'lowercase', 'upper'/'uppercase', 'cap'/'capitalize'

"lowercase"
return_kDF bool

是否返回kDataFrame对象

True

返回:

类型 描述
DataFrame or kDataFrame

列名统一后的数据表

示例:

>>> df = pd.DataFrame({'NAME': [1], 'Age': [2]})
>>> convert_colnames(df, 'lower')
源代码位于: Modeling_Tool/Core/utils.py
def convert_colnames(data, how = "lowercase", return_kDF = True):
    """
    统一DataFrame列名的格式。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    how : str, default "lowercase"
        转换方式,可选值:'lower'/'lowercase', 'upper'/'uppercase', 'cap'/'capitalize'
    return_kDF : bool, default True
        是否返回kDataFrame对象

    Returns
    -------
    pandas.DataFrame or kDataFrame
        列名统一后的数据表

    Examples
    --------
    >>> df = pd.DataFrame({'NAME': [1], 'Age': [2]})
    >>> convert_colnames(df, 'lower')
    """
    cols = data.columns
    if how.lower() == "lower" or how.lower() == "lowercase":
        res = [name.lower() for name in cols]
    if how.lower() == "upper" or how.lower() == "uppercase":
        res = [name.upper() for name in cols]
    if how.lower() == "cap" or how.lower() == "capitalize":
        res = [name.capitalize() for name in cols]
    data.columns = res
    if return_kDF:
        return kDataFrame(data)
    return data

proc_freq

proc_freq(data, var: str, return_kDF=True) -> DataFrame

实现SAS的PROC FREQ功能,计算频数和百分比。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
var str

需要统计的列名

必需
return_kDF bool

是否返回kDataFrame对象

True

返回:

类型 描述
DataFrame

包含frequency, percent, cumFrequency, cumPercent的统计表

示例:

>>> df = pd.DataFrame({'category': ['A', 'B', 'A', 'C', 'A']})
>>> proc_freq(df, 'category')
源代码位于: Modeling_Tool/Core/utils.py
def proc_freq(data, var: str, return_kDF = True) -> pd.DataFrame:
    """
    实现SAS的PROC FREQ功能,计算频数和百分比。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    var : str
        需要统计的列名
    return_kDF : bool, default True
        是否返回kDataFrame对象

    Returns
    -------
    pandas.DataFrame
        包含frequency, percent, cumFrequency, cumPercent的统计表

    Examples
    --------
    >>> df = pd.DataFrame({'category': ['A', 'B', 'A', 'C', 'A']})
    >>> proc_freq(df, 'category')
    """

    f = data[var].value_counts(dropna = False)
    p = data[var].value_counts(dropna = False, normalize = True)
    df = pd.concat([f,p], axis = 1, keys = ['frequency', 'percent'])
    df = df.sort_index()
    df['cumFrequency'] = df['frequency'].cumsum()
    df['cumPercent'] = df['percent'].cumsum()
    if return_kDF:
        return kDataFrame(df)
    return df

proc_means

proc_means(data, varlist=None, quantiles=[0.05, 0.15, 0.25, 0.5, 0.75, 0.95, 0.99])

实现SAS的PROC MEANS功能,计算描述性统计量。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
varlist list

需要统计的列名列表,默认为所有列

None
quantiles list

分位数列表

[0.05, 0.15, 0.25, 0.5, 0.75, 0.95, 0.99]

返回:

类型 描述
DataFrame

包含统计量的数据表,包括count, mean, std, min, max及指定分位数

示例:

>>> df = pd.DataFrame({'a': [1, 2, 3, 4, 5], 'b': [10, 20, 30, 40, 50]})
>>> proc_means(df)
源代码位于: Modeling_Tool/Core/utils.py
def proc_means(data, varlist = None, quantiles = [0.05, 0.15, 0.25, 0.5, 0.75, 0.95, 0.99]):
    """
    实现SAS的PROC MEANS功能,计算描述性统计量。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    varlist : list, optional
        需要统计的列名列表,默认为所有列
    quantiles : list, default [0.05, 0.15, 0.25, 0.5, 0.75, 0.95, 0.99]
        分位数列表

    Returns
    -------
    pandas.DataFrame
        包含统计量的数据表,包括count, mean, std, min, max及指定分位数

    Examples
    --------
    >>> df = pd.DataFrame({'a': [1, 2, 3, 4, 5], 'b': [10, 20, 30, 40, 50]})
    >>> proc_means(df)
    """

    if varlist is None:
        varlist = data.columns

    means = data[varlist].describe(percentiles = quantiles).T.rename(columns={"count":"n"})

    # Rename colnames.
    means.columns = [x.upper() for x in means.columns]
    quantile_rename = {str(int(x * 100)) + "%": "Q" + str(int(x * 100)) for x in quantiles}
    means = means.rename(columns = quantile_rename)

    # Compute Missing Rate
    means["MISSING_RATE"] = 1 - means["N"]/data.shape[0]
    return means

capping_score

capping_score(data, pb_score: str, multiplier=1, df_type: str = 'DataFrame')

对模型分数进行缩放和上限处理。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
pb_score str

分数列名

必需
multiplier float

分数缩放倍数

1
df_type str

数据类型,'DataFrame'或'h2o'

'DataFrame'

返回:

类型 描述
Series or H2OFrame

处理后的分数

示例:

>>> df = pd.DataFrame({'score': [0.1, 0.5, 0.99, 1.0]})
>>> capping_score(df, 'score', multiplier=600, df_type='DataFrame')
源代码位于: Modeling_Tool/Core/utils.py
def capping_score(data, pb_score: str, multiplier = 1, df_type: str = 'DataFrame'):
    """
    对模型分数进行缩放和上限处理。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    pb_score : str
        分数列名
    multiplier : float, default 1
        分数缩放倍数
    df_type : str, default 'DataFrame'
        数据类型,'DataFrame'或'h2o'

    Returns
    -------
    pandas.Series or h2o.H2OFrame
        处理后的分数

    Examples
    --------
    >>> df = pd.DataFrame({'score': [0.1, 0.5, 0.99, 1.0]})
    >>> capping_score(df, 'score', multiplier=600, df_type='DataFrame')
    """
    scores = data[pb_score] * multiplier
    cond = (scores > 0.9999999)

    if df_type.lower() == 'h2o':
        return cond.ifelse(0.9999999, scores)

    return (0.9999999 if scores > 0.9999999 else scores)

get_filenames

get_filenames(path: str, regex: str) -> [str]

获取指定路径下匹配正则表达式的文件名列表。

参数:

名称 类型 描述 默认
path str

文件夹路径

必需
regex str

正则表达式模式

必需

返回:

类型 描述
list

匹配的文件名列表

示例:

>>> get_filenames('/path/to/files', '.*\.csv')
['file1.csv', 'file2.csv']
源代码位于: Modeling_Tool/Core/utils.py
def get_filenames(path: str, regex: str) -> [str]:
    """
    获取指定路径下匹配正则表达式的文件名列表。

    Parameters
    ----------
    path : str
        文件夹路径
    regex : str
        正则表达式模式

    Returns
    -------
    list
        匹配的文件名列表

    Examples
    --------
    >>> get_filenames('/path/to/files', '.*\\.csv')
    ['file1.csv', 'file2.csv']
    """
    import re
    outfiles = []
    for (Dirs, subdirs, files) in os.walk(path):
        for file in files:
            if re.search(regex, file):
                outfiles.append(file)
    return outfiles 

sas_to_csv_by_folder

sas_to_csv_by_folder(folder_path: str)

将指定文件夹中的所有SAS数据集转换为CSV文件。

参数:

名称 类型 描述 默认
folder_path str

包含SAS文件的文件夹路径

必需

返回:

类型 描述
int

执行状态码(0表示成功)

示例:

>>> sas_to_csv_by_folder('/path/to/sas/files')
源代码位于: Modeling_Tool/Core/utils.py
def sas_to_csv_by_folder(folder_path: str):
    """
    将指定文件夹中的所有SAS数据集转换为CSV文件。

    Parameters
    ----------
    folder_path : str
        包含SAS文件的文件夹路径

    Returns
    -------
    int
        执行状态码(0表示成功)

    Examples
    --------
    >>> sas_to_csv_by_folder('/path/to/sas/files')
    """
    filenames = get_filenames(path = folder_path, regex = ".*?sas7bdat")
    sasfilepaths = [folder_path + file for file in filenames]
    from tqdm import tqdm
    with tqdm(total = len(sasfilepaths), position = 0, leave = True, file = sys.stdout) as pbar:
        for i in range(len(sasfilepaths)):
            saspath = sasfilepaths[i]
            logger.info(f"=> converting {filenames[i]}...")
            csvpath = sasfilepaths[i].replace("sas7bdat", "csv")
            sas_to_csv(saspath, csvpath)
            pbar.update()
    return 0

read_attr_list

read_attr_list(path: str = 'pe_attr_list.txt', lower=False)

读取属性列表文件(每行一个属性)。

参数:

名称 类型 描述 默认
path str

文件路径

"pe_attr_list.txt"
lower bool

是否转换为小写

False

返回:

类型 描述
list

属性列表

示例:

>>> read_attr_list('vars.txt', lower=True)
源代码位于: Modeling_Tool/Core/utils.py
def read_attr_list(path: str = "pe_attr_list.txt", lower = False):
    """
    读取属性列表文件(每行一个属性)。

    Parameters
    ----------
    path : str, default "pe_attr_list.txt"
        文件路径
    lower : bool, default False
        是否转换为小写

    Returns
    -------
    list
        属性列表

    Examples
    --------
    >>> read_attr_list('vars.txt', lower=True)
    """
    with open(path) as f:
        lines = f.readlines()

    ls = []
    for line in lines:
        ls.append(line.strip().upper())
    f.close()
    if lower:
        return [x.lower() for x in ls]
    return ls

write_attr_list

write_attr_list(var_list: list, path: str = '_vls_results.txt', sep='\n', quote='double')
将变量列表写入文件。
Parameters
var_list : list
    要写入的变量列表
path : str, default "_vls_results.txt"
    输出文件路径
sep : str, default "

" 分隔符 quote : str, default 'double' 引号类型,'double', 'single', 或 'none'

Returns
None
Examples
>>> write_attr_list(['var1', 'var2'], 'output.txt', quote='single')
源代码位于: Modeling_Tool/Core/utils.py
def write_attr_list(var_list: list, path: str = "_vls_results.txt", sep="\n", quote='double'):
    """
    将变量列表写入文件。

    Parameters
    ----------
    var_list : list
        要写入的变量列表
    path : str, default "_vls_results.txt"
        输出文件路径
    sep : str, default "\n"
        分隔符
    quote : str, default 'double'
        引号类型,'double', 'single', 或 'none'

    Returns
    -------
    None

    Examples
    --------
    >>> write_attr_list(['var1', 'var2'], 'output.txt', quote='single')
    """
    with open(path, "w+") as f:
        for v in var_list:
            if quote == 'double':
                f.writelines('"'+str(v)+'"'+sep)
            elif quote == 'single':
                f.writelines("'"+str(v)+"'"+sep)
            else:
                f.writelines(str(v)+sep)
        f.close()
    return None

list_filter_regex

list_filter_regex(ls, regex)

使用正则表达式过滤列表元素。

参数:

名称 类型 描述 默认
ls list

输入列表

必需
regex str

正则表达式模式

必需

返回:

类型 描述
list

匹配的元素列表

示例:

>>> list_filter_regex(['abc', 'def', 'abf'], 'ab.*')
['abc', 'abf']
源代码位于: Modeling_Tool/Core/utils.py
def list_filter_regex(ls, regex):
    """
    使用正则表达式过滤列表元素。

    Parameters
    ----------
    ls : list
        输入列表
    regex : str
        正则表达式模式

    Returns
    -------
    list
        匹配的元素列表

    Examples
    --------
    >>> list_filter_regex(['abc', 'def', 'abf'], 'ab.*')
    ['abc', 'abf']
    """
    import re
    ret = []
    for elem in ls:
        if re.search(regex, elem):
            ret.append(elem)
    return ret

list_to_h2oFrame

list_to_h2oFrame(val: str or float or int, length: int)

将值转换为指定长度的H2O Frame。

参数:

名称 类型 描述 默认
val str or float or int

必需
length int

长度

必需

返回:

类型 描述
H2OFrame

包含重复值的H2OFrame

示例:

>>> list_to_h2oFrame(5, 10)
源代码位于: Modeling_Tool/Core/utils.py
def list_to_h2oFrame(val: str or float or int, length: int):
    """
    将值转换为指定长度的H2O Frame。

    Parameters
    ----------
    val : str or float or int

    length : int
        长度

    Returns
    -------
    h2o.H2OFrame
        包含重复值的H2OFrame

    Examples
    --------
    >>> list_to_h2oFrame(5, 10)
    """
    """ convert list to H2O Frame """
    import h2o
    return h2o.H2OFrame([val] * length)

odds_score

odds_score(pb_score, event_ratio=15, margin_point=20, score_point=500)

根据概率分数计算Odds分数。

用于将概率值转换为信用分数刻度。

参数:

名称 类型 描述 默认
pb_score float

预测概率(0到1之间)

必需
event_ratio float

事件比例

15
margin_point float

分数点差

20
score_point float

基础分数点

500

返回:

类型 描述
float

Odds分数

示例:

>>> odds_score(0.03, event_ratio=15, margin_point=20, score_point=500)
619.3...
源代码位于: Modeling_Tool/Core/utils.py
def odds_score(pb_score, event_ratio = 15, margin_point = 20, score_point = 500):
    """
    根据概率分数计算Odds分数。

    用于将概率值转换为信用分数刻度。

    Parameters
    ----------
    pb_score : float
        预测概率(0到1之间)
    event_ratio : float, default 15
        事件比例
    margin_point : float, default 20
        分数点差
    score_point : float, default 500
        基础分数点

    Returns
    -------
    float
        Odds分数

    Examples
    --------
    >>> odds_score(0.03, event_ratio=15, margin_point=20, score_point=500)
    619.3...
    """
    a = (margin_point / np.log(2))
    b = (np.log(event_ratio) + np.log(pb_score/(1 - pb_score)))
    return (score_point - a * b)

last_Month_Vintage

last_Month_Vintage(year: int, month: int, day: int) -> str

获取上一个月的年月字符串。

参数:

名称 类型 描述 默认
year int

年份

必需
month int

月份

必需
day int

日期

必需

返回:

类型 描述
str

上一个月的年月字符串,格式为'YYYYMM'

示例:

>>> last_Month_Vintage(2025, 3, 15)
202502
源代码位于: Modeling_Tool/Core/utils.py
def last_Month_Vintage(year: int, month: int, day: int) -> str:
    """
    获取上一个月的年月字符串。

    Parameters
    ----------
    year : int
        年份
    month : int
        月份
    day : int
        日期

    Returns
    -------
    str
        上一个月的年月字符串,格式为'YYYYMM'

    Examples
    --------
    >>> last_Month_Vintage(2025, 3, 15)
    202502
    """
    lastDate = dt(year, month, day) + rd(months=-1)
    yr = str(lastDate.year)
    mth = "0" + str(lastDate.month) if len([char for char in str(lastDate.month)]) == 1 else str(lastDate.month)
    lastMthVtge = yr + mth
    return int(lastMthVtge)

read_sas_file

read_sas_file(file_path_name='')

读取SAS数据集文件。

使用latin-1编码读取SAS文件,这是SAS Studio和SAS Grid的默认编码。

参数:

名称 类型 描述 默认
file_path_name str

SAS文件路径

''

返回:

类型 描述
kDataFrame

包含数据的kDataFrame对象

示例:

>>> df = read_sas_file('data.sas7bdat')
源代码位于: Modeling_Tool/Core/utils.py
def read_sas_file(file_path_name=''):
    """
    读取SAS数据集文件。

    使用latin-1编码读取SAS文件,这是SAS Studio和SAS Grid的默认编码。

    Parameters
    ----------
    file_path_name : str
        SAS文件路径

    Returns
    -------
    kDataFrame
        包含数据的kDataFrame对象

    Examples
    --------
    >>> df = read_sas_file('data.sas7bdat')
    """
    df = pd.read_sas(file_path_name,
        format = 'sas7bdat', encoding="latin-1")
    return kDataFrame(df)

sas_to_csv

sas_to_csv(fileNameWithPath, outputFileNameWithPath, timecounter=True)

将SAS数据集转换为CSV文件。

参数:

名称 类型 描述 默认
fileNameWithPath str

输入SAS文件路径

必需
outputFileNameWithPath str

输出CSV文件路径

必需
timecounter bool

是否打印执行时间

True

返回:

类型 描述
int

执行状态码(0表示成功)

示例:

>>> sas_to_csv('input.sas7bdat', 'output.csv')
Completed! It took 0.1234 minutes to run.
0
源代码位于: Modeling_Tool/Core/utils.py
def sas_to_csv(fileNameWithPath, outputFileNameWithPath, timecounter = True):
    """
    将SAS数据集转换为CSV文件。

    Parameters
    ----------
    fileNameWithPath : str
        输入SAS文件路径
    outputFileNameWithPath : str
        输出CSV文件路径
    timecounter : bool, default True
        是否打印执行时间

    Returns
    -------
    int
        执行状态码(0表示成功)

    Examples
    --------
    >>> sas_to_csv('input.sas7bdat', 'output.csv')
    Completed! It took 0.1234 minutes to run.
    0
    """
    import time as t

    t0 = t.time()
    df = read_sas_file(fileNameWithPath)
    df.to_csv(outputFileNameWithPath, index = False)
    t1 = t.time()
    if timecounter:
        logger.info(f"Completed! It took {round((t1 - t0)/60, 4)} minutes to run.")
    return 0

merge_all_data

merge_all_data(*args, on='APPLICATION_ID', how='left', return_kDF=True)

合并多个数据集。

参数:

名称 类型 描述 默认
*args

要合并的DataFrame列表

()
on str

连接键列名

"APPLICATION_ID"
how str

连接方式,'left', 'right', 'inner', 'outer'

"left"
return_kDF bool

是否返回kDataFrame对象

True

返回:

类型 描述
DataFrame or kDataFrame

合并后的数据表

示例:

>>> df1 = pd.DataFrame({'id': [1, 2], 'a': [3, 4]})
>>> df2 = pd.DataFrame({'id': [1, 2], 'b': [5, 6]})
>>> merge_all_data(df1, df2, on='id')
源代码位于: Modeling_Tool/Core/utils.py
def merge_all_data(*args, on = "APPLICATION_ID", how = "left", return_kDF = True):
    """
    合并多个数据集。

    Parameters
    ----------
    *args
        要合并的DataFrame列表
    on : str, default "APPLICATION_ID"
        连接键列名
    how : str, default "left"
        连接方式,'left', 'right', 'inner', 'outer'
    return_kDF : bool, default True
        是否返回kDataFrame对象

    Returns
    -------
    pandas.DataFrame or kDataFrame
        合并后的数据表

    Examples
    --------
    >>> df1 = pd.DataFrame({'id': [1, 2], 'a': [3, 4]})
    >>> df2 = pd.DataFrame({'id': [1, 2], 'b': [5, 6]})
    >>> merge_all_data(df1, df2, on='id')
    """
    argList = list(args)
    data = argList[0]
    for i in range(len(argList)-1):
        data = pd.merge(data, argList[i+1], on=on, how = how, suffixes=(f'_merge{i}', f'_merge{i+1}'))
    if return_kDF:
        return kDataFrame(data)
    return data

get_valid_vintages

get_valid_vintages(sVintage, eVintage)

获取指定范围内的有效Vintage列表。

参数:

名称 类型 描述 默认
sVintage int

起始Vintage(格式YYYYMM)

必需
eVintage int

结束Vintage(格式YYYYMM)

必需

返回:

类型 描述
list

有效的Vintage列表

示例:

>>> get_valid_vintages(202001, 202503)
[202001, 202002, ..., 202012, 202101, ..., 202503]
源代码位于: Modeling_Tool/Core/utils.py
def get_valid_vintages(sVintage, eVintage):
    """
    获取指定范围内的有效Vintage列表。

    Parameters
    ----------
    sVintage : int
        起始Vintage(格式YYYYMM)
    eVintage : int
        结束Vintage(格式YYYYMM)

    Returns
    -------
    list
        有效的Vintage列表

    Examples
    --------
    >>> get_valid_vintages(202001, 202503)
    [202001, 202002, ..., 202012, 202101, ..., 202503]
    """
    vintages = []
    for vintage in range(sVintage, eVintage + 1):
        if (vintage % 100) > 0 and (vintage % 100) < 13:
            vintages.append(vintage)
    return vintages

set_non_number_str

set_non_number_str(h2o_tbl_path)

导入文件为H2OFrame并将所有非数值列设置为字符串类型。

参数:

名称 类型 描述 默认
h2o_tbl_path str

H2O表路径

必需

返回:

类型 描述
H2OFrame

处理后的H2OFrame

示例:

>>> hf = set_non_number_str('/path/to/file.csv')
源代码位于: Modeling_Tool/Core/utils.py
def set_non_number_str(h2o_tbl_path):
    """
    导入文件为H2OFrame并将所有非数值列设置为字符串类型。

    Parameters
    ----------
    h2o_tbl_path : str
        H2O表路径

    Returns
    -------
    h2o.H2OFrame
        处理后的H2OFrame

    Examples
    --------
    >>> hf = set_non_number_str('/path/to/file.csv')
    """
    """ Import file as H2O Frame and set all non-numeric columns to string type. """
    import h2o
    h2o.init(min_mem_size='100G')
    df = h2o.import_file(h2o_tbl_path)
    orig_types = list(df.types.values())
    fnl_types = ['string' if ((tp !='int') and (tp != 'enum') and (tp != 'string') and (tp != 'real')) else str(tp) for tp in orig_types]
    fnl_df = h2o.import_file(h2o_tbl_path, col_types = fnl_types)
    return fnl_df

list_to_SQL

list_to_SQL(ls, excl=[], prefix='', wquote=False)

将列表转换为SQL格式的字符串。

参数:

名称 类型 描述 默认
ls list

输入列表

必需
excl list

要排除的元素列表

[]
prefix str

列名前缀(如表别名)

''
wquote bool

是否用引号包裹元素

False

返回:

类型 描述
str

SQL格式的字符串

示例:

>>> list_to_SQL(['col1', 'col2', 'col3'], prefix='t')
't.col1,t.col2,t.col3'
源代码位于: Modeling_Tool/Core/utils.py
def list_to_SQL(ls, excl=[], prefix = '', wquote=False):
    """
    将列表转换为SQL格式的字符串。

    Parameters
    ----------
    ls : list
        输入列表
    excl : list, default []
        要排除的元素列表
    prefix : str, default ''
        列名前缀(如表别名)
    wquote : bool, default False
        是否用引号包裹元素

    Returns
    -------
    str
        SQL格式的字符串

    Examples
    --------
    >>> list_to_SQL(['col1', 'col2', 'col3'], prefix='t')
    't.col1,t.col2,t.col3'
    """
    sqlFmt = ""

    for i, var in enumerate(ls):
        if wquote:
            var = f"'{var}'"
        if var in excl:
            continue
        if i != len(ls) - 1:
            if prefix == '' or prefix is None:
                sqlFmt += var + ","
            else:
                sqlFmt += prefix + "." + var + ","
        else:
            if prefix == '' or prefix is None:
                sqlFmt += var
            else:
                sqlFmt += prefix + "." + var
    return sqlFmt

bool_to_str

bool_to_str(data)

将DataFrame中的布尔类型列转换为字符串类型。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需

返回:

类型 描述
DataFrame

转换后的数据表

示例:

>>> df = pd.DataFrame({'a': [True, False], 'b': [1, 2]})
>>> bool_to_str(df)
源代码位于: Modeling_Tool/Core/utils.py
def bool_to_str(data):
    """
    将DataFrame中的布尔类型列转换为字符串类型。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表

    Returns
    -------
    pandas.DataFrame
        转换后的数据表

    Examples
    --------
    >>> df = pd.DataFrame({'a': [True, False], 'b': [1, 2]})
    >>> bool_to_str(df)
    """
    dfc = data.copy()
    type_dict = data.dtypes.to_dict()
    for k,v in type_dict.items():
        if str(v).lower() == 'bool':
            dfc = dfc.astype({k: str})
    return dfc

get_dtypes_file

get_dtypes_file(data, outputFile=None, ck_format=False)

获取DataFrame各列的数据类型。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
outputFile str

输出文件路径

None
ck_format bool

是否使用自定义格式

False

返回:

类型 描述
DataFrame

包含列名和数据类型的DataFrame

示例:

>>> df = pd.DataFrame({'a': [1], 'b': ['x'], 'c': [1.5]})
>>> get_dtypes_file(df)
源代码位于: Modeling_Tool/Core/utils.py
def get_dtypes_file(data, outputFile = None, ck_format=False):
    """
    获取DataFrame各列的数据类型。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    outputFile : str, optional
        输出文件路径
    ck_format : bool, default False
        是否使用自定义格式

    Returns
    -------
    pandas.DataFrame
        包含列名和数据类型的DataFrame

    Examples
    --------
    >>> df = pd.DataFrame({'a': [1], 'b': ['x'], 'c': [1.5]})
    >>> get_dtypes_file(df)
    """
    df = bool_to_str(data)
    res = pd.DataFrame(df.dtypes)
    res = res.reset_index()
    res.columns = ["colname", "dtype"]
    if ck_format:
        res['dtype'] = res['dtype'].astype(str).str.strip().map(ck_dtype)
    res['dtype'] = res['dtype'].astype(str).str.strip()
    if outputFile is not None:
        res.to_csv(outputFile, index = False, header=False)
    return res

add_path_suffix

add_path_suffix(file, suffix='_cut')

为文件路径添加后缀。

参数:

名称 类型 描述 默认
file str

文件路径

必需
suffix str

要添加的后缀

"_cut"

返回:

类型 描述
str

添加后缀后的文件路径

示例:

>>> add_path_suffix('/path/to/file.csv', '_processed')
'/path/to/file_processed.csv'
源代码位于: Modeling_Tool/Core/utils.py
def add_path_suffix(file, suffix = "_cut"):
    """
    为文件路径添加后缀。

    Parameters
    ----------
    file : str
        文件路径
    suffix : str, default "_cut"
        要添加的后缀

    Returns
    -------
    str
        添加后缀后的文件路径

    Examples
    --------
    >>> add_path_suffix('/path/to/file.csv', '_processed')
    '/path/to/file_processed.csv'
    """
    whole_path = file.split("/")
    path = [item for item in whole_path if item != whole_path[-1]]
    file = whole_path[-1].split(".")
    filename = file[0]
    ext = file[1]
    res = "/".join(path)+"/"+filename+suffix+"."+ext
    return res

h2o_apply_regex

h2o_apply_regex(data, colname, func)

对H2O Frame的列应用正则表达式转换函数。

参数:

名称 类型 描述 默认
data H2OFrame

输入数据

必需
colname str

列名

必需
func callable

应用函数

必需

返回:

类型 描述
H2OFrame

转换后的H2OFrame

示例:

>>> h2o_apply_regex(hf, 'name', lambda x: x.upper())
源代码位于: Modeling_Tool/Core/utils.py
def h2o_apply_regex(data, colname, func):
    """
    对H2O Frame的列应用正则表达式转换函数。

    Parameters
    ----------
    data : h2o.H2OFrame
        输入数据
    colname : str
        列名
    func : callable
        应用函数

    Returns
    -------
    h2o.H2OFrame
        转换后的H2OFrame

    Examples
    --------
    >>> h2o_apply_regex(hf, 'name', lambda x: x.upper())
    """
    """ Apply lambda function to h2o frame. """
    if isinstance(colname, str):
        fnl_res = h2o.H2OFrame(data[colname].as_data_frame()[colname].apply(func).tolist())
        fnl_res = fnl_res.rename({'C1':colname})
    return fnl_res

get_summary_rpt

get_summary_rpt(means_rpt, iv_psi_rpt, corr_rpt)

合并生成特征汇总报告。

将Means报告、IV/PSI报告和相关性报告合并为一个综合报告。

参数:

名称 类型 描述 默认
means_rpt DataFrame

Means统计报告

必需
iv_psi_rpt DataFrame

IV/PSI报告

必需
corr_rpt DataFrame

相关性报告

必需

返回:

类型 描述
DataFrame

合并后的汇总报告

示例:

>>> summary = get_summary_rpt(means, iv_psi, corr)
源代码位于: Modeling_Tool/Core/utils.py
def get_summary_rpt(means_rpt, iv_psi_rpt, corr_rpt):
    """
    合并生成特征汇总报告。

    将Means报告、IV/PSI报告和相关性报告合并为一个综合报告。

    Parameters
    ----------
    means_rpt : pandas.DataFrame
        Means统计报告
    iv_psi_rpt : pandas.DataFrame
        IV/PSI报告
    corr_rpt : pandas.DataFrame
        相关性报告

    Returns
    -------
    pandas.DataFrame
        合并后的汇总报告

    Examples
    --------
    >>> summary = get_summary_rpt(means, iv_psi, corr)
    """
    iv_psi_rpt = iv_psi_rpt.set_index("Var_Name")
    corr_rpt = corr_rpt.set_index("Var_Name")

    fnl_rpt = means_rpt\
    .merge(iv_psi_rpt, left_index = True, right_index = True)\
    .merge(corr_rpt, left_index = True, right_index = True)

    fnl_rpt.columns = [x.upper() for x in fnl_rpt.columns]
    return fnl_rpt

flatten_json_attr

flatten_json_attr(data, jsonColname='data')

展开JSON格式的模型属性列。

参数:

名称 类型 描述 默认
data DataFrame

包含JSON列的数据表

必需
jsonColname str

JSON列名

"data"

返回:

类型 描述
DataFrame

展开后的数据表

示例:

>>> df = pd.DataFrame({'id': [1], 'data': ['{"key1":"val1"}']})
>>> flatten_json_attr(df)
源代码位于: Modeling_Tool/Core/utils.py
def flatten_json_attr(data, jsonColname= "data"):
    """
    展开JSON格式的模型属性列。

    Parameters
    ----------
    data : pandas.DataFrame
        包含JSON列的数据表
    jsonColname : str, default "data"
        JSON列名

    Returns
    -------
    pandas.DataFrame
        展开后的数据表

    Examples
    --------
    >>> df = pd.DataFrame({'id': [1], 'data': ['{"key1":"val1"}']})
    >>> flatten_json_attr(df)
    """
    """ Flatten Json-format model attributes. """
    import ast
    json_data = data[jsonColname].tolist()
    json_data = [ast.literal_eval(x) for x in json_data]
    info_list = [x for x in data.columns if x != 'data']
    drv = data[info_list]
    drv_w_attr = pd.concat([drv, pd.json_normalize(data=json_data)], axis = 1)
    logging.info(f"Flattened Data Shape: {drv_w_attr.shape}")
    return drv_w_attr

parse_odps_schema

parse_odps_schema(schema_list)

解析ODPS Schema。

参数:

名称 类型 描述 默认
schema_list list

ODPS Schema列表

必需

返回:

类型 描述
dict

字段名到数据类型的字典

示例:

>>> schema = parse_odps_schema(['column col1 type=string, column col2 type=bigint'])
{'col1': 'string', 'col2': 'bigint'}
源代码位于: Modeling_Tool/Core/utils.py
def parse_odps_schema(schema_list):   
    """
    解析ODPS Schema。

    Parameters
    ----------
    schema_list : list
        ODPS Schema列表

    Returns
    -------
    dict
        字段名到数据类型的字典

    Examples
    --------
    >>> schema = parse_odps_schema(['column col1 type=string, column col2 type=bigint'])
    {'col1': 'string', 'col2': 'bigint'}
    """
    import re

    fnl_dict = {}
    for x in schema_list:
        res = re.sub(r'[<>]', '', str(x).replace(" type ", "").replace("column ", "")).replace(" ", "").split(',')
        fnl_dict[res[0]] = res[1]
    return fnl_dict

npnan2none

npnan2none(df)

将DataFrame中的np.nan和np.nat转换为None值。

参数:

名称 类型 描述 默认
df DataFrame

输入数据表

必需

返回:

类型 描述
DataFrame

转换后的数据表

示例:

>>> df = pd.DataFrame({'a': [1, np.nan], 'b': [np.nan, 2]})
>>> npnan2none(df)
源代码位于: Modeling_Tool/Core/utils.py
def npnan2none(df):
    """
    将DataFrame中的np.nan和np.nat转换为None值。

    Parameters
    ----------
    df : pandas.DataFrame
        输入数据表

    Returns
    -------
    pandas.DataFrame
        转换后的数据表

    Examples
    --------
    >>> df = pd.DataFrame({'a': [1, np.nan], 'b': [np.nan, 2]})
    >>> npnan2none(df)
    """
    """ Convert np.nan, np.nat to None value. """

    obj_colist = [k for k, v in df.dtypes.items() if v == 'O']
    for x in obj_colist:
        try:
            df[x] = df[x].astype(float).where(df[x].notnull(), None)
        except:
            df[x] = df[x].astype(object).where(df[x].notnull(), None)

    df = df.replace({np.nan: None})
    return df

drop_tmp_cols

drop_tmp_cols(df, drop_list=['py_inserttime'])

删除DataFrame中的临时列。

参数:

名称 类型 描述 默认
df DataFrame

输入数据表

必需
drop_list list

要删除的临时列列表

['py_inserttime']

返回:

类型 描述
DataFrame

删除临时列后的数据表

示例:

>>> df = pd.DataFrame({'a': [1, 2], 'py_inserttime': [0, 0]})
>>> drop_tmp_cols(df)
源代码位于: Modeling_Tool/Core/utils.py
def drop_tmp_cols(df, drop_list = ['py_inserttime']):
    """
    删除DataFrame中的临时列。

    Parameters
    ----------
    df : pandas.DataFrame
        输入数据表
    drop_list : list, default ['py_inserttime']
        要删除的临时列列表

    Returns
    -------
    pandas.DataFrame
        删除临时列后的数据表

    Examples
    --------
    >>> df = pd.DataFrame({'a': [1, 2], 'py_inserttime': [0, 0]})
    >>> drop_tmp_cols(df)
    """
    """ Drop Temporary Columns. """

    col_exist_dict = {}
    for x in drop_list:
        if x in df.columns:
            col_exist_dict[x] = 1
        else:
            col_exist_dict[x] = 0

    df = df.drop(columns = [k for k, v in col_exist_dict.items() if v == 1])

    return df

mkdir_if_not_exist

mkdir_if_not_exist(folder_path, replace=False)

如果目录不存在则创建目录。

参数:

名称 类型 描述 默认
folder_path str

文件夹路径

必需
replace bool

如果目录已存在是否替换

False

返回:

类型 描述
int

状态码:0表示成功,1表示目录已存在

示例:

>>> mkdir_if_not_exist('/path/to/new/folder')
源代码位于: Modeling_Tool/Core/utils.py
def mkdir_if_not_exist(folder_path, replace = False):
    """
    如果目录不存在则创建目录。

    Parameters
    ----------
    folder_path : str
        文件夹路径
    replace : bool, default False
        如果目录已存在是否替换

    Returns
    -------
    int
        状态码:0表示成功,1表示目录已存在

    Examples
    --------
    >>> mkdir_if_not_exist('/path/to/new/folder')
    """
    """ Make new directory if the given path does not exist. """

    if folder_path is None:
        return None

    if os.path.isdir(folder_path):

        if replace:
            logging.info(f"Folder {folder_path} has been replaced!")
            os.makedirs(folder_path, exist_ok=replace)
            return 0

#         logging.info(f"Folder {folder_path} has already existed!")
        return 1

    else:
        os.makedirs(folder_path, exist_ok=False)
        logging.info(f"Folder {folder_path} created!")

    return 0

parse_sql_file

parse_sql_file(sql_path: str = None, sql_query: str = None, split: bool = False, format_select: bool = False, **kwargs)

解析SQL文件并替换变量。

参数:

名称 类型 描述 默认
sql_path str

SQL文件路径

None
sql_query str

SQL查询字符串

None
split bool

是否分割多个查询

False
format_select bool

是否自动格式化SELECT字段(每个字段一行,前置逗号风格)

False
**kwargs

SQL中要替换的变量

{}

返回:

类型 描述
str or list

解析后的SQL字符串或字符串列表

示例:

>>> parse_sql_file(sql_path='query.sql', table_name='my_table', date='2025-01-01')
>>> parse_sql_file(sql_path='query.sql', format_select=True, table_name='my_table')
源代码位于: Modeling_Tool/Core/utils.py
def parse_sql_file(sql_path:str=None,
                   sql_query:str=None,
                   split:bool=False,
                   format_select:bool=False,
                   **kwargs):
    """
    解析SQL文件并替换变量。

    Parameters
    ----------
    sql_path : str, optional
        SQL文件路径
    sql_query : str, optional
        SQL查询字符串
    split : bool, default False
        是否分割多个查询
    format_select : bool, default False
        是否自动格式化SELECT字段(每个字段一行,前置逗号风格)
    **kwargs
        SQL中要替换的变量

    Returns
    -------
    str or list
        解析后的SQL字符串或字符串列表

    Examples
    --------
    >>> parse_sql_file(sql_path='query.sql', table_name='my_table', date='2025-01-01')
    >>> parse_sql_file(sql_path='query.sql', format_select=True, table_name='my_table')
    """
    import re
    import warnings

    if (sql_path is None) and (sql_query is None):
        raise AttributeError("please give either sql_path or sql_query.")

    if (sql_path is not None) and (sql_query is not None):
        raise AttributeError("sql_path and sql_query can not be BOTH given.")

    sql = sql_query
    if sql_path is not None:
        # Read sql file.
        with open(sql_path, 'r') as file:
            sql = file.read().strip()

    # Remove all comments in sql file.
    sql = _remove_comments(sql)

    # Find and parse all argments in sql file.
    all_args = list(set(re.findall(r"{(.*?)}", sql)))
    #### Parse Arguments
    for k, v in kwargs.items():
        if k in all_args:
            sql = sql.replace("{%s}" % k, v)
    args_left = list(set(re.findall(r"{(.*?)}", sql)))

    # Identify if mutliple queries in one .sql file
    ## if Yes: identify if split sql file.
    query_list = _split_sql_queries(sql)
    query_list = [query.strip() for query in query_list if query != '']

    # Optionally format SELECT clause for readability
    if format_select:
        query_list = [_format_sql_select(q) for q in query_list]

    if len(args_left) != 0:
        # Raise a warning if not all arguments are given through the function.
        warnings.warn(f"Missing argument(s) {', '.join(args_left)} in the given SQL file.")

    # Ensure all the arguments have been claimed.
    if split:
        return query_list if len(query_list) > 1 else query_list[0]
    else:
        return '; '.join(query_list)+";"

calc_woe

calc_woe(data, bad_pct, good_pct, fillwoe=True)

计算WOE(Weight of Evidence)值。

WOE = ln(组正样本占比 / 组负样本占比)

参数:

名称 类型 描述 默认
data DataFrame

包含比例的数据表

必需
bad_pct str

坏样本占比列名

必需
good_pct str

好样本占比列名

必需
fillwoe bool

当比例为0时是否将woe置为0

True

返回:

类型 描述
float or Series

WOE值

示例:

>>> df = pd.DataFrame({'bad_pct': [0.3, 0.5], 'good_pct': [0.7, 0.5]})
>>> calc_woe(df, 'bad_pct', 'good_pct')
源代码位于: Modeling_Tool/Core/utils.py
def calc_woe(data, bad_pct, good_pct, fillwoe=True):
    """
    计算WOE(Weight of Evidence)值。

    WOE = ln(组正样本占比 / 组负样本占比)

    Parameters
    ----------
    data : pandas.DataFrame
        包含比例的数据表
    bad_pct : str
        坏样本占比列名
    good_pct : str
        好样本占比列名
    fillwoe : bool, default True
        当比例为0时是否将woe置为0

    Returns
    -------
    float or pandas.Series
        WOE值

    Examples
    --------
    >>> df = pd.DataFrame({'bad_pct': [0.3, 0.5], 'good_pct': [0.7, 0.5]})
    >>> calc_woe(df, 'bad_pct', 'good_pct')
    """

    if len(data[bad_pct]) > 0 and len(data[good_pct]) > 0:
        woe = np.log(data[bad_pct] / data[good_pct])
    else:
        if fillwoe:
            woe = 0
        else:
            woe = np.nan

    return woe

calc_iv

calc_iv(data, bad_pct, good_pct, filliv=True)

计算IV(Information Value)值。

IV = (组正样本占比 - 组负样本占比) * WOE

参数:

名称 类型 描述 默认
data DataFrame

包含比例的数据表

必需
bad_pct str

坏样本占比列名

必需
good_pct str

好样本占比列名

必需
filliv bool

当比例为0时是否将iv置为0

True

返回:

类型 描述
float or Series

IV值

示例:

>>> df = pd.DataFrame({'bad_pct': [0.3, 0.5], 'good_pct': [0.7, 0.5]})
>>> calc_iv(df, 'bad_pct', 'good_pct')
源代码位于: Modeling_Tool/Core/utils.py
def calc_iv(data, bad_pct, good_pct, filliv=True):
    """
    计算IV(Information Value)值。

    IV = (组正样本占比 - 组负样本占比) * WOE

    Parameters
    ----------
    data : pandas.DataFrame
        包含比例的数据表
    bad_pct : str
        坏样本占比列名
    good_pct : str
        好样本占比列名
    filliv : bool, default True
        当比例为0时是否将iv置为0

    Returns
    -------
    float or pandas.Series
        IV值

    Examples
    --------
    >>> df = pd.DataFrame({'bad_pct': [0.3, 0.5], 'good_pct': [0.7, 0.5]})
    >>> calc_iv(df, 'bad_pct', 'good_pct')
    """

    if len(data[bad_pct]) > 0 and len(data[good_pct]) > 0:
        iv = (data[bad_pct] - data[good_pct]) * np.log(data[bad_pct] / data[good_pct])
    else:
        if filliv:
            iv = 0
        else:
            iv = np.nan

    return iv

save_model

save_model(model, filename)

使用pickle保存lightGBM模型。

参数:

名称 类型 描述 默认
model object

要保存的模型对象

必需
filename str

保存路径

必需

返回:

类型 描述
int

执行状态码(0表示成功)

示例:

>>> save_model(model, 'model.pkl')
源代码位于: Modeling_Tool/Core/utils.py
def save_model(model, filename):
    """
    使用pickle保存lightGBM模型。

    Parameters
    ----------
    model : object
        要保存的模型对象
    filename : str
        保存路径

    Returns
    -------
    int
        执行状态码(0表示成功)

    Examples
    --------
    >>> save_model(model, 'model.pkl')
    """
    """ Save lightGBM model using pickle. """
    import joblib

    joblib.dump(model, filename)
    return 0

load_model

load_model(model_path)

加载pickle模型。

参数:

名称 类型 描述 默认
model_path str

模型文件路径

必需

返回:

类型 描述
object

加载的模型对象

示例:

>>> model = load_model('model.pkl')
源代码位于: Modeling_Tool/Core/utils.py
def load_model(model_path):
    """
    加载pickle模型。

    Parameters
    ----------
    model_path : str
        模型文件路径

    Returns
    -------
    object
        加载的模型对象

    Examples
    --------
    >>> model = load_model('model.pkl')
    """
    """ Load Pickle Model. """
    import joblib

    model = joblib.load(model_path)
    return model

scoring

scoring(data, model, varlist, scr_name, keeplist=None, all_missing_spec_value=None)

使用模型对数据进行评分。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
model sklearn-like model

机器学习模型

必需
varlist list

特征变量列表

必需
scr_name str

分数列名

必需
keeplist list

要保留的列列表

None
all_missing_spec_value float

全缺失样本的指定分数值

None

返回:

类型 描述
DataFrame

包含分数的数据表

示例:

>>> df = scoring(data, model, ['feat1', 'feat2'], 'score')
源代码位于: Modeling_Tool/Core/utils.py
def scoring(data, model, varlist, scr_name, keeplist = None, all_missing_spec_value = None):
    """
    使用模型对数据进行评分。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    model : sklearn-like model
        机器学习模型
    varlist : list
        特征变量列表
    scr_name : str
        分数列名
    keeplist : list, optional
        要保留的列列表
    all_missing_spec_value : float, optional
        全缺失样本的指定分数值

    Returns
    -------
    pandas.DataFrame
        包含分数的数据表

    Examples
    --------
    >>> df = scoring(data, model, ['feat1', 'feat2'], 'score')
    """
    """ Model Soring. """

    fnl_data = data.copy()
    fnl_data[scr_name] = model.predict_proba(fnl_data.loc[:, varlist])[:, 1]

    nohit_condition = (pd.isnull(fnl_data[varlist]).sum(axis = 1) == len(varlist))
    if fnl_data[nohit_condition].shape[0] > 0:

        all_missing_data = fnl_data[nohit_condition]
        other_data = fnl_data[~nohit_condition]


        all_missing_data[scr_name] = model.predict_proba(fnl_data[nohit_condition].loc[:, varlist])[:, 1]
        logger.info("Score for All-Missing Cases: ", all_missing_data[scr_name].unique())

        if all_missing_spec_value:
            all_missing_data[scr_name] = all_missing_spec_value
            logger.info(f"Score for All-Missing Cases Has Been Reset to {all_missing_spec_value}")

        fnl_data = pd.concat([other_data, all_missing_data])

    assert fnl_data.shape[0] == data.shape[0]

    if keeplist is None:
        keeplist = fnl_data.columns.tolist()
    else:
        keeplist = keeplist + [scr_name]

    return fnl_data[keeplist]

get_missing_indicator

get_missing_indicator(data, subset=None)

Add Missing Indicator.

源代码位于: Modeling_Tool/Core/utils.py
def get_missing_indicator(data, subset = None):
    """ Add Missing Indicator. """

    all_missing_logic = lambda data: (pd.isnull(data[subset]).sum(axis = 1) == len(subset))
    return all_missing_logic(data).astype(int)

upload_score

upload_score(data, model, varlist, scr_name, table_name, keeplist=None, retPandas=False, all_missing_spec_value=None)

将模型分数上传到Maxcompute。

参数:

名称 类型 描述 默认
data DataFrame

输入数据表

必需
model sklearn-like model

机器学习模型

必需
varlist list

特征变量列表

必需
scr_name str

分数列名

必需
table_name str

目标表名

必需
keeplist list

要保留的列列表

None
retPandas bool

是否返回pandas DataFrame

False
all_missing_spec_value float

全缺失样本的指定分数值

None

返回:

类型 描述
int or DataFrame

状态码或数据表

示例:

>>> upload_score(data, model, ['feat1', 'feat2'], 'score', 'output_table')
源代码位于: Modeling_Tool/Core/utils.py
def upload_score(data, model, varlist, scr_name, table_name, keeplist = None, retPandas = False, all_missing_spec_value = None):
    """
    将模型分数上传到Maxcompute。

    Parameters
    ----------
    data : pandas.DataFrame
        输入数据表
    model : sklearn-like model
        机器学习模型
    varlist : list
        特征变量列表
    scr_name : str
        分数列名
    table_name : str
        目标表名
    keeplist : list, optional
        要保留的列列表
    retPandas : bool, default False
        是否返回pandas DataFrame
    all_missing_spec_value : float, optional
        全缺失样本的指定分数值

    Returns
    -------
    int or pandas.DataFrame
        状态码或数据表

    Examples
    --------
    >>> upload_score(data, model, ['feat1', 'feat2'], 'score', 'output_table')
    """
    """ Upload Score to Maxcompute. """

    data = scoring(data = data, model = model, varlist = varlist, scr_name = scr_name, keeplist = keeplist, all_missing_spec_value = all_missing_spec_value)


    sqlrunner = ODPSRunner()

    fnl_scr_upload = data.copy()
    fnl_scr_upload = uf.npnan2none(fnl_scr_upload)
    fnl_scr_upload = uf.drop_tmp_cols(fnl_scr_upload)

    if keeplist is None:
        keeplist = fnl_scr_upload.columns.tolist()
    else:
        keeplist = keeplist + [scr_name]

    sqlrunner.upload_df(fnl_scr_upload[keeplist], table_name)

    if retPandas:
        return fnl_scr_upload[keeplist]

    return 0

pull_attributes_in_batch

pull_attributes_in_batch(table_name, varlist, batch_num=6, unikey='flow_id', main_info_select=['*'], add_query='')

Pull Data from DataWorks in Vertical Batch.

源代码位于: Modeling_Tool/Core/utils.py
def pull_attributes_in_batch(table_name, varlist, batch_num = 6, unikey = 'flow_id', main_info_select = ['*'], add_query = ''):
    """ Pull Data from DataWorks in Vertical Batch. """

    from .ODPS_Tool import ODPSRunner
    import multiprocessing

    n_process = multiprocessing.cpu_count() - 1
    logger.info(n_process)
    sqlrunner = ODPSRunner()

    n = 6

    batch_varlist = cut2pieces(varlist, n)    

    assert (len(varlist) == len([x for varlist in batch_varlist for x in varlist]))

    res = {}
    i = 0
    while i < len(batch_varlist):

        logger.info(i)
        sql_query = f""" 
            SELECT {unikey}, {", ".join(batch_varlist[i])} 
            FROM {table_name}
            {add_query};
        """

        res[f'batch{i}'] = sqlrunner.run_sql(sql_query, n_process = n_process)

        i += 1

    sql_query = f""" 
        SELECT {','.join(main_info_select)} EXCEPT ({", ".join(varlist)}) 
        FROM {table_name}
        {add_query};
    """

    main_info = sqlrunner.run_sql(sql_query, n_process = n_process)
    master_df = main_info.copy()

    for k, data in res.items():    
        master_df = master_df.merge(data, on = [unikey])

    return master_df

get_feature_names

get_feature_names(model, model_type=None)

获取模型的特征名称列表。

自动检测模型类型并返回其特征名称。 支持LightGBM、XGBoost、sklearn等多种模型。

参数:

名称 类型 描述 默认
model object

训练好的机器学习模型对象

必需
model_type str

模型类型提示,可选值: - 'lgb' 或 'lightgbm': LightGBM模型 - 'xgb' 或 'xgboost': XGBoost模型 - 'sklearn': sklearn模型 - None: 自动检测(默认)

None

返回:

类型 描述
list

特征名称列表

引发:

类型 描述
ValueError

当无法获取特征名称时抛出

示例:

>>> # 通用方式
>>> feature_names = get_feature_names(model)
>>> # 指定类型
>>> feature_names = get_feature_names(lgb_model, model_type='lgb')
>>> # 处理XGBoost
>>> feature_names = get_feature_names(xgb_model, model_type='xgb')
源代码位于: Modeling_Tool/Core/utils.py
def get_feature_names(model, model_type=None):
    """获取模型的特征名称列表。

    自动检测模型类型并返回其特征名称。
    支持LightGBM、XGBoost、sklearn等多种模型。

    Parameters
    ----------
    model : object
        训练好的机器学习模型对象
    model_type : str, optional
        模型类型提示,可选值:
        - 'lgb' 或 'lightgbm': LightGBM模型
        - 'xgb' 或 'xgboost': XGBoost模型
        - 'sklearn': sklearn模型
        - None: 自动检测(默认)

    Returns
    -------
    list
        特征名称列表

    Raises
    ------
    ValueError
        当无法获取特征名称时抛出

    Examples
    --------
    >>> # 通用方式
    >>> feature_names = get_feature_names(model)

    >>> # 指定类型
    >>> feature_names = get_feature_names(lgb_model, model_type='lgb')

    >>> # 处理XGBoost
    >>> feature_names = get_feature_names(xgb_model, model_type='xgb')
    """
    # 如果指定了模型类型,优先使用专用函数
    if model_type is not None:
        model_type_lower = model_type.lower()
        if model_type_lower in ['lgb', 'lightgbm']:
            return get_feature_names_lgb(model)
        elif model_type_lower in ['xgb', 'xgboost']:
            return get_feature_names_xgb(model)

    # 自动检测模型类型并获取特征名
    model_class_name = model.__class__.__name__.lower()

    # SMF GradientBoostingModel wraps the fitted estimator in _model.model and
    # stores DataFrame column names on _model.feature_names_ after fit.
    wrapped_model_type = getattr(model, 'model_type', None)
    wrapped_backend = getattr(model, '_model', None)
    if wrapped_model_type is not None and wrapped_backend is not None:
        wrapped_feature_names = getattr(wrapped_backend, 'feature_names_', None)
        if wrapped_feature_names is not None:
            return list(wrapped_feature_names)

        wrapped_estimator = getattr(wrapped_backend, 'model', None)
        if wrapped_estimator is not None:
            wrapped_model_type_lower = str(wrapped_model_type).lower()
            if wrapped_model_type_lower in ['lgb', 'lightgbm']:
                return get_feature_names_lgb(wrapped_estimator)
            if wrapped_model_type_lower in ['xgb', 'xgboost']:
                return get_feature_names_xgb(wrapped_estimator)
            return get_feature_names(wrapped_estimator)

    # LightGBM 检测
    if 'lgb' in model_class_name or 'lightgbm' in model_class_name:
        return get_feature_names_lgb(model)

    # XGBoost 检测
    if 'xgb' in model_class_name or 'xgboost' in model_class_name:
        return get_feature_names_xgb(model)

    if 'logisticregression' in model_class_name:
        return list(model.feature_names_in_)

    # 尝试通用sklearn方式
    # 方法1: feature_names_in 属性 (sklearn >= 1.0)
    if hasattr(model, 'feature_names_in_'):
        return list(model.feature_names_in_)

    # 方法2: feature_names 属性
    if hasattr(model, 'feature_names'):
        feature_names = model.feature_names
        if callable(feature_names):
            return list(feature_names())
        return list(feature_names)

    # 方法3: booster方式 (LightGBM特有)
    if hasattr(model, 'booster_'):
        try:
            return model.booster_.feature_name()
        except (AttributeError, TypeError):
            pass

    # 方法4: 尝试从模型参数中获取
    if hasattr(model, 'feature_name'):
        try:
            feature_names = model.feature_name
            if callable(feature_names):
                return list(feature_names())
            return list(feature_names)
        except (AttributeError, TypeError):
            pass

    # 无法获取特征名
    raise ValueError(
        f"无法获取模型 '{model_class_name}' 的特征名称。\n"
        f"请尝试:\n"
        f"1. 显式指定 model_type 参数\n"
        f"2. 使用专用函数:get_feature_names_lgb() 或 get_feature_names_xgb()"
    )

get_feature_names_lgb

get_feature_names_lgb(model)

获取LightGBM模型的特征名称。

参数:

名称 类型 描述 默认
model LGBMClassifier or LGBMRegressor

训练好的LightGBM模型

必需

返回:

类型 描述
list

特征名称列表

引发:

类型 描述
ValueError

当无法获取特征名称时抛出

示例:

>>> import lightgbm as lgb
>>> model = lgb.LGBMClassifier().fit(X_train, y_train)
>>> feature_names = get_feature_names_lgb(model)
>>> print(feature_names)
['feature_1', 'feature_2', 'feature_3']
源代码位于: Modeling_Tool/Core/utils.py
def get_feature_names_lgb(model):
    """获取LightGBM模型的特征名称。

    Parameters
    ----------
    model : lgb.LGBMClassifier or lgb.LGBMRegressor
        训练好的LightGBM模型

    Returns
    -------
    list
        特征名称列表

    Raises
    ------
    ValueError
        当无法获取特征名称时抛出

    Examples
    --------
    >>> import lightgbm as lgb
    >>> model = lgb.LGBMClassifier().fit(X_train, y_train)
    >>> feature_names = get_feature_names_lgb(model)
    >>> print(feature_names)
    ['feature_1', 'feature_2', 'feature_3']
    """
    # 方法1: booster_.feature_name() (最可靠)
    if hasattr(model, 'booster_') and model.booster_ is not None:
        try:
            return model.booster_.feature_name()
        except (AttributeError, TypeError):
            pass

    # 方法2: feature_name_ 属性
    if hasattr(model, 'feature_name_'):
        return list(model.feature_name_)

    # 方法3: feature_name 属性/方法
    if hasattr(model, 'feature_name'):
        feature_names = model.feature_name
        if callable(feature_names):
            return list(feature_names())
        return list(feature_names)

    raise ValueError(
        "无法获取LightGBM模型的特征名称。\n"
        "确保模型已正确训练。"
    )

get_feature_names_xgb

get_feature_names_xgb(model)

获取XGBoost模型的特征名称。

参数:

名称 类型 描述 默认
model XGBClassifier or XGBRegressor

训练好的XGBoost模型

必需

返回:

类型 描述
list

特征名称列表

引发:

类型 描述
ValueError

当无法获取特征名称时抛出

示例:

>>> import xgboost as xgb
>>> model = xgb.XGBClassifier().fit(X_train, y_train)
>>> feature_names = get_feature_names_xgb(model)
>>> print(feature_names)
['feature_1', 'feature_2', 'feature_3']
源代码位于: Modeling_Tool/Core/utils.py
def get_feature_names_xgb(model):
    """获取XGBoost模型的特征名称。

    Parameters
    ----------
    model : xgb.XGBClassifier or xgb.XGBRegressor
        训练好的XGBoost模型

    Returns
    -------
    list
        特征名称列表

    Raises
    ------
    ValueError
        当无法获取特征名称时抛出

    Examples
    --------
    >>> import xgboost as xgb
    >>> model = xgb.XGBClassifier().fit(X_train, y_train)
    >>> feature_names = get_feature_names_xgb(model)
    >>> print(feature_names)
    ['feature_1', 'feature_2', 'feature_3']
    """
    # 方法1: feature_names_in 属性 (sklearn风格)
    if hasattr(model, 'feature_names_in_'):
        return list(model.feature_names_in_)

    # 方法2: booster.get_feature_names() (原生XGBoost)
    if hasattr(model, 'get_booster'):
        try:
            booster = model.get_booster()
            feature_names = booster.get_feature_names()
            return list(feature_names) if feature_names else []
        except (AttributeError, TypeError):
            pass

    # 方法3: feature_names 属性
    if hasattr(model, 'feature_names'):
        feature_names = model.feature_names
        if callable(feature_names):
            return list(feature_names())
        return list(feature_names)

    raise ValueError(
        "无法获取XGBoost模型的特征名称。\n"
        "确保模型已正确训练。"
    )

get_feature_names_batch

get_feature_names_batch(models, model_type=None)

批量获取多个模型的特征名称。

参数:

名称 类型 描述 默认
models dict or list

模型字典 {name: model} 或模型列表

必需
model_type str

模型类型提示

None

返回:

类型 描述
dict or list

特征名称字典或列表,与输入结构对应

示例:

>>> models = {'lgb': lgb_model, 'xgb': xgb_model}
>>> feature_names_dict = get_feature_names_batch(models)
>>> print(feature_names_dict)
{'lgb': ['f1', 'f2'], 'xgb': ['f1', 'f2']}
源代码位于: Modeling_Tool/Core/utils.py
def get_feature_names_batch(models, model_type=None):
    """批量获取多个模型的特征名称。

    Parameters
    ----------
    models : dict or list
        模型字典 {name: model} 或模型列表
    model_type : str, optional
        模型类型提示

    Returns
    -------
    dict or list
        特征名称字典或列表,与输入结构对应

    Examples
    --------
    >>> models = {'lgb': lgb_model, 'xgb': xgb_model}
    >>> feature_names_dict = get_feature_names_batch(models)
    >>> print(feature_names_dict)
    {'lgb': ['f1', 'f2'], 'xgb': ['f1', 'f2']}
    """
    if isinstance(models, dict):
        return {
            name: get_feature_names(model, model_type=model_type)
            for name, model in models.items()
        }
    elif isinstance(models, list):
        return [get_feature_names(model, model_type=model_type) for model in models]
    else:
        raise TypeError("models参数应为dict或list类型")

加密 — XOR_Encryptor

XOR_Encryptor

TextEncryptor

基于XOR算法的文本加密解密工具类。

该类提供文本加密和解密功能,支持单个字符串以及整个Pandas DataFrame的加解密操作。 加密后的数据使用Base64 URL安全编码,便于存储和传输。

Attributes: key (str): 加密解密使用的密钥。如果为None,则使用空字符串作为密钥。 suffix (str): DataFrame列名加密后的后缀,默认为'_encrypted'。

Example: >>> encryptor = TextEncryptor(key="my_secret_key") >>> encrypted = encryptor.encrypt("Hello World") >>> decrypted = encryptor.decrypt(encrypted) >>> print(decrypted) # 输出: Hello World

源代码位于: Modeling_Tool/Core/XOR_Encryptor.py
class TextEncryptor:
    """
    基于XOR算法的文本加密解密工具类。

    该类提供文本加密和解密功能,支持单个字符串以及整个Pandas DataFrame的加解密操作。
    加密后的数据使用Base64 URL安全编码,便于存储和传输。

    Attributes:
        key (str): 加密解密使用的密钥。如果为None,则使用空字符串作为密钥。
        suffix (str): DataFrame列名加密后的后缀,默认为'_encrypted'。

    Example:
        >>> encryptor = TextEncryptor(key="my_secret_key")
        >>> encrypted = encryptor.encrypt("Hello World")
        >>> decrypted = encryptor.decrypt(encrypted)
        >>> print(decrypted)  # 输出: Hello World
    """

    def __init__(self, key=None, suffix='_encrypted'):
        """
        初始化加密器实例。

        Parameters:
            key (str, optional): 加密解密使用的密钥。如果为None,则使用空字符串作为密钥。
                               注意:使用空密钥加密后的数据将不具有保密性。
            suffix (str, optional): 当对DataFrame进行加密时,列名添加的后缀。
                                  默认为'_encrypted'。解密时会移除此后缀。
        """
        self.key = key
        self.suffix = suffix

    def encrypt(self, text):
        """
        对输入的文本进行加密。

        使用XOR算法将明文与密钥进行异或操作,然后通过Base64 URL安全编码输出。
        加密结果包含原始文本长度信息(前2个字节),用于解密时的验证。

        Parameters:
            text (str): 需要加密的明文字符串。

        Returns:
            str: 加密后的字符串,使用Base64 URL安全编码。

        Raises:
            AttributeError: 如果key属性为None(self.key为None时,实际使用空字符串)。

        Example:
            >>> encryptor = TextEncryptor(key="secret")
            >>> encrypted = encryptor.encrypt("Hello")
            >>> print(encrypted)  # 输出类似: aAAAAS垂涎==
        """
        # Text to bytes
        text_bytes = text.encode('utf-8')

        # Expand byte length
        key_bytes = (self.key * (len(text_bytes) // len(self.key) + 1)).encode('utf-8')
        key_bytes = key_bytes[:len(text_bytes)]

        # XOR encryption
        encrypted_bytes = bytes([text_bytes[i] ^ key_bytes[i] for i in range(len(text_bytes))])

        # Add 2 more bytes for verification
        length_byte = len(text).to_bytes(2, 'big')

        # combine
        result_bytes = length_byte + encrypted_bytes
        return base64.urlsafe_b64encode(result_bytes).decode('utf-8')

    def decrypt(self, encrypted_text):
        """
        对加密后的文本进行解密。

        首先使用Base64解码,然后提取长度信息(前2字节),接着使用XOR算法与密钥进行异或操作恢复明文。
        解密后会验证恢复文本的长度是否与存储的长度信息匹配,以确保数据完整性。

        Parameters:
            encrypted_text (str): 经过encrypt方法加密的Base64编码字符串。

        Returns:
            str: 解密后的原始明文字符串。

        Raises:
            ValueError: 如果解密失败,可能原因包括:
                       - Base64解码失败(输入不是有效的Base64字符串)
                       - 长度验证失败(数据被篡改或使用了不同的密钥)
                       - 其他解码错误

        Example:
            >>> encryptor = TextEncryptor(key="secret")
            >>> encrypted = encryptor.encrypt("Hello")
            >>> decrypted = encryptor.decrypt(encrypted)
            >>> print(decrypted)  # 输出: Hello
        """
        try:
            # b64 decryption
            decoded_bytes = base64.urlsafe_b64decode(encrypted_text.encode('utf-8'))

            # extract length info
            text_length = int.from_bytes(decoded_bytes[:2], 'big')

            # extract encryption info
            encrypted_bytes = decoded_bytes[2:]

            # regenerate key
            key_bytes = (self.key * (len(encrypted_bytes) // len(self.key) + 1)).encode('utf-8')
            key_bytes = key_bytes[:len(encrypted_bytes)]

            # XOR Decrypt
            decrypted_bytes = bytes([encrypted_bytes[i] ^ key_bytes[i] for i in range(len(encrypted_bytes))])

            # Check Length
            if len(decrypted_bytes) != text_length:
                raise ValueError("Text Length Does Not Match!")

            return decrypted_bytes.decode('utf-8')
        except:
            raise ValueError("Decrypt Failed! Data Might be Destroyed or Incorrect Key!")

    def encrypt_dataframe(self, data):
        """
        对整个Pandas DataFrame进行加密。

        将DataFrame中的所有列值转换为字符串格式后进行加密,同时为列名添加指定的后缀。
        该方法返回一个全新的DataFrame,原始数据不会被修改。

        Parameters:
            data (pandas.DataFrame): 需要加密的Pandas DataFrame对象。
                                   所有列的值都会被转换为字符串格式进行加密。

        Returns:
            pandas.DataFrame: 加密后的新DataFrame,具有以下特点:
                             - 所有列值都经过加密,使用Base64编码
                             - 所有列名都添加了初始化时指定的后缀(默认为'_encrypted')
                             - 返回的是副本,原始DataFrame保持不变

        Raises:
            AttributeError: 如果key属性为None导致加密失败。

        Note:
            - 加密后的DataFrame无法直接用于数据分析,必须先解密
            - 建议在加密前备份原始DataFrame的列名对应关系

        Example:
            >>> import pandas as pd
            >>> df = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [25, 30]})
            >>> encryptor = TextEncryptor(key="secret")
            >>> encrypted_df = encryptor.encrypt_dataframe(df)
            >>> print(encrypted_df.columns.tolist())  # 输出: ['name_encrypted', 'age_encrypted']
        """
        res = data.copy()
        collist = data.columns.tolist()
        for col in collist:
            ## Encryption
            res[col] = res[col].astype(str)
            res[col] = res[col].apply(lambda x: self.encrypt(x))
        res.columns = [x + self.suffix for x in res.columns]
        return res

    def decrypt_dataframe(self, data):
        """
        对加密后的Pandas DataFrame进行解密。

        遍历DataFrame中的所有列,对每个列值进行解密,同时移除列名中的加密后缀。
        该方法返回一个全新的DataFrame,原始数据不会被修改。

        Parameters:
            data (pandas.DataFrame): 需要解密的Pandas DataFrame对象。
                                   应该是由encrypt_dataframe方法加密产生的DataFrame。

        Returns:
            pandas.DataFrame: 解密后的新DataFrame,具有以下特点:
                             - 所有列值都经过解密,恢复为原始字符串格式
                             - 所有列名都移除了初始化时指定的后缀(默认为'_encrypted')
                             - 返回的是副本,原始DataFrame保持不变

        Raises:
            ValueError: 如果解密失败,可能原因包括:
                       - 列值不是有效的加密字符串
                       - 使用了错误的密钥进行解密
                       - 数据在传输或存储过程中被损坏
            UnicodeDecodeError: 如果解密后的字节无法正确解码为UTF-8字符串。

        Note:
            - 加密和解密必须使用相同的密钥
            - 如果DataFrame包含非加密的列,解密操作可能会失败

        Example:
            >>> import pandas as pd
            >>> df = pd.DataFrame({'name_encrypted': ['aGVsbG8=', 'd29ybGQ='],
            ...                    'age_encrypted': ['c2F2ZWQ=', 'dGVzdA==']})
            >>> encryptor = TextEncryptor(key="secret")
            >>> decrypted_df = encryptor.decrypt_dataframe(df)
            >>> print(decrypted_df.columns.tolist())  # 输出: ['name', 'age']
        """
        res = data.copy()
        collist = data.columns.tolist()
        for col in collist:
            ## Encryption
            res[col] = res[col].apply(lambda x: self.decrypt(x))
        res.columns = [x.replace(self.suffix, "") for x in res.columns]
        return res

encrypt

encrypt(text)

对输入的文本进行加密。

使用XOR算法将明文与密钥进行异或操作,然后通过Base64 URL安全编码输出。 加密结果包含原始文本长度信息(前2个字节),用于解密时的验证。

Parameters: text (str): 需要加密的明文字符串。

Returns: str: 加密后的字符串,使用Base64 URL安全编码。

Raises: AttributeError: 如果key属性为None(self.key为None时,实际使用空字符串)。

Example: >>> encryptor = TextEncryptor(key="secret") >>> encrypted = encryptor.encrypt("Hello") >>> print(encrypted) # 输出类似: aAAAAS垂涎==

源代码位于: Modeling_Tool/Core/XOR_Encryptor.py
def encrypt(self, text):
    """
    对输入的文本进行加密。

    使用XOR算法将明文与密钥进行异或操作,然后通过Base64 URL安全编码输出。
    加密结果包含原始文本长度信息(前2个字节),用于解密时的验证。

    Parameters:
        text (str): 需要加密的明文字符串。

    Returns:
        str: 加密后的字符串,使用Base64 URL安全编码。

    Raises:
        AttributeError: 如果key属性为None(self.key为None时,实际使用空字符串)。

    Example:
        >>> encryptor = TextEncryptor(key="secret")
        >>> encrypted = encryptor.encrypt("Hello")
        >>> print(encrypted)  # 输出类似: aAAAAS垂涎==
    """
    # Text to bytes
    text_bytes = text.encode('utf-8')

    # Expand byte length
    key_bytes = (self.key * (len(text_bytes) // len(self.key) + 1)).encode('utf-8')
    key_bytes = key_bytes[:len(text_bytes)]

    # XOR encryption
    encrypted_bytes = bytes([text_bytes[i] ^ key_bytes[i] for i in range(len(text_bytes))])

    # Add 2 more bytes for verification
    length_byte = len(text).to_bytes(2, 'big')

    # combine
    result_bytes = length_byte + encrypted_bytes
    return base64.urlsafe_b64encode(result_bytes).decode('utf-8')

decrypt

decrypt(encrypted_text)

对加密后的文本进行解密。

首先使用Base64解码,然后提取长度信息(前2字节),接着使用XOR算法与密钥进行异或操作恢复明文。 解密后会验证恢复文本的长度是否与存储的长度信息匹配,以确保数据完整性。

Parameters: encrypted_text (str): 经过encrypt方法加密的Base64编码字符串。

Returns: str: 解密后的原始明文字符串。

Raises: ValueError: 如果解密失败,可能原因包括: - Base64解码失败(输入不是有效的Base64字符串) - 长度验证失败(数据被篡改或使用了不同的密钥) - 其他解码错误

Example: >>> encryptor = TextEncryptor(key="secret") >>> encrypted = encryptor.encrypt("Hello") >>> decrypted = encryptor.decrypt(encrypted) >>> print(decrypted) # 输出: Hello

源代码位于: Modeling_Tool/Core/XOR_Encryptor.py
def decrypt(self, encrypted_text):
    """
    对加密后的文本进行解密。

    首先使用Base64解码,然后提取长度信息(前2字节),接着使用XOR算法与密钥进行异或操作恢复明文。
    解密后会验证恢复文本的长度是否与存储的长度信息匹配,以确保数据完整性。

    Parameters:
        encrypted_text (str): 经过encrypt方法加密的Base64编码字符串。

    Returns:
        str: 解密后的原始明文字符串。

    Raises:
        ValueError: 如果解密失败,可能原因包括:
                   - Base64解码失败(输入不是有效的Base64字符串)
                   - 长度验证失败(数据被篡改或使用了不同的密钥)
                   - 其他解码错误

    Example:
        >>> encryptor = TextEncryptor(key="secret")
        >>> encrypted = encryptor.encrypt("Hello")
        >>> decrypted = encryptor.decrypt(encrypted)
        >>> print(decrypted)  # 输出: Hello
    """
    try:
        # b64 decryption
        decoded_bytes = base64.urlsafe_b64decode(encrypted_text.encode('utf-8'))

        # extract length info
        text_length = int.from_bytes(decoded_bytes[:2], 'big')

        # extract encryption info
        encrypted_bytes = decoded_bytes[2:]

        # regenerate key
        key_bytes = (self.key * (len(encrypted_bytes) // len(self.key) + 1)).encode('utf-8')
        key_bytes = key_bytes[:len(encrypted_bytes)]

        # XOR Decrypt
        decrypted_bytes = bytes([encrypted_bytes[i] ^ key_bytes[i] for i in range(len(encrypted_bytes))])

        # Check Length
        if len(decrypted_bytes) != text_length:
            raise ValueError("Text Length Does Not Match!")

        return decrypted_bytes.decode('utf-8')
    except:
        raise ValueError("Decrypt Failed! Data Might be Destroyed or Incorrect Key!")

encrypt_dataframe

encrypt_dataframe(data)

对整个Pandas DataFrame进行加密。

将DataFrame中的所有列值转换为字符串格式后进行加密,同时为列名添加指定的后缀。 该方法返回一个全新的DataFrame,原始数据不会被修改。

Parameters: data (pandas.DataFrame): 需要加密的Pandas DataFrame对象。 所有列的值都会被转换为字符串格式进行加密。

Returns: pandas.DataFrame: 加密后的新DataFrame,具有以下特点: - 所有列值都经过加密,使用Base64编码 - 所有列名都添加了初始化时指定的后缀(默认为'_encrypted') - 返回的是副本,原始DataFrame保持不变

Raises: AttributeError: 如果key属性为None导致加密失败。

Note: - 加密后的DataFrame无法直接用于数据分析,必须先解密 - 建议在加密前备份原始DataFrame的列名对应关系

Example: >>> import pandas as pd >>> df = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [25, 30]}) >>> encryptor = TextEncryptor(key="secret") >>> encrypted_df = encryptor.encrypt_dataframe(df) >>> print(encrypted_df.columns.tolist()) # 输出: ['name_encrypted', 'age_encrypted']

源代码位于: Modeling_Tool/Core/XOR_Encryptor.py
def encrypt_dataframe(self, data):
    """
    对整个Pandas DataFrame进行加密。

    将DataFrame中的所有列值转换为字符串格式后进行加密,同时为列名添加指定的后缀。
    该方法返回一个全新的DataFrame,原始数据不会被修改。

    Parameters:
        data (pandas.DataFrame): 需要加密的Pandas DataFrame对象。
                               所有列的值都会被转换为字符串格式进行加密。

    Returns:
        pandas.DataFrame: 加密后的新DataFrame,具有以下特点:
                         - 所有列值都经过加密,使用Base64编码
                         - 所有列名都添加了初始化时指定的后缀(默认为'_encrypted')
                         - 返回的是副本,原始DataFrame保持不变

    Raises:
        AttributeError: 如果key属性为None导致加密失败。

    Note:
        - 加密后的DataFrame无法直接用于数据分析,必须先解密
        - 建议在加密前备份原始DataFrame的列名对应关系

    Example:
        >>> import pandas as pd
        >>> df = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [25, 30]})
        >>> encryptor = TextEncryptor(key="secret")
        >>> encrypted_df = encryptor.encrypt_dataframe(df)
        >>> print(encrypted_df.columns.tolist())  # 输出: ['name_encrypted', 'age_encrypted']
    """
    res = data.copy()
    collist = data.columns.tolist()
    for col in collist:
        ## Encryption
        res[col] = res[col].astype(str)
        res[col] = res[col].apply(lambda x: self.encrypt(x))
    res.columns = [x + self.suffix for x in res.columns]
    return res

decrypt_dataframe

decrypt_dataframe(data)

对加密后的Pandas DataFrame进行解密。

遍历DataFrame中的所有列,对每个列值进行解密,同时移除列名中的加密后缀。 该方法返回一个全新的DataFrame,原始数据不会被修改。

Parameters: data (pandas.DataFrame): 需要解密的Pandas DataFrame对象。 应该是由encrypt_dataframe方法加密产生的DataFrame。

Returns: pandas.DataFrame: 解密后的新DataFrame,具有以下特点: - 所有列值都经过解密,恢复为原始字符串格式 - 所有列名都移除了初始化时指定的后缀(默认为'_encrypted') - 返回的是副本,原始DataFrame保持不变

Raises: ValueError: 如果解密失败,可能原因包括: - 列值不是有效的加密字符串 - 使用了错误的密钥进行解密 - 数据在传输或存储过程中被损坏 UnicodeDecodeError: 如果解密后的字节无法正确解码为UTF-8字符串。

Note: - 加密和解密必须使用相同的密钥 - 如果DataFrame包含非加密的列,解密操作可能会失败

Example: >>> import pandas as pd >>> df = pd.DataFrame({'name_encrypted': ['aGVsbG8=', 'd29ybGQ='], ... 'age_encrypted': ['c2F2ZWQ=', 'dGVzdA==']}) >>> encryptor = TextEncryptor(key="secret") >>> decrypted_df = encryptor.decrypt_dataframe(df) >>> print(decrypted_df.columns.tolist()) # 输出: ['name', 'age']

源代码位于: Modeling_Tool/Core/XOR_Encryptor.py
def decrypt_dataframe(self, data):
    """
    对加密后的Pandas DataFrame进行解密。

    遍历DataFrame中的所有列,对每个列值进行解密,同时移除列名中的加密后缀。
    该方法返回一个全新的DataFrame,原始数据不会被修改。

    Parameters:
        data (pandas.DataFrame): 需要解密的Pandas DataFrame对象。
                               应该是由encrypt_dataframe方法加密产生的DataFrame。

    Returns:
        pandas.DataFrame: 解密后的新DataFrame,具有以下特点:
                         - 所有列值都经过解密,恢复为原始字符串格式
                         - 所有列名都移除了初始化时指定的后缀(默认为'_encrypted')
                         - 返回的是副本,原始DataFrame保持不变

    Raises:
        ValueError: 如果解密失败,可能原因包括:
                   - 列值不是有效的加密字符串
                   - 使用了错误的密钥进行解密
                   - 数据在传输或存储过程中被损坏
        UnicodeDecodeError: 如果解密后的字节无法正确解码为UTF-8字符串。

    Note:
        - 加密和解密必须使用相同的密钥
        - 如果DataFrame包含非加密的列,解密操作可能会失败

    Example:
        >>> import pandas as pd
        >>> df = pd.DataFrame({'name_encrypted': ['aGVsbG8=', 'd29ybGQ='],
        ...                    'age_encrypted': ['c2F2ZWQ=', 'dGVzdA==']})
        >>> encryptor = TextEncryptor(key="secret")
        >>> decrypted_df = encryptor.decrypt_dataframe(df)
        >>> print(decrypted_df.columns.tolist())  # 输出: ['name', 'age']
    """
    res = data.copy()
    collist = data.columns.tolist()
    for col in collist:
        ## Encryption
        res[col] = res[col].apply(lambda x: self.decrypt(x))
    res.columns = [x.replace(self.suffix, "") for x in res.columns]
    return res

扩展 DataFrame — kDataFrame

kDataFrame

kSeries

Bases: Series

源代码位于: Modeling_Tool/Core/kDataFrame.py
class kSeries(Series):
    @property
    def _constructor(self):
        return kSeries

    @property
    def _constructor_expanddim(self):
        return kDataFrame

    def __init__(self, data, *args, **kwargs):
        super().__init__(data, *args, **kwargs)

    def to_pdSeries(self, inplace: bool = False):
        """
        Convert kSeries back to pandas Series.
        """
        if inplace:
            data = self
        else:
            data = self.copy()

        df = Series(data)
        return df

    def odds_score(self):
        """
        Calculate the odd score based on the given probability score.

        Odds Score = (count of sth happening) / (count of sth not happening)
        Odds Score = pb_score / (1-pb_score)  [This ranges from 0 to infinity.]
        Log Odds Score = np.log(pb_score / (1-pb_score))  [This ranges from -infinity to +infinity]
        This is helpful for solving binary classification problem.
        Odds Ratio: The ratio of odds.
        """
        pb_score = self
        a = (20 / np.log(2))
        b = (np.log(15) + np.log(pb_score/(1 - pb_score)))
        return (500 - a * b)

    def scale_score(self):
        """
        Scale the model scores (for internt segment of MCI model)
        """
        data = self.copy()
        def app_func(x):
            scores = x * 1.112
            if (scores > 0.9999999):
                return 0.9999999
            return scores
        return data.apply(app_func)

    def proc_freq(self) -> pd.DataFrame:
        """
        Implement the Python version "proc freq" query in SAS.
        """
        data = self.copy()
        f = data.value_counts(dropna = False)
        p = data.value_counts(dropna = False, normalize = True)
        df = pd.concat([f,p], axis = 1, keys = ['frequency', 'percent'])
        df['cumFrequency'] = df['frequency'].cumsum()
        df['cumPercent'] = df['percent'].cumsum()
        return df

to_pdSeries

to_pdSeries(inplace: bool = False)

Convert kSeries back to pandas Series.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def to_pdSeries(self, inplace: bool = False):
    """
    Convert kSeries back to pandas Series.
    """
    if inplace:
        data = self
    else:
        data = self.copy()

    df = Series(data)
    return df

odds_score

odds_score()

Calculate the odd score based on the given probability score.

Odds Score = (count of sth happening) / (count of sth not happening) Odds Score = pb_score / (1-pb_score) [This ranges from 0 to infinity.] Log Odds Score = np.log(pb_score / (1-pb_score)) [This ranges from -infinity to +infinity] This is helpful for solving binary classification problem. Odds Ratio: The ratio of odds.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def odds_score(self):
    """
    Calculate the odd score based on the given probability score.

    Odds Score = (count of sth happening) / (count of sth not happening)
    Odds Score = pb_score / (1-pb_score)  [This ranges from 0 to infinity.]
    Log Odds Score = np.log(pb_score / (1-pb_score))  [This ranges from -infinity to +infinity]
    This is helpful for solving binary classification problem.
    Odds Ratio: The ratio of odds.
    """
    pb_score = self
    a = (20 / np.log(2))
    b = (np.log(15) + np.log(pb_score/(1 - pb_score)))
    return (500 - a * b)

scale_score

scale_score()

Scale the model scores (for internt segment of MCI model)

源代码位于: Modeling_Tool/Core/kDataFrame.py
def scale_score(self):
    """
    Scale the model scores (for internt segment of MCI model)
    """
    data = self.copy()
    def app_func(x):
        scores = x * 1.112
        if (scores > 0.9999999):
            return 0.9999999
        return scores
    return data.apply(app_func)

proc_freq

proc_freq() -> DataFrame

Implement the Python version "proc freq" query in SAS.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def proc_freq(self) -> pd.DataFrame:
    """
    Implement the Python version "proc freq" query in SAS.
    """
    data = self.copy()
    f = data.value_counts(dropna = False)
    p = data.value_counts(dropna = False, normalize = True)
    df = pd.concat([f,p], axis = 1, keys = ['frequency', 'percent'])
    df['cumFrequency'] = df['frequency'].cumsum()
    df['cumPercent'] = df['percent'].cumsum()
    return df

kDataFrame

Bases: DataFrame

The extension class of Pandas DataFrame including more useful methods to manipulate dataset.

Parameters:

data [DataFrame]: Any pandas dataframe dataset.

Return:

data [kDataFrame]: the extension object of dataframe.

源代码位于: Modeling_Tool/Core/kDataFrame.py
class kDataFrame(DataFrame):
    """
    The extension class of Pandas DataFrame including more useful methods to manipulate dataset.

    Parameters:
    -----------
    data [DataFrame]: Any pandas dataframe dataset.

    Return:
    -------
    data [kDataFrame]: the extension object of dataframe.

    """
    _metadata = ['added_property']
    added_property = 1  # This will be passed to copies

    @property
    def _constructor(self):
        return kDataFrame

    @property
    def _constructor_sliced(self):
        return kSeries

    def __init__(self, data, *args, **kwargs):
        super().__init__(data, *args, **kwargs)

    def move_column(self, colname: str, idx: int, inplace: bool = False):
        """
        To move a column into specific place by index.
        """
        if inplace:
            data = self
        else:
            data = self.copy()
        colarray = data.columns.tolist()
        colarray.remove(colname)
        colarray.insert(idx, colname)
        data = data[colarray]
        return data

    def convert_to_vintage(self, vintage_colname: str = 'VINTAGE', by: str = 'TRAN_TMS', inplace: bool = False):
        """
        To obtain a vintage column by a time/data column.
        """
        if inplace:
            data = self
        else:
            data = self.copy()
        import re
        data[vintage_colname] = data[by].apply(lambda x: re.search("\d{4}-\d{2}", x).group().replace('-', ''))
        if inplace:
            self = data
        return data

    def col_filter_regex(self, regex: str = ".*?of_co_at_12m", case_sensitive = True, inplace: bool = False):
        """
        To filter the DataFrame columns by regular expression.
        """
        if inplace:
            data = self
        else:
            data = self.copy()

        fltr = data.columns[data.columns.str.contains(regex, regex = True, case = case_sensitive)]
        return data[fltr]

    def row_filter_regex(self, col = None, regex: str = None, case_sensitive = True,
                         as_index = False, inplace: bool = False):
        """
        To filter the string format row using regex.
        """
        if inplace:
            data = self
        else:
            data = self.copy()

        fltr = data[col].astype('str').str.contains(pat = regex, regex = True, case = case_sensitive)
        if as_index:
            return data[fltr].set_index(col)
        return data[fltr]

    def scale_score(self, pb_score: str):
        """
        Scale the model scores (for internt segment of MCI model)
        """
        data = self.copy()
        def app_func(x):
            scores = x * 1.112
            if (scores > 0.9999999):
                return 0.9999999
            return scores
        return data[pb_score].apply(app_func)

    def proc_freq(self, var: str):
        """
        Implement the Python version "proc freq" query in SAS.
        """
        data = self.copy()
        f = data[var].value_counts(dropna = False)
        p = data[var].value_counts(dropna = False, normalize = True)
        df = pd.concat([f,p], axis = 1, keys = ['frequency', 'percent'])
        df['cumFrequency'] = df['frequency'].cumsum()
        df['cumPercent'] = df['percent'].cumsum()
        return df

    def unify_table_col_names(self, how: str = "lowercase", inplace: bool = False):
        """
        Unify the format of column names.
        """

        if inplace:
            data = self
        else:
            data = self.copy()

        cols = data.columns
        if how.lower() == "lower" or how.lower() == "lowercase":
            res = [name.lower() for name in cols]
        if how.lower() == "upper" or how.lower() == "uppercase":
            res = [name.upper() for name in cols]
        if how.lower() == "cap" or how.lower() == "capitalize":
            res = [name.capitalize() for name in cols]
        data.columns = res
        return data

    def convert_strlist_to_list(self, col: str):
        """
        cast string-type lists in a specified Series into real lists.
        """
        import re

        data = self.copy()
        str_col = kSeries([re.findall("\w+", str(x)) for x in data[col]])
        return str_col

    def to_pdDataFrame(self, inplace: bool = False):
        """
        Convert df_extension back to DataFrame.
        """
        if inplace:
            data = self
        else:
            data = self.copy()

        df = DataFrame(data)
        return df

move_column

move_column(colname: str, idx: int, inplace: bool = False)

To move a column into specific place by index.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def move_column(self, colname: str, idx: int, inplace: bool = False):
    """
    To move a column into specific place by index.
    """
    if inplace:
        data = self
    else:
        data = self.copy()
    colarray = data.columns.tolist()
    colarray.remove(colname)
    colarray.insert(idx, colname)
    data = data[colarray]
    return data

convert_to_vintage

convert_to_vintage(vintage_colname: str = 'VINTAGE', by: str = 'TRAN_TMS', inplace: bool = False)

To obtain a vintage column by a time/data column.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def convert_to_vintage(self, vintage_colname: str = 'VINTAGE', by: str = 'TRAN_TMS', inplace: bool = False):
    """
    To obtain a vintage column by a time/data column.
    """
    if inplace:
        data = self
    else:
        data = self.copy()
    import re
    data[vintage_colname] = data[by].apply(lambda x: re.search("\d{4}-\d{2}", x).group().replace('-', ''))
    if inplace:
        self = data
    return data

col_filter_regex

col_filter_regex(regex: str = '.*?of_co_at_12m', case_sensitive=True, inplace: bool = False)

To filter the DataFrame columns by regular expression.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def col_filter_regex(self, regex: str = ".*?of_co_at_12m", case_sensitive = True, inplace: bool = False):
    """
    To filter the DataFrame columns by regular expression.
    """
    if inplace:
        data = self
    else:
        data = self.copy()

    fltr = data.columns[data.columns.str.contains(regex, regex = True, case = case_sensitive)]
    return data[fltr]

row_filter_regex

row_filter_regex(col=None, regex: str = None, case_sensitive=True, as_index=False, inplace: bool = False)

To filter the string format row using regex.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def row_filter_regex(self, col = None, regex: str = None, case_sensitive = True,
                     as_index = False, inplace: bool = False):
    """
    To filter the string format row using regex.
    """
    if inplace:
        data = self
    else:
        data = self.copy()

    fltr = data[col].astype('str').str.contains(pat = regex, regex = True, case = case_sensitive)
    if as_index:
        return data[fltr].set_index(col)
    return data[fltr]

scale_score

scale_score(pb_score: str)

Scale the model scores (for internt segment of MCI model)

源代码位于: Modeling_Tool/Core/kDataFrame.py
def scale_score(self, pb_score: str):
    """
    Scale the model scores (for internt segment of MCI model)
    """
    data = self.copy()
    def app_func(x):
        scores = x * 1.112
        if (scores > 0.9999999):
            return 0.9999999
        return scores
    return data[pb_score].apply(app_func)

proc_freq

proc_freq(var: str)

Implement the Python version "proc freq" query in SAS.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def proc_freq(self, var: str):
    """
    Implement the Python version "proc freq" query in SAS.
    """
    data = self.copy()
    f = data[var].value_counts(dropna = False)
    p = data[var].value_counts(dropna = False, normalize = True)
    df = pd.concat([f,p], axis = 1, keys = ['frequency', 'percent'])
    df['cumFrequency'] = df['frequency'].cumsum()
    df['cumPercent'] = df['percent'].cumsum()
    return df

unify_table_col_names

unify_table_col_names(how: str = 'lowercase', inplace: bool = False)

Unify the format of column names.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def unify_table_col_names(self, how: str = "lowercase", inplace: bool = False):
    """
    Unify the format of column names.
    """

    if inplace:
        data = self
    else:
        data = self.copy()

    cols = data.columns
    if how.lower() == "lower" or how.lower() == "lowercase":
        res = [name.lower() for name in cols]
    if how.lower() == "upper" or how.lower() == "uppercase":
        res = [name.upper() for name in cols]
    if how.lower() == "cap" or how.lower() == "capitalize":
        res = [name.capitalize() for name in cols]
    data.columns = res
    return data

convert_strlist_to_list

convert_strlist_to_list(col: str)

cast string-type lists in a specified Series into real lists.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def convert_strlist_to_list(self, col: str):
    """
    cast string-type lists in a specified Series into real lists.
    """
    import re

    data = self.copy()
    str_col = kSeries([re.findall("\w+", str(x)) for x in data[col]])
    return str_col

to_pdDataFrame

to_pdDataFrame(inplace: bool = False)

Convert df_extension back to DataFrame.

源代码位于: Modeling_Tool/Core/kDataFrame.py
def to_pdDataFrame(self, inplace: bool = False):
    """
    Convert df_extension back to DataFrame.
    """
    if inplace:
        data = self
    else:
        data = self.copy()

    df = DataFrame(data)
    return df

CDC JSON 转换 — Json_Data_Converter

Json_Data_Converter

df_to_json

df_to_json(drv_df: DataFrame, input_vars: Optional[List[str]] = None, metadata_cols: Optional[List[str]] = None) -> Dict[str, Any]

将 drv_df DataFrame 转换为 JSON 格式。

两种模式: - 分区模式 (input_vars 指定): 元信息列(非 input_vars 列)提取为标量,input_vars 列收集为数组 放入 cdc_credit_inputs 下。 输出: {"": , ..., "cdc_credit_inputs": {"": [...], ...}}

  • 平铺模式 (input_vars=None): 所有列都作为数组放在一级 JSON 下,不再区分 metadata / input_vars。 输出: {"": [...], "": [...], ...}

参数:

名称 类型 描述 默认
drv_df DataFrame

源 DataFrame,每行一条记录。

必需
input_vars Optional[List[str]]

入模特征列名列表。为 None 时使用平铺模式,所有列均为数组。

None
metadata_cols Optional[List[str]]

分区模式下显式指定元信息列。平铺模式下忽略。

None

返回:

类型 描述
Dict[str, Any]

JSON 格式的字典。

引发:

类型 描述
ValueError

分区模式下,如果 metadata 列值不一致。

KeyError

如果 input_vars 中的列在 DataFrame 中不存在。

源代码位于: Modeling_Tool/Core/Json_Data_Converter.py
def df_to_json(
    drv_df: pd.DataFrame,
    input_vars: Optional[List[str]] = None,
    metadata_cols: Optional[List[str]] = None,
) -> Dict[str, Any]:
    """将 drv_df DataFrame 转换为 JSON 格式。

    两种模式:
      - 分区模式 (input_vars 指定):
          元信息列(非 input_vars 列)提取为标量,input_vars 列收集为数组
          放入 cdc_credit_inputs 下。
          输出: {"<meta>": <scalar>, ..., "cdc_credit_inputs": {"<var>": [...], ...}}

      - 平铺模式 (input_vars=None):
          所有列都作为数组放在一级 JSON 下,不再区分 metadata / input_vars。
          输出: {"<col_1>": [...], "<col_2>": [...], ...}

    Parameters
    ----------
    drv_df : pd.DataFrame
        源 DataFrame,每行一条记录。
    input_vars : Optional[List[str]]
        入模特征列名列表。为 None 时使用平铺模式,所有列均为数组。
    metadata_cols : Optional[List[str]]
        分区模式下显式指定元信息列。平铺模式下忽略。

    Returns
    -------
    Dict[str, Any]
        JSON 格式的字典。

    Raises
    ------
    ValueError
        分区模式下,如果 metadata 列值不一致。
    KeyError
        如果 input_vars 中的列在 DataFrame 中不存在。
    """
    # ── 平铺模式: 所有列直接作为一级字段 ──
    #   - 单行 DataFrame → 值直接为标量
    #   - 多行 DataFrame → 值为数组
    if input_vars is None:
        result: Dict[str, Any] = {}
        if len(drv_df) == 1:
            first_row = drv_df.iloc[0]
            for col in drv_df.columns:
                result[col] = _safe_json_value(first_row[col])
        else:
            for col in drv_df.columns:
                result[col] = _safe_series_to_list(drv_df[col])
        return result

    # ── 分区模式 (原有逻辑) ──
    missing_cols = set(input_vars) - set(drv_df.columns)
    if missing_cols:
        raise KeyError(
            f"input_vars 中的列在 DataFrame 中不存在: {missing_cols}"
        )

    if metadata_cols is None:
        metadata_cols = [c for c in drv_df.columns if c not in input_vars]

    missing_meta = set(metadata_cols) - set(drv_df.columns)
    if missing_meta:
        raise KeyError(
            f"metadata_cols 中的列在 DataFrame 中不存在: {missing_meta}"
        )

    # 校验 metadata 列的值在整个 DataFrame 中是否一致
    for col in metadata_cols:
        unique_vals = drv_df[col].drop_duplicates()
        if len(unique_vals) > 1:
            raise ValueError(
                f"元信息列 '{col}' 存在多个不同的值: {unique_vals.to_list()}。"
                f"预期 metadata 列在所有行中保持一致,请检查数据或调整 metadata_cols 参数。"
            )

    result = {}

    # 1) 元信息: 取自第一行
    first_row = drv_df.iloc[0]
    for col in metadata_cols:
        result[col] = _safe_json_value(first_row[col])

    # 2) cdc_credit_inputs: 每个 input_var 的值收集为数组
    cdc_credit_inputs: Dict[str, List[Any]] = {}
    for col in input_vars:
        cdc_credit_inputs[col] = _safe_series_to_list(drv_df[col])

    result["cdc_credit_inputs"] = cdc_credit_inputs

    return result

df_to_json_custom

df_to_json_custom(drv_df: DataFrame, input_vars: List[str], inputs_key: str = 'inputs', metadata_cols: Optional[List[str]] = None, unwrap_single: bool = True) -> Dict[str, Any]

将 drv_df DataFrame 转换为分区 JSON 格式,支持自定义二级 key 和自动解包。

与 df_to_json 分区模式类似,但: - 二级 JSON 的 key 名称可自定义(通过 inputs_key) - 二级 JSON 中长度为 1 的数组自动解包为标量(通过 unwrap_single)

参数:

名称 类型 描述 默认
drv_df DataFrame

源 DataFrame。

必需
input_vars List[str]

放入二级 JSON 的列名列表。

必需
inputs_key str

二级 JSON 的 key 名称,默认 "inputs"。

'inputs'
metadata_cols Optional[List[str]]

一级元信息列。为 None 时自动推导(非 input_vars 的列)。

None
unwrap_single bool

是否将二级 JSON 中长度为 1 的数组解包为标量。默认 True。

True

返回:

类型 描述
Dict[str, Any]

{ "": , ..., "": { "": , ... } }

示例:

>>> df = pd.DataFrame({'req': ['a','a'], 'x': [1,2], 'y': [3,4]})
>>> df_to_json_custom(df, input_vars=['x','y'], inputs_key='features')
{'req': 'a', 'features': {'x': [1,2], 'y': [3,4]}}
>>> df_single = pd.DataFrame({'req': ['a'], 'x': [1], 'y': [3]})
>>> df_to_json_custom(df_single, input_vars=['x','y'], inputs_key='features')
{'req': 'a', 'features': {'x': 1, 'y': 3}}
源代码位于: Modeling_Tool/Core/Json_Data_Converter.py
def df_to_json_custom(
    drv_df: pd.DataFrame,
    input_vars: List[str],
    inputs_key: str = "inputs",
    metadata_cols: Optional[List[str]] = None,
    unwrap_single: bool = True,
) -> Dict[str, Any]:
    """将 drv_df DataFrame 转换为分区 JSON 格式,支持自定义二级 key 和自动解包。

    与 df_to_json 分区模式类似,但:
      - 二级 JSON 的 key 名称可自定义(通过 inputs_key)
      - 二级 JSON 中长度为 1 的数组自动解包为标量(通过 unwrap_single)

    Parameters
    ----------
    drv_df : pd.DataFrame
        源 DataFrame。
    input_vars : List[str]
        放入二级 JSON 的列名列表。
    inputs_key : str
        二级 JSON 的 key 名称,默认 "inputs"。
    metadata_cols : Optional[List[str]]
        一级元信息列。为 None 时自动推导(非 input_vars 的列)。
    unwrap_single : bool
        是否将二级 JSON 中长度为 1 的数组解包为标量。默认 True。

    Returns
    -------
    Dict[str, Any]
        {
            "<meta>": <scalar>,
            ...,
            "<inputs_key>": {
                "<var>": <scalar_or_array>,
                ...
            }
        }

    Examples
    --------
    >>> df = pd.DataFrame({'req': ['a','a'], 'x': [1,2], 'y': [3,4]})
    >>> df_to_json_custom(df, input_vars=['x','y'], inputs_key='features')
    {'req': 'a', 'features': {'x': [1,2], 'y': [3,4]}}

    >>> df_single = pd.DataFrame({'req': ['a'], 'x': [1], 'y': [3]})
    >>> df_to_json_custom(df_single, input_vars=['x','y'], inputs_key='features')
    {'req': 'a', 'features': {'x': 1, 'y': 3}}
    """
    # ── 参数校验 ──
    missing_cols = set(input_vars) - set(drv_df.columns)
    if missing_cols:
        raise KeyError(
            f"input_vars 中的列在 DataFrame 中不存在: {missing_cols}"
        )

    if metadata_cols is None:
        metadata_cols = [c for c in drv_df.columns if c not in input_vars]

    missing_meta = set(metadata_cols) - set(drv_df.columns)
    if missing_meta:
        raise KeyError(
            f"metadata_cols 中的列在 DataFrame 中不存在: {missing_meta}"
        )

    # 校验 metadata 列的值在整个 DataFrame 中是否一致
    for col in metadata_cols:
        unique_vals = drv_df[col].drop_duplicates()
        if len(unique_vals) > 1:
            raise ValueError(
                f"元信息列 '{col}' 存在多个不同的值: {unique_vals.to_list()}。"
                f"预期 metadata 列在所有行中保持一致,请检查数据或调整 metadata_cols 参数。"
            )

    # ── 构建输出 ──
    result: Dict[str, Any] = {}

    # 1) 元信息标量
    first_row = drv_df.iloc[0]
    for col in metadata_cols:
        result[col] = _safe_json_value(first_row[col])

    # 2) 二级 JSON: 自定义 key + 单元素自动解包
    inputs: Dict[str, Any] = {}
    for col in input_vars:
        arr = _safe_series_to_list(drv_df[col])
        if unwrap_single and len(arr) == 1:
            inputs[col] = arr[0]
        else:
            inputs[col] = arr

    result[inputs_key] = inputs

    return result

json_to_df

json_to_df(json_data: Union[str, Dict[str, Any]], input_vars: Optional[List[str]] = None, metadata_cols: Optional[List[str]] = None) -> DataFrame

将 JSON 格式还原为 DataFrame(自动检测三种格式)。

三种模式: 1) Row-Oriented 模式 (JSON 有 cdc_query_credits key)——NEW 一级标量为 metadata,cdc_query_credits 为对象数组,每个对象一行。 输出: metadata 列广播到所有行 + 对象字段展开为列。

   {
       "requestId": "req@123", "pullLogId": 3836171, ...,
       "cdc_query_credits": [
           {"_id": "id1", "montoPagar": 922, ...},
           {"_id": "id2", "montoPagar": 0,   ...}
       ]
   }

2) 分区模式 (JSON 有 cdc_credit_inputs key) 从 cdc_credit_inputs 取数组列,其余一级 key 为 metadata scalars。

   {
       "requestid": "req_001", "pulllogid": 3836171, ...,
       "cdc_credit_inputs": {
           "account_open_days": [2381, 4493],
           "pagoactual": ["V", "V"]
       }
   }

3) 平铺模式 (JSON 无 cdc_credit_inputs 也无 cdc_query_credits) 所有一级 key 均为列名,list 值展开为行,scalar 值广播。

参数:

名称 类型 描述 默认
json_data Union[str, Dict[str, Any]]

JSON 字符串或字典。

必需
input_vars Optional[List[str]]

分区模式下的入模特征列名。为 None 时自动检测 JSON 格式。 注: row-oriented 模式下忽略此参数(cdc_query_credits 中的全部字段均展开)。

None
metadata_cols Optional[List[str]]

元信息列名。为 None 时自动推导(非 cdc_* 的一级 key)。

None

返回:

类型 描述
DataFrame

还原后的 DataFrame,metadata 列在前,数据列在后。

引发:

类型 描述
ValueError

如果数组长度不一致,或 cdc_query_credits 不是数组。

源代码位于: Modeling_Tool/Core/Json_Data_Converter.py
def json_to_df(
    json_data: Union[str, Dict[str, Any]],
    input_vars: Optional[List[str]] = None,
    metadata_cols: Optional[List[str]] = None,
) -> pd.DataFrame:
    """将 JSON 格式还原为 DataFrame(自动检测三种格式)。

    三种模式:
      1) Row-Oriented 模式 (JSON 有 cdc_query_credits key)——NEW
           一级标量为 metadata,cdc_query_credits 为对象数组,每个对象一行。
           输出: metadata 列广播到所有行 + 对象字段展开为列。

           {
               "requestId": "req@123", "pullLogId": 3836171, ...,
               "cdc_query_credits": [
                   {"_id": "id1", "montoPagar": 922, ...},
                   {"_id": "id2", "montoPagar": 0,   ...}
               ]
           }

      2) 分区模式 (JSON 有 cdc_credit_inputs key)
           从 cdc_credit_inputs 取数组列,其余一级 key 为 metadata scalars。

           {
               "requestid": "req_001", "pulllogid": 3836171, ...,
               "cdc_credit_inputs": {
                   "account_open_days": [2381, 4493],
                   "pagoactual": ["V", "V"]
               }
           }

      3) 平铺模式 (JSON 无 cdc_credit_inputs 也无 cdc_query_credits)
           所有一级 key 均为列名,list 值展开为行,scalar 值广播。

    Parameters
    ----------
    json_data : Union[str, Dict[str, Any]]
        JSON 字符串或字典。
    input_vars : Optional[List[str]]
        分区模式下的入模特征列名。为 None 时自动检测 JSON 格式。
        注: row-oriented 模式下忽略此参数(cdc_query_credits 中的全部字段均展开)。
    metadata_cols : Optional[List[str]]
        元信息列名。为 None 时自动推导(非 cdc_* 的一级 key)。

    Returns
    -------
    pd.DataFrame
        还原后的 DataFrame,metadata 列在前,数据列在后。

    Raises
    ------
    ValueError
        如果数组长度不一致,或 cdc_query_credits 不是数组。
    """
    if isinstance(json_data, str):
        data = json.loads(json_data)
    else:
        data = json_data

    has_cdc_inputs = "cdc_credit_inputs" in data
    has_cdc_credits = "cdc_query_credits" in data

    # ═══════════════════════════════════════════════════════════════════════
    # 模式 1: Row-Oriented — cdc_query_credits(NEW)
    # ═══════════════════════════════════════════════════════════════════════
    if has_cdc_credits:
        credits = data["cdc_query_credits"]
        if not isinstance(credits, list):
            raise ValueError(
                f"cdc_query_credits 必须是数组,实际类型为 {type(credits).__name__}"
            )

        # 从对象数组构建 DataFrame
        if len(credits) == 0:
            df = pd.DataFrame()
        else:
            df = pd.DataFrame(credits)

        # 广播一级 metadata 标量到所有行
        for key, val in data.items():
            if key == "cdc_query_credits":
                continue
            if len(df) == 0:
                df[key] = pd.Series(dtype=type(val) if val is not None else object)
            else:
                df[key] = val

        # 列排序: metadata 在前 → 数据字段在后
        meta_cols_result = [k for k in data if k != "cdc_query_credits"]
        credit_cols_result = [c for c in df.columns if c not in meta_cols_result]
        df = df[meta_cols_result + credit_cols_result]

        return df

    # ═══════════════════════════════════════════════════════════════════════
    # 模式 2 & 3: cdc_credit_inputs 分区模式 / 平铺模式
    # ═══════════════════════════════════════════════════════════════════════

    # ── 自动检测: input_vars 未指定时的处理 ──
    if input_vars is None:
        if has_cdc_inputs:
            # 分区模式: input_vars = cdc_credit_inputs 的全部 key
            input_vars = list(data["cdc_credit_inputs"].keys())
        else:
            # 平铺模式: 所有 list 值都是列
            input_vars = []  # 无特殊 input_vars 区分

    # ── 模式 3: 平铺模式: 所有一级 key 直接作为列 ──
    if not has_cdc_inputs:
        # 找到所有 list 列和 scalar 列
        list_cols = {}
        scalar_cols = {}
        n_rows = 0
        for key, val in data.items():
            if isinstance(val, list):
                list_cols[key] = val
                if n_rows == 0:
                    n_rows = len(val)
                elif len(val) != n_rows:
                    raise ValueError(
                        f"数组长度不一致: 期望 {n_rows},'{key}' 长度为 {len(val)}"
                    )
            else:
                scalar_cols[key] = val

        if n_rows == 0:
            # 没有 list 列时: 若存在 scalar 列,构造单行 DataFrame;
            # 否则(空 JSON)返回空 DataFrame。
            if scalar_cols:
                return pd.DataFrame([scalar_cols])
            return pd.DataFrame()

        df = pd.DataFrame(list_cols)
        for key, val in scalar_cols.items():
            df[key] = val
        return df

    # ── 模式 2: 分区模式 (原有逻辑) ──
    cdc_inputs = data["cdc_credit_inputs"]

    missing_inputs = set(input_vars) - set(cdc_inputs.keys())
    if missing_inputs:
        raise ValueError(
            f"input_vars 中的字段在 cdc_credit_inputs 中不存在: {missing_inputs}"
        )

    # 校验所有数组长度一致
    lengths: Dict[str, int] = {}
    for key in cdc_inputs:
        if isinstance(cdc_inputs[key], list):
            lengths[key] = len(cdc_inputs[key])

    if lengths:
        ref_key = next((k for k in input_vars if k in lengths), list(lengths.keys())[0])
        ref_len = lengths[ref_key]
        for key, length in lengths.items():
            if length != ref_len:
                raise ValueError(
                    f"cdc_credit_inputs 中数组长度不一致: "
                    f"'{ref_key}' 长度为 {ref_len},但 '{key}' 长度为 {length}"
                )
        n_rows = ref_len
    else:
        n_rows = 0

    # 构建 DataFrame
    df = pd.DataFrame({col: cdc_inputs[col] for col in input_vars})

    extra_input_cols = [k for k in cdc_inputs if k not in input_vars]
    for col in extra_input_cols:
        df[col] = cdc_inputs[col]

    if metadata_cols is None:
        metadata_cols = [k for k in data if k != "cdc_credit_inputs"]

    for col in metadata_cols:
        if col in data:
            df[col] = data[col]

    ordered_cols = (
        [c for c in metadata_cols if c in df.columns]
        + [c for c in input_vars if c in df.columns and c not in metadata_cols]
        + [c for c in extra_input_cols if c in df.columns]
    )
    df = df[ordered_cols]

    return df

df_to_json_string

df_to_json_string(drv_df: DataFrame, input_vars: Optional[List[str]] = None, metadata_cols: Optional[List[str]] = None, indent: Optional[int] = 2, ensure_ascii: bool = False) -> str

df_to_json 的便捷封装,直接返回 JSON 字符串。

源代码位于: Modeling_Tool/Core/Json_Data_Converter.py
def df_to_json_string(
    drv_df: pd.DataFrame,
    input_vars: Optional[List[str]] = None,
    metadata_cols: Optional[List[str]] = None,
    indent: Optional[int] = 2,
    ensure_ascii: bool = False,
) -> str:
    """df_to_json 的便捷封装,直接返回 JSON 字符串。"""
    result = df_to_json(drv_df, input_vars, metadata_cols)
    result = _sanitize_for_json(result)
    return json.dumps(result, indent=indent, ensure_ascii=ensure_ascii)

json_string_to_df

json_string_to_df(json_string: str, input_vars: Optional[List[str]] = None, metadata_cols: Optional[List[str]] = None) -> DataFrame

json_to_df 的便捷封装,接受 JSON 字符串输入。

源代码位于: Modeling_Tool/Core/Json_Data_Converter.py
def json_string_to_df(
    json_string: str,
    input_vars: Optional[List[str]] = None,
    metadata_cols: Optional[List[str]] = None,
) -> pd.DataFrame:
    """json_to_df 的便捷封装,接受 JSON 字符串输入。"""
    return json_to_df(json_string, input_vars, metadata_cols)

df_to_json_file

df_to_json_file(drv_df: DataFrame, output_path: str, input_vars: Optional[List[str]] = None, metadata_cols: Optional[List[str]] = None, indent: Optional[int] = 2, ensure_ascii: bool = False) -> str

将 drv_df DataFrame 转换为 expected JSON 格式并写入 .json 文件。

参数:

名称 类型 描述 默认
drv_df DataFrame

源 DataFrame,每行一条征信账户记录。

必需
input_vars List[str]

入模特征列名列表。

None
output_path str

输出 .json 文件的路径。

必需
metadata_cols Optional[List[str]]

显式指定元信息列名。为 None 时自动推导。

None
indent Optional[int]

JSON 缩进空格数。None 表示紧凑输出(单行),默认 2。

2
ensure_ascii bool

是否将非 ASCII 字符转义为 \uXXXX。默认 False,保留中文等原始字符。

False

返回:

类型 描述
str

写入文件的绝对路径。

源代码位于: Modeling_Tool/Core/Json_Data_Converter.py
def df_to_json_file(
    drv_df: pd.DataFrame,
    output_path: str,
    input_vars: Optional[List[str]] = None,
    metadata_cols: Optional[List[str]] = None,
    indent: Optional[int] = 2,
    ensure_ascii: bool = False,
) -> str:
    """将 drv_df DataFrame 转换为 expected JSON 格式并写入 .json 文件。

    Parameters
    ----------
    drv_df : pd.DataFrame
        源 DataFrame,每行一条征信账户记录。
    input_vars : List[str]
        入模特征列名列表。
    output_path : str
        输出 .json 文件的路径。
    metadata_cols : Optional[List[str]]
        显式指定元信息列名。为 None 时自动推导。
    indent : Optional[int]
        JSON 缩进空格数。None 表示紧凑输出(单行),默认 2。
    ensure_ascii : bool
        是否将非 ASCII 字符转义为 \\uXXXX。默认 False,保留中文等原始字符。

    Returns
    -------
    str
        写入文件的绝对路径。
    """
    result = df_to_json(drv_df, input_vars, metadata_cols)
    result = _sanitize_for_json(result)
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(result, f, indent=indent, ensure_ascii=ensure_ascii)
    return os.path.abspath(output_path)

load_json_file

load_json_file(file_path: str) -> Dict[str, Any]

从 .json 文件加载为字典。

参数:

名称 类型 描述 默认
file_path str

.json 文件的路径。

必需

返回:

类型 描述
Dict[str, Any]

解析后的字典,结构符合 expected JSON 格式。

源代码位于: Modeling_Tool/Core/Json_Data_Converter.py
def load_json_file(file_path: str) -> Dict[str, Any]:
    """从 .json 文件加载为字典。

    Parameters
    ----------
    file_path : str
        .json 文件的路径。

    Returns
    -------
    Dict[str, Any]
        解析后的字典,结构符合 expected JSON 格式。
    """
    with open(file_path, 'r', encoding='utf-8') as f:
        return json.load(f)

json_to_file

json_to_file(data: Dict[str, Any], output_path: str, indent: Optional[int] = 2, ensure_ascii: bool = False) -> str

将 Python dict 写入 .json 文件。

参数:

名称 类型 描述 默认
data Dict[str, Any]

待写入的字典。

必需
output_path str

输出 .json 文件路径。

必需
indent Optional[int]

JSON 缩进空格数。None 表示紧凑单行,默认 2。

2
ensure_ascii bool

是否转义非 ASCII 字符。默认 False。

False

返回:

类型 描述
str

写入文件的绝对路径。

源代码位于: Modeling_Tool/Core/Json_Data_Converter.py
def json_to_file(
    data: Dict[str, Any],
    output_path: str,
    indent: Optional[int] = 2,
    ensure_ascii: bool = False,
) -> str:
    """将 Python dict 写入 .json 文件。

    Parameters
    ----------
    data : Dict[str, Any]
        待写入的字典。
    output_path : str
        输出 .json 文件路径。
    indent : Optional[int]
        JSON 缩进空格数。None 表示紧凑单行,默认 2。
    ensure_ascii : bool
        是否转义非 ASCII 字符。默认 False。

    Returns
    -------
    str
        写入文件的绝对路径。
    """
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(_sanitize_for_json(data), f, indent=indent, ensure_ascii=ensure_ascii)
    return os.path.abspath(output_path)