HW1 - Interactive Visualizations

Databases and Plotly Visualization - Climate Data

For this tutorial we’re going to look into building SQLite databases from NOAA climate data and then wrangling that data to create interactive visualizations in Plotly.

First, lets import the requisite libraries.

import pandas as pd
import numpy as np
import sqlite3

Creating a Database

Next, we’ll read in our temperature data from the .csv (which should be stored in the same working directory as this notebook).

temps = pd.read_csv("temps_stacked.csv")
temps.head()
ID Year Month Temp
0 ACW00011604 1961 1 -0.89
1 ACW00011604 1961 2 2.36
2 ACW00011604 1961 3 4.72
3 ACW00011604 1961 4 7.73
4 ACW00011604 1961 5 11.28

…And then our country code csv (which we will use to interpret the weather station ID’s)

countries = pd.read_csv('countries.csv')
countries.head()
FIPS 10-4 ISO 3166 Name
0 AF AF Afghanistan
1 AX - Akrotiri
2 AL AL Albania
3 AG DZ Algeria
4 AQ AS American Samoa

We then need to remove the whitespace in our column names so that everything play’s nice with SQL.

# whitespaces in column names are bad for SQL
countries = countries.rename(columns= {"FIPS 10-4": "FIPS_10-4", "ISO 3166": "ISO_3166"})
countries.head()
FIPS_10-4 ISO_3166 Name
0 AF AF Afghanistan
1 AX - Akrotiri
2 AL AL Albania
3 AG DZ Algeria
4 AQ AS American Samoa

Lastly, we’ll read in our weather station geo-data from a url:

url = "https://raw.githubusercontent.com/PhilChodrow/PIC16B/master/datasets/noaa-ghcn/station-metadata.csv"
stations = pd.read_csv(url)
stations.head()
ID LATITUDE LONGITUDE STNELEV NAME
0 ACW00011604 57.7667 11.8667 18.0 SAVE
1 AE000041196 25.3330 55.5170 34.0 SHARJAH_INTER_AIRP
2 AEM00041184 25.6170 55.9330 31.0 RAS_AL_KHAIMAH_INTE
3 AEM00041194 25.2550 55.3640 10.4 DUBAI_INTL
4 AEM00041216 24.4300 54.4700 3.0 ABU_DHABI_BATEEN_AIR

Now we get into the sqlite. For each of our dataframes, we will invoke the .to_sql function which will read these df’s to our local sqlite mock database.

# open a connection to temps.db so that you can 'talk' to it using python
conn = sqlite3.connect("temps.db")
temps.to_sql("temperatures", conn, if_exists="replace", index=False)
countries.to_sql("countries", conn, if_exists="replace", index=False)
stations.to_sql("stations", conn, if_exists="replace", index=False)
# always close your connection
conn.close()

Querying our Database

Next we’ll write a function to query our data base, which accepts four arguments:

country: a string giving the name of a country (e.g. ‘South Korea’) for which data should be returned.

year_begin and year_end: two integers giving the earliest and latest years for which should be returned.

month: an integer giving the month of the year for which should be returned.

The return value of query_climate_database() is a Pandas dataframe of temperature readings for the specified country, in the specified date range, in the specified month of the year.

def query_climate_database(country, year_begin, year_end, month) :
    """
    Function to return a Pandas dataframe of temperature readings for the 
    specified country, in the specified date range, in the specified month of the year,
    by querying an sqlite database
    
    Parameters
    ----------
    country: str - the country to pull for
    year_begin: str - start year for the query
    year_end: str - end year for the query
    month: str - month of the year to query for across specified year range
    
    Returns
    -------
    df - pandas dataframe constructed by function
    """
    
    conn = sqlite3.connect("temps.db") #open connection to temps database
    
    #condition the sql command to include specified columns
    cmd = \
    """
    SELECT C.name, S.name, S.latitude, S.longitude, T.year, T.month, T.temp
    FROM temperatures T
    LEFT JOIN stations S ON T.id = S.id
    LEFT JOIN countries C ON SUBSTRING(S.id, 1, 2) = C."FIPS_10-4"
    WHERE C.name = (?) AND T.year >= (?) AND T.year <= (?) AND T.month = (?)
    """
    
    #read data for our given command and connection to a pandas dataframe
    df = pd.read_sql(cmd, conn, params=(country, year_begin, year_end, month))
    conn.close() #close connection
    
    return df #return our new dataframe
    

