Abstract

The following code accesses snow and weather data from automated weather stations at the Jackson Hole ski resort, Wyoming. Optional arguments allow the user to select a station for snow data, a station for wind data, and a time resolution (either 1 hr or 15 min). Plots are generated using current data for recent conditions. Whether you're looking to check out ski conditions or if you are monitoring changes in weather and snowpack for avalanche hazard, this script will be a good tool. It is set up to accept other stations relatively easy by a few additions to the code.

Intro

The BTNF Avalanche forecast center has a number of existing plots linked to their website, however these plots are not combining ideal data and time periods for exactly what I would like to see. This program accesses urls containing recent measurements from automated on-mountain stations, and then parses and loads the data into a format that can be plotted. In this case, the data happens to be very conveniently formatted as tab-delimited .txt files for each station, available at either 15 min or 1 hr resolution. This data is archived and maintained here, under the 'Raw Data Directory' link. (update: also available directly through main BTAC area forecast pages)

In [43]:
from IPython.display import HTML
HTML('')
Out[43]:

Raw data is then listed as:

In [44]:
HTML('')
Out[44]:

As an example, we'll access data from the Rendezvous Bowl Study Plot and the Summit wind station. I'm interested in current time series of total snow depth, new snow received, water content of new snow, air temperature, wind speed, max wind gust, and wind direction. All of these measurements are recorded at 15 minute intervals at the automated stations. Here are some site photos from the Rendezvous Bowl plot:

In [45]:
HTML('')
Out[45]:

The program

First, I import necesarry modules, state the usage, and define command line arguments that will be passed to the script. For the purposes of running the code in this notebook, I have commented out the actual sys.argv[] assignments, and manually entered some arguments.

In [46]:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from pandas.tseries.offsets import *
import datetime as dt
import sys

# change the following two variables each season:
year1 = '2015'
year2 = '2016'

TEMP_SNOW_STNS = ['raymer','mid','rendezvous']
WIND_STNS = ['raymer', 'summit']
TIME_RES = ['1h', '15min']


def usage():
    print """Usage:
    python snow_scraper_v2.py    
     is snow and air temp data, can be "raymer", "mid", or "rendezvous"
     is wind data, can be "raymer" or "summit"
     is time resolution, can be "1h" for 1 hour, or "15min" for 15 min"""

#def bail():
#  usage()
#  sys.exit(-1)

# Check number of args
#if len(sys.argv) < 4:
#  bail()

# Check valid args...
#if sys.argv[1] not in TEMP_SNOW_STNS:
#  bail()
#if sys.argv[2] not in WIND_STNS:
#  bail()
#if sys.argv[3] not in TIME_RES:
#  bail()

#assign arguments to local variables
#snow_site = sys.argv[1]
#wind_site = sys.argv[2]
#time_res = sys.argv[3]

snow_site = "rendezvous"
wind_site = "summit"
time_res = "1h"

print 'plotting data for...',snow_site, wind_site, time_res
plotting data for... rendezvous summit 1h

Now, tweak all the file import parameters, and define which sites and time-resolutions we want. This is a somewhat thick piece of code that could be more concise, but it allows the user to go directly to the import parameters for each station and time combination to edit or troubleshoot. It also provides a format for additional stations to be dropped into the list. The execution of the conditional statements depends on the input sys.argv[] parameters, and this block of code will output the path and appropriate parsing and plotting parameters for one snow data station, and one wind data station. A bit of brute force programming, but it does the job:

In [47]:
#-----------------------------------------------------------------------------------------
# SETUP PARAMETERS FOR FILE IMPORT
#-----------------------------------------------------------------------------------------

# SNOW SITES, 1 HR
if snow_site == 'raymer' and time_res == '1h':
  url_snow = 'http://wxstns.net/wxstns/jhnet/RAYMER.txt'
  title = "Raymer Study Plot 9,360' (hourly)"
  name_list=['Date','Time','NaN1','Battery','NaN2','NaN3','Air_Temp','NaN4','NaN5',
  'RH','NaN6','NaN7','Int_snow','NaN8','NaN9','Ttl_Snow','NaN9','Precip']
  skiprows_snow = 6
if snow_site == 'mid' and time_res == '1h':
  url_snow = 'http://wxstns.net/wxstns/jhnet/MID.txt'
  title = "Mid Mountain Study Plot 8,180' (hourly)"
  name_list=['Date','Time','NaN1','Battery','NaN2','NaN3','Air_Temp','NaN4','NaN5',
  'RH','NaN6','NaN7','Int_snow','NaN8','NaN9','Ttl_Snow','NaN9','Precip']
  skiprows_snow = 6
