diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/reports/persona_report/template.html b/reports/persona_report/template.html new file mode 100644 index 0000000..639b0ac --- /dev/null +++ b/reports/persona_report/template.html @@ -0,0 +1,256 @@ + + + + + + + + + 分析报告 - 结网 + + + + + + + + + + + + + + + + + +
+ +

基于MovieLens的用户画像建模报告

+ +
+ + 刘弼仁 + + | + + {{report_date}} + +
+ +

报告背景

+ +

用户画像是一种将用户属性、行为和好恶等信息转化为标签并关联而形成的用户模型。其目的是连接用户需求和产品运营,帮助企业更高效、更精准的认识、理解用户,从而更有效的分配资源提供为用户提供产品和服务、提高运营效率。

+ +
+ + Ta是一个怎样的人? + +
+ + 男性,中青年,产品经理,北方人,喜欢美食、宠物和3C和网络游戏(阴阳师和原神)。 + +
+ +

本次报告数据来源于MovieLens,通过介绍用户画像建模并将其应用于用户分群和电影推荐的流程,希望读者能够对用户画像有所了解。

+ +

用户画像建模

+ +

数据集解释

+ +

本次报告使用的数据集为MovieLens10K。该数据集为GroupLens从MovieLens网站收集并提供的用户、电影和评分数据。发布时间为2009年1月1日。

+ +

数据集文件结构

+ + + +

建模流程

+ +

获取数据

+ +

通过打开本地数据文件获取数据集,其中用户数为{{users}},电影数为{{movies}},评分数为{{ratings}}。

+ +

构建标签体系

+ +

考虑本次报告所构建的用户画像将应用于用户分群和电影推荐,设计标签分类如下:

+ + + +

因为在用户分群时将使用基于核函数的主成分分析算法和K均值聚类算法、电影推荐时将使用Apriori算法,所以本次报告使用独热编码方法生成标签。

+ +
+ + 独热编码(One-Hot Encoding) + +
+ + 一种将称名或顺序型特征转化为机器学习算法容易处理的格式的方法,它为特征的每个值衍生一个二进制的新特征。在该衍生特征中,如果某份样本属于该特征值则为1,否则为0。 + +
+ +
+ + 例如,“性别”特征将会衍生“gender: male”和“gender: female”两个新特征,并会为所有样本按照衍生特征赋值0/1。其中,男性为{“gender:male”: 1},{“gender:female”: 0};女性为{“gender:male”: 0},{“gender:female”: 1}。 + +
+ +

整体用户画像

+ +

通过各特征来描绘用户整体画像。

+ +

性别

+ + + +

年龄

+ +

定义年龄小于18岁为“age: under18”,18至24岁为“age: 18~24”,25至34岁为“age: 25~34”,35至44岁为“age: 35~44”,45至54岁至“age: 45~54”,大于54岁为“age: above54”。

+ + + +

职业

+ + + +

州级行政区

+ +

根据邮政编码(美国)解析州级行政区。

+ + + +

评分数

+ +

统计每位用户评分数并按照等频分箱、分箱数为5。

+ + + +

平均评分

+ +

统计每位用户平均评分并定义为平均评分并按照等频分箱、分箱数为5。

+ + + +

最近评分最高的电影TOP1至5

+ +

统计每位用户最近评分最高的电影前五名并定义为最喜欢的电影TOP1至5。

+ + + +

喜欢的电影体裁TOP1至5

+ +

统计每位用户评分过的电影体裁数前五名并定义为喜欢的电影体裁TOP1至5。

+ + + +

用户分群

+ +

按照用户标签的关联性进行归类分组,企业可以将用户群体划分为具有相似属性、行为和好恶的细分群体。

+ +

算法说明

+ +

本次报告首先使用基于核函数的主成分分析算法就衍生特征进行降维处理,最后使用K均值聚类算法进行聚类处理并基于间隔统计量确定最优聚类簇数,最终实现用户分群目的。

