{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  },
  "colab": {
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-0000",
   "metadata": {},
   "source": [
    "# Loops & Iteration\n",
    "\n",
    "Repeat work over entire datasets with for and while loops, enumerate and zip, list comprehensions, and the accumulator pattern.\n",
    "\n",
    "*Part of the free [Python for Data Science](https://ramadnsyh.dev/courses/python-for-data-science) course by [Muhammad Ramadiansyah](https://ramadnsyh.dev). This notebook is generated from the interactive lesson — [read it online](https://ramadnsyh.dev/courses/python-for-data-science/loops).*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0001",
   "metadata": {},
   "source": [
    "Data science is repetition at scale: apply the same step to every row, every\n",
    "file, every experiment. Copy-pasting a line five times doesn't scale to five\n",
    "million — loops do. In this lesson you'll learn `for` and `while`, the\n",
    "helpers `range`, `enumerate`, and `zip`, list comprehensions, and the\n",
    "accumulator pattern that underlies nearly every summary statistic."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0002",
   "metadata": {},
   "source": [
    "## Why loops exist\n",
    "\n",
    "Suppose you need to print a reminder five times. You *could* write five\n",
    "`print()` calls... and then the requirement changes to fifty. A `for` loop\n",
    "with `range(n)` runs its indented body `n` times:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0003",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# The painful way\n",
    "print(\"I will not repeat the same mistake again\")\n",
    "print(\"I will not repeat the same mistake again\")\n",
    "\n",
    "# The loop way - change 5 to 5000 without touching anything else\n",
    "for i in range(5):\n",
    "    print(f\"({i}) I will not repeat the same mistake again\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0004",
   "metadata": {},
   "source": [
    "Note that `range(5)` yields `0, 1, 2, 3, 4` — it starts at 0 and stops\n",
    "*before* 5, exactly like list slicing. `range(start, stop)` and\n",
    "`range(start, stop, step)` give you more control: `range(2, 10, 2)` is\n",
    "`2, 4, 6, 8`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0005",
   "metadata": {},
   "source": [
    "## Looping over collections\n",
    "\n",
    "The real power move: `for` iterates directly over any collection — no index\n",
    "bookkeeping needed. Lists yield items, strings yield characters, and\n",
    "dictionaries yield **keys**:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0006",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "animals = [\"cat\", \"dog\", \"fish\"]\n",
    "for animal in animals:\n",
    "    print(animal.upper())\n",
    "\n",
    "for letter in \"data\":\n",
    "    print(letter)\n",
    "\n",
    "person = {\"name\": \"Rama\", \"age\": 22}\n",
    "for key in person:\n",
    "    print(f\"{key} -> {person[key]}\")\n",
    "\n",
    "# Or get key and value together with .items()\n",
    "for key, value in person.items():\n",
    "    print(key, \"=\", value)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0007",
   "metadata": {},
   "source": [
    "Read `for animal in animals` aloud: \"for each animal in animals\". Choosing a\n",
    "singular loop variable for a plural collection makes loops self-explanatory."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0008",
   "metadata": {},
   "source": [
    "## enumerate and zip\n",
    "\n",
    "Two built-ins solve the most common loop chores. `enumerate` gives you the\n",
    "index *and* the item — perfect for numbered output. `zip` walks two (or\n",
    "more) lists **in lockstep**, pairing up corresponding items:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0009",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "animals = [\"cat\", \"dog\", \"fish\"]\n",
    "for i, animal in enumerate(animals):\n",
    "    print(f\"{i + 1}. {animal}\")\n",
    "\n",
    "products = [\"pc\", \"laptop\", \"mouse\"]\n",
    "prices = [9000, 12000, 300]\n",
    "for product, price in zip(products, prices):\n",
    "    print(f\"{product}: {price}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0010",
   "metadata": {},
   "source": [
    "If you ever catch yourself writing `for i in range(len(items))` just to do\n",
    "`items[i]`, reach for `enumerate` (need the index) or plain `for item in\n",
    "items` (don't) instead — same result, far more readable."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0011",
   "metadata": {},
   "source": [
    "## The accumulator pattern\n",
    "\n",
    "Here is the single most important loop idiom in data work. To compute a\n",
    "total (or count, or running maximum), you: **(1)** create a variable *before*\n",
    "the loop, **(2)** update it on every iteration, **(3)** use it after the\n",
    "loop:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0012",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "quantities = [5, 4, 8, 9]\n",
    "prices = [1000, 2000, 3000, 4000]\n",
    "\n",
    "revenue = 0                                # 1. start the accumulator\n",
    "for quantity, price in zip(quantities, prices):\n",
    "    revenue += quantity * price            # 2. update it each pass\n",
    "print(f\"Total revenue: {revenue}\")         # 3. use the result\n",
    "\n",
    "# Counting is the same pattern with += 1\n",
    "scores = [80, 45, 92, 55, 71, 38]\n",
    "passed = 0\n",
    "for score in scores:\n",
    "    if score >= 60:\n",
    "        passed += 1\n",
    "print(f\"{passed} of {len(scores)} students passed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0013",
   "metadata": {},
   "source": [
    "`revenue += x` is shorthand for `revenue = revenue + x`. Built-ins like\n",
    "`sum()` are this exact pattern packaged up — but you'll constantly need\n",
    "custom versions of it, like the conditional count above."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0014",
   "metadata": {},
   "source": [
    "## while: loop until a condition changes\n",
    "\n",
    "A `for` loop runs once per item; a `while` loop keeps running **as long as\n",
    "its condition stays true**. Use it when you don't know the number of\n",
    "iterations in advance:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0015",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# How many years to double your money at 8% interest?\n",
    "balance = 1000\n",
    "years = 0\n",
    "while balance < 2000:\n",
    "    balance = balance * 1.08\n",
    "    years += 1\n",
    "print(f\"Doubled after {years} years (balance: {balance:.0f})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0016",
   "metadata": {},
   "source": [
    "The body **must** move the condition toward `False` — here the balance grows\n",
    "every pass. Forget that, and you get an infinite loop that never stops\n",
    "(if it happens in a notebook, interrupt the kernel)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0017",
   "metadata": {},
   "source": [
    "## break and continue\n",
    "\n",
    "Two keywords fine-tune any loop. `break` exits the loop immediately;\n",
    "`continue` skips the rest of the current iteration and jumps to the next:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0018",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "readings = [4.2, 3.9, None, 5.1, -999, 4.8]\n",
    "\n",
    "total, count = 0, 0\n",
    "for r in readings:\n",
    "    if r is None:\n",
    "        continue        # skip missing values, keep looping\n",
    "    if r == -999:\n",
    "        print(\"Sensor failure code found - stopping early\")\n",
    "        break           # abandon the loop entirely\n",
    "    total += r\n",
    "    count += 1\n",
    "\n",
    "print(f\"Averaged {count} readings: {total / count:.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0019",
   "metadata": {},
   "source": [
    "This is a very realistic pattern: skimming a data stream, skipping bad\n",
    "records, and bailing out on a fatal one."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0020",
   "metadata": {},
   "source": [
    "## Nested loops\n",
    "\n",
    "A loop inside a loop: the inner loop runs completely for **each** pass of\n",
    "the outer one. That's how you cover every combination — every cell of a\n",
    "grid, every pair of items:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0021",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "sizes = [\"S\", \"M\", \"L\"]\n",
    "colors = [\"red\", \"blue\"]\n",
    "\n",
    "for size in sizes:\n",
    "    for color in colors:\n",
    "        print(f\"{size}-{color}\")\n",
    "\n",
    "print(\"---\")\n",
    "# Total iterations = len(sizes) * len(colors) = 6"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0022",
   "metadata": {},
   "source": [
    "Nested loops multiply: 1000 × 1000 items means a million iterations. Fine\n",
    "for small data — but when things feel slow later in the course, a nested\n",
    "loop is often the culprit (and NumPy or pandas the cure)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0023",
   "metadata": {},
   "source": [
    "## List comprehensions\n",
    "\n",
    "Python has a beloved shortcut for the \"build a new list from an old one\"\n",
    "loop. A **list comprehension** packs create-loop-append into one readable\n",
    "line, with an optional `if` to filter:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0024",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "scores = [80, 45, 92, 55, 71]\n",
    "\n",
    "# The loop way\n",
    "curved = []\n",
    "for s in scores:\n",
    "    curved.append(s + 5)\n",
    "print(curved)\n",
    "\n",
    "# The comprehension way - identical result\n",
    "curved = [s + 5 for s in scores]\n",
    "print(curved)\n",
    "\n",
    "# With a filter: keep only passing scores\n",
    "passing = [s for s in scores if s >= 60]\n",
    "print(passing)\n",
    "\n",
    "# Transform AND filter at once\n",
    "labels = [f\"pass ({s})\" for s in scores if s >= 60]\n",
    "print(labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0025",
   "metadata": {},
   "source": [
    "The template is `[expression for item in collection if condition]`.\n",
    "Comprehensions are everywhere in real Python code — use them for simple\n",
    "transform/filter jobs, and fall back to a full loop when the logic needs\n",
    "multiple statements."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0026",
   "metadata": {},
   "source": [
    "> **Loops today, vectorization later**\n",
    "> \n",
    "> In the pandas and NumPy lessons ahead, many explicit loops disappear -\n",
    "> df[\"price\"] * 1.1 multiplies a million rows at once. But those tools are\n",
    "> loops under the hood, and whenever logic gets too custom for them, you'll\n",
    "> be back here. Master the patterns now."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0027",
   "metadata": {},
   "source": [
    "### 🏋️ Exercise — A weekly sales report\n",
    "\n",
    "best_count:\n",
    "        best_week, best_count = week, len(items)\n",
    "print(f\"Busiest: {best_week} with {best_count} items\")\n",
    "\n",
    "# Unique catalog\n",
    "catalog = set()\n",
    "for items in data.values():\n",
    "    for item in items:\n",
    "        catalog.add(item)\n",
    "print(catalog)\n",
    "`}\n",
    ">\n",
    "You have four weeks of sales, one list of sold items per week, in the dict\n",
    "`data` shown in the hints/solution (weeks `week1`–`week4` with items like\n",
    "`\"pc\"`, `\"laptop\"`, `\"mouse\"`). Write **(1)** a function\n",
    "`total_sold(data, product)` that loops over the dictionary and uses\n",
    "`.count()` with an accumulator to return how many units of `product` were\n",
    "sold across all weeks — verify `\"pc\"` → 9, `\"mouse\"` → 4, `\"keyboard\"` → 2.\n",
    "Then **(2)** find which week sold the most items overall, and **(3)** build\n",
    "the set of unique products ever sold."
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0028",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Your solution here"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0029",
   "metadata": {},
   "source": [
    "<details><summary>✅ Show solution</summary>\n",
    "\n",
    "```python\n",
    "data = {\n",
    "    \"week1\": [\"pc\", \"laptop\", \"pc\", \"pc\", \"mouse\"],\n",
    "    \"week2\": [\"pc\", \"pc\", \"mouse\"],\n",
    "    \"week3\": [\"keyboard\", \"pc\", \"pc\", \"mouse\"],\n",
    "    \"week4\": [\"pc\", \"laptop\", \"pc\", \"keyboard\", \"mouse\"],\n",
    "}\n",
    "\n",
    "def total_sold(data, product):\n",
    "    total = 0\n",
    "    for week in data:\n",
    "        total += data[week].count(product)\n",
    "    return total\n",
    "\n",
    "print(total_sold(data, \"pc\"))        # 9\n",
    "print(total_sold(data, \"mouse\"))     # 4\n",
    "print(total_sold(data, \"keyboard\"))  # 2\n",
    "\n",
    "# Busiest week\n",
    "best_week, best_count = None, 0\n",
    "for week, items in data.items():\n",
    "    if len(items) > best_count:\n",
    "        best_week, best_count = week, len(items)\n",
    "print(f\"Busiest: {best_week} with {best_count} items\")\n",
    "\n",
    "# Unique catalog\n",
    "catalog = set()\n",
    "for items in data.values():\n",
    "    for item in items:\n",
    "        catalog.add(item)\n",
    "print(catalog)\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0030",
   "metadata": {},
   "source": [
    "That wraps up Python fundamentals — next module, you'll put these building\n",
    "blocks to work on real tabular data with NumPy and pandas."
   ]
  }
 ]
}