if snow_site == 'rendezvous' and time_res == '1h':
  url_snow = 'http://wxstns.net/wxstns/jhnet/RVBOWL.txt'
  title = "Rendezvous Bowl Plot 9,580' (hourly)"
  name_list=['Date','Time','NaN1','Battery','NaN2','NaN3','Air_Temp','NaN4','NaN5',
  'Int_snow','NaN8','NaN9','Ttl_Snow','NaN9','Precip']
  skiprows_snow = 7
  
# SNOW SITES, 15 MIN
if snow_site == 'raymer' and time_res == '15min':
  url_snow = 'http://wxstns.net/wxstns/jhnet/RAYMER2.txt'
  title = "Raymer Study Plot 9,360' (15 min)"
  name_list=['Date','Time','NaN1','Battery','NaN2','NaN3','Air_Temp','NaN4','NaN5',
  'RH','NaN6','NaN7','Int_snow','NaN8','NaN9','Ttl_Snow','NaN9','Precip']
  skiprows_snow = 6
if snow_site == 'mid' and time_res == '15min':
  url_snow = 'http://wxstns.net/wxstns/jhnet/MID2.txt'
  title = "Mid Mountain Study Plot 8,180' (15 min)"
  name_list=['Date','Time','NaN1','Battery','NaN2','NaN3','Air_Temp','NaN4','NaN5',
  'RH','NaN6','NaN7','Int_snow','NaN8','NaN9','Ttl_Snow','NaN9','Precip']
  skiprows_snow = 6
if snow_site == 'rendezvous' and time_res == '15min':
  url_snow = 'http://wxstns.net/wxstns/jhnet/RVBOWL2.txt'
  title = "Rendezvous Bowl Plot 9,580' (15 min)"
  name_list=['Date','Time','NaN1','Battery','NaN2','NaN3','Air_Temp','NaN4','NaN5',
  'Int_snow','NaN8','NaN9','Ttl_Snow','NaN9','Precip']
  skiprows_snow = 7
  
# WIND SITES, 1 HR
if wind_site == 'raymer' and time_res == '1h':
  url_wind = 'http://wxstns.net/wxstns/jhnet/RAYMRWND.txt'
  name_list_wind=['Date','Time','NaN1','Battery','NaN2','NaN3','Wind_Speed',
  'NaN4','Wind_Dir', 'NaN5','NaN6','Max_Gust']
  wind_title = "Wind at Raymer, 9,360'"
  skiprows_wind = 6
if wind_site == 'summit' and time_res == '1h':
  url_wind = 'http://wxstns.net/wxstns/jhnet/SUMMIT.txt'
  name_list_wind=['Date','Time','NaN1','NaN2','Summit_Air_Temp','NaN3','NaN4','Wind_Speed',
  'NaN5', 'Wind_Dir', 'NaN6', 'NaN7', 'Max_Gust'] 
  wind_title = "Wind at Summit, 10,450'"
  skiprows_wind = 7
  
# WIND SITES, 15 MIN 
if wind_site == 'raymer' and time_res == '15min':
  url_wind = 'http://wxstns.net/wxstns/jhnet/RAYMRWND2.txt'
  wind_title = "Wind at Raymer, 9,360'"
  name_list_wind=['Date','Time','NaN1','Battery','NaN2','NaN3','Wind_Speed',
  'NaN4','Wind_Dir', 'NaN5','NaN6','Max_Gust']
  skiprows_wind = 6
if wind_site == 'summit' and time_res == '15min':
  url_wind = 'http://wxstns.net/wxstns/jhnet/SUMMIT2.txt'
  wind_title = "Wind at Summit, 10,450'"
  name_list_wind=['Date','Time','NaN1','NaN2','Summit_Air_Temp','NaN3','NaN4','Wind_Speed',
  'NaN5', 'Wind_Dir', 'NaN6', 'NaN7', 'Max_Gust'] 
  skiprows_wind = 7

Now, we scrape the data from the online archive. I explored many options for this procedure which all seemed like valid options, including lxml, html5lib, and Beautiful Soup. In the end, I wanted my data in a Pandas dataframe, for the conveniences of working with timeseries data that come with Pandas. I discovered the pd.io.parsers.read_csv() function in Pandas, and it turned out that with the straightfoward formatting of the .txt files, the Python parsing engine associated with this function did the trick. Perfect. Now the data reads straight into a Pandas dataframe. It took a fair bit of experimentation and head-scratching to figure out all the proper parsing, assignment of dataframe columns to header names, and conversion to a Pandas datetime index, but in the end it boiled down to the following code.

