Skip to contents

The Personality Inventory for DSM-5 (PID-5) instrument has 220 items and yields 25 facet scales, 5 domain scales, and 5 validity scales. We can demonstrate the package’s functionality using some simulated data.

First, we load the package into memory using the library() function. If this doesn’t work, make sure you installed the package properly (see the README on GitHub).

Score simulated PID-5 data

The sim_pid5 dataset is built into the package and can be loaded using the data() function. It contains 100 rows (each representing a simulated participant) and 220 columns named pid_1 to pid_220 (each representing an item from the PID-5).

data("sim_pid5")
sim_pid5
#> # A tibble: 100 × 220
#>    pid_1 pid_2 pid_3 pid_4 pid_5 pid_6 pid_7 pid_8 pid_9 pid_10 pid_11 pid_12
#>    <int> <int> <int> <int> <int> <int> <int> <int> <int>  <int>  <int>  <int>
#>  1     0     3     2     1     1     3     1     3     3      0      0      3
#>  2     3     3     0     3     0     3     2     2     1      2      0      0
#>  3     3     2     3     2     3     3     0     3     3      2      3      3
#>  4     1     3     0     2     1     0     2     0     3      2      3      2
#>  5     0     1     3     2     3     1     0     1     2      2      2      2
#>  6     2     1     1     3     3     2     2     0     1      3      1      3
#>  7     1     1     3     3     1     3     1     0     1      1      0      2
#>  8     2     0     3     0     3     2     0     1     3      1      2      0
#>  9     1     1     3     0     1     1     2     3     1      1      3      1
#> 10     0     3     2     3     3     0     1     2     1      3      0      2
#> # ℹ 90 more rows
#> # ℹ 208 more variables: pid_13 <int>, pid_14 <int>, pid_15 <int>, pid_16 <int>,
#> #   pid_17 <int>, pid_18 <int>, pid_19 <int>, pid_20 <int>, pid_21 <int>,
#> #   pid_22 <int>, pid_23 <int>, pid_24 <int>, pid_25 <int>, pid_26 <int>,
#> #   pid_27 <int>, pid_28 <int>, pid_29 <int>, pid_30 <int>, pid_31 <int>,
#> #   pid_32 <int>, pid_33 <int>, pid_34 <int>, pid_35 <int>, pid_36 <int>,
#> #   pid_37 <int>, pid_38 <int>, pid_39 <int>, pid_40 <int>, pid_41 <int>, …

To turn these item-level data into scale scores on the 25 facets and 5 domains, we can use the score_pid5() function. We will need to tell the function which columns contain our items and which version of the PID this is. There are several ways we can specify the items. First, we can provide the column numbers and use the : shortcut. In this tibble, the items are from column 1 to column 220 so we can use items = 1:220. I am going to also set append = FALSE so that you can quickly see the scale scores. I also can set the version to "FULL" (or leave that argument off, as that is the default, shown in the example below) to let it know we are using the full 220-item version.

scores <- score_pid5(sim_pid5, items = 1:220, version = "FULL", append = FALSE)
scores
#> # A tibble: 100 × 30
#>    pid_anhedonia pid_suspiciousness pid_riskTaking pid_impulsivity
#>            <dbl>              <dbl>          <dbl>           <dbl>
#>  1          1.25               1.71           1.36           2.33 
#>  2          1.38               1.57           1.43           2    
#>  3          1.88               1              1.29           1.83 
#>  4          1.25               2.43           1.21           1.5  
#>  5          1.12               1.57           1.64           2.5  
#>  6          2.12               1              1.79           1.83 
#>  7          1.38               1.14           1.86           1.17 
#>  8          1.5                1.71           1.86           0.667
#>  9          1.12               1.14           1.86           1.67 
#> 10          1.38               1.86           2.07           2    
#> # ℹ 90 more rows
#> # ℹ 26 more variables: pid_eccentricity <dbl>, pid_distractibility <dbl>,
#> #   pid_restrictedAffectivity <dbl>, pid_submissiveness <dbl>,
#> #   pid_withdrawal <dbl>, pid_callousness <dbl>,
#> #   pid_separationInsecurity <dbl>, pid_attentionSeeking <dbl>,
#> #   pid_emotionalLability <dbl>, pid_depressivity <dbl>, pid_hostility <dbl>,
#> #   pid_irresponsibility <dbl>, pid_rigidPerfectionism <dbl>, …

If I had instead set append = TRUE (or left it off, as that is the default), we would get back the sim_pid5 tibble with the scale scores added to the end as extra columns. Notice below how we now have 250 columns instead of 220 or 30.

scores <- score_pid5(sim_pid5, items = 1:220)
scores
#> # A tibble: 100 × 250
#>    pid_1 pid_2 pid_3 pid_4 pid_5 pid_6 pid_7 pid_8 pid_9 pid_10 pid_11 pid_12
#>    <int> <int> <int> <int> <int> <int> <int> <int> <int>  <int>  <int>  <int>
#>  1     0     3     2     1     1     3     1     3     3      0      0      3
#>  2     3     3     0     3     0     3     2     2     1      2      0      0
#>  3     3     2     3     2     3     3     0     3     3      2      3      3
#>  4     1     3     0     2     1     0     2     0     3      2      3      2
#>  5     0     1     3     2     3     1     0     1     2      2      2      2
#>  6     2     1     1     3     3     2     2     0     1      3      1      3
#>  7     1     1     3     3     1     3     1     0     1      1      0      2
#>  8     2     0     3     0     3     2     0     1     3      1      2      0
#>  9     1     1     3     0     1     1     2     3     1      1      3      1
#> 10     0     3     2     3     3     0     1     2     1      3      0      2
#> # ℹ 90 more rows
#> # ℹ 238 more variables: pid_13 <int>, pid_14 <int>, pid_15 <int>, pid_16 <int>,
#> #   pid_17 <int>, pid_18 <int>, pid_19 <int>, pid_20 <int>, pid_21 <int>,
#> #   pid_22 <int>, pid_23 <int>, pid_24 <int>, pid_25 <int>, pid_26 <int>,
#> #   pid_27 <int>, pid_28 <int>, pid_29 <int>, pid_30 <int>, pid_31 <int>,
#> #   pid_32 <int>, pid_33 <int>, pid_34 <int>, pid_35 <int>, pid_36 <int>,
#> #   pid_37 <int>, pid_38 <int>, pid_39 <int>, pid_40 <int>, pid_41 <int>, …

Alternatively, we could provide the item column names as a character string. Typing out all 220 item names would be a hassle, but luckily this data named them consistently so we can build the names automatically using sprintf(). If we use the “pid_%d” format and apply that across the numbers 1 to 220, that will create the column names we need.

scores <- score_pid5(sim_pid5, items = sprintf("pid_%d", 1:220))
scores
#> # A tibble: 100 × 250
#>    pid_1 pid_2 pid_3 pid_4 pid_5 pid_6 pid_7 pid_8 pid_9 pid_10 pid_11 pid_12
#>    <int> <int> <int> <int> <int> <int> <int> <int> <int>  <int>  <int>  <int>
#>  1     0     3     2     1     1     3     1     3     3      0      0      3
#>  2     3     3     0     3     0     3     2     2     1      2      0      0
#>  3     3     2     3     2     3     3     0     3     3      2      3      3
#>  4     1     3     0     2     1     0     2     0     3      2      3      2
#>  5     0     1     3     2     3     1     0     1     2      2      2      2
#>  6     2     1     1     3     3     2     2     0     1      3      1      3
#>  7     1     1     3     3     1     3     1     0     1      1      0      2
#>  8     2     0     3     0     3     2     0     1     3      1      2      0
#>  9     1     1     3     0     1     1     2     3     1      1      3      1
#> 10     0     3     2     3     3     0     1     2     1      3      0      2
#> # ℹ 90 more rows
#> # ℹ 238 more variables: pid_13 <int>, pid_14 <int>, pid_15 <int>, pid_16 <int>,
#> #   pid_17 <int>, pid_18 <int>, pid_19 <int>, pid_20 <int>, pid_21 <int>,
#> #   pid_22 <int>, pid_23 <int>, pid_24 <int>, pid_25 <int>, pid_26 <int>,
#> #   pid_27 <int>, pid_28 <int>, pid_29 <int>, pid_30 <int>, pid_31 <int>,
#> #   pid_32 <int>, pid_33 <int>, pid_34 <int>, pid_35 <int>, pid_36 <int>,
#> #   pid_37 <int>, pid_38 <int>, pid_39 <int>, pid_40 <int>, pid_41 <int>, …

There are other useful arguments to the function that you can read about using its documentation by typing the following into your R console: ?score_pid5 or through the package website.

