Created
April 12, 2022 08:23
-
-
Save francois-durand/c022fb2001bdbeeb023dcbfb8bcbcfb5 to your computer and use it in GitHub Desktop.
architecture_in_oop.ipynb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "# Architecture in Object-Oriented Programming: \n\n# A Case Study " | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "What is often done:\n* Explain basics of OOP (e.g. syntax),\n* Give principles of clean programming (e.g. SOLID) with simplified examples, more or less realistic." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "What I want to do today:\n\n* Take a more realistic example, based on my personal experience,\n* See cases where application of principles is not as obvious as in didactic examples,\n* Show how understanding the \"spirit\" of the principles is the most important, to be able to adapt them to real problems." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Words of caution:\n* Examples related to my **personal experience**, not as universal as didactic examples.\n* Code architecture results from a series of **choices**. Some are quite clearly good or bad, others are more debatable... Maybe we will not agree on all choices. But the interesting part is to **discuss** them and improve our general **understanding** of the consequences of these choices, in order to make more **educated decisions**.\n* This session will be much more interesting if it is **interactive**. Please share your ideas!" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T06:48:03.824058Z", | |
"end_time": "2022-04-12T06:48:03.832059Z" | |
}, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Our case study: design a package to implement **voting rules**.\n \nBased on my experience to code Whalrus (Which Alternative Represents Us): https://github.com/francois-durand/whalrus.\n\n", | |
"attachments": { | |
"image.png": { | |
"image/png": "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AayUUuccAR97/1dePmPi3Y5vkO41KQ1gpZS6CAj4xPu/8rgL5s65lroMKWUawEopdSEQ8JH3fy3yCZiIIvERSMvQKdLTkJRS6qJIatGe2RBPQaZZ7ms3OD3aA1ZKqQuEgLve/2/xXxDldCJ0qrQHrJRSFwsBd700IBuiu2GlSQNYKaUuoi+8Vp9TpiVopZRSKgUawEoppVQKNICVUkqpFGgAK6WUUinQAFZKKaVSoAGslFJKpUADWCmllEqBBrBSSimVAg1gpZRSKgUawEoppVQKNICVUkqpFGgAK6WUUinQAFZKKaVSoAGslFJKpUADWCmllEqBBrBSSimVAg1gpZRSKgUawEoppVQKNICVUkqpFGgAK6WUUinQAFZKKaVSoAGslFJKpUADWCmllEqBBrBSSimVAg1gpZRSKgUawEoppVQKNICVUkqpFGgAK6WUUinQAFZKKaVSoAGslFJKpUADWCmllEqBBrBSSimVAg1gpZRSKgUawEoppVQKNICVUkqpFNi9z4QQEwlRilejlFJKnVtEsu93nQCOvUelMrNTklY7latSSimlzjfvY7RaIp0U7gTw2MTE0Ge/nfzgl9779K5NKaWUOre8943xcReGyW87AfxHf/zHU9PTztqj/6JSSimluici/AevXZqaSn5Le31hpZRSSvWNzoJWSimlUvD/ARN9npJrP62pAAAAAElFTkSuQmCC" | |
} | |
} | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "c6a76dac", | |
"cell_type": "markdown", | |
"source": "## Ballots" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "7ca41e57", | |
"cell_type": "markdown", | |
"source": "Several kinds of ballots:\n\n* (Possibly weak) order: $d \\sim b > a > c$.\n* Linear order: $d > b > a > c$.\n* Grades: $d: 10, b: 7, a: 1, c: 0$.\n* Maybe we will add other kinds of ballots during the life of the project." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "c83d613a", | |
"cell_type": "markdown", | |
"source": "### Classes or not?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "196c9962", | |
"cell_type": "markdown", | |
"source": "**Option 1:** no classes.\n \n* (Possibly weak) order: ``[{'d', 'b'}, 'a', 'c']``.\n* Linear order: ``['d', 'b', 'a', 'c']``.\n* Grades: ``{'d': 10, 'b': 7, 'a': 1, 'c': 0}``.\n\n**Option 2:** with classes BallotOrder, BallotLinearOrder, BallotGrades..." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T07:40:19.612860Z", | |
"end_time": "2022-04-12T07:40:19.632861Z" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Which one would you recommend? Why?\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "f4485064", | |
"cell_type": "markdown", | |
"source": "Consider option 1 (no class). You will probably add functions to display ballots, extract the top candidates, etc." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:16:22.435370Z", | |
"end_time": "2022-04-12T08:16:22.442370Z" | |
}, | |
"trusted": true | |
}, | |
"id": "6c2d4821", | |
"cell_type": "code", | |
"source": "def ballot_to_str(ballot):\n if isinstance(ballot, dict):\n return ', '.join([\n f\"Candidate {k}: grade {v}\"\n for k, v in ballot.items()\n ])\n if isinstance(ballot, list):\n return ' > '.join([\n ' ~ '.join([str(c) for c in sorted(x)]) if isinstance(x, set) else str(x)\n for x in ballot\n ])\n raise NotImplementedError", | |
"execution_count": 1, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:16:22.775857Z", | |
"end_time": "2022-04-12T08:16:22.797570Z" | |
}, | |
"trusted": true | |
}, | |
"id": "4829176c", | |
"cell_type": "code", | |
"source": "ballot_to_str(ballot={'d': 10, 'b': 7, 'a': 1, 'c': 0})", | |
"execution_count": 2, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 2, | |
"data": { | |
"text/plain": "'Candidate d: grade 10, Candidate b: grade 7, Candidate a: grade 1, Candidate c: grade 0'" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:16:23.017049Z", | |
"end_time": "2022-04-12T08:16:23.031150Z" | |
}, | |
"trusted": true | |
}, | |
"id": "ec7b74d6", | |
"cell_type": "code", | |
"source": "ballot_to_str(ballot=[{'d', 'b'}, 'a', 'c'])", | |
"execution_count": 3, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 3, | |
"data": { | |
"text/plain": "'b ~ d > a > c'" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:16:26.571461Z", | |
"end_time": "2022-04-12T08:16:26.576473Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "81e644e9", | |
"cell_type": "code", | |
"source": "def top_candidates(ballot):\n if isinstance(ballot, dict):\n max_grade = max(ballot.values())\n return {k for k, v in ballot.items() if v == max_grade}\n if isinstance(ballot, list):\n top_equivalence_class = ballot[0]\n return top_equivalence_class if isinstance(top_equivalence_class, set) else {top_equivalence_class}\n raise NotImplementedError", | |
"execution_count": 4, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:16:26.803078Z", | |
"end_time": "2022-04-12T08:16:26.809066Z" | |
}, | |
"trusted": true | |
}, | |
"id": "0ad87759", | |
"cell_type": "code", | |
"source": "ballot={'d': 10, 'b': 7, 'a': 1, 'c': 0}\ntop_candidates(ballot)", | |
"execution_count": 5, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 5, | |
"data": { | |
"text/plain": "{'d'}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:16:27.177978Z", | |
"end_time": "2022-04-12T08:16:27.182991Z" | |
}, | |
"trusted": true | |
}, | |
"id": "e8d0dbdd", | |
"cell_type": "code", | |
"source": "ballot=[{'d', 'b'}, 'a', 'c']\ntop_candidates(ballot)", | |
"execution_count": 6, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 6, | |
"data": { | |
"text/plain": "{'b', 'd'}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "4982f277", | |
"cell_type": "markdown", | |
"source": "Drawbacks: your ideas?\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Drawbacks (continued):\n \n* Later in the project, if we **add another type of ballot**, we will need to do a \"treasure hunt\" to find all the functions that need to be updated: `ballot_to_str`, `top_candidates`, etc. $\\Rightarrow$ not convenient and prone to mistakes." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Drawbacks (continued):\n \n* Sometimes, the type of ballot may not be so **easily recognizable**. Consider the following ones:\n\n * **Verbal evaluations:** $d \\to \\text{Excellent}, b \\to \\text{Good}, a \\to \\text{Fair}, c \\to \\text{Poor}$. Naturally represented by a dictionary, so we may need an intricate \"if\" clause to distinguish it from a ballot with grades.\n * **Partial order from the top:** $d > b > \\text{others}$ (not meaning that others are equivalent, like in a weak order, but simply that the voter did not want to rank them). Or **partial order from the bottom:** $c < a < \\text{others}$ (same remark). These ballots are naturally represented by lists, but not with the same meaning, so it will be difficult to treat them correctly in the above functions!" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T07:12:43.477049Z", | |
"end_time": "2022-04-12T07:12:43.484052Z" | |
}, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Drawbacks (continued):\n\n \n* Imagine that I want to apply my code to a case where **candidates are not strings**, but sets... Consider preferences like $\\{a, c\\} > \\{a, b\\} > \\{b, c\\}$, represented by ``[{'a', 'c'}, {'a', 'b'}, {'b', 'c'}]``. My code will fail because it will think it means $a \\sim c > a \\sim b > b \\sim c$, which makes no sense!" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:01.318949Z", | |
"end_time": "2022-04-12T08:17:01.328958Z" | |
}, | |
"trusted": true | |
}, | |
"cell_type": "code", | |
"source": "ballot_to_str([{'a', 'c'}, {'a', 'b'}, {'b', 'c'}])", | |
"execution_count": 7, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 7, | |
"data": { | |
"text/plain": "'a ~ c > a ~ b > b ~ c'" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "5e7ec17f", | |
"cell_type": "markdown", | |
"source": "All these problems are easily solved if you choose the option with **classes**.\n\nNow, let us see how to do that exactly!" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "429c71cc", | |
"cell_type": "markdown", | |
"source": "### Who is a subclass of who?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "ffcad7f3", | |
"cell_type": "markdown", | |
"source": "For the moment, just consider **BallotGrades** and **BallotOrder** (representing orders that may be weak).\n\nWhich one should be the parent class? The child class? Your arguments?\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "9829b93c", | |
"cell_type": "markdown", | |
"source": "Some (more or less good) arguments...\n\n**Option 1:** Parent = BallotGrades, child = BallotOrder.\n\n* Internally, an order $d \\sim b > a > c$ can be represented by grades: ``{'d': 2, 'b': 2, 'a': 1, 'c': 0}``. Hence it can be a subclass of BallotGrades.\n* Ballots with an order give \"less information\" than grades, so it is a subset of ballots with grades, hence it should be a subclass.\n\n**Option 2:** Parent = BallotOrder, child = BallotGrades.\n\n* A ballot with grades $d \\to 10, b \\to 7, a \\to 1, c \\to 0$ naturally induces a (possibly weak) order $d > b > a > c$. Grades being more specific, they should be the child class.\n\nWhich one would you recommend? Why? ..." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"end_time": "2022-02-22T08:34:58.949124Z", | |
"start_time": "2022-02-22T08:34:58.937164Z" | |
}, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "eb377b5d", | |
"cell_type": "markdown", | |
"source": "Let us expand on option 1: parent = BallotGrades, child = BallotOrder." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:19.658121Z", | |
"end_time": "2022-04-12T08:17:19.666131Z" | |
}, | |
"trusted": true | |
}, | |
"id": "33833623", | |
"cell_type": "code", | |
"source": "class BallotGrades:\n def __init__(self, d_candidate_grade):\n self.d_candidate_grade = d_candidate_grade\nclass BallotOrder(BallotGrades):\n def __init__(self, equivalence_classes):\n self.equivalence_classes = [\n equiv_class if isinstance(equiv_class, set) else {equiv_class}\n for equiv_class in equivalence_classes\n ]\n d_candidate_grade = {\n c: len(self.equivalence_classes) - i - 1\n for i, equiv_class in enumerate(self.equivalence_classes)\n for c in equiv_class\n }\n super().__init__(d_candidate_grade)", | |
"execution_count": 8, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:19.932806Z", | |
"end_time": "2022-04-12T08:17:19.945987Z" | |
}, | |
"trusted": true | |
}, | |
"id": "b8bc71b4", | |
"cell_type": "code", | |
"source": "ballot = BallotOrder([{'d', 'b'}, 'a', 'c'])\nballot.d_candidate_grade", | |
"execution_count": 9, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 9, | |
"data": { | |
"text/plain": "{'b': 2, 'd': 2, 'a': 1, 'c': 0}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Now let us implement Range voting." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:21.805993Z", | |
"end_time": "2022-04-12T08:17:21.812978Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "ab444682", | |
"cell_type": "code", | |
"source": "from collections import Counter\ndef range_voting(ballots):\n scores = Counter()\n for ballot in ballots:\n scores.update(ballot.d_candidate_grade)\n return dict(scores)", | |
"execution_count": 10, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:22.081799Z", | |
"end_time": "2022-04-12T08:17:22.092796Z" | |
}, | |
"trusted": true | |
}, | |
"id": "c01a7395", | |
"cell_type": "code", | |
"source": "range_voting(ballots=[\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0}),\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0})\n])", | |
"execution_count": 11, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 11, | |
"data": { | |
"text/plain": "{'d': 20, 'b': 14, 'a': 2, 'c': 0}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:22.330609Z", | |
"end_time": "2022-04-12T08:17:22.341619Z" | |
}, | |
"trusted": true | |
}, | |
"id": "51941b36", | |
"cell_type": "code", | |
"source": "range_voting(ballots=[\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0}),\n BallotOrder([{'d', 'b'}, 'a', 'c'])\n])", | |
"execution_count": 12, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 12, | |
"data": { | |
"text/plain": "{'d': 12, 'b': 9, 'a': 2, 'c': 0}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"cell_type": "markdown", | |
"source": "Is it reasonable? What should be the result in your opinion? ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "(Reminder from last slide:)" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "-" | |
}, | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:25.222712Z", | |
"end_time": "2022-04-12T08:17:25.239703Z" | |
}, | |
"trusted": true | |
}, | |
"cell_type": "code", | |
"source": "range_voting(ballots=[\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0}),\n BallotOrder([{'d', 'b'}, 'a', 'c'])\n])", | |
"execution_count": 13, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 13, | |
"data": { | |
"text/plain": "{'d': 12, 'b': 9, 'a': 2, 'c': 0}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "e173ee1a", | |
"cell_type": "markdown", | |
"source": "This is **bad** because the result relies on an \"implementation detail\" of BallotOrder.\n\nThe code above would better **raise an error**." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "d921d516", | |
"cell_type": "markdown", | |
"source": "Now let us expand on option 2: parent = BallotOrder, child = BallotGrades." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:28.321769Z", | |
"end_time": "2022-04-12T08:17:28.341029Z" | |
}, | |
"trusted": true | |
}, | |
"id": "a6182302", | |
"cell_type": "code", | |
"source": "class BallotOrder:\n def __init__(self, equivalence_classes):\n self.equivalence_classes = [\n equiv_class if isinstance(equiv_class, set) else {equiv_class}\n for equiv_class in equivalence_classes\n ]\n @property\n def top_candidate(self):\n if not self.equivalence_classes or len(self.equivalence_classes[0]) > 1:\n return None\n return list(self.equivalence_classes[0])[0]\nclass BallotGrades(BallotOrder):\n def __init__(self, d_candidate_grade):\n self.d_candidate_grade = d_candidate_grade\n self.equivalence_classes = [\n {k for k in self.d_candidate_grade.keys() if self.d_candidate_grade[k] == v}\n for v in sorted(set(self.d_candidate_grade.values()), reverse=True)\n ]", | |
"execution_count": 14, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
}, | |
"ExecuteTime": { | |
"start_time": "2022-04-12T07:28:15.173426Z", | |
"end_time": "2022-04-12T07:28:15.180435Z" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Now let us implement Plurality voting." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:30.745300Z", | |
"end_time": "2022-04-12T08:17:30.749301Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "56c9a2e2", | |
"cell_type": "code", | |
"source": "from collections import defaultdict\ndef plurality_voting(ballots):\n scores = defaultdict(int)\n for ballot in ballots:\n top_candidate = ballot.top_candidate\n if top_candidate is not None:\n scores[top_candidate] += 1\n return dict(scores)", | |
"execution_count": 15, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:30.986814Z", | |
"end_time": "2022-04-12T08:17:30.997813Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "3b85d5cf", | |
"cell_type": "code", | |
"source": "plurality_voting([\n BallotOrder([{'a', 'b'}, 'c', 'd']),\n BallotOrder(['c', {'a', 'b'}, 'd']),\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0})\n])", | |
"execution_count": 16, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 16, | |
"data": { | |
"text/plain": "{'c': 1, 'd': 1}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "4b0a9b07", | |
"cell_type": "markdown", | |
"source": "Is is reasonable? What should be the result in your opinion? ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Let us test Range voting (we did not change the implementation):" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:33.124269Z", | |
"end_time": "2022-04-12T08:17:33.208179Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "74440f04", | |
"cell_type": "code", | |
"source": "range_voting(ballots=[\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0}),\n BallotOrder([{'d', 'b'}, 'a', 'c'])\n])", | |
"execution_count": 17, | |
"outputs": [ | |
{ | |
"output_type": "error", | |
"ename": "AttributeError", | |
"evalue": "'BallotOrder' object has no attribute 'd_candidate_grade'", | |
"traceback": [ | |
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", | |
"\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", | |
"\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_2932/1091531290.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m range_voting(ballots=[\n\u001b[0m\u001b[0;32m 2\u001b[0m \u001b[0mBallotGrades\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m'd'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m10\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'b'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m7\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'a'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'c'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[0mBallotOrder\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m'd'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'b'\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'a'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'c'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 4\u001b[0m ])\n", | |
"\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_2932/2117891113.py\u001b[0m in \u001b[0;36mrange_voting\u001b[1;34m(ballots)\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[0mscores\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mCounter\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mballot\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mballots\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mscores\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mballot\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0md_candidate_grade\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 6\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mdict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mscores\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", | |
"\u001b[1;31mAttributeError\u001b[0m: 'BallotOrder' object has no attribute 'd_candidate_grade'" | |
] | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"id": "0db3bf78", | |
"cell_type": "markdown", | |
"source": "Is it reasonable? ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "122c7acd", | |
"cell_type": "markdown", | |
"source": "Remark: for implementation reasons, it can be useful to have a attribute ``_d_candidate_pseudograde`` in BallotOrder." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:41.377054Z", | |
"end_time": "2022-04-12T08:17:41.387751Z" | |
}, | |
"trusted": true | |
}, | |
"id": "c2b3df68", | |
"cell_type": "code", | |
"source": "from functools import cached_property\nclass BallotOrder:\n def __init__(self, equivalence_classes):\n self.equivalence_classes = [\n equiv_class if isinstance(equiv_class, set) else {equiv_class}\n for equiv_class in equivalence_classes\n ]\n @cached_property\n def _d_candidate_pseudograde(self):\n return {\n c: len(self.equivalence_classes) - i - 1\n for i, equiv_class in enumerate(self.equivalence_classes)\n for c in equiv_class\n }\n def strictly_prefers(self, c, d):\n return self._d_candidate_pseudograde[c] > self._d_candidate_pseudograde[d]\nclass BallotGrades(BallotOrder):\n def __init__(self, d_candidate_grade):\n self.d_candidate_grade = d_candidate_grade\n self.equivalence_classes = [\n {k for k in self.d_candidate_grade.keys() if self.d_candidate_grade[k] == v}\n for v in sorted(set(self.d_candidate_grade.values()), reverse=True)\n ]\n @cached_property\n def _d_candidate_pseudograde(self):\n return self.d_candidate_grade", | |
"execution_count": 18, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Examples that (indirectly) rely on these pseudogrades:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:43.815351Z", | |
"end_time": "2022-04-12T08:17:43.823349Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "8ec2a71d", | |
"cell_type": "code", | |
"source": "ballot = BallotOrder([{'d', 'b'}, 'a', 'c'])\nballot.strictly_prefers('d', 'a')", | |
"execution_count": 19, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 19, | |
"data": { | |
"text/plain": "True" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:44.015548Z", | |
"end_time": "2022-04-12T08:17:44.025531Z" | |
}, | |
"trusted": true | |
}, | |
"id": "215f8783", | |
"cell_type": "code", | |
"source": "ballot = BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0})\nballot.strictly_prefers('d', 'a')", | |
"execution_count": 20, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 20, | |
"data": { | |
"text/plain": "True" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "20c521dd", | |
"cell_type": "markdown", | |
"source": "Remark that it is an \"implementation detail\", hence it is not part of the API." | |
}, | |
{ | |
"metadata": {}, | |
"id": "9cc4fa97", | |
"cell_type": "markdown", | |
"source": "Final remark: actually, we could choose \"standard\" pseudogrades (like Borda scores), and make it part of the API. Instead of ``ballot._d_candidate_pseudograde``, we would then have ``ballot.d_candidate_borda_score`` (with no leading underscore ``_``)." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "88e11180", | |
"cell_type": "markdown", | |
"source": "### What about linear orders?" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "506b09fe", | |
"cell_type": "markdown", | |
"source": "Reminder: parent class = BallotOrder, child class = BallotGrades. A **linear order** is a particular case of BallotOrder, where all equivalence classes are of size 1.\n\n* Should it be a subclass of BallotOrder?\n* Should it be a subclass of BallotGrades?\n* Other ideas of design?\n\nYour answers and ideas:\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "75dd76d1", | |
"cell_type": "markdown", | |
"source": "**Option 1:** subclass of BallotOrder (and whether subclass of BallotGrades or not)." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:50.228515Z", | |
"end_time": "2022-04-12T08:17:50.238524Z" | |
}, | |
"trusted": true | |
}, | |
"id": "f192ae44", | |
"cell_type": "code", | |
"source": "class BallotOrder:\n def __init__(self, equivalence_classes):\n self.equivalence_classes = [\n equiv_class if isinstance(equiv_class, set) else {equiv_class}\n for equiv_class in equivalence_classes\n ]\nclass BallotLinearOrder(BallotOrder):\n def __init__(self, ranking):\n self.ranking = ranking\n equivalence_classes = [{candidate} for candidate in ranking]\n super().__init__(equivalence_classes=equivalence_classes)", | |
"execution_count": 21, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:50.440061Z", | |
"end_time": "2022-04-12T08:17:50.454180Z" | |
}, | |
"trusted": true | |
}, | |
"id": "2c4ba886", | |
"cell_type": "code", | |
"source": "ballot = BallotLinearOrder(['d', 'b', 'a', 'c'])\nballot.equivalence_classes", | |
"execution_count": 22, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 22, | |
"data": { | |
"text/plain": "[{'d'}, {'b'}, {'a'}, {'c'}]" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Let us implement a positional scoring rule (assigning weights to each rank):" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:52.913340Z", | |
"end_time": "2022-04-12T08:17:52.920562Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "2e89c548", | |
"cell_type": "code", | |
"source": "def positional_scoring_rule(ballots, weights):\n scores = defaultdict(int)\n for ballot in ballots:\n for candidate, weight in zip(ballot.ranking, weights):\n scores[candidate] += weight\n return dict(scores)", | |
"execution_count": 23, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:53.095129Z", | |
"end_time": "2022-04-12T08:17:53.106407Z" | |
}, | |
"trusted": true | |
}, | |
"id": "d6bc2402", | |
"cell_type": "code", | |
"source": "positional_scoring_rule(ballots=[\n BallotLinearOrder(['d', 'b', 'a', 'c']),\n BallotLinearOrder(['c', 'a', 'd', 'b'])\n], weights=[4, 2, 1, 0])", | |
"execution_count": 24, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 24, | |
"data": { | |
"text/plain": "{'d': 5, 'b': 2, 'a': 3, 'c': 4}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": {}, | |
"cell_type": "markdown", | |
"source": "Is it reasonable? What should be the result? ..." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:17:59.781213Z", | |
"end_time": "2022-04-12T08:17:59.800208Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "f363e975", | |
"cell_type": "code", | |
"source": "positional_scoring_rule(ballots=[\n BallotLinearOrder(['d', 'b', 'a', 'c']),\n BallotOrder(['c', 'a', 'd', 'b'])\n], weights=[4, 2, 1, 0])", | |
"execution_count": 25, | |
"outputs": [ | |
{ | |
"output_type": "error", | |
"ename": "AttributeError", | |
"evalue": "'BallotOrder' object has no attribute 'ranking'", | |
"traceback": [ | |
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", | |
"\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", | |
"\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_2932/3280533472.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m positional_scoring_rule(ballots=[\n\u001b[0m\u001b[0;32m 2\u001b[0m \u001b[0mBallotLinearOrder\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'd'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'b'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'a'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'c'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[0mBallotOrder\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'c'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'a'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'd'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'b'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 4\u001b[0m ], weights=[4, 2, 1, 0])\n", | |
"\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_2932/3415144726.py\u001b[0m in \u001b[0;36mpositional_scoring_rule\u001b[1;34m(ballots, weights)\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[0mscores\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mdefaultdict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mint\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mballot\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mballots\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 4\u001b[1;33m \u001b[1;32mfor\u001b[0m \u001b[0mcandidate\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mweight\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mzip\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mballot\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mranking\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 5\u001b[0m \u001b[0mscores\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcandidate\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m+=\u001b[0m \u001b[0mweight\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mdict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mscores\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", | |
"\u001b[1;31mAttributeError\u001b[0m: 'BallotOrder' object has no attribute 'ranking'" | |
] | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T07:46:42.590613Z", | |
"end_time": "2022-04-12T07:46:42.600612Z" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Is it reasonable? What should be the result? ..." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:18:02.460668Z", | |
"end_time": "2022-04-12T08:18:02.471664Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "fcfea548", | |
"cell_type": "code", | |
"source": "positional_scoring_rule(ballots=[\n BallotLinearOrder(['d', 'b', 'a', 'c']),\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0})\n], weights=[4, 2, 1, 0])", | |
"execution_count": 26, | |
"outputs": [ | |
{ | |
"output_type": "error", | |
"ename": "AttributeError", | |
"evalue": "'BallotGrades' object has no attribute 'ranking'", | |
"traceback": [ | |
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", | |
"\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", | |
"\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_2932/1625634434.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m positional_scoring_rule(ballots=[\n\u001b[0m\u001b[0;32m 2\u001b[0m \u001b[0mBallotLinearOrder\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'd'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'b'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'a'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'c'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[0mBallotGrades\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m{\u001b[0m\u001b[1;34m'd'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m10\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'b'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m7\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'a'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'c'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 4\u001b[0m ], weights=[4, 2, 1, 0])\n", | |
"\u001b[1;32m~\\AppData\\Local\\Temp/ipykernel_2932/3415144726.py\u001b[0m in \u001b[0;36mpositional_scoring_rule\u001b[1;34m(ballots, weights)\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[0mscores\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mdefaultdict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mint\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mballot\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mballots\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 4\u001b[1;33m \u001b[1;32mfor\u001b[0m \u001b[0mcandidate\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mweight\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mzip\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mballot\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mranking\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 5\u001b[0m \u001b[0mscores\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcandidate\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m+=\u001b[0m \u001b[0mweight\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mdict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mscores\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", | |
"\u001b[1;31mAttributeError\u001b[0m: 'BallotGrades' object has no attribute 'ranking'" | |
] | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T07:46:42.590613Z", | |
"end_time": "2022-04-12T07:46:42.600612Z" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Is it reasonable? What should be the result? ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "How to solve the problems we just saw? Your ideas:\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "c4a94caa", | |
"cell_type": "markdown", | |
"source": "**Option 2:** being a strict linear order is just something that we can test about a BallotOrder." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:18:18.108424Z", | |
"end_time": "2022-04-12T08:18:18.123373Z" | |
}, | |
"trusted": true | |
}, | |
"id": "e652a134", | |
"cell_type": "code", | |
"source": "class BallotOrder:\n def __init__(self, equivalence_classes):\n self.equivalence_classes = [\n equiv_class if isinstance(equiv_class, set) else {equiv_class}\n for equiv_class in equivalence_classes\n ]\n @property\n def is_strict(self):\n return all([len(equiv_class) == 1 for equiv_class in self.equivalence_classes])\n @property\n def ranking(self):\n if not self.is_strict:\n raise ValueError('The order is not strict.')\n return [list(equiv_class)[0] for equiv_class in self.equivalence_classes]\nclass BallotGrades(BallotOrder):\n def __init__(self, d_candidate_grade):\n self.d_candidate_grade = d_candidate_grade\n self.equivalence_classes = [\n {k for k in self.d_candidate_grade.keys() if self.d_candidate_grade[k] == v}\n for v in sorted(set(self.d_candidate_grade.values()), reverse=True)\n ]", | |
"execution_count": 27, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Example of application:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:18:20.465012Z", | |
"end_time": "2022-04-12T08:18:20.484008Z" | |
}, | |
"trusted": true | |
}, | |
"id": "9f2a7f3e", | |
"cell_type": "code", | |
"source": "positional_scoring_rule(ballots=[\n BallotOrder(['c', 'a', 'd', 'b']),\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0})\n], weights=[4, 2, 1, 0])", | |
"execution_count": 28, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 28, | |
"data": { | |
"text/plain": "{'c': 4, 'a': 3, 'd': 5, 'b': 2}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T07:46:42.590613Z", | |
"end_time": "2022-04-12T07:46:42.600612Z" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Is it reasonable? What should be the result? ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"id": "bfd64d45", | |
"cell_type": "markdown", | |
"source": "## Voting rules" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "What kind of architecture would you consider for the voting rules? Your ideas:\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "9dd1b98e", | |
"cell_type": "markdown", | |
"source": "**Option 1:** design the voting rules as functions.\n\n* Input: a list of (relevant) ballots.\n* Output: Winner? Scores? Ranking of all the candidates?\n\nWhat do you think?\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "b59948c5", | |
"cell_type": "markdown", | |
"source": "**Suboption 1:** return the scores, since it conveys more information." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:18:29.682094Z", | |
"end_time": "2022-04-12T08:18:29.693086Z" | |
}, | |
"trusted": true | |
}, | |
"id": "9c8c519c", | |
"cell_type": "code", | |
"source": "def some_scoring_rule(ballots):\n scores = dict()\n # Some code...\n return scores\ndef winner(scores):\n # We break ties using the alphabetical order on candidates.\n return sorted(candidate for candidate, score in scores.items() if score == max(scores.values()))[0]\ndef ranking(scores):\n # For the sake of simplicity, we do not care about ties here.\n return sorted(scores.keys(), key=scores.__getitem__, reverse=True)", | |
"execution_count": 29, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:18:29.890184Z", | |
"end_time": "2022-04-12T08:18:29.894182Z" | |
}, | |
"trusted": true | |
}, | |
"id": "a1e4fc07", | |
"cell_type": "code", | |
"source": "scores = {'a': 4, 'b': 2, 'c': 12, 'd': 12}\nwinner(scores)", | |
"execution_count": 30, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 30, | |
"data": { | |
"text/plain": "'c'" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:18:30.075433Z", | |
"end_time": "2022-04-12T08:18:30.080438Z" | |
}, | |
"trusted": true | |
}, | |
"id": "222817ee", | |
"cell_type": "code", | |
"source": "ranking(scores)", | |
"execution_count": 31, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 31, | |
"data": { | |
"text/plain": "['c', 'd', 'a', 'b']" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "924858c5", | |
"cell_type": "markdown", | |
"source": "But not all voting rules rely on a score! Output the winner in that case?" | |
}, | |
{ | |
"metadata": { | |
"trusted": true, | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:18:46.097569Z", | |
"end_time": "2022-04-12T08:18:46.105569Z" | |
} | |
}, | |
"id": "82d26d7f", | |
"cell_type": "code", | |
"source": "def some_non_scoring_rule(ballots):\n winner = None\n # Compute the winner...\n return winner", | |
"execution_count": 32, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "087e0f7c", | |
"cell_type": "markdown", | |
"source": "$\\Rightarrow$ inconsistency in the code." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "fbfc9d18", | |
"cell_type": "markdown", | |
"source": "**Suboption 2:** return the winner, because a voting rule must always be able to do that." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:18:50.771476Z", | |
"end_time": "2022-04-12T08:18:50.777467Z" | |
}, | |
"trusted": true | |
}, | |
"id": "130c547d", | |
"cell_type": "code", | |
"source": "def some_voting_rule(ballots):\n winner = None\n # 1. Compute the scores, since we need them anyway.\n # 2. Deduce the winner...\n return winner", | |
"execution_count": 33, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "be47df14", | |
"cell_type": "markdown", | |
"source": "This is stupid, because we do not have access to the scores!" | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "843a5776", | |
"cell_type": "markdown", | |
"source": "**Suboption 3:** return a dictionary (or a tuple), with all the relevant information." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:02.500889Z", | |
"end_time": "2022-04-12T08:19:02.511830Z" | |
}, | |
"trusted": true | |
}, | |
"id": "88552ed7", | |
"cell_type": "code", | |
"source": "def some_voting_rule(ballots):\n scores = dict()\n winner = None\n # 1. Compute the scores, since we need them anyway.\n # 2. Deduce the winner...\n return {'scores': scores, 'winner': winner}", | |
"execution_count": 34, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"trusted": true, | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:02.700558Z", | |
"end_time": "2022-04-12T08:19:02.714010Z" | |
} | |
}, | |
"id": "0122283a", | |
"cell_type": "code", | |
"source": "def some_non_scoring_rule(ballots):\n winner = None\n # Compute the winner\n return {'winner': winner}", | |
"execution_count": 35, | |
"outputs": [] | |
}, | |
{ | |
"metadata": {}, | |
"id": "f22689ae", | |
"cell_type": "markdown", | |
"source": "This is less bad, but not ideal: for example, we lose auto-completion, there are risks of typos, etc. Your ideas of drawbacks?\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "d1a31da2", | |
"cell_type": "markdown", | |
"source": "**Suboption 4:** return an object of a custom class, with all the relevant information." | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:07.236945Z", | |
"end_time": "2022-04-12T08:19:07.245946Z" | |
}, | |
"trusted": true | |
}, | |
"id": "28d860fb", | |
"cell_type": "code", | |
"source": "class ElectionResult:\n def __init__(self, winner=None, scores=None, ranking=None):\n self._winner = winner\n self._scores = scores\n self._ranking = ranking\n @cached_property\n def scores(self):\n if self._scores is not None: return self._scores\n raise ValueError('No scores')\n @cached_property\n def ranking(self):\n if self._ranking is not None: return self._ranking\n if self._scores is not None:\n return sorted(self._scores.keys(), key=self._scores.__getitem__, reverse=True)\n raise ValueError('No ranking')\n @cached_property\n def winner(self):\n if self._winner is not None: return self._winner\n if self._ranking is not None: return self._ranking[0]\n if self._scores is not None:\n return sorted(\n candidate \n for candidate, score in self._scores.items() \n if score == max(self._scores.values())\n )[0]\n raise ValueError('No winner')", | |
"execution_count": 36, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
}, | |
"ExecuteTime": { | |
"start_time": "2022-04-12T07:59:59.113958Z", | |
"end_time": "2022-04-12T07:59:59.128954Z" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Let us check that it has the desired behavior:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:11.307812Z", | |
"end_time": "2022-04-12T08:19:11.316747Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "-" | |
} | |
}, | |
"id": "8ebb5254", | |
"cell_type": "code", | |
"source": "result = ElectionResult(scores={'a': 4, 'b': 2, 'c': 12, 'd': 12})\nprint(f\"{result.scores=}\")\nprint(f\"{result.ranking=}\")\nprint(f\"{result.winner=}\")", | |
"execution_count": 37, | |
"outputs": [ | |
{ | |
"output_type": "stream", | |
"text": "result.scores={'a': 4, 'b': 2, 'c': 12, 'd': 12}\nresult.ranking=['c', 'd', 'a', 'b']\nresult.winner='c'\n", | |
"name": "stdout" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:11.480561Z", | |
"end_time": "2022-04-12T08:19:11.489472Z" | |
}, | |
"trusted": true | |
}, | |
"id": "acfb8695", | |
"cell_type": "code", | |
"source": "result = ElectionResult(ranking=['c', 'd', 'a', 'b'])\nprint(f\"{result.ranking=}\")\nprint(f\"{result.winner=}\")", | |
"execution_count": 38, | |
"outputs": [ | |
{ | |
"output_type": "stream", | |
"text": "result.ranking=['c', 'd', 'a', 'b']\nresult.winner='c'\n", | |
"name": "stdout" | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Let us examine the implementation of voting rules with this design:" | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:15.170040Z", | |
"end_time": "2022-04-12T08:19:15.183034Z" | |
}, | |
"trusted": true | |
}, | |
"id": "55e59dbe", | |
"cell_type": "code", | |
"source": "def some_scoring_rule(ballots):\n scores = dict()\n # Compute the scores here...\n return ElectionResult(scores=scores)", | |
"execution_count": 39, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:15.360897Z", | |
"end_time": "2022-04-12T08:19:15.370091Z" | |
}, | |
"trusted": true | |
}, | |
"id": "bb30663b", | |
"cell_type": "code", | |
"source": "def some_non_scoring_rule(ballots):\n ranking = []\n # Compute the ranking here...\n return ElectionResult(ranking=ranking)", | |
"execution_count": 40, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "Pros and cons of the design with ElectionResult? Your ideas:\n\n* ...\n* ...\n* ..." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "f78df191", | |
"cell_type": "markdown", | |
"source": "**Option 2:** classes for algorithms (my personal preference).\n\nCf. my talk in the Python Workshop: [\"Some Architectural Considerations for Algorithms in Python\"](https://www.lincs.fr/events/some-architectural-considerations-for-algorithms-in-python/)." | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
}, | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:22.772696Z", | |
"end_time": "2022-04-12T08:19:22.786986Z" | |
}, | |
"trusted": true | |
}, | |
"cell_type": "code", | |
"source": "def _cache(f):\n \"\"\"Auxiliary decorator used by :meth:`cached_property`.\n\n Parameters\n ----------\n f : callable\n A method with no argument (except ``self``).\n\n Returns\n -------\n callable\n The same function, but with a `caching` behavior.\n \"\"\"\n name = f.__name__\n\n def _f(*args):\n try:\n return args[0]._cached_properties[name]\n except (KeyError, AttributeError):\n value = f(*args)\n try:\n # Not stored in cache\n args[0]._cached_properties[name] = value\n except AttributeError:\n # Cache does not even exist\n args[0]._cached_properties = {name: value}\n return value\n _f.__doc__ = f.__doc__\n return _f", | |
"execution_count": 41, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "subslide" | |
}, | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:24.337314Z", | |
"end_time": "2022-04-12T08:19:24.348596Z" | |
}, | |
"trusted": true | |
}, | |
"cell_type": "code", | |
"source": "def cached_property(f):\n \"\"\"Decorator used in replacement of ``@property`` to put the value in cache automatically.\n\n Notes\n -----\n The first time the attribute is used, it is computed on-demand and put in cache. Later accesses to the\n attributes will use the cached value.\n\n Examples\n --------\n Cf. :class:`DeleteCacheMixin`.\n \"\"\"\n return property(_cache(f))", | |
"execution_count": 42, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:25.893433Z", | |
"end_time": "2022-04-12T08:19:25.906439Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "d1730dbf", | |
"cell_type": "code", | |
"source": "class DeleteCacheMixin:\n \"\"\"Mixin used to delete cached properties.\n\n Notes\n -----\n Cf. decorator :meth:`cached_property`.\n\n Examples\n --------\n >>> class Example(DeleteCacheMixin):\n ... @cached_property\n ... def x(self):\n ... print('Big computation...')\n ... return 6 * 7\n >>> a = Example()\n >>> a.x\n Big computation...\n 42\n >>> a.x\n 42\n >>> a.delete_cache()\n >>> a.x\n Big computation...\n 42\n \"\"\"\n def delete_cache(self) -> None:\n self._cached_properties = dict()", | |
"execution_count": 43, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:28.367118Z", | |
"end_time": "2022-04-12T08:19:28.380581Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "71a9b81c", | |
"cell_type": "code", | |
"source": "class Rule(DeleteCacheMixin):\n def __call__(self, ballots):\n self.delete_cache()\n self.ballots_ = ballots\n return self\n @cached_property\n def winner_(self):\n raise NotImplementedError", | |
"execution_count": 44, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:29.780732Z", | |
"end_time": "2022-04-12T08:19:29.794733Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "c022bb0a", | |
"cell_type": "code", | |
"source": "class RuleScore(Rule):\n @cached_property\n def scores_(self):\n raise NotImplementedError\n @cached_property\n def winner_(self):\n return sorted(\n candidate \n for candidate, score in self.scores_.items() \n if score == max(self.scores_.values())\n )[0]", | |
"execution_count": 45, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:31.656917Z", | |
"end_time": "2022-04-12T08:19:31.665212Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "9ae8b0ab", | |
"cell_type": "code", | |
"source": "class RuleRangeVoting(RuleScore):\n @cached_property\n def scores_(self):\n scores = Counter()\n for ballot in self.ballots_:\n scores.update(ballot.d_candidate_grade)\n return dict(scores)", | |
"execution_count": 46, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:33.540201Z", | |
"end_time": "2022-04-12T08:19:33.558763Z" | |
}, | |
"trusted": true, | |
"slideshow": { | |
"slide_type": "subslide" | |
} | |
}, | |
"id": "354269a6", | |
"cell_type": "code", | |
"source": "election = RuleRangeVoting()(ballots=[\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0}),\n BallotGrades({'d': 10, 'b': 7, 'a': 1, 'c': 0})\n])", | |
"execution_count": 48, | |
"outputs": [] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:33.783335Z", | |
"end_time": "2022-04-12T08:19:33.791676Z" | |
}, | |
"trusted": true | |
}, | |
"id": "a32f4cea", | |
"cell_type": "code", | |
"source": "election.scores_", | |
"execution_count": 49, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 49, | |
"data": { | |
"text/plain": "{'d': 20, 'b': 14, 'a': 2, 'c': 0}" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"ExecuteTime": { | |
"start_time": "2022-04-12T08:19:34.233924Z", | |
"end_time": "2022-04-12T08:19:34.238953Z" | |
}, | |
"trusted": true | |
}, | |
"id": "bdb8beba", | |
"cell_type": "code", | |
"source": "election.winner_", | |
"execution_count": 50, | |
"outputs": [ | |
{ | |
"output_type": "execute_result", | |
"execution_count": 50, | |
"data": { | |
"text/plain": "'d'" | |
}, | |
"metadata": {} | |
} | |
] | |
}, | |
{ | |
"metadata": { | |
"slideshow": { | |
"slide_type": "slide" | |
} | |
}, | |
"cell_type": "markdown", | |
"source": "## Conclusion" | |
}, | |
{ | |
"metadata": {}, | |
"cell_type": "markdown", | |
"source": "Thank you for your attention!" | |
}, | |
{ | |
"metadata": {}, | |
"cell_type": "markdown", | |
"source": "Questions?" | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"name": "python3", | |
"display_name": "Python 3 (ipykernel)", | |
"language": "python" | |
}, | |
"language_info": { | |
"name": "python", | |
"version": "3.9.7", | |
"mimetype": "text/x-python", | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"pygments_lexer": "ipython3", | |
"nbconvert_exporter": "python", | |
"file_extension": ".py" | |
}, | |
"toc": { | |
"nav_menu": {}, | |
"number_sections": true, | |
"sideBar": true, | |
"skip_h1_title": true, | |
"base_numbering": 1, | |
"title_cell": "Table of Contents", | |
"title_sidebar": "Contents", | |
"toc_cell": false, | |
"toc_position": { | |
"height": "calc(100% - 180px)", | |
"width": "279.263px", | |
"left": "10px", | |
"top": "150px" | |
}, | |
"toc_section_display": true, | |
"toc_window_display": false | |
}, | |
"celltoolbar": "Diaporama", | |
"gist": { | |
"id": "", | |
"data": { | |
"description": "architecture_in_oop.ipynb", | |
"public": true | |
} | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 5 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment