# Chapter 10 - Subset choice

In this chapter you will learn how to use computer simulations as a theoretical
tool, namely to analyze the consequences different formalizations of verbal
theories. To reach that goal, you will also learn how to read an implementation
of formal theory in Scala `mathlib`

. At the end of this chapter, you will be
able to use (adapt and run) the provided simulation code to compare three formal
models. You will be able to test your intuitions about the theory and derive qualitative differences between them.

We pick up the conversation between Verbal and Formal from Chapter 4 - Subset Choice. Formal is very excited to share the computer simulations they implemented of the theoretical models Formal and Verbal created. Formal has some suggestions on how to use the simulations, which they explain to Verbal.

**Formal:** Welcome dr. Verbal! As promised, I have implemented computer simulations for three of our computational-level models.

**Verbal:** That’s great. You said we can use the simulations to explore the models’ empirical implications. How does that work?

**Formal:** The three formal models each make different tradeofs in optimizing selecting guests…

**Verbal:** Yes, I remember. Shall I run some experiments to see which one is best?

**Formal:** …wait! Before you leave me alone again for a few months, let’s do a bit more theory before the test (van Rooij & Baggio, 2021). Do we even know if the models are different in important and meaningful ways? Even if they are different, are they so under sensible conditions?

**Verbal:** But the formalizations are different, so the models must behave differently, right?

**Formal:**
Not necessarily. Formalizations that are different may behave the same or very similarly. Sometimes we can analytically derive such equivalenceYou can read about mathematically proving model equivalence in Chapter 5 - Coherence. but this is not always possible. Then computer simulations can come in handy.

**Verbal:** Ah, I see. I would like to know if there are important differences between the theories. That way we can possibly rule out theories that cannot explain the phenomenon or find ways to update them, just like when we were formalizing my verbal theories.

**Formal:** Indeed, that is the idea.

If you jumped here directly from Chapter 4 - Subset choice
you may find it helpful to first read Chapter 9 - Scala and mathlib to learn how to read (and write) Scala code using the `mathlib`

library. In addition to the default `mathlib`

library, the simulation code on this page includes supporting code which we explain first.

## Supporting code

Running simulations requires input instances as specified by the theoretical model. While we could code input by hand, that is a lot of work. The beauty of using computer simulations is that it can do the hard work for us by *automatically* generating input. To that end, Formal has written supporting code. Supporting code is often written specifically for a domain. For example, simulating Coherence uses different support code.

For now, it is not important that you know how to write support code. However, in order to explore and adapt the code that Formal has provided, being able to *use* support code is recommended. Let’s explore some examples. Remember that you can run (and adapt) the code in your browser using the button.

The theoretical models for selecting invitees (subset choice) take as input sets of persons and a function that for pairs of persons returns if they like eachother or not. The support code helps us generate these parts of the input.

### Persons

A particular person is identified by their name, and can be defined by using `Person(name: String)`

. This function takes a string as input and returns a Person object with the given name:

```
Person("Jamie")
```

Persons with the same name are considered to refer to the same individual, since the computer cannot distinguish between them.

```
val person1 = Person("Jamie")
val person2 = Person("Jamie")
person1 == person2
```

We can also create random persons. Their names are randomly selected from a predefined list with 100 names. Running the code below multiple times will create different persons.

```
Person.random
```

We can also generate groups of `n`

random individuals.

```
Person.randomGroup(5)
```

These functions will help us create sets of persons. We can then use `mathlib`

to work with these sets as expected. For example, we can create a set of random persons \(P\), randomly take 2 persons who are liked \(L\), and create a set of persons who are disliked \(D=P \setminus L\):

```
val persons = Person.randomGroup(5)
val personsLiked = persons.take(2) // Take 2 people from persons.
val personsDisliked = persons \ personsLiked
println(personsLiked)
println(personsDisliked)
println(persons)
```

### Like-function

The final support code Formal provided is used to create like relationships between persons. In the formal model this function is defined as \(like: P\times P \rightarrow \{true,false\}\). After discussing with a colleague (see Exercise X in Chapter 4), Formal recognized that the like function was intended to exclude reflection (i.e., self-liking) and is symmetrical \(like(a,b)=like(b,a)\) (i.e., it formalizes like or dislike *eachother*).The formalizations in this chapter are updated with these properties.

One could specify a like relationship manually. Simply create persons, store them in values so we can refer to them and then use `likes`

or `dislikes`

to create like relationships.

```
val lela = Person("Lela")
val carlos = Person("Carlos")
val ervin = Person("Ervin")
println(lela likes carlos)
println(carlos dislikes ervin)
println(carlos dislikes lela)
```

Specifying a *complete* like function for a set of persons, however, will be quite a chore: for each pair you need to explicate if \(a\) likes \(b\) and vice versa. For \(10\) persons, that is a list of \(10 \cdot 10=100\) likes. Support functions help us reduce this chore.

The code snippet below is interleaved with explanation text. Pressing removes the explanation text to run the code. You can get the explanation back by reloading this webpage. When given a partial specification of the like function, we can complete it by assuming that any non-specified relationship is a dislike. Use the support function `.deriveLikeFunction(partialLikes: Set[Likes])`

on a set of persons to create a like function for which the domain consists of all pairs of persons (including \((a,b)\), \((b,a)\) and \(a,a\)). It will complete `partialLikes`

by assuming non-specified relationships are dislikes.

```
val lela = Person("Lela")
val carlos = Person("Carlos")
val ervin = Person("Ervin")
val persons = Set(lela, carlos, ervin)
val partialLikings = Set(lela likes carlos, carlos likes ervin,carlos dislikes lela)
def like = persons.deriveLikeFunction(partialLikings)
```

The `Viz.render()`

function can draw graphs specified in the DOT language. The `.toDotString(like)`

helper function transforms persons and a like function to graph figures.

```
Viz.render(persons.toDotString(like))
```

And we can view the truth values associated with all pairs of persons.

```
List(
like(lela, carlos),
like(lela, ervin),
like(carlos, ervin)
)
```

While this approach is useful to manually explore small examples, it still is a lot of manual work. Wouldn’t it be nice if we can generate a complete like function randomly? Use the support function `.randomLikeFunction(probability: Double)`

on a set of persons to create a random like function. For each pair (including \((a,b)\), \((b,a)\) and \(a,a\)), it generates `true`

with probability equal to the ratio or false otherwise.

```
val lela = Person("Lela")
val carlos = Person("Carlos")
val ervin = Person("Ervin")
val persons = Set(lela, carlos, ervin)
def like = persons.randomLikeFunction(0.7)
Viz.render(persons.toDotString(like))
List(
like(lela, carlos),
like(lela, ervin),
like(carlos, ervin)
)
```

What happens to the output of the like function when you change the probability?

A final example to illustrate how to generate a random input instance. An alternative visualization is used to indicate which persons are liked by the host or not. Note that generating a visualization graph with many persons will not display properly or potentially crash your browser due to the many relationships.

```
val persons = Person.randomGroup(5)
val personsLiked = persons.take(2)
val personsDisliked = persons \ personsLiked
def like = persons.randomLikeFunction(0.7)
Viz.render(persons.toDotString(personsLiked, personsDisliked, like))
```

With these support functions, we can randomly create instances for the formal models of selecting invitees. Why is this helpful?

## Simulating Selecting Invitees

In this section we cover how to simulate Selecting invitees (version
4, 5 and 6) . You will learn how to read Scala `mathlib`

simulation code
and how it relates to the formalization. We go through Selecting
invitees (version 4) step by step, after which you can explore versions 5 and
6 yourself. To make the code more readable, we use names in the code that are
more descriptive than the single letters used in math (see Table 1).

Table 1: the mapping from math notation to Scala code.

Math | Scala | Description |
---|---|---|

\(P\) | `persons` |
Set of persons from which to select invitees. |

\(L\) | `personsLiked` |
Subset of persons that is liked. |

\(D\) | `personsDisliked` |
Subset of persons that is disliked. |

\(like\) | `like` |
Function that captures if two persons like each other or not. |

\(k\) | `k` |
Value that states how many of the invited persons at most can be disliked. |

\(G\) | `invitees` |
Set of invited persons. |

\(X\) | `x` |
Set of all unique pairs of persons that like each other. |

\(Y\) | `y` |
Set of all unique pairs of persons that dislike each other. |

Take a moment to familiarize yourself again with the formalization. If you need more context, you can go back to Chapter 4 - Subset choice where the formalization was introduced.

Selecting invitees (version 4)

Input: A set \(P\), subsets \(L \subseteq P\) and \(D \subseteq P\) with \(L \cap D = \emptyset\) and \(L \cup D = P\), a function \(like: P \times P \rightarrow \{true, false\}\), and a threshold value \(k\).

Output:
\(G \subseteq P\) such that \(|G\cap D| \leq k\) and \(|X| + |G|\) is maximized (where \(X = \{p_i,p_j \in G~|~like(p_i,p_j) = true \wedge i\neq j\}\)).

Let’s see how this formalization translates to simulation code. The
formalization is implemented in the `si4`

function, all of the input (\(P\),
\(L\), \(D\), \(like\) and \(k\)) is listed as an argument of the function. The
type of the output also needs to be defined. In this case the output is a subset
\(G\subseteq P\) of persons, translating to type `Set[Person]`

.

```
def si4(persons: Set[Person],
personsLiked: Set[Person],
personsDisliked: Set[Person],
like: (Person, Person) => Boolean,
k: Int): Set[Person] = {
```

The input in the formalization is subject to a few constraints. We check those constraints in the code and stop the program of the constraints are not met with an informative error message.

```
// Input must satisfy these constraints, otherwise error.
require(personsLiked <= persons,
"personsLiked must be a subset of persons")
require(personsDisliked <= persons,
"personsDisliked must be a subset of persons")
require(personsLiked /\ personsDisliked == Set.empty,
"personsLiked intersect personsDisliked must be emtpy")
require(personsLiked \/ personsDisliked == persons,
"personsLiked union personsLiked must equal persons")
```

The output is defined using two properties. To define output using the set builder we write two functions that compute these properties. First, the host wants to invite at most \(k\) people they dislike \(|G \cap D|\leq k\). This function returns a Boolean if a given (sub)set of people does not have this property.

```
// Specify that invitees is valid if |G /\ D| <= k.
def atMostKDislikes(invitees: Set[Person]): Boolean = {
(invitees /\ personsDisliked).size <= k
}
```

Second, the formalization states that the number of invited pairs that like each other plus the number of invited people \(|X| + |G|\) is maximal. This is an optimality condition. This function computes for a given (sub)set of people, the set \(X\) and returns an integer corresponding to \(|X| + |G|\).

The `.tupled`

function transforms a function
with \(n\) arguments into a function with 1 argument, where that argument is an
\(n\)-tuple. This is needed when applying a function on a set of tuples that
correspond to the arguments of that function.

```
// Specify the optimality condition.
def xg(invitees: Set[Person]): Int = {
// The number of unique pairs that like eachother.
val x = { invitees.uniquePairs | like.tupled }.size
// The number of total invitees.
val g = invitees.size
x + g
}
```

Here we specify the set of possible valid outputs. Remember that for any given
formalization multiple possible outputs may exist that satisfy the output
conditions. Below, we consider all possible subsets of people (the powerset
\(\mathcal{P}(P)\)). Any \(G\in\mathcal{P}(P)\) is a subset of people
\(G\subseteq P\). From this set of sets we build a set of sets of people that
satisfy \(|G \cap D|\leq k\) using `atMostKDislikes`

and the
optimality condition \(\arg\max_{|X|+|G|}\) using `argMax(xg)`

