【12月DW打卡】joyful-pandas

第九章 pandas 类别型数据(categories)

小结

纠错

  1. 二、有序分类1. 序的建立中出现无需类别错别字,应为有序类别。 地址:https://datawhalechina.github.io/joyful-pandas/build/html/目录/ch9.html

导包

import numpy as np
import pandas as pd
pd.__version__
'1.2.0'
file_prefix = 'E:\PycharmProjects\DatawhaleChina\joyful-pandas\data\'
print(file_prefix)
E:PycharmProjectsDatawhaleChinajoyful-pandasdata

一、cat对象

1. cat对象的属性

在 pandas 中提供了 category 类型,使用户能够处理分类类型的变量,将一个普通序列转换成分类变量可以使用 astype 方法。

df = pd.read_csv(file_prefix+'learn_pandas.csv', usecols = ['Grade', 'Name', 'Gender', 'Height', 'Weight'])

s = df.Grade.astype('category')
s.head()
0     Freshman
1     Freshman
2       Senior
3    Sophomore
4    Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman', 'Junior', 'Senior', 'Sophomore']

在一个分类类型的 Series 中定义了 cat 对象,它和上一章中介绍的 str 对象类似,定义了一些属性和方法来进行分类类别的操作。
对于一个具体的分类,有两个组成部分,其一为类别的本身,它以 Index 类型存储,其二为是否有序,它们都可以通过 cat 的属性被访问:

s.cat.categories
Index(['Freshman', 'Junior', 'Senior', 'Sophomore'], dtype='object')
s.cat.ordered
False

另外,每一个序列的类别会被赋予唯一的整数编号,它们的编号取决于 cat.categories 中的顺序,该属性可以通过 codes 访问:

s.cat.codes.head()
0    0
1    0
2    2
3    3
4    3
dtype: int8

2. 类别类型的增加、删除和修改

类别不得直接修改

在第三章中曾提到,索引 Index 类型是无法用 index_obj[0] = item 来修改的,而 categories 被存储在 Index 中,因此 pandas 在 cat 属性上定义了若干方法来达到相同的目的。
# 首先,对于类别的增加可以使用 add_categories :
# 示例: 增加一个毕业生类别
s.cat.add_categories('Graduate')
0       Freshman
1       Freshman
2         Senior
3      Sophomore
4      Sophomore
         ...    
195       Junior
196       Senior
197       Senior
198       Senior
199    Sophomore
Name: Grade, Length: 200, dtype: category
Categories (5, object): ['Freshman', 'Junior', 'Senior', 'Sophomore', 'Graduate']

若要删除某一个类别可以使用 remove_categories ,同时所有原来序列中的该类会被设置为缺失。例如,删除大一的类别:

s.cat.remove_categories('Freshman')

0            NaN
1            NaN
2         Senior
3      Sophomore
4      Sophomore
         ...    
195       Junior
196       Senior
197       Senior
198       Senior
199    Sophomore
Name: Grade, Length: 200, dtype: category
Categories (3, object): ['Junior', 'Senior', 'Sophomore']

此外可以使用 set_categories 直接设置序列的新类别,原来的类别中如果存在元素不属于新类别,那么会被设置为缺失。

s.cat.set_categories(['Sophomore','PhD']) # 新类别为大二学生和博士

0            NaN
1            NaN
2            NaN
3      Sophomore
4      Sophomore
         ...    
195          NaN
196          NaN
197          NaN
198          NaN
199    Sophomore
Name: Grade, Length: 200, dtype: category
Categories (2, object): ['Sophomore', 'PhD']

如果想要删除未出现在序列中的类别,可以使用 remove_unused_categories 来实现:

s = s.cat.remove_unused_categories() # 移除了未出现的博士生类别
s

0       Freshman
1       Freshman
2         Senior
3      Sophomore
4      Sophomore
         ...    
195       Junior
196       Senior
197       Senior
198       Senior
199    Sophomore
Name: Grade, Length: 200, dtype: category
Categories (4, object): ['Freshman', 'Junior', 'Senior', 'Sophomore']
s.cat.rename_categories({'Sophomore':'本科二年级学生'})

0      Freshman
1      Freshman
2        Senior
3       本科二年级学生
4       本科二年级学生
         ...   
