Solar PV analysis

Introduction

As the world increasingly shifts towards renewable energy sources, my nation is taking decisive steps to transition from reliance on fossil fuels to establishing a more sustainable and resilient energy infrastructure. This pivot, with a pronounced emphasis on harnessing solar photovoltaic (PV) technology, is not only an integral component of our strategy to meet environmental sustainability objectives but also a pivotal move to counteract climate change and fortify our energy security. Nonetheless, this shift to green energy technologies brings to the fore significant concerns, particularly regarding the adequacy, reliability of supply chains, and the environmental ramifications associated with mining essential minerals such as copper and silicon, which are crucial for the production of solar PV systems.

This essay aims to delve into the intricate dynamics of mineral demand driven by the solar PV industry, with a special focus on understanding the critical roles and projected future demands of copper and silicon. This analysis is underpinned by insights and data from the International Energy Agency’s (IEA) 2023 Critical Minerals Report and the comprehensive IEA Database, providing a foundation for evaluating how these materials contribute to the evolving landscape of renewable energy and the challenges that lie ahead in ensuring their sustainable and ethical procurement.

Data Tidying

options(repos = c(CRAN = "https://cran.rstudio.com/"))
install.packages("tidyverse")

The downloaded binary packages are in
    /var/folders/cp/x4c86z354zq1gfn38__r36tc0000gn/T//RtmpSgb6XH/downloaded_packages
install.packages("readxl")

The downloaded binary packages are in
    /var/folders/cp/x4c86z354zq1gfn38__r36tc0000gn/T//RtmpSgb6XH/downloaded_packages
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.0     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(readxl)
install.packages("here")

The downloaded binary packages are in
    /var/folders/cp/x4c86z354zq1gfn38__r36tc0000gn/T//RtmpSgb6XH/downloaded_packages
library(here)
here() starts at /Users/shiyahuang/Desktop/24 Spring/sustainable finance/R Data
path_to_sheet <- here("data-raw", "CM_Data_Explorer.xlsx")

path_to_sheet
[1] "/Users/shiyahuang/Desktop/24 Spring/sustainable finance/R Data/data-raw/CM_Data_Explorer.xlsx"
read_solar_sheet <- partial(
  .f = read_excel,
  path = path_to_sheet,
  sheet = "2.1 Solar PV",
  col_names = FALSE
)

sheet_header <- read_solar_sheet(range = "A4:W5")
New names:
• `` -> `...1`
• `` -> `...2`
• `` -> `...3`
• `` -> `...4`
• `` -> `...5`
• `` -> `...6`
• `` -> `...7`
• `` -> `...8`
• `` -> `...9`
• `` -> `...10`
• `` -> `...11`
• `` -> `...12`
• `` -> `...13`
• `` -> `...14`
• `` -> `...15`
• `` -> `...16`
• `` -> `...17`
• `` -> `...18`
• `` -> `...19`
• `` -> `...20`
• `` -> `...21`
• `` -> `...22`
• `` -> `...23`
sheet_header
# A tibble: 2 × 23
  ...1   ...2 ...3  ...4    ...5  ...6  ...7  ...8  ...9 ...10 ...11 ...12 ...13
  <lgl> <dbl> <lgl> <chr>  <dbl> <dbl> <dbl> <dbl> <dbl> <lgl> <chr> <dbl> <dbl>
1 NA       NA NA    State…    NA    NA    NA    NA    NA NA    Anno…    NA    NA
2 NA     2022 NA    2025    2030  2035  2040  2045  2050 NA    2025   2030  2035
# ℹ 10 more variables: ...14 <dbl>, ...15 <dbl>, ...16 <dbl>, ...17 <lgl>,
#   ...18 <chr>, ...19 <dbl>, ...20 <dbl>, ...21 <dbl>, ...22 <dbl>,
#   ...23 <dbl>

Tidying the headers

sheet_header_processed <- sheet_header |> 
  # transpose the data
  t() |>
  # turn it back into a tibble
  as_tibble() |>
  # make them meaningful
  rename(scenario = V1, year = V2) |>
  # fill scenario down
  fill(scenario) |>
  #insert "Current" at top
  replace_na(list(scenario = "Current Year"))