Scale Reliability

As we compute scale scores, we can also estimate their inter-item reliability using Cronbach’s α (alpha) or McDonald’s ω (omega total). α is fast and widely used, but it assumes tau-equivalence (all items load equally on a single factor); violations can make α under- or over-estimate reliability. ω is based on a congeneric single-factor model, allowing items to have different loadings and error variances; it typically provides a more accurate reliability estimate for unit-weighted sums. Both assume the scale is essentially unidimensional; α and ω coincide when tau-equivalence holds.

We estimate reliability with the reliability_pid5() function, which returns a tibble with one row per scale and columns for the number of items and the requested coefficients. By default it computes both alpha and omega; for the latter, we will need the lavaan package installed (set omega = FALSE to skip it). Note that, because this is naively simulated data, we would expect the reliability in this example to be poor.

reliability_pid5(
  data = sim_pid5,
  items = sprintf("pid_%d", 1:220),
  version = "FULL"
)
#> # A tibble: 25 × 4
#>    scale                  nItems   alpha    omega
#>    <chr>                   <int>   <dbl>    <dbl>
#>  1 Anhedonia                   8 -0.211  NA      
#>  2 Suspiciousness              7 -0.211   0.0411 
#>  3 Risk Taking                14 -0.0128  0.00697
#>  4 Impulsivity                 6  0.141  NA      
#>  5 Eccentricity               13  0.0554  0.152  
#>  6 Distractibility             9 -0.154   0.00807
#>  7 Restricted Affectivity      7 -0.144  NA      
#>  8 Submissiveness              4 -0.194   0.116  
#>  9 Withdrawal                 10 -0.0290 NA      
#> 10 Callousness                14  0.0147  0.0279 
#> # ℹ 15 more rows

Validity Scales for the PID-5

There are also several validity scales that have been developed for the full PID-5, including measures of overreporting, inconsistent responding, and positive impression management. We can use the simulated data to demonstrate the ability of the validity_pid5() function to calculate these scores and flag issues. The function arguments will be consistent with what we just learned. Note that, because the data is fake, we would expect there to be lots of validity issues.

validity_pid5(sim_pid5, items = 1:220, append = FALSE)
#> ! A total of 99 observations (99.0%) met criteria for inconsistent responding on the INC (0 missing).
#>  Consider removing them with `dplyr::filter(df, pid_INC < 17)`
#> ! A total of 53 observations (53.0%) met criteria for overreporting on the ORS (0 missing).
#>  Consider removing them with `dplyr::filter(df, pid_ORS < 3)`
#> ! A total of 92 observations (92.0%) met criteria for defensiveness on the SDTD (0 missing).
#>  Consider removing them with `dplyr::filter(df, pid_SDTD < 19)`
#> # A tibble: 100 × 5
#>    pid_PNA pid_INC pid_ORS pid_PRD pid_SDTD
#>      <dbl>   <dbl>   <dbl>   <dbl>    <dbl>
#>  1       0      25       2      40       26
#>  2       0      18       2      34       31
#>  3       0      32       2      34       29
#>  4       0      29       3      34       31
#>  5       0      24       0      36       17
#>  6       0      23       2      35       36
#>  7       0      42       2      31       19
#>  8       0      17       1      28       21
#>  9       0      25       4      41       24
#> 10       0      30       5      31       29
#> # ℹ 90 more rows

Normative Scores

The pid_norms dataset carries the normative score distributions published by Markon et al. (2024). The norm_pid5() function looks scored columns up in those tables and returns, for each one, the T score and percentile printed against the nearest tabled raw score. It converts scores rather than computing them, so we hand it the output of score_pid5() — and of validity_pid5(), if we want those scales converted too.

scored <- score_pid5(sim_pid5, items = 1:220)
scored <- validity_pid5(scored, items = 1:220)

For the full form the published tables cover the five domain scales, all 25 facet scales, and three of the validity scales — INC, ORS, and PRD. They do not cover SD-TD at all. Each converted scale gains a _ptl column, and those whose tables print T scores also gain a _t column. The validity scales are distributed as percentiles only, so they get no _t column.

