All projects
github.com/ksprihar/ontario-energy-mix

ontario-energy-mix

Tracing Natural Gas's rising share of Ontario's electricity mix since 2015

energypythonsqldockersql-servert-sqldata-analysisdata-engineeringjupyter-notebookpandasplotlyseaborn
Jupyter Notebook Python TSQL
01 — Motivation

Why this project

Ontario's electricity mix has been quietly reshaped since 2015, and Natural Gas's growing role is something I'd seen debated anecdotally without ever seeing the numbers laid out end-to-end. Having spent years assessing energy systems in the field, I wanted to apply the same rigour to a grid-scale question: is Gas becoming baseload, or is it still just a peaker fuel?

The drop in Nuclear output from 2020-2023 because of Ontario's ongoing Nuclear Refurbishment Program gave me a natural before/after window to test that question, and the demand climb starting in 2024 gave me a second, independent one. I wanted an analysis that held up under both. Key Findings section explores the answer to the question asked above.

02 — Methodology

How it was built

Data comes from IESO's public reports — monthly generation by fuel type and monthly demand — pulled via a Python ingestion script using requests: the generation feed arrives as XML and is parsed with xml.etree.ElementTree, while the demand feed arrives as CSV; both are reshaped to the pipeline's schema and written out as CSV with pandas.

From there, the database layer runs in Docker: a containerized SQL Server 2022 instance is built from scratch by Docker Compose, with T-SQL scripts creating the schema, loading the generation and demand csv files, and running data-integrity checks before any analysis touches the data.

Once the data was validated, the heavy lifting — 12-month rolling averages, year-over-year fuel share, and peak-demand segmentation — was done in T-SQL using window functions and CTEs. Results were pulled into a Jupyter notebook for the final analysis and visualised with Plotly and Seaborn.

03 — Results

Key findings

  • Demand entered a sharp structural climb starting in 2024, breaking years of relative stability
  • Gas absorbed essentially all of this growth — total generation rose 1,138 GWh from Jan 2024 to May 2026, while Gas output alone rose 1,232 GWh
  • Gas covered most of the output lost during Ontario's Nuclear Refurbishment Program (2020–2023), when Nuclear output fell 1,387 GWh against a total system drop of just 251 GWh
  • Gas's share of generation rises sharply with demand stress — from 7.8% in low-demand months, to 12.8% in high-demand months, to 15.8% in the top 10 most extreme peak months, and to as much as 18.0% in the top 5.

Ontario's electricity grid has quietly shifted Natural Gas from a flexible peaking resource into a structural baseload fallback — driven by both rising demand and a temporary loss of nuclear capacity.

The following chart shows just that: Gas expands to cover the Nuclear gap during the 2020–2023 period, then keeps growing as demand climbs from 2024 onward.

04 — Code

Code highlight

Comparing Gas's share of generation in the 10 highest peak-demand months vs. every other month (T-SQL)

WITH month_ranks AS (
    SELECT
        month,
        DENSE_RANK() OVER(ORDER BY peak_demand_gw DESC) AS rnk
    FROM demand_matching_generation_range
),
peak_months AS (
    -- Assign each month to a category based on its rank
    SELECT
        month,
        CASE
            WHEN rnk <= 10 THEN 'Top 10 Months'
            ELSE 'Other Months'
        END AS month_category
    FROM month_ranks
),
generation_total AS (
    -- Collapse all non-Gas fuels into 'Others', then sum generation per category.
    -- SUM(SUM(...)) OVER(PARTITION BY month_category) is a window-of-aggregate:
    -- the inner SUM produces per-group row totals (via GROUP BY), the outer SUM
    -- window adds them up within each month_category to get the category grand total
    SELECT
        pm.month_category,
        CASE
            WHEN g.fuel = 'GAS' THEN 'Gas'
            ELSE 'Others'
        END AS fuel_category,
        SUM(g.output_gwh) AS fuel_category_output,
        SUM(SUM(g.output_gwh)) OVER(PARTITION BY pm.month_category) AS month_category_total
    FROM generation g
    JOIN peak_months pm
        ON g.month = pm.month
    GROUP BY
        pm.month_category,
        CASE
            WHEN g.fuel = 'GAS' THEN 'Gas'
            ELSE 'Others'
        END
)
SELECT
    *,
    ROUND(fuel_category_output * 100.0 / month_category_total, 2) AS percentage_of_total_generation
FROM generation_total
ORDER BY month_category DESC, fuel_category ASC