作为我的数据科学职业生涯跟踪训练营的一部分,我必须完成一些个人工作。 对于这个特殊的顶峰,我选择专注于构建自己个人关心的东西-与从事激情项目相比,有什么更好的学习方式并可能构建有价值的东西。
此外,我相信要花很多时间来打动别人,而且生活太短了,无法处理您个人认为有趣的事情。 鉴于此,我决定构建一个拳击预测Web应用程序。
可以显示的是用户在战斗中获得给定结果的可能性。 为了重现拳击的现实,即不同体重级别的人不能互相搏斗,所涉及的战斗必须在相同体重级别的战斗机之间进行。
在我的上一篇文章中,我写了关于获取训练和构建模型以执行这些预测所必需的大多数数据的过程。 我还进入了看似费力但很有趣的过程,即清理数据,以交互式仪表板的形式呈现数据并将其部署在Heroku上。
全文可在Hackernoon上找到 。 我还选择根据获胜情况和在较小程度上根据给定的拳击手击败对手的能力来为每个分区建立自定义的前十名。 但是,我没有使用elo-rank类型的评分系统,公平地讲,这可能是拳击界更为真实的表现。
更多清理和改造
为了建立模型,我必须以某种方式转换我已经拥有的数据,这样我就可以为每一回合以及拳击手A和对手的相关统计数据创建一行。 此过程涉及使用列表理解来创建与特定统计信息或对手相关的所有列名称的列表。
cols = [ 'secondBoxer' +str(i) for i in range( 1 , 85 )]
two = [ 'secondBoxerWeight' +str(i) for i in range( 1 , 85 )]
...
对于与回合相关的每个统计信息的列的每个列表,我使用了以拳击手的姓名,性别,部门和全局ID作为标识符变量的未透视列,然后将这些数据帧连接为一个。 最终结果是一个“长数据框”,每一行显示与给定战斗机进行的每场战斗有关的数据,以及对手的状态和与该特定回合相关的状态。
concated = pd.melt(data,id_vars=[ 'name' , 'global_id' , 'sex' , 'division' ], value_vars = cols,var_name= 'label' ,value_name= 'opposition' )
concated_two = pd.melt(data,id_vars=[ 'name' , 'global_id' , 'sex' , 'division' ],value_vars=two,var_name= 'weightb_label' ,value_name= 'opp_weight' ).drop(columns=[ 'global_id' , 'name' , 'sex' , 'division' ])
concated_three = pd.melt(data,id_vars=[ 'name' , 'global_id' , 'sex' , 'division' ],value_vars=three,var_name= 'last6_label' ,value_name= 'opp_last6' ).drop(columns=[ 'global_id' , 'name' , 'sex' , 'division' ])
... #merge all
fully_merged = pd.concat([concated,concated_two,concated_three,concated_four,concated_five,
concated_six,concated_seven,concated_eight,concated_nine,concated_ten,
concated_eleven,concated_twelve,concated_thirteen,concated_fourteen,concated_fifteen],axis= 1 ,sort= False )
fully_merged = fully_merged.set_index( 'name' )
许多sklearn模型通常要求特征是数字而不是分类,因此我发现将所有分类变量转换为数字变量是先发制人的。 例如,在一列中,我得到了对手之前6场比赛的结果。 在这种情况下,我搜索了字符串win。 我计算了发现的总获胜字符串,并将结果乘以10,乘以每个包含抽奖单词的字符串的5个字符串,并为每个损失乘以0的值,从而得到了代表对手最近6次回合的数值得分。
#converting last 6 fights to points
fully_merged[ 'opp_last6' ] = fully_merged.opp_last6.str.count( 'win' )* 10 +fully_merged.opp_last6.str.count( 'draw' )* 5 +fully_merged.opp_last6.str.count( 'loss' )* -5
我在其中一列中列出了一个列表,每个嵌套的列表中都包含了拳击手和对手的裁判得分。 清理此特定列需要提取所有数值,将每个分数的列拆成6列,然后重命名创建的多级列,然后将这些列分配给“主数据框”。 我遵循相同的逻辑来确定每个拳击手赢得的回合数。
ref_points =fully_merged.judge.str.extractall( r'(\b\d+\b)' ).unstack().reindex(fully_merged.index)
ref_points.columns = ref_points.columns.map( '{0[0]}_{0[1]}' .format)
fully_merged[[ 'judge1boxer' , 'judge1opp' , 'judge2boxer' , 'judge2opp' , 'judge3boxer' , 'judge3opp' ]] = ref_points[[ '0_0' , '0_1' , '0_2' , '0_3' , '0_4' , '0_5' ]]
虽然为每个拳击手提供统计数据并有可能将其用作我的模型的功能非常有用,但我还希望为每个对手获得相同的属性和统计数据。 在excel中,有一个称为vlookup的函数,其目的是从特定列中检索值。 例如,我们可以查找一个人的名字,或者一个类似于该名字的字符串,然后在其他列中返回与该人的名字相关的任何值。 使用类似的逻辑。 我使用map键将对手的名字“映射”到name列。 对于每个匹配项,我都获取了与匹配项名称关联的体重,身高和其他统计信息。
dataset[ 'opp_KO ratio' ]= dataset[ 'opposition' ].map(dataset.drop_duplicates( 'name' ).set_index( 'name' )[ 'KO ratio' ])
dataset[ 'opp_KnockedOut ratio' ]= dataset[ 'opposition' ].map(dataset.drop_duplicates( 'name' ).set_index( 'name' )[ 'KnockedOut ratio' ])
#get opponents age, height and bmi type ratio
dataset[ 'opp_age' ]= dataset[ 'opposition' ].map(dataset.drop_duplicates( 'name' ).set_index( 'name' )[ 'age' ])
dataset[ 'opp_height' ]= dataset[ 'opposition' ].map(dataset.drop_duplicates( 'name' ).set_index( 'name' )[ 'height' ])
....
用更多数据充实我的数据集
虽然我已经有相当多的数据需要处理,但是作为一个狂热的拳击迷,我知道那里还有更多的数据可能会帮助我改善模型。 在给定的战斗中投掷和着陆的拳头数量可以汇总为有趣的特征,既显示给定拳击手的整体准确性,又根据对着所述拳击手的拳头百分比来判断拳击手对其他对手的防御能力。