In [48]:
#----------------------------------------------------------------------------
# FORMAT FOR SNOW DATA
#----------------------------------------------------------------------------

# Read in data:
data = pd.io.parsers.read_csv(url_snow, skiprows=skiprows_snow, sep='  ', engine='python', 
skipfooter=1, index_col=False, header=None, names=name_list)

# Create a year string, based on first digit of month
year = np.ones(len(data['Date']), dtype=int) # dummy array
month_leading_digit = data['Date'].map(lambda x: str(x)[:-4])  

for index, value in enumerate(data['Date']):
  if month_leading_digit[index] == '0':
    year[index] = year2
  if month_leading_digit[index] == '1':
    year[index] = year1
    
year_string = np.array(map(str, year))

# Get a datestring that the 'infer_datetime_format' function can recognize
datestring = data['Date'].apply(str) + '/' + year_string + data['Time'].apply(str)

# Convert string to a Pandas datetime object
dt = pd.to_datetime(datestring, infer_datetime_format=True)

# set index of dataframe to datetime
data_date = data.set_index(dt)

# routine to get last 24 new snow and water content totals:
current = data_date.index[0]
#minus24 = current - np.timedelta64(24,'h')
minus12 = current - np.timedelta64(12,'h')
subset = data_date[current:minus12]
maxlast12 = subset.Int_snow.max()

if time_res == '1h':
  maxlast12_index = subset.Int_snow.idxmax() #+ pd.DateOffset(hours=1)
if time_res == '15min':
  maxlast12_index = subset.Int_snow.idxmax() #+ pd.DateOffset(minutes=15)

waterlast24_range = data_date.Precip[maxlast12_index:(maxlast12_index - np.timedelta64(24,'h'))]
waterlast24 = waterlast24_range.sum() 

data_date.sort_index(inplace=True) #reverse order of data to plot most recent on right

#----------------------------------------------------------------------------
# FORMAT FOR WIND DATA 
#----------------------------------------------------------------------------

# Read in data:
data_wind = pd.io.parsers.read_csv(url_wind, skiprows=skiprows_wind, sep='  ', engine='python', 
skipfooter=1, index_col=False, header=None, names=name_list_wind)

# Create a year string, based on first digit of month
year = np.ones(len(data_wind['Date']), dtype=int) # dummy array
month_leading_digit = data_wind['Date'].map(lambda x: str(x)[:-4])  

for index, value in enumerate(data_wind['Date']):
  if month_leading_digit[index] == '0':
    year[index] = year2
  if month_leading_digit[index] == '1':
    year[index] = year1
    
year_string = np.array(map(str, year))

#Get a datestring that the 'infer_datetime_format' function can recognize
datestring = data_wind['Date'].apply(str) + '/' + year_string + data_wind['Time'].apply(str)

#Convert string to a Pandas datetime object
dt_wind = pd.to_datetime(datestring, infer_datetime_format=True)

#set index of dataframe to datetime
data_wind_date = data_wind.set_index(dt_wind)
data_wind_date.sort_index(inplace=True)

Now we have all necessary data for plotting. The plotting code is quite thick and time-consuming to write, but in the end, we have a plot that can ingest any of the defined data combinations, with neccesarry axes defined to modulate with the scale of incoming data.

Here is the result:

In [3]:
from IPython.display import Image
Image(filename='20160130_event.png') 
Out[3]:

Looking at the latest storm system to hit the Teton area (late Jan. 2016), there were snow totals of around 20" reported for Jan 30. Winds during this event were from the SW to W, with gusts at Rendezvous summit to 40-45 mph. The storm system arrives relatively warm, and cools throughout the 30th and 31st. Also to note: the two days preceeding the precip event both showed air temperature spikes to above freezing at 9,500'.

As currently implemented, with simple changes to the command line input we could quickly check a handful of other stations at different locations/elevation on the mountain, and at 15-min resolution over the last day, instead of 1 hr over the last week.

Conclusion

Overall, a handy program for monitoring of current and recent snow conditions. Time-consuming to customize all the parsing and plotting procedures, but in the end you have something robust that can be executed with a simple command every morning while sipping on coffee and planning a ski tour.

This program will serve as a base for future modification and improvement.