Python数据分析实战:用Pandas处理2012-2019年运动员收入排行榜(附完整代码)
从数据清洗到商业洞察:用Pandas深度解析全球顶级运动员收入变迁
最近几年,数据驱动的决策方式几乎渗透到了每个行业,体育产业也不例外。无论是球队经理评估球员价值,还是品牌方寻找代言人,抑或是我们这些数据爱好者探索商业规律,一份详实的数据集总能带来意想不到的发现。手头恰好有一份2012年至2019年间全球运动员的收入排行榜,这不仅仅是一串数字,更像是一扇观察顶级体育商业世界的窗口。这份数据里藏着哪些故事?哪些运动项目是“金矿”?运动员的收入结构又发生了怎样的演变?今天,我们就抛开那些复杂的理论,直接用Python的Pandas库,像侦探一样,一步步挖掘数据背后的真相。整个过程,我会把每一步的代码和思考都摊开来讲,无论你是刚接触数据分析的新手,还是想看看Pandas在真实场景中如何施展拳脚的老朋友,相信都能有所收获。
1. 数据初探与清洗:打好分析的地基
拿到一份原始数据,尤其是从网络或公开渠道获取的CSV文件,直接进行分析往往是灾难的开始。数据中可能隐藏着缺失值、格式不一致、多余字符等问题。我们的第一步,不是急着算平均值、画图表,而是静下心来,像考古学家清理文物一样,先把数据“清洗”干净。
1.1 读取数据与首次“体检”
使用Pandas读取CSV文件非常简单,一行pd.read_csv就能搞定。但关键在于读取之后的检查。我们需要了解数据的整体面貌:有多少行、多少列、每一列是什么类型、有没有明显的异常。
import pandas as pd
# 读取数据,注意原始数据可能包含特殊字符或表头格式
df = pd.read_csv('athlete_earnings_2012_2019.csv', encoding='utf-8')
print("数据形状(行数, 列数):", df.shape)
print("\n前5行数据预览:")
print(df.head())
print("\n数据基本信息:")
print(df.info())
print("\n查看各列的唯一值数量:")
print(df.nunique())
运行这几行代码,你可能会立刻发现一些问题。比如,列名可能带有空格或特殊符号,Rank列前面可能有个不起眼的#号,收入金额列(如Pay、Salary)可能是带有$和M(代表百万)的字符串,这根本无法进行数值计算。这就是我们清洗的起点。
1.2 关键清洗步骤:让数据“规整”起来
清洗工作通常围绕几个核心目标:统一格式、处理缺失、纠正错误、转换类型。针对这份运动员收入数据,我们需要进行以下操作:
- 列名标准化:去除列名中的空格、特殊字符,并改为更易处理的英文小写格式。
- 处理‘Rank’列:移除可能存在的
#前缀,并转换为整数类型。 - 货币字段转换:这是核心步骤。需要将
$127 M这样的字符串转换为纯浮点数127.0。 - 处理年份和运动项目:确保
Year列为整数,Sport列大小写统一。 - 检查并处理重复或缺失值。
下面是一个将货币字符串转换为数值的实用函数,也是数据清洗中的一个小技巧:
def currency_to_float(currency_series):
"""
将形如 '$127 M' 或 '$1.5 M' 的字符串转换为浮点数。
"""
# 移除美元符号$和字母M,并去除空格
return currency_series.str.replace('$', '', regex=False)\
.str.replace(' M', '', regex=False)\
.astype(float)
# 应用转换
df['pay_numeric'] = currency_to_float(df['Pay'])
df['salary_numeric'] = currency_to_float(df['Salary/Winnings'])
df['endorsement_numeric'] = currency_to_float(df['Endorsements'])
# 验证转换结果
print(df[['Pay', 'pay_numeric']].head())
注意:在真实数据中,货币单位可能不统一(例如,可能有
K代表千)。上述函数假设所有数据单位都是M(百万)。如果存在混合单位,需要更复杂的逻辑进行识别和标准化。
清洗完成后,我们得到的是一个干净、规整的DataFrame,所有数值列都可以直接用于数学运算和统计分析。这是所有后续高级分析的基础,千万不能省略或敷衍。
2. 核心分析:回答具体的业务问题
数据清洗完毕,我们就可以开始“提问”了。原始需求中提到了几个具体的查询功能,这正好对应了数据分析中常见的场景:筛选、排序、分组聚合。我们用Pandas来逐一实现,并理解其背后的逻辑。
2.1 查询某年收入最高的K位运动员
这个需求本质上是按条件筛选 + 排序 + 取前N项。Pandas的链式调用让这个操作非常优雅。
def get_top_k_athletes(year, k, df_clean):
"""
获取指定年份收入排名前K的运动员信息。
参数:
year (int): 目标年份,如2019
k (int): 需要返回的排名数量
df_clean (DataFrame): 清洗后的数据框
返回:
DataFrame: 包含前K名运动员详细信息的DataFrame
"""
# 1. 筛选指定年份的数据
year_data = df_clean[df_clean['year'] == year].copy()
# 2. 按收入(pay_numeric)降序排序
year_data_sorted = year_data.sort_values(by='pay_numeric', ascending=False)
# 3. 取前K行,如果K大于总人数,则取全部
top_k_data = year_data_sorted.head(k) if k <= len(year_data_sorted) else year_data_sorted
# 4. 格式化输出(可选,根据需求调整列)
# 重置索引,让排名从1开始显示
top_k_data_reset = top_k_data.reset_index(drop=True)
top_k_data_reset.index = top_k_data_reset.index + 1
# 选择需要输出的列
output_columns = ['rank', 'name', 'pay', 'salary/winnings', 'endorsements', 'sport', 'year']
# 注意:这里输出的是原始字符串列,以保持$和M的格式
return top_k_data_reset[output_columns]
# 示例:获取2019年收入前5的运动员
top_5_2019 = get_top_k_athletes(2019, 5, df)
print(top_5_2019.to_string(index=True, header=True))
这个函数清晰地展示了数据分析的流程化思维。你完全可以修改它,比如按endorsement_numeric(代言收入)排序,来找出“广告天王”。
2.2 按运动项目进行深度钻取
第二个需求更进了一步:先列出某年所有运动项目菜单,再让用户选择一项,查看该运动所有运动员的详情并计算总收入。这涉及到分组(GroupBy) 和数据透视的概念。
def analyze_sport_by_year(year, df_clean):
"""
分析指定年份的运动项目分布,并允许后续钻取。
参数:
year (int): 目标年份
df_clean (DataFrame): 清洗后的数据框
返回:
tuple: (运动项目菜单字典, 筛选后的年份数据框)
"""
# 筛选年份
year_data = df_clean[df_clean['year'] == year].copy()
# 获取该年份所有不重复的运动项目,并按字母顺序排序
sports_list = sorted(year_data['sport'].unique())
# 生成带编号的菜单字典,方便用户选择
sports_menu = {idx+1: sport for idx, sport in enumerate(sports_list)}
print(f"{year}年运动项目菜单:")
for num, sport in sports_menu.items():
print(f"{num}: {sport}")
return sports_menu, year_data
def get_sport_details(year_data, sport_name):
"""
从年份数据中获取指定运动项目的所有运动员数据,并计算总收入。
参数:
year_data (DataFrame): 已经筛选好的某年数据
sport_name (str): 运动项目名称
返回:
tuple: (该运动运动员数据DataFrame, 总收入)
"""
# 筛选特定运动
sport_data = year_data[year_data['sport'] == sport_name].copy()
# 按收入排序
sport_data_sorted = sport_data.sort_values(by='pay_numeric', ascending=False)
sport_data_sorted = sport_data_sorted.reset_index(drop=True)
sport_data_sorted.index = sport_data_sorted.index + 1
# 计算该运动总收入(使用清洗后的数值列)
total_income = sport_data_sorted['pay_numeric'].sum()
return sport_data_sorted, total_income
# 使用示例
menu, year_df = analyze_sport_by_year(2019, df)
# 假设用户通过菜单选择了‘Soccer’(对应编号9)
selected_sport = menu[9]
soccer_players, soccer_total = get_sport_details(year_df, selected_sport)
print(f"\n{selected_sport}项目运动员详情:")
# 输出格式化详情,这里可以自定义输出格式,例如用to_string控制
print(soccer_players[['name', 'pay', 'salary/winnings', 'endorsements']].to_string())
print(f"\n{selected_sport}项目{2019}年总收入: ${soccer_total:.2f} M")
这里的关键是groupby思想的灵活运用。我们虽然没有直接使用df.groupby(‘sport’),但通过先筛选年份再筛选运动项目,实现了同样的分组效果,并且更利于交互式操作。
3. 超越基础:多维度探索与可视化
完成了基本查询,我们可以玩点更花的。数据分析的魅力在于发现那些一眼看不出的模式和趋势。Pandas结合Matplotlib或Seaborn库,能让这些发现直观地呈现出来。
3.1 趋势分析:哪些运动在崛起?
我们不再只看单一年份,而是把时间维度拉长,看看从2012到2019这八年里,不同运动项目的总收入变化趋势。这需要用到按年份和运动项目分组聚合。
# 计算每年每个运动项目的总收入
year_sport_income = df_clean.groupby(['year', 'sport'])['pay_numeric'].sum().reset_index()
year_sport_income.rename(columns={'pay_numeric': 'total_pay'}, inplace=True)
# 透视表:让数据更适合绘制折线图
pivot_table = year_sport_income.pivot(index='year', columns='sport', values='total_pay')
print("各运动项目年度总收入透视表:")
print(pivot_table.head())
# 选出总收入常年较高的几个项目进行趋势观察
top_sports_overall = df_clean.groupby('sport')['pay_numeric'].sum().nlargest(5).index.tolist()
print(f"\n历史总收入最高的5个运动项目是: {top_sports_overall}")
有了这个透视表,我们可以轻松地用折线图画出篮球、足球、高尔夫等项目的收入变化曲线,一眼就能看出谁是“常青树”,谁的增长势头最猛。
3.2 收入结构剖析:工资 vs. 代言
顶级运动员的收入构成大有学问。拳击手可能主要靠比赛出场费(工资),而网球巨星则可能代言收入远超比赛奖金。我们可以计算每个运动员的代言收入占比,并分析不同运动项目的差异。
# 计算代言收入占比
df_clean['endorsement_ratio'] = df_clean['endorsement_numeric'] / df_clean['pay_numeric']
# 分析各运动项目的平均代言收入占比
sport_endorsement = df_clean.groupby('sport')['endorsement_ratio'].mean().sort_values(ascending=False)
print("各运动项目平均代言收入占比(从高到低):")
print(sport_endorsement.head(10))
# 我们可以用柱状图来可视化这个结果
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
sport_endorsement.head(10).plot(kind='barh') # 水平柱状图更易阅读
plt.xlabel('平均代言收入占比')
plt.title('各运动项目对代言收入的依赖程度(Top 10)')
plt.tight_layout()
plt.show()
这个分析对于品牌营销人员极具价值。如果他们想找一位代言人其收入主要来自粉丝和市场的认可(即高代言占比),那么网球、高尔夫等项目中的顶级选手可能是比某些高工资的团队项目球员更好的选择。
3.3 发现异常值与有趣个体
数据中总有一些“特例”,它们可能指向错误,也可能隐藏着最有趣的故事。我们可以利用描述性统计和筛选来发现它们。
# 查看收入(pay_numeric)的基本统计信息
print(df_clean['pay_numeric'].describe())
# 找出历史上收入最高的运动员(所有年份中)
highest_earning_athlete = df_clean.loc[df_clean['pay_numeric'].idxmax()]
print(f"\n历史单年收入最高的运动员:")
print(highest_earning_athlete[['name', 'year', 'pay', 'sport']])
# 找出代言收入高于工资收入的运动员(“吸金王”)
endorsement_kings = df_clean[df_clean['endorsement_numeric'] > df_clean['salary_numeric']]
print(f"\n代言收入高于工资收入的运动员共有 {len(endorsement_kings)} 位。")
print("其中代言占比最高的几位:")
print(endorsement_kings.nlargest(5, 'endorsement_ratio')[['name', 'year', 'endorsement_ratio', 'sport']])
你可能会发现,像罗杰·费德勒这样的运动员,其代言收入占比常年高得惊人,这反映了其超越运动本身的巨大个人品牌价值。
4. 构建可复用的分析管道与输出
一次性的分析脚本价值有限。更好的做法是将整个流程封装成模块化的函数或类,形成一个数据分析管道。这样,当有新数据(比如2020-2023年的数据)到来时,我们可以快速复用整个分析流程。
4.1 设计一个简单的分析类
将数据加载、清洗、核心查询方法封装在一个类里,使代码更整洁、更易维护和扩展。
class AthleteEarningsAnalyzer:
"""运动员收入数据分析器"""
def __init__(self, filepath):
self.filepath = filepath
self.df_raw = None
self.df_clean = None
self._load_and_clean()
def _load_and_clean(self):
"""私有方法:执行数据加载和清洗的所有步骤"""
# 加载原始数据
self.df_raw = pd.read_csv(self.filepath, encoding='utf-8')
df = self.df_raw.copy()
# 示例清洗步骤(需根据实际数据调整)
# 1. 标准化列名
df.columns = df.columns.str.strip().str.lower().str.replace(' ', '_').str.replace('/', '_')
# 2. 清洗rank列
if 'rank' in df.columns:
df['rank'] = df['rank'].astype(str).str.replace('#', '', regex=False).astype(int)
# 3. 转换货币列
currency_cols = ['pay', 'salary_winnings', 'endorsements']
for col in currency_cols:
if col in df.columns:
new_col_name = col + '_numeric'
df[new_col_name] = df[col].apply(self._parse_currency)
# 4. 确保年份为整数
if 'year' in df.columns:
df['year'] = df['year'].astype(int)
self.df_clean = df
print(f"数据清洗完成。原始形状: {self.df_raw.shape}, 清洗后形状: {self.df_clean.shape}")
@staticmethod
def _parse_currency(val):
"""静态方法:解析货币字符串"""
if isinstance(val, str):
# 移除 $ 和 M,并转换为浮点数
try:
return float(val.replace('$', '').replace(' M', '').strip())
except ValueError:
return None
return val
# 可以将前面章节的 get_top_k_athletes, analyze_sport_by_year 等方法定义为这个类的方法
def get_top_k(self, year, k):
# ... 实现代码 ...
pass
def generate_sport_report(self, year, sport):
# ... 实现代码 ...
pass
# 使用示例
analyzer = AthleteEarningsAnalyzer('athlete_earnings_2012_2019.csv')
top_10_2018 = analyzer.get_top_k(2018, 10)
4.2 生成结构化的分析报告
最后,我们可以将关键发现输出成一份更友好的报告,比如一个简单的Markdown文件或HTML片段。
def generate_analysis_summary(analyzer, output_file='analysis_summary.md'):
"""生成一份简单的分析总结报告"""
df = analyzer.df_clean
with open(output_file, 'w', encoding='utf-8') as f:
f.write("# 全球运动员收入数据分析报告 (2012-2019)\n\n")
f.write("## 关键摘要\n")
f.write(f"- 数据覆盖年份: {df['year'].min()} 至 {df['year'].max()}\n")
f.write(f"- 总记录数: {len(df)} 条\n")
f.write(f"- 涉及运动项目: {df['sport'].nunique()} 种\n")
f.write(f"- 历史最高单年收入: ${df['pay_numeric'].max():.2f} M\n")
f.write(f"- 平均年收入: ${df['pay_numeric'].mean():.2f} M\n\n")
f.write("## 收入最高的运动项目(按历史总收入)\n")
top_sports = df.groupby('sport')['pay_numeric'].sum().nlargest(5)
for sport, income in top_sports.items():
f.write(f"- **{sport}**: ${income:.2f} M\n")
f.write("\n## 代言依赖度最高的运动项目\n")
top_endorsement_sports = df.groupby('sport')['endorsement_ratio'].mean().nlargest(3)
for sport, ratio in top_endorsement_sports.items():
f.write(f"- **{sport}**: {ratio:.1%} 的收入来自代言\n")
print(f"分析报告已生成至: {output_file}")
# 执行报告生成
generate_analysis_summary(analyzer)
走到这一步,你已经不仅仅是在完成一个数据处理任务,而是在构建一个从原始数据到商业洞察的完整工作流。这份报告里的每一个数字,都是从一行行代码和一次次思考中提炼出来的,它比任何泛泛而谈的评论都更有力量。
整个流程下来,最大的感触是,数据分析项目中,清洗和探索性分析所花的时间,往往比最终的建模或复杂计算要多得多,但这也是价值密度最高的部分。Pandas提供的各种灵活方法,本质上是在降低我们做这些“脏活累活”和“思维体操”的门槛。下次当你面对一堆杂乱的数据时,别急着下结论,先像我们今天这样,用df.head()看看,用df.info()摸摸底,再用groupby和plot问几个问题,故事自己就会浮现出来。剩下的,就是用代码把它清晰地讲述出来。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)