Group project: the Climate System¶
Friesinger, Steger
Instructions¶
Objectives
In this final project, you will apply the methods you learned over the past weeks to answer the questions below.
Deadline
Please submit your project via OLAT before Thursday January 11 at 00H (in the night from Wednesday to Thursday).
Formal requirements
You will work in groups of two. If we are an odd number of students, one group can have three participants. (Tip: I recommend that students who have not followed a programming class to team up with students who have).
Each group will submit one (executed) jupyter notebook containing the code, plots, and answers to the questions (text in the markdown format). Please also submit an HTML version of the notebook. Each group member must contribute to the notebook. The notebook should be self-contained and the answers must be well structured. The plots must be as understandable as possible (title, units, x and y axis labels, appropriate colors and levels…).
Please be concise in your answer. We expect a few sentences per answer at most - there is no need to write a new text book in this project! Use links and references to the literature or your class slides where appropriate.
Grading
We will give one grade per project, according to the following table (total 10 points):
- correctness of the code and the plots: content, legends, colors, units, etc. (3 points)
- quality of the answers: correctness, preciseness, appropriate use of links and references to literature or external resources (5 points)
- originality and quality of the open research question (2 points)
# Import the tools we are going to need today:
import matplotlib.pyplot as plt # plotting library
import numpy as np # numerical library
import xarray as xr # netCDF library
import pandas as pd # tabular library
import cartopy # Map projections libary
import cartopy.crs as ccrs # Projections list
import cartopy.feature as cfeature
# Some defaults:
plt.rcParams['figure.figsize'] = (12, 5) # Default plot size
Part 1 - temperature climatology¶
Open the ERA5 temperature data:
ds = xr.open_dataset('ERA5_LowRes_Monthly_t2m.nc') - 273.15 # convert to °C
ds_sst = xr.open_dataset('ERA5_LowRes_Monthly_sst.nc')
ds_prec = xr.open_dataset('ERA5_LowRes_Monthly_tp.nc') * 1000 #convert to mm
ds_wind = xr.open_dataset('ERA5_LowRes_Monthly_uvslp.nc')
ds = xr.merge([ds, ds_sst, ds_wind, ds_prec])
# Open csv as dataframe with matching length
df = pd.read_csv('co2_mm_gl.csv', skiprows=38, skipfooter=57, parse_dates={'date' : [0, 1]}, index_col='date')
# Add needed column to dataset
ds['co2'] = ('time', df['average'])
ds
C:\Users\michl\AppData\Local\Temp\ipykernel_12320\2067514920.py:12: ParserWarning: Falling back to the 'python' engine because the 'c' engine does not support skipfooter; you can avoid this warning by specifying engine='python'. df = pd.read_csv('co2_mm_gl.csv', skiprows=38, skipfooter=57, parse_dates={'date' : [0, 1]}, index_col='date') C:\Users\michl\AppData\Local\Temp\ipykernel_12320\2067514920.py:12: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format. df = pd.read_csv('co2_mm_gl.csv', skiprows=38, skipfooter=57, parse_dates={'date' : [0, 1]}, index_col='date')
<xarray.Dataset> Dimensions: (longitude: 480, latitude: 241, time: 480) Coordinates: * longitude (longitude) float32 -179.6 -178.9 -178.1 ... 178.1 178.9 179.6 * latitude (latitude) float32 90.0 89.25 88.5 87.75 ... -88.5 -89.25 -90.0 * time (time) datetime64[ns] 1979-01-01 1979-02-01 ... 2018-12-01 Data variables: t2m (time, latitude, longitude) float32 -28.44 -28.44 ... -23.82 sst (time, latitude, longitude) float32 ... u10 (time, latitude, longitude) float32 ... v10 (time, latitude, longitude) float32 ... msl (time, latitude, longitude) float32 ... tp (time, latitude, longitude) float32 0.2937 0.2937 ... 0.3377 co2 (time) float64 336.6 337.3 337.9 338.3 ... 406.6 408.1 409.2 Attributes: Conventions: CF-1.6 history: 2019-11-18 09:36:58 GMT by grib_to_netcdf-2.14.0: /opt/ecmw...
Plot three global maps:
- Compute and plot the temporal mean temperature ¯T for the entire period (unit °C)
- Compute and plot ¯T∗ (see lesson), the zonal anomaly map of average temperature.
- Compute and plot the monthly average temperature for each month ¯TM (annual cycle). I expect a variable of dimensions (month: 12, latitude: 241, longitude: 480). Hint: remember the
.groupby()
command we learned in the lesson. Now plot the average monthly temperature range, i.e. ¯TMmax - ¯TMmin on a map.
Questions:
- Look at the zonal temperature anomaly map.
- Explain why norther Europe and the North Atlantic region is significantly warmer than the same latitudes in North America or Russia.
- Explain why the Northern Pacific Ocean does not have a similar pattern.
- Look at the average monthly temperature range map.
- Explain why the temperature range is smaller in the tropics than at higher latitudes
- Explain why the temperature range is smaller over oceans than over land
- Where is the temperature range largest? Explain why.
Answer:
The temperature difference can be attributed to the ocean currents:
- In the Atlantic, the Gulf Stream brings warm waters from the tropics to the North Atlantic.
- While there are also warm ocean currents in the Pacific, like the Kuro Shio current, their impact on Alska, Canada, and Russia is not as pronounced due to different current patterns.
The temperature range varies due to several factors:
- The temperature range in the tropics is smaller than in higher latitudes due to more constant solar radiation throughout the year.
- The temperature range is smaller over oceans than over land because water has a higher specific heat capacity than land, meaning it takes more energy to change the temperature of water compared to land. As a result, ocean temperatures tend to vary less than land temperatures.
- The temperature range is largest in the heart of Siberia, this is due to the Continental Climate. As we already learned, the heat capacity of land is low, which means it heats up a lot during the summer months and cools a lot throughout the Arctic winter.
# Compute the temporal mean temperature
T_mean = ds['t2m'].mean(dim='time')
# Compute the zonal anomaly map of average temperature
T_star = ds['t2m'].mean(dim='time') - ds['t2m'].mean(dim=['time', 'longitude'])
# Compute the monthly average temperature for each month
T_M = ds['t2m'].groupby('time.month').mean(dim='time')
# Compute the average monthly temperature range
T_M_range = T_M.max(dim='month') - T_M.min(dim='month')
# Plot the temporal mean temperature
T_mean.plot()
plt.title('Temporal Mean Temperature')
plt.show()
# Plot the zonal anomaly map of average temperature
T_star.plot()
plt.title('Zonal Anomaly Map of Average Temperature')
plt.show()
# Plot montly average temperature (annual cycle)
T_M.mean(dim=['longitude', 'latitude']).plot(figsize=[12,5])
plt.title('Annual cycle of mean temperature')
plt.show()
levels = np.linspace(0,60,11)
cmap = 'YlGnBu'
# Plot the average monthly temperature range
T_M_range.plot(cmap='Reds')
plt.title('Average Monthly Temperature Range')
plt.show()
Part 2 - Precipitation climatology¶
Open the precipitation file and explore it. The units of monthly precipitation are wrongly labeled (unfortunately). They should read: m per day.
Using .groupby()
, compute the average daily precipitation for each month of the year (I expect a variable of dimensions (month: 12, latitude: 241, longitude: 480)). Convert the units to mm per day. Plot a map of average daily precipitation in January and in August with the levels [0.5, 1, 2, 3, 4, 5, 7, 10, 15, 20, 40]
and the colormap `YlGnBu'
Questions:
- Describe the location of the ITCZ in January and February. Without going into the details, explain (in one or two sentences)
- Describe the precipitation seasonality in West Africa and in India. Name the phenomenon at play.
Answer:
- The ITCZ extends in the south of the äquator during Jannuary and February becaus of the tilt of the earth. It moves abou 40° of latitude between summer and winter. (keep in mind the ocean heats more slowly. delay.)
- Its due to the movemend of the ITCZ. Rainy season in west africa and India is in the summer months and is called monsoon. Warm water in the ocean evaporats and gets advected over land.
month_dict = {
1: 'January',
2: 'February',
3: 'March',
4: 'April',
5: 'May',
6: 'June',
7: 'July',
8: 'August',
9: 'September',
10: 'October',
11: 'November',
12: 'December'
}
# Compute the average daily precipitation for each month
monthly_precip = ds['tp'].groupby('time.month').mean('time')
# Define the levels and colormap for the plot
levels = [0.5, 1, 2, 3, 4, 5, 7, 10, 15, 20, 40]
cmap = 'YlGnBu'
# Create a single figure with two subplots
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12,10), subplot_kw={'projection':ccrs.PlateCarree()})
# Plot average daily precipitation for January
month = 1
monthly_precip.sel(month=month).plot(ax=ax1, levels=levels, cmap=cmap, transform=ccrs.PlateCarree())
ax1.add_feature(cfeature.COASTLINE)
gridlines = ax1.gridlines(draw_labels=True)
gridlines.bottom_labels = False
gridlines.right_labels = False
ax1.set_title(f'Average Daily Precipitation in {month_dict[month]}', rotation=90, x=-0.1, y=0)
# Plot average daily precipitation for August
month = 8
monthly_precip.sel(month=month).plot(ax=ax2, levels=levels, cmap=cmap, transform=ccrs.PlateCarree())
ax2.add_feature(cfeature.COASTLINE)
gridlines = ax2.gridlines(draw_labels=True)
gridlines.top_labels = False
gridlines.right_labels = False
ax2.set_title(f'Average Daily Precipitation in {month_dict[month]}', rotation=90, x=-0.1, y=0)
plt.savefig('rain.png')
plt.show()
Part 3: sea-level pressure and surface winds¶
Open the file containing the surface winds (u10
and v10
) and sea-level pressure (msl
).
Compute [¯SLP] (the temporal and zonal average of sea-level pressure). Convert it to hPa, and plot it (line plot). With the help of plt.axhline, add the standard atmosphere pressure line to the plot to emphasize high and low pressure regions. Repeat with [¯u10] and [¯v10] (in m s−1) and add the 0 horizontal line to the plot (to detect surface westerlies from easterlies for example).
Questions:
- Based on your knowledge about the general circulation of the atmosphere, explain the latitude location of the climatological high and low pressure systems of Earth.
- Similarly, explain the direction and strength of the zonal and meridional winds on earth (tip: the sea-level pressure plot helps)
Answer:
Climatological High and Low Pressure Systems:
High pressure systems at ~30°/90° becaue of the desend of air adiabatically, foreced by general circulation. Low pressure systems at 0° because of solar heating and the rising of airmasses. At 60° becaues of mid latitude cyclons that try to bring the energy to an equalibrium.
Zonal and Meridional Winds:
In general high pressure areas have diverging winds, low pressure areas have converging winds. As we can see in the graf, switch of sign at the pressure peaks. The Meridional winds are due to those climatological high and low pressure systems. The zonal winds are due to the coriolis effekt that modifys the Meriodal flow. The zonal winds are in general stronger.
ds = ds.rename({'u10':'Zonal Winds', 'v10':'Meridional Winds'})
# Compute the temporal and zonal average of sea-level pressure
avg_slp = ds['msl'].mean(dim=['time', 'longitude'])
# Convert it to hPa
avg_slp_hpa = avg_slp / 100
fig, ax1 = plt.subplots(figsize=(10, 6))
color = 'tab:red'
ax1.axhline(y=1013.25, color='r', linestyle='--', label='standard atmosphere pressure') # standard atmosphere pressure
ax1.set_xlabel('Latitude')
ax1.set_ylabel('Pressure (hPa)', color=color)
ax1.plot(avg_slp_hpa['latitude'], avg_slp_hpa, color=color, label='sea-lvl-pressure') # specify latitude as x-axis
ax1.tick_params(axis='y', labelcolor=color)
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
colors = ['blue', 'lightblue']
variables = ['Zonal Winds', 'Meridional Winds']
ax2.set_ylabel('Speed (m/s)') # we already handled the x-label with ax1
ax2.axhline(y=0, color='b', linestyle='--', linewidth=1, label='0-line')
#ax1.axvline(x=)
for var, color in zip(variables, colors):
avg_var = ds[var].mean(dim=['time', 'longitude'])
ax2.plot(avg_var['latitude'], avg_var, color=color, label=var) # specify latitude as x-axis
ax2.tick_params(axis='y', labelcolor=color)
# Adjust the y-axis limits so that s_msl corresponds to 0
ax1.set_ylim(bottom=983, top=1043.5)
ax2.set_ylim(bottom=-8, top=8)
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.title('Temporal and Zonal Average of Sea-Level Pressure and Wind Speeds')
# Add legends
ax1.legend(loc='upper left')
ax2.legend(loc='upper right')
plt.savefig('msl.png')
plt.show()
Part 4: temperature change and CO2 concentrations¶
Download the global average CO2 concentration timeseries data in the CSV format (source: NOAA). Here, let me help your read them using pandas:
Prepare three plots:
- plot the monthly global CO2 concentration as a function of time.
- plot the annual average timeseries of global CO2 concentration as a function of time.
- plot the annual average timeseries of global CO2 concentration and of global 2m temperature from ERA5 on the same plot (using a secondary y axis for temperature).
Questions:
- Describe and explain the annual cycle of CO2 concentrations
- What was the CO2 concentration in the atmosphere in the pre-industrial era? Compute the annual increase in CO2 concentration (unit: ppm per year) between 1980 and 1985 and between 2016 and 2021.
- Describe the relationship between global temperatures and CO2 concentrations. Beside CO2, name three processes that can influence temperature variability and change at the global scale.
Answer:
- The anual cycle of CO2 is due to the seasonalyty, because most of the landmass is in the northern hemisphere.
- In the pre-industrial era the co2 concentration was ~ 342 ppm. The annual increase back than was ~ 1.33 ppm/year. Nowadays (2013-2018) the annual increase is ~ 2.52 ppm/year.
- co2 absorbs radiation in the infrared this absorbtion leads to an increas of temperature. Earths black body radiation peaks arund 10 μm and extends far in the infrared.
- albedo: i.e. Volcanic arubtions can change the albedo
- other gases in the atmosphere: i.e. a warmer atmospher can hold more H2O another important greenhousgas
- land usage: i.e. diffrent landsurfaces have diffrent brightness.
def slope(ds):
y = ds.values
x = np.arange(len(y))
return np.polyfit(x, y, 1)[0]
annual_avg = ds['co2'].resample(time='Y').mean()
pre_idust = annual_avg.sel(time=slice('1980', '1985'))
idust = annual_avg.sel(time=slice('2013', '2018'))
co2_pi = pre_idust.mean()
co2_i = idust.mean()
print(f'pre industrial incrase (1980-1985) = {slope(pre_idust):.2f} ppm/year')
print(f'industrial incrase in (2013-2018) = {slope(idust):.2f} ppm/year')
print(f'pre industrial co2 concentratin (1980-1985): {co2_pi.values:.2f} ppm')
print(f'industrial co2 concentratin (2013-2018): {co2_i.values:.2f} ppm')
# Compute the global temp for every timestep resample by years and get means
avg_temp = ds['t2m'].mean(dim=['longitude', 'latitude']).resample(time='Y').mean()
# Initialize figure
fig, ax1 = plt.subplots(figsize=(10, 6))
# Plot monthly and yearly average of CO2 concentration
color = 'tab:blue'
ax1.set_xlabel('Year')
ax1.set_ylabel('CO2 Concentration (ppm)', color=color)
ln1 = ax1.plot(ds['time'], ds['co2'], linewidth=1, color='black', label='Monthly CO2 Concentration')
ln2 = ax1.plot(annual_avg['time'], annual_avg, linewidth=3, color=color, label='Annual Mean CO2')
ax1.tick_params(axis='y', labelcolor=color)
# Create second y-axis
ax2 = ax1.twinx()
# Plot yearly average temperature and convert it from Kelvin to Celsius
color = 'tab:red'
ax2.set_ylabel('Temperature (°C)', color=color)
ln3 = ax2.plot(avg_temp['time'], avg_temp, linewidth=3, color=color, label='Annual Mean Temperature')
ax2.tick_params(axis='y', labelcolor=color)
# Plot title
plt.title('Annual Average Global CO2 Concentration and 2m Temperature')
# Add a grid
plt.grid(True)
# Add a common legend
lns = ln1 + ln2 + ln3
labs = [l.get_label() for l in lns]
ax1.legend(lns, labs, loc='upper left')
plt.show()
pre industrial incrase (1980-1985) = 1.33 ppm/year industrial incrase in (2013-2018) = 2.52 ppm/year pre industrial co2 concentratin (1980-1985): 342.00 ppm industrial co2 concentratin (2013-2018): 401.38 ppm
Part 5: variability and ENSO (open research question)¶
As of November 2023, the world is currently experiencing El Niño conditions, with projections indicating its persistence through the upcoming spring (source). To understand the implications and compare with La Niña, we will utilize our available data.
Using your learned skills, you should create global anomalie maps of sea-surface temperature, precipitation, and air temperature, comparing one El Niño with one La Niña year. Describe the anomaly patterns you are seeing in your plots (e.g. where do we see an increase/decrease in precipitation).
We suggest to look for literature or a google search on strong ENSO events in the past 40 years, and select one good example for a positive and a negative phase. With citation or links, explain why you picked these years as examples. I used this Website to find good examples for positive and negative phase. I looked for phases where every month was in the top rankings.
def compute_anomalies(ds, variable, start_year, end_year):
"""Compute the zonal anomaly map of average for a given variable and time period.
Parameters:
ds (xarray.Dataset): The dataset containing the variable data.
variable (str): The variable to compute anomalies for.
start_year (str): The start year of the period.
end_year (str): The end year of the period.
Returns:
xarray.DataArray: The anomalies for the specified variable and period.
"""
# Compute the long-term mean
long_term_mean = ds[variable].mean(dim='time')
# Compute the mean for the specified period
period_mean = ds.sel(time=slice(start_year, end_year))[variable].mean(dim='time')
# Compute the anomalies
anomalies = period_mean - long_term_mean
return anomalies
variables = ['t2m', 'sst', 'tp']
periods = {'El Nino' : ('1997-06-01', '1998-05-01'),
'El Nino2' : ('1982-07-01', '1983-07-01'),
'La Nina' : ('1999-06-01', '2000-05-01'),
'La Nina2' : ('2010-07-01', '2011-04-01')
}
col = len(periods)
row = len(variables)
fig, axs = plt.subplots(row, col, figsize=(col*12, row*5))
# Add a title for the whole figure
fig.suptitle('Strong El Nino / La Nina seasons anomalies against longterm mean', fontsize=32)
for i, variable in enumerate(variables):
for j, (period_name, (start_year, end_year)) in enumerate(periods.items()):
# Compute anomalies
anomalies = compute_anomalies(ds, variable, start_year, end_year)
# trash outliers to get a better readable plot
mask = anomalies > 10
count = mask.sum().values
print(f'{period_name}:{count}')
#more deatail
anomalies = anomalies.where(anomalies < 10, np.nan)
# Choose a colormap
cmap = 'RdBu_r' if variable != 'tp' else 'RdBu'
# Plot the anomalies
anomalies.plot(ax=axs[i, j], cmap=cmap)
# Add a title for each row
if j == 0:
axs[i, j].set_ylabel(f'{variable.capitalize()}', rotation=90, fontsize=16, )#labelpad=30)
# Add a title for each column
if i == 0:
axs[i, j].set_title(f'{period_name} ({start_year},{end_year})', fontsize=16, pad=20)
plt.tight_layout(rect=[0, 0, 1, 0.96]) # Adjust for the suptitle
plt.savefig('anomalies.png')
plt.show()
El Nino:0 El Nino2:0 La Nina:0 La Nina2:0 El Nino:0 El Nino2:0 La Nina:0 La Nina2:0 El Nino:12 El Nino2:6 La Nina:0 La Nina2:0