{"cells":[{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"import numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\nfrom math import pi\nimport itertools\nfrom glob import glob\n\nfrom scipy.optimize import curve_fit\nimport scipy.stats as ss\nfrom sklearn.metrics import confusion_matrix\n\nimport pydicom\n\nfrom bokeh.io import output_notebook, show\nfrom bokeh.palettes import Category20c\nfrom bokeh.palettes import Spectral4\nfrom bokeh.plotting import figure\nfrom bokeh.transform import cumsum\nfrom bokeh.models import ColumnDataSource, HoverTool, Panel\nfrom bokeh.models import Band\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport albumentations as A","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"TEST_PATH = '../input/osic-pulmonary-fibrosis-progression/test/'\nTRAIN_PATH = '../input/osic-pulmonary-fibrosis-progression/train/'\nTEST_CSV_PATH = '../input/osic-pulmonary-fibrosis-progression/test.csv'\nTRAIN_CSV_PATH = '../input/osic-pulmonary-fibrosis-progression/train.csv'","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Pulmonary Fibrosis Progression EDA","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Better understading the prognosis of [pulmonary fibrosis](https://en.wikipedia.org/wiki/Pulmonary_fibrosis) disease could not only help the patients and their families, but also positively impact treatment trial design and accelerate the clinical development of novel treatments.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"## Overview of the CSV Datasets","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"First of all let's overview the patients information: number of patients, gender, smoking status and age.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train_df = pd.read_csv(TRAIN_CSV_PATH)\ntest_df = pd.read_csv(TEST_CSV_PATH)\n\ntrain_df.head()","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"# See the example of pie chart implementation in bokeh:\n# https://docs.bokeh.org/en/latest/docs/gallery/pie_chart.html?highlight=pie%20chart\ndef plot_pie_chart(data_dict, varname, title=''):\n    \"\"\"Plots the pie chart of the data in the dictionary.\"\"\"\n\n    data = pd.Series(data_dict).reset_index(name='value').rename(columns={'index': varname})\n    data['angle'] = data['value']/data['value'].sum() * 2*pi\n    data['color'] = Category20c[max(3,len(data_dict))][:len(data_dict)]\n\n    p = figure(plot_height=350, title=title, toolbar_location=None,\n            tools=\"hover\", tooltips=\"@{}: @value\".format(varname))\n\n    p.wedge(x=0, y=1, radius=0.4,\n            start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'),\n            line_color=\"white\", fill_color='color', source=data, legend_field=varname)\n\n    output_notebook()\n    show(p)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_pie_chart({'train (records)':len(train_df), 'test (records)':len(test_df)}, 'datasets', title='Train vs Test dataset size')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_pie_chart({'train (patients)':train_df.Patient.nunique(), 'test (patients)':test_df.Patient.nunique()}, 'datasets', title='Number of unique patients')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"patient_df = train_df[['Patient', 'Sex', 'SmokingStatus', 'Age']].drop_duplicates()\nmale = patient_df[patient_df.Sex == 'Male'].shape[0]\nfemale = patient_df[patient_df.Sex == 'Female'].shape[0]\n\nplot_pie_chart({'male':male, 'female':female}, 'gender', title='Gender in Train Set')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"exsmoker = patient_df[patient_df.SmokingStatus == 'Ex-smoker'].shape[0]\nneversmoker = patient_df[patient_df.SmokingStatus == 'Never smoked'].shape[0]\ncursmoker = patient_df[patient_df.SmokingStatus == 'Currently smokes'].shape[0]\n\nplot_pie_chart({'Ex-smoker':exsmoker, 'Never smoked':neversmoker, 'Currently smokes':cursmoker}, 'SmokingStatus', title='Smoking Status in Train set')","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"# See this article on how to plot bar charts with Bokeh:\n# https://towardsdatascience.com/interactive-histograms-with-bokeh-202b522265f3\ndef hist_hover(dataframe, column, colors=[\"#94c8d8\", \"#ea5e51\"], bins=30, title=''):\n    hist, edges = np.histogram(dataframe[column], bins = bins)\n    \n    hist_df = pd.DataFrame({column: hist,\n                             \"left\": edges[:-1],\n                             \"right\": edges[1:]})\n    hist_df[\"interval\"] = [\"%d to %d\" % (left, right) for left, \n                           right in zip(hist_df[\"left\"], hist_df[\"right\"])]\n\n    src = ColumnDataSource(hist_df)\n    plot = figure(plot_height = 400, plot_width = 600,\n          title = title,\n          x_axis_label = column,\n          y_axis_label = \"Count\")    \n    plot.quad(bottom = 0, top = column,left = \"left\", \n        right = \"right\", source = src, fill_color = colors[0], \n        line_color = \"#35838d\", fill_alpha = 0.7,\n        hover_fill_alpha = 0.7, hover_fill_color = colors[1])\n        \n    hover = HoverTool(tooltips = [('Interval', '@interval'),\n                              ('Count', str(\"@\" + column))])\n    plot.add_tools(hover)\n    \n    output_notebook()\n    show(plot)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"hist_hover(patient_df, 'Age', colors=[\"#94c8d8\", \"#ea5e51\"], bins=30, title='Distribution of Age in train dataset')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Some quick conclusions:\n* We have 176 patiens in the train dataset with 1549 observations in total;\n* Most of the patients are male;\n* Most of the patients are ex-smokers;\n* Most of the patients are between 60 and 80 years old;","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Now let's look at FVC progressions. Plot FVC curves for random patients from the train set (you can use the interactive tools of the charts to explore):","execution_count":null},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"# Example of a line chart in bokeh:\n# https://docs.bokeh.org/en/latest/docs/user_guide/interaction/legends.html\ndef plot_FVC(data, patient_ids):\n    p = figure(plot_width=800, plot_height=300)\n    p.title.text = 'FVC progression'\n\n    for patient_id, color in zip(patient_ids, Spectral4):\n        df = data[data.Patient == patient_id].sort_values(by=['Weeks'], ascending=True)\n        p.line(df['Weeks'], df['FVC'], line_width=2, color=color, alpha=0.9, legend_label='{}'.format(patient_id))\n        s1 = p.scatter(df['Weeks'], df['FVC'], marker=\"circle\", size=7,\n              line_color=color, fill_color=color, alpha=1)\n        p.add_tools(HoverTool(renderers=[s1], tooltips={\"Week\":\"$x\", \"FVC\":\"$y\"}))\n\n    p.legend.location = \"center_right\"\n    p.legend.click_policy=\"hide\"\n\n    output_notebook()\n\n    show(p)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"patient_ids_rnd = [patient_df.iloc[np.random.randint(len(patient_df))].Patient for i in range(3)]\n\nplot_FVC(train_df, patient_ids_rnd)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"As we see, the FVC usually decreases through time. But the firts few observations of FVC usually don't show any trend (it looks like they just oscillate).","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Let's plot the distribution of the FVC for each week:","execution_count":null},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"plt.figure(figsize=(20,7))\n\nweekly_df = train_df.pivot(columns='Weeks', values='FVC')\nweeks = weekly_df.columns.values\n\ndata = []\nfor col in weeks:\n    x = weekly_df[col].values\n    x = x[~np.isnan(x)]\n    data.append(x)\n    \nplt.boxplot(data)\nplt.title('Distribution of FVC by Week')\n\nplt.xticks(range(len(weeks)), weeks, rotation=90)\n\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"It looks like in general the FVC just oscillates around some value over the weeks. We should consider different factors to predict the FVC.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Let's try to fit a simple exponential model to our data:","execution_count":null},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"def exp_func(x, a, c, d):\n    \"\"\"Exponential function.\"\"\"\n    return a*np.exp(-c*x)+d\n\ndef linear_func(x, a, b):\n    \"\"\"Linear function.\"\"\"\n    return a*x+b\n\ndef fit_exp_curve(x, y):\n    \"\"\"Fit the exponential curve to the data.\"\"\"\n    popt, pcov = curve_fit(exp_func, x, y, p0=(1, 1e-6, 1))\n    \n    return popt\n\ndef fit_linear_curve(x, y):\n    \"\"\"Fit the linear curve to the data.\"\"\"\n    popt, pcov = curve_fit(linear_func, x, y, p0=(1, 1))\n    \n    return popt\n\ndef fit_curve_fvc(data, patient_id, function='exp'):\n    \"\"\"Fit the exponential curve to the patient's data.\"\"\"\n    x = data[data.Patient == patient_id].sort_values(by=['Weeks'], ascending=True).Weeks\n    y = data[data.Patient == patient_id].sort_values(by=['Weeks'], ascending=True).FVC\n    \n    if function == 'exp':\n        popt = fit_exp_curve(x, y)\n    elif function == 'linear':\n        popt = fit_linear_curve(x, y)\n    else:\n        raise ValueError('function should be one of: linear, exp')\n    \n    return popt\n\ndef get_prediction_interval(y, yhat):\n    \"\"\"Compute the prediction interval.\n    \n    Read more: https://machinelearningmastery.com/prediction-intervals-for-machine-learning/s\n    \"\"\"\n    sum_errs = np.sum((y - yhat)**2)\n    stdev = np.sqrt(1/(len(y)-2) * sum_errs)\n    \n    interval = 1.96 * stdev # 1.96 stdev contains 95% of values\n    lower, upper = yhat - interval, yhat + interval\n    \n    return lower, upper, interval\n\ndef plot_curve(data, patient_id):\n    \"\"\"Plot patient's FVC and the fitted curve.\"\"\"\n    p = figure(plot_width=800, plot_height=300)\n    p.title.text = 'FVC progression for {}'.format(patient_id)\n\n    df = data[data.Patient == patient_id].sort_values(by=['Weeks'], ascending=True)\n    p.line(df['Weeks'], df['FVC'], line_width=2, color=\"#94c8d8\", alpha=0.9, legend_label='FVC')\n    s1 = p.scatter(df['Weeks'], df['FVC'], marker=\"circle\", size=7,\n          line_color=\"#94c8d8\", fill_color=\"#94c8d8\", alpha=1)\n    p.add_tools(HoverTool(renderers=[s1], tooltips={\"Week\":\"$x\", \"FVC\":\"$y\"}))\n    \n    # plot fitted exponential curve\n    popt = fit_curve_fvc(data, patient_id)\n    x = np.linspace(0, 80, 100)\n    y = [exp_func(x, popt[0], popt[1], popt[2]) for x in np.linspace(0, 80, 100)]\n    p.line(x, y, line_width=2, color=\"#ea5e51\", alpha=0.9, legend_label='exponential curve', line_dash='dashed')\n    \n    # plot fitted linear curve\n    popt = fit_curve_fvc(data, patient_id, function='linear')\n    x = np.linspace(0, 80, 100)\n    y = [linear_func(x, popt[0], popt[1]) for x in np.linspace(0, 80, 100)]\n    p.line(x, y, line_width=2, color='#b35da6', alpha=0.9, legend_label='linear curve, a={:0.2f}, b={:0.2f}'.format(popt[0], popt[1]), line_dash='dashed')\n\n    # get the prediction interval\n    x = data[data.Patient == patient_id].sort_values(by=['Weeks'], ascending=True).Weeks\n    y_true = data[data.Patient == patient_id].sort_values(by=['Weeks'], ascending=True).FVC\n    yhat = [linear_func(xi, popt[0], popt[1]) for xi in x]\n    \n    lower, upper, interval = get_prediction_interval(y_true, yhat)\n    \n    source = ColumnDataSource({\n        'base': np.linspace(0, 80, 100),\n        'lower':[linear_func(x, popt[0], popt[1])-interval for x in np.linspace(0, 80, 100)],\n        'upper':[linear_func(x, popt[0], popt[1])+interval for x in np.linspace(0, 80, 100)],\n        })\n    band = Band(base='base', lower='lower', upper='upper', level='underlay', source=source,\n            fill_alpha=0.5, line_width=1, line_color=None)\n    p.add_layout(band)\n    \n    p.legend.location = \"bottom_left\"\n    p.legend.click_policy=\"hide\"\n\n    output_notebook()\n\n    show(p)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_curve(train_df, 'ID00030637202181211009029')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_curve(train_df, 'ID00305637202281772703145')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_curve(train_df, 'ID00027637202179689871102')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Now let's try to fit the curves and look how age/gender/smoking status is related to the fitted curve parameters.","execution_count":null},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"# fit exponential curves to all patient's records\nparams_df = pd.DataFrame()\n# loop through unique patients\nfor i, row in patient_df.iterrows():\n    try:\n        # fit curve to the patient's records\n        exp_a, exp_c, exp_d =  fit_curve_fvc(train_df, row.Patient)\n        lin_a, lin_b =  fit_curve_fvc(train_df, row.Patient, function='linear')\n    except:\n        # curve fitting failed\n        continue\n    # add the params to the dataframe\n    params_df = params_df.append(\n    {\n        'Patient': row.Patient,\n        'Age': row.Age,\n        'Sex': row.Sex,\n        'SmokingStatus': row.SmokingStatus,\n        'exp_a': exp_a,\n        'exp_c': exp_c,\n        'exp_d': exp_d,\n        'lin_a': lin_a,\n        'lin_b': lin_b,\n    }, ignore_index = True)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's explore the correlations between patient's age/gender/smoking status and the parameters of the fitted curve.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"params_dummies_df = pd.get_dummies(params_df[['Age', 'Sex', 'SmokingStatus', 'exp_a', 'exp_c', 'exp_d', 'lin_a', 'lin_b']])\ncorr_df = params_dummies_df.corr()\n\nfig = plt.figure(figsize=(10,7))\nsns.heatmap(corr_df, annot=True, cmap=sns.diverging_palette(220, 20, as_cmap=True))\nplt.title('Correlation Heatmap')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Quick conclusions:\n* Coefficients of the linear model have some correlation with other patient's features, the exponential model coefficients don't have any correlation with other features at all. Does it mean that linear model is better? I don't think that linear model has to be better, because correlation only reflects linear relationships.\n* The slope (lin_a) of the linear model has slight positive correlation with SmokingStatus_CurrentlySmokes. Current smokers have higher slope coefficients. Could it mean that FVC declines faster for non-smikong people? :)\n* The intercept (lin_b) has somewhat correlation with gender. It doesn't mean that FVC is higher for male patients, it just means that male patients may have larger lungs.\n* The intercept has slight positive correlation with ex-smoker status and slight negative correlation with never smoked status. \n\nIn general, it makes sense to add age, gender and smoking status to the final model.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"## Overview of the Images","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Let's look at some images:","execution_count":null},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"# get the paths to the images from train dataset\ntrain_fns = glob(TRAIN_PATH+'/*/*')\n\ndef plot_images(train_data, n_rows=3, n_cols=3):\n    ids = np.random.randint(0, len(train_fns), n_rows*n_cols)\n    fig, axs = plt.subplots(n_rows, n_cols, figsize=(15, 15))\n    \n    for i in range(n_rows):\n        for j in range(n_cols):\n            filename = train_fns[ids[i*n_cols + j]]\n            idx = filename.split('/')[-2]\n            \n            data = train_data[(train_data.Patient == idx)]\n            age = data.Age.values[0]\n            gender = data.Sex.values[0]\n            fvc = None\n            data = train_data[(train_data.Patient == idx) & (train_data.Weeks == 0)]\n            if len(data) > 0:\n                fvc = data.FVC.values[0]\n            \n            img = pydicom.read_file(filename).pixel_array\n            axs[i,j].imshow(img, cmap='bone')\n            axs[i,j].axis('off')\n            if fvc:\n                axs[i,j].set_title('{}, age {} \\n FVC {}'.format(gender, age, fvc))\n            else:\n                axs[i,j].set_title('{}, age {}'.format(gender, age))\n     \n    plt.suptitle('CTs')\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_images(train_df)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"FVC images all have same sizes (512 X 512), but they all have different intensities, so using some filters like CLAHE will be very helpful.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Let's compare the brightests and the darkest images:","execution_count":null},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"def get_brightness(filename):\n    \"\"\"Open the file and return the average brightness of the pixels.\"\"\"\n    img = pydicom.read_file(filename).pixel_array\n    # return the average intensity of the pixels\n    return np.mean(np.where(img>0, img,0))\n\ndef generate_brightness_df(train_data):\n    \"\"\"Generates pandas dataframe with average brightness for each image.\"\"\"\n    brightness_df = pd.DataFrame()\n    \n    for fname in train_fns:\n        try:\n            brightness = get_brightness(fname)\n            idx = fname.split('/')[-2]\n\n            brightness_df = brightness_df.append(\n            {\n                'Patient':idx,\n                'Fname':fname,\n                'Brightness':brightness,\n            },\n            ignore_index=True)\n        except:\n            continue\n        \n    return brightness_df\n\ndef plot_top_bright(brighness_df, n_rows=3, n_cols=3, bright=True):\n    \"\"\"Plot top brightest/darkest images.\"\"\"\n    if bright:\n        title = 'Top {} brightest images'.format(n_rows*n_cols)\n        data = brighness_df.sort_values(by=['Brightness'], ascending=True)\n    else:\n        title = 'Top {} darkest images'.format(n_rows*n_cols)\n        data = brighness_df.sort_values(by=['Brightness'], ascending=False)\n        \n    fig, axs = plt.subplots(n_rows, n_cols, figsize=(15, 15))\n    \n    for i in range(n_rows):\n        for j in range(n_cols):\n            \n            row = data.iloc[i*n_cols+j]\n            fname = row.Fname\n            \n            img = pydicom.read_file(fname).pixel_array\n            \n            axs[i,j].imshow(img, cmap='bone')\n            axs[i,j].set_title(row.Patient)\n            axs[i,j].axis('off')\n            \n    plt.suptitle(title)\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"brightness_df = generate_brightness_df(train_df)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_top_bright(brightness_df)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_top_bright(brightness_df, bright=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's try some augmentations:","execution_count":null},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"# took an example of augmentation pipeline from here:\n# https://github.com/albumentations-team/albumentations_examples/blob/master/notebooks/example.ipynb\ntransform = A.Compose([\n    A.HorizontalFlip(p=0.5),\n    A.ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.1, rotate_limit=10, p=.5),\n    A.Blur(blur_limit=3),\n    A.OpticalDistortion(),\n    A.GridDistortion(),\n])\n\ndef visualize_augmentations(n_rows=3, n_cols=3):\n    idx = np.random.randint(0, len(train_fns))\n    fname = train_fns[idx]\n    image = pydicom.read_file(fname).pixel_array\n    \n    fig, axs = plt.subplots(n_rows, n_cols, figsize=(15, 15))\n    for i in range(n_rows):\n        for j in range(n_cols):\n            \n            augmented_image = transform(image=image)['image']\n\n            axs[i,j].imshow(augmented_image, cmap='bone')\n            axs[i,j].axis('off')\n\n    plt.suptitle('Examples of augmentations {}'.format(fname.split('/')[-2]))\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"visualize_augmentations()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Useful Links\n\n1. [Challenges in pulmonary fibrosis](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2117220/). Article contains some examples of CT scans with pulmonary fibrosis.\n2. [Previous Pneumothorax Segmentation competition](https://www.kaggle.com/c/siim-acr-pneumothorax-segmentation). This competition was also related to the lung images. Some image augmentation methods used in this competition may be applicable.\n3. [Computer-Aided Diagnosis of Pulmonary Fibrosis Using Deep Learning and CT Images](https://journals.lww.com/investigativeradiology/FullText/2019/10000/Computer_Aided_Diagnosis_of_Pulmonary_Fibrosis.2.aspx). A related article. Could be very helpful here.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Conclusions\n\nIn this analysis we:\n* looked at the distributions of patients' features and FVC in the dataset;\n* tried to apply curve fitting to find some relationships between the features (age, gender, smoking status) and the FVC trend;\n* viewed some CT scans and applied little augmentations.","execution_count":null}],"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}