Search code examples
pythonpandasdataframepyside6

QTableView update dynamic based on another cell Python


I want to change a cell based on the value in another cell. I'm using a QTableView populated by QAbstractTableModel with a pandas dataframe. Here is the code of Pandas Model:

class PandasModelEditable(QAbstractTableModel):
    def __init__(self, data, parent=None):
        QAbstractItemModel.__init__(self, parent)
        self._data = data

    def rowCount(self, parent=None):
        return len(self._data.values)

    def columnCount(self, parent=None):
        return self._data.columns.size

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

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            try:
                return '%s' % unicode(self._data.columns.tolist()[section])
            except (IndexError,):
                return unicode()
        elif orientation == Qt.Vertical:
            try:
                return '%s' % unicode(self._data.index.tolist()[section])
            except (IndexError,):
                return unicode()

    def flags(self, index):
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable

    def setData(self, index, value, role=Qt.EditRole):
        if index.isValid():
            self._data.iloc[index.row(), index.column()] = value
            if self.data(index, Qt.DisplayRole) == value:
                self.dataChanged.emit(index, index)
                return True
        return False

The principal dataframe (df in code) has this structure:

| Player  | O1_Score | O2_Score | O3_Score | O1_Rank | O2_Rank | O3_Rank |
| ------- | -------- | -------- | -------- | ------- | ------- | ------- |
| First   | 9.66     | 10.24    | 8.52     |   2     |   1     |   3     |
| Second  | 7.45     | 11.31    | 12.56    |   3     |   2     |   1     |

Obviously my dataframe has more scores (83) and its respective rank, just for example

The dataframe for table (dataframe_tp in code) and table has exactly the same columns shown below, in GUI has this visual:

| Player  | Selected O | Score | FO | NO1 | SO | NO2 | TO | NO3 |
| ------- | ---------- | ----- | -- | --- | -- | --- | -- | --- |
| First   | ---------- | ----- | 1  | --- | 2  | --- | 3  | --- |
| Second  | ---------- | ----- | 1  | --- | 2  | --- | 3  | --- |

Where: Selected O means Selected Option, FO means First Option, SO means Second Option, TO means Third Option, NO means Name Option with a number to identify the above options

Based on the table above, Score depends on Selected O, NO1 depends on FO, NO2 depends on SO, NO3 depends on TO.

Code:

self.squad_table = QTableView()

self.dataframe_tp = df.loc[:, ['Player']]
self.dataframe_tp["Selected O"] = '-'
self.dataframe_tp["Score"] = 0
self.dataframe_tp['FO'] = 1
self.dataframe_tp['NO1'] = '-'
self.dataframe_tp['SO'] = 2
self.dataframe_tp['NO2'] = '-'
self.dataframe_tp['TO'] = 3
self.dataframe_tp['NO3'] = '-'

model = PandasModelEditable(self.dataframe_tp)
self.squad_table.setModel(model)
self.squad_table.show()

And the expected output would be:

| Player  | Selected O | Score | FO |   NO1   | SO |    NO2   | TO |    NO3   |
| ------- | ---------- | ----- | -- | ------- | -- | -------- | -- | -------- |
| First   | O3_Score   | 8.52  | 1  | O2_Rank | 2  | O1_Rank  | 3  | O3_Rank  |
| Second  | O2_Score   | 11.31 | 1  | O3_Rank | 2  | O2_Rank  | 3  | O1_Rank  |

FO, SO, TO should traverse over ranks of every player and bring the column header.

And a modification can be change value of Selected O, or FO, or SO like below:

| Player  | Selected O | Score | FO |   NO1   | SO |    NO2   | TO |    NO3   |
| ------- | ---------- | ----- | -- | ------- | -- | -------- | -- | -------- |
| First   | O1_Score   | 9.66  | 2  | O1_Rank | 2  | O1_Rank  | 3  | O3_Rank  |
| Second  | O2_Score   | 11.31 | 1  | O3_Rank | 1  | O3_Rank  | 3  | O1_Rank  |

Solution

  • For the first use only edit the classes as @musicamante told me implementing this code:

    class CustomizedPandasModel(QAbstractTableModel):
    
        # Previous code hidden to clarify changes, above code is exactly the same
    
        def setData(self, index, value, role=Qt.EditRole):
            based_columns = [6, 8, 10, 12]
            if role == Qt.EditRole:
                row = index.row()
                column = index.column()
                tmp = str(value)
                
                if column in based_columns:
    
                    # If selected column in list of accepted columns to edit
    
                    if column == 6 and tmp in self._data.columns.values.tolist():
    
                        # If value is in the list of headers
    
                        index_no = self._data.columns.get_loc(tmp) # Get index of column
                        self._data.iloc[row, column + 1] = self._data.iloc[row, index_no] # Set adjacent column as the value of that attribute
                        self._data.iloc[row, column] = tmp # Set actual cell to new value
                    self.dataChanged.emit(index, index)
    

    Basically the only change was in class Model def setdata where I added a list of the index values columns(based_columns) for my utility, look up if the value to change was within the list of columns, get its index based on the value, and then define the next column as the value of that particular attribute based on the row of the edited cell and the column being looked up.