Loading...

Saturday, August 9, 2014

Visualizing CDC's Morbidity and Mortality Weekly Report (MMWR) on Infrequently Reported Diseases


Hello Readers,

Here we will download, organize, and visualize disease data the Morbidity and Mortality Weekly Report (MMWR) published by the Centers for Disease Control and Prevention (CDC). Recent news from Texas and other states include a cyclosporiasis outbreak originating from alleged imported Mexican produce. In June of 2013, there were 631 people sickened by a cyclosporiasis outbreak, so this year's outbreak is not new.


Cyclospora cayetanensis Parasite
This post acknowledges and thanks Aaron Kite-Powell of the Armed Forces Health Surveillance Center with collaboration in writing the R code. Check them out if you're interested in disease surveillance!


Notifiable Diseases and Mortality Tables


Looking at the CDC link above for notifiable diseases in Table I, we see over 55 diseases- some with no reported cases for the week (anthrax) and some with more than a few (measles). The diseases are deemed infrequent from having less than 1000 cases reported in the past year from the National Notifiable Diseases Surveillance System (NNDSS). Clicking the export data button brings us to the data.cdc.gov page where we can see the cases reported for each week of this year. We will download this data in a CSV file and plot the cases by disease in R. Selecting the Export button, we have several choices of download formats, and we can manually download the CSV file, or we can copy the link and use R to retrieve the CSV file. The R code will demonstrate both methods later.


Figure 1. Infrequently Reported Diseases Data


Accessing the Data in R


Now that we have the CSV file downloaded, and the link address copied, we can attempt both methods to load the data into R.

Manual CSV Download
First, we load all the libraries we will use ("plyr", "ggplot2", "RCurl"), and load the file location. Then using "read.csv()" we read in the CSV file, making sure that the strings are not factors with "stringsAsFactors=F". Note that we have 20 columns and 1650 rows of disease data in various weeks.

CSV Code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
> ### CDC MMWR Scraping/Formatting
> ### https://data.cdc.gov/NNDSS/NNDSS-Table-I-infrequently-reported-notifiable-dis/wcwi-x3uk
> setwd("~/YOURDIRECTORY")
> # load libraries
> library(plyr)
> library(ggplot2)
> library(RCurl)
> 
> # CSV read-in method
> file <- "NNDSS_-_Table_I._infrequently_reported_notifiable_diseases.csv"
> nndss <- read.csv(file, stringsAsFactors=F)
> dim(nndss) # 1650 rows by 20 columns
[1] 1650   20
> names(nndss) # column names
 [1] "Disease"                                         
 [2] "MMWR.year"                                       
 [3] "MMWR.week"                                       
 [4] "Current.week"                                    
 [5] "Current.week..flag"                              
 [6] "Cum.2014"                                        
 [7] "Cum.2014..flag"                                  
 [8] "X5.year.weekly.averageâ.."                       
 [9] "X5.year.weekly.averageâ....flag"                 
[10] "Total.cases.reported..2013"                      
[11] "Total.cases.reported..2013..flag"                
[12] "Total.cases.reported.2012"                       
[13] "Total.cases.reported.2012..flag"                 
[14] "Total.cases.reported.2011"                       
[15] "Total.cases.reported.2011..flag"                 
[16] "Total.cases.reported.2010"                       
[17] "Total.cases.reported.2010..flag"                 
[18] "Total.cases.reported.2009"                       
[19] "Total.cases.reported.2009..flag"                 
[20] "States.reporting.cases.during.current.week..No.."
> 


R URL Download
Next we will use R and "download.file()" to access our target CSV file. After specifying the file name ("reportedDiseases.csv") and the method, ("method="curl""), we can read in the CSV with "read.csv()". Looking at the dimensions, we see 20 columns, but 1705 rows. This is because the URL refers to the most recent data from the CDC, whereas the CSV file we downloaded manually came before another week of data was added. Therefore, using the data retrieved from the URL method is more optimal since it has more recent data (1650 old rows + 55 new week= 1705). So the current week is 1705/55 = week 31.