Now let’s test this function to make sure it works…

query_climate_database(country = "India", year_begin = 1980, year_end = 2020, month = 1)
Name NAME LATITUDE LONGITUDE Year Month Temp
0 India PBO_ANANTAPUR 14.583 77.633 1980 1 23.48
1 India PBO_ANANTAPUR 14.583 77.633 1981 1 24.57
2 India PBO_ANANTAPUR 14.583 77.633 1982 1 24.19
3 India PBO_ANANTAPUR 14.583 77.633 1983 1 23.51
4 India PBO_ANANTAPUR 14.583 77.633 1984 1 24.81
... ... ... ... ... ... ... ...
3147 India DARJEELING 27.050 88.270 1983 1 5.10
3148 India DARJEELING 27.050 88.270 1986 1 6.90
3149 India DARJEELING 27.050 88.270 1994 1 8.10
3150 India DARJEELING 27.050 88.270 1995 1 5.60
3151 India DARJEELING 27.050 88.270 1997 1 5.70

3152 rows × 7 columns

Awesome, it works! Now let’s import plotly express to start working on our visualizations

import plotly.io as pio #only needed if saving figures
from plotly import express as px

Writing our First Geographic Scatter Function

Now, we’ll write a function to plot a visualization of the average yearly change in temperature for a given station. This function will accept five explicit arguments, and an undetermined number of keyword arguments.

country, year_begin, year_end, and month will be as in the previous part.

min_obs: the minimum required number of years of data for any given station. Only data for stations with at least min_obs years worth of data in the specified month should be plotted; the others should be filtered out.

kwargs: additional keyword arguments passed to px.scatter_mapbox(). These can be used to control the colormap used, the mapbox style, etc.

However, we’ll first need to define a function to estimate this yearly change. Let’s do so by computing the first coefficient of a linear regression for each station. Thus, it’s time to bring in our old faithful, the LR model from sklearn.

from sklearn.linear_model import LinearRegression #import LR model from sklearn


def coef(data_group):
    """
    Function to return first order coefficient of model given a grouped df
    
    Parameters
    ----------
    data_group - the given grouped df to process
    
    Returns
    -------
    slope - LR coefficient for year and temp of grouped, input df
    """
    
    X = data_group[["Year"]]
    y = data_group["Temp"]
    LR = LinearRegression()
    LR.fit(X, y)
    slope = LR.coef_[0]
    return slope

Now, we can move on to our plotting function. We’ll first invoke our prior query function to build our starting df. Then, we’ll need to group by name (grouping all enteries for a given station together) and dropping those which do not exceed our minimum observation parameter (as measured by calling .size on each grouping via transform). We’ll then apply our linear regression coefficient function to generate a new df, coef, which contains this new calculation, along with the names, latitudes, and longitudes for each entery. From here, we’ll then round our values and feed our original parameters to scatter_mapbox, including the kwargs, which allow the user to tweak all the various input arguments built into this plotly function.

def temperature_coefficient_plot(country, year_begin, year_end, month, min_obs, **kwargs):
    
    """
    Function to create an interactive, geographic plot of estimated average increase
    in temperature over a provided year range, for a given country, for a given month
    
    Parameters
    ----------
    country: str - the country to pull and plot for
    year_begin: str - start year for the query
    year_end: str - end year for the query
    month: str - month of the year to query for across specified year range
    min_obs: int - minimum number of observations for a station to be included
    kwargs: variable arguments fed to the plotly graphing function
    
    Returns
    -------
    fig - the plotly fig generated by the function
    
    """
    
    #use prior defined query function to build our df
    df = query_climate_database(country, year_begin, year_end, month)
    
    #group by station name, find size of group (# of observations at station), add col
    df["obs_count"] = df.groupby(["NAME"])["NAME"].transform(np.size)
    
    #drop all entries where the total observations for that station < minimum observations
    df = df.drop(df[df.obs_count < min_obs].index)
    
    #group our df for a given country, in a given month, over a range of years by station
    coefs = df.groupby(["NAME", "LATITUDE", "LONGITUDE"]).apply(coef)
    coefs = coefs.reset_index()
    
    #rename new column 
    coefs = coefs.rename(columns= {0: "ESTIMATED YEAR INCREASE (C)"})
    
    #round temp values
    coefs = coefs.round(4)
    
    #pass dataframe and parameters to plot the scatter map_box 
    fig = px.scatter_mapbox(coefs, #our df w/ first order coeficients for yearly temp change
                            lat = "LATITUDE", #column name for latitude informataion
                            lon = "LONGITUDE", #column name for longitude information
                            hover_name = "NAME", #hover will display station name
                            **kwargs, #all other keyword args for scatter_mapbox()
                            color = "ESTIMATED YEAR INCREASE (C)",
                            color_continuous_midpoint=0)
    
    #tweak layout and add title
    str_month = (str(month))
    str_start = (str(year_begin))
    str_end = (str(year_end))
    fig.update_layout(title = "Estimates of yearly increase in temperature in " 
                     + country + " years " + str_start + " - " + str_end) 
    
    return fig #return figure to calling scope
                       

