### Epidemic simulation accounting for possible imprecision of reported movement dates ###

##################################### 1. Data import #####################################
setwd("C:/Users/Younjung Kim/Desktop/Mayotte")

# import necessary packages
library(plyr)
library(truncnorm)

# import official (data.1) and truck (data.2) datasets
data.1 = read.csv(file = "Supplementary Information 1.csv", header = TRUE)
data.2 = read.csv(file = "Supplementary Information 2.csv", header = TRUE)

# convert strings to dates
data.1$Date = as.Date(data.1$Date, format = ("%m/%d/%Y"))
data.2$Date = as.Date(data.2$Date, format = ("%m/%d/%Y"))

# remove movements that occurred within communes
data.1 = data.1[data.1$Exit.commune != data.1$Entry.commune,]
data.2 = data.2[data.2$Exit.commune != data.2$Entry.commune,]    

# change column order
data.1 = data.1[,c("Exit.commune", "Entry.commune", "Date")]
data.2 = data.2[,c("Exit.commune", "Entry.commune", "Date")]

###### 2. Simulation #####

pINF = 0.1            # pINF: probability of an animal from an infected commune spreading the infection to the commune in which it is moved
Timesteps = 365*8     # Timestep: the number of days on which livestock movements will be simulated 
MinDayInf = 1         
MaxDayInf = Timesteps
nS = 20000            # nS: number of simulations 
Tinf = Inf            # Tinf: length of infectious period
nC = 17               # nC: number of communes

No.Inf = vector(mode = "list", length = nS)
No.Sus = vector(mode = "list", length = nS)
No.Day = vector(mode = "list", length = nS)
Inf.Co = vector(mode = "list", length = nS)