norm_pid5(
  scored,
  scores = paste0(
    "pid_",
    c("negativeAffectivity", "detachment", "antagonism", "disinhibition",
      "psychoticism", "INC", "ORS", "PRD")
  ),
  version = "FULL",
  append = FALSE
)
#> Warning: ! 0 observations below and 63 above the printed range were capped to the
#>   nearest printed row.
#>  A capped score's T and percentile are the end row's printed values, not an
#>   extrapolation.
#> # A tibble: 100 × 13
#>    pid_negativeAffectivity_t pid_negativeAffectivity_ptl pid_detachment_t
#>                        <int>                       <dbl>            <int>
#>  1                        60                        0.84               56
#>  2                        54                        0.71               59
#>  3                        58                        0.81               68
#>  4                        59                        0.82               67
#>  5                        57                        0.79               63
#>  6                        64                        0.89               66
#>  7                        61                        0.85               59
#>  8                        58                        0.81               65
#>  9                        62                        0.87               59
#> 10                        57                        0.79               70
#> # ℹ 90 more rows
#> # ℹ 10 more variables: pid_detachment_ptl <dbl>, pid_antagonism_t <int>,
#> #   pid_antagonism_ptl <dbl>, pid_disinhibition_t <int>,
#> #   pid_disinhibition_ptl <dbl>, pid_psychoticism_t <int>,
#> #   pid_psychoticism_ptl <dbl>, pid_INC_ptl <dbl>, pid_ORS_ptl <dbl>,
#> #   pid_PRD_ptl <dbl>

The facets convert the same way. Anhedonia, for instance, is an item mean over its eight full-form items, and its table runs from a floor of 0.00 up to a printed 3.84 — past the 3.00 an item mean can actually reach, which is true of most of the facet columns and is a property of the published tables rather than of the conversion:

norm_pid5(scored, scores = "pid_anhedonia", version = "FULL", append = FALSE)
#> # A tibble: 100 × 2
#>    pid_anhedonia_t pid_anhedonia_ptl
#>              <int>             <dbl>
#>  1              57              0.79
#>  2              59              0.8 
#>  3              67              0.93
#>  4              57              0.79
#>  5              55              0.76
#>  6              71              0.95
#>  7              59              0.8 
#>  8              61              0.84
#>  9              55              0.76
#> 10              59              0.8 
#> # ℹ 90 more rows

Every number returned is a cell of a published table: the nearest printed row is selected and nothing is interpolated. A score that falls outside a printed range is capped to the nearest end rather than extrapolated, and a warning reports how many observations that happened to — the PRD sum reaches 66 while its table stops at 55, so it is a common one to see. A scale the tables do not cover returns NA in both columns with a warning naming it. Every report this function makes is a warning, so a single suppressWarnings() call silences it.

If the items were answered on a four-option response scale that starts somewhere other than 0 — 1 to 4, say — pass that range as srange and each score is reconciled to the published 0–3 metric before it is looked up, with a warning naming which scales were adjusted and which were left alone. The per-scale formulas are given in ?norm_pid5.

Profile Plots

Once a respondent’s scores are normed, plot_pid5() draws them as a profile against the published metric. It takes one respondent — a profile plot shows one person — so we norm the whole dataset and hand it a single row.

domains <- paste0(
  "pid_",
  c("negativeAffectivity", "detachment", "antagonism", "disinhibition",
    "psychoticism")
)
normed <- norm_pid5(scored, scores = domains, version = "FULL")
plot_pid5(normed[1, ], version = "FULL")

The dashed line marks T = 50, the normative sample’s mean, and the score axis spans the range the published tables actually print for these scales — so the axis does not rescale from respondent to respondent and two profiles are directly comparable. Nothing on the plot says whether a score is high, low, or concerning: {hitop} presents scores against norms and leaves the interpreting to you.

Passing level = "facet" plots all 25 facets instead, grouped into a panel per domain. The APA key ties three facets to each domain; the remaining ten define no domain and are grouped separately rather than dropped.

facets <- paste0("pid_", pid_scales[["FULL"]]$camelCase)
normed_facets <- norm_pid5(scored, scores = facets, version = "FULL")
plot_pid5(normed_facets[1, ], version = "FULL", level = "facet")

Set metric = "percentile" for a percentile axis instead of T scores. norm_pid5() returns percentiles as a proportion; the plot multiplies them by 100 so the axis reads 0–100.

plot_pid5(normed[1, ], version = "FULL", metric = "percentile")

The result is an ordinary ggplot object, so you can restyle it with any ggplot2 layer — + ggplot2::labs(title = ...), a different theme, and so on.