This completed example follows the Discussion 10/16 exercises. It shows the R code, the output produced by that code, and a short interpretation of each result. All numerical answers are calculated from the supplied automobile data.

To reproduce the report: keep this .Rmd, automobile_26.RDS, and automobile_26.csv in the same folder. Open the .Rmd in RStudio and click Knit. R Markdown uses the document’s folder as the working directory while knitting, so no computer-specific setwd() path is needed. For running chunks interactively, first choose Session > Set Working Directory > To Source File Location in RStudio. The analysis uses base R; rmarkdown and knitr are needed to render the report.

1 Import and inspect the data

The RDS file stores an R object, including its attributes. A CSV stores text that R must interpret when it imports the file. Blank CSV fields are treated as missing values using na.strings = "".

auto <- readRDS(file = "automobile_26.RDS")
auto2 <- read.csv(
  file = "automobile_26.csv",
  na.strings = "",
  stringsAsFactors = FALSE
)

# Number of rows and columns in each import.
dim(auto)
## [1] 74  6
dim(auto2)
## [1] 74  6
# First six observations and a summary of all variables.
head(auto)
##          name price mpg weight length foreign
## 1 AMC Concord  4099  22   2930    186      No
## 2   AMC Pacer  4749  17   3350    173      No
## 3  AMC Spirit  3799  22     NA    168      No
## 4   Audi 5000  9690  17   2830    189     Yes
## 5    Audi Fox  6295  23   2070    174     Yes
## 6    BMW 320i  9735  25   2650    177     Yes
summary(auto)
##      name               price            mpg            weight         length     
##  Length:74          Min.   : 3291   Min.   :12.00   Min.   :1760   Min.   :142.0  
##  Class :character   1st Qu.: 4220   1st Qu.:18.00   1st Qu.:2348   1st Qu.:170.0  
##  Mode  :character   Median : 5006   Median :20.00   Median :3235   Median :190.5  
##                     Mean   : 6165   Mean   :21.38   Mean   :3066   Mean   :187.6  
##                     3rd Qu.: 6332   3rd Qu.:25.00   3rd Qu.:3675   3rd Qu.:203.8  
##                     Max.   :15906   Max.   :41.00   Max.   :4840   Max.   :233.0  
##                                     NA's   :3       NA's   :6      NA's   :4      
##    foreign         
##  Length:74         
##  Class :character  
##  Mode  :character  
##                    
##                    
##                    
## 

The RDS data contain 74 cars and 6 variables: name, price, mpg, weight, length, and foreign. Prices are in dollars, weights in pounds, and lengths in inches.

2 Compare the RDS and CSV imports

2.1 Exact comparison and near-equality

identical(auto, auto2)
## [1] FALSE
all.equal(auto, auto2)
## [1] "Attributes: < Component \"class\": Lengths (3, 1) differ (string compare on first 1) >"
## [2] "Attributes: < Component \"class\": 1 string mismatch >"

identical() returns FALSE. Here, all.equal() also reports differences instead of returning TRUE: the imported objects have different class attributes. Inspect their classes and column storage types to see why.

class(auto)
## [1] "tbl_df"     "tbl"        "data.frame"
class(auto2)
## [1] "data.frame"
rbind(
  RDS = vapply(auto, typeof, character(1)),
  CSV = vapply(auto2, typeof, character(1))
)
##     name        price     mpg       weight    length    foreign    
## RDS "character" "double"  "double"  "double"  "double"  "character"
## CSV "character" "integer" "integer" "integer" "integer" "character"

The RDS preserves a tibble, which also inherits from data.frame. The CSV imports as a plain data.frame. The four quantitative columns are stored as double in the RDS and integer in the CSV.

Converting the tibble to a plain data frame makes the container classes match:

all.equal(as.data.frame(auto), auto2)
## [1] TRUE
identical(as.data.frame(auto), auto2)
## [1] FALSE

Now all.equal() returns TRUE: the values agree. identical() remains FALSE because the numeric storage types still differ. Unlike identical(), all.equal() normally tolerates the difference between integer and double representations of the same numbers. It returns either TRUE or diagnostic text; use isTRUE(all.equal(x, y)) if you need a single logical answer.

