简介面向金融科技与图数据库课程的作业实践这份资料实现了一个基于图数据库的信贷风险分析与反欺诈系统完整覆盖信贷交易数据的图结构存储、信贷风险模型构建、前端可视化展示与后端数据处理全流程适合高校学生、竞赛团队及入门图数据库的开发者参考学习也可作为课程设计或毕业设计的原型基础。包体为zip压缩格式共13个文件约7.94MB包含Python脚本、Jupyter Notebook、说明文档、RAR数据包、CSV统计结果及图片等其中py脚本承担图形工具封装、服务调用、图数据评测等逻辑ipynb用于模拟数据生成与图查询测试文本和docx文档补充说明使用方法与设计思路整体分工清晰。目前已有32人学习下载。通过该项目可系统掌握图数据库在金融风控中的落地方式例如如何将借贷关系、交易记录映射为节点与边如何利用关联查询识别风险点和反欺诈信号并借助可视化能力辅助分析同时可获得一份可直接运行或二次开发的课程作业源码配套模拟数据、统计导出结果和附赠说明文档便于快速复现和扩展。1. 信贷反欺诈课程作业里图数据库到底能查出什么银行信贷场景里最难查的往往不是单一客户的坏账而是坏账之间的“关系”同一部手机注册了多个借贷账号、同一个紧急联系人在多笔借据里反复出现、A转给B的钱隔两层又转回A。这些信号在关系型数据库里要写十几次自连接才可能捞出来而一个基于图数据库的信贷风险分析与反欺诈系统核心做法正是把这些实体和交易转成图结构存储再用 Cypher 直接跑路径、环和社区。接下来按我搭建这套系统时的顺序讲先建图模型再写风险查询然后接前后端做可视化分析最后补最容易翻车的几个坑。适合正在做这门课程作业、需要交付一个能跑通能答辩的完整项目的人也适合想用最少代码接触图数据库落地的后端同学。2. 信贷数据的图结构存储从关系表到节点与边的建模2.1 先画图模型把借款人、借据、设备映射成节点与关系课程作业最容易犯的第一个错误是拿到数据就导入边建边想最后查询写不出来。做图结构存储必须先画模型而且要让模型能对应到真实风控问题多头借贷、资金循环、共用设备、失联修复。常见做法是把“实体”设计成节点“行为”设计成关系。实体有借款人、借据、公司、设备、银行卡行为有申请借款、转账、登录、填联系人。对应到 Neo4j 里一张比较完整的信贷图模型长这样节点标签关键属性建模说明Personid, name, id_card, phone借款人和关联人全图核心Loanloan_id, contract_no, amount, apply_date一笔借款合同连接借款人和放款机构Companyid, name, credit_code借款人任职或担保的单位Devicedevice_id, device_type登录设备指纹团伙识别关键BankAccountaccount_no, bank_name收款卡和还款卡资金流经节点关系类型两端属性风控含义APPLIED_FORPerson - Loanapply_date谁在何时申请了贷款FROM_BANKLoan - Companychannel放款机构用于统计多头LOGIN_WITHPerson - Devicelast_login人机关联查共用设备CALL_RELATEPerson - Personrelation紧急联系人关系查关联网络TRANSFER_TOPerson - Personamount, ts聚合转账边用于路径和环检测TRANSFER_TOBankAccount - BankAccountamount, ts原始流水边带账户级明细这里有一个建模决策值得说清楚要不要把 BankAccount 单独建成节点。我的建议是保留。因为真实信贷流水里“A 转 B”看起来是两个人的关系但实际是 A 的卡转到 B 的卡如果不经过账户节点多卡归集、快进快出这些特征就丢了。写课程作业时可以在入图阶段把原始流水做成账户级边同时在 Person 之间再维护一条聚合转账边这样查“资金环”时路径短查明细时又能下钻到账户。2.2 建库与导入Cypher 建约束CSV 灌入交易数据模型定好后先建约束再导数据。约束的作用有两层一是保证 id 唯一二是自动建索引否则后面按 id 匹配会全库扫描。// 4.x 及以上版本语法3.x 用 CREATE CONSTRAINT ON (p:Person) ASSERT p.id IS UNIQUE CREATE CONSTRAINT person_id IF NOT EXISTS FOR (p:Person) REQUIRE p.id IS UNIQUE; CREATE CONSTRAINT loan_id IF NOT EXISTS FOR (l:Loan) REQUIRE l.loan_id IS UNIQUE; CREATE CONSTRAINT device_id IF NOT EXISTS FOR (d:Device) REQUIRE d.device_id IS UNIQUE;约束建完再导数据。课程作业一般给你的是 CSV常见做法是放到 Neo4j 安装目录的 import 文件夹下然后用 LOAD CSV 导入LOAD CSV WITH HEADERS FROM file:///persons.csv AS row MERGE (p:Person {id: row.id}) SET p.name row.name, p.phone row.phone, p.id_card row.id_card;这里用 MERGE 而不是 CREATE因为 CSV 可能重复导入MERGE 是幂等的跑两遍不会生成重复节点。SET 负责把变化的字段覆盖更新。导入转账流水时情况复杂一点要先 MATCH 两端节点再 MERGE 关系LOAD CSV WITH HEADERS FROM file:///transfers.csv AS row MATCH (from:Person {id: row.from_id}) MATCH (to:Person {id: row.to_id}) MERGE (from)-[t:TRANSFER_TO {txn_id: row.txn_id}]-(to) SET t.amount toFloat(row.amount), t.ts datetime(row.tx_time);逻辑说明txn_id 作为关系的业务主键避免重复导入产生重边toFloat 把金额从字符串转成数值之后做“转账金额大于 10 万”这类过滤时才能比较datetime() 把时间字符串转成时间类型方便按时间窗口切数据。参数说明如果 CSV 是分片导入的大文件可以在 LOAD CSV 后面加字段过滤提前排除无关行减少无效匹配。2.3 为什么要用图结构存储一条 SQL 和多度查询的对比有些同学在答辩时会被问“关系型数据库也能做反欺诈为什么用图”。这个问题要提前准备好。以“查 P001 到 P017 之间是否存在 4 跳以内的转账链路”为例关系型数据库要写 4 次自连接SQL 会膨胀到三四十行而且跳数越多需要提前写死的 JOIN 层级越多无法灵活扩展到 6 跳。图数据库一条可变长度路径查询搞定跳数上限在查询时用参数控制。MATCH path (a:Person {id: P001})-[:TRANSFER_TO*1..5]-(b:Person {id: P017}) RETURN [n IN nodes(path) | n.id] AS person_chain, length(path) AS hop_count;这个查询的返回值是路径上的所有节点 id 列表以及路径长度。可变长度关系 *1..5 的意思是从 1 跳到 5 跳之间任意长度都匹配。开发时注意跳数上限一定要写死不要让用户从前端传一个“无边界”的深度否则在密集图上会爆炸式展开直接拖垮数据库。实际课程作业里几百个节点没感觉一旦扩到真实规模无界跳数就是性能黑洞。3. 在图数据库上跑风险模型三类图谱指标与反欺诈规则的 Cypher 实现3.1 直连风险指标多头借贷和共用联系人的查询图模型建好后风险规则要能落到可解释的查询上。第一类指标是“直连风险”即从一个借款人出发一跳之内能统计出的异常。多头借贷是最典型的例子。一个正常借款人往往只跟一两家银行打交道如果一个人向 5 家以上机构借过钱说明他可能靠借新还旧维持资金链MATCH (p:Person)-[:APPLIED_FOR]-(l:Loan)-[:FROM_BANK]-(c:Company) WITH p, count(DISTINCT c) AS bank_count WHERE bank_count 3 RETURN p.id AS person_id, p.name, bank_count ORDER BY bank_count DESC;这里count(DISTINCT c) 是关键。如果不加 DISTINCT同一家银行给同一个人放了两笔贷款会被算成两个机构导致误报。宁可阈值设低一点也要让口径真实。返回结果时按 bank_count 降序排方便前端可视化分析面板把高风险客户放在最前面。第二类直连特征是“共用联系人”。正常人的紧急联系人只有几个如果一个联系人在 10 个借款人那里都出现这个人要么是黑中介要么是专门被拿来填表的“公共联系人”MATCH (p1:Person)-[:CALL_RELATE]-(c:Person)-[:CALL_RELATE]-(p2:Person) WHERE p1.id p2.id RETURN c.name AS contact, count(DISTINCT p1.id) AS borrower_count ORDER BY borrower_count DESC LIMIT 20;逻辑说明这个查询先找到中间联系人 c再统计通过 CALL_RELATE 关系连到 c 的不同借款人数量。WHERE p1.id p2.id 是为了排除自己填自己当联系人的情况。borrower_count 超过 5 一般就值得拉出来看一眼超过 10 基本可以直接送人工核查。3.2 传导风险与团伙识别环检测、最短路径与社区发现直连指标能抓个人风险抓不了团伙。团伙欺诈的特征是“闭环”A 转给 BB 转给 CC 又转回 A形成一个资金环。环在关系型数据库里很难查在图上就是一条起点等于终点的路径MATCH path (a:Person)-[:TRANSFER_TO*2..6]-(a) WHERE length(path) 2 RETURN [n IN nodes(path) | n.id] AS loop_persons, length(path) AS loop_len, [r IN relationships(path) | r.amount] AS loop_amounts LIMIT 20;逻辑说明这里基于前面建模时维护的 Person 聚合转账边来查询路径是 a - b - c - a 这种闭环。返回的 loop_persons 展示环上所有人loop_amounts 展示每一笔金额。如果环上每笔金额都接近同一个数比如 5 万大概率是刷流水的操作。环检测的短路径版本是查“两个看似无关的人之间的关联链”。风控里常用来做关联黑名单传导MATCH (a:Person {id: P001}), (b:Person {id: P017}) MATCH path shortestPath((a)-[:TRANSFER_TO|LOGIN_WITH|CALL_RELATE*..5]-(b)) RETURN [n IN nodes(path) | n.id] AS chain, length(path) AS hop_count;注意shortestPath 会限制搜索范围必须加跳数上限否则图一旦变大这个查询就是灾难。关系类型竖线分隔意思是三类关系都可以作为链路这符合风控的直觉没转过账也可能共用设备没共用设备也可能填过同一个联系人。第三个指标是社区发现。团伙往往不是一个环而是几十个人共用一套设备、一批联系人的网状结构。用社区发现把关系图切分成一个个团伙再对每个团伙做集中度统计比一条条拉路径效率高很多。Neo4j 里用 GDS 图数据科学库的 Louvain 算法做这件事// 先把图投影到内存TRANSFER_TO 边带金额属性 CALL gds.graph.project(credit, Person, { TRANSFER_TO: { properties: [amount] }, CALL_RELATE: {} }); // 跑 Louvain 社区发现输出社区 id CALL gds.louvain.stream(credit) YIELD nodeId, communityId RETURN gds.util.asNode(nodeId).id AS person_id, communityId ORDER BY communityId;参数说明credit 是投影图的名称后面是节点类型和关系类型TRANSFER_TO 边只保留 amount 属性CALL_RELATE 不需要属性。Louvain 是无监督算法同一社区里的人往往有密集的转账和联系人关系把社区人数和社区内黑名单人数交叉比对能快速定位疑似团伙。跑完记得删投影图释放内存CALL gds.graph.drop(credit)。3.3 把图谱指标落成分数阈值和规则优先级怎么定查询写好了还得定阈值。课程作业答辩时被问最多的问题就是“你怎么确定这个规则有效”。我的经验是先用训练集把每个指标的分布跑出来观察正常客户和已知黑名单客户的重叠区间再选“能把两类人分开”的阈值。风险类型触发条件建议处置多头借贷关联机构数 3降额或人工复核共用联系人同一联系人关联借款人 5标记疑似中介资金环2~6 跳内存在闭环冻结审批人工调查共用设备同一设备登录账号 3标记团伙风险黑名单关联与黑名单路径长度 2收紧授信额度规则优先级我一般这样排资金环 共用设备 共用联系人 多头借贷 黑名单关联。原因是前两类直接指向团伙欺诈作案意图明显多头借贷只能说明“缺钱”不一定有欺诈意图。落地时不需要造复杂的分数模型课程作业里用规则打分就行命中一条加 20 分总分超过 60 触发人工复核超过 80 直接拒贷。这样做的好处是每条规则都可解释答辩时能讲清楚为什么给这个客户打分。4. 前后端数据打通图查询 API 与可视化分析面板的实现路径4.1 后端接口用 Python Flask 封装图查询 API图数据库本身不直接对前端开放中间要有一层后端接口。课程作业我一般推荐 Python Flask因为代码量少、驱动简单规规矩矩分层。生产环境换 Spring Boot 也是同样的思路只是把 driver 换成 neo4j-java-driver。先装依赖并初始化驱动pip install flask neo4j flask-corsfrom flask import Flask, request, jsonify from neo4j import GraphDatabase app Flask(__name__) driver GraphDatabase.driver( bolt://localhost:7687, auth(neo4j, your_password) ) app.route(/api/subgraph, methods[GET]) def subgraph(): pid request.args.get(personId, ) depth int(request.args.get(depth, 3)) limit int(request.args.get(limit, 80)) nodes {} links [] with driver.session() as session: result session.run( MATCH (a:Person {id:$pid})-[*1..$depth]-(b) RETURN a.id AS source, labels(a)[0] AS source_label, b.id AS target, labels(b)[0] AS target_label LIMIT $limit, pidpid, depthdepth, limitlimit ) for rec in result: nodes.setdefault(rec[source], rec[source_label]) nodes.setdefault(rec[target], rec[target_label]) links.append({from: rec[source], to: rec[target]}) return jsonify({ nodes: [{id: nid, label: label} for nid, label in nodes.items()], links: links }) if __name__ __main__: app.run(host127.0.0.1, port5000, debugTrue)逻辑说明接口接收三个参数personId 是中心客户depth 展开层数limit 控制返回数量。查询用了参数化 Cypher避免字符串拼接导致注入风险。LIMIT $limit 是后端硬限制防止前端传一个过大的值把图数据库拖垮。返回结构里 nodes 去重links 保留每条边。注意一个坑Cypher 的可变长度跳数参数 $depth 在 Neo4j 4.0 之后才支持。如果你们的 Neo4j 是 3.x这里会报语法错误。低版本常见做法是把 depth 拼进查询字符串但拼接前必须校验它是一个 1 到 6 之间的整数depth max(1, min(int(request.args.get(depth, 3)), 6))用 int 转换和上下限截断双保险就不会出现“用户传了 99 层导致图数据库卡死”的翻车现场。4.2 前端面板用 ECharts 绘制关系图与数据下钻图结构存储的可视化分析是这门课的验收重点界面不用花哨但关系图必须能拖拽、缩放、点节点下钻。前端我一般用 ECharts 的 graph 系列不需要额外引关系可视化专用库学习成本低。后端接口数据格式定好后前端这样渲染import * as echarts from echarts; const chart echarts.init(document.getElementById(graph)); const baseUrl http://127.0.0.1:5000/api/subgraph; async function renderGraph(personId) { const resp await fetch(${baseUrl}?personId${personId}depth3limit80); const data await resp.json(); chart.setOption({ tooltip: { formatter: (p) 客户ID${p.data.id}br类型${p.data.label} }, title: { text: 以 ${personId} 为中心的关联图谱, left: center }, series: [{ type: graph, layout: force, roam: true, draggable: true, data: data.nodes.map((n) ({ id: n.id, name: n.id, category: n.label, symbolSize: n.label Person ? 38 : 26, itemStyle: { color: n.label Person ? #e06343 : n.label Device ? #37a2da : #bda29a } })), links: data.links.map((l) ({ source: l.from, target: l.to })), force: { repulsion: 380, edgeLength: [40, 110] }, label: { show: true, fontSize: 10 } }] }); } chart.on(click, (params) { if (params.dataType node params.data.label Person) { renderGraph(params.data.id); } }); renderGraph(P001);逻辑说明fetch 请求后端接口拿到节点和边setOption 配置 ECharts 关系图。force 布局的效果由 repulsion 和 edgeLength 控制repulsion 越大节点分开越明显edgeLength 越小边越短越紧凑。roam 开启缩放和拖拽节点多的时候能手动浏览。点击事件实现下钻图上点任何一个 Person 节点自动以它为新一轮中心重新拉图。前后端分离在这个项目里的意思就是Flask 只提供 JSON 数据ECharts 只负责渲染两者通过 HTTP 接口解耦。这样后端换语言、前端换框架都不影响逻辑。4.3 前后端联调注意节点上限与布局参数联调时最容易出问题的是“数据太大前端画不动”。ECharts 画几百个节点没问题但图数据库里一笔转账关系就能查出上千条边浏览器直接卡死。我的习惯是后端接口默认限制 80 个节点前端再加一个总边数保护if (data.links.length 150) { alert(路径展开过大已限制显示请缩小查询范围); return; }前后端还需要约定一个“无结果”和“节点不存在”的返回格式。Flask 接口里如果 pid 查不到Cypher 会返回空结果集此时返回空列表而不是抛错前端对应渲染“未找到该用户”的空状态。跨域问题只在前后端不同端口时出现。Flask 开启 CORSfrom flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})课程作业里直接用 * 省事注意上生产时要收紧到具体域名。5. 图数据库信贷项目避坑5 个高频翻车点与排查方法5.1 CSV 中文乱码数据库默认 UTF-8你的文件是 ANSI现象LOAD CSV 导入后人的名字、公司名全是乱码看起来像“锟斤拷”一类的字符。原因Windows 环境下 Excel 保存 CSV 默认是 ANSI 编码而 Neo4j 的 LOAD CSV 默认按 UTF-8 解析两边对不上。解决导入前用 Python 批量转码不要手动另存为。手动另存很容易漏文件而且是玄学跑第二次可能又乱码import pandas as pd df pd.read_csv(persons.csv, encodinggbk) df.to_csv(persons_utf8.csv, indexFalse, encodingutf-8-sig)Saved with utf-8-sig 会在文件头加 BOMNeo4j 能正确识别如果文件本来就是 UTF-8编码参数改成 utf-8 即可。导入前先跑一条 COUNT 验证一下中文属性能不能正常显示再全量导。5.2 数据量一上来查询就卡死索引与堆内存配置现象几十万行流水导进去之后一条“按 id 查节点”的查询要十几秒有的查询直接超时。原因没有建约束和索引Neo4j 匹配节点时全库扫描或者默认堆内存太小根本没给图库“思考”的空间。解决第一步补约束和索引。前面建过的唯一约束会自动带索引但其他属性也要按需补CREATE INDEX person_phone IF NOT EXISTS FOR (p:Person) ON (p.phone); CREATE INDEX loan_apply_date IF NOT EXISTS FOR (l:Loan) ON (l.apply_date);第二步检查 Neo4j 配置文件 conf/neo4j.conf 里的堆内存参数。默认值在数据量大时不够用dbms.memory.heap.initial_size1G dbms.memory.heap.max_size2G dbms.memory.pagecache.size1G改完需要重启 Neo4j。如果重启后查询还是慢基本可以确定是 Cypher 写法问题最常见的就是可变长度跳数没写上限检查一下查询里有没有[*]或者[*1..]改成[*1..5]会立竿见影。5.3 前端关系图挤成一团布局参数还是数据太多现象ECharts 图是出来了但所有节点堆在画布中央根本看不清谁连着谁。原因两种情况。一是节点数太多超过两三百个force 布局跑不动二是 repulsion 参数设太小节点间排斥力不够全被吸在一起。解决先调参数repulsion 从默认值往上加到 400 左右观察效果。注意 edgeLength 和 repulsion 需要配合调只调一个往往没用。如果节点确实多后端接口限制返回节点数同时前端用legend开关控制某类节点显隐legend: { data: [Person, Device, BankAccount], selectedMode: multiple }前端还有一个细节每次重新渲染前调用chart.clear()否则下钻会残留上一张图的节点图层越叠越乱。这也是我做可视化分析面板时踩过的坑一开始没 clear连续下钻三次后图就成黑匣子了。5.4 把金额建模成节点查询路径绕远一倍现象想查“转账金额超过 10 万的借款关系”Cypher 写了十几行还得 JOIN 两次中间节点跑出来结果还不对。原因建模时把“金额”这种属性做成了独立的 Amount 节点导致 TRANSFER_TO 关系上没有了 amount 属性。这属于过度建模事件属性放进关系里不该单独成节点。解决金额、时间、渠道一律建模为关系的属性。正确的查询是这样MATCH (a:Person)-[t:TRANSFER_TO]-(b:Person) WHERE t.amount 100000 RETURN a.id, b.id, t.amount, t.ts;而错误建模会写成(a)-[:HAS_AMOUNT]-(amt)-[:PAID_TO]-(b)查询要多走一层索引也没法直接用纯属给自己挖坑。反过来的教训是身份证号、手机号这种有唯一识别价值的实名信息才值得建唯一索引或独立节点普通数值属性老老实实放在边上。5.5 驱动版本不匹配Python 驱动和 Neo4j 版本跨代现象Flask 后端启动没问题一执行查询就报Failed to establish connection或者The client is unauthorized due to authentication failure。原因neo4j 驱动和 Neo4j 服务端版本跨代。3.x 的 Neo4j 用 4.x 驱动连bolt 协议握不上手驱动版本过旧连 5.x 服务端也会有兼容问题。解决装驱动时直接对齐 Neo4j 大版本。Neo4j 4.4 的 Docker 或安装包就用pip install neo4j4.4.*5.x 用 5.x 驱动。一个快速确认方法python -c import neo4j; print(neo4j.__version__)如果项目时间紧我推荐直接用 Docker 起一个 Neo4j 5 的容器配合同版本驱动省去本地安装的环境问题。跑这个命令时注意先确认端口 7687 没被占用占满时驱动报错会误导你去查用户名密码。6. 加分项用设备指纹与环路检测把反欺诈模型做厚课程作业的验收通常会看“风险规则识别的准确率”。没有真实黑名单数据时最好的办法是造一组已知答案的合成数据自验证规则能命中再拍胸脯说系统能跑。先在测试库里造一个三人资金环和两台共用设备MERGE (a:Person {id: A}) MERGE (b:Person {id: B}) MERGE (c:Person {id: C}) MERGE (a)-[:TRANSFER_TO {amount: 50000}]-(b) MERGE (b)-[:TRANSFER_TO {amount: 50000}]-(c) MERGE (c)-[:TRANSFER_TO {amount: 50000}]-(a) MERGE (d:Device {device_id: MEIZU-001}) MERGE (a)-[:LOGIN_WITH]-(d) MERGE (b)-[:LOGIN_WITH]-(d);然后跑设备指纹聚合查询找出共用设备的所有人MATCH (p:Person)-[:LOGIN_WITH]-(dev:Device) WITH dev, count(p) AS user_count WHERE user_count 2 MATCH (p:Person)-[:LOGIN_WITH]-(dev) RETURN dev.device_id, collect(p.id) AS users, user_count;逻辑说明第一段 MATCH 统计每台设备的关联人数过滤掉只登录一个人的设备第二段 MATCH 重新取人collect 聚合出同一设备上所有使用者。这个查询比环检测更快见效因为设备指纹的团伙特征非常明显三个不相干的人在同一台手机上完成注册、申请、转账基本可以锁定欺诈团伙。把这条规则加进反欺诈系统后正常的可视化分析图里就能看到设备节点连着多个 Person前端高亮这类节点演示效果比单纯跑一个多头借贷更抓人。我做一个图项目有个习惯先写节点和关系的字典再动手建库改模型比写查询贵十倍。这条经验是从“把金额建模成节点”那次翻车里换来的后来项目维护顺畅了不少。希望帮到你。本文还有配套的精品资源点击获取