2581 lines
89 KiB
Plaintext
2581 lines
89 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 90,
|
||
"id": "5c77481b",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Requirements\n",
|
||
"import numpy as np\n",
|
||
"import pandas as pd\n",
|
||
"import io\n",
|
||
"import json\n",
|
||
"\n",
|
||
"\n",
|
||
"# In this notebook, I take the ideas from experiments-1, and we use the quest levels to discover zones instead of using hard coded zone names. "
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 91,
|
||
"id": "d0b3427f",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Total quests for level bracket 1-3: 77\n",
|
||
" Tirisfal Glades: 15 quests\n",
|
||
" Mulgore: 11 quests\n",
|
||
" Durotar: 20 quests\n",
|
||
" Warlock: 4 quests\n",
|
||
" Warrior: 5 quests\n",
|
||
" Hunter: 4 quests\n",
|
||
" Rogue: 4 quests\n",
|
||
" Shaman: 4 quests\n",
|
||
" Priest: 2 quests\n",
|
||
" Mage: 3 quests\n",
|
||
" Druid: 2 quests\n",
|
||
" Blackrock Depths: 1 quests\n",
|
||
" Uncategorized: 23 quests\n",
|
||
" Darkmoon Faire: 1 quests\n",
|
||
" Winter Veil: 4 quests\n",
|
||
" Paladin: 2 quests\n",
|
||
" Zephras Isle: 13 quests\n",
|
||
"Total quests for level bracket 4-6: 126\n",
|
||
" Tirisfal Glades: 32 quests\n",
|
||
" Mulgore: 32 quests\n",
|
||
" Durotar: 35 quests\n",
|
||
" Shaman: 8 quests\n",
|
||
" Warlock: 2 quests\n",
|
||
" Priest: 4 quests\n",
|
||
" Paladin: 2 quests\n",
|
||
" Uncategorized: 72 quests\n",
|
||
" Zephras Isle: 30 quests\n",
|
||
"Total quests for level bracket 7-9: 85\n",
|
||
" Undercity: 4 quests\n",
|
||
" Tirisfal Glades: 16 quests\n",
|
||
" Mulgore: 17 quests\n",
|
||
" Thunder Bluff: 3 quests\n",
|
||
" Durotar: 19 quests\n",
|
||
" Miscellaneous: 1 quests\n",
|
||
" Uncategorized: 30 quests\n",
|
||
" Zephras Isle: 30 quests\n",
|
||
"Total quests for level bracket 10-12: 216\n",
|
||
" Tirisfal Glades: 45 quests\n",
|
||
" Silverpine Forest: 17 quests\n",
|
||
" Thunder Bluff: 30 quests\n",
|
||
" Mulgore: 29 quests\n",
|
||
" Durotar: 38 quests\n",
|
||
" The Barrens: 25 quests\n",
|
||
" Loch Modan: 1 quests\n",
|
||
" Warlock: 6 quests\n",
|
||
" Warrior: 10 quests\n",
|
||
" Shaman: 11 quests\n",
|
||
" Rogue: 2 quests\n",
|
||
" Mage: 4 quests\n",
|
||
" Priest: 12 quests\n",
|
||
" Orgrimmar: 29 quests\n",
|
||
" Druid: 11 quests\n",
|
||
" Hunter: 19 quests\n",
|
||
" Winter Veil: 1 quests\n",
|
||
" Miscellaneous: 2 quests\n",
|
||
" Paladin: 13 quests\n",
|
||
" Uncategorized: 29 quests\n",
|
||
" Zephras Isle: 41 quests\n",
|
||
"Total quests for level bracket 13-15: 79\n",
|
||
" Silverpine Forest: 13 quests\n",
|
||
" Thunder Bluff: 7 quests\n",
|
||
" Tirisfal Glades: 4 quests\n",
|
||
" Mulgore: 4 quests\n",
|
||
" Durotar: 6 quests\n",
|
||
" The Barrens: 31 quests\n",
|
||
" Undercity: 11 quests\n",
|
||
" Shaman: 3 quests\n",
|
||
" Rogue: 6 quests\n",
|
||
" Mage: 3 quests\n",
|
||
" Priest: 3 quests\n",
|
||
" Ragefire Chasm: 1 quests\n",
|
||
" Orgrimmar: 13 quests\n",
|
||
" Druid: 5 quests\n",
|
||
" Uncategorized: 7 quests\n",
|
||
" Zephras Isle: 7 quests\n",
|
||
"Total quests for level bracket 16-18: 76\n",
|
||
" Druid: 4 quests\n",
|
||
" Westfall: 5 quests\n",
|
||
" Silverpine Forest: 14 quests\n",
|
||
" The Barrens: 35 quests\n",
|
||
" Herbalism: 1 quests\n",
|
||
" Wailing Caverns: 6 quests\n",
|
||
" Stonetalon Mountains: 7 quests\n",
|
||
" Thunder Bluff: 10 quests\n",
|
||
" Mage: 1 quests\n",
|
||
" Rogue: 6 quests\n",
|
||
" Ragefire Chasm: 5 quests\n",
|
||
" Orgrimmar: 9 quests\n",
|
||
" Undercity: 10 quests\n",
|
||
" Zephras Isle: 0 quests\n",
|
||
"Total quests for level bracket 19-21: 116\n",
|
||
" Westfall: 2 quests\n",
|
||
" Orgrimmar: 21 quests\n",
|
||
" Silverpine Forest: 5 quests\n",
|
||
" Hillsbrad Foothills: 3 quests\n",
|
||
" Thunder Bluff: 9 quests\n",
|
||
" The Barrens: 46 quests\n",
|
||
" Shaman: 5 quests\n",
|
||
" Stonetalon Mountains: 11 quests\n",
|
||
" Warlock: 15 quests\n",
|
||
" Wailing Caverns: 1 quests\n",
|
||
" Warrior: 3 quests\n",
|
||
" Rogue: 4 quests\n",
|
||
" Priest: 6 quests\n",
|
||
" Ashenvale: 7 quests\n",
|
||
" Warsong Gulch: 2 quests\n",
|
||
" Winter Veil: 1 quests\n",
|
||
" Miscellaneous: 1 quests\n",
|
||
" Uncategorized: 16 quests\n",
|
||
" Mage: 1 quests\n",
|
||
" Ruins of Lordaeron: 4 quests\n",
|
||
" Undercity: 36 quests\n",
|
||
" Druid: 8 quests\n",
|
||
" Zephras Isle: 0 quests\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"path = rf\"D:\\Project\\Py\\WowheadScraper\\quest_analyzer\\resources\\quests.jsonl\"\n",
|
||
"df = pd.read_json(path, lines=True)\n",
|
||
"\n",
|
||
"if df is None:\n",
|
||
" raise Exception(\"DataFrame is None. Failed to read the CSV file.\")\n",
|
||
"\n",
|
||
"df.describe()\n",
|
||
"\n",
|
||
"horde_only_quests = df[(df[\"side\"] == \"Horde\") | (df[\"side\"] == \"Both\")]\n",
|
||
"\n",
|
||
"def strip_bracket_string(bracket_string):\n",
|
||
" # Split the string by \": \" and take the second part\n",
|
||
" parts = bracket_string.split(\": \")\n",
|
||
" if len(parts) > 1:\n",
|
||
" return parts[1]\n",
|
||
" return bracket_string # Return the original string if it doesn't contain \": \"\n",
|
||
"\n",
|
||
"def locations_contains_zone(row, zone_name):\n",
|
||
" locations = row.get(\"locations\")\n",
|
||
" if not isinstance(locations, list):\n",
|
||
" return False\n",
|
||
" for location in locations:\n",
|
||
" if location.get(\"role\") == \"start\" and zone_name in location[\"zone\"]:\n",
|
||
" return True\n",
|
||
" if location.get(\"role\") == \"end\" and zone_name in location[\"zone\"]:\n",
|
||
" return True\n",
|
||
" return False\n",
|
||
"\n",
|
||
"def get_quests_by_zone(_df: pd.DataFrame, zone_name: str) -> pd.DataFrame:\n",
|
||
" location_mask = _df.apply(lambda row: locations_contains_zone(row, zone_name), axis=1)\n",
|
||
" category_mask = (_df[\"category\"] == zone_name) | (_df[\"category2\"] == zone_name)\n",
|
||
" return _df[location_mask | category_mask]\n",
|
||
"\n",
|
||
"def get_quests_by_level_bracket(_df: pd.DataFrame, min_level: int, max_level: int) -> pd.DataFrame:\n",
|
||
" return _df[(_df[\"level\"] >= min_level) & (_df[\"level\"] <= max_level)]\n",
|
||
"\n",
|
||
"# Now, our brackets are going to be every three levels instead of 10. Let's use loops to make things\n",
|
||
"# a little more efficient.\n",
|
||
"index_prefix = 1\n",
|
||
"brackets = {}\n",
|
||
"for i in range(1, 21, 3):\n",
|
||
" min_level = i\n",
|
||
" max_level = i + 2\n",
|
||
" bracket_quests = get_quests_by_level_bracket(horde_only_quests, min_level, max_level)\n",
|
||
" print(f\"Total quests for level bracket {min_level}-{max_level}: {len(bracket_quests)}\")\n",
|
||
"\n",
|
||
" bracket_zones = bracket_quests[[\"category\", \"category2\"]].drop_duplicates().to_dict(orient='records')\n",
|
||
" bracket_zones.append({\"category\": \"Zephras Isle\", \"category2\": \"Unknown\"}) # Add a placeholder for unknown zones\n",
|
||
" \n",
|
||
" \n",
|
||
" for zone in bracket_zones:\n",
|
||
" zone_name = zone.get(\"category\") or zone.get(\"category2\")\n",
|
||
" if zone_name:\n",
|
||
" zone_quests = get_quests_by_zone(bracket_quests, zone_name)\n",
|
||
" print(f\" {zone_name}: {len(zone_quests)} quests\")\n",
|
||
" brackets[(zone_name, f\"{index_prefix}: {min_level}-{max_level}\")] = zone_quests \n",
|
||
" index_prefix += 1\n",
|
||
"\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 92,
|
||
"id": "e826b3eb",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>zone</th>\n",
|
||
" <th>bracket</th>\n",
|
||
" <th>quests</th>\n",
|
||
" <th>total_xp</th>\n",
|
||
" <th>avg_xp</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>Ashenvale</td>\n",
|
||
" <td>7: 19-21</td>\n",
|
||
" <td>7</td>\n",
|
||
" <td>2150</td>\n",
|
||
" <td>307.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>Blackrock Depths</td>\n",
|
||
" <td>1: 1-3</td>\n",
|
||
" <td>1</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>Darkmoon Faire</td>\n",
|
||
" <td>1: 1-3</td>\n",
|
||
" <td>1</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>Druid</td>\n",
|
||
" <td>1: 1-3</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>125</td>\n",
|
||
" <td>62.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>Druid</td>\n",
|
||
" <td>4: 10-12</td>\n",
|
||
" <td>11</td>\n",
|
||
" <td>4540</td>\n",
|
||
" <td>413.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>...</th>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>103</th>\n",
|
||
" <td>Zephras Isle</td>\n",
|
||
" <td>3: 7-9</td>\n",
|
||
" <td>30</td>\n",
|
||
" <td>15090</td>\n",
|
||
" <td>503.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>104</th>\n",
|
||
" <td>Zephras Isle</td>\n",
|
||
" <td>4: 10-12</td>\n",
|
||
" <td>41</td>\n",
|
||
" <td>25465</td>\n",
|
||
" <td>621.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>105</th>\n",
|
||
" <td>Zephras Isle</td>\n",
|
||
" <td>5: 13-15</td>\n",
|
||
" <td>7</td>\n",
|
||
" <td>3045</td>\n",
|
||
" <td>435.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>106</th>\n",
|
||
" <td>Zephras Isle</td>\n",
|
||
" <td>6: 16-18</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>NaN</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>107</th>\n",
|
||
" <td>Zephras Isle</td>\n",
|
||
" <td>7: 19-21</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>NaN</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"<p>108 rows × 5 columns</p>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" zone bracket quests total_xp avg_xp\n",
|
||
"0 Ashenvale 7: 19-21 7 2150 307.0\n",
|
||
"1 Blackrock Depths 1: 1-3 1 0 0.0\n",
|
||
"2 Darkmoon Faire 1: 1-3 1 0 0.0\n",
|
||
"3 Druid 1: 1-3 2 125 62.0\n",
|
||
"4 Druid 4: 10-12 11 4540 413.0\n",
|
||
".. ... ... ... ... ...\n",
|
||
"103 Zephras Isle 3: 7-9 30 15090 503.0\n",
|
||
"104 Zephras Isle 4: 10-12 41 25465 621.0\n",
|
||
"105 Zephras Isle 5: 13-15 7 3045 435.0\n",
|
||
"106 Zephras Isle 6: 16-18 0 0 NaN\n",
|
||
"107 Zephras Isle 7: 19-21 0 0 NaN\n",
|
||
"\n",
|
||
"[108 rows x 5 columns]"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"def quest_xp(rewards) -> int:\n",
|
||
" if isinstance(rewards, dict) and rewards.get(\"xp\"):\n",
|
||
" return rewards[\"xp\"]\n",
|
||
" return 0\n",
|
||
"\n",
|
||
"xp_summary = pd.DataFrame([\n",
|
||
" {\"zone\": zone, \"bracket\": bracket, \"quests\": len(quests), \"total_xp\": quests[\"rewards\"].apply(quest_xp).sum()}\n",
|
||
" for (zone, bracket), quests in brackets.items()\n",
|
||
"])\n",
|
||
"xp_summary[\"avg_xp\"] = (xp_summary[\"total_xp\"] / xp_summary[\"quests\"]).round(0)\n",
|
||
"\n",
|
||
"show_df = xp_summary.sort_values(by=[\"zone\", \"bracket\"]).reset_index(drop=True)\n",
|
||
"display(show_df)\n",
|
||
"# Now we calculate the total experience available from quests for each zone and level bracket\n",
|
||
"show_df.to_csv(rf\"D:\\Project\\Py\\WowheadScraper\\quest_analyzer\\output\\quests.csv\", index=False)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "ab76fa6f",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th>quests</th>\n",
|
||
" <th>with_objective</th>\n",
|
||
" <th>total_xp</th>\n",
|
||
" <th>total_to_objective</th>\n",
|
||
" <th>total_from_objective</th>\n",
|
||
" <th>total_travel</th>\n",
|
||
" <th>avg_travel</th>\n",
|
||
" <th>xp_per_travel</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>zone</th>\n",
|
||
" <th>bracket</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>Ashenvale</th>\n",
|
||
" <th>7: 19-21</th>\n",
|
||
" <td>7</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>2150</td>\n",
|
||
" <td>86.2</td>\n",
|
||
" <td>86.2</td>\n",
|
||
" <td>172.4</td>\n",
|
||
" <td>43.1</td>\n",
|
||
" <td>12.5</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Blackrock Depths</th>\n",
|
||
" <th>1: 1-3</th>\n",
|
||
" <td>1</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>NaN</td>\n",
|
||
" <td>NaN</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Darkmoon Faire</th>\n",
|
||
" <th>1: 1-3</th>\n",
|
||
" <td>1</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>NaN</td>\n",
|
||
" <td>NaN</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">Druid</th>\n",
|
||
" <th>1: 1-3</th>\n",
|
||
" <td>2</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>125</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>NaN</td>\n",
|
||
" <td>inf</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4: 10-12</th>\n",
|
||
" <td>11</td>\n",
|
||
" <td>0</td>\n",
|
||
" <td>4540</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>NaN</td>\n",
|
||
" <td>inf</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>...</th>\n",
|
||
" <th>...</th>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>...</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"5\" valign=\"top\">Zephras Isle</th>\n",
|
||
" <th>1: 1-3</th>\n",
|
||
" <td>13</td>\n",
|
||
" <td>4</td>\n",
|
||
" <td>1475</td>\n",
|
||
" <td>14.5</td>\n",
|
||
" <td>14.5</td>\n",
|
||
" <td>42.2</td>\n",
|
||
" <td>3.2</td>\n",
|
||
" <td>35.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2: 4-6</th>\n",
|
||
" <td>30</td>\n",
|
||
" <td>9</td>\n",
|
||
" <td>8780</td>\n",
|
||
" <td>62.6</td>\n",
|
||
" <td>62.6</td>\n",
|
||
" <td>251.4</td>\n",
|
||
" <td>8.4</td>\n",
|
||
" <td>34.9</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3: 7-9</th>\n",
|
||
" <td>30</td>\n",
|
||
" <td>17</td>\n",
|
||
" <td>15090</td>\n",
|
||
" <td>182.4</td>\n",
|
||
" <td>183.2</td>\n",
|
||
" <td>501.8</td>\n",
|
||
" <td>16.7</td>\n",
|
||
" <td>30.1</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4: 10-12</th>\n",
|
||
" <td>41</td>\n",
|
||
" <td>20</td>\n",
|
||
" <td>25465</td>\n",
|
||
" <td>251.0</td>\n",
|
||
" <td>186.7</td>\n",
|
||
" <td>646.0</td>\n",
|
||
" <td>16.1</td>\n",
|
||
" <td>39.4</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>5: 13-15</th>\n",
|
||
" <td>7</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>3045</td>\n",
|
||
" <td>23.1</td>\n",
|
||
" <td>23.1</td>\n",
|
||
" <td>54.0</td>\n",
|
||
" <td>10.8</td>\n",
|
||
" <td>56.4</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"<p>106 rows × 8 columns</p>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" quests with_objective total_xp \\\n",
|
||
"zone bracket \n",
|
||
"Ashenvale 7: 19-21 7 3 2150 \n",
|
||
"Blackrock Depths 1: 1-3 1 0 0 \n",
|
||
"Darkmoon Faire 1: 1-3 1 0 0 \n",
|
||
"Druid 1: 1-3 2 0 125 \n",
|
||
" 4: 10-12 11 0 4540 \n",
|
||
"... ... ... ... \n",
|
||
"Zephras Isle 1: 1-3 13 4 1475 \n",
|
||
" 2: 4-6 30 9 8780 \n",
|
||
" 3: 7-9 30 17 15090 \n",
|
||
" 4: 10-12 41 20 25465 \n",
|
||
" 5: 13-15 7 3 3045 \n",
|
||
"\n",
|
||
" total_to_objective total_from_objective \\\n",
|
||
"zone bracket \n",
|
||
"Ashenvale 7: 19-21 86.2 86.2 \n",
|
||
"Blackrock Depths 1: 1-3 0.0 0.0 \n",
|
||
"Darkmoon Faire 1: 1-3 0.0 0.0 \n",
|
||
"Druid 1: 1-3 0.0 0.0 \n",
|
||
" 4: 10-12 0.0 0.0 \n",
|
||
"... ... ... \n",
|
||
"Zephras Isle 1: 1-3 14.5 14.5 \n",
|
||
" 2: 4-6 62.6 62.6 \n",
|
||
" 3: 7-9 182.4 183.2 \n",
|
||
" 4: 10-12 251.0 186.7 \n",
|
||
" 5: 13-15 23.1 23.1 \n",
|
||
"\n",
|
||
" total_travel avg_travel xp_per_travel \n",
|
||
"zone bracket \n",
|
||
"Ashenvale 7: 19-21 172.4 43.1 12.5 \n",
|
||
"Blackrock Depths 1: 1-3 0.0 NaN NaN \n",
|
||
"Darkmoon Faire 1: 1-3 0.0 NaN NaN \n",
|
||
"Druid 1: 1-3 0.0 NaN inf \n",
|
||
" 4: 10-12 0.0 NaN inf \n",
|
||
"... ... ... ... \n",
|
||
"Zephras Isle 1: 1-3 42.2 3.2 35.0 \n",
|
||
" 2: 4-6 251.4 8.4 34.9 \n",
|
||
" 3: 7-9 501.8 16.7 30.1 \n",
|
||
" 4: 10-12 646.0 16.1 39.4 \n",
|
||
" 5: 13-15 54.0 10.8 56.4 \n",
|
||
"\n",
|
||
"[106 rows x 8 columns]"
|
||
]
|
||
},
|
||
"execution_count": 93,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Now we try to calculate travel requirements for each quest. We define travel as the distance from the quest giver to the objective area, and then from the objective area to the quest turn-in. For delivery/talk-to quests, we just measure straight from giver to turn-in.\n",
|
||
"# An additional challenge is that the coordinates given are percentage based based off of the map frame.\n",
|
||
"# Some digging online found some map size estimates, and they are the closest I have to a reliable source for now.\n",
|
||
"\n",
|
||
"OBJECTIVE_ROLES = (\"requirement\", \"sourcerequirement\")\n",
|
||
"\n",
|
||
"def is_point(coord):\n",
|
||
" # Instance/dungeon zones have no map coords and are stored as [\"\"]\n",
|
||
" return isinstance(coord, list) and len(coord) == 2 and all(isinstance(v, (int, float)) for v in coord)\n",
|
||
"\n",
|
||
"def location_center(loc):\n",
|
||
" # An npc/object can have many spawn points, so use the center of all of them instead of the single representative coord\n",
|
||
" points = [c for c in loc.get(\"coords\") or [] if is_point(c)]\n",
|
||
" if points:\n",
|
||
" return np.mean(np.array(points, dtype=float), axis=0)\n",
|
||
" if is_point(loc.get(\"coord\")):\n",
|
||
" return np.array(loc[\"coord\"], dtype=float)\n",
|
||
" return None\n",
|
||
"\n",
|
||
"def zone_coords(locations, roles, zone_name):\n",
|
||
" centers = [location_center(loc) for loc in locations\n",
|
||
" if loc.get(\"role\") in roles and loc.get(\"zone\") == zone_name]\n",
|
||
" return [c for c in centers if c is not None]\n",
|
||
"\n",
|
||
"def distance(a, b):\n",
|
||
" if a is None or b is None:\n",
|
||
" return np.nan\n",
|
||
" return float(np.linalg.norm(np.subtract(a, b)))\n",
|
||
"\n",
|
||
"def quest_travel(row, zone_name):\n",
|
||
" \n",
|
||
" locations = row[\"locations\"] if isinstance(row[\"locations\"], list) else []\n",
|
||
" starts = zone_coords(locations, (\"start\",), zone_name)\n",
|
||
" ends = zone_coords(locations, (\"end\",), zone_name)\n",
|
||
" objectives = zone_coords(locations, OBJECTIVE_ROLES, zone_name)\n",
|
||
"\n",
|
||
" start = starts[0] if starts else None\n",
|
||
" end = ends[0] if ends else None\n",
|
||
" \n",
|
||
" # Objective area = mean of every objective npc/object's spawn center\n",
|
||
" objective = np.mean(np.array(objectives), axis=0) if objectives else None\n",
|
||
"\n",
|
||
" if objective is None:\n",
|
||
" # Delivery / talk-to quests: straight from giver to turn-in\n",
|
||
" to_objective, from_objective = np.nan, np.nan\n",
|
||
" total = distance(start, end)\n",
|
||
" else:\n",
|
||
" to_objective = distance(start, objective)\n",
|
||
" from_objective = distance(objective, end)\n",
|
||
" total = to_objective + from_objective\n",
|
||
"\n",
|
||
" return pd.Series({\n",
|
||
" \"start_x\": start[0] if start is not None else np.nan,\n",
|
||
" \"start_y\": start[1] if start is not None else np.nan,\n",
|
||
" \"objective_x\": objective[0] if objective is not None else np.nan,\n",
|
||
" \"objective_y\": objective[1] if objective is not None else np.nan,\n",
|
||
" \"end_x\": end[0] if end is not None else np.nan,\n",
|
||
" \"end_y\": end[1] if end is not None else np.nan,\n",
|
||
" \"to_objective\": to_objective,\n",
|
||
" \"from_objective\": from_objective,\n",
|
||
" \"total_travel\": total,\n",
|
||
" })\n",
|
||
"\n",
|
||
"def build_travel(quests: pd.DataFrame, zone_name: str, bracket: str) -> pd.DataFrame:\n",
|
||
" travel = quests.apply(lambda row: quest_travel(row, zone_name), axis=1)\n",
|
||
" travel.insert(0, \"zone\", zone_name)\n",
|
||
" travel.insert(1, \"bracket\", bracket)\n",
|
||
" travel.insert(2, \"id\", quests[\"id\"])\n",
|
||
" travel.insert(3, \"name\", quests[\"name\"])\n",
|
||
" travel.insert(4, \"level\", quests[\"level\"])\n",
|
||
" travel.insert(5, \"xp\", quests[\"rewards\"].apply(quest_xp))\n",
|
||
" return travel\n",
|
||
"\n",
|
||
"# Some zone + bracket entries have no quests (e.g. the Zephras Isle placeholder), and apply() on an empty frame doesn't return the travel columns\n",
|
||
"travel_df = pd.concat(\n",
|
||
" [build_travel(quests, zone, bracket) for (zone, bracket), quests in brackets.items() if not quests.empty],\n",
|
||
" ignore_index=True,\n",
|
||
")\n",
|
||
"\n",
|
||
"travel_summary = travel_df.groupby([\"zone\", \"bracket\"]).agg(\n",
|
||
" quests=(\"id\", \"count\"),\n",
|
||
" with_objective=(\"objective_x\", \"count\"),\n",
|
||
" total_xp=(\"xp\", \"sum\"),\n",
|
||
" total_to_objective=(\"to_objective\", \"sum\"),\n",
|
||
" total_from_objective=(\"from_objective\", \"sum\"),\n",
|
||
" total_travel=(\"total_travel\", \"sum\"),\n",
|
||
" avg_travel=(\"total_travel\", \"mean\"),\n",
|
||
")\n",
|
||
"travel_summary[\"xp_per_travel\"] = (travel_summary[\"total_xp\"] / travel_summary[\"total_travel\"]).round(1)\n",
|
||
"travel_summary.round(1)\n",
|
||
"\n",
|
||
"\n",
|
||
"save_path = rf\"D:\\Project\\Py\\WowheadScraper\\quest_analyzer\\output\\travel-horde.csv\"\n",
|
||
"travel_summary.to_csv(save_path, index=True)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 94,
|
||
"id": "7fe45672",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Ashenvale 19-21: 1 hotspots from 3 objectives\n",
|
||
"Durotar 1-3: 1 hotspots from 2 objectives\n",
|
||
"Durotar 4-6: 1 hotspots from 8 objectives\n",
|
||
"Durotar 7-9: 1 hotspots from 6 objectives\n",
|
||
"Durotar 10-12: 2 hotspots from 13 objectives\n",
|
||
"Mulgore 1-3: 1 hotspots from 2 objectives\n",
|
||
"Mulgore 4-6: 2 hotspots from 7 objectives\n",
|
||
"Mulgore 7-9: 1 hotspots from 7 objectives\n",
|
||
"Mulgore 10-12: 2 hotspots from 7 objectives\n",
|
||
"Orgrimmar 10-12: 1 hotspots from 3 objectives\n",
|
||
"Orgrimmar 13-15: 1 hotspots from 2 objectives\n",
|
||
"Orgrimmar 19-21: 1 hotspots from 2 objectives\n",
|
||
"Silverpine Forest 10-12: 1 hotspots from 5 objectives\n",
|
||
"Silverpine Forest 13-15: 2 hotspots from 4 objectives\n",
|
||
"Silverpine Forest 16-18: 2 hotspots from 4 objectives\n",
|
||
"Silverpine Forest 19-21: 1 hotspots from 3 objectives\n",
|
||
"Stonetalon Mountains 16-18: 1 hotspots from 3 objectives\n",
|
||
"Stonetalon Mountains 19-21: 2 hotspots from 5 objectives\n",
|
||
"The Barrens 10-12: 1 hotspots from 2 objectives\n",
|
||
"The Barrens 13-15: 2 hotspots from 12 objectives\n",
|
||
"The Barrens 16-18: 2 hotspots from 19 objectives\n",
|
||
"The Barrens 19-21: 2 hotspots from 20 objectives\n",
|
||
"Thunder Bluff 10-12: 1 hotspots from 2 objectives\n",
|
||
"Tirisfal Glades 1-3: 2 hotspots from 5 objectives\n",
|
||
"Tirisfal Glades 4-6: 4 hotspots from 11 objectives\n",
|
||
"Tirisfal Glades 7-9: 1 hotspots from 7 objectives\n",
|
||
"Tirisfal Glades 10-12: 2 hotspots from 16 objectives\n",
|
||
"Tirisfal Glades 13-15: 1 hotspots from 3 objectives\n",
|
||
"Westfall 19-21: 1 hotspots from 2 objectives\n",
|
||
"Zephras Isle 1-3: 2 hotspots from 4 objectives\n",
|
||
"Zephras Isle 4-6: 2 hotspots from 9 objectives\n",
|
||
"Zephras Isle 7-9: 1 hotspots from 17 objectives\n",
|
||
"Zephras Isle 10-12: 2 hotspots from 20 objectives\n",
|
||
"Zephras Isle 13-15: 1 hotspots from 3 objectives\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th>center_x</th>\n",
|
||
" <th>center_y</th>\n",
|
||
" <th>quests</th>\n",
|
||
" <th>total_xp</th>\n",
|
||
" <th>avg_to_objective</th>\n",
|
||
" <th>avg_from_objective</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>zone</th>\n",
|
||
" <th>bracket</th>\n",
|
||
" <th>objective_hotspot</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>Ashenvale</th>\n",
|
||
" <th>7: 19-21</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>52.7</td>\n",
|
||
" <td>48.0</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>1450</td>\n",
|
||
" <td>28.7</td>\n",
|
||
" <td>28.7</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"5\" valign=\"top\">Durotar</th>\n",
|
||
" <th>1: 1-3</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>42.9</td>\n",
|
||
" <td>65.1</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>420</td>\n",
|
||
" <td>3.6</td>\n",
|
||
" <td>3.6</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2: 4-6</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>45.6</td>\n",
|
||
" <td>56.6</td>\n",
|
||
" <td>8</td>\n",
|
||
" <td>3310</td>\n",
|
||
" <td>11.0</td>\n",
|
||
" <td>10.4</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3: 7-9</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>58.7</td>\n",
|
||
" <td>65.4</td>\n",
|
||
" <td>6</td>\n",
|
||
" <td>3990</td>\n",
|
||
" <td>16.0</td>\n",
|
||
" <td>16.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">4: 10-12</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>67.5</td>\n",
|
||
" <td>84.8</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>1680</td>\n",
|
||
" <td>15.3</td>\n",
|
||
" <td>15.3</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>47.3</td>\n",
|
||
" <td>27.9</td>\n",
|
||
" <td>11</td>\n",
|
||
" <td>9260</td>\n",
|
||
" <td>15.2</td>\n",
|
||
" <td>15.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"6\" valign=\"top\">Mulgore</th>\n",
|
||
" <th>1: 1-3</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>46.5</td>\n",
|
||
" <td>84.4</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>420</td>\n",
|
||
" <td>7.5</td>\n",
|
||
" <td>7.5</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">2: 4-6</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>45.8</td>\n",
|
||
" <td>57.9</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>1355</td>\n",
|
||
" <td>8.5</td>\n",
|
||
" <td>8.5</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>60.7</td>\n",
|
||
" <td>79.1</td>\n",
|
||
" <td>4</td>\n",
|
||
" <td>1830</td>\n",
|
||
" <td>17.7</td>\n",
|
||
" <td>16.5</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3: 7-9</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>49.7</td>\n",
|
||
" <td>56.3</td>\n",
|
||
" <td>7</td>\n",
|
||
" <td>4840</td>\n",
|
||
" <td>9.6</td>\n",
|
||
" <td>9.6</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">4: 10-12</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>59.4</td>\n",
|
||
" <td>45.5</td>\n",
|
||
" <td>5</td>\n",
|
||
" <td>4370</td>\n",
|
||
" <td>17.5</td>\n",
|
||
" <td>17.7</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>54.6</td>\n",
|
||
" <td>26.5</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>1470</td>\n",
|
||
" <td>19.5</td>\n",
|
||
" <td>4.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"3\" valign=\"top\">Orgrimmar</th>\n",
|
||
" <th>4: 10-12</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>50.9</td>\n",
|
||
" <td>57.4</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>2025</td>\n",
|
||
" <td>22.2</td>\n",
|
||
" <td>22.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>5: 13-15</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>43.8</td>\n",
|
||
" <td>52.4</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>1605</td>\n",
|
||
" <td>10.3</td>\n",
|
||
" <td>10.3</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>7: 19-21</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>49.7</td>\n",
|
||
" <td>50.1</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>2300</td>\n",
|
||
" <td>4.8</td>\n",
|
||
" <td>4.8</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"6\" valign=\"top\">Silverpine Forest</th>\n",
|
||
" <th>4: 10-12</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>54.0</td>\n",
|
||
" <td>27.1</td>\n",
|
||
" <td>5</td>\n",
|
||
" <td>3310</td>\n",
|
||
" <td>12.6</td>\n",
|
||
" <td>13.9</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">5: 13-15</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>50.9</td>\n",
|
||
" <td>34.5</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>3010</td>\n",
|
||
" <td>13.5</td>\n",
|
||
" <td>13.5</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>46.0</td>\n",
|
||
" <td>72.8</td>\n",
|
||
" <td>1</td>\n",
|
||
" <td>1350</td>\n",
|
||
" <td>33.0</td>\n",
|
||
" <td>33.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">6: 16-18</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>68.8</td>\n",
|
||
" <td>28.6</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>3300</td>\n",
|
||
" <td>28.5</td>\n",
|
||
" <td>27.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>56.2</td>\n",
|
||
" <td>69.4</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>2300</td>\n",
|
||
" <td>32.3</td>\n",
|
||
" <td>32.3</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>7: 19-21</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>55.8</td>\n",
|
||
" <td>76.7</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>4350</td>\n",
|
||
" <td>38.9</td>\n",
|
||
" <td>38.9</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"3\" valign=\"top\">Stonetalon Mountains</th>\n",
|
||
" <th>6: 16-18</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>71.3</td>\n",
|
||
" <td>88.9</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>4050</td>\n",
|
||
" <td>29.0</td>\n",
|
||
" <td>29.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">7: 19-21</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>56.2</td>\n",
|
||
" <td>77.9</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>4650</td>\n",
|
||
" <td>20.3</td>\n",
|
||
" <td>22.4</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>66.4</td>\n",
|
||
" <td>52.3</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>3100</td>\n",
|
||
" <td>12.8</td>\n",
|
||
" <td>12.8</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"7\" valign=\"top\">The Barrens</th>\n",
|
||
" <th>4: 10-12</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>52.7</td>\n",
|
||
" <td>27.1</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>1820</td>\n",
|
||
" <td>4.0</td>\n",
|
||
" <td>4.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">5: 13-15</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>52.8</td>\n",
|
||
" <td>42.0</td>\n",
|
||
" <td>5</td>\n",
|
||
" <td>4990</td>\n",
|
||
" <td>10.9</td>\n",
|
||
" <td>10.9</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>50.6</td>\n",
|
||
" <td>19.4</td>\n",
|
||
" <td>7</td>\n",
|
||
" <td>6540</td>\n",
|
||
" <td>7.4</td>\n",
|
||
" <td>7.4</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">6: 16-18</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>49.8</td>\n",
|
||
" <td>16.5</td>\n",
|
||
" <td>9</td>\n",
|
||
" <td>11230</td>\n",
|
||
" <td>12.0</td>\n",
|
||
" <td>12.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>53.0</td>\n",
|
||
" <td>43.4</td>\n",
|
||
" <td>10</td>\n",
|
||
" <td>12600</td>\n",
|
||
" <td>12.2</td>\n",
|
||
" <td>12.3</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">7: 19-21</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>49.6</td>\n",
|
||
" <td>51.8</td>\n",
|
||
" <td>17</td>\n",
|
||
" <td>17450</td>\n",
|
||
" <td>10.6</td>\n",
|
||
" <td>10.7</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>43.8</td>\n",
|
||
" <td>22.3</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>5950</td>\n",
|
||
" <td>9.6</td>\n",
|
||
" <td>9.6</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Thunder Bluff</th>\n",
|
||
" <th>4: 10-12</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>25.1</td>\n",
|
||
" <td>33.4</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>1300</td>\n",
|
||
" <td>24.4</td>\n",
|
||
" <td>24.4</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"10\" valign=\"top\">Tirisfal Glades</th>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">1: 1-3</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>50.1</td>\n",
|
||
" <td>50.3</td>\n",
|
||
" <td>1</td>\n",
|
||
" <td>170</td>\n",
|
||
" <td>24.8</td>\n",
|
||
" <td>24.8</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>32.7</td>\n",
|
||
" <td>62.8</td>\n",
|
||
" <td>4</td>\n",
|
||
" <td>695</td>\n",
|
||
" <td>3.9</td>\n",
|
||
" <td>3.9</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"4\" valign=\"top\">2: 4-6</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>51.7</td>\n",
|
||
" <td>54.4</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>810</td>\n",
|
||
" <td>8.1</td>\n",
|
||
" <td>8.1</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>61.5</td>\n",
|
||
" <td>50.2</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>900</td>\n",
|
||
" <td>14.7</td>\n",
|
||
" <td>3.9</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>35.8</td>\n",
|
||
" <td>66.8</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>1475</td>\n",
|
||
" <td>4.9</td>\n",
|
||
" <td>4.9</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>26.5</td>\n",
|
||
" <td>60.8</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>890</td>\n",
|
||
" <td>7.2</td>\n",
|
||
" <td>7.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3: 7-9</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>51.8</td>\n",
|
||
" <td>48.6</td>\n",
|
||
" <td>7</td>\n",
|
||
" <td>5170</td>\n",
|
||
" <td>14.9</td>\n",
|
||
" <td>14.9</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">4: 10-12</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>15.5</td>\n",
|
||
" <td>57.4</td>\n",
|
||
" <td>4</td>\n",
|
||
" <td>3140</td>\n",
|
||
" <td>13.7</td>\n",
|
||
" <td>13.8</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>67.2</td>\n",
|
||
" <td>47.6</td>\n",
|
||
" <td>12</td>\n",
|
||
" <td>8790</td>\n",
|
||
" <td>15.4</td>\n",
|
||
" <td>18.1</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>5: 13-15</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>48.3</td>\n",
|
||
" <td>44.2</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>2730</td>\n",
|
||
" <td>24.1</td>\n",
|
||
" <td>24.1</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>Westfall</th>\n",
|
||
" <th>7: 19-21</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>32.4</td>\n",
|
||
" <td>62.9</td>\n",
|
||
" <td>2</td>\n",
|
||
" <td>3000</td>\n",
|
||
" <td>23.2</td>\n",
|
||
" <td>23.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"8\" valign=\"top\">Zephras Isle</th>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">1: 1-3</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>46.0</td>\n",
|
||
" <td>27.2</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>565</td>\n",
|
||
" <td>4.2</td>\n",
|
||
" <td>4.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>47.2</td>\n",
|
||
" <td>19.8</td>\n",
|
||
" <td>1</td>\n",
|
||
" <td>250</td>\n",
|
||
" <td>2.0</td>\n",
|
||
" <td>2.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">2: 4-6</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>45.6</td>\n",
|
||
" <td>46.0</td>\n",
|
||
" <td>6</td>\n",
|
||
" <td>1890</td>\n",
|
||
" <td>8.2</td>\n",
|
||
" <td>8.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>37.3</td>\n",
|
||
" <td>28.4</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>1475</td>\n",
|
||
" <td>4.5</td>\n",
|
||
" <td>4.5</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3: 7-9</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>49.4</td>\n",
|
||
" <td>61.2</td>\n",
|
||
" <td>17</td>\n",
|
||
" <td>9945</td>\n",
|
||
" <td>10.7</td>\n",
|
||
" <td>10.8</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th rowspan=\"2\" valign=\"top\">4: 10-12</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>61.3</td>\n",
|
||
" <td>41.2</td>\n",
|
||
" <td>8</td>\n",
|
||
" <td>6840</td>\n",
|
||
" <td>16.4</td>\n",
|
||
" <td>14.1</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>60.0</td>\n",
|
||
" <td>69.3</td>\n",
|
||
" <td>12</td>\n",
|
||
" <td>8220</td>\n",
|
||
" <td>10.0</td>\n",
|
||
" <td>6.2</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>5: 13-15</th>\n",
|
||
" <th>0</th>\n",
|
||
" <td>58.0</td>\n",
|
||
" <td>41.0</td>\n",
|
||
" <td>3</td>\n",
|
||
" <td>1820</td>\n",
|
||
" <td>7.7</td>\n",
|
||
" <td>7.7</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" center_x center_y quests \\\n",
|
||
"zone bracket objective_hotspot \n",
|
||
"Ashenvale 7: 19-21 0 52.7 48.0 3 \n",
|
||
"Durotar 1: 1-3 0 42.9 65.1 2 \n",
|
||
" 2: 4-6 0 45.6 56.6 8 \n",
|
||
" 3: 7-9 0 58.7 65.4 6 \n",
|
||
" 4: 10-12 0 67.5 84.8 2 \n",
|
||
" 1 47.3 27.9 11 \n",
|
||
"Mulgore 1: 1-3 0 46.5 84.4 2 \n",
|
||
" 2: 4-6 0 45.8 57.9 3 \n",
|
||
" 1 60.7 79.1 4 \n",
|
||
" 3: 7-9 0 49.7 56.3 7 \n",
|
||
" 4: 10-12 0 59.4 45.5 5 \n",
|
||
" 1 54.6 26.5 2 \n",
|
||
"Orgrimmar 4: 10-12 0 50.9 57.4 3 \n",
|
||
" 5: 13-15 0 43.8 52.4 2 \n",
|
||
" 7: 19-21 0 49.7 50.1 2 \n",
|
||
"Silverpine Forest 4: 10-12 0 54.0 27.1 5 \n",
|
||
" 5: 13-15 0 50.9 34.5 3 \n",
|
||
" 1 46.0 72.8 1 \n",
|
||
" 6: 16-18 0 68.8 28.6 2 \n",
|
||
" 1 56.2 69.4 2 \n",
|
||
" 7: 19-21 0 55.8 76.7 3 \n",
|
||
"Stonetalon Mountains 6: 16-18 0 71.3 88.9 3 \n",
|
||
" 7: 19-21 0 56.2 77.9 3 \n",
|
||
" 1 66.4 52.3 2 \n",
|
||
"The Barrens 4: 10-12 0 52.7 27.1 2 \n",
|
||
" 5: 13-15 0 52.8 42.0 5 \n",
|
||
" 1 50.6 19.4 7 \n",
|
||
" 6: 16-18 0 49.8 16.5 9 \n",
|
||
" 1 53.0 43.4 10 \n",
|
||
" 7: 19-21 0 49.6 51.8 17 \n",
|
||
" 1 43.8 22.3 3 \n",
|
||
"Thunder Bluff 4: 10-12 0 25.1 33.4 2 \n",
|
||
"Tirisfal Glades 1: 1-3 0 50.1 50.3 1 \n",
|
||
" 1 32.7 62.8 4 \n",
|
||
" 2: 4-6 0 51.7 54.4 2 \n",
|
||
" 1 61.5 50.2 3 \n",
|
||
" 2 35.8 66.8 3 \n",
|
||
" 3 26.5 60.8 3 \n",
|
||
" 3: 7-9 0 51.8 48.6 7 \n",
|
||
" 4: 10-12 0 15.5 57.4 4 \n",
|
||
" 1 67.2 47.6 12 \n",
|
||
" 5: 13-15 0 48.3 44.2 3 \n",
|
||
"Westfall 7: 19-21 0 32.4 62.9 2 \n",
|
||
"Zephras Isle 1: 1-3 0 46.0 27.2 3 \n",
|
||
" 1 47.2 19.8 1 \n",
|
||
" 2: 4-6 0 45.6 46.0 6 \n",
|
||
" 1 37.3 28.4 3 \n",
|
||
" 3: 7-9 0 49.4 61.2 17 \n",
|
||
" 4: 10-12 0 61.3 41.2 8 \n",
|
||
" 1 60.0 69.3 12 \n",
|
||
" 5: 13-15 0 58.0 41.0 3 \n",
|
||
"\n",
|
||
" total_xp avg_to_objective \\\n",
|
||
"zone bracket objective_hotspot \n",
|
||
"Ashenvale 7: 19-21 0 1450 28.7 \n",
|
||
"Durotar 1: 1-3 0 420 3.6 \n",
|
||
" 2: 4-6 0 3310 11.0 \n",
|
||
" 3: 7-9 0 3990 16.0 \n",
|
||
" 4: 10-12 0 1680 15.3 \n",
|
||
" 1 9260 15.2 \n",
|
||
"Mulgore 1: 1-3 0 420 7.5 \n",
|
||
" 2: 4-6 0 1355 8.5 \n",
|
||
" 1 1830 17.7 \n",
|
||
" 3: 7-9 0 4840 9.6 \n",
|
||
" 4: 10-12 0 4370 17.5 \n",
|
||
" 1 1470 19.5 \n",
|
||
"Orgrimmar 4: 10-12 0 2025 22.2 \n",
|
||
" 5: 13-15 0 1605 10.3 \n",
|
||
" 7: 19-21 0 2300 4.8 \n",
|
||
"Silverpine Forest 4: 10-12 0 3310 12.6 \n",
|
||
" 5: 13-15 0 3010 13.5 \n",
|
||
" 1 1350 33.0 \n",
|
||
" 6: 16-18 0 3300 28.5 \n",
|
||
" 1 2300 32.3 \n",
|
||
" 7: 19-21 0 4350 38.9 \n",
|
||
"Stonetalon Mountains 6: 16-18 0 4050 29.0 \n",
|
||
" 7: 19-21 0 4650 20.3 \n",
|
||
" 1 3100 12.8 \n",
|
||
"The Barrens 4: 10-12 0 1820 4.0 \n",
|
||
" 5: 13-15 0 4990 10.9 \n",
|
||
" 1 6540 7.4 \n",
|
||
" 6: 16-18 0 11230 12.0 \n",
|
||
" 1 12600 12.2 \n",
|
||
" 7: 19-21 0 17450 10.6 \n",
|
||
" 1 5950 9.6 \n",
|
||
"Thunder Bluff 4: 10-12 0 1300 24.4 \n",
|
||
"Tirisfal Glades 1: 1-3 0 170 24.8 \n",
|
||
" 1 695 3.9 \n",
|
||
" 2: 4-6 0 810 8.1 \n",
|
||
" 1 900 14.7 \n",
|
||
" 2 1475 4.9 \n",
|
||
" 3 890 7.2 \n",
|
||
" 3: 7-9 0 5170 14.9 \n",
|
||
" 4: 10-12 0 3140 13.7 \n",
|
||
" 1 8790 15.4 \n",
|
||
" 5: 13-15 0 2730 24.1 \n",
|
||
"Westfall 7: 19-21 0 3000 23.2 \n",
|
||
"Zephras Isle 1: 1-3 0 565 4.2 \n",
|
||
" 1 250 2.0 \n",
|
||
" 2: 4-6 0 1890 8.2 \n",
|
||
" 1 1475 4.5 \n",
|
||
" 3: 7-9 0 9945 10.7 \n",
|
||
" 4: 10-12 0 6840 16.4 \n",
|
||
" 1 8220 10.0 \n",
|
||
" 5: 13-15 0 1820 7.7 \n",
|
||
"\n",
|
||
" avg_from_objective \n",
|
||
"zone bracket objective_hotspot \n",
|
||
"Ashenvale 7: 19-21 0 28.7 \n",
|
||
"Durotar 1: 1-3 0 3.6 \n",
|
||
" 2: 4-6 0 10.4 \n",
|
||
" 3: 7-9 0 16.0 \n",
|
||
" 4: 10-12 0 15.3 \n",
|
||
" 1 15.2 \n",
|
||
"Mulgore 1: 1-3 0 7.5 \n",
|
||
" 2: 4-6 0 8.5 \n",
|
||
" 1 16.5 \n",
|
||
" 3: 7-9 0 9.6 \n",
|
||
" 4: 10-12 0 17.7 \n",
|
||
" 1 4.2 \n",
|
||
"Orgrimmar 4: 10-12 0 22.2 \n",
|
||
" 5: 13-15 0 10.3 \n",
|
||
" 7: 19-21 0 4.8 \n",
|
||
"Silverpine Forest 4: 10-12 0 13.9 \n",
|
||
" 5: 13-15 0 13.5 \n",
|
||
" 1 33.0 \n",
|
||
" 6: 16-18 0 27.2 \n",
|
||
" 1 32.3 \n",
|
||
" 7: 19-21 0 38.9 \n",
|
||
"Stonetalon Mountains 6: 16-18 0 29.0 \n",
|
||
" 7: 19-21 0 22.4 \n",
|
||
" 1 12.8 \n",
|
||
"The Barrens 4: 10-12 0 4.0 \n",
|
||
" 5: 13-15 0 10.9 \n",
|
||
" 1 7.4 \n",
|
||
" 6: 16-18 0 12.0 \n",
|
||
" 1 12.3 \n",
|
||
" 7: 19-21 0 10.7 \n",
|
||
" 1 9.6 \n",
|
||
"Thunder Bluff 4: 10-12 0 24.4 \n",
|
||
"Tirisfal Glades 1: 1-3 0 24.8 \n",
|
||
" 1 3.9 \n",
|
||
" 2: 4-6 0 8.1 \n",
|
||
" 1 3.9 \n",
|
||
" 2 4.9 \n",
|
||
" 3 7.2 \n",
|
||
" 3: 7-9 0 14.9 \n",
|
||
" 4: 10-12 0 13.8 \n",
|
||
" 1 18.1 \n",
|
||
" 5: 13-15 0 24.1 \n",
|
||
"Westfall 7: 19-21 0 23.2 \n",
|
||
"Zephras Isle 1: 1-3 0 4.2 \n",
|
||
" 1 2.0 \n",
|
||
" 2: 4-6 0 8.2 \n",
|
||
" 1 4.5 \n",
|
||
" 3: 7-9 0 10.8 \n",
|
||
" 4: 10-12 0 14.1 \n",
|
||
" 1 6.2 \n",
|
||
" 5: 13-15 0 7.7 "
|
||
]
|
||
},
|
||
"execution_count": 94,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Afterwards, we use X-means clustering to find hotspots for quest objective locations.\n",
|
||
"# X-means = k-means that keeps splitting clusters in two while the split improves the\n",
|
||
"# BIC score (Pelleg & Moore, 2000), so we don't have to pick k ourselves.\n",
|
||
"\n",
|
||
"def kmeans(points: np.ndarray, centers: np.ndarray, iterations: int = 100):\n",
|
||
" for _ in range(iterations):\n",
|
||
" labels = np.argmin(np.linalg.norm(points[:, None] - centers[None], axis=2), axis=1)\n",
|
||
" new_centers = np.array([\n",
|
||
" points[labels == k].mean(axis=0) if np.any(labels == k) else centers[k]\n",
|
||
" for k in range(len(centers))\n",
|
||
" ])\n",
|
||
" if np.allclose(new_centers, centers):\n",
|
||
" break\n",
|
||
" centers = new_centers\n",
|
||
" return labels, centers\n",
|
||
"\n",
|
||
"def bic(points: np.ndarray, labels: np.ndarray, centers: np.ndarray, min_variance: float = 1.25) -> float:\n",
|
||
" r, m = points.shape\n",
|
||
" k = len(centers)\n",
|
||
" if r <= k:\n",
|
||
" return -np.inf\n",
|
||
" # Many quests share the exact same objective coord, so without a floor a tiny cluster gets ~0 variance and an infinite likelihood. 1.0 = roughly 1% of the map.\n",
|
||
" variance = max(np.sum((points - centers[labels]) ** 2) / (m * (r - k)), min_variance)\n",
|
||
" log_likelihood = 0.0\n",
|
||
" for i in range(k):\n",
|
||
" r_i = np.sum(labels == i)\n",
|
||
" if r_i == 0:\n",
|
||
" continue\n",
|
||
" log_likelihood += (r_i * np.log(r_i) - r_i * np.log(r)\n",
|
||
" - r_i * m / 2 * np.log(2 * np.pi * variance)\n",
|
||
" - (r_i - 1) * m / 2)\n",
|
||
" free_params = k * (m + 1)\n",
|
||
" return log_likelihood - free_params / 2 * np.log(r)\n",
|
||
"\n",
|
||
"def best_split(members: np.ndarray, rng, attempts: int):\n",
|
||
" # k-means only finds a local optimum, so try a few random starts and keep the best split\n",
|
||
" best_score, best_children = -np.inf, None\n",
|
||
" for _ in range(attempts):\n",
|
||
" child_labels, children = kmeans(members, members[rng.choice(len(members), 2, replace=False)])\n",
|
||
" score = bic(members, child_labels, children)\n",
|
||
" if score > best_score:\n",
|
||
" best_score, best_children = score, children\n",
|
||
" return best_score, best_children\n",
|
||
"\n",
|
||
"def xmeans(points: np.ndarray, k_max: int = 15, split_attempts: int = 10, seed: int = 0):\n",
|
||
" rng = np.random.default_rng(seed)\n",
|
||
" labels, centers = kmeans(points, points.mean(axis=0, keepdims=True))\n",
|
||
" score = bic(points, labels, centers)\n",
|
||
" while True:\n",
|
||
" new_centers = []\n",
|
||
" for k, center in enumerate(centers):\n",
|
||
" members = points[labels == k]\n",
|
||
" if len(members) < 4:\n",
|
||
" new_centers.append(center)\n",
|
||
" continue\n",
|
||
" parent_score = bic(members, np.zeros(len(members), dtype=int), center[None])\n",
|
||
" child_score, children = best_split(members, rng, split_attempts)\n",
|
||
" new_centers.extend(children if child_score > parent_score else [center])\n",
|
||
" if len(new_centers) == len(centers) or len(new_centers) > k_max:\n",
|
||
" return labels, centers\n",
|
||
" # The per-cluster test is lenient on small clusters, so only keep the splits if they also improve the BIC of the whole zone\n",
|
||
" new_labels, new_centers = kmeans(points, np.array(new_centers))\n",
|
||
" new_score = bic(points, new_labels, new_centers)\n",
|
||
" if new_score <= score:\n",
|
||
" return labels, centers\n",
|
||
" labels, centers, score = new_labels, new_centers, new_score\n",
|
||
"\n",
|
||
"# Cluster each zone + bracket's objective areas separately, then classify every questin the travel data by the hotspot its objective falls in.\n",
|
||
"# Hotspot ids are only unique within a zone + bracket.\n",
|
||
"travel_df[\"objective_hotspot\"] = pd.NA\n",
|
||
"travel_df[\"hotspot_distance\"] = np.nan\n",
|
||
"hotspots = {}\n",
|
||
"\n",
|
||
"for (zone_name, bracket), bracket_travel in travel_df.groupby([\"zone\", \"bracket\"]):\n",
|
||
" has_objective = bracket_travel.dropna(subset=[\"objective_x\", \"objective_y\"])\n",
|
||
" points = has_objective[[\"objective_x\", \"objective_y\"]].to_numpy()\n",
|
||
" if len(points) < 2:\n",
|
||
" continue\n",
|
||
" labels, centers = xmeans(points)\n",
|
||
" hotspots[(zone_name, bracket)] = centers\n",
|
||
" travel_df.loc[has_objective.index, \"objective_hotspot\"] = labels\n",
|
||
" travel_df.loc[has_objective.index, \"hotspot_distance\"] = np.linalg.norm(points - centers[labels], axis=1)\n",
|
||
" print(f\"{zone_name} {strip_bracket_string(bracket)}: {len(centers)} hotspots from {len(points)} objectives\")\n",
|
||
"\n",
|
||
"hotspot_summary = travel_df.dropna(subset=[\"objective_hotspot\"]).groupby([\"zone\", \"bracket\", \"objective_hotspot\"]).agg(\n",
|
||
" center_x=(\"objective_x\", \"mean\"),\n",
|
||
" center_y=(\"objective_y\", \"mean\"),\n",
|
||
" quests=(\"id\", \"count\"),\n",
|
||
" total_xp=(\"xp\", \"sum\"),\n",
|
||
" avg_to_objective=(\"to_objective\", \"mean\"),\n",
|
||
" avg_from_objective=(\"from_objective\", \"mean\"),\n",
|
||
")\n",
|
||
"hotspot_summary.round(1)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 95,
|
||
"id": "64996499",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/vnd.plotly.v1+json": {
|
||
"data": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"thickness": 12,
|
||
"title": {
|
||
"text": "Total XP"
|
||
}
|
||
},
|
||
"colorscale": [
|
||
[
|
||
0,
|
||
"#cde2fb"
|
||
],
|
||
[
|
||
0.16666666666666666,
|
||
"#9ec5f4"
|
||
],
|
||
[
|
||
0.3333333333333333,
|
||
"#6da7ec"
|
||
],
|
||
[
|
||
0.5,
|
||
"#3987e5"
|
||
],
|
||
[
|
||
0.6666666666666666,
|
||
"#256abf"
|
||
],
|
||
[
|
||
0.8333333333333334,
|
||
"#184f95"
|
||
],
|
||
[
|
||
1,
|
||
"#0d366b"
|
||
]
|
||
],
|
||
"hoverinfo": "skip",
|
||
"text": [
|
||
[
|
||
"<b>1,165 XP</b><br>11 quests<br>avg 106",
|
||
"<b>10,420 XP</b><br>32 quests<br>avg 326",
|
||
"<b>8,725 XP</b><br>17 quests<br>avg 513",
|
||
"<b>16,585 XP</b><br>29 quests<br>avg 572",
|
||
"<b>2,120 XP</b><br>4 quests<br>avg 530",
|
||
"",
|
||
""
|
||
],
|
||
[
|
||
"<b>1,470 XP</b><br>15 quests<br>avg 98",
|
||
"<b>9,940 XP</b><br>32 quests<br>avg 311",
|
||
"<b>8,805 XP</b><br>16 quests<br>avg 550",
|
||
"<b>24,840 XP</b><br>45 quests<br>avg 552",
|
||
"<b>2,835 XP</b><br>4 quests<br>avg 709",
|
||
"",
|
||
""
|
||
],
|
||
[
|
||
"<b>1,540 XP</b><br>20 quests<br>avg 77",
|
||
"<b>10,525 XP</b><br>35 quests<br>avg 301",
|
||
"<b>12,045 XP</b><br>19 quests<br>avg 634",
|
||
"<b>21,430 XP</b><br>38 quests<br>avg 564",
|
||
"<b>4,005 XP</b><br>6 quests<br>avg 668",
|
||
"",
|
||
""
|
||
],
|
||
[
|
||
"<b>1,475 XP</b><br>13 quests<br>avg 113",
|
||
"<b>8,780 XP</b><br>30 quests<br>avg 293",
|
||
"<b>15,090 XP</b><br>30 quests<br>avg 503",
|
||
"<b>25,465 XP</b><br>41 quests<br>avg 621",
|
||
"<b>3,045 XP</b><br>7 quests<br>avg 435",
|
||
"",
|
||
""
|
||
]
|
||
],
|
||
"textfont": {
|
||
"size": 12
|
||
},
|
||
"texttemplate": "%{text}",
|
||
"type": "heatmap",
|
||
"x": [
|
||
"1-3",
|
||
"4-6",
|
||
"7-9",
|
||
"10-12",
|
||
"13-15",
|
||
"16-18",
|
||
"19-21"
|
||
],
|
||
"xgap": 2,
|
||
"y": [
|
||
"Mulgore",
|
||
"Tirisfal Glades",
|
||
"Durotar",
|
||
"Zephras Isle"
|
||
],
|
||
"ygap": 2,
|
||
"z": {
|
||
"bdata": "AAAAAAA0kkAAAAAAAFrEQAAAAACACsFAAAAAAEAy0EAAAAAAAJCgQAAAAAAAAPh/AAAAAAAA+H8AAAAAAPiWQAAAAAAAasNAAAAAAIAywUAAAAAAAELYQAAAAAAAJqZAAAAAAAAA+H8AAAAAAAD4fwAAAAAAEJhAAAAAAICOxEAAAAAAgIbHQAAAAACA7dRAAAAAAABKr0AAAAAAAAD4fwAAAAAAAPh/AAAAAAAMl0AAAAAAACbBQAAAAAAAec1AAAAAAEDe2EAAAAAAAMqnQAAAAAAAAPh/AAAAAAAA+H8=",
|
||
"dtype": "f8",
|
||
"shape": "4, 7"
|
||
}
|
||
}
|
||
],
|
||
"layout": {
|
||
"font": {
|
||
"color": "#0b0b0b",
|
||
"family": "system-ui, -apple-system, Segoe UI, sans-serif",
|
||
"size": 12
|
||
},
|
||
"height": 480,
|
||
"margin": {
|
||
"b": 40,
|
||
"l": 10,
|
||
"r": 10,
|
||
"t": 60
|
||
},
|
||
"paper_bgcolor": "#fcfcfb",
|
||
"plot_bgcolor": "#fcfcfb",
|
||
"template": {
|
||
"data": {
|
||
"bar": [
|
||
{
|
||
"error_x": {
|
||
"color": "#2a3f5f"
|
||
},
|
||
"error_y": {
|
||
"color": "#2a3f5f"
|
||
},
|
||
"marker": {
|
||
"line": {
|
||
"color": "white",
|
||
"width": 0.5
|
||
},
|
||
"pattern": {
|
||
"fillmode": "overlay",
|
||
"size": 10,
|
||
"solidity": 0.2
|
||
}
|
||
},
|
||
"type": "bar"
|
||
}
|
||
],
|
||
"barpolar": [
|
||
{
|
||
"marker": {
|
||
"line": {
|
||
"color": "white",
|
||
"width": 0.5
|
||
},
|
||
"pattern": {
|
||
"fillmode": "overlay",
|
||
"size": 10,
|
||
"solidity": 0.2
|
||
}
|
||
},
|
||
"type": "barpolar"
|
||
}
|
||
],
|
||
"carpet": [
|
||
{
|
||
"aaxis": {
|
||
"endlinecolor": "#2a3f5f",
|
||
"gridcolor": "#C8D4E3",
|
||
"linecolor": "#C8D4E3",
|
||
"minorgridcolor": "#C8D4E3",
|
||
"startlinecolor": "#2a3f5f"
|
||
},
|
||
"baxis": {
|
||
"endlinecolor": "#2a3f5f",
|
||
"gridcolor": "#C8D4E3",
|
||
"linecolor": "#C8D4E3",
|
||
"minorgridcolor": "#C8D4E3",
|
||
"startlinecolor": "#2a3f5f"
|
||
},
|
||
"type": "carpet"
|
||
}
|
||
],
|
||
"choropleth": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
},
|
||
"type": "choropleth"
|
||
}
|
||
],
|
||
"contour": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
},
|
||
"colorscale": [
|
||
[
|
||
0,
|
||
"#0d0887"
|
||
],
|
||
[
|
||
0.1111111111111111,
|
||
"#46039f"
|
||
],
|
||
[
|
||
0.2222222222222222,
|
||
"#7201a8"
|
||
],
|
||
[
|
||
0.3333333333333333,
|
||
"#9c179e"
|
||
],
|
||
[
|
||
0.4444444444444444,
|
||
"#bd3786"
|
||
],
|
||
[
|
||
0.5555555555555556,
|
||
"#d8576b"
|
||
],
|
||
[
|
||
0.6666666666666666,
|
||
"#ed7953"
|
||
],
|
||
[
|
||
0.7777777777777778,
|
||
"#fb9f3a"
|
||
],
|
||
[
|
||
0.8888888888888888,
|
||
"#fdca26"
|
||
],
|
||
[
|
||
1,
|
||
"#f0f921"
|
||
]
|
||
],
|
||
"type": "contour"
|
||
}
|
||
],
|
||
"contourcarpet": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
},
|
||
"type": "contourcarpet"
|
||
}
|
||
],
|
||
"heatmap": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
},
|
||
"colorscale": [
|
||
[
|
||
0,
|
||
"#0d0887"
|
||
],
|
||
[
|
||
0.1111111111111111,
|
||
"#46039f"
|
||
],
|
||
[
|
||
0.2222222222222222,
|
||
"#7201a8"
|
||
],
|
||
[
|
||
0.3333333333333333,
|
||
"#9c179e"
|
||
],
|
||
[
|
||
0.4444444444444444,
|
||
"#bd3786"
|
||
],
|
||
[
|
||
0.5555555555555556,
|
||
"#d8576b"
|
||
],
|
||
[
|
||
0.6666666666666666,
|
||
"#ed7953"
|
||
],
|
||
[
|
||
0.7777777777777778,
|
||
"#fb9f3a"
|
||
],
|
||
[
|
||
0.8888888888888888,
|
||
"#fdca26"
|
||
],
|
||
[
|
||
1,
|
||
"#f0f921"
|
||
]
|
||
],
|
||
"type": "heatmap"
|
||
}
|
||
],
|
||
"histogram": [
|
||
{
|
||
"marker": {
|
||
"pattern": {
|
||
"fillmode": "overlay",
|
||
"size": 10,
|
||
"solidity": 0.2
|
||
}
|
||
},
|
||
"type": "histogram"
|
||
}
|
||
],
|
||
"histogram2d": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
},
|
||
"colorscale": [
|
||
[
|
||
0,
|
||
"#0d0887"
|
||
],
|
||
[
|
||
0.1111111111111111,
|
||
"#46039f"
|
||
],
|
||
[
|
||
0.2222222222222222,
|
||
"#7201a8"
|
||
],
|
||
[
|
||
0.3333333333333333,
|
||
"#9c179e"
|
||
],
|
||
[
|
||
0.4444444444444444,
|
||
"#bd3786"
|
||
],
|
||
[
|
||
0.5555555555555556,
|
||
"#d8576b"
|
||
],
|
||
[
|
||
0.6666666666666666,
|
||
"#ed7953"
|
||
],
|
||
[
|
||
0.7777777777777778,
|
||
"#fb9f3a"
|
||
],
|
||
[
|
||
0.8888888888888888,
|
||
"#fdca26"
|
||
],
|
||
[
|
||
1,
|
||
"#f0f921"
|
||
]
|
||
],
|
||
"type": "histogram2d"
|
||
}
|
||
],
|
||
"histogram2dcontour": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
},
|
||
"colorscale": [
|
||
[
|
||
0,
|
||
"#0d0887"
|
||
],
|
||
[
|
||
0.1111111111111111,
|
||
"#46039f"
|
||
],
|
||
[
|
||
0.2222222222222222,
|
||
"#7201a8"
|
||
],
|
||
[
|
||
0.3333333333333333,
|
||
"#9c179e"
|
||
],
|
||
[
|
||
0.4444444444444444,
|
||
"#bd3786"
|
||
],
|
||
[
|
||
0.5555555555555556,
|
||
"#d8576b"
|
||
],
|
||
[
|
||
0.6666666666666666,
|
||
"#ed7953"
|
||
],
|
||
[
|
||
0.7777777777777778,
|
||
"#fb9f3a"
|
||
],
|
||
[
|
||
0.8888888888888888,
|
||
"#fdca26"
|
||
],
|
||
[
|
||
1,
|
||
"#f0f921"
|
||
]
|
||
],
|
||
"type": "histogram2dcontour"
|
||
}
|
||
],
|
||
"mesh3d": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
},
|
||
"type": "mesh3d"
|
||
}
|
||
],
|
||
"parcoords": [
|
||
{
|
||
"line": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "parcoords"
|
||
}
|
||
],
|
||
"pie": [
|
||
{
|
||
"automargin": true,
|
||
"type": "pie"
|
||
}
|
||
],
|
||
"scatter": [
|
||
{
|
||
"fillpattern": {
|
||
"fillmode": "overlay",
|
||
"size": 10,
|
||
"solidity": 0.2
|
||
},
|
||
"type": "scatter"
|
||
}
|
||
],
|
||
"scatter3d": [
|
||
{
|
||
"line": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"marker": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "scatter3d"
|
||
}
|
||
],
|
||
"scattercarpet": [
|
||
{
|
||
"marker": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "scattercarpet"
|
||
}
|
||
],
|
||
"scattergeo": [
|
||
{
|
||
"marker": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "scattergeo"
|
||
}
|
||
],
|
||
"scattergl": [
|
||
{
|
||
"marker": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "scattergl"
|
||
}
|
||
],
|
||
"scattermap": [
|
||
{
|
||
"marker": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "scattermap"
|
||
}
|
||
],
|
||
"scatterpolar": [
|
||
{
|
||
"marker": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "scatterpolar"
|
||
}
|
||
],
|
||
"scatterpolargl": [
|
||
{
|
||
"marker": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "scatterpolargl"
|
||
}
|
||
],
|
||
"scatterternary": [
|
||
{
|
||
"marker": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"type": "scatterternary"
|
||
}
|
||
],
|
||
"surface": [
|
||
{
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
},
|
||
"colorscale": [
|
||
[
|
||
0,
|
||
"#0d0887"
|
||
],
|
||
[
|
||
0.1111111111111111,
|
||
"#46039f"
|
||
],
|
||
[
|
||
0.2222222222222222,
|
||
"#7201a8"
|
||
],
|
||
[
|
||
0.3333333333333333,
|
||
"#9c179e"
|
||
],
|
||
[
|
||
0.4444444444444444,
|
||
"#bd3786"
|
||
],
|
||
[
|
||
0.5555555555555556,
|
||
"#d8576b"
|
||
],
|
||
[
|
||
0.6666666666666666,
|
||
"#ed7953"
|
||
],
|
||
[
|
||
0.7777777777777778,
|
||
"#fb9f3a"
|
||
],
|
||
[
|
||
0.8888888888888888,
|
||
"#fdca26"
|
||
],
|
||
[
|
||
1,
|
||
"#f0f921"
|
||
]
|
||
],
|
||
"type": "surface"
|
||
}
|
||
],
|
||
"table": [
|
||
{
|
||
"cells": {
|
||
"fill": {
|
||
"color": "#EBF0F8"
|
||
},
|
||
"line": {
|
||
"color": "white"
|
||
}
|
||
},
|
||
"header": {
|
||
"fill": {
|
||
"color": "#C8D4E3"
|
||
},
|
||
"line": {
|
||
"color": "white"
|
||
}
|
||
},
|
||
"type": "table"
|
||
}
|
||
]
|
||
},
|
||
"layout": {
|
||
"annotationdefaults": {
|
||
"arrowcolor": "#2a3f5f",
|
||
"arrowhead": 0,
|
||
"arrowwidth": 1
|
||
},
|
||
"autotypenumbers": "strict",
|
||
"coloraxis": {
|
||
"colorbar": {
|
||
"outlinewidth": 0,
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"colorscale": {
|
||
"diverging": [
|
||
[
|
||
0,
|
||
"#8e0152"
|
||
],
|
||
[
|
||
0.1,
|
||
"#c51b7d"
|
||
],
|
||
[
|
||
0.2,
|
||
"#de77ae"
|
||
],
|
||
[
|
||
0.3,
|
||
"#f1b6da"
|
||
],
|
||
[
|
||
0.4,
|
||
"#fde0ef"
|
||
],
|
||
[
|
||
0.5,
|
||
"#f7f7f7"
|
||
],
|
||
[
|
||
0.6,
|
||
"#e6f5d0"
|
||
],
|
||
[
|
||
0.7,
|
||
"#b8e186"
|
||
],
|
||
[
|
||
0.8,
|
||
"#7fbc41"
|
||
],
|
||
[
|
||
0.9,
|
||
"#4d9221"
|
||
],
|
||
[
|
||
1,
|
||
"#276419"
|
||
]
|
||
],
|
||
"sequential": [
|
||
[
|
||
0,
|
||
"#0d0887"
|
||
],
|
||
[
|
||
0.1111111111111111,
|
||
"#46039f"
|
||
],
|
||
[
|
||
0.2222222222222222,
|
||
"#7201a8"
|
||
],
|
||
[
|
||
0.3333333333333333,
|
||
"#9c179e"
|
||
],
|
||
[
|
||
0.4444444444444444,
|
||
"#bd3786"
|
||
],
|
||
[
|
||
0.5555555555555556,
|
||
"#d8576b"
|
||
],
|
||
[
|
||
0.6666666666666666,
|
||
"#ed7953"
|
||
],
|
||
[
|
||
0.7777777777777778,
|
||
"#fb9f3a"
|
||
],
|
||
[
|
||
0.8888888888888888,
|
||
"#fdca26"
|
||
],
|
||
[
|
||
1,
|
||
"#f0f921"
|
||
]
|
||
],
|
||
"sequentialminus": [
|
||
[
|
||
0,
|
||
"#0d0887"
|
||
],
|
||
[
|
||
0.1111111111111111,
|
||
"#46039f"
|
||
],
|
||
[
|
||
0.2222222222222222,
|
||
"#7201a8"
|
||
],
|
||
[
|
||
0.3333333333333333,
|
||
"#9c179e"
|
||
],
|
||
[
|
||
0.4444444444444444,
|
||
"#bd3786"
|
||
],
|
||
[
|
||
0.5555555555555556,
|
||
"#d8576b"
|
||
],
|
||
[
|
||
0.6666666666666666,
|
||
"#ed7953"
|
||
],
|
||
[
|
||
0.7777777777777778,
|
||
"#fb9f3a"
|
||
],
|
||
[
|
||
0.8888888888888888,
|
||
"#fdca26"
|
||
],
|
||
[
|
||
1,
|
||
"#f0f921"
|
||
]
|
||
]
|
||
},
|
||
"colorway": [
|
||
"#636efa",
|
||
"#EF553B",
|
||
"#00cc96",
|
||
"#ab63fa",
|
||
"#FFA15A",
|
||
"#19d3f3",
|
||
"#FF6692",
|
||
"#B6E880",
|
||
"#FF97FF",
|
||
"#FECB52"
|
||
],
|
||
"font": {
|
||
"color": "#2a3f5f"
|
||
},
|
||
"geo": {
|
||
"bgcolor": "white",
|
||
"lakecolor": "white",
|
||
"landcolor": "white",
|
||
"showlakes": true,
|
||
"showland": true,
|
||
"subunitcolor": "#C8D4E3"
|
||
},
|
||
"hoverlabel": {
|
||
"align": "left"
|
||
},
|
||
"hovermode": "closest",
|
||
"paper_bgcolor": "white",
|
||
"plot_bgcolor": "white",
|
||
"polar": {
|
||
"angularaxis": {
|
||
"gridcolor": "#EBF0F8",
|
||
"linecolor": "#EBF0F8",
|
||
"ticks": ""
|
||
},
|
||
"bgcolor": "white",
|
||
"radialaxis": {
|
||
"gridcolor": "#EBF0F8",
|
||
"linecolor": "#EBF0F8",
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"scene": {
|
||
"xaxis": {
|
||
"backgroundcolor": "white",
|
||
"gridcolor": "#DFE8F3",
|
||
"gridwidth": 2,
|
||
"linecolor": "#EBF0F8",
|
||
"showbackground": true,
|
||
"ticks": "",
|
||
"zerolinecolor": "#EBF0F8"
|
||
},
|
||
"yaxis": {
|
||
"backgroundcolor": "white",
|
||
"gridcolor": "#DFE8F3",
|
||
"gridwidth": 2,
|
||
"linecolor": "#EBF0F8",
|
||
"showbackground": true,
|
||
"ticks": "",
|
||
"zerolinecolor": "#EBF0F8"
|
||
},
|
||
"zaxis": {
|
||
"backgroundcolor": "white",
|
||
"gridcolor": "#DFE8F3",
|
||
"gridwidth": 2,
|
||
"linecolor": "#EBF0F8",
|
||
"showbackground": true,
|
||
"ticks": "",
|
||
"zerolinecolor": "#EBF0F8"
|
||
}
|
||
},
|
||
"shapedefaults": {
|
||
"line": {
|
||
"color": "#2a3f5f"
|
||
}
|
||
},
|
||
"ternary": {
|
||
"aaxis": {
|
||
"gridcolor": "#DFE8F3",
|
||
"linecolor": "#A2B1C6",
|
||
"ticks": ""
|
||
},
|
||
"baxis": {
|
||
"gridcolor": "#DFE8F3",
|
||
"linecolor": "#A2B1C6",
|
||
"ticks": ""
|
||
},
|
||
"bgcolor": "white",
|
||
"caxis": {
|
||
"gridcolor": "#DFE8F3",
|
||
"linecolor": "#A2B1C6",
|
||
"ticks": ""
|
||
}
|
||
},
|
||
"title": {
|
||
"x": 0.05
|
||
},
|
||
"xaxis": {
|
||
"automargin": true,
|
||
"gridcolor": "#EBF0F8",
|
||
"linecolor": "#EBF0F8",
|
||
"ticks": "",
|
||
"title": {
|
||
"standoff": 15
|
||
},
|
||
"zerolinecolor": "#EBF0F8",
|
||
"zerolinewidth": 2
|
||
},
|
||
"yaxis": {
|
||
"automargin": true,
|
||
"gridcolor": "#EBF0F8",
|
||
"linecolor": "#EBF0F8",
|
||
"ticks": "",
|
||
"title": {
|
||
"standoff": 15
|
||
},
|
||
"zerolinecolor": "#EBF0F8",
|
||
"zerolinewidth": 2
|
||
}
|
||
}
|
||
},
|
||
"title": {
|
||
"text": "Quest XP by zone and level bracket (Horde)"
|
||
},
|
||
"xaxis": {
|
||
"showgrid": false,
|
||
"side": "top",
|
||
"tickfont": {
|
||
"color": "#898781"
|
||
},
|
||
"type": "category"
|
||
},
|
||
"yaxis": {
|
||
"showgrid": false,
|
||
"tickfont": {
|
||
"color": "#898781"
|
||
},
|
||
"type": "category"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Chart the XP summary as a heatmap of total XP per zone + bracket, with each cell's stats printed on it.\n",
|
||
"import plotly.graph_objects as go\n",
|
||
"\n",
|
||
"SURFACE = \"#fcfcfb\"\n",
|
||
"INK = \"#0b0b0b\"\n",
|
||
"MUTED = \"#898781\"\n",
|
||
"# Single-hue sequential ramp, light = little XP, dark = lots\n",
|
||
"XP_RAMP = [\"#cde2fb\", \"#9ec5f4\", \"#6da7ec\", \"#3987e5\", \"#256abf\", \"#184f95\", \"#0d366b\"]\n",
|
||
"\n",
|
||
"chart_layout = dict(\n",
|
||
" template=\"plotly_white\",\n",
|
||
" paper_bgcolor=SURFACE,\n",
|
||
" plot_bgcolor=SURFACE,\n",
|
||
" font=dict(family=\"system-ui, -apple-system, Segoe UI, sans-serif\", color=INK, size=12),\n",
|
||
" margin=dict(l=10, r=10, t=60, b=40),\n",
|
||
")\n",
|
||
"\n",
|
||
"chart_df = xp_summary.copy()\n",
|
||
"# Bracket keys look like \"1: 1-3\", the prefix only exists to sort them, so show just the level range\n",
|
||
"bracket_order = sorted(chart_df[\"bracket\"].unique())\n",
|
||
"bracket_labels = [strip_bracket_string(b) for b in bracket_order]\n",
|
||
"\n",
|
||
"target_zones = [\"Durotar\", \"Mulgore\", \"Tirisfal Glades\", \"Zephras Isle\"]\n",
|
||
"chart_df = chart_df[chart_df[\"zone\"].isin(target_zones)].copy()\n",
|
||
"# Ascending so the zone with the most XP ends up at the top of the chart\n",
|
||
"zone_totals = chart_df.groupby(\"zone\")[\"total_xp\"].sum().sort_values()\n",
|
||
"\n",
|
||
"def zone_grid(column):\n",
|
||
" return chart_df.pivot(index=\"zone\", columns=\"bracket\", values=column).reindex(index=zone_totals.index, columns=bracket_order)\n",
|
||
"\n",
|
||
"quest_grid = zone_grid(\"quests\")\n",
|
||
"avg_grid = zone_grid(\"avg_xp\")\n",
|
||
"# Brackets with no quests (e.g. the Zephras Isle placeholder) are left blank rather than shaded as 0 XP\n",
|
||
"xp_grid = zone_grid(\"total_xp\").where(quest_grid > 0)\n",
|
||
"\n",
|
||
"def cell_label(xp, quests, avg):\n",
|
||
" if pd.isna(quests) or quests == 0:\n",
|
||
" return \"\"\n",
|
||
" return f\"<b>{xp:,.0f} XP</b><br>{quests:.0f} quests<br>avg {avg:,.0f}\"\n",
|
||
"\n",
|
||
"# Stats go on the cells themselves instead of a tooltip, so the chart reads in a screenshot\n",
|
||
"cell_text = [\n",
|
||
" [cell_label(xp, quests, avg) for xp, quests, avg in zip(xp_row, quest_row, avg_row)]\n",
|
||
" for xp_row, quest_row, avg_row in zip(xp_grid.to_numpy(), quest_grid.to_numpy(), avg_grid.to_numpy())\n",
|
||
"]\n",
|
||
"\n",
|
||
"heatmap = go.Figure(go.Heatmap(\n",
|
||
" z=xp_grid.to_numpy(),\n",
|
||
" x=bracket_labels,\n",
|
||
" y=xp_grid.index,\n",
|
||
" text=cell_text,\n",
|
||
" texttemplate=\"%{text}\",\n",
|
||
" textfont=dict(size=12),\n",
|
||
" hoverinfo=\"skip\",\n",
|
||
" colorscale=[[i / (len(XP_RAMP) - 1), c] for i, c in enumerate(XP_RAMP)],\n",
|
||
" xgap=2, ygap=2,\n",
|
||
" colorbar=dict(title=\"Total XP\", outlinewidth=0, thickness=12),\n",
|
||
"))\n",
|
||
"heatmap.update_layout(\n",
|
||
" **chart_layout,\n",
|
||
" title=\"Quest XP by zone and level bracket (Horde)\",\n",
|
||
" # Rows need room for three lines of text\n",
|
||
" height=90 * len(xp_grid) + 120,\n",
|
||
" # Without type=\"category\", plotly reads labels like \"1-3\" as dates\n",
|
||
" xaxis=dict(type=\"category\", side=\"top\", showgrid=False, tickfont_color=MUTED),\n",
|
||
" yaxis=dict(type=\"category\", showgrid=False, tickfont_color=MUTED),\n",
|
||
")\n",
|
||
"heatmap.show()"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": ".venv (3.14.6)",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.14.6"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|