Warning: The `x` argument of `as_tibble.matrix()` must have unique column names if
`.name_repair` is omitted as of tibble 2.0.0.
ℹ Using compatibility `.name_repair`.
sheet_header_processed
# A tibble: 23 × 2
   scenario                 year 
   <chr>                    <chr>
 1 Current Year             <NA> 
 2 Current Year             2022 
 3 Current Year             <NA> 
 4 Stated policies scenario 2025 
 5 Stated policies scenario 2030 
 6 Stated policies scenario 2035 
 7 Stated policies scenario 2040 
 8 Stated policies scenario 2045 
 9 Stated policies scenario 2050 
10 Stated policies scenario <NA> 
# ℹ 13 more rows
cases_name <- read_solar_sheet(range = "A7") |> 
  pull()
New names:
• `` -> `...1`
cases_name
[1] "Comeback of high Cd-Te technology"
cases_info <- read_solar_sheet(range = "A8:W23")
New names:
• `` -> `...1`
• `` -> `...2`
• `` -> `...3`
• `` -> `...4`
• `` -> `...5`
• `` -> `...6`
• `` -> `...7`
• `` -> `...8`
• `` -> `...9`
• `` -> `...10`
• `` -> `...11`
• `` -> `...12`
• `` -> `...13`
• `` -> `...14`
• `` -> `...15`
• `` -> `...16`
• `` -> `...17`
• `` -> `...18`
• `` -> `...19`
• `` -> `...20`
• `` -> `...21`
• `` -> `...22`
• `` -> `...23`
cases_info
# A tibble: 16 × 23
   ...1         ...2 ...3     ...4    ...5    ...6    ...7    ...8    ...9 ...10
   <chr>       <dbl> <lgl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl> <lgl>
 1 Cadmium   4.99e-1 NA    6.90e-1 1.06e+0 1.32e+0 1.70e+0 1.95e+0 2.19e+0 NA   
 2 Copper    6.82e+2 NA    7.79e+2 9.07e+2 9.25e+2 9.59e+2 1.12e+3 1.26e+3 NA   
 3 Gallium   1.20e-2 NA    1.94e-2 1.08e-1 4.71e-1 2.59e+0 3.00e+0 3.39e+0 NA   
 4 Germanium 1.36e-2 NA    1.87e-2 2.27e-2 1.84e-2 1.52e-2 1.57e-2 1.52e-2 NA   
 5 Indium    4.50e-2 NA    7.03e-2 1.08e-1 1.36e-1 1.92e-1 2.29e-1 2.58e-1 NA   
 6 Lead      1.15e+0 NA    1.29e+0 1.44e+0 1.39e+0 1.36e+0 1.54e+0 1.85e+0 NA   
 7 Molybden… 1.98e-1 NA    3.29e-1 5.69e-1 7.20e-1 9.22e-1 1.17e+0 1.41e+0 NA   
 8 Nickel    2.50e-1 NA    2.80e-1 3.11e-1 3.02e-1 2.95e-1 3.31e-1 3.61e-1 NA   
 9 Selenium  1.05e-1 NA    1.55e-1 2.08e-1 2.40e-1 2.74e-1 3.01e-1 2.99e-1 NA   
10 Silicon   7.45e+2 NA    7.74e+2 7.46e+2 6.86e+2 6.30e+2 6.68e+2 6.74e+2 NA   
11 Silver    3.78e+0 NA    3.86e+0 3.61e+0 2.86e+0 2.17e+0 1.78e+0 1.24e+0 NA   
12 Tellurium 5.18e-1 NA    7.11e-1 1.06e+0 1.27e+0 1.55e+0 1.67e+0 1.71e+0 NA   
13 Tin       1.20e+0 NA    1.38e+0 1.64e+0 1.67e+0 1.76e+0 2.04e+0 2.31e+0 NA   
14 Zinc      6.06e+0 NA    6.95e+0 8.28e+0 8.44e+0 8.88e+0 1.03e+1 1.16e+1 NA   
15 Arsenic   0       NA    0       1.90e-1 1.08e+0 6.38e+0 7.39e+0 8.35e+0 NA   
16 Total so… 1.44e+3 NA    1.57e+3 1.67e+3 1.63e+3 1.62e+3 1.82e+3 1.97e+3 NA   
# ℹ 13 more variables: ...11 <dbl>, ...12 <dbl>, ...13 <dbl>, ...14 <dbl>,
#   ...15 <dbl>, ...16 <dbl>, ...17 <lgl>, ...18 <dbl>, ...19 <dbl>,
#   ...20 <dbl>, ...21 <dbl>, ...22 <dbl>, ...23 <dbl>
cases_info_col_names <- names(cases_info)