195      Junior
196      Senior
197      Senior
198      Senior
199     本科二年级学生
Name: Grade, Length: 200, dtype: category
Categories (4, object): ['Freshman', 'Junior', 'Senior', '本科二年级学生']

有序的分类类型

有序类别和无序类别可以通过 as_unordered 和 reorder_categories 互相转化,需要注意的是后者传入的参数必须是由当前序列的无序类别构成的列表,不能够增加新的类别,也不能缺少原来的类别,并且必须指定参数 ordered=True ,否则方法无效。例如,对年级高低进行相对大小的类别划分,然后再恢复无序状态:

s = df.Grade.astype('category')
print('有序化:')
s_order = s.cat.reorder_categories(['Freshman', 'Sophomore',                              'Junior', 'Senior'], ordered=True)
s_order
有序化:





0       Freshman
1       Freshman
2         Senior
3      Sophomore
4      Sophomore
         ...    
195       Junior
196       Senior
197       Senior
198       Senior
199    Sophomore
Name: Grade, Length: 200, dtype: category
Categories (4, object): ['Freshman', 'Sophomore', 'Junior', 'Senior']
print('可以先用 s.cat.as_ordered() 转化为有序类别')
s_order2 = s.cat.as_ordered()
s_order2 = s_order2.cat.reorder_categories(['Freshman', 'Sophomore',                              'Junior', 'Senior'] )
s_order2
可以先用 s.cat.as_ordered() 转化为有序类别





0       Freshman
1       Freshman
2         Senior
3      Sophomore
4      Sophomore
         ...    
195       Junior
196       Senior
197       Senior
198       Senior
199    Sophomore
Name: Grade, Length: 200, dtype: category
Categories (4, object): ['Freshman' < 'Sophomore' < 'Junior' < 'Senior']
print('无序化:')
s_unorder = s.cat.as_unordered()
s_unorder
无序化:





0       Freshman
1       Freshman
2         Senior
3      Sophomore
4      Sophomore
         ...    
195       Junior
196       Senior
197       Senior
198       Senior
199    Sophomore
Name: Grade, Length: 200, dtype: category
Categories (4, object): ['Freshman', 'Junior', 'Senior', 'Sophomore']

2. 排序和比较

在第二章中,曾提到了字符串和数值类型序列的排序,此时就要说明分类变量的排序:只需把列的类型修改为 category 后,再赋予相应的大小关系,就能正常地使用 sort_index 和 sort_values 。例如,对年级进行排序:

df.Grade = df.Grade.astype('category')
df.Grade = df.Grade.cat.reorder_categories(['Freshman', 'Sophomore',                              'Junior', 'Senior'], ordered=True)
df.sort_values('Grade').head() # 值排序

Grade Name Gender Height Weight
0 Freshman Gaopeng Yang Female 158.9 46.0
105 Freshman Qiang Shi Female 164.5 52.0
96 Freshman Changmei Feng Female 163.8 56.0
88 Freshman Xiaopeng Han Female 164.1 53.0
81 Freshman Yanli Zhang Female 165.1 52.0
df.set_index('Grade').sort_index().head() # 索引排序

Name Gender Height Weight
Grade
Freshman Gaopeng Yang Female 158.9 46.0
Freshman Qiang Shi Female 164.5 52.0
Freshman Changmei Feng Female 163.8 56.0
Freshman Xiaopeng Han Female 164.1 53.0
Freshman Yanli Zhang Female 165.1 52.0

由于序的建立,因此就可以进行比较操作。

分类变量的比较操作分为两类,第一种是 == 或 != 关系的比较,比较的对象可以是标量或者同长度的 Series (或 list ),第二种是 >,>=,<,<= 四类大小关系的比较,比较的对象和第一种类似,但是所有参与比较的元素必须属于原序列的 categories ,同时要和原序列具有相同的索引。

# 测试相等
(df.Grade == 'Sophomore').head()

0    False
1    False
2    False
3     True
4     True
Name: Grade, dtype: bool
# 测试小于
( df.Grade <= 'Sophomore').head()
0     True
1     True
2    False
3     True
4     True
Name: Grade, dtype: bool

三、区间类别

1. 利用cut和qcut进行区间构造

