diff --git a/reports/scorecard_report/scorecard_report.html b/reports/scorecard_report/scorecard_report.html new file mode 100644 index 0000000..81c1e1f --- /dev/null +++ b/reports/scorecard_report/scorecard_report.html @@ -0,0 +1,283 @@ + + + + + + + + + 分析报告 - 结网 + + + + + + + + + + + + + + + + + +
+ +

基于GiveMeSomeCredit的贷款申请评分卡建模报告

+ +
+ + 刘弼仁 + + | + + 19-05-28 + +
+ +

建模背景

+ +

贷款申请评分卡是一种成熟的应用统计模型,其作用是对申请人做风险评估,识别可能产生逾期的客户并做出决策,包括申请审批和风险定价等,具有较高的准确性和可靠性。

+ +

本报告数据来源于GiveMeSomeCredit,通过介绍评分卡典型建模流程,希望读者能够就贷款申请评分模型有了初步了解。

+ +

算法选择说明

+ +

在贷款申请评分卡建模过程中,通常选择逻辑回归(Logistics Regression)算法,该算法的函数作用是将申请人的贷款申请信息综合起来并转化为逾期概率,为决策人员提供了量化风险评估的依据。

+ +

决策人员的风险评估思路如下:

+ + + +

审批贷款申请时,假设只有通过或拒绝两种审批结果,审批通过的概率为\(approve\)。审批通过后,客户也只有还款或逾期两种还款结果,逾期的概率为\(overdue\)(还款的概率为\(repay\),\(overdue+repay=1\))。银行就逾期的损失为\(loss\),就还款的收益为\(revenue\)。综合收益为:

+ + \[approve(repay\times revenue-overdue\times loss)\] + +

站在决策人员的立场,审批通过的充分条件为综合收益大于零,推导可得:

+ + \[overdue / repay < revenue / loss\] + +

就是说,当该申请人发生逾期的概率和还款的概率的比值(定义为逾期还款概率比率,\(odd\))小于收益和损失的比值审批通过。所以,计算申请人的逾期还款概率比率成为首要工作。

+ +

假设,已知申请人的贷款申请信息(定义为特征变量的值的集合,\(x=({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m)})\)),则该申请人的逾期还款概率比率为:

+ + \[odd(overdue|({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m}))=p(overdue|({x}_{1,}{x}_{2},\cdot \cdot \cdot {x}_{m}))/p(repay|({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m}))\] + + \[=p(overdue)/p(repay)\times (f({x}_{1,}{x}_{2},\cdot \cdot \cdot {x}_{m}|overdue)/f({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m}|repay))\] + + \[=p(overdue)/p(repay)\times f({x}_{1}|overdue)/f({x}_{1}|repay)\times f({x}_{2}|overdue)/f({x}_{2}|repay)\times \cdot \cdot \cdot f({x}_{m}|overdue)/f({x}_{m}|repay)\] + + \[\to F(x)=ln(odd(overdue|({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m})))\] + + \[=ln(p(overdue)/p(repay))+ln(f({x}_{1}|overdue)/f({x}_{1}|repay))+\cdot \cdot \cdot ln(f({x}_{m}|overdue)/f({x}_{m}|repay))\] + +

定义\(ln(f({x}_{i}|overdue)/f({x}_{i}|repay))\)为特征变量的值的的证据权重\(woe({x}_{i})\),就数据集而言证据权重是评价某个特征变量逾期还款分布情况的较好统计量。

+ +

综上所述,在每个特征变量相互独立的情况下,计算申请人的逾期还款概率比率为对数逾期还款样本比率加上各特征变量的值的证据权重,即\(F(x)=a+\sum ^{m}_{i=1} {woe({x}_{i})}\)。

+ +

另外,推导可得:

+ + \[odd(overdue|F({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m})=F(x))={e}^{F(x)}\] + + \[=p(overdue|({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m}))/p(repay|({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m}))\] + + \(\to p(overdue|({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m}))=1/(1+{e}^{-F(x)})\),刚好是逻辑回归函数! + +

将\(F(x)\)由对数比率经线性转化则为贷款申请评分卡!

+ +

建模流程

+ +

获取数据

+ +

连接数据库获取原始数据集,目标变量为SeriousDlqin2yrs,特征变量数为10个。数据预览如下:

+ + + +

数据预处理

+ +

数据清洗

+ +

删除目标变量包含缺失值和重复的样本。处理后,样本数为149210份。

+ +

缺失值处理

+ +

