From 232de9f145862a1a1e493478ba5fe8a6e4866e5a Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Thu, 5 Oct 2023 21:28:11 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20"=D0=9B=D0=B8=D0=BD=D0=B5=D0=B9=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=BE=D0=BB=D0=B8=D0=BD=D0=BE=D0=BC"=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 2 +- pyproject.toml | 6 +- statapp/__main__.py | 1 - statapp/calculations.py | 45 ++++++++++++++- statapp/linear_polynom_window.py | 42 ++++++++++++++ statapp/main_window.py | 64 ++++++++++++++------- statapp/models/linear_polynom_model.py | 31 ++++++++++ statapp/ui/linear_polynom_window.ui | 49 ++++++++++++++++ statapp/ui/main_window.ui | 8 ++- statapp/ui/ui_linear_polynom_window.py | 79 ++++++++++++++++++++++++++ statapp/ui/ui_main_window.py | 6 +- statapp/utils.py | 2 + 12 files changed, 309 insertions(+), 26 deletions(-) create mode 100644 statapp/linear_polynom_window.py create mode 100644 statapp/models/linear_polynom_model.py create mode 100644 statapp/ui/linear_polynom_window.ui create mode 100644 statapp/ui/ui_linear_polynom_window.py diff --git a/poetry.lock b/poetry.lock index 539e932..1c709bf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -771,4 +771,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.9" -content-hash = "a652dcb9e1a8a200480197c5618b7ddb4cb33fbf1a89278058eeda745ea7293f" +content-hash = "ff683c2a3f778cd6ad946d6aa4b1f567514f36026e07fccffdb2cd7e86778e0a" diff --git a/pyproject.toml b/pyproject.toml index cfc236f..2292dac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,9 +12,11 @@ readme = "README.md" python = ">=3.8,<3.9" pre-commit = "^3.4.0" pyinstaller = "^6.0.0" -pandas = "^2.0" pyside2 = "^5.15.2.1" -pylint = "^2" +pandas = { version = "^2", markers = "python_version < '3.9'" } +pylint = { version = "^2", markers = "python_version < '3.9'" } +# scipy = { version = "^1", markers = "python_version < '3.9'" } +# openpyxl = "^3.1.2" [build-system] diff --git a/statapp/__main__.py b/statapp/__main__.py index 85a118a..5e760de 100644 --- a/statapp/__main__.py +++ b/statapp/__main__.py @@ -39,6 +39,5 @@ def main(): window.show() return app.exec_() - if __name__ == "__main__": sys.exit(main()) diff --git a/statapp/calculations.py b/statapp/calculations.py index 871fafb..25381ea 100644 --- a/statapp/calculations.py +++ b/statapp/calculations.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from dataclasses import dataclass + import numpy as np import pandas as pd @@ -24,6 +26,10 @@ DIRECT_LINK = 0 INDIRECT_LINK = 1 +def generateYValues(mean, std, count): + return np.random.normal(mean, std, size=(count, 1)) + + def generateXValues(mean, std, typeConnection, yColumn): yMean = np.mean(yColumn) values = [] @@ -38,7 +44,9 @@ def generateXValues(mean, std, typeConnection, yColumn): else: x = mean values.append(x) - return np.array(values) + + res = np.array(values) + return res.reshape(len(res), 1) def varianceAnalysis(data): @@ -49,3 +57,38 @@ def varianceAnalysis(data): def correlationAnalysis(data): return pd.DataFrame(data).corr().to_numpy() + +@dataclass() +class LinearPolynomResult: + paramsAndImportance: np.ndarray + residualVariance: np.float64 + + +def linearPolynom(inputData) -> LinearPolynomResult: + x = inputData[:, 1:] + y = inputData[:, 0] + data = pd.DataFrame(x) + data.insert(0, 'const', 1) + # --- + result = np.linalg.lstsq(data, y, rcond=None) + # Коэффициенты регрессии + params = result[0] + # Остатки + residues = result[1] + + # Степень свободы + dof = len(data) - len(params) + mse = residues / dof + cov = mse * np.diagonal(np.linalg.inv(data.T @ data)) + se = np.sqrt(cov) + tStatistics = params / se + + # возможно стоит сделать через np.reshape + np.concatenate + out = pd.DataFrame() + out[0] = params + out[1] = tStatistics + + return LinearPolynomResult( + out.to_numpy(), + np.float64(mse[0]) + ) diff --git a/statapp/linear_polynom_window.py b/statapp/linear_polynom_window.py new file mode 100644 index 0000000..b37bfd3 --- /dev/null +++ b/statapp/linear_polynom_window.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2023 Maxim Slipenko, Eugene Lazurenko. +# +# This file is part of Statapp +# (see https://github.com/shizand/statapp). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from PySide2.QtWidgets import QDialog, QHeaderView + +from statapp.calculations import linearPolynom +from statapp.models.linear_polynom_model import LinearPolynomModel +from statapp.ui.ui_linear_polynom_window import Ui_LinearPolynomWindow +from statapp.utils import addIcon + + +class LinearPolynomWindow(QDialog): + def __init__(self, data): + super().__init__() + self.ui = Ui_LinearPolynomWindow() + self.ui.setupUi(self) + addIcon(self) + + result = linearPolynom(data) + + self.model = LinearPolynomModel(result.paramsAndImportance.round(2)) + self.ui.tableView.setModel(self.model) + header = self.ui.tableView.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + + self.ui.residualVarianceValueLabel.setText(str(result.residualVariance)) diff --git a/statapp/main_window.py b/statapp/main_window.py index eb6172d..8f90f37 100644 --- a/statapp/main_window.py +++ b/statapp/main_window.py @@ -21,8 +21,9 @@ import numpy as np from PySide2.QtCore import Slot from PySide2.QtWidgets import QMainWindow, QMessageBox -from statapp.calculations import generateXValues +from statapp.calculations import generateXValues, generateYValues from statapp.generate_factor_window import GenerateFactorWindow +from statapp.linear_polynom_window import LinearPolynomWindow from statapp.models.input_values_model import InputValuesModel from statapp.generate_window import GenerateWindow from statapp.about_window import AboutWindow @@ -46,12 +47,48 @@ class MainWindow(QMainWindow): self.ui.varianceAnalysisAction.setEnabled(False) self.ui.correlationAnalisisAction.setEnabled(False) + self.mainActions = [ + self.ui.varianceAnalysisAction, + self.ui.correlationAnalisisAction, + self.ui.linearPolynomAction, + ] + self.aboutWindow = None self.isDataChanged = False self.model = InputValuesModel() self.fileModel = FileSLCModel() self.ui.tableView.setModel(self.model) + self.model.layoutChanged.connect(self.updateActionsEnabled) + self.updateActionsEnabled() + # + # Для быстрой отладки + # n = 10 + # y = generateYValues(100, 5, n) + # x1 = generateXValues(20, 2, 0, y) + # x2 = generateXValues(10, 1, 0, y) + # self.model.updateAllData(np.concatenate([y, x1, x2], axis=1)) + + + def updateActionsEnabled(self): + data = self.model.getData() + + # есть только отклик + if data.shape[1] == 1: + self.ui.generateXaction.setEnabled(True) + self.setEnabledMainActions(False) + # есть отклик и фактор(ы) + elif data.shape[1] > 1: + self.ui.generateXaction.setEnabled(True) + self.setEnabledMainActions(True) + else: + self.ui.generateXaction.setEnabled(False) + self.setEnabledMainActions(False) + + + def setEnabledMainActions(self, enabled): + for action in self.mainActions: + action.setEnabled(enabled) @Slot() def on_openfileaction_triggered(self): @@ -85,18 +122,6 @@ class MainWindow(QMainWindow): self.model.updateAllData(data) self.isDataChanged = False - if data.shape[1] == 1: - self.ui.generateXaction.setEnabled(True) - self.ui.varianceAnalysisAction.setEnabled(False) - self.ui.correlationAnalisisAction.setEnabled(False) - elif data.shape[1] > 1: - self.ui.generateXaction.setEnabled(True) - self.ui.varianceAnalysisAction.setEnabled(True) - self.ui.correlationAnalisisAction.setEnabled(True) - else: - self.ui.generateXaction.setEnabled(False) - self.ui.varianceAnalysisAction.setEnabled(False) - self.ui.correlationAnalisisAction.setEnabled(False) @Slot() def on_savefileaction_triggered(self): @@ -112,10 +137,9 @@ class MainWindow(QMainWindow): gw = GenerateWindow() if gw.exec(): - y = np.random.normal(gw.mat, gw.deviation, size=(gw.count, 1)) + y = generateYValues(gw.mat, gw.deviation, gw.count) self.model.updateAllData(y.round(2)) self.isDataChanged = True - self.ui.generateXaction.setEnabled(True) @Slot() def on_generateXaction_triggered(self): @@ -125,11 +149,8 @@ class MainWindow(QMainWindow): data = self.model.getData() y = self.model.getY() xValues = generateXValues(gfw.mat, gfw.deviation, gfw.typeConnection, y) - xValues = xValues.reshape(len(xValues), 1).round(2) - data = np.concatenate((data, xValues), axis=1) + data = np.concatenate((data, xValues.round(2)), axis=1) self.model.updateAllData(data) - self.ui.varianceAnalysisAction.setEnabled(True) - self.ui.correlationAnalisisAction.setEnabled(True) self.isDataChanged = True @Slot() @@ -147,6 +168,11 @@ class MainWindow(QMainWindow): dw = CorrelationAnalysisWindow(self.model.getData()) dw.exec() + @Slot() + def on_linearPolynomAction_triggered(self): + dw = LinearPolynomWindow(self.model.getData()) + dw.exec() + def closeEvent(self, event): if self.isDataChanged: file = '' diff --git a/statapp/models/linear_polynom_model.py b/statapp/models/linear_polynom_model.py new file mode 100644 index 0000000..249ae16 --- /dev/null +++ b/statapp/models/linear_polynom_model.py @@ -0,0 +1,31 @@ +# +# Copyright (c) 2023 Maxim Slipenko, Eugene Lazurenko. +# +# This file is part of Statapp +# (see https://github.com/shizand/statapp). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from PySide2.QtCore import QModelIndex + +from statapp.models.ro_table_model import ROTableModel + + +class LinearPolynomModel(ROTableModel): + def getHorizontalHeader(self): + return ['Коэффициент регрессии', 'Коэффициент значимости'] + + def getVerticalHeader(self): + count = (self.rowCount(QModelIndex())) + return ['Свободный член'] + [f'X{i}' for i in range(1, count)] diff --git a/statapp/ui/linear_polynom_window.ui b/statapp/ui/linear_polynom_window.ui new file mode 100644 index 0000000..09e0768 --- /dev/null +++ b/statapp/ui/linear_polynom_window.ui @@ -0,0 +1,49 @@ + + + LinearPolynomWindow + + + + 0 + 0 + 630 + 400 + + + + Линейный полином + + + + + + + + + + + 10 + + + + + Остаточная дисперсия: + + + + + + + undefined + + + + + + + + + + + + diff --git a/statapp/ui/main_window.ui b/statapp/ui/main_window.ui index 37a6252..dd762e4 100644 --- a/statapp/ui/main_window.ui +++ b/statapp/ui/main_window.ui @@ -40,7 +40,7 @@ 0 0 800 - 21 + 27 @@ -69,6 +69,7 @@ Моделирование + @@ -123,6 +124,11 @@ Корреляционный анализ + + + Линейный полином + + diff --git a/statapp/ui/ui_linear_polynom_window.py b/statapp/ui/ui_linear_polynom_window.py new file mode 100644 index 0000000..254cb4e --- /dev/null +++ b/statapp/ui/ui_linear_polynom_window.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Maxim Slipenko, Eugene Lazurenko. +# +# This file is part of Statapp +# (see https://github.com/shizand/statapp). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + + +################################################################################ +## Form generated from reading UI file 'linear_polynom_window.ui' +## +## Created by: Qt User Interface Compiler version 5.15.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import * +from PySide2.QtGui import * +from PySide2.QtWidgets import * + + +class Ui_LinearPolynomWindow(object): + def setupUi(self, LinearPolynomWindow): + if not LinearPolynomWindow.objectName(): + LinearPolynomWindow.setObjectName(u"LinearPolynomWindow") + LinearPolynomWindow.resize(630, 400) + self.gridLayout_2 = QGridLayout(LinearPolynomWindow) + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.gridLayout = QGridLayout() + self.gridLayout.setObjectName(u"gridLayout") + self.tableView = QTableView(LinearPolynomWindow) + self.tableView.setObjectName(u"tableView") + + self.gridLayout.addWidget(self.tableView, 0, 0, 1, 1) + + self.gridLayout_3 = QGridLayout() + self.gridLayout_3.setObjectName(u"gridLayout_3") + self.gridLayout_3.setContentsMargins(-1, 10, -1, -1) + self.residualVarianceLabel = QLabel(LinearPolynomWindow) + self.residualVarianceLabel.setObjectName(u"residualVarianceLabel") + + self.gridLayout_3.addWidget(self.residualVarianceLabel, 0, 0, 1, 1) + + self.residualVarianceValueLabel = QLabel(LinearPolynomWindow) + self.residualVarianceValueLabel.setObjectName(u"residualVarianceValueLabel") + + self.gridLayout_3.addWidget(self.residualVarianceValueLabel, 0, 1, 1, 1) + + + self.gridLayout.addLayout(self.gridLayout_3, 1, 0, 1, 1) + + + self.gridLayout_2.addLayout(self.gridLayout, 0, 0, 1, 1) + + + self.retranslateUi(LinearPolynomWindow) + + QMetaObject.connectSlotsByName(LinearPolynomWindow) + # setupUi + + def retranslateUi(self, LinearPolynomWindow): + LinearPolynomWindow.setWindowTitle(QCoreApplication.translate("LinearPolynomWindow", u"\u041b\u0438\u043d\u0435\u0439\u043d\u044b\u0439 \u043f\u043e\u043b\u0438\u043d\u043e\u043c", None)) + self.residualVarianceLabel.setText(QCoreApplication.translate("LinearPolynomWindow", u"\u041e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u0430\u044f \u0434\u0438\u0441\u043f\u0435\u0440\u0441\u0438\u044f:", None)) + self.residualVarianceValueLabel.setText(QCoreApplication.translate("LinearPolynomWindow", u"undefined", None)) + # retranslateUi diff --git a/statapp/ui/ui_main_window.py b/statapp/ui/ui_main_window.py index 89f3292..92106cf 100644 --- a/statapp/ui/ui_main_window.py +++ b/statapp/ui/ui_main_window.py @@ -54,6 +54,8 @@ class Ui_MainWindow(object): self.varianceAnalysisAction.setObjectName(u"varianceAnalysisAction") self.correlationAnalisisAction = QAction(MainWindow) self.correlationAnalisisAction.setObjectName(u"correlationAnalisisAction") + self.linearPolynomAction = QAction(MainWindow) + self.linearPolynomAction.setObjectName(u"linearPolynomAction") self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") self.gridLayout = QGridLayout(self.centralwidget) @@ -73,7 +75,7 @@ class Ui_MainWindow(object): MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 800, 21)) + self.menubar.setGeometry(QRect(0, 0, 800, 27)) self.filemenu = QMenu(self.menubar) self.filemenu.setObjectName(u"filemenu") self.generatemenu = QMenu(self.menubar) @@ -101,6 +103,7 @@ class Ui_MainWindow(object): self.generatemenu.addAction(self.generateXaction) self.analyzemenu.addAction(self.varianceAnalysisAction) self.analyzemenu.addAction(self.correlationAnalisisAction) + self.modelmenu.addAction(self.linearPolynomAction) self.helpmenu.addAction(self.aboutmenuaction) self.retranslateUi(MainWindow) @@ -118,6 +121,7 @@ class Ui_MainWindow(object): self.closefileaction.setText(QCoreApplication.translate("MainWindow", u"\u0417\u0430\u043a\u0440\u044b\u0442\u044c", None)) self.varianceAnalysisAction.setText(QCoreApplication.translate("MainWindow", u"\u0414\u0438\u0441\u043f\u0435\u0440\u0441\u0438\u043e\u043d\u043d\u044b\u0439 \u0430\u043d\u0430\u043b\u0438\u0437", None)) self.correlationAnalisisAction.setText(QCoreApplication.translate("MainWindow", u"\u041a\u043e\u0440\u0440\u0435\u043b\u044f\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0430\u043d\u0430\u043b\u0438\u0437", None)) + self.linearPolynomAction.setText(QCoreApplication.translate("MainWindow", u"\u041b\u0438\u043d\u0435\u0439\u043d\u044b\u0439 \u043f\u043e\u043b\u0438\u043d\u043e\u043c", None)) self.label.setText(QCoreApplication.translate("MainWindow", u"\u0421\u0422\u0410\u0422\u0418\u0421\u0422\u0418\u0427\u0415\u0421\u041a\u0418\u0415 \u0414\u0410\u041d\u041d\u042b\u0415", None)) self.filemenu.setTitle(QCoreApplication.translate("MainWindow", u"\u0424\u0430\u0439\u043b", None)) self.generatemenu.setTitle(QCoreApplication.translate("MainWindow", u"\u0413\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0435\u0439", None)) diff --git a/statapp/utils.py b/statapp/utils.py index c4f019f..9d1e83c 100644 --- a/statapp/utils.py +++ b/statapp/utils.py @@ -34,11 +34,13 @@ def resourcePath(relative): bundleDir = os.path.dirname(os.path.abspath(__file__)) return os.path.join(bundleDir, relative) + def addIcon(windowOrDialog): icon = QIcon() icon.addFile(resourcePath("ui/images/logo.ico"), QSize(), QIcon.Normal, QIcon.Off) windowOrDialog.setWindowIcon(icon) + def safeListGet(lst, idx, default): try: return lst[idx]