Climate Report

Additional time series data about climate change come in all the time. Here are some plots that I like to track.

I think that what is essential for this problem is a global consciousness,
a view that transcends our exclusive identifications with the generational
and political groupings into which by accident we have been born.

The solution to these problems requires a perspective that embraces the
planet and the future, because we are all in this greenhouse together. 

Carl Sagan, https://youtu.be/Wp-WiNXH6hI?t=985

Keeling Curve (Mauna Loa CO2)

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import rdtools

plt.rcParams['figure.dpi'] = 100

url = 'https://gml.noaa.gov/webdata/ccgg/trends/co2/co2_weekly_mlo.csv'
df = pd.read_csv(url, skiprows=47, na_values=-999.99)
df.index = pd.to_datetime(df[['year', 'month', 'day']])

ppm_weekly = df['average']
ppm_daily = ppm_weekly.resample('d').interpolate()

_, _, calc_info = rdtools.degradation_year_on_year(ppm_daily)
yoy_changes = calc_info['YoY_values']

moving_avg = ppm_daily.rolling(365, center=True).mean()
ppm_deseasonalized = ppm_daily - moving_avg
grouper = ppm_deseasonalized.groupby(ppm_deseasonalized.index.dayofyear)
median_seasonality = grouper.median()
upper_seasonality = grouper.quantile(0.95)
lower_seasonality = grouper.quantile(0.05)
fig, axes = plt.subplots(2, 1, sharex=True, figsize=(6, 6))

# plot this first because it has a shorter index
yoy_changes.plot(ax=axes[1])
yoy_changes.rolling(52, center=True).median().plot(ax=axes[1])
axes[1].set_xlabel(None)
axes[1].set_ylabel('Year-on-year change [%/yr]')
axes[1].grid()

moving_avg.plot(ax=axes[0])
ppm_daily.plot(ax=axes[0])
axes[0].set_ylabel('Dry CO$_2$ Fraction [ppm]')
axes[0].grid()

fig.tight_layout()
_images/6da882eeba59b2d1541c295d1ec8877263bdc2618ed6182b43d6234e0b97a2ab.png

EIA Monthly: Electric Power Sector

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

url = 'https://www.eia.gov/totalenergy/data/browser/csv.php?tbl=T07.02B'
df = pd.read_csv(url)
df = df.loc[~df['Description'].str.contains('Generation Total')]
df = df.loc[~df['YYYYMM'].astype(str).str.endswith('13')]
df['source'] = df['Description'].str.split(",").str[0].str.replace('Electricity Net Generation From ', '')
df['date'] = pd.to_datetime(df['YYYYMM'], format='%Y%m')
df2 = df.pivot(index='date', columns='source', values='Value')
df2 = df2.rename(columns={'Conventional Hydroelectric Power': 'Hydroelectric',
                          'Nuclear Electric Power': 'Nuclear'})
df2 = df2.replace('Not Available', np.nan).replace('Not Meaningful', np.nan).astype(float)
df2 = df2.loc[:, df2.max() > 5000]  # only keep the main players

fractions = 100 * df2.divide(df2.sum(axis=1), axis=0).clip(lower=0)

fig, ax = plt.subplots(figsize=(10, 6))
df2.rolling(12, center=True).mean().plot(ax=ax, colormap='tab10')
ax.legend()
ax.set_ylabel('Million kWh')
fig.tight_layout()
df2.plot(ax=ax, alpha=0.2, colormap='tab10', legend=False)
plt.show()
_images/770084ffef3fe72415bef27ca6aede1bf552593907f050cc223b11197bf1b6a4.png
fig, axes = plt.subplots(3, 1, sharex=True, figsize=(6, 7))
fig.suptitle('Utility-Scale Solar Monthly Generation')

df2['Solar'].dropna().loc['2009':].plot(ax=axes[0])
axes[0].set_ylabel('Million kWh')

fractions['Solar'].loc['2009':].plot(ax=axes[1])
moving_average = fractions['Solar'].rolling(12).mean()
moving_average.loc['2009':].plot(ax=axes[1])
axes[1].set_ylabel('Fraction of total [%]')

yoy_change = moving_average - moving_average.shift(12)
yoy_change.loc['2009':].plot(ax=axes[2])
axes[2].set_ylabel('Year-on-year change [% of total]')

for ax in axes:
    ax.axvline('2017-02-01', ls='--', c='k')
    ax.grid(which='both')

fig.tight_layout()
_images/e2581779dd613a370d845e87fc1bbe2976d0e5db727e12b9db7b2c8bd9c786f0.png
fig, axes = plt.subplots(3, 1, sharex=True, figsize=(6, 7))
fig.suptitle('Utility-Scale Wind Monthly Generation')

df2['Wind'].dropna().loc['2000':].plot(ax=axes[0])
axes[0].set_ylabel('Million kWh')

