Skip to content

Instantly share code, notes, and snippets.

@francois-durand
Created April 12, 2022 08:23
Show Gist options
  • Save francois-durand/c022fb2001bdbeeb023dcbfb8bcbcfb5 to your computer and use it in GitHub Desktop.
Save francois-durand/c022fb2001bdbeeb023dcbfb8bcbcfb5 to your computer and use it in GitHub Desktop.
architecture_in_oop.ipynb
Display the source blob
Display the rendered blob
Raw
{
"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![image.png](attachment:image.png)",
"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