{"cells":[{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import numpy as np \nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport pydicom\nimport matplotlib.animation as animation\nimport re\nimport matplotlib.animation as animation\nfrom IPython.display import HTML\nimport os\nimport plotly.express as px\nimport plotly.figure_factory as ff\nfrom plotly.subplots import make_subplots\nimport plotly.graph_objects as go\nfrom tqdm import tqdm\nfrom PIL import Image\nimport cv2\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.preprocessing import RobustScaler\nfrom sklearn.preprocessing import StandardScaler\nimport tensorflow.keras.backend as K\nfrom tensorflow.keras.layers import Input, Dense, Flatten, ZeroPadding2D, Conv2D, BatchNormalization, Activation, MaxPooling2D, AveragePooling2D, add, concatenate, Dropout\nfrom tensorflow.keras.applications.resnet50 import ResNet50\nfrom tensorflow.keras.models import Model \nimport tensorflow as tf\nimport random\nfrom tensorflow.keras.utils import Sequence\nfrom tensorflow.keras.callbacks import  ReduceLROnPlateau\nimport warnings\nfrom plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot\nimport plotly as py\ninit_notebook_mode(connected=True)\nwarnings.filterwarnings(\"ignore\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"IMG_DIR_TRAIN = \"/kaggle/input/osic-pulmonary-fibrosis-progression/train/\"\nIMG_DIR_TEST = \"/kaggle/input/osic-pulmonary-fibrosis-progression/test/\"\nFILE_DIR = \"/kaggle/input/osic-pulmonary-fibrosis-progression/\"\nIMAGE_SIZE = 224","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"train_data = pd.read_csv(FILE_DIR + \"train.csv\")\ntest_data = pd.read_csv(FILE_DIR + \"test.csv\")\ntrain_data.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Explore first patient in the dataset ","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"PATIENT_ID = train_data[\"Patient\"][0]    \nprint(\"Patient : \", PATIENT_ID)\nprint(\"Number of FVC observations : \", len(train_data[train_data[\"Patient\"] == PATIENT_ID]))\nprint(\"Age : \", (train_data[train_data[\"Patient\"] == PATIENT_ID][\"Age\"].values[0]))\nprint(\"Sex : \", (train_data[train_data[\"Patient\"] == PATIENT_ID][\"Sex\"].values[0]))\nprint(\"SmokingStatus : \", (train_data[train_data[\"Patient\"] == PATIENT_ID][\"SmokingStatus\"].values[0]))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Animate the 3-D slice of lungs**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def natural_sort(l): \n    convert = lambda text: int(text) if text.isdigit() else text.lower() \n    alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] \n    return sorted(l, key = alphanum_key)\n\nfig = plt.figure()\n\nimg_names = []\nfor dirictory,_,img in os.walk(IMG_DIR_TRAIN + train_data[\"Patient\"][0]):\n    img_names.append(img)\n\nimg_names = natural_sort(img_names[0])\n\nimages = []\nk = 0\nfor i in img_names:\n    images.append([plt.imshow(pydicom.dcmread(IMG_DIR_TRAIN + PATIENT_ID + \"/\" + i).pixel_array, cmap=plt.cm.bone)])\n    k += 1\n    \nani = animation.ArtistAnimation(fig, images)\nplt.close()\n\nHTML('<center>' + ani.to_html5_video() + '</center>')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Plot how FVC changes over time after CT**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"FVC = train_data[train_data[\"Patient\"] == PATIENT_ID][\"FVC\"]\nWeek = train_data[train_data[\"Patient\"] == PATIENT_ID][\"Weeks\"]\n\nfig = px.line(x=Week, y=FVC, title='FVC over time of patient with id ' + PATIENT_ID)\n\nfig.update_layout(\n    xaxis=dict(title = \"Week\"),\n    yaxis=dict(title = \"FVC\"),\n    plot_bgcolor='white'\n)\n\nfig.add_shape(\n            type=\"line\",\n            x0=0,\n            y0=min(FVC),\n            x1=0,\n            y1=max(FVC),\n            line=dict(\n                color=\"Red\",\n                width=2,\n                dash=\"dashdot\",\n            ),\n    )\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Explore all patients in the dataset ","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"print(\"Number of patients: \", len(train_data[\"Patient\"].unique()))\nl1 = list(train_data[\"SmokingStatus\"].unique())\nsmokers = \"\"\nfor i in l1:\n    smokers = smokers + i + \", \"\nprint(\"Among them: \", smokers[:-2])\nmin_Age = min(train_data[\"Age\"].unique())\nmax_Age = max(train_data[\"Age\"].unique())\nprint(\"Ages vary from \", min_Age,\" to \",max_Age)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Age distribution","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train_data[\"dummy\"] = 1\nfig = px.bar(train_data.drop_duplicates(subset=[\"Patient\"])[[\"Sex\",\"Age\",\"dummy\"]].groupby([\"Sex\",\"Age\"]).sum().reset_index().rename(columns={\"dummy\":\"Count\"}), x=\"Age\", y=\"Count\",color = \"Sex\")\nfig.update_layout(\n    plot_bgcolor='white'\n)\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Smokers distribution","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"fig = px.bar(train_data.drop_duplicates(subset=[\"Patient\"])[[\"Sex\",\"SmokingStatus\",\"dummy\"]].groupby([\"Sex\",\"SmokingStatus\"]).sum().reset_index().rename(columns={\"dummy\":\"Count\"}), x=\"SmokingStatus\", y=\"Count\",color = \"Sex\")\nfig.update_layout(\n    plot_bgcolor='white'\n)\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# First 15 patients FVC time series","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"fig = go.Figure()\nPat_Ids = train_data[\"Patient\"].unique()\nfor i in Pat_Ids[:15]:\n    fig.add_trace(go.Scatter(\n            x=train_data[train_data[\"Patient\"]==i][\"Weeks\"],\n            y=train_data[train_data[\"Patient\"]==i][\"FVC\"],\n            name = i\n        ))\nfig.update_layout(\n    plot_bgcolor='white'\n)\nfig.add_shape(\n            type=\"line\",\n            x0=0,\n            y0=1000,\n            x1=0,\n            y1=5000,\n            line=dict(\n                color=\"Red\",\n                width=2,\n                dash=\"dashdot\",\n            ),\n    )\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"As we can see from graphics above FVC is decreasing with the number of weeks, which may push us to use linear model.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Scatter plot to find any correlations in data","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"fig = px.scatter_matrix(train_data, dimensions=[\"Weeks\", \"FVC\", \"Percent\", \"Age\"], color=\"Sex\")\nfig.update_layout(\n    plot_bgcolor='white'\n)\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# As our target variable is FVC, we won't use it in PCA or any other technique to reduce dimensions","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Let's see how many images are available for each patient","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"num_files = []\nfor i in Pat_Ids:\n    num_files.append(len([name for name in os.listdir(IMG_DIR_TRAIN + i + '/') if os.path.isfile(os.path.join(IMG_DIR_TRAIN + i + '/', name))]))\nfig = go.Figure(go.Bar(name='SF Zoo',x=Pat_Ids,y=num_files))\nfig.update_layout(\n    plot_bgcolor='white'\n)\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_data = train_data.drop(columns =[\"dummy\"])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# From here we conclude, that some Patients contain a lot of photos in directory, and we should prepare the data before feeding it in our model.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# First let's define what model we will use\nFor this proect I decided to use a model on image","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"[![2020-08-17-15-21-38.png](https://i.postimg.cc/mkNWJwWK/2020-08-17-15-21-38.png)](https://postimg.cc/30RqD2RZ)","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"In the end we will get the lower bound, our result and the upper bound (15%, 50%, 85% quantiles).","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Data upload","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Normalize and resize our data while uploading","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"class DataGenCT(Sequence):\n    \n    def __init__(self, patients, dataset, cols, batch_size=32, train = 1):\n        \n        self.patients = [i for i in patients if i not in ['ID00011637202177653955184', 'ID00052637202186188008618']]\n        self.dataset = dataset\n        self.batch_size = batch_size\n        self.cols = cols\n        self.patient_scans = {}\n        self.train = train\n        IMG_DIR_TRAIN = \"/kaggle/input/osic-pulmonary-fibrosis-progression/train/\"\n        IMG_DIR_TEST = \"/kaggle/input/osic-pulmonary-fibrosis-progression/test/\"\n        if train:\n            self.IMG_DIR = IMG_DIR_TRAIN\n        else:\n            self.IMG_DIR = IMG_DIR_TEST\n        \n        for patient in patients:\n            self.patient_scans[patient] = natural_sort([i for i in os.listdir(self.IMG_DIR + patient + \"/\")])\n    \n    def __len__(self):\n        return 1100\n\n    def __getitem__(self,idx):\n        CT_Scan = []\n        Answer, Table = [], [] \n        \n        keys = np.random.choice(self.patients, size = self.batch_size)\n        for key in keys:\n            try:\n                idx = np.random.choice(self.patient_scans[key], size=1)[0]\n                dataset_copy = self.dataset[self.dataset[\"Patient\"] == key]\n                rand_week = random.choice(list(dataset_copy[\"Weeks\"]))\n\n                img = pydicom.dcmread(self.IMG_DIR + key + \"/\" + idx).pixel_array\n                img_min = img.min()\n                img_max = img.max()\n                img = cv2.resize((img - img_min) / (img_max - img_min), (IMAGE_SIZE, IMAGE_SIZE))\n                CT_Scan.append(img)\n                Answer.append(dataset_copy[dataset_copy[\"Weeks\"] == rand_week][\"FVC\"].values[0])\n                Table.append(dataset_copy[dataset_copy[\"Weeks\"] == rand_week][self.cols].values[0])\n            except Exception as e:\n                continue\n\n        CT_Scan = np.expand_dims(np.array(CT_Scan), axis=-1)\n        return [CT_Scan, np.array(Table)] , np.array(Answer)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Encode variables in .csv data","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"* Sex - Label encoder;\n* Smoking Status - Label encoder;","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"le_sex = LabelEncoder()\nle_smoke = LabelEncoder()\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"le_sex = le_sex.fit(train_data[\"Sex\"])\ntrain_data[\"Sex\"] = le_sex.transform(train_data[\"Sex\"])\nle_smoke = le_smoke.fit(train_data[\"SmokingStatus\"])\ntrain_data[\"SmokingStatus\"] = le_smoke.transform(train_data[\"SmokingStatus\"])\ntest_data[\"Sex\"] = le_sex.transform(test_data[\"Sex\"])\ntest_data[\"SmokingStatus\"] = le_smoke.transform(test_data[\"SmokingStatus\"])\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Normalize variables","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Before doing it, let's look at distributions of variables we want to normalize","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"fig = ff.create_distplot([train_data[\"Weeks\"].values], ['Weeks distribution'], show_rug=False)\nfig.update_layout(\n    plot_bgcolor='white'\n)\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"fig = ff.create_distplot([train_data[\"Percent\"].values], ['Percent distribution'], show_rug=False)\nfig.update_layout(\n    plot_bgcolor='white'\n)\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"fig = ff.create_distplot([train_data[\"Age\"].values], ['Age distribution'], show_rug=False)\nfig.update_layout(\n    plot_bgcolor='white'\n)\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"As we can see here, that for the first 2 distributions it is better to use RobustScaler, and for the third StandardScaler","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"transformer_weeks = RobustScaler().fit(np.array(train_data[\"Weeks\"]).reshape(-1, 1))\ntrain_data[\"Weeks\"] = transformer_weeks.transform(np.array(train_data[\"Weeks\"]).reshape(-1, 1)).reshape(1,-1)[0]\ntransformer_perc = RobustScaler().fit(np.array(train_data[\"Percent\"]).reshape(-1, 1))\ntrain_data[\"Percent\"] = transformer_perc.transform(np.array(train_data[\"Percent\"]).reshape(-1, 1)).reshape(1,-1)[0]\ntransformer_age = RobustScaler().fit(np.array(train_data[\"Age\"]).reshape(-1, 1))\ntrain_data[\"Age\"] = transformer_age.transform(np.array(train_data[\"Age\"]).reshape(-1, 1)).reshape(1,-1)[0]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_data[\"Weeks\"] = transformer_weeks.transform(np.array(test_data[\"Weeks\"]).reshape(-1, 1)).reshape(1,-1)[0]\ntest_data[\"Percent\"] = transformer_perc.transform(np.array(test_data[\"Percent\"]).reshape(-1, 1)).reshape(1,-1)[0]\ntest_data[\"Age\"] = transformer_age.transform(np.array(test_data[\"Age\"]).reshape(-1, 1)).reshape(1,-1)[0]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Now .csv and image data is ready","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train_data.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_data.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Make our model","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"* First define loss and metric according to the competition","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"C1 = tf.constant(70, dtype='float32')\nC2 = tf.constant(1000, dtype='float32')\nquantiles = [.15, .50, .85]\n\ndef metric(y_true, y_pred, Sigma):\n    Sigma_clipped = np.clip(Sigma, 70, 9e9)  \n    Delta = np.clip(np.abs(y_true - y_pred), 0 , 1000)  \n    return np.mean(-1 * (np.sqrt(2) * Delta / Sigma_clipped) - np.log(np.sqrt(2) * Sigma_clipped))\n\ndef FVC_score(y_true, y_pred):\n    y_true = tf.dtypes.cast(y_true, tf.float32)\n    y_pred = tf.dtypes.cast(y_pred, tf.float32)\n    sigma = y_pred[:, 2] - y_pred[:, 1]\n    fvc_pred = y_pred[:, 1]\n    Sigma_clipped = tf.maximum(sigma, C1)\n    Delta = tf.abs(y_true[:, 0] - fvc_pred)\n    Delta = tf.minimum(Delta, C2)\n    sq2 = tf.sqrt(tf.dtypes.cast(2, dtype=tf.float32))\n    metric = sq2 * (Delta / Sigma_clipped) * sq2 + tf.math.log(Sigma_clipped * sq2)\n    return K.mean(metric)\n\ndef Quantile_loss(y_true, y_pred):\n    q = tf.constant(np.array([quantiles]), dtype=tf.float32)\n    y_true = tf.dtypes.cast(y_true, tf.float32)\n    y_pred = tf.dtypes.cast(y_pred, tf.float32)\n    e = y_true - y_pred\n    v = tf.maximum(q * e, (q - 1) * e)\n    return K.mean(v)\n\ndef model_loss():\n    def loss(y_true, y_pred):\n        lambd = 0.8\n        return lambd * Quantile_loss(y_true, y_pred) + (1 - lambd) * FVC_score(y_true, y_pred)\n    return loss","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"* Define a net architecutre","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def identity_block(input_tensor, filters):\n  \n    filters1, filters2, filters3 = filters\n\n\n    x = Conv2D(filters1, (1, 1))(input_tensor)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n\n    x = Conv2D(filters2, (3, 3), padding='same')(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n\n    x = Conv2D(filters3, (1, 1))(x)\n    x = BatchNormalization()(x)\n\n    x = add([x, input_tensor])\n    x = Activation('relu')(x)\n    return x\n\ndef conv_block(input_tensor, filters):\n   \n    filters1, filters2, filters3 = filters\n\n\n    x = Conv2D(filters1, (1, 1), strides=(2, 2))(input_tensor)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n\n    x = Conv2D(filters2, 3, padding='same')(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n\n    x = Conv2D(filters3, (1, 1))(x)\n    x = BatchNormalization()(x)\n\n    shortcut = Conv2D(filters3, (1, 1), strides=(2, 2))(input_tensor)\n    shortcut = BatchNormalization()(shortcut)\n\n    x = add([x, shortcut])\n    x = Activation('relu')(x)\n    return x","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"start1 = Input(shape=(5,),name = \"Tab_input\")\nstart2 = Input(shape=(IMAGE_SIZE, IMAGE_SIZE,1), name = \"Image_input\")\n\nx = ZeroPadding2D((3, 3))(start2)\nx = Conv2D(64, (7, 7), strides=(2, 2))(x)\nx = BatchNormalization()(x)\nx = Activation('relu')(x)\nx = MaxPooling2D((3, 3), strides=(2, 2))(x)\n\nx = conv_block(x, [64, 64, 256])\nx = identity_block(x, [64, 64, 256])\nx = identity_block(x, [64, 64, 256])\n\nx = conv_block(x, [128, 128, 512])\nx = identity_block(x, [128, 128, 512])\nx = identity_block(x, [128, 128, 512])\nx = identity_block(x, [128, 128, 512])\n\nx = AveragePooling2D((7, 7))(x)\nx = Flatten()(x)\nx1 = Dense(100, activation=\"relu\")(start1)\nx1 = Dense(100, activation=\"relu\")(x1)\nx = concatenate([x, x1])\nx = Dense(50, activation=\"relu\")(x)\n\nout = Dense(3, activation='relu',)(x)\nmodel = Model([start2, start1], out)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model.compile(loss=model_loss(), \n              optimizer=tf.keras.optimizers.Adam(lr=0.1, beta_1=0.9, beta_2=0.999, \n                                                 epsilon=None, decay=0.01, amsgrad=False), \n              metrics=[FVC_score])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Add lr decreaser for better perfomance ","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"Lr_decr = ReduceLROnPlateau(\n    monitor='val_loss',\n    factor= 0.9,\n    patience=3,\n    min_lr=1e-5,\n    mode='min',\n    verbose = 1\n)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Make our Test and Train Generators","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"Test_generator = DataGenCT(patients=Pat_Ids[150:len(Pat_Ids)] ,\n                            dataset = train_data,\n                            cols= [\"Weeks\",\"Percent\",\"Age\",\"Sex\",\"SmokingStatus\"],\n                          )\n\n\nTrain_generator = DataGenCT(patients=Pat_Ids[0:150],\n                            dataset = train_data,\n                            cols= [\"Weeks\",\"Percent\",\"Age\",\"Sex\",\"SmokingStatus\"])\n\n\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Fit the model","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"history = model.fit_generator(Train_generator , \n                    steps_per_epoch = 100,\n                    epochs = 10,\n                    validation_data = Test_generator,\n                    use_multiprocessing = False,\n                    workers = 1,\n                    callbacks = [Lr_decr],\n                    validation_steps = 20,\n                    verbose=1\n                             )","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"fig = go.Figure()\n\nfig.add_trace(go.Scatter(\n        x=np.r_[1:11],\n        y=history.history[\"loss\"],\n        name = \"training loss\"\n    ))\nfig.add_trace(go.Scatter(\n        x=np.r_[1:11],\n        y=history.history[\"val_loss\"],\n        name = \"validation loss\"\n    ))\nfig.update_layout(\n    xaxis=dict(title = \"Epoch\"),\n    yaxis=dict(title = \"Loss\"),\n    plot_bgcolor='white',\n    title = \"Loss over epoch\"\n)\n\npy.offline.iplot(fig)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}