URL Method Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
> # URL Method
> url <- "https://data.cdc.gov/api/views/wcwi-x3uk/rows.csv?accessType=DOWNLOAD"
> download.file(url, destfile="reportedDiseases.csv", method="curl")
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  125k    0  125k    0     0  74997      0 --:--:--  0:00:01 --:--:-- 75658
> nndss.1 <- read.csv("reportedDiseases.csv", strip.white=T, stringsAsFactors=F)
> dim(nndss.1)
[1] 1705   20
> names(nndss.1)
 [1] "Disease"                                         
 [2] "MMWR.year"                                       
 [3] "MMWR.week"                                       
 [4] "Current.week"                                    
 [5] "Current.week..flag"                              
 [6] "Cum.2014"                                        
 [7] "Cum.2014..flag"                                  
 [8] "X5.year.weekly.averageâ.."                       
 [9] "X5.year.weekly.averageâ....flag"                 
[10] "Total.cases.reported..2013"                      
[11] "Total.cases.reported..2013..flag"                
[12] "Total.cases.reported.2012"                       
[13] "Total.cases.reported.2012..flag"                 
[14] "Total.cases.reported.2011"                       
[15] "Total.cases.reported.2011..flag"                 
[16] "Total.cases.reported.2010"                       
[17] "Total.cases.reported.2010..flag"                 
[18] "Total.cases.reported.2009"                       
[19] "Total.cases.reported.2009..flag"                 
[20] "States.reporting.cases.during.current.week..No.."
> nndss.1$Disease <- factor(nndss.1$Disease)
> 

Also remember to transform the "$Disease" column into a factor, because we have 55 diseases, not 1705.


Infrequently Reported Diseases


Now that we have our data, the data are all separated and organized by "$MMWR.week", but we want each diseased grouped together, progressing by each week so we can evaluate the cases. This is where we use "ddply()" to wrestle our data and transform it to our needs. First specify the data set, "nndss.1", then the variables by which we will organize ".(Disease, MMWR.week)", and our value that we pull is the "Current.week" counts. So for week 1, it will pull the week 1 cases, and push it to the output, and so on for each week. Again, we transform "$MMWR.week" into a factor type variable, since it takes on discrete values from 1 to 31.

Data Re-Arrangement Code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
> # combine measure by disease type
> # for each week
> d <- ddply(nndss.1, .(Disease, MMWR.week), summarize,
+            count = Current.week)
> d$MMWR.week <- factor(d$MMWR.week)
>
> summary(d)
                                                               Disease    
 Anthrax                                                           :  31  
 Arboviral diseases, California serogroup virus disease§,¶       :  31  
 Arboviral diseases, Eastern equine encephalitis virus disease§,¶:  31  
 Arboviral diseases, Powassan virus disease§,¶                   :  31  
 Arboviral diseases, St. Louis encephalitis virus disease§,¶     :  31  
 Arboviral diseases, Western equine encephalitis virus disease§,¶:  31  
 (Other)                                                           :1519  
   MMWR.week        count       
 1      :  55   Min.   : 1.000  
 2      :  55   1st Qu.: 1.000  
 3      :  55   Median : 2.000  
 4      :  55   Mean   : 3.787  
 5      :  55   3rd Qu.: 4.000  
 6      :  55   Max.   :87.000  
 (Other):1375   NA's   :1255   
