Files
NexQuant/rdagent/scenarios/kaggle/kaggle_crawler.py
T

433 lines
17 KiB
Python
Raw Normal View History

# %%
import bisect
import json
import os
import shutil
2024-09-20 20:49:44 +08:00
import subprocess
import tarfile
import time
2024-09-20 20:49:44 +08:00
import zipfile
from itertools import chain
from pathlib import Path
import nbformat
from rich import print
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from rdagent.core.conf import ExtendedBaseSettings
2024-11-04 14:57:10 +08:00
from rdagent.core.exception import KaggleError
from rdagent.core.utils import cache_with_pickle
from rdagent.log import rdagent_logger as logger
from rdagent.oai.llm_utils import APIBackend
from rdagent.scenarios.data_science.debug.data import create_debug_data
from rdagent.utils.agent.tpl import T
from rdagent.utils.env import MLEBDockerEnv
# %%
options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--headless")
def crawl_descriptions(
competition: str, local_data_path: str, wait: float = 3.0, force: bool = False
) -> dict[str, str] | str:
if (fp := Path(f"{local_data_path}/{competition}/description.md")).exists() and not force:
logger.info(f"Found {competition}/description.md, loading from it.")
return fp.read_text()
if (fp := Path(f"{local_data_path}/{competition}.json")).exists() and not force:
logger.info(f"Found {competition}.json, loading from local file.")
with fp.open("r") as f:
return json.load(f)
# Use webdriver-manager to automatically download and manage ChromeDriver version
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
overview_url = f"https://www.kaggle.com/competitions/{competition}/overview"
driver.get(overview_url)
time.sleep(wait)
site_body = driver.find_element(By.ID, "site-content")
descriptions = {}
# Get the subtitles
elements = site_body.find_elements(By.CSS_SELECTOR, f"a[href^='/competitions/{competition}/overview/']")
subtitles = []
for e in elements:
inner_text = ""
for child in e.find_elements(By.XPATH, ".//*"):
inner_text += child.get_attribute("innerHTML").strip()
subtitles.append(inner_text)
def kaggle_description_css_selectors() -> tuple[str, str]:
# Get the class name of the main contents
ab_elm = site_body.find_element(By.ID, "abstract")
others_elm = ab_elm.find_element(By.XPATH, "../*[2]")
first_elm = others_elm.find_element(By.XPATH, "./*[1]")
first_content_elm = first_elm.find_element(By.XPATH, "./*[1]/*[2]")
selector_elm = first_content_elm.find_element(By.XPATH, "./*[1]/*[1]")
main_class = selector_elm.get_attribute("class").split()[-1]
# Get the class name of the citation
citation_elm = site_body.find_element(By.ID, "citation")
citation_content_elm = citation_elm.find_element(By.XPATH, "./*[1]/*[2]/*[1]/*[1]")
citation_class = citation_content_elm.get_attribute("class").split()[-1]
return main_class, citation_class
main_class, citation_class = kaggle_description_css_selectors()
# Get main contents
contents = []
elements = site_body.find_elements(By.CSS_SELECTOR, f".{main_class}")
for e in elements:
content = e.get_attribute("innerHTML")
contents.append(content)
assert len(subtitles) == len(contents) + 1 and subtitles[-1] == "Citation"
for i in range(len(subtitles) - 1):
descriptions[subtitles[i]] = contents[i]
# Get the citation
element = site_body.find_element(By.CSS_SELECTOR, f".{citation_class}")
citation = element.get_attribute("innerHTML")
descriptions[subtitles[-1]] = citation
data_url = f"https://www.kaggle.com/competitions/{competition}/data"
driver.get(data_url)
time.sleep(wait)
data_element = driver.find_element(By.CSS_SELECTOR, f".{main_class}")
descriptions["Data Description"] = data_element.get_attribute("innerHTML")
driver.quit()
with open(f"{local_data_path}/{competition}.json", "w") as f:
json.dump(descriptions, f)
return descriptions
2025-06-18 14:35:45 +08:00
def download_data(competition: str, settings: ExtendedBaseSettings, enable_create_debug_data: bool = True) -> None:
local_path = settings.local_data_path
if settings.if_using_mle_data:
2024-11-05 15:41:50 +08:00
zipfile_path = f"{local_path}/zip_files"
2024-11-05 20:42:20 +08:00
zip_competition_path = Path(zipfile_path) / competition
competition_local_path = Path(local_path) / competition
if not zip_competition_path.exists():
2025-03-12 11:36:28 +08:00
mleb_env = MLEBDockerEnv()
mleb_env.prepare()
(Path(zipfile_path)).mkdir(parents=True, exist_ok=True)
mleb_env.check_output(
f"mlebench prepare -c {competition} --data-dir ./zip_files",
local_path=local_path,
running_extra_volume={str(Path("~/.kaggle").expanduser().absolute()): "/root/.kaggle"},
)
if not competition_local_path.exists() or list(competition_local_path.iterdir()) == []:
competition_local_path.mkdir(parents=True, exist_ok=True)
2025-03-12 11:36:28 +08:00
mleb_env = MLEBDockerEnv()
mleb_env.prepare()
mleb_env.check_output(
f"cp -r ./zip_files/{competition}/prepared/public/* ./{competition}", local_path=local_path
)
2025-01-24 13:34:47 +08:00
for zip_path in competition_local_path.rglob("*.zip"):
2025-01-24 13:34:47 +08:00
with zipfile.ZipFile(zip_path, "r") as zip_ref:
if len(zip_ref.namelist()) == 1:
mleb_env.check_output(
f"unzip -o ./{zip_path.relative_to(competition_local_path)} -d {zip_path.parent.relative_to(competition_local_path)}",
local_path=competition_local_path,
2025-01-24 13:34:47 +08:00
)
else:
mleb_env.check_output(
f"mkdir -p ./{zip_path.parent.relative_to(competition_local_path)}/{zip_path.stem}; unzip -o ./{zip_path.relative_to(competition_local_path)} -d ./{zip_path.parent.relative_to(competition_local_path)}/{zip_path.stem}",
local_path=competition_local_path,
)
for tar_path in competition_local_path.rglob("*.tar*"):
if not tarfile.is_tarfile(tar_path):
logger.error(f"{tar_path} is not a valid tar file.")
continue
is_gzip_file = open(tar_path, "rb").read(2) == b"\x1f\x8b"
with tarfile.open(tar_path, "r:gz") if is_gzip_file else tarfile.open(tar_path, "r") as tar_ref:
if len(tar_ref.getmembers()) == 1:
mleb_env.check_output(
f"tar -{'xzf' if is_gzip_file else 'xf'} ./{tar_path.relative_to(competition_local_path)} -C {tar_path.parent.relative_to(competition_local_path)}",
local_path=competition_local_path,
)
else:
folder_name = tar_path.name.replace(".tar", "").replace(".gz", "")
mleb_env.check_output(
f"mkdir -p ./{tar_path.parent.relative_to(competition_local_path)}/{folder_name}; tar -{'xzf' if is_gzip_file else 'xf'} ./{tar_path.relative_to(competition_local_path)} -C ./{tar_path.parent.relative_to(competition_local_path)}/{folder_name}",
local_path=competition_local_path,
2025-01-24 13:34:47 +08:00
)
# NOTE:
# Patching: due to mle has special renaming mechanism for different competition;
# We have to switch the schema back to a uniform one;
if competition in {"new-york-city-taxi-fare-prediction"}:
cpath = Path(local_path) / f"{competition}"
labels_path = cpath / "labels.csv"
train_path = cpath / "train.csv"
if labels_path.exists():
shutil.copy(labels_path, train_path)
else:
logger.error(f"labels.csv not found in {cpath}")
raise FileNotFoundError(f"{labels_path} does not exist")
2024-11-05 15:41:50 +08:00
else:
zipfile_path = f"{local_path}/zip_files"
2024-11-05 20:42:20 +08:00
if not Path(f"{zipfile_path}/{competition}.zip").exists():
2024-11-05 15:41:50 +08:00
try:
subprocess.run(
["kaggle", "competitions", "download", "-c", competition, "-p", zipfile_path],
check=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
except subprocess.CalledProcessError as e:
logger.error(f"Download failed: {e}, stderr: {e.stderr}, stdout: {e.stdout}")
raise KaggleError(f"Download failed: {e}, stderr: {e.stderr}, stdout: {e.stdout}")
2024-11-05 20:42:20 +08:00
# unzip data
unzip_path = f"{local_path}/{competition}"
if not Path(unzip_path).exists():
unzip_data(unzip_file_path=f"{zipfile_path}/{competition}.zip", unzip_target_path=unzip_path)
for sub_zip_file in Path(unzip_path).rglob("*.zip"):
unzip_data(sub_zip_file, unzip_target_path=unzip_path)
2024-11-04 14:57:10 +08:00
# sample data
if enable_create_debug_data and not Path(f"{local_path}/sample/{competition}").exists():
create_debug_data(competition, dataset_path=local_path)
2024-11-04 14:57:10 +08:00
def _safe_extract_zip(zip_ref: zipfile.ZipFile, path: str) -> None:
"""Extract zipfile safely, preventing path traversal attacks (CWE-22)."""
abs_path = os.path.realpath(path)
for member in zip_ref.infolist():
member_path = os.path.realpath(os.path.join(abs_path, member.filename))
if not member_path.startswith(abs_path):
raise ValueError(f"Attempted path traversal in zip file: {member.filename}")
zip_ref.extractall(path=path) # nosec B202:tarfile_unsafe_members - validated above
2024-11-04 14:57:10 +08:00
def unzip_data(unzip_file_path: str, unzip_target_path: str) -> None:
with zipfile.ZipFile(unzip_file_path, "r") as zip_ref:
_safe_extract_zip(zip_ref, unzip_target_path)
2024-09-20 20:49:44 +08:00
@cache_with_pickle(hash_func=lambda x: x, force=True)
2024-09-30 04:15:07 +08:00
def leaderboard_scores(competition: str) -> list[float]:
from kaggle.api.kaggle_api_extended import KaggleApi
api = KaggleApi()
api.authenticate()
2025-07-24 18:50:29 +08:00
return [i.score for i in api.competition_leaderboard_view(competition)]
2024-09-30 04:15:07 +08:00
def get_metric_direction(competition: str) -> bool:
"""
Return **True** if the metric is *bigger is better*, **False** if *smaller is better*.
"""
if competition == "aerial-cactus-identification":
return True
if competition == "leaf-classification":
return False
leaderboard = leaderboard_scores(competition)
return float(leaderboard[0]) > float(leaderboard[-1])
# FIXME: current score_rank is incorrect because kaggle api returns only the first page leaderboard
def score_rank(competition: str, score: float) -> tuple[int, float]:
"""
Return
------
rank: int
rank_percent: float
"""
scores = leaderboard_scores(competition)
if scores[0] < scores[-1]: # Ascending order
rank = bisect.bisect_right(scores, score)
else: # Descending order
scores = scores[::-1] # Reverse the list to use bisect
rank = len(scores) - bisect.bisect_right(scores, score)
rank = rank + 1
rank_percent = rank / len(scores) * 100
return rank, rank_percent
2025-06-18 14:35:45 +08:00
def download_notebooks(competition: str, local_path: str, num: int = 15) -> None:
data_path = Path(f"{local_path}/{competition}")
from kaggle.api.kaggle_api_extended import KaggleApi
api = KaggleApi()
api.authenticate()
# judge the sort_by
ll = api.competition_leaderboard_view(competition)
score_diff = float(ll[0].score) - float(ll[-1].score)
if score_diff > 0:
sort_by = "scoreDescending"
else:
sort_by = "scoreAscending"
# download notebooks
nl = api.kernels_list(competition=competition, sort_by=sort_by, page=1, page_size=num)
for nb in nl:
author = nb.ref.split("/")[0]
api.kernels_pull(nb.ref, path=data_path / author)
print(f"Downloaded {len(nl)} notebooks for {competition}. ([red]{sort_by}[/red])")
def notebook_to_knowledge(notebook_text: str) -> str:
sys_prompt = T(".prompts:gen_knowledge_from_code_mini_case.system").r()
user_prompt = T(".prompts:gen_knowledge_from_code_mini_case.user").r(notebook=notebook_text)
response = APIBackend().build_messages_and_create_chat_completion(
user_prompt=user_prompt,
system_prompt=sys_prompt,
json_mode=False,
)
return response
2025-06-18 14:35:45 +08:00
def convert_notebooks_to_text(competition: str, local_path: str) -> None:
data_path = Path(f"{local_path}/{competition}")
converted_num = 0
# convert ipynb and irnb files
for nb_path in chain(data_path.glob("**/*.ipynb"), data_path.glob("**/*.irnb")):
with nb_path.open("r", encoding="utf-8") as f:
nb = nbformat.read(f, as_version=4)
text = []
for cell in nb.cells:
if cell.cell_type == "markdown":
text.append(f"```markdown\n{cell.source}```")
elif cell.cell_type == "code":
text.append(f"```code\n{cell.source}```")
text = "\n\n".join(text)
text = notebook_to_knowledge(text)
text_path = nb_path.with_suffix(".txt")
text_path.write_text(text, encoding="utf-8")
converted_num += 1
# convert py files
for py_path in data_path.glob("**/*.py"):
with py_path.open("r", encoding="utf-8") as f:
text = f"```code\n{f.read()}```"
text = notebook_to_knowledge(text)
text_path = py_path.with_suffix(".txt")
text_path.write_text(text, encoding="utf-8")
converted_num += 1
print(f"Converted {converted_num} notebooks to text files.")
2025-06-18 14:35:45 +08:00
def collect_knowledge_texts(notebooks_path: str | Path) -> dict[str, list[str]]:
"""
{
"competition1": [
"knowledge_text1",
"knowledge_text2",
...
],
“competition2”: [
"knowledge_text1",
"knowledge_text2",
...
],
...
}
"""
2025-06-18 14:35:45 +08:00
notebooks_dir = Path(notebooks_path)
competition_knowledge_texts_dict = {}
for competition_dir in notebooks_dir.iterdir():
knowledge_texts = []
for text_path in competition_dir.glob("**/*.txt"):
text = text_path.read_text(encoding="utf-8")
knowledge_texts.append(text)
competition_knowledge_texts_dict[competition_dir.name] = knowledge_texts
return competition_knowledge_texts_dict
# %%
if __name__ == "__main__":
2024-09-29 15:51:51 +08:00
mini_case_cs = [
"feedback-prize-english-language-learning",
"playground-series-s3e11",
"playground-series-s3e14",
"spaceship-titanic",
"playground-series-s3e18",
"playground-series-s3e16",
"playground-series-s3e9",
"playground-series-s3e25",
"playground-series-s3e26",
"playground-series-s3e24",
"playground-series-s3e23",
]
other_cs = [
"amp-parkinsons-disease-progression-prediction",
"arc-prize-2024",
"ariel-data-challenge-2024",
"child-mind-institute-detect-sleep-states",
"connectx",
"contradictory-my-dear-watson",
"digit-recognizer",
"fathomnet-out-of-sample-detection",
"forest-cover-type-prediction",
"gan-getting-started",
"google-research-identify-contrails-reduce-global-warming",
"house-prices-advanced-regression-techniques",
"isic-2024-challenge",
"leash-BELKA",
"llm-20-questions",
"nlp-getting-started",
"playground-series-s4e1",
"playground-series-s4e2",
"playground-series-s4e3",
"playground-series-s4e4",
"playground-series-s4e5",
"playground-series-s4e6",
"playground-series-s4e7",
"playground-series-s4e8",
"rsna-2024-lumbar-spine-degenerative-classification",
"sf-crime",
"store-sales-time-series-forecasting",
"titanic",
"tpu-getting-started",
# scenario competition
"covid19-global-forecasting-week-1",
"statoil-iceberg-classifier-challenge",
"optiver-realized-volatility-prediction",
"facebook-v-predicting-check-ins",
]
2024-09-30 04:15:07 +08:00
# all_cs = mini_case_cs + other_cs
# for c in all_cs:
# convert_notebooks_to_text(c)
# exit()
# from kaggle.api.kaggle_api_extended import KaggleApi
2024-09-30 04:15:07 +08:00
# api = KaggleApi()
# api.authenticate()
# cs = api.competitions_list()
# for c in cs:
# name = c.ref.split("/")[-1]
# crawl_descriptions(name)
res = leaderboard_scores(competition="playground-series-s4e8")
rank, rank_percent = score_rank(competition="playground-series-s4e8", score=0.9832)
print(rank, rank_percent)
2024-09-30 04:15:07 +08:00
# %%