cases_info_col_names
 [1] "...1"  "...2"  "...3"  "...4"  "...5"  "...6"  "...7"  "...8"  "...9" 
[10] "...10" "...11" "...12" "...13" "...14" "...15" "...16" "...17" "...18"
[19] "...19" "...20" "...21" "...22" "...23"
sheet_headers_and_col_names <- sheet_header_processed |> 
  add_column(cases_info_col_names = cases_info_col_names)

sheet_headers_and_col_names
# A tibble: 23 × 3
   scenario                 year  cases_info_col_names
   <chr>                    <chr> <chr>               
 1 Current Year             <NA>  ...1                
 2 Current Year             2022  ...2                
 3 Current Year             <NA>  ...3                
 4 Stated policies scenario 2025  ...4                
 5 Stated policies scenario 2030  ...5                
 6 Stated policies scenario 2035  ...6                
 7 Stated policies scenario 2040  ...7                
 8 Stated policies scenario 2045  ...8                
 9 Stated policies scenario 2050  ...9                
10 Stated policies scenario <NA>  ...10               
# ℹ 13 more rows
cases_info_long <- cases_info |> 
  rename(indicator = `...1`) |> 
  pivot_longer(cols = -indicator,
               names_to = "cases_info_col_names") |> 
  add_column(cases_name)

cases_info_long
# A tibble: 352 × 4
   indicator cases_info_col_names  value cases_name                       
   <chr>     <chr>                 <dbl> <chr>                            
 1 Cadmium   ...2                  0.499 Comeback of high Cd-Te technology
 2 Cadmium   ...3                 NA     Comeback of high Cd-Te technology
 3 Cadmium   ...4                  0.690 Comeback of high Cd-Te technology
 4 Cadmium   ...5                  1.06  Comeback of high Cd-Te technology
 5 Cadmium   ...6                  1.32  Comeback of high Cd-Te technology
 6 Cadmium   ...7                  1.70  Comeback of high Cd-Te technology
 7 Cadmium   ...8                  1.95  Comeback of high Cd-Te technology
 8 Cadmium   ...9                  2.19  Comeback of high Cd-Te technology
 9 Cadmium   ...10                NA     Comeback of high Cd-Te technology
10 Cadmium   ...11                 0.819 Comeback of high Cd-Te technology
# ℹ 342 more rows
combined_data <- cases_info_long |> 
  left_join(sheet_headers_and_col_names, by = join_by(cases_info_col_names)) |> 
  # filter out what were empty columns (where years are NA) 
  filter(!is.na(year)) |> 
  mutate(
    # convert the year column from character to numeric
    year = as.integer(year)
  ) |> 
  rename(mineral = indicator,
         tech_scenarios = cases_name,
         policy_scenarios = scenario) |> 
  select(mineral, tech_scenarios, policy_scenarios, year, value)

combined_data
# A tibble: 304 × 5
   mineral tech_scenarios                    policy_scenarios         year value
   <chr>   <chr>                             <chr>                   <int> <dbl>
 1 Cadmium Comeback of high Cd-Te technology Current Year             2022 0.499
 2 Cadmium Comeback of high Cd-Te technology Stated policies scenar…  2025 0.690
 3 Cadmium Comeback of high Cd-Te technology Stated policies scenar…  2030 1.06 
 4 Cadmium Comeback of high Cd-Te technology Stated policies scenar…  2035 1.32 
 5 Cadmium Comeback of high Cd-Te technology Stated policies scenar…  2040 1.70 
 6 Cadmium Comeback of high Cd-Te technology Stated policies scenar…  2045 1.95 
 7 Cadmium Comeback of high Cd-Te technology Stated policies scenar…  2050 2.19 
 8 Cadmium Comeback of high Cd-Te technology Announced pledges scen…  2025 0.819
 9 Cadmium Comeback of high Cd-Te technology Announced pledges scen…  2030 1.38 
10 Cadmium Comeback of high Cd-Te technology Announced pledges scen…  2035 1.91 
# ℹ 294 more rows

