{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Data Services\n", "\n", "You can connect to [CARTO Data Services API](https://carto.com/developers/data-services-api/) directly from CARTOframes. This API consists of a set of location-based functions that can be applied to your data to perform geospatial analyses without leaving the context of your notebook. For instance, you can **geocode** a pandas DataFrame with addresses on the fly, and then perform a trade area analysis by computing **isodistances** or **isochrones** programmatically.\n", "\n", "Using Data Services requires to be authenticated. For more information about how to authenticate, please read the [Authentication guide](/developers/cartoframes/guides/Authentication/). For further learning you can also check out the [Data Services examples](/developers/cartoframes/examples/#example-data-services)." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from cartoframes.auth import set_default_credentials\n", "\n", "set_default_credentials('creds.json')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> Depending on your CARTO account plan, some of these data services are subject to different [quota limitations](https://carto.com/developers/data-services-api/support/quota-information/)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Geocoding\n", "\n", "To get started, let's read in and explore the Starbucks location data we have. With the Starbucks store data in a DataFrame, we can see that there are two columns that can be used in the **geocoding** service: `name` and `address`. There's also a third column that reflects the annual revenue of the store." ] }, { "cell_type": "code", "execution_count": 2, "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", "
nameaddressrevenue
0Franklin Ave & Eastern Pkwy341 Eastern Pkwy,Brooklyn, NY 112381321040.772
1607 Brighton Beach Ave607 Brighton Beach Avenue,Brooklyn, NY 112351268080.418
265th St & 18th Ave6423 18th Avenue,Brooklyn, NY 112041248133.699
3Bay Ridge Pkwy & 3rd Ave7419 3rd Avenue,Brooklyn, NY 112091185702.676
4Caesar's Bay Shopping Center8973 Bay Parkway,Brooklyn, NY 112141148427.411
\n", "
" ], "text/plain": [ " name address \\\n", "0 Franklin Ave & Eastern Pkwy 341 Eastern Pkwy,Brooklyn, NY 11238 \n", "1 607 Brighton Beach Ave 607 Brighton Beach Avenue,Brooklyn, NY 11235 \n", "2 65th St & 18th Ave 6423 18th Avenue,Brooklyn, NY 11204 \n", "3 Bay Ridge Pkwy & 3rd Ave 7419 3rd Avenue,Brooklyn, NY 11209 \n", "4 Caesar's Bay Shopping Center 8973 Bay Parkway,Brooklyn, NY 11214 \n", "\n", " revenue \n", "0 1321040.772 \n", "1 1268080.418 \n", "2 1248133.699 \n", "3 1185702.676 \n", "4 1148427.411 " ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "\n", "df = pd.read_csv('http://libs.cartocdn.com/cartoframes/samples/starbucks_brooklyn.csv')\n", "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Quota consumption\n", "\n", "Each time you run Data Services, quota is consumed. For this reason, we provide the ability to check in advance the **amount of credits** an operation will consume by using the `dry_run` parameter when running the service function.\n", "\n", "It is also possible to check your available quota by running the `available_quota` function." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "from cartoframes.data.services import Geocoding\n", "\n", "geo_service = Geocoding()\n", "\n", "city_ny = {'value': 'New York'}\n", "country_usa = {'value': 'USA'}\n", "\n", "_, geo_dry_metadata = geo_service.geocode(df, street='address', city=city_ny, country=country_usa, dry_run=True)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'total_rows': 10,\n", " 'required_quota': 10,\n", " 'previously_geocoded': 0,\n", " 'previously_failed': 0,\n", " 'records_with_geometry': 0}" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "geo_dry_metadata" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "4999402" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "geo_service.available_quota()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Success! Data geocoded correctly\n" ] } ], "source": [ "geo_gdf, geo_metadata = geo_service.geocode(df, street='address', city=city_ny, country=country_usa)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's compare `geo_dry_metadata` and `geo_metadata` to see the differences between the information returned with and without the `dry_run` option. As we can see, this information reflects that all the locations have been geocoded successfully and that it has consumed 10 credits of quota." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'total_rows': 10,\n", " 'required_quota': 10,\n", " 'previously_geocoded': 0,\n", " 'previously_failed': 0,\n", " 'records_with_geometry': 0,\n", " 'final_records_with_geometry': 10,\n", " 'geocoded_increment': 10,\n", " 'successfully_geocoded': 10,\n", " 'failed_geocodings': 0}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "geo_metadata" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "4999392" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "geo_service.available_quota()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If the input data file ever changes, cached results will only be applied to unmodified\n", "records, and new geocoding will be performed only on _new or changed records_. In order to use cached results, we have to save the results to a CARTO table using the `table_name` and `cached=True` parameters." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The resulting data is a `GeoDataFrame` that contains three new columns:\n", "\n", "* `geometry`: The resulting geometry\n", "* `gc_status_rel`: The percentage of accuracy of each location\n", "* `carto_geocode_hash`: Geocode information" ] }, { "cell_type": "code", "execution_count": 9, "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", " \n", " \n", " \n", " \n", " \n", " \n", "
the_geomnameaddressrevenuegc_status_relcarto_geocode_hash
0POINT (-73.95746 40.67102)Franklin Ave & Eastern Pkwy341 Eastern Pkwy,Brooklyn, NY 112381321040.7720.97c834a8e289e5bce280775a9bf1f833f1
1POINT (-73.96122 40.57796)607 Brighton Beach Ave607 Brighton Beach Avenue,Brooklyn, NY 112351268080.4180.997d39a3fff93efd9034da88aa9ad2da79
2POINT (-73.98978 40.61944)65th St & 18th Ave6423 18th Avenue,Brooklyn, NY 112041248133.6990.981a2312049ddea753ba42bf77f5ccf718
3POINT (-74.02750 40.63202)Bay Ridge Pkwy & 3rd Ave7419 3rd Avenue,Brooklyn, NY 112091185702.6760.98827ab4dcc2d49d5fd830749597976d4a
4POINT (-74.00098 40.59321)Caesar's Bay Shopping Center8973 Bay Parkway,Brooklyn, NY 112141148427.4110.98119a38c7b51195cd4153fc81605a8495
\n", "
" ], "text/plain": [ " the_geom name \\\n", "0 POINT (-73.95746 40.67102) Franklin Ave & Eastern Pkwy \n", "1 POINT (-73.96122 40.57796) 607 Brighton Beach Ave \n", "2 POINT (-73.98978 40.61944) 65th St & 18th Ave \n", "3 POINT (-74.02750 40.63202) Bay Ridge Pkwy & 3rd Ave \n", "4 POINT (-74.00098 40.59321) Caesar's Bay Shopping Center \n", "\n", " address revenue gc_status_rel \\\n", "0 341 Eastern Pkwy,Brooklyn, NY 11238 1321040.772 0.97 \n", "1 607 Brighton Beach Avenue,Brooklyn, NY 11235 1268080.418 0.99 \n", "2 6423 18th Avenue,Brooklyn, NY 11204 1248133.699 0.98 \n", "3 7419 3rd Avenue,Brooklyn, NY 11209 1185702.676 0.98 \n", "4 8973 Bay Parkway,Brooklyn, NY 11214 1148427.411 0.98 \n", "\n", " carto_geocode_hash \n", "0 c834a8e289e5bce280775a9bf1f833f1 \n", "1 7d39a3fff93efd9034da88aa9ad2da79 \n", "2 1a2312049ddea753ba42bf77f5ccf718 \n", "3 827ab4dcc2d49d5fd830749597976d4a \n", "4 119a38c7b51195cd4153fc81605a8495 " ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "geo_gdf.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In addition, to prevent geocoding records that have been **previously geocoded**, and thus spend quota **unnecessarily**, you should always preserve the ``the_geom`` and ``carto_geocode_hash`` columns generated by the geocoding process.\n", "\n", "This will happen **automatically** in these cases:\n", "\n", "1. Your input is a **table** from CARTO processed in place (without a ``table_name`` parameter)\n", "2. If you save your results to a CARTO table using the ``table_name`` parameter, and only use the resulting table for any further geocoding.\n", "\n", "If you try to geocode this DataFrame now that it contains both ``the_geom`` and the ``carto_geocode_hash``, you will see that the required quota is 0 because it has already been geocoded." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "_, geo_metadata = geo_service.geocode(geo_gdf, street='address', city=city_ny, country=country_usa, dry_run=True)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "geo_metadata.get('required_quota')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Precision\n", "\n", "The `address` column is more complete than the `name` column, and therefore, the resulting coordinates calculated by the service will be more accurate. If we check this, the accuracy values using the `name` column are lower than the ones we get by using the `address` column for geocoding." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Success! Data geocoded correctly\n" ] } ], "source": [ "geo_name_gdf, geo_name_metadata = geo_service.geocode(df, street='name', city=city_ny, country=country_usa)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.93, 0.96, 0.85, 0.83, 0.74, 0.87])" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "geo_name_gdf.gc_status_rel.unique()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.97, 0.99, 0.98])" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "geo_gdf.gc_status_rel.unique()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Visualize the results\n", "\n", "Finally, we can visualize the precision of the geocoded results using a CARTOframes [visualization layer](/developers/cartoframes/examples/#example-color-bins-layer)." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", " None\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", " Static map image\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", " StackTrace\n", "
    \n", "
    \n", "
    \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from cartoframes.viz import Layer, color_bins_style, popup_element\n", "\n", "Layer(\n", " geo_gdf,\n", " color_bins_style('gc_status_rel', method='equal', bins=geo_gdf.gc_status_rel.unique().size),\n", " popup_hover=[popup_element('address', 'Address'), popup_element('gc_status_rel', 'Precision')],\n", " title='Geocoding Precision'\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Isolines\n", "\n", "There are two **Isoline** functions: **isochrones** and **isodistances**. In this guide we will use the **isochrones** function to calculate walking areas _by time_ for each Starbucks store and the **isodistances** function to calculate the walking area _by distance_.\n", "\n", "By definition, isolines are concentric polygons that display equally calculated levels over a given surface area, and they are calculated as the intersection areas from the origin point, measured by:\n", "\n", "* **Time** in the case of **isochrones**\n", "* **Distance** in the case of **isodistances**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Isochrones\n", "\n", "For isochrones, let's calculate the time ranges of 5, 15 and 30 minutes. These ranges are input in `seconds`, so they will be **300**, **900**, and **1800** respectively." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "from cartoframes.data.services import Isolines\n", "\n", "iso_service = Isolines()\n", "\n", "_, isochrones_dry_metadata = iso_service.isochrones(geo_gdf, [300, 900, 1800], mode='walk', dry_run=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Remember to always **check the quota** using `dry_run` parameter and `available_quota` method before running the service!" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "available 115394, required 30\n" ] } ], "source": [ "print('available {0}, required {1}'.format(\n", " iso_service.available_quota(),\n", " isochrones_dry_metadata.get('required_quota'))\n", ")" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Success! Isolines created correctly\n" ] } ], "source": [ "isochrones_gdf, isochrones_metadata = iso_service.isochrones(geo_gdf, [300, 900, 1800], mode='walk')" ] }, { "cell_type": "code", "execution_count": 19, "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", "
    source_iddata_rangethe_geom
    00300MULTIPOLYGON (((-73.95936 40.67087, -73.95910 ...
    10900MULTIPOLYGON (((-73.96743 40.67345, -73.96683 ...
    201800MULTIPOLYGON (((-73.97584 40.67499, -73.97558 ...
    31300MULTIPOLYGON (((-73.96417 40.57749, -73.96391 ...
    41900MULTIPOLYGON (((-73.97035 40.57749, -73.97009 ...
    \n", "
    " ], "text/plain": [ " source_id data_range the_geom\n", "0 0 300 MULTIPOLYGON (((-73.95936 40.67087, -73.95910 ...\n", "1 0 900 MULTIPOLYGON (((-73.96743 40.67345, -73.96683 ...\n", "2 0 1800 MULTIPOLYGON (((-73.97584 40.67499, -73.97558 ...\n", "3 1 300 MULTIPOLYGON (((-73.96417 40.57749, -73.96391 ...\n", "4 1 900 MULTIPOLYGON (((-73.97035 40.57749, -73.97009 ..." ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isochrones_gdf.head()" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", " None\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", " Static map image\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", " StackTrace\n", "
      \n", "
      \n", "
      \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from cartoframes.viz import Layer, basic_style, basic_legend\n", "\n", "Layer(isochrones_gdf, basic_style(opacity=0.5), basic_legend('Isochrones'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Isodistances\n", "\n", "For isodistances, let's calculate the distance ranges of 100, 500 and 1000 meters. These ranges are input in `meters`, so they will be **100**, **500**, and **1000** respectively." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "_, isodistances_dry_metadata = iso_service.isodistances(geo_gdf, [100, 500, 1000], mode='walk', dry_run=True)" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "available 115364, required 30\n" ] } ], "source": [ "print('available {0}, required {1}'.format(\n", " iso_service.available_quota(),\n", " isodistances_dry_metadata.get('required_quota'))\n", ")" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Success! Isolines created correctly\n" ] } ], "source": [ "isodistances_gdf, isodistances_metadata = iso_service.isodistances(geo_gdf, [100, 500, 1000], mode='walk')" ] }, { "cell_type": "code", "execution_count": 24, "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", "
      source_iddata_rangethe_geom
      00100MULTIPOLYGON (((-73.95782 40.67139, -73.95721 ...
      10500MULTIPOLYGON (((-73.96262 40.67276, -73.96219 ...
      201000MULTIPOLYGON (((-73.96880 40.67345, -73.96820 ...
      31100MULTIPOLYGON (((-73.96125 40.57800, -73.96065 ...
      41500MULTIPOLYGON (((-73.96605 40.57800, -73.96563 ...
      \n", "
      " ], "text/plain": [ " source_id data_range the_geom\n", "0 0 100 MULTIPOLYGON (((-73.95782 40.67139, -73.95721 ...\n", "1 0 500 MULTIPOLYGON (((-73.96262 40.67276, -73.96219 ...\n", "2 0 1000 MULTIPOLYGON (((-73.96880 40.67345, -73.96820 ...\n", "3 1 100 MULTIPOLYGON (((-73.96125 40.57800, -73.96065 ...\n", "4 1 500 MULTIPOLYGON (((-73.96605 40.57800, -73.96563 ..." ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isodistances_gdf.head()" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", " None\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", " Static map image\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", " StackTrace\n", "
        \n", "
        \n", "
        \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from cartoframes.viz import Layer, basic_style, basic_legend\n", "\n", "Layer(isodistances_gdf, basic_style(opacity=0.5), basic_legend('Isodistances'))" ] } ], "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.7" } }, "nbformat": 4, "nbformat_minor": 4 }