利用python爬虫技术实现基于本福特定律的上市公司报表真假检验方案
一、本福特定律
做假账是很多公司,尤其是上市公司的主要工作,为了对付假账,人们想了很多办法。其中一个就是利用奔福德定律来检查各种数据是否有造假。
本福特定律是说一堆从实际生活得出的数据中,以1为首位数字的数的出现机率约为总数的三成,接近期望值1/9的3倍,越大的数,以它为首几位的数出现的机率就越低。本福特定律说明在b进位制中,以数n起头的数出现的机率为logb(n 1) − logb(n) 。值得一提的是,本福特定律满足尺度不不变性,即如果我们换一套单位制,本福特定律仍然成立。本福特定律不但适用于个位数字,连多位的数也可用。在十进制首位数字的出现机率(%,小数点后一个位):1打头概率是30.1%,2是17.6%,3是12.5%,4是9.7%,5是7.9%,6是6.7%,7是5.8%,8是5.1%,9是4.6%。
1935年,美国的一位叫做本福特的物理学家在图书馆翻阅对数表时发现,对数表的头几页比后面的页更脏一些,这说明头几页在平时被更多的人翻阅。本福特再进一步研究后发现,只要数据的样本足够多,数据中以1为开头的数字出现的频率并不是1/9,而是30.1%。而以2为首的数字出现的频率是17.6%,往后出现频率依次减少,9的出现频率最低,只有4.6%。
本福特开始对其它数字进行调查,发现各种完全不相同的数据,比如人口、物理和化学常数、棒球统计表以及斐波纳契数列数字中,均有这个定律的身影。就是只要是由度量单位制获得的数据都符合这一定律,它的适用范围异常的广泛,几乎所有日常生活中没有人为规则的统计数据都满足这个定律。另一方面,任意获得的和受限数据通常都不符合本福特定律。比如,彩票数字、电话号码、汽油价格、日期和一组人的体重或者身高数据是比较随意的,或者是任意指定的,并不是由度量单位制获得的。
1961年,一位美国科学家提出,本福特定律其实是数字累加造成的现象,即使没有单位的数字。比如,假设股票市场上的指数一开始是1000点,并以每年10%的程度上升,那么要用7年多时间,这个指数才能从1000点上升到2000点的水平;而由2000点上升到3000点只需要4年多时间;但是,如果要让指数从10000点上升到20000点,还需要等7年多的时间。因此我们看到,以1为开头的指数数据比以其他数字打头的指数数据要高很多。
2001年,美国最大的能源交易商安然公司宣布破产,当时传出了该公司高层管理人员涉嫌做假账的传闻。事后人们发现,安然公司在2001年到2002年所公布的每股盈利数字就不符合本福特定律,这证明了安然的高层领导确实改动过这些数据。
二、实现思路
如果不用计算机实现基于本福特定律的上市公司报表真假检验,就只能是下载每家上市公司从上市到现在的每年财务报表(3张表,资产负债表,利润表,现金流量表),然后手工记录每个数字的首数字是多少,然后统计每家公司的财务报表的首数字的分布概率。沪深3000多家公司,平均十年的财务报表,将近3万个报表,统计量太大,必须用计算机语言来实现自动取数和统计计算。大致思路如下:
(1)获取沪深两市所有的股票编号和名称。
(2)基于股票编号,到网站(我选择网易股票)上抓取每年的财务数据,并统计每家公司的财务报表首数字的分布概率。第二步是关键,可以细化为:
2.1 统计资产负债表的首数字分布情况
2.1.1 按照股票编号到网站上抓取各年的资产负债表数据
2.1.2 处理每个单元格的数据,如果是数字,绝对值大于1,取首位,绝对值小于1,乘以1亿后,取首位。
2.1.3 按照首位数字的情况,将对应的统计变量加一。然后循环到2.1.2直到结束。
2.2 统计利润表的首数字分布情况
2.3 统计现金流量表的首数字分布情况
2.4 合并统计3张表的首数字分布情况
(3)输出分布概率到excel文件。
三、代码实现
#coding=utf-8
import tushare as ts
import talib as ta
import numpy as np
import pandas as pd
import os,time,sys,re,datetime
import csv
import scipy
import re,urllib2
import xlwt
from BeautifulSoup import BeautifulSoup
#获取股票列表
#code,代码 name,名称 industry,所属行业 area,地区 pe,市盈率 outstanding,流通股本 totals,总股本(万) totalAssets,总资产(万)liquidAssets,流动资产
# fixedAssets,固定资产 reserved,公积金 reservedPerShare,每股公积金 eps,每股收益 bvps,每股净资 pb,市净率 timeToMarket,上市日期
def Get_Stock_List():
df = ts.get_stock_basics()
return df[0:2]
#取财务报表每个单元格的首位数字
def Get_First(inputtext):
outputtext = 0
try:
if inputtext<>'--' and len(inputtext)>0:
#print inputtext
inputtext = float(inputtext.replace(',',''))
inputtext = abs(inputtext)
if inputtext>1:
outputtext=int(str(inputtext)[0])
else:
temp=inputtext*10000*10000
outputtext=int(str(temp)[0])
except:
return
return outputtext
#计算每个首位数字出现的次数,计入统计数组
def Get_Count(numcount,inputtext):
outputtext = Get_First(inputtext)
if outputtext ==1:
numcount[0]+=1
elif outputtext ==2:
numcount[1]+=1
elif outputtext ==3:
numcount[2]+=1
elif outputtext ==4:
numcount[3]+=1
elif outputtext ==5:
numcount[4]+=1
elif outputtext ==6:
numcount[5]+=1
elif outputtext ==7:
numcount[6]+=1
elif outputtext ==8:
numcount[7]+=1
elif outputtext ==9:
numcount[8]+=1
#elif outputtext ==0:
#print inputtext,outputtext
return numcount
#抓取网页数据,统计每位数字频率
def Get_Num(url,code,numcount):
headers = {"User-Agent":"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6"}
req = urllib2.Request(url, headers = headers)
try:
content = urllib2.urlopen(req).read()
except:
return
soup = BeautifulSoup(content)
table = soup.find("table",{"class":"table_bg001 border_box limit_sale scr_table"})
for row in table.findAll("tr"):
cells = row.findAll("td")
if len(cells) > 0:#
i = 0
lencell = len(cells)#统计财务报表的年数
#print lencell
while i < len(cells):
numcount = Get_Count(numcount,cells[i].text)
#print cells[i].text
i=i+1
return (numcount,lencell)
def Benfude(df_Code,count):
for Code in df_Code.index:
print(u"股票代码:" + Code)
Name = df_Code.loc[Code,'name']
print Name
ws.write(count, 0, Code)
ws.write(count, 1, Name)
# 1,2,3,4,5,6,7,8,9
NumCount = [0,0,0,0,0,0,0,0,0]
#资产负债表
Url1 = 'http://quotes.money.163.com/f10/zcfzb_'+Code+'.html?type=year'
(NumCount,LenCell) = Get_Num(Url1,Code,NumCount)
#利润表
Url2 = 'http://quotes.money.163.com/f10/lrb_'+Code+'.html?type=year'
(NumCount,LenCell) = Get_Num(Url2,Code,NumCount)
#现金流量表
Url3 = 'http://quotes.money.163.com/f10/xjllb_'+Code+'.html?type=year'
(NumCount,LenCell) = Get_Num(Url3,Code,NumCount)
print NumCount
Number =[1,2,3,4,5,6,7,8,9]
for i in Number:
ws.write(count, i+1, NumCount[i-1])
SumCount = sum(NumCount)
ws.write(count, 11, SumCount)
#统计每个数字的出现频率并写入文件
NumberCount = NumCount
for i in Number:
NumberCount[i-1]=round(NumCount[i-1]/float(SumCount),3)
ws.write(count, i+11, NumberCount[i-1])
print NumberCount
ws.write(count, 21, LenCell)
wb.save('NumCount.xls')
count = count +1
#主函数
df = Get_Stock_List()
count = 1
if __name__ == '__main__':
#定义excel表格内容
wb = xlwt.Workbook()
ws = wb.add_sheet(u'统计表')
ws.write(0, 0, u'股票代码')
ws.write(0, 1, u'股票名称')
ws.write(0, 2, u'1')
ws.write(0, 3, u'2')
ws.write(0, 4, u'3')
ws.write(0, 5, u'4')
ws.write(0, 6, u'5')
ws.write(0, 7, u'6')
ws.write(0, 8, u'7')
ws.write(0, 9, u'8')
ws.write(0, 10, u'9')
ws.write(0, 11, u'求和')
ws.write(0, 12, u'1')
ws.write(0, 13, u'2')
ws.write(0, 14, u'3')
ws.write(0, 15, u'4')
ws.write(0, 16, u'5')
ws.write(0, 17, u'6')
ws.write(0, 18, u'7')
ws.write(0, 19, u'8')
ws.write(0, 20, u'9')
ws.write(0, 21, u'年数')
Benfude(df,count)
四、统计结果分析
沪深两市2713家股票的本福德分布如下,整体上是服从本福德分布的。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
0.05 | 0.252 | 0.14 | 0.098 | 0.074 | 0.059 | 0.048 | 0.04 | 0.034 | 0.03 |
0.25 | 0.28 | 0.16 | 0.114 | 0.088 | 0.072 | 0.059 | 0.05 | 0.044 | 0.038 |
0.5 | 0.299 | 0.174 | 0.125 | 0.098 | 0.08 | 0.067 | 0.057 | 0.05 | 0.045 |
0.75 | 0.317 | 0.189 | 0.137 | 0.108 | 0.089 | 0.075 | 0.064 | 0.057 | 0.051 |
0.95 | 0.347 | 0.212 | 0.157 | 0.125 | 0.105 | 0.088 | 0.076 | 0.068 | 0.061 |
标准差 | 0.030 | 0.022 | 0.018 | 0.015 | 0.014 | 0.012 | 0.011 | 0.010 | 0.009 |
均值 | 0.299 | 0.175 | 0.126 | 0.098 | 0.081 | 0.068 | 0.058 | 0.051 | 0.045 |
本福德 | 0.301 | 0.176 | 0.125 | 0.097 | 0.079 | 0.067 | 0.058 | 0.051 | 0.046 |
但是对于造假公司来说,就不是了。
退市的欣泰电气,1和9的分布很不正常
300372 | 欣泰电气 | 0.26 | 0.193 | 0.143 | 0.107 | 0.085 | 0.064 | 0.067 | 0.056 | 0.025 |
ST博元,5和6的分布也很不正常
600656 | *ST博元 | 0.28 | 0.156 | 0.131 | 0.115 | 0.103 | 0.052 | 0.063 | 0.043 | 0.058 |
ST的舜船,2的分布很不正常
002608 | *ST舜船 | 0.285 | 0.227 | 0.121 | 0.084 | 0.07 | 0.076 | 0.055 | 0.039 | 0.043 |
http://mt.sohu.com/20160616/n454782551.shtml
万福生科,承认造价,其3和5的分布就很不正常。
300268 | 万福生科 | 0.315 | 0.156 | 0.162 | 0.092 | 0.102 | 0.049 | 0.042 | 0.037 | 0.045 |
莲花味精,为避免业绩亏损而造假,2和4的分布很不正常。
600186 | 莲花味精 | 0.29 | 0.227 | 0.129 | 0.079 | 0.086 | 0.064 | 0.03 | 0.059 | 0.037 |
http://stock.hexun.com/2016-03-16/182790986.html
如果按照每个公司的1到9的首位数字分布是否在本福德均值的2倍标准差范围内看的话,只要有一个数字超出这个范围,这个公司的标示值就加一,最高加到9,2713家A股公司中有250家公司的首位数分布中至少有2个数字是超出本福德定律给出的均值加减2倍标准差的。具体情况如下,具体名单就不用说了,按照前面的代码运行处结果计算后就能看到。
只能说A股打假还要持续啊。
累计 | 汇总 |
0 | 1891 |
1 | 572 |
2 | 179 |
3 | 49 |
4 | 18 |
5 | 3 |
6 | 1 |
总计 | 2713 |