Outline

  1. Installing the R and RStudio
    • Windows
      • Install R
      • Install RStudio
    • Mac
      • Install R
      • Install RStudio
  2. Basic data types
    • Variables and assignment
    • Operators
    • Numeric and integer values
    • Character values
    • Factors
    • Special values (NA, NULL, NaN, Inf)
  3. Data structure
    • Vectors and indexing
    • Matrix
    • List
    • Data frame
  4. Read files
  5. Data exploration
    • Installing the required packages
    • Summary
    • Deal with missing values
    • Tables
    • Bar Chart
    • Correlation matrix with GGally
    • Scatterplot
    • Boxplot
    • Histogram
    • Normal Q-Q Plot
  6. Getting help and further resources
    • Getting help inside R
    • Where to learn more
    • Reproducibility tips

1. Installing the R and RStudio

R is a programming language for statistical computing and graphics supported by the R Core Team and the R Foundation for Statistical Computing. R is used among data miners, bioinformaticians and statisticians for data analysis and developing statistical software. Users have created packages to augment the functions of the R language.

To work with R, you need to download the program and install it. I recommend that you also install R-Studio. R-Studio is a separate program that makes R easier to use .

Windows

Install R

Download the latest R installer (.exe) for Windows. Install the downloaded file as any other windows app.

Install RStudio

Now that R is installed, you need to download and install RStudio. First download the installer for Windows. Run the installer (.exe file) and follow the instructions.

Mac

Install R

First download the latest release (“R-version.pkg”) of R Save the .pkg file, double-click it to open, and follow the installation instructions. Now that R is installed, you need to download and install RStudio.

Install RStudio

First download the the version for Mac. After downloading, double-click the file to open it, and then drag and drop it to your applications folder.

RStudio: integrated development environment for the programming language R
RStudio: integrated development environment for the programming language R

Note: RStudio the company was renamed Posit in 2022, so current downloads are found at posit.co/download/rstudio-desktop. The desktop application itself is still called “RStudio”. It’s good practice to update both R and RStudio periodically — new R versions are released roughly once a year (typically in April), and packages sometimes require a recent R version to install.

After installation, confirm everything works by checking the version numbers:

# Check your R version
R.version.string

# Check RStudio version (run inside RStudio, from the R console)
rstudioapi::versionInfo()$version

2. Basic data types

Variables and assignment

In R, values are stored in variables using the assignment operator <- (read as “gets”). You can also use =, but <- is the R convention and is preferred in scripts because = is reserved for passing arguments inside function calls.

x <- 10      # preferred style
y = 5        # also works, but less idiomatic
x + y
## [1] 15
# You can also assign left-to-right (less common)
15 -> z
z
## [1] 15

Variable names in R are case-sensitive, must start with a letter (or a dot not followed by a number), and can contain letters, numbers, dots, and underscores (e.g. my_var, my.var, myVar2).

Operators

# Arithmetic operators
7 + 3    # addition
## [1] 10
7 - 3    # subtraction
## [1] 4
7 * 3    # multiplication
## [1] 21
7 / 3    # division
## [1] 2.333333
7 %/% 3  # integer division
## [1] 2
7 %% 3   # modulus (remainder)
## [1] 1
7 ^ 3    # exponent
## [1] 343
# Comparison operators (return TRUE/FALSE)
5 == 5   # equal to
## [1] TRUE
5 != 4   # not equal to
## [1] TRUE
5 > 4
## [1] TRUE
5 <= 5
## [1] TRUE
# Logical operators
TRUE & FALSE   # element-wise AND
## [1] FALSE
TRUE | FALSE   # element-wise OR
## [1] TRUE
!TRUE          # NOT
## [1] FALSE

Numeric and integer values

# create create variable "a" that is a vector of one number.
a <- 7
a = 7
a
## [1] 7
# create vectors that consists of multiple numbers.
b <- c(1.25, 2.9, 3.0)
b
## [1] 1.25 2.90 3.00
# create a regular sequence of whole numbers
c <- 1:10
c
##  [1]  1  2  3  4  5  6  7  8  9 10
# seq(from, to, by) function ro creat sequence of numbers
d <- seq(1,10,0.5)
d
##  [1]  1.0  1.5  2.0  2.5  3.0  3.5  4.0  4.5  5.0  5.5  6.0  6.5  7.0  7.5  8.0
## [16]  8.5  9.0  9.5 10.0
# rep() function to repeat a single number, or a sequence of numbers.

rep(99, times=5)
## [1] 99 99 99 99 99
rep(c(1,2,3), times=5)
##  [1] 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
rep(c(1,2,3),each = 5)
##  [1] 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3

Character values

x <- "Monday"
class(x)
## [1] "character"
y <- c("Mon","Tue","Wed","Thu","Fri","Sat","Sun")
class(y)
## [1] "character"

Factors

A factor is a nominal (categorical) variable with a set of known possible values called levels. They can be created using the as.factor function.

yy <- as.factor(y)
yy
## [1] Mon Tue Wed Thu Fri Sat Sun
## Levels: Fri Mon Sat Sun Thu Tue Wed

Special values

R has several reserved values used to represent missing, undefined, or unbounded data. Understanding the difference matters because they behave differently in calculations.

  • NA — “Not Available”: a missing value (of any type: numeric, character, logical…)
  • NULL — represents the absence of a value or an empty object; it has length 0
  • NaN — “Not a Number”: the result of an undefined mathematical operation (e.g. 0/0)
  • Inf / -Inf — positive/negative infinity (e.g. from dividing a number by 0)
