From dff03ff687433228a9708d1cc7e3228337145e62 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Fri, 24 Jan 2025 04:25:51 +0000 Subject: [PATCH 01/10] manually port from shuowei-arima-plus branch, now I cannot pass format test --- bigframes/ml/base.py | 57 ++ bigframes/ml/core.py | 23 +- bigframes/ml/forecasting.py | 51 +- tests/data/time_series.jsonl | 1099 ++++++++++++++------- tests/data/time_series_schema.json | 5 + tests/system/large/ml/test_forecasting.py | 101 +- tests/system/small/ml/conftest.py | 18 + tests/system/small/ml/test_core.py | 32 +- tests/system/small/ml/test_forecasting.py | 320 ++++-- 9 files changed, 1234 insertions(+), 472 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index f06de99181..f6f97ad03b 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -165,6 +165,63 @@ def fit( return self._fit(X, y) +''' +class SupervisedTrainableWithIdColPredictor(SupervisedTrainablePredictor): + """Inherits from SupervisedTrainablePredictor, + but adds an optional id_col parameter to fit().""" + def __init__(self, id_col: Optional[utils.ArrayType]=None): + super().__init__() + self.id_col = id_col + self._bqml_model = None + + def _fit( + self, + X: utils.ArrayType, + y: utils.ArrayType, + transforms=None + ): + return self._fit(X, y, transforms=transforms, id_col=self.id_col) + + def score( + self, + X: utils.ArrayType, + y: utils.ArrayType + ): + return self.score(X, y) +''' + + +class TrainableWithIdColPredictor(TrainablePredictor): + """A BigQuery DataFrames ML Model base class that can be used to fit and predict outputs. + Additional id_col can be provided in the fit phase.""" + + @abc.abstractmethod + def _fit(self, X, y, transforms=None, id_col=None): + pass + + @abc.abstractmethod + def score(self, X, y): + pass + + +class SupervisedTrainableWithIdColPredictor(TrainableWithIdColPredictor): + """A BigQuery DataFrames ML Supervised Model base class that can be used to fit and predict outputs. + Need to provide both X and y in supervised tasks. + Additional id_col can be provided in the fit phase. + """ + + _T = TypeVar("_T", bound="SupervisedTrainableWithIdColPredictor") + + def fit( + self: _T, + X: utils.ArrayType, + y: utils.ArrayType, + id_col: Optional[utils.ArrayType] = None, + ) -> _T: + return self._fit(X, y, id_col=id_col) + + + class TrainableWithEvaluationPredictor(TrainablePredictor): """A BigQuery DataFrames ML Model base class that can be used to fit and predict outputs. diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index d038b8f4c0..b66e6e754b 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -181,15 +181,19 @@ def detect_anomalies( def forecast(self, options: Mapping[str, int | float]) -> bpd.DataFrame: sql = self._model_manipulation_sql_generator.ml_forecast(struct_options=options) - return self._session.read_gbq(sql, index_col="forecast_timestamp").reset_index() + index_cols = ["forecast_timestamp"] + if "id" in self._session.read_gbq(sql).columns: + index_cols.append("id") + return self._session.read_gbq(sql, index_col=index_cols).reset_index() def explain_forecast(self, options: Mapping[str, int | float]) -> bpd.DataFrame: sql = self._model_manipulation_sql_generator.ml_explain_forecast( struct_options=options ) - return self._session.read_gbq( - sql, index_col="time_series_timestamp" - ).reset_index() + index_cols = ["time_series_timestamp"] + if "id" in self._session.read_gbq(sql).columns: + index_cols.append("id") + return self._session.read_gbq(sql, index_col=index_cols).reset_index() def evaluate(self, input_data: Optional[bpd.DataFrame] = None): sql = self._model_manipulation_sql_generator.ml_evaluate( @@ -390,6 +394,7 @@ def create_time_series_model( self, X_train: bpd.DataFrame, y_train: bpd.DataFrame, + id_col: Optional[bpd.DataFrame] = None, transforms: Optional[Iterable[str]] = None, options: Mapping[str, Union[str, int, float, Iterable[str]]] = {}, ) -> BqmlModel: @@ -399,13 +404,21 @@ def create_time_series_model( assert ( y_train.columns.size == 1 ), "Time stamp data input must only contain 1 column." + assert id_col is None or ( + id_col is not None and id_col.columns.size == 1 + ), "Time series id input is either None or must only contain 1 column." options = dict(options) # Cache dataframes to make sure base table is not a snapshot # cached dataframe creates a full copy, never uses snapshot - input_data = X_train.join(y_train, how="outer").cache() + input_data = X_train.join(y_train, how="outer") + if id_col is not None: + input_data = input_data.join(id_col, how="outer") + input_data = input_data.cache() options.update({"TIME_SERIES_TIMESTAMP_COL": X_train.columns.tolist()[0]}) options.update({"TIME_SERIES_DATA_COL": y_train.columns.tolist()[0]}) + if id_col is not None: + options.update({"TIME_SERIES_ID_COL": id_col.columns.tolist()[0]}) session = X_train._session model_ref = self._create_model_ref(session._anonymous_dataset) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 4e6c5036e7..4f3cd799f0 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -45,7 +45,7 @@ @log_adapter.class_logger -class ARIMAPlus(base.SupervisedTrainablePredictor): +class ARIMAPlus(base.SupervisedTrainableWithIdColPredictor): """Time Series ARIMA Plus model. Args: @@ -183,37 +183,54 @@ def _fit( X: utils.ArrayType, y: utils.ArrayType, transforms: Optional[List[str]] = None, - ): + id_col: Optional[utils.ArrayType] = None, + ) -> ARIMAPlus: """Fit the model to training data. Args: - X (bigframes.dataframe.DataFrame or bigframes.series.Series): - A dataframe of training timestamp. - - y (bigframes.dataframe.DataFrame or bigframes.series.Series): + X (bigframes.dataframe.DataFrame, or bigframes.series.Series, + or pandas.core.frame.DataFrame or pandas.core.series.Series): + A dataframe or series of trainging timestamp. + y (bigframes.dataframe.DataFrame, or bigframes.series.Series, + or pandas.core.frame.DataFrame or pandas.core.series.Series): Target values for training. transforms (Optional[List[str]], default None): Do not use. Internal param to be deprecated. Use bigframes.ml.pipeline instead. + id_col (Optional[bigframes.dataframe.DataFrame] + or Optional[bigframes.series.Series], + or Optional[pandas.core.frame.DataFrame], + or Optional[pandas.core.frame.Series], default None): + An optional dataframe or series of training id col. Returns: ARIMAPlus: Fitted estimator. """ X, y = utils.batch_convert_to_dataframe(X, y) - if X.columns.size != 1: + if X.columns.size < 1: raise ValueError( - "Time series timestamp input X must only contain 1 column." + "Time series timestamp input X contain at least 1 column." ) if y.columns.size != 1: raise ValueError("Time series data input y must only contain 1 column.") + if id_col is not None: + (id_col,) = utils.batch_convert_to_dataframe(id_col) + + if id_col.columns.size != 1: + raise ValueError( + "Time series id input id_col must only contain 1 column." + ) + self._bqml_model = self._bqml_model_factory.create_time_series_model( X, y, + id_col=id_col, transforms=transforms, options=self._bqml_options, ) + return self def predict( self, X=None, *, horizon: int = 3, confidence_level: float = 0.95 @@ -237,7 +254,7 @@ def predict( Returns: bigframes.dataframe.DataFrame: The predicted DataFrames. Which - contains 2 columns: "forecast_timestamp" and "forecast_value". + contains 2 columns: "forecast_timestamp", "id" as optional, and "forecast_value". """ if horizon < 1 or horizon > 1000: raise ValueError(f"horizon must be [1, 1000], but is {horizon}.") @@ -356,12 +373,18 @@ def score( Args: X (bigframes.dataframe.DataFrame or bigframes.series.Series or pandas.core.frame.DataFrame or pandas.core.series.Series): - A BigQuery DataFrame only contains 1 column as + A dataframe or series only contains 1 column as evaluation timestamp. The timestamp must be within the horizon of the model, which by default is 1000 data points. y (bigframes.dataframe.DataFrame or bigframes.series.Series or pandas.core.frame.DataFrame or pandas.core.series.Series): - A BigQuery DataFrame only contains 1 column as + A dataframe or series only contains 1 column as evaluation numeric values. + id_col (Optional[bigframes.dataframe.DataFrame], + or Optional[bigframes.series.Series], + or Optional[pandas.core.frame.DataFrame], + or Optional[pandas.core.series.Series], default None): + An optional dataframe or series contains at least 1 column as + evualtion id column. Returns: bigframes.dataframe.DataFrame: A DataFrame as evaluation result. @@ -371,7 +394,11 @@ def score( X, y = utils.batch_convert_to_dataframe(X, y, session=self._bqml_model.session) input_data = X.join(y, how="outer") - return self._bqml_model.evaluate(input_data) + if id_col is not None: + (id_col,) = utils.batch_convert_to_dataframe(id_col) + input_data = input_data.join(id_col, how="outer") + + return self._bqml_model.evaluate(input_data) def summary( self, diff --git a/tests/data/time_series.jsonl b/tests/data/time_series.jsonl index e0f9ca7ae2..c6ea2a46ed 100644 --- a/tests/data/time_series.jsonl +++ b/tests/data/time_series.jsonl @@ -1,366 +1,733 @@ -{"parsed_date":"2017-07-01 00:00:00 UTC","total_visits":"2048"} -{"parsed_date":"2016-09-07 00:00:00 UTC","total_visits":"2562"} -{"parsed_date":"2016-10-25 00:00:00 UTC","total_visits":"3842"} -{"parsed_date":"2017-04-10 00:00:00 UTC","total_visits":"2563"} -{"parsed_date":"2017-01-09 00:00:00 UTC","total_visits":"2308"} -{"parsed_date":"2017-05-02 00:00:00 UTC","total_visits":"2564"} -{"parsed_date":"2016-11-11 00:00:00 UTC","total_visits":"3588"} -{"parsed_date":"2017-07-30 00:00:00 UTC","total_visits":"1799"} -{"parsed_date":"2017-06-10 00:00:00 UTC","total_visits":"1545"} -{"parsed_date":"2016-08-14 00:00:00 UTC","total_visits":"1801"} -{"parsed_date":"2017-05-14 00:00:00 UTC","total_visits":"1290"} -{"parsed_date":"2017-02-08 00:00:00 UTC","total_visits":"2570"} -{"parsed_date":"2017-06-01 00:00:00 UTC","total_visits":"2826"} -{"parsed_date":"2017-04-23 00:00:00 UTC","total_visits":"1548"} -{"parsed_date":"2016-11-04 00:00:00 UTC","total_visits":"3596"} -{"parsed_date":"2017-02-04 00:00:00 UTC","total_visits":"1549"} -{"parsed_date":"2016-12-09 00:00:00 UTC","total_visits":"2830"} -{"parsed_date":"2016-10-30 00:00:00 UTC","total_visits":"3086"} -{"parsed_date":"2017-03-28 00:00:00 UTC","total_visits":"2577"} -{"parsed_date":"2017-06-11 00:00:00 UTC","total_visits":"1555"} -{"parsed_date":"2016-12-17 00:00:00 UTC","total_visits":"2324"} -{"parsed_date":"2016-09-22 00:00:00 UTC","total_visits":"2581"} -{"parsed_date":"2017-01-29 00:00:00 UTC","total_visits":"1814"} -{"parsed_date":"2017-03-22 00:00:00 UTC","total_visits":"2582"} -{"parsed_date":"2017-02-21 00:00:00 UTC","total_visits":"2582"} -{"parsed_date":"2016-10-14 00:00:00 UTC","total_visits":"2838"} -{"parsed_date":"2017-04-27 00:00:00 UTC","total_visits":"2838"} -{"parsed_date":"2016-10-26 00:00:00 UTC","total_visits":"4375"} -{"parsed_date":"2016-08-22 00:00:00 UTC","total_visits":"2584"} -{"parsed_date":"2016-12-07 00:00:00 UTC","total_visits":"2840"} -{"parsed_date":"2017-01-20 00:00:00 UTC","total_visits":"2074"} -{"parsed_date":"2017-03-07 00:00:00 UTC","total_visits":"2586"} -{"parsed_date":"2017-05-16 00:00:00 UTC","total_visits":"3098"} -{"parsed_date":"2017-05-03 00:00:00 UTC","total_visits":"2588"} -{"parsed_date":"2017-05-01 00:00:00 UTC","total_visits":"2588"} -{"parsed_date":"2016-11-27 00:00:00 UTC","total_visits":"3356"} -{"parsed_date":"2017-04-29 00:00:00 UTC","total_visits":"1566"} -{"parsed_date":"2016-09-18 00:00:00 UTC","total_visits":"1822"} -{"parsed_date":"2017-03-23 00:00:00 UTC","total_visits":"2847"} -{"parsed_date":"2017-03-14 00:00:00 UTC","total_visits":"2338"} -{"parsed_date":"2016-12-21 00:00:00 UTC","total_visits":"2594"} -{"parsed_date":"2016-10-11 00:00:00 UTC","total_visits":"2850"} -{"parsed_date":"2017-01-24 00:00:00 UTC","total_visits":"3618"} -{"parsed_date":"2017-03-05 00:00:00 UTC","total_visits":"1827"} -{"parsed_date":"2017-01-19 00:00:00 UTC","total_visits":"2083"} -{"parsed_date":"2016-08-09 00:00:00 UTC","total_visits":"2851"} -{"parsed_date":"2017-04-08 00:00:00 UTC","total_visits":"1829"} -{"parsed_date":"2017-04-12 00:00:00 UTC","total_visits":"2341"} -{"parsed_date":"2016-09-29 00:00:00 UTC","total_visits":"2597"} -{"parsed_date":"2016-12-20 00:00:00 UTC","total_visits":"3110"} -{"parsed_date":"2017-01-15 00:00:00 UTC","total_visits":"1576"} -{"parsed_date":"2017-04-14 00:00:00 UTC","total_visits":"1834"} -{"parsed_date":"2017-02-28 00:00:00 UTC","total_visits":"2347"} -{"parsed_date":"2016-09-16 00:00:00 UTC","total_visits":"2603"} -{"parsed_date":"2016-10-18 00:00:00 UTC","total_visits":"3628"} -{"parsed_date":"2017-02-24 00:00:00 UTC","total_visits":"2093"} -{"parsed_date":"2017-05-17 00:00:00 UTC","total_visits":"3117"} -{"parsed_date":"2017-06-23 00:00:00 UTC","total_visits":"2095"} -{"parsed_date":"2016-11-12 00:00:00 UTC","total_visits":"3119"} -{"parsed_date":"2016-11-21 00:00:00 UTC","total_visits":"4143"} -{"parsed_date":"2017-02-27 00:00:00 UTC","total_visits":"2352"} -{"parsed_date":"2016-12-26 00:00:00 UTC","total_visits":"1586"} -{"parsed_date":"2017-04-25 00:00:00 UTC","total_visits":"2354"} -{"parsed_date":"2017-03-21 00:00:00 UTC","total_visits":"2611"} -{"parsed_date":"2016-12-22 00:00:00 UTC","total_visits":"2100"} -{"parsed_date":"2016-10-01 00:00:00 UTC","total_visits":"1589"} -{"parsed_date":"2016-09-24 00:00:00 UTC","total_visits":"1845"} -{"parsed_date":"2017-06-21 00:00:00 UTC","total_visits":"2357"} -{"parsed_date":"2016-09-02 00:00:00 UTC","total_visits":"2613"} -{"parsed_date":"2016-08-26 00:00:00 UTC","total_visits":"2359"} -{"parsed_date":"2016-10-12 00:00:00 UTC","total_visits":"2871"} -{"parsed_date":"2017-05-15 00:00:00 UTC","total_visits":"2360"} -{"parsed_date":"2017-06-12 00:00:00 UTC","total_visits":"2361"} -{"parsed_date":"2016-08-16 00:00:00 UTC","total_visits":"2873"} -{"parsed_date":"2017-04-30 00:00:00 UTC","total_visits":"1594"} -{"parsed_date":"2017-04-05 00:00:00 UTC","total_visits":"2619"} -{"parsed_date":"2016-08-12 00:00:00 UTC","total_visits":"2619"} -{"parsed_date":"2016-11-08 00:00:00 UTC","total_visits":"3899"} -{"parsed_date":"2016-08-13 00:00:00 UTC","total_visits":"1596"} -{"parsed_date":"2017-05-09 00:00:00 UTC","total_visits":"2108"} -{"parsed_date":"2017-02-23 00:00:00 UTC","total_visits":"2364"} -{"parsed_date":"2017-07-31 00:00:00 UTC","total_visits":"2620"} -{"parsed_date":"2017-06-25 00:00:00 UTC","total_visits":"1597"} -{"parsed_date":"2017-07-29 00:00:00 UTC","total_visits":"1597"} -{"parsed_date":"2016-09-17 00:00:00 UTC","total_visits":"1853"} -{"parsed_date":"2016-12-27 00:00:00 UTC","total_visits":"1855"} -{"parsed_date":"2017-05-20 00:00:00 UTC","total_visits":"1855"} -{"parsed_date":"2016-10-08 00:00:00 UTC","total_visits":"2114"} -{"parsed_date":"2016-10-27 00:00:00 UTC","total_visits":"4162"} -{"parsed_date":"2017-07-08 00:00:00 UTC","total_visits":"1859"} -{"parsed_date":"2016-08-24 00:00:00 UTC","total_visits":"2627"} -{"parsed_date":"2016-12-23 00:00:00 UTC","total_visits":"1604"} -{"parsed_date":"2017-02-02 00:00:00 UTC","total_visits":"2372"} -{"parsed_date":"2016-09-08 00:00:00 UTC","total_visits":"2628"} -{"parsed_date":"2017-04-02 00:00:00 UTC","total_visits":"1861"} -{"parsed_date":"2017-02-15 00:00:00 UTC","total_visits":"2629"} -{"parsed_date":"2017-07-05 00:00:00 UTC","total_visits":"2885"} -{"parsed_date":"2016-10-17 00:00:00 UTC","total_visits":"3397"} -{"parsed_date":"2017-02-20 00:00:00 UTC","total_visits":"2374"} -{"parsed_date":"2017-03-24 00:00:00 UTC","total_visits":"2374"} -{"parsed_date":"2017-04-20 00:00:00 UTC","total_visits":"2374"} -{"parsed_date":"2016-11-18 00:00:00 UTC","total_visits":"3654"} -{"parsed_date":"2017-07-25 00:00:00 UTC","total_visits":"2631"} -{"parsed_date":"2016-11-13 00:00:00 UTC","total_visits":"3144"} -{"parsed_date":"2017-03-18 00:00:00 UTC","total_visits":"1610"} -{"parsed_date":"2016-08-03 00:00:00 UTC","total_visits":"2890"} -{"parsed_date":"2016-08-19 00:00:00 UTC","total_visits":"2379"} -{"parsed_date":"2017-02-14 00:00:00 UTC","total_visits":"2379"} -{"parsed_date":"2017-07-11 00:00:00 UTC","total_visits":"2635"} -{"parsed_date":"2017-04-22 00:00:00 UTC","total_visits":"1612"} -{"parsed_date":"2016-10-07 00:00:00 UTC","total_visits":"2892"} -{"parsed_date":"2016-09-05 00:00:00 UTC","total_visits":"2125"} -{"parsed_date":"2016-09-23 00:00:00 UTC","total_visits":"2381"} -{"parsed_date":"2016-11-15 00:00:00 UTC","total_visits":"4685"} -{"parsed_date":"2017-01-28 00:00:00 UTC","total_visits":"1614"} -{"parsed_date":"2017-07-14 00:00:00 UTC","total_visits":"2382"} -{"parsed_date":"2017-01-07 00:00:00 UTC","total_visits":"1615"} -{"parsed_date":"2017-04-03 00:00:00 UTC","total_visits":"2383"} -{"parsed_date":"2017-03-20 00:00:00 UTC","total_visits":"2383"} -{"parsed_date":"2016-12-18 00:00:00 UTC","total_visits":"2128"} -{"parsed_date":"2017-03-17 00:00:00 UTC","total_visits":"2129"} -{"parsed_date":"2017-05-23 00:00:00 UTC","total_visits":"2129"} -{"parsed_date":"2016-11-30 00:00:00 UTC","total_visits":"4435"} -{"parsed_date":"2017-01-01 00:00:00 UTC","total_visits":"1364"} -{"parsed_date":"2017-01-02 00:00:00 UTC","total_visits":"1620"} -{"parsed_date":"2016-09-25 00:00:00 UTC","total_visits":"1877"} -{"parsed_date":"2016-08-07 00:00:00 UTC","total_visits":"1622"} -{"parsed_date":"2016-10-09 00:00:00 UTC","total_visits":"2134"} -{"parsed_date":"2017-03-01 00:00:00 UTC","total_visits":"2390"} -{"parsed_date":"2017-01-04 00:00:00 UTC","total_visits":"2390"} -{"parsed_date":"2017-06-06 00:00:00 UTC","total_visits":"2391"} -{"parsed_date":"2017-04-18 00:00:00 UTC","total_visits":"2391"} -{"parsed_date":"2017-04-06 00:00:00 UTC","total_visits":"2647"} -{"parsed_date":"2017-01-30 00:00:00 UTC","total_visits":"2392"} -{"parsed_date":"2016-10-16 00:00:00 UTC","total_visits":"2649"} -{"parsed_date":"2016-08-04 00:00:00 UTC","total_visits":"3161"} -{"parsed_date":"2016-10-21 00:00:00 UTC","total_visits":"3419"} -{"parsed_date":"2016-08-02 00:00:00 UTC","total_visits":"2140"} -{"parsed_date":"2017-03-06 00:00:00 UTC","total_visits":"2396"} -{"parsed_date":"2016-09-13 00:00:00 UTC","total_visits":"2396"} -{"parsed_date":"2016-09-14 00:00:00 UTC","total_visits":"2652"} -{"parsed_date":"2017-04-19 00:00:00 UTC","total_visits":"2397"} -{"parsed_date":"2017-06-19 00:00:00 UTC","total_visits":"2142"} -{"parsed_date":"2016-12-13 00:00:00 UTC","total_visits":"3166"} -{"parsed_date":"2017-06-20 00:00:00 UTC","total_visits":"2143"} -{"parsed_date":"2016-10-10 00:00:00 UTC","total_visits":"2911"} -{"parsed_date":"2017-07-06 00:00:00 UTC","total_visits":"2658"} -{"parsed_date":"2017-01-03 00:00:00 UTC","total_visits":"2403"} -{"parsed_date":"2017-01-08 00:00:00 UTC","total_visits":"1637"} -{"parsed_date":"2017-02-25 00:00:00 UTC","total_visits":"1638"} -{"parsed_date":"2017-05-24 00:00:00 UTC","total_visits":"2406"} -{"parsed_date":"2016-11-22 00:00:00 UTC","total_visits":"3942"} -{"parsed_date":"2017-05-06 00:00:00 UTC","total_visits":"1383"} -{"parsed_date":"2017-07-02 00:00:00 UTC","total_visits":"1895"} -{"parsed_date":"2016-12-01 00:00:00 UTC","total_visits":"4200"} -{"parsed_date":"2017-03-16 00:00:00 UTC","total_visits":"2409"} -{"parsed_date":"2016-12-12 00:00:00 UTC","total_visits":"3433"} -{"parsed_date":"2016-12-25 00:00:00 UTC","total_visits":"1386"} -{"parsed_date":"2017-02-26 00:00:00 UTC","total_visits":"1643"} -{"parsed_date":"2017-04-28 00:00:00 UTC","total_visits":"2411"} -{"parsed_date":"2016-08-11 00:00:00 UTC","total_visits":"2667"} -{"parsed_date":"2017-07-20 00:00:00 UTC","total_visits":"2668"} -{"parsed_date":"2017-05-21 00:00:00 UTC","total_visits":"1645"} -{"parsed_date":"2017-06-17 00:00:00 UTC","total_visits":"1391"} -{"parsed_date":"2016-12-29 00:00:00 UTC","total_visits":"1647"} -{"parsed_date":"2017-07-17 00:00:00 UTC","total_visits":"2671"} -{"parsed_date":"2017-01-16 00:00:00 UTC","total_visits":"1906"} -{"parsed_date":"2017-03-03 00:00:00 UTC","total_visits":"2162"} -{"parsed_date":"2016-11-14 00:00:00 UTC","total_visits":"4466"} -{"parsed_date":"2016-08-30 00:00:00 UTC","total_visits":"2675"} -{"parsed_date":"2016-08-27 00:00:00 UTC","total_visits":"1654"} -{"parsed_date":"2017-02-09 00:00:00 UTC","total_visits":"2678"} -{"parsed_date":"2017-06-03 00:00:00 UTC","total_visits":"1399"} -{"parsed_date":"2017-05-07 00:00:00 UTC","total_visits":"1400"} -{"parsed_date":"2016-11-02 00:00:00 UTC","total_visits":"3960"} -{"parsed_date":"2016-12-15 00:00:00 UTC","total_visits":"2937"} -{"parsed_date":"2017-04-01 00:00:00 UTC","total_visits":"2170"} -{"parsed_date":"2017-07-21 00:00:00 UTC","total_visits":"2427"} -{"parsed_date":"2016-08-06 00:00:00 UTC","total_visits":"1663"} -{"parsed_date":"2016-09-01 00:00:00 UTC","total_visits":"2687"} -{"parsed_date":"2017-06-28 00:00:00 UTC","total_visits":"2687"} -{"parsed_date":"2016-08-20 00:00:00 UTC","total_visits":"1664"} -{"parsed_date":"2017-04-26 00:00:00 UTC","total_visits":"4224"} -{"parsed_date":"2017-07-09 00:00:00 UTC","total_visits":"1921"} -{"parsed_date":"2017-07-28 00:00:00 UTC","total_visits":"2433"} -{"parsed_date":"2016-09-19 00:00:00 UTC","total_visits":"2689"} -{"parsed_date":"2017-07-24 00:00:00 UTC","total_visits":"2436"} -{"parsed_date":"2017-06-13 00:00:00 UTC","total_visits":"2181"} -{"parsed_date":"2016-09-15 00:00:00 UTC","total_visits":"2949"} -{"parsed_date":"2017-02-03 00:00:00 UTC","total_visits":"2182"} -{"parsed_date":"2016-09-10 00:00:00 UTC","total_visits":"1671"} -{"parsed_date":"2017-06-09 00:00:00 UTC","total_visits":"1927"} -{"parsed_date":"2017-01-11 00:00:00 UTC","total_visits":"2185"} -{"parsed_date":"2017-02-19 00:00:00 UTC","total_visits":"2187"} -{"parsed_date":"2017-01-17 00:00:00 UTC","total_visits":"2443"} -{"parsed_date":"2017-05-12 00:00:00 UTC","total_visits":"1932"} -{"parsed_date":"2016-12-16 00:00:00 UTC","total_visits":"2956"} -{"parsed_date":"2017-02-01 00:00:00 UTC","total_visits":"2445"} -{"parsed_date":"2016-11-26 00:00:00 UTC","total_visits":"3213"} -{"parsed_date":"2017-06-02 00:00:00 UTC","total_visits":"2190"} -{"parsed_date":"2016-08-05 00:00:00 UTC","total_visits":"2702"} -{"parsed_date":"2016-11-01 00:00:00 UTC","total_visits":"3728"} -{"parsed_date":"2017-01-05 00:00:00 UTC","total_visits":"2193"} -{"parsed_date":"2017-03-08 00:00:00 UTC","total_visits":"2449"} -{"parsed_date":"2016-08-28 00:00:00 UTC","total_visits":"1682"} -{"parsed_date":"2017-07-04 00:00:00 UTC","total_visits":"1938"} -{"parsed_date":"2017-03-10 00:00:00 UTC","total_visits":"2194"} -{"parsed_date":"2017-07-07 00:00:00 UTC","total_visits":"2450"} -{"parsed_date":"2016-10-29 00:00:00 UTC","total_visits":"2964"} -{"parsed_date":"2016-10-13 00:00:00 UTC","total_visits":"2964"} -{"parsed_date":"2016-12-04 00:00:00 UTC","total_visits":"3220"} -{"parsed_date":"2017-01-21 00:00:00 UTC","total_visits":"1685"} -{"parsed_date":"2017-06-29 00:00:00 UTC","total_visits":"2709"} -{"parsed_date":"2016-08-29 00:00:00 UTC","total_visits":"2454"} -{"parsed_date":"2016-12-19 00:00:00 UTC","total_visits":"3222"} -{"parsed_date":"2017-05-30 00:00:00 UTC","total_visits":"2199"} -{"parsed_date":"2017-02-10 00:00:00 UTC","total_visits":"2199"} -{"parsed_date":"2016-08-31 00:00:00 UTC","total_visits":"3223"} -{"parsed_date":"2017-06-18 00:00:00 UTC","total_visits":"1432"} -{"parsed_date":"2017-01-12 00:00:00 UTC","total_visits":"2203"} -{"parsed_date":"2017-05-18 00:00:00 UTC","total_visits":"2715"} -{"parsed_date":"2016-10-23 00:00:00 UTC","total_visits":"2971"} -{"parsed_date":"2016-09-04 00:00:00 UTC","total_visits":"1692"} -{"parsed_date":"2016-12-10 00:00:00 UTC","total_visits":"2207"} -{"parsed_date":"2016-12-11 00:00:00 UTC","total_visits":"2208"} -{"parsed_date":"2017-04-11 00:00:00 UTC","total_visits":"2464"} -{"parsed_date":"2016-09-21 00:00:00 UTC","total_visits":"2720"} -{"parsed_date":"2016-11-06 00:00:00 UTC","total_visits":"3232"} -{"parsed_date":"2017-01-26 00:00:00 UTC","total_visits":"2209"} -{"parsed_date":"2016-09-12 00:00:00 UTC","total_visits":"2465"} -{"parsed_date":"2017-04-21 00:00:00 UTC","total_visits":"2210"} -{"parsed_date":"2017-01-06 00:00:00 UTC","total_visits":"2210"} -{"parsed_date":"2017-04-04 00:00:00 UTC","total_visits":"2978"} -{"parsed_date":"2017-01-22 00:00:00 UTC","total_visits":"1700"} -{"parsed_date":"2017-07-26 00:00:00 UTC","total_visits":"2725"} -{"parsed_date":"2016-08-18 00:00:00 UTC","total_visits":"2725"} -{"parsed_date":"2016-09-27 00:00:00 UTC","total_visits":"2727"} -{"parsed_date":"2016-12-02 00:00:00 UTC","total_visits":"3751"} -{"parsed_date":"2017-05-05 00:00:00 UTC","total_visits":"1960"} -{"parsed_date":"2016-11-19 00:00:00 UTC","total_visits":"2984"} -{"parsed_date":"2016-11-09 00:00:00 UTC","total_visits":"3752"} -{"parsed_date":"2016-12-05 00:00:00 UTC","total_visits":"4265"} -{"parsed_date":"2017-05-11 00:00:00 UTC","total_visits":"2218"} -{"parsed_date":"2017-01-25 00:00:00 UTC","total_visits":"2986"} -{"parsed_date":"2017-03-11 00:00:00 UTC","total_visits":"1707"} -{"parsed_date":"2017-03-30 00:00:00 UTC","total_visits":"2731"} -{"parsed_date":"2016-10-20 00:00:00 UTC","total_visits":"3755"} -{"parsed_date":"2017-02-07 00:00:00 UTC","total_visits":"2476"} -{"parsed_date":"2017-02-22 00:00:00 UTC","total_visits":"2477"} -{"parsed_date":"2017-07-23 00:00:00 UTC","total_visits":"1966"} -{"parsed_date":"2016-11-03 00:00:00 UTC","total_visits":"4014"} -{"parsed_date":"2016-08-01 00:00:00 UTC","total_visits":"1711"} -{"parsed_date":"2017-01-13 00:00:00 UTC","total_visits":"1967"} -{"parsed_date":"2017-05-19 00:00:00 UTC","total_visits":"2223"} -{"parsed_date":"2016-11-20 00:00:00 UTC","total_visits":"3247"} -{"parsed_date":"2016-11-25 00:00:00 UTC","total_visits":"3759"} -{"parsed_date":"2017-03-25 00:00:00 UTC","total_visits":"1712"} -{"parsed_date":"2017-01-27 00:00:00 UTC","total_visits":"1969"} -{"parsed_date":"2017-06-26 00:00:00 UTC","total_visits":"2226"} -{"parsed_date":"2017-05-25 00:00:00 UTC","total_visits":"2228"} -{"parsed_date":"2017-01-31 00:00:00 UTC","total_visits":"2229"} -{"parsed_date":"2017-07-13 00:00:00 UTC","total_visits":"2741"} -{"parsed_date":"2017-03-15 00:00:00 UTC","total_visits":"2486"} -{"parsed_date":"2017-05-28 00:00:00 UTC","total_visits":"1463"} -{"parsed_date":"2017-03-09 00:00:00 UTC","total_visits":"2231"} -{"parsed_date":"2017-07-15 00:00:00 UTC","total_visits":"1721"} -{"parsed_date":"2016-11-24 00:00:00 UTC","total_visits":"3770"} -{"parsed_date":"2016-10-05 00:00:00 UTC","total_visits":"3770"} -{"parsed_date":"2016-12-31 00:00:00 UTC","total_visits":"1211"} -{"parsed_date":"2016-10-02 00:00:00 UTC","total_visits":"1724"} -{"parsed_date":"2017-07-22 00:00:00 UTC","total_visits":"1724"} -{"parsed_date":"2016-09-11 00:00:00 UTC","total_visits":"1725"} -{"parsed_date":"2017-06-15 00:00:00 UTC","total_visits":"2237"} -{"parsed_date":"2017-06-05 00:00:00 UTC","total_visits":"2493"} -{"parsed_date":"2017-02-06 00:00:00 UTC","total_visits":"2238"} -{"parsed_date":"2016-10-15 00:00:00 UTC","total_visits":"2495"} -{"parsed_date":"2016-08-21 00:00:00 UTC","total_visits":"1730"} -{"parsed_date":"2016-08-23 00:00:00 UTC","total_visits":"2754"} -{"parsed_date":"2017-06-30 00:00:00 UTC","total_visits":"2499"} -{"parsed_date":"2017-01-18 00:00:00 UTC","total_visits":"2245"} -{"parsed_date":"2016-08-10 00:00:00 UTC","total_visits":"2757"} -{"parsed_date":"2016-12-08 00:00:00 UTC","total_visits":"3013"} -{"parsed_date":"2016-11-28 00:00:00 UTC","total_visits":"4807"} -{"parsed_date":"2017-05-22 00:00:00 UTC","total_visits":"2248"} -{"parsed_date":"2016-09-20 00:00:00 UTC","total_visits":"2760"} -{"parsed_date":"2016-10-06 00:00:00 UTC","total_visits":"3016"} -{"parsed_date":"2016-09-06 00:00:00 UTC","total_visits":"2508"} -{"parsed_date":"2016-09-03 00:00:00 UTC","total_visits":"1741"} -{"parsed_date":"2016-12-06 00:00:00 UTC","total_visits":"3021"} -{"parsed_date":"2016-12-24 00:00:00 UTC","total_visits":"1231"} -{"parsed_date":"2016-10-28 00:00:00 UTC","total_visits":"3791"} -{"parsed_date":"2016-12-30 00:00:00 UTC","total_visits":"1232"} -{"parsed_date":"2017-05-29 00:00:00 UTC","total_visits":"1745"} -{"parsed_date":"2017-07-10 00:00:00 UTC","total_visits":"2769"} -{"parsed_date":"2017-06-22 00:00:00 UTC","total_visits":"2258"} -{"parsed_date":"2017-07-19 00:00:00 UTC","total_visits":"2514"} -{"parsed_date":"2016-10-03 00:00:00 UTC","total_visits":"2514"} -{"parsed_date":"2017-06-14 00:00:00 UTC","total_visits":"2517"} -{"parsed_date":"2016-10-22 00:00:00 UTC","total_visits":"3029"} -{"parsed_date":"2017-01-23 00:00:00 UTC","total_visits":"2262"} -{"parsed_date":"2017-04-24 00:00:00 UTC","total_visits":"2263"} -{"parsed_date":"2016-11-10 00:00:00 UTC","total_visits":"4055"} -{"parsed_date":"2016-09-26 00:00:00 UTC","total_visits":"2776"} -{"parsed_date":"2016-10-19 00:00:00 UTC","total_visits":"3544"} -{"parsed_date":"2017-03-04 00:00:00 UTC","total_visits":"1753"} -{"parsed_date":"2017-05-26 00:00:00 UTC","total_visits":"2009"} -{"parsed_date":"2017-02-13 00:00:00 UTC","total_visits":"2266"} -{"parsed_date":"2017-02-18 00:00:00 UTC","total_visits":"1755"} -{"parsed_date":"2017-03-02 00:00:00 UTC","total_visits":"2267"} -{"parsed_date":"2017-03-31 00:00:00 UTC","total_visits":"2268"} -{"parsed_date":"2017-01-10 00:00:00 UTC","total_visits":"2268"} -{"parsed_date":"2017-03-29 00:00:00 UTC","total_visits":"2525"} -{"parsed_date":"2017-03-27 00:00:00 UTC","total_visits":"2525"} -{"parsed_date":"2016-11-23 00:00:00 UTC","total_visits":"3805"} -{"parsed_date":"2017-05-27 00:00:00 UTC","total_visits":"1502"} -{"parsed_date":"2016-10-24 00:00:00 UTC","total_visits":"4063"} -{"parsed_date":"2016-12-14 00:00:00 UTC","total_visits":"3040"} -{"parsed_date":"2017-02-11 00:00:00 UTC","total_visits":"1761"} -{"parsed_date":"2017-07-27 00:00:00 UTC","total_visits":"2529"} -{"parsed_date":"2017-02-17 00:00:00 UTC","total_visits":"2785"} -{"parsed_date":"2017-04-15 00:00:00 UTC","total_visits":"1506"} -{"parsed_date":"2016-11-05 00:00:00 UTC","total_visits":"3042"} -{"parsed_date":"2016-10-04 00:00:00 UTC","total_visits":"4322"} -{"parsed_date":"2017-05-13 00:00:00 UTC","total_visits":"1251"} -{"parsed_date":"2017-04-16 00:00:00 UTC","total_visits":"1507"} -{"parsed_date":"2016-12-28 00:00:00 UTC","total_visits":"1763"} -{"parsed_date":"2016-08-15 00:00:00 UTC","total_visits":"3043"} -{"parsed_date":"2016-12-03 00:00:00 UTC","total_visits":"3044"} -{"parsed_date":"2017-06-27 00:00:00 UTC","total_visits":"2789"} -{"parsed_date":"2017-06-24 00:00:00 UTC","total_visits":"1510"} -{"parsed_date":"2017-07-16 00:00:00 UTC","total_visits":"1766"} -{"parsed_date":"2017-04-09 00:00:00 UTC","total_visits":"1766"} -{"parsed_date":"2017-06-07 00:00:00 UTC","total_visits":"2279"} -{"parsed_date":"2017-04-17 00:00:00 UTC","total_visits":"2279"} -{"parsed_date":"2016-09-28 00:00:00 UTC","total_visits":"2535"} -{"parsed_date":"2017-03-26 00:00:00 UTC","total_visits":"1768"} -{"parsed_date":"2017-05-10 00:00:00 UTC","total_visits":"2024"} -{"parsed_date":"2017-06-08 00:00:00 UTC","total_visits":"2280"} -{"parsed_date":"2017-05-08 00:00:00 UTC","total_visits":"2025"} -{"parsed_date":"2017-03-13 00:00:00 UTC","total_visits":"2537"} -{"parsed_date":"2016-11-17 00:00:00 UTC","total_visits":"4074"} -{"parsed_date":"2016-08-25 00:00:00 UTC","total_visits":"2539"} -{"parsed_date":"2017-02-16 00:00:00 UTC","total_visits":"2539"} -{"parsed_date":"2017-06-16 00:00:00 UTC","total_visits":"2028"} -{"parsed_date":"2016-11-16 00:00:00 UTC","total_visits":"4334"} -{"parsed_date":"2016-08-17 00:00:00 UTC","total_visits":"2799"} -{"parsed_date":"2017-03-19 00:00:00 UTC","total_visits":"1776"} -{"parsed_date":"2016-11-29 00:00:00 UTC","total_visits":"4337"} -{"parsed_date":"2017-02-05 00:00:00 UTC","total_visits":"1522"} -{"parsed_date":"2016-10-31 00:00:00 UTC","total_visits":"3827"} -{"parsed_date":"2017-05-31 00:00:00 UTC","total_visits":"2292"} -{"parsed_date":"2017-07-18 00:00:00 UTC","total_visits":"2804"} -{"parsed_date":"2017-03-12 00:00:00 UTC","total_visits":"1781"} -{"parsed_date":"2016-09-09 00:00:00 UTC","total_visits":"2549"} -{"parsed_date":"2017-01-14 00:00:00 UTC","total_visits":"1526"} -{"parsed_date":"2017-05-04 00:00:00 UTC","total_visits":"2806"} -{"parsed_date":"2016-11-07 00:00:00 UTC","total_visits":"3832"} -{"parsed_date":"2017-04-07 00:00:00 UTC","total_visits":"2297"} -{"parsed_date":"2017-07-12 00:00:00 UTC","total_visits":"2554"} -{"parsed_date":"2017-04-13 00:00:00 UTC","total_visits":"2300"} -{"parsed_date":"2017-08-01 00:00:00 UTC","total_visits":"2556"} -{"parsed_date":"2017-06-04 00:00:00 UTC","total_visits":"1534"} -{"parsed_date":"2017-02-12 00:00:00 UTC","total_visits":"1790"} -{"parsed_date":"2017-07-03 00:00:00 UTC","total_visits":"2046"} -{"parsed_date":"2016-09-30 00:00:00 UTC","total_visits":"2303"} -{"parsed_date":"2016-08-08 00:00:00 UTC","total_visits":"2815"} +{"parsed_date":"2017-07-01 00:00:00 UTC","id":"1","total_visits":"2048"} +{"parsed_date":"2016-09-07 00:00:00 UTC","id":"1","total_visits":"2562"} +{"parsed_date":"2016-10-25 00:00:00 UTC","id":"1","total_visits":"3842"} +{"parsed_date":"2017-04-10 00:00:00 UTC","id":"1","total_visits":"2563"} +{"parsed_date":"2017-01-09 00:00:00 UTC","id":"1","total_visits":"2308"} +{"parsed_date":"2017-05-02 00:00:00 UTC","id":"1","total_visits":"2564"} +{"parsed_date":"2016-11-11 00:00:00 UTC","id":"1","total_visits":"3588"} +{"parsed_date":"2017-07-30 00:00:00 UTC","id":"1","total_visits":"1799"} +{"parsed_date":"2017-06-10 00:00:00 UTC","id":"1","total_visits":"1545"} +{"parsed_date":"2016-08-14 00:00:00 UTC","id":"1","total_visits":"1801"} +{"parsed_date":"2017-05-14 00:00:00 UTC","id":"1","total_visits":"1290"} +{"parsed_date":"2017-02-08 00:00:00 UTC","id":"1","total_visits":"2570"} +{"parsed_date":"2017-06-01 00:00:00 UTC","id":"1","total_visits":"2826"} +{"parsed_date":"2017-04-23 00:00:00 UTC","id":"1","total_visits":"1548"} +{"parsed_date":"2016-11-04 00:00:00 UTC","id":"1","total_visits":"3596"} +{"parsed_date":"2017-02-04 00:00:00 UTC","id":"1","total_visits":"1549"} +{"parsed_date":"2016-12-09 00:00:00 UTC","id":"1","total_visits":"2830"} +{"parsed_date":"2016-10-30 00:00:00 UTC","id":"1","total_visits":"3086"} +{"parsed_date":"2017-03-28 00:00:00 UTC","id":"1","total_visits":"2577"} +{"parsed_date":"2017-06-11 00:00:00 UTC","id":"1","total_visits":"1555"} +{"parsed_date":"2016-12-17 00:00:00 UTC","id":"1","total_visits":"2324"} +{"parsed_date":"2016-09-22 00:00:00 UTC","id":"1","total_visits":"2581"} +{"parsed_date":"2017-01-29 00:00:00 UTC","id":"1","total_visits":"1814"} +{"parsed_date":"2017-03-22 00:00:00 UTC","id":"1","total_visits":"2582"} +{"parsed_date":"2017-02-21 00:00:00 UTC","id":"1","total_visits":"2582"} +{"parsed_date":"2016-10-14 00:00:00 UTC","id":"1","total_visits":"2838"} +{"parsed_date":"2017-04-27 00:00:00 UTC","id":"1","total_visits":"2838"} +{"parsed_date":"2016-10-26 00:00:00 UTC","id":"1","total_visits":"4375"} +{"parsed_date":"2016-08-22 00:00:00 UTC","id":"1","total_visits":"2584"} +{"parsed_date":"2016-12-07 00:00:00 UTC","id":"1","total_visits":"2840"} +{"parsed_date":"2017-01-20 00:00:00 UTC","id":"1","total_visits":"2074"} +{"parsed_date":"2017-03-07 00:00:00 UTC","id":"1","total_visits":"2586"} +{"parsed_date":"2017-05-16 00:00:00 UTC","id":"1","total_visits":"3098"} +{"parsed_date":"2017-05-03 00:00:00 UTC","id":"1","total_visits":"2588"} +{"parsed_date":"2017-05-01 00:00:00 UTC","id":"1","total_visits":"2588"} +{"parsed_date":"2016-11-27 00:00:00 UTC","id":"1","total_visits":"3356"} +{"parsed_date":"2017-04-29 00:00:00 UTC","id":"1","total_visits":"1566"} +{"parsed_date":"2016-09-18 00:00:00 UTC","id":"1","total_visits":"1822"} +{"parsed_date":"2017-03-23 00:00:00 UTC","id":"1","total_visits":"2847"} +{"parsed_date":"2017-03-14 00:00:00 UTC","id":"1","total_visits":"2338"} +{"parsed_date":"2016-12-21 00:00:00 UTC","id":"1","total_visits":"2594"} +{"parsed_date":"2016-10-11 00:00:00 UTC","id":"1","total_visits":"2850"} +{"parsed_date":"2017-01-24 00:00:00 UTC","id":"1","total_visits":"3618"} +{"parsed_date":"2017-03-05 00:00:00 UTC","id":"1","total_visits":"1827"} +{"parsed_date":"2017-01-19 00:00:00 UTC","id":"1","total_visits":"2083"} +{"parsed_date":"2016-08-09 00:00:00 UTC","id":"1","total_visits":"2851"} +{"parsed_date":"2017-04-08 00:00:00 UTC","id":"1","total_visits":"1829"} +{"parsed_date":"2017-04-12 00:00:00 UTC","id":"1","total_visits":"2341"} +{"parsed_date":"2016-09-29 00:00:00 UTC","id":"1","total_visits":"2597"} +{"parsed_date":"2016-12-20 00:00:00 UTC","id":"1","total_visits":"3110"} +{"parsed_date":"2017-01-15 00:00:00 UTC","id":"1","total_visits":"1576"} +{"parsed_date":"2017-04-14 00:00:00 UTC","id":"1","total_visits":"1834"} +{"parsed_date":"2017-02-28 00:00:00 UTC","id":"1","total_visits":"2347"} +{"parsed_date":"2016-09-16 00:00:00 UTC","id":"1","total_visits":"2603"} +{"parsed_date":"2016-10-18 00:00:00 UTC","id":"1","total_visits":"3628"} +{"parsed_date":"2017-02-24 00:00:00 UTC","id":"1","total_visits":"2093"} +{"parsed_date":"2017-05-17 00:00:00 UTC","id":"1","total_visits":"3117"} +{"parsed_date":"2017-06-23 00:00:00 UTC","id":"1","total_visits":"2095"} +{"parsed_date":"2016-11-12 00:00:00 UTC","id":"1","total_visits":"3119"} +{"parsed_date":"2016-11-21 00:00:00 UTC","id":"1","total_visits":"4143"} +{"parsed_date":"2017-02-27 00:00:00 UTC","id":"1","total_visits":"2352"} +{"parsed_date":"2016-12-26 00:00:00 UTC","id":"1","total_visits":"1586"} +{"parsed_date":"2017-04-25 00:00:00 UTC","id":"1","total_visits":"2354"} +{"parsed_date":"2017-03-21 00:00:00 UTC","id":"1","total_visits":"2611"} +{"parsed_date":"2016-12-22 00:00:00 UTC","id":"1","total_visits":"2100"} +{"parsed_date":"2016-10-01 00:00:00 UTC","id":"1","total_visits":"1589"} +{"parsed_date":"2016-09-24 00:00:00 UTC","id":"1","total_visits":"1845"} +{"parsed_date":"2017-06-21 00:00:00 UTC","id":"1","total_visits":"2357"} +{"parsed_date":"2016-09-02 00:00:00 UTC","id":"1","total_visits":"2613"} +{"parsed_date":"2016-08-26 00:00:00 UTC","id":"1","total_visits":"2359"} +{"parsed_date":"2016-10-12 00:00:00 UTC","id":"1","total_visits":"2871"} +{"parsed_date":"2017-05-15 00:00:00 UTC","id":"1","total_visits":"2360"} +{"parsed_date":"2017-06-12 00:00:00 UTC","id":"1","total_visits":"2361"} +{"parsed_date":"2016-08-16 00:00:00 UTC","id":"1","total_visits":"2873"} +{"parsed_date":"2017-04-30 00:00:00 UTC","id":"1","total_visits":"1594"} +{"parsed_date":"2017-04-05 00:00:00 UTC","id":"1","total_visits":"2619"} +{"parsed_date":"2016-08-12 00:00:00 UTC","id":"1","total_visits":"2619"} +{"parsed_date":"2016-11-08 00:00:00 UTC","id":"1","total_visits":"3899"} +{"parsed_date":"2016-08-13 00:00:00 UTC","id":"1","total_visits":"1596"} +{"parsed_date":"2017-05-09 00:00:00 UTC","id":"1","total_visits":"2108"} +{"parsed_date":"2017-02-23 00:00:00 UTC","id":"1","total_visits":"2364"} +{"parsed_date":"2017-07-31 00:00:00 UTC","id":"1","total_visits":"2620"} +{"parsed_date":"2017-06-25 00:00:00 UTC","id":"1","total_visits":"1597"} +{"parsed_date":"2017-07-29 00:00:00 UTC","id":"1","total_visits":"1597"} +{"parsed_date":"2016-09-17 00:00:00 UTC","id":"1","total_visits":"1853"} +{"parsed_date":"2016-12-27 00:00:00 UTC","id":"1","total_visits":"1855"} +{"parsed_date":"2017-05-20 00:00:00 UTC","id":"1","total_visits":"1855"} +{"parsed_date":"2016-10-08 00:00:00 UTC","id":"1","total_visits":"2114"} +{"parsed_date":"2016-10-27 00:00:00 UTC","id":"1","total_visits":"4162"} +{"parsed_date":"2017-07-08 00:00:00 UTC","id":"1","total_visits":"1859"} +{"parsed_date":"2016-08-24 00:00:00 UTC","id":"1","total_visits":"2627"} +{"parsed_date":"2016-12-23 00:00:00 UTC","id":"1","total_visits":"1604"} +{"parsed_date":"2017-02-02 00:00:00 UTC","id":"1","total_visits":"2372"} +{"parsed_date":"2016-09-08 00:00:00 UTC","id":"1","total_visits":"2628"} +{"parsed_date":"2017-04-02 00:00:00 UTC","id":"1","total_visits":"1861"} +{"parsed_date":"2017-02-15 00:00:00 UTC","id":"1","total_visits":"2629"} +{"parsed_date":"2017-07-05 00:00:00 UTC","id":"1","total_visits":"2885"} +{"parsed_date":"2016-10-17 00:00:00 UTC","id":"1","total_visits":"3397"} +{"parsed_date":"2017-02-20 00:00:00 UTC","id":"1","total_visits":"2374"} +{"parsed_date":"2017-03-24 00:00:00 UTC","id":"1","total_visits":"2374"} +{"parsed_date":"2017-04-20 00:00:00 UTC","id":"1","total_visits":"2374"} +{"parsed_date":"2016-11-18 00:00:00 UTC","id":"1","total_visits":"3654"} +{"parsed_date":"2017-07-25 00:00:00 UTC","id":"1","total_visits":"2631"} +{"parsed_date":"2016-11-13 00:00:00 UTC","id":"1","total_visits":"3144"} +{"parsed_date":"2017-03-18 00:00:00 UTC","id":"1","total_visits":"1610"} +{"parsed_date":"2016-08-03 00:00:00 UTC","id":"1","total_visits":"2890"} +{"parsed_date":"2016-08-19 00:00:00 UTC","id":"1","total_visits":"2379"} +{"parsed_date":"2017-02-14 00:00:00 UTC","id":"1","total_visits":"2379"} +{"parsed_date":"2017-07-11 00:00:00 UTC","id":"1","total_visits":"2635"} +{"parsed_date":"2017-04-22 00:00:00 UTC","id":"1","total_visits":"1612"} +{"parsed_date":"2016-10-07 00:00:00 UTC","id":"1","total_visits":"2892"} +{"parsed_date":"2016-09-05 00:00:00 UTC","id":"1","total_visits":"2125"} +{"parsed_date":"2016-09-23 00:00:00 UTC","id":"1","total_visits":"2381"} +{"parsed_date":"2016-11-15 00:00:00 UTC","id":"1","total_visits":"4685"} +{"parsed_date":"2017-01-28 00:00:00 UTC","id":"1","total_visits":"1614"} +{"parsed_date":"2017-07-14 00:00:00 UTC","id":"1","total_visits":"2382"} +{"parsed_date":"2017-01-07 00:00:00 UTC","id":"1","total_visits":"1615"} +{"parsed_date":"2017-04-03 00:00:00 UTC","id":"1","total_visits":"2383"} +{"parsed_date":"2017-03-20 00:00:00 UTC","id":"1","total_visits":"2383"} +{"parsed_date":"2016-12-18 00:00:00 UTC","id":"1","total_visits":"2128"} +{"parsed_date":"2017-03-17 00:00:00 UTC","id":"1","total_visits":"2129"} +{"parsed_date":"2017-05-23 00:00:00 UTC","id":"1","total_visits":"2129"} +{"parsed_date":"2016-11-30 00:00:00 UTC","id":"1","total_visits":"4435"} +{"parsed_date":"2017-01-01 00:00:00 UTC","id":"1","total_visits":"1364"} +{"parsed_date":"2017-01-02 00:00:00 UTC","id":"1","total_visits":"1620"} +{"parsed_date":"2016-09-25 00:00:00 UTC","id":"1","total_visits":"1877"} +{"parsed_date":"2016-08-07 00:00:00 UTC","id":"1","total_visits":"1622"} +{"parsed_date":"2016-10-09 00:00:00 UTC","id":"1","total_visits":"2134"} +{"parsed_date":"2017-03-01 00:00:00 UTC","id":"1","total_visits":"2390"} +{"parsed_date":"2017-01-04 00:00:00 UTC","id":"1","total_visits":"2390"} +{"parsed_date":"2017-06-06 00:00:00 UTC","id":"1","total_visits":"2391"} +{"parsed_date":"2017-04-18 00:00:00 UTC","id":"1","total_visits":"2391"} +{"parsed_date":"2017-04-06 00:00:00 UTC","id":"1","total_visits":"2647"} +{"parsed_date":"2017-01-30 00:00:00 UTC","id":"1","total_visits":"2392"} +{"parsed_date":"2016-10-16 00:00:00 UTC","id":"1","total_visits":"2649"} +{"parsed_date":"2016-08-04 00:00:00 UTC","id":"1","total_visits":"3161"} +{"parsed_date":"2016-10-21 00:00:00 UTC","id":"1","total_visits":"3419"} +{"parsed_date":"2016-08-02 00:00:00 UTC","id":"1","total_visits":"2140"} +{"parsed_date":"2017-03-06 00:00:00 UTC","id":"1","total_visits":"2396"} +{"parsed_date":"2016-09-13 00:00:00 UTC","id":"1","total_visits":"2396"} +{"parsed_date":"2016-09-14 00:00:00 UTC","id":"1","total_visits":"2652"} +{"parsed_date":"2017-04-19 00:00:00 UTC","id":"1","total_visits":"2397"} +{"parsed_date":"2017-06-19 00:00:00 UTC","id":"1","total_visits":"2142"} +{"parsed_date":"2016-12-13 00:00:00 UTC","id":"1","total_visits":"3166"} +{"parsed_date":"2017-06-20 00:00:00 UTC","id":"1","total_visits":"2143"} +{"parsed_date":"2016-10-10 00:00:00 UTC","id":"1","total_visits":"2911"} +{"parsed_date":"2017-07-06 00:00:00 UTC","id":"1","total_visits":"2658"} +{"parsed_date":"2017-01-03 00:00:00 UTC","id":"1","total_visits":"2403"} +{"parsed_date":"2017-01-08 00:00:00 UTC","id":"1","total_visits":"1637"} +{"parsed_date":"2017-02-25 00:00:00 UTC","id":"1","total_visits":"1638"} +{"parsed_date":"2017-05-24 00:00:00 UTC","id":"1","total_visits":"2406"} +{"parsed_date":"2016-11-22 00:00:00 UTC","id":"1","total_visits":"3942"} +{"parsed_date":"2017-05-06 00:00:00 UTC","id":"1","total_visits":"1383"} +{"parsed_date":"2017-07-02 00:00:00 UTC","id":"1","total_visits":"1895"} +{"parsed_date":"2016-12-01 00:00:00 UTC","id":"1","total_visits":"4200"} +{"parsed_date":"2017-03-16 00:00:00 UTC","id":"1","total_visits":"2409"} +{"parsed_date":"2016-12-12 00:00:00 UTC","id":"1","total_visits":"3433"} +{"parsed_date":"2016-12-25 00:00:00 UTC","id":"1","total_visits":"1386"} +{"parsed_date":"2017-02-26 00:00:00 UTC","id":"1","total_visits":"1643"} +{"parsed_date":"2017-04-28 00:00:00 UTC","id":"1","total_visits":"2411"} +{"parsed_date":"2016-08-11 00:00:00 UTC","id":"1","total_visits":"2667"} +{"parsed_date":"2017-07-20 00:00:00 UTC","id":"1","total_visits":"2668"} +{"parsed_date":"2017-05-21 00:00:00 UTC","id":"1","total_visits":"1645"} +{"parsed_date":"2017-06-17 00:00:00 UTC","id":"1","total_visits":"1391"} +{"parsed_date":"2016-12-29 00:00:00 UTC","id":"1","total_visits":"1647"} +{"parsed_date":"2017-07-17 00:00:00 UTC","id":"1","total_visits":"2671"} +{"parsed_date":"2017-01-16 00:00:00 UTC","id":"1","total_visits":"1906"} +{"parsed_date":"2017-03-03 00:00:00 UTC","id":"1","total_visits":"2162"} +{"parsed_date":"2016-11-14 00:00:00 UTC","id":"1","total_visits":"4466"} +{"parsed_date":"2016-08-30 00:00:00 UTC","id":"1","total_visits":"2675"} +{"parsed_date":"2016-08-27 00:00:00 UTC","id":"1","total_visits":"1654"} +{"parsed_date":"2017-02-09 00:00:00 UTC","id":"1","total_visits":"2678"} +{"parsed_date":"2017-06-03 00:00:00 UTC","id":"1","total_visits":"1399"} +{"parsed_date":"2017-05-07 00:00:00 UTC","id":"1","total_visits":"1400"} +{"parsed_date":"2016-11-02 00:00:00 UTC","id":"1","total_visits":"3960"} +{"parsed_date":"2016-12-15 00:00:00 UTC","id":"1","total_visits":"2937"} +{"parsed_date":"2017-04-01 00:00:00 UTC","id":"1","total_visits":"2170"} +{"parsed_date":"2017-07-21 00:00:00 UTC","id":"1","total_visits":"2427"} +{"parsed_date":"2016-08-06 00:00:00 UTC","id":"1","total_visits":"1663"} +{"parsed_date":"2016-09-01 00:00:00 UTC","id":"1","total_visits":"2687"} +{"parsed_date":"2017-06-28 00:00:00 UTC","id":"1","total_visits":"2687"} +{"parsed_date":"2016-08-20 00:00:00 UTC","id":"1","total_visits":"1664"} +{"parsed_date":"2017-04-26 00:00:00 UTC","id":"1","total_visits":"4224"} +{"parsed_date":"2017-07-09 00:00:00 UTC","id":"1","total_visits":"1921"} +{"parsed_date":"2017-07-28 00:00:00 UTC","id":"1","total_visits":"2433"} +{"parsed_date":"2016-09-19 00:00:00 UTC","id":"1","total_visits":"2689"} +{"parsed_date":"2017-07-24 00:00:00 UTC","id":"1","total_visits":"2436"} +{"parsed_date":"2017-06-13 00:00:00 UTC","id":"1","total_visits":"2181"} +{"parsed_date":"2016-09-15 00:00:00 UTC","id":"1","total_visits":"2949"} +{"parsed_date":"2017-02-03 00:00:00 UTC","id":"1","total_visits":"2182"} +{"parsed_date":"2016-09-10 00:00:00 UTC","id":"1","total_visits":"1671"} +{"parsed_date":"2017-06-09 00:00:00 UTC","id":"1","total_visits":"1927"} +{"parsed_date":"2017-01-11 00:00:00 UTC","id":"1","total_visits":"2185"} +{"parsed_date":"2017-02-19 00:00:00 UTC","id":"1","total_visits":"2187"} +{"parsed_date":"2017-01-17 00:00:00 UTC","id":"1","total_visits":"2443"} +{"parsed_date":"2017-05-12 00:00:00 UTC","id":"1","total_visits":"1932"} +{"parsed_date":"2016-12-16 00:00:00 UTC","id":"1","total_visits":"2956"} +{"parsed_date":"2017-02-01 00:00:00 UTC","id":"1","total_visits":"2445"} +{"parsed_date":"2016-11-26 00:00:00 UTC","id":"1","total_visits":"3213"} +{"parsed_date":"2017-06-02 00:00:00 UTC","id":"1","total_visits":"2190"} +{"parsed_date":"2016-08-05 00:00:00 UTC","id":"1","total_visits":"2702"} +{"parsed_date":"2016-11-01 00:00:00 UTC","id":"1","total_visits":"3728"} +{"parsed_date":"2017-01-05 00:00:00 UTC","id":"1","total_visits":"2193"} +{"parsed_date":"2017-03-08 00:00:00 UTC","id":"1","total_visits":"2449"} +{"parsed_date":"2016-08-28 00:00:00 UTC","id":"1","total_visits":"1682"} +{"parsed_date":"2017-07-04 00:00:00 UTC","id":"1","total_visits":"1938"} +{"parsed_date":"2017-03-10 00:00:00 UTC","id":"1","total_visits":"2194"} +{"parsed_date":"2017-07-07 00:00:00 UTC","id":"1","total_visits":"2450"} +{"parsed_date":"2016-10-29 00:00:00 UTC","id":"1","total_visits":"2964"} +{"parsed_date":"2016-10-13 00:00:00 UTC","id":"1","total_visits":"2964"} +{"parsed_date":"2016-12-04 00:00:00 UTC","id":"1","total_visits":"3220"} +{"parsed_date":"2017-01-21 00:00:00 UTC","id":"1","total_visits":"1685"} +{"parsed_date":"2017-06-29 00:00:00 UTC","id":"1","total_visits":"2709"} +{"parsed_date":"2016-08-29 00:00:00 UTC","id":"1","total_visits":"2454"} +{"parsed_date":"2016-12-19 00:00:00 UTC","id":"1","total_visits":"3222"} +{"parsed_date":"2017-05-30 00:00:00 UTC","id":"1","total_visits":"2199"} +{"parsed_date":"2017-02-10 00:00:00 UTC","id":"1","total_visits":"2199"} +{"parsed_date":"2016-08-31 00:00:00 UTC","id":"1","total_visits":"3223"} +{"parsed_date":"2017-06-18 00:00:00 UTC","id":"1","total_visits":"1432"} +{"parsed_date":"2017-01-12 00:00:00 UTC","id":"1","total_visits":"2203"} +{"parsed_date":"2017-05-18 00:00:00 UTC","id":"1","total_visits":"2715"} +{"parsed_date":"2016-10-23 00:00:00 UTC","id":"1","total_visits":"2971"} +{"parsed_date":"2016-09-04 00:00:00 UTC","id":"1","total_visits":"1692"} +{"parsed_date":"2016-12-10 00:00:00 UTC","id":"1","total_visits":"2207"} +{"parsed_date":"2016-12-11 00:00:00 UTC","id":"1","total_visits":"2208"} +{"parsed_date":"2017-04-11 00:00:00 UTC","id":"1","total_visits":"2464"} +{"parsed_date":"2016-09-21 00:00:00 UTC","id":"1","total_visits":"2720"} +{"parsed_date":"2016-11-06 00:00:00 UTC","id":"1","total_visits":"3232"} +{"parsed_date":"2017-01-26 00:00:00 UTC","id":"1","total_visits":"2209"} +{"parsed_date":"2016-09-12 00:00:00 UTC","id":"1","total_visits":"2465"} +{"parsed_date":"2017-04-21 00:00:00 UTC","id":"1","total_visits":"2210"} +{"parsed_date":"2017-01-06 00:00:00 UTC","id":"1","total_visits":"2210"} +{"parsed_date":"2017-04-04 00:00:00 UTC","id":"1","total_visits":"2978"} +{"parsed_date":"2017-01-22 00:00:00 UTC","id":"1","total_visits":"1700"} +{"parsed_date":"2017-07-26 00:00:00 UTC","id":"1","total_visits":"2725"} +{"parsed_date":"2016-08-18 00:00:00 UTC","id":"1","total_visits":"2725"} +{"parsed_date":"2016-09-27 00:00:00 UTC","id":"1","total_visits":"2727"} +{"parsed_date":"2016-12-02 00:00:00 UTC","id":"1","total_visits":"3751"} +{"parsed_date":"2017-05-05 00:00:00 UTC","id":"1","total_visits":"1960"} +{"parsed_date":"2016-11-19 00:00:00 UTC","id":"1","total_visits":"2984"} +{"parsed_date":"2016-11-09 00:00:00 UTC","id":"1","total_visits":"3752"} +{"parsed_date":"2016-12-05 00:00:00 UTC","id":"1","total_visits":"4265"} +{"parsed_date":"2017-05-11 00:00:00 UTC","id":"1","total_visits":"2218"} +{"parsed_date":"2017-01-25 00:00:00 UTC","id":"1","total_visits":"2986"} +{"parsed_date":"2017-03-11 00:00:00 UTC","id":"1","total_visits":"1707"} +{"parsed_date":"2017-03-30 00:00:00 UTC","id":"1","total_visits":"2731"} +{"parsed_date":"2016-10-20 00:00:00 UTC","id":"1","total_visits":"3755"} +{"parsed_date":"2017-02-07 00:00:00 UTC","id":"1","total_visits":"2476"} +{"parsed_date":"2017-02-22 00:00:00 UTC","id":"1","total_visits":"2477"} +{"parsed_date":"2017-07-23 00:00:00 UTC","id":"1","total_visits":"1966"} +{"parsed_date":"2016-11-03 00:00:00 UTC","id":"1","total_visits":"4014"} +{"parsed_date":"2016-08-01 00:00:00 UTC","id":"1","total_visits":"1711"} +{"parsed_date":"2017-01-13 00:00:00 UTC","id":"1","total_visits":"1967"} +{"parsed_date":"2017-05-19 00:00:00 UTC","id":"1","total_visits":"2223"} +{"parsed_date":"2016-11-20 00:00:00 UTC","id":"1","total_visits":"3247"} +{"parsed_date":"2016-11-25 00:00:00 UTC","id":"1","total_visits":"3759"} +{"parsed_date":"2017-03-25 00:00:00 UTC","id":"1","total_visits":"1712"} +{"parsed_date":"2017-01-27 00:00:00 UTC","id":"1","total_visits":"1969"} +{"parsed_date":"2017-06-26 00:00:00 UTC","id":"1","total_visits":"2226"} +{"parsed_date":"2017-05-25 00:00:00 UTC","id":"1","total_visits":"2228"} +{"parsed_date":"2017-01-31 00:00:00 UTC","id":"1","total_visits":"2229"} +{"parsed_date":"2017-07-13 00:00:00 UTC","id":"1","total_visits":"2741"} +{"parsed_date":"2017-03-15 00:00:00 UTC","id":"1","total_visits":"2486"} +{"parsed_date":"2017-05-28 00:00:00 UTC","id":"1","total_visits":"1463"} +{"parsed_date":"2017-03-09 00:00:00 UTC","id":"1","total_visits":"2231"} +{"parsed_date":"2017-07-15 00:00:00 UTC","id":"1","total_visits":"1721"} +{"parsed_date":"2016-11-24 00:00:00 UTC","id":"1","total_visits":"3770"} +{"parsed_date":"2016-10-05 00:00:00 UTC","id":"1","total_visits":"3770"} +{"parsed_date":"2016-12-31 00:00:00 UTC","id":"1","total_visits":"1211"} +{"parsed_date":"2016-10-02 00:00:00 UTC","id":"1","total_visits":"1724"} +{"parsed_date":"2017-07-22 00:00:00 UTC","id":"1","total_visits":"1724"} +{"parsed_date":"2016-09-11 00:00:00 UTC","id":"1","total_visits":"1725"} +{"parsed_date":"2017-06-15 00:00:00 UTC","id":"1","total_visits":"2237"} +{"parsed_date":"2017-06-05 00:00:00 UTC","id":"1","total_visits":"2493"} +{"parsed_date":"2017-02-06 00:00:00 UTC","id":"1","total_visits":"2238"} +{"parsed_date":"2016-10-15 00:00:00 UTC","id":"1","total_visits":"2495"} +{"parsed_date":"2016-08-21 00:00:00 UTC","id":"1","total_visits":"1730"} +{"parsed_date":"2016-08-23 00:00:00 UTC","id":"1","total_visits":"2754"} +{"parsed_date":"2017-06-30 00:00:00 UTC","id":"1","total_visits":"2499"} +{"parsed_date":"2017-01-18 00:00:00 UTC","id":"1","total_visits":"2245"} +{"parsed_date":"2016-08-10 00:00:00 UTC","id":"1","total_visits":"2757"} +{"parsed_date":"2016-12-08 00:00:00 UTC","id":"1","total_visits":"3013"} +{"parsed_date":"2016-11-28 00:00:00 UTC","id":"1","total_visits":"4807"} +{"parsed_date":"2017-05-22 00:00:00 UTC","id":"1","total_visits":"2248"} +{"parsed_date":"2016-09-20 00:00:00 UTC","id":"1","total_visits":"2760"} +{"parsed_date":"2016-10-06 00:00:00 UTC","id":"1","total_visits":"3016"} +{"parsed_date":"2016-09-06 00:00:00 UTC","id":"1","total_visits":"2508"} +{"parsed_date":"2016-09-03 00:00:00 UTC","id":"1","total_visits":"1741"} +{"parsed_date":"2016-12-06 00:00:00 UTC","id":"1","total_visits":"3021"} +{"parsed_date":"2016-12-24 00:00:00 UTC","id":"1","total_visits":"1231"} +{"parsed_date":"2016-10-28 00:00:00 UTC","id":"1","total_visits":"3791"} +{"parsed_date":"2016-12-30 00:00:00 UTC","id":"1","total_visits":"1232"} +{"parsed_date":"2017-05-29 00:00:00 UTC","id":"1","total_visits":"1745"} +{"parsed_date":"2017-07-10 00:00:00 UTC","id":"1","total_visits":"2769"} +{"parsed_date":"2017-06-22 00:00:00 UTC","id":"1","total_visits":"2258"} +{"parsed_date":"2017-07-19 00:00:00 UTC","id":"1","total_visits":"2514"} +{"parsed_date":"2016-10-03 00:00:00 UTC","id":"1","total_visits":"2514"} +{"parsed_date":"2017-06-14 00:00:00 UTC","id":"1","total_visits":"2517"} +{"parsed_date":"2016-10-22 00:00:00 UTC","id":"1","total_visits":"3029"} +{"parsed_date":"2017-01-23 00:00:00 UTC","id":"1","total_visits":"2262"} +{"parsed_date":"2017-04-24 00:00:00 UTC","id":"1","total_visits":"2263"} +{"parsed_date":"2016-11-10 00:00:00 UTC","id":"1","total_visits":"4055"} +{"parsed_date":"2016-09-26 00:00:00 UTC","id":"1","total_visits":"2776"} +{"parsed_date":"2016-10-19 00:00:00 UTC","id":"1","total_visits":"3544"} +{"parsed_date":"2017-03-04 00:00:00 UTC","id":"1","total_visits":"1753"} +{"parsed_date":"2017-05-26 00:00:00 UTC","id":"1","total_visits":"2009"} +{"parsed_date":"2017-02-13 00:00:00 UTC","id":"1","total_visits":"2266"} +{"parsed_date":"2017-02-18 00:00:00 UTC","id":"1","total_visits":"1755"} +{"parsed_date":"2017-03-02 00:00:00 UTC","id":"1","total_visits":"2267"} +{"parsed_date":"2017-03-31 00:00:00 UTC","id":"1","total_visits":"2268"} +{"parsed_date":"2017-01-10 00:00:00 UTC","id":"1","total_visits":"2268"} +{"parsed_date":"2017-03-29 00:00:00 UTC","id":"1","total_visits":"2525"} +{"parsed_date":"2017-03-27 00:00:00 UTC","id":"1","total_visits":"2525"} +{"parsed_date":"2016-11-23 00:00:00 UTC","id":"1","total_visits":"3805"} +{"parsed_date":"2017-05-27 00:00:00 UTC","id":"1","total_visits":"1502"} +{"parsed_date":"2016-10-24 00:00:00 UTC","id":"1","total_visits":"4063"} +{"parsed_date":"2016-12-14 00:00:00 UTC","id":"1","total_visits":"3040"} +{"parsed_date":"2017-02-11 00:00:00 UTC","id":"1","total_visits":"1761"} +{"parsed_date":"2017-07-27 00:00:00 UTC","id":"1","total_visits":"2529"} +{"parsed_date":"2017-02-17 00:00:00 UTC","id":"1","total_visits":"2785"} +{"parsed_date":"2017-04-15 00:00:00 UTC","id":"1","total_visits":"1506"} +{"parsed_date":"2016-11-05 00:00:00 UTC","id":"1","total_visits":"3042"} +{"parsed_date":"2016-10-04 00:00:00 UTC","id":"1","total_visits":"4322"} +{"parsed_date":"2017-05-13 00:00:00 UTC","id":"1","total_visits":"1251"} +{"parsed_date":"2017-04-16 00:00:00 UTC","id":"1","total_visits":"1507"} +{"parsed_date":"2016-12-28 00:00:00 UTC","id":"1","total_visits":"1763"} +{"parsed_date":"2016-08-15 00:00:00 UTC","id":"1","total_visits":"3043"} +{"parsed_date":"2016-12-03 00:00:00 UTC","id":"1","total_visits":"3044"} +{"parsed_date":"2017-06-27 00:00:00 UTC","id":"1","total_visits":"2789"} +{"parsed_date":"2017-06-24 00:00:00 UTC","id":"1","total_visits":"1510"} +{"parsed_date":"2017-07-16 00:00:00 UTC","id":"1","total_visits":"1766"} +{"parsed_date":"2017-04-09 00:00:00 UTC","id":"1","total_visits":"1766"} +{"parsed_date":"2017-06-07 00:00:00 UTC","id":"1","total_visits":"2279"} +{"parsed_date":"2017-04-17 00:00:00 UTC","id":"1","total_visits":"2279"} +{"parsed_date":"2016-09-28 00:00:00 UTC","id":"1","total_visits":"2535"} +{"parsed_date":"2017-03-26 00:00:00 UTC","id":"1","total_visits":"1768"} +{"parsed_date":"2017-05-10 00:00:00 UTC","id":"1","total_visits":"2024"} +{"parsed_date":"2017-06-08 00:00:00 UTC","id":"1","total_visits":"2280"} +{"parsed_date":"2017-05-08 00:00:00 UTC","id":"1","total_visits":"2025"} +{"parsed_date":"2017-03-13 00:00:00 UTC","id":"1","total_visits":"2537"} +{"parsed_date":"2016-11-17 00:00:00 UTC","id":"1","total_visits":"4074"} +{"parsed_date":"2016-08-25 00:00:00 UTC","id":"1","total_visits":"2539"} +{"parsed_date":"2017-02-16 00:00:00 UTC","id":"1","total_visits":"2539"} +{"parsed_date":"2017-06-16 00:00:00 UTC","id":"1","total_visits":"2028"} +{"parsed_date":"2016-11-16 00:00:00 UTC","id":"1","total_visits":"4334"} +{"parsed_date":"2016-08-17 00:00:00 UTC","id":"1","total_visits":"2800"} +{"parsed_date":"2017-03-19 00:00:00 UTC","id":"1","total_visits":"1776"} +{"parsed_date":"2016-11-29 00:00:00 UTC","id":"1","total_visits":"4337"} +{"parsed_date":"2017-02-05 00:00:00 UTC","id":"1","total_visits":"1522"} +{"parsed_date":"2016-10-31 00:00:00 UTC","id":"1","total_visits":"3827"} +{"parsed_date":"2017-05-31 00:00:00 UTC","id":"1","total_visits":"2292"} +{"parsed_date":"2017-07-18 00:00:00 UTC","id":"1","total_visits":"2804"} +{"parsed_date":"2017-03-12 00:00:00 UTC","id":"1","total_visits":"1781"} +{"parsed_date":"2016-09-09 00:00:00 UTC","id":"1","total_visits":"2549"} +{"parsed_date":"2017-01-14 00:00:00 UTC","id":"1","total_visits":"1526"} +{"parsed_date":"2017-05-04 00:00:00 UTC","id":"1","total_visits":"2806"} +{"parsed_date":"2016-11-07 00:00:00 UTC","id":"1","total_visits":"3832"} +{"parsed_date":"2017-04-07 00:00:00 UTC","id":"1","total_visits":"2297"} +{"parsed_date":"2017-07-12 00:00:00 UTC","id":"1","total_visits":"2554"} +{"parsed_date":"2017-04-13 00:00:00 UTC","id":"1","total_visits":"2300"} +{"parsed_date":"2017-08-01 00:00:00 UTC","id":"1","total_visits":"2556"} +{"parsed_date":"2017-06-04 00:00:00 UTC","id":"1","total_visits":"1534"} +{"parsed_date":"2017-02-12 00:00:00 UTC","id":"1","total_visits":"1790"} +{"parsed_date":"2017-07-03 00:00:00 UTC","id":"1","total_visits":"2046"} +{"parsed_date":"2016-09-30 00:00:00 UTC","id":"1","total_visits":"2303"} +{"parsed_date":"2016-08-08 00:00:00 UTC","id":"1","total_visits":"2815"} +{"parsed_date":"2017-07-01 00:00:00 UTC","id":"2","total_visits":"2048"} +{"parsed_date":"2016-09-07 00:00:00 UTC","id":"2","total_visits":"2562"} +{"parsed_date":"2016-10-25 00:00:00 UTC","id":"2","total_visits":"3842"} +{"parsed_date":"2017-04-10 00:00:00 UTC","id":"2","total_visits":"2563"} +{"parsed_date":"2017-01-09 00:00:00 UTC","id":"2","total_visits":"2308"} +{"parsed_date":"2017-05-02 00:00:00 UTC","id":"2","total_visits":"2564"} +{"parsed_date":"2016-11-11 00:00:00 UTC","id":"2","total_visits":"3588"} +{"parsed_date":"2017-07-30 00:00:00 UTC","id":"2","total_visits":"1799"} +{"parsed_date":"2017-06-10 00:00:00 UTC","id":"2","total_visits":"1545"} +{"parsed_date":"2016-08-14 00:00:00 UTC","id":"2","total_visits":"1801"} +{"parsed_date":"2017-05-14 00:00:00 UTC","id":"2","total_visits":"1290"} +{"parsed_date":"2017-02-08 00:00:00 UTC","id":"2","total_visits":"2570"} +{"parsed_date":"2017-06-01 00:00:00 UTC","id":"2","total_visits":"2826"} +{"parsed_date":"2017-04-23 00:00:00 UTC","id":"2","total_visits":"1548"} +{"parsed_date":"2016-11-04 00:00:00 UTC","id":"2","total_visits":"3596"} +{"parsed_date":"2017-02-04 00:00:00 UTC","id":"2","total_visits":"1549"} +{"parsed_date":"2016-12-09 00:00:00 UTC","id":"2","total_visits":"2830"} +{"parsed_date":"2016-10-30 00:00:00 UTC","id":"2","total_visits":"3086"} +{"parsed_date":"2017-03-28 00:00:00 UTC","id":"2","total_visits":"2577"} +{"parsed_date":"2017-06-11 00:00:00 UTC","id":"2","total_visits":"1555"} +{"parsed_date":"2016-12-17 00:00:00 UTC","id":"2","total_visits":"2324"} +{"parsed_date":"2016-09-22 00:00:00 UTC","id":"2","total_visits":"2581"} +{"parsed_date":"2017-01-29 00:00:00 UTC","id":"2","total_visits":"1814"} +{"parsed_date":"2017-03-22 00:00:00 UTC","id":"2","total_visits":"2582"} +{"parsed_date":"2017-02-21 00:00:00 UTC","id":"2","total_visits":"2582"} +{"parsed_date":"2016-10-14 00:00:00 UTC","id":"2","total_visits":"2838"} +{"parsed_date":"2017-04-27 00:00:00 UTC","id":"2","total_visits":"2838"} +{"parsed_date":"2016-10-26 00:00:00 UTC","id":"2","total_visits":"4375"} +{"parsed_date":"2016-08-22 00:00:00 UTC","id":"2","total_visits":"2584"} +{"parsed_date":"2016-12-07 00:00:00 UTC","id":"2","total_visits":"2840"} +{"parsed_date":"2017-01-20 00:00:00 UTC","id":"2","total_visits":"2074"} +{"parsed_date":"2017-03-07 00:00:00 UTC","id":"2","total_visits":"2586"} +{"parsed_date":"2017-05-16 00:00:00 UTC","id":"2","total_visits":"3098"} +{"parsed_date":"2017-05-03 00:00:00 UTC","id":"2","total_visits":"2588"} +{"parsed_date":"2017-05-01 00:00:00 UTC","id":"2","total_visits":"2588"} +{"parsed_date":"2016-11-27 00:00:00 UTC","id":"2","total_visits":"3356"} +{"parsed_date":"2017-04-29 00:00:00 UTC","id":"2","total_visits":"1566"} +{"parsed_date":"2016-09-18 00:00:00 UTC","id":"2","total_visits":"1822"} +{"parsed_date":"2017-03-23 00:00:00 UTC","id":"2","total_visits":"2847"} +{"parsed_date":"2017-03-14 00:00:00 UTC","id":"2","total_visits":"2338"} +{"parsed_date":"2016-12-21 00:00:00 UTC","id":"2","total_visits":"2594"} +{"parsed_date":"2016-10-11 00:00:00 UTC","id":"2","total_visits":"2850"} +{"parsed_date":"2017-01-24 00:00:00 UTC","id":"2","total_visits":"3618"} +{"parsed_date":"2017-03-05 00:00:00 UTC","id":"2","total_visits":"1827"} +{"parsed_date":"2017-01-19 00:00:00 UTC","id":"2","total_visits":"2083"} +{"parsed_date":"2016-08-09 00:00:00 UTC","id":"2","total_visits":"2851"} +{"parsed_date":"2017-04-08 00:00:00 UTC","id":"2","total_visits":"1829"} +{"parsed_date":"2017-04-12 00:00:00 UTC","id":"2","total_visits":"2341"} +{"parsed_date":"2016-09-29 00:00:00 UTC","id":"2","total_visits":"2597"} +{"parsed_date":"2016-12-20 00:00:00 UTC","id":"2","total_visits":"3110"} +{"parsed_date":"2017-01-15 00:00:00 UTC","id":"2","total_visits":"1576"} +{"parsed_date":"2017-04-14 00:00:00 UTC","id":"2","total_visits":"1834"} +{"parsed_date":"2017-02-28 00:00:00 UTC","id":"2","total_visits":"2347"} +{"parsed_date":"2016-09-16 00:00:00 UTC","id":"2","total_visits":"2603"} +{"parsed_date":"2016-10-18 00:00:00 UTC","id":"2","total_visits":"3628"} +{"parsed_date":"2017-02-24 00:00:00 UTC","id":"2","total_visits":"2093"} +{"parsed_date":"2017-05-17 00:00:00 UTC","id":"2","total_visits":"3117"} +{"parsed_date":"2017-06-23 00:00:00 UTC","id":"2","total_visits":"2095"} +{"parsed_date":"2016-11-12 00:00:00 UTC","id":"2","total_visits":"3119"} +{"parsed_date":"2016-11-21 00:00:00 UTC","id":"2","total_visits":"4143"} +{"parsed_date":"2017-02-27 00:00:00 UTC","id":"2","total_visits":"2352"} +{"parsed_date":"2016-12-26 00:00:00 UTC","id":"2","total_visits":"1586"} +{"parsed_date":"2017-04-25 00:00:00 UTC","id":"2","total_visits":"2354"} +{"parsed_date":"2017-03-21 00:00:00 UTC","id":"2","total_visits":"2611"} +{"parsed_date":"2016-12-22 00:00:00 UTC","id":"2","total_visits":"2100"} +{"parsed_date":"2016-10-01 00:00:00 UTC","id":"2","total_visits":"1589"} +{"parsed_date":"2016-09-24 00:00:00 UTC","id":"2","total_visits":"1845"} +{"parsed_date":"2017-06-21 00:00:00 UTC","id":"2","total_visits":"2357"} +{"parsed_date":"2016-09-02 00:00:00 UTC","id":"2","total_visits":"2613"} +{"parsed_date":"2016-08-26 00:00:00 UTC","id":"2","total_visits":"2359"} +{"parsed_date":"2016-10-12 00:00:00 UTC","id":"2","total_visits":"2871"} +{"parsed_date":"2017-05-15 00:00:00 UTC","id":"2","total_visits":"2360"} +{"parsed_date":"2017-06-12 00:00:00 UTC","id":"2","total_visits":"2361"} +{"parsed_date":"2016-08-16 00:00:00 UTC","id":"2","total_visits":"2873"} +{"parsed_date":"2017-04-30 00:00:00 UTC","id":"2","total_visits":"1594"} +{"parsed_date":"2017-04-05 00:00:00 UTC","id":"2","total_visits":"2619"} +{"parsed_date":"2016-08-12 00:00:00 UTC","id":"2","total_visits":"2619"} +{"parsed_date":"2016-11-08 00:00:00 UTC","id":"2","total_visits":"3899"} +{"parsed_date":"2016-08-13 00:00:00 UTC","id":"2","total_visits":"1596"} +{"parsed_date":"2017-05-09 00:00:00 UTC","id":"2","total_visits":"2108"} +{"parsed_date":"2017-02-23 00:00:00 UTC","id":"2","total_visits":"2364"} +{"parsed_date":"2017-07-31 00:00:00 UTC","id":"2","total_visits":"2620"} +{"parsed_date":"2017-06-25 00:00:00 UTC","id":"2","total_visits":"1597"} +{"parsed_date":"2017-07-29 00:00:00 UTC","id":"2","total_visits":"1597"} +{"parsed_date":"2016-09-17 00:00:00 UTC","id":"2","total_visits":"1853"} +{"parsed_date":"2016-12-27 00:00:00 UTC","id":"2","total_visits":"1855"} +{"parsed_date":"2017-05-20 00:00:00 UTC","id":"2","total_visits":"1855"} +{"parsed_date":"2016-10-08 00:00:00 UTC","id":"2","total_visits":"2114"} +{"parsed_date":"2016-10-27 00:00:00 UTC","id":"2","total_visits":"4162"} +{"parsed_date":"2017-07-08 00:00:00 UTC","id":"2","total_visits":"1859"} +{"parsed_date":"2016-08-24 00:00:00 UTC","id":"2","total_visits":"2627"} +{"parsed_date":"2016-12-23 00:00:00 UTC","id":"2","total_visits":"1604"} +{"parsed_date":"2017-02-02 00:00:00 UTC","id":"2","total_visits":"2372"} +{"parsed_date":"2016-09-08 00:00:00 UTC","id":"2","total_visits":"2628"} +{"parsed_date":"2017-04-02 00:00:00 UTC","id":"2","total_visits":"1861"} +{"parsed_date":"2017-02-15 00:00:00 UTC","id":"2","total_visits":"2629"} +{"parsed_date":"2017-07-05 00:00:00 UTC","id":"2","total_visits":"2885"} +{"parsed_date":"2016-10-17 00:00:00 UTC","id":"2","total_visits":"3397"} +{"parsed_date":"2017-02-20 00:00:00 UTC","id":"2","total_visits":"2374"} +{"parsed_date":"2017-03-24 00:00:00 UTC","id":"2","total_visits":"2374"} +{"parsed_date":"2017-04-20 00:00:00 UTC","id":"2","total_visits":"2374"} +{"parsed_date":"2016-11-18 00:00:00 UTC","id":"2","total_visits":"3654"} +{"parsed_date":"2017-07-25 00:00:00 UTC","id":"2","total_visits":"2631"} +{"parsed_date":"2016-11-13 00:00:00 UTC","id":"2","total_visits":"3144"} +{"parsed_date":"2017-03-18 00:00:00 UTC","id":"2","total_visits":"1610"} +{"parsed_date":"2016-08-03 00:00:00 UTC","id":"2","total_visits":"2890"} +{"parsed_date":"2016-08-19 00:00:00 UTC","id":"2","total_visits":"2379"} +{"parsed_date":"2017-02-14 00:00:00 UTC","id":"2","total_visits":"2379"} +{"parsed_date":"2017-07-11 00:00:00 UTC","id":"2","total_visits":"2635"} +{"parsed_date":"2017-04-22 00:00:00 UTC","id":"2","total_visits":"1612"} +{"parsed_date":"2016-10-07 00:00:00 UTC","id":"2","total_visits":"2892"} +{"parsed_date":"2016-09-05 00:00:00 UTC","id":"2","total_visits":"2125"} +{"parsed_date":"2016-09-23 00:00:00 UTC","id":"2","total_visits":"2381"} +{"parsed_date":"2016-11-15 00:00:00 UTC","id":"2","total_visits":"4685"} +{"parsed_date":"2017-01-28 00:00:00 UTC","id":"2","total_visits":"1614"} +{"parsed_date":"2017-07-14 00:00:00 UTC","id":"2","total_visits":"2382"} +{"parsed_date":"2017-01-07 00:00:00 UTC","id":"2","total_visits":"1615"} +{"parsed_date":"2017-04-03 00:00:00 UTC","id":"2","total_visits":"2383"} +{"parsed_date":"2017-03-20 00:00:00 UTC","id":"2","total_visits":"2383"} +{"parsed_date":"2016-12-18 00:00:00 UTC","id":"2","total_visits":"2128"} +{"parsed_date":"2017-03-17 00:00:00 UTC","id":"2","total_visits":"2129"} +{"parsed_date":"2017-05-23 00:00:00 UTC","id":"2","total_visits":"2129"} +{"parsed_date":"2016-11-30 00:00:00 UTC","id":"2","total_visits":"4435"} +{"parsed_date":"2017-01-01 00:00:00 UTC","id":"2","total_visits":"1364"} +{"parsed_date":"2017-01-02 00:00:00 UTC","id":"2","total_visits":"1620"} +{"parsed_date":"2016-09-25 00:00:00 UTC","id":"2","total_visits":"1877"} +{"parsed_date":"2016-08-07 00:00:00 UTC","id":"2","total_visits":"1622"} +{"parsed_date":"2016-10-09 00:00:00 UTC","id":"2","total_visits":"2134"} +{"parsed_date":"2017-03-01 00:00:00 UTC","id":"2","total_visits":"2390"} +{"parsed_date":"2017-01-04 00:00:00 UTC","id":"2","total_visits":"2390"} +{"parsed_date":"2017-06-06 00:00:00 UTC","id":"2","total_visits":"2391"} +{"parsed_date":"2017-04-18 00:00:00 UTC","id":"2","total_visits":"2391"} +{"parsed_date":"2017-04-06 00:00:00 UTC","id":"2","total_visits":"2647"} +{"parsed_date":"2017-01-30 00:00:00 UTC","id":"2","total_visits":"2392"} +{"parsed_date":"2016-10-16 00:00:00 UTC","id":"2","total_visits":"2649"} +{"parsed_date":"2016-08-04 00:00:00 UTC","id":"2","total_visits":"3161"} +{"parsed_date":"2016-10-21 00:00:00 UTC","id":"2","total_visits":"3419"} +{"parsed_date":"2016-08-02 00:00:00 UTC","id":"2","total_visits":"2140"} +{"parsed_date":"2017-03-06 00:00:00 UTC","id":"2","total_visits":"2396"} +{"parsed_date":"2016-09-13 00:00:00 UTC","id":"2","total_visits":"2396"} +{"parsed_date":"2016-09-14 00:00:00 UTC","id":"2","total_visits":"2652"} +{"parsed_date":"2017-04-19 00:00:00 UTC","id":"2","total_visits":"2397"} +{"parsed_date":"2017-06-19 00:00:00 UTC","id":"2","total_visits":"2142"} +{"parsed_date":"2016-12-13 00:00:00 UTC","id":"2","total_visits":"3166"} +{"parsed_date":"2017-06-20 00:00:00 UTC","id":"2","total_visits":"2143"} +{"parsed_date":"2016-10-10 00:00:00 UTC","id":"2","total_visits":"2911"} +{"parsed_date":"2017-07-06 00:00:00 UTC","id":"2","total_visits":"2658"} +{"parsed_date":"2017-01-03 00:00:00 UTC","id":"2","total_visits":"2403"} +{"parsed_date":"2017-01-08 00:00:00 UTC","id":"2","total_visits":"1637"} +{"parsed_date":"2017-02-25 00:00:00 UTC","id":"2","total_visits":"1638"} +{"parsed_date":"2017-05-24 00:00:00 UTC","id":"2","total_visits":"2406"} +{"parsed_date":"2016-11-22 00:00:00 UTC","id":"2","total_visits":"3942"} +{"parsed_date":"2017-05-06 00:00:00 UTC","id":"2","total_visits":"1383"} +{"parsed_date":"2017-07-02 00:00:00 UTC","id":"2","total_visits":"1895"} +{"parsed_date":"2016-12-01 00:00:00 UTC","id":"2","total_visits":"4200"} +{"parsed_date":"2017-03-16 00:00:00 UTC","id":"2","total_visits":"2409"} +{"parsed_date":"2016-12-12 00:00:00 UTC","id":"2","total_visits":"3433"} +{"parsed_date":"2016-12-25 00:00:00 UTC","id":"2","total_visits":"1386"} +{"parsed_date":"2017-02-26 00:00:00 UTC","id":"2","total_visits":"1643"} +{"parsed_date":"2017-04-28 00:00:00 UTC","id":"2","total_visits":"2411"} +{"parsed_date":"2016-08-11 00:00:00 UTC","id":"2","total_visits":"2667"} +{"parsed_date":"2017-07-20 00:00:00 UTC","id":"2","total_visits":"2668"} +{"parsed_date":"2017-05-21 00:00:00 UTC","id":"2","total_visits":"1645"} +{"parsed_date":"2017-06-17 00:00:00 UTC","id":"2","total_visits":"1391"} +{"parsed_date":"2016-12-29 00:00:00 UTC","id":"2","total_visits":"1647"} +{"parsed_date":"2017-07-17 00:00:00 UTC","id":"2","total_visits":"2671"} +{"parsed_date":"2017-01-16 00:00:00 UTC","id":"2","total_visits":"1906"} +{"parsed_date":"2017-03-03 00:00:00 UTC","id":"2","total_visits":"2162"} +{"parsed_date":"2016-11-14 00:00:00 UTC","id":"2","total_visits":"4466"} +{"parsed_date":"2016-08-30 00:00:00 UTC","id":"2","total_visits":"2675"} +{"parsed_date":"2016-08-27 00:00:00 UTC","id":"2","total_visits":"1654"} +{"parsed_date":"2017-02-09 00:00:00 UTC","id":"2","total_visits":"2678"} +{"parsed_date":"2017-06-03 00:00:00 UTC","id":"2","total_visits":"1399"} +{"parsed_date":"2017-05-07 00:00:00 UTC","id":"2","total_visits":"1400"} +{"parsed_date":"2016-11-02 00:00:00 UTC","id":"2","total_visits":"3960"} +{"parsed_date":"2016-12-15 00:00:00 UTC","id":"2","total_visits":"2937"} +{"parsed_date":"2017-04-01 00:00:00 UTC","id":"2","total_visits":"2170"} +{"parsed_date":"2017-07-21 00:00:00 UTC","id":"2","total_visits":"2427"} +{"parsed_date":"2016-08-06 00:00:00 UTC","id":"2","total_visits":"1663"} +{"parsed_date":"2016-09-01 00:00:00 UTC","id":"2","total_visits":"2687"} +{"parsed_date":"2017-06-28 00:00:00 UTC","id":"2","total_visits":"2687"} +{"parsed_date":"2016-08-20 00:00:00 UTC","id":"2","total_visits":"1664"} +{"parsed_date":"2017-04-26 00:00:00 UTC","id":"2","total_visits":"4224"} +{"parsed_date":"2017-07-09 00:00:00 UTC","id":"2","total_visits":"1921"} +{"parsed_date":"2017-07-28 00:00:00 UTC","id":"2","total_visits":"2433"} +{"parsed_date":"2016-09-19 00:00:00 UTC","id":"2","total_visits":"2689"} +{"parsed_date":"2017-07-24 00:00:00 UTC","id":"2","total_visits":"2436"} +{"parsed_date":"2017-06-13 00:00:00 UTC","id":"2","total_visits":"2181"} +{"parsed_date":"2016-09-15 00:00:00 UTC","id":"2","total_visits":"2949"} +{"parsed_date":"2017-02-03 00:00:00 UTC","id":"2","total_visits":"2182"} +{"parsed_date":"2016-09-10 00:00:00 UTC","id":"2","total_visits":"1671"} +{"parsed_date":"2017-06-09 00:00:00 UTC","id":"2","total_visits":"1927"} +{"parsed_date":"2017-01-11 00:00:00 UTC","id":"2","total_visits":"2185"} +{"parsed_date":"2017-02-19 00:00:00 UTC","id":"2","total_visits":"2187"} +{"parsed_date":"2017-01-17 00:00:00 UTC","id":"2","total_visits":"2443"} +{"parsed_date":"2017-05-12 00:00:00 UTC","id":"2","total_visits":"1932"} +{"parsed_date":"2016-12-16 00:00:00 UTC","id":"2","total_visits":"2956"} +{"parsed_date":"2017-02-01 00:00:00 UTC","id":"2","total_visits":"2445"} +{"parsed_date":"2016-11-26 00:00:00 UTC","id":"2","total_visits":"3213"} +{"parsed_date":"2017-06-02 00:00:00 UTC","id":"2","total_visits":"2190"} +{"parsed_date":"2016-08-05 00:00:00 UTC","id":"2","total_visits":"2702"} +{"parsed_date":"2016-11-01 00:00:00 UTC","id":"2","total_visits":"3728"} +{"parsed_date":"2017-01-05 00:00:00 UTC","id":"2","total_visits":"2193"} +{"parsed_date":"2017-03-08 00:00:00 UTC","id":"2","total_visits":"2449"} +{"parsed_date":"2016-08-28 00:00:00 UTC","id":"2","total_visits":"1682"} +{"parsed_date":"2017-07-04 00:00:00 UTC","id":"2","total_visits":"1938"} +{"parsed_date":"2017-03-10 00:00:00 UTC","id":"2","total_visits":"2194"} +{"parsed_date":"2017-07-07 00:00:00 UTC","id":"2","total_visits":"2450"} +{"parsed_date":"2016-10-29 00:00:00 UTC","id":"2","total_visits":"2964"} +{"parsed_date":"2016-10-13 00:00:00 UTC","id":"2","total_visits":"2964"} +{"parsed_date":"2016-12-04 00:00:00 UTC","id":"2","total_visits":"3220"} +{"parsed_date":"2017-01-21 00:00:00 UTC","id":"2","total_visits":"1685"} +{"parsed_date":"2017-06-29 00:00:00 UTC","id":"2","total_visits":"2709"} +{"parsed_date":"2016-08-29 00:00:00 UTC","id":"2","total_visits":"2454"} +{"parsed_date":"2016-12-19 00:00:00 UTC","id":"2","total_visits":"3222"} +{"parsed_date":"2017-05-30 00:00:00 UTC","id":"2","total_visits":"2199"} +{"parsed_date":"2017-02-10 00:00:00 UTC","id":"2","total_visits":"2199"} +{"parsed_date":"2016-08-31 00:00:00 UTC","id":"2","total_visits":"3223"} +{"parsed_date":"2017-06-18 00:00:00 UTC","id":"2","total_visits":"1432"} +{"parsed_date":"2017-01-12 00:00:00 UTC","id":"2","total_visits":"2203"} +{"parsed_date":"2017-05-18 00:00:00 UTC","id":"2","total_visits":"2715"} +{"parsed_date":"2016-10-23 00:00:00 UTC","id":"2","total_visits":"2971"} +{"parsed_date":"2016-09-04 00:00:00 UTC","id":"2","total_visits":"1692"} +{"parsed_date":"2016-12-10 00:00:00 UTC","id":"2","total_visits":"2207"} +{"parsed_date":"2016-12-11 00:00:00 UTC","id":"2","total_visits":"2208"} +{"parsed_date":"2017-04-11 00:00:00 UTC","id":"2","total_visits":"2464"} +{"parsed_date":"2016-09-21 00:00:00 UTC","id":"2","total_visits":"2720"} +{"parsed_date":"2016-11-06 00:00:00 UTC","id":"2","total_visits":"3232"} +{"parsed_date":"2017-01-26 00:00:00 UTC","id":"2","total_visits":"2209"} +{"parsed_date":"2016-09-12 00:00:00 UTC","id":"2","total_visits":"2465"} +{"parsed_date":"2017-04-21 00:00:00 UTC","id":"2","total_visits":"2210"} +{"parsed_date":"2017-01-06 00:00:00 UTC","id":"2","total_visits":"2210"} +{"parsed_date":"2017-04-04 00:00:00 UTC","id":"2","total_visits":"2978"} +{"parsed_date":"2017-01-22 00:00:00 UTC","id":"2","total_visits":"1700"} +{"parsed_date":"2017-07-26 00:00:00 UTC","id":"2","total_visits":"2725"} +{"parsed_date":"2016-08-18 00:00:00 UTC","id":"2","total_visits":"2725"} +{"parsed_date":"2016-09-27 00:00:00 UTC","id":"2","total_visits":"2727"} +{"parsed_date":"2016-12-02 00:00:00 UTC","id":"2","total_visits":"3751"} +{"parsed_date":"2017-05-05 00:00:00 UTC","id":"2","total_visits":"1960"} +{"parsed_date":"2016-11-19 00:00:00 UTC","id":"2","total_visits":"2984"} +{"parsed_date":"2016-11-09 00:00:00 UTC","id":"2","total_visits":"3752"} +{"parsed_date":"2016-12-05 00:00:00 UTC","id":"2","total_visits":"4265"} +{"parsed_date":"2017-05-11 00:00:00 UTC","id":"2","total_visits":"2218"} +{"parsed_date":"2017-01-25 00:00:00 UTC","id":"2","total_visits":"2986"} +{"parsed_date":"2017-03-11 00:00:00 UTC","id":"2","total_visits":"1707"} +{"parsed_date":"2017-03-30 00:00:00 UTC","id":"2","total_visits":"2731"} +{"parsed_date":"2016-10-20 00:00:00 UTC","id":"2","total_visits":"3755"} +{"parsed_date":"2017-02-07 00:00:00 UTC","id":"2","total_visits":"2476"} +{"parsed_date":"2017-02-22 00:00:00 UTC","id":"2","total_visits":"2477"} +{"parsed_date":"2017-07-23 00:00:00 UTC","id":"2","total_visits":"1966"} +{"parsed_date":"2016-11-03 00:00:00 UTC","id":"2","total_visits":"4014"} +{"parsed_date":"2016-08-01 00:00:00 UTC","id":"2","total_visits":"1711"} +{"parsed_date":"2017-01-13 00:00:00 UTC","id":"2","total_visits":"1967"} +{"parsed_date":"2017-05-19 00:00:00 UTC","id":"2","total_visits":"2223"} +{"parsed_date":"2016-11-20 00:00:00 UTC","id":"2","total_visits":"3247"} +{"parsed_date":"2016-11-25 00:00:00 UTC","id":"2","total_visits":"3759"} +{"parsed_date":"2017-03-25 00:00:00 UTC","id":"2","total_visits":"1712"} +{"parsed_date":"2017-01-27 00:00:00 UTC","id":"2","total_visits":"1969"} +{"parsed_date":"2017-06-26 00:00:00 UTC","id":"2","total_visits":"2226"} +{"parsed_date":"2017-05-25 00:00:00 UTC","id":"2","total_visits":"2228"} +{"parsed_date":"2017-01-31 00:00:00 UTC","id":"2","total_visits":"2229"} +{"parsed_date":"2017-07-13 00:00:00 UTC","id":"2","total_visits":"2741"} +{"parsed_date":"2017-03-15 00:00:00 UTC","id":"2","total_visits":"2486"} +{"parsed_date":"2017-05-28 00:00:00 UTC","id":"2","total_visits":"1463"} +{"parsed_date":"2017-03-09 00:00:00 UTC","id":"2","total_visits":"2231"} +{"parsed_date":"2017-07-15 00:00:00 UTC","id":"2","total_visits":"1721"} +{"parsed_date":"2016-11-24 00:00:00 UTC","id":"2","total_visits":"3770"} +{"parsed_date":"2016-10-05 00:00:00 UTC","id":"2","total_visits":"3770"} +{"parsed_date":"2016-12-31 00:00:00 UTC","id":"2","total_visits":"1211"} +{"parsed_date":"2016-10-02 00:00:00 UTC","id":"2","total_visits":"1724"} +{"parsed_date":"2017-07-22 00:00:00 UTC","id":"2","total_visits":"1724"} +{"parsed_date":"2016-09-11 00:00:00 UTC","id":"2","total_visits":"1725"} +{"parsed_date":"2017-06-15 00:00:00 UTC","id":"2","total_visits":"2237"} +{"parsed_date":"2017-06-05 00:00:00 UTC","id":"2","total_visits":"2493"} +{"parsed_date":"2017-02-06 00:00:00 UTC","id":"2","total_visits":"2238"} +{"parsed_date":"2016-10-15 00:00:00 UTC","id":"2","total_visits":"2495"} +{"parsed_date":"2016-08-21 00:00:00 UTC","id":"2","total_visits":"1730"} +{"parsed_date":"2016-08-23 00:00:00 UTC","id":"2","total_visits":"2754"} +{"parsed_date":"2017-06-30 00:00:00 UTC","id":"2","total_visits":"2499"} +{"parsed_date":"2017-01-18 00:00:00 UTC","id":"2","total_visits":"2245"} +{"parsed_date":"2016-08-10 00:00:00 UTC","id":"2","total_visits":"2757"} +{"parsed_date":"2016-12-08 00:00:00 UTC","id":"2","total_visits":"3013"} +{"parsed_date":"2016-11-28 00:00:00 UTC","id":"2","total_visits":"4807"} +{"parsed_date":"2017-05-22 00:00:00 UTC","id":"2","total_visits":"2248"} +{"parsed_date":"2016-09-20 00:00:00 UTC","id":"2","total_visits":"2760"} +{"parsed_date":"2016-10-06 00:00:00 UTC","id":"2","total_visits":"3016"} +{"parsed_date":"2016-09-06 00:00:00 UTC","id":"2","total_visits":"2508"} +{"parsed_date":"2016-09-03 00:00:00 UTC","id":"2","total_visits":"1741"} +{"parsed_date":"2016-12-06 00:00:00 UTC","id":"2","total_visits":"3021"} +{"parsed_date":"2016-12-24 00:00:00 UTC","id":"2","total_visits":"1231"} +{"parsed_date":"2016-10-28 00:00:00 UTC","id":"2","total_visits":"3791"} +{"parsed_date":"2016-12-30 00:00:00 UTC","id":"2","total_visits":"1232"} +{"parsed_date":"2017-05-29 00:00:00 UTC","id":"2","total_visits":"1745"} +{"parsed_date":"2017-07-10 00:00:00 UTC","id":"2","total_visits":"2769"} +{"parsed_date":"2017-06-22 00:00:00 UTC","id":"2","total_visits":"2258"} +{"parsed_date":"2017-07-19 00:00:00 UTC","id":"2","total_visits":"2514"} +{"parsed_date":"2016-10-03 00:00:00 UTC","id":"2","total_visits":"2514"} +{"parsed_date":"2017-06-14 00:00:00 UTC","id":"2","total_visits":"2517"} +{"parsed_date":"2016-10-22 00:00:00 UTC","id":"2","total_visits":"3029"} +{"parsed_date":"2017-01-23 00:00:00 UTC","id":"2","total_visits":"2262"} +{"parsed_date":"2017-04-24 00:00:00 UTC","id":"2","total_visits":"2263"} +{"parsed_date":"2016-11-10 00:00:00 UTC","id":"2","total_visits":"4055"} +{"parsed_date":"2016-09-26 00:00:00 UTC","id":"2","total_visits":"2776"} +{"parsed_date":"2016-10-19 00:00:00 UTC","id":"2","total_visits":"3544"} +{"parsed_date":"2017-03-04 00:00:00 UTC","id":"2","total_visits":"1753"} +{"parsed_date":"2017-05-26 00:00:00 UTC","id":"2","total_visits":"2009"} +{"parsed_date":"2017-02-13 00:00:00 UTC","id":"2","total_visits":"2266"} +{"parsed_date":"2017-02-18 00:00:00 UTC","id":"2","total_visits":"1755"} +{"parsed_date":"2017-03-02 00:00:00 UTC","id":"2","total_visits":"2267"} +{"parsed_date":"2017-03-31 00:00:00 UTC","id":"2","total_visits":"2268"} +{"parsed_date":"2017-01-10 00:00:00 UTC","id":"2","total_visits":"2268"} +{"parsed_date":"2017-03-29 00:00:00 UTC","id":"2","total_visits":"2525"} +{"parsed_date":"2017-03-27 00:00:00 UTC","id":"2","total_visits":"2525"} +{"parsed_date":"2016-11-23 00:00:00 UTC","id":"2","total_visits":"3805"} +{"parsed_date":"2017-05-27 00:00:00 UTC","id":"2","total_visits":"1502"} +{"parsed_date":"2016-10-24 00:00:00 UTC","id":"2","total_visits":"4063"} +{"parsed_date":"2016-12-14 00:00:00 UTC","id":"2","total_visits":"3040"} +{"parsed_date":"2017-02-11 00:00:00 UTC","id":"2","total_visits":"1761"} +{"parsed_date":"2017-07-27 00:00:00 UTC","id":"2","total_visits":"2529"} +{"parsed_date":"2017-02-17 00:00:00 UTC","id":"2","total_visits":"2785"} +{"parsed_date":"2017-04-15 00:00:00 UTC","id":"2","total_visits":"1506"} +{"parsed_date":"2016-11-05 00:00:00 UTC","id":"2","total_visits":"3042"} +{"parsed_date":"2016-10-04 00:00:00 UTC","id":"2","total_visits":"4322"} +{"parsed_date":"2017-05-13 00:00:00 UTC","id":"2","total_visits":"1251"} +{"parsed_date":"2017-04-16 00:00:00 UTC","id":"2","total_visits":"1507"} +{"parsed_date":"2016-12-28 00:00:00 UTC","id":"2","total_visits":"1763"} +{"parsed_date":"2016-08-15 00:00:00 UTC","id":"2","total_visits":"3043"} +{"parsed_date":"2016-12-03 00:00:00 UTC","id":"2","total_visits":"3044"} +{"parsed_date":"2017-06-27 00:00:00 UTC","id":"2","total_visits":"2789"} +{"parsed_date":"2017-06-24 00:00:00 UTC","id":"2","total_visits":"1510"} +{"parsed_date":"2017-07-16 00:00:00 UTC","id":"2","total_visits":"1766"} +{"parsed_date":"2017-04-09 00:00:00 UTC","id":"2","total_visits":"1766"} +{"parsed_date":"2017-06-07 00:00:00 UTC","id":"2","total_visits":"2279"} +{"parsed_date":"2017-04-17 00:00:00 UTC","id":"2","total_visits":"2279"} +{"parsed_date":"2016-09-28 00:00:00 UTC","id":"2","total_visits":"2535"} +{"parsed_date":"2017-03-26 00:00:00 UTC","id":"2","total_visits":"1768"} +{"parsed_date":"2017-05-10 00:00:00 UTC","id":"2","total_visits":"2024"} +{"parsed_date":"2017-06-08 00:00:00 UTC","id":"2","total_visits":"2280"} +{"parsed_date":"2017-05-08 00:00:00 UTC","id":"2","total_visits":"2025"} +{"parsed_date":"2017-03-13 00:00:00 UTC","id":"2","total_visits":"2537"} +{"parsed_date":"2016-11-17 00:00:00 UTC","id":"2","total_visits":"4074"} +{"parsed_date":"2016-08-25 00:00:00 UTC","id":"2","total_visits":"2539"} +{"parsed_date":"2017-02-16 00:00:00 UTC","id":"2","total_visits":"2539"} +{"parsed_date":"2017-06-16 00:00:00 UTC","id":"2","total_visits":"2028"} +{"parsed_date":"2016-11-16 00:00:00 UTC","id":"2","total_visits":"4334"} +{"parsed_date":"2016-08-17 00:00:00 UTC","id":"2","total_visits":"2800"} +{"parsed_date":"2017-03-19 00:00:00 UTC","id":"2","total_visits":"1776"} +{"parsed_date":"2016-11-29 00:00:00 UTC","id":"2","total_visits":"4337"} +{"parsed_date":"2017-02-05 00:00:00 UTC","id":"2","total_visits":"1522"} +{"parsed_date":"2016-10-31 00:00:00 UTC","id":"2","total_visits":"3827"} +{"parsed_date":"2017-05-31 00:00:00 UTC","id":"2","total_visits":"2292"} +{"parsed_date":"2017-07-18 00:00:00 UTC","id":"2","total_visits":"2804"} +{"parsed_date":"2017-03-12 00:00:00 UTC","id":"2","total_visits":"1781"} +{"parsed_date":"2016-09-09 00:00:00 UTC","id":"2","total_visits":"2549"} +{"parsed_date":"2017-01-14 00:00:00 UTC","id":"2","total_visits":"1526"} +{"parsed_date":"2017-05-04 00:00:00 UTC","id":"2","total_visits":"2806"} +{"parsed_date":"2016-11-07 00:00:00 UTC","id":"2","total_visits":"3832"} +{"parsed_date":"2017-04-07 00:00:00 UTC","id":"2","total_visits":"2297"} +{"parsed_date":"2017-07-12 00:00:00 UTC","id":"2","total_visits":"2554"} +{"parsed_date":"2017-04-13 00:00:00 UTC","id":"2","total_visits":"2300"} +{"parsed_date":"2017-08-01 00:00:00 UTC","id":"2","total_visits":"2556"} +{"parsed_date":"2017-06-04 00:00:00 UTC","id":"2","total_visits":"1534"} +{"parsed_date":"2017-02-12 00:00:00 UTC","id":"2","total_visits":"1790"} +{"parsed_date":"2017-07-03 00:00:00 UTC","id":"2","total_visits":"2046"} +{"parsed_date":"2016-09-30 00:00:00 UTC","id":"2","total_visits":"2303"} +{"parsed_date":"2016-08-08 00:00:00 UTC","id":"2","total_visits":"2815"} + diff --git a/tests/data/time_series_schema.json b/tests/data/time_series_schema.json index 857595b9e6..35473dc0e3 100644 --- a/tests/data/time_series_schema.json +++ b/tests/data/time_series_schema.json @@ -4,6 +4,11 @@ "name": "parsed_date", "type": "TIMESTAMP" }, + { + "mode": "NULLABLE", + "name": "id", + "type": "STRING" + }, { "mode": "NULLABLE", "name": "total_visits", diff --git a/tests/system/large/ml/test_forecasting.py b/tests/system/large/ml/test_forecasting.py index bb53305b94..7c070fd200 100644 --- a/tests/system/large/ml/test_forecasting.py +++ b/tests/system/large/ml/test_forecasting.py @@ -33,38 +33,65 @@ ] -@pytest.fixture(scope="module") -def arima_model(time_series_df_default_index): +def _fit_arima_model(time_series_df_default_index): model = forecasting.ARIMAPlus() X_train = time_series_df_default_index["parsed_date"] y_train = time_series_df_default_index[["total_visits"]] + return model, X_train, y_train + + +@pytest.fixture(scope="module") +def arima_model(time_series_df_default_index): + model, X_train, y_train = _fit_arima_model(time_series_df_default_index) model.fit(X_train, y_train) return model +@pytest.fixture(scope="module") +def arima_model_w_id(time_series_df_default_index): + model, X_train, y_train = _fit_arima_model(time_series_df_default_index) + id_cols = time_series_df_default_index[["id"]] + model.fit(X_train, y_train, id_col=id_cols) + return model + + +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_model_fit_score( dataset_id, new_time_series_df, + new_time_series_df_w_id, arima_model, + arima_model_w_id, + id_col_name, ): - - result = arima_model.score( - new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] - ).to_pandas() + curr_model = arima_model_w_id if id_col_name else arima_model + if id_col_name: + result = curr_model.score( + new_time_series_df_w_id[["parsed_date"]], + new_time_series_df_w_id[["total_visits"]], + id_col=new_time_series_df_w_id[[id_col_name]], + ).to_pandas() + else: + result = curr_model.score( + new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] + ).to_pandas() + expected_columns = [ + "mean_absolute_error", + "mean_squared_error", + "root_mean_squared_error", + "mean_absolute_percentage_error", + "symmetric_mean_absolute_percentage_error", + ] + if id_col_name: + expected_columns.insert(0, id_col_name) utils.check_pandas_df_schema_and_index( result, - columns=[ - "mean_absolute_error", - "mean_squared_error", - "root_mean_squared_error", - "mean_absolute_percentage_error", - "symmetric_mean_absolute_percentage_error", - ], - index=1, + columns=expected_columns, + index=2 if id_col_name else 1, ) # save, load to ensure configuration was kept - reloaded_model = arima_model.to_gbq( + reloaded_model = curr_model.to_gbq( f"{dataset_id}.temp_arima_plus_model", replace=True ) assert ( @@ -72,14 +99,22 @@ def test_arima_plus_model_fit_score( ) -def test_arima_plus_model_fit_summary(dataset_id, arima_model): - result = arima_model.summary().to_pandas() +@pytest.mark.parametrize("id_col_name", [None, "id"]) +def test_arima_plus_model_fit_summary( + dataset_id, arima_model, arima_model_w_id, id_col_name +): + curr_model = arima_model_w_id if id_col_name else arima_model + result = curr_model.summary().to_pandas() + expected_columns = ( + [id_col_name] + ARIMA_EVALUATE_OUTPUT_COL + if id_col_name + else ARIMA_EVALUATE_OUTPUT_COL + ) utils.check_pandas_df_schema_and_index( - result, columns=ARIMA_EVALUATE_OUTPUT_COL, index=1 + result, columns=expected_columns, index=2 if id_col_name else 1 ) - # save, load to ensure configuration was kept - reloaded_model = arima_model.to_gbq( + reloaded_model = curr_model.to_gbq( f"{dataset_id}.temp_arima_plus_model", replace=True ) assert ( @@ -87,17 +122,29 @@ def test_arima_plus_model_fit_summary(dataset_id, arima_model): ) -def test_arima_coefficients(arima_model): - result = arima_model.coef_.to_pandas() +@pytest.mark.parametrize("id_col_name", [None, "id"]) +def test_arima_coefficients(arima_model, arima_model_w_id, id_col_name): + result = ( + arima_model_w_id.coef_.to_pandas() + if id_col_name + else arima_model.coef_.to_pandas() + ) expected_columns = [ "ar_coefficients", "ma_coefficients", "intercept_or_drift", ] - utils.check_pandas_df_schema_and_index(result, columns=expected_columns, index=1) + if id_col_name: + expected_columns.insert(0, id_col_name) + utils.check_pandas_df_schema_and_index( + result, columns=expected_columns, index=2 if id_col_name else 1 + ) -def test_arima_plus_model_fit_params(time_series_df_default_index, dataset_id): +@pytest.mark.parametrize("id_col_name", [None, "id"]) +def test_arima_plus_model_fit_params( + time_series_df_default_index, dataset_id, id_col_name +): model = forecasting.ARIMAPlus( horizon=100, auto_arima=True, @@ -115,7 +162,11 @@ def test_arima_plus_model_fit_params(time_series_df_default_index, dataset_id): X_train = time_series_df_default_index[["parsed_date"]] y_train = time_series_df_default_index["total_visits"] - model.fit(X_train, y_train) + if id_col_name is None: + model.fit(X_train, y_train) + else: + id_cols = time_series_df_default_index[[id_col_name]] + model.fit(X_train, y_train, id_col=id_cols) # save, load to ensure configuration was kept reloaded_model = model.to_gbq(f"{dataset_id}.temp_arima_plus_model", replace=True) diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index c1643776a5..0e8489c513 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -304,6 +304,14 @@ def time_series_bqml_arima_plus_model( return core.BqmlModel(session, model) +@pytest.fixture(scope="session") +def time_series_bqml_arima_plus_model_w_id( + session, time_series_arima_plus_model_name_w_id +) -> core.BqmlModel: + model = session.bqclient.get_model(time_series_arima_plus_model_name_w_id) + return core.BqmlModel(session, model) + + @pytest.fixture(scope="session") def time_series_arima_plus_model( session, time_series_arima_plus_model_name @@ -314,6 +322,16 @@ def time_series_arima_plus_model( ) +@pytest.fixture(scope="session") +def time_series_arima_plus_model_w_id( + session, time_series_arima_plus_model_name_w_id +) -> forecasting.ARIMAPlus: + return cast( + forecasting.ARIMAPlus, + session.read_gbq_model(time_series_arima_plus_model_name_w_id), + ) + + @pytest.fixture(scope="session") def imported_tensorflow_model_path() -> str: return "gs://cloud-training-demos/txtclass/export/exporter/1549825580/*" diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index 2a2e68b230..d1356ef3bd 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -410,11 +410,26 @@ def test_model_generate_text( ) -def test_model_forecast(time_series_bqml_arima_plus_model: core.BqmlModel): +@pytest.mark.parametrize("id_col_name", [None, "id"]) +def test_model_forecast( + time_series_bqml_arima_plus_model: core.BqmlModel, + time_series_bqml_arima_plus_model_w_id: core.BqmlModel, + id_col_name, +): utc = pytz.utc - forecast = time_series_bqml_arima_plus_model.forecast( - {"horizon": 4, "confidence_level": 0.8} - ).to_pandas()[["forecast_timestamp", "forecast_value"]] + forecast_cols = ["forecast_timestamp", "forecast_value"] + if id_col_name: + forecast_cols.insert(0, id_col_name) + + forecast = ( + time_series_bqml_arima_plus_model_w_id.forecast( + {"horizon": 4, "confidence_level": 0.8} + ) + if id_col_name + else time_series_bqml_arima_plus_model.forecast( + {"horizon": 4, "confidence_level": 0.8} + ) + ).to_pandas()[forecast_cols] expected = pd.DataFrame( { "forecast_timestamp": [ @@ -423,13 +438,20 @@ def test_model_forecast(time_series_bqml_arima_plus_model: core.BqmlModel): datetime(2017, 8, 4, tzinfo=utc), datetime(2017, 8, 5, tzinfo=utc), ], - "forecast_value": [2724.472284, 2593.368389, 2353.613034, 1781.623071], + "forecast_value": [2634.796023, 2621.332462, 2396.095463, 1742.878278], } ) expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) + if id_col_name: + expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( + drop=True + ) + expected_expanded.insert(0, "id", ["1", "2", "1", "2", "1", "2", "1", "2"]) + expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") + expected = expected_expanded pd.testing.assert_frame_equal( forecast, expected, diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index 1b3a650388..7d4b072c72 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -16,6 +16,7 @@ import pandas as pd import pyarrow as pa +import pytest import pytz from bigframes.ml import forecasting @@ -35,13 +36,28 @@ ] +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_predict_default( time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + id_col_name, ): utc = pytz.utc - predictions = time_series_arima_plus_model.predict().to_pandas() - assert predictions.shape == (3, 8) + predictions = ( + ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ) + .predict() + .to_pandas() + ) + assert predictions.shape == ((6, 9) if id_col_name else (3, 8)) result = predictions[["forecast_timestamp", "forecast_value"]] + if id_col_name: + result["id"] = predictions[["id"]] + result = result[["id", "forecast_timestamp", "forecast_value"]] + expected = pd.DataFrame( { "forecast_timestamp": [ @@ -49,13 +65,24 @@ def test_arima_plus_predict_default( datetime(2017, 8, 3, tzinfo=utc), datetime(2017, 8, 4, tzinfo=utc), ], - "forecast_value": [2724.472284, 2593.368389, 2353.613034], + "forecast_value": [ + 2634.796023420504, + 2621.332461736945, + 2396.0954626721273, + ], } ) expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) + if id_col_name: + expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( + drop=True + ) + expected_expanded.insert(0, "id", ["1", "2", "1", "2", "1", "2"]) + expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") + expected = expected_expanded pd.testing.assert_frame_equal( result, @@ -65,17 +92,31 @@ def test_arima_plus_predict_default( ) +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_predict_explain_default( time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + id_col_name, ): utc = pytz.utc - predictions = time_series_arima_plus_model.predict_explain().to_pandas() - assert predictions.shape[0] == 369 + predictions = ( + ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ) + .predict_explain() + .to_pandas() + ) + assert predictions.shape[0] == (738 if id_col_name else 369) predictions = predictions[ predictions["time_series_type"] == "forecast" ].reset_index(drop=True) - assert predictions.shape[0] == 3 + assert predictions.shape[0] == (6 if id_col_name else 3) result = predictions[["time_series_timestamp", "time_series_data"]] + if id_col_name: + result["id"] = predictions[["id"]] + result = result[["id", "time_series_timestamp", "time_series_data"]] expected = pd.DataFrame( { "time_series_timestamp": [ @@ -83,7 +124,11 @@ def test_arima_plus_predict_explain_default( datetime(2017, 8, 3, tzinfo=utc), datetime(2017, 8, 4, tzinfo=utc), ], - "time_series_data": [2727.693349, 2595.290749, 2370.86767], + "time_series_data": [ + 2634.796023420504, + 2621.332461736945, + 2396.0954626721273, + ], } ) expected["time_series_data"] = expected["time_series_data"].astype( @@ -92,6 +137,13 @@ def test_arima_plus_predict_explain_default( expected["time_series_timestamp"] = expected["time_series_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) + if id_col_name: + expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( + drop=True + ) + expected_expanded.insert(0, "id", ["1", "2", "1", "2", "1", "2"]) + expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") + expected = expected_expanded pd.testing.assert_frame_equal( result, @@ -101,13 +153,27 @@ def test_arima_plus_predict_explain_default( ) -def test_arima_plus_predict_params(time_series_arima_plus_model: forecasting.ARIMAPlus): +@pytest.mark.parametrize("id_col_name", [None, "id"]) +def test_arima_plus_predict_params( + time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + id_col_name, +): utc = pytz.utc - predictions = time_series_arima_plus_model.predict( - horizon=4, confidence_level=0.9 - ).to_pandas() - assert predictions.shape == (4, 8) + predictions = ( + ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ) + .predict(horizon=4, confidence_level=0.9) + .to_pandas() + ) + assert predictions.shape == ((8, 9) if id_col_name else (4, 8)) result = predictions[["forecast_timestamp", "forecast_value"]] + if id_col_name: + result["id"] = predictions[["id"]] + result = result[["id", "forecast_timestamp", "forecast_value"]] expected = pd.DataFrame( { "forecast_timestamp": [ @@ -116,13 +182,25 @@ def test_arima_plus_predict_params(time_series_arima_plus_model: forecasting.ARI datetime(2017, 8, 4, tzinfo=utc), datetime(2017, 8, 5, tzinfo=utc), ], - "forecast_value": [2724.472284, 2593.368389, 2353.613034, 1781.623071], + "forecast_value": [ + 2634.796023420504, + 2621.332461736945, + 2396.0954626721273, + 1781.623071, + ], } ) expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) + if id_col_name: + expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( + drop=True + ) + expected_expanded.insert(0, "id", ["1", "2", "1", "2", "1", "2", "1", "2"]) + expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") + expected = expected_expanded pd.testing.assert_frame_equal( result, @@ -132,12 +210,21 @@ def test_arima_plus_predict_params(time_series_arima_plus_model: forecasting.ARI ) +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_predict_explain_params( time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + id_col_name, ): - predictions = time_series_arima_plus_model.predict_explain( - horizon=4, confidence_level=0.9 - ).to_pandas() + predictions = ( + ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ) + .predict_explain(horizon=4, confidence_level=0.9) + .to_pandas() + ) assert predictions.shape[0] >= 1 prediction_columns = set(predictions.columns) expected_columns = { @@ -156,24 +243,44 @@ def test_arima_plus_predict_explain_params( "seasonal_period_daily", "holiday_effect", } + if id_col_name: + expected_columns.add("id") assert expected_columns <= prediction_columns +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_detect_anomalies( - time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df + time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + new_time_series_df, + new_time_series_df_w_id, + id_col_name, ): - anomalies = time_series_arima_plus_model.detect_anomalies( - new_time_series_df - ).to_pandas() + anomalies = ( + ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ) + .detect_anomalies( + new_time_series_df_w_id if id_col_name else new_time_series_df + ) + .to_pandas() + ) expected = pd.DataFrame( { "is_anomaly": [False, False, False], - "lower_bound": [2349.301736, 2153.614829, 1849.040192], - "upper_bound": [3099.642833, 3033.12195, 2858.185876], - "anomaly_probability": [0.757824, 0.322559, 0.43011], + "lower_bound": [2229.930578, 2149.645455, 1892.873256], + "upper_bound": [3039.6614686, 3093.019467, 2899.317669], + "anomaly_probability": [0.48545926, 0.3856835, 0.314156], }, ) + if id_col_name: + expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( + drop=True + ) + expected = expected_expanded pd.testing.assert_frame_equal( anomalies[["is_anomaly", "lower_bound", "upper_bound", "anomaly_probability"]], expected, @@ -183,21 +290,40 @@ def test_arima_plus_detect_anomalies( ) +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_detect_anomalies_params( - time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df + time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + new_time_series_df, + new_time_series_df_w_id, + id_col_name, ): - anomalies = time_series_arima_plus_model.detect_anomalies( - new_time_series_df, anomaly_prob_threshold=0.7 - ).to_pandas() + anomalies = ( + ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ) + .detect_anomalies( + new_time_series_df_w_id if id_col_name else new_time_series_df, + anomaly_prob_threshold=0.7, + ) + .to_pandas() + ) expected = pd.DataFrame( { - "is_anomaly": [True, False, False], - "lower_bound": [2525.5363, 2360.1870, 2086.0609], - "upper_bound": [2923.408256, 2826.54981, 2621.165188], - "anomaly_probability": [0.757824, 0.322559, 0.43011], + "is_anomaly": [False, False, False], + "lower_bound": [2420.11419, 2360.1870, 2086.0609], + "upper_bound": [2849.47785, 2826.54981, 2621.165188], + "anomaly_probability": [0.485459, 0.385683, 0.314156], }, ) + if id_col_name: + expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( + drop=True + ) + expected = expected_expanded pd.testing.assert_frame_equal( anomalies[["is_anomaly", "lower_bound", "upper_bound", "anomaly_probability"]], expected, @@ -207,22 +333,40 @@ def test_arima_plus_detect_anomalies_params( ) +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_score( - time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df + time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + new_time_series_df, + new_time_series_df_w_id, + id_col_name, ): - result = time_series_arima_plus_model.score( - new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] - ).to_pandas() + if id_col_name: + result = time_series_arima_plus_model_w_id.score( + new_time_series_df_w_id[["parsed_date"]], + new_time_series_df_w_id[["total_visits"]], + new_time_series_df_w_id[["id"]], + ).to_pandas() + else: + result = time_series_arima_plus_model.score( + new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] + ).to_pandas() expected = pd.DataFrame( { - "mean_absolute_error": [154.742547], - "mean_squared_error": [26844.868855], - "root_mean_squared_error": [163.844038], - "mean_absolute_percentage_error": [6.189702], - "symmetric_mean_absolute_percentage_error": [6.097155], + "mean_absolute_error": [120.0110074], + "mean_squared_error": [14562.5623594], + "root_mean_squared_error": [120.675442], + "mean_absolute_percentage_error": [4.80044], + "symmetric_mean_absolute_percentage_error": [4.744332], }, dtype="Float64", ) + if id_col_name: + expected_expanded = pd.concat([expected, expected], ignore_index=True) + expected_expanded["id"] = ["2", "1"] + expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") + expected_expanded = expected_expanded[["id"] + list(expected.columns)] + expected = expected_expanded pd.testing.assert_frame_equal( result, expected, @@ -231,38 +375,82 @@ def test_arima_plus_score( ) -def test_arima_plus_summary(time_series_arima_plus_model: forecasting.ARIMAPlus): - result = time_series_arima_plus_model.summary() - assert result.shape == (1, 12) - assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) +@pytest.mark.parametrize("id_col_name", [None, "id"]) +def test_arima_plus_summary( + time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + id_col_name, +): + result = ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ).summary() + assert result.shape == ((2, 13) if id_col_name else (1, 12)) + expected_columns = ( + [id_col_name] + ARIMA_EVALUATE_OUTPUT_COL + if id_col_name + else ARIMA_EVALUATE_OUTPUT_COL + ) + assert all(column in result.columns for column in expected_columns) +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_summary_show_all_candidates( time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + id_col_name, ): - result = time_series_arima_plus_model.summary( + result = ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ).summary( show_all_candidate_models=True, ) assert result.shape[0] > 1 - assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) + expected_columns = ( + [id_col_name] + ARIMA_EVALUATE_OUTPUT_COL + if id_col_name + else ARIMA_EVALUATE_OUTPUT_COL + ) + assert all(column in result.columns for column in expected_columns) +@pytest.mark.parametrize("id_col_name", [None, "id"]) def test_arima_plus_score_series( - time_series_arima_plus_model: forecasting.ARIMAPlus, new_time_series_df + time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + new_time_series_df, + new_time_series_df_w_id, + id_col_name, ): - result = time_series_arima_plus_model.score( - new_time_series_df["parsed_date"], new_time_series_df["total_visits"] - ).to_pandas() + if id_col_name: + result = time_series_arima_plus_model_w_id.score( + new_time_series_df_w_id["parsed_date"], + new_time_series_df_w_id["total_visits"], + new_time_series_df_w_id["id"], + ).to_pandas() + else: + result = time_series_arima_plus_model.score( + new_time_series_df["parsed_date"], new_time_series_df["total_visits"] + ).to_pandas() expected = pd.DataFrame( { - "mean_absolute_error": [154.742547], - "mean_squared_error": [26844.868855], - "root_mean_squared_error": [163.844038], - "mean_absolute_percentage_error": [6.189702], - "symmetric_mean_absolute_percentage_error": [6.097155], + "mean_absolute_error": [120.0110074], + "mean_squared_error": [14562.5623594], + "root_mean_squared_error": [120.675442], + "mean_absolute_percentage_error": [4.80044], + "symmetric_mean_absolute_percentage_error": [4.744332], }, dtype="Float64", ) + if id_col_name: + expected_expanded = pd.concat([expected, expected], ignore_index=True) + expected_expanded["id"] = ["2", "1"] + expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") + expected_expanded = expected_expanded[["id"] + list(expected.columns)] + expected = expected_expanded pd.testing.assert_frame_equal( result, expected, @@ -271,7 +459,21 @@ def test_arima_plus_score_series( ) -def test_arima_plus_summary_series(time_series_arima_plus_model: forecasting.ARIMAPlus): - result = time_series_arima_plus_model.summary() - assert result.shape == (1, 12) - assert all(column in result.columns for column in ARIMA_EVALUATE_OUTPUT_COL) +@pytest.mark.parametrize("id_col_name", [None, "id"]) +def test_arima_plus_summary_series( + time_series_arima_plus_model: forecasting.ARIMAPlus, + time_series_arima_plus_model_w_id: forecasting.ARIMAPlus, + id_col_name, +): + result = ( + time_series_arima_plus_model_w_id + if id_col_name + else time_series_arima_plus_model + ).summary() + assert result.shape == ((2, 13) if id_col_name else (1, 12)) + expected_columns = ( + [id_col_name] + ARIMA_EVALUATE_OUTPUT_COL + if id_col_name + else ARIMA_EVALUATE_OUTPUT_COL + ) + a From 818198ef8b26e69909e207b32f410aa4af5a7912 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Fri, 24 Jan 2025 04:34:18 +0000 Subject: [PATCH 02/10] I have resolve all conflicts after manual porting --- bigframes/ml/base.py | 1 - bigframes/ml/forecasting.py | 7 +++---- tests/data/time_series.jsonl | 1 - tests/system/small/ml/test_forecasting.py | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index f6f97ad03b..ec51369708 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -221,7 +221,6 @@ def fit( return self._fit(X, y, id_col=id_col) - class TrainableWithEvaluationPredictor(TrainablePredictor): """A BigQuery DataFrames ML Model base class that can be used to fit and predict outputs. diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 4f3cd799f0..acc4e98680 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -209,9 +209,7 @@ def _fit( X, y = utils.batch_convert_to_dataframe(X, y) if X.columns.size < 1: - raise ValueError( - "Time series timestamp input X contain at least 1 column." - ) + raise ValueError("Time series timestamp input X contain at least 1 column.") if y.columns.size != 1: raise ValueError("Time series data input y must only contain 1 column.") @@ -362,6 +360,7 @@ def score( self, X: utils.ArrayType, y: utils.ArrayType, + id_col: Optional[utils.ArrayType] = None, ) -> bpd.DataFrame: """Calculate evaluation metrics of the model. @@ -398,7 +397,7 @@ def score( (id_col,) = utils.batch_convert_to_dataframe(id_col) input_data = input_data.join(id_col, how="outer") - return self._bqml_model.evaluate(input_data) + return self._bqml_model.evaluate(input_data) def summary( self, diff --git a/tests/data/time_series.jsonl b/tests/data/time_series.jsonl index c6ea2a46ed..329e5a8b61 100644 --- a/tests/data/time_series.jsonl +++ b/tests/data/time_series.jsonl @@ -730,4 +730,3 @@ {"parsed_date":"2017-07-03 00:00:00 UTC","id":"2","total_visits":"2046"} {"parsed_date":"2016-09-30 00:00:00 UTC","id":"2","total_visits":"2303"} {"parsed_date":"2016-08-08 00:00:00 UTC","id":"2","total_visits":"2815"} - diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index 7d4b072c72..c65fb3cd1f 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -476,4 +476,4 @@ def test_arima_plus_summary_series( if id_col_name else ARIMA_EVALUATE_OUTPUT_COL ) - a + assert all(column in result.columns for column in expected_columns) From 961a1973278d2678de9b84f5bf388ff0e9ecfe1e Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Fri, 24 Jan 2025 23:01:37 +0000 Subject: [PATCH 03/10] use inherritance for arima plus model, and add sql models --- bigframes/ml/base.py | 50 ++++++------------------ tests/system/conftest.py | 84 +++++++++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 53 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index ec51369708..c353e47f3a 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -165,60 +165,32 @@ def fit( return self._fit(X, y) -''' class SupervisedTrainableWithIdColPredictor(SupervisedTrainablePredictor): """Inherits from SupervisedTrainablePredictor, but adds an optional id_col parameter to fit().""" - def __init__(self, id_col: Optional[utils.ArrayType]=None): + + def __init__(self): super().__init__() - self.id_col = id_col - self._bqml_model = None + self.id_col = None def _fit( self, X: utils.ArrayType, y: utils.ArrayType, - transforms=None - ): - return self._fit(X, y, transforms=transforms, id_col=self.id_col) - - def score( - self, - X: utils.ArrayType, - y: utils.ArrayType + transforms=None, + id_col: Optional[utils.ArrayType] = None, ): - return self.score(X, y) -''' - - -class TrainableWithIdColPredictor(TrainablePredictor): - """A BigQuery DataFrames ML Model base class that can be used to fit and predict outputs. - Additional id_col can be provided in the fit phase.""" - - @abc.abstractmethod - def _fit(self, X, y, transforms=None, id_col=None): - pass - - @abc.abstractmethod - def score(self, X, y): - pass - - -class SupervisedTrainableWithIdColPredictor(TrainableWithIdColPredictor): - """A BigQuery DataFrames ML Supervised Model base class that can be used to fit and predict outputs. - Need to provide both X and y in supervised tasks. - Additional id_col can be provided in the fit phase. - """ - - _T = TypeVar("_T", bound="SupervisedTrainableWithIdColPredictor") + return self def fit( - self: _T, + self, X: utils.ArrayType, y: utils.ArrayType, + transforms=None, id_col: Optional[utils.ArrayType] = None, - ) -> _T: - return self._fit(X, y, id_col=id_col) + ): + self.id_col = id_col + return self._fit(X, y, transforms=transforms, id_col=self.id_col) class TrainableWithEvaluationPredictor(TrainablePredictor): diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 251b9da4ac..29234bc4ef 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -772,6 +772,31 @@ def new_time_series_df(session, new_time_series_pandas_df): return session.read_pandas(new_time_series_pandas_df) +@pytest.fixture(scope="session") +def new_time_series_pandas_df_w_id(): + """Additional data matching the time series dataset. The values are dummy ones used to basically check the prediction scores.""" + utc = pytz.utc + return pd.DataFrame( + { + "parsed_date": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + ], + "id": ["1", "2", "1", "2", "1", "2"], + "total_visits": [2500, 2500, 2500, 2500, 2500, 2500], + } + ) + + +@pytest.fixture(scope="session") +def new_time_series_df_w_id(session, new_time_series_pandas_df_w_id): + return session.read_pandas(new_time_series_pandas_df_w_id) + + @pytest.fixture(scope="session") def penguins_pandas_df_default_index() -> pd.DataFrame: """Consistently ordered pandas dataframe for penguins test data""" @@ -1015,12 +1040,34 @@ def penguins_xgbregressor_model_name( return model_name +def _get_or_create_arima_plus_model( + session: bigframes.Session, dataset_id_permanent, sql +) -> str: + """Internal helper to compute a model name by hasing the given SQL. + attempst to retreive the model, create it if not exist. + retursn the fully qualitifed model""" + + # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited + model_name = f"{dataset_id_permanent}.time_series_arima_plus_{hashlib.md5(sql.encode()).hexdigest()}" + sql = sql.replace("$model_name", model_name) + try: + session.bqclient.get_model(model_name) + except google.cloud.exceptions.NotFound: + logging.info( + "time_series_arima_plus_model fixture was not found in the permanent dataset, regenerating it..." + ) + session.bqclient.query(sql).result() + finally: + return model_name + + @pytest.fixture(scope="session") def time_series_arima_plus_model_name( session: bigframes.Session, dataset_id_permanent, time_series_table_id ) -> str: """Provides a pretrained model as a test fixture that is cached across test runs. - This lets us run system tests without having to wait for a model.fit(...)""" + This lets us run system tests without having to wait for a model.fit(...). + This version does not include time_series_id_col.""" sql = f""" CREATE OR REPLACE MODEL `$model_name` OPTIONS ( @@ -1028,21 +1075,30 @@ def time_series_arima_plus_model_name( time_series_timestamp_col = 'parsed_date', time_series_data_col = 'total_visits' ) AS SELECT - * + parsed_date, + total_visits FROM `{time_series_table_id}`""" - # We use the SQL hash as the name to ensure the model is regenerated if this fixture is edited - model_name = f"{dataset_id_permanent}.time_series_arima_plus_{hashlib.md5(sql.encode()).hexdigest()}" - sql = sql.replace("$model_name", model_name) + return _get_or_create_arima_plus_model(session, dataset_id_permanent, sql) - try: - session.bqclient.get_model(model_name) - except google.cloud.exceptions.NotFound: - logging.info( - "time_series_arima_plus_model fixture was not found in the permanent dataset, regenerating it..." - ) - session.bqclient.query(sql).result() - finally: - return model_name + +@pytest.fixture(scope="session") +def time_series_arima_plus_model_name_w_id( + session: bigframes.Session, dataset_id_permanent, time_series_table_id +) -> str: + """Provides a pretrained model as a test fixture that is cached across test runs. + This lets us run system tests without having to wait for a model.fit(...). + This version includes time_series_id_col.""" + sql = f""" +CREATE OR REPLACE MODEL `$model_name` +OPTIONS ( + model_type='ARIMA_PLUS', + time_series_timestamp_col = 'parsed_date', + time_series_data_col = 'total_visits', + time_series_id_col = 'id' +) AS SELECT + * +FROM `{time_series_table_id}`""" + return _get_or_create_arima_plus_model(session, dataset_id_permanent, sql) @pytest.fixture(scope="session") From d56188a60c5466038d44e52d41e69623617308dd Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Fri, 24 Jan 2025 23:14:09 +0000 Subject: [PATCH 04/10] resolve unexpected indent for docstring --- bigframes/ml/forecasting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index acc4e98680..c0a0742af4 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -379,9 +379,9 @@ def score( A dataframe or series only contains 1 column as evaluation numeric values. id_col (Optional[bigframes.dataframe.DataFrame], - or Optional[bigframes.series.Series], - or Optional[pandas.core.frame.DataFrame], - or Optional[pandas.core.series.Series], default None): + or Optional[bigframes.series.Series], + or Optional[pandas.core.frame.DataFrame], + or Optional[pandas.core.series.Series], default None): An optional dataframe or series contains at least 1 column as evualtion id column. From 79cf06671e27738f7b3dbaea5572adae3643c3b1 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Sat, 25 Jan 2025 00:12:55 +0000 Subject: [PATCH 05/10] fix docstring --- bigframes/ml/forecasting.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index c0a0742af4..f8754331b8 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -188,20 +188,20 @@ def _fit( """Fit the model to training data. Args: - X (bigframes.dataframe.DataFrame, or bigframes.series.Series, - or pandas.core.frame.DataFrame or pandas.core.series.Series): - A dataframe or series of trainging timestamp. - y (bigframes.dataframe.DataFrame, or bigframes.series.Series, - or pandas.core.frame.DataFrame or pandas.core.series.Series): - Target values for training. + X (bigframes.dataframe.DataFrame, bigframes.series.Series, + pandas.core.frame.DataFrame, or pandas.core.series.Series): + A dataframe or series of trainging timestamp. + y (bigframes.dataframe.DataFrame, bigframes.series.Series, + pandas.core.frame.DataFrame, or pandas.core.series.Series): + Target values for training. transforms (Optional[List[str]], default None): Do not use. Internal param to be deprecated. Use bigframes.ml.pipeline instead. id_col (Optional[bigframes.dataframe.DataFrame] - or Optional[bigframes.series.Series], - or Optional[pandas.core.frame.DataFrame], - or Optional[pandas.core.frame.Series], default None): - An optional dataframe or series of training id col. + Optional[bigframes.series.Series], + Optional[pandas.core.frame.DataFrame], or + Optional[pandas.core.frame.Series], default None): + An optional dataframe or series of training id col. Returns: ARIMAPlus: Fitted estimator. @@ -379,11 +379,11 @@ def score( A dataframe or series only contains 1 column as evaluation numeric values. id_col (Optional[bigframes.dataframe.DataFrame], - or Optional[bigframes.series.Series], - or Optional[pandas.core.frame.DataFrame], - or Optional[pandas.core.series.Series], default None): - An optional dataframe or series contains at least 1 column as - evualtion id column. + Optional[bigframes.series.Series], + Optional[pandas.core.frame.DataFrame], + or Optional[pandas.core.series.Series], default None): + An optional dataframe or series contains at least 1 column as + evualtion id column. Returns: bigframes.dataframe.DataFrame: A DataFrame as evaluation result. From 9d8a1a66efc531f5cd46db80ed5fde41d9de5378 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Sat, 25 Jan 2025 00:28:50 +0000 Subject: [PATCH 06/10] fix docstring --- bigframes/ml/forecasting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index f8754331b8..8e0de634b4 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -380,10 +380,10 @@ def score( evaluation numeric values. id_col (Optional[bigframes.dataframe.DataFrame], Optional[bigframes.series.Series], - Optional[pandas.core.frame.DataFrame], - or Optional[pandas.core.series.Series], default None): + Optional[pandas.core.frame.DataFrame], or + Optional[pandas.core.series.Series], default None): An optional dataframe or series contains at least 1 column as - evualtion id column. + evaluation id column. Returns: bigframes.dataframe.DataFrame: A DataFrame as evaluation result. From 1d502f7de64c77942adc66a3c9f53ed761f9bc6d Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Sat, 25 Jan 2025 00:51:29 +0000 Subject: [PATCH 07/10] fix docstring --- bigframes/ml/forecasting.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 8e0de634b4..1a18175363 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -188,20 +188,20 @@ def _fit( """Fit the model to training data. Args: - X (bigframes.dataframe.DataFrame, bigframes.series.Series, - pandas.core.frame.DataFrame, or pandas.core.series.Series): + X (bigframes.dataframe.DataFrame, or bigframes.series.Series, + or pandas.core.frame.DataFrame, or pandas.core.series.Series): A dataframe or series of trainging timestamp. - y (bigframes.dataframe.DataFrame, bigframes.series.Series, - pandas.core.frame.DataFrame, or pandas.core.series.Series): + y (bigframes.dataframe.DataFrame, or bigframes.series.Series, + or pandas.core.frame.DataFrame, or pandas.core.series.Series): Target values for training. transforms (Optional[List[str]], default None): Do not use. Internal param to be deprecated. Use bigframes.ml.pipeline instead. id_col (Optional[bigframes.dataframe.DataFrame] - Optional[bigframes.series.Series], - Optional[pandas.core.frame.DataFrame], or - Optional[pandas.core.frame.Series], default None): - An optional dataframe or series of training id col. + or Optional[bigframes.series.Series], + or Optional[pandas.core.frame.DataFrame], + or Optional[pandas.core.frame.Series], default None): + An optional dataframe or series of training id col. Returns: ARIMAPlus: Fitted estimator. @@ -371,19 +371,21 @@ def score( for the outputs relevant to this model type. Args: - X (bigframes.dataframe.DataFrame or bigframes.series.Series or pandas.core.frame.DataFrame or pandas.core.series.Series): + X (bigframes.dataframe.DataFrame or bigframes.series.Series + or pandas.core.frame.DataFrame or pandas.core.series.Series): A dataframe or series only contains 1 column as evaluation timestamp. The timestamp must be within the horizon of the model, which by default is 1000 data points. - y (bigframes.dataframe.DataFrame or bigframes.series.Series or pandas.core.frame.DataFrame or pandas.core.series.Series): + y (bigframes.dataframe.DataFrame or bigframes.series.Series + or pandas.core.frame.DataFrame or pandas.core.series.Series): A dataframe or series only contains 1 column as evaluation numeric values. id_col (Optional[bigframes.dataframe.DataFrame], - Optional[bigframes.series.Series], - Optional[pandas.core.frame.DataFrame], or - Optional[pandas.core.series.Series], default None): - An optional dataframe or series contains at least 1 column as - evaluation id column. + or Optional[bigframes.series.Series], + or Optional[pandas.core.frame.DataFrame], + or Optional[pandas.core.series.Series], default None): + An optional dataframe or series contains at least 1 column as + evaluation id column. Returns: bigframes.dataframe.DataFrame: A DataFrame as evaluation result. From f5d082d5323481094cadf337996940b02f0fd8f2 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Sat, 25 Jan 2025 02:10:53 +0000 Subject: [PATCH 08/10] remove commas from docstring arg keys to let `nox -s docs` pass --- bigframes/ml/forecasting.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 1a18175363..5e2bf97ecd 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -190,16 +190,16 @@ def _fit( Args: X (bigframes.dataframe.DataFrame, or bigframes.series.Series, or pandas.core.frame.DataFrame, or pandas.core.series.Series): - A dataframe or series of trainging timestamp. + A dataframe or series of trainging timestamp. y (bigframes.dataframe.DataFrame, or bigframes.series.Series, or pandas.core.frame.DataFrame, or pandas.core.series.Series): - Target values for training. + Target values for training. transforms (Optional[List[str]], default None): Do not use. Internal param to be deprecated. Use bigframes.ml.pipeline instead. id_col (Optional[bigframes.dataframe.DataFrame] - or Optional[bigframes.series.Series], - or Optional[pandas.core.frame.DataFrame], + or Optional[bigframes.series.Series] + or Optional[pandas.core.frame.DataFrame] or Optional[pandas.core.frame.Series], default None): An optional dataframe or series of training id col. @@ -380,9 +380,9 @@ def score( or pandas.core.frame.DataFrame or pandas.core.series.Series): A dataframe or series only contains 1 column as evaluation numeric values. - id_col (Optional[bigframes.dataframe.DataFrame], - or Optional[bigframes.series.Series], - or Optional[pandas.core.frame.DataFrame], + id_col (Optional[bigframes.dataframe.DataFrame] + or Optional[bigframes.series.Series] + or Optional[pandas.core.frame.DataFrame] or Optional[pandas.core.series.Series], default None): An optional dataframe or series contains at least 1 column as evaluation id column. From 0acc9cd9512f5ea089bc30564d4f6b2856b617ad Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Tue, 28 Jan 2025 00:04:10 +0000 Subject: [PATCH 09/10] change the testcase expected results to improve readability --- bigframes/ml/forecasting.py | 12 +- tests/system/small/ml/test_core.py | 57 ++-- tests/system/small/ml/test_forecasting.py | 361 +++++++++++++++------- 3 files changed, 289 insertions(+), 141 deletions(-) diff --git a/bigframes/ml/forecasting.py b/bigframes/ml/forecasting.py index 5e2bf97ecd..7aa8ba5a5f 100644 --- a/bigframes/ml/forecasting.py +++ b/bigframes/ml/forecasting.py @@ -188,8 +188,8 @@ def _fit( """Fit the model to training data. Args: - X (bigframes.dataframe.DataFrame, or bigframes.series.Series, - or pandas.core.frame.DataFrame, or pandas.core.series.Series): + X (bigframes.dataframe.DataFrame or bigframes.series.Series, + or pandas.core.frame.DataFrame or pandas.core.series.Series): A dataframe or series of trainging timestamp. y (bigframes.dataframe.DataFrame, or bigframes.series.Series, or pandas.core.frame.DataFrame, or pandas.core.series.Series): @@ -200,7 +200,8 @@ def _fit( id_col (Optional[bigframes.dataframe.DataFrame] or Optional[bigframes.series.Series] or Optional[pandas.core.frame.DataFrame] - or Optional[pandas.core.frame.Series], default None): + or Optional[pandas.core.frame.Series] + or None, default None): An optional dataframe or series of training id col. Returns: @@ -208,7 +209,7 @@ def _fit( """ X, y = utils.batch_convert_to_dataframe(X, y) - if X.columns.size < 1: + if X.columns.size != 1: raise ValueError("Time series timestamp input X contain at least 1 column.") if y.columns.size != 1: raise ValueError("Time series data input y must only contain 1 column.") @@ -383,7 +384,8 @@ def score( id_col (Optional[bigframes.dataframe.DataFrame] or Optional[bigframes.series.Series] or Optional[pandas.core.frame.DataFrame] - or Optional[pandas.core.series.Series], default None): + or Optional[pandas.core.series.Series] + or None, default None): An optional dataframe or series contains at least 1 column as evaluation id column. diff --git a/tests/system/small/ml/test_core.py b/tests/system/small/ml/test_core.py index d1356ef3bd..1c2591b90a 100644 --- a/tests/system/small/ml/test_core.py +++ b/tests/system/small/ml/test_core.py @@ -430,28 +430,49 @@ def test_model_forecast( {"horizon": 4, "confidence_level": 0.8} ) ).to_pandas()[forecast_cols] - expected = pd.DataFrame( - { - "forecast_timestamp": [ - datetime(2017, 8, 2, tzinfo=utc), - datetime(2017, 8, 3, tzinfo=utc), - datetime(2017, 8, 4, tzinfo=utc), - datetime(2017, 8, 5, tzinfo=utc), - ], - "forecast_value": [2634.796023, 2621.332462, 2396.095463, 1742.878278], - } - ) + if id_col_name: + expected = pd.DataFrame( + { + "id": ["1", "2", "1", "2", "1", "2", "1", "2"], + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 5, tzinfo=utc), + datetime(2017, 8, 5, tzinfo=utc), + ], + "forecast_value": [ + 2634.796023, + 2634.796023, + 2621.332462, + 2621.332462, + 2396.095463, + 2396.095463, + 1742.878278, + 1742.878278, + ], + } + ) + expected["id"] = expected["id"].astype("string[pyarrow]") + else: + expected = pd.DataFrame( + { + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 5, tzinfo=utc), + ], + "forecast_value": [2634.796023, 2621.332462, 2396.095463, 1742.878278], + } + ) expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) - if id_col_name: - expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( - drop=True - ) - expected_expanded.insert(0, "id", ["1", "2", "1", "2", "1", "2", "1", "2"]) - expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") - expected = expected_expanded pd.testing.assert_frame_equal( forecast, expected, diff --git a/tests/system/small/ml/test_forecasting.py b/tests/system/small/ml/test_forecasting.py index c65fb3cd1f..d1b6b18fbe 100644 --- a/tests/system/small/ml/test_forecasting.py +++ b/tests/system/small/ml/test_forecasting.py @@ -58,31 +58,48 @@ def test_arima_plus_predict_default( result["id"] = predictions[["id"]] result = result[["id", "forecast_timestamp", "forecast_value"]] - expected = pd.DataFrame( - { - "forecast_timestamp": [ - datetime(2017, 8, 2, tzinfo=utc), - datetime(2017, 8, 3, tzinfo=utc), - datetime(2017, 8, 4, tzinfo=utc), - ], - "forecast_value": [ - 2634.796023420504, - 2621.332461736945, - 2396.0954626721273, - ], - } - ) + if id_col_name: + expected = pd.DataFrame( + { + "id": ["1", "2", "1", "2", "1", "2"], + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + ], + "forecast_value": [ + 2634.796023, + 2634.796023, + 2621.332461, + 2621.332461, + 2396.095462, + 2396.095462, + ], + } + ) + expected["id"] = expected["id"].astype("string[pyarrow]") + else: + expected = pd.DataFrame( + { + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + ], + "forecast_value": [ + 2634.796023, + 2621.332461, + 2396.095462, + ], + } + ) expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) - if id_col_name: - expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( - drop=True - ) - expected_expanded.insert(0, "id", ["1", "2", "1", "2", "1", "2"]) - expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") - expected = expected_expanded pd.testing.assert_frame_equal( result, @@ -117,33 +134,50 @@ def test_arima_plus_predict_explain_default( if id_col_name: result["id"] = predictions[["id"]] result = result[["id", "time_series_timestamp", "time_series_data"]] - expected = pd.DataFrame( - { - "time_series_timestamp": [ - datetime(2017, 8, 2, tzinfo=utc), - datetime(2017, 8, 3, tzinfo=utc), - datetime(2017, 8, 4, tzinfo=utc), - ], - "time_series_data": [ - 2634.796023420504, - 2621.332461736945, - 2396.0954626721273, - ], - } - ) + if id_col_name: + expected = pd.DataFrame( + { + "id": ["1", "2", "1", "2", "1", "2"], + "time_series_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + ], + "time_series_data": [ + 2634.796023, + 2634.796023, + 2621.332461, + 2621.332461, + 2396.095462, + 2396.095462, + ], + } + ) + expected["id"] = expected["id"].astype("string[pyarrow]") + else: + expected = pd.DataFrame( + { + "time_series_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + ], + "time_series_data": [ + 2634.796023, + 2621.332461, + 2396.095462, + ], + } + ) expected["time_series_data"] = expected["time_series_data"].astype( pd.Float64Dtype() ) expected["time_series_timestamp"] = expected["time_series_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) - if id_col_name: - expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( - drop=True - ) - expected_expanded.insert(0, "id", ["1", "2", "1", "2", "1", "2"]) - expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") - expected = expected_expanded pd.testing.assert_frame_equal( result, @@ -174,33 +208,55 @@ def test_arima_plus_predict_params( if id_col_name: result["id"] = predictions[["id"]] result = result[["id", "forecast_timestamp", "forecast_value"]] - expected = pd.DataFrame( - { - "forecast_timestamp": [ - datetime(2017, 8, 2, tzinfo=utc), - datetime(2017, 8, 3, tzinfo=utc), - datetime(2017, 8, 4, tzinfo=utc), - datetime(2017, 8, 5, tzinfo=utc), - ], - "forecast_value": [ - 2634.796023420504, - 2621.332461736945, - 2396.0954626721273, - 1781.623071, - ], - } - ) + + if id_col_name: + expected = pd.DataFrame( + { + "id": ["1", "2", "1", "2", "1", "2", "1", "2"], + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 5, tzinfo=utc), + datetime(2017, 8, 5, tzinfo=utc), + ], + "forecast_value": [ + 2634.796023, + 2634.796023, + 2621.332461, + 2621.332461, + 2396.095462, + 2396.095462, + 1781.623071, + 1781.623071, + ], + } + ) + expected["id"] = expected["id"].astype("string[pyarrow]") + else: + expected = pd.DataFrame( + { + "forecast_timestamp": [ + datetime(2017, 8, 2, tzinfo=utc), + datetime(2017, 8, 3, tzinfo=utc), + datetime(2017, 8, 4, tzinfo=utc), + datetime(2017, 8, 5, tzinfo=utc), + ], + "forecast_value": [ + 2634.796023, + 2621.332461, + 2396.095462, + 1781.623071, + ], + } + ) expected["forecast_value"] = expected["forecast_value"].astype(pd.Float64Dtype()) expected["forecast_timestamp"] = expected["forecast_timestamp"].astype( pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ) - if id_col_name: - expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( - drop=True - ) - expected_expanded.insert(0, "id", ["1", "2", "1", "2", "1", "2", "1", "2"]) - expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") - expected = expected_expanded pd.testing.assert_frame_equal( result, @@ -268,19 +324,45 @@ def test_arima_plus_detect_anomalies( .to_pandas() ) - expected = pd.DataFrame( - { - "is_anomaly": [False, False, False], - "lower_bound": [2229.930578, 2149.645455, 1892.873256], - "upper_bound": [3039.6614686, 3093.019467, 2899.317669], - "anomaly_probability": [0.48545926, 0.3856835, 0.314156], - }, - ) if id_col_name: - expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( - drop=True + expected = pd.DataFrame( + { + "is_anomaly": [False, False, False, False, False, False], + "lower_bound": [ + 2229.930578, + 2229.930578, + 2149.645455, + 2149.645455, + 1892.873256, + 1892.873256, + ], + "upper_bound": [ + 3039.6614686, + 3039.6614686, + 3093.019467, + 3093.019467, + 2899.317669, + 2899.317669, + ], + "anomaly_probability": [ + 0.48545926, + 0.48545926, + 0.3856835, + 0.3856835, + 0.314156, + 0.314156, + ], + }, + ) + else: + expected = pd.DataFrame( + { + "is_anomaly": [False, False, False], + "lower_bound": [2229.930578, 2149.645455, 1892.873256], + "upper_bound": [3039.6614686, 3093.019467, 2899.317669], + "anomaly_probability": [0.48545926, 0.3856835, 0.314156], + }, ) - expected = expected_expanded pd.testing.assert_frame_equal( anomalies[["is_anomaly", "lower_bound", "upper_bound", "anomaly_probability"]], expected, @@ -310,20 +392,45 @@ def test_arima_plus_detect_anomalies_params( ) .to_pandas() ) - - expected = pd.DataFrame( - { - "is_anomaly": [False, False, False], - "lower_bound": [2420.11419, 2360.1870, 2086.0609], - "upper_bound": [2849.47785, 2826.54981, 2621.165188], - "anomaly_probability": [0.485459, 0.385683, 0.314156], - }, - ) if id_col_name: - expected_expanded = expected.loc[expected.index.repeat(2)].reset_index( - drop=True + expected = pd.DataFrame( + { + "is_anomaly": [False, False, False, False, False, False], + "lower_bound": [ + 2420.11419, + 2420.11419, + 2360.1870, + 2360.1870, + 2086.0609, + 2086.0609, + ], + "upper_bound": [ + 2849.47785, + 2849.47785, + 2826.54981, + 2826.54981, + 2621.165188, + 2621.165188, + ], + "anomaly_probability": [ + 0.485459, + 0.485459, + 0.385683, + 0.385683, + 0.314156, + 0.314156, + ], + }, + ) + else: + expected = pd.DataFrame( + { + "is_anomaly": [False, False, False], + "lower_bound": [2420.11419, 2360.1870, 2086.0609], + "upper_bound": [2849.47785, 2826.54981, 2621.165188], + "anomaly_probability": [0.485459, 0.385683, 0.314156], + }, ) - expected = expected_expanded pd.testing.assert_frame_equal( anomalies[["is_anomaly", "lower_bound", "upper_bound", "anomaly_probability"]], expected, @@ -351,22 +458,31 @@ def test_arima_plus_score( result = time_series_arima_plus_model.score( new_time_series_df[["parsed_date"]], new_time_series_df[["total_visits"]] ).to_pandas() - expected = pd.DataFrame( - { - "mean_absolute_error": [120.0110074], - "mean_squared_error": [14562.5623594], - "root_mean_squared_error": [120.675442], - "mean_absolute_percentage_error": [4.80044], - "symmetric_mean_absolute_percentage_error": [4.744332], - }, - dtype="Float64", - ) if id_col_name: - expected_expanded = pd.concat([expected, expected], ignore_index=True) - expected_expanded["id"] = ["2", "1"] - expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") - expected_expanded = expected_expanded[["id"] + list(expected.columns)] - expected = expected_expanded + expected = pd.DataFrame( + { + "id": ["2", "1"], + "mean_absolute_error": [120.011007, 120.011007], + "mean_squared_error": [14562.562359, 14562.562359], + "root_mean_squared_error": [120.675442, 120.675442], + "mean_absolute_percentage_error": [4.80044, 4.80044], + "symmetric_mean_absolute_percentage_error": [4.744332, 4.744332], + }, + dtype="Float64", + ) + expected["id"] = expected["id"].astype(str).str.replace(r"\.0$", "", regex=True) + expected["id"] = expected["id"].astype("string[pyarrow]") + else: + expected = pd.DataFrame( + { + "mean_absolute_error": [120.0110074], + "mean_squared_error": [14562.5623594], + "root_mean_squared_error": [120.675442], + "mean_absolute_percentage_error": [4.80044], + "symmetric_mean_absolute_percentage_error": [4.744332], + }, + dtype="Float64", + ) pd.testing.assert_frame_equal( result, expected, @@ -435,22 +551,31 @@ def test_arima_plus_score_series( result = time_series_arima_plus_model.score( new_time_series_df["parsed_date"], new_time_series_df["total_visits"] ).to_pandas() - expected = pd.DataFrame( - { - "mean_absolute_error": [120.0110074], - "mean_squared_error": [14562.5623594], - "root_mean_squared_error": [120.675442], - "mean_absolute_percentage_error": [4.80044], - "symmetric_mean_absolute_percentage_error": [4.744332], - }, - dtype="Float64", - ) if id_col_name: - expected_expanded = pd.concat([expected, expected], ignore_index=True) - expected_expanded["id"] = ["2", "1"] - expected_expanded["id"] = expected_expanded["id"].astype("string[pyarrow]") - expected_expanded = expected_expanded[["id"] + list(expected.columns)] - expected = expected_expanded + expected = pd.DataFrame( + { + "id": ["2", "1"], + "mean_absolute_error": [120.011007, 120.011007], + "mean_squared_error": [14562.562359, 14562.562359], + "root_mean_squared_error": [120.675442, 120.675442], + "mean_absolute_percentage_error": [4.80044, 4.80044], + "symmetric_mean_absolute_percentage_error": [4.744332, 4.744332], + }, + dtype="Float64", + ) + expected["id"] = expected["id"].astype(str).str.replace(r"\.0$", "", regex=True) + expected["id"] = expected["id"].astype("string[pyarrow]") + else: + expected = pd.DataFrame( + { + "mean_absolute_error": [120.0110074], + "mean_squared_error": [14562.5623594], + "root_mean_squared_error": [120.675442], + "mean_absolute_percentage_error": [4.80044], + "symmetric_mean_absolute_percentage_error": [4.744332], + }, + dtype="Float64", + ) pd.testing.assert_frame_equal( result, expected, From 51c51ee91638f8013bbfe7b3acb0eefe0a27ae59 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Tue, 28 Jan 2025 19:58:01 +0000 Subject: [PATCH 10/10] bug fix: time_series_id_col does not always have the same name --- bigframes/ml/core.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index b66e6e754b..ad00ed3f2c 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -181,18 +181,22 @@ def detect_anomalies( def forecast(self, options: Mapping[str, int | float]) -> bpd.DataFrame: sql = self._model_manipulation_sql_generator.ml_forecast(struct_options=options) - index_cols = ["forecast_timestamp"] - if "id" in self._session.read_gbq(sql).columns: - index_cols.append("id") + timestamp_col_name = "forecast_timestamp" + index_cols = [timestamp_col_name] + first_col_name = self._session.read_gbq(sql).columns.values[0] + if timestamp_col_name != first_col_name: + index_cols.append(first_col_name) return self._session.read_gbq(sql, index_col=index_cols).reset_index() def explain_forecast(self, options: Mapping[str, int | float]) -> bpd.DataFrame: sql = self._model_manipulation_sql_generator.ml_explain_forecast( struct_options=options ) - index_cols = ["time_series_timestamp"] - if "id" in self._session.read_gbq(sql).columns: - index_cols.append("id") + timestamp_col_name = "time_series_timestamp" + index_cols = [timestamp_col_name] + first_col_name = self._session.read_gbq(sql).columns.values[0] + if timestamp_col_name != first_col_name: + index_cols.append(first_col_name) return self._session.read_gbq(sql, index_col=index_cols).reset_index() def evaluate(self, input_data: Optional[bpd.DataFrame] = None):