library(tidyverse)
library(magrittr)
library(knitr)
library(kableExtra)
library(sparkline)
library(reactable)
library(table1)
library(lubridate)
library(reactablefmtr)
library(htmltools)
library(flextable)
The U.S. General Services Administration sets rates for travel reimbursement annually for both lodging and for meals and incidental expenses. In 2023, the standard reimbursement for lodging will be $98 and the standard reimbursement for meals will be $59. Typically a large metropolis or a popular tourist destination will have its own per Diem that may vary depending on the season.
I have taken the per diem files created tables from multiple years to show what the rates would be during a holiday in the Spring (St. Patrick’s Day), the Summer (Independence Day), and the Fall (Thanksgiving Day). I create a reactable table that is filterable by year to show what the most expensive destinations may be.
I upload the data and reformat the dollar columns as numbers. I calculate the difference between the destination’s lodging rate and the standard US lodging rate. For destinations without seasonal rate dependence, I set the season to start on January 1st and end on December 31. I use the lubridate package to convert the dates for seasonal rates into time intervals that the holidays may fall into.
To compare data from different years, I move these data manipulations into a function. I run the function on data from 2020-2022 and combine the rows into a large tibble.
# get the standard rates for continental US over all years
standardrates <- read.csv("conusSTANDARDrates.csv",header=TRUE)
perdiem2023 <- read.csv("FY2023_PerDiemMasterRatesFile.csv",header=TRUE,skip=2)
newnames <- c("index","State","Destination","County","Season.Start","Season.End","Lodging","MandIE")
names(perdiem2023) <- newnames
perdiem2023 %<>% mutate(Lodging=as.integer(str_remove(Lodging,"[ $]"))) %>%
mutate(MandIE=as.integer(str_remove(MandIE,"[ $]"))) %>% select(-index) %>%
mutate(State=as.factor(State))
USAdefaultLodging <- (standardrates %>% filter(year==2023))$standardLodging
USAdefaultMandIE <- (standardrates %>% filter(year==2023))$standardMIE
perdiem2023 %<>% mutate(LdiffUSDefault=Lodging-USAdefaultLodging)
perdiem2023 %<>% mutate(MdiffUSDefault=MandIE-USAdefaultMandIE)
perdiem2023 %>% head(2)
## State Destination County Season.Start Season.End Lodging MandIE
## 1 AL Birmingham Jefferson 113 69
## 2 AL Gulf Shores Baldwin October 1 February 28 137 69
## LdiffUSDefault MdiffUSDefault
## 1 15 10
## 2 39 10
perdiem2023%<>% mutate(Season.Start=if_else(Season.Start=="","January 1",Season.Start))
perdiem2023%<>% mutate(Season.End=if_else(Season.End=="","December 31",Season.End))
perdiem2023%<>% mutate(Season.End=mdy(paste(Season.End," 2023")))
perdiem2023%<>% mutate(Season.Start=mdy(paste(Season.Start," 2023")))
perdiem2023 %<>% mutate(Season=interval(Season.Start,Season.End))
baddates1 <- perdiem2023 %>% filter(Season<0)
baddates2 <- perdiem2023 %>% filter(Season<0)
perdiem2023 %<>% filter(Season>0)
baddates1 %<>% mutate(Season.End=mdy("December 31 2023")) %>% mutate(Season=interval(Season.Start,Season.End))
baddates2 %<>% mutate(Season.Start=mdy("January 1 2023")) %>% mutate(Season=interval(Season.Start,Season.End))
PD2023 <- bind_rows(perdiem2023,baddates1,baddates2) %>% arrange(State,Destination,County,Season.Start)
PD2023 %<>% mutate(Holiday="NOT")
dfID <- PD2023 %>% mutate(Holiday=if_else(ymd("2023-07-4")%within%Season,"Independence Day",Holiday)) %>% dplyr::filter(Holiday!="NOT")
dfTD <- PD2023 %>% mutate(Holiday=if_else(ymd("2023-11-24")%within%Season,"Thanksgiving Day",Holiday)) %>% dplyr::filter(Holiday!="NOT")
dfPD <- PD2023 %>% mutate(Holiday=if_else(ymd("2023-3-17")%within%Season,"St. Patricks Day",Holiday)) %>% dplyr::filter(Holiday!="NOT")
PD2023 <- bind_rows(dfID,dfTD,dfPD) %>% arrange(State,Destination,County,Season.Start)
PD2023 %<>% mutate(Holiday=as.factor(Holiday))
PD2023 %<>%mutate(year=2023)
convert_PD_df <- function(mydf,myyear){
newnames <- c("index","State","Destination","County","Season.Start","Season.End","Lodging","MandIE")
names(mydf) <- newnames
mydf %<>% mutate(Lodging=as.integer(str_remove(Lodging,"[ $]"))) %>%
mutate(MandIE=as.integer(str_remove(MandIE,"[ $]"))) %>% select(-index) %>%
mutate(State=as.factor(State))
USAdefaultLodging <- (standardrates %>% filter(year==myyear))$standardLodging
USAdefaultMandIE <- (standardrates %>% filter(year==myyear))$standardMIE
mydf %<>% mutate(LdiffUSDefault=Lodging-USAdefaultLodging)
mydf %<>% mutate(MdiffUSDefault=MandIE-USAdefaultMandIE)
mydf %>% head(2)
mydf%<>% mutate(Season.Start=if_else(Season.Start=="","January 1",Season.Start))
mydf%<>% mutate(Season.End=if_else(Season.End=="","December 31",Season.End))
mydf%<>% mutate(Season.End=mdy(paste(Season.End,myyear)))
mydf%<>% mutate(Season.Start=mdy(paste(Season.Start,myyear)))
mydf %<>% mutate(Season=interval(Season.Start,Season.End))
baddates1 <- mydf %>% filter(Season<0)
baddates2 <- mydf %>% filter(Season<0)
mydf %<>% filter(Season>0)
yearend=paste("December 31",myyear)
yearstart=paste("January 1",myyear)
baddates1 %<>% mutate(Season.End=mdy(yearend)) %>%
mutate(Season=interval(Season.Start,Season.End))
baddates2 %<>% mutate(Season.Start=mdy(yearstart)) %>%
mutate(Season=interval(Season.Start,Season.End))
mynewPD <- bind_rows(mydf,baddates1,baddates2) %>%
arrange(State,Destination,County,Season.Start)
mynewPD %<>% mutate(Holiday="NOT")
july4=paste("July 4",myyear)
thankgiv <- paste("November 24",myyear)
stpaddy <- paste("March 17",myyear)
dfID <- mynewPD %>% mutate(Holiday=if_else(mdy(july4)%within%Season,
"Independence Day",Holiday)) %>%
dplyr::filter(Holiday!="NOT")
dfTD <- mynewPD %>% mutate(Holiday=if_else(mdy(thankgiv)%within%Season,
"Thanksgiving Day",Holiday)) %>%
dplyr::filter(Holiday!="NOT")
dfPD <- mynewPD %>% mutate(Holiday=if_else(mdy(stpaddy)%within%Season,
"St. Patricks Day",Holiday)) %>%
dplyr::filter(Holiday!="NOT")
mynewPD <- bind_rows(dfID,dfTD,dfPD) %>%
arrange(State,Destination,County,Season.Start)
mynewPD %<>% mutate(Holiday=as.factor(Holiday))
mynewPD %<>%mutate(year=as.integer(myyear))
return(mynewPD)
}
perdiem2022 <- read.csv("FY2022_PerDiemMasterRatesFile.csv",header=TRUE,skip=2)
perdiem2021 <- read.csv("FY2021_PerDiemMasterRatesFile.csv",header=TRUE,skip=2)
perdiem2020 <- read.csv("FY2020_PerDiemMasterRatesFile.csv",header=TRUE,skip=2)
PD_allyears <- bind_rows(PD2023,convert_PD_df(perdiem2022,2022),
convert_PD_df(perdiem2021,2021),
convert_PD_df(perdiem2020,2020))
PD_allyears %>% filter(Destination=="Fresno",Holiday=="Independence Day")
## State Destination County Season.Start Season.End Lodging MandIE
## 1 CA Fresno Fresno 2023-01-01 2023-12-31 113 69
## 2 CA Fresno Fresno 2022-01-01 2022-12-31 110 69
## 3 CA Fresno Fresno 2021-01-01 2021-12-31 110 66
## 4 CA Fresno Fresno 2020-01-01 2020-12-31 110 66
## LdiffUSDefault MdiffUSDefault Season Holiday
## 1 15 10 2023-01-01 UTC--2023-12-31 UTC Independence Day
## 2 14 10 2022-01-01 UTC--2022-12-31 UTC Independence Day
## 3 14 11 2021-01-01 UTC--2021-12-31 UTC Independence Day
## 4 14 11 2020-01-01 UTC--2020-12-31 UTC Independence Day
## year
## 1 2023
## 2 2022
## 3 2021
## 4 2020
I use table1 package to compare Lodging and Meals rates across different holidays for the 2023 data set. Then I create a table comparing Summer and Fall rates between New York and Florida for the 2023 set.
label(PD2023$MandIE) <- "Meals and Incidentals,USD"
label(PD2023$Lodging) <- "Lodging,USD"
tableA <- table1(~Lodging+MandIE|Holiday, data=PD2023 ,topclass="Rtable1-shade")
tableA
Independence Day (N=316) |
St. Patricks Day (N=316) |
Thanksgiving Day (N=316) |
Overall (N=948) |
|
---|---|---|---|---|
Lodging,USD | ||||
Mean (SD) | 151 (54.8) | 142 (54.3) | 131 (33.0) | 141 (49.1) |
Median [Min, Max] | 133 [98.0, 459] | 124 [98.0, 485] | 122 [98.0, 289] | 126 [98.0, 485] |
Meals and Incidentals,USD | ||||
Mean (SD) | 68.1 (5.47) | 68.1 (5.47) | 68.1 (5.47) | 68.1 (5.46) |
Median [Min, Max] | 69.0 [59.0, 79.0] | 69.0 [59.0, 79.0] | 69.0 [59.0, 79.0] | 69.0 [59.0, 79.0] |
tableB <- table1(~Lodging+MandIE|Holiday*State, data=PD2023 %>% filter(State%in%c("FL","NY"),Holiday%in%c("Independence Day","Thanksgiving Day")))
tableB
Independence Day |
Thanksgiving Day |
Overall |
||||
---|---|---|---|---|---|---|
FL (N=22) |
NY (N=19) |
FL (N=22) |
NY (N=19) |
FL (N=44) |
NY (N=38) |
|
Lodging,USD | ||||||
Mean (SD) | 163 (64.8) | 138 (37.5) | 139 (38.4) | 129 (41.4) | 151 (54.1) | 133 (39.3) |
Median [Min, Max] | 137 [98.0, 329] | 119 [101, 220] | 132 [98.0, 289] | 117 [98.0, 286] | 134 [98.0, 329] | 118 [98.0, 286] |
Meals and Incidentals,USD | ||||||
Mean (SD) | 67.2 (3.29) | 69.5 (4.68) | 67.2 (3.29) | 69.5 (4.68) | 67.2 (3.25) | 69.5 (4.62) |
Median [Min, Max] | 69.0 [59.0, 74.0] | 69.0 [64.0, 79.0] | 69.0 [59.0, 74.0] | 69.0 [64.0, 79.0] | 69.0 [59.0, 74.0] | 69.0 [64.0, 79.0] |
I created a Reactable table to show the most expensive Destinations to travel. I aggregate to display the maximum lodging and the mean meals expenses for each for each state by year. I allow selection of the data by year. As you drill down into the state’s destinations, there is a bar chart to show how much more expensive they would be compared to the standard lodging.
# PD_allyears%>%
# select(State, Destination, Lodging,LdiffUSDefault,MandIE, Holiday,year)
reactable_table <-PD_allyears %>%
group_by(State,year) %>%
dplyr::select(State, Destination, Lodging,LdiffUSDefault,MandIE, Holiday,year) %>%
reactable(groupBy = c("year","State"),
paginateSubRows = TRUE,
filterable = TRUE,
style = list(fontSize = "1rem"),columns = list(
year=colDef(name="Year",filterable=TRUE,
filterInput = function(values, name) {
tags$select(
# Set to undefined to clear the filter
onchange = sprintf("Reactable.setFilter('year-select',
'%s', event.target.value || undefined)", name),
# "All" has an empty value to clear the filter, and is the default option
tags$option(value = "", "All"),
lapply(unique(values), tags$option),
"aria-label" = sprintf("Filter %s", name),
style = "width: 100%; height: 28px;"
) },
),
State=colDef(
filterInput = function(values, name) {
tags$select(
# Set to undefined to clear the filter
onchange = sprintf("Reactable.setFilter('year-select',
'%s', event.target.value || undefined)", name),
# "All" has an empty value to clear the filter, and is the default option
tags$option(value = "", "All"),
lapply(unique(values), tags$option),
"aria-label" = sprintf("Filter %s", name),
style = "width: 100%; height: 28px;"
) }
),
Destination=colDef(
filterInput = function(values, name) {
tags$select(
# Set to undefined to clear the filter
onchange = sprintf("Reactable.setFilter('year-select',
'%s', event.target.value || undefined)", name),
# "All" has an empty value to clear the filter, and is the default option
tags$option(value = "", "All"),
lapply(unique(values), tags$option),
"aria-label" = sprintf("Filter %s", name),
style = "width: 100%; height: 28px;"
) }
),
Lodging = colDef(aggregate = "max",filterable = FALSE,
format = list(cell=colFormat(currency = "USD"),
aggregated = colFormat(prefix = "maximum =",
currency = "USD")),
name = "Lodging"),
LdiffUSDefault = colDef(name = "Over Standard Lodging Rate",filterable = FALSE,
cell = data_bars(., fill_color = c("red","black"),
fill_gradient = TRUE,
number_fmt=scales::number_format(accuracy=1)
)),
MandIE = colDef(aggregate = "mean",filterable = FALSE,
format = list(cell=colFormat(currency = "USD"),
aggregated = colFormat(prefix = "mean =",
currency = "USD")),
name = "Meals & Incidentals"),
Holiday=colDef(
filterInput = function(values, name) {
tags$select(
# Set to undefined to clear the filter
onchange = sprintf("Reactable.setFilter('year-select',
'%s', event.target.value || undefined)", name),
# "All" has an empty value to clear the filter, and is the default option
tags$option(value = "", "All"),
lapply(unique(values), tags$option),
"aria-label" = sprintf("Filter %s", name),
style = "width: 100%; height: 28px;"
) }
)
),
defaultSorted = "Lodging",
defaultSortOrder = "desc",
theme=clean(),
defaultPageSize = 15,
elementId = "year-select") %>% add_title("Per Diem Travel Rates ") %>%
add_subtitle("as issued by the US Goverment for various holidays.") %>%
add_source("https://www.gsa.gov/travel/plan-book/per-diem-rates/per-diem-files")
reactable_table
https://www.gsa.gov/travel/plan-book/per-diem-rates/per-diem-files