Skip to content

Commit aa1db9b

Browse files
committed
Improved handling of realtime parameters in get_series().
Arguments realtime_start and realtime_end in get_series() now cause a pandas.DataFrame to be returned with pandas.MultiIndex for realtime data. Added simple test for the new feature and documentation. Added __init__.py in fredapi.tests so it's correctly interpreted as a package. Now we could revert to python setup.py test in .travis.yml. Fixed test_invalid_kwarg_in_get_series() as we sometimes get a TypeError and sometimes a ValueError. Seems that pandas passes through whatever exception it gets, might be a good reason for this so we follow the same policy. Simplified comparison of dataframe output in tests.
1 parent fb61173 commit aa1db9b

File tree

3 files changed

+116
-18
lines changed

3 files changed

+116
-18
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ For instance, there has been three observations (data points) for the GDP of 201
5050

5151
This means the GDP value for Q1 2014 has been released three times. First release was on 4/30/2014 for a value of 17149.6, and then there have been two revisions on 5/29/2014 and 6/25/2014 for revised values of 17101.3 and 17016.0, respectively.
5252

53+
If you pass realtime_start and/or realtime_end to `get_series`, you will get a pandas.DataFrame with a pandas.MultiIndex instead of a pandas.Series.
54+
55+
For instance, with observation_start and observation_end set to 2015-01-01 and
56+
realtime_start set to 2015-01-01, one will get:
57+
```
58+
GDP
59+
obs_date rt_start rt_end
60+
2015-01-01 2015-04-29 2015-05-28 00:00:00 17710.0
61+
2015-05-29 2015-06-23 00:00:00 17665.0
62+
2015-06-24 9999-12-31 17693.3
63+
```
64+
5365
### Get first data release only (i.e. ignore revisions)
5466

5567
```python
@@ -83,6 +95,7 @@ this outputs:
8395
2014-04-01 17294.7
8496
dtype: float64
8597
```
98+
8699
### Get latest data known on a given date
87100