NA + 1        # any operation with NA returns NA
## [1] NA
is.na(NA)     # TRUE
## [1] TRUE
length(NULL)  # 0
## [1] 0
0/0           # NaN
## [1] NaN
1/0           # Inf
## [1] Inf
is.nan(0/0)
## [1] TRUE
is.infinite(1/0)
## [1] TRUE

3. Data structure

Vectors and indexing

A vector is the most basic data structure in R — even a single number (like a <- 7 above) is technically a vector of length 1. Elements of a vector are accessed using square brackets [ ].

v <- c(10, 20, 30, 40, 50)
v[1]        # first element (R indexing starts at 1, not 0)
## [1] 10
v[2:4]      # elements 2 through 4
## [1] 20 30 40
v[-1]       # all elements except the first
## [1] 20 30 40 50
v[c(1,3)]   # elements 1 and 3
## [1] 10 30
v[v > 20]   # logical indexing: elements greater than 20
## [1] 30 40 50
length(v)   # number of elements
## [1] 5

Matrix

mat0 <- matrix(NA, ncol=3, nrow=2)
mat0
##      [,1] [,2] [,3]
## [1,]   NA   NA   NA
## [2,]   NA   NA   NA
mat1 <- matrix(1:6, ncol=3, nrow=2)
mat1
##      [,1] [,2] [,3]
## [1,]    1    3    5
## [2,]    2    4    6
mat2 <- matrix(1:6, ncol=3, nrow=2,byrow = TRUE)
mat2
##      [,1] [,2] [,3]
## [1,]    1    2    3
## [2,]    4    5    6

List

A list is a very flexible container to store data. Each element of a list can contain any type of R object, e.g. a vector, matrix, data.frame, another list, or more complex data types.

lst <- list(d,yy,mat2)
lst
## [[1]]
##  [1]  1.0  1.5  2.0  2.5  3.0  3.5  4.0  4.5  5.0  5.5  6.0  6.5  7.0  7.5  8.0
## [16]  8.5  9.0  9.5 10.0
## 
## [[2]]
## [1] Mon Tue Wed Thu Fri Sat Sun
## Levels: Fri Mon Sat Sun Thu Tue Wed
## 
## [[3]]
##      [,1] [,2] [,3]
## [1,]    1    2    3
## [2,]    4    5    6

Data frame

The data.frame is commonly used for statistical data analysis in R. It is a special type of list that requires that all elements (variables) have the same length.

# ToothGrowth is a built-in example dataset (from the datasets package,
# loaded automatically with R) - no need to read a file for it
data(ToothGrowth)
# Print the first 6 rows (the argument is "n", not "row")
head(ToothGrowth, n = 6)
##    len supp dose
## 1  4.2   VC  0.5
## 2 11.5   VC  0.5
## 3  7.3   VC  0.5
## 4  5.8   VC  0.5
## 5  6.4   VC  0.5
## 6 10.0   VC  0.5
# Examine data structure of the dataframe
str(ToothGrowth)
## 'data.frame':    60 obs. of  3 variables:
##  $ len : num  4.2 11.5 7.3 5.8 6.4 10 11.2 11.2 5.2 7 ...
##  $ supp: Factor w/ 2 levels "OJ","VC": 2 2 2 2 2 2 2 2 2 2 ...
##  $ dose: num  0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 ...

4. Read files

To read a file into R, you need to know the path (directory) to the file and the file’s name. There are two common ways to do this:

  1. Using an absolute path with setwd() — points to one exact folder on your computer. This is what many tutorials show, but it means the code will only work on your computer, with your specific folder structure and username — it will break for anyone else (or on a different OS).
  2. Using a relative path / RStudio Project (recommended) — if your .Rmd file and your data file live in the same folder, you can just refer to the file by name, with no setwd() needed at all. In R Markdown, code chunks automatically use the folder that contains the .Rmd file as the working directory. This is the more reproducible approach, especially if you share the project folder (e.g. via Git or Google Drive) with someone else.
# Option 1: absolute path (edit to match YOUR computer - not portable)
setwd("C:/Users/YourName/Documents/IntroductionToR/")

# Option 2 (recommended): keep the .Rmd and the data file in the same
# folder, or use the here package, which builds paths relative to the
# project's top-level folder regardless of who runs the script or what OS they use
# install.packages("here")
library(here)
dt <- read.csv(here("insurance.csv"), header = TRUE)
#Content: source https://www.kaggle.com/datasets/mirichoi0218/insurance
#age: age of primary beneficiary
#sex: insurance contractor gender, female, male
#bmi: Body mass index, providing an understanding of body, weights that are relatively high or low relative to height,
#children: Number of children covered by health insurance / Number of dependents,
#smoker: Smoking
#region: the beneficiary's residential area in the US, northeast, southeast, southwest, northwest.
#charges: Individual medical costs billed by health insurance

