在本系列的前文[1,2]中,我们介绍了如何使用Python语言图分析库NetworkX[3]+NebulaGraph[4]来进行权力的游戏中人物关系图谱分析。
在本文中我们将介绍如何使用Java语言的图分析库JGraphT[5]并借助绘图库mxgraph[6],可视化探索A股的行业个股的相关性随时间的变化情况。

本文主要分析方法参考了[7,8],有两种数据集:
股票数据(点集)从A股中按股票代码顺序选取了160只股票(排除摘牌或者ST的)。每一支股票都被建模成一个点,每个点的属性有股票代码,股票名称,以及证监会对该股票对应上市公司所属板块分类等三种属性;
表1:点集示例

边只有一个属性,即权重。边的权重代表边的源点和目标点所代表的两支股票所属上市公司业务上的的相似度——相似度的具体计算方法参考[7,8]:取一段时间(2014年1月1日-2020年1月1日)内,个股的日收益率的时间序列相关性

再定义个股之间的距离为(也即两点之间的边权重):

通过这样的处理,距离取值范围为[0,2]。这意味着距离越远的个股,两个之间的收益率相关性越低。
表2:边集示例

这样的点集和边集构成一个图网络,可以将这个网络存储在图数据库NebulaGraph中。
JGraphTJGraphT是一个开放源代码的Java类库,它不仅为我们提供了各种高效且通用的图数据结构,还为解决最常见的图问题提供了许多有用的算法:
支持有向边、无向边、权重边、非权重边等;
支持简单图、多重图、伪图;
提供了用于图遍历的专用迭代器(DFS,BFS)等;
提供了大量常用的的图算法,如路径查找、同构检测、着色、公共祖先、游走、连通性、匹配、循环检测、分区、切割、流、中心性等算法;
可以方便地导入/导出GraphViz[9]。导出的GraphViz可被导入可视化工具Gephi[10]进行分析与展示;
可以方便地使用其他绘图组件,如:JGraphX,mxGraph,GuavaGraphsGenerators等工具绘制出图网络。
下面,我们来实践一把,先在JGraphT中创建一个有向图:
*;*;*;*;*;*;*;*;GraphURI,DefaultEdgeg=newDefaultDirectedGraph();
添加顶点:
URIgoogle=newURI("");URIwikipedia=newURI("");URIjgrapht=newURI("");//(google);(wikipedia);(jgrapht);添加边:
//(jgrapht,wikipedia);(google,jgrapht);(google,wikipedia);(wikipedia,google);图数据库NebulaGraphDatabase
JGraphT通常使用本地文件作为数据源,这在静态网络研究的时候没什么问题,但如果图网络经常会发生变化——例如,股票数据每日都在变化——每次生成全新的静态文件再加载分析就有些麻烦,最好整个变化过程可以持久化地写入一个数据库中,并且可以实时地直接从数据库中加载子图或者全图做分析。本文选用NebulaGraph作为存储图数据的图数据库。
NebulaGraph的Java客户端Nebula-Java[11]提供了两种访问NebulaGraph方式:一种是通过图查询语言nGQL[12]与查询引擎层[13]交互,这通常适用于有复杂语义的子图访问类型;另一种是通过API与底层的存储层(storaged)[14]直接交互,用于获取全量的点和边。除了可以访问NebulaGraph本身外,Nebula-Java还提供了与Neo4j[15]、JanusGraph[16]、Spark[17]等交互的示例。
在本文中,我们选择直接访问存储层(storaged)来获取全部的点和边。下面两个接口可以用来读取所有的点、边数据:
//space为待扫描的图空间名称,returnCols为需要读取的点/边及其属性列,//returnCols参数格式:{tag1Name:prop1,prop2,tag2Name:prop3,prop4,prop5}IteratorScanVertexResponsescanVertex(Stringspace,MapString,ListStringreturnCols);IteratorScanEdgeResponsescanEdge(Stringspace,MapString,ListStringreturnCols);第一步:初始化一个客户端,和一个ScanVertexProcessor。ScanVertexProcessor用来对读出来的顶点数据进行解码:
MetaClientImplmetaClientImpl=newMetaClientImpl(metaHost,metaPort);();StorageClientstorageClient=newStorageClientImpl(metaClientImpl);Processorprocessor=newScanVertexProcessor(metaClientImpl);
第二步:调用scanVertex接口,该接口会返回一个scanVertexResponse对象的迭代器:
IteratorScanVertexResponseiterator=(spaceName,returnCols);
第三步:不断读取该迭代器所指向的scanVertexResponse对象中的数据,直到读取完所有数据。读取出来的顶点数据先保存起来,后面会将其添加到到JGraphT的图结构中:
while(()){ScanVertexResponseresponse=();if(response==null){("Erroroccurswhilescanvertex");break;}Resultresult=(spaceName,response);((TAGNAME));}读取边数据的方法和上面的流程类似。
在JGraphT中进行图分析第一步:在JGraphT中创建一个无向加权图graph:
GraphString,MyEdgegraph=().weighted(true).allowingMultipleEdges(true).allowingSelfLoops(false).vertexSupplier(()).edgeSupplier(()).buildGraph();
第二步:将上一步从NebulaGraph图空间中读出来的点、边数据添加到graph中:
for(VertexDomainvertex:vertexDomainList){(().toString());(().toString(),vertex);}for(EdgeDomainedgeDomain:edgeDomainList){(().toString(),().toString());MyEdgenewEdge=(().toString(),().toString());(newEdge,());}第三步:参考[7,8]中的分析法,对刚才的图graph使用Prim最小生成树算法(minimun-spanning-tree),并调用封装好的drawGraph接口画图:
=newPrimMinimumSpanningTree(graph).getSpanningTree();((),filename,stockIdToName);
第四步:drawGraph方法封装了画图的布局等各项参数设置。这个方法将同一板块的股票渲染为同一颜色,将距离接近的股票排列聚集在一起。
publicclassLeg{publicstaticvoiddrawGraph(SetMyEdgeedges,Stringfilename,MapString,VertexDomainidVertexMap)throwsIOException{//CreatesgraphwithmodelmxGraphgraph=newmxGraph();Objectparent=();//().beginUpdate();mxStylesheetmyStylesheet=();(setMsStylesheet(myStylesheet));MapString,ObjectidMap=newHashMap();MapString,StringindustryColor=newHashMap();intcolorIndex=0;for(MyEdgeedge:edges){Objectsrc,dst;if(!(())){VertexDomainsrcNode=(());StringnodeColor;if((())){nodeColor=(());}else{nodeColor=COLOR_LIST[colorIndex++];((),nodeColor);}src=(parent,null,(),0,0,105,50,"fillColor="+nodeColor);((),src);}else{src=(());}if(!(())){VertexDomaindstNode=(());StringnodeColor;if((())){nodeColor=(());}else{nodeColor=COLOR_LIST[colorIndex++];((),nodeColor);}dst=(parent,null,(),0,0,105,50,"fillColor="+nodeColor);((),dst);}else{dst=(());}(parent,null,"",src,dst);}("vertice"+());("colorsize"+());mxFastOrganicLayoutlayout=newmxFastOrganicLayout(graph);(2000);//(10D);(parent);().Update();//CreatesanimagethancanbesavedusingImageIOBufferedImageimage=createBufferedImage(graph,null,1,,true,null);//Forthesakeofthisexamplewedisplaytheimageinawindow//SaveasJPEGFilefile=newFile(filename);(image,"JPEG",file);}}第五步:生成可视化:
图1中每个顶点的颜色代表证监会对该股票所属上市公司归类的板块。
可以看到,实际业务近似度较高的股票已经聚拢成簇状(例如:高速板块、银行版本、机场航空板块),但也会有部分关联性不明显的个股被聚类在一起,具体原因需要单独进行个股研究。

图1:基于2015-01-01至2020-01-01的股票数据计算出的聚集性
第六步:基于不同时间窗口的一些其他动态探索
上节中,结论主要基于2015-01-01到2020-01-01的个股聚集性。这一节我们还做了一些其他的尝试:以2年为一个时间滑动窗口,分析方法不变,定性探索聚集群是否随着时间变化会发生改变。

图2:基于2014-01-01至2016-01-01的股票数据计算出的聚集性

图3:基于2015-01-01至2017-01-01的股票数据计算出的聚集性

图4:基于2016-01-01至2018-01-01的股票数据计算出的聚集性

图5:基于2017-01-01至2019-01-01的股票数据计算出的聚集性

图6:基于2018-01-01至2020-01-01的股票数据计算出的聚集性
粗略分析看,随着时间窗口变化,有些板块(高速、银行、机场航空、房产、能源)的板块内部个股聚集性一直保持比较好——这意味着随着时间变化,这个版块内各种一直保持比较高的相关性;但有些板块(制造)的聚集性会持续变化——意味着相关性一直在发生变化。
Disclaim
本文不构成任何投资建议,且作者不持有本文中任一股票。
受限于停牌、熔断、涨跌停、送转、并购、主营业务变更等情况,数据处理可能有错误,未做一一检查。
受时间所限,本文只选用了160个个股样本过去6年的数据,只采用了最小扩张树一种办法来做聚类分类。未来可以使用更大的数据集(例如美股、衍生品、数字货币),尝试更多种图机器学习的办法。
本文代码可见[18]
Reference[1]用NetworkX+Gephi+NebulaGraph分析权力的游戏人物关系(上篇)
[2]用NetworkX+Gephi+NebulaGraph分析权力的游戏人物关系(下篇)
[3]NetworkX:aPythonpackageforthecreation,manipulation,andstudyofthestructure,dynamics,andfunctionsofcomplexnetworks.
[4]NebulaGraph:Apowerfullydistributed,scalable,lightning-fastgraphdatabasewritteninC++.
[5]JGraphT:aJavalibraryofgraphtheorydatastructuresandalgorithms.
[6]mxGraph:JavaScriptdiagramminglibrarythatenablesinteractivegraphandchartingapplications.
[7]Bonanno,GiovanniLillo,FabrizioMantegna,Rosario.(2000).,/713665554.
[8]Mantegna,,193–197(1999).
[9]
[10]
[11]
[12]NebulaGraphQueryLanguage(nGQL).
[13]NebulaGraphQueryEngine.
[14]Nebula-storage:Adistributedconsistentgraphstorage.
[15]
[16]
[17]
[18]