.

```
val invitees = { powerset(persons) | atMostKDislikes _ }.argMax(xg)
```

To complete the implementation, we need to output one valid solution if any exist. If multiple possible solutions exist, we return one at random. Minimally one solution will always exist, namely the empty set, so we can safily ask for a random one.

```
// Return a (valid) set of invitees at random.
invitees.random.get
}
```

This conclused the implementation of Selecting Invitees (version 4) . Now we need to create some input on which it can run and to do that we use the helper functions.

```
val group = Person.randomGroup(10) // Generate random group
val personsLiked = group.take(5) // The first 5 are liked
val personsDisliked = group.drop(5) // The rest is disliked
def like = group.randomLikeFunction(.7) // Autogenerate random like relations
Viz.render(group.toDotString(personsLiked, personsDisliked, like))
```

Then we simply evaluate `si4(.)`

on this input.

```
si4(group, personsLiked, personsDisliked, like, k = 2)
```

Try to play around with the ratios of people that are liked by the host and the ratio of pairs that like eachother. Look at the visualization of the input and see if you can find some interesting observations on the output.

In these simulations you can generate groups of any size. The simulation,
however, considers all possible subsets of people. How many possible subsets
exist given 3 people? The first person can be *in* or *out*, that’s two
options. The second person can also be *in* or *out*, that’s again two options,
but combined with the first thats \(2 \times 2\) options. The third person can
be *in* or *out* making \(2\times 2\times 2=8\) options. How many possible
subsets exist for 4 people? And for 8? and 15?

Keep in mind that the search space grows exponentially with the size of \(P\). If your computer crashes or is taking a long time, you are probably trying to simulate for large (\(>10\)) groups.

From here on, you are free to explore the implementations of Selecting Invitees (versions 5 and 6) on your own. Try simulating some inputs to get a feeling for the differences between the three formalizations. You can even change the simulation code if you want. Perhaps try implementing any of the other versions? After simulating the three models individually, we provide a sandbox for you to compare their behaviour directly.

Selecting invitees (version 5)

Input: A set \(P\), subsets \(L \subseteq P\) and \(D \subseteq P\) with \(L \cap D = \emptyset\) and \(L \cup D = P\), and a function \(like: P \times P \rightarrow \{true, false\}\).

Output:
\(G \subseteq P\) such that \(|G\cap L| + |X| + |G|\) is maximized (where \(X = \{p_i,p_j \in G\}~|~like(p_i,p_j) = true \wedge i\neq j\}\)).

```
def si5(persons: Set[Person],
personsLiked: Set[Person],
personsDisliked: Set[Person],
like: (Person, Person) => Boolean): Set[Person] = {
// Input must satisfy these constraints, otherwise error.
require(personsLiked <= persons,
"personsLiked must be a subset of persons")
require(personsDisliked <= persons,
"personsDisliked must be a subset of persons")
require(personsLiked /\ personsDisliked == Set.empty,
"personsLiked intersect personsDisliked must be emtpy")
require(personsLiked \/ personsDisliked == persons,
"personsLiked union personsLiked must equal persons")
// Specify the optimality condition.
def gl_x_g(invitees: Set[Person]): Int = {
// The number of invitees the host likes.
val gl = (invitees /\ personsLiked).size
// The number of unique pairs that like eachother.
val x = { invitees.uniquePairs | like.tupled }.size
// The number of total invitees.
val g = invitees.size
gl + x + g
}
val invitees = powerset(persons).argMax(gl_x_g)
// Return a (valid) set of invitees at random.
invitees.random.get
}
val group = Person.randomGroup(10) // Generate random group
val personsLiked = group.take(5) // The first 5 are liked
val personsDisliked = group.drop(5) // The rest is disliked
def like = group.randomLikeFunction(.7) // Autogenerate random like relations
Viz.render(group.toDotString(personsLiked, personsDisliked, like))
si5(group, personsLiked, personsDisliked, like)
```

