R Basics: Objects, Vectors, Data Frames, Functions, and Data Analysis
R. Rationale
R is a programming language for data analysis. Its core is not complex: objects save data, functions process objects, and packages provide additional functions and data sets. The note is based on these basic concepts and goes back into data import, collation and exploration.
R itself contains an interpreter and a standard library, RStudio is a common integrated development environment. Both need to be installed separately; RStudio edits the code, manages the project and display objects, and the actual calculation is still R.
function. Enter the name of the function object directly, and R will print its definition; the function will only be called if it is in parentheses.
ls # 查看函数对象
ls() # 调用函数
function is also an object, so an object cannot be judged by whether it is a function by whether it is in round brackets. Available for inspection is.function()。
is.function(ls)
The R package is installed in the local library and passed before use library() Loads the package to the current session. The package will only be installed once, but it will be reloaded every time a new R session starts.
install.packages("readxl") # 只需安装一次
library(readxl) # 每个新会话都要加载
Object & Value
R Common <- Granted.=、assign() And from right to left. -> It can be given value, but mixing reduces readability.
n <- 10
n = 10
assign("n", 10)
10 -> n
n
Object name is case sensitive. The generic name starts with the letter or does not follow the number, and can be followed by letters, numbers, points and underlineds.x and X Two different objects.
When the expression is not given a value, the result will only be printed to the console and will not be automatically saved. Comment from # Start, continue until the end of the line.
2 + 3 # 打印 5,但不保存结果
result <- 2 + 3 result
Data Structure for R
R Object has both a bottom storage type and a possible possession class、dim、names equals. It is more accurate to understand these two layers of information than to create a rigid “type” of mutually exclusive objects.
Object Properties and Information
The bottom types common to atomic vectors include logical, integer, double-precision, plural, character and original bytes. The factor is not a separate bottom type, but has levels and class = "factor" attribute's integer vector.
x <- 1:5
typeof(x) # 底层存储类型 class(x) # 对象类别 length(x) # 元素个数 attributes(x) # 对象属性
is.*() function is used to judge objects,as.*() function. Call with the actual object.
is.numeric(x)
digits <- as.character(x)
R uses several special values to describe situations that cannot be treated in normal values:
NAis the missing value.NaNindicates undefined numerical results, e.g.0 / 0。Infand-InfIt means positive and negative.NULLUsually indicates that the object or result does not exist and the missing value is one lengthNADifferent.
Character values are placed in single or double quotation marks. When similar quotation marks are required in a string, you can transpose them with a backslash.
message <- "Double quotes \" delimit R strings."
Common data structures are as follows:
| Data structure | Main features | Whether columns or elements are allowed to have different types |
|---|---|---|
| Vector | One dimension, homogeneity | Yes |
| Factor | Encoding classification levels with integer | Yes |
| Array | Multi-dimensional, homogenous | Yes |
| Matrix | 2-D array | Yes |
| Data Box | 2D table, each column is a vector | Yes. |
| Time series | Vector or matrix with time index properties | Yes |
| List | Can accommodate any object | Yes. |
Some common operators:
| Operations | Operators |
|---|---|
| Multiplication | ^ |
| Modelling | %% |
| Division | %/% |
| Matrix Multiplication | %*% |
ls() You can list objects in the current environment or you can filter them by name.rm() to remove the object.
ls() ls(pattern = "^m")
rm(x) rm(list = ls(pattern = "^m"))
Vector
Vector is a set of elements of the same type.c()、:、seq() and rep() The most common way to create it.
Numerical vector
1:10
seq(1, 5, by = 0.5)
rep(2:5, times = 2)
c(42, 7, 64, 9)
scan() Data can be read from the console or text connection, but the script is usually more suitable for using clear data import functions.
Character vector
colors <- c("green", "blue sky", "-99")
paste(c("X", "Y"), 1:2, sep = "")
paste() It connects characters,paste0() Equivalent paste(..., sep = "")。
Logical vector
Logical values include TRUE、FALSE and NAI don't know. Comparative calculations usually generate a logical vector.
x <- c(10.4, 5.6, 3.1, 6.4, 21.7) x > 13 7 != 6
all(1:7 > 3) any(1:7 > 3)
T and F It can be revalued and not suitable for replacement in the official code. TRUE and FALSE。
Factor
The factor is used to represent the classification variable. It saves the integer coding for observation and passes levels Record category name. Sets when categories are sequential ordered = TRUE。
colors <- c("green", "blue", "green", "yellow") color_factor <- factor(colors)
scores <- factor( c(1, 2, 3, 1), levels = c(1, 2, 3), labels = c("low", "middle", "high"), ordered = TRUE )
gl() A rule-based factor can be generated.n It's horizontal.k Is the number of consecutive repetitions at each level.length is the length of the result.
gl(n = 3, k = 2, length = 6, labels = c("A", "B", "C"))
The following functions are commonly used to check and summarize factors:
sex <- factor(c("M", "F", "M", "M", "F")) height <- c(174, 165, 180, 171, 160)
is.factor(sex) levels(sex) table(sex) tapply(height, sex, mean)
Vector Operations & Cycle Completion
Vector operations are done on an element-by-element basis. The length of the two vectors is different and the shorter vectors are reused, which is referred to as the recycling rule. If the length of the longer vector is not a multiple of the integer length of the shorter vector, R usually gives a warning; relying on this incomplete loop to fill in can easily hide an error.
x <- c(10.4, 5.6, 3.1, 6.4, 21.7) x * 2 + 1
c(1, 2, 3, 4) + c(10, 20)
Common summary functions:
| Functions | Role |
|---|---|
min(x)、max(x) |
Min & Maximum |
which.min(x)、which.max(x) |
Location of minimum and maximum values |
mean(x)、median(x) |
Mean to Medium |
var(x)、sd(x) |
Difference to Standard |
quantile(x) |
Bits |
summary(x) |
Returns common summary by object category |
sort(x) |
Sort |
sum(x)、prod(x) |
Summation and Multiplication |
cov(x, y)、cor(x, y) |
Differences and related factors |
A lot of summary functions. na.rm parameter. When data contains missing values, it is necessary to clearly decide whether to exclude the missing values, rather than mechanically adding na.rm = TRUE。
mean(c(1, 2, NA), na.rm = TRUE)
The extraction of vector elements
The R index starts at 1. In square brackets you can use positive integers, negative integers, logical vectors or names; positive and negative indices cannot be mixed.0 Except for.
x <- c(42, 7, 64, 9, 10, 8)x[1:5] x[c(1, 4)] x[x > 10] x[-(1:5)]
x[x > 10] <- 10
When checking special values, pass the object to the judgement function:
values <- c(1, NA, NaN, Inf)
is.na(values) is.nan(values) is.finite(values) is.infinite(values)
Numeric & Matrix
The array is tape. dim property. The matrix is a two-dimensional array. They can only save one lower type; if you mix a character value, the value is usually converted to a character.
Create arrays and matrices
A <- array(1:8, dim = c(2, 2, 2)) AX <- matrix(1:8, nrow = 2, ncol = 4) X_by_row <- matrix(1:8, nrow = 2, ncol = 4, byrow = TRUE)
diagonal <- diag(c(10, 20, 30))
R Default to fill the matrix by column.byrow = TRUE will be filled by row.
Matrix and array index
Matrix Use [行, 列] Index. Ignores one dimension for all elements that select the dimension.
x <- matrix(1:6, nrow = 2, ncol = 3)x[2, 2] x[2, ] x[, 3] <- NA x[is.na(x)] <- 1
x[-1, ] x[, -2]
The default of a single line or column result may be reduced to a vector. Use when retaining matrix structure drop = FALSE。
x[1, , drop = FALSE]
Matrix Operations
X <- matrix(1:4, nrow = 2)
t(X) # 转置 diag(X) # 对角线元素 det(X) # 行列式 X * X # 对应元素相乘 X %*% X # 矩阵乘法
Data Box
Data box is the most common table structure in R. Each row usually corresponds to one observation and each column corresponds to one variable. The length of each column must be consistent, but a value, character, logical value or factor can be saved separately.
Create and check data boxes
measurements <- data.frame( id = 1:4, value = c(42, 7, 64, 9), group = c("A", "A", "B", "B") )
str(measurements) summary(measurements) head(measurements)
In actual analysis, data boxes are more derived from external documents. Import methods are followed by the section on Data Import, Storage and Analysis Preparedness.
Direct Use data$column or function data Parameters, ratio attach() More clearly, the data will not be wrong because of the existence of the same name object in the search path.
cor(Puromycin$conc, Puromycin$rate)
pairs(Puromycin, panel = panel.smooth)
xtabs(~ state, data = Puromycin)
Select Rows and Columns
The data boxes support both matrix indexes and listing access.
Puromycin[1, 1] Puromycin[c(1, 3, 5), c("conc", "rate")] Puromycin$conc
subset(Puromycin, state == "treated" & rate > 160)
subset() Appropriate for interactive analysis. When a function that requires strict control of the value-seeking environment is prepared, it is more prudent to use a visible bracketed index.
selected <- leadership[
leadership$age >= 35 | leadership$age < 24,
c("q1", "q2", "q3", "q4")
]
Create and Rename Variables
Puromycin$inverse_conc <- 1 / Puromycin$concPuromycin <- transform( Puromycin, inverse_conc = 1 / conc, sqrt_conc = sqrt(conc) )
names(Puromycin)[names(Puromycin) == "rate"] <- "reaction_rate"
with() is appropriate to read the columns in the data box, but the assigned value does not automatically write back to the original data box.fix() It opens an interactive editor, which is not conducive to recurrence and is therefore not a regular data management method.
Merge and add data
A data box with a common key can be used. merge() Connect. The default result is an internal connection, with only the keys that match both sides;all.x = TRUE You can get a left link.
total <- merge(dataframe_a, dataframe_b, by = "ID")
left_total <- merge(dataframe_a, dataframe_b, by = "ID", all.x = TRUE)
cbind() If you spell objects horizontally in the current line order, you will not be aligned by key, so you should first confirm that the number of lines and the order of lines are consistent.rbind() A vertical data box is added to require that the type of listing and column correspond.
wide <- cbind(dataframe_a, extra_columns)
long <- rbind(dataframe_a, dataframe_b)
Line identifier
Line names can save instance identifiers, but are usually better suited to keep identifiers as normal columns, so they are more directly exported, linked and checked for duplicate values.
patient_data <- data.frame( patient_id = patient_id, age = age, diabetes = diabetes, status = status )
anyDuplicated(patient_data$patient_id)
List
Lists can accommodate objects of different types and lengths, including vectors, matrices, data frames, functions and other lists. Many modelling functions return to the list, as the adhesion results usually contain coefficients, disabilities and diagnostic information at the same time.
results <- list( values = 1:6, matrix = matrix(1:4, nrow = 2) )
results$values results[[1]] # 提取第一个元素本身 results[1] # 返回只含第一个元素的子列表
Time series
ts() Adds a rule time index to the vector or matrix. It is suitable for an intervalent sequence; data with irregular dates or time zone information are usually used for other time objects.
ts(
data = NA,
start = 1,
end = numeric(0),
frequency = 1,
deltat = 1,
names = NULL
)
datais a one-dollar vector or a multiple matrix.startandendSpecifies the position of the end-of-pipe observations.frequencyIndicates the number of observations in each time unit, e.g., quarterly data extraction 4 and monthly data acquisition 12.deltatis the time interval between adjacent observations andfrequencyTwo or one.namesListing for multiple sequences.
annual <- ts(1:10, start = 1959) monthly <- ts(1:47, frequency = 12, start = c(1959, 2)) quarterly <- ts(1:10, frequency = 4, start = c(1959, 2))
multivariate <- ts( matrix(rpois(36, lambda = 5), nrow = 12, ncol = 3), start = c(1961, 1), frequency = 12 )
Date and time
Date The object is stored at the bottom in numerical terms, representing the relative number of days 1970-01-01. Character Date Required as.Date() Convert to actual format.
date_strings <- c("01/05/1965", "08/16/1975")
dates <- as.Date(date_strings, format = "%m/%d/%Y")
Common formatrs:
| Symbol | Meaning | Example: |
|---|---|---|
%d |
Two dates | 01 to 31 |
%a |
Weekly abbreviations | Mon |
%A |
Full week name | Monday |
%m |
Two months. | 01 to 12 |
%b |
Month abbreviation | Jan |
%B |
Full Month Name | January |
%y |
Two years. | 07 |
%Y |
Four years. | 2007 |
Date of acquisition, formatting and comparison:
today <- Sys.Date() now <- Sys.time()format(today, format = "%B %d %Y")
dob <- as.Date("1956-10-12") difftime(today, dob, units = "weeks")
date() returns the character of the current date and time; should continue to calculate Sys.Date() or Sys.time()。
Data import, storage and analysis readiness
Recurring analyses should use the project catalogue and relative paths to the extent possible.getwd() You can view the current working directory.file.path() It is possible to clutter paths across platforms. Do not write the absolute path on the PC in shared scripts.
getwd()
data_path <- file.path("data", "measurements.csv")
Storage data
Text, CSV and RData are common storage formats. CSV is easy to trade with other software, and RData can save multiple R objects in one file.
d <- data.frame( observation = c(1, 2, 3), treatment = c("A", "B", "A"), weight = c(2.3, NA, 9) )write.table( d, file = file.path("data", "observations.txt"), row.names = FALSE, quote = FALSE )
write.csv( d, file = file.path("data", "observations.csv"), row.names = FALSE )
save(d, file = file.path("data", "objects.RData"))
Read Data
Base R can read text and CSV files directly, and can load data sets and RData files with packages.
houses <- read.table("houses.txt", header = TRUE) scores <- read.csv("educ_scores.csv")
data("mtcars") load(file.path("data", "objects.RData"))
read.delim("clipboard") The clipboard is readable in part of the desktop environment, but it relies on the operating system and is not suitable for scripts that require stable recurrence.
When reading formats such as SSS, SAS Transport and Stata, you can use foreign Bag. It is a traditional scheme in the basic workflow, and the level of support for different formats in specific functions is not entirely consistent.
library(foreign)
spss_data <- read.spss("educ_scores.sav", to.data.frame = TRUE) sas_data <- read.xport("educ_scores.xpt") stata_data <- read.dta("educ_scores.dta")
Excel file not valid foreign Read, save as CSV, or readxl。
library(readxl)
so2_data <- read_excel(file.path("data", "SO2.xlsx"))
After import, check the structure, rows and key variables before entering data management. The ability of the file to read does not mean that column type, missing value code and identifier are correct.
str(so2_data)
dim(so2_data)
summary(so2_data)
Missing data
Missing values often appear in real data. Before processing, it is necessary to determine where, why and how the observations were used in the analytical methods.is.na() Return the element-by-component result.complete.cases() Mark full row.
is.na(d)
colSums(is.na(d))
complete.cases(d)
Try Restore
If the original questionnaire, log or other field can determine the missing value, it can be restored to the data source. For example, when there is a definite relationship between the total score and the sub-item, the missing sub-item can sometimes be checked against it. The recovery must be based on clear grounds and cannot return speculation to original data.
Full case analysis
na.omit() Deletes the entire line containing the missing value. Many statistical functions will pass. na.action Adopt a similar treatment. This is simple, but it may reduce the volume of samples; it may also introduce deviations when the missing are not entirely random.
complete_data <- na.omit(d)
The deletion of rows should be determined jointly by missing mechanisms, missing proportions and subsequent analysis, rather than as a default cleansing step.
Multiple plugs
Multiple plug-ins generate a number of reasonably complete data sets, combining models and consolidating estimates and uncertainties.mice The package provides common realization.
library(mice)imp <- mice(data, m = 5, seed = 123)
fits <- with(imp, lm(y ~ x1 + x2)) pooled <- pool(fits) summary(pooled)
completed_data <- complete(imp, action = 1)
Of which:
datais a data box or matrix with missing values.impSaves information on multiple plug-in data sets and plug-in processes.with()Implement the same analytical expression on each plug-in data set.pool()Merge model results with multiple plug-in rules.complete()You can extract a complete data set.
Only one plug-in data set continues to be extrapolated, and uncertainties between plug-ins are lost. If the follow-up is not directly compatible with() and pool()This limitation and the consolidation strategy used should be clearly documented.
Explored data analysis
The exploratory data analysis (EDA) took place before formal modelling. The objective is to understand the relationship between the distribution of variables, anomalies, missing patterns and variables and to check whether the data fit into the research design.
str(data) summary(data) table(data$group, useNA = "ifany")
numeric_data <- data[vapply(data, is.numeric, logical(1))] cor(numeric_data, use = "pairwise.complete.obs")
Numerical summary cannot replace graphics. Histograms, box charts, scatter charts and group charts often reveal structures that are invisible to the coefficient. See more complete statistical methods. EDA and descriptive statisticsSee you at the drawing. R Statistical visualization and R Statistical Graphics。
R Programming
Conditions and Looping
R Provided if、else、switch、for、while and repeat Like control structures. The brackets avoid ambiguity in multi-line branches.
if (condition_1) { statement_1 } else if (condition_2) { statement_2 } else { statement_3 }for (i in 1:5) { print(i) }
i <- 1 while (i <= 5) { print(i) i <- i + 1 }
switch() Select a branch according to the character or position to handle a small number of fixed options.
operation <- "mean"
switch( operation, mean = mean(1:5), sum = sum(1:5), stop("Unknown operation") )
Quantified
Quantification is given to a vector function or logical index on an element-by-element basis, rather than a prominent writing cycle. It is usually simpler, and some functions can also call on optimized bottoms, but quantification itself is not equivalent to automatic parallels.
y <- numeric(length(x)) y[x == b] <- 0 y[x != b] <- 1
y <- ifelse(x == b, 0, 1)
The cycle is not wrong. Clear cycles are often more appropriate when the operation is pre- and post-existing, each time it returns complex objects, or when there is no suitable vector function.
Custom Functions
function consists of a list of parameters and functions. It can return any R object; no visible call return() , the last expression returns the value.
plot_file <- function(title_text, file_path) { data <- read.table(file_path, header = TRUE) plot(data[[1]], data[[2]], type = "l") title(title_text)
invisible(data) }
You can call a function by location or name. Naming parameters need not follow the order of definitions, but full names should be used to avoid the ambiguity of partial matching.
foo1(u, v, w)
foo1(arg3 = w, arg2 = v, arg1 = u)
Parameters can have default values or pass ... Receives additional parameters.
foo2 <- function(arg1, arg2 = 5, arg3 = FALSE, ...) {
list(arg1 = arg1, arg2 = arg2, arg3 = arg3, extra = list(...))
}
R supports a recursive function, but a deeper regression may be limited by the call stack. Many data processing tasks are more direct using circular or vector functions.
Help system
R Help system.help.start() It opens the front page of the help.help() and ? to query functions or special syntax.
help.start()help("lm") ?lm
help("bs", try.all.packages = TRUE) help("bs", package = "splines")
Part of the package also provides a vignette document that describes design thinking or complete workflow.
vignette()
vignette(package = "survival")
Normal functions, broad and method
Normal functions directly perform fixed realizations, and generic functions select methods according to object categories. S3 General UseMethod() Distribution. For example:mean() It's based on class(x) Selection mean.default、mean.Date When it's done, it's still called in form. mean(x)。
mean
methods("mean")
The typical output shows a broad definition and registered method:
function (x, ...) UseMethod("mean")
[1] mean.Date mean.default mean.difftime mean.POSIXct mean.POSIXlt
View function source
For functions performed by R code and currently visible, you can print a function name.
lm
When viewing S3 methods, you can use getS3method()。methods() The output's asterisk means the method is invisible. getAnywhere() You can search for objects and their defined location.
getS3method("mean", "default")
methods("predict") getAnywhere("predict.Arima")
If the core of the function is achieved by C or Fortran, the printing R function usually only sees the containment layer that calls the compiler code. The source code of the R or the corresponding package needs to be viewed while continuing the tracking.
Common Internal Functions
Math Functions
abs(x) # 绝对值
acos(x) # 反余弦
asin(x) # 反正弦
atan(x) # 反正切
atan2(y, x) # 根据 x、y 坐标计算反正切
ceiling(x) # 向上取整
floor(x) # 向下取整
round(x, digits) # 四舍五入
cos(x) # 余弦
cosh(x) # 双曲余弦
exp(x) # 指数函数
log(x) # 自然对数
log10(x) # 以 10 为底的对数
logb(x, base) # 指定底数的对数
sin(x) # 正弦
sinh(x) # 双曲正弦
sqrt(x) # 平方根
tan(x) # 正切
tanh(x) # 双曲正切
Statistical Functions
mean(x) # 均值 median(x) # 中位数 sum(x) # 总和 min(x) # 最小值 max(x) # 最大值 range(x) # 返回最小值和最大值 diff(range(x)) # 极差 diff(x) # 相邻元素之差 prod(x) # 连乘 var(x) # 样本方差 sd(x) # 样本标准差 cor(x, y) # 相关系数 cov(x, y) # 协方差 quantile(x, probs) # 分位数
t.test(x, y) chisq.test(x) cor.test(x, y)
Probability distribution function
R Use uniform naming for common distributions. Add abbreviations before distribution d、p、q or r, obtains a probability density or probability mass, cumulative distribution, fraction and random number functions, respectively. Only part of the distribution is achieved, such as multiple distributions dmultinom() and rmultinom()But there's no match. p*() and q*() function.
| Distribution | R Abbreviations | Common parameters |
|---|---|---|
| Beta | beta |
shape1, shape2 |
| Two | binom |
size, prob |
| Cauchy | cauchy |
location, scale |
| Index | exp |
rate |
| Kafane. | chisq |
df, ncp |
| F | f |
df1, df2, ncp |
| Gamma | gamma |
shape, rate or scale |
| Geometry | geom |
prob |
| Super Geometry | hyper |
m, n, k |
| logarithmic normal | lnorm |
meanlog, sdlog |
| Logistic | logis |
location, scale |
| Multiple | multinom |
size, prob |
| Normal | norm |
mean, sd |
| Negative 2 | nbinom |
size, prob or mu |
| Poisson | pois |
lambda |
| Student t | t |
df |
For example:
dnorm(0) # x = 0 处的密度
pnorm(1.96) # P(X <= 1.96)
qnorm(0.975) # 97.5% 分位数
rnorm(100, mean = 0, sd = 1) # 生成 100 个随机数
Continuously distributed d*() Return density, scattered d*() Returns the probability quality.r*() The first parameter is usually a random number to generate.p*() and q*() Common lower.tail Control left or right end of calculation. Use log.p Controls whether logarithmic probability is used.
pnorm(1.96, lower.tail = FALSE)
qnorm(log(0.025), log.p = TRUE)
Sample and grouping
sample() Sample from vector.size It's the extraction amount.replace Whether or not to put it back,prob You can specify the sampling weight corresponding to one element.
sample(x, size = 5)
sample(x, size = 5, replace = TRUE)
sample(x, size = 5, replace = TRUE, prob = weights)
Arrange group calculations that can be used factorial() and choose()。
factorial(5) choose(52, 4)从 52 个对象中依次取 4 个且不放回的排列数
prod(52:49)
Character processing function
nchar(x) # 字符数
substr(x, start, stop) # 提取或替换子串
substring(x, first, last) # 向量化的子串操作
paste(..., sep = " ", collapse = NULL)
paste0(..., collapse = NULL)
sprintf(format, ...) # 格式化字符串
toupper(x)
tolower(x)
trimws(x) # 去除两端空白
strtrim(x, width) # 截断到指定显示宽度
cat(..., sep = "", file = "") # 连接并输出
gsub(pattern, replacement, x) # 替换全部匹配
sub(pattern, replacement, x) # 替换首个匹配
chartr(old, new, x) # 逐字符替换
strsplit(x, split, fixed = FALSE)
grep(pattern, x, value = FALSE)
regexpr(pattern, text)
gregexpr(pattern, text)
Apply function to matrix and data box
apply() Call a function along the specified dimensions of the matrix or array.MARGIN = 1 This means yes, yes.MARGIN = 2 indicates the column.
apply(x, MARGIN, FUN, ...)
apply(matrix_data, 1, mean) apply(matrix_data, 2, sd)
Use for mixed data frames apply() , the data box may first be converted into a matrix, resulting in a uniform column type. When processing data boxes by columns,lapply() or vapply() Usually more appropriate.
lapply(dataframe, class)
vapply(dataframe, is.numeric, logical(1))
aggregate() Summarizes the numerical columns by one or more grouping variables.
aggregate(x, by, FUN, ...)
aggregate( reaction_rate ~ state, data = Puromycin, FUN = mean )
Formula interface will be pressed state Group, calculate each group reaction_rate average. Returns value remains a data box that allows continued connection or drawing.
- Title: R Basics: Objects, Vectors, Data Frames, Functions, and Data Analysis
- Author: Hyacehila
- Created at : 2024-09-05 02:29:12
- Link: https://hyacehila.github.io//blog/2024/09/05/r-basic-learning-notes/
- License: This work is licensed under CC BY-NC-SA 4.0.