Skip to content

Commit

Permalink
Revised many vignettes
Browse files Browse the repository at this point in the history
  • Loading branch information
yhoriuchi committed Aug 16, 2023
1 parent b0464cd commit 82bc3f6
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 30 deletions.
Binary file modified .DS_Store
Binary file not shown.
242 changes: 242 additions & 0 deletions data-raw/ACHR_Modified_2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<?php
// Code to randomly generate conjoint profiles to send to a Qualtrics instance

// Terminology clarification:
// Task = Set of choices presented to respondent in a single screen (i.e. pair of candidates)
// Profile = Single list of attributes in a given task (i.e. candidate)
// Attribute = Category characterized by a set of levels (i.e. education level)
// Level = Value that an attribute can take in a particular choice task (i.e. "no formal education")

// Attributes and Levels stored in a 2-dimensional Array

// Function to generate weighted random numbers
function weighted_randomize($prob_array, $at_key){
$prob_list = $prob_array[$at_key];

// Create an array containing cutpoints for randomization
$cumul_prob = array();
$cumulative = 0.0;
for ($i=0; $i<count($prob_list); $i++){
$cumul_prob[$i] = $cumulative;
$cumulative = $cumulative + floatval($prob_list[$i]);
}

// Generate a uniform random floating point value between 0.0 and 1.0
$unif_rand = mt_rand() / mt_getrandmax();

// Figure out which integer should be returned
$outInt = 0;
for ($k = 0; $k < count($cumul_prob); $k++){
if ($cumul_prob[$k] <= $unif_rand){
$outInt = $k + 1;
}
}

return($outInt);
}



function weighted_randomize_race(){
$draw = mt_rand(1, 100);
if($draw <= 50){
return('Black');
} elseif($draw <= 75){
return('Hispanic');
} else{
return("Asian");
}
}

$featurearray = array(
"Gender" => array("Man", "Woman"),
"Age" => array("40", "52", "61"),
"Graduate degree from" => array("University of Michigan",
"Florida State University",
"Kennesaw State University",
"Capella University"),
"Prior years of relevant experience" => array("5", "10", "15"),
"Communication skills" => array("Moderate", "Strong", "Very Strong"),
"Strength of references" => array("Moderate", "Strong", "Very Strong"),
"Quality of writing sample" => array("Moderate", "Strong", "Very Strong"),
"Race/ethnicity" => array("White", "Black", "Hispanic", "Asian")
);

$restrictionarray = array();

$probabilityarray = array(
"Gender" => array(0.5, 0.5),
"Age" => array(0.33333, 0.33333, 0.33333),
"Graduate degree from" => array(0.25, 0.25, 0.25, 0.25),
"Prior years of relevant experience" => array(0.33333, 0.33333, 0.33333),
"Communication skills" => array(0.33333, 0.33333, 0.33333),
"Strength of references" => array(0.33333, 0.33333, 0.33333),
"Quality of writing sample" => array(0.33333, 0.33333, 0.33333),
"Race/ethnicity" => array(0.4, 0.2, 0.2, 0.2)
);

// Indicator for whether weighted randomization should be enabled or not
$weighted = 1;

// K = Number of tasks displayed to the respondent
$K = 20;

// N = Number of profiles displayed in each task
$N = 2;

// num_attributes = Number of Attributes in the Array
$num_attributes = count($featurearray);


$attrconstraintarray = array();


// Re-randomize the $featurearray

// Place the $featurearray keys into a new array
$featureArrayKeys = array();
$incr = 0;

foreach($featurearray as $attribute => $levels){
$featureArrayKeys[$incr] = $attribute;
$incr = $incr + 1;
}

// Backup $featureArrayKeys
$featureArrayKeysBackup = $featureArrayKeys;

// If order randomization constraints exist, drop all of the non-free attributes
if (count($attrconstraintarray) != 0){
foreach ($attrconstraintarray as $constraints){
if (count($constraints) > 1){
for ($p = 1; $p < count($constraints); $p++){
if (in_array($constraints[$p], $featureArrayKeys)){
$remkey = array_search($constraints[$p],$featureArrayKeys);
unset($featureArrayKeys[$remkey]);
}
}
}
}
}
// Re-set the array key indices
$featureArrayKeys = array_values($featureArrayKeys);
// Re-randomize the $featurearray keys
shuffle($featureArrayKeys);

