{"cells":[{"metadata":{},"cell_type":"markdown","source":"This kernel uses two approaches:\n - [Pandas-Bokeh](https://github.com/PatrikHlobil/Pandas-Bokeh) - if possible\n - [Native Bokeh](https://bokeh.pydata.org/en/latest/docs/user_guide.html) - otherwise"},{"metadata":{},"cell_type":"markdown","source":"## 1. Load libraries"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"#!pip install pandas-bokeh # must be turn on internet or install from settings\n\nimport pandas as pd\nimport numpy as np\nimport warnings\nwarnings.filterwarnings('ignore')\n\nfrom ipywidgets import (Checkbox, interactive, interactive_output, \n                        ToggleButton, IntSlider, VBox, HBox)\nimport ipywidgets as widgets\n\nimport pandas_bokeh\n#pandas_bokeh.output_notebook()\n\nfrom bokeh.models import (ColumnDataSource, HoverTool, Legend, \n                          LegendItem, LabelSet, Label, Panel, Tabs)\nfrom bokeh.io import show, output_notebook, push_notebook\noutput_notebook()\nfrom bokeh.plotting import figure\nfrom bokeh.layouts import layout, grid, gridplot\nfrom bokeh.transform import dodge\nfrom bokeh.core.properties import value\nfrom bokeh.models.widgets import Paragraph, Div","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 2. Load data"},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"train = pd.read_csv('../input/train.csv', index_col='id')\n# test = pd.read_csv('../input/test.csv', index_col='id')\nstructures = pd.read_csv('../input/structures.csv')\nscalar_coupling_contributions = pd.read_csv('../input/scalar_coupling_contributions.csv')\npotential_energy = pd.read_csv('../input/potential_energy.csv')\nmulliken_charges = pd.read_csv('../input/mulliken_charges.csv')\nmagnetic_shielding_tensors = pd.read_csv('../input/magnetic_shielding_tensors.csv')\ndipole_moments = pd.read_csv('../input/dipole_moments.csv')","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"_kg_hide-output":false,"trusted":true},"cell_type":"code","source":"def scalar_coupling_contributions_merge(df):\n    df = pd.merge(\n        df, \n        scalar_coupling_contributions, \n        how = 'left',\n        left_on  = ['molecule_name', 'atom_index_0', 'atom_index_1', 'type'],\n        right_on = ['molecule_name', 'atom_index_0', 'atom_index_1', 'type']\n    )\n    \n    return df\n\ndef structures_merge(df, atom_idx):\n    df = pd.merge(\n        df,\n        structures,\n        how = 'left',\n        left_on  = ['molecule_name', f'atom_index_{atom_idx}'],\n        right_on = ['molecule_name',  'atom_index']\n    )\n    \n    df = df.drop('atom_index', axis=1)\n    df = df.rename(columns={\n        'atom': f'struct_atom_{atom_idx}',\n        'x': f'struct_x_{atom_idx}',\n        'y': f'struct_y_{atom_idx}',\n        'z': f'struct_z_{atom_idx}'\n    })\n    \n    return df\n\ndef potential_energy_merge(df):\n    df = pd.merge(\n        df, \n        potential_energy, \n        how = 'left',\n        left_on  = ['molecule_name'],\n        right_on = ['molecule_name']\n    )\n    \n    return df\n\ndef mulliken_charges_merge(df, atom_idx):\n    df = pd.merge(\n        df,\n        mulliken_charges,\n        how = 'left',\n        left_on  = ['molecule_name', f'atom_index_{atom_idx}'],\n        right_on = ['molecule_name',  'atom_index']\n    )\n    \n    df = df.drop('atom_index', axis=1)\n    df = df.rename(columns={'mulliken_charge': f'mulliken_charge_{atom_idx}'})\n    \n    return df\n\ndef magnetic_shielding_tensors_merge(df, atom_idx):\n    df = pd.merge(\n        df,\n        magnetic_shielding_tensors,\n        how = 'left',\n        left_on  = ['molecule_name', f'atom_index_{atom_idx}'],\n        right_on = ['molecule_name',  'atom_index']\n    )\n    \n    df = df.drop('atom_index', axis=1)\n    df = df.rename(columns={\n        'XX': f'mst_XX_{atom_idx}',\n        'YX': f'mst_YX_{atom_idx}',\n        'ZX': f'mst_ZX_{atom_idx}',\n        'XY': f'mst_XY_{atom_idx}',\n        'YY': f'mst_YY_{atom_idx}',\n        'ZY': f'mst_ZY_{atom_idx}',\n        'XZ': f'mst_XZ_{atom_idx}',\n        'YZ': f'mst_YZ_{atom_idx}',\n        'ZZ': f'mst_ZZ_{atom_idx}'\n    })\n\n    return df\n\ndef dipole_moments_merge(df):\n    df = pd.merge(\n        df, \n        dipole_moments, \n        how = 'left',\n        left_on  = ['molecule_name'],\n        right_on = ['molecule_name']\n    )\n    \n    df = df.rename(columns={\n        'X': 'dipole_X',\n        'Y': 'dipole_Y',\n        'Z': 'dipole_Z'\n    })\n        \n    return df\n\ntrain = scalar_coupling_contributions_merge(train)\ntrain = structures_merge(train, 0)\ntrain = structures_merge(train, 1)\ntrain = potential_energy_merge(train)\ntrain = mulliken_charges_merge(train, 0)\ntrain = mulliken_charges_merge(train, 1)\ntrain = magnetic_shielding_tensors_merge(train, 0)\ntrain = magnetic_shielding_tensors_merge(train, 1)\ntrain = dipole_moments_merge(train)\n\ndel (structures, scalar_coupling_contributions, potential_energy, \n     mulliken_charges, magnetic_shielding_tensors, dipole_moments)\n\ntrain.head(5).T","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"- molecule_name – 85 003 unique values in train. Train and test contain different names of molecules.\n- atom_index_0/1 – is the atom indices of the atom-pair creating the coupling and scalar_coupling_constant. 28 unique values.\n- type - coupling type between two atoms in a molecule. 8 unique values.\n- **scalar_coupling_constant** – effectively the magnetic interactions between magnetic interaction between two atoms in a molecule. Target variable.\n- from tructures.scv (map by molecule_name, atom_index_0/1):\n  - atom_0/1 – types of two atoms in a molecule, the interactions between which must be predicted. One molecule can contain more than two atoms, but it is only necessary to predict the interaction for the two listed in the train and test files. atom_0 - 1 unique value (H - hydrogen), atom_1 - 3 unique values (C - carbon, H - hydrogen, N).\n  - x_0/1, y_0/1, z_0/1 - cartesian coordinates of each atom.\n  \n  \n**--- The data below is for test only ---**\n\n\n- from scalar_coupling_contributions.scv (map by molecule_name, atom_index_0, atom_index_1, type):\n  - fc – high correlation with the target variable\n  - sd\n  - pso\n  - dso\n- from potential_energy_merge.scv (map by molecule_name):\n  - potential_energy\n- from mulliken_charges_merge.scv (map by molecule_name, atom_index_0/1):\n  - mulliken_charge_0\n  - mulliken_charge_1\n- from magnetic_shielding_tensors_merge.scv (map by molecule_name, atom_index_0/1):\n  - XX, YX, ZX\n  - XY, YY, ZY\n  - XZ, YZ, ZZ\n- from dipole_moments_merge.scv (map by molecule_name, atom_index_0/1):\n  - dipole_X\n  - dipole_Y\n  - dipole_Z"},{"metadata":{},"cell_type":"markdown","source":"## 3.1. Distribution"},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"colors = {'1JHC':'#1f77b4', \n          '2JHH':'#ff7f0e', \n          '1JHN':'#2ca02c', \n          '2JHN':'#d62728', \n          '2JHC':'#9467bd', \n          '3JHH':'#8c564b', \n          '3JHC':'#e377c2', \n          '3JHN':'#7f7f7f'}\n\ndef cor_plot(bins, types = train['type'].unique()):\n    ds = pd.DataFrame()\n\n    scc_min = train.query('type in @types')['scalar_coupling_constant'].min()\n    scc_max = train.query('type in @types')['scalar_coupling_constant'].max()\n\n    bottom = np.array([0] * bins)\n\n    for i, cop_type in enumerate(types):\n        data = train.query('type == @cop_type')['scalar_coupling_constant'].dropna()\n        hist, edges = np.histogram(data, density=False, bins=bins, range=(scc_min, scc_max))\n\n        top = bottom + np.array(hist)\n\n        part_df = pd.DataFrame({\n            'top':top,\n            'bottom':bottom, \n            'left':edges[:-1], \n            'right':edges[1:],\n            'color':colors[cop_type],\n            'type':cop_type\n        })\n\n        bottom = top\n\n        ds = ds.append(part_df)\n\n    \n    p = figure(\n        #x_range=(scc_min, scc_max),\n        x_axis_label='Scalar Coupling Constant', # y_axis_label='Distribution'\n        title='\"Scalar Coupling Constant\" Distribution by \"Type\"',\n        plot_width=805, plot_height=350\n    ) \n   \n    p.quad(\n        source = ds, \n        top = 'top', bottom = 'bottom', \n        left = 'left', right = 'right',\n        \n        color = 'color', line_color = 'white', #hover_fill_color = 'color', \n        legend = 'type'        \n    )\n    \n    p.xgrid.grid_line_color = None\n    p.left[0].formatter.use_scientific = False\n    #p.legend.click_policy=\"mute\"\n    \n    return p\n\n#del ui, out, tb, slider, p, plot","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"p = cor_plot(100)\nshow(p);","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"cor_tabs = []\nfor t in train['type'].unique():\n    p = cor_plot(40, [t])\n    cor_tabs.append(Panel(child=p, title=t))\nshow(Tabs(tabs=cor_tabs))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 3.2. Count"},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"atom_index_0_dist = train['atom_index_0'].value_counts().sort_index().values\natom_index_1_dist = train['atom_index_1'].value_counts().sort_index().values\natom_index_value_counts = pd.DataFrame({'atom_index_0':atom_index_0_dist, 'atom_index_1':atom_index_1_dist})\natom_index_value_counts.index.name = 'Atom Index Num'\n\naivc = atom_index_value_counts.plot_bokeh(\n    kind='bar',\n    figsize=(805, 350),\n    title='Atom Index Count',\n    xlabel='Atom Index Num', #ylabel='Count',\n    disable_scientific_axes='y',\n    zooming=False, panning=False,\n    show_figure=False\n);\n\ngroupby_ta0 = train[['atom_index_0','atom_index_1','type']].groupby(\n    ['type','atom_index_0']).count().rename(columns = {'atom_index_1':'count'})\ngroupby_ta0 = groupby_ta0.reset_index().pivot(index='atom_index_0', columns='type', values='count')\n\ngroupby_ta0_plot = groupby_ta0.plot_bokeh(\n    kind='line',\n    figsize=(805, 350),\n    title='\"Atom Index 0\" Count by Type',\n    xlabel='Atom Index 0 Num',\n    disable_scientific_axes='y',\n    zooming=False, panning=False,\n    colormap='Paired',\n    show_figure=False\n);\n\ngroupby_ta1 = train[['atom_index_0','atom_index_1','type']].groupby(\n    ['type','atom_index_1']).count().rename(columns = {'atom_index_0':'count'})\ngroupby_ta1 = groupby_ta1.reset_index().pivot(index='atom_index_1', columns='type', values='count')\n\ngroupby_ta1_plot = groupby_ta1.plot_bokeh(\n    kind='line',\n    figsize=(805, 350),\n    title='\"Atom Index 1\" Count by Type',\n    xlabel='Atom Index 1 Num',\n    disable_scientific_axes='y',\n    zooming=False, panning=False,\n    colormap='Paired',\n    show_figure=False\n);\n\npandas_bokeh.plot_grid([\n    [aivc],\n    [groupby_ta0_plot], \n    [groupby_ta1_plot]\n])\n\ndel groupby_ta0, groupby_ta1, groupby_ta0_plot, groupby_ta1_plot\ndel atom_index_0_dist, atom_index_1_dist, atom_index_value_counts","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"df_group = train[['struct_atom_1','type','scalar_coupling_constant']]\ndf_group = pd.pivot_table(df_group, values='scalar_coupling_constant', index=['type'], columns=['struct_atom_1'], aggfunc='count')\ndf_group = df_group.reset_index().rename_axis(None, axis=1).set_index('type', drop=True)\ndf_group = df_group.fillna(0).astype(int)\n\ndf_group.plot_bokeh(\n    kind='bar',\n    figsize=(805, 350),\n    title='\"Type\" Count by \"Atom 1\"',\n    xlabel='Type', ylabel='',\n    disable_scientific_axes='y',\n    legend='top_right',\n    stacked=True,\n    zooming=False, panning=False\n);\n\ndel df_group","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"df_group = train[['struct_atom_1','type','scalar_coupling_constant']]\ndf_group = pd.pivot_table(df_group, values='scalar_coupling_constant', index=['struct_atom_1'], columns=['type'], aggfunc='count')\ndf_group = df_group.reset_index().rename_axis(None, axis=1)\ndf_group = df_group.fillna(0)\n\ndf_group.plot_bokeh(\n    kind='bar',\n    x='struct_atom_1',\n    figsize=(805, 350),\n    title='\"Atom 1\" Count by \"Type\"',\n    xlabel='Atom 1', ylabel='',\n    disable_scientific_axes='y',\n    legend = \"top_right\",\n    stacked=True,\n    zooming=False, panning=False\n);\n\ndel df_group","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 3.4. Scatter Plots"},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"t_aa_scc = train[['atom_index_0','atom_index_1','type','scalar_coupling_constant']]\nscc_min = abs(t_aa_scc['scalar_coupling_constant'].min())\nt_aa_scc['scalar_coupling_constant'] = t_aa_scc['scalar_coupling_constant'] + scc_min + 1\nt_aa_scc = t_aa_scc.sample(10_000)\n\nt_aa_scc.plot_bokeh(\n    kind='scatter',\n    \n    x='atom_index_0',\n    y='atom_index_1',\n    category='type',\n    size='scalar_coupling_constant',\n    \n    xlim = (-3, 30), ylim = (-7, 30),\n    figsize=(805, 780),\n    title='Correlation between atom_index_0, atom_index_1 and scalar_coupling_constant (circle size) by type (ver.1)',\n    xlabel='atom_index_0', ylabel='atom_index_1',\n    hovertool=False,\n    alpha=0.5,\n    line_width=0.2,\n    zooming=False, panning=False,\n    disable_scientific_axes='y'\n);\n\ndel t_aa_scc","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"t_aa_list = {}\ncolors = ['#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#7f7f7f']\n\nfor i, cop_type in enumerate(train['type'].unique()):\n    t_aa_df = train.query('type == @cop_type').sample(10_000)[['atom_index_0','atom_index_1']]\n    t_aa_df.rename(columns = {'atom_index_1':cop_type}, inplace=True)\n    \n    if i == 0: \n        title='Atom Index corellation by type'\n    else:\n        title=''\n        \n    t_aa_plot = t_aa_df.plot_bokeh(\n        kind='point',\n        \n        x='atom_index_0',\n        xticks=range(0, 29, 5),\n        xlim=(-1, 28), ylim=(-1, 28),\n        ylabel='atom_index_1',\n        title=title,\n        hovertool=False,\n        color = colors[i],\n        zooming=False, panning=False,\n        show_figure=False,\n        alpha=0.01\n    );\n    \n    t_aa_list[cop_type] = t_aa_plot\n\npandas_bokeh.plot_grid([\n    [t_aa_list['1JHC'], t_aa_list['1JHN'], t_aa_list['2JHH']],\n    [t_aa_list['2JHN'], t_aa_list['2JHC'], t_aa_list['3JHH']],\n    [t_aa_list['3JHC'], t_aa_list['3JHN']]\n], plot_width=268, plot_height=268);\n\ndel t_aa_list, t_aa_df, t_aa_plot","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"t_aa_list = {}\ncolors = [\n    ['#1f77b4','#b45c1f'],\n    ['#ff7f0e','#0e8eff'],\n    ['#2ca02c','#a02ca0'],\n    ['#d62728','#27d6d5'],\n    ['#9467bd','#90bd67'],\n    ['#8c564b','#4b818c'],\n    ['#e377c2','#77e398'],\n    ['#7f7f7f','#de2020']\n]\n\nfor i, cop_type in enumerate(train['type'].unique()):\n    t_aa_df = train.query('type == @cop_type').sample(5_000)[['atom_index_0','atom_index_1','scalar_coupling_constant']]\n    \n    x_min = t_aa_df['scalar_coupling_constant'].min().astype(int) - 2\n    x_max = t_aa_df['scalar_coupling_constant'].max().astype(int) + 2\n\n    Y_min = t_aa_df[['atom_index_0','atom_index_1']].stack().min().astype(int) - 2\n    Y_max = t_aa_df[['atom_index_0','atom_index_1']].stack().max().astype(int) + 2\n    \n    t_aa_plot = t_aa_df.plot_bokeh(\n        kind='point',\n        \n        x='scalar_coupling_constant',\n        xticks=range(x_min, x_max, 25),\n        xlim=(x_min, x_max), ylim=(Y_min, Y_max),\n        ylabel='atom_index_0/1',\n        title=cop_type,\n        hovertool=False,\n        colormap=colors[i],\n        zooming=False, panning=False,\n        show_figure=False,\n        alpha=0.1\n    );\n    \n    t_aa_list[cop_type] = t_aa_plot\n\n\nd = Div(text=\"\"\"<b>Correlation between atom_index_0, atom_index_1 and scalar_coupling_constant by type (ver.2)</b>\"\"\", width=805, height=15)\n\ngrid = pandas_bokeh.plot_grid([\n    [t_aa_list['1JHC'], t_aa_list['1JHN']], \n    [t_aa_list['2JHH'], t_aa_list['2JHN']],\n    [t_aa_list['2JHC'], t_aa_list['3JHH']],\n    [t_aa_list['3JHC'], t_aa_list['3JHN']]\n], plot_width=402, plot_height=402, show_plot=False);\n\nl = layout([\n    [d],\n    [grid]\n])\n\nshow(l)\n    \ndel t_aa_list, t_aa_df, t_aa_plot","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"t_aa_list = {}\ncolors = [\n    ['#1f77b4','#b45c1f'],\n    ['#ff7f0e','#0e8eff'],\n    ['#2ca02c','#a02ca0'],\n    ['#d62728','#27d6d5'],\n    ['#9467bd','#90bd67'],\n    ['#8c564b','#4b818c'],\n    ['#e377c2','#77e398'],\n    ['#7f7f7f','#de2020']\n]\n\nfor i, cop_type in enumerate(train['type'].unique()):\n    t_aa_df = train.query('type == @cop_type').sample(5_000)[['mulliken_charge_0','mulliken_charge_1','scalar_coupling_constant']]\n    \n    x_min = t_aa_df['scalar_coupling_constant'].min().astype(int) - 1\n    x_max = t_aa_df['scalar_coupling_constant'].max().astype(int) + 1\n\n    y_min = t_aa_df[['mulliken_charge_0','mulliken_charge_1']].stack().min().astype(int) - 1\n    y_max = t_aa_df[['mulliken_charge_0','mulliken_charge_1']].stack().max().astype(int) + 1\n\n    t_aa_plot = t_aa_df.plot_bokeh(\n        kind='point',\n        x='scalar_coupling_constant',\n        xticks=range(x_min, x_max, 25),\n        xlim=(x_min, x_max), ylim=(y_min, y_max),\n        ylabel='mulliken_charge_0/1',\n        title=cop_type,\n        hovertool=False,\n        colormap=colors[i],\n        zooming=False, panning=False,\n        show_figure=False,\n        alpha=0.1\n    );\n    \n    t_aa_list[cop_type] = t_aa_plot\n\n\nd = Div(text=\"\"\"<b>Correlation between mulliken_charge_0, mulliken_charge_1 and scalar_coupling_constant by type</b>\"\"\", width=805, height=15)\n\ngrid = pandas_bokeh.plot_grid([\n    [t_aa_list['1JHC'], t_aa_list['1JHN']], \n    [t_aa_list['2JHH'], t_aa_list['2JHN']],\n    [t_aa_list['2JHC'], t_aa_list['3JHH']],\n    [t_aa_list['3JHC'], t_aa_list['3JHN']]\n], plot_width=402, plot_height=402, show_plot=False);\n\nl = layout([\n    [d],\n    [grid]\n])\n\nshow(l)\n    \ndel t_aa_list, t_aa_df, t_aa_plot","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"def cor_plot(field, types=train['type'].unique()):\n    colors = {\n        '1JHC':'#1f77b4',\n        '2JHH':'#ff7f0e', \n        '1JHN':'#2ca02c', \n        '2JHN':'#d62728', \n        '2JHC':'#9467bd', \n        '3JHH':'#8c564b', \n        '3JHC':'#e377c2', \n        '3JHN':'#7f7f7f'\n    }\n\n    p1 = figure(\n        x_axis_label=field,\n        y_axis_label='scalar_coupling_constant',\n        title=f'Corellation between \"{field}\" and \"scalar_coupling_constant\" by \"type\"',\n        plot_width=805, plot_height=805\n    )\n\n    for i, t in enumerate(types):\n        t_fc_df = train.query('type == @t').sample(3_000)[[field,'scalar_coupling_constant']]\n\n        p1.circle(\n            source = ColumnDataSource(t_fc_df), \n            x = field, \n            y = 'scalar_coupling_constant', \n            legend=t,\n            size = 5,\n            color = colors[t], \n            alpha = 0.1\n        )\n\n    p1.legend.location = 'top_left'\n    p1.legend.click_policy = 'hide'\n\n    return p1","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"show(cor_plot('fc'))","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"cor_tabs = []\n\nfor t in train['type'].unique():\n    p = cor_plot('fc', [t])\n    cor_tabs.append(Panel(child=p, title=t))\n    \nshow(Tabs(tabs=cor_tabs))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Native Bokeh Cons:\n- No native 3D chart (custom in Kaggle does not work)\n- To build a histogram, you must manually calculate it.\n- No separate color management for the legend and chart.\n- Some Bokeh widgets in the Kaggle notebooks do not work.\n- There are a lot of problems with drawing a large number of bokeh's plots in kaggle notebooks."}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}