瀏覽代碼

Fix bug about holiday year different from input year and others (#670)

* Allow both both hoidays and append holidays

Match holidays in predict and fit

Add test for append_holiday features; minor fixes

Add column name validation for append_holidays names; allow only one country

Fix bug about holiday year different from input year and also other bugs

Change function description

Add append holiday feature for R

* Add test for R/Pyrhon; fix bugs
ziye666 6 年之前
父節點
當前提交
95fa4460a9

+ 1 - 1
R/DESCRIPTION

@@ -32,7 +32,7 @@ Suggests:
     readr
 License: BSD_3_clause + file LICENSE
 LazyData: true
-RoxygenNote: 6.0.1
+RoxygenNote: 6.1.0
 VignetteBuilder: knitr
 SystemRequirements: C++11
 Encoding: UTF-8

+ 2 - 0
R/NAMESPACE

@@ -8,7 +8,9 @@ export(add_seasonality)
 export(cross_validation)
 export(dyplot.prophet)
 export(fit.prophet)
+export(get_holiday_names)
 export(make_future_dataframe)
+export(make_holidays_df)
 export(performance_metrics)
 export(plot_cross_validation_metric)
 export(plot_forecast_component)

+ 12 - 0
R/R/data.R

@@ -0,0 +1,12 @@
+## Copyright (c) 2017-present, Facebook, Inc.
+## All rights reserved.
+
+## This source code is licensed under the BSD-style license found in the
+## LICENSE file in the root directory of this source tree. An additional grant
+## of patent rights can be found in the PATENTS file in the same directory.
+
+
+#' holidays table
+#'
+#' @format A data frame with five variables: ds, holiday, country, year
+"generated_holidays"

+ 1 - 0
R/R/diagnostics.R

@@ -154,6 +154,7 @@ prophet_copy <- function(m, cutoff = NULL) {
     weekly.seasonality = FALSE,
     daily.seasonality = FALSE,
     holidays = m$holidays,
+    append.holidays = m$append.holidays,
     seasonality.mode = m$seasonality.mode,
     seasonality.prior.scale = m$seasonality.prior.scale,
     changepoint.prior.scale = m$changepoint.prior.scale,

+ 46 - 0
R/R/make_holidays.R

@@ -0,0 +1,46 @@
+## Copyright (c) 2017-present, Facebook, Inc.
+## All rights reserved.
+
+## This source code is licensed under the BSD-style license found in the
+## LICENSE file in the root directory of this source tree. An additional grant
+## of patent rights can be found in the PATENTS file in the same directory.
+
+
+#' Return all possible holiday names of given country
+#'
+#' @param country.name Country name (character).
+#'
+#' @return A vector of all possible holiday names (unique) of given country.
+#' @export
+get_holiday_names <- function(country.name){
+    holidays <- generated_holidays %>% 
+      dplyr::filter(country == country.name) %>%
+      dplyr::select(holiday) %>%
+      unique()
+  return(holidays$holiday)
+}
+
+
+#' Make dataframe of holidays for given years and countries
+#'
+#' @param country.name Country name (character).
+#'
+#' @return Dataframe with 'ds' and 'holiday', which can directly feed
+#'  to 'holidays' params in Prophet
+#' @export
+make_holidays_df <- function(years, country.name){
+  country.holidays = generated_holidays %>%
+    dplyr::filter(country == country.name)
+  max.year <- max(country.holidays$year)
+  min.year <- min(country.holidays$year)
+  if (max(years) > max.year ||  min(years) < min.year){
+    warning.msg = paste("Holidays for", country.name, "are only supported from", min.year, 
+                        "to", max.year)
+    warning(warning.msg)
+  }
+  holidays.df <- country.holidays %>%
+    dplyr::filter(year %in% years) %>%
+    dplyr::select(ds, holiday) %>%
+    data.frame
+  return(holidays.df)
+}

+ 66 - 22
R/R/prophet.R

@@ -8,8 +8,8 @@
 ## Makes R CMD CHECK happy due to dplyr syntax below
 globalVariables(c(
   "ds", "y", "cap", ".",
-  "component", "dow", "doy", "holiday", "holidays", "holidays_lower", "holidays_upper", "ix",
-  "lower", "n", "stat", "trend", "row_number", "extra_regressors", "col",
+  "component", "dow", "doy", "holiday", "holidays", "append.holidays", "holidays_lower", 
+  "holidays_upper", "ix", "lower", "n", "stat", "trend", "row_number", "extra_regressors", "col",
   "trend_lower", "trend_upper", "upper", "value", "weekly", "weekly_lower", "weekly_upper",
   "x", "yearly", "yearly_lower", "yearly_upper", "yhat", "yhat_lower", "yhat_upper"))
 
@@ -43,6 +43,7 @@ globalVariables(c(
 #'  range of days around the date to be included as holidays. lower_window=-2
 #'  will include 2 days prior to the date as holidays. Also optionally can have
 #'  a column prior_scale specifying the prior scale for each holiday.
+#' @param append.holidays country name or abbreviation (character).
 #' @param seasonality.mode 'additive' (default) or 'multiplicative'.
 #' @param seasonality.prior.scale Parameter modulating the strength of the
 #'  seasonality model. Larger values allow the model to fit larger seasonal
@@ -87,6 +88,7 @@ prophet <- function(df = NULL,
                     weekly.seasonality = 'auto',
                     daily.seasonality = 'auto',
                     holidays = NULL,
+                    append.holidays = NULL,
                     seasonality.mode = 'additive',
                     seasonality.prior.scale = 10,
                     holidays.prior.scale = 10,
@@ -110,6 +112,7 @@ prophet <- function(df = NULL,
     weekly.seasonality = weekly.seasonality,
     daily.seasonality = daily.seasonality,
     holidays = holidays,
+    append.holidays = append.holidays,
     seasonality.mode = seasonality.mode,
     seasonality.prior.scale = seasonality.prior.scale,
     changepoint.prior.scale = changepoint.prior.scale,
@@ -129,6 +132,7 @@ prophet <- function(df = NULL,
     params = list(),
     history = NULL,
     history.dates = NULL,
+    train.holiday.names = NULL,
     train.component.cols = NULL,
     component.modes = NULL
   )
@@ -177,6 +181,11 @@ validate_inputs <- function(m) {
       validate_column_name(m, h, check_holidays = FALSE)
     }
   }
+  if (!is.null(m$append.holidays)) {
+    if (!(m$append.holidays %in% generated_holidays$country)){
+      stop("Holidays in ", m$append.holidays," are not currently supported!")
+    }
+  }
   if (!(m$seasonality.mode %in% c('additive', 'multiplicative'))) {
     stop("seasonality.mode must be 'additive' or 'multiplicative'")
   }
@@ -214,6 +223,11 @@ validate_column_name <- function(
      (name %in% unique(m$holidays$holiday))){
     stop("Name ", name, " already used for a holiday.")
   }
+  if(check_holidays & !is.null(m$append.holidays)){
+    if(name %in% get_holiday_names(m$append.holidays)){
+      stop("Name ", name, " is a holiday name in ", m$append.holidays, ".")
+    }
+  }
   if(check_seasonalities & (!is.null(m$seasonalities[[name]]))){
     stop("Name ", name, " already used for a seasonality.")
   }
@@ -534,10 +548,28 @@ make_seasonality_features <- function(dates, period, series.order, prefix) {
 make_holiday_features <- function(m, dates) {
   # Strip dates to be just days, for joining on holidays
   dates <- set_date(format(dates, "%Y-%m-%d"))
-  wide <- m$holidays %>%
+  all.holidays <- m$holidays 
+  if (!is.null(m$append.holidays)){
+    years <- as.numeric(unique(format(dates, "%Y")))
+    append.holidays.df <- make_holidays_df(years, m$append.holidays) %>%
+      dplyr::mutate(ds=as.character(ds), holiday=as.character(holiday))
+    all.holidays <- suppressWarnings(dplyr::bind_rows(all.holidays, append.holidays.df))
+  }
+  # Make fit.prophet and predict.prophet holidays components match
+  if (!is.null(m$append.holidays) && !is.null(m$train.holiday.names)){
+    row.to.keep <- which(all.holidays$holiday %in% m$train.holiday.names)
+    all.holidays <- all.holidays[row.to.keep,]
+    holidays.to.add <- data.frame(holiday=setdiff(m$train.holiday.names,
+                                                    all.holidays$holiday))
+    all.holidays <- suppressWarnings(dplyr::bind_rows(all.holidays, holidays.to.add))
+  }
+  if (nrow(all.holidays)==0){
+    return(NULL)
+  }
+  wide <- all.holidays %>%
     dplyr::mutate(ds = set_date(ds)) %>%
     dplyr::group_by(holiday, ds) %>%
-    dplyr::filter(row_number() == 1) %>%
+    dplyr::filter(dplyr::row_number() == 1) %>%
     dplyr::do({
       if (exists('lower_window', where = .) && !is.na(.$lower_window)
           && !is.na(.$upper_window)) {
@@ -555,16 +587,17 @@ make_holiday_features <- function(m, dates) {
   holiday.features <- data.frame(ds = set_date(dates)) %>%
     dplyr::left_join(wide, by = 'ds') %>%
     dplyr::select(-ds)
-
+  # Make sure fit.prophet and predict.prophet component.cols perfectly equal
+  holiday.features <- holiday.features %>% dplyr::select(sort(names(.)))
   holiday.features[is.na(holiday.features)] <- 0
-
+  
   # Prior scales
-  if (!('prior_scale' %in% colnames(m$holidays))) {
-    m$holidays$prior_scale <- m$holidays.prior.scale
+  if (!('prior_scale' %in% colnames(all.holidays))) {
+    all.holidays$prior_scale <- m$holidays.prior.scale
   }
   prior.scales.list <- list()
-  for (name in unique(m$holidays$holiday)) {
-    df.h <- m$holidays[m$holidays$holiday == name, ]
+  for (name in unique(all.holidays$holiday)) {
+    df.h <- all.holidays[all.holidays$holiday == name, ]
     ps <- unique(df.h$prior_scale)
     if (length(ps) > 1) {
       stop('Holiday ', name, ' does not have a consistent prior scale ',
@@ -584,9 +617,14 @@ make_holiday_features <- function(m, dates) {
     sn <- strsplit(name, '_delim_', fixed = TRUE)[[1]][1]
     prior.scales <- c(prior.scales, prior.scales.list[[sn]])
   }
-  return(list(holiday.features = holiday.features,
+  holiday.names <- names(prior.scales.list)
+  if (is.null(m$train.holiday.names)){
+    m$train.holiday.names <- holiday.names
+  }
+  return(list(m = m,
+              holiday.features = holiday.features,
               prior.scales = prior.scales,
-              holiday.names = names(prior.scales.list)))
+              holiday.names = holiday.names))
 }
 
 #' Add an additional regressor to be used for fitting and predicting.
@@ -738,12 +776,15 @@ make_all_seasonality_features <- function(m, df) {
   }
 
   # Holiday features
-  if (!is.null(m$holidays)) {
-    hf <- make_holiday_features(m, df$ds)
-    seasonal.features <- cbind(seasonal.features, hf$holiday.features)
-    prior.scales <- c(prior.scales, hf$prior.scales)
-    modes[[m$seasonality.mode]] <- c(
-      modes[[m$seasonality.mode]], hf$holiday.names)
+  if (!is.null(m$holidays) || !is.null(m$append.holidays)) {
+    out <- make_holiday_features(m, df$ds)
+    if (!is.null(out)){
+      m <- out$m
+      seasonal.features <- cbind(seasonal.features, out$holiday.features)
+      prior.scales <- c(prior.scales, out$prior.scales)
+      modes[[m$seasonality.mode]] <- c(
+        modes[[m$seasonality.mode]], out$holiday.names)
+    }
   }
 
   # Additional regressors
@@ -761,7 +802,8 @@ make_all_seasonality_features <- function(m, df) {
   }
 
   components.list <- regressor_column_matrix(m, seasonal.features, modes)
-  return(list(seasonal.features = seasonal.features,
+  return(list(m = m,
+              seasonal.features = seasonal.features,
               prior.scales = prior.scales,
               component.cols = components.list$component.cols,
               modes = components.list$modes))
@@ -792,9 +834,9 @@ regressor_column_matrix <- function(m, seasonal.features, modes) {
                     extra = "merge", fill = "right") %>%
     dplyr::select(col, component)
   # Add total for holidays
-  if(!is.null(m$holidays)){
+  if(!is.null(m$train.holiday.names)){
     components <- add_group_component(
-      components, 'holidays', unique(m$holidays$holiday))
+      components, 'holidays', unique(m$train.holiday.names))
   }
   # Add totals for additive and multiplicative components, and regressors
   for (mode in c('additive', 'multiplicative')) {
@@ -1061,6 +1103,7 @@ fit.prophet <- function(m, df, ...) {
   m$history <- history
   m <- set_auto_seasonalities(m)
   out2 <- make_all_seasonality_features(m, history)
+  m <- out2$m
   seasonal.features <- out2$seasonal.features
   prior.scales <- out2$prior.scales
   component.cols <- out2$component.cols
@@ -1136,7 +1179,7 @@ fit.prophet <- function(m, df, ...) {
     m$params <- stan.fit$par
     n.iteration <- 1
   }
-
+  
   # Cast the parameters to have consistent form, whether full bayes or MAP
   for (name in c('delta', 'beta')){
     m$params[[name]] <- matrix(m$params[[name]], nrow = n.iteration)
@@ -1296,6 +1339,7 @@ predict_trend <- function(model, df) {
 #' @keywords internal
 predict_seasonal_components <- function(m, df) {
   out <- make_all_seasonality_features(m, df)
+  m <- out$m
   seasonal.features <- out$seasonal.features
   component.cols <- out$component.cols
   lower.p <- (1 - m$interval.width)/2

+ 10 - 0
R/data-raw/generated_holidays.R

@@ -0,0 +1,10 @@
+## Copyright (c) 2017-present, Facebook, Inc.
+## All rights reserved.
+
+## This source code is licensed under the BSD-style license found in the
+## LICENSE file in the root directory of this source tree. An additional grant
+## of patent rights can be found in the PATENTS file in the same directory.
+
+
+generated_holidays <- read.csv("data-raw/generated_holidays.csv")
+devtools::use_data(generated_holidays, overwrite = TRUE)

文件差異過大導致無法顯示
+ 67401 - 0
R/data-raw/generated_holidays.csv


二進制
R/data/generated_holidays.rda


+ 14 - 0
R/man/generated_holidays.Rd

@@ -0,0 +1,14 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/data.R
+\docType{data}
+\name{generated_holidays}
+\alias{generated_holidays}
+\title{holidays table}
+\format{A data frame with five variables: ds, holiday, country, year}
+\usage{
+generated_holidays
+}
+\description{
+holidays table
+}
+\keyword{datasets}

+ 17 - 0
R/man/get_holiday_names.Rd

@@ -0,0 +1,17 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/make_holidays.R
+\name{get_holiday_names}
+\alias{get_holiday_names}
+\title{Return all possible holiday names of given country}
+\usage{
+get_holiday_names(country.name)
+}
+\arguments{
+\item{country.name}{Country name (character).}
+}
+\value{
+A vector of all possible holiday names (unique) of given country.
+}
+\description{
+Return all possible holiday names of given country
+}

+ 18 - 0
R/man/make_holidays_df.Rd

@@ -0,0 +1,18 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/make_holidays.R
+\name{make_holidays_df}
+\alias{make_holidays_df}
+\title{Make dataframe of holidays for given years and countries}
+\usage{
+make_holidays_df(years, country.name)
+}
+\arguments{
+\item{country.name}{Country name (character).}
+}
+\value{
+Dataframe with 'ds' and 'holiday', which can directly feed
+ to 'holidays' params in Prophet
+}
+\description{
+Make dataframe of holidays for given years and countries
+}

+ 2 - 1
R/man/plot_forecast_component.Rd

@@ -4,7 +4,8 @@
 \alias{plot_forecast_component}
 \title{Plot a particular component of the forecast.}
 \usage{
-plot_forecast_component(m, fcst, name, uncertainty = TRUE, plot_cap = FALSE)
+plot_forecast_component(m, fcst, name, uncertainty = TRUE,
+  plot_cap = FALSE)
 }
 \arguments{
 \item{m}{Prophet model}

+ 6 - 4
R/man/prophet.Rd

@@ -8,10 +8,10 @@ prophet(df = NULL, growth = "linear", changepoints = NULL,
   n.changepoints = 25, changepoint.range = 0.8,
   yearly.seasonality = "auto", weekly.seasonality = "auto",
   daily.seasonality = "auto", holidays = NULL,
-  seasonality.mode = "additive", seasonality.prior.scale = 10,
-  holidays.prior.scale = 10, changepoint.prior.scale = 0.05,
-  mcmc.samples = 0, interval.width = 0.8, uncertainty.samples = 1000,
-  fit = TRUE, ...)
+  append.holidays = NULL, seasonality.mode = "additive",
+  seasonality.prior.scale = 10, holidays.prior.scale = 10,
+  changepoint.prior.scale = 0.05, mcmc.samples = 0,
+  interval.width = 0.8, uncertainty.samples = 1000, fit = TRUE, ...)
 }
 \arguments{
 \item{df}{(optional) Dataframe containing the history. Must have columns ds
@@ -51,6 +51,8 @@ range of days around the date to be included as holidays. lower_window=-2
 will include 2 days prior to the date as holidays. Also optionally can have
 a column prior_scale specifying the prior scale for each holiday.}
 
+\item{append.holidays}{country name or abbreviation (character).}
+
 \item{seasonality.mode}{'additive' (default) or 'multiplicative'.}
 
 \item{seasonality.prior.scale}{Parameter modulating the strength of the

+ 3 - 1
R/tests/testthat/test_diagnostics.R

@@ -121,6 +121,7 @@ test_that("copy", {
     weekly.seasonality = c(TRUE, FALSE),
     daily.seasonality = c(TRUE, FALSE),
     holidays = c('null', 'insert_dataframe'),
+    append.holidays = c(NULL, 'US'),
     seasonality.mode = c('additive', 'multiplicative')
   )
   products <- expand.grid(inputs)
@@ -139,6 +140,7 @@ test_that("copy", {
       weekly.seasonality = products$weekly.seasonality[i],
       daily.seasonality = products$daily.seasonality[i],
       holidays = holidays,
+      append.holidays = products$append.holidays[i],
       seasonality.prior.scale = 1.1,
       holidays.prior.scale = 1.1,
       changepoints.prior.scale = 0.1,
@@ -153,7 +155,7 @@ test_that("copy", {
     m1 <- prophet:::set_auto_seasonalities(m1)
     m2 <- prophet:::prophet_copy(m1)
     # Values should be copied correctly
-    args <- c('growth', 'changepoints', 'n.changepoints', 'holidays',
+    args <- c('growth', 'changepoints', 'n.changepoints', 'holidays', 'append.holidays',
               'seasonality.prior.scale', 'holidays.prior.scale',
               'changepoints.prior.scale', 'mcmc.samples', 'interval.width',
               'uncertainty.samples', 'seasonality.mode', 'changepoint.range')

+ 43 - 0
R/tests/testthat/test_prophet.R

@@ -349,6 +349,49 @@ test_that("fit_with_holidays", {
   expect_error(predict(m), NA)
 })
 
+test_that("fit_with_append_holidays", {
+  skip_if_not(Sys.getenv('R_ARCH') != '/i386')
+  holidays <- data.frame(ds = c('2012-06-06', '2013-06-06'),
+                         holiday = c('seans-bday', 'seans-bday'),
+                         lower_window = c(0, 0),
+                         upper_window = c(1, 1))
+  append.holidays = 'US'
+  # Test with holidays and append_holidays
+  m <- prophet(DATA, 
+               holidays = holidays, 
+               append.holidays = append.holidays, 
+               uncertainty.samples = 0)
+  expect_error(predict(m), NA)
+  # There are training holidays missing in the test set
+  train2 <- DATA %>% head(155)
+  future2 <- DATA %>% tail(355)
+  model <- prophet(train2,
+                   append.holidays = append.holidays, 
+                   uncertainty.samples = 0)
+  expect_error(predict(m, future2), NA)
+  # There are test holidays missing in the training set
+  train2 <- DATA %>% tail(355)
+  future2 <- DATA2
+  model <- prophet(train2,
+                   append.holidays = append.holidays, 
+                   uncertainty.samples = 0)
+  expect_error(predict(m, future2), NA)
+  # Append_holidays with non-existing year
+  max.year <- generated_holidays %>% 
+    dplyr::filter(country==append.holidays) %>%
+    dplyr::select(year) %>%
+    max()
+  train2 <- data.frame('ds'=c(paste(max.year+1, "-01-01", sep=''),
+                              paste(max.year+1, "-01-02", sep='')),
+                       'y'=1)
+  expect_warning(prophet(train2, 
+                         append.holidays = append.holidays))
+  # Append_holidays with non-existing country
+  append.holidays = 'Utopia'
+  expect_error(prophet(DATA, 
+                       append.holidays = append.holidays))
+})
+
 test_that("make_future_dataframe", {
   skip_if_not(Sys.getenv('R_ARCH') != '/i386')
   train.t <- DATA[1:234, ]

+ 271 - 131
python/fbprophet/hdays.py

@@ -17,6 +17,7 @@ from datetime import date, timedelta
 from convertdate.islamic import to_gregorian, from_gregorian
 import warnings
 
+
 # Official public holidays at a country level
 # ------------ Holidays in Brazil---------------------
 class Brazil(HolidayBase):
@@ -89,7 +90,10 @@ class Indonesia(HolidayBase):
 
         # Chinese New Year/ Spring Festival
         name = "Chinese New Year"
-        self[LunarDate(year, 1, 1).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 1, 1).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # Day of Silence / Nyepi
         # Note:
@@ -126,10 +130,12 @@ class Indonesia(HolidayBase):
             pass
 
         # Ascension of the Prophet
-        islam_year = from_gregorian(year, 3, 17)[0]
-        y, m, d = to_gregorian(islam_year, 7, 27)
         name = "Ascension of the Prophet"
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 3, 17)[0]
+            y, m, d = to_gregorian(islam_year, 7, 27)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Labor Day
         name = "Labor Day"
@@ -137,12 +143,17 @@ class Indonesia(HolidayBase):
 
         # Ascension of Jesus Christ
         name = "Ascension of Jesus"
-        hdate = easter(year) + rd(days=+39)
-        self[hdate] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset) + rd(days=+39)
+            if ds.year == year:
+                self[ds] = name
 
         # Buddha's Birthday
         name = "Buddha's Birthday"
-        self[LunarDate(year, 4, 15).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 4, 15).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # Pancasila Day, since 2017
         if year >= 2017:
@@ -150,34 +161,43 @@ class Indonesia(HolidayBase):
             self[date(year, 6, 1)] = name
 
         # Eid al-Fitr
-        islam_year = from_gregorian(year, 6, 15)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 10, 1)
-        y2, m2, d2 = to_gregorian(islam_year, 10, 2)
         name = "Eid al-Fitr"
-        self[date(year, m1, d1)] = name
-        self[date(year, m2, d2)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 6, 15)[0]
+            y1, m1, d1 = to_gregorian(islam_year, 10, 1)
+            y2, m2, d2 = to_gregorian(islam_year, 10, 2)
+            if y1 == year:
+                self[date(y1, m2, d2)] = name
+            if y2 == year:
+                self[date(y2, m2, d2)] = name
 
         # Independence Day
         name = "Independence Day"
         self[date(year, 8, 17)] = name
 
         # Feast of the Sacrifice
-        islam_year = from_gregorian(year, 8, 22)[0]
-        y, m, d = to_gregorian(islam_year, 12, 10)
         name = "Feast of the Sacrifice"
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 8, 22)[0]
+            y, m, d = to_gregorian(islam_year, 12, 10)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Islamic New Year
-        islam_year = from_gregorian(year, 9, 11)[0]
-        y, m, d = to_gregorian(islam_year + 1, 1, 1)
         name = "Islamic New Year"
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 9, 11)[0]
+            y, m, d = to_gregorian(islam_year + 1, 1, 1)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Birth of the Prophet
-        islam_year = from_gregorian(year, 11, 20)[0]
-        y, m, d = to_gregorian(islam_year + 1, 3, 12)
         name = "Birth of the Prophet"
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 11, 20)[0]
+            y, m, d = to_gregorian(islam_year + 1, 3, 12)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Christmas
         self[date(year, 12, 25)] = "Christmas"
@@ -298,32 +318,41 @@ class India(HolidayBase):
 
         # Day of Ashura
         # 10th day of 1st Islamic month
-        islam_year = from_gregorian(year, 10, 1)[0]
-        y, m, d = to_gregorian(islam_year, 1, 10)
         name = "Day of Ashura"
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 10, 1)[0]
+            y, m, d = to_gregorian(islam_year, 1, 10)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Mawlid, Birth of the Prophet
         # 12th day of 3rd Islamic month
-        islam_year = from_gregorian(year, 11, 20)[0]
-        y, m, d = to_gregorian(islam_year, 3, 12)
         name = "Mawlid"
-        self[date(y, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 11, 20)[0]
+            y, m, d = to_gregorian(islam_year, 3, 12)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Eid ul-Fitr
         # 1st and 2nd day of 10th Islamic month
-        islam_year = from_gregorian(year, 6, 15)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 10, 1)
-        y2, m2, d2 = to_gregorian(islam_year, 10, 2)
         name = "Eid al-Fitr"
-        self[date(year, m1, d1)] = name
-        self[date(year, m2, d2)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 6, 15)[0]
+            y1, m1, d1 = to_gregorian(islam_year, 10, 1)
+            y2, m2, d2 = to_gregorian(islam_year, 10, 2)
+            if y1 == year:
+                self[date(y1, m1, d1)] = name
+            if y2 == year:
+                self[date(y2, m2, d2)] = name
 
         # Eid al-Adha, i.e., Feast of the Sacrifice
-        islam_year = from_gregorian(year, 8, 22)[0]
-        y, m, d = to_gregorian(islam_year, 12, 10)
         name = "Feast of the Sacrifice"
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 8, 22)[0]
+            y, m, d = to_gregorian(islam_year, 12, 10)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # --------------------------------
         # Christian holidays
@@ -345,23 +374,38 @@ class India(HolidayBase):
 
         # Palm Sunday
         name = "Palm Sunday"
-        self[easter(year) - rd(days=7)] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset) - rd(days=7)
+            if ds.year == year:
+                self[ds] = name
 
         # Maundy Thursday
         name = "Maundy Thursday"
-        self[easter(year) - rd(days=3)] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset) - rd(days=3)
+            if ds.year == year:
+                self[ds] = name
 
         # Good Friday
         name = "Good Friday"
-        self[easter(year) - rd(days=2)] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset) - rd(days=2)
+            if ds.year == year:
+                self[ds] = name
 
         # Easter Sunday
         name = "Easter Sunday"
-        self[easter(year)] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset)
+            if ds.year == year:
+                self[ds] = name
 
         # Feast of Pentecost
         name = "Feast of Pentecost"
-        self[easter(year) + rd(days=49)] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset) + rd(days=49)
+            if ds.year == year:
+                self[ds] = name
 
         # Fest of St. Theresa of Calcutta
         name = "Fest of St. Theresa of Calcutta"
@@ -416,14 +460,19 @@ class Malaysia(HolidayBase):
 
         # Birthday of Prophet, Mawlid in India
         # 12th day of 3rd Islamic month
-        islam_year = from_gregorian(year, 11, 20)[0]
-        y, m, d = to_gregorian(islam_year, 3, 12)
         name = "Birth of Prophet"
-        self[date(y, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 11, 20)[0]
+            y, m, d = to_gregorian(islam_year, 3, 12)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Chinese New Year
         name = "Chinese New Year"
-        self[LunarDate(year, 1, 1).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 1, 1).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # Tamil New Year
         # Note: it's not necessarily 04/14
@@ -434,7 +483,10 @@ class Malaysia(HolidayBase):
 
         # Good Friday
         name = "Good Friday"
-        self[easter(year) - rd(days=2)] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset) - rd(days=2)
+            if ds.year == year:
+                self[ds] = name
 
         # Labor Day
         name = "Labor Day"
@@ -442,7 +494,10 @@ class Malaysia(HolidayBase):
 
         # Buddha's Birthday
         name = "Wesak Day"
-        self[LunarDate(year, 4, 15).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 4, 15).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # King's birthday
         # https://www.thestar.com.my/news/nation/2017/04/26/
@@ -471,28 +526,35 @@ class Malaysia(HolidayBase):
             self[saturdays[-1]] = name
 
         # Eid al-Fitr
-        islam_year = from_gregorian(year, 6, 15)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 10, 1)
-        y2, m2, d2 = to_gregorian(islam_year, 10, 2)
         name = "Eid al-Fitr"
-        self[date(year, m1, d1)] = name
-        self[date(year, m2, d2)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 6, 15)[0]
+            y1, m1, d1 = to_gregorian(islam_year, 10, 1)
+            y2, m2, d2 = to_gregorian(islam_year, 10, 2)
+            if y1 == year:
+                self[date(y1, m1, d1)] = name
+            if y2 == year:
+                self[date(y2, m2, d2)] = name
 
         # Malaysia Day
         name = "Malaysia Day"
         self[date(year, 9, 16)] = name
 
         # Feast of the Sacrifice
-        islam_year = from_gregorian(year, 8, 22)[0]
-        y, m, d = to_gregorian(islam_year, 12, 10)
         name = "Feast of the Sacrifice"
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 8, 22)[0]
+            y, m, d = to_gregorian(islam_year, 12, 10)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # First Day of Muharram
-        islam_year = from_gregorian(year, 9, 11)[0]
-        y, m, d = to_gregorian(islam_year + 1, 1, 1)
         name = "First Day of Muharram"
-        self[date(y, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 9, 11)[0]
+            y, m, d = to_gregorian(islam_year + 1, 1, 1)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Christmas
         name = "Christmas Day"
@@ -523,16 +585,35 @@ class Vietnam(HolidayBase):
 
         # Vietnamese New Year
         name = "Vietnamese New Year"
-        self[LunarDate(year - 1, 12, 30).toSolarDate()] = name
-        self[LunarDate(year, 1, 1).toSolarDate()] = name
-        self[LunarDate(year, 1, 2).toSolarDate()] = name
-        self[LunarDate(year, 1, 3).toSolarDate()] = name
-        self[LunarDate(year, 1, 4).toSolarDate()] = name
-        self[LunarDate(year, 1, 5).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            try:
+                ds = LunarDate(year - 1 + offset, 12, 30).toSolarDate()
+            except ValueError:
+                ds = LunarDate(year - 1 + offset, 12, 29).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
+            ds = LunarDate(year + offset, 1, 1).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
+            ds = LunarDate(year + offset, 1, 2).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
+            ds = LunarDate(year + offset, 1, 3).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
+            ds = LunarDate(year + offset, 1, 4).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
+            ds = LunarDate(year + offset, 1, 5).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # Hung Kings Commemorations
         name = "Hung Kings Commemorations"
-        self[LunarDate(year, 3, 10).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 3, 10).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # Reunification Day
         name = "Reunification Day"
@@ -606,7 +687,10 @@ class Thailand(HolidayBase):
 
         # Buddha's Birthday
         name = "Buddha's Birthday"
-        self[LunarDate(year, 4, 15).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 4, 15).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # Coronation Day, removed in 2017
         name = "Coronation Day"
@@ -754,11 +838,17 @@ class Philippines(HolidayBase):
 
         # Maundy Thursday
         name = "Maundy Thursday"
-        self[easter(year) - rd(days=3)] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset) - rd(days=3)
+            if ds.year == year:
+                self[ds] = name
 
         # Good Friday
         name = "Good Friday"
-        self[easter(year) - rd(days=2)] = name
+        for offset in range(-1, 2, 1):
+            ds = easter(year + offset) - rd(days=2)
+            if ds.year == year:
+                self[ds] = name
 
         # Day of Valor
         name = "Day of Valor"
@@ -774,15 +864,20 @@ class Philippines(HolidayBase):
 
         # Eid al-Fitr
         name = "Eid al-Fitr"
-        islam_year = from_gregorian(year, 6, 15)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 10, 1)
-        self[date(year, m1, d1) - timedelta(days=1)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 6, 15)[0]
+            y, m, d = to_gregorian(islam_year, 10, 1)
+            ds = date(y, m, d) - timedelta(days=1)
+            if ds.year == year:
+                self[ds] = name
 
         # Eid al-Adha, i.e., Feast of the Sacrifice
         name = "Feast of the Sacrifice"
-        islam_year = from_gregorian(year, 8, 22)[0]
-        y, m, d = to_gregorian(islam_year, 12, 10)
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 8, 22)[0]
+            y, m, d = to_gregorian(islam_year, 12, 10)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # National Heroes' Day
         name = "National Heroes' Day"
@@ -849,15 +944,20 @@ class Turkey(HolidayBase):
 
         # Eid al-Fitr
         name = "Eid al-Fitr"
-        islam_year = from_gregorian(year, 6, 15)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 10, 1)
-        self[date(year, m1, d1) - timedelta(days=1)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 6, 15)[0]
+            y, m, d = to_gregorian(islam_year, 10, 1)
+            ds = date(y, m, d) - timedelta(days=1)
+            if ds.year == year:
+                self[ds] = name
 
         # Eid al-Adha, i.e., Feast of the Sacrifice
         name = "Feast of the Sacrifice"
-        islam_year = from_gregorian(year, 8, 22)[0]
-        y, m, d = to_gregorian(islam_year, 12, 10)
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 8, 22)[0]
+            y, m, d = to_gregorian(islam_year, 12, 10)
+            if y == year:
+                self[date(y, m, d)] = name
 
 
 class TU(Turkey):
@@ -906,45 +1006,60 @@ class Pakistan(HolidayBase):
 
         # Eid al-Adha, i.e., Feast of the Sacrifice
         name = "Feast of the Sacrifice"
-        islam_year = from_gregorian(year, 8, 22)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 12, 10)
-        y2, m2, d2 = to_gregorian(islam_year, 12, 11)
-        y3, m3, d3 = to_gregorian(islam_year, 12, 12)
-        self[date(year, m1, d1)] = name
-        self[date(year, m2, d2)] = name
-        self[date(year, m3, d3)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 8, 22)[0]
+            y1, m1, d1 = to_gregorian(islam_year, 12, 10)
+            y2, m2, d2 = to_gregorian(islam_year, 12, 11)
+            y3, m3, d3 = to_gregorian(islam_year, 12, 12)
+            if y1 == year:
+                self[date(y1, m1, d1)] = name
+            if y2 == year:
+                self[date(y2, m2, d2)] = name
+            if y3 == year:
+                self[date(y3, m3, d3)] = name
 
         # Eid al-Fitr
         name = "Eid al-Fitr"
-        islam_year = from_gregorian(year, 6, 15)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 10, 1)
-        y2, m2, d2 = to_gregorian(islam_year, 10, 2)
-        y3, m3, d3 = to_gregorian(islam_year, 10, 3)
-        self[date(year, m1, d1)] = name
-        self[date(year, m2, d2)] = name
-        self[date(year, m3, d3)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 6, 15)[0]
+            y1, m1, d1 = to_gregorian(islam_year, 10, 1)
+            y2, m2, d2 = to_gregorian(islam_year, 10, 2)
+            y3, m3, d3 = to_gregorian(islam_year, 10, 3)
+            if y1 == year:
+                self[date(y1, m1, d1)] = name
+            if y2 == year:
+                self[date(y2, m2, d2)] = name
+            if y3 == year:
+                self[date(y3, m3, d3)] = name
 
         # Mawlid, Birth of the Prophet
         # 12th day of 3rd Islamic month
-        islam_year = from_gregorian(year, 11, 20)[0]
-        y, m, d = to_gregorian(islam_year, 3, 12)
         name = "Mawlid"
-        self[date(y, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 11, 20)[0]
+            y, m, d = to_gregorian(islam_year, 3, 12)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Day of Ashura
         # 10th and 11th days of 1st Islamic month
-        islam_year = from_gregorian(year, 10, 1)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 1, 10)
-        y2, m2, d2 = to_gregorian(islam_year, 1, 11)
         name = "Day of Ashura"
-        self[date(year, m1, d1)] = name
-        self[date(year, m2, d2)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 10, 1)[0]
+            y1, m1, d1 = to_gregorian(islam_year, 1, 10)
+            y2, m2, d2 = to_gregorian(islam_year, 1, 11)
+            if y1 == year:
+                self[date(y1, m1, d1)] = name
+            if y2 == year:
+                self[date(y2, m2, d2)] = name
 
         # Shab e Mairaj
-        islam_year = from_gregorian(year, 4, 13)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 7, 27)
         name = "Shab e Mairaj"
-        self[date(year, m1, d1)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 4, 13)[0]
+            y, m, d = to_gregorian(islam_year, 7, 27)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Defence Day
         name = "Defence Day"
@@ -1052,44 +1167,60 @@ class Egypt(HolidayBase):
         # Sham El Nessim
         # The Monday following Orthodox Easter
         name = "Sham El Nessim"
-        orthodox_easter = easter(year, method=2)
-        self[orthodox_easter + timedelta(days=1)] = name
+        for offset in range(-1, 2, 1):
+            orthodox_easter = easter(year + offset, method=2)
+            ds = orthodox_easter + timedelta(days=1)
+            if ds.year == year:
+                self[ds] = name
 
         # Islamic New Year
-        islam_year = from_gregorian(year, 9, 11)[0]
-        y, m, d = to_gregorian(islam_year + 1, 1, 1)
         name = "Islamic New Year"
-        self[date(year, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 9, 11)[0]
+            y, m, d = to_gregorian(islam_year + 1, 1, 1)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Birthday of Prophet, Mawlid in India
         # 12th day of 3rd Islamic month
-        islam_year = from_gregorian(year, 11, 20)[0]
-        y, m, d = to_gregorian(islam_year, 3, 12)
         name = "Birth of Prophet"
-        self[date(y, m, d)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 11, 20)[0]
+            y, m, d = to_gregorian(islam_year, 3, 12)
+            if y == year:
+                self[date(y, m, d)] = name
 
         # Eid ul-Fitr
         # 1st and 2nd day of 10th Islamic month
-        islam_year = from_gregorian(year, 6, 15)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 10, 1)
-        y2, m2, d2 = to_gregorian(islam_year, 10, 2)
-        y3, m3, d3 = to_gregorian(islam_year, 10, 3)
         name = "Eid al-Fitr"
-        self[date(year, m1, d1)] = name
-        self[date(year, m2, d2)] = name
-        self[date(year, m3, d3)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 6, 15)[0]
+            y1, m1, d1 = to_gregorian(islam_year, 10, 1)
+            y2, m2, d2 = to_gregorian(islam_year, 10, 2)
+            y3, m3, d3 = to_gregorian(islam_year, 10, 3)
+            if y1 == year:
+                self[date(y1, m1, d1)] = name
+            if y2 == year:
+                self[date(y2, m2, d2)] = name
+            if y3 == year:
+                self[date(y3, m3, d3)] = name
 
         # Eid al-Adha, i.e., Feast of the Sacrifice
-        islam_year = from_gregorian(year, 8, 22)[0]
-        y1, m1, d1 = to_gregorian(islam_year, 12, 10)
-        y2, m2, d2 = to_gregorian(islam_year, 12, 11)
-        y3, m3, d3 = to_gregorian(islam_year, 12, 12)
-        y4, m4, d4 = to_gregorian(islam_year, 12, 13)
         name = "Feast of the Sacrifice"
-        self[date(year, m1, d1)] = name
-        self[date(year, m2, d2)] = name
-        self[date(year, m3, d3)] = name
-        self[date(year, m4, d4)] = name
+        for offset in range(-1, 2, 1):
+            islam_year = from_gregorian(year + offset, 8, 22)[0]
+            y1, m1, d1 = to_gregorian(islam_year, 12, 10)
+            y2, m2, d2 = to_gregorian(islam_year, 12, 11)
+            y3, m3, d3 = to_gregorian(islam_year, 12, 12)
+            y4, m4, d4 = to_gregorian(islam_year, 12, 13)
+            if y1 == year:
+                self[date(y1, m1, d1)] = name
+            if y2 == year:
+                self[date(y2, m2, d2)] = name
+            if y3 == year:
+                self[date(y3, m3, d3)] = name
+            if y4 == year:
+                self[date(y4, m4, d4)] = name
 
 
 class EG(Egypt):
@@ -1116,7 +1247,10 @@ class China(HolidayBase):
 
         # Chinese New Year/ Spring Festival
         name = "Chinese New Year"
-        self[LunarDate(year, 1, 1).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 1, 1).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # Tomb-Sweeping Day
         name = "Tomb-Sweeping Day"
@@ -1129,11 +1263,17 @@ class China(HolidayBase):
 
         # Dragon Boat Festival
         name = "Dragon Boat Festival"
-        self[LunarDate(year, 5, 5).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 5, 5).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # Mid-Autumn Festival
         name = "Mid-Autumn Festival"
-        self[LunarDate(year, 8, 15).toSolarDate()] = name
+        for offset in range(-1, 2, 1):
+            ds = LunarDate(year + offset, 8, 15).toSolarDate()
+            if ds.year == year:
+                self[ds] = name
 
         # National Day
         name = "National Day"

+ 2 - 3
python/fbprophet/make_holidays.py

@@ -25,9 +25,8 @@ def get_holiday_names(country):
     country: country name
 
     Returns
-    ------- a
-    Dataframe with 'ds' and 'holiday', which can directly feed
-    to 'holidays' params in Prophet
+    -------
+    A set of all possible holiday names of given country
     """
     years = np.arange(1995, 2045)
     try:

+ 3 - 0
python/fbprophet/tests/test_diagnostics.py

@@ -141,6 +141,7 @@ class TestDiagnostics(TestCase):
         # These values are created except for its default values
         holiday = pd.DataFrame(
             {'ds': pd.to_datetime(['2016-12-25']), 'holiday': ['x']})
+        append_holidays = 'US'
         products = itertools.product(
             ['linear', 'logistic'],  # growth
             [None, pd.to_datetime(['2016-12-25'])],  # changepoints
@@ -150,6 +151,7 @@ class TestDiagnostics(TestCase):
             [True, False],  # weekly_seasonality
             [True, False],  # daily_seasonality
             [None, holiday],  # holidays
+            [None, append_holidays],  # append_holidays
             ['additive', 'multiplicative'],  # seasonality_mode
             [1.1],  # seasonality_prior_scale
             [1.1],  # holidays_prior_scale
@@ -182,6 +184,7 @@ class TestDiagnostics(TestCase):
                 self.assertEqual(m1.holidays, m2.holidays)
             else:
                 self.assertTrue((m1.holidays == m2.holidays).values.all())
+            self.assertEqual(m1.append_holidays, m2.append_holidays)
             self.assertEqual(m1.seasonality_mode, m2.seasonality_mode)
             self.assertEqual(m1.seasonality_prior_scale, m2.seasonality_prior_scale)
             self.assertEqual(m1.changepoint_prior_scale, m2.changepoint_prior_scale)

+ 62 - 0
python/scripts/generate_holidays_file.py

@@ -0,0 +1,62 @@
+# Copyright (c) 2017-present, Facebook, Inc.
+# All rights reserved.
+#
+# This source code is licensed under the BSD-style license found in the
+# LICENSE file in the root directory of this source tree. An additional grant
+# of patent rights can be found in the PATENTS file in the same directory.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import pandas as pd
+import numpy as np
+import warnings
+import holidays as hdays_part1
+import fbprophet.hdays as hdays_part2
+import inspect
+
+
+def generate_holidays_file():
+    """Generate csv file of all possible holiday names, ds,
+     and countries, year combination
+    """
+    years = np.arange(1995, 2045, 1)
+    all_holidays = []
+    # class names in holiday packages which are not countries
+    class_to_exclude = set(['rd', 'datetime', 'date', 'HolidayBase', 'Calendar',
+                            'LunarDate', 'timedelta', 'date'])
+
+    class_list2 = inspect.getmembers(hdays_part2, inspect.isclass)
+    country_set2 = set(list(zip(*class_list2))[0])
+    country_set2 -= class_to_exclude
+    for country in country_set2:
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            temp = getattr(hdays_part2, country)(years=years)
+        temp_df = pd.DataFrame(list(temp.items()),
+                                columns=['ds', 'holiday'])
+        temp_df['country'] = country
+        all_holidays.append(temp_df)
+
+    class_list1 = inspect.getmembers(hdays_part1, inspect.isclass)
+    country_set1 = set(list(zip(*class_list1))[0])
+    country_set1 -= class_to_exclude
+    # Avoid overwrting holidays get from hdays_part2
+    country_set1 -= country_set2
+    for country in country_set1:
+        temp = getattr(hdays_part1, country)(years=years)
+        temp_df = pd.DataFrame(list(temp.items()),
+                                columns=['ds', 'holiday'])
+        temp_df['country'] = country
+        all_holidays.append(temp_df)
+
+    generated_holidays = pd.concat(all_holidays, axis=0, ignore_index=True)
+    generated_holidays['year'] = generated_holidays.ds.apply(lambda x: x.year)
+    generated_holidays.to_csv("../R/data-raw/generated_holidays.csv")
+
+
+if __name__ == "__main__":
+    # execute only if run as a script
+    generate_holidays_file()