以前发过一个流水穿透闭环工具,传送门:银行流水闭环核查工具

那个是我用VBA写的,可以指定一个终点,穿透所有从起点账户到终点账户的路径,这个工具主要有几个问题:

一是没有考虑时间因素,比如5号A→B,3号B→C,这条路径应该是错的(资金的时间先后),但还是被穿出来了;

二是必须要设置一个终点,如果我想看所有的流水是怎么发散的,看不到;

三是可视化程度比较一般

对于第一个问题,我其实琢磨过很久,如果流水的完整性不能保证的话,日期其实没有太大意义,比如5号A→B,3号B→C,如果考虑日期先后,那么这个路径就不成立了,但万一是因为你少要了一张卡的流水,或者他取现了,缺了2号A→B的数据呢,那么实际应该穿透的反而没有穿透出来..不过完全不考虑日期也有点说不过去..

这次利用了自己的服务器,重写并部署了一个可视化的流水穿透工具给大家免费使用,大概长这样↓

打开网易新闻 查看精彩图片

看似比较乱... 别急,接着看↓

01

首先鼠标放在账户的节点(节点就是这个圆圈,代表这个账户)上,可以显示所有他所在的路径(计算了时间先后顺序),比如这里右下角的U账户↓

打开网易新闻 查看精彩图片

02

其次,我们可以看到节点有大小之分,颜色深浅之分,线有粗细之分

节点的大小,代表他的边数,最大的点,代表经过他的线是最多的,也就是资金汇集或者分发的地方,比如这个这个最大的节点,选中的时候他所关联的线都会加粗显示↓

打开网易新闻 查看精彩图片

03

节点颜色的深浅,是使用了pagerank算法计算的重要度,pagerank算法是谷歌搜索引擎的网页排序算法,可以用在有向图上,感兴趣的可以去搜索一下这个算法,颜色越深代表这个账户在该算法的下的重要性越高。

当然,这个重要性也只是用来作为参考

打开网易新闻 查看精彩图片

04

在没有选中任何节点时,线的粗细代表他的金额,金额最大的线就是最粗的,比如这条最粗的线,全场最高,500W↓

打开网易新闻 查看精彩图片

05

可以利用筛选的功能,将我们关注的账户筛出来,比如在03图中,我看到了A→F→D这条线,想看看他们的情况,我可以在网页上方的筛选器中依次选择node(节点)-id(名称),并多选账户名↓

打开网易新闻 查看精彩图片

或者我想看一下所有与E账户有关的路径,那么筛选edge(边)-from/to-E账户,筛选两次↓

打开网易新闻 查看精彩图片

06

最下面还有一些图的物理属性可以设置,比如线的长短,松紧度等等,可以自己玩玩↓

打开网易新闻 查看精彩图片

所以这个图看起来好像挺乱,但是配合这些要素,可以比较轻松地浏览一个大概出来,再配合筛选去详细看他们的交易应该能省掉一些翻流水的时间

下面是使用方法:

打开网易新闻 查看精彩图片

1.打开流水的填写模板,把信息填进去并保存

2.填完以后打开网页,选择刚保存好的模板,点击预览,即可在线看到解析结果,点击下载即可将文档(html网页文件)下载到本地

注意:

这个解析工具是部署在我自己的服务器上,因此数据是需要上传到我的服务器进行解析的,由于流水是非常敏感的数据,大家肯定在数据的安全性上有更多的考虑,毕竟如果我自己,当我得知什么账套啊,流水数据要上传到别人的服务器解析,那我是坚决不会用的

因此,我在流水的模板中又写了一个混淆的功能,点击混淆-字母,会将本方、对方账户名全部用A、B、C等字母替换,点击混淆-数字会自动生成6位随机数替换账户名,以此完成自动化脱敏,大家按自己心情选择一种混淆方式就行,然后把混淆字典存好备查

混淆后会生成字典↓

打开网易新闻 查看精彩图片

混淆的代码可以直接打开VBE编辑器(ALT+F11)查看,动没动手脚一看便知↓

打开网易新闻 查看精彩图片

如果你既不想混淆,又过于谨慎.. 又想用..

那么写好的代码直接给你,你用python自己跑一下也行

