diff --git a/questions/189_calculate-the-pinball-loss/description.md b/questions/189_calculate-the-pinball-loss/description.md new file mode 100644 index 00000000..2b74b4ac --- /dev/null +++ b/questions/189_calculate-the-pinball-loss/description.md @@ -0,0 +1 @@ +Implement a function to calculate the mean Pinball Loss (also known as the Quantile Loss) between arrays of actual and predicted values for a given quantile level. The Pinball Loss is the standard metric used to evaluate quantile regression models: unlike symmetric losses such as MAE or MSE, it penalizes under-predictions and over-predictions differently depending on the target quantile. diff --git a/questions/189_calculate-the-pinball-loss/example.json b/questions/189_calculate-the-pinball-loss/example.json new file mode 100644 index 00000000..c5668445 --- /dev/null +++ b/questions/189_calculate-the-pinball-loss/example.json @@ -0,0 +1,5 @@ +{ + "input": "y_true = np.array([3, -0.5, 2, 7]), y_pred = np.array([2.5, 0.0, 2, 8]), quantile = 0.5", + "output": "0.25", + "reasoning": "The per-sample errors (y_true - y_pred) are [0.5, -0.5, 0, -1]. With quantile 0.5 each error contributes 0.5 * |error|, giving [0.25, 0.25, 0, 0.5]. The mean of these values is 1.0 / 4 = 0.25." +} diff --git a/questions/189_calculate-the-pinball-loss/learn.md b/questions/189_calculate-the-pinball-loss/learn.md new file mode 100644 index 00000000..47c3799d --- /dev/null +++ b/questions/189_calculate-the-pinball-loss/learn.md @@ -0,0 +1,59 @@ +## Pinball Loss (Quantile Loss) + +The **Pinball Loss**, also called the **Quantile Loss**, measures how well a model predicts a specific quantile $\tau \in (0, 1)$ of the target distribution. When $\tau = 0.5$ it corresponds (up to a factor of 2) to the Mean Absolute Error, but for other quantiles it becomes an **asymmetric** loss, which is exactly what makes it useful for quantile regression. + +1. **Per-sample Formula**: + + For a single observation with true value $y_i$ and prediction $\hat{y}_i$, let the error be $e_i = y_i - \hat{y}_i$. The pinball loss is: + + $$ + L_\tau(y_i, \hat{y}_i) = + \begin{cases} + \tau \, (y_i - \hat{y}_i) & \text{if } y_i \geq \hat{y}_i \\[4pt] + (1 - \tau)(\hat{y}_i - y_i) & \text{if } y_i < \hat{y}_i + \end{cases} + $$ + + A convenient closed form that avoids the branch is: + + $$ + L_\tau(y_i, \hat{y}_i) = \max\big(\tau \, e_i,\; (\tau - 1)\, e_i\big) + $$ + +2. **Mean over the dataset**: + + The reported loss is the average over all $n$ observations: + + $$ + \text{PinballLoss} = \frac{1}{n} \sum_{i=1}^{n} L_\tau(y_i, \hat{y}_i) + $$ + +3. **Intuition**: + - When the model **under-predicts** ($\hat{y}_i < y_i$), the error is weighted by $\tau$. + - When the model **over-predicts** ($\hat{y}_i > y_i$), the error is weighted by $1 - \tau$. + - Choosing a **high** quantile (e.g. $\tau = 0.9$) makes under-prediction expensive, pushing predictions upward. Choosing a **low** quantile (e.g. $\tau = 0.1$) does the opposite. + +4. **Example Calculation**: + + For the values: + ``` + y_true = [3, -0.5, 2, 7] + y_pred = [2.5, 0.0, 2, 8] + quantile = 0.5 + ``` + + Per-sample errors $e_i = y_i - \hat{y}_i$ are $[0.5, -0.5, 0, -1]$, giving: + + $$ + \begin{align*} + \text{PinballLoss} &= \frac{1}{4}\big(0.5\cdot0.5 + 0.5\cdot0.5 + 0 + 0.5\cdot1\big) \\ + &= \frac{1}{4}(0.25 + 0.25 + 0 + 0.5) \\ + &= \frac{1.0}{4} = 0.25 + \end{align*} + $$ + +5. **Properties**: + - Pinball loss is always non-negative: $\text{PinballLoss} \geq 0$. + - Perfect predictions give a loss of $0$. + - At $\tau = 0.5$ the loss equals $\tfrac{1}{2}\,\text{MAE}$. + - It is convex in the prediction, so it can be minimized with gradient-based optimization. diff --git a/questions/189_calculate-the-pinball-loss/meta.json b/questions/189_calculate-the-pinball-loss/meta.json new file mode 100644 index 00000000..5ee299e0 --- /dev/null +++ b/questions/189_calculate-the-pinball-loss/meta.json @@ -0,0 +1,15 @@ +{ + "id": "189", + "title": "Calculate the Pinball Loss", + "difficulty": "easy", + "category": "Machine Learning", + "video": "", + "likes": "0", + "dislikes": "0", + "contributor": [ + { + "profile_link": "https://github.com/thealper2", + "name": "Alper Karaca" + } + ] +} diff --git a/questions/189_calculate-the-pinball-loss/solution.py b/questions/189_calculate-the-pinball-loss/solution.py new file mode 100644 index 00000000..dcbd3cb6 --- /dev/null +++ b/questions/189_calculate-the-pinball-loss/solution.py @@ -0,0 +1,24 @@ +import numpy as np + +def pinball_loss(y_true, y_pred, quantile=0.5): + """ + Calculate the mean Pinball (Quantile) Loss between two arrays. + + Parameters: + y_true (numpy.ndarray): Array of true values + y_pred (numpy.ndarray): Array of predicted values + quantile (float): Target quantile in the open interval (0, 1) + + Returns: + float: Mean Pinball Loss rounded to 3 decimal places + """ + if y_true.shape != y_pred.shape: + raise ValueError("Arrays must have the same shape") + if y_true.size == 0: + raise ValueError("Arrays cannot be empty") + if not 0 < quantile < 1: + raise ValueError("quantile must be in the open interval (0, 1)") + + error = y_true - y_pred + loss = np.maximum(quantile * error, (quantile - 1) * error) + return round(float(np.mean(loss)), 3) diff --git a/questions/189_calculate-the-pinball-loss/starter_code.py b/questions/189_calculate-the-pinball-loss/starter_code.py new file mode 100644 index 00000000..1bb3a05f --- /dev/null +++ b/questions/189_calculate-the-pinball-loss/starter_code.py @@ -0,0 +1,17 @@ +import numpy as np + +def pinball_loss(y_true, y_pred, quantile=0.5): + """ + Calculate the mean Pinball (Quantile) Loss between two arrays. + + Parameters: + y_true (numpy.ndarray): Array of true values + y_pred (numpy.ndarray): Array of predicted values + quantile (float): Target quantile in the open interval (0, 1) + + Returns: + float: Mean Pinball Loss rounded to 3 decimal places + """ + # Your code here + pass + return round(val, 3) diff --git a/questions/189_calculate-the-pinball-loss/tests.json b/questions/189_calculate-the-pinball-loss/tests.json new file mode 100644 index 00000000..5178c4c5 --- /dev/null +++ b/questions/189_calculate-the-pinball-loss/tests.json @@ -0,0 +1,22 @@ +[ + { + "test": "print(pinball_loss(np.array([3, -0.5, 2, 7]), np.array([2.5, 0.0, 2, 8]), 0.5))", + "expected_output": "0.25" + }, + { + "test": "print(pinball_loss(np.array([10, 20, 30]), np.array([12, 18, 32]), 0.9))", + "expected_output": "0.733" + }, + { + "test": "print(pinball_loss(np.array([1, 2, 3, 4]), np.array([1.5, 2.5, 2.5, 5]), 0.1))", + "expected_output": "0.463" + }, + { + "test": "print(pinball_loss(np.array([5, 5, 5]), np.array([5, 5, 5]), 0.5))", + "expected_output": "0.0" + }, + { + "test": "print(pinball_loss(np.array([[1, 2], [3, 4]]), np.array([[1.5, 1], [2, 5]]), 0.7))", + "expected_output": "0.462" + } +]