#Note: This is a work in progress. Any feedback or suggestions are greatly appreciated.
#The flow of this kernel is different than typical kernels. Model training is still done first, followed by predicting test data, however objects are imported/created only when necessary and removed thereafter to minimize ram usage and allow for a larger training dataset.

#Load necessary libraries.
library(tidyverse)
library(lightgbm)

#Import training dataset.
train_full <- read.csv("../input/train_V2.csv", header = T, stringsAsFactors = F)

set.seed <- 42 #The answer to life, the Universe and everything. 
train <- train_full[sample(nrow(train_full), 3825000), ] #Sampling to test run-time. Choose a sample size, or nrow(train_full) for all training data. Max size currently 3.5M rows with only max and mean aggregations (incl. rank). Use 1M for exploring.
rm(train_full)

#TODO: EDA.

#Feature engineering (some feature engineering is omitted (commented out) to preserve memory):

#Players per match.
playersPerMatch <- train %>% group_by(matchId) %>% summarize(playersPerMatch = n())
train <- merge(train, playersPerMatch, by = 'matchId', all.x = T)
rm(playersPerMatch)

#Party stats (party size; TBD).
custom_group_stats <- train %>% group_by(matchId, groupId) %>% summarize(partySize = n())
train <- merge(train, custom_group_stats, by = c('matchId', 'groupId'), all.x = T)
rm(custom_group_stats)

#TODO: Add other engineered features here.
#TODO: Flag cheaters and zombies. Remove? How to treat outliers.

#Combination metrics (based on PUBG top kernels):
#Sum of heals and boosts.
#train <- mutate(train, healsAndBoosts = heals + boosts) #Could add weaponsAcquired to make this total items acquired (totalItemsAcquired).

#Total distance travelled.
#train <- mutate(train, totalDistance = walkDistance + rideDistance + swimDistance)

#Teamwork metric.
#train <- mutate(train, teamwork = assists + revives)

#Sum of kills and assists.
#train <- mutate(train, killsAndAssists = kills + assists)

#Combat metric.
#train <- mutate(train, combatPerf = damageDealt/100 + kills - teamKills + DBNOs + assists) #May be useful.

#Group mean and max values per match. Common approach used in PUBG top kernels. Removed ', playersPerMatch, partySize'
train_mean_max <- train %>% select(-c(winPlacePerc, playersPerMatch, partySize)) %>% group_by(matchId, groupId) %>% summarize_if(is.numeric, funs(mean, max)) #Only included max and mean aggregations under assumption weakest player's performance isn't important (i.e. leaders carry teams).