# -*- coding: utf-8 -*-import networkx as nximport pandas as pdimport matplotlib.pyplot as pltfrom pyvis.network import Network
def cmap_to_hex(cmap, value):rgba = cmap(value)r, g, b, a = int(rgba[0]*255), int(rgba[1]*255), int(rgba[2]*255), int(rgba[3]*255)return f"#{r:02x}{g:02x}{b:02x}"
def is_time_ordered(date_list):for k in range(0, len(date_list) - 1):if date_list[k] > date_list[k + 1]:return Falsereturn True
df = pd.read_excel('资金穿透模板.xlsm','数据源')G = nx.MultiDiGraph() # 创建多边有向图grouped = df.groupby(['本方账户', '对方账户', '日期'])['支出'].sum() # 汇总grouped2 = df.groupby(['本方账户', '对方账户', '日期'])['收入'].sum() # 汇总
max_value = grouped.max()min_value = grouped.min() # 获取最小值weight_factor = 3 / (max_value - min_value) # 假设边的最大宽度为 3
for index, value in grouped.items():source, target, dated = index # 解包得到分组的键date = dated.strftime('%Y-%m-%d')if value != 0:weight = int(value * weight_factor) # 根据金额计算边的粗细amount_str = "{:,.2f}".format(value) # 显示千分符G.add_edge(str(source), str(target), amount=amount_str,prev=str(source) + '→' + str(target) + ':' + str(amount_str) + ',时间:' + date,weight=weight, date=date)

for index, value in grouped2.items():target, source, dated = index # 解包得到分组的键date = dated.strftime('%Y-%m-%d')if value != 0:weight = int(value * weight_factor) # 根据金额计算边的粗细amount_str = "{:,.2f}".format(value) # 显示千分符G.add_edge(str(source), str(target), amount=amount_str,prev=str(source) + '→' + str(target) + ':' + str(amount_str) + ',时间:' + date,weight=weight, date=date)

nodesize = dict(G.out_degree) # 节点大小max_Ns = max(nodesize.values())node_sizes = {}node_size_scale = 800
edgecount = nx.pagerank(G, alpha=0.85) # 颜色深浅cmap = plt.get_cmap('YlOrRd')
net = Network(height='800px', width='100%', directed=True, notebook=True, filter_menu=True) # filter_menu=True notebook
# 拿到所有路径的列表path_list = []for node in G.nodes:try:paths = nx.shortest_path(G, source=node)path_list.extend(paths.values())except:pass
# 将包含该节点的穿透路径(考虑时间)添加title信息for node in G.nodes:path_node_list = [] # 包含该节点的所有路径for path in path_list:if node in path:path_node_list.append(path)
# 拿了该节点所有路径以后,判断时间,加载到titlenode_path_str = []for i in range(0, len(path_node_list) - 1):
if len(path_node_list[i]) > 1: # 如果子列表有2个元素,才开始遍历,判断时间date_list = []for j in range(0, len(path_node_list[i]) - 1):st = path_node_list[i][j]ed = path_node_list[i][j + 1]edge_data = G.get_edge_data(st, ed) # 拿到所有两个节点的路径
edge_minindex = min(edge_data, key=lambda x: edge_data[x]['date'])min_date = edge_data[edge_minindex]['date'] # 拿到所有路径中的最小日期date_list.append(min_date)
if len(date_list) == 1: # 如果时间列表只有一个,那么只有2个节点,不用判断时间对穿透的影响,直接添加node_path_str.append(path_node_list[i])else:if is_time_ordered(date_list): # 如果时间列表超过一个,那么从前向后判断时间是否小于后者,满足要求再添加node_path_str.append(path_node_list[i])
node_path_title = '\n'.join([' -> '.join(map(str, node_path)) for node_path in node_path_str])net.add_node(node, value=nodesize[node] / max_Ns * node_size_scale,color=cmap_to_hex(cmap, edgecount[node] / max(edgecount.values())),alpha=0.8, label=node, title=node_path_title)
for u, v, d in G.edges(data=True):# 添加边,并设置title属性net.add_edge(u, v, title=str(d['prev']), width=d['weight'], color='black',label=d['date'] + ":" + d['amount']) # ,label=d['date']
net.force_atlas_2based(spring_length=500, overlap=1, spring_strength=0.001)net.show_buttons(filter_=['physics']) # filter_=['physics']net.show('资金穿透.html')

温馨提示:加入审友交流群/转载/投稿请联系:审家小编 shenjizhijia1。