《点球成金》中的耶鲁高材生Peter向奥克兰运动家队总经理Billy推荐用OBP(On Base Percentage 上垒率)来代替BA(Batting Average 安打率)选材。这一妙招直接让3000万总薪水的小球会,取得了比上亿总薪水的豪门还好的成绩。
我们先来看BA的计算公式。
其中:
AB:At-Bat,有效打席。
H:HIT,安打数。
理解BA,首先需要理解AB和H。理解AB,首先需要理解PA,Plate Appearance,轮击次数,因为AB是PA的一个子集。
当你拎着棒子上场去打击,经过和投手对决,要么被投手投出局,要么可以变成跑垒员,就记录你一个PA。不是轮到你上场打击就肯定有PA,因为还有其他情况轮不到你去打击这一局或者整场比赛就结束的情况。比如垒上发生盗垒被杀出局攻守交换,或者盗垒跑垒得分比赛结束,或者极端天气之类各种不可抗因素导致比赛终止等等。PA就是有确定的由击球员和投手对决后的结果才算。
那么哪些情况算AB呢?就是在PA中,H安打数,E失误Error,FC选杀Fielder’s Choice和O出局数Out的和。即:
特别说明一下FC选杀,垒上有人时,击球员击出球让防守方有多种选择,去杀垒上跑垒员,没有选择杀一垒,击球员安全上一垒,记为FC。
既然AB是PA的子集,那么PA中哪些情况不算AB有效打席呢?
SAC:牺牲打,Sacrifice,又分为SB,SAC Bunt,牺牲触击打(触击成地滚球推进一垒跑垒员到二垒),以及SF,SAC Fly,牺牲高飞打(打成高飞球让三垒跑垒员在外场手接球后回踏垒垫后跑本垒抢分)。
BB:四坏球保送,Base on Balls (Walks)。
HBP:投球中身,Hit by Pitcher。
CI: 捕手干扰,Catcher’s Interference,捕手的动作干扰了击球员的打击。如果CI发生时垒上出现其他有利于攻方的局面,比如盗垒成功了,进攻方教练可以选择不判CI。如果接受CI,那么这次PA也不计入AB。
所以AB也可以表达为:
所以我们看到的PA、AB、H的包含关系是这样的:
我们关心的安打率BA就是H占AB的比例。首先击球员没有被投手投出局,跑垒也没有被传杀出局,他的上垒也不是因为防守方接球失误或者传得不好,也不是因为防守方要去杀前位跑垒员没有被顾及,是完完全全凭借击球能力上垒。在统计口径中,没有计入执行战术需要的各种牺牲打,没有计入因为防守方的问题,比如投手的保送中身或是捕手(甚至还会极个别出现其他场员)的干扰问题。
BA成为比较纯粹的衡量击球员能力的重要指标,直接关联棒球手的身价,这都是容易理解的。当时市场上各类球员也按照BA明码标价排序了。球队如果球员薪金预算有限,就很难保证球队在击球方面有充沛的实力。这时候Peter的研究发现可以用OBP选到实力不俗但市场价值远远被低估的球员。
OBP的计算公式:
OBP衡量了一个球员的上垒能力,他的打击能力也某种程度上包含在里面了。安全上垒没有给防守方出局也是一个球员进攻效率的表现。一般BA数据好的,OBP也很好,但如果市场过度强调BA,发现BA一般但OBP很好的,就是发现了价值洼地。
我们可以用下面的Python命令还原《点球成金》中Peter和Billy所做的分析。
# 导入数据分析和统计分析包
import pandas as pd
import statsmodels.formula.api as sm
# 准备球队数据
# 可以自Lahman’s Baseball Database下载数据包
teams = pd.read_csv('Teams.csv')
teams = teams[teams['yearID'] >= 1985]
teams = teams[['yearID', 'teamID', 'Rank', 'R', 'RA', 'G', 'W', 'H', 'BB', 'HBP', 'AB', 'SF', 'HR', '2B', '3B']]
# 计算BA, OBP和SLG
teams['BA'] = teams['H']/teams['AB']
teams['OBP'] = (teams['H'] + teams['BB'] + teams['HBP']) / (teams['AB'] + teams['BB'] + teams['HBP'] + teams['SF'])
teams['SLG'] = (teams['H'] + (2*teams['2B']) + (3*teams['3B']) + (4*teams['HR'])) / teams['AB']
# 回归分析
print(sm.ols("R~BA", teams).fit().summary())
print(sm.ols("R~OBP", teams).fit().summary())
print(sm.ols("R~OBP+SLG", teams).fit().summary())
这是用普通最小二乘法分析结果,R~BA,得分和安打率的关系:
R~OBP,得分和上垒率的关系:
确定系数R的平方的值,说明了OBP可以解释80%的R(就是Run,得分),而BA只可以解释50%的R,OBP比BA更能决定R。F-statistic也表明R~OBP要拟合得更好。
这充分说明了OBP是比BA对于预测得分能力更靠谱的指标。
这里还一并计算了常用指标OPS, 上垒加长打率,OBP Plus SLG。长打率SLG,是Slugging Percentage的缩写。长打率为一垒安打、二垒安打、三垒安打和本垒打都设置了乘数,更能反映球员的击球能力。
R~OPS的相关度和拟合度结果表明,OPS是衡量一个球员得分能力更好的指标。
当然,当年奥克兰运动家队只运用OBP就实现了以小博大的成绩。
下面两个图也是用Python生成的球队薪水和胜场关系。
1997年运动家还是一个薪水低胜场少的状态,所谓巧妇难为无米之炊。当应用大数据分析后,薪水不到扬基和红袜这样豪门的1/3,但胜场数却实现了神奇的超越。
绘图代码也不长,有Python环境的同学可以自行演算一下不同年份的情况。
# 导入数据分析和绘图包
import pandas as pd
import matplotlib.pyplot as plt
# 绘图识别中文字符
plt.rc("font", family='YouYuan')
# 准备球队数据
teams = pd.read_csv('Teams.csv')
teams = teams[teams['yearID'] >= 1985]
teams = teams[['yearID', 'teamID', 'W']]
teams = teams.set_index(['yearID', 'teamID'])
# 准备薪水数据
# 历史薪水数据在同一个数据包里
salaries = pd.read_csv('Salaries.csv')
salaries_by_yearID_teamID = salaries.groupby(['yearID', 'teamID'])['salary'].sum()
# 并表薪水和球队
teams = teams.join(salaries_by_yearID_teamID)
# 选取年份
year = 2001
teams_year = teams.xs(year)
fig, ax = plt.subplots()
# 标出奥克兰运动家队,纽约扬基队,波士顿红袜队
for i in teams_year.index:
if i == 'OAK' or i == 'NYA' or i == 'BOS':
ax.scatter(teams_year['salary'][i], teams_year['W'][i], color="green", s=200)
ax.annotate(i, (teams_year['salary'][i], teams_year['W'][i]))
else:
ax.scatter(teams_year['salary'][i], teams_year['W'][i], color="grey", s=200)
# 画坐标轴
ax.tick_params(axis='x', labelsize=15)
ax.tick_params(axis='y', labelsize=15)
ax.set_xlabel('薪水', fontsize=20)
ax.set_ylabel('胜场', fontsize=20)
ax.set_title('薪水-胜场关系: ' + str(year), fontsize=25, fontweight='bold')
# 画图
plt.show()