-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
290 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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! | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters