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
This commit is contained in:
ziye666 2018-10-18 17:40:17 -07:00 committed by Ben Letham
parent ad095ac690
commit 95fa4460a9
20 changed files with 67980 additions and 163 deletions

View file

@ -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

View file

@ -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
R/R/data.R Normal file
View file

@ -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"

View file

@ -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
R/R/make_holidays.R Normal file
View file

@ -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)
}

View file

@ -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

View file

@ -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)

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -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}

View file

@ -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
R/man/make_holidays_df.Rd Normal file
View file

@ -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
}

View file

@ -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}

View file

@ -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

View file

@ -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')

View file

@ -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, ]

View file

@ -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"

View file

@ -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:

View file

@ -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)

View file

@ -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()