diff --git a/NAMESPACE b/NAMESPACE index 02ddcd3..f971ec4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,7 @@ # Generated by roxygen2: do not edit by hand S3method(print,northstar) +export(.as) export(assign_adjustments) export(calculate_amounts) export(carc_add_dash) diff --git a/R/aaa.R b/R/aaa.R index be7c0f1..29ff8ea 100644 --- a/R/aaa.R +++ b/R/aaa.R @@ -1,3 +1,9 @@ + +#' @keywords internal +#' +#' @autoglobal +#' +#' @export .as <- list( chr = \(...) as.character(...), int = \(...) as.integer(...), @@ -7,30 +13,3 @@ dte = \(...) as.Date(...), vct = \(...) as.vector(...) ) - - -# .sets <- new.env() -# -# .sets$ncci <- list( -# aoc = get_pin("ncci_aoc_nested"), -# mue = get_pin("ncci_mue_prac"), -# ptp = get_pin("ncci_ptp_prac") -# ) - -# .sets$ncci$aoc -# -# .pin <- list( -# adj = get_pin("adj_codes"), -# aoc = get_pin("ncci_aoc_nested"), -# mue = get_pin("ncci_mue_prac"), -# ptp = get_pin("ncci_ptp_prac"), -# rvu = \(type) switch(type, -# amt = get_pin("pfs_rvu_amt"), -# ind = get_pin("pfs_rvu_ind")), -# gpci = get_pin("pfs_gpci"), -# desc = get_pin("hcpcs_descriptions"), -# mod = get_pin("modifiers"), -# hcpcs = get_pin("hcpcs_lvl2"), -# pos = get_pin("pos_codes"), -# rbcs = get_pin("rbcs") -# ) diff --git a/R/adjustments.R b/R/adjustments.R index 852d736..bca1fca 100644 --- a/R/adjustments.R +++ b/R/adjustments.R @@ -1,6 +1,4 @@ -#' Search Adjustment Codes -#' -#' CARC and RARC Codes +#' Search Claim Adjustment Codes #' #' @details Claim Adjustment Reason Codes: #' @@ -12,11 +10,12 @@ #' #' The Claim Adjustment *Group Codes* are internal to the X12 standard. The #' format is always two alpha characters: -#' - **CO**: Contractual Obligations -#' - **CR**: Corrections and Reversals -#' - **OA**: Other Adjustments -#' - **PI**: Payer Initiated Reductions -#' - **PR**: Patient Responsibility +#' +#' - **CO** (Contractual Obligations): Indicates patient is responsible for remaining balance +#' - **CR** (Corrections and Reversals) +#' - **OA** (Other Adjustments): Indicates denial is related to other insurance coverage +#' - **PI** (Payer-Initiated Reductions) +#' - **PR** (Patient Responsibility): Indicates patient responsibility has been adjusted #' #' @details Remittance Advice Remark Codes: #' @@ -61,8 +60,7 @@ #' @export search_adjustments <- function(adj_code = NULL, adj_type = NULL, ...) { - adj_type <- if (!is.null(adj_type)) match.arg( - adj_type, + adj_type <- if (!is.null(adj_type)) match.arg(adj_type, c("Group", "CARC", "RARC")) adj <- get_pin("adj_codes") diff --git a/R/regex.R b/R/regex.R index d933971..dac36d4 100644 --- a/R/regex.R +++ b/R/regex.R @@ -110,7 +110,13 @@ construct_regex2 <- function(x) { purrr::list_c() |> paste0(collapse = "") - paste0("^", to_vec, "$") + x <- paste0("^", to_vec, "$") + + x <- gsub(paste0(0:9, collapse = ""), "0-9", x) + + x <- gsub(paste0(LETTERS, collapse = ""), "A-Z", x) + + return(x) } #' Internal function for `construct_regex2()` @@ -131,8 +137,7 @@ pos_re2 <- function(x) { paste0("[", fuimus::collapser(alphabet), fuimus::collapser(numbers), - "]", - "{1}" + "]" ) } diff --git a/R/standards.R b/R/standards.R index 32456d8..005a369 100644 --- a/R/standards.R +++ b/R/standards.R @@ -25,6 +25,9 @@ #' code = c("A0021", "V5362", "J9264", "G8916")) |> #' search_descriptions(column = "code") #' +#' search_descriptions() |> +#' dplyr::filter(is_cpt_category_III(hcpcs_code)) +#' #' @autoglobal #' #' @family HIPAA Standards @@ -203,6 +206,91 @@ search_plas <- function(hcpcs_code = NULL, ...) { #' of Medicare, Medicaid and private insurance services provided by a given #' provider. #' +#' ## Code Structure +#' +#' The first digit denotes service category and the second specifies location or +#' service type. +#' +#' POS code 11 represents an Office for direct patient services. +#' +#' For instance, code 11 designates an Office, encompassing +#' physician offices, clinics, group practices, and standalone facilities +#' providing direct patient services. +#' +#' Conversely, place of service code 22 +#' denotes an On Campus-Outpatient Hospital, covering services in a +#' hospital-based outpatient department where the patient is admitted as an +#' outpatient. +#' +#' * Structure: First digit for category, second for location/type +#' * Choose POS based on the majority of services in a specific encounter +#' * Modify POS codes if the location changes during a single encounter +#' +#' ## Location Types +#' +#' Based on the location of service, POS codes can be grouped into four +#' categories: Facility, Non-Facility, Telehealth, and Other. +#' +#' ### Facility Codes +#' +#' Facility POS codes are used to indicate services provided in a facility +#' setting such as hospitals, nursing homes, or skilled nursing facilities. +#' These include: +#' +#' * Urgent Care Facility (20) +#' * Inpatient Hospital (21) +#' * Outpatient Hospital (22) +#' * Emergency Room-Hospital (23) +#' * Ambulatory Surgical Center (24) +#' * Skilled Nursing Facility (31) +#' * Hospice Facility (32) +#' +#' These codes indicate that the services were provided in a facility that is +#' owned and operated by a healthcare provider. +#' +#' ### Non-Facility Codes +#' +#' Non-facility POS codes are used for services provided in non-facility +#' settings such as physician offices or independent clinics. These include: +#' +#' * School (03) +#' * Office (11) +#' * Home (12) +#' * Independent Clinic (49) +#' +#' The above codes indicate that the services were provided in a setting not +#' owned or operated by a healthcare provider. +#' +#' ### Telehealth POS Codes +#' +#' Telehealth place-of-service codes are used to indicate services provided +#' through telecommunication technology. These include: +#' +#' * Telehealth (02) +#' * Store and Forward Telemedicine Services (18) +#' +#' This category of POS codes was introduced due to the increasing use of +#' telehealth services in healthcare, and to differentiate them from traditional +#' in-person services. +#' +#' ### Other POS Codes +#' +#' In addition to facility-specific POS codes, there are other codes that play a +#' vital role in accurately describing healthcare encounters. These codes are +#' designed for specific scenarios, such as visits to retail clinics, public +#' health clinics, or rural health clinics. Each code serves a distinct purpose +#' in the healthcare landscape. +#' +#' * Homeless Shelter (04) +#' * Retail Clinic (17) +#' * Rural Health Clinic (72) +#' +#' These codes, including Retail Clinic (17), Home Shelter (04), and Rural +#' Health Clinic (72), cater to unique healthcare settings, ensuring +#' comprehensive coverage in billing and reporting. Understanding and applying +#' these codes appropriately contributes to the precision of healthcare +#' documentation and facilitates effective reimbursement processes. +#' #' @param pos_code `` vector of 2-character Place of Service codes; default #' is `NULL` #' diff --git a/R/utils.R b/R/utils.R index a1df0cb..e9ed400 100644 --- a/R/utils.R +++ b/R/utils.R @@ -2,6 +2,8 @@ #' #' @param source `` `"local"` or `"remote"` #' +#' @template args-dots +#' #' @returns `` or `` #' #' @autoglobal @@ -9,55 +11,63 @@ #' @keywords internal #' #' @export -mount_board <- function(source = c("local", "remote")) { +mount_board <- function(source = c("local", "remote"), ...) { source <- match.arg(source) switch( source, - local = pins::board_folder(fs::path_package("extdata/pins", package = "northstar")), - remote = pins::board_url("https://raw.githubusercontent.com/andrewallenbruce/northstar/master/inst/extdata/pins/")) + local = pins::board_folder( + fs::path_package( + "extdata/pins", + package = "northstar" + ) + ), + remote = pins::board_url( + "https://raw.githubusercontent.com/andrewallenbruce/northstar/master/inst/extdata/pins/" + ) + ) } -#' Get a pinned dataset from a [pins][pins::pins-package] board -#' -#' @param pin `` string name of pinned dataset +#' List pins from a [pins][pins::pins-package] board #' -#' @template args-dots +#' @param ... arguments to pass to [mount_board()] #' -#' @returns `` +#' @returns `` of [pins][pins::pins-package] #' #' @autoglobal #' #' @keywords internal #' #' @export -get_pin <- function(pin, ...) { +list_pins <- function(...) { board <- mount_board(...) - pin <- rlang::arg_match0(pin, list_pins()) - - pins::pin_read(board, pin) + pins::pin_list(board) } -#' List pins from a [pins][pins::pins-package] board +#' Get a pinned dataset from a [pins][pins::pins-package] board #' -#' @param ... arguments to pass to [mount_board()] +#' @param pin `` string name of pinned dataset #' -#' @returns `` of [pins][pins::pins-package] +#' @template args-dots +#' +#' @returns `` #' #' @autoglobal #' #' @keywords internal #' #' @export -list_pins <- function(...) { +get_pin <- function(pin, ...) { board <- mount_board(...) - pins::pin_list(board) + pin <- rlang::arg_match0(pin, list_pins()) + + pins::pin_read(board, pin) } @@ -80,7 +90,6 @@ get_example <- function(name = c("report", "practicum")) { } - #' Apply {gt} Theme #' #' @param gt_object `` A [gt][gt::gt-package] table object diff --git a/man/mount_board.Rd b/man/mount_board.Rd index 74d2a12..d481180 100644 --- a/man/mount_board.Rd +++ b/man/mount_board.Rd @@ -4,10 +4,12 @@ \alias{mount_board} \title{Mount \link[pins:pins-package]{pins} board} \usage{ -mount_board(source = c("local", "remote")) +mount_board(source = c("local", "remote"), ...) } \arguments{ \item{source}{\verb{} \code{"local"} or \code{"remote"}} + +\item{...}{These dots are for future extensions and must be empty.} } \value{ \verb{} or \verb{} diff --git a/man/search_adjustments.Rd b/man/search_adjustments.Rd index 8c64b99..82edff0 100644 --- a/man/search_adjustments.Rd +++ b/man/search_adjustments.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/adjustments.R \name{search_adjustments} \alias{search_adjustments} -\title{Search Adjustment Codes} +\title{Search Claim Adjustment Codes} \usage{ search_adjustments(adj_code = NULL, adj_type = NULL, ...) } @@ -18,7 +18,7 @@ default is \code{NULL}} a \link[tibble:tibble-package]{tibble} } \description{ -CARC and RARC Codes +Search Claim Adjustment Codes } \details{ Claim Adjustment Reason Codes: @@ -32,11 +32,11 @@ amounts. The Claim Adjustment \emph{Group Codes} are internal to the X12 standard. The format is always two alpha characters: \itemize{ -\item \strong{CO}: Contractual Obligations -\item \strong{CR}: Corrections and Reversals -\item \strong{OA}: Other Adjustments -\item \strong{PI}: Payer Initiated Reductions -\item \strong{PR}: Patient Responsibility +\item \strong{CO} (Contractual Obligations): Indicates patient is responsible for remaining balance +\item \strong{CR} (Corrections and Reversals) +\item \strong{OA} (Other Adjustments): Indicates denial is related to other insurance coverage +\item \strong{PI} (Payer-Initiated Reductions) +\item \strong{PR} (Patient Responsibility): Indicates patient responsibility has been adjusted } Remittance Advice Remark Codes: diff --git a/man/search_descriptions.Rd b/man/search_descriptions.Rd index 87abfd1..468fcce 100644 --- a/man/search_descriptions.Rd +++ b/man/search_descriptions.Rd @@ -41,6 +41,9 @@ dplyr::tibble( code = c("A0021", "V5362", "J9264", "G8916")) |> search_descriptions(column = "code") +search_descriptions() |> +dplyr::filter(is_cpt_category_III(hcpcs_code)) + } \seealso{ Other HIPAA Standards: diff --git a/man/search_pos.Rd b/man/search_pos.Rd index b58e51f..170b89b 100644 --- a/man/search_pos.Rd +++ b/man/search_pos.Rd @@ -44,6 +44,101 @@ set to be used for describing sites of service in such claims. POS information is often needed to determine the acceptability of direct billing of Medicare, Medicaid and private insurance services provided by a given provider. +\subsection{Code Structure}{ + +The first digit denotes service category and the second specifies location or +service type. + +POS code 11 represents an Office for direct patient services. + +For instance, code 11 designates an Office, encompassing +physician offices, clinics, group practices, and standalone facilities +providing direct patient services. + +Conversely, place of service code 22 +denotes an On Campus-Outpatient Hospital, covering services in a +hospital-based outpatient department where the patient is admitted as an +outpatient. +\itemize{ +\item Structure: First digit for category, second for location/type +\item Choose POS based on the majority of services in a specific encounter +\item Modify POS codes if the location changes during a single encounter +} +} + +\subsection{Location Types}{ + +Based on the location of service, POS codes can be grouped into four +categories: Facility, Non-Facility, Telehealth, and Other. +\subsection{Facility Codes}{ + +Facility POS codes are used to indicate services provided in a facility +setting such as hospitals, nursing homes, or skilled nursing facilities. +These include: +\itemize{ +\item Urgent Care Facility (20) +\item Inpatient Hospital (21) +\item Outpatient Hospital (22) +\item Emergency Room-Hospital (23) +\item Ambulatory Surgical Center (24) +\item Skilled Nursing Facility (31) +\item Hospice Facility (32) +} + +These codes indicate that the services were provided in a facility that is +owned and operated by a healthcare provider. +} + +\subsection{Non-Facility Codes}{ + +Non-facility POS codes are used for services provided in non-facility +settings such as physician offices or independent clinics. These include: +\itemize{ +\item School (03) +\item Office (11) +\item Home (12) +\item Independent Clinic (49) +} + +The above codes indicate that the services were provided in a setting not +owned or operated by a healthcare provider. +} + +\subsection{Telehealth POS Codes}{ + +Telehealth place-of-service codes are used to indicate services provided +through telecommunication technology. These include: +\itemize{ +\item Telehealth (02) +\item Store and Forward Telemedicine Services (18) +} + +This category of POS codes was introduced due to the increasing use of +telehealth services in healthcare, and to differentiate them from traditional +in-person services. +} + +\subsection{Other POS Codes}{ + +In addition to facility-specific POS codes, there are other codes that play a +vital role in accurately describing healthcare encounters. These codes are +designed for specific scenarios, such as visits to retail clinics, public +health clinics, or rural health clinics. Each code serves a distinct purpose +in the healthcare landscape. +\itemize{ +\item Homeless Shelter (04) +\item Retail Clinic (17) +\item Rural Health Clinic (72) +} + +These codes, including Retail Clinic (17), Home Shelter (04), and Rural +Health Clinic (72), cater to unique healthcare settings, ensuring +comprehensive coverage in billing and reporting. Understanding and applying +these codes appropriately contributes to the precision of healthcare +documentation and facilitates effective reimbursement processes. +} + +} } \examples{ search_pos(pos_code = c("11", "21")) diff --git a/vignettes/articles/calculations.Rmd b/vignettes/articles/calculations.Rmd index f54106b..0f33817 100644 --- a/vignettes/articles/calculations.Rmd +++ b/vignettes/articles/calculations.Rmd @@ -45,6 +45,19 @@ national average fee. Rates are adjusted according to geographic indices based on provider locality. Payers other than Medicare that adopt these relative values may apply a higher or lower conversion factor. + +```{r} +dplyr::tibble( + Component = c("Work", "Practice Expense", "Malpractice"), + Weight = c(0.51, 0.45, 0.04), + Description = c("Time, effort, skill, and stress associated with the physician’s performance of a service.", "Overhead costs of maintaining a practice, including staff, equipment, and supplies.", "Cost of malpractice insurance based on the risk associated with the service provided.") +) |> + gt::gt(groupname_col = "Component") |> + gt::fmt_percent(columns = 2, decimals = 0) |> + gt_theme_northstar() +``` + + ## How Medicare Part B Fees are Calculated by Providers There are many factors providers must take into account when calculating the diff --git a/vignettes/northstar.Rmd b/vignettes/northstar.Rmd index fcfe001..835b3f2 100644 --- a/vignettes/northstar.Rmd +++ b/vignettes/northstar.Rmd @@ -31,27 +31,44 @@ library(gt) ``` - * Calculate Patient Age -> Check ICD/HCPCS for Age Conflicts - * Calculate Provider Lag -> Days between DOS and DOR - * Calculate Balance -> Charges - (Payments + Adjustments) - * Calculate Coinsurance -> Charges - Allowed +```{r data, include = FALSE} +example <- get_example(name = "report") |> + dplyr::filter( + !hcpcs %in% c( + "WCPAIN", + "MATERIALCTR", + "MATERIALOFF", + "MEDREC", + "SPBARIATRIC", + "LETTER" + ), + class != "Self-Pay", + !is.na(allowed) + ) |> + dplyr::mutate( + .id = dplyr::row_number(), + .before = order + ) +``` + + + * Patient Age -> Check ICD/HCPCS for Age Conflicts + * Provider Lag -> Days between DOS and DOR + * Balance -> Charges - (Payments + Adjustments) + * Coinsurance -> Charges - Allowed -## Load Example +## Example Data ```{r} -(x <- get_example() |> - dplyr::filter(!hcpcs %in% c("WCPAIN", - "MATERIALCTR", - "MATERIALOFF", - "MEDREC", - "SPBARIATRIC", - "LETTER")) |> +example |> dplyr::mutate( - # coinsurance = charges - allowed, + # base_inspmt = allowed * 0.8, + # base_ptres = allowed * 0.2, + all_pmt = payments / allowed, balance = charges - (payments + adjustments), - .after = adjustments)) + .after = adjustments) ``` ## Place of Service @@ -59,56 +76,67 @@ library(gt) ```{r} pos_trie <- triebeard::trie( keys = search_pos()$pos_code, - values = chr(search_pos()$pos_type)) + values = .as$chr(search_pos()$pos_type)) -pos_trie -``` - - -```{r} -x_pos <- x |> +example <- example |> dplyr::mutate( pos_type = triebeard::longest_match(pos_trie, pos), .after = pos) -x_pos +example |> + dplyr::summarise( + claim_lines = dplyr::n(), + pmt_mean = mean(payments, na.rm = TRUE), + pmt_sum = sum(payments, na.rm = TRUE), + .by = c(class, pos_type)) |> + dplyr::arrange(class, dplyr::desc(pos_type)) |> + gt::gt( + groupname_col = "class", + rowname_col = "pos_type", + row_group_as_column = TRUE + ) |> + gt::fmt_currency( + columns = c(pmt_sum, pmt_mean), + decimals = 0) ``` ## Categorize HCPCS ```{r} -# hcpcs_unq <- collapse::funique(x$hcpcs[!x$hcpcs %in% not_hcpcs]) -hcpcs_unq <- collapse::funique(x$hcpcs) +hcpcs_unq <- collapse::funique(example$hcpcs) search_rbcs(hcpcs_code = hcpcs_unq) |> dplyr::select(hcpcs_code:rbcs_family) |> dplyr::left_join( search_descriptions(hcpcs_code = hcpcs_unq, - hcpcs_desc_type = "Long") |> + hcpcs_desc_type = "Short") |> dplyr::select(hcpcs_code, hcpcs_description), - by = dplyr::join_by(hcpcs_code)) + by = dplyr::join_by(hcpcs_code)) |> + dplyr::select(hcpcs_code, rbcs_family, hcpcs_description) |> + gt::gt() ``` ## Define Modifiers ```{r} -mod_unq <- strsplit(toupper(x$mod[!is.na(x$mod)]), "-") |> - list_c() |> - funique() +mod_unq <- strsplit(toupper(example$mod[!is.na(example$mod)]), "-") |> + purrr::list_c() |> + collapse::funique() search_modifiers(mod_code = mod_unq) |> - select(mod_code, mod_description) + dplyr::select(mod_code, mod_description) |> + gt::gt() ``` ```{r} -x |> - select(claim_id, dos, order, hcpcs) |> - group_by(claim_id) |> - filter(n() > 1) |> - ungroup() +example |> + dplyr::select(claim_id, dos, .id, order, hcpcs) |> + dplyr::group_by(claim_id) |> + dplyr::filter(dplyr::n() > 1) |> + dplyr::ungroup() ``` ```{r} @@ -116,28 +144,28 @@ aocs <- search_aocs(hcpcs_code = hcpcs_unq) |> tidyr::unnest(cols = aoc_complements) aoc_pairs <- aocs |> - filter(aoc_type == "Primary") |> - select(hcpcs_primary = hcpcs_code, hcpcs_addon = aoc_complement) + dplyr::filter(aoc_type == "Primary") |> + dplyr::select( + hcpcs_primary = hcpcs_code, + hcpcs_addon = aoc_complement) -ex_aoc <- x |> - select(claim_id, dos, order, hcpcs) |> - group_by(claim_id) |> - filter(n() > 1) |> - ungroup() +ex_aoc <- example |> + dplyr::select(claim_id, dos, order, hcpcs) |> + dplyr::group_by(claim_id) |> + dplyr::filter(dplyr::n() > 1) |> + dplyr::ungroup() prim <- ex_aoc |> - filter(order == 1) |> - select(claim_id, dos, hcpcs_primary = hcpcs) + dplyr::filter(order == 1) |> + dplyr::select(claim_id, dos, hcpcs_primary = hcpcs) addon <- ex_aoc |> - filter(order != 1) |> - select(claim_id, dos, order_addon = order, hcpcs_addon = hcpcs) + dplyr::filter(order != 1) |> + dplyr::select(claim_id, dos, order_addon = order, hcpcs_addon = hcpcs) addon |> - left_join(prim, by = join_by(claim_id, dos)) |> - filter(claim_id %in% c('46440-13', '16057-35', '57128-01')) #|> - # semi_join(aoc_pairs) |> - # pull(claim_id) + dplyr::left_join(prim, by = dplyr::join_by(claim_id, dos)) |> + dplyr::filter(claim_id %in% c('46440-13', '16057-35', '57128-01')) ```