# Read data file and keep it in dt
# (assumes insurance.csv is in the same folder as this .Rmd file)
dt <- read.csv("insurance.csv", header = TRUE)
head(dt) # show the first six rows of the data
##   Obs age    sex   bmi children smoker    region   charges
## 1   1  19 female 27.90        0    yes southwest        NA
## 2   2  18   male 33.77        1     no southeast  1725.552
## 3   3  28   male 33.00        3     no southeast  4449.462
## 4   4  33   male    NA        0     no northwest 21984.471
## 5   5  32   male    NA        0     no northwest  3866.855
## 6   6  31 female 25.74        0     no southeast  3756.622
str(dt)  # show data structure 
## 'data.frame':    1338 obs. of  8 variables:
##  $ Obs     : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ age     : int  19 18 28 33 32 31 46 37 37 60 ...
##  $ sex     : chr  "female" "male" "male" "male" ...
##  $ bmi     : num  27.9 33.8 33 NA NA ...
##  $ children: int  0 1 3 0 0 0 1 3 2 0 ...
##  $ smoker  : chr  "yes" "no" "no" "no" ...
##  $ region  : chr  "southwest" "southeast" "southeast" "northwest" ...
##  $ charges : num  NA 1726 4449 21984 3867 ...
# Set data structure
dt$sex <- as.factor(dt$sex)
dt$smoker <- as.factor(dt$smoker)
dt$region <- as.factor(dt$region)
str(dt)
## 'data.frame':    1338 obs. of  8 variables:
##  $ Obs     : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ age     : int  19 18 28 33 32 31 46 37 37 60 ...
##  $ sex     : Factor w/ 2 levels "female","male": 1 2 2 2 2 1 1 1 2 1 ...
##  $ bmi     : num  27.9 33.8 33 NA NA ...
##  $ children: int  0 1 3 0 0 0 1 3 2 0 ...
##  $ smoker  : Factor w/ 2 levels "no","yes": 2 1 1 1 1 1 1 1 1 1 ...
##  $ region  : Factor w/ 4 levels "northeast","northwest",..: 4 3 3 2 2 3 3 2 1 2 ...
##  $ charges : num  NA 1726 4449 21984 3867 ...

Tip: read.csv() is part of base R and always works, but the readr package’s read_csv() (part of the tidyverse) is faster on large files, guesses column types more reliably, and reads strings as character by default instead of factor. For very messy files, check read.csv()’s sep, dec, na.strings, and fileEncoding arguments (the latter is useful for files with Thai or other non-ASCII text).

5. Data exploration

Installing the required packages

This section uses several packages that don’t ship with base R: skimr, dplyr, mice, ggplot2, GGally, and qqplotr. Each only needs to be installed once per computer with install.packages(), but must be loaded with library() in every new R session where you want to use it.

install.packages(c("skimr", "dplyr", "mice", "ggplot2", "GGally", "qqplotr"))

Summary:

Calculate descriptive statistics

summary(dt)
##       Obs              age            sex           bmi           children    
##  Min.   :   1.0   Min.   :18.00   female:662   Min.   :15.96   Min.   :0.000  
##  1st Qu.: 335.2   1st Qu.:27.00   male  :676   1st Qu.:26.27   1st Qu.:0.000  
##  Median : 669.5   Median :39.00                Median :30.40   Median :1.000  
##  Mean   : 669.5   Mean   :39.21                Mean   :30.80   Mean   :1.095  
##  3rd Qu.:1003.8   3rd Qu.:51.00                3rd Qu.:34.77   3rd Qu.:2.000  
##  Max.   :1338.0   Max.   :64.00                Max.   :99.00   Max.   :5.000  
##                                                NAs    :14                     
##  smoker           region       charges     
##  no :1060   northeast:324   Min.   : 1122  
##  yes: 271   northwest:325   1st Qu.: 4747  
##  NAs:   7   southeast:364   Median : 9389  
##             southwest:325   Mean   :13286  
##                             3rd Qu.:16584  
##                             Max.   :63770  
##                             NAs    :24
# aggregate: compute summary statistics of data subsets
aggregate(charges~smoker, data = dt, FUN=summary)
##   smoker charges.Min. charges.1st Qu. charges.Median charges.Mean
## 1     no     1121.874        3996.989       7345.405     8450.999
## 2    yes    12829.455       20984.094      34472.841    32121.864
##   charges.3rd Qu. charges.Max.
## 1       11365.285    36910.608
## 2       41034.221    63770.428
aggregate(charges~smoker+region, data = dt, FUN=summary)
##   smoker    region charges.Min. charges.1st Qu. charges.Median charges.Mean
## 1     no northeast     1694.796        4441.213       8342.909     9200.639
## 2    yes northeast    12829.455       19964.746      28101.333    29883.443
## 3     no northwest     1621.340        4175.355       7239.755     8567.929
## 4    yes northwest    14711.744       21098.554      28868.664    30370.641
## 5     no southeast     1121.874        3486.011       6652.529     8025.767
## 6    yes southeast    16577.780       22853.594      37149.531    34595.779
## 7     no southwest     1241.565        3988.405       7359.957     8036.305
## 8    yes southwest    13844.506       20624.748      35491.640    32623.909
##   charges.3rd Qu. charges.Max.
## 1       11938.256    32108.663
## 2       39597.407    58571.074
## 3       11376.680    33471.972
## 4       39983.426    60021.399
## 5       10998.107    36580.282
## 6       43180.856    63770.428
## 7       11044.263    36910.608
## 8       39970.204    52590.829
tmp <- aggregate(cbind(charges,bmi,age)~sex+smoker+region, data = dt, FUN=summary)
# write the output table into .csv file (saved to the working directory)
write.csv(tmp, file = "skim_table.csv")

# Count NA values by column 
colSums(is.na(dt))                 
##      Obs      age      sex      bmi children   smoker   region  charges 
##        0        0        0       14        0        7        0       24

Skim: an alternative to summary() , quickly providing a broad overview of a data frame

library(skimr)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
skim(dt)
Data summary
Name dt
Number of rows 1338
Number of columns 8
_______________________
Column type frequency:
factor 3
numeric 5
________________________
Group variables None

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
sex 0 1.00 FALSE 2 mal: 676, fem: 662
smoker 7 0.99 FALSE 2 no: 1060, yes: 271
region 0 1.00 FALSE 4 sou: 364, nor: 325, sou: 325, nor: 324

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
Obs 0 1.00 669.50 386.39 1.00 335.25 669.50 1003.75 1338.00 ▇▇▇▇▇
age 0 1.00 39.21 14.05 18.00 27.00 39.00 51.00 64.00 ▇▅▅▆▆
bmi 14 0.99 30.80 6.90 15.96 26.27 30.40 34.77 99.00 ▇▅▁▁▁
children 0 1.00 1.09 1.21 0.00 0.00 1.00 2.00 5.00 ▇▂▂▁▁
charges 24 0.98 13285.52 12141.46 1121.87 4746.52 9388.75 16584.32 63770.43 ▇▂▁▁▁
#skimr by categorical variables
dt %>% 
  dplyr::group_by(sex) %>%
  skim()
Data summary
Name Piped data
Number of rows 1338
Number of columns 8
_______________________
Column type frequency:
factor 2
numeric 5
________________________
Group variables sex

Variable type: factor

skim_variable sex n_missing complete_rate ordered n_unique top_counts
smoker female 0 1.00 FALSE 2 no: 547, yes: 115
smoker male 7 0.99 FALSE 2 no: 513, yes: 156
region female 0 1.00 FALSE 4 sou: 175, nor: 164, sou: 162, nor: 161
region male 0 1.00 FALSE 4 sou: 189, nor: 163, sou: 163, nor: 161

Variable type: numeric

skim_variable sex n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
Obs female 0 1.00 668.05 388.70 1.00 336.25 659.50 1012.50 1338.00 ▇▇▇▇▇
Obs male 0 1.00 670.92 384.40 2.00 335.00 680.50 1002.25 1334.00 ▇▇▇▇▇
age female 0 1.00 39.50 14.05 18.00 27.00 40.00 51.75 64.00 ▇▅▆▆▆
age male 0 1.00 38.92 14.05 18.00 26.00 39.00 51.00 64.00 ▇▅▅▆▅
bmi female 4 0.99 30.34 6.02 16.82 26.05 30.02 34.27 48.07 ▂▇▇▅▁
bmi male 10 0.99 31.26 7.65 15.96 26.41 30.79 35.20 99.00 ▇▅▁▁▁
children female 0 1.00 1.07 1.19 0.00 0.00 1.00 2.00 5.00 ▇▂▂▁▁
children male 0 1.00 1.12 1.22 0.00 0.00 1.00 2.00 5.00 ▇▂▂▁▁
charges female 11 0.98 12625.83 11181.08 1607.51 4897.72 9432.93 14465.16 63770.43 ▇▂▁▁▁
charges male 13 0.98 13933.28 12991.61 1121.87 4591.51 9361.33 18884.66 62592.87 ▇▂▁▁▁
tmp <- dt %>% 
 dplyr::group_by(smoker,region,sex) %>%
 skim(charges,bmi,age)

Deal with missing values

In R the missing values are coded by the symbol NA. Sometimes values are stored as 99 that you can convert into NA using the following command.

dt$bmi[dt$bmi==99] <- NA
#skim(dt)

The function na.omit() returns the object with deletion of missing values

dt.rmna <- na.omit(dt)
#skim(dt.rmna)

MICE package: linear regression is used to predict continuous missing values. Logistic regression is used for categorical missing values. Once this cycle is complete, multiple data sets are generated. These data sets differ only in imputed missing values. Generally, it’s considered to be a good practice to build models on these data sets separately and combining their results.

  • PMM (Predictive Mean Matching) — For numeric variables
  • logreg(Logistic Regression) — For Binary Variables( with 2 levels)
  • polyreg(Bayesian polytomous regression) — For Factor Variables (>= 2 levels)
  • Proportional odds model (ordered, >= 2 levels)
