#I created this kernel as a resource for those looking to parse the nested tables in the `hits` column.
#The approach I took was to extract the nested tables from the `hits` column and convert them into dataframes with keys linking the contents back to train.csv.
#I also parse all remaining columns so the training dataset is fully ready for EDA, feature engineering and model training.

#I'm a coder in training so please excuse any obvious inefficiencies. In fact, I would greatly appreciate any tips, suggestions, or code improvements- especially re. vectorization.
#One thing I would greatly appreciate are suggestions regarding alternative functions/approaches to reduce CPU time. I use for loops which have significant time CPU for even a 20k sample of the train.csv.

#Importing necessary libaries
library(jsonlite)
library(plyr)
library(stringr)

#Due to the size of the training dataset, I'll only bring in enough rows to demonstrate the code.
train <- read.csv("../input/train_v2.csv", header = T, stringsAsFactors = F, colClasses = c(fullVisitorId = "character"), nrow = 20000)

#Taking a peek at the data.
head(train)

#Lots of text in JSON format and some that can be connvered to JSON format (customDimensions).
str(train)

#Parsing JSON the quicker way. This was taken from https://www.kaggle.com/mrlong/r-flatten-json-columns-to-make-single-data-frame. 
#This creates a large character object which can be parsed by fromJSON.
#paste("[",paste(train$`anyJSONcolumn`, collapse=","),"]")

#Prepare customDimensions table. This table will be merged with training later.
train$customDimensions <- gsub("\'", "\"", train$customDimensions) #Using gsub function to replace single quotes with double quotes- necessary for JSON parsing with fromJSON.
train$customDimensions <- gsub("\\[\\]", "[{}]", train$customDimensions) #Replacing empty lists with {}, required for JSON parsing.
train$customDimensions <- gsub("\\[|\\]", "", train$customDimensions) #Removing [] from each object, also required for fromJSON to work.
customDimensions <- lapply(paste("[",train$customDimensions,"]"),function(x) fromJSON(x, flatten=T)) #Glorious flattening using fromJSON and converting to dataframe.
customDimensions <- do.call("rbind.fill", customDimensions) #Include system.time wrapper to keep an eye on efficiencies. Also apending dataframes created from flatten.
head(customDimensions) #Check dataframe for issues.
dim(customDimensions) #Checking to ensure size is consistent with training dataset.
train$customDimensions <- NULL


#Prepare to parse the gargantuan hits column.
hitNumberlist <- str_extract_all(train$hits, "(?<='hitNumber': ')([0-9]*)") #I created a separate nested list containing the hits numbers for each visit. There's definitely a better way to do this.


#The `hits` column contains seven nested tables, but only two are not empty (based on the first 20k rows). The first is the product table.
train$hits <- gsub("(\'product\': \\[\\])", "\'product\': \\[\\{\\}\\]", perl = T, train$hits) #Fill empty dict with {}.