build the function

read_iea_solar_table <-
  function(cases_name_range, cases_info_range) {
    cases_name <-
      read_solar_sheet(range = cases_name_range) |>
      pull()
    
    cases_info <- read_solar_sheet(range = cases_info_range)
    
    cases_info_col_names <- names(cases_info)
    
    cases_info_long <- cases_info |>
      rename(indicator = `...1`) |>
      pivot_longer(cols = -indicator,
                   names_to = "cases_info_col_names") |>
      add_column(cases_name)
    
    combined_data <- cases_info_long |>
      left_join(sheet_headers_and_col_names, by = join_by(cases_info_col_names)) |>
      # filter out what were empty columns (where years are NA)
      filter(!is.na(year)) |>
      # case_when is supercharged if else
      mutate(
        # convert the year column from character to numeric
        year = as.integer(year)
      ) |> 
      rename(mineral = indicator,
             tech_scenarios = cases_name,
             policy_scenarios = scenario) |> 
      select(mineral, tech_scenarios, policy_scenarios, year, value)
    
    combined_data
  }
comeback_of_high_CdTe_technology_table <- read_iea_solar_table(
  cases_name_range = "A7",
  cases_info_range = "A8:W23"
)
New names:
New names:
• `` -> `...1`
wider_adaption_of_GasAs_technology_table <- read_iea_solar_table(
  cases_name_range = "A26",
  cases_info_range = "A27:W42"
)
New names:
New names:
• `` -> `...1`
wider_adoption_of_perovskite_solar_cells_table <- read_iea_solar_table(
  cases_name_range = "A45",
  cases_info_range = "A46:W61"
)
New names:
New names:
• `` -> `...1`
polar_base_case_table <- read_iea_solar_table(
  cases_name_range = "A64",
  cases_info_range = "A65:W80"
)
New names:
New names:
• `` -> `...1`
final_iea_solar_table <- polar_base_case_table |>
  bind_rows(comeback_of_high_CdTe_technology_table) |>
  bind_rows(wider_adaption_of_GasAs_technology_table) |>
  bind_rows(wider_adoption_of_perovskite_solar_cells_table)  # Corrected typo in variable name

final_iea_solar_table
# A tibble: 1,216 × 5
   mineral tech_scenarios policy_scenarios            year value
   <chr>   <chr>          <chr>                      <int> <dbl>
 1 Cadmium Base case      Current Year                2022 0.406
 2 Cadmium Base case      Stated policies scenario    2025 0.412
 3 Cadmium Base case      Stated policies scenario    2030 0.371
 4 Cadmium Base case      Stated policies scenario    2035 0.379
 5 Cadmium Base case      Stated policies scenario    2040 0.400
 6 Cadmium Base case      Stated policies scenario    2045 0.447
 7 Cadmium Base case      Stated policies scenario    2050 0.490
 8 Cadmium Base case      Announced pledges scenario  2025 0.489
 9 Cadmium Base case      Announced pledges scenario  2030 0.483
10 Cadmium Base case      Announced pledges scenario  2035 0.548
# ℹ 1,206 more rows

Growth Rate Comparison within the “Comeback of high Cd-Te technology” tech scenario, and then create the smoothed Year-on-Year Growth Rates graph for these specific conditions, you ca

Scenario Impact Analysis

scenario_impact_data <- filtered_data %>%
  group_by(policy_scenarios, mineral) %>%
  summarize(total_demand = sum(value, na.rm = TRUE))
