Here is my python code, it implements a data editor for time series that then shall be used with the pyEDM pacakge to estimate the optimal embeding dimension using the simplex projection from pyEDM with the EmbedDimension function and plot the results with matplotlib.  The logic for the optimal embeding dimension calculation is currently not implemented fully, also, the user shall have the ability to manualy input the required parameters ( like T, etc). Implement these improvements, pay attention to the pyEDM documentation! "import sys
import pandas as pd
import numpy as np
from datetime import datetime
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex, QSortFilterProxyModel, QDate, QSize
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QTableView, QSplitter, QWidget, QVBoxLayout,
    QFileDialog, QTabWidget, QToolBar, QStyledItemDelegate, QMessageBox, QMenu,
    QStatusBar, QLabel, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QComboBox,
    QDateEdit, QSpinBox, QPushButton
)

# Import pyEDM and matplotlib
import pyEDM
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


# Custom Model to interface pandas DataFrame with QTableView
class PandasModel(QAbstractTableModel):
    dataChangedSignal = QtCore.Signal()

    def __init__(self, df=pd.DataFrame(), parent=None):
        super().__init__(parent)
        self._df = df.copy()

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            try:
                return str(self._df.columns[section])
            except (IndexError,):
                return None
        elif orientation == Qt.Vertical:
            try:
                return str(self._df.index[section])
            except (IndexError,):
                return None

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        if role in (Qt.DisplayRole, Qt.EditRole):
            value = self._df.iat[index.row(), index.column()]
            return str(value)
        return None

    def setData(self, index, value, role=Qt.EditRole):
        if index.isValid():
            try:
                self._df.iat[index.row(), index.column()] = value
                self.dataChanged.emit(index, index, [role])
                self.dataChangedSignal.emit()
                return True
            except Exception as e:
                QMessageBox.warning(None, "Error", f"Failed to set data: {e}")
        return False

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

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

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

    def load_data(self, df):
        self.beginResetModel()
        self._df = df.copy()
        self.endResetModel()
        self.dataChangedSignal.emit()

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


# Custom Delegate for different data types
class CustomDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)

    def createEditor(self, parent, option, index):
        # Implement different editors based on column data type if needed
        return super().createEditor(parent, option, index)


# Variable Model (modified to remove Label)
class VariableModel(QAbstractTableModel):
    def __init__(self, df=pd.DataFrame(), parent=None):
        super().__init__(parent)
        self._df = df.copy()

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            try:
                return str(self._df.columns[section])
            except IndexError:
                return None
        elif orientation == Qt.Vertical:
            try:
                return str(self._df.index[section])
            except IndexError:
                return None

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        if role in (Qt.DisplayRole, Qt.EditRole):
            value = self._df.iat[index.row(), index.column()]
            return str(value)
        return None

    def setData(self, index, value, role=Qt.EditRole):
        # No Label editing anymore
        return False

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

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

    def flags(self, index):
        # No Label editing anymore
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled

    def load_data(self, df):
        self.beginResetModel()
        self._df = df.copy()
        self.endResetModel()


