python拟合曲线

!/usr/bin/env python

-- coding: utf-8 --

"""
散点曲线拟合系统 - 美化版
使用tkinter创建美观的用户界面
"""

兼容Python 2.x和3.x的tkinter导入

try:
# Python 3.x
import tkinter as tk
from tkinter import messagebox, simpledialog, ttk
from tkinter import font as tkfont
except ImportError:
# Python 2.x
import Tkinter as tk
import tkMessageBox as messagebox
import tkSimpleDialog as simpledialog
import ttk
import tkFont as tkfont

import random
import math

定义颜色主题

THEME_COLOR = {
"background": "#f5f5f5", # 背景色
"button_bg": "#e74c3c", # 按钮背景色(改为更醒目的红色)
"button_fg": "#ffffff", # 按钮文字颜色(白色)
"button_text": "#ff0000", # 按钮内文字颜色(红色)
"button_active": "#c0392b", # 按钮激活颜色
"canvas_bg": "white", # 画布背景色
"axis_color": "#333333", # 坐标轴颜色
"point_color": "#e74c3c", # 数据点颜色
"line_color": "#2980b9", # 拟合线颜色
"text_color": "#2c3e50", # 文字颜色
"header_bg": "#2c3e50", # 标题栏背景色
"header_fg": "white", # 标题栏文字颜色
"grid_color": "#e0e0e0", # 网格线颜色
"axis_label": "#555555", # 坐标轴标签颜色
}

class EnhancedApp:
def init(self, root):
self.root = root
self.root.title("散点曲线拟合系统 - 美化版")
self.root.geometry("900x650")
self.root.configure(bg=THEME_COLOR["background"])

    # 设置窗口图标和样式
    self.setup_styles()
    
    # 创建界面
    self.create_widgets()
    
    # 初始数据
    self.x_data = []
    self.y_data = []
    
    # 调用随机数据生成函数,初始化时不显示消息
    self.generate_data(show_message=False)

def setup_styles(self):
    """设置自定义样式"""
    # 创建自定义样式
    self.style = ttk.Style()
    self.style.configure("TFrame", background=THEME_COLOR["background"])
    self.style.configure("TButton", 
                        background=THEME_COLOR["button_bg"], 
                        foreground=THEME_COLOR["button_text"],  # 使用红色文字
                        font=("微软雅黑", 12, "bold"),  # 增加字体大小
                        padding=10)  # 增加内边距
    self.style.map("TButton",
                  foreground=[('active', THEME_COLOR["button_text"])],  # 激活时也保持红色
                  background=[('active', THEME_COLOR["button_active"])])
    
    # 设置标题字体
    self.title_font = tkfont.Font(family="微软雅黑", size=20, weight="bold")  # 增大标题字体
    self.info_font = tkfont.Font(family="微软雅黑", size=12)  # 增大信息字体
    self.formula_font = tkfont.Font(family="微软雅黑", size=14, weight="bold")  # 增大公式字体
    self.axis_font = tkfont.Font(family="微软雅黑", size=10)  # 增大坐标轴字体

