Python实战03:关于运动员伤病预测数据的探索

背景描述

在竞技体育中,运动员的健康和安全是至关重要的。运动损伤不仅可能会对运动员的职业生涯造成严重影响,还可能影响到团队的整体表现。因此,预防和减少运动员的伤病风险成为了许多体育组织和教练团队的首要任务。通过深入了解运动员的身体状况、训练情况以及过往的伤病历史,我们可以更好地预测未来的伤病情况,从而采取相应的措施来保障运动员的健康。

问题描述

运动员受伤风险的影响因素分析
特征相关性分析
特征重要性分析

数据来源

和鲸社区(搜运动员之类关键词)

数据处理及预览

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

# 随机森林模型(Random Forest)
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
df = pd.read_csv(r'D:\pythonbag\Datas\injury_data.csv')
print('-'*80)
print('数据集存在重复值个数:')
print(df.duplicated().sum())
print('数据集存在缺失值个数:')
print(df.isna().sum())
print('-'*80)
print(df.info())
print('-'*80)

print('数据集的统计描述:')
display(df.describe())
print('-'*80)
print('数据集的预览:')
display(df.head(5))

step1.受伤风险的影响因素分析

探索各个特征对受伤可能性的影响大小,例如年龄、体重、身高、曾受过伤与否、身体恢复时长、训练强度

avg_df = df.groupby(by='Likelihood_of_Injury').mean()
display(avg_df)

my_palette=['#C6E3A1','#FF6F61','#362F80']
sns.set_palette(my_palette)
plt.rcParams['font.sans-serif']=['SimHei']
fig,axes=plt.subplots(nrows=2,ncols=3,figsize=(12,10))

sns.barplot(x='Likelihood_of_Injury',y='Player_Age',data=avg_df,ax=axes[0,0],hue='Likelihood_of_Injury',legend=False,width=0.3)
axes[0,0].set_title('受伤与否的年龄平均分布',fontsize=12)

sns.barplot(x='Likelihood_of_Injury',y='Player_Weight',data=avg_df,ax=axes[0,1],hue='Likelihood_of_Injury',legend=False,width=0.3)
axes[0,1].set_title('受伤与否的体重平均分布',fontsize=12)

sns.barplot(x='Likelihood_of_Injury',y='Player_Height',data=avg_df,ax=axes[0,2],hue='Likelihood_of_Injury',legend=False,width=0.3)
axes[0,2].set_title('受伤与否的身高平均分布',fontsize=12)

sns.barplot(x='Likelihood_of_Injury',y='Previous_Injuries',data=avg_df,ax=axes[1,0],hue='Likelihood_of_Injury',legend=False,width=0.3)
axes[1,0].set_title('受伤与否的曾受过伤与否平均分布',fontsize=12)

sns.barplot(x='Likelihood_of_Injury',y='Training_Intensity',data=avg_df,ax=axes[1,1],hue='Likelihood_of_Injury',legend=False,width=0.3)
axes[1,1].set_title('受伤与否的训练强度平均分布',fontsize=12)

sns.barplot(x='Likelihood_of_Injury',y='Recovery_Time',data=avg_df,ax=axes[1,2],hue='Likelihood_of_Injury',legend=False,width=0.3)
axes[1,2].set_title('受伤与否的恢复时长平均分布',fontsize=12)

for index,row in avg_df.iterrows():
    # row是指每一行数据,row.name是指每一行的索引即0和1
    axes[0,0].text(row.name,row['Player_Age'],str(round(row['Player_Age'],2))+'岁',ha='center',va='bottom',fontsize=10)
    axes[0,1].text(row.name,row['Player_Weight'],str(round(row['Player_Weight'],2))+'kg',ha='center',va='bottom',fontsize=10)
    axes[0,2].text(row.name,row['Player_Height'],str(round(row['Player_Height'],2))+'cm',ha='center',va='bottom',fontsize=10)
    axes[1,0].text(row.name,row['Previous_Injuries'],str(round(row['Previous_Injuries']*100,2))+'%',ha='center',va='bottom',fontsize=10)
    axes[1,1].text(row.name,row['Training_Intensity'],str(round(row['Training_Intensity'],2)),ha='center',va='bottom',fontsize=10)
    axes[1,2].text(row.name,row['Recovery_Time'],str(round(row['Recovery_Time'],2))+'天',ha='center',va='bottom',fontsize=10)

