Spaces:
Sleeping
Sleeping
| import csv | |
| import gzip | |
| import pickle | |
| import hashlib | |
| import os | |
| import shutil | |
| import uuid | |
| import multiprocessing as mp | |
| from tqdm import tqdm | |
| import numpy as np | |
| import lmdb | |
| import requests | |
| from src.utils import utils | |
| ctx = mp.get_context("spawn") # safer with LMDB/pickle | |
| Process = ctx.Process | |
| Queue = ctx.Queue | |
| def _is_test(draft_id: str, pct=10): | |
| h = int.from_bytes(hashlib.sha1(draft_id.encode()).digest()[:4], "little") | |
| return h % 100 < pct | |
| def lmdb_writer(db_path, q: Queue, commit_every=200_000, map_size=int(16e9)): | |
| env = lmdb.open(db_path, map_size=map_size, subdir=True, lock=True, | |
| sync=False, metasync=False, readahead=False, writemap=False) # writemap=False to avoid big prealloc on some FS | |
| txn, n = env.begin(write=True), 0 | |
| while True: | |
| item = q.get() | |
| if item is None: | |
| break | |
| key, value = item | |
| try: | |
| txn.put(key, value) | |
| except lmdb.MapFullError: | |
| txn.abort() | |
| env.set_mapsize(env.info()["map_size"] * 2) | |
| txn = env.begin(write=True) | |
| txn.put(key, value) | |
| n += 1 | |
| if n % commit_every == 0: | |
| txn.commit(); txn = env.begin(write=True) | |
| txn.commit() | |
| with env.begin(write=True) as t2: | |
| t2.put(b"__len__", str(n).encode()) | |
| env.sync(); env.close() | |
| def get_name(obstructed_name): | |
| if 'pack_card_' in obstructed_name: | |
| return obstructed_name.replace('pack_card_','') | |
| elif 'pool_' in obstructed_name: | |
| return obstructed_name.replace('pool_','') | |
| else: | |
| print(obstructed_name) | |
| raise ValueError('Invalid name') | |
| def encode_sample(sample): | |
| return pickle.dumps(sample, protocol=pickle.HIGHEST_PROTOCOL) | |
| def stream_csv(csv_path, train_q, test_q): | |
| with open(csv_path, "r", newline="", buffering=16*1024*1024) as f: | |
| reader = csv.reader(f) | |
| header = next(reader) | |
| pack_idx = [i for i,c in enumerate(header) if c.startswith("pack_card_")] | |
| pool_idx = [i for i,c in enumerate(header) if c.startswith("pool_")] | |
| pick_idx = header.index("pick") | |
| wins_idx = header.index("event_match_wins") | |
| loss_idx = header.index("event_match_losses") | |
| try: | |
| user_games = header.index("user_n_games_bucket") | |
| except: | |
| user_games = header.index("user_n_matches_bucket") | |
| try: | |
| user_wr = header.index("user_game_win_rate_bucket") | |
| except: | |
| try: | |
| user_wr = header.index("user_match_win_rate_bucket") | |
| except: | |
| user_wr = None | |
| draft_idx = 2 # your draft id column | |
| pack_names = {i: header[i].replace("pack_card_","") for i in pack_idx} | |
| pool_names = {i: header[i].replace("pool_","") for i in pool_idx} | |
| for row in tqdm(reader): | |
| try: | |
| draft_id = row[draft_idx] | |
| positive = row[pick_idx] | |
| except: | |
| print(f"Skipping row with missing draft_id or pick: {row}") | |
| continue | |
| negatives = [] | |
| for i in pack_idx: | |
| s = row[i] | |
| if s and s != "0": | |
| cnt = int(s) | |
| name = pack_names[i] | |
| if name != positive: | |
| negatives.extend([name]*cnt) | |
| anchor = [] | |
| for i in pool_idx: | |
| s = row[i] | |
| if s and s != "0": | |
| anchor.extend([pool_names[i]]*int(s)) | |
| wins = int(row[wins_idx]) | |
| losses = int(row[loss_idx]) | |
| u_g = int(row[user_games]) | |
| u_wr = float(row[user_wr]) if user_wr and row[user_wr] else 0.0 | |
| payload = encode_sample((positive, negatives, anchor, wins, losses, u_g, u_wr)) | |
| key = os.urandom(16) | |
| (test_q if _is_test(draft_id) else train_q).put((key, payload)) | |
| _17LANDS_URL = ( | |
| "https://17lands-public.s3.amazonaws.com/analysis_data/draft_data/" | |
| "draft_data_public.{set_tag}.{draft_format}.csv.gz" | |
| ) | |
| def download_17lands(set_tag, raw_data_folder, draft_format="PremierDraft"): | |
| url = _17LANDS_URL.format(set_tag=set_tag, draft_format=draft_format) | |
| out_dir = os.path.join(raw_data_folder, set_tag) | |
| os.makedirs(out_dir, exist_ok=True) | |
| csv_path = os.path.join(out_dir, f"{set_tag}_{draft_format}.csv") | |
| if os.path.exists(csv_path): | |
| print(f"{csv_path} already exists, skipping download") | |
| return | |
| print(f"Downloading {url}") | |
| response = requests.get(url, stream=True) | |
| if response.status_code == 404: | |
| raise FileNotFoundError( | |
| f"17lands data not found for {set_tag}/{draft_format}.\n" | |
| f"URL tried: {url}\n" | |
| f"Check https://17lands.com/public/data for the correct set code and format." | |
| ) | |
| response.raise_for_status() | |
| gz_path = csv_path + ".gz" | |
| total = int(response.headers.get("content-length", 0)) | |
| with open(gz_path, "wb") as f, tqdm(total=total, unit="B", unit_scale=True, | |
| desc=f"Downloading {set_tag}") as bar: | |
| for chunk in response.iter_content(chunk_size=1 << 17): | |
| f.write(chunk) | |
| bar.update(len(chunk)) | |
| print(f"Decompressing {gz_path}") | |
| with gzip.open(gz_path, "rb") as f_in, open(csv_path, "wb") as f_out: | |
| shutil.copyfileobj(f_in, f_out) | |
| os.remove(gz_path) | |
| print(f"Saved to {csv_path}") | |
| def stream_csv_trajectories(csv_path, train_q, test_q): | |
| """Buffer all picks by draft_id, then write one trajectory per draft.""" | |
| drafts = {} | |
| with open(csv_path, "r", newline="", buffering=16*1024*1024) as f: | |
| reader = csv.reader(f) | |
| header = next(reader) | |
| pack_idx = [i for i,c in enumerate(header) if c.startswith("pack_card_")] | |
| pick_idx = header.index("pick") | |
| wins_idx = header.index("event_match_wins") | |
| loss_idx = header.index("event_match_losses") | |
| draft_idx = 2 | |
| try: | |
| user_games = header.index("user_n_games_bucket") | |
| except: | |
| user_games = header.index("user_n_matches_bucket") | |
| try: | |
| user_wr = header.index("user_game_win_rate_bucket") | |
| except: | |
| try: | |
| user_wr = header.index("user_match_win_rate_bucket") | |
| except: | |
| user_wr = None | |
| if "expansion_pick_number" in header: | |
| order_idx = header.index("expansion_pick_number") | |
| pack_num_idx = None | |
| pick_num_idx = None | |
| else: | |
| order_idx = None | |
| pack_num_idx = header.index("pack_number") | |
| pick_num_idx = header.index("pick_number") | |
| maindeck_idx = header.index("pick_maindeck_rate") if "pick_maindeck_rate" in header else None | |
| pack_names = {i: header[i].replace("pack_card_", "") for i in pack_idx} | |
| for row in tqdm(reader): | |
| try: | |
| draft_id = row[draft_idx] | |
| positive = row[pick_idx] | |
| except: | |
| continue | |
| step = int(row[order_idx]) if order_idx is not None \ | |
| else int(row[pack_num_idx]) * 15 + int(row[pick_num_idx]) | |
| # pick first, then the rest of the pack | |
| pack_cards = [positive] | |
| for i in pack_idx: | |
| s = row[i] | |
| if s and s != "0": | |
| name = pack_names[i] | |
| if name != positive: | |
| pack_cards.extend([name] * int(s)) | |
| wins = int(row[wins_idx]) | |
| losses = int(row[loss_idx]) | |
| u_g = int(row[user_games]) | |
| u_wr = float(row[user_wr]) if user_wr and row[user_wr] else 0.0 | |
| in_md = float(row[maindeck_idx]) if maindeck_idx is not None and row[maindeck_idx] else 0.0 | |
| if positive in ('Plains', 'Island', 'Swamp', 'Mountain', 'Forest'): | |
| in_md = 0.0 | |
| if draft_id not in drafts: | |
| drafts[draft_id] = {'steps': {}, 'wins': wins, 'losses': losses, | |
| 'u_g': u_g, 'u_wr': u_wr} | |
| if step not in drafts[draft_id]['steps']: | |
| drafts[draft_id]['steps'][step] = (pack_cards, in_md) | |
| for draft_id, data in drafts.items(): | |
| sorted_steps = sorted(data['steps'].items()) | |
| sequence = [pack for _, (pack, _) in sorted_steps] | |
| in_maindeck = [md for _, (_, md) in sorted_steps] | |
| if not sequence: | |
| continue | |
| payload = encode_sample((sequence, in_maindeck, data['wins'], data['losses'], | |
| data['u_g'], data['u_wr'])) | |
| (test_q if _is_test(draft_id) else train_q).put((draft_id.encode(), payload)) | |
| def all_preprocessing_for_set(set_tag, raw_data_folder, out_folder): | |
| base = os.path.join(raw_data_folder, set_tag) | |
| files = [os.path.join(base, f) for f in os.listdir(base) if f.endswith(".csv")] | |
| out = os.path.join(out_folder, set_tag) | |
| for name in ('train.lmdb', 'test.lmdb'): | |
| p = os.path.join(out, name) | |
| if os.path.exists(p): | |
| shutil.rmtree(p) | |
| os.makedirs(out, exist_ok=True) | |
| train_q = Queue(maxsize=50_000) | |
| test_q = Queue(maxsize=50_000) | |
| train_writer = Process(target=lmdb_writer, args=(os.path.join(out, "train.lmdb"), train_q)) | |
| test_writer = Process(target=lmdb_writer, args=(os.path.join(out, "test.lmdb"), test_q)) | |
| train_writer.start(); test_writer.start() | |
| for csv_path in files: | |
| print(f"Processing {os.path.basename(csv_path)}") | |
| stream_csv_trajectories(csv_path, train_q, test_q) | |
| train_q.put(None); test_q.put(None) | |
| train_writer.join(); test_writer.join() | |
| if __name__ == "__main__": | |
| config = utils.load_config('src/configs/config.yaml') | |
| raw_data_folder = config['raw_data_folder'] | |
| out_folder = config['super_folder'] | |
| os.makedirs(out_folder, exist_ok=True) | |
| for folder in os.listdir(raw_data_folder): | |
| if folder not in os.listdir(out_folder) and folder != "cube": | |
| print(f"Processing set {folder}") | |
| all_preprocessing_for_set(folder, raw_data_folder, out_folder) | |