diff --git a/.gitignore b/.gitignore index 11676a02..673554e4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,12 @@ _book/ node_modules/ *.pyc *.swp + book.pdf analysis-essentials.pdf shell/files/data-shell.zip build .ipynb_checkpoints -*~ \ No newline at end of file +*~ +/.idea + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4c5969c..f6e5de40 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: v5.0.0 hooks: - id: check-added-large-files - args: ['--maxkb=1000'] + args: [ '--maxkb=1000' ] - id: check-merge-conflict - id: check-case-conflict - id: check-symlinks @@ -21,16 +21,20 @@ repos: - id: nbqa-pyupgrade additional_dependencies: [ pyupgrade ] - args: [ --py38-plus ] -# -# - repo: https://github.com/ambv/black -# rev: 21.9b0 -# hooks: -# - id: black -# args: [ --line-length=120 ] -# + args: [ --py39-plus ] + - repo: https://github.com/kynan/nbstripout rev: 0.8.1 hooks: - id: nbstripout + args: [ --extra-keys=metadata.language_info.codemirror_mode.version metadata.kernelspec metadata.language_info.pygments_lexer metadata.language_info.version ] + + + +# needs rust, only activate if needed +# - repo: https://github.com/shssoichiro/oxipng +# rev: v9.1.2 +# hooks: +# - id: oxipng +# args: [ --best --strip all --quiet ] diff --git a/advanced-python/10Basics.ipynb b/advanced-python/10Basics.ipynb index 9301d4bd..c55798b7 100644 --- a/advanced-python/10Basics.ipynb +++ b/advanced-python/10Basics.ipynb @@ -391,7 +391,7 @@ "metadata": {}, "outputs": [], "source": [ - "{'a': 'b'}.get?" + "get?" ] }, { @@ -506,22 +506,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" }, "nbsphinx": { "execute": "auto" diff --git a/advanced-python/11AdvancedPython.ipynb b/advanced-python/11AdvancedPython.ipynb index 2a1c253b..1d9bcd4b 100644 --- a/advanced-python/11AdvancedPython.ipynb +++ b/advanced-python/11AdvancedPython.ipynb @@ -965,22 +965,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/12AdvancedClasses.ipynb b/advanced-python/12AdvancedClasses.ipynb index 41185a08..faeab09e 100644 --- a/advanced-python/12AdvancedClasses.ipynb +++ b/advanced-python/12AdvancedClasses.ipynb @@ -503,22 +503,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/20DataAndPlotting.ipynb b/advanced-python/20DataAndPlotting.ipynb index df8e83ee..cec014d2 100644 --- a/advanced-python/20DataAndPlotting.ipynb +++ b/advanced-python/20DataAndPlotting.ipynb @@ -113,8 +113,9 @@ "metadata": {}, "outputs": [], "source": [ - "my_file = uproot.open('https://cern.ch/starterkit/data/advanced-python-2018/real_data.root',\n", - " httpsource={'chunkbytes': 1024*1024, 'limitbytes': 33554432, 'parallel': 64})\n", + "my_file = uproot.open(\n", + " \"https://cern.ch/starterkit/data/advanced-python-2018/real_data.root\"\n", + ")\n", "\n", "my_file.keys()" ] @@ -132,9 +133,9 @@ "metadata": {}, "outputs": [], "source": [ - "tree = my_file['DecayTree']\n", + "tree = my_file[\"DecayTree\"]\n", "# Get a numpy array containing the J/Ψ mass\n", - "tree['Jpsi_M'].array(library='np')" + "tree[\"Jpsi_M\"].array(library=\"np\")" ] }, { @@ -144,8 +145,8 @@ "outputs": [], "source": [ "# Load data as a pandas DataFrame\n", - "data_df = tree.arrays(library='pd')\n", - "my_file.close() # usually, it's better to open the file with a \"with\" statement -> needs no closing\n", + "data_df = tree.arrays(library=\"pd\")\n", + "my_file.close() # usually, it's better to open the file with a \"with\" statement -> closes automatically if outside block\n", "\n", "# Show the first 5 lines of the DataFrame\n", "data_df.head()" @@ -175,8 +176,8 @@ "outputs": [], "source": [ "# Start with a basic histogram\n", - "plt.hist(data_df['Jpsi_M'])\n", - "plt.xlabel('Jpsi mass')" + "plt.hist(data_df[\"Jpsi_M\"])\n", + "plt.xlabel(\"Jpsi mass\")" ] }, { @@ -224,8 +225,8 @@ "outputs": [], "source": [ "# plotting again\n", - "plt.hist(data_df['Jpsi_M'], bins=40)\n", - "plt.xlabel('Jpsi mass')" + "plt.hist(data_df[\"Jpsi_M\"], bins=40)\n", + "plt.xlabel(\"Jpsi mass\")" ] }, { @@ -239,8 +240,8 @@ "outputs": [], "source": [ "# And similar with mplhep\n", - "mplhep.histplot(*np.histogram(data_df['Jpsi_M'], bins=30))\n", - "plt.xlabel('Jpsi mass')" + "mplhep.histplot(*np.histogram(data_df[\"Jpsi_M\"], bins=30))\n", + "plt.xlabel(\"Jpsi mass\")" ] }, { @@ -256,13 +257,13 @@ "# ... except that it can do a lot more!\n", "\n", "# we need to histogram only once!\n", - "h, bins = np.histogram(data_df['Jpsi_M'], bins=50) # TRY OUT: change the binning\n", - "plt.subplots(1,2,figsize=(20, 6))\n", + "h, bins = np.histogram(data_df[\"Jpsi_M\"], bins=50) # TRY OUT: change the binning\n", + "plt.subplots(1, 2, figsize=(20, 6))\n", "plt.subplot(1, 2, 1)\n", "mplhep.histplot(h, bins, yerr=True) # error can also be array\n", "plt.subplot(1, 2, 2)\n", "half_binwidths = (bins[1] - bins[0]) / 2\n", - "mplhep.histplot(h, bins, histtype='errorbar', yerr=True, xerr=half_binwidths)" + "mplhep.histplot(h, bins, histtype=\"errorbar\", yerr=True, xerr=half_binwidths)" ] }, { @@ -272,10 +273,10 @@ "outputs": [], "source": [ "def plot_mass(df):\n", - " h, bins = np.histogram(df['Jpsi_M'], bins=100, range=[2.75, 3.5])\n", + " h, bins = np.histogram(df[\"Jpsi_M\"], bins=100, range=[2.75, 3.5])\n", " mplhep.histplot(h, bins, yerr=True) # feel free to adjust\n", " # You can also use LaTeX in the axis label\n", - " plt.xlabel('$J/\\\\psi$ mass [GeV]')\n", + " plt.xlabel(\"$J/\\\\psi$ mass [GeV]\")\n", " plt.xlim(bins[0], bins[-1])\n", "\n", "\n", @@ -296,8 +297,8 @@ "outputs": [], "source": [ "# When making the ROOT file we forgot to add some variables, no bother lets add them now!\n", - "data_df.eval('Jpsi_eta = arctanh(Jpsi_PZ/Jpsi_P)', inplace=True)\n", - "data_df.head()['Jpsi_eta']" + "data_df.eval(\"Jpsi_eta = arctanh(Jpsi_PZ/Jpsi_P)\", inplace=True)\n", + "data_df.head()[\"Jpsi_eta\"]" ] }, { @@ -313,10 +314,10 @@ "metadata": {}, "outputs": [], "source": [ - "data_df.eval('mup_P = sqrt(mup_PX**2 + mup_PY**2 + mup_PZ**2)', inplace=True)\n", - "data_df.eval('mum_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)\n", + "data_df.eval(\"mup_P = sqrt(mup_PX**2 + mup_PY**2 + mup_PZ**2)\", inplace=True)\n", + "data_df.eval(\"mum_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)\", inplace=True)\n", "# We can also get multiple columns at the same time\n", - "data_df.head()[['mum_P', 'mup_P']]" + "data_df.head()[[\"mum_P\", \"mup_P\"]]" ] }, { @@ -342,7 +343,7 @@ "outputs": [], "source": [ "plot_mass(data_df)\n", - "data_with_cuts_df = data_df.query('Jpsi_PT > 4')\n", + "data_with_cuts_df = data_df.query(\"Jpsi_PT > 4\")\n", "plot_mass(data_with_cuts_df)" ] }, @@ -353,10 +354,12 @@ "outputs": [], "source": [ "plot_mass(data_df)\n", - "data_with_cuts_df = data_df.query('Jpsi_PT > 4')\n", + "data_with_cuts_df = data_df.query(\"Jpsi_PT > 4\")\n", "plot_mass(data_with_cuts_df)\n", "# Lets add some PID cuts as well\n", - "data_with_cuts_df = data_df.query('(Jpsi_PT > 4) & ((mum_ProbNNmu > 0.9) & (mup_ProbNNmu > 0.9))')\n", + "data_with_cuts_df = data_df.query(\n", + " \"(Jpsi_PT > 4) & ((mum_ProbNNmu > 0.9) & (mup_ProbNNmu > 0.9))\"\n", + ")\n", "plot_mass(data_with_cuts_df)" ] }, @@ -375,19 +378,21 @@ "outputs": [], "source": [ "def plot_mass(df, **kwargs):\n", - " h, bins = np.histogram(df['Jpsi_M'], bins=100, range=[2.75, 3.5])\n", + " h, bins = np.histogram(df[\"Jpsi_M\"], bins=100, range=[2.75, 3.5])\n", " mplhep.histplot(h, bins, yerr=True, **kwargs) # feel free to adjust\n", " # You can also use LaTeX in the axis label\n", - " plt.xlabel('$J/\\\\psi$ mass [GeV]')\n", + " plt.xlabel(\"$J/\\\\psi$ mass [GeV]\")\n", " plt.xlim(bins[0], bins[-1])\n", "\n", "\n", - "plot_mass(data_df, label='No cuts', density=1)\n", - "data_with_cuts_df = data_df.query('Jpsi_PT > 4')\n", - "plot_mass(data_with_cuts_df, label='$J/\\\\psi$ p$_T$ only', density=1)\n", - "data_with_cuts_df = data_df.query('(Jpsi_PT > 4) & ((mum_ProbNNmu > 0.9) & (mup_ProbNNmu > 0.9))')\n", - "plot_mass(data_with_cuts_df, label='$J/\\\\psi$ p$_T$ and muon PID', density=1)\n", - "plt.legend(loc='best')" + "plot_mass(data_df, label=\"No cuts\", density=1)\n", + "data_with_cuts_df = data_df.query(\"Jpsi_PT > 4\")\n", + "plot_mass(data_with_cuts_df, label=\"$J/\\\\psi$ p$_T$ only\", density=1)\n", + "data_with_cuts_df = data_df.query(\n", + " \"(Jpsi_PT > 4) & ((mum_ProbNNmu > 0.9) & (mup_ProbNNmu > 0.9))\"\n", + ")\n", + "plot_mass(data_with_cuts_df, label=\"$J/\\\\psi$ p$_T$ and muon PID\", density=1)\n", + "plt.legend(loc=\"best\")" ] }, { @@ -414,14 +419,16 @@ "source": [ "from python_lesson import check_truth\n", "\n", - "print('Originally the significance is')\n", + "print(\"Originally the significance is\")\n", "check_truth(data_df)\n", "\n", - "print('\\nCutting on pT gives us')\n", - "check_truth(data_df.query('Jpsi_PT > 4'))\n", + "print(\"\\nCutting on pT gives us\")\n", + "check_truth(data_df.query(\"Jpsi_PT > 4\"))\n", "\n", - "print('\\nCutting on pT and ProbNNmu gives us')\n", - "check_truth(data_df.query('(Jpsi_PT > 4) & ((mum_ProbNNmu > 0.9) & (mup_ProbNNmu > 0.9))'))" + "print(\"\\nCutting on pT and ProbNNmu gives us\")\n", + "check_truth(\n", + " data_df.query(\"(Jpsi_PT > 4) & ((mum_ProbNNmu > 0.9) & (mup_ProbNNmu > 0.9))\")\n", + ")" ] }, { @@ -443,14 +450,16 @@ "metadata": {}, "outputs": [], "source": [ - "with uproot.open('https://starterkit.web.cern.ch/starterkit/data/advanced-python-2018/simulated_data.root') as mc_file:\n", - " mc_df = mc_file['DecayTree'].arrays(library='pd')\n", + "with uproot.open(\n", + " \"https://starterkit.web.cern.ch/starterkit/data/advanced-python-2018/simulated_data.root\"\n", + ") as mc_file:\n", + " mc_df = mc_file[\"DecayTree\"].arrays(library=\"pd\")\n", "\n", "# mc_file = uproot.open('https://starterkit.web.cern.ch/starterkit/data/advanced-python-2018/simulated_data.root')\n", "# mc_df = mc_file['DecayTree'].arrays(library='pd')\n", - "mc_df.eval('Jpsi_eta = arctanh(Jpsi_PZ/Jpsi_P)', inplace=True)\n", - "mc_df.eval('mup_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)\n", - "mc_df.eval('mum_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)" + "mc_df.eval(\"Jpsi_eta = arctanh(Jpsi_PZ/Jpsi_P)\", inplace=True)\n", + "mc_df.eval(\"mup_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)\", inplace=True)\n", + "mc_df.eval(\"mum_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)\", inplace=True)" ] }, { @@ -470,7 +479,7 @@ "metadata": {}, "outputs": [], "source": [ - "bkg_df = data_df.query('~(3.0 < Jpsi_M < 3.2)')\n", + "bkg_df = data_df.query(\"~(3.0 < Jpsi_M < 3.2)\")\n", "plot_mass(bkg_df)" ] }, @@ -496,16 +505,16 @@ "metadata": {}, "outputs": [], "source": [ - "var = 'Jpsi_PT'\n", + "var = \"Jpsi_PT\"\n", "# let's first create the histograms\n", "hsig, bins = np.histogram(mc_df[var], bins=60)\n", "hbkg, bins = np.histogram(bkg_df[var], bins=bins) # using the same bins here\n", "# then plot them\n", - "mplhep.histplot((hsig, bins), label='MC Signal')\n", - "mplhep.histplot(hbkg, bins=bins, label='Data Bkg')\n", + "mplhep.histplot((hsig, bins), label=\"MC Signal\")\n", + "mplhep.histplot(hbkg, bins=bins, label=\"Data Bkg\")\n", "plt.xlabel(var)\n", "plt.xlim(bins[0], bins[-1])\n", - "plt.legend(loc='best')" + "plt.legend(loc=\"best\")" ] }, { @@ -517,11 +526,11 @@ "# Those are hard to compare!\n", "# We can add the density keyword argument to normalise the distributions\n", "\n", - "mplhep.histplot(hsig, bins=bins, label='MC Signal', density=1)\n", - "mplhep.histplot(hbkg, bins=bins, label='Data Bkg', density=1)\n", + "mplhep.histplot(hsig, bins=bins, label=\"MC Signal\", density=1)\n", + "mplhep.histplot(hbkg, bins=bins, label=\"Data Bkg\", density=1)\n", "plt.xlabel(var)\n", "plt.xlim(bins[0], bins[-1])\n", - "plt.legend(loc='best')" + "plt.legend(loc=\"best\")" ] }, { @@ -542,11 +551,14 @@ " hsig, bins = np.histogram(mc_df[var], bins=60, density=1)\n", " hbkg, bins = np.histogram(bkg_df[var], bins=bins, density=1)\n", "\n", - " mplhep.histplot((hsig, bins), label='MC Signal', )\n", - " mplhep.histplot(hbkg, bins=bins, label='Data Bkg')\n", + " mplhep.histplot(\n", + " (hsig, bins),\n", + " label=\"MC Signal\",\n", + " )\n", + " mplhep.histplot(hbkg, bins=bins, label=\"Data Bkg\")\n", " plt.xlabel(var)\n", " plt.xlim(bins[0], bins[-1])\n", - " plt.legend(loc='best')" + " plt.legend(loc=\"best\")" ] }, { @@ -646,22 +658,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/30Classification.ipynb b/advanced-python/30Classification.ipynb index 9c07d9aa..22c99b49 100644 --- a/advanced-python/30Classification.ipynb +++ b/advanced-python/30Classification.ipynb @@ -19,7 +19,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "jupyter": { + "is_executing": true + } + }, "outputs": [], "source": [ "%store -r bkg_df\n", @@ -30,7 +34,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "jupyter": { + "is_executing": true + } + }, "outputs": [], "source": [ "import mplhep\n", @@ -650,22 +658,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/31ClassificationExtension.ipynb b/advanced-python/31ClassificationExtension.ipynb index 3213818e..607bc71e 100644 --- a/advanced-python/31ClassificationExtension.ipynb +++ b/advanced-python/31ClassificationExtension.ipynb @@ -336,22 +336,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/32BoostingToUniformity.ipynb b/advanced-python/32BoostingToUniformity.ipynb index ae62b845..eb29139c 100644 --- a/advanced-python/32BoostingToUniformity.ipynb +++ b/advanced-python/32BoostingToUniformity.ipynb @@ -237,22 +237,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" }, "nbsphinx": { "execute": "never" diff --git a/advanced-python/33ModelTuning.ipynb b/advanced-python/33ModelTuning.ipynb deleted file mode 100644 index be6ea321..00000000 --- a/advanced-python/33ModelTuning.ipynb +++ /dev/null @@ -1,681 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Model tuning setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#%store -r training_data\n", - "#%store -r training_columns\n", - "#%store -r bkg_df\n", - "#%store -r mc_df\n", - "#%store -r data_df" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#@title\n", - "#!pip install uproot\n", - "#!pip install sklearn\n", - "\n", - "import time\n", - "\n", - "import numpy as np\n", - "import pandas as pd\n", - "import uproot\n", - "import xgboost as xgb\n", - "from matplotlib import pyplot as plt\n", - "from sklearn import metrics\n", - "from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier\n", - "from sklearn.metrics import auc, roc_curve\n", - "from sklearn.model_selection import (GridSearchCV, KFold, cross_val_score,\n", - " cross_validate, train_test_split)\n", - "from xgboost.sklearn import XGBClassifier" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Time and processing check for the lesson\n", - "stt = time.time()\n", - "stc = time.process_time()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def plot_mass(df, label=\"\", norm=True):\n", - " counts, bins, _ = plt.hist(df['Jpsi_M'], label=label, bins=100, range=[2.75, 3.5], histtype='step', density=norm)\n", - " # You can also use LaTeX in the axis label\n", - " plt.xlabel('$J/\\\\psi$ mass [GeV]')\n", - " plt.xlim(bins[0], bins[-1])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def plot_comparision(var, mc_df, bkg_df):\n", - " _, bins, _ = plt.hist(mc_df[var], bins=100, histtype='step', label='MC', density=1)\n", - " _, bins, _ = plt.hist(bkg_df[var], bins=bins, histtype='step', label='Background', density=1)\n", - " plt.xlabel(var)\n", - " plt.xlim(bins[0], bins[-1])\n", - " plt.legend(loc='best')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def plot_roc(bdt, training_data, training_columns, label=None):\n", - " y_score = bdt.predict_proba(training_data[training_columns])[:,1]\n", - " fpr, tpr, thresholds = roc_curve(training_data['catagory'], y_score)\n", - " area = auc(fpr, tpr)\n", - "\n", - " plt.plot([0, 1], [0, 1], color='grey', linestyle='--')\n", - " if label:\n", - " plt.plot(fpr, tpr, label=f'{label} (area = {area:.2f})')\n", - " else:\n", - " plt.plot(fpr, tpr, label=f'ROC curve (area = {area:.2f})')\n", - " plt.xlim(0.0, 1.0)\n", - " plt.ylim(0.0, 1.0)\n", - " plt.xlabel('False Positive Rate')\n", - " plt.ylabel('True Positive Rate')\n", - " plt.legend(loc='lower right')\n", - " # We can make the plot look nicer by forcing the grid to be square\n", - " plt.gca().set_aspect('equal', adjustable='box')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def plot_significance(bdt, training_data, training_columns, label):\n", - " y_score = bdt.predict_proba(training_data[training_columns])[:,1]\n", - " fpr, tpr, thresholds = roc_curve(training_data['catagory'], y_score)\n", - "\n", - " n_sig = 1200\n", - " n_bkg = 23000\n", - " S = n_sig*tpr + (n_sig*tpr==0)*1\n", - " B = n_bkg*fpr + (n_bkg*tpr==0)*1\n", - " metric = S/np.sqrt(S+B)\n", - "\n", - " plt.plot(thresholds, metric, label=label)\n", - " plt.xlabel('BDT cut value')\n", - " plt.ylabel('$\\\\frac{S}{\\\\sqrt{S+B}}$')\n", - " plt.xlim(0, 1.0)\n", - "\n", - " optimum = np.max(metric)\n", - " optimal_cut = thresholds[np.argmax(metric)]\n", - " print(label, \": S/sqrt(S+B) =\", optimum, \" at x =\", optimal_cut)\n", - " plt.axvline(x=optimal_cut, color='black', linewidth=1.0, linestyle='--')\n", - "\n", - " return optimal_cut" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#max_entries = 1000 # try running with low stats for bug fixing your changes quickly\n", - "data_df = uproot.open('https://cern.ch/starterkit/data/advanced-python-2018/real_data.root',\n", - " httpsource={'chunkbytes': 1024*1024, 'limitbytes': 33554432, 'parallel': 64}\n", - " )['DecayTree'].arrays(library='pd')#,entry_stop=max_entries)\n", - "mc_df = uproot.open('https://cern.ch/starterkit/data/advanced-python-2018/simulated_data.root',\n", - " httpsource={'chunkbytes': 1024*1024, 'limitbytes': 33554432, 'parallel': 64}\n", - " )['DecayTree'].arrays(library='pd')#,entry_stop=max_entries)\n", - "bkg_df = data_df.query('~(3.0 < Jpsi_M < 3.2)')\n", - "\n", - "for df in [mc_df, data_df, bkg_df]:\n", - " df.eval('Jpsi_eta = arctanh(Jpsi_PZ/Jpsi_P)', inplace=True)\n", - " df.eval('mup_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)\n", - " df.eval('mum_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)\n", - "\n", - "bkg_df['catagory'] = 0 # Use 0 for background\n", - "mc_df['catagory'] = 1 # Use 1 for signal\n", - "training_data = pd.concat([bkg_df, mc_df], copy=True, ignore_index=True)\n", - "for df in [mc_df, bkg_df, data_df, training_data]:\n", - " df['IPdiff'] = np.abs(df['mum_PT'] - df['mup_PT'])\n", - "\n", - "training_columns = [\n", - " 'Jpsi_PT',\n", - " 'mup_PT', 'mup_eta', 'mup_ProbNNmu', 'mup_IP',\n", - " 'mum_PT', 'mum_eta', 'mum_ProbNNmu', 'mum_IP',\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Previously we trained an XGBClassifier with the default settings, with learning rate = 0.3 and maximum iterations = 100. This cut off to the training process may be limiting the performance of our model. We can monitor the performance of our model as a function of training iteration and stop the training when the gradient approximates zero. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "X1, y1 = training_data[training_columns], training_data['catagory']\n", - "X_train, X_test, y_train, y_test = train_test_split(X1, y1)\n", - "# default train_size = 0.25, this can be varied to suit your data\n", - "\n", - "LR = 0.3 # the coefficient of step size decay, eta, has alias 'learning_rate' with default 0.3\n", - "\n", - "stime = time.time()\n", - "bdt = XGBClassifier(learning_rate = LR, n_estimators=100, seed=123, n_jobs=-1)\n", - "bdt.fit(training_data[training_columns], training_data['catagory'])\n", - "print(\"XGBoost --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - "for df in [mc_df, bkg_df, data_df, training_data]:\n", - " df['XGB'] = bdt.predict_proba(df[training_columns])[:,1]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Cross-validation\n", - "\n", - "Splitting the data into randomised subsets for training allows you to monitor your model's performance on the fly using the statistically independant remainder of your sample - this is called cross-validation (CV). We can see below that at the 100th iteration the metrics still show a trend of improvement." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def training_monitor(alg):\n", - "\n", - " # A model trained with eval_set and eval_metric will return evals_result\n", - " results = alg.evals_result()\n", - " epochs = len(results['validation_0']['logloss'])\n", - " x_axis = range(0, epochs)\n", - "\n", - " # Plotting logLoss as a function of training iteration\n", - " fig, ax = plt.subplots()\n", - " ax.plot(x_axis, results['validation_0']['logloss'], label='Train') # for each eval_set\n", - " if results['validation_1']: ax.plot(x_axis, results['validation_1']['logloss'], label='Test')\n", - " ax.legend()\n", - " plt.ylabel('LogLoss')\n", - " plt.title('LogLoss')\n", - " plt.show()\n", - "\n", - " # Plotting classification error as a function of training iteration\n", - " fig, ax = plt.subplots()\n", - " ax.plot(x_axis, results['validation_0']['error'], label='Train') # for each eval_set\n", - " if results['validation_1']: ax.plot(x_axis, results['validation_1']['error'], label='Test')\n", - " ax.legend()\n", - " plt.ylabel('Error')\n", - " plt.title('Error')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This involves training on less data but allows us to monitor progress to check if the model is becoming over-specific to our training sample. The minimisation of loss and classification error are common metrics for model assessment. As shown below, the cost to performance is negligible. If the test sample gradient were to invert this would be considered overtraining and is why monitoring performance without CV can be a time costly pitfall." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Defining a model with multi-threading set to maximum\n", - "bdt_cv = XGBClassifier(learning_rate = LR, n_estimators=100, seed=123, n_threads=-1)\n", - "\n", - "# Model fitting with CV and printing out processing time\n", - "stime = time.time()\n", - "bdt_cv.fit(X_train, y_train, eval_metric=[\"logloss\",\"error\"],\n", - " eval_set=[(X_train, y_train), (X_test, y_test)], verbose=False)\n", - "print(\"\\nXGBoost cross-validation --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - "# Writing model predictions out for data\n", - "training_monitor(bdt_cv)\n", - "for df in [mc_df, bkg_df, data_df, training_data]:\n", - " df['XGBcv'] = bdt_cv.predict_proba(df[training_columns])[:,1]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Drawing plot of model respone for signal and background classes\n", - "plt.figure()\n", - "plot_comparision('XGB', mc_df, bkg_df)\n", - "plot_comparision('XGBcv', mc_df, bkg_df)\n", - "\n", - "# Drawing the signal efficiency vs background rejection curve (ROC)\n", - "plt.figure()\n", - "plot_roc(bdt, training_data, training_columns)\n", - "plot_roc(bdt_cv, training_data, training_columns)\n", - "\n", - "# Drawing signal significance comparison as a function of minimum cut on model response\n", - "plt.figure()\n", - "bdt_cut = plot_significance(bdt, training_data, training_columns, \"bdt\")\n", - "bdt_cv_cut = plot_significance(bdt_cv, training_data, training_columns, \"bdt_cv\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### $k$-folding & early stopping\n", - "\n", - "Performing CV on each of a number, k, of ways to split your data gives you k models to choose from. Some choose to average the performance across the models from each fold as any instability might imply the model will not be reliable. The results below seem stable; each fold provides a consistant performance across multiple metrics, so we'll just choose the best one." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Defining the folds with a seed to test consistently\n", - "splits = 4 # to match 0.25 value of test_train_split default though this may not be optimal\n", - "kf = KFold(n_splits=splits, shuffle=True, random_state=123)\n", - "\n", - "# Printing processing time of the kfold cross-validation\n", - "stime = time.time()\n", - "for train, test in kf.split(X1):\n", - " X_train, X_test = X1.iloc[train], X1.iloc[test]\n", - " y_train, y_test = y1.iloc[train], y1.iloc[test]\n", - " bdt.fit(X_train,y_train)\n", - "print(\"\\nXGBoost k-folding --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - "# Calculating scores of each fold using variety of CV-metrics\n", - "cv_acc = cross_val_score(bdt, X_test, y_test, cv=splits, scoring=\"accuracy\", n_jobs=-1)\n", - "cv_los = cross_val_score(bdt, X_test, y_test, cv=splits, scoring=\"neg_log_loss\", n_jobs=-1)\n", - "cv_auc = cross_val_score(bdt, X_test, y_test, cv=splits, scoring=\"roc_auc\", n_jobs=-1)\n", - "\n", - "# Printing results and indicating best fold\n", - "print(\"accuracy: \",cv_acc, \" -> best fold =\", np.argmax(cv_acc) )\n", - "print(\"-logloss: \",cv_los, \" -> best fold =\", np.argmax(cv_los) )\n", - "print(\"roc_auc: \",cv_auc, \" -> best fold =\", np.argmax(cv_auc) )\n", - "bestfold = np.argmax(cv_acc)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Early stopping defines a maximum number of rounds the cross-validation metric (we'll use 'error'=1-accuracy) is allowed to not improve before training is terminated. As is standard, we will be reverting back to a 'previous best' model based on test sample score, this helps avoid overtraining. Early stopping prevents us training too many of extra models thus saving time. Set the limit too small though and your training might be cut off prematurely." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def modelfit(alg, metric, params, label, predictors, kfold, fbest, early_stop=10):\n", - "\n", - " # Loading data split inputs providing best fold result\n", - " for k, (train, test) in enumerate(kf.split(params)):\n", - " if (k==fbest):\n", - " X_train, X_test = params.iloc[train], params.iloc[test]\n", - " y_train, y_test = label.iloc[train], label.iloc[test]\n", - "\n", - " # Defining data in terms of training variables and class label\n", - " xgb_param = alg.get_xgb_params()\n", - " data = xgb.DMatrix(params, label=label, feature_names=predictors, nthread=-1)\n", - "\n", - " # Runs timed CV on our model using early stopping based on our metric\n", - " stime = time.time()\n", - " cvresult = xgb.cv(xgb_param,\n", - " data,\n", - " num_boost_round=alg.get_params()['n_estimators'],\n", - " #nfold=cv_folds, # to use in build folding\n", - " folds=kfold, # use -> ignores nfold\n", - " metrics=metric,\n", - " early_stopping_rounds=early_stop)\n", - " alg.set_params(n_estimators=cvresult.shape[0])\n", - " print(\"\\nXGBoost early-stop folding --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - " # Fitting the algorithm on the data with CV evaluation early stopping\n", - " stime = time.time()\n", - " alg.fit(X_train, y_train, eval_metric=[\"logloss\",\"error\"],\n", - " eval_set=[(X_train, y_train), (X_test, y_test)],\n", - " verbose=False, early_stopping_rounds=early_stop)\n", - " training_monitor(alg)\n", - " print(\"XGBoost early-stop limit --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - " # Predicting training set:\n", - " train_predictions = alg.predict(X_train)\n", - " test_predictions = alg.predict(X_test)\n", - "\n", - " # Printing model report:\n", - " print(\"\\nModel Report : best iteration \"+str(cvresult.shape[0]))\n", - " print(\"Train Accuracy : \"+str(metrics.accuracy_score(y_train, train_predictions)))\n", - " print(\"Test Accuracy : \"+str(metrics.accuracy_score(y_test, test_predictions)))\n", - " return cvresult.shape[0]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This function incorporates the k-folding CV and early stopping, saving not only the optimal model but also the index of its training iteration. This means, in our subsequent steps, we can apply an upper limit on training for models based on the convergence of the default hyperparameters, saving us some time. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Defining model with high maximum estimators for use with early stopping\n", - "bdt_es = XGBClassifier(learning_rate = LR, n_estimators=1000,\n", - " # Default values of other hyperparamters\n", - " #max_depth=6, min_child_weight=1,\n", - " #gamma=0, subsample=0.8,\n", - " #colsample_bytree=0.8, scale_pos_weight=1,\n", - " #objective='binary:logistic', # default for binary classification\n", - " #objective='mutli:softprob', num_class=3, # for multiclassifiers\n", - " seed=123, n_jobs=-1)\n", - "\n", - "# Timing the CV using early stopping\n", - "stime = time.time()\n", - "estimators = modelfit(bdt_es, \"error\", X1, y1, training_columns, kf, bestfold)\n", - "print(\"\\nmodelfit(bdt_es) --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - "# Saving model predictions\n", - "for df in [mc_df, bkg_df, data_df, training_data]:\n", - " df['XGBes'] = bdt_es.predict_proba(df[training_columns])[:,1]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This provides us with an improved model as well as a benchmark to test against in both performance and training efficiency. When training using new combinations of hyperparameters, the maximum number of estimators from our model report will cut off any new models improving more slowly than our default, while, for more efficient models, the early stopping will kick in." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Drawing plot to compare model response for signal and background classes\n", - "plt.figure()\n", - "plot_comparision('XGBcv', mc_df, bkg_df)\n", - "plot_comparision('XGBes', mc_df, bkg_df)\n", - "\n", - "# Drawing comaprison of the signal efficiency vs background rejection curve (ROC)\n", - "plt.figure()\n", - "plot_roc(bdt_cv, training_data, training_columns)\n", - "plot_roc(bdt_es, training_data, training_columns)\n", - "\n", - "# Drawing signal significance comparison as a function of minimum cut on model response\n", - "plt.figure()\n", - "bdt_cut_cv = plot_significance(bdt_cv, training_data, training_columns, \"bdt_cv\")\n", - "bdt_cut_es = plot_significance(bdt_es, training_data, training_columns, \"bdt_es\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Hyperameter optimisation\n", - "\n", - "Below we provide a \"grid\" of hyperparameters, defining the structure of the trees and constraints on the learning, but there are many more values to choose from and a larger parameter space to be explored. These optimsations are very problem specific and their impact will have to be weighed against the computing resources and timeframe you have at your disposal. For the sake of expedient demonstration we are comparing the default parameters to only one predetermined variation in 2 parameters. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define a function that performs a gridscan of HPs\n", - "\n", - "\n", - "def hpgridscan(alg, metric, params, label, kfold, fbest, early_stop=10):\n", - "\n", - " # Load data fold with best performance\n", - " for k, (train, test) in enumerate(kf.split(params)):\n", - " if (k==fbest):\n", - " X_train, X_test = params.iloc[train], params.iloc[test]\n", - " y_train, y_test = label.iloc[train], label.iloc[test]\n", - "\n", - " # Define a dictionary of numpy arrays for our HPs\n", - " params = {\n", - " 'max_depth':np.array([7]),\n", - " 'min_child_weight':np.array([3]),\n", - " #'max_depth':np.arange( 5, 9, 1 ),\n", - " #'min_child_weight':np.arange( 1, 5, 1 ),\n", - " ##'gamma':np.arange( 0.0, 1.0, 0.1 ),\n", - " ##'colsample_bytree':np.arange( 0.4, 1.0, 0.1 ),\n", - " ##'subsample':np.arange( 0.4, 1.0, 0.1 ),\n", - " ##'scale_pos_weight':np.arange( 0.4, 1.6, 0.1 )\n", - " }\n", - "\n", - " # Perform timed grid scan with established n_estimator cutoff and early stopping\n", - " stime = time.time()\n", - " gs = GridSearchCV(estimator=alg,\n", - " param_grid=params,\n", - " scoring=metric,\n", - " #iid=False,\n", - " cv=kf,\n", - " n_jobs=-1)\n", - " gs.fit(X_train, y_train, eval_metric=[\"logloss\",\"error\"],\n", - " eval_set=[(X_train, y_train), (X_test, y_test)],\n", - " verbose=False, early_stopping_rounds=early_stop)\n", - " print(\"XGBoost grid-scan --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - " # Return suggested parameters, performance and best model\n", - " training_monitor(gs.best_estimator_)\n", - " print(\"Suggestion:\", gs.best_params_)\n", - " print(\"Accuracy:\" ,gs.best_score_)\n", - " return gs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Running with estimators maximum for shortened training\n", - "bdt_st = XGBClassifier( learning_rate = LR, n_estimators=estimators,\n", - " seed=123, n_jobs=-1)\n", - "\n", - "# Running timed hyperparameter gridscan\n", - "stime = time.time()\n", - "gs = hpgridscan(bdt_st, \"accuracy\", X1, y1, kf, bestfold)\n", - "bdt_gs = gs.best_estimator_\n", - "print(\"\\nhpgridscan(bdt_st) --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - "# Get model predictions\n", - "for df in [mc_df, bkg_df, data_df, training_data]:\n", - " df['XGBgs'] = bdt_gs.predict_proba(df[training_columns])[:,1]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Even this naive grid scan, using the same fold as before for fair comparison, can provide significant improvements as demonstrated above. These may be pushed further by including more hyperparameters for a trade off with processing time. However, even with parrallisation these tasks can take hours or longer and might only provide improvement of O(>1%)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## We could define a model using optimal hyperparameters from our grid scan\n", - "#bdt_opt = XGBClassifier( learning_rate = LR, n_estimators=1000,\n", - "# max_depth=gs.best_params_['max_depth'],\n", - "# min_child_weight=gs.best_params_['min_child_weight'],\n", - "# seed=123, n_jobs=-1 )\n", - "\n", - "## Run with CV early stopping\n", - "#stime = time.time()\n", - "#estimators = modelfit(bdt_opt, 'error', X1, y1, training_columns, kf, bestfold)\n", - "#print(\"\\nmodelfit(bdt_opt) --- %s seconds ---\" % (time.time() - stime))\n", - "\n", - "## Get model predictions\n", - "#for df in [mc_df, bkg_df, data_df, training_data]:\n", - "# df['XGBopt'] = bdt_opt.predict_proba(df[training_columns])[:,1]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Comapring model response from the end of last session to the end of this one\n", - "plt.figure()\n", - "plot_comparision('XGB', mc_df, bkg_df)\n", - "plot_comparision('XGBgs', mc_df, bkg_df)\n", - "\n", - "# Comparing model performance for each level of tuning\n", - "plt.figure()\n", - "plot_roc(bdt, training_data, training_columns)\n", - "plot_roc(bdt_cv, training_data, training_columns)\n", - "plot_roc(bdt_es, training_data, training_columns)\n", - "plot_roc(bdt_gs, training_data, training_columns)\n", - "#plot_roc(bdt_opt, training_data, training_columns)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Comparing the impact on projected performance at each stage of the tutorial\n", - "plt.figure()\n", - "bdt_cut = plot_significance(bdt, training_data, training_columns, \"bdt\")\n", - "bdt_cv_cut = plot_significance(bdt_cv, training_data, training_columns, \"bdt_cv\")\n", - "bdt_es_cut = plot_significance(bdt_es, training_data, training_columns, \"bdt_es\")\n", - "bdt_gs_cut = plot_significance(bdt_gs, training_data, training_columns, \"bdt_gs\")\n", - "#bdt_opt_cut = plot_significance(bdt_opt, training_data, training_columns, \"bdt_opt\")\n", - "\n", - "# Comparing best cuts impact on mass for original and tuned model\n", - "plt.figure()\n", - "data_bdt_cut = data_df.query('XGB > %f' %bdt_cut )\n", - "plot_mass(data_bdt_cut, label='XGB default', norm=True)\n", - "data_gs_cut = data_df.query('XGBgs > %f' %bdt_gs_cut )\n", - "plot_mass(data_gs_cut, label='XGB tuned', norm=True)\n", - "plt.legend(loc='best')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Comparing our data sample's mass plot having applied the cut optimised for $\\sigma=\\frac{S}{\\sqrt{S+B}}$ from each BDT output, we can see how the improved model reduces relative background. However, while we define our signal training sample from MC you'll remember we defined our background training sample from the data !(3.0 < JPsi_M < 3.2).\n", - "\n", - "We can see shoulders at the edges of the regions where we define our background training sample in our data's mass spectrum now. Our training and validation samples include a subset of our data sample so there's potential that our model is learning the difference between MC and data and exploiting that or demonstrating overtraining on the 'previously seen' data (remember we could see our train and test samples beginning to diverge in our validation metrics with more iterations).\n", - "\n", - "Below you can see replotting the normalised mass distribution from just the data not included in training demonstrates no significant improvement. This is not ideal and might be addressed by choosing the setup of our training more carefully. For example, we could train using background from same-sign muon MC across the full mass range (a common practice in LHC experiments) or, using other libraries such as UGBoost to introduce a punishment to the training for introducing a depedance of efficiency on mass." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sig_df = data_df.query('(3.0 < Jpsi_M < 3.2)')\n", - "sig_bdt_cut = sig_df.query('XGB > %f' %bdt_cut )\n", - "plot_mass(sig_bdt_cut, label='XGB default', norm=True)\n", - "sig_gs_cut = sig_df.query('XGBgs > %f' %bdt_gs_cut )\n", - "plot_mass(sig_gs_cut, label='XGB tuned', norm=True)\n", - "plt.legend(loc='best')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can also choose a higher learning rate to perform course scans of your space and decrease it again to retrain your final model. If you can afford to, it might be best to include learning rate itself as a parameter in your grid. With some libraries you can specify your choice of kernel. Both these choices will impact your optimal maximum number of iterations, so setting it sufficiently high and using early stopping might be a good strategy.\n", - "\n", - "For less exhaustive and non-discritised methods try smart combinations of the following to perform adaptive scans or build your own:\n", - "* sklearn.model_selection.RandomizedSearchCV\n", - "* sklearn.model_selection.GridSearchCV\n", - "\n", - "Moving to higher dimentional optimisation problems may require more sophisticated solutions:\n", - "* skopt.BayesSearchCV\n", - "* hyperopt.tpe" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Full stats plots saved here: bit.ly/LHCb_XGB_Tuning\n", - "Run with full stats by removing entrystop at max_events in cell 8." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Final lesson time and processing time check\n", - "print(\"Notebook real time --- %s seconds ---\" % (time.time() - stt))\n", - "print(\"Notebook CPU time --- %s seconds ---\" % (time.process_time() - stc))" - ] - } - ], - "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.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/advanced-python/33ModelTuning.ipynbBKP b/advanced-python/33ModelTuning.ipynbBKP new file mode 100644 index 00000000..38e469e9 --- /dev/null +++ b/advanced-python/33ModelTuning.ipynbBKP @@ -0,0 +1,884 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Model tuning setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "#%store -r training_data\n", + "#%store -r training_columns\n", + "#%store -r bkg_df\n", + "#%store -r mc_df\n", + "#%store -r data_df" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "#@title\n", + "#!pip install uproot\n", + "#!pip install sklearn\n", + "\n", + "import time\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import uproot\n", + "import xgboost as xgb\n", + "from matplotlib import pyplot as plt\n", + "from sklearn import metrics\n", + "from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier\n", + "from sklearn.metrics import auc, roc_curve\n", + "from sklearn.model_selection import (GridSearchCV, KFold, cross_val_score,\n", + " cross_validate, train_test_split)\n", + "from xgboost.sklearn import XGBClassifier" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Time and processing check for the lesson\n", + "stt = time.time()\n", + "stc = time.process_time()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_mass(df, label=\"\", norm=True):\n", + " counts, bins, _ = plt.hist(df['Jpsi_M'], label=label, bins=100, range=[2.75, 3.5], histtype='step', density=norm)\n", + " # You can also use LaTeX in the axis label\n", + " plt.xlabel('$J/\\\\psi$ mass [GeV]')\n", + " plt.xlim(bins[0], bins[-1])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_comparision(var, mc_df, bkg_df):\n", + " _, bins, _ = plt.hist(mc_df[var], bins=100, histtype='step', label='MC', density=1)\n", + " _, bins, _ = plt.hist(bkg_df[var], bins=bins, histtype='step', label='Background', density=1)\n", + " plt.xlabel(var)\n", + " plt.xlim(bins[0], bins[-1])\n", + " plt.legend(loc='best')" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_roc(bdt, training_data, training_columns, label=None):\n", + " y_score = bdt.predict_proba(training_data[training_columns])[:,1]\n", + " fpr, tpr, thresholds = roc_curve(training_data['catagory'], y_score)\n", + " area = auc(fpr, tpr)\n", + "\n", + " plt.plot([0, 1], [0, 1], color='grey', linestyle='--')\n", + " if label:\n", + " plt.plot(fpr, tpr, label=f'{label} (area = {area:.2f})')\n", + " else:\n", + " plt.plot(fpr, tpr, label=f'ROC curve (area = {area:.2f})')\n", + " plt.xlim(0.0, 1.0)\n", + " plt.ylim(0.0, 1.0)\n", + " plt.xlabel('False Positive Rate')\n", + " plt.ylabel('True Positive Rate')\n", + " plt.legend(loc='lower right')\n", + " # We can make the plot look nicer by forcing the grid to be square\n", + " plt.gca().set_aspect('equal', adjustable='box')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_significance(bdt, training_data, training_columns, label):\n", + " y_score = bdt.predict_proba(training_data[training_columns])[:,1]\n", + " fpr, tpr, thresholds = roc_curve(training_data['catagory'], y_score)\n", + "\n", + " n_sig = 1200\n", + " n_bkg = 23000\n", + " S = n_sig*tpr + (n_sig*tpr==0)*1\n", + " B = n_bkg*fpr + (n_bkg*tpr==0)*1\n", + " metric = S/np.sqrt(S+B)\n", + "\n", + " plt.plot(thresholds, metric, label=label)\n", + " plt.xlabel('BDT cut value')\n", + " plt.ylabel('$\\\\frac{S}{\\\\sqrt{S+B}}$')\n", + " plt.xlim(0, 1.0)\n", + "\n", + " optimum = np.max(metric)\n", + " optimal_cut = thresholds[np.argmax(metric)]\n", + " print(label, \": S/sqrt(S+B) =\", optimum, \" at x =\", optimal_cut)\n", + " plt.axvline(x=optimal_cut, color='black', linewidth=1.0, linestyle='--')\n", + "\n", + " return optimal_cut" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_113570/96520257.py:9: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " df.eval('Jpsi_eta = arctanh(Jpsi_PZ/Jpsi_P)', inplace=True)\n", + "/tmp/ipykernel_113570/96520257.py:10: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " df.eval('mup_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)\n", + "/tmp/ipykernel_113570/96520257.py:11: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " df.eval('mum_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)\n", + "/tmp/ipykernel_113570/96520257.py:13: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " bkg_df['catagory'] = 0 # Use 0 for background\n", + "/tmp/ipykernel_113570/96520257.py:17: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " df['IPdiff'] = np.abs(df['mum_PT'] - df['mup_PT'])\n" + ] + } + ], + "source": [ + "#max_entries = 1000 # try running with low stats for bug fixing your changes quickly\n", + "data_df = uproot.open('https://cern.ch/starterkit/data/advanced-python-2018/real_data.root',\n", + " )['DecayTree'].arrays(library='pd')#,entry_stop=max_entries)\n", + "mc_df = uproot.open('https://cern.ch/starterkit/data/advanced-python-2018/simulated_data.root',\n", + " )['DecayTree'].arrays(library='pd')#,entry_stop=max_entries)\n", + "bkg_df = data_df.query('~(3.0 < Jpsi_M < 3.2)')\n", + "\n", + "for df in [mc_df, data_df, bkg_df]:\n", + " df.eval('Jpsi_eta = arctanh(Jpsi_PZ/Jpsi_P)', inplace=True)\n", + " df.eval('mup_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)\n", + " df.eval('mum_P = sqrt(mum_PX**2 + mum_PY**2 + mum_PZ**2)', inplace=True)\n", + "\n", + "bkg_df['catagory'] = 0 # Use 0 for background\n", + "mc_df['catagory'] = 1 # Use 1 for signal\n", + "training_data = pd.concat([bkg_df, mc_df], copy=True, ignore_index=True)\n", + "for df in [mc_df, bkg_df, data_df, training_data]:\n", + " df['IPdiff'] = np.abs(df['mum_PT'] - df['mup_PT'])\n", + "\n", + "training_columns = [\n", + " 'Jpsi_PT',\n", + " 'mup_PT', 'mup_eta', 'mup_ProbNNmu', 'mup_IP',\n", + " 'mum_PT', 'mum_eta', 'mum_ProbNNmu', 'mum_IP',\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Previously we trained an XGBClassifier with the default settings, with learning rate = 0.3 and maximum iterations = 100. This cut off to the training process may be limiting the performance of our model. We can monitor the performance of our model as a function of training iteration and stop the training when the gradient approximates zero. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "XGBoost --- 37.32467699050903 seconds ---\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_113570/3009250180.py:13: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " df['XGB'] = bdt.predict_proba(df[training_columns])[:,1]\n" + ] + } + ], + "source": [ + "X1, y1 = training_data[training_columns], training_data['catagory']\n", + "X_train, X_test, y_train, y_test = train_test_split(X1, y1)\n", + "# default train_size = 0.25, this can be varied to suit your data\n", + "\n", + "LR = 0.3 # the coefficient of step size decay, eta, has alias 'learning_rate' with default 0.3\n", + "\n", + "stime = time.time()\n", + "bdt = XGBClassifier(learning_rate = LR, n_estimators=100, seed=123, n_jobs=-1)\n", + "bdt.fit(training_data[training_columns], training_data['catagory'])\n", + "print(\"XGBoost --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + "for df in [mc_df, bkg_df, data_df, training_data]:\n", + " df['XGB'] = bdt.predict_proba(df[training_columns])[:,1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cross-validation\n", + "\n", + "Splitting the data into randomised subsets for training allows you to monitor your model's performance on the fly using the statistically independant remainder of your sample - this is called cross-validation (CV). We can see below that at the 100th iteration the metrics still show a trend of improvement." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def training_monitor(alg):\n", + "\n", + " # A model trained with eval_set and eval_metric will return evals_result\n", + " results = alg.evals_result()\n", + " epochs = len(results['validation_0']['logloss'])\n", + " x_axis = range(0, epochs)\n", + "\n", + " # Plotting logLoss as a function of training iteration\n", + " fig, ax = plt.subplots()\n", + " ax.plot(x_axis, results['validation_0']['logloss'], label='Train') # for each eval_set\n", + " if results['validation_1']: ax.plot(x_axis, results['validation_1']['logloss'], label='Test')\n", + " ax.legend()\n", + " plt.ylabel('LogLoss')\n", + " plt.title('LogLoss')\n", + " plt.show()\n", + "\n", + " # Plotting classification error as a function of training iteration\n", + " fig, ax = plt.subplots()\n", + " ax.plot(x_axis, results['validation_0']['error'], label='Train') # for each eval_set\n", + " if results['validation_1']: ax.plot(x_axis, results['validation_1']['error'], label='Test')\n", + " ax.legend()\n", + " plt.ylabel('Error')\n", + " plt.title('Error')\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This involves training on less data but allows us to monitor progress to check if the model is becoming over-specific to our training sample. The minimisation of loss and classification error are common metrics for model assessment. As shown below, the cost to performance is negligible. If the test sample gradient were to invert this would be considered overtraining and is why monitoring performance without CV can be a time costly pitfall." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/jonas/mambaforge/envs/anaessentials311/lib/python3.12/site-packages/xgboost/core.py:158: UserWarning: [02:00:05] WARNING: /workspace/src/learner.cc:740: \n", + "Parameters: { \"n_threads\" } are not used.\n", + "\n", + " warnings.warn(smsg, UserWarning)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "XGBoost cross-validation --- 50.098893880844116 seconds ---\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAGxCAYAAACKvAkXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/TGe4hAAAACXBIWXMAAA9hAAAPYQGoP6dpAABlqElEQVR4nO3dd3gVVf7H8fe96T2kVwKhhV5CR4oi2AVdBRvoqqvYVuSnq65rwdVF3VVZC7pWrIiKWFGJhSZYgATpHQIhISQhhfTcO78/RqIxASE3yaR8Xs8zD+RMud87q+azZ845YzMMw0BERESkDbFbXYCIiIhIU1MAEhERkTZHAUhERETaHAUgERERaXMUgERERKTNUQASERGRNkcBSERERNocBSARERFpcxSAREREpM1RABIRS8ydOxebzcbq1aub7DPHjBlDr169muzzRKT5UgASERGRNkcBSERERNocBSARabZWrFjB2LFjCQgIwNfXl+HDh/PZZ5/VedywYcPw9vYmNjaWe++9l5deegmbzcaePXtO6jOdTiePPfYYSUlJeHl5ERERwdSpU9m/f3+N41JTUzn33HOJiIjAy8uLmJgYzjnnnBrHvffeewwZMoSgoCB8fX1JTEzk6quvrte9EJGGpQAkIs3S0qVLOe200ygoKODll19m3rx5BAQEcN555zF//vzq437++WfGjRtHSUkJr732Gs8//zxr167l4Ycfrtfn3nDDDdx5552MGzeOjz/+mH/+85988cUXDB8+nJycHACKi4sZN24cBw8e5NlnnyUlJYXZs2fTvn17ioqKAFi1ahWTJ08mMTGRd955h88++4z77ruPqqoq12+OiLjOEBGxwKuvvmoAxk8//VTn/qFDhxoRERFGUVFRdVtVVZXRq1cvIy4uznA6nYZhGMbFF19s+Pn5GYcOHao+zuFwGD169DAAY/fu3dXto0ePNnr27HnMmjZv3mwAxo033lij/YcffjAA4+9//7thGIaxevVqAzA+/PDDY17rP//5jwEY+fn5x74JImIZ9QCJSLNTXFzMDz/8wEUXXYS/v391u5ubG1OmTGH//v1s3boV+LWnKCwsrPo4u93OpEmTTvpzv/32WwCuuuqqGu2DBw+me/fufP311wB07tyZdu3aceedd/L888+zadOmWtcaNGgQAJMmTeLdd98lIyPjpOsRkcajACQizc7hw4cxDIPo6Oha+2JiYgDIzc2t/jMyMrLWcXW1/ZGj1zzW5x7dHxQUxNKlS+nXrx9///vf6dmzJzExMdx///1UVlYCMGrUKD788EOqqqqYOnUqcXFx9OrVi3nz5p10XSLS8BSARKTZadeuHXa7nczMzFr7Dhw4AFDd4xMaGsrBgwdrHZeVlXXSnxsaGgpwzM/9bS9T7969eeedd8jNzSUtLY3Jkyfz4IMP8vjjj1cfM2HCBL7++msKCgpYsmQJcXFxXHbZZaxateqkaxORhqUAJCLNjp+fH0OGDOGDDz6gtLS0ut3pdPLmm28SFxdH165dARg9ejTffPNN9QDlo8e99957J/25p512GgBvvvlmjfaffvqJzZs3M3bs2Frn2Gw2+vbty5NPPklwcDBr166tdYyXlxejR4/m0UcfBcwZZCJiLXerCxCRtu2bb76pc6r6rFmzGDduHKeeeiq33347np6ezJkzhw0bNjBv3jxsNhsA99xzD5988gljx47lnnvuwcfHh+eff57i4mLAHA/0W4WFhbz//vu1Pi88PJzRo0dz3XXX8fTTT2O32znrrLPYs2cP9957L/Hx8dx2220AfPrpp8yZM4eJEyeSmJiIYRh88MEH5OfnM27cOADuu+8+9u/fz9ixY4mLiyM/P5///ve/eHh4MHr06Ia8hSJSH1aPwhaRtunoLLBjbbt37zaWL19unHbaaYafn5/h4+NjDB061Pjkk09qXWv58uXGkCFDDC8vLyMqKsq44447jEcffbTWLKzRo0cf8/NGjx5tGIY5g+zRRx81unbtanh4eBhhYWHGFVdcYezbt6/6Olu2bDEuvfRSo1OnToaPj48RFBRkDB482Jg7d271MZ9++qlx1llnGbGxsYanp6cRERFhnH322cby5csb76aKyAmzGYZhWBG8REQa0/jx49mzZw/btm2zuhQRaYb0CExEWrwZM2bQv39/4uPjycvL46233iIlJYWXX37Z6tJEpJlSABKRFs/hcHDfffeRlZWFzWajR48evPHGG1xxxRVWlyYizZQegYmIiEibo2nwIiIi0uYoAImIiEibowAkIiIibY4GQdfB6XRy4MABAgICqhdbExERkebNMAyKioqIiYmptQjq7ykA1eHAgQPEx8dbXYaIiIjUw759+4iLizvuMQpAdQgICADMGxgYGGhxNSIiInIiCgsLiY+Pr/49fjwKQHU4+tgrMDBQAUhERKSFOZHhKxoELSIiIm2OApCIiIi0OQpAIiIi0uZoDJCIiEgTcTgcVFZWWl1Gi+bh4YGbm5vL11EAEhERaQJHjhxh//796BWcrrHZbMTFxeHv7+/SdRSAREREGpnD4WD//v34+voSHh6uRXbryTAMDh06xP79++nSpYtLPUEKQCIiIo2ssrISwzAIDw/Hx8fH6nJatPDwcPbs2UNlZaVLAUiDoEVERJqIen5c11D3UAFIRERE2hwFIBEREWlzFIBERESkyYwZM4bp06dbXYYGQYuIiEhtfzTW5sorr2Tu3Lknfd0PPvgADw+PelbVcCzvAZozZw4dO3bE29ub5ORkli9fftzjy8vLueeee0hISMDLy4tOnTrxyiuvVO+fO3cuNput1lZWVtbYX+UPlR/cRvbCu9m38D6rSxERETmuzMzM6m327NkEBgbWaPvvf/9b4/gTXeAxJCTkhN7W3tgsDUDz589n+vTp3HPPPaSmpjJy5EjOOuss0tPTj3nOpEmT+Prrr3n55ZfZunUr8+bNIykpqcYxv/8fKTMzE29v78b+On9o4/adRKybg9fPb1ldioiIWMgwDEoqqizZTnQhxqioqOotKCgIm81W/XNZWRnBwcG8++67jBkzBm9vb958801yc3O59NJLiYuLw9fXl969ezNv3rwa1/39I7AOHTrwr3/9i6uvvpqAgADat2/PCy+80JC3u06WPgJ74oknuOaaa7j22msBmD17Nl9++SXPPfccs2bNqnX8F198wdKlS9m1axchISGAeeN+7+j/SM1NZKe+8BVEGDlUluTj4RtsdUkiImKB0koHPe770pLP3vTgGfh6Nsyv/zvvvJPHH3+cV199FS8vL8rKykhOTubOO+8kMDCQzz77jClTppCYmMiQIUOOeZ3HH3+cf/7zn/z973/n/fff54YbbmDUqFG1OjgakmU9QBUVFaxZs4bx48fXaB8/fjwrV66s85yPP/6YgQMH8thjjxEbG0vXrl25/fbbKS0trXHckSNHSEhIIC4ujnPPPZfU1NTj1lJeXk5hYWGNrTFER0aTbbQDIGvnz43yGSIiIk1l+vTpXHjhhXTs2JGYmBhiY2O5/fbb6devH4mJidxyyy2cccYZvPfee8e9ztlnn82NN95I586dufPOOwkLC2PJ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r3/9K15eXkydOlX3ugHdeeedFBQUkJSUhJubGw6Hg4cffphLL70U0D/XjelE7m1WVhaenp60a9eu1jGu/v5UAGpCNputxs+GYdRqk/q7+eab+fnnn1mxYkWtfbr3rtu3bx+33norixcvxtvb+5jH6V67zul0MnDgQP71r38B0L9/fzZu3Mhzzz3H1KlTq4/TvXbd/PnzefPNN3n77bfp2bMnaWlpTJ8+nZiYGK688srq43SvG0997m1D3H89AmsCYWFhuLm51Uqr2dnZtZKv1M8tt9zCxx9/zLfffktcXFx1e1RUFIDufQNYs2YN2dnZJCcn4+7ujru7O0uXLuWpp57C3d29+n7qXrsuOjqaHj161Gjr3r076enpgP65bkh33HEHd911F5dccgm9e/dmypQp3HbbbcyaNQvQvW5MJ3Jvo6KiqKio4PDhw8c8pr4UgJqAp6cnycnJpKSk1GhPSUlh+PDhFlXVOhiGwc0338wHH3zAN998Q8eOHWvs79ixI1FRUTXufUVFBUuXLtW9P0ljx45l/fr1pKWlVW8DBw7k8ssvJy0tjcTERN3rBjJixIhayzls27aNhIQEQP9cN6SSkhLs9pq/Ct3c3KqnweteN54TubfJycl4eHjUOCYzM5MNGza4fv9dGkItJ+zoNPiXX37Z2LRpkzF9+nTDz8/P2LNnj9WltWg33HCDERQUZCxZssTIzMys3kpKSqqPeeSRR4ygoCDjgw8+MNavX29ceumlmsLaQH47C8wwdK8byo8//mi4u7sbDz/8sLF9+3bjrbfeMnx9fY0333yz+hjd64Zx5ZVXGrGxsdXT4D/44AMjLCzM+Nvf/lZ9jO51/RUVFRmpqalGamqqARhPPPGEkZqaWr0EzInc22nTphlxcXHGV199Zaxdu9Y47bTTNA2+pXn22WeNhIQEw9PT0xgwYED1VG2pP6DO7dVXX60+xul0Gvfff78RFRVleHl5GaNGjTLWr19vXdGtyO8DkO51w/nkk0+MXr16GV5eXkZSUpLxwgsv1Nive90wCgsLjVtvvdVo37694e3tbSQmJhr33HOPUV5eXn2M7nX9ffvtt3X+N/rKK680DOPE7m1paalx8803GyEhIYaPj49x7rnnGunp6S7XZjMMw3CtD0lERESkZdEYIBEREWlzFIBERESkzVEAEhERkTZHAUhERETaHAUgERERaXMUgERERKTNUQASERGRNkcBSERERNocBSARERFpcxSAREREpM1RABIREZE25/8BQKkgrjGXL/oAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkkAAAGxCAYAAAB2qSLdAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/TGe4hAAAACXBIWXMAAA9hAAAPYQGoP6dpAABnH0lEQVR4nO3dd3hUVf7H8fekF0hvhIQkFIEQQAgQihSliahgBVyaZRU7sv5UVhdZXcW6srqCgiiWFRHFjmJQqjQNRXqHQEgICZBK+v39cTUaEzAJydyUz+t55glz5sy937mWfDj3zDk2wzAMRERERKQMB6sLEBEREamLFJJEREREKqCQJCIiIlIBhSQRERGRCigkiYiIiFRAIUlERESkAgpJIiIiIhVQSBIRERGpgEKSiIiISAUUkkSkwZg/fz42m+2cjxUrVlhdoojUI05WFyAiUtPeeust2rVrV649OjragmpEpL5SSBKRBicmJoZu3bpVur9hGOTl5eHu7l7utbNnz+Lm5obNZqt2Pbm5uXh4eFT7/SJiDd1uE5FGx2azcc899/Daa6/Rvn17XF1defvtt0tv13377bfccsstBAYG4uHhQX5+PiUlJTz33HO0a9cOV1dXgoKCGD9+PMeOHStz7AEDBhATE8OqVavo3bs3Hh4e3HLLLRZ9UhG5EBpJEpEGp7i4mKKiojJtNpsNR0fH0ueffvopq1evZtq0aYSEhBAUFMSPP/4IwC233MLw4cN59913ycnJwdnZmTvvvJM5c+Zwzz33cOWVV3L48GH+8Y9/sGLFCjZt2kRAQEDpsZOTkxk7diwPPfQQTz/9NA4O+vuoSH2kkCQiDU7Pnj3LtTk6OpYJTtnZ2Wzbtg1fX9/Stl9D0sCBA3n99ddL23fv3s2cOXO46667eOWVV0rbu3TpQlxcHC+99BJPPfVUafupU6dYtGgRl112WY1+LhGxL4UkEWlw3nnnHdq3b1+m7Y9zii677LIyAen3rrvuujLPly9fDsDEiRPLtPfo0YP27dvz3XfflQlJvr6+CkgiDYBCkog0OO3bt//TidvNmjWr9Gvp6ennfE9oaChHjhyp9LFFpP7QjXIRaZTO9221P77m7+8PmHON/uj48eNl5iP92bFFpP5QSBIR+RO/3jp77733yrT/+OOP7Nq1i4EDB1pRlojUMt1uE5EGZ/v27eW+3QbQqlUrAgMDq3y8tm3bcvvtt/PKK6/g4ODAsGHDSr/dFh4ezgMPPFATZYtIHaOQJCINzs0331xh+9y5c7ntttuqdczZs2fTqlUr5s2bx6uvvoq3tzeXX345M2bMKL0dJyINi80wDMPqIkRERETqGs1JEhEREamAQpKIiIhIBRSSRERERCqgkCQiIiJSAYUkERERkQooJImIiIhUQOskVVNJSQnHjx+nadOm2oJARESknjAMg6ysLEJDQ3FwOP9YkUJSNR0/fpzw8HCryxAREZFqOHr0KGFhYefto5BUTU2bNgXMi+zl5WVxNSIiIlIZmZmZhIeHl/4ePx+FpGr69Rabl5eXQpKIiEg9U5mpMpq4LSIiIlIBhSQRERGRCigkiYiIiFRAc5JERETqkOLiYgoLC60uo95ydHTEycmpRpbnUUgSERGpI7Kzszl27BiGYVhdSr3m4eFBs2bNcHFxuaDjKCSJiIjUAcXFxRw7dgwPDw8CAwO1UHE1GIZBQUEBJ0+e5NChQ7Rp0+ZPF4w8H4UkERGROqCwsBDDMAgMDMTd3d3qcuotd3d3nJ2dOXLkCAUFBbi5uVX7WJq4LSIiUodoBOnCXcjoUZnj1MhRRERERBoYhSQRERGRCigkiYiISJ0yYMAAJk+ebHUZmrgtIiIi1fNn86cmTJjA/Pnzq3zcxYsX4+zsXM2qao5CUh1z/PAe0r99noKQWGKvvtPqckRERM4pOTm59M8LFy5k2rRp7Nmzp7Ttj9/SKywsrFT48fPzq7kiL4But9UxSesW0vH4IpptfRlKiq0uR0RELGIYBrkFRZY8KruYZUhISOnD29sbm81W+jwvLw8fHx8+/PBDBgwYgJubG++99x7p6emMGTOGsLAwPDw86NixIwsWLChz3D/ebouMjOTpp5/mlltuoWnTprRo0YI5c+bU5OWukEaS6piIwXdzevfrhBYf5/RPi/DtMdrqkkRExAJnC4uJnrbUknPvfGIoHi41ExEefvhhXnzxRd566y1cXV3Jy8sjNjaWhx9+GC8vL7766ivGjRtHy5YtiYuLO+dxXnzxRZ588kn+/ve/89FHH3HnnXfSr18/2rVrVyN1VkQjSXVMUIA/3zYdCUDJ6n+DlqYXEZF6bPLkyVx77bVERUURGhpK8+bNefDBB7n44otp2bIl9957L0OHDmXRokXnPc4VV1zBXXfdRevWrXn44YcJCAhgxYoVtVq7RpLqoILYv5KzYhH+WXtg/zJoM9jqkkRExM7cnR3Z+cRQy85dU7p161bmeXFxMc888wwLFy4kKSmJ/Px88vPz8fT0PO9xOnXqVPrnX2/rpaam1lidFVFIqoMu69KW978byF+dllCw4gVcFJJERBodm81WY7e8rPTH8PPiiy/y0ksvMXPmTDp27IinpyeTJ0+moKDgvMf544Rvm81GSUlJjdf7e7rdVgc193FnXdBo8g0nXJLWw5F1VpckIiJSI1avXs2IESMYO3YsnTt3pmXLluzbt8/qsiqkkFRH9bw4ho+L+5pP1vzb2mJERERqSOvWrYmPj2ft2rXs2rWLO+64g5SUFKvLqpBCUh01LKYZrxdfRbFhg33fQvLPVpckIiJywf7xj3/QtWtXhg4dyoABAwgJCWHkyJFWl1Uhm1HZxRCkjMzMTLy9vcnIyMDLy6tWznHVK2v4a+q/uNpxHXS4Fm54q1bOIyIi1svLy+PQoUNERUXh5uZmdTn12vmuZVV+f2skqQ4b1jGE2UVXm092fgondlhaj4iISGOikFSHDYtpxi4jgqUl3cEogUU3Q3621WWJiIg0CgpJdVhUgCftm3nx94JbyHUNgrQ98OVkLTApIiJiBwpJddwVMSGk481Mn6lgc4Rti+CnN60uS0REpMFTSKrjhnVsBsBbx0I42/8fZuM3j8DxzRZWJSIi0vApJNVxrYOa0C6kKYXFBrfv70XxRVdAcQF8OAHOnra6PBERkQZLIakeeOqajrg7O7J6fzpT8m/H8GkBZ47A5/dZXZqIiEiDpZBUD8RG+PLGhG64ODnw2Z5cXvB+FMPBCXZ9DruXWF2eiIhIg6SQVE/0aR3Aa2O74uxo49U9TVnhN8p84euHoCDH2uJEREQaIIWkeuSydsH8Z3QXHGxw17GBnHEJgYyjsPJZq0sTERFpcBSS6pkrOjbjhRs6cxY3/pYz1mxc96pW4xYREbuz2WznfUycOLHax46MjGTmzJk1Vmt1OFl6dqmWa7uGsXzPSb7YChua9CYufy18OQVu/hoclHtFRMQ+kpOTS/+8cOFCpk2bxp49e0rb3N3drSirxug3aj31yLB2uDk7MDljDEWOHnB0PWx5z+qyRESkphiGOefUikcld3YICQkpfXh7e2Oz2cq0rVq1itjYWNzc3GjZsiX//Oc/KSoqKn3/9OnTadGiBa6uroSGhnLffea3tgcMGMCRI0d44IEHSkelrKCRpHqquY87t/drxcvflTDbdiP3Mh/ip0HbK8AzwOryRETkQhXmwtOh1pz778fBxfOCDrF06VLGjh3Lyy+/TN++fTlw4AC33347AI8//jgfffQRL730Eh988AEdOnQgJSWFrVu3ArB48WI6d+7M7bffzl//+tcL/jjVpZGkemxS/5aEeLkxM3sgaZ4XmYtLLvk/q8sSERHhqaee4pFHHmHChAm0bNmSwYMH8+STT/L6668DkJiYSEhICIMGDaJFixb06NGjNBD5+fnh6OhI06ZNS0elrKCRpHrMw8WJqVe04/4PtnBn1kQ+dPwHth2LocNIiB5hdXkiInIhnD3MER2rzn2BEhIS+PHHH3nqqadK24qLi8nLyyM3N5cbbriBmTNn0rJlSy6//HKuuOIKrrrqKpyc6k40qTuVSLVc3TmUt9ce5sfESJaFjWFw2nvmJO6IPrrtJiJSn9lsF3zLy0olJSX885//5Nprry33mpubG+Hh4ezZs4f4+HiWLVvGXXfdxfPPP8/KlStxdna2oOLyLL/dNmvWLKKionBzcyM2NpbVq1efs++aNWvo06cP/v7+uLu7065dO1566aUyfebOnUvfvn3x9fXF19eXQYMGsXHjxjJ9pk+fXu5rilYN5V0om83G41d1AODuY4M569sWctNgyYMWVyYiIo1Z165d2bNnD61bty73cPjlm9ju7u5cffXVvPzyy6xYsYJ169axbds2AFxcXCguLrbyI1g7krRw4UImT57MrFmz6NOnD6+//jrDhg1j586dtGjRolx/T09P7rnnHjp16oSnpydr1qzhjjvuwNPTs3Qy2IoVKxgzZgy9e/fGzc2N5557jiFDhrBjxw6aN29eeqwOHTqwbNmy0ueOjo61/4FrSedwH66PDeOjhGNMzr+d12wPYdvxiXnLrcM1VpcnIiKN0LRp07jyyisJDw/nhhtuwMHBgZ9//plt27bxr3/9i/nz51NcXExcXBweHh68++67uLu7ExERAZjrJK1atYrRo0fj6upKQIAFd0cMC/Xo0cOYNGlSmbZ27doZjzzySKWPcc011xhjx4495+tFRUVG06ZNjbfffru07fHHHzc6d+5c5Xp/LyMjwwCMjIyMCzpOTTmdk290/1e8EfHw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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_113570/3527880945.py:13: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " df['XGBcv'] = bdt_cv.predict_proba(df[training_columns])[:,1]\n" + ] + } + ], + "source": [ + "# Defining a model with multi-threading set to maximum\n", + "bdt_cv = XGBClassifier(learning_rate = LR, n_estimators=100, seed=123, n_threads=-1, eval_metric=[\"logloss\",\"error\"])\n", + "\n", + "# Model fitting with CV and printing out processing time\n", + "stime = time.time()\n", + "bdt_cv.fit(X_train, y_train,\n", + " eval_set=[(X_train, y_train), (X_test, y_test)], verbose=False)\n", + "print(\"\\nXGBoost cross-validation --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + "# Writing model predictions out for data\n", + "training_monitor(bdt_cv)\n", + "for df in [mc_df, bkg_df, data_df, training_data]:\n", + " df['XGBcv'] = bdt_cv.predict_proba(df[training_columns])[:,1]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bdt : S/sqrt(S+B) = 15.405805978293522 at x = 0.80857503\n", + "bdt_cv : S/sqrt(S+B) = 15.179113634975664 at x = 0.7901888\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGwCAYAAAD16iy9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/TGe4hAAAACXBIWXMAAA9hAAAPYQGoP6dpAABWW0lEQVR4nO3deXhTZd4+8Dtbk3RLNyptKdAWC0MRkU0BFzZZRAeXeQVllMXXVwZckFVEFmVTEBmURQcVmBEFFRRHEEUdEBgWLfQ3Dju0QGkLpdC9abae3x+hJz1Z2qakTdLcn+vqJTk5J+dpgpw7z/k+zyMTBEEAERERkQfIvd0AIiIiaj4YLIiIiMhjGCyIiIjIYxgsiIiIyGMYLIiIiMhjGCyIiIjIYxgsiIiIyGOUTX3Cqqoq5ObmIiwsDDKZrKlPT0RERA0gCAJKS0sRHx8Pudx1v0STB4vc3FwkJiY29WmJiIjIA7Kzs9GqVSuXzzd5sAgLCwNgbVh4eHhTn56IiIgaoKSkBImJieJ13JUmDxbVtz/Cw8MZLIiIiPxMXWUMLN4kIiIij2GwICIiIo9hsCAiIiKPafIaCyIi8i0WiwUmk8nbzSAvU6lUUCgUN/06DBZERAFKEARcvnwZRUVF3m4K+YiIiAi0bNnypuaZYrAgIgpQ1aEiNjYWwcHBnLQwgAmCgIqKCuTn5wMA4uLiGvxaDBZERAHIYrGIoSI6OtrbzSEfoNVqAQD5+fmIjY1t8G0RFm8SEQWg6pqK4OBgL7eEfEn134ebqblhsCAiCmC8/UE1eeLvA4MFEREReQxrLIiISCKnSI/CcmOTnS8yJAgJEdomOx81LgYLIiIS5RTpMXDZHuhNliY7p1alwI9T7mO4aCYYLIiISFRYboTeZMFfR3RBu9jQRj/f2fwyTNqcgcJyY72DxZgxY7BhwwY899xzeP/99yXPTZgwAWvWrMHo0aOxfv16ANZhtQsXLsT27duRk5OD2NhYdOnSBZMmTcKAAQM8/SsFPAYLIiJy0C42FJ0SdN5uhkuJiYnYtGkTli9fLg6TrKysxGeffYbWrVuL+50/fx59+vRBREQElixZgs6dO8NkMuH777/HxIkTcfLkSW/9Cs0WgwUREfmdrl27IjMzE1u3bsWoUaMAAFu3bkViYiKSk5PF/SZMmACZTIbDhw8jJCRE3J6WloZx48Y1ebv9SV5ZHgoNheLjstKyeh3HYEFERH5p7NixWLdunRgsPv74Y4wbNw67d+8GAFy/fh07d+7EwoULJaGiWkRERBO21r/kl+fjTzv/BL1ZL26z6OtXd8NgQUREfumpp57CzJkzcf78echkMuzfvx+bNm0Sg8XZs2chCAI6dOjg3Yb6oRJjCfRmPRbfsxjJOmsP0H8v/RcjMKLOYxksiIjIL8XExGDYsGHYsGEDBEHAsGHDEBMTIz4vCAIATgJ2M5J1yegY3RFA/W+FcIIsIiLyW+PGjcP69euxYcMGh5qJW2+9FTKZDCdOnPBS6wITgwUREfmtIUOGwGg0wmg0YvDgwZLnoqKiMHjwYKxatQrl5eUOx3K5+MbBWyFEROTgbH79ur29fR6FQiH2SDhbjXP16tXo3bs3evbsiTfeeAOdO3eG2WzGrl27sGbNGvZmNAIGCyIiEkWGBEGrUmDS5owmO6dWpUBkSFCDjw8PD3f5XFJSEo4cOYKFCxdiypQpyMvLQ4sWLdCtWzesWbOmweck1xgsiIhIlBChxY9T7vPptUKqZ9R05euvv5Y8jouLw8qVK7Fy5coGtI7cxWBBREQSCRFarttBDcbiTSIiIvIYBgsiIiLyGAYLIiIi8hgGCyIiIvIYBgsiIiLyGAYLIiIi8hgGCyIiIvIYzmNBRERSRdlAxbWmO19wNBCR2HTnq4e2bdti0qRJmDRpkreb4jHr16/HpEmTGn2NFAYLIiKyKcoGVvUETBVNd05VMDDxcL3DxZgxY7BhwwbxcVRUFHr06IElS5agc+fOjdVKqicGCyIisqm4Zg0Vj64FYlIb/3wFp4Gtz1rP60avxZAhQ7Bu3ToAwOXLl/Haa6/hwQcfxMWLFxurpTfNaDQiKKjha6L4C9ZYEBGRo5hUIL5L4/80MLyo1Wq0bNkSLVu2RJcuXTBjxgxkZ2fj6tWrAIAZM2YgNTUVwcHBSE5OxuzZs2EymSSv8c0336B79+7QaDSIiYnBo48+6vJ869atg06nw65duwAApaWlGDVqFEJCQhAXF4fly5ejb9++klsnbdu2xYIFCzBmzBjodDo8++yzAIAtW7YgLS0NarUabdu2xbJlyyTnkslkDuudREREiGuknD9/HjKZDFu3bkW/fv0QHByM22+/HQcOHJAcs379erRu3RrBwcF45JFHcO1a09zeYrAgIiK/VlZWho0bN6Jdu3aIjo4GAISFhWH9+vU4fvw4VqxYgbVr12L58uXiMdu3b8ejjz6KYcOG4ejRo/jpp5/QvXt3p6//9ttvY+rUqfj+++9x//33AwAmT56M/fv345tvvsGuXbuwd+9eHDlyxOHYpUuXolOnTkhPT8fs2bORnp6Oxx9/HCNHjsTvv/+OefPmYfbs2XUurObMrFmzMHXqVGRkZCA1NRVPPPEEzGYzAODQoUMYN24cJkyYgIyMDPTr1w8LFixw+xwNIjSx4uJiAYBQXFzc1KcmIqIb9Hq9cPz4cUGv10ufyDkqCHPDrf9tCg043+jRowWFQiGEhIQIISEhAgAhLi5OSE9Pd3nMkiVLhG7duomPe/XqJYwaNcrl/m3atBGWL18uvPLKK0JcXJzwn//8R3yupKREUKlUwhdffCFuKyoqEoKDg4WXXnpJ8hoPP/yw5HWffPJJ4f7775dsmzZtmtCxY0fxMQDhq6++kuyj0+mEdevWCYIgCFlZWQIA4cMPPxSfP3bsmABAOHHihCAIgvDEE08IQ4YMkbzGiBEjBJ1O5/J3FgTb34vfc38XOq3vJBwrOCY+dyjrUL2u3+yxICIiv9OvXz9kZGQgIyMDhw4dwqBBgzB06FBcuHABAPDll1/i7rvvRsuWLREaGorZs2dL6i8yMjIwYMCAWs+xbNkyfPDBB9i3bx9uu+02cXtmZiZMJhN69uwpbtPpdGjfvr3Da9j3gpw4cQJ9+vSRbOvTpw/OnDkDi8VS/zcAkBSqxsXFAQDy8/PF8/Tq1Uuyv/3jxsJgQUREfickJATt2rVDu3bt0LNnT3z00UcoLy/H2rVrcfDgQYwcORJDhw7Ft99+i6NHj2LWrFkwGo3i8Vpt3cvC33PPPbBYLPj8888l262dCtZaCGfb7dtpv09dx8lkModt9vUhAKBSqSTHAEBVVZXLtjTEufwy/DenGP/NKUbW1bJ6HcNRIURE5PdkMhnkcjn0ej3279+PNm3aYNasWeLz1T0Z1Tp37oyffvoJY8eOdfmaPXv2xAsvvIDBgwdDoVBg2rRpAICUlBSoVCocPnwYiYnWkSwlJSU4c+YM7rvvvlrb2bFjR+zbt0+y7d///jdSU1OhUCgAAC1atEBeXp74/JkzZ1BR4d7w344dO+LgwYOSbfaPa3O9whrCXtqcgapKa0EsZGfrdSyDBRER+R2DwYDLly8DAAoLC7Fy5UqUlZXhoYceQnFxMS5evIhNmzahR48e2L59O7766ivJ8XPnzsWAAQOQkpKCkSNHwmw247vvvsP06dMl+/Xq1QvfffcdhgwZAqVSiZdffhlhYWEYPXo0pk2bhqioKMTGxmLu3LmQy+UOvRH2pkyZgh49emD+/PkYMWIEDhw4gJUrV2L16tXiPv3798fKlStx1113oaqqCjNmzJD0TtTHiy++iN69e2PJkiV4+OGH8cMPP2Dnzp31Pr680loEOvX+VNzb9g4AwPfHgKlv1n0sgwURETkqOO3T59m5c6dYVxAWFoYOHTrgiy++QN++fQEAL7/8Mp5//nkYDAYMGzYMs2fPxrx588Tj+/btiy+++ALz58/Hm2++ifDwcNx7771Oz9WnTx9s374dDzzwABQKBV588UW88847GD9+PB588EGEh4dj+vTpyM7OhkajqbXdXbt2xeeff445c+Zg/vz5iIuLwxtvvIExY8aI+yxbtgxjx47Fvffei/j4eKxYsQLp6eluvT933XUXPvzwQ8ydOxfz5s3DwIED8dprr2H+/PluvU5iVDA6JegAAMdy6759BAAywVM3YuqppKQEOp0OxcXFCA8Pb8pTExHRDZWVlcjKykJSUpL0YugHM2/6ovLyciQkJGDZsmV45plnvN2cBqv+e3G+qhTTf3sGi3p+hIf+YC1S3fzrzxjZc0Cd12/2WBARkU1EovUiH+BrhdTl6NGjOHnyJHr27Ini4mK88cYbAIDhw4d7uWXex2BBRERSEYl+d6H3hrfffhunTp1CUFAQunXrhr179yImJsbbzfI6BgsiIiI33XHHHW7XPQQKzmNBREREHsNgQURERB7DYEFEREQe41awmDdvHmQymeSnZcuWjdU2IiIi8jNuF2+mpaXhxx9/FB9XT0FKRERE5HawUCqV7KUgIiIip9wOFmfOnEF8fDzUajXuvPNOLFq0CMnJyS73NxgMMBgM4uOSkpKGtZSIiJpEXlkeCg2FTXa+SHUk4kLjmux81LjcChZ33nkn/v73vyM1NRVXrlzBggUL0Lt3bxw7dgzR0dFOj1m8eDFef/11jzSWiIgaV15ZHoZvGw69Wd9k59Qqtdg2fFu9w8WYMWOwYcMGPPfcc3j//fclz02YMAFr1qzB6NGjsX79egDA5cuXsXDhQmzfvh05OTmIjY1Fly5dMGnSJAwYMMDTv07AcytYDB06VPzzbbfdhl69eiElJQUbNmzA5MmTnR4zc+ZMyXMlJSXiMrNERORbCg2F0Jv1WHzPYiTrXPdGe0pmcSZm7p2JQkOhW70WiYmJ2LRpE5YvXw6t1ro4VmVlJT777DO0bt1a3O/8+fPo06cPIiIisGTJEnTu3Bkmkwnff/89Jk6ciJMnT3r8dwp0NzXzZkhICG677TacOXPG5T5qtRpqtfpmTkNERE0sWZeMjtEdvd0Ml7p27YrMzExs3boVo0aNAgBs3boViYmJktvzEyZMgEwmw+HDhxESEiJuT0tLw7hx45q83YHgpuaxMBgMOHHihLh0LRERUVMZO3Ys1q1bJz7++OOPJWHh+vXr2LlzJyZOnCgJFdUiIiKaopkBx61gMXXqVOzZswdZWVk4dOgQ/vSnP6GkpASjR49urPYRERE59dRTT2Hfvn04f/48Lly4gP379+PPf/6z+PzZs2chCAI6dOjgxVYGHrduhVy6dAlPPPEECgoK0KJFC9x11104ePAg2rRp01jtIyIiciomJgbDhg3Dhg0bIAgChg0bJlldVBAEAIBMJvNWEwOSW8Fi06ZNjdUOIiIit40bNw7PP/88AGDVqlWS52699VbIZDKcOHECDz/8sBdaF5i4VggREfmtIUOGwGg0wmg0YvDgwZLnoqKiMHjwYKxatQrl5eUOxxYVFTVRKwMLgwUREfkthUKBEydO4MSJE06XmFi9ejUsFgt69uyJLVu24MyZMzhx4gTeffdd9OrVywstbv5uargpERE1T5nFmX5znvDwcJfPJSUl4ciRI1i4cCGmTJmCvLw8tGjRAt26dcOaNWtu+tzkiMGCiIhEkepIaJVazNw7s8nOqVVqEamOrPf+1TNquvL1119LHsfFxWHlypVYuXJlA1pH7mKwICIiUVxoHLYN38a1QqjBGCyIiEgiLjSOF3pqMBZvEhERkccwWBAREZHHMFgQERGRxzBYEBERkccwWBAREZHHeG9USN5/gLJQ65+Do4GIRK81hYiIiDzDe8Fi/QOA+saKc6pgYOJhhgsiIiI/571gMWYHEBYKFJwGtj4LVFxjsCAi8gGm3FyYC5tugixlZCRU8fFNdr76aNu2LSZNmoRJkyZ5uykes379ekyaNKnRF1/zXrCI6wzUMr87ERE1PVNuLs4NexCCXt9k55RptUjZ/m29w8WYMWOwYcMG8XFUVBR69OiBJUuWoHPnzo3VTKonzrxJREQic2EhBL0e8UuXICg5udHPZ8zMRO606TAXFrrVazFkyBCsW7cOAHD58mW89tprePDBB3Hx4sXGaupNMxqNCAoK8nYzGh1HhRARkYOg5GRo09Ia/aeh4UWtVqNly5Zo2bIlunTpghkzZiA7OxtXr14FAMyYMQOpqakIDg5GcnIyZs+eDZPJJHmNb775Bt27d4dGo0FMTAweffRRl+dbt24ddDoddu3aBQAoLS3FqFGjEBISgri4OCxfvhx9+/aV3Dpp27YtFixYgDFjxkCn0+HZZ58FAGzZsgVpaWlQq9Vo27Ytli1bJjmXTCZzWEgtIiJCXHzt/PnzkMlk2Lp1K/r164fg4GDcfvvtOHDggOSY9evXo3Xr1ggODsYjjzyCa9eu1fv9vRkMFkRE5NfKysqwceNGtGvXDtHR0QCAsLAwrF+/HsePH8eKFSuwdu1aLF++XDxm+/btePTRRzFs2DAcPXoUP/30E7p37+709d9++21MnToV33//Pe6//34AwOTJk7F//35888032LVrF/bu3YsjR444HLt06VJ06tQJ6enpmD17NtLT0/H4449j5MiR+P333zFv3jzMnj27zhVbnZk1axamTp2KjIwMpKam4oknnoDZbAYAHDp0COPGjcOECROQkZGBfv36YcGCBW6foyF4K4SIiPzOt99+i9BQ65QF5eXliIuLw7fffgu53Pp9+bXXXhP3bdu2LaZMmYLNmzdj+vTpAICFCxdi5MiReP3118X9br/9dofzzJw5Exs2bMDu3btx2223AbD2VmzYsAGffvopBgwYAMDaoxHv5FZO//79MXXqVPHxqFGjMGDAAMyePRsAkJqaiuPHj2Pp0qUYM2aMW+/B1KlTMWzYMADA66+/jrS0NJw9exYdOnTAihUrMHjwYLzyyivief79739j586dbp2jIbzWY6E/cQL6Y8egP50F/XUVTFcKvNUUIiLyM/369UNGRgYyMjJw6NAhDBo0CEOHDsWFCxcAAF9++SXuvvtutGzZEqGhoZg9e7ak/iIjI0MMBa4sW7YMH3zwAfbt2yeGCgDIzMyEyWRCz549xW06nQ7t27d3eA37XpATJ06gT58+km19+vTBmTNnYLFY6v8GAJJC1bg462q0+fn54nl69eol2d/+cWPxWrC4+OencP6xP+H8+Fdx/ocWODd2Kky5ud5qDhER+ZGQkBC0a9cO7dq1Q8+ePfHRRx+hvLwca9euxcGDBzFy5EgMHToU3377LY4ePYpZs2bBaDSKx2u12jrPcc8998BiseDzzz+XbBcEAYC1FsLZdvt22u9T13Eymcxhm319CACoVCrJMQBQVVXlsi1NxWvBouWC+Wi75Uu0fX8R4u8qhFBpaNJx00RE1HzIZDLI5XLo9Xrs378fbdq0waxZs9C9e3fceuutYk9Gtc6dO+Onn36q9TV79uyJnTt3YtGiRVi6dKm4PSUlBSqVCocPHxa3lZSU4MyZM3W2s2PHjti3b59k27///W+kpqZCoVAAAFq0aIG8vDzx+TNnzqCioqLO17Y/z8GDByXb7B83Fq/VWAS1bQttWhqQawLCzd5qBhEROWHMzPTp8xgMBly+fBkAUFhYiJUrV6KsrAwPPfQQiouLcfHiRWzatAk9evTA9u3b8dVXX0mOnzt3LgYMGICUlBSMHDkSZrMZ3333nViDUa1Xr1747rvvMGTIECiVSrz88ssICwvD6NGjMW3aNERFRSE2NhZz586FXC536I2wN2XKFPTo0QPz58/HiBEjcODAAaxcuRKrV68W9+nfvz9WrlyJu+66C1VVVZgxY4akd6I+XnzxRfTu3RtLlizBww8/jB9++KFJ6isAFm8SEVENyshIyLRa5E6bXvfOHiLTaqGMjHTrmJ07d4p1BWFhYejQoQO++OIL9O3bFwDw8ssv4/nnn4fBYMCwYcMwe/ZszJs3Tzy+b9+++OKLLzB//ny8+eabCA8Px7333uv0XH369MH27dvxwAMPQKFQ4MUXX8Q777yD8ePH48EHH0R4eDimT5+O7OxsaDSaWtvdtWtXfP7555gzZw7mz5+PuLg4vPHGG5LCzWXLlmHs2LG49957ER8fjxUrViA9Pd2t9+euu+7Chx9+iLlz52LevHkYOHAgXnvtNcyfP9+t12kImdDEN2JKSkqg0+lw+eBB3HLnnUBuBvRvDsT5H1qg7ZYvrb0YRETUqCorK5GVlYWkpCSHiyGn9HZfeXk5EhISsGzZMjzzzDPebk6DVf+9OF9Vium/PYNFPT/CQ3+wFqlu/vVnjOw5AMXFxQivZeZs9lgQEZGEKj7e7y/0je3o0aM4efIkevbsieLiYrzxxhsAgOHDh3u5Zd7HYEFERNQAb7/9Nk6dOoWgoCB069YNe/fuRUxMjLeb5XUMFkRERG6644473K57CBSc0puIiIg8hsGCiCiAeXMiJfI91X8fbuZvBYMFEVEAqp4Xwd2Jl6h5q/77UFX7dBy1Yo0FEVEAUigUiIiIENeWCA4OrnNyJ2q+BEFARUUF8vPzERERAaGotMGvxWBBRBSgWrZsCcC2cBVRRESE9e9FUXaDX4PBgogoQMlkMsTFxSE2NtbpIlcUWFQqlbheyc1gsCAiCnAKhcIjFxQigMWbRERE5EEMFkREROQxDBZERETkMQwWRERE5DEMFkREROQxDBZERETkMQwWRERE5DEMFkREROQxDBZERETkMQwWRERE5DEMFkREROQxDBZERETkMQwWRERE5DEMFkREROQxDBZERETkMQwWRERE5DEMFkREROQxNxUsFi9eDJlMhkmTJnmoOUREROTPGhwsfv31V/ztb39D586dPdkeIiIi8mMNChZlZWUYNWoU1q5di8jISE+3iYiIiPxUg4LFxIkTMWzYMAwcOLDOfQ0GA0pKSiQ/RERE1Dwp3T1g06ZNOHLkCH799dd67b948WK8/vrrbjeMiIiI/I9bPRbZ2dl46aWX8Mknn0Cj0dTrmJkzZ6K4uFj8yc7OblBDiYiIyPe51WORnp6O/Px8dOvWTdxmsVjwyy+/YOXKlTAYDFAoFJJj1Go11Gq1Z1pLREREPs2tYDFgwAD8/vvvkm1jx45Fhw4dMGPGDIdQQURERIHFrWARFhaGTp06SbaFhIQgOjraYTsREREFHs68SURERB7j9qgQe7t37/ZAM4iIiKg5YI8FEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXmM0tsNkLh6Gsg1Wf8cHA1EJHq3PUREROQW7weL4GhAqQEAGNf/BQg3W7crNVD+3zaoOnTzYuOIiIjIHd4PFhGJUP7fNsh+fga5ByMlT8l+fgYpO3ZAFR/vpcYRERGRO7wfLACoOnRDyo4dMBcWWjdcPQ3j+r8g92AkzIWFDBZERER+wieCBQCo4uNtASLXZLslQkRERH7DZ4IFEREReUdOkR6F5Ubxcfb1iga/FoMFERFRAMsp0mPgsj3QmyziNrkmByFJQJhW5fbrMVgQEREFsMJyI/QmC/46ogvaxYYCALJKTuHVw0BsmNrt12OwICIiIrSLDUWnBB0AQK4JbfDrcOZNIiIi8hj2WBAREQU4mbIIWSWnxJ6KzOLMBr8WgwUREVEAK6i8jJCUZXj1sEmyXavUIlId6eIo1xgsiIiIAlipsRgyuQkT0+bg3qQ0cXukOhJxoXFuvx6DBRERESEhpA06Rne86ddh8SYRERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXmMW8FizZo16Ny5M8LDwxEeHo5evXrhu+++a6y2ERERkZ9xK1i0atUKb775Jn777Tf89ttv6N+/P4YPH45jx441VvuIiIjIj7g1QdZDDz0kebxw4UKsWbMGBw8eRFpamtNjDAYDDAaD+LikpKQBzSQiIiJ/0OAaC4vFgk2bNqG8vBy9evVyud/ixYuh0+nEn8TExIaekoiIiHyc28Hi999/R2hoKNRqNcaPH4+vvvoKHTu6ngJ05syZKC4uFn+ys7NvqsFERETku9xeK6R9+/bIyMhAUVERtmzZgtGjR2PPnj0uw4VarYZarb7phhIREZHvcztYBAUFoV27dgCA7t2749dff8WKFSvwwQcfeLxxRERE5F9ueh4LQRAkxZlEREQUuNzqsXj11VcxdOhQJCYmorS0FJs2bcLu3buxc+fOxmofERER+RG3gsWVK1fw1FNPIS8vDzqdDp07d8bOnTtx//33N1b7iIiIyI+4FSw++uijxmoHERERNQNcK4SIiIg8hsGCiIiIPIbBgoiIiDyGwYKIiIg8hsGCiIiIPIbBgoiIiDzG7Sm9iYiIyH/lFOlRWG4UH2dfr/Do6zNYEBERBYicIj0GrvgaBqFU3CYPyoc2AQjTqjxyDgYLIiKiAHHm2kUoWi9FiNwk2a5WaNA+pqVHzuHzwcKYmSn+WRkZCVV8vBdbQ0RE5L9KjcWQyU2YmDYH9yalidsj1ZGIC43zyDl8Nlgo1VWQadTInTZd3CbTapGy/VuGCyIiopuQENIGHaM7Nspr+2ywUIVYkLLubZiDrAnKmJmJ3GnTYS4sZLAgIiLyUT4bLABAdUsMVPFpde9IREREPsGngwUKTtv+fDXLe+0gIiKievHNYBEcDaiCga3P2rZdVwFoAZReAcBeDCIiIl/km8EiIhGYeBiouGbbdng38MMHQGWx15pFREREtfPNYAFYw0VEYo3HvBVCRETk67hWCBEREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReYzvDjclIiKim5JTpEdhuVF8nH29otHPyWBBRETUDOUU6TFw2R7oTRZxm1yTg5AkIEyrarTzMlgQERE1Q4XlRuhNFvx1RBe0iw0FAGSVnMKrh4HYMHWjnZfBgoiIqBlrFxuKTgk6AIBcE9ro52PxJhEREXkMgwURERF5DIMFEREReQxrLIiIiJopmbIIWSWnxNqKzOLMRj8ngwUREVEzVFB5GSEpy/DqYZNku1apRaQ6stHOy2BBRETUDJUaiyGTmzAxbQ7uTUoTt0eqIxEXGtdo52WwICIiasYSQtqgY3THJjsfizeJiIjIY/yvx6LoApCbYXscHA1EJHqtOURERL7AG+uCOOM/wUJjnTXM+PWbwM8LxM3K0CCoph9iuCAiooDlrXVBnPGbYKFs3QEyjRq5B6WVrDJFFVL+5xRUdzBYEBFRYPLWuiDO+E2wUMXHI2XHDpgLC8VtxvR/IXfRKpiLS9G0eYyIiMj3NPW6IM74TbAArOFCFR9v23D1tPcaQ0RE5EO8MRmWM34VLIiIiMiRtybDcobBgoiIyM95azIsZxgsiIiImommngzLGU6QRURERB7DYEFEREQew2BBREREHsNgQURERB7TLIo3jRdzgGPHxMfKyEjpfBdERETUJPw6WCh1YZApqpC7aBWAVeJ2mVaLlO3fMlwQERE1Mb8OFqpbYpDywFWY+8wBItoAsPZe5C5aBXNhIYMFERE1S0dzs3Cp+Kr4+EjeKS+2RsqvgwWCo6GKUEOVMde27boKQAug9AqANFdHEhER+aWjuVl46vvHIJNLZ9kUqlRopWvhpVbZ+HewiEgEJh4GKq7Zth3eDfzwAVBZ7LVmEREReUpOkR6F5Ubx8YHzFyCTm/CnxOnoGtde3N5K1wJ3xCd5o4kSbgWLxYsXY+vWrTh58iS0Wi169+6Nt956C+3bt6/74MYSkWj9ER9nea8tREREHpRTpMfAZXugN1nEbXJNDkKSgPuSO6Fv2zu82Drn3AoWe/bswcSJE9GjRw+YzWbMmjULgwYNwvHjxxESEtJYbSQiIgpIheVG6E0W/HVEF7SLta5amlVyCq8eBmLD1F5unXNuBYudO3dKHq9btw6xsbFIT0/Hvffe6/QYg8EAg8EgPi4pKWlAM4mIiAJXu9hQdErQAYC4LLqvuqkJsoqLrXUMUVFRLvdZvHgxdDqd+JOYmOhyXyIiIvJvDQ4WgiBg8uTJuPvuu9GpUyeX+82cORPFxcXiT3Z2dkNPSUREFHBkyiJklZzC8WvHcfzacWQWZ3q7SbVq8KiQ559/Hv/5z3+wb9++WvdTq9VQq5v+PhBn4yQiIn9XUHkZISnL8Oph6dBSrVKLSHWkl1pVuwYFixdeeAHffPMNfvnlF7Rq1crTbbopnI2TiIj8VV5ZHgoNheLjk4XHIJObMDFtDu5Nss3NFKmORFxonDeaWCe3goUgCHjhhRfw1VdfYffu3UhK8v54WXvibJzDPgJapAIAjJmZyJ02nbNxEhGRz8ory8NDX/8RBkulZLtQpUKHyM7oGO3FqR3c4FawmDhxIj799FNs27YNYWFhuHz5MgBAp9NBq9U2SgMbQhVigSo1CYjnzJtEROQfThVchsFSCX3OCFQZY8XtalkYbo1u7cWWucetYLFmzRoAQN++fSXb161bhzFjxniqTZ5RcNr256ucNIuIiHxbqd5aRzH5vrtxb42JryJDgpAQ4Ttf3uvi9q0QnxccDaiCga3P2rZx/RAiIvITiVHB4pwV/si/1wpxhuuHEBEReU3zCxYA1w8hIiLykpuaeZOIiIiopubZY+FK0QUgN8P65+Boaa8GERFRE7KfsyKn/IIXW+M5gREsNDeKYH5eAGTMtf5ZFWytxWC4ICKiJlbbnBVhQf5buAkESrAIu8X630fXAqlJ1qGoW5+1FngyWBARkQflFOlRWG6UbLMfMtpc5qxwJjCCxQ3GUiVQqLIOP72ugvJKAVSciJOIiDwkp0iPgcv2QG+ySLZrVQq8/1Q3RIcEAQCO51hHKfr7nBXOBESwUEZGQqbVInfa9BpbW0D2y1Sk7NjBab6JiMgjCsuN0Jss+OuILmgXGwoAuFZuxPjPfsTYT78S95MH5UObAHRM0Pn1nBXOBESwUMXHI2X7tzAX3iiSuXoaxvV/Qe7BSK4fQkREHtcuNlQMDHlleQhJeQdKu3oKtUKD9jEtvdG8RhUQwQKwhgsxQOSagHCzdxtEREQBodBQCIOlEovvWYxkXbK43ZdXKL0ZARMsiIiIPM1+yGhWSRlkyiKn+ybrktExumMTtcx7GCyIiIgaIK8sD8O3DYferJdsD0lRoaCyO4DmVTtRXwwWREREduozZLTQUAi9WS+5xfFL1jGsOvYGSo2BuzZVwAcLY2am5LEyMpLFnEREASynSI+BK76GQSiVbFfLwvDjSw87DAcVDLGoqkwAAFQZ8pusnb4qYIOFUl0FmUZtNwQVkGm1SNn+LcMFEVGAsK+TOJpzCYrWSxEiN0n2E6pUOHOtKxIi2gMA8ksNAICXNmegqvIqAECuyUFIElBkzsHxa8cBAJnF0i+wzV3ABgtViAUp696GOchWkWvMzETutOkcgkpE1EzZ3+IoqLyMqQf+7DC1NqDCK3cswx0JrQDYbnHsuXBYvM1xJO8UAGDq/aniJFfW11uLJUfmAEdsr6ZVahGpjmy8X8yHBGywAADVLTFQxad5uxlEFIDqcw8fcPw2DTgfpljf/QKZs1kxrT0MlZje9Q10i7f2RJzLL8OLG8/g9kF3omO0tQDTYNBi5e8qfJm9BF9m215TqFKhV1IbdIqvLtTU4Z+3fBPQn0VABwsiIm+obdrnH6fcJ4YLV6MOtEottg3fJl6o6rufP/JkYHI2K+Yv549izVkgQpkgDgWtqiyGYL4qOfaO+CT8Y/AWXCqWbm+la4E74pMk2+JC4/z6Pb9ZgR0sCk5LH5de9047iCigOLvAnc0vw6TNGSgsN4rBwtmog8ziTMzcOxOFhkLx4lW938S0OUgIaQPAugT3qmNvSPbzJfXpsaktMC3vuxyRGtuthfqGDZmyCApNDuQa6/suV1+t4wibO+KTHEIEOQrMYBEcbV02feuz0u0lYQDCOFKEKMDV9zZFQ1/vbH4ZAOm0z7Wpa2Kl6iLCJd8WoqoyGAAg1xQiJMn6XMfoBjW70dS3x8ZZsCqsLMTLu1/G+B/HS4+tR+9MQeVlhKQsw6uHHYsy/X2pcl8SmMEiIhGYeNi6bHq1gtNQ/mM8ZOoopyNFWr37LhRRtnTMsEHUPNX3oueKfdd9fqkBf/kkHQZzlfT1tGGIvLHSZTWZsgi/nD+KrBJrOMgpv1CvNpfqrRfKmkWE1V38ezL/Kz4fplWhfUxLycW3viHK2X6A9WJdc86G+pzjbH5ZvXpsqtkHq23Dt0ne4+penG0n94k9NoDjbYpSYzFkchMmps3BvUnW+rrqeoqiLqH4740VR6uDHzVMYAYLwBouIhJtj4OjoYpQI2XQJZgNcnGzxaLFpX1ByH5W2rvBYalEzVNhuRGVwjVMfygBiVHWC3z29Qos/S7H6UWvJldd98pEx39s1QoN5Mq+AKyvZ5JdR0jKMqw56/ht+lRuFaoqrRe9rBLrRe+XrGM4d+MCWD06ITEqWOwBMcnaYPVpx2JDtUKDfz78DeJC48QQVSlcg0xZbttHKcen4waIF2VXYUumLEJIyjLI7IZlqhUarOj3V0RqIsVgVVmphWCOEPfRqhTokRQleT9lyiJklZwSb1O4GqZZZY4Qe2YAwFReBaFKhVXH3nB47/4xeIvD7YuEkDZiUNEp9NDIrmPS5gzJPlqVwiH4Uf0EbrCwd6MXQ1VxDarqbRUFwOankDIoWxI2jOXByN0PVKSnI6jQlprZi0Hk/6q7y+0v8M6mabbvncgsznTouj+XX4aXNmdgxYguSIm1XTDt6yTUar34bbr6W3ex3oSF/7yIl09dAGDtvbBezJ1fRFvpWoiP7YsNs69X4J09+4CEzeI3++zrFTAGnUBkm40wCQbJ6z3z4/uY0mURwlURyL5eAYPiIuY92APdW9kW0coqOYVXD9vanH29Ast+Pgp1208ltyqUiUCkTI13+mxEjMa6mqd9r4ir2xT2wzRd9ihpp2HWQ62h01r/BT+SdwpfZi/BgawLUAlR4ntgLyFCix+n3OfRW1+BjsGiJvteDMAxbFTfMtE4v2XCXgwi31WfEQbOusudTdOcV5aHh77+o8P8B2qFBt1iu4mvWVVZjKrKq0gKby8OXaxW8xt59Z/vTUqTdPv3S+7qcNErqOzuMGW0s9EJNYsNc4r0eO/HPIdv9sGtAblcg/f7vS/2MEz47BcILTfgzaNTxP1CkoCVZzT4Z9o34u9W3bNQ3eb/5hRjSXk53u79CWIjrBf+c/llePmrH4CEzYiNsDi8B9Wcve+AdZjnteJgXCu23aawv40COAaBMK0KX2YD7+zZh7d3WQv15UH50CZYn6spIULLEOFBDBZ1cRI2OLkWkf+p7wiD6rqGmt3l55zccz9VcBkGSyX0OSNQZYwVt5tlYaiq0eXvTKQ6ElqlFjP3znRoi/0kSs4veu4XGiZEaPHjSw/jzLWutdZEdIwGdv2lA85cGyLZr8icgyVH5tRrlElRSShiboQOS2WZ5P2pZh/ynL3vOUV6DFzjvN7F/jaKvfYxLaFWaICEzZLtaoUG7WNa1tp+ujkMFg2kUlyHKjLGtqFFiPcaQ0QO6nObwtUIA1ejBKprGWr+efJ9d4sFk9UFiL9mXUdhjaJEe3GhcQ4FiEDjT6JkDSntG7Tf8WvHJTNJOhMZEgStSuFYrxAqlzx2FfLs33dnw3Krz1NXD0NcaBz++XBgT1TlLQwW7qpjqCoReZ+rC5daoUGocCuqKq3fWHVIwAf9NkOttu1XPUogZpDtW20rXQsIVXXPuujywuqkENBfJ1Fydvummqt6hQJTJl7YY3vsbBips/e9Wn2H5drz1/fY3zFYuMvFUFV8OAGcA4PINzibMKq6EHLMf88COCvuq1Up8P5T3RB948JvqSxr8KyLzbkQsL63b5zdujl+TQ3AFkSq/1tzGGn1bJc1e3g47NM/MVg0hJO6C66W2owUZUuDI2DtqbIv7A2UdvgYV/Mp1JRxxdr9XXPCKADQqiKxYZwtRFwrN2L8P9Ix+uPDkuOd9TDUd9bF5loIeDO3b5yFEvtA4k5vD/k2BgsPYUFnM1GUDazqCZjshqWpgq09VU11UfeVdviYnCI9Bq74GgahtNb9qqv/3/hjGrrc0knc7qznoLn2MDSGht5acBZK7ANJc+7tCTQMFh5Ur9VS+S3Ue+rz3ldcs17MH10LxKRatxWcttbUVFxrus/JV9rhY85cuwhF66UIsZuQyRm1QoMBqcmIC6393nxz7WHwNfUJJfwsmgcGC0+qa1GzQPwW6itByt33PiYViO8iPjSVK2A+nQUU2sa/N7h+xp33xK4d9X69+vKzUOtqrgNnWP1P5B0MFp5Q35EijfEt1Fcu3M7cZJAy5ebCXCi9n9vgi7mL9970j/EwZ/wGtCixbruaBWW5AjWnzzFdKcC5HS0g/PNVyUvKNGq0ev1lKHThtvbpwqC6JUayn6nEArNFY31QegX4/GnALJ1UCUoNlP+3DaoO3aTHXimAufCY2DZcVwE1A86N11MqKqAKkY71rxcfD7X29RTVMyfWnOuAiHwLg4Un1DFSxIGTb6GSC8gNdV5Efb0H5CaClCk3F+eGPQhBLx0ueNPFsDXee5eBQdECKU8WQHXjFGaDHIJFjvi7ChEUbgYAWAxyXNoXiewZb9odW4WUB66KF3lTuQLndsRCsMhq7OV8aLLs52eQsmOH+LuZyhU4N3YqhMqaUy23AH541e7IMMjUUUhZ/45DqKmVj99acTZ1s1yTg5Akx5kTich3MFh4irPpwG8Qh6A6+8Z5NQuWPDUuOVxAnFxE7XsnCk77xX14kyUK5urf97oKuK6C8ortwu2MubAQgl6P+KVLEJRsHefuTjGs6WQ6zJcv2jYUXXB4742nLjkEBmOJErkHI2E2yG29FmG3AACCxqyBNtU2KiDlSgHMxbYiQuPFHOQuWgXzsI+gurGf+fBuCP/8APGvTkRQt37A1Rufz6NrgRY3PrOrp2Fc/xfreWv8bmaDHEKlwfYeVB/b/zUgoo34exm/ftN6bFBc3TU+fsTZ5EjWtSmA2DC1l1tHRK4wWDQipboKMrXKbgiqs2+c0ZBpgMS1a8Wl2R0uorX1TrTu5TMhwp6rb92yX6ZKvp27EpScDG2aexdL08l0nHtslF0vgfW89u+9TKNG8Euf2L7pn84CDr4qhgnp4alAjQu3Kh6SWyY4dgzAKul+EVnW36N1gvX3yDUBUSYgNcm2T64JuBFsnBHfg6JwYL8KyJgr3SGq+U7MJlMWQaHJEdekkBnyvdwiIqoLg0VjcbEMO5Qa4PG/2y5cN76FKh9eBFVKKIAb1e5hZtvzuSbnvRM3zmOqUMCcI70P79Aj0ES1GJK6iKtZMF4NcvjWXf3tvKGrwzpMQqaohCpcIT42n9wLwSJD/AuPISitRs2CRucQGBzOWXjzXew122e8mOPesen/sn7mRRdgLLH739PZLTcAuHgd2DGxoc11LDpughqd+sxF8dulzHqtdklEvoXBorE4W4YdcPxHu/pb6C/TgF9q7HddBaCFtes76sY/rE56J5zXIlh7BFq9t9LaA+KqwO9majGcBBVTiQXnnnjWri2R1l6Bbt2sF/BcE5QtjM4nE6tRDClekKuDFQClscD5cQoBKQ/k2363G+9dULf+0N7V3/3fDXbhwC7IuKKMjIRMq3XSviooq64CuRmOF3EACI6GMjQIMkUVchetqvGE9b1TRta4iDq75WZXm1OtzuJXV0XHLv5e3Ewxbc11O/JLDfjLJ+morNRCqLFYl0xZBJmyXHxsnYvChOld30C3eNu6FRztQeTbGCwaUy11F5J9nH0LPZ0F/PAqjN3nAa0TrNs0OiCnBMixXUiMmZnSWoSrp2H57DlcOhSP7GdrXjDsCvxu1GKYsk7BHFQiObXDxcI+RFQUAJufgqnIIOmNMZYHQ9CHONQEKMdukryew2RipVdg+cdoXNpdJSmGlCmqoNz+DLDbGhhUAFIeCIH5j5+IPQ/G9H8hd9EqVLSdIPZOGC/mAD+scn47ow4uw4FWK73AO6GKj0fK9m+lF9/SK1B+82eoagZHVbD1ol4tIhGq6YeQ8j+nJDUb0OigbN2h3oWqNQOQ5XohLr34Yu3Frzf+7pmyapy36ALw8wIos05BdUddAdYaBlPWvW27leSkt+Nobhae/elxGKpsI2GUiUCkTI0pXRYhXBWBElMRlv+/v0r2AaxzUQxMuotBgsiPMFj4AmdThCPWeoGTfIN1TqbVSnoEEGdAyjsTYJa3sO7gosDPef2D3TDKyiLgh9kOwyMtFi0u7YuFYJB2Z8vUKgS30kIVaQIsN+oJnIxUkEwmlmsCWpQg5b3ZtjbDydDNgtNQbX0WqtZRYn2C0phn/ab/3hYAWyTvSV1BwBmn4QD1/2auio+32y8N6HBIGsyc3WqISITqjkQ05EZMbWHIWd1OzVtQ1gAy3bHHa89kySgT88Xr0gBbegXGtWOQux8wrxsJVc1etRq9HTlFejz58U9QJkqXF9eo9VAnfoI3j04Rz6pVavF+//fF5csB9k4Q+SMGCx/l6gLnjLPubdUv06QXKScFfg6jDlz0HLgcHqlV2C5cNW+3bHusxi9i9+28tt+5Q0+o6poMCpDcTlApriPlgaswD/vINsoCN7f4m2M4uEn16bm6CfUNQ/UKIGJgCJEGhhtzsgSFmaGNNAGWAiCkAkCIte4nNcnpiKTCciMM5iooASx/ZBCSwq23NCJDgiBXPl7rFM9E5J8YLHxYgy5wdRT41Rz6Wl0cKI46cNFz4KzoEbC/cKUBHQ7Uv0C0Zq2Bs7oDZ1zUBKgigqHq0t1nR8Y0hfr8XalfAEkDsB7YP9EWGCoKgNVjrE/XrPlRWsOmsVRpLXq9MZT4+oXTKFZbF43KKimDPMg6kiMlNhQdo2tOr61lkCBqhhgsmiOnt1ZynXxbjYRMrYLSmGcbeQI3eg7qcV4HtRUM1tWz4So0+cpMo36gXmG1un6lOjAgDsaeC4Adb9nCBgBliQWyfz3rMJza8K+5mPSsAtd01uG+2gRALddwJAdRgGCwCBAO31Y9cOuiQW42HDTybQWqo4A19U7gRjDJD9bD/NFnQHERAEBddBam7dOh3heOeSGPI/bWrrh0vQIf/vAbpt6ThIjfDkKs5HCzMJWI/IdMEAShKU9YUlICnU6HywcP4pY772zKU5M9X15nhLyqrqGl1dNtVwrXxCGiKcjBFGEtbtmqg6buxUetI0rqMUkaEfmGzb/+jJE9B6C4uBjh4eEu92OPRSDjt39ywf6WiTihVU4xAOBsfhkqhWuITF0Ok2AdVXQZwDREI+F/LVh7vgAxlioAQKWgQmX/1xER6WSU0ulDUOHGNOkMtUTNAoMFEdXK2WJgAKAN1cMkGLD4nsVI1lnXc7l2+SKUFUYY+kahes7R0MhbENf6VtuBRdnAL29b/2w/AdyIfwDBdSykxgBC5NMYLIioVs4WAwOAAlMmXtgDJOuSbUuY12cp84hE67T2OybaJoCrLILyl1lQffJYnYf7zOq9ROQUgwUR1UtEeBnkmmLxcZnBvXVQalK27uAwAZxME2ubmK16PyeTpPna6r1EJMVgQUR1kimLMPXff3aYcruhC4LZj1KqnoJcOjGb3RTkN5jKFTCfzqpzwbibmSSNiBqOwYKIHNRcNCyrpAyK4CwYqiol9RTAzc2WaV8gaj95l9MpyM/+P1za0QLCP1+t8/WdhRIianxuB4tffvkFS5cuRXp6OvLy8vDVV1/h4YcfboSmEVFjc7Z8eUHlZYfeiepJrrrFdmu02TLtg4br1WKBxLdegaJdd5evVR1KzIWFDBZETcztYFFeXo7bb78dY8eOxWOP1aPQioh8kqvRHnJNDkKSpIuGqZVy/H3cgCadgtvpFORXT0O5/RmokrRAZI3JMjhShMhnuB0shg4diqFDhzZGW4ioCVWP9pj3SDyiw20X6ZzyCqw65rhoWEKEtsnb6DAFeVE4sF/tfEp4JyNFai4lD7DugqgpNHqNhcFggMFgW5a7pKSksU9JRC441E6EnMbK0/OcFmV2T0xEXKjO2ct4j7Mp4Z2MFKltWnLWXRA1rkYPFosXL8brr7/e2KchojrkleVh+Lbh0JvFFTsQ3BoANHh/4PuI1NhGd/j0EuauZoytsUquCkDKZ2thtmjEbay7IGoajR4sZs6cicmTJ4uPS0pKkJjIe6FETa3QUAi9WS+O7DiXX4aXNmfgvafuQ5+E9t5uXsO5WDFXpQqGytntkfR/AVdtIcRhrozq12TNBlGDNHqwUKvVUKvVjX0aIqpDfqn1lqRgiEVVZQIslWWoqryKGE1LL7fsJtX39oiiEjKFIJmUCwBkiiq0ursQCnWVuE0ZGgTV9EMMF0QNwHksiAJATpEef/kkHcpE4KXNGaiqvAoA0KoUiAwJ8nLrPKA+t0eM55DyQD7MfeYAEW0AAJbiElyauxzZe+SSw2SKKqT8zymo7mCwIHKX28GirKwMZ8+eFR9nZWUhIyMDUVFRaN26tUcbR0SeUVhuhMFcBSWAFSO6eH20R6NzdXskIhiqvo9JQkhKj/ulE3Ol/wu5i1bBXFyK2uf2JCJn3A4Wv/32G/r16yc+rq6fGD16NNavX++xhhFRw9lPfHU2v0z8c0psKDpG+9hoD09zdnsEcFo74TCk9Ub9hfFiDnDsmLiZQ1WJ6sftYNG3b18IgtAYbSEiD3C9zLncxRHNlKvbI3VQ6sIgU1TdqMWouUiaGq3eWwlFlG30DMMGkSPWWBA1M64mvioyG7HkiBcb5idUSe2RMrwU5jJbj4/FIMelfVHIflZ6a8XpImm5udLZQsEAQoGFwYKoGZIpi1xOfNWQ1UgDSkQiVNMPQWU3yiQlfLyk8NN4Mcdai1FjXgxTbi7ODXsQgl4veUlOzEWBhMGCyM85q6eQKcs9vhppQLG/jRIcDVWEGqqMubZt11UAWgClVwCkAQDMhYUQ9HrEL12CoGTr+86JuSjQMFgQ+bHqeopK4RpkynJxu0ZbAABI1iWjY3RHbzWv+XBWDHp4N/DDBzCePg6E3QLAtjZJUHIytGlpXmgokfcxWBD5kZwiPc5cu4hSYzEAIPt6BYxBJxDZZiNMgkGyL297eJhdL4ayTYHLIk9lpOP7zgXRKFAwWBD5iZwiPQau+BqK1kshk9uKMoNbA3K5Bu/386P1PpoBZ0WewI1ZO4NtI3K4IBoFGgYLIj9RWG6EQShFiNyEiWlzkBBiLSIM06rQPqYlQ0RTc1Hkia3PAhcPiLdNuCAaBRoGCyIfZr/MuTwoHwBwb1Iaayd8gZMiT7cWROPtEWqGGCyIfNTR3Cw8+9PjkiGj2gRALdewdsJX1XdBtMhIyDRqx9sjGjVSduxguCC/xmBB5INyivR48uOfoEyshD5nBKqMsQAAtVKOv48bwNsevqw+C6JVFFgXRKtRn2EsUSL3YCTMF08yWJBfY7Ag8gHO5qKoXjRs+SODmv+iYc2Zq9sjIcFQjdsEBMdYNxzeDRz8AKgsbvo2EnkQgwWRl9W1tkdALBrWnNV3QbSILACA8Vi6dD+NTpwnoxprMciXMVgQeRnX9ggA9VgQTdmyNWQKAbnvbQGwpdZ9OVSVfBmDBZEP4NoepOrQDSlbNsJ8+aJtY9EF4OcFwKNrgRapADhUlXwfgwWRF9gPI1UEZ3FtD4KqQzeoOnSzbcjNADLmAlEmIPJGb1aYGQCHqpLvYrAgamJ5ZXkYvm049GbbCpjVw0i7xXZjkCAbJ4WfynIFZIpYzuRJPovBgqiR1eydAID03FPQm/Xi7JnZ1yvw9q7TeO+p+xgqSMpJ4aeq4DRSUPcS7kTewmBB1IjyyvLw0Nd/hMEirZ0QqlR462sjBPNVAIBW1Rq3Rrf2RhPJ17mxhLvx918lh/L2CHkDgwVRIzpVcBkGi+MkV4uG34VbB9kuFpyfgurNSS+G8uRhyH5ahtx5b0l25e0R8gYGC6JGVKq3FtxNvu9u3Nv2DgAMEeQBdr0YKgApD1x1enukIj0dQYW2W3HsxaDGxmBB1AQSo4LRKYGTXFEjcXJ7hEWe5C0MFkRE/s5ZkWdFAVJUT0vXIykPRu5+sMiTGhWDBZEHHc3NwqXiq+LjI3mnvNgaCihOZvdUTT8EVXXYKDgNfDgBQAhw9TSQa3J8jWr2040TuYHBgqge7BcJc+bM9WzM/u1pyOTSf7CFKhVa6Vo0ZvOInKsZNoKjAaUGAGBc/xcg3Oz6OKUGyv/bJp2si6ieGCyI6uBqkTB7ck0OQpJMGN3uVbSPShG3t9K1wB3xSY3dTKLaRSRC+X/bIPv5GeQerHuaeNnPz6DVeyuhiLLty8JPqg8GC6I6FJYbUSlcw/SHEpAYFSxuDwvSIUbTUnycVXIKrx4GHuhwOzpGd/RGU4lqperQDSk7dsBcY5SIg6unYfnsOVw62BLZz0qXepdptWj17rsMG1QrBguiOhRUXkZIyjKsOSu9xaFVarG873JEaqz/yMoM+d5oHpFbVPHxtQeBonBgvwIp4ZdgNsjFzRaLFpf2q52GDY4yoZoYLIjs2E/BfbLwGGRyEyamzcG9SWkAgMLKQry8+2WM/3G85FiuRkp+78YIE1XFNaiqtxWcBrY+i5R3ZsMst9ULGS8XI3feWxxlQhIMFkQ1OFsgDLAWYHaI7IyO0e3FbduGb5MEEICrkVIz4WQacaiCofplmi1sAEBJGIAw6bFF2ZJhr+LxHGUSMBgsiGooNBRKFggDgOzrFVj6XQ5iBrWU7BsXGscQQYHByTwZtuGrYbYl3EuvAJ8/DaWiAqqQGsXOqmDr8QwXAYHBgqiG/FIDAGDJt4WoqrQVampV0YgMCfJWs4i8z8k8GUp1FWQatd3snmGQqaPQ6o0pUOjCgaILwM8LgN1bxOnGAUDZsjWHszZTDBYU0BzqKQrOAgCm3p8qru0BcH0PImdUIRakvDPBVndRdAGW7xbh0qF4ZM94s8aeLYAfPpAcK1MISNmykeGiGWKwoIBhHyIKKwvx0r8mOV3SvGPLOK7tQVQbV3UXrYOR8vJHMFs0tm2lV4DKYvGh8Vg6ct/bAvPJvVCFK6Svydslfo/BggJCXlkeHvr6j05DhP7SOAiWEHGbWhaGW6NbN3UTifyLs7oL4MaCaInSsIE06T4aHYAtMH79pvU2yQ3K0CCoph9iuPBzDBYUEE4VXIbBUgl9zghUGWPF7WpZGD5+YiCia9RP8LYHUT05qbuoD2XrDtbaDLsZQGWKKrS682co2nW37csJuPwOgwU1S65qJybfdzdrJ4i8TBUf7zADqOXsb7j06iK72gxAplGj1esvWwtBb3BW+GnKzXWYUZShxDsYLKjZqW0uCtZOEPkGhxlAE8KRMnymZJl3i0GOS/siHcOGQkCrZYuhSEy17ne9EJdefBGCXvr/PGcF9Q4GC/J79r0TmcWZ9Z6Lgoh8RESidJn3G1KuFMBcXCo+tlw8gUtvbUD2pFcl+8k0aiSuXSuuY2LMzETutOmcFdQLGCzIr9XWO/HW10YI5qviNs5FQeTjnNRsqOIhLQQt6o6UkyslPRvAjcLP29qw8NMHMFiQX3Gnd2L5Y33RLjZU3Jf1FETNgLOejRtrmeDiAdsolatZN/57Gsi1LSBosusBAThZl6cxWJDfcLd3okdSFIMEUXPkYi0TbK2x8up1FYAWMK7/CxBuBmCr2RAscsnL2ddsACz8vBkMFuSz2DtBRPXiZE4N5ZUCyH6Z6jikVaNG4iLbKJPaajZSduxguGgABgvyGTWDRG2zYrJ3gogc2PViqOLhMKQVcNIT4aRmw1iiRO7BSJj/8z1UsA1PdzYzKIe5OmKwIJ/g7DaHq1kx1z/NCa2IqG4OQ1qdcVaz8Z+jwMElMG6aCewwi5uVoUFQjfs7EBwDwFqvcW7sVAiVBslLBnpvB4MF+YRTBZcltzl4i4OImoxdb4cSsZBpVjiZGVRAq2tPQqGuAmDt2RAqIxF/VyGCbtRxVPd2VPzyA4Ju62F7zQDqxWCwoEaVU6RHYbnRYXtB5WWUGq2LEhXrTVi0aw8ULaXLlfMWBxF5g9OZQa8X4tILzyN7T7RkX5lGjeCXPoHqFmsvhvLkYch+XYbceW857BcovRgMFtRocor0GLhsDyqFa5Apy8XtMkU5tK3+AZncNgRM0RIIkmvw3lP3IUZjncSKvRNE5C3ObqPUp2ZDFRyNlOHzArpmg8GCPCanSI8z1y6KPRHZ1ytgDDqByDYbYRKk9yDVcg1evv1NhKsiAABhWhXax7REXGhcUzebiKheGqVmo8ZqrqbcXJwb9qDfT03OYBHg7Id0uhKpjpRc9O1DRLHehEXf/wZ5yw2Snojg1oBcrsH7/d5HpCbS5esRETUb9a7ZqEKrDv+AovUfAADGizkQ9HrEL12CoORk6zY/nJqcwSKAuZpwyhmtUovlfZcjUhOJ/FIDJnz2i0OIUMRbb2dMZk8EEZHIac1G9mlcmjIT2Yv+IdlXphAQHK2HKvLGv61hN4pC0/9lnUW0mkYHhN1S57m9cRuFwSKAFRoKoTfrsfiexUjWJbvc78y1y5h/aAbG/zhe3KaIB1QyNaZ0WcYQQURUB4fbKGlpSGnbGubLF23bKoug/GUWVD/YZhBVlisgU7RA7qJVDTqvN26jMFg0Q/UZiQEAOeUXAACCIRZVlQlOX+tauRGv/OMiKoVJkgJMtVKOv48bgDvikzzceiKiwKDq0M1xjZK7BkpmEFUBSHnSbn2TogvAzwuA/q8BEW1cvr7xYg5yF62C+fQhqFDj32onRaOe1KBgsXr1aixduhR5eXlIS0vDX//6V9xzzz2ebltAcVbr4KwOwX6//FIDSvW22xHFehMW7jgBg7lKcpyzkRiAdRKqFzeekcxkaU+rUmD904M4KRURUWOr1wqv2cCxt4CMubW/lpP1UgAASg3w+N8lt1I8ecvE7WCxefNmTJo0CatXr0afPn3wwQcfYOjQoTh+/Dhat27tkUb5A08FAQCoUpRh4a+vONQ6WEdOLBRvNZSYirD8/82CoUo6zbU9ZaLzD9Z+JAYAhAXpEDOoZa2vxxBBRORDnKyN4oyr9VIAADsmSh56cp4NmSAIgjsH3HnnnejatSvWrFkjbvvDH/6Ahx9+GIsXL67z+JKSEuh0Olw+eBC33HmnuL2+F2p7zo5zdvEO06oQG6audZ/6cnWBb2gQAKqLHq3HuhphAVRPc/2UOM21WinHrAf+AJ3Wlmftf9dqHIlBRBRYHObFKL0CfP40YLZdl6rn2Yif8jSCbu1g29euQHTnsV/x6IixKC4uRnh4uMtzuhUsjEYjgoOD8cUXX+CRRx4Rt7/00kvIyMjAnj17HI4xGAwwGGxzGBQXF6N169b4ZN1qKNPaAwBKTMVY/d/5Ti/UEzrNRrhK57Q9ro5rCkKVCpU5IyFUWWeJlMkroEnY5DQI1NwvSCHHtEHtEa619imU6M1Y+sMpGAwaCOYI8TiNSo65jyRAqayQvF6oSodoTaz4OCI4CPHsTSAiovoqugTor4sPTRfPIuuFhRAssloPK6uyoH9mJoqKiqDTOb8uAwAEN+Tk5AgAhP3790u2L1y4UEhNTXV6zNy5cwUA/OEPf/jDH/7wpxn8ZGdn15oVGlS8KZNJU40gCA7bqs2cOROTJ08WH1dVVeH69euIjo52eQx5VklJCRITE5GdnV1r9xU1HX4mvoefiW/h5+F7BEFAaWkp4uuow3ArWMTExEChUODy5cuS7fn5+bjlFucTdajVaqjV0vv9ERER7pyWPCQ8PJz/g/oYfia+h5+Jb+Hn4VtqvQVyg9ydFwwKCkK3bt2wa9cuyfZdu3ahd+/e7rWOiIiImh23b4VMnjwZTz31FLp3745evXrhb3/7Gy5evIjx48fXfTARERE1a24HixEjRuDatWt44403kJeXh06dOmHHjh1o06ZNY7SPPECtVmPu3LkOt6TIe/iZ+B5+Jr6Fn4f/cnseCyIiIiJX3KqxICIiIqoNgwURERF5DIMFEREReQyDBREREXkMg0UzsXr1aiQlJUGj0aBbt27Yu3evy323bt2K+++/Hy1atEB4eDh69eqF77//vglbGxjc+Uxq2r9/P5RKJbp06dK4DQww7n4eBoMBs2bNQps2baBWq5GSkoKPP/64iVobGNz9TDZu3Ijbb78dwcHBiIuLw9ixY3HtWu0rfJIXuLNWCPmmTZs2CSqVSli7dq1w/Phx4aWXXhJCQkKECxcuON3/pZdeEt566y3h8OHDwunTp4WZM2cKKpVKOHLkSBO3vPly9zOpVlRUJCQnJwuDBg0Sbr/99qZpbABoyOfxxz/+UbjzzjuFXbt2CVlZWcKhQ4cc1kmihnP3M9m7d68gl8uFFStWCJmZmcLevXuFtLQ04eGHH27illNdGCyagZ49ewrjx4+XbOvQoYPwyiuv1Ps1OnbsKLz++uueblrAauhnMmLECOG1114T5s6dy2DhQe5+Ht99952g0+mEa9euNUXzApK7n8nSpUuF5ORkybZ3331XaNWqVaO1kRqGt0L8nNFoRHp6OgYNGiTZPmjQIPz73/+u12tUVVWhtLQUUVFRjdHEgNPQz2TdunU4d+4c5s6d29hNDCgN+Ty++eYbdO/eHUuWLEFCQgJSU1MxdepU6PX6pmhys9eQz6R37964dOkSduzYAUEQcOXKFXz55ZcYNmxYUzSZ3NCg1U3JdxQUFMBisTgsAnfLLbc4LBbnyrJly1BeXo7HH3+8MZoYcBrymZw5cwavvPIK9u7dC6WS/1t6UkM+j8zMTOzbtw8ajQZfffUVCgoKMGHCBFy/fp11Fh7QkM+kd+/e2LhxI0aMGIHKykqYzWb88Y9/xHvvvdcUTSY3sMeimXBnKfuaPvvsM8ybNw+bN29GbGxsYzUvINX3M7FYLHjyySfx+uuvIzU1tamaF3Dc+X+kqqoKMpkMGzduRM+ePfHAAw/gnXfewfr169lr4UHufCbHjx/Hiy++iDlz5iA9PR07d+5EVlYW16nyQfxq5OcaspR9tc2bN+OZZ57BF198gYEDBzZmMwOKu59JaWkpfvvtNxw9ehTPP/88AOuFTRAEKJVK/PDDD+jfv3+TtL05asj/I3FxcUhISJAsEf2HP/wBgiDg0qVLuPXWWxu1zc1dQz6TxYsXo0+fPpg2bRoAoHPnzggJCcE999yDBQsWIC4urtHbTfXDHgs/19Cl7D/77DOMGTMGn376Ke9Repi7n0l4eDh+//13ZGRkiD/jx49H+/btkZGRgTvvvLOpmt4sNeT/kT59+iA3NxdlZWXittOnT0Mul6NVq1aN2t5A0JDPpKKiAnK59JKlUCgAWHs6yId4r26UPKV62NZHH30kHD9+XJg0aZIQEhIinD9/XhAEQXjllVeEp556Stz/008/FZRKpbBq1SohLy9P/CkqKvLWr9DsuPuZ2OOoEM9y9/MoLS0VWrVqJfzpT38Sjh07JuzZs0e49dZbhf/93//11q/Q7Lj7maxbt05QKpXC6tWrhXPnzgn79u0TunfvLvTs2dNbvwK5wGDRTKxatUpo06aNEBQUJHTt2lXYs2eP+Nzo0aOF++67T3x83333CQAcfkaPHt30DW/G3PlM7DFYeJ67n8eJEyeEgQMHClqtVmjVqpUwefJkoaKioolb3by5+5m8++67QseOHQWtVivExcUJo0aNEi5dutTEraa6cNl0IiIi8hjWWBAREZHHMFgQERGRxzBYEBERkccwWBAREZHHMFgQERGRxzBYEBERkccwWBAREZHHMFgQERGRxzBYEBERkccwWBAFIIvFgt69e+Oxxx6TbC8uLkZiYiJee+01cduWLVvQv39/REZGIjg4GO3bt8e4ceNw9OhRcZ/169dDJpOJP6GhoejWrRu2bt3aZL8TEfkGBguiAKRQKLBhwwbs3LkTGzduFLe/8MILiIqKwpw5cwAAM2bMwIgRI9ClSxd88803OHbsGP72t78hJSUFr776quQ1w8PDkZeXh7y8PBw9ehSDBw/G448/jlOnTjXp70ZEXubtxUqIyHtWrFghREZGCjk5OcLXX38tqFQq4ejRo4IgCMKBAwcEAMKKFSucHltVVSX+ed26dYJOp5M8b7FYBJVKJXz++efitsrKSmHatGlCq1athKCgIKFdu3bChx9+KFgsFiEhIUFYs2aN5DXS09MFAMK5c+c88wsTUaNTejvYEJH3vPDCC/jqq6/w9NNP4/fff8ecOXPQpUsXAMBnn32G0NBQTJgwwemxMpnM5etaLBb8/e9/BwB07dpV3P7000/jwIEDePfdd3H77bcjKysLBQUFkMvlGDlyJDZu3Ijx48eL+3/66afo1asXkpOTPfDbElGT8HayISLvOnHihABAuO222wSTySRuHzJkiNC5c2fJvsuWLRNCQkLEn6KiIkEQrD0WAMTtcrlcUKvVwrp168RjT506JQAQdu3a5bQdR44cEWQymXD+/HlBEASxF2PVqlUe/o2JqDGxxoIowH388ccIDg5GVlYWLl26JHnOvldi3LhxyMjIwAcffIDy8nIIgiA+FxYWhoyMDGRkZODo0aNYtGgRnnvuOfzzn/8EAGRkZEChUOC+++5z2o477rgDHTp0wGeffQYA2LNnD/Lz8/H444978tclokbGYEEUwA4cOIDly5dj27Zt6NWrF5555hkxLNx66604d+4cTCaTuH9ERATatWuHhIQEh9eSy+Vo164d2rVrh86dO2Py5Mno168f3nrrLQCAVqutsz2jRo3Cp59+CsB6G2Tw4MGIiYnxxK9KRE2EwYIoQOn1eowePRrPPfccBg4ciA8//BC//vorPvjgAwDAE088gbKyMqxevbrB51AoFNDr9QCA2267DVVVVdizZ4/L/Z988kn8/vvvSE9Px5dffolRo0Y1+NxE5B0s3iQKUK+88gqqqqrEHoXWrVtj2bJlmDx5MoYMGYJevXphypQpmDJlCi5cuIBHH30UiYmJyMvLw0cffQSZTAa53PbdRBAEXL58GYA1tOzatQvff/+9OHS1bdu2GD16NMaNGycWb164cEFyuyMpKQm9e/fGM888A7PZjOHDhzfxu0JEN83LNR5E5AW7d+8WFAqFsHfvXofnBg0aJPTv318cTrp582ahb9++gk6nE1QqldCqVSvhySefFA4ePCgeU128Wf2jVquF1NRUYeHChYLZbBb30+v1wssvvyzExcWJw00//vhjyflXrVolABCefvrpRvrtiagxyQShRvUVERER0U1gjQURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFEREReQyDBREREXkMgwURERF5DIMFERERecz/B+cnb8XvSvRlAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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JU4qLjTQRpYRWLSR557lA3y2WDp48CC33nortWrVIjg4mC5duhAXF+fptERERMRENew2ss6MLLnpMpxPTvBOTU2lX79+DB48mK+//pqoqCj27NlDRESEp1MTERERE9UItHGAopGl/Ox0t/Thk8XS008/TUxMDPPmzXO2NWnSpNR9cnNziw2Xpqe75zdcREREzBMSYOPU6ctwuVnu+e72yctwixcvpnv37txwww1ERUXRtWtX5s6dW+o+s2bNIjw83PmKiYmppGxFRMRbtGjRgqVLl9KiRQtT4qTi/KwWcqxFl+HyTqW5pQ+fLJYSEhJ49dVXadmyJd988w0TJ07knnvu4d133y1xn+nTp5OWluZ8JSUlVWLGIiLiDcLCwhg2bBhhYWGmxIk58vyCi37N1tIBLnM4HHTr1o0nn3ySrl27ctddd3HnnXfy6quvlriP3W4nLCys2EtERORchw8fZsaMGRw+fNiUODFHvl8IAIUqllxXr1492rVrV6ytbdu27N+/30MZiYiILzh8+DAzZ850qVhyJU7McWZkSesslUG/fv34/fffi7Xt3LmTxo0beygjERERcZcCW1GxZORqnSWXTZkyhR9//JEnn3yS3bt3s2DBAl5//XUmT57s6dRERETEZH5+RTf31zj5m1uO75PFUo8ePfj888/54IMP6NChA4899hizZ89mzJgxnk5NRERETNY/eyUAEWnb3XJ8n1xnCWDkyJGMHDnS02mIiIgPiYyMZMyYMURGRpoSJ+aIC7+MRsd2u+34PlssiYiImK1p06a8//77psWJOU4GNQLgaGj7Mw8+MZVPXoYTERFxh5ycHHbv3k1OTo4pcWIOv4CiCd7WQvf8fqtYEhERcdH27dtp2bIl27eXPjfG1TgxhyWgaDzJT8WSiIiIyPnOjCzZCnMvElk+KpZERETEq9nsRQ/StTlULImIiIicx2Y/PbJkqFgSEREROc+Zy3ABRh4YhunH19IBIiIiLurWrRuGC1/GrsaJOWz2kLMbBeZP8tbIkoiIiHg1/8Dgsxv52aYfX8WSiIiIi37//Xf69Olz3sPayxsn5ggICCDf8CvaKDB/3pKKJRERERdlZWXx448/kpVV+tPtXY0TcwT6+5FDQNFGgUaWRERERIqx26zk4F+0ka85SyIiIiLFBPr7kescWVKxJCIiIlKM3WYlxzhdLGlk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cp2l2rVr4+fnd94oUnJy8nmjTWfY7XbsdntlpCciIiJexCdHlgICAoiNjWX58uXF2pcvX07fvn09lJWIiHi7+Ph4LBYL8fHxpsSJd/DJkSWAqVOnMnbsWLp3706fPn14/fXX2b9/PxMnTvR0aiIiIuJFfLZYuummmzh+/DiPPvoohw8fpkOHDnz11Vc0btzY06mJiIiIF/HZYglg0qRJTJo0ydNpiIiIiBfzyTlLIiIiImbx6ZElERERM7Vr145du3bRsGFDU+LEO6hYEhERcVFgYCAtWrQwLU68gy7DiYiIuCgxMZFbb72VxMREU+LEO6hYEhERcVFqairz588nNTXVlDjxDiqWREREREqhYklERESkFJrgXQLDMABIT0/3cCYiIlJVZGZmOn8t7fvB1Tgx35nf7zPf42awGGYezYckJCTQvHlzT6chIiIi5bBnzx6aNWtmyrE0slSCmjVrArB//37Cw8M9nE31lp6eTkxMDElJSYSFhXk6nWpN56Lq0LmoOnQuqpa0tDQaNWrk/B43g4qlElitRdO5wsPD9Ye/iggLC9O5qCJ0LqoOnYuqQ+eiajnzPW7KsUw7koiIiIgPUrEkIiIiUgoVSyWw2+088sgj2O12T6dS7elcVB06F1WHzkXVoXNRtbjjfOhuOBEREZFSaGRJREREpBQqlkRERERKoWJJREREpBQqlkRERERKUW2LpVdeeYWmTZsSGBhIbGwsa9euLTV+9erVxMbGEhgYSLNmzZgzZ04lZVo9lOV8LFy4kMsuu4w6deoQFhZGnz59+OabbyoxW99W1r8bZ3z//ffYbDa6dOni3gSrkbKei9zcXB588EEaN26M3W6nefPmvPXWW5WUrW8r67mYP38+nTt3Jjg4mHr16jF+/HiOHz9eSdn6rjVr1jBq1Cjq16+PxWJh0aJFF93HlO9voxr68MMPDX9/f2Pu3LnG9u3bjXvvvdcICQkx9u3bd8H4hIQEIzg42Lj33nuN7du3G3PnzjX8/f2NTz/9tJIz901lPR/33nuv8fTTTxs///yzsXPnTmP69OmGv7+/ER8fX8mZ+56ynoszTp48aTRr1swYOnSo0blz58pJ1seV51xceeWVRq9evYzly5cbiYmJxk8//WR8//33lZi1byrruVi7dq1htVqN559/3khISDDWrl1rtG/f3rj66qsrOXPf89VX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EIC8vDwee+wxfvnlFxYtWkRiYiK33357uX82gLlz5/Lggw/yxBNPsGPHDp588kkefvhh3nnnnQodV0TKxubpBETEe3355ZfUqFEDgKysLOrVq8eXX36J1Xrx/4e1adMGgL1795a7/2eeeYbu3bsXG21p3769y/vXqVMHODuKVZKbbrqJKVOmsG7dOgYMGADAggULuOWWW5w/65///GdnfLNmzXjhhRfo2bMnmZmZzt+jsnrsscd49tlnufbaawFo2rQp27dv57XXXmPcuHHlOqaIlJ1GlkSk3AYPHszmzZvZvHkzP/30E0OHDmXEiBHs27fvovueGaWpyOTtMyNL7lanTh0uu+wy5s+fD0BiYiLr169nzJgxzphNmzZx1VVX0bhxY0JDQxk0aBAA+/fvL1efx44dIykpiQkTJlCjRg3n6/HHH2fPnj0V/plExHUaWRKRcgsJCaFFixbO7djYWMLDw5k7dy6PP/54qfvu2LEDKBotKa+goKBSP7darcUunQHk5+eXq68xY8Zw77338uKLL7JgwQLat29P586dgaJRtaFDhzJ06FDef/996tSpw/79+xk2bBh5eXnlys3hcABFl+J69epVLM7Pz69cP4OIlI9GlkTENBaLBavVSnZ29kVjZ8+eTVhYGEOGDCl3f506deK7774r8fM6deoUu5MuPT2dxMTEYjH+/v4uzXG6+uqrycnJYenSpSxYsIBbb73V+dlvv/1GSkoKTz31FAMGDKBNmzalTu4+k9uRI0eKFUybN292vo+OjqZBgwYkJCTQokWLYq+KFJgiUnYaWRKRcsvNzeXIkSMApKam8tJLL5GZmcmoUaOKxZ08eZIjR46Qm5vLzp07ee2111i0aBHvvvsuERER5e5/+vTpdOzYkUmTJjFx4kQCAgJYuXIlN9xwA7Vr1+bSSy/l7bffZtSoUURGRvLwww+fNyrTpEkTvvvuO/r164fdbicyMvKCfYWEhHDVVVfx8MMPs2PHDm655RbnZ40aNSIgIIAXX3yRiRMn8uuvv/LYY4+VmvugQYM4duwYzzzzDNdffz1Lly7l66+/JiwszBkzY8YM7rnnHsLCwhgxYgS5ubls3LiR1NRUpk6dWu7fNxEpI0NEpBzGjRtnAM5XaGio0aNHD+PTTz8tFnduTGBgoNG8eXNj3LhxRlxcXInHBozPP//cpTxWrVpl9O3b17Db7UZERIQxbNgwIzU11TAMw0hLSzNuvPFGIywszIiJiTHefvtto3PnzsYjjzzi3H/x4sVGixYtDJvNZjRu3LjUvpYsWWIAxiWXXHLeZwsWLDCaNGli2O12o0+fPsbixYsNwNi0aZNhGIaxcuVKA3DmZhiG8eqrrxoxMTFGSEiIcdtttxlPPPHEeTnMnz/f6NKlixEQEGBERkYal1xyibFw4UKXfm9ExBwWw/jDRXMRERERcdKcJREREZFSqFgSERERKYWKJREREZFSqFgSERERKYWKJREREZFSqFgSERERKYWKJREREZFSqFgSERERKYWKJREREZFSqFgSERERKYWKJREREZFS/D8mxZds2GyCOQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Drawing plot of model respone for signal and background classes\n", + "plt.figure()\n", + "plot_comparision('XGB', mc_df, bkg_df)\n", + "plot_comparision('XGBcv', mc_df, bkg_df)\n", + "\n", + "# Drawing the signal efficiency vs background rejection curve (ROC)\n", + "plt.figure()\n", + "plot_roc(bdt, training_data, training_columns)\n", + "plot_roc(bdt_cv, training_data, training_columns)\n", + "\n", + "# Drawing signal significance comparison as a function of minimum cut on model response\n", + "plt.figure()\n", + "bdt_cut = plot_significance(bdt, training_data, training_columns, \"bdt\")\n", + "bdt_cv_cut = plot_significance(bdt_cv, training_data, training_columns, \"bdt_cv\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### $k$-folding & early stopping\n", + "\n", + "Performing CV on each of a number, k, of ways to split your data gives you k models to choose from. Some choose to average the performance across the models from each fold as any instability might imply the model will not be reliable. The results below seem stable; each fold provides a consistant performance across multiple metrics, so we'll just choose the best one." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "XGBoost k-folding --- 100.34803915023804 seconds ---\n", + "accuracy: [0.78412488 0.77760064 0.78477481 0.78856118] -> best fold = 3\n", + "-logloss: [-0.42945196 -0.43137575 -0.42765048 -0.42374993] -> best fold = 3\n", + "roc_auc: [0.87520356 0.87239133 0.8768386 0.87975731] -> best fold = 3\n" + ] + } + ], + "source": [ + "# Defining the folds with a seed to test consistently\n", + "splits = 4 # to match 0.25 value of test_train_split default though this may not be optimal\n", + "kf = KFold(n_splits=splits, shuffle=True, random_state=123)\n", + "\n", + "# Printing processing time of the kfold cross-validation\n", + "stime = time.time()\n", + "for train, test in kf.split(X1):\n", + " X_train, X_test = X1.iloc[train], X1.iloc[test]\n", + " y_train, y_test = y1.iloc[train], y1.iloc[test]\n", + " bdt.fit(X_train,y_train)\n", + "print(\"\\nXGBoost k-folding --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + "# Calculating scores of each fold using variety of CV-metrics\n", + "cv_acc = cross_val_score(bdt, X_test, y_test, cv=splits, scoring=\"accuracy\", n_jobs=-1)\n", + "cv_los = cross_val_score(bdt, X_test, y_test, cv=splits, scoring=\"neg_log_loss\", n_jobs=-1)\n", + "cv_auc = cross_val_score(bdt, X_test, y_test, cv=splits, scoring=\"roc_auc\", n_jobs=-1)\n", + "\n", + "# Printing results and indicating best fold\n", + "print(\"accuracy: \",cv_acc, \" -> best fold =\", np.argmax(cv_acc) )\n", + "print(\"-logloss: \",cv_los, \" -> best fold =\", np.argmax(cv_los) )\n", + "print(\"roc_auc: \",cv_auc, \" -> best fold =\", np.argmax(cv_auc) )\n", + "bestfold = np.argmax(cv_acc)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Early stopping defines a maximum number of rounds the cross-validation metric (we'll use 'error'=1-accuracy) is allowed to not improve before training is terminated. As is standard, we will be reverting back to a 'previous best' model based on test sample score, this helps avoid overtraining. Early stopping prevents us training too many of extra models thus saving time. Set the limit too small though and your training might be cut off prematurely." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "jupyter": { + "is_executing": true + } + }, + "outputs": [], + "source": [ + "def modelfit(alg, metric, params, label, predictors, kfold, fbest, early_stop=10):\n", + "\n", + " # Loading data split inputs providing best fold result\n", + " for k, (train, test) in enumerate(kf.split(params)):\n", + " if (k==fbest):\n", + " X_train, X_test = params.iloc[train], params.iloc[test]\n", + " y_train, y_test = label.iloc[train], label.iloc[test]\n", + "\n", + " # Defining data in terms of training variables and class label\n", + " xgb_param = alg.get_xgb_params()\n", + " data = xgb.DMatrix(params, label=label, feature_names=predictors)\n", + "\n", + " # Runs timed CV on our model using early stopping based on our metric\n", + " stime = time.time()\n", + " cvresult = xgb.cv(xgb_param,\n", + " data,\n", + " num_boost_round=alg.get_params()['n_estimators'],\n", + " #nfold=cv_folds, # to use in build folding\n", + " folds=kfold, # use -> ignores nfold\n", + " metrics=metric)\n", + " alg.set_params(n_estimators=cvresult.shape[0])\n", + " print(\"\\nXGBoost early-stop folding --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + " # Fitting the algorithm on the data with CV evaluation early stopping\n", + " stime = time.time()\n", + " alg.fit(X_train, y_train,\n", + " eval_set=[(X_train, y_train), (X_test, y_test)],\n", + " verbose=False)\n", + " training_monitor(alg)\n", + " print(\"XGBoost early-stop limit --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + " # Predicting training set:\n", + " train_predictions = alg.predict(X_train)\n", + " test_predictions = alg.predict(X_test)\n", + "\n", + " # Printing model report:\n", + " print(\"\\nModel Report : best iteration \"+str(cvresult.shape[0]))\n", + " print(\"Train Accuracy : \"+str(metrics.accuracy_score(y_train, train_predictions)))\n", + " print(\"Test Accuracy : \"+str(metrics.accuracy_score(y_test, test_predictions)))\n", + " return cvresult.shape[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This function incorporates the k-folding CV and early stopping, saving not only the optimal model but also the index of its training iteration. This means, in our subsequent steps, we can apply an upper limit on training for models based on the convergence of the default hyperparameters, saving us some time. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "XGBoost early-stop folding --- 41.88707709312439 seconds ---\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "ename": "KeyError", + "evalue": "'error'", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mKeyError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[15], line 13\u001B[0m\n\u001B[1;32m 11\u001B[0m \u001B[38;5;66;03m# Timing the CV using early stopping\u001B[39;00m\n\u001B[1;32m 12\u001B[0m stime \u001B[38;5;241m=\u001B[39m time\u001B[38;5;241m.\u001B[39mtime()\n\u001B[0;32m---> 13\u001B[0m estimators \u001B[38;5;241m=\u001B[39m \u001B[43mmodelfit\u001B[49m\u001B[43m(\u001B[49m\u001B[43mbdt_es\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43merror\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mX1\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43my1\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mtraining_columns\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mkf\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mbestfold\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 14\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[38;5;124mmodelfit(bdt_es) --- \u001B[39m\u001B[38;5;132;01m%s\u001B[39;00m\u001B[38;5;124m seconds ---\u001B[39m\u001B[38;5;124m\"\u001B[39m \u001B[38;5;241m%\u001B[39m (time\u001B[38;5;241m.\u001B[39mtime() \u001B[38;5;241m-\u001B[39m stime))\n\u001B[1;32m 16\u001B[0m \u001B[38;5;66;03m# Saving model predictions\u001B[39;00m\n", + "Cell \u001B[0;32mIn[14], line 29\u001B[0m, in \u001B[0;36mmodelfit\u001B[0;34m(alg, metric, params, label, predictors, kfold, fbest, early_stop)\u001B[0m\n\u001B[1;32m 25\u001B[0m stime \u001B[38;5;241m=\u001B[39m time\u001B[38;5;241m.\u001B[39mtime()\n\u001B[1;32m 26\u001B[0m alg\u001B[38;5;241m.\u001B[39mfit(X_train, y_train,\n\u001B[1;32m 27\u001B[0m eval_set\u001B[38;5;241m=\u001B[39m[(X_train, y_train), (X_test, y_test)],\n\u001B[1;32m 28\u001B[0m verbose\u001B[38;5;241m=\u001B[39m\u001B[38;5;28;01mFalse\u001B[39;00m)\n\u001B[0;32m---> 29\u001B[0m \u001B[43mtraining_monitor\u001B[49m\u001B[43m(\u001B[49m\u001B[43malg\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 30\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mXGBoost early-stop limit --- \u001B[39m\u001B[38;5;132;01m%s\u001B[39;00m\u001B[38;5;124m seconds ---\u001B[39m\u001B[38;5;124m\"\u001B[39m \u001B[38;5;241m%\u001B[39m (time\u001B[38;5;241m.\u001B[39mtime() \u001B[38;5;241m-\u001B[39m stime))\n\u001B[1;32m 32\u001B[0m \u001B[38;5;66;03m# Predicting training set:\u001B[39;00m\n", + "Cell \u001B[0;32mIn[10], line 19\u001B[0m, in \u001B[0;36mtraining_monitor\u001B[0;34m(alg)\u001B[0m\n\u001B[1;32m 17\u001B[0m \u001B[38;5;66;03m# Plotting classification error as a function of training iteration\u001B[39;00m\n\u001B[1;32m 18\u001B[0m fig, ax \u001B[38;5;241m=\u001B[39m plt\u001B[38;5;241m.\u001B[39msubplots()\n\u001B[0;32m---> 19\u001B[0m ax\u001B[38;5;241m.\u001B[39mplot(x_axis, \u001B[43mresults\u001B[49m\u001B[43m[\u001B[49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[38;5;124;43mvalidation_0\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[43m]\u001B[49m\u001B[43m[\u001B[49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[38;5;124;43merror\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[43m]\u001B[49m, label\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mTrain\u001B[39m\u001B[38;5;124m'\u001B[39m) \u001B[38;5;66;03m# for each eval_set\u001B[39;00m\n\u001B[1;32m 20\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m results[\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mvalidation_1\u001B[39m\u001B[38;5;124m'\u001B[39m]: ax\u001B[38;5;241m.\u001B[39mplot(x_axis, results[\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mvalidation_1\u001B[39m\u001B[38;5;124m'\u001B[39m][\u001B[38;5;124m'\u001B[39m\u001B[38;5;124merror\u001B[39m\u001B[38;5;124m'\u001B[39m], label\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mTest\u001B[39m\u001B[38;5;124m'\u001B[39m)\n\u001B[1;32m 21\u001B[0m ax\u001B[38;5;241m.\u001B[39mlegend()\n", + "\u001B[0;31mKeyError\u001B[0m: 'error'" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Defining model with high maximum estimators for use with early stopping\n", + "bdt_es = XGBClassifier(learning_rate = LR, n_estimators=1000,\n", + " # Default values of other hyperparamters\n", + " #max_depth=6, min_child_weight=1,\n", + " #gamma=0, subsample=0.8,\n", + " #colsample_bytree=0.8, scale_pos_weight=1,\n", + " #objective='binary:logistic', # default for binary classification\n", + " #objective='mutli:softprob', num_class=3, # for multiclassifiers\n", + " seed=123, nthread=-1)\n", + "\n", + "# Timing the CV using early stopping\n", + "stime = time.time()\n", + "estimators = modelfit(bdt_es, \"error\", X1, y1, training_columns, kf, bestfold)\n", + "print(\"\\nmodelfit(bdt_es) --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + "# Saving model predictions\n", + "for df in [mc_df, bkg_df, data_df, training_data]:\n", + " df['XGBes'] = bdt_es.predict_proba(df[training_columns])[:,1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This provides us with an improved model as well as a benchmark to test against in both performance and training efficiency. When training using new combinations of hyperparameters, the maximum number of estimators from our model report will cut off any new models improving more slowly than our default, while, for more efficient models, the early stopping will kick in." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Drawing plot to compare model response for signal and background classes\n", + "plt.figure()\n", + "plot_comparision('XGBcv', mc_df, bkg_df)\n", + "plot_comparision('XGBes', mc_df, bkg_df)\n", + "\n", + "# Drawing comaprison of the signal efficiency vs background rejection curve (ROC)\n", + "plt.figure()\n", + "plot_roc(bdt_cv, training_data, training_columns)\n", + "plot_roc(bdt_es, training_data, training_columns)\n", + "\n", + "# Drawing signal significance comparison as a function of minimum cut on model response\n", + "plt.figure()\n", + "bdt_cut_cv = plot_significance(bdt_cv, training_data, training_columns, \"bdt_cv\")\n", + "bdt_cut_es = plot_significance(bdt_es, training_data, training_columns, \"bdt_es\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Hyperameter optimisation\n", + "\n", + "Below we provide a \"grid\" of hyperparameters, defining the structure of the trees and constraints on the learning, but there are many more values to choose from and a larger parameter space to be explored. These optimsations are very problem specific and their impact will have to be weighed against the computing resources and timeframe you have at your disposal. For the sake of expedient demonstration we are comparing the default parameters to only one predetermined variation in 2 parameters. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define a function that performs a gridscan of HPs\n", + "\n", + "\n", + "def hpgridscan(alg, metric, params, label, kfold, fbest, early_stop=10):\n", + "\n", + " # Load data fold with best performance\n", + " for k, (train, test) in enumerate(kf.split(params)):\n", + " if (k==fbest):\n", + " X_train, X_test = params.iloc[train], params.iloc[test]\n", + " y_train, y_test = label.iloc[train], label.iloc[test]\n", + "\n", + " # Define a dictionary of numpy arrays for our HPs\n", + " params = {\n", + " 'max_depth':np.array([7]),\n", + " 'min_child_weight':np.array([3]),\n", + " #'max_depth':np.arange( 5, 9, 1 ),\n", + " #'min_child_weight':np.arange( 1, 5, 1 ),\n", + " ##'gamma':np.arange( 0.0, 1.0, 0.1 ),\n", + " ##'colsample_bytree':np.arange( 0.4, 1.0, 0.1 ),\n", + " ##'subsample':np.arange( 0.4, 1.0, 0.1 ),\n", + " ##'scale_pos_weight':np.arange( 0.4, 1.6, 0.1 )\n", + " }\n", + "\n", + " # Perform timed grid scan with established n_estimator cutoff and early stopping\n", + " stime = time.time()\n", + " gs = GridSearchCV(estimator=alg,\n", + " param_grid=params,\n", + " scoring=metric,\n", + " #iid=False,\n", + " cv=kf,\n", + " eval_metric=[\"logloss\",\"error\"],\n", + " n_jobs=-1)\n", + " gs.fit(X_train, y_train,\n", + " eval_set=[(X_train, y_train), (X_test, y_test)],\n", + " verbose=False)\n", + " print(\"XGBoost grid-scan --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + " # Return suggested parameters, performance and best model\n", + " training_monitor(gs.best_estimator_)\n", + " print(\"Suggestion:\", gs.best_params_)\n", + " print(\"Accuracy:\" ,gs.best_score_)\n", + " return gs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Running with estimators maximum for shortened training\n", + "bdt_st = XGBClassifier( learning_rate = LR, n_estimators=estimators,\n", + " seed=123, n_jobs=-1)\n", + "\n", + "# Running timed hyperparameter gridscan\n", + "stime = time.time()\n", + "gs = hpgridscan(bdt_st, \"accuracy\", X1, y1, kf, bestfold)\n", + "bdt_gs = gs.best_estimator_\n", + "print(\"\\nhpgridscan(bdt_st) --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + "# Get model predictions\n", + "for df in [mc_df, bkg_df, data_df, training_data]:\n", + " df['XGBgs'] = bdt_gs.predict_proba(df[training_columns])[:,1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Even this naive grid scan, using the same fold as before for fair comparison, can provide significant improvements as demonstrated above. These may be pushed further by including more hyperparameters for a trade off with processing time. However, even with parrallisation these tasks can take hours or longer and might only provide improvement of O(>1%)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "## We could define a model using optimal hyperparameters from our grid scan\n", + "#bdt_opt = XGBClassifier( learning_rate = LR, n_estimators=1000,\n", + "# max_depth=gs.best_params_['max_depth'],\n", + "# min_child_weight=gs.best_params_['min_child_weight'],\n", + "# seed=123, n_jobs=-1 )\n", + "\n", + "## Run with CV early stopping\n", + "#stime = time.time()\n", + "#estimators = modelfit(bdt_opt, 'error', X1, y1, training_columns, kf, bestfold)\n", + "#print(\"\\nmodelfit(bdt_opt) --- %s seconds ---\" % (time.time() - stime))\n", + "\n", + "## Get model predictions\n", + "#for df in [mc_df, bkg_df, data_df, training_data]:\n", + "# df['XGBopt'] = bdt_opt.predict_proba(df[training_columns])[:,1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Comapring model response from the end of last session to the end of this one\n", + "plt.figure()\n", + "plot_comparision('XGB', mc_df, bkg_df)\n", + "plot_comparision('XGBgs', mc_df, bkg_df)\n", + "\n", + "# Comparing model performance for each level of tuning\n", + "plt.figure()\n", + "plot_roc(bdt, training_data, training_columns)\n", + "plot_roc(bdt_cv, training_data, training_columns)\n", + "plot_roc(bdt_es, training_data, training_columns)\n", + "plot_roc(bdt_gs, training_data, training_columns)\n", + "#plot_roc(bdt_opt, training_data, training_columns)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Comparing the impact on projected performance at each stage of the tutorial\n", + "plt.figure()\n", + "bdt_cut = plot_significance(bdt, training_data, training_columns, \"bdt\")\n", + "bdt_cv_cut = plot_significance(bdt_cv, training_data, training_columns, \"bdt_cv\")\n", + "bdt_es_cut = plot_significance(bdt_es, training_data, training_columns, \"bdt_es\")\n", + "bdt_gs_cut = plot_significance(bdt_gs, training_data, training_columns, \"bdt_gs\")\n", + "#bdt_opt_cut = plot_significance(bdt_opt, training_data, training_columns, \"bdt_opt\")\n", + "\n", + "# Comparing best cuts impact on mass for original and tuned model\n", + "plt.figure()\n", + "data_bdt_cut = data_df.query('XGB > %f' %bdt_cut )\n", + "plot_mass(data_bdt_cut, label='XGB default', norm=True)\n", + "data_gs_cut = data_df.query('XGBgs > %f' %bdt_gs_cut )\n", + "plot_mass(data_gs_cut, label='XGB tuned', norm=True)\n", + "plt.legend(loc='best')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Comparing our data sample's mass plot having applied the cut optimised for $\\sigma=\\frac{S}{\\sqrt{S+B}}$ from each BDT output, we can see how the improved model reduces relative background. However, while we define our signal training sample from MC you'll remember we defined our background training sample from the data !(3.0 < JPsi_M < 3.2).\n", + "\n", + "We can see shoulders at the edges of the regions where we define our background training sample in our data's mass spectrum now. Our training and validation samples include a subset of our data sample so there's potential that our model is learning the difference between MC and data and exploiting that or demonstrating overtraining on the 'previously seen' data (remember we could see our train and test samples beginning to diverge in our validation metrics with more iterations).\n", + "\n", + "Below you can see replotting the normalised mass distribution from just the data not included in training demonstrates no significant improvement. This is not ideal and might be addressed by choosing the setup of our training more carefully. For example, we could train using background from same-sign muon MC across the full mass range (a common practice in LHC experiments) or, using other libraries such as UGBoost to introduce a punishment to the training for introducing a depedance of efficiency on mass." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sig_df = data_df.query('(3.0 < Jpsi_M < 3.2)')\n", + "sig_bdt_cut = sig_df.query('XGB > %f' %bdt_cut )\n", + "plot_mass(sig_bdt_cut, label='XGB default', norm=True)\n", + "sig_gs_cut = sig_df.query('XGBgs > %f' %bdt_gs_cut )\n", + "plot_mass(sig_gs_cut, label='XGB tuned', norm=True)\n", + "plt.legend(loc='best')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also choose a higher learning rate to perform course scans of your space and decrease it again to retrain your final model. If you can afford to, it might be best to include learning rate itself as a parameter in your grid. With some libraries you can specify your choice of kernel. Both these choices will impact your optimal maximum number of iterations, so setting it sufficiently high and using early stopping might be a good strategy.\n", + "\n", + "For less exhaustive and non-discritised methods try smart combinations of the following to perform adaptive scans or build your own:\n", + "* sklearn.model_selection.RandomizedSearchCV\n", + "* sklearn.model_selection.GridSearchCV\n", + "\n", + "Moving to higher dimentional optimisation problems may require more sophisticated solutions:\n", + "* skopt.BayesSearchCV\n", + "* hyperopt.tpe" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Full stats plots saved here: bit.ly/LHCb_XGB_Tuning\n", + "Run with full stats by removing entrystop at max_events in cell 8." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Final lesson time and processing time check\n", + "print(\"Notebook real time --- %s seconds ---\" % (time.time() - stt))\n", + "print(\"Notebook CPU time --- %s seconds ---\" % (time.process_time() - stc))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:anaessentials311] *", + "language": "python", + "name": "python" + }, + "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.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/advanced-python/40Histograms.ipynb b/advanced-python/40Histograms.ipynb index 2827a6a9..917384e4 100644 --- a/advanced-python/40Histograms.ipynb +++ b/advanced-python/40Histograms.ipynb @@ -547,22 +547,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/45DemoReweighting.ipynb b/advanced-python/45DemoReweighting.ipynb index 02dd9a47..77215a8a 100644 --- a/advanced-python/45DemoReweighting.ipynb +++ b/advanced-python/45DemoReweighting.ipynb @@ -53,12 +53,10 @@ "columns = ['hSPD', 'pt_b', 'pt_phi', 'vchi2_b', 'mu_pt_sum']\n", "\n", "with uproot.open('https://starterkit.web.cern.ch/starterkit/data/advanced-python-2019/MC_distribution.root',\n", - " httpsource={'chunkbytes': 1024*1024, 'limitbytes': 33554432, 'parallel': 64}\n", " ) as original_file:\n", " original_tree = original_file['tree']\n", " original = original_tree.arrays(library='pd')\n", "with uproot.open('https://starterkit.web.cern.ch/starterkit/data/advanced-python-2019/RD_distribution.root',\n", - " httpsource={'chunkbytes': 1024*1024, 'limitbytes': 33554432, 'parallel': 64}\n", " ) as target_file:\n", " target_tree = target_file['tree']\n", " target = target_tree.arrays(library='pd')\n", @@ -487,22 +485,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/50LikelihoodInference.ipynb b/advanced-python/50LikelihoodInference.ipynb index 0ec7c601..536fc13f 100644 --- a/advanced-python/50LikelihoodInference.ipynb +++ b/advanced-python/50LikelihoodInference.ipynb @@ -41,7 +41,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "jupyter": { + "is_executing": true + } + }, "outputs": [], "source": [ "%store -r bkg_df\n", @@ -91,19 +95,7 @@ "metadata": {}, "outputs": [], "source": [ - "obs = zfit.Space('Jpsi_M', limits=(2.8, 3.5)) # defining the observable" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# bkg = zfit.Data.from_pandas(bkg_df['Jpsi_M'], obs=obs)\n", - "# OR\n", - "# obs_bkg = zfit.Space('Jpsi_M', limits=(2.8, 3.0)) + zfit.Space('Jpsi_M', limits=(3.2, 3.5))\n", - "# bkg_two = zfit.Data.from_pandas(data_df['Jpsi_M'], obs=obs_bkg)" + "obs = zfit.Space('Jpsi_M', 2.8, 3.5, label='$J/\\\\psi$ mass [GeV]') # defining the observable" ] }, { @@ -120,11 +112,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Difference of the two spaces\n", - "\n", - "While the first space is defined over the whole space from 2.8 to 3.5, the second consists of two distinct regions. Therefore we can use the original space and zfit applies the cut, the same as we did before to the `bkg_df`.\n", + "## Creating the model\n", "\n", - "The difference comes when using the normalization in the PDF: we can either normalize it over the whole range or only over part of it." + "In the following, we create an extended background and signal model and combine it.\n", + "zfit takes care of correctly normalizing and adding the PDFs and the yields." ] }, { @@ -133,12 +124,12 @@ "metadata": {}, "outputs": [], "source": [ - "lambd = zfit.Parameter('lambda', -0.1, -2, 2)\n", - "bkg_yield = zfit.Parameter('bkg_yield', 5000, 0, 200000, step_size=1)\n", + "lambd = zfit.Parameter('lambda', -0.1, -2, 2, label=r\"$\\lambda$\") # label is optional\n", + "bkg_yield = zfit.Parameter('bkg_yield', 5000, 0, 200000, step_size=1, label=\"Bkg yield\")\n", "\n", "mu = zfit.Parameter('mu', 3.1, 2.9, 3.3)\n", "sigma = zfit.Parameter('sigma', 0.1, 0, 0.5)\n", - "sig_yield = zfit.Parameter('sig_yield', 200, 0, 10000, step_size=1)" + "sig_yield = zfit.Parameter('sig_yield', 200, 0, 10000, step_size=1, label=\"Signal yield\")" ] }, { @@ -147,8 +138,7 @@ "metadata": {}, "outputs": [], "source": [ - "bkg_pdf = zfit.pdf.Exponential(lambd, obs=obs)\n", - "bkg_pdf.set_yield(bkg_yield)" + "bkg_pdf = zfit.pdf.Exponential(lambd, obs=obs, extended=bkg_yield)" ] }, { @@ -157,8 +147,7 @@ "metadata": {}, "outputs": [], "source": [ - "sig_pdf = zfit.pdf.Gauss(obs=obs, mu=mu, sigma=sigma)\n", - "sig_pdf.set_yield(sig_yield)" + "sig_pdf = zfit.pdf.Gauss(obs=obs, mu=mu, sigma=sigma, extended=sig_yield)" ] }, { @@ -190,15 +179,15 @@ " if ax is None:\n", " ax = plt.gca()\n", "\n", - " lower, upper = data.space.limit1d\n", + " lower, upper = data.space.v1.limits\n", "\n", " # Creates and histogram of the data and plots it with mplhep.\n", - " counts, bin_edges = np.histogram(data.unstack_x(), bins=nbins)\n", - " mplhep.histplot(counts, bins=bin_edges, histtype=\"errorbar\", yerr=True,\n", + " binneddata = data.to_binned(nbins)\n", + " mplhep.histplot(binneddata, histtype=\"errorbar\", yerr=True,\n", " label=\"Data\", ax=ax, color=\"black\")\n", "\n", - " binwidth = np.diff(bin_edges)[0]\n", - " x = np.linspace(lower, upper, num=1000) # or tf.linspace\n", + " binwidth = binneddata.space.binning[0].widths[0] # all have the same width\n", + " x = np.linspace(lower, upper, num=1000)\n", "\n", " # Line plots of the total pdf and the sub-pdfs.\n", " y = model.ext_pdf(x) * binwidth\n", @@ -207,8 +196,8 @@ " ym = m.ext_pdf(x) * binwidth\n", " ax.plot(x, ym, label=l, color=c)\n", "\n", - " plt.xlabel('$J/\\\\psi$ mass [GeV]')\n", - " ax.set_title(data.data_range.obs[0])\n", + " plt.xlabel(data.space.labels[0])\n", + " ax.set_title(data.space.labels[0])\n", " ax.set_xlim(lower, upper)\n", " ax.legend(fontsize=15)\n", "\n", @@ -255,7 +244,7 @@ "metadata": {}, "outputs": [], "source": [ - "minimizer = zfit.minimize.Minuit()\n", + "minimizer = zfit.minimize.Minuit(gradient=\"zfit\") # to use the analytic gradient. Set it to true will use the iminuit one\n", "# minimizer = zfit.minimize.NLoptLBFGSV1() # can be changed but maybe not as powerful as iminuit\n", "# minimizer = zfit.minimize.ScipySLSQPV1()" ] @@ -393,22 +382,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/60sPlot.ipynb b/advanced-python/60sPlot.ipynb index cfac4125..37337fda 100644 --- a/advanced-python/60sPlot.ipynb +++ b/advanced-python/60sPlot.ipynb @@ -880,22 +880,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/70ScikitHEPUniverse.ipynb b/advanced-python/70ScikitHEPUniverse.ipynb index 98710b73..a146ec27 100644 --- a/advanced-python/70ScikitHEPUniverse.ipynb +++ b/advanced-python/70ScikitHEPUniverse.ipynb @@ -272,22 +272,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/advanced-python/README.md b/advanced-python/README.md index 2dde2d2d..e56dac86 100644 --- a/advanced-python/README.md +++ b/advanced-python/README.md @@ -4,6 +4,8 @@ Welcome to the advanced Python tutorials of the starterkit. This lecture covers and the notebooks available may fill more than the scheduled lesson. However, they also serve as a knowledge base that one can always come back to lock up things. +[//]: # (33ModelTuning.ipynb was commented out below, fails, would need to be fixed) + ```eval_rst .. toctree:: :maxdepth: 3 @@ -17,7 +19,6 @@ a knowledge base that one can always come back to lock up things. 30Classification.ipynb 31ClassificationExtension.ipynb 32BoostingToUniformity.ipynb - 33ModelTuning.ipynb 40Histograms.ipynb 45DemoReweighting.ipynb 50LikelihoodInference.ipynb diff --git a/environment.yml b/environment.yml index a10aefbc..3ed6c4f8 100644 --- a/environment.yml +++ b/environment.yml @@ -1,7 +1,9 @@ name: analysis-essentials channels: - conda-forge + - nodefaults dependencies: + - python ~=3.11.0 - boost-histogram - hep_ml - hepunits @@ -11,23 +13,26 @@ dependencies: - mplhep - nb_conda - nb_conda_kernels - - notebook<7.0.0 # fixes failed nb_conda install https://github.com/DeepLabCut/DeepLabCut/issues/2322 + - notebook # <7.0.0 # fixes failed nb_conda install https://github.com/DeepLabCut/DeepLabCut/issues/2322 - numpy - pandas - particle - pandoc - pip + - uv - scikit-learn - scipy - - uproot <5.0.0 # 5.0.0 breaks the httpsource argument with open, TODO upgrade (what's the equivalent?) in the "get_truth" function - - uproot3 + - uproot >=5.0.0 # 5.0.0 breaks the httpsource argument with open, TODO upgrade (what's the equivalent?) in the "get_truth" function + - aiohttp # needed for uproot http access + - requests # needed for uproot http access - vector - wget -# - xgboost - - zfit >=0.14.0 + # - xgboost - hepstats - pip: - - git+https://github.com/hsf-training/python-lesson.git - - formulate - - starterkit-ci - - xgboost + - zfit >=0.24.0 # to have the newest version, TensorFlow is a bit stuck: https://github.com/conda-forge/tensorflow-feedstock/pull/408 + - zfit-physics >=0.7.0 + - git+https://github.com/hsf-training/python-lesson.git + - formulate + - starterkit-ci + - xgboost diff --git a/git/fig/git-freshly-made-gitlab-repo.png b/git/fig/git-freshly-made-gitlab-repo.png index 41064276..0ed30de8 100644 Binary files a/git/fig/git-freshly-made-gitlab-repo.png and b/git/fig/git-freshly-made-gitlab-repo.png differ diff --git a/git/fig/github-add-collaborators.png b/git/fig/github-add-collaborators.png index 96922cc7..29faa14b 100644 Binary files a/git/fig/github-add-collaborators.png and b/git/fig/github-add-collaborators.png differ diff --git a/git/fig/github-change-repo-string.png b/git/fig/github-change-repo-string.png index 29720991..f8072d27 100644 Binary files a/git/fig/github-change-repo-string.png and b/git/fig/github-change-repo-string.png differ diff --git a/git/fig/github-create-repo-01.png b/git/fig/github-create-repo-01.png index 6dc6bf21..804d0d80 100644 Binary files a/git/fig/github-create-repo-01.png and b/git/fig/github-create-repo-01.png differ diff --git a/git/fig/github-create-repo-02.png b/git/fig/github-create-repo-02.png index d0c006f9..2da6a07b 100644 Binary files a/git/fig/github-create-repo-02.png and b/git/fig/github-create-repo-02.png differ diff --git a/git/fig/github-create-repo-03.png b/git/fig/github-create-repo-03.png index ea56feb0..16577fd9 100644 Binary files a/git/fig/github-create-repo-03.png and b/git/fig/github-create-repo-03.png differ diff --git a/git/fig/github-find-repo-string.png b/git/fig/github-find-repo-string.png index 00276599..b0144b2c 100644 Binary files a/git/fig/github-find-repo-string.png and b/git/fig/github-find-repo-string.png differ diff --git a/git/fig/gitlab-add-collaborators.png b/git/fig/gitlab-add-collaborators.png index 39bec341..d436913c 100644 Binary files a/git/fig/gitlab-add-collaborators.png and b/git/fig/gitlab-add-collaborators.png differ diff --git a/git/fig/gitlab-ci-artefacts.png b/git/fig/gitlab-ci-artefacts.png index 493b63d7..de15a97c 100644 Binary files a/git/fig/gitlab-ci-artefacts.png and b/git/fig/gitlab-ci-artefacts.png differ diff --git a/git/fig/gitlab-ci-first-log.png b/git/fig/gitlab-ci-first-log.png index 3a5162cd..4804bf4c 100644 Binary files a/git/fig/gitlab-ci-first-log.png and b/git/fig/gitlab-ci-first-log.png differ diff --git a/git/fig/gitlab-ci-pipeline-link.png b/git/fig/gitlab-ci-pipeline-link.png index e05c3de9..2d3bf3f9 100644 Binary files a/git/fig/gitlab-ci-pipeline-link.png and b/git/fig/gitlab-ci-pipeline-link.png differ diff --git a/git/fig/gitlab-ci-view-log.png b/git/fig/gitlab-ci-view-log.png index 6365d567..e9029c28 100644 Binary files a/git/fig/gitlab-ci-view-log.png and b/git/fig/gitlab-ci-view-log.png differ diff --git a/git/fig/gitlab-ci-view-pipeline.png b/git/fig/gitlab-ci-view-pipeline.png index 6200cab5..b10365e5 100644 Binary files a/git/fig/gitlab-ci-view-pipeline.png and b/git/fig/gitlab-ci-view-pipeline.png differ diff --git a/git/fig/gitlab-create-repo-01.png b/git/fig/gitlab-create-repo-01.png index e53818d2..5a24d3e8 100644 Binary files a/git/fig/gitlab-create-repo-01.png and b/git/fig/gitlab-create-repo-01.png differ diff --git a/git/fig/gitlab-create-repo-02.png b/git/fig/gitlab-create-repo-02.png index 31db79ed..b60b525a 100644 Binary files a/git/fig/gitlab-create-repo-02.png and b/git/fig/gitlab-create-repo-02.png differ diff --git a/git/fig/gitlab-create-repo-03.png b/git/fig/gitlab-create-repo-03.png index 55eca77d..b47c3eef 100644 Binary files a/git/fig/gitlab-create-repo-03.png and b/git/fig/gitlab-create-repo-03.png differ diff --git a/git/fig/gitlab-find-repo-string.png b/git/fig/gitlab-find-repo-string.png index c990b425..06bec1f4 100644 Binary files a/git/fig/gitlab-find-repo-string.png and b/git/fig/gitlab-find-repo-string.png differ diff --git a/git/fig/gitlab-pr-close.png b/git/fig/gitlab-pr-close.png index 0366f465..6520fbf6 100644 Binary files a/git/fig/gitlab-pr-close.png and b/git/fig/gitlab-pr-close.png differ diff --git a/git/fig/gitlab-pr-commits.png b/git/fig/gitlab-pr-commits.png index 44acc14d..e0ee89a1 100644 Binary files a/git/fig/gitlab-pr-commits.png and b/git/fig/gitlab-pr-commits.png differ diff --git a/git/fig/gitlab-pr-diff.png b/git/fig/gitlab-pr-diff.png index 9a7378ff..e01e9802 100644 Binary files a/git/fig/gitlab-pr-diff.png and b/git/fig/gitlab-pr-diff.png differ diff --git a/git/fig/gitlab-pr-discussion.png b/git/fig/gitlab-pr-discussion.png index ef8d5fdf..44d4c265 100644 Binary files a/git/fig/gitlab-pr-discussion.png and b/git/fig/gitlab-pr-discussion.png differ diff --git a/git/fig/gitlab-pr-fork.png b/git/fig/gitlab-pr-fork.png index c3e61773..8dfb56c6 100644 Binary files a/git/fig/gitlab-pr-fork.png and b/git/fig/gitlab-pr-fork.png differ diff --git a/git/fig/gitlab-pr-merge.png b/git/fig/gitlab-pr-merge.png index 97c8d3bb..a879a065 100644 Binary files a/git/fig/gitlab-pr-merge.png and b/git/fig/gitlab-pr-merge.png differ diff --git a/git/fig/gitlab-pr-newmergerequest.png b/git/fig/gitlab-pr-newmergerequest.png index 587a33f6..af897372 100644 Binary files a/git/fig/gitlab-pr-newmergerequest.png and b/git/fig/gitlab-pr-newmergerequest.png differ diff --git a/git/fig/gitlab-pr-selectbranch.png b/git/fig/gitlab-pr-selectbranch.png index 0af33f43..683b9355 100644 Binary files a/git/fig/gitlab-pr-selectbranch.png and b/git/fig/gitlab-pr-selectbranch.png differ diff --git a/git/fig/gitlab-pr-title.png b/git/fig/gitlab-pr-title.png index 3d28fc92..e1b26240 100644 Binary files a/git/fig/gitlab-pr-title.png and b/git/fig/gitlab-pr-title.png differ diff --git a/git/fig/gitlab-pr-wheretofork.png b/git/fig/gitlab-pr-wheretofork.png index ca5e2581..6d5c7014 100644 Binary files a/git/fig/gitlab-pr-wheretofork.png and b/git/fig/gitlab-pr-wheretofork.png differ diff --git a/git/fig/phd101212s.png b/git/fig/phd101212s.png index c47c428b..535a1a0b 100644 Binary files a/git/fig/phd101212s.png and b/git/fig/phd101212s.png differ diff --git a/hsf_logo_angled.png b/hsf_logo_angled.png index 8c1dbc78..b44dc09b 100644 Binary files a/hsf_logo_angled.png and b/hsf_logo_angled.png differ diff --git a/python/01basics.ipynb b/python/01basics.ipynb index 451812bd..5c158344 100644 --- a/python/01basics.ipynb +++ b/python/01basics.ipynb @@ -3,9 +3,11 @@ { "cell_type": "markdown", "metadata": { + "editable": true, "slideshow": { "slide_type": "slide" - } + }, + "tags": [] }, "source": [ "# 1: Basics" @@ -54,6 +56,9 @@ "cell_type": "code", "execution_count": null, "metadata": { + "jupyter": { + "is_executing": true + }, "slideshow": { "slide_type": "subslide" } @@ -127,6 +132,9 @@ "cell_type": "code", "execution_count": null, "metadata": { + "jupyter": { + "is_executing": true + }, "slideshow": { "slide_type": "subslide" } @@ -139,7 +147,7 @@ "\n", "# several ways for strings\n", "c = \"hello\"\n", - "d = 'world'\n", + "d = \"world\"\n", "cd = \"welcome to this 'world' here\" # we can now use '' inside (or vice versa)\n", "e = \"\"\"hello world\"\"\" # which we can also wrap\n", "e2 = \"\"\"hello\n", @@ -153,6 +161,9 @@ "cell_type": "code", "execution_count": null, "metadata": { + "jupyter": { + "is_executing": true + }, "slideshow": { "slide_type": "subslide" } @@ -551,7 +562,12 @@ }, "outputs": [], "source": [ - "person = {'name': \"Jonas Eschle\", 'age': 42, 5: True, 11: \"hi\"} # we can use strings but also other elements\n", + "person = {\n", + " \"name\": \"Jonas Eschle\",\n", + " \"age\": 42,\n", + " 5: True,\n", + " 11: \"hi\",\n", + "} # we can use strings but also other elements\n", "print(person)" ] }, @@ -565,7 +581,7 @@ }, "outputs": [], "source": [ - "print(person['name'])\n", + "print(person[\"name\"])\n", "print(person[5])\n", "print(person[11])" ] @@ -591,7 +607,7 @@ }, "outputs": [], "source": [ - "person['age'] = '42.00001'" + "person[\"age\"] = \"42.00001\"" ] }, { @@ -615,7 +631,7 @@ }, "outputs": [], "source": [ - "person['alias'] = \"Mayou36\"\n", + "person[\"alias\"] = \"Mayou36\"\n", "print(person)" ] }, @@ -645,7 +661,7 @@ }, "outputs": [], "source": [ - "person['nationality']" + "person[\"nationality\"]" ] }, { @@ -669,7 +685,9 @@ }, "outputs": [], "source": [ - "hair_color = person.get('hair_color', 'unknown color') # the second argument gets returned if key is not in dict\n", + "hair_color = person.get(\n", + " \"hair_color\", \"unknown color\"\n", + ") # the second argument gets returned if key is not in dict\n", "print(hair_color)" ] }, @@ -1013,8 +1031,8 @@ }, "outputs": [], "source": [ - "a = 'spam'\n", - "list_a = [1, 5, 2, 'world', 1]\n", + "a = \"spam\"\n", + "list_a = [1, 5, 2, \"world\", 1]\n", "print(a, b)\n", "print(list_a, list_b)" ] @@ -1056,7 +1074,7 @@ }, "outputs": [], "source": [ - "list_a[1] = 'hello'\n", + "list_a[1] = \"hello\"\n", "print(list_a, list_b)" ] }, @@ -1096,7 +1114,7 @@ }, "outputs": [], "source": [ - "list_a[2] = 'my'\n", + "list_a[2] = \"my\"\n", "print(list_a)\n", "print(list_b)\n", "print(list_c)" @@ -1141,7 +1159,7 @@ }, "outputs": [], "source": [ - "list_c[2] = 'my' # make it the same as the other lists\n", + "list_c[2] = \"my\" # make it the same as the other lists\n", "print(list_a == list_c)" ] }, @@ -1220,7 +1238,7 @@ "list_of_squares = [i**2 for i in range(N)]\n", "sum_of_squares = sum(list_of_squares)\n", "\n", - "print('Sum of squares for', N, 'is', sum_of_squares)" + "print(\"Sum of squares for\", N, \"is\", sum_of_squares)" ] }, { @@ -1257,6 +1275,9 @@ "cell_type": "code", "execution_count": null, "metadata": { + "jupyter": { + "is_executing": true + }, "slideshow": { "slide_type": "subslide" } @@ -1278,7 +1299,7 @@ "outputs": [], "source": [ "N = 5\n", - "print('The square of', N, 'is', squares[N])" + "print(\"The square of\", N, \"is\", squares[N])" ] }, { @@ -1366,29 +1387,21 @@ "eta_low = 2\n", "eta_high = 5\n", "\n", - "cut_string = f'(PT > {pt_cut:.2f}) & ({eta_low} < ETA < {eta_high})'\n", + "cut_string = f\"(PT > {pt_cut:.2f}) & ({eta_low} < ETA < {eta_high})\"\n", "print(cut_string)" ] } ], "metadata": { "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" + "nbconvert_exporter": "python" }, "nbsphinx": { "execute": "auto" diff --git a/python/README.md b/python/README.md index 3d5f98ff..e8c22269 100644 --- a/python/README.md +++ b/python/README.md @@ -47,11 +47,11 @@ Sounds good? Then let’s get going! 00scripts.ipynb 01basics.ipynb operators.md - numbers.md - strings.md - lists.md - dictionaries.md - conditions.md + numbers.ipynb + strings.ipynb + lists.ipynb + dictionaries.ipynb + conditions.ipynb methods.md scripting.md modules.md diff --git a/python/classes.ipynb b/python/classes.ipynb index 4fe59ea0..6c660277 100644 --- a/python/classes.ipynb +++ b/python/classes.ipynb @@ -484,22 +484,14 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { - "name": "ipython", - "version": 3 + "name": "ipython" }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" + "nbconvert_exporter": "python" } }, "nbformat": 4, diff --git a/python/conditions.ipynb b/python/conditions.ipynb new file mode 100644 index 00000000..3223d4cc --- /dev/null +++ b/python/conditions.ipynb @@ -0,0 +1,966 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Conditions\n", + "\n", + "Sometimes, often while looping, you only want to do things depending on\n", + "something’s value. Specifying _conditions_ like this is pretty simple in Python.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pizzas = [\"Pineapple\", \"Cheese\", \"Pepperoni\", \"Hot dog\"]\n", + "for p in pizzas:\n", + " if p == \"Cheese\":\n", + " print(\"Nice pizza!\")\n", + " elif p == \"Pepperoni\":\n", + " print(\"Amazing pizza!\")\n", + " else:\n", + " print(\"Weird pizza.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Like the \"body\" of the `for` loop, called a _block_, the block in the `if`,\n", + "`elif`, and `else` statements must be indented. The convention we adopt is to\n", + "use four spaces for indentation.\n", + "\n", + "The `if` statement starts with `if` (duh!) and what follows is a _condition_.\n", + "If this condition isn’t met, the next `elif` (for \"else-if\") condition is\n", + "evaluated. If this also isn’t met, the `else` block is run. You can use as many\n", + "`elif` conditions as you like, or none at all, and the `else` block is optional.\n", + "\n", + "\n", + "Python evaluates a condition and sees whether it is truth-like or not. If it is\n", + "truth-like, the code in the block is run.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if pizzas[0] == \"Cheese\":\n", + " print(\"It is cheese, my dudes.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pizzas[0] == \"Cheese\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pizzas[1] == \"Cheese\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`False` and `True` are variables. They\n", + "correspond to the possible values a boolean variable can have.\n", + "\n", + "The result of a comparison is `True` or `False`, and we can perform comparisons\n", + "using several operators, like `==` for equality, `!=` for inequality, `>` and\n", + "`<` for relative magnitude, and so on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "True is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "True is not False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "1 > 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "(1 > 2) is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This shows that we can combine comparison operators, just like with `+` and\n", + "friends. We can also use `and` to require multiple conditions, `or` to require\n", + "at least one, and `not` to negate a result.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "1 < x and x < 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "3 < x or 1 < x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "not 1 < x" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that `and`, `or` and `not` have lower precedence than `>`, `<` and `==`, but\n", + "you can (and in general should) use parentheses to be more explicit.\n", + "\n", + "**Exercise**\n", + "\n", + "Play around with the booleans and try to answer the following! Can you do\n", + " - double negotiations\n", + " - convert a boolean to an integer\n", + " - use parentheses to create a more complicated expression" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Of course, we can compare everything we have played around with so far." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = [1, 2]\n", + "y = [1, 2]\n", + "z = {\"hero\": \"thor\"}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x == y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "y == z" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For collection objects like lists, tuples, and dictionaries, we can easily ask\n", + "them if they contain something in particular using the `in` operator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "3 in x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "2 in y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"thor\" in z" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The last statement is `False` because `in` queries a dictionaries _keys_, not \n", + "its values. This is useful if you want to access a key in a dictionary that \n", + "might not exist:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "z[\"pizza\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "if \"pizza\" in z:\n", + " print(\"We have pizza\", z[\"pizza\"])\n", + "else:\n", + " print(\"No pizza :(\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Note that `in` doesn’t dive into nested collections, but only looks at the top\n", + "level." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "1 in [[1, 2], [3, 4]] # the elements are two lists" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "[1, 2] in [[1, 2], [3, 4]]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "\n", + "**Advanced (skip on first read)**\n", + "\n", + "Find the double-underscore method on lists and dictionaries that corresponds to\n", + "the `in` operator, and check that it does the same thing as the operator.\n", + "\n", + "**Solutions**\n", + "\n", + "Taking lists as an example, the `dir` method can tell us what methods are\n", + "available. The `__contains__` method sounds promising." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "x = [1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "x.__contains__(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "x.__contains__(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Strings work a lot like lists, which makes sense because they are effectively\n", + "collections of single characters. This means we can also query string contents\n", + "with `in`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "fact = \"The best hero is Thor.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "\"Thor\" in fact" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "\"Iron Man\" in fact" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Truthiness\n", + "\n", + "It’s conventional not to explicitly compare a condition to `True`, because the\n", + "`if` statement already does that for us." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "if (\"Pineapple\" in pizzas) is True:\n", + " print(\"Weird.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "if \"Pineapple\" in pizzas:\n", + " print(\"Not weird.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Likewise, rather than comparing for False, we just use `not`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "if (\"Pineapple\" in pizzas) is False:\n", + " print(\"Weird.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "if \"Pineapple\" not in pizzas:\n", + " print(\"Not weird.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "\"Pineapple\" not in pizzas # not ('Pineapple' in pizzas)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "\"Pineapple\" not in pizzas" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The last two lines show that we can use `not in` for checking that something\n", + "_is not_ in a collection. This reads more naturally." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "All Python objects are truth-like unless they are the value `False`, the value\n", + "`None`, or are empty collections (such as `\"\"`, `[]`, `()`, `{}`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "if list() or dict() or tuple() or \"\":\n", + " print(\"You won’t see me!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The value `None`, which is available as the variable named `None`, is often\n", + "used as placeholder for an empty value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "favourite = None\n", + "for p in pizzas:\n", + " if \"Olives\" in p:\n", + " favourite = p" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "if favourite:\n", + " print(f\"Found favourite: {favourite}\")\n", + "else:\n", + " print(\"No favourite :(\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "It behaves as false-y value in conditions." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Conditions in loops\n", + "\n", + "`for` loops and comprehensions are the most common ways of iterating in Python.\n", + "We’ve already seen that using conditions in these can be useful." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "not_cheesy = [p for p in pizzas if \"cheese\" not in p.lower()]\n", + "not_cheesy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Another way of iterating is with `while`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "i = 5\n", + "while i > 0:\n", + " print(f\"T-minus {i} seconds\")\n", + " # Equivalent to `i = i - 1`\n", + " i -= 1\n", + "print(\"Blast off!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The `while` loop checks the condition, runs the block, and then re-checks the\n", + "condition. If we don’t do something in the loop to change the result of the\n", + "condition, we will end up looping forever!\n", + "\n", + "You can uncomment and run the below, you may need to stop the kernel manually with the stop button." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "# i = 5\n", + "# while i > 0:\n", + "# print('All work and no play makes Jack a dull boy')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Because we do not change the value of `i` in the loop, the condition always\n", + "evaluates to `True`, so we’re stuck. You can stop Python running the code by\n", + "typing the `Ctrl-c` key combination." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Sometimes you want to stop iterating when some condition is met. You could\n", + "achieve this with a `while` loop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "ok = False\n", + "i = 0\n", + "while not ok:\n", + " ok = \"cheese\" in pizzas[i].lower()\n", + " # Equivalent to `i = i + 1`\n", + " i += 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "i" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "pizzas[i - 1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "It is not nice to have to keep track of these `ok` and `i` variables. Instead,\n", + "we can use a `for` loop, which feels much more natural when iterating over a\n", + "collection, and `break` to stop looping (we can also use a break with a `while` loop)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for pizza in pizzas:\n", + " if \"cheese\" in pizza.lower():\n", + " yum = pizza\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "yum" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython" + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/conditions.md b/python/conditions.md deleted file mode 100644 index 68f8686b..00000000 --- a/python/conditions.md +++ /dev/null @@ -1,319 +0,0 @@ -# Conditions - -Sometimes, often while looping, you only want to do things depending on -something’s value. Specifying _conditions_ like this is pretty simple in Python. - -```python ->>> pizzas = ['Pineapple', 'Cheese', 'Pepperoni', 'Hot dog'] ->>> for p in pizzas: -... if p == 'Cheese': -... print('Nice pizza!') -... elif p == 'Pepperoni': -... print('Amazing pizza!') -... else: -... print('Weird pizza.') -... -Weird pizza. -Nice pizza! -Amazing pizza! -Weird pizza. -``` - -Like the "body" of the `for` loop, called a _block_, the block in the `if`, -`elif`, and `else` statements must be indented. The convention we adopt is to -use four spaces for indentation. - -The `if` statement starts with `if` (duh!) and what follows is a _condition_. -If this condition isn’t met, the next `elif` (for "else-if") condition is -evaluated. If this also isn’t met, the `else` block is run. You can use as many -`elif` conditions as you like, or none at all, and the `else` block is optional. - -{% callout "Ternary conditional operator" %} - -You can use a succinct one-line syntax for conditional assignments like this: - -```python ->>> x = 'ok' if pizzas[0] == 'Cheese' else 'not ok' ->>> x -'not ok' -``` - -Make sure your line does not get too long in order not to impair its -readability! - -{% endcallout %} - - -Python evaluates a condition and sees whether it is truth-like or not. If it is -truth-like, the code in the block is run. - -```python ->>> if pizzas[0] == 'Cheese': -... print('It is cheese, my dudes.') -... ->>> pizzas[0] == 'Cheese' -False ->>> pizzas[1] == 'Cheese' -True -``` - -`False` and `True` are variables and they can actually be reassigned to some -other value (though it is quite pointless and dangerous to do that!) They -correspond to the possible values a boolean variable can have. Here is why you -should never touch those variables: - -```python ->>> True = 1 ->>> True -1 ->>> True = False ->>> True == False -True -``` - -The result of a comparison is `True` or `False`, and we can perform comparisons -using several operators, like `==` for equality, `!=` for inequality, `>` and -`<` for relative magnitude, and so on. - -```python ->>> True, False -(True, False) ->>> False -False ->>> True == False -False ->>> True != False -True ->>> 1 > 2 -False ->>> (1 > 2) == False -True -``` - -This shows that we can combine comparison operators, just like with `+` and -friends. We can also use `and` to require multiple conditions, `or` to require -at least one, and `not` to negate a result. - -```python -x = 2 ->>> 1 < x and x < 3 ->>> 3 < x or 1 < x -True ->>> not 1 < x -False -``` - -Note that `and`, `or` and `not` have lower precedence than `>`, `<` and `==`, but -you can use parentheses to be more explicit. - -Of course, we can compare everything we have played around with so far. - -```python ->>> x = [1, 2] ->>> y = [1, 2] ->>> z = {'hero': 'thor'} ->>> x == y -True ->>> y == z -False -``` - -For collection objects like lists, tuples, and dictionaries, we can easily ask -them if they contain something in particular using the `in` operator. - -```python ->>> 3 in x -False ->>> 2 in y -True ->>> 'thor' in z -False -``` - -The last statement is `False` because `in` queries a dictionaries _keys_, not -its values. This is useful if you want to access a key in a dictionary that -might not exist: - -```python ->>> z['pizza'] -Traceback (most recent call last): - File "", line 1, in -NameError: name 'z' is not defined ->>> if 'pizza' in z: -... print('We have pizza', z['pizza']) -... else -... print('No pizza :(') -``` - -Note that `in` doesn’t dive into nested collections, but only looks at the top -level. - -```python ->>> 1 in [[1, 2], [3, 4]] -False ->>> [1, 2] in [[1, 2], [3, 4]] -True -``` - -{% challenge "The `in` operator" %} - -Find the double-underscore method on lists and dictionaries that corresponds to -the `in` operator, and check that it does the same thing as the operator. - -{% solution "Solution" %} - -Taking lists as an example, the `dir` method can tell us what methods are -available. The `__contains__` method sounds promising. - -```python ->>> x= [1] ->>> x.__contains__(1) -True ->>> x.__contains__(2) -False -``` - -{% endsolution %} - -{% endchallenge %} - -Strings work a lot like lists, which makes sense because they are effectively -collections of single characters. This means we can also query string contents -with `in`. - -```python ->>> fact = 'The best hero is Thor.' ->>> 'Thor' in fact -True ->>> 'Iron Man' in fact -False -``` - -## Truthiness - -It’s conventional not to explicitly compare a condition to `True`, because the -`if` statement already does that for us. - -```python ->>> if ('Pineapple' in pizzas) == True: -... print('Weird.') -... ->>> if 'Pineapple' in pizzas: -... print('Not weird.') -... -``` - -Likewise, rather than comparing for False, we just use `not`. - -```python ->>> if ('Pineapple' in pizzas) == False: -... print ('Weird.') -... ->>> if not 'Pineapple' in pizzas: -... print ('Not weird.') -... ->>> not 'Pineapple' in pizzas: ->>> 'Pineapple' not in pizzas: -``` - -The last two lines show that we can use `not in` for checking that something -_is not_ in a collection. This reads more naturally. - -All Python objects are truth-like unless they are the value `False`, the value -`None`, or are empty collections (such as `""`, `[]`, `()`, `{}`). - -```python ->>> if list() or dict() or tuple() or str(): -... print ("You won’t see me!") -``` - -The value `None`, which is available as the variable named `None`, is often -used as placeholder for an empty value. - -```python ->>> favourite = None ->>> for p in pizzas: -... if 'Olives' in p: -... favourite = p -... ->>> if favourite: -... print ('Found favourite: {0}'.format(favourite)) -... else: -... print ('No favourite :(') -No favourite :( -``` - -It behaves as false-y value in conditions. - -## Conditions in loops - -`for` loops and comprehensions are the most common ways of iterating in Python. -We’ve already seen that using conditions in these can be useful. - -```python ->>> not_cheesy = [p for p in pizzas if 'cheese' not in p.lower()] ->>> not_cheesy -['Pineapple', 'Pepperoni', 'Hot dog'] -``` - -Another way of iterating is with `while`. - -```python ->>> i = 5 ->>> while i > 0: -... print 'T-minus {0} seconds'.format(i) -... # Equivalent to `i = i - 1` -... i -= 1 -... print ('Blast off!') -T-minus 5 seconds -T-minus 4 seconds -T-minus 3 seconds -T-minus 2 seconds -T-minus 1 seconds -Blast off! -``` - -The `while` loop checks the condition, runs the block, and then re-checks the -condition. If we don’t do something in the loop to change the result of the -condition, we will end up looping forever! - -```python ->>> i = 5 ->>> while i > 0: -... print('All work and no play makes Jack a dull boy') -``` - -Because we do not change the value of `i` in the loop, the condition always -evaluates to `True`, so we’re stuck. You can stop Python running the code by -typing the `Ctrl-c` key combination. - -Sometimes you want to stop iterating when some condition is met. You could -achieve this with a `while` loop. - -```python ->>> ok = False ->>> i = 0 ->>> while not ok: -... ok = 'cheese' in pizza[i].lower() -... # Equivalent to `i = i + 1` -... i += 1 -... ->>> i -2 ->>> pizza[i - 1] -'Cheese' -``` - -It is not nice to have to keep track of these `ok` and `i` variables. Instead, -we can use a `for` loop, which feels much more natural when iterating over a -collection, and `break` to stop looping. - -```python ->>> for pizza in pizzas: -... if 'cheese' in pizza.lower(): -... yum = pizza -... break -... ->>> yum -'Cheese' -``` diff --git a/python/dictionaries.ipynb b/python/dictionaries.ipynb new file mode 100644 index 00000000..45e63655 --- /dev/null +++ b/python/dictionaries.ipynb @@ -0,0 +1,606 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Dictionaries\n", + "\n", + "You can think of lists as a _mapping_ from indices to values. The indices are\n", + "always integers and go from `0` to `len(the_list) - 1`, and the values are the\n", + "items.\n", + "\n", + "Dictionaries are collections, just like lists, but they have important\n", + "differences:\n", + "\n", + "* lists map sequential numeric indices to items, whereas dictionaries can map\n", + " most object types to any object,\n", + "* lists are _ordered_ collections of items, whereas dictionaries have no\n", + " ordering.\n", + "\n", + "Since anything can be used as index for an item, indices must be always\n", + "specified when creating a dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "d = {1: 0.5, 'excellent index': math.sin, 0.1: 2}\n", + "d[1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "d['excellent index']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "d[0.1] = 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The \"indices\" of a dictionary are called **keys**, and the things they map to\n", + "are **values**. Together, each key-value pair is an **item**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "d.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "d.values()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "d" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "As you can see, the values of a dictionary can be whatever we like, and need not\n", + "be the same type of object.\n", + "\n", + "You can see that the order of the keys, values and items we get back are not the\n", + "same as the order we created the dictionary with. If you run the same example on\n", + "your own you might get a different ordering. This is what we mean when we define\n", + "dictionaries as _unordered collections_: when you iterate over its content, you\n", + "cannot rely on the ordering.\n", + "\n", + "It is however guaranteed that the _n_-th item returned by `keys()` corresponds\n", + "to the _n_-th item returned by `values()`. This allows the following example\n", + "to work flawlessly:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for key, value in zip(d.keys(), d.values()):\n", + " print(key, ':', value)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Of course, this could be considerably simpler just by using `items()`, which\n", + "gives us _tuples of key-value pairs_." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for key, value in d.items():\n", + " print(key, ':', value)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We can create dictionaries from lists of 2-item lists." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "dict(enumerate(['a thing', 'another']))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "And also with _dictionary comprehensions_, in a similar manner to list\n", + "comprehensions, with the additional specification of the key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "{i: i**i for i in range(5) if i != 3}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Dictionary keys\n", + "\n", + "There is no restriction on values a dictionary might hold, but there is on keys." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "numbers = [1, 4, 3]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "dd = {}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "dd[numbers] = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In essence, keys must not be mutable. This includes numbers, strings, and\n", + "tuples, but not lists. This restriction is a trade-off that allows Python to\n", + "make accessing values in a dictionary by key very fast." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Advanced (skip on first read)**\n", + "Immutable data types in Python have a `__hash__()` function, you can test it\n", + "yourself:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "s = \"a string\"\n", + "s.__hash__()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "[A hashing function](https://en.wikipedia.org/wiki/Hash_function) creates an\n", + "encoded (but not unique) representation of the object as a number. When you\n", + "look up an item in a Python dictionary with `my_dict[\"my_key\"]`, what happens\n", + "internally is:\n", + "\n", + "* hash of `\"my_key\"` is calculated,\n", + "* this number is compared to every hash of every key in the dictionary, until a\n", + " match between the hashes is found,\n", + "* if two hashes match, _and_ the two objects are really identical, the\n", + " corresponding dictionary item is returned.\n", + "\n", + "Looking up numbers instead of strings or tuples is considerably faster, but\n", + "since two different strings can have the same hash, their content has to be\n", + "compared as well to really tell whether they are equal. If two hashes are\n", + "different on the other hand we are sure that the objects are different as well." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Iteration over dictionaries is over their keys." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for key in d:\n", + " print(key, ':', d[key])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We have already seen how to iterate over values (using `d.values()`) or keys\n", + "and values simultaneously (using `d.items()`)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Exercise**\n", + "Alphabet mapping\n", + "\n", + "Map each letter of the alphabet to a number with a dictionary comprehension,\n", + "starting with `0` for `a` and ending with `25` for `z`.\n", + "\n", + "You can get a string containing the letters of the alphabet like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import string" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "string.ascii_lowercase" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "You can iterate over a string exactly like a list." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for character in string.ascii_lowercase:\n", + " print(character)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Then, create the \"reverse\" dictionary, again with a comprehension, mapping\n", + "letters to numbers." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Solution**\n", + "You need to have a list containing one number per letter, and to loop over that\n", + "list along with the characters in the string. This is exactly the same as\n", + "looping over items in a list alongside the index, so we can use `enumerate`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "alphabet_map = {i: c for i, c in enumerate(string.ascii_lowercase)}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We can create the inverse map by swapping the key and value in the\n", + "comprehension." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "reverse_map = {c: i for i, c in alphabet_map.items()}" + ] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython" + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/dictionaries.md b/python/dictionaries.md deleted file mode 100644 index 88beb7c1..00000000 --- a/python/dictionaries.md +++ /dev/null @@ -1,214 +0,0 @@ -# Dictionaries - -You can think of lists as a _mapping_ from indices to values. The indices are -always integers and go from `0` to `len(the_list) - 1`, and the values are the -items. - -Dictionaries are collections, just like lists, but they have important -differences: - -* lists map sequential numeric indices to items, whereas dictionaries can map - most object types to any object, -* lists are _ordered_ collections of items, whereas dictionaries have no - ordering. - -Since anything can be used as index for an item, indices must be always -specified when creating a dictionary: - -```python ->>> d = {1: 0.5, 'excellent index': math.sin, 0.1: 2} ->>> d[1] -0.5 ->>> d['excellent index'] - ->>> d[0.1] = 3 -``` - -The "indices" of a dictionary are called **keys**, and the things they map to -are **values**. Together, each key-value pair is an **item**. - -```python ->>> d.keys() -dict_keys([1, 0.1, 'excellent index']) ->>> d.values() -dict_values([0.5, 2, ]) ->>> d -{0.1: 2, 1: 0.5, 'excellent index': } -``` - -As you can see, the values of a dictionary can be whatever we like, and need not -be the same type of object. - -You can see that the order of the keys, values and items we get back are not the -same as the order we created the dictionary with. If you run the same example on -your own you might get a different ordering. This is what we mean when we define -dictionaries as _unordered collections_: when you iterate over its content, you -cannot rely on the ordering. - -It is however guaranteed that the _n_-th item returned by `keys()` corresponds -to the _n_-th item returned by `values()`. This allows the following example -to work flawlessly: - -```python ->>> for key, value in zip(d.keys(), d.values()): -... print(key, ':', value) -... -1 : 0.5 -0.1 : 3 -excellent index : -``` - -Of course, this could be considerably simpler just by using `items()`, which -gives us _tuples of key-value pairs_. - -```python ->>> for key, value in d.items(): -... print(key, ':', value) -... -1 : 0.5 -0.1 : 3 -excellent index : -``` - -We can create dictionaries from lists of 2-item lists. - -```python ->>> dict(enumerate(['a thing', 'another'])) -{0: 'a thing', 1: 'another'} -``` - -And also with _dictionary comprehensions_, in a similar manner to list -comprehensions, with the additional specification of the key. - -```python ->>> {i: i**i for i in range(5) if i != 3} -{0: 1, 1: 1, 2: 4, 4: 256, 5: 3125} -``` - -Note that dictionary comprehensions require at least Python 2.7 to work. - - -## Dictionary keys - -There’s no restriction on values a dictionary might hold, but there is on keys. - -```python ->>> l = [1, 4, 3] ->>> dd = {} ->>> dd[l] = 0 -Traceback (most recent call last): - File "", line 1, in -TypeError: unhashable type: 'list' -``` - -In essence, keys must not be mutable. This includes numbers, strings, and -tuples, but not lists. This restriction is a trade-off that allows Python to -make accessing values in a dictionary by key very fast. - -{% callout "Hashing" %} - -Immutable data types in Python have a `__hash__()` function, you can test it -yourself: - -```python ->>> s = "a string" ->>> s.__hash__() --8411828025894108412 -``` - -[A hashing function](https://en.wikipedia.org/wiki/Hash_function) creates an -encoded (but not unique) representation of the object as a number. When you -look up an item in a Python dictionary with `my_dict["my_key"]`, what happens -internally is: - -* hash of `"my_key"` is calculated, -* this number is compared to every hash of every key in the dictionary, until a - match between the hashes is found, -* if two hashes match, _and_ the two objects are really identical, the - corresponding dictionary item is returned. - -Looking up numbers instead of strings or tuples is considerably faster, but -since two different strings can have the same hash, their content has to be -compared as well to really tell whether they are equal. If two hashes are -different on the other hand we are sure that the objects are different as well. - -{% endcallout %} - -Iteration over dictionaries is over their keys. - -```python ->>> for key in d: -... print key, ':' d[key] -... -1 : 0.5 -0.1 : 3 -excellent index : -``` - -We have already seen how to iterate over values (using `d.values()`) or keys -and values simultaneously (using `d.items()`). - -{% callout "On the efficiency of items()" %} - -In Python 2, using items copies the keys and values of a dictionary, and gives -you back those copies. This can be problematic for large dictionaries as the -amount of memory your program uses can double. Python 3 uses a much more -memory-efficient way of implementing items so that you don't have to worry. - -If you're having memory problems with using items in Python 2, you can use -`viewitems()` instead, which behaves the same way as items does in Python 3. - -Note that there are also similar methods for keys and values, called -`viewkeys()` and `viewvalues()`, and that all of these view methods are only -available from -Python 2.7. - -{% endcallout %} - -{% challenge "Alphabet mapping" %} - -Map each letter of the alphabet to a number with a dictionary comprehension, -starting with `0` for `a` and ending with `25` for `z`. - -You can get a string containing the letters of the alphabet like this: - -```python ->>> import string ->>> string.ascii_lowercase -'abcdefghijklmnopqrstuvwxyz' -``` - -You can iterate over a string exactly like a list. - -```python ->>> for character in string.ascii_lowercase: -... print character -... -a -b -... -z -``` -Then, create the "reverse" dictionary, again with a comprehension, mapping -letters to numbers. - -{% solution "Solution" %} - -You need to have a list containing one number per letter, and to loop over that -list along with the characters in the string. This is exactly the same as -looping over items in a list alongside the index, so we can use `enumerate`. - -```python ->>> alphabet_map = {i: c for i, c in enumerate(string.ascii_lowercase)} -``` - -We can create the inverse map by swapping the key and value in the -comprehension. - -```python ->>> reverse_map = {c: i for i, c in alphabet_map.items()} -``` - -{% endsolution %} - -{% endchallenge %} diff --git a/python/figs/B_flight_distance.png b/python/figs/B_flight_distance.png index b92304c7..eca41d0a 100644 Binary files a/python/figs/B_flight_distance.png and b/python/figs/B_flight_distance.png differ diff --git a/python/figs/B_flight_distance_v2.png b/python/figs/B_flight_distance_v2.png index 4632b297..d927291e 100644 Binary files a/python/figs/B_flight_distance_v2.png and b/python/figs/B_flight_distance_v2.png differ diff --git a/python/figs/B_flight_distance_v3.png b/python/figs/B_flight_distance_v3.png index d037bc63..b6ebb7b0 100644 Binary files a/python/figs/B_flight_distance_v3.png and b/python/figs/B_flight_distance_v3.png differ diff --git a/python/figs/B_flight_distance_with_cut_compare.png b/python/figs/B_flight_distance_with_cut_compare.png index 9c552e86..4af8e8ac 100644 Binary files a/python/figs/B_flight_distance_with_cut_compare.png and b/python/figs/B_flight_distance_with_cut_compare.png differ diff --git a/python/lists.ipynb b/python/lists.ipynb new file mode 100644 index 00000000..d25f8d7d --- /dev/null +++ b/python/lists.ipynb @@ -0,0 +1,1773 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Lists and looping\n", + "\n", + "Now things start to get _really_ interesting! Lists are collections of things \n", + "stored in a specific order. They can be defined literally by wrapping things in \n", + "square brackets `[]`, separating items with commas `,`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a = [4, 2, 9, 3]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Python lists can contain collections of whatever you like." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "excellent = [41, 'Hello', math.sin]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Each item in the list can be accessed by its _index_, its position in the list, \n", + "which starts at zero for the first item. Indexing by negative numbers starts \n", + "from the _last_ item of the list." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We’ll get an error if we try to access an index that doesn’t exist:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "a[99]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Like strings, lists have a length which can be found with the `len` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "len(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Unlike strings, lists are _mutable_, which means we can modify lists in-place, \n", + "without creating a new one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a.append(45)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "len(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We can see that lists are mutable because using the `append` method didn’t \n", + "print anything, and our variable `a` now has a different value.\n", + "\n", + "Because lists are mutable, we can use the special `del` keyword to remove \n", + "specific indices from the list." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "del a[-2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "*Good to know*\n", + "`del` is a language keyword representing an action, and not a function. The\n", + "syntactic difference is that functions take their arguments between parentheses,\n", + "such as `my_function(1, 2, 3)`, whereas `del` does not." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "You can retrieve sub-lists by using _slice_ notation whilst indexing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[1:-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This retrieves the part of list `a` starting from index `1` until _just before_ \n", + "index `-1`. The indexing is ‘exclusive’ in that it excludes the item of the \n", + "last index. This is the convention of indexing in Python.\n", + "\n", + "You can omit a number in the first or second indexing position, and Python will \n", + "assume you mean the first element (index zero) and last element (index \n", + "`len(array)`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[:-2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[1:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[:]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Slicing returns a copy of the array, so modifying the return value doesn’t \n", + "affect the original array." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b = a[1:]\n", + "print(b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b[0] = 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We did something cool there by assigning a value to a specific index, `b[0] =\n", + "3`. The same trick works with slices." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b[:2] = [99, 2, 78]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This is equivalent of _replacing_ a certain range (`:2`, or items at position 0\n", + "and 1) of the list `b` with other items from another list. Note that in our\n", + "example we replace 2 elements with 3. The same syntax might be used for\n", + "inserting elements at an arbitrary position in the list. If we want to insert\n", + "the number 6 between the 2 and the 78 in the list above, we would use:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b[2:0] = [6]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "meaning _take out 0 elements from the list starting a position 2 and insert the\n", + "content of the list `[6]` in that position_." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Exercise**\n", + "\n", + "Slicing creates a copy, so what notation could you use to copy the full list?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Solution**\n", + "You need to slice from the very beginning to the very end of the list." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[:]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This is equivalent to specifying the indices explicitly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a[0:len(a)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a == a[:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a is a" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a is a[:] # creates a copy!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Looping\n", + "\n", + "When you’ve got a collection of things, it’s pretty common to want to access \n", + "each one sequentially. This is called looping, or iterating, and is super easy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for item in a:\n", + " print(item)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We have to indent the code inside the `for` loop to tell Python that these\n", + "lines should be run for every iteration." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "### Indentation\n", + "\n", + "The `for` loop is a block, and every Python block requires indentation, unlike\n", + "other \"free-form\" languages such as C++ or Java. This means that Python will\n", + "throw an error if you don't indent:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "for i in b:\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Indentation must be consistent within the same block, so if you indent two lines\n", + "in the same `for` loop using a different number of spaces, Python will complain\n", + "once again:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "for i in b:\n", + " print(\"I am in a loop\")\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Indentation is necessary as Python does not use any keyword or symbol to\n", + "determine the end of a block (_e.g._ there is no `endfor`). As a side effect,\n", + "indentation forces you to make your code more readable!\n", + "\n", + "Note that it does not matter how many spaces you use for indentation. **As a\n", + "convention, we are using four spaces.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The variable name `item` can be whatever we want, but its value is changed by \n", + "Python to be the element of the item we’re currently on, starting from the \n", + "first.\n", + "\n", + "Because lists are mutable, we can try to modify the length of the list whilst \n", + "we’re iterating." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_copy = a[:]\n", + "for item in a_copy:\n", + " del a_copy[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Intuitively, you might expect `a_copy` to be empty, but it’s not! The technical\n", + "reasons aren’t important, but this highlights an important rule: **never\n", + "modify the length of a list whilst iterating over it!** You won’t end up with\n", + "what you expect.\n", + "\n", + "You can, however, freely modify the _values_ of each item in the list whilst \n", + "looping. This is a very common use case." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_copy = a[:]\n", + "i = 0\n", + "for item in a_copy:\n", + " a_copy[i] = 2*item\n", + " i += 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Keeping track of the current index ourselves, with `i` is annoying, but luckily \n", + "Python gives us a nicer way of doing that." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_doubled = a[:]\n", + "for index, item in enumerate(a_doubled):\n", + " a_doubled[index] = 2 * item" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_doubled" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "There’s a lot going on here. Firstly, note that Python lets you assign values \n", + "to multiple variables at the same time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "one, two = [34, 43]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(f\"one: {one}, two: {two}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "That’s already pretty cool! But then think about what happens if you had a list \n", + "where each item was another list, each containing two numbers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "nested = [[20, 29], [30, 34]]\n", + "for item in nested:\n", + " print(item)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "So, we can just assign each item in the sublist to separate variables in the \n", + "`for` statement." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for one, two in nested:\n", + " print(two, one)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Now we can understand a little better what `enumerate` does: for each item in \n", + "the list, it returns a new list containing the current index and the item." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "enumerate(a)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "list(enumerate(a))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "For more advanced reasons `enumerate` doesn’t return a list directly, but instead\n", + "something that the `for` statement knows how to iterate over (this is called a\n", + "[generator](https://wiki.python.org/moin/Generators) and for the moment you\n", + "don't need to know how it works). We can convert it to a list with the `list`\n", + "method when we want to see what’s it doing.\n", + "\n", + "This technique of looping over lists of lists lets us loop over two lists \n", + "simultaneously, using the `zip` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for item, item2 in zip(a, a_doubled):\n", + " print(item2, item)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Neat! As before, we can see what `zip` is doing explicitly by using `list`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "list(zip(a, a_doubled))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "You can see that the structure of the list that’s iterated over, the output of \n", + "`zip`, is identical to that for `enumerate`.\n", + "\n", + "Finally, we’ll take a quick look at the `range` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for i in range(0, 10):\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The arguments to `range` work just like slicing, the second argument is treated \n", + "exclusively, as its value is excluded from the output. Again like slicing, we \n", + "can specify a third argument as the step size for the iteration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for i in range(0, 10, 2):\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "If you only give a single argument to `range`, it assumes you’ve given the end \n", + "value, and want a starting value of zero." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for i in range(5):\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This reads “give me a list of length 5, in steps of 1, starting from zero”.\n", + "\n", + "Now that we know how to easily generate sequences of numbers, we can write \n", + "`enumerate` by hand!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "for index, item in zip(range(len(a)), a):\n", + " print(index, item)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Just like before! When you see something cool like `enumerate`, it can be fun \n", + "trying to see how you’d accomplish something similar with different building \n", + "blocks." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## List comprehension (Sugar, can be skipped on first read)\n", + "\n", + "We’ve already made a new list from an existing one when we created `a_doubled`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_doubled = a[:]\n", + "for index, item in enumerate(a_doubled):\n", + " a_doubled[index] = 2*item" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Creating a new list from an existing one is a common operation, so Python has a \n", + "shorthand syntax called _list comprehension_." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_doubled = [2*item for item in a]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a_doubled" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Isn’t that beautiful?\n", + "\n", + "We can use the same multi-variable stuff we learnt whilst looping." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "[index*item for index, item in enumerate(a)]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We’re not restricted to creating new lists with the same structure as the \n", + "original." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "[[item, item*item] for item in a]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We can even filter out items from the original list using `if`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "[[item, item*item] for item in a if item % 2 == 0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "List comprehensions are a powerful way of succinctly creating new lists. But be \n", + "responsible; if you find you’re doing something complicated, it’s probably \n", + "better to write a full `for` loop." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Exercise**\n", + "Write a list comprehension yourself\n", + "\n", + "Compute the square of the magnitude of the sum of the following two \n", + "three-vectors, using a single list comprehension and the global `sum` method.\n", + "\n", + "It might help to first think about how you’d compute the quantity for a single \n", + "vector.\n", + "\n", + "Not sure what the `sum` method does? Ask for `help`!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "help(sum)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "kaon = [3.4, 4.3, 20.0]\n", + "pion = [1.4, 0.9, 19.8]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Solution**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The square magnitude is the sum of the squares of the components, where the \n", + "components are the sum of the two input vectors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "magsq = sum([(k + pi)**2 for k, pi in zip(kaon, pion)])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The square root of this is around 40.42." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Tuples\n", + "\n", + "A close relative of lists are tuples, which differ in that they cannot be \n", + "mutated after creation. You can create tuples literally using parentheses, or \n", + "convert things to tuples using the `tuple` method.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a = (3, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "del a[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "a.append(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Tuples are usually used to describe data whose length is meaningful in and of \n", + "itself. For example, you could express coordinates as a tuple." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "coords = (3.2, 0.1)\n", + "x, y = coords" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This is nice because it doesn’t make sense to append to an `$ (x, y) $` \n", + "coordinate, nor to ‘delete’ a dimension. Generally, it can be useful if the \n", + "data structure you’re using respects the _meaning_ of the data you’re storing.\n", + "\n", + "If you can’t think of a use for tuples yourself, its worth keeping in mind that \n", + "Python creates tuples for groups of things by default. We saw that \n", + "earlier when we used `enumerate`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "list(enumerate([4, 9]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each element of the list is a tuple." + ] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython" + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/lists.md b/python/lists.md deleted file mode 100644 index df6ee45f..00000000 --- a/python/lists.md +++ /dev/null @@ -1,520 +0,0 @@ -# Lists and looping - -Now things start to get _really_ interesting! Lists are collections of things -stored in a specific order. They can be defined literally by wrapping things in -square brackets `[]`, separating items with commas `,`. - -```python ->>> a = [4, 2, 9, 3] ->>> a -[4, 2, 9, 3] -``` - -Python lists can contain collections of whatever you like. - -``` ->>> excellent = [41, 'Hello', math.sin] -``` - -Each item in the list can be accessed by its _index_, its position in the list, -which starts at zero for the first item. Indexing by negative numbers starts -from the _last_ item of the list. - -```python ->>> a[0] -4 ->>> a[2] -9 ->>> a[-1] -3 -``` - -We’ll get an error if we try to access an index that doesn’t exist: - -```python ->>> a[99] -Traceback (most recent call last): - File "", line 1, in -IndexError: list index out of range -``` - -Like strings, lists have a length which can be found with the `len` method. - -```python ->>> len(a) -4 -``` - -Unlike strings, lists are _mutable_, which means we can modify lists in-place, -without creating a new one. - -```python ->>> a.append(45) ->>> a -[4, 2, 9, 3, 45] ->>> len(a) -5 -``` - -We can see that lists are mutable because using the `append` method didn’t -print anything, and our variable `a` now has a different value. - -Because lists are mutable, we can use the special `del` keyword to remove -specific indices from the list. - -```python ->>> del a[-2] ->>> a -[4, 2, 9, 45] -``` - -{% callout "Functions and keywords" %} - -`del` is a language keyword representing an action, and not a function. The -syntactic difference is that functions take their arguments between parentheses, -such as `my_function(1, 2, 3)`, whereas `del` does not. - -{% endcallout %} - -You can retrieve sub-lists by using _slice_ notation whilst indexing. - -```python ->>> a[1:-1] -[2, 9] -``` - -This retrieves the part of list `a` starting from index `1` until _just before_ -index `-1`. The indexing is ‘exclusive’ in that it excludes the item of the -last index. This is the convention of indexing in Python. - -You can omit a number in the first or second indexing position, and Python will -assume you mean the first element (index zero) and last element (index -`len(array)`). - -```python ->>> a[:-2] -[4, 2 ->>> a[1:] -[2, 9, 45] ->>> a[:] -[4, 2, 9, 45] -``` - -Slicing returns a copy of the array, so modifying the return value doesn’t -affect the original array. - -```python ->>> b = a[1:] ->>> b -[2, 9, 45] ->>> b[0] = 3 ->>> b -[3, 9, 45] ->>> a -[4, 2, 9, 45] -``` - -We did something cool there by assigning a value to a specific index, `b[0] = -3`. The same trick works with slices. - -```python ->>> b[:2] = [99, 2, 78] ->>> b -[99, 2, 78, 45] -``` - -This is equivalent of _replacing_ a certain range (`:2`, or items at position 0 -and 1) of the list `b` with other items from another list. Note that in our -example we replace 2 elements with 3. The same syntax might be used for -inserting elements at an arbitrary position in the list. If we want to insert -the number 6 between the 2 and the 78 in the list above, we would use: - -```python ->>> b[2:0] = [6] ->>> b -[99, 2, 6, 78, 45] -``` - -meaning _take out 0 elements from the list starting a position 2 and insert the -content of the list `[6]` in that position_. - -{% challenge "Copying a list" %} - -Slicing creates a copy, so what notation could you use to copy the full list? - -{% solution "Solution" %} - -You need to slice from the very beginning to the very end of the list. -```python ->>> a[:] -[4, 2, 9, 45] -``` -This is equivalent to specifying the indices explicitly. -```python ->>> a[0:len(a)] -[4, 2, 9, 45] -``` - -{% endsolution %} - -{% endchallenge %} - -## Looping - -When you’ve got a collection of things, it’s pretty common to want to access -each one sequentially. This is called looping, or iterating, and is super easy. - -```python ->>> for item in a: -... print item -... -4 -2 -9 -45 -``` - -We have to indent the code inside the `for` loop to tell Python that these -lines should be run for every iteration. - -{% callout "Indentation in Python" %} - -The `for` loop is a block, and every Python block requires indentation, unlike -other "free-form" languages such as C++ or Java. This means that Python will -throw an error if you don't indent: - -```python ->>> for i in b: -... print(i) - File "", line 2 - print(i) - ^ -IndentationError: expected an indented block -``` - -Indentation must be consistent within the same block, so if you indent two lines -in the same `for` loop using a different number of spaces, Python will complain -once again: - -```python ->>> for i in b: -... print("I am in a loop") -... print(i) - File "", line 3 - print(i) - ^ -IndentationError: unexpected indent -``` - -Indentation is necessary as Python does not use any keyword or symbol to -determine the end of a block (_e.g._ there is no `endfor`). As a side effect, -indentation forces you to make your code more readable! - -Note that it does not matter how many spaces you use for indentation. **As a -convention, we are using four spaces.** - -{% endcallout %} - -The variable name `item` can be whatever we want, but its value is changed by -Python to be the element of the item we’re currently on, starting from the -first. - -Because lists are mutable, we can try to modify the length of the list whilst -we’re iterating. - -```python ->>> a_copy = a[:] ->>> for item in a_copy: -... del a_copy[0] -... ->>> a_copy -[9, 45] -``` - -Intuitively, you might expect `a_copy` to be empty, but it’s not! The technical -reasons aren’t important, but this highlights an important rule: **never -modify the length of a list whilst iterating over it!** You won’t end up with -what you expect. - -You can, however, freely modify the _values_ of each item in the list whilst -looping. This is a very common use case. - -```python ->>> a_copy = a[:] ->>> i = 0 ->>> for item in a_copy: -... a_copy[i] = 2*item -... i += 1 -... ->>> a_copy -[8, 4, 18, 90] -``` - -Keeping track of the current index ourselves, with `i` is annoying, but luckily -Python gives us a nicer way of doing that. - -```python ->>> a_doubled = a[:] ->>> for index, item in enumerate(a_doubled): -... a_doubled[index] = 2*item -... ->>> a_doubled -[8, 4, 18, 90] -``` - -There’s a lot going on here. Firstly, note that Python lets you assign values -to multiple variables at the same time. - -```python ->>> one, two = [34, 43] ->>> print(two, one) -43 34 -``` - -That’s already pretty cool! But then think about what happens if you had a list -where each item was another list, each containing two numbers. - -```python ->>> nested = [[20, 29], [30, 34]] ->>> for item in nested: -... print(item) -... -[20, 29] -[30, 34] -``` - -So, we can just assign each item in the sublist to separate variables in the -`for` statement. - -```python ->>> for one, two in nested: -... print(two, one) -... -29 20 -34 30 -``` - -Now we can understand a little better what `enumerate` does: for each item in -the list, it returns a new list containing the current index and the item. - -```python ->>> enumerate(a) - ->>> list(enumerate(a)) -[(0, 4), (1, 2), (2, 9), (3, 45)] -``` - -For performance reasons `enumerate` doesn’t return a list directly, but instead -something that the `for` statement knows how to iterate over (this is called a -[generator](https://wiki.python.org/moin/Generators) and for the moment you -don't need to know how it works). We can convert it to a list with the `list` -method when we want to see what’s it doing. - -This technique of looping over lists of lists lets us loop over two lists -simultaneously, using the `zip` method. - -```python ->>> for item, item2 in zip(a, a_doubled): -... print(item2, item) -... -8 4 -4 2 -18 9 -90 45 -``` - -Neat! As before, we can see what `zip` is doing explicitly by using `list`. - -```python ->>> list(zip(a, a_doubled)) -[(4, 8), (2, 4), (9, 18), (45, 90)] -``` - -You can see that the structure of the list that’s iterated over, the output of -`zip`, is identical to that for `enumerate`. - -Finally, we’ll take a quick look at the `range` method. - -```python ->>> for i in range(0, 10): - print(i) -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -``` - -The arguments to `range` work just like slicing, the second argument is treated -exclusively, as its value is excluded from the output. Again like slicing, we -can specify a third argument as the step size for the iteration. - -```python ->>> for i in range(0, 10, 2): - print(i) -0 -2 -4 -6 -8 -``` - -If you only give a single argument to `range`, it assumes you’ve given the end -value, and want a starting value of zero. - -```python ->>> for i in range(5): - print(i) -0 -1 -2 -3 -4 -``` - -This reads “give me a list of length 5, in steps of 1, starting from zero”. - -Now that we know how to easily generate sequences of numbers, we can write -`enumerate` by hand! - -```python ->>> for index, item in zip(range(len(a)), a): -... print(index, item) -... -0 4 -1 2 -2 9 -3 45 -``` - -Just like before! When you see something cool like `enumerate`, it can be fun -trying to see how you’d accomplish something similar with different building -blocks. - -## List comprehension - -We’ve already made a new list from an existing one when we created `a_doubled`. - -```python ->>> a_doubled = a[:] ->>> for index, item in enumerate(a_doubled): -... a_doubled[index] = 2*item -``` - -Creating a new list from an existing one is a common operation, so Python has a -shorthand syntax called _list comprehension_. - -```python ->>> a_doubled = [2*item for item in a] ->>> a_doubled -[8, 4, 18, 90] -``` - -Isn’t that beautiful? - -We can use the same multi-variable stuff we learnt whilst looping. - -```python ->>> [index*item for index, item in enumerate(a)] -[0, 2, 18, 135] -``` - -We’re not restricted to creating new lists with the same structure as the -original. - -```python ->>> [[item, item*item] for item in a] -[[4, 16], [2, 4], [9, 81], [45, 2025]] -``` - -We can even filter out items from the original list using `if`. - -```python ->>> [[item, item*item] for item in a if item % 2 == 0] -[[4, 16], [2, 4]] -``` - -List comprehensions are a powerful way of succinctly creating new lists. But be -responsible; if you find you’re doing something complicated, it’s probably -better to write a full `for` loop. - -## Tuples - -A close relative of lists are tuples, which differ in that they cannot be -mutated after creation. You can create tuples literally using parentheses, or -convert things to tuples using the `tuple` method. - -```python ->>> a = (3, 4) ->>> del a[0] -Traceback (most recent call last): - File "", line 1, in -TypeError: 'tuple' object doesn't support item deletion ->>> a.append(5) -Traceback (most recent call last): - File "", line 1, in -AttributeError: 'tuple' object has no attribute 'append' ->>> a[0] = 1 -Traceback (most recent call last): - File "", line 1, in -TypeError: 'tuple' object does not support item assignment ->>> b = tuple([65, 'yes']) ->>> b -(65, 'yes') -``` - -Tuples are usually used to describe data whose length is meaningful in and of -itself. For example, you could express coordinates as a tuple. - -```python ->>> coords = (3.2, 0.1) ->>> x, y = coords -``` - -This is nice because it doesn’t make sense to append to an `$ (x, y) $` -coordinate, nor to ‘delete’ a dimension. Generally, it can be useful if the -data structure you’re using respects the _meaning_ of the data you’re storing. - -If you can’t think of a use for tuples yourself, its worth keeping in mind that -Python creates tuples for groups of things by default. We saw that -earlier when we used `enumerate`. - -```python ->>> list(enumerate([4, 9])) -[(0, 4), (1, 9)] -``` - -Each element of the list is a tuple. - -{% challenge "Write a list comprehension yourself" %} - -Compute the square of the magnitude of the sum of the following two -three-vectors, using a single list comprehension and the global `sum` method. -```python ->>> kaon = [3.4, 4.3, 20.0] ->>> pion = [1.4, 0.9, 19.8] -``` -It might help to first think about how you’d compute the quantity for a single -vector. - -{% solution "Solution" %} - -Not sure what the `sum` method does? Ask for `help`! -```python ->>> help(sum) -``` -The square magnitude is the sum of the squares of the components, where the -components are the sum of the two input vectors. -```python ->>> magsq = sum([(k + pi)**2 for k, pi in zip(kaon, pion)]) -``` -The square root of this is around 40.42. - -{% endsolution %} - -{% endchallenge %} diff --git a/python/methods.ipynb b/python/methods.ipynb new file mode 100644 index 00000000..c27225fc --- /dev/null +++ b/python/methods.ipynb @@ -0,0 +1,1675 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Functions\n", + "Functions, or methods if they are associated with a class, take some input and return some output. \n", + "\n", + "They are the equivalent of a mathematical function $f(x) = y$, where the function `f` takes zero or more aruments and returns zero or more values. The best way to think about a function is that it is executed and _replaced_ by the return value, like a mathematical function. `f` is a function, `x` are values. `f(x)` is not a function anymore and isn't `x` either, it's whatever `y` is, the return value. Like mathematical functions, you could \"copy-paste\" the code-block of the function in the place (technically).\n", + "\n", + "We have already used lots of *functions*, like `len`, `abs` and `print` as well as *methods* like `append` from `list`, `get` from `dict` or `replace` from `str`. In this lesson we will start creating our own.\n", + "\n", + "As we have seen, methods can do a lot of stuff with very little typing. Methods are normally used to encapsulate small pieces of code that we want to reuse.\n", + "\n", + "Let’s rewrite len as an example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def length(obj):\n", + " \"\"\"Return the number of elements in `obj`.\n", + " Args\n", + " ----\n", + " obj (iterable): Object the length will be calculated from.\n", + " Return\n", + " ------\n", + " int: number of elements in `obj`.\n", + " \"\"\"\n", + " i = 0\n", + " for _ in obj:\n", + " i += 1\n", + " return i" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "length" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "help(length)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "or viewing the docs view in your preferred editor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "length('A b c!')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "length(range(5))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "There’s a lot going on here, so we will break it down line-by-line.\n", + "\n", + "1. `def length(obj)`: methods are _defined_ using `def`, followed by a space,\n", + " and then the name you want to give the method. Inside the parentheses\n", + " after the name, we list the inputs, or _arguments_, that we want our method\n", + " to accept. In this case, we only need a single input: the thing we want to\n", + " compute the length of. Finally, there’s a colon at the end, just like with\n", + " a `for` or `if`, which means a _block_ of code follows (which must be\n", + " indented).\n", + " Names are conventionally in lowercase, with underscores separating words - snakecase.\n", + "3. `\"\"\"Return the number of elements in obj.\"\"\"`: This is the _docstring_. It’s\n", + " just a documentation string, defined literally with three double quotes so that we can\n", + " include linebreaks. By placing a string here, Python makes the string\n", + " available to use when we pass our function to `help` and in a lot of other places\n", + " like docs viewer of a decent editor or even allows to automatically generate \n", + " documents including HTML with the docs. Documenting your\n", + " functions is a very good idea! It makes it clear to others, and to\n", + " future-you, what the method is supposed to do.\n", + " The formatting of docstrings is standardized (there are 2-3 different ones).\n", + " As for code style, do not invent your own but make it easier for everyone\n", + " (including your future self).\n", + "4. The method block. This is the code that will run whenever you _call_ your\n", + " method, like `length([1])`. The code in the block has access to the\n", + " arguments and to any variables defined _before_ the method definition." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Remark: there are comments (with `#`) and docstrings. Both serve a very different purpose\n", + "\n", + " - comments `#` are for people who _read_ the code. Other developers that don't want to just\n", + " use your function but _change_ it. They can be short and serve the purpose to make the\n", + " code more readable. Typical example: adding a comment on a `- 1` or `+ 1` added somewhere,\n", + " such as ` len(x) - 1 # we don't need the border`. If a block of code implements a hard\n", + " to read algorithm, it is also appropriate to use several `#` lines to explain beforehand\n", + " what is going to happen.\n", + " _Never_ use tripple quotes `\"\"\"` to make a large comment! Use always `#`, any decent\n", + " editor is able to (un)comment several lines at once. (usually ctrl + /)\n", + " - Docstrings are for users. If someone imports your function, the docstrings tells\n", + " _how to use it_ and what it does exactly. It does, however, not contain any (unnecessary)\n", + " information about the implementation. It's for someone who will _not_ read the source code.\n", + " Example are functions that we used, like `len`: we never looked at the source code, but the\n", + " `help(len)` gave us all the information that we needed to *use* it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "x = 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def top_function():\n", + " \"\"\"Do something silly.\"\"\"\n", + " print(x)\n", + " print(y)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "top_function()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "y = 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "top_function()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "In general, you should try to minimise the number of variables outside your\n", + " method that you use inside. It makes figuring out what the method does much\n", + " harder, as you have to look elsewhere in the code to find things out.\n", + " \n", + "4. `return i`: This defines the _output_ of the method, the thing that you get\n", + " back when you call the method. You don’t have to return anything, in which\n", + " case Python will implicitly make your function return `None`, or you can\n", + " return multiple things at once." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def no_return():\n", + " 1 + 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "no_return()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "no_return() is None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def such_output():\n", + " return 'wow', 'much clever', 213 # equivalent to (return 'wow', 'much clever', 213)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "You can see that returning multiple things implicitly means returning a tuple, so we can choose to assign one variable per value while calling the method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "help(len)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def add(x, y):\n", + " \"\"\"Return the sum of x and y.\"\"\"\n", + " return x + y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "add(1, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "add(x=1, y=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "add(1, y=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "add(y=2, x=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "add(y=2, 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Specifying the argument’s name explicitly when calling a method is nice because\n", + "it reminds you what the argument is supposed to do. It also means you don’t\n", + "have to remember the order in which the arguments were defined, you can specify\n", + "_keyword arguments_ in any order. You can even mix _positional arguments_ with\n", + "keyword arguments, but any keyword arguments must come last.\n", + "The rule is simply: is it unambigious? You can do it. Otherwise, it's not allowed.\n", + "\n", + "Using keyword arguments is particularly useful for arguments which act as\n", + "on/off flags, because it’s often not obvious what your `True` or `False` is\n", + "doing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def add(x, y, show):\n", + " \"\"\"Return the sum of x and y.\n", + " Optionally print the result before returning it.\n", + " \"\"\"\n", + " if show:\n", + " print(x + y)\n", + " return x + y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "_ = add(1, 2, True) # Hmm, what is True doing again?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "_ = add(1, 2, show=True) # Aha! Much clearer" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "_remark on `_`: the character `_` is just a variable like any other. By convention, this is used in places where there is a return value but it signals, that it is **deliberately** ignored, as it won't be used. Contrary, just calling `add(...)` without the assignement is a \"code smell\": a possible bug, because why would someone call it and not use it's value?_" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Always having to specify that flag is annoying. It would be much nicer if\n", + "`show` had a _default value_, so that we don’t _have_ to provide a value when\n", + "calling the method, but can optionally override it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def add(x, y, show=False):\n", + " \"\"\"Return the sum of x and y.\n", + " Optionally print the result before returning it.\n", + " \"\"\"\n", + " if show:\n", + " print(x + y)\n", + " return x + y" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "_ = add(1, 2) # No printing!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "_ = add(1, 2, show=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Perfect.\n", + "\n", + "Of course, function arguments can be anything, even other functions!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def run_method(method, x):\n", + " \"\"\"Call `method` with `x`.\"\"\"\n", + " return method(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "run_method(len, [1, 2, 3])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Exercise**\n", + "Methods returning methods\n", + "\n", + "What does this method do? Think about it, what _exactly_ happens. Be precise, discuss with your neighbours." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def make_incrementor(increment):\n", + " def func(var):\n", + " return var + increment\n", + " return func" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Solution**\n", + "\n", + "It returns a function whose `increment` value has been filled by the argument\n", + "to `make_incrementor`. If we called `make_incrementor(3)`, then `increment` has\n", + "the value 3, and we can fill in the returned method in our heads." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def func(var):\n", + " return var + 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "So when we call _this_ method, we’ll get back what we put in, but plus 3." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "increment_one = make_incrementor(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "increment_two = make_incrementor(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(increment_one(42), increment_two(42))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(make_incrementor(3)(42)) # Do it in one go!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## *args and **kwargs\n", + "\n", + "This is a brief introduction, for a more detailed explanation on the packing and unpacking of arguments, [see here](https://hsf-training.github.io/analysis-essentials/advanced-python/11AdvancedPython.html#Packing-and-unpacking-of-values)\n", + "\n", + "What if you like to accept an arbitrary number of arguments? For example, we\n", + "can also write a `total` method that takes two arguments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def total(x, y):\n", + " \"\"\"Return the sum of the arguments.\"\"\"\n", + " return x + y" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "But what if we want to allow the caller to pass more than two arguments? It\n", + "would be tedious to define many arguments explicitly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def total(*args):\n", + " \"\"\"Return the sum of the arguments.\"\"\"\n", + " # For seeing what `*` does\n", + " print(f'Got {len(args)} arguments: {args}')\n", + " return sum(args)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "total(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "total(1, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "total(1, 2, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The `*args` syntax says “stuff any arguments into a tuple and call it `args`”.\n", + "This let’s us capture any number of arguments. As `args` is a tuple, one could\n", + "loop over it, access a specific element, and so on.\n", + "\n", + "*remark: `args`, like `_`, is just a name that by convention is used in this way, but has no special function*\n", + "\n", + "We can also _expand_ lists into separate arguments with the same syntax when\n", + "_calling_ a method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def reverse_args(x, y):\n", + " return y, x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "l = ['a', 'b']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "reverse_args(l)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "reverse_args(*l)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "A similar syntax exists for keyword arguments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def ages(**people):\n", + " \"\"\"Print people's information.\"\"\"\n", + " # For seeing what `**` does\n", + " print(f'Got {len(people)} arguments: {people}')\n", + " for person in people:\n", + " print(f'Person {person} is {people[person]}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "ages(steve=31)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "ages(steve=31, helen=70, zorblax=9963)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "As you can see from the debug print statement, `**people` is a dictionary\n", + "containing the keyword arguments we passed to the `ages` method. The keys of\n", + "the dictionary are the names of the argument as strings, and the values are the\n", + "values of the arguments. Just like for the `*` syntax, `**` can also be used to\n", + "expand a dictionary into keyword arguments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "data = {'thor': 5000, 'yoda': -1}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "ages(**data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The order of the keyword arguments used to call the method are not necessarily\n", + "the same as those that the function block sees!\n", + "This is because dictionaries are unordered, and the `**` syntax effectively creates a dictionary." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Exercise**\n", + "The most generic method\n", + "\n", + "The most generic method would take any number of positional arguments _and_ any\n", + "number of keyword arguments. What would this method look like?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Solution**\n", + "\n", + "It would use both `*` and `**` syntax in defining the arguments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def generic(*args, **kwargs):\n", + " print(f'Got args: {args}')\n", + " print(f'Got kwargs: {kwargs}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "data = {'bing': 'baz'}\n", + "generic(1, 2, 'abc', foo='bar', **data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Inline methods\n", + "\n", + "Some methods take other methods as arguments, like the built-in `map` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "map(str, range(5))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "`map` takes a function and an iterable, and applies the function to each element in\n", + "the iterable. It returs however an generator, an object that is, for advanced reasons, not actually evaluated yet. In most cases, you can treat this `list` or `tuple`-like.\n", + "\n", + "To make sure it is evaluated, we can explicitly convert it to a container, _i.e._ a list with the results. We can define and then pass our own functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "list(map(str, range(5)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def cube(x):\n", + " \"\"\"Return the third power of x.\"\"\"\n", + " return x*x*x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "list(map(cube, range(5)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "For such a simple method, this is a lot of typing! We can use a `lambda` function to\n", + "define such simple methods inline." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "list(map(lambda x: x*x*x, range(5)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The syntax of defining a `lambda` is like this:\n", + "```\n", + "lambda : \n", + "```\n", + "`` is a command-separate set of variables that the `lambda` can take as\n", + "arguments, and `` is the code that is run. A `lambda`\n", + "automatically returns whatever the result of the expression is, you don’t need\n", + "a `return` (the `return` is _implicit_).\n", + "\n", + "Writing a `lambda` statement defines a method, which you can capture as a\n", + "variable just like any other object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "div2 = lambda x: x / 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "div2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "list(map(div2, range(5)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Note however that _if we assing the function to a variable_, the general preferred way to do is using the normal function definition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def div2(x):\n", + " return x / 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Exercise**\n", + "Sum in quadrature\n", + "\n", + "Write a method that accepts an arbitrary number of arguments, and returns the\n", + "sum of the arguments computed in quadrature. A “sum in quadrature” is the\n", + "square root of the sum of the squares of each number. You should use `lambda`\n", + "to define a squaring and a square root function, and `map` to apply the\n", + "squaring method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Solution**\n", + "We need a little square root method and a method to square its input." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "square = lambda x: x*x\n", + "sqrt = lambda x: x**0.5" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We then define a method that can accept any number of arguments using the\n", + "`*args` syntax, and use `map` to call the `square` method on the list of\n", + "arguments. Then we can call `sum` on the result, and then `sqrt`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def quadrature(*args):\n", + " \"\"\"Return the sum in quadrature of the arguments.\"\"\"\n", + " return sqrt(sum(map(square, args)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "quadrature(1, 1) # should be equal to sqrt(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "2**0.5" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Another good use case for `lambda` (remember, we can just define the function, it's more of a \"nice-to-have\") is the built-in `filter` method (see:\n", + "`help(filter)`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "# filter and return the even numbers only\n", + "filter(lambda x: x % 2 == 0, range(10)) # returns again a generator" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "list(filter(lambda x: x % 2 == 0, range(10)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Exercise**\n", + "List comprehension\n", + "\n", + "How would you rewrite the `filter` example above using a list comprehension?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "**Solution**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "[ x for x in range(10) if x % 2 == 0 ]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Generally, you should only use `lambda` methods to define little throw-away\n", + "methods. The main downside with using them is that you can’t attach a docstring\n", + "to them, and they become unwieldy when there’s complex logic.\n", + "\n", + "Golden rules:\n", + " - Make functions idempotent where possible (stateless, the same input values will return the same output). This is of course different for classes.\n", + " - Don't use globals (if anyhow avoidable).\n", + " - Do not alter the input argument if they are mutable. If it's convenient, make a copy of the object first (remember copies of lists?)\n", + " - Put a docstring there. Probably even before you implement your function. This makes it\n", + " not only to everyone else but also to you clear what comes in and what comes out.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython" + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/methods.md b/python/methods.md deleted file mode 100644 index 3e88c4b7..00000000 --- a/python/methods.md +++ /dev/null @@ -1,476 +0,0 @@ -# Functions - -Functions, or methods if they are associated with a class, take some input and return some output. We have -already used lots of functions, like `len`, `abs` and `print` as well as methods like `append` from `list`, `get` from `dict` or `replace` from `str`. In this lesson -we will start creating our own. - -As we have seen, methods can do a lot of stuff with very little typing. Methods -are normally used to encapsulate small pieces of code that we want to reuse. - -Let’s rewrite `len` as an example. - -```python ->>> def length(obj): -... """Return the number of elements in `obj`. -... -... Args -... ---- -... obj (iterable): Object the length will be calculated from. -... -... Return -... ------ -... int: number of elements in `obj`. -... """ -... i = 0 -... for _ in obj: -... i += 1 -... return i ->>> length - ->>> help(length) -Help on function length in module __main__: - -length(obj) - Return the number of elements in `obj`. - - Args - obj (iterable): Object the length will be calculated from. - - Return - int: number of elements in `obj`. - -or viewing the docs view in your preferred editor. - ->>> length('A b c!') -6 ->>> length(range(5)) -5 -``` - -There’s a lot going on here, so we will break it down line-by-line. - -1. `def length(obj)`: methods are _defined_ using `def`, followed by a space, - and then the name you want to give the method.[^1] Inside the parentheses - after the name, we list the inputs, or _arguments_, that we want our method - to accept. In this case, we only need a single input: the thing we want to - compute the length of. Finally, there’s a colon at the end, just like with - a `for` or `if`, which means a _block_ of code follows (which must be - indented). -2. `"""Return the number of elements in obj."""`: This is the _docstring_. It’s - just a documentation string, defined literally with three double quotes so that we can - include linebreaks. By placing a string here, Python makes the string - available to use when we pass our function to `help` and in a lot of other places - like docs viewer of a decent editor or even allows to automatically generate - documents including HTML with the docs. Documenting your - functions is a very good idea! It makes it clear to others, and to - future-you, what the method is supposed to do. -3. The method block. This is the code that will run whenever you _call_ your - method, like `length([1])`. The code in the block has access to the - arguments and to any variables defined _before_ the method definition. -```python ->>> x = 1 ->>> def top_function(): -... """Do something silly.""" -... print(x) -... print(y) -... ->>> y = 2 ->>> top_function() -1 -Traceback (most recent call last): - File "", line 1, in - File "", line 3, in top_function -NameError: global name 'y' is not defined -``` - In general, you should try to minimise the number of variables outside your - method that you use inside. It makes figuring out what the method does much - harder, as you have to look elsewhere in the code to find things out. -4. `return i`: This defines the _output_ of the method, the thing that you get - back when you call the method. You don’t have to return anything, in which - case Python will implicitly make your function return `None`, or you can - return multiple things at once. -```python ->>> def no_return(): -... 1 + 1 -... ->>> no_return() ->>> no_return() == None ->>> def such_output(): -... return 'wow', 'much clever', 213 -... ->>> such_output() -('wow', 'much clever', 213) ->>> a, b, c = such_output() ->>> b -'much clever' -``` - You can see that returning multiple things implicitly means returning a - tuple, so we can choose to assign one variable per value while calling the - method. - -[^1]: Names are conventionally in lowercase, with underscores separating words. - -Remark: there are comments (with `#`) and docstrings. Both serve a very different purpose - - - comments `#` are for people who _read_ the code. Other developers that don't want to just - use your function but _change_ it. They can be short and serve the purpose to make the - code more readable. Typical example: adding a comment on a `- 1` or `+ 1` added somewhere, - such as ` len(x) - 1 # we don't need the border`. If a block of code implements a hard - to read algorithm, it is also appropriate to use several `#` lines to explain beforehand - what is going to happen. - _Never_ use tripple quotes `"""` to make a large comment! Use always `#`, any decent - editor is able to (un)comment several lines at once. - - Docstrings are for users. If someone imports your function, the docstrings tells - _how to use it_ and what it does exactly. It does, however, not contain any (unnecessary) - information about the implementation. - - -Functions can be called in several ways. - -```python ->>> def add(x, y): -... """Return the sum of x and y.""" -... return x + y -... ->>> add(1, 2) ->>> add(x=1, y=2) ->>> add(1, y=2) ->>> add(y=2, x=1) ->>> add(y=2, 1) - File "", line 1 -SyntaxError: non-keyword arg after keyword arg ->>> add(y=2, =1) - File "", line 1 - add(y=2, =1) - ^ -SyntaxError: invalid syntax -``` - -Specifying the argument’s name explicitly when calling a method is nice because -it reminds you what the argument is supposed to do. It also means you don’t -have to remember the order in which the arguments were defined, you can specify -_keyword arguments_ in any order. You can even mix _positional arguments_ with -keyword arguments, but any keyword arguments must come last. - -Using keyword arguments is particularly useful for arguments which act as -on/off flags, because it’s often not obvious what your `True` or `False` is -doing. - -```python ->>> def add(x, y, show): -... """Return the sum of x and y. -... -... Optionally print the result before returning it. -... """ -... if show: -... print(x + y) -... return x + y -... ->>> _ = add(1, 2, True) # Hmm, what is True doing again? -3 ->>> _ = add(1, 2, show=True) # Aha! Much clearer -``` - -Always having to specify that flag is annoying. It would be much nicer if -`show` had a _default value_, so that we don’t _have_ to provide a value when -calling the method, but can optionally override it. - -```python ->>> def add(x, y, show=False): -... """Return the sum of x and y. -... -... Optionally print the result before returning it. -... """ -... if show: -... print(x + y) -... return x + y -... ->>> _ = add(1, 2) # No printing! ->>> _ = add(1, 2, show=True) -3 -``` - -Perfect. - -Of course, function arguments can be anything, even other functions! - -```python ->>> def run_method(method, x): -... """Call `method` with `x`.""" -... return method(x) -... ->>> run_method(len, [1, 2, 3]) -3 -``` - -{% challenge "Methods returning methods" %} - -What does this method do? - -```python ->>> def make_incrementor(increment): -... def func(var): -... return var + increment -... return func -``` - -{% solution "Solution" %} - -It returns a function whose `increment` value has been filled by the argument -to `make_incrementor`. If we called `make_incrementor(3)`, then `increment` has -the value 3, and we can fill in the returned method in our heads. - -```python -def func(var): - return var + 3 -``` - -So when we call _this_ method, we’ll get back what we put in, but plus 3. - -```python ->>> increment_one = make_incrementator(1) ->>> increment_two = make_incrementator(2) ->>> print increment_one(42), increment_two(42) -43 44 ->>> print make_incrementator(3)(42) # Do it in one go! -45 -``` - -{% endsolution %} - -{% endchallenge %} - -What if you like to accept an arbitrary number of arguments? For example, we -can also write a `total` method that takes two arguments. - -```python ->>> def total(x, y): -... """Return the sum of the arguments.""" -... return x + y -... ->>> -``` - -But what if we want to allow the caller to pass more than two arguments? It -would be tedious to define many arguments explicitly. - -```python ->>> def total(*args): -... """Return the sum of the arguments.""" -... # For seeing what `*` does -... print('Got {0} arguments: {1}'.format(len(args), args)) -... return sum(args) -... ->>> total(1) -Got 1 arguments: (1,) -1 ->>> total(1, 2) -Got 2 arguments: (1, 2) -3 ->>> total(1, 2, 3) -Got 3 arguments: (1, 2, 3) -6 -``` - -The `*args` syntax says “stuff any arguments into a tuple and call it `args`”. -This let’s us capture any number of arguments. As `args` is a tuple, one could -loop over it, access a specific element, and so on. - -We can also _expand_ lists into separate arguments with the same syntax when -_calling_ a method. - -```python ->>> def reverse_args(x, y): -... return y, x -... ->>> l = ['a', 'b'] ->>> reverse_args(l) -Traceback (most recent call last): - File "", line 1, in -TypeError: reverse_args() takes exactly 2 arguments (1 given) ->>> reverse_args(*l) -('b', 'a') -``` - -A similar syntax exists for keyword arguments. - -```python ->>> def ages(**people): -... """Print people's information.""" -... # For seeing what `**` does -... print('Got {0} arguments: {1}'.format(len(people), people)) -... for person in people: -... print('Person {0} is {1}'.format(person, people[person])) -... ->>> ages(steve=31) -Got 1 arguments: {'steve': 31} -Person steve is 31 ->>> ages(steve=31, helen=70, zorblax=9963) -Got 3 arguments: {'steve': 31, 'zorblax': 9963, 'helen': 70} -Person steve is 31 -Person zorblax is 9963 -Person helen is 70 -``` - -As you can see from the debug print statement, `**people` is a dictionary -containing the keyword arguments we passed to the `ages` method. The keys of -the dictionary are the names of the argument as strings, and the values are the -values of the arguments. Just like for the `*` syntax, `**` can also be used to -expand a dictionary into keyword arguments. - -```python ->>> d = {'thor': 5000, 'yoda': -1} ->>> ages(**d) -Got 2 arguments: {'yoda': -1, 'thor': 5000} -Person yoda is -1 -Person thor is 5000 -``` - -The order of the keyword arguments used to call the method are not necessarily -the same as those that the function block sees! -This is because dictionaries are unordered, and the `**` syntax effectively creates a dictionary. - - -{% challenge "The most generic method" %} - -The most generic method would take any number of positional arguments _and_ any -number of keyword arguments. What would this method look like? - -{% solution "Solution" %} - -It would use both `*` and `**` syntax in defining the arguments. -```python ->>> def generic(*args, **kwargs): -... print('Got args: {0}'.format(args)) -... print('Got kwargs: {0}'.format(kwargs)) -... ->>> d = {'bing': 'baz'} ->>> generic(1, 2, 'abc', foo='bar', **d) -Got args: (1, 2, 'abc') -Got kwargs: {'bing': 'baz', 'foo': 'bar'} -``` - -{% endsolution %} - -{% endchallenge %} - -## Inline methods - -Some methods take other methods as arguments, like the built-in `map` method. - -```python ->>> map(str, range(5)) -['0', '1', '2', '3', '4'] -``` - -`map` takes a function and an iterable, and applies the function to each element in -the iterable. It returns a new list with the results. We can define and then pass -our own functions. - -```python ->>> def cube(x): -... """Return the third power of x.""" -... return x*x*x -... ->>> map(cube, range(5)) -[0, 1, 8, 27, 64] -``` - -For such a simple method, this is a lot of typing! We can use a `lambda` function to -define such simple methods inline. - -```python ->>> map(lambda x: x*x*x, range(5)) -[0, 1, 8, 27, 64] -``` - -The syntax of defining a `lambda` is like this: - -``` -lambda : -``` - -`` is a command-separate set of variables that the `lambda` can take as -arguments, and `` is the code that is run. A `lambda` -automatically returns whatever the result of the expression is, you don’t need -a `return` (the `return` is _implicit_). - -Writing a `lambda` statement defines a method, which you can capture as a -variable just like any other object. - -```python ->>> div2 = lambda x: x/2 ->>> div2 - at 0x7fc6b2207758> ->>> map(div2, range(5)) -[0.0, 0.5, 1.0, 1.5, 2.0] -``` - -Note that we got real numbers back because we are using Python 2 with `from __future__ import division`. - -{% challenge "Sum in quadrature" %} - -Write a method that accepts an arbitrary number of arguments, and returns the -sum of the arguments computed in quadrature. A “sum in quadrature” is the -square root of the sum of the squares of each number. You should use `lambda` -to define a squaring and a square root function, and `map` to apply the -squaring method. - -{% solution "Solution" %} - -We need a little square root method and a method to square its input. -```python ->>> square = lambda x: x*x ->>> sqrt = lambda x: x**0.5 -``` -We then define a method that can accept any number of arguments using the -`*args` syntax, and use `map` to call the `square` method on the list of -arguments. Then we can call `sum` on the result, and then `sqrt`. -```python ->>> def quadrature(*args): -... """Return the sum in quadrature of the arguments.""" -... return sqrt(sum(map(square, args))) -... ->>> quadrature(1, 1) # should be equal to sqrt(2) -1.4142135623730951 ->>> 2**0.5 -1.4142135623730951 -``` - -{% endsolution %} - -{% endchallenge %} - -Another use case for `lambda` is the built-in `filter` method (see: -`help(filter)`). - -```python ->>> filter(lambda x: x % 2 == 0, range(10)) # filter and return the even numbers only -[0, 2, 4, 6, 8] -``` - -{% challenge "List comprehension" %} - -How would you rewrite the `filter` example above using a list comprehension? - -{% solution "Solution" %} - -```python ->>> [ x for x in range(10) if x % 2 == 0 ] -[0, 2, 4, 6, 8] -``` - -{% endsolution %} - -{% endchallenge %} - -Generally, you should only use `lambda` methods to define little throw-away -methods. The main downside with using them is that you can’t attach a docstring -to them, and they become unwieldy when there’s complex logic. - -Golden rules: - - Make functions idempotent where possible (stateless, the same input values will return the same output). This is of course different for classes. - - Don't use globals (if anyhow avoidable). - - Do not alter the input argument if they are mutable. - - Put a docstring there. Probably even before you implement your function. This makes it - not only to everyone else but also to you clear what comes in and what comes out. diff --git a/python/modules.ipynb b/python/modules.ipynb new file mode 100644 index 00000000..bac0c7c6 --- /dev/null +++ b/python/modules.ipynb @@ -0,0 +1,894 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Modules\n", + "\n", + "Python comes with lots of useful stuff, which is provided with modules\n", + "(and submodules, see later).\n", + "We have already met the maths module, but did not talk about how we started using it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "math.pi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "math.sin(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The path after `from` might look different on your computer.\n", + "\n", + "So, `math` is a _module_, and this seems to behave a lot like other objects we\n", + "have met: it is a container with properties and methods attached that we can\n", + "access with the dot operator `.`. Actually, that is pretty much all there is to\n", + "them." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Using modules into your code: import\n", + "\n", + "The keyword `import`, usually specified at the beginning of your source code, is\n", + "used to tell Python what modules you want to make available to your current\n", + "code.\n", + "\n", + "There are different ways of specifying an import. The one we have seen already\n", + "simply makes the module available to you:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "random.uniform(0, 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The module `random` contains functions useful for random number generation: with\n", + "the `import` above, we have made the `random` module accessible, and everything\n", + "within that module is accessible via the syntax `random.`. For the record,\n", + "the `uniform(x,y)` method returns a pseudo-random number within the range\n", + "`$ [x,y] $`.\n", + "\n", + "Sometimes you want to make only one or more things from a given module\n", + "accessible: Python gives you the ability to import just those:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from random import choice, uniform" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "uniform(0, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "choice([ 33, 56, 42, -1 ])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "\n", + "In this case the `uniform` and `choice` names are available _directly_, _i.e._\n", + "without using the `random` prefix. All other functions in the `random` module\n", + "are not available in this case. For the record, the `choice` function returns a\n", + "random element from a given collection.\n", + "\n", + "Another option is to import _all_ functions of a certain module and make them\n", + "available without a prefix, which you should *never* do (except in very special cases if you're writing a library)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "# from random import * # don't run this :) never" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This is not that recommended as you generally do not know what is the extent\n", + "of what you are importing and you might end up with name clashes between your\n", + "current code and the imported module, as it will all be in the same namespace,\n", + "meaning directly available with no need for a `.` syntax. \n", + "\n", + "Lastly, it is possible to import modules, or specific names from a module,\n", + "under an alias." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from random import uniform as uni" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "uni(0, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "np.arccos(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This option is useful when you need to assign shorter aliases to names you will\n", + "use frequently. In particular, the alias `np` for the `numpy` module will be\n", + "encountered a lot.\n", + "\n", + "Note that modules can have submodules, specified with extra dots `.`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from pathlib import Path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "Path('..').absolute()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "When importing a module, its **submodules are not available by default and you\n", + "must import them explicitly**:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "os.getcwd()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "It is also possible to import several modules with a single import command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import math\n", + "import os\n", + "import sys" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "but this is [not recommended by the Python style guide][https://www.python.org/dev/peps/pep-0008/#imports], which\n", + "suggests to use several import statements, one per module, as it improves\n", + "readability:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import math\n", + "import os\n", + "import sys" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "If you need to import several names from a single module, you can split an import\n", + "function over multiple lines:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from math import e, exp, floor, log" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "floor(exp(log(e)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "However, while these are possibilities, importing the modules is the usual way to go. If you're unsure, just look around for good examples; remember, consistency is the key!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## The standard library\n", + "\n", + "The set of things that Python comes with, from all of the types of objects to\n", + "all of the different modules, is called the [standard library][stl]. It is\n", + "recommended to browse through the standard library documentation to see what is\n", + "available: Python is rich of standard modules, and you should reuse them as much\n", + "as possible instead of rewriting code on your own.\n", + "\n", + "Some of the categories for which standard modules are available are:\n", + "\n", + "* processing paths\n", + "* date and time manipulation\n", + "* mathematical functions\n", + "* parsing of certain file formats\n", + "* support for multiple processes and threads\n", + "* ...\n", + "\n", + "Use standard Python library modules with confidence: being part of any standard\n", + "Python distribution, your code will be easily portable." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Modules from PyPi\n", + "\n", + "Many external modules can be found on [PyPi][pypi], the Python Package Index repository.\n", + "\n", + "If a certain module you need is not available on your distribution you can\n", + "easily install it with the `pip` shell command as seen in the previous lectures." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## (Advanced) Write your first Python module\n", + "\n", + "The simplest Python module you can write is just a `.py` file with some\n", + "functions inside:\n", + "\n", + "\n", + "```python\n", + "# myfirstmodule.py\n", + "\n", + "def one():\n", + " print('this is my first function')\n", + "\n", + "def two():\n", + " print('this is my second function')\n", + "```\n", + "\n", + "You can now fire an `ipython` shell and use those functions right away (because it automatically picks up modules aka `.py` files inside the working directory)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "```python\n", + ">>> import myfirstmodule\n", + ">>> myfirstmodule.one()\n", + "this is my first function\n", + ">>> myfirstmodule.two()\n", + "this is my second function\n", + "```\n", + "\n", + "By simply calling the file `myfirstmodule.py` we have made it available as a\n", + "module named `myfirstmodule` - given that the file is in the same directory\n", + "where we have launched the Python interpreter.\n", + "\n", + "### Module name restrictions\n", + "\n", + "Note that you cannot pick any name you want for a module! From the\n", + "[Python style guide][https://www.python.org/dev/peps/pep-0008/#package-and-module-names\n", + "], we gather that we should use \"short,\n", + "all-lowercase names\". As a matter of fact, if we used dashes in the file name,\n", + "we would have ended up with a syntax error while trying to load it:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "```python\n", + ">>> import my-first-module\n", + " File \"\", line 1\n", + " import my-first-module\n", + " ^\n", + "SyntaxError: invalid syntax\n", + "```\n", + "\n", + "Python treats `-` as a minus and does not understand your intentions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "{% endcallout %}\n", + "\n", + "\n", + "## Write a structured module\n", + "\n", + "Let's now create a more structured module, with submodules and different files.\n", + "We can start from the `myfirstmodule.py` file and create a directory structure:\n", + "\n", + "```bash\n", + "$ mkdir yabba\n", + "$ cp myfirstmodule.py yabba/__init__.py\n", + "```\n", + "\n", + "We have reused the same file created before, copied it into a directory called\n", + "`yabba` and renamed it to `__init__.py`. The double underscore should ring a\n", + "bell: this is a Python special name, and it represents the \"main file\" within\n", + "a module, whereas the directory name now represents the module name.\n", + "\n", + "This means that our module is called `yabba`, and if we import it, functions\n", + "from `__init__.py` will be available:\n", + "\n", + "```python\n", + ">>> import yabba\n", + ">>> yabba.one()\n", + "this is my first function\n", + ">>> yabba.two()\n", + "this is my second function\n", + "```\n", + "\n", + "We can create an additional file inside the `yabba` directory, say\n", + "`yabba/extra.py` and have more functions there:\n", + "\n", + "```python\n", + "# yabba/extra.py\n", + "\n", + "def three():\n", + " print 'this function will return the number three'\n", + " return 3\n", + "```\n", + "\n", + "We have effectively made `extra` a submodule of `yabba`. Let's try:\n", + "\n", + "```python\n", + ">>> import yabba\n", + ">>> filter(lambda x: not x.startswith('__'), dir(yabba))\n", + "['one', 'two']\n", + ">>> import yabba.extra\n", + ">>> yabba.extra.three()\n", + "yabba.extra.three()\n", + "this function will return the number three\n", + "3\n", + "```\n", + "\n", + "{% challenge \"What have I done with the filter function?\" %}\n", + "\n", + "We have used the filter function above to list the functions we have defined\n", + "in our module. Can you describe in detail what the commands above do?\n", + "{% solution \"Solution\" %}\n", + "\n", + "The `dir(module)` command lists all _names_ (not necessarily functions, not\n", + "necessarily defined by us) contained in a given imported module. We have used the\n", + "`filter()` command to filter out all names starting with two underscores. Every\n", + "item returned by `dir()` is passed as `x` to the lambda function which returns\n", + "`True` or `False`, determining whether the `filter()` function should keep or\n", + "discard the current element.\n", + "\n", + "{% endsolution %}\n", + "\n", + "{% endchallenge %}\n", + "\n", + "\n", + "## Run a module\n", + "\n", + "We can make a Python module that can be easily imported by other Python\n", + "programs, but we can also make it in a way that it can be run directly as a\n", + "Python script.\n", + "\n", + "Let's write this special module and call it `runnable.py`:\n", + "\n", + "```python\n", + "#!/usr/bin/env python\n", + "\n", + "long_format = False\n", + "\n", + "def print_label(label, msg):\n", + " if long_format:\n", + " out = '{0}: {1}'.format(label.upper(), str(msg))\n", + " else:\n", + " out = '{0}-{1}'.format(label[0].upper(), str(msg))\n", + " print out\n", + "\n", + "def debug(msg):\n", + " print_label('debug', msg)\n", + "\n", + "def warning(msg):\n", + " print_label('warning', msg)\n", + "\n", + "if __name__ == '__main__':\n", + " print '*** Testing print functions ***'\n", + " debug('This is a debug message')\n", + " long_format = True\n", + " warning('This is a warning message with a long label')\n", + "else:\n", + " print 'Module {0} is being imported'.format(__name__)\n", + "```\n", + "\n", + "Now let's make it executable:\n", + "\n", + "```bash\n", + "$ chmod +x runnable.py\n", + "```\n", + "\n", + "It can be now run as a normal executable from your shell:\n", + "\n", + "```\n", + "$ ./runnable.py\n", + "*** Testing print functions ***\n", + "D-This is a debug message\n", + "WARNING: This is a warning message with a long label\n", + "```\n", + "\n", + "There are two outstanding notions here. First off, the first line is a\n", + "\"shebang\": it really has to be the _first_ line in a file (it cannot be the\n", + "second, or \"one of the first\", or the first non-empty) and it basically tells\n", + "your shell that your executable text file has to be interpreted by the current\n", + "Python interpreter. Just use this line as it is.\n", + "\n", + "Secondly, we notice we have a peculiar `if` condition with a block that gets\n", + "executed when we run the file. `__name__` is a special internal Python variable\n", + "which is set to the module name in case the module is imported. When the module\n", + "is ran, it is set to the special value `\"__main__\"`.\n", + "\n", + "The `else:` condition we have added is just to show what happens when you import\n", + "the module instead:\n", + "\n", + "```python\n", + ">>> import runnable\n", + "Module runnable is being imported\n", + ">>> runnable.warning('hey I can use it from here too')\n", + "W-hey I can use it from here too\n", + "```\n", + "\n", + "Now, the `if` condition is not necessary when you want to run the module - those\n", + "lines in the `if` block will be executed anyway. It is however used to _prevent_\n", + "some lines from being executed when you import the file as a module.\n", + "\n", + "Please also note that module imports are typically _silent_, so the `else:`\n", + "condition with a printout would not exist in real life.\n", + "\n", + "\n", + "[stl]: https://docs.python.org/2/library/index.html\n", + "[pep8-import]: https://www.python.org/dev/peps/pep-0008/#imports\n", + "[pep8-modulenames]: https://www.python.org/dev/peps/pep-0008/#package-and-module-names\n", + "[pypi]: https://pypi.org/\n", + "[anaconda]: https://www.anaconda.com/distribution/\n", + "[lcg_virtualenv]: https://gitlab.cern.ch/cburr/lcg_virtualenv/\n" + ] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython" + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/modules.md b/python/modules.md deleted file mode 100644 index 4a318657..00000000 --- a/python/modules.md +++ /dev/null @@ -1,430 +0,0 @@ -# Modules - -Python comes with lots of useful stuff, which is provided with modules -(and submodules, see later). -We have already met the maths module, but did not talk about how we started using it. - -```python ->>> import math ->>> math - ->>> math.pi -3.141592653589793 ->>> math.sin(1) -0.8414709848078965 -``` - -The path after `from` might look different on your computer. - -So, `math` is a _module_, and this seems to behave a lot like other objects we -have met: it is a container with properties and methods attached that we can -access with the dot operator `.`. Actually, that is pretty much all there is to -them. - - -## Using modules into your code: import - -The keyword `import`, usually specified at the beginning of your source code, is -used to tell Python what modules you want to make available to your current -code. - -There are different ways of specifying an import. The one we have seen already -simply makes the module available to you: - -```python ->>> import random ->>> random.uniform(0, 1) -0.5877109428927353 -``` - -The module `random` contains functions useful for random number generation: with -the `import` above, we have made the `random` module accessible, and everything -within that module is accessible via the syntax `random.`. For the record, -the `uniform(x,y)` method returns a pseudo-random number within the range -`$ [x,y] $`. - -Sometimes you want to make only one or more things from a given module -accessible: Python gives you the ability to import just those: - -```python ->>> from random import uniform, choice ->>> uniform(0, 1) -0.4059007502204043 ->>> choice([ 33, 56, 42, -1 ]) -42 -``` - -In this case the `uniform` and `choice` names are available _directly_, _i.e._ -without using the `random` prefix. All other functions in the `random` module -are not available in this case. For the record, the `choice` function returns a -random element from a given collection. - -Another option is to import _all_ functions of a certain module and make them -available without a prefix: - -```python ->>> from random import * ->>> gauss(0, 1) --1.639334770284028 -``` - -This is not that recommended as you generally do not know what is the extent -of what you are importing and you might end up with name clashes between your -current code and the imported module, as it will all be in the same namespace, -meaning directly available with no need for a `.` syntax. - -Lastly, it is possible to import modules, or specific names from a module, -under an alias. - -```python ->>> from random import uniform as uni ->>> uni(0, 1) -0.7288973406605329 ->>> import numpy as np -np.arccos(1) -0.0 -``` - -This option is useful when you need to assign shorter aliases to names you will -use frequently. In particular, the alias `np` for the `numpy` module will be -encountered a lot. - -Note that modules can have submodules, specified with extra dots `.`: - -```python ->>> from os.path import abspath ->>> abspath('..') -'/afs/cern.ch/user/d' -``` - -When importing a module, its **submodules are not available by default and you -must import them explicitly**: - -```python ->>> import os ->>> os.getcwd() -'/afs/cern.ch/user/d/dberzano' ->>> import os.path ->>> os.path.basename(os.getcwd()) -'dberzano' -``` - -Note that due to the current Python implementation of the `os` module, `os.path` -functions are _actually_ available _even without importing `os.path`. But just -`os`_. You cannot and should not rely on this implementation, which represents an exception -and might change in the future. Always import submodules explicitly! - -It is also possible to import several modules with a single import command: - -```python ->>> import os, sys, math -``` - -but this is [not recommended by the Python style guide][pep8-import], which -suggests to use several import statements, one per module, as it improves -readability: - -```python ->>> import os ->>> import sys ->>> import math -``` - -If you need to import several names from a single module, you can split an import -function over multiple lines: - -```python ->>> from math import ( -... exp, -... log, -... e, -... floor -... ) ->>> floor(exp(log(e))) -2.0 -``` - - -## The standard library - -The set of things that Python comes with, from all of the types of objects to -all of the different modules, is called the [standard library][stl]. It is -recommended to browse through the standard library documentation to see what is -available: Python is rich of standard modules, and you should reuse them as much -as possible instead of rewriting code on your own. - -Some of the categories for which standard modules are available are: - -* processing paths -* date and time manipulation -* mathematical functions -* parsing of certain file formats -* support for multiple processes and threads -* ... - -Use standard Python library modules with confidence: being part of any standard -Python distribution, your code will be easily portable. - - -## Modules from PyPi - -Many external modules can be found on [PyPi][pypi], the Python Package Index repository. -Some of those modules are -already part of some Python distributions (such as [Anaconda][anaconda], which -comes with more than a thousand science-oriented modules preinstalled). - -If a certain module you need is not available on your distribution you can -easily install it with the `pip` shell command. Since you typically do not have write -access to the standard Python installation's directories, `pip` allows you to -install modules only for yourself, under your current user's home directory. -It is recommended to set up in your shell startup script (such as `~/.bashrc`) -the following two lines telling once and for all where to install and search for -Python user modules: - -```bash -export PYTHONUSERBASE=$HOME/.local -export PATH=$PYTHONUSERBASE/bin:$PATH -``` - -Once you have done that, close your current terminal window and open a new one, -and you will be ready to use `pip`. We will see in a later lesson how to install -the `root_pandas` module with: - -```bash -pip install --user root_pandas -``` - -## Modules inside a virtual environment - -It is however usually preferable and safer to do everything inside a virtual environement. -The latter is like a copy of your current environement. Thus you can modify your virtual -environement (including installing/deleting/updating modules) without affecting your default -environement. If at some point you realize you have broken everything, you can always exit -the virtual environement and go back to the default lxplus one. - -To build a virtual environement based on LCG views, you can use [LCG_virtualenv][lcg_virtualenv]: - -```bash -git clone https://gitlab.cern.ch/cburr/lcg_virtualenv.git -./lcg_virtualenv/create_lcg_virtualenv myVenv -``` -To activate the virtual environement do: - -```bash -source myVenv/bin/activate -``` - -You can then install stuff with `pip`, like for instance `root_pandas`: - -```bash -pip install --upgrade root_pandas matplotlib -python -c 'import pandas; print(f"Got pandas from {pandas.__file__}")' -python -c 'import root_pandas; print(f"Got root_pandas from {root_pandas.__file__}")' -python -c 'import matplotlib; print(f"Got matplotlib from {matplotlib.__file__}")' -``` - -You can go back to the default environement using the `deactivate` command. - - -## Write your first Python module - -The simplest Python module you can write is just a `.py` file with some -functions inside: - -```python -# myfirstmodule.py - -def one(): - print('this is my first function') - -def two(): - print('this is my second function') -``` - -You can now fire an `ipython` shell and use those functions right away: - -```python ->>> import myfirstmodule ->>> myfirstmodule.one() -this is my first function ->>> myfirstmodule.two() -this is my second function -``` - -By simply calling the file `myfirstmodule.py` we have made it available as a -module named `myfirstmodule` - given that the file is in the same directory -where we have launched the Python interpreter. - -{% callout "Module name restrictions" %} - -Note that you cannot pick any name you want for a module! From the -[Python style guide][pep8-modulenames], we gather that we should use "short, -all-lowercase names". As a matter of fact, if we used dashes in the file name, -we would have ended up with a syntax error while trying to load it: - -```python ->>> import my-first-module - File "", line 1 - import my-first-module - ^ -SyntaxError: invalid syntax -``` - -Python treats `-` as a minus and does not understand your intentions. - -{% endcallout %} - - -## Write a structured module - -Let's now create a more structured module, with submodules and different files. -We can start from the `myfirstmodule.py` file and create a directory structure: - -```bash -$ mkdir yabba -$ cp myfirstmodule.py yabba/__init__.py -``` - -We have reused the same file created before, copied it into a directory called -`yabba` and renamed it to `__init__.py`. The double underscore should ring a -bell: this is a Python special name, and it represents the "main file" within -a module, whereas the directory name now represents the module name. - -This means that our module is called `yabba`, and if we import it, functions -from `__init__.py` will be available: - -```python ->>> import yabba ->>> yabba.one() -this is my first function ->>> yabba.two() -this is my second function -``` - -We can create an additional file inside the `yabba` directory, say -`yabba/extra.py` and have more functions there: - -```python -# yabba/extra.py - -def three(): - print 'this function will return the number three' - return 3 -``` - -We have effectively made `extra` a submodule of `yabba`. Let's try: - -```python ->>> import yabba ->>> filter(lambda x: not x.startswith('__'), dir(yabba)) -['one', 'two'] ->>> import yabba.extra ->>> yabba.extra.three() -yabba.extra.three() -this function will return the number three -3 -``` - -{% challenge "What have I done with the filter function?" %} - -We have used the filter function above to list the functions we have defined -in our module. Can you describe in detail what the commands above do? -{% solution "Solution" %} - -The `dir(module)` command lists all _names_ (not necessarily functions, not -necessarily defined by us) contained in a given imported module. We have used the -`filter()` command to filter out all names starting with two underscores. Every -item returned by `dir()` is passed as `x` to the lambda function which returns -`True` or `False`, determining whether the `filter()` function should keep or -discard the current element. - -{% endsolution %} - -{% endchallenge %} - - -## Run a module - -We can make a Python module that can be easily imported by other Python -programs, but we can also make it in a way that it can be run directly as a -Python script. - -Let's write this special module and call it `runnable.py`: - -```python -#!/usr/bin/env python - -long_format = False - -def print_label(label, msg): - if long_format: - out = '{0}: {1}'.format(label.upper(), str(msg)) - else: - out = '{0}-{1}'.format(label[0].upper(), str(msg)) - print out - -def debug(msg): - print_label('debug', msg) - -def warning(msg): - print_label('warning', msg) - -if __name__ == '__main__': - print '*** Testing print functions ***' - debug('This is a debug message') - long_format = True - warning('This is a warning message with a long label') -else: - print 'Module {0} is being imported'.format(__name__) -``` - -Now let's make it executable: - -```bash -$ chmod +x runnable.py -``` - -It can be now run as a normal executable from your shell: - -``` -$ ./runnable.py -*** Testing print functions *** -D-This is a debug message -WARNING: This is a warning message with a long label -``` - -There are two outstanding notions here. First off, the first line is a -"shebang": it really has to be the _first_ line in a file (it cannot be the -second, or "one of the first", or the first non-empty) and it basically tells -your shell that your executable text file has to be interpreted by the current -Python interpreter. Just use this line as it is. - -Secondly, we notice we have a peculiar `if` condition with a block that gets -executed when we run the file. `__name__` is a special internal Python variable -which is set to the module name in case the module is imported. When the module -is ran, it is set to the special value `"__main__"`. - -The `else:` condition we have added is just to show what happens when you import -the module instead: - -```python ->>> import runnable -Module runnable is being imported ->>> runnable.warning('hey I can use it from here too') -W-hey I can use it from here too -``` - -Now, the `if` condition is not necessary when you want to run the module - those -lines in the `if` block will be executed anyway. It is however used to _prevent_ -some lines from being executed when you import the file as a module. - -Please also note that module imports are typically _silent_, so the `else:` -condition with a printout would not exist in real life. - - -[stl]: https://docs.python.org/2/library/index.html -[pep8-import]: https://www.python.org/dev/peps/pep-0008/#imports -[pep8-modulenames]: https://www.python.org/dev/peps/pep-0008/#package-and-module-names -[pypi]: https://pypi.org/ -[anaconda]: https://www.anaconda.com/distribution/ -[lcg_virtualenv]: https://gitlab.cern.ch/cburr/lcg_virtualenv/ diff --git a/python/numbers.ipynb b/python/numbers.ipynb new file mode 100644 index 00000000..d622607d --- /dev/null +++ b/python/numbers.ipynb @@ -0,0 +1,293 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Numbers\n", + "\n", + "There’s nothing magical about numbers in Python, and we’ve already discovered \n", + "how we perform operations on them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "(2 * (1 + 3) - 5) / 0.5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "11 % 4" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Python also lets you manipulate complex numbers, using `j` to represent the \n", + "complex term." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a = 1 + 4j\n", + "b = 4 - 1j\n", + "a - b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Complex numbers are objects, of course, and have some useful functions and \n", + "properties attached to them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a.conjugate()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a.imag" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a.real" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Somewhat confusingly, computing the magnitude of a complex number can be done \n", + "with the `abs` method, which is available globally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "abs(a)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "(a.real**2 + a.imag**2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "(a.real**2 + a.imag**2) ** 0.5" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This also demonstrates the `**` operator, which for real numbers corresponds to \n", + "exponentiation.\n", + "\n", + "Each type of number can be created _literally_, like we’ve been doing, by just \n", + "typing the number into your shell or source code, and by using the correspond \n", + "methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "int()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "float()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "complex()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython" + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/numbers.md b/python/numbers.md deleted file mode 100644 index d3c2aaf4..00000000 --- a/python/numbers.md +++ /dev/null @@ -1,135 +0,0 @@ -# Numbers - -There’s nothing magical about numbers in Python, and we’ve already discovered -how we perform operations on them. - -```python ->>> (2 * (1 + 3) - 5) / 0.5 -2 ->>> 11 % 4 -3 -``` - - -{% callout "Integer division in Python 2" %} - - -If for any reason (e.g. you want to use LHCb or Alice software) you have to use Python 2, -beware of that Python 2 has a few different _types_ of numbers, and they can -behave differently. - -```python ->>> 10/3 -3 ->>> 10.0/3.0 -3.3333333333333335 -``` - -Interesting. Something different happens when we use numbers with and without -decimal places! This occurs because numbers given with decimal places, like -`3.14` are _floats_, while those without, like `3`, are _integers_. - -For historical reasons, dividing two integers in Python 2 returns an integer, -where the intermediate result is always rounded down. Division using _at least -one_ float gives us the more intuitive answer. - -In Python 3, division with integers works the same way as with floats. You can -ask to have this behaviour in Python 2. - -```python ->>> from __future__ import division ->>> 3 / 4 -0.75 ->>> 3.0 / 4.0 -0.75 -``` - -Because the default behaviour in Python 2 is quite unintuitive, we recommend -using the `from __future__ import division` line everywhere. We’ll come to what exactly this -line is doing shortly. - -If you _do_ want a rounding division, you then can ask for it explicitly with -round, or, if you want an integer division (truncating, rounding towards 0): - -```python ->>> 5 / 3 -1.66666666667 - ->>> round(5 / 3) -2 - ->>> 5 // 3 -1 -``` - -{% endcallout %} - -{% callout "Operators" %} - -This behaviour can be explained in terms of operators and the double-underscore -methods. You can see that numbers have two methods for division: - -```python ->>> dir(1) -[..., - '__floordiv__', - ... - '__truediv__', - ...] -``` - -In Python 2, the `/` operator corresponded to the `__floordiv__` method when -used with integers, but the `__truediv__` operator when used with floats. In -Python 3, and when using the `from __future__ import division` line, the `/` -operator always uses the `__truediv__` method. - -{% endcallout %} - -Python also lets you manipulate complex numbers, using `j` to represent the -complex term. - -```python ->>> a = 1 + 4j ->>> b = 4 - 1j ->>> a - b -(-3+5j) -``` - -Complex numbers are objects, of course, and have some useful functions and -properties attached to them. - -```python ->>> a.conjugate() -(1-4j) ->>> a.imag -4.0 ->>> a.real -1.0 -``` - -Somewhat confusingly, computing the magnitude of a complex number can be done -with the `abs` method, which is available globally. - -```python ->>> abs(a) -4.123105625617661 ->>> import numpy as np ->>> np.sqrt(a.real**2 + a.imag**2) -4.123105625617661 -``` - -This also demonstrates the `**` operator, which for real numbers corresponds to -exponentiation. - -Each type of number can be created _literally_, like we’ve been doing, by just -typing the number into your shell or source code, and by using the correspond -methods. - -```python ->>> int() -0 ->>> float() -0.0 ->>> complex() -0j -``` diff --git a/python/strings.ipynb b/python/strings.ipynb new file mode 100644 index 00000000..79f8e71f --- /dev/null +++ b/python/strings.ipynb @@ -0,0 +1,689 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Strings\n", + "\n", + "Number objects are useful for storing values which are, well, numbers. But what \n", + "if we want to store a sentence? Enter _strings_!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "a = \"What's orange and sounds like a parrot?\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Strings can be joined with `+`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b = 'A carrot'\n", + "a + ' ' + b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "And they can be multiplied by numbers, amazingly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "c = 'omg'\n", + "10 * c" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We’ve specified strings _literally_, _in_ the source code, by wrapping the text \n", + "with singles quotes or double quotes. There’s no difference; most people choose \n", + "one and stick with it.\n", + "\n", + "It can be useful to change if your text contains the quote character. If it \n", + "contains both, you can _escape_ the quote mark by preceding it with a \n", + "backslash. This tells Python that the quote is part of the string you want, and \n", + "not the ending quote." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "fact = \"Gary's favourite word is \\\"python\\\".\"\n", + "fact" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Python prints strings by surrounding them with _single_ quotes, so it escapes \n", + "the single quotes in our string. This is useful because we can copy-paste the \n", + "string into some Python code to use it somewhere else, without having to worry \n", + "about escaping things.\n", + "\n", + "We can create multi-line strings by using three quotation marks. \n", + "Conventionally, double quotations are usually used for these." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "long_fact = \"\"\"This is a long string.\n", + "Quite long indeed.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(long_fact)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Creating strings like this is useful when you want to include line breaks in \n", + "your string. You can also use `\\n` in strings to insert line breaks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "'This is a long string\\n\\nQuite long indeed.\\n'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We can convert things to strings by using the `str` method, which can also \n", + "create an _empty_ string for us." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "'A number: ' + str(999 - 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Strings are objects, and have lots of useful methods attached to them. If you \n", + "want to know how many characters are in a string, you use the global `len` \n", + "method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b.upper()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b.upper().lower()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b.replace('carrot', 'parrot').replace(' ', '_')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "len(b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Notice that none of these operations _modify_ the value of the `b` variable. \n", + "Operations on strings _always_ return _new_ strings. Strings are said to be \n", + "_immutable_ for this reason: you can never change a string, just make new ones." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Formatting\n", + "\n", + "One of the most common things you’ll find yourself doing with strings is \n", + "interleaving values into them. For example, you’ve finished an amazing \n", + "analysis, and want to print the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "result1 = 42.0\n", + "result2 = 123.21\n", + "print('My results are: ' + str(result1) + ', ' + str(result2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "\n", + "This is already quite ugly, and will only get worse with more results. We can \n", + "instead use the `f-string` and use the \n", + "special `{}` placeholders to say where we want the values to go in the string by placing an `f` in front of the string (a \"formatted\" string)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "output = f'My results are: {result1} {result2}'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Instead, we can also just create a string withouth the `f` in front and later insert values. This is not only the more historical method, it also provides the ability to template a string and then use the `format` method that’s available on strings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "template = 'My results are: {}, {}'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(template.format(result1, result2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Much better! We define the whole string at once, and then place the missing \n", + "values in later.\n", + "\n", + "We can add numbers inside the placeholders, `{0}` and `{1}`, which correspond to the indices \n", + "of the arguments passed to the `format` method, where `0` is the first \n", + "argument, `1` is the second, and so on. By referencing positions like this, we \n", + "can easily repeat placeholders in the string, but only pass the values once to \n", + "`format`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "template = 'My results are: {1}, {0}. The best is {0}, obviously.' # no need to start with 0 here" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(template.format(result1, result2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "You can also use _named_ placeholders, then passing the values to `format` \n", + "using the same name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "template3 = 'My results are: {best}, {worst}. But the best is {best}, obviously.'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(template3.format(best=result1, worst=result2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "But remember the `f-string`, if we don't need to do something fancy, it's a lot more convenient to use it (and the 99% use-case)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "f'My results are: {result1}, {result2}. But the best is {result1}, obviously.'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(template3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This is nice because it gives more meaning to what the placeholders are for.\n", + "\n", + "There’s [a lot you can do inside the placeholders](https://docs.python.org/3/tutorial/inputoutput.html#the-string-format-method), such as specifying that you want to format a number with a certain number of decimal places." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(f'This number is great: {result1:.3f}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "If you want to print a literal curly brace using `format`, you will need to\n", + "escape it by doubling it, so that `{{` will become `{` and `}}` will become `}`.\n", + "Here's an example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "print(f'This number will be surrounded by curly braces: {{{result1}}}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The innermost `{0}` is replaced with the number, and `{{...}}` becomes `{...}`." + ] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython" + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/strings.md b/python/strings.md deleted file mode 100644 index 00020744..00000000 --- a/python/strings.md +++ /dev/null @@ -1,184 +0,0 @@ -# Strings - -Number objects are useful for storing values which are, well, numbers. But what -if we want to store a sentence? Enter _strings_! - -```python ->>> a = "What's orange and sounds like a parrot?" -``` - -Strings can be joined with `+`. - -```python ->>> b = 'A carrot' ->>> a + ' ' + b -'What's orange and sounds like a parrot? A carrot' -``` - -And they can be multiplied by numbers, amazingly. - -```python ->>> c = 'omg' ->>> 10*c -'omgomgomgomgomgomgomgomgomgomg' -``` - -We’ve specified strings _literally_, _in_ the source code, by wrapping the text -with singles quotes or double quotes. There’s no difference; most people choose -one and stick with it. - -It can be useful to change if your text contains the quote character. If it -contains both, you can _escape_ the quote mark by preceding it with a -backslash. This tells Python that the quote is part of the string you want, and -not the ending quote. - -```python ->>> fact = "Gary's favourite word is \"python\"." ->>> fact -'Gary\'s favourite word is "python".' -``` - -Python prints strings by surrounding them with _single_ quotes, so it escapes -the single quotes in our string. This is useful because we can copy-paste the -string into some Python code to use it somewhere else, without having to worry -about escaping things. - -We can create multi-line strings by using three quotation marks. -Conventionally, double quotations are usually used for these. - -```python ->>> long_fact = """This is a long string. -... -... Quite long indeed. -... """ ->>> print(long_fact) -This is a long string. - -Quite long indeed. - ->>> -``` - -Creating strings like this is useful when you want to include line breaks in -your string. You can also use `\n` in strings to insert line breaks. - -```python ->>> 'This is a long string\n\nQuite long indeed.\n' -``` - -We can convert things to strings by using the `str` method, which can also -create an _empty_ string for us. - -```python ->>> str() -'' ->>> 'A number: ' + str(999 - 1) -'A number: 998' -``` - -Strings are objects, and have lots of useful methods attached to them. If you -want to know how many characters are in a string, you use the global `len` -method. - -```python ->>> b.upper() -'A CARROT' ->>> b.upper().lower() -'a carrot' ->>> b.replace('carrot', 'parrot').replace(' ', '_') -'A_parrot' ->>> len(b) -8 ->>> b -'A carrot' -``` - -Notice that none of these operations _modify_ the value of the `b` variable. -Operations on strings _always_ return _new_ strings. Strings are said to be -_immutable_ for this reason: you can never change a string, just make new ones. - -## Formatting - -One of the most common things you’ll find yourself doing with strings is -interleaving values into them. For example, you’ve finished an amazing -analysis, and want to print the results. - -```python ->>> result1 = 123.0 ->>> result2 = 122.3 ->>> print('My results are: ' + str(result1) + ', ' + str(result2)) -My results are: 123.0, 122.3 -``` - -This is already quite ugly, and will only get worse with more results. We can -instead use the `format` method that’s available on strings, and use the -special `{}` placeholders to say where we want the values to go in the string. - -```python ->>> template = 'My results are: {0}, {1}' ->>> print(template.format(result1, result2)) -My results are: 123.0, 122.3 -``` - -Much better! We define the whole string at once, and then place the missing -values in later. - -The numbers inside the placeholders, `{0}` and `{1}`, correspond to the indices -of the arguments passed to the `format` method (but are not mandatory in newer Python), where `0` is the first -argument, `1` is the second, and so on. By referencing positions like this, we -can easily repeat placeholders in the string, but only pass the values once to -`format`. - -```python ->>> template2 = 'My results are: {0}, {1}. But the best is {0}, obviously.' ->>> print(template2.format(result1, result2)) -My results are: 123.0, 122.3. But the best is 123.0, obviously. -``` - -You can also use _named_ placeholders, then passing the values to `format` -using the same name. - -```python ->>> template3 = 'My results are: {best}, {worst}. But the best is {best}, obviously.' ->>> print(template3.format(best=result1, worst=result2)) -My results are: 123.0, 122.3. But the best is 123.0, obviously. -``` - -We can do even better! With Python 3.6+, there are so called f-strings that allow to directly enter a Python expression into the brackets. The syntax is to add an `f` in front of the string. - - -```python ->>> template3 = f'My results are: {result1}, {result2}. But the best is {result1}, obviously.' ->>> print(template3) -My results are: 123.0, 122.3. But the best is 123.0, obviously. -``` - - -This is nice because it gives more meaning to what the placeholders are for. - -There’s [a lot you can do inside the placeholders][strformat], such as specifying that you want to format a number with a certain number of decimal places. - -```python ->>> print('This number is great: {0:.3f}'.format(result1)) -This number is great: 123.000 -``` - -The same works again with f-strings. - -```python ->>> print(f'This number is great: {result1:.3f}') -This number is great: 123.000 -``` - -If you want to print a literal curly brace using `format`, you will need to -escape it by doubling it, so that `{{` will become `{` and `}}` will become `}`. -Here's an example: - -```python ->>> print('This number will be surrounded by curly braces: {{{0}}}'.format(123)) -This number will be surrounded by curly braces: {123} -``` - -The innermost `{0}` is replaced with the number, and `{{...}}` becomes `{...}`. - -[strformat]: https://pyformat.info/ diff --git a/shell/fig/cern_change_shell.png b/shell/fig/cern_change_shell.png index e3183835..bab34f00 100644 Binary files a/shell/fig/cern_change_shell.png and b/shell/fig/cern_change_shell.png differ diff --git a/shell/fig/nano-screenshot.png b/shell/fig/nano-screenshot.png index 50fb1710..9aca8356 100644 Binary files a/shell/fig/nano-screenshot.png and b/shell/fig/nano-screenshot.png differ diff --git a/shell/fig/redirects-and-pipes.png b/shell/fig/redirects-and-pipes.png index 3c595a59..a3332300 100644 Binary files a/shell/fig/redirects-and-pipes.png and b/shell/fig/redirects-and-pipes.png differ diff --git a/snakemake/img/DAG_multiple.png b/snakemake/img/DAG_multiple.png index 66d4c4b0..430ab062 100644 Binary files a/snakemake/img/DAG_multiple.png and b/snakemake/img/DAG_multiple.png differ diff --git a/snakemake/img/DAG_single-wide.png b/snakemake/img/DAG_single-wide.png index 1b2be4ac..7908d80f 100644 Binary files a/snakemake/img/DAG_single-wide.png and b/snakemake/img/DAG_single-wide.png differ diff --git a/snakemake/img/DAG_single.png b/snakemake/img/DAG_single.png index 7b6e80f4..aae2b33a 100644 Binary files a/snakemake/img/DAG_single.png and b/snakemake/img/DAG_single.png differ diff --git a/snakemake/img/Reporting_DAG.png b/snakemake/img/Reporting_DAG.png index 14359aaa..c30ed7f3 100644 Binary files a/snakemake/img/Reporting_DAG.png and b/snakemake/img/Reporting_DAG.png differ diff --git a/snakemake/img/Reporting_rule.png b/snakemake/img/Reporting_rule.png index dccd94ce..3060de92 100644 Binary files a/snakemake/img/Reporting_rule.png and b/snakemake/img/Reporting_rule.png differ diff --git a/snakemake/img/Reporting_stats.png b/snakemake/img/Reporting_stats.png index a3f71b04..508110e4 100644 Binary files a/snakemake/img/Reporting_stats.png and b/snakemake/img/Reporting_stats.png differ