> 
> # rename levels ####
> levels(d$Disease)[3] <- "Arbo,EEE"
> levels(d$Disease)[2] <- "Arbo,CA serogroup"
> levels(d$Disease)[4] <- "Arbo,Powassan"
> levels(d$Disease)[5] <- "Arbo,St Louis"
> levels(d$Disease)[6] <- "Arbo,WEE"
> levels(d$Disease)[9] <- "Botulism other"
> levels(d$Disease)[14] <- "Cyclosporiasis"
> levels(d$Disease)[16] <- "H flu <5 non-b"
> levels(d$Disease)[17] <- "H flu <5 b"
> levels(d$Disease)[18] <- "H flu <5 unknown"
> levels(d$Disease)[19] <- "Hansen disease"
> levels(d$Disease)[20] <- "Hantavirus PS"
> levels(d$Disease)[21] <- "HUS,postdiarrheal"
> levels(d$Disease)[22] <- "HBV,perinatal"
> levels(d$Disease)[23] <- "Influenza ped mort"
> levels(d$Disease)[26] <- "Measles"
> levels(d$Disease)[27] <- "Mening a,c,y,w-135"
> levels(d$Disease)[28] <- "Mening other"
> levels(d$Disease)[29] <- "Mening serogroup b"
> levels(d$Disease)[30] <- "Mening unknown"
> levels(d$Disease)[31] <- "Novel influenza A"
> levels(d$Disease)[33] <- "Polio nonparalytic"
> levels(d$Disease)[35] <- "Psittacosis"
> levels(d$Disease)[38] <- "Q fever, total"
> levels(d$Disease)[40] <- "Rubella"
> levels(d$Disease)[42] <- "SARS-CoV"
> levels(d$Disease)[43] <- "Smallpox"
> levels(d$Disease)[44] <- "Strep toxic shock synd"
> levels(d$Disease)[45] <- "Syphilis congenital <1yr"
> levels(d$Disease)[47] <- "Toxic shock synd staph"
> levels(d$Disease)[51] <- "Vanco Interm Staph A"
> levels(d$Disease)[52] <- "Vanco Resist Staph A"
> levels(d$Disease)[53] <- "Vibrio non-cholera"
> levels(d$Disease)[54] <- "Viral hemorr fever"
> # levels finish
> 

Afterwards, we see the names of the diseases, and realize that they are a mess, format-wise. So we rename each level in the "$Disease" factor variable as they need renaming. Thanks Aaron for making the disease names more readable!



Plotting Disease Cases


The next step in R creates the visualizations necessary to understand and see the temporal aspect of the reported disease cases and possible outbreaks. Utilizing the layers in "ggplot()", we place the "MMWR.week" on the x-axis and the "count" on the y-axis. Each plot will be a histogram through "geom_histogram()", and we plot all of the 55 diseases in the same plot with "facet_wrap()". 55 plots is divided perfectly into 11 rows of 5 columns. The "theme()" designates no grid lines, and "scale_x_discrete()" provides the break points and labels on the week x-axis.

Plot Code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
> # plotting diseases
> # use d dataset
> plot <- ggplot(d, aes(x=MMWR.week, y=count)) +
+   geom_histogram(stat="identity") +
+   ggtitle("MMWR Infrequently Reported Diseases") +
+   facet_wrap(~Disease, nrow=11, ncol=5, scales="free") +
+   theme(panel.grid.minor=element_blank(), panel.grid.major=element_blank()) +
+   scale_x_discrete(breaks=c("1", "10", "20", "30"), labels=c("1","10","20","30"))
> 
> # write to file
> png(file="MMWR Infreq Diseases.png", bg="white",
+ width=850, height=850)
> # plot plots in png file
> plot
> # turns off png device
> dev.off()
RStudioGD 
        2 
> 

With "png()" we can write the plot directly to a .png file, while specifying the dimensions. Call the plot name "plot" while the png device is active and it will write with that device to the designated destination. "dev.off()" will turn the device off, returning it to the original RStudio plot device. Other formats can be used, such as "jpg()" or "bmp()". "png()" was chosen because it preserves the image quality.



Figure 2. Plot of Infrequently Reported Diseases

We can see the cyclosporiasis plot, in the third row, second from the right. Note the steep right in cases in recent weeks- owed to outbreak in Texas and other states mentioned at the beginning of the post. 


Visualizing the Cyclosporiasis Outbreak

By isolating the cyclosporiasis plot, we gain better perspective in observing the outbreak in reported cases. Those infected begin to develop symptoms of diarrhea, fever, and nausea, from two days to two weeks after infection, lasting from a few weeks to a two months. The vector is through contaminated feces, and thought to be present on some imported produce.

Figure 3. Cyclosporiasis Reported Cases
As we can see, the reported cases rise dramatically in the month of July, and hit a high of 58 cases in early August. As of the NNDSS report, there have been 221 cases of cyclosporiasis, however, not all of them are due to this specific outbreak- but the majority of them are.
Plotting Cyclosporiasis Code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
> levels(d$Disease)
 [1] "Anthrax"                  "Arbo,CA serogroup"        "Arbo,EEE"                
 [4] "Arbo,Powassan"            "Arbo,St Louis"            "Arbo,WEE"                
 [7] "Botulism, foodborne"      "Botulism, infant"         "Botulism other"          
