Z检验和t检验的运用--趣味例子--打麻将概率分析

主要内容:Z检验和t检验的PYTHON实现和分析,比如我们随便伪造一份数据,打麻将的日期以及输赢,数据长这样子。

play为1则是打了麻将,并记录输赢;

假设一个人,自称只是周末打打麻将,那么一年下来,打麻将的概率约为28.8%;

但经过一段时间的观察和记录,发现打麻将的概率明显不止这些,那么我们如何确定,他/她是否在说谎;

1.创建数据:

首先我们把2023年的周一到周五设为0,周六周日设为1,作为对照组;

import numpy as np
import pandas as pd
import datetime

temp_dates = pd.date_range("2023-01-01","2023-12-31",freq='D')
# temp_dates
def get_it(da):
    return 1 if da.weekday() == 5 or da.weekday() == 6 else 0
temp_df = pd.DataFrame({'date':temp_dates})
# dodo是随便起的列名周末为1,其余时间为0
temp_df['dodo'] = list(map(get_it,temp_dates))
temp_df

如下,这样算下来,概率就是28.8%这样子;

由于很多实现假设检验的库,需要一个列表或者数组,我们就弄出来;

# 2023年伪造的数据
raw_data = temp_df.dodo.values
# 近期观察记录的数据
new_data=df.values.ravel()
new_data

也就是42天打了17次麻将,概率为41.4%;

2.计算均值标准差

# 只是周末打牌的
mean_raw = np.mean(raw_data)
std_raw = np.std(raw_data)
print('源数据均值:',mean_raw,'源数据标准误差:',std_raw)

# 近期观察的
mean_new = np.mean(new_data)
std_new= np.std(new_data)
print('近期数据均值:',mean_new,'近期数据标准误差:',std_new)

注意numpy计算的时候,ddof自由度为0,即除以n而不是n-1;

 3.尝试Z检验

假如,我们要确认,打牌概率是否从周末打(28.8%)上升到近期观察的(41.4%),需要一定的样本量,我们看看42天的观察记录是否达到要求,若是样本量过少,则容易犯取伪错误;

3.1样本量的要求

而样本量的选择,依赖几个参数;

# 如果用Z检验,最小样本量
# 取alpha或0.05;beta为20%;
z_alpha = norm.ppf(0.95)
z_beta = norm.ppf(0.8)
# z_beta,z_alpha
d = mean_new-2/7 # 均值之差
n_least_z = ((z_alpha+z_beta)*std_raw/d)**2
n_least_z

# np.float64(76.22639987239985)

shit!居然还不够数,就像摇骰子,每个骰子的概率为1/6,但还是可以摇到五个一,有可能只是近期发生得过于频繁而已;但是弃真错误,不依赖样本量,依然可以强搞!

3.2 双尾检验,均值是否有差异

# 双边检验--双样本方差相同
# 看近期数据,是否和2/7的概率有明显差异
import statsmodels.stats.weightstats as sw
z,p = sw.ztest(raw_data,new_data,alternative='two-sided',usevar='pooled')
print(z)
print(p)

-1.682999425881847
0.09237520256460527

淦,9.2%,很难完全拒绝原假设,无法从统计学上定死,概率变化了;

双尾检验要求比较严格嘛,我们可以用单尾;

3.3 单尾检验

原假设:七分之二的概率大于等于近期打牌概率

备择假设:近期打牌概率大于2/7(这是我们希望通过反证法验证的结果)

import statsmodels.stats.weightstats as sw
z,p = sw.ztest(raw_data,new_data,alternative='smaller')
print(z)
print(p)

-1.682999425881847
0.046187601282302634

这下好了,低于5%,可以拒绝原假设,认为raw_data 是小于new_data的均值的,即确认,经过一段时间的观察,打牌的概率根本不止2/7,要高一些的;不过,我们样本量不够,容易犯取伪错误;

3.4 找上下限

虽然近期打牌的概率为41.4%,但我可以自称只是近期比较频繁,平时是坚决不打麻将的;亦或者在麻将圈中,别人说你怎么最近没怎么打麻将,我可以自称近期比较忙,我平时绝对不会只打这么少次数的,即确定上下限,我们循环查找;

找下限:

result_df = pd.DataFrame()
index=1
pd.set_option('display.max_rows',None)
for i in np.arange(0.1,1,0.01):
    z,p = sw.ztest(new_data,value=i,alternative='larger')
    result_df.loc[index,'打牌概率'] = i
    result_df.loc[index,'Z值'] = z
    result_df.loc[index,'p值'] = p
    index+=1

# 看起来聪明一点的办法找到临界值
rate_down = result_df[result_df['p值']>=0.05].head(1)['打牌概率'].values
rate_down

同理找到上限:

pd.set_option('display.float_format', lambda x: '%.4f' % x) # 4位小数
result_df2 = pd.DataFrame()
index=1
pd.set_option('display.max_rows',None)
for i in np.arange(0.1,1,0.01):
    z,p = sw.ztest(new_data,value=i,alternative='smaller')
    result_df2.loc[index,'打牌概率'] = i
    result_df2.loc[index,'Z值'] = z
    result_df2.loc[index,'p值'] = p
    index+=1
result_df2
rate_ceil = result_df2[result_df2['p值']>=0.05].tail(1).打牌概率.values
rate_ceil

4.t检验 

一般地认为,Z检验要求总体正态分布,并且样本量大于30,但这基本是个经验之谈,很多软件或者代码的内部实现,依然是T检验;

4.1 样本量要求

T检验的样本量,跟alpha,beta和均值之差除以样本标准差有关,此处可计算

delta = np.abs(mean_new-mean_raw)/std_raw
delta  # np.float64(0.28047129562700596)

查表可知,约需要71个,还是不符合要求,不过观察太麻烦,就这样了;

下面的流程同Z检验,不过调用的库不同;

4.2 均值是否有差异

tstat, pval = stats.ttest_ind(a=raw_data, b=new_data, alternative="two-sided")
print("t-stat: {:.2f}   pval: {:.4f}".format(tstat, pval))
# 双边检验证明差异性
t-stat: -1.68   pval: 0.0931

还是要接受原假设;

tstat, pval = stats.ttest_ind(a=raw_data, b=new_data, alternative="less")
print("t-stat: {:.2f}   pval: {:.4f}".format(tstat, pval))
# 拒绝原假设,认为近期数据大于2/7
t-stat: -1.68   pval: 0.0466

4.3 找下限

result_df3 = pd.DataFrame()
index=1
for rate in np.arange(0.1,0.8,0.01):
    t_stat, t_pval = stats.ttest_1samp(a=new_data, popmean=rate, alternative='greater')
    result_df3.loc[index,'rate']=rate
    result_df3.loc[index,'t_stat']=t_stat
    result_df3.loc[index,'pval']=t_pval
    index+=1

rate_down_t = result_df3[result_df3['pval']>=0.05].iloc[0]['rate']
rate_down_t

结果同Z,29%,因为我这分的比较粗旷,Z和T计算公式不同,小数点再精确一位,估计就能看出差异了;

4.4 找上限

result_df4 = pd.DataFrame()
index=1
for rate in np.arange(0.1,0.8,0.01):
    t_stat, t_pval = stats.ttest_1samp(a=new_data, popmean=rate, alternative='less')
    result_df4.loc[index,'rate']=rate
    result_df4.loc[index,'t_stat']=t_stat
    result_df4.loc[index,'pval']=t_pval
    index+=1

rate_ceil_t = result_df4[result_df4['pval']>=0.05].iloc[-1]['rate']
rate_ceil_t

结果同Z,54%;

5.我们将代码封装一下,同时添加一些图表功能

数据较之前增加了一些,因为这个类是我后来修改文章补充的内容;

import numpy as np
import pandas as pd
import datetime
from scipy.stats import norm
import matplotlib.pyplot as plt
import statsmodels.stats.weightstats as sw
import math
from scipy import stats


# 创建一年中周六周日为1的数据
class DaMajiang(object):
    def __init__(self, path="D:\打麻将记录.xlsx", raw_data=None, use_z=True, use_t=True, if_draw=True, alpha=0.05,
                 beta=0.2):
        self.path = path
        self.alpha = alpha
        self.beta = beta
        self.use_z = use_z
        self.use_t = use_t
        self.if_draw = if_draw
        if not raw_data:
            "生成2023年周六周日为1其余为0的打麻将数据"
            temp_dates = pd.date_range("2023-01-01", "2023-12-31", freq='D')

            def get_it(da):
                return 1 if da.weekday() == 5 or da.weekday() == 6 else 0

            temp_df = pd.DataFrame({'date': temp_dates})
            # dodo是随便起的列名周末为1,其余时间为0
            temp_df['dodo'] = list(map(get_it, temp_dates))
            self.raw_rate = temp_df.dodo.value_counts()[1] / len(temp_df)
            self.raw_data = temp_df.dodo.values
        temp = pd.read_excel(self.path, sheet_name="Sheet1", dtype={'play': int}, usecols='C')
        self.new_data = temp.values.ravel()
        if raw_data:
            self.raw_data = raw_data
            self.raw_rate = np.mean(self.raw_data)


    def calculate_info(self):
        self.mean_raw = np.mean(self.raw_data)  # 假设历史数据均值
        self.std_raw = np.std(self.raw_data)  # 假设历史数据标准差
        self.mean_new = np.mean(self.new_data)  # 近期数据均值
        self.std_new = np.std(self.new_data)  # 近期数据标准差

    def try_z(self):
        delta = np.abs(self.mean_new - self.mean_raw)
        z_alpha = norm.ppf(1 - self.alpha)
        z_beta = norm.ppf(1 - self.beta)
        n_least_z = ((z_alpha + z_beta) * self.std_raw / delta) ** 2
        print("不犯取伪错误的情况下,样本量至少要", math.ceil(n_least_z), "个")
        print("目前采集的样本数", len(self.new_data))
        # 双样本同方差,双边检验
        print('----运用Z检验进行验证----')
        print("----开始验证近期打牌概率,与之前是否相同----")
        z, p = sw.ztest(self.raw_data, self.new_data, alternative='two-sided', usevar='pooled')
        print("z-test:", z, "p-value:", p)
        print(f"原本假设周六周日打牌,根据近期观察的情况判断,在假设历史打牌概率为{round(self.raw_rate, 2)}的情况下")
        print('经过观察记录近期打牌的次数,这一系列事件的发生概率为', round(p, 4))
        if p < self.alpha:
            print("结论----拒绝原假设,近期数据的均值与假设历史数据的均值不同----")
        else:
            print("结论----接受原假设,近期数据的均值与假设历史数据的均值相同----")

        # 单边检验找下限
        result_df = pd.DataFrame()
        index = 1
        for i in np.arange(0.1, 1, 0.01):
            z, p = sw.ztest(self.new_data, value=i, alternative='larger')
            result_df.loc[index, '打牌概率'] = i
            result_df.loc[index, 'Z值'] = z
            result_df.loc[index, 'p值'] = p
            index += 1
        rate_down = result_df[result_df['p值'] >= 0.05].head(1)['打牌概率'].values
        print("打牌概率下限:", round(rate_down[0], 2))

        # 单边检验找上限
        result_df2 = pd.DataFrame()
        index_2 = 1
        pd.set_option('display.max_rows', 20)
        for i in np.arange(0.1, 1, 0.01):
            z, p = sw.ztest(self.new_data, value=i, alternative='smaller')
            result_df2.loc[index_2, '打牌概率'] = i
            result_df2.loc[index_2, 'Z值'] = z
            result_df2.loc[index_2, 'p值'] = p
            index_2 += 1
        rate_ceil = result_df2[result_df2['p值'] >= 0.05].tail(1).打牌概率.values
        print("打牌概率上限:", round(rate_ceil[0], 2))
        print('近期打牌均值为:', self.mean_new)
        # print('可自称打牌概率下限:', round(rate_down[0],2))
        # print('可自称打牌概率上限为', round(rate_ceil[0],2))

    def try_t(self):
        # 双样本独立,双侧检验
        print('-' * 50)
        print('----运用T检验进行验证----')
        print('验证近期数据是否与往期数据发生改变')
        tstat, pval = stats.ttest_ind(a=self.raw_data, b=self.new_data, alternative="two-sided")
        print("t-test:", tstat, "p-value:", pval)
        print(f"原本假设周六周日打牌,根据近期观察的情况判断,在假设历史打牌概率为{round(self.raw_rate, 2)}的情况下")
        print('经过观察记录近期打牌的次数,这一系列事件的发生概率为', round(pval, 4))
        if pval < self.alpha:
            print("结论----拒绝原假设,近期数据的均值与假设历史数据的均值不同----")
        else:
            print("结论----接受原假设,近期数据的均值与假设历史数据的均值相同----")

        # 单边检验找下限
        result_df3 = pd.DataFrame()
        index = 1
        for rate in np.arange(0.1, 1, 0.01):
            t_stat, t_pval = stats.ttest_1samp(a=self.new_data, popmean=rate, alternative='greater')
            result_df3.loc[index, 'rate'] = rate
            result_df3.loc[index, 't_stat'] = t_stat
            result_df3.loc[index, 'pval'] = t_pval
            index += 1
        rate_down_t = result_df3[result_df3['pval'] >= 0.05].iloc[0]['rate']
        print("T检验打牌概率下限:", round(rate_down_t, 2))
        # 单边检验找上限
        result_df4 = pd.DataFrame()
        index_2 = 1
        for rate in np.arange(0.1, 1, 0.01):
            t_stat, t_pval = stats.ttest_1samp(a=self.new_data, popmean=rate, alternative='less')
            result_df4.loc[index_2, 'rate'] = rate
            result_df4.loc[index_2, 't_stat'] = t_stat
            result_df4.loc[index_2, 'pval'] = t_pval
            index_2 += 1
        rate_ceil_t = result_df4[result_df4['pval'] >= 0.05].iloc[-1]['rate']
        print("T检验打牌概率上限:", round(rate_ceil_t, 2))
        print('近期打牌均值为:', self.mean_new)

    def draw_bar(self):
        draw_data = pd.read_excel(io=self.path)
        need_data = draw_data[draw_data['play'] == 1][["时间", "play", "输赢", "金额"]]
        # 检查数据录入是否有错误
        for index in need_data.index:
            temp = need_data.loc[index, "输赢"] * need_data.loc[index, "金额"]
            if temp < 0:
                print('数据录入错误的时间为')
                print(need_data.loc[index, "时间"])
                raise ValueError("数据录入错误")

        my_xlabels = [i.strftime("%Y-%m-%d") for i in need_data["时间"]]
        play_numbers = len(need_data)
        win_times = len(need_data[need_data["输赢"] == 1])
        loose_times = len(need_data[need_data["输赢"] == -1])
        win_rate = str(round((win_times / play_numbers) * 100, 2)) + '%'
        total_money = round(need_data["金额"].sum())
        plt.rcParams['font.sans-serif'] = ['SimHei']
        plt.rcParams['axes.unicode_minus'] = False  # 显示负号
        plt.figure(figsize=(10, 8))
        plt.bar(my_xlabels, need_data['金额'], color=np.where(need_data['金额'] > 0, 'blue', 'firebrick'), alpha=0.8,
                width=0.5)
        plt.xticks(rotation=90)
        plt.axhline(y=0, color='black', linestyle='--')
        plt.title("近期打麻将记录\n" + f"总次数{play_numbers}次,赢{win_times}场,输{loose_times}场,胜率{win_rate},金额输赢为{total_money}元",
                  fontsize=20)
        #  数据标注
        for a, b in zip(my_xlabels, need_data['金额']):
            plt.text(a, np.where(b > 0, b + 100, b - 200), '%d' % b, ha='center', va='bottom', fontsize=10)
        plt.show()

    def get_result(self):
        self.calculate_info()
        if self.use_z:
            self.try_z()
        if self.use_t:
            self.try_t()
        if self.if_draw:
            self.draw_bar()