`summarise()` has grouped output by 'policy_scenarios'. You can override using
the `.groups` argument.
# Plotting impact of policy scenarios on total demand
ggplot(scenario_impact_data, aes(x = policy_scenarios, y = total_demand, fill = mineral)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(title = "Impact of Policy Scenarios on Total Demand for Copper and Silicon",
       x = "Policy Scenario",
       y = "Total Demand (Sum of Values)",
       fill = "Mineral") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

the impact of policy scenarios on the total demand for Copper and Silicon for each tech scenario separately

library(ggplot2)
library(dplyr)

# Base case
base_case_data <- final_iea_solar_table %>%
  filter(mineral %in% c('Copper', 'Silicon')) %>%
  filter(tech_scenarios == "Base case") %>%
  filter(policy_scenarios != "Current Year") %>%  # Exclude 'Current Year' data
  group_by(policy_scenarios, mineral) %>%
  summarize(total_demand = sum(value, na.rm = TRUE))
`summarise()` has grouped output by 'policy_scenarios'. You can override using
the `.groups` argument.
ggplot(base_case_data, aes(x = policy_scenarios, y = total_demand, fill = mineral)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(title = "Impact of Policy Scenarios on Copper and Silicon Demand (Base Case)",
       x = "Policy Scenario",
       y = "Total Demand (Sum of Values)",
       fill = "Mineral") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

# Comeback of high Cd-Te technology excluding 'Current Year'
cd_te_data <- final_iea_solar_table %>%
  filter(mineral %in% c('Copper', 'Silicon')) %>%
  filter(tech_scenarios == "Comeback of high Cd-Te technology") %>%
  filter(policy_scenarios != "Current Year") %>%  # Exclude 'Current Year' data
  group_by(policy_scenarios, mineral) %>%
  summarize(total_demand = sum(value, na.rm = TRUE))
`summarise()` has grouped output by 'policy_scenarios'. You can override using
the `.groups` argument.
# Plot for Comeback of high Cd-Te technology without 'Current Year'
ggplot(cd_te_data, aes(x = policy_scenarios, y = total_demand, fill = mineral)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(title = "Impact of Policy Scenarios on Copper and Silicon Demand (Comeback of high Cd-Te)",
       x = "Policy Scenario",
       y = "Total Demand (Sum of Values)",
       fill = "Mineral") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

# Wider adoption of perovskite solar cells excluding 'Current Year'
perovskite_data <- final_iea_solar_table %>%
  filter(mineral %in% c('Copper', 'Silicon')) %>%
  filter(tech_scenarios == "Wider adoption of perovskite solar cells") %>%
  filter(policy_scenarios != "Current Year") %>%  # Exclude 'Current Year' data
  group_by(policy_scenarios, mineral) %>%
  summarize(total_demand = sum(value, na.rm = TRUE), .groups = 'drop')  # Including .groups = 'drop' to prevent grouping message

# Plot for Wider adoption of perovskite solar cells without 'Current Year'
ggplot(perovskite_data, aes(x = policy_scenarios, y = total_demand, fill = mineral)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(title = "Impact of Policy Scenarios on Copper and Silicon Demand (Wider adoption of perovskite solar cells)",
       x = "Policy Scenario",
       y = "Total Demand (Sum of Values)",
       fill = "Mineral") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

The total demand for copper and silicon varies significantly under different energy policy scenarios and technological developments, affecting the future landscape of renewable energy infrastructure.

The provided R code and corresponding graphs offer an insightful comparative analysis of how different policy scenarios and technological advancements impact the total demand for copper and silicon. The “Base Case” scenario depicts how initial policy commitments, labeled as “Announced Pledges Scenario,” trigger a noticeable increase in demand for both minerals, essential for solar PV systems. This scenario suggests a proactive yet measured approach towards renewable energy adoption.

The second set of data visualizations reflects a “Comeback of high Cd-Te Technology,” which represents a technological shift possibly decreasing the demand for silicon due to the adoption of alternative photovoltaic materials. Despite this, copper demand remains relatively stable across scenarios, highlighting its indispensable role in the renewable sector.

In the final set, “Wider adoption of perovskite solar cells,” demonstrates an alternative technological path that could lead to different demand dynamics for copper and silicon. Here, the demand for silicon might decrease if perovskite solar cells, which do not require silicon, become more prevalent.

These graphical analyses underscore the dynamic interplay between policy decisions, technological advancements, and mineral demand. They reveal that while policy initiatives significantly drive demand for these critical minerals, technological innovations can dramatically alter these trajectories. This nuanced understanding is crucial for developing strategies that ensure sustainable and secure supply chains for the renewable energy sector.

Conclusion

In conclusion, the demand for critical minerals like copper and silicon is heavily influenced by policy frameworks and technological advancements. The visualizations and analyses demonstrate that aggressive and ambitious policy scenarios lead to higher demand, underscoring the need for comprehensive strategies that support renewable energy transitions while considering the impacts of new technologies. As we move forward, it is crucial to balance policy ambitions with technological innovation to ensure sustainable and secure supply chains for the renewable energy sector, ultimately contributing to the global effort against climate change.