for(i in 1:nS) {
  InfStatus = rep(0,nC)   # InfStatus: infection status of a commune (all communes are susceptible at the start of a simulation)
  InfDays   = rep(0,nC)   # InfDays: the number of days that a commune is infected
  
  FirstInfDay = sample(MinDayInf:MaxDayInf,1) # randomly select the day of first infection
  FirstInfCom = sample(1:nC,1)                # randomly select a commune  
  InfStatus[FirstInfCom] = 1                  # seed infection in the commune
  
  No.Inf[[i]] = matrix(nrow = 1, ncol = 1);  No.Inf[[i]][] = 0   # the number of infected communes in each time step
  No.Sus[[i]] = matrix(nrow = 1, ncol = 1);  No.Sus[[i]][] = nC  # the number of susceptible communes in each time step
  No.Day[[i]] = matrix(nrow = 1, ncol = 1);  No.Day[[i]][] = 0   # the number of days elapsed from the day of first infection
  Inf.Co[[i]] = matrix(nrow = 1, ncol = nC); Inf.Co[[i]][] = 0   # infected communes in each time step

  # for each simulation, we genearte an edgelist which accounts for the possible imprecision of reported movement dates.
  data.1_rn = data.1   # a replicate of the official dataset
  data.2_rn = data.2   # a replicate of the truck dataset

  # assume that X% (randomly selected from the Uniform distribution between 10-30%) of the movements occurred on a different date 
  data.1_rn$rn = rbinom(nrow(data.1_rn), 1, runif(1, min = 10, max = 30)*0.01)  
  data.2_rn$rn = rbinom(nrow(data.2_rn), 1, runif(1, min = 10, max = 30)*0.01)  
  
  # assume that X% of livestock movements occurred with 30 days before or after their reported dates, with the degree of reporting error following the Normal distribution     
  for (j in 1:nrow(data.1_rn)) {
    if(data.1_rn$rn[j] == 0) data.1_rn$Date_rn[j] = data.1_rn$Date[j]
    if(data.1_rn$rn[j] != 0) data.1_rn$Date_rn[j] = data.1_rn$Date[j] + round(truncnorm::rtruncnorm(1, a = -30, b = 30, mean = 0, sd = 10))}
  for (j in 1:nrow(data.2_rn)) {
    if(data.2_rn$rn[j] == 0) data.2_rn$Date_rn[j] = data.2_rn$Date[j]
    if(data.2_rn$rn[j] != 0) data.2_rn$Date_rn[j] = data.2_rn$Date[j] + round(truncnorm::rtruncnorm(1, a = -30, b = 30, mean = 0, sd = 10))}
  
  data.1_rn$Date_rn = as.Date(data.1_rn$Date_rn, origin="1970-01-01") 
  data.2_rn$Date_rn = as.Date(data.2_rn$Date_rn, origin="1970-01-01") 
  
  # compute the number of movements that occurred between the same communes and on the same date 
  data.1_rn = plyr::ddply(data.1_rn,. (Exit.commune, Entry.commune, Date_rn), nrow)
  data.2_rn = plyr::ddply(data.2_rn,. (Exit.commune, Entry.commune, Date_rn), nrow)
  colnames(data.1_rn) = c("Exit", "Entry", "Date_rn", "No.")
  colnames(data.2_rn) = c("Exit", "Entry", "Date_rn", "No.")
  
  # merge official and truck datasets by exit and entry communes and movement date
  list = merge(data.1_rn, data.2_rn, by.x = c("Exit", "Entry", "Date_rn"), by.y = c("Exit", "Entry", "Date_rn"), all = TRUE)
  colnames(list) = c("Exit", "Entry", "Date_rn", "No.data.1", "No.data.2")

  # sum the number of movements in official and truck datasets by exit and entry communes and movement date
  # to account for the possible overlap between the two datasets, randomly select the number of movements from the range between the minimum and maximun possible overlaps 
  for(k in 1:nrow(list)) {
    if(is.na(list$No.data.1[k]) == FALSE & is.na(list$No.data.2[k]) == FALSE) {
      range_upper = list$No.data.1[k] + list$No.data.2[k]
      range_lower = max(list$No.data.1[k],list$No.data.2[k])
      list$No.total[k] = sample(range_lower:range_upper, 1)} 
    if(is.na(list$No.data.1[k]) == TRUE  & is.na(list$No.data.2[k]) == FALSE) {
      list$No.total[k] = list$No.data.2[k]}
    if(is.na(list$No.data.1[k]) == FALSE & is.na(list$No.data.2[k]) == TRUE) {
      list$No.total[k] = list$No.data.1[k]}}

  # prepare edgelist for epidemic simulation (tDays: the number of days elapsed from 2007-01-01) 
  edgelist = list[,c("Exit", "Entry", "Date_rn", "No.total")]
  edgelist = edgelist[order(edgelist$Date_rn),]
  edgelist = transform(edgelist, nDays = as.numeric(strftime(as.Date(Date_rn, format='%d/%m/%Y'), '%j')))
  edgelist = transform(edgelist, tDays = cumsum(c(1, ifelse(diff(nDays) > 0, diff(nDays), diff(nDays) %% 365))))
  edgelist = edgelist[c("Exit", "Entry", "tDays", "No.total")]
  
  # with the edgelist, simulate epidemic spread from 'FirstInfDay'
  for(day in FirstInfDay:Timesteps){
    
    InfDays[InfStatus == 1] = InfDays[InfStatus == 1] + 1
    
    # Identify potentially infected movements on 'day' 
    PotentiallyInfectedEdge = edgelist[edgelist[,3] == day & 
                                         edgelist[,1] %in% as.vector(which(InfStatus == 1)) & 
                                         edgelist[,2] %in% as.vector(which(InfStatus == 0)),]
    
    # if there are any movements from infected communes to susceptible communes, susceptible communes will be infected by a Bernoulli trial with '1, 1-(1-pINF)^Weight'.
    if(length(PotentiallyInfectedEdge) > 0){
      
      Receiver = PotentiallyInfectedEdge[,2]     
      Weight   = PotentiallyInfectedEdge[,4]
      InfStatusMovt = rbinom(length(Weight), 1, 1-(1-pINF)^Weight)
      NewlyInfectedCommune = unique( Receiver[InfStatusMovt == 1] )
      
      if(length(NewlyInfectedCommune) > 0){ InfStatus[NewlyInfectedCommune] = 1 }}
    
    # update infecion status before going to the next iteration
    No.Inf[[i]] = rbind(No.Inf[[i]], sum(InfStatus))          
    No.Sus[[i]] = rbind(No.Sus[[i]], 17 - sum(InfStatus))
    No.Day[[i]] = rbind(No.Day[[i]], day - FirstInfDay + 1)
    Inf.Co[[i]] = rbind(Inf.Co[[i]], InfStatus)
    
    # initialise necessary parameters before going to the next iteration
    InfStatus[InfDays == Tinf] = 0 
    InfDays[InfDays == Tinf] = 0}}

##################################  3. Save time-to-infection ################################## 
T.Inf    = vector(mode = 'list', length = nC) # the number of days elapsed from the day of first infection until a given commune becomes infected. 
S.InfCom = vector(mode = 'list', length = nC) # simulations in which a given commune becomes infected after infection is first introduced.   
for(i in 1:nC) {
  T.Inf[[i]]    = matrix(nrow = 1, ncol = 1)
  S.InfCom[[i]] = matrix(nrow = 1, ncol = 1)}

