{
 "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": [
    "# Functions\n",
    "\n",
    "Package logic into reusable functions — parameters, return values, defaults, scope, docstrings, and a first look at lambdas.\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/functions).*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0001",
   "metadata": {},
   "source": [
    "You've already *used* plenty of functions — `print()`, `len()`, `type()` —\n",
    "without writing one yourself. In this lesson you'll learn to define your own:\n",
    "how inputs (parameters) and outputs (return values) work, why `return` is\n",
    "not the same as `print`, and how small, well-named functions turn a messy\n",
    "notebook into readable analysis."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0002",
   "metadata": {},
   "source": [
    "## Functions you already know\n",
    "\n",
    "Python ships with dozens of **built-in functions**. Each one takes input,\n",
    "does a job, and hands back output:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0003",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "numbers = [3, 2, 4, 5]\n",
    "\n",
    "print(sum(numbers))     # add everything up\n",
    "print(len(numbers))     # count the items\n",
    "print(max(numbers))     # largest value\n",
    "print(round(3.14159, 2))# round to 2 decimals\n",
    "print(pow(3, 3))        # 3 to the power of 3"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0004",
   "metadata": {},
   "source": [
    "That input → work → output pattern is the whole idea. Writing your own\n",
    "function just means defining what happens in the \"work\" step."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0005",
   "metadata": {},
   "source": [
    "## Defining your own: def and return\n",
    "\n",
    "The anatomy of a function definition:\n",
    "\n",
    "- `def` starts the definition, followed by the **name** and parentheses\n",
    "- names inside the parentheses are **parameters** — placeholders for inputs\n",
    "- the indented body is the work\n",
    "- `return` sends a value back to whoever called the function"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0006",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def calculate_mean(numbers):\n",
    "    total = sum(numbers)\n",
    "    count = len(numbers)\n",
    "    return total / count\n",
    "\n",
    "# The values you pass in are called ARGUMENTS\n",
    "student1 = [90, 80, 80, 60]\n",
    "student2 = [65, 50, 80, 60]\n",
    "\n",
    "print(calculate_mean(student1))\n",
    "print(calculate_mean(student2))\n",
    "print(calculate_mean([1, 2, 3, 4]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0007",
   "metadata": {},
   "source": [
    "One definition, unlimited reuses — that's the payoff. A quick vocabulary\n",
    "note: **parameters** are the names in the definition (`numbers`);\n",
    "**arguments** are the actual values you pass when calling (`student1`)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0008",
   "metadata": {},
   "source": [
    "## return vs print\n",
    "\n",
    "This distinction confuses every beginner, so let's nail it down. `print`\n",
    "*displays* a value on screen; `return` *hands the value back* so the rest of\n",
    "your program can use it. A function that only prints gives you nothing to\n",
    "work with:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0009",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def mean_that_prints(numbers):\n",
    "    print(sum(numbers) / len(numbers))   # displays, returns nothing\n",
    "\n",
    "def mean_that_returns(numbers):\n",
    "    return sum(numbers) / len(numbers)   # hands the value back\n",
    "\n",
    "a = mean_that_prints([1, 2, 3])\n",
    "b = mean_that_returns([1, 2, 3])\n",
    "\n",
    "print(a)        # None - there was nothing to catch!\n",
    "print(b * 10)   # a real number we can keep computing with"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0010",
   "metadata": {},
   "source": [
    "A function without a `return` statement implicitly returns `None`. Rule of\n",
    "thumb: **compute with return, display with print** — and do the printing at\n",
    "the call site, not inside the function."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0011",
   "metadata": {},
   "source": [
    "## Default parameters and keyword arguments\n",
    "\n",
    "Sometimes a parameter has a sensible usual value. Give it a **default** in\n",
    "the definition and callers can omit it. Here's a function that computes the\n",
    "n-th term of an arithmetic sequence (first term `a`, common difference `d`,\n",
    "term formula `a + (n - 1) * d`):"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0012",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def nth_term(sequence, n=10):\n",
    "    a = sequence[0]\n",
    "    d = sequence[1] - sequence[0]\n",
    "    return a + (n - 1) * d\n",
    "\n",
    "print(nth_term([1, 2, 3, 4]))        # n falls back to the default 10\n",
    "print(nth_term([1, 2, 3, 4], 100))   # positional argument overrides it\n",
    "print(nth_term([1, 11, 21], n=300))  # keyword argument: name it explicitly"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0013",
   "metadata": {},
   "source": [
    "**Keyword arguments** (`n=300`) name the parameter at the call site. They\n",
    "make calls self-documenting and let you skip over defaults you don't want to\n",
    "change — you'll see them everywhere in pandas and scikit-learn, where\n",
    "functions have ten or more optional parameters."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0014",
   "metadata": {},
   "source": [
    "## Returning multiple values\n",
    "\n",
    "`return` can send back several values separated by commas — Python bundles\n",
    "them into a tuple, and you can **unpack** them into separate variables on\n",
    "the receiving end:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0015",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def describe(numbers):\n",
    "    mean = sum(numbers) / len(numbers)\n",
    "    lowest = min(numbers)\n",
    "    highest = max(numbers)\n",
    "    return mean, lowest, highest\n",
    "\n",
    "scores = [65, 50, 80, 60, 95]\n",
    "\n",
    "stats = describe(scores)\n",
    "print(stats)             # it's really one tuple\n",
    "\n",
    "avg, lo, hi = describe(scores)   # unpack into three names\n",
    "print(f\"mean={avg:.1f}, min={lo}, max={hi}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0016",
   "metadata": {},
   "source": [
    "## Scope: what happens in a function stays in a function\n",
    "\n",
    "Variables created inside a function are **local** — they exist only while\n",
    "the function runs, then vanish. This is a feature: functions can't\n",
    "accidentally trample your notebook's variables, and vice versa:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0017",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "total = 1000   # a \"global\" variable, outside any function\n",
    "\n",
    "def add_fee(amount):\n",
    "    fee = 50           # local: exists only inside this call\n",
    "    total = amount + fee   # a NEW local total, not the outer one\n",
    "    return total\n",
    "\n",
    "print(add_fee(200))\n",
    "print(total)           # still 1000 - the function never touched it\n",
    "\n",
    "try:\n",
    "    print(fee)         # locals are gone after the call\n",
    "except NameError as e:\n",
    "    print(\"Error:\", e)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0018",
   "metadata": {},
   "source": [
    "Functions *can read* outer variables, but relying on that makes code fragile.\n",
    "Best practice: pass everything a function needs in as parameters, and get\n",
    "everything out via `return`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0019",
   "metadata": {},
   "source": [
    "## Docstrings: explain your function\n",
    "\n",
    "A string right under the `def` line is a **docstring** — documentation baked\n",
    "into the function itself. Tools like `help()`, Jupyter's `Shift+Tab`, and\n",
    "your future teammates all read it:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0020",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def trimmed_sum(numbers, trim=0):\n",
    "    \"\"\"Sum a list after dropping trim items from each end.\n",
    "\n",
    "    numbers: list of numbers\n",
    "    trim: how many items to drop from the front AND the back\n",
    "    \"\"\"\n",
    "    kept = numbers[trim:len(numbers) - trim]\n",
    "    return sum(kept)\n",
    "\n",
    "print(trimmed_sum([1, 2, 3, 4, 100], trim=1))\n",
    "help(trimmed_sum)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0021",
   "metadata": {},
   "source": [
    "## Lambda: tiny anonymous functions\n",
    "\n",
    "A `lambda` is a one-expression function with no name and no `def`. These two\n",
    "definitions behave identically:"
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0022",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def calculate_mean(numbers):\n",
    "    return sum(numbers) / len(numbers)\n",
    "\n",
    "calculate_mean_v2 = lambda numbers: sum(numbers) / len(numbers)\n",
    "\n",
    "print(calculate_mean([1, 2, 3, 4]))\n",
    "print(calculate_mean_v2([1, 2, 3, 4]))\n",
    "\n",
    "# The real use case: quick throwaway logic passed to another function\n",
    "people = [(\"Rama\", 25), (\"Budi\", 30), (\"Teguh\", 28)]\n",
    "by_age = sorted(people, key=lambda person: person[1])\n",
    "print(by_age)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0023",
   "metadata": {},
   "source": [
    "For anything you'd call by name, prefer `def` — it gets a docstring and a\n",
    "readable name in error messages. Lambdas shine as short one-off arguments to\n",
    "functions like `sorted`, and later to pandas' `.apply()`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0024",
   "metadata": {},
   "source": [
    "> **Functions compose**\n",
    "> \n",
    "> Well-factored code builds big functions out of small ones. If nth_term\n",
    "> already computes a sequence's n-th term, a sum-of-sequence function can call\n",
    "> it instead of repeating the formula. Small, single-purpose, well-named\n",
    "> functions are the difference between a notebook you can revisit in six\n",
    "> months and one you rewrite from scratch."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0025",
   "metadata": {},
   "source": [
    "### 🏋️ Exercise — Write a trimmed mean function\n",
    "\n",
    "A **trimmed mean** is an average computed after cutting off the extremes —\n",
    "a robust statistic that outliers can't drag around. Write\n",
    "`trimmed_mean(numbers, trim=0)` that drops `trim` items from the **front and\n",
    "back** of the list, then averages what remains. For example, with\n",
    "`[1, 2, 3, 4, 10]` and `trim=1` you average `[2, 3, 4]` and get 3.0. Verify:\n",
    "`trimmed_mean([1, 2, 3, 4, 10])` → 4.0,\n",
    "`trimmed_mean([1, 2, 3, 4, 10], 1)` → 3.0, and\n",
    "`trimmed_mean([1, 2, 3, 4, 4, 4, 4, 4, 10], 2)` → 3.8. Include a docstring."
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-0026",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Your solution here"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0027",
   "metadata": {},
   "source": [
    "<details><summary>✅ Show solution</summary>\n",
    "\n",
    "```python\n",
    "def trimmed_mean(numbers, trim=0):\n",
    "    \"\"\"Mean of numbers after dropping trim items from each end.\"\"\"\n",
    "    kept = numbers[trim:len(numbers) - trim]\n",
    "    return sum(kept) / len(kept)\n",
    "\n",
    "print(trimmed_mean([1, 2, 3, 4, 10]))                    # 4.0\n",
    "print(trimmed_mean([1, 2, 3, 4, 10], 1))                 # 3.0\n",
    "print(trimmed_mean([1, 2, 3, 4, 4, 4, 4, 4, 10], 2))     # 3.8\n",
    "```\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-0028",
   "metadata": {},
   "source": [
    "Next up: conditionals — teaching your code to make decisions with if, elif,\n",
    "and else."
   ]
  }
 ]
}