December 13th, 2020
(Blog Post 13 || Final Project)
For my Final Project, I decided to pick up a project I'd abruptly ended a few years ago. The solution involves mining insider trading data from the SEC's publicly available EDGAR database. My original project was written in C#, so I did not bother with porting the existing code. Instead, I simply tried to remember as much of the process involved as I could without looking at the original code. As demonstrated below, this approach worked out well.
The script is available in the following Gist: https://gist.github.com/blarosen95/e36aaff51e7bf21d74bd186a7b960a90
A basic overview of the process in play is as follows:
First, the user must enter a stock's ticker symbol. This value is then stored in a variable in its lower-case equivalent. The script then searches as very large (seriously, I would not recommend opening the ticker.txt URL on line 22 unless you have at least 10GB of free RAM) crosswalk for this symbol in order to retrieve the equivalent Central Index Key (CIK) value.
From here on, it gets a bit more complicated. The next step involves pulling the transaction records for the given CIK. This is done with RCurl, and then munged to just the desired table element from the HTML file à la XML and rlist. The resulting data frame is further cleaned by dropping 3 undesired columns, followed by filtering on the Transaction Type column for Sales and Purchases using sqldf. It's worth noting that the maximum number of rows is 80 due to the target site's server-sided pagination feature.
Next, the most frequent trader is determined by sorting on the Owner CIK column. This column is preferred over Reporting Owner, as reporting owners' names may change due to events such as marriage but their personal CIK remains the same. At this point a new data frame is constructed using sqldf. The data frame is built to include only three, relevant columns: shares (the number of securities held by the entity after the transaction designated in a given row), dated (the date the transaction was filed on), and direction (the direction of the trade, A for acquisition and D for disposition).
Before continuing to detail the remainder of the process, some additional context is necessary. The filing date for a given insider's transaction is not (necessarily) the date the trade takes place, the SEC requires these individuals file for each transaction in advance as they have inside knowledge into the near-term direction of the company's stock. A few steps are taken in the remainder of the process to potentially turn this limitation into the user's benefit. Additionally, narrowing the data down to the most frequently transacting individual already helped to soften some of the impact of this limitation.
The next step is plotting the given individual's transactions on a scatter-plot. The number of shares held is plotted against the filing date. Additionally, the points are distinguished using one color for acquisitions and another for dispositions. Examples of this will be towards the end of this writing, in a section dedicated to the results of such back-testing.
The final step involves determining the price per share on the date the transaction was filed as well as on both the next market day (short-term reliability) and the most recent market day (as close to longer-term as is possible to retrieve without forecasting stock prices). This was one of the more complicated steps. It involves using curl to download the HTML contents of a webpage from MarketWatch, interpolating the given market dates and ticker symbol into the URL. The resulting information is used to calculate 2 different scores (one short-term, one longer-term) for each acquisition or disposition reported by the most frequently transacting insider.
A score of 0 occurs when there is no change in share price between the filing and short/longer term market dates. This is not as rare as it seems; insiders can file to trade on days when the market is closed, so the date of filing ends up being calculated as the next market day, and the next market day is still calculated as the first market day after the original filing date.
For dispositions, a score of -1 occurs when the share price increased at close of the next (with respect to short or longer term) market date; this would score as a 1 for acquisitions, as the individual paid less on the date filed. For dispositions, a score of 1 occurs when the share price decreased at close of the next date; this would score as a -1 for acquisitions, as the individual paid more.
This seems like it would be about as simple as pulling data from the SEC was, but it was in fact much more difficult. The following image shows a screenshot of the value desired from MarketWatch (the dollar figure to the right of "Closing Price:")
The closing price (along with the other price figures) are not text values, they are each their own image. Upon its discovery, I planned to work around this issue by using some form of OCR package. In the past, I've utilized OCR in Python to solve very large math problems from websites that limited me on submission time as well as providing the equation in a hard-to-read captcha style. However, I quickly realized this wouldn't be necessary here. The following HTML snippet shows the source for the quote data section (as was shown in the previous screenshot):
<div id="quotedata"> <span>Tuesday, December 01, 2020</span> <div> <img src="http://www.marketwatch.com/tools/investing/AMZN/number?countrycode=US&data=80.022,3$"/> <span>Closing price:</span> </div> <div> <img src="http://www.marketwatch.com/tools/investing/AMZN/number?countrycode=US&data=05.881,3$"/> <span>Open:</span> </div> <div> <img src="http://www.marketwatch.com/tools/investing/AMZN/number?countrycode=US&data=59.842,3$"/> <span>High:</span> </div> <div> <img src="http://www.marketwatch.com/tools/investing/AMZN/number?countrycode=US&data=81.751,3$"/> <span>Low:</span> </div> <div> <img src="http://www.marketwatch.com/tools/investing/AMZN/number?countrycode=US&data=M45.4"/> <span>Volume:</span> </div> </div>
The URL for the image associated with the closing price has a parameter called data (at the end of the URL). As can be proven by changing the numbers in the data parameter's value, this value actually creates the content of the image, the image is not pre-existing. More importantly, the image is created using this string in reverse. All I needed to do was munge out all the other data, reverse the URL, and munge it down to just the dollar figure. This is performed in the getPrice function (starts line 70).
The following figures are the results of back-testing this script against Sinclair Broadcast Group Incorporated (SBGI), Marathon Oil Corporation (MRO), and Amazon (AMZN).

The above figures for SBGI demonstrate a 100% longer-term reliability for David Smith's trades (whom I believe is their CEO), but only ~34.48% day-trading reliability. However, the immense volume of transactions from March 17th appropriately depicts a common trend among insiders of cashing in on the impact of COVID-19 on their own companies.

The above figures for MRO's CEO, Lee Tillman, demonstrate only a 50% day-trading reliability but 100% reliability for longer-term. Worth mention is that Tillman neither purchased nor sold any shares in MRO from mid 2018 (a sale) to the point when the pandemic knocked the price from ~$11 to ~$4; this would be important information to have but cannot always exist (mostly due to the previously mentioned pagination limit of 80 records). Tillman's shares are worth roughly $4 million (about 50%) more now than they were on his last transaction.

Finally, Amazon's Jeff Wilkes is the most frequently transacting insider for the companies past 80 insider transactions. Scoring a ~16% for short-term reliability and a 92% for longer-term reliability, there's not as much to say about the results from back-testing the project's current state against Amazon. However, this will be covered in part in the next and final section of this writing.
Closing Thoughts
The astute reader may have noticed that some plots depict handfuls of transactions in a single direction without enough in the opposite direction to account for an insider's change in total equity. This is accounted for in other forms of transactions such as gifts and awards, which were filtered out when the data set was narrowed down to purchases and sales only.
The results of this project would do well from further back-testing once the pandemic's effect on the US economy winds down. Of course insiders are buying into their company when the pandemic has pushed just about everyone lower; they will likely correct after the pandemic has settled. However, some insiders might be failing to factor in the fact that their business, as are all others, is a finite game with regards to game theory; Yes, the pandemic will end and the economy will eventually recover, but plenty of companies may no longer exist by then.
Finally, the program is fairly slow; these tables take minutes to generate. This is almost all due to the use of a for loop. From what I've read in the past, R does not perform for loops efficiently. There are alternative approaches available in the R language but this script would likely do best in another language (Python, Ruby, etc.). Further analyzing the results of this script (i.e. time series and forecasting)would likely do well in R, however, as that is what R is designed for.
November 15th, 2020
(Blog Post 12)
This week's assignment explores time series and exponential smoothing. More specifically, the exponential smoothing model utilized is Holt-Winters.
In order to construct the required time series, I wrote the following code:
time <- paste(month.abb) # Lifehack
a <- c(31.9, 27, 31.3, 31, 39.4, 40.7, 42.3, 49.5, 45, 50, 50.9, 58.5)
b <- c(39.4, 36.2, 40.5, 44.6, 46.8, 44.7, 52.2, 54, 48.8, 55.8, 58.7, 63.4)
df <- data.frame(a, b)
colnames(df) <- c('2012', '2013')
charge_series <- ts(df)
As shown in the snippet above, I first had to reconstruct the dataframe. I accomplished this with the built-in constant month.abb (which is not used directly in either the dataframe or the time series but will show up later on in the code), as well as the two quantitative columns. After setting the column names, I instantiate a time series, charge_series, using the ts() method.
In order to complete the rest of the assignment, as well as plot the time series with the forecast, I wrote the following code:
charge_forecast <- HoltWinters(charge_series, beta = F, gamma = F)
charge_forecast
df$forecast <- charge_forecast$fitted[1:12, 1]
cf_series <- ts(df)
plot.ts(cf_series, axes = F, main = 'Charges', plot.type = 'single', col = rainbow(3), ylab='Charged')
axis(2)
axis(1, labels = time, at = seq(from = 1, by = 1, length.out = 12))
box()
legend("top", bty = 'n', lty = c(1, 1), col = rainbow(3), legend = c('2012', '2013', 'Forecasted'))
As for the outcome of the Holt-Winters model itself, the SSE is 835.38; quite high, suggesting quite a bit of variance. However, the results of both this time series and the smoothing model are much more easily interpreted once visualized. The following plot does just that.

The plot type is univariate for a few reasons. First, the data allows for it, especially with different colors for each line along with a corresponding legend. Additionally, multivariate plotting did not look as great with plot.ts() as it did with ts.plot() (which had each plot separated by a horizontal line but did not work out for getting the majority of the formatting achieved under plot.ts()). Finally, I ended up preferring this univariate plot over the multivariate plot available with built-in methods. Normally I would probably plot this data multivariate but continuing along the X-axis rather than the Y-axis; this sort of bucketing/drill-down is not (at least not immediately) apparent in plot.ts().
As is demonstrated by the plot, the forecast data is very similar to 2012, and only slightly more similar to 2013 than 2012's data was. As a closing note, this plot clarifies the fact that all three years have a moderately established month-to-month trend in direction as well as strength of eachchange.
November 8th, 2020
(Blog Post 11)
This week's assignment explore chi-squared testing in the R language.
The following is the script that I wrote in order to solve this assignment:
# Title : Module 11 Assignment
# Objective : Chi Square Tests and Visualization in R
# Created by: Blake Rosenberg
# Created on: 11/8/2020
library(lattice)
vec_nominals <- c('Would choose same hotel again', 'Wouldn\'t choose same hotel again')
vec_beachcomber <- c(163, 64)
vec_windsurfer <- c(154, 108)
vec_colnames <- c('Beachcomber', 'Windsurfer')
hotel_df <- data.frame(vec_beachcomber, vec_windsurfer, row.names = vec_nominals)
colnames(hotel_df) <- vec_colnames
print.data.frame(hotel_df)
chisq.test(hotel_df)
boxplot(hotel_df)
The chi-squared test is performed on the dataframe formed by combining the various vectors. The results are as detailed below:
Pearson's Chi-squared test with Yates' continuity correction
data: hotel_df
X-squared = 8.4903, df = 1, p-value = 0.00357
In addition, we visualized the data from this assignment. I opted to use the boxplot function from the lattice package. The following image is the result of plotting the dataframe.

