{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "37e8cfe9",
   "metadata": {},
   "source": [
    "# Tension Board 2: Climbing Statistics\n",
    "This data was downloaded using [Boardlib](https://github.com/lemeryfertitta/BoardLib). It was downloaded in March 2026, but (most of) the data was actually updated 2026-01-22 (as can be seen in the `kits` table of the database). As such, we only take data into account up to this date. Moreover, we only have data about the climbs, with no specific user data. That said, we can still use climb features like `ascensionist_count` and `fa_at` (the first ascent date for the climb) to understand climbing trends.\n",
    "\n",
    "We've explored the database using SQL in [sql/01_data_exploration.sql](../sql/01_data_exploration.sql). We'll now use some of our findings to visualize some data and give some statistics. We'll begin by loading the necessary data. We will also include ways to do similary queries in SQL vs pandas (although, we still do our initial query in SQL); these can be found in [sql/02_alternative_queries.sql](../sql/02_alternative_queries.sql).\n",
    "\n",
    "\n",
    "## Understanding the data\n",
    "\n",
    "## Notebook Structure\n",
    "1. [Setup and Imports](#setup-and-imports)\n",
    "2. [Popularity](#popularity)\n",
    "3. [Climbing Statistics: Grades, Angles, Quality, and \"Match\" vs. \"No Match\"](#climbing-statistics-grades-angles-quality-and-matching)\n",
    "4. [Prolific Statistics](#prolific-statistics)\n",
    "5. [Conclusion](#conclusion)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "898cad20",
   "metadata": {},
   "source": [
    "## Setup and Imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "e48e2d25",
   "metadata": {},
   "outputs": [],
   "source": [
    "### Setup\n",
    "\n",
    "# Imports\n",
    "import pandas as pd\n",
    "import sqlite3\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "import numpy as np\n",
    "\n",
    "import matplotlib.patches as mpatches\n",
    "import sqlite3\n",
    "\n",
    "\n",
    "# Set some display options\n",
    "pd.set_option('display.max_columns', None)\n",
    "pd.set_option('display.max_rows', 100)\n",
    "\n",
    "# Set style\n",
    "palette=['steelblue', 'coral', 'seagreen']  #(for multi-bar graphs)\n",
    "\n",
    "# Connect to the database\n",
    "DB_PATH=\"../data/tb2.db\"\n",
    "conn = sqlite3.connect(DB_PATH)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2e7b5862",
   "metadata": {},
   "outputs": [],
   "source": [
    "### Query our data from the DB\n",
    "\n",
    "# Query climb data\n",
    "climbs_query = \"\"\"\n",
    "SELECT\n",
    "    c.uuid,\n",
    "    c.name AS climb_name,\n",
    "    c.setter_username,\n",
    "    c.layout_id AS layout_id,\n",
    "    c.description,\n",
    "    c.is_nomatch,\n",
    "    c.is_listed,\n",
    "    l.name AS layout_name,\n",
    "    p.name AS board_name,\n",
    "    c.frames,\n",
    "    cs.angle,\n",
    "    cs.display_difficulty,\n",
    "    dg.boulder_name AS boulder_grade,\n",
    "    cs.ascensionist_count,\n",
    "    cs.quality_average,\n",
    "    cs.fa_at\n",
    "    \n",
    "FROM climbs c\n",
    "JOIN layouts l ON c.layout_id = l.id\n",
    "JOIN products p ON l.product_id = p.id\n",
    "JOIN climb_stats cs ON c.uuid = cs.climb_uuid\n",
    "JOIN difficulty_grades dg ON ROUND(cs.display_difficulty) = dg.difficulty\n",
    "WHERE cs.display_difficulty IS NOT NULL AND c.is_listed=1;\n",
    "\"\"\"\n",
    "\n",
    "# Load it into a DataFrame\n",
    "df = pd.read_sql_query(climbs_query, conn)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "70a88be4",
   "metadata": {},
   "source": [
    "The above query will allow us to gather basically anything we need to in order to analyze climbing statistics. We leave out information about climging holds and things like this, because they will be analyzed in a different notebook. Let's see what our DataFrame looks like."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8b0057f5",
   "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>uuid</th>\n",
       "      <th>climb_name</th>\n",
       "      <th>setter_username</th>\n",
       "      <th>layout_id</th>\n",
       "      <th>description</th>\n",
       "      <th>is_nomatch</th>\n",
       "      <th>is_listed</th>\n",
       "      <th>layout_name</th>\n",
       "      <th>board_name</th>\n",
       "      <th>frames</th>\n",
       "      <th>angle</th>\n",
       "      <th>display_difficulty</th>\n",
       "      <th>boulder_grade</th>\n",
       "      <th>ascensionist_count</th>\n",
       "      <th>quality_average</th>\n",
       "      <th>fa_at</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>00163801596af1064d549ad75b684539</td>\n",
       "      <td>Duroxmanie 2.0</td>\n",
       "      <td>vinsewah</td>\n",
       "      <td>9</td>\n",
       "      <td>No matching</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>Original Layout</td>\n",
       "      <td>Tension Board</td>\n",
       "      <td>p3r4p29r2p59r1p65r2p75r3p89r2p157r4p158r4</td>\n",
       "      <td>30</td>\n",
       "      <td>16.3333</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>3</td>\n",
       "      <td>3.00000</td>\n",
       "      <td>2024-07-21 01:37:45</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>00163801596af1064d549ad75b684539</td>\n",
       "      <td>Duroxmanie 2.0</td>\n",
       "      <td>vinsewah</td>\n",
       "      <td>9</td>\n",
       "      <td>No matching</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>Original Layout</td>\n",
       "      <td>Tension Board</td>\n",
       "      <td>p3r4p29r2p59r1p65r2p75r3p89r2p157r4p158r4</td>\n",
       "      <td>35</td>\n",
       "      <td>16.3333</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>3</td>\n",
       "      <td>2.00000</td>\n",
       "      <td>2021-02-17 02:31:28</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>001945feb7509ce231c9d8b237082a39</td>\n",
       "      <td>1 am</td>\n",
       "      <td>ssssss</td>\n",
       "      <td>9</td>\n",
       "      <td></td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>Original Layout</td>\n",
       "      <td>Tension Board</td>\n",
       "      <td>p18r1p22r1p57r2p70r2p149r3p161r4p162r4</td>\n",
       "      <td>20</td>\n",
       "      <td>13.0000</td>\n",
       "      <td>5a/V1</td>\n",
       "      <td>1</td>\n",
       "      <td>2.00000</td>\n",
       "      <td>2021-01-30 21:46:16</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>001945feb7509ce231c9d8b237082a39</td>\n",
       "      <td>1 am</td>\n",
       "      <td>ssssss</td>\n",
       "      <td>9</td>\n",
       "      <td></td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>Original Layout</td>\n",
       "      <td>Tension Board</td>\n",
       "      <td>p18r1p22r1p57r2p70r2p149r3p161r4p162r4</td>\n",
       "      <td>30</td>\n",
       "      <td>14.6250</td>\n",
       "      <td>5c/V2</td>\n",
       "      <td>8</td>\n",
       "      <td>2.87500</td>\n",
       "      <td>2021-04-05 01:29:07</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>001945feb7509ce231c9d8b237082a39</td>\n",
       "      <td>1 am</td>\n",
       "      <td>ssssss</td>\n",
       "      <td>9</td>\n",
       "      <td></td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>Original Layout</td>\n",
       "      <td>Tension Board</td>\n",
       "      <td>p18r1p22r1p57r2p70r2p149r3p161r4p162r4</td>\n",
       "      <td>35</td>\n",
       "      <td>15.0000</td>\n",
       "      <td>5c/V2</td>\n",
       "      <td>7</td>\n",
       "      <td>2.71429</td>\n",
       "      <td>2020-12-15 23:24:57</td>\n",
       "    </tr>\n",
       "    <tr>\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",
       "      <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>143196</th>\n",
       "      <td>9A09A2F4F82C46EFA8F8309DF263E878</td>\n",
       "      <td>nemesis 1</td>\n",
       "      <td>Hibiki0108</td>\n",
       "      <td>10</td>\n",
       "      <td>No matching.</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>Tension Board 2 Mirror</td>\n",
       "      <td>Tension Board 2</td>\n",
       "      <td>p339r6p347r6p412r6p583r5p664r7p713r8</td>\n",
       "      <td>50</td>\n",
       "      <td>24.0000</td>\n",
       "      <td>7b/V8</td>\n",
       "      <td>1</td>\n",
       "      <td>3.00000</td>\n",
       "      <td>2026-01-31 00:50:26</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>143197</th>\n",
       "      <td>EAEED5ED9B4A43ECA6EE71D602A431B4</td>\n",
       "      <td>So Tuff</td>\n",
       "      <td>canardo</td>\n",
       "      <td>11</td>\n",
       "      <td>No matching.</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>Tension Board 2 Spray</td>\n",
       "      <td>Tension Board 2</td>\n",
       "      <td>p822r8p918r8p961r8p1003r8p1069r6p1078r7p1125r6...</td>\n",
       "      <td>45</td>\n",
       "      <td>18.5000</td>\n",
       "      <td>6b+/V4</td>\n",
       "      <td>2</td>\n",
       "      <td>3.00000</td>\n",
       "      <td>2026-01-31 00:45:30</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>143198</th>\n",
       "      <td>98DE5842AC624739926D561DD68540AA</td>\n",
       "      <td>Ate That Up</td>\n",
       "      <td>canardo</td>\n",
       "      <td>11</td>\n",
       "      <td>No matching.</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>Tension Board 2 Spray</td>\n",
       "      <td>Tension Board 2</td>\n",
       "      <td>p992r7p1088r8p1091r6p1121r8p1122r5p1124r6p1155...</td>\n",
       "      <td>45</td>\n",
       "      <td>20.0000</td>\n",
       "      <td>6c/V5</td>\n",
       "      <td>1</td>\n",
       "      <td>3.00000</td>\n",
       "      <td>2026-01-31 00:49:18</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>143199</th>\n",
       "      <td>8FD9E8A483C44D4C88ECD014034A1688</td>\n",
       "      <td>Surprise sur Prise</td>\n",
       "      <td>canardo</td>\n",
       "      <td>11</td>\n",
       "      <td>No matching.</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>Tension Board 2 Spray</td>\n",
       "      <td>Tension Board 2</td>\n",
       "      <td>p807r8p824r7p836r6p905r6p909r6p918r8p938r5p941...</td>\n",
       "      <td>45</td>\n",
       "      <td>24.0000</td>\n",
       "      <td>7b/V8</td>\n",
       "      <td>1</td>\n",
       "      <td>3.00000</td>\n",
       "      <td>2026-01-31 01:05:44</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>143200</th>\n",
       "      <td>4D8849CDCC1745C9AD02CD02B5C4DEFF</td>\n",
       "      <td>THE BIG K</td>\n",
       "      <td>EnzoB</td>\n",
       "      <td>9</td>\n",
       "      <td></td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "      <td>Original Layout</td>\n",
       "      <td>Tension Board</td>\n",
       "      <td>p2r3p13r2p16r2p28r2p30r2p32r4p47r4p57r2p59r1p6...</td>\n",
       "      <td>30</td>\n",
       "      <td>16.0000</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>1</td>\n",
       "      <td>3.00000</td>\n",
       "      <td>2026-01-31 01:07:46</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>143201 rows × 16 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                                    uuid          climb_name setter_username  \\\n",
       "0       00163801596af1064d549ad75b684539      Duroxmanie 2.0        vinsewah   \n",
       "1       00163801596af1064d549ad75b684539      Duroxmanie 2.0        vinsewah   \n",
       "2       001945feb7509ce231c9d8b237082a39                1 am          ssssss   \n",
       "3       001945feb7509ce231c9d8b237082a39                1 am          ssssss   \n",
       "4       001945feb7509ce231c9d8b237082a39                1 am          ssssss   \n",
       "...                                  ...                 ...             ...   \n",
       "143196  9A09A2F4F82C46EFA8F8309DF263E878           nemesis 1      Hibiki0108   \n",
       "143197  EAEED5ED9B4A43ECA6EE71D602A431B4             So Tuff         canardo   \n",
       "143198  98DE5842AC624739926D561DD68540AA         Ate That Up         canardo   \n",
       "143199  8FD9E8A483C44D4C88ECD014034A1688  Surprise sur Prise         canardo   \n",
       "143200  4D8849CDCC1745C9AD02CD02B5C4DEFF           THE BIG K           EnzoB   \n",
       "\n",
       "        layout_id   description  is_nomatch  is_listed  \\\n",
       "0               9   No matching           1          1   \n",
       "1               9   No matching           1          1   \n",
       "2               9                         0          1   \n",
       "3               9                         0          1   \n",
       "4               9                         0          1   \n",
       "...           ...           ...         ...        ...   \n",
       "143196         10  No matching.           1          1   \n",
       "143197         11  No matching.           1          1   \n",
       "143198         11  No matching.           1          1   \n",
       "143199         11  No matching.           1          1   \n",
       "143200          9                         0          1   \n",
       "\n",
       "                   layout_name       board_name  \\\n",
       "0              Original Layout    Tension Board   \n",
       "1              Original Layout    Tension Board   \n",
       "2              Original Layout    Tension Board   \n",
       "3              Original Layout    Tension Board   \n",
       "4              Original Layout    Tension Board   \n",
       "...                        ...              ...   \n",
       "143196  Tension Board 2 Mirror  Tension Board 2   \n",
       "143197   Tension Board 2 Spray  Tension Board 2   \n",
       "143198   Tension Board 2 Spray  Tension Board 2   \n",
       "143199   Tension Board 2 Spray  Tension Board 2   \n",
       "143200         Original Layout    Tension Board   \n",
       "\n",
       "                                                   frames  angle  \\\n",
       "0               p3r4p29r2p59r1p65r2p75r3p89r2p157r4p158r4     30   \n",
       "1               p3r4p29r2p59r1p65r2p75r3p89r2p157r4p158r4     35   \n",
       "2                  p18r1p22r1p57r2p70r2p149r3p161r4p162r4     20   \n",
       "3                  p18r1p22r1p57r2p70r2p149r3p161r4p162r4     30   \n",
       "4                  p18r1p22r1p57r2p70r2p149r3p161r4p162r4     35   \n",
       "...                                                   ...    ...   \n",
       "143196               p339r6p347r6p412r6p583r5p664r7p713r8     50   \n",
       "143197  p822r8p918r8p961r8p1003r8p1069r6p1078r7p1125r6...     45   \n",
       "143198  p992r7p1088r8p1091r6p1121r8p1122r5p1124r6p1155...     45   \n",
       "143199  p807r8p824r7p836r6p905r6p909r6p918r8p938r5p941...     45   \n",
       "143200  p2r3p13r2p16r2p28r2p30r2p32r4p47r4p57r2p59r1p6...     30   \n",
       "\n",
       "        display_difficulty boulder_grade  ascensionist_count  quality_average  \\\n",
       "0                  16.3333         6a/V3                   3          3.00000   \n",
       "1                  16.3333         6a/V3                   3          2.00000   \n",
       "2                  13.0000         5a/V1                   1          2.00000   \n",
       "3                  14.6250         5c/V2                   8          2.87500   \n",
       "4                  15.0000         5c/V2                   7          2.71429   \n",
       "...                    ...           ...                 ...              ...   \n",
       "143196             24.0000         7b/V8                   1          3.00000   \n",
       "143197             18.5000        6b+/V4                   2          3.00000   \n",
       "143198             20.0000         6c/V5                   1          3.00000   \n",
       "143199             24.0000         7b/V8                   1          3.00000   \n",
       "143200             16.0000         6a/V3                   1          3.00000   \n",
       "\n",
       "                      fa_at  \n",
       "0       2024-07-21 01:37:45  \n",
       "1       2021-02-17 02:31:28  \n",
       "2       2021-01-30 21:46:16  \n",
       "3       2021-04-05 01:29:07  \n",
       "4       2020-12-15 23:24:57  \n",
       "...                     ...  \n",
       "143196  2026-01-31 00:50:26  \n",
       "143197  2026-01-31 00:45:30  \n",
       "143198  2026-01-31 00:49:18  \n",
       "143199  2026-01-31 01:05:44  \n",
       "143200  2026-01-31 01:07:46  \n",
       "\n",
       "[143201 rows x 16 columns]"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7eb46cc",
   "metadata": {},
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ba4fc956",
   "metadata": {},
   "source": [
    "# Popularity\n",
    "\n",
    "## Popularity of Tension Board\n",
    "\n",
    "Since we do not have access to user data, we will examine the popular of the Tension Boards by counting first ascents and unique setters by year. Often it's the case that the first ascensionist is the also the setter of the climb, but not always. None the less, we group up first ascensionists by year, with an extra tidbit about how many unique setters there were. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3dff1bd7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 62788 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Popular of tension board by year. \n",
    "### First ascents by year + unique setters by year\n",
    "\n",
    "# Convert df['fa_at'] to datetime format. (For some reason, it does not register as such)\n",
    "df['fa_at'] = pd.to_datetime(df['fa_at'])\n",
    "\n",
    "# Add a new column for the year\n",
    "df['fa_year'] = df['fa_at'].dt.year\n",
    "\n",
    "# Make a new DataFrame with year, first_ascents, and unique_setters\n",
    "df_growth = df.groupby('fa_year').agg(\n",
    "    first_ascents=('uuid', 'count'),\n",
    "    unique_setters=('setter_username', 'nunique')\n",
    ").reset_index()\n",
    "\n",
    "# Disregard the year 2026 since the data only goes one month in. \n",
    "df_growth = df_growth[df_growth['fa_year'] < 2026]\n",
    "\n",
    "## Plot\n",
    "# Dual index plotting\n",
    "\n",
    "fig, ax1 = plt.subplots(figsize=(12,6))\n",
    "\n",
    "# Bar chart for first ascents\n",
    "ax1.bar(df_growth['fa_year'], df_growth['first_ascents'], label='First Ascents', color='coral')\n",
    "ax1.set_xlabel('Year')\n",
    "ax1.set_ylabel('First Ascents')\n",
    "ax1.set_title('TB First Ascents & Unique Setters over Time')\n",
    "#ax1.tick_params(axis='y')\n",
    "\n",
    "# Line chart for unique setters (secondary axis)\n",
    "ax2 = ax1.twinx()\n",
    "ax2.plot(df_growth['fa_year'], df_growth['unique_setters'], color='steelblue', marker='o', label='Unique Setters')\n",
    "ax2.set_ylabel('Unique Setters', color='steelblue')\n",
    "ax2.tick_params(axis='y', labelcolor='steelblue')\n",
    "\n",
    "# Other stuff\n",
    "fig.legend(loc='upper left', bbox_to_anchor=(0.15,0.85))\n",
    "\n",
    "plt.xticks()\n",
    "plt.savefig('../images/stats/first_ascents_by_year.png')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e2bca15a",
   "metadata": {},
   "source": [
    "## Seasonal analysis\n",
    "\n",
    "Next, we examine when the Tension board is most popular. Again, we will work with what we have and use first ascent data. We will plot first ascents by month, combing all years. We exclude the year 2026 because this can skew the analysis as some of the month of January has data (and clearly, 2026 is when the TB2 is the most popular, so this can actually add quite a bit bias). "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "8912306b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 35148 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Season analysis: first ascents by month\n",
    "\n",
    "# First let us add a column for the month to our data\n",
    "df['fa_month'] = df['fa_at'].dt.month\n",
    "\n",
    "# Filter to years < 2026 since the data only goes one month in\n",
    "df_filter = df[df['fa_year'] < 2026]\n",
    "\n",
    "# Make a new DataFrame with month and first ascents\n",
    "df_season = df_filter.groupby('fa_month').agg(\n",
    "    first_ascents=('uuid', 'count'),\n",
    ").reset_index()\n",
    "\n",
    "# We also add a column for the month name. \n",
    "month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', \n",
    "               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\n",
    "df_season['fa_month_name'] = df_season['fa_month'].apply(lambda x: month_names[x-1])\n",
    "\n",
    "# Plot the data\n",
    "fig,ax = plt.subplots(figsize=(12,6))\n",
    "ax.bar(df_season['fa_month_name'], df_season['first_ascents'], color='coral')\n",
    "ax.set_title('First Ascents by Month (All Years Combined)')\n",
    "ax.set_xlabel('Month')\n",
    "ax.set_ylabel('Total First Ascents')\n",
    "\n",
    "# Save the file\n",
    "plt.savefig('../images/stats/first_ascents_by_month.png')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb802d74",
   "metadata": {},
   "source": [
    "This should be what we expect: that the winter months (Dec/Jan) see the most traffic. This is probably when the outdoor climbers are hitting the boards because they're stuck inside. The warmer months see the least number of first ascents since the strong climbers are probably outdoors.\n",
    "\n",
    "We can plot the number of first ascents by day of week. Removing the 2026 data shouldn't make a difference here, so we opt to keep it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "071cfd5f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 30696 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Day of Week analysis\n",
    "\n",
    "# Let us add a column in our DataFrame for the day of the week.\n",
    "# Note that df.dt.day_of_week will have Monday be 0 and Sunday be 6. \n",
    "\n",
    "df['fa_day_of_week'] = df['fa_at'].dt.day_of_week\n",
    "\n",
    "\n",
    "# Make a new DataFrame with month and first ascents\n",
    "df_days = df.groupby('fa_day_of_week').agg(\n",
    "    first_ascents=('uuid', 'count'),\n",
    ").reset_index()\n",
    "\n",
    "# We also add a column for the month name. \n",
    "day_names = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']\n",
    "df_days['fa_day_name'] = df_days['fa_day_of_week'].apply(lambda x: day_names[x])\n",
    "\n",
    "# Plot the data\n",
    "fig,ax = plt.subplots(figsize=(12,6))\n",
    "ax.bar(df_days['fa_day_name'], df_days['first_ascents'], color='coral')\n",
    "ax.set_title('First Ascents by Day of Week (All Years Combined)')\n",
    "ax.set_xlabel('Day')\n",
    "ax.set_ylabel('Total First Ascents')\n",
    "\n",
    "# Save the file\n",
    "plt.savefig('../images/stats/first_ascents_by_day_of_week.png')\n",
    "plt.show()\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9961e92e",
   "metadata": {},
   "source": [
    "Interesting, Tuesday and Wednesday have the most traffic, while Monday is the least popular."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ddc49b3",
   "metadata": {},
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "533dc4a2",
   "metadata": {},
   "source": [
    "# Climbing Statistics: Grades, Angles, Quality, and Matching\n",
    "\n",
    "We will visualize the climbing grade distribution. Recall that we have the following table of grades (with some other unlisted grades).\n",
    "\n",
    "|difficulty|boulder_name|route_name|\n",
    "|----------|------------|----------|\n",
    "|        10|4a/V0       |5b/5.9    |\n",
    "|        11|4b/V0       |5c/5.10a  |\n",
    "|        12|4c/V0       |6a/5.10b  |\n",
    "|        13|5a/V1       |6a+/5.10c |\n",
    "|        14|5b/V1       |6b/5.10d  |\n",
    "|        15|5c/V2       |6b+/5.11a |\n",
    "|        16|6a/V3       |6c/5.11b  |\n",
    "|        17|6a+/V3      |6c+/5.11c |\n",
    "|        18|6b/V4       |7a/5.11d  |\n",
    "|        19|6b+/V4      |7a+/5.12a |\n",
    "|        20|6c/V5       |7b/5.12b  |\n",
    "|        21|6c+/V5      |7b+/5.12c |\n",
    "|        22|7a/V6       |7c/5.12d  |\n",
    "|        23|7a+/V7      |7c+/5.13a |\n",
    "|        24|7b/V8       |8a/5.13b  |\n",
    "|        25|7b+/V8      |8a+/5.13c |\n",
    "|        26|7c/V9       |8b/5.13d  |\n",
    "|        27|7c+/V10     |8b+/5.14a |\n",
    "|        28|8a/V11      |8c/5.14b  |\n",
    "|        29|8a+/V12     |8c+/5.14c |\n",
    "|        30|8b/V13      |9a/5.14d  |\n",
    "|        31|8b+/V14     |9a+/5.15a |\n",
    "|        32|8c/V15      |9b/5.15b  |\n",
    "|        33|8c+/V16     |9b+/5.15c |\n",
    "\n",
    "We will use the actual difficulty in our work, and then unpack translations into boulder_name."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f986836",
   "metadata": {},
   "source": [
    "## Grade distribution"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "5fa594ba",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 86860 characters]",
      "text/plain": [
       "<Figure size 1600x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Difficulty distribution by layout (with total)\n",
    "\n",
    "grade_counts = df['boulder_grade'].value_counts()\n",
    "grade_order = df.groupby('boulder_grade')['display_difficulty'].mean().sort_values().index.tolist()\n",
    "grade_counts = grade_counts.reindex(grade_order)\n",
    "\n",
    "# Prepare data in long format\n",
    "df_long = df.groupby(['boulder_grade', 'layout_name']).size().reset_index(name='count')\n",
    "\n",
    "# Calculate total for background\n",
    "df_total = df.groupby('boulder_grade').size().reset_index(name='count')\n",
    "df_total['layout_name'] = 'All Layouts'\n",
    "\n",
    "# Reindex to correct grade order\n",
    "df_long['grade_order'] = df_long['boulder_grade'].map(\n",
    "    {g: i for i, g in enumerate(grade_order)}\n",
    ")\n",
    "df_long = df_long.sort_values('grade_order')\n",
    "\n",
    "df_total['grade_order'] = df_total['boulder_grade'].map(\n",
    "    {g: i for i, g in enumerate(grade_order)}\n",
    ")\n",
    "df_total = df_total.sort_values('grade_order')\n",
    "\n",
    "# Plot\n",
    "fig, ax = plt.subplots(figsize=(16, 8))\n",
    "\n",
    "# Plot \"All Layouts\" behind (light gray)\n",
    "sns.barplot(\n",
    "    data=df_total,\n",
    "    x='boulder_grade',\n",
    "    y='count',\n",
    "    color='lightgray',\n",
    "    ax=ax,\n",
    "    zorder=1,\n",
    "    width=0.6,\n",
    "    order=grade_order\n",
    ")\n",
    "\n",
    "# Plot individual layouts (grouped) in front\n",
    "sns.barplot(\n",
    "    data=df_long,\n",
    "    x='boulder_grade',\n",
    "    y='count',\n",
    "    hue='layout_name',\n",
    "    palette=['steelblue', 'coral', 'seagreen'],\n",
    "    ax=ax,\n",
    "    zorder=2,\n",
    "    order=grade_order\n",
    ")\n",
    "\n",
    "# Create custom legend with \"All Layouts\" included\n",
    "handles, labels = ax.get_legend_handles_labels()\n",
    "all_layouts_patch = mpatches.Patch(color='lightgray', label='All Layouts')\n",
    "handles.insert(0, all_layouts_patch)\n",
    "ax.legend(handles=handles, title='Layout', fontsize=10)\n",
    "\n",
    "ax.set_xlabel('Grade', fontsize=11)\n",
    "ax.set_ylabel('Number of Climbs', fontsize=11)\n",
    "ax.set_title('Difficulty Distribution by Board Layout', fontsize=14)\n",
    "ax.tick_params(axis='x', rotation=45)\n",
    "ax.grid(axis='y', alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('../images/stats/difficulty_distribution_by_layout_with_total.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "871bb45d",
   "metadata": {},
   "source": [
    "As a climber in North America, I tend to just use the V-grade and not look at the French grade. So let us group the V-grades together and show the distribution like that. We'll usually just stick the boulder_grade (e.g., 5c/V2) instead of grouping the V-grades though. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "d7f0b911",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 56804 characters]",
      "text/plain": [
       "<Figure size 1600x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# V-Grade distribution by layout (with total)\n",
    "\n",
    "# Let's add a v_grade column and v_grade_counts\n",
    "df['v_grade'] = df['boulder_grade'].str.split('/').str[1]\n",
    "v_grade_counts = df['v_grade'].value_counts()\n",
    "v_grade_order = df.groupby('v_grade')['display_difficulty'].mean().sort_values().index.tolist()\n",
    "v_grade_counts = grade_counts.reindex(v_grade_order)\n",
    "\n",
    "# Prepare data in long format\n",
    "df_long = df.groupby(['v_grade', 'layout_name']).size().reset_index(name='count')\n",
    "\n",
    "# Calculate total for background\n",
    "df_total = df.groupby('v_grade').size().reset_index(name='count')\n",
    "df_total['layout_name'] = 'All Layouts'\n",
    "\n",
    "# Reindex to correct grade order\n",
    "df_long['v_grade_order'] = df_long['v_grade'].map(\n",
    "    {g: i for i, g in enumerate(v_grade_order)}\n",
    ")\n",
    "df_long = df_long.sort_values('v_grade_order')\n",
    "\n",
    "df_total['v_grade_order'] = df_total['v_grade'].map(\n",
    "    {g: i for i, g in enumerate(v_grade_order)}\n",
    ")\n",
    "df_total = df_total.sort_values('v_grade_order')\n",
    "\n",
    "# Plot\n",
    "fig, ax = plt.subplots(figsize=(16, 8))\n",
    "\n",
    "# Plot \"All Layouts\" behind (light gray)\n",
    "sns.barplot(\n",
    "    data=df_total,\n",
    "    x='v_grade',\n",
    "    y='count',\n",
    "    color='lightgray',\n",
    "    ax=ax,\n",
    "    zorder=1,\n",
    "    width=0.6,\n",
    "    order=v_grade_order\n",
    ")\n",
    "\n",
    "# Plot individual layouts (grouped) in front\n",
    "sns.barplot(\n",
    "    data=df_long,\n",
    "    x='v_grade',\n",
    "    y='count',\n",
    "    hue='layout_name',\n",
    "    palette=palette,\n",
    "    ax=ax,\n",
    "    zorder=2,\n",
    "    order=v_grade_order\n",
    ")\n",
    "\n",
    "# Create legend with \"All Layouts\" included\n",
    "handles, labels = ax.get_legend_handles_labels()\n",
    "all_layouts_patch = mpatches.Patch(color='lightgray', label='All Layouts')\n",
    "handles.insert(0, all_layouts_patch)\n",
    "ax.legend(handles=handles, title='Layout', fontsize=10)\n",
    "\n",
    "ax.set_xlabel('Grade', fontsize=11)\n",
    "ax.set_ylabel('Number of Climbs', fontsize=11)\n",
    "ax.set_title('Difficulty Distribution by Board Layout', fontsize=14)\n",
    "ax.tick_params(axis='x', rotation=45)\n",
    "ax.grid(axis='y', alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('../images/stats/v_grade_distribution_by_layout_with_total.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e27f4277",
   "metadata": {},
   "source": [
    "So the grade distribution actually varies quite a bit from board to board. Some key differences in grades are the angle at which the climb is. Note that climbs can be done at different angles."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6a34766",
   "metadata": {},
   "source": [
    "## Angle Distribution\n",
    "\n",
    "What about the angle distribution? Since the TB1 goes from 0 to 50 and the TB2 goes from 0 to 65 (although my local board only goes to 60?), let's do an analysis on each."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "7d65b6cd",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 52284 characters]",
      "text/plain": [
       "<Figure size 1600x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Angle distribution.\n",
    "\n",
    "# TB1 goes up to 50 degrees, TB2 up to 65. (Although my local TB2 only goes up to 60 -- brutal climbing)\n",
    "\n",
    "# Prepare data in long format\n",
    "df_long = df.groupby(['angle', 'layout_name']).size().reset_index(name='count')\n",
    "\n",
    "# Calculate total for background\n",
    "df_total = df.groupby('angle').size().reset_index(name='count')\n",
    "df_total['layout_name'] = 'All Layouts'\n",
    "\n",
    "# Reindex to correct order\n",
    "angle_order = sorted(df['angle'].unique())\n",
    "\n",
    "# Plot\n",
    "fix, ax = plt.subplots(figsize=(16,8))\n",
    "\n",
    "# Plot All Layouts\n",
    "sns.barplot(\n",
    "    data=df_total,\n",
    "    x='angle',\n",
    "    y='count',\n",
    "    color='lightgray',\n",
    "    ax=ax,\n",
    "    zorder=1,\n",
    "    width=0.6,\n",
    "    order=angle_order\n",
    ")\n",
    "\n",
    "# Plt indivudual layouts\n",
    "sns.barplot(\n",
    "    data=df_long,\n",
    "    x='angle',\n",
    "    y='count',\n",
    "    hue='layout_name',\n",
    "    palette=palette,\n",
    "    order=angle_order,\n",
    "    ax=ax,\n",
    "    zorder=2\n",
    ")\n",
    "\n",
    "handles,labels = ax.get_legend_handles_labels()\n",
    "all_layouts_patch = mpatches.Patch(color='lightgray', label='All Layouts')\n",
    "handles.insert(0, all_layouts_patch)\n",
    "ax.legend(handles=handles, title='Layout')\n",
    "\n",
    "ax.set_xlabel('Angle')\n",
    "ax.set_ylabel('Number of Climbs')\n",
    "ax.set_title('Angle Distribution by Board Layout')\n",
    "ax.grid(axis='y', alpha=0.3)\n",
    "\n",
    "\n",
    "plt.suptitle('Angle Distribution by Board Layout')\n",
    "plt.savefig('../images/stats/angle_distribution_by_layout.png')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a00ee20",
   "metadata": {},
   "source": [
    "We see that amongst all the boards, 40 degrees is the most common angle."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc164cee",
   "metadata": {},
   "source": [
    "## Angle vs grade\n",
    "\n",
    "How is the distribution between angles and grades? Let's do this with a heatmap."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "2023cb49",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 112748 characters]",
      "text/plain": [
       "<Figure size 1600x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Angle vs grade\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(16, 8))\n",
    "\n",
    "# Create mapping from difficulty to V-grade\n",
    "grade_mapping = df.groupby('display_difficulty')['boulder_grade'].first().to_dict()\n",
    "\n",
    "# Plot \"All Layouts\" as faint background boxes\n",
    "sns.boxplot(\n",
    "    data=df,\n",
    "    x='angle',\n",
    "    y='display_difficulty',\n",
    "    color='lightgray',\n",
    "    order=angle_order,\n",
    "    showfliers=False,\n",
    "    width=0.6,\n",
    "    ax=ax,\n",
    "    zorder=1\n",
    ")\n",
    "\n",
    "# Plot individual layouts in front\n",
    "sns.boxplot(\n",
    "    data=df,\n",
    "    x='angle',\n",
    "    y='display_difficulty',\n",
    "    hue='layout_name',\n",
    "    palette=['steelblue', 'coral', 'seagreen'],\n",
    "    order=angle_order,\n",
    "    showfliers=False,\n",
    "    ax=ax,\n",
    "    width=0.5,\n",
    "    zorder=2\n",
    ")\n",
    "\n",
    "# Relabel y-axis with V-grades\n",
    "yticks_rounded = sorted(set(int(round(t)) for t in df['display_difficulty'].unique() if not pd.isna(t)))\n",
    "ylabels = [grade_mapping.get(t, '') for t in yticks_rounded]\n",
    "ax.set_yticks(yticks_rounded)\n",
    "ax.set_yticklabels(ylabels)\n",
    "\n",
    "# Custom legend with \"All Layouts\"\n",
    "handles, labels = ax.get_legend_handles_labels()\n",
    "all_patch = mpatches.Patch(color='lightgray', label='All Layouts')\n",
    "handles.insert(0, all_patch)\n",
    "ax.legend(handles=handles, title='Layout', fontsize=10)\n",
    "\n",
    "ax.set_xlabel('Angle (degrees)', fontsize=11)\n",
    "ax.set_ylabel('V-Grade', fontsize=11)\n",
    "ax.set_title('Difficulty Distribution by Angle and Layout', fontsize=14)\n",
    "ax.grid(axis='y', alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('../images/stats/difficulty_by_angle_boxplot_by_layout.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c7a5973a",
   "metadata": {},
   "source": [
    "## The Quality of a climb\n",
    "\n",
    "Next we examine the quality of a climb. First we look at how quality relates to the number of ascents."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "bf5bd013",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 32464 characters]",
      "text/plain": [
       "<Figure size 500x500 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Climb quality vs popularity\n",
    "\n",
    "# Filter to climbs with quality ratings\n",
    "df_quality = df[(df['quality_average'].notna()) & (df['quality_average'] > 0)]\n",
    "\n",
    "# Sample for performance\n",
    "df_sample = df_quality.sample(min(2000, len(df_quality)))\n",
    "\n",
    "g = sns.jointplot(\n",
    "    data=df_sample,\n",
    "    x='quality_average',\n",
    "    y='ascensionist_count',\n",
    "    kind='scatter',\n",
    "    color='teal',\n",
    "    height=5\n",
    ")\n",
    "\n",
    "g.ax_joint.set_xlabel('Quality Rating')\n",
    "g.ax_joint.set_ylabel('Ascensionist Count')\n",
    "g.fig.suptitle('Quality vs Popularity')\n",
    "\n",
    "plt.savefig('../images/stats/quality_popularity.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bf0ca290",
   "metadata": {},
   "source": [
    "Next we visualize the average quality vs the angle and grade, by means of a heatmap. Keep in mind that the harder the climb and steeper the angle, the less people will be doing it. So harder climbs are skewed towards people who can actually do it. The point is that, on boards, the climb quality isn't always the best metric. As such, we won't spend too much time on the quality and will only do a heatmap which takes into account all layouts."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "af7282b9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 81652 characters]",
      "text/plain": [
       "<Figure size 1600x800 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Average quality by angle and grade\n",
    "\n",
    "# Filter to climbs with quality ratings\n",
    "df_quality = df[(df['quality_average'].notna()) & (df['quality_average'] > 0)]\n",
    "\n",
    "\n",
    "# Create pivot table\n",
    "quality_pivot = df_quality.pivot_table(\n",
    "    index='boulder_grade',\n",
    "    columns='angle',\n",
    "    values='quality_average',\n",
    "    aggfunc='mean'\n",
    ")\n",
    "quality_pivot = quality_pivot.reindex(grade_order)\n",
    "quality_pivot = quality_pivot.reindex(columns=[a for a in angle_order if a in quality_pivot.columns])\n",
    "\n",
    "# Plot\n",
    "fig, ax = plt.subplots(figsize=(16, 8))\n",
    "\n",
    "sns.heatmap(\n",
    "    quality_pivot,\n",
    "    cmap='RdYlGn',\n",
    "    cbar_kws={'label': 'Avg Quality Rating'},\n",
    "    ax=ax\n",
    ")\n",
    "\n",
    "ax.set_xlabel('Angle (°)')\n",
    "ax.set_ylabel('Grade')\n",
    "ax.invert_yaxis()\n",
    "ax.set_title('Average Quality Rating by Grade and Angle (All Layouts)')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('../images/stats/quality_heatmap_all_layouts.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3630b077",
   "metadata": {},
   "source": [
    "## \"Match\" vs. \"No Match\"\n",
    "\n",
    "Some setters opt to put the \"no match\" tag onto their climbs. This means that the climber is not allowed to match their hands on any hold. Let's do an analysis of the differences."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "078cd6b6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 86972 characters]",
      "text/plain": [
       "<Figure size 1800x600 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Match vs No Match analysis. \n",
    "\n",
    "# Create status column (Match vs No Match)\n",
    "df['status'] = df.apply(\n",
    "    lambda x: 'No Match' if (\n",
    "        pd.notna(x['description']) and 'No matching' in str(x['description'])\n",
    "    ) or x.get('is_nomatch', 0) == 1 else 'Matched',\n",
    "    axis=1\n",
    ")\n",
    "\n",
    "# Aggregate by layout and status\n",
    "df_agg = df.groupby(['layout_name', 'status']).agg(\n",
    "    count=('uuid', 'count'),\n",
    "    avg_ascensionists=('ascensionist_count', 'mean'),\n",
    "    avg_difficulty=('display_difficulty', 'mean')\n",
    ").reset_index()\n",
    "\n",
    "# Calculate All Layouts totals\n",
    "df_all = df.groupby('status').agg(\n",
    "    count=('uuid', 'count'),\n",
    "    avg_ascensionists=('ascensionist_count', 'mean'),\n",
    "    avg_difficulty=('display_difficulty', 'mean')\n",
    ").reset_index()\n",
    "df_all['layout_name'] = 'All Layouts'\n",
    "\n",
    "# Combine\n",
    "df_combined = pd.concat([df_agg, df_all], ignore_index=True)\n",
    "\n",
    "# Order\n",
    "status_order = ['Matched', 'No Match']\n",
    "\n",
    "\n",
    "# Plot\n",
    "fig, axes = plt.subplots(1, 3, figsize=(18, 6))\n",
    "\n",
    "for ax, metric, title in zip(axes, ['count', 'avg_difficulty', 'avg_ascensionists'], \n",
    "                               ['Total Climbs', 'Average Difficulty', 'Avg Ascensionists']):\n",
    "    \n",
    "    # All Layouts as background (separate for each status)\n",
    "    for i, status in enumerate(status_order):\n",
    "        df_bg = df_combined[(df_combined['layout_name'] == 'All Layouts') & \n",
    "                            (df_combined['status'] == status)]\n",
    "        \n",
    "        if not df_bg.empty:\n",
    "            # Position for status\n",
    "            x_pos = i\n",
    "            ax.bar(\n",
    "                x_pos,\n",
    "                df_bg[metric].values[0],\n",
    "                width=0.7,\n",
    "                color='lightgray',\n",
    "                zorder=1\n",
    "            )\n",
    "    \n",
    "    # Individual layouts in front\n",
    "    df_plot = df_combined[df_combined['layout_name'] != 'All Layouts']\n",
    "    \n",
    "    sns.barplot(\n",
    "        data=df_plot,\n",
    "        x='status',\n",
    "        y=metric,\n",
    "        hue='layout_name',\n",
    "        palette=palette,\n",
    "        order=status_order,\n",
    "        ax=ax,\n",
    "        zorder=2\n",
    "    )\n",
    "    \n",
    "    ax.set_title(title, fontsize=12)\n",
    "    ax.set_xlabel('')\n",
    "    ax.set_ylabel(title if metric == 'count' else ('Grade' if metric == 'avg_difficulty' else 'Count'), fontsize=11)\n",
    "    ax.legend_.remove()\n",
    "    ax.grid(axis='y', alpha=0.3)\n",
    "\n",
    "# Y-axis labels for difficulty plot\n",
    "yticks = [11, 13, 15, 17, 19, 21, 23]\n",
    "ylabels = [grade_mapping.get(t, f\"V{t-10}\") for t in yticks]\n",
    "axes[1].set_yticks(yticks)\n",
    "axes[1].set_yticklabels(ylabels)\n",
    "axes[1].set_ylim(bottom=10)\n",
    "\n",
    "# Add V-grade annotations on difficulty bars\n",
    "for i, status in enumerate(status_order):\n",
    "    for j, layout in enumerate(df['layout_name'].unique()):\n",
    "        row = df_combined[(df_combined['layout_name'] == layout) & (df_combined['status'] == status)]\n",
    "        if not row.empty:\n",
    "            diff = row['avg_difficulty'].values[0]\n",
    "            v_grade = grade_mapping.get(round(diff), '')\n",
    "            # Position: x = status index + offset for layout\n",
    "            x_pos = i + (j - 1) * 0.27\n",
    "            axes[1].text(x_pos, diff + 0.3, v_grade, ha='center', fontsize=8, fontweight='bold')\n",
    "\n",
    "\n",
    "# Custom Legend\n",
    "handles,labels = ax.get_legend_handles_labels()\n",
    "all_layouts_patch = mpatches.Patch(color='lightgray', label='All Layouts')\n",
    "handles.insert(0, all_layouts_patch)\n",
    "fig.legend(handles=handles, title='Layout', bbox_to_anchor=(1.08, 0.9))\n",
    "\n",
    "plt.suptitle('Match vs No Match Climbs by Layout', fontsize=14, y=1.02)\n",
    "plt.tight_layout()\n",
    "plt.savefig('../images/stats/match_vs_nomatch_by_layout.png', dpi=150, bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6f51156a",
   "metadata": {},
   "source": [
    "So we gather the following about \"no match\" climbs:\n",
    "\n",
    "- they are far fewer than \"match\" climbs,\n",
    "- they are on average hard than \"match\" climbs,\n",
    "- and that they tend to have a bit more ascensionists on the TB2, and less on the TB1 (although, much more overall)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "21e12faa",
   "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 tr th {\n",
       "        text-align: left;\n",
       "    }\n",
       "\n",
       "    .dataframe thead tr:last-of-type th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr>\n",
       "      <th></th>\n",
       "      <th colspan=\"2\" halign=\"left\">avg_ascensionists</th>\n",
       "      <th colspan=\"2\" halign=\"left\">avg_difficulty</th>\n",
       "      <th colspan=\"2\" halign=\"left\">count</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>status</th>\n",
       "      <th>Matched</th>\n",
       "      <th>No Match</th>\n",
       "      <th>Matched</th>\n",
       "      <th>No Match</th>\n",
       "      <th>Matched</th>\n",
       "      <th>No Match</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>layout_name</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>All Layouts</th>\n",
       "      <td>22.66</td>\n",
       "      <td>31.89</td>\n",
       "      <td>18.16</td>\n",
       "      <td>20.15</td>\n",
       "      <td>82923.0</td>\n",
       "      <td>60278.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Original Layout</th>\n",
       "      <td>22.68</td>\n",
       "      <td>21.66</td>\n",
       "      <td>17.35</td>\n",
       "      <td>19.61</td>\n",
       "      <td>48123.0</td>\n",
       "      <td>26595.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Tension Board 2 Mirror</th>\n",
       "      <td>20.71</td>\n",
       "      <td>46.15</td>\n",
       "      <td>19.20</td>\n",
       "      <td>20.50</td>\n",
       "      <td>20752.0</td>\n",
       "      <td>22688.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Tension Board 2 Spray</th>\n",
       "      <td>25.48</td>\n",
       "      <td>27.22</td>\n",
       "      <td>19.42</td>\n",
       "      <td>20.71</td>\n",
       "      <td>14048.0</td>\n",
       "      <td>10995.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                       avg_ascensionists          avg_difficulty           \\\n",
       "status                           Matched No Match        Matched No Match   \n",
       "layout_name                                                                 \n",
       "All Layouts                        22.66    31.89          18.16    20.15   \n",
       "Original Layout                    22.68    21.66          17.35    19.61   \n",
       "Tension Board 2 Mirror             20.71    46.15          19.20    20.50   \n",
       "Tension Board 2 Spray              25.48    27.22          19.42    20.71   \n",
       "\n",
       "                          count           \n",
       "status                  Matched No Match  \n",
       "layout_name                               \n",
       "All Layouts             82923.0  60278.0  \n",
       "Original Layout         48123.0  26595.0  \n",
       "Tension Board 2 Mirror  20752.0  22688.0  \n",
       "Tension Board 2 Spray   14048.0  10995.0  "
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "### Match vs No Match Summary\n",
    "summary = df_combined.pivot_table(\n",
    "    index='layout_name',\n",
    "    columns='status',\n",
    "    values=['count', 'avg_difficulty', 'avg_ascensionists']\n",
    ").round(2)\n",
    "\n",
    "summary"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de8eb20e",
   "metadata": {},
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "754abf39",
   "metadata": {},
   "source": [
    "# Prolific statistics\n",
    "\n",
    "Here we will take note of some prolific statistics: what are the most popular climbs and who are the most popular setters?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "935c0ea3",
   "metadata": {},
   "source": [
    "## Most popular climbs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "42f93d59",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 150028 characters]",
      "text/plain": [
       "<Figure size 1600x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Most popular climbs\n",
    "# The ascensionist_count column will allow us to easily deduce the top 15 climbs. \n",
    "\n",
    "# Create a DataFrame with the top 15 climbs\n",
    "df_popular_climbs = df.sort_values(by='ascensionist_count', ascending=False).head(15)[::-1]\n",
    "\n",
    "# We want the y-axis to say \"Climb @ angle\". Let's just create a new column for this. \n",
    "df_popular_climbs['y_label'] = df_popular_climbs.apply(\n",
    "    lambda row: f\"{row['climb_name']} @ {row['angle']}°\", axis=1\n",
    ")\n",
    "\n",
    "# Plot it\n",
    "fig, ax = plt.subplots(figsize=(16,8))\n",
    "bars = ax.barh(df_popular_climbs['y_label'], df_popular_climbs['ascensionist_count'], color='teal')\n",
    "ax.set_xlabel('Ascensionist Count')\n",
    "ax.set_title('Top 15 Most Popular Climbs (at a specific angle)')\n",
    "\n",
    "# Add grade and angle labels\n",
    "for bar, (_, row) in zip(bars, df_popular_climbs.iterrows()):\n",
    "    label = f\"{row['boulder_grade']} on {row['layout_name']}\"\n",
    "    ax.text(100,\n",
    "            bar.get_y() + bar.get_height()/2,\n",
    "            label,\n",
    "            va='center',\n",
    "            ha='left',\n",
    "            color='white')\n",
    "\n",
    "\n",
    "plt.savefig('../images/stats/top_15_climbs.png', bbox_inches='tight')\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9d70aa42",
   "metadata": {},
   "source": [
    "It's unsuprising that every one of these climbs is at 40° given that 40° is the most popular angle, by a long shot.\n",
    "\n",
    "What about an angle-agnostic analysis? What are the top climbs amonst all angles?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "404c67aa",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 106796 characters]",
      "text/plain": [
       "<Figure size 1600x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Top 15 most popular climbs (angle agnostic)\n",
    "\n",
    "# Aggregate by climb_name (sum counts across all angles)\n",
    "df_agg = df.groupby(['climb_name', 'layout_name']).agg(\n",
    "    total_ascensionists=('ascensionist_count', 'sum'),\n",
    "    avg_difficulty=('display_difficulty', 'mean')\n",
    ").reset_index()\n",
    "\n",
    "\n",
    "# Sort and select top 15\n",
    "df_popular_climbs_aa = df_agg.sort_values(by='total_ascensionists', ascending=False).head(15)\n",
    "\n",
    "df_popular_climbs_aa\n",
    "\n",
    "# Plot\n",
    "fig, ax = plt.subplots(figsize=(16, 8))\n",
    "bars = ax.barh(df_popular_climbs_aa['climb_name'], df_popular_climbs_aa['total_ascensionists'], color='teal')\n",
    "ax.set_xlabel('Total Ascensionist Count (All Angles)')\n",
    "ax.set_title('Top 15 Most Popular Climbs (Angle Agnostic)')\n",
    "ax.invert_yaxis()\n",
    "\n",
    "# Add grade labels inside bars\n",
    "for bar, (_, row) in zip(bars, df_popular_climbs.iterrows()):\n",
    "    # Create a label like \"(Tension Board 2 Mirror)\"\n",
    "    label = f\"{row['layout_name']}\" \n",
    "    \n",
    "    ax.text(\n",
    "        100,  # Position inside bar\n",
    "        bar.get_y() + bar.get_height() / 2,\n",
    "        label,\n",
    "        va='center',\n",
    "        ha='left',\n",
    "        color='white'\n",
    "    )\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.savefig('../images/stats/top_15_climbs_angle_agnostic.png', bbox_inches='tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff455a15",
   "metadata": {},
   "source": [
    "What about the top climbs (with/without angles) for each of the board layouts? Let's do with angles first. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "1d50a209",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Original Layout\n",
      "\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>Name</th>\n",
       "      <th>Angle</th>\n",
       "      <th>Grade</th>\n",
       "      <th>Ascensionists</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Pre-game</td>\n",
       "      <td>40°</td>\n",
       "      <td>4a/V0</td>\n",
       "      <td>10378</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Laser Camera</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>8457</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>lefty loosy</td>\n",
       "      <td>40°</td>\n",
       "      <td>5a/V1</td>\n",
       "      <td>7248</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Riopio</td>\n",
       "      <td>40°</td>\n",
       "      <td>4a/V0</td>\n",
       "      <td>6961</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Sea Freight</td>\n",
       "      <td>40°</td>\n",
       "      <td>5c/V2</td>\n",
       "      <td>6953</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Pre-game</td>\n",
       "      <td>30°</td>\n",
       "      <td>4a/V0</td>\n",
       "      <td>6863</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Bubbles</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b+/V4</td>\n",
       "      <td>6653</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Laser Camera</td>\n",
       "      <td>30°</td>\n",
       "      <td>5a/V1</td>\n",
       "      <td>6409</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Holler</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>6288</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Jiggles</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>5568</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Shimmy</td>\n",
       "      <td>40°</td>\n",
       "      <td>6c/V5</td>\n",
       "      <td>5489</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Bubbles</td>\n",
       "      <td>30°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>5473</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Gunter</td>\n",
       "      <td>40°</td>\n",
       "      <td>4a/V0</td>\n",
       "      <td>5461</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>It's Alive!</td>\n",
       "      <td>30°</td>\n",
       "      <td>5c/V2</td>\n",
       "      <td>5357</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>It's Alive!</td>\n",
       "      <td>40°</td>\n",
       "      <td>5b/V1</td>\n",
       "      <td>5301</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "            Name Angle   Grade  Ascensionists\n",
       "1       Pre-game   40°   4a/V0          10378\n",
       "2   Laser Camera   40°   6a/V3           8457\n",
       "3    lefty loosy   40°   5a/V1           7248\n",
       "4         Riopio   40°   4a/V0           6961\n",
       "5    Sea Freight   40°   5c/V2           6953\n",
       "6       Pre-game   30°   4a/V0           6863\n",
       "7        Bubbles   40°  6b+/V4           6653\n",
       "8   Laser Camera   30°   5a/V1           6409\n",
       "9         Holler   40°   6a/V3           6288\n",
       "10       Jiggles   40°   6b/V4           5568\n",
       "11        Shimmy   40°   6c/V5           5489\n",
       "12       Bubbles   30°   6a/V3           5473\n",
       "13        Gunter   40°   4a/V0           5461\n",
       "14   It's Alive!   30°   5c/V2           5357\n",
       "15   It's Alive!   40°   5b/V1           5301"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Tension Board 2 Mirror\n",
      "\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>Name</th>\n",
       "      <th>Angle</th>\n",
       "      <th>Grade</th>\n",
       "      <th>Ascensionists</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Masquerade</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>12123</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Drain from the Brain</td>\n",
       "      <td>40°</td>\n",
       "      <td>6c/V5</td>\n",
       "      <td>10242</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Compliments To The Climber</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>10204</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Putty</td>\n",
       "      <td>40°</td>\n",
       "      <td>5c/V2</td>\n",
       "      <td>10162</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Propagation</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>9640</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Thunderstruck</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>9352</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>All Plastic</td>\n",
       "      <td>40°</td>\n",
       "      <td>5a/V1</td>\n",
       "      <td>9052</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>You Look Great Today</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>8881</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>love is an open door.</td>\n",
       "      <td>40°</td>\n",
       "      <td>6c/V5</td>\n",
       "      <td>8693</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Sherman</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>8212</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>hop scotch</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>7936</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Dear John</td>\n",
       "      <td>40°</td>\n",
       "      <td>5c/V2</td>\n",
       "      <td>7821</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Dancing in the Moonlight</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>7109</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>Doomscroll</td>\n",
       "      <td>40°</td>\n",
       "      <td>4c/V0</td>\n",
       "      <td>7107</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>Poseidon</td>\n",
       "      <td>40°</td>\n",
       "      <td>6c/V5</td>\n",
       "      <td>7078</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                          Name Angle  Grade  Ascensionists\n",
       "1                   Masquerade   40°  6a/V3          12123\n",
       "2         Drain from the Brain   40°  6c/V5          10242\n",
       "3   Compliments To The Climber   40°  6b/V4          10204\n",
       "4                        Putty   40°  5c/V2          10162\n",
       "5                  Propagation   40°  6b/V4           9640\n",
       "6                Thunderstruck   40°  6b/V4           9352\n",
       "7                  All Plastic   40°  5a/V1           9052\n",
       "8         You Look Great Today   40°  6a/V3           8881\n",
       "9        love is an open door.   40°  6c/V5           8693\n",
       "10                     Sherman   40°  6a/V3           8212\n",
       "11                  hop scotch   40°  6b/V4           7936\n",
       "12                   Dear John   40°  5c/V2           7821\n",
       "13    Dancing in the Moonlight   40°  6b/V4           7109\n",
       "14                  Doomscroll   40°  4c/V0           7107\n",
       "15                    Poseidon   40°  6c/V5           7078"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Tension Board 2 Spray\n",
      "\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>Name</th>\n",
       "      <th>Angle</th>\n",
       "      <th>Grade</th>\n",
       "      <th>Ascensionists</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Pizza Box</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>6649</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Aw, shoot</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>5453</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Write This Way</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>5145</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Shoulder Rust</td>\n",
       "      <td>40°</td>\n",
       "      <td>5a/V1</td>\n",
       "      <td>5018</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Nacho Mango</td>\n",
       "      <td>40°</td>\n",
       "      <td>5c/V2</td>\n",
       "      <td>4841</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Frictionless</td>\n",
       "      <td>40°</td>\n",
       "      <td>6c/V5</td>\n",
       "      <td>4536</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Put It Up</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>4499</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Pour Cece</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>4225</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Authorized</td>\n",
       "      <td>40°</td>\n",
       "      <td>6c/V5</td>\n",
       "      <td>4207</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>It’s A Mid-West Alcohol</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>4081</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Center Left</td>\n",
       "      <td>40°</td>\n",
       "      <td>6c/V5</td>\n",
       "      <td>4008</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Lost Glove</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>3563</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Perfect Pizza Party</td>\n",
       "      <td>40°</td>\n",
       "      <td>6a/V3</td>\n",
       "      <td>3420</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>Lightyears</td>\n",
       "      <td>40°</td>\n",
       "      <td>5c/V2</td>\n",
       "      <td>3379</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>The Where</td>\n",
       "      <td>40°</td>\n",
       "      <td>6b/V4</td>\n",
       "      <td>3310</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                       Name Angle  Grade  Ascensionists\n",
       "1                 Pizza Box   40°  6a/V3           6649\n",
       "2                 Aw, shoot   40°  6b/V4           5453\n",
       "3            Write This Way   40°  6a/V3           5145\n",
       "4             Shoulder Rust   40°  5a/V1           5018\n",
       "5               Nacho Mango   40°  5c/V2           4841\n",
       "6              Frictionless   40°  6c/V5           4536\n",
       "7                 Put It Up   40°  6b/V4           4499\n",
       "8                 Pour Cece   40°  6a/V3           4225\n",
       "9                Authorized   40°  6c/V5           4207\n",
       "10  It’s A Mid-West Alcohol   40°  6b/V4           4081\n",
       "11              Center Left   40°  6c/V5           4008\n",
       "12               Lost Glove   40°  6b/V4           3563\n",
       "13      Perfect Pizza Party   40°  6a/V3           3420\n",
       "14               Lightyears   40°  5c/V2           3379\n",
       "15                The Where   40°  6b/V4           3310"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Top 15 most popular climbs by layout\n",
    "\n",
    "layouts = df['layout_name'].unique()\n",
    "\n",
    "for layout in layouts:\n",
    "    # Filter data for this layout\n",
    "    df_layout = df[df['layout_name'] == layout]\n",
    "    \n",
    "    # Sort by popularity and take top 15\n",
    "    df_top = df_layout.sort_values(by='ascensionist_count', ascending=False).head(15).reset_index(drop=True)\n",
    "    \n",
    "    # Select desired columns\n",
    "    df_display = df_top[['climb_name', 'angle', 'boulder_grade', 'ascensionist_count']].copy()\n",
    "    \n",
    "    # Rename columns for display\n",
    "    df_display.columns = ['Name', 'Angle', 'Grade', 'Ascensionists']\n",
    "    \n",
    "    # Format angle as string with degree symbol\n",
    "    df_display['Angle'] = df_display['Angle'].astype(int).astype(str) + '°'\n",
    "    \n",
    "    # Reset index to show rank 1-15\n",
    "    df_display.index = df_display.index + 1\n",
    "    \n",
    "    print(f\"\\n{layout}\\n\")\n",
    "    display(df_display)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e60b4705",
   "metadata": {},
   "source": [
    "On the TB2, it looks like 40 degrees constantly wins out. It is cool to see that, on the TB1, at least one climb made it to the top 15 twice, at two different angles! Congrats \"It's Alive.\"\n",
    "\n",
    "Now let us do a per-board angle agnostic analysis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "6f99580f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "### Original Layout\n",
      "\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>Name</th>\n",
       "      <th>Total Ascensionists</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Pre-game</td>\n",
       "      <td>27468</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Laser Camera</td>\n",
       "      <td>20968</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Bubbles</td>\n",
       "      <td>16161</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Sea Freight</td>\n",
       "      <td>15264</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Holler</td>\n",
       "      <td>14275</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>It's Alive!</td>\n",
       "      <td>13545</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Dull Scissors</td>\n",
       "      <td>11488</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>lefty loosy</td>\n",
       "      <td>11275</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Getting By</td>\n",
       "      <td>11116</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Go Figure</td>\n",
       "      <td>10483</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Intro To Power</td>\n",
       "      <td>10377</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Switching Places</td>\n",
       "      <td>9748</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Shimmy</td>\n",
       "      <td>9427</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>Foles Gold</td>\n",
       "      <td>9339</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>Big Pinch Pinchin'</td>\n",
       "      <td>8823</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                  Name  Total Ascensionists\n",
       "1             Pre-game                27468\n",
       "2         Laser Camera                20968\n",
       "3              Bubbles                16161\n",
       "4          Sea Freight                15264\n",
       "5               Holler                14275\n",
       "6          It's Alive!                13545\n",
       "7        Dull Scissors                11488\n",
       "8          lefty loosy                11275\n",
       "9           Getting By                11116\n",
       "10           Go Figure                10483\n",
       "11      Intro To Power                10377\n",
       "12    Switching Places                 9748\n",
       "13              Shimmy                 9427\n",
       "14          Foles Gold                 9339\n",
       "15  Big Pinch Pinchin'                 8823"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "### Tension Board 2 Mirror\n",
      "\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>Name</th>\n",
       "      <th>Total Ascensionists</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Masquerade</td>\n",
       "      <td>19655</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Putty</td>\n",
       "      <td>19445</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Compliments To The Climber</td>\n",
       "      <td>16844</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Thunderstruck</td>\n",
       "      <td>16121</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Further</td>\n",
       "      <td>15981</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Propagation</td>\n",
       "      <td>15606</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Poseidon</td>\n",
       "      <td>15549</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>All Plastic</td>\n",
       "      <td>15135</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>You Look Great Today</td>\n",
       "      <td>14607</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Doomscroll</td>\n",
       "      <td>14150</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Drain from the Brain</td>\n",
       "      <td>12910</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Prime</td>\n",
       "      <td>11384</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Endearing</td>\n",
       "      <td>10946</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>Dancing in the Moonlight</td>\n",
       "      <td>10855</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>Guided By Angels</td>\n",
       "      <td>10757</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                          Name  Total Ascensionists\n",
       "1                   Masquerade                19655\n",
       "2                        Putty                19445\n",
       "3   Compliments To The Climber                16844\n",
       "4                Thunderstruck                16121\n",
       "5                      Further                15981\n",
       "6                  Propagation                15606\n",
       "7                     Poseidon                15549\n",
       "8                  All Plastic                15135\n",
       "9         You Look Great Today                14607\n",
       "10                  Doomscroll                14150\n",
       "11        Drain from the Brain                12910\n",
       "12                       Prime                11384\n",
       "13                   Endearing                10946\n",
       "14    Dancing in the Moonlight                10855\n",
       "15            Guided By Angels                10757"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "### Tension Board 2 Spray\n",
      "\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>Name</th>\n",
       "      <th>Total Ascensionists</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Pizza Box</td>\n",
       "      <td>11884</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Nacho Mango</td>\n",
       "      <td>10325</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Write This Way</td>\n",
       "      <td>9299</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Shoulder Rust</td>\n",
       "      <td>8749</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Marshmallow Dragon</td>\n",
       "      <td>8001</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Authorized</td>\n",
       "      <td>7387</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>The Where</td>\n",
       "      <td>7325</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Aw, shoot</td>\n",
       "      <td>7006</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Perfect Pizza Party</td>\n",
       "      <td>6575</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Frictionless</td>\n",
       "      <td>6536</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>New Gold</td>\n",
       "      <td>6529</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Pour Cece</td>\n",
       "      <td>6022</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Put It Up</td>\n",
       "      <td>6012</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>It’s A Mid-West Alcohol</td>\n",
       "      <td>5863</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>Bring The Ruckus</td>\n",
       "      <td>5689</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                       Name  Total Ascensionists\n",
       "1                 Pizza Box                11884\n",
       "2               Nacho Mango                10325\n",
       "3            Write This Way                 9299\n",
       "4             Shoulder Rust                 8749\n",
       "5        Marshmallow Dragon                 8001\n",
       "6                Authorized                 7387\n",
       "7                 The Where                 7325\n",
       "8                 Aw, shoot                 7006\n",
       "9       Perfect Pizza Party                 6575\n",
       "10             Frictionless                 6536\n",
       "11                 New Gold                 6529\n",
       "12                Pour Cece                 6022\n",
       "13                Put It Up                 6012\n",
       "14  It’s A Mid-West Alcohol                 5863\n",
       "15         Bring The Ruckus                 5689"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Top 15 climbs by layout (angle agnostic)\n",
    "\n",
    "layouts = df['layout_name'].unique()\n",
    "\n",
    "# Aggregate counts and collect angles\n",
    "df_agg = df.groupby(['climb_name', 'layout_name']).agg(\n",
    "    total_ascensionists=('ascensionist_count', 'sum')\n",
    ").reset_index()\n",
    "\n",
    "for layout in layouts:\n",
    "    df_layout = df_agg[df_agg['layout_name'] == layout]\n",
    "    df_top = df_layout.sort_values(by='total_ascensionists', ascending=False).head(15).reset_index(drop=True)\n",
    "    \n",
    "    df_display = df_top[['climb_name', 'total_ascensionists']].copy()\n",
    "    df_display.columns = ['Name', 'Total Ascensionists']\n",
    "    \n",
    "    # Appropriate index\n",
    "    df_display.index = df_display.index + 1\n",
    "    \n",
    "    print(f\"\\n### {layout}\\n\")\n",
    "    display(df_display)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cab87caa",
   "metadata": {},
   "source": [
    "## Prolific setters\n",
    "\n",
    "Next, we will make a simple table of the most prolific setters by board."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "3053ddc3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Original Layout\n",
      "\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>Username</th>\n",
       "      <th>Climbs</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>adamf1234</td>\n",
       "      <td>420</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>jonlackman</td>\n",
       "      <td>383</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>willanglin</td>\n",
       "      <td>332</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>kylejosephharding</td>\n",
       "      <td>326</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>tmon</td>\n",
       "      <td>246</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>gibbs</td>\n",
       "      <td>240</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>str8_crimpin</td>\n",
       "      <td>238</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>senderone</td>\n",
       "      <td>223</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>topoutclimbinggym</td>\n",
       "      <td>216</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>memphisben</td>\n",
       "      <td>198</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             Username  Climbs\n",
       "1           adamf1234     420\n",
       "2          jonlackman     383\n",
       "3          willanglin     332\n",
       "4   kylejosephharding     326\n",
       "5                tmon     246\n",
       "6               gibbs     240\n",
       "7        str8_crimpin     238\n",
       "8           senderone     223\n",
       "9   topoutclimbinggym     216\n",
       "10         memphisben     198"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Tension Board 2 Mirror\n",
      "\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>Username</th>\n",
       "      <th>Climbs</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>tensionclimbing</td>\n",
       "      <td>506</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>SocksonBlocs</td>\n",
       "      <td>370</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>jaketiger111</td>\n",
       "      <td>315</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>mo3_3az</td>\n",
       "      <td>305</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>limberlimb</td>\n",
       "      <td>299</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>lijahl</td>\n",
       "      <td>261</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>iansutherland</td>\n",
       "      <td>215</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>theruz</td>\n",
       "      <td>202</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>AlexK</td>\n",
       "      <td>200</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>nicholson.brendan</td>\n",
       "      <td>174</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             Username  Climbs\n",
       "1     tensionclimbing     506\n",
       "2        SocksonBlocs     370\n",
       "3        jaketiger111     315\n",
       "4             mo3_3az     305\n",
       "5          limberlimb     299\n",
       "6              lijahl     261\n",
       "7       iansutherland     215\n",
       "8              theruz     202\n",
       "9               AlexK     200\n",
       "10  nicholson.brendan     174"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Tension Board 2 Spray\n",
      "\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>Username</th>\n",
       "      <th>Climbs</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>MaxClark</td>\n",
       "      <td>239</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>ianmek</td>\n",
       "      <td>216</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Jeremy_Fullerton</td>\n",
       "      <td>171</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>tensionclimbing</td>\n",
       "      <td>170</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>danielwoodseyetattoo</td>\n",
       "      <td>148</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Fatbeninco</td>\n",
       "      <td>136</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>rprops</td>\n",
       "      <td>128</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>SPIGGOTT</td>\n",
       "      <td>119</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>cotton125</td>\n",
       "      <td>115</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>graydawgggg</td>\n",
       "      <td>107</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                Username  Climbs\n",
       "1               MaxClark     239\n",
       "2                 ianmek     216\n",
       "3       Jeremy_Fullerton     171\n",
       "4        tensionclimbing     170\n",
       "5   danielwoodseyetattoo     148\n",
       "6             Fatbeninco     136\n",
       "7                 rprops     128\n",
       "8               SPIGGOTT     119\n",
       "9              cotton125     115\n",
       "10           graydawgggg     107"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "### Top 10 setters by layout\n",
    "\n",
    "# Make a DataFrame for the setters\n",
    "df_setters = df.groupby(['setter_username', 'layout_name']).agg(\n",
    "    climb_count=('uuid', 'nunique')\n",
    ").reset_index()\n",
    "\n",
    "layouts = df['layout_name'].unique()\n",
    "\n",
    "for layout in layouts:\n",
    "    df_layout = df_setters[df_setters['layout_name'] == layout]\n",
    "    df_top = df_layout.sort_values(by='climb_count', ascending=False).head(10).reset_index(drop=True)\n",
    "\n",
    "    df_display = df_top[['setter_username', 'climb_count']].copy()\n",
    "    df_display.columns = ['Username', 'Climbs']\n",
    "\n",
    "    # Reset index to show rank\n",
    "    df_display.index = df_display.index + 1\n",
    "\n",
    "    # Display\n",
    "    print(f\"\\n{layout}\\n\")\n",
    "    display(df_display)\n",
    "    \n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19dd4fad",
   "metadata": {},
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "085c99ea",
   "metadata": {},
   "source": [
    "\n",
    "# Conclusion\n",
    "\n",
    "With that, we will move on to analyzing specific holds on the boards. We will do this in [02_hold_analysis_and_board_heatmap.ipynb](02_hold_analysis_and_board_heatmap.ipynb)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