pd.cut的说明

  • 最重要的参数是 bin ,如果传入整数 n ,则代表把整个传入数组的按照最大和最小值等间距地分为 n 段
  • 由于区间默认是左开右闭,需要进行调整把最小值包含进去,在 pandas 中的解决方案是在值最小的区间左端点再减去 0.001*(max-min) ,因此如果对序列 [1,2] 划分为2个箱子时,第一个箱子的范围 (0.999,1.5] ,第二个箱子的范围是 (1.5,2] 。
  • 如果需要指定左闭右开时,需要把 right 参数设置为 False ,相应的区间调整方法是在值最大的区间右端点再加上 0.001*(max-min) 。
s = pd.Series([1,2])
pd.cut(s, bins=2)
0    (0.999, 1.5]
1      (1.5, 2.0]
dtype: category
Categories (2, interval[float64]): [(0.999, 1.5] < (1.5, 2.0]]
print('设置左开右开')
pd.cut(s, bins=2, right=False)
设置左开右开





0      [1.0, 1.5)
1    [1.5, 2.001)
dtype: category
Categories (2, interval[float64]): [[1.0, 1.5) < [1.5, 2.001)]

bins 的另一个常见用法是指定区间分割点的列表(使用 np.infty 可以表示无穷大)

pd.cut(s, bins=[-np.infty, 1.2, 1.8, 2.2, np.infty])
0    (-inf, 1.2]
1     (1.8, 2.2]
dtype: category
Categories (4, interval[float64]): [(-inf, 1.2] < (1.2, 1.8] < (1.8, 2.2] < (2.2, inf]]

另外两个常用参数为 labels 和 retbins ,分别代表了区间的名字和是否返回分割点(默认不返回):
返回时,返回一个元组

pd.cut(s, bins=2, labels=['small', 'big'], retbins=True)

(0    small
 1      big
 dtype: category
 Categories (2, object): ['small' < 'big'], array([0.999, 1.5  , 2.   ]))

分位数分箱qcut

  • cut 和 cut 几乎没有差别,只是把 bins 参数变成的 q 参数, qcut 中的 q 是指 quantile 。
  • 这里的 q 为整数 n 时,指按照 n 等分位数把数据分箱,还可以传入浮点列表指代相应的分位数分割点。
pd.qcut(df.Weight, q=3).head()
0    (33.999, 48.0]
1      (55.0, 89.0]
2      (55.0, 89.0]
3    (33.999, 48.0]
4      (55.0, 89.0]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 48.0] < (48.0, 55.0] < (55.0, 89.0]]
pd.qcut(df.Weight, q=[0,0.2,0.8,1]).head()

0      (44.0, 69.4]
1      (69.4, 89.0]
2      (69.4, 89.0]
3    (33.999, 44.0]
4      (69.4, 89.0]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 44.0] < (44.0, 69.4] < (69.4, 89.0]]

2. 一般区间的构造( pd.Interval()对象和IntervalIndex())

对于某一个具体的区间而言,其具备三个要素,即左端点、右端点和端点的开闭状态,其中开闭状态可以指定 right, left, both, neither 中的一类(指定的一端为闭合):

my_interval = pd.Interval(0, 1, 'right')
my_interval
Interval(0, 1, closed='right')

pd.Interval()对象其属性包含了 mid, length, right, left, closed ,分别表示中点、长度、右端点、左端点和开闭状态。
使用 in 可以判断元素是否属于区间:

0.5 in my_interval
True

使用 overlaps 可以判断两个区间是否有交集:

my_interval_2 = pd.Interval(0.5, 1.5, 'left')
my_interval.overlaps(my_interval_2)
True

一般而言, pd.IntervalIndex 对象有四类方法生成,分别是 from_breaks, from_arrays, from_tuples, interval_range ,它们分别应用于不同的情况:

from_breaks 的功能类似于 cut 或 qcut 函数,只不过后两个是通过计算得到的风格点,而前者是直接传入自定义的分割点:

pd.IntervalIndex.from_breaks([1,3,6,10], closed='both')

IntervalIndex([[1, 3], [3, 6], [6, 10]],
              closed='both',
              dtype='interval[int64]')

from_arrays 是分别传入左端点和右端点的列表,适用于有交集并且知道起点和终点的情况:

pd.IntervalIndex.from_arrays(left = [1,3,6,10],
right = [5,4,9,11],
closed = 'neither')
IntervalIndex([(1, 5), (3, 4), (6, 9), (10, 11)],
              closed='neither',
              dtype='interval[int64]')

from_tuples 传入的是起点和终点元组构成的列表:

pd.IntervalIndex.from_tuples([(1,5),(3,4),(6,9),(10,11)],closed='neither')

IntervalIndex([(1, 5), (3, 4), (6, 9), (10, 11)],
              closed='neither',
              dtype='interval[int64]')

一个等差的区间序列由起点、终点、区间个数和区间长度决定,其中三个量确定的情况下,剩下一个量就确定了, interval_range 中的 start, end, periods, freq 参数就对应了这四个量,从而就能构造出相应的区间:
(四个参数全填并且填不对的话,会报错!)

# periods表示区间个数
pd.interval_range(start=1,end=5,periods=8)
IntervalIndex([(1.0, 1.5], (1.5, 2.0], (2.0, 2.5], (2.5, 3.0], (3.0, 3.5], (3.5, 4.0], (4.0, 4.5], (4.5, 5.0]],
              closed='right',
              dtype='interval[float64]')
# freq(单个区间的频率,区间长度)
pd.interval_range(end=5,periods=8,freq=0.5)
IntervalIndex([(1.0, 1.5], (1.5, 2.0], (2.0, 2.5], (2.5, 3.0], (3.0, 3.5], (3.5, 4.0], (4.0, 4.5], (4.5, 5.0]],
              closed='right',
              dtype='interval[float64]')

练一练

无论是 interval_range 还是下一章时间序列中的 date_range 都是给定了等差序列中四要素中的三个,从而确定整个序列。请回顾等差数列中的首项、末项、项数和公差的联系,写出 interval_range 中四个参数之间的恒等关系。

等差数列式子: an=a1+(n-1)*d

end = start + (periods) * freq

除此之外,如果直接使用 pd.IntervalIndex([...], closed=...) ,把 Interval 类型的列表组成传入其中转为区间索引,那么所有的区间会被强制转为指定的 closed 类型,因为 pd.IntervalIndex 只允许存放同一种开闭区间的 Interval 对象。

pd.IntervalIndex([my_interval, my_interval_2], closed='left')

IntervalIndex([[0.0, 1.0), [0.5, 1.5)],
              closed='left',
              dtype='interval[float64]')

3. 区间的属性与方法(IntervalIndex)

IntervalIndex 上也定义了一些有用的属性和方法。同时,如果想要具体利用 cut 或者 qcut 的结果进行分析,那么需要先将其转为该种索引类型:

id_interval = pd.IntervalIndex(pd.cut(s, 3))
id_interval
IntervalIndex([(0.999, 1.333], (1.667, 2.0]],
              closed='right',
              dtype='interval[float64]')

与单个 Interval 类型相似, IntervalIndex 有若干常用属性: left, right, mid, length ,分别表示左右端点、两端点均值和区间长度。

IntervalIndex 还有两个常用方法,包括 contains 和 overlaps ,分别指逐个判断每个区间是否包含某元素,以及是否和一个 pd.Interval 对象有交集。

id_interval.contains(4)
array([False, False])
id_interval.overlaps(pd.Interval(40,60))

array([False, False])

四、练习

Ex1:统计未出现的类别

在第五章中介绍了crosstab函数,在默认参数下它能够对两个列的组合出现的频数进行统计汇总:

df = pd.DataFrame({'A':['a','b','c','a'], 'B':['cat','cat','dog','cat']})
pd.crosstab(df.A, df.B)
B cat dog
A
a 2 0
b 1 0
c 0 1

但事实上有些列存储的是分类变量,列中并不一定包含所有的类别,此时如果想要对这些未出现的类别在crosstab结果中也进行汇总,则可以指定dropna参数为False

df.B = df.B.astype('category').cat.add_categories('sheep')
pd.crosstab(df.A, df.B, dropna=False)
B cat dog sheep
A
a 2 0 0
b 1 0 0
c 0 1 0

请实现一个带有dropna参数的my_crosstab函数来完成上面的功能。

"""
解析答案的思路:
1. 找出各自的类别种类集合
2. 各自累加
"""
def my_crosstab(s1, s2, dropna=True):
    idx1 = (s1.cat.categories if s1.dtype.name == 'category' and  not dropna else s1.unique())
    idx2 = (s2.cat.categories if s2.dtype.name == 'category' and  not dropna else s2.unique())
    res = pd.DataFrame(np.zeros((idx1.shape[0], idx2.shape[0])), index=idx1, columns=idx2)
    for i, j in zip(s1, s2):
        res.at[i, j] += 1
    res = res.rename_axis(index=s1.name, columns=s2.name).astype('int')
    return res
# 测试
my_crosstab(df.A, df.B, dropna=False)
B cat dog sheep
A
a 2 0 0
b 1 0 0
c 0 1 0

Ex2:钻石数据集

现有一份关于钻石的数据集,其中carat, cut, clarity, price分别表示克拉重量、切割质量、纯净度和价格,样例如下:

df = pd.read_csv(file_prefix + 'diamonds.csv')
df.head(3)
carat cut clarity price
0 0.23 Ideal SI2 326
1 0.21 Premium SI1 326
2 0.23 Good VS1 327
  1. 分别对df.cutobject类型和category类型下使用nunique函数,并比较它们的性能。
%timeit -n 30 df.astype('object').nunique()
20 ms ± 888 µs per loop (mean ± std. dev. of 7 runs, 30 loops each)
%timeit -n 30 df.astype('category').nunique()
10.8 ms ± 769 µs per loop (mean ± std. dev. of 7 runs, 30 loops each)
print('显而易见,category更快一些 ')
  1. 钻石的
    切割质量可以分为五个等级,由次到好分别是Fair, Good, Very Good, Premium, Ideal
    纯净度有八个等级,由次到好分别是I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF

请对切割质量按照由好到次的顺序排序,相同切割质量的钻石,按照纯净度进行由次到好的排序。

df['cut'] = df['cut'].astype('category').cat.reorder_categories(['Fair', 'Good', 'Very Good', 'Premium', 'Ideal'],ordered=True)
df.clarity = df.clarity.astype('category').cat.reorder_categories([
     'I1', 'SI2', 'SI1', 'VS2', 'VS1', 'VVS2', 'VVS1', 'IF'],ordered=True)
print('排序:')
df.sort_values(['cut', 'clarity'], ascending=[False, True])
排序:
carat cut clarity price
315 0.96 Ideal I1 2801
535 0.96 Ideal I1 2826
551 0.97 Ideal I1 2830
653 1.01 Ideal I1 2844
718 0.97 Ideal I1 2856
... ... ... ... ...
41242 0.30 Fair IF 1208
43778 0.37 Fair IF 1440
47407 0.52 Fair IF 1849
49683 0.52 Fair IF 2144
50126 0.47 Fair IF 2211

53940 rows × 4 columns

  1. 分别采用两种不同的方法,把cut, clarity这两列按照由好到次的顺序,映射到从0到n-1的整数,其中n表示类别的个数。
print('使用cat.codes映射索引')
df_copy = df.copy()
for name in ['cut', 'clarity']:
    df_copy[name] = df[name].astype('category').cat.codes

df_copy.head()
cut方法
carat cut clarity price
0 0.23 4 1 326
1 0.21 3 2 326
2 0.23 1 4 327
3 0.29 3 3 334
4 0.31 1 1 335
print('replace一个字典大发~~')
df_copy2 = df.copy()
for name in ['cut', 'clarity']:
    cat_cats = df[name].cat.categories
    df_copy2[name] = df[name].replace(
        dict(zip(cat_cats, np.arange(len(cat_cats))))
        )
df_copy2.head()
qcut方法
carat cut clarity price
0 0.23 4 1 326
1 0.21 3 2 326
2 0.23 1 4 327
3 0.29 3 3 334
4 0.31 1 1 335
  1. 对每克拉的价格按照分别按照分位数(q=[0.2, 0.4, 0.6, 0.8])与[1000, 3500, 5500, 18000]割点进行分箱得到五个类别Very Low, Low, Mid, High, Very High,并把按这两种分箱方法得到的category序列依次添加到原表中。