[10] "Botulism, total"          "Brucellosis"              "Chancroid"               
[13] "Cholera"                  "Cyclosporiasis"           "Diphtheria"              
[16] "H flu <5 non-b"           "H flu <5 b"               "H flu <5 unknown"        
[19] "Hansen disease"           "Hantavirus PS"            "HUS,postdiarrheal"       
[22] "HBV,perinatal"            "Influenza ped mort"       "Leptospirosis"           
[25] "Listeriosis"              "Measles"                  "Mening a,c,y,w-135"      
[28] "Mening other"             "Mening serogroup b"       "Mening unknown"          
[31] "Novel influenza A"        "Plague"                   "Polio nonparalytic"      
[34] "Poliomyelitis, paralytic" "Psittacosis"              "Q fever, acute"          
[37] "Q fever, chronic"         "Q fever, total"           "Rabies, human"           
[40] "Rubella"                  "Rubella†††"         "SARS-CoV"                
[43] "Smallpox"                 "Strep toxic shock synd"   "Syphilis congenital <1yr"
[46] "Tetanus"                  "Toxic shock synd staph"   "Trichinellosis"          
[49] "Tularemia"                "Typhoid fever"            "Vanco Interm Staph A"    
[52] "Vanco Resist Staph A"     "Vibrio non-cholera"       "Viral hemorr fever"      
[55] "Yellow fever"            
> 
> # plot cyclosporiasis
> cyclo <- ggplot(d[d$Disease==levels(d$Disease)[14],],
+                 aes(x=MMWR.week, y=count)) +
+   geom_histogram(stat="identity") +
+   ggtitle("Cyclosporiasis Reported Cases in 2014") +
+   scale_x_discrete(breaks=c(1,6,10,14,19,23,27,31), # first weeks of each month
+                    labels=month.name[1:8]) # month names
> 
> # write to file
> png(filename="cyclosporiasis_plot.png", bg="white",
+     width=550, height=550)
> cyclo
> dev.off()
RStudioGD 
        2 
> 


And there we have it folks! We gathered reported disease data from the CDC's MMWR and loaded into R. Then we cleaned, and transformed the data, and visualized the cases report by disease. Additionally, we focused on cyclosporiasis, and we were able to see the rise in reported cases coinciding with the imported produce outbreak in Texas and other states.


By looking at the health data, we can track near real-time and even predict future cases. Refer to the MMWR if you would like weekly reports on the latest outbreaks!


Thanks for reading,

Wayne
@beyondvalence
LinkedIn

Friday, August 1, 2014

Deploying Database Changes from Visual Studio to SQL Server 2012


Hello Readers,

Today we will demonstrate how to deploy a database using SQL Server Data Tools (SSDT), available in Visual Studio Shell. SSDT is a power tool where we can crease databases and database objects- and also perform database schema compares. Here I will show you how to connect Visual Studio to SQL Server and deploy (publish) a database.

SQL Server



When you login to SQL Server Management Studio, in the Object Explorer to the left, you can navigate through the databases. Here we have the famous AdventureWorks database already loaded. We aim to modify a column in the Person.Address table.


Figure 1. Object Explorer with AdventureWorks

Right-clicking the AddressLine1 column and select Properties to see the column properties. We will increase the length from 65 to 70, as an example.


Figure 2. AddressLine1 Properties

Data Tools in Visual Studio



Now that we have located the target column, we open Visual Studio to use the SQL Server Data Tools. First we need to create a new Project and a new connection to the AdventureWorks database in SQL Server.


Figure 3. Visual Studio New Project

Make sure you have selected the SQL Server Template to your left, and the SQL Server Database Project should appear in the middle dialogue box. Name your database and click OK. 


To connect to the SQL Server and database, right-click the project name and select Import   -> Database. Select New Connection and type in the server information and choose the database name from the drop down list towards the bottom of the Connection Properties window. You can click the Test Connection button at the bottom left, to see if you have the correct server typed in, and valid database selected.

Figure 4. Adding a New Connection

