Repository: alteryx/evalml (python)

Issue:
Time series regression: add MASE/MAPE/SMAPE as metrics
MASE is a metric for time series regression which EvalML supports. MASE and SMAPE are two other commonly used metrics for time series regression.

Pull request description:
Added MASE metric and `y_train` parameter to objectives
Resolves #4217 

Patch:
diff --git a/docs/source/api_index.rst b/docs/source/api_index.rst
index 9092038e..f87a4ec0 100644
--- a/docs/source/api_index.rst
+++ b/docs/source/api_index.rst
@@ -409,7 +409,9 @@ Regression Objectives
 
     evalml.objectives.ExpVariance
     evalml.objectives.MAE
+    evalml.objectives.MASE
     evalml.objectives.MAPE
+    evalml.objectives.SMAPE
     evalml.objectives.MSE
     evalml.objectives.MeanSquaredLogError
     evalml.objectives.MedianAE
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 6a2264ad..652f8eac 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -19,6 +19,7 @@ Release Notes
     * Enhancements
         * Add run_feature_selection to AutoMLSearch and Default Algorithm :pr:`4210`
         * Added ``SMAPE`` to the standard metrics for time series problems :pr:`4220`
+        * Added ``MASE`` metric and ``y_train`` parameter to objectives :pr:`4221`
     * Fixes
         * `IDColumnsDataCheck` now works with Unknown data type :pr:`4203`
     * Changes