fractions['Wind'].loc['2000':].plot(ax=axes[1])
moving_average = fractions['Wind'].rolling(12).mean()
moving_average.loc['2000':].plot(ax=axes[1])
axes[1].set_ylabel('Fraction of total [%]')

yoy_change = moving_average - moving_average.shift(12)
yoy_change.loc['2000':].plot(ax=axes[2])
axes[2].set_ylabel('Year-on-year change [% of total]')

for ax in axes:
    ax.grid(which='both')

fig.tight_layout()
_images/be5a99970a9ccc9fae2fbb13912251d65529204e1dc35e29b38e2aa3246bad5a.png
# reorder for plotting purposes
col_order = ['Nuclear', 'Hydroelectric', 'Wind', 'Solar',
             'Coal', 'Natural Gas', 'Petroleum']
fractions = fractions[col_order]
fractions.iloc[-12*10:].plot.area(lw=0, figsize=(10, 4))
plt.ylabel('Share of total generation [%]')

handles, labels = plt.gca().get_legend_handles_labels()
plt.legend(reversed(handles), reversed(labels),
           loc='center right', bbox_to_anchor=(1.2, 0.5))
plt.tight_layout()
plt.grid()
_images/7a6546b041318a336fd4d74623a16d0d6c981b1ec3c713f51498fdb76440db06.png
clean = ['Solar', 'Wind', 'Hydroelectric', 'Nuclear']
clean_fraction = 100 * df2[clean].sum(axis=1) / df2.sum(axis=1)
clean_fraction.plot()
clean_fraction_rolling = 100 * (
    df2[clean].sum(axis=1).rolling(12, center=True).sum() /
    df2.sum(axis=1).rolling(12, center=True).sum()
)
clean_fraction_rolling.plot()
plt.ylabel('Clean Fraction [%]')
plt.tight_layout()
plt.grid()
_images/979ba045dc2c62b377cfa92d842408a9a37fa61331793a26253c338602e01ddc.png
fractions.assign(clean_fraction=clean_fraction).tail(24)
source Nuclear Hydroelectric Wind Solar Coal Natural Gas Petroleum clean_fraction
date
2020-09-01 20.722888 5.867653 7.283648 2.419150 21.415741 41.939091 0.351829 36.293339
2020-10-01 19.947467 6.298296 9.649658 2.363718 19.939519 41.418723 0.382618 38.259140
2020-11-01 21.671042 7.302242 11.583107 2.008710 21.316108 35.654483 0.464306 42.565102
2020-12-01 21.400579 6.557329 9.764229 1.549242 23.921286 36.317728 0.489607 39.271379
2021-01-01 21.568254 7.726720 9.123949 1.708809 24.358307 35.057843 0.456117 40.127733
2021-02-01 20.232108 6.918272 8.599784 2.047226 28.088203 33.377148 0.737259 37.797390
2021-03-01 21.524656 7.253476 13.464893 3.109543 20.804169 33.388248 0.455015 45.352568
2021-04-01 20.613114 6.896434 13.027281 3.881814 19.333900 35.859000 0.388457 44.418643
2021-05-01 20.953200 7.500017 11.065173 4.034643 20.960580 35.080056 0.406332 43.553032
2021-06-01 18.522126 6.721281 7.438509 3.297800 24.347450 39.326462 0.346372 35.979716
2021-07-01 17.781657 5.687010 5.549288 3.056849 26.115728 41.439960 0.369507 32.074804
2021-08-01 17.557164 5.270914 6.747998 2.965353 25.629573 41.366647 0.462351 32.541429
2021-09-01 19.442799 5.389966 8.625739 3.325565 23.629964 39.140676 0.445292 36.784069
2021-10-01 18.888144 5.939761 10.723468 3.043927 20.607307 40.797393 NaN 38.595299
2021-11-01 21.006165 6.816418 12.023371 2.615439 18.956266 38.065861 0.516480 42.461393
2021-12-01 21.970067 7.933967 12.594438 1.959319 18.445046 36.661973 0.435191 44.457791
2022-01-01 19.540750 7.449320 10.566333 2.201032 24.084032 35.139284 1.019249 39.757435
2022-02-01 19.826930 7.554435 12.221207 2.929947 22.529049 34.451338 0.487094 42.532520
2022-03-01 20.422092 8.416541 13.968529 3.819068 19.483063 33.452772 0.437936 46.626230
2022-04-01 19.139718 6.957363 15.987214 4.635510 18.903234 33.972141 0.404819 46.719805
2022-05-01 19.372439 7.288297 12.795123 4.601087 18.882402 36.619500 0.441151 44.056947
2022-06-01 18.069341 7.394303 9.264253 4.351615 20.005025 40.502984 0.412478 39.079513
2022-07-01 16.913401 6.303477 7.203069 3.819279 21.076070 44.358010 0.326694 34.239225
2022-08-01 17.376960 5.824824 6.143558 3.578472 21.313493 45.381963 0.380730 32.923814

This page was last regenerated on:

import datetime
datetime.date.today().strftime('%Y-%m-%d')
'2022-12-04'