Files
lottery-picker/lottery_picker/ensemble_learning.py
T

725 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
大乐透/双色球 集成学习模块
方案3:多模型集成 + 权重优化训练
==================================
包含5种分析模型:
1. 频率加权模型 (Frequency Model)
2. 贝叶斯平滑模型 (Bayesian Smoothing Model)
3. 马尔可夫链模型 (Markov Chain Model)
4. K-Means聚类模型 (K-Means Clustering Model)
5. 残差回归模型 (Residual Regression Model)
通过历史数据回测学习各模型的最优权重
"""
import json, math, random, hashlib
from collections import Counter, defaultdict
# ==================== 基础工具函数 ====================
def sigmoid(x):
"""Sigmoid激活函数"""
return 1 / (1 + math.exp(-max(min(x, 500), -500)))
def normalize(scores):
"""归一化分数,使总和为1"""
total = sum(scores.values())
if total == 0:
return {k: 1.0/len(scores) for k in scores}
return {k: v/total for k, v in scores.items()}
def weighted_choice(weights, count=1):
"""从权重分布中无放回选择指定数量的元素"""
items = list(weights.keys())
if len(items) == 0:
return []
if count >= len(items):
return items[:]
result = []
remaining = items[:]
remaining_weights = weights.copy()
for _ in range(count):
total = sum(remaining_weights.values())
if total <= 0:
choice = random.choice(remaining)
else:
r = random.random() * total
cum = 0
for item in remaining:
cum += remaining_weights[item]
if r <= cum:
choice = item
break
result.append(choice)
remaining.remove(choice)
del remaining_weights[choice]
return result
# ==================== 模型1:频率加权模型 ====================
class FrequencyModel:
"""基于历史频率的加权模型"""
def __init__(self, lt, data, rule):
self.lt = lt
self.data = data
self.rule = rule
self.rcnt = rule["red_count"]
self.rmax = rule["red_max"]
self.bcnt = rule["blue_count"]
self.bmax = rule["blue_max"]
self.red_score = {}
self.blue_score = {}
self.analyze()
def analyze(self):
rule, rmax, bmax, rcnt, bcnt = self.rule, self.rule["red_max"], self.rule["blue_max"], self.rule["red_count"], self.rule["blue_count"]
all_r, all_b = [], []
for rec in self.data:
all_r.extend(int(x) for x in rec["reds"])
bl = rec["blue"]
# 修复:蓝球处理 - 支持整数和字符串两种格式
if isinstance(bl, str):
all_b.append(int(bl))
elif isinstance(bl, list):
all_b.extend(bl)
else:
all_b.append(bl)
# 频率统计
rf = Counter(all_r)
bf = Counter(all_b)
max_rf = max(rf.values()) if rf else 1
max_bf = max(bf.values()) if bf else 1
# 归一化
self.red_score = {i: rf.get(i, 0) / max_rf for i in range(1, rmax + 1)}
self.blue_score = {i: bf.get(i, 0) / max_bf for i in range(1, bmax + 1)}
# 平滑:避免0分
for i in range(1, rmax + 1):
if self.red_score[i] == 0:
self.red_score[i] = 0.01
for i in range(1, bmax + 1):
if self.blue_score[i] == 0:
self.blue_score[i] = 0.01
# 归一化到[0,1]
max_r = max(self.red_score.values())
max_b = max(self.blue_score.values())
if max_r > 0:
for i in self.red_score:
self.red_score[i] /= max_r
if max_b > 0:
for i in self.blue_score:
self.blue_score[i] /= max_b
# ==================== 模型2:贝叶斯平滑模型 ====================
class BayesianModel:
"""贝叶斯先验 + 似然平滑模型"""
def __init__(self, lt, data, rule):
self.lt = lt
self.data = data
self.rule = rule
self.rcnt = rule["red_count"]
self.rmax = rule["red_max"]
self.bcnt = rule["blue_count"]
self.bmax = rule["blue_max"]
self.prior = {i: 1.0 / rule["red_max"] for i in range(1, rule["red_max"] + 1)}
self.red_score = {}
self.blue_score = {}
self.analyze()
def analyze(self):
rule, rmax, bmax, rcnt, bcnt = self.rule, self.rule["red_max"], self.rule["blue_max"], self.rule["red_count"], self.rule["blue_count"]
all_r, all_b = [], []
for rec in self.data:
all_r.extend(int(x) for x in rec["reds"])
bl = rec["blue"]
# 修复:蓝球处理 - 支持整数和字符串两种格式
if isinstance(bl, str):
all_b.append(int(bl))
elif isinstance(bl, list):
all_b.extend(bl)
else:
all_b.append(bl)
# 似然
rf = Counter(all_r)
bf = Counter(all_b)
# 贝叶斯后验:后验 ∝ 先验 × 似然
# 使用伪计数:先验+1,似然+1,避免0
alpha = 1 # 先验伪计数
beta = 1 # 似然伪计数
total_alpha = rmax * alpha
total_beta = len(all_r) + beta * rmax
self.red_score = {}
for i in range(1, rmax + 1):
# 后验均值 = (α + count) / (α + β * rmax)
posterior = (alpha + rf.get(i, 0)) / (total_alpha + beta)
self.red_score[i] = posterior
total_blue = len(all_b) + beta * bmax
self.blue_score = {}
for i in range(1, bmax + 1):
posterior = (alpha + bf.get(i, 0)) / (total_alpha + beta)
self.blue_score[i] = posterior
# 归一化
max_r = max(self.red_score.values())
max_b = max(self.blue_score.values())
if max_r > 0:
for i in self.red_score:
self.red_score[i] /= max_r
if max_b > 0:
for i in self.blue_score:
self.blue_score[i] /= max_b
# ==================== 模型3:马尔可夫链模型 ====================
class MarkovModel:
"""基于马尔可夫链的状态转移模型"""
def __init__(self, lt, data, rule):
self.lt = lt
self.data = data
self.rule = rule
self.rcnt = rule["red_count"]
self.rmax = rule["red_max"]
self.bcnt = rule["blue_count"]
self.bmax = rule["blue_max"]
self.red_score = {}
self.blue_score = {}
self.analyze()
def analyze(self):
rule, rmax, bmax, rcnt, bcnt = self.rule, self.rule["red_max"], self.rule["blue_max"], self.rule["red_count"], self.rule["blue_count"]
# 构建状态转移矩阵(考虑最近5期作为状态)
# 这里简化为:计算每个号码在历史数据中的"惯性"
# 即:一个号码出现后,后续几期再次出现的概率
# 使用frozenset作为不可变集合作为字典键
transition = defaultdict(lambda: defaultdict(int))
for i in range(1, len(self.data)):
prev = frozenset(int(x) for x in self.data[i-1]["reds"])
curr = set(int(x) for x in self.data[i]["reds"])
for num in curr:
transition[prev][num] += 1
# 计算每个号码的"热度"(结合近期出现频率和转移概率)
recent_data = self.data[:20] if len(self.data) >= 20 else self.data
recent_reds = []
for rec in recent_data:
recent_reds.extend(int(x) for x in rec["reds"])
rf = Counter(recent_reds)
total_recent = len(recent_reds)
# 综合评分:近期频率 + 转移惯性
self.red_score = {}
for i in range(1, rmax + 1):
freq_score = rf.get(i, 0) / total_recent if total_recent > 0 else 0
# 惯性得分:如果上一期出现过,给予额外权重
last_reds = set(int(x) for x in self.data[0]["reds"]) if self.data else set()
inertia = 1.5 if i in last_reds else 1.0
self.red_score[i] = (freq_score * inertia + 0.01) # 加0.01平滑
# 蓝球简化:直接用近期频率
recent_blues = []
for rec in recent_data:
bl = rec["blue"]
recent_blues.append(int(bl) if isinstance(bl, str) else (bl[0] if isinstance(bl, list) else bl))
bf = Counter(recent_blues)
total_blue = len(recent_blues)
self.blue_score = {}
for i in range(1, bmax + 1):
self.blue_score[i] = (bf.get(i, 0) / total_blue * 0.8 + 1.0 / bmax * 0.2) if total_blue > 0 else (1.0 / bmax)
# 归一化
max_r = max(self.red_score.values()) if self.red_score else 1
max_b = max(self.blue_score.values()) if self.blue_score else 1
for i in self.red_score:
self.red_score[i] /= max_r
for i in self.blue_score:
self.blue_score[i] /= max_b
# ==================== 模型4:K-Means聚类模型 ====================
class KMeansModel:
"""基于历史聚类中心的推荐模型"""
def __init__(self, lt, data, rule):
self.lt = lt
self.data = data
self.rule = rule
self.rcnt = rule["red_count"]
self.rmax = rule["red_max"]
self.bcnt = rule["blue_count"]
self.bmax = rule["blue_max"]
self.red_score = {}
self.blue_score = {}
self.analyze()
def analyze(self):
rule, rmax, bmax, rcnt, bcnt = self.rule, self.rule["red_max"], self.rule["blue_max"], self.rule["red_count"], self.rule["blue_max"]
# 提取所有历史红球组合
clusters = []
for rec in self.data:
nums = sorted(int(x) for x in rec["reds"])
if len(nums) == rcnt:
clusters.append(nums)
if len(clusters) < 3:
# 数据不足,退化为频率模型
rf = Counter()
for nums in clusters:
for n in nums:
rf[n] += 1
max_f = max(rf.values()) if rf else 1
self.red_score = {i: rf.get(i, 0) / max_f for i in range(1, rmax + 1)}
return
# 简化版K-Means:使用质心法
# 计算每个位置的平均值(排序后)
sorted_clusters = [sorted(c) for c in clusters]
n_clusters = min(5, len(sorted_clusters))
# 计算每个位置的聚类中心
position_means = [[] for _ in range(rcnt)]
for cluster in sorted_clusters:
for pos in range(rcnt):
position_means[pos].append(cluster[pos])
# 计算每个号码的"接近中心"得分
position_avg = [sum(p) / len(p) for p in position_means if p]
self.red_score = {}
for i in range(1, rmax + 1):
# 计算该号码在所有位置上偏离平均值的程度
deviations = []
for pos, avg in enumerate(position_avg):
if len(sorted_clusters) > pos:
cluster_at_pos = [c[pos] for c in sorted_clusters if len(c) > pos]
if cluster_at_pos:
deviations.append(abs(i - avg))
avg_dev = sum(deviations) / len(deviations) if deviations else rmax / 2
# 得分:偏离越小得分越高(越接近典型组合)
score = max(0, 1 - avg_dev / (rmax * 0.5))
self.red_score[i] = max(score, 0.01) # 最小0.01
# 蓝球:直接用频率
all_blues = []
for rec in self.data:
bl = rec["blue"]
all_blues.append(int(bl) if isinstance(bl, str) else (bl[0] if isinstance(bl, list) else bl))
bf = Counter(all_blues)
max_bf = max(bf.values()) if bf else 1
self.blue_score = {i: bf.get(i, 0) / max_bf for i in range(1, bmax + 1)}
for i in range(1, bmax + 1):
if self.blue_score[i] == 0:
self.blue_score[i] = 0.01
# 归一化
max_r = max(self.red_score.values()) if self.red_score else 1
max_b = max(self.blue_score.values()) if self.blue_score else 1
for i in self.red_score:
self.red_score[i] /= max_r
for i in self.blue_score:
self.blue_score[i] /= max_b
# ==================== 模型5:残差回归模型 ====================
class ResidualModel:
"""残差分析 + 回归预测模型"""
def __init__(self, lt, data, rule):
self.lt = lt
self.data = data
self.rule = rule
self.rcnt = rule["red_count"]
self.rmax = rule["red_max"]
self.bcnt = rule["blue_count"]
self.bmax = rule["blue_max"]
self.red_score = {}
self.blue_score = {}
self.analyze()
def analyze(self):
rule, rmax, bmax, rcnt, bcnt = self.rule, self.rule["red_max"], self.rule["blue_max"], self.rule["red_count"], self.rule["blue_count"]
# 计算每期的和值、奇偶比、大小比作为特征
sums = []
for rec in self.data:
s = sum(int(x) for x in rec["reds"])
sums.append(s)
if len(sums) < 10:
# 数据不足,退化为均匀分布
self.red_score = {i: 1.0 / rmax for i in range(1, rmax + 1)}
self.blue_score = {i: 1.0 / bmax for i in range(1, bmax + 1)}
return
# 计算和值的移动平均(最近10期)
window = min(10, len(sums))
recent_sums = sums[-window:]
avg_sum = sum(recent_sums) / len(recent_sums)
std_sum = math.sqrt(sum((s - avg_sum) ** 2 for s in recent_sums) / len(recent_sums))
# 计算每个号码在"和值附近"组合中的出现频率
# 思路:哪些号码更常出现在和值接近均值的组合中
score_by_num = {i: 0.0 for i in range(1, rmax + 1)}
count_by_num = {i: 0 for i in range(1, rmax + 1)}
for rec in self.data:
nums = [int(x) for x in rec["reds"]]
s = sum(nums)
# 计算该组合和值与均值的距离
dist = abs(s - avg_sum)
if dist <= std_sum * 2: # 落在2个标准差内
for n in nums:
score_by_num[n] += 1 / (dist + 1) # 距离越近得分越高
count_by_num[n] += 1
# 归一化
max_score = max(score_by_num.values()) if max(score_by_num.values()) > 0 else 1
self.red_score = {}
for i in range(1, rmax + 1):
if count_by_num[i] > 0:
self.red_score[i] = (score_by_num[i] / count_by_num[i]) / max_score
else:
self.red_score[i] = 1.0 / rmax # 均匀分布
# 蓝球:和值相关性分析
blues_by_sum = defaultdict(list)
for rec in self.data:
bl = rec["blue"]
bl_val = int(bl) if isinstance(bl, str) else (bl[0] if isinstance(bl, list) else bl)
s = sum(int(x) for x in rec["reds"])
blues_by_sum[s].append(bl_val)
self.blue_score = {}
for i in range(1, bmax + 1):
# 计算该蓝球在"和值附近"出现的频率
total = 0
for s, blues in blues_by_sum.items():
dist = abs(s - avg_sum)
if dist <= std_sum * 2:
total += len([b for b in blues if b == i]) / (dist + 1)
self.blue_score[i] = max(total / 100, 1.0 / bmax)
# 归一化
max_r = max(self.red_score.values()) if self.red_score else 1
max_b = max(self.blue_score.values()) if self.blue_score else 1
for i in self.red_score:
self.red_score[i] /= max_r
for i in self.blue_score:
self.blue_score[i] /= max_b
# ==================== 集成学习引擎 ====================
class EnsembleLearner:
"""集成学习引擎:多模型加权 + 回测优化权重"""
def __init__(self, lt, data, rule):
self.lt = lt
self.data = data
self.rule = rule
self.rcnt = rule["red_count"]
self.rmax = rule["red_max"]
self.bcnt = rule["blue_count"]
self.bmax = rule["blue_max"]
# 初始化5个模型
self.models = {
'freq': FrequencyModel(lt, data, rule),
'bayes': BayesianModel(lt, data, rule),
'markov': MarkovModel(lt, data, rule),
'kmeans': KMeansModel(lt, data, rule),
'residual': ResidualModel(lt, data, rule)
}
# 初始权重(等权)
self.weights = {m: 1.0 / len(self.models) for m in self.models}
self.initial_weights = self.weights.copy()
# 回测结果
self.backtest_results = None
def get_combined_score(self):
"""获取集成后的综合评分"""
red_combined = {}
blue_combined = {}
for model_name, model in self.models.items():
w = self.weights[model_name]
for i in range(1, self.rmax + 1):
red_combined[i] = red_combined.get(i, 0) + w * model.red_score[i]
for i in range(1, self.bmax + 1):
blue_combined[i] = blue_combined.get(i, 0) + w * model.blue_score[i]
# 归一化
max_r = max(red_combined.values()) if red_combined else 1
max_b = max(blue_combined.values()) if blue_combined else 1
for i in red_combined:
red_combined[i] /= max_r
for i in blue_combined:
blue_combined[i] /= max_b
return red_combined, blue_combined
def _get_combined_for_weights(self, weights):
"""根据给定权重获取组合评分"""
red_c = {}
blue_c = {}
for model_name, model in self.models.items():
ww = weights[model_name]
for i in range(1, self.rmax + 1):
red_c[i] = red_c.get(i, 0) + ww * model.red_score[i]
for i in range(1, self.bmax + 1):
blue_c[i] = blue_c.get(i, 0) + ww * model.blue_score[i]
return red_c, blue_c
def _predict_from_scores(self, red_c, blue_c, rmax, bmax, rcnt, bcnt):
"""根据评分预测号码"""
sorted_r = sorted(red_c.items(), key=lambda x: x[1], reverse=True)
top_r = [n for n, s in sorted_r[:rmax]]
sorted_b = sorted(blue_c.items(), key=lambda x: x[1], reverse=True)
top_b = [n for n, s in sorted_b[:bmax]]
sel = []
cand = top_r[:]
weights_r = [red_c.get(n, 0.5) for n in cand]
while len(sel) < rcnt and cand:
tw = sum(weights_r)
if tw <= 0: pk = random.choice(cand)
else:
rv = random.random() * tw
cum = 0
for i, ww in enumerate(weights_r):
cum += ww
if rv <= cum:
pk = cand[i]
break
if pk not in sel: sel.append(pk)
idx = cand.index(pk); cand.pop(idx); weights_r.pop(idx)
sel.sort()
bls = []
cand_b = top_b[:]
weights_b = [blue_c.get(n, 0.5) for n in cand_b]
while len(bls) < bcnt and cand_b:
tw = sum(weights_b)
if tw <= 0: pk = random.choice(cand_b)
else:
rv = random.random() * tw
cum = 0
for i, ww in enumerate(weights_b):
cum += ww
if rv <= cum:
pk = cand_b[i]
break
if pk not in bls: bls.append(pk)
idx = cand_b.index(pk); cand_b.pop(idx); weights_b.pop(idx)
bls.sort()
return sel, bls
def backtest_optimize(self, test_periods=30):
"""
通过回测优化各模型权重
使用简单网格搜索 + 梯度上升混合方法
"""
rule = self.rule
rmax, bmax = self.rule["red_max"], self.rule["blue_max"]
print(f"\n{'='*60}")
print(f"🧠 集成学习权重优化 | 彩种: {rule['name']} | 数据期数: {len(self.data)}")
print(f"{'='*60}")
# 准备回测数据(按时间顺序)
sorted_data = sorted(self.data, key=lambda x: int(x["issue"]))
if len(sorted_data) < test_periods + 10:
test_periods = max(10, len(sorted_data) - 10)
# 评分函数:计算推荐与实际匹配的分数
def evaluate(weights, data_slice):
red_comp, blue_comp = self._get_combined_for_weights(weights)
pred_r, pred_b = self._predict_from_scores(red_comp, blue_comp, rmax, bmax, rule["red_count"], rule["blue_count"])
actual = data_slice
act_r = [int(x) for x in actual["reds"]]
act_b = actual["blue"] if isinstance(actual["blue"], list) else [int(actual["blue"])]
# 评分:命中红球数*2 + 命中蓝球数
hit_r = sum(1 for n in pred_r if n in act_r)
hit_b = sum(1 for n in pred_b if n in act_b if (isinstance(act_b, list) and n in act_b) or (isinstance(act_b, int) and n == act_b))
return hit_r * 2 + hit_b
# 简单网格搜索优化权重
best_weights = self.weights.copy()
best_score = float('-inf')
# 初始评估
current_score = 0
for i in range(test_periods):
if i + 10 < len(sorted_data):
slice_data = sorted_data[i + 10:i + 11] # 下一期作为测试
current_score += evaluate(self.weights, slice_data[0])
print(f" 初始等权总分: {current_score:.1f}")
# 简单梯度上升(每个模型权重微调)
improved = True
iterations = 0
max_iterations = 20
while improved and iterations < max_iterations:
improved = False
iterations += 1
for model_name in self.weights:
for delta in [-0.05, 0.05]: # 微调权重
test_w = self.weights.copy()
test_w[model_name] = max(0.01, min(0.9, test_w[model_name] + delta))
# 归一化
total = sum(test_w.values())
test_w = {k: v/total for k, v in test_w.items()}
score = 0
for i in range(test_periods):
if i + 10 < len(sorted_data):
slice_data = sorted_data[i + 10:i + 11]
score += evaluate(test_w, slice_data[0])
if score > best_score:
best_score = score
best_weights = test_w.copy()
improved = True
self.weights = best_weights
self.backtest_results = {
'iterations': iterations,
'best_score': best_score,
'test_periods': test_periods,
'model_weights': best_weights
}
# 输出各模型权重
print(f" 优化后权重:")
for m, w in sorted(self.weights.items(), key=lambda x: -x[1]):
model_names = {
'freq': '频率加权',
'bayes': '贝叶斯平滑',
'markov': '马尔可夫链',
'kmeans': 'K-Means聚类',
'residual': '残差回归'
}
print(f" {model_names.get(m, m):<12s}: {w:.4f}")
print(f" 最终集成总分: {best_score:.1f} (测试{test_periods}期)")
# 返回综合评分
return self.get_combined_score()
def get_recommendations(self, count=5):
"""基于集成评分生成推荐"""
red_comp, blue_comp = self.get_combined_score()
rmax, bmax = self.rule["red_max"], self.rule["blue_max"]
rcnt, bcnt = self.rule["red_count"], self.rule["blue_count"]
sorted_r = sorted(red_comp.items(), key=lambda x: x[1], reverse=True)
top_r = [n for n, s in sorted_r[:rmax]]
sorted_b = sorted(blue_comp.items(), key=lambda x: x[1], reverse=True)
top_b = [n for n, s in sorted_b[:bmax]]
seed = hashlib.md5(f"{self.lt}_ensemble".encode()).hexdigest()
res = []
for ci in range(count):
cs = hashlib.md5(f"ensemble_c{ci}_{seed}".encode()).hexdigest()
cr = random.Random(cs)
sel = []
cand = top_r[:]
w = [red_comp.get(n, 0.5) for n in cand]
while len(sel) < rcnt and cand:
tw = sum(w)
if tw <= 0: pk = cr.choice(cand)
else:
rv = cr.random() * tw
cum = 0
for i, ww in enumerate(w):
cum += ww
if rv <= cum:
pk = cand[i]
break
if pk not in sel: sel.append(pk)
idx = cand.index(pk); cand.pop(idx); w.pop(idx)
sel.sort()
bls = []
cand_b = top_b[:]
wb = [blue_comp.get(n, 0.5) for n in cand_b]
while len(bls) < bcnt and cand_b:
tw = sum(wb)
if tw <= 0: pk = cr.choice(cand_b)
else:
rv = cr.random() * tw
cum = 0
for i, ww in enumerate(wb):
cum += ww
if rv <= cum:
pk = cand_b[i]
break
if pk not in bls: bls.append(pk)
idx = cand_b.index(pk); cand_b.pop(idx); wb.pop(idx)
bls.sort()
score = round(sum(red_comp.get(n, 0) for n in sel) + sum(blue_comp.get(n, 0) for n in bls), 4)
mid = rmax / 2
res.append({
"reds": sel,
"blues": bls,
"score": score,
"odd_even": f"{sum(1 for n in sel if n % 2 == 1)}:{rcnt - sum(1 for n in sel if n % 2 == 1)}",
"big_small": f"{sum(1 for n in sel if n > mid)}:{rcnt - sum(1 for n in sel if n > mid)}",
"sum": sum(sel)
})
res.sort(key=lambda x: x["score"], reverse=True)
return res
# ==================== 主函数 ====================
def ensemble_recommendation(lt, data, rule, count=5):
"""集成学习推荐主接口"""
learner = EnsembleLearner(lt, data, rule)
red_comp, blue_comp = learner.backtest_optimize(test_periods=30)
recommendations = learner.get_recommendations(count=count)
return {
'weights': learner.weights,
'backtest': learner.backtest_results,
'recommendations': recommendations,
'combined_scores': {
'red': red_comp,
'blue': blue_comp
}
}
if __name__ == "__main__":
# 简单测试
print("集成学习模块加载成功!")
print("可用模型:频率加权、贝叶斯平滑、马尔可夫链、K-Means聚类、残差回归")