【免费】基于Spark实时医疗健康数据监测与疾病预测系统(Python版本+pyspark+可视化大屏+Kafka+FastAPI+Vue3) 锋哥原创出品,必属精品

大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Spark实时医疗健康数据监测与疾病预测系统(Python版本+pyspark+可视化大屏+Kafka+FastAPI+Vue3) 

项目介绍

随着可穿戴设备与智慧医疗的快速发展,医疗健康数据呈现高并发、连续产生和强时效性等特征。传统以离线批处理为主的健康管理系统难以满足实时监测、即时预警和风险趋势预测的应用需求。针对上述问题,本文设计并实现了一套基于 Spark 的实时医疗健康数据监测与疾病预测系统。系统采用前后端分离架构:前端基于 Vue3、Element Plus 与 ECharts 构建管理端与数据大屏;后端基于 Python 与 FastAPI 提供 RESTful 接口与 JWT 身份认证;实时链路采用 Kafka 承载体征事件流,使用 Spark Streaming 完成窗口聚合统计,并基于 Spark ML 线性回归对健康风险指数进行预测,同时计算 RMSE、MAE、MAPE 等误差指标。业务数据统一持久化到 MySQL 数据库 db_health 中,涵盖管理员、患者、疾病类型、体征监测、实时统计、预测结果与误差指标等核心表。系统实现了管理员登录与个人中心、首页统计看板、患者档案管理、监测数据查询、实时统计展示、风险预测分析以及可视化大屏等功能。测试结果表明,系统能够稳定完成实时数据采集、流式计算、风险预警与预测展示,具有较好的完整性、可扩展性和工程实践价值,可为智慧医疗场景下的实时健康监测提供参考方案。

源码下载 

链接: https://pan.baidu.com/s/1LnfwLSRd8TZXFkivm35zgw?pwd=1234
提取码: 1234

系统展示

核心代码

"""
预测分析路由
"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session

from database import get_db
from models.prediction import Prediction
from models.error_metric import ErrorMetric
from schemas import ResponseModel, PageResult, PredictionOut, ErrorMetricOut

router = APIRouter(prefix="/prediction", tags=["预测分析"])


def _normalize_window_time(wt: str) -> str:
    """将窗口时间规范为完整日期时间"""
    if not wt:
        return ""
    text = str(wt).strip()
    if len(text) == 13 and text[10] == " ":
        return f"{text}:00:00"
    if len(text) == 16 and text[13] == ":":
        return f"{text}:00"
    return text


def _to_prediction_out(row: Prediction) -> PredictionOut:
    """将 ORM 行转为输出模型"""
    item = PredictionOut.model_validate(row)
    item.window_time = _normalize_window_time(item.window_time)
    return item


@router.get("/list", response_model=ResponseModel[PageResult[PredictionOut]])
def list_predictions(
    page: int = Query(1, ge=1),
    size: int = Query(10, ge=1, le=100),
    db: Session = Depends(get_db),
):
    """
    分页查询预测结果
    """
    total = db.query(Prediction).count()
    rows = (
        db.query(Prediction)
        .order_by(Prediction.window_time.asc())
        .offset((page - 1) * size)
        .limit(size)
        .all()
    )
    items = [_to_prediction_out(r) for r in rows]
    return ResponseModel(data=PageResult(total=total, items=items))


@router.get("/compare", response_model=ResponseModel[list[PredictionOut]])
def get_compare_chart(db: Session = Depends(get_db)):
    """
    获取真实值 vs 预测值对比数据
    """
    rows = (
        db.query(Prediction)
        .order_by(Prediction.window_time.asc())
        .all()
    )
    return ResponseModel(data=[_to_prediction_out(r) for r in rows])


@router.get("/error", response_model=ResponseModel[ErrorMetricOut])
def get_error_metric(db: Session = Depends(get_db)):
    """
    获取最新误差指标
    """
    metric = (
        db.query(ErrorMetric)
        .order_by(ErrorMetric.create_time.desc())
        .first()
    )
    if not metric:
        return ResponseModel(data=ErrorMetricOut(id=0, rmse=0, mae=0, mape=0))
    return ResponseModel(data=ErrorMetricOut.model_validate(metric))


@router.post("/refresh", response_model=ResponseModel)
def refresh_prediction(db: Session = Depends(get_db)):
    """
    手动触发预测刷新
    """
    from spark.predict import run_spark_prediction, save_predictions_to_db
    from spark.fallback import predict_with_sklearn
    from models.realtime_stat import RealtimeStat

    rows = db.query(RealtimeStat).order_by(RealtimeStat.window_time.asc()).all()
    risk_series = [
        {"window_time": r.window_time, "avg_risk_score": float(r.avg_risk_score or 0)}
        for r in rows
    ]

    predictions, error = run_spark_prediction(risk_series)
    if predictions is None:
        predictions, error = predict_with_sklearn(risk_series)

    if predictions:
        save_predictions_to_db(predictions, error)
        return ResponseModel(message=f"预测刷新完成,共 {len(predictions)} 条")
    return ResponseModel(message="数据不足,无法预测")
<template>
  <div class="page-container">
    <div class="page-card">
      <div class="page-title">健康风险指数预测分析</div>
      <div style="margin-bottom:16px">
        <el-button type="primary" @click="refreshPrediction">刷新预测</el-button>
      </div>
      <div class="error-cards">
        <div class="error-card">
          <div class="metric-label">RMSE (均方根误差)</div>
          <div class="metric-value">{{ errorMetric.rmse }}</div>
        </div>
        <div class="error-card">
          <div class="metric-label">MAE (平均绝对误差)</div>
          <div class="metric-value">{{ errorMetric.mae }}</div>
        </div>
        <div class="error-card">
          <div class="metric-label">MAPE (平均绝对百分比误差 %)</div>
          <div class="metric-value">{{ errorMetric.mape }}%</div>
        </div>
      </div>
      <div ref="compareRef" class="pred-chart pred-chart-compare"></div>
      <div ref="residualRef" class="pred-chart pred-chart-residual"></div>
      <el-table :data="tableData" stripe border style="width:100%">
        <el-table-column prop="window_time" label="时间窗口" min-width="170">
          <template #default="{ row }">{{ formatWindowTime(row.window_time) }}</template>
        </el-table-column>
        <el-table-column prop="true_risk" label="真实风险指数" min-width="130">
          <template #default="{ row }">
            <span style="color:#13a8a8;font-weight:600">{{ row.true_risk }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="pred_risk" label="预测风险指数" min-width="130">
          <template #default="{ row }">
            <span style="color:#52c41a;font-weight:600">{{ row.pred_risk }}</span>
          </template>
        </el-table-column>
        <el-table-column label="误差" min-width="120">
          <template #default="{ row }">
            <span :style="{ color: Math.abs(row.true_risk - row.pred_risk) > 5 ? '#ff4d4f' : '#909399' }">
              {{ (row.true_risk - row.pred_risk).toFixed(2) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="create_time" label="生成时间" min-width="170">
          <template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
        </el-table-column>
      </el-table>
      <el-pagination
        style="margin-top:16px;justify-content:flex-end"
        v-model:current-page="page"
        v-model:page-size="size"
        :total="total"
        layout="total, prev, pager, next"
        @change="loadTable"
      />
    </div>
  </div>
</template>

<script setup>
/**
 * 预测分析页面:真实风险 vs 预测风险对比图 + 误差分析
 */
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import request from '@/utils/request'
import { formatDateTime, formatWindowTime } from '@/utils/format'
import { ElMessage } from 'element-plus'

const errorMetric = ref({ rmse: 0, mae: 0, mape: 0 })
const tableData = ref([])
const page = ref(1)
const size = ref(10)
const total = ref(0)
const compareRef = ref(null)
const residualRef = ref(null)
let charts = []

function buildAxisLabel() {
  return {
    rotate: 30,
    interval: 'auto',
    hideOverlap: true,
    fontSize: 11,
    margin: 16,
    formatter(val) {
      const text = formatWindowTime(val)
      if (text.length >= 16) return `${text.slice(0, 10)}\n${text.slice(11)}`
      return text
    },
  }
}

function initCompareChart(data) {
  const chart = echarts.init(compareRef.value)
  const labels = data.map(d => formatWindowTime(d.window_time))
  chart.setOption({
    title: { text: '真实风险指数 vs 预测风险指数 对比', left: 'center', textStyle: { fontSize: 15 } },
    tooltip: { trigger: 'axis' },
    legend: { data: ['真实风险指数', '预测风险指数'], top: 32 },
    xAxis: { type: 'category', data: labels, axisLabel: buildAxisLabel() },
    yAxis: { type: 'value', name: '风险指数' },
    series: [
      {
        name: '真实风险指数',
        type: 'line',
        smooth: true,
        data: data.map(d => Number(d.true_risk)),
        itemStyle: { color: '#13a8a8' },
        lineStyle: { width: 3 },
        symbol: 'circle',
        symbolSize: 8,
      },
      {
        name: '预测风险指数',
        type: 'line',
        smooth: true,
        data: data.map(d => Number(d.pred_risk)),
        itemStyle: { color: '#52c41a' },
        lineStyle: { width: 3, type: 'dashed' },
        symbol: 'diamond',
        symbolSize: 8,
      },
    ],
    grid: { left: 20, right: 24, bottom: 28, top: 72, containLabel: true },
  })
  charts.push(chart)
}

function initResidualChart(data) {
  const chart = echarts.init(residualRef.value)
  const labels = data.map(d => formatWindowTime(d.window_time))
  const residuals = data.map(d => ({
    residual: Number(d.true_risk) - Number(d.pred_risk),
  }))
  chart.setOption({
    title: { text: '预测残差分析 (真实值 - 预测值)', left: 'center', textStyle: { fontSize: 15 } },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: labels, axisLabel: buildAxisLabel() },
    yAxis: { type: 'value', name: '残差' },
    series: [{
      type: 'bar',
      data: residuals.map(d => ({
        value: d.residual,
        itemStyle: { color: d.residual >= 0 ? '#13a8a8' : '#ff4d4f' },
      })),
      barWidth: 20,
    }],
    grid: { left: 20, right: 24, bottom: 28, top: 56, containLabel: true },
  })
  charts.push(chart)
}

async function loadData() {
  const [errorRes, compareRes] = await Promise.all([
    request.get('/prediction/error'),
    request.get('/prediction/compare'),
  ])
  errorMetric.value = errorRes.data
  charts.forEach(c => c.dispose())
  charts = []
  const compareData = compareRes.data || []
  initCompareChart(compareData)
  initResidualChart(compareData)
}

async function loadTable() {
  const res = await request.get('/prediction/list', { params: { page: page.value, size: size.value } })
  tableData.value = res.data.items
  total.value = res.data.total
}

async function refreshPrediction() {
  await request.post('/prediction/refresh')
  ElMessage.success('预测已刷新')
  loadData()
  loadTable()
}

onMounted(() => { loadData(); loadTable() })
onUnmounted(() => charts.forEach(c => c.dispose()))
</script>

<style scoped>
.pred-chart { width: 100%; margin-bottom: 24px; }
.pred-chart-compare { height: 480px; }
.pred-chart-residual { height: 420px; }
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值