Skip to content

Commit

Permalink
Issue110 - NWS Forecast (#112)
Browse files Browse the repository at this point in the history
* Start implementing NWS Forecast class. Still a work in progress

* Issue #110 - working out some hourly vs daily bugs

* start NWS tests

* pass daily and hourly data

* NWS does not implement the normal get_daily_data, but instead implements a get_daily_forecast that ONLY takes in variable list. This is motivation to have a more generic class as a base that does not assume we will implement the get_daily_data methods, etc.

* No need from from_geometry anymore

* Add some usage and README stuff

* flake8
  • Loading branch information
micah-prime authored Jul 3, 2024
1 parent 58dc522 commit a1e9585
Show file tree
Hide file tree
Showing 9 changed files with 574 additions and 44 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Features
* `SNOTEL <https://www.nrcs.usda.gov/wps/portal/wcc/home/dataAccessHelp/webService/webServiceReference/>`_
* `MESOWEST <https://developers.synopticdata.com/mesonet/>`_
* `USGS <https://waterservices.usgs.gov/rest/>`_
* `NWS FORECAST <https://api.weather.gov>`_
* `GEOSPHERE AUSTRIA <https://data.hub.geosphere.at/dataset/>`_
* `UCSB CUES <https://snow.ucsb.edu/#>`_
* `MET NORWAY <https://frost.met.no/index.html>`_
Expand Down
34 changes: 34 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,40 @@ use metloom to find 15-minute streamflow from USGS::
print(df)


National Weather Service (NWS) Forecast
---------------------------------------

The NWS forecast pulls the current forecast starting from today. When defining
a point, give it your own name and id, and make sure to provide the latitude
and longitude as a ``shapely point`` for the initial metadata.

Then you can use ``get_daily_forecast`` or ``get_hourly_forecast``
to retrive data.

**Note: the data will be aggregated to hourly or daily using mean or sum depending**
**on ``accumulated=True`` on the variable description**

Also - the point metadata is the **center of the NWS pixel** containing
your initial input point.

Example of pulling the daily forecast::

from metloom.pointdata import NWSForecastPointData
from metloom.variables import NWSForecastVariables
from shapely.geometry import Point

inintial_point = Point(-119, 43)
pt = NWSForecastPointData(
"my_point_id", "my_point_name", initial_metadata=inintial_point
)

df = pt.get_daily_forecast([
NWSForecastVariables.TEMP,
NWSForecastVariables.PRECIPITATIONACCUM,
]



Mesowest
--------
You can also use the Mesowest network if you sign up for an API token which is
Expand Down
2 changes: 2 additions & 0 deletions metloom/pointdata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .geosphere_austria import GeoSphereHistPointData, GeoSphereCurrentPointData
from .norway import MetNorwayPointData
from .cues import CuesLevel1
from .nws_forecast import NWSForecastPointData
from .files import CSVPointData, StationInfo
from .snowex import SnowExMet
from .csas import CSASMet
Expand All @@ -14,5 +15,6 @@
"PointData", "PointDataCollection", "CDECPointData", "SnotelPointData",
"MesowestPointData", "USGSPointData", "GeoSphereHistPointData",
"GeoSphereCurrentPointData", "CuesLevel1", "MetNorwayPointData",
"NWSForecastPointData",
"CSVPointData", "StationInfo", "SnowExMet", "CSASMet"
]
149 changes: 105 additions & 44 deletions metloom/pointdata/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,19 @@ def __iter__(self):
yield item


class PointData(object):
class GenericPoint(object):
"""
Class for storing metadata. and defining the expected data format
returned from `get_data` methods
"""
ALLOWED_VARIABLES = VariableBase
ITERATOR_CLASS = PointDataCollection
DATASOURCE = None
EXPECTED_COLUMNS = ["geometry", "datasource"]
EXPECTED_INDICES = ["datetime", "site"]
NON_VARIABLE_COLUMNS = EXPECTED_INDICES + EXPECTED_COLUMNS

# Default kwargs for function points from geometry
POINTS_FROM_GEOM_DEFAULTS = {
'within_geometry': True, 'snow_courses': False,
'buffer': 0.0, "filter_to_active": False
}

def __init__(self, station_id, name, metadata=None):
"""
Expand All @@ -94,6 +93,105 @@ def __init__(self, station_id, name, metadata=None):
self._metadata = metadata
self.desired_tzinfo = "UTC"

def _get_metadata(self):
"""
Method to get a shapely Point object to describe the station location
Returns:
shapely.point.Point object in Longitude, Latitude
"""
raise NotImplementedError("_get_metadata is not implemented")

def _handle_df_tz(self, val):
"""
Covert one entry from a df from cls.TZINFO to UTC
"""
if pd.isna(val):
return val
else:
local = val.tz_localize(self.tzinfo)
return local.tz_convert(self.desired_tzinfo)

@property
def tzinfo(self):
"""
tzinfo that pandas can use for tz_localize
"""
return self._tzinfo

@property
def metadata(self):
"""
metadata property
Returns:
shapely.point.Point object in Longitude, Latitude with z in ft
"""
if self._metadata is None:
self._metadata = self._get_metadata()
return self._metadata

@classmethod
def validate_sensor_df(cls, gdf: gpd.GeoDataFrame):
"""
Validate that the GeoDataFrame returned is formatted correctly.
The goal of this method is to ensure base classes are returning a
consistent format of dataframe
"""
if gdf is None:
return
assert isinstance(gdf, gpd.GeoDataFrame)
columns = gdf.columns
index_names = gdf.index.names
# check for required indexes
for ei in cls.EXPECTED_INDICES:
if ei not in index_names:
raise DataValidationError(
f"{ei} was expected, but not found as an"
f" index of the final dataframe"
)
# check for expected columns - avoid modifying at class level
expected_columns = copy.deepcopy(cls.EXPECTED_COLUMNS)
possible_extras = ["measurementDate", "quality_code"]
for pe in possible_extras:
if pe in columns:
expected_columns += [pe]
for column in expected_columns:
if column not in columns:
raise DataValidationError(
f"{column} was expected, but not found as a"
f" column of the final dataframe"
)

remaining_columns = [c for c in columns if c not in expected_columns]
# make sure all variables have a units column as well
for rc in remaining_columns:
if "_units" not in rc:
assert f"{rc}_units" in remaining_columns

def __repr__(self):
return f"{self.__class__.__name__}({self.id!r}, {self.name!r})"

def __str__(self):
return f"{self.name} ({self.id})"

def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self.id == other.id and self.name == other.name


class PointData(GenericPoint):
"""
Extend GenericPoint and add functions for finding data from geometry
and for gettings daily, hourly, or snow course data
"""

# Default kwargs for function points from geometry
POINTS_FROM_GEOM_DEFAULTS = {
'within_geometry': True, 'snow_courses': False,
'buffer': 0.0, "filter_to_active": False
}

def get_daily_data(
self,
start_date: datetime,
Expand Down Expand Up @@ -169,43 +267,6 @@ def get_snow_course_data(
"""
raise NotImplementedError("get_snow_course_data is not implemented")

def _get_metadata(self):
"""
Method to get a shapely Point object to describe the station location
Returns:
shapely.point.Point object in Longitude, Latitude
"""
raise NotImplementedError("_get_metadata is not implemented")

def _handle_df_tz(self, val):
"""
Covert one entry from a df from cls.TZINFO to UTC
"""
if pd.isna(val):
return val
else:
local = val.tz_localize(self.tzinfo)
return local.tz_convert(self.desired_tzinfo)

@property
def tzinfo(self):
"""
tzinfo that pandas can use for tz_localize
"""
return self._tzinfo

@property
def metadata(self):
"""
metadata property
Returns:
shapely.point.Point object in Longitude, Latitude with z in ft
"""
if self._metadata is None:
self._metadata = self._get_metadata()
return self._metadata

@classmethod
def _add_default_kwargs(cls, kwargs):
"""
Expand Down
Loading

0 comments on commit a1e9585

Please sign in to comment.