#install.packages("mice")
library(mice)
## Warning: package 'mice' was built under R version 4.6.1
## 
## Attaching package: 'mice'
## The following object is masked from 'package:stats':
## 
##     filter
## The following objects are masked from 'package:base':
## 
##     cbind, rbind
dt.imputed<- mice(dt)
## 
##  iter imp variable
##   1   1  bmi  smoker  charges
##   1   2  bmi  smoker  charges
##   1   3  bmi  smoker  charges
##   1   4  bmi  smoker  charges
##   1   5  bmi  smoker  charges
##   2   1  bmi  smoker  charges
##   2   2  bmi  smoker  charges
##   2   3  bmi  smoker  charges
##   2   4  bmi  smoker  charges
##   2   5  bmi  smoker  charges
##   3   1  bmi  smoker  charges
##   3   2  bmi  smoker  charges
##   3   3  bmi  smoker  charges
##   3   4  bmi  smoker  charges
##   3   5  bmi  smoker  charges
##   4   1  bmi  smoker  charges
##   4   2  bmi  smoker  charges
##   4   3  bmi  smoker  charges
##   4   4  bmi  smoker  charges
##   4   5  bmi  smoker  charges
##   5   1  bmi  smoker  charges
##   5   2  bmi  smoker  charges
##   5   3  bmi  smoker  charges
##   5   4  bmi  smoker  charges
##   5   5  bmi  smoker  charges
summary(dt.imputed)
## Class: mids
## Number of multiple imputations:  5 
## Imputation methods:
##      Obs      age      sex      bmi children   smoker   region  charges 
##       ""       ""       ""    "pmm"       "" "logreg"       ""    "pmm" 
## PredictorMatrix:
##          Obs age sex bmi children smoker region charges
## Obs        0   1   1   1        1      1      1       1
## age        1   0   1   1        1      1      1       1
## sex        1   1   0   1        1      1      1       1
## bmi        1   1   1   0        1      1      1       1
## children   1   1   1   1        0      1      1       1
## smoker     1   1   1   1        1      0      1       1
# check imputed values
dt.imputed$imp$charges
##             1         2         3         4         5
## 1   26125.675 15359.104 15359.104 14711.744 17085.268
## 7    9863.472 35160.135 13415.038  6289.755 25992.821
## 13   4992.376  5209.579  2632.992  1880.070  5855.903
## 22   5257.508  3956.071  7526.706  1633.044 11737.849
## 23   1748.774  2261.569  3176.816  1972.950  2566.471
## 53  42112.236 39983.426 42112.236 38709.176 38998.546
## 75   8932.084  8232.639  7050.642  6272.477  6548.195
## 92   8615.300 12475.351 11729.680  9447.382 28340.189
## 93  47462.894 46130.526 47291.055 46151.124 45702.022
## 158 32548.340 15006.579 16297.846 14711.744 17085.268
## 211  4133.642  5693.431  2203.472  2257.475  2261.569
## 212 13415.038  8410.047  1163.463 25656.575  7337.748
## 220 21595.382  2198.190  1837.237  1711.027  2457.502
## 305  8944.115 21797.000 36580.282 13607.369 10977.206
## 308  4779.602  3393.356  2899.489  3172.018  2480.979
## 313 63770.428 42856.838 24869.837 48885.136 27533.913
## 314  8527.532 11658.115  9174.136 13012.209 11658.379
## 365  1627.282  3732.625 21595.382  1627.282  1515.345
## 397  7445.918  8551.347 24513.091 11070.535 12629.897
## 431  2566.471  3161.454  2200.831  2156.752  1261.442
## 537  9863.472  5397.617  6986.697  9095.068  9500.573
## 856  1719.436  3268.847  1621.883  1842.519  2974.126
## 895 15161.534 10601.412 10977.206 13887.969 13228.847
## 955 51194.559 20984.094 38126.247 21098.554 37465.344
dt.imputed$imp$bmi
##          1      2      3      4      5
## 4   35.500 33.110 40.480 25.740 24.700
## 5   28.380 30.115 32.300 36.195 30.800
## 9   32.775 27.265 27.600 29.500 26.315
## 43  28.600 32.670 37.070 28.160 26.620
## 44  45.320 32.800 29.370 27.720 45.430
## 130 33.700 36.600 31.600 30.495 29.000
## 147 29.590 41.325 43.010 43.700 36.670
## 423 30.800 36.850 31.800 30.200 36.200
## 480 37.290 33.440 33.330 30.875 33.770
## 496 26.885 28.785 30.590 32.965 25.460
## 531 40.920 31.460 38.060 46.200 35.310
## 542 23.210 39.270 34.800 36.630 28.000
## 543 25.740 41.470 30.690 27.360 30.115
## 544 31.350 33.330 35.310 33.330 31.350
## 545 34.800 18.335 32.700 23.980 42.400
## 546 29.700 19.300 29.070 28.025 27.300
## 547 29.925 19.855 22.705 32.965 23.940
dt.imputed$imp$smoker
##       1   2   3   4   5
## 11   no  no  no  no  no
## 20  yes yes yes yes yes
## 54  yes yes yes yes yes
## 61   no  no  no  no  no
## 213  no  no  no  no  no
## 422 yes yes yes yes yes
## 440  no  no  no  no  no
# Since there are 5 imputed data sets, you can select any using complete() function.
dt.completed <- complete(dt.imputed,1)
#str(dt.completed)

Tables

# Frequency for one categorical variable
table(dt.completed$smoker)
## 
##   no  yes 
## 1064  274
# Contingency table of two categorical variables.
tt <- table(dt.completed[c("sex", "smoker")])
tt
##         smoker
## sex       no yes
##   female 547 115
##   male   517 159
# 2 Way Proportion Table 
prop.table(tt)
##         smoker
## sex              no        yes
##   female 0.40881913 0.08594918
##   male   0.38639761 0.11883408
# Add margin totals to your table. 
addmargins(tt)
##         smoker
## sex        no  yes  Sum
##   female  547  115  662
##   male    517  159  676
##   Sum    1064  274 1338
# Three-ways frequency table
ftable(dt.completed$region, dt.completed$sex, dt.completed$smoker)
##                    no yes
##                          
## northeast female  132  29
##           male    125  38
## northwest female  135  29
##           male    132  29
## southeast female  139  36
##           male    134  55
## southwest female  141  21
##           male    126  37

Bar Chart

A bar chart shows the count (or proportion) of observations in each category of a categorical variable. Use it to answer questions like “how many/what share of the sample falls into each group?” and, with a second variable added via fill=, “how does that breakdown change across another category?”

#Install package 'ggplot2'
library(ggplot2)

#Aesthetics tell ggplot what should be on the x-axis, what should be on the y-axis, and what the colors should be.
ggplot(dt.completed,aes(x = smoker)) + 
  geom_bar()

# Add label
# Note: ..count.. is old ggplot2 syntax, still works but is superseded by
# after_stat(count) in ggplot2 >= 3.3.0; both are shown below
ggplot(dt.completed,aes(x = smoker)) + 
  geom_bar()+
  geom_label(aes(x= smoker,label=..count..),
            stat='count', color="black")
## Warning: The dot-dot notation (`..count..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(count)` instead.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