Time       = vector(mode = "list", length = nC)
Simulation = vector(mode = "list", length = nC) 
col.Time = c("Days", "ID")
F.Time   = vector(mode = "list", length = nC)

for(i in 1:nS) {
  for(j in 1:nC) {
    
    if((No.Inf[[i]][nrow(No.Inf[[i]])] >=8) &    # only consider simulations in which more than half of the communes are infected,
       (Inf.Co[[i]][nrow(Inf.Co[[i]]),j] == 1) & # and commune 'j' is infected. 
       (Inf.Co[[i]][2,j] != 1)) {                # But, simulations in which infection was introduced into commune 'j' on the first day is excluded. 
      S.InfCom[[j]] = rbind(S.InfCom[[j]], i)
      
      # among simulations which meet the above conditions, record the number of days elapsed from the day of first infection until commune 'j' becomes infected. 
      for(k in 2:nrow(Inf.Co[[i]])) {
        if(Inf.Co[[i]][k,j] == 1 & Inf.Co[[i]][k-1,j] == 0) { 
          T.Inf[[j]] = rbind(T.Inf[[j]], k-1)}}}}}

for(i in 1:nC) {
  Time[[i]] = as.data.frame(T.Inf[[i]][-1,])
  Simulation[[i]] = as.data.frame(S.InfCom[[i]][-1,])
  
  Time[[i]]$cID   = i
  colnames(Time[[i]])   = col.Time}

F.Time   = do.call("rbind", Time)
F.Time$ID = as.factor(F.Time$ID)
write.csv(F.Time, file = "Time_0.1_rm.csv")

###### 4. Save percentage simulation in which infected #####
Tot.Inf  = vector(mode = 'list', length = nC) # Simulations in which 1) more than 7 communes were infected and 2) infection was not seeded in commune j. 
Pos.Inf  = vector(mode = 'list', length = nC) # Simulations in which 1) more than 7 communes were infected and 2) infection was not seeded in commune j and 3) commune j was infected.
Not.Inf  = vector(mode = 'list', length = nC) # Simulations in which 1) more than 7 communes were infected and 2) infection was not seeded in commune j and 3) commune j was not infected.

for(i in 1:nC) {
  Tot.Inf[[i]]  = matrix(nrow = 1, ncol = 1)
  Pos.Inf[[i]]  = matrix(nrow = 1, ncol = 1)
  Not.Inf[[i]]  = matrix(nrow = 1, ncol = 1)}

## Total
for(i in 1:nS) {
  for(j in 1:nC) {
    if((No.Inf[[i]][nrow(No.Inf[[i]])] >=8) &     
       (Inf.Co[[i]][2,j] != 1)) {                 
      Tot.Inf[[j]] = rbind(Tot.Inf[[j]], i)}}}

## Positive
for(i in 1:nS) {
  for(j in 1:nC) {
    if((No.Inf[[i]][nrow(No.Inf[[i]])] >=8) &     
       (Inf.Co[[i]][2,j] != 1) &                  
       (Inf.Co[[i]][nrow(Inf.Co[[i]]),j] == 1)) {
      Pos.Inf[[j]] = rbind(Pos.Inf[[j]], i)}}}

## Negative
for(i in 1:nS) {
  for(j in 1:nC) {
    if((No.Inf[[i]][nrow(No.Inf[[i]])] >=8) &     
       (Inf.Co[[i]][2,j] != 1) &                  
       (Inf.Co[[i]][nrow(Inf.Co[[i]]),j] == 0)) { 
      Not.Inf[[j]] = rbind(Not.Inf[[j]], i)}}}

Not.Inf.Sum = vector(length = 17)
Tot.Inf.Sum = vector(length = 17)
Pos.Inf.Sum = vector(length = 17)

for(i in 1:nC) {
  Tot.Inf[[i]] = Tot.Inf[[i]][-1]
  Tot.Inf.Sum[i] = length(Tot.Inf[[i]])
  Not.Inf[[i]] = Not.Inf[[i]][-1]
  Not.Inf.Sum[i] = length(Not.Inf[[i]])
  Pos.Inf[[i]] = Pos.Inf[[i]][-1]
  Pos.Inf.Sum[i] = length(Pos.Inf[[i]])}

Inf.Sum = as.data.frame(cbind(Tot.Inf.Sum, Pos.Inf.Sum, Not.Inf.Sum))
Inf.Sum$Not.P = round(Inf.Sum$Not.Inf.Sum/Inf.Sum$Tot.Inf.Sum,3)
write.csv(Inf.Sum, file = "Inf.Sum_0.1_rm.csv", row.names = FALSE)
############################################################################################################################################################################################