在特征变量证据权重编码时,将对缺失值单独作为一箱并纳入模型。

+ +

异常值处理

+ +

在特征变量证据权重编码时,可消除异常值的影响,故不作异常值处理。

+ +

特征变量证据权重编码

+ +

逻辑回归假设之一为特征变量和目标变量之间存在线性关系,但在实际情况多为非线性。通过分箱,可将非线性关系转化为线性。另外,分箱可以减少缺失值和异常值对逻辑回归的影响并提升逻辑回归的鲁棒性。

+ +

本次报告使用决策树进行分箱,分箱后使用证据权重编码。以特征变量“Age”为例,其证据权重编码结果如下:

+ + + +

由上图可看出,特征变量“Age”分箱后各箱证据权重呈线性关系且单调递减,即随着年龄升高逾期还款概率比率降低。这与贷款申请审批经验符合,其经济稳定性的增强、收入水平的提升、信用记录的积累、消费观念的成熟以及风险管理能力的提升,表现出更低的逾期风险。

+ +

决策树分箱说明

+ +
    + +
  1. 统计特征变量值数,取其与5的最小值作为决策树算法的最大叶节点数(本次报告控制最大分箱数为5,最小叶节点样本数为5%)。
  2. + +
  3. 基于最大叶节点数使用决策树算法就特征变量拟合目标变量,利用决策树的分裂节点作为划分点进而将连续型特征变量划分为不同的区间。
  4. + +
  5. 统计各区间的证据权重并检验单调性。
  6. + +
  7. 如果检验通过,则将上述区间作为特征变量分箱结果。如果检验未通过,则将最大叶节点数减1并重复上述步骤至检验通过。
  8. + +
+ +

特征变量选择

+ +

基于信息价值选择特征变量

+ +

信息价值是与证据权重密切相关的指标,可用来评估特征变量的预测能力。通常,选择信息价值大于等于0.1的特征变量。

+ + \[iv=(overduty-repay)\times ln(odd)\] + +

信息价值说明

+ +

概率是描述随机变量确定性的量度,熵是描述随机变量不确定性的量度。假设\(p(x)\)和\(q(x)\)是逾期和还款的两个概率分布,可使用相对熵表示\(q(x)\)拟合\(p(x)\)所产生的信息损失,公式如下:

+ + \[D(p||q)=\sum {p(x)log(p(x)/q(x))}\] + +

相对熵没有对称性,即\(D(p||q)\neq D(q||p)\),如果将两个概率分布之间的相对熵求和,和越大说明两个概率分布的距离越大。该和即为KL距离,公式如下:

+ + \[DistanceKL=\int {(f(p|overduty)-f(p|repay))\times log(f(p|overduty)/f(p|overduty))dx}\] + +

上式离散形式即为信息价值。在选择特征变量时,特征变量的信息价值越大说明逾期还款的概率分布的距离越大、区分逾期还款的能力越强。

+ +

基于有条件的后向步进淘汰特征变量

+ +

使用逻辑回归算法需检验其前提条件:

+ + + +

本次报告使用方差扩大因子(Variance Inflation Factor)评估特征变量与其它变量的共线性。通常,淘汰方差扩大因子大于5的特征变量。

+ + \[vif=1/(1-{maximun(r)}^{2})\] + +

其中,\(r\)为特征变量与其它特征变量的复相关系数。

+ +

有条件的后向步进淘汰特征变量说明

+ +
    + +
  1. 统计特征变量的方差扩大因子和回归系数。
  2. + +
  3. 淘汰方差扩大因子大于5或回归系数小于0.1且方差扩子因子最大的特征特征变量。
  4. + +
  5. 重复上述步骤至没有特征变量可淘汰。
  6. + +
+ +

处理后,选择的特征变量数为5个,特征变量预览如下:

+ + + +

评分卡开发和验证

+ +

评分卡开发

+ +

本次报告中贷款申请评分卡公式为(本次报告控制\(a\)为500,\(b\)为\(50/ln(2)\)):

+ + \[score=a-blog(odd(overdue|({x}_{1},{x}_{2},\cdot \cdot \cdot {x}_{m})))\] + + \[=a-b({\beta }_{0}+{\beta }_{1}woe({x}_{1})+{\beta }_{2}woe({x}_{2})+\cdot \cdot \cdot {\beta }_{m}woe({x}_{m}))\] + +

其中,\({\beta }_{i}\)为特征变量的回归系数(\({\beta }_{0}\)基于回归系数分摊至各特征变量)。

+ +

