这是 数学重学路线图 阶段一的子页面
数据素养:别被数据骗了 在大数据时代,每天有无数图表、报告、新闻标题试图用数据说服你
但数据可以被操纵——不是篡改数字,而是用呈现方式 误导你的判断
这一章教你识破数据骗术,做一个有数据素养的人
一、图表骗术识别 骗术1:Y轴不从0开始 这是最常见的图表欺骗手法
对比一下:
诚实的图(Y轴从0开始) :
1 2 3 4 5 6 7 8 9 10 11 销售额(万) 100| ██ 80| ██ ██ 60| ██ ██ ██ 40| ██ ██ ██ ██ 20|██ ██ ██ ██ ██ 0|██ ██ ██ ██ ██ +----+----+----+----+---- 1月 2月 3月 4月 5月 看起来:稳步增长,正常趋势
骗人的图(Y轴从80开始) :
1 2 3 4 5 6 7 8 9 10 11 销售额(万) 100| ██████ 95| ██████ 90| ██████ 85| ██████ 80|██████ +----+----+----+----+---- 1月 2月 3月 4月 5月 看起来:暴涨!业绩翻了好几倍! 实际上:从80涨到100,只涨了25%
什么时候Y轴可以不从0开始?
当数据变化范围很小但变化本身很重要时(如体温36.5→38.5)
但必须明确标注,不能故意误导
骗术2:截断柱状图 把柱状图中间截断(用波浪线),让差异看起来巨大
A产品销量 1000,B产品销量 1050
完整柱状图:几乎一样高
截断后只显示 990-1060 的范围:B看起来是A的好几倍
骗术3:3D饼图视觉欺骗 3D饼图因为透视效果,前面的扇区看起来比实际大
永远不要用3D饼图 ——这是数据可视化的基本原则
用普通饼图或者更好的——条形图
1 2 3 4 5 6 3D饼图的问题: 正面看:A =30%, B =30%, C =40% 3D旋转后:放在前面的C看起来占了一半以上 结论:展示占比时,用水平条形图最清晰
骗术4:双Y轴误导 左Y轴是”收入(万)”0-100,右Y轴是”满意度(%)”90-100
两条线看起来趋势一致→暗示”收入增长带来满意度提升”
实际上:调整右Y轴的范围,可以让任何两条线看起来相关
双Y轴图本身不是错,但很容易被滥用来暗示因果
骗术5:面积/体积误导 用图标大小表示数据,但人眼对面积的感知是非线性的
A=100,B=200,B是A的2倍
如果用圆的直径表示:B的圆直径是A的2倍→面积是4倍→视觉感觉B是A的4倍
正确做法:用面积(而非直径)来映射数据
Python 代码:好图 vs 骗图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import matplotlib.pyplot as pltimport matplotlibmatplotlib.rcParams['font.sans-serif' ] = ['SimHei' , 'Arial Unicode MS' ] matplotlib.rcParams['axes.unicode_minus' ] = False quarters = ['Q1' , 'Q2' , 'Q3' , 'Q4' ] revenue = [980 , 1010 , 1030 , 1055 ] fig, axes = plt.subplots(1 , 2 , figsize=(12 , 5 )) axes[0 ].bar(quarters, revenue, color='#e74c3c' , width=0.5 ) axes[0 ].set_ylim(960 , 1070 ) axes[0 ].set_title('骗人的图:看起来暴涨' , fontsize=14 , fontweight='bold' ) axes[0 ].set_ylabel('收入(万)' ) for i, v in enumerate (revenue):axes[0 ].text(i, v + 2 , str (v), ha='center' , fontweight='bold' ) axes[1 ].bar(quarters, revenue, color='#2ecc71' , width=0.5 ) axes[1 ].set_ylim(0 , 1200 ) axes[1 ].set_title('诚实的图:其实很平稳' , fontsize=14 , fontweight='bold' ) axes[1 ].set_ylabel('收入(万)' ) for i, v in enumerate (revenue):axes[1 ].text(i, v + 20 , str (v), ha='center' , fontweight='bold' ) plt.tight_layout() plt.savefig('honest_vs_deceptive_chart.png' , dpi=150 ) plt.show() print ("同样的数据,不同的Y轴范围,完全不同的观感" )
二、相关不等于因果 经典案例 冰淇淋销量和溺水人数正相关
冰淇淋卖得越多,溺水的人越多
难道冰淇淋导致溺水?当然不是
共同因子 :天气热→人们买冰淇淋 + 人们去游泳→溺水增多
尼古拉斯·凯奇电影数量和泳池溺亡数正相关
相关系数 r = 0.87(非常强的正相关)
纯粹的巧合——这叫伪相关(spurious correlation)
鹳鸟数量和新生儿数量正相关 (欧洲古老的迷信)
共同因子:农村地区鹳鸟多,农村生育率也高
如何判断因果? 相关性只是因果性的必要条件 ,不是充分条件
判断因果需要:
时间顺序 :原因在前,结果在后
排除混杂变量 :有没有第三个因素同时影响两者
合理机制 :在理论上说得通
随机对照实验 :金标准——随机分组,控制变量
口诀:**”相关只是线索,因果需要证据”**
工作场景 “部署了新版本后错误率下降了” → 是新版本的功劳吗?
可能同时流量也下降了(周末效应)
可能运维同时做了其他调整
需要A/B测试或者回滚验证
“用了新安全规则后攻击减少了” → 规则有效吗?
可能攻击者换了目标
可能是季节性波动
需要对照组
三、辛普森悖论 什么是辛普森悖论? 分组看数据,A比B好;合并看数据,B比A好
听起来不可能?看这个例子:
经典例子:两家医院的手术成功率 轻症手术 :
A医院:90/100 = 90%
B医院:800/1000 = 80%
A医院更好
重症手术 :
A医院:300/900 = 33.3%
B医院:50/100 = 50%
不对,B医院更好
合并数据 :
A医院:(90+300)/(100+900) = 390/1000 = 39%
B医院:(800+50)/(1000+100) = 850/1100 = 77.3%
B医院远远好于A
分组看A在轻症上赢了,但合并后B大幅领先
为什么会这样? 样本量不均匀
A医院接了900例重症、100例轻症(重症多,拉低了总成功率)
B医院接了100例重症、1000例轻症(轻症多,拉高了总成功率)
A医院可能是更好的医院(专门接疑难杂症),但合并数据反而不好看
怎么防止被辛普森悖论骗? 看分组数据 ,不要只看合并数据
关注样本量分布 ,子组大小是否均匀
识别隐藏变量 (这里是病情严重程度)
Python 代码:辛普森悖论模拟 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import pandas as pddata = { '医院' : ['A' , 'A' , 'B' , 'B' ],'病情' : ['轻症' , '重症' , '轻症' , '重症' ],'成功' : [90 , 300 , 800 , 50 ],'总数' : [100 , 900 , 1000 , 100 ],} df = pd.DataFrame(data) df['成功率' ] = df['成功' ] / df['总数' ] * 100 print ("=== 分组数据 ===" )for _, row in df.iterrows():print (f"{row['医院' ]} 医院 {row['病情' ]} : " f"{row['成功' ]} /{row['总数' ]} = {row['成功率' ]:.1 f} %" ) print ("\n=== 合并数据 ===" )for hospital in ['A' , 'B' ]:subset = df[df['医院' ] == hospital] total_success = subset['成功' ].sum () total_cases = subset['总数' ].sum () rate = total_success / total_cases * 100 print (f"{hospital} 医院总体: {total_success} /{total_cases} = {rate:.1 f} %" )print ("\n=== 悖论解释 ===" )print ("A医院分组都不差,但因为接了大量重症(900例)," )print ("合并后被重症的低成功率拉低了整体数据" )print ("B医院虽然重症成功率只有50%,但重症只有100例," )print ("1000例轻症的80%成功率撑起了整体数据" )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import matplotlib.pyplot as pltimport matplotlibmatplotlib.rcParams['font.sans-serif' ] = ['SimHei' , 'Arial Unicode MS' ] matplotlib.rcParams['axes.unicode_minus' ] = False fig, axes = plt.subplots(1 , 3 , figsize=(15 , 5 )) axes[0 ].bar(['A医院' , 'B医院' ], [90 , 80 ], color=['#3498db' , '#e74c3c' ]) axes[0 ].set_title('轻症成功率 (%)' ) axes[0 ].set_ylim(0 , 100 ) axes[0 ].text(0 , 92 , '90%' , ha='center' , fontsize=14 , fontweight='bold' ) axes[0 ].text(1 , 82 , '80%' , ha='center' , fontsize=14 , fontweight='bold' ) axes[0 ].annotate('A赢' , xy=(0.5 , 85 ), fontsize=12 , ha='center' , color='blue' ) axes[1 ].bar(['A医院' , 'B医院' ], [33.3 , 50 ], color=['#3498db' , '#e74c3c' ]) axes[1 ].set_title('重症成功率 (%)' ) axes[1 ].set_ylim(0 , 100 ) axes[1 ].text(0 , 35.3 , '33%' , ha='center' , fontsize=14 , fontweight='bold' ) axes[1 ].text(1 , 52 , '50%' , ha='center' , fontsize=14 , fontweight='bold' ) axes[1 ].annotate('B赢' , xy=(0.5 , 45 ), fontsize=12 , ha='center' , color='red' ) axes[2 ].bar(['A医院' , 'B医院' ], [39 , 77.3 ], color=['#3498db' , '#e74c3c' ]) axes[2 ].set_title('合并成功率 (%) — 悖论!' ) axes[2 ].set_ylim(0 , 100 ) axes[2 ].text(0 , 41 , '39%' , ha='center' , fontsize=14 , fontweight='bold' ) axes[2 ].text(1 , 79.3 , '77%' , ha='center' , fontsize=14 , fontweight='bold' ) axes[2 ].annotate('B大赢?' , xy=(0.5 , 60 ), fontsize=12 , ha='center' , color='red' ) plt.suptitle("辛普森悖论:分组A赢,合并B赢" , fontsize=16 , fontweight='bold' ) plt.tight_layout() plt.savefig('simpson_paradox.png' , dpi=150 ) plt.show()
四、幸存者偏差 直觉理解 你只看到了”活下来的”,没看到”死掉的”
于是得出错误的结论
经典故事:二战飞机装甲 二战时,盟军统计返航飞机的弹孔分布
机翼和机身中弹最多 → 直觉:加强机翼和机身的装甲
统计学家亚伯拉罕·沃尔德说:应该加强驾驶舱和引擎的装甲
为什么?
返航的飞机机翼中弹多→说明机翼中弹还能飞回来
驾驶舱和引擎中弹的飞机→没回来 (坠毁了)
你看到的数据有偏——只包含”幸存者”
生活中的幸存者偏差 创业成功学
“大学辍学也能成功!看看比尔·盖茨、扎克伯格!”
你没看到的:无数辍学后找不到工作的人
幸存者偏差让你高估了辍学创业的成功率
老房子比新房子质量好?
“以前的房子住了50年还好好的”
质量差的老房子早就塌了或拆了,你看不到了
留下来的都是好的→产生”以前质量好”的错觉
读书无用论
“我认识的有钱人很多没读过大学”
统计数据:学历和收入正相关
你的社交圈不是随机样本
安全领域的幸存者偏差 只分析被捕获的攻击
WAF拦截了1000次SQL注入→”我们的防御很好”
问题:绕过WAF的攻击你根本不知道
被捕获的是”幸存样本”,真正危险的是没被捕获的
只分析已知漏洞
CVE库里的漏洞 = 被发现的
0day漏洞 = 没被发现的 = 更危险的
安全防御不能只基于已知威胁
渗透测试报告
“渗透测试没发现高危漏洞” ≠ “系统没有高危漏洞”
只是在有限时间和方法内没找到而已
五、其他统计陷阱 选择性报告(Cherry Picking) 只展示有利的数据,隐藏不利的
“我们的产品用户满意度95%”
样本是怎么选的?主动填写问卷的用户本身就偏满意
不满意的用户早就卸载了
防范 :问”原始数据呢?””样本怎么选的?””有没有被排除的数据?”
基数谬误 “我们的系统可用性从99.9%提升到99.99%”
听起来只提升了0.09个百分点?
换个角度:
99.9%:全年停机 8.76小时
99.99%:全年停机 52.6分钟
停机时间减少了约90%
越接近100%,每提升一点点都越难
99% → 99.9%:故障时间减少90%(3天→8.7小时)
99.9% → 99.99%:故障时间再减少90%(8.7小时→52分钟)
99.99% → 99.999%:故障时间再减少90%(52分钟→5.2分钟)
这就是”几个9”的工程代价——每多一个9,成本可能翻10倍
均值回归 现象 :极端表现之后,通常会向平均值靠拢
例子:
运动员打破纪录后,下一场通常表现”退步”——不是退步,是回归正常
学生考试考得特别差→家长骂→下次考好了→”骂有用”→其实是均值回归
某个月服务器故障率飙升→加班排查→下个月恢复正常→”排查有效”→可能本来就会恢复
防范 :对极端值保持冷静,多看几个周期的趋势
伯克森悖论 在特定人群中观察到的相关性,在总体中不存在
例:”为什么帅的人都渣?”
你的约会对象 = 帅 OR 性格好(至少有一项才会吸引你)
帅+性格好:你会遇到
帅+性格差:你也会遇到(因为帅)
不帅+性格好:你也会遇到(因为性格好)
不帅+性格差:你不会遇到
在你的样本中,帅和性格好呈负相关——但总体中可能没有这种相关
德州神枪手谬误 先打枪再画靶——围绕结果编故事
“我们分析了100个指标,发现X和Y高度相关!”
100个指标里,纯靠随机就可能出现几对”相关”
这叫多重比较问题
防范:预先设定假设,用 Bonferroni 校正等方法调整显著性阈值
六、如何正确看数据 检查清单 看到任何数据/图表/报告时,问自己这些问题:
1. 样本量多大?
10个人的调查 vs 10000个人的调查,可信度天差地别
小样本容易出现极端值
2. 数据来源是什么?
谁收集的?有没有利益冲突?
烟草公司资助的”吸烟无害”研究,你信吗?
3. 有没有对照组?
“吃了这个药的人80%好了” → 不吃药的人呢?也许自愈率就是80%
没有对照组的实验基本没有说服力
4. 置信区间是多少?
“A方案转化率5.2%,B方案5.5%”→ B更好?
如果置信区间重叠,差异可能没有统计显著性
看到百分比,一定要问”误差范围是多少”
5. 图表有没有动手脚?
Y轴从哪里开始?
比例尺是否均匀?
是不是3D图?
6. 是相关还是因果?
两个事情一起变化 ≠ 一个导致另一个
有没有被遗漏的第三变量?
数据素养口诀 来源可靠吗 ?谁说的,有什么动机
样本够大吗 ?小样本 = 高噪声
有对照组吗 ?没有对照 = 没有说服力
是因果还是相关 ?相关只是线索
图表诚实吗 ?Y轴、比例、维度都检查
有没有幸存者偏差 ?你看到的是全部吗
七、工作中的数据素养 安全领域 “本月0天被入侵” → 真的安全还是没检测到?
“漏洞扫描发现0个高危” → 扫描器覆盖率如何?
安全的数据往往有观察者效应 :你只能衡量你能检测到的
大数据/后端 A/B测试时:
样本量够不够?跑了多久?
P值是多少?(P < 0.05 通常被认为显著)
是不是看了太多指标导致多重比较问题?
性能监控:
P99延迟比平均延迟重要得多
平均值可能掩盖长尾问题
例:平均延迟50ms,但P99是2000ms——有1%的用户等了2秒
产品分析 “DAU增长了20%” → 是真增长还是刷量/作弊?
“留存率提升5%” → 定义变了吗?口径一致吗?
“用户评分4.8” → 刷好评?强制弹窗引导好评?
八、Python 综合实践 模拟幸存者偏差 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import randomdef simulate_survivor_bias (n_startups=10000 ):"""模拟创业幸存者偏差""" random.seed(42 ) startups = [] for i in range (n_startups): talent = random.gauss(50 , 15 ) luck = random.gauss(50 , 15 ) success_score = talent * 0.4 + luck * 0.6 startups.append({ 'talent' : talent, 'luck' : luck, 'success' : success_score, 'survived' : success_score > 70 }) survived = [s for s in startups if s['survived' ]] failed = [s for s in startups if not s['survived' ]] avg_talent_survived = sum (s['talent' ] for s in survived) / len (survived) avg_luck_survived = sum (s['luck' ] for s in survived) / len (survived) avg_talent_failed = sum (s['talent' ] for s in failed) / len (failed) avg_luck_failed = sum (s['luck' ] for s in failed) / len (failed) print (f"总共 {n_startups} 家创业公司,存活 {len (survived)} 家" )print (f"\n存活公司: 平均能力={avg_talent_survived:.1 f} , 平均运气={avg_luck_survived:.1 f} " )print (f"失败公司: 平均能力={avg_talent_failed:.1 f} , 平均运气={avg_luck_failed:.1 f} " )print (f"\n如果只采访成功者,你会以为:" )print (f" '成功者能力平均{avg_talent_survived:.0 f} 分,远超常人'" )print (f" 但你看不到运气的巨大作用(平均运气{avg_luck_survived:.0 f} 分)" )print (f" 也看不到能力不差但运气不好的失败者(能力{avg_talent_failed:.0 f} 分)" )simulate_survivor_bias()
演示双Y轴骗术 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import matplotlib.pyplot as pltimport numpy as npimport matplotlibmatplotlib.rcParams['font.sans-serif' ] = ['SimHei' , 'Arial Unicode MS' ] matplotlib.rcParams['axes.unicode_minus' ] = False months = ['1月' ,'2月' ,'3月' ,'4月' ,'5月' ,'6月' , '7月' ,'8月' ,'9月' ,'10月' ,'11月' ,'12月' ] np.random.seed(42 ) ice_cream = [20 , 25 , 35 , 50 , 70 , 85 , 95 , 90 , 65 , 40 , 25 , 20 ] drowning = [3 , 4 , 5 , 8 , 12 , 15 , 18 , 16 , 10 , 6 , 3 , 2 ] fig, axes = plt.subplots(1 , 2 , figsize=(14 , 5 )) ax1 = axes[0 ] ax2 = ax1.twinx() l1 = ax1.plot(months, ice_cream, 'o-' , color='#e74c3c' , label='冰淇淋销量' , linewidth=2 ) l2 = ax2.plot(months, drowning, 's-' , color='#3498db' , label='溺水人数' , linewidth=2 ) ax1.set_ylabel('冰淇淋销量(万)' , color='#e74c3c' ) ax2.set_ylabel('溺水人数' , color='#3498db' ) ax1.set_title('误导图:看起来冰淇淋导致溺水!' , fontweight='bold' ) lines = l1 + l2 labels = [l.get_label() for l in lines] ax1.legend(lines, labels, loc='upper left' ) ax1.tick_params(axis='x' , rotation=45 ) temperature = [2 , 5 , 12 , 18 , 25 , 30 , 33 , 32 , 26 , 18 , 10 , 3 ] axes[1 ].scatter(temperature, ice_cream, color='#e74c3c' , s=80 , label='冰淇淋销量' ) axes[1 ].scatter(temperature, [d*5 for d in drowning], color='#3498db' , s=80 , label='溺水人数(×5)' ) axes[1 ].set_xlabel('气温 (°C)' ) axes[1 ].set_ylabel('数值' ) axes[1 ].set_title('真相:气温才是共同因子' , fontweight='bold' ) axes[1 ].legend() axes[1 ].grid(True , alpha=0.3 ) plt.tight_layout() plt.savefig('correlation_not_causation.png' , dpi=150 ) plt.show()
练习题 题1:图表识别 某公司财报显示”收入增长”图表,Y轴从950万开始,最高1050万
实际收入从980万增长到1020万
问:实际增长率是多少?图表视觉上让你感觉增长了多少?
实际增长率 = (1020-980)/980 × 100% ≈ 4.1%
视觉上:柱状图从底部30%位置涨到70%位置,感觉翻了一倍多
教训:永远看Y轴起点和数字,不要只看柱子高度
题2:辛普森悖论 两个广告方案在两个渠道的转化率:
渠道A(高质量流量):方案1: 8/100=8%, 方案2: 50/500=10%
渠道B(低质量流量):方案1: 50/900=5.6%, 方案2: 5/50=10%
合并后哪个方案更好?分开看呢?该选哪个?
合并:方案1: (8+50)/(100+900)=58/1000=5.8%, 方案2: (50+5)/(500+50)=55/550=10%
合并看方案2远好于方案1(10% vs 5.8%)
分开看:渠道A方案2更好(10%>8%), 渠道B方案2也更好(10%>5.6%)
这次没有悖论——方案2确实更好
但要注意方案1在渠道B投了900个样本,方案2只投了50个
渠道B的方案2样本太小,10%可能不可靠
题3:幸存者偏差 你分析了过去一年WAF日志,发现SQL注入攻击最多,XSS其次,请求走私最少
你据此决定重点加强SQL注入防护,减少请求走私的投入
这个决策有什么问题?
幸存者偏差:WAF能检测到的攻击才会出现在日志中
SQL注入多可能是因为WAF对它的检测规则最完善
请求走私少可能是因为WAF根本检测不到(绕过了)
正确做法:结合其他数据源(应用日志、安全审计、漏洞扫描)综合判断
安全领域的原则:你看到的威胁 ≠ 全部威胁
题4:基数谬误 某安全产品宣称”检测率99.99%”
假设每天有100万次正常请求和100次真实攻击
按99.99%检测率和0.01%误报率计算
问:报警时真的是攻击的概率是多少?
真阳性 = 100 × 99.99% ≈ 100次
假阳性 = 1000000 × 0.01% = 100次
报警总数 = 100 + 100 = 200次
报警时真的是攻击的概率 = 100/200 = 50%
即使检测率99.99%、误报率0.01%,一半的报警还是误报
因为正常请求的基数远大于攻击请求——这就是基数谬误
题5:相关与因果 数据显示:某公司加班越多的团队,项目上线越快
管理层结论:”应该鼓励加班来加速交付”
列出至少3个可能的混杂因素说明这个因果推断的问题
反向因果:项目deadline紧→被迫加班,是紧迫感导致快而非加班导致快
团队能力:优秀团队既有动力加班也有能力快速交付(共同因子:团队积极性)
项目规模:小项目容易完成也不需要太多加班,但相对上线快
选择偏差:上线快的项目被记录了,加班多但延期的项目可能被排除了
沉没成本:越加班的团队越不想延期(心理因素而非效率提升)
正确做法:对照实验,或者用回归分析控制混杂变量
本章小结 图表骗术 :Y轴截断、3D饼图、双Y轴、面积误导——看数字不看形状
相关≠因果 :有共同因子、反向因果、纯巧合三种可能
辛普森悖论 :分组结论和合并结论相反,看样本量分布
幸存者偏差 :你看到的不是全部,特别是安全领域
基数谬误 :当真实事件很稀有时,误报会淹没真阳性
核心认知 :带着怀疑看数据,问来源、问样本、问对照、问因果
上一章 → 06-生活中的数学
下一章 → 数学重学/08-方程与代数思维