plt.show()

小结:初步判断曾受过伤与否和训练强度对受伤可能性有一些影响。曾受过伤的运动员可能会受伤概率略高于不曾受过伤的运动员的0.038,可能会受伤的运动员的训练强度平均为0.516,而可能不会受伤的运动员的训练强度平均为0.465。其他属性例如年龄、体重、身高、恢复时长无明显的差距,也许和受伤可能性的相关性比较低。

接下来进一步对年龄、体重、身高数据下钻分析。这些数据需要划分区间,转换成离散数据才能绘图,因此本案例采用了四分位数法和自定义区间进行划分。不过我个人觉得四分位数法划分的区间不具有实际意义,自定义划分相对更合适些。

# 这样就不会跟df共享内存
age_df = df.copy()
age_df['Age_Q'] = pd.cut(age_df['Player_Age'],bins=[18,23,30,36,40],labels=['18-22岁青少年','23-29岁成年早期','30-35岁成年中期','36-40岁成年晚期'])

# 方法一:数透
# age_df = pd.pivot_table(df,values='Recovery_Time',index='Player_Age',columns='Likelihood_of_Injury',aggfunc='count')
# # 使用字典map修改列名
# injury_dict={0:'NoLikelihood_Num',1:'Likelihood_Num'} 
# age_df.columns = age_df.columns.map(injury_dict)
# # 计算受伤与否的概率
# age_df['NoLikelihood_Per'] = round(age_df['NoLikelihood_Num']/(age_df['NoLikelihood_Num'] + age_df['Likelihood_Num'])*100,2)
# age_df['Likelihood_Per'] = round(age_df['Likelihood_Num']/(age_df['NoLikelihood_Num'] + age_df['Likelihood_Num'])*100,2)

# 方法二:groupby后apply
# age_per()函数是分组后计算的,即每一组数据执行一次
def age_per(x):
    total = x.shape[0]
    likelihood_num = (x['Likelihood_of_Injury']==1).sum()
    nolikelihood_num = (x['Likelihood_of_Injury'] == 0).sum()
    likelihood_per =round(likelihood_num/total*100,2)
    nolikelihood_per = round(nolikelihood_num/total*100,2)
    # 返回dataframe类型的数据便可以跟原数据合并起来
    return pd.Series({'Likelihood_Per':likelihood_per,'NoLikelihood_Per':nolikelihood_per})
    
age_df = age_df.groupby(by=['Age_Q'],as_index=False).apply(age_per)

display(age_df)

sns.set_palette(my_palette)
plt.rcParams['font.sans-serif']=['SimHei']
plt.figure(figsize=(8,5)) # 创建绘图大小

# ax创建一个新的坐标轴对象,并把绘图加入里面;ax=ax意思是把另一个新绘图加入原有的坐标轴对象里,即在同一张图上绘制多个图表。
ax = sns.barplot(x='Age_Q',y='Likelihood_Per',data=age_df,label='Likelihood',width=0.2)
sns.barplot(x='Age_Q',y='NoLikelihood_Per',data=age_df,label='NoLikelihood',ax=ax,width=0.06)

ax.set_title('各个年龄组可能受伤与否的运动员的占比',pad=10,fontsize=14)
ax.set_xlabel('Age_Q')
ax.set_ylabel('per(%)')

for index,row in age_df.iterrows():
    # ax.text(x,y,value,ha=None,va=None)
    # row.name返回每一行的索引名即index(0~21)
    ax.text(row.name,row['Likelihood_Per'],str(row['Likelihood_Per'])+'%',ha='center',va='bottom',fontsize=10)
    ax.text(row.name,row['NoLikelihood_Per'],str(row['NoLikelihood_Per'])+'%',ha='center',va='bottom',fontsize=10)