The main question for this assignment was whether the degrees of freedom was greater or less than 4; the degrees of freedom in this data is less than 4, sitting at 1.
November 1st, 2020
(Blog Post 10)
This week's assignment focuses on ANOVA testing in R language.
The following is the code that I wrote in R in order to solve this assignment:
# Title : Module 10 Assignment
# Objective : ANOVA testing
# Created by: Blake Rosenberg
# Created on: 10/30/2020
high <- c(10, 9, 8, 9, 10, 8)
moderate <- c(8, 10, 6, 7, 8, 8)
low <- c(4, 6, 6, 4, 2, 2)
migraine <- data.frame(high, moderate, low)
migraine
# This is just to distinguish the previous output from the final output
print(paste0(rep("-", 20), collapse=''))
res <- aov(values ~ ind, data = stack(migraine))
summary(res)
As shown above, I begin by assigning the columns from the assignment's table to three vectors: high, moderate, low. I then instantiate a dataframe, migraine, using these three vectors (I could've declared the vectors in-line, but wanted to aid in legibility as this is already a short script).
Finally, I run an ANOVA test on the stacked vectors from the migraine dataframe, storing the value in res, using the formula values ~ ind. The summary of the test's results are displayed using the summary method.
The following is an explanation for the reported summary of the test:
- Df: As there are 3 indices, the degrees of freedom here is 3-1 == 2. The degrees of freedom for the residuals is the number of residuals minus the indices (18 - 3 == 15).
- The summary then lists the sum of squares and the mean of squares for the indices and residuals as 82.11, 41.06, 28.83, and 1.92, respectively.
- With an F value of 21.36 and a Probability of exceeding F of 4.08 * 10^-5, there is enough evidence to reject a given null hypothesis as these results are statistically significant.
October 21st, 2020
(Blog Post 9)
I began this assignment by declaring the values of two vectors, x and y as follows:
x <- c(9, 8, 3, 8, 10)
y <- c(3, 1, 2, 6, 4, 3, 6)
The following line of R code determines the one sample t-test statistic for x:
t.test(x)
The output is as follows:
One Sample t-test
data: x
t = 6.2898, df = 4, p-value = 0.003264
alternative hypothesis: true mean is not equal to 0
95 percent confidence interval:
4.245209 10.954791
sample estimates:
mean of x
7.6
The following line of R code determines the Welch two sample t-test statistic for x and y:
t.test(x, y)
The output is as follows:
Welch Two Sample t-test
data: x and y
t = 2.8651, df = 6.7687, p-value = 0.02505
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
0.6805904 7.3765524
sample estimates:
mean of x mean of y
7.600000 3.571429
I wrote the following code in R to address the main assignment's questions:
x.xbar <- mean(x)
y.xbar <- mean(y)
x.n <- length(x)
y.n <- length(y)
dof <- x.n + y.n - 2
x.sigma <- sd(x)
y.sigma <- sd(y)
isVarianceEqual <- x.sigma^2 / y.sigma^2 < 4.5337
sigma_pool <- sqrt(((x.n - 1) * x.sigma^2 + (y.n - 1) * y.sigma^2) / (x.n + y.n - 2))
if (isVarianceEqual) {
print("Student's t-test")
test_stat <- (x.xbar - y.xbar) / (sigma_pool * sqrt(1 / x.n + 1 / y.n))
} else {
print("Welch's t-test")
test_stat <- (x.xbar - y.xbar) / (sqrt(x.sigma^2 / x.n + y.sigma^2 / y.n))
}
p <- 2 * pnorm(abs(test_stat), lower.tail = FALSE)
if (p < 0.05) {
sprintf("With a p value of %f, an f statistic of %f is considered significant at an alpha of 0.05.", p, test_stat)
} else {
sprintf("With a p value of %f, and f statistic of %f is not considered significant at an alpha of 0.05.", p,
test_stat)
}
- Find your two sample means:
- x.xbar = 7.6
- y.xbar = 3.571429
- Find the degrees of freedom:
- dof = 10
- Independently, they are 4 and 6 for x and y respectively.
- Find t-test statistic score:
- My script programmatically chooses whether to use Welch's t-test or Student's t-test as the result of the value of isVarianceEqual
- The decision to compare the quotient of x and y's variance to 4.5337 was determined by finding the associated critical value in an F distribution table.
- As the variances are considered equal, the t-test statistic is calculated as 3.049121 using Student's t-test.
- My script programmatically chooses whether to use Welch's t-test or Student's t-test as the result of the value of isVarianceEqual
- Find the P value:
- As this is a two-tailed test, the p value is calculated as twice the value returned by the pnorm function when passed the absolute value of the test statistic with the lower.tail parameter set to FALSE.
- The resulting p value in this case is 0.002295121
- Assume you had chosen an alpha value of .05. Would this result have been statistically significant?
- This is determined by an if else statement such that: if p is less than alpha, then the statistic is significant. Otherwise it is not significant.
- "With a p value of 0.002295, an f statistic of 3.049121 is considered significant at an alpha of 0.05."
- What critical value would your obtained t-test value have to exceed to be significant at the .01 level (assume a two-tailed test)?
- My code uses the student t distribution quartile function, qt, to determine the critical values to exceed at an alpha level of .01.
- The values are determined as -3.169273 and 3.169273
- As such, the actual output for this question from my code is as follows:
- "To be significant at an alpha of 0.01, the test statistic must be less than -3.169273 or greater than 3.169273"
October 18th, 2020
(Blog Post 8)
In order to answer the first section of questions, I wrote the following code in R:
# Question 1A
one.hyp.null <- "H\u2080: The new machine is operating at or above manufacturer specifications (\u03BC \u2265 70)."
one.hyp.alt <- "H\u03B1: The new machine is operating below manufacturer specifications (\u03BC \u003C 70)."
print(one.hyp.null)
print(one.hyp.alt)# Question 1B
one.mu <- 70
one.sigma <- 3.5
one.n <- 49
one.xbar <- 69.1
one.alpha <- 0.05
one.conf <- 0.95
one.z_test <- (one.xbar - one.mu) / (one.sigma / sqrt(one.n))
one.z <- -1 * qnorm(one.conf + (one.alpha / 2))
if (one.z_test > one.z) {
sprintf("Cannot reject null hypothesis; %.2f is greater than %.2f", one.z_test, one.z)
} else {
sprintf("Null hypothesis can be rejected as %.2f is less than %.2f", one.z_test, one.z)
}# Question 1C
one.p <- pnorm(one.z_test)
if (one.p > one.alpha) {
sprintf("Cannot reject null hypothesis; p of %.3f is greater than the alpha of %.3f", one.p, one.alpha)
} else {
sprintf("Null hypothesis can be rejected as p (%.3f) is less than than alpha (%.3f).", one.p, one.alpha)
}# Question 1D
one.test2 <- (one.xbar - one.mu) / (1.75 / sqrt(one.n))
if (one.test2 > one.z) {
sprintf("Cannot reject null hypothesis; %.2f is greater than %.2f", one.test2, one.z)
} else {
sprintf("Null hypothesis can be rejected as %.2f is less than %.2f", one.test2, one.z)
}# Question 1E
one.test3 <- (69 - one.mu) / (one.sigma / sqrt(one.n))
if (one.test3 > one.z) {
sprintf("Cannot reject null hypothesis; %.2f is greater than %.2f", one.test3, one.z)
} else {
sprintf("Null hypothesis can be rejected as %.2f is less than %.2f", one.test3, one.z)
}
The output is annotated with the original questions below:
A. State the null and alternative hypothesis:
"H₀: The new machine is operating at or above manufacturer specifications (μ ≥ 70)."
"Hα: The new machine is operating below manufacturer specifications (μ < 70)."
B. Is there evidence that the machine is not meeting the manufacturer's specifications for average strength? Use a 0.05 level of significance:
"Cannot reject null hypothesis; -1.80 is greater than -1.96"
C. Compute the p value and interpret its meaning:
"Null hypothesis can be rejected as p (0.036) is less than than alpha (0.050)."
This answers B as "yes", but the original answer on B remains as no without this test.
D. What would be your answer in (B) if the standard deviation were specified as 1.75 pounds?
"Null hypothesis can be rejected as -3.60 is less than -1.96"
Answer would be "yes".
E. What would be your answer in (B) if the sample mean were 69 pounds and the standard deviation is 3.5 pounds?
"Null hypothesis can be rejected as -2.00 is less than -1.96"
Answer would be "yes".
To answer the second question, I wrote the following in R:
two.xbar <- 85
two.sigma <- 8
two.n <- 64
two.ci <- 0.95
calc2_a <- (1 - two.ci) / 2
calc2_error <- qnorm(two.ci + calc2_a) * two.sigma / sqrt(two.n)
calc2_left <- two.xbar - calc2_error
calc2_right <- two.xbar + calc2_error
sprintf("With %.0f%% confidence, \u03BC must lie between %.2f and %.2f (%.2f , %.2f).", two.ci * 100, calc2_left, calc2_right, calc2_left, calc2_right)
Which prints the following answer:
"With 95% confidence, μ must lie between 83.04 and 86.96 (83.04 , 86.96)."
Finally, for the third question, I wrote the following:
three.df_girls <- read.csv('Copy of module # 8.csv', header = FALSE, nrows = 5, skip = 1, row.names =)
three.df_girls <- as.data.frame(t(as.matrix(three.df_girls)))
three.df_girls <- three.df_girls[-c(5:9),]
colnames(three.df_girls) <- three.df_girls[1,]
three.df_girls <- three.df_girls[-1,]
three.df_girls <- three.df_girls[, -c(2:3)]
three.df_girls <- three.df_girls[, -3]
row.names(three.df_girls) <- paste0('Girl#', seq_len(3))
three.df_boys <- read.csv('Copy of module # 8.csv', header = FALSE, nrows = 5, skip = 8)
three.df_boys <- as.data.frame(t(as.matrix(three.df_boys)))
three.df_boys <- three.df_boys[-c(5:9),]
colnames(three.df_boys) <- three.df_boys[1,]
three.df_boys <- three.df_boys[-1,]
three.df_boys <- three.df_boys[, -c(2:3)]
three.df_boys <- three.df_boys[, -3]
row.names(three.df_boys) <- paste0('Boy#', seq_len(3))
three.df <- rbind(three.df_girls, three.df_boys)
three.df[] <- lapply(three.df, as.numeric)
three.pear <- cor(three.df$`Time spend on assignment`, three.df$Goals)
three.spear <- cor(three.df$`Time spend on assignment`, three.df$Goals, method = 's')
corrgram(three.df)
# However, I do not fully understand how to interpret this correlogram plot.
three.df$Gender <- c(rep('Girl', times = 3), rep('Boy', times = 3))
ggplot(three.df, aes(`Time spend on assignment`, `Goals`)) +
geom_point(size = 9, aes(color = Gender)) +
geom_smooth(method = 'lm') +
labs(title = paste('Pearson\'s r:', three.pear, '\nSpearman\'s \u03C1:', three.spear)) +
ylab('Goals') +
xlab('Time Spent on Assignment') +
theme(plot.title = element_text(hjust = 0.5))
A lot of this code is just munging the data (which I previously converted to CSV outside of the script) so as to remove unneccesary columns and rows. I also brought the csv in as two separate data frames: one for girls and one for boys. Before munging either of them, I transposed them so that variables weren't handled as the occurences.
Do note that I am calculating correlations on y (time spent on assignment) ~ x (Goals) with boys and girls considered as one for the calculations. However, the scatterplot at the end of this submission distinguishes between boys and girls using point color.
I used the corrgram package as instructed, the resulting plot is shown below:

However, I do not fully understand how to interpret such a plot -- even after reading the manual -- so I went ahead and created an additional plot; this time a scatterplot à la ggplot2:

This scatterplot interpolates the values for Pearson's r and Spearman's rho into the title.
Closing notes: I wanted to try out the xkcd theme I enjoy messing around with when using ggplot in python. However, I could not get the font to load even after installing it to my system fonts (probably a Windows issue as most SO questions regarding troubleshooting the extrafont package were from unix users). While I could've spun up a docker container for this module's project, I would have then had other issues involving HAXM which I need for Kotlin projects. As such, I decided not to troubleshoot the matter any further as it was a triviality to begin with.
October 9th, 2020
(Blog Post 7)
In order to determine a 95% confidence interval estimate of µ, for x̄ = 85, σ = 8 and n = 64, I wrote the following code in R:
one_xbar <- 85
one_sigma <- 8
one_n <- 64
one_ci <- 0.95
calc1_a <- (1 - one_ci) / 2
calc1_error <- qnorm(one_ci + calc1_a) * one_sigma/sqrt(one_n)
calc1_left <- one_xbar - calc1_error
calc1_right <- one_xbar + calc1_error
sprintf("With %.0f%% confidence, µ must lie between %.2f and %.2f", one_ci*100, calc1_left, calc1_right)
The result is "With 95% confidence, µ must lie between 83.04 and 86.96".
In order to determine a 99% confidence interval estimate of µ, for x̄ = 125, σ = 24 and n = 36, I wrote the following code in R:
two_xbar <- 125
two_sigma <- 24
two_n <- 36
two_ci <- 0.99
calc2_a <- (1-two_ci)/2
calc2_error <- qnorm(two_ci+calc2_a) * two_sigma/sqrt(two_n)
calc2_left <- two_xbar - calc2_error
calc2_right <- two_xbar + calc2_error
sprintf("With %.0f%% confidence, µ must lie between %.2f and %.2f", two_ci*100, calc2_left, calc2_right)
The result is "With 99% confidence, µ must lie between 114.70 and 135.30".
In order to determine a 99% confidence interval estimate of µ, for x̄ = 0.99, σ = 0.02 and n = 50, I wrote the following code in R:
three_sigma <- 0.02
three_n <- 50
three_xbar <- 0.99
three_ci <- 0.99
calc3_a <- (1-three_ci)/2
calc3_error <- qnorm(three_ci+calc3_a) * three_sigma/sqrt(three_n)
calc3_left <- three_xbar - calc3_error
calc3_right <- three_xbar + calc3_error
sprintf("With %.0f%% confidence, µ must lie between %.4f and %.4f gallons.", three_ci*100, calc3_left, calc3_right)
The result is "With 99% confidence, µ must lie between 0.9827 and 0.9973 gallons."
While it may be easy to argue that any complaint about the discrepancy between the expected and actual amounts of paint per 1-gallon can is weak due to the difference being less than 38 milliliters per can, the individual in the question is not the end consumer but rather a reseller. As the reseller of this paint, there are a few important details to note: resellers buy in wholesale quantities, resellers' customers purchase amounts greater than or equal to what is required for their projects.
On the subject of wholesale quantities: while I haven't personally called around to ask for real-life figures, it's not farfetched to estimate that a mom-and-pop supply store probably orders a few hundred cans per order (for simplicity, let's just say 250 1-gallon cans per order) and big businesses such as Home Depot probably receive thousands of 1-gallon cans per order per location (again, let's simplify this as 1200 1-gallon cans per order for any given location). For the hypothetical mom-and-pop store, this comes out to about 9.5 liters per order; approximately 2.5 cans worth of paint missing per order. For the hypothetical Home Depot location, the discrepancy per order is approximately 12 cans worth of missing paint. With this data, the validity in a supplier's complaint of missing paint leans fairly heavily on their profit margins.
However, it's not worth theorizing the amount of money lost regardless of the suppliers profit margins due to the second factor I'd previously mentioned: customers purchase a number of 1-gallon cans greater than or equal to what is required for their projects. I'd imagine it would be quite a marvel of engineering to design a project that only requires cans of paint that will be completely consumed in the process (i.e. without needing part of an extra can of paint). Typically, customers will be buying an extra can of paint regardless of whether the cans are exactly one gallon or slightly under one gallon. While the manager of such supply stores may have the moral obligation to inform customers of the discrepancy between advertised content and average content per 1-gallon can, they certainly have no right to complain to the manufacturer. This discrepancy further increases their profit per order. To help demonstrate this concept, consider the following:
A customer's project requires 40.5 gallons of paint. In a (nearly) perfect world, this would require purchasing 41 cans of paint. With the lower end of the estimate for µ (0.9827), this requires purchasing 42 cans of paint.
As the supply store is selling the cans of paint rather than using them, this discrepancy simply increases their sales. While most suppliers would likely issue a refund if the customer opened a 1-gallon can of paint to find it was only half full, customers are highly unlikely to ask for a refund (let alone notice the difference) on the basis of a few missing drops of paint per can.
In order to determine a 95% confidence interval estimate of µ, for x̄ = $1.67, σ = 0.32 and n = 20, I wrote the following code in R:
four_n <- 20
four_xbar <- 1.67
four_sigma <- 0.32
four_ci <- 0.95
calc4_a <- (1-four_ci)/2
calc4_error <- qnorm(four_ci+calc4_a) * four_sigma/sqrt(four_n)
calc4_left <- four_xbar - calc4_error
calc4_right <- four_xbar + calc4_error
sprintf("With %.0f%% confidence, µ must lie between $%.2f and $%.2f for all cards in the store.", four_ci*100,calc4_left,calc4_right)
The result is "With 95% confidence, µ must lie between $1.53 and $1.81 for all cards in the store."
This result can aid the store owner to estimate the mean price for all cards in their store as falling between $1.53 and $1.81. Further, this value can help to determine whether or not to lower prices on cards set at over $1.81, depending on how well the store sells them.
In order to determine the required sample size, for Z=1.96, E=5 and σ=15, I wrote the following code in R:
five_z <- 1.96
five_c <- 5
five_sigma <- 15
five_n <- ceiling( ( (five_z * five_sigma) / five_c) **2)
sprintf("A sample size of %d is required.", five_n)
The result is "A sample size of 35 is required."
I decided on the following for my null and alternative hypotheses:
Null = In terms of USF, no significant correlation exists between state funding and cost to students (H_0 : p = 0).
Alternative = In terms of USF, there is a correlation between decreases in state funding and cost to students (H_a : p =/= 0).
I decided on this one-tailed test of correlation because I am also writing a paper for another class on the same subject. As such, I had sources handy but still had to do a minor amount of munging.
I went a bit further here and did some analysis on the data I've already found (operational budget: https://www.usf.edu/business-finance/resource-management-analysis/documents/operating-budget-19-20.pdf | Tableau dashboard: http://usfweb.usf.edu/ODS/SystemFacts/Enrollments.aspx )
I wrote up the following code in R to both look at rejecting the null hypothesis, and plotting the relevant data:
six_data <- read.csv("C:\\Users\\TheJerkBox\\Desktop\\Adv Stats\\USFOperatingBudgetDecadeHistory.csv")
six_data <- six_data[-1]
six_data <- rename(six_data, c("X"="FY2011", "X.1"="FY2012", "X.2"="FY2013",
"X.3"="FY2014", "X.4"="FY2015", "X.5"="FY2016",
"X.6"="FY2017", "X.7"="FY2018", "X.8"="FY2019",
"X.9"="FY2020"))
row.names(six_data) <- c("STATE APPROPRIATIONS", "TUITION & FEES",
"CONTRACT & GRANTS", "AUXILIARY", "OTHER")
six_nullHyp <- "In terms of USF, there exists a significant correlation between state funding and cost to students (H_0 : p = 0)."
six_altHyp <- "In terms of USF, there is a positive correlation between decreases in state funding and cost to students (H_a : p =/= 0)."
six_a <- 0.05
six_crit.left <- -0.632
six_crit.right <- 0.632
t_six_data <- transpose(six_data)
row.names(t_six_data) <- c("FY2011", "FY2012", "FY2013", "FY2014", "FY2015",
"FY2016", "FY2017", "FY2018", "FY2019", "FY2020")
colnames(t_six_data) <- c("V1"="STATE APPROPRIATIONS", "V2"="TUITION & FEES",
"V3"="CONTRACT & GRANTS", "V4"="AUXILIARY",
"V5"="OTHER")
six_r <- cor(t_six_data$`STATE APPROPRIATIONS`, t_six_data$`TUITION & FEES`)
t_six_relevant <- t_six_data[1:2]
t_six_relevant <- rename(t_six_relevant, c("STATE APPROPRIATIONS"="Funding",
"TUITION & FEES"="Cost"))
t_six_relevant$Year <- row.names(t_six_relevant)
ggplot(t_six_relevant, aes(x=Funding, y=Cost)) + geom_point(size=9, aes(color=Year)) +
geom_path(color='mediumturquoise', size=1.25) + geom_smooth(method='lm') +
labs(title = paste('r =', six_r, '\nSignificant if ≤', six_crit.left, "or ≥", six_crit.right)) +
theme(plot.title = element_text(hjust = 0.5))
if (six_r < six_crit.left || six_r > six_crit.right) {
sprintf("With a correlation coefficient of %f, there is sufficient evidence to reject the null hypotheses on the basis that a significant linear relationship exists between state funding and cost to students at USF.", six_r)
six_direction <- "negative"
if(sign(six_r) == 1) {
print("This statistically significant correlation suggests that, as state funding for USF increases, funds sourced from tuition & fees also increase. Likewise, as funding decreases, collections from tuition & fees also decrease.")
} else {
print("This statistically significant correlation suggests that, as state funding for USF increases, funds sourced from tuition & fees decrease. Likewise, as funding decreases, collections from tuition & fees increase.")
}
} else {
sprintf("With a correlation coefficient of %f, there is not sufficient evidence to reject the null hypotheses.", six_r)
}
six_third_variable_frame <- t_six_relevant[7,]
six_third_variable_frame[2,] <- t_six_relevant[9,]
six_third_variable_frame$Enrolled_Fulltime <- c(35704,35650)
# Full-Time Enrollment for 2017 and 2019 sourced from Tableau dashboard made available by USF's Office of Decision Support
six_third_variable_frame$DollarCostPerStudent <- (six_third_variable_frame$Cost / six_third_variable_frame$Enrolled_Fulltime) * 1000000
six_third_variable_frame
The following is the data as plotted:

