点击上方“编程派”,选择设为“设为星标”
优质文章,第一时间送达!
作者:BuyiXiao
出处:月小水长公众号
在一篇论文中,最吸引审稿人目光的莫过于枯燥的文字间精美的图表。在一份项目路演 ppt 中,酷炫的财务报表往往是打动投资人的最后一剂强心剂
作为数据分析最后也是最直接的一环,数据可视化的重要性不言而喻
数据可视化大致可分为两类,一类是 excel、powerBI 这类不需要写代码的,另一类是需要写代码的;而对于 Python 来说,数据可视化框架,我个人觉得大致可以分为以下两类(推荐程度从高到底)
1、如果对于数据交互性没有特殊要求的话,首推matplotlib+seaborn,其中 matplotlib 中成熟而强大的绘图 api 应有尽有,seaborn 相当于调色笔,修改 matplotlib 本身的主题、配色风格等;matplotlib 的另一大优势是可以结合 pandas 快速喂入数据。
2、如果对数据交互性要求高,需要点击图表查看数据,首推pyecharts;如果还喜欢二次元可爱风的话,可以用cutecharts, cutecharts 和 pycharts 均基于百度主导的 JavaScript 可视化框架 Echarts。
可以看到,cutecharts 绘制的图表比较 Q(显然不能做正式数据报表和论文图表),当鼠标悬停到天线原理这一列时,会显示出学生 A 和 B 的成绩,但是 cutecharts 生成的是一个 HTML 文件,需要用浏览器打开才能显示图表,而 Python 第三方 GUI 库PyQt5实现了浏览器组件QtWebEngineWidgets,结合 Echarts 的 JavaScript API 就能不打开浏览器实现酷炫的数据交互效果。
实现效果
今天要讲的主题就是使用 PyQt5+Echarts 实现股票数据看板,股票数据采集自网上公开接口,考虑到网易财经历史数据全但有延时,Tushare 数据更新快颗粒度高但调用次数有限制,融合使用网易财经和 Tushare,爬虫这部分代码不是今天的主题,可以跳过,且所有代码均已上传,关注本公众号并在后台回复股票即可获得所有代码(包括爬虫+可视化)的下载链接。
界面布局
如上图所示,界面可细分为三大块,左上角的昨日股票涨跌行情饼状图,右上角的展示股票排行榜的 QTabWidget 表格,以及下方的某只股票Open-Close-High-Low折线图。上图中,考虑到计算量的问题,饼状图和表格的数据都是直接伪造的,只有股票的折线图数据是真实的。
整个界面继承自 QMainWindow,最外层的布局是竖直布局 QVBoxLayout,它包含界面上部分的 QHBoxLayout 和下方的 QHBoxLayout,并同时设置这两个 QHBoxLayout 的 拉伸因子为 1,这样就能够实现上下部分等分整个界面并大小随界面自适应改变,其语法格式是
vbox=QVBoxLayout()vbox.addLayout(QHBoxLayout())vbox.addLayout(QHBoxLayout())#第一个参数表示vbox中组件的序号,也就是添加顺序#第二个参数表示组件在vbox中的权重vbox.setStretch(0,1)vbox.setStretch(1,1)
从小的方面来说,左上角和下部分的布局都是 PyQt5 中的 QtWebEngineWidgets 组件,它就像一个浏览器,通过 QtWebEngineWidgets 调用 Echarts 中的 API,就能在 PyQt5 的界面中显示 Echarts 各种各样的的图表。而右上角是一个 QTabWidget 组件,为了减少代码之间的耦合,我单独把它写成一个 RightTableView 类,
#-*-coding:utf-8-*-#author:inspurer(月小水长)#pc_typelenovo#create_time:/12/1821:54#file_name:rightview.py#github/inspurer#qq邮箱2391527690@#微信公众号月小水长(ID:inspurer)importsysfromPyQt5.QtWidgetsimportQApplication,QWidget,QVBoxLayout,QTabWidget,QLabel,QTableWidget,QAbstractItemView,QTableWidgetItemfromPyQt5.QtCoreimportQtclassRightTableView(QWidget):def__init__(self):super().__init__()self.mainLayout=QVBoxLayout()tabWidgets=QTabWidget()label=QLabel("前一日涨幅排名前十的股票详细信息")tabWidgets.addTab(label,"涨幅排名")label=QLabel("前一日成交量排名前十的股票详细信息")tabWidgets.addTab(label,"成交量排名")tabWidgets.currentChanged['int'].connect(self.tabClicked)#绑定标签点击时的信号与槽函数self.mainLayout.addWidget(tabWidgets)self.tableView=QTableWidget()self.table=QTableWidget(self)self.table.setColumnCount(6)self.table.setSelectionBehavior(QAbstractItemView.SelectRows)#设置表格的选取方式是行选取self.table.setSelectionMode(QAbstractItemView.SingleSelection)#设置选取方式为单个选取self.table.setHorizontalHeaderLabels(["股票代码","开盘","收盘",'最高','最低','成交量'])#设置行表头self.mainLayout.addWidget(self.table)self.mainLayout.setStretch(0,1)self.mainLayout.setStretch(1,12)self.setLayout(self.mainLayout)self.updateView()defupdateView(self):self.table.insertRow(0)stock_code=QTableWidgetItem("1001")stock_code.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)stock_code.setTextAlignment(Qt.AlignCenter)stock_open=QTableWidgetItem("10.20")stock_open.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)stock_open.setTextAlignment(Qt.AlignCenter)stock_close=QTableWidgetItem("10.20")stock_close.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)stock_close.setTextAlignment(Qt.AlignCenter)stock_high=QTableWidgetItem("10.20")stock_high.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)stock_high.setTextAlignment(Qt.AlignCenter)stock_low=QTableWidgetItem("10.20")stock_low.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)stock_dealNum.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)stock_dealNum.setTextAlignment(Qt.AlignCenter)self.table.setItem(0,0,stock_code)self.table.setItem(0,1,stock_open)self.table.setItem(0,2,stock_close)self.table.setItem(0,3,stock_high)self.table.setItem(0,4,stock_low)self.table.setItem(0,5,stock_dealNum)deftabClicked(self,index):'''tab监听事件,在此添加业务逻辑'''print(index)
RightTableView 实现了 tab 的监听,切换不同的 tab 可根据 index 展示不同的数据,因此,在主模块中初始化 RightTableView 类的时候,应当给定 RightTableView 可能用到的所有数据,这样可以避免使用 Signal 信号来进行主模块和 RightTableView 模块的通信。
在主模块中,通过 from rightview import RightTableView
即可引入 RightTableView 布局,其中 rightview 是文件名,RightTableView 是类名,如改行代码爆红(实际上不影响运行),可在项目上右键 Mark Dircectory as -> Sources Root 解决之。
数据驱动
实际上,在三个小布局中,界面上部的两个布局的数据均是伪造的,因为这个数据的采集及计算太过耗时
在 basic 表中,我记录了 5000 支股票的基础信息:股票交易所、股票发行公司、股票代码,上部的两个布局需要这 5000 支股票的整体数据,即 5000 支股票昨日相较于前日的跌涨幅,5000 支股票的跌涨幅度的排名,作为一个客户端软件,我觉得一个操作所能忍受的时延是 3 s 以内,优于接口还有速率限制,粗略计算了一下,这个过程远远超过了 30 s,所以我觉得可行的办法是将这种采集和计算过程部署到服务器,通过设置定时任务执行,客户端每次打开只需要一个简单的 Get 请求即可立即渲染数据。
而下方的股票 Open-Close-High-Low 折线图所需数据的计算量比较小,可直接完成,用户输入股票发行公司,即可返回该公司发行股票的代码,(因为我们一般记住的是股票发行公司而不是股票代码,就行我们往往记住网站的域名而不是 ip 地址),如果数据库中不存在代码该股票的表(表名=发行公司_股票代码),就新建,并抓取指定日期的数据存入该表;如果表存在但是缺少用户想要的数据,则更新数据即可;这样设计的好处是尽可能减少平均操作时延。
再说这个用户输入股票发行公司,即可返回该公司发行股票的代码
,乍一看就是一个 key-value 字典,为了减少数据库的操作,在程序初始化过程中,我们需要把 basic 表中的股票数据全部加载进内存,也就是放进字典里,但是由于一个公司可能发现很多股票,但是 Python 内置的字典 一个 key 只能对应一个 value ,我们很容易想到把 value 设计成一个列表,但是这样破环了字典的原子性,假如后面我们新加了一个需要,根据 value 反查 key,也就是说根据股票代码反查股票发行公司,如果设计成列表,这个反查耗时将是巨大的,考虑再三,我在不破坏字典 item 的原子性的前提下,实现了 value 可重复 dict,其本质是一个列表,列表元素为字典,核心思想是把键重复的item分散到不同字典,不过经过封装,对外操作和字典一样,下面是该可重复字典的实现
#-*-coding:utf-8-*-#author:inspurer(月小水长)#pc_typelenovo#create_time:/12/212:25#file_name:myDict.py#github/inspurer#qq邮箱2391527690@#微信公众号月小水长(ID:inspurer)classAllowKeyRepeatDict():'''自定义允许键重复的字典其本质是一个列表,列表元素为字典,核心思想是把键重复的item分散到不同字典封装后列表对外操作像字典'''def__init__(self):self.dictList=[]defadd(self,key,value):length=len(self.dictList)i=0whilei<length:ifnotself.dictList[i].get(key,None):self.dictList[i][key]=valuereturnii+=1newDict={}newDict[key]=valueself.dictList.append(newDict)returnidefdelete(self,key):''':paramkey:根据key删除所有item'''length=len(self.dictList)foriinrange(length):response=self.dictList[i].pop(key,None)ifnotresponse:break#清除哪些空容器,注意从后往前删,否则会出现下标越界whilelength>0:ifself.dictList[length-1]=={}:delself.dictList[length-1]length-=1defquery(self,key):''':paramkey:查询的健:return:由于允许键重复,返回形式是一个列表'''result=[]length=len(self.dictList)foriinrange(length):response=self.dictList[i].get(key,None)ifnotresponse:returnresultresult.append(response)returnresultdef__str__(self):''':return:打印整个字典'''resStr=''length=len(self.dictList)iflength==0:return'该字典为空'foriinrange(length):fork,vinself.dictList[i].items():aItem='key:{:<8}value:{:<8}\n'.format(k,v)resStr+=aItemreturnresStrif__name__=='__main__':app=QApplication(sys.argv)mainWin=RightTableView()mainWin.show()sys.exit(app.exec_())
设计模式
当 QtWebEngineWidgets 需要新建一个图表获取句柄时,它希望屏蔽掉新建的具体细节,我们可以设计一个函数对应一种图表来实现这个功能,但是
我们又不想每次新建图表时去找对应的函数,这个时候可以再设计一个代理函数,告诉这个代理函数我们需要怎样的图表即可获取相应图表的句柄。
#代理函数defgetOptions(self,type):iftype==Noneortype=='K':returnself.createKlines()eliftype=='Pie':returnself.create_pie(v=[3000,600,5000])#K图表工具函数defcreateKlines(self):overlap=Overlap()forquoteinself.quote_data:line=Line(quote['title'])print(quote)line.add('open',quote['date'],quote['open'],is_smooth=True)line.add('close',quote['date'],quote['close'],is_smooth=True)line.add('high',quote['date'],quote['high'],is_smooth=True)line.add('low',quote['date'],quote['low'],is_smooth=True)overlap.add(line)snippet=TRANSLATOR.translate(overlap.options)options=snippet.as_snippet()returnoptions#饼图工具函数defcreate_pie(self,v):pie=Pie()pie.add("昨日行情",['涨','平','跌'],v,is_label_show=True)snippet=TRANSLATOR.translate(pie.options)options=snippet.as_snippet()returnoptions
以上就是本次话题的所有内容,代码开源,关注本公众号并在后台回复股票即可获得所有代码。
回复下方「关键词」,获取优质资源
回复关键词「pybook03」,立即获取主页君与小伙伴一起翻译的《Think Python 2e》电子版
回复关键词「入门资料」,立即获取主页君整理的10 本 Python 入门书的电子版
回复关键词「m」,立即获取Python精选优质文章合集
回复关键词「book 数字」,将数字替换成 0 及以上数字,有惊喜好礼哦~
题图:pexels,CC0 授权。
好文章,我在看❤️
如果觉得《使用 PyQt5 和 Echarts 打造股票数据看板》对你有帮助,请点赞、收藏,并留下你的观点哦!