diff --git a/evalml/objectives/__init__.py b/evalml/objectives/__init__.py
index e05bfb4b..0281d77d 100644
--- a/evalml/objectives/__init__.py
+++ b/evalml/objectives/__init__.py
@@ -15,6 +15,7 @@ from evalml.objectives.standard_metrics import (
     AUC,
     F1,
     MAE,
+    MASE,
     MAPE,
     SMAPE,
     MSE,
diff --git a/evalml/objectives/cost_benefit_matrix.py b/evalml/objectives/cost_benefit_matrix.py
index 6ad12510..ae561dfc 100644
--- a/evalml/objectives/cost_benefit_matrix.py
+++ b/evalml/objectives/cost_benefit_matrix.py
@@ -35,12 +35,20 @@ class CostBenefitMatrix(BinaryClassificationObjective):
         self.false_positive = false_positive
         self.false_negative = false_negative
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Calculates cost-benefit of the using the predicted and true values.
 
         Args:
             y_predicted (pd.Series): Predicted labels.
             y_true (pd.Series): True labels.
+            y_train (pd.Series): Ignored.
             X (pd.DataFrame): Ignored.
             sample_weight (pd.DataFrame): Ignored.
 
diff --git a/evalml/objectives/fraud_cost.py b/evalml/objectives/fraud_cost.py
index 07a6127a..6bbaccb4 100644
--- a/evalml/objectives/fraud_cost.py
+++ b/evalml/objectives/fraud_cost.py
@@ -36,12 +36,20 @@ class FraudCost(BinaryClassificationObjective):
         self.fraud_payout_percentage = fraud_payout_percentage
         self.amount_col = amount_col
 
-    def objective_function(self, y_true, y_predicted, X, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        X,
+        y_train=None,
+        sample_weight=None,
+    ):
         """Calculate amount lost to fraud per transaction given predictions, true values, and dataframe with transaction amount.
 
         Args:
             y_predicted (pd.Series): Predicted fraud labels.
             y_true (pd.Series): True fraud labels.
+            y_train (pd.Series): Ignored.
             X (pd.DataFrame): Data with transaction amounts.
             sample_weight (pd.DataFrame): Ignored.
 
diff --git a/evalml/objectives/lead_scoring.py b/evalml/objectives/lead_scoring.py
index dbbeb885..3231a278 100644
--- a/evalml/objectives/lead_scoring.py
+++ b/evalml/objectives/lead_scoring.py
@@ -25,12 +25,20 @@ class LeadScoring(BinaryClassificationObjective):
         self.true_positives = true_positives
         self.false_positives = false_positives
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Calculate the profit per lead.
 
         Args:
-            y_predicted (pd.Series): Predicted labels
-            y_true (pd.Series): True labels
+            y_predicted (pd.Series): Predicted labels.
+            y_true (pd.Series): True labels.
+            y_train (pd.Series): Ignored.
             X (pd.DataFrame): Ignored.
             sample_weight (pd.DataFrame): Ignored.
 
diff --git a/evalml/objectives/objective_base.py b/evalml/objectives/objective_base.py
index 0bb82e57..b32ee79f 100644
--- a/evalml/objectives/objective_base.py
+++ b/evalml/objectives/objective_base.py
@@ -61,12 +61,20 @@ class ObjectiveBase(ABC):
 
     @classmethod
     @abstractmethod
-    def objective_function(cls, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        cls,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Computes the relative value of the provided predictions compared to the actual labels, according a specified metric.
 
         Args:
             y_predicted (pd.Series): Predicted values of length [n_samples]
             y_true (pd.Series): Actual class labels of length [n_samples]
+            y_train (pd.Series): Observed training values of length [n_samples]
             X (pd.DataFrame or np.ndarray): Extra data of shape [n_samples, n_features] necessary to calculate score
             sample_weight (pd.DataFrame or np.ndarray): Sample weights used in computing objective value result
 
@@ -79,12 +87,13 @@ class ObjectiveBase(ABC):
         """If True, this objective is only valid for positive data. Defaults to False."""
         return False
 
-    def score(self, y_true, y_predicted, X=None, sample_weight=None):
+    def score(self, y_true, y_predicted, y_train=None, X=None, sample_weight=None):
         """Returns a numerical score indicating performance based on the differences between the predicted and actual values.
 
         Args:
             y_predicted (pd.Series): Predicted values of length [n_samples]
             y_true (pd.Series): Actual class labels of length [n_samples]
+            y_train (pd.Series): Observed training values of length [n_samples]
             X (pd.DataFrame or np.ndarray): Extra data of shape [n_samples, n_features] necessary to calculate score
             sample_weight (pd.DataFrame or np.ndarray): Sample weights used in computing objective value result
 
@@ -93,12 +102,15 @@ class ObjectiveBase(ABC):
         """
         if X is not None:
             X = self._standardize_input_type(X)
+        if y_train is not None:
+            y_train = self._standardize_input_type(y_train)
         y_true = self._standardize_input_type(y_true)
         y_predicted = self._standardize_input_type(y_predicted)
         self.validate_inputs(y_true, y_predicted)
         return self.objective_function(
             y_true,
             y_predicted,
+            y_train=y_train,
             X=X,
             sample_weight=sample_weight,
         )
diff --git a/evalml/objectives/standard_metrics.py b/evalml/objectives/standard_metrics.py
index 5db498de..584233f3 100644
--- a/evalml/objectives/standard_metrics.py
+++ b/evalml/objectives/standard_metrics.py
@@ -5,7 +5,10 @@ import numpy as np
 import pandas as pd
 from sklearn import metrics
 from sklearn.preprocessing import label_binarize
-from sktime.performance_metrics.forecasting import MeanAbsolutePercentageError
+from sktime.performance_metrics.forecasting import (
+    MeanAbsolutePercentageError,
+    MeanAbsoluteScaledError,
+)
 
 from evalml.objectives.binary_classification_objective import (
     BinaryClassificationObjective,
@@ -36,7 +39,14 @@ class AccuracyBinary(BinaryClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for accuracy score for binary classification."""
         return metrics.accuracy_score(y_true, y_predicted, sample_weight=sample_weight)
 
@@ -57,7 +67,14 @@ class AccuracyMulticlass(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for accuracy score for multiclass classification."""
         return metrics.accuracy_score(y_true, y_predicted, sample_weight=sample_weight)
 
@@ -78,7 +95,14 @@ class BalancedAccuracyBinary(BinaryClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for accuracy score for balanced accuracy for binary classification."""
         return metrics.balanced_accuracy_score(
             y_true,
@@ -103,7 +127,14 @@ class BalancedAccuracyMulticlass(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for accuracy score for balanced accuracy for multiclass classification."""
         return metrics.balanced_accuracy_score(
             y_true,
@@ -128,7 +159,14 @@ class F1(BinaryClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for F1 score for binary classification."""
         return metrics.f1_score(
             y_true,
@@ -154,7 +192,14 @@ class F1Micro(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for F1 score for multiclass classification."""
         return metrics.f1_score(
             y_true,
@@ -181,7 +226,14 @@ class F1Macro(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for F1 score for multiclass classification using macro averaging."""
         return metrics.f1_score(
             y_true,
@@ -208,7 +260,14 @@ class F1Weighted(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for F1 score for multiclass classification using weighted averaging."""
         return metrics.f1_score(
             y_true,
@@ -235,7 +294,14 @@ class Precision(BinaryClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for precision score for binary classification."""
         return metrics.precision_score(
             y_true,
@@ -261,7 +327,14 @@ class PrecisionMicro(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for precision score for binary classification using micro-averaging."""
         return metrics.precision_score(
             y_true,
@@ -288,7 +361,14 @@ class PrecisionMacro(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for precision score for multiclass classification using macro-averaging."""
         return metrics.precision_score(
             y_true,
@@ -315,7 +395,14 @@ class PrecisionWeighted(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for precision score for multiclass classification using weighted averaging."""
         return metrics.precision_score(
             y_true,
@@ -342,7 +429,14 @@ class Recall(BinaryClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for recall score for binary classification."""
         return metrics.recall_score(
             y_true,
@@ -368,7 +462,14 @@ class RecallMicro(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for recall score for multiclass classification using micro-averaging."""
         return metrics.recall_score(
             y_true,
@@ -395,7 +496,14 @@ class RecallMacro(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for recall score for multiclass classification using macro-averaging."""
         return metrics.recall_score(
             y_true,
@@ -422,7 +530,14 @@ class RecallWeighted(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for recall score for multiclass classification using weighted averaging."""
         return metrics.recall_score(
             y_true,
@@ -449,7 +564,14 @@ class AUC(BinaryClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for AUC score for binary classification."""
         return metrics.roc_auc_score(y_true, y_predicted, sample_weight=sample_weight)
 
@@ -475,7 +597,14 @@ class AUCMicro(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for AUC score for multiclass classification using micro-averaging."""
         y_true, y_predicted = _handle_predictions(y_true, y_predicted)
         return metrics.roc_auc_score(
@@ -507,7 +636,14 @@ class AUCMacro(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for AUC score for multiclass classification using macro-averaging."""
         y_true, y_predicted = _handle_predictions(y_true, y_predicted)
         return metrics.roc_auc_score(
@@ -539,7 +675,14 @@ class AUCWeighted(MulticlassClassificationObjective):
     is_bounded_like_percentage = True
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for AUC Score for multiclass classification using weighted averaging."""
         y_true, y_predicted = _handle_predictions(y_true, y_predicted)
         return metrics.roc_auc_score(
@@ -566,7 +709,14 @@ class Gini(BinaryClassificationObjective):
     is_bounded_like_percentage = False
     expected_range = [-1, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for Gini coefficient for binary classification."""
         auc = metrics.roc_auc_score(y_true, y_predicted, sample_weight=sample_weight)
         return 2 * auc - 1
@@ -588,7 +738,14 @@ class LogLossBinary(BinaryClassificationObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for log loss for binary classification."""
         return metrics.log_loss(y_true, y_predicted, sample_weight=sample_weight)
 
@@ -614,7 +771,14 @@ class LogLossMulticlass(MulticlassClassificationObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for log loss for multiclass classification."""
         return metrics.log_loss(y_true, y_predicted, sample_weight=sample_weight)
 
@@ -635,7 +799,14 @@ class MCCBinary(BinaryClassificationObjective):
     is_bounded_like_percentage = False  # Range [-1, 1]
     expected_range = [-1, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for Matthews correlation coefficient for binary classification."""
         with warnings.catch_warnings():
             # catches runtime warning when dividing by 0.0
@@ -663,7 +834,14 @@ class MCCMulticlass(MulticlassClassificationObjective):
     is_bounded_like_percentage = False  # Range [-1, 1]
     expected_range = [-1, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for Matthews correlation coefficient for multiclass classification."""
         with warnings.catch_warnings():
             # catches runtime warning when dividing by 0.0
@@ -691,7 +869,14 @@ class RootMeanSquaredError(RegressionObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, float("inf")]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for root mean squared error for regression."""
         return metrics.mean_squared_error(
             y_true,
@@ -719,7 +904,14 @@ class RootMeanSquaredLogError(RegressionObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, float("inf")]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for root mean squared log error for regression."""
 
         def rmsle(y_true, y_pred):
@@ -767,7 +959,14 @@ class MeanSquaredLogError(RegressionObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, float("inf")]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for mean squared log error for regression."""
         return metrics.mean_squared_log_error(
             y_true,
@@ -797,7 +996,14 @@ class R2(RegressionObjective):
     is_bounded_like_percentage = False  # Range (-Inf, 1]
     expected_range = [-1, 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for coefficient of determination for regression."""
         return metrics.r2_score(y_true, y_predicted, sample_weight=sample_weight)
 
@@ -818,7 +1024,14 @@ class MAE(RegressionObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, float("inf")]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for mean absolute error for regression."""
         return metrics.mean_absolute_error(
             y_true,
@@ -827,6 +1040,43 @@ class MAE(RegressionObjective):
         )
 
 
+class MASE(TimeSeriesRegressionObjective):
+    """Mean absolute scaled error for time series regression.
+
+    Only valid if there exists a nonzero input in y_train. Otherwise, will throw a ValueError.
+
+    Example:
+        >>> y_train = pd.Series([5, 0.5, 4, 6, 3, 5, 2])
+        >>> y_true = pd.Series([3, -0.5, 2, 7, 2])
+        >>> y_pred = pd.Series([2.5, 0.0, 2, 8, 1.25])
+        >>> np.testing.assert_almost_equal(MASE().objective_function(y_true, y_pred, y_train), 0.18333333333333335)
+    """
+
+    name = "Mean Absolute Scaled Error"
+    greater_is_better = False
+    score_needs_proba = False
+    perfect_score = 0.0
+    is_bounded_like_percentage = False  # Range [0, Inf)
+    expected_range = [0, float("inf")]
+
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train,
+        X=None,
+        sample_weight=None,
+    ):
+        """Objective function for mean absolute scaled error for time series regression."""
+        if (y_train.values == 0).all():
+            raise ValueError(
+                "Mean Absolute Scaled Error cannot be used when "
+                "all training targets contain the value 0.",
+            )
+        mase = MeanAbsoluteScaledError()
+        return mase(y_true, y_predicted, y_train=y_train)
+
+
 class MAPE(TimeSeriesRegressionObjective):
     """Mean absolute percentage error for time series regression. Scaled by 100 to return a percentage.
 
@@ -845,7 +1095,14 @@ class MAPE(TimeSeriesRegressionObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, float("inf")]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for mean absolute percentage error for time series regression."""
         if 0 in y_true.values:
             raise ValueError(
@@ -855,14 +1112,9 @@ class MAPE(TimeSeriesRegressionObjective):
         mape = MeanAbsolutePercentageError()
         return mape(y_true, y_predicted) * 100
 
-    @classproperty
-    def positive_only(self):
-        """If True, this objective is only valid for positive data."""
-        return True
-
 
 class SMAPE(TimeSeriesRegressionObjective):
-    """Mean absolute percentage error for time series regression. Scaled by 100 to return a percentage.
+    """Symmetric mean absolute percentage error for time series regression. Scaled by 100 to return a percentage.
 
     Only valid for nonzero inputs. Otherwise, will throw a ValueError.
 
@@ -879,22 +1131,23 @@ class SMAPE(TimeSeriesRegressionObjective):
     is_bounded_like_percentage = True  # Range [0, 200]
     expected_range = [0, 200]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
-        """Objective function for mean absolute percentage error for time series regression."""
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
+        """Objective function for symmetric mean absolute percentage error for time series regression."""
         if 0 in (abs(y_true) + abs(y_predicted)).values:
             raise ValueError(
                 "Symmetric Mean Absolute Percentage Error cannot be used when "
                 "true and predicted targets both contain the value 0.",
             )
-
         smape = MeanAbsolutePercentageError(symmetric=True)
         return smape(y_true, y_predicted) * 100
 
-    @classproperty
-    def positive_only(self):
-        """If True, this objective is only valid for positive data."""
-        return True
-
 
 class MSE(RegressionObjective):
     """Mean squared error for regression.
@@ -912,7 +1165,14 @@ class MSE(RegressionObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, float("inf")]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for mean squared error for regression."""
         return metrics.mean_squared_error(
             y_true,
@@ -937,7 +1197,14 @@ class MedianAE(RegressionObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, float("inf")]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for median absolute error for regression."""
         return metrics.median_absolute_error(
             y_true,
@@ -962,7 +1229,14 @@ class MaxError(RegressionObjective):
     is_bounded_like_percentage = False  # Range [0, Inf)
     expected_range = [0, float("inf")]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for maximum residual error for regression."""
         # Multiseries time series regression
         if isinstance(y_true, pd.DataFrame):
@@ -993,7 +1267,14 @@ class ExpVariance(RegressionObjective):
     is_bounded_like_percentage = False  # Range (-Inf, 1]
     expected_range = [float("-inf"), 1]
 
-    def objective_function(self, y_true, y_predicted, X=None, sample_weight=None):
+    def objective_function(
+        self,
+        y_true,
+        y_predicted,
+        y_train=None,
+        X=None,
+        sample_weight=None,
+    ):
         """Objective function for explained variance score for regression."""
         return metrics.explained_variance_score(
             y_true,
diff --git a/evalml/pipelines/pipeline_base.py b/evalml/pipelines/pipeline_base.py
index 4b726b1e..a5b71c7b 100644
--- a/evalml/pipelines/pipeline_base.py
+++ b/evalml/pipelines/pipeline_base.py
@@ -352,7 +352,7 @@ class PipelineBase(ABC, metaclass=PipelineBaseMeta):
 
     @staticmethod
     def _score(X, y, predictions, objective):
-        return objective.score(y, predictions, X)
+        return objective.score(y, predictions, X=X)
 
     def _score_all_objectives(self, X, y, y_pred, y_pred_proba, objectives):
         """Given data, model predictions or predicted probabilities computed on the data, and an objective, evaluate and return the objective score.


Test patch:
diff --git a/evalml/tests/data_checks_tests/test_data_checks.py b/evalml/tests/data_checks_tests/test_data_checks.py
index 69768957..02aef2db 100644
--- a/evalml/tests/data_checks_tests/test_data_checks.py
+++ b/evalml/tests/data_checks_tests/test_data_checks.py
@@ -674,7 +674,6 @@ def test_data_checks_raises_value_errors_on_init(
     [
         "Root Mean Squared Log Error",
         "Mean Squared Log Error",
-        "Mean Absolute Percentage Error",
     ],
 )
 def test_errors_warnings_in_invalid_target_data_check(objective, ts_data):
diff --git a/evalml/tests/data_checks_tests/test_invalid_target_data_check.py b/evalml/tests/data_checks_tests/test_invalid_target_data_check.py
index 996f7406..591bc027 100644
--- a/evalml/tests/data_checks_tests/test_invalid_target_data_check.py
+++ b/evalml/tests/data_checks_tests/test_invalid_target_data_check.py
@@ -15,7 +15,10 @@ from evalml.data_checks import (
     InvalidTargetDataCheck,
 )
 from evalml.exceptions import DataCheckInitError
-from evalml.objectives import MAPE, SMAPE, MeanSquaredLogError, RootMeanSquaredLogError
+from evalml.objectives import (
+    MeanSquaredLogError,
+    RootMeanSquaredLogError,
+)
 from evalml.problem_types import ProblemTypes, is_binary, is_multiclass, is_regression
 from evalml.utils.woodwork_utils import numeric_and_boolean_ww
 
@@ -342,7 +345,6 @@ def test_invalid_target_data_check_n_unique(problem_type):
     [
         "Root Mean Squared Log Error",
         "Mean Squared Log Error",
-        "Mean Absolute Percentage Error",
     ],
 )
 def test_invalid_target_data_check_invalid_labels_for_nonnegative_objective_names(
@@ -397,7 +399,7 @@ def test_invalid_target_data_check_invalid_labels_for_nonnegative_objective_name
 
 @pytest.mark.parametrize(
     "objective",
-    [RootMeanSquaredLogError(), MeanSquaredLogError(), MAPE(), SMAPE()],
+    [RootMeanSquaredLogError(), MeanSquaredLogError()],
 )
 def test_invalid_target_data_check_invalid_labels_for_nonnegative_objective_instances(
     objective,
@@ -465,7 +467,6 @@ def test_invalid_target_data_check_invalid_labels_for_objectives(
     [
         "Root Mean Squared Log Error",
         "Mean Squared Log Error",
-        "Mean Absolute Percentage Error",
     ],
 )
 def test_invalid_target_data_check_valid_labels_for_nonnegative_objectives(objective):
diff --git a/evalml/tests/objective_tests/test_fraud_detection.py b/evalml/tests/objective_tests/test_fraud_detection.py
index 95acb00a..47a35b55 100644
--- a/evalml/tests/objective_tests/test_fraud_detection.py
+++ b/evalml/tests/objective_tests/test_fraud_detection.py
@@ -129,13 +129,13 @@ def test_fraud_objective_score():
     out = fraud_cost.decision_function(y_predicted, 0.45)
     assert isinstance(out, pd.Series)
     pd.testing.assert_series_equal(out, y_true, check_dtype=False, check_names=False)
-    score = fraud_cost.score(y_true, out, extra_columns)
+    score = fraud_cost.score(y_true=y_true, y_predicted=out, X=extra_columns)
     assert score == 0.0
 
     out = fraud_cost.decision_function(y_predicted.to_numpy(), 0.45)
     assert isinstance(out, pd.Series)
     pd.testing.assert_series_equal(out, y_true, check_names=False)
-    score = fraud_cost.score(y_true, out, extra_columns)
+    score = fraud_cost.score(y_true=y_true, y_predicted=out, X=extra_columns)
     assert score == 0.0
 
     # testing with other types of inputs
@@ -143,7 +143,7 @@ def test_fraud_objective_score():
     extra_columns = pd.DataFrame({"value": [100, 5, 250]})
     out = fraud_cost.decision_function(y_predicted, 0.45)
     pd.testing.assert_series_equal(out, y_true, check_names=False)
-    score = fraud_cost.score(y_true, out, extra_columns)
+    score = fraud_cost.score(y_true=y_true, y_predicted=out, X=extra_columns)
     assert score == 0.0
 
     y_predicted = pd.Series([0.2, 0.01, 0.01])
@@ -152,7 +152,7 @@ def test_fraud_objective_score():
     expected_y_pred = pd.Series([True, False, False])
     out = fraud_cost.decision_function(y_predicted, 0.1)
     pd.testing.assert_series_equal(out, expected_y_pred, check_names=False)
-    score = fraud_cost.score(y_true, out, extra_columns)
+    score = fraud_cost.score(y_true=y_true, y_predicted=out, X=extra_columns)
     assert score == 0.255
 
 
@@ -166,7 +166,7 @@ def test_fraud_objective_score_list():
     out = fraud_cost.decision_function(y_predicted, 0.45)
     assert isinstance(out, pd.Series)
     pd.testing.assert_series_equal(out, pd.Series(y_true), check_names=False)
-    score = fraud_cost.score(y_true, out, extra_columns)
+    score = fraud_cost.score(y_true=y_true, y_predicted=out, X=extra_columns)
     assert score == 0.0
 
 
@@ -181,5 +181,5 @@ def test_fraud_objective_one_prediction_penalty(predictions, score):
     extra_columns = pd.DataFrame({"value": [100, 5, 250]})
 
     out = fraud_cost.decision_function(y_predicted, 0.45)
-    scores = fraud_cost.score(y_true, out, extra_columns)
+    scores = fraud_cost.score(y_true=y_true, y_predicted=out, X=extra_columns)
     assert scores == score
diff --git a/evalml/tests/objective_tests/test_objectives.py b/evalml/tests/objective_tests/test_objectives.py
index a14e7eb5..85fccda4 100644
--- a/evalml/tests/objective_tests/test_objectives.py
+++ b/evalml/tests/objective_tests/test_objectives.py
@@ -27,7 +27,7 @@ from evalml.objectives import (
 )
 from evalml.objectives.fraud_cost import FraudCost
 from evalml.objectives.objective_base import ObjectiveBase
-from evalml.objectives.standard_metrics import MAPE, SMAPE
+from evalml.objectives.standard_metrics import MAPE, MASE, SMAPE
 from evalml.objectives.utils import _all_objectives_dict
 from evalml.problem_types import ProblemTypes
 
@@ -111,21 +111,21 @@ def test_get_core_objectives_types():
     assert len(get_core_objectives(ProblemTypes.MULTICLASS)) == 13
     assert len(get_core_objectives(ProblemTypes.BINARY)) == 8
     assert len(get_core_objectives(ProblemTypes.REGRESSION)) == 7
-    assert len(get_core_objectives(ProblemTypes.TIME_SERIES_REGRESSION)) == 8
+    assert len(get_core_objectives(ProblemTypes.TIME_SERIES_REGRESSION)) == 9
 
 
 def test_get_optimization_objectives_types():
     assert len(get_optimization_objectives(ProblemTypes.MULTICLASS)) == 13
     assert len(get_optimization_objectives(ProblemTypes.BINARY)) == 8
     assert len(get_optimization_objectives(ProblemTypes.REGRESSION)) == 7
-    assert len(get_optimization_objectives(ProblemTypes.TIME_SERIES_REGRESSION)) == 8
+    assert len(get_optimization_objectives(ProblemTypes.TIME_SERIES_REGRESSION)) == 9
 
 
 def test_get_ranking_objectives_types():
     assert len(get_ranking_objectives(ProblemTypes.MULTICLASS)) == 16
     assert len(get_ranking_objectives(ProblemTypes.BINARY)) == 9
     assert len(get_ranking_objectives(ProblemTypes.REGRESSION)) == 9
-    assert len(get_ranking_objectives(ProblemTypes.TIME_SERIES_REGRESSION)) == 11
+    assert len(get_ranking_objectives(ProblemTypes.TIME_SERIES_REGRESSION)) == 12
 
 
 def test_optimization_excludes_ranking():
@@ -135,7 +135,7 @@ def test_optimization_excludes_ranking():
 
 
 def test_get_time_series_objectives_types(time_series_objectives):
-    assert len(time_series_objectives) == 11
+    assert len(time_series_objectives) == 12
 
 
 def test_objective_outputs(
@@ -202,16 +202,17 @@ def test_get_objectives_all_expected_ranges(obj):
 
 @pytest.mark.parametrize("obj", [obj for obj in _all_objectives_dict().values()])
 @pytest.mark.parametrize(
-    "nullable_y_true_ltype",
+    "nullable_ltype",
     ["BooleanNullable", "IntegerNullable", "AgeNullable"],
 )
 def test_objectives_support_nullable_types(
-    nullable_y_true_ltype,
+    nullable_ltype,
     obj,
     nullable_type_target,
 ):
-    y_true = nullable_type_target(ltype=nullable_y_true_ltype, has_nans=False)
+    y_true = nullable_type_target(ltype=nullable_ltype, has_nans=False)
     y_pred = pd.Series([0, 1, 0, 1, 1] * 4, dtype="int64")
+    y_train = nullable_type_target(ltype=nullable_ltype, has_nans=False)
     X = None
 
     # Instantiate objective with any needed parameters
@@ -235,8 +236,13 @@ def test_objectives_support_nullable_types(
         # Replace numeric inputs containing 0
         y_true = y_true.ww.replace({0: 10})
         y_pred = y_pred.replace({0: 10})
+    elif isinstance(obj, MASE):
+        if isinstance(y_train.ww.logical_type, BooleanNullable):
+            pytest.skip("MASE doesn't support inputs containing all 0")
+        # Replace numeric inputs containing 0
+        y_train = y_train.ww.replace({0: 10})
 
-    score = obj.score(y_true=y_true, y_predicted=y_pred, X=X)
+    score = obj.score(y_true=y_true, y_predicted=y_pred, y_train=y_train, X=X)
     assert not pd.isna(score)
 
 
diff --git a/evalml/tests/objective_tests/test_standard_metrics.py b/evalml/tests/objective_tests/test_standard_metrics.py
index 4aee13db..3642a239 100644
--- a/evalml/tests/objective_tests/test_standard_metrics.py
+++ b/evalml/tests/objective_tests/test_standard_metrics.py
@@ -7,6 +7,7 @@ import pytest
 from evalml.objectives import (
     F1,
     MAPE,
+    MASE,
     MSE,
     SMAPE,
     AccuracyBinary,
@@ -166,9 +167,10 @@ def test_regression_handles_dataframes(objective_class):
 
     y_predicted = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
     y_true = pd.DataFrame({"a": [1, 2, 3], "b": [4, 6, 6]})
+    y_train = pd.DataFrame({"a": [1, 2, 3], "b": [4, 6, 6]})
 
     objective = objective_class()
-    score = objective.score(y_true, y_predicted)
+    score = objective.score(y_true, y_predicted, y_train=y_train)
     assert isinstance(score, float)  # Output should be a float average
 
 
@@ -712,6 +714,34 @@ def test_mcc_catches_warnings():
         assert len(record) == 0
 
 
+def test_mase_time_series_model():
+    obj = MASE()
+
+    s1_actual = np.array([0, 1, 0, 1, 2, 0])
+    s1_predicted = np.array([0, 2, 2, 1, 3, 2])
+    s1_train = np.array([0, 0, 0, 0, 0, 0])
+
+    s2_actual = np.array([-1, -2, 1, 3])
+    s2_predicted = np.array([1, 2, -1, -3])
+    s2_train = np.array([-1, 2, 1, 1])
+
+    s3_actual = np.array([1, 2, 4, 2, 1, 2])
+    s3_predicted = np.array([0, 2, 2, 1, 3, 2])
+    s3_train = np.array([1, 1, 3, 2, 3, 1])
+
+    with pytest.raises(
+        ValueError,
+        match="Mean Absolute Scaled Error cannot be used when "
+        "all training targets contain the value 0.",
+    ):
+        obj.score(s1_actual, s1_predicted, s1_train)
+
+    assert obj.score(s2_actual, s2_predicted, s2_train) == pytest.approx(
+        (3 / 4) * (14 / 4),
+    )
+    assert obj.score(s3_actual, s3_predicted, s3_train) == pytest.approx(5 / 6)
+
+
 def test_mape_time_series_model():
     obj = MAPE()