With the function written, let’s give it a go:

# assumes you have imported necessary packages
color_map = px.colors.diverging.RdGy_r # choose a colormap

fig = temperature_coefficient_plot("India", 1980, 2020, 1, 
                                   min_obs = 10,
                                   zoom = 2,
                                   mapbox_style="carto-positron",
                                   color_continuous_scale=color_map)

fig.show()
pio.write_html(fig,"hw1_geo1") #comment out if not saving

Cool, look’s nice! Let’s try another country with another time window just for good measure:

#now for another country
color_map = px.colors.diverging.RdGy_r # choose a colormap

fig = temperature_coefficient_plot("Mexico", 1975, 2020, 1, 
                                   min_obs = 10,
                                   zoom = 2,
                                   mapbox_style="carto-positron",
                                   color_continuous_scale=color_map)

fig.show()
pio.write_html(fig,"hw1_geo2") #comment out if not saving

So, let’s examine this idea starting with our new query function:

#function to query accross a latitude range
def lat_query(lat_start, lat_end):
    """
    Function to return a Pandas dataframe of temperature readings for a specified
    latitude range by querying an sqlite database
    
    Parameters
    ----------
    lat_start - int: the start of the latitude range to query for
    lat_end - int: the end of the latitude range to query for
    
    Returns
    -------
    df - pandas dataframe constructed by function
    """
    conn = sqlite3.connect("temps.db") #open connection to temps database
    
    #write command 
    cmd = \
    """
    SELECT C.name, S.name, S.latitude, S.longitude, T.year, T.month, T.temp
    FROM temperatures T
    LEFT JOIN stations S ON T.id = S.id
    LEFT JOIN countries C ON SUBSTRING(S.id, 1, 2) = C."FIPS_10-4"
    WHERE S.latitude > (?) AND S.latitude < (?) 
    """
    #"""
    #SELECT S.name, S.latitude, S.longitude, T.year, T.month, T.temp
    #FROM temperatures T
    #LEFT JOIN stations S ON T.id = S.id
    #WHERE S.latitude > (?) AND S.latitude < (?)
    #"""
    
    #read data for our given command and connection to a pandas dataframe
    df = pd.read_sql(cmd, conn, params=(lat_start, lat_end))
    conn.close() #close connection
    
    return df #return our new dataframe

df = lat_query(-30, 30)
df
Name NAME LATITUDE LONGITUDE Year Month Temp
0 United Arab Emirates SHARJAH_INTER_AIRP 25.333 55.517 1933 5 28.9
1 United Arab Emirates SHARJAH_INTER_AIRP 25.333 55.517 1933 6 28.8
2 United Arab Emirates SHARJAH_INTER_AIRP 25.333 55.517 1933 7 32.0
3 United Arab Emirates SHARJAH_INTER_AIRP 25.333 55.517 1933 8 34.8
4 United Arab Emirates SHARJAH_INTER_AIRP 25.333 55.517 1933 9 30.2
... ... ... ... ... ... ... ...
1983795 Zimbabwe GWELO -19.430 29.750 1970 8 15.4
1983796 Zimbabwe GWELO -19.430 29.750 1970 9 20.4
1983797 Zimbabwe GWELO -19.430 29.750 1970 10 20.3
1983798 Zimbabwe GWELO -19.430 29.750 1970 11 21.3
1983799 Zimbabwe GWELO -19.430 29.750 1970 12 21.5

1983800 rows × 7 columns

Graphing Function 1.5 - Latitude Ranges Test

