Repository: holoviz/panel

Issue:
DataFrame widget raises exception if two columns have the same names
Panel: 0.7

#### Description of expected behavior and the observed behavior

Normally I don't have the same column names for my dataframes. But I was trying to reproduce this dashboard https://getbootstrap.com/docs/4.3/examples/dashboard/ and they have the same column names in multiple columns.

I can see that the panel.widgets.DataFrame does not support this. I would have expected it to support this situation.

#### Complete, minimal, self-contained example code that reproduces the issue

```python
import panel as pn
import pandas as pd

table_data = pd.DataFrame([(1001, "Lorem"),], columns=["Header", "Header"])
pn.widgets.DataFrame(table_data).servable()
```
#### Stack traceback and/or browser JavaScript console output

```bash
$ python -m panel serve 'scripts\issue2.py' --dev
2019-11-29 09:00:56,834 Starting Bokeh server version 1.4.0 (running on Tornado 6.0.3)
2019-11-29 09:00:56,836 User authentication hooks NOT provided (default user enabled)
2019-11-29 09:00:56,840 Bokeh app running at: http://localhost:5006/issue2
2019-11-29 09:00:56,840 Starting Bokeh server with process id: 10992
2019-11-29 09:00:58,603 Error running application handler <bokeh.application.handlers.script.ScriptHandler object at 0x00000028FF59DD48>: 'DataFrame' object has no attribute 'dtype'
File "generic.py", line 5179, in __getattr__:
return object.__getattribute__(self, name) Traceback (most recent call last):
  File "C:\repos\private\awesome-panel\.venv\lib\site-packages\bokeh\application\handlers\code_runner.py", line 179, in run
    exec(self._code, module.__dict__)
  File "C:\repos\private\awesome-panel\scripts\issue2.py", line 5, in <module>
    pn.widgets.DataFrame(table_data).servable()
  File "C:\repos\private\awesome-panel\.venv\lib\site-packages\panel\viewable.py", line 525, in servable
    self.server_doc(title=title)
  File "C:\repos\private\awesome-panel\.venv\lib\site-packages\panel\viewable.py", line 498, in server_doc
    model = self.get_root(doc)
  File "C:\repos\private\awesome-panel\.venv\lib\site-packages\panel\viewable.py", line 434, in get_root
    root = self._get_model(doc, comm=comm)
  File "C:\repos\private\awesome-panel\.venv\lib\site-packages\panel\widgets\tables.py", line 112, in _get_model
    model = DataTable(**self._get_properties())
  File "C:\repos\private\awesome-panel\.venv\lib\site-packages\panel\widgets\tables.py", line 99, in _get_properties
    props['columns'] = self._get_columns()
  File "C:\repos\private\awesome-panel\.venv\lib\site-packages\panel\widgets\tables.py", line 61, in _get_columns
    kind = data.dtype.kind
  File "C:\repos\private\awesome-panel\.venv\lib\site-packages\pandas\core\generic.py", line 5179, in __getattr__
    return object.__getattribute__(self, name)
AttributeError: 'DataFrame' object has no attribute 'dtype'

2019-11-29 09:00:58,990 WebSocket connection opened
2019-11-29 09:00:58,991 ServerConnection created
```


Discussion:
Just had a look at this one. The widget is based on the bokeh `DataTable` widget and its source of data is a `ColumnDataSource` object which seems to reduce dataframes that have multiple columns with the same name to the last one:
```python
from bokeh.models import ColumnDataSource
import pandas as pd
table_data = pd.DataFrame([(1001, "Lorem"),], columns=["Header", "Header"])
cds = ColumnDataSource(data=table_data)
print(cds.data)
```

```
{'index': array([0], dtype=int64), 'Header': array(['Lorem'], dtype=object)}
``` 

As having a dataframe/table with non unique column names doesn't seem to be a good idea anyway I would just add a better error message indicating that this case isn't supported. Note that it'd be better than what bokeh seems to do since it's just deleting the first columns.
Another idea would be to create internally a copy of the original dataframe and whose columns which share the same name would be renamed by appending an index such as ``pd.DataFrame([(1001, "Lorem"),], columns=["Header_1", "Header_2"])``. This would also require to handle *editors* and *formatters* so that ``editors={"Header": editor}`` would work as expected. I don't know however how this renamed dataframe would be kept in sync with the original dataframe so that ``dataframe_widget.value`` would return the original dataframe but with updated values if the user changes a cell.

``pane.DataFrame`` can actually display properly a dataframe with identical column names:
![image](https://user-images.githubusercontent.com/35924738/80795934-3ba53d80-8b9e-11ea-9437-65c8f478040e.png)

So if you don't need what the widget offers (editing and sorting) that's the best solution IMO.
@MarcSkovMadsen I've seen that you opened a few issues with regards to this widget, any suggestion?
My vote would go for better error message. Thanks so much for looking into this.
+1 on improving the error message.

Resolving patch:
diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py
--- a/panel/widgets/tables.py
+++ b/panel/widgets/tables.py
@@ -47,8 +47,18 @@ class DataFrame(Widget):
 
     def __init__(self, value=None, **params):
         super(DataFrame, self).__init__(value=value, **params)
+        self.param.watch(self._validate, 'value')
+        self._validate(None)
         self._renamed_cols = {}
 
+    def _validate(self, event):
+        if self.value is None:
+            return
+        cols = self.value.columns
+        if len(cols) != len(cols.drop_duplicates()):
+            raise ValueError('Cannot display a pandas.DataFrame with '
+                             'duplicate column names.')
+
     def _get_columns(self):
         if self.value is None:
             return []
@@ -122,6 +132,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
         return model
 
     def _manual_update(self, events, model, doc, root, parent, comm):
+        self._validate(None)
         for event in events:
             if event.name == 'value':
                 cds = model.source


Test patch:
diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py
--- a/panel/tests/widgets/test_tables.py
+++ b/panel/tests/widgets/test_tables.py
@@ -115,3 +115,20 @@ def test_dataframe_process_data_event(dataframe):
     table._process_events({'data': {'int': {1: 3, 2: 4, 0: 1}}})
     df['int'] = [1, 3, 4]
     pd.testing.assert_frame_equal(table.value, df)
+
+
+def test_dataframe_duplicate_column_name(document, comm):
+    df = pd.DataFrame([[1, 1], [2, 2]], columns=['col', 'col'])
+    with pytest.raises(ValueError):
+        table = DataFrame(df)
+
+    df = pd.DataFrame([[1, 1], [2, 2]], columns=['a', 'b'])
+    table = DataFrame(df)
+    with pytest.raises(ValueError):
+        table.value = table.value.rename(columns={'a': 'b'})
+    
+    df = pd.DataFrame([[1, 1], [2, 2]], columns=['a', 'b'])
+    table = DataFrame(df)
+    table.get_root(document, comm)
+    with pytest.raises(ValueError):
+        table.value = table.value.rename(columns={'a': 'b'})
\ No newline at end of file