3cb-data

Unnamed repository; edit this file 'description' to name the repository.
git clone git://git.alexwennerberg.com/3cb-data.git
Log | Files | Refs | README | LICENSE

commit c1ec08e486dc384113c450a3a4c91bc90b0672e7
parent d8d299dd12cad4a29d7e40317396e2a8197f1066
Author: alex wennerberg <awennerb@twitch.tv>
Date:   Sun, 29 Oct 2023 15:11:10 -0400

Add bans and other data analysis

Diffstat:
Mgetdata.py | 177+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mrequirements.txt | 1+
Asql/export.sql | 7+++++++
Asql/topdecks.sql | 17+++++++++++++++++
Asql/workbook.md | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 402 insertions(+), 76 deletions(-)

diff --git a/getdata.py b/getdata.py @@ -9,6 +9,30 @@ from Levenshtein import distance con = sqlite3.connect("3cb.db") allcards = set(open('allcards.txt','r').read().splitlines()) +# cache +def fileids_in_db(): + return [a[0] for a in con.cursor().execute("select fileid from round").fetchall()] + +def bans_from_db(): + return [a[0] for a in con.cursor().execute("select name from bans").fetchall()] + +def main(): + cache = fileids_in_db() + for n, file in enumerate(get_round_fileids()): + if file in cache: + continue + if file not in cache: + print(f"analyzing round {n+1}") + save_round(n+1, file) + +# scrape site to list rounds +def get_round_fileids(): + allrounds = requests.get("https://sites.google.com/view/3cb-metashape/pairings-results/past-results") + sheetre = re.compile("spreadsheets/d/(.*?)/") + curr_round = requests.get("https://sites.google.com/view/3cb-metashape/pairings-results") + ids = sheetre.findall(allrounds.text + curr_round.text) + return ids + def file_to_wb(fileid): params = {"mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"} headers = {"x-goog-api-key": os.environ.get("GOOG_KEY")} @@ -21,14 +45,82 @@ def file_to_wb(fileid): os.exit(1) return openpyxl.load_workbook(io.BytesIO(res.content), data_only=True) -# scrape site to list rounds -def get_round_fileids(): - allrounds = requests.get("https://sites.google.com/view/3cb-metashape/pairings-results/past-results") - sheetre = re.compile("spreadsheets/d/(.*?)/") - curr_round = requests.get("https://sites.google.com/view/3cb-metashape/pairings-results") - ids = sheetre.findall(curr_round.text) - return ids +def update_bans(): + sheet = file_to_wb("1NZuROOCctbq4p4-CAHE-jOC0675QQjuqVnUqdXFCVD8")["banlist"] + cards = [row[1].value for row in [*sheet.rows][3:92]] + cur = con.cursor() + cur.execute("drop table if exists ban") + cur.execute("create table ban (name text)") + for card in cards: + if card: + cur.execute("insert into ban values (?)", (clean_card(card),)) + con.commit() + +# not really normalized well +schema = [""" +create table if not exists round ( + id integer primary key, + fileid text not null +); +""", +""" +create table if not exists card ( + round integer not null, + player varchar not null, + name text +);""",""" +create table if not exists match ( + round integer, + group_name text, + player text, + opp_player text, + score integer +);"""] + +def save_sheet(cur, sheet, n, final): + rows = [[cell.value for cell in row if cell.value != None] for row in sheet.rows] + group = "" + i = 0 + while i < len(rows): + row = rows[i] + i += 1 + if len(row) < 3: + continue + group = row[0].partition(" ")[2] + if final: + group = "final" + try: + start = row.index("VS") + 1 + end = row.index("Score") + except ValueError: + start = 2 + end = len(row) + players = row[start:end] + for _ in range(len(players)): + row = rows[i] + i += 1 + player = row[0] + cards = row[1].split("\n") + for card in cards: + if not final: #already done + cur.execute("insert into card values (?,?,?);", (n, player, clean_card(card))) + for o, s in enumerate(row[start:end]): + cur.execute("insert into match values (?, ?, ?, ?, ?);", + (n, group, player,players[o],s)); +def save_round(n, fileid): + wb = file_to_wb(fileid) + try: + groups = wb["Groups"] + except KeyError: + groups = wb["Group"] + cur = con.cursor() + save_sheet(cur, groups, n, False) + if len(wb._sheets) > 1: + final = wb["Final Group"] + save_sheet(cur, final, n, True) + cur.execute("insert into round values (?, ?);", (n, fileid)); + con.commit() # fixing some data issues def best_guess(card): m = 6 @@ -65,7 +157,7 @@ def replacement(card): "Dark Ritual (BTD 21)": "Dark Ritual", "Swamp (BRB 18)": "Swamp", "Monty Python and the Holy Grail Black Knight (Oathsworn Knight)": "Oathsworn Knight", - "Red Sun's Zenith (Again) ": "Red Sun's Zenith", + "Red Sun's Zenith (Again)": "Red Sun's Zenith", "Dwarven Hold (Again)": "Dwarven Hold", } if card in m: @@ -81,76 +173,9 @@ def clean_card(card_name): return best_guess(clean) return clean -# not really normalized well -schema = [""" -create table if not exists round ( - id integer primary key, - fileid text not null -); -""", -""" -create table if not exists card ( - round integer not null, - player varchar not null, - name text -);""",""" -create table if not exists match ( - round integer, - group_name text, - player text, - opp_player text, - score integer -);"""] - -def save_sheet(cur, sheet, n): - rows = [[cell.value for cell in row if cell.value != None] for row in sheet.rows] - group = "" - i = 0 - while i < len(rows): - row = rows[i] - i += 1 - if len(row) == 0: - continue - group = row[0].partition(" ")[2] - players = row[2:-1] - print("analysinzg group", players) - for _ in range(len(players)-1): - row = rows[i] - i += 1 - print(row) - player = row[0] - cards = row[1].split("\n") - for card in cards: - cur.execute("insert into card values (?,?,?);", (n, player, clean_card(card))) - for o, s in enumerate(row[2:-1]): - cur.execute("insert into match values (?, ?, ?, ?, ?);", - (n, group, player,players[o],s)); - -def save_round(n, fileid): - wb = file_to_wb(fileid) - groups = wb["Groups"] - cur = con.cursor() - save_sheet(cur, groups, n) - if len(wb) > 1: - final = wb["Final Group"] - save_sheet(cur, final, n) - cur.execute("insert into round values (?, ?);", (n, fileid)); - con.commit() - -# cache -def fileids_in_db(): - return con.cursor().execute("select fileid from round").fetchall() - -def main(): - cache = fileids_in_db() - for n, file in enumerate(get_round_fileids()): - n = n +len(cache) - if file not in cache: - print(f"analyzing round {n+1}") - save_round(n+1, file) - if __name__ == "__main__": for table in schema: con.execute(table) + update_bans() main() diff --git a/requirements.txt b/requirements.txt @@ -1,3 +1,4 @@ requests titlecase openpyxl +Levenshtein diff --git a/sql/export.sql b/sql/export.sql @@ -0,0 +1,7 @@ +select match.round,group_name,match.player,deck,sum(score) as score +from match +join (select round, player, group_concat(name, ';') as deck +from (select * from card order by round,player,name) +group by 1,2) as deck +on deck.round = match.round and deck.player = match.player +group by 1,2,3 diff --git a/sql/topdecks.sql b/sql/topdecks.sql @@ -0,0 +1,17 @@ +with deck as (select round, player, group_concat(name, ';') as deck +from ( + select round,player,card.name from card + left join ban on card.name = ban.name + where ban.name is null + order by round,player,card.name +) +group by 1,2 +having count(name) == 3 +) +select deck.deck, cast(avg(score) * 100 as int) / 100.0 as avgscore, count(1) from +deck +join match on deck.round = match.round and deck.player = match.player +join deck d2 on deck.round = d2.round and match.opp_player = d2.player where d2.player is not null +group by 1 +having count(1) > 10 +order by 2 desc; diff --git a/sql/workbook.md b/sql/workbook.md @@ -0,0 +1,276 @@ +### Most popular cards (top 50) +```sql +select name,count(1) from card group by 1 order by 2 desc limit 50; +``` +``` ++-------------------------+----------+ +| name | count(1) | ++-------------------------+----------+ +| Black Lotus | 104 | +| Chancellor of the Forge | 74 | +| City of Traitors | 73 | +| Chancellor of the Annex | 70 | +| Mox Pearl | 64 | +| Memnite | 64 | +| Thassa's Oracle | 50 | +| Wasteland | 44 | +| Swamp | 43 | +| Icatian Store | 42 | +| Mox Sapphire | 41 | +| Lion's Eye Diamond | 39 | +| Karakas | 39 | +| Mental Misstep | 34 | +| Hickory Woodlot | 34 | +| Leyline of Anticipation | 33 | +| Force of Will | 30 | +| Crystal Vein | 30 | +| Mana Crypt | 29 | +| Cavern of Souls | 29 | +| Emrakul, the Aeons Torn | 28 | +| Dryad Arbor | 28 | +| Channel | 28 | +| Dwarven Hold | 27 | +| Crashing Footfalls | 27 | +| Chronomaton | 27 | +| Mountain | 26 | +| Lotus Bloom | 26 | +| Mishra's Workshop | 24 | +| Mishra's Factory | 24 | +| Forest | 24 | +| Strip Mine | 23 | +| Death's Shadow | 23 | +| Student of Warfare | 22 | +| Saprazzan Cove | 22 | +| Force of Negation | 22 | +| Plains | 21 | +| Hexdrinker | 21 | +| Thoughtseize | 19 | +| Leyline of Singularity | 19 | +| Elite Spellbinder | 19 | +| Chalice of the Void | 19 | +| Blackmail | 19 | +| Undiscovered Paradise | 18 | +| Steel Overseer | 18 | +| Shadowgrange Archfiend | 18 | +| Sand Silos | 18 | +| Cenn's Tactician | 18 | +| Burning Inquiry | 18 | +| The Rack | 17 | ++-------------------------+----------+ +``` +### Best decks, played in at least 30 games +```sql +select deck, cast(avg(score) * 100 as int) / 100.0 as avgscore, count(1) from +(select round, player, group_concat(name, ';') as deck +from (select * from card order by round,player,name) +group by 1,2) as deck +join match on deck.round = match.round and deck.player = match.player +group by 1 +having count(1) > 30 +order by 2 desc; +``` +``` ++------------------------------------------------------------+----------+----------+ +| deck | avgscore | count(1) | ++------------------------------------------------------------+----------+----------+ +| Cenn's Tactician;Karakas;Leyline of Singularity | 4.77 | 36 | +| Crystal Vein;Mox Emerald;Words of Wilding | 4.31 | 35 | +| Isochron Scepter;Mana Crypt;Verdant Command | 3.93 | 32 | +| Death's Shadow;Tarnished Citadel;Thoughtseize | 3.86 | 83 | +| Mana Crypt;Phyrexian Dreadnought;The Mycosynth Gardens | 3.74 | 35 | +| Mox Pearl;Rishadan Port;Skrelv's Hive | 3.7 | 54 | +| Chancellor of the Annex;Chancellor of the Forge;Strip Mine | 3.66 | 62 | +| Chancellor of the Annex;Icatian Store;Strip Mine | 3.44 | 79 | +| Cenn's Tactician;Curse of Silence;Plains | 3.44 | 54 | +| City of Traitors;Magus of the Moon;Mox Ruby | 3.4 | 47 | +| Black Lotus;Black Lotus;Minsc & Boo, Timeless Heroes | 3.4 | 52 | +| Crashing Footfalls;Dismember;Forest | 3.37 | 45 | +| Disrupting Shoal;Evermind;Memnite | 3.3 | 36 | +| Autonomous Assembler;Mishra's Factory;Mox Pearl | 3.25 | 36 | +| Chalice of the Void;City of Traitors;Hangarback Walker | 3.2 | 64 | +| Emrakul, the Aeons Torn;Lion's Eye Diamond;Shelldock Isle | 3.17 | 93 | +| Chancellor of the Tangle;Crashing Footfalls;Force of Vigor | 3.17 | 67 | +| Mana Crypt;Trinisphere;Urza's Saga | 3.16 | 36 | +| Force of Despair;Memnite;Unmask | 3.14 | 49 | +| Barren Glory;Lotus Bloom;Lotus Bloom | 3.14 | 85 | +| Black Lotus;Black Lotus;Steel Seraph | 3.13 | 46 | +| Retrofitter Foundry;Sol Ring;Wasteland | 3.11 | 69 | +| Cenn's Tactician;Concealed Courtyard;Thoughtseize | 3.11 | 36 | +| Island;Laboratory Maniac;Mana Crypt | 3.08 | 35 | +| Dark Depths;Urborg, Tomb of Yawgmoth;Vampire Hexmage | 3.05 | 73 | ++------------------------------------------------------------+----------+----------+ +``` +### Best decks, played in at least 30 games, filter bans + +### Avg card scores, played in at least 100 games + +```sql +select name, cast(avg(score) * 100 as int) / 100.0 as score, count(1) from card +join match on card.round = match.round and card.player = match.player +group by 1 +having count(1) > 100 +order by 2 desc +``` + +``` ++-------------------------------------+-------+----------+ +| name | score | count(1) | ++-------------------------------------+-------+----------+ +| Wild Magic Surge | 3.63 | 102 | +| Mayor of Avabruck // Howlpack Alpha | 3.58 | 116 | +| Ratchet Bomb | 3.54 | 135 | +| Rishadan Port | 3.41 | 217 | +| Barren Glory | 3.4 | 198 | +| Curse of Silence | 3.39 | 190 | +| Minsc & Boo, Timeless Heroes | 3.37 | 131 | +| Thoughtseize | 3.35 | 515 | +| Cenn's Tactician | 3.34 | 378 | +| Strip Mine | 3.28 | 398 | +| Retrofitter Foundry | 3.28 | 285 | +| The Rack | 3.26 | 387 | +| Tarnished Citadel | 3.26 | 336 | +| Stromkirk Noble | 3.24 | 254 | +| Steel Overseer | 3.21 | 464 | +| Luminarch Aspirant | 3.19 | 121 | +| Gisa's Bidding | 3.19 | 168 | +| Angel of Sanctions | 3.19 | 151 | +| Phyrexian Dreadnought | 3.16 | 139 | +| Skrelv's Hive | 3.15 | 164 | +| Unmask | 3.14 | 122 | +| Force of Despair | 3.11 | 185 | +| Chalice of the Void | 3.11 | 335 | +| Dark Depths | 3.1 | 187 | +| Sandstone Needle | 3.09 | 166 | +| Sol Ring | 3.08 | 382 | +| Disrupting Shoal | 3.08 | 149 | +| Death's Shadow | 3.08 | 517 | +| Mox Emerald | 3.07 | 248 | +| Boseiju, Who Endures | 3.07 | 198 | +| Vampire Hexmage | 3.06 | 123 | +| Solitude | 3.05 | 130 | +| Noxious Revival | 3.05 | 187 | +| Chancellor of the Annex | 3.04 | 1005 | +| Bayou | 3.04 | 120 | +| Isochron Scepter | 3.03 | 111 | +| Mana Crypt | 3.02 | 634 | +| Lotus Bloom | 3.02 | 601 | +| Hangarback Walker | 3.02 | 433 | +| Show and Tell | 3.01 | 186 | +| Overgrown Tomb | 3.0 | 177 | +| Magus of the Moon | 3.0 | 244 | +| Daze | 3.0 | 120 | +| Crashing Footfalls | 2.98 | 662 | +| Shrieking Affliction | 2.97 | 113 | +| Anointed Peacekeeper | 2.97 | 174 | +| Sheltered Valley | 2.95 | 133 | +| Grief | 2.95 | 172 | +| Crystal Vein | 2.95 | 568 | +| Thassa's Oracle | 2.94 | 742 | +| Blackmail | 2.94 | 386 | +| Inkmoth Nexus | 2.93 | 298 | +| Tropical Island | 2.92 | 194 | +| Funeral Charm | 2.92 | 118 | +| Mishra's Factory | 2.9 | 508 | +| Leyline of Singularity | 2.9 | 340 | +| City of Traitors | 2.9 | 1413 | +| Cavern of Souls | 2.9 | 460 | +| Alpine Moon | 2.9 | 275 | +| Mind Swords | 2.89 | 283 | +| Force of Vigor | 2.89 | 144 | +| Dismember | 2.89 | 157 | +| Trinisphere | 2.88 | 194 | +| Autonomous Assembler | 2.88 | 233 | +| Snapback | 2.87 | 213 | +| Piracy Charm | 2.87 | 110 | +| Phyrexian Revoker | 2.87 | 107 | +| Urza's Saga | 2.86 | 304 | +| Ulamog, the Infinite Gyre | 2.86 | 301 | +| Swarm Shambler | 2.86 | 303 | +| Shelldock Isle | 2.86 | 156 | +| Oboro, Palace in the Clouds | 2.86 | 264 | +| Dwarven Hold | 2.85 | 405 | +| Mox Pearl | 2.83 | 1512 | +| Undiscovered Paradise | 2.82 | 462 | +| Mana Vault | 2.82 | 168 | +| Swamp | 2.81 | 795 | +| Oko, Thief of Crowns | 2.81 | 315 | +| Interplanar Beacon | 2.8 | 237 | +| Icatian Store | 2.8 | 757 | +| Urborg, Tomb of Yawgmoth | 2.79 | 143 | +| Sand Silos | 2.79 | 340 | +| Blooming Marsh | 2.79 | 260 | +| Mutavault | 2.78 | 121 | +| Meddling Mage | 2.78 | 144 | +| Cabal Therapy | 2.78 | 296 | +| Flash | 2.77 | 134 | +| Cathedral of War | 2.77 | 203 | +| The Tabernacle at Pendrell Vale | 2.76 | 123 | +| Dark Ritual | 2.75 | 182 | +| Mox Jet | 2.74 | 322 | +| Hexdrinker | 2.74 | 513 | +| Saprazzan Skerry | 2.73 | 215 | +| Laboratory Maniac | 2.73 | 167 | +| Emrakul, the Aeons Torn | 2.73 | 492 | +| Island Sanctuary | 2.72 | 110 | +| Student of Warfare | 2.71 | 510 | +| Mox Sapphire | 2.7 | 775 | +| Black Lotus | 2.7 | 1419 | +| Balance | 2.7 | 271 | +| Force of Will | 2.69 | 354 | +| Burning Inquiry | 2.69 | 314 | +| Memnite | 2.68 | 1139 | +| Lion's Eye Diamond | 2.68 | 757 | +| Chancellor of the Forge | 2.68 | 1172 | +| Sphere of Resistance | 2.67 | 115 | +| Island | 2.67 | 213 | +| Chancellor of the Tangle | 2.67 | 256 | +| Fury | 2.66 | 271 | +| Chronomaton | 2.66 | 552 | +| Misdirection | 2.65 | 127 | +| Mind Rake | 2.65 | 138 | +| Mishra's Workshop | 2.63 | 384 | +| Maze of Ith | 2.63 | 258 | +| Fountain of Cho | 2.63 | 206 | +| Simian Spirit Guide | 2.62 | 283 | +| Young Wolf | 2.6 | 170 | +| Mountain | 2.6 | 421 | +| Karakas | 2.6 | 580 | +| Nether Spirit | 2.59 | 321 | +| Hickory Woodlot | 2.59 | 612 | +| Ghost Quarter | 2.59 | 257 | +| Force of Negation | 2.59 | 361 | +| Elite Spellbinder | 2.59 | 386 | +| Wasteland | 2.58 | 847 | +| Dryad Arbor | 2.56 | 364 | +| Dunes of the Dead | 2.55 | 173 | +| Underground River | 2.53 | 105 | +| Plains | 2.52 | 376 | +| Energy Field | 2.52 | 238 | +| Saprazzan Cove | 2.51 | 266 | +| Jace, Wielder of Mysteries | 2.5 | 124 | +| Bottomless Vault | 2.49 | 153 | +| Nullhide Ferox | 2.45 | 232 | +| Remote Farm | 2.44 | 157 | +| Forest | 2.44 | 418 | +| Darksteel Citadel | 2.44 | 181 | +| Karn Liberated | 2.42 | 106 | +| Shadowgrange Archfiend | 2.4 | 367 | +| Mox Ruby | 2.38 | 110 | +| Channel | 2.38 | 435 | +| Sea's Claim | 2.37 | 182 | +| Old-Growth Dryads | 2.34 | 330 | +| Smallpox | 2.33 | 104 | +| Hollow Trees | 2.32 | 246 | +| Thalia, Guardian of Thraben | 2.3 | 156 | +| Lavinia, Azorius Renegade | 2.29 | 127 | +| Blazing Rootwalla | 2.29 | 163 | +| Treetop Village | 2.27 | 120 | +| Swords to Plowshares | 2.27 | 159 | +| Pyrokinesis | 2.22 | 116 | +| Mental Misstep | 2.13 | 616 | +| Leyline of Anticipation | 2.09 | 413 | +| Bitterblossom | 2.06 | 149 | +| Peat Bog | 1.8 | 139 | +| Leyline of Sanctity | 1.47 | 183 | ++-------------------------------------+-------+----------+ +```