The idea is to display a dataframe via the PyQt5 MV programming idiom and perform some basic sorting and filtering operations on the presented dataframe.
The displaying part all went fine however now I am stuck on the sorting part of the tool. Print statement showed me the dataframe it self was sorted, it is the view that is not updated. So now to the code:
import sys
import operator
tmp = [('23-02-1978', '19:03:13', 'eh', None, 'even more some data'),
('23-02-1978', '19:01:45', 'ss', 'some data ', 'even more some data'),
('23-02-1978', '19:02:55', 'he', 'some data ', 'even more some data')]
tmp1 = [('23-02-1978', '19:02:33', 'eh', 'some data ', '666', 'even more some data'),
('23-02-1978', '19:03:22', 'ss', 'some data ', '777', 'even more some data'),
('23-02-1978', '19:01:45', 'he', 'some data ', '888', 'even more some data')]
from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QAction,
QGroupBox, QCheckBox, QTableView, QTableWidgetItem,
QTabWidget, QGridLayout,QLineEdit, QFormLayout,
QVBoxLayout, QHBoxLayout, QLabel, QDialog, QHeaderView)
from PyQt5.QtGui import QIcon, QFont
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QAbstractTableModel, QVariant, QModelIndex, QSortFilterProxyModel
from pandas import DataFrame
class DataFrameModel(QAbstractTableModel):
def __init__(self):
""" datain: a list of lists
headerdata: a list of strings
"""
super(DataFrameModel, self).__init__()
self._df = DataFrame()
def setDataFrame(self, df):
self._df = df;
def signalUpdate(self):
''' tell viewers to update their data (this is full update, not
efficient)'''
self.layoutChanged.emit()
#------------- table display functions -----------------
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
if orientation == Qt.Horizontal:
try:
return self._df.columns.tolist()[section]
except (IndexError, ):
return QVariant()
elif orientation == Qt.Vertical:
try:
# return self.df.index.tolist()
return self._df.index.tolist()[section]
except (IndexError, ):
return QVariant()
def data(self, index, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
if not index.isValid():
return QVariant()
return QVariant(str(self._df.ix[index.row(), index.column()]))
def flags(self, index):
flags = super(DataFrameModel, self).flags(index)
return flags
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=QModelIndex()):
return len(self._df.index)
def columnCount(self, parent=QModelIndex()):
return len(self._df.columns)
def sort(self, column, order=Qt.AscendingOrder):
"""Sort table by given column number.
"""
print('sort clicked col {} order {}'.format(column, order))
self.layoutAboutToBeChanged.emit()
print(self._df.columns[column])
self._df.sort_values('time', ascending=order == Qt.AscendingOrder, inplace=True)
print(self._df)
self.layoutChanged.emit()
class DataFrameWidget(QWidget):
''' a simple widget for using DataFrames in a gui '''
def __init__(self, dataFrame, parent=None):
super(DataFrameWidget, self).__init__(parent)
self.dataModel = DataFrameModel()
# Set DataFrame
self.dataTable = QTableView()
# self.proxy = QSortFilterProxyModel()
# self.proxy.setSourceModel(self.dataModel)
self.dataTable.setModel(self.dataModel)
self.dataTable.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.setDataFrame(dataFrame)
self.dataTable.setSortingEnabled(True)
self.dataTable.sortByColumn(0,0)
layout = QVBoxLayout()
layout.addWidget(self.dataTable)
self.setLayout(layout)
def setDataFrame(self, dataFrame):
self.dataModel.setDataFrame(dataFrame)
self.dataModel.signalUpdate()
def testDf():
''' creates test dataframe '''
# data = {'int': [1, 2, 3], 'float': [1.5, 2.5, 3.5],
# 'string': ['a', 'b', 'c'], 'nan': [np.nan, np.nan, np.nan]}
# data = [(1, 1.5, 'a', np.nan),
# (2, 2.5, 'b', np.nan),
# (3, 3.5, 'c', np.nan)]
return DataFrame(tmp, columns=['date', 'time', 'string', 'nan', 'bla'])
class Form(QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
df = testDf() # make up some data
widget = DataFrameWidget(df)
layout = QVBoxLayout()
layout.addWidget(widget)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
form = Form()
form.show()
exit(app.exec_())
Using the SortFilterProxy worked for this example, but was very slow on larger dataframes.
The above code expample did work, read sorted, for not-dataframe data. Creating a model/view with just the list of tuples worked fine.
The advices I found were mainly in two direction: remember to signal the view or use sortfilterproxy. I remembered and tried but not succeeded so far. Seems to be related to the usage of a dataframe. All advice is welcome. Thanks in advance.
In the next part I show the result of your impression, in it we see that it is reordered, but also the index is reordered, and this causes that when it is not updated the change.
sort clicked col 0 order 1
date
date time string nan bla
0 23-02-1978 19:03:13 eh None even more some data
2 23-02-1978 19:02:55 he some data even more some data
1 23-02-1978 19:01:45 ss some data even more some data
sort clicked col 0 order 0
date
date time string nan bla
1 23-02-1978 19:01:45 ss some data even more some data
2 23-02-1978 19:02:55 he some data even more some data
0 23-02-1978 19:03:13 eh None even more some data
To update the data you must reset the indexes with reset_index()
.
def sort(self, column, order):
"""Sort table by given column number.
"""
print('sort clicked col {} order {}'.format(column, order))
self.layoutAboutToBeChanged.emit()
print(self._df.columns[column])
self._df.sort_values('time', ascending=order == Qt.AscendingOrder, inplace=True)
self._df.reset_index(inplace=True, drop=True) # <-- this is the change
print(self._df)
self.layoutChanged.emit()