print('对每克拉的价格按照分位数(q=[0.2, 0.4, 0.6, 0.8])处理:')
q = [0, 0.2, 0.4, 0.6, 0.8, 1]
avg_price = df.price / df.carat
df['new_qcut'] = pd.qcut(avg_price, q=q, labels=['Very Low', 'Low', 'Mid', 'High', 'Very High'])

print('对每克拉的价格按照[1000, 3500, 5500, 18000]割点进行分箱:')
df['new_cut'] = pd.cut(avg_price, bins=[-np.infty, 1000, 3500, 5500, 18000, np.infty], labels=['Very Low', 'Low', 'Mid', 'High', 'Very High'])
df
对每克拉的价格按照分位数(q=[0.2, 0.4, 0.6, 0.8])处理:
对每克拉的价格按照[1000, 3500, 5500, 18000]割点进行分箱:
carat cut clarity price new_qcut new_cut
0 0.23 Ideal SI2 326 Very Low Low
1 0.21 Premium SI1 326 Very Low Low
2 0.23 Good VS1 327 Very Low Low
3 0.29 Premium VS2 334 Very Low Low
4 0.31 Good SI2 335 Very Low Low
... ... ... ... ... ... ...
53935 0.72 Ideal SI1 2757 Mid Mid
53936 0.72 Good SI1 2757 Mid Mid
53937 0.70 Very Good SI1 2757 Mid Mid
53938 0.86 Premium SI2 2757 Mid Low
53939 0.75 Ideal SI2 2757 Mid Mid

53940 rows × 6 columns

  1. 第4问中按照整数分箱得到的序列中,是否出现了所有的类别?如果存在没有出现的类别请把该类别删除。
df['new_cut'].unique()
['Low', 'Mid', 'High']
Categories (3, object): ['Low' < 'Mid' < 'High']
print('以下类别部分没有出现过:')
df.new_cut.cat.categories
以下类别没有出现过:





Index(['Very Low', 'Low', 'Mid', 'High', 'Very High'], dtype='object')
df.new_cut.cat.remove_categories(['Very Low', 'Very High'])
0        Low
1        Low
2        Low
3        Low
4        Low
        ... 
53935    Mid
53936    Mid
53937    Mid
53938    Low
53939    Mid
Name: new_cut, Length: 53940, dtype: category
Categories (3, object): ['Low' < 'Mid' < 'High']
  1. 对第4问中按照分位数分箱得到的序列,求每个样本对应所在区间的左右端点值和长度。
my_interval = pd.IntervalIndex(pd.qcut(avg_price, q=q))
my_interval
IntervalIndex([(1051.162, 2295.0], (1051.162, 2295.0], (1051.162, 2295.0], (1051.162, 2295.0], (1051.162, 2295.0] ... (3073.293, 4031.683], (3073.293, 4031.683], (3073.293, 4031.683], (3073.293, 4031.683], (3073.293, 4031.683]],
              closed='right',
              dtype='interval[float64]')
print('每个样本对应所在区间的左右端点值和长度:')
my_interval.left
每个样本对应所在区间的左右端点值和长度:





Float64Index([1051.162, 1051.162, 1051.162, 1051.162, 1051.162, 1051.162,
              1051.162, 1051.162, 1051.162, 1051.162,
              ...
              3073.293, 3073.293, 3073.293, 3073.293, 3073.293, 3073.293,
              3073.293, 3073.293, 3073.293, 3073.293],
             dtype='float64', length=53940)
my_interval.right
Float64Index([  2295.0,   2295.0,   2295.0,   2295.0,   2295.0,   2295.0,
                2295.0,   2295.0,   2295.0,   2295.0,
              ...
              4031.683, 4031.683, 4031.683, 4031.683, 4031.683, 4031.683,
              4031.683, 4031.683, 4031.683, 4031.683],
             dtype='float64', length=53940)
my_interval.length
Float64Index([         1243.838,          1243.838,          1243.838,
                       1243.838,          1243.838,          1243.838,
                       1243.838,          1243.838,          1243.838,
                       1243.838,
              ...
              958.3899999999999, 958.3899999999999, 958.3899999999999,
              958.3899999999999, 958.3899999999999, 958.3899999999999,
              958.3899999999999, 958.3899999999999, 958.3899999999999,
              958.3899999999999],
             dtype='float64', length=53940)
原文地址:https://www.cnblogs.com/zhazhaacmer/p/14243691.html