Card Sorting: Removing Replicates

Dan Gray



As an exercise in skeletal-programming I wanted to evaluate how many times a given set of assorted items needs to be randomly split into two piles, compared against each other and then reassembled removing a given proportion of duplicates at each iteration, until ultimately no duplicate items remain!

Skeletal programming is assembling the bare-minimum programmatic structures needed to test an idea at the level of intuition

I want to use this “wax-rinse-repeat” process in a small project to sort, and categorise a catalogue of tens of thousands of images taken over a decade. The basic idea is to split subsets of the library of images into two directories, apply image-hashing via Python to allocate IDs to each image. A collection of bash tools such as awk, sed are then used to list, collate and remove duplicates in a continuous cycle.

The results from this experimental “card-sorting” exercise suggests that as few as 6-10 splits-repeats are need to reach unity.


I use a model function, and a stepper function to power my simulation. Reproducibility and robustness of results is estimated through repetition using the replicate function.

Model Function

# model (compute) function
# splits duplicates into 2 piles

shares <-function(x,sp){
  fracA <-length(x)*sp
  fracB <-length(x)-fracA
  # generate two pools of data to compare
  sideA <-sample(x,fracA,replace=FALSE)
  sideB <-x[-which(sideA %in% x)]
  # check which tiems are in both pools
  leftB <-sideB[-which(sideB %in% sideA)]
  leftAB <-c(sideA,leftB)

Stepper Function

# stepper function
# passes all parameters to the model function
# holds a loop function for iteration
# writes diagnostics/results to file

stepper <-function(nsteps=15,pool=500,dups=0.25,splits=0.70){
  # generate initial data-set
  sams <-c(seq(1,pool,1),c(sample(seq(1,pool,1),dups*pool)),c(
  # prepare holder for the results
  transactions <-matrix(ncol=2,nrow=nsteps)
  # initialise the loop
  for (i in seq_len(nsteps)){
    sams <-shares(sams,sp=splits)
    res <-round(as.numeric(length(unique(sams))/length(sams)),2)
    trn <-res
    transactions[i,2] <-trn
    transactions[i] <-i # update :: keep track of step in loop

A single run can be returned by running the stepper function with default parameters.

tr <-stepper()
##  num [1:15, 1:2] 1 2 3 4 5 6 7 8 9 10 ...

Robust Estimation of Event-Space

Using the replicate function one can return hundreds of simulations to gain an estimation of the range of possible outcomes and trajectories.

Its worth noting that the output of the replicate function can be returned in a variety of forms - such as an array, matrix etc. Further processing and reshaping can be added to build an object ready for tidy plotting.

# define number of replicate runs
reps <- 100

# returning an array (the default)
opt.array <-replicate(reps,stepper())

# returning a matrix :: using replicate in combination with do-call/rbind
opt.matrix <, replicate(reps, stepper(), simplify=FALSE))

# returning a list :: using lapply in combiantion with
opt.list <, lapply(1:reps, function(i) stepper()))

# returning a dataframe :: convert an array to a data.frame.table without plyr
opt.df <
##   Var1 Var2 Var3 Freq
## 1    A    A    A    1
## 2    B    A    A    2
## 3    C    A    A    3
## 4    D    A    A    4
## 5    E    A    A    5
## 6    F    A    A    6
opt.df %>% datatable(., rownames = TRUE, filter="none",
            options = list(pageLength = 10, scrollX=F)) %>%
  DT::formatStyle(columns = c(1:4), fontSize = '85%')

Tidying and Plotting

I chose to rearrange the returned data.frame into a tidy form. The key-variable pair is: Var2 which contains the iterations of the splits, and Freq which contains and the percentage of unique items remaining.

Var3 contains the alphanumeric count of the simulation runs (1-100). Similarly Var1 contains the step number of the replicate function.

# rearrange the data into a tidy form
df.tidy <-spread(data = opt.df, key = Var2, value = Freq)
##   Var1 Var3 A    B
## 1    A    A 1 0.91
## 2    A    B 1 0.93
## 3    A    C 1 0.92
## 4    A    D 1 0.91
## 5    A    E 1 0.91
## 6    A    F 1 0.92
# preview of tidy data
df.tidy %>% datatable(., rownames = TRUE, filter="none",
            options = list(pageLength = 10, scrollX=F)) %>%
  DT::formatStyle(columns = c(1:4), fontSize = '85%')
# plot simulated runs
ggplot(aes(A,B),data=df.tidy) + 
  geom_line(aes(group=Var3),alpha=0.025) + 
  geom_smooth() + 
  xlab("Number of Iterations Required") + 
  ylab("Fraction of Unique Items") +
  labs(title="Removing Duplicates Through An Iterative Split-Compare-Regroup Approach") +
  theme_plain(base_size = 10)