#Create match rankings for each group. Ditto.
train_mean_max_rank <- train_mean_max %>% group_by(matchId) %>% mutate(assists_mean_rank = rank(assists_mean, ties.method = 'min')/numGroups_max*100, #Mean
                                                                       boosts_mean_rank = rank(boosts_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       damageDealt_mean_rank = rank(damageDealt_mean, ties.method = 'min')/numGroups_max*100,
                                                                       DBNOs_mean_rank = rank(DBNOs_mean, ties.method = 'min')/numGroups_max*100,
                                                                       headshotKills_mean_rank = rank(headshotKills_mean, ties.method = 'min')/numGroups_max*100,
                                                                       heals_mean_rank = rank(heals_mean, ties.method = 'min')/numGroups_max*100,
                                                                       killPlace_mean_rank = rank(killPlace_mean, ties.method = 'min')/numGroups_max*100, #May not be necessary
                                                                       killPoints_mean_rank = rank(killPoints_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       kills_mean_rank = rank(kills_mean, ties.method = 'min')/numGroups_max*100,
                                                                       killStreaks_mean_rank = rank(killStreaks_mean, ties.method = 'min')/numGroups_max*100,
                                                                       longestKill_mean_rank = rank(longestKill_mean, ties.method = 'min')/numGroups_max*100,
                                                                       matchDuration_mean_rank = rank(matchDuration_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       maxPlace_mean_rank = rank(maxPlace_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       numGroups_mean_rank = rank(numGroups_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       rankPoints_mean_rank = rank(rankPoints_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       revives_mean_rank = rank(revives_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       rideDistance_mean_rank = rank(rideDistance_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       roadKills_mean_rank = rank(roadKills_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       swimDistance_mean_rank = rank(swimDistance_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       teamKills_mean_rank = rank(teamKills_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       vehicleDestroys_mean_rank = rank(vehicleDestroys_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       walkDistance_mean_rank = rank(walkDistance_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       weaponsAcquired_mean_rank = rank(weaponsAcquired_mean, ties.method = 'min')/numGroups_max*100, 
                                                                       winPoints_mean_rank = rank(winPoints_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       #healsAndBoosts_mean_rank = rank(healsAndBoosts_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                       #teamwork_mean_rank = rank(teamwork_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                       #killsAndAssists_mean_rank = rank(killsAndAssists_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                       #totalDistance_mean_rank = rank(totalDistance_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                       #combatPerf_mean_rank = rank(combatPerf_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                       assists_max_rank = rank(assists_max, ties.method = 'min')/numGroups_max*100, #Max
                                                                       boosts_max_rank = rank(boosts_max, ties.method = 'min')/numGroups_max*100,
                                                                       damageDealt_max_rank = rank(damageDealt_max, ties.method = 'min')/numGroups_max*100,
                                                                       DBNOs_max_rank = rank(DBNOs_max, ties.method = 'min')/numGroups_max*100,
                                                                       headshotKills_max_rank = rank(headshotKills_max, ties.method = 'min')/numGroups_max*100,
                                                                       heals_max_rank = rank(heals_max, ties.method = 'min')/numGroups_max*100,
                                                                       killPlace_max_rank = rank(killPlace_max, ties.method = 'min')/numGroups_max*100, #May not be necessary
                                                                       killPoints_max_rank = rank(killPoints_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       kills_max_rank = rank(kills_max, ties.method = 'min')/numGroups_max*100,
                                                                       killStreaks_max_rank = rank(killStreaks_max, ties.method = 'min')/numGroups_max*100,
                                                                       longestKill_max_rank = rank(longestKill_max, ties.method = 'min')/numGroups_max*100,
                                                                       matchDuration_max_rank = rank(matchDuration_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       maxPlace_max_rank = rank(maxPlace_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       numGroups_max_rank = rank(numGroups_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       rankPoints_max_rank = rank(rankPoints_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       revives_max_rank = rank(revives_max, ties.method = 'min')/numGroups_max*100, 
                                                                       rideDistance_max_rank = rank(rideDistance_max, ties.method = 'min')/numGroups_max*100, 
                                                                       roadKills_max_rank = rank(roadKills_max, ties.method = 'min')/numGroups_max*100, 
                                                                       swimDistance_max_rank = rank(swimDistance_max, ties.method = 'min')/numGroups_max*100, 
                                                                       teamkills_max_rank = rank(teamKills_max, ties.method = 'min')/numGroups_max*100, 
                                                                       vehicleDestroys_max_rank = rank(vehicleDestroys_max, ties.method = 'min')/numGroups_max*100, 
                                                                       walkDistance_max_rank = rank(walkDistance_max, ties.method = 'min')/numGroups_max*100, 
                                                                       weaponsAcquired_max_rank = rank(weaponsAcquired_max, ties.method = 'min')/numGroups_max*100, 
                                                                       winPoints_max_rank = rank(winPoints_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                       #healsAndBoosts_max_rank = rank(healsAndBoosts_max, ties.method = 'min')/numGroups_max*100, #Custom
                                                                       #teamwork_max_rank = rank(teamwork_max, ties.method = 'min')/numGroups_max*100, #Custom
                                                                       #killsAndAssists_max_rank = rank(killsAndAssists_max, ties.method = 'min')/numGroups_max*100, #Custom
                                                                       #totalDistance_max_rank = rank(totalDistance_max, ties.method = 'min')/numGroups_max*100 #Custom
                                                                       #combatPerf_max_rank = rank(combatPerf_max, ties.method = 'min')/numGroups_max*100 #Custom
                                                                       )
rm(train_mean_max)
gc()

#Merging mean variables with original training dataset.
train_prep <- merge(train, train_mean_max_rank, by = c('matchId', 'groupId'), all.x = T)
rm(train_mean_max_rank)
rm(train)

#One-hot encoding of matchType.
#TODO: Group all match types by solo, duo, squad (both custom and competitive). Could add flag for competitive or custom.
matchType_train <- model.matrix(~matchType-1, train_prep)

#Excluding Ids and matchType.
train_prep <- train_prep %>% select(-c('Id', 'groupId', 'matchId', 'matchType'))

#Merging in matchType dummy variables.
train_prep <- cbind(train_prep, matchType_train)
rm(matchType_train)

#Preparing data for model training.
output <- train_prep %>% na.omit() %>% select(winPlacePerc) 
train_prep <- train_prep %>% na.omit() %>% select(-winPlacePerc) %>% as.matrix()

#Training LightGBM model with ad-hoc selection of parameters.
#TODO: Incorporate grid search to find optimal parameters.
#TODO: Incorporate stacked ensembling.
#TODO: Incorporate model evaluation.
#TODO: Explore removing unimportant features based on variable importance.
#TODO: Explore PCA to reduce dimensionality and add more data.
    #train_prep.pca <- prcomp(train_prep[,apply(train_prep, 2, var, na.rm=TRUE) != 0], scale. = TRUE)
    #summary(train_prep.pca)

gc()
model <- lightgbm(data = train_prep,
                  label = output$winPlacePerc,
                  nrounds = 500,
                  max_depth = 6,
                  min_data_in_leaf = 5, #Or 3.
                  early_stopping_round = 3,
                  learning_rate = 0.1,
                  objective = 'regression',
                  metric='mae'
                  )
rm(train_prep)
rm(output)
gc()

#Repeat feature engineering for test dataset.
test <- read.csv("../input/test_V2.csv", header = T, stringsAsFactors = F)

#Players per match.
playersPerMatch <- test %>% group_by(matchId) %>% summarize(playersPerMatch = n())
test <- merge(test, playersPerMatch, by = 'matchId', all.x = T)
rm(playersPerMatch)

#Party stats (party size; TBD).
custom_group_stats <- test %>% group_by(matchId, groupId) %>% summarize(partySize = n())
test <- merge(test, custom_group_stats, by = c('matchId', 'groupId'), all.x = T)
rm(custom_group_stats)

#Sum of heals and boosts.
#test <- mutate(test, healsAndBoosts = heals + boosts)

#Total distance travelled.
#test <- mutate(test, totalDistance = walkDistance + rideDistance + swimDistance)

#Teamwork metric.
#test <- mutate(test, teamwork = assists + revives)

#Sum of kills and assists.
#test <- mutate(test, killsAndAssists = kills + assists)

#Combat metric.
#test <- mutate(test, combatPerf = damageDealt/100 + kills - teamKills + DBNOs + assists)

#Group mean, min, max values per match. Removed '%>% select(-c(playersPerMatch, partySize))'
test_mean_max <- test %>% select(-c(playersPerMatch, partySize)) %>% group_by(matchId, groupId) %>% summarize_if(is.numeric, funs(mean, max))

#Create match rankings for each group.
test_mean_max_rank <- test_mean_max %>% group_by(matchId) %>% mutate(assists_mean_rank = rank(assists_mean, ties.method = 'min')/numGroups_max*100, #Mean
                                                                     boosts_mean_rank = rank(boosts_mean, ties.method = 'min')/numGroups_max*100,
                                                                     damageDealt_mean_rank = rank(damageDealt_mean, ties.method = 'min')/numGroups_max*100,
                                                                     DBNOs_mean_rank = rank(DBNOs_mean, ties.method = 'min')/numGroups_max*100,
                                                                     headshotKills_mean_rank = rank(headshotKills_mean, ties.method = 'min')/numGroups_max*100,
                                                                     heals_mean_rank = rank(heals_mean, ties.method = 'min')/numGroups_max*100,
                                                                     killPlace_mean_rank = rank(killPlace_mean, ties.method = 'min')/numGroups_max*100, #May not be necessary
                                                                     killPoints_mean_rank = rank(killPoints_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     kills_mean_rank = rank(kills_mean, ties.method = 'min')/numGroups_max*100,
                                                                     killStreaks_mean_rank = rank(killStreaks_mean, ties.method = 'min')/numGroups_max*100,
                                                                     longestKill_mean_rank = rank(longestKill_mean, ties.method = 'min')/numGroups_max*100,
                                                                     matchDuration_mean_rank = rank(matchDuration_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     maxPlace_mean_rank = rank(maxPlace_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     numGroups_mean_rank = rank(numGroups_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     rankPoints_mean_rank = rank(rankPoints_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     revives_mean_rank = rank(revives_mean, ties.method = 'min')/numGroups_max*100, 
                                                                     rideDistance_mean_rank = rank(rideDistance_mean, ties.method = 'min')/numGroups_max*100, 
                                                                     roadKills_mean_rank = rank(roadKills_mean, ties.method = 'min')/numGroups_max*100, 
                                                                     swimDistance_mean_rank = rank(swimDistance_mean, ties.method = 'min')/numGroups_max*100, 
                                                                     teamKills_mean_rank = rank(teamKills_mean, ties.method = 'min')/numGroups_max*100, 
                                                                     vehicleDestroys_mean_rank = rank(vehicleDestroys_mean, ties.method = 'min')/numGroups_max*100, 
                                                                     walkDistance_mean_rank = rank(walkDistance_mean, ties.method = 'min')/numGroups_max*100, 
                                                                     weaponsAcquired_mean_rank = rank(weaponsAcquired_mean, ties.method = 'min')/numGroups_max*100, 
                                                                     winPoints_mean_rank = rank(winPoints_mean, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     #healsAndBoosts_mean_rank = rank(healsAndBoosts_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                     #teamwork_mean_rank = rank(teamwork_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                     #killsAndAssists_mean_rank = rank(killsAndAssists_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                     #totalDistance_mean_rank = rank(totalDistance_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                     #combatPerf_mean_rank = rank(combatPerf_mean, ties.method = 'min')/numGroups_max*100, #Custom
                                                                     assists_max_rank = rank(assists_max, ties.method = 'min')/numGroups_max*100, #Max
                                                                     boosts_max_rank = rank(boosts_max, ties.method = 'min')/numGroups_max*100,
                                                                     damageDealt_max_rank = rank(damageDealt_max, ties.method = 'min')/numGroups_max*100,
                                                                     DBNOs_max_rank = rank(DBNOs_max, ties.method = 'min')/numGroups_max*100,
                                                                     headshotKills_max_rank = rank(headshotKills_max, ties.method = 'min')/numGroups_max*100,
                                                                     heals_max_rank = rank(heals_max, ties.method = 'min')/numGroups_max*100,
                                                                     killPlace_max_rank = rank(killPlace_max, ties.method = 'min')/numGroups_max*100, #May not be necessary
                                                                     killPoints_max_rank = rank(killPoints_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     kills_max_rank = rank(kills_max, ties.method = 'min')/numGroups_max*100,
                                                                     killStreaks_max_rank = rank(killStreaks_max, ties.method = 'min')/numGroups_max*100,
                                                                     longestKill_max_rank = rank(longestKill_max, ties.method = 'min')/numGroups_max*100,
                                                                     matchDuration_max_rank = rank(matchDuration_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     maxPlace_max_rank = rank(maxPlace_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     numGroups_max_rank = rank(numGroups_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     rankPoints_max_rank = rank(rankPoints_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     revives_max_rank = rank(revives_max, ties.method = 'min')/numGroups_max*100, 
                                                                     rideDistance_max_rank = rank(rideDistance_max, ties.method = 'min')/numGroups_max*100, 
                                                                     roadKills_max_rank = rank(roadKills_max, ties.method = 'min')/numGroups_max*100, 
                                                                     swimDistance_max_rank = rank(swimDistance_max, ties.method = 'min')/numGroups_max*100, 
                                                                     teamkills_max_rank = rank(teamKills_max, ties.method = 'min')/numGroups_max*100, 
                                                                     vehicleDestroys_max_rank = rank(vehicleDestroys_max, ties.method = 'min')/numGroups_max*100, 
                                                                     walkDistance_max_rank = rank(walkDistance_max, ties.method = 'min')/numGroups_max*100, 
                                                                     weaponsAcquired_max_rank = rank(weaponsAcquired_max, ties.method = 'min')/numGroups_max*100, 
                                                                     winPoints_max_rank = rank(winPoints_max, ties.method = 'min')/numGroups_max*100, #Ditto
                                                                     #healsAndBoosts_max_rank = rank(healsAndBoosts_max, ties.method = 'min')/numGroups_max*100, #Custom
                                                                     #teamwork_max_rank = rank(teamwork_max, ties.method = 'min')/numGroups_max*100, #Custom
                                                                     #killsAndAssists_max_rank = rank(killsAndAssists_max, ties.method = 'min')/numGroups_max*100, #Custom
                                                                     #totalDistance_max_rank = rank(totalDistance_max, ties.method = 'min')/numGroups_max*100 #Custom
                                                                     #combatPerf_max_rank = rank(combatPerf_max, ties.method = 'min')/numGroups_max*100 #Custom
                                                                     )
rm(test_mean_max)

#Merging mean variables with original test dataset.
test_prep <- merge(test, test_mean_max_rank, by = c('matchId', 'groupId'), all.x = T)
rm(test_mean_max_rank)
rm(test)

#One-hot encoding of matchType.
matchType_test <- model.matrix(~matchType-1, test_prep)

#Excluding Ids and matchType.
test.Id <- test_prep$Id
test_prep <- test_prep %>% select(-c('Id', 'groupId', 'matchId', 'matchType'))

#Merging in matchType dummy variables.
test_prep <- cbind(test_prep, matchType_test)
rm(matchType_test)

#Generate predictions on test dataset.
test_prep$winPlacePerc.predict <- predict(model, as.matrix(test_prep))
submission <- data.frame(Id = test.Id, winPlacePerc = test_prep$winPlacePerc.predict, stringsAsFactors = F)
submission$winPlacePerc <- ifelse(submission$winPlacePerc < 0, 0, ifelse(submission$winPlacePerc > 1, 1, submission$winPlacePerc))
write.csv(submission, "submission_lgbm_nrow3.75M_nrounds500.csv", row.names = F)