for (i in 1:length(train$hits)) { #This code places what I think are four attributes from the train dataset into the new product table which form a unique key for each row in the train dataset. This way, any analysis/aggregation on the nested tables can be linked back to the source row.
  train$hits[i] <- gsub("\'productSKU\'", paste0("\'visitStartTime\': \'",train$visitStartTime[i], "\', \'productSKU\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitStartTime\'", paste0("\'visitNumber\': \'",train$visitNumber[i], "\', \'visitStartTime\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitNumber\'", paste0("\'visitId\': \'",train$visitId[i], "\', \'visitNumber\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitId\'", paste0("\'fullVisitorId\': \'",train$fullVisitorId[i], "\', \'visitId\'"), train$hits[i])
}

product <- str_extract_all(train$hits, "(\\{\'fullVisitorId\': )(.*?(\\}\\]))") #Extracting the `product` table from `hits`, `productSKU` is a unique identifier for each item in the dataframe.

for (i in 1:length(product)) { #Each element in train$hits can contain multiple "hits" (blobs of data denoted by `hitNumber`). This is also a unique identifier and we want to make sure it stays with each dataframe we create.
  for(j in 1:length(product[[i]])) {
    product[[i]][j] <- gsub("\'productSKU\'", paste0("\'hitNumber4SKU\': \'",hitNumberlist[[i]][j],"\', \'productSKU\'"), product[[i]][j])
  }
}

#Extracting each row from the `product` table.
product <- rapply(product, function(x) gsub("(\'v2ProductCategory\': \'\\$\\{)(.*?\\})", "\'v2ProductCategory\': \'NA", x), how = "list") #Replacing dubious values in `v2ProductCategory` which conflict with extraction code.
productsku <- str_extract_all(product,"(\\{\'fullVisitorId\')(.*?\\})") #Extracting each productSKU row into a nested list element.
rm(product) #No longer needed.

#A series of gsub operations to prep data for parsing. Credit goes to Alexandros Papageorgiou, the author of this notebook https://www.kaggle.com/alpapag/approach-for-working-with-the-hits-column-in-r. Some of this may be irrelevant as I've coded this in stages to address issues I ecountered with each approach.
productsku <- rapply(productsku, function(x) gsub("\\", "", fixed=T, x), how = "list") #Removing slashes.
productsku <- rapply(productsku, function(x) gsub("\"", "\'", x), how = "list") #Double quotes to single quotes before converting all single quotes to double qoutes.
productsku <- rapply(productsku, function(x) gsub("\'(?![a-z])|(?<=\\{|\\s)\'", "\"", perl = T, x), how = "list") #All single quotes except apostrophes to double quotes.
productsku <- rapply(productsku, function(x) gsub("True", "\"True\"", x), how = "list") #True to logical.
#productsku <- gsub("\" : \"", "\":\"", productsku) #Remove extra spaces. DIdn't end up using this.
productsku <- rapply(productsku, function(x) gsub("(?<!: )\"(?= |é)", "", perl = T, x), how = "list") #Remove unnecessary double quotes.
productsku <- rapply(productsku, function(x) gsub("\\[\\]", "[{}]", x), how = "list") #Replace NULL Python dictionary spaces with empty JSON lists.
productsku <- rapply(productsku, function(x) gsub("\\[|\\]", "", x), how = "list") #Remove beginning and ending square brackets.

#Finalizing the dataframe.
productsku <- lapply(productsku, data.frame, stringsAsFactors = FALSE) #Converts each list element into a dataframe.
productsku <- rbind.fill(productsku) #Appending those dataframes.
colnames(productsku) <- c("hits")
productsku <- na.omit(productsku)
productsku <- lapply(paste("[",productsku$hits,"]"),function(x) fromJSON(x, flatten=T)) #fromJSON doing its thing.
productsku <- do.call("rbind.fill", productsku) #Appending into all flattened tables into one dataframe.
#This is basically the process repeated for each other table within the `hits` column, so I'll refrain from redundant comments. That being said, I found six other lists/tables nested within the `hits` attribute. All but one was empty in 20k rows.

#Removing `product` dictionary from hits.
train$hits <- gsub("(\'product\')(.*?)(.*?(\\}\\])\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\'product\': \\[\\{\\}\\]\\, )", "", perl = T, train$hits)

#creating promotion df.
train$hits <- gsub("(\'promotion\': \\[\\])", "\'promotion\': \\[\\{\\}\\]", perl = T, train$hits) #fill empty dict with {}

for (i in 1:length(train$hits)) {
  train$hits[i] <- gsub("\'promoId\'", paste0("\'visitStartTime\': \'",train$visitStartTime[i], "\', \'promoId\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitStartTime\'", paste0("\'visitNumber\': \'",train$visitNumber[i], "\', \'visitStartTime\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitNumber\'", paste0("\'visitId\': \'",train$visitId[i], "\', \'visitNumber\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitId\'", paste0("\'fullVisitorId\': \'",train$fullVisitorId[i], "\', \'visitId\'"), train$hits[i])
}

promotion <- str_extract_all(train$hits, "(\\{\'fullVisitorId\': )(.*?(\\}\\]))") #Extracting the `product` table from `hits`, `promoId` is a unique identifier for each item in the dataframe.

for (i in 1:length(promotion)) {
  for(j in 1:length(promotion[[i]])) {
    promotion[[i]][j] <- gsub("\'promoId\'", paste0("\'hitNumber4promo\': \'",hitNumberlist[[i]][j],"\', \'promoId\'"), promotion[[i]][j])
  }
}

promoId <- str_extract_all(promotion,"(\\{\'fullVisitorId\')(.*?\\})") #No fields with values which interfered with extraction encountered.
rm(promotion)

promoId <- rapply(promoId, function(x) gsub("\\", "", fixed=T, x), how = "list")
promoId <- rapply(promoId, function(x) gsub("\"", "\'", x), how = "list")
promoId <- rapply(promoId, function(x) gsub("\'(?![a-z])|(?<=\\{|\\s)\'", "\"", perl = T, x), how = "list")
promoId <- rapply(promoId, function(x) gsub("True", "\"True\"", x), how = "list")
#promoId <- gsub("\" : \"", "\":\"", promoId)
promoId <- rapply(promoId, function(x) gsub("(?<!: )\"(?= |é)", "", perl = T, x), how = "list")
promoId <- rapply(promoId, function(x) gsub("\\[\\]", "[{}]", x), how = "list")
promoId <- rapply(promoId, function(x) gsub("\\[|\\]", "", x), how = "list")

promoId <- lapply(promoId, data.frame, stringsAsFactors = FALSE)
promoId <- rbind.fill(promoId)
colnames(promoId) <- c("hits")
promoId <- na.omit(promoId)
promoId <- lapply(paste("[",promoId$hits,"]"),function(x) fromJSON(x, flatten=T))
promoId <- do.call("rbind.fill", promoId)

#Removing promotions table from hits.
train$hits <- gsub("(\'promotion\')(.*?\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\'promotion\': \\[\\{\\}\\]\\, )", "", perl = T, train$hits)

#```creating experiment table
train$hits <- gsub("(\'experiment\': \\[\\])", "\'experiment\': \\[\\{\\}\\]", perl = T, train$hits) #Fill empty dict with {}
train$hits <- gsub("(\'experiment\': \\[\\{\\}\\])", "", perl = T, train$hits) #Remove empty dict.

experiment <- str_extract_all(train$hits, "(\'experiment\': )(.*?(\\}\\]))")

#Checking to see if this list is empty.
sum(sapply(experiment, function(x) length(x)==0))

#experiment is empty so we remove it.
train$hits <- gsub("(\'experiment\')(.*?\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\'experiment\': \\[\\{\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\\, \\, )", "\\, ", perl = T, train$hits) #Since we removed all `experiment` lists, we're left with the before and after commas. We have to fix this.
rm(experiment)

#```customVariables table

train$hits <- gsub("(\'customVariables\': \\[\\])", "\'customVariables\': \\[\\{\\}\\]", perl = T, train$hits)
train$hits <- gsub("(\'customVariables\': \\[\\{\\}\\])", "", perl = T, train$hits)

customVariables <- str_extract_all(train$hits, "(\'customVariables\': )(.*?(\\}\\]))")

sum(sapply(customVariables, function(x) length(x)==0))

train$hits <- gsub("(\'customVariables\')(.*?\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\'customVariables\': \\[\\{\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\\, \\, )", "\\, ", perl = T, train$hits)
rm(customVariables)

#```customDimensions table

train$hits <- gsub("(\'customDimensions\': \\[\\])", "\'customDimensions\': \\[\\{\\}\\]", perl = T, train$hits)
train$hits <- gsub("(\'customDimensions\': \\[\\{\\}\\])", "", perl = T, train$hits)

customDimensions.hits <- str_extract_all(train$hits, "(\'customDimensions\': )(.*?(\\}\\]))")

sum(sapply(customDimensions.hits, function(x) length(x)==0))

train$hits <- gsub("(\'customDimensions\')(.*?\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\'customDimensions\': \\[\\{\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\\, \\, )", "\\, ", perl = T, train$hits)
rm(customDimensions.hits)

#```customMetrics table

train$hits <- gsub("(\'customMetrics\': \\[\\])", "\'customMetrics\': \\[\\{\\}\\]", perl = T, train$hits)
train$hits <- gsub("(\'customMetrics\': \\[\\{\\}\\])", "", perl = T, train$hits)

customMetrics <- str_extract_all(train$hits, "(\'customMetrics\': )(.*?(\\}\\]))")

sum(sapply(customMetrics, function(x) length(x)==0))

train$hits <- gsub("(\'customMetrics\')(.*?\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\'customMetrics\': \\[\\{\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\\, \\, )", "\\, ", perl = T, train$hits)
rm(customMetrics)

#```publisher_infos table

train$hits <- gsub("(\'publisher_infos\': \\[\\])", "\'publisher_infos\': \\[\\{\\}\\]", perl = T, train$hits)
train$hits <- gsub("(\\, \'publisher_infos\': \\[\\{\\}\\])", "", perl = T, train$hits)

publisher_infos <- str_extract_all(train$hits, "(\\, \'publisher_infos\': )(.*?(\\}\\]))")

sum(sapply(publisher_infos, function(x) length(x)==0))

train$hits <- gsub("(\'publisher_infos\')(.*?\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\'publisher_infos\': \\[\\{\\}\\]\\, )", "", perl = T, train$hits)
train$hits <- gsub("(\\, \\, )", "\\, ", perl = T, train$hits)
rm(publisher_infos)


#Clean remaining hits table.
for (i in 1:length(train$hits)) {
  train$hits[i] <- gsub("\'hitNumber\'", paste0("\'visitStartTime\': \'",train$visitStartTime[i], "\', \'hitNumber\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitStartTime\'", paste0("\'visitNumber\': \'",train$visitNumber[i], "\', \'visitStartTime\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitNumber\'", paste0("\'visitId\': \'",train$visitId[i], "\', \'visitNumber\'"), train$hits[i])
  train$hits[i] <- gsub("\'visitId\'", paste0("\'fullVisitorId\': \'",train$fullVisitorId[i], "\', \'visitId\'"), train$hits[i])
}

hitNumber <- str_extract_all(train$hits, "(\\{\'fullVisitorId\': )(.*?(\\}\\]))")
hitNumber <- rapply(hitNumber, function(x) gsub("\\", "", fixed=T, x), how = "list")
hitNumber <- rapply(hitNumber, function(x) gsub("\"", "\'", x), how = "list")
hitNumber <- rapply(hitNumber, function(x) gsub("\'(?![a-z])|(?<=\\{|\\s)\'", "\"", perl = T, x), how = "list")
hitNumber <- rapply(hitNumber, function(x) gsub("True", "\"True\"", x), how = "list")
#hits <- gsub("\" : \"", "\":\"", hitNumber) #Remove extra spaces.
hitNumber <- rapply(hitNumber, function(x) gsub("(?<!: )\"(?= |é)", "", perl = T, x), how = "list")
hitNumber <- rapply(hitNumber, function(x) gsub("\\[\\]", "[{}]", x), how = "list")
hitNumber <- rapply(hitNumber, function(x) gsub("\\[|\\]", "", x), how = "list")

hitNumber <- lapply(hitNumber, data.frame, stringsAsFactors = FALSE)
hitNumber <- rbind.fill(hitNumber)
colnames(hitNumber) <- c("hits")
hitNumber <- na.omit(hitNumber)
hitNumber <- lapply(paste("[",hitNumber$hits,"]"),function(x) fromJSON(x, flatten=T))
hitNumber <- do.call("rbind.fill", hitNumber)
train$hits <- NULL
rm(hitNumberlist)


#Prep device
device <- lapply(paste("[",train$device,"]"),function(x) fromJSON(x, flatten=T))
device <- do.call("rbind.fill", device)
head(device)
dim(device)
train$device <- NULL

#Prep geoNetwork
geoNetwork <- lapply(paste("[",train$geoNetwork,"]"),function(x) fromJSON(x, flatten=T))
geoNetwork <- do.call("rbind.fill", geoNetwork)
head(geoNetwork)
dim(geoNetwork)
train$geoNetwork <- NULL

#Prep totals
totals <- lapply(paste("[",train$totals,"]"),function(x) fromJSON(x, flatten=T))
totals <- do.call("rbind.fill", totals)
head(totals)       
dim(totals)
train$totals <- NULL

#Prep trafficSource
trafficSource <- lapply(paste("[",train$trafficSource,"]"),function(x) fromJSON(x,flatten=T))
trafficSource <- do.call("rbind.fill", trafficSource)
head(trafficSource)
dim(trafficSource)
train$trafficSource <- NULL


#Binding flattened dataframes to train.
train <- cbind(train, customDimensions, device, geoNetwork, totals, trafficSource)
rm(customDimensions)
rm(device)
rm(geoNetwork)
rm(totals)
rm(trafficSource)


#At this point next steps would include casting variables to the appropriate class; omitting empty or really sparse columns; and performing EDA on the train, productSKU, and promoId tables in preparation for model training.
#To get started...
#is.na(colSums(train))
#sapply(train, function(x) sum(is.na(x)))
#train$channelGrouping <- as.factor(train$channelGrouping)
#train$date <- as.factor(train$date)
#train$visitId<-as.character(train$visitId)
#train$sessionId<-NULL
#train$socialEngagementType <- as.factor(train$socialEngagementType)
#train$socialEngagementType<-NULL
#train$visitStartTime <- as.POSIXct(train$visitStartTime, origin="1970-01-01")
#train$browser <- as.factor(train$browser)