if __name__ == '__main__':
    # make_data = [1 if i %7==1 else 0 for i in range(365)]
    # dm = DaMajiang(use_z=True,if_draw=False,raw_data=make_data)
    dm = DaMajiang(use_z=True)
    dm.get_result()

# 输出内容

不犯取伪错误的情况下,样本量至少要 62 个
目前采集的样本数 51
----运用Z检验进行验证----
----开始验证近期打牌概率,与之前是否相同----
z-test: -2.0932827432500782 p-value: 0.036323922190464236
原本假设周六周日打牌,根据近期观察的情况判断,在假设历史打牌概率为0.29的情况下
经过观察记录近期打牌的次数,这一系列事件的发生概率为 0.0363
结论----拒绝原假设,近期数据的均值与假设历史数据的均值不同----
打牌概率下限: 0.32
打牌概率上限: 0.54
近期打牌均值为: 0.43137254901960786
--------------------------------------------------
----运用T检验进行验证----
验证近期数据是否与往期数据发生改变
t-test: -2.0932827432500782 p-value: 0.03693189558919915
原本假设周六周日打牌,根据近期观察的情况判断,在假设历史打牌概率为0.29的情况下
经过观察记录近期打牌的次数,这一系列事件的发生概率为 0.0369
结论----拒绝原假设,近期数据的均值与假设历史数据的均值不同----
T检验打牌概率下限: 0.32
T检验打牌概率上限: 0.54
近期打牌均值为: 0.43137254901960786

posted @ 2024-10-16 00:26    阅读(14)  评论(0)    收藏  举报  来源