+ +
+ + 基于核函数的主成分分析 + +
+ + 首先使用核函数(例如径向基)方法,将高维且线性不可分的数据映射到更高维但线性可分的特征空间,最后在该特征空间使用主成分分析算法进行降维处理。 + +
+ +
+ + 主成分分析 + +
+ + 一种常用的数据降维和特征提取的统计方法,其假设是数据内在存在线性关系,通过将数据转换到由主成分构成的坐标系统,从而在最大保留数据信息的前提下降维。 + +
+ +
+ + 核函数 + +
+ + 考虑本次报告使用独热编码方法生成的标签数据具有高维性和稀疏性,通常使用基于余弦相似度核函数(Cosine Similarity Kernel)的主成分分析算法的降维效果更好。 + +
+ +
+ + K均值聚类 + +
+ + 一种无监督学习的基于距离的聚类算法,其目标是将数据点划分为K个簇,使得相同簇的数据点尽可能相同、不同簇的数据点尽可能不同。缺点是需要预先指定聚类簇数,本报告使用间隔统计量确定最优聚类簇数。 + +
+ +
+ + 间隔统计量(GapStatistic) + +
+ + 用于评估聚类效果的统计方法,公式如下: + + \[Gap(K)=E(log{D}_{K})-log{D}_{K}\] + + 其中,\({D}_{K}=\sum {\sum {dist{(x,c)}^{2}}}\)。选择\(Gap(K)>=Gap(K+1)\)的最小K值作为最优聚类簇数。 + +
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/reports/scorecard_report/template.html b/reports/scorecard_report/template.html new file mode 100644 index 0000000..4bd3f57 --- /dev/null +++ b/reports/scorecard_report/template.html @@ -0,0 +1,283 @@ + + + + + + + + + 分析报告 - 结网 + + + + + + + + + + + + + + + + + +
+ +

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

+ +
+ + 刘弼仁 + + | + + {{report_date}} + +
+ +

建模背景

+ +

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

+ +

本报告数据来源于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个。数据预览如下:

+ + + +

数据预处理

+ +

数据清洗

+ +

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

+ +

缺失值处理

+ +

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

+ +

异常值处理

+ +

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

+ +

特征变量证据权重编码

+ +

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

+ +

本次报告使用决策树进行分箱,分箱后使用证据权重编码。以特征变量“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. + +
+ +

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

+ + + +

评分卡开发和验证

+ +

评分卡开发

+ +

