热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

基于R语言的卡方分箱

在做风控策略分析时,我们经常要对某个变量进行离散化,查看不同阶段的好坏用户分布情况,好的离散化方法可以让我们找到比较好的策略分界点。本片文章主要讲述卡方分箱一、理论基本思想:卡方分

在做风控策略分析时,我们经常要对某个变量进行离散化,查看不同阶段的好坏用户分布情况,好的离散化方法可以让我们找到比较好的策略分界点。
本片文章主要讲述卡方分箱

一、理论

基本思想:卡方分箱是依赖于 卡方检验的分箱方法,在统计指标上选择卡方统计量(chi-Square)进行判别。卡方分箱的基本思想是判断相邻的两个区间是否有分布差异,如果两个相邻的区间具有非常类似的分布,则这两个区间可以合并;否则,它们应当保持分开。基于卡方统计量的结果进行自下而上的合并,直到满足分箱的限制条件为止。

卡方分箱的实现步骤:

1. 预先设定一个卡方的阈值或者分箱个数的阈值。

在做分箱处理时可以使用两种限制条件:

  • 分箱个数:限制最终的分箱个数结果,每次将样本中具有最小卡方值的区间与相邻的最小卡方区间进行合并,直到分箱个数达到限制条件为止。

  • 卡方阈值:根据自由度和显著性水平得到对应的卡方阈值,如果分箱的各区间最小卡方值小于卡方阈值,则继续合并,直到最小卡方值超过设定阈值为止。

通过显著性水平和自由度计算出这个阈值,然后数据的卡方值与这个阈值进行比较,如果卡方值大于阈值,就可以推翻原假设(两个相邻区间的分布无差异);如果卡方值小于阈值,则不能推翻原假设(两个相邻区间的分布无差异),即可合并。

  • 显著性水平,当置信度90%时显著性水平为10%,ChiMerge算法推荐使用置信度为0.90、0.95、0.99。

  • 自由度,比分类数量小1。例如:有3类,自由度为2。

类别和属性独立时,有90%的可能性,计算得到的卡方值会小于4.6(在excel中用CHIINV(0.1,2)算出)。大于阈值4.6的卡方值就说明属性和类不是相互独立的,不能合并。如果阈值选的大,区间合并就会进行很多次,离散后的区间数量少、区间大。

2. 初始化:根据要离散化的数据对实例进行排序,每个实例属于一个区间

3. 合并区间:

  1. 计算每一对相邻区间的卡方值

  2. 将卡方值最小的一对区间合并(合并需要符合以下两个条件之一)

《基于R语言的卡方分箱》 image.png

4.评估指标

分完箱之后需要评估,常用的评估手段是计算出WOE和IV值。对于WOE和IV值的含义,看 数据挖掘模型中的IV和WOE详解

分箱的注意点

对于连续型变量,

• 使用ChiMerge进行分箱(默认分成5个箱)

• 检查分箱后的bad rate单调性;倘若不满足,需要进行相邻两箱的合并,直到bad rate为止

• 上述过程是收敛的,因为当箱数为2时,bad rate自然单调

• 分箱必须覆盖所有训练样本外可能存在的值!

• 原始值很多时,为了减小时间的开销,通常选取较少(例如50个)初始切分点。但是要注意分布不均匀!

对于类别型变量,

• 当类别数较少时,原则上不需要分箱

• 当某个或者几个类别的bad rate为0时,需要和最小的非0 的bad rate的箱进行合并

• 当该变量可以完全区分目标变量时,需要认真检查该变量的合理性

要求分箱完之后:

(1)不超过5箱

(2)Bad Rate单调

(3)每箱同时包含好坏样本

(4)特殊值如-1,单独成一箱

连续型变量可直接分箱

类别型变量:

(a)当取值较多时,先用bad rate编码,再用连续型分箱的方式进行分箱

(b)当取值较少时:

(b1)如果每种类别同时包含好坏样本,无需分箱

(b2)如果有类别只包含好坏样本的一种,需要合并

