一、模型表现:
前端界面和效果:
前端代码(templates/index.html):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>eBay商品预测分析</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.6.0/css/bootstrap.min.css" rel="stylesheet">
<style>
.container { max-width: 600px; margin: 2rem auto; }
.alert-box { transition: opacity 0.3s; }
.result-card { display: none; margin-top: 1.5rem; }
.loading-overlay {
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(255,255,255,0.9);
display: none;
z-index: 999;
}
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.input-error { border-color: #dc3545 !important; }
.error-msg { color: #dc3545; font-size: 0.875em; }
</style>
</head>
<body>
<div class="container">
<h2 class="text-center mb-4">eBay商品预测系统</h2>
<!-- 错误提示 -->
<div class="alert alert-danger alert-box" id="errorAlert" style="display: none;"></div>
<!-- 预测表单 -->
<form id="predictForm">
<div class="form-group">
<label>商品价格 ($)</label>
<input type="text" class="form-control" id="price"
placeholder="例如:19.99 或 15.99-29.99" required>
</div>
<div class="form-group">
<label>卖家评分 (%)</label>
<input type="text" class="form-control" id="seller_rating"
placeholder="例如:95%"
pattern="^\d+%$"
required>
<small class="error-msg" id="ratingError"></small>
</div>
<div class="form-group">
<label>卖家反馈数</label>
<input type="number" class="form-control" id="feedbackCount"
placeholder="例如:1500"
min="0"
required>
<small class="error-msg" id="feedbackError"></small>
</div>
<div class="form-group">
<label>库存数量</label>
<input type="text" class="form-control" id="availability"
placeholder="例如:10件库存 或 5 in stock" required>
</div>
<div class="form-group">
<label>商品标题</label>
<input type="text" class="form-control" id="title"
placeholder="输入商品标题(至少3个字符)" required minlength="3">
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>开始日期</label>
<input type="date" class="form-control" id="start" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>结束日期</label>
<input type="date" class="form-control" id="end" required>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">
立即预测
</button>
</form>
<!-- 预测结果 -->
<div class="card result-card" id="resultCard">
<div class="card-body">
<h4 class="card-title">预测结果</h4>
<hr>
<p class="card-text">
预计关注人数:<span class="text-primary" id="predWatchers">--</span>
</p>
<p class="card-text">
预计销售量:<span class="text-success" id="predSold">--</span>
</p>
</div>
</div>
<!-- 加载动画 -->
<div class="loading-overlay">
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;">
<span class="sr-only">加载中...</span>
</div>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>
$(document).ready(function() {
const $form = $('#predictForm');
const $result = $('#resultCard');
const $error = $('#errorAlert');
const $loading = $('.loading-overlay');
// 实时验证卖家评分格式
$('#seller_rating').on('input', function() {
const isValid = /^\d+%$/.test($(this).val());
$(this).toggleClass('input-error', !isValid);
$('#ratingError').text(isValid ? "" : "必须为数字加百分号(如95%)");
});
// 实时验证反馈数格式
$('#feedbackCount').on('input', function() {
const val = $(this).val();
const isValid = val !== "" && parseInt(val) >= 0;
$(this).toggleClass('input-error', !isValid);
$('#feedbackError').text(isValid ? "" : "必须为非负整数");
});
// 表单提交处理
$form.on('submit', async function(e) {
e.preventDefault();
$error.hide();
$loading.show();
$result.hide();
// 提交前验证
let hasError = false;
$('.input-error').removeClass('input-error');
$('.error-msg').text('');
// 卖家评分验证
const sellerRating = $('#seller_rating').val().trim();
if (!/^\d+%$/.test(sellerRating)) {
$('#seller_rating').addClass('input-error');
$('#ratingError').text("必须为数字加百分号(如95%)");
hasError = true;
}
// 反馈数验证
const feedbackVal = $('#feedbackCount').val().trim();
if (feedbackVal === "" || parseInt(feedbackVal) < 0) {
$('#feedbackCount').addClass('input-error');
$('#feedbackError').text("必须为非负整数");
hasError = true;
}
if (hasError) {
$loading.hide();
return;
}
try {
// 组装符合后端要求的JSON结构
const formData = {
price: $('#price').val().trim(),
seller: {
rating: sellerRating,
feedbackCount: feedbackVal
},
availability: $('#availability').val().trim(),
title: $('#title').val().trim(),
start: $('#start').val(),
end: $('#end').val()
};
// 发送请求
const response = await axios.post('/predict', formData, {
timeout: 5000
});
// 显示结果
$('#predWatchers').text(response.data.predicted_watchers);
$('#predSold').text(response.data.predicted_sold);
$result.show();
} catch (error) {
let message = '请求失败,请检查输入格式';
if (error.response) {
message = error.response.data.error || message;
}
$error.text(message).show();
} finally {
$loading.hide();
}
});
});
</script>
</body>
</html>
后端服务器代码(app.py):
import os
import json
import logging
import re
import numpy as np
import pandas as pd
from flask import Flask, request, jsonify, render_template
import joblib
import tensorflow as tf
from tensorflow import keras
# 使用时保持一致
load_model = keras.models.load_model
from keras.preprocessing.sequence import pad_sequences
from keras.preprocessing.text import tokenizer_from_json
from sklearn.exceptions import NotFittedError
from datetime import datetime
from sklearn.impute import SimpleImputer
app = Flask(__name__, static_folder='static', template_folder='templates')
app.logger.setLevel(logging.INFO)
# ==================== 增强版数据清洗工具 ====================
class RobustDataCleaner:
"""鲁棒性数据清洗工具"""
@staticmethod
def clean_price(price):
"""价格清洗(支持多种格式)"""
try:
if pd.isna(price):
return 0.0
if isinstance(price, str):
price = price.replace("$", "").replace(",", "").strip()
if "-" in price:
parts = [float(p) for p in re.findall(r"\d+\.?\d*", price)[:2]]
return sum(parts) / len(parts)
return float(price)
return float(price)
except:
return 0.0
@staticmethod
def process_seller(seller):
"""卖家信息解析(增强容错)"""
default = {"rating": 0.0, "feedbackCount": 0}
try:
if not isinstance(seller, dict):
return default
return {
"rating": float(str(seller.get("rating", "0%")).replace("%", "")) / 100,
"feedbackCount": int(seller.get("feedbackCount", 0))
}
except:
return default
# ==================== 模型服务初始化 ====================
MODEL_DIR = "C:/Users/hanbi/Downloads/model" #模型放置位置
# 检查模型目录是否存在
if not os.path.exists(MODEL_DIR):
raise FileNotFoundError(f"模型目录不存在: {MODEL_DIR}")
MODEL_PATHS = {
"lstm": os.path.join(MODEL_DIR, "optimized_lstm.h5"),
"xgb": os.path.join(MODEL_DIR, "optimized_xgb.pkl"),
"rf": os.path.join(MODEL_DIR, "optimized_rf.pkl"),
"meta": os.path.join(MODEL_DIR, "meta_model.pkl"),
"preprocessor": os.path.join(MODEL_DIR, "preprocessor.pkl"),
"tokenizer": os.path.join(MODEL_DIR, "tokenizer_config.json")
}
def initialize_models():
"""安全初始化所有模型组件"""
models = {}
try:
# 加载预处理组件
models['preprocessor'] = joblib.load(MODEL_PATHS['preprocessor'])
# 加载文本处理组件
with open(MODEL_PATHS['tokenizer'], 'r', encoding='utf-8') as f:
models['tokenizer'] = tokenizer_from_json(json.load(f))
# 加载预测模型
models.update({
'lstm': load_model(MODEL_PATHS['lstm']),
'xgb': joblib.load(MODEL_PATHS['xgb']),
'rf': joblib.load(MODEL_PATHS['rf']),
'meta': joblib.load(MODEL_PATHS['meta'])
})
return models
except Exception as e:
app.logger.error(f"模型加载失败: {str(e)}", exc_info=True)
raise
models = initialize_models()
# ==================== 数据处理管道 ====================
def process_input(data: dict) -> tuple:
"""鲁棒的输入处理管道"""
try:
# 数值特征处理
cleaned = {
'price': RobustDataCleaner.clean_price(data.get('price')),
'seller_rating': RobustDataCleaner.process_seller(data.get('seller', {})).get('rating', 0.0),
'feedbackCount': RobustDataCleaner.process_seller(data.get('seller', {})).get('feedbackCount', 0),
'stock': int(re.search(r'\d+', str(data.get('availability', '0'))).group()),
'active_days': max(0, (pd.to_datetime(data.get('end'), errors='coerce') -
pd.to_datetime(data.get('start'), errors='coerce')).days),
'title_length': len(str(data.get('title', '')))
}
# 构建DataFrame
numeric_df = pd.DataFrame([cleaned], columns=models['preprocessor'].feature_names_in_)
# 文本特征处理
sequences = models['tokenizer'].texts_to_sequences([data.get('title', '')])
text_features = pad_sequences(sequences, maxlen=30, padding='post')
# 数值特征标准化
numeric_scaled = models['preprocessor'].transform(numeric_df)
return numeric_scaled, text_features
except Exception as e:
app.logger.error(f"输入处理失败: {str(e)}")
raise ValueError("输入数据格式错误")
# ==================== API端点 ====================
@app.route('/')
def index():
return render_template('index.html')
@app.route('/predict', methods=['POST'])
def predict():
try:
data = request.get_json()
required_fields = ['price', 'seller', 'availability', 'title', 'start', 'end']
if missing := [f for f in required_fields if f not in data]:
return jsonify({"error": f"缺少必填字段: {', '.join(missing)}"}), 400
# 特征处理
X_num, X_text = process_input(data)
# 基模型预测
xgb_pred = models['xgb'].predict(X_num)
rf_pred = models['rf'].predict(X_num)
lstm_preds = models['lstm'].predict(X_text)
# 元特征构建
meta_features = np.column_stack([
xgb_pred.flatten(),
rf_pred.flatten(),
lstm_preds[0].flatten(),
lstm_preds[1].flatten()
])
# 最终预测
final_pred = models['meta'].predict(meta_features)
return jsonify({
"predicted_watchers": round(float(final_pred[0][0])),
"predicted_sold": round(float(final_pred[0][1]))
})
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
app.logger.error(f"预测失败: {str(e)}")
return jsonify({"error": "内部服务器错误"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
记得修改模型加载路径!
模型: