我如何构建一个交互式仪表板Web应用程序以可视化拳击数据

我是格斗运动的忠实粉丝,尤其是拳击是我的最爱。 尽管它看起来可能是纯粹的体育运动,而您的唯一目标是发件箱或将对手击倒,但人们所期望的却更具战略意义,并且融入了元素心理学。 就像下棋游戏一样,每次掷拳都要计算,漫不经心地过度伸展自己可能会使您更容易受到反击的打击,而过于被动和防守可能会使势头向对手有利,而不会使您获得足够的积分来赢得战斗。 如果您让自我怀疑陷入困境或被对手吓倒,那么您已经输掉了这场战斗。 最重要的是,您需要尊重这项运动及其带来的威胁生命的危险。 用Sugar Ray Leonard的话来说,“你不玩拳击”。

从表面上看,这可能不是“绅士运动”。 大多数职业拳击手如何以令人难以置信的运动精神操守自己,总是给我留下深刻的印象。 我发现这类似于生活,而对手就是生活的挑战。

鉴于对拳击的兴趣,我决定为我的Capstone项目之一构建一个Web应用程序,该应用程序将显示最终用户根据所选战斗机而出现不同战斗结果的概率。 但是,在此之前,我想观察数据并回答我的一些遗留问题。 我对不同年龄组之间的胜率和战斗姿态特别感兴趣。 我还想清楚地了解我的数据集中的拳击手所赢得的胜利数量,并将其与打出的回合数量并列。 最后,我想根据每个拳击手打的回合的结果和他们击败的对手的能力来建立每个分区前10名拳击手的名单。 为了回答这些问题,我决定构建一个交互式仪表板,其中包含与问题相关的可视化效果。 互动性使我可以使用自己的体重级别/分区和性别作为过滤器,以针对所选体重级别和性别的拳击手定制可视化效果。 这将作为我项目的探索性数据分析部分。

进入数据

对于这个项目,我获得了3843个拳击手的列表,每个拳击手都有指向其个人资料的URL。 该链接包括与每个拳击手关联的唯一标识符。

data[ 'id' ] = data[ 'players_links' ].str.extract( '(\d+)' )

为了丰富我在每架战斗机上的数据,我需要提取数字唯一标识符。 我在GitHub上的一个基于node.js的API上找到了一个API,该API要求拳击手的唯一ID,以便提取与每个战斗员进行的比赛有关的所有数据,裁判的记分卡,每次比赛的长度,结果和其他数据这可能对我的项目很有价值。

然后,使用此列表,我在node.js中创建了一个函数,该函数将读取从每个战斗机的配置文件URL中提取的每个唯一标识符,将ID附加到API中的方法中,该方法将允许我获得所需的输出并将输出写入到新的csv。 您可以在我的GitHub上查看所有代码。

async function writeData ( )  {
    const csv = require ( 'csv-parser' )
    const results = [];
    fs.createReadStream( 'C:\\Users\\User\\Documents\\GitHub\\Springboard Capstone BoxingPredictionWebApp\\boxingdata\\readdata.csv' )
        .pipe(csv())
        .on( 'data' , ( data )=> results.push(data))
        .on( 'end' , async () => {
          const cookieJar = await getCookieJar();
          const promises = [];
          results.forEach( ( data ) => {
            promises.push(boxrec.getPersonById(cookieJar,data.id));
          })
          const fighters = await Promise .all(promises); 
          fighters.forEach( ( fighter ) => {
              let data = '' ;
              for ( const key in fighter.output) {
                  if ( Array .isArray(fighter.output[key])) {
                      data += JSON .stringify(fighter.output[key]) + ',' ;
                  } else if ( typeof fighter.output[key] === 'object' ) {
                      data += JSON .stringify(fighter.output[key]) + ',' ;
                  } else {
                      data += fighter.output[key] + ',' ;
                  }
              }
              data = data.replace( /(^,)|(,$)/g , "" );
              data += '\n' ;
              
              fs.appendFile( 'C:\\Users\\User\\Documents\\datatest.csv' ,data, ( err ) => {
                  if (err) throw err;
              });

由于API以JSON格式返回输出。 我留下的数据不干净而且有点混乱。 列间距不一致,对于某些拳击手,第7列似乎与给定拳击手的出生日期相关,而在其他情况下,该列具有与给定拳击手的首次比赛日期相关的数据。

观察和理解数据以及输出的性质揭示了一种模式。 每次打架都以日期键开始,键的值是给定打架的日期。 我使用这种逻辑根据给定战斗机进行的独特回合来划分栏目。

df = pd.DataFrame(file_full[file_full.columns[ 0 :]].apply( lambda x: ' ' .join(x.astype(str)),axis= 1 ))
df = pd.DataFrame(df[ 0 ].str.replace( 'date' , 'dateday' ))
#split each fight into a separate column
df_split = pd.DataFrame(df[ 0 ].str.split( 'date' ,expand= True ))
def func (df,col) :
    return df[col].str.extract( 'day(?P<day>.*?)firstBoxerRating(?P<firstBoxerRating>.*?)firstBoxerWeight(?P<firstBoxerWeight>.*?)judges(?P<JudgeID>.*?)links(?P<Links>.*?)location(?P<location>.*?)metadata(?P<metadata>.*?)numberOfRounds(?P<numberofrounds>.*?)outcome(?P<outcome>.*?)rating(?P<rating>.*?)referee(?P<referee>.*?)secondBoxer(?P<secondBoxer>.*?)secondBoxerLast6(?P<secondBoxerLast6>.*?)secondBoxerRating(?P<secondBoxerRating>.*?)secondBoxerRecord(?P<secondBoxerRecord>.*?)secondBoxerWeight(?P<secondBoxerWeight>.*?)titles(?P<titles>.*?){' )
for i in range( 1 ,len(df_split.columns) -1 ):
    df_split[[ 'date' +str(i), 'firstBoxerRating' +str(i), 'firstBoxerWeight' +str(i), 'JudgeID' +str(i), 'Links' +str(i),
       'location' +str(i), 'metadata' +str(i), 'numberofrounds' +str(i), 'outcome' +str(i), 'rating' +str(i),
       'referee' +str(i), 'secondBoxer' +str(i), 'secondBoxerLast6' +str(i), 'secondBoxerRating' +str(i),
       'secondBoxerRecord' +str(i), 'secondBoxerWeight' +str(i), 'titles' +str(i)]] = func(df_split,i)

我继续将数据集中的列连接为一列。 我认为将所有数据都放在一列中将使提取与我指定的字符模式匹配的数据组更加容易,然后根据此数据创建列。 我遍历各列,将不同的字符模式提取到多个唯一的列中。 对于每场比赛,我都有回合的日期,拳击手的等级和体重,与裁判相关的信息(法官姓名和记分卡),回合的位置,元数据,包括比赛的时间,拳击手的别名等,轮次,回合的结果,与裁判有关的信息,对手的姓名,最近6次的回合结果,他们的等级,当时的所有拳击记录(发生回合时),他们的体重和任何头衔由两个拳击手举行。 对于这些属性中的每一个,我都有85列,这大概是因为该数字是我的数据集中拳击手搏击次数最多的一次。

进一步的清理涉及两个过程,我不得不重复多次。 这涉及删除字符并将数据格式更改为字符串或浮点数,


#cleanup first boxer rating
def remove_colon (col_name) :
    return merged[col_name].str.replace( ':' , '' )
#weights need to be converted to float
def firstweight (col_name) :
    a = remove_colon(col_name)
    return pd.to_numeric(a,errors= 'coerce' )
#update first boxer rating columns
boxer_var = list_var( 'firstBoxerRating' )
for i in boxer_var:
    merged[i] = remove_colon(i)
#update weight
weight_var = list_var( 'firstBoxerWeight' )
for i in weight_var:
    merged[i] = firstweight(i)

或从每列中提取字符模式。 对于清理过程的完整分解,您可以查看我的完整代码

def split_time (col) :
    return merged[col].str.extract( '(\d*\:\d+)' ,expand= True )
times = list_var( 'metadata' )
for i in times:
    merged[i] = split_time(i)

为了找到战斗姿态之间的胜率,我决定建立一个热图。 为此,我将数据从宽到长整形,将数据专门限制在需要的列上。 简而言之,在每次战斗中,我查看了两个拳击手的战斗姿态,计算了战斗姿态的获胜次数,计算了每种姿态组合之间的总搏斗次数,然后将两者相除,得出我所定义的“胜利”。率”。

我使用相同的逻辑来构建数据集,然后使用该逻辑来构建另一个热图来比较不同年龄组之间的胜率。 对于年龄段,我将战斗机年龄划分为5年时间间隔,以创建20-25、25-30等年龄段。我强烈希望看到30-35岁年龄段(可能是35-40岁年龄段)的胜率会大大提高男性体重师。 有趣的是,这些似乎是大多数精英拳击手似乎达到顶峰的年龄范围。 我编写的代码在GitHub存储库中可用。

因为我想按分区显示前十名的拳击手。 我需要弄清楚自定义评分的工作原理。 尽管我本可以完全专注于获胜和平局,但每次获胜和平局都应获得奖励,但我认为我需要惩罚战斗机的损失。 我给每场比赛X10奖励胜利,X5奖励平局,-X10罚分损失。 我还想考虑一架特定战斗机击败对手的能力。 击败许多对手是一项了不起的壮举,但击败一名优秀的拳击手与击败一名旅行社之间是有区别的。

opp_names = [ 'secondBoxer' +str(i) for i in range( 1 , 85 )]
outcome_cols = [ 'outcome' +str(i) for i in range( 1 , 85 )]
#remove quotation marks from secondboxer name
data[opp_names] = data[opp_names].astype(str).apply( lambda x: x.str.replace( '"' , '' ))
#get points for each opponent, if negative convert to zero
data[opp_names]=data[opp_names].apply( lambda x : x.map(dict(zip(topten.name,topten.total_points))))
data[opp_names] = data[opp_names].fillna( 0 )
data[opp_names] = data[opp_names].mask(data[opp_names] < 0 , 0 )
data[outcome_cols] = data[outcome_cols].astype(str).apply( lambda x: x.str.replace( '"' , '' ))
#add opp points to total points if outcome was a win
topten[ 'total_points' ] = (data[opp_names].where(data[outcome_cols].eq( 'win ' ).values, 0 ).sum(axis= 1 )/ 5 ) + (topten[ 'total_points' ])

我使用上一段所述的奖励系统,为每个对手返回了总分。 对于拳击手来说,没有得分(可能是因为缺少数据或拳击手还没有打架)或得分为负(因为拳击手输掉了更多赢得他/她的打斗),所以我用0。然后,我将对手的得分相加,即战斗的结果是获胜(即,result_cols中列出的列中的字符串等于获胜),将得分除以5,并将其加到战斗机的总得分中。

我将得分除以5,是因为我不希望对手的得分超过从胜利和失败得出的得分。 我不想创造一个场景,在这个场景中击败得分为500的玩家会使他的整体得分翻倍。 但是,如果给定的战斗机有击败高素质对手的历史,那么他们对手的才能就会大大提高他们的等级。

但是,我的自定义评分的问题在于,在很多情况下,我缺少有关某些拳击手(包括一些精英拳击手)的数据。 但是,我将不断改进此仪表板。 此等级的另一个局限性在于,得分不取决于战斗发生的时间以及对手在该时间点的得分。 我只是根据对手的总赢,输和平局来查看对手的当前得分。

构建交互式仪表板

我选择使用破折号包来帮助构建仪表板。 该软件包结合了flask和react.js元素,使其成为一个非常强大且美观的软件包,可用于构建交互式仪表板。

对于每个可视化,我都通过回调装饰器声明了输入和输出。 输入要么包括过滤器(例如分区和性别),要么是我创建的要显示在信息中心顶部的图像轮播的图片。

@app.callback(
    dash.dependencies.Output( 'total-bouts-v-bouts-won' , 'figure' ),
    [dash.dependencies.Input( 'weight_class' , 'value' ),
     dash.dependencies.Input( 'gender' , 'value' )])

在装饰器下,我创建了一些函数,用于根据用户的输入选择来更新可视化效果。

def update_scatterplot (weight_class,gender) :
    if weight_class is None or weight_class == []:
        weight_class = WEIGHT_CLASS
    if gender is None or gender == []:
        gender = GENDER

    weight_df = data[(data[ 'division' ].isin(weight_class))]
    weight_df = weight_df[(weight_df[ 'sex' ].isin(gender))]
    return {
        'data' : [
            go.Scatter(
                x=weight_df[ 'bouts_fought' ],
                y=weight_df[ 'w' ],
                text=weight_df[ 'name' ],
                mode= 'markers' ,
                opacity= 0.5 ,
                marker={
                    'size' : 14 ,
                    'line' : { 'width' : 0.5 , 'color' : 'blue' }
                },
            )
        ],

例如,通过散点图显示整个集合,重点放在给定战斗机所赢得的胜利和总回合上,选择给定的分区和/或性别会根据用户的选择更改图形的输出。

创建这些过滤器的过程涉及使用破折号上的一些核心组件,特别是使用多个下拉组件来创建下拉菜单,使人们可以选择单个或多个选项。

  dcc.Dropdown(
        id= 'weight_class' ,
        options=[{ 'label' : i, 'value' : i} for i in data[ 'division' ].unique()],
        multi= True
    ),
    dcc.Dropdown(
        id= 'gender' ,
        options=[{ 'label' : i, 'value' :i} for i in fight_outcomes[ 'sex' ].unique()],
        multi= True
    ),

Dash还允许您使用HTML组件,我使用了其中的一些组件将图像轮播放置在仪表板顶部的中心,并控制图像在轮播上的变化速度。

html.Section(id= 'slideshow' ,children=[
        html.Div(style={ 'backgroundColor' :colors[ 'background' ],
                        'textAlign' : 'center' },id= 'slideshow-container' ,children=[
            html.Div(id= 'image' ),
            dcc.Interval(id= 'interval' ,interval= 3000 ),
        ])
    ])

我遇到的一个小问题是我的热图上的轴排序。 为确保x轴和y轴的顺序一致且顺序正确,我通过使用categoryarray定义了我的轴出现的顺序来对x轴进行自定义排序。 这使我可以根据我在列表中定义的自定义定义顺序来设置x轴的顺序。 您可以在此处获得我用来构建这些可视化效果的代码的完整视图。

为了使设计最简单的仪表板对所有人可见,我选择将仪表板部署在Heroku上 。 欢迎在Twitter @emmoemm上将任何反馈,建议或评论发送给我

From: https://hackernoon.com/d-nr1o32po

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值