21

I have a problem with the line below self.tableView.set??????????(df) that supposed to display the data frame in PyQt5. I put ??? there where I am missing the code I need.

def btn_clk(self):
        path = self.lineEdit.text()
        df = pd.read_csv(path)
        self.tableView.set??????????(df)

The rest of the code works, because if I use print(df) in the above code, the data frame is printed in the IPython console. So, Pandas reads the CSV and prints it.

But, I tried many things to get it displayed in PyQt5 and nothing works. I am not very familiar with PyQt, just started to play around with it and I am stuck here.

Here is my code:

from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(662, 512)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
        self.lineEdit.setObjectName("lineEdit")
        self.verticalLayout.addWidget(self.lineEdit)
        self.tableView = QtWidgets.QTableView(self.centralwidget)
        self.tableView.setObjectName("tableView")
        self.verticalLayout.addWidget(self.tableView)
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setObjectName("pushButton")
        self.verticalLayout.addWidget(self.pushButton)
        self.horizontalLayout.addLayout(self.verticalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 662, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton.setText(_translate("MainWindow", "PushButton"))


        self.pushButton.clicked.connect(self.btn_clk)

        MainWindow.show()

    def btn_clk(self):
        path = self.lineEdit.text()
        df = pd.read_csv(path)
        self.tableView.set????????????(df)


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Joe T. Boka
  • 6,554
  • 6
  • 29
  • 48

3 Answers3

79

In the case of QTableView the data must be provided through a model since it implements the MVC (Model-View-Controller) paradigm, in the case of pandas there is no default model but we can create a custom as shown in the following part:

class PandasModel(QtCore.QAbstractTableModel): 
    def __init__(self, df = pd.DataFrame(), parent=None): 
        QtCore.QAbstractTableModel.__init__(self, parent=parent)
        self._df = df.copy()

    def toDataFrame(self):
        return self._df.copy()

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.DisplayRole:
            return QtCore.QVariant()

        if orientation == QtCore.Qt.Horizontal:
            try:
                return self._df.columns.tolist()[section]
            except (IndexError, ):
                return QtCore.QVariant()
        elif orientation == QtCore.Qt.Vertical:
            try:
                # return self.df.index.tolist()
                return self._df.index.tolist()[section]
            except (IndexError, ):
                return QtCore.QVariant()

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.DisplayRole:
            return QtCore.QVariant()

        if not index.isValid():
            return QtCore.QVariant()

        return QtCore.QVariant(str(self._df.ix[index.row(), index.column()]))

    def setData(self, index, value, role):
        row = self._df.index[index.row()]
        col = self._df.columns[index.column()]
        if hasattr(value, 'toPyObject'):
            # PyQt4 gets a QVariant
            value = value.toPyObject()
        else:
            # PySide gets an unicode
            dtype = self._df[col].dtype
            if dtype != object:
                value = None if value == '' else dtype.type(value)
        self._df.set_value(row, col, value)
        return True

    def rowCount(self, parent=QtCore.QModelIndex()): 
        return len(self._df.index)

    def columnCount(self, parent=QtCore.QModelIndex()): 
        return len(self._df.columns)

    def sort(self, column, order):
        colname = self._df.columns.tolist()[column]
        self.layoutAboutToBeChanged.emit()
        self._df.sort_values(colname, ascending= order == QtCore.Qt.AscendingOrder, inplace=True)
        self._df.reset_index(inplace=True, drop=True)
        self.layoutChanged.emit()

And then use it:

def btn_clk(self):
    path = self.lineEdit.text()
    df = pd.read_csv(path)
    model = PandasModel(df)
    self.tableView.setModel(model)

enter image description here

The complete code is here

Update 03-07-2019:

Some Pandas methods are deprecated so I have implemented a new version (which can also be used in QML as this answer shows):

class DataFrameModel(QtCore.QAbstractTableModel):
    DtypeRole = QtCore.Qt.UserRole + 1000
    ValueRole = QtCore.Qt.UserRole + 1001

    def __init__(self, df=pd.DataFrame(), parent=None):
        super(DataFrameModel, self).__init__(parent)
        self._dataframe = df

    def setDataFrame(self, dataframe):
        self.beginResetModel()
        self._dataframe = dataframe.copy()
        self.endResetModel()

    def dataFrame(self):
        return self._dataframe

    dataFrame = QtCore.pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)

    @QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str)
    def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self._dataframe.columns[section]
            else:
                return str(self._dataframe.index[section])
        return QtCore.QVariant()

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return len(self._dataframe.index)

    def columnCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return self._dataframe.columns.size

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid() or not (0 <= index.row() < self.rowCount() \
            and 0 <= index.column() < self.columnCount()):
            return QtCore.QVariant()
        row = self._dataframe.index[index.row()]
        col = self._dataframe.columns[index.column()]
        dt = self._dataframe[col].dtype

        val = self._dataframe.iloc[row][col]
        if role == QtCore.Qt.DisplayRole:
            return str(val)
        elif role == DataFrameModel.ValueRole:
            return val
        if role == DataFrameModel.DtypeRole:
            return dt
        return QtCore.QVariant()

    def roleNames(self):
        roles = {
            QtCore.Qt.DisplayRole: b'display',
            DataFrameModel.DtypeRole: b'dtype',
            DataFrameModel.ValueRole: b'value'
        }
        return roles
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks so much for doing this. Am I supposed to import `PandasModel` to my original code with `from PandasModel import PandasModel`? I am trying to put it all together but it's not working yet. When I try to import `PandasModel` I get the `no module named PandasModel` error. I am sure I am missing something. Can you please point me in the right direction? – Joe T. Boka Jun 17 '17 at 14:36
  • Update my answer by adding the code of how you should use it – eyllanesc Jun 17 '17 at 14:41
  • Wow! This is really awesome! Thanks a lot for this. It's a big help. It works perfectly. – Joe T. Boka Jun 17 '17 at 14:47
  • 1
    great answer, however PySide2 doesn't use QVariant, any recommendations for how to alter the code to work with PySide2? – BCR Nov 28 '18 at 21:32
  • @eyllanesc I have 10 Different data frame. So is it possible to add data in different row in PyQt Tablewidget? – Viral Parmar Apr 03 '19 at 17:59
  • @ViralParmar Why do not you join the Dataframes? – eyllanesc Apr 03 '19 at 18:01
  • @eyllanesc like appending the data frame? Isn't it be possible that I read one CSV to data frame and display it on Table Widget, then I will read the second CSV and then I'll put it into another row in table widget? – Viral Parmar Apr 03 '19 at 18:05
  • @ViralParmar If possible, but my solution only shows a dataframe, so my solution will not work. – eyllanesc Apr 03 '19 at 18:10
  • @eyllanesc Do you have any regarding my query? Like where to start with? – Viral Parmar Apr 03 '19 at 18:19
  • @ViralParmar I have seen that you have posted a question, I am implementing a solution – eyllanesc Apr 03 '19 at 18:19
  • This sorting function has some issues in it - doesn't support ascending and descending and doesn't sort 'naturally', what would be the solution there? – NL23codes Oct 03 '19 at 15:55
  • self._df.ix[index.row(), index.column()] givem me "no attribute ix" error. I think you should use "iloc" instead of "ix" – Mustafa Uçar Apr 05 '20 at 12:07
  • 1
    The class above didn't work for me. It fails to load smoothly couple tousands of rows. I've found another example of such PandasModel, it works like charm with more than 1mln rows: https://learndataanalysis.org/display-pandas-dataframe-with-pyqt5-qtableview-widget/ And on youtube channel: https://www.youtube.com/watch?v=hJEQEECZSH0&t=556s – Daniel R Apr 22 '20 at 14:33
  • Amazing. Tx a lot, very useful – Je Je Apr 17 '22 at 21:45
2

Like @DanielR says, the marked answer isn't working any more, but this tutorial seems to be.

pandas.version='1.0.1', PYQT_VERSION_STR = 5.11.3


class pandasModel(QAbstractTableModel):

    def __init__(self, data):
        QAbstractTableModel.__init__(self)
        self._data = data

    def rowCount(self, parent=None):
        return self._data.shape[0]

    def columnCount(self, parnet=None):
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole:
                return str(self._data.iloc[index.row(), index.column()])
        return None

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._data.columns[col]
        return None
cefect
  • 88
  • 6
1

To fix the sorting...

from natsort import natsorted, index_natsorted, order_by_index
def sort(self, column, order):
    if order == 0:
        self._dataframe = self._dataframe.reindex(index=order_by_index(self._dataframe.index, index_natsorted(self._dataframe[column])))
    else:
        self._dataframe = self._dataframe.reindex(index=order_by_index(self._dataframe.index, reversed(index_natsorted(self._dataframe[column]))))

    self._dataframe.reset_index(inplace=True, drop=True)
    self.setDataFrame(self._dataframe)
    
NL23codes
  • 1,181
  • 1
  • 14
  • 31