// Re-insert the non-free attributes constrained by $attrconstraintarray
if (count($attrconstraintarray) != 0){
foreach ($attrconstraintarray as $constraints){
if (count($constraints) > 1){
$insertloc = $constraints[0];
if (in_array($insertloc, $featureArrayKeys)){
$insert_block = array($insertloc);
for ($p = 1; $p < count($constraints); $p++){
if (in_array($constraints[$p], $featureArrayKeysBackup)){
array_push($insert_block, $constraints[$p]);
}
}

$begin_index = array_search($insertloc, $featureArrayKeys);
array_splice($featureArrayKeys, $begin_index, 1, $insert_block);
}
}
}
}


// Re-generate the new $featurearray - label it $featureArrayNew

$featureArrayNew = array();
foreach($featureArrayKeys as $key){
$featureArrayNew[$key] = $featurearray[$key];
}


// Initialize the array returned to the user
// Naming Convention
// Level Name: B-[task number]-[profile number]-[attribute number]
// Attribute Name: B-[task number]-[attribute number]
// Example: B-1-3-2, Returns the level corresponding to Task 1, Profile 3, Attribute 2
// B-3-3, Returns the attribute name corresponding to Task 3, Attribute 3

$returnarray = array();

// For each task $p
for($p = 1; $p <= $K; $p++){
weighted_randomize_race();
$type = weighted_randomize_race();

// Repeat until non-restricted profile generated
$complete = False;

while ($complete == False){

// For each profile $i
for($i = 1; $i <= $N; $i++){


// Create a count for $attributes to be incremented in the next loop
$attr = 0;

// Create a dictionary to hold profile's attributes
$profile_dict = array();

// For each attribute $attribute and level array $levels in task $p
foreach($featureArrayNew as $attribute => $levels){

// Increment attribute count
$attr = $attr + 1;

if ($attribute == "Race/ethnicity"){
$treat_number = $attr;
}

// Create key for attribute name
$attr_key = "B-" . (string)$p . "-" . (string)$attr;

// Store attribute name in $returnarray
$returnarray[$attr_key] = $attribute;

// Get length of $levels array
$num_levels = count($levels);

// Randomly select one of the level indices
if ($weighted == 1){
$level_index = weighted_randomize($probabilityarray, $attribute) - 1;

}else{
$level_index = mt_rand(1,$num_levels) - 1;
}

// Pull out the selected level
$chosen_level = $levels[$level_index];

// Store selected level in $profileDict
$profile_dict[$attribute] = $chosen_level;

// Create key for level in $returnarray
$level_key = "B-" . (string)$p . "-" . (string)$i . "-" . (string)$attr;

// Store selected level in $returnarray
$returnarray[$level_key] = $chosen_level;

}

}

$treat_profile_one = "B-" . (string)$p . "-1-" . (string)$treat_number;
$treat_profile_two = "B-" . (string)$p . "-2-" . (string)$treat_number;
$cond1 = $returnarray[$treat_profile_one] == "White" && $returnarray[$treat_profile_two] == $type;
$cond2 = $returnarray[$treat_profile_two] == "White" && $returnarray[$treat_profile_one] == $type;

if ($cond1 or $cond2){
$complete = True;
}

}
}


// Return the array back to Qualtrics
print json_encode($returnarray);
?>
46 changes: 39 additions & 7 deletions vignettes/01-setup.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,64 @@ vignette: >
%\VignetteEncoding{UTF-8}
---

We are working on developing our own tools for building a conjoint survey in Qualtrics. In the meantime, we recommend using Anton Strezhnev's **Conjoint Survey Design Tool** (Link: [conjointSDT](https://github.com/astrezhnev/conjointsdt/)). Strezhnev's guide to designing and implementing conjoint surveys in Qualtrics is excellent and we will not reiterate it here. But to summarize: researchers should first design their conjoint study in ConjointSDT and produce a JavaScript file, which is then inputted into the first screen of your Qualtrics survey, using the Edit Question Javascript functionality. See the following screenshot as an example:
<span style="color:red">We are working on developing our own tools for building a conjoint survey in Qualtrics. In the meantime, we recommend using Anton Strezhnev's **Conjoint Survey Design Tool** (Link: [conjointSDT](https://github.com/astrezhnev/conjointsdt/)).</span>

