Repository: pandas-dev/pandas

Issue:
BUG: DataFrame constructor ignores copy=True argument if dtype is set
I just noticed that `DataFrame` constructor ignores the `copy=True` argument if `dtype` is set. In the code snippet below, the `orig` dataframe should stay unmodified after any modification of `new1` and `new2`. Instead, the columns of `new2` (or at least the first one as shown in the snippet) are references to the same data, as highlighted by the modification shown on statement 13 and onwards.

``` python
In [1]: import numpy as np

In [2]: import pandas as pd

In [3]: pd.show_versions()

INSTALLED VERSIONS
------------------
commit: None
python: 2.7.8.final.0
python-bits: 64
OS: Linux
OS-release: 3.16.0-25-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8

pandas: 0.15.2
nose: 1.3.4
Cython: 0.21.1
numpy: 1.9.1
scipy: 0.14.0
statsmodels: 0.4.3
IPython: 2.3.1
sphinx: 1.1.2
patsy: 0.3.0
dateutil: 2.3
pytz: 2014.10
bottleneck: 0.6.0
tables: None
numexpr: 2.4
matplotlib: 1.3.1
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: 0.999
httplib2: 0.7.4
apiclient: None
rpy2: None
sqlalchemy: 0.9.7
pymysql: None
psycopg2: None

In [4]: orig_data = {
   ...:     'col1': [1.],
   ...:     'col2': [2.],
   ...:     'col3': [3.],}

In [5]: orig = pd.DataFrame(orig_data)

In [6]: new1 = pd.DataFrame(orig, copy=True)

In [7]: new2 = pd.DataFrame(orig, dtype=float, copy=True)

In [8]: new1
Out[8]: 
   col1  col2  col3
0     1     2     3

In [9]: new2
Out[9]: 
   col1  col2  col3
0     1     2     3

In [10]: new1['col1'] = 100.

In [11]: new1
Out[11]: 
   col1  col2  col3
0   100     2     3

In [12]: orig
Out[12]: 
   col1  col2  col3
0     1     2     3

In [13]: new2['col1'] = 200.

In [14]: new2
Out[14]: 
   col1  col2  col3
0   200     2     3

In [15]: orig
Out[15]: 
   col1  col2  col3
0   200     2     3

In [16]:
```



Discussion:
fix should happen here: https://github.com/pydata/pandas/blob/master/pandas/core/generic.py#L127
(the frames are constructed from a frame itself, so it goes thru the manager construction).

welcome a pull-request on this, should be pretty straightforward.


Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -121,11 +121,11 @@ def _init_mgr(self, mgr, axes=None, dtype=None, copy=False):
                 mgr = mgr.reindex_axis(
                     axe, axis=self._get_block_manager_axis(a), copy=False)
 
-        # do not copy BlockManager unless explicitly done
-        if copy and dtype is None:
+        # make a copy if explicitly requested
+        if copy:
             mgr = mgr.copy()
-        elif dtype is not None:
-            # avoid copy if we can
+        if dtype is not None:
+            # avoid further copies if we can
             if len(mgr.blocks) > 1 or mgr.blocks[0].values.dtype != dtype:
                 mgr = mgr.astype(dtype=dtype)
         return mgr


Test patch:
diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py
--- a/pandas/tests/test_frame.py
+++ b/pandas/tests/test_frame.py
@@ -2489,6 +2489,17 @@ def test_constructor_cast_failure(self):
         # this is ok
         df['foo2'] = np.ones((4,2)).tolist()
 
+    def test_constructor_dtype_copy(self):
+        orig_df = DataFrame({
+            'col1': [1.],
+            'col2': [2.],
+            'col3': [3.]})
+
+        new_df = pd.DataFrame(orig_df, dtype=float, copy=True)
+
+        new_df['col1'] = 200.
+        self.assertEqual(orig_df['col1'][0], 1.)
+
     def test_constructor_dtype_nocast_view(self):
         df = DataFrame([[1, 2]])
         should_be_view = DataFrame(df, dtype=df[0].dtype)