Selecting invitees (version 6)

Input: A set \(P\), subsets \(L \subseteq P\) and \(D \subseteq P\) with \(L \cap D = \emptyset\) and \(L \cup D = P\), a function \(like: P \times P \rightarrow \{true, false\}\), and a threshold value \(k\).

Output:
\(G \subseteq P\) such that \(|Y| \leq k\) and \(|G\cap L|+|G|\) is maximized (where \(Y = \{p_i,p_j \in G\}~|~like(p_i,p_j) = false \wedge i\neq j \}\)).

```
def si6(persons: Set[Person],
personsLiked: Set[Person],
personsDisliked: Set[Person],
like: (Person, Person) => Boolean,
k: Int): Set[Person] = {
// Input must satisfy these constraints, otherwise error.
require(personsLiked <= persons,
"personsLiked must be a subset of persons")
require(personsDisliked <= persons,
"personsDisliked must be a subset of persons")
require(personsLiked /\ personsDisliked == Set.empty,
"personsLiked intersect personsDisliked must be emtpy")
require(personsLiked \/ personsDisliked == persons,
"personsLiked union personsLiked must equal persons")
// Specify that invitees is valid if |Y| <= k.
def atMostKPairDislikes(invitees: Set[Person]): Boolean = {
{ invitees.uniquePairs | like.tupled }.size <= k
}
// Specify the optimality condition.
def gl_g(invitees: Set[Person]): Int = {
// The number of invitees the host likes.
val gl = (invitees /\ personsLiked).size
// The number of total invitees.
val g = invitees.size
gl + g
}
val invitees = { powerset(persons) | atMostKPairDislikes _ }.argMax(gl_g)
// Return a (valid) set of invitees at random.
invitees.random.get
}
val group = Person.randomGroup(10) // Generate random group
val personsLiked = group.take(5) // The first 5 are liked
val personsDisliked = group.drop(5) // The rest is disliked
def like = group.randomLikeFunction(.7) // Autogenerate random like relations
Viz.render(group.toDotString(personsLiked, personsDisliked, like))
si6(group, personsLiked, personsDisliked, like, k = 2)
```

### Analyzing and comparing formalizations

Simulations are a powerful tool to uncover consequences of formalization choices, especially those that are hard to derive mathematically. However, the full power of simulations is yet to be unlocked. Looking at single input instances of single formalizations is not very informative and wouldn’t be worth the effort of coding. Let’s see what we can learn about the three versions of Selecting Invitees by comparing them to eachother. We follow the example questions from Chapter 8.

First we ask: *Are these formalizations truly different, or are they
equivalent?* We can run the simulation for all three versions on the same input
to compare their output. To prevent redundant copying, the implementations can
be found in `SelectingInvitees.si4(.)`

, `SelectingInvitees.si5(.)`

and `SelectingInvitees.si6(.)`

.

Using the code below, can you find input where two or more of the models give the same output? If you find such input(s), what is it about them that leads to equivalence?

```
val group = Person.randomGroup(10) // Generate random group
val personsLiked = group.take(5) // The first 5 are liked
val personsDisliked = group.drop(5) // The rest is disliked
def like = group.randomLikeFunction(.7) // Autogenerate random like relations
val k = 2
println("Output SI4: " + SelectingInvitees.si4(group, personsLiked, personsDisliked, like, k))
println("Output SI5: " + SelectingInvitees.si5(group, personsLiked, personsDisliked, like))
println("Output SI6: " + SelectingInvitees.si6(group, personsLiked, personsDisliked, like, k))
Viz.render(group.toDotString(personsLiked, personsDisliked, like))
```

For some inputs the formalizations might be equivalent, but for many others they
are not. Next, try to answer the question: *How would you be able tell different
formalizations apart in terms of the behaviour that they predict?* Finally your
hard work will pay off, because you can use simulations to do this. The code
below consists of three steps: (1) generate a set of inputs, (2) compute for all
inputs the corresponding output using `si4`

, `si5`

and `si6`

, (3)
perform data analysis and plotting.

For Step 1 and 3 some additional (helper) code is introduced. Step 1 introduces
code that generates input using the same helper functions we’ve already seen,
but at a larger scale (i.e., more inputs) and by giving control over input
properties. This is the *constrained input generator* (see Chapter
7). In Step 3, we perform
an example analysis of the simulation data.

Using the code below, what kind of differences can you find between the three formal theories and when do you find them? Under what conditions do they disappear?

```
// Generate inputs
val inputData: List[SelectingInvitees.Input] =
SelectingInvitees.inputGenerator(groupSize = 5,
likeDislikeRatio = .2,
pairLikeRatio = .4,
k = 2,
sampleSize = 50)
// Compute outputs
val outputDataSI4: List[Set[Person]] = inputData.map(input =>
SelectingInvitees.si4(input.group,
input.personsLiked,
input.personsDisliked,
input.like,
input.k))
val outputDataSI5: List[Set[Person]] = inputData.map(input =>
SelectingInvitees.si5(input.group,
input.personsLiked,
input.personsDisliked,
input.like))
val outputDataSI6: List[Set[Person]] = inputData.map(input =>
SelectingInvitees.si6(input.group,
input.personsLiked,
input.personsDisliked,
input.like,
input.k))
// Perform data analyses
def analysis1(io: (SelectingInvitees.Input, Set[Person])): (Double, Double) = {
val input = io._1
val output = io._2
val nrLikes = input.group.uniquePairs.filter(input.like.tupled).size
val nrDislikes = input.group.uniquePairs.filter(!input.like.tupled(_)).size
val ldRatio = nrLikes.toDouble / nrDislikes
val size = output.size.doubleValue
(ldRatio, size)
}
val dataAnalysis1SI4 = (inputData zip outputDataSI4).map(analysis1)
val dataAnalysis1SI5 = (inputData zip outputDataSI5).map(analysis1)
val dataAnalysis1SI6 = (inputData zip outputDataSI6).map(analysis1)
def analysis2(io: (SelectingInvitees.Input, Set[Person])): (Double, Double) = {
val input = io._1
val output = io._2
val nrLikes = input.group.uniquePairs.filter(input.like.tupled).size
val nrDislikes = input.group.uniquePairs.filter(!input.like.tupled(_)).size
val ldRatio = nrLikes.toDouble / nrDislikes
val avgLikes = output.uniquePairs.filter(input.like.tupled).size
(ldRatio, avgLikes)
}
val dataAnalysis2SI4 = (inputData zip outputDataSI4).map(analysis2)
val dataAnalysis2SI5 = (inputData zip outputDataSI5).map(analysis2)
val dataAnalysis2SI6 = (inputData zip outputDataSI6).map(analysis2)
// Plot analysis 1
val trace14 = Trace(dataAnalysis1SI4, "SI4", PlotType.Line).mean
val trace15 = Trace(dataAnalysis1SI5, "SI5", PlotType.Line).mean
val trace16 = Trace(dataAnalysis1SI6, "SI6", PlotType.Line).mean
Plot(List(trace14, trace15, trace16),
xAxisTitle = "pair-wise like/dislike ratio",
yAxisTitle = "nr invitees").render
// Plot analysis 2
val trace24 = Trace(dataAnalysis2SI4, "SI4", PlotType.Line).mean
val trace25 = Trace(dataAnalysis2SI5, "SI5", PlotType.Line).mean
val trace26 = Trace(dataAnalysis2SI6, "SI6", PlotType.Line).mean
Plot(List(trace24, trace25, trace26),
xAxisTitle = "pair-wise like/dislike ratio",
yAxisTitle = "average likes among invitees").render
```

