@@ -4,55 +4,80 @@ Time and time zones
4
4
===================
5
5
6
6
Dealing with time and time zones can be a frustrating experience in any
7
- programming language and for any application. pvlib relies on :py:mod: `pandas `
8
- and
9
- pytz to handle time and time zones. Therefore, the vast majority of the
10
- information in this document applies to any time series analysis using
11
- pandas and is not specific to pvlib-python.
7
+ programming language and for any application. pvlib-python relies on
8
+ :py:mod: `pandas ` and `pytz <http://pythonhosted.org/pytz/ >`_ to handle
9
+ time and time zones. Therefore, the vast majority of the information in
10
+ this document applies to any time series analysis using pandas and is
11
+ not specific to pvlib-python.
12
+
13
+ General functionality
14
+ ---------------------
12
15
13
16
pvlib makes extensive use of pandas due to its excellent time series
14
17
functionality. Take the time to become familiar with pandas' `Time
15
18
Series / Date functionality page
16
19
<http://pandas.pydata.org/pandas-docs/version/0.18.0/timeseries.html> `_.
17
20
It is also worthwhile to become familiar with pure Python's
18
- :py:mod: `python:datetime ` module, although we typically recommend
19
- using the corresponding pandas functionality where it exists.
21
+ :py:mod: `python:datetime ` module, although we usually recommend
22
+ using the corresponding pandas functionality where possible.
23
+
24
+ First, we'll import the libraries that we'll use to explore the basic
25
+ time and time zone functionality in python and pvlib.
20
26
21
27
.. ipython :: python
22
28
23
29
import datetime
24
- import numpy as np
25
30
import pandas as pd
26
31
import pytz
27
32
28
33
You can obtain a list of all valid time zone strings with
29
- pytz.all_timezones. Here, we print only every 20th time zone.
34
+ ``pytz.all_timezones ``. It's a long list, so we only print every 20th
35
+ time zone.
30
36
31
37
.. ipython :: python
32
38
33
39
len (pytz.all_timezones)
34
40
pytz.all_timezones[::20 ]
35
41
36
- :py:class: `pandas.Timestamp `'s and :py:class: `pandas.DatetimeIndex `'s
37
- can be created in many ways. Here
38
- we focus on the time zone issues surrounding them; see the pandas
39
- documentation for more information.
42
+ :py:class: `pandas.Timestamp ` and :py:class: `pandas.DatetimeIndex `
43
+ can be created in many ways. Here we focus on the time zone issues
44
+ surrounding them; see the pandas documentation for more information.
40
45
41
46
First, create a time zone naive pandas.Timestamp.
42
47
43
48
.. ipython :: python
44
49
45
50
pd.Timestamp(' 2015-1-1 00:00' )
46
51
47
- You can specify the time zone using the tz keyword argument or
48
- the tz_localize method of Timestamp
49
- and DatetimeIndex objects.
52
+ You can specify the time zone using the ``tz `` keyword argument or the
53
+ ``tz_localize `` method of Timestamp and DatetimeIndex objects.
50
54
51
55
.. ipython :: python
52
56
53
57
pd.Timestamp(' 2015-1-1 00:00' , tz = ' America/Denver' )
54
58
pd.Timestamp(' 2015-1-1 00:00' ).tz_localize(' America/Denver' )
55
59
60
+ Localized ``Timestamps `` can be converted from one time zone to another.
61
+
62
+ .. ipython :: python
63
+
64
+ midnight_mst = pd.Timestamp(' 2015-1-1 00:00' , tz = ' MST' )
65
+ corresponding_utc = midnight_mst.tz_convert(' UTC' ) # returns a new Timestamp
66
+ corresponding_utc
67
+
68
+ It does not make sense to convert a time stamp that has not been
69
+ localized, and pandas will raise an exception if you try to do so.
70
+
71
+ .. ipython :: python
72
+ :okexcept:
73
+
74
+ midnight = pd.Timestamp(' 2015-1-1 00:00' )
75
+ midnight.tz_convert(' UTC' )
76
+
77
+ The difference between ``tz_localize `` and ``tz_convert `` is a common
78
+ source of confusion for new users. Just remember: localize first,
79
+ convert later.
80
+
56
81
Some time zones are aware of daylight savings time and some are not. For
57
82
example the winter time results are the same for US/Mountain and MST,
58
83
but the summer time results are not.
@@ -82,7 +107,6 @@ Here is the pandas time representation of the integer 1.
82
107
So if we specify times consistent with the specified time zone, pandas
83
108
will use the same integer to represent them.
84
109
85
-
86
110
.. ipython :: python
87
111
88
112
# US/Mountain
@@ -98,8 +122,8 @@ will use the same integer to represent them.
98
122
pd.Timestamp(' 2015-6-1 07:00' ).value
99
123
100
124
As stated above, pandas will assume UTC if you do not specify a time
101
- zone. This is dangerous, and we always recommend using using localized
102
- timeseries, even if it is UTC.
125
+ zone. This is dangerous, and we recommend using localized timeseries,
126
+ even if it is UTC.
103
127
104
128
Timezones can also be specified with a fixed offset in minutes from UTC.
105
129
@@ -122,16 +146,169 @@ the string formulation.
122
146
123
147
pd.Timestamp(' 2015-1-1 00:00+0200' )
124
148
125
- pandas time objects can also be created from time zone aware or naive
126
- datetime.date or datetime.datetime objects. The behavior is generally as
127
- expected.
149
+ pandas Timestamp objects can also be created from time zone aware or
150
+ naive :py:class: `python:datetime.datetime ` objects.
151
+ The behavior is as expected.
152
+
153
+ .. ipython :: python
154
+
155
+ # tz naive python datetime.datetime object
156
+ naive_python_dt = datetime.datetime(2015 , 6 , 1 , 0 )
157
+
158
+ # tz naive pandas Timestamp object
159
+ pd.Timestamp(naive_python_dt)
160
+
161
+ # tz aware python datetime.datetime object
162
+ aware_python_dt = pytz.timezone(' US/Mountain' ).localize(naive_python_dt)
163
+
164
+ # tz aware pandas Timestamp object
165
+ pd.Timestamp(aware_python_dt)
166
+
167
+ One thing to watch out for is that python
168
+ :py:class: `python:datetime.date ` objects gain time information when
169
+ passed to ``Timestamp ``.
170
+
171
+ .. ipython :: python
172
+
173
+ # tz naive python datetime.date object
174
+ naive_python_date = datetime.date(2015 , 6 , 1 )
175
+
176
+ # tz naive pandas Timestamp object
177
+ pd.Timestamp(naive_python_date)
178
+
179
+ You cannot localize a pure Python date object.
180
+
181
+ .. ipython :: python
182
+ :okexcept:
183
+
184
+ # fail
185
+ pytz.timezone(' US/Mountain' ).localize(naive_python_date)
186
+
187
+
188
+ pvlib-specific functionality
189
+ ----------------------------
190
+
191
+ .. note ::
192
+
193
+ This section applies to pvlib >= 0.3. Version 0.2 of pvlib used a
194
+ ``Location `` object's ``tz `` attribute to auto-magically correct for
195
+ some time zone issues. This behavior was counter-intuitive to many
196
+ users and was removed in version 0.3.
197
+
198
+ How does this general functionality interact with pvlib? Perhaps the two
199
+ most common places to get tripped up with time and time zone issues in
200
+ solar power analysis occur during data import and solar position
201
+ calculations.
202
+
203
+ Let's first examine how pvlib handles time when it imports a tmy3 file.
128
204
129
205
.. ipython :: python
130
206
131
- # tz naive
132
- pd.Timestamp(datetime.datetime(2015 ,6 ,1 ,0 ))
207
+ import os
208
+ import inspect
209
+ import pvlib
210
+
211
+ # some gymnastics to find the example file
212
+ pvlib_abspath = os.path.dirname(os.path.abspath(inspect.getfile(pvlib)))
213
+ file_abspath = os.path.join(pvlib_abspath, ' data' , ' 703165TY.csv' )
214
+ tmy3_data, tmy3_metadata = pvlib.tmy.readtmy3(file_abspath)
215
+
216
+ tmy3_metadata
217
+
218
+ The metadata has a ``'TZ' `` key with a value of ``-9.0 ``. This is the
219
+ UTC offset in hours in which the data has been recorded. The
220
+ :py:func: `~pvlib.tmy.readtmy3 ` function read the data in the file,
221
+ created a :py:class: `~pandas.DataFrame ` with that data, and then
222
+ localized the DataFrame's index to have this fixed offset. Here, we
223
+ print just a few of the rows and columns of the large dataframe.
224
+
225
+ .. ipython :: python
226
+
227
+ tmy3_data.index.tz
228
+
229
+ tmy3_data.ix[0 :3 , [' GHI' , ' DNI' , ' AOD' ]]
230
+
231
+ The :py:func: `~pvlib.tmy.readtmy2 ` function also returns a DataFrame
232
+ with a localized DatetimeIndex.
233
+
234
+ The correct solar position can be immediately calculated from the
235
+ DataFrame's index since the index has been localized.
236
+
237
+ .. ipython :: python
238
+ :suppress:
239
+
240
+ import seaborn as sns
241
+ sns.set_color_codes()
242
+
243
+ .. ipython :: python
244
+
245
+ solar_position = pvlib.solarposition.get_solarposition(tmy3_data.index,
246
+ tmy3_metadata[' latitude' ],
247
+ tmy3_metadata[' longitude' ])
248
+
249
+ ax = solar_position.ix[0 :24 , [' apparent_zenith' , ' apparent_elevation' , ' azimuth' ]].plot()
250
+
251
+ ax.legend(loc = 1 );
252
+ ax.axhline(0 , color = ' darkgray' ); # add 0 deg line for sunrise/sunset
253
+ ax.axhline(180 , color = ' darkgray' ); # add 180 deg line for azimuth at solar noon
254
+ ax.set_ylim(- 60 , 200 ); # zoom in, but cuts off full azimuth range
255
+ ax.set_xlabel(' Local time ({} )' .format(solar_position.index.tz));
256
+ @savefig solar -position.png width=6in
257
+ ax.set_ylabel(' (degrees)' );
258
+
259
+ `According to the US Navy
260
+ <http://aa.usno.navy.mil/rstt/onedaytable?ID=AA&year=1997&month=1&day=1&state=AK&place=sand+point> `_,
261
+ on January 1, 1997 at Sand Point, Alaska, sunrise was at 10:09 am, solar
262
+ noon was at 1:46 pm, and sunset was at 5:23 pm. This is consistent with
263
+ the data plotted above (and depressing).
264
+
265
+ What if we had a DatetimeIndex that was not localized, such as the one
266
+ below? The solar position calculator will assume UTC time.
267
+
268
+ .. ipython :: python
269
+
270
+ index = pd.DatetimeIndex(start = ' 1997-01-01 01:00' , freq = ' 1h' , periods = 24 )
271
+ index
272
+
273
+ solar_position_notz = pvlib.solarposition.get_solarposition(index,
274
+ tmy3_metadata[' latitude' ],
275
+ tmy3_metadata[' longitude' ])
276
+
277
+ ax = solar_position_notz.ix[0 :24 , [' apparent_zenith' , ' apparent_elevation' , ' azimuth' ]].plot()
278
+
279
+ ax.legend(loc = 1 );
280
+ ax.axhline(0 , color = ' darkgray' ); # add 0 deg line for sunrise/sunset
281
+ ax.axhline(180 , color = ' darkgray' ); # add 180 deg line for azimuth at solar noon
282
+ ax.set_ylim(- 60 , 200 ); # zoom in, but cuts off full azimuth range
283
+ ax.set_xlabel(' Time (UTC)' );
284
+ @savefig solar -position-nolocal.png width=6in
285
+ ax.set_ylabel(' (degrees)' );
286
+
287
+ This looks like the plot above, but shifted by 9 hours.
288
+
289
+ In principle, one could localize the tz-naive solar position data to
290
+ UTC, and then convert it to the desired time zone.
291
+
292
+ .. ipython :: python
293
+
294
+ fixed_tz = pytz.FixedOffset(tmy3_metadata[' TZ' ] * 60 )
295
+ solar_position_hack = solar_position_notz.tz_localize(' UTC' ).tz_convert(fixed_tz)
296
+
297
+ solar_position_hack.index
298
+
299
+ ax = solar_position_hack.ix[0 :24 , [' apparent_zenith' , ' apparent_elevation' , ' azimuth' ]].plot()
300
+
301
+ ax.legend(loc = 1 );
302
+ ax.axhline(0 , color = ' darkgray' ); # add 0 deg line for sunrise/sunset
303
+ ax.axhline(180 , color = ' darkgray' ); # add 180 deg line for azimuth at solar noon
304
+ ax.set_ylim(- 60 , 200 ); # zoom in, but cuts off full azimuth range
305
+ ax.set_xlabel(' Local time ({} )' .format(solar_position_hack.index.tz));
306
+ @savefig solar -position-hack.png width=6in
307
+ ax.set_ylabel(' (degrees)' );
133
308
134
- # start is tz aware python datetime object
135
- start = pytz.timezone(' US/Mountain' ).localize(datetime.datetime(2015 , 6 , 1 , 0 ))
136
- pd.Timestamp(start)
309
+ Note that the time has been correctly localized and converted, however,
310
+ the calculation bounds still correspond to the original assumed-UTC range.
137
311
312
+ For this and other reasons, we recommend that users supply time zone
313
+ information at the beginning of a calculation rather than localizing and
314
+ converting the results at the end of a calculation.
0 commit comments