# Modern equivalent of the plot above
ggplot(dt.completed,aes(x = smoker)) + 
  geom_bar()+
  geom_label(aes(x= smoker,label=after_stat(count)),
            stat='count', color="black")

# Change the order of items 
ggplot(dt.completed,aes(x = smoker)) + 
  geom_bar()+
  geom_label(aes(x= smoker,label=..count..),
            stat='count', color="black")+
  scale_x_discrete(limits=c("yes", "no"))

#Using color= or fill= to refer to a categorical variable (called a “discrete” variable in ggplot) allows you to separate the shape by that category.

# Stacked bars
ggplot(dt.completed,aes(x = smoker,fill=sex)) + 
  geom_bar(position = "stack")

# Dodged bars
ggplot(dt.completed,aes(x = smoker,fill=sex)) + 
  geom_bar(position = "dodge")

# Filled bars
ggplot(dt.completed,aes(x = smoker,fill=sex)) + 
  geom_bar(position="fill")+
  scale_fill_manual(values = c("#009999", "darkblue"))

How to read it: the height of each bar is the count in that category — taller means more observations. The three fill=sex versions each answer a slightly different question:

  • Stacked — good for seeing total counts per smoker group while still showing the sex split, but hard to compare sex proportions across bars because the bar segments don’t share a common baseline.
  • Dodged — puts male/female side by side, making it easy to compare raw counts of sex within each smoker group directly.
  • Filled (100%-stacked) — rescales every bar to the same height, showing the proportion of sex within each smoker group rather than counts. Use this when group sizes differ a lot and you care about the ratio, not the raw numbers. In this dataset, if the yes/no bars look roughly 50/50 by sex, that tells you smoking status isn’t strongly associated with sex here — if they were noticeably lopsided, fill position would make that imbalance obvious in a way the stacked version hides.

Correlation matrix with GGally

GGally helps to produces a matrix of scatter plots for visualizing the correlation between variables.

#install.packages("GGally")
library(GGally)
## Warning: package 'GGally' was built under R version 4.6.1
ggpairs(dt.completed[,c(2,4,5,8)], title="correlogram with ggpairs()") 

# ggcorr: correlation matrixes 
ggcorr(dt.completed[,c(2,4,5,8)], label = TRUE, label_size = 4, label_round = 4) 

# ggpairs split by group (smoker)
ggpairs(dt.completed, columns = c(2,4,5,8), ggplot2::aes(colour=smoker)) 

How to read it:

  • ggpairs() grid — each row/column is one numeric variable. The diagonal shows that variable’s own distribution (a density curve); the lower triangle shows pairwise scatterplots; the upper triangle shows the numeric correlation coefficient for that pair (closer to ±1 = stronger linear relationship, closer to 0 = little to no linear relationship, sign tells you the direction). This is a fast way to screen many variable pairs at once before picking specific ones to model or plot in detail.
  • ggcorr() heatmap — the same correlation coefficients as color-coded tiles, typically dark/warm for strong positive correlation, cool for strong negative, and near-white/grey for close to zero. Scan for the darkest tiles first — those are the pairs worth investigating further with a scatterplot.
  • Splitting by smoker — a correlation that looks weak overall can be much stronger within a subgroup (or vice versa). If bmi and charges correlate more strongly among smokers than non-smokers, that’s evidence the relationship between BMI and medical cost depends on smoking status — a clue that an interaction effect might matter in a later model.
  • Caveat: correlation coefficients only capture linear relationships and are sensitive to outliers — always look at the actual scatterplot, not just the number, before concluding two variables are (un)related.

Scatterplot

A data display that shows the relationship between two numerical variables.

# Scatterplot between two quantitative variables 
ggplot(dt.completed,aes(x=bmi,y=charges))+
  geom_point()+
  theme_bw()

# Shaped by sex
ggplot(dt.completed,aes(x=bmi,y=charges))+
  geom_point(aes(shape=sex))+
  theme_bw()

# Colored by sex
ggplot(dt.completed,aes(x=bmi,y=charges))+
  geom_point(aes(col=sex))+
  theme_bw()

# Change the point size by children
ggplot(dt.completed,aes(x=bmi,y=charges))+
  geom_point(aes(col=sex,size=children))+
  theme_bw()

# Facet wrap to create multiple plots by categorical variable(s)
ggplot(dt.completed,aes(x=bmi,y=charges))+
  geom_point(aes(col=sex))+
  theme_bw()+
  facet_wrap(region ~ .)

# Grid by region and smoker 
ggplot(dt.completed,aes(x=bmi,y=charges))+
  geom_point(aes(col=sex))+
  theme_bw()+
  facet_grid(cols = vars(region),rows=vars(smoker))+
   theme(legend.position="top")#change the position of label

# Grid by region and sex
pl1 <- ggplot(dt.completed,aes(x=bmi,y=charges))+
  geom_point(aes(col=smoker))+
  theme_bw()+
  facet_grid(cols = vars(region),rows=vars(sex))+
  theme(legend.position="top")
pl1

# Add regression lines
pl1+ geom_smooth(method=lm, aes(fill=smoker))
## `geom_smooth()` using formula = 'y ~ x'

How to read it: each point is one observation, placed by its bmi (x) and charges (y). Three things to look for:

  • Direction and strength — points trending up-and-to-the-right suggest a positive relationship (higher BMI, higher charges); a tight, narrow band means a strong relationship, while a wide scattered cloud means a weak one.
  • Shape — is the trend a straight line, a curve, or is there no visible pattern at all? A linear regression line (geom_smooth(method=lm)) is only an appropriate summary if the underlying trend actually looks straight.
  • Clusters and subgroups — this is the most important thing the colored/faceted versions reveal here: plotting charges vs bmi for the whole sample looks like a messy cloud with a weak trend, but once you color by smoker, two distinct bands typically separate out — non-smokers cluster at lower charges regardless of BMI, while smokers’ charges climb steeply with BMI. That’s a classic case where an overall correlation understates what’s really going on: the relationship between BMI and cost is conditional on smoking status, not a single global trend. Faceting by region on top of that lets you check whether this pattern holds consistently across regions or is driven by one area.
  • geom_smooth(method=lm) adds a fitted regression line with a shaded 95% confidence band; a narrow band means the line is estimated precisely, a wide band means more uncertainty (often because there are fewer points in that facet).

Boxplot

A standardized way of displaying the distribution of numerical data based on a five number summary (min, first quartile (Q1), median, third quartile (Q3), and max)

# Boxplot of charges by sex
 ggplot(dt.completed,aes(x=sex,y=charges))+
  geom_boxplot()+
  theme_bw()

# Colored by smoker
ggplot(dt.completed,aes(x=sex,y=charges))+
  geom_boxplot(aes(col=smoker))+
  coord_flip()+
  theme_bw()

# Grided by region
p <- ggplot(dt.completed,aes(x=sex,y=charges,fill=smoker))+
  geom_boxplot()+
  theme_bw()+
  facet_grid(cols = vars(region))

# Box plot with dot plot;  visualize the data on boxplot
p + geom_point(position=position_jitterdodge(),alpha=0.3) +
  theme_bw()

How to read it: the box spans the interquartile range (IQR) — the middle 50% of the data, from Q1 to Q3 — with a line at the median. The whiskers typically extend to the most extreme point within 1.5×IQR of the box, and any points beyond that are plotted individually as potential outliers.

  • Comparing medians across boxes tells you whether the typical value differs between groups — e.g. comparing charges by smoker here, the smokers’ box usually sits noticeably higher than the non-smokers’ box, meaning the typical smoker pays substantially more, not just a few extreme cases.
  • Comparing box height (IQR) and whisker length tells you about spread/variability — a taller box or longer whiskers means more variation within that group. If the smoker group’s box is both higher and taller than the non-smoker group’s, that means smokers not only pay more on average but their charges are also more variable and less predictable.
  • Outlier points above the top whisker are worth a closer look — in this dataset they often correspond to smokers with high BMI, which is exactly the interaction the scatterplot section above surfaces too.
  • Faceting by region lets you check whether the smoker/non-smoker gap in charges is consistent everywhere, or whether one region drives it.
  • The jittered dot overlay (last plot) shows every individual observation on top of the summary — useful for confirming the boxplot isn’t hiding a bimodal or oddly shaped distribution within a group.

Histogram

A graph that shows the frequency of numerical data using rectangles.

# Basic histogram
ggplot(dt.completed, aes(x=charges)) + 
  geom_histogram()+
  theme_bw()
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.

# Add mean line
ggplot(dt.completed, aes(x=charges)) + 
  geom_histogram()+
  geom_vline(aes(xintercept=mean(charges)),color="blue", 
             linetype="dashed", size=1)+
  theme_bw()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.

# Histogram plot colors by groups
ggplot(dt.completed, aes(x=charges, fill=smoker,color=smoker)) +
  geom_histogram( alpha=0.5, position="identity")+
  theme_bw()
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.

# Combine histogram and density plots :
# (as noted earlier, ..density.. can also be written as after_stat(density))
ggplot(dt.completed, aes(x=charges, fill=smoker,color=smoker)) +
  geom_histogram(aes(y=..density..),alpha=0.5, position="identity")+
  geom_density(alpha=0.6)+
  theme_bw()
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.

ggplot(dt.completed, aes(x=charges, fill=smoker,color=smoker)) +
  geom_histogram(aes(y=..density..),alpha=0.5, position="identity")+
  geom_density(alpha=0.6)+
  theme_bw()+
  facet_grid(cols = vars(region),rows=vars(sex))
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.

How to read it: the x-axis is binned values of charges, and bar height is how many observations fall in each bin — so the overall shape of the bars is the distribution.

  • Skew — the plain histogram of charges is a good example of a right-skewed (positively skewed) distribution: a tall cluster of relatively low values with a long tail stretching toward high values. This matters practically because it means the mean (the dashed blue line) sits noticeably to the right of most people’s actual costs — a small number of very expensive cases pull the average up, so the mean alone would overstate what a “typical” person pays. This is exactly why the median (visible on the boxplot above) is often a more honest summary for skewed cost/income-type data than the mean.
  • Modality — a single smooth hump is unimodal; two or more distinct humps (multimodal) usually means the data is a mix of different subgroups that behave differently. Splitting the histogram by smoker typically reveals this: what looked like one skewed distribution overall resolves into two separate, more coherent shapes — non-smokers clustered at lower charges, smokers shifted higher — which is the same story the boxplot and scatterplot sections tell from different angles.
  • Histogram vs. density curve — the density curve (geom_density) is a smoothed version of the same shape, useful for comparing groups of different sizes since it’s rescaled to have equal area regardless of sample size, whereas raw histogram bar heights depend on how many observations are in each group.

Normal Q-Q Plot

A normal probability plot is used to check if the given data set is normally distributed or not. It is used to compare a data set with the normal distribution. we use the provided stat_qq_ functions to construct a complete Q-Q plot with the points, reference line, and the confidence bands. As default, the standard Q-Q Normal plot with Normal confidence bands is constructed:

