Repository: pandas-dev/pandas

Issue:
BUG: linspace behavior for date_range is incorrect when tz aware
#### Code Sample, a copy-pastable example if possible

xref #20846

```python
In [2]: dti = pd.date_range('20180101', '20180103', periods=3, tz='US/Eastern')

In [3]: dti
Out[3]: DatetimeIndex(['2017-12-31', '2018-01-01', '2018-01-02'], dtype='datetime64[ns, US/Eastern]', freq=None)

In [4]: dti[0]
Out[4]: Timestamp('2017-12-31 19:00:00-0500', tz='US/Eastern')
```
#### Problem description
The dates in the `date_range` are not being localized correctly; the dates are being shifted back by the UTC offset, then having the timezone appended to the shifted back date.

#### Expected Output
I'd expect this to be identical to the equivalent non-linspace behavior call:
```python
In [5]: dti2 = pd.date_range('20180101', periods=3, freq='D', tz='US/Eastern')

In [6]: dti2
Out[6]:
DatetimeIndex(['2018-01-01 00:00:00-05:00', '2018-01-02 00:00:00-05:00',
               '2018-01-03 00:00:00-05:00'],
              dtype='datetime64[ns, US/Eastern]', freq='D')

In [7]: dti2[0]
Out[7]: Timestamp('2018-01-01 00:00:00-0500', tz='US/Eastern', freq='D')
```

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: b3f07b2551b7eb40408ca28416ca5e21f8fb37b4
python: 3.6.1.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.23.0rc2+23.gb3f07b2
pytest: 3.1.2
pip: 9.0.1
setuptools: 39.0.1
Cython: 0.28.2
numpy: 1.13.3
scipy: 1.0.0
pyarrow: 0.6.0
xarray: 0.9.6
IPython: 6.1.0
sphinx: 1.5.6
patsy: 0.4.1
dateutil: 2.6.0
pytz: 2017.2
blosc: None
bottleneck: 1.2.1
tables: 3.4.2
numexpr: 2.6.4
feather: 0.4.0
matplotlib: 2.0.2
openpyxl: 2.4.8
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 0.9.8
lxml: 3.8.0
bs4: None
html5lib: 0.999
sqlalchemy: 1.1.13
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
fastparquet: 0.1.5
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:

Resolving patch:
diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py
--- a/pandas/core/indexes/datetimes.py
+++ b/pandas/core/indexes/datetimes.py
@@ -587,10 +587,13 @@ def _generate(cls, start, end, periods, name, freq,
                     if end is not None:
                         end = end.tz_localize(tz).asm8
             else:
+                # Create a linearly spaced date_range in local time
+                start = start.tz_localize(tz)
+                end = end.tz_localize(tz)
                 index = tools.to_datetime(np.linspace(start.value,
-                                                      end.value, periods))
-                if tz is not None:
-                    index = index.tz_localize('UTC').tz_convert(tz)
+                                                      end.value, periods),
+                                          utc=True)
+                index = index.tz_convert(tz)
 
         if not left_closed and len(index) and index[0] == start:
             index = index[1:]


Test patch:
diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py
--- a/pandas/tests/indexes/datetimes/test_date_range.py
+++ b/pandas/tests/indexes/datetimes/test_date_range.py
@@ -164,20 +164,39 @@ def test_date_range_ambiguous_arguments(self):
 
     def test_date_range_convenience_periods(self):
         # GH 20808
-        rng = date_range('2018-04-24', '2018-04-27', periods=3)
-        exp = DatetimeIndex(['2018-04-24 00:00:00', '2018-04-25 12:00:00',
-                             '2018-04-27 00:00:00'], freq=None)
+        result = date_range('2018-04-24', '2018-04-27', periods=3)
+        expected = DatetimeIndex(['2018-04-24 00:00:00',
+                                  '2018-04-25 12:00:00',
+                                  '2018-04-27 00:00:00'], freq=None)
 
-        tm.assert_index_equal(rng, exp)
+        tm.assert_index_equal(result, expected)
 
         # Test if spacing remains linear if tz changes to dst in range
-        rng = date_range('2018-04-01 01:00:00', '2018-04-01 04:00:00',
-                         tz='Australia/Sydney', periods=3)
-        exp = DatetimeIndex(['2018-04-01 01:00:00+11:00',
-                             '2018-04-01 02:00:00+11:00',
-                             '2018-04-01 02:00:00+10:00',
-                             '2018-04-01 03:00:00+10:00',
-                             '2018-04-01 04:00:00+10:00'], freq=None)
+        result = date_range('2018-04-01 01:00:00',
+                            '2018-04-01 04:00:00',
+                            tz='Australia/Sydney',
+                            periods=3)
+        expected = DatetimeIndex([Timestamp('2018-04-01 01:00:00+1100',
+                                            tz='Australia/Sydney'),
+                                  Timestamp('2018-04-01 02:00:00+1000',
+                                            tz='Australia/Sydney'),
+                                  Timestamp('2018-04-01 04:00:00+1000',
+                                            tz='Australia/Sydney')])
+        tm.assert_index_equal(result, expected)
+
+    @pytest.mark.parametrize('start,end,result_tz', [
+        ['20180101', '20180103', 'US/Eastern'],
+        [datetime(2018, 1, 1), datetime(2018, 1, 3), 'US/Eastern'],
+        [Timestamp('20180101'), Timestamp('20180103'), 'US/Eastern'],
+        [Timestamp('20180101', tz='US/Eastern'),
+         Timestamp('20180103', tz='US/Eastern'), 'US/Eastern'],
+        [Timestamp('20180101', tz='US/Eastern'),
+         Timestamp('20180103', tz='US/Eastern'), None]])
+    def test_date_range_linspacing_tz(self, start, end, result_tz):
+        # GH 20983
+        result = date_range(start, end, periods=3, tz=result_tz)
+        expected = date_range('20180101', periods=3, freq='D', tz='US/Eastern')
+        tm.assert_index_equal(result, expected)
 
     def test_date_range_businesshour(self):
         idx = DatetimeIndex(['2014-07-04 09:00', '2014-07-04 10:00',