{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ea1d5966",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Initialize Otter\n",
    "import otter\n",
    "grader = otter.Notebook(\"practice02.ipynb\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "219626cd-cab5-4c46-a4f2-af1359ee59b6",
   "metadata": {},
   "source": [
    "**Student names and e-mails:**\n",
    "\n",
    "_YOUR NAME — your@calvin.edu_\n",
    "\n",
    "_YOUR NAME — your@calvin.edu_"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7044a719-4218-4e11-b97c-e160dcac21c4",
   "metadata": {},
   "source": [
    "# Practice 02 — Cleaning, Grouping, and Visualizing Data\n",
    "\n",
    "In this practice you will work with a real dataset about footballers from the English Premier League. Each task is tagged with the SLO it covers:\n",
    "\n",
    "| SLO | Description |\n",
    "|-----|-------------|\n",
    "| **03A** | Clean and transform text data using string operations in DataFrames |\n",
    "| **03B** | Group data to calculate aggregates such as counts, means, or sums |\n",
    "| **03C** | Produce and interpret histograms, scatter plots, line plots, and bar charts to explore a dataset visually |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "30bdb43b-de9e-4052-a14c-42a50532fd48",
   "metadata": {},
   "source": [
    "---\n",
    "## The Dataset: Premier League 2023/24 Player Stats\n",
    "\n",
    "![Premier League football](https://images.unsplash.com/photo-1473976345543-9ffc928e648d?q=80&w=1859&auto=format&fit=crop)\n",
    "\n",
    "This dataset contains season-long statistics for individual players from the 2023/24 Premier League season — goals, assists, progressive passing and carrying, expected goals, and more. It is a simplified version of a dataset originally published on [Kaggle](https://www.kaggle.com/datasets/orkunaktas/premier-league-all-players-stats-2324). Each row is one player."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa3894d8-d121-4536-af21-c3a928550ee4",
   "metadata": {},
   "source": [
    "**Column descriptions (the ones you'll use most):**\n",
    "\n",
    "| Column | Meaning |\n",
    "|---|---|\n",
    "| `Player` | The player's name |\n",
    "| `Nation` | A lowercase 2-letter code and an uppercase 3-letter code together, e.g. `\"eng ENG\"` |\n",
    "| `Pos` | The player's position(s), e.g. `\"FW\"` or `\"FW,MF\"` for a player who plays both |\n",
    "| `Age` | The player's age during the season |\n",
    "| `Gls` | Total goals scored |\n",
    "| `Ast` | Total assists |\n",
    "| `xG` | Expected goals — an estimate of how many goals the player *should* have scored, based on shot quality |\n",
    "| `CrdY`, `CrdR` | Total yellow / red cards received |\n",
    "| `PrgC`, `PrgP` | Progressive carries / passes — forward-moving ball actions |\n",
    "| `Team` | The player's Premier League club |\n",
    "\n",
    "`Nation` and `Pos` are exactly the kind of columns Monday's class warned you about: readable to a human, not yet usable by `groupby()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1a6c6f2b-16e1-4d98-99f6-7001e83bed58",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import plotly.express as px"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eb3d7208-a7f7-4901-8379-86c6d5dc3208",
   "metadata": {},
   "source": [
    "The cell below loads the data from a CSV file into a pandas DataFrame called `players`. Run it and look at the first few rows."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6cb5cadf-09ef-4d44-b675-5cf2d732c9fb",
   "metadata": {},
   "outputs": [],
   "source": [
    "players = pd.read_csv('https://cs.calvin.edu/courses/data/202/fsantos/premier-league.csv')\n",
    "players.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6d0b5283-9229-4376-8ee4-7ac6d0cf4f4e",
   "metadata": {},
   "source": [
    "Notice two things before we do anything else:\n",
    "- `Nation` packs *two* codes into one string, e.g. `\"eng ENG\"` — not usable for grouping by country yet.\n",
    "- `Pos` can hold *more than one* position per player, e.g. `\"FW,MF\"` — not usable for grouping by position yet either.\n",
    "\n",
    "Part 1 fixes both."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a0eec102-f541-4f51-a982-2f0fefe920f4",
   "metadata": {},
   "source": [
    "---\n",
    "## Part 1 — Cleaning String Data (SLO 03A)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fe1a44a0-2308-4604-892a-56e06e972848",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 03A.1 — Extracting the Nation Code *(1 pt)*\n",
    "\n",
    "The `Nation` column looks like `\"eng ENG\"` — a lowercase code, a space, then the uppercase 3-letter code you actually want.\n",
    "\n",
    "Using `.str.split(' ')` and indexing (or another string method of your choice), create a new column `players['Nation_Code']` containing just the uppercase 3-letter code, e.g. `\"ESP\"`, `\"ENG\"`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "54e869f5-8be8-4bd5-b426-39790e233529",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "players['Nation_Code'] = ...\n",
    "players[['Player', 'Nation', 'Nation_Code']].head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0db32fdf",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"03A.1\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2517fdcf-b896-4009-9d27-8ae656870f54",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 03A.2 — Flagging Multi-Position Players *(2 pts)*\n",
    "\n",
    "The `Pos` column holds one or more positions per player, e.g. `\"FW,MF\"`.\n",
    "\n",
    "1. Split `Pos` on the comma into a new column `players['Pos_List']`, where each value is a *list* of positions (`.str.split(',')`).\n",
    "2. From `Pos_List`, create a boolean column `players['Is_Multi_Position']` that is `True` for players with more than one position.\n",
    "3. From `Pos_List`, create `players['Primary_Pos']` holding just the *first* listed position for each player (their main position).\n",
    "4. Assign the total number of multi-position players to `n_multi_position`.\n",
    "\n",
    "*Hint: `.str.len()` on a column of lists gives you each list's length; `.str[0]` gives you its first element.*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5d2f1551-5516-4e82-a4ec-8c030ceafd97",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "players['Pos_List'] = ...\n",
    "players['Is_Multi_Position'] = ...\n",
    "players['Primary_Pos'] = ...\n",
    "n_multi_position = ...\n",
    "print(f'Multi-position players: {n_multi_position}')\n",
    "players[['Player', 'Pos', 'Pos_List', 'Is_Multi_Position', 'Primary_Pos']].head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f68fe560",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"03A.2\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b78d93c-be78-4c92-a1ff-c78f567ae4fb",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 03A.3 — Mapping Codes to Full Country Names *(2 pts)*\n",
    "\n",
    "Even after extracting `Nation_Code`, you still only have an abbreviation — turning `\"ESP\"` into `\"Spain\"` isn't something regex or case rules can do, because it's not a spelling problem, it's an *abbreviation* problem. That's exactly the distinction Monday's class made with `\"SF\"` → `\"San Francisco\"`: it needed an explicit lookup dictionary, not a pattern.\n",
    "\n",
    "Here's a starter dictionary covering six countries:\n",
    "\n",
    "```python\n",
    "nation_map = {\n",
    "    'ENG': 'England', 'ESP': 'Spain', 'BRA': 'Brazil',\n",
    "    'ARG': 'Argentina', 'FRA': 'France', 'POR': 'Portugal',\n",
    "}\n",
    "```\n",
    "\n",
    "Use `.replace()` with `nation_map` to create `players['Nation_Name']` from `players['Nation_Code']`. (Codes *not* in the dictionary will simply stay as their 3-letter code — that's expected; a real lookup table would need many more entries.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6964453-9fab-409a-8a88-50260ab1ffd2",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "nation_map = {\n",
    "    'ENG': 'England', 'ESP': 'Spain', 'BRA': 'Brazil',\n",
    "    'ARG': 'Argentina', 'FRA': 'France', 'POR': 'Portugal',\n",
    "}\n",
    "\n",
    "players['Nation_Name'] = ...\n",
    "players[['Player', 'Nation_Code', 'Nation_Name']].drop_duplicates('Nation_Code').head(10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7f6a92af",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"03A.3\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "994ed776-1d83-4add-9f9b-7c20c2ac5f36",
   "metadata": {},
   "source": [
    "---\n",
    "## Part 2 — Grouping and Aggregating (SLO 03B)\n",
    "\n",
    "Use **named aggregation** where you can — it's the syntax we've been recommending, and it's what the tests below expect your column names to match:\n",
    "\n",
    "```python\n",
    "new_dataframe = (\n",
    "    dataframe\n",
    "    .groupby('Column', as_index=False)\n",
    "    .agg(new_column1=('Col', 'function'),\n",
    "         new_column2=('Col', 'function'))\n",
    ")\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7c2cac0-f1a7-441b-841e-27d5222fb9aa",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 03B.1 — Total Goals by Nation *(2 pts)*\n",
    "\n",
    "Group `players` by `Nation_Code` and use named aggregation to compute `total_goals` (the sum of `Gls`) per nation. Sort so the highest-scoring nations come first, and keep only the top 5. Assign the result to `top5_nations`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fd4e9091-b4db-4ac1-ab03-4af8ab7cc504",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "top5_nations = (\n",
    "    players\n",
    "    .groupby('Nation_Code', as_index=False)\n",
    "    .agg(total_goals=('Gls', 'sum'))\n",
    "    .sort_values('total_goals', ascending=False)\n",
    "    .head(5)\n",
    "...\n",
    "top5_nations"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bb0470d4",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"03B.1\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8eead02e-8ce4-4a28-a871-3d00a6b966b6",
   "metadata": {},
   "source": [
    "We'll now create age groups (`< 25`, `25-30`, `> 30`) using `pd.cut()`. Run the cell below — you don't need to modify it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f2fad81b-5b7d-4623-88ab-3ef633611975",
   "metadata": {},
   "outputs": [],
   "source": [
    "bins = [0, 25, 30, 100]  # Defining bins for age groups\n",
    "labels = ['< 25', '25-30', '> 30']  # Defining labels for the bins\n",
    "players['Age Group'] = pd.cut(players['Age'], bins=bins, labels=labels, right=False)\n",
    "players[['Player', 'Age', 'Age Group']].head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c7a77d01-60f7-407c-bcdf-7964cfe6279c",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 03B.2 — Cards by Age Group *(2 pts)*\n",
    "\n",
    "Group `players` by `Age Group` and use named aggregation to compute both `total_yellow` (sum of `CrdY`) and `total_red` (sum of `CrdR`) per group, in a single `.agg(...)` call. Assign the result to `cards_by_age`.\n",
    "\n",
    "*Hint: pass `observed=True` to `.groupby()` to avoid an extra empty-category warning.*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7576ec2f-f5d4-448c-9314-bd25124ede9f",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "cards_by_age = (\n",
    "    players\n",
    "    .groupby('Age Group', as_index=False, observed=True)\n",
    "    .agg(total_yellow=('CrdY', 'sum'), total_red=('CrdR', 'sum'))\n",
    "...\n",
    "cards_by_age"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bac6bf19",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"03B.2\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "177be754-a815-4f55-a47a-83fd0f29c237",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 03B.3 — Age Range by Team *(2 pts)*\n",
    "\n",
    "Not every summary has a ready-made function. Using a **custom `lambda`** (like Monday's `x.max() - x.min()` example), group `players` by `Team` and compute the *range* of `Age` on each squad — the oldest player's age minus the youngest's. Sort from largest range to smallest. Assign the result to `age_range_by_team`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8a0b73c7-cc50-4b6c-90b2-c8308177e4ac",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "age_range_by_team = (\n",
    "    players\n",
    "    .groupby('Team')['Age']\n",
    "    .agg(lambda x: x.max() - x.min())\n",
    "    .sort_values(ascending=False)\n",
    "...\n",
    "age_range_by_team.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "780b7ed5",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"03B.3\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73009f7b-7a5c-4922-8b08-69b69dda3957",
   "metadata": {},
   "source": [
    "---\n",
    "## Part 3 — Visual Encodings (SLO 03C)\n",
    "\n",
    "| Question | Plot |\n",
    "|---|---|\n",
    "| What's the distribution of one number? | **Histogram** |\n",
    "| How do two numbers relate to each other? | **Scatter** |\n",
    "| How do groups compare? | **Bar** |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c4f3bcd7-74f7-46e0-ada1-b9f8172f9802",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig_example = px.bar(\n",
    "    top5_nations,\n",
    "    x='Nation_Code',\n",
    "    y='total_goals',\n",
    "    title='Top 5 Nations by Total Goals (Premier League 2023/24)',\n",
    "    labels={'Nation_Code': 'Nation', 'total_goals': 'Total Goals'}\n",
    ")\n",
    "fig_example.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09714184-ac2a-4b9e-aaaf-8981b635c1ef",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "<!-- BEGIN QUESTION -->\n",
    "\n",
    "### Task 03C.1 — Distribution of Goals *(2 pts)*\n",
    "\n",
    "Create a **histogram** of `Gls` (goals scored) across all players.\n",
    "\n",
    "- x-axis: `'Gls'`\n",
    "- A descriptive title\n",
    "- Axis label via the `labels=` argument\n",
    "\n",
    "Assign the figure to `fig1` and display it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "970441d1-194c-4cf8-b25c-448bee574dec",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "...\n",
    "fig1.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eabfc8a7-820a-4ecb-b3c4-ca15223683fe",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "<!-- END QUESTION -->\n",
    "\n",
    "<!-- BEGIN QUESTION -->\n",
    "\n",
    "### Task 03C.2 — Expected vs. Actual Goals *(3 pts)*\n",
    "\n",
    "Using your `Primary_Pos` column from Part 1, create a **scatter plot** comparing each player's `xG` (expected goals) to their actual `Gls` (goals scored).\n",
    "\n",
    "Requirements:\n",
    "- Chart type: **scatter**\n",
    "- x-axis: `'xG'`\n",
    "- y-axis: `'Gls'`\n",
    "- **Color** encoding: `'Primary_Pos'`\n",
    "- A title and axis labels\n",
    "\n",
    "Assign to `fig2` and display it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9beb7586-fbb4-4c8b-8b6b-cceea6f11688",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "...\n",
    "fig2.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6b795a18-d937-43be-9156-8e8d60b79c29",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "<!-- END QUESTION -->\n",
    "\n",
    "<!-- BEGIN QUESTION -->\n",
    "\n",
    "### Task 03C.3 — Evaluate Your Visualization *(2 pts)*\n",
    "\n",
    "Look critically at `fig2`. In **3–5 sentences**, answer:\n",
    "\n",
    "1. Does coloring by `Primary_Pos` help you spot which positions tend to over- or under-perform their `xG`? Why or why not?\n",
    "2. What would you change to make the pattern clearer?\n",
    "3. What does this plot **not** show — what information is hidden or lost?\n",
    "\n",
    "*Edit the cell below and write your answer.*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15d0b57e-473e-4312-993f-93129ecdb645",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "_Your answer here._"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3302e933-ffbd-4de3-8029-e70919ce7600",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "<!-- END QUESTION -->\n",
    "\n",
    "---\n",
    "## Some food for thought: what is the point of sports analytics?\n",
    "\n",
    "*(Not graded — just something to chew on.)*\n",
    "\n",
    "- Check out this [article](https://theconvivialsociety.substack.com/p/the-limits-of-optimization) by L. M. Sacasas commenting on how sports today have changed with the introduction of quantification and analytics.\n",
    "- Are we really having fun with that?\n",
    "\n",
    "> \"So you can't blame anyone for the way the game has developed,\" Jacobs concludes. \"It has become more rational, with a better command of the laws of probability, and stricter, more rigorous canons of efficiency.\"\n",
    "\n",
    "> \"It's worth pausing to consider wherein the purported rationality lies. It is the logic of competition. Within the sporting world, of course, the point is to win, and to do so in a way that can be clearly determined quantitatively. There are no grounds for anyone to ask a manager or a player to pursue a strategy that will diminish their competitive edge. Most of life, however, is not a game with quantifiable outcomes, and probably shouldn't be treated as such. However, the triumph of technique in Ellul's sense encourages the competitive mode of experience. Indeed, quantification itself invites it. This dynamic can be put to beneficial use, and, in clearly delineated circumstances, is perfectly appropriate. But applied uncritically and indiscriminately or even nefariously (see e.g. social media metrics) it can introduce destructive tendencies and eclipse qualitative or otherwise unquantifiable values. Generally speaking, quantification and the logic of optimization which it encourages tend to transform our field of experience into points of aggression, as the sociologist Hartmut Rosa has aptly put it. Data-driven optimization is, in this sense, a way of perceiving the world. And what may matter most about this is not necessarily what it allows us to see, but it keeps us from perceiving: in short, all that cannot be quantified or measured.\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a186434",
   "metadata": {},
   "source": [
    "---\n",
    "## Submission\n",
    "\n",
    "Save your notebook, then upload the **`.ipynb` file** directly to our Moodle assignment page. Don't export, zip, or submit anything else — just the notebook."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10.0"
  },
  "otter": {
   "OK_FORMAT": true,
   "assignment_name": "practice02",
   "tests": {
    "03A.1": {
     "name": "03A.1",
     "points": 1,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert 'Nation_Code' in players.columns, \"Column 'Nation_Code' not found\"\n>>> assert players['Nation_Code'].str.len().eq(3).all(), 'Nation_Code should be exactly 3 letters'\n>>> assert players['Nation_Code'].str.isupper().all(), 'Nation_Code should be uppercase'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "03A.2": {
     "name": "03A.2",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert 'Primary_Pos' in players.columns, \"Column 'Primary_Pos' not found\"\n>>> assert players['Primary_Pos'].isin(['GK', 'DF', 'MF', 'FW']).all(), 'Primary_Pos should only contain the four base positions'\n>>> assert 0 < n_multi_position < len(players), 'n_multi_position should be a count between 0 and the number of players'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "03A.3": {
     "name": "03A.3",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert 'Nation_Name' in players.columns, \"Column 'Nation_Name' not found\"\n>>> assert players.loc[players['Nation_Code'] == 'ESP', 'Nation_Name'].iloc[0] == 'Spain'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "03B.1": {
     "name": "03B.1",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert len(top5_nations) == 5, 'top5_nations should have exactly 5 rows'\n>>> goals = list(top5_nations['total_goals'])\n>>> assert goals == sorted(goals, reverse=True), 'top5_nations should be sorted by total_goals descending'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "03B.2": {
     "name": "03B.2",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert 'total_yellow' in cards_by_age.columns and 'total_red' in cards_by_age.columns, 'cards_by_age should have total_yellow and total_red columns'\n>>> assert len(cards_by_age) == 3, 'cards_by_age should have one row per age group'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "03B.3": {
     "name": "03B.3",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert len(age_range_by_team) == 20, 'age_range_by_team should have one row per team'\n>>> vals = list(age_range_by_team)\n>>> assert vals == sorted(vals, reverse=True), 'age_range_by_team should be sorted descending'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    }
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