def create_widgets(self):
    """创建界面组件"""
    # 创建标题栏
    header_frame = tk.Frame(self.root, bg=THEME_COLOR["header_bg"], height=60)
    header_frame.pack(fill=tk.X, pady=0)
    
    # 标题标签
    title_label = tk.Label(header_frame, 
                          text="散点曲线拟合系统", 
                          font=self.title_font, 
                          bg=THEME_COLOR["header_bg"], 
                          fg=THEME_COLOR["header_fg"])
    title_label.pack(pady=15)
    
    # 创建主框架
    main_frame = ttk.Frame(self.root)
    main_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=15)
    
    # 创建按钮框架
    button_frame = ttk.Frame(main_frame)
    button_frame.pack(side=tk.TOP, fill=tk.X, pady=10)
    
    # 添加按钮,增加间距使文字更清晰
    ttk.Button(button_frame, text="生成随机数据", command=self.generate_data).pack(side=tk.LEFT, padx=10, pady=8)
    ttk.Button(button_frame, text="手动输入数据", command=self.input_data).pack(side=tk.LEFT, padx=10, pady=8)
    ttk.Button(button_frame, text="清空数据", command=self.clear_data).pack(side=tk.LEFT, padx=10, pady=8)
    ttk.Button(button_frame, text="执行拟合", command=self.fit_data).pack(side=tk.LEFT, padx=10, pady=8)
    
    # 创建拟合阶数选择框架
    fit_frame = ttk.Frame(main_frame)
    fit_frame.pack(fill=tk.X, pady=5)
    
    # 添加拟合阶数选择标签
    ttk.Label(fit_frame, text="拟合阶数:", 
             font=("微软雅黑", 11, "bold")).pack(side=tk.LEFT, padx=5)
    
    # 创建拟合阶数下拉框
    self.degree_var = tk.StringVar()
    self.degree_var.set("1")  # 默认为一阶(线性)拟合
    degree_options = ["1", "2", "3", "4", "5"]
    degree_menu = ttk.Combobox(fit_frame, 
                              textvariable=self.degree_var,
                              values=degree_options,
                              width=5,
                              font=("微软雅黑", 10))
    degree_menu.pack(side=tk.LEFT, padx=5)
    
    # 创建信息框架
    info_frame = ttk.Frame(main_frame)
    info_frame.pack(fill=tk.X, pady=8)
    
    self.info_label = tk.Label(info_frame, 
                              text="当前数据点: 0", 
                              font=self.info_font,
                              bg=THEME_COLOR["background"],
                              fg=THEME_COLOR["text_color"])
    self.info_label.pack(side=tk.LEFT)
    
    self.formula_label = tk.Label(info_frame,
                                text="", 
                                font=self.formula_font,
                                bg=THEME_COLOR["background"],
                                fg=THEME_COLOR["line_color"])
    self.formula_label.pack(side=tk.RIGHT)
    
    # 创建画布框架(带边框效果)
    canvas_container = ttk.Frame(main_frame, style="TFrame")
    canvas_container.pack(fill=tk.BOTH, expand=True, pady=5)
    
    # 创建画布
    self.canvas = tk.Canvas(canvas_container, 
                           bg=THEME_COLOR["canvas_bg"],
                           highlightthickness=1,
                           highlightbackground="#cccccc")
    self.canvas.pack(fill=tk.BOTH, expand=True)
    
    # 创建状态栏
    status_frame = tk.Frame(self.root, bg="#e0e0e0", height=25)
    status_frame.pack(side=tk.BOTTOM, fill=tk.X)
    
    status_label = tk.Label(status_frame, 
                           text="准备就绪", 
                           bg="#e0e0e0", 
                           fg="#555555",
                           anchor=tk.W)
    status_label.pack(side=tk.LEFT, padx=10)

def generate_data(self, show_message=True):
    """生成随机数据"""
    self.x_data = []
    self.y_data = []
    
    # 设置随机种子,确保每次生成不同的随机数据
    random.seed()
    
    # 随机决定数据点数量(8-15个点)
    num_points = random.randint(8, 15)
    
    for i in range(num_points):
        # 生成随机x值(0-10之间)
        x = random.uniform(0, 10)
        # 使用更多随机性来生成数据
        y = 2 * x + 3 + random.uniform(-2, 2)
        self.x_data.append(x)
        self.y_data.append(y)
    
    # 按x值排序,便于画图
    points = sorted(zip(self.x_data, self.y_data))
    self.x_data = [p[0] for p in points]
    self.y_data = [p[1] for p in points]
    
    self.plot_data()
    self.update_info_label()
    self.formula_label.config(text="")
    
    if show_message:
        messagebox.showinfo("成功", "已生成{}个新的随机数据点".format(num_points))

