{"cells":[{"metadata":{},"cell_type":"markdown","source":"# **ASHRAE Energy Prediction**"},{"metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","trusted":true},"cell_type":"code","source":"# Import Statements\n\nimport datetime\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport pickle\nimport statistics\nimport seaborn as sns\nfrom lightgbm import LGBMRegressor, plot_importance\nfrom sklearn.metrics import mean_squared_log_error as msle, mean_squared_error as mse\nfrom sklearn.model_selection import KFold, train_test_split, TimeSeriesSplit\nfrom sklearn.preprocessing import OneHotEncoder\nfrom sklearn.linear_model import LinearRegression\nfrom tqdm import tqdm\nfrom copy import copy\n# Input data files are available in the \"../input/\" directory.\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\n#         print(os.path.join(dirname, filename))\n\n# # Any results you write to the current directory are saved as output.\npd.set_option('display.max_columns', 100)\npd.set_option('display.max_rows', 10000)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Code from https://www.kaggle.com/caesarlupum/ashrae-start-here-a-gentle-introduction \n# Function to reduce the DF size\ndef reduce_mem_usage(df, verbose=True):\n    numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']\n    start_mem = df.memory_usage().sum() / 1024**2    \n    for col in df.columns:\n        col_type = df[col].dtypes\n        if col_type in numerics:\n            c_min = df[col].min()\n            c_max = df[col].max()\n            if str(col_type)[:3] == 'int':\n                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:\n                    df[col] = df[col].astype(np.int8)\n                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:\n                    df[col] = df[col].astype(np.int16)\n                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:\n                    df[col] = df[col].astype(np.int32)\n                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:\n                    df[col] = df[col].astype(np.int64)  \n            else:\n                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:\n                    df[col] = df[col].astype(np.float16)\n                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:\n                    df[col] = df[col].astype(np.float32)\n                else:\n                    df[col] = df[col].astype(np.float64)    \n    end_mem = df.memory_usage().sum() / 1024**2\n    if verbose: print('Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction)'.format(end_mem, 100 * (start_mem - end_mem) / start_mem))\n    return df\n\n# function to calculate evaluation metric\ndef rmsle(y_true: pd.Series, y_predict: pd.Series) -> float:\n    \"\"\"\n    Evaluate root mean squared log error\n    :param y_true:\n    :param y_predict:\n    :return:\n    \"\"\"\n    return np.sqrt(msle(y_true, y_predict))","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"# Import data\nINPUT = \"../input/ashrae-energy-prediction/\"\n\ndf_train = pd.read_csv(f\"{INPUT}train.csv\")\ndf_test = pd.read_csv(f\"{INPUT}test.csv\")\nbldg_metadata = pd.read_csv(f\"{INPUT}building_metadata.csv\")\nweather_train = pd.read_csv(f\"{INPUT}weather_train.csv\")\nweather_test = pd.read_csv(f\"{INPUT}weather_test.csv\")\nsample = pd.read_csv(f\"{INPUT}sample_submission.csv\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_test.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_test = df_test.drop(columns=['row_id'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train = reduce_mem_usage(df=df_train)\ndf_test = reduce_mem_usage(df=df_test)\nweather_train = reduce_mem_usage(df=weather_train)\nweather_test = reduce_mem_usage(df=weather_test)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train = df_train.merge(bldg_metadata, on='building_id', how='left')\ndf_test = df_test.merge(bldg_metadata, on='building_id', how='left')\ndf_train = df_train.merge(weather_train, on=['site_id', 'timestamp'], how='left')\ndf_test = df_test.merge(weather_test, on=['site_id', 'timestamp'], how='left')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import gc\ndel weather_train, weather_test, bldg_metadata\ngc.collect()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train['timestamp'] = pd.to_datetime(arg=df_train['timestamp'])\ndf_test['timestamp'] = pd.to_datetime(arg=df_test['timestamp'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Feature Engineering"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Extracting date features from timestamp\ndf_train['year'] = df_train['timestamp'].dt.year\ndf_train['month'] = df_train['timestamp'].dt.month\ndf_train['day'] = df_train['timestamp'].dt.day\ndf_train['hour'] = df_train['timestamp'].dt.hour\ndf_test['year'] = df_test['timestamp'].dt.year\ndf_test['month'] = df_test['timestamp'].dt.month\ndf_test['day'] = df_test['timestamp'].dt.day\ndf_test['hour'] = df_test['timestamp'].dt.hour\ndf_train['dayofweek'] = df_train['timestamp'].dt.dayofweek\ndf_test['dayofweek'] = df_test['timestamp'].dt.dayofweek\n\n# 1: day, 2:night. Hope this save some memory\ndf_train['day-and-night'] = np.where((df_train['hour'] < 6), 2, 1)\ndf_test['day-and-night'] = np.where((df_test['hour'] < 6), 2, 1)\n\n#1: winter, 2: spring, 3: summer, 4: autumn. Hope this save some memory \ndf_train.loc[df_train['month'].isin([12, 1, 2]), 'season'] = 1\ndf_train.loc[df_train['month'].isin([3, 4, 5]), 'season'] = 2\ndf_train.loc[df_train['month'].isin([6, 7, 8]), 'season'] = 3\ndf_train.loc[df_train['month'].isin([9, 10, 11]), 'season'] = 4\ndf_test.loc[df_test['month'].isin([12, 1, 2]), 'season'] = 1\ndf_test.loc[df_test['month'].isin([3, 4, 5]), 'season'] = 2\ndf_test.loc[df_test['month'].isin([6, 7, 8]), 'season'] = 3\ndf_test.loc[df_test['month'].isin([9, 10, 11]), 'season'] = 4","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Because we made few new features we can try to reduce memory once again\ndf_train = reduce_mem_usage(df=df_train)\ndf_test = reduce_mem_usage(df=df_test)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Making age feature\ndf_train['age'] = df_train['year'] - df_train['year_built']\ndf_test['age'] = df_test['year'] - df_test['year_built']\n\n# Making number of hours passed from start\nnew_df = df_train.groupby(by=['building_id'], as_index=False)['timestamp'].min()\nnew_df = new_df.rename(columns = {'timestamp': 'start_ts'})\n\ndf_train = df_train.merge(new_df, on = 'building_id', how='left')\ndf_test = df_test.merge(new_df, on = 'building_id', how='left')\n\ndf_train['hours_passed'] = (df_train['timestamp'] - df_train['start_ts']).dt.total_seconds()/3600\ndf_test['hours_passed'] = (df_test['timestamp'] - df_test['start_ts']).dt.total_seconds()/3600","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Because we made few new features we can try to reduce memory once again\ndf_train = reduce_mem_usage(df=df_train)\ndf_test = reduce_mem_usage(df=df_test)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Making combination of categorical variable to see if they help model become better\n# df_train['building_id_meter_hours_passed'] = df_train['building_id'].astype(str) + '_' + df_train['meter'].astype(str) + '_' + df_train['hours_passed'].astype(str)\n# df_test['building_id_meter_hours_passed'] = df_test['building_id'].astype(str) + '_' + df_test['meter'].astype(str) + '_' + df_test['hours_passed'].astype(str)\n\n# # Because we made few new features we can try to reduce memory once again\n# df_train = reduce_mem_usage(df=df_train)\n# df_test = reduce_mem_usage(df=df_test)\n\n# df_train['building_id_meter'] = df_train['building_id'].astype(str) + '_' + df_train['meter'].astype(str)\n# df_test['building_id_meter'] = df_test['building_id'].astype(str) + '_' + df_test['meter'].astype(str)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Data Cleaning"},{"metadata":{"trusted":true},"cell_type":"code","source":"# site_id =0 has some building where meter readings before May 21, 2016 are not reliable so dropping those records \ndf_train = df_train.query('not(site_id==0 & timestamp<\"2016-05-21 00:00:00\")')\n\ndf_train = df_train.loc[df_train['meter_reading'] > 0, :]\n# df_test = df_test.loc[df_test['meter_reading'] > 0, :]\n\n# Missing value handling\ncols = ['floor_count', 'air_temperature', 'cloud_coverage', 'dew_temperature', 'precip_depth_1_hr', 'sea_level_pressure', \n        'wind_direction', 'wind_speed']\ndf_train.loc[:, cols] = df_train.loc[:, cols].interpolate(axis=0)\ndf_test.loc[:, cols] = df_test.loc[:, cols].interpolate(axis=0)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Convert to categorical datatype\ncat_cols = ['meter', 'primary_use', 'site_id', 'building_id', 'year', 'month', 'day', 'hour', 'dayofweek', 'season', 'day-and-night']\nfor col in cat_cols:\n    df_train[col] = df_train[col].astype('category')\n    df_test[col] = df_test[col].astype('category')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Modelling"},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train = df_train.reset_index(drop=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Make validation set based on time split\ndf_val = df_train.loc[df_train['timestamp'] >= '2016-11-01 00:00:00']\ndf_train = df_train.loc[df_train['timestamp'] < '2016-11-01 00:00:00']\ny_train = df_train['meter_reading']\ny_val = df_val['meter_reading']\n\ny_train = np.log1p(y_train)\ny_val = np.log1p(y_val)\ndf_train = df_train.drop(columns=['meter_reading'])\ndf_val = df_val.drop(columns=['meter_reading'])\n\n# # Create input and target\n# y_train = df_train['meter_reading']\n# y_train = np.log1p(y_train)\n# df_train = df_train.drop(columns=['meter_reading'])\n\n# # Make validation set based on train_test_split\n# df_train, df_val, y_train, y_val = train_test_split(df_train, y_train, test_size=0.2, random_state=42)\n\n# Drop timestamp because model does not accept\ndf_train = df_train.drop(columns=['timestamp', 'start_ts'])\ndf_val = df_val.drop(columns=['timestamp', 'start_ts'])\ndf_test = df_test.drop(columns=['timestamp', 'start_ts'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# # Cross Validation\n# scores = []\n# tss = TimeSeriesSplit(n_splits=5)\n# fold = 0\n# for train_index, val_index in tss.split(df_train): \n#     fold+=1\n#     lgbmr = LGBMRegressor(n_estimators=1000, random_state=10)\n#     lgbmr.fit(df_train.loc[train_index, :], y_train[train_index])\n#     y_predict = lgbmr.predict(df_train.loc[val_index, :])\n#     score = np.sqrt(mse(y_train[val_index], y_predict))\n#     print(f\"fold{fold}: {score}\")\n#     scores.append(score)\n# print(f\"Mean score: {sum(scores)/len(scores)}    Std. dev: {statistics.stdev(scores)}\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"gc.collect()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Model\nlgbmr = LGBMRegressor(n_estimators=500, random_state=10)\nlgbmr.fit(df_train, y_train)\ny_predict = lgbmr.predict(df_val)\nscore = np.sqrt(mse(y_val, y_predict))\n# score = rmsle(y_val, y_predict)\nprint(f\"score: {score}\")\n\n# # Training the model on full train dataset\n# lgbmr.fit(pd.concat([df_train, df_val], axis=0), pd.concat([y_train, y_val], axis=0))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Lets try to visualize model predicts vs actual meter_readings\nviz_data = pd.concat(objs=[df_val, y_val, pd.Series(data=y_predict, name='predictions', index=df_val.index)], \n                     axis=1)\n\nviz_data['error'] = np.abs(y_predict - y_val)\n\nlgbmr_errors = (viz_data.groupby(by=['site_id', 'building_id', 'meter'], as_index=False, observed=True)['error'].mean()).merge(df_val.loc[:, ['site_id', 'building_id', 'meter', 'primary_use']].drop_duplicates(), on = ['site_id', 'building_id', 'meter'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"lgbmr_errors.sort_values(by='error', ascending=False).head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# # lets viualize what is going on wrong with these buildings in train and test set both\n\n# #Train set\n# fig, ax = plt.subplots(figsize=(12,9))\n# data = df_train.copy()\n# data['meter_reading'] = y_train\n# data = data.loc[(data['site_id'] == 7) & (data['building_id'] == 799) & (data['meter'] == 0), :].reset_index(drop=True)\n# ax.scatter(data.index ,data['meter_reading'], c='blue', s=5)\n# ax.legend(data['primary_use'])\n# plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# lets viualize what is going on wrong with these buildings in train and test set both\n\n#Train set\nfor row in lgbmr_errors.sort_values(by='error', ascending=False).head(10).index:\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12,6), sharex=True, sharey=True)\n    data = df_train.copy()\n    data['meter_reading'] = y_train\n    data = data.loc[(data['site_id'] == lgbmr_errors.loc[row, 'site_id']) & (data['building_id'] == lgbmr_errors.loc[row, 'building_id']) & (data['meter'] == lgbmr_errors.loc[row, 'meter']), :].reset_index(drop=True)\n    ax1.scatter(data.index ,data['meter_reading'], c='blue', s=5)\n    ax1.set_title(f\"{lgbmr_errors.loc[row, 'site_id']}-{lgbmr_errors.loc[row, 'building_id']}-{lgbmr_errors.loc[row, 'meter']}\")\n    ax1.legend(data['primary_use'])\n    data = viz_data.loc[(viz_data['site_id'] == lgbmr_errors.loc[row, 'site_id']) & (viz_data['building_id'] == lgbmr_errors.loc[row, 'building_id']) & (viz_data['meter'] == lgbmr_errors.loc[row, 'meter']), :].reset_index(drop=True)\n    ax2.scatter(data.index ,data['meter_reading'], c='blue', s=5)\n    ax2.scatter(data.index ,data['predictions'], c='orange', s=5)\n    ax2.set_title(f\"{lgbmr_errors.loc[row, 'site_id']}-{lgbmr_errors.loc[row, 'building_id']}-{lgbmr_errors.loc[row, 'meter']}\")\n    ax2.legend(data['primary_use'])\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_importance(lgbmr)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# #Val set\n# fig, ax = plt.subplots(figsize=(12,9))\n# data = viz_data.loc[(viz_data['site_id'] == 7) & (viz_data['building_id'] == 799) & (viz_data['meter'] == 0), :].reset_index(drop=True)\n# ax.scatter(data.index ,data['meter_reading'], c='blue', s=5)\n# ax.scatter(data.index ,data['predictions'], c='orange', s=5)\n# ax.legend(data['primary_use'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Ensemble Modelling"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Half and half learning\nX_1st_half = df_train[:int(df_train.shape[0]/2)]\ny_1st_half = y_train[:int(df_train.shape[0]/2)]\nX_2nd_half = df_train[int(df_train.shape[0]/2):]\ny_2nd_half = y_train[int(df_train.shape[0]/2):]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"lgbmr_1st_half = LGBMRegressor(random_state=10)\nlgbmr_2nd_half = LGBMRegressor(random_state=10)\nlgbmr_1st_half.fit(X_1st_half, y_1st_half)\nlgbmr_2nd_half.fit(X_2nd_half, y_2nd_half)\ny_predict_1 = lgbmr_1st_half.predict(df_val)\ny_predict_2 = lgbmr_2nd_half.predict(df_val)\ny_predict_1_2 = (pd.Series(data=y_predict_1, name='prediction_1') + pd.Series(data=y_predict_2, name='prediction_2'))/2\nscore = np.sqrt(mse(y_val, y_predict_1_2))\nprint(f\"score: {score}\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Training model on entire dataset using Half and half learning methodology\nX_1st_half = df_train[:int(pd.concat([df_train, df_val], axis=0).shape[0]/2)]\ny_1st_half = y_train[:int(pd.concat([df_train, df_val], axis=0).shape[0]/2)]\nX_2nd_half = df_train[int(pd.concat([df_train, df_val], axis=0).shape[0]/2):]\ny_2nd_half = y_train[int(pd.concat([df_train, df_val], axis=0).shape[0]/2):]\n\nlgbmr_1st_half = LGBMRegressor(random_state=10)\nlgbmr_2nd_half = LGBMRegressor(random_state=10)\nlgbmr_1st_half.fit(X_1st_half, y_1st_half)\nlgbmr_2nd_half.fit(X_2nd_half, y_2nd_half)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Saving model\nfilename = 'lgbm_model1.pickle'\npickle.dump(lgbmr, open(filename, 'wb'))\n# load the model from disk\nloaded_model = pickle.load(open(filename, 'rb'))\n\nfilename = 'lgbmr_1st_half.pickle'\npickle.dump(lgbmr, open(filename, 'wb'))\n# load the model from disk\nlgbmr_1st_half = pickle.load(open(filename, 'rb'))\nfilename = 'lgbmr_2nd_half.pickle'\npickle.dump(lgbmr, open(filename, 'wb'))\n# load the model from disk\nlgbmr_2nd_half = pickle.load(open(filename, 'rb'))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# # Important features\n# fig, ax = plt.subplots(figsize=(12, 9))\n# plot_importance(lgbmr, ax=ax)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# fig, ax = plt.subplots(figsize=(12, 9))\n# plot_importance(lgbmr_1st_half, ax=ax)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# fig, ax = plt.subplots(figsize=(12, 9))\n# plot_importance(lgbmr_2nd_half, ax=ax)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"del df_train, df_val, y_train, y_val, lgbmr\ngc.collect()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Make predictions on test set\n# STEP = 1000000\n# y_test_predict = []\n# for i in range(0, df_test.shape[0], STEP):\n#     batch_prediction = loaded_model.predict(df_test.loc[i:i+STEP-1,:])\n#     y_test_predict.append(list(batch_prediction))\n# y_test = []\n# for predictions in y_test_predict:\n#     y_test = y_test + predictions\n\nSTEP = 1000000\ny_test_predict = []\nfor i in range(0, df_test.shape[0], STEP):\n    batch_prediction = lgbmr_1st_half.predict(df_test.loc[i:i+STEP-1,:])\n    y_test_predict.append(list(batch_prediction))\ny_test_1st_half = []\nfor predictions in y_test_predict:\n    y_test_1st_half = y_test_1st_half + predictions\n    \nSTEP = 1000000\ny_test_predict = []\nfor i in range(0, df_test.shape[0], STEP):\n    batch_prediction = lgbmr_2nd_half.predict(df_test.loc[i:i+STEP-1,:])\n    y_test_predict.append(list(batch_prediction))\ny_test_2nd_half = []\nfor predictions in y_test_predict:\n    y_test_2nd_half = y_test_2nd_half + predictions\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# sample['meter_reading'] = y_test\nsample['meter_reading'] = (pd.Series(data=y_test_1st_half, name='pred_1st_half') + \n                           pd.Series(data=y_test_2nd_half, name='pred_2nd_half'))/2 ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from math import e\n# sample['meter_reading'] = e**sample['meter_reading'] - 1\nsample['meter_reading'] = np.expm1(sample['meter_reading'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sample.to_csv(\"submission.csv\", index=False, float_format='%.4f')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"References:\n* https://www.kaggle.com/caesarlupum/ashrae-start-here-a-gentle-introduction\n* https://www.kaggle.com/rohanrao/ashrae-half-and-half"},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"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":1}