![](../man/figures/embedded_js.png){#id .class width=80% height=80%}
### 1.1 Generate a JavaScript or PHP randomizer

Strezhnev's guide to designing and implementing conjoint surveys in Qualtrics is excellent and we will not reiterate it here. But to summarize: researchers should first use ConjointSDT to produce a JavaScript or PHP randomizer.

#### JavaScript

The JavaScript randomizer can be inputted into the first screen of your Qualtrics survey, using the Edit Question Javascript functionality. See the following screenshot as an example:

![](../man/figures/embedded_js.png){#id .class width=80% height=80%}
The above Javascript is available from [here](https://raw.githubusercontent.com/yhoriuchi/projoint/master/data-raw/example.js).

The JavaScript produced by ConjointSDT will run internally within Qualtrics and automatically creates the values for *embedded fields*. These are the attributes and levels that constitute the profile pairs of each conjoint task. For example, embedded field "K-1-1-7" contains the value (level) of the seventh attribute for the first profile (of two) in the first task, and "K-5-2-5" contains the level of the fifth attribute for the second profile (of two) in the fifth task.

What you should do is to design, for each task, a table using HTML and insert these embedded fiels. The following is the screenshot of the first task in our example.
#### PHP

The PHP randomizer must be hosted by a server. The following is an example hosted at our server: https://www.horiuchi.org/php/ACHR_Modified_2.php. (This PHP file is available from [here](https://raw.githubusercontent.com/yhoriuchi/projoint/master/data-raw/ACHR_Modified_2.php).) This is the PHP randomizer used by [Agadjanian, Carey, Horiuchi, and Ryan (2023)](https://www.nowpublishers.com/article/Details/QJPS-21119).

### 1.2 Modify your Javascript or PHP randomizer

To add some additional constraints, such as removing ties between two profiles, you need to revise your PHP scripts or Javascript manually. In the future release of this package, we will add additional feature to make this step easier. For now, we recommend you use other resources, including [Open AI's GPT-4](https://openai.com/gpt-4)), to learn how to modify your code. The above PHP example has the following manually added component at the end so that the race of one profile (candidate) is always "White" and another is always "Black," "Asian," or "Hispanic."

```{r, eval=FALSE}
$treat_profile_one = "B-" . (string)$p . "-1-" . (string)$treat_number;
$treat_profile_two = "B-" . (string)$p . "-2-" . (string)$treat_number;
$cond1 = $returnarray[$treat_profile_one] == "White" && $returnarray[$treat_profile_two] == $type;
$cond2 = $returnarray[$treat_profile_two] == "White" && $returnarray[$treat_profile_one] == $type;
if ($cond1 or $cond2){$complete = True;}
```

If you have some other examples of adding additional constraints, please send an email to the package maintainer, [Yusaku Horiuchi](mailto:[email protected]) Thank you!

### 1.3 Add conjoint tables with embedded fields in Qualtrics

After you set up your JavaScript or PHP randomizer, the next step is to design, for each task, a table using HTML and insert these embedded fiels. The following is the screenshot of the first task in our example.

![](../man/figures/screenshot_first.png){#id .class width=80% height=80%}

The complete HTML for the first conjoint task in this survey is available from [here](https://raw.githubusercontent.com/yhoriuchi/projoint/master/data-raw/task_first.html).

A typical conjoint study will include five to ten tasks. The number of questions with such HTML pages should correspond to the number of tasks. The embedded fields in each task are different: The first digit after the "K" will increment from 1 to 10 as the tasks progress.
A typical conjoint study will include 5-10 tasks. The number of questions with such HTML pages should correspond to the number of tasks. The embedded fields in each task are different: The first digit after the "K" will increment from 1 to 10 as the tasks progress.

#### Adding a repeated task (recommended!)

If we wish to implement *a repeated task*, then, all we need to do is copy the task to be repeated (say, the first task) to a later point in the survey (say, after the fifth task). We recommend that researchers *flip the order* of the two profiles as well. Therefore, Profile 1 in the original task becomes Profile 2 in the new task. To do this, we would simply swap the middle digit of the embedded fields as below.

The repeated task will then look like the following:
![](../man/figures/screenshot_last.png){#id .class width=80% height=80%}

The complete HTML for the first conjoint task in this survey is available from [here](https://raw.githubusercontent.com/yhoriuchi/projoint/master/data-raw/task_repeated.html).
The complete HTML for the repeated conjoint task in this survey is available from [here](https://raw.githubusercontent.com/yhoriuchi/projoint/master/data-raw/task_repeated.html).

With the repeated task, researchers can use to measure intra-respondent reliability (IRR).
Importantly,with the repeated task, researchers can use to measure intra-respondent reliability (IRR).

For more information about [conjointSDT](https://github.com/astrezhnev/conjointsdt/), see Strezhnev's detailed instructions.
### 1.4 A sample Qualtrics survey

If you want to copy the Qualtric survey that we designed for our exmaple, download the QSF file below and import it to your Qualtrics account. You can then use it as a template to design your own conjoint survey experiment!

Expand Down
4 changes: 2 additions & 2 deletions vignettes/02-wrangle.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,13 @@ You can find this data set on GitHub: [labels_original.csv](https://github.com/y
The figure based on the original order and labels is in the alphabetical order:
```{r, fig.height=8, fig.width=8}
mm <- projoint(out1, .estimand = "mm")
plot(mm, .estimand = "mm")
plot(mm)
```

The labels and order of all attribute-levels in the second figure is the same as Figure 2 in [Mummolo and Nall (2017)](https://doi.org/10.1086/687569).
```{r, fig.height=8, fig.width=8}
mm <- projoint(out1_arranged, .estimand = "mm")
plot(mm, .estimand = "mm")
plot(mm)
```


Expand Down
25 changes: 6 additions & 19 deletions vignettes/03-predict.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,14 @@ When there is a repeated task, it is easy to estimate intra-respondent reliabili
library(projoint)
```

### 3.2 Read and wrangle data without the repeated tasks
### 3.2 Predict IRR based on the extrapolation method

As before, we start by reshaping our data and setting `.repeated` to `FALSE`:
As before, you should start by reading your Qualtrics file and reshaping it using `reshape_projiont()`. See [2.2 Read and wrangle data, with the flipped repeated tasks](https://yhoriuchi.github.io/projoint/articles/02-wrangle.html#read-and-wrangle-data). But we skip this step in this instruction and we use the already wrangled data named "out1_arranged".

We pass this data set to the `predict_tau` function, which both calculates IRR and produces a figure showing the extrapolation method visually. (see [2.3 Arrange the order and labels of attributes and levels](https://yhoriuchi.github.io/projoint/articles/02-wrangle.html#arrange-the-order-and-labels-of-attributes-and-levels})).
With the flipped repeated tasks
```{r}
outcomes <- paste0("choice", seq(from = 1, to = 8, by = 1))
out3 <- reshape_projoint(.dataframe = exampleData3,
.idvar = "ResponseId",
.outcomes = outcomes,
.outcomes_ids = c("A", "B"),
.alphabet = "K",
.repeated = FALSE,
.flipped = NULL)
```

### 3.3 Predict IRR based on the extrapolation method

Then, we pass this data set to the `predict_tau` function, which both calculates IRR and produces a figure showing the extrapolation method visually.

```{r}
predicted_irr <- predict_tau(out3)
predicted_irr <- predict_tau(out1_arranged)
```

A `projoint_tau` object, created by `predict_tau`, can be explored using the usual tools. The `print` method explains that this estimate of tau was produced via extrapolation rather than assumed or calculated using a repeated task and presents that estimate:
Expand All @@ -54,6 +41,6 @@ summary(predicted_irr)

And the `plot` method renders a plot showing the extrapolated value of tau:

```{r}
```{r, fig.height=4, fig.width=8}
plot(predicted_irr)
```
3 changes: 1 addition & 2 deletions vignettes/04-estimate.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ vignette: >
%\VignetteEncoding{UTF-8}
---

Due to IRR-induced measurement error, default MMs and AMCEs in conjoint analysis are biased. By default, `projoint` produces bias-corrected estimates automatically.

Due to IRR-induced measurement error, default MMs and AMCEs in conjoint analysis are biased. By default, `projoint` produces bias-corrected estimates automatically.

### 4.1 Load the projoint package and set up the data

Expand Down

0 comments on commit 82bc3f6

Please sign in to comment.