def input_data(self):
    """手动输入多组数据"""
    # 创建自定义对话框,使文字更清晰
    dialog = tk.Toplevel(self.root)
    dialog.title("输入数据")
    dialog.geometry("400x200")
    dialog.resizable(False, False)
    dialog.transient(self.root)
    dialog.grab_set()
    
    # 说明标签
    tk.Label(dialog, 
            text="请输入数据点,格式为:x1,y1;x2,y2;x3,y3...", 
            font=("微软雅黑", 12)).pack(pady=10)
    tk.Label(dialog, 
            text="例如:1,2;3,4;5,6", 
            font=("微软雅黑", 11)).pack(pady=5)
    
    # 输入框
    entry = tk.Entry(dialog, font=("微软雅黑", 12), width=35)
    entry.pack(pady=10, padx=20)
    entry.focus_set()
    
    # 结果变量
    result = [None]
    
    # 确定和取消按钮
    def on_ok():
        result[0] = entry.get()
        dialog.destroy()
        
    def on_cancel():
        dialog.destroy()
        
    button_frame = tk.Frame(dialog)
    button_frame.pack(pady=15)
    
    tk.Button(button_frame, 
             text="确定", 
             font=("微软雅黑", 11, "bold"),
             fg="#ff0000",  # 红色文字
             command=on_ok).pack(side=tk.LEFT, padx=20)
    tk.Button(button_frame, 
             text="取消", 
             font=("微软雅黑", 11),
             command=on_cancel).pack(side=tk.LEFT)
    
    # 等待对话框关闭
    self.root.wait_window(dialog)
    
    # 处理输入数据
    data_input = result[0]
    if not data_input:
        return
    
    try:
        # 清空现有数据
        self.x_data = []
        self.y_data = []
        
        # 解析输入数据
        point_pairs = data_input.strip().split(';')
        for pair in point_pairs:
            if not pair.strip():
                continue
            x_str, y_str = pair.split(',')
            x = float(x_str.strip())
            y = float(y_str.strip())
            self.x_data.append(x)
            self.y_data.append(y)
        
        # 按x值排序
        points = sorted(zip(self.x_data, self.y_data))
        self.x_data = [p[0] for p in points]
        self.y_data = [p[1] for p in points]
        
        self.plot_data()
        self.update_info_label()
        self.formula_label.config(text="")
        
        messagebox.showinfo("成功", "已输入{}个数据点".format(len(self.x_data)))
    except Exception as e:
        messagebox.showerror("错误", "数据格式错误: {}\n请使用正确格式: x1,y1;x2,y2;...".format(str(e)))

def clear_data(self):
    """清空所有数据"""
    self.x_data = []
    self.y_data = []
    self.plot_data()
    self.update_info_label()
    self.formula_label.config(text="")
    messagebox.showinfo("成功", "已清空所有数据")

def update_info_label(self):
    """更新信息标签"""
    self.info_label.config(text="当前数据点: {}".format(len(self.x_data)))

def fit_data(self):
    """进行多项式拟合"""
    if not self.x_data or not self.y_data:
        messagebox.showerror("错误", "没有数据可拟合")
        return
    
    try:
        # 获取用户选择的拟合阶数
        degree = int(self.degree_var.get())
        
        # 使用纯Python进行多项式拟合
        coeffs = self.polyfit(self.x_data, self.y_data, degree)
        
        # 绘制拟合曲线
        self.plot_data()
        
        # 获取画布尺寸
        width = self.canvas.winfo_width()
        height = self.canvas.winfo_height()
        
        # 计算数据范围
        x_min, x_max = min(self.x_data), max(self.x_data)
        
        # 生成拟合曲线的点
        x_fit = []
        y_fit = []
        num_points = 100
        for i in range(num_points):
            x = x_min + (x_max - x_min) * i / (num_points - 1)
            y = self.polyval(coeffs, x)
            x_fit.append(x)
            y_fit.append(y)
        
        # 绘制拟合曲线
        points = []
        for x, y in zip(x_fit, y_fit):
            cx, cy = self.data_to_canvas(x, y, width, height)
            points.append(cx)
            points.append(cy)
        
        if len(points) >= 4:  # 至少需要两个点才能画线
            self.canvas.create_line(points, fill=THEME_COLOR["line_color"], width=2, smooth=True, tags="fit_line")
        
        # 构建拟合公式字符串
        formula = self.get_formula_string(coeffs)
        
        # 显示拟合公式
        self.formula_label.config(text=formula)
        
        # 在画布上也显示公式
        self.canvas.create_text(width / 2, 30, 
                               text=formula, 
                               font=self.formula_font, 
                               fill=THEME_COLOR["line_color"], 
                               tags="formula")
        
        messagebox.showinfo("成功", "拟合完成: " + formula)
        
    except Exception as e:
        messagebox.showerror("错误", "拟合过程中出现错误: {}".format(str(e)))

def polyfit(self, x_data, y_data, degree):
    """使用最小二乘法进行多项式拟合(不使用numpy)"""
    # 对于一阶拟合(线性拟合),使用简单的线性回归
    if degree == 1:
        n = len(x_data)
        sum_x = sum(x_data)
        sum_y = sum(y_data)
        sum_xx = sum(x*x for x in x_data)
        sum_xy = sum(x*y for x, y in zip(x_data, y_data))
        
        # 计算斜率和截距
        a = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x)
        b = (sum_y - a * sum_x) / n
        
        return [a, b]  # 返回系数 [a, b] 对应 ax + b
    
    # 对于高阶拟合,使用矩阵方法
    else:
        # 构建范德蒙德矩阵
        X = []
        for x in x_data:
            row = [x ** p for p in range(degree, -1, -1)]
            X.append(row)
        
        # 转置矩阵X
        X_T = [[X[j][i] for j in range(len(X))] for i in range(len(X[0]))]
        
        # 计算X^T * X
        X_T_X = []
        for i in range(len(X_T)):
            row = []
            for j in range(len(X_T)):
                val = sum(X_T[i][k] * X_T[j][k] for k in range(len(X_T[0])))
                row.append(val)
            X_T_X.append(row)
        
        # 计算X^T * y
        X_T_y = []
        for i in range(len(X_T)):
            val = sum(X_T[i][j] * y_data[j] for j in range(len(y_data)))
            X_T_y.append(val)
        
        # 求解线性方程组 X^T * X * coeffs = X^T * y
        coeffs = self.solve_linear_system(X_T_X, X_T_y)
        
        return coeffs

def solve_linear_system(self, A, b):
    """使用高斯消元法求解线性方程组"""
    n = len(A)
    # 创建增广矩阵
    augmented = [row[:] + [b[i]] for i, row in enumerate(A)]
    
    # 前向消元
    for i in range(n):
        # 寻找主元
        max_row = i
        for j in range(i + 1, n):
            if abs(augmented[j][i]) > abs(augmented[max_row][i]):
                max_row = j
        
        # 交换行
        augmented[i], augmented[max_row] = augmented[max_row], augmented[i]
        
        # 消元
        for j in range(i + 1, n):
            factor = augmented[j][i] / augmented[i][i]
            for k in range(i, n + 1):
                augmented[j][k] -= factor * augmented[i][k]
    
    # 回代求解
    x = [0] * n
    for i in range(n - 1, -1, -1):
        x[i] = augmented[i][n]
        for j in range(i + 1, n):
            x[i] -= augmented[i][j] * x[j]
        x[i] /= augmented[i][i]
    
    return x

def polyval(self, coeffs, x):
    """计算多项式在x点的值(不使用numpy)"""
    result = 0
    for i, coeff in enumerate(coeffs):
        power = len(coeffs) - 1 - i
        result += coeff * (x ** power)
    return result