# Data Viewer Class
class DataViewer(QTableView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.model = PandasModel()
        self.setModel(self.model)
        self.setSelectionBehavior(QTableView.SelectItems)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.open_context_menu)
        self.setItemDelegate(CustomDelegate(self))
        # Connect data changes to emit signals
        self.model.dataChangedSignal.connect(self.on_data_changed)

    def on_data_changed(self):
        # Emit a custom signal or handle updates if needed
        pass

    def load_csv(self, file_path):
        try:
            df = pd.read_csv(file_path, parse_dates=True)
            self.model.load_data(df)
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to load CSV file:\n{e}")

    def load_excel(self, file_path):
        try:
            df = pd.read_excel(file_path, parse_dates=True)
            self.model.load_data(df)
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to load Excel file:\n{e}")

    def save_csv(self, file_path):
        try:
            self.model.get_dataframe().to_csv(file_path, index=False)
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to save CSV file:\n{e}")

    def save_excel(self, file_path):
        try:
            self.model.get_dataframe().to_excel(file_path, index=False)
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to save Excel file:\n{e}")

    def open_context_menu(self, position):
        menu = QMenu()
        add_row_action = menu.addAction("Add Row")
        remove_row_action = menu.addAction("Remove Row")
        add_column_action = menu.addAction("Add Column")
        remove_column_action = menu.addAction("Remove Column")

        action = menu.exec_(self.viewport().mapToGlobal(position))
        if action == add_row_action:
            self.add_row()
        elif action == remove_row_action:
            self.remove_row()
        elif action == add_column_action:
            self.add_column()
        elif action == remove_column_action:
            self.remove_column()

    def add_row(self):
        df = self.model.get_dataframe()
        new_row = pd.Series({col: "" for col in df.columns})
        df = df.append(new_row, ignore_index=True)
        self.model.load_data(df)

    def remove_row(self):
        index = self.currentIndex()
        if not index.isValid():
            QMessageBox.warning(self, "Remove Row", "No row selected.")
            return
        df = self.model.get_dataframe()
        if 0 <= index.row() < len(df):
            df = df.drop(index=row.index).reset_index(drop=True)
            df = df.drop(index=index.row()).reset_index(drop=True)
            self.model.load_data(df)
        else:
            QMessageBox.warning(self, "Remove Row", "Invalid row selected.")

    def add_column(self):
        df = self.model.get_dataframe()
        new_col_name, ok = QtWidgets.QInputDialog.getText(self, "Add Column", "Column Name:")
        if ok and new_col_name:
            if new_col_name in df.columns:
                QMessageBox.warning(self, "Add Column", "Column already exists.")
                return
            df[new_col_name] = ""
            self.model.load_data(df)

    def remove_column(self):
        index = self.currentIndex()
        if not index.isValid():
            QMessageBox.warning(self, "Remove Column", "No column selected.")
            return
        col = self.model._df.columns[index.column()]
        confirmation = QMessageBox.question(
            self, "Remove Column", f"Are you sure you want to remove column '{col}'?",
            QMessageBox.Yes | QMessageBox.No
        )
        if confirmation == QMessageBox.Yes:
            df = self.model.get_dataframe()
            df = df.drop(columns=[col])
            self.model.load_data(df)


# Variable Viewer (modified to remove Label)
class VariableViewer(QTableView):
    def __init__(self, data_viewer, parent=None):
        super().__init__(parent)
        self.data_viewer = data_viewer
        self.model = VariableModel(pd.DataFrame(columns=["Name", "Type"]))  # Removed Label
        self.setModel(self.model)
        self.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked | QtWidgets.QAbstractItemView.SelectedClicked)
        self.data_viewer.model.modelReset.connect(self.update_variables)

    def update_variables(self):
        df = self.data_viewer.model.get_dataframe()
        var_info = {
            "Name": df.columns,
            "Type": df.dtypes.astype(str),
            # "Label": [""] * len(df.columns)  # Removed Label
        }
        var_df = pd.DataFrame(var_info)
        self.model.load_data(var_df)


# Dialog for Generating Random Time Series Data
class RandomDataDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Generate Random Time Series Data")
        self.setModal(True)
        self.init_ui()

    def init_ui(self):
        layout = QFormLayout()

        # Start Date
        self.start_date_edit = QDateEdit(self)
        self.start_date_edit.setCalendarPopup(True)
        self.start_date_edit.setDate(QDate.currentDate().addYears(-1))
        layout.addRow("Start Date:", self.start_date_edit)

        # End Date
        self.end_date_edit = QDateEdit(self)
        self.end_date_edit.setCalendarPopup(True)
        self.end_date_edit.setDate(QDate.currentDate())
        layout.addRow("End Date:", self.end_date_edit)

        # Frequency
        self.frequency_combo = QComboBox(self)
        self.frequency_combo.addItems(["Daily", "Weekly", "Monthly", "Quarterly", "Yearly"])
        layout.addRow("Frequency:", self.frequency_combo)

        # Number of Variables
        self.num_vars_spin = QSpinBox(self)
        self.num_vars_spin.setRange(1, 100)
        self.num_vars_spin.setValue(5)
        layout.addRow("Number of Variables:", self.num_vars_spin)

        # Variable Name Prefix
        self.var_prefix_edit = QLineEdit(self)
        self.var_prefix_edit.setText("Variable")
        layout.addRow("Variable Name Prefix:", self.var_prefix_edit)

        # Buttons
        button_layout = QHBoxLayout()
        self.generate_button = QPushButton("Generate", self)
        self.cancel_button = QPushButton("Cancel", self)
        button_layout.addWidget(self.generate_button)
        button_layout.addWidget(self.cancel_button)
        layout.addRow(button_layout)

        self.setLayout(layout)

        # Connect buttons
        self.generate_button.clicked.connect(self.accept)
        self.cancel_button.clicked.connect(self.reject)

    def get_parameters(self):
        start_date = self.start_date_edit.date().toPython()
        end_date = self.end_date_edit.date().toPython()
        frequency = self.frequency_combo.currentText()
        num_vars = self.num_vars_spin.value()
        var_prefix = self.var_prefix_edit.text()
        return start_date, end_date, frequency, num_vars, var_prefix