以“Age”为例,其评分卡编制结果如下:

+ + + +

由上表可看出,\(分数=加权基础分数+加权回归系数\times 证据权重\)。

+ +

评分卡验证

+ +

本次报告使用柯斯和提升统计量评估评分卡,柯斯统计量为55.09,提升统计量为7.28

+ +

柯斯统计量说明

+ +

柯斯统计量全称Kolmogorov-Smirnov,常用于评估模型对于目标变量的区分能力。先将总分数划分为若干区间并作为横坐标,再将逾期和还款的累计样本数占比作为纵坐标,即可绘制两条洛伦兹曲线。柯斯统计量就是两条洛伦兹曲线间最大距离。

+ +

通常,柯斯统计量小于20不建议使用该评分卡,20~40说明该评分卡区分能力较好、40~50良好、50~60很好、60~75非常好,大于75建议审慎使用。

+ +

提升统计量说明

+ +

提升统计量,常用于量化评估模型对目标变量的预测能力较随机选择的提升程度。先将总分数划分为若干区间并作为横坐标,再计算各区间的累计逾期样本数占比和累计样本数占比的比值,最大值就是提升统计量。

+ +

通常,提升统计量折线图在高位保持若干区间后迅速下降至1时,表示该评分卡区分能力较好。

+ + + +

评分卡评价表

+ + + +

以分箱[500, 550)为例,该分箱5.61%是逾期客户。假设,审批通过16位客户产生的收益可平衡1位逾期客户的损失,5.61%可作为平衡点,拒绝规则不能低于550,否则损失大于收益。

+ +

以拒绝规则<550为例,若选择该拒绝规则,则会拒绝36.53%客户,这部分中15.72%是逾期客户。使用该评分卡后,逾期客户减少85.59%。

+ +
+ + + + + + + + \ No newline at end of file diff --git a/reports/scorecard_report/statistics.html b/reports/scorecard_report/statistics.html new file mode 100644 index 0000000..15dea17 --- /dev/null +++ b/reports/scorecard_report/statistics.html @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特征变量名信息价值方差扩大因子回归系数
RevolvingUtilizationOfUnsecuredLines1.041.200.72
NumberOfTimes90DaysLate0.831.190.64
NumberOfTime60-89DaysPastDueNotWorse0.571.180.55
NumberOfTime30-59DaysPastDueNotWorse0.671.170.69
Age0.231.080.55
+ + + + + + \ No newline at end of file diff --git a/reports/stylesheet.css b/reports/stylesheet.css new file mode 100644 index 0000000..b2fdd96 --- /dev/null +++ b/reports/stylesheet.css @@ -0,0 +1,193 @@ +/*样式表*/ +@page { + + size: A4; + +} + +/*全局样式*/ +* { + + font-family: "PingFang SC", "Nunito"; + + font-size: 14px; + + color: rgb(29, 33, 41); + +} + +body { + + margin: 0px; + +} + +/*页面布局*/ +.document-container { + + width: 16cm; + + margin: auto; + +} + +/*文档标题*/ +.document-block-title { + + font-size: 24px; + + font-weight: 700; + +} + +/*span*/ +span { + + color: rgb(134, 144, 156); + +} + +/*第一级标题*/ +h1 { + + font-size: 20px; + + font-weight: 500; + +} + +/*第二级标题*/ +h2 { + + font-size: 18px; + + font-weight: 500; + +} + +/*第三级标题*/ +h3 { + + font-size: 16px; + + font-weight: 500; + +} + +/*引用*/ +blockquote { + + margin: 12px 0; + + padding-left: 12px; + + border-left: 2px solid #E5E6EB; + + color: #86909C; + +} + +blockquote * { + + color: #86909C; + +} + +/*列表项目*/ +li { + + margin-bottom: 12px; + +} + +/*表格*/ +table { + + width: 16cm; + + table-layout: auto; + + border-collapse: collapse; + + text-align: right; + + scrolling: no; + +} + +/*表格-首行*/ +table tr th { + + height: 35px; + + font-size: 14px; + + font-weight: 500; + + background-color: rgb(242 243 245); + + padding: 8px 16px; + +} + +/*表格-数据行*/ +table tr td { + + height: 30px; + + font-size: 12px; + + font-weight: 400; + + border-bottom: 1px solid rgb(229 230 235); + + padding: 8px 16px; + +} + +/*表格第一列左对齐*/ +table th:first-child, table td:first-child { + + text-align: left; + +} + +img { + + object-fit: cover; + + width: auto; + + height: 400px; + +} + +iframe { + + display: block; + + width: 100%; + + height: 400px; + + border: None; + + scrolling: no; + + overflow: hidden; + +} + +footer { + + display: flex; + + justify-content: center; + + height: 200px; + + text-align: center; + + align-items: flex-end; + +} diff --git a/rfm/rfm_report.html b/rfm/rfm_report.html new file mode 100644 index 0000000..7bbedef --- /dev/null +++ b/rfm/rfm_report.html @@ -0,0 +1,238 @@ + + + + + + + + + 数据报告 + + + + + + + + + +
+ +

基于RFM模型的客户价值分析报告

+ +
+ + 刘弼仁 + + | + + 2025-08-12 + +
+ +

分析背景

+ +

在面向客户制定运营策略时,我们希望针对不同的客户推行不同的策略,实现精准化运营,以期获得最大的投入产出比(ROI)。精准化运营的前提是客户分类。通过客户分类,细分出不同的客户群体,对不同的客户群体采取不同的运营策略,合理分配有限的资源,以实现投入产出最大化。

+ +

在客户分类中,RFM模型是一个经典的客户分类模型,该模型利用交易环节中最核心的三个变量,即最近消费(Recency)、消费频率(Frequency)和消费金额(Monetray)细分客户群体,从而分析不同群体的客户价值。

+ +

本报告使用Kaggle的SuperstoreData作为数据集,探索如何基于RFM模型对客户群体进行细分,以及细分后如何对客户价值进行分析。

+ +

分析过程

+ +

数据预览

+ + + +

数据集共24751份样本。其中,客户ID数据类型为字符串,交易金额为小数,交易日期为日期。

+ +

构建RFM模型

+ +

其中,R为最近一次交易日期距最远交易日期间隔,单位为日,数据类型为整数;F为交易笔数,数据类型为整数;M为累计交易金额,数据类型为小数。R、F和M均已正向化。

+ +

客户分类

+ +

本报告就R、F和M基于平均值划分为小于等于平均值部分和大于部分:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
客户分类R大于R平均值F大于F平均值M大于M平均值
流失客户
一般维持客户
新客户
潜力客户
重要挽留客户
重要深耕客户
重要唤回客户
重要价值客户
+ +

数据解读

+ +

近十二个自然月客户数趋势

+ + + +

上图表示近十二个自然月的每个自然月对应的前后滚动十二个自然月的客户数,反映了客户发展的趋势为越来越多。

+ +

客户分类分布

+ + + +

上图表示各客户分类在R、F和M分布,其中:R越靠近右侧反映了该客户分类越最近交易,F越靠近上侧反映了该客户分类越交易频繁,M越大反映了该客户分类越交易金额大。

+ +

客户占比

+ + + +

上图表示各客户分类的客户占比,反映了重要价值客户、流失客户和新客户这三类客户分类的客户占比较大,是后续分析的重点。

+ +

交易金额占比

+ + + +

上图表示各客户分类的交易金额占比,反映了重要价值客户、新客户和重要唤回客户这三类客户分类的交易金额占比较大。

+ +

近十二个自然月客户占比趋势

+ + + +

上图表示重要价值客户、流失客户和新客户这三类客户分类的客户占比,反映了近期新客户占比提升、重要价值客户和流式客户占比下降,建议针对重要价值客户制定相应运营策略。

+ +

近十二个自然月留存率趋势

+ + + +

上图表示重要价值客户、流失客户和新客户这三类客户分类的近十二个自然月的留存率,反映了重要价值客户较流式客户和新客户黏性大,近期新客户黏性较大。

+ +

通过客户分类,我们可以根据客户细分群体制定相应的产品和运营策略和方案:

+ + + +
+ + + + + + \ No newline at end of file diff --git a/rfm/stylesheet.css b/rfm/stylesheet.css new file mode 100644 index 0000000..b42ddbc --- /dev/null +++ b/rfm/stylesheet.css @@ -0,0 +1,191 @@ +/*样式表*/ +@page { + + size: A4; + +} + +/*全局样式*/ +* { + + font-family: "PingFang SC", "Nunito"; + + font-size: 14px; + + color: rgb(29, 33, 41); + +} + +body { + + margin: 0px; + +} + +/*页面布局*/ +.document-container { + + width: 16cm; + + margin: auto; + +} + +/*文档标题*/ +.document-block-title { + + font-size: 24px; + + font-weight: 700; + +} + +/*span*/ +span { + + color: rgb(134, 144, 156); + +} + +/*第一级标题*/ +h1 { + + font-size: 20px; + + font-weight: 500; + +} + +/*第二级标题*/ +h2 { + + font-size: 18px; + + font-weight: 500; + +} + +/*第三级标题*/ +h3 { + + font-size: 16px; + + font-weight: 500; + +} + +/*引用*/ +blockquote { + + margin: 12px 0; + + padding-left: 12px; + + border-left: 2px solid #E5E6EB; + + color: #86909C; + +} + +blockquote * { + + color: #86909C; + +} + +/*列表项目*/ +li { + + margin-bottom: 12px; + +} + +/*表格*/ +table { + + width: 16cm; + + table-layout: auto; + + border-collapse: collapse; + + text-align: right; + + scrolling: no; + +} + +/*表格-首行*/ +table tr th { + + height: 35px; + + font-size: 14px; + + font-weight: 500; + + background-color: rgb(242 243 245); + + padding: 8px 16px; + +} + +/*表格-数据行*/ +table tr td { + + height: 30px; + + font-size: 12px; + + font-weight: 400; + + border-bottom: 1px solid rgb(229 230 235); + + padding: 8px 16px; + +} + +/*表格第一列左对齐*/ +table th:first-child, table td:first-child { + + text-align: left; + +} + +img { + + object-fit: cover; + + width: auto; + + height: 400px; + +} + +iframe { + + display: block; + + width: 100%; + + height: 400px; + + border: None; + + overflow: hidden; + +} + +footer { + + display: flex; + + justify-content: center; + + height: 200px; + + text-align: center; + + align-items: flex-end; + +} diff --git a/utils/scorecrad_calculate.txt b/utils/scorecrad_calculate.txt new file mode 100644 index 0000000..ad85609 --- /dev/null +++ b/utils/scorecrad_calculate.txt @@ -0,0 +1,49 @@ +def Calculate(sample): + + score = 0 + + match sample["RevolvingUtilizationOfUnsecuredLines"]: + + case x if x < 0.115: score += 180.0 + + case x if x < 0.215 and x >= 0.115: score += 148.0 + + case x if x < 0.495 and x >= 0.215: score += 126.0 + + case x if x < 0.775 and x >= 0.495: score += 90.0 + + case x if x >= 0.775: score += 48.0 + + match sample["NumberOfTimes90DaysLate"]: + + case x if x < 0.5: score += 119.0 + + case x if x >= 0.5: score += -1.0 + + match sample["NumberOfTime60-89DaysPastDueNotWorse"]: + + case x if x < 0.5: score += 98.0 + + case x if x >= 0.5: score += 8.0 + + match sample["NumberOfTime30-59DaysPastDueNotWorse"]: + + case x if x < 0.5: score += 134.0 + + case x if x < 1.5 and x >= 0.5: score += 68.0 + + case x if x >= 1.5: score += 25.0 + + match sample["Age"]: + + case x if x < 33.5: score += 67.0 + + case x if x < 42.5 and x >= 33.5: score += 74.0 + + case x if x < 56.5 and x >= 42.5: score += 82.0 + + case x if x < 63.5 and x >= 56.5: score += 101.0 + + case x if x >= 63.5: score += 124.0 + + return score \ No newline at end of file diff --git a/任务调度服务器/scheduled_tasks.py b/任务调度服务器/scheduled_tasks.py new file mode 100644 index 0000000..c10fd30 --- /dev/null +++ b/任务调度服务器/scheduled_tasks.py @@ -0,0 +1,361 @@ +# -*- coding: utf-8 -*- + +''' + +脚本说明: + +本脚本用于Docker中Python执行定时任务 + +''' + +#使用APScheduler +from apscheduler.schedulers.background import BackgroundScheduler + +#初始化调度器 +scheduler = BackgroundScheduler() + +import pytz + +#定义时区 +timezone = pytz.timezone('Asia/Shanghai') + +from datetime import datetime + +import time + +from sqlalchemy import create_engine, types + +from urllib import parse, request, error + +import json + +import pandas + +import smtplib + +from email.mime.text import MIMEText + +from email.header import Header + +''' + + + +''' + +#定义类:读取数据结构 +class Response(): + + def __init__(self, code, messsage, data): + + self.code = code + + self.messsage = messsage + + self.data = data + +#定义函数:POST方式通过请求接口获取数据 +def read_request_post(url, headers, data): + + #dict转为字符串 + data = bytes(json.dumps(obj = data), encoding = 'utf8') + + try : + + #访问接口并发送请求 + api_request = request.Request(url = url, headers = headers, data = data, method = 'POST') + + #获取响应并解码 + api_response = request.urlopen(url = api_request).read().decode('utf8') + + #json转为dict + data = json.loads(api_response) + + return Response(100, '读取成功', data) + + except : + + return Response(900, '读取失败', '') + +#定义函数:GET方式通过请求接口获取数据 +def read_request_get(url, headers, data): + + try : + + #访问接口并发送请求 + api_request = request.Request(url = url, headers = headers, data = data, method = 'GET') + + #获取响应并解码 + api_response = request.urlopen(url = api_request).read().decode('utf8') + + #json转为dict + data = json.loads(api_response) + + return Response(100, '读取成功', data) + + except : + + return Response(900, '读取失败', '') + +#定义函数:基于SQLAlchemy格式化数据库连接信息 +def database_connect(username, password, host, port, database): + + return 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'.format(username, password, host, port, database) + +#定义函数:连接数据库并读取数据 +def read_mysql(database_connect, query): + + try : + + #初始化数据库连接 + database = create_engine(database_connect) + + #创建连接 + connection = database.connect() + + data = pandas.read_sql_query(con = connection, sql = query) + + #关闭连接 + connection.close() + + database.dispose() + + return Response(100, '读取成功', data) + + except : + + return Response(900, '读取失败', '') + +#定义函数:连接数据库并写入数据 +def write_mysql(database_connect, table_name, dataset, data_types): + + try : + + #初始化数据库连接 + database = create_engine(database_connect) + + #创建连接 + connection = database.connect() + + dataset.to_sql(con = connection, name = table_name, if_exists = 'replace', index = False, dtype = data_types) + + #关闭连接 + connection.close() + + database.dispose() + + return Response(100, '写入成功', '') + + except : + + return Response(900, '写入失败', '') + +#定义函数:基础定时任务,支持连接出库,查询数据,写入入库 +def task_basic(task_id, database_connect_read, query, database_connect_write, table_name, data_types): + + #查询数据 + response = read_mysql(database_connect_read, query) + + if response.code == 100: + + dataset = response.data + + #写入数据 + response = write_mysql(database_connect_write, table_name, dataset, data_types) + + if response.code == 100: + + code = 100 + + messsage = '执行成功' + + else: + + code = 902 + + messsage = '执行失败(连接或写入异常)' + + else: + + code = 901 + + messsage = '执行失败(连接或查询异常)' + + messsage = '[定时任务]-[{}]-[{}]-[{}]'.format(task_id, messsage, datetime.now(timezone).strftime('%y-%m-%d %H:%M:%S')) + + return Response(code, messsage, '') + +#定义函数:发送邮件 +def send_mail(messsage): + + mail_message = MIMEText(messsage, 'plain', 'utf-8') + + mail_message['From'] = Header('我', 'utf-8') + + mail_message['To'] = Header('刘弼仁', 'utf-8') + + mail_message['Subject'] = Header('Scheduled Task Report', 'utf-8') + + try : + + #连接SMTP服务器 + smtp_server = smtplib.SMTP_SSL('smtp.feishu.cn', 465) + + #登录 + smtp_server.login('mars@liubiren.cloud', '8AI2kUKn5x5Le4Hg') + + smtp_server.sendmail('mars@liubiren.cloud', ['mars@liubiren.cloud'], mail_message.as_string()) + + return Response(100, '发送成功', '') + + except : + + return Response(900, '发送失败', '') + +''' + + + +''' + +#刘弼仁私人数据库连接信息 +my_database_connect = database_connect('root', 'Te198752', 'cdb-7z9lzx4y.cd.tencentcdb.com', '10039', 'hzdd') + +#杭州刀豆数据库链接信息 +hzdd_database_connect = database_connect('liubiren', 'G18HamnVf96frq0K', 'daodou-prod-proxy-public.rwlb.rds.aliyuncs.com', '3306', 'whaleip') + +''' + + + +''' + +''' + +定时任务001 + +从杭州刀豆数据库抽取租户注册数据并每日同步至刘弼仁私人数据库 + +''' + +def scheduled_task_001(): + + #定义查询语句 + query = "select date_format(table1.insert_time, '%%y-%%m-%%d') as 'register_date', table1.customer_id as 'tenant_id', (case table1.customer_type when 1 then '企业' when 2 then '个人' else null end) as 'tenant_type', table1.full_name as 'tenant_name', table1.company_USCI as 'tenant_uuid', s_customer_register_channel.channel_name as 'register_channel' from ( select insert_time, id as 'customer_id', customer_type, full_name, company_USCI, source_type from s_customer_info where status = 0 and test_status = 0 and full_name not regexp '花豆|刀豆|鲸版权|维权骑士|测试|test') as table1 left join s_customer_register_channel on table1.source_type = s_customer_register_channel.channel_key order by tenant_id desc" + + #定义数据类型 + data_types = {'register_date': types.Date(), 'tenant_id': types.Integer(), 'tenant_type': types.Text(), 'tenant_name': types.Text(), 'tenant_uuid': types.Text(), 'register_channel': types.Text()} + + return task_basic('001', hzdd_database_connect, query, my_database_connect, 'hzdd_tenant', data_types) + +''' + +定时任务002 + +从杭州刀豆数据库抽取作品权利证明数据并每日同步至刘弼仁私人数据库 + +''' + +def scheduled_task_002(): + + #定义查询语句 + query = "select date_format(s_customer_works_base.insert_time, '%%y-%%m-%%d') as 'works_enter_date', s_customer_works_base.id as 'works_id', s_customer_works_base.customer_id as 'tenant_id', ( case s_customer_works_base.source_type when 0 then '用户添加' when 1 then '天网添加' when 2 then '骑士添加' when 3 then '它方推送' when 4 then '前台添加' else '其它' end) as 'works_enter_type', date_format(s_customer_file_tsa.right_time, '%%y-%%m-%%d') as 'works_tsa_date', ( case s_customer_file_tsa.source_type when 0 then '用户申请' when 1 then '它方推送' when 2 then '用户上传' else null end) as 'tsa_enter_type', date_format(s_customer_copyright_task.insert_time, '%%y-%%m-%%d') as 'works_copyright_date', ( case s_customer_copyright_task.source_type when 1 then '用户申请' when 2 then '用户上传' else null end) as 'copyright_enter_type', ( case s_customer_copyright_task.works_type when 1 then '文字' when 2 then '口述' when 3 then '音乐' when 4 then '戏剧' when 5 then '曲艺' when 6 then '舞蹈' when 7 then '杂技' when 8 then '美术' when 9 then '建筑' when 10 then '摄影' when 11 then '电影' when 13 then '影视' when 14 then '设计图' when 15 then '地图' when 16 then '模型' when 18 then '录音' when 19 then '录像' when 20 then '视听' else null end) as 'copyright_works_type', date_format(s_customer_other_confirm_right.insert_time, '%%y-%%m-%%d') as 'works_tpa_date' from s_customer_works_base left join s_customer_file_tsa on s_customer_works_base.id = s_customer_file_tsa.works_id left join s_customer_copyright_task on s_customer_works_base.id = s_customer_copyright_task.works_id left join s_customer_other_confirm_right on s_customer_works_base.id = s_customer_other_confirm_right.work_id where s_customer_works_base.status = 0 having tenant_id in ( select id from s_customer_info where status = 0 and test_status = 0 and full_name not regexp '花豆|刀豆|鲸版权|维权骑士|测试|test' )" + + #定义数据类型 + data_types = {'works_enter_date': types.Date(), 'works_id': types.Integer(), 'tenant_id': types.Integer(), 'works_enter_type': types.Text(), 'works_tsa_date': types.Date(), 'tsa_enter_type': types.Text(), 'works_copyright_date': types.Date(), 'copyright_enter_type': types.Text(), 'copyright_works_type': types.Text(), 'works_tpa_date': types.Date()} + + return task_basic('002', hzdd_database_connect, query, my_database_connect, 'hzdd_works_rights', data_types) + +''' + +定时任务003 + +从杭州刀豆数据库抽取作品版权检测保护数据并每日同步至刘弼仁私人数据库 + +''' + +def scheduled_task_003(): + + #定义查询语句 + query = "select date_format(s_customer_works_base.insert_time, '%%y-%%m-%%d') as 'works_enter_date', s_customer_works_base.id as 'works_id', s_customer_works_base.customer_id as 'tenant_id', ( case s_customer_works_base.source_type when 0 then '用户添加' when 1 then '天网添加' when 2 then '骑士添加' when 3 then '它方推送' when 4 then '前台添加' else '其它' end) as 'works_enter_type', date_format(s_customer_monitor_work.start_time, '%%y-%%m-%%d') as 'monitoring_start_date', date_format(s_customer_monitor_work.end_time, '%%y-%%m-%%d') as 'monitoring_end_date', date_format(s_customer_protect_works.start_time, '%%y-%%m-%%d') as 'protection_start_date', date_format(s_customer_protect_works.end_time, '%%y-%%m-%%d') as 'protection_end_date' from s_customer_works_base left join s_customer_monitor_work on s_customer_works_base.id = s_customer_monitor_work.work_id left join s_customer_protect_works on s_customer_works_base.id = s_customer_protect_works.works_id" + + #定义数据类型 + data_types = {'works_enter_date': types.Date(), 'works_id': types.Integer(), 'tenant_id': types.Integer(), 'works_enter_type': types.Text(), 'monitoring_start_date': types.Date(), 'monitoring_end_date': types.Date(), 'protection_start_date': types.Date(), 'protection_end_date': types.Date()} + + return task_basic('003', hzdd_database_connect, query, my_database_connect, 'hzdd_works_protection', data_types) + +''' + +定时任务004 + +从杭州刀豆数据库抽取订单数据并每日同步至刘弼仁私人数据库 + +''' + +def scheduled_task_004(): + + #定义查询语句 + query = "select table_temporary.tenant_id, table_temporary.order_id, table_temporary.service_name, ( case when service_name = '监控定制版' then if(service_price = 0, 2000 * order_count, order_price) when service_name = '风控定制版' then if(service_price = 0, 2000 * order_count, order_price) when service_name = '敏感词定制版' then if(service_price = 0, 2000 * order_count, order_price) when service_name = '保护定制版' then if(service_price = 0, if(order_count >= 12, 350000, reference_price * order_count), order_price) else if(service_price = 0, reference_price * order_count, order_price) end) as 'order_amount', table_temporary.order_creat_date from ( select tenant_id, order_id, service.name as 'service_name', service.price as 'reference_price', service_price, order_count, order_price, order_creat_date from ( select customer_id as 'tenant_id', order_number as 'order_id', unit_price as 'service_price', number as 'order_count', price as 'order_price', date_format(order_detail.insert_time, '%%y-%%m-%%d') as 'order_creat_date', service_id from order_detail left join ( select fk_refund_order_id from order_detail where fk_refund_order_id != 0 ) as table_temporary on id = table_temporary.fk_refund_order_id where order_detail.fk_refund_order_id = 0 and table_temporary.fk_refund_order_id is null ) as table_temporary left join service on table_temporary.service_id = service.id where service.name != '监控试用版' and tenant_id in ( select id from s_customer_info where status = 0 and test_status = 0 and full_name not regexp '花豆|刀豆|鲸版权|维权骑士|测试|test' ) ) as table_temporary left join order_info on table_temporary.order_id = order_info.order_number where order_info.status = 2 order by order_creat_date desc" + + #定义数据类型 + data_types = {'tenant_id': types.Integer(), 'order_id': types.Text(), 'service_name': types.Text(), 'order_amount': types.Integer(), 'order_creat_date': types.Date()} + + return task_basic('004', hzdd_database_connect, query, my_database_connect, 'hzdd_orders', data_types) + +''' + +每日定时项目 + +''' + +def scheduled_tasks(): + + ''' + + 依次执行定时任务 + + ''' + + response1 = scheduled_task_001() + + time.sleep(10) + + response2 = scheduled_task_002() + + time.sleep(10) + + response3 = scheduled_task_003() + + time.sleep(10) + + response4 = scheduled_task_004() + + time.sleep(10) + + messsage = response1.messsage + '\n' + response2.messsage + '\n' + response3.messsage + '\n' + response4.messsage + + response = send_mail(messsage) + + return response.code + +def scheduled_tasks(): + + print('[定时任务]-[{}]'.format(datetime.now(timezone).strftime('%y-%m-%d %H:%M:%S'))) + + +try: + + #scheduler.add_job(func = scheduled_tasks, trigger = 'cron', day = '*/1', hour = 8, minute = 30, second = 0, timezone = timezone, id = 'daily') + + scheduler.add_job(func = scheduled_tasks, trigger = 'cron', minute = '*/1', second = 0, timezone = timezone, id = 'daily') + + scheduler.start() + + print('定时任务脚本已启动') + +except: + + scheduler.shutdown(wait = False) + + print('定时任务脚本启动失败') + diff --git a/普康健康自动化录入/SQLite.db b/普康健康自动化录入/SQLite.db new file mode 100644 index 0000000..006b8ea Binary files /dev/null and b/普康健康自动化录入/SQLite.db differ