Create Custom Pre/Post Frequency and Percent tables in SPSS

This tutorial outlines how to create a pre/post table using SPSS. For each variable, summary measures are shown over four rows. The first two rows show measures for pretest responses. The second two rows show measures for posttest responses. Each of these row pairs has a similar pattern: the first row shows counts; the second row displays percentages. By following the steps presented here, you’ll end up with concise tables fit for publication.

Prepare data for analysis

  1. Navigate to the IBM SPSS Statistics Data Editor
    • Click tab Variable View
  2. For each desired variable, change the Measure scale to Ordinal
  3. For each desired variable, make sure values under Values are exactly the same
    • To do this, copy one variable value and paste it into the values of the remaining variables
  4. Arrange your variables so that each pretest variable comes before each post variable

Open the Custom Tables window

  1. Click Analyze
    • Click Tables
      • Click Custom Tables…
      • The Custom Tables window will appear

Create custom table

  1. Navigate to the Custom Tables window
    • Add variables to table
      • Under Variables:, select desired variables
        • Drag selected variables to the Rows bar
          • A table object will appear in the Rows Columns area
    • Reshape custom table
      • Under the Category Position: dropdown menu, select Row Labels in Columns
      • Navigate to the Summary Statistics area (not the button)
        • Under the Position: dropdown menu, select Rows

Specify statistics

  1. Select all variables
  2. Under Define, click Summary Statistics…
    • The Summary Statistics: window will appear
  3. Under Display:, clear the default statistics
    • To clear a statistic, select it and click the left directed arrow
      • The statistic will no longer appear in the Display: area
      • The statistic will now appear in under the Statistics: area
  4. Under Statistics:, select desired statistics
    • Select Count
      • Under Statistics:, click Count then click the right directed arrow
        • The statistic will now appear in the Display: area
        • The statistic will no longer appear in under the Statistics: area
    • Select Row Valid N %
      • Under Statistics:, click Row Valid N %then click the right directed arrow
        • The statistic will now appear in the Display: area
        • The statistic will no longer appear in under the Statistics: area
    • Click Apply to Selection

Specify custom statistic

  1. Select all variables
  2. Under Define, click Categories and Totals…
    • The Categories and Totals window will appear
  3. Under Subtotals and Computed Categories, click Add Category…
    • The Define Computed Category window will appear
  4. Create a statistic
    • Next to Label for Computed Category:, type the word Total
    • Under Expression for Computed Category:, type in the expression [1] + [2] + [3] + [4] + [5]
    • Next to Hide categories used in expression from table, uncheck the box
    • Click Continue
  5. From the Display area, reposition the Total value
    • Under Label, select the Total
    • Click the upward directed arrow until Total is at the top of the list
  6. Click Apply

Produce table

  1. From the Custom Tables window, click OK

SPSS syntax to create table

* Custom Tables.
/PCOMPUTE &cat1 = EXPR([1] + [2] + [3] + [4] + [5])
/CATEGORIES VARIABLES=V1_pre V1_post [&cat1, 1, 2, 3, 4, 5, OTHERNM] EMPTY=INCLUDE.

Identifying cliques and clique comembers in R using the SNA package

The R package SNA provides a number of tools for analyzing social network data. This post reviews the function clique.census from the SNA package, and shows how it can be used to better understand the group structure among a list of network members.

Let’s start by creating a toy network. Say Henry, Sarah, Rick, Joe and Annie are all colleagues in a criminology department. Now imagine they’ve been asked to identify the most important people they collaborate with. The results of this fictitious effort are shown in the following edgelist.

edgelist <- matrix(c("Henry", "Sarah", "Henry", "Joe", "Sarah", "Joe", "Henry", "Sarah", "Henry", "Rick", "Sarah", "Rick", "Sarah", "Henry", "Sarah", "Rick", "Henry", "Rick", "Sarah", "Henry", "Sarah", "Joe", "Henry", "Joe", "Sarah", "Rick", "Sarah", "Annie", "Rick", "Annie"), ncol = 2)

Recall, a clique is a group where everyone in it “likes” everyone else. To identify cliques among our network of criminology researchers, we first transform it into a network object and then apply the SNA function clique.census.


net, enumerate=TRUE, clique.comembership="bysize") # Identify cliques


sna clique comembership 1

After applying the function clique.census, we see there were two cliques among our respondents, each involving three researchers.

To identify the comembers of these cliques, we inspect the contents of the variable net_cc$clique.comemb[3, , ].

To paraphrase the SNA documentation, the variable net_cc$clique.comemb is a three dimensional matrix of size max clique size x n x n. In this example we observed cliques involving only three network members each. As such, the 3-clique comembership information is stored in the variable net_cc$clique.comemb[3, , ]. (Note: if we observed one or more cliques with more than three members, say, a 4-clique, we could examine their comembership using the variable net_cc$clique.comemb[4, , ]).

Notably, the format by which net_cc$clique.comemb[3, , ] organizes clique comembership takes some getting used to. In fact, the main point of this post is to explain this organizational scheme in a more everyday kind of way.

Given our criminologist cliques, here’s how we find out who was in them. Recall, the largest clique we observed contained three individuals. Further recall that our network only contained five respondents. As such, the matrix net_cc$clique.comemb[3, , ] is of size 5 x 5. This matrix mimics the network structure itself. That is, network members are listed along the rows and, in the exact same order, listed again across the columns. The values within the matrix then identify researchers who were in cliques together.

sna clique comembership 2

Let’s go through a couple columns together to better understand what this matrix is telling us exactly.

The values in the column Joe show who Joe was in a clique with. Zero values indicate who Joe was not in a clique with, while values greater than zero indicate who he was in a clique with. More than just showing who Joe was in a clique with, these values identify the different cliques he was a part of. Reading the column from top to bottom, Joe and Annie were in zero cliques together. Next we see that Joe was in a clique with Henry. Notably, Joe was in one clique with himself (i.e., there was only one clique that involved Joe). The rest of the column values show that Joe was in zero cliques with Rick and one clique with Sarah. In total, this column tell us there was one 3-clique that involved Joe (see Joe’s value for Joe) and that this clique involved the researchers Henry and Sarah.

Let’s look at a trickier column. The values in the column for Henry ranged from 0 to 2. As with Joe, the values show who Henry was in a clique with and how many times they were in the same clique. Going down the column, we see a zero value for Henry and Annie. That is, Henry and Annie were not part of the same clique. Notably, Henry is in two cliques with himself. That is, there were two cliques, each of which involved Henry. The rest of the values show that Henry was in one clique with Joe, another clique with Rick and two cliques with Sarah.

Combined, these column values tell us there were two 3-cliques: one clique involving Henry, Sarah and Joe and another involving Henry, Sarah and Rick.

Omit outdated records after adding amended records

In the juvenile court judges often have the power to amend charges applied to a case. A charge of “INTENT TO DISTRIBUTE” may, for instance, be reduced to something less severe, say “POSSESSION OF PARAPHERNALIA.” When this occurs, a new charge is added to a case record and the old charge is, in some cases, retained. This short post presents R code to omit outdated charges after amended charges have been added.

Data for this example consist of charges applied to a single court record. These data are provided below:

charge=c("Count 1", "Count 2", "Amended", "Count 3", "Amended")
section=c("21-5807(a)(3)", "21-5807(a)(3)", "21-5801(b)(4)", "21-5807(a)(3)", "21-5801(b)(4)")
date=c("09/13/15", "09/20/15", "04/04/16", "10/03/15", "04/04/16")
acs=c("", "", "", "", "")
drug=c("", "", "", "", "")
pl=c("", "", "", "", "")
tp=c("F", "F", "M", "F", "M")
lvl=c("9", "9", "A", "9", "A")
pn=c("N", "N", "N", "N", "N")
sentence_date=c("", "", "04/04/2016", "", "04/04/2016")

df <- data.frame(charge, section, date, title, acs, drug, finding, tp, lvl, pn, sentence_date,stringsAsFactors=FALSE)

The goal here is to remove every charge that is followed by an amended charge. The following dataset illustrates the desired result:

charge=c("Count 1", "Amended", "Amended")
section=c("21-5807(a)(3)", "21-5801(b)(4)", "21-5801(b)(4)")
date=c("09/13/15", "04/04/16", "04/04/16")
acs=c("", "", "")
drug=c("", "", "")
pl=c("", "", "")
tp=c("F", "M", "M")
lvl=c("9", "A", "A")
pn=c("N", "N", "N")
sentence_date=c("", "04/04/2016", "04/04/2016")

df <- data.frame(charge, section, date, title, acs, drug, finding, tp, lvl, pn, sentence_date,stringsAsFactors=FALSE)

One way to achieve this new dataset is to identify all charges that are followed by an amended charge and extract all charges except these original charges.

First, the positions of the updated charges are identified. The following expression identifies the updated charges as TRUE and all other charges as FALSE.


Second, the positions of the updated charges are used to identify the positions of the original charges that were amended. This is accomplished by recognizing a pattern in the charges themselves. Updated charges in this dataset are always preceded by their original charge. Since the updated charges were identified as TRUE (and the non-updated charges identified as FALSE), shifting these values by one position identifies the original charges.

c(c(df$charge=="Amended")[-1], c(df$charge=="Amended")[1])

Finally, the positions of the original charges (i.e., the charges followed by the amended ones) are used to extract the desired dataset. The desired dataset consists of every charge not followed by an updated charge.

df[!c(c(df$charge=="Amended")[-1], c(df$charge=="Amended")[1]),]

Enroll in SOCIO460 Youth and Crime!

Looking for an engaging course in criminology this summer? Interested in the theories and facts of youth crime and crime control? Want to experience the youth justice field firsthand? Then have I got a course for you!

SOCIO460 Youth and Crime offers theories, facts and firsthand experiences with the youth justice field. Through this course you will:

  • Learn facts and explore trends in delinquency, status offenses and victimization.
  • Explore theories on crime and crime control, everything from rational choice to intersectional feminist thought.
  • Discuss pressing social issues with juvenile court and care professionals.
  • Sift through juvenile court case records and examine the workings of a Kansas juvenile court.

Special guests include: Judge Delia M. York, Evan Mitchel (LMSW), ISO Kristy Blagg and Kristin B. Kelly (MA).

socio460 flyer v3

Print friendly flyer: socio460 flyer

Plot Network Data in R with iGraph

I recently had a conversation on Twitter about a plot I made a while back. Recall, the plot showed my Twitter network, my friends and my friend’s friends.

Here’s the Twitter thread:

And here’s the R code:

#### Load R libraries

#### Load edgelist
r <- read.csv(file="edgelist_friends.csv-03-25.csv",header=TRUE,stringsAsFactors=FALSE)[,-1]

#### Convert to graph object
gr <-,directed=TRUE)

#### gr
# Describe graph
ecount(gr) # Edge count
vcount(gr) # Node count
diameter(gr) # Network diameter
farthest.nodes(gr) # Nodes furthest apart
V(gr)$indegree = degree(gr,mode="in") # Calculate indegree

#### Plot graph
E(gr)$color = "gray"
E(gr)$width = .5
E(gr)$arrow.width = .25
V(gr)$label.color = "black"
V(gr)$color = "dodgerblue"
V(gr)$size = 4

l <- layout.fruchterman.reingold(gr)


Create a dictionary of authors and author attributes and values for a journal article using the Scopus API and Python

As an exercise to brush up my Python skills, I decided to tinker around with the Scopus API. Scopus is an online database maintained by Elsevier that records and provides access to information about peer reviewed publications. Not only does Scopus let users search for journal articles based on key words and various other criteria, but the web services also allows users to explore these articles as networks of articles, authors, institutions, and so forth. If you’re interested in risk factors that lead to scholarly publications, publication citations, or impact factors, this is a place to start.

The following code yields a dictionary of author information by requesting content through the abstract retrieval API. This request is made using the Python package requests and parsed using the package BeautifulSoup. Enjoy!

#### Import python packages
import requests
from bs4 import BeautifulSoup

#### Set API key
my_api_key = 'YoUr_ApI_kEy'

#### Abstract retrieval API
# API documentation at
# Get article info using unique article ID
eid = '2-s2.0-84899659621'
url = '' + eid

header = {'Accept' : 'application/xml',
          'X-ELS-APIKey' : my_api_key}

resp = requests.get(url, headers=header)

print 'API Response code:', resp.status_code # resp.status_code != 200 i.e. API response error

# Write response to file
#with open(eid, 'w') as f:
#    f.write(resp.text.encode('utf-8'))

soup = BeautifulSoup(resp.content.decode('utf-8','ignore'), 'lxml')

soup_author_groups = soup.find_all('author-group')

print 'Number author groups:', len(soup_author_groups)

author_dict = {}

# Traverse author groups
for i in soup_author_groups:

    # Traverse authors within author groups
    for j in i.find_all('author'):

        author_dict.update({j.attrs['auid']:j.attrs}) # Return dictionary of attributes
        j.contents.pop(-1) # Pop dicitonary of attributes
        # Traverse author contents within author
        for k in j.contents:

            author_dict[j.attrs['auid']].update({ : k.contents[0]})
print author_list

Jitter scatterplot value positions with value labels in R using ggplot2

The following R code creates a scatterplot using ggplot2. Points on this plot are represented by identification numbers. The jitter option removes overlap between these plotted values.

#### Attach R libraries

#### Generate random data set 
theData <- data.frame(id=1:20, xVar=sample(1:4, 20, replace=TRUE), yVar=sample(1:4, 20, replace=TRUE))

#### Plot scatterplot
p <- ggplot(theData)
p + theme_bw() + geom_text(aes(x=xVar,y=yVar,label=id),size=3,position=position_jitter(w=.2, h=.2))