# Ribbon UI Implementation
class RibbonUI(QToolBar):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMovable(False)
        self.setIconSize(QSize(32, 32))
        self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
        self.init_ui()

    def init_ui(self):
        self.init_file_actions()
        self.addSeparator()
        self.init_edit_actions()
        self.addSeparator()
        self.init_data_actions()

    def init_file_actions(self):
        self.new_action = self.addAction(QIcon.fromTheme("document-new"), "New")
        self.open_action = self.addAction(QIcon.fromTheme("document-open"), "Open")
        self.save_action = self.addAction(QIcon.fromTheme("document-save"), "Save")
        self.save_as_action = self.addAction(QIcon.fromTheme("document-save-as"), "Save As")
        self.generate_random_action = self.addAction(QIcon.fromTheme("document-new"), "Generate\nRandom Data")

    def init_edit_actions(self):
        self.cut_action = self.addAction(QIcon.fromTheme("edit-cut"), "Cut")
        self.copy_action = self.addAction(QIcon.fromTheme("edit-copy"), "Copy")
        self.paste_action = self.addAction(QIcon.fromTheme("edit-paste"), "Paste")
        self.undo_action = self.addAction(QIcon.fromTheme("edit-undo"), "Undo")
        self.redo_action = self.addAction(QIcon.fromTheme("edit-redo"), "Redo")

    def init_data_actions(self):
        self.sort_action = self.addAction(QIcon.fromTheme("view-sort-ascending"), "Sort")
        self.filter_action = self.addAction(QIcon.fromTheme("view-filter"), "Filter")
        self.compute_action = self.addAction(QIcon.fromTheme("accessories-calculator"), "Compute")
        self.recode_action = self.addAction(QIcon.fromTheme("edit-find-replace"), "Recode")


# Embedding Dimension Analysis Class
class EmbeddingDimensionAnalysis:
    def __init__(self, time_series_data):
        self.time_series_data = time_series_data
        self.optimal_E = None
        self.rhos = []

    def run_analysis(self):
        # Use pyEDM's EmbedDimension to calculate optimal E
        # (Replace with your actual analysis logic)
        for E in range(1, 11):  # Example: Test dimensions 1 to 10
            library_string = "1 {}".format(len(self.time_series_data) - E)
            prediction_string = "{} {}".format(len(self.time_series_data) - E + 1, len(self.time_series_data))
            rho = pyEDM.EmbedDimension(dataFrame=self.time_series_data,
                                      lib=library_string,
                                      pred=prediction_string,
                                      E=E,
                                      columns="Value",
                                      target="Value",
                                      showPlot=False)
            self.rhos.append(rho["rho"][0])

        self.optimal_E = self.rhos.index(max(self.rhos)) + 1

    def generate_plot(self):
        fig = Figure()
        ax = fig.add_subplot(111)
        ax.plot(range(1, len(self.rhos) + 1), self.rhos, marker='o')
        ax.set_xlabel("Embedding Dimension")
        ax.set_ylabel("Forecasting Skill (rho)")
        ax.set_title("Optimal Embedding Dimension")
        return fig


