{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Plotly ternary contour plot" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "2d point position can be given in a cartesian, polar or barycentric system of coordinates.\n", "Ternary plots work in barycentric coordinates.\n", "\n", "Given a triangle of vertices $V_k$, $k=0, 1, 2$, indexed such that the sequence $V_0, V_1, V_2$ is run in counter-clockwise\n", "direction, like in the figure below, then each 2D point, $P$, can be expressed as a barycentric combination of the vertices:\n", "$$P=aV_0+bV_1+cV_2,$$ with $a+b+c=1$. The scalars $a, b, c$ are called barycentric coordinates of the point P\n", "with respect to the given triangle." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\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", "\n", "\n", "\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.display import SVG\n", "SVG(filename='Data/triangle.svg')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All points within the triangular region, bounded by the reference triangle, have positive barycentric coordinates, i.e.\n", "$a, b, c\\geq 0$, $a+b+c=1$.\n", "\n", "A ternary plot is a scatter plot of n points in a triangular region, represented by their barycentric coordinates with respect to that triangle.\n", "\n", "\n", "Ternary plot is used to represent triplets of values of\n", " three dependent positive variables $A, B, C$, whose sum is a constant, $k$. Scaling each variable we get $\\displaystyle\\frac{1}{k}A+\\displaystyle\\frac{1}{k}B+\\displaystyle\\frac{1}{k}C=1$.\n", "Hence n normalized triplets interpreted as values of a variable $Y=\\left(\\displaystyle\\frac{1}{k}A, \\displaystyle\\frac{1}{k}B, \\displaystyle\\frac{1}{k}C\\right)$ can be interpreted as barycentric coordinates with respect to a triangle, and can be represented by points in a triangular region, i.e. as a scatter ternary plot. In most cases the constant $k$ is 100, and a, b, c represent the percent of parts in a composition of three elements.\n", "\n", "The reference triangle in a ternary plot is, by convention, an equilateral triangle. \n", "\n", "\n", "If the triangle vertices, $V_k$, have the cartesian coordinates $(x_k, y_k)$, $k=0, 1, 2$, and a point $P$ has the barycentric coordinates $(a, b, c)$, then its cartesian coordinates, $P(x,y)$, are derived from this relation:\n", " $$\\left[\\begin{array}{c} x\\\\y\\\\1\\end{array}\\right ]= \\left(\\begin{array}{ccc}x_0&x_1&x_2\\\\y_0&y_1&y_2\\\\1&1&1\\end{array}\\right)\\left[\\begin{array}{c} a\\\\b\\\\c\\end{array}\\right ]$$\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A ternary contour plot is the contour plot of a function $z=f(a, b, c)$, $a, b, c \\geq 0$, $a+b+c=1$, \n", "i.e. a function of positive barycentric coordinates." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Plotly provides a `scatterternary` trace, but a ternary contour trace that maps z-values to a continuous colorscale is not introduced yet." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Task:\n", "\n", "*Given n points of barycentric coordinates, (a, b, c), with respect to an equilateral triangle of vertices $V_k(x_k, y_k)$, $k=0, 1, 2$, and n values\n", "in a list or array, z, extract data for a Plotly ternary contour plot or heatmap*. \n", "\n", "Since these Plotly traces work only in cartesian coordinates\n", "we proceed as follows:\n", "\n", "- compute the cartesian coordinates, (x, y), of the given points from their barycentric coordinates, via the above tranformation;\n", "\n", "- define a meshgrid on the rectangle $[min(x), max(x)] \\times [min(y), max(y)]$;\n", "\n", "- interpolate data (x, y; z) and evaluate the interpolatory function at the meshgrid points to get an array, `grid_z`;\n", "\n", "- compute the barycentric coordinates of the meshgrid points, and associate a `nan` value to `grid_z` where at least one of these barycentric coordinates is negative (i.e. insert `nan` in the position of points that are outside the reference triangle).\n", "\n", "- define a contour type trace from `grid_z`" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Python version: 3.6.4\n", "Plotly version: 3.4.0\n" ] } ], "source": [ "import platform\n", "import plotly\n", "print(f'Python version: {platform.python_version()}')\n", "print(f'Plotly version: {plotly.__version__}')" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "import plotly.graph_objs as go\n", "import numpy as np\n", "from scipy.interpolate import griddata" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define data for a ternary contour plot:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "A = np.array([0, .3 ,.25, .34 ,0, .4 ,.65, 0.05, 0, 1, .47, .2, .7]) #pos 10 1, 0, 0\n", "B = np.array([1, .1, .45, .56, 0, .5, .3, 0.75, .85, 0, .33, .3, .13])\n", "C = np.array([0, .6 ,.3, .1, 1, .1, .05, .2, .15, 0, .2, .5, .17])\n", "\n", "z=np.array([1.27036107, 1.27893858, 0.52255697, 1.50035059, 0.84853798,\n", " 1.27722501, 1.20920733, 0.88965008, 0.59293362, 0.9223051 ,\n", " 1.57173859, 1.33606612, 1.08977333])\n", "A+B+C" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Note** that to get a full triangle contour plot the initial data must contain the vertex points and values z at these points.\n", "Otherwise the contour plot will be generated in a subregion of the triangular one!!!!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To get acquainted with Plotly ternary axes we first plot the points $(a_j, b_j, c_j)$, with $a_j \\in A, b_j\\in B, c_j\\in C$, $j\\in\\{0, 1, \\ldots, 12\\}$:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "f15d7a595e1446fd84ca860d7390d39f", "version_major": 2, "version_minor": 0 }, "text/plain": [ "FigureWidget({\n", " 'data': [{'a': array([0. , 0.3 , 0.25, 0.34, 0. , 0.4 , 0.65, 0.05, 0. , 1. , 0.47, 0.2…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "pl_ternary=dict(type='scatterternary',\n", " a=A,\n", " b=B, \n", " c=C,\n", " mode='markers',\n", " marker=dict(size=10, color='red'))\n", "\n", "layout=dict(width=500, height=400,\n", " ternary= {'sum':1,\n", " 'aaxis':{'title': 'a', 'min': 0.001, 'linewidth':0.5, 'ticks':'outside' },\n", " 'baxis':{'title': 'b', 'min': 0.001, 'linewidth':0.5, 'ticks':'outside' },\n", " 'caxis':{'title': 'c', 'min': 0.001, 'linewidth':0.5, 'ticks':'outside' }},\n", " showlegend= False,\n", " paper_bgcolor='#EBF0F8')\n", "\n", "fw=go.FigureWidget(data=[pl_ternary], layout=layout)\n", "fw #this plot is visible only when the notebook is run" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The triangle sides opposite to the vertices marked by a, b, respectively c, are the lines of 0 barycentric coordinate: a=0, b=0, respectively c=0.\n", "Each parallel to such a side has a constant a, b, respectively c coordinate. Notice that the ticks are drawn correspondingly. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Functions that define elements for plotting a ternary contour plot" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def tr_b2c2b():\n", " # returns the transformation matrix from barycentric to cartesian coordinates and conversely\n", " tri_verts = np.array([[0.5, np.sqrt(3)/2], [0, 0], [1, 0]])# reference triangle\n", " M = np.array([tri_verts[:,0], tri_verts[:, 1], np.ones(3)]) \n", " return M, np.linalg.inv(M) " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def contour_trace(x, y, z, tooltip, \n", " colorscale='Viridis', reversescale=False,\n", " linewidth=0.5, linecolor='rgb(150,150,150)'):\n", " \n", " return dict(type='contour',\n", " x=x,\n", " y=y, \n", " z=z,\n", " text=tooltip,\n", " hoverinfo='text', \n", " colorscale=colorscale,\n", " reversescale=reversescale,\n", " line=dict(width=linewidth, color=linecolor),\n", " colorbar=dict(thickness=20, ticklen=4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As outlined above, our ternary contour is plotted in cartesian coordinates, hence we cannot use the Plotly ternary \n", "layout defined for `scatterternary` trace.\n", "We have to define the tick positions and directions in a cartesian system.\n", "\n", "The next two functions return the barycentric coordinates of tick starting points, respectively the lists of x, and y-coordinates of points that defines the ticks, as well as the position of ticklabels:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def barycentric_ticks(side):\n", " # side 0, 1 or 2; side j has 0 in the j^th position of barycentric coords of tick origin\n", " # returns the list of tick origin barycentric coords\n", " p = 10\n", " if side == 0: #where a=0\n", " return np.array([(0, j/p, 1-j/p) for j in range(p-2, 0, -2)])\n", " elif side == 1: # b=0\n", " return np.array([(i/p, 0, 1-i/p) for i in range( 2, p, 2) ])\n", " elif side == 2: #c=0\n", " return np.array([(i/p, j/p, 0) for i in range(p-2, 0, -2) for j in range(p-i, -1, -1) if i+j==p])\n", " else:\n", " raise ValueError('The side can be only 0, 1, 2')\n", "\n", "\n", "\n", "def cart_coord_ticks(side, t=0.01):\n", " # side 0, 1 or 2\n", " # each tick segment is parameterized as (x(s), y(s)), s in [0, t]\n", " global M, xt, yt, posx, posy\n", " # M is the transformation matrix from barycentric to cartesian coords\n", " # xt, yt are the lists of x, resp y-coords of tick segments\n", " # posx, posy are the lists of ticklabel positions for side 0, 1, 2 (concatenated)\n", " \n", " baryc = barycentric_ticks(side)\n", " xy1 = np.dot(M, baryc.T)\n", " xs, ys = xy1[:2] \n", " \n", " if side == 0:\n", " for i in range(4):\n", " xt.extend([xs[i], xs[i]+t, None])\n", " yt.extend([ys[i], ys[i]-np.sqrt(3)*t, None])\n", " posx.extend([xs[i]+t for i in range(4)])\n", " posy.extend([ys[i]-np.sqrt(3)*t for i in range(4)])\n", " \n", " elif side == 1:\n", " for i in range(4):\n", " xt.extend([xs[i], xs[i]+t, None])\n", " yt.extend([ys[i], ys[i]+np.sqrt(3)*t, None])\n", " posx.extend([xs[i]+t for i in range(4)]) \n", " posy.extend([ys[i]+np.sqrt(3)*t for i in range(4)])\n", " \n", " elif side == 2:\n", " for i in range(4):\n", " xt.extend([xs[i], xs[i]-2*t, None])\n", " yt.extend([ys[i], ys[i], None])\n", " posx.extend([xs[i]-2*t for i in range(4)])\n", " posy.extend([ys[i] for i in range(4)]) \n", " else:\n", " raise ValueError('side can be only 0,1,2')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Layout definition:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def ternary_layout(title='Ternary contour plot', width=550, height=525, \n", " fontfamily= 'Balto, sans-serif' , lfontsize=14,\n", " plot_bgcolor='rgb(240,240,240)',\n", " vertex_text=['a', 'b', 'c'], v_fontsize=14):\n", "\n", " return dict(title=title,\n", " font=dict(family=fontfamily, size=lfontsize),\n", " width=width, height=height,\n", " xaxis=dict(visible=False),\n", " yaxis=dict(visible=False),\n", " plot_bgcolor=plot_bgcolor,\n", " showlegend=False,\n", " #annotations for strings placed at the triangle vertices\n", " annotations=[dict(showarrow=False,\n", " text=vertex_text[0],\n", " x=0.5,\n", " y=np.sqrt(3)/2,\n", " align='center',\n", " xanchor='center',\n", " yanchor='bottom',\n", " font=dict(size=v_fontsize)),\n", " dict(showarrow=False,\n", " text=vertex_text[1],\n", " x=0,\n", " y=0,\n", " align='left',\n", " xanchor='right',\n", " yanchor='top',\n", " font=dict(size=v_fontsize)),\n", " dict(showarrow=False,\n", " text=vertex_text[2],\n", " x=1,\n", " y=0,\n", " align='right',\n", " xanchor='left',\n", " yanchor='top',\n", " font=dict(size=v_fontsize))\n", " ])\n", "\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def set_ticklabels(annotations, posx, posy, proportion=True):\n", " #annotations: list of annotations previously defined in layout definition as a dict,\n", " # not as an instance of go.Layout\n", " #posx, posy: lists containing ticklabel position coordinates\n", " #proportion - boolean; True when ticklabels are 0.2, 0.4, ... False when they are 20%, 40%...\n", " \n", " if not isinstance(annotations, list):\n", " raise ValueError('annotations should be a list')\n", " \n", " ticklabel = [0.8, 0.6, 0.4, 0.2] if proportion else ['80%', '60%', '40%', '20%'] \n", " \n", " annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 0\n", " text=f'{ticklabel[j]}',\n", " x=posx[j],\n", " y=posy[j],\n", " align='center',\n", " xanchor='center', \n", " yanchor='top',\n", " font=dict(size=12)) for j in range(4)])\n", " \n", " annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 1\n", " text=f'{ticklabel[j]}',\n", " x=posx[j+4],\n", " y=posy[j+4],\n", " align='center',\n", " xanchor='left', \n", " yanchor='middle',\n", " font=dict(size=12)) for j in range(4)])\n", "\n", " annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 2\n", " text=f'{ticklabel[j]}',\n", " x=posx[j+8],\n", " y=posy[j+8],\n", " align='center',\n", " xanchor='right', \n", " yanchor='middle',\n", " font=dict(size=12)) for j in range(4)])\n", " return annotations" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def styling_traces():\n", " global xt, yt\n", " side_trace = dict(type='scatter',\n", " x=[0.5, 0, 1, 0.5],\n", " y=[np.sqrt(3)/2, 0, 0, np.sqrt(3)/2],\n", " mode='lines',\n", " line=dict(width=2, color='#444444'),\n", " hoverinfo='none')\n", " \n", " tick_trace = dict(type='scatter',\n", " x=xt,\n", " y=yt,\n", " mode='lines',\n", " line=dict(width=1, color='#444444'),\n", " hoverinfo='none')\n", " \n", " return side_trace, tick_trace" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Extract and process data for a ternary contour plot" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "M, invM = tr_b2c2b()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Convert the barycentric coordinates of data points, (a, b, c), to cartesian coordinates:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "cartes_coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C)))\n", "xx, yy = cartes_coord_points[:2]" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "a, b = xx.min(), xx.max()\n", "c, d = yy.min(), yy.max()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define a meshgrid on the rectangle [a,b] x [c,d]:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "N=150\n", "gr_x = np.linspace(a,b, N)\n", "gr_y = np.linspace(c,d, N)\n", "grid_x, grid_y = np.meshgrid(gr_x, gr_y)\n", "\n", "#interpolate data (cartes_coords[:2].T; z) and evaluate the interpolatory function at the meshgrid points to get grid_z\n", "grid_z = griddata(cartes_coord_points[:2].T, z, (grid_x, grid_y), method='cubic')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Compute the barycentric coordinates of meshgrid points:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "bar_coords = np.einsum('ik, kmn -> imn', invM, np.stack((grid_x, grid_y, np.ones(grid_x.shape))))\n", "bar_coords[np.where(bar_coords<0)] = None # invalidate the points outside of the reference triangle\n", "xy1 = np.einsum('ik, kmn -> imn', M, bar_coords) # recompute back the cartesian coordinates of bar_coords with invalid positions\n", " # and extract indices where x are nan\n", "\n", "I = np.where(np.isnan(xy1[0]))\n", "grid_z[I] = None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define the hover text for proportions and percents, i.e. when a hovered point displays a, b, c in [0,1], respectively in [0,100]:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "# tooltips for proportions, i.e. a+b+c=1\n", "\n", "t_proportions = [[f'a: {round(bar_coords[0][i,j], 2)}
b: {round(bar_coords[1][i,j], 2)}'+\\\n", " f'
c: {round(1-round(bar_coords[0][i,j], 2)-round(bar_coords[1][i,j], 2), 2)}'+\\\n", " f'
z: {round(grid_z[i,j],2)}' if ~np.isnan(xy1[0][i,j]) else '' for j in range(N)]\n", " for i in range(N)] \n", "\n", "# tooltips for percents, i.e. a+b+c=100\n", "t_percents=[[f'a: {int(100*bar_coords[0][i,j]+0.5)}
b: {int(100*bar_coords[1][i,j]+0.5)}'+\\\n", " f'
c: {100-int(100*bar_coords[0][i,j]+0.5)-int(100*bar_coords[1][i,j]+0.5)}'+\\\n", " f'
z: {round(grid_z[i,j],2)}' if ~np.isnan(xy1[0][i,j]) else '' for j in range(N)] \n", " for i in range(N)] " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Colorscale for contour:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "pl_deep = [[0.0, 'rgb(253, 253, 204)'],\n", " [0.1, 'rgb(201, 235, 177)'],\n", " [0.2, 'rgb(145, 216, 163)'],\n", " [0.3, 'rgb(102, 194, 163)'],\n", " [0.4, 'rgb(81, 168, 162)'],\n", " [0.5, 'rgb(72, 141, 157)'],\n", " [0.6, 'rgb(64, 117, 152)'],\n", " [0.7, 'rgb(61, 90, 146)'],\n", " [0.8, 'rgb(65, 64, 123)'],\n", " [0.9, 'rgb(55, 44, 80)'],\n", " [1.0, 'rgb(39, 26, 44)']]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ternary contour plot that displays proportions" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "xt = []\n", "yt = []\n", "posx = []\n", "posy = []\n", "for side in [0, 1, 2]:\n", " cart_coord_ticks(side, t=0.01)\n", "\n", "\n", "tooltip = t_proportions\n", "layout = ternary_layout()\n", "annotations = set_ticklabels(layout['annotations'], posx, posy, proportion=True)\n", "\n", "c_trace = contour_trace(gr_x, gr_y, grid_z, tooltip, colorscale=pl_deep, reversescale=True)\n", "side_trace, tick_trace =styling_traces()\n", "fw1 = go.FigureWidget(data=[c_trace, tick_trace, side_trace], layout=layout)\n", "fw1.layout.annotations=annotations\n", " \n", "#fw1" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import plotly.plotly as py\n", "py.sign_in('empet', 'api_key')\n", "py.iplot(fw1, filename='ternary1_cont')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ternary contour plot that displays percents" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Note**: When we associate the ternary contour plot to variables, A, B, C, with A+B+C=100, we have to scale each one by \n", " $1./100$, because the code works with barycentric coordinates. In this case however we can pass the tooltips corresponding to percents and plot the percents as ticklabels:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "xt = []\n", "yt = []\n", "posx = []\n", "posy = []\n", "for side in [0, 1, 2]:\n", " cart_coord_ticks(side, t=0.01)# set xt, yt posx, posy for this side\n", "\n", "\n", "tooltip = t_percents\n", "layout = ternary_layout(title='Ternary contour plot that displays percents')\n", "annotations = set_ticklabels(layout['annotations'], posx, posy, proportion=False)\n", "\n", "c_trace = contour_trace(gr_x, gr_y, grid_z, tooltip, colorscale=pl_deep, reversescale=True)\n", "side_trace, tick_trace = styling_traces()\n", "fw2 = go.FigureWidget(data=[c_trace, tick_trace, side_trace], layout=layout)\n", "fw2.layout.annotations=annotations\n", "#fw2 " ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import plotly.plotly as py\n", "py.iplot(fw2, filename='ternary2_cont')" ] }, { "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.4" } }, "nbformat": 4, "nbformat_minor": 2 }