This document includes the steps used for ingesting EMS data from Idaho exports, and for determining what fields can be used for matching with ED data, as well as the QA/QC steps needed.
First let’s set up the envronment. In your RStudio session enter the following code. If you have problems you may need to install these libraries.
library(tidyverse)
library(zipcode)
data(zipcode)
library(geosphere)
library(udunits2)
library(lubridate)
First we have to see if we need to convert the file format from windows. This has to be done in a terminal tab, not R. Let’s check one of the files.
file -i BioSense2_1-17-19_to_5-15-19-2.csv
If the message says UTF-16, we need to take the exported files and convert the character encoding to UTF-8, again - in a terminal tab, not in R.
iconv -f UTF-16LE -t UTF-8 BioSense1_1-29-18_to_5-15-19-2.csv -o BioSense1_1-29-18_to_5-15-19-2.UTF-8.txt
iconv -f UTF-16LE -t UTF-8 BioSense2_1-17-19_to_5-15-19-2.csv -o BioSense2_1-17-19_to_5-15-19-2.UTF-8.txt
Now, let’s read the converted data into R. In this case, there were multiple exports by date.
df.EMS.1<-read_tsv("BioSense1_1-29-18_to_5-15-19-2.UTF-8.txt", col_names = TRUE)
df.EMS.2<-read_tsv("BioSense2_1-17-19_to_5-15-19-2.UTF-8.txt", col_names = TRUE)
df.EMS<-bind_rows(df.EMS.1,df.EMS.2)
The ‘bind_rows’ function will merge the two separate export files together. If you have more than two files to load, then just use the above code as an example, copy the read.tsv line as many times as you need, and add the other file to the bind_rows function.
We need to rename the columns to get rid of nonword characters and spaces - this will make it easier to work with this dataset.
df.EMS<-df.EMS %>%
rename_all(funs(gsub("([[:punct:]]|\\s)+", "_", names(df.EMS))))
df.EMS<-df.EMS %>%
rename_all(funs(gsub("_$", "", names(df.EMS))))
Here we’re using the lubridate package to normalize birth date format.
df.EMS$Patient_Date_of_Birth <- mdy_hm(df.EMS$Patient_Date_of_Birth)
# create a birthday field
birthdays<-paste(month(as.POSIXlt(df.EMS$Patient_Date_of_Birth)),"-",day(as.POSIXlt(df.EMS$Patient_Date_of_Birth)),sep="")
birthdays[which(birthdays == "NA-NA")]<-NA
df.EMS$Birthday<-birthdays
# create age as if age unit is years - will handle hours, days, and months below
df.EMS$Age_In_Years<-as.integer(df.EMS$Patient_Age_E6_14)
# age unit is hours
df.EMS$Age_In_Years[which(df.EMS$Patient_Age_Units_E6_15 %in% "Hours")] <- as.integer(df.EMS$Age_In_Years[which(df.EMS$Patient_Age_Units_E6_15 %in% "Hours")]/8760)
# age unit is days
df.EMS$Age_In_Years[which(df.EMS$Patient_Age_Units_E6_15 %in% "Days")] <- as.integer(df.EMS$Age_In_Years[which(df.EMS$Patient_Age_Units_E6_15 %in% "Days")]/365)
# age unit is months
df.EMS$Age_In_Years[which(df.EMS$Patient_Age_Units_E6_15 %in% "Months")] <- as.integer(df.EMS$Age_In_Years[which(df.EMS$Patient_Age_Units_E6_15 %in% "Months")]/12)
For zip codes per patient, we need to create a temporary ID per patient so we can make sure each patient has their zip code fields populated.
# create temporary unique ID
df.EMS<-df.EMS %>% mutate(Unique_Incident_Number = gsub("([[:punct:]]|\\s)+", "_", paste(Service_Name, Incident_Call_Number, Incident_Number, Age_In_Years, Patient_Gender_E6_11, Birthday, sep = '_')))
We need to fix missing zipcodes from patients where zipcode was not consistently recorded, with some missing (-20)
df.zip.id <- distinct(df.EMS[,c("Unique_Incident_Number","Patient_Postal_Code_E6_8")])
# below are the ones that need fixing
ids.with.zipcodes.to.fix<-df.zip.id %>%
group_by(Unique_Incident_Number) %>%
filter(n()>1, Patient_Postal_Code_E6_8==-20) %>%
select(Unique_Incident_Number) %>%
pull()
# create lookup table for zipcode by patient
fix.zip.lookup<-df.zip.id %>%
filter(Unique_Incident_Number %in% ids.with.zipcodes.to.fix, Patient_Postal_Code_E6_8 != -20) %>%
rename(new_postal_code=Patient_Postal_Code_E6_8)
# and finally, fix the missing zipcodes
df.EMS<-df.EMS %>%
left_join(fix.zip.lookup, by = "Unique_Incident_Number") %>%
mutate(Patient_Postal_Code_E6_8 = ifelse(!is.na(new_postal_code), new_postal_code, Patient_Postal_Code_E6_8)) %>%
select(-new_postal_code)
Now we can create a better unique id per patient - now including zipcode.
df.EMS<-df.EMS %>%
mutate(Unique_Incident_Number = gsub("([[:punct:]]|\\s)+", "_", paste(Service_Name, Incident_Call_Number, Incident_Number, Age_In_Years, Patient_Gender_E6_11, Birthday, Patient_Postal_Code_E6_8, sep = '_')))
We want our EMS data dates to match the ED data dates per hospital. So we’re going to read in the crosswalk file containing the specified dates, as well as the cross-reference for PERCS destination code to BioSense facility ID.
crosswalk<-read_tsv("data_pull_dates_and_crosswalk.txt",col_names=T)
# add dates/times for EMS data based on hosp online
df.EMS<-left_join(
df.EMS,
select(crosswalk, PERCS_Destination_Code, C_Biosense_Facility_ID, Date_to_pull_from, Timezone),
by=c("Destination_Code_E20_2"="PERCS_Destination_Code")
)
# convert pull date
df.EMS$Date_to_pull_from<-mdy(df.EMS$Date_to_pull_from)
df.EMS$Dates_Arrived_at_Destination_E5_10<-mdy_hm(df.EMS$Dates_Arrived_at_Destination_E5_10)
# filter EMS data based on dates
df.EMS<-df.EMS %>%
filter(Dates_Arrived_at_Destination_E5_10 >= Date_to_pull_from)
Now we need to collapse fields like chief complaint from multiple rows into one, separated by “|”
# start doing the collapsing and de-selecting: create lookup tables for all the collapsable fields first
# Chief_Complaint_E9_5
df.cc.ems<-df.EMS %>%
select(Unique_Incident_Number, Chief_Complaint_E9_5) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Chief_Complaint_E9_5 = paste0(Chief_Complaint_E9_5, collapse = "|")) %>%
distinct()
# Primary_Symptom_E9_13
df.ps.ems<-df.EMS %>%
select(Unique_Incident_Number, Primary_Symptom_E9_13) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Primary_Symptom_E9_13 = paste0(Primary_Symptom_E9_13, collapse = "|")) %>%
distinct()
# Drug_Use_Indicators_Code_E12_19
df.du.ems<-df.EMS %>%
select(Unique_Incident_Number, Drug_Use_Indicators_Code_E12_19) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Drug_Use_Indicators_Code_E12_19 = paste0(Drug_Use_Indicators_Code_E12_19, collapse = "|")) %>%
distinct()
# Dispatch_Complaint_Report_E3_1
df.cr.ems<-df.EMS %>%
select(Unique_Incident_Number, Dispatch_Complaint_Report_E3_1) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Dispatch_Complaint_Report_E3_1 = paste0(Dispatch_Complaint_Report_E3_1, collapse = "|")) %>%
distinct()
# Complaint_Anatomic_Location_E9_11
df.cal.ems<-df.EMS %>%
select(Unique_Incident_Number, Complaint_Anatomic_Location_E9_11) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Complaint_Anatomic_Location_E9_11 = paste0(Complaint_Anatomic_Location_E9_11, collapse = "|")) %>%
distinct()
# Secondary_Impression_E9_16
df.si.ems<-df.EMS %>%
select(Unique_Incident_Number, Secondary_Impression_E9_16) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Secondary_Impression_E9_16 = paste0(Secondary_Impression_E9_16, collapse = "|")) %>%
distinct()
# Complaint_Organ_System_E9_12
df.cos.ems<-df.EMS %>%
select(Unique_Incident_Number, Complaint_Organ_System_E9_12) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Complaint_Organ_System_E9_12 = paste0(Complaint_Organ_System_E9_12, collapse = "|")) %>%
distinct()
# Dates_Arrived_at_Destination_E5_10
df.da.ems<-df.EMS %>%
select(Unique_Incident_Number, Dates_Arrived_at_Destination_E5_10) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Dates_Arrived_at_Destination_E5_10 = paste0(Dates_Arrived_at_Destination_E5_10, collapse = "|")) %>%
distinct()
# Injury_Present_E9_4
df.ip.ems<-df.EMS %>%
select(Unique_Incident_Number, Injury_Present_E9_4) %>%
distinct() %>%
group_by(Unique_Incident_Number) %>%
mutate(Injury_Present_E9_4 = paste0(Injury_Present_E9_4, collapse = "|")) %>%
distinct()
# now create final data set, adding in collapsed fields
df.EMS<-df.EMS %>%
select(Unique_Incident_Number, Patient_Date_of_Birth, Age_In_Years, Patient_Gender_E6_11, Birthday, C_Biosense_Facility_ID, Patient_Postal_Code_E6_8, Timezone, Agency_Time_Zone, Service_Name, Incident_Call_Number, Incident_Number) %>%
distinct() %>%
left_join(df.da.ems , by="Unique_Incident_Number") %>%
left_join(df.cos.ems , by="Unique_Incident_Number") %>%
left_join(df.si.ems , by="Unique_Incident_Number") %>%
left_join(df.cal.ems , by="Unique_Incident_Number") %>%
left_join(df.cr.ems , by="Unique_Incident_Number") %>%
left_join(df.cc.ems , by="Unique_Incident_Number") %>%
left_join(df.ps.ems , by="Unique_Incident_Number") %>%
left_join(df.du.ems , by="Unique_Incident_Number") %>%
left_join(df.ip.ems , by="Unique_Incident_Number")
nrow(df.EMS)
## [1] 70162
hist(df.EMS$Age_In_Years,breaks=100)
The distribution is what we expect to see in EMS data.
birth_dates<-distinct(df.EMS[,c("Unique_Incident_Number","Patient_Date_of_Birth")])
birth_dates %>%
filter(is.na(Patient_Date_of_Birth))
## # A tibble: 254 x 2
## Unique_Incident_Number Patient_Date_of_Bir…
## <chr> <dttm>
## 1 Ada_County_City_EMS_System_E6_BFDTRAIN1022103_68_M… NA
## 2 Ada_County_City_EMS_System_M13_18_0015138_77_Male_… NA
## 3 Ada_County_City_EMS_System_M13_18_0019540_55_Male_… NA
## 4 Ada_County_City_EMS_System_M13_18_0026186_45_Male_… NA
## 5 Ada_County_City_EMS_System_M13_18_0041780_17_Femal… NA
## 6 Ada_County_City_EMS_System_M13_19_0007664_30_Male_… NA
## 7 Ada_County_City_EMS_System_M15_18_0004371_28_Male_… NA
## 8 Ada_County_City_EMS_System_M15_18_0005870_28_Male_… NA
## 9 Ada_County_City_EMS_System_M15_18_0011177_25_Male_… NA
## 10 Ada_County_City_EMS_System_M15_18_0012380_55_Male_… NA
## # … with 244 more rows
254/70162*100
## [1] 0.3620193
patient_birthdays<-distinct(df.EMS[,c("Unique_Incident_Number","Birthday")])
plot(table(patient_birthdays$Birthday))
There is a spike at 1/1 - we need to clean by setting these to NA.
df.EMS <- df.EMS %>% mutate(Patient_Date_of_Birth = replace(Patient_Date_of_Birth, which(Birthday == "1-1"), NA))
filter(df.EMS, is.na(Dates_Arrived_at_Destination_E5_10))
## # A tibble: 0 x 21
## # … with 21 variables: Unique_Incident_Number <chr>,
## # Patient_Date_of_Birth <dttm>, Age_In_Years <int>,
## # Patient_Gender_E6_11 <chr>, Birthday <chr>,
## # C_Biosense_Facility_ID <dbl>, Patient_Postal_Code_E6_8 <dbl>,
## # Timezone <chr>, Agency_Time_Zone <chr>, Service_Name <chr>,
## # Incident_Call_Number <chr>, Incident_Number <chr>,
## # Dates_Arrived_at_Destination_E5_10 <chr>,
## # Complaint_Organ_System_E9_12 <chr>, Secondary_Impression_E9_16 <chr>,
## # Complaint_Anatomic_Location_E9_11 <chr>,
## # Dispatch_Complaint_Report_E3_1 <chr>, Chief_Complaint_E9_5 <chr>,
## # Primary_Symptom_E9_13 <chr>, Drug_Use_Indicators_Code_E12_19 <chr>,
## # Injury_Present_E9_4 <chr>
100% compliant, and has the arrival date/time at the hospital.
patient_sexes<-distinct(df.EMS[,c("Unique_Incident_Number","Patient_Gender_E6_11")])
table(patient_sexes$Patient_Gender_E6_11)
##
## Female Male Not Applicable Not Known Not Recorded
## 37284 32821 1 11 17
Gender is almost 100% complete.
chief_complaints<-distinct(df.EMS[,c("Unique_Incident_Number","Chief_Complaint_E9_5")])
filter(chief_complaints, is.na(Chief_Complaint_E9_5))
## # A tibble: 0 x 2
## # … with 2 variables: Unique_Incident_Number <chr>,
## # Chief_Complaint_E9_5 <chr>
This is 100% compliant. #### Chief Complaint Anatomic Location
chief_complaints_loc<-distinct(df.EMS[,c("Unique_Incident_Number","Complaint_Anatomic_Location_E9_11")])
filter(chief_complaints_loc, is.na(Complaint_Anatomic_Location_E9_11))
## # A tibble: 0 x 2
## # … with 2 variables: Unique_Incident_Number <chr>,
## # Complaint_Anatomic_Location_E9_11 <chr>
100% compliant.
chief_complaints_orgs<-distinct(df.EMS[,c("Unique_Incident_Number","Complaint_Organ_System_E9_12")])
filter(chief_complaints_orgs, is.na(Complaint_Organ_System_E9_12))
## # A tibble: 0 x 2
## # … with 2 variables: Unique_Incident_Number <chr>,
## # Complaint_Organ_System_E9_12 <chr>
100% compliant.
drug_use<-distinct(df.EMS[,c("Unique_Incident_Number","Drug_Use_Indicators_Code_E12_19")])
filter(drug_use, is.na(Drug_Use_Indicators_Code_E12_19))
## # A tibble: 0 x 2
## # … with 2 variables: Unique_Incident_Number <chr>,
## # Drug_Use_Indicators_Code_E12_19 <chr>
100% compliant.
dest_trans<-distinct(df.EMS[,c("Unique_Incident_Number","C_Biosense_Facility_ID")])
filter(dest_trans, is.na(C_Biosense_Facility_ID))
## # A tibble: 0 x 2
## # … with 2 variables: Unique_Incident_Number <chr>,
## # C_Biosense_Facility_ID <dbl>
100% compliant.
df.EMS %>% filter(grepl("\\|", Dates_Arrived_at_Destination_E5_10)) %>% select(Dates_Arrived_at_Destination_E5_10) %>% print(n=Inf)
## # A tibble: 27 x 1
## Dates_Arrived_at_Destination_E5_10
## <chr>
## 1 2018-02-24 08:55:00|2018-02-24 08:50:00
## 2 2018-02-24 09:44:00|2018-02-24 09:43:00
## 3 2018-03-29 17:26:00|2018-03-29 17:30:00
## 4 2018-04-03 10:38:00|2018-04-03 10:37:00
## 5 2018-04-08 13:08:00|2018-04-08 13:15:00
## 6 2018-04-17 13:52:00|2018-04-17 13:53:00
## 7 2018-05-11 20:36:00|2018-05-11 20:42:00
## 8 2018-06-05 23:26:00|2018-06-05 22:58:00
## 9 2018-06-14 20:49:00|2018-06-14 20:48:00
## 10 2018-07-07 18:00:00|2018-07-07 17:58:00
## 11 2018-07-09 22:32:00|2018-07-09 22:31:00
## 12 2018-07-17 12:49:00|2018-07-17 12:45:00
## 13 2018-07-27 18:53:00|2018-07-27 18:15:00
## 14 2018-07-30 19:26:00|2018-07-30 19:27:00
## 15 2018-09-07 19:45:00|2018-09-07 19:38:00
## 16 2018-09-17 08:15:00|2018-09-17 07:50:00
## 17 2018-09-25 13:38:00|2018-09-25 13:07:00
## 18 2018-09-29 10:22:00|2018-09-29 10:23:00
## 19 2018-11-04 04:52:00|2018-11-04 04:42:00
## 20 2019-01-03 03:11:00|2019-01-03 02:38:00
## 21 2019-01-19 15:39:00|2019-01-19 15:45:00
## 22 2019-01-21 11:13:00|2019-01-21 11:12:00
## 23 2019-01-21 22:26:00|2019-01-21 22:29:00
## 24 2019-02-06 18:51:00|2019-02-06 19:19:00
## 25 2019-03-04 10:59:00|2019-03-04 10:20:00
## 26 2019-03-05 14:20:00|2019-03-05 13:51:00
## 27 2019-03-10 02:55:00|2019-03-10 04:10:00
We have to deal with fixing these few arrival dates that differ in the same EMS run - they’re all pretty close, so we’ll just take the average per EMS run.
time.rows_to_fix<-df.EMS %>% filter(grepl("\\|", Dates_Arrived_at_Destination_E5_10)) %>% select(Unique_Incident_Number) %>% pull()
for (i in 1:length(time.rows_to_fix)) {
df.EMS[df.EMS$Unique_Incident_Number %in% time.rows_to_fix[i],]$Dates_Arrived_at_Destination_E5_10<-as.POSIXct(mean(as.numeric(ymd_hms(strsplit(df.EMS[df.EMS$Unique_Incident_Number %in% time.rows_to_fix[i],]$Dates_Arrived_at_Destination_E5_10,"\\|")[[1]]))), origin='1970-01-01')
}
df.EMS$Dates_Arrived_at_Destination_E5_10<-ymd_hms(df.EMS$Dates_Arrived_at_Destination_E5_10)
Now let’s check the arrival date distribution of all the EMS runs.
arrival_dates<-hist(df.EMS$Dates_Arrived_at_Destination_E5_10, "months", freq = TRUE)
This is what we expect to see given the dates in the crosswalk pull file.
We need to normalize gender to 1 letter code so it matches the ED data.
df.EMS$Patient_Gender_E6_11<-substr(df.EMS$Patient_Gender_E6_11,1,1)
We need to make zipcode a character field, to be compatible with ED data.
df.EMS$Patient_Postal_Code_E6_8 <- as.character(df.EMS$Patient_Postal_Code_E6_8)
We found that if a patient in an EMS run is missing the zipcode, it is impossible to match this EMS run using probabilistic matching. So we need to drop these runs.
df.EMS <- df.EMS %>%
filter((!is.na(Patient_Postal_Code_E6_8)) & Patient_Postal_Code_E6_8 != -20)
save(df.EMS, crosswalk, file="~/Idaho_EMS/EMS_cleaned_and_crosswalk.RData")