二、代码

2.1 R包–discretization

discretization包,是一个用来做有监督离散化的工具集,主要用于卡方分箱算法,它提供了几种常用的离散化工具函数,可以按照自上而下或自下而上,实施离散化算法。

项目主页: https://cran.r-project.org/web/packages/discretization/

提供了几个主要的离散化的工具函数:

  • chiM,ChiM算法进行离散化

  • chi2, Chi2算法进行离散化,在chiM的基础上进行优化

  • mdlp,最小描述长度原理(MDLP)进行离散化

  • modChi2,改进的Chi2方法离散数值属性

  • disc.Topdown,自上而下的离散化

  • extendChi2,扩展Chi2算法离散数值属性

chiM算法进行离散化(根据卡方阈值来设定合并停止条件)

ChiM()函数,使用ChiMerge算法基于卡方检验进行自下而上的合并。通过卡方检验判断相邻阈值的相对类频率,是否有明显不同,或者它们是否足够相似,从而合并为一个区间。
chiM(data,alpha)函数解读。
* 第一个参数data,是输入数据集,要求最后一列是分类属性。
* 第二个参数alpha,表示显著性水平。
* 自由度,通过数据计算获得是2,一共3个分类减去1。

《基于R语言的卡方分箱》 image.png

2.2 自定义函数ChiMerge

