Full Code of itlubber/scorecardpipeline for AI

main 793ee47b03f3 cached
54 files
12.6 MB
3.3M tokens
325 symbols
1 requests
Copy disabled (too large) Download .txt
Showing preview only (13,395K chars total). Download the full file to get everything.
Repository: itlubber/scorecardpipeline
Branch: main
Commit: 793ee47b03f3
Files: 54
Total size: 12.6 MB

Directory structure:
gitextract_wka03khu/

├── .gitignore
├── .readthedocs.yaml
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs/
│   ├── Makefile
│   ├── make.bat
│   ├── requirements.txt
│   └── source/
│       ├── CNAME
│       ├── conf.py
│       ├── index.rst
│       ├── install.html
│       ├── quickstart.md
│       └── scorecardpipeline.rst
├── examples/
│   ├── auto_report.ipynb
│   ├── automl_example.ipynb
│   ├── cycle_end_vintage.sql
│   ├── gbm_model_examples.ipynb
│   ├── loan_replan.py
│   ├── model_report/
│   │   ├── auto_eda.html
│   │   ├── 三方数据测试报告.xlsx
│   │   ├── 决策树组合策略挖掘.xlsx
│   │   └── 评分卡模型报告.xlsx
│   ├── month_end_vintage.sql
│   ├── profitability_calculation.py
│   ├── quickstart.ipynb
│   ├── rule_efficiency.ipynb
│   ├── rule_extraction.ipynb
│   ├── rule_test.py
│   ├── scorecard.pmml
│   ├── scorecard_samples.ipynb
│   ├── third_party_data_describe.ipynb
│   ├── 同盾百融三方数据评分卡建模样例.ipynb
│   ├── 数据分布情况.xlsx
│   ├── 权益类产品盈利测算表.xlsx
│   └── 特征选择.ipynb
├── requirements.txt
├── scorecardpipeline/
│   ├── __init__.py
│   ├── auto_eda.py
│   ├── auto_report.py
│   ├── excel_writer.py
│   ├── feature_engineering.py
│   ├── feature_selection.py
│   ├── financial.py
│   ├── germancredit.csv
│   ├── logger.py
│   ├── model.py
│   ├── processing.py
│   ├── rule.py
│   ├── rule_extraction.py
│   ├── scorecard.py
│   ├── template.xlsx
│   └── utils.py
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/#use-with-ide
.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# itlubber
*.DS_Store
*.virtual_documents
test*
*.png
examples/model_report/*.png
examples/scorecard_samples_360.ipynb


================================================
FILE: .readthedocs.yaml
================================================
version: 2

build:
  os: ubuntu-22.04
  tools:
    python: "3.11"

python:
  install:
    - requirements: docs/requirements.txt

sphinx:
  configuration: docs/source/conf.py


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 itlubber

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include scorecardpipeline/*.ttf scorecardpipeline/*.csv scorecardpipeline/*.xlsx requirements*.txt


================================================
FILE: README.md
================================================
# 评分卡pipeline建模包

<img src="https://itlubber.art/upload/scorecardpipeline.png" alt="itlubber.png" width="100%" border=0/> 

`scorecardpipeline`封装常用的风控策略分析和评分卡建模相关组件,API风格参考`sklearn`,支持`pipeline`式端到端评分卡建模、三方数据分析报告输出、规则集效果评估、特征有效性分析、`excel`报告输出、评分卡`PMML`文件导出、评分卡全流程超参数搜索等功能

> 教程:https://scorecardpipeline.itlubber.art
> 
> pipy包:https://pypi.org/project/scorecardpipeline
>
> 仓库地址:https://github.com/itlubber/scorecardpipeline
> 
> 博文地址:https://scorecardpipeline.itlubber.art/quickstart.html
> 
> 微信公共号推文:https://mp.weixin.qq.com/s/eCTp4h0fau77xOgf_V28wQ

> PS: *新注册账号请勿 `star` 本项目*

## 交流

|  微信 |  微信公众号 |
| :---: | :----: |
| <img src="https://itlubber.art/upload/itlubber.png" alt="itlubber.png" width="50%" border=0/> | <img src="https://itlubber.art/upload/itlubber_art.png" alt="itlubber_art.png" width="50%" border=0/> |
|  itlubber  | itlubber_art |


## 引言

作为一名金融搬砖工作者,评分卡建模怎么也算是基操。本文主要对笔者日常使用的评分卡建模代码进行讲解,说明如何一步步从原始数据到最终评分卡模型以及如何解读产出的模型报告文档。

本文所有代码已全部提交至笔者GITHUB公开仓库,各位看官按需取用,用完记得顺带给个star以鼓励笔者继续开源相关工作。

本文使用笔者对toad、scorecardpy、optbinning等库进行二次封装后的代码进行实操,文中会对仓库中的部分代码细节进行说明。本文旨在对仓库评分卡建模流程进行说明,并提供一个可以直接运行的完整示例,让更多金融从业小伙伴掌握整套评分卡模型构建方法。


## 项目说明

### 仓库地址

https://github.com/itlubber/scorecardpipeline

### 代码结构

该仓库下代码主要用于提供评分卡建模相关的组件,项目结构如下:

```base
>> tree
.
├── LICENSE                         # 开源协议
├── README.md                       # 相关说明文档
├── requirements.txt                # 相关依赖包
└── setup.py                        # 打包文件
├── examples                        # 演示样例
│   └── scorecard_samples.ipynb
└── scorecardpipeline               # scorecardpipeline 包文件
    ├── excel_writer.py             # 操作 excel 的公共方法
    ├── template.xlsx               # excel 模版文件
    ├── matplot_chinese.ttf         # 中文字体
    ├── processing.py               # 数据处理相关代码
    ├── model.py                    # 模型相关代码
    └── utils.py                    # 公用方法
```

### 简要说明

+ `processing` 中提供了数据前处理相关的方法:特征筛选方法(`FeatureSelection`、`StepwiseSelection`)、变量分箱方法(`Combiner`)、变量证据权重转换方法(`WOETransformer`),方法继承`sklearn.base`中的`BaseEstimator`和`TransformerMixin`,能够支持构建`pipeline`和超参数搜索
+ `model`中提供了基于`sklearn.linear_model.LogisticRegression`实现的`ITLubberLogisticRegression`,同时重写了`toad.ScoreCard`,以支持模型相关内容的输出
+ `excel_writer` 中提供了操作 `excel` 的一系列公共方法,包含设置条件格式、设置列宽、设置数字格式、插入指定样式的内容(`insert_value2sheet`)、插入图片数据(`insert_pic2sheet`)、插入`dataframe`数据内容(`insert_df2sheet`)、保存`excel`文件(`save`)等方法

### `scorecardpipeline` 安装

+ `pipy` 安装

```bash
>> pip install scorecardpipeline -i https://pypi.Python.org/simple/

Looking in indexes: https://pypi.Python.org/simple/
Collecting scorecardpipeline
  Downloading scorecardpipeline-0.1.5-py3-none-any.whl (36 kB)
  ......
Installing collected packages: sklearn-pandas, scorecardpy, sklearn2pmml, scorecardpipeline
Successfully installed scorecardpipeline-0.1.5 scorecardpy-0.1.9.2 sklearn-pandas-2.2.0 sklearn2pmml-0.92.2
```

+ 源码编译

```bash
python setup.py sdist bdist_wheel
pip install dist/scorecardpipeline-0.1.11-py3-none-any.whl
# twine upload dist/*
```

+ 特别说明

`评分卡` 转 `PMML` 目前部分依赖 `java` ,如果需要解锁这部分功能,需要先安装 `jdk` 环境,推荐 `jdk 1.8 +`

如果 `windows` 用户在安装过程中报错提示 `Microsoft Visual C++` 相关的异常信息,请下载安装 [`Latest Supported Visual C++ Redistributable`](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version) 后再重新安装


## 评分卡建模

### 数据准备

笔者仓库下几乎所有评分卡相关项目默认使用`scorecardpy`库中提供的`germancredit`数据集进行示例,数据集中包含类别型变量、数值型变量、样本好坏标签,共`1000`条数据,数据集中不包含缺失值,在部分示例中会随机替换数据集中的内容为缺失值,模拟实际生产中的真实数据。

```python
target = "creditability"
data = sc.germancredit()
data[target] = data[target].map({"good": 0, "bad": 1})
```

### 数据集拆分

通常建模过程中会将数据集拆分成训练数据集和测试数据集集,训练数据集用于模型参数的训练,测试数据集用于评估模型好坏,且在整个建模过程中实际上只能使用一次(任何针对测试数据集调模型的,都存在模型过拟合的情况,除非你的数据不包含任何噪声数据)。而为了弥补测试数据集只能用一次用于评估模型好坏的尴尬,大多都会再拆分一个验证数据集,用于模型训练过程中判别模型是否向好的方向进行优化。

在金融建模场景中,上述三个数据集通常被称为训练数据集、测试数据集(即上述三个数据集中的验证集)、跨时间验证集(即上述三个数据集中的测试集)。同时,在金融场景中,跨时间验证集的拆分方式通常使用类似时间序列数据集的拆分方法,按照时间将数据集拆分为建模集(包含训练集和测试集)、跨时间验证集(Out of Time,OOT),以保证模型在不同时间段内的泛化能力。

本文为了简单起见,只拆分了训练集、测试集,OOT直接拷贝训练集来演示评分卡建模完整过程。

+ 数据集拆分示例


```python
train, test = train_test_split(data, test_size=0.3, shuffle=True, stratify=data[target])
oot = data.copy()
```

### 特征粗筛(可选)

通常当数据特征数量较多时,针对每个特征做分箱太耗时间的情况下,可以在分箱之前针对数据集进行一次简单的特征筛选工作。剔除数据集中空值或唯一值占比较高、变量之间相关性过高、变量`IV值`(`Information Value`)过低的特征。

+ 方法简介

```python
class FeatureSelection(TransformerMixin, BaseEstimator):
    
    def __init__(self, target="target", empty=0.95, iv=0.02, corr=0.7, exclude=None, return_drop=True, identical=0.95, remove=None, engine="scorecardpy", target_rm=False):
        """
        ITLUBBER提供的特征筛选方法

        Args:
            target: 数据集中标签名称,默认 target
            empty: 空值率,默认 0.95, 即空值占比超过 95% 的特征会被剔除
            iv: IV值,默认 0.02,即iv值小于 0.02 时特征会被剔除
            corr: 相关性,默认 0.7,即特征之间相关性大于 0.7 时会剔除iv较小的特征
            identical: 唯一值占比,默认 0.95,即当特征的某个值占比超过 95% 时,特征会被剔除
            engine: 特征筛选使用的引擎,可选 "toad", "scorecardpy" 两种,默认 scorecardpy
            remove: 引擎使用 scorecardpy 时,可以传入需要强制删除的变量
            return_drop: 是否返回删除信息,默认 True,即默认返回删除特征信息
            target_rm: 是否剔除标签,默认 False,即不剔除
            exclude: 是否需要强制保留某些特征
        """
```

+ 使用方法


```python
# 常规使用方法
selection = FeatureSelection(target=target, engine="toad", return_drop=True, corr=0.9, iv=0.01)
train = selection.fit_transform(train)

# pipeline 使用方法
feature_pipeline = Pipeline([
        ("preprocessing_select", FeatureSelection(target=target, engine="scorecardpy")),
        ......
    ])
feature_pipeline.fit(train)
```

### 特征分箱

在评分卡建模过程中,为了保证模型效果的稳定性,需要对变量进行一定的前处理。

对于数值型变量,常规的处理是使用等频分箱、等距分箱、决策树分箱、卡方分箱、最优KS分箱等方式对数值型变量进行离散化处理,以保证特征稳定的同时,能够从业务上有一定的逻辑依据和解释性。对数值型变量进行分箱还可以减少异常值对模型的影响,降低模型复杂度。

对于类别型变量,很多类别的客群属性比较相似,客户违约概率相近,将这些客群合并能够增加变量稳定性、降低模型的复杂程度和增强特征可解释性。

为了对上述两类特征进行分箱,笔者将toad和optbinning两个库的分箱功能结合,合并进了processing.Combiner,能够支持变量单调分箱、“U”型分箱以及optbinning支持的所有形式的最优特征分箱方案,同时保留缺失值单独一箱,保证了评分卡模型后期上线后缺失值不知道如何填充的尴尬。

+ 方法简介

```python
class Combiner(TransformerMixin, BaseEstimator):
    
    def __init__(self, target="target", method='chi', engine="toad", empty_separate=False, min_samples=0.05, min_n_bins=2, max_n_bins=3, max_n_prebins=10, min_prebin_size=0.02, min_bin_size=0.05, max_bin_size=None, gamma=0.01, monotonic_trend="auto_asc_desc", rules={}, n_jobs=1):
        """
        特征分箱封装方法

        Args:
            target: 数据集中标签名称,默认 target
            method: 特征分箱方法,可选 "chi", "dt", "quantile", "step", "kmeans", "cart", "mdlp", "uniform", 参考 toad.Combiner & optbinning.OptimalBinning
            engine: 分箱引擎,可选 "optbinning", "toad"
            empty_separate: 是否空值单独一箱, 默认 False,推荐设置为 True
            min_samples: 最小叶子结点样本占比,参考对应文档进行设置,默认 5%
            min_n_bins: 最小分箱数,默认 2,即最小拆分2箱
            max_n_bins: 最大分像素,默认 3,即最大拆分3箱,推荐设置 3 ~ 5,不宜过多,偶尔使用 optbinning 时不起效
            max_n_prebins: 使用 optbinning 时预分箱数量
            min_prebin_size: 使用 optbinning 时预分箱叶子结点(或者每箱)样本占比,默认 2%
            min_bin_size: 使用 optbinning 正式分箱叶子结点(或者每箱)最小样本占比,默认 5%
            max_bin_size: 使用 optbinning 正式分箱叶子结点(或者每箱)最大样本占比,默认 None
            gamma: 使用 optbinning 分箱时限制过拟合的正则化参数,值越大惩罚越多,默认 0。01
            monotonic_trend: 使用 optbinning 正式分箱时的坏率策略,默认 auto,可选 "auto", "auto_heuristic", "auto_asc_desc", "ascending", "descending", "convex", "concave", "peak", "valley", "peak_heuristic", "valley_heuristic"
            rules: 自定义分箱规则,toad.Combiner 能够接收的形式
            n_jobs: 使用多进程加速的worker数量,默认单进程
        """
```

+ 使用方法


```python
combiner = Combiner(min_samples=0.2, empty_separate=True, target=target)
combiner.fit(train)
train = combiner.transform(train)

# pipeline
feature_pipeline = Pipeline([
......
        ("combiner", Combiner(target=target, min_samples=0.2)),
        ......
    ])
feature_pipeline.fit(train)

# save all bin_plot
_combiner = feature_pipeline.named_steps["combiner"]
for col in woe_train.columns:
    if col != target:
        _combiner.bin_plot(train, col, labels=True, save=f"outputs/bin_plots/train_{col}.png")
        _combiner.bin_plot(test, col, labels=True, save=f"outputs/bin_plots/test_{col}.png")
        _combiner.bin_plot(oot, col, labels=True, save=f"outputs/bin_plots/oot_{col}.png")
```

### 特征编码

特征分箱后,本质上只是将特征离散化为几箱,每一箱的值被赋予了一个标签,例如0、1、2、3、...,尽管这些标签在一定程度上也能够表征客户的风险水平,能够作为输入建立一个模型,但基于上述标签构建的模型在评估客户风险状况时比较局限,很难精准刻画客户的风险状况。

对于上述离散化后的特征,常规的处理方式有ONE-HOT编码、顺序编码、TARGET编码、COUNT编码等方式。在金融建模场景,尤其是评分卡建模时,通常使用WOE编码对离散化后的特征进行编码。WOE编码将每个分箱内的坏样本比例除以好样本比例后取对数来编码,能够反映每个分箱内客户的坏客户分布与好客户分布之间的差异以及该箱内坏好比(odds)相对于总体的坏好比之间的差异性。

+ 笔者基于toad.transform.WOETransformer重写了相关的方法,方法如下:


```python
class WOETransformer(TransformerMixin, BaseEstimator):
    
    def __init__(self, target="target", exclude=None):
        """
        WOE转换器

        Args:
            target: 数据集中标签名称,默认 target
            exclude: 不需要转换 woe 的列
        """
```

+ 使用方法

```python
# 常规使用方式
transformer = WOETransformer(target=target)
train = transformer.fit_transform(train)

# pipeline
feature_pipeline = Pipeline([
    ......
    ("transformer", WOETransformer(target=target)),
    ......
])
feature_pipeline.fit(train)
```

### 特征精筛

通常对特征`WOE`编码转换后,特征之间的相关性会上升、唯一值占比会增加、变量`IV`值会下降(同一特征分箱数越多`IV`值越大),为了避免入模特征`相关性过高`、`唯一值占比过高`、`IV过低`等问题的出现,需要在转换编码后进行特征精筛,筛选的限制条件相较分箱前的粗筛更严,以保证模型稳定性和泛化能力。

相关方法参照特征粗筛,在此不过多赘述。


### 逐步回归特征筛选

特征在进行完前面的步骤后,在正式建模前,通常还会使用逐步回归对特征做进一步的筛选。通过假设检验的方法来得到最优的入模特征组合,能够有效的解决特征之间的多重共线性。

+ 笔者基于toad.selection.stepwise重新实现了StepwiseSelection,参数与toad包中的所有参数一致,方法如下:


```python
class StepwiseSelection(TransformerMixin, BaseEstimator):
    
    def __init__(self, target="target", estimator="ols", direction="both", criterion="aic", max_iter=None, return_drop=True, exclude=None, intercept=True, p_value_enter=0.2, p_remove=0.01, p_enter=0.01, target_rm=False):
        """
        逐步回归筛选方法

        Args:
            target: 数据集中标签名称,默认 target
            estimator: 预估器,默认 ols,可选 "ols", "lr", "lasso", "ridge",通常默认即可
            direction: 逐步回归方向,默认both,可选 "forward", "backward", "both",通常默认即可
            criterion: 评价指标,默认 aic,可选 "aic", "bic",通常默认即可
            max_iter: 最大迭代次数,sklearn中使用的参数,默认为 None
            return_drop: 是否返回特征剔除信息,默认 True
            exclude: 强制保留的某些特征
            intercept: 是否包含截距,默认为 True
            p_value_enter: 特征进入的 p 值,用于前向筛选时决定特征是否进入模型
            p_remove: 特征剔除的 p 值,用于后向剔除时决定特征是否要剔除
            p_enter: 特征 p 值,用于判断双向逐步回归是否剔除或者准入特征
            target_rm: 是否剔除数据集中的标签,默认为 False,即剔除数据集中的标签
        """
```

+ 使用方法

```python
# 常规使用方法
stepwise = StepwiseSelection(target=target)
train = stepwise.fit_transform(train)

# pipeline
feature_pipeline = Pipeline([
        ......
        ("stepwise", StepwiseSelection(target=target, target_rm=False)),
        ......
    ])
feature_pipeline.fit(train)
```

## 逻辑回归建模

逻辑回归基本上都是使用的`sklearn.linear_model`模块里的`LogisticRegression`(偏模型)或者`statsmodels.api`提供的`Logit`(偏统计),两者各有优点。`sklearn`库提供了可以自行调整超参数的逻辑回归模型,`statsmodels`库提供了很完整的模型摘要输出,两者类似鱼与熊掌一样,不可兼得。

笔者参考社区搜寻了很多评分卡建模相关的仓库,最终在`skorecard.liner_model`中`LogisticRegression`的实现上进行优化,增加了一些相关的统计信息输出,实现了类似`statsmodels`的模型输出的效果,同时保留了`sklearn`版`LogisticRegression`可以手工设置超参数或者基于超参数搜索框架搜寻最有超参数组合的特性。同时,也对`statsmodels`库中的`Logit`进行了一定的封装,以满足笔者评分卡建模的`pipeline`组件要求以及更详细的模型摘要信息输出和保存。

+ 方法简介

```python
class ITLubberLogisticRegression(LogisticRegression):
    """
    Extended Logistic Regression.
    Extends [sklearn.linear_model.LogisticRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html).
    This class provides the following extra statistics, calculated on `.fit()` and accessible via `.summary()`:
    - `cov_matrix_`: covariance matrix for the estimated parameters.
    - `std_err_intercept_`: estimated uncertainty for the intercept
    - `std_err_coef_`: estimated uncertainty for the coefficients
    - `z_intercept_`: estimated z-statistic for the intercept
    - `z_coef_`: estimated z-statistic for the coefficients
    - `p_value_intercept_`: estimated p-value for the intercept
    - `p_value_coef_`: estimated p-value for the coefficients
    
    Example:
    ```python
    feature_pipeline = Pipeline([
        ("preprocessing_select", FeatureSelection(target=target, engine="scorecardpy")),
        ("combiner", Combiner(target=target, min_samples=0.2)),
        ("transform", WOETransformer(target=target)),
        ("processing_select", FeatureSelection(target=target, engine="scorecardpy")),
        ("stepwise", StepwiseSelection(target=target)),
        # ("logistic", LogisticClassifier(target=target)),
        ("logistic", ITLubberLogisticRegression(target=target)),
    ])
    
    feature_pipeline.fit(train)
    summary = feature_pipeline.named_steps['logistic'].summary()
    ```
    
    An example output of `.summary()`:
    
    | | Coef. | Std.Err | z | P>|z| | [ 0.025 | 0.975 ] | VIF |
    |:------------------|----------:|----------:|---------:|------------:|-----------:|----------:|--------:|
    | const | -0.844037 | 0.0965117 | -8.74544 | 2.22148e-18 | -1.0332 | -0.654874 | 1.05318 |
    | duration.in.month | 0.847445 | 0.248873 | 3.40513 | 0.000661323 | 0.359654 | 1.33524 | 1.14522 |
    """

    def __init__(self, target="target", penalty="l2", calculate_stats=True, dual=False, tol=0.0001, C=1.0, fit_intercept=True, intercept_scaling=1, class_weight=None, random_state=None, solver="lbfgs", max_iter=100, multi_class="auto", verbose=0, warm_start=False, n_jobs=None, l1_ratio=None,):
        """
        Extends [sklearn.linear_model.LogisticRegression.fit()](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html).
        
        Args:
            target (str): your dataset's target name
            calculate_stats (bool): If true, calculate statistics like standard error during fit, accessible with .summary()
        """
```

+ 使用方法

```python
# 常规使用方法
logistic = ITLubberLogisticRegression(target=target)
logistic.fit(woe_train)

logistic.plot_weights(save="outputs/logistic_train.png")
summary = logistic.summary().reset_index().rename(columns={"index": "Features"})
train_corr = logistic.corr(woe_train, save="outputs/train_corr.png")
train_report = logistic.report(woe_train)

# pipeline
feature_pipeline = Pipeline([
    ......
    ("logistic", ITLubberLogisticRegression(target=target)),
])

feature_pipeline.fit(train)
y_pred_train = feature_pipeline.predict(train.drop(columns=target))
```


### 评分卡转换

笔者重写了`toad.ScoreCard`以适配`pipeline`模式的端到端的评分卡建模,同时支持传入训练好的逻辑回归模型、输出评分排序性、评分分布、评分稳定性等。

+ 相关代码说明

```python
class ScoreCard(toad.ScoreCard, TransformerMixin):
    
    def __init__(self, target="target", pdo=60, rate=2, base_odds=35, base_score=750, combiner={}, transer=None, pretrain_lr=None, pipeline=None, **kwargs):
        """
        评分卡模型转换

        Args:
            target: 数据集中标签名称,默认 target
            pdo: odds 每增加 rate 倍时减少 pdo 分,默认 60
            rate: 倍率
            base_odds: 基础 odds,通常根据业务经验设置的基础比率(违约概率/正常概率),估算方法:(1-样本坏客户占比)/坏客户占比,默认 35,即 35:1 => 0.972 => 坏样本率 2.8%
            base_score: 基础 odds 对应的分数,默认 750
            combiner: 分箱转换器,传入 pipeline 时可以为None
            transer: woe转换器,传入 pipeline 时可以为None
            pretrain_lr: 预训练好的逻辑回归模型,可以不传
            pipeline: 训练好的 pipeline,必须包含 Combiner 和 WOETransformer
            **kwargs: 其他相关参数,具体参考 toad.ScoreCard
        """
```

+ 使用方法

```python
# 使用方法
feature_pipeline = Pipeline([
        ("preprocessing_select", FeatureSelection(target=target, engine="scorecardpy")),
        ("combiner", Combiner(target=target, min_samples=0.2)),
        ("transformer", WOETransformer(target=target)),
        ("processing_select", FeatureSelection(target=target, engine="scorecardpy")),
        ("stepwise", StepwiseSelection(target=target, target_rm=False)),
    ])
woe_train = feature_pipeline.fit_transform(train)

combiner = feature_pipeline.named_steps['combiner'].combiner
transformer = feature_pipeline.named_steps['transformer'].transformer

score_card = ScoreCard(target=target, combiner=combiner, transer=transformer, )
score_card.fit(woe_train)

data["score"] = score_card.transform(data)

# 评分效果
clip = 50
clip_start = max(math.ceil(train["score"].min() / clip) * clip, math.ceil(train["score"].quantile(0.01) / clip) * clip)
clip_end = min(math.ceil(train["score"].max() / clip) * clip, math.ceil(train["score"].quantile(0.99) / clip) * clip)
score_clip = [i for i in range(clip_start, clip_end, clip)]

train_score_rank = feature_bin_stats(train, "score", target=target, rules=score_clip, verbose=0, method="step", ks=True)
ks_plot(train["score"], train[target], title="Train Dataset", save="model_report/train_ksplot.png")
score_hist(train["score"], train[target], save="model_report/train_scorehist.png", bins=30, figsize=(13, 10))
```


### 超参数优化

笔者根据`sklearn.pipeline`的构造规则,重写了评分卡建模相关模块,并在此基础上加入了部分笔者认为可能会有用的方法。通过对相关模块重写后,可以支持评分卡端到端`pipeline`式的超参数搜索方案。

+ 使用方法

```python
feature_pipeline = Pipeline([
    ("preprocessing_select", FeatureSelection(target=target, engine="scorecardpy")),
    ("combiner", Combiner(target=target, min_samples=0.2)),
    ("transformer", WOETransformer(target=target)),
    ("processing_select", FeatureSelection(target=target, engine="scorecardpy")),
    ("stepwise", StepwiseSelection(target=target, target_rm=False)),
    # ("logistic", StatsLogisticRegression(target=target)),
    ("logistic", ITLubberLogisticRegression(target=target)),
])

# pipeline 超参数搜索参数:{pipeline_step_name}__{param}
params_grid = {
    "logistic__C": [i / 1. for i in range(1, 10, 2)],
    "logistic__penalty": ["l2"],
    "logistic__class_weight": [None, "balanced"], # + [{1: i / 10.0, 0: 1 - i / 10.0} for i in range(1, 10)],
    "logistic__max_iter": [100],
    "logistic__solver": ["sag"] # ["liblinear", "sag", "lbfgs", "newton-cg"],
    "logistic__intercept": [True, False],
}

clf = GridSearchCV(feature_pipeline, params_grid, cv=5, scoring='roc_auc', verbose=-1, n_jobs=2, return_train_score=True)
clf.fit(train, train[target])

y_pred_train = clf.best_estimator_.predict(train)
print(clf.best_params_)
```

## 模型报告

### 报告说明

输出评分卡模型报告的意义在于能够对其他人呈现评分卡模型的好坏以及相关的模型信息。

过去在评分卡建模过程中必须将各种表格和图片汇总到某个文件夹内或者几个文件中,格式每次都需要进行调整和优化,很耽误时间,如果中途有改动的话有需要重新再造一遍轮子。笔者基于日常评分卡建模工作中的需求,通过`openpyxl`实现了向`excel`文件中写入制定样式的内容(文本、图片、数据表、条件格式、设定宽度、字体对齐等),并在此基础上将评分卡建模过程中需要输出的内容以设定的格式全部保存至`excel`文件中,以减少后续建模过程中的重复性工作。

+ 相关代码说明

```python
class ExcelWriter:

    def __init__(self, style_excel='报告输出模版.xlsx', style_sheet_name="初始化", fontsize=10, font='楷体', theme_color='2639E9'):
        """
        excel 文件内容写入公共方法
        
        :param style_excel: 样式模版文件,默认当前路径下的 报告输出模版.xlsx ,如果项目路径调整需要进行相应的调整
        :param style_sheet_name: 模版文件内初始样式sheet名称,默认即可
        :param fontsize: 插入excel文件中内容的字体大小,默认 10
        :param font: 插入excel文件中内容的字体,默认 楷体
        :param theme_color: 主题色,默认 2639E9,注意不包含 #
        """

    def add_conditional_formatting(self, worksheet, start_space, end_space):
        """
        设置条件格式

        :param worksheet: 当前选择设置条件格式的sheet
        :param start_space: 开始单元格位置
        :param end_space: 结束单元格位置
        """

    @staticmethod
    def set_column_width(worksheet, column, width):
        """
        调整excel列宽

        :param worksheet: 当前选择调整列宽的sheet
        :param column: 列,可以直接输入 index 或者 字母
        :param width: 设置列的宽度
        """

    @staticmethod
    def set_number_format(worksheet, space, _format):
        """
        设置数值显示格式

        :param worksheet: 当前选择调整数值显示格式的sheet
        :param space: 单元格范围
        :param _format: 显示格式,参考 openpyxl
        """

    def get_sheet_by_name(self, name):
        """
        获取sheet名称为name的工作簿,如果不存在,则从初始模版文件中拷贝一个名称为name的sheet
        
        :param name: 需要获取的工作簿名称
        """

    def insert_value2sheet(self, worksheet, insert_space, value="", style="content", auto_width=False):
        """
        向sheet中的某个单元格插入某种样式的内容

        :param worksheet: 需要插入内容的sheet
        :param insert_space: 内容插入的单元格位置,可以是 "B2" 或者 (2, 2) 任意一种形式
        :param value: 需要插入的内容
        :param style: 渲染的样式,参考 init_style 中初始设置的样式
        :param auto_width: 是否开启自动调整列宽
        """

    def insert_pic2sheet(self, worksheet, fig, insert_space, figsize=(600, 250)):
        """
        向excel中插入图片内容
        
        :param worksheet: 需要插入内容的sheet
        :param fig: 需要插入的图片路径
        :param insert_space: 插入图片的起始单元格
        :param figsize: 图片大小设置
        """

    def insert_df2sheet(self, worksheet, data, insert_space, merge_column=None, header=True, index=False, auto_width=False):
        """
        向excel文件中插入制定样式的dataframe数据

        :param worksheet: 需要插入内容的sheet
        :param data: 需要插入的dataframe
        :param insert_space: 插入内容的起始单元格位置
        :param merge_column: 需要分组显示的列,index或者列明
        :param header: 是否存储dataframe的header,暂不支持多级表头
        :param index: 是否存储dataframe的index
        :param auto_width: 是否自动调整列宽
        :return 返回插入元素最后一列之后、最后一行之后的位置
        """

    def save(self, filename):
        """
        保存excel文件
        
        :param filename: 需要保存 excel 文件的路径
        """
```

+ 使用方法

```python
writer = ExcelWriter(style_excel="./utils/报告输出模版.xlsx", theme_color="2639E9")

# 评分卡刻度
scorecard_kedu = pd.DataFrame(
    [
        ["base_odds", card.base_odds, "根据业务经验设置的基础比率(违约概率/正常概率),估算方法:(1-样本坏客户占比)/坏客户占比"],
        ["base_score", card.base_score, "基础ODDS对应的分数"],
        ["rate", card.rate, "设置分数的倍率"],
        ["pdo", card.pdo, "表示分数增长PDO时,ODDS值增长到RATE倍"],
        ["B", card.offset, "补偿值,计算方式:pdo / ln(rate)"],
        ["A", card.factor, "刻度,计算方式:base_score - B * ln(base_odds)"],
    ],
    columns=["刻度项", "刻度值", "备注"],
)

worksheet = writer.get_sheet_by_name("评分卡结果")
start_row, start_col = 2, 2
end_row, end_col = writer.insert_value2sheet(worksheet, (start_row, start_col), value="评分卡刻度", style="header")
end_row, end_col = writer.insert_df2sheet(worksheet, scorecard_kedu, (end_row + 1, start_col))

end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="训练数据集评分模型效果", style="header")
ks_row = end_row
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/train_ksplot.png", (ks_row, start_col))
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/train_scorehist.png", (ks_row, end_col))
end_row, end_col = writer.insert_df2sheet(worksheet, train_score_rank, (end_row + 1, start_col))

for c in ["坏样本率", "LIFT值", "分档KS值"]:
    conditional_column = get_column_letter(start_col + train_score_rank.columns.get_loc(c))
    writer.add_conditional_formatting(worksheet, f'{conditional_column}{end_row - len(train_score_rank)}', f'{conditional_column}{end_row}')
```

### 汇总信息

汇总信息包含评分卡模型相关的背景说明、取样说明、数据集划分方式以及不同时点数据分布情况。

### LR拟合结果

逻辑回归拟合结果主要包含了`LR`模型的拟合情况、稳定性、模型`AUC`、`KS`等指标。

### 入模特征信息

入模特征信息包含了入模特征的数据字典、分布情况、相关性、分箱信息以及分箱图等信息,主要为了展示评分卡入模变量相关的信息。

### 评分卡结果

评分卡模型结果主要包含评分卡参数信息、变量分箱及对应分数、评分`AUC`和`KS`指标、评分分布情况、评分排序性、各个评分区间相关指标信息。

### 补充说明

后续准备有闲暇时加上`shap`、`bad case`等相关的内容,并将支持`lightgbm`、`catboost`、`xgboost`等模型的报告输出。


## 评分卡PMML转换

### 背景说明

关于评分卡上线部署方案,目前现有的大致有四种

+ 提需求给公司开发部门上线部署
+ 将评分卡模型保存为`pickle`文件提供开发部署
+ 直接使用`python`提供`api服务`部署或者定时更新
+ 评分卡转`PMML`文件部署

前几种相对简单,提供`API服务`可能有一定技术门槛,但基本上网上有现成的方案,但评分卡转换`PMML`文件部署有一定难度,网上能够参考的实现方案比较难找,本章就笔者提供的评分卡转`PMML`相关的实现进行说明。

+ 相关代码说明

```python

class ScoreCard(toad.ScoreCard, TransformerMixin):
    
    ......

    def scorecard2pmml(self, pmml: str = 'scorecard.pmml', debug: bool = False):
        """export a scorecard to pmml

        Args:
            pmml (str): io to write pmml file.
            debug (bool): If true, print information about the conversion process.
        """
```

+ 使用说明

```python
# 保存 PMML 评分卡模型
card.scorecard2pmml(pmml='scorecard.pmml', debug=True)

# 模型验证
from pypmml import Model

model = Model.fromFile("scorecard.pmml")
data["score"] = model.predict(data[model.inputNames]).values
```


## 参考资料

> https://github.com/itlubber/LogisticRegressionPipeline
>
> https://github.com/itlubber/itlubber-excel-writer
>
> https://github.com/itlubber/openpyxl-excel-style-template
>
> https://github.com/itlubber/scorecard2pmml
>
> https://pypi.org/project/pdtr/
>
> https://github.com/amphibian-dev/toad
>
> https://github.com/ShichenXie/scorecardpy
>
> https://github.com/ing-bank/skorecard
>
> http://gnpalencia.org/optbinning
>
> https://github.com/yanghaitian1/risk_control


================================================
FILE: docs/Makefile
================================================
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS    ?=
SPHINXBUILD   ?= sphinx-build
SOURCEDIR     = source
BUILDDIR      = build

# Put it first so that "make" without argument is like "make help".
help:
	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)


================================================
FILE: docs/make.bat
================================================
@ECHO OFF

pushd %~dp0

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
	set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build

if "%1" == "" goto help

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
	echo.
	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
	echo.installed, then set the SPHINXBUILD environment variable to point
	echo.to the full path of the 'sphinx-build' executable. Alternatively you
	echo.may add the Sphinx directory to PATH.
	echo.
	echo.If you don't have Sphinx installed, grab it from
	echo.http://sphinx-doc.org/
	exit /b 1
)

%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end

:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%

:end
popd


================================================
FILE: docs/requirements.txt
================================================
sphinx>=6,<8
myst-parser>=2.0.0,<2.1
pygments>=2.15.1,<2.16
sphinx-intl>=2.0,<2.2
sphinx-design>=0.4,<0.5
sphinx_copybutton>=0.5.0,<1.0.0
sphinx-nefertiti>=0.1.11
recommonmark
sphinx-markdown-tables
jieba3k


================================================
FILE: docs/source/CNAME
================================================
scorecardpipeline.itlubber.art


================================================
FILE: docs/source/conf.py
================================================
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
import sphinx_nefertiti


on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if on_rtd:
    html_theme_path = [sphinx_nefertiti.get_html_theme_path()]
else:
    sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))


# -- Project information -----------------------------------------------------

project = 'scorecardpipeline'
copyright = '2023, itlubber'
author = 'itlubber'

# The full version, including alpha/beta/rc tags
import sys
sys.path.append("../../scorecardpipeline")

import scorecardpipeline

release = scorecardpipeline.__version__


# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.viewcode',
    'sphinx.ext.githubpages',
    'myst_parser',
    'sphinx_design',
    'sphinx_copybutton',
    'sphinx_markdown_tables',
]

myst_enable_extensions = [
    'amsmath',
    'attrs_block',
    'colon_fence',
    'deflist',
    'dollarmath',
    'fieldlist',
    'tasklist',
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'zh_CN'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
# html_theme = "sphinx_rtd_theme"

html_theme = 'sphinx_nefertiti'
pygments_style = "sas"
pygments_dark_style = "dracula"

html_theme_options = {
    "style": "indigo",
    "footer_links": ",".join([
        "itlubber|https://itlubber.art",
        "github|https://github.com/itlubber/scorecardpipeline",
        "examples|https://github.com/itlubber/scorecardpipeline/blob/main/examples/scorecard_samples.ipynb",
    ]),
    "show_powered_by": False,
    "project_name_font": "Nunito",
    "doc_headers_font": "Nunito",
    "documentation_font": "Nunito",
    "versions": [
        (f"v{release}", "https://itlubber.github.io/scorecardpipeline-docs"),
    ],
    "repository_url": "https://github.com/itlubber/scorecardpipeline",
    "repository_name": "itlubber/scorecardpipeline",
    "current_version": f"v{release}",
    "show_colorset_choices": False,
}

htmlhelp_basename = "scorecardpipeline"
html_last_updated_fmt = "%Y-%m-%d %H:%M:%S"
html_permalinks = False

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_extra_path = ["CNAME"]
suppress_warnings = []
html_show_search_summary = True

# 显示类的 __init__ 相关文档
autodoc_default_options = {
    "members": True,
    "member-order": "bysource",
    "special-members": "__init__",
    "undoc-members": True,
    "exclude-members": "__weakref__,set_fit_request,set_transform_request,set_decision_function_request,set_score_request,set_predict_request,Pipeline,FeatureUnion,RuleState,RuleStateError,RuleUnAppliedError,make_pipeline,make_union,RFE,RFECV,SelectKBest,SelectFromModel,GenericUnivariateSelect",
}

# # 支持文档格式
# source_suffix = {
#     '.rst': 'restructuredtext',
#     '.md': 'markdown',
# }


================================================
FILE: docs/source/index.rst
================================================
.. scorecardpipeline documentation master file, created by
   sphinx-quickstart on Fri Nov 10 13:40:57 2023.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.

scorecardpipeline
=============================================

`scorecardpipeline` 封装了 `toad`、`scorecardpy`、`optbinning` 等评分卡建模相关组件,`API` 风格与 `sklearn` 高度一致,支持 `pipeline` 式端到端评分卡建模、模型报告输出、导出 `PMML` 文件、超参数搜索等功能


快速开始
---------------------------------------------

.. raw:: html
   :file: install.html


.. toctree::
   :maxdepth: 2

   quickstart


接口文档
---------------------------------------------

.. toctree::
   :maxdepth: 3

   scorecardpipeline


快速查找
---------------------------------------------

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`


================================================
FILE: docs/source/install.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>scorecardpipeline环境安装</title>
  <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.7.0.min.js"></script>
</head>
<body>
  <style>
    .quick-start {
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      margin-bottom: 20px;
    }
  
    .title-column {
      flex-grow: 0;
      margin-right: 10px;
    }
  
    .content-column {
      flex-grow: 1;
    }
  
    .row {
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      width: fit-content;
    }
  
    .title-column div, .row div {
      white-space: nowrap;
    }
  
    .title-column div {
      padding: 14px 10px 12px 0;
      font-weight: 700;
    }
  
    .row div {
      flex-grow: 1;
      text-align: center;
      margin: 2px;
      padding: 12px 0 10px 0;
      background: rgba(102, 54, 234,0.1);
      cursor: pointer;
    }
    
    .row div.selected {
      background: rgba(102, 54, 234,0.9);
      color: #ffffff;
    }
  
    #command {
      margin: 2px 0px 1rem -11px;
      padding: 17px 3px 14px 16px;
      background: rgba(102, 54, 234,0.1);
      box-shadow: 0 8px 32px 0 rgba(102, 54, 234,0.1);
      backdrop-filter: blur( 20px );
      -webkit-backdrop-filter: blur( 20px );
      border-radius: 0;
      border: 1px solid rgba( 255, 255, 255, 0.18 );
      width: 99.6%;
    }
  
    #command pre {
      background: none;
      border: none;
      white-space: pre-wrap; 
      word-wrap: break-word; 
      padding: 0;
      margin: 0;
    }
  
  </style>
  
  <div class="quick-start">
    <div class="title-column">
        <div>系统环境</div>
        <div>PIPY镜像</div>
        <div>网络环境</div>
        <div>安装命令</div>
    </div>
    <div class="content-column">
      <div class="row" id="os" style="width: 100%;"></div>
      <div class="row" id="pipy" style="width: 100%;"></div>
      <div class="row" id="line" style="width: 100%;"></div>
      <div class="row" id="command"><pre style="width: 100%;"></pre></div>
    </div>
  </div>
  
  <script>
    var osList = [
      ['linux', 'Linux'],
      ['mac', 'Mac'],
      ['windows', 'Windows'],
    ];
  
    var pipyList = [
      ['github', '源码'],
      ['aliyun', '阿里'],
      ['nouse', '纯净'],
      ['office', 'PIPY'],
    ];

    var netList = [
      ['online', '在线'],
      ['offline', '离线'],
    ];
  
    osList.forEach(x => $("#os").append(`<div id="${x[0]}" style="width: ${(100-2*(osList.length-1))/osList.length}%;">${x[1]}</div>`));
    pipyList.forEach(x => $("#pipy").append(`<div id="${x[0]}" style="width: ${(100-2*(pipyList.length-1))/pipyList.length}%;">${x[1]}</div>`));
    netList.forEach(x => $("#line").append(`<div id="${x[0]}" style="width: ${(100-2*(netList.length-1))/netList.length}%;">${x[1]}</div>`));
  
    function updateCommand() {
      var os = $("#command").attr("os");
      var pipy = $("#command").attr("pipy");
      var line = $("#command").attr("line");

      if (pipy == "office") {
        if (line == "offline") {
          $("#command pre").text('# 在线环境中运行\npip download -d site-packages/ scorecardpipeline -i https://pypi.org/simple\n# 离线环境运行\npip install --no-index --find-links=site-packages scorecardpipeline');
        }
        else {
          $("#command pre").text(`pip install scorecardpipeline -i https://pypi.org/simple`);
        }
      }
      else if (pipy == "aliyun") {
        if (line == "offline") {
          $("#command pre").text('# 在线环境中运行\npip download -d site-packages/ scorecardpipeline -i https://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com\n# 离线环境运行\npip install --no-index --find-links=site-packages scorecardpipeline');
        }
        else {
          $("#command pre").text(`pip install scorecardpipeline -i https://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com`);
        }
      }
      // else if (pipy == "tsinghua") {
      //   if (line == "offline") {
      //     $("#command pre").text('# 在线环境中运行\npip download -d site-packages/ scorecardpipeline -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn\n# 离线环境运行\npip install --no-index --find-links=site-packages scorecardpipeline');
      //   }
      //   else {
      //     $("#command pre").text(`pip install scorecardpipeline -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn`);
      //   }
      // }
      // else if (pipy == "douban") {
      //   if (line == "offline") {
      //     $("#command pre").text('# 在线环境中运行\npip download -d site-packages/ scorecardpipeline -i https://pypi.doubanio.com/simple --trusted-host pypi.doubanio.com\n# 离线环境运行\npip install --no-index --find-links=site-packages scorecardpipeline');
      //   }
      //   else {
      //     $("#command pre").text(`pip install scorecardpipeline -i https://pypi.doubanio.com/simple --trusted-host pypi.doubanio.com`);
      //   }
      // }
      else if (pipy == "github") {
        $("#command pre").text(`pip install git+https://github.com/itlubber/scorecardpipeline.git`);
      }
      else {
        if (line == "offline") {
          $("#command pre").text('# 在线环境中运行\npip download -d site-packages/ scorecardpipeline\n# 离线环境运行\npip install --no-index --find-links=site-packages scorecardpipeline');
        }
        else {
          $("#command pre").text(`pip install scorecardpipeline`);
        }
      }
    }
  
    $(".quick-start .content-column .row div").click(function() {
      $(this).parent().children().removeClass("selected");
      $(this).addClass("selected");
      $("#command").attr($(this).parent().attr("id"), $(this).attr("id"));
      updateCommand();
    });
  
    $("#linux").click();
    $("#nouse").click();
    $("#online").click();
  
  </script>
</body>
</html>

================================================
FILE: docs/source/quickstart.md
================================================
## 简介

`scorecardpipeline` 封装了 `toad`、`scorecardpy`、`optbinning` 等评分卡建模相关组件,`API` 风格与 `sklearn` 高度一致,支持 `pipeline` 式端到端评分卡建模、模型报告输出、导出 `PMML` 文件、超参数搜索等功能。

> 在线文档: [`https://scorecardpipeline.itlubber.art/`](https://scorecardpipeline.itlubber.art//)
> 
> `PIPY` 包: [`https://pypi.org/project/scorecardpipeline`](https://pypi.org/project/scorecardpipeline/)
>
> 仓库地址: [`https://github.com/itlubber/scorecardpipeline`](https://github.com/itlubber/scorecardpipeline)


## 版本说明

| **序号** | **版本号** |  **发布日期**  |                                       **版本说明**                                        |
|:------:|:-------:|:----------:|:-------------------------------------------------------------------------------------:|
|   01   |  0.1.0  | 2023.05.04 |                                          初始化                                          |
|   ..   | ......  |   ......   |                                        ......                                         |
|   26   | 0.1.26  | 2023.11.13 |         稳定版本,新增方法[`说明文档`](https://itlubber.github.io/scorecardpipeline-docs/)         |
|   27   | 0.1.27  | 2023.12.09 |                               修复分类变量空值导致转评分卡异常报错及其他相关问题                               |
|   28   | 0.1.28  | 2023.12.15 |                      修复sklearn版本问题、新增自动EDA方法 `auto_eda_sweetviz`                      |
|   29   | 0.1.29  | 2023.12.29 | 新增三方数据测试报告输出方法 `auto_data_testing_report` 及示例文件 `examples/model_report/三方数据测试报告.xlsx` |
|   30   | 0.1.30  | 2024.01.29 |                                                                                       |
|   31   | 0.1.31  | 2024.02.28 |                新增 `rule.Rule` 规则集相关内容、修复 `processing.Combiner.load` 方法                |
|   32   | 0.1.32  | 2024.02.29 |         合并 `pdtr` 内容,新增 `rule_extraction.DecisionTreeRuleExtractor` 决策树策略挖掘方法         |
|   33   | 0.1.33  | 2024.03.07 |                `excel_writer.ExcelWriter` 支持多层级索引存储、多层级表头存储、多列合并单元格等功能                |
|   34   | 0.1.34  |    预发布     |                                   修复异常、部分新功能上线 QaQ                                    |


## 环境安装

1. 安装 `python` 环境

推荐 `python 3.8.13` ,参照网上教程自行安装即可,也可直接使用系统提供的 `python3` , 版本不易过高, 最低支持 `python 3.6`,最高支持 `python 3.10`

2. 安装 `scorecardpipeline` 包

+ 在线环境安装

直接通过 `pip` 安装即可,安装后需要确认下版本是否安装正确,推荐 `-i https://test.pypi.org/simple` 指定源安装

```shell
pip install scorecardpipeline
```

+ 离线环境安装

离线环境安装需先找一个 `python` 版本和系统 (`windows`、`linux`、`mac`) 与生产一致的有网机器或者容器,在有网的环境中下载相关依赖项后拷贝到离线环境安装

```shell
# 在线环境依赖下载
pip download -d site-packages/ scorecardpipeline
# 离线环境
pip install --no-index --find-links=site-packages scorecardpipeline
```

3. 安装 `jdk 1.8+` [可选]

如果有将训练完成的评分卡模型直接导出 `PMML` 模型文件部署的需求,需要单独安装 `java` 环境

> `mac` & `windows` 参考: [`https://developer.aliyun.com/article/1082599`](https://developer.aliyun.com/article/1082599)
>
> `linux` 参考: [`https://blog.csdn.net/muzi_gao/article/details/132169159`](https://blog.csdn.net/muzi_gao/article/details/132169159)


## 使用示例

本节将就 `scorecardpipeline` 的相关功能做简要介绍,并提供一个[`简单的示例`](https://github.com/itlubber/scorecardpipeline/blob/main/examples/scorecard_samples.ipynb),旨在让读者对如何使用 `scorecardpipeline` 进行评分卡模型构建有一个简单的印象。


### 导入依赖

```python
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from scorecardpipeline import *

# 固定随机种子以保证结果可复现
init_setting(seed=10)
```

### 数据准备

使用 `scorecardpipeline` 进行评分卡建模,需要您提前准备好一个包含目标变量的数据集,`scorecardpipeline` 提供了一个德国信贷数据集 [`germancredit`](https://archive.ics.uci.edu/dataset/144/statlog+german+credit+data),示例加载该数据集进行演示,读者可以直接替换为您本地的数据集,并修改目标变量名称为数据集中的目标变量列名

[`germancredit`](https://archive.ics.uci.edu/dataset/144/statlog+german+credit+data) 数据集中包含类别型变量、数值型变量、好坏标签,共1000条数据,由于数据集中不包含缺失值,为了模拟实际生产中的真实数据,笔者将在固定随机种子的情况下随机替换数据集中的部分内容为 `np.nan`
<!-- 
<details>

  <summary>数据字典</summary> -->

 **序号** | **类型** | **特征名**                                                  | **释义**          
:------:|:------:|:--------------------------------------------------------:|:---------------:
 0      | 类别型    | creditability                                            | 客户好坏标签          
 1      | 数值型    | duration in month                                        | 月持续时间           
 2      | 数值型    | credit amount                                            | 信用额度            
 3      | 数值型    | installment rate in percentage of disposable income      | 分期付款率占可支配收入的百分比 
 4      | 数值型    | present residence since                                  | 现居住地至今          
 5      | 数值型    | age in years                                             | 年龄              
 6      | 数值型    | number of existing credits at this bank                  | 这家银行的现有信贷数量     
 7      | 数值型    | number of people being liable to provide maintenance for | 有责任为其提供维修服务的人数  
 8      | 类别型    | status of existing checking account                      | 现有支票账户的状态       
 9      | 类别型    | credit history                                           | 信用记录            
 10     | 类别型    | purpose                                                  | 目的              
 11     | 类别型    | savings account or bonds                                 | 储蓄账户/债券         
 12     | 类别型    | present employment since                                 | 至今工作至今          
 13     | 类别型    | personal status and sex                                  | 个人地位和性别         
 14     | 类别型    | other debtors or guarantors                              | 其他债务人/担保人       
 15     | 类别型    | property                                                 | 财产              
 16     | 类别型    | other installment plans                                  | 其他分期付款计划        
 17     | 类别型    | housing                                                  | 住房情况         
 18     | 类别型    | job                                                      | 工作              
 19     | 类别型    | telephone                                                | 电话              
 20     | 类别型    | foreign worker                                           | 外籍工人            

<!-- </details> -->

<br>

```python
# 加载数据集
data = germancredit()

# 设置目标变量名称 & 映射目标变量值域为 {0, 1}
target = "creditability"
data[target] = data[target].map({"good": 0, "bad": 1})

# 随机替换 20% 的数据为 np.nan
for col in data.columns.drop(target):
    for i in range(len(data)):
        if np.random.rand() > 0.8:
            data[col].loc[i] = np.nan
```


### 数据拆分

通常建模过程中会将数据集拆分成训练数据集和测试数据集集,训练数据集用于模型参数的训练,测试数据集用于评估模型好坏,且在整个建模过程中实际上只能使用一次(任何针对测试数据集调模型的,都存在模型过拟合的情况,除非你的数据不包含任何噪声数据)。而为了弥补测试数据集只能用一次用于评估模型好坏的尴尬,大多都会再拆分一个验证数据集,用于模型训练过程中判别模型是否向好的方向进行优化。

在金融建模场景中,上述三个数据集通常被称为训练数据集、测试数据集(即上述三个数据集中的验证集)、跨时间验证集(即上述三个数据集中的测试集)。同时,在金融场景中,跨时间验证集的拆分方式通常使用类似时间序列数据集的拆分方法,按照时间将数据集拆分为建模集(包含训练集和测试集)、跨时间验证集(`Out of Time,OOT`),以保证模型在不同时间段内的泛化能力。

简单起见,示例只拆分训练集、测试集,OOT直接拷贝训练集来演示评分卡建模完整过程。

```python
train, test = train_test_split(data, test_size=0.3, shuffle=True, stratify=data[target])

oot = train.copy()
```


### 数据预处理

在评分卡建模标准流程中,会先针对每个变量分箱后转WOE,再筛选特征训练逻辑回归模型,训练完后转评分卡,当数据集的特征个数比较多时,可以再分箱之前先进行特征初筛,以降低分箱时的计算耗时。评分卡建模完整流程可以参照如下流程:

<div style="display: flex; justify-content: space-around;">
    <img src="https://itlubber.art/upload/scorecard-modeling-process.png" style="width: 70%; "/>
</div>


#### 特征筛选

特征筛选主要从几个方面来评估特征是否有效:

1. 单一值占比
2. 缺失值占比
3. 特征相关性
4. 特征 `IV(information value)` 值
5. 特征稳定性 `PSI(population stability index)`
6. 特征重要性
7. 方差膨胀因子 `VIF(variance inflation factor)`


`scorecardpipeline` 提供了几种常见的特征筛选方法(`1`、`2`、`3`、`4`),可以通过实例化的方式进行配置,并在训练数据集中训练完成后应用到不同数据集中。

```python
# 初始化筛选器
select = FeatureSelection(target=target, engine="toad", identical=0.95, empty=0.95, iv=0.02, corr=0.6)
# 训练数据上训练
select.fit(train)
# 应用到不同数据集
train_select = select.transform(train)
test_select = select.transform(test)
```

在 `scorecardpipeline` 中,所有筛选器都包含 `dropped` 属性,用来存放特征被剔除的原因。

```python
# 输出特征筛选信息
select.dropped
```

|    | variable                                                 | rm_reason   |
|:---:|:---------------------------------------------------------:|:------------:|
|  0 | number_of_existing_credits_at_this_bank                  | iv          |
|  1 | job                                                      | iv          |
|  2 | number_of_people_being_liable_to_provide_maintenance_for | iv          |
|  3 | telephone                                                | iv          |
|  4 | duration_in_month                                        | corr        |


#### 特征分箱


特征分箱能够提升模型的鲁棒性,特征分箱时通常对每个箱内的样本个数或占比有要求,数据中的异常值和极端值会被分到某个箱中,在转化为 `WOE` 之后能够很大程度上避免异常值或极端值对模型训练时造成偏差。同时,特征分箱转 `WOE` 之后,所有特征的值域都被缩放到了某个固定尺度下,逻辑回归模型的系数在一定程度上能够代表特征的重要程度。

+ 在 `scorecardpipeline` 中,集成了 `toad` 和 `optbinning` 两个库中提供的分箱方法,后续会陆续增加更多的分箱方法,相关内容参考: [`scorecardpipeline.Combiner`](/scorecardpipeline.html#scorecardpipeline.Combiner)


```python
# 初始化分箱器
combiner = Combiner(target=target, min_bin_size=0.2)
# 训练
combiner.fit(train_select)
# 对数据集进行分箱
train_bins = combiner.transform(train_select)
test_bins = combiner.transform(test_select)
```

+ 为了方便调整分箱,[`scorecardpipeline.Combiner`]() 提供了输出分箱图和分箱统计信息的功能,方便快速手工调整分箱。

```python
# 查看 credit_amount 信用额度 的分箱信息,并显示分箱统计信息
combiner.bin_plot(train_select, "credit_amount", result=True, desc="信用额度")
```

<div style="display: flex; justify-content: space-around;">
    <img src="https://itlubber.art/upload/sp_credit_amount.png" />
</div>


+ 当特征分箱不符合业务逻辑或者单调性不满足时,可以通过自定义规则的方式查看分箱效果。(此时不会对 `Combiner` 中的规则进行更新)

```python
# 通过 rule 字段传入自定义规则,实时查看分箱效果
combiner.bin_plot(train_select, "credit_amount", result=True, desc="信用额度", rule=[4000.0, np.nan])
```

<div style="display: flex; justify-content: space-around;">
    <img src="https://itlubber.art/upload/sp_credit_amount_rule.png" />
</div>


+ 当特征分箱调整到符合业务预期或者单调满足建模需求时,可以通过 `update` 方法更新规则 `Combiner` 中的规则。

```python
# 更新 credit_amount 的分箱规则
combiner.update({"credit_amount": [4000.0, np.nan]})
# 打印分箱规则
combiner["credit_amount"] # array([4000.,   nan])
```


#### `WOE` 转换

特征分箱后,只是将特征离散化为几箱,每一箱的值被赋予了一个标签,例如0、1、2、3、...,尽管这些标签在一定程度上也能够表征客户的风险水平,能够作为训练数据训练出一个模型,但在评估客户风险状况时很难精准刻画客户的风险状况。

通常使用 `WOE(Weight of Evidence)编码` 对分箱后的特征进行编码。`WOE编码` 将每个分箱内的坏样本占比除以好样本占比后取对数来编码,能够反映每个分箱内客户的坏客户分布与好客户分布之间的差异以及该箱内坏好比与总体的坏好比之间的差异性。


特征 `WOE(Weight of Evidence)编码` 后,有如下好处:

+ 能够将每个特征的值域放缩到同样的尺度下
+ `woe` 中包含了样本好坏信息,比原始值和分箱序号更能反应客户违约概率大小
+ `woe` 的计算公式 $woe = ln(\frac{bad_i}{bad}/\frac{good_i}{good} )$ 中引入了非线性,一定程度能够增强逻辑回归拟合非线性模型的能力


```python
# 初始化 WOE 转换器
transform = WOETransformer(target=target)
# 训练
transform.fit(train_bins)
# 转换分箱为WOE
train_woe = transform.transform(train_bins)
test_woe = transform.transform(test_bins)
```

`WOE(Weight of Evidence)编码器` 提供了方法允许使用者通过如下方式查看每个分箱对应的 `woe` 值。

<div style="display: flex; justify-content: space-around;">
    <img src="https://itlubber.art/upload/sp_woetransformer.png" />
</div>

在特征从原始值编码为 `woe` 的过程中,特征的值从连续值或者分类值映射到了有限的几个分箱对应的 `woe` 值,同时值域也统一到了一个基于样本好坏分布情况为基准的空间中,会造成特征之间的相关性增大、单一值占比上升、特征 `iv` 值较前筛时小幅度下降,所以推荐在 `WOE编码` 后,再进行一次特征精筛,以保证后续模型结果的稳定性和整体可解释性。


#### 逐步回归特征筛选

在正式训练 `逻辑回归(logistic regression,LR)模型` 前,通常会再使用逐步回归来剔除特征,能够一定程度上解决特征之间的多重共线性,以及在剔除尽可能多的特征的同时保证 `LR模型` 效果的有效性。

```python
# 初始化逐步回归特征筛选器
stepwise = StepwiseSelection(target=target)
# 训练
stepwise.fit(train_woe)
# 应用逐步回归特征筛选器
train_woe_stepwise = stepwise.transform(train_woe)
test_woe_stepwise = stepwise.transform(test_woe)
```

与 `FeatureSelection` 类似,`scorecardpipeline` 中所有特征筛选器都包含了 `dropped` 属性,可以直接输出每个特征被剔除的原因。

```python
# 逐步回归特征筛选明细信息
stepwise.dropped
```

|    | variable                    | rm_reason   |
|:--:|:---------------------------:|:-----------:|
|  0 | present_residence_since     | stepwise    |
|  1 | savings_account_and_bonds   | stepwise    |
|  2 | other_debtors_or_guarantors | stepwise    |
|  3 | property                    | stepwise    |
|  4 | other_installment_plans     | stepwise    |
|  5 | foreign_worker              | stepwise    |


### 训练 `LR` 模型

对数据集预处理完成后,数据集中的特征的值域被映射到了一个与样本好坏息息相关的空间中,特征值的大小能够一定程度上反映出客户违约概率的高低,通过 `WOE编码` 方法对原始特征进行转换时引入的非线性能够在一定程度上弥补建模时使用简单模型拟合能力不足的问题,同时,数据集经过特征筛选后,冗余信息被尽可能多的剔除,多重共线性也在一定程度上得到解决,训练模型时能够在保证模型效果的前提下提升模型的鲁棒能力。

在 `scorecardpipeline` 中,提供了基于 `sklearn` 重新实现的 `ITLubberLogisticRegression` 逻辑回归模型,能够满足丰富的超参数设置和丰富的统计信息输出。

当然,假如您想直接使用 `sklearn` 提供的 `LogisticRegression` 去完成评分卡模型构建和结果输出,也能够支持,但转换评分卡后,有部分扩展功能会受到限制,但不会影响评分卡模型转换、评分预测以及持久化存储的相关功能。

```python
# 逻辑回归模型构建
logistic = ITLubberLogisticRegression(target=target)
# 训练
logistic.fit(train_woe_stepwise)
# 预测数据集样本违约概率
y_pred_train = logistic.predict_proba(train_woe_stepwise.drop(columns=target))[:, 1]
y_pred_test = logistic.predict_proba(test_woe_stepwise.drop(columns=target))[:, 1]
```

逻辑回归模型训练完成后,能够通过 `summary` 或 `summary2` 方法输出模型训练时的各项统计信息

```python
# 数据字典或特征描述信息
feature_map = {
    "const": "截距项",
    "status_of_existing_checking_account": "现有支票账户的状态",
    "credit_history": "信用记录",
    "purpose": "目的",
    "credit_amount": "信用额度",
    "present_employment_since": "现居住地至今",
    "installment_rate_in_percentage_of_disposable_income": "分期付款率占可支配收入的百分比",
    "personal_status_and_sex": "个人地位和性别",
    "age_in_years": "年龄",
    "housing": "住房情况",
}

# summary 仅支持输出简单的统计信息,使用 summary2 可以输出有特征描述的统计信息表
logistic.summary2(feature_map=feature_map)
```

| **Features**                                        | **Describe**    | **Coef.** | **Std.Err** | **z** | **P>\|z\|** | **[ 0.025** | **0.975 ]** | **VIF** |
|:---------------------------------------------------:|:---------------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|
| const                                               | 截距项             | -0.8422  |  0.0940  | -8.9567  | 0.0000   | -1.0265  | -0.6579  | 1.0464   |
| status_of_existing_checking_account                 | 现有支票账户的状态       | 0.8083   |  0.1549  | 5.2191   | 0.0000   | 0.5048   | 1.1119   | 1.0565   |
| credit_history                                      | 信用记录            | 0.8427   |  0.1823  | 4.6224   | 0.0000   | 0.4854   | 1.2000   | 1.0754   |
| purpose                                             | 目的              | 1.0387   |  0.2188  | 4.7462   | 0.0000   | 0.6097   | 1.4676   | 1.0186   |
| credit_amount                                       | 信用额度            | 1.0188   |  0.2345  | 4.3456   | 0.0000   | 0.5593   | 1.4784   | 1.0220   |
| present_employment_since                            | 现居住地至今          | 0.7043   |  0.3693  | 1.9074   | 0.0565   | -0.0194  | 1.4281   | 1.0620   |
| installment_rate_in_percentage_of_disposable_income | 分期付款率占可支配收入的百分比 | 1.3074   |  0.4023  | 3.2496   | 0.0012   | 0.5189   | 2.0960   | 1.0192   |
| personal_status_and_sex                             | 个人地位和性别         | 0.8170   |  0.4786  | 1.7071   | 0.0878   | -0.1211  | 1.7551   | 1.0058   |
| age_in_years                                        | 年龄              | 0.7926   |  0.3023  | 2.6219   | 0.0087   | 0.2001   | 1.3852   | 1.0724   |
| housing                                             | 住房情况            | 0.7277   |  0.4115  | 1.7684   | 0.0770   | -0.0788  | 1.5342   | 1.0165   |

`ITLubberLogisticRegression` 逻辑回归模型还支持直接通过画图查看模型系数稳定性

```python
# 逻辑回归系数稳定情况
logistic.plot_weights(figsize=(10, 6))
```

<div style="display: flex; justify-content: space-around;">
    <img src="https://itlubber.art/upload/sp_lr_weight.png" />
</div>

同时,也提供了 `report` 方法快速查看模型在某个数据集(`woe`后的数据集)上的分类效果

```python
# 查看某个数据集的分类效果
logistic.report(train_woe_stepwise)
```

|    | desc         | precision          | recall             |   f1-score |   support |
|:--:|:------------:|:------------------:|:------------------:|:----------:|:---------:|
|  0 | 好客户       | 0.7785588752196837 | 0.9040816326530612 |   0.836638 |       490 |
|  1 | 坏客户       | 0.6412213740458015 | 0.4                |   0.492669 |       210 |
|  2 | macro avg    | 0.7098901246327426 | 0.6520408163265305 |   0.664653 |       700 |
|  3 | weighted avg | 0.7373576248675191 | 0.7528571428571429 |   0.733447 |       700 |
|  4 | accuracy     |                    |                    |   0.752857 |       700 |

+ 在模型分类报告中,$macro\ avg$ 和 $weighted\ avg$ 的计算方法如下:

$macro\ avg = \frac{metric_1 + metric_2}{2}$

$weighted\ avg = \frac{metric_1 \times \frac{support_1}{total\ count} + metric_2 \times \frac{support_2}{total\ count}}{2} = \frac{metric_1 \times support_1 + metric_2 \times support_2}{2 \times total\ count}$


### 评分卡转换

在金融领域,如果直接使用模型预测的概率作为最终的评分,可能会对业务人员造成一定程度上的理解难度,同时,[模型预测的概率其实很难代表客户真实的违约概率](https://scikit-learn.org/stable/modules/calibration.html#calibration),不同模型预测的概率与真实违约概率之间存在不同程度的偏差。

<div style="display: flex; justify-content: space-around; ">
    <img width="100%" src="https://itlubber.art/upload/sphx_glr_plot_compare_calibration_001.png" />
</div>

<span style="display: flex; justify-content: space-around; font-size: smaller;">不同模型预测概率与真实概率之间的偏差</span>

由于模型预测概率与真实违约概率之间存在不同程度的偏差,评分卡不直接使用违约概率,而是引入了 `odds`(客户违约概率与正常概率的比值)来刻画客户的违约情况,其中 $odds = \frac{p}{1-p}$ ,$p$ 为模型预测的违约概率。

评分卡通常基于逻辑回归模型来制作,一方面是逻辑回归模型预测的违约概率与真实概率之间的偏差相比其他模型更接近,另一方面是逻辑回归模型能够很方便的转换为评分卡。

逻辑回归模型的数学公式如下:

$$
p = \frac{1}{1+e^{-\theta^Tx}}
$$

根据逻辑回归的数学公式很容易推导出入模特征与 `odds` 之间的关系:

$$
\begin{aligned}
\frac{1}{p} &= 1+e^{-\theta^Tx} \\
\frac{1}{p}-1 = \frac{1-p}{p} &= e^{-\theta^Tx} \\
ln(\frac{p}{1-p}) = ln(odds) &= \theta^Tx = \theta_0x_0 + \theta_1x_1 + \theta_2x_2 + ...... + \theta_nx_n
\end{aligned}
$$

评分卡模型是在原有模型预测的违约概率 $p$ 的基础上计算 $odds$,再根据 $ln(odds)$ 进行平移和伸缩变换得到最终评分卡分数的模型,本质上是一种将 $[0,\ 1]$ 概率空间映射到实数空间 $R$ 的一种方法。

假设评分卡模型数学形式如下:

$$
score = A - B \times ln(odds) = A - B \times ln(\frac{p}{1-p}) = A - B \times (\theta_0x_0 + \theta_1x_1 + \theta_2x_2 + ...... + \theta_nx_n)
$$

假设客户违约概率为 $p_0$ ,$base\_odds=p_0/(1-p_0)$ 时,对应的评分卡分数为 $base\_score$ ,当 $odds$ 增加 $rate$ 倍时,评分卡分数降低 $pdo$ 分,可以得到一个二元一次方程:

$$
\left\{
\begin{aligned}
base\_score &= A - B \times ln(base\_odds) \\
base\_score - pdo &= A - B \times ln(rate \times base\_odds)
\end{aligned}
\right.
$$

根据上述二元一次方程求解可以得到:

$$
\begin{aligned}
B &= pdo / ln(rate) \\
A &= base\_score + \frac{pdo}{ln(rate)} \times ln(base\_odds)
\end{aligned}
$$

> **注:** 在 `toad` 中使用的是 $woe = ln(\frac{good_i}{good}/\frac{bad_i}{bad})$ ,在上述推导中符号是反的,故而在 `toad` 库中的 `offset = base_score - factor * np.log(base_odds)`,形式上的差异并不会导致最终 `A` 和 `B` 的值,但在计算 $base\_odds$ 时需要注意使用对应的方式进行计算

当计算得到 $A$ 和 $B$ 后,概率转评分的模型也就确定了,下面我们正式进入评分转换方法。

```python
# 逻辑回归模型转评分卡
card = ScoreCard(target=target, combiner=combiner, transer=transform, pretrain_lr=logistic, base_score=50, base_odds=20, pdo=10)
# 传入 woe 数据计算评分卡参数
card.fit(train_woe_stepwise)

# 预测评分
train["score"] = card.predict(train)
test["score"] = card.predict(test)
```

在 `scorecardpipeline` 中提供了输出评分卡刻度的方法 `scorecard_scale` :

```python
# 输出评分卡刻度信息
card.scorecard_scale()
```

| 序号 | 刻度项     |   刻度值 | 备注                                                                                       |
|:--:|:----------|:--------:|:-------------------------------------------------------------------------------------------|
|  0 | base_odds  | 20       | 根据业务经验设置的基础比率(违约概率/正常概率),估算方法:(1-样本坏客户占比)/坏客户占比 |
|  1 | base_score | 50       | 基础ODDS对应的分数                                                                         |
|  2 | rate       |  2       | 设置分数的倍率                                                                             |
|  3 | pdo        | 10       | 表示分数增长PDO时,ODDS值增长到RATE倍                                                      |
|  4 | B          | 14.427   | 补偿值,计算方式:pdo / ln(rate)                                                           |
|  5 | A          |  6.78072 | 刻度,计算方式:base_score - B * ln(base_odds)                                             |

在 `scorecardpipeline` 中也提供了输出评分卡刻度的方法 `scorecard_points` :

```python
# 输出评分卡刻度信息
card.scorecard_points()
```

| 序号 | 变量名称                                            | 变量分箱                                                                                                                          |   对应分数 |
|---:|:----------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------|-----------:|
|  0 | status_of_existing_checking_account                 | no checking account                                                                                                               |    21.3836 |
|  1 | status_of_existing_checking_account                 | ... >= 200 DM / salary assignments for at least 1 year,nan                                                                        |     6.0824 |
|  2 | status_of_existing_checking_account                 | 0 <= ... < 200 DM                                                                                                                 |    -0.3605 |
|  3 | status_of_existing_checking_account                 | ... < 0 DM                                                                                                                        |    -5.0818 |
|  4 | purpose                                             | car (used),radio/television                                                                                                       |    12.0818 |
|  5 | purpose                                             | retraining,furniture/equipment,education,business,nan,car (new),repairs,domestic appliances,others                                |     2.3125 |
|  6 | credit_amount                                       | [-inf ~ 1382.0)                                                                                                                   |     2.4344 |
|  7 | credit_amount                                       | [1382.0 ~ 3804.0)                                                                                                                 |    13.6608 |
|  8 | credit_amount                                       | [3804.0 ~ inf)                                                                                                                    |    -2.9765 |
|  9 | savings_account_and_bonds                           | ... >= 1000 DM,500 <= ... < 1000 DM,unknown/ no savings account                                                                   |    12.0593 |
| 10 | savings_account_and_bonds                           | nan                                                                                                                               |     5.4614 |
| 11 | savings_account_and_bonds                           | 100 <= ... < 500 DM,... < 100 DM                                                                                                  |     2.5373 |
| 12 | present_employment_since                            | 4 <= ... < 7 years,... >= 7 years                                                                                                 |     9.1647 |
| 13 | present_employment_since                            | 1 <= ... < 4 years,nan,unemployed,... < 1 year                                                                                    |     3.2488 |
| 14 | installment_rate_in_percentage_of_disposable_income | [-inf ~ 4.0)                                                                                                                      |     7.997  |
| 15 | installment_rate_in_percentage_of_disposable_income | [4.0 ~ inf)                                                                                                                       |     0.6935 |
| 16 | personal_status_and_sex                             | female : divorced/separated/married,nan                                                                                           |     7.4426 |
| 17 | personal_status_and_sex                             | male : single,male : divorced/separated,male : married/widowed                                                                    |     3.122  |
| 18 | property                                            | real estate                                                                                                                       |    10.5651 |
| 19 | property                                            | building society savings agreement/ life insurance,nan,car or other, not in attribute Savings account/bonds,unknown / no property |     3.5272 |
| 20 | age_in_years                                        | [-inf ~ 23.0)                                                                                                                     |     6.0181 |
| 21 | age_in_years                                        | [23.0 ~ 35.0)                                                                                                                     |    -0.7208 |
| 22 | age_in_years                                        | [35.0 ~ inf)                                                                                                                      |    11.0045 |
| 23 | housing                                             | nan,own                                                                                                                           |     6.1523 |
| 24 | housing                                             | rent,for free                                                                                                                     |     1.6267 |

`scorecardpipeline` 还提供了一些方法,让您可以快速查看评分效果和分布情况:

```
# 查看 KS 和 ROC 曲线
ks_plot(train["score"], train[target], figsize=(10, 5), title="Train Dataset", save="model_report/sp_train_ksplot.png")
```

<div style="display: flex; justify-content: space-around; ">
    <img width="100%" src="https://itlubber.art/upload/sp_train_ksplot.png" />
</div>

```
# 查看评分分布情况
hist_plot(train["score"], train[target], figsize=(10, 6), save="model_report/sp_train_histplot.png")
```

<div style="display: flex; justify-content: space-around; ">
    <img width="100%" src="https://itlubber.art/upload/train_scorehist.png" />
</div>

通过 `scorecardpipeline` 提供的一些方法,您可以快速查看评分排序性,并得到相关的统计信息:

```
# 训练集评分排序性
score_clip = card.score_clip(train["score"], clip=20) # 计算等频分箱的间隔
score_table_train = feature_bin_stats(train, "score", desc="训练集模型评分", target=target, rules=score_clip) # 计算统计信息
bin_plot(score_table_train, desc="训练集模型评分", figsize=(10, 6), anchor=0.935, save="model_report/train_score_bins.png") # 画图
```

| 指标名称   | 指标含义       | 分箱          |   样本总数 |   样本占比 |   好样本数 |   好样本占比 |   坏样本数 |   坏样本占比 |   坏样本率 |   分档WOE值 |    分档IV值 |   指标IV值 |   LIFT值 |   累积LIFT值 |   累积好样本数 |   累积坏样本数 |   分档KS值 |
|:-----------|:---------------|:--------------|-----------:|-----------:|-----------:|-------------:|-----------:|-------------:|-----------:|------------:|------------:|-----------:|---------:|-------------:|---------------:|---------------:|-----------:|
| score      | 训练集模型评分 | [负无穷 , 20) |          9 |  0.0128571 |          1 |   0.00204082 |          8 |   0.0380952  |  0.888889  |  -2.92677   | 0.105523    |    1.20035 |  2.96296 |      2.96296 |              1 |              8 |  0.0360544 |
| score      | 训练集模型评分 | [20 , 40)     |        160 |  0.228571  |         63 |   0.128571   |         97 |   0.461905   |  0.60625   |  -1.27888   | 0.426292    |    1.20035 |  2.02083 |      2.07101 |             64 |            105 |  0.369388  |
| score      | 训练集模型评分 | [40 , 60)     |        283 |  0.404286  |        197 |   0.402041   |         86 |   0.409524   |  0.303887  |  -0.0184439 | 0.000138015 |    1.20035 |  1.01296 |      1.40855 |            261 |            191 |  0.376871  |
| score      | 训练集模型评分 | [60 , 80)     |        187 |  0.267143  |        170 |   0.346939   |         17 |   0.0809524  |  0.0909091 |   1.45527   | 0.387083    |    1.20035 |  0.30303 |      1.08503 |            431 |            208 |  0.110884  |
| score      | 训练集模型评分 | [80 , 正无穷) |         61 |  0.0871429 |         59 |   0.120408   |          2 |   0.00952381 |  0.0327869 |   2.53699   | 0.281312    |    1.20035 |  0.10929 |      1       |            490 |            210 |  0         |

<div style="display: flex; justify-content: space-around; ">
    <img width="100%" src="https://itlubber.art/upload/train_score_bins.png" />
</div>

除了在单个数据集上查看评分效果,评分卡的稳定性也是一个重要的评估指标,`scorecardpipeline` 内提供了查看两个数据集某个特征的 `PSI` 指标,同时针对评分卡模型,也提供了查看入模特征 `CSI` 的方法:

```python
# 查看某个特征的 PSI
score_clip = card.score_clip(train["score"], clip=10)
score_table_train = feature_bin_stats(train, "score", desc="训练集模型评分", target=target, rules=score_clip)
score_table_test = feature_bin_stats(test, "score", desc="测试集模型评分", target=target, rules=score_clip)
train_test_score_psi = psi_plot(score_table_train, score_table_test, labels=["训练数据集", "测试数据集"], save="model_report/train_test_psiplot.png", result=True)

# 查看某个入模特征的 CSI
for col in card._feature_names:
    feature_table_train = feature_bin_stats(train, col, target=target, desc="训练集分布", combiner=combiner)
    feature_table_test = feature_bin_stats(test, col, target=target, desc="测试集分布", combiner=combiner)
    train_test_csi_table = csi_plot(feature_table_train, feature_table_test, card[col], desc=col, result=True, plot=True, max_len=35, figsize=(10, 6), labels=["训练数据集", "测试数据集"], save=f"model_report/csi_{col}.png")
```

<div style="display: flex; justify-content: space-around; ">
    <img width="100%" src="https://itlubber.art/upload/train_test_psiplot.png" />
</div>

<div style="display: flex; justify-content: space-around; ">
    <img width="100%" src="https://itlubber.art/upload/csi_status_of_existing_checking_account.png" />
</div>


### 模型持久化存储

`scorecardpipeline` 提供了几种可选的模型持久化存储方式,可以将训练好的评分卡模型保存为 `pickle` 或 `pmml` 格式的模型文件,供后续生产部署或离线回溯使用,非常方便快捷

```python
# 将评分卡模型保存 pmml 文件
scorecard_pipeline = card.scorecard2pmml(pmml="model_report/scorecard.pmml", debug=True)
# 将评分卡模型保存 pickle 文件
save_pickle(card, "model_report/scorecard.pkl")
```


### 评分卡 `pipeline` 建模

在 `scorecardpipeline` 中,几乎所以的模型和数据预处理步骤都支持 `pipeline` 式构建模型,同时还可以与 `sklearn` 中其他的 `pipeline` 组件一起构建模型。

```python
# 构建 pipeline
model_pipeline = Pipeline([
    ("preprocessing", FeatureSelection(target=target, engine="scorecardpy")),
    ("combiner", Combiner(target=target, min_bin_size=0.2)),
    ("transform", WOETransformer(target=target)),
    ("processing_select", FeatureSelection(target=target, engine="toad")),
    ("stepwise", StepwiseSelection(target=target)),
    ("logistic", ITLubberLogisticRegression(target=target)),
])
# 训练 pipeline
model_pipeline.fit(train)
# 转换评分卡
card = ScoreCard(target=target, pipeline=model_pipeline, base_score=50, base_odds=(1 - bad_rate) / bad_rate, pdo=10)
card.fit(model_pipeline[:-1].transform(train))
card.scorecard_points()
```

| 序号 | 变量名称                                            | 变量分箱                                                                                                                             |   对应分数 |
|:--:|:----------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|-----------:|
|  0 | purpose                                             | radio/television,car (used)                                                                                                          |    12.8514 |
|  1 | purpose                                             | business,furniture/equipment,others,education,domestic appliances,retraining,car (new),缺失值,repairs                                |     2.7818 |
|  2 | installment_rate_in_percentage_of_disposable_income | [负无穷 , 4.0)                                                                                                                       |     7.7878 |
|  3 | installment_rate_in_percentage_of_disposable_income | [4.0 , 正无穷)                                                                                                                       |     0.9877 |
|  4 | installment_rate_in_percentage_of_disposable_income | 缺失值                                                                                                                               |    10.7462 |
|  5 | savings_account_and_bonds                           | 500 <= ... < 1000 DM,unknown/ no savings account,... >= 1000 DM                                                                      |     5.9834 |
|  6 | savings_account_and_bonds                           | ... < 100 DM,100 <= ... < 500 DM                                                                                                     |    12.1585 |
|  7 | savings_account_and_bonds                           | 缺失值                                                                                                                               |     3.2466 |
|  8 | present_employment_since                            | 4 <= ... < 7 years,... >= 7 years                                                                                                    |     9.4896 |
|  9 | present_employment_since                            | 缺失值,unemployed,1 <= ... < 4 years,... < 1 year                                                                                    |     3.8957 |
| 10 | age_in_years                                        | [负无穷 , 35.0)                                                                                                                      |     0.8917 |
| 11 | age_in_years                                        | [35.0 , 正无穷)                                                                                                                      |    10.8457 |
| 12 | age_in_years                                        | 缺失值                                                                                                                               |     6.7213 |
| 13 | property                                            | real estate                                                                                                                          |    11.2541 |
| 14 | property                                            | 缺失值,building society savings agreement/ life insurance,unknown / no property,car or other, not in attribute Savings account/bonds |     4.0428 |
| 15 | personal_status_and_sex                             | 缺失值,female : divorced/separated/married                                                                                           |     7.8997 |
| 16 | personal_status_and_sex                             | male : single,male : married/widowed,male : divorced/separated                                                                       |     3.7463 |
| 17 | credit_amount                                       | [负无穷 , 2145.0)                                                                                                                    |     8.0057 |
| 18 | credit_amount                                       | [2145.0 , 3804.0)                                                                                                                    |    14.2751 |
| 19 | credit_amount                                       | [3804.0 , 正无穷)                                                                                                                    |    -2.6347 |
| 20 | credit_amount                                       | 缺失值                                                                                                                               |     2.1778 |
| 21 | status_of_existing_checking_account                 | no checking account                                                                                                                  |    22.4653 |
| 22 | status_of_existing_checking_account                 | 缺失值,... >= 200 DM / salary assignments for at least 1 year                                                                        |     6.6694 |
| 23 | status_of_existing_checking_account                 | 0 <= ... < 200 DM                                                                                                                    |     0.0181 |
| 24 | status_of_existing_checking_account                 | ... < 0 DM                                                                                                                           |    -4.8558 |


### 评分卡全流程超参数搜索

```python
# 导入超参数搜索方法
from sklearn.model_selection import GridSearchCV

# 构建 pipeline
model_pipeline = Pipeline([
    ("preprocessing", FeatureSelection(target=target, engine="scorecardpy")),
    ("combiner", Combiner(target=target, min_bin_size=0.2)),
    ("transform", WOETransformer(target=target)),
    ("processing_select", FeatureSelection(target=target, engine="toad")),
    ("stepwise", StepwiseSelection(target=target)),
    ("logistic", ITLubberLogisticRegression(target=target)),
])

# 定义超参数搜索空间,参数命名: {pipeline名称}__{对应超参数名称}
params_grid = {
    "combiner__max_n_bins": [3],
    "logistic__C": [np.power(2, i) for i in range(5)],
    "logistic__penalty": ["l2"],
    "logistic__class_weight": [None, "balanced"] + [{1: i / 10.0, 0: 1 - i / 10.0} for i in range(1, 10, 2)],
    "logistic__max_iter": [10, 50, 100],
    "logistic__solver": ["sag"], # ["liblinear", "sag", "lbfgs", "newton-cg"],
}

pipeline_grid_search = GridSearchCV(model_pipeline, params_grid, cv=3, scoring='roc_auc', verbose=1, n_jobs=-1, return_train_score=True)
pipeline_grid_search.fit(train, train[target])

print(pipeline_grid_search.best_params_)

# 更新模型
model_pipeline.set_params(**pipeline_grid_search.best_params_)
model_pipeline.fit(train)

# 转换评分卡
card = ScoreCard(target=target, pipeline=model_pipeline, base_score=50, base_odds=(1 - bad_rate) / bad_rate, pdo=10)
card.fit(model_pipeline[:-1].transform(train))
```


### 模型报告输出

在 `scorecardpipeline` 中,提供了操作 `excel` 文件的写入器 `ExcelWriter`,支持将文字、表格、图像等过程内容保存至 `excel` 文件中,对相关方法抽象和封装后,能够满足日常大部分数据分析过程中结果保存的需求。

`ExcelWriter`支持调整列宽、调整单元格格式、条件格式、指定位置插入数据、插入图片等功能,且提供了 `dataframe2excel` 来在日常工作中快速保存 `dataframe` 至 `excel` 文件中,并且自动设置样式。

```python
# 初始化 Excel 写入器
writer = ExcelWriter()

start_row, start_col = 2, 2

# ////////////////////////////////////// 样本说明 ///////////////////////////////////// #
worksheet = writer.get_sheet_by_name("汇总信息")

# 样本总体分布情况
end_row, end_col = writer.insert_value2sheet(worksheet, (start_row, start_col), value="样本总体分布情况", style="header")
end_row, end_col = dataframe2excel(dataset_summary, writer, worksheet, percent_cols=["样本占比", "坏客户占比"], start_row=end_row + 1)

# 建模样本时间分布情况
temp = distribution_plot(df, date="date", target=target, save="model_report/all_sample_time_count.png", result=True)
end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="建模样本时间分布情况", style="header")
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/all_sample_time_count.png", (end_row, start_col), figsize=(720, 370))
end_row, end_col = dataframe2excel(temp, writer, worksheet, percent_cols=["样本占比", "好样本占比", "坏样本占比", "坏样本率"], condition_cols=["坏样本率"], start_row=end_row)

# ////////////////////////////////////// 模型报告 ///////////////////////////////////// #
summary = logistic.summary2(feature_map=feature_map)

# 逻辑回归拟合情况
worksheet = writer.get_sheet_by_name("逻辑回归拟合结果")

end_row, end_col = writer.insert_value2sheet(worksheet, (start_row, start_col), value="逻辑回归拟合效果", style="header")
end_row, end_col = dataframe2excel(summary, writer, worksheet, condition_cols=["Coef."], start_row=end_row + 1)

end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="训练数据集拟合报告", style="header")
end_row, end_col = dataframe2excel(logistic.report(train_woe_stepwise), writer, worksheet, percent_cols=["precision", "recall", "f1-score"], start_row=end_row + 1)

end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="测试数据集拟合报告", style="header")
end_row, end_col = dataframe2excel(logistic.report(test_woe_stepwise), writer, worksheet, percent_cols=["precision", "recall", "f1-score"], start_row=end_row + 1)

# ////////////////////////////////////// 特征概述 ///////////////////////////////////// #
worksheet = writer.get_sheet_by_name("模型变量信息")

start_row, start_col = 2, 2
end_row, end_col = writer.insert_value2sheet(worksheet, (start_row, start_col), value="入模变量信息", style="header")
end_row, end_col = writer.insert_df2sheet(worksheet, feature_describe.reset_index().rename(columns={"index": "序号"}), (end_row + 1, start_col))

# 变量分布情况
import toad
data_info = toad.detect(data[card.rules.keys()]).reset_index().rename(columns={"index": "变量名称", "type": "变量类型", "size": "样本个数", "missing": "缺失值", "unique": "唯一值个数"})
end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="变量分布情况", style="header")
end_row, end_col = writer.insert_df2sheet(worksheet, data_info, (end_row + 1, start_col))

# 变量相关性
data_corr = train_woe_stepwise.corr()
logistic.corr(train_woe_stepwise, save="model_report/train_corr.png", annot=False)
end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="变量相关性", style="header")
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/train_corr.png", (end_row + 1, start_col), figsize=(700, 500))
end_row, end_col = dataframe2excel(data_corr.reset_index().rename(columns={"index": ""}), writer, worksheet, color_cols=list(data_corr.columns), start_row=end_row + 1)

# 变量分箱信息
end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="变量分箱信息", style="header")

for col in logistic.feature_names_in_:
    feature_table = feature_bin_stats(data, col, target=target, desc=feature_map.get(col, "") or "逻辑回归入模变量", combiner=combiner)
    _ = bin_plot(feature_table, desc=feature_map.get(col, "") or "逻辑回归入模变量", figsize=(8, 4), save=f"model_report/bin_plots/data_{col}.png")
    
    end_row, end_col = writer.insert_pic2sheet(worksheet, f"model_report/bin_plots/data_{col}.png", (end_row + 1, start_col), figsize=(700, 400))
    end_row, end_col = dataframe2excel(feature_table, writer, worksheet, percent_cols=["样本占比", "好样本占比", "坏样本占比", "坏样本率", "LIFT值", "累积LIFT值"], condition_cols=["坏样本率", "LIFT值"], start_row=end_row)

# ////////////////////////////////////// 评分卡说明 ///////////////////////////////////// #
worksheet = writer.get_sheet_by_name("评分卡结果")

# 评分卡刻度
scorecard_kedu = card.scorecard_scale()
scorecard_points = card.scorecard_points(feature_map=feature_map)
scorecard_clip = card.score_clip(train["score"], clip=10)

start_row, start_col = 2, 2
end_row, end_col = writer.insert_value2sheet(worksheet, (start_row, start_col), value="评分卡刻度", style="header")
end_row, end_col = writer.insert_df2sheet(worksheet, scorecard_kedu, (end_row + 1, start_col))

# 评分卡对应分数
end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="评分卡分数", style="header")
end_row, end_col = writer.insert_df2sheet(worksheet, scorecard_points, (end_row + 1, start_col), merge_column="变量名称")

# 评分效果
score_table_train = feature_bin_stats(train, "score", desc="测试集模型评分", target=target, rules=scorecard_clip)
score_table_test = feature_bin_stats(test, "score", desc="测试集模型评分", target=target, rules=scorecard_clip)

ks_plot(train["score"], train[target], title="Train \tDataset", save="model_report/train_ksplot.png")
ks_plot(test["score"], test[target], title="Test \tDataset", save="model_report/test_ksplot.png")

hist_plot(train["score"], train[target], save="model_report/train_scorehist.png", bins=30)
hist_plot(test["score"], test[target], save="model_report/test_scorehist.png", bins=30)

end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="训练数据集评分模型效果", style="header")
ks_row = end_row
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/train_ksplot.png", (ks_row, start_col))
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/train_scorehist.png", (ks_row, end_col))
end_row, end_col = dataframe2excel(score_table_train, writer, worksheet, percent_cols=["样本占比", "好样本占比", "坏样本占比", "坏样本率", "LIFT值", "累积LIFT值", "分档KS值"], condition_cols=["坏样本率", "LIFT值", "分档KS值"], start_row=end_row + 1)

end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="测试数据集评分模型效果", style="header")
ks_row = end_row
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/test_ksplot.png", (ks_row, start_col))
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/test_scorehist.png", (ks_row, end_col))
end_row, end_col = dataframe2excel(score_table_test, writer, worksheet, percent_cols=["样本占比", "好样本占比", "坏样本占比", "坏样本率", "LIFT值", "累积LIFT值", "分档KS值"], condition_cols=["坏样本率", "LIFT值", "分档KS值"], start_row=end_row + 1)

# ////////////////////////////////////// 模型稳定性 ///////////////////////////////////// #
worksheet = writer.get_sheet_by_name("模型稳定性")
start_row, start_col = 2, 2

# 评分分布稳定性
train_test_score_psi = psi_plot(score_table_train, score_table_test, labels=["训练数据集", "测试数据集"], save="model_report/train_test_psiplot.png", result=True)

end_row, end_col = writer.insert_value2sheet(worksheet, (start_row, start_col), value="模型评分稳定性指标 (Population Stability Index, PSI): 训练数据集 vs 测试数据集", style="header")
end_row, end_col = writer.insert_pic2sheet(worksheet, "model_report/train_test_psiplot.png", (end_row, start_col), figsize=(800, 400))
end_row, end_col = dataframe2excel(train_test_score_psi, writer, worksheet, percent_cols=["训练数据集样本占比", "训练数据集坏样本率", "测试数据集样本占比", "测试数据集坏样本率"], condition_cols=["分档PSI值"], start_row=end_row + 1)

# 变量 PSI 表
for col in card._feature_names:
    feature_table_train = feature_bin_stats(train, col, target=target, desc=feature_map.get(col, "") or "逻辑回归入模变量", combiner=combiner)
    feature_table_test = feature_bin_stats(test, col, target=target, desc=feature_map.get(col, "") or "逻辑回归入模变量", combiner=combiner)
    psi_table = psi_plot(feature_table_train, feature_table_test, desc=col, result=True, plot=True, max_len=35, figsize=(10, 6), labels=["训练数据集", "测试数据集"], save=f"model_report/psi_{col}.png")
    
    end_row, end_col = writer.insert_pic2sheet(worksheet, f"model_report/psi_{col}.png", (end_row, start_col), figsize=(700, 400))
    end_row, end_col = dataframe2excel(psi_table, writer, worksheet, percent_cols=["训练数据集样本占比", "训练数据集坏样本率", "测试数据集样本占比", "测试数据集坏样本率", "测试数据集% - 训练数据集%"], condition_cols=["分档PSI值"], start_row=end_row + 1)

# 变量 CSI 表
end_row, end_col = writer.insert_value2sheet(worksheet, (end_row + 2, start_col), value="入模变量稳定性指标 (Characteristic Stability Index, CSI): 训练数据集 vs 测试数据集", style="header")

for col in card._feature_names:
    feature_table_train = feature_bin_stats(train, col, target=target, desc=feature_map.get(col, "") or "逻辑回归入模变量", combiner=combiner)
    feature_table_test = feature_bin_stats(test, col, target=target, desc=feature_map.get(col, "") or "逻辑回归入模变量", combiner=combiner)
    train_test_csi_table = csi_plot(feature_table_train, feature_table_test, card[col], desc=col, result=True, plot=True, max_len=35, figsize=(10, 6), labels=["训练数据集", "测试数据集"], save=f"model_report/csi_{col}.png")
    
    end_row, end_col = writer.insert_pic2sheet(worksheet, f"model_report/csi_{col}.png", (end_row, start_col), figsize=(700, 400))
    end_row, end_col = dataframe2excel(train_test_csi_table, writer, worksheet, percent_cols=["训练数据集样本占比", "训练数据集坏样本率", "测试数据集样本占比", "测试数据集坏样本率", "测试数据集% - 训练数据集%"], condition_cols=["分档CSI值"], start_row=end_row + 1)

# 保存结果文件
writer.save("model_report/评分卡模型报告.xlsx")
```

<div style="display: flex; justify-content: space-around; ">
    <img width="100%" src="https://itlubber.art/upload/scorecardpipeline.png" />
</div>


## 交流

<div style="display: flex; justify-content: space-around;">
    <img width="30%" alt="itlubber" src="https://itlubber.art/upload/itlubber.png"/>
    <img width="30%" alt="itlubber_art" src="https://itlubber.art/upload/itlubber_art.png"/>
</div>

<div style="display: flex; justify-content: space-around; margin-bottom: 2rem;">
    <span>微信<code>: itlubber</code></span>
    <span>订阅号<code>: itlubber_art</code></span>
</div>


================================================
FILE: docs/source/scorecardpipeline.rst
================================================
scorecardpipeline
======================================

.. automodule:: scorecardpipeline
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.processing
======================================

.. automodule:: scorecardpipeline.processing
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.feature_selection
======================================

.. automodule:: scorecardpipeline.feature_selection
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.model
======================================

.. automodule:: scorecardpipeline.model
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.auto_eda
======================================

.. automodule:: scorecardpipeline.auto_eda
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.auto_report
======================================

.. automodule:: scorecardpipeline.auto_report
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.utils
======================================

.. automodule:: scorecardpipeline.utils
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.excel_writer
======================================

.. automodule:: scorecardpipeline.excel_writer
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.rule
======================================

.. automodule:: scorecardpipeline.rule
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.rule_extraction
======================================

.. automodule:: scorecardpipeline.rule_extraction
   :members:
   :undoc-members:
   :show-inheritance:


scorecardpipeline.logger
======================================

.. automodule:: scorecardpipeline.logger
   :members:
   :undoc-members:
   :show-inheritance:


================================================
FILE: examples/auto_report.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import warnings\n",
    "\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "import sys\n",
    "sys.path.append(\"../\")\n",
    "\n",
    "import os\n",
    "import re\n",
    "import six\n",
    "import random\n",
    "import joblib\n",
    "import warnings\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib import font_manager\n",
    "from matplotlib.ticker import PercentFormatter, FuncFormatter\n",
    "import seaborn as sns\n",
    "from scorecardpipeline import *\n",
    "\n",
    "\n",
    "init_setting()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "### 三方数据评估报告输出"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████| 20/20 [00:07<00:00,  2.56it/s, feature=foreign_worker]                                          \n"
     ]
    },
    {
     "data": {
      "text/plain": "(653, 22)"
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "text/plain": "<Figure size 1600x800 with 2 Axes>",
      "image/png": "iVBORw0KGgoAAAANSUhEUgAABFIAAAPQCAYAAAAVZV9XAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hT1R/H8U+66GCXssoWBGQLioBQNoqCggoKMlT2KCgICDJkyKwylfVjgwjKUhBBRoEyyxSUKavsUuigLW3T/P6ICS0tEKRpC32/nidPkntP7j3n2zTJ/d5zzjWYTCaTAAAAAAAA8EgOaV0BAAAAAACApwWJFAAAAAAAABuRSAEAAAAAALARiRQAAAAAAAAbkUgBAAAAAACwEYkUAAAAAAAAG5FIAQAAAAAAsBGJFAAAAAAAABuRSAEAAAAAALARiRQAAAAAAAAbOaV1BQAAAAAAAJITHBys4cOHq0SJEurZs2eyZX755Rf9/vvv8vDwUJ48edS7d285OJj7jQQEBGjRokXy9PSUg4ODBg4cKFdX1yeqEz1SAAAAAABAumI0GrV06VJ99913+v333x9YLjAwUMOHD9fo0aM1duxY7d+/XzNnzpQkXbhwQd26ddPAgQM1cuRIRUdHa9SoUU9cNxIpAAAAAAAgXYmLi1PlypX18ccfP7Tc1KlTVbp0aWXJkkWSVK1aNc2ZM0eRkZGaMWOGcuXKpYIFC1rXrVixQpcuXXqiujG0BwAAAAAA2EW9evUeun7Tpk3JLs+UKZNKlCihoKCgB742Ojpae/fu1WuvvWZd5unpqdDQUB08eFD+/v7y9vZOtC4uLk4BAQFq0aLFY7bkHhIpAAAAAAA8416rmkY7zmy/TYeGhspoNMrZ2dm6zPL45s2bunXrlooUKZLsuidBIgUA7CzNvrSecev3mO8nDI9N24o8o/oOMf/QCAuLSOOaPJuyZjX/qiS+9kF87Yv42hfxtS9LfJF6HtTjJCVkzZpVjo6OioyMtC6LjTX/NvT09FSOHDkeuO5JMEcKAAAAAAB46ri5uenll1/W9evXrctu3rypLFmyqGLFivLx8Um0LiQkRI6OjqpevfoT7ZdECgAAAAAAeCrExMSoU6dOmjdvniSpZ8+eOnHihEJCQiRJe/bsUfv27eXh4aEuXbooIiJCx48flyTt3r1bb731lgoUKPBEdWBoDwAAAAAASHf27t2rH3/8UZL0+++/K1++fGrcuLFOnTplvRJP5cqV5efnp0GDBil79uwqW7asunbtKkkqWLCg5s6dq2+//VZ58uSRg4ODBg8e/MT1MphMJtMTbwUA8EDMkWIfzJFiX8yRYl/MgWBfxNe+iK99EV/7yshzpKTVb1LLb7ZnCUN7AAAAAAAAbEQiBQAAAAAAwEYkUgAAAAAAAGxEIgUAAAAAAMBGJFIAAAAAAABsRCIFAAAAAADARiRSAAAAAAAAbEQiBQAAAAAAwEYkUgAAAAAAAGxEIgUAAAAAAMBGJFIAAAAAAABsRCIFAAAAAADARiRSAAAAAAAAbEQiBQAAAAAAwEYkUgAAAAAAAGxEIgUAAAAAAMBGJFIAAAAAAABsRCIFAAAAAADARiRSAAAAAAAAbEQiBQAAAAAAwEYkUgAAAAAAAGxEIgUAAAAAAMBGJFIAAAAAAABsRCIFAAAAAADARiRSAAAAAAAAbEQiBQAAAAAAwEYkUgAAAAAAAGxEIgUAAAAAAMBGJFIAAAAAAABsRCIFAAAAAADARiRS8MwJDg7W+vXr1blzZ02dOjWtq4N07tKlS1qyZInq16+voKCgtK4OMgCDQcqb35DW1QAAAMB/5JTWFQBS0p07d7RmzRpNnz5doaGhKlu2bFpXKZF9+/ape/fuatSokUaMGJGmdVm3bp0GDRqkjh07qlu3bmlal7Ry8+ZNDR06VNu3b0/rqthd7YbSBx9JnT94cJnSZaUOvuYD/ctB0rTxUlTkvfWNmkhvNDc/Dtgq/TjfrlV+KlR91UElSpqTIof2x+voIdMDyxoMUuWqDipR2qCArfHW5U5OUtWaDvLKbVA+b4OuXDJp83qjwkLtXv10z2Qyad68udqyZYsk6d1331XTpm8lW/bIkSOaPHmi4uNNKliwoPr16y8PDw+ZTCZVq1ZVRqMxyWvGjh2nunXr2bUN6VVKxNZi7dpfNXv2LAUHB6ts2bIaOvQr5c2bN1Xa8TTYtm2b5syZLUmqUKGievbsKScn52TLBgcHa9SokbpzJ0KOjo7q3/8LFSlSJNmyM2ZM15UrVzRs2Ff2qvpTIaXjGxQUpC++6K/evT9V5cpV7F39dC8l47tixQrt27dXR4/+qRw5cqpXr17EGE8teqTgmeLh4aGPP/5YPj4+aV2VZF29elWhoaE6f/58WldFFy9eVGRkpC5evJjWVUkznp6e+u6779K6GnaVOavU/XOp90DJ1fXB5fJ5SyMnS4tmS591lJycpf7D762vWVfq1Fsa1Evq01lq/LbUvJW9a5++vVTNQeUqOuiHeUb9vMSouo0cVaJU8j1NMmWSWrZzVInSBv202KgLZ+8lXGrUcdCR/fFa9aNRyxbEqXAxg5q+x3kOSVq0aKFWr16l2bP/p8mTJ8vPb4I2b96UpFxQ0EX5+vZQx46dNGfOXMXGxurLLwdJkkJDQ5UlSxYVLlzYesufP788PT1Vq1b6/K5IDSkRW0kKDNynK1euaMyYsWrVqrUCAwO1dOkPqdmUdO3IkSMaMKCfhg0brrlz5+vPP4/o22+/TbZsXFysfH17qkSJEpo5c7bq12+gHj26KTw8PEnZY8eOau7cOfaufrqX0vFduXKFevf21fHjx1OrCelaSsZ3zZrVypcvr0aPHqOlS5cpOjpaffp8puvXr6dmk4AUQyIFzyRHR8e0roL8/f2TLGvSpMm/mf20//HTuXNnbd26VV9//XVaVyXVGI3GJL1PXFxc0qg29ufkJH3UVVo6Twq99fCybTqa7w/uNd/v8pdeqSmVf9Hck6KDr3T0kBQeJsXGSIG7pdYdJDd3e7Yg/cqUSarm46Czp+NlNEpRUdKliyb5NEj+s+fNdx2VOYtBPy8xKjbm3nJXV3Ovn/Aw8/ObwVLwdZPy5jfI3SPZTWUY4eHhmjVrpqpXryEXFxdlz55D5ctX0KRJk2QyJe75M2PGDEnSyy9XlSTVrl1bO3Zs1/79gbp7964mTpysn35aYb21bdtOb7zxppycMmbCKqViK0nZsmVXhw4dVbJkKbVt206S0l1v0LQ0efJEFSxYSEWKFJHBYFCtWj76+eefFBSU9CTGr7/+qlOnTqp27TqSJB+f2rp27Zp++GFJonLR0VEaPfrrZHtZZTQpGd+lS3+Ql5eXGjZslKptSM9SMr7nzp1TtWrVJZlPfNasWVN37tzRoUOHUq09QEoikQI8IZPJpJCQEOvzmJgYTZ06VcOHD0+2fJ48edLNj/d8+fLJYMgYczWEhISod+/eWrt2bVpXJdXExUlTxko3bzy8nJOTVL2OdP3KvWVX/p0uxqeB9EJ5KU8+6erlBOsvSR4e0ss1Ur7eT4Nizxvk4mJQ6O17y27fMil7DvPwnIRKlDKo6HMO2htgVMzdxNuJjpb27YxPtCxTJoNiYkyKvGOnyj8ltm/frqioKOXPn9+6rECBArp8+ZKOHj1qXRYbG6stWzYrb9681s8zb+8CkqQNGzYoT548KlOmTKJt//LLGr3+euNUaEX6lFKxlaQSJUpYy69Y8bP69Omr+vUbpEYz0r2rV6/q8OHDSeJsNBr1xx9/JClviamlfK5cueTq6qoNG35PVG7atGl6880mdqz50yGl4/v++x/o1VdrpkLNnw4pHd/u3XskKm/pqZI/fz671B+wt/RxNAc8oR9//FELFy6Us7OzHBwcFBYWZl33xx9/6O+//5YkZcmSRe3bt9fPP/+sy5fNR4Xe3t5q3ry5tfzevXs1ceJE+fr6qnfv3ipcuLCWLFkiR0dH/fnnn/Lz89Ply5fl7OwsLy8vhYaGqm7duurZs6d2796t8ePH6+jRo3JxcVGbNm0kSV26dFGNGjUUHh6uKVOmKGvWrOrRw/yFEhUVpe+++06///67PDw8dOvWLZUuXVq+vr4qXbq0JPOX2bZt2zR16lT973//05gxYxQYGKgCBQpo2LBheumllx47Zjdu3NCYMWNUo0YNNW/eXHfv3tXRo0e1dOlSFShQQG5ublqyZIkiIiLUqFEjffXVV4+VALpy5YpWrVqlw4cPKzo6Wq+//rq2bdsmf39/5cuXT/3791dUVJS2bt2qbdu2ydPTU8OHD9fLL79s3cby5cu1ZMkSRUdHKy4uTjlz5tQnn3yihg0bSpLCwsK0d+9ezZo1Sx06dNC2bdv022+/ydXVVV27dlXr1q0lSYsXL9bMmTN19epV5cqVy/p3mTRpknLmzGnd3927dzVs2DCtWbNGDg4OatasmQYNGqRnnXchc8+IhAfudyLM9yVKSefOmB9HRiS/3n9j6tQzPcmd13xQeffuvbP3d/9NkuTJZ57nxKJsRfM5i+DrUou2jnJ0lI4eitefB81lEnYAyJvfoJy5DNoTwJnmkydPSJIyZ85sXWZ5/Pfff6tcuXKSpPPnz+vu3bsPLHe/s2fP6u7duypevLjd6p7epXRsjUajvvtumlavXqXSpUurVKlSqlixkt3bkd6dOnVSUvJxPn486XvzQX+X8+fPKzIyUu7u7goICFDOnDkTJbAyKnvEF/fYM7537tyRv7+/ypUrrzJl6MGGpxM9UvDUGz9+vIYMGaIyZcpo5cqVmjNnjiIi7h3x1alTR0ajUVOnTtWCBQskmYfYmEwmTZ06VStXrpQk7d+/X++++67atGmjCxcuaPTo0bp165YOHTqkW7du6dSpU2rTpo1u3ryplStXavHixfrrr7/0119/Wff1yiuvqF+/fpIkLy8vLVy4UAsXLlSNGjX0zTffqFatWpo/f76167TJZFKnTp00c+ZMNW3aVCtXrtS8efO0c+dOtWrVSqdPn1ZwcLBGjBihwYMH69q1a/Lz81PXrl316aef6vTp0+rbt+9jx6x///6qU6eOfv31V+uyTZs2qWvXrlqzZo2WLl0qNzc3TZ48Wfnz59dPP/1kjZOt8uXLJwcHB23ZskX79u1TyZIlNW3aNL322mu6cOGCevXqpatXr8rPz09DhgzR2bNnE/XimTx5sr788ktly5ZNa9eu1S+//KLIyEj17NlTK1askCTNmjVL3bt316FDhzRp0iT5+Pho4sSJioiI0MiRI3X27FlJUuvWrfXuu+9KkmrWrGn9uyRMokhSv379FB4ermbNmikqKkoLFizIEBPRZs5ivk/YSzwuznzvkSXB+vhk1t/7vZShZMpkTqSYEsQk/t/4ZbpvLpq8+Q2KjjbpcpBJyxYYde6MSY2aOOm55xP3XHF2lho1cdTZ0/EK2JK4l0pGFBFhPlvp4HBvuJQlmWtZ9zjlLNav/y3Dd91P6dj6+29V6dKl1a9ff508eUq+vj115UqCLm4ZVHi4+beIo+O9n9uW+FnWJWT57ZJwePK9eEfo1q1bWrHiZ+sQqowupeOLxOwZ32++8VOmTC76+uvRGaZnNJ49JFLwVDt37px1vpGOHc2TPGTLli1RDw1HR0cVKlQo0etcXFxUoECBRMsqV66sdu3MP05u3LihiRMnqlOnTnrzzTeVK1cuTZ06VVFRUWrZsqU8PDyUPXt2VaxY0ea6du3aNckZpN27d2vvXvOkFC1btpQkFSlSRDVq1FBkZKS+//575cqVy5qckaSpU6eqSpUqatXKPNPn1atXdfPmTZvrIUlffPGFcufOnWhZ48aNVatWLUlSixYt1KZNG5UvX946ce+xY8ceax+SOZkkSeXLl7fGqkoV8+zsOXLkUIcOHazrJemff/6RJEVGRmr2bPMM8e+9954cHBzk6uqqd955R5I0ceJEmUwm9enTR3ny5JEkDRw4UPXr19err76qkiVLKj4+Ptmz0Q/zzjvvyM/PT4MHD1bNmubuvY+7jaeRpSdKwg5Hlsd3Ih6xPoMOP4mJMSdDExxfWh/fP3zH1S3xssP7zUmSki/c+wo2GKQ333HUjWsmrfrRqHjyKPL4N0sXZ8naJXic8IynreUsNmz4XfXq1U/5Cj9FUjq2devWU/36DdSwYSN17NhRUVFR2rkzwH4NeEpYrmxk63vzYeU9PDw0btxY9ezpmy7mgUsPUjq+SMxe8Z03b66OHz+u2bPncHUvPNUY2oOnWkBAgOLj4+Xs7KznnnvOuvy/ds+0JBdy5cqlokWLqk+fPtZ1u3fvlqREl3HLnj27zdt2c3NTsWLFdPjwYesyS3IiU6ZM1qSDJBUuXFiSdPDgQUnJZ/cTTpIaFRVlcz0s9fb29talS5cSLbdsO+EQHst+oqOjH2sfyW034fZcXFysZyEs7bNMnHf69Gnd/XecRMKElyUu165d0+XLl+Xt7W3ddsIY/dc6WxJJ0r2/raUez7Kg8+YD/SxZ7y2z9DQ5c0L655T58YPWZ0Q3rpoTKa5uBknmx5kymdddu5p4ss7IO1LC457of/9dE07UW7+xg65fNVkvi2wwmOOdkS+BbEk8hyUIguWsZsmSJa3LChcuLBcXl2TLPf/8vXKSdOLECbm5uSVJrmc09oitRf783pKe7DvjabVz507Nnz/P+rx27dqSpNDQe8ONk4uzRYkSJbR//36FhoYqV65c1vIFCxZUUFCQTp8+pdGjzRPEW+aX2LVrlzp37qQZM2bao0npij3jSyIldeK7fv1vOnDggGbP/p/c3NwkSRcuXMjwn8l4OpFIwVMtMjJSkuTs7JyiXQMzWY6IktlXfIJTxZZlT+pBZ5fuv3pCRuTgkPCs/b2/cTyn7J9YJlfpbrQUGyvt9Jeq1zYPL4mNlfL9m7/6bbV0+rh045pUsPC91+bzlkJvSwFb06Di6cCZkybFxpiU0/PeezJ7DoOCr5t0+aLJGkdJunLJpBKlDHLJZE5YWRIoN66b/79fKGdQXJysSRRJKlzMoBw5DTq4L+O+z2vWrCVXV1edO3fOuiwoKEhFixZThQoVFRUVJTc3N7m4uKh27TrasmWzYmJi5OLiokuXzLMlN2vWLNE2t23zt15RIiOzR2wtLlw4L4PBoKpVX0mNpqQr1atXV/Xq1RMt27hxg86fP2d9HhQUJCcnJ+tksdHR0cqUKZMMBoMaNmyk/fv36/z5c8qVK5eCg4MVHR2tt99uppIlS2r58p+t29m/P1BdunRWtWrVNGzYV6nSvrRmz/jC/vE9d+6cVqxYoalTp1lPeEVEmK8gNmLEyNRpJJCCGNqDp5olgx0ZGfnQ4S2WXguhoaHWXg+PeyBumYU8KCjIuswyB0dCzs7OkmzrJWIZ7hIZGZnoyj/nz5+XZB5ulBGVKFHCevYiYa8Zy4/+vHnzJhma9SiP83d5Fjk4Jh6GUqOOtHKL1OjfCz/Mn2E+8K/x7zFmNR9pb4B08i8pPl6aOUkqXV7KnVdydpGqvCItX5h0GEtGER0t7dwWr2IlDHJxkdzcJO+CBu30N6pEKYN8BzipbEVzkmXPDvMlkstWMH/llirjoOhokwJ3xcvBQfJp4CgXF4PqN3ZQ/cYOavimoxq+6aioyIydSM2WLZs6duykgIAdioiI0O3bt3TkyGF16tRJmzdvko9PTa1evUqS1KVLV7m4uGjz5k2SpK1bt6pGjRpJJjHctWuXatasdf+uMpyUim1wcLBq1qyh//3PPBQzNDRUy5cvU6tWrTP0ZL4J9er1qa5fv67Dhw/LZDJp27ZtevvtZsqdO7euXr2ihg3ra9iwoZKkpk2bqkSJ57V+/XpJ5rlnvLy81KxZ84ftIkOzR3wtvxONxoybyLZIyfhOnTpZbm5u8vOboNGjv9bo0V+rW7du1p4pwNOGHil4qtWuXVv58uXTlStXtGDBAn366acymUy6ffu2pHs9OizDcSIiItS9e3d5e3vr1CnzeIXbt2/r7t27ypQpk7V8ckmWJk2aaMqUKfrll1/0/vvv68CBA9axnwl7pnh7e8tgMCgkJETr1q1Tvnz5FB0drWrVqin231PUlu1XqVJFdevW1ebNm7VkyRL16NFDZ86c0fbt2xNd2SdhfYxGoxwdHa3bkhKPR7XV/XWxbDvhfcJtJ9yfrSxduxO+1pLIiImJSbJfS33c3Nzk6+ur0aNH68cff1TDhg0VHR2t5cuXy2AwaMCAAdbeKcklxiz7SxiXggULSjJPKnzgwAGFhoaqaNGiiRIyCetpqV/Cuj2NylSQ3mgueZmnklGfwdKKpeaeKFFR5ptkvtzxIF+pc2+p6XvmSx2PGXxvO9s3mXtSDB5rvsrM+jXST4tSvTnpyr6d8TIYpBZtnSSZtGWDUSf/NqlocYNiYqTYf9/i166Y9PMSo+o2clSpsgYZjdLSeXGKvCMVKmqQR2aDylVK2qMug+b8Emnbtp2MRqO6du0sg8Gg3r0/Vf36DRQQECB3d3e5/du9p2DBgpoyZZq+/dZPy5YtU/78+TVy5NeJthUdHaUrVy6rVKlSadGUdCclYuvp6amWLd/XsmU/yt9/q9zd3dW9ew/VrVsvLZuWrlSoUEHjx0+Qn98EGQwGlStXVr16fSrJfJLH3d1dWbJk/ve5syZPnqJRo0bqk08+lpOTk6ZN+15ZsmRJyyakaykZ36ioKE2f/r01ETB16hS98847euutt9OkbelBSsX37t272rFjR7K/qV55JeP1XsOzwWBi7ACeckeOHNHw4cN15swZvfjiiypbtqz27NmjgwcP6sUXX1SXLl3k4+OjESNGaNWqVXJ1dVWHDh3k6emp6dOnq0SJEipdurQaNWqkcePGafPmzZJkvYKP5Ud3bGysxo4dq5UrV6pUqVJq3LixChUqpL59+ypTpkwaMGCAGjduLEmaNm2a5s+fL4PBoCZNmqhv374aN26cfv75Z0VHRytXrlzq27evmjVrpsjISE2ZMkW///67smbNqvDwcFWsWFG+vr4qXLiwjh07phEjRljnSylXrpwmTZqk4cOHa+vWrZLMPVdGjBiRaJ6YB4mKitKQIUO0du1aGY1GFSxYUIMHD9b+/fu1aNEi3blzR+7u7ho0aJCcnJw0fvx4BQcHy8PDQ507d1bnzp1t+rv88ccf+vrrr3Xp0iU5OjqqcePGeu+99/T555/r2rVrkqRGjRrp7bff1tSpU63zxbz77rsaOXKkDAaDli5dqsWLF1uTGvny5VPnzuauzBERERo4cKA2bNggk8mkvHnzatq0aVq7dq0WLlyo2NhYFSpUSAMGDFC9evUUExOjzz77TDt27JCnp6c+/vhjvf/++/L19dUff/whSSpevLhGjhypAwcOaNKkSbp79668vLw0fPhw1a1b16Z2J+e1qv/5pXiI9XvM9xOGP36SD4/Wd4i5F1dYGFezsIesWc0HH8TXPoivfRFf+yK+9mWJb0aUVr9JLb/ZniUkUvBMioyMlLOzs3U4x39lMplSbO4VS0+SBz1PjTpYtmcymRLNPWI0GuXg4JCi+4mJiZGTk5N1P3fv3pXBYJDRaJSLi4vi4+MVExOTZIK36Ohoubq6JrdJu4iPj08Ui/ufpwQSKfZBIsW+SKTYFwdK9kV87Yv42hfxtS8SKanvWUykMLQHz6T/etWe+6VkYuH+pImtly+0tQ4bN26Un5/fI8s1aNAg0dWIHqcuktSuXTtrj5KHmT9/vvXSxFLSCXwdHR2TTXSlZhJFUpKkSUonUQAAAAA8W0ikAM+I8PDwZCe/vd+NGzeeaD8XL15Mctnk5PyXOVUAAAAAIL0jkQI8I5o3b67mze0/s79lDhkAAAAAyIjoww4AAAAAAGAjEikAAAAAAAA2IpECAAAAAABgIxIpAAAAAAAANiKRAgAAAAAAYCOu2gMAAAAAwDOuWIn4NNrzs9d/49lrEQAAAAAAgJ2QSAEAAAAAALARiRQAAAAAAAAbkUgBAAAAAACwEYkUAAAAAAAAG5FIAQAAAAAAsBGJFAAAAAAAABuRSAEAAAAAALARiRQAAAAAAAAbkUgBAAAAAACwEYkUAAAAAAAAG5FIAQAAAAAAsBGJFAAAAAAAABuRSAEAAAAAAM+8iIgIfffdd0+8HacUqAsAAAAAAECKCgkJ0ahRo+Th4aHg4GD17NlTpUuXTlSmS5cu2rJlS6JlLi4u2rRpk3Lnzq02bdpo79691nXvvffeE9eLRAoAAAAAAEh3+vTpo0qVKsnX11e7du3SRx99pI0bNypLlizWMnny5FGvXr2sz48fP64sWbIod+7ckqSiRYuqUaNG1vWlSpV64nqRSAEAAAAAAOlKYGCgdu7cqW7dukmSXnrpJYWHh2vx4sXq0qWLtVzXrl2VN29e6/OePXuqdevW1uclS5ZM9DwlkEgBAAAAAAB2Ua9evYeu37RpU7LLt23bJkny9PSUJDk5OSl79uzaunVrokRKwiTK1atXde3aNb3wwgvWZfv379eUKVMUFxenihUratCgQSpatOh/bo/EZLMAAAAAACCdCQkJkSQ5Oztblzk7O1uXJ2f58uV6++23Ey2rWrWqRo0apV69eunPP/9U586dFRsb+0R1o0cKAAAAAACwiwf1OHmUHDlySJIiIyOty2JjYxP1QEkoPj5ev/76q5YtW5ZoecuWLa2PPT099emnn+rMmTNPNFcKPVIAAAAAAEC64uPjI0m6fv26JCkuLk6hoaGqVatWsuUDAwP1/PPPK1u2bA/cZokSJSRJHh4eT1Q3EikAAAAAACBdqVKlimrWrKkdO3ZIMs914uHhoRYtWqhTp06aN29eovLr169X48aNEy3btWuXtm7dan2+f/9+NWvWTAULFnyiujG0BwAAAAAApDuTJk3S0KFDNXToUF27dk2zZ8+Wu7u7Tp06lSQZsnv3bvXt2zfRsrCwME2aNEmbN29WuXLlFBUVpeHDhz9xvUikAAAAAACAdMfDw0MTJkxIsnzLli1Jlq1bty7JskaNGqlRo0YpXi+G9gAAAAAAANiIRAoAAAAAAICNSKQAAAAAAADYiEQKAAAAAACAjQwmk8mU1pUAAAAAAAD20+3D+DTZ73eLnr3+G89eiwAAAAAAAOyEyx8DgJ1NGB6b1lV4JvUd4ixJeq1qGlfkGbV+j/n+8IHItK3IM6rCi+6SpLCwiDSuybMpa9bMkoivvRBf+yK+9mWJL/Ak6JECAAAAAABgIxIpAAAAAAAANiKRAgAAAAAAYCMSKQAAAAAAADYikQIAAAAAAGAjEikAAAAAAAA2IpECAAAAAABgIxIpAAAAAAAANiKRAgAAAAAAYCMSKQAAAAAAADYikQIAAAAAAGAjEikAAAAAAAA2IpECAAAAAABgIxIpAAAAAAAANiKRAgAAAAAAYCMSKQAAAAAAADYikQIAAAAAAGAjEikAAAAAAAA2IpECAAAAAABgIxIpAAAAAAAANiKRAgAAAAAAYCMSKQAAAAAAADYikQIAAAAAAGAjEikAAAAAAAA2ckrrCgAAAAAAAPsq9rwxjfb87PXfePZaBAAAAAAAYCckUgAAAAAAAGxEIgUAAAAAAMBGJFIAAAAAAABsRCIFAAAAAADARiRSAAAAAAAAbEQiBQAAAAAAwEYkUgAAAAAAAGxEIgUAAAAAAMBGJFKAZ0BUVJQOHjyo8ePHq0mTJmldHTzlDAYpb35DWlcDAAAASJec0roCAJ7c3r17NX36dB04cEDe3t6J1s2ZM0dTpkzRkCFD1KxZszSqIdJK1VcdVKKkOSlyaH+8jh4yPbCswSBVruqgEqUNCtgab13u5CRVrekgr9wG5fM26MolkzavNyos1O7Vf2rUbih98JHU+YMHlyldVurga47z5SBp2ngpKvLe+kZNpDeamx8HbJV+nG/XKj9VAvf7a8XK2ZKkkiUrqPUHveTk5Jxs2aPH9mmr/xqdO3dCd+9GqcmbbdSwQQtJ0nfTh2mr/+okr3nxxVoa8Pkk+zUgHTKZTJo3b662bNkiSXr33XfVtOlbyZY9cuSIJk+eqPh4kwoWLKh+/frLw8PDun7t2l81e/YsBQcHq2zZsho69CvlzZs3VdqRXhFf+0qp+EZHR2nu3Lk6ffq0jh49qjJlyujzz/spX758qdaW9Ij4Ao9GjxTgGeDj4yNfX99k1507d06RkZG6dOlSKtcKae2lag4qV9FBP8wz6uclRtVt5KgSpZLvaZIpk9SynaNKlDbop8VGXTh7L+FSo46DjuyP16ofjVq2IE6FixnU9D3y8JKUOavU/XOp90DJ1fXB5fJ5SyMnS4tmS591lJycpf7D762vWVfq1Fsa1Evq01lq/LbUvJW9a/90OHnysL6Z+Ll6dBuhUSMW6OSpI1qw0C/ZshcuntaJE4fUo9sIjR/7o0qWrKjZc0YrMHCrJCkqKkL58xdJdHN2dlHDeu+mYovSh0WLFmr16lWaPft/mjx5svz8Jmjz5k1JygUFXZSvbw917NhJc+bMVWxsrL78cpB1fWDgPl25ckVjxoxVq1atFRgYqKVLf0jNpqRLxNe+Uiq+06dPV7NmzeXn942+/3669u7do379+qZmU9Il4gs8GokU4Bnh4JD8v/OwYcPk7++vHj16JFoeGRmpffv2pUbVnjpGo1Hbt29P62o8kUyZpGo+Djp7Ol5GoxQVJV26aJJPA8dky7/5rqMyZzHo5yVGxcbcW+7qau41ER5mfn4zWAq+blLe/Aa5eyS7qQzDyUn6qKu0dJ4UeuvhZdt0NN8f3Gu+3+UvvVJTKv+iuYdKB1/p6CFznGNjpMDdUusOkpu7PVvwdFi4ZKLy5S2k/PmLyGAw6KXKtbXhj5909drFJGV37vpdTZu0kyQZDAbVqN5IknTwcIAkqearjTXRb6X1Nmr4AuXMkVsVK1ZPvQalA+Hh4Zo1a6aqV68hFxcXZc+eQ+XLV9CkSZNkMiXutTZjxgxJ0ssvV5Uk1a5dWzt2bNf+/YGSpGzZsqtDh44qWbKU2rY1x75s2bKp2Jr0h/jaV0rFNzQ0VNmzZ7f27ilWrJiKFy+u48eP6+bNm6nbqHSE+AK2IZECPOMcHBySdAEOCgpSx44dtXv37hTdV1xcnG7fvp2i20xtISEh6t27t9auXZvWVXkixZ43yMXFoNDb95bdvmVS9hzm4TkJlShlUNHnHLQ3wKiYu4m3Ex0t7dsZn2hZpkwGxcSYFHnHTpV/SsTFSVPGSjdvPLyck5NUvY50/cq9ZVeCzPc+DaQXykt58klXLydYf0ny8JBerpHy9X6aBN+8qhMnDskr970hi3nyFFR8vFG7d29MUr7Fu13k7OxifX7nTrgkycsrvyTp5ZfqJiofsPM3vfRSHTk4JJ9gfFZt375dUVFRyp8/v3VZgQIFdPnyJR09etS6LDY2Vlu2bFbevHllMJg/N7y9C0iSNmzYIEkqUaKEtfyKFT+rT5++ql+/QWo0I90ivvaVUvHNli2b2rRpm2jb4eERcnNzU44cOVKhJekT8QVsQ99swM6WL1+uX375Rbdu3dLVq1dVq1YtjRo1Sg4ODpo1a5ZMJpNu376tn376Sb1791b79u0VHx+v//3vf1q1apWyZcum0NBQvfXWW+rYsaP1y+r48eOaOHGiTp06JS8vLxmNxmT3f+HCBQ0bNkydO3dW1apVNWXKFC1atEi3b9/WlStXtHfvXjk4OGj+/MebkGHHjh2aPHmyIiIiZDQaVaRIEZ09e1ZdunRR8+bNdfr0aa1bt06HDh1Sjhw5VLZsWfn7+yswMFBFixbVsGHD9Ndff2nfvn3avn27ihYtqjFjxuj555+XZE7KzJkzR6tWrZKTk5PCw8NVuHBhde3aVVWrVtWlS5e0cuVK69mRZs2aKSwsTJs23et62q5dO2XNmlVhYWHau3evZs2apQ4dOmjbtm367bff5Orqqq5du6p169aSpMWLF2vmzJm6evWqcuXKpTZt2kiSJk2apJw5cz7eHz6N5c5rfp/cvXvv7NHdf5MkefKZ5zmxKFvRnFMPvi61aOsoR0fp6KF4/XnQXCbhCai8+Q3KmcugPQHJv9+QlHchc8+ehImnOxHm+xKlpHNnzI8jI5Jf7580X5BhnD9/UpLk7pbZuszd3fz4n7N/Jyl/f0Jk+451cnfPrNq1mia7/a3+v6jDJwNTqrpPjZMnT0iSMme+F1fL47///lvlypWTJJ0/f1537959YDkLo9Go776bptWrV6l06dIqVaqUKlasZPd2pFfE175SMr6Ojvc+M44dO6YLF86rbdt2D+zlmxEQX8A2vIsBOxo7dqy++uorDR06VGvWrFGXLl20bds2zZ07Vz4+Ppo8ebK2bt2qX375RVFRUdqxY4ckyc/PTxMmTFCPHj20ZMkSlSxZUn5+fvrpp58kSUePHtX777+vP//8Uz/88IOWLl2qkiVLJtp3bGysOnTooEaNGikgIMC6vGfPnqpTp44kc/Jh4cKFj51E2blzpzp27KgsWbJozZo1mjlzpnbs2KHz589byxQvXlzBwcEKCAiQv7+/6tevr3nz5ql8+fI6efKk2rVrJ09PT02ePFmdO3fWsWPH5Od3b96DQYMGyc/PTxUqVNCaNWv0888/6/Tp0/roo4+0a9cueXt764033tDUqVM1depUXbp0SS+88EKiZWFh5vEos2bNUvfu3XXo0CFNmjRJPj4+mjhxoiIiIjRy5EidPXtWktS6dWu9+655roSaNWtq4cKFWrhw4VOXRJHMvUYkyZSgM0n8v7mPTPfN5ZE3v0HR0SZdDjJp2QKjzp0xqVETJz33fOKeK87OUqMmjjp7Ol4BWxL3UsGDZc5ivk+Y64yLM997ZEmwPj6Z9fd+n2ZIlh4lCX90Ozo6JVr3INu2/6qjx/bqs17jlT27Z5L1V69e0J3IcBUrWjoFa/x0iIiwxPXeQY6Tk1OidY9Tzt9/q0qXLq1+/frr5MlT8vXtqStXEnTBymCIr32ldHwl89UPR44crmrVqqtLl672qfhTgvgCtiGRAtjJmTNnNHfuXFWqVEnPPfecDAaDPvnkE+3bt09du3bVCy+8IMn8pbNu3TpVrVpVzZo105UrVzRv3jw5ODho8eLFatOmjY4dOyZvb29rl8rRo0crKipKb731lnLnzi1JqlevXqL9Ozs7a/jw4XJ92AyY/9E333yj+Ph4ffjhh3JyclLhwoVVpEiRJOW8vLwkmcfMFixYUJJUuXJlSVKZMmXUuHFjSVL58uUlSf/8848kcy+aVatWSZJatmwpScqZM6def/11GY1GTZpkvrpG0aJFk+yzWLFiSZb16dNHefLkkSQNHDhQ9evX16uvvqqSJUsqPj4+0Zm/Z0VMjLkbScIT9JbH9w/fcXVLvOzwfvMRfckX7n1FGAzSm+846sY1k1b9aFQ8eRSbWXqiOCXoA2p5fCfiEesz+PApdzfzRDxGY5x1meWxpWdKcv76K1CLf5isQV98p/LlX0m2zI6d6/VK1fopWNunh8e/Gbq4uHtxtTxOeHbZ1nJ169ZT/foN1LBhI3Xs2FFRUVHaufNeAj+jIb72ldLxjYuL08CBA1S8eAlNmOAnZ+fkrwiWURBfwDYM7QHsZMeOHTKZTNZEx/0sSYYqVarI09NTCxYskCStX79ecXFx8vT01KJFi5K87s6dO9q/f78kJeqF4u6edFbK/PnzK0eOHIqMjEyy7r+KiIiwJnQSJk+yZ8/+wNc4JThCdHFxSXQv3ev6aRmedOzYMeu6AgUKWB8XLlxYkvlSe/Hx8Y/VNdRSh4TdTC11iI6Otnk7T4sbV82JFFc3gyTz40yZzOuuXU08WVzkHSlBWBQdZb5PONFp/cYOun7VZL0sssEgZckqLoFsg6Dz5kRVlqz3lll6mpw5If1zyvz4QeszkkOHArRqzVzrc8ucJhERYdZlkf+OgSpapFSy2wi69I/mLZigr4bMVt68hSRJV66cV758hROV27N3k7p1/ipF6/+0sMy7EZbgHzgiwhzXhN8rhQsXlouLS7Llnn8+cS9Ii/z5zfPZPIufq7YivvaV0vEdN26snn++pLp27SbJfOB//fr1RHOEZCTEF7ANPVIAO7n774QUFy5ceGi5TJaj23/F/3uq/+bNm7p8+XKS8tHR0dZ5Qe5/bWqIioqy7j8+QbeElEzWJJQwWWKZH8ZkMiWZOR6JnTlpUmyMSTk97w3PyZ7DoODrJl2+aFLCE0JXLpnk7iG5/Pt2siRQblw3x/iFcgbFxcmaRJGkwsUMeu55vkIexjKEKjZW2ukv5ckva9zz/Zsf/G21dOywdOOaVDDBcX4+byn0thSwNTVrnPYqVqyhYUNmW2+NX2+lEiXK6fLls9Yy165dlKOjk2r7mOc9iYm595kYGxujGTOHq+9nftYkiiTNv+9yyTduXFZkZISKFk0+GfOsq1mzllxdXXXu3DnrsqCgIBUtWkwVKlRUVJQ5m+ri4qLatevo8uXLiokxX87r0iXzTMnNmjVLdtsXLpyXwWBQ1arJ9wTKCIivfaVkfNetWysXFxfrQb4k7du3Vzt2PN1X7nsSxBewDb+CATuxZO2PHDlinX1fMmfrT58+/cDXlS1b1powGDx4sMLDw62v++mnn+Tp6SkPD3N394sXk17+0xaWbpWWL8PH4enpae3JERRk/sKMi4t7ZMLocZQrV87ac+TSpUvW5ZYv9YoVK1rXW3qahISESEqc3PkvniQ26Ul0tLRzW7yKlTDIxUVyc5O8Cxq009+oEqUM8h3gpLIVze+zPTvMl0guW8H8lVCqjIOio00K3BUvBwfJp4GjXFwMqt/YQfUbO6jhm45q+KajoiJJZlk4OCYeRlWjjrRyi9Soifn5/BnmhEoN8/REquYj7Q2QTv4lxcdLMydJpctLufNKzi5SlVek5QuTDsPKiNp++JlCbt3QiZOHZDKZFLjfX/XqNlfOnLkVHHxFn3Sqq2nfD5Ykbdi4XNF3o7RqzVzNnD1SM2eP1NjxvZJcKvnwkV2qXKlmWjQnXciWLZs6duykgIAdioiI0O3bt3TkyGF16tRJmzdvko9PTa1evUqS1KVLV7m4uGjzZvNE3lu3blWNGjVUpkxZBQcHq2bNGvrf/2ZLkkJDQ7V8+TK1atVaxYsXT6vmpTnia18pFd/Y2FhNmjRJkZGRGj36a40e/bVGjhyhUaNGKlu2bGnYwrRFfAHbMLQHsJNatWqpYsWKOnTokHx9fVWxYkXlzp1bp06d0syZM5Pt1SFJhQoVUrNmzbRixQrt2LFDr7zyiry8vHTz5k3NnDlTknnekDlz5mjFihVq1aqVPDw8rImE+3tqWM4SJNyPZb6Sbdu2qXHjxjp//ryqV69u0+XoHBwc9Oabb2rFihVavXq1fHx8tH79euXMmVMRERGJeqZYuh7HxsZal1kSFJZ6SfeG9FjqWKBAAbVq1UoLFy7UkiVLNHr0aAUHB2vt2rVydnZW3759ra8tUqSITp8+LT8/Px05csQ6wawkXb9+3To06P59JKxXwvG9ltjs379fBw4cUGhoqIoWLZrsHDDp3b6d8TIYpBZtnSSZtGWDUSf/NqlocYNiYqTYf/8E166Y9PMSo+o2clSpsgYZjdLSeXGKvCMVKmqQR2aDylUyJNn+U55rShFlKkhvNJe8zFPwqM9gacVS6W60OT6WGF0Jkgb5Sp17S03fM1/qeMzge9vZvsncE2jwWPNVktavkX5KOrIvQyr5fEX1/cxP8+aPlwwGlSheTm0//EySeeJZNzcPububZ+zds3eTzp8/ab3az71tVEj0/K+/96t6tUap04B0qm3bdjIajeratbMMBoN69/5U9es3UEBAgNzd3eX2b9e0ggULasqUafr2Wz8tW7ZM+fPn18iRX0syJ9Zbtnxfy5b9KH//rXJ3d1f37j1Ut269h+06QyC+9pUS8T148KBCQm7ql1/WJNn+w4YrZwTEF3g0g4n+8YDdhIaGasKECdqwYYNiYmJUqVIlDRgwQOfOndPIkSN17do1eXl5qXXr1mrfvr3c3NwkmRMQEyZM0Lp16xQZGamSJUuqZ8+eevXVVyWZh9EMGzZM27dvl4eHh+rWrau4uDgtXrxY7u7u6tu3r+rXr6+vvvrKejngkiVLasSIEapQoYJu3bolX19fHTlyRN7e3urZs6def/11m9sVERGhoUOHatOmTXr55Zf11ltv6e7duxo5cqTy5MmjIUOGKCgoSN98841CQkKUKVMmvffee3r11VfVr18/hYWFydHRUS1btlTFihX13Xff6dy5c3JwcFDXrl3l6+uruLg4zZ49WytXrpSLi4uio6NVvHhx9ejRQ2XKlLHWZceOHRoyZIhu3rypV199VSNGjFDTpk31/PPPq0SJEmrWrJm+++47bdiwQSaTSXnz5tW0adO0du1aLVy4ULGxsSpUqJAGDBigevXqKSYmRp999pl27NghT09Pffzxx9bLI/9XE4bHProQHlvfIebeQ69VTeOKPKPW7zHfHz5gn2F7GV2FF80HImFhEY8oif8ia1bzREPE1z6Ir30RX/uyxDcjSqvfpJbfbM8SEilAOmAymazDef6r2NhYxcbGWiedjY+Pl8FgSLRdo9GYaLJVe4qJiZGzs7N1/9HR0XJwcFB8fLxcXFwUFxenuLi4JJPk3r17N03mfrEnEin2QSLFvkik2BeJFPviQNS+iK99EV/7IpGS+p7FRApDe4B04EmTKJJ5bo+El5RL7oo2D0ui+Pn5aePGjY/cT58+fdSgQYNHlkt4VR5JSS7D7OLikqSMlDYT6AIAAACArUikAJAk3bhxQ2fPnn1kOcvktwAAAACQEZFIASBJGjNmjMaMGZPW1QAAAACAdI3LHwMAAAAAANiIRAoAAAAAAICNSKQAAAAAAADYiEQKAAAAAACAjUikAAAAAAAA2IhECgAAAAAAgI1IpAAAAAAAANiIRAoAAAAAAICNSKQAAAAAAIBnQkhIiObMmWPXfTjZdesAAAAAAAD/QUhIiEaNGiUPDw8FBwerZ8+eKl26dJJydevW1aVLl6zPe/fubX0cEBCgRYsWydPTUw4ODho4cKBcXV2fqF4kUgAAAAAAQLrTp08fVapUSb6+vtq1a5c++ugjbdy4UVmyZElUrlKlSvr444+tzytXrixJunDhgrp166Zff/1VBQsWVL9+/TRq1CiNGDHiierF0B4AAAAAAJCuBAYGaufOnapWrZok6aWXXlJ4eLgWL16cpOyLL76oDz/80Hqz9FqZMWOGcuXKpYIFC0qSqlWrphUrViTqvfJf0CMFAAAAAADYRb169R66ftOmTcku37ZtmyTJ09NTkuTk5KTs2bNr69at6tKlS6Ky69at0zfffCMnJydVq1ZNAwcOVO7cueXv7y9vb29rOU9PT8XFxSkgIEAtWrT4z20ikQIAAAAAANKVkJAQSZKzs7N1mbOzs3V5Qg0aNNAnn3yiU6dO6fvvv9fNmze1cOFC3bp1S0WKFEn0ekm6efPmE9WNRAoAAAAAALCLB/U4eZQcOXJIkiIjI63LYmNjlTdv3iRl27dvL8k86ayDg4MmTJig8PBw5ciRI8nrpXu9XP4r5kgBAAAAAADpio+PjyTp+vXrkqS4uDiFhoaqVq1aD31diRIl5OLiIicnJ/n4+FhfL5l7uTg6Oqp69epPVDcSKQAAAAAAIF2pUqWKatasqR07dkiS9u/fLw8PD7Vo0UKdOnXSvHnzJEm///67Dhw4YH3d/v371alTJ7m5ualLly6KiIjQ8ePHJUm7d+/WW2+9pQIFCjxR3RjaAwAAAAAA0p1JkyZp6NChGjp0qK5du6bZs2fL3d1dp06dsl6JJzg4WN98843q168vb29veXt7WyeSLViwoObOnatvv/1WefLkkYODgwYPHvzE9SKRAgAAAAAA0h0PDw9NmDAhyfItW7ZYH7du3VqtW7d+4DYqVaqkGTNmpGi9GNoDAAAAAABgIxIpAAAAAAAANiKRAgAAAAAAYCMSKQAAAAAAADYikQIAAAAAAGAjEikAAAAAAAA2IpECAAAAAABgIxIpAAAAAAAANiKRAgAAAAAAYCOntK4AAAAAAACwr2Il4tO6Cs8MeqQAAAAAAADYiEQKAAAAAACAjUikAAAAAAAA2IhECgAAAAAAgI1IpAAAAAAAANjIYDKZTGldCQAAAAAAYD8rfribJvtt/kGmNNmvPdEjBQAAAAAAwEZOaV0BAHjWhYVFpHUVnklZs2aWJB0+EJnGNXk2VXjRXZL0WtU0rsgzav0e8z2fD/Zh+XwgvvZBfO2L+NqXJb7Ak6BHCgAAAAAAgI1IpAAAAAAAANiIRAoAAAAAAICNSKQAAAAAAADYiEQKAAAAAACAjUikAAAAAAAA2IhECgAAAAAAgI1IpAAAAAAAANiIRAoAAAAAAICNSKQAAAAAAADYiEQKAAAAAACAjUikAAAAAAAA2IhECgAAAAAAgI1IpAAAAAAAANiIRAoAAAAAAICNSKQAAAAAAADYiEQKAAAAAACAjUikAAAAAAAA2IhECgAAAAAAgI1IpAAAAAAAANiIRAoAAAAAAICNSKQAAAAAAADYiEQKAAAAAACAjUikAAAAAAAA2IhECgAAAAAAgI1IpAAAAAAAANiIRAoAAAAAAICNSKQAAAAAAADYiEQKAAAAAACAjUikAAAAAAAA2IhECgAAAAAAgI1IpAAAAAAAANiIRAoAAAAAAICNSKQAAAAAAADYiEQKAAAA8JS4c+eOTp8+ldbVAIAMzSmtK5CafvrpJx05ckRHjhxR6dKlNXr0aO3atUv+/v46duyYrl+/rt9//z2tqwlkeNeuXdPEiRO1Z88e3bp1S2+88YZGjhz5n7Z1+/ZtHT58WGvWrJGLi4tGjx6dwrVN30wmk+bNm6stW7ZIkt599101bfpWsmWPHDmiyZMnKj7epIIFC6pfv/7y8PCQyWRStWpVZTQak7xm7Nhxqlu3nl3b8DQI3O+vFStnS5JKlqyg1h/0kpOTc7Jljx7bp63+a3Tu3AndvRulJm+2UcMGLSRJ300fpq3+q5O85sUXa2nA55Ps14B0rnZD6YOPpM4fPLhM6bJSB1/JYJAuB0nTxktRkffWN2oivdHc/Dhgq/TjfLtWOd1Lic8GSQoPD1fdurUTlS9VqpQWLlxs1/o/DVIqxha3b9/WzJkzdOXKZfXp09fu9U/PUiq20dFRmjt3rk6fPq2jR4+qTJky+vzzfsqXL1+qtSW927Ztm+bMMX+/VahQUT179nzg91twcLBGjRqpO3ci5OjoqP79v1CRIkUkSV27dlZgYGCS17Ro0VKff97PbvUH7MVuPVJu3bqlhg0bqlmzZoqOjrbXbh7Lc889p23btunvv/+WyWSSJBUoUEAnTpzQ3r17FRsbm8Y1zNi6d++u6tWr6/jx42ldFaShW7du6Z133lGzZs00cuRIRUVFafXq1f/p/9NoNOq3337T0KFD9euvv1r/7zOSRYsWavXqVZo9+3+aPHmy/PwmaPPmTUnKBQVdlK9vD3Xs2Elz5sxVbGysvvxykCQpNDRUWbJkUeHCha23/Pnzy9PTU7Vq+aR2k9KdkycP65uJn6tHtxEaNWKBTp46ogUL/ZIte+HiaZ04cUg9uo3Q+LE/qmTJipo9Z7QCA7dKkqKiIpQ/f5FEN2dnFzWs924qtij9yJxV6v651Hug5Or64HL5vKWRk6VFs6XPOkpOzlL/4ffW16wrdeotDeol9eksNX5bat7K3rVP31Lis8EiW7ZsiT4fSpYsmVrNSNdSMsanTp3SBx+0lMFg0DffTFSBAgVTqxnpUkrFdvr06WrWrLn8/L7R999P1969e9SvX8ZOUiV05MgRDRjQT8OGDdfcufP1559H9O233yZbNi4uVr6+PVWiRAnNnDlb9es3UI8e3RQeHi5Jio6OTvQ5UbhwYTk4OKh583dSs0lAirFbIiUiIkJXr17V5cuX000ipVKlSnrllVcSLStYsKCaNGmSRjVCQqdPn9bt27d1+/bttK4K0tC0adMUHBysF198UdWrV9f06dM1a9YsOTsnf/bjYRwdHfXBBx/o7bffTvmKPgXCw8M1a9ZMVa9eQy4uLsqePYfKl6+gSZMmJUkqzZgxQ5L08stVJUm1a9fWjh3btX9/oO7evauJEyfrp59WWG9t27bTG2+8KSenDNWxMVkLl0xUvryFlD9/ERkMBr1UubY2/PGTrl67mKTszl2/q2mTdpIkg8GgGtUbSZIOHg6QJNV8tbEm+q203kYNX6CcOXKrYsXqqdegdMLJSfqoq7R0nhR66+Fl23Q03x/ca77f5S+9UlMq/6K5h0oHX+noISk8TIqNkQJ3S607SG7u9mxB+pVSnw0WHTp0TPT58OWXQ1KvMelUSsb41q1b6tWrp8qVK6++fT+XwWBI3cakMykV29DQUGXPnl158+aVJBUrVkzFixfX8ePHdfPmzdRtVDo1efJEFSxYSEWKmL/fatXy0c8//6SgoKTfb7/++qtOnTqp2rXrSJJ8fGrr2rVr+uGHJZKktm3bJfqcmDDBTy+88IKee+65VG0TkFLslkgpWLCgtm3bps2bNyt79uwpuu3Tp08rKCgoRbeZnhmNRm3fvj2tq6EDBw4oLCzMbttfvXq1tm/fniTZldLi4+O1d+9eu+4jNaWX90dKWb9+vdzc3KwH6LVr137i94Sjo2NKVO2ps337dkVFRSl//vzWZQUKFNDly5d09OhR67LY2Fht2bJZefPmtf5A9/YuIEnasGGD8uTJozJlyiTa9i+/rNHrrzdOhVakb8E3r+rEiUPyyu1tXZYnT0HFxxu1e/fGJOVbvNtFzs4u1ud37pjP1Hl5mf9GL79UN1H5gJ2/6aWX6sjBIeO9h+PipCljpZs3Hl7OyUmqXke6fuXesiv//kTwaSC9UF7Kk0+6ejnB+kuSh4f0co2Ur/fTIKU+GywyZ86cSjV/eqRkjGfPnqUbN26oe/ceGT6JIqVcbLNly6Y2bdom2nZ4eITc3NyUI0eOVGhJ+nb16lUdPnw4SZyNRqP++OOPJOUt71dL+Vy5csnV1VUbNpinTahTJ/H32y+//MLvCDzV7DrZbPbs2ZOM73xS+/bt08cff6xLly7ZVD4sLEwxMTEpWofUFBISot69e2vt2rWput/7M/EbNmxQp06d7JpIcXV1laenp922L5nn3ujatatWrFhh1/2klsd9f5hMJoWEhDyyXFr931y/fl03bjziqAk2O3nyhKTEBzmWx3///bd12fnz53X37t1HlrM4e/as7t69q+LFi9ul3k+T8+dPSpLc3e7Fzt3d/Pifs0ljd39CZPuOdXJ3z6zatZomu/2t/r/o1Rqvp1R1n0nehczDfiLv3Ft2J8J8X6KUVKyE+XFkRPLrM6KU/myYPXuWateupZo1a+jTT3sne7Y6o0mpGBuNRq1bt1aFCxfWuXPn1L59W/Xo0U2HDh1MjWakSyn5/k14ouXYsWO6cOG83nuvhRwcuB7HqVPm77fk4nf8eNLvtwf9Xc6fP6/IyMhEZY1Go37/fb3q12+Q4vUGUovd+mQbjUb9/PPP+uOPPzRjxgxdvHhRmzZt0urVqzV48GCNHz9ef//9t55//nmNGjVKzz//vCTzgdT48eN16tS92ciHDx+uIkWKaNSoUfr1118VFxenr7/+WlmzZlWZMmU0YMAASdKCBQu0ePFiZc6cWWFhYSpdurR27typVatWqUCBAjbX/fbt2/r555/1559/6syZM+rSpYu2bNmiDRs2KEeOHOratasKFCigNWvWaMeOHXJ1ddXnn3+u11+/92N306ZNmjFjhkwmk6KiolSpUiV9/vnnypo1q8LCwrR3717NmjVLHTp00LZt2/Tbb7/J1dVVXbt2VevWrSVJixcv1syZM3X16lXlypVLbdq0kSRNmjRJOXPmtLk9Fy9e1MCBA/XZZ59p8ODBun37thYvXqzChQsrODhY48aN0759+5QjRw7Fx8fLy8tL0dHRWrhwoS5duqQxY8ZYs8yfffaZMmXKJB8fH3Xo0EF3797V1KlTtWnTJmXLlk0RERFq37693nnn8cc77tmzR4MGDbJmuW/evKmdO3fq+++/1/Dhw7Vo0SL5+/srZ86c6tevnxo1avRY258+fbqmTZummJgYNW7cWKNHj7bOvVGpUiUNHjxYhQsXliSb2rV3715NnDhRvr6+6t27twoXLqwlS5bI0dFRly5d0qRJk3Tt2jUdPXpUuXPnVr9+/VSnjrm7Y2q8P/7880/5+fnp8uXLcnZ2lpeXl0JDQ1W3bl317NlTkrl3zuzZs7V8+XLlyJFDISEheuGFFxL93wQGBmrKlCkKCwuT0WhUsWLF9MUXXyhPnjyPFf+goCBNmjRJgYGBypEjh27duqWaNWuqd+/e1vfzX3/99VjbfJANGzZo5syZio6OVqZMmXT37t0U2e7TJiLC3Nsh4cG7paePZd3jlLNYv/43NWz4eP9/zypLj5KEP7odHZ0SrXuQbdt/1dFje9W/7yRlz540iXz16gXdiQxXsaKlU7DGz57MWcz3CedCjosz33tkSbA+Ppn1GbQjRUp+Njg7O6lVqw9Vvnw5HT9+XJMnT1Lnzp3044/LM3RPlZSK8YULFxQREaHKlSvLx8dHNWrUUO/evvL17amff14pLy8vu7clvbHHd1tUVJRGjhyuatWqq0uXrvap+FMmPNyccXZ0vPf9ZomfZV1CERGW8snFO0Lu7vfGUgYGBqpIkSKPdTwDpDd2Sbf+9ttvqlOnjgYPHqyoqCidP39eAwcO1JgxY3TmzBktWbJEX3zxhVq3bq0jR45o2LBh1td27dpVW7Zs0fLly7Vq1Sq98sorOnv2rLJmzaqxY8daD94GDhyohQsXWpMokydP1qhRo9SkSRP9/PPP6tq1q37//XfrBEePI3v27MqTJ49+++03nTx5Uk5OTpowYYI++ugjXb16VcOHD9fmzZs1btw4TZ06VZcuXdLgwYMV9+8vM39/f3Xr1k1lypTR8uXL9eGHH2rZsmUaMWKEJGnWrFnq3r27Dh06pEmTJsnHx0cTJ05URESERo4cqbNnz0qSWrdurXffNU8wWLNmTS1cuFALFy60+UPn+vXr6t69uxo1aqS9e/fqm2++0fnz53Xjxg0dO3ZMUVFRatOmjX7//Xf973//04oVK5QlSxZt27bNug1vb29NmTLF+vybb77RwoUL1aFDB0lS//79NXPmTI0ZM0Y//PCD3N3dNXDgQO3atcvmeJ8+fVpvvfWW2rZtq4sXzWexoqKi9M0336hv3746c+aM/Pz81KJFC40aNUpBQUEaMGDAY/9tu3TpYk1kBAYG6sUXX9TYsWOVOXNmbd++XR9//LG1F8bD2rV//369++67atOmjS5cuKDRo0fr1q1bOnTokG7duqWLFy/q3Xfflclk0vz587Vy5UplzpzZmiBKjffHqVOn1KZNG928eVMrV67U4sWL9ddffyVJVIwbN05+fn5q27atli1bpo8++ijR/83x48fVvn17ubq6asWKFfr888/122+/6dNPP32s2N+4cUMtWrTQmjVrNHbsWK1YsUK9e/fWjz/+qDZt2ig6Olp//vmnfvjhB+vff+3atVq7dq1Onz79WPtasmSJevbsKXd3d61evdq6zYzI49+jRMtnU8LHCQ9wbC1nsWHD76pXr37KV/gp5O5m7nVpNN6LneWxpWdKcv76K1CLf5isQV98p/Llkx+6tmPner1SlTg/iqUnSsLpeiyP70Q8Yn2CXiwZSUp+Nri6uqlFixYqVaq03n67mXr29NX169e1bZu/fRuRzqVUjC09gd3dzZ81Tk5Oatr0LUVFRSkgYIcdW5B+pfR3W1xcnAYOHKDixUtowgS//zQn27PIMqrA1t8GDyt//wgFfkfgWWCXREqjRo3UsGFD6/MiRYqoW7dukqScOXPKz89PFSpUsB4EHjt2zFr2zJkzCg8P19SpUxUUFKQOHTo8svv47du3NWvWLDk4OKh9+/aSzPMqPAlLhj9PnjzWng9VqlSRZO5tY0nglC9fXpJ54qvg4GBJsl5e9eDBg2rTpo0WL14sb29vay+bPn36JEoI1a9fX6+++qpKliyp+Pj4ZLvS/xe5c+fWkCFDrJcsffPNNzV+/Hi99NJLevnll7VixQr9888/8vHxUbFixSRJPj62X4Hj4MGD+u2335QpUyaNHz9eH374oW7evClvb28dOnTI5u0UL17c+v6wcHNz01dffWV9PmbMGFWvXl2NGzeWl5eXIiMjrQmFx2HJhteoUUONGjWSj4+PRo0aJcncY2Lbtm2PbFflypXVrp15ssgbN25o4sSJ6tSpk958803lypVL33zzjUJCQqzv70KFCmn58uXW/aTG+2Pq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VatWTWvWrNGePXsSlUupGEnSH3/8IV9fX3311Vd67733tGPHDn3xxRc6dOiQChcu/NA2vvfeezbFMTY29rFjMWvWLGvGedKkSerdu7caNWqkHj16aOTIkapevbqKFi0qSfr000+1Z88erVu3TlmzZtU333yjFStWqE+fPjIYDA+NVY4cOR77fZGWPP49gomzHNEkeJzwB6Wt5erWrWd9HBYWprFjx2jnzgC98867dqh9+kd87Ssl4+vq6qYWLVpIkkqVKi2TyaSvvx6lbdv81bgxQyQexdITxSnBL0bL4zsRj1ifoBdLRuLmZr5iidGY4H357+OEvVQSWrV6js6ePa4RX81TzpzJ97oM2Pm73m76UQrX9umUUgf7ZcqUVf783jpy5LBOnDiukiVL6fZt86RBOXPmtG8j0qmU+vwNDw/XvHlzNWHCN3avM9I/y3HWg2zalHR+I0natm2bJFmTnk5OTsqePbu2bt2qLl26SDInPffu3avXXnvN+jpPT0+Fhobq4MGD8vf3l7e3d6J1cXFxCggIsP4++C/okYInYummlzt3brVv315VqlTRlClT5Obmpri4OK1fv17t2pknT7xx44YmTpyoTp066c0331SuXLk0fvx4xcbG6sKFC2rbtq38/Pzk7e2tq1evKjIyUmfOnJEkzZw5U3/++afq1q2rBg3McwcsXLhQQUFB/15OrZvat2+vfPnyKUuWLDp+/LiqV6+upk3N4zF9fHzUs2dPlStXTg0bNpQkHTt2LMXi0LVrV73wwguSzP/g69atU9WqVdWsWTNduXJF8+bNk4ODgxYvXqw2bdro2LFj8vb21tGjRxUeHq7r16/r3LlzmjNnjkJCQtSrVy/lzZtXnp6eGjQo6eznc+bM0blz51SuXDlrhja5D6iUipHRaNTw4cPl6Oiot94yXyng1Vdf1fbt2/XWW289so22+i+x6NOnj/LkySNJGjhwoOrXr69XX31VJUuWVHx8vPXM1LZt2/T777+rTp068vT0lLOzs/r37689e/bI3d39kbF62ljm3QgLC7Uus5yBT3jFksKFC8vFxSXZcs8/n/yVTfLnN38ZZdSzdRLxtTd7xrdChYqSpODg4BSt87Mq6LwUc1fKkvXeMktPkzMnpH9OmR8/aH1GVLjQ85KkiIh778vIf8c+FSmSdLzTjoDf9Nff+zV82FxrEuXK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mvnz5zNs2DA+/fTTeHmdiIiIaJPJmt4uEvIu7qRJk6hcubJ5tZ6EZjAYEvyutC5EE4Yu9BOW2jdhKZGSsJRISVhKpCQsJVISlhIpiS8lJlI0tEckGZo5cyazZ89m8ODBtG/fnoCAAPbu3UuxYsVo3rz5S39+4MCBnDlz5qX1fvzxR0qWfDrhWlwSDmfOnGHgwIEvrVeyZEmqVq3KggULyJUrF48fP6ZPnz4Wv87rSs1d+0VEREREJO6USBFJhlq1asXFixeZMWMGmzdvJigoiFq1atGjRw/zEsQvcufOHYsmYo3rMsbP/qwlr+Hu7o6vry+XL1/G0dGRMWPGvPJrioiIiIiIJDQlUkSSoTx58jB79uxX/vmFCxfGYzSxq1ixonmFDEu0adMmAaMRERERERGJH1r+WERERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIWUSBERERERERERsZASKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIWUSBERERERERERsZASKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIWUSBERERERERERsZASKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIWUSBERERERERERsZASKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYyMZgMBisHYSIiIiIiIiIJJw1S0Os8rpNW6WxyusmJPVIERERERERERGxkL21AxARSel8ff2tHUKKlC6dK6D2TShq34Sl9k1YpvY9fSLQypGkTKXKugDwfkUrB5JCbTls/K7zN2GYzl+R16EeKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCyk5Y9FREREREREUrh/L6sfRXxRS4qI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fQYOHGjRZMc//vgjJUtqoj4RkfiQmlc/EhGR1ClRlj9OTW7evBlt8tjniTovxKt4/PjxcycIfbZeSvLmm2/yxRdfsHnzZho2bEjatGlJnz49K1eu5O2332bp0qUWtYufn18iRJt4goKCLDru+LhbaMnrWMudO3csii8+J78VEREREZHUJcGH9oiIpHYpoet+UpSShkYkRWrfhKX2TVga2pOwNLQnYWloT8LS0J7ElxKH9iSZ5Y9FRERERERERJI6JVJERERERERERCykRIqIiIiIiIiIiIWUSBERERERERERsZASKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCxkb+0ARERERERERCRh/XvJztohpBjqkSIiIiIiIiIiYiElUkRERERERERELKREioiIiIiIiIiIhZRIERERERERERGxkBIpIiIiIiIiIiIWUiJFRERERERERMRCSqSIiIiIiIiIiFhIiRQREREREREREQspkSIiIiIiIiIiYiElUkRERERERERELKREioiIiIiIiIiIhZRIERERERERERGxkBIpIiIiIiIiIiIWUiJFRERERERERMRCSqSIiIiIiIiIiFhIiRQREREREREREQspkSIiIiIiIiIiYiElUkRERERERERELKREioiIiIiIiIiIhZRIERERERERERGxkBIpIiIiIiIiIiIWUiJFRERERERERMRCSqSIiIiIiIiIiFjIxmAwGKwdhIiIiIiIiIgknO6fRVrldX9dlPL6b6S8IxIRERERERERSSD21g5ARCSl8/X1t3YIKVK6dK6A2jehqH0Tlto3YZna9/SJQCtHkjKVKusCqH0Tiql9369o5UBSqC2HrR2BpATqkSIiIiIiIiIiYiElUkRERERERERELKREioiIiIiIiIiIhZRIERERERERERGxkBIpIiIiIiIiIpIqXb9+nRUrVsTpZ7Rqj4iIiIiIiIgkef/88w9Tp07F3d2dwMBAhgwZQsaMGWPUK1OmDIGB0VcWe+utt/j9998BKFy4cLRtP/30U5ziUCJFRERERERERJI0X19fOnTowKRJk6hUqRKTJ0+mX79+zJkzJ1q9oKAgqlevTpEiRcxlO3bsoF27dubntWrVonr16ubnZcqUiVMsSqSIiIiIiIiISJK2aNEi/P39KV++PACVK1fm119/5cSJE5QtW9Zcz87OjuHDh5MpUyYAwsLC2Lx5Mx988IG5To0aNfj0009fORYlUkREREREREQkQdSuXfuF2//880+L9rN3714yZMiAvb0xjZE5c2YA9uzZEy2R4ujoaE6iAGzbto1q1arh6OhoLpszZw4//fQTrq6ueHh4MHDgQFxdXS0+JiVSRERERERERCRJe/ToEQ4ODubnpsTIw4cPX/hzy5YtY/jw4dHKPv74Y4oUKcLJkyeZPXs2oaGhjB071uJYlEgRERERERERkQRhaY+Tl8mYMSPXrl0zPw8NDQWe9kyJzfXr1wkODubNN9+MVt69e3cAPDw88PPzY9OmTXGKRYkUEREREREREUlSDhw4wNq1a83Pq1evztmzZwkKCsLZ2ZlHjx6Zy5/n999/p1GjRi98nUKFCpE2bdo4xaZEioiIiIiIiIgkKVWrVqVq1arm535+fixdupS//vqL2rVrc+jQISpWrEj58uX5999/+eabb/jqq6+oUKGC+We2bdvG7Nmzo+131apVlC5dmoIFCwJw/PhxevbsGafYlEgRERERERERkSTNzc2NRYsWMW7cOPbt28eTJ0+YNGkSYEyyXL58mQcPHpjr37lzBxcXF9zd3aPtx8vLi1mzZvHhhx/i5uZGnTp1qF+/fpxisTEYDIbXPiIREXkuX19/a4eQIqVLZ5xZXe2bMNS+CUvtm7BM7Xv6RKCVI0mZSpV1AdS+CcXUvu9XtHIgKdSWw9aOwHq6fxZpldf9dZGtVV43IaW8IxIRERERERERSSBKpIiIiIiIiIiIWEiJFBERERERERERCymRIiIiIiIiIiJiISVSREREREREREQspESKiIiIiIiIiIiFlEgREREREREREbGQEikiIiIiIiIiIhZSIkVERERERERExEJKpIiIiIiIiIiIWEiJFBERERERERERCymRIiIiIiIiIiJiISVSREREREREREQspESKiIiIiIiIiIiF7K0dgIiIiIiIiIgkrH8vqx9FfFFLioiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIWS3ao9/v7+zJ49m8uXL3P58mW6d+9O48aNrR2WxSIjI/H09OTAgQMcO3aM4sWLM27cON544w1r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GzduMGXKFLy9vQFjD5Fbt27RqFEj87LF8enu3bvMmfN0DPnUqVM5fvw4LVu2JHPmzK+9/8Rq3/jy5ptv8sUXX7B582YaNmxI2rRpSZ8+PStXruTtt99m6dKlFv0eTUs0JxeJ9Xuy9HVu375tnt8IoH///hQvXpz27du/1uuLiIiIiIhYyuKhPWJdpl9T1LvqsQ0Jic/Xe/YOfmzDY0QS07OTxkLyGAalrvsJQ0MjEpbaN2GpfROWhvYkLA3tSVga2pOwUuIwE0tpaE/8UY+UZCK25EVCJjVi229CJGxE4uJ1h2iJiIiIiIi8Ll0Zi4iIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIWUSBERERERERERsZASKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIWUSBERERERERERsZASKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIWUSBERERERERERsZASKSIiIiIiIiIiFlIiRURERERERETEQkqkiIiIiIiIiIhYSIkUERERERERERELKZEiIiIiIiIiImIhJVJERERERERERCykRIqIiIiIiIiIiIXsrR2AiIiIiIiIiIglzp07x+DBgxkyZAgVK1aMtU5kZCQTJkzA29uboKAg6tWrR8OGDc3b582bx6lTp7CxsaFUqVK0b98+TjEokSIiIiIiIiIiSZqvry8LFizg/PnzXLx48YV1Z8yYwalTp1i0aBG+vr54eHiQPXt2ypcvz4YNG1i8eDGbN28G4P333ydz5sx8+OGHFseioT0iIiIiIiIikqT5+fnRunVrateu/cJ6AQEBzJkzh0qVKgGQLl06ihYtyq+//orBYGDatGmUL18ee3t77O3teeedd5g2bVqcYlGPFBGRBJYunau1Q0jR1L4JS+2bsNS+CatUWRdrh5CiqX0T1pbD1o5AJH68LPHx559/WrSfXLlyWVTv5MmT+Pr6kiVLFnNZlixZ2Lp1KxcvXuT69evUq1fPvC1z5sxcu3aNa9eukT9/foteQ4kUERERERERkRTOWsm5l+RR4t2jR48AcHBwMJc5ODgQERFBQEBArNsAHj58qESKiIiIiIiIiFiXpT1O4kvGjBkBCAwMNJeFhYVhZ2dH2rRpY90Gxp4pllIiRURERERERESSlAMHDrB27Vrz86+++oqcOXO+9OfKlClDunTp8Pb2Npc9fPiQChUqULhwYfLlyxdjW758+ciXL5/FsWmyWRERERERERFJUqpWrcrPP/9s/npREuXff//lk08+4ciRI7i6utKpUycOHDiAwWDA39+ff/75h+7du2NjY0Pv3r05cuQIISEhhIeHc/z4cfM2S6lHioiIiIiIiIgkeevWrWP58uUAzJ07l8jISCpXroyfnx+XL1/mwYMHAHTu3JnQ0FAGDhxIcHAwQ4cOpUKFCgA0aNAAf39/BgwYgL29PS1atKBJkyZxisPGYDAY4vXIRERERERERERSKA3tERERERERERGxkBIpIiIiIiIiIiIWUiJFRERERERERMRCSqSIiIiIiIiIiFhIiRQREREREREREQspkSIiIiIiIiIiYiElUkRERERERERELKREioiIiIiIiIiIhZRIERERERERERGxkBIpIiKpXGhoKNu2bWPWrFksWbKE69evWzukFGfPnj14enqyYMECrl69au1wUgSDwcDKlStjLd+yZYsVIkoZ9u3bZ1G9PXv2JHAkqYO/vz8bN27E09OTxYsXc+XKFWuHlGIsWLAg1vKDBw8SHh6eyNGkPGpfSe3srR2AiIhYz9mzZ+nVqxd37twxl9na2vL5558zYMAAK0aWMvj5+dGpUydOnTplLrOxsaF9+/YMGjTIeoGlADY2NkydOpXIyEgcHR3N5f/88w/btm3jvffew9bWeL/o5s2buLm5kSFDBitFm3xMnz6dBw8evLBOYGAgv/32G3/++Sdp0qRJpMhSnjNnzvDFF1/g5+cXrbxx48aMGjUKe3t9TH8dnp6ePHr0KFo7hoeHs2HDBqZNm8bbb79txeiSP7WvpHY2BoPBYO0gRETEOlq0aMHZs2cBcHZ2JiwsjPDwcAwGA99//z0tW7a0coTJ27Bhw8y9JkqWLEmePHm4f/8+J06cYNiwYTRv3tzKESZvRYsW5XkfY0aMGEGLFi3w9fWlevXqZMuWjW3btiVyhMnPi9r0WZ06daJfv34JHFHK1ahRIy5fvkzm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
Download .txt
gitextract_wka03khu/

├── .gitignore
├── .readthedocs.yaml
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs/
│   ├── Makefile
│   ├── make.bat
│   ├── requirements.txt
│   └── source/
│       ├── CNAME
│       ├── conf.py
│       ├── index.rst
│       ├── install.html
│       ├── quickstart.md
│       └── scorecardpipeline.rst
├── examples/
│   ├── auto_report.ipynb
│   ├── automl_example.ipynb
│   ├── cycle_end_vintage.sql
│   ├── gbm_model_examples.ipynb
│   ├── loan_replan.py
│   ├── model_report/
│   │   ├── auto_eda.html
│   │   ├── 三方数据测试报告.xlsx
│   │   ├── 决策树组合策略挖掘.xlsx
│   │   └── 评分卡模型报告.xlsx
│   ├── month_end_vintage.sql
│   ├── profitability_calculation.py
│   ├── quickstart.ipynb
│   ├── rule_efficiency.ipynb
│   ├── rule_extraction.ipynb
│   ├── rule_test.py
│   ├── scorecard.pmml
│   ├── scorecard_samples.ipynb
│   ├── third_party_data_describe.ipynb
│   ├── 同盾百融三方数据评分卡建模样例.ipynb
│   ├── 数据分布情况.xlsx
│   ├── 权益类产品盈利测算表.xlsx
│   └── 特征选择.ipynb
├── requirements.txt
├── scorecardpipeline/
│   ├── __init__.py
│   ├── auto_eda.py
│   ├── auto_report.py
│   ├── excel_writer.py
│   ├── feature_engineering.py
│   ├── feature_selection.py
│   ├── financial.py
│   ├── germancredit.csv
│   ├── logger.py
│   ├── model.py
│   ├── processing.py
│   ├── rule.py
│   ├── rule_extraction.py
│   ├── scorecard.py
│   ├── template.xlsx
│   └── utils.py
└── setup.py
Download .txt
SYMBOL INDEX (325 symbols across 16 files)

FILE: examples/loan_replan.py
  function get_EPEI (line 27) | def get_EPEI(num):
  function get_MIRD (line 46) | def get_MIRD(num):
  function get_MCEP (line 65) | def get_MCEP(num):
  function get_MCEI (line 85) | def get_MCEI(num):
  function get_MCAT (line 106) | def get_MCAT(num, pay):
  function get_BFTO (line 120) | def get_BFTO(num):

FILE: examples/rule_test.py
  function cv (line 22) | def cv(x):
  function cv_type (line 31) | def cv_type(x):
  function get_trend (line 43) | def get_trend(data, method=np.mean, key='', flag=['高', '低']):
  function calculate_var_lift (line 106) | def calculate_var_lift(data, data_sub, hit_flag, target, cut_point, dire...
  function calculate_var_odds (line 127) | def calculate_var_odds(data, data_sub, hit_flag, target, cut_point, dire...
  function get_month_odds (line 157) | def get_month_odds(data, rule_name, rule_type, cut_point, target, rule_l...
  function get_weeks_hit (line 445) | def get_weeks_hit(data, rule_name, rule_type, cut_point, target, rule_li...
  function get_days_hit (line 603) | def get_days_hit(data, rule_name, rule_type, cut_point, target, rule_lim...
  function get_mths_result (line 761) | def get_mths_result(data, rule_name, rule_type, var, cut_point, target, ...
  function get_weeks_result (line 813) | def get_weeks_result(data, rule_name, rule_type, var, cut_point, target,...
  function get_days_result (line 862) | def get_days_result(data, rule_name, rule_type, var, cut_point, target, ...
  function rule_combine_results (line 903) | def rule_combine_results(data, rules_dict, rule_all, use_credit_flag, ci...
  function std_result_output_01 (line 1065) | def std_result_output_01(wb, sheetname, data, offset):
  function rule_combine_results_01 (line 1200) | def rule_combine_results_01(data, rule_all, use_credit_flag, cut_point, ...
  function get_same_len (line 1351) | def get_same_len(x):
  function get_max_len (line 1369) | def get_max_len(data, nrows, ncols, max_len=60):
  function description_output (line 1398) | def description_output(wb, sheetname, df1, df2, offset, base_lift):
  function add_combine_plot (line 1546) | def add_combine_plot(wb, ws, column, label_x_start=1 + 6, x_start=1 + 6 ...
  function add_plot (line 1605) | def add_plot(wb, ws, column, label_x_start=1 + 6, x_start=1 + 6 + 1, x_e...
  function std_result_output (line 1631) | def std_result_output(data, freq):

FILE: scorecardpipeline/auto_eda.py
  function auto_eda_sweetviz (line 18) | def auto_eda_sweetviz(all_data, target=None, save="model_report/auto_eda...

FILE: scorecardpipeline/auto_report.py
  function auto_data_testing_report (line 23) | def auto_data_testing_report(data: pd.DataFrame, features=None, target="...

FILE: scorecardpipeline/excel_writer.py
  class ExcelWriter (line 30) | class ExcelWriter:
    method __init__ (line 32) | def __init__(self, style_excel=None, style_sheet_name="初始化", mode="rep...
    method add_conditional_formatting (line 65) | def add_conditional_formatting(self, worksheet, start_space, end_space...
    method set_column_width (line 77) | def set_column_width(worksheet, column, width):
    method set_number_format (line 88) | def set_number_format(worksheet, space, _format):
    method set_freeze_panes (line 107) | def set_freeze_panes(self, worksheet, space):
    method get_sheet_by_name (line 122) | def get_sheet_by_name(self, name):
    method move_sheet (line 136) | def move_sheet(self, worksheet, offset: int = 0, index: int = None):
    method insert_hyperlink2sheet (line 152) | def insert_hyperlink2sheet(self, worksheet, insert_space, hyperlink=No...
    method insert_value2sheet (line 192) | def insert_value2sheet(self, worksheet, insert_space, value="", style=...
    method insert_pic2sheet (line 248) | def insert_pic2sheet(self, worksheet, fig, insert_space, figsize=(600,...
    method insert_rows (line 271) | def insert_rows(self, worksheet, row, row_index, col_index, merge_rows...
    method insert_df2sheet (line 319) | def insert_df2sheet(self, worksheet, data, insert_space, merge_column=...
    method merge_cells (line 481) | def merge_cells(self, worksheet, start, end):
    method check_contain_chinese (line 527) | def check_contain_chinese(check_str):
    method astype_insertvalue (line 543) | def astype_insertvalue(value, decimal_point=4):
    method calc_continuous_cnt (line 559) | def calc_continuous_cnt(list_, index_=0):
    method itlubber_border (line 592) | def itlubber_border(border, color, white=False):
    method get_cell_space (line 617) | def get_cell_space(space):
    method calculate_rgba_color (line 646) | def calculate_rgba_color(hex_color, opacity, prefix="#"):
    method init_style (line 660) | def init_style(self, font, fontsize, theme_color):
    method save (line 749) | def save(self, filename, close=True):
  function dataframe2excel (line 787) | def dataframe2excel(data, excel_writer, sheet_name=None, title=None, hea...

FILE: scorecardpipeline/feature_engineering.py
  class NumExprDerive (line 13) | class NumExprDerive(BaseEstimator, TransformerMixin):
    method __init__ (line 24) | def __init__(self, derivings=None):
    method fit (line 30) | def fit(self, X, y=None):
    method _check_keywords (line 35) | def _check_keywords(self):
    method _get_context (line 51) | def _get_context(X, feature_names=None):
    method _transform_frame (line 56) | def _transform_frame(self, X):
    method _transform_ndarray (line 72) | def _transform_ndarray(self, X):
    method transform (line 83) | def transform(self, X):
    method _more_tags (line 88) | def _more_tags(self):

FILE: scorecardpipeline/feature_selection.py
  class SelectorMixin (line 43) | class SelectorMixin(BaseEstimator, TransformerMixin):
    method __init__ (line 45) | def __init__(self):
    method transform (line 51) | def transform(self, x):
    method __call__ (line 55) | def __call__(self, *args, **kwargs):
    method fit (line 59) | def fit(self, x, y=None):
  class TypeSelector (line 63) | class TypeSelector(SelectorMixin):
    method __init__ (line 65) | def __init__(self, dtype_include=None, dtype_exclude=None, exclude=None):
    method fit (line 71) | def fit(self, x: pd.DataFrame, y=None, **fit_params):
  class RegexSelector (line 97) | class RegexSelector(SelectorMixin):
    method __init__ (line 98) | def __init__(self, pattern=None, exclude=None):
    method fit (line 106) | def fit(self, x: pd.DataFrame, y=None, **fit_params):
  function value_ratio (line 127) | def value_ratio(x, value):
  function mode_ratio (line 134) | def mode_ratio(x, dropna=True):
  class NullSelector (line 142) | class NullSelector(SelectorMixin):
    method __init__ (line 144) | def __init__(self, threshold=0.95, missing_values=np.nan, exclude=None...
    method fit (line 155) | def fit(self, x: pd.DataFrame, y=None):
  class ModeSelector (line 174) | class ModeSelector(SelectorMixin):
    method __init__ (line 176) | def __init__(self, threshold=0.95, exclude=None, dropna=True, n_jobs=N...
    method fit (line 188) | def fit(self, x: pd.DataFrame, y=None):
  class CardinalitySelector (line 207) | class CardinalitySelector(SelectorMixin):
    method __init__ (line 218) | def __init__(self, threshold=10, exclude=None, dropna=True):
    method fit (line 224) | def fit(self, x, y=None, **fit_params):
  function IV (line 242) | def IV(x, y, regularization=1.0):
  function _IV (line 270) | def _IV(x, y, regularization=1.0, n_jobs=None):
  class InformationValueSelector (line 281) | class InformationValueSelector(SelectorMixin):
    method __init__ (line 283) | def __init__(self, threshold=0.02, target="target", regularization=1.0...
    method fit (line 297) | def fit(self, x: pd.DataFrame, y=None):
  function LIFT (line 324) | def LIFT(y_pred, y_true):
  class LiftSelector (line 353) | class LiftSelector(SelectorMixin):
    method __init__ (line 363) | def __init__(self, target="target", threshold=3.0, n_jobs=None, method...
    method fit (line 379) | def fit(self, x: pd.DataFrame, y=None, **fit_params):
  class VarianceSelector (line 411) | class VarianceSelector(SelectorMixin):
    method __init__ (line 414) | def __init__(self, threshold=0.0, exclude=None):
    method fit (line 422) | def fit(self, x, y=None):
  function VIF (line 454) | def VIF(x, n_jobs=None, missing=-1):
  class VIFSelector (line 464) | class VIFSelector(SelectorMixin):
    method __init__ (line 466) | def __init__(self, threshold=4.0, exclude=None, missing=-1, n_jobs=None):
    method fit (line 483) | def fit(self, x: pd.DataFrame, y=None):
  class CorrSelector (line 500) | class CorrSelector(SelectorMixin):
    method __init__ (line 501) | def __init__(self, threshold=0.7, method="pearson", weights=None, excl...
    method fit (line 512) | def fit(self, x: pd.DataFrame, y=None):
  function _psi_score (line 584) | def _psi_score(expected, actual):
  function PSI (line 601) | def PSI(train, test, n_jobs=None, verbose=0, pre_dispatch='2*n_jobs'):
  class PSISelector (line 607) | class PSISelector(SelectorMixin):
    method __init__ (line 609) | def __init__(self, threshold=0.1, cv=None, method=None, exclude=None, ...
    method fit (line 623) | def fit(self, x: pd.DataFrame, y=None, groups=None):
  class NullImportanceSelector (line 658) | class NullImportanceSelector(SelectorMixin):
    method __init__ (line 660) | def __init__(self, estimator, target="target", threshold=1.0, norm_ord...
    method _feature_score_v0 (line 671) | def _feature_score_v0(actual_importances, null_importances):
    method _feature_score_v1 (line 675) | def _feature_score_v1(actual_importances, null_importances):
    method _feature_score_v2 (line 681) | def _feature_score_v2(actual_importances, null_importances):
    method fit (line 685) | def fit(self, x: pd.DataFrame, y=None):
  class TargetPermutationSelector (line 742) | class TargetPermutationSelector(NullImportanceSelector):
    method __init__ (line 744) | def __init__(self, estimator, target="target", threshold=1.0, norm_ord...
  class ExhaustiveSelector (line 748) | class ExhaustiveSelector(SelectorMixin, MetaEstimatorMixin):
    method __init__ (line 774) | def __init__(self, estimator, min_features=1, max_features=1, scoring=...
    method _validate_params (line 795) | def _validate_params(self, x, y):
    method _calc_score (line 808) | def _calc_score(estimator, x, y, indices, groups=None, scoring=None, c...
    method ncr (line 828) | def ncr(n, r):
    method _calc_confidence (line 843) | def _calc_confidence(scores, confidence=0.95):
    method fit (line 848) | def fit(self, X, y, groups=None, **fit_params):
    method _get_support_mask (line 898) | def _get_support_mask(self):
  class BorutaSelector (line 903) | class BorutaSelector(SelectorMixin):
    method __init__ (line 905) | def __init__(self):
  class MICSelector (line 910) | class MICSelector(SelectorMixin):
  class FeatureImportanceSelector (line 914) | class FeatureImportanceSelector(SelectorMixin):
  class StabilitySelector (line 918) | class StabilitySelector(SelectorMixin):
  class REFSelector (line 922) | class REFSelector(SelectorMixin):
  class SequentialFeatureSelector (line 926) | class SequentialFeatureSelector(SelectorMixin):

FILE: scorecardpipeline/financial.py
  class NoRealSolutionError (line 31) | class NoRealSolutionError(Exception):
  class IterationsExceededError (line 35) | class IterationsExceededError(Exception):
  function _convert_when (line 39) | def _convert_when(when):
  function _return_ufunc_like (line 50) | def _return_ufunc_like(array):
  function _is_object_array (line 59) | def _is_object_array(array):
  function _use_decimal_dtype (line 63) | def _use_decimal_dtype(*arrays):
  function _to_decimal_array_1d (line 67) | def _to_decimal_array_1d(array):
  function _to_decimal_array_2d (line 71) | def _to_decimal_array_2d(array):
  function _get_output_array_shape (line 76) | def _get_output_array_shape(*arrays):
  function fv (line 80) | def fv(rate, nper, pmt, pv, when='end'):
  function pmt (line 187) | def pmt(rate, nper, pv, fv=0, when='end'):
  function nper (line 281) | def nper(rate, pmt, pv, fv=0, when='end'):
  function _value_like (line 356) | def _value_like(arr, value):
  function ipmt (line 363) | def ipmt(rate, per, nper, pv, fv=0, when='end'):
  function _rbl (line 475) | def _rbl(rate, per, pmt, pv, when):
  function ppmt (line 486) | def ppmt(rate, per, nper, pv, fv=0, when='end'):
  function pv (line 514) | def pv(rate, nper, pmt, fv=0, when='end'):
  function _g_div_gp (line 615) | def _g_div_gp(r, n, p, x, y, w):
  function rate (line 635) | def rate(
  function irr (line 730) | def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=...
  function _npv_native (line 860) | def _npv_native(rates, values, out):
  function _npv_decimal (line 871) | def _npv_decimal(rates, values, out):
  function npv (line 880) | def npv(rate, values):
  function mirr (line 1001) | def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):

FILE: scorecardpipeline/logger.py
  function init_logger (line 7) | def init_logger(filename=None, stream=True, fmt="[ %(asctime)s ][ %(leve...

FILE: scorecardpipeline/model.py
  class ITLubberLogisticRegression (line 28) | class ITLubberLogisticRegression(LogisticRegression):
    method __init__ (line 30) | def __init__(self, target="target", penalty="l2", calculate_stats=True...
    method fit (line 84) | def fit(self, x, sample_weight=None, **kwargs):
    method decision_function (line 147) | def decision_function(self, x):
    method corr (line 163) | def corr(self, data, save=None, annot=True):
    method report (line 172) | def report(self, data):
    method summary (line 184) | def summary(self):
    method summary2 (line 238) | def summary2(self, feature_map={}):
    method convert_sparse_matrix (line 253) | def convert_sparse_matrix(x):
    method plot_weights (line 260) | def plot_weights(self, save=None, figsize=(15, 8), fontsize=14, color=...
  class ScoreCard (line 302) | class ScoreCard(toad.ScoreCard, TransformerMixin):
    method __init__ (line 304) | def __init__(self, target="target", pdo=60, rate=2, base_odds=35, base...
    method fit (line 334) | def fit(self, x):
    method transform (line 366) | def transform(self, x):
    method _check_rules (line 375) | def _check_rules(self, combiner, transer):
    method score_clip (line 412) | def score_clip(score, clip=50):
    method scorecard_scale (line 424) | def scorecard_scale(self):
    method format_bins (line 443) | def format_bins(self, bins, index=False, ellipsis=None, decimal=4):
    method scorecard_points (line 487) | def scorecard_points(self, feature_map={}):
    method scorecard2pmml (line 500) | def scorecard2pmml(self, pmml: str = 'scorecard.pmml', debug: bool = F...
    method KS_bucket (line 591) | def KS_bucket(y_pred, y_true, bucket=10, method="quantile"):
    method KS (line 604) | def KS(y_pred, y_true):
    method AUC (line 615) | def AUC(y_pred, y_true):
    method perf_eva (line 626) | def perf_eva(y_pred, y_true, title="", plot_type=["ks", "roc"], save=N...
    method ks_plot (line 650) | def ks_plot(score, y_true, title="", fontsize=14, figsize=(16, 8), sav...
    method PSI (line 664) | def PSI(y_pred_train, y_pred_oot):
    method perf_psi (line 674) | def perf_psi(y_pred_train, y_pred_oot, y_true_train, y_true_oot, keys=...
    method score_hist (line 699) | def score_hist(score, y_true, figsize=(15, 10), bins=20, save=None, **...
    method _format_rule (line 711) | def _format_rule(self, rule, decimal=4, **kwargs):
    method class_steps (line 724) | def class_steps(pipeline, query):
    method feature_bin_stats (line 734) | def feature_bin_stats(self, data, feature, rules={}, method='step', ma...

FILE: scorecardpipeline/processing.py
  function drop_identical (line 24) | def drop_identical(frame, threshold=0.95, return_drop=False, exclude=Non...
  function drop_corr (line 62) | def drop_corr(frame, target=None, threshold=0.7, by='IV', return_drop=Fa...
  function select (line 131) | def select(frame, target='target', empty=0.95, iv=0.02, corr=0.7, identi...
  class FeatureSelection (line 175) | class FeatureSelection(TransformerMixin, BaseEstimator):
    method __init__ (line 177) | def __init__(self, target="target", empty=0.95, iv=0.02, corr=0.7, exc...
    method fit (line 204) | def fit(self, x, y=None):
    method transform (line 230) | def transform(self, x, y=None):
  class StepwiseSelection (line 240) | class StepwiseSelection(TransformerMixin, BaseEstimator):
    method __init__ (line 242) | def __init__(self, target="target", estimator="ols", direction="both",...
    method fit (line 273) | def fit(self, x, y=None):
    method transform (line 293) | def transform(self, x, y=None):
  class FeatureImportanceSelector (line 303) | class FeatureImportanceSelector(BaseEstimator, TransformerMixin):
    method __init__ (line 305) | def __init__(self, top_k=126, target="target", selector="catboost", pa...
    method fit (line 325) | def fit(self, x, y=None):
    method transform (line 351) | def transform(self, x, y=None):
    method catboost_selector (line 360) | def catboost_selector(self, x, y, cat_features=None):
  class Combiner (line 394) | class Combiner(TransformerMixin, BaseEstimator):
    method __init__ (line 396) | def __init__(self, target="target", method='chi', empty_separate=True,...
    method update (line 429) | def update(self, rules):
    method optbinning_bins (line 441) | def optbinning_bins(feature, data=None, target="target", min_n_bins=2,...
    method fit (line 490) | def fit(self, x: pd.DataFrame, y=None):
    method check_rules (line 528) | def check_rules(self, feature=None):
    method transform (line 548) | def transform(self, x, y=None, labels=False):
    method export (line 558) | def export(self, to_json=None):
    method load (line 567) | def load(self, from_json):
    method feature_bin_stats (line 578) | def feature_bin_stats(cls, data, feature, target="target", rules=None,...
    method bin_plot (line 726) | def bin_plot(self, data, x, rule={}, desc="", result=False, save=None,...
    method proportion_plot (line 744) | def proportion_plot(self, data, x, transform=False, labels=False, keys...
    method corr_plot (line 758) | def corr_plot(self, data, transform=False, figure_size=(20, 15), save=...
    method badrate_plot (line 771) | def badrate_plot(self, data, date_column, feature, labels=True):
    method rules (line 782) | def rules(self):
    method rules (line 787) | def rules(self, value):
    method __len__ (line 794) | def __len__(self):
    method __contains__ (line 801) | def __contains__(self, key):
    method __getitem__ (line 809) | def __getitem__(self, key):
    method __setitem__ (line 817) | def __setitem__(self, key, value):
    method __iter__ (line 825) | def __iter__(self):
  function feature_bin_stats (line 833) | def feature_bin_stats(data, feature, target="target", overdue=None, dpd=...
  class WOETransformer (line 936) | class WOETransformer(TransformerMixin, BaseEstimator):
    method __init__ (line 938) | def __init__(self, target="target", exclude=None):
    method fit (line 948) | def fit(self, x, y=None):
    method transform (line 957) | def transform(self, x, y=None):
    method export (line 966) | def export(self, to_json=None):
    method load (line 975) | def load(self, from_json):
    method rules (line 986) | def rules(self):
    method rules (line 991) | def rules(self, value):
    method __len__ (line 994) | def __len__(self):
    method __contains__ (line 997) | def __contains__(self, key):
    method __getitem__ (line 1000) | def __getitem__(self, key):
    method __setitem__ (line 1003) | def __setitem__(self, key, value):
    method __iter__ (line 1006) | def __iter__(self):
  function feature_efficiency_analysis (line 1010) | def feature_efficiency_analysis(data, feature, overdue=["MOB1"], dpd=[7,...

FILE: scorecardpipeline/rule.py
  function _get_context (line 23) | def _get_context(X, feature_names):
  function _apply_expr_on_array (line 27) | def _apply_expr_on_array(expr, X, feature_names):
  function get_columns_from_query (line 32) | def get_columns_from_query(query_str):
  class RuleState (line 59) | class RuleState(str, Enum):
  class RuleStateError (line 64) | class RuleStateError(RuntimeError):
  class RuleUnAppliedError (line 68) | class RuleUnAppliedError(RuleStateError):
  function json2expr (line 81) | def json2expr(data, max_index, feature_list):
  class Rule (line 107) | class Rule:
    method __init__ (line 108) | def __init__(self, expr):  # expr 既可以传递字符串,也可以传递dict
    method __str__ (line 131) | def __str__(self):
    method __repr__ (line 134) | def __repr__(self):
    method predict (line 137) | def predict(self, X: DataFrame, part=""):  # dict预测对应part_dict 、字符串表达式...
    method report (line 171) | def report(self, datasets: pd.DataFrame, target="target", overdue=None...
    method result (line 275) | def result(self):
    method __eq__ (line 280) | def __eq__(self, other):
    method __or__ (line 291) | def __or__(self, other):
    method __and__ (line 360) | def __and__(self, other):
    method __xor__ (line 429) | def __xor__(self, other):
    method __mul__ (line 441) | def __mul__(self, other):
    method __invert__ (line 444) | def __invert__(self):
    method save (line 453) | def save(report, excel_writer, sheet_name=None, merge_column=None, per...
  function ruleset_report (line 475) | def ruleset_report(datasets: pd.DataFrame, rules: List[Rule], target="ta...
  function bin_table_badrate_prediction (line 504) | def bin_table_badrate_prediction(group, amount=None):
  function sawpin_badrate_prediction_by_score (line 519) | def sawpin_badrate_prediction_by_score(base: pd.DataFrame, test: pd.Data...

FILE: scorecardpipeline/rule_extraction.py
  class DecisionTreeRuleExtractor (line 28) | class DecisionTreeRuleExtractor:
    method __init__ (line 29) | def __init__(self, target="target", labels=["positive", "negative"], f...
    method encode_cat_features (line 65) | def encode_cat_features(self, X, y):
    method get_dt_rules (line 86) | def get_dt_rules(self, tree):
    method select_dt_rules (line 106) | def select_dt_rules(self, decision_tree, x, y, lift=0., max_samples=1....
    method query_dt_rules (line 193) | def query_dt_rules(self, x, y, parsed_rules=None):
    method insert_dt_rules (line 206) | def insert_dt_rules(self, parsed_rules, end_row, start_col, save=None,...
    method fit (line 219) | def fit(self, x, y=None, max_depth=2, lift=0., max_samples=1., min_sco...
    method transform (line 267) | def transform(self, x, y=None):
    method report (line 279) | def report(self, valid=None, sheet="组合策略汇总", save=None):

FILE: scorecardpipeline/scorecard.py
  class BaseScoreTransformer (line 19) | class BaseScoreTransformer(BaseEstimator, TransformerMixin):
    method __init__ (line 20) | def __init__(self, down_lmt=300, up_lmt=1000, greater_is_better=True, ...
    method predict (line 27) | def predict(self, x):
    method score_clip (line 31) | def score_clip(score, clip=50):
  class StandardScoreTransformer (line 44) | class StandardScoreTransformer(BaseScoreTransformer):
    method __init__ (line 47) | def __init__(self, base_score=660, pdo=75, rate=2, bad_rate=0.15, down...
    method fit (line 54) | def fit(self, X, y=None, **fit_params):
    method scorecard_scale (line 78) | def scorecard_scale(self):
    method _transform (line 96) | def _transform(self, X):
    method transform (line 107) | def transform(self, X):
    method predict (line 115) | def predict(self, X):
    method _inverse_transform (line 129) | def _inverse_transform(self, X):
    method inverse_transform (line 139) | def inverse_transform(self, X):
    method _more_tags (line 147) | def _more_tags(self):
  class NPRoundStandardScoreTransformer (line 153) | class NPRoundStandardScoreTransformer(StandardScoreTransformer):
    method __init__ (line 155) | def __init__(self, base_score=660, pdo=75, bad_rate=0.15, down_lmt=300...
    method _transform (line 160) | def _transform(self, X):
  class RoundStandardScoreTransformer (line 167) | class RoundStandardScoreTransformer(StandardScoreTransformer):
    method __init__ (line 170) | def __init__(self, base_score=660, pdo=75, bad_rate=0.15, down_lmt=300...
    method _transform (line 175) | def _transform(self, X):
  class BoxCoxScoreTransformer (line 182) | class BoxCoxScoreTransformer(BaseScoreTransformer):
    method __init__ (line 183) | def __init__(self, down_lmt=300, up_lmt=1000, greater_is_better=True, ...
    method _box_cox_optimize (line 187) | def _box_cox_optimize(x):
    method fit (line 196) | def fit(self, X, y=None, **fit_params):
    method _transform (line 209) | def _transform(self, X):
    method transform (line 220) | def transform(self, X):
    method predict (line 228) | def predict(self, X):
    method _inverse_transform (line 254) | def _inverse_transform(self, X):
    method inverse_transform (line 266) | def inverse_transform(self, X):
    method _box_cox_inverse_tranform (line 275) | def _box_cox_inverse_tranform(x, lmbda):

FILE: scorecardpipeline/utils.py
  function seed_everything (line 34) | def seed_everything(seed: int, freeze_torch=False):
  function init_setting (line 53) | def init_setting(font_path=None, seed=None, freeze_torch=False, logger=F...
  function load_pickle (line 100) | def load_pickle(file, engine="joblib"):
  function save_pickle (line 121) | def save_pickle(obj, file, engine="joblib"):
  function feature_describe (line 141) | def feature_describe(data, feature=None, percentiles=None, missing=None,...
  function groupby_feature_describe (line 183) | def groupby_feature_describe(data, by=None, n_jobs=-1, **kwargs):
  function germancredit (line 213) | def germancredit():
  function round_float (line 248) | def round_float(num, decimal=4):
  function feature_bins (line 263) | def feature_bins(bins, decimal=4):
  function extract_feature_bin (line 300) | def extract_feature_bin(bin_var):
  function inverse_feature_bins (line 319) | def inverse_feature_bins(feature_table, bin_col="分箱"):
  function bin_plot (line 350) | def bin_plot(feature_table, desc="", figsize=(10, 6), colors=["#2639E9",...
  function corr_plot (line 408) | def corr_plot(data, figure_size=(16, 8), fontsize=16, mask=False, save=N...
  function ks_plot (line 464) | def ks_plot(score, target, title="", fontsize=14, figsize=(16, 8), save=...
  function hist_plot (line 575) | def hist_plot(score, y_true=None, figsize=(15, 10), bins=30, save=None, ...
  function psi_plot (line 639) | def psi_plot(expected, actual, labels=["预期", "实际"], desc="", save=None, ...
  function csi_plot (line 709) | def csi_plot(expected, actual, score_bins, labels=["预期", "实际"], desc="",...
  function dataframe_plot (line 779) | def dataframe_plot(df, row_height=0.4, font_size=14, header_color='#2639...
  function distribution_plot (line 835) | def distribution_plot(data, date="date", target="target", save=None, fig...
  function sample_lift_transformer (line 897) | def sample_lift_transformer(df, rule, target='target', sample_rate=0.7):
  function tasks_executor (line 931) | def tasks_executor(tasks, n_jobs=-1, pool="thread"):
  function monotonic_bad_rate_binning (line 961) | def monotonic_bad_rate_binning(df, feature, target, target_rates, greate...

FILE: setup.py
  function get_version (line 8) | def get_version():
  function get_requirements (line 13) | def get_requirements(stage=None):
Copy disabled (too large) Download .json
Condensed preview — 54 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (13,528K chars).
[
  {
    "path": ".gitignore",
    "chars": 3197,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 174,
    "preview": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n\npython:\n  install:\n    - requirements: docs/requireme"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2023 itlubber\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "MANIFEST.in",
    "chars": 99,
    "preview": "include scorecardpipeline/*.ttf scorecardpipeline/*.csv scorecardpipeline/*.xlsx requirements*.txt\n"
  },
  {
    "path": "README.md",
    "chars": 24112,
    "preview": "# 评分卡pipeline建模包\n\n<img src=\"https://itlubber.art/upload/scorecardpipeline.png\" alt=\"itlubber.png\" width=\"100%\" border=0/"
  },
  {
    "path": "docs/Makefile",
    "chars": 638,
    "preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the "
  },
  {
    "path": "docs/make.bat",
    "chars": 799,
    "preview": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sp"
  },
  {
    "path": "docs/requirements.txt",
    "chars": 207,
    "preview": "sphinx>=6,<8\nmyst-parser>=2.0.0,<2.1\npygments>=2.15.1,<2.16\nsphinx-intl>=2.0,<2.2\nsphinx-design>=0.4,<0.5\nsphinx_copybut"
  },
  {
    "path": "docs/source/CNAME",
    "chars": 31,
    "preview": "scorecardpipeline.itlubber.art\n"
  },
  {
    "path": "docs/source/conf.py",
    "chars": 4388,
    "preview": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common op"
  },
  {
    "path": "docs/source/index.rst",
    "chars": 788,
    "preview": ".. scorecardpipeline documentation master file, created by\n   sphinx-quickstart on Fri Nov 10 13:40:57 2023.\n   You can "
  },
  {
    "path": "docs/source/install.html",
    "chars": 5882,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, in"
  },
  {
    "path": "docs/source/quickstart.md",
    "chars": 47712,
    "preview": "## 简介\n\n`scorecardpipeline` 封装了 `toad`、`scorecardpy`、`optbinning` 等评分卡建模相关组件,`API` 风格与 `sklearn` 高度一致,支持 `pipeline` 式端到端评"
  },
  {
    "path": "docs/source/scorecardpipeline.rst",
    "chars": 1832,
    "preview": "scorecardpipeline\n======================================\n\n.. automodule:: scorecardpipeline\n   :members:\n   :undoc-membe"
  },
  {
    "path": "examples/auto_report.ipynb",
    "chars": 2907641,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outp"
  },
  {
    "path": "examples/automl_example.ipynb",
    "chars": 273385,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 14,\n   \"metadata\": {\n    \"tags\": []\n   },\n   \"outputs\": "
  },
  {
    "path": "examples/cycle_end_vintage.sql",
    "chars": 3913,
    "preview": "WITH loan AS (\n    # 筛选需要计算的订单数据\n    SELECT id, user_id, flow_channel, bank_channel, apply_time, loan_time, periods, amo"
  },
  {
    "path": "examples/gbm_model_examples.ipynb",
    "chars": 44608,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": "
  },
  {
    "path": "examples/loan_replan.py",
    "chars": 4279,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2024/3/22 15:25\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport pandas as "
  },
  {
    "path": "examples/model_report/auto_eda.html",
    "chars": 1516123,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"data:image/x-icon;base64,A"
  },
  {
    "path": "examples/month_end_vintage.sql",
    "chars": 9246,
    "preview": "WITH months_end(观察时点) AS (\n    select date(month_start)\n    FROM (\n        SELECT month_start\n        FROM (\n           "
  },
  {
    "path": "examples/profitability_calculation.py",
    "chars": 3428,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2024/3/19 10:47\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport sys\n\nsys.p"
  },
  {
    "path": "examples/quickstart.ipynb",
    "chars": 398494,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n "
  },
  {
    "path": "examples/rule_efficiency.ipynb",
    "chars": 21902,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n "
  },
  {
    "path": "examples/rule_extraction.ipynb",
    "chars": 25531,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outp"
  },
  {
    "path": "examples/rule_test.py",
    "chars": 111041,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2024/4/18 10:15\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport copy\nimpor"
  },
  {
    "path": "examples/scorecard.pmml",
    "chars": 9044,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<PMML xmlns=\"http://www.dmg.org/PMML-4_4\" xmlns:data=\"http://jpm"
  },
  {
    "path": "examples/scorecard_samples.ipynb",
    "chars": 4257886,
    "preview": "{\n \"cells\": [\n  {\n   \"attachments\": {},\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# scorecardpi"
  },
  {
    "path": "examples/third_party_data_describe.ipynb",
    "chars": 27190,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n "
  },
  {
    "path": "examples/同盾百融三方数据评分卡建模样例.ipynb",
    "chars": 2830786,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 60,\n   \"id\": \"06bac5a1-08b0-41c4-87bb-400afc0dec87\",\n   "
  },
  {
    "path": "examples/特征选择.ipynb",
    "chars": 103433,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"> https://github.com/YC-Coder-Chen/"
  },
  {
    "path": "requirements.txt",
    "chars": 409,
    "preview": "Cython\nwget\nnumpy>1.23.1,<1.24.0\npandas\nmatplotlib\nseaborn>=0.10.0\nscipy>=1.6.0\nstatsmodels<0.14,>=0.13.2\nscikit-learn>="
  },
  {
    "path": "scorecardpipeline/__init__.py",
    "chars": 2919,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/2/15 17:55\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nfrom toad.metrics"
  },
  {
    "path": "scorecardpipeline/auto_eda.py",
    "chars": 2330,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/12/29 11:17\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\n\nimport os\nimpor"
  },
  {
    "path": "scorecardpipeline/auto_report.py",
    "chars": 12528,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/12/29 11:17\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport traceback"
  },
  {
    "path": "scorecardpipeline/excel_writer.py",
    "chars": 58138,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/05/14 16:23\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport sys\nimpor"
  },
  {
    "path": "scorecardpipeline/feature_engineering.py",
    "chars": 3701,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2024/5/10 10:28\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport numpy as n"
  },
  {
    "path": "scorecardpipeline/feature_selection.py",
    "chars": 38325,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2024/5/8 14:06\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\n\nimport operator\ni"
  },
  {
    "path": "scorecardpipeline/financial.py",
    "chars": 36059,
    "preview": "\"\"\"Some simple financial calculations.\n\npatterned after spreadsheet computations.\n\nThere is some complexity in each func"
  },
  {
    "path": "scorecardpipeline/germancredit.csv",
    "chars": 268042,
    "preview": "status_of_existing_checking_account,duration_in_month,credit_history,purpose,credit_amount,savings_account_and_bonds,pre"
  },
  {
    "path": "scorecardpipeline/logger.py",
    "chars": 1732,
    "preview": "import os\nimport sys\nfrom logging.handlers import RotatingFileHandler, TimedRotatingFileHandler\nfrom logging import getL"
  },
  {
    "path": "scorecardpipeline/model.py",
    "chars": 31446,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/05/21 16:23\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\n\nimport os\nimpor"
  },
  {
    "path": "scorecardpipeline/processing.py",
    "chars": 43098,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/05/21 16:23\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\n\nimport numpy as"
  },
  {
    "path": "scorecardpipeline/rule.py",
    "chars": 30356,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2024/2/26 12:00\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport ast\nimport"
  },
  {
    "path": "scorecardpipeline/rule_extraction.py",
    "chars": 15926,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2024/2/29 13:29\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport warnings\ni"
  },
  {
    "path": "scorecardpipeline/scorecard.py",
    "chars": 12347,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2024/4/15 16:52\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport math\nfrom "
  },
  {
    "path": "scorecardpipeline/utils.py",
    "chars": 41128,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\n@Time    : 2023/05/21 16:23\n@Author  : itlubber\n@Site    : itlubber.art\n\"\"\"\nimport warnings\n"
  },
  {
    "path": "setup.py",
    "chars": 1704,
    "preview": "import os\nimport re\nfrom setuptools import setup, find_packages, Extension\n\nNAME = 'scorecardpipeline'\n\n\ndef get_version"
  }
]

// ... and 6 more files (download for full content)

About this extraction

This page contains the full source code of the itlubber/scorecardpipeline GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 54 files (12.6 MB), approximately 3.3M tokens, and a symbol index with 325 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!