Also, change the Folder Structure in the Import Settings to Object Type.

Figure 5. Finishing Importing AdventureWorks

After you click Start, Visual Studio will begin to import AdventureWorks. After it is finished, in the Solution Explorer to the right, navigate to the Tables folder and select the first table, Address.sql. This table contains the column, AddressLine1, whose length we want to modify.

Figure 6. AddressLine1 Column in Address.sql Table

Simply click the Data Type nvarchar(65) and change it to nvarchar(70), and save all.

Deploying AdventureWorks


Since we have finished with our changes, we now need to deploy those changes into SQL Server. Right-click the AdventureWorks Project Folder in the Solution Explorer and select Publish. Re-enter the connection details and hit Publish. 

Figure 7. Publishing AdventureWorks

The middle highlighted box is the Publishing Options, and the bottom box shows the Output Dialogue Box. Note that it says the "Build: 1 succeeded".

Figure 8. Data Tools Output- Publishing

Turning back to SQL Server, in the Object Explorer, click the Refresh button with the circular arrows. Then navigate to the Person.Address Table and right-click the AddressLine1 column. Observe the change in length! It is now 70 characters long.

Figure 9. Checking AddressLine1 Length 70

Fantastic! We were able to connect to the AdventureWorks database in SQL Server, and change the length of a column in a table using SQL Server Data Tools in Visual Studio. Then we deployed those changes back into SQL Server and observed the changes. This is just a taste of what SQL Server is capable of when combined with SQL Server Data Tools- Visual Studio.

Stay tuned for more SQL posts!


Thanks for reading,

Wayne
@beyondvalence
LinkedIn

Sunday, July 27, 2014

Predicting Fraudulent Transactions in R: Part 2. Handling Missing Data


Hello Readers,

"We have missing data." How many times have you heard that sentence and cringed inside? If you worked with user-generated data before, you most likely have happened upon the "NULL" or "NA" or "99999" values, which possibly diminished your usable data. It gets me every time, too. After a while, you get used to those encounters, because as data scientists, we have the tools and methods to overcome incomplete data, and still carry out the analysis. In this post, I discuss missing data values in our Case Study for Fraudulent Transactions.


One Possible 'Option' When Encountering Missing Data

This is Part 2 of the Case Study, and here we handle the missing data. Let's begin where we left off in Part 1.


