File size: 10,502 Bytes
d5ecd7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791c23b
 
d5ecd7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791c23b
5b85fd3
 
d5ecd7e
 
 
 
 
791c23b
d5ecd7e
 
791c23b
 
 
d5ecd7e
 
791c23b
d5ecd7e
 
 
 
 
 
 
 
 
5b85fd3
 
 
 
d5ecd7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
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)