# Main Window Class (modified to add the new tab)
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Time Series Data Editor")
        self.resize(1200, 800)

        self.central_widget = QWidget()
        # self.setCentralWidget(self.central_widget)  # Moved below

        # Setup Ribbon UI
        self.ribbon = RibbonUI(self)
        self.addToolBar(self.ribbon)

        # Setup Splitter with DataViewer and VariableViewer
        self.splitter = QSplitter(Qt.Horizontal)
        self.data_viewer = DataViewer()
        self.variable_viewer = VariableViewer(self.data_viewer)

        self.splitter.addWidget(self.data_viewer)
        self.splitter.addWidget(self.variable_viewer)
        self.splitter.setStretchFactor(0, 3)
        self.splitter.setStretchFactor(1, 1)

        # Setup Tab Widget
        self.tab_widget = QTabWidget()
        self.tab_widget.addTab(self.splitter, "Data")

        # Optimal Embedding Dimension Tab
        self.embedding_tab = QWidget()
        embedding_layout = QVBoxLayout()
        self.calculate_button = QPushButton("Calculate Optimal E")
        self.canvas = FigureCanvas(Figure())  # Create a canvas for the plot
        embedding_layout.addWidget(self.calculate_button)
        embedding_layout.addWidget(self.canvas)
        self.embedding_tab.setLayout(embedding_layout)
        self.tab_widget.addTab(self.embedding_tab, "Optimal Embedding Dimension")

        # Set Tab Widget as Central Widget
        self.setCentralWidget(self.tab_widget)

        # Setup Status Bar
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        self.status_label = QLabel("Ready")
        self.status_bar.addWidget(self.status_label)

        self.current_file = None

        self.connect_actions()

    def connect_actions(self):
        # Connect actions from RibbonUI
        self.ribbon.new_action.triggered.connect(self.new_file)
        self.ribbon.open_action.triggered.connect(self.open_file)
        self.ribbon.save_action.triggered.connect(self.save_file)
        self.ribbon.save_as_action.triggered.connect(self.save_file_as)
        self.ribbon.generate_random_action.triggered.connect(self.generate_random_data)

        self.ribbon.cut_action.triggered.connect(self.cut)
        self.ribbon.copy_action.triggered.connect(self.copy)
        self.ribbon.paste_action.triggered.connect(self.paste)
        self.ribbon.undo_action.triggered.connect(self.undo)
        self.ribbon.redo_action.triggered.connect(self.redo)

        self.ribbon.sort_action.triggered.connect(self.sort_data)
        self.ribbon.filter_action.triggered.connect(self.filter_data)
        self.ribbon.compute_action.triggered.connect(self.compute_data)
        self.ribbon.recode_action.triggered.connect(self.recode_data)

        self.calculate_button.clicked.connect(self.calculate_optimal_e)

    def new_file(self):
        confirmation = QMessageBox.question(
            self, "New File", "Are you sure you want to create a new file?\nUnsaved changes will be lost.",
            QMessageBox.Yes | QMessageBox.No
        )
        if confirmation == QMessageBox.Yes:
            self.data_viewer.model.load_data(pd.DataFrame())
            self.current_file = None
            self.status_label.setText("New file created.")

    def open_file(self):
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getOpenFileName(
            self, "Open Data File", "",
            "CSV Files (*.csv);;Excel Files (*.xlsx *.xls);;All Files (*)",
            options=options)
        if file_name:
            if file_name.endswith('.csv'):
                self.data_viewer.load_csv(file_name)
            elif file_name.endswith(('.xlsx', '.xls')):
                self.data_viewer.load_excel(file_name)
            else:
                QMessageBox.warning(self, "Unsupported Format", "Only CSV and Excel files are supported.")
                return
            self.current_file = file_name
            self.status_label.setText(f"Loaded file: {file_name}")

    def save_file(self):
        if self.current_file:
            if self.current_file.endswith('.csv'):
                self.data_viewer.save_csv(self.current_file)
            elif self.current_file.endswith(('.xlsx', '.xls')):
                self.data_viewer.save_excel(self.current_file)
            self.status_label.setText(f"Saved file: {self.current_file}")
        else:
            self.save_file_as()

    def save_file_as(self):
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getSaveFileName(
            self, "Save Data File As", "",
            "CSV Files (*.csv);;Excel Files (*.xlsx *.xls);;All Files (*)",
            options=options)
        if file_name:
            if file_name.endswith('.csv'):
                self.data_viewer.save_csv(file_name)
            elif file_name.endswith(('.xlsx', '.xls')):
                self.data_viewer.save_excel(file_name)
            else:
                QMessageBox.warning(self, "Unsupported Format",
                                    "Please use a valid file extension (.csv, .xlsx, .xls).")
                return
            self.current_file = file_name
            self.status_label.setText(f"Saved file as: {file_name}")

    def generate_random_data(self):
        dialog = RandomDataDialog(self)
        if dialog.exec() == QDialog.Accepted:
            start_date, end_date, frequency, num_vars, var_prefix = dialog.get_parameters()
            try:
                # Map frequency to pandas offset aliases
                freq_map = {
                    "Daily": "D",
                    "Weekly": "W",
                    "Monthly": "M",
                    "Quarterly": "Q",
                    "Yearly": "A"
                }
                pandas_freq = freq_map.get(frequency, "D")

                # Generate date range
                dates = pd.date_range(start=start_date, end=end_date, freq=pandas_freq)
                if len(dates) == 0:
                    QMessageBox.warning(self, "Invalid Date Range",
                                        "No dates generated. Please adjust the date range or frequency.")
                    return

                # Generate random data
                data = {}
                for i in range(1, num_vars + 1):
                    col_name = f"{var_prefix}_{i}"
                    data[col_name] = np.random.randn(len(dates))  # Normally distributed data

                df = pd.DataFrame(data, index=dates)
                df.index.name = "Date"

                # Load data into DataViewer
                self.data_viewer.model.load_data(df)
                self.status_label.setText("Random time series data generated.")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to generate random data:\n{e}")

    def cut(self):
        self.copy()
        indexes = self.data_viewer.selectionModel().selectedIndexes()
        if indexes:
            rows = sorted(index.row() for index in indexes)
            df = self.data_viewer.model.get_dataframe()
            df = df.drop(index=rows).reset_index(drop=True)
            self.data_viewer.model.load_data(df)
            self.status_label.setText("Cut selected cells.")
        else:
            QMessageBox.warning(self, "Cut", "No cells selected.")

    def copy(self):
        indexes = self.data_viewer.selectionModel().selectedIndexes()
        if indexes:
            # Sort indexes to iterate properly
            indexes = sorted(indexes, key=lambda x: (x.row(), x.column()))
            rows = set(index.row() for index in indexes)
            cols = set(index.column() for index in indexes)
            df = self.data_viewer.model.get_dataframe()
            selection = df.iloc[list(rows), list(cols)]
            clipboard_text = selection.to_csv(sep='\t', index=False, header=False)
            QtGui.QGuiApplication.clipboard().setText(clipboard_text)
            self.status_label.setText("Copied selected cells.")
        else:
            QMessageBox.warning(self, "Copy", "No cells selected.")

    def paste(self):
        clipboard = QtGui.QGuiApplication.clipboard().text()
        if not clipboard:
            QMessageBox.warning(self, "Paste", "Clipboard is empty.")
            return
        try:
            # Read clipboard data into DataFrame
            from io import StringIO
            data = pd.read_csv(StringIO(clipboard), sep='\t', header=None)
            df = self.data_viewer.model.get_dataframe()
            selected = self.data_viewer.selectionModel().selectedIndexes()
            if not selected:
                QMessageBox.warning(self, "Paste", "No cell selected to paste into.")
                return
            start_row = selected[0].row()
            start_col = selected[0].column()
            for i, row in data.iterrows():
                for j, value in row.iteritems():
                    if start_row + i < len(df.columns) and start_col + j < len(df.columns):
                        df.iat[start_row + i, start_col + j] = value
            self.data_viewer.model.load_data(df)
            self.status_label.setText("Pasted data from clipboard.")
        except Exception as e:
            QMessageBox.warning(self, "Paste Error", f"Failed to paste data:\n{e}")

    def undo(self):
        # Placeholder for undo functionality
        QMessageBox.information(self, "Undo", "Undo operation not implemented yet.")

    def redo(self):
        # Placeholder for redo functionality
        QMessageBox.information(self, "Redo", "Redo operation not implemented yet.")

    def sort_data(self):
        # Placeholder for sort dialog and functionality
        QMessageBox.information(self, "Sort", "Sort operation not implemented yet.")

    def filter_data(self):
        # Placeholder for filter dialog and functionality
        QMessageBox.information(self, "Filter", "Filter operation not implemented yet.")

    def compute_data(self):
        # Placeholder for compute dialog and functionality
        QMessageBox.information(self, "Compute", "Compute operation not implemented yet.")

    def recode_data(self):
        # Placeholder for recode dialog and functionality
        QMessageBox.information(self, "Recode", "Recode operation not implemented yet.")

    def calculate_optimal_e(self):
        df = self.data_viewer.model.get_dataframe()
        if df.empty:
            QMessageBox.warning(self, "No Data", "Please load data first.")
            return

        # Assuming your time series data is in a column named "Value"
        if "Value" not in df.columns:
            QMessageBox.warning(self, "No 'Value' Column", "Please ensure your time series data is in a column named 'Value'.")
            return

        time_series_data = df[["Value"]].copy()  # Select the time series data
        analysis = EmbeddingDimensionAnalysis(time_series_data)
        analysis.run_analysis()

        # Get the generated plot and display it on the canvas
        fig = analysis.generate_plot()
        self.canvas.figure = fig
        self.canvas.draw()

        QMessageBox.information(self, "Optimal E", f"The optimal embedding dimension is: {analysis.optimal_E}")


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()"