{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":53666,"databundleVersionId":6589269,"sourceType":"competition"},{"sourceId":6507308,"sourceType":"datasetVersion","datasetId":3706667},{"sourceId":151583600,"sourceType":"kernelVersion"}],"dockerImageVersionId":30558,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### Version 39 is fully based on this publick notebook: https://www.kaggle.com/code/lccburk/feature-engineering-and-random-forest-prediction/notebook\n### In subsequent versions of this notebook I will try to combine the previous and this solutions.\n### Last modified: September 24","metadata":{}},{"cell_type":"markdown","source":"# <p style=\"font-family:Georgia; font-weight:normal; letter-spacing: 2px; color:#1192AA; font-size:140%; text-align:left;padding: 0px; border-bottom: 3px solid #1192AA\">Libraries</p>","metadata":{}},{"cell_type":"code","source":"# Misc\nimport numpy as np\nimport pandas as pd\nimport plotly.express as px\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport random\nimport os\nimport gc\nfrom copy import deepcopy\nfrom functools import partial\nfrom itertools import combinations\nfrom itertools import groupby\nfrom tqdm import tqdm\nimport polars as pl\nimport datetime\n\n# Import sklearn classes for model selection, cross validation, and performance evaluation\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.model_selection import StratifiedKFold, KFold\nfrom sklearn.metrics import roc_auc_score, accuracy_score, log_loss\nfrom sklearn.model_selection import cross_validate\nfrom sklearn.metrics import RocCurveDisplay\nfrom sklearn.metrics import confusion_matrix\nfrom sklearn.metrics import ConfusionMatrixDisplay\nfrom sklearn.metrics import precision_score\nfrom sklearn.metrics import average_precision_score\n\n# Import libraries for Hypertuning\nimport optuna\n\n#Import libraries for gradient boosting\nimport xgboost as xgb\nimport lightgbm as lgb\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.ensemble import GradientBoostingClassifier\nfrom sklearn.pipeline import Pipeline\nfrom catboost import Pool\n\n\nfrom metric import score # Import event detection ap score function\n\n# These are variables to be used by the score function\ncolumn_names = {\n    'series_id_column_name': 'series_id',\n    'time_column_name': 'step',\n    'event_column_name': 'event',\n    'score_column_name': 'score',\n}\n\ntolerances = {\n    'onset': [12, 36, 60, 90, 120, 150, 180, 240, 300, 360], \n    'wakeup': [12, 36, 60, 90, 120, 150, 180, 240, 300, 360]\n}\n\n\n# Useful line of code to set the display option so we could see all the columns in pd dataframe\npd.set_option('display.max_columns', None)\n\n# Suppress warnings\nimport warnings\nwarnings.filterwarnings(\"ignore\", category=UserWarning)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2023-09-22T23:23:21.360521Z","iopub.execute_input":"2023-09-22T23:23:21.360948Z","iopub.status.idle":"2023-09-22T23:23:27.863442Z","shell.execute_reply.started":"2023-09-22T23:23:21.360916Z","shell.execute_reply":"2023-09-22T23:23:27.862339Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <p style=\"font-family:Georgia; font-weight:normal; letter-spacing: 2px; color:#1192AA; font-size:140%; text-align:left;padding: 0px; border-bottom: 3px solid #1192AA\">Dataset</p>","metadata":{}},{"cell_type":"code","source":"# Importing data \n\n# Column transformations\n\ndt_transforms = [\n    pl.col('timestamp').str.to_datetime(), \n    (pl.col('timestamp').str.to_datetime().dt.year()-2000).cast(pl.UInt8).alias('year'), \n    pl.col('timestamp').str.to_datetime().dt.month().cast(pl.UInt8).alias('month'),\n    pl.col('timestamp').str.to_datetime().dt.day().cast(pl.UInt8).alias('day'), \n    pl.col('timestamp').str.to_datetime().dt.hour().cast(pl.UInt8).alias('hour')\n]\n\ndata_transforms = [\n    pl.col('anglez').cast(pl.Int16), # Casting anglez to 16 bit integer\n    (pl.col('enmo')*1000).cast(pl.UInt16), # Convert enmo to 16 bit uint\n]\n\ntrain_series = pl.scan_parquet('/kaggle/input/child-mind-institute-detect-sleep-states/train_series.parquet').with_columns(\n    dt_transforms + data_transforms\n    )\n\ntrain_events = pl.read_csv('/kaggle/input/child-mind-institute-detect-sleep-states/train_events.csv').with_columns(\n    dt_transforms\n    )\n\ntest_series = pl.scan_parquet('/kaggle/input/child-mind-institute-detect-sleep-states/test_series.parquet').with_columns(\n    dt_transforms + data_transforms\n    )\n\n# Getting series ids as a list for convenience\nseries_ids = train_events['series_id'].unique(maintain_order=True).to_list()\n\n# Removing series with mismatched counts: \nonset_counts = train_events.filter(pl.col('event')=='onset').group_by('series_id').count().sort('series_id')['count']\nwakeup_counts = train_events.filter(pl.col('event')=='wakeup').group_by('series_id').count().sort('series_id')['count']\n\ncounts = pl.DataFrame({'series_id':sorted(series_ids), 'onset_counts':onset_counts, 'wakeup_counts':wakeup_counts})\ncount_mismatches = counts.filter(counts['onset_counts'] != counts['wakeup_counts'])\n\ntrain_series = train_series.filter(~pl.col('series_id').is_in(count_mismatches['series_id']))\ntrain_events = train_events.filter(~pl.col('series_id').is_in(count_mismatches['series_id']))\n\n# Updating list of series ids, not including series with no non-null values.\nseries_ids = train_events.drop_nulls()['series_id'].unique(maintain_order=True).to_list()","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:23:27.865648Z","iopub.execute_input":"2023-09-22T23:23:27.866351Z","iopub.status.idle":"2023-09-22T23:23:28.145961Z","shell.execute_reply.started":"2023-09-22T23:23:27.866312Z","shell.execute_reply":"2023-09-22T23:23:28.144817Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"features, feature_cols = [pl.col('hour')], ['hour']\n\nfor mins in [5, 30, 60*2, 60*8] :\n    features += [\n        pl.col('enmo').rolling_mean(12 * mins, center=True, min_periods=1).abs().cast(pl.UInt16).alias(f'enmo_{mins}m_mean'),\n        pl.col('enmo').rolling_max(12 * mins, center=True, min_periods=1).abs().cast(pl.UInt16).alias(f'enmo_{mins}m_max')\n    ]\n\n    feature_cols += [ \n        f'enmo_{mins}m_mean', f'enmo_{mins}m_max'\n    ]\n\n    # Getting first variations\n    for var in ['enmo', 'anglez'] :\n        features += [\n            (pl.col(var).diff().abs().rolling_mean(12 * mins, center=True, min_periods=1)*10).abs().cast(pl.UInt32).alias(f'{var}_1v_{mins}m_mean'),\n            (pl.col(var).diff().abs().rolling_max(12 * mins, center=True, min_periods=1)*10).abs().cast(pl.UInt32).alias(f'{var}_1v_{mins}m_max')\n        ]\n\n        feature_cols += [ \n            f'{var}_1v_{mins}m_mean', f'{var}_1v_{mins}m_max'\n        ]\n\nid_cols = ['series_id', 'step', 'timestamp']\n\ntrain_series = train_series.with_columns(\n    features\n).select(id_cols + feature_cols)\n\ntest_series = test_series.with_columns(\n    features\n).select(id_cols + feature_cols)","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:23:28.147784Z","iopub.execute_input":"2023-09-22T23:23:28.148573Z","iopub.status.idle":"2023-09-22T23:23:28.165556Z","shell.execute_reply.started":"2023-09-22T23:23:28.148527Z","shell.execute_reply":"2023-09-22T23:23:28.163996Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def make_train_dataset(train_data, train_events, drop_nulls=False) :\n    \n    series_ids = train_data['series_id'].unique(maintain_order=True).to_list()\n    X, y = pl.DataFrame(), pl.DataFrame()\n    for idx in tqdm(series_ids) : \n        \n        # Normalizing sample features\n        sample = train_data.filter(pl.col('series_id')==idx).with_columns(\n            [(pl.col(col) / pl.col(col).std()).cast(pl.Float32) for col in feature_cols if col != 'hour']\n        )\n        \n        events = train_events.filter(pl.col('series_id')==idx)\n        \n        if drop_nulls : \n            # Removing datapoints on dates where no data was recorded\n            sample = sample.filter(\n                pl.col('timestamp').dt.date().is_in(events['timestamp'].dt.date())\n            )\n        \n        X = X.vstack(sample[id_cols + feature_cols])\n\n        onsets = events.filter((pl.col('event') == 'onset') & (pl.col('step') != None))['step'].to_list()\n        wakeups = events.filter((pl.col('event') == 'wakeup') & (pl.col('step') != None))['step'].to_list()\n\n        # NOTE: This will break if there are event series without any recorded onsets or wakeups\n        y = y.vstack(sample.with_columns(\n            sum([(onset <= pl.col('step')) & (pl.col('step') <= wakeup) for onset, wakeup in zip(onsets, wakeups)]).cast(pl.Boolean).alias('asleep')\n            ).select('asleep')\n            )\n    \n    y = y.to_numpy().ravel()\n    \n    return X, y","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:23:28.167915Z","iopub.execute_input":"2023-09-22T23:23:28.168369Z","iopub.status.idle":"2023-09-22T23:23:28.183241Z","shell.execute_reply.started":"2023-09-22T23:23:28.168334Z","shell.execute_reply":"2023-09-22T23:23:28.181799Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_events(series, classifier) :\n    '''\n    Takes a time series and a classifier and returns a formatted submission dataframe.\n    '''\n    \n    series_ids = series['series_id'].unique(maintain_order=True).to_list()\n    events = pl.DataFrame(schema={'series_id':str, 'step':int, 'event':str, 'score':float})\n\n    for idx in tqdm(series_ids) : \n\n        # Collecting sample and normalizing features\n        scale_cols = [col for col in feature_cols if (col != 'hour') & (series[col].std() !=0)]\n        X = series.filter(pl.col('series_id') == idx).select(id_cols + feature_cols).with_columns(\n            [(pl.col(col) / series[col].std()).cast(pl.Float32) for col in scale_cols]\n        )\n\n        # Applying classifier to get predictions and scores\n        preds, probs = classifier.predict(X[feature_cols]), classifier.predict_proba(X[feature_cols])[:, 1]\n\n        #NOTE: Considered using rolling max to get sleep periods excluding <30 min interruptions, but ended up decreasing performance\n        X = X.with_columns(\n            pl.lit(preds).cast(pl.Int8).alias('prediction'), \n            pl.lit(probs).alias('probability')\n                        )\n        \n        # Getting predicted onset and wakeup time steps\n        pred_onsets = X.filter(X['prediction'].diff() > 0)['step'].to_list()\n        pred_wakeups = X.filter(X['prediction'].diff() < 0)['step'].to_list()\n        \n        if len(pred_onsets) > 0 : \n            \n            # Ensuring all predicted sleep periods begin and end\n            if min(pred_wakeups) < min(pred_onsets) : \n                pred_wakeups = pred_wakeups[1:]\n\n            if max(pred_onsets) > max(pred_wakeups) :\n                pred_onsets = pred_onsets[:-1]\n\n            # Keeping sleep periods longer than 30 minutes\n            sleep_periods = [(onset, wakeup) for onset, wakeup in zip(pred_onsets, pred_wakeups) if wakeup - onset >= 12 * 30]\n\n            for onset, wakeup in sleep_periods :\n                # Scoring using mean probability over period\n                score = X.filter((pl.col('step') >= onset) & (pl.col('step') <= wakeup))['probability'].mean()\n\n                # Adding sleep event to dataframe\n                events = events.vstack(pl.DataFrame().with_columns(\n                    pl.Series([idx, idx]).alias('series_id'), \n                    pl.Series([onset, wakeup]).alias('step'),\n                    pl.Series(['onset', 'wakeup']).alias('event'),\n                    pl.Series([score, score]).alias('score')\n                ))\n\n    # Adding row id column\n    events = events.to_pandas().reset_index().rename(columns={'index':'row_id'})\n\n    return events","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:32:22.308941Z","iopub.execute_input":"2023-09-22T23:32:22.309460Z","iopub.status.idle":"2023-09-22T23:32:22.327110Z","shell.execute_reply.started":"2023-09-22T23:32:22.309425Z","shell.execute_reply":"2023-09-22T23:32:22.326080Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# We will collect datapoints and take 1 million samples\ntrain_data = train_series.filter(pl.col('series_id').is_in(series_ids)).collect().sample(int(1e6))","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:23:28.184927Z","iopub.execute_input":"2023-09-22T23:23:28.185394Z","iopub.status.idle":"2023-09-22T23:26:17.069116Z","shell.execute_reply.started":"2023-09-22T23:23:28.185358Z","shell.execute_reply":"2023-09-22T23:26:17.066461Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <p style=\"font-family:Georgia; font-weight:normal; letter-spacing: 2px; color:#1192AA; font-size:140%; text-align:left;padding: 0px; border-bottom: 3px solid #1192AA\">Models</p>","metadata":{}},{"cell_type":"code","source":"# Creating train dataset\nX_train, y_train = make_train_dataset(train_data, train_events)","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:26:17.073035Z","iopub.execute_input":"2023-09-22T23:26:17.074352Z","iopub.status.idle":"2023-09-22T23:26:22.183016Z","shell.execute_reply.started":"2023-09-22T23:26:17.074294Z","shell.execute_reply":"2023-09-22T23:26:22.181681Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Recovering memory\ndel train_data \ngc.collect()","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:26:22.184572Z","iopub.execute_input":"2023-09-22T23:26:22.184940Z","iopub.status.idle":"2023-09-22T23:26:22.402767Z","shell.execute_reply.started":"2023-09-22T23:26:22.184910Z","shell.execute_reply":"2023-09-22T23:26:22.401591Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"lgb_opt =  {\n    'num_leaves': 205,\n    'learning_rate': 0.0765,\n    'random_state': 42\n}\n\nmy_classifier = lgb.LGBMClassifier(**lgb_opt)\nmy_classifier.fit(X_train[feature_cols], y_train)\n","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:31:15.166464Z","iopub.execute_input":"2023-09-22T23:31:15.166890Z","iopub.status.idle":"2023-09-22T23:31:21.885285Z","shell.execute_reply.started":"2023-09-22T23:31:15.166857Z","shell.execute_reply":"2023-09-22T23:31:21.884078Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <p style=\"font-family:Georgia; font-weight:normal; letter-spacing: 2px; color:#1192AA; font-size:140%; text-align:left;padding: 0px; border-bottom: 3px solid #1192AA\">Make Submission</p>","metadata":{}},{"cell_type":"code","source":"# Getting event predictions for test set and saving submission\nsubmission = get_events(test_series.collect(), my_classifier)\nsubmission.to_csv('submission.csv', index=False)","metadata":{"execution":{"iopub.status.busy":"2023-09-22T23:32:25.401466Z","iopub.execute_input":"2023-09-22T23:32:25.401873Z","iopub.status.idle":"2023-09-22T23:32:25.510511Z","shell.execute_reply.started":"2023-09-22T23:32:25.401844Z","shell.execute_reply":"2023-09-22T23:32:25.509017Z"},"trusted":true},"outputs":[],"execution_count":null}]}