library(dplyr)
## Warning: package 'dplyr' was built under R version 4.3.3
##
## 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
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.3.3
library(plotly)
## Warning: package 'plotly' was built under R version 4.3.3
##
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
##
## last_plot
## The following object is masked from 'package:stats':
##
## filter
## The following object is masked from 'package:graphics':
##
## layout
library(packcircles)
## Warning: package 'packcircles' was built under R version 4.3.3
library(viridis)
## Warning: package 'viridis' was built under R version 4.3.3
## Loading required package: viridisLite
library(hrbrthemes)
## NOTE: Either Arial Narrow or Roboto Condensed fonts are required to use these themes.
## Please use hrbrthemes::import_roboto_condensed() to install Roboto Condensed and
## if Arial Narrow is not on your system, please see https://bit.ly/arialnarrow
library(scales)
## Warning: package 'scales' was built under R version 4.3.3
##
## Attaching package: 'scales'
## The following object is masked from 'package:viridis':
##
## viridis_pal
library(gridExtra)
## Warning: package 'gridExtra' was built under R version 4.3.3
##
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
##
## combine
library(ggrepel)
## Warning: package 'ggrepel' was built under R version 4.3.3
library(reshape2)
## Warning: package 'reshape2' was built under R version 4.3.3
library(lubridate)
##
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
library(sf)
## Warning: package 'sf' was built under R version 4.3.3
## Linking to GEOS 3.11.2, GDAL 3.8.2, PROJ 9.3.1; sf_use_s2() is TRUE
library(rnaturalearth)
## Warning: package 'rnaturalearth' was built under R version 4.3.3
library(rnaturalearthdata)
## Warning: package 'rnaturalearthdata' was built under R version 4.3.3
##
## Attaching package: 'rnaturalearthdata'
## The following object is masked from 'package:rnaturalearth':
##
## countries110
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.3.3
## Warning: package 'tidyr' was built under R version 4.3.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ purrr 1.0.2 ✔ tibble 3.2.1
## ✔ readr 2.1.4 ✔ tidyr 1.3.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ readr::col_factor() masks scales::col_factor()
## ✖ gridExtra::combine() masks dplyr::combine()
## ✖ purrr::discard() masks scales::discard()
## ✖ plotly::filter() masks dplyr::filter(), stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
library(ggthemes)
## Warning: package 'ggthemes' was built under R version 4.3.3
library(scales)
library(reshape2)
library(readxl)
## Warning: package 'readxl' was built under R version 4.3.3
library(treemap)
## Warning: package 'treemap' was built under R version 4.3.3
library(treemapify)
## Warning: package 'treemapify' was built under R version 4.3.3
The Global Superstore dataset is a detailed collection of sales data from a fictonal retail company that operates around the world. The dataset includes lots of information about orders, customers, products, and shipments, allowing for many types of analysis.
Given that we can only present 3 answers to questions by visualizing data we have choosen to go with 2 umbrella topics from the Global Superstore Dataset: Customer Analysis & Product Analysis
df <- read_excel('C:/Users/use/Desktop/Global Superstore.xls')
Dataset contains:
colnames(df)
## [1] "Row ID" "Order ID" "Order Date" "Ship Date"
## [5] "Ship Mode" "Customer ID" "Customer Name" "Segment"
## [9] "City" "State" "Country" "Postal Code"
## [13] "Market" "Region" "Product ID" "Category"
## [17] "Sub-Category" "Product Name" "Sales" "Quantity"
## [21] "Discount" "Profit" "Shipping Cost" "Order Priority"
For That We Will Answer The Following Questions:
I also removed the $ so to convert the sales column into a numeric value
df$Sales <- gsub(",", "", df$Sales)
df$Sales <- gsub("\\$", "", df$Sales)
df$Sales <- as.numeric(df$Sales)
head(df$Sales)
## [1] 2309.650 3709.395 5175.171 2892.510 2832.960 2862.675
df$Profit <- gsub(",", "", df$Profit)
df$Profit <- gsub("\\$", "", df$Profit)
df$Profit <- as.numeric(df$Profit)
head(df$Profit)
## [1] 762.1845 -288.7650 919.9710 -96.5400 311.5200 763.2750
df$Discount <- df$Discount * 100
head(df$Discount)
## [1] 0 10 10 10 0 10
df <- df %>%
rename(Sub_Category = `Sub-Category`)
head(df)
## # A tibble: 6 × 24
## `Row ID` `Order ID` `Order Date` `Ship Date` `Ship Mode`
## <dbl> <chr> <dttm> <dttm> <chr>
## 1 32298 CA-2012-124891 2012-07-31 00:00:00 2012-07-31 00:00:00 Same Day
## 2 26341 IN-2013-77878 2013-02-05 00:00:00 2013-02-07 00:00:00 Second Class
## 3 25330 IN-2013-71249 2013-10-17 00:00:00 2013-10-18 00:00:00 First Class
## 4 13524 ES-2013-1579342 2013-01-28 00:00:00 2013-01-30 00:00:00 First Class
## 5 47221 SG-2013-4320 2013-11-05 00:00:00 2013-11-06 00:00:00 Same Day
## 6 22732 IN-2013-42360 2013-06-28 00:00:00 2013-07-01 00:00:00 Second Class
## # ℹ 19 more variables: `Customer ID` <chr>, `Customer Name` <chr>,
## # Segment <chr>, City <chr>, State <chr>, Country <chr>, `Postal Code` <chr>,
## # Market <chr>, Region <chr>, `Product ID` <chr>, Category <chr>,
## # Sub_Category <chr>, `Product Name` <chr>, Sales <dbl>, Quantity <dbl>,
## # Discount <dbl>, Profit <dbl>, `Shipping Cost` <dbl>, `Order Priority` <chr>
df <- df %>%
mutate(Year = as.numeric(format(as.Date(`Order Date`), "%Y")))
yearly_product_profits <- df %>%
group_by(Year,Sub_Category) %>%
summarise(Total_Profit = sum(Profit, na.rm = TRUE)) %>%
ungroup()
## `summarise()` has grouped output by 'Year'. You can override using the
## `.groups` argument.
top_5_profit_product_types <- yearly_product_profits %>%
group_by(Year) %>%
top_n(5, Total_Profit) %>%
arrange(Year, desc(Total_Profit)) %>%
ungroup()
top_product_types <- top_5_profit_product_types %>%
select(Sub_Category) %>%
distinct() %>%
pull()
filtered_data <- yearly_product_profits %>%
filter(Sub_Category %in% top_product_types)
plot <- plot_ly(filtered_data, x = ~Year, y = ~Total_Profit, color = ~Sub_Category, type = 'scatter', mode = 'lines+markers') %>%
layout(title = "Top Products By Profit Per Year",
xaxis = list(title = "Year"),
yaxis = list(title = "Total Profit"),
legend = list(title = list(text = 'Product Type')),
margin = list(l = 50, r = 50, t = 50, b = 50))
plot
Generally we can say that there is an upward trends for the 5 product categories listed above but Accessories have had the highest growth.
yearly_product_profits <- df %>%
group_by(Year, Sub_Category) %>%
summarise(Total_Profit = sum(Profit, na.rm = TRUE)) %>%
ungroup()
## `summarise()` has grouped output by 'Year'. You can override using the
## `.groups` argument.
top_5_loss_product_types <- yearly_product_profits %>%
group_by(Year) %>%
top_n(-5, Total_Profit) %>%
arrange(Year, Total_Profit) %>%
ungroup()
top_loss_product_types <- top_5_loss_product_types %>%
select(Sub_Category) %>%
distinct() %>%
pull()
filtered_loss_data <- yearly_product_profits %>%
filter(Sub_Category %in% top_loss_product_types)
loss_plot <- plot_ly(filtered_loss_data, x = ~Year, y = ~Total_Profit, color = ~Sub_Category, type = 'scatter', mode = 'lines+markers') %>%
layout(title = "Products With Loosing Profit Over Time",
xaxis = list(title = "Year"),
yaxis = list(title = "Total Loss"),
legend = list(title = list(text = 'Product Type')),
margin = list(l = 50, r = 50, t = 50, b = 50))
loss_plot
Tablets seem to have taken a significant hit from 2012 and onwards. Everything else seems to be keep the trending of slowly dying. Makes sense as most people now use email to communicate and there is very little demand for envelopes or labels.
I will treat this question as a top of a funnel approach. We will start to look at where are the top sales coming from and we can further drill down on the data.
agg_data <- df %>%
group_by(Region) %>%
summarize(TotalSales = sum(Sales))
ggplot(agg_data, aes(x = TotalSales, y = Region, size = TotalSales, color = Region)) +
geom_point(alpha = 0.6) +
geom_text_repel(aes(label = Region), size = 3.5, box.padding = 0.5, point.padding = 0.9) +
labs(title = "Bubble Chart of Sales by Region", x = "Total Sales", y = "Region") +
theme_minimal() +
scale_size_continuous(range = c(3, 15)) +
scale_x_continuous(labels = scales::label_comma(), breaks = seq(0, max(agg_data$TotalSales), by = 1000000)) +
guides(color = FALSE, size = FALSE)
## Warning: The `<scale>` argument of `guides()` cannot be `FALSE`. Use "none" instead as
## of ggplot2 3.3.4.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Becase West, East and Central Refer To The US market we will merge
the and view the bubble chart as a more simplified breakup of the
markets by continent
#for that we will need to create a mapping
region_mapping <- c(
"East" = "North America",
"Oceania" = "Australia",
"Central" = "North America",
"Africa" = "Africa",
"West" = "North America",
"South" = "South America",
"Central Asia" = "Asia",
"EMEA" = "Europe",
"North Asia" = "Asia",
"North" = "North America",
"Caribbean" = "North America",
"Southeast Asia" = "Asia",
"Canada" = "North America"
)
#PUTTING THEM TOGETHER
df <- df %>%
mutate(Region = region_mapping[Region])
#CHECKING THE NEW REGIONS COLUMN
unique(df$Region)
## [1] "North America" "Australia" "Africa" "South America"
## [5] "Asia" "Europe"
agg_data <- df %>%
group_by(Region) %>%
summarize(TotalSales = sum(Sales))
ggplot(agg_data, aes(x = TotalSales, y = Region, size = TotalSales, color = Region)) +
geom_point(alpha = 0.6) +
geom_text_repel(aes(label = Region), size = 3.5, box.padding = 0.5, point.padding = 0.9) +
labs(title = "Bubble Chart of Sales by Region", x = "Total Sales", y = "Region") +
theme_minimal() +
scale_size_continuous(range = c(3, 15)) +
scale_x_continuous(labels = scales::label_comma(), breaks = seq(0, max(agg_data$TotalSales), by = 1000000)) +
guides(color = FALSE, size = FALSE) # Hide the legends for color and size
## Using The Same Bubble Chart To Compare It With Profit
agg_data <- df %>%
group_by(Region) %>%
summarize(TotalProfit = sum(Profit))
ggplot(agg_data, aes(x = TotalProfit, y = Region, size = TotalProfit, color = Region)) +
geom_point(alpha = 0.6) +
geom_text_repel(aes(label = Region), size = 3.5, box.padding = 0.5, point.padding = 0.9) +
labs(title = "Bubble Chart of Profit by Region", x = "Total Profits", y = "Region") +
theme_minimal() +
scale_size_continuous(range = c(3, 15)) +
scale_x_continuous(labels = scales::label_comma(), breaks = seq(0, max(agg_data$TotalProfit), by = 100000)) +
guides(color = FALSE, size = FALSE)
## Since the North American region remains to be the leading market we
will take a close look at the top states contributing to this profit
df <- df %>% filter(Country == "United States")
state_sales <- df %>%
group_by(State) %>%
summarize(TotalSales = sum(Sales), .groups = 'drop') %>%
arrange(desc(TotalSales))
top_states <- head(state_sales, 10)
ggplot(top_states, aes(y = reorder(State, TotalSales), x = TotalSales, fill = TotalSales)) +
geom_col() +
geom_text(aes(label = comma(TotalSales), x = TotalSales + 0.05 * TotalSales), hjust = 0) + # Adjust position dynamically
labs(title = "Top Performing States in the US by Sales", x = "Total Sales", y = "State") +
scale_fill_gradient(low = "#FFB6C1", high = "#D2042D", name = "Total Sales") + # Light pink to red gradient
scale_x_continuous(labels = label_comma(),
limits = c(0, max(top_states$TotalSales) * 1.1)) + # Extend the limit by 10%
theme_minimal() +
theme(axis.text.y = element_text(angle = 0, hjust = 1),
legend.position = "none")
## Taking A Closer Look At Profit Will Reveal Intresting Insights For
The US Market
df <- df %>% filter(Country == "United States")
state_profits <- df %>%
group_by(State) %>%
summarize(TotalProfit = sum(Profit), .groups = 'drop') %>%
arrange(desc(TotalProfit))
top_profit <- head(state_profits, 10)
head(top_profit)
## # A tibble: 6 × 2
## State TotalProfit
## <chr> <dbl>
## 1 California 76381.
## 2 New York 74039.
## 3 Washington 33403.
## 4 Michigan 24463.
## 5 Virginia 18598.
## 6 Indiana 18383.
state_sales <- df %>%
group_by(State) %>%
summarize(TotalSales = sum(Sales), .groups = 'drop')
# Merge the sales data with the top profits data
top_states_data <- merge(top_profit, state_sales, by = "State")
# View the data to ensure it looks correct
print(head(top_states_data))
## State TotalProfit TotalSales
## 1 California 76381.387 457687.63
## 2 Delaware 9977.375 27451.07
## 3 Georgia 16250.043 49095.84
## 4 Indiana 18382.936 53555.36
## 5 Kentucky 11199.697 36591.75
## 6 Michigan 24463.188 76269.61
top_states_long <- pivot_longer(top_states_data,
cols = c(TotalProfit, TotalSales),
names_to = "Metric",
values_to = "Amount")
ggplot(top_states_long, aes(y = reorder(State, -Amount, FUN = max), x = Amount, fill = Metric)) +
geom_col(position = position_dodge(width = 0.9)) +
scale_x_continuous(labels = comma) + # Ensures numbers are not in scientific notation
scale_fill_manual(values = c("TotalProfit" = "#FFB6C1", "TotalSales" = "lightblue")) +
labs(title = "Comparison of Sales vs. Profit for Top 10 States (Ranked by Profit)",
y = "State",
x = "Amount (USD)",
fill = "Metric") +
theme_minimal() +
theme(axis.text.y = element_text(angle = 0, hjust = 1))
It’s interesting how 400k in sales turn out to less than 100k in profit.
A good reason why Californians are leaving California.
But interestingly since the US is the biggest market for the Global Superstore we take it a step further and analyze where a new market must be launched, in which state. For this the profitability ratio must be calculated. PR= Total Profit/ Total Sales
state_metrics <- df %>%
group_by(State) %>%
summarize(TotalSales = sum(Sales), TotalProfit = sum(Profit), .groups = 'drop') %>%
filter(TotalProfit > 0)
state_metrics <- state_metrics %>%
mutate(SalesProfitRatio = (TotalProfit / TotalSales) * 100,
Rank = rank(-SalesProfitRatio))
top_states_by_ratio <- state_metrics %>%
arrange(Rank) %>%
top_n(15, SalesProfitRatio)
ggplot(top_states_by_ratio, aes(y = reorder(State, -SalesProfitRatio), x = SalesProfitRatio, fill = SalesProfitRatio)) +
geom_col() +
geom_text(aes(label = sprintf("%.1f%%", SalesProfitRatio), x = SalesProfitRatio + 1), hjust = 0, color = "black") +
labs(title = "Top 10 States by Sales/Profit Ratio",
x = "Sales to Profit Ratio (%)",
y = "State") +
scale_fill_gradient(low = "#FFB6C1", high = "#D2042D", name = "Sales/Profit Ratio (%)") +
scale_x_continuous(expand = expansion(add = 5)) +
theme_minimal() +
theme(axis.text.y = element_text(angle = 0, hjust = 1),
legend.position = "right")
## Where Not To Open A New store?
##first we find the worst profitability ratios
state_metrics <- df %>%
group_by(State) %>%
summarize(TotalSales = sum(Sales), TotalProfit = sum(Profit), .groups = 'drop') %>%
filter(TotalProfit > 0) %>%
mutate(SalesProfitRatio = (TotalProfit / TotalSales) * 100,
Rank = rank(SalesProfitRatio))
bottom_states_by_ratio <- state_metrics %>%
arrange(Rank) %>%
slice_head(n = 5)
ggplot(bottom_states_by_ratio, aes(y = reorder(State, SalesProfitRatio), x = SalesProfitRatio, fill = SalesProfitRatio)) +
geom_col() +
geom_text(aes(label = sprintf("%.1f%%", SalesProfitRatio), x = SalesProfitRatio + 1), hjust = 0, color = "black") + # Offset labels to the right of bars
labs(title = "Bottom 5 States by Sales/Profit Ratio",
x = "Sales to Profit Ratio (%)",
y = "State") +
scale_fill_gradient(low = "#FFB6C1", high = "#D2042D", name = "Sales/Profit Ratio (%)") +
scale_x_continuous(expand = expansion(add = 5)) +
theme_minimal() +
theme(axis.text.y = element_text(angle = 0, hjust = 1),
legend.position = "right")
## Based off this chart we can see that California has a low % of Sales
to Profit ratio, yet this does not tell the full story. At The End Of
The Day Investors Want To See $$, So For That Reason We Will Plot Top
& Bottom 10 Profit & Margin by State in terms of Dollars
state_profits <- df %>%
group_by(State) %>%
summarise(TotalProfit = sum(Profit, na.rm = TRUE)) %>%
ungroup()
top_states <- state_profits %>%
top_n(10, TotalProfit)
bottom_states <- state_profits %>%
top_n(-10, TotalProfit)
combined_states <- bind_rows(top_states, bottom_states) %>%
arrange(desc(TotalProfit))
ggplot(combined_states, aes(y = reorder(combined_states$State, combined_states$TotalProfit), x = combined_states$TotalProfit, fill = combined_states$TotalProfit)) +
geom_col() +
geom_text(aes(label = scales::comma(combined_states$TotalProfit), x = combined_states$TotalProfit), hjust = -0.2, color = "black", size = 3.5) +
scale_fill_gradient(low = "#FFB6C1", high = "#D2042D", name = "Profit (USD)") +
labs(title = "Profit Comparison: Top 10 Best vs. Worst Performing States",
y = "State",
x = "Total Profit (USD)") +
theme_minimal() +
theme(axis.text.y = element_text(angle = 0, hjust = 1),
legend.position = "right")
## Warning: Use of `combined_states$TotalProfit` is discouraged.
## ℹ Use `TotalProfit` instead.
## Warning: Use of `combined_states$State` is discouraged.
## ℹ Use `State` instead.
## Warning: Use of `combined_states$TotalProfit` is discouraged.
## ℹ Use `TotalProfit` instead.
## Use of `combined_states$TotalProfit` is discouraged.
## ℹ Use `TotalProfit` instead.
## Use of `combined_states$TotalProfit` is discouraged.
## ℹ Use `TotalProfit` instead.
## Use of `combined_states$TotalProfit` is discouraged.
## ℹ Use `TotalProfit` instead.
## Warning: Use of `combined_states$State` is discouraged.
## ℹ Use `State` instead.
## Warning: Use of `combined_states$TotalProfit` is discouraged.
## ℹ Use `TotalProfit` instead.
## Use of `combined_states$TotalProfit` is discouraged.
## ℹ Use `TotalProfit` instead.
Without diving too deep into calculations we have only use the profit as
our measure of intrest here. Despite California being on the lower end
of the Profit/Sales ratio it still sits on top of the charts when it
comes to producing the most profit at the end of the day. Things like
tax rates and California regulations might cause the company to have
more overhead hence less profitability ratio.
critical_orders <- df %>%
filter(`Order Priority` == "Critical")
critical_counts <- critical_orders %>%
group_by(`Product Name`) %>%
summarise(Critical_Count = n())
# Select top 5 products with the most Critical orders
top_5_critical_products <- critical_counts %>%
top_n(5, wt = Critical_Count) %>%
arrange(desc(Critical_Count))
print(top_5_critical_products)
## # A tibble: 17 × 2
## `Product Name` Critical_Count
## <chr> <int>
## 1 "Staples" 23
## 2 "Bush Heritage Pine Collection 5-Shelf Bookcase, Albany Pine … 4
## 3 "Hewlett Packard LaserJet 3310 Copier" 4
## 4 "Logitech 910-002974 M325 Wireless Mouse for Web Scrolling" 4
## 5 "ACCOHIDE 3-Ring Binder, Blue, 1\"" 3
## 6 "Acco 6 Outlet Guardian Premium Plus Surge Suppressor" 3
## 7 "Adams Telephone Message Book W/Dividers/Space For Phone Numb… 3
## 8 "GBC DocuBind P100 Manual Binding Machine" 3
## 9 "GBC DocuBind P50 Personal Binding Machine" 3
## 10 "Hon Deluxe Fabric Upholstered Stacking Chairs, Rounded Back" 3
## 11 "Ibico Plastic Spiral Binding Combs" 3
## 12 "Logitech G19 Programmable Gaming Keyboard" 3
## 13 "Safco Contoured Stacking Chairs" 3
## 14 "Tyvek Top-Opening Peel & Seel Envelopes, Plain White" 3
## 15 "Xerox 1912" 3
## 16 "Xerox 212" 3
## 17 "XtraLife ClearVue Slant-D Ring Binders by Cardinal" 3
treemap(top_5_critical_products,
index = "Product Name",
vSize = "Critical_Count",
title = "Top 5 Products Bought at Critical Priority",
#palette = "Reds",
border.col = "black",
fontsize.labels = 12,
fontfamily.labels = "sans",
align.labels = list(c("center", "center")),
overlap.labels = 0.5,
inflate.labels = TRUE)
A treemap like this shows the products we filtered in a tree like graph.
The size of the boxes that make up the bigger picture show the count of
observations.
Stapels intrestingly are bought at critical order priority? I wonder why!
subcategory_returns <- df %>%
group_by(Sub_Category) %>%
summarise(Returns = n())
subcategory_returns <- subcategory_returns %>%
mutate(Percentage = Returns / sum(Returns) * 100,
Label = paste0(Sub_Category, "\n", round(Percentage, 1), "%"))
library(showtext)
## Warning: package 'showtext' was built under R version 4.3.3
## Loading required package: sysfonts
## Warning: package 'sysfonts' was built under R version 4.3.3
## Loading required package: showtextdb
## Warning: package 'showtextdb' was built under R version 4.3.3
font_add_google(name = "Roboto", family = "roboto")
showtext_auto()
ggplot(subcategory_returns, aes(area = Returns, fill = Sub_Category, label = Label)) +
geom_treemap() +
geom_treemap_text(colour = "white", place = "centre", grow = TRUE, size = 0.5, family = "roboto") +
labs(title = "No. of Returned Products by Subcategory") +
theme_minimal() +
theme(
legend.position = "none",
plot.title = element_text(size = 16, face = "bold", hjust = 0.9, family = "roboto")
)
Here I have tried to show the graph with percentages. The subcategories make up the whole 100%.