10 · Project — Data Analysis Script¶
A small end-to-end project combining everything from Level 1: variables, control flow, functions, vectors, data frames, reading CSV data, plotting, and packages.
What you'll build¶
A single R script, analyze_sales.R, that:
- Reads a small sales CSV dataset
- Cleans and inspects it
- Computes summary statistics per product category
- Flags underperforming products with a custom function
- Produces two plots (a bar chart and a histogram) saved to disk
Project layout¶
sales.csv — the sample dataset¶
Create this file by hand (or generate it with a short script):
product,category,units_sold,revenue
Widget A,Hardware,120,2400
Widget B,Hardware,45,900
Gadget X,Electronics,300,15000
Gadget Y,Electronics,80,4800
Tool Z,Hardware,15,225
Gizmo Q,Electronics,210,10500
analyze_sales.R — the full script¶
# analyze_sales.R
# A small end-to-end sales data analysis script.
# --- 1. Load packages -------------------------------------------------------
if (!require("readr", quietly = TRUE)) {
install.packages("readr")
library(readr)
}
# --- 2. Read and inspect the data -------------------------------------------
sales <- read_csv("sales.csv", show_col_types = FALSE)
cat("---- Structure ----\n")
str(sales)
cat("\n---- First rows ----\n")
print(head(sales))
if (anyNA(sales)) {
warning("Data contains missing values -- check before trusting summaries.")
}
# --- 3. Custom analysis functions -------------------------------------------
# Average revenue per unit sold, rounded to 2 decimal places
revenue_per_unit <- function(revenue, units) {
round(revenue / units, 2)
}
# Flags a product as "underperforming" if it sold fewer than a threshold
classify_performance <- function(units_sold, threshold = 50) {
ifelse(units_sold < threshold, "Underperforming", "On Track")
}
sales$rev_per_unit <- revenue_per_unit(sales$revenue, sales$units_sold)
sales$performance <- classify_performance(sales$units_sold)
cat("\n---- Sales with derived columns ----\n")
print(sales)
# --- 4. Summarize by category ------------------------------------------------
categories <- unique(sales$category)
cat("\n---- Category summaries ----\n")
for (cat_name in categories) {
subset_data <- sales[sales$category == cat_name, ]
total_units <- sum(subset_data$units_sold)
total_revenue <- sum(subset_data$revenue)
avg_rev_per_unit <- round(total_revenue / total_units, 2)
cat(sprintf(
"%s: %d units, $%d revenue, $%.2f avg revenue/unit\n",
cat_name, total_units, total_revenue, avg_rev_per_unit
))
}
# tapply() gives the same category totals more concisely
cat("\n---- tapply() cross-check: total revenue by category ----\n")
print(tapply(sales$revenue, sales$category, sum))
# --- 5. Flag underperforming products ---------------------------------------
underperformers <- sales[sales$performance == "Underperforming", "product"]
cat("\nUnderperforming products:", paste(underperformers, collapse = ", "), "\n")
# --- 6. Plots ----------------------------------------------------------------
category_revenue <- tapply(sales$revenue, sales$category, sum)
png("revenue_by_category.png", width = 800, height = 600)
barplot(category_revenue,
main = "Total Revenue by Category",
ylab = "Revenue ($)",
col = c("steelblue", "seagreen"))
dev.off()
png("units_sold_distribution.png", width = 800, height = 600)
hist(sales$units_sold,
main = "Distribution of Units Sold",
xlab = "Units Sold",
col = "cornflowerblue",
breaks = 5)
dev.off()
cat("\nSaved plots: revenue_by_category.png, units_sold_distribution.png\n")
cat("Analysis complete.\n")
Running it¶
---- Structure ----
spc_tbl_ [6 x 4] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
$ product : chr "Widget A" "Widget B" "Gadget X" "Widget Y" ...
$ category : chr "Hardware" "Hardware" "Electronics" "Electronics" ...
$ units_sold: num 120 45 300 80 15 210
$ revenue : num 2400 900 15000 4800 225 10500
---- Category summaries ----
Hardware: 180 units, $3525 revenue, $19.58 avg revenue/unit
Electronics: 590 units, $30300 revenue, $51.36 avg revenue/unit
Underperforming products: Widget B, Tool Z
Saved plots: revenue_by_category.png, units_sold_distribution.png
Analysis complete.
Each function (revenue_per_unit, classify_performance) is small,
testable in isolation, and vectorized — calling it once on the whole
units_sold column computes every product's classification at once, no
explicit loop required. The for loop over categories is used
deliberately for the readable per-category printout, with tapply() shown
right after as the more idiomatic one-liner for the same aggregation.
How It Actually Works¶
Running a script with Rscript analyze_sales.R starts a fresh R
interpreter process, sources your file top to bottom exactly as if you'd
pasted each line into a console, and exits — no leftover state carries
over between runs, which is why scripts should never depend on objects
left in your interactive session. Each pipeline stage in the script
(reading, aggregate()/dplyr grouping, plotting) hands off its output as
a plain in-memory R object to the next stage; there's no persistence
between them beyond ordinary variable assignment, so the whole analysis
re-executes from read.csv() onward every time you re-run the file.
The aggregation step (grouping and summarizing) walks the data once to
build a hash-keyed index of group membership (one bucket per unique
combination of grouping columns), then applies your summary function to
each bucket's row indices — this is why grouped operations scale roughly
linearly with row count rather than needing one pass per group. The final
write.csv()/plot output is just R serializing its in-memory
representation back to a file format, with no connection to the original
CSV once loaded.
Stretch goals¶
- Add a
profit_margincolumn if you extendsales.csvwith acostcolumn, and flag products with margin below some threshold. - Replace the
forloop overcategorieswithsapply()once you reach Level 2, Module 3. - Swap the base
barplot()/hist()calls forggplot2once you reach Level 2, Module 4 — same data, more polished output. - Add basic input validation: what should
analyze_sales.Rdo ifsales.csvdoesn't exist, or aunits_soldvalue is0(division by zero inrevenue_per_unit)?
Completing this project means you're ready for Level 2 · Intermediate.