3 Transform prices and examine numerical precision

auto$price_thousands <- auto$price / 1000

# The fourth car's price, in dollars and in thousands of dollars.
auto$price[4]
## [1] 9690
auto$price_thousands[4]
## [1] 9.69
# Request more significant digits.
print(auto$price_thousands[4], digits = 20)
## [1] 9.6899999999999995026
# Request exactly 20 places after the decimal point.
sprintf("%.20f", auto$price_thousands[4])
## [1] "9.68999999999999950262"

For Audi 5000, the price is $9,690, or 9.69 thousand dollars. The long decimal expansion reflects binary floating-point representation; it does not mean the recorded price is wrong. digits = 20 requests significant digits, while sprintf("%.20f", ...) requests 20 decimal places.

4 Work with missing weights

4.1 What happens without removing missing values?

summary(auto$weight)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##    1760    2348    3235    3066    3675    4840       6
sum(is.na(auto$weight))
## [1] 6
mean(auto$weight)
## [1] NA
sd(auto$weight)
## [1] NA

Both the mean and standard deviation return NA, because the vector contains missing observations and these functions do not ignore them by default.

4.2 Compute summaries using the observed weights

weight_n <- sum(!is.na(auto$weight))
weight_mean <- mean(auto$weight, na.rm = TRUE)
weight_sd <- sd(auto$weight, na.rm = TRUE)

data.frame(
  nonmissing_n = weight_n,
  missing_n = sum(is.na(auto$weight)),
  mean_weight_lb = weight_mean,
  sample_sd_lb = weight_sd
)
##   nonmissing_n missing_n mean_weight_lb sample_sd_lb
## 1           68         6       3066.176     783.5892

There are 6 missing weights and 68 observed weights. The mean observed weight is 3066.18 pounds, and the sample standard deviation is 783.59 pounds.

na.rm = TRUE ignores missing values for that calculation; it does not delete rows from auto. The sd() function computes the sample standard deviation, using n - 1 in its denominator.

5 Create a logical variable and a numeric indicator

auto$foreign_YES <- auto$foreign == "Yes"
auto$foreign_YES_ind <- as.numeric(auto$foreign_YES)

head(auto[c("foreign", "foreign_YES", "foreign_YES_ind")])
##   foreign foreign_YES foreign_YES_ind
## 1      No       FALSE               0
## 2      No       FALSE               0
## 3      No       FALSE               0
## 4     Yes        TRUE               1
## 5     Yes        TRUE               1
## 6     Yes        TRUE               1
table(auto$foreign, useNA = "ifany")
## 
##   No  Yes <NA> 
##   49   22    3
mean(auto$foreign_YES, na.rm = TRUE)
## [1] 0.3098592
mean(auto$foreign_YES_ind, na.rm = TRUE)
## [1] 0.3098592
foreign_yes_n <- sum(auto$foreign_YES, na.rm = TRUE)
foreign_known_n <- sum(!is.na(auto$foreign_YES))
foreign_proportion <- mean(auto$foreign_YES, na.rm = TRUE)

The comparison produces TRUE for "Yes", FALSE for "No", and NA for a missing value. as.numeric() converts these to 1, 0, and NA.

Among the 71 cars with known foreign status, 22 are foreign-made. The proportion is 0.3099, or 30.99%. The two means agree because R treats logical TRUE as 1 and FALSE as 0 in this calculation. Missing foreign status is unknown, not a recorded "No".

6 Identify suspected MPG outliers using the IQR rule

First obtain the quartiles from the data. Computing them avoids relying on rounded values copied from a summary.

summary(auto$mpg)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##   12.00   18.00   20.00   21.38   25.00   41.00       3
mpg_quartiles <- quantile(auto$mpg, probs = c(0.25, 0.75), na.rm = TRUE)
q1 <- unname(mpg_quartiles[1])
q3 <- unname(mpg_quartiles[2])
mpg_iqr <- q3 - q1
lower_fence <- q1 - 1.5 * mpg_iqr
upper_fence <- q3 + 1.5 * mpg_iqr

data.frame(
  Q1 = q1,
  Q3 = q3,
  IQR = mpg_iqr,
  lower_fence = lower_fence,
  upper_fence = upper_fence
)
##   Q1 Q3 IQR lower_fence upper_fence
## 1 18 25   7         7.5        35.5

Q1 is 18 MPG, Q3 is 25 MPG, and the IQR is 7 MPG. The lower and upper fences are 7.5 and 35.5 MPG. Values strictly outside these fences are flagged as suspected outliers.

iqr_flag <- auto$mpg < lower_fence | auto$mpg > upper_fence
iqr_rows <- which(iqr_flag)

# Original row numbers and their MPG values.
iqr_rows
## [1] 72
auto$mpg[iqr_rows]
## [1] 41
# Identify the corresponding vehicles.
data.frame(
  row = iqr_rows,
  name = auto$name[iqr_rows],
  mpg = auto$mpg[iqr_rows]
)
##   row      name mpg
## 1  72 VW Diesel  41

The IQR rule flags row 72: VW Diesel, with 41 MPG.

7 Understand logical subsetting when values are missing

# NA MPG observations also produce NA values in the logical condition.
which(is.na(auto$mpg))
## [1] 12 23 50
# Direct logical indexing retains an NA for each unknown selection.
auto$mpg[iqr_flag]
## [1] NA NA NA 41
# which() returns only the positions that are definitely TRUE.
auto$mpg[which(iqr_flag)]
## [1] 41
# An equivalent logical approach explicitly excludes missing conditions.
auto$mpg[!is.na(iqr_flag) & iqr_flag]
## [1] 41

Direct logical indexing produces three NA entries and the value 41. The NA entries come from MPG values that could not be evaluated against the outlier condition. They are not additional confirmed outliers. Both which() and the explicit nonmissing condition select only the confirmed TRUE entries.

8 Identify suspected MPG outliers using z-scores

A z-score is (value - sample mean) / sample standard deviation. The mean and standard deviation must be calculated using nonmissing MPG observations.

mpg_mean <- mean(auto$mpg, na.rm = TRUE)
mpg_sd <- sd(auto$mpg, na.rm = TRUE)
auto$mpg_Z <- (auto$mpg - mpg_mean) / mpg_sd

mpg_mean
## [1] 21.38028
mpg_sd
## [1] 5.863607
# Either condition tests whether a z-score is beyond -3 or +3.
z_rows <- which(auto$mpg_Z < -3 | auto$mpg_Z > 3)
z_rows
## [1] 72
which(abs(auto$mpg_Z) > 3)
## [1] 72
auto$mpg[z_rows]
## [1] 41
data.frame(
  row = z_rows,
  name = auto$name[z_rows],
  mpg = auto$mpg[z_rows],
  z_score = auto$mpg_Z[z_rows]
)
##   row      name mpg  z_score
## 1  72 VW Diesel  41 3.346015
# Check whether the two rules identify the same observations here.
identical(iqr_rows, z_rows)
## [1] TRUE

The sample mean MPG is 21.3803 and the sample standard deviation is 5.8636. The z-score rule also flags VW Diesel at 41 MPG, with z-score 3.3460.

The two methods agree for these data. They need not agree in general: their cutoffs use different summaries of the distribution. A suspected outlier is a value to investigate, not automatically an error to remove.

9 Results at a glance

Quantity Result
Cars in the dataset 74
Observed / missing weights 68 / 6
Mean observed weight 3066.18 pounds
Sample SD of observed weight 783.59 pounds
Foreign-made among cars with known status 22 / 71 = 30.99%
MPG IQR fences 7.5 to 35.5 MPG
IQR outlier Row 72: VW Diesel (41 MPG)
Z-score outlier Row 72: VW Diesel (z = 3.3460)

The HTML report contains both the code and its executed results. You can use Print > Save as PDF in a browser if you also want a PDF copy.