首先使此过程有些棘手的是,要获取每个拳击手的拳打统计。 我最初设想模拟单击查看/下载按钮,以显示每次战斗的拳击状态。 为了做到这一点,我尝试使用硒输入给定拳击手的全名,然后模拟单击“查看/下载统计信息”按钮,并为给定拳击手战斗的每一回合读取带有打孔统计信息的文本。 但是,我最终选择使用请求库从辅助网址中获取每个战斗机的打卡统计数据,这被证明是更可行的选择。
我使用了请求库,首先为数据源中可用的每个拳击手创建了一个ID列表,然后遍历该列表,并为每个拳击手ID返回记录的战斗以及每次战斗的打卡统计信息。 尽管我可以一轮打破打卡统计数据,但我选择专注于总数。 每次战斗中投掷和着陆的刺棍,力量拳和整体拳的总数。
# get punch stats per fight
def punch_stats (df) :
final_rounds_df = pd.DataFrame()
final_df = pd.DataFrame()
stats_pattern = re.compile( '\d+\.?\d?(?=%)|\d+\/\d+' )
for index, row in df.iterrows():
...
# create the data/parameters for each request
dataload = { "event_id" : row[ 'event_id' ],
"fighter1_id" : row[ 'fighter1id' ],
"fighter2_id" : row[ 'fighter2id' ],
"fighter1_name" : row[ 'fighter1ln' ],
"fighter2_name" : row[ 'fighter2ln' ]
}
...
# scrape all the round data from the response
stats = re.findall(stats_pattern, r.text)
slice1 = []
for no in range( 78 ):
slice1.append( 2 )
data_input = iter(stats)
stats = [list(islice(data_input, elem)) for elem in slice1]
slice2 = [ 12 , 12 , 12 , 12 , 12 , 12 , 3 , 3 ]
input2 = iter(stats)
stats = [list(islice(input2, elem)) for elem in slice2]
# final punch stats
for idx, fighter in enumerate(stats[ -2 :]):
total_df = pd.DataFrame(fighter)
#add the event_id
total_df[ 'event_id' ] = row[ 'event_id' ]
#fighters name
if idx % 2 == 0 :
total_df[ 'fighter' ] = row[ 'fighter1ln' ]
else :
total_df[ 'fighter' ] = row[ 'fighter2ln' ]
#add stat titles
total_df[ 'punch_stat' ] = [ 'Total Punches' , 'Jabs' , 'Power Punches' ]
#append dataframes to the corresponding dataframes
final_df = final_df.append(total_df)
#renaming columns
final_df.rename(columns={ 0 : 'punches' , 1 : 'pct_landed' }, inplace= True )
#dropping duplicates
final_df.drop_duplicates(inplace= True )
return final_df
为了简洁起见,我没有包括我编写的全部代码,而是用省略号代替了某些部分。
与往常一样,此步骤之后是清理和转换过程。 这涉及将数据从长到宽旋转以轻松汇总每个拳击手的打孔统计数据,然后删除包含对我的模型或交互式仪表板均无用的信息的列。
不幸的是,将其与我的数据合并后,我发现CompuBox在我的数据集中约有16%-20%的拳击手具有打孔统计数据,从而减少了这些统计数据对我的模型的总体影响。 为了构建更具视觉吸引力的Web应用程序,我还决定使用beautifulsoup库刮擦数据集中所有拳击手的照片。 其背后的想法是确保如果用户选择了一个拳击手,则假设该拳击手的图片(假设可用)将显示在用户选择的正下方。
path = 'https://raw.githubusercontent.com/EmmS21/SpringboardCapstoneBoxingPredictionWebApp/master/boxingdata/df2.csv'
file = pd.read_csv(path)
for index,row in file.iterrows():
sleep( 2 )
site = row[ 'players_links' ]
response = requests.get(site)
soup = BeautifulSoup(response.text, 'html.parser' )
pics = soup.find( 'img' )
try :
pic_url = pics[ 'src' ]
urllib.request.urlretrieve(pic_url, 'C:\\Users\\User\\Documents\\GitHub\\SpringboardCapstoneBoxingPredictionWebApp\\pictures\\' + str(site.split( '/' )[ -1 ])+ '.jpg' )
except :
image = 'https://chapters.theiia.org/central-mississippi/About/ChapterOfficers/_w/person-placeholder_jpg.jpg'
urllib.request.urlretrieve(image, 'C:\\Users\\User\\Documents\\GitHub\\SpringboardCapstoneBoxingPredictionWebApp\\pictures\\' +str(site.split( '/' )[ -1 ])+ '.jpg' )
通过检查每个配置文件的元素,我注意到只有配置文件图片被封装在img标签中,使用此知识,我遍历了数据集,并为指向拳击手配置文件的每个URL链接找到了标签“ img”中的每个URL然后使用try和除了从提取的URL中检索每张图片,以捕获可能发生的任何异常。 例如,捕获在给定页面中没有个人资料图片的情况。
建立模型
在建立实际模型以执行预测时,我最初选择使用随机森林分类器。 为了简单地解释它,请考虑人类通常如何做出决定。 让我们以决定去健身房的例子为例。 我们可以形成决策树来模拟此过程。 我首先问自己,以前的锻炼是否让我的身体仍然疲倦。 最初的问题分为两个节点: 是和否。假设我很累,然后我会问自己,我的身体是否真的很累,或者我是否只是不想去健身房。 然后,两个分支将出自此节点。 随着该过程在所有节点上继续进行,每个分支最终将生成其自己的结论/最终结果。 我可以选择去健身房,选择在家室内锻炼,选择进行其他形式的体育锻炼或休息。 我们可以将每个节点视为一个功能或功能的组合。 随机森林本质上是多个决策树。 但是,对于每棵树,都会引入一个随机性元素,每棵树会采用一组随机的特征和样本进行替换(对于采样的每个项目,绘制的项目将在绘制下一个样本之前返回到数据集中)。 然后,该模型基于从树中得出的多数预测进行预测。
但是,长期的拳击迷可以证明,拳击比赛很少而且很遥远,这在我的数据中是显而易见的。 因此,我正在处理高度失衡的阶级,而“阶级”一词指的是给定战斗中可能产生的结果。 结果,评估我的混淆矩阵时,预测抽奖的准确性大大降低。 为了“平衡”这些预测,我决定为数据集中的每个类别分配单独的权重,并为绘图分配更大的权重。 通过此过程,使用GridSearch进行超参数调整,并对最重要的功能进行过滤,最终我将绘画预测的准确性提高了50%以上。
经过与导师的进一步研究和交谈之后,我选择测试分类提升模型或“ CatBoost” 。 有趣的是,该模型可以处理分类值和数值,而无需用户在预处理阶段将分类特征转换为数值。 与其他梯度提升算法非常相似,CatBoost实现了决策树,但似乎减少了过拟合,并且比XGBoost所需的参数调整少得多。 有一些文章可以更好地解释这种模型的工作原理,例如我在本文中所附的文章。
通过CatBoost,我能够建立一个性能比我的RandomForest好得多的模型。 评估我的混乱矩阵后发现,输赢的准确度为70-77%,平局为73%。
共享模型
现在,假设假设的汤米(Tommy)在家中将观看Wilder v Fury 2以及2020年2月22日的所有打底打架比赛。出于好奇,他想使用此模型来预测当晚的几次打架结果。 受到重创的Deontay Wilder是否会像在Bermane Stiverne复赛中一样轻松地将他淘汰出局,从而将Tyson Fury加入他的精彩场面? 或者,愤怒会激怒并令怀尔德的防守感到恐惧,不仅保留他直系的“头衔”,而且获得WBC重量级冠军? 为了简化此过程,我决定构建一个Web应用程序,该应用程序将使用我构建的模型来生成以特定方式结束战斗的可能性。

