diff --git a/doc/source/whatsnew/v0.17.1.txt b/doc/source/whatsnew/v0.17.1.txt index 2762d84d73ba0..418d306a2de7f 100755 --- a/doc/source/whatsnew/v0.17.1.txt +++ b/doc/source/whatsnew/v0.17.1.txt @@ -30,6 +30,7 @@ Other Enhancements - ``pd.read_*`` functions can now also accept :class:`python:pathlib.Path`, or :class:`py:py._path.local.LocalPath` objects for the ``filepath_or_buffer`` argument. (:issue:`11033`) +- ``DataFrame`` now uses the fields of a ``namedtuple`` as columns, if columns are not supplied (:issue:`11181`) - Improve the error message displayed in :func:`pandas.io.gbq.to_gbq` when the DataFrame does not match the schema of the destination table (:issue:`11359`) .. _whatsnew_0171.api: diff --git a/pandas/core/common.py b/pandas/core/common.py index ac3e61a500bb6..d6aa6e6bb90cc 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -2676,6 +2676,9 @@ def is_list_like(arg): return (hasattr(arg, '__iter__') and not isinstance(arg, compat.string_and_binary_types)) +def is_named_tuple(arg): + return isinstance(arg, tuple) and hasattr(arg, '_fields') + def is_null_slice(obj): """ we have a null slice """ return (isinstance(obj, slice) and obj.start is None and diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 827373c9a330b..31b7aacefcb60 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -261,6 +261,8 @@ def __init__(self, data=None, index=None, columns=None, dtype=None, data = list(data) if len(data) > 0: if is_list_like(data[0]) and getattr(data[0], 'ndim', 1) == 1: + if com.is_named_tuple(data[0]) and columns is None: + columns = data[0]._fields arrays, columns = _to_arrays(data, columns, dtype=dtype) columns = _ensure_index(columns) diff --git a/pandas/tests/test_common.py b/pandas/tests/test_common.py index 003fd134cf210..89826209fa46d 100644 --- a/pandas/tests/test_common.py +++ b/pandas/tests/test_common.py @@ -538,6 +538,15 @@ def test_is_list_like(): for f in fails: assert not com.is_list_like(f) +def test_is_named_tuple(): + passes = (collections.namedtuple('Test',list('abc'))(1,2,3),) + fails = ((1,2,3), 'a', Series({'pi':3.14})) + + for p in passes: + assert com.is_named_tuple(p) + + for f in fails: + assert not com.is_named_tuple(f) def test_is_hashable(): diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py index dc0e0e2670565..5c7f1ec9e0037 100644 --- a/pandas/tests/test_frame.py +++ b/pandas/tests/test_frame.py @@ -16,8 +16,7 @@ from pandas.compat import( map, zip, range, long, lrange, lmap, lzip, - OrderedDict, u, StringIO, string_types, - is_platform_windows + OrderedDict, u, StringIO, is_platform_windows ) from pandas import compat @@ -33,8 +32,7 @@ import pandas.core.datetools as datetools from pandas import (DataFrame, Index, Series, Panel, notnull, isnull, MultiIndex, DatetimeIndex, Timestamp, date_range, - read_csv, timedelta_range, Timedelta, CategoricalIndex, - option_context, period_range) + read_csv, timedelta_range, Timedelta, option_context, period_range) from pandas.core.dtypes import DatetimeTZDtype import pandas as pd from pandas.parser import CParserError @@ -2239,7 +2237,6 @@ class TestDataFrame(tm.TestCase, CheckIndexing, _multiprocess_can_split_ = True def setUp(self): - import warnings self.frame = _frame.copy() self.frame2 = _frame2.copy() @@ -3568,6 +3565,20 @@ def test_constructor_tuples(self): expected = DataFrame({'A': Series([(1, 2), (3, 4)])}) assert_frame_equal(result, expected) + def test_constructor_namedtuples(self): + # GH11181 + from collections import namedtuple + named_tuple = namedtuple("Pandas", list('ab')) + tuples = [named_tuple(1, 3), named_tuple(2, 4)] + expected = DataFrame({'a': [1, 2], 'b': [3, 4]}) + result = DataFrame(tuples) + assert_frame_equal(result, expected) + + # with columns + expected = DataFrame({'y': [1, 2], 'z': [3, 4]}) + result = DataFrame(tuples, columns=['y', 'z']) + assert_frame_equal(result, expected) + def test_constructor_orient(self): data_dict = self.mixed_frame.T._series recons = DataFrame.from_dict(data_dict, orient='index') @@ -4418,7 +4429,7 @@ def test_timedeltas(self): def test_operators_timedelta64(self): - from datetime import datetime, timedelta + from datetime import timedelta df = DataFrame(dict(A = date_range('2012-1-1', periods=3, freq='D'), B = date_range('2012-1-2', periods=3, freq='D'), C = Timestamp('20120101')-timedelta(minutes=5,seconds=5))) @@ -9645,7 +9656,6 @@ def test_replace_mixed(self): assert_frame_equal(result,expected) # test case from - from pandas.util.testing import makeCustomDataframe as mkdf df = DataFrame({'A' : Series([3,0],dtype='int64'), 'B' : Series([0,3],dtype='int64') }) result = df.replace(3, df.mean().to_dict()) expected = df.copy().astype('float64') @@ -12227,7 +12237,6 @@ def test_sort_index_inplace(self): assert_frame_equal(df, expected) def test_sort_index_different_sortorder(self): - import random A = np.arange(20).repeat(5) B = np.tile(np.arange(5), 20) @@ -13301,7 +13310,6 @@ def test_quantile(self): def test_quantile_axis_parameter(self): # GH 9543/9544 - from numpy import percentile df = DataFrame({"A": [1, 2, 3], "B": [2, 3, 4]}, index=[1, 2, 3]) @@ -16093,8 +16101,6 @@ def test_query_doesnt_pickup_local(self): n = m = 10 df = DataFrame(np.random.randint(m, size=(n, 3)), columns=list('abc')) - from numpy import sin - # we don't pick up the local 'sin' with tm.assertRaises(UndefinedVariableError): df.query('sin > 5', engine=engine, parser=parser) @@ -16392,7 +16398,6 @@ def setUpClass(cls): cls.frame = _frame.copy() def test_query_builtin(self): - from pandas.computation.engines import NumExprClobberingError engine, parser = self.engine, self.parser n = m = 10 @@ -16413,7 +16418,6 @@ def setUpClass(cls): cls.frame = _frame.copy() def test_query_builtin(self): - from pandas.computation.engines import NumExprClobberingError engine, parser = self.engine, self.parser n = m = 10