plt.legend(loc=4)
plt.show()

年龄方面:成年早期在运动领域中被认为是黄金时期,在这段时间训练强度也许有所增强,导致可能受伤的概率更大一些。

weight_df = df.copy() # 这样就不会共享内存

# 方法一:np.quantile(可能没有实际意义)
# weight_bins = np.quantile(weight_df['Player_Weight'],np.linspace(0,1,5))
# weight_df['Weight_Q'] = pd.cut(weight_df['Player_Weight'],bins=weight_bins,labels=['40-68kg','68-75kg','75-81kg','81-105kg'])

# 方法二:pandas 计算每个四分位数的边界
# weight_max = weight_df['Player_Weight'].max()
# weight_min = weight_df['Player_Weight'].min()
# weight_q2 = weight_df['Player_Weight'].quantile(0.25)
# weight_q3 = weight_df['Player_Weight'].quantile(0.5)
# weight_q4 = weight_df['Player_Weight'].quantile(0.75)
# weight_bins = [weight_min,weight_q2,weight_q3,weight_q4,weight_max]
# weight_df['Weight_Q'] = pd.cut(weight_df['Player_Weight'],bins=weight_bins,labels=['40-68kg','68-75kg','75-81kg','81-105kg'])

# 方法三:自定义划分区间
weight_df['Weight_Q'] = pd.cut(weight_df['Player_Weight'],bins=[0,50,75,90,110],labels=['<50kg','50-75kg','75-90kg','>90kg'])

def weight_per(x):
    total = x.shape[0]
    likelihood_num = (x['Likelihood_of_Injury']==1).sum()
    nolikelihood_num = (x['Likelihood_of_Injury'] == 0).sum()
    likelihood_per =round(likelihood_num/total*100,2)
    nolikelihood_per = round(nolikelihood_num/total*100,2)
    return pd.Series({'Likelihood_Per':likelihood_per,'NoLikelihood_Per':nolikelihood_per})
    
weight_df = weight_df.groupby(by='Weight_Q',as_index=False).apply(weight_per)

display(weight_df)

sns.set_palette(my_palette)
plt.rcParams['font.sans-serif']=['SimHei']
plt.figure(figsize=(8,5))

ax = sns.barplot(x='Weight_Q',y='Likelihood_Per',data=weight_df,width=0.2,label='Likelihood_Per')
sns.barplot(x='Weight_Q',y='NoLikelihood_Per',data=weight_df,ax=ax,width=0.06,label='NoLikelihood_Per')

for index,row in weight_df.iterrows():
    plt.text(row.name,row['Likelihood_Per'],'绿:'+str(row['Likelihood_Per'])+'%',ha='center',va='bottom')
    plt.text(row.name,row['NoLikelihood_Per'],'红:'+str(row['NoLikelihood_Per'])+'%',ha='center',va='top')

plt.title('各个体重组可能受伤与否的运动员的占比',pad=10,fontsize=14)
plt.legend(loc=4)

plt.show()

体重方面:小于50kg的可能受伤概率达到83.3%,说明身体瘦弱的运动员相对更容易受伤。

height_df = df.copy()

# 采用四分位数法进行划分的区间可能不具实际意义
# height_bins = np.quantile(height_df['Player_Height'],np.linspace(0,1,5))
# height_df['Height_Q'] = pd.cut(height_df['Player_Height'],bins=height_bins,labels=['145-173cm','173-180cm','180-187cm','187-207cm'])

height_df['Height_Q'] = pd.cut(height_df['Player_Height'],bins=[0,165,175,185,210],labels=['<165cm','165-175cm','175-185cm','>185cm'])

def height_per(x):
    total=x.shape[0]
    likelihood_num = (x['Likelihood_of_Injury']==1).sum()
    nolikelihood_num = (x['Likelihood_of_Injury']==0).sum()
    likelihood_per = round(likelihood_num/total*100,2)
    nolikelihood_per = round(nolikelihood_num/total*100,2)
    return pd.Series({'Likelihood_Per':likelihood_per,'NoLikelihood_Per':nolikelihood_per})

