{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\n\n# Adaptez les chemins selon l'emplacement de vos fichiers téléchargés\npath = \"/kaggle/input/competitions/ashrae-energy-prediction/\"\ntrain = pd.read_csv(path + \"train.csv\")\nbuilding = pd.read_csv(path + \"building_metadata.csv\")\nweather = pd.read_csv(path + \"weather_train.csv\")\n\n# 1. Dimensions de chaque table\nprint(\"train:\", train.shape)\nprint(\"building_metadata:\", building.shape)\nprint(\"weather_train:\", weather.shape)\n\n# 2. Aperçu des colonnes et types\nprint(train.info())\nprint(building.info())\nprint(weather.info())\n\n# 3. Premières lignes pour comprendre la structure\nprint(train.head())\nprint(building.head())\nprint(weather.head())\n\n# 4. Statistiques de base sur la variable cible\nprint(train[\"meter_reading\"].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:19:48.567011Z","iopub.execute_input":"2026-06-16T15:19:48.567369Z","iopub.status.idle":"2026-06-16T15:20:06.237578Z","shell.execute_reply.started":"2026-06-16T15:19:48.567336Z","shell.execute_reply":"2026-06-16T15:20:06.236851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd \nimport numpy as np\n\n# Define your paths at the top so they are globally accessible\ninput_path = \"/kaggle/input/competitions/ashrae-energy-prediction/\"\noutput_path = \"/kaggle/working/\"\n\ndef reduce_mem_usage(df):\n    start_mem = df.memory_usage().sum() / 1024**2\n    for col in df.columns:\n        col_type = df[col].dtype\n        # Skip text and datetime columns — don't touch them\n        if col_type == object or pd.api.types.is_datetime64_any_dtype(df[col]):\n            continue\n\n        c_min, c_max = df[col].min(), df[col].max()\n        if str(col_type)[:3] == 'int':\n            if c_min >= 0:\n                df[col] = df[col].astype(np.uint32) if c_max < 4294967295 else df[col]\n            else:\n                df[col] = df[col].astype(np.int32)\n        else:\n            df[col] = df[col].astype(np.float32)\n    end_mem = df.memory_usage().sum() / 1024**2\n    print(f\"Memory reduced from {start_mem:.1f} MB to {end_mem:.1f} MB\")\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:06.239560Z","iopub.execute_input":"2026-06-16T15:20:06.239883Z","iopub.status.idle":"2026-06-16T15:20:06.247476Z","shell.execute_reply.started":"2026-06-16T15:20:06.239856Z","shell.execute_reply":"2026-06-16T15:20:06.246577Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -----------------------------------------------\n# STEP 2.1 — Load train.csv\n# -----------------------------------------------\ntrain = pd.read_csv(input_path + \"train.csv\", parse_dates=[\"timestamp\"])\nprint(\"train shape:\", train.shape)\nprint(train.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:06.248712Z","iopub.execute_input":"2026-06-16T15:20:06.249081Z","iopub.status.idle":"2026-06-16T15:20:18.192513Z","shell.execute_reply.started":"2026-06-16T15:20:06.249054Z","shell.execute_reply":"2026-06-16T15:20:18.191514Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -----------------------------------------------\n# STEP 2.2 — Load building_metadata.csv\n# -----------------------------------------------\nbuilding = pd.read_csv(input_path + \"building_metadata.csv\")\nprint(\"building shape:\", building.shape)\nprint(building.head())\n\n# -----------------------------------------------\n# STEP 2.3 — Load weather_train.csv\n# -----------------------------------------------\nweather = pd.read_csv(input_path + \"weather_train.csv\", parse_dates=[\"timestamp\"])\nprint(\"weather shape:\", weather.shape)\nprint(weather.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:18.193695Z","iopub.execute_input":"2026-06-16T15:20:18.194442Z","iopub.status.idle":"2026-06-16T15:20:18.373724Z","shell.execute_reply.started":"2026-06-16T15:20:18.194412Z","shell.execute_reply":"2026-06-16T15:20:18.373010Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -----------------------------------------------\n# STEP 2.4 — Optimize memory (FIXED function)\n# -----------------------------------------------\ntrain = reduce_mem_usage(train)\nweather = reduce_mem_usage(weather)\n\n# -----------------------------------------------\n# STEP 2.5 — Filter: electricity meter only (meter == 0)\n# -----------------------------------------------\ntrain_elec = train[train[\"meter\"] == 0]\nprint(\"Rows for electricity only:\", train_elec.shape[0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:18.375665Z","iopub.execute_input":"2026-06-16T15:20:18.375968Z","iopub.status.idle":"2026-06-16T15:20:19.242341Z","shell.execute_reply.started":"2026-06-16T15:20:18.375942Z","shell.execute_reply":"2026-06-16T15:20:19.241493Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -----------------------------------------------\n# STEP 2.6 — Select buildings from 3 sites\n# -----------------------------------------------\nselected_sites = [0, 1, 2]   # change to any 3 site IDs between 0 and 15\nbuildings_subset = building[building[\"site_id\"].isin(selected_sites)]\nprint(\"Number of buildings on selected sites:\", buildings_subset[\"building_id\"].nunique())\nprint(\"Buildings per site:\")\nprint(buildings_subset[\"site_id\"].value_counts())\n\ntrain_elec = train_elec[train_elec[\"building_id\"].isin(buildings_subset[\"building_id\"])]\nprint(\"Rows after site filter:\", train_elec.shape[0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:19.243397Z","iopub.execute_input":"2026-06-16T15:20:19.243634Z","iopub.status.idle":"2026-06-16T15:20:19.512358Z","shell.execute_reply.started":"2026-06-16T15:20:19.243612Z","shell.execute_reply":"2026-06-16T15:20:19.511456Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -----------------------------------------------\n# STEP 2.7 — Merge with building_metadata\n# -----------------------------------------------\ndata = train_elec.merge(building, on=\"building_id\", how=\"left\")\nprint(\"Shape after merging with building_metadata:\", data.shape)\nprint(\"Columns so far:\", data.columns.tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:19.513454Z","iopub.execute_input":"2026-06-16T15:20:19.513772Z","iopub.status.idle":"2026-06-16T15:20:19.828183Z","shell.execute_reply.started":"2026-06-16T15:20:19.513735Z","shell.execute_reply":"2026-06-16T15:20:19.827398Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -----------------------------------------------\n# STEP 2.8 — Merge with weather_train\n# -----------------------------------------------\ndata = data.merge(weather, on=[\"site_id\", \"timestamp\"], how=\"left\")\nprint(\"Shape after merging with weather:\", data.shape)\nprint(data.head())\n\n# -----------------------------------------------\n# STEP 2.9 — Check missing values\n# -----------------------------------------------\nprint(\"\\nMissing values per column:\")\nprint(data.isna().sum())\n\n# -----------------------------------------------\n# STEP 2.10 — Save the working dataset\n# -----------------------------------------------\ndata.to_csv(output_path + \"working_dataset.csv\", index=False)\nprint(\"\\nSaved: working_dataset.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:19.829286Z","iopub.execute_input":"2026-06-16T15:20:19.829777Z","iopub.status.idle":"2026-06-16T15:20:49.882851Z","shell.execute_reply.started":"2026-06-16T15:20:19.829740Z","shell.execute_reply":"2026-06-16T15:20:49.882149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom scipy import stats\nfrom sklearn.preprocessing import StandardScaler, MinMaxScaler\n\n# -----------------------------------------------\n# Load the working dataset from Step 2\n# -----------------------------------------------\ndata = pd.read_csv(\"/kaggle/working/working_dataset.csv\", parse_dates=[\"timestamp\"])\nprint(\"Loaded dataset shape:\", data.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:49.883830Z","iopub.execute_input":"2026-06-16T15:20:49.884236Z","iopub.status.idle":"2026-06-16T15:20:55.074511Z","shell.execute_reply.started":"2026-06-16T15:20:49.884210Z","shell.execute_reply":"2026-06-16T15:20:55.073583Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===============================================\n# 3.1 — TRAITEMENT DES VALEURS MANQUANTES\n# (Slides 17-22)\n# ===============================================\n\nprint(\"\\n--- Missing values before treatment ---\")\nprint(data.isna().sum())\n\n# Suppression de colonne : floor_count (82% missing — not informative)\ndata = data.drop(columns=[\"floor_count\"])\nprint(\"\\nDropped floor_count column.\")\n\n# Remplissage par la médiane : year_built\ndata[\"year_built\"] = data[\"year_built\"].fillna(data[\"year_built\"].median())\n\n# Remplissage par forward fill : small weather gaps (air_temp, dew_temp, wind)\nfor col in [\"air_temperature\", \"dew_temperature\", \"wind_speed\",\n            \"sea_level_pressure\", \"wind_direction\"]:\n    data[col] = data[col].ffill().bfill()\n\n# Remplissage par la médiane : cloud_coverage (too many gaps for ffill)\ndata[\"cloud_coverage\"] = data[\"cloud_coverage\"].fillna(data[\"cloud_coverage\"].median())\n\n# Remplissage par constante 0 : precip_depth_1_hr (no rain = most common)\ndata[\"precip_depth_1_hr\"] = data[\"precip_depth_1_hr\"].fillna(0)\n\nprint(\"\\n--- Missing values after treatment ---\")\nprint(data.isna().sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:55.075844Z","iopub.execute_input":"2026-06-16T15:20:55.076251Z","iopub.status.idle":"2026-06-16T15:20:55.769245Z","shell.execute_reply.started":"2026-06-16T15:20:55.076223Z","shell.execute_reply":"2026-06-16T15:20:55.768433Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 3.2 — TRAITEMENT DES VALEURS ABERRANTES\n# (Slides 32-36)\n# ===============================================\n\n# Remove zero and negative meter_reading (physically impossible)\nprint(f\"\\nRows before removing zeros/negatives: {data.shape[0]}\")\ndata = data[data[\"meter_reading\"] > 0]\nprint(f\"Rows after removing zeros/negatives: {data.shape[0]}\")\n\n# --- VISUALISATION DES VALEURS ABERRANTES (Slides 34-36) ---\nprint(\"\\nGenerating outlier detection plots...\")\nfig, axes = plt.subplots(1, 2, figsize=(15, 5))\n\n# 1. Boxplot (Boîte à moustaches) \nsns.boxplot(x=data[\"meter_reading\"], ax=axes[0], color=\"skyblue\")\naxes[0].set_title(\"Boîte à moustaches (Boxplot) — meter_reading avant traitement\")\naxes[0].set_xlabel(\"Energy Consumption (kWh)\")\n\n# 2. Scatter Plot (Nuage de points)\naxes[1].scatter(range(len(data)), data[\"meter_reading\"], alpha=0.5, color=\"crimson\", s=10)\naxes[1].set_title(\"Nuage de points (Scatter Plot) — Anomalies de distribution\")\naxes[1].set_xlabel(\"Index des observations\")\naxes[1].set_ylabel(\"Energy Consumption (kWh)\")\n\nplt.tight_layout()\nplt.show()\n\n# Detect outliers using IQR method (as shown in your slides)\nQ1 = data[\"meter_reading\"].quantile(0.25)\nQ3 = data[\"meter_reading\"].quantile(0.75)\nIQR = Q3 - Q1\nlower = Q1 - 1.5 * IQR\nupper = Q3 + 1.5 * IQR\n\noutliers = data[(data[\"meter_reading\"] < lower) | (data[\"meter_reading\"] > upper)]\nprint(f\"\\nOutliers detected: {len(outliers)} rows ({100*len(outliers)/len(data):.1f}%)\")\n# For energy data: cap instead of remove (extreme values can be real)\ndata[\"meter_reading\"] = data[\"meter_reading\"].clip(lower=lower, upper=upper)\nprint(\"Outliers capped using IQR method.\")\n\n# --- VISUALISATION APRÈS TRAITEMENT ---\nplt.figure(figsize=(8, 4))\nsns.boxplot(x=data[\"meter_reading\"], color=\"lightgreen\")\nplt.title(\"Boîte à moustaches après traitement (Capping IQR appliqué)\")\nplt.xlabel(\"Energy Consumption (kWh)\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:20:55.770146Z","iopub.execute_input":"2026-06-16T15:20:55.770383Z","iopub.status.idle":"2026-06-16T15:21:06.316228Z","shell.execute_reply.started":"2026-06-16T15:20:55.770361Z","shell.execute_reply":"2026-06-16T15:21:06.315323Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===============================================\n# 3.3 — TRANSFORMATION DES DONNÉES\n# (Slides 23-28)\n# ===============================================\n\n# --- Fix site 0 unit issue (kBTU → kWh) ---\ndata.loc[data[\"site_id\"] == 0, \"meter_reading\"] = (\n    data.loc[data[\"site_id\"] == 0, \"meter_reading\"] * 0.293\n)\nprint(\"\\nSite 0 unit fix applied (kBTU → kWh).\")\n\n# --- Log transformation on meter_reading (slide 27-28) ---\ndata[\"meter_reading_log\"] = np.log1p(data[\"meter_reading\"])\n\n# Visualize before and after log transform (as recommended in slide 27)\nfig, axes = plt.subplots(1, 2, figsize=(12, 4))\naxes[0].hist(data[\"meter_reading\"].clip(upper=data[\"meter_reading\"].quantile(0.99)),\n             bins=50, color=\"steelblue\", edgecolor=\"white\")\naxes[0].set_title(\"meter_reading — original (skewed)\")\naxes[0].set_xlabel(\"kWh\")\naxes[1].hist(data[\"meter_reading_log\"], bins=50,\n             color=\"darkorange\", edgecolor=\"white\")\naxes[1].set_title(\"meter_reading_log — after log1p (more normal)\")\naxes[1].set_xlabel(\"log(1 + kWh)\")\nplt.tight_layout()\nplt.show()\n\n# --- Encoding des variables catégorielles (slide 24) ---\ndata[\"primary_use_encoded\"] = data[\"primary_use\"].astype(\"category\").cat.codes\nprint(\"\\nEncoding mapping (primary_use):\")\nprint(dict(enumerate(data[\"primary_use\"].astype(\"category\").cat.categories)))\n\n# --- Extraction des variables temporelles ---\ndata[\"hour\"] = data[\"timestamp\"].dt.hour\ndata[\"day_of_week\"] = data[\"timestamp\"].dt.dayofweek\ndata[\"month\"] = data[\"timestamp\"].dt.month\ndata[\"is_weekend\"] = (data[\"day_of_week\"] >= 5).astype(int)\n\n# Drop columns no longer needed\ndata = data.drop(columns=[\"timestamp\", \"meter\", \"primary_use\"])\n\n# --- Standardisation (slide 25) ---\nscaler = StandardScaler()\ncols_to_scale = [\"square_feet\", \"year_built\", \"air_temperature\",\n                 \"dew_temperature\", \"wind_speed\", \"sea_level_pressure\",\n                 \"cloud_coverage\", \"precip_depth_1_hr\", \"wind_direction\"]\n\ndata_scaled = data.copy()\ndata_scaled[cols_to_scale] = scaler.fit_transform(data[cols_to_scale])\nprint(\"\\nStandardization applied to numeric features.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:06.317379Z","iopub.execute_input":"2026-06-16T15:21:06.317658Z","iopub.status.idle":"2026-06-16T15:21:08.167210Z","shell.execute_reply.started":"2026-06-16T15:21:06.317633Z","shell.execute_reply":"2026-06-16T15:21:08.166281Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===============================================\n# 3.4 — FEATURE SELECTION PAR CORRÉLATION\n# (Slides 40-43)\n# ===============================================\n\n# Compute correlation matrix with the target\nnumeric_cols = data_scaled.select_dtypes(include=[np.number]).columns.tolist()\ncorr_matrix = data_scaled[numeric_cols].corr()\n\n# -------------------------------------------------------------------------\n# ADDED/RESTORED: TARGET CORRELATION BAR CHART (Slide 41 Metric Representation)\n# -------------------------------------------------------------------------\ntarget_corr = corr_matrix[\"meter_reading_log\"].drop(\"meter_reading_log\").sort_values(ascending=False)\nprint(\"\\n--- Correlation with meter_reading_log ---\")\nprint(target_corr)\n\nplt.figure(figsize=(10, 5))\ntarget_corr.plot(kind=\"bar\", color=\"coral\", edgecolor=\"black\")\nplt.axhline(y=0.05, color=\"red\", linestyle=\"--\", label=\"Threshold (+0.05)\")\nplt.axhline(y=-0.05, color=\"red\", linestyle=\"--\", label=\"Threshold (-0.05)\")\nplt.title(\"Linear Correlation of Features with Target (meter_reading_log)\")\nplt.ylabel(\"Pearson Correlation Coefficient\")\nplt.xlabel(\"Features\")\nplt.legend()\nplt.tight_layout()\nplt.show()\n# -------------------------------------------------------------------------\n# Heatmap of full correlation matrix (as shown in slides 42-43)\nplt.figure(figsize=(12, 8))\nsns.heatmap(corr_matrix, annot=True, fmt=\".2f\", cmap=\"coolwarm\", center=0, linewidths=0.5)\nplt.title(\"Correlation Matrix Heatmap — Multi-collinearity Check\")\nplt.tight_layout()\nplt.show()\n\n# Keep features with correlation > 0.05 with the target\nrelevant_features = target_corr[abs(target_corr) > 0.05].index.tolist()\nrelevant_features.append(\"meter_reading_log\")\nprint(f\"\\nSelected features based on threshold filtering: {relevant_features}\")\n\ndata_final = data_scaled[relevant_features]\n# ===============================================\n# 3.5 — SAVE CLEANED DATASET\n# ===============================================\ndata_final.to_csv(\"/kaggle/working/cleaned_dataset.csv\", index=False)\nprint(\"\\nSaved: cleaned_dataset.csv\")\nprint(\"Final shape:\", data_final.shape)\nprint(\"Final columns:\", data_final.columns.tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:22:31.261320Z","iopub.execute_input":"2026-06-16T15:22:31.261673Z","iopub.status.idle":"2026-06-16T15:23:03.191144Z","shell.execute_reply.started":"2026-06-16T15:22:31.261644Z","shell.execute_reply":"2026-06-16T15:23:03.190298Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Reload both files\ndata = pd.read_csv(\"/kaggle/working/cleaned_dataset.csv\")\nworking = pd.read_csv(\"/kaggle/working/working_dataset.csv\", \n                      parse_dates=[\"timestamp\"])\n\nprint(\"cleaned_dataset rows :\", len(data))\nprint(\"working_dataset rows :\", len(working))\n\n# Filter working to match the same rows as cleaned_dataset\n# using meter_reading and building_id as keys\nworking_filtered = working[\n    working[\"meter_reading\"].isin(data[\"meter_reading\"]) &\n    working[\"building_id\"].isin(data[\"building_id\"])\n].reset_index(drop=True)\n\n# Re-add time features directly from timestamp\ndata = data.reset_index(drop=True)\ndata[\"hour\"]        = working[\"timestamp\"].dt.hour[:len(data)].values\ndata[\"day_of_week\"] = working[\"timestamp\"].dt.dayofweek[:len(data)].values\ndata[\"month\"]       = working[\"timestamp\"].dt.month[:len(data)].values\ndata[\"is_weekend\"]  = (working[\"timestamp\"].dt.dayofweek[:len(data)] >= 5).astype(int).values\n\nprint(\"\\nColumns now:\")\nprint(data.columns.tolist())\nprint(f\"Shape: {data.shape}\")\n\n# Save\ndata.to_csv(\"/kaggle/working/cleaned_dataset.csv\", index=False)\nprint(\"Saved updated cleaned_dataset.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:34:27.159256Z","iopub.execute_input":"2026-06-16T15:34:27.159580Z","iopub.status.idle":"2026-06-16T15:35:04.996849Z","shell.execute_reply.started":"2026-06-16T15:34:27.159554Z","shell.execute_reply":"2026-06-16T15:35:04.995983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom scipy import stats\n\n# -----------------------------------------------\n# Load the cleaned dataset from Step 3\n# -----------------------------------------------\ndata = pd.read_csv(\"/kaggle/working/cleaned_dataset.csv\")\nprint(\"Loaded dataset shape:\", data.shape)\nprint(data.columns.tolist())\n\n# ===============================================\n# 4.1 — STATISTIQUES DESCRIPTIVES UNIVARIÉES\n# (Slide 47 — moyenne, médiane, écart-type, skewness, kurtosis)\n# ===============================================\n\nprint(\"\\n--- Descriptive Statistics ---\")\nprint(data.describe())\n\n# Skewness and Kurtosis on the target variable\nprint(f\"\\nSkewness of meter_reading_log: {data['meter_reading_log'].skew():.4f}\")\nprint(f\"Kurtosis of meter_reading_log: {data['meter_reading_log'].kurt():.4f}\")\n\n# ===============================================\n# 4.2 — VISUALISATION UNIVARIÉE\n# Distribution of the target variable\n# (Slide 50-51)\n# ===============================================\n\nfig, axes = plt.subplots(1, 3, figsize=(16, 4))\n\n# Histogram\naxes[0].hist(data[\"meter_reading_log\"], bins=50, color=\"steelblue\", edgecolor=\"white\")\naxes[0].set_title(\"Distribution of meter_reading_log\")\naxes[0].set_xlabel(\"log(1 + kWh)\")\naxes[0].set_ylabel(\"Frequency\")\n\n# Boxplot — detects remaining outliers\naxes[1].boxplot(data[\"meter_reading_log\"], vert=True, patch_artist=True,\n                boxprops=dict(facecolor=\"steelblue\", color=\"navy\"))\naxes[1].set_title(\"Boxplot — meter_reading_log\")\naxes[1].set_ylabel(\"log(1 + kWh)\")\n\n# Distribution by primary_use (if still in data, else by site_id)\nif \"primary_use_encoded\" in data.columns:\n    data.groupby(\"primary_use_encoded\")[\"meter_reading_log\"].mean().plot(\n        kind=\"bar\", ax=axes[2], color=\"darkorange\", edgecolor=\"white\"\n    )\n    axes[2].set_title(\"Mean consumption by building type\")\n    axes[2].set_xlabel(\"primary_use (encoded)\")\n    axes[2].set_ylabel(\"mean log(kWh)\")\n\nplt.tight_layout()\nplt.show()\n\n# ===============================================\n# 4.3 — VISUALISATION BIVARIÉE\n# Relation between features and target\n# (Slide 47 — deux variables quantitatives: corrélation)\n# ===============================================\n\nfig, axes = plt.subplots(2, 2, figsize=(14, 10))\n\n# air_temperature vs meter_reading_log\naxes[0, 0].scatter(data[\"air_temperature\"], data[\"meter_reading_log\"],\n                   alpha=0.1, color=\"steelblue\", s=1)\naxes[0, 0].set_title(\"air_temperature vs meter_reading_log\")\naxes[0, 0].set_xlabel(\"air_temperature (standardized)\")\naxes[0, 0].set_ylabel(\"log(1 + kWh)\")\n\n# square_feet vs meter_reading_log\naxes[0, 1].scatter(data[\"square_feet\"], data[\"meter_reading_log\"],\n                   alpha=0.1, color=\"darkorange\", s=1)\naxes[0, 1].set_title(\"square_feet vs meter_reading_log\")\naxes[0, 1].set_xlabel(\"square_feet (standardized)\")\naxes[0, 1].set_ylabel(\"log(1 + kWh)\")\n\n# hour vs meter_reading_log — average consumption per hour\n\nhourly = data.groupby(\"hour\")[\"meter_reading_log\"].mean()\naxes[1, 0].plot(hourly.index, hourly.values, color=\"green\", marker=\"o\", linewidth=2)\naxes[1, 0].set_title(\"Average consumption by hour of day\")\naxes[1, 0].set_xlabel(\"Hour (0-23)\")\naxes[1, 0].set_ylabel(\"mean log(kWh)\")\naxes[1, 0].grid(True)\n\n# month vs meter_reading_log — average consumption per month\nmonthly = data.groupby(\"month\")[\"meter_reading_log\"].mean()\naxes[1, 1].plot(monthly.index, monthly.values, color=\"purple\", marker=\"o\", linewidth=2)\naxes[1, 1].set_title(\"Average consumption by month\")\naxes[1, 1].set_xlabel(\"Month (1-12)\")\naxes[1, 1].set_ylabel(\"mean log(kWh)\")\naxes[1, 1].grid(True)\n\nplt.tight_layout()\nplt.show()\n\n# ===============================================\n# 4.4 — ANALYSE BIVARIÉE QUALITATIVE + QUANTITATIVE\n# (Slide 47 — groupby, moyennes par groupe)\n# ===============================================\n\nprint(\"\\n--- Mean consumption by weekend vs weekday ---\")\nprint(data.groupby(\"is_weekend\")[\"meter_reading_log\"].agg([\"mean\", \"median\", \"std\"]))\n\nprint(\"\\n--- Mean consumption by month ---\")\nprint(data.groupby(\"month\")[\"meter_reading_log\"].agg([\"mean\", \"median\", \"std\"]))\n\nprint(\"\\n--- Mean consumption by hour ---\")\nprint(data.groupby(\"hour\")[\"meter_reading_log\"].agg([\"mean\", \"median\"]))\n\n# ===============================================\n# 4.5 — MATRICE DE CORRÉLATION\n# (Slides 41-43 — Pearson correlation heatmap)\n# ===============================================\n\nplt.figure(figsize=(12, 8))\ncorr = data.corr(numeric_only=True)\nsns.heatmap(corr, annot=True, fmt=\".2f\", cmap=\"coolwarm\",\n            center=0, linewidths=0.5, annot_kws={\"size\": 8})\nplt.title(\"Correlation Matrix — All Features\")\nplt.tight_layout()\nplt.show()\n\n# Top correlations with the target\nprint(\"\\n--- Correlation with meter_reading_log (sorted) ---\")\ntarget_corr = corr[\"meter_reading_log\"].drop(\"meter_reading_log\").sort_values(\n    key=abs, ascending=False\n)\nprint(target_corr)\n\n# ===============================================\n# 4.6 — WEEKEND vs WEEKDAY VISUAL\n# ===============================================\n\nfig, ax = plt.subplots(figsize=(8, 5))\ndata.groupby([\"hour\", \"is_weekend\"])[\"meter_reading_log\"].mean().unstack().plot(\n    ax=ax, marker=\"o\", linewidth=2\n)\nax.set_title(\"Hourly consumption: Weekday vs Weekend\")\nax.set_xlabel(\"Hour of day\")\nax.set_ylabel(\"mean log(kWh)\")\nax.legend([\"Weekday\", \"Weekend\"])\nax.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:35:36.591121Z","iopub.execute_input":"2026-06-16T15:35:36.591403Z","iopub.status.idle":"2026-06-16T15:35:47.248219Z","shell.execute_reply.started":"2026-06-16T15:35:36.591377Z","shell.execute_reply":"2026-06-16T15:35:47.247393Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.ensemble import RandomForestRegressor\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import mean_squared_error, r2_score\n\n# =========================================================================\n# 1. CHARGEMENT ET PRÉPARATION DES DONNÉES EN MATRICE LÉGALE\n# =========================================================================\nprint(\"Loading the cleaned dataset...\")\ndf_model = pd.read_csv(\"/kaggle/working/cleaned_dataset.csv\")\nprint(f\"Dataset ready for modeling. Shape: {df_model.shape}\")\n\nX = df_model.drop(columns=[\"meter_reading_log\", \"meter_reading\"], errors=\"ignore\")\ny = df_model[\"meter_reading_log\"]\n\n# Split Temporel Chronologique Séquentiel (Slide 119 Setup)\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.25, shuffle=False)\n\n# CRITICAL FIX: Initialize dictionaries globally at the absolute start\nresults = {}\nval_predictions = {}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:40:40.446315Z","iopub.execute_input":"2026-06-16T15:40:40.446912Z","iopub.status.idle":"2026-06-16T15:40:44.892697Z","shell.execute_reply.started":"2026-06-16T15:40:40.446843Z","shell.execute_reply":"2026-06-16T15:40:44.891879Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================================\n# 2. ENTRAÎNEMENT DU MODÈLE A : TUNED RANDOM FOREST (RÉGULARISÉ)\n# =========================================================================\nprint(\"\\nTraining Tuned Random Forest...\")\nrf_tuned = RandomForestRegressor(\n    n_estimators=30, \n    max_depth=10, \n    min_samples_leaf=5, \n    max_features='sqrt', \n    random_state=42, \n    n_jobs=-1\n)\nrf_tuned.fit(X_train, y_train)\n\nrf_tr_preds = rf_tuned.predict(X_train)\nrf_va_preds = rf_tuned.predict(X_val)\n\n# Secure predictions in memory immediately\nval_predictions['Tuned Random Forest'] = rf_va_preds\n\nresults['Tuned Random Forest'] = {\n    'Train_RMSLE': np.sqrt(mean_squared_error(y_train, rf_tr_preds)),\n    'Val_RMSLE': np.sqrt(mean_squared_error(y_val, rf_va_preds)),\n    'Train_R2': r2_score(y_train, rf_tr_preds),\n    'Val_R2': r2_score(y_val, rf_va_preds)\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:41:15.180088Z","iopub.execute_input":"2026-06-16T15:41:15.180398Z","iopub.status.idle":"2026-06-16T15:41:35.904555Z","shell.execute_reply.started":"2026-06-16T15:41:15.180372Z","shell.execute_reply":"2026-06-16T15:41:35.903455Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================================\n# 3. ENTRAÎNEMENT DU MODÈLE B : LIGHTGBM REGRESSOR (BOOSTING OPTIMAL)\n# =========================================================================\nprint(\"Training LightGBM Regressor...\")\ntry:\n    import lightgbm as lgb\n    lgb_model = lgb.LGBMRegressor(\n        n_estimators=100,\n        learning_rate=0.05,\n        max_depth=8,\n        random_state=42,\n        n_jobs=-1,\n        verbose=-1\n    )\n    lgb_model.fit(X_train, y_train)\n\n    lgb_tr_preds = lgb_model.predict(X_train)\n    lgb_va_preds = lgb_model.predict(X_val)\n\n    val_predictions['LightGBM Regressor'] = lgb_va_preds\n\n    results['LightGBM Regressor'] = {\n        'Train_RMSLE': np.sqrt(mean_squared_error(y_train, lgb_tr_preds)),\n        'Val_RMSLE': np.sqrt(mean_squared_error(y_val, lgb_va_preds)),\n        'Train_R2': r2_score(y_train, lgb_tr_preds),\n        'Val_R2': r2_score(y_val, lgb_va_preds)\n    }\nexcept Exception as e:\n    print(f\"⚠️ LightGBM framework warning: {e}. Running optimized boosting fallback...\")\n    # Safe fallback using Sklearn's native HistGradientBoostingRegressor if LightGBM is locked\n    from sklearn.ensemble import HistGradientBoostingRegressor\n    fallback_model = HistGradientBoostingRegressor(max_iter=100, max_depth=8, random_state=42)\n    fallback_model.fit(X_train, y_train)\n    \n    lgb_tr_preds = fallback_model.predict(X_train)\n    lgb_va_preds = fallback_model.predict(X_val)\n    \n    val_predictions['LightGBM Regressor'] = lgb_va_preds\n    results['LightGBM Regressor'] = {\n        'Train_RMSLE': np.sqrt(mean_squared_error(y_train, lgb_tr_preds)),\n        'Val_RMSLE': np.sqrt(mean_squared_error(y_val, lgb_va_preds)),\n        'Train_R2': r2_score(y_train, lgb_tr_preds),\n        'Val_R2': r2_score(y_val, lgb_va_preds)\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:49.349174Z","iopub.status.idle":"2026-06-16T15:21:49.349515Z","shell.execute_reply.started":"2026-06-16T15:21:49.349370Z","shell.execute_reply":"2026-06-16T15:21:49.349392Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================================\n# 4. RESTITUTION QUANTITATIVE : TABLEAU COMPARATIF (FORMAT DÉCIMAL)\n# =========================================================================\nprint(\"\\n=========================================================================\")\nprint(\"                    COMPREHENSIVE MODEL EVALUATION                       \")\nprint(\"=========================================================================\")\nprint(f\"{'Model Name':<22} | {'Train RMSLE':<11} | {'Val RMSLE':<10} | {'Train R²':<12} | {'Val R²'}\")\nprint(\"-------------------------------------------------------------------------\")\nprint(f\"{'Baseline Random Forest':<22} | {0.3503:<11.4f} | {0.6321:<10.4f} | {'N/A':<12} | {'N/A'}\")\n\nfor model_name, m in results.items():\n    print(f\"{model_name:<22} | {m['Train_RMSLE']:<11.4f} | {m['Val_RMSLE']:<10.4f} | {m['Train_R2']:<12.4f} | {m['Val_R2']:.4f}\")\nprint(\"=========================================================================\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:49.350578Z","iopub.status.idle":"2026-06-16T15:21:49.350992Z","shell.execute_reply.started":"2026-06-16T15:21:49.350747Z","shell.execute_reply":"2026-06-16T15:21:49.350770Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================================\n# 5. RESTITUTION VISUELLE : COUPLAGE DES ANALYSES DE RÉSIDUS (GRAPHES)\n# =========================================================================\nprint(\"\\nGenerating comparative diagnostic graphs...\")\nfig, axes = plt.subplots(2, 2, figsize=(16, 12))\n\nnp.random.seed(42)\nsample_size = min(10000, len(y_val))\nsample_idx = np.random.choice(len(y_val), size=sample_size, replace=False)\ny_val_sampled = y_val.iloc[sample_idx]\n\n# Dynamically adapt to whatever models successfully populated predictions\nmodels_to_plot = [name for name in ['Tuned Random Forest', 'LightGBM Regressor'] if name in val_predictions]\ncolors = ['dodgerblue', 'teal']\n\nfor i, name in enumerate(models_to_plot):\n    preds_sampled = val_predictions[name][sample_idx]\n    errors = y_val_sampled - preds_sampled\n    \n    # ---- Row i, Column 0 : Regression Alignment Plot ----\n    ax_scatter = axes[i, 0]\n    sns.scatterplot(x=y_val_sampled, y=preds_sampled, alpha=0.3, color=colors[i], edgecolor=None, ax=ax_scatter)\n    \n    min_bound = min(y_val_sampled.min(), preds_sampled.min())\n    max_bound = max(y_val_sampled.max(), preds_sampled.max())\n    ax_scatter.plot([min_bound, max_bound], [min_bound, max_bound], color='red', linestyle='--', linewidth=2, label=r\"Ajustement Idéal ($Y = \\hat{Y}$)\")\n\n    ax_scatter.set_title(f\"{name} : Valeurs Réelles vs Prédites (Validation)\", fontsize=11, fontweight='bold')\n    ax_scatter.set_xlabel(\"Consommation Réelle (Log-kWh)\", fontsize=9)\n    ax_scatter.set_ylabel(\"Consommation Prédite (Log-kWh)\", fontsize=9)\n    ax_scatter.grid(True, linestyle='--', alpha=0.5)\n    ax_scatter.legend()\n    \n    # ---- Row i, Column 1 : Error Distribution Histogram ----\n    ax_hist = axes[i, 1]\n    sns.histplot(errors, bins=60, kde=True, color=colors[i], edgecolor='black', alpha=0.6, ax=ax_hist)\n    ax_hist.axvline(x=0, color='red', linestyle='--', linewidth=2, label=r\"Biais Nul ($\\epsilon = 0$)\")\n    \n    val_r2_val = results[name]['Val_R2']\n    ax_hist.set_title(f\"{name} : Distribution des Résidus ($R^2 = {val_r2_val:.4f}$)\", fontsize=11, fontweight='bold')\n    ax_hist.set_xlabel(r\"Erreur de Prédiction ($Y - \\hat{Y}$)\", fontsize=9)\n    ax_hist.set_ylabel(\"Nombre d'Observations\", fontsize=9)\n    ax_hist.grid(True, linestyle='--', alpha=0.5)\n    ax_hist.legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:49.352664Z","iopub.status.idle":"2026-06-16T15:21:49.353103Z","shell.execute_reply.started":"2026-06-16T15:21:49.352885Z","shell.execute_reply":"2026-06-16T15:21:49.352910Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport lightgbm as lgb\nfrom sklearn.preprocessing import StandardScaler\n\nprint(\"=========================================================================\")\nprint(\"     PHASE 6 : PRÉ-TRAITEMENT DU JEU DE TEST ET PRÉDICTIONS FINALES     \")\nprint(\"=========================================================================\")\n\n# =========================================================\n# 6.0 — RECHERCHE AUTOMATIQUE DES CHEMINS D'ACCÈS\n# =========================================================\nprint(\"Recherche des fichiers de données dans /kaggle/input...\")\npaths = {}\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        if filename in ['test.csv', 'building_metadata.csv', 'weather_test.csv']:\n            paths[filename] = os.path.join(dirname, filename)\n\n# Vérification de sécurité pour s'assurer que tous les fichiers requis sont présents\nrequired_files = ['test.csv', 'building_metadata.csv', 'weather_test.csv']\nif not all(f in paths for f in required_files):\n    raise FileNotFoundError(\n        f\"Erreur : Impossible de localiser tous les fichiers requis. \"\n        f\"Trouvés uniquement : {list(paths.keys())}. \"\n        f\"Veuillez vérifier que le dataset ASHRAE est correctement associé à votre notebook.\"\n    )\n\nprint(f\"-> Fichier de Test localisé : {paths['test.csv']}\")\nprint(f\"-> Fichier de Métadonnées localisé : {paths['building_metadata.csv']}\")\nprint(f\"-> Fichier Météo localisé : {paths['weather_test.csv']}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:49.355039Z","iopub.status.idle":"2026-06-16T15:21:49.355390Z","shell.execute_reply.started":"2026-06-16T15:21:49.355239Z","shell.execute_reply":"2026-06-16T15:21:49.355262Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 6.1 — CHARGEMENT DES DONNÉES DE TEST ET MÉTADONNÉES\n# =========================================================\nprint(\"\\nChargement des fichiers d'évaluation...\")\ntest_df = pd.read_csv(paths['test.csv'], parse_dates=[\"timestamp\"])\nbuilding_df = pd.read_csv(paths['building_metadata.csv'])\nweather_test_df = pd.read_csv(paths['weather_test.csv'], parse_dates=[\"timestamp\"])\n\nprint(f\"Dimensions initiales -> Test: {test_df.shape} | Weather: {weather_test_df.shape}\")\n\n# Fusion (Merge) des structures de données identique à la phase d'apprentissage\ntest_merged = test_df.merge(building_df, on=\"building_id\", how=\"left\")\ntest_merged = test_merged.merge(weather_test_df, on=[\"site_id\", \"timestamp\"], how=\"left\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:49.356924Z","iopub.status.idle":"2026-06-16T15:21:49.357640Z","shell.execute_reply.started":"2026-06-16T15:21:49.357485Z","shell.execute_reply":"2026-06-16T15:21:49.357503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 6.2 — ALIGNEMENT DU PRÉ-TRAITEMENT ET NETTOYAGE\n# =========================================================\nprint(\"Alignement des étapes de nettoyage...\")\n# Suppression de la colonne floor_count (trop de valeurs manquantes en train)\ntest_merged = test_merged.drop(columns=[\"floor_count\"], errors=\"ignore\")\n\n# Imputation de l'année de construction via la médiane du train\ntest_merged[\"year_built\"] = test_merged[\"year_built\"].fillna(test_merged[\"year_built\"].median())\n\n# Imputation par propagation (Forward/Backward Fill) pour les variables météorologiques\nfor col in [\"air_temperature\", \"dew_temperature\", \"wind_speed\", \"sea_level_pressure\", \"wind_direction\"]:\n    test_merged[col] = test_merged[col].ffill().bfill()\ntest_merged[\"cloud_coverage\"] = test_merged[\"cloud_coverage\"].fillna(test_merged[\"cloud_coverage\"].median())\ntest_merged[\"precip_depth_1_hr\"] = test_merged[\"precip_depth_1_hr\"].fillna(0)\n\n# Extraction des variables temporelles (Engineering des composants cycliques)\ntest_merged[\"hour\"] = test_merged[\"timestamp\"].dt.hour\ntest_merged[\"day_of_week\"] = test_merged[\"timestamp\"].dt.dayofweek\ntest_merged[\"month\"] = test_merged[\"timestamp\"].dt.month\ntest_merged[\"is_weekend\"] = (test_merged[\"day_of_week\"] >= 5).astype(int)\n\n# Encodage de la variable catégorielle 'primary_use'\ntest_merged[\"primary_use_encoded\"] = test_merged[\"primary_use\"].astype(\"category\").cat.codes\n\n# Isolation des identifiants uniques de ligne requis pour la soumission\nrow_ids = test_merged[\"row_id\"]\n\n# Extraction de la matrice finale des descripteurs numériques\ntest_features = test_merged.drop(columns=[\"timestamp\", \"meter\", \"primary_use\", \"row_id\"], errors=\"ignore\")\n# Alignement strict des colonnes avec les features d'entraînement vues par LightGBM\n# (X_train provient de la cellule d'évaluation précédente)\nselected_features_list = X_train.columns.tolist()\ntest_features_final = test_features[selected_features_list]\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:49.358536Z","iopub.status.idle":"2026-06-16T15:21:49.358930Z","shell.execute_reply.started":"2026-06-16T15:21:49.358734Z","shell.execute_reply":"2026-06-16T15:21:49.358757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 6.3 — STANDARDISATION DES DESCRIPTEURS DU TEST\n# =========================================================\ncols_to_scale = [\"square_feet\", \"year_built\", \"air_temperature\", \"dew_temperature\", \n                 \"wind_speed\", \"sea_level_pressure\", \"cloud_coverage\", \"precip_depth_1_hr\", \n                 \"wind_direction\"]\ncols_to_scale = [c for c in cols_to_scale if c in test_features_final.columns]\n\nscaler = StandardScaler()\ntest_features_final = test_features_final.copy()\nif len(cols_to_scale) > 0:\n    test_features_final[cols_to_scale] = scaler.fit_transform(test_features_final[cols_to_scale])\n\nprint(f\"Dimensions de la matrice finale transmise au modèle : {test_features_final.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:49.360101Z","iopub.status.idle":"2026-06-16T15:21:49.361100Z","shell.execute_reply.started":"2026-06-16T15:21:49.360878Z","shell.execute_reply":"2026-06-16T15:21:49.360906Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 6.4 — CALCUL DES PRÉDICTIONS ET INVERSION DE L'ÉCHELLE LOG\n# =========================================================\nprint(\"\\nCalcul des prédictions via l'algorithme LightGBM Regressor...\")\ntest_log_preds = lgb_model.predict(test_features_final)\n\n# Inversion de la transformation logarithmique pour revenir aux unités physiques (kWh)\n# expm1 effectue l'opération mathématique inverse exacte : exp(x) - 1\nfinal_meter_readings = np.expm1(test_log_preds)\n\n# Sécurité technique : Forcer à zéro toute valeur résiduelle accidentellement négative\nfinal_meter_readings = np.clip(final_meter_readings, a_min=0, a_max=None)\n\n# =========================================================\n# 6.5 — COMPILATION ET EXPORTATION DU FICHIER SUBMISSION\n# =========================================================\nsubmission = pd.DataFrame({\n    \"row_id\": row_ids,\n    \"meter_reading\": final_meter_readings\n})\n\n# Exportation sous format CSV standard pour l'évaluation de fin de projet\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(\"\\n\" + \"=\"*80)\nprint(\"  SUCCÈS ! Le fichier d'évaluation finale 'submission.csv' a été généré.  \")\nprint(\"=\"*80)\nprint(submission.head(10).round(4))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T15:21:49.362596Z","iopub.status.idle":"2026-06-16T15:21:49.363390Z","shell.execute_reply.started":"2026-06-16T15:21:49.363161Z","shell.execute_reply":"2026-06-16T15:21:49.363187Z"}},"outputs":[],"execution_count":null}]}