Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue110 - NWS Forecast #112

Merged
merged 9 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading