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.
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.
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.
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.
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.
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.
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".
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.
# 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.
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.
| 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.