{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":66052,"databundleVersionId":7284431,"sourceType":"competition"}],"dockerImageVersionId":30646,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"This is my first time working on anomaly-detection. I believe my solution is quite simple and basic.\n* Treat the dataset as i.i.d. tabular data, instead of time-series data. I drop the time feature.\n* No transformation is performed to the features.\n* My model is a `sklearn.ensemble.IsolationForest`.\n\nTuning the isolation-forest's `contamination` parameter makes the training process run slower (seems like a bug within scikit-learn). Thus I don't specify that parameter, instead I postprocess the raw anomaly-score from the model to separate anomaly and normal sample based on a certain threshold value. To find out a good threshold value, I simply probe the public leaderboard score.","metadata":{}},{"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)\nfrom sklearn import ensemble","metadata":{"execution":{"iopub.status.busy":"2024-02-10T10:34:41.595819Z","iopub.execute_input":"2024-02-10T10:34:41.596747Z","iopub.status.idle":"2024-02-10T10:34:41.602285Z","shell.execute_reply.started":"2024-02-10T10:34:41.596707Z","shell.execute_reply":"2024-02-10T10:34:41.601029Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%%time\ntrain = '/kaggle/input/ai-serving-grid-stability/train.csv'\nusecols = 'Demand,correction,correctedDemand,FRCE,controlBandPos,controlBandNeg,LFCInput,aFRRactivation,aFRRrequest,participationCMO,participationIN,correctionEcho,BandLimitedCorrectedDemand,controlArea'\nusecols = usecols.split(',')\ntrain = pd.read_csv(\n    train,\n    usecols=usecols,\n)\ntrain","metadata":{"execution":{"iopub.status.busy":"2024-02-10T10:35:47.512128Z","iopub.execute_input":"2024-02-10T10:35:47.512574Z","iopub.status.idle":"2024-02-10T10:36:13.534511Z","shell.execute_reply.started":"2024-02-10T10:35:47.512544Z","shell.execute_reply":"2024-02-10T10:36:13.533175Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test = '/kaggle/input/ai-serving-grid-stability/test.csv'\ntest = pd.read_csv(\n    test,\n    usecols=usecols,\n)\ntest","metadata":{"execution":{"iopub.status.busy":"2024-02-10T10:36:13.536792Z","iopub.execute_input":"2024-02-10T10:36:13.537368Z","iopub.status.idle":"2024-02-10T10:36:13.693877Z","shell.execute_reply.started":"2024-02-10T10:36:13.537321Z","shell.execute_reply":"2024-02-10T10:36:13.692653Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%%time\nmodel = ensemble.IsolationForest(\n    n_jobs=-1,\n    random_state=1,\n)\nmodel.fit(train)","metadata":{"execution":{"iopub.status.busy":"2024-02-10T10:36:47.386560Z","iopub.execute_input":"2024-02-10T10:36:47.387006Z","iopub.status.idle":"2024-02-10T10:36:51.145110Z","shell.execute_reply.started":"2024-02-10T10:36:47.386959Z","shell.execute_reply":"2024-02-10T10:36:51.143852Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's check the predicted contamination level on the test set.","metadata":{}},{"cell_type":"code","source":"pred = model.predict(test)\npred = np.where(pred > 0, 'normal', 'anomaly')\npred = pd.Series(pred)\npred.value_counts() / len(pred)","metadata":{"execution":{"iopub.status.busy":"2024-02-10T10:41:09.349758Z","iopub.execute_input":"2024-02-10T10:41:09.350195Z","iopub.status.idle":"2024-02-10T10:41:10.192013Z","shell.execute_reply.started":"2024-02-10T10:41:09.350162Z","shell.execute_reply":"2024-02-10T10:41:10.190581Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Around 25% of the sample is predicted as anomaly. This default prediction scores are 0.51861 (public) and 0.50582 (private).\n\nThe competition metric is F-beta score, thus it's better if we tune the contamination threshold manually instead of relying on the default one.","metadata":{}},{"cell_type":"code","source":"CONTAMINATION = .4 # I tried 0.2, 0.3, 0.4, 0.5, and 0.6. Turns out 0.4 give better public leaderboard score.\nscore = model.decision_function(test)\nthreshold = np.quantile(score, CONTAMINATION)\npred = score <= threshold # True is anomaly, False is normal\npred = pred.astype('int') # 1 is anomaly, 0 is normal\npred","metadata":{"execution":{"iopub.status.busy":"2024-02-10T11:02:25.559190Z","iopub.execute_input":"2024-02-10T11:02:25.559778Z","iopub.status.idle":"2024-02-10T11:02:26.381050Z","shell.execute_reply.started":"2024-02-10T11:02:25.559727Z","shell.execute_reply":"2024-02-10T11:02:26.379806Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"During the search for a good `CONTAMINATION` value, I only use a single decimal digit. It's possible that the leaderboard score can be better if I add more decimal digit during the search.\n\n`CONTAMINATION = .4` means 40% of the sample will be marked as anomaly.","metadata":{}},{"cell_type":"code","source":"submit = '/kaggle/input/ai-serving-grid-stability/sample_submission.csv'\nsubmit = pd.read_csv(\n    submit,\n    index_col=0,\n)\nsubmit[submit.columns[0]] = pred\nsubmit","metadata":{"execution":{"iopub.status.busy":"2024-02-10T11:04:14.179113Z","iopub.execute_input":"2024-02-10T11:04:14.179563Z","iopub.status.idle":"2024-02-10T11:04:14.219500Z","shell.execute_reply.started":"2024-02-10T11:04:14.179534Z","shell.execute_reply":"2024-02-10T11:04:14.218455Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"submit.to_csv('submission.csv')\n!head submission.csv","metadata":{"execution":{"iopub.status.busy":"2024-02-10T11:04:53.398028Z","iopub.execute_input":"2024-02-10T11:04:53.398955Z","iopub.status.idle":"2024-02-10T11:04:54.544926Z","shell.execute_reply.started":"2024-02-10T11:04:53.398902Z","shell.execute_reply":"2024-02-10T11:04:54.543646Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"This change on the contamination threshold improves the scores to 0.59586 (public) and 0.58058 (private).\n\n| Contamination | Score (public) | Score (private) |\n| - | - | - |\n| auto | 0.51861 | 0.50582 |\n| 0.4 | 0.59586 | 0.58058 |","metadata":{}}]}