As a first test of the prior query, and the idea in general, let’s do a little remix of our prior geographic plot function, this time for a spatial, north/south range, rather than for a temporal one:

def temperature_coef_lat_plot(lat_start, lat_end, delta_threshold, **kwargs):
    
    """
    Function to create an interactive, geographic plot of estimated average increase
    in temperature over a provided latitude range
    
    Parameters
    ----------
    lat_start - int: the start of the latitude range to query for
    lat_end - int: the end of the latitude range to query for
    delta_threshold - int: threshold for inclusion in the plot (avoids say, every weather
    station in America from being plotted)
    kwargs: variable arguments fed to the plotly graphing function
    
    Returns
    -------
    fig - the plotly fig generated by the function
    """
    
    #use prior defined query function to build our df
    df = lat_query(lat_start, lat_end)
    
    #group our df by station
    coefs = df.groupby(["NAME", "LATITUDE", "LONGITUDE"]).apply(coef)
    coefs = coefs.reset_index()
    
    #rename new column 
    coefs = coefs.rename(columns= {0: "ESTIMATED YEAR INCREASE (C)"})
    
    #round temp values
    coefs = coefs.round(4)
    
    #drop all entries where the average yearly temp delta is below our threshold
    coefs = coefs.drop(coefs["ESTIMATED YEAR INCREASE (C)"][coefs["ESTIMATED YEAR INCREASE (C)"] < delta_threshold].index)
    
    #pass dataframe and parameters to plot the scatter map_box 
    fig = px.scatter_mapbox(coefs, #our df w/ first order coeficients for yearly temp change
                            lat = "LATITUDE", #column name for latitude informataion
                            lon = "LONGITUDE", #column name for longitude information
                            hover_name = "NAME", #hover will display station name
                            **kwargs, #all other keyword args for scatter_mapbox()
                            color = "ESTIMATED YEAR INCREASE (C)",
                           color_continuous_midpoint=0)
    
    #tweak layout and add title
    str_start = (str(lat_start))
    str_end = (str(lat_end))
    str_thres = (str(delta_threshold))
    fig.update_layout(title = "Estimates of Yearly Increase in Temperature from "
                      + str_start + " to " + str_end + " Degrees Latitude")
    
    return fig #return figure to calling scope
   
    
    #Note: I wrote one of the "coefs" above as "coef" and thus kept getting a "local 
    #variable referenced before assignment" for .apply(coef)...most baffling single letter
    #typo turned bug I've had in a long time...it thought coefs the function was the local
    #scope variable typo many lines down...I only rewrote half of it before I realized...

Now let’s test it:

fig = temperature_coef_lat_plot(60, 90, .2, zoom = 1,
                                   mapbox_style="carto-positron",
                                   color_continuous_scale=color_map)

fig.show()
pio.write_html(fig,"hw1_geo3")

Graphing Function 2 - Density Heatmap

Now that we’ve seen the concept in action, let’s try out an entirely different sort of plot, a density heatmap (2dhistogram). Now this is the real meat and potatoes of our idea. We’re going to write a function to create a plot that visually aggregates the average of the estimated average temperature change for a given latitude range. Here the bins (nxbins and nybins kwargs fed to the plotting function) determine the spatial resolution of our plot. The ratios of number of xbins to latitude range and number of ybins to longitude determine the x and y size of each bin, i.e. over how large of an area the stations are grouped together to average their yearly temp delta averages. Sounds weird right? It’ll make sense when you see it. It’s basically a flexible, Atari vibes, pixel-art-looking way to visualize temperate change that unlike a cloropleth, isn’t tied to geometry of borders or some other premade polygonal set; it’s visual aggregation you can change the resolution of and clumping behavior of using only a couple whole number inputs.

Here it goes:

def temperature_heat_map(lat_start, lat_end, delta_threshold, **kwargs):
    """
    Function to create an interactive, geographic plot of estimated average increase
    in temperature over a provided latitude range using a heat density map  wherein
    spatial resolution, and by proxy the grouping of temp delta averaging, via the 
    manipulation of bin counts and latitude ranges
    
    Parameters
    ----------
    lat_start - int: the start of the latitude range to query for
    lat_end - int: the end of the latitude range to query for
    delta_threshold - int: threshold for inclusion in the plot (avoids say, every weather
    station in America from being plotted if you're looking for outliers only over a narrow
    latitude band)
    kwargs: variable arguments fed to the plotly graphing function
    
    Returns
    -------
    fig - the plotly fig generated by the function
    """
    
    #use prior defined query function to build our df
    df = lat_query(lat_start, lat_end)
    
    #group our df by station
    coefs = df.groupby(["NAME", "LATITUDE", "LONGITUDE"]).apply(coef)
    coefs = coefs.reset_index()
    
    #rename new column 
    coefs = coefs.rename(columns= {0: "ESTIMATED YEAR INCREASE (C)"})
    
    #round temp values
    coefs = coefs.round(4)
    
    #output strings
    str_start = str(lat_start)
    str_end = str(lat_end)
    
    fig = px.density_heatmap(coefs, x="LONGITUDE", y="LATITUDE", 
                             z="ESTIMATED YEAR INCREASE (C)", histfunc="avg", 
                             color_continuous_midpoint=0,
                             title="Estimates of Yearly Change in Temperature from " 
                             + str_start + " to " + str_end + " Degrees Latitude", 
                             **kwargs)
    

    return fig
#call function
fig = temperature_heat_map(-90, 90, 0, nbinsx = 72, nbinsy = 36, 
                          color_continuous_scale="rdbu_r")
#show figure
fig.show()

#comment out if you do not wish to save you fig
pio.write_html(fig,"hw4_heatdensity_map")

It makes a lot more sense once you see it, right? From this plot we can even make out actual landmasses…since that’s where the stations are…it’s basically a Minecraft map for ecological destruction. So what can we glean?

  • The planet overall, is getting hotter on average, every year, for the later half of the 20th century as shown by the tint of the continents
  • Polar latitudes are experiencing the highest average changes in both directions; this is where the most tumultuous temperature change rates are occuring compared to the more uniform, milder heating as one heads toward the equator.
  • The Antartic in particular has seen the worst estimated average yearly increases in temperature.
  • Could spatial shifts in artic air currents as the seas warm explain the spots of avg. temperature decrease seen in this area? I know the jet stream is certainly shifting, so things aren’t exactly stable in the North Atlantic.

So this map certainly indicates that the polar latitudes are experiencing the worst, absolute, average changes in temperature…if this was a four plot assignment I’d probably replot the above as relative to preindustrial baseline, calculation, but you get the idea.

Graphing Function 2.5 - Faceting

What would it look like if we broke the above plot down over a variable year range, into a variable number of subdivisions, and then calcuated the average change averages within those temporal subdivisions to better visual this change over time?

Enter the faceted density heatmap. Below we’ll write a function to plot these subdivisions of a given time frame all within the same plot, with consistent binning and scale between them:

def temperature_heat_map_faceted(lat_start, lat_end, start_year, end_year, 
                                 div_period, **kwargs):
    """
    Function to create an interactive, geographic plot of estimated average increase
    in temperature over a provided latitude range, for each of a specificied subdivision
    duration of a specified year span using a faceted heat density map  wherein
    spatial resolution, and by proxy the grouping of temp delta averaging, via the 
    manipulation of bin counts and latitude ranges
    
    Parameters
    ----------
    lat_start - int: the start of the latitude range to query for
    lat_end - int: the end of the latitude range to query for
    start_year: str - start year for the query
    start_year: str - end year for the query
    div_period: int - length in years of each subdivision to plot
    kwargs: variable arguments fed to the plotly graphing function
    
    Returns
    -------
    fig - the plotly fig generated by the function
    """    
    
    
    #query using prior function
    df = lat_query(lat_start, lat_end)
    
    #year range filter
    df = df[df["Year"] >= start_year]
    df = df[df["Year"] <= end_year]


    #loop through periods, moving year window
    start = start_year
    end = start_year + div_period


    #build first period in column
    period_string = str(start) + "-" + str(end)
    df['Period'] = [period_string if (year >= start and year < end) else " " for year in df['Year']]

    #update window
    start = end
    end = end + div_period 

    while(end <= end_year):
        #logical index to fill periods
        df.loc[((df["Year"] >= start) & (df["Year"] < end), 'Period')] = str(start) + "-" + str(end)
        #update windows
        start = end
        end = end + div_period  
    
    #just in case, drop any unfilled vals
    df = df[df["Period"] != " "]
    
    #group our df for a given station, 
    coefs = df.groupby(["NAME", "LATITUDE", "LONGITUDE", "Period"]).apply(coef)
    coefs = coefs.reset_index()
    
    #rename new column 
    coefs = coefs.rename(columns= {0: "ESTIMATED YEAR INCREASE (C)"})
    
    #round temp values
    coefs = coefs.round(4)
    
    #make output strings
    str_start = str(start_year)
    str_end = str(end_year)
    str_div = str(div_period)
    fig = px.density_heatmap(coefs, x="LONGITUDE", y="LATITUDE", 
                             z="ESTIMATED YEAR INCREASE (C)", histfunc="avg", 
                             color_continuous_midpoint=0, facet_row="Period",
                             title="Estimated Yearly Temp. Increase from " + str_start +
                             " to " + str_end + " by " + str_div + " Year Periods",
                             **kwargs)
    
    #shorten color bar (it's quite long otherwise)
    fig.update_layout(coloraxis_colorbar=dict(
    len=0.5, ))

    return fig
