无论是投资股票还是投资债券,买任何东西都不能离开偏离价值这个词而讨论其他,首先要对公司的质地、估值做出估计。对公司的认识要从两个方面下手,其一,定性分析,主要分析难以量化的东西,比如公司的管理层是否诚实可靠,公司所在的行业是红海还是蓝海等等,我们结合穆迪的评分系统,针对公司定性分析设计了一套问卷,简称公司灵魂定性评分表(名字就是搞笑,但是很有用,后续文章单独详细介绍)。其二,定量分析,定量分析一旦展开指标体系存在无限扩张的风险,而一般人能同时评估的指标不会多于5个,所以人们难免会想:能不能给一个综合指标一眼就能看出哪些公司好?答案是可以,即所谓的综合排名。
综合排名就是通过某些算法将多个变量揉合为一个指数,这个指数大概能反映特定的期望。它本质是一个推荐算法,结果是否反映了用户的预期作为评价标准。综合排名最重要的是确定各个变量的权重,综合排名确定权重的方法有两种,第一种为专家法,就是由领域内的精英专家确定各个变量的权重,如层次分析等;另外一种是数据驱动的方法如降维技术、信息熵值等方法给各变量确定权重;很明显第一种方法更加主观。
但无论采取哪种方法,综合排名本质上是一种降维,维度降低难免造成信息的损失,所以每年大学排名都会被大家吐槽,这也是不能正确使用综合排名给大家带来的困扰。既然信息损失了,我们就不能追求绝对准确,而是追求大概准确,比如清华大学怎么排也不会低于全国前五,再比如如果一个系统选出的前100名公司都是很漂亮的公司,就是一个好系统,它能够快速地将3000多家公司缩小到一个100家以内的列表。
其实如果我们脑洞一下,任何通过多变量输入,单变量输出的模型都可以看作一种综合排名,比如破发概率排名,营收增速预测等等,都可以说是一种综合现状对未来某个预期的排名。
今天就用简单的均权算法结合一些硬性条件筛选,对上市公司进行综合排名。对公司进行排名时,首先要思考清楚我们要评价哪些方面,我们就从公司估值、股东集中度、盈利能力、成长性、偿债能力、股息等方面进行多变量排名。
我使用python进行建模和数据处理,我假设大家都是从头开始学习,我把每一句都交代清楚。另外至于python及其模块的安装,大家百度一下,目前使用的都是最新的anaconda。
######加载包
```{python}
import pandas as pd
import numpy as np
import tushare as ts
```
首先,引入三个常用的包,尤其是pandas和numpy,分别对应到R语言里的data.frame和array,是我们做数据处理最常用的包。这里重点介绍下tushare,tushare是比较成熟的量化包,并且拥有巨大的量化社区和测试平台,tushare非专业版对所有人开放。这些年我最大的感受就是专业的事要专业的人去做,比如数据不用tushare也能自己抓取获得,但可能花费大量的精力来维护,所以我们尽量选择已有的且比较稳定的数据源,即使花点会员费也可以接受,何况tushare是免费的。尽管出于种种原因,我们还是需要自己补充一些数据,但我会尽量将数据及时的分享出来。记得tushare数据调用时需要联网的哦。
我们将一个包import到内存并给他们起一个简单的别称,后面函数调用就可以在这个别称下调取包里的函数,例如pd.DataFrame()。
######设定通用参数
```{python}
timeToMarket = 20191201
financial_year = 2019
financial_quarter = 3
```
在写代码时,如果你有些参数是通用的且需要设定,最好在脚本开头就声明,这里使用timeToMarket设定评估2019年12月1号之前上市的公司,评估所用的财务数据为2019年3季度。
######读取上市公司基本信息
```{python}
stock_basics = ts.get_stock_basics()
stock_basics = stock_basics.loc[stock_basics.timeToMarket < timeToMarket, ]
stock_basics = stock_basics.loc[stock_basics.timeToMarket > 0, ]
```
tushare的`get_stock_basics`函数调取已经上市的或即将上市的公司基本信息,具体返回的字段请参考tushare的api说明,我这里就不多说了,2句和3句对上市时间进行筛选,pandas的数据框同时进行列和行的筛选时需要加一个`loc`函数,即通过索引的具体值进行筛选,2句筛出上市时间小于声明时间的公司,3句筛出已经上市交易的公司,没上市交易的timeToMarket字段为0.
######数据清洗
```{python}
stock_basics['code'] = stock_basics.index
stock_basics.loc[-stock_basics.code.str.contains('^6'), 'code'] = 'sz' + stock_basics.loc[-stock_basics.code.str.contains('^6'), ].code
stock_basics.loc[stock_basics.code.str.contains('^6'), 'code'] = 'sh' + stock_basics.loc[stock_basics.code.str.contains('^6'), ].code
stock_basics = stock_basics.sort_values(by = ['code'], ascending = True)
```
1句将pandas的行编号赋值给code,即股票的交易代码,但交易代码如果不加特殊字符,写出csv后会变成数字,很多以0开头的深市公司的交易代码就不完整了。因此我们需要给交易代码添加一个字符编号,2句查找非6开头的公司,在code前面添加‘sz’深圳缩写,这里首先调用`str`函数将code转化为字符,然后调用`contains`查找非以6开头的code,添加完成后重新赋值给code;3句同上,查找以6开头的code,给它们的code前面添加‘sh’上海缩写;4句调用`sort_values`函数按code进行升序排列。
######硬性筛选条件
```{python}
stock_basics = stock_basics.loc[~stock_basics.name.str.contains('ST')]
stock_basics = stock_basics[stock_basics.esp >= 0]
```
在做排名之前我们可能设定一些硬性入组条件,比如需要排除一些退市风险警示的公司(以ST开头);2句筛除一些每股收益小于0的公司,即亏损公司,也可以继续设定其他硬性指标。
######估值及盈利情况
```{python}
ep_holder_growth = stock_basics[['code', 'name', 'industry', 'area', 'pe', 'holders', 'rev']]
ep_holder_growth = ep_holder_growth.reset_index(drop = True)
ep_holder_growth.loc[ep_holder_growth.pe == 0, 'pe'] = 0.000001
ep_holder_growth['ep'] = 1/ep_holder_growth.pe
ep_holder_growth.loc[pd.isnull(ep_holder_growth.rev), 'rev'] = min(ep_holder_growth.rev)
```
我们一般希望买入便宜的公司,pe值能够大概反映公司是否便宜,pe越高相当于卖方要价越高,当然别人卖的也许是货真价实,就是值得拥有的高pe公司,比如恒瑞医药一直都有很高的pe;营收的增长提前于利润的增长,利润来源于营收,营收同比增长越快越好。这里暂时不去纠结单个变量的优缺点。
1句提取一些对后续比较重要的列,比如市盈率(pe)、股东人数(holders),收入同比(rev),这里选择收入同比而不是利润同比,主要原因是收入是利润之母,而且利润作为下级指标波动性更大;2句重新设定了行编号,这里`reset_index`函数设置了drop参数为真,如果不设定它就将编号作为新的一列加入到数据框;3句将pe为0的pe替换为一个极小的数值,因为4句计算pe的倒数,为0会报错。
均权法也有加法均权和乘法均权,这里使用加法均权,所以要保证各个变量的从小到大与最终得分同向,比如我们希望综合得分越高的企业越好,那高pe的公司普遍高估(并非所有),与目标相反,需要计算pe的倒数ep来参与排名。5句调用`isnull`函数筛出rev缺失的公司赋值为最小的rev。
由股东人数和流通市值可以计算出股东平均持有流通市值,这个指标可以衡量股权集中度,在买东西时我们希望具有稀缺性,不希望每个人手里都有,另外股权越集中也越容易形成合力,所以股权集中度是一个很重要的考虑方面。
######股东平均持有流通市值
```{python}
nmc = ts.get_today_all()
nmc.loc[-nmc.code.str.contains('^6'), 'code'] = 'sz' + nmc.loc[-nmc.code.str.contains('^6'), ].code
nmc.loc[nmc.code.str.contains('^6'), 'code'] = 'sh' + nmc.loc[nmc.code.str.contains('^6'), ].code
nmc = nmc[['code', 'nmc']]
ep_holder_growth = pd.merge(ep_holder_growth, nmc, how = 'left', on = 'code')
```
tushare里的`get_today_all`函数可以调取当日的交易数据,具体字段参考tushare说明,2、3句处理code;4句提取流通市值和代码;5句与之前的数据框左关联在一起,调用的是pandas里的`merge`函数,使用on参数设置关联的id。
######计算股东平均持有流通市值
```{python}
ep_holder_growth = ep_holder_growth[ep_holder_growth.holders != 0]
ep_holder_growth['nmc_per_holder'] = ep_holder_growth.nmc/ep_holder_growth.holders
```
1句剔除股东人数等于0的初上市公司;2句nmc除以股东人数计算平均每股东持有流通市值。
入股公司当然希望公司的盈利能力超强,roe即净资产收益率,比如公司拿股东1块钱,一年赚1块,这盈利能力就超强,而一年只赚1分钱,就不如银行存款。
######盈利能力
```{python}
roe = ts.get_profit_data(financial_year,financial_quarter)
roe = roe[['code', 'roe']]
roe.loc[-roe.code.str.contains('^6'), 'code'] = 'sz' + roe.loc[-roe.code.str.contains('^6'), ].code
roe.loc[roe.code.str.contains('^6'), 'code'] = 'sh' + roe.loc[roe.code.str.contains('^6'), ].code
ep_holder_growth = pd.merge(ep_holder_growth, roe, how = 'left', on = 'code')
ep_holder_growth.loc[pd.isnull(ep_holder_growth.roe), 'roe'] = min(ep_holder_growth.roe)
```
`get_profit_data`函数读取个股盈利数据,该函数需要设定年度和季度;2句提取code和roe;3、4句处理code字段;5句将roe关联到ep_holder_growth数据框;6句对roe缺失的使用其他公司最小的roe替换。
公司的偿债能力我们做风险考量的重点,毕竟不希望公司因为违约而带来退市等麻烦。我们首先希望公司的现金等价物基本上就能覆盖流动负债,即现金比率=(货币资金+有价证券)÷流动负债。
######偿债能力
```{python}
cashratio = ts.get_debtpaying_data(financial_year, financial_quarter)
cashratio = cashratio[['code', 'cashratio']]
cashratio.loc[-cashratio.code.str.contains('^6'), 'code'] = 'sz' + cashratio.loc[-cashratio.code.str.contains('^6'), ].code
cashratio.loc[cashratio.code.str.contains('^6'), 'code'] = 'sh' + cashratio.loc[cashratio.code.str.contains('^6'), ].code
```
`get_debtpaying_data`函数读取偿债能力的数据,需要设定年度和季度;2句提取code和现金比率字段;3、4句处理code字段。由于现金比率字段存在一些异常数据我们需要对它进行处理。
######偿债能力数据清洗
```{python}
cashratio.cashratio = cashratio.cashratio.astype(str)
cashratio.loc[cashratio.cashratio.str.contains('--'), 'cashratio'] = "0"
cashratio.cashratio = cashratio.cashratio.astype(float)
ep_holder_growth = pd.merge(ep_holder_growth, cashratio, how = 'left', on = 'code')
ep_holder_growth.loc[pd.isnull(ep_holder_growth.cashratio), 'cashratio'] = min(ep_holder_growth.cashratio)
```
1句使用`astype`函数将现金比率字段转化为str字符类型;2句将现金比率为‘--’的替换为0;3句将现金比率的数据类型转换为浮点小数;4句关联到ep_holder_growth数据框;5句用现金比率的最小值替换那些现金比率的缺失值。
在量纲不同的变量之间计算加权综合得分,需要将量纲统一,比如收入和身高如果不统一量纲,可能基于他们计算出来的综合得分,体现的全是收入能力。因此,我们使用归一化函数将各变量统一在一个范围内。
######计算综合得分
```{python}
res = ep_holder_growth.drop(['holders', 'nmc'], axis = 1)
temp = res[['rev', 'ep', 'nmc_per_holder', 'roe', 'cashratio']]
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
x_scaled = scaler.fit_transform(temp)
temp = pd.DataFrame(x_scaled)
temp.loc[:,'rank_score'] = temp.sum(axis=1)
temp.loc[:,'rank_score'] = temp.rank_score/(len(temp.columns)-1)
```
1句使用`drop`函数剔除不再需要的变量;2句提取将要进入综合分计算的变量;3句载入sklearn包里的`MinMaxScaler`函数,该函数完成各变量的归一化;4句创建归一化实例;5句使用归一化实例将temp归一化;归一化实例会记得这个数据集中各列的最大值最小值,保存下来下次可以继续调用;6句将标准化的数据转化为数据框;7句使用sum函数将每一行的值等权相加得出总得分,axis参数表示对行求和;7句总得分除以变量数,将其控制在与变量量纲相同的范围。
######按列捆绑
```{python}
temp.columns = ['rev_scale', 'ep_scale', 'nmc_per_holder_scale', 'roe_scale', 'cashratio_scale', 'rank_score']
res = pd.concat([res.reset_index(drop=True), temp], axis=1)
```
1句给temp所有列重新命名;2句使用`concat`函数将temp与res按列捆绑在一起,axis = 1表示按列捆绑,记得捆绑之前要使用`reset_index`重设行索引。pandas里的行索引往往会给数据框的合并、关联操作带来不必要的麻烦,如不是必要,记得重设。
######综合得分百分化
```{python}
res['rank_score'] = (res.rank_score - min(res.rank_score))/(max(res.rank_score) - min(res.rank_score))*100
res = res.rename(columns = {'rev':'收入同比',
'ep':'pe倒数',
'nmc_per_holder':'平均股东持有流通市值',
'cashratio':'现金比率'})
```
1句使用归一化将综合得分限定在0-1的范围,然后乘以100即完成百分制化;2句将一些变量的英文缩写改成中文名。
######结果输出
```{python}
from datetime import datetime
outfile = 'D:/dsod/bond/result/stock_rank'+datetime.now().strftime('%Y%m%d_%H%M%S')+'.csv'
res.to_csv(outfile, index =None)
```
1句载入datetime包;2句用`datetime.now`函数获取日期时间,并用`strftime`提取合适的格式,然后将路径+时间+文件格式粘贴为一个完整的结果输出路径;python里很多操作具有管道性质,即一个函数的结果可以继续提交给后面紧跟的函数。3句写出csv到指定路径。
通过以上工作我们基本上完成了一个比较简单的排名,这个排名比较全面的考验了一家公司的估值、股权集中度、成长性、盈利能力、偿债能力等,由于我的数据还没到位,暂时没有拿到股息率数据,后面我会将股息率添加进来。从排名的结果上看还是不错的,很多著名公司都可以进入前200名。最靠前的公司往往具有某方面的极端表现,比如茅台,因为其股价高,所以股东平均持有流通市值傲视全A股。
尽管我们获得了一个比较不错的股池,但我们在做个股投资时还是需要更加详细的研究,毕竟数据是结果,而不是原因。但如果我们筛选前200名按比例买入,就不会有深研个股的困扰,这也是我崇尚指数投资的原因。每一个策略都需要回测,我们才刚起步,后面逐渐会选取策略进行回测。
加微信“大音如霜”一起交流数据


雷达卡




京公网安备 11010802022788号







