{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Python для сбора данных\n", "\n", "*Алла Тамбовцева, НИУ ВШЭ*\n", "\n", "## Краткое введение в массивы NumPy и датафреймы Pandas" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Массивы NumPy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сегодня мы познакомимся с библиотекой NumPy (сокращение от *Numeric Python*), которая часто используется в задачах, связанных с анализом данных и машинным обучением.\n", "\n", "Чтобы мы смогли на конкретных примерах увидеть, зачем эта библиотека используется, давайте ее импортируем. Если вы уже устанавливали Anaconda, то библиотека NumPy также была установлена на ваш компьютер. Проверим: импортируем библиотеку с сокращенным названием, так часто делают, чтобы не «таскать» за собой в коде длинное название. Сокращение `np` для библиотеки `numpy` – общепринятое, его часто можно увидеть в документации или официальных тьюториалах." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Основным объектом NumPy является *Ndarray* – это n-мерный массив (от *n-dimensional array*), структура данных, которая позволяет хранить набор элементов одного типа: либо целые числа, либо числа с плавающей точкой, либо строки, либо логические значения `True` и `False`. Массивы могут быть одномерными, то есть визуально ничем не отличаться от простого списка значений:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0, 2, 3, 4])" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.array([0, 2, 3, 4])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "А могут быть двумерными, то есть представлять собой таблицу, похожую на вложенный список или «список списков»):" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2],\n", " [1, 0]])" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.array([[1, 2], \n", " [1, 0]])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Массивы могут быть и большей размерности (список таблиц или что-то более объемное – вкладывать списки в списки мы можем довольно долго), но на практике они нужны редко. \n", "\n", "Зачем изучать массивы? Во-первых, с массивами гораздо приятнее работать, чем со списками, плюс, они занимают меньше памяти. Во-вторых, особенности массивов позволят нам лучше понять, как устроены столбцы в датафреймах (таблицах с данными), с которыми нам предстоит работать дальше.\n", "\n", "Для того, чтобы увидеть, почему массивы удобнее списков, рассмотрим такую задачу. У нас есть список `money_k`, который содержит некоторые суммы в кнатах (волшебная валюта)." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "money_k = [210, 265, 570, 120, 180, 194]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как получить новый список `money_s`, где те же суммы записаны в сиклях (1 сикль = 29 кнатов)? Либо создать пустой список и заполнить его через цикл for, либо использовать списковые включения (генераторы списков). Пойдем по второму пути:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[7.241379310344827,\n", " 9.137931034482758,\n", " 19.655172413793103,\n", " 4.137931034482759,\n", " 6.206896551724138,\n", " 6.689655172413793]" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "money_s = [i/29 for i in money_k] \n", "money_s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вроде бы быстро, но без цикла все равно не обошлось. Поступим проще – сделаем из списка массив:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([210, 265, 570, 120, 180, 194])" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Money_k = np.array([210, 265, 570, 120, 180, 194])\n", "Money_k" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "А теперь просто разделим его на 29:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 7.24137931, 9.13793103, 19.65517241, 4.13793103, 6.20689655,\n", " 6.68965517])" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Money_s = Money_k / 29\n", "Money_s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Почему такое возможно? Потому что подобные операции производятся поэлементно, то есть над каждым элементом массива в отдельности. Такие операции еще назвают *векторизованными*. То же будет работать и для нескольких массивов. Допустим, у нас есть два нюхлера (ниффлера), которые в течение 3 часов собирают монетки:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "Niff_one = np.array([83, 73, 65]) \n", "Niff_two = np.array([34, 56, 40])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посчитаем, сколько они насобирали вместе за каждый час:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([117, 129, 105])" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Niff_sum = Niff_one + Niff_two\n", "Niff_sum" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Довольно быстро и удобно! \n", "\n", "**Важно!** Запомните эту особенность массивов, нам она очень пригодится, когда будем работать с датафреймами pandas. Если мы решим сложить столбцы в таблице, они тоже будут складываться поэлементно." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Типы данных в массивах и преобразование типов\n", "\n", "Чуть раньше мы зафиксировали, что массивы могут состоять только из элементов одного типа. Посмотрим, что это за типы:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dtype('int64')" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# integer\n", "Niff_sum.dtype " ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dtype('float64')" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ " # float\n", "Money_s.dtype " ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dtype('bool')" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# boolean\n", "YN = np.array([True, False])\n", "YN.dtype " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Числа 64 или 32, дописанные в конце названия типа, зависят от системы (32-битная или 64-битная), на это можно не обращать внимания. А вот на что стоит обратить внимание, так это на то, что после `.dtype` нет круглых скобок. Раньше, когда мы дописывали что-то к объекту после точки, это «что-то» было методом (вспомните методы `.lower()` и `.capitalize()` на строках). Здесь `dtype` – это не метод, а *атрибут* массива, то есть какая-то его характеристика. \n", "\n", "Три типа рассмотрели, остались строки. Создадим массив со строками:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dtype(' 0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Неравенство выше было автоматически применено к каждому элементу массива, поэтому мы получили новый массив из `True` и `False`, которые сообщают нам, выполнено ли это условие для конкретного элемента или нет. Как посчитать число игроков, которые заработали больше 0 очков? Посчитать число `True`. А если учесть, что вместо `True` Python видит 1, а вместо `False` – 0? Посчитать сумму всех элементов массива:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "4" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(points > 10).sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "А как получить массив, в котором будут только те элементы `points`, которые удовлетворяют некоторому условию? Записать это условие в квадратных скобках, как раньше мы указывали индекс элемента:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([150, 20, 30, 20])" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "points[points > 10]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Запись выше означает, что из `points` Python должен выбрать те элементы, где `points > 10` возвращает `True`.\n", "\n", "Если условия сложные, то их нужно формулировать с помощью операторов `&` (одновременное выполнение условий) или `|` (хотя бы одно из условий верно). " ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([20, 20])" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "points[(points > 10) & (points < 30)] " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "«Словесные» операторы `and` и `or` здесь не подойдут. Плюс, всегда нужно ставить скобки вокруг каждой части условия, иначе Python начнет «раскручивать» условие со знаков `&` или `|`, что закончится ошибкой:" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "ename": "ValueError", "evalue": "The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpoints\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mpoints\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m10\u001b[0m \u001b[0;34m&\u001b[0m \u001b[0mpoints\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m30\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;31m# пытался сопоставить 10 и массив points\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mValueError\u001b[0m: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()" ] } ], "source": [ "points[points > 10 & points < 30] # пытался сопоставить 10 и массив points" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если нужны индексы элементов, удовлетворяющих условиям, можно воспользоваться методом `where`:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(array([0, 2, 4, 5]),)" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.where(points > 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Последовательности pandas Series" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь перейдем к объектам из библиотеки pandas. Библиотека pandas – библиотека для более удобной работы с данными в табличном виде (например, файлы Excel) или базами данных.\n", "\n", "Как и библиотека NumPy, библиотека pandas была загружена вместе с Anaconda. Импортируем библиотеку с сокращенным названием:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "import pandas as pd" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь рассмотрим объект, структуру данных, которая называется *Series* или последовательность pandas. Эта структура является своеобразным звеном между массивом и датафреймом (таблицей). Датафрейм pandas – это набор объектов типа *Series*, а *Series* – это один столбец в таблице.\n", "\n", "Создадим пустой *Series*:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Series([], dtype: float64)" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pd.Series() " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Series – объект, который связан с массивом NumPy и который наследует многие его атрибуты и методы. Так, у массива, как мы выяснили, были атрибуты `dtype` и `size`, значит, у *Series* тоже такие атрибуты будут. Мы могли привести массив одного типа к другому с помощью метода `.astype()`, значит, с *Series* сможем проделать то же самое. И так далее.\n", "\n", "Но объект *Series* похож не только на массив. Давайте создадим массив `Points` со значениями числа очков, которые набрали члены одной команды по квиддичу за одну игру:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0 150\n", "1 0\n", "2 20\n", "3 0\n", "4 30\n", "5 20\n", "6 0\n", "dtype: int64" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Points = pd.Series([150, 0, 20, 0, 30, 20, 0])\n", "Points" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На что похож `Points`? На словарь! На словарь, где ключами являются индесы строк, а значениями – сами значения в столбце. Чтобы стало совсем похоже на те словари, которые мы обсуждали, вместо абстрактных индексов строк добавим имена игроков. Добавим аргумент `index`, в котором перечислим новые названия строк:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "Points = pd.Series([150, 0, 20, 0, 30, 20, 0], \n", " index = [\"Harry\", \"Fred\", \"Alicia\", \n", " \"George\", \"Katie\", \"Angelina\", \"Oliver\"])" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Harry 150\n", "Fred 0\n", "Alicia 20\n", "George 0\n", "Katie 30\n", "Angelina 20\n", "Oliver 0\n", "dtype: int64" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Points" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "И, как у словаря, у *Series* есть атрибут `.values`:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([150, 0, 20, 0, 30, 20, 0])" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Points.values" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь перейдем к самому главному – к датафреймам (таблицам), которые создаются с помощью библиотеки pandas. Самый простой способ получить датафрейм – загрузить данные из файла csv или Excel и сохранить их как датафрейм. Однако мы начнем с обратной задачи: создадим датафрейм из более простой структуры в Python и выгрузим его в файл.\n", "\n", "Рассмотрим словарь `data_dict` с именами игроков, набранными ими очками и их ролью в команде:" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "data_dict = {\"Name\" : [\"Harry\", \"Fred\", \"Alicia\", \n", " \"George\", \"Katie\", \"Angelina\", \"Oliver\"],\n", " \"Score\" : [150, 0, 20, 0, 30, 20, 0],\n", " \"Status\": [\"seeker\", \"beater\", \"chaser\", \n", " \"beater\", \"chaser\", \"chaser\", \"keeper\"]}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Получим из него датафрейм `df`:" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
NameScoreStatus
0Harry150seeker
1Fred0beater
2Alicia20chaser
3George0beater
4Katie30chaser
5Angelina20chaser
6Oliver0keeper
\n", "
" ], "text/plain": [ " Name Score Status\n", "0 Harry 150 seeker\n", "1 Fred 0 beater\n", "2 Alicia 20 chaser\n", "3 George 0 beater\n", "4 Katie 30 chaser\n", "5 Angelina 20 chaser\n", "6 Oliver 0 keeper" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df = pd.DataFrame(data_dict)\n", "df" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь экспортируем полученный датафрейм в файл Excel и назовем этот файл `scores.xlsx`:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "df.to_excel(\"scores.xlsx\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Файл `scores.xlsx` автоматически был сохранен в рабочую папку. Вспомним, как проверить, какая папка является рабочей:" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'/Users/allat/Desktop'" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import os\n", "os.getcwd() # cwd – current working directory" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В моем случае файл `scores.xlsx` нужно искать в папке `Desktop`.\n", "\n", "Из списка списков тоже можно сделать датафрейм:" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [], "source": [ "L = [[150, 0, 20, 0, 30, 20, 0], \n", "[\"seeker\", \"beater\", \"chaser\", \n", "\"beater\", \"chaser\", \"chaser\", \"keeper\"]]" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
0123456
0150020030200
1seekerbeaterchaserbeaterchaserchaserkeeper
\n", "
" ], "text/plain": [ " 0 1 2 3 4 5 6\n", "0 150 0 20 0 30 20 0\n", "1 seeker beater chaser beater chaser chaser keeper" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pd.DataFrame(L)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Однако в таком случае значения будут записываться по строкам. Чтобы это поправить, можем транспонировать полученный датафрейм – поменять местами строки и столбцы:" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
01
0150seeker
10beater
220chaser
30beater
430chaser
520chaser
60keeper
\n", "
" ], "text/plain": [ " 0 1\n", "0 150 seeker\n", "1 0 beater\n", "2 20 chaser\n", "3 0 beater\n", "4 30 chaser\n", "5 20 chaser\n", "6 0 keeper" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pd.DataFrame(L).T # T – транспонирование" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Со списком кортежей будет та же история:" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [], "source": [ "L2 = [(150, 0, 20, 0, 30, 20, 0), \n", "(\"seeker\", \"beater\", \"chaser\", \n", "\"beater\", \"chaser\", \"chaser\", \"keeper\")]" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
01
0150seeker
10beater
220chaser
30beater
430chaser
520chaser
60keeper
\n", "
" ], "text/plain": [ " 0 1\n", "0 150 seeker\n", "1 0 beater\n", "2 20 chaser\n", "3 0 beater\n", "4 30 chaser\n", "5 20 chaser\n", "6 0 keeper" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pd.DataFrame(L2).T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Наконец, датафрейм можно получить из списка словарей:" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [], "source": [ "D = [{\"name\" : \"Anna\", \n", " \"age\" : 23},\n", " {\"name\" : \"Katie\", \n", " \"age\" : 23}]" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
nameage
0Anna23
1Katie23
\n", "
" ], "text/plain": [ " name age\n", "0 Anna 23\n", "1 Katie 23" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dat = pd.DataFrame(D)\n", "dat" ] }, { "cell_type": "markdown", "metadata": {}, "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.7.4" } }, "nbformat": 4, "nbformat_minor": 2 }