利用历史同赔数据预测的Java案例
在体育博彩/彩票分析领域,"同赔"指的是相同或相近赔率的比赛,通过统计历史中相同赔率组合下比赛的实际结果,可以对新比赛进行概率预测,下面给出一个完整的Java案例。

核心思路
历史数据(赔率 + 结果)
↓
按赔率特征建立索引(主胜/平/客胜赔率)
↓
查询新比赛赔率 → 找历史同赔比赛
↓
统计同赔比赛结果分布 → 输出预测概率
数据模型设计
import java.time.LocalDate;
import java.util.*;
/**
* 一场比赛的历史记录
*/
public class MatchRecord {
private String matchId;
private LocalDate date;
private double homeOdds; // 主胜赔率
private double drawOdds; // 平局赔率
private double awayOdds; // 客胜赔率
private int result; // 3=主胜, 1=平, 0=客胜
public MatchRecord(String matchId, LocalDate date,
double homeOdds, double drawOdds, double awayOdds, int result) {
this.matchId = matchId;
this.date = date;
this.homeOdds = homeOdds;
this.drawOdds = drawOdds;
this.awayOdds = awayOdds;
this.result = result;
}
// getters
public String getMatchId() { return matchId; }
public LocalDate getDate() { return date; }
public double getHomeOdds() { return homeOdds; }
public double getDrawOdds() { return drawOdds; }
public double getAwayOdds() { return awayOdds; }
public int getResult() { return result; }
}
同赔匹配引擎
关键点:赔率不能精确相等(博彩公司小数位多),要用"容差"匹配。
import java.util.*;
import java.util.stream.Collectors;
public class SameOddsPredictor {
private final List<MatchRecord> history;
private final double tolerance; // 容差,默认 0.05
public SameOddsPredictor(List<MatchRecord> history, double tolerance) {
this.history = history;
this.tolerance = tolerance;
}
/**
* 判断两条赔率是否"同赔"
*/
private boolean isSameOdds(MatchRecord a, MatchRecord b) {
return Math.abs(a.getHomeOdds() - b.getHomeOdds()) <= tolerance
&& Math.abs(a.getDrawOdds() - b.getDrawOdds()) <= tolerance
&& Math.abs(a.getAwayOdds() - b.getAwayOdds()) <= tolerance;
}
/**
* 找出与目标比赛同赔的所有历史记录
*/
public List<MatchRecord> findSameOddsMatches(double homeOdds,
double drawOdds,
double awayOdds) {
MatchRecord target = new MatchRecord("target", LocalDate.now(),
homeOdds, drawOdds, awayOdds, -1);
return history.stream()
.filter(r -> isSameOdds(r, target))
.collect(Collectors.toList());
}
/**
* 基于同赔数据做预测
*/
public PredictionResult predict(double homeOdds, double drawOdds, double awayOdds) {
List<MatchRecord> same = findSameOddsMatches(homeOdds, drawOdds, awayOdds);
int total = same.size();
if (total == 0) {
return new PredictionResult(0, 0, 0, 0);
}
long win = same.stream().filter(r -> r.getResult() == 3).count();
long draw = same.stream().filter(r -> r.getResult() == 1).count();
long lose = same.stream().filter(r -> r.getResult() == 0).count();
return new PredictionResult(
total,
win * 1.0 / total,
draw * 1.0 / total,
lose * 1.0 / total
);
}
/**
* 加权预测:近期比赛权重更高(时间衰减)
*/
public PredictionResult predictWithDecay(double homeOdds, double drawOdds,
double awayOdds, double halfLifeDays) {
List<MatchRecord> same = findSameOddsMatches(homeOdds, drawOdds, awayOdds);
if (same.isEmpty()) return new PredictionResult(0, 0, 0, 0);
LocalDate now = LocalDate.now();
double wWin = 0, wDraw = 0, wLose = 0;
for (MatchRecord r : same) {
long days = java.time.temporal.ChronoUnit.DAYS.between(r.getDate(), now);
double weight = Math.pow(0.5, days / halfLifeDays); // 半衰期衰减
switch (r.getResult()) {
case 3 -> wWin += weight;
case 1 -> wDraw += weight;
case 0 -> wLose += weight;
}
}
double sum = wWin + wDraw + wLose;
return new PredictionResult(same.size(),
wWin / sum, wDraw / sum, wLose / sum);
}
}
预测结果封装
public class PredictionResult {
private final int sampleSize;
private final double homeWinProb;
private final double drawProb;
private final double awayWinProb;
public PredictionResult(int sampleSize, double homeWinProb,
double drawProb, double awayWinProb) {
this.sampleSize = sampleSize;
this.homeWinProb = homeWinProb;
this.drawProb = drawProb;
this.awayWinProb = awayWinProb;
}
public int getSampleSize() { return sampleSize; }
public double getHomeWinProb() { return homeWinProb; }
public double getDrawProb() { return drawProb; }
public double getAwayWinProb() { return awayWinProb; }
@Override
public String toString() {
return String.format(
"样本数=%d | 主胜=%.2f%% 平=%.2f%% 客胜=%.2f%%",
sampleSize,
homeWinProb * 100, drawProb * 100, awayWinProb * 100
);
}
}
运行示例
import java.time.LocalDate;
import java.util.*;
public class Main {
public static void main(String[] args) {
// 1. 构造历史数据(实际应从数据库/CSV加载)
List<MatchRecord> history = new ArrayList<>();
history.add(new MatchRecord("M1", LocalDate.now().minusDays(10), 2.10, 3.30, 3.20, 3));
history.add(new MatchRecord("M2", LocalDate.now().minusDays(20), 2.12, 3.28, 3.18, 1));
history.add(new MatchRecord("M3", LocalDate.now().minusDays(30), 2.08, 3.35, 3.25, 3));
history.add(new MatchRecord("M4", LocalDate.now().minusDays(40), 2.15, 3.30, 3.22, 0));
history.add(new MatchRecord("M5", LocalDate.now().minusDays(50), 2.10, 3.32, 3.19, 3));
history.add(new MatchRecord("M6", LocalDate.now().minusDays(60), 1.50, 4.00, 6.00, 3));
history.add(new MatchRecord("M7", LocalDate.now().minusDays(70), 2.09, 3.31, 3.21, 1));
SameOddsPredictor predictor = new SameOddsPredictor(history, 0.05);
// 2. 待预测比赛赔率
double h = 2.11, d = 3.30, a = 3.20;
// 3. 普通同赔统计
PredictionResult p1 = predictor.predict(h, d, a);
System.out.println("【普通同赔】 " + p1);
// 4. 时间衰减加权(半衰期 30 天)
PredictionResult p2 = predictor.predictWithDecay(h, d, a, 30);
System.out.println("【加权同赔】 " + p2);
}
}
输出示例:
【普通同赔】 样本数=5 | 主胜=60.00% 平=20.00% 客胜=20.00%
【加权同赔】 样本数=5 | 主胜=71.35% 平=13.72% 客胜=14.93%
工程化扩展建议
| 场景 | 方案 |
|---|---|
| 数据量大 | 用 Redis / ElasticSearch 建立赔率区间索引,避免每次全表扫描 |
| 多博彩公司 | 取多家公司欧赔均值,或分别统计(不同公司开盘风格不同) |
| 特征扩展 | 加入亚盘、凯利指数、赔率变化(初盘→终盘)等多维特征 |
| 匹配算法 | 欧氏距离 / KD-Tree 近邻,替代三字段容差 |
| 概率校准 | 用 Platt Scaling / Isotonic Regression 校准输出概率 |
| 模型融合 | 同赔概率 + Elo + Poisson 模型加权融合 |
| 持久化 | 用 JDBC/MyBatis 从 MySQL 加载历史,结果写回预测表 |
避坑要点
- 同赔不等于结果相同——样本量 < 20 时可信度低,需设最小阈值。
- 容差不能太大——0.05 是经验值;亚盘/欧赔接近时容差要放宽,反之收紧。
- 时间漂移——3 年前的同赔与现在的同赔含义不同,一定要加权衰减。
- 数据泄漏——训练/验证必须按时间切分,不能用未来的同赔预测过去。
- 过拟合——三个赔率都精确匹配样本会很少,考虑只匹配主胜赔率或让球方向。
如果需要从 MySQL 读取数据或接入 Spring Boot 提供 REST API 的完整版本,我可以进一步给出代码。