This report presents an exploratory and predictive analysis of inpatient hospital care patterns using a dataset of 55,500 patient records spanning May 2019 through May 2024. The analysis examines length of stay, billing patterns, admission characteristics, and test result outcomes across six medical conditions, three admission types, and five insurance providers.
Key findings:
Average length of stay was 15.5 days across all admission types and medical conditions, with remarkably consistent distributions suggesting stable operational patterns across the patient population.
Billing amounts averaged $25,539 per admission with no material difference across insurance providers, indicating a balanced payer mix with no disproportionate revenue dependency on any single insurer.
Test results were evenly distributed across Normal (33.4%), Abnormal (33.6%), and Inconclusive (33.0%) outcomes — an unusual uniformity that informed the interpretation of predictive modeling results.
Cancer patients admitted via emergency represented the longest average length of stay at 16 days, the only condition-admission type combination to meaningfully exceed the overall average.
Predictive modeling confirmed that patient-level variables available in this dataset do not independently predict length of stay or test result outcomes, a finding consistent with synthetically generated data. Recommendations for extending this analysis with real clinical data are provided in the final section.
Hospital administrators and clinical operations teams routinely monitor a core set of performance indicators to manage capacity, control costs, and improve patient outcomes. Among the most closely watched are length of stay (LOS) and 30-day readmission rates — both publicly reported by the Centers for Medicare and Medicaid Services (CMS) and directly tied to hospital reimbursement under the Hospital Readmissions Reduction Program (HRRP).
This analysis asks three questions relevant to that operational context:
The dataset used in this analysis is a structured patient records dataset containing 55,500 inpatient encounters sourced from Kaggle (Prasad, 2024). It includes demographic information, admission and discharge dates, medical conditions, medications, insurance provider, billing amounts, and test results.
# Dataset dimensions and date range
tibble(
Metric = c("Total Records", "Variables", "Date Range",
"Medical Conditions", "Insurance Providers",
"Admission Types"),
Value = c(
format(nrow(df_clean), big.mark = ","),
ncol(df_clean),
paste(min(df_clean$date_of_admission), "to",
max(df_clean$date_of_admission)),
nlevels(df_clean$medical_condition),
nlevels(df_clean$insurance_provider),
nlevels(df_clean$admission_type)
)
) %>%
kable(
format = "html",
caption = "Table 1: Dataset Overview",
align = c("l", "l")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE,
position = "left"
) %>%
column_spec(1, bold = TRUE) %>%
row_spec(0, background = "#2C3E50", color = "white")
| Metric | Value |
|---|---|
| Total Records | 55,500 |
| Variables | 21 |
| Date Range | 2019-05-08 to 2024-05-07 |
| Medical Conditions | 6 |
| Insurance Providers | 5 |
| Admission Types | 3 |
A note on data provenance: The uniformity observed throughout this analysis is consistent with synthetically generated data, where outcomes are sampled independently of patient characteristics rather than reflecting true clinical relationships. Findings should be interpreted in this context. Section 7 discusses how this analysis would extend to real administrative data sources such as HCUP or institutional EHR exports. —
Rather than applying cleaning operations individually to each variable — a common but brittle approach — this analysis implements a reusable cleaning function applied systematically across the dataset. This design means the same cleaning logic can be applied to new data files, additional monthly extracts, or department-level subsets without rewriting any code. The full cleaning function is available in the project GitHub repository.
The following operations were applied to produce the analytical dataset:
tibble(
Operation = c(
"Column name standardization",
"Name capitalization correction",
"Date parsing",
"Length of stay calculation",
"Admission month extraction",
"Admission day of week extraction",
"Age group classification",
"Billing tier classification",
"Data quality flagging",
"Factor conversion"
),
Detail = c(
"Snake_case applied via janitor::clean_names()",
"Inconsistent mixed case corrected to title case",
"Date.of.Admission and Discharge.Date parsed from character to Date type",
"Derived as integer difference between discharge and admission dates",
"Extracted from admission date as ordered factor (Jan–Dec)",
"Extracted from admission date as ordered factor (Monday–Sunday)",
"Binned into Pediatric, Young Adult, Middle Aged, Senior",
"Binned into Low (<$15K), Medium ($15K–$35K), High (>$35K)",
"108 records with negative billing amounts flagged and excluded from cost analysis",
"Nine categorical variables converted from character to factor type"
)
) %>%
kable(
format = "html",
caption = "Table 2: Data Preparation Operations",
align = c("l", "l")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE
) %>%
column_spec(1, bold = TRUE, width = "30%") %>%
column_spec(2, width = "70%") %>%
row_spec(0, background = "#2C3E50", color = "white")
| Operation | Detail |
|---|---|
| Column name standardization | Snake_case applied via janitor::clean_names() |
| Name capitalization correction | Inconsistent mixed case corrected to title case |
| Date parsing | Date.of.Admission and Discharge.Date parsed from character to Date type |
| Length of stay calculation | Derived as integer difference between discharge and admission dates |
| Admission month extraction | Extracted from admission date as ordered factor (Jan–Dec) |
| Admission day of week extraction | Extracted from admission date as ordered factor (Monday–Sunday) |
| Age group classification | Binned into Pediatric, Young Adult, Middle Aged, Senior |
| Billing tier classification | Binned into Low (<$15K), Medium ($15K–$35K), High (>$35K) |
| Data quality flagging | 108 records with negative billing amounts flagged and excluded from cost analysis |
| Factor conversion | Nine categorical variables converted from character to factor type |
# Missing value assessment
missing_summary <- df_clean %>%
summarise(across(everything(), ~ sum(is.na(.)))) %>%
pivot_longer(everything(),
names_to = "Column",
values_to = "Missing Values")
# Data quality flag summary
quality_summary <- df_clean %>%
count(data_quality_flag) %>%
mutate(
Percentage = round(n / nrow(df_clean) * 100, 2)
) %>%
rename(
"Flag" = data_quality_flag,
"Records" = n,
"% of Total" = Percentage
)
quality_summary %>%
kable(
format = "html",
caption = "Table 3: Data Quality Flag Summary",
align = c("l", "r", "r")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE,
position = "left"
) %>%
column_spec(1, bold = TRUE) %>%
row_spec(0, background = "#2C3E50", color = "white") %>%
row_spec(1, color = "#27AE60") %>% # green for OK records
row_spec(2, color = "#E74C3C") # red for flagged records
| Flag | Records | % of Total |
|---|---|---|
| Negative billing — exclude from cost analysis | 108 | 0.19 |
| OK | 55392 | 99.81 |
Data quality note: 108 records (0.19% of the dataset) carried negative billing amounts with no clinical explanation. These records were flagged rather than silently removed, preserving the audit trail while excluding them from all cost-related analysis. All 55,500 records are retained in the master dataset for non-billing analyses.
Five analytical variables were derived from existing fields to support downstream exploration and modeling:
tibble(
`New Variable` = c("length_of_stay", "admission_month",
"admission_day_of_week", "age_group",
"billing_tier"),
`Derived From` = c("discharge_date − date_of_admission",
"date_of_admission",
"date_of_admission",
"age",
"billing_amount"),
`Type` = c("Integer", "Ordered factor",
"Ordered factor", "Factor", "Factor"),
`Purpose` = c("Primary outcome variable for LOS analysis",
"Seasonal pattern detection",
"Admission timing analysis",
"Demographic segmentation",
"Cost tier segmentation")
) %>%
kable(
format = "html",
caption = "Table 4: Engineered Features",
align = c("l", "l", "l", "l")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE
) %>%
column_spec(1, bold = TRUE, color = "#2C3E50") %>%
row_spec(0, background = "#2C3E50", color = "white")
| New Variable | Derived From | Type | Purpose |
|---|---|---|---|
| length_of_stay | discharge_date − date_of_admission | Integer | Primary outcome variable for LOS analysis |
| admission_month | date_of_admission | Ordered factor | Seasonal pattern detection |
| admission_day_of_week | date_of_admission | Ordered factor | Admission timing analysis |
| age_group | age | Factor | Demographic segmentation |
| billing_tier | billing_amount | Factor | Cost tier segmentation |
Exploratory analysis examined four dimensions of the patient population: length of stay, billing patterns, admission volume over time, and the interaction between medical condition and admission type. Visualizations were developed using a consistent custom theme to support readability across all charts.
Length of stay is one of the most closely monitored operational metrics in hospital administration, directly influencing staffing requirements, bed capacity planning, and reimbursement under prospective payment systems.
ggplot(df_clean,
aes(x = reorder(medical_condition, length_of_stay, median),
y = length_of_stay,
fill = medical_condition)) +
geom_boxplot(alpha = 0.8, outlier.size = 1, outlier.alpha = 0.3) +
scale_fill_manual(values = condition_colors) +
coord_flip() +
labs(
title = "Length of Stay by Medical Condition",
subtitle = "Median LOS is similar across conditions — distributions reveal the real variation",
x = NULL,
y = "Length of Stay (Days)",
caption = "Source: Kaggle Healthcare Dataset | n = 55,500"
) +
theme_hospital() +
theme(legend.position = "none")
Figure 1: Length of Stay Distribution by Medical Condition
LOS distributions were broadly consistent across all six medical conditions, with medians ranging from approximately 14 to 16 days. The interquartile ranges overlapped substantially across conditions, suggesting that diagnosis alone is not a strong determinant of inpatient stay duration in this dataset. In a real clinical environment, procedure complexity, comorbidity burden, and discharge disposition would be expected to introduce greater condition-level variation.
Understanding billing patterns across payers is a core function of hospital financial analytics, informing contract negotiations, revenue cycle management, and payer mix strategy.
ggplot(df_billing,
aes(x = reorder(insurance_provider, billing_amount, median),
y = billing_amount,
fill = insurance_provider)) +
geom_violin(alpha = 0.7, color = NA) +
geom_boxplot(width = 0.15, fill = "white", color = "#333333",
outlier.size = 0.8, outlier.alpha = 0.3) +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(labels = dollar_format()) +
coord_flip() +
labs(
title = "Billing Amount Distribution by Insurance Provider",
subtitle = "Violin width shows frequency — wider = more patients at that billing level",
x = NULL,
y = "Billing Amount (USD)",
caption = "Source: Kaggle Healthcare Dataset | Negative billing values excluded (n = 108)"
) +
theme_hospital() +
theme(legend.position = "none")
Figure 2: Billing Amount Distribution by Insurance Provider
Billing distributions were remarkably consistent across all five insurance providers, with median amounts clustering around $25,500 regardless of payer. This uniformity indicates a balanced payer mix with no disproportionate revenue concentration — a favorable characteristic from a financial stability standpoint. It also suggests that reimbursement rates do not vary materially by insurer in this dataset, which would be unusual in real administrative data where Medicare and Medicaid typically reimburse at lower rates than commercial insurers.
Seasonal admission patterns inform staffing models, supply chain planning, and capacity management decisions across hospital operations teams.
df_clean %>%
count(admission_month, admission_type) %>%
ggplot(aes(x = admission_month,
y = n,
color = admission_type,
group = admission_type)) +
geom_line(linewidth = 1.1) +
geom_point(size = 2.5) +
scale_color_manual(values = hospital_colors) +
scale_y_continuous(limits = c(0, NA)) +
facet_wrap(~ admission_type, ncol = 1) +
labs(
title = "Monthly Admission Volume by Admission Type",
subtitle = "Examining seasonal patterns across Elective, Emergency, and Urgent admissions",
x = NULL,
y = "Number of Admissions",
caption = "Source: Kaggle Healthcare Dataset"
) +
theme_hospital() +
theme(legend.position = "none")
Figure 3: Monthly Admission Volume by Admission Type
Admission volumes showed no clinically meaningful seasonal pattern across any of the three admission types. Elective, Emergency, and Urgent admissions all remained stable throughout the year with minor month-to-month fluctuation attributable to normal statistical variation. In real hospital data, emergency admissions typically spike during winter months due to respiratory illness seasonality, and elective admissions often dip in summer and major holiday periods. The absence of these patterns further supports the synthetic nature of the dataset.
Single-variable analysis revealed limited variation in LOS. Examining the interaction between medical condition and admission type provides a more granular view of where stay duration differences emerge.
df_clean %>%
group_by(medical_condition, admission_type) %>%
summarise(avg_los = round(mean(length_of_stay), 1),
.groups = "drop") %>%
ggplot(aes(x = admission_type,
y = medical_condition,
fill = avg_los)) +
geom_tile(color = "white", linewidth = 0.8) +
geom_text(aes(label = avg_los), size = 4, fontface = "bold",
color = "white") +
scale_fill_gradient(low = "#D6E4F0",
high = "#1A5276",
name = "Avg LOS (Days)") +
labs(
title = "Average Length of Stay by Condition and Admission Type",
subtitle = "Darker blue = longer average stay",
x = "Admission Type",
y = NULL,
caption = "Source: Kaggle Healthcare Dataset"
) +
theme_hospital() +
theme(
panel.grid = element_blank(),
legend.position = "right"
)
Figure 4: Average LOS by Condition and Admission Type
The heatmap reveals that the overall range of average LOS across all condition-admission type combinations spans only 15.1 to 16.0 days — a spread of less than one day across 18 distinct patient subgroups. Cancer patients admitted via emergency represented the longest average stay at 16.0 days, while diabetic patients admitted for urgent care showed the shortest at 15.1 days. The narrow range confirmed that no single combination of condition and admission type drives meaningfully differentiated LOS outcomes, motivating the multivariate modeling approach described in the following section. —
Exploratory analysis consistently revealed narrow distributions
across patient subgroups, suggesting that no single variable
meaningfully differentiates patient outcomes in this dataset. Two
supervised learning models were developed using the
tidymodels framework to formally test whether combinations
of patient-level variables could predict length of stay or test result
outcomes.
Both models followed the same structured workflow:
# Reproducible train/test split
set.seed(42)
df_model <- df_clean %>%
filter(data_quality_flag == "OK") %>%
mutate(
abnormal_test = factor(
if_else(test_results == "Abnormal", "Abnormal", "Not_Abnormal"),
levels = c("Not_Abnormal", "Abnormal")
)
) %>%
select(length_of_stay, abnormal_test, age, gender,
medical_condition, admission_type, insurance_provider,
billing_tier, age_group)
hospital_split <- initial_split(df_model, prop = 0.80,
strata = length_of_stay)
hospital_train <- training(hospital_split)
hospital_test <- testing(hospital_split)
# Display split summary
tibble(
Dataset = c("Training Set", "Test Set", "Total"),
Records = c(nrow(hospital_train),
nrow(hospital_test),
nrow(hospital_train) + nrow(hospital_test)),
Proportion = c("80%", "20%", "100%")
) %>%
kable(
format = "html",
caption = "Table 5: Train/Test Split Summary",
align = c("l", "r", "r")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE,
position = "left"
) %>%
column_spec(1, bold = TRUE) %>%
row_spec(0, background = "#2C3E50", color = "white") %>%
row_spec(3, bold = TRUE, background = "#F5F5F5")
| Dataset | Records | Proportion |
|---|---|---|
| Training Set | 44312 | 80% |
| Test Set | 11080 | 20% |
| Total | 55392 | 100% |
# Recipe
los_recipe <- recipe(
length_of_stay ~ age + gender + medical_condition +
admission_type + insurance_provider +
billing_tier + age_group,
data = hospital_train
) %>%
step_dummy(all_nominal_predictors(), one_hot = FALSE) %>%
step_zv(all_predictors()) %>%
step_normalize(all_numeric_predictors())
# Specification
los_spec <- linear_reg() %>%
set_engine("lm") %>%
set_mode("regression")
# Workflow and fit
los_fit <- workflow() %>%
add_recipe(los_recipe) %>%
add_model(los_spec) %>%
fit(data = hospital_train)
# Predictions on test set
los_predictions <- los_fit %>%
predict(hospital_test) %>%
bind_cols(hospital_test %>%
select(length_of_stay, medical_condition,
admission_type, age_group))
# Metrics
los_metrics <- los_predictions %>%
metrics(truth = length_of_stay, estimate = .pred)
los_metrics %>%
filter(.metric %in% c("rmse", "rsq", "mae")) %>%
mutate(
Metric = case_when(
.metric == "rmse" ~ "RMSE (Days)",
.metric == "rsq" ~ "R-Squared",
.metric == "mae" ~ "MAE (Days)"
),
Value = case_when(
.metric == "rsq" ~ round(.estimate, 6),
TRUE ~ round(.estimate, 2)
)
) %>%
select(Metric, Value) %>%
kable(
format = "html",
caption = "Table 6: Linear Regression Performance Metrics",
align = c("l", "r")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE,
position = "left"
) %>%
column_spec(1, bold = TRUE) %>%
row_spec(0, background = "#2C3E50", color = "white")
| Metric | Value |
|---|---|
| RMSE (Days) | 8.650000 |
| R-Squared | 0.000052 |
| MAE (Days) | 7.510000 |
ggplot(los_predictions, aes(x = length_of_stay, y = .pred)) +
geom_point(alpha = 0.15, size = 1.2, color = "#2C3E50") +
geom_abline(slope = 1, intercept = 0,
color = "#E74C3C",
linewidth = 1,
linetype = "dashed") +
scale_x_continuous(limits = c(0, 35)) +
scale_y_continuous(limits = c(0, 35)) +
labs(
title = "Predicted vs Actual Length of Stay",
subtitle = "Points falling on the red line indicate perfect prediction",
x = "Actual LOS (Days)",
y = "Predicted LOS (Days)",
caption = "Evaluated on held-out test set (n = 11,080)"
) +
theme_hospital()
Figure 5: Predicted vs Actual Length of Stay
The linear regression model yielded an R² of approximately 0.00 and an RMSE of 8.65 days, indicating that the selected patient-level variables do not meaningfully predict length of stay. The predicted vs actual plot confirms this — rather than tracking the diagonal reference line, predictions cluster in a narrow horizontal band centered near the dataset mean of 15.5 days. The model is effectively predicting the population average for every patient regardless of their characteristics, which is the expected behavior when predictors carry no true signal.
# Recipe
logistic_recipe <- recipe(
abnormal_test ~ age + gender + medical_condition +
admission_type + insurance_provider +
billing_tier + age_group + length_of_stay,
data = hospital_train
) %>%
step_dummy(all_nominal_predictors(), one_hot = FALSE) %>%
step_zv(all_predictors()) %>%
step_normalize(all_numeric_predictors())
# Specification
logistic_spec <- logistic_reg() %>%
set_engine("glm") %>%
set_mode("classification")
# Workflow and fit
logistic_fit <- workflow() %>%
add_recipe(logistic_recipe) %>%
add_model(logistic_spec) %>%
fit(data = hospital_train)
# Predictions
logistic_predictions <- logistic_fit %>%
predict(hospital_test) %>%
bind_cols(
predict(logistic_fit, hospital_test, type = "prob"),
hospital_test %>%
select(abnormal_test, medical_condition,
admission_type, age_group)
)
# Confusion matrix
conf_mat_result <- logistic_predictions %>%
conf_mat(truth = abnormal_test, estimate = .pred_class)
# Metrics table
logistic_predictions %>%
metrics(truth = abnormal_test,
estimate = .pred_class) %>%
mutate(
Metric = case_when(
.metric == "accuracy" ~ "Accuracy",
.metric == "kap" ~ "Kappa",
TRUE ~ .metric
),
Value = round(.estimate, 4)
) %>%
select(Metric, Value) %>%
kable(
format = "html",
caption = "Table 7: Logistic Regression Performance Metrics",
align = c("l", "r")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE,
position = "left"
) %>%
column_spec(1, bold = TRUE) %>%
row_spec(0, background = "#2C3E50", color = "white")
| Metric | Value |
|---|---|
| Accuracy | 0.6649 |
| Kappa | 0.0000 |
conf_mat_result %>%
autoplot(type = "mosaic") +
labs(
title = "Confusion Matrix — Abnormal Test Result Prediction",
subtitle = "Rows = Actual | Columns = Predicted",
caption = "Evaluated on held-out test set (n = 11,080)"
) +
theme_hospital()
Figure 6: Confusion Matrix — Abnormal Test Result Prediction
The logistic regression model achieved an accuracy of 66.5% with a Kappa of 0.00 — confirming it added no predictive value beyond naively predicting the majority class for every observation. The confusion matrix shows the model predicted “Not Abnormal” for all 11,080 test records, never once classifying a patient as having an abnormal result. This is known as the null classifier or majority class problem: when no learnable signal exists in the predictors, a classification model defaults to always predicting the most frequent outcome.
Modeling interpretation: An accuracy of 66.5% sounds reasonable in isolation but is entirely meaningless here — the same score is achievable by predicting “Not Abnormal” for every patient with a single line of code. Kappa, which measures accuracy beyond what chance alone would produce, is the more honest metric and its value of 0.00 tells the complete story. Reporting null results transparently is standard practice in rigorous analytical work — a model that appears to perform well on synthetic data by overfitting to noise would produce dangerously misleading conclusions if applied to real patient populations. —
The most significant limitation of this analysis is the synthetic nature of the underlying dataset. Several characteristics of the data are inconsistent with real administrative hospital data and should be understood before drawing operational conclusions:
tibble(
Limitation = c(
"Uniform outcome distributions",
"No payer reimbursement differential",
"Absent seasonal admission patterns",
"No comorbidity information",
"No procedure or ICD-10 codes",
"No readmission data",
"Single facility implied",
"No discharge disposition detail"
),
`Observed Pattern` = c(
"Test results split evenly ~33% across three categories",
"Billing amounts nearly identical across all five insurers",
"Admission volume flat across all twelve months",
"No comorbidity scores or secondary diagnoses available",
"Medical conditions represented as free text labels only",
"No 30-day readmission flag or subsequent encounter data",
"No facility identifier — cross-site comparison not possible",
"Discharge to home, rehab, or skilled nursing not recorded"
),
`Real Data Expectation` = c(
"Abnormal results concentrated in specific conditions and age groups",
"Medicare and Medicaid typically reimburse at 60–80% of commercial rates",
"Emergency admissions spike in winter; elective dips in holiday periods",
"Elixhauser or Charlson comorbidity indices materially predict LOS",
"ICD-10 diagnosis and procedure codes enable granular clinical grouping",
"Readmission rates are publicly reported CMS quality metrics",
"Multi-facility data enables benchmarking and site-level variation analysis",
"Discharge disposition is a strong predictor of readmission risk"
)
) %>%
kable(
format = "html",
caption = "Table 8: Dataset Limitations and Real-World Expectations",
align = c("l", "l", "l")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE
) %>%
column_spec(1, bold = TRUE, width = "25%") %>%
column_spec(2, width = "35%") %>%
column_spec(3, width = "40%") %>%
row_spec(0, background = "#2C3E50", color = "white")
| Limitation | Observed Pattern | Real Data Expectation |
|---|---|---|
| Uniform outcome distributions | Test results split evenly ~33% across three categories | Abnormal results concentrated in specific conditions and age groups |
| No payer reimbursement differential | Billing amounts nearly identical across all five insurers | Medicare and Medicaid typically reimburse at 60–80% of commercial rates |
| Absent seasonal admission patterns | Admission volume flat across all twelve months | Emergency admissions spike in winter; elective dips in holiday periods |
| No comorbidity information | No comorbidity scores or secondary diagnoses available | Elixhauser or Charlson comorbidity indices materially predict LOS |
| No procedure or ICD-10 codes | Medical conditions represented as free text labels only | ICD-10 diagnosis and procedure codes enable granular clinical grouping |
| No readmission data | No 30-day readmission flag or subsequent encounter data | Readmission rates are publicly reported CMS quality metrics |
| Single facility implied | No facility identifier — cross-site comparison not possible | Multi-facility data enables benchmarking and site-level variation analysis |
| No discharge disposition detail | Discharge to home, rehab, or skilled nursing not recorded | Discharge disposition is a strong predictor of readmission risk |
Beyond data provenance, two methodological limitations apply to the modeling section specifically:
Feature availability: The predictors available in this dataset — demographics, admission type, insurance provider, and medical condition label — represent a minimal feature set relative to what clinical decision support models typically incorporate. Real LOS prediction models in production hospital environments commonly include procedure complexity scores, prior utilization history, social determinants of health, and real-time clinical measurements.
Model scope: This analysis evaluated linear and logistic regression as interpretable baseline models appropriate for an administrative data context. More flexible approaches such as gradient boosting or random forests were not pursued, as added model complexity is unlikely to recover signal that does not exist in the underlying data. In a real clinical dataset with richer features, ensemble methods would be a natural extension of this baseline work.
The analytical framework developed here — a reusable cleaning pipeline, consistent visualization theme, and structured modeling workflow — translates directly to real administrative data. The following extensions are recommended when applying this approach to institutional data sources:
tibble(
Priority = c("High", "High", "Medium", "Medium", "Low"),
Recommendation = c(
"Incorporate ICD-10 diagnosis and procedure codes",
"Add 30-day readmission flag as a modeling outcome",
"Integrate Elixhauser comorbidity index scores",
"Expand to multi-facility data for benchmarking",
"Explore ensemble modeling methods"
),
Rationale = c(
"Enables clinical grouping via DRG classification, materially improving
LOS prediction and condition-level analysis",
"Readmission is a CMS-reported quality metric with direct reimbursement
implications under the Hospital Readmissions Reduction Program",
"Comorbidity burden is consistently among the strongest predictors of
LOS and readmission risk in published clinical literature",
"Cross-site variation analysis enables identification of high-performing
facilities and dissemination of best practices",
"Gradient boosting and random forest models are well-suited to the
high-dimensional feature spaces produced by ICD-10 coding"
)
) %>%
kable(
format = "html",
caption = "Table 9: Recommendations for Extension to Real Clinical Data",
align = c("l", "l", "l")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE
) %>%
column_spec(1, bold = TRUE, width = "10%",
color = if_else(c(TRUE, TRUE, FALSE, FALSE, FALSE),
"#E74C3C", "#F39C12")) %>%
column_spec(2, bold = TRUE, width = "35%") %>%
column_spec(3, width = "55%") %>%
row_spec(0, background = "#2C3E50", color = "white")
| Priority | Recommendation | Rationale |
|---|---|---|
| High | Incorporate ICD-10 diagnosis and procedure codes | Enables clinical grouping via DRG classification, materially improving LOS prediction and condition-level analysis |
| High | Add 30-day readmission flag as a modeling outcome | Readmission is a CMS-reported quality metric with direct reimbursement implications under the Hospital Readmissions Reduction Program |
| Medium | Integrate Elixhauser comorbidity index scores | Comorbidity burden is consistently among the strongest predictors of LOS and readmission risk in published clinical literature |
| Medium | Expand to multi-facility data for benchmarking | Cross-site variation analysis enables identification of high-performing facilities and dissemination of best practices |
| Low | Explore ensemble modeling methods | Gradient boosting and random forest models are well-suited to the high-dimensional feature spaces produced by ICD-10 coding |
Three immediate next steps would meaningfully strengthen this work:
1. Apply to HCUP data. The Healthcare Cost and Utilization Project provides de-identified inpatient administrative data with ICD-10 codes, DRG classifications, comorbidity flags, and discharge disposition for millions of encounters annually. The cleaning function and modeling workflow developed here require minimal modification to operate on HCUP data structures.
2. Add a readmission outcome variable. Linking index admissions to subsequent encounters within a 30-day window would enable modeling of the metric most directly tied to hospital reimbursement under current CMS payment programs.
3. Incorporate geospatial analysis. Hospital zip code data, where available, enables analysis of social determinants of health at the census tract level — an increasingly important dimension of population health management and a growing area of hospital analytics practice.
tibble(
Component = c("R Version", "Primary Packages",
"Modeling Framework", "Report Format"),
Detail = c(
paste(R.version$major, R.version$minor, sep = "."),
"tidyverse 2.0, janitor, skimr, kableExtra",
"tidymodels",
"R Markdown — HTML with floating TOC and code folding"
)
) %>%
kable(
format = "html",
caption = "Table 10: Technical Environment",
align = c("l", "l")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE,
position = "left"
) %>%
column_spec(1, bold = TRUE) %>%
row_spec(0, background = "#2C3E50", color = "white")
| Component | Detail |
|---|---|
| R Version | 4.5.3 |
| Primary Packages | tidyverse 2.0, janitor, skimr, kableExtra |
| Modeling Framework | tidymodels |
| Report Format | R Markdown — HTML with floating TOC and code folding |
Prasad, R. (2024). Healthcare Dataset [Data set]. Kaggle. https://www.kaggle.com/datasets/prasad22/healthcare-dataset
Full source code, including the data cleaning function, exploratory analysis scripts, and this report, is available at: github.com/brianketchens/hospital-analysis
Analysis conducted in R 4.5.3 | Report generated April 16, 2026