为了在闪亮的应用程序中重用catboost模型,我将其保存为pickle文件。 由于我本质上需要在R中调用Python代码,因此我需要使用reticulate包 。 这允许Python和R之间的互操作性,从而使我能够与Python交互。 将此应用程序构建和部署到Shinyapp.io的过程可以分为三个步骤。 首先,将虚拟环境设置为应用程序所需的python版本,然后显式安装我需要在Shinyapps.io上使用的python库。
virtualenv_create(envname = "python_environment" )
virtualenv_install( "python_environment" , packages =c( "pandas" , "catboost" ))
use_virtualenv( "python_environment" ,required = TRUE )
第二步涉及构建应用程序的用户界面。 它既可以创建为单独的文件,也可以创建为单个文件应用程序中的函数。 在这里,我设置了背景图片,添加了样式标签,创建了对用户输入和下拉菜单有反应的输出标签,以允许用户根据自己的体重分类来过滤拳击手,
...
fluidRow(column(offset = 5 , width = 2 ,align= "center" ,
titlePanel(h5(selectInput( "dropdown" , "Select Boxer Weights" ,choices=unique(boxing$division)))))),
fluidRow(column(offset = 3 , width= 3 ,
wellPanel(
fluidRow(
uiOutput( "Names" ),
uiOutput( "boxerA" )))),
...
最后,我创建了定义应用程序逻辑的服务器功能。 例如,这定义了以下过程:基于用户在下拉选项中选择的权重类别(与权重类别有关)过滤义和团数据集和显示给用户的名称,并渲染与所选义和团相关联的ID的图像。
#filter opponent names based on selection
output$Opponent <- renderUI({
req(input$dropdown)
df <- boxing %>% filter(division % in % input$dropdown)
selectInput( "names2" , "Opponent" ,choices = df$name)
不幸的是,我使用的数据目前存在限制。 拳击迷会注意到,缺少很多大牌。 但是,我将定期更新我的数据,很快就会包含更多拳击手。 对于那些感兴趣的人,可以在这里使用该应用程序-https: //thebeyonder.shinyapps.io/boxingapp2/ 。 免费发送任何反馈或建议以帮助我在Emmoemm的Twitter上改进我的应用程序。
From: https://hackernoon.com/i-built-a-boxing-prediction-web-app-on-shiny-here-is-how-jz8932xt