fig = temperature_heat_map_faceted(-90, 90, start_year=1980, end_year=2020, div_period=10,
                                    width = 1000, height = 1500, nbinsx = 72, nbinsy = 36,
                                    color_continuous_scale="rdbu_r")

fig.show()
pio.write_html(fig,"hw1_hd_faceted1") #comment out if not saving

Cool, so what does this faceted plot tell us?

  • The 90’s were really rought for the Antarctic - we have +10 degree yearly delta estimates even!
  • The more gradual yearly temp deltas as we approach the equator appear even more subtle when calculated within the decade, indicating that the overall delta estimates are likely skewed by particularly bad sub-periods—the pixel map of continents is replaced by more diffuse demarcations of more severe changes.

Graphing Function 3 - Histogram

So, this final function comes about mostly because the marginal histogram functionality for density heatmaps don’t play nice with averages instead of counts. So to answer the most literal and reduced version of what we’ve been asking, we’re going to make a plain-old histogram to show “What is the relationship between latitude band and estimated yearly temperature change?”

def lat_bar(lat_start, lat_end, start_year, end_year, **kwargs):
    
    """
    Function to create an interactive, histogram of estimated average increase
    in temperature over a provided latitude range, for a  specified time span 
    in years.
    
    Parameters
    ----------
    lat_start - int: the start of the latitude range to query for
    lat_end - int: the end of the latitude range to query for
    start_year: str - start year for the query
    start_year: str - end year for the query
    kwargs: variable arguments fed to the plotly graphing function
    
    Returns
    -------
    fig - the plotly fig generated by the function
    """    
    
    #use prior defined query function to build our df
    df = lat_query(lat_start, lat_end)
    
    #year range filter
    df = df[df["Year"] >= start_year]
    df = df[df["Year"] <= end_year]
    
    #group our df by station
    coefs = df.groupby(["NAME", "LATITUDE", "LONGITUDE"]).apply(coef)
    coefs = coefs.reset_index()
    
    #rename new column 
    coefs = coefs.rename(columns= {0: "ESTIMATED YEAR INCREASE (C)"})
    
    #round temp values
    coefs = coefs.round(4)
    
    #rename new column 
    coefs = coefs.rename(columns= {0: "ESTIMATED YEAR INCREASE (C)"})
    
    #round temp values
    coefs = coefs.round(4)
    
    #output strings
    str_lat1 = str(lat_start)
    str_lat2 = str(lat_end)
    str_start = str(start_year)
    str_end = str(end_year)
    
    fig = px.histogram(coefs, x="LATITUDE", y="ESTIMATED YEAR INCREASE (C)",
            nbins = (abs(lat_end - lat_start)//10), histfunc='avg', 
                    title="Estimated Yearly Temp. Change by Latitude from " + str_start 
                    + " to " + str_end + " between " + str_lat1 + " and "
                    + str_lat2 + '\u00b0')
    
    return fig
    
fig = lat_bar(lat_start=-90, lat_end=0, start_year=1960, end_year=2010)

fig.show()
pio.write_html(fig,"hw1_hist1") #comment out if not saving

So we can see, somewhat more starkly than before, that from 1960 to 2010, looking at the south hemisphere to address the worst decades in terms of change for the highest rate of change area (the Antartic), that average yearly temperature changes get more severe the closer you get to the pole.

Thanks for reading; I hope you learned a bit about plotly.

Written on April 15, 2022