The analysis and plotting functionality within the online Scala system is quite
limited. If you want to explore the simulations more extensively consider
running the simulations in a dedicated Scala development environment (see
Installing Scala and `mathlib`

) and
download the code here. You can also use the code block below and download the
raw data to perform analyses in your favorite statistical analysis software. The
code below might take longer to run as it simulates Selecting Invitees
for many more combinations of parameters. The resulting CSV file will also
be possibly large. Table 2 lists the CSV format.

Table 2: CSV format for group size \(n\).

column | type | description |
---|---|---|

p0 .. pn | true/false | host likes pi |

p0-p1 .. pn-p(n-1) | true/false | pi and pj like each other |

k | int | k value |

p0-si4 .. pn-si4 | true/false | pi is invited in si4 |

p0-si5 .. pn-si5 | true/false | pi is invited in si5 |

p0-si6 .. pn-si6 | true/false | pi is invited in si6 |

```
val groupSize = 6
val likeDislikeRatios = Set(0, 0.22, 0.66, 1.0)
val pairLikeRatios = Set(0, 0.22, 0.66, 1.0)
val ks = Set(0, 0.22, 0.66, 1.0)
val sampleSize = 1
val inputData: List[SelectingInvitees.Input] =
(for(likeDislikeRatio <- likeDislikeRatios;
pairLikeRatio <- pairLikeRatios;
k <- ks) yield {
SelectingInvitees.inputGenerator(
groupSize,
likeDislikeRatio,
pairLikeRatio,
(k * groupSize).intValue,
sampleSize
)
}).toList.flatten
// Compute outputs
val outputDataSI4: List[Set[Person]] = inputData.map(input =>
SelectingInvitees.si4(input.group,
input.personsLiked,
input.personsDisliked,
input.like,
input.k))
val outputDataSI5: List[Set[Person]] = inputData.map(input =>
SelectingInvitees.si5(input.group,
input.personsLiked,
input.personsDisliked,
input.like))
val outputDataSI6: List[Set[Person]] = inputData.map(input =>
SelectingInvitees.si6(input.group,
input.personsLiked,
input.personsDisliked,
input.like,
input.k))
// Safe data to CSV
def inputHeader(input: SelectingInvitees.Input): String = {
val groupList = input.group.toList
val people = for(i <- groupList.indices) yield s"p$i"
val pairs = for(i <- groupList.indices; j <- groupList.indices if i != j) yield s"p$i-p$j"
people.mkString("", ",\t", ",\t") + pairs.mkString("", ",\t", ",\t") + "k"
}
def outputHeader(input: SelectingInvitees.Input, label: String): String = {
val groupList = input.group.toList
val people = for(i <- groupList.indices) yield s"p$i-$label"
people.mkString(",\t")
}
def dataToCSV(input: SelectingInvitees.Input, outputs: List[Set[Person]]): String = {
val groupList = input.group.toList
val hostLikes = groupList.map(person => person in input.personsLiked)
val likings = for(p1 <- groupList; p2 <- groupList if p1 != p2) yield input.like(p1, p2)
val k = input.k
val results = outputs.map(output => groupList.map(_ in output).mkString(",\t"))
hostLikes.mkString("", ",\t", ",\t") + likings.mkString("", ",\t", ",\t") + input.k + results.mkString(",\t", ",\t", "")
}
val header = inputHeader(inputData.head) + ",\t" +
outputHeader(inputData.head, "si4") + ",\t" +
outputHeader(inputData.head, "si5") + ",\t" +
outputHeader(inputData.head, "si6")
val rows = for(i <- inputData.indices) yield
dataToCSV(inputData(i), List(outputDataSI4(i), outputDataSI5(i), outputDataSI6(i)))
val csv = header + "%0A" + rows.mkString("%0A")
Fiddle.print(a(href:=s"data:text/csv,$csv", target:="_blank", attr("download"):="data.csv", "Right click and Save link as..."))
```

### References

van Rooij, I., & Baggio, G. (2021). Theory before the test: How to build high-verisimilitude explanatory theories in psychological science. *Perspectives on Psychological Science, 16*(4) 682–697.