From 81c0819fae4ee232c79a92d60aa50256b8b516b3 Mon Sep 17 00:00:00 2001 From: Neil Smith Date: Mon, 17 Sep 2018 12:56:42 +0100 Subject: [PATCH] Task 0 in Python --- src/task0/task0.ipynb | 124 +++++ src/task0/worked-solution.ipynb | 779 ++++++++++++++++++++++++++++++++ 2 files changed, 903 insertions(+) create mode 100644 src/task0/task0.ipynb create mode 100644 src/task0/worked-solution.ipynb diff --git a/src/task0/task0.ipynb b/src/task0/task0.ipynb new file mode 100644 index 0000000..bbc23cc --- /dev/null +++ b/src/task0/task0.ipynb @@ -0,0 +1,124 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import collections" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import the names, splitting the lines into individual names." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "invites = [line.strip().split(', ') for line in open('../../data/00-invites.txt')]\n", + "len(invites)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Find the line with most names" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max(len(invite) for invite in invites)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a `Counter` from the flattended list of list of names. Then count how many items are in the `Counter` with a count of more than 1." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "457" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "invite_counts = collections.Counter(name for invite in invites for name in invite)\n", + "sum(1 for name in invite_counts if invite_counts[name] > 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/task0/worked-solution.ipynb b/src/task0/worked-solution.ipynb new file mode 100644 index 0000000..7f595b5 --- /dev/null +++ b/src/task0/worked-solution.ipynb @@ -0,0 +1,779 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Summer of Code: Problem 0 worked solution\n", + "\n", + "How to go about solving this problem? This post is a worked example of how to go about it.\n", + "\n", + "If you've not done so already, [have a look at the problem](https://learn2.open.ac.uk/mod/quiz/view.php?id=1352295).\n", + "\n", + "In summary, we're given an input file that looks like this…\n", + "```\n", + "Tilly, Daisy-May, Tori\n", + "Iona, Deniz, Kobe, Grayson, Luka\n", + "Demi, Reanne, Tori\n", + "Dafydd, Reanne, Rohit, Kai, Iona, Nojus\n", + "Tommy, Rosa, Demi\n", + "Daisy-May, Tilly, Grayson, Deniz, Kobe, Tommy, Rohit\n", + "Sultan, Iona, Dafydd, Rosa, Kobe, Devan\n", + "Tilly, Rohit, Tori, Deniz, Kobe, Jennie\n", + "Luka, Tori, Tommy\n", + "Kobe, Rosa, Demi\n", + "```\n", + "…and have to solve some tasks with it. \n", + "\n", + "I'll implement this solution in Python 3, but the ideas should translate across to any other programming language. \n", + "\n", + "# Part 1\n", + "I'll only think about the first task first. I'll deal with the second part once I've got the first part working.\n", + "\n", + "In summary, the task is to find the maximum number of names on any one line.\n", + "\n", + "## Where to start\n", + "Let's think about the overall shape of a solution to this task: the **algorithm**. We're given a text file of invitations and we have to read it. Once we've read the file, we have to split it into lines and then split each line into names, and then do something about counting names on a line.\n", + "\n", + "So, the subtasks are:\n", + "1. Read the text file.\n", + "2. Split the file into lines.\n", + "3. For each line, split the line into names.\n", + "4. Count the number of names on each line.\n", + "5. Find the highest count.\n", + "\n", + "That doesn't seem anything particularly complicated. There's lots of repetition, but no really complex logic to deal with. \n", + "\n", + "Next, let's think about the **data structures** we'll need. I find that often, once the data structures are sorted, the rest of the program follows. When choosing data structures, I find it helpful to think about what's stored and what operations we need to perform on that data. \n", + "\n", + "In this case, we need to store a bunch of lines and a bunch of names within each line.\n", + "\n", + "We don't do anything with a _name_ apart from count it, so storing it as a simple `string` should be sufficient.\n", + "\n", + "A _line_ is a group of names. All we need to do is count how many names are in a line, so a simple container should do. Let's keep it simple and just just a `list` of names (a `list` of `string`s).\n", + "\n", + "We also need to store the whole set of lines. We'll call that the _invitations_. All we need to do is process all the lines in the invitations to get the answer. Again, a simple container such as a `list` of lines will do. \n", + "\n", + "Let's now take each of these subtasks in turn." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Steps 1 and 2: read a text file\n", + "In Python, the `open()` built-in command reads a file. By default, it reads the file as text. It also conveniently allows us to process the file line-by-line. The standard Python idiom for this is:\n", + "\n", + "```\n", + "for line in open('path/to/file'):\n", + " # do some processing\n", + "```\n", + "As this datafile is in a strange place, store the path in a variable for reuse." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "invitation_filename = '../../data/00-invites.txt'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, Python leaves the trailing newline character on the end of each line. We can get rid of that with the `str.strip()` method.\n", + "\n", + "We also need to create the _invitations_ to store the lines in. This starts as an empty list, and we `append()` lines to it as they're read.\n", + "\n", + "Note that the line-as-read is just a long `string` of all the names." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the empty list\n", + "invitation_lines = []\n", + "\n", + "# Iterate over all the lines in the file\n", + "for line in open(invitation_filename):\n", + " invitation_lines.append(line.strip())\n", + " \n", + "# Finish by saying how many lines we've read\n", + "len(invitation_lines)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: split the invitation lines into names\n", + "Now we have the list of invitation lines, we can process each line and split it into names. Luckily for us, the built-in `str.split()` method does this for us. It works like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Tom', 'Dick', 'Harry']" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "'Tom, Dick, Harry'.split(', ')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This splits a string into parts, using the given delimiter. \n", + "\n", + "We can use that to `split` each instruction line into names. We follow the same overall pattern as above, creating an empty list lf `invitations` and `append`ing to it as we process each line." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(200,\n", + " ['Caprice',\n", + " 'Marlene',\n", + " 'Carlie',\n", + " 'Fatema',\n", + " 'Glyn',\n", + " 'Kaycee',\n", + " 'Ainsley',\n", + " 'Cloe',\n", + " 'Zunaira',\n", + " 'Tyrell',\n", + " 'Annaliese',\n", + " 'Ameera',\n", + " 'Darrell',\n", + " 'Caiden',\n", + " 'Reyansh',\n", + " 'Oran',\n", + " 'Ebonie',\n", + " 'Corben',\n", + " 'Dionne',\n", + " 'Dafydd',\n", + " 'Harrison',\n", + " 'Mikolaj',\n", + " 'Tommy',\n", + " 'Marley',\n", + " 'Crystal',\n", + " 'Aryan',\n", + " 'Sebastian',\n", + " 'Xena'])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the empty list\n", + "invitations = []\n", + "\n", + "# Iterate over all the invitations\n", + "for line in invitation_lines:\n", + " invitations.append(line.split(', '))\n", + " \n", + "# Finish by saying how many lines we've processed, and the first split invitation\n", + "len(invitations), invitations[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: count the names in each line\n", + "The built-in `len()` function counts the number of items in a list. As we have a bunch of name lists, we can use `len()` to count the number of names in each line. \n", + "\n", + "Again, we'll use the same pattern of iterating over all the lines in the invitations." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[28, 13, 20, 20, 22]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the empty list\n", + "invitation_lengths = []\n", + "\n", + "# Iterate over all the invitations\n", + "for line in invitations:\n", + " invitation_lengths.append(len(line))\n", + " \n", + "# Finish by giving the first few counts\n", + "invitation_lengths[:5]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5: find the highest count\n", + "There's a couple of ways we can go here. The built-in `max()` function, when passed a list of numbers, finds the maximum value in that list. That means the solution is easy:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max(invitation_lengths)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But there's a more general pattern for when `max()` isn't applicable. It uses an _accumulator_ which starts off at some initial value and is updated as each item is processed. The accumulator could be something like a running total when going through a list of amounts. In this case, the accumulator is the most names we've seen on a line so far. If we see a longer line than any we've seen before, we update the most names accumulator.\n", + "\n", + "That gives this pattern:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Set the accumulator to some initial value\n", + "most_names = 0\n", + "\n", + "# Iterate over all the invitation lengths\n", + "for name_count in invitation_lengths:\n", + " if name_count > most_names:\n", + " most_names = name_count\n", + "\n", + "# Finish by giving the highest name\n", + "most_names" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Refactoring\n", + "We have a working solution, but it's rather repetitive. Can we make it simpler? In particular, do we really need to walk over the list of invitations so many times?\n", + "\n", + "_Refactoring_ is the process of tidying up and simplifiying an already-working solution into something better.\n", + "\n", + "Here's the solution we have." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the empty list\n", + "invitation_lines = []\n", + "\n", + "# Iterate over all the lines in the file\n", + "for line in open(invitation_filename):\n", + " invitation_lines.append(line.strip())\n", + "\n", + "invitations = []\n", + "\n", + "# Iterate over all the invitations\n", + "for line in invitation_lines:\n", + " invitations.append(line.split(', '))\n", + " \n", + "invitation_lengths = []\n", + "\n", + "# Iterate over all the invitations\n", + "for line in invitations:\n", + " invitation_lengths.append(len(line))\n", + " \n", + "# Give the solution\n", + "max(invitation_lengths)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, each stage involves cycling over the list of all invitations. As each stage just transforms individual lines, we can do all of the operations within one loop." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the empty list\n", + "invitation_lengths = []\n", + "\n", + "# Iterate over all the lines in the file\n", + "for line in open(invitation_filename):\n", + " stripped_line = line.strip()\n", + " names = stripped_line.split(', ')\n", + " name_count = len(names)\n", + " invitation_lengths.append(name_count)\n", + "\n", + "# Give the solution\n", + "max(invitation_lengths)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we include the \"accumulator\" version of finding the longest line, we don't need to store any of the invitation lines at all, like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Set the accumulator to some initial value\n", + "most_names = 0\n", + "\n", + "# Iterate over all the lines in the file\n", + "for line in open(invitation_filename):\n", + " stripped_line = line.strip()\n", + " names = stripped_line.split(', ')\n", + " name_count = len(names)\n", + " if name_count > most_names:\n", + " most_names = name_count\n", + "\n", + "# Finish by giving the highest name\n", + "most_names" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## A more Pythonic approach\n", + "If you're interested, a more \"Pythonic\" approach (using idiomatic Python) to this problem is to use _list comprehensions_ rather than lots of `append` calls. \n", + "\n", + "We can find `invitation_lines` like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "invitation_lines = [line.strip() for line in open(invitation_filename)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "and then find the `invitations` from `invitation_lines`" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "invitations = [line.split(', ') for line in invitation_lines]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or even combine the two steps in one call:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "invitations = [line.strip().split(', ') for line in open(invitation_filename)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From `invitations`, we can find the number of names on each line with another comprehension:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "invitation_lengths = [len(names) for names in invitations]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "and then find the maximum length:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max(invitation_lengths)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you really want to go to town, you can combine all these steps into a one-liner:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max(len(line.strip().split(', ')) for line in open(invitation_filename))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Part 2\n", + "Now to look at the second part. This is a bit more complex. We have to _count_ the number of times each name appears anywhere in the file, and then count how many names appear more than once.\n", + "\n", + "The first chunk of the algorithm is the same as before. In summary, my approach is:\n", + "\n", + "So, the subtasks are:\n", + "1. Read the text file.\n", + "2. Split the file into lines.\n", + "3. For each line, split the line into names.\n", + "4. _Something something count names in whole input._\n", + "5. Count how many names appear more than once.\n", + "\n", + "The first three steps are the same as before.\n", + "\n", + "Note the detail about step 4. \n", + "\n", + "Perhaps things will be clearer if we think about the **data structure** for keeping track of the counts of all the names.\n", + "\n", + "What we want is a data structure that holds a bunch of names, and associates each name with the number of times we've seen that name so far. When we first see a name in the file, we want to include it in the data structure. When we see a name _again_, we want to incremement the number times we've seen it.\n", + "\n", + "In subtask 5, we can look at that data structure and pick out all the names with a count of two or more.\n", + "\n", + "This association of a _name_ and a _number_ is a classic use of a _key-value store_, which crops up all over computing. In this case, the name is the key and the number of occurrences is the value. A simple built-in key-value store is called a `dict` in Python, a `Hash` in Ruby, and a `Map` in Java (and an `object` in JavaScript, but… well, [JavaScript](https://www.destroyallsoftware.com/talks/wat)).\n", + "\n", + "That seems like a useful thing to use. Going back to the problem, we have the updated list of subtasks:\n", + "\n", + "1. Read the text file.\n", + "2. Split the file into lines.\n", + "3. For each line, split the line into names.\n", + "4. For each name in the input:\n", + " 1. If it's in the name counts, increase its count by 1\n", + " 2. Otherwise, include the name in the name counts with a count of 1\n", + "5. Count how many names appear more than once.\n", + "\n", + "This is a slightly more complex algorithm as it includes both loops and conditionals, but it's not too bad.\n", + "\n", + "We can reuse the first three subtasks from Part 1, and assume the `invitations` list-of-lists-of-names already exists. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4\n", + "When we implement step 4 using the logic above, the Python code looks like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "# Empty dict\n", + "name_counts = {}\n", + "\n", + "# for each name in the input\n", + "for invite in invitations:\n", + " for name in invite:\n", + " # record how many times we've now seen this name\n", + " if name in name_counts:\n", + " name_counts[name] += 1\n", + " else:\n", + " name_counts[name] = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you squint, you can see something like the \"accumulator\" pattern again, with the `name_counts` `dict` being the accumulator.\n", + "\n", + "(If you want a more Pythonic approach, take a look at the [`defaultdict`](https://docs.python.org/3/library/collections.html#defaultdict-objects) and [`Counter`](https://docs.python.org/3/library/collections.html#counter-objects) objects in the [`collections`](https://docs.python.org/3/library/collections.html) standard library.)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5\n", + "Talking of accumulators, we use that pattern again to do step 5, but this time we're iterating over the `name_counts` rather than the input. In this case, the accumulator of the number of people invited. " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "457" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "number_invited = 0\n", + "for name in name_counts:\n", + " if name_counts[name] > 1:\n", + " number_invited += 1\n", + " \n", + "number_invited" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## An alternative way\n", + "There's another way of solving this task. As we only need to record the names we've seen twice or more, we can walk over the input building that list of invitiations directly. \n", + "\n", + "The trick is to maintain two lists of names: one is the list of names to invite, and one is a list of names we've seen in the input. \n", + "\n", + "When we see a name for the first time, we add it to the list of seen names. When we see a name again, it's already in the seen names so we know to add it ot the list of invited people. \n", + "\n", + "In both cases, we don't want to add a name to either list if it's already there. That implementation gets easier if we use `set`s, which don't allow duplicate items. That means we don't use `list`s for `seen` and `invited`, but use `set`s instead. \n", + "\n", + "That gives the algorithm of:\n", + "1. For each name in the input\n", + " 1. If the name is in the seen set\n", + " 1. Add it to the invited set\n", + " 2. Add the name to the seen set\n", + " \n", + "Note that the order of these steps is important: we want to check for inviting a person _before_ recording that we've seen them at all." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "457" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# create empty sets\n", + "seen = set() # all the names we've seen\n", + "invited = set() # the people to invite\n", + "\n", + "# for each name in the input\n", + "for invite in invitations:\n", + " for name in invite:\n", + " # invite this person if we've seen them before\n", + " if name in seen:\n", + " invited.add(name)\n", + " # record that we've now seen this person\n", + " seen.add(name)\n", + " \n", + "len(invited)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Which is better?\n", + "Which of these two ways is better? It depends a lot on what you mean by \"better\". \n", + "\n", + "The approach with the `dict` of counts is perhaps easier to follow as a _process_. It's also more easily extendable to other conditions and tasks, such as different thresholds for getting an invite or finding the most popular person (i.e. the person who was mentioned the most).\n", + "\n", + "The approach with `set`s of names is perhaps closer to a description of the _conditions_ the solution must fulfil: `invited` is the `set` of names we've `seen` and `seen` again. But it's less flexible: how would you use that approach to ensure you only invited people who were mentioned at least three times, or ten times?\n", + "\n", + "But as a learning exercise, the main thing is seeing many different ways of solving problems so you have a range of alternatives to choose from when you come across fresh challenges." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} -- 2.34.1