#install.package(qqplotr)
#library(ggplot2)
library(qqplotr)
## Warning: package 'qqplotr' was built under R version 4.6.1
## 
## Attaching package: 'qqplotr'
## The following objects are masked from 'package:ggplot2':
## 
##     stat_qq_line, StatQqLine
ggplot(dt.completed, aes(sample = charges))+
    stat_qq_point()+
    stat_qq_line()+
    labs(x = "Theoretical Quantiles", y = "Sample Quantiles")+
    theme_bw()

ggplot(dt.completed, aes(sample = charges))+
    stat_qq_point()+
    stat_qq_line()+
    labs(x = "Theoretical Quantiles", y = "Sample Quantiles")+
    theme_bw()+
    facet_grid(cols = vars(region),rows=vars(smoker))

How to read it: each point compares one observed value’s quantile against the quantile it would have if the data were perfectly normally distributed. If the data really is approximately normal, the points fall closely along the diagonal reference line.

  • Points hugging the line = consistent with normality — safe to use methods that assume a normal distribution (e.g. standard t-tests, many regression diagnostics).
  • Points curving away from the line at one or both ends = the tails of the distribution are heavier or lighter than a normal distribution’s, i.e. more extreme values than a bell curve would produce.
  • An upward-bending curve (points below the line on the left, above it on the right) is the signature of right skew — which is exactly what we’d expect here, since the histogram above already showed charges is right-skewed. Seeing that same skew confirmed in the Q-Q plot is a useful cross-check between the two.
  • Practical takeaway: this is usually the deciding evidence for whether to transform a variable (e.g. log(charges)) before fitting a linear model, since many standard statistical methods lean on a normality assumption that a skewed variable like raw charges violates. The facet_grid version lets you check whether that skew is present in every region/smoker subgroup or concentrated in a few.

6. Getting help and further resources

Getting help inside R

?mean            # opens the help page for a function
help(mean)        # same as above
??"linear model"  # searches help pages for a phrase
example(mean)      # runs the worked examples from the help page
vignette(package = "dplyr")  # lists long-form guides bundled with a package

When something errors, copy the exact error message into a search engine along with the package/function name — R’s error messages are usually specific enough that someone else has hit (and solved) the same issue.

Where to learn more

Reproducibility tips

  • Keep each analysis in its own RStudio Project (.Rproj) so working directories and file paths stay predictable and portable, as discussed in Section 4.
  • Record the exact package versions used to produce a report with sessionInfo(), so results can be reproduced later even if packages are updated:
sessionInfo()
## R version 4.6.0 (2026-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=English_United States.utf8 
## [2] LC_CTYPE=English_United States.utf8   
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.utf8    
## 
## time zone: Asia/Bangkok
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] qqplotr_0.0.7 GGally_2.4.0  ggplot2_4.0.3 mice_3.19.0   dplyr_1.2.1  
## [6] skimr_2.2.2  
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6          shape_1.4.6.1         xfun_0.57            
##  [4] bslib_0.11.0          twosamples_2.0.1      caTools_1.18.4       
##  [7] lattice_0.22-9        bitops_1.1-0          vctrs_0.7.3          
## [10] tools_4.6.0           Rdpack_2.6.6          generics_0.1.4       
## [13] parallel_4.6.0        pbmcapply_1.5.1       tibble_3.3.1         
## [16] DEoptimR_1.2-0        pan_2.0               pkgconfig_2.0.3      
## [19] jomo_2.7-6            Matrix_1.7-5          RColorBrewer_1.1-3   
## [22] S7_0.2.2              lifecycle_1.0.5       compiler_4.6.0       
## [25] farver_2.1.2          stringr_1.6.0         repr_1.1.7           
## [28] codetools_0.2-20      htmltools_0.5.9       sass_0.4.10          
## [31] yaml_2.3.12           glmnet_5.0            pracma_2.4.6         
## [34] pillar_1.11.1         nloptr_2.2.1          jquerylib_0.1.4      
## [37] tidyr_1.3.2           MASS_7.3-65           cachem_1.1.0         
## [40] reformulas_0.4.4      iterators_1.0.14      rpart_4.1.27         
## [43] boot_1.3-32           foreach_1.5.2         mitml_0.4-5          
## [46] robustbase_0.99-7     nlme_3.1-169          ggstats_0.13.0       
## [49] tidyselect_1.2.1      digest_0.6.39         stringi_1.8.7        
## [52] purrr_1.2.2           labeling_0.4.3        splines_4.6.0        
## [55] fastmap_1.2.0         grid_4.6.0            cli_3.6.6            
## [58] qqconf_1.3.2          magrittr_2.0.5        base64enc_0.1-6      
## [61] survival_3.8-6        broom_1.0.13          withr_3.0.2          
## [64] opdisDownsampling_1.6 scales_1.4.0          backports_1.5.1      
## [67] rmarkdown_2.31        nnet_7.3-20           lme4_2.0-1           
## [70] evaluate_1.0.5        knitr_1.51            rbibutils_2.4.1      
## [73] doParallel_1.0.17     mgcv_1.9-4            rlang_1.2.0          
## [76] Rcpp_1.1.1-1.1        glue_1.8.1            rstudioapi_0.18.0    
## [79] minqa_1.2.8           jsonlite_2.0.0        R6_2.6.1
  • For projects that need long-term reproducibility (e.g. a thesis or a published paper), consider the renv package, which snapshots the exact package versions used so the project can be rebuilt identically on another machine.