Multiwave Object


The base functions of optimall allow users to efficiently determine optimum allocation, select samples, and split strata during the design of a multi-wave stratified sampling survey. Despite these features, an efficient sampling workflow in R still requires the user to manually organize the many moving parts of the process including design data, a list of samples, data extracted from the samples, and merged data for each wave. When a sampling design involves many waves, these parts can be difficult to keep track of and may be prone to errors. Even more, it may be difficult to go back and reproduce results at the end of a long sampling process.

This vignette describes an addition to optimall called the multiwave object, which stores the metadata, design, samples, and merged data from each step of the multi-wave sampling process in an accessible format. It is optional for the user to use, but it offers the advantages of automatic organization, efficient compatibility with other functions in optimall, and an option for a summary of the sampling design to be printed at any point. It is recommended that user thoroughly reads the introductory package vignette titled “Using Optimall” before reading this vignette.

This addition contributes towards optimall's goal of being a tool to streamline the often cumbersome aspects of the multi-wave sampling workflow.


The multiwave object uses the S4 class system, which makes it slightly more complicated to work with in R. Fortunately, all of these potential complications are dealt with in the package design and will be unseen by the user. Here is what the user will see:

The multiwave object contains three S4 classes:

The light blue objects in the figure above represent the slots that hold the survey information directly:

Getting Started with the Multiwave Object

To work with the multiwave object, users only need to know a few functions. This section describes how to initialize an object, how to access and write slots of it, and how to deploy some of its useful features. This will be demonstrated in the context of the same two-phase, multiwave sampling design used in the package vignette.

A multiwave object is initialized by the function new_multiwave():

MySurvey <- new_multiwave(phases = 2, waves = c(1,3))

We now have an object that can hold all of the information for our two-phase survey for which the second phase will be conducted over three waves. Note that the length of the ‘waves‘ argument must match the number of phases. Phase 1 will almost always have one wave.

Access and Write slots

As is standard for an S4 object in R, components of the multiwave object are stored in slots. To access and write slots of the multiwave object, the user could use @ and $:

but this is overly complicated and potentially unstable. Instead, any slot of the multiwave object can be accessed or written using the function get_data():

Note that calls to get data from phase 1 do not require wave to be specified, since phase 1 only consists of one wave.

Let’s suppose that our phase 1 data has been collected. We can add it to the phase 1 data slot of our object and include any relevant metadata:

View Summary Diagram of Survey

At any point during the multi-wave sampling workflow, optimall allows users to view a diagram of the structure of their survey with multiwave_diagram():

Notice that the title of the survey, “Maternal Weight Survey”, was found from the overall survey metadata and that boxes are colored based on whether they have been filled yet. Slots that have been filled are blue and contain a short description of their contents. This function enables users to track their progress during a multiwave sampling survey.

Call optimall functions with fewer arguments

Another advantage of the multiwave object is that the primary functions of optimall such as optimum_allocation(), allocate_wave(), and sample_strata() can be called on the object using the function apply_multiwave(), The apply_multiwave() function takes the standard arguments to the function as well as phase and wave, which are used to determine the input dataframe(s) and the slot of the object where the output should be placed. If the arguments, including names of columns (which tend to be repetitive when used without the multiwave object framework), are specified in the metadata, the function will find them itself, allowing calls to the function to be much simpler. It is recommended that users gain familiarity with the basic uses of these functions on dataframes before using them on multiwave objects.

When working with a multiwave object, a new function called merge_samples() also becomes available. This allows users to quickly and efficiently merge the sampled data with the previous wave’s data.

To demonstrate, we return to the example of sampling sepal width from the iris dataset with an adaptive, multi-wave design. Suppose that we have collected data on sepal length, petal width, and petal length for all 150 iris plants in phase 1, but we have not yet collected any of the ”expensive” sepal width variable. We can start by placing the data in the appropriate slot of our multiwave object:

Now we want to begin our first wave of sampling sepal width in phase 2. Since we expect that sepal length is correlated with our variable of interest, we decide to x-allocate the first wave of samples using integer-valued Neyman allocation on the inexpensive sepal length variable. Since we are working in the multiwave object framework, we can use apply_multiwave() to apply the optimum_allocation() function:

Since "strata" will be "Species" for every wave, we may instead move that argument to the phase metadata so that we don’t have to repetitively specify the same argument in every function call:

Now we no longer have to specify strata in the function call:

