This notebook is a personal, restructured version of a data analytics assignment I completed (FIT3152, Monash University): analysing country-level predictors of confidence in social institutions using the World Values Survey (WVS) Wave 7 data.
My assigned focus country is Japan (JPN). The four themes are:
The data is a per-student random subset of the WVS Wave 7 extract: 50,000 respondents and a personally-assigned set of 40 columns (a fixed 13 identifier/demographic columns, 17 randomly sampled “value/attitude” predictors, and 10 randomly sampled confidence-in-institutions outcomes), drawn with a seed tied to my student ID so the subset is reproducible.
library(ggplot2)
library(GGally)
library(tidyr)
library(reshape2)
id <- as.numeric(Sys.getenv("STUDENT_ID"))
set.seed(id)
VCData = read.csv("WVSExtract.csv")
VC = VCData[sample(1:nrow(VCData), 50000, replace = FALSE), ]
VC = VC[, c(1:6, sort(sample(7:46, 17, replace = FALSE)), 47:53,
sort(sample(54:69, 10, replace = FALSE)))]
The dataset has 50,000 observations across 40 variables. All
variables are integer-coded except Country, which is
character (the ISO3 country code).
dim(VC)
## [1] 50000 40
str(VC)
## 'data.frame': 50000 obs. of 40 variables:
## $ Country : chr "DEU" "AUS" "MAC" "TUR" ...
## $ TPeople : int 1 1 2 2 2 1 1 2 1 2 ...
## $ TFamily : int 2 1 1 1 1 1 1 2 1 1 ...
## $ TNeighbourhood: int 2 2 1 3 2 2 2 3 2 2 ...
## $ TKnow : int 2 1 2 2 1 2 1 1 2 3 ...
## $ TMeet : int 3 3 3 4 2 3 2 2 3 2 ...
## $ VFamily : int 1 1 1 1 1 1 1 2 1 1 ...
## $ VWork : int 2 2 2 2 1 2 2 1 3 2 ...
## $ VReligion : int 3 4 2 3 2 3 3 1 1 2 ...
## $ HChoice : int 9 6 6 7 8 5 8 8 4 3 ...
## $ HFood : int 4 4 4 3 4 4 4 4 4 1 ...
## $ HCrime : int 3 4 4 3 4 4 4 4 4 2 ...
## $ HShelter : int 4 4 4 3 4 4 4 4 4 2 ...
## $ EEquality : int 7 2 6 3 6 6 10 6 7 2 ...
## $ ECompetition : int 2 6 6 6 1 4 1 7 6 2 ...
## $ EHardWork : int 7 7 4 8 2 4 2 7 6 2 ...
## $ SSecure : int 3 2 2 3 3 2 3 2 2 2 ...
## $ SJob : int 3 1 4 3 2 2 3 2 2 1 ...
## $ STRight : int 3 5 5 5 1 6 9 9 6 4 ...
## $ STImportant : int 1 9 6 6 1 6 10 8 6 5 ...
## $ PNewspaper : int 1 2 2 5 2 3 5 2 1 2 ...
## $ PTelevision : int 4 1 3 1 3 1 5 1 1 3 ...
## $ PMobile : int 1 5 3 1 2 1 5 1 4 3 ...
## $ PDemImp : int 8 9 7 8 7 7 9 7 6 2 ...
## $ PDemCurrent : int 4 6 7 6 4 4 3 7 6 2 ...
## $ PSatisfied : int 3 4 6 5 2 3 2 9 6 2 ...
## $ MF : int 2 1 1 2 1 2 2 2 1 1 ...
## $ Age : int 45 56 34 33 21 43 37 41 48 16 ...
## $ Edu : int 7 3 6 1 5 4 4 7 6 3 ...
## $ Employment : int 3 7 1 5 6 1 1 1 1 6 ...
## $ CReligious : int 3 3 2 3 2 2 2 2 1 2 ...
## $ CArmedForces : int 3 1 2 2 2 2 3 1 3 2 ...
## $ CTelevision : int 3 3 2 3 3 3 4 2 3 3 ...
## $ CPolice : int 2 1 2 2 3 2 3 1 3 3 ...
## $ CCourts : int 2 2 2 3 4 2 3 2 3 2 ...
## $ CPParties : int 3 3 2 2 4 3 4 2 3 3 ...
## $ CUniversities : int 2 2 2 1 2 2 3 2 3 3 ...
## $ CElections : int 2 2 2 2 1 3 4 2 3 3 ...
## $ CBanks : int 3 2 2 1 2 2 4 3 3 3 ...
## $ CEnvOrg : int 2 2 2 1 4 2 4 2 3 2 ...
There are 65 countries in the dataset, and the number of respondents per country varies quite a lot. This is a real imbalance to keep in mind for anything that pools countries together.
length(unique(VC$Country)) # Number of countries
## [1] 65
min(table(VC$Country)) # Lowest number of instances by country
## [1] 510
max(table(VC$Country)) # Highest number of instances by country
## [1] 2085
Cyprus (CYP) appears the least, with 510 respondents, while Canada (CAN) appears the most, with 2,085.
Looking at summary(VC), every numerical attribute has a
minimum value of -5, which is not a real response. Per the WVS Wave 7
documentation, values from -5 to -1 are missing-data codes (refused,
don’t know, not applicable, etc.), not genuine scale points.
summary(VC)
## Country TPeople TFamily TNeighbourhood
## Length:50000 Min. :-5.000 Min. :-5.000 Min. :-5.000
## Class :character 1st Qu.: 1.000 1st Qu.: 1.000 1st Qu.: 2.000
## Mode :character Median : 2.000 Median : 1.000 Median : 2.000
## Mean : 1.708 Mean : 1.262 Mean : 2.151
## 3rd Qu.: 2.000 3rd Qu.: 1.000 3rd Qu.: 3.000
## Max. : 2.000 Max. : 4.000 Max. : 4.000
## TKnow TMeet VFamily VWork
## Min. :-5.000 Min. :-5.000 Min. :-5.000 Min. :-5.000
## 1st Qu.: 2.000 1st Qu.: 2.000 1st Qu.: 1.000 1st Qu.: 1.000
## Median : 2.000 Median : 3.000 Median : 1.000 Median : 1.000
## Mean : 2.036 Mean : 2.948 Mean : 1.107 Mean : 1.509
## 3rd Qu.: 2.000 3rd Qu.: 4.000 3rd Qu.: 1.000 3rd Qu.: 2.000
## Max. : 4.000 Max. : 4.000 Max. : 4.000 Max. : 4.000
## VReligion HChoice HFood HCrime
## Min. :-5.000 Min. :-5.000 Min. :-5.000 Min. :-5.000
## 1st Qu.: 1.000 1st Qu.: 6.000 1st Qu.: 3.000 1st Qu.: 3.000
## Median : 2.000 Median : 7.000 Median : 4.000 Median : 4.000
## Mean : 1.948 Mean : 7.136 Mean : 3.464 Mean : 3.414
## 3rd Qu.: 3.000 3rd Qu.: 9.000 3rd Qu.: 4.000 3rd Qu.: 4.000
## Max. : 4.000 Max. :10.000 Max. : 4.000 Max. : 4.000
## HShelter EEquality ECompetition EHardWork
## Min. :-5.000 Min. :-5.000 Min. :-5.000 Min. :-5.000
## 1st Qu.: 4.000 1st Qu.: 4.000 1st Qu.: 1.000 1st Qu.: 1.000
## Median : 4.000 Median : 7.000 Median : 4.000 Median : 4.000
## Mean : 3.679 Mean : 6.162 Mean : 3.973 Mean : 4.384
## 3rd Qu.: 4.000 3rd Qu.: 9.000 3rd Qu.: 6.000 3rd Qu.: 7.000
## Max. : 4.000 Max. :10.000 Max. :10.000 Max. :10.000
## SSecure SJob STRight STImportant
## Min. :-5.000 Min. :-5.000 Min. :-5.00 Min. :-5.000
## 1st Qu.: 1.000 1st Qu.: 1.000 1st Qu.: 3.00 1st Qu.: 2.000
## Median : 2.000 Median : 2.000 Median : 5.00 Median : 4.000
## Mean : 1.981 Mean : 1.949 Mean : 5.07 Mean : 4.353
## 3rd Qu.: 2.000 3rd Qu.: 3.000 3rd Qu.: 8.00 3rd Qu.: 7.000
## Max. : 4.000 Max. : 4.000 Max. :10.00 Max. :10.000
## PNewspaper PTelevision PMobile PDemImp
## Min. :-5.000 Min. :-5.000 Min. :-5.00 Min. :-5.000
## 1st Qu.: 2.000 1st Qu.: 1.000 1st Qu.: 1.00 1st Qu.: 7.000
## Median : 4.000 Median : 1.000 Median : 1.00 Median : 9.000
## Mean : 3.307 Mean : 1.812 Mean : 2.38 Mean : 8.186
## 3rd Qu.: 5.000 3rd Qu.: 2.000 3rd Qu.: 5.00 3rd Qu.:10.000
## Max. : 5.000 Max. : 5.000 Max. : 5.00 Max. :10.000
## PDemCurrent PSatisfied MF Age
## Min. :-5.000 Min. :-5.000 Min. :-5.000 Min. :-5.00
## 1st Qu.: 4.000 1st Qu.: 3.000 1st Qu.: 1.000 1st Qu.:29.00
## Median : 6.000 Median : 5.000 Median : 2.000 Median :41.00
## Mean : 5.956 Mean : 5.055 Mean : 1.522 Mean :42.88
## 3rd Qu.: 8.000 3rd Qu.: 7.000 3rd Qu.: 2.000 3rd Qu.:55.00
## Max. :10.000 Max. :10.000 Max. : 2.000 Max. :99.00
## Edu Employment CReligious CArmedForces
## Min. :-5.000 Min. :-5.00 Min. :-5.000 Min. :-5.000
## 1st Qu.: 2.000 1st Qu.: 1.00 1st Qu.: 1.000 1st Qu.: 1.000
## Median : 3.000 Median : 3.00 Median : 2.000 Median : 2.000
## Mean : 3.497 Mean : 3.05 Mean : 2.113 Mean : 1.896
## 3rd Qu.: 6.000 3rd Qu.: 5.00 3rd Qu.: 3.000 3rd Qu.: 3.000
## Max. : 8.000 Max. : 8.00 Max. : 4.000 Max. : 4.000
## CTelevision CPolice CCourts CPParties
## Min. :-5.000 Min. :-5.000 Min. :-5.000 Min. :-5.000
## 1st Qu.: 2.000 1st Qu.: 2.000 1st Qu.: 2.000 1st Qu.: 2.000
## Median : 3.000 Median : 2.000 Median : 2.000 Median : 3.000
## Mean : 2.567 Mean : 2.219 Mean : 2.255 Mean : 2.827
## 3rd Qu.: 3.000 3rd Qu.: 3.000 3rd Qu.: 3.000 3rd Qu.: 4.000
## Max. : 4.000 Max. : 4.000 Max. : 4.000 Max. : 4.000
## CUniversities CElections CBanks CEnvOrg
## Min. :-5.00 Min. :-5.000 Min. :-5.000 Min. :-5.000
## 1st Qu.: 1.00 1st Qu.: 2.000 1st Qu.: 2.000 1st Qu.: 2.000
## Median : 2.00 Median : 3.000 Median : 2.000 Median : 2.000
## Mean : 2.01 Mean : 2.406 Mean : 2.313 Mean : 2.142
## 3rd Qu.: 3.00 3rd Qu.: 3.000 3rd Qu.: 3.000 3rd Qu.: 3.000
## Max. : 4.00 Max. : 4.000 Max. : 4.000 Max. : 4.000
To see how missing values are distributed across attributes, I count how many values of each predictor fall in the negative (missing-code) range:
count_missing = function() {
counts = data.frame(country = character(), missing_count = numeric(), percentage = numeric())
for (col in names(VC[, -1])) {
count = colSums(VC[, -1] < 0)[col]
counts[nrow(counts) + 1, ] = data.frame(
country = col,
missing_count = count,
percentage = count / nrow(VC) * 100
)
}
rownames(counts) = NULL
return(counts)
}
missing_values = count_missing()
head(missing_values)
ggplot(missing_values, aes(x = country, y = missing_count)) +
geom_bar(stat = "identity") +
theme(axis.text.x = element_text(angle = 45, vjust = 0.5, hjust = 0.5)) +
labs(x = "Variable", y = "Missing count", title = "Missing values by variable")
Figure 1: Missing value count by variable
The number of missing values varies quite a bit from attribute to
attribute. Respondent’s sex (MF) has the fewest missing
values (55 instances, 0.11%) whereas confidence in environmental
protection movements (CEnvOrg) has the most, at 3,287
(6.57%) of all responses. Confidence-related questions and the
science/technology-attitude questions tend to sit at the higher end,
while demographic and trust questions tend to sit at the lower end.
For everything that follows, I replace the missing-data codes with
proper NAs so they don’t get treated as real responses:
VC[VC < 0 & VC >= -5] = NA
To see how Japanese respondents compare to everyone else, I split the sample into “JPN” and “Others”, z-score standardise every numeric variable (so variables on very different scales are comparable), and take the average response of each group.
scaled_VC = as.data.frame(lapply(VC, function(col) {
if (is.numeric(col)) scale(col) else col
})) # Scaling data
VC_JPN_Others_raw = VC
VC_JPN_Others_raw$Country = ifelse(VC$Country == "JPN", "JPN", "Others")
VC_JPN_Others = scaled_VC
VC_JPN_Others$Country = ifelse(VC$Country == "JPN", "JPN", "Others")
response_JPN_Others = t(aggregate(. ~ Country, VC_JPN_Others, mean)[, -1])
colnames(response_JPN_Others) = c("JPN", "Others")
The heat map below shows the (standardised) difference between the average Japanese response and the average response from every other country pooled together.
heat_map_difference = function(data, col_1, col_2) {
difference = data[, col_1] - data[, col_2]
heat_map_data = data.frame(variables = rownames(data), difference = difference)
return(ggplot(heat_map_data, aes(x = "", y = variables, fill = difference)) +
geom_tile(color = "black", width = 2) +
scale_fill_gradient2(
low = "#D73027",
mid = "white",
high = "#1A9850"
) +
labs(x = ""))
}
heat_map_difference(response_JPN_Others, "JPN", "Others")
Figure 2: Differences between responses in Japan and responses in other countries
Quite a few variables show a substantial difference. The most
noticeable is VReligion (importance of religion in life),
which has the largest positive difference: religion matters more to
Japanese respondents than to respondents elsewhere.
CReligious (confidence in religious institutions) shows the
same pattern, which lines up with the VReligion result.
VWork (importance of work) is also higher, while
VFamily (importance of family) is slightly lower.
On average, Japanese respondents place a bit less emphasis on family
relative to the rest of the world.
Age also stands out: the average age of Japanese respondents (53.5) is noticeably higher than the international average (41.7), reflecting Japan’s older population.
On the negative side, PNewspaper and
PTelevision (frequency of using newspapers/TV as an
information source) both register red on the heat map, but for these
variables, a lower value means more frequent use, so this
actually means Japanese respondents consume news from newspapers and
television more often than respondents elsewhere.
HChoice (how much control people feel they have over
their own life, 1 = none, 10 = a great deal) is also clearly lower for
Japan, suggesting Japanese respondents feel they have less control over
their lives on average than respondents elsewhere. Variables like
TFamily, SJob, and TMeet sit
close to zero, meaning Japan looks fairly similar to the rest of the
world on those.
Rather than comparing Japan to every other country as one undifferentiated group, it’s also worth asking which countries in the survey are genuinely similar to Japan, and whether comparing Japan to those countries specifically tells a different story. I use external governance data from the World Bank’s Worldwide Governance Indicators (WGI), which includes Voice and Accountability, Regulatory Quality, Political Stability and Absence of Violence/Terrorism, Rule of Law, Government Effectiveness, and Control of Corruption. For each of those six dimensions I use six sub-indicators: the governance estimate itself, its standard deviation, the number of data sources it’s based on, its percentile rank, and the lower and upper bounds of its 90% confidence interval. That’s 36 indicators describing each country’s institutional quality, for the most recent year available (2022).
wgi = read.csv("wgidataset.csv")
wgi = wgi[wgi["year"] == 2022, ]
wgi = pivot_wider(wgi,
id_cols = c(code, countryname),
names_from = indicator,
values_from = c(estimate, stddev, nsource, pctrank, pctranklower, pctrankupper))
wgi = wgi[!apply(wgi == "..", 1, any), !(colnames(wgi) %in% c("codeindyr", "year"))]
wgi = as.data.frame(wgi)
A couple of country codes don’t match between the two datasets:
unique(VC$Country)[!(unique(VC$Country) %in% wgi$code)]
## [1] "ROU" "AND"
ROU (Romania) is coded ROM in the WGI data,
and AND (Andorra) is coded ADO, so I fix those
before filtering the WGI data down to just the countries in my survey
extract and scaling the indicators:
wgi$code[wgi$code == "ROM"] = "ROU"
wgi$code[wgi$code == "ADO"] = "AND"
wgi = wgi[wgi$code %in% unique(VC$Country), ]
rownames(wgi) = wgi$code
wgi[, 3:length(wgi)] = sapply(wgi[, 3:length(wgi)], function(col) as.numeric(col))
wgi[, 3:length(wgi)] = scale(wgi[, 3:length(wgi)])
nrow(wgi)
## [1] 65
I fit k-means with \(k = \lfloor n/5 \rfloor\) clusters (a rule of thumb aiming for groups of around 5 countries), with 20 random starts to stabilise the result:
id <- as.numeric(Sys.getenv("STUDENT_ID"))
set.seed(id)
country_fit = kmeans(wgi[, 3:length(wgi)], floor(nrow(wgi[, 3:length(wgi)]) / 5), nstart = 20)
table(country_fit$cluster)
##
## 1 2 3 4 5 6 7 8 9 10 11 12 13
## 11 7 2 1 6 3 11 5 5 5 2 5 2
country_fit$cluster["JPN"]
## JPN
## 1
cluster_countries = names(country_fit$cluster[country_fit$cluster == country_fit$cluster["JPN"]])
cluster_countries
## [1] "AUS" "CAN" "DEU" "JPN" "KOR" "NLD" "NZL" "TWN" "GBR" "USA" "URY"
Japan’s cluster contains 11 countries: Australia, Canada, Germany, Japan, South Korea, the Netherlands, New Zealand, Taiwan, the United Kingdom, the United States, and Uruguay. These are mostly high-income, high-governance-quality democracies, which makes intuitive sense given the indicators used.
pca = prcomp(wgi[, 3:length(wgi)], scale. = TRUE)
pca_df = data.frame(
pca$x[, 1:2],
Cluster = factor(country_fit$cluster),
Country = wgi$code)
jpn_cluster_id = country_fit$cluster["JPN"]
ggplot(pca_df, aes(PC1, PC2, color = Cluster)) +
geom_point(size = 2) +
geom_point(data = subset(pca_df, Cluster == jpn_cluster_id),
aes(PC1, PC2),
shape = 21, fill = NA, color = "black", size = 2, stroke = 1) +
theme_minimal()
Figure 6: Visualisation of the k-means clusters (first two principal components), Japan’s cluster outlined in black
Japan’s cluster (outlined) sits clearly apart from the bulk of countries along the first principal component, which broadly separates higher- from lower-governance-quality countries.
MF up to 6.57% for
CEnvOrg).A few small, deliberate departures from how I originally ran this analysis, made purely so the whole notebook can be re-run end-to-end and always produce the tables and figures shown above:
step() is called with trace = 0 to
suppress its per-iteration AIC log, which would otherwise print pages of
intermediate output for each of the 30 stepwise models fit in Theme
4.eval tables computed earlier in the same session,
rather than via a manually-edited intermediate CSV file.set.seed() calls are placed immediately before
the k-means clustering and before each group’s train/test-split loop, so
re-knitting this document is fully deterministic.