#初始化划分
SplitData <- function(df,col,numOfSplit,special_attribute=NULL){
library(dplyr)
#当连续变量的初始取值集合太多时(>100),我们先对其进行初步划分
#:param df: 按照col排序后的数据集
#:param col: 待分箱的变量
#:param numOfSplit: 切分的组别数
#:param special_attribute: 在切分数据集的时候,某些特殊值需要排除在外
#:return: 在原数据集上增加一列,把原始细粒度的col重新划分成粗粒度的值,便于分箱中的合并处理
df2 <- df
if(length(special_attribute)>0){
df2 <- filter(df,! col %in% special_attribute)
}
N <- dim(df2)[1] #行数
n <- floor(N/numOfSplit) #每组样本数
splitPointIndex <- seq(1,numOfSplit-1,1)*n #分割点的下标
rawValues <- sort(df2[,col]) #对取值进行升序排序
splitPoint <- rep(0,length(rawValues))
for(i in splitPointIndex){
splitPoint[i] <- rawValues[i] #分割点的取值
}
splitPoint <- sort(unique(splitPoint)) #对取值进行升序排序
if(splitPoint[1]==0){
splitPoint<- splitPoint[-1]
}
return(splitPoint)
}
#计算每个值的好坏比率
BinBadRate <- function(df,col,target,grantRateIndicator=0){
library(dplyr)
#:param df:需要计算好坏比率的数据集
#:param col:需要计算好坏比率的特征
#:param target:好坏标签
#:param grantRateIndicator:1返回总体的坏样本率,0不返回
#:return:每箱的坏样本率以及总体的坏样本率(当grantRateIndicator=1时)
#total <- df %>% group_by(col) %>% summarise(total=n())
#bad <- df %>% group_by(col) %>% summarise(bad=sum(target))
total <- data.frame(table(df[,col]))
names(total) <- c(col,'total')
bad <- data.frame(tapply(df[,target],df[,col],FUN = sum))
bad$bad <- row.names(bad)
names(bad) <- c('bad',col)
regroup <- left_join(total,bad,by=col)
#regroup$bad_rate <- bad/total
regroup <- mutate(regroup,bad_rate = bad/total)
dicts <- regroup[,'bad_rate'] #每箱对应的坏样本率组成的向量
names(dicts) <- regroup[,col]
if(grantRateIndicator==0){
return(list(dicts,regroup))
}else{
N =sum(regroup[,'total'])
B = sum(regroup[,'bad'])
overallRate = B*1.0/N
return(list(dicts,regroup,overallRate))
}
}
#计算卡方值
Chi2 <- function(df,total_col,bad_col){
library(dplyr)
df2 <- df
# 求出df中,总体的坏样本率和好样本率
badRate <- sum(df2[,bad_col])/sum(df2[,total_col])
# 当全部样本只有好或者坏样本时,卡方值为0
if(badRate %in% c(0,1)){
return(0)
}
good=df2[,total_col]-df2[,bad_col]
df2 <- cbind(df2,good)
goodRate = sum(df2[,'good'])/ sum(df2[,total_col])
# 期望坏(好)样本个数=全部样本个数*平均坏(好)样本占比
badExpected=df2[,total_col]*badRate
goodExpected=df2[,total_col]*goodRate
df2 <- cbind(df2,badExpected)
df2 <- cbind(df2,goodExpected)
badChi <- sum(((df2[,bad_col]-df2[,'badExpected'])^2)/df2[,'badExpected'])
goodChi <- sum(((df2[,'good']-df2[,'goodExpected'])^2)/df2[,'goodExpected'])
chi2 <- badChi+goodChi
return(chi2)
}
AssignBin <- function(x,cutOffPoints,special_attribute=NULL){
# :param x: 某个变量的某个取值
# :param cutOffPoints:上述变量的分箱结果,用切分点表示
# :param special_attribute:不参与分箱的特殊取值
# :return:分箱后的对应的第几个箱,从0开始
# for example, if cutOffPoints = c(10,20,30), if x = 7, return Bin 0. If x = 35, return Bin 3

numBin = length(cutOffPoints)+1+length(special_attribute)
if(x %in% special_attribute){
i <- which(special_attribute==x)
return(paste('Bin',0-i))
}
if(x<= cutOffPoints[1]){
return('Bin 0')
}else if(x>cutOffPoints[length(cutOffPoints)]){
return(paste("Bin",numBin-1))
}else{
for(i in seq(1,numBin-1)){
if(cutOffPoints[i] return(paste('Bin',i))
}
}
}
}
AssignGroup <- function(x,bin){
# '
# :param x:某个变量的某个取值
# :param bin:上述变量的分箱结果
# :return:x在分箱结果下的映射
#
N = length(bin)
if(x<=min(bin)){
return(min(bin))
}else if(x>max(bin)){
return(10e10)
}else{
for(i in 1:N-1){
if(bin[i] return(bin[i+1])
}
}
}
}
ChiMerge <- function(df,col,target,max_interval=5,special_attribute=NULL,minBinPcnt=0,numOfSplit=100){
# '''
# 通过指定最大分箱数,使用卡方值分割连续变量
# :param df:包含目标变量和分箱变量的数据框
# :param target:目标变量,取值0或1
# :param col:需要分箱的变量
# :param max_interval:最大分箱数,如果原始变量的取值个数低于该参数,不执行这个函数
# :param special_attribute:不参与分箱的变量取值,注意:必须是向量形式
# :param minBinPcnt:最小箱的占比,默认为0
# :param numOfSplit:当连续变量的初始取值集合太多时(>100),我们先对其进行初步划分,切分的组别数
# :return :分箱结果
# '''
library(dplyr)
colLevels=sort(unique(df[,col])) #升序排序变量值
N_distinct = length(colLevels) #不同取值的个数
if(N_distinct<=max_interval){ #如果原始变量的取值个数低于max_interval,不执行这个函数
print(paste(col,'变量的取值个数低于设定的最大分箱数max_interval:',max_interval))
return(colLevels[-length(colLevels)]) #去掉最后一个值
}else{
if(length(special_attribute)>=1){
df1 <- filter(df,col %in% special_attribute)
df2 <- filter(df,!col %in% special_attribute)
}else{
df2 <- df
} N_distinct <- length(unique(df2[,col])) #该变量的不同取值个数
#步骤一:通过col对数据集进行分组,求出每组的总样本数和坏样本数
if(N_distinct>numOfSplit){
split_x <- SplitData(df2,col,numOfSplit)
#temp <- cut(df2[,col],breaks = split_x,include.lowest = TRUE)
temp <- apply(df2[col],1,AssignGroup,split_x)
df2 <- cbind(df2,temp)
}else{
temp <- df2[,col]
df2 <- cbind(df2,temp)
}
#总体bad rate将被用来计算expected bad count
ha <- BinBadRate(df2,'temp',target)
regroup <- ha[[2]]
binBadRate<- ha[[1]]
#首先,每个单独的属性值将被分为单独的一组
#对属性值进行排序,然后两两组别进行合并
colLevels<- sort(unique(df2[,'temp'])) groupIntervals <- list()
for(i in 1:length(colLevels)){
groupIntervals[i] <-list(colLevels[i])
} # #步骤二,建立循环,不断合并最优的相邻的两个组别,直到:
# #1.最终分裂出来的分箱数<=预设的最大分箱数
# #2.每箱的占比不低于预设值(可选)
# #3.每箱同时包含好坏样本
# #如果有特殊属性,那么最终分裂出来的分箱数=预设的最大分箱数-特殊属性的个数
split_intervals= max_interval-length(special_attribute)
while(length(groupIntervals)>=split_intervals){ #终止条件
#每次循环时,计算合并相邻组别后的卡方值。具有最小卡方值值的合并方案,是最优方案
chisqList <- rep(100000000,length(groupIntervals)-1)
for(k in 1:(length(groupIntervals)-1)){
temp_group <- c(groupIntervals[[k]],groupIntervals[[k+1]])
df2b <- filter(regroup, temp %in% temp_group)
chisq = Chi2(df2b,'total','bad')
chisqList[k] <- chisq
}
best_combined <- order(chisqList)[1] #找到最小值的位置
#合并
groupIntervals[[best_combined]] = c(groupIntervals[[best_combined]],groupIntervals[[best_combined+1]])
# after combining two intervals, we need to remove one of them
groupIntervals[[best_combined+1]] <- NULL
} for(i in 1:length(groupIntervals)){
groupIntervals[[i]]<- sort(groupIntervals[[i]])
} cutOffPoints <- rep(0,length(groupIntervals)-1)
for(i in 1:(length(groupIntervals)-1)){
cutOffPoints[i] <- max(groupIntervals[[i]])
} # 检查是否有箱没有好或者坏样本。如果有,需要跟相邻的箱进行合并,直到每箱同时包含好坏样本
groupedvalues <- apply(df2['temp'],1,AssignBin,cutOffPoints,special_attribute)
temp_Bin<-groupedvalues
df2 <- cbind(df2,temp_Bin)
#返回(每箱坏样本率列表,和包含“列名、坏样本数、总样本数、坏样本率的数据框”)
middle <- BinBadRate(df2,'temp_Bin',target)
binBadRate <- middle[[1]]
regroup <- middle[[2]]
minBadRate <- min(binBadRate)
maxBadRate <- max(binBadRate)
while(minBadRate ==0 || maxBadRate == 1){
# 找出全部为好/坏样本的箱
indexForBad01 <- filter(regroup,bad_rate %in% c(0,1))[,'temp_Bin']
bin <- indexForBad01[1]
return(bin)
# 如果是最后一箱,则需要和上一个箱进行合并,也就意味着分裂点cutOffPoints中的最后一个需要移除
if(bin==max(regroup[,'temp_Bin'])){
cutOffPoints <- cutOffPoints[1:length(cutOffPoints)-1]
}else if(bin == min(regroup[,'temp_Bin'])){
# 如果是第一箱,则需要和下一个箱进行合并,也就意味着分裂点cutOffPoints中的第一个需要移除
cutOffPoints[1] <- NULL
}else{
# 如果是中间的某一箱,则需要和前后中的一个箱进行合并,依据是较小的卡方值
# 和前一箱进行合并,并且计算卡方值
currentIndex <- which(regroup[,'temp_Bin']==bin)
prevIndex <- regroup[,'temp_Bin'][currentIndex - 1]
df3 <- filter(df2,temp_Bin %in% c(prevIndex,bin))
middle <- BinBadRate(df3, 'temp_Bin', target)
binBadRate <- middle[[1]]
df2b <- middle[[2]]
chisq1 = Chi2(df2b, 'total', 'bad')
# 和后一箱进行合并,并且计算卡方值
laterIndex <- regroup[,'temp_Bin'][currentIndex + 1]
df3b <- filter(df2,temp_Bin %in% c(prevIndex,bin))
middle <- BinBadRate(df3b, 'temp_Bin', target)
binBadRate <- middle[[1]]
df2b <- middle[[2]]
chisq2 = Chi2(df2b, 'total', 'bad')
if(chisq1 cutOffPoints[currentIndex - 1] <- NULL
}else{cutOffPoints[currentIndex] <- NULL}
}
# 完成合并之后,需要再次计算新的分箱准则下,每箱是否同时包含好坏样本
groupedvalues <- apply(df2['temp'],1,AssignBin,cutOffPoints,special_attribute)
temp_Bin = groupedvalues
df2 <- cbind(df2,temp_Bin)
middle <- BinBadRate(df2, 'temp_Bin', target)
binBadRate <- middle[[1]]
regroup <- middle[[2]]
minBadRate <- min(binBadRate)
maxBadRate <- maxmax(binBadRate)
}
if(minBinPcnt > 0){
groupedvalues <- apply(df2['temp'],1,AssignBin,cutOffPoints,special_attribute)
temp_Bin = groupedvalues
df2 <- cbind(df2,temp_Bin)
valueCounts <- data.frame(table(groupedvalues))
names(valueCounts)[2] <- 'temp'
pcnt=valueCounts[,'temp']/sum(valueCounts[,'temp'])
valueCounts <- cbind(valueCounts,pcnt)
valueCounts <- arrange(valueCounts,Var1)
minPcnt = min(valueCounts[,'pcnt'])
while(minPcnt 2){
# 找出占比最小的箱
indexForMinPcnt = filter(valueCounts,valueCounts[,'pcnt'] == minPcnt)[,'var1'][1]
# 如果占比最小的箱是最后一箱,则需要和上一个箱进行合并,也就意味着分裂点cutOffPoints中的最后一个需要移除
if(indexForMinPcnt==max(valueCounts[,'var1'])){
cutOffPoints[length(cutOffPoints)] <- NULL
}else if(indexForMinPcnt==min(valueCounts[,'var1'])){
# 如果占比最小的箱是第一箱,则需要和下一个箱进行合并,也就意味着分裂点cutOffPoints中的第一个需要移除
cutOffPoints[1] <- NULL
}else{
# 如果占比最小的箱是中间的某一箱,则需要和前后中的一个箱进行合并,依据是较小的卡方值
# 和前一箱进行合并,并且计算卡方值
currentIndex <- which(valueCounts[,'pcnt']==indexForMinPcnt)
prevIndex <- valueCounts[,'var1'][currentIndex-1]
df3 <- filter(df2,var1 %in% c(prevIndex, indexForMinPcnt))
middle <- BinBadRate(df3, 'temp_Bin', target)
binBadRate <- middle[[1]]
df2b <- middle[[2]]
chisq1 = Chi2(df2b, 'total', 'bad')
# 和后一箱进行合并,并且计算卡方值
laterIndex <- valueCounts[,'var1'][currentIndex-1]
df3b <- filter(df2,temp_Bin %in% c(laterIndex, indexForMinPcnt))
middle <- BinBadRate(df3b, 'temp_Bin', target)
binBadRate <- middle[[1]]
df2b <- middle[[2]]
chisq2 = Chi2(df2b, 'total', 'bad')
if(chisq1 cutOffPoints[currentIndex - 1] <- NULL
}else{cutOffPoints[currentIndex] <- NULL}
}
groupedvalues <- apply(df2['temp'],1,AssignBin,cutOffPoints,special_attribute)
temp_Bin = groupedvalues
df2 <- cbind(df2,temp_Bin)
valueCounts <- data.frame(table(groupedvalues))
names(valueCounts)[2] <- 'temp'
pcnt=valueCounts[,'temp']/sum(valueCounts[,'temp'])
valueCounts <- cbind(valueCounts,pcnt)
valueCounts <- arrange(valueCounts,Var1)
minPcnt = min(valueCounts[,'pcnt'])
}
}
cutOffPoints = c(special_attribute , cutOffPoints)
return(cutOffPoints)
}
}

