中文版题目翻译在这里先不放了,重点说一下我和队友讨论出来的一个简单思路。
data_dictionary.csv
, summerOly_athletes.csv
, summerOly_medal_counts.csv
, summerOly_hosts.csv
, summerOly_programs.csv
)。目标: 为每个国家建立奖牌预测模型,至少预测金牌和总奖牌数,并给出预测的置信区间,评估模型性能。
输入: (至少)一个字典,key 为运动员名称,value 为运动员的过往竞赛数据。
输出: 一个字典,key 为运动员名称,value 为预测的该运动员获得的奖牌数量(可以是单个数值或一个概率分布)。
核心: 研究运动员和奖牌之间的关系。
关键词: athletes
, medal counts
, gold medal
, quantification
, mathematical modeling
.
参考论文:
模型建议:
目标: 基于 Q1 模型,预测 2028 年洛杉矶奥运会的奖牌榜,包括预测区间,并分析哪些国家可能进步或退步。
输出:
核心: 应用已建立的奖牌预测模型进行预测。
目标: 模型需包括未获奖牌国家,预测多少国家在下届奥运会首次获奖,并给出概率。
输入: 一个字典,每个 key 为运动员,每个 value 为运动员的过往竞赛记录。
输出:
理解: 将模型准确率视为预测的可能性。
核心: 分析运动员参赛经历、奖牌记录与首次获奖之间的关系。
目标: 探索赛事类型(如游泳、举重)和国家奖牌数之间的关系,分析哪些体育项目对不同国家重要,以及主办国选择如何影响结果。
输入: 对于每个国家,赛事类型 (x)。
输出: 对于每个国家,奖牌数量 (y)。
方法:
核心: 揭示赛事类型对国家奖牌数的潜在影响。
当然可以,我们先从 Q1 的模型开始,一步一步来完成代码。请确保你已经安装了 pandas
和 scikit-learn
库。
Q1 完整解答:模型开发、不确定性评估和性能衡量
目标: 开发一个更完善的奖牌预测模型,并包含不确定性估计和性能评估。
改进方向:
代码实现:
步骤 1: 扩展特征工程 - 加入历史奖牌特征
我们在特征中加入过去几届奥运会的金牌和总奖牌数均值,以捕捉国家历史奖牌表现的趋势。
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
# 加载数据 (保持之前的数据加载和清洗代码不变)
athletes_df = pd.read_csv('summerOly_athletes.csv', encoding='utf-8')
medal_counts_df = pd.read_csv('summerOly_medal_counts.csv', encoding='utf-8')
medal_counts_df = medal_counts_df[~medal_counts_df['NOC'].isin(['Mixed team', 'ANZ'])]
medal_counts_df['Year'] = medal_counts_df['Year'].astype(int)
athletes_df['Year'] = athletes_df['Year'].astype(int)
# 特征工程 - 加入历史奖牌特征
def create_historical_features(df, years_ago_list=[4, 8, 12]): # 考虑过去 4, 8, 12 年的奥运会
df_features = df.copy()
for years_ago in years_ago_list:
df_shifted = df.copy()
df_shifted['Year'] = df_shifted['Year'] + years_ago # 年份向前平移
df_shifted = df_shifted[['Year', 'NOC', 'Gold', 'Total']] # 只保留需要的列
df_shifted.rename(columns={'Gold': f'Gold_Past_{years_ago}Y', 'Total': f'Total_Past_{years_ago}Y'}, inplace=True) # 重命名列名
df_features = pd.merge(df_features, df_shifted, on=['Year', 'NOC'], how='left') # 左连接合并
return df_features
medal_counts_featured_df = create_historical_features(medal_counts_df)
# 填充缺失值 (历史数据可能缺失,用 0 填充)
medal_counts_featured_df.fillna(0, inplace=True)
# 准备训练数据 (与之前的代码基本相同,但使用新的特征数据集)
features = ['Year', 'Gold_Past_4Y', 'Total_Past_4Y', 'Gold_Past_8Y', 'Total_Past_8Y', 'Gold_Past_12Y', 'Total_Past_12Y'] # 加入历史奖牌特征
target_gold = 'Gold'
target_total = 'Total'
data = medal_counts_featured_df[['Year', 'NOC', 'Gold', 'Total'] + features[1:]].copy() # 选择需要的列
data = pd.get_dummies(data, columns=['NOC']) # One-Hot 编码
train_data = data[data['Year'] < 2024].copy()
test_data = data[data['Year'] == 2024].copy()
X_train = train_data.drop([target_gold, target_total], axis=1)
y_gold_train = train_data[target_gold]
y_total_train = train_data[target_total]
X_test = test_data.drop([target_gold, target_total], axis=1)
y_gold_test = test_data[target_gold]
y_total_test = test_data[target_total]
print("\n特征工程后的训练特征 (X_train) 前几行:")
print(X_train.head())
代码解释 - 特征工程改进:
create_historical_features
函数:
df
) 和要考虑的过去奥运会年份列表 (years_ago_list
)。years_ago_list
中的每个年份 (例如 4, 8, 12 年)。df
) 复制一份 (df_shifted
)。df_shifted
的 ‘Year’ 列向前平移 years_ago
年,例如,如果 years_ago
是 4,则 1996 年的数据会变成 2000 年的 “过去 4 年数据”。df_shifted
的 ‘Gold’ 和 ‘Total’ 列,添加 _Past_{years_ago}Y
后缀,例如 Gold_Past_4Y
。df_features
和 df_shifted
进行左连接合并 (pd.merge
)。左连接保证所有原始数据行都保留,即使没有对应的历史数据(对于早期的奥运会)。df_features
)。调用 create_historical_features
:
medal_counts_featured_df = create_historical_features(medal_counts_df)
: 对原始 medal_counts_df
应用特征工程函数。填充缺失值:
medal_counts_featured_df.fillna(0, inplace=True)
: 由于左连接,对于早期奥运会年份,可能没有过去 4, 8, 12 年的数据,会产生缺失值 (NaN)。使用 0 填充这些缺失值,表示在奥运会历史早期,没有更早的奖牌数据。更新特征列表:
features = ['Year', 'Gold_Past_4Y', 'Total_Past_4Y', ..., 'Total_Past_12Y']
: 将新创建的历史奖牌特征添加到特征列表中。数据准备: 后续的数据准备代码与之前基本相同,但现在使用的是 medal_counts_featured_df
这个包含了更多特征的数据集。
步骤 2: 模型训练和评估
# 模型训练 - 随机森林回归 (代码与之前相同)
model_gold = RandomForestRegressor(random_state=42)
model_total = RandomForestRegressor(random_state=42)
model_gold.fit(X_train, y_gold_train)
model_total.fit(X_train, y_total_train)
# 模型预测 (代码与之前相同)
y_gold_pred = model_gold.predict(X_test)
y_total_pred = model_total.predict(X_test)
# 模型评估 (代码与之前相同)
mse_gold = mean_squared_error(y_gold_test, y_gold_pred)
r2_gold = r2_score(y_gold_test, y_gold_pred)
mse_total = mean_squared_error(y_total_test, y_total_pred)
r2_total = r2_score(y_total_test, y_total_pred)
print("\n金牌数模型评估 (更新特征后):")
print(f"均方误差 (MSE): {mse_gold:.2f}")
print(f"R 平方值 (R^2): {r2_gold:.2f}")
print("\n总奖牌数模型评估 (更新特征后):")
print(f"均方误差 (MSE): {mse_total:.2f}")
print(f"R 平方值 (R^2): {r2_total:.2f}")
步骤 3: 预测不确定性 - Bootstrap 方法
我们使用 Bootstrap 方法来估计预测结果的不确定性,并计算 95% 的预测区间。
def bootstrap_predict(model, X_test, n_bootstrap=1000): # Bootstrap 预测函数
predictions = []
for _ in range(n_bootstrap):
# 从训练数据集中有放回地抽样
indices = np.random.choice(len(X_train), len(X_train), replace=True)
X_sample = X_train.iloc[indices]
y_sample_gold = y_gold_train.iloc[indices]
y_sample_total = y_total_train.iloc[indices]
# 训练模型
model_sample_gold = RandomForestRegressor(random_state=42) # 每次 bootstrap 训练一个新的模型
model_sample_total = RandomForestRegressor(random_state=42)
model_sample_gold.fit(X_sample, y_sample_gold)
model_sample_total.fit(X_sample, y_sample_total)
# 预测测试集
y_pred_gold_bootstrap = model_sample_gold.predict(X_test)
y_pred_total_bootstrap = model_sample_total.predict(X_test)
predictions.append(np.column_stack((y_pred_gold_bootstrap, y_pred_total_bootstrap))) # 保存每次 bootstrap 的预测结果
predictions = np.concatenate(predictions, axis=0) # 将所有 bootstrap 的预测结果合并
return predictions
bootstrap_preds = bootstrap_predict(RandomForestRegressor(random_state=42), X_test) # 调用 bootstrap 预测函数
# 计算预测区间 (95% 置信区间)
lower_percentile = 2.5
upper_percentile = 97.5
y_gold_pred_lower = np.percentile(bootstrap_preds[:, 0], lower_percentile, axis=0) # 金牌数预测区间下界
y_gold_pred_upper = np.percentile(bootstrap_preds[:, 0], upper_percentile, axis=0) # 金牌数预测区间上界
y_total_pred_lower = np.percentile(bootstrap_preds[:, 1], lower_percentile, axis=0) # 总奖牌数预测区间下界
y_total_pred_upper = np.percentile(bootstrap_preds[:, 1], upper_percentile, axis=0) # 总奖牌数预测区间上界
# 查看带预测区间的结果
predictions_df_with_interval = pd.DataFrame({
'Actual_Gold': y_gold_test,
'Predicted_Gold': y_gold_pred.round(0),
'Gold_Lower_95CI': y_gold_pred_lower.round(0),
'Gold_Upper_95CI': y_gold_pred_upper.round(0),
'Actual_Total': y_total_test,
'Predicted_Total': y_total_pred.round(0),
'Total_Lower_95CI': y_total_pred_lower.round(0),
'Total_Upper_95CI': y_total_pred_upper.round(0)
}, index = test_data.filter(like='NOC_').columns.str.replace('NOC_', '')) # 使用 One-Hot 编码后的 NOC 列名作为索引,并去除前缀
print("\n2024 年奖牌预测结果 (部分国家) - 带 95% 预测区间:")
print(predictions_df_with_interval.head(10))
代码解释 - Bootstrap 预测不确定性:
bootstrap_predict
函数:
model
),测试集特征 (X_test
),Bootstrap 迭代次数 (n_bootstrap
)。n_bootstrap
次 (例如 1000 次)。model_sample_gold
, model_sample_total
)。X_test
。predictions
),形状为 (n_bootstrap * len(X_test), 2)
,其中每一行是每次 Bootstrap 迭代对所有测试样本的金牌数和总奖牌数的预测结果。调用 bootstrap_predict
:
bootstrap_preds = bootstrap_predict(...)
: 调用 bootstrap_predict
函数,得到 Bootstrap 预测结果。计算预测区间:
lower_percentile = 2.5
, upper_percentile = 97.5
: 设置置信区间的上下界百分位数 (95% 置信区间)。y_gold_pred_lower = np.percentile(...)
, y_gold_pred_upper = np.percentile(...)
: 使用 np.percentile
函数,分别计算金牌数和总奖牌数在所有 Bootstrap 预测结果中的下界 (2.5% 分位数) 和上界 (97.5% 分位数)。创建带预测区间的 DataFrame:
predictions_df_with_interval = pd.DataFrame(...)
: 创建一个新的 DataFrame,除了实际值和点预测值外,还包括金牌数和总奖牌数的 95% 预测区间的上下界。index = test_data.filter(like='NOC_').columns.str.replace('NOC_', '')
: 使用 One-Hot 编码后的 NOC 列名作为索引,并去除 ‘NOC_’ 前缀,使索引更清晰。打印带预测区间的预测结果: 打印包含预测区间的新 DataFrame 的前几行。
运行完整代码:
现在运行完整的代码,你将得到:
解答 Q1:
至此,我们已经完成了解答 Q1 的主要部分:
请尝试运行这段代码,并根据输出结果进行分析和调整。 接下来我们会继续更新 Q1.1, Q1.2 等问题的代码实现。
目标: 分析教练对国家奖牌数的影响,量化这种效应,并为三个国家挑选合适的运动项目引入“优秀教练”。
问题:
建模思路 (推荐第二种):
核心: 通过模型误差分析间接推断教练的作用。
目标: 挖掘关于奥运奖牌数的其他原创见解,并解释如何帮助国家奥委会。
思路:
核心: 从数据中挖掘除了直接预测以外的其他有用信息。