Qualtrics
About
This page documents some work related to using Qualtrics.
Setup
The {qualtRics} package provides useful commands for interacting with the Qualtrics API.
The package should install as part of the set of package dependencies managed by {renv}. But you can install it directly via the R console: install.packages("qualtRics").
This site provides a helpful introduction to the package: https://cran.r-project.org/web/packages/qualtRics/vignettes/qualtRics.html
First, we load the package and store our survey credentials.
If the command runs properly, you should see the following message in your console:
You don’t want your Qualtrics API key to get out into the wild. That’s why I don’t show mine in this example. So, do the following:
- Run the above commands in an R, RStudio, or Positron R console.
- Do not put save these credentials in a file that you check-in to version control like Git.
- Do not share API keys. Have each person who needs access to your survey generate their own API key.
You’ll then need to restart R for the securly stored credentials to be available to {qualtRics}. You may also wish to run library(qualtRics) so that the package commands can be called by their short names.
Viewing surveys
To view the surveys available to you1, run the following:
# A tibble: 8 × 5
name ownerId lastModified creationDate isActive
<chr> <chr> <chr> <chr> <lgl>
1 bootcamp-2026 UR_8rd… 2026-05-01T… 2026-05-01T… TRUE
2 Sex difference survey REV 2019-11-… UR_8rd… 2021-08-26T… 2019-11-18T… TRUE
3 Sex difference survey REV 2019-11-… UR_8rd… 2022-04-19T… 2019-11-11T… TRUE
4 Sex differences survey REV 2019-11… UR_8rd… 2019-11-04T… 2019-11-04T… TRUE
5 Sex differences survey OLD UR_8rd… 2019-11-04T… 2019-10-29T… TRUE
6 Sex difference survey UR_8rd… 2019-10-29T… 2019-10-24T… TRUE
7 Individual differences in visual p… UR_8rd… 2019-10-24T… 2019-10-24T… FALSE
8 peep-II-test UR_8rd… 2015-09-17T… 2015-09-17T… FALSE
You’ll see that the bootcamp-2026 survey is the first one in the list.
My Qualtrics API key gives permission to see all of the surveys I have access to.
This is why you don’t want to share your personal API key.
Since this page will be open to the public, I also dropped the id column from the list above.
The Qualtrics API is probably robust to efforts from outsiders to leverage access using the id column from a specific survey, but I didn’t want to take that chance.
Note also that if the data were being downloaded to a file, and the data contained any sensitive information, I’d add the data file directory to my .gitignore file so that it didn’t get synched to GitHub.
To retrieve this survey, we run the following:
The bootcamp_survey variable is a rectangular data structure called a tibble. Here are the variable names in it:
Here are the questions:
# A tibble: 2 × 4
qid qname question force_resp
<chr> <chr> <chr> <lgl>
1 QID1 Q1 How tall are you in spans (1 span = distance from tip … FALSE
2 QID2 Q2 How enthusiastic are you about the following? FALSE
Let’s retrieve a copy of the results.
|
| | 0%
|
|======================================================================| 100%
[1] FALSE
To view the structure of the answers variable we use the str() function.
tibble [1 × 23] (S3: tbl_df/tbl/data.frame)
$ StartDate : POSIXct[1:1], format: "2026-05-01 08:31:17"
$ EndDate : POSIXct[1:1], format: "2026-05-01 08:31:49"
$ Status : chr "IP Address"
..- attr(*, "label")= Named chr "Response Type"
.. ..- attr(*, "names")= chr "Status"
$ IPAddress : chr "165.85.38.17"
..- attr(*, "label")= Named chr "IP Address"
.. ..- attr(*, "names")= chr "IPAddress"
$ Progress : num 100
..- attr(*, "label")= Named chr "Progress"
.. ..- attr(*, "names")= chr "Progress"
$ Duration (in seconds): num 31
..- attr(*, "label")= Named chr "Duration (in seconds)"
.. ..- attr(*, "names")= chr "Duration (in seconds)"
$ Finished : logi TRUE
..- attr(*, "label")= Named chr "Finished"
.. ..- attr(*, "names")= chr "Finished"
$ RecordedDate : POSIXct[1:1], format: "2026-05-01 08:31:49"
$ ResponseId : chr "R_5WrEY7SSM6Ruo2l"
..- attr(*, "label")= Named chr "Response ID"
.. ..- attr(*, "names")= chr "ResponseId"
$ RecipientLastName : logi NA
..- attr(*, "label")= Named chr "Recipient Last Name"
.. ..- attr(*, "names")= chr "RecipientLastName"
$ RecipientFirstName : logi NA
..- attr(*, "label")= Named chr "Recipient First Name"
.. ..- attr(*, "names")= chr "RecipientFirstName"
$ RecipientEmail : logi NA
..- attr(*, "label")= Named chr "Recipient Email"
.. ..- attr(*, "names")= chr "RecipientEmail"
$ ExternalReference : logi NA
..- attr(*, "label")= Named chr "External Data Reference"
.. ..- attr(*, "names")= chr "ExternalReference"
$ LocationLatitude : num 39
..- attr(*, "label")= Named chr "Location Latitude"
.. ..- attr(*, "names")= chr "LocationLatitude"
$ LocationLongitude : num -77.5
..- attr(*, "label")= Named chr "Location Longitude"
.. ..- attr(*, "names")= chr "LocationLongitude"
$ DistributionChannel : chr "anonymous"
..- attr(*, "label")= Named chr "Distribution Channel"
.. ..- attr(*, "names")= chr "DistributionChannel"
$ UserLanguage : chr "EN"
..- attr(*, "label")= Named chr "User Language"
.. ..- attr(*, "names")= chr "UserLanguage"
$ Q1 : Ord.factor w/ 1 level "Click to write Choice 1": 1
..- attr(*, "label")= Named chr "How tall are you in spans (1 span = distance from tip of little finger to tip of thumb)? - Selected Choice"
.. ..- attr(*, "names")= chr "Q1"
$ Q1_1_TEXT : num 35
..- attr(*, "label")= Named chr "How tall are you in spans (1 span = distance from tip of little finger to tip of thumb)? - Click to write Choice 1 - Text"
.. ..- attr(*, "names")= chr "Q1_1_TEXT"
$ Q2_1 : num 40
..- attr(*, "label")= Named chr "How enthusiastic are you about the following? - Curling (sport)"
.. ..- attr(*, "names")= chr "Q2_1"
$ Q2_2 : num 100
..- attr(*, "label")= Named chr "How enthusiastic are you about the following? - Banjo music"
.. ..- attr(*, "names")= chr "Q2_2"
$ Q2_3 : num 100
..- attr(*, "label")= Named chr "How enthusiastic are you about the following? - Stargazing"
.. ..- attr(*, "names")= chr "Q2_3"
$ Q2_4 : num 34
..- attr(*, "label")= Named chr "How enthusiastic are you about the following? - Hex stickers"
.. ..- attr(*, "names")= chr "Q2_4"
- attr(*, "column_map")= tibble [23 × 7] (S3: tbl_df/tbl/data.frame)
..$ qname : chr [1:23] "StartDate" "EndDate" "Status" "IPAddress" ...
..$ description: chr [1:23] "Start Date" "End Date" "Response Type" "IP Address" ...
..$ main : chr [1:23] "Start Date" "End Date" "Response Type" "IP Address" ...
..$ sub : chr [1:23] "" "" "" "" ...
..$ ImportId : chr [1:23] "startDate" "endDate" "status" "ipAddress" ...
..$ timeZone : chr [1:23] "America/New_York" "America/New_York" NA NA ...
..$ choiceId : logi [1:23] NA NA NA NA NA NA ...
There’s a lot of metadata to unpack and understand. But for our purposes here, we want a more human readable, and ‘tidier’ format.
| qname | description | main | sub | ImportId | timeZone | choiceId |
|---|---|---|---|---|---|---|
| StartDate | Start Date | Start Date | startDate | America/New_York | NA | |
| EndDate | End Date | End Date | endDate | America/New_York | NA | |
| Status | Response Type | Response Type | status | NA | NA | |
| IPAddress | IP Address | IP Address | ipAddress | NA | NA | |
| Progress | Progress | Progress | progress | NA | NA | |
| Duration (in seconds) | Duration (in seconds) | Duration (in seconds) | duration | NA | NA | |
| Finished | Finished | Finished | finished | NA | NA | |
| RecordedDate | Recorded Date | Recorded Date | recordedDate | America/New_York | NA | |
| ResponseId | Response ID | Response ID | _recordId | NA | NA | |
| RecipientLastName | Recipient Last Name | Recipient Last Name | recipientLastName | NA | NA | |
| RecipientFirstName | Recipient First Name | Recipient First Name | recipientFirstName | NA | NA | |
| RecipientEmail | Recipient Email | Recipient Email | recipientEmail | NA | NA | |
| ExternalReference | External Data Reference | External Data Reference | externalDataReference | NA | NA | |
| LocationLatitude | Location Latitude | Location Latitude | locationLatitude | NA | NA | |
| LocationLongitude | Location Longitude | Location Longitude | locationLongitude | NA | NA | |
| DistributionChannel | Distribution Channel | Distribution Channel | distributionChannel | NA | NA | |
| UserLanguage | User Language | User Language | userLanguage | NA | NA | |
| Q1 | How tall are you in spans (1 span = distance from tip of little finger to tip of thumb)? - Selected Choice | How tall are you in spans (1 span = distance from tip of little finger to tip of thumb)? | Selected Choice | QID1 | NA | NA |
| Q1_1_TEXT | How tall are you in spans (1 span = distance from tip of little finger to tip of thumb)? - Click to write Choice 1 - Text | How tall are you in spans (1 span = distance from tip of little finger to tip of thumb)? | Click to write Choice 1 - Text | QID1_1_TEXT | NA | NA |
| Q2_1 | How enthusiastic are you about the following? - Curling (sport) | How enthusiastic are you about the following? | Curling (sport) | QID2_1 | NA | NA |
| Q2_2 | How enthusiastic are you about the following? - Banjo music | How enthusiastic are you about the following? | Banjo music | QID2_2 | NA | NA |
| Q2_3 | How enthusiastic are you about the following? - Stargazing | How enthusiastic are you about the following? | Stargazing | QID2_3 | NA | NA |
| Q2_4 | How enthusiastic are you about the following? - Hex stickers | How enthusiastic are you about the following? | Hex stickers | QID2_4 | NA | NA |
Let’s make a simpler dataset with just the answers to our questions.
There are always tradeoffs. Always. Here, rather than grab the questions from the still-unfamiliar-to-me {qualtRics} package using code, I grabbed and renamed the files by hand.
If you think that you will need to run your code more than once, it’s often worth taking time to do something the more elegant and robust way. That usually means writing a function and accessing data that’s already in a structured form. The output from the qualtRics::fetch_survey() function is well-structed. I just ran out of time to learn enough about it for the purposes of this demo.
If you use Qualtrics surveys frequently and want to use R for scripted data processing, then you should definitely dig in and figure out the better way.
Saving cleaned copy
Visualizing
Here are some extremely simple visualizations of these data.
I typically use the package::function() syntax. This helps me remember which functions are from which packages, and eliminates conflicts between functions in different packages that have the same name.
On the other hand, I usually “source” or bring into memory using the library() command the {ggplot2} package. The typical ggplot figure involves so many function calls and involves the atypical-for-R + syntax that I find this easier to write and to read.
As my children will tell you we really said to them at one point, “Make good choices.”
Footnotes
In descending order by the date the survey was created.↩︎