双色球从2003年开售到现在,二十多年攒下来的开奖记录,说多不多,说少也不少——三千多期,每期六红一蓝,加上期号、日期、销售额、奖池这些字段,整理成一张干净的表,其实是一件挺考验耐心的事。我前后折腾过好几版数据,最早是手动从网页上一页页翻着抄,后来写脚本抓,再后来干脆把整份数据落进数据库里做查询分析。踩过的坑包括但不限于:期号断档、日期格式混乱、红球顺序不统一、某些年份的字段缺失、Excel打开CSV时把号码当成日期自动转换等等。这篇就把我整理这份2003到2025年、三千两百多期双色球历史数据集的完整思路摊开讲,包括数据从哪来、怎么清洗、Excel和MySQL两种形态各自怎么落地、字段怎么设计、以及拿到数据之后能做哪些真正有意思的统计。适合两类人看:一类是想拿现成数据集做练手项目的数据分析新手,另一类是想自己动手整理一份干净历史数据、但不想重复踩坑的老手。
1. 先想清楚这份数据集到底要解决什么问题
很多人一上来就问“数据在哪下载”,但真正决定这份数据集好不好用的,不是数据量,而是字段设计和一致性。三千多期听起来不少,可如果期号有断档、红球没排序、蓝球混在红球里,那这份数据基本没法直接拿来做统计。所以动手之前,我先把需求拆成了三层。
1.1 三层需求:能查、能算、能复现
第一层是“能查”。给定一个期号或者一个日期,我要能立刻定位到那一期的完整开奖信息。这要求主键设计必须唯一且稳定,期号是最自然的选择,但要注意跨年时某些年份的期号编排规则,不能简单用“年份+序号”拼接,否则排序会乱。
第二层是“能算”。做频率统计、冷热号分析、和值分布、区间分布这些,要求红球必须是排好序的独立字段,而不是一个“01,05,12,18,22,30”的字符串。字符串看着直观,但你要统计某个号码出现次数时,就得反复做字符串匹配,效率低还容易出错。所以红球我拆成了六个独立整型字段。
第三层是“能复现”。这份数据要能被别人拿去直接用,意味着清洗规则、字段含义、缺失值处理方式都得写清楚,最好连导入脚本一起给。否则别人拿到一份Excel,看到某几行空着,根本不知道是原始数据就缺,还是清洗时被误删了。
提示:字段设计阶段多花一小时,后面做分析能省一整天。尤其是号码字段,一定要拆开存整型,别图省事存字符串。
1.2 为什么选Excel和MySQL两种形态
这两种形态面向的是完全不同的使用场景,不是重复劳动。Excel适合快速浏览、做透视表、画个简单图表,对不写代码的人最友好;MySQL适合做批量查询、复杂聚合、以及后续接程序做自动化分析。我见过不少人只给Excel,结果对方想做个“最近一百期每个号码出现次数”的统计,硬是在Excel里拉了一堆辅助列,其实一条SQL就出来了。
反过来,只给数据库也不合适,很多人拿到一个.sql文件根本不知道怎么导入,还得装客户端、建库、跑脚本,门槛一下就上去了。所以两种形态都提供,Excel负责“开箱即看”,MySQL负责“开箱即算”,各司其职。
| 形态 | 目标用户 | 典型操作 | 注意事项 |
|---|---|---|---|
| Excel | 不写代码、做快速浏览 | 透视表、条件格式、简单图表 | 防止号码被识别成日期 |
| MySQL | 写SQL、做批量统计 | 聚合查询、窗口函数、关联分析 | 字符集与字段类型要统一 |
1.3 数据范围与期数核对
2003年到2025年,双色球每年开奖期数并不固定,早期一年一百多期,后来稳定在每年一百五十期上下,遇到特殊调整还会略有浮动。所以“3287期”这个数字不是拍脑袋来的,而是逐期核对后的结果。核对方法很简单:按年份分组计数,再和官方公布的年度开奖期数对照,哪一年对不上就回去查那一年是不是有漏抓或者重复。
这一步千万别跳过。我第一版数据就是少了某一年中间的两期,原因是抓取时那一页的结构和其他页不一样,脚本静默跳过了。后来做年度统计时发现那一年的期数比前后都少,才回头补上。所以拿到数据第一件事,永远是做一次“按年计数”的体检。
2. 数据从哪来,以及抓取时最容易翻车的地方
数据来源这块我不展开具体站点,只说方法。公开的开奖记录一般以列表页加分页的形式呈现,每页若干期,翻页参数在URL里。思路就是遍历年份和页码,把每页的表格解析出来,落成结构化记录。听起来简单,实际动手问题一堆。
2.1 分页结构的坑:页码不连续与末页判断
最常见的坑是页码不连续。有些年份的列表页并不是从1开始连续排到N,中间可能因为数据归档出现跳号,如果你用“页码从1递增直到抓不到数据为止”的逻辑,遇到跳号就会提前终止,后面的数据全丢。我的做法是先抓第一页,解析出总页数或者总条数,再按总数反推需要请求的页码,而不是盲目递增。
另一个坑是末页判断。有的页面最后一页返回的是空表格而不是404,有的则返回上一页的内容。稳妥的做法是对每一页解析出的记录做去重,用期号作为唯一键,抓完之后统计去重前后的数量差,差得太多说明有重复页被反复抓了。
# 伪代码:按期号去重,避免重复页污染 seen = set() records = [] for page in page_list: rows = parse_page(page) for row in rows: if row["issue"] not in seen: seen.add(row["issue"]) records.append(row)2.2 号码解析:前导零、分隔符与顺序
开奖号码在页面上通常显示成“01 05 12 18 22 30 + 08”这种形式,解析时三个细节必须处理。第一是前导零,号码01到09必须保留两位,存进数据库时转成整型没问题,但展示时要补零,否则“1”和“01”混着看很别扭。第二是分隔符不统一,有的用空格,有的用逗号,有的用竖线,正则里要把这些情况都覆盖到。第三是红球顺序,页面上偶尔不是升序排列,而统计时我们默认红球是升序的,所以解析完必须做一次排序。
import re def parse_numbers(text): # 兼容空格、逗号、竖线等分隔符 nums = re.findall(r"\d{1,2}", text) reds = sorted(int(n) for n in nums[:6]) blue = int(nums[6]) return reds, blue排序这一步看着不起眼,但如果你不做,后面统计“某两个号码同时出现”时,会因为顺序不同被当成不同组合,结果全错。
2.3 字段缺失与异常值的处理原则
抓下来的原始数据里,销售额、奖池这类字段偶尔会缺,尤其是早期年份。处理原则是:号码字段绝对不能缺,缺了这条记录就没有意义,直接标记为异常待人工核对;金额类字段允许为空,用NULL表示,不要用0填充,因为0和“未知”在统计时含义完全不同。
异常值方面,红球范围是1到33,蓝球是1到16,超出范围的直接判定为解析错误。还有一种隐蔽的异常:同一期出现重复红球,这在实际开奖中不可能发生,出现就说明解析时把某个号码重复计入了,必须回查原始页面。
注意:金额字段用NULL而不是0,这个习惯在做均值、求和统计时能帮你避免大量错误结论。
3. 清洗环节:把原始记录变成能直接用的表
抓下来的数据只能叫“原始记录”,离“能用的数据集”还差一轮系统清洗。我一般把清洗拆成四步:统一格式、补全字段、校验逻辑、生成衍生列。每一步都有具体的判断标准,不是凭感觉。
3.1 统一日期与期号格式
日期格式混乱是重灾区。有的记录是“2003-02-23”,有的是“2003/2/23”,还有的是“03-02-23”。统一成“YYYY-MM-DD”是最稳妥的,因为这种格式在Excel和MySQL里都能被正确识别为日期类型,排序也不会出错。
期号格式同样要统一。双色球的期号一般是“年份+三位序号”,比如“2025001”。但早期有些记录写成“2003001”之外的变体,清洗时要统一成七位字符串。这里有个细节:期号作为字符串存储比作为整数存储更好,因为整数会丢掉前导零,而前导零在期号里是有意义的。
-- MySQL中统一日期格式的示例 UPDATE lottery SET draw_date = STR_TO_DATE(draw_date, '%Y-%m-%d') WHERE draw_date IS NOT NULL;3.2 红球排序与蓝球独立校验
前面提过红球要排序,这里补充校验逻辑。清洗时对每一行做三项检查:红球是否恰好六个、是否都在1到33之间、是否有重复;蓝球是否只有一个、是否在1到16之间。任何一项不通过,就把这一行单独拎出来人工核对,而不是直接删掉。删掉是最省事但最危险的做法,因为你不知道删掉的是真异常还是解析bug。
我一般会生成一张“异常清单”表,记录期号、异常类型、原始内容,核对完再决定是修正还是剔除。这份清单本身也有价值,能反映数据源的稳定程度。
3.3 衍生字段:和值、跨度、奇偶比、区间分布
原始字段够用,但做分析时反复算这些指标很烦,不如在清洗阶段一次性算好存进去。常用的衍生字段有这么几个:
- 和值:六个红球之和,范围从最小的21(1+2+3+4+5+6)到最大的183(28+29+30+31+32+33)。
- 跨度:最大红球减最小红球,反映号码分散程度。
- 奇偶比:奇数个数比偶数个数,常见写法如“3:3”“4:2”。
- 区间分布:把1到33分成三个区间(1-11、12-22、23-33),统计每个区间的号码个数。
-- 生成和值与跨度的示例 SELECT issue, red1+red2+red3+red4+red5+red6 AS sum_value, red6-red1 AS span_value FROM lottery;这些衍生字段存进去之后,做分布统计就变成一句简单的GROUP BY,不用每次都在查询里现算。代价是表稍微宽一点,但换来的是查询效率和可读性,很划算。
3.4 用Excel做最后一道人工抽检
自动化清洗再完善,也建议用Excel做一轮人工抽检。方法很简单:随机抽二十期,把清洗后的记录和原始页面逐字段对照,看号码、日期、期号是否一致。这一步花不了多少时间,但能发现很多脚本层面的系统性错误,比如某个正则把“10”误匹配成“1”和“0”。
Excel在这里的优势是直观,条件格式一开,重复值、空值、超范围值都能高亮出来,扫一眼就知道有没有问题。
4. Excel形态的落地细节:别让号码变成日期
Excel版本的核心诉求是“打开就能用”,所以格式细节比数据库更讲究。我见过太多人做的Excel表,号码“01-02”被自动识别成日期,或者“1/2”变成“1月2日”,一列数据全乱。这些问题的根源都在于单元格格式没提前设定。
4.1 导入前的列格式预设
最稳妥的做法是在导入数据之前,先把号码列的格式设成“文本”,再粘贴或导入数据。如果数据已经乱了,可以用“分列”功能重新指定为文本。金额列则设成数值并保留两位小数,日期列设成日期格式。
| 列名 | 建议格式 | 原因 |
|---|---|---|
| 期号 | 文本 | 保留前导零,避免被当数字 |
| 开奖日期 | 日期 | 支持排序和日期计算 |
| 红球1-6 | 数值(整数) | 便于排序和统计 |
| 蓝球 | 数值(整数) | 同上 |
| 销售额 | 数值(两位小数) | 金额精度 |
| 奖池 | 数值(两位小数) | 同上 |
4.2 用表格而非普通区域组织数据
把数据区域转成“表格”(Ctrl+T),好处是筛选、排序、结构化引用都更方便,而且新增行时格式会自动延续。做透视表时,表格作为数据源也比普通区域稳定,不会因为插入行导致引用错位。
另外建议单独建一个“说明”工作表,写清楚每个字段的含义、数据范围、更新日期、清洗规则。别人拿到文件第一眼看到的就是这张说明表,比在正文里加批注清楚得多。
4.3 透视表做基础统计的快捷路径
Excel版本最实用的功能就是透视表。想看每个红球号码出现次数,把六个红球列堆叠成一列(可以用“逆透视”或者手动复制粘贴),然后拖进透视表按号码计数即可。想看每年开奖期数,把日期拖到行、按年组合,期号计数拖到值,一张年度分布表就出来了。
这里有个小技巧:做号码频率统计时,先把六个红球列复制到一个新表里纵向堆叠,去重后得到1到33的完整号码列表,再用COUNTIF统计每个号码在原数据里出现的次数。这样比在透视表里折腾多重合并计算区域要直观。
5. MySQL形态的落地细节:建表、导入与索引
数据库版本的价值在于查询能力,但前提是表结构设计合理、导入过程不出错。我见过不少人建表时把所有字段都设成VARCHAR,结果做数值比较时各种隐式转换,查询慢还容易出逻辑错误。
5.1 表结构设计与字段类型选择
号码字段用TINYINT就够,范围完全覆盖。期号用CHAR(7),日期用DATE,金额用DECIMAL(12,2)。主键用期号,因为期号唯一且稳定。衍生字段(和值、跨度)也用TINYINT或SMALLINT。
CREATE TABLE lottery ( issue CHAR(7) PRIMARY KEY, draw_date DATE NOT NULL, red1 TINYINT NOT NULL, red2 TINYINT NOT NULL, red3 TINYINT NOT NULL, red4 TINYINT NOT NULL, red5 TINYINT NOT NULL, red6 TINYINT NOT NULL, blue TINYINT NOT NULL, sales DECIMAL(12,2) DEFAULT NULL, pool DECIMAL(12,2) DEFAULT NULL, sum_value SMALLINT DEFAULT NULL, span_value TINYINT DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字符集统一用utf8mb4,虽然这份数据里没有中文,但统一字符集能避免导入时出现乱码警告,也方便以后扩展。
5.2 批量导入的两种方式与常见报错
小数据量可以用INSERT语句批量插入,三千多行分几批执行完全没问题。数据量大或者要反复导入时,用LOAD DATA INFILE更高效。
LOAD DATA LOCAL INFILE '/path/lottery.csv' INTO TABLE lottery FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' IGNORE 1 LINES (issue, draw_date, red1, red2, red3, red4, red5, red6, blue, sales, pool);常见报错有这么几类:日期格式不匹配导致整行被拒、金额字段有空值但列定义成了NOT NULL、CSV里有逗号导致列错位。解决办法分别是:导入前统一日期格式、金额列允许NULL、CSV字段用引号包裹或者换用制表符分隔。
注意:LOAD DATA时如果遇到“Incorrect date value”,先检查CSV里的日期是不是“2003/2/23”这种非标准格式,统一成“2003-02-23”再导。
5.3 为高频查询建索引
三千多行的表,其实不建索引查询也很快,但如果你要反复做按年份、按号码的查询,建几个索引能让体验更好。按日期建索引支持时间范围查询,按蓝球建索引支持蓝球相关统计。
CREATE INDEX idx_draw_date ON lottery(draw_date); CREATE INDEX idx_blue ON lottery(blue);红球字段不建议单独建索引,因为统计时往往是六个字段一起用,单列索引帮助有限,反而增加写入开销。真要优化红球查询,可以考虑把六个红球拼成一个排序后的字符串字段做辅助索引,但这属于进阶优化,三千行的量级没必要。
6. 拿到数据之后,这些统计才真正有意思
数据整理好只是起点,真正有价值的是拿它做分析。下面这几个方向是我实际做过、也觉得有意义的,不涉及任何预测,纯粹是描述性统计和分布观察。
6.1 号码频率与冷热号的正确看法
统计每个号码出现次数是最基础的。一条SQL就能出来:
SELECT red, COUNT(*) AS cnt FROM ( SELECT red1 AS red FROM lottery UNION ALL SELECT red2 FROM lottery UNION ALL SELECT red3 FROM lottery UNION ALL SELECT red4 FROM lottery UNION ALL SELECT red5 FROM lottery UNION ALL SELECT red6 FROM lottery ) t GROUP BY red ORDER BY cnt DESC;但这里要泼一盆冷水:三千多期、33个号码,平均每个号码出现约六百次,实际统计出来的偏差在正常波动范围内。所谓“冷号”“热号”更多是心理感受,不是可操作的规律。做这个统计的意义在于理解随机分布长什么样,而不是据此选号。
6.2 和值与跨度的分布形态
和值分布是很有意思的观察对象。理论上六个从1到33里随机取的数,和值应该集中在中间区域,实际统计出来也确实如此,两头低中间高,接近钟形。跨度同理,大多数期次的跨度落在二十几到三十之间。
用MySQL做和值分布:
SELECT FLOOR(sum_value/10)*10 AS bucket, COUNT(*) AS cnt FROM lottery GROUP BY bucket ORDER BY bucket;这种分桶统计能直观看出分布形态,比看单个数值有意思得多。
6.3 连号与重号的统计口径
连号指同一期出现相邻号码,比如12和13。重号指相邻两期出现相同号码。这两个统计的口径必须先定义清楚,否则不同人算出来的结果没法比。我的口径是:连号按“存在至少一组相邻号码”计为一期有连号;重号按“相邻两期红球交集个数”统计。
-- 统计相邻两期红球交集个数(示意,需借助临时表或窗口函数) SELECT a.issue, COUNT(*) AS overlap_cnt FROM red_numbers a JOIN red_numbers b ON b.issue = next_issue(a.issue) WHERE a.red = b.red GROUP BY a.issue;这类统计的价值在于验证“直觉”和“数据”的差距。很多人觉得连号很罕见,实际统计下来出现频率并不低。
6.4 年度销售额与奖池的趋势观察
金额类字段适合做趋势分析。按年汇总销售额,能看到整体规模的变化;奖池的波动则能反映某些时期的累积情况。这类分析用Excel的折线图最直观,用MySQL做年度聚合再导出也很快。
SELECT YEAR(draw_date) AS yr, SUM(sales) AS total_sales, AVG(pool) AS avg_pool FROM lottery GROUP BY yr ORDER BY yr;需要提醒的是,早期年份的金额字段可能有缺失,做趋势图时要么标注缺失区间,要么只用完整年份的数据,别让空值把趋势线拉断还看不出来。
7. 几个我踩过、你大概率也会踩的坑
最后这部分是纯经验,都是实际操作中真金白银换来的教训,按重要性排。
7.1 期号排序的陷阱
期号是字符串,按字符串排序时“2025001”排在“2024100”后面是对的,但如果你把期号存成整数,前导零丢失后排序逻辑就变了。更隐蔽的坑是跨年排序:如果期号格式不统一,比如有的年份是七位有的年份是六位,排序结果会完全错乱。所以期号一律用固定长度的字符串,排序前先确认格式统一。
7.2 Excel自动转换的连锁反应
Excel的自动类型识别有时候很“聪明”,聪明到把“01-02”识别成日期、把“1E5”识别成科学计数法。一旦发生,原始数据就被改了,而且保存后不可逆。防范方法只有一个:导入前把目标列格式设成文本,或者用“数据-从文本/CSV”导入向导,在向导里逐列指定格式,别直接双击打开CSV。
7.3 数据库导入的字符集与换行符问题
从Windows环境导出的CSV,换行符是“\r\n”,在Linux下用LOAD DATA导入时如果只按“\n”分割,每行末尾会多一个“\r”,导致最后一个字段解析异常。解决办法是在导入语句里指定“LINES TERMINATED BY '\r\n'”,或者导入前用工具统一换行符。字符集方面,确保CSV保存为UTF-8,数据库连接也指定UTF-8,两头一致就不会乱码。
7.4 别把“数据全”当成“数据对”
三千多期听起来很完整,但完整不等于正确。我见过一份流传的数据集,期数一期不少,但某几年的红球顺序是乱的,做组合统计时结果全偏。所以拿到任何数据集,先做三项体检:按年计数对不对、号码范围对不对、红球是否升序。三项都过了,再谈分析。
提示:体检脚本建议固化成一个小工具,每次拿到新数据先跑一遍,比肉眼检查靠谱得多。
7.5 更新维护的节奏
这份数据不是一次性的,每年都会有新期次加入。维护节奏建议是:每年年初补上一年完整数据,同时核对上一年度期数;每次更新后重跑一遍体检脚本;Excel和MySQL两个版本同步更新,避免版本分叉。如果要做长期项目,最好把抓取、清洗、导入写成一条流水线,每年跑一次就行,手动操作迟早会出错。
我个人在实际操作中的体会是,整理这类历史数据,技术难度其实不高,难的是耐心和一致性。规则定好之后严格执行,比反复改规则要省事得多。数据这东西,干净比多更重要,一份三千期但字段规整、校验通过的数据,价值远高于一份号称五千期但到处是坑的数据。