《基于R语言的卡方分箱》 image.png

2. 3自定义并行化分箱函数

有时候数据量大的时候卡方分箱的计算大会导致运行速度慢,所以我们可以合理利用我们电脑的多核

《基于R语言的卡方分箱》 image.png


推荐阅读
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 在探讨Hibernate框架的高级特性时,缓存机制和懒加载策略是提升数据操作效率的关键要素。缓存策略能够显著减少数据库访问次数,从而提高应用性能,特别是在处理频繁访问的数据时。Hibernate提供了多层次的缓存支持,包括一级缓存和二级缓存,以满足不同场景下的需求。懒加载策略则通过按需加载关联对象,进一步优化了资源利用和响应时间。本文将深入分析这些机制的实现原理及其最佳实践。 ... [详细]
  • 本文介绍了如何在iOS平台上使用GLSL着色器将YV12格式的视频帧数据转换为RGB格式,并展示了转换后的图像效果。通过详细的技术实现步骤和代码示例,读者可以轻松掌握这一过程,适用于需要进行视频处理的应用开发。 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 应用链时代,详解 Avalanche 与 Cosmos 的差异 ... [详细]
  • 解决问题:1、批量读取点云las数据2、点云数据读与写出3、csf滤波分类参考:https:github.comsuyunzzzCSF论文题目ÿ ... [详细]
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 深入解析Struts、Spring与Hibernate三大框架的面试要点与技巧 ... [详细]
  • 如何使用 `org.apache.tomcat.websocket.server.WsServerContainer.findMapping()` 方法及其代码示例解析 ... [详细]
  • 在ElasticStack日志监控系统中,Logstash编码插件自5.0版本起进行了重大改进。插件被独立拆分为gem包,每个插件可以单独进行更新和维护,无需依赖Logstash的整体升级。这不仅提高了系统的灵活性和可维护性,还简化了插件的管理和部署过程。本文将详细介绍这些编码插件的功能、配置方法,并通过实际生产环境中的应用案例,展示其在日志处理和监控中的高效性和可靠性。 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • 本文详细介绍了定时器输入捕捉技术的原理及其应用。通过配置定时器通道的引脚模式为输入模式,并设置相应的捕获触发条件,可以实现对外部信号的精确捕捉。该技术在实时控制系统中具有广泛的应用,如电机控制、频率测量等场景。文中还提供了具体的配置步骤和示例代码,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 本文将继续探讨 JavaScript 函数式编程的高级技巧及其实际应用。通过一个具体的寻路算法示例,我们将深入分析如何利用函数式编程的思想解决复杂问题。示例中,节点之间的连线代表路径,连线上的数字表示两点间的距离。我们将详细讲解如何通过递归和高阶函数等技术实现高效的寻路算法。 ... [详细]
  • 在Android 4.4系统中,通过使用 `Intent` 对象并设置动作 `ACTION_GET_CONTENT` 或 `ACTION_OPEN_DOCUMENT`,可以从相册中选择图片并获取其路径。具体实现时,需要为 `Intent` 添加相应的类别,并处理返回的 Uri 以提取图片的文件路径。此方法适用于需要从用户相册中选择图片的应用场景,能够确保兼容性和用户体验。 ... [详细]
  • 在前文探讨了Spring如何为特定的bean选择合适的通知器后,本文将进一步深入分析Spring AOP框架中代理对象的生成机制。具体而言,我们将详细解析如何通过代理技术将通知器(Advisor)中包含的通知(Advice)应用到目标bean上,以实现切面编程的核心功能。 ... [详细]
author-avatar
推球了
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有