以前发过一个流水穿透闭环工具,传送门:银行流水闭环核查工具
那个是我用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 nx
import pandas as pd
import matplotlib.pyplot as plt
from 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 False
return 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)
# 拿了该节点所有路径以后,判断时间,加载到title
node_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。
热门跟贴