本次报告中贷款申请评分卡公式为(本次报告控制\(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 证据权重\)。

+ +

评分卡验证

+ +

本次报告使用柯斯和提升统计量评估评分卡,柯斯统计量为{{ks}},提升统计量为{{lift}}

+ +

柯斯统计量说明

+ +

柯斯统计量全称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/rfm/template.html b/rfm/template.html new file mode 100644 index 0000000..16def59 --- /dev/null +++ b/rfm/template.html @@ -0,0 +1,238 @@ + + + + + + + + + 数据报告 + + + + + + + + + +
+ +

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

+ +
+ + 刘弼仁 + + | + + {{report_date}} + +
+ +

分析背景

+ +

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

+ +

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

+ +

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

+ +

分析过程

+ +

数据预览

+ + + +

数据集共{{sample_size}}份样本。其中,客户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/交易金额占比.html b/rfm/交易金额占比.html new file mode 100644 index 0000000..a076391 --- /dev/null +++ b/rfm/交易金额占比.html @@ -0,0 +1,224 @@ + + + + + + Awesome-pyecharts + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/rfm/客户分类分布.html b/rfm/客户分类分布.html new file mode 100644 index 0000000..bd5a10b --- /dev/null +++ b/rfm/客户分类分布.html @@ -0,0 +1,613 @@ + + + + + + Awesome-pyecharts + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/rfm/客户占比.html b/rfm/客户占比.html new file mode 100644 index 0000000..3bfa813 --- /dev/null +++ b/rfm/客户占比.html @@ -0,0 +1,224 @@ + + + + + + Awesome-pyecharts + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/utils/templates/table.html b/utils/templates/table.html new file mode 100644 index 0000000..0113663 --- /dev/null +++ b/utils/templates/table.html @@ -0,0 +1,59 @@ + + + + + + pyecharts + + + {{ html_content }} + + \ No newline at end of file diff --git a/任务调度服务器/workspace.yaml b/任务调度服务器/workspace.yaml new file mode 100644 index 0000000..f0d30c9 --- /dev/null +++ b/任务调度服务器/workspace.yaml @@ -0,0 +1,2 @@ +load_from: + - python_file: dagster.py \ No newline at end of file diff --git a/普康健康自动化录入/template.html b/普康健康自动化录入/template.html new file mode 100644 index 0000000..e0355b9 --- /dev/null +++ b/普康健康自动化录入/template.html @@ -0,0 +1,602 @@ + + + + + + 赔案档案 + + + +
+
+
+
+

赔案档案

+
+

赔案编号: {{ dossier.赔案层.赔案编号 }} | 保险总公司: {{ dossier.赔案层.保险总公司 }}

+
+
+
+
+ +
+

影像件层

+ + + + + + + + + + + + {% for image in dossier.影像件层 %} + + + + + + + + {% endfor %} + +
影像件序号影像件名称已分类(含旋正)影像件类型已识别
{{ image.影像件序号 }}{{ image.影像件名称 }}{{ image.已分类 }}{{ image.影像件类型 }}{{ image.已识别 }}
+
+ +
+

赔案层

+
+
+

申请人信息

+
+
+
与被保险人关系
+
{{ dossier.赔案层.申请人信息.与被保险人关系 }}
+
+
+
姓名
+
{{ dossier.赔案层.申请人信息.姓名 }}
+
+
+
证件类型
+
{{ dossier.赔案层.申请人信息.证件类型 }}
+
+
+
证件号码
+
{{ dossier.赔案层.申请人信息.证件号码 }}
+
+
+
证件有效期
+
{{ dossier.赔案层.申请人信息.证件有效期起 | date }} 至 {{ + dossier.赔案层.申请人信息.证件有效期止 | date }} +
+
+
+
性别
+
{{ dossier.赔案层.申请人信息.性别 }}
+
+
+
出生
+
{{ dossier.赔案层.申请人信息.出生 | date }} | {{ + dossier.赔案层.申请人信息.年龄 }}岁 +
+
+
+
手机号
+
{{ dossier.赔案层.申请人信息.手机号 }}
+
+
+
住址
+
{{ dossier.赔案层.申请人信息.省 }} {{ dossier.赔案层.申请人信息.地 }} {{ + dossier.赔案层.申请人信息.县 }} +
+
{{ dossier.赔案层.申请人信息.详细地址 }}
+
+
+
+ +
+

受益人信息

+
+
+
与被保险人关系
+
{{ dossier.赔案层.受益人信息.与被保险人关系 }}
+
+
+
户名
+
{{ dossier.赔案层.受益人信息.户名 }}
+
+
+
开户银行
+
{{ dossier.赔案层.受益人信息.开户银行 }}
+
+
+
银行账号
+
{{ dossier.赔案层.受益人信息.银行账号 }}
+
+
+
+
+
+ +
+

发票层

+ {% for invoice in dossier.发票层 %} +
+
+
+
{{ invoice.票据类型 }} | {{ invoice.票据号码 }}
+ 关联影像件序号: {{ invoice.关联影像件序号 }} +
+
+ {% if invoice.查验状态 == '真票' %} + {{ invoice.查验状态 }} + {% elif invoice.查验状态 == '无法查验' %} + {{ invoice.查验状态 }} + {% else %} + {{ invoice.查验状态 }} + {% endif %} +
+
+ +
+
+
就诊人
+
{{ invoice.就诊人 }}
+
+
+
票据代码
+
{{ invoice.票据代码 }}
+
+
+
校验码后六位
+
{{ invoice.校验码后六位 }}
+
+
+
开票日期
+
{{ invoice.开票日期 | date }}
+
+
+
票据金额
+
{{ invoice.票据金额 }}元
+
+
+
就诊类型
+
{{ invoice.就诊类型 }}
+
+
+
医药机构
+
{{ invoice.医药机构 }}
+
+
+
推定疾病
+
{{ invoice.推定疾病 }}
+
+
+ + + + + + + + + + + + {% for item in invoice.项目 %} + + + + + + + {% endfor %} + +
大项小项数量金额
{{ item.大项 }}{{ item.小项 }}{{ item.数量 }}{{ item.金额 }}元
+
总金额: {{ invoice.票据金额 }}元
+
+ {% endfor %} +
+ + +
+ + \ No newline at end of file diff --git a/普康健康自动化录入/test.py b/普康健康自动化录入/test.py new file mode 100644 index 0000000..bf03dd2 --- /dev/null +++ b/普康健康自动化录入/test.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- + +import json +import re +from csv import DictReader, DictWriter +from pathlib import Path +from typing import List, Dict + +import torch +from transformers import BertTokenizerFast, BertForTokenClassification + + +# 命名实体识别 +class NER: + def __init__(self): + # 实体标签映射 + self.label_map = { + 0: "O", # 非药品命名实体 + 1: "B-DRUG", # 药品命名实体-开始 + 2: "I-DRUG", # 药品命名实体-中间 + } + + # 加载预训练分词器 + self.tokenizer = BertTokenizerFast.from_pretrained( + pretrained_model_name_or_path=Path("./models/bert-base-chinese").resolve() + ) + + # 加载预训练模型 + self.model = BertForTokenClassification.from_pretrained( + pretrained_model_name_or_path=Path("./models/bert-base-chinese").resolve(), + ) + + # 设置模型为预测模式 + self.model.eval() + + def recognize_drugs(self, text: str) -> List[Dict]: + """识别药品命名实体""" + + if not text.strip(): + return [] + + # 分词编码 + inputs = self.tokenizer( + text, + return_tensors="pt", + padding=True, + truncation=True, + return_offsets_mapping=True, + ) + + # TOKEN于文本中起止位置 + offset_mapping = inputs.pop("offset_mapping")[0].cpu().numpy() + + with torch.no_grad(): + # 模型预测 + outputs = self.model(**inputs) + # 获取TOKEN预测标签 + predictions = torch.argmax(outputs.logits, dim=2) + + entities = [] + current_entity = None + + # 遍历所有TOKEN、预测标签索引和起止索引 + for token, offset, label_id in zip( + self.tokenizer.convert_ids_to_tokens(inputs["input_ids"][0]), + offset_mapping, + predictions[0].cpu().numpy(), + ): + print(label_id) + continue + + # 映射TOKEN标签 + label = self.label_map.get(label_id, "O") + + # 若遇到特殊TOKEN则跳过 + if ( + token in ["[CLS]", "[SEP]", "[PAD]"] + or offset[0] == 0 + and offset[1] == 0 + ): + continue + + if label == "B-DRUG": + if current_entity: + self._combine_tokens(current_entity, text) + entities.append(current_entity) + + current_entity = { + "start": offset[0], + "end": offset[1], + "tokens": [token], + "offsets": [offset], + "type": label, + } + + elif label == "I-DRUG": + if current_entity: + if offset[0] == current_entity["end"]: + current_entity["end"] = offset[1] + current_entity["tokens"].append(token) + current_entity["offsets"].append(offset) + else: + self._combine_tokens(current_entity, text) + entities.append(current_entity) + current_entity = { + "start": offset[0], + "end": offset[1], + "tokens": [token], + "offsets": [offset], + "type": label, + } + + else: + if current_entity: + self._combine_tokens(current_entity, text) + entities.append(current_entity) + current_entity = None + + if current_entity: + self._combine_tokens(current_entity, text) + entities.append(current_entity) + + return entities + + @staticmethod + def _combine_tokens(current_entity: Dict, text: str): + """合并TOKEN""" + + # 从文本中提取命名实体文本 + current_entity["text"] = text[current_entity["start"] : current_entity["end"]] + + +""" + +# 使用示例(需要训练好的模型) +dl_ner = NER() +text = "患者需要硫酸吗啡缓释片治疗癌症疼痛" +entities = dl_ner.recognize_drugs(text) +print(entities) + +exit() + +""" + + +def drug_extraction(text) -> tuple[str, str | None]: + """药品数据提取""" + + # 正则匹配两个“*”之间内容作为药品类别,第二个“*”之后内容作为药品名称。 + if match := re.match( + pattern=r"\*(?P.*?)\*(?P.*)", + string=(text := text.strip()), + ): + # 药品类别 + drug_type = match.group("drug_type").strip() + + # 药品名称 + drug_name = ( + match.group("drug_name") + .upper() # 小写转大写 + .replace("(", " ") + .replace(")", " ") + .replace("(", " ") + .replace(")", " ") + .replace("[", " ") + .replace("]", " ") + .replace("【", " ") + .replace("】", " ") + .replace(":", " ") + .replace(":", " ") + .replace(",", " ") + .replace(",", " ") + .replace("·", " ") + .replace("`", " ") + .replace("@", " ") + .replace("#", " ") + .replace("*", " ") + .replace("/", " ") # 就指定符号替换为空格 + .strip() + ) + + # 就药品名称中多个空格替换为一个空格 + drug_name = re.sub(pattern=r"\s+", repl=" ", string=drug_name) + + for section in drug_name.split(" "): + print(section) + + # 若匹配失败则药品类型默认为文本、药品名称默认为None + else: + drug_type, drug_name = text, None + + return drug_type, drug_name + + +dataframe = [] + +# 就票据查验结果和疾病对应关系进行数据清洗(暂仅考虑增值税发票且为真票) +with open("票据查验结果和疾病对应关系.csv", "r", encoding="utf-8") as file: + for row in DictReader(file): + try: + disease = row["疾病"] + + response = json.loads(row["票据查验结果"]) + + # 遍历项目 + for item in response["data"]["details"]["items"]: + + name = item["name"] + + drug_extraction(name) + + exit() + + except Exception as e: + print(e) + exit() + +with open("1.csv", "w", newline="", encoding="utf-8") as file: + writer = DictWriter(file, fieldnames=dataframe[0].keys()) + writer.writeheader() + writer.writerows(dataframe)