88101
```python

fredapi/fred.py

+41-10
Original file line numberDiff line numberDiff line change
@@ -98,25 +98,35 @@ def get_series_info(self, series_id):
9898
info = pd.Series(root.getchildren()[0].attrib)
9999
return info
100100

101-
def get_series(self, series_id, observation_start=None, observation_end=None, **kwargs):
101+
def get_series(self, series_id, observation_start=None,
102+
observation_end=None, realtime_start=None,
103+
realtime_end=None, **kwargs):
102104
"""
103105
Get data for a Fred series id. This fetches the latest known data, and is equivalent to get_series_latest_release()
104106
105107
Parameters
106108
----------
107109
series_id : str
108110
Fred series id such as 'CPIAUCSL'
109-
observation_start : datetime or datetime-like str such as '7/1/2014', optional
110-
earliest observation date
111-
observation_end : datetime or datetime-like str such as '7/1/2014', optional
112-
latest observation date
111+
112+
observation_start : datetime or datetime-like str such as '7/1/2014'
113+
earliest observation date (optional)
114+
observation_end : datetime or datetime-like str such as '7/1/2014'
115+
latest observation date (optional)
116+
realtime_start : datetime or datetime-like str such as '7/1/2014'
117+
earliest as-of date (optional)
118+
realtime_end : datetime or datetime-like str such as '7/1/2014'
119+
latest as-of date (optional)
113120
kwargs : additional parameters
114-
Any additional parameters supported by FRED. You can see https://api.stlouisfed.org/docs/fred/series_observations.html for the full list
121+
Any additional parameters supported by FRED. You can see
122+
https://api.stlouisfed.org/docs/fred/series_observations.html
123+
for the full list
115124
116125
Returns
117126
-------
118127
data : Series
119-
a Series where each index is the observation date and the value is the data for the Fred series
128+
a pandas Series where each index is the observation date and the
129+
value is the data for the Fred series
120130
"""
121131
url = "%s/series/observations?series_id=%s" % (self.root_url, series_id)
122132
if observation_start is not None:
@@ -126,20 +136,41 @@ def get_series(self, series_id, observation_start=None, observation_end=None, **
126136
if observation_end is not None:
127137
observation_end = pd.to_datetime(observation_end, errors='raise')
128138
url += '&observation_end=' + observation_end.strftime('%Y-%m-%d')
139+
if realtime_start is not None:
140+
realtime_start = pd.to_datetime(realtime_start, errors='raise')
141+
url += '&realtime_start=' + realtime_start.strftime('%Y-%m-%d')
142+
if realtime_end is not None:
143+
realtime_end = pd.to_datetime(realtime_end, errors='raise')
144+
url += '&realtime_end=' + realtime_end.strftime('%Y-%m-%d')
129145
if kwargs.keys():
130146
url += '&' + urlencode(kwargs)
131147
root = self.__fetch_data(url)
132148
if root is None:
133149
raise ValueError('No data exists for series id: ' + series_id)
134-
data = {}
150+
realtime = (realtime_start or realtime_end)
151+
values = []
152+
obsdates = []
153+
rtstarts = []
154+
rtends = []
135155
for child in root.getchildren():
136156
val = child.get('value')
137157
if val == self.nan_char:
138158
val = float('NaN')
139159
else:
140160
val = float(val)
141-
data[self._parse(child.get('date'))] = val
142-
return pd.Series(data)
161+
values.append(val)
162+
obsdates.append(self._parse(child.get('date')))
163+
if realtime:
164+
rtstarts.append(self._parse(child.get('realtime_start')))
165+
rtends.append(self._parse(child.get('realtime_end')))
166+
if realtime:
167+
names = ['obs_date', 'rt_start', 'rt_end']
168+
index = pd.MultiIndex.from_arrays([obsdates, rtstarts, rtends],
169+
names=names)
170+
return pd.DataFrame(values, index=index, columns=[series_id])
171+
else:
172+
return pd.Series(values, index=obsdates)
173+
143174

144175
def get_series_latest_release(self, series_id):
145176
"""

fredapi/tests/test_fred.py

+62-8
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
import textwrap
1717
import contextlib
1818

19-
import pandas as pd
20-
2119
import fredapi
2220
import fredapi.fred
2321

@@ -120,6 +118,39 @@ def __init__(self, rel_url, response=None, side_effect=None):
120118
last_updated="2015-06-05 08:47:20-05"
121119
popularity="86" notes="..." />
122120
</seriess>'''))
121+
gdp_obs_rt_call = HTTPCall('series/observations?{}&{}&{}&{}'.
122+
format('series_id=GDP',
123+
'observation_start=2014-07-01',
124+
'observation_end=2015-01-01',
125+
'realtime_start=2014-07-01'),
126+
response=textwrap.dedent('''\
127+
<?xml version="1.0" encoding="utf-8" ?>
128+
<observations realtime_start="2014-07-01" realtime_end="9999-12-31"
129+
observation_start="2014-07-01" observation_end="2015-01-01"
130+
units="lin" output_type="1" file_type="xml"
131+
order_by="observation_date" sort_order="asc" count="9"
132+
offset="0" limit="100000">
133+
<observation realtime_start="2014-10-30" realtime_end="2014-11-24"
134+
date="2014-07-01" value="17535.4"/>
135+
<observation realtime_start="2014-11-25" realtime_end="2014-12-22"
136+
date="2014-07-01" value="17555.2"/>
137+
<observation realtime_start="2014-12-23" realtime_end="9999-12-31"
138+
date="2014-07-01" value="17599.8"/>
139+
<observation realtime_start="2015-01-30" realtime_end="2015-02-26"
140+
date="2014-10-01" value="17710.7"/>
141+
<observation realtime_start="2015-02-27" realtime_end="2015-03-26"
142+
date="2014-10-01" value="17701.3"/>
143+
<observation realtime_start="2015-03-27" realtime_end="9999-12-31"
144+
date="2014-10-01" value="17703.7"/>
145+
<observation realtime_start="2015-04-29" realtime_end="2015-05-28"
146+
date="2015-01-01" value="17710.0"/>
147+
<observation realtime_start="2015-05-29" realtime_end="2015-06-23"
148+
date="2015-01-01" value="17665.0"/>
149+
<observation realtime_start="2015-06-24" realtime_end="9999-12-31"
150+
date="2015-01-01" value="17693.3"/>
151+
</observations>
152+
'''))
153+
123154

124155

125156
class TestFred(unittest.TestCase):
@@ -232,9 +263,9 @@ def test_invalid_kwarg_in_get_series(self, urlopen):
232263
fred_api_key)
233264
side_effect = fredapi.fred.HTTPError(url, 400, '', '', sys.stderr)
234265
self.prepare_urlopen(urlopen, side_effect=side_effect)
235-
with self.assertRaises(ValueError) as context:
236-
self.fred.get_series('SP500',
237-
observation_start='invalid-datetime-str')
266+
# FIXME: different environment throw ValueError or TypeError.
267+
with self.assertRaises(Exception):
268+
self.fred.get_series('SP500', observation_start='invalid')
238269
self.assertFalse(urlopen.called)
239270

240271
@mock.patch('fredapi.fred.urlopen')
@@ -249,12 +280,35 @@ def test_search(self, urlopen):
249280
'seasonal_adjustment_short']])
250281
expected = textwrap.dedent('''\
251282
popularity observation_start seasonal_adjustment_short
252-
series id
283+
series id
253284
PCPI01001 0 1969-01-01 NSA
254285
PCPI01003 0 1969-01-01 NSA
255286
PCPI01005 0 1969-01-01 NSA''')
256-
for aline, eline in zip(actual.split('\n'), expected.split('\n')):
257-
self.assertEqual(aline.strip(), eline.strip())
287+
self.assertEqual(actual.split('\n'), expected.split('\n'))
288+
289+
@mock.patch('fredapi.fred.urlopen')
290+
def test_get_series_with_realtime(self, urlopen):
291+
"""Test get_series with realtime argument."""
292+
side_effects = [gdp_obs_rt_call.response]
293+
self.prepare_urlopen(urlopen, side_effect=side_effects)
294+
df = self.fred.get_series('GDP', observation_start='7/1/2014',
295+
observation_end='1/1/2015',
296+
realtime_start='7/1/2014')
297+
urlopen.assert_called_with(gdp_obs_rt_call.url)
298+
actual = str(df)
299+
expected = textwrap.dedent('''\
300+
GDP
301+
obs_date rt_start rt_end
302+
2014-07-01 2014-10-30 2014-11-24 00:00:00 17535.4
303+
2014-11-25 2014-12-22 00:00:00 17555.2
304+
2014-12-23 9999-12-31 17599.8
305+
2014-10-01 2015-01-30 2015-02-26 00:00:00 17710.7
306+
2015-02-27 2015-03-26 00:00:00 17701.3
307+
2015-03-27 9999-12-31 17703.7
308+
2015-01-01 2015-04-29 2015-05-28 00:00:00 17710.0
309+
2015-05-29 2015-06-23 00:00:00 17665.0
310+
2015-06-24 9999-12-31 17693.3''')
311+
self.assertEqual(actual.split('\n'), expected.split('\n'))
258312

259313

260314
if __name__ == '__main__':

0 commit comments

Comments
 (0)