In the absence of a specific strata argument, apply_multiwave() turns to the wave, phase, and then overall metadata to look for the missing argument. In this case, it finds strata = "Species" in the phase metadata. By specifying the phase and wave in the function call, we are telling optimum_allocation() to use the most recent version of the data (the full data from the previous wave, or in this case, the phase 1 data) as input and to output the results in the corresponding slot of the specified wave. As such, both calls to apply_multiwave() output an updated multiwave object with the results of optimum_allocation() in the phase 2, wave 1 "design" slot:

The allocate_wave() function can be applied to multiwave objects in the same manner and will be demonstrated in the following examples section. After these functions have been used to specify a ”design” data frame (or a manually created design data frame has been placed in the ”design” slot to implement a different allocation strategy), we can use apply_multiwave() to apply sample_strata() and select the ids to sample for the given sampling wave:

Note that we did not have to specify the data or design_data as we do in the standard version of split_strata() because they are extracted using the phase and wave arguments. We also did not have to specify the strata argument again because it was available in the phase metadata. The result of this call to apply_multiwave() is an updated IrisSurvey with a character vector of ids to sample in the ”samples” slot:

When working with a multiwave object, a new function called merge_samples() also becomes available. This function allows users to quickly and efficiently merge the sampled data with the previous wave’s data. Suppose that we have collected the sepal width for these 30 plant ids and placed the data in the “sampled_data” slot of phase 2, wave 1 of IrisSurvey:

We can call merge_samples() to smoothly merge the sampled_data of the current wave with the (full) data from the previous wave and place the output in the data slot of the current wave:

In the "data" slot of phase 2, wave 1, we will now have an updated dataframe with all of the phase 1 data and a sepal width column that and has the sampled data for the selected ids and NA values for the rest of the plants. There is also a new column called "sampled_phase2" that holds an indicator for which samples have been sampled in phase 2 thus far:

Calls to merge_samples() in later waves of phase 2 will update the phase sampled indicator each time. The sample_strata function will use this column to ensure that units sampled in previous waves are not selected again. The utility of merge_samples() and apply_multiwave() are demonstrated further below.

Example Wave Workflow Using optimall with a Multiwave Object

In this section, we demonstrate an example of how the multiwave object can be useful in the adaptive multi-wave sampling workflow. In this example, we will create and execute Wave 1 of Phase 2 of our sample.

The first step of Phase 2, Wave 1 is to specify the metadata for both the phase and wave. Note that can be useful to specify a title, description, function arguments, and anything else relevant to the survey in the metadata, but nothing is required to be specified. If no function arguments are found in the metadata, they must instead be specified during the call to apply_multiwave(). In this example we choose to specify a description and function arguments in the wave metadata:

After the metadata is specified, we create a dataframe specifying the survey design. It contains one variable holding the stratum names and one variable specifying how many samples should be allocated to each stratum in Wave 1. We will use proportional sampling for the first 250 samples.

We can use this design to randomly select ids to sample from the previous wave (in this case the overall Phase 1 data) using apply_multiwave() to apply the function sample_strata() to the multiwave object. When applied to an object of class “Multiwave”, sample_strata() will find "data" and "design_data" in the object and will look in the wave, phase, then overall metadata for the other arguments to sample_strata() if they are not provided in the call. It will return the same multiwave object with a vector of ids to sample in the "samples" slot of the specified wave.

We can use these sample ids to sample the Phase 2, Wave 1 data from MatWgt_Sim, which holds the (hypothetically unknown) mat_weight_true variable:

Then, we can use the function merge_samples() to merge the sampled data (250 rows of data sampled in wave 1 of phase 2) that we just collected into our full dataframe (10,335 rows of data sampled in phase 1). This function generates the dataframe for the "data" slot of the specified wave using the "sampled data" and the data from the previous wave (Phase 1 data in this case).

All the slots for this wave are now filled! We can check to make sure everything that we have done so far looks good using multiwave_diagram():

It does! Now that we have sampled data which we can use to estimate stratum standard deviations, we can use allocate_wave() to allocate samples optimally for the next wave. When applied to a multiwave object using apply_multiwave(), allocate_wave() will find the data from the previous wave (now the previous wave is Wave 1 of Phase 2) and will look for other function arguments for allocate_wave() in the metadata. The output will be placed in the "design" slot of the specified wave:

Now the design for Wave 2 is specified. We can continue with similar steps for future waves!


Through these examples, we see the many benefits that the creation of a Multiwave object offers in optimall.