def get_formula_string(self, coeffs):
    """根据系数生成公式字符串"""
    degree = len(coeffs) - 1
    terms = []
    
    for i, coeff in enumerate(coeffs):
        power = degree - i
        
        if power == 0:
            # 常数项
            terms.append("{:.2f}".format(coeff))
        elif power == 1:
            # 一次项
            terms.append("{:.2f}x".format(coeff))
        else:
            # 高次项
            terms.append("{:.2f}x^{}".format(coeff, power))
    
    # 组合所有项
    formula = "y = " + " + ".join(terms)
    
    # 替换"+ -"为"-"
    formula = formula.replace("+ -", "- ")
    
    return formula

def plot_data(self):
    """绘制数据点"""
    self.canvas.delete("all")
    
    if not self.x_data or not self.y_data:
        return
    
    # 获取画布尺寸
    width = self.canvas.winfo_width()
    height = self.canvas.winfo_height()
    
    # 计算数据范围
    x_min, x_max = min(self.x_data), max(self.x_data)
    y_min, y_max = min(self.y_data), max(self.y_data)
    
    # 添加边距
    margin = (y_max - y_min) * 0.1
    y_min -= margin
    y_max += margin
    
    x_margin = (x_max - x_min) * 0.1
    x_min -= x_margin
    x_max += x_margin
    
    # 计算规则的刻度间隔
    def calculate_nice_interval(min_val, max_val, target_steps=5):
        range_val = max_val - min_val
        if range_val == 0:
            return 1  # 防止除零错误
        
        # 计算粗略的步长
        rough_step = range_val / target_steps
        
        # 寻找合适的步长(1, 2, 5, 10, 20, 50, ...的倍数)
        magnitude = 10 ** math.floor(math.log10(rough_step))
        possible_steps = [magnitude, 2*magnitude, 5*magnitude, 10*magnitude]
        
        # 选择最接近目标步数的步长
        step = min(possible_steps, key=lambda x: abs(range_val/x - target_steps))
        
        return step
    
    # 计算规则的X轴和Y轴刻度
    x_step = calculate_nice_interval(x_min, x_max)
    y_step = calculate_nice_interval(y_min, y_max)
    
    # 计算起始刻度(向下取整到最接近的刻度倍数)
    x_start = math.floor(x_min / x_step) * x_step
    y_start = math.floor(y_min / y_step) * y_step
    
    # 画坐标轴
    self.canvas.create_line(50, height - 50, width - 50, height - 50, 
                          arrow=tk.LAST, width=3,  # 增加线宽
                          fill=THEME_COLOR["axis_color"])  # X轴
    self.canvas.create_line(50, height - 50, 50, 50, 
                          arrow=tk.LAST, width=3,  # 增加线宽
                          fill=THEME_COLOR["axis_color"])  # Y轴
    
    # 添加坐标轴标签
    self.canvas.create_text(width - 35, height - 30, 
                          text="X", 
                          font=("微软雅黑", 12, "bold"),  # 增大字体
                          fill=THEME_COLOR["text_color"])
    self.canvas.create_text(30, 50, 
                          text="Y", 
                          font=("微软雅黑", 12, "bold"),  # 增大字体
                          fill=THEME_COLOR["text_color"])
    
    # 添加X轴刻度和数字标识(使用规则间隔)
    x = x_start
    while x <= x_max:
        if x >= x_min:  # 只显示范围内的刻度
            # 计算画布坐标
            x_pos = 50 + (x - x_min) / (x_max - x_min) * (width - 100)
            # 绘制刻度线
            self.canvas.create_line(x_pos, height - 50, x_pos, height - 45, 
                                  width=1, fill=THEME_COLOR["axis_color"])
            # 绘制数字标识(根据数值大小选择格式)
            if abs(x) < 0.1 or abs(x) >= 100:
                format_str = "{:.1e}"  # 科学计数法
            elif abs(x) >= 10:
                format_str = "{:.0f}"  # 整数
            else:
                format_str = "{:.1f}"  # 一位小数
            
            self.canvas.create_text(x_pos, height - 30, 
                                  text=format_str.format(x), 
                                  font=self.axis_font, 
                                  fill=THEME_COLOR["axis_label"])
        x += x_step
    
    # 添加Y轴刻度和数字标识(使用规则间隔)
    y = y_start
    while y <= y_max:
        if y >= y_min:  # 只显示范围内的刻度
            # 计算画布坐标
            y_pos = height - 50 - (y - y_min) / (y_max - y_min) * (height - 100)
            # 绘制刻度线
            self.canvas.create_line(50, y_pos, 45, y_pos, 
                                  width=1, fill=THEME_COLOR["axis_color"])
            # 绘制数字标识(根据数值大小选择格式)
            if abs(y) < 0.1 or abs(y) >= 100:
                format_str = "{:.1e}"  # 科学计数法
            elif abs(y) >= 10:
                format_str = "{:.0f}"  # 整数
            else:
                format_str = "{:.1f}"  # 一位小数
                
            self.canvas.create_text(30, y_pos, 
                                  text=format_str.format(y), 
                                  font=self.axis_font, 
                                  fill=THEME_COLOR["axis_label"])
        y += y_step
    
    # 绘制网格线(淡色)
    self.draw_grid(width, height, x_min, x_max, y_min, y_max, x_step, y_step)
    
    # 绘制数据点
    for x, y in zip(self.x_data, self.y_data):
        cx, cy = self.data_to_canvas(x, y, width, height)
        # 绘制点外圈
        self.canvas.create_oval(cx-5, cy-5, cx+5, cy+5, 
                              outline=THEME_COLOR["point_color"], 
                              width=2)
        # 绘制点内圈
        self.canvas.create_oval(cx-3, cy-3, cx+3, cy+3, 
                              fill=THEME_COLOR["point_color"], 
                              outline="")

def draw_grid(self, width, height, x_min, x_max, y_min, y_max, x_step, y_step):
    """绘制网格线"""
    # 绘制基于刻度的水平网格线
    y = y_step * math.ceil(y_min / y_step)
    while y <= y_max:
        y_pos = height - 50 - (y - y_min) / (y_max - y_min) * (height - 100)
        self.canvas.create_line(50, y_pos, width - 50, y_pos, 
                              fill=THEME_COLOR["grid_color"], 
                              dash=(2, 4))
        y += y_step
    
    # 绘制基于刻度的垂直网格线
    x = x_step * math.ceil(x_min / x_step)
    while x <= x_max:
        x_pos = 50 + (x - x_min) / (x_max - x_min) * (width - 100)
        self.canvas.create_line(x_pos, height - 50, x_pos, 50, 
                              fill=THEME_COLOR["grid_color"], 
                              dash=(2, 4))
        x += x_step

def data_to_canvas(self, x, y, width, height):
    """将数据坐标转换为画布坐标"""
    if not self.x_data or not self.y_data:
        return 50, height - 50
    
    x_min, x_max = min(self.x_data), max(self.x_data)
    y_min, y_max = min(self.y_data), max(self.y_data)
    
    # 添加边距
    margin = (y_max - y_min) * 0.1
    y_min -= margin
    y_max += margin
    
    x_margin = (x_max - x_min) * 0.1
    x_min -= x_margin
    x_max += x_margin
    
    # 转换坐标
    canvas_x = 50 + (x - x_min) / (x_max - x_min) * (width - 100)
    canvas_y = height - 50 - (y - y_min) / (y_max - y_min) * (height - 100)
    
    return canvas_x, canvas_y

def main():
root = tk.Tk()
app = EnhancedApp(root)
root.mainloop()

if name == "main":
main()


posted @ 2025-06-07 20:16  曹明阳  阅读(36)  评论(0)    收藏  举报