Investigating statistical significance of the correlation between state funding and tuition and fees at USF, r = 0.6876541, returned the following result:
"This statistically significant correlation suggests that, as state funding for USF increases, funds sourced from tuition & fees also increase. Likewise, as funding decreases, collections from tuition & fees also decrease."
Already aware of a massive third variable problem, I rolled up a new dataframe which included only fiscal years 2017 and 2019. I then added the full-time enrollment data (from the Office of Decision Support's Tableau dashboard for enrollment data from 2017 to 2019) for these years. Finally, I calculate the cost per student for each of these two years by dividing the funds sourced from tuition & fees by the number of students enrolled full-time the same year.
The resulting dataframe is as follows:
Funding Cost Enrolled_Fulltime DollarCostPerStudent
FY2017 384.5 281.0 35704 7870.267
FY2019 424.5 289.5 35650 8120.617
While only for part of the plotted data, this accounts for the fact that the correlation between state funding and funds sourced from tuition & fees was not affected by the third-variable problem of tuition & fees funding increasing due to increased students.
October 3rd, 2020
(Blog Post 6)
Question # 1.
A publishing company has just published a new textbook. Before the company decides the price at which to sell this textbook, it want to know the average price of all such textbooks in the market. The research department at the company took a sample of 25 comparable textbooks and collected information on their prices. This information produced a mean price of $145 for this sample. It is known that the standard deviation of the prices of all such textbooks is $35 and the population of such prices is normal.
A. What is the point of estimate of the mean price all such textbooks?
The point of estimate of the mean price for the population of comparable textbooks is the sample mean, $145.
B. Construct a 90% confidence interval for the mean price of all such textbooks
In order to do find this interval in R, I wrote the following code:
known1_n <- 25
known1_xBar <- 145
known1_sigma <- 35
known1_ci <- 0.9
calc1_a <- (1-known1_ci)/2
calc1_error <- qnorm(known1_ci+calc1_a)*known1_sigma/sqrt(known1_n)
calc1_left <- known1_xBar - calc1_error
calc1_right <- known1_xBar + calc1_errorsprintf("The %.0f%% CI is ($%.2f , $%.2f)", known1_ci*100, calc1_left, calc1_right)
Which prints out "The 90% CI is ($133.49 , $156.51)".
Question # 2
A. According to Mobes Services Inc. an individual checking his/her account at major U.S banks via cellphones cost the banks between $350 and $650. A recent random sample of 600 such checking accounts produced a mean annual cost of $500 to major U.S banks. Assume that the standard deviation of annual costs to major US banks of all such checking account is $40. Make a 99% confidence interval for the current mean annual cost to major banks all such checking account.
Following the calculation of a 90% CI for the previous question, I wrote the code below:
known2_n <- 600
known2_xBar <- 500
known2_sigma <- 40
known2_ci <- 0.99
calc2_a <- (1-known2_ci)/2
calc2_error <- qnorm(known2_ci+calc2_a)*known2_sigma/sqrt(known2_n)
calc2_left <- known2_xBar - calc2_error
calc2_right <- known2_xBar + calc2_error
sprintf("The %.0f%% CI is ($%.2f , $%.2f)", known2_ci*100, calc2_left, calc2_right)
Which prints out "The 99% CI is ($495.79 , $504.21)".
Additional note: I could have written the variable names in unicode, such that known1_sigma would become known1_σ, as R's lack of naming conventions does technically allow unicode characters in object names. However, the added time to reference such variables likely would have greatly outweighed any added clarity.
September 25th, 2020
(Blog Post 5)
For a population of five housemates' ice cream purchases (8, 14, 16, 10, 11), the mean is 11.8 and the standard deviation is 3.19374388453426.
A random sample of size 2 was selected from the population and, at the most recent execution (in which the sample contained 14 and 10), had a mean of 12 and a standard deviation of 2.82842712474619.
As you'll see the sample selection is truly random each time. However, with this execution, the sample mean and the mean of the population are fairly similar as are the standard deviations.
The following code was written to accomplish what I wrote above:
purchases <- c(8,14,16,10,11)
purchases.mean <- mean(purchases)
purchases.sd <- sd(purchases)
print(paste("The mean of the population is", purchases.mean, "and the standard deviation of the population is", purchases.sd))samp <- sample(purchases, 2)
samp.mean <- mean(samp)
samp.sd <- sd(samp)
print(paste("Randomly sampled from the population were", samp[1], "and", samp[2], "which have a mean of", samp.mean, "and a standard deviation of", samp.sd))
Supposing a sample size, n, of 100 and a population proportion, p, of 0.95, the sample proportion does not have an approximately normal distribution. In order for the sample proportion to be approximately normal in distribution, both the number of successes ( "np" ) and the number of failures ( "n(1-p)" )must be greater than or equal to 10. In this case, np (95) is greater than or equal to 10 but n(1-p) is equal to 5.
In order to determine the smallest value of n for which the sampling distribution of p is approximately normal, I wrote the following code:
possibilities <- 0:1000
smallest_n <- 0
smallest_hit <- TRUEfor (possibility in possibilities) {
if (possibility * p >= 10) {
if (possibility * (1 - p) >= 10) {
smallest_n <- possibility
break
}
}
else if (possibility == tail(possibilities, n=1)) {
smallest_hit <- FALSE
break
}
}hit <- paste("The smallest value of n for which the sampling distribution of p is approximately normal is", smallest_n)
not_hit <- "Appropriate value not found in range of possibilities, will need to increase the range."`?` <- function(x, y) if(x) y[[1]] else y[[2]]
`:` <- function(y, z) list(y, z)
smallest_hit ? hit : not_hit
rm(`?`)
rm(`:`)
To start, this instantiates three variables: possibilities with a range starting at 1 up to and including 100, smallest_n which is equal to 0, and smallest_hit which is equal to TRUE.
For every number within the range in the possibilities variable, the code first checks if the product of the number and the population proportion are greater than or equal to 10. If they are, then it also checks if the number of failures is greater than or equal to 10. If this is also true, then the code breaks out of the for loop after storing the current iteration's value in smallest_n.
However, if the current iteration does not meet either condition, the loop determines if the current value is the last value in the possibilities variable. If it is, the value of smallest_hit is replaced with FALSE and then it breaks out of the loop.
I then assign a different string to two variables, one with formatted output in the event that an appropriate value was found and one that informs the user that the range needs to be adjusted in order to find a match.
Finally, I did some pretty spooky operator assignments.
I assign the ? operator as a function the checks if the first value passed to it (which is a conditional statement) is logically true. If it is true, it returns the first value from the second argument passed, otherwise it returns the second value of the second argument.
Next up I assign the : operator as a function that creates a list given two values surrounding itself. This is crucial as the ? operator is now expecting a list as its second argument in order to behave properly.
Using these newly reassigned operators I pass in the conditional statement smallest_hit (which is set as either TRUE or FALSE depending on the outcome of the previous for loop). The : operator is surrounded by the values from hit and not_hit which get passed to the ? operator as y[[1]] and y[[2]], respectively.
What this will do is return the value stored in the hit variable if smallest_hit is true, otherwise it will return the value stored in the not_hit variable.
In other programming languages these two operators are collectively referred to as the "ternary operator". However, as both the ? and : symbols are very important for other R functions, this is not how I want to leave my session/environment configured. In order to restore these functions to doing what they're supposed to do I use the rm() function.
The resulting output from this ternary statement is "The smallest value of n for which the sampling distribution of p is approximately normal is 200".
One thing that I would like to clarify is that R does have an ifelse method. However, the resulting output from passing this through an ifelse() call was doubled. Writing up these operator reassignments was considerably quicker than the time I had already sunk into attempting to debug the original ifelse call.
September 20th, 2020
(Blog Post 4)
As someone who struggles with probability theory, I not only found this week's assignment challenging but I also learned quite a bit. In fact, I had to contact my boss to help me understand the table presented in the assignment's prompt but after that, it was fairly smooth sailing.
In order to get going I recreated the table as a dataframe in R as follows:
tab_one <- data.frame("B" = c(10, 20), "B1" = c(20, 40), row.names = c("A", "A1"))
The following image shows how this table appears in RStudio:
I used this dataframe to create another dataframe containing relative frequencies:
tab_one.rel_freq <- tab_one / sum(tab_one)
As it appears in RStudio:
I then used the following code to calculate probabilities for event A, event B, and event A or event B:
prob_a <- sum(tab_one.rel_freq["A",])
prob_aprob_b <- sum(tab_one.rel_freq$B)
prob_bprob_either <- prob_a + prob_b - prob_a * prob_b
prob_either
The output is as follows:
> prob_a
[1] 0.3333333> prob_b
[1] 0.3333333> prob_either
[1] 0.5555556
In order to determine if these are disjoint events, P(A or B) = P(A) + P(B), I wrote the following code:
isDisjoint = prob_either == prob_a + prob_b
ifelse(isDisjoint, "True, these are disjoint events.", paste("False; whereas P(A or B) is", prob_either, "P(A) + P(B) is", prob_a + prob_b))
The resulting output, selected based on the result of the ternary-like conditional statement used:
[1] "False; whereas P(A or B) is 0.555555555555556 P(A) + P(B) is 0.666666666666667"
In order to proof the provided calculation for Bayes Theorem, I wrote the following code:
tab_two <- data.frame("Usual Days" = c(5/365,360/365), "Forecast" = c(0.9, 0.1), row.names = c("Will Rain", "Will Not Rain"))
tab_two
tab_two.bayes <- (tab_two$Forecast[1] * tab_two$Usual.Days[1]) / (tab_two$Forecast[1] * tab_two$Usual.Days[1] + tab_two$Forecast[2] * tab_two$Usual.Days[2])
tab_two.bayes
The resulting table is as appears below:
Additionally the value assigned to tab_two.bayes is 0.1111111
It is true that the weatherman's predictions are only going to be correct ~11% of the time considering the fact that it only has been raining 5 days a year for the past few years.
This may be due to the fact that, while the weatherman correctly predicts rain 90% of the time, there are still only 5 days for the weatherman to be right assuming a non-anomalous year with only 5 days of precipitation.
September 13th, 2020
(Blog Post 3)
For this assignment I started by creating three vectors: case_vector, screeners_vector, and violations_vector as shown in the code snippet below.
case_vector = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
screeners_vector = c(287, 243, 237, 227, 247, 264, 247, 247, 251, 254, 277, 303, 285, 254, 280, 264, 261, 292, 248, 253)
violations_vector = c(271, 261, 230, 225, 236, 252, 243, 247, 238, 274, 256, 305, 273, 234, 261, 265, 241, 292, 228, 252)
In order to establish a high-level on association between boarding screeners and discovered security violations I divided the two as shown below:
screeners_vector / violations_vector
Which gave me the following output:
[1] 1.0590406 0.9310345 1.0304348 1.0088889 1.0466102 1.0476190 1.0164609 1.0000000 1.0546218 0.9270073 1.0820312 0.9934426 1.0439560 1.0854701 1.0727969
[16] 0.9962264 1.0829876 1.0000000 1.0877193 1.0039683
From this very high-level analysis it appears as though there's roughly one security violation discovered for every boarding screener present.
As shown below, I've created a dataframe, case_df, which contains the screeners_vector and violations_vector as rows. I then calculate Pearson's Sample Correlation Coefficient and assign the value to a variable, r. Before printing out the value of r.
case_df <- data.frame(screeners_vector, violations_vector)
r <- cor(case_df, use="all.obs", method="pearson")
r
Given the output below, I've determined the value of r_xy to be 0.8375321.
screeners_vector violations_vector
screeners_vector 1.0000000 0.8375321
violations_vector 0.8375321 1.0000000
This is indicative of a very strong positive correlation.
In order to calculate and display Spearman's Rank Coefficient, I added the following code to my script:
p <- cor(case_df, use="all.obs", method="spearman")
p
Given the output below, I've determined the value of p to be 0.7575423.
screeners_vector violations_vector
screeners_vector 1.0000000 0.7575423
violations_vector 0.7575423 1.0000000
This is indicative of a fairly strong positive association between ranks.
Finally, I used R to create a scatter plot in which the number of screeners is the independent variable, and the number of discovered violations is the dependent variable.
The code:
plot(case_df$screeners_vector, case_df$violations_vector)
The resultant plot:

With how much work can go into calculating Pearson's r, I was pleasantly surprised to find it (and Spearman's p) as ready-to-use arguments in R.
September 6th, 2020
(Blog Post 2)
1.) I've calculated the measures of central tendency for each data set as follows:
Data Set #1:
Mean: 4.333333
Median: 3.5
Mode: 2
Data Set #2:
Mean: 14
Median: 13
Mode: 12
2.) I've calculated the measures of variation for each data set as follows:
Data Set #1:
Range: 2-10
Interquartile: 2.00, 2.25, 3.50, 4.75, and 10.00 (respectively)
Variance: 9.066667
Standard Deviation: 3.011091
Data Set #2:
Range: 12-20
Interquartile: 12.0, 12.0, 13.0, 14.5, and 20.0 (respectively)
Variance: 8.333333
Standard Deviation: 2.886751
3.) Both data sets are fairly well centered in terms of the median and mean values as they are relatively small vectors. Variability between the two sets also appears to be roughly equal, with the first data set being slightly more variable than the second. The differences in calculations is mostly due to the numerical content found in each data set.
Below you will find the screenshot of my session from RStudio. I used an answer that I found on StackOverflow in order to calculate the mode. However, this function appears to calculate the frequency of each object in a vector and determine which object was most frequent.

August 30th, 2020
(Blog Post 1)
I installed R console and RStudio; RStudio launches without any noticeable issues.
I have read and understand the syllabus for this class.
© Copyright Blake Rosenberg's Blog