{"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_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Evaluation Metric\n![eval_metric_undraw.png](attachment:36f07d81-002f-4128-8eea-6548477a115e.png)\n\nThe [evaluation metric](https://www.kaggle.com/competitions/amex-default-prediction/overview/evaluation), 𝑀, for the [American Express - Default Prediction](https://www.kaggle.com/competitions/amex-default-prediction) competition is the mean of two measures of rank ordering: Normalized Gini Coefficient, 𝐺, and default rate captured at 4%, 𝐷.\n\n```𝑀 = 0.5 * (𝐺 + 𝐷)```\n\nThe default rate captured at 4% is the percentage of the positive labels (defaults) captured within the highest-ranked 4% of the predictions, and represents a Sensitivity/Recall statistic.\n\nFor both of the sub-metrics 𝐺 and 𝐷, the negative labels are given a weight of 20 to adjust for downsampling.\n\nThis notebook provides snippets of this custom metric using different implementations:\n\n* [Pandas (Official)](#Pandas-Implementation-%28Official%29)\n* [Datatable](#Datatable-Implementation)\n* [JAX](#JAX-Implementation)\n* [Numpy](#Numpy-Implementation)\n* [PyTorch](#PyTorch-Implementation)\n* [TensorFlow](#TensorFlow-Implementation)","metadata":{},"attachments":{"36f07d81-002f-4128-8eea-6548477a115e.png":{"image/png":"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"}}},{"cell_type":"markdown","source":"## Pandas Implementation (Official)\nThe official Python code for implementing the metric using pandas dataframes can be found in [this notebook](https://www.kaggle.com/code/inversion/amex-competition-metric-python).","metadata":{}},{"cell_type":"code","source":"# https://www.kaggle.com/code/inversion/amex-competition-metric-python\nimport pandas as pd\n\ndef amex_metric_official(y_true: pd.DataFrame, y_pred: pd.DataFrame) -> float:\n\n    def top_four_percent_captured(y_true: pd.DataFrame, y_pred: pd.DataFrame) -> float:\n        df = (pd.concat([y_true, y_pred], axis='columns')\n              .sort_values('prediction', ascending=False))\n        df['weight'] = df['target'].apply(lambda x: 20 if x==0 else 1)\n        four_pct_cutoff = int(0.04 * df['weight'].sum())\n        df['weight_cumsum'] = df['weight'].cumsum()\n        df_cutoff = df.loc[df['weight_cumsum'] <= four_pct_cutoff]\n        return (df_cutoff['target'] == 1).sum() / (df['target'] == 1).sum()\n\n    def weighted_gini(y_true: pd.DataFrame, y_pred: pd.DataFrame) -> float:\n        df = (pd.concat([y_true, y_pred], axis='columns')\n              .sort_values('prediction', ascending=False))\n        df['weight'] = df['target'].apply(lambda x: 20 if x==0 else 1)\n        df['random'] = (df['weight'] / df['weight'].sum()).cumsum()\n        total_pos = (df['target'] * df['weight']).sum()\n        df['cum_pos_found'] = (df['target'] * df['weight']).cumsum()\n        df['lorentz'] = df['cum_pos_found'] / total_pos\n        df['gini'] = (df['lorentz'] - df['random']) * df['weight']\n        return df['gini'].sum()\n\n    def normalized_weighted_gini(y_true: pd.DataFrame, y_pred: pd.DataFrame) -> float:\n        y_true_pred = y_true.rename(columns={'target': 'prediction'})\n        return weighted_gini(y_true, y_pred) / weighted_gini(y_true, y_true_pred)\n\n    g = normalized_weighted_gini(y_true, y_pred)\n    d = top_four_percent_captured(y_true, y_pred)\n\n    return 0.5 * (g + d)\n","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:09.174995Z","iopub.execute_input":"2022-06-03T07:43:09.175474Z","iopub.status.idle":"2022-06-03T07:43:09.215023Z","shell.execute_reply.started":"2022-06-03T07:43:09.175385Z","shell.execute_reply":"2022-06-03T07:43:09.214043Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Datatable Implementation\nThe metric can be implemented using [datatable](https://datatable.readthedocs.io) and runs about 6x faster than pandas as shared in [this post](https://www.kaggle.com/competitions/amex-default-prediction/discussion/327984).","metadata":{}},{"cell_type":"code","source":"import datatable as dt\n\ndef amex_metric_datatable(y_true: dt.Frame, y_pred: dt.Frame) -> float:\n\n    # create datatable frame\n    df = dt.Frame(target=y_true, prediction=y_pred)\n\n    # sort by descending prediction values\n    df = df[:, :, dt.sort(-dt.f.prediction)]\n\n    # create row weights, its percentage & cumulative sum\n    df['weight'] = 20 - (dt.f.target * 19)\n    df['weight_perc'] = dt.f.weight / df['weight'].sum1()\n    df['weight_perc_cumsum'] = df['weight_perc'].to_numpy().cumsum() # use native datatable cumsum when v1.1.0 is released\n\n    # filter the top 4%\n    four_pct_filter = dt.f.weight_perc_cumsum <= 0.04\n\n    # default rate captured at 4%\n    d = df[four_pct_filter, 'target'].sum1() / df['target'].sum1()\n\n    # weighted gini coefficient\n    df['weighted_target'] = dt.f.target * dt.f.weight\n    df['weighted_target_perc'] = dt.f.weighted_target / df['weighted_target'].sum1()\n    df['lorentz'] = df['weighted_target_perc'].to_numpy().cumsum() # use native datatable cumsum when v1.1.0 is released\n    df['gini'] = (dt.f.lorentz - dt.f.weight_perc_cumsum) * dt.f.weight\n    gini = df['gini'].sum1()\n\n    # max weighted gini coefficient\n    total_pos = df['target'].sum1()\n    total_neg = df.nrows - total_pos\n    gini_max = 10 * total_neg * (total_pos + 20 * total_neg - 19) / (total_pos + 20 * total_neg)\n\n    # normalized weighted gini coefficient\n    g = gini / gini_max\n\n    # amex metric\n    m = 0.5 * (g + d)\n\n    return m\n","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:09.667257Z","iopub.execute_input":"2022-06-03T07:43:09.667994Z","iopub.status.idle":"2022-06-03T07:43:09.761024Z","shell.execute_reply.started":"2022-06-03T07:43:09.667946Z","shell.execute_reply":"2022-06-03T07:43:09.760321Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## JAX Implementation\nThe metric can be implemented using a JIT-compiled [JAX](https://jax.readthedocs.io) function which is typically useful for JAX-based pipelines or models.","metadata":{}},{"cell_type":"code","source":"import jax\nimport jax.numpy as jnp\nfrom jax.config import config\n\nconfig.update('jax_enable_x64', True)\n\n@jax.jit\ndef amex_metric_jax(y_true: jnp.array, y_pred: jnp.array) -> float:\n\n    # convert dtypes to float64\n    y_true = jnp.array(y_true, dtype=jnp.float64)\n    y_pred = jnp.array(y_pred, dtype=jnp.float64)\n\n    # count of positives and negatives\n    n_pos = y_true.sum()\n    n_neg = y_true.shape[0] - n_pos\n\n    # sorting by descring prediction values\n    indices = jnp.argsort(y_pred)[::-1]\n    preds, target = y_pred[indices], y_true[indices]\n\n    # filter the top 4% by cumulative row weights\n    weight = 20.0 - target * 19.0\n    cum_norm_weight = (weight / weight.sum()).cumsum()\n    four_pct_filter = cum_norm_weight <= 0.04\n\n    # default rate captured at 4%\n    d = jnp.where(four_pct_filter, target, 0).sum() / n_pos\n\n    # weighted gini coefficient\n    lorentz = (target / n_pos).cumsum()\n    gini = ((lorentz - cum_norm_weight) * weight).sum()\n\n    # max weighted gini coefficient\n    gini_max = 10 * n_neg * (1 - 19 / (n_pos + 20 * n_neg))\n\n    # normalized weighted gini coefficient\n    g = gini / gini_max\n\n    return 0.5 * (g + d)\n","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:10.013489Z","iopub.execute_input":"2022-06-03T07:43:10.014175Z","iopub.status.idle":"2022-06-03T07:43:11.751302Z","shell.execute_reply.started":"2022-06-03T07:43:10.014127Z","shell.execute_reply":"2022-06-03T07:43:11.750363Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Numpy Implementation\nThe metric can be implemented using [numpy](https://numpy.org) as described in [this post](https://www.kaggle.com/competitions/amex-default-prediction/discussion/328020) by [Pims](https://www.kaggle.com/yunchonggan) (almost 20x faster than pandas, hence used this version here) or in [this post](https://www.kaggle.com/competitions/amex-default-prediction/discussion/327534) by [kyakovlev](https://www.kaggle.com/kyakovlev).\n","metadata":{}},{"cell_type":"code","source":"# https://www.kaggle.com/competitions/amex-default-prediction/discussion/328020\nimport numpy as np\n\ndef amex_metric_numpy(y_true: np.array, y_pred: np.array) -> float:\n\n    # count of positives and negatives\n    n_pos = y_true.sum()\n    n_neg = y_true.shape[0] - n_pos\n\n    # sorting by descring prediction values\n    indices = np.argsort(y_pred)[::-1]\n    preds, target = y_pred[indices], y_true[indices]\n\n    # filter the top 4% by cumulative row weights\n    weight = 20.0 - target * 19.0\n    cum_norm_weight = (weight / weight.sum()).cumsum()\n    four_pct_filter = cum_norm_weight <= 0.04\n\n    # default rate captured at 4%\n    d = target[four_pct_filter].sum() / n_pos\n\n    # weighted gini coefficient\n    lorentz = (target / n_pos).cumsum()\n    gini = ((lorentz - cum_norm_weight) * weight).sum()\n\n    # max weighted gini coefficient\n    gini_max = 10 * n_neg * (1 - 19 / (n_pos + 20 * n_neg))\n\n    # normalized weighted gini coefficient\n    g = gini / gini_max\n\n    return 0.5 * (g + d)\n","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:11.753245Z","iopub.execute_input":"2022-06-03T07:43:11.754136Z","iopub.status.idle":"2022-06-03T07:43:11.764902Z","shell.execute_reply.started":"2022-06-03T07:43:11.754090Z","shell.execute_reply":"2022-06-03T07:43:11.763869Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## PyTorch Implementation\nThe metric can be implemented using [PyTorch](https://pytorch.org) which is typically useful for PT-based pipelines or models.","metadata":{}},{"cell_type":"code","source":"import torch\n\ndef amex_metric_pytorch(y_true: torch.Tensor, y_pred: torch.Tensor) -> float:\n\n    # convert dtypes to float64\n    y_true = y_true.double()\n    y_pred = y_pred.double()\n\n    # count of positives and negatives\n    n_pos = y_true.sum()\n    n_neg = y_pred.shape[0] - n_pos\n\n    # sorting by descring prediction values\n    indices = torch.argsort(y_pred, dim=0, descending=True)\n    preds, target = y_pred[indices], y_true[indices]\n\n    # filter the top 4% by cumulative row weights\n    weight = 20.0 - target * 19.0\n    cum_norm_weight = (weight / weight.sum()).cumsum(dim=0)\n    four_pct_filter = cum_norm_weight <= 0.04\n\n    # default rate captured at 4%\n    d = target[four_pct_filter].sum() / n_pos\n\n    # weighted gini coefficient\n    lorentz = (target / n_pos).cumsum(dim=0)\n    gini = ((lorentz - cum_norm_weight) * weight).sum()\n\n    # max weighted gini coefficient\n    gini_max = 10 * n_neg * (1 - 19 / (n_pos + 20 * n_neg))\n\n    # normalized weighted gini coefficient\n    g = gini / gini_max\n\n    return 0.5 * (g + d)\n","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:11.766906Z","iopub.execute_input":"2022-06-03T07:43:11.767498Z","iopub.status.idle":"2022-06-03T07:43:13.676177Z","shell.execute_reply.started":"2022-06-03T07:43:11.767437Z","shell.execute_reply":"2022-06-03T07:43:13.675355Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## TensorFlow Implementation\nThe metric can be implemented using [TensorFlow](https://www.tensorflow.org) which is typically useful for TF-based pipelines or models.","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\n\ndef amex_metric_tensorflow(y_true: tf.Tensor, y_pred: tf.Tensor) -> float:\n\n    # convert dtypes to float64\n    y_true = tf.cast(y_true, dtype=tf.float64)\n    y_pred = tf.cast(y_pred, dtype=tf.float64)\n\n    # count of positives and negatives\n    n_pos = tf.math.reduce_sum(y_true)\n    n_neg = tf.cast(tf.shape(y_true)[0], dtype=tf.float64) - n_pos\n\n    # sorting by descring prediction values\n    indices = tf.argsort(y_pred, axis=0, direction='DESCENDING')\n    preds, target = tf.gather(y_pred, indices), tf.gather(y_true, indices)\n\n    # filter the top 4% by cumulative row weights\n    weight = 20.0 - target * 19.0\n    cum_norm_weight = tf.cumsum(weight / tf.reduce_sum(weight))\n    four_pct_filter = cum_norm_weight <= 0.04\n\n    # default rate captured at 4%\n    d = tf.reduce_sum(target[four_pct_filter]) / n_pos\n\n    # weighted gini coefficient\n    lorentz = tf.cumsum(target / n_pos)\n    gini = tf.reduce_sum((lorentz - cum_norm_weight) * weight)\n\n    # max weighted gini coefficient\n    gini_max = 10 * n_neg * (1 - 19 / (n_pos + 20 * n_neg))\n\n    # normalized weighted gini coefficient\n    g = gini / gini_max\n\n    return 0.5 * (g + d)\n","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:13.678431Z","iopub.execute_input":"2022-06-03T07:43:13.679368Z","iopub.status.idle":"2022-06-03T07:43:18.821241Z","shell.execute_reply.started":"2022-06-03T07:43:13.679319Z","shell.execute_reply":"2022-06-03T07:43:18.820122Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Cross Verification\nCross verifying if the metric calculation using the different implementation yields the (almost) same results. In some cases there might be minor differences due to different sorting mechanisms of equal values.","metadata":{}},{"cell_type":"code","source":"## load data\nimport pandas as pd\n\ntrain_labels = pd.read_csv('../input/amex-default-prediction/train_labels.csv', index_col='customer_ID')\ntrain_labels","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:18.822639Z","iopub.execute_input":"2022-06-03T07:43:18.823310Z","iopub.status.idle":"2022-06-03T07:43:19.804847Z","shell.execute_reply.started":"2022-06-03T07:43:18.823264Z","shell.execute_reply":"2022-06-03T07:43:19.803713Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"## generate random predictions\nimport random\n\npredictions = pd.DataFrame({'prediction': (train_labels['target'] / 2 + [random.random() for i in range(train_labels.shape[0])])/2})\npredictions","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:19.806418Z","iopub.execute_input":"2022-06-03T07:43:19.806927Z","iopub.status.idle":"2022-06-03T07:43:19.955856Z","shell.execute_reply.started":"2022-06-03T07:43:19.806883Z","shell.execute_reply":"2022-06-03T07:43:19.954888Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print('Metric Evaluation Values\\n')\nprint(f'Pandas (Official): {amex_metric_official(train_labels, predictions)}')\nprint(f'Datatable:         {amex_metric_datatable(dt.Frame(train_labels), dt.Frame(predictions))}')\nprint(f'JAX:               {amex_metric_jax(jnp.array(train_labels).ravel(), jnp.array(predictions).ravel())}')\nprint(f'Numpy:             {amex_metric_numpy(train_labels.to_numpy().ravel(), predictions.to_numpy().ravel())}')\nprint(f'PyTorch:           {amex_metric_pytorch(torch.tensor(train_labels.values).ravel(), torch.tensor(predictions.values).ravel())}')\nprint(f'TensorFlow:        {amex_metric_tensorflow(tf.convert_to_tensor(train_labels), tf.convert_to_tensor(predictions))}')\n","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:19.957289Z","iopub.execute_input":"2022-06-03T07:43:19.957751Z","iopub.status.idle":"2022-06-03T07:43:24.648381Z","shell.execute_reply.started":"2022-06-03T07:43:19.957707Z","shell.execute_reply":"2022-06-03T07:43:24.647234Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Speed Comparison\nComparing the processing time of metric calculation using the different implementations. Each implementation is run 10 times and its average is taken.","metadata":{}},{"cell_type":"code","source":"import time\n\ntime_official = []\ntime_datatable = []\ntime_jax = []\ntime_numpy = []\ntime_pytorch = []\ntime_tensorflow = []\n\nfor i in range(10):\n    start_official = time.time()\n    metric_official = amex_metric_official(train_labels, predictions)\n    end_official = time.time()\n    time_official.append((end_official-start_official))\n\n    start_datatable = time.time()\n    metric_datatable = amex_metric_datatable(dt.Frame(train_labels), dt.Frame(predictions))\n    end_datatable = time.time()\n    time_datatable.append((end_datatable-start_datatable))\n\n    start_jax = time.time()\n    metric_jax = amex_metric_jax(jnp.array(train_labels).ravel(), jnp.array(predictions).ravel())\n    end_jax = time.time()\n    time_jax.append((end_jax-start_jax))\n\n    start_numpy = time.time()\n    metric_numpy = amex_metric_numpy(train_labels.to_numpy().ravel(), predictions.to_numpy().ravel())\n    end_numpy = time.time()\n    time_numpy.append((end_numpy-start_numpy))\n\n    start_pytorch = time.time()\n    metric_pytorch = amex_metric_pytorch(torch.tensor(train_labels.values).ravel(), torch.tensor(predictions.values).ravel())\n    end_pytorch = time.time()\n    time_pytorch.append((end_pytorch-start_pytorch))\n\n    start_tensorflow = time.time()\n    metric_tensorflow = amex_metric_tensorflow(tf.convert_to_tensor(train_labels), tf.convert_to_tensor(predictions))\n    end_tensorflow = time.time()\n    time_tensorflow.append((end_tensorflow-start_tensorflow))\n\nprint(f'Metric Processing Time (in seconds)\\n')\nprint(f'Numpy:             {sum(time_numpy) / len(time_numpy)}')\nprint(f'PyTorch:           {sum(time_pytorch) / len(time_pytorch)}')\nprint(f'TensorFlow:        {sum(time_tensorflow) / len(time_tensorflow)}')\nprint(f'Datatable:         {sum(time_datatable) / len(time_datatable)}')\nprint(f'JAX:               {sum(time_jax) / len(time_jax)}')\nprint(f'Pandas (Official): {sum(time_official) / len(time_official)}')\n","metadata":{"execution":{"iopub.status.busy":"2022-06-03T07:43:24.650509Z","iopub.execute_input":"2022-06-03T07:43:24.650972Z","iopub.status.idle":"2022-06-03T07:43:46.451492Z","shell.execute_reply.started":"2022-06-03T07:43:24.650932Z","shell.execute_reply":"2022-06-03T07:43:46.450411Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Different / Better / Faster\nHave a different or better or faster implementation of the metric?\n\nPlease share it in the comments and I'll be happy to update the notebook with more implementations as a growing compilation.","metadata":{}}]}