Pandas追加DataFrame引发for loop异常,寻求技术帮助
哈哈,这个问题我之前踩过一模一样的坑!核心原因是变量作用域的陷阱——你在apply_test函数里修改全局的game_df时,没有明确声明它是全局变量,导致Python在函数内部创建了一个同名的局部变量,这直接干扰了你的循环逻辑。
为什么会出现循环变量重复的怪异现象?
当你执行game_df = game_df.append(...)这行代码时,Python会默认把game_df当作函数内的局部变量处理。但你在赋值前先引用了game_df(也就是game_df.append这部分),这会触发隐式的变量绑定,而这种绑定会干扰函数内循环变量yr的解析逻辑——这就是为什么注释掉这行后,循环立刻恢复正常的原因:没有了局部变量的干扰,Python能正确解析循环的迭代值。
解决方案1:明确声明全局变量(快速修复)
在apply_test函数的开头加上global game_df,告诉Python你要操作的是外部的全局DataFrame,而不是创建局部变量:
def apply_test(row): global game_df # 关键!必须声明这是全局变量 url = row['url'] for yr in range(int(row['startYear']),int(row['endYear'])+1): print(yr) content = requests.get(url.split(".htm")[0]+"/gamelog/"+str(yr)).content soup = bs(content,'html.parser').find("div",{"id":"all_stats"}) # ... 后面的表头处理代码不变 ... for row in soup.find("tbody").find_all("tr"): temp_row = {} for i,col in enumerate(row.find_all("td")): temp_row[all_headers[i]] = col.text game_df = game_df.append(temp_row,ignore_index=True)
修改后,循环的yr就会正常输出2013和2014,game_df也能正确追加数据。
解决方案2:无全局变量的优雅写法(推荐)
使用全局变量很容易引发各种作用域问题,更符合Pandas最佳实践的方式是让apply_test返回每个玩家的比赛数据,最后用pd.concat批量合并所有结果:
import pandas as pd from bs4 import BeautifulSoup as bs import requests import string import numpy as np # 获取球员列表的代码保持不变 players = pd.DataFrame({"name":[],"url":[],"positions":[],"startYear":[],"endYear":[]}) letters = list(string.ascii_uppercase) for letter in letters: print(letter) players_html = requests.get("https://www.pro-football-reference.com/players/"+letter+"/") soup = bs(players_html.content,"html.parser") for player in soup.find("div",{"id":"div_players"}).find_all("p"): temp_row = {} temp_row["url"] = "https://www.pro-football-reference.com"+player.find("a")["href"] temp_row["name"] = player.text.split("(")[0].strip() years = player.text.split(")")[1].strip() temp_row["startYear"] = int(years.split("-")[0]) temp_row["endYear"] = int(years.split("-")[1]) temp_row["positions"] = player.text.split("(")[1].split(")")[0] players = players.append(temp_row,ignore_index=True) players = players[players.endYear > 2000] players.reset_index(inplace=True,drop=True) def apply_test(row): url = row['url'] player_game_data = [] # 用列表收集当前玩家的所有比赛数据 for yr in range(int(row['startYear']),int(row['endYear'])+1): print(yr) content = requests.get(url.split(".htm")[0]+"/gamelog/"+str(yr)).content soup = bs(content,'html.parser').find("div",{"id":"all_stats"}) # 处理表头逻辑不变 over_headers = [] for over in soup.find("thead").find("tr").find_all("th"): if("colspan" in over.attrs.keys()): over_headers.extend([over.text]*int(over['colspan'])) else: over_headers.append(over.text) headers = [header.text for header in soup.find("thead").find_all("tr")[1].find_all("th")] all_headers = [f"{a}___"+b for a,b in zip(over_headers,headers)][1:] # 移除无意义的第一列 # 收集单场比赛数据 for game_row in soup.find("tbody").find_all("tr"): temp_row = {} for i,col in enumerate(game_row.find_all("td")): temp_row[all_headers[i]] = col.text # 额外添加球员名和年份,方便后续分析 temp_row['player_name'] = row['name'] temp_row['year'] = yr player_game_data.append(temp_row) # 返回当前玩家的比赛数据DataFrame return pd.DataFrame(player_game_data) # 合并所有玩家的比赛数据 all_game_data = pd.concat(players.apply(apply_test, axis=1).tolist(), ignore_index=True)
这个方案的优势很明显:
- 彻底避免了全局变量的作用域问题
- 代码逻辑更清晰,每个函数只负责单一职责
- 效率更高(
append是逐行添加,而列表收集+concat是批量合并,速度提升明显) - 额外添加的
player_name和year字段,能让后续的统计分析更方便
额外提醒:Pandas的append已弃用
从Pandas 2.0版本开始,DataFrame.append()已经被正式弃用,官方推荐用pd.concat()替代。所以不管你选择哪种方案,都尽量避免使用append,改用列表收集数据后再合并,既符合官方规范,也能避免潜在的bug。
内容的提问来源于stack exchange,提问作者zachvac
相关产品推荐
相关产品推荐