(This is a series from Luis Torgo's  Data Mining with R book.)


Dual Unknown Values


When we took the "summary()" of our SALES data, we found our "Quant" and "Val" variables had NA values. For each transaction, the quantity of the product sold and the value of the sale are important predictors (only, in this case) of being a fraudulent transaction or deemed "ok". So when 888 transactions are missing both variables, we find it difficult to impute any value due to the number of unknowns. If we had a "Quant" or "Val" variable available, we might have been able to use the unit-price of the product (value/quantity) to calculate the missing variable.

Salespeople and Product NAs Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
> attach(sales)
> 
> totS <- table(ID)
> totP <- table(Prod)
> 
> # transactions per values and product for NAs
> nas <- sales[which(is.na(Quant) & is.na(Val)), c("ID", "Prod")]
> 
> # obtain salesppl NAs
> propS <- 100* table(nas$ID)/totS
> propS[order(propS, decreasing=T)[1:10]]

    v1237     v4254     v4038     v5248     v3666     v4433     v4170 
13.793103  9.523810  8.333333  8.333333  6.666667  6.250000  5.555556 
    v4926     v4664     v4642 
 5.555556  5.494505  4.761905 
> #
> # the transactions represent a small proportion of transactions
> # looking at the products:
> propP <- 100* table(nas$Prod)/totP
> propP[order(propP, decreasing=T)[1:10]]

   p2689    p2675    p4061    p2780    p4351    p2686    p2707    p2690 
39.28571 35.41667 25.00000 22.72727 18.18182 16.66667 14.28571 14.08451 
   p2691    p2670 
12.90323 12.76596 
> 
> # several products more than 20% of their transactions would be
> # removed: that is too much, p2689 would have 40% removed
> detach(sales)
> sales <- sales[-which(is.na(sales$Quant) & is.na(sales$Val)),]

Examining the salespeople first, we create a table for the number of transactions by each salesperson. Then we subset the SALES data, pulling the transactions with NAs in "Quant" and "Val", with the information on the "ID" and "Prod" (salesperson ID and product ID). Dividing this table of NA present transactions by the full table of all the transactions, measures the proportion of NAs in each salesperson's transactions. Taking the top 10 salespeople who NAs in their transactions with "order()", we discover that salesperson "v1237" had the highest percentage of transactions with dual NAs, at 13.8%. That percentage is not too high, and all the other salespeople have lower percentages. We can breathe easy, since not one single salesperson had the majority of his or her transaction reports filled with NAs. So if we remove transactions with dual NAs, not one single salesperson will be overly affected.


Investigating the products next, we do the same procedure and create tables for all of the products, for products with NAs, and the percentage of products with NAs with division. Looking at the 10 products with missing values, product "p2689" has nearly 40 of its transactions incomplete. Unlike the NAs grouped by salespeople, if we delete the dual NA transactions, product "p2689", and "p2675" will have over 35% of their transactions removed, and 4 products would have at least 20% removed! Clearly some products have more missing values than others.



Alternatives


There are generally 3 alternatives available to us as options when we encounter missing data. The first is to remove those rows. Second, fill in the missing values using a calculated method, or third, use tools to handle those values. If you work with an industry specific program or have specific handling instructions, then option three would be the best decision. But here, we can only choose from the first two options.

We could impute unit-prices for the missing products, but with product "p2689", we would only have 60% of the data to fill in the 40%. If there are too many transactions removed, we would then join those transactions with ones from similar products for outlier detection tests. The most best option would be to remove those transactions. So "detach()" the SALES data and remove those transactions with both missing quantity and value elements, via sub-setting the SALES data.


Single Unknown Values


Now that we have resolved the unknown values in both quantity and value variables, we refocus onto transactions with one unknown in either variable. There were 888 transactions with both missing, and for the single missing value, there are 13,248 transactions, nearly 15 times as many. Let us first start with the quantity variable, "Quant".

We stratify the quantities by the product code, thus searching for the proportion of NAs present in the quantities of a certain product. We flag a product if a high number of its transactions have missing quantities.

Missing Quantity for Products Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
> # move to unknown value either quantity or value ####
> # how many transactions?
> #
> length(which(is.na(sales$Quant) | is.na(sales$Val)))
[1] 13248
> #
> # check missing quantity first
> nnasQp <- tapply(sales$Quant, list(sales$Prod),
+                  function(x) sum(is.na(x)))
> propNAsQp <- nnasQp/table(sales$Prod)
> propNAsQp[order(propNAsQp, decreasing=T)[1:10]]
    p2442     p2443     p1653     p4101     p4243      p903     p3678 
1.0000000 1.0000000 0.9090909 0.8571429 0.6842105 0.6666667 0.6666667 
    p3955     p4464     p1261 
0.6428571 0.6363636 0.6333333 
> # there are two products with all transactions of unknown
> # quantity, p2442, p2443
> sales[sales$Prod %in% c("p2442", "p2443"),]
          ID  Prod Quant    Val  Insp
21259  v2921 p2442    NA   5715  unkn
21260  v2922 p2442    NA  21250  unkn
21261  v2923 p2442    NA 102210  unkn
21262  v2922 p2442    NA 213155  unkn
21263  v2924 p2442    NA   4870  unkn
21264  v2925 p2443    NA  17155    ok
58422  v2924 p2442    NA   4870  unkn
58423  v2922 p2442    NA  21250  unkn
58424  v4356 p2442    NA  53815  unkn
58425  v2922 p2442    NA 213155  unkn
58426  v2925 p2443    NA   3610 fraud
58427  v4355 p2443    NA   5260  unkn
58428  v4355 p2443    NA   1280 fraud
102076 v2921 p2442    NA 137625  unkn
102077 v2920 p2442    NA  21310  unkn
102078 v4839 p2442    NA   5190  unkn
102079 v4356 p2442    NA  11320  unkn
102080 v2922 p2442    NA  34180  unkn
102081 v2925 p2443    NA   3610  unkn
102082 v4355 p2443    NA   5260  unkn
102083 v4355 p2443    NA   1280  unkn
102084 v2925 p2443    NA   3075  unkn
153543 v5077 p2442    NA   7720  unkn
153544 v2924 p2442    NA   9620  unkn
153545 v2920 p2442    NA  34365  unkn
153546 v2925 p2443    NA   3455  unkn
195784 v5077 p2442    NA   7720  unkn
195785 v4356 p2442    NA  43705  unkn
195786 v2939 p2443    NA   5465  unkn
195787 v2925 p2443    NA  14990  unkn
252153 v2924 p2442    NA   4870  unkn
252154 v2921 p2442    NA 137625  unkn
252155 v5077 p2442    NA   7720  unkn
252156 v2922 p2442    NA  66820  unkn
252157 v5077 p2442    NA  12035  unkn
252158 v2920 p2442    NA  79320  unkn
252159 v2925 p2443    NA   3610  unkn
325280 v2924 p2442    NA   4870  unkn
325281 v2921 p2442    NA 137625  unkn
325282 v5077 p2442    NA   7720  unkn
325283 v2922 p2442    NA  66820  unkn
325284 v5077 p2442    NA  12350  unkn
325285 v5077 p2442    NA  12035  unkn
325286 v2920 p2442    NA  43180  unkn
325289 v2925 p2443    NA   3610  unkn
325290 v4355 p2443    NA   5260  unkn
325291 v4355 p2443    NA   1280  unkn
325292 v2925 p2443    NA   2890  unkn
390840 v5077 p2442    NA  11515  unkn
390841 v4356 p2442    NA   4695  unkn
390842 v2923 p2442    NA  15580  unkn
390843 v2920 p2442    NA  27320  unkn
390844 v6044 p2442    NA  21215  unkn
390845 v4356 p2442    NA  53190  unkn
> # delete them because both have OK and Fraud
> sales <- sales[!sales$Prod %in% c("p2442", "p2443"),]
> # update levels
> #
> nlevels(sales$Prod) # 4548
[1] 4548
> sales$Prod <- factor(sales$Prod)
> nlevels(sales$Prod) # 4846
[1] 4546
> # now has correct number, after we removed the 2 products

Looking at the proportions, product "p2442" and "p2443" lack the quantity metric all of their transactions! Also, "p1653" has 90% missing, and "p4101" has 86% missing. These are quite stark numbers. For those products that lack all of their quantity values, looking at their fraud inspection status, we see some labeled "ok" and some labeled "fraud". Because it is not statistically sound to use those evaluations given the lack of data, we will remove those two problem products. Additionally, remember to update the levels in the "Prod" variable, since we removed "p2442" and "p2443".


Missing Quantity by Salespeople Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
> # check salesppl with transactions of unknown quantity
> nnasQs <- tapply(sales$Quant, list(sales$ID),
+                  function(x) sum(is.na(x)))
> propNAsQs <- nnasQs/table(sales$ID)
> propNAsQs[order(propNAsQs, decreasing=T)[1:10]]
    v2925     v5537     v5836     v6058     v6065     v4368     v2923 
1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 0.8888889 0.8750000 
    v2970     v4910     v4542 
0.8571429 0.8333333 0.8095238 
> # quite a few salesppl did not fill out the quantity info
> # but we can use numbers from other sales ppl under unitprice
> # so no need to delete

Here we again take the table of number of missing quantities by salespeople and find the proportion by dividing with the table of salespeople. Then taking the top 10 results, we see that salesperson "v2925", "v5537", "v5836", "v6058", and "v6065", have all of their transactions missing the quantity variable. However, as long as we have transactions of the same product by other people, we can use the unit-price to calculate the quantity from the present value element.


Missing Value by Product Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
> # check unknown values
> # by product
> nnasVp <- tapply(sales$Val, list(sales$Prod),
+                  function(x) sum(is.na(x)))
> propNAsVp <- nnasVp/table(sales$Prod)
> propNAsVp[order(propNAsVp, decreasing=T)[1:10]]
     p1110      p1022      p4491      p1462        p80      p4307 
0.25000000 0.17647059 0.10000000 0.07500000 0.06250000 0.05882353 
     p4471      p2821      p1017      p4287 
0.05882353 0.05389222 0.05263158 0.05263158 
> # reasonable results, no need to delete

Using the similar pattern of finding the proportion of missing "Val" values for each product, we pleasantly discover no high percentages. Since no product has high NAs proportions, we do not need to delete them.


Missing Value by Salespeople Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
> # unknown values by salespeople
> #
> nnasVs <- tapply(sales$Val, list(sales$ID),
+ function(x) sum(is.na(x)))
> propNAsVs <- nnasVs/table(sales$ID)
> propNAsVs[order(propNAsVs, decreasing=T)[1:10]]
     v5647        v74      v5946      v5290      v4472      v4022 
0.37500000 0.22222222 0.20000000 0.15384615 0.12500000 0.09756098 
      v975      v2814      v2892      v3739 
0.09574468 0.09090909 0.09090909 0.08333333 
> # reasonable results again

Now examining the missing values in "Val" by salespeople, we observe no salesperson with overly high proportions of NAs involving "Val". We have acceptable results, with no need to delete any more transactions.



Imputing Missing Data


Now that we have removed the transactions with insufficient information, we can fill in the remaining values using our fill-in strategy of relying on the unit-price. Also, we need to skip those transactions previously audited and labeled as "fraud". We utilize the median unit price of transactions as the typical price for their respective products. (I saved the SALES data because we took out invalid transactions, you might want to do the same.) To find the median unit-price without consulting those fraudulent transactions, we specify those transactions with the "Insp" (inspection) variable as not equal to fraud, '!= "fraud" '.

Filling In 'Quant' and 'Val' Code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
> load("sales.rdata") ####
> # imputing ####
> #
> # calculate median price, while ignoring frauds
> #
> tPrice <- tapply(sales[sales$Insp != "fraud", "Uprice"],
+                  list(sales[sales$Insp != "fraud", "Prod"]),
+                  median, na.rm=T)
> # now we can use median unit-price to calculate Quant and Val
> #
> # we already eliminated transactions with both missing
> #
> # begin with Quant, find missing Quants
> noQuant <- which(is.na(sales$Quant))
> sum(is.na(sales$Quant))
[1] 12900
> # Imputing Quant
> # round Quant up
> sales[noQuant,'Quant'] <- ceiling(sales[noQuant, 'Val']/
+                           tPrice[sales[noQuant,"Prod"]])
> #
> # next Val, find missing Vals
> noVal <- which(is.na(sales$Val))
> sum(is.na(sales$Val))
[1] 294
> # impute Vals
> sales[noVal, 'Val'] <- sales[noVal,'Quant']*
+                        tPrice[sales[noVal, 'Prod']]

We fill in the missing 'Quant' values by creating a missing index, and discover 12,900 transactions ready to be imputed. Then we round all the 'Quant' values we will impute up, since quantity is an integer. Remember, value/qantity = unit-price, so we divide value by the unit-price to obtain the quantity values.


Next we tackle the missing 'Val' values, create the missing 'Val' index, and we find 294 we can fill in. Again, using the simple formula, we multiply the quantity by the unit-price to obtain the value.



Clean Up


Now that we have no more unknown values in 'Quant' or 'Val', we have a complete, or clean dataset. But we are not finished! Since we have all the quantity and values, we can recalculate the unit-price with all the values present. And make sure to save this SALES dataset, in case you have not yet already. Naming the file 'salesClean.rdata' allows us to differentiate the regular SALES set with the clean SALES set.

Recalculating Unit-Price Code:
1
2
3
4
5
6
> # all unit-prices present, so recalculate
> #
> sales$Uprice <- sales$Val/sales$Quant
> # now we have a dataset free of unknowns
> # save our progress!
> save(sales, file='salesClean.rdata')



Summary



Whew! Handling missing data is a necessary task, and when to remove rows entries requires data wrangling experience and reliance/knowledge of the other data present. OK folks, so here we located and quantified the unknown values in the variables, identified which rows (transactions) we needed and which to remove, and removed those while imputing values from the same products using the median unit-price. Sometimes in user-generated data, a variable might not be composed of other variables, so imputation is impossible. Other times, we can infer values based on other variables. Either way, working with 'clean' data is not a privilege, since we usually have to work to refine it!