height_df = height_df.groupby(by='Height_Q',as_index=False).apply(height_per)

display(height_df)

sns.set_palette(my_palette)
plt.rcParams['font.sans-serif']=['SimHei']
plt.figure(figsize=(8,5))

ax = sns.barplot(x='Height_Q',y='Likelihood_Per',data=height_df,width=0.2,label='Likelihood_Per')
sns.barplot(x='Height_Q',y='NoLikelihood_Per',data=height_df,ax=ax,width=0.06,label='NoLikelihood_Per')

for index,row in height_df.iterrows():
    plt.text(row.name,row['Likelihood_Per'],str(row['Likelihood_Per'])+'%',ha='center',va='bottom',fontsize=10)
    plt.text(row.name,row['NoLikelihood_Per'],str(row['NoLikelihood_Per'])+'%',ha='center',va='bottom',fontsize=10)    

plt.title('各个身高组可能受伤与否的运动员的占比',pad=10,fontsize=14)
plt.legend(loc=4)
plt.show()

身高方面:175-185cm的运动员相对其它身高更容易受伤。我也不知道为啥....

step2.特征相关性分析

本项目采用皮尔逊相关系数算法

# 皮尔逊相关系数
correlation_matrix=df.corr(method='pearson')
display(correlation_matrix)

plt.rcParams['font.sans-serif']=['SimHei']
plt.figure(figsize=(6,5))
# annot=True显示数据标签
sns.heatmap(correlation_matrix,annot=True,cmap='coolwarm',linewidth=0.5) 
plt.title('运动员受伤可能性和其他属性的相关性热图',pad=20)
plt.show()

小结:从热图上来看,受伤可能性和训练强度的相关系数是0.089,说明有一些相关关系,但不是很强。

step3.特征重要性分析

# 准备数据集
# x是包含身高、体重、年龄等的特征数据,y是可能受伤与否数据的目标变量
x,y = df.iloc[:,:-1],df['Likelihood_of_Injury']

# 将数据集划分为训练数据集和测试数据集
# test_size=0.2是值测试数据集20%,训练数据集80%;random_state=42值
x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.2,random_state=42)
# 选择模型并训练
model = RandomForestClassifier()
model.fit(x_train,y_train)
# 使用训练好的模型预测
pre = model.predict(x_test)
# 计算并输出模型在测试集上的预测准确率(跟y_test对比)
print("Model prediction accuracy: {}%\n".format(round(accuracy_score(y_test, pre),2)*100))

# 也可以不训练,直接使用模型输出特征的重要程度
# model.fit(x,y)

# 使用该模型输出特征的重要性
feature_importances = (model.feature_importances_*100).round(2)

fi_df = pd.DataFrame(data=feature_importances,index=df.columns[:-1],columns=['Importance']).sort_values('Importance',ascending=False)

# 绘图
sns.set_palette(my_palette)
plt.rcParams['font.sans-serif']=['SimHei']
plt.figure(figsize=(8,5))
plt.title('Random Forest Feature Importances',pad=20)

sns.barplot(x='Importance',y=fi_df.index,data=fi_df,width=0.5)

for index,row in fi_df.iterrows():
    plt.text(row['Importance'],row.name,str(row['Importance'])+'%',ha='right',va='center')

plt.show()

本案例采用随机森林算法建模来预测特征的重要性,准确率为53%,可能需要考虑其他更合适的模型来预测。

由此图可知训练强度、体重和身高这些特征较为重要,与前面的分析结果比较符合。

总结

综合上述,可得出以下结论:

训练强度:前面分析体现训练强度和受伤可能性有微弱的正相关性。训练强度越大,受伤可能性也就随之提高。建议根据运动员自身各个条件包括年龄、体重、耐性等指定个性化训练表。

体重:前面各个体重组的分析,其中<50kg运动员受伤可能性比其他体重组要更大,说明瘦弱身材扛不住日常训练。需要运动员重视下日常饮食的摄入和增肌的锻炼量。

身高:...(要不留给你们来补充?)

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值