pandas常用数据处理函数整理

pandas数据处理常用函数整理

参考:《joyfulpandas》

数据下载:https://www.heywhale.com/mw/dataset/625d2653e22b670017093353/file

分组

# 分组
# 1.分组模式及其对象
# 1.1分组的一般模式
# 想要实现分组操作,必须明确三个要素:分组一句、数据来源、操作及其返回结果
# df.groupby(分组依据)[数据来源].使用操作
df = pd.read_csv('data/learn_pandas.csv')
# 按照性别统计身高中位数
df.groupby('Gender')['Longevity'].mean()
# 1.2分组依据的本质
# 如果现在需要根据多个维度进行分组,该如何做?事实上,只需在 groupby 中传入相应列名构成的列表即可。
# 例如,现想根据学校和性别进行分组,统计身高的均值就可以如下写出
df.groupby(['School', 'Gender'])['Height'].mean()
# 那如果想要通过一定的复杂逻辑来分组,例如根据学生体重是否超过总体均值来分组,同样还是计算身高的均值
# 分组条件
condition = df.Weight > df.Weight.mean()
df.groupby(condition)['Height'].mean()
# Weight
# False 159.034646
# True 172.705357
# Name: Height, dtype: float64

# 按照条件列表中元素的值(此处是 True 和 False )来分组
# 下面用随机传入字母序列来验证这一想法
item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()
# a 163.924242
# b 162.928814
# c 162.708621
# Name: Height, dtype: float64


# 此处的索引就是原先 item 中的元素,如果传入多个序列进入 groupby
# 那么最后分组的依据就是这两个序列对应行的唯一组合
df.groupby([condition, item])['Height'].mean()
# Weight
# False a 160.193617
# b 158.921951
# c 157.756410
# True a 173.152632
# b 172.055556
# c 172.873684
# Name: Height, dtype: float64

# 之前传入列名只是一种简便的记号,事实上等价于传入的是一个或多个列,最后分组的依据
# 来自于数据来源组合的 unique 值,通过 drop_duplicates 就能知道具体的组类别
df[['School', 'Gender']].drop_duplicates()
#                           School  Gender
# 0   Shanghai Jiao Tong University  Female
# 1               Peking University    Male
# 2   Shanghai Jiao Tong University    Male
# 3                Fudan University  Female
# 4                Fudan University    Male
# 5             Tsinghua University  Female
# 9               Peking University  Female
# 16            Tsinghua University    Male

df.groupby([df['School'], df['Gender']])['Height'].mean()
# School                         Gender
# Fudan University               Female    158.776923
#                                Male      174.212500
# Peking University              Female    158.666667
#                                Male      172.030000
# Shanghai Jiao Tong University  Female    159.122500
#                                Male      176.760000
# Tsinghua University            Female    159.753333
#                                Male      171.638889
# Name: Height, dtype: float64

# 1.3 Groupby对象
# 能够注意到,最终具体做分组操作时,所调用的方法都来自于 pandas 中的 groupby 对象,
# 这个对象上定义了许多方法,也具有一些方便的属性
gb = df.groupby(['School', 'Grade'])
# 通过 ngroups 属性,可以访问分为了多少组
gb.ngroups
# 16

# 通过 groups 属性,可以返回从 组名映射到 组索引列表的字典:
res = gb.groups
res.keys()  # 字典的值由于是索引,元素个数过多,此处只展示字典的键
# dict_keys([('Fudan University', 'Freshman'), ('Fudan University', 'Junior'), ('Fudan␣ , →University', 'Senior'), ('Fudan University', 'Sophomore'), ('Peking University', 'Freshman'), (
# ,
# →'Peking University', 'Junior'), ('Peking University', 'Senior'), ('Peking University', 'Sophomore
# ,
# →'), ('Shanghai Jiao Tong University', 'Freshman'), ('Shanghai Jiao Tong University', 'Junior'), (
# ,
# →'Shanghai Jiao Tong University', 'Senior'), ('Shanghai Jiao Tong University', 'Sophomore'), (
# ,
# →'Tsinghua University', 'Freshman'), ('Tsinghua University', 'Junior'), ('Tsinghua University',
# ,
# →'Senior'), ('Tsinghua University', 'Sophomore')])

# 当 size 作为 DataFrame 的属性时,返回的是表长乘以表宽的大小,
# 但在 groupby 对象上表示统计每个组的元素个数
gb.size()
# School                         Grade
# Fudan University               Freshman      9
#                                Junior       12
#                                Senior       11
#                                Sophomore     8
# Peking University              Freshman     13
#                                Junior        8
#                                Senior        8
#                                Sophomore     5
# Shanghai Jiao Tong University  Freshman     13
#                                Junior       17
#                                Senior       22
#                                Sophomore     5
# Tsinghua University            Freshman     17
#                                Junior       22
#                                Senior       14
#                                Sophomore    16
# dtype: int64

# 通过 get_group 方法可以直接获取所在组对应的行,此时必须知道组的具体名字
gb.get_group(('Fudan University', 'Freshman')).iloc[:3, :3]  # 展示一部分
#               School     Grade             Name
# 15  Fudan University  Freshman  Changqiang Yang
# 28  Fudan University  Freshman     Gaoqiang Qin
# 63  Fudan University  Freshman     Gaofeng Zhao

# 1.4 分组的三大操作 :聚合、变换和过滤 agg 、transform 和 filter
# • 第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量 size 等 •
# • 第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个 Series 类型
# • 第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了 DataFrame 类型


# 2.聚合函数
# 2.1 内置聚合函数
#  根 据 返 回 标 量 值 的 原 则, 包 括 如 下 函 数:
# max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod
gb = df.groupby('Gender')['Height']
gb.idxmin()
# Gender
# Female 143
# Male 199
# Name: Height, dtype: int64

gb.quantile(0.95)
# Gender
# Female 166.8
# Male 185.9
# Name: Height, dtype: float64

# 这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算

gb = df.groupby('Gender')[['Height', 'Weight']]
gb.max()
# Height Weight
# Gender
# Female 170.2 63.0
# Male 193.9 89.0

# 2.2 agg 方法
# groupby 对象上定义了许多方便的函数,但仍然有以下不便之处:
# • 无法同时使用多个函数
# • 无法对特定的列使用特定的聚合函数
# • 无法使用自定义的聚合函数
# • 无法直接对结果的列名在聚合前进行自定义命名

# 使用多个函数
gb.agg(['sum', 'skew'])  # 'idxmax',

# 对特定的列使用特定的聚合函数
# 可以通过构造字典传入 agg 中实现,其中字典以列名为键,以聚合字符串或字符串列表为值。
gb.agg({'Height': ['mean', 'max'], 'Weight': 'count'})
#            Height        Weight
#              mean    max  count
# Gender
# Female  159.19697  170.2    135
# Male    173.62549  193.9     54

# 使用自定义函数
# 在 agg 中可以使用具体的自定义函数,需要注意传入函数的参数是之前数据源中的列,逐列进行计算。
# 下面分组计算身高和体重的极差:
gb.agg(lambda x: x.mean() - x.min())


#           Height     Weight
# Gender
# Female  13.79697  13.918519
# Male    17.92549  21.759259

# 由于传入的是序列,因此序列上的方法和属性都是可以在函数中使用的,只需保证返回值是标量即可
# 下面 的例子是指,如果组的指标均值,超过该指标的总体均值,返回 High,否则返回 Low
def my_func(s):
    res = 'High'
    if s.mean() <= df[s.name].mean():
        res = 'Low'
    return res


gb.agg(my_func)
#        Height Weight
# Gender
# Female    Low    Low
# Male     High   High

# 聚合结果重命名
# 如果想要对结果进行重命名,只需要将上述函数的位置改写成元组,
# 元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数
gb.agg([('range', lambda x: x.max() - x.min()), ('my_sum', 'sum')])
#        Height          Weight
#         range   my_sum  range  my_sum
# Gender
# Female   24.8  21014.0   29.0  6469.0
# Male     38.2   8854.9   38.0  3929.0

gb.agg({'Height': [('my_func', my_func), 'sum'],
        'Weight': lambda x: x.max()})
#         Height            Weight
#        my_func      sum 
# Gender
# Female     Low  21014.0     63.0
# Male      High   8854.9     89.0


# 使用对一个或者多个列使用单个聚合的时候,重命名需要加方括号,
# 否则就不知道是新的名字还是手误输错的内置函数字符串
gb.agg([('my_sum', 'sum')])
#          Height  Weight
#          my_sum  my_sum
# Gender
# Female  21014.0  6469.0
# Male     8854.9  3929.0

gb.agg({'Height': [('my_func', my_func), 'sum'],
        'Weight': [('range', lambda x: x.max())]})
#         Height          Weight
#        my_func      sum  range
# Gender
# Female     Low  21014.0   63.0
# Male      High   8854.9   89.0


# 3.变换和过滤
# 3.1 变换函数与 transform 方法
# 变 换 函 数 的 返 回 值 为 同 长 度 的 序 列,
# 最 常 用 的 内 置 变 换 函 数 是 累 计 函 数:cumcount/cumsum/cumprod/cummax/cummin ,
# 它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。
# 此外在 groupby 对象上还定义了填充类和滑窗类的变换函数


# 当用自定义变换时需要使用 transform 方法,被调用的自定义函数,其传入值为数据源的序列,与 agg 的传
# 入类型是一致的,其最后的返回结果是行列索引与数据源一致的 DataFrame
gb.cummax().head()
#    Height  Weight
# 0   158.9    46.0
# 1   166.5    70.0
# 2   188.9    89.0
# 3     NaN    46.0
# 4   188.9    89.0

# 现对身高和体重进行分组标准化,即减去组均值后除以组的标准差
gb.transform(lambda x: (x-x.mean())/x.std()).head()
#      Height    Weight
# 0 -0.058760 -0.354888
# 1 -1.010925 -0.355000
# 2  2.167063  2.089498
# 3       NaN -1.279789
# 4  0.053133  0.159631


# 前面提到了 transform 只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所
# 在的整个组,这种 标量广播的技巧在特征工程中是非常常见的。例如,构造两列新特征来分别表示样本所在
# 性别组的身高均值和体重均值:
gb.transform('mean').head() # 传入返回标量的函数也是可以的

# 3.2 组索引与过滤
# 过滤在分组中是对于组的过滤,而索引是对于行的过滤,在第二章中的返回值,无论是布尔列表还是元素列
# 表或者位置列表,本质上都是对于行的筛选,即如果筛选条件的则选入结果的表,否则不选入。
# 组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回 True 则会被保留,False
# 则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为 DataFrame 返回。
# 在 groupby 对象中,定义了 filter 方法进行组的筛选,其中自定义函数的输入参数为数据源构成的 DataFrame
# 本身,在之前例子中定义的 groupby 对象中,传入的就是 df[['Height', 'Weight']] ,因此所有表方法和属性
# 都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。

# 例如,在原表中通过过滤得到所有容量大于 100 的组
gb.filter(lambda x: x.shape[0] > 100).head()
#   Height  Weight
# 0   158.9    46.0
# 3     NaN    41.0
# 5   158.0    51.0
# 6   162.5    52.0
# 7   161.9    50.0

# 4.跨列分组
# 4.1 apply 的引入
# 之前强调过聚合函数是逐列处理的,而不能够 多列数
# 还有一种常见的分组场景,无法用前面介绍的任何一种方法处理,
# 身体质量指数 BMI据同时处理。由此,引出了 apply 函数来解决这一问题

# 4.2  apply 的使用
# 在设计上,apply 的自定义函数传入参数与 filter 完全一致,只不过后者只允许返回布尔值
def BMI(x):
     Height = x['Height']/100
     Weight = x['Weight']
     BMI_value = Weight/Height**2
     return BMI_value.mean()
gb.apply(BMI)
# Gender
# Female    18.860930
# Male      24.318654
# dtype: float64

# 除了返回标量之外,apply 方法还可以返回一维 Series 和二维 DataFrame

# 标量情况:结果得到的是 Series ,索引与 agg 的结果一致
gb = df.groupby(['Gender','Test_Number'])[['Height','Weight']]
gb.apply(lambda x: 0)
# Gender  Test_Number
# Female  1              0
#         2              0
#         3              0
# Male    1              0
#         2              0
#         3              0
# dtype: int64

gb.apply(lambda x: [0, 0]) # 虽然是列表,但是作为返回值仍然看作标量

# Gender Test_Number
# Female    1 [0, 0]
#           2 [0, 0]
#           3 [0, 0]
# Male      1 [0, 0]
#           2 [0, 0]
#           3 [0, 0]

# Series 情况:得到的是 DataFrame ,行索引与标量情况一致,列索引为 Series 的索引
gb.apply(lambda x: pd.Series([0,0],index=['a','b']))
#                     a  b
# Gender Test_Number
# Female 1            0  0
#        2            0  0
#        3            0  0
# Male   1            0  0
#        2            0  0
#        3            0  0

# DataFrame 情况:得到的是 DataFrame ,行索引最内层在每个组原先 agg 的结果索引上,再加一层返
# 回的 DataFrame 行索引,同时分组结果 DataFrame 的列索引和返回的 DataFrame 列索引一致
gb.apply(lambda x: pd.DataFrame(np.ones((2,2)),
     index = ['a','b'],
     columns=pd.Index([('w','x'),('y','z')])))

# 最后需要强调的是,apply 函数的灵活性是以牺牲一定性能为代价换得的,除非需要使用跨列处理的分组处
# 理,否则应当使用其他专门设计的 groupby 对象方法,否则在性能上会存在较大的差距。同时,在使用聚合
# 函数和变换函数时,也应当优先使用内置函数,它们经过了高度的性能优化,一般而言在速度上都会快于用
# 自定义函数来实现。

变形

# 变形
# 1.0长宽表的变形
# 什么是长表?什么是宽表?这个概念是对于某一个特征而言的。例如:一个表中把性别存储在某一个列中,
# 那么它就是关于性别的长表;如果把性别作为列名,列中的元素是某一其他的相关特征数值,那么这个表是
# 关于性别的宽表。下面的两张表就分别是关于性别的长表和宽表:

pd.DataFrame({'Gender': ['F', 'F', 'M', 'M'], 'Height': [163, 160, 175, 180]})
#   Gender  Height
# 0      F     163
# 1      F     160
# 2      M     175
# 3      M     180
pd.DataFrame({'Height: F': [163, 160], 'Height: M': [175, 180]})
#    Height: F  Height: M
# 0        163        175
# 1        160        180

# 1.1  pivot
# pivot 是一种典型的长表变宽表的函数
df = pd.DataFrame({'Class': [1, 1, 2, 2],
                   'Name': ['San Zhang', 'San Zhang', 'Si Li', 'Si Li'],
                   'Subject': ['Chinese', 'Math', 'Chinese', 'Math'],
                   'Grade': [80, 75, 90, 85]})
df
#    Class       Name  Subject  Grade
# 0      1  San Zhang  Chinese     80
# 1      1  San Zhang     Math     75
# 2      2      Si Li  Chinese     90
# 3      2      Si Li     Math     85

# 对于一个基本的长变宽的操作而言,最重要的有三个要素,分别是变形后的行索引、需要转到列索引的列,
# 以及这些列和行索引对应的数值,它们分别对应了 pivot 方法中的 index, columns, values 参数。新生成表的
# 列索引是 columns 对应列的 unique 值,而新表的行索引是 index 对应列的 unique 值,而 values 对应了想
# 要展示的数值列
df.pivot(index='Name', columns='Subject', values='Grade')
# Subject    Chinese  Math
# Name
# San Zhang       80    75
# Si Li           90    85

# 利用 pivot 进行变形操作需要满足唯一性的要求,即由于在新表中的行列索引对应了唯一的 value ,因此原
# 表中的 index 和 columns 对应两个列的行组合必须唯一

# pandas 从 1.1.0 开始,pivot 相关的三个参数允许被设置为列表,这也意味着会返回多级索引
df = pd.DataFrame({'Class': [1, 1, 2, 2, 1, 1, 2, 2],
                   'Name': ['San Zhang', 'San Zhang', 'Si Li', 'Si Li',
                            'San Zhang', 'San Zhang', 'Si Li', 'Si Li'],
                   'Examination': ['Mid', 'Final', 'Mid', 'Final',
                                   'Mid', 'Final', 'Mid', 'Final'],
                   'Subject': ['Chinese', 'Chinese', 'Chinese', 'Chinese',
                               'Math', 'Math', 'Math', 'Math'],
                   'Grade': [80, 75, 85, 65, 90, 85, 92, 88],
                   'rank': [10, 15, 21, 15, 20, 7, 6, 2]})

df
# 把测试类型和科目联合组成的四个类别(期中语文、期末语文、期中数学、期末数学)转到列索引,
# 并且同时统计成绩和排名
pivot_multi = df.pivot(index=['Class', 'Name'],
                       columns=['Subject', 'Examination'],
                       values=['Grade', 'rank'])

pivot_multi
#                   Grade                     rank
# Subject         Chinese       Math       Chinese       Math
# Examination         Mid Final  Mid Final     Mid Final  Mid Final
# Class Name
# 1     San Zhang      80    75   90    85      10    15   20     7
# 2     Si Li          85    65   92    88      21    15    6

# 根据唯一性原则,新表的行索引等价于对 index 中的多列使用 drop_duplicates ,而列索引的长度为 values
# 中的元素个数乘以 columns 的唯一组合数量(与 index 类似)。

# 1.2 pivot_table
# pivot 的使用依赖于唯一性条件,那如果不满足唯一性条件,那么必须通过聚合操作使得相同行列组合对应
# 的多个值变为一个值。例如,张三和李四都参加了两次语文考试和数学考试,按照学院规定,最后的成绩是
# 两次考试分数的平均值,此时就无法通过 pivot 函数来完成。
df = pd.DataFrame({'Name': ['San Zhang', 'San Zhang',
                            'San Zhang', 'San Zhang',
                            'Si Li', 'Si Li', 'Si Li', 'Si Li'],
                   'Subject': ['Chinese', 'Chinese', 'Math', 'Math',
                               'Chinese', 'Chinese', 'Math', 'Math'],
                   'Grade': [80, 90, 100, 90, 70, 80, 85, 95]})

df
# pandas 中提供了 pivot_table 来实现,其中的 aggfunc 参数就是使用的聚合函数。
df.pivot_table(index='Name',
               columns='Subject',
               values='Grade',
               aggfunc='mean')
#         Name  Subject  Grade
# 0  San Zhang  Chinese     80
# 1  San Zhang  Chinese     90
# 2  San Zhang     Math    100
# 3  San Zhang     Math     90
# 4      Si Li  Chinese     70
# 5      Si Li  Chinese     80
# 6      Si Li     Math     85
# 7      Si Li     Math     95

# 这里传入 aggfunc 包含了上一章中介绍的所有合法聚合字符串,此外还可以传入以序列为输入标量为输出的
# 聚合函数来实现自定义操作
df.pivot_table(index='Name',
               columns='Subject',
               values='Grade',
               aggfunc=lambda x: x.mean())
# Subject Chinese Math
# Name
# San Zhang 85 95
# Si Li 75 90

# 此外,pivot_table 具有边际汇总的功能,可以通过设置 margins=True 来实现,其中边际的聚合方式与
# aggfunc 中给出的聚合方法一致。下面就分别统计了语文均分和数学均分、张三均分和李四均分,以及总体
# 所有分数的均分

df.pivot_table(index='Name',
               columns='Subject',
               values='Grade',
               aggfunc='mean',
               margins=True)
# Subject    Chinese  Math    All
# Name
# San Zhang       85  95.0  90.00
# Si Li           75  90.0  82.50
# All             80  92.5  86.25

# 1.3 melt
# 长宽表只是数据呈现方式的差异,但其包含的信息量是等价的,前面提到了利用 pivot 把长表转为宽表,那
# 么就可以通过相应的逆操作把宽表转为长表,melt 函数就起到了这样的作用。
df = pd.DataFrame({'Class': [1, 2],
                   'Name': ['San Zhang', 'Si Li'],
                   'Chinese': [80, 90],
                   'Math': [80, 75]})
df
#    Class       Name  Chinese  Math
# 0      1  San Zhang       80    80
# 1      2      Si Li       90    75

df_melted = df.melt(id_vars=['Class', 'Name'],
                    value_vars=['Chinese', 'Math'],
                    var_name='Subject',
                    value_name='Grade')
df_melted

df_unmelted = df_melted.pivot(index=['Class', 'Name'],
                              columns='Subject',
                              values='Grade')
df_unmelted  # 下面需要恢复索引,并且重命名列索引名称
# Subject Chinese Math
# Class Name
# 1 San Zhang 80 80
# 2 Si Li 90 75

df_unmelted = df_unmelted.reset_index().rename_axis(
    columns={'Subject': ''})
df_unmelted.equals(df)

#  wide_to_long
# melt 方法中,在列索引中被压缩的一组值对应的列元素只能代表同一层次的含义,即 values_name 。现在
# 如果列中包含了交叉类别,比如期中期末的类别和语文数学的类别,那么想要把 values_name 对应的 Grade
# 扩充为两列分别对应语文分数和数学分数,只把期中期末的信息压缩,这种需求下就要使用 wide_to_long
# 函数来完成

df = pd.DataFrame({'Class': [1, 2], 'Name': ['San Zhang', 'Si Li'],
                   'Chinese_Mid': [80, 75], 'Math_Mid': [90, 85],
                   'Chinese_Final': [80, 75], 'Math_Final': [90, 85]})
df
#    Class       Name  Chinese_Mid  Math_Mid  Chinese_Final  Math_Final
# 0      1  San Zhang           80        90             80          90
# 1      2      Si Li           75        85             75          85

pd.wide_to_long(df,
                stubnames=['Chinese', 'Math'],
                i=['Class', 'Name'],
                j='Examination',
                sep='_',
                suffix='.+')

# Chinese Math
# Class Name Examination
# 1 San Zhang Mid 80 90
# Final 80 90
# 2 Si Li Mid 75 85
# Final 75 85

# 2.索引的变形
# 2.1 stack与unstack
# 利用 swaplevel 或者 reorder_levels 进行索引内部的层交换,下面就要讨论 行列索引之间
# 的交换,由于这种交换带来了 DataFrame 维度上的变化,因此属于变形操作。
# unstack 函数的作用是把行索引转为列索引,
df = pd.DataFrame(np.ones((4, 2)),
                  index=pd.Index([('A', 'cat', 'big'),
                                  ('A', 'dog', 'small'),
                                  ('B', 'cat', 'big'),
                                  ('B', 'dog', 'small')]),
                  columns=['col_1', 'col_2'])

df
#              col_1  col_2
# A cat big      1.0    1.0
#   dog small    1.0    1.0
# B cat big      1.0    1.0
#   dog small    1.0    1.0

df.unstack()
# col_1 col_2
#       big small big small
# A cat 1.0 NaN 1.0 NaN
#   dog NaN 1.0 NaN 1.0
# B cat 1.0 NaN 1.0 NaN
#   dog NaN 1.0 NaN 1.0

# unstack 的主要参数是移动的层号,默认转化最内层,移动到列索引的最内层,同时支持同时转化多个层
df.unstack(2)
#       col_1       col_2
#         big small   big small
# A cat   1.0   NaN   1.0   NaN
#   dog   NaN   1.0   NaN   1.0
# B cat   1.0   NaN   1.0   NaN
#   dog   NaN   1.0   NaN   1.0

df.unstack([0, 2])
#     col_1                  col_2
#         A          B           A          B
#       big small  big small   big small  big small
# cat   1.0   NaN  1.0   NaN   1.0   NaN  1.0   NaN
# dog   NaN   1.0  NaN   1.0   NaN   1.0  NaN   1.0
# 类似于 pivot 中的唯一性要求,在 unstack 中必须保证 被转为列索引的行索引层和 被保留的行索引层构成
# 的组合是唯一的,把前两个列索引改成相同的破坏唯一性,那么就会报错

# 与 unstack 相反,stack 的作用就是把列索引的层压入行索引,其用法完全类似。
df = pd.DataFrame(np.ones((4, 2)),
                  index=pd.Index([('A', 'cat', 'big'),
                                  ('A', 'dog', 'small'),
                                  ('B', 'cat', 'big'),
                                  ('B', 'dog', 'small')]),
                  columns=['index_1', 'index_2']).T
df
#            A          B
#          cat   dog  cat   dog
#          big small  big small
# index_1  1.0   1.0  1.0   1.0
# index_2  1.0   1.0  1.0   1.0

df.stack()
#                  A         B
#                cat  dog  cat  dog
# index_1 big    1.0  NaN  1.0  NaN
#         small  NaN  1.0  NaN  1.0
# index_2 big    1.0  NaN  1.0  NaN
#         small  NaN  1.0  NaN  1.0

df.stack([1, 2])
# A B
# index_1 cat big 1.0 1.0
# dog small 1.0 1.0
# index_2 cat big 1.0 1.0
# dog small 1.0 1.0

# 2.2 聚合与变形的关系
# 在上面介绍的所有函数中,除了带有聚合效果的 pivot_table 以外,所有的函数在变形前后并不会带来 values
# 个数的改变,只是这些值在呈现的形式上发生了变化。在上一章讨论的分组聚合操作,由于生成了新的行列
# 索引,因此必然也属于某种特殊的变形操作,但由于聚合之后把原来的多个值变为了一个值,因此 values 的
# 个数产生了变化,这也是分组聚合与变形函数的最大区别

# 3.其他变形函数
# 3.1 crosstab
# crosstab 并不是一个值得推荐使用的函数,因为它能实现的所有功能 pivot_table 都能完成,并且速度更快。
# 在默认状态下,crosstab 可以统计元素组合出现的频数,即 count 操作
df = pd.read_csv('data/learn_pandas.csv')
pd.crosstab(index=df.School, columns=df.Transfer)
# Transfer                        N  Y
# School
# Fudan University               38  1
# Peking University              28  2
# Shanghai Jiao Tong University  53  0
# Tsinghua University            62  4

# 等价
pd.crosstab(index=df.School, columns=df.Transfer,
            values=[0] * df.shape[0], aggfunc='count')
# Transfer                          N    Y
# School
# Fudan University               38.0  1.0
# Peking University              28.0  2.0
# Shanghai Jiao Tong University  53.0  NaN
# Tsinghua University            62.0  4.0

df.pivot_table(index='School',
               columns='Transfer',
               values='Name',
               aggfunc='count')

# 从上面可以看出这两个函数的区别在于,crosstab 的对应位置传入的是具体的序列,而 pivot_table 传入的
# 是被调用表对应的名字,若传入序列对应的值则会报错

# 除了默认状态下的 count 统计,所有的聚合字符串和返回标量的自定义函数都是可用的
pd.crosstab(index=df.School, columns=df.Transfer,
            values=df.Height, aggfunc='mean')
# Transfer                                N       Y
# School
# Fudan University               162.043750  177.20
# Peking University              163.429630  162.40
# Shanghai Jiao Tong University  163.953846     NaN
# Tsinghua University            163.253571  164.55

# explode
# explode 参数能够对某一列的元素进行纵向的展开,被展开的单元格必须存储 list, tuple, Series, np.ndarray
# 中的一种类型
df_ex = pd.DataFrame({'A': [[1, 2],
                            'my_str',
                            {1, 2},
                            pd.Series([3, 4])],
                      'B': 1})
df_ex
#                             A  B
# 0                      [1, 2]  1
# 1                      my_str  1
# 2                      {1, 2}  1
# 3  0    3
# 1    4
# dtype: int64  1

df_ex.explode('A')
#         A  B
# 0       1  1
# 0       2  1
# 1  my_str  1
# 2  {1, 2}  1
# 3       3  1
# 3       4  1

#  get_dummies
# get_dummies 是用于特征构建的重要函数之一,其作用是把类别特征转为指示变量。例如,对年级一列转为
# 指示变量,属于某一个年级的对应列标记为 1,否则为 0
pd.get_dummies(df.Grade).head()
#    Freshman  Junior  Senior  Sophomore
# 0         1       0       0          0
# 1         1       0       0          0
# 2         0       0       1          0
# 3         0       0       0          1
# 4         0       0       0          1

连接

# 连接
# 1. 关系型连接
# 1.1 连接的基本概念
# 把两张相关的表按照某一个或某一组键连接起来是一种常见操作,
# 另一个重要的要素是连接的形式。在 pandas 中的关系型连接函数 merge 和 join 中提供了 how 参数来代表
# 连接形式,分为左连接 left 、右连接 right 、内连接 inner 、外连接 outer

# 1.2 值连接
# 两张表根据某一列的值来连接,事实上还可以通过几列值的组合进行连接,这种
# 基于值的连接在 pandas 中可以由 merge 函数实现
df1 = pd.DataFrame({'Name':['San Zhang','Si Li'],
    'Age':[20,30]})

df2 = pd.DataFrame({'Name':['Si Li','Wu Wang'],
    'Gender':['F','M']})

df1.merge(df2, on='Name', how='left')
#         Name  Age Gender
# 0  San Zhang   20    NaN
# 1      Si Li   30      F

# 如果两个表中想要连接的列不具备相同的列名,可以通过 left_on 和 right_on 指定
df1 = pd.DataFrame({'df1_name':['San Zhang','Si Li'],
    'Age':[20,30]})
df2 = pd.DataFrame({'df2_name':['Si Li','Wu Wang'],
    'Gender':['F','M']})
df1.merge(df2, left_on='df1_name', right_on='df2_name', how='left')

# 如果两个表中的列出现了重复的列名,那么可以通过 suffixes 参数指定。
df1 = pd.DataFrame({'Name':['San Zhang'],'Grade':[70]})
df2 = pd.DataFrame({'Name': ['San Zhang'], 'Grade': [80]})
df1.merge(df2, on='Name', how='left', suffixes=['_Chinese','_Math'])
#         Name  Grade_Chinese  Grade_Math
# 0  San Zhang             70          80

# 在某些时候出现重复元素是麻烦的,例如两位同学来自不同的班级,但是姓名相同,这种时候就要指定 on
# 参数为多个列使得正确连接

# 从上面的例子来看,在进行基于唯一性的连接下,如果键不是唯一的,那么结果就会产生问题。举例中的行
# 数很少,但如果实际数据中有几十万到上百万行的进行合并时,如果想要保证唯一性,除了用 duplicated 检
# 查是否重复外,merge 中也提供了 validate 参数来检查连接的唯一性模式。
# 这里共有三种模式,即一对一连接 1:1 ,一对多连接 1:m ,多对一连接 m:1 连接,
# 第一个是指左右表的键都是唯一的,后面两个分别指左表键唯一和右表键唯一

# 1.3 索引连接
# 所谓索引连接,就是把索引当作键,因此这和值连接本质上没有区别,pandas 中利用 join 函数来处理索引
# 连接,它的参数选择要少于 merge ,除了必须的 on 和 how 之外,可以对重复的列指定左右后缀 lsuffix 和
# rsuffix 。其中,on 参数指索引名,单层索引时省略参数表示按照当前索引连接
df1 = pd.DataFrame({'Age':[20,30]},
    index=pd.Series(
    ['San Zhang','Si Li'],name='Name'))

df2 = pd.DataFrame({'Gender':['F','M']},
    index=pd.Series(
    ['Si Li','Wu Wang'],name='Name'))
df1.join(df2, how='left')
# Age Gender
# Name
# San Zhang 20 NaN
# Si Li 30 F

df1 = pd.DataFrame({'Grade':[70]}, index=pd.Series(['San Zhang'],name='Name'))

df2 = pd.DataFrame({'Grade':[80]},index=pd.Series(['San Zhang'],name='Name'))

df1.join(df2, how='left', lsuffix='_Chinese', rsuffix='_Math')
#            Grade_Chinese  Grade_Math
# Name
# San Zhang             70          80

# 如果想要进行类似于 merge 中以多列为键的操作的时候,join 需要使用多级索引
df1 = pd.DataFrame({'Age':[20,21]},
    index=pd.MultiIndex.from_arrays(
    [['San Zhang', 'San Zhang'],['one', 'two']],
    names=('Name','Class')))

df2 = pd.DataFrame({'Gender':['F', 'M']},
    index=pd.MultiIndex.from_arrays(
    [['San Zhang', 'San Zhang'],['two', 'one']],
    names=('Name','Class')))

df1.join(df2)
#                  Age Gender
# Name      Class
# San Zhang one     20      M
#           two     21      F


# 2.方向连接
# 2.1 concat
# 前面介绍了关系型连接,其中最重要的参数是 on 和 how ,但有时候用户并不关心以哪一列为键来合并,只
# 是希望把两个表或者多个表按照纵向或者横向拼接,为这种需求,pandas 中提供了 concat 函数来实现

# 在 concat 中,最常用的有三个参数,它们是 axis, join, keys ,分别表示拼接方向,连接形式,以及在新表中
# 指示来自于哪一张旧表的名字。这里需要特别注意,join 和 keys 与之前提到的 join 函数和键的概念没有任
# 何关系。
# 在默认状态下的 axis=0 ,表示纵向拼接多个表,常常用于多个样本的拼接;而 axis=1 表示横向拼接多个表,
# 常用于多个字段或特征的拼接。
df1 = pd.DataFrame({'Name': ['San Zhang', 'Si Li'], 'Age':[20,30]})
df2 = pd.DataFrame({'Name': ['Wu Wang'], 'Age':[40]})
pd.concat([df1, df2])
#         Name  Age
# 0  San Zhang   20
# 1      Si Li   30
# 0    Wu Wang   40

df2 = pd.DataFrame({'Grade':[80, 90]})
df3 = pd.DataFrame({'Gender':['M', 'F']})
pd.concat([df1, df2, df3], 1)
# Name Age Grade Gender
# 0 San Zhang 20 80 M
# 1 Si Li 30 90 F

# 虽然说 concat 不是处理关系型合并的函数,但是它仍然是关于索引进行连接的。纵向拼接会根据列索引对
# 其,默认状态下 join=outer ,表示保留所有的列,并将不存在的值设为缺失;join=inner ,表示保留两个表
# 都出现过的列。横向拼接则根据行索引对齐,join 参数可以类似设置。
df2 = pd.DataFrame({'Name':['Wu Wang'], 'Gender':['M']})
pd.concat([df1, df2])
# Name Age Gender
# 0 San Zhang 20.0 NaN
# 1 Si Li 30.0 NaN
# 0 Wu Wang NaN M

df2 = pd.DataFrame({'Grade':[80, 90]}, index=[1, 2])
pd.concat([df1, df2], 1)
# Name Age Grade
# 0 San Zhang 20.0 NaN
# 1 Si Li 30.0 80.0
# 2 NaN NaN 90.0

pd.concat([df1, df2], axis=1, join='inner')
# Name Age Grade
# 1 Si Li 30 80

# 因此,当确认要使用多表直接的方向合并时,尤其是横向的合并,可以先用 reset_index 方法恢复默认整数
# 索引再进行合并,防止出现由索引的误对齐和重复索引的笛卡尔积带来的错误结果

# 最后,keys 参数的使用场景在于多个表合并后,用户仍然想要知道新表中的数据来自于哪个原表,这时可以
# 通过 keys 参数产生多级索引进行标记
df1 = pd.DataFrame({'Name':['San Zhang','Si Li'],
    'Age':[20,21]})
df2 = pd.DataFrame({'Name':['Wu Wang'],'Age':[21]})

pd.concat([df1, df2], keys=['one', 'two'])
#             Name  Age
# one 0  San Zhang   20
#     1      Si Li   21
# two 0    Wu Wang   21

# 2.2 序列与表的合并
# 利用 concat 可以实现多个表之间的方向拼接,如果想要把一个序列追加到表的行末或者列末,则可以分别
# 使用 append 和 assign 方法。

# 在 append 中,如果原表是默认整数序列的索引,那么可以使用 ignore_index=True 对新序列对应索引的自
# 动标号,否则必须对 Series 指定 name 属性
s = pd.Series(['Wu Wang', 21], index = df1.columns)
df1.append(s, ignore_index=True)
# Name Age
# 0 San Zhang 20
# 1 Si Li 21
# 2 Wu Wang 21

# 对于 assign 而言,虽然可以利用其添加新的列,但一般通过 df['new_col'] = ... 的形式就可以等价地添加新
# 列。同时,使用 [] 修改的缺点是它会直接在原表上进行改动,而 assign 返回的是一个临时副本
s = pd.Series([80, 90])
df1.assign(Grade=s)
# Name Age Grade
# 0 San Zhang 20 80
# 1 Si Li 21 90

df1['Grade'] = s
df1
# Name Age Grade
# 0 San Zhang 20 80
# 1 Si Li 21 90

# 3.类连接操作
# 除了上述介绍的若干连接函数之外,pandas 中还设计了一些函数能够对两个表进行某些操作,这里把它们统
# 称为类连接操作
# 3.1  比较
# compare 是在 1.1.0 后引入的新函数,它能够比较两个表或者序列的不同处并将其汇总展示:
df1 = pd.DataFrame({'Name':['San Zhang', 'Si Li', 'Wu Wang'],
    'Age':[20, 21 ,21],
    'Class':['one', 'two', 'three']})
#         Name  Age  Class
# 0  San Zhang   20    one
# 1      Si Li   21    two
# 2    Wu Wang   21  three

df2 = pd.DataFrame({'Name':['San Zhang', 'Li Si', 'Wu Wang'],
    'Age':[20, 21 ,21],
    'Class':['one', 'two', 'Three']})
#         Name  Age  Class
# 0  San Zhang   20    one
# 1      Li Si   21    two
# 2    Wu Wang   21  Three

df1.compare(df2)
# Name Class
# self other self other
# 1 Si Li Li Si NaN NaN
# 2 NaN NaN three Three

# 结果中返回了不同值所在的行列,如果相同则会被填充为缺失值 NaN ,
# 其中 other 和 self 分别指代传入的参数表和被调用的表自身。
# 如果想要完整显示表中所有元素的比较情况,可以设置 keep_shape=True
df1.compare(df2, keep_shape=True)
# Name Age Class
# self other self other self other
# 0 NaN NaN NaN NaN NaN NaN
# 1 Si Li Li Si NaN NaN NaN NaN
# 2 NaN NaN NaN NaN three Three

# 3.2 组合
# combine 函数能够让两张表按照一定的规则进行组合,在进行规则比较时会自动进行列索引的对齐。对于传
# 入的函数而言,每一次操作中输入的参数是来自两个表的同名 Series ,依次传入的列是两个表列名的并集,

# 例如下面这个例子会依次传入 A,B,C,D 四组序列,每组为左右表的两个序列。同时,进行 A 列比较的时
# 候,s1 指代的就是一个全空的序列,因为它在被调用的表中并不存在,并且来自第一个表的序列索引会被
# reindex 成两个索引的并集。具体的过程可以通过在传入的函数中插入适当的 print 方法查看。

def choose_min(s1, s2):
    s2 = s2.reindex_like(s1)
    res = s1.where(s1<s2, s2)
    res = res.mask(s1.isna()) # isna 表示是否为缺失值,返回布尔序列
    return res

df1 = pd.DataFrame({'A':[1,2], 'B':[3,4], 'C':[5,6]})
#    A  B  C
# 0  1  3  5
# 1  2  4  6

df2 = pd.DataFrame({'B':[5,6], 'C':[7,8], 'D':[9,10]},
                   index=[1,2])
df2
#    B  C   D
# 1  5  7   9
# 2  6  8  10

df1.combine(df2, choose_min)
#     A    B    C   D
# 0 NaN  NaN  NaN NaN
# 1 NaN  4.0  6.0 NaN
# 2 NaN  NaN  NaN NaN

# 设置 overtwrite 参数为 False 可以保留 被调用表中未出现在传入的参数表中的列,而不会设置未缺失值
df1.combine(df2, choose_min, overwrite=False)
# A B C D
# 0 1.0 NaN NaN NaN
# 1 2.0 4.0 6.0 NaN
# 2 NaN NaN NaN NaN

# 除了 combine 之外,pandas 中还有一个 combine_first 方法,其功能是在对两张表组合时,若第
# 二张表中的值在第一张表中对应索引位置的值不是缺失状态,那么就使用第一张表的值填充
df1 = pd.DataFrame({'A':[1,2], 'B':[3,np.nan]})
df1
#    A    B
# 0  1  3.0
# 1  2  NaN

df2 = pd.DataFrame({'A':[5,6], 'B':[7,8]}, index=[1,2])
df2
#    A  B
# 1  5  7
# 2  6  8

df1.combine_first(df2)
#      A    B
# 0  1.0  3.0
# 1  2.0  7.0
# 2  6.0  8.0

索引

# 索引
# 索引器


import pandas as pd
import numpy as np

# 1.1表的列索引
# 列索引是最常见的索引形式,一般通过 [] 来实现。
df = pd.read_csv('data/learn_pandas.csv', usecols=['School', 'Grade', 'Name',
                                                   'Gender', 'Weight',
                                                   'Transfer'])
# 通过 [列名] 可以从 DataFrame 中取出相应的列,返回值为 Series ,例如从表中取出姓名一列
# 此外,若要取出单列,且列名中不包含空格,则可以用 . 列名 取出,这和 [列名] 是等价的
df['Name']
df.Name
# 如果要取出多个列,则可以通过 [列名组成的列表] ,其返回值为一个 DataFrame
df[['Gender', 'Name']]

# 1.2序列的行索引
# 以字符串为索引的 Series
# 如果取出单个索引的对应元素,则可以使用 [item] ,若 Series 只有单个值对应,则返回这个标量值,如果有
# 多个值对应,则返回一个 Series
s = pd.Series([1, 2, 3, 4, 5, 6],
              index=['a', 'b', 'a', 'a', 'a', 'c'])
s['a']
# a    1
# a    3
# a    4
# a    5
# dtype: int64

s['b']
# 2

# 如果取出多个索引的对应元素,则可以使用 [items 的列表]
s[['c', 'b']]
# c    6
# b    2
# dtype: int64


# 如果想要取出某两个索引之间的元素,并且这两个索引是在整个索引中唯一出现,
# 则可以使用切片,同时需要注意这里的切片会包含两个端点
s['c': 'b': -2]


# 以整数为索引的 Series
# 在使用数据的读入函数时,如果不特别指定所对应的列作为索引,
# 那么会生成从 0 开始的整数索引作为默认索引。
# 当然,任意一组符合长度要求的整数都可以作为索引。
s = pd.Series(['a', 'b', 'c', 'd', 'e', 'f'], index=[1, 3, 1, 2, 5, 4])

s[1]
# 1    a
# 1    c
# dtype: object

s[[2, 3]]
# 2 d
# 3 b
# dtype: object

# 如果使用整数切片,则会取出对应索引 位置的值,注意这里的整数切片同 Python 中的切片一样不包含右端点
s[1:-1:2]
# 3    b
# 2    d
# dtype: object

# 1.3 loc索引器

# 前面讲到了对 DataFrame 的列进行选取,下面要讨论其行的选取。
# 对于表而言,有两种索引器,一种是基于元素的 loc 索引器,另一种是基于 位置的 iloc 索引器

# loc 索引器的一般形式是 loc[*, *] ,
# 其中第一个 * 代表行的选择,第二个 * 代表列的选择,如果省略第二个位置写作 loc[*] ,这个 * 是指行的筛选。
# 其中,* 的位置一共有五类合法对象,分别是:单个元素、元素列表、元素切片、布尔列表以及函数

# 利用 set_index 方法把 Name 列设为索引
df_demo = df.set_index('Name')

df_demo.head()

# 单个元素
# 元素列表
# 此时,直接取出相应的行或列,如果该元素在索引中重复则结果为 DataFrame,否则为 Series

df_demo.loc['Qiang Sun'] # 多个人叫此名字
df_demo.loc['Quan Zhao'] # 名字唯一

# 以同时选择行和列
df_demo.loc['Qiang Sun', 'School']  # 返回 Series

df_demo.loc['Quan Zhao', 'School'] # 返回单个元素

# 取出列表中所有元素值对应的行或列
df_demo.loc[['Qiang Sun','Quan Zhao'], ['School','Gender']]

# 切片
# 之前的 Series 使用字符串索引时提到,如果是唯一值的起点和终点字符,
# 那么就可以使用切片,并且包含两个端点,如果不唯一则报错
df_demo.loc['Gaojuan You':'Gaoqiang Qian', 'School':'Gender']

# 如果 DataFrame 使用整数索引,其使用整数切片的时候和上面字符串索引的要求一致,
# 都是 元素切片,包含端点且起点、终点不允许有重复值
df_loc_slice_demo = df_demo.copy()
df_loc_slice_demo.index = range(df_demo.shape[0],0,-1)
df_loc_slice_demo.loc[5:3]
df_loc_slice_demo.loc[3:5] # 没有返回,说明不是整数位置切片

# 布尔列表
# 在实际的数据处理中,根据条件来筛选行是极其常见的,
# 此处传入 loc 的布尔列表与 DataFrame 长度相同,且列表为 True 的位置所对应的行会被选中,False 则会被剔除

df_demo.loc[df_demo.Weight>70]
# 也可以通过 isin 方法返回的布尔列表等价写出
df_demo.loc[df_demo.Grade.isin(['Freshman', 'Senior'])]

# 对于复合条件而言,可以用 |(或), &(且), ~(取反) 的组合来实现
condition_1_1 = df_demo.School == 'Fudan University'
condition_1_2 = df_demo.Grade == 'Senior'
condition_1_3 = df_demo.Weight > 70
condition_1 = condition_1_1 & condition_1_2 & condition_1_3

condition_2_1 = df_demo.School == 'Peking University'
condition_2_2 = df_demo.Grade == 'Senior'
condition_2_3 = df_demo.Weight > 80
condition_2 = condition_2_1 & (~condition_2_2) & condition_2_3

df_demo.loc[condition_1 | condition_2]

#  函数
# 这里的函数,必须以前面的四种合法形式之一为返回值,并且函数的输入值为 DataFrame 本身。假设仍然
# 是上述复合条件筛选的例子,可以把逻辑写入一个函数中再返回,需要注意的是函数的形式参数 x 本质上即
# 为 df_demo
def condition(x):
    condition_1_1 = x.School == 'Fudan University'
    condition_1_2 = x.Grade == 'Senior'
    condition_1_3 = x.Weight > 70
    condition_1 = condition_1_1 & condition_1_2 & condition_1_3
    condition_2_1 = x.School == 'Peking University'
    condition_2_2 = x.Grade == 'Senior'
    condition_2_3 = x.Weight > 80
    condition_2 = condition_2_1 & (~condition_2_2) & condition_2_3
    result = condition_1 | condition_2
    return result
df_demo.loc[condition]
# 支持使用 lambda 表达式,其返回值也同样必须是先前提到的四种形式之一
df_demo.loc[lambda x:'Quan Zhao', lambda x:'Gender']

# 由于函数无法返回如 start: end: step 的切片形式,故返回切片时要用 slice 对象进行包装
df_demo.loc[lambda x: slice('Gaojuan You', 'Gaoqiang Qian')]
#                                       School      Grade  ... Weight  Transfer
# Name                                                     ...
# Gaojuan You                 Fudan University  Sophomore  ...   74.0         N
# Xiaoli Qian              Tsinghua University   Freshman  ...   51.0         N
# Qiang Chu      Shanghai Jiao Tong University   Freshman  ...   52.0         N
# Gaoqiang Qian            Tsinghua University     Junior  ...   50.0         N
# [4 rows x 5 columns]

# 对于 Series 也可以使用 loc 索引,其遵循的原则与 DataFrame 中用于行筛选的 loc[*]完全一致

# 不要使用链式赋值
# 在对表或者序列赋值时,应当在使用一层索引器后直接进行赋值操作,这样做是由于进行多次
# 索引后赋值是赋在临时返回的 copy 副本上的,而没有真正修改元素从而报出 SettingWithCopyWarning 警告。
df_chain = pd.DataFrame([[0,0],[1,0],[-1,0]], columns=list('AB'))

import warnings

with warnings.catch_warnings():
    warnings.filterwarnings('error')
    try:
        df_chain[df_chain.A!=0].B = 1 # 使用方括号列索引后,再使用点的列索引
    except Warning as w:
        Warning_Msg = w

print(Warning_Msg)
# A value is trying to be set on a copy of a slice from a DataFrame.
# Try using .loc[row_indexer,col_indexer] = value instead
# See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
df_chain
#    A  B
# 0  0  0
# 1  1  0
# 2 -1  0
df_chain.loc[df_chain.A!=0,'B'] = 1
df_chain
#    A  B
# 0  0  0
# 1  1  1
# 2 -1  1

# 1.4 iloc索引器
# iloc 的使用与 loc 完全类似,只不过是针对位置进行筛选,在相应的 * 位置处一共也有五类合法对象,分别
# 是:整数、整数列表、整数切片、布尔列表以及函数,函数的返回值必须是前面的四类合法对象中的一个,其
# 输入同样也为 DataFrame 本身
df_demo.iloc[1, 1]  # 第二行第二列
df_demo.iloc[[0, 1], [0, 1]]  # 前两行前两列
df_demo.iloc[1: 4, 2:4]  # 切片不包含结束端点
df_demo.iloc[lambda x: slice(1, 4)]  # 传入切片为返回值的函数

# 在使用布尔列表的时候要特别注意,不能传入 Series 而必须传入序列的 values ,
# 否则会报错(ValueError: iLocation based boolean indexing cannot use an indexable as a mask)。因此,在使用布尔筛选的时候还是应当优先考虑 loc 的方式
df_demo.iloc[(df_demo.Weight>80).values]

# 对 Series 而言同样也可以通过 iloc 返回相应位置的值或子序列
df_demo.School.iloc[1]

df_demo.School.iloc[1:5:2]

# 1.5 query方法
# 在 pandas 中,支持把字符串形式的查询表达式传入 query 方法来查询数据,其表达式的执行结果必须返回
# 布尔列表。在进行复杂索引时,由于这种检索方式无需像普通方法一样重复使用 DataFrame 的名字来引用
# 列名,一般而言会使代码长度在不降低可读性的前提下有所减少
df.query('((School == "Fudan University")&'
' (Grade == "Senior")&'
' (Weight > 70))|'
'((School == "Peking University")&'
' (Grade != "Senior")&'
' (Weight > 80))')

# 在 query 表达式中,帮用户注册了所有来自 DataFrame 的列名,所有属于该 Series 的方法都可以被调用,和
# 正常的函数调用并没有区别,例如查询体重超过均值的学生:
df.query('Weight > Weight.mean()').head()

# query 中引用带空格的列名
# 对于含有空格的列名,需要使用 `col name` 的方式进行引用。
# 在 query 中还注册了若干英语的字面用法,帮助提高可读性,例如:or, and, or, is in, not in
# 筛选出男生中不是大一大二的学生:
df.query('(Grade not in ["Freshman", "Sophomore"]) and'
'(Gender == "Male")').head()

# 在字符串中出现与列表的比较时,== 和 != 分别表示元素出现在列表和没有出现在列表,等价于 is in 和 not in
# 查询所有大三和大四的学生
df.query('Grade == ["Junior", "Senior"]').head()

# 对于 query 中的字符串,如果要引用外部变量,只需在变量名前加 @ 符号
low, high = 70, 80
df.query('Weight.between(@low, @high)').head()

# 1.6 随机抽样
# 如果把 DataFrame 的每一行看作一个样本,或把每一列看作一个特征,再把整个 DataFrame 看作总体,想
# 要对样本或特征进行随机抽样就可以用 sample 函数。有时在拿到大型数据集后,想要对统计特征进行计算
# 来了解数据的大致分布,但是这很费时间。同时,由于许多统计特征在等概率不放回的简单随机抽样条件下,
# 是总体统计特征的无偏估计,比如样本均值和总体均值,那么就可以先从整张表中抽出一部分来做近似估计。

# sample 函数中的主要参数为 n, axis, frac, replace, weights ,
# 前三个分别是指抽样数量、抽样的方向(0 为 行、1 为列)和抽样比例(0.3 则为从总体中抽出 30% 的样本)。
# replace 和 weights 分别是指是否放回和每个样本的抽样相对概率,
# 当 replace = True 则表示有放回抽样。
# 例如,对下面构造的 df_sample 以 value 值的相对大小为抽样概率进行有放回抽样,抽样数量为 3。
df_sample = pd.DataFrame({'id': list('abcde'), 'value': [1, 2, 3, 4, 90]})
df_sample
df_sample.sample(3, replace=True, weights=df_sample.value)
#   id  value
# 4  e     90
# 4  e     90
# 4  e     90


# 2.多级索引
# 多级索引及其表的结构
np.random.seed(0)

multi_index = pd.MultiIndex.from_product([list('ABCD'),
df.Gender.unique()], names=('School', 'Gender'))

multi_column = pd.MultiIndex.from_product([['Height', 'Weight'],
df.Grade.unique()], names=('Indicator', 'Grade'))

df_multi = pd.DataFrame(np.c_[(np.random.randn(8, 4)*5 + 163).tolist(),
 (np.random.randn(8, 4)*5 + 65).tolist()], index = multi_index,
columns=multi_column).round(1)

df_multi
# Indicator       Height                           Weight
# Grade         Freshman Senior Sophomore Junior Freshman Senior Sophomore Junior
# School Gender
# A      Female    171.8  165.0     167.9  174.2     60.6   55.1      63.3   65.8
#        Male      172.3  158.1     167.8  162.2     71.2   71.0      63.1   63.5
# B      Female    162.5  165.1     163.7  170.3     59.8   57.9      56.5   74.8
#        Male      166.8  163.6     165.2  164.7     62.5   62.8      58.7   68.9
# C      Female    170.5  162.0     164.6  158.7     56.9   63.9      60.5   66.9
#        Male      150.2  166.3     167.3  159.3     62.4   59.1      64.9   67.1
# D      Female    174.3  155.7     163.2  162.1     65.3   66.5      61.8   63.2
#        Male      170.7  170.3     163.8  164.9     61.6   63.2      60.9   56.4

# 这里的行索引和列索引都是 MultiIndex 类型,只不过 索引中的一个元素是元组而不是单层索引
# 中的标量。例如,行索引的第四个元素为 (”B”, ”Male”) ,列索引的第二个元素为 (”Height”, ”Senior”) ,这
# 里需要注意,外层连续出现相同的值时,第一次之后出现的会被隐藏显示,使结果的可读性增强。
# 与单层索引类似,MultiIndex 也具有名字属性,图中的 School 和 Gender 分别对应了表的第一层和第二层
# 行索引的名字,Indicator 和 Grade 分别对应了第一层和第二层列索引的名字
df_multi.index.names
# FrozenList(['School', 'Gender'])
df_multi.columns.names
# FrozenList(['Indicator', 'Grade'])
df_multi.index.values
# array([('A', 'Female'), ('A', 'Male'), ('B', 'Female'), ('B', 'Male'),
#        ('C', 'Female'), ('C', 'Male'), ('D', 'Female'), ('D', 'Male')],
#       dtype=object)
df_multi.columns.values
# array([('Height', 'Freshman'), ('Height', 'Senior'),
#        ('Height', 'Sophomore'), ('Height', 'Junior'),
#        ('Weight', 'Freshman'), ('Weight', 'Senior'),
#        ('Weight', 'Sophomore'), ('Weight', 'Junior')], dtype=object)

# 如果想要得到某一层的索引,则需要通过 get_level_values 获得
df_multi.index.get_level_values(0)
# Index(['A', 'A', 'B', 'B', 'C', 'C', 'D', 'D'], dtype='object', name='School')

# 2.2 多级索引中的loc索引器
# 熟悉了结构后,现在回到原表,将学校和年级设为索引,此时的行为多级索引,列为单级索引,由于默认状
# 态的列索引不含名字,因此对应于刚刚图中 Indicator 和 Grade 的索引名位置是空缺的
df_multi = df.set_index(['School', 'Grade'])
df_multi
# School                        Grade                      ...
# Shanghai Jiao Tong University Freshman     Gaopeng Yang  ...        N
# Peking University             Freshman   Changqiang You  ...        N
# Shanghai Jiao Tong University Senior            Mei Sun  ...        N
# Fudan University              Sophomore    Xiaojuan Sun  ...        N
#                               Sophomore     Gaojuan You  ...        N
# ...                                                 ...  ...      ...
#                               Junior       Xiaojuan Sun  ...        N
# Tsinghua University           Senior            Li Zhao  ...        N
# Shanghai Jiao Tong University Senior     Chengqiang Chu  ...        N
#                               Senior      Chengmei Shen  ...        N
# Tsinghua University           Sophomore     Chunpeng Lv  ...        N

# 由于多级索引中的单个元素以元组为单位,因此之前在第一节介绍的 loc 和 iloc 方法完全可以照搬,只需把
# 标量的位置替换成对应的元组,不过在索引前最好对 MultiIndex 进行排序以避免性能警告
df_multi = df_multi.sort_index()
df_multi.loc[('Fudan University', 'Junior')].head()

df_multi.loc[[('Fudan University', 'Senior'),
('Shanghai Jiao Tong University', 'Freshman')]]

df_multi.loc[df_multi.Weight > 70].head() # 布尔列表也是可用的

df_multi.loc[lambda x: ('Fudan University', 'Junior')].head()

# 此外,在多级索引中的元组有一种特殊的用法,可以对多层的元素进行交叉组合后索引,
# 但同时需要指定 loc 的列,全选则用 : 表示。其中,每一层需要选中的元素用列表存放,
# 传入 loc 的形式为 [(level_0_list, level_1_list), cols]

# 例如,想要得到所有北大和复旦的大二大三学生,可以如下写出
res = df_multi.loc[(['Peking University', 'Fudan University'],
['Sophomore', 'Junior']), :]

# 是选出北大的大三学生和复旦的大二学生
res = df_multi.loc[[('Peking University', 'Junior'),
('Fudan University', 'Sophomore')]]

# 2.3 IndexSliece对象
# 前面介绍的方法,即使在索引不重复的时候,也只能对元组整体进行切片,而不能对每层进行切片,也不允
# 许将切片和布尔列表混合使用,引入 IndexSlice 对象就能解决这个问题。
# Slice 对象一共有两种形式,第一种为 loc[idx[*,*]] 型,第二种为 loc[idx[*,*],idx[*,*]] 型,
# 下面将进行介绍。为了方便演示,下面构造一个 索引 不重复的 DataFrame
np.random.seed(0)
L1, L2 = ['A','B','C'],['a','b','c']
mul_index1 = pd.MultiIndex.from_product([L1,L2],names=('Upper', 'Lower'))
L3,L4 = ['D','E','F'],['d','e','f']
mul_index2 = pd.MultiIndex.from_product([L3,L4],names=('Big', 'Small'))
df_ex = pd.DataFrame(np.random.randint(-9,10,(9,9)), index=mul_index1, columns=mul_index2)
# Big          D        E        F
# Small        d  e  f  d  e  f  d  e  f
# Upper Lower
# A     a      3  6 -9 -6 -6 -2  0  9 -5
#       b     -3  3 -8 -3 -2  5  8 -4  4
#       c     -1  0  7 -4  6  6 -9  9 -6
# B     a      8  5 -2 -9 -8  0 -9  1 -6
#       b      2  9 -7 -9 -9 -5 -4 -3 -1
#       c      8  6 -5  0  1 -8 -8 -2  0
# C     a     -6 -3  2  5  9 -9  5 -6  3
#       b      1  2 -5 -3 -5  6 -6  3 -5
#       c     -1  5  6 -6  6  4  7  8 -4

# 为了使用 silce 对象,先要进行定义
idx = pd.IndexSlice
# loc[idx[*,*]] 型
# 这种情况并不能进行多层分别切片,前一个 * 表示行的选择,后一个 * 表示列的选择,与单纯的 loc 是类似的
df_ex.loc[idx['C':, ('D', 'f'):]]
# Big D E F
# Small f d e f d e f
# Upper Lower
# C a 2 5 9 -9 5 -6 3
# b -5 -3 -5 6 -6 3 -5
# c 6 -6 6 4 7 8 -4

# 另外,也支持布尔序列的索引
df_ex.loc[idx[:'A', lambda x:x.sum()>0]] # 列和大于 0
# Big D F
# Small d e e
# Upper Lower
# A a 3 6 9
# b -3 3 -4
# c -1 0 9

# loc[idx[*,*],idx[*,*]] 型
# 这种情况能够分层进行切片,前一个 idx 指代的是行索引,后一个是列索引
df_ex.loc[idx[:'A', 'b':], idx['E':, 'e':]]
# Big E F
# Small e f e f
# Upper Lower
# A b -2 5 -4 4
# c 6 6 9 -6

# 2.4 多级索引的构造
# 前面提到了多级索引表的结构和切片,那么除了使用 set_index 之外,如何自己构造多级索引呢?
# 常用的有 from_tuples, from_arrays, from_product 三种方法, 它们都是 pd.MultiIndex 对象下的函数。
# from_tuples 指根据传入由元组组成的列表进行构造
my_tuple = [('a','cat'),('a','dog'),('b','cat'),('b','dog')]
pd.MultiIndex.from_tuples(my_tuple, names=['First','Second'])
# MultiIndex([('a', 'cat'),
#             ('a', 'dog'),
#             ('b', 'cat'),
#             ('b', 'dog')],
#            names=['First', 'Second'])

# from_arrays 指根据传入列表中,对应层的列表进行构造
my_array = [list('aabb'), ['cat', 'dog'] * 2]
pd.MultiIndex.from_arrays(my_array, names=['First','Second'])
# MultiIndex([('a', 'cat'),
#             ('a', 'dog'),
#             ('b', 'cat'),
#             ('b', 'dog')],
#            names=['First', 'Second'])

# from_product 指根据给定多个列表的笛卡尔积进行构造
my_list1 = ['a','b']
my_list2 = ['cat', 'dog']
pd.MultiIndex.from_product([my_list1, my_list2], names=['First','Second'])
# MultiIndex([('a', 'cat'),
#             ('a', 'dog'),
#             ('b', 'cat'),
#             ('b', 'dog')],
#            names=['First', 'Second'])

# 3. 索引的常用方法
# 索引层的交换和删除
# 构造一个三级索引的例子
np.random.seed(0)
L1,L2,L3 = ['A','B'],['a','b'],['alpha','beta']
mul_index1 = pd.MultiIndex.from_product([L1,L2,L3], names=('Upper', 'Lower','Extra'))
L4,L5,L6 = ['C','D'],['c','d'],['cat','dog']
mul_index2 = pd.MultiIndex.from_product([L4,L5,L6], names=('Big', 'Small', 'Other'))
df_ex = pd.DataFrame(np.random.randint(-9,10,(8,8)), index=mul_index1, columns=mul_index2)
df_ex
# Big                 C               D
# Small               c       d       c       d
# Other             cat dog cat dog cat dog cat dog
# Upper Lower Extra
# A     a     alpha   3   6  -9  -6  -6  -2   0   9
#             beta   -5  -3   3  -8  -3  -2   5   8
#       b     alpha  -4   4  -1   0   7  -4   6   6
#             beta   -9   9  -6   8   5  -2  -9  -8
# B     a     alpha   0  -9   1  -6   2   9  -7  -9
#             beta   -9  -5  -4  -3  -1   8   6  -5
#       b     alpha   0   1  -8  -8  -2   0  -6  -3
#             beta    2   5   9  -9   5  -6   3   1
# 索引层的交换由 swaplevel 和 reorder_levels 完成,
# 前者只能交换两个层,而后者可以交换任意层,两者都可以指定交换的是轴是哪一个,即行索引或列索引
df_ex.swaplevel(0, 2, axis=1).head() # 列索引的第一层和第三层交换
# Other             cat dog cat dog cat dog cat dog
# Small               c   c   d   d   c   c   d   d
# Big                 C   C   C   C   D   D   D   D
# Upper Lower Extra
# A     a     alpha   3   6  -9  -6  -6  -2   0   9
#             beta   -5  -3   3  -8  -3  -2   5   8
#       b     alpha  -4   4  -1   0   7  -4   6   6
#             beta   -9   9  -6   8   5  -2  -9  -8
# B     a     alpha   0  -9   1  -6   2   9  -7  -9

df_ex.reorder_levels([2, 0, 1], axis=0).head()  # 列表数字指代原来索引中的层
# Big                 C               D
# Small               c       d       c       d
# Other             cat dog cat dog cat dog cat dog
# Extra Upper Lower
# alpha A     a       3   6  -9  -6  -6  -2   0   9
# beta  A     a      -5  -3   3  -8  -3  -2   5   8
# alpha A     b      -4   4  -1   0   7  -4   6   6
# beta  A     b      -9   9  -6   8   5  -2  -9  -8
# alpha B     a       0  -9   1  -6   2   9  -7  -9

# 若想要删除某一层的索引,可以使用 droplevel 方法
df_ex.droplevel(1, axis=1)
# Big                 C               D
# Other             cat dog cat dog cat dog cat dog
# Upper Lower Extra
# A     a     alpha   3   6  -9  -6  -6  -2   0   9
#             beta   -5  -3   3  -8  -3  -2   5   8
#       b     alpha  -4   4  -1   0   7  -4   6   6
#             beta   -9   9  -6   8   5  -2  -9  -8
# B     a     alpha   0  -9   1  -6   2   9  -7  -9
#             beta   -9  -5  -4  -3  -1   8   6  -5
#       b     alpha   0   1  -8  -8  -2   0  -6  -3
#             beta    2   5   9  -9   5  -6   3   1

df_ex.droplevel([0, 1], axis=0)
# Big     C               D
# Small   c       d       c       d
# Other cat dog cat dog cat dog cat dog
# Extra
# alpha   3   6  -9  -6  -6  -2   0   9
# beta   -5  -3   3  -8  -3  -2   5   8
# alpha  -4   4  -1   0   7  -4   6   6
# beta   -9   9  -6   8   5  -2  -9  -8
# alpha   0  -9   1  -6   2   9  -7  -9
# beta   -9  -5  -4  -3  -1   8   6  -5
# alpha   0   1  -8  -8  -2   0  -6  -3
# beta    2   5   9  -9   5  -6   3   1

# 3.2 索引属性的修改
# 通过 rename_axis 可以对索引层的名字进行修改,常用的修改方式是传入字典的映射
df_ex.rename_axis(index={'Upper':'Changed_row'}, columns={'Other':'Changed_Col'}).head()
# 通过 rename 可以对索引的值进行修改,如果是多级索引需要指定修改的层号 level
df_ex.rename(columns={'cat':'not_cat'}, level=2).head()
# Big C D
# Small c d c d
# Other not_cat dog not_cat dog not_cat dog not_cat dog
# Upper Lower Extra
# A a alpha 3 6 -9 -6 -6 -2 0 9
# beta -5 -3 3 -8 -3 -2 5 8
# b alpha -4 4 -1 0 7 -4 6 6
# beta -9 9 -6 8 5 -2 -9 -8
# B a alpha 0 -9 1 -6 2 9 -7 -9

# 传入参数也可以是函数,其输入值就是索引元素
df_ex.rename(index=lambda x: str.upper(x), level=2).head()

# 对于整个索引的元素替换,可以利用迭代器实现
new_values = iter(list('abcdefgh'))
df_ex.rename(index=lambda x: next(new_values), level=2)

# 若想要对某个位置的元素进行修改,在单层索引时容易实现,即先取出索引的 values 属性,再给对得到的列
# 表进行修改,最后再对 index 对象重新赋值。但是如果是多级索引的话就有些麻烦,一个解决的方案是先把
# 某一层索引临时转为表的元素,然后再进行修改,最后重新设定为索引

# 另外一个需要介绍的函数是 map ,它是定义在 Index 上的方法,
# 与前面 rename 方法中层的函数式用法是类似的,只不过它传入的不是层的标量值,
# 而是直接传入索引的元组,这为用户进行跨层的修改提供了遍历

# 例如,可以等价地写出上面的字符串转大写的操作
df_temp = df_ex.copy()
new_idx = df_temp.index.map(lambda x: (x[0], x[1], str.upper(x[2])))
df_temp.index = new_idx
df_temp.head()
# Big C D
# Small c d c d
# Other cat dog cat dog cat dog cat dog
# Upper Lower Extra
# A a ALPHA 3 6 -9 -6 -6 -2 0 9
# BETA -5 -3 3 -8 -3 -2 5 8
# b ALPHA -4 4 -1 0 7 -4 6 6
# BETA -9 9 -6 8 5 -2 -9 -8
# B a ALPHA 0 -9 1 -6 2 9 -7 -9

# 关于 map 的另一个使用方法是对多级索引的压缩
df_temp = df_ex.copy()
new_idx = df_temp.index.map(lambda x: (x[0]+'-'+x[1]+'-'+ x[2]))
df_temp.index = new_idx
# MultiIndex([('A', 'a', 'alpha'),
#             ('A', 'a',  'beta'),
#             ('A', 'b', 'alpha'),
#             ('A', 'b',  'beta'),
#             ('B', 'a', 'alpha'),
#             ('B', 'a',  'beta'),
#             ('B', 'b', 'alpha'),
#             ('B', 'b',  'beta')],
#            names=['Upper', 'Lower', 'Extra'])

# 同时,也可以反向地展开
new_idx = df_temp.index.map(lambda x:tuple(x.split('-')))
# Big C D
# Small c d c d
# Other cat dog cat dog cat dog cat dog
# A a alpha 3 6 -9 -6 -6 -2 0 9
# beta -5 -3 3 -8 -3 -2 5 8
# b alpha -4 4 -1 0 7 -4 6 6
# beta -9 9 -6 8 5 -2 -9 -8
# B a alpha 0 -9 1 -6 2 9 -7 -9

# 3.3 索引的设置与重置
df_new = pd.DataFrame({'A':list('aacd'),'B':list('PQRT'),'C':[1,2,3,4]})
df_new
#    A  B  C
# 0  a  P  1
# 1  a  Q  2
# 2  c  R  3
# 3  d  T  4

# 索引的设置可以使用 set_index 完成,
# 这里的主要参数是 append ,表示是否来保留原来的索引,直接把新设定的添加到原索引的内层
df_new.set_index('A')
#    A  B  C
# 0  a  P  1
# 1  a  Q  2
# 2  c  R  3
# 3  d  T  4

df_new.set_index('A', append=True)
#      B  C
#   A
# 0 a  P  1
# 1 a  Q  2
# 2 c  R  3
# 3 d  T  4

# 如果想要添加索引的列没有出现再其中,那么可以直接在参数中传入相应的 Series
my_index = pd.Series(list('WXYZ'), name='D')
df_new = df_new.set_index(['A', my_index])
df_new
#      B  C
# A D
# a W  P  1
#   X  Q  2
# c Y  R  3
# d Z  T  4

# reset_index 是 set_index 的逆函数,
# 其主要参数是 drop ,表示是否要把去掉的索引层丢弃,而不是添加到列中
df_new.reset_index(['D'])
#    D  B  C
# A
# a  W  P  1
# a  X  Q  2
# c  Y  R  3
# d  Z  T  4

df_new.reset_index(['D'], drop=True)
# B C
# A
# a P 1
# a Q 2
# c R 3
# d T 4

# 如果重置了所有的索引,那么 pandas 会直接重新生成一个默认索引
df_new.reset_index()
# A D B C
# 0 a W P 1
# 1 a X Q 2
# 2 c Y R 3
# 3 d Z T 4

# 3.4索引的变形
# 在某些场合下,需要对索引做一些扩充或者剔除,更具体地要求是给定一个新的索引,把原表中相应的索引
# 对应元素填充到新索引构成的表中。

# 例如,下面的表中给出了员工信息,需要重新制作一张新的表,要求增加一名员工的同时去掉身高列并增加性别列:

df_reindex = pd.DataFrame({"Weight":[60,70,80], "Height":[176,180,179]},
     index=['1001','1003','1002'])

df_reindex
#       Weight  Height
# 1001      60     176
# 1003      70     180
# 1002      80     179

df_reindex.reindex(index=['1001','1002','1003','1004'],
     columns=['Weight','Gender'])
#       Weight  Gender
# 1001    60.0     NaN
# 1002    80.0     NaN
# 1003    70.0     NaN
# 1004     NaN     NaN

# 这种需求常出现在时间序列索引的时间点填充以及 ID 编号的扩充。
# 另外,需要注意的是原来表中的数据和新表中会根据索引自动对其,
# 例如原先的 1002 号位置在 1003 号之后,而新表中相反,
# 那么 reindex 中会根据元素对其,与位置无关


# 还有一个与 reindex 功能类似的函数是 reindex_like ,其功能是仿照传入的表的索引来进行被调用表索引的
# 变形。例如,现在以及存在一张表具备了目标索引的条件,那么上述功能可以如下等价地写出:

df_existed = pd.DataFrame(
    index=['1001','1002','1003','1004'],
     columns=['Weight','Gender'])
df_reindex.reindex_like(df_existed)

# 4.索引运算
# 4.1集合的运算法则
# 经常会有一种利用集合运算来取出符合条件行的需求,例如有两张表 A 和 B ,它们的索引都是员工编号,现
# 在需要筛选出两表索引交集的所有员工信息,此时通过 Index 上的运算操作就很容易实现

# 4.2一般的索引运算
# 由于集合的元素是互异的,但是索引中可能有相同的元素,先用 unique 去重后再进行运算。
df_set_1 = pd.DataFrame([[0,1],[1,2],[3,4]],
     index = pd.Index(['a','b','a'],name='id1'))
#      0  1
# id1
# a    0  1
# b    1  2
# a    3  4

df_set_2 = pd.DataFrame([[4,5],[2,6],[7,1]],
     index = pd.Index(['b','b','c'],name='id2'))
#      0  1
# id2
# b    4  5
# b    2  6
# c    7  1

id1, id2 = df_set_1.index.unique(), df_set_2.index.unique()
id1.intersection(id2) # id1 & id2
# Index(['b'], dtype='object')

id1.union(id2) # id1 | id2
# Index(['a', 'b', 'c'], dtype='object')

id1.difference(id2) #  (id1 ^ id2) & id1
# Index(['a'], dtype='object')

id1.symmetric_difference(id2) # id1 ^ id2 # ^ 符号即对称差,不共同拥有的
# Index(['a', 'c'], dtype='object')


# 若两张表需要做集合运算的列并没有被设置索引,一种办法是先转成索引,运算后再恢复,另一种方法是利用 isin 函数
# 在重置索引的第一张表中选出 id 列交集的所在行
df_set_in_col_1 = df_set_1.reset_index()
df_set_in_col_2 = df_set_2.reset_index()
df_set_in_col_1
df_set_in_col_2
df_set_in_col_1[df_set_in_col_1.id1.isin(df_set_in_col_2.id2)]

缺失数据

# 缺失数据
# 缺失值统计和删除
#  缺失数据可以使用 isna 或 isnull (两个函数没有区别)来查看每个单元格是否缺失,通过和 sum 的组合可
# 以计算出每列缺失值的比例
import numpy as np
import pandas as pd

df = pd.DataFrame([1, np.NAN], columns=['Height'])
df.isna().sum() / df.shape[0]  # 查看缺失的比例
# 如果想要查看某一列缺失或者非缺失的行,可以利用 Series 上的 isna 或者 notna 进行布尔索引
df[df.Height.isna()]
df[df.Height.notna()]
# 全部缺失 all
# 至少有一个 any

# 缺失信息的删除
# dropna函数可以删除缺失值,主要参数为
# 轴方向axis(默认0)
# 删除方式 how
# 删除的非缺失值个数阈值 thresh
# (非缺失值没有达到这个数量的相应维度会被删除)
# 备选的删除子集 subset
# 其中 how 主要有 any 和 all两种参数可以选择

# 删除身高体重至少有一个缺失的行
res = df.dropna(how='any', subset=['Height', 'Weight'])
# 等价
# res = df.loc[df[['Height', 'Weight']].notna().all(1)]
# 删除超过 15 个缺失值的列
res = df.dropna(1, thresh=df.shape[0] - 15)
# 等价
# res = df.loc[:, ~(df.isna().sum()>15)]

# 缺失值的填充和插值

# fillna
# 在 fillna 中有三个参数是常用的:value, method, limit 。其中,value 为填充值,可以是标量,也可以是索
# 引到元素的字典映射;method 为填充方法,有用前面的元素填充 ffill 和用后面的元素填充 bfill 两种类型,
# limit 参数表示连续缺失值的最大填充次数。


s = pd.Series([np.nan, 1, np.nan, np.nan, 2, np.nan], list('aaabcd'))
s
# a    NaN
# a    1.0
# a    NaN
# b    NaN
# c    2.0
# d    NaN
# dtype: float64
s.fillna(method='ffill')  # 用前面的值向后填充
# a    NaN
# a    1.0
# a    1.0
# b    1.0
# c    2.0
# d    2.0
# dtype: float64
s.fillna(method='ffill', limit=1)  # 连续出现的缺失,最多填充一次

s.fillna(s.mean())  # value 为标量

s.fillna({'a': 100, 'd': 200})  # 通过索引映射填充的值
# a    100.0
# a      1.0
# a    100.0
# b      NaN
# c      2.0
# d    200.0

# 有时为了更加合理地填充,需要先进行分组后再操作
df.groupby('Grade')['Height'].transform(lambda x: x.fillna(x.mean())).head()

# 插值函数
# 包括了大量scipy的方法,即线性插值、最近邻插值和索引插值

# 对于 interpolate 而言,除了插值方法(默认为 linear 线性插值)之外,
# 有与 fillna 类似的两个常用参数,
# 一个是控制方向的 limit_direction
# 另一个是控制最大连续缺失值插值个数的 limit 。
# 其中,限制插值的方向
# 默认为 forward ,这与 fillna 的 method 中的 ffill 是类似的,若想要后向限制插值或者双向限制插值可以指
# 定为 backward 或 both

s = pd.Series([np.nan, np.nan, 1, np.nan, np.nan, np.nan, 2, np.nan, np.nan])
s.values
# 在默认线性插值法下分别进行 backward 和双向限制插值,同时限制最大连续条数为 1
res = s.interpolate(limit_direction='backward', limit=1)
res.values
# array([ nan, 1.  , 1.  ,  nan,  nan, 1.75, 2.  ,  nan,  nan])
res = s.interpolate(limit_direction='both', limit=1)
res.values
# 最近邻插值,即缺失值的元素和离它最近的非缺失值元素一样
s.interpolate('nearest').values
# array([nan, nan, 1., 1., 1., 2., 2., nan, nan])
s = pd.Series([0, np.nan, 10], index=[0, 1, 10])
# 0 0.0
# 1 NaN
# 10 10.0
s.interpolate()  # 默认的线性插值,等价于计算中点的值
# 0 0.0
# 1 5.0
# 10 10.0
# dtype: float64
s.interpolate(method='index')  # 和索引有关的线性插值,计算相应索引大小对应的值
# 0      0.0
# 1      1.0
# 10    10.0
# dtype: float64

# Nullable类型
# 缺失记号及其缺憾
# 在 python 中的缺失值用 None 表示,该元素除了等于自己本身之外,与其他任何元素不相等
# 在 numpy 中利用 np.nan 来表示缺失值,该元素除了不和其他任何元素相等之外,和自身的比较结果也返回False

# 值得注意的是,虽然在对缺失序列或表格的元素进行比较操作的时候,np.nan 的对应位置会返回 False ,但
# 是在使用 equals 函数进行两张表或两个序列的相同性检验时,会自动跳过两侧表都是缺失值的位置,直接返回 True
s1 = pd.Series([1, np.nan])
s2 = pd.Series([1, 2])
s3 = pd.Series([1, np.nan])
s1 == 1
# 0 True
# 1 False
# dtype: bool
s1.equals(s2)
# False
s1.equals(s3)
# True

# 在时间序列的对象中,pandas 利用 pd.NaT 来指代缺失值,它的作用和 np.nan 是一致的
pd.to_timedelta(['30s', np.nan])  # Timedelta 中的 NaT
# TimedeltaIndex(['0 days 00:00:30', NaT], dtype='timedelta64[ns]', freq=None)
pd.to_datetime(['20200101', np.nan])  # Datetime 中的 NaT

# np.nan 的本身是一种浮点类型,由于 np.nan 的浮点性质,如果在一个整数的 Series 中出现缺失,那么其类型会转变为 float64 ;而如
# 果在一个布尔类型的序列中出现缺失,那么其类型就会转为 object 而不是 bool :
type(np.nan)
# float
pd.Series([1, np.nan]).dtype
# dtype('float64')
pd.Series([True, False, np.nan]).dtype
# dtype('O')

# Nullable类型的性质
s = pd.Series(['a', 'b'])
s_bool = pd.Series([True, np.nan])
s_boolean = pd.Series([True, np.nan]).astype('boolean')
# 带有缺失的布尔列表无法进行索引器中的选择,而 boolean 会把缺失值看作 False
# s[s_bool] # 报错
s[s_boolean]
# 0 a
# dtype: object
# 进行逻辑运算时,bool 类型在缺失处返回的永远是 False ,而 boolean 会根据逻辑运算是否能确
# 定唯一结果来返回相应的值
s_boolean & True
# 0 True
# 1 
# dtype: boolean
s_boolean | True
# 0 True
# 1 True
# dtype: boolean
~s_boolean  # 取反操作同样是无法唯一地判断缺失结果
# 0 False
# 1 
# dtype: boolean

# 缺失数据的计算和分组

#  当调用函数 sum, prob 使用加法和乘法的时候,缺失数据等价于被分别视作 0 和 1,即不改变原来的计算结果
s = pd.Series([2, 3, np.nan, 4, 5])
s.sum()
# 14
s.prod()
# 120.0

# 当使用累计函数时,会自动跳过缺失值所处的位置
s.cumsum()
# 0 2.0
# 1 5.0
# 2 NaN
# 3 9.0
# 4 14.0
# dtype: float64

# 当进行单个标量运算的时候,除了 np.nan ** 0 和 1 ** np.nan 这两种情况为确定的值之外,所有运算结果
# 全为缺失(pd.NA 的行为与此一致)
#  np.nan 在比较操作时一定返回 False ,而 pd.NA 返回 pd.NA
np.nan == 0
#  False
pd.NA > 0
#  

np.nan ** 0
# 1.0
pd.NA ** 0
# 1
1 ** np.nan
# 1.0
1 ** pd.NA
# 1

# diff, pct_change 这两个函数虽然功能相似,但是对于缺失的处理不同,前者凡是参与缺
# 失计算的部分全部设为了缺失值,而后者缺失值位置会被设为 0% 的变化率
s.diff()
# 0 NaN
# 1 1.0
# 2 NaN
# 3 NaN
# 4 1.0
# dtype: float64

s.pct_change()
# 0 NaN
# 1 0.500000
# 2 0.000000
# 3 0.333333
# 4 0.250000
# dtype: float64

# 对于一些函数而言,缺失可以作为一个类别处理,例如在 groupby, get_dummies 中可以设置相应的参数来
# 进行增加缺失类别

df_nan = pd.DataFrame(
    {'category': ['a', 'a', 'b', np.nan, np.nan], 'value': [1, 3, 5, 7, 9]})
df_nan
#   category  value
# 0        a      1
# 1        a      3
# 2        b      5
# 3      NaN      7
# 4      NaN      9

df_nan.groupby('category', dropna=False)['value'].mean()  # pandas 版本大于 1.1.0
# category
# a      2
# b      5
# NaN    8
# Name: value, dtype: int64

pd.get_dummies(df_nan.category, dummy_na=True)
#    a  b  NaN
# 0  1  0    0
# 1  1  0    0
# 2  0  1    0
# 3  0  0    1
# 4  0  0    1

文本数据

# 文本数据
# str 对象是定义在 Index 或 Series 上的属性,专门用于逐元素处理文本内容,其内部定义了大量方法,因此
# 对一个序列进行文本处理,首先需要获取其 str 对象。
var = 'abcd'
str.upper(var)  # Python 内置 str 模块
# ABCD'

s = pd.Series(['abcd', 'efg', 'hi'])
s.str
s.str.upper()

# 索引器
# 在一般的字符串中,通过 [] 可以取出某个位置的元素
var[0]
var[-1: 0: -2]
s.str[0]
s.str[-1: 0: -2]

# string对象
# object 类型只应当存储混合类型,例如同时存储浮点、字符串、
# 字典、列表、自定义类型等,因此字符串有必要同数值型或 category 一样,具有自己的数据存放类型,从而
# 引入了 string 类型。
# 首先,应当尽量保证每一个序列中的值都是字符串的情况下才使用 str 属性
# string 类型的 str 对象和 object 类型的 str 对象返回结果可能是不同的。
s = pd.Series([{1: 'temp_1', 2: 'temp_2'}, ['a', 'b'], 0.5, 'my_string'])
s.str[1]
# 0 temp_1
# 1 b 2 NaN
# 3 y
# dtype: object
s.astype('string').str[1]
# 0    1
# 1    '
# 2    .
# 3    y
# dtype: string
# 除了最后一个字符串元素,前三个元素返回的值都不同,其原因在于当序列类型为 object 时,是对于每一个
# 元素进行 [] 索引,因此对于字典而言,返回 temp_1 字符串,对于列表则返回第二个值,而第三个为不可迭
# 代对象,返回缺失值,第四个是对字符串进行 [] 索引

# 除了对于某些对象的 str 序列化方法不同之外,两者另外的一个差别在于,string 类型是 Nullable 类型,但
# object 不是。这意味着 string 类型的序列,如果调用的 str 方法返回值为整数 Series 和布尔 Series 时,其分
# 别对应的 dtype 是 Int 和 boolean 的 Nullable 类型,而 object 类型则会分别返回 int/float 和 bool/object
# ,取决于缺失值的存在与否。同时,字符串的比较操作,也具有相似的特性,string 返回 Nullable 类型,但
# object 不会。

s = pd.Series(['a', np.nan])  # 带有缺失值
s.str.len()
# 0 1.0
# 1 NaN
# dtype: float64

s.astype('string').str.len()
# 0 1
# 1 
# dtype: Int64

# 正则表达式基础
# 正则表达式是一种按照某种正则模式,从左到右匹配字符串中内容的一种工具

# 使用findall函数来匹配所有出现过但是不重叠的模式,
# 第一个参数是正则表达式,第二个参数是待匹配的字符串。
import re

re.findall('Apple', 'Apple! This Is an Apple!')
#  ['Apple', 'Apple']
# 基础
# 元字符 描述
# . 匹配除换行符以外的任意字符
# [ ] 字符类,匹配方括号中包含的任意字符。
# [^ ] 否定字符类,匹配方括号中不包含的任意字符
# * 匹配前面的子表达式零次或多次
# + 匹配前面的子表达式一次或多次
# ? 匹配前面的子表达式零次或一次
# {n,m} 花括号,匹配前面字符至少 n 次,但是不超过 m 次
# (xyz) 字符组,按照确切的顺序匹配字符 xyz。 | 分支结构,匹配符号之前的字符或后面的字符
# \ 转义符,它可以还原元字符原来的含义
# ^ 匹配行的开始
# $ 匹配行的结束
re.findall('.', 'abc')
#  ['a', 'b', 'c']
re.findall('[ac]', 'abc')
# ['a', 'c']
re.findall('[^ac]', 'abc')
# ['b']
re.findall('[ab]{2}', 'aaaabbbb')  # {n} 指匹配 n 次
#  ['aa', 'aa', 'bb', 'bb']
re.findall('aaa|bbb', 'aaaabbbb')
# ['aaa', 'bbb']
re.findall('a\\?|a\*', 'aa?a*a')
# ['a?', 'a*']
re.findall('a?.', 'abaacadaae')
['ab', 'aa', 'c', 'ad', 'aa', 'e']
# 简写字符集
# 简写 描述
# \w 匹配所有字母、数字、下划线: [a-zA-Z0-9_]
# \W 匹配非字母和数字的字符: [^\w]
# \d 匹配数字: [0-9]
# \D 匹配非数字: [^\d]
# \s 匹配空格符: [\t\n\f\r\p{Z}]
# \S 匹配非空格符: [^\s]
# \B 匹配一组非空字符开头或结尾的位置,不代表具体字符
re.findall('.s', 'Apple! This Is an Apple!')
# ['is', 'Is']
re.findall('\w{2}', '09 8? 7w c_ 9q p@')
#  ['09', '7w', 'c_', '9q']
re.findall('\w\W\B', '09 8? 7w c_ 9q p@')
#  ['8?', 'p@']
re.findall('.\s.', 'Constant dropping wears the stone.')
# ['t d', 'g w', 's t', 'e s']
re.findall('上海市 (.{2,3} 区)(.{2,3} 路)(\d+ 号)', '上海市黄浦区方浜中路 249 号 上海市宝山区密山路 5 号')
# [('黄浦区', '方浜中路', '249 号'), ('宝山区', '密山路', '5 号')]

# 文本处理的五类操作
# 拆分
# str.split 能够把字符串的列进行拆分,其中第一个参数为正则表达式,
# 可选参数包括从左到右的最大拆分次数 n ,是否展开为多个列 expand 。
s = pd.Series(['上海市黄浦区方浜中路 249 号', '上海市宝山区密山路 5 号'])
s.str.split('[市区路]')
# 0 [上海, 黄浦, 方浜中, 249 号] 1 [上海, 宝山, 密山, 5 号]
# dtype: object
s.str.split('[市区路]', n=2, expand=True)
# 0 1 2 0 上海 黄浦 方浜中路 249 号 1 上海 宝山 密山路 5 号
# 与其类似的函数是 str.rsplit ,其区别在于使用 n 参数的时候是从右到左限制最大拆分次数。但是当前版本
# 下 rsplit 因为 bug 而无法使用正则表达式进行分割

# 合并
# 关于合并一共有两个函数,分别是 str.join 和 str.cat 。
# str.join 表示用某个连接符把 Series 中的字符串列表
# 连接起来,如果列表中出现了字符串元素则返回缺失值
s = pd.Series([['a', 'b'], [1, 'a'], [['a', 'b'], 'c']])
s.str.join('-')
# 0 a-b
# 1 NaN
# 2 NaN
# dtype: object
# str.cat 用于合并两个序列,主要参数为连接符 sep 、连接形式 join 以及缺失值替代符号 na_rep ,其中连接
# 形式默认为以索引为键的左连接
s1 = pd.Series(['a', 'b'])
s2 = pd.Series(['cat', 'dog'])
s1.str.cat(s2, sep='-')
# 0 a-cat
# 1 b-dog
# dtype: object
s2.index = [1, 2]
s1.str.cat(s2, sep='-', na_rep='?', join='outer')

# 匹配
# str.contains 返回了每个字符串是否包含正则模式的布尔序列
s = pd.Series(['my cat', 'he is fat', 'railway station'])
s.str.contains('\s\wat')
# 0     True
# 1     True
# 2    False
# dtype: bool

s.str.startswith('my')
# 0 True
# 1 False
# 2 False
# dtype: bool

s.str.endswith('t')
# 1 True
# 2 False
# dtype: bool

# 要用正则表达式来检测开始或结束字符串的模式,可以使用 str.match ,其返回了每个字符串起始处
# 是否符合给定正则模式的布尔序列

s.str.match('m|h')
# 0     True
# 1     True
# 2    False
# dtype: bool

s.str[::-1].str.match('ta[f|g]|n')  # 反转后匹配
# 0 False
# 1 True
# 2 True
# dtype: bool

s.str.contains('^[m|h]')
# 0 True
# 1 True
# 2 False
# dtype: bool

s.str.contains('^[m|h]')
# 0 False
# 1 True
# 2 True
# dtype: bool

# 除了上述返回值为布尔的匹配之外,还有一种返回索引的匹配函数,即 str.find 与 str.rfind ,其分别返回从左
# 到右和从右到左第一次匹配的位置的索引,未找到则返回-1。需要注意的是这两个函数不支持正则匹配,只
# 能用于字符子串的匹配
s = pd.Series(['This is an apple. That is not an apple.'])
s.str.find('apple')
# 0 11
# dtype: int64
s.str.rfind('apple')
# 0 33
# dtype: int64

# 替换
# str.replace 和 replace 并不是一个函数,在使用字符串替换时应当使用前者。
s = pd.Series(['a_1_b', 'c_?'])
s.str.replace('\d|\?', 'new')
# 0 a_new_b
# 1 c_new
# dtype: object

# 当需要对不同部分进行有差别的替换时,可以利用 子组 的方法,并且此时可以通过传入自定义的替换函数
# 来分别进行处理,注意 group(k) 代表匹配到的第 k 个子组(圆括号之间的内容)
s = pd.Series(['上海市黄浦区方浜中路 249 号',
               '上海市宝山区密山路 5 号',
               '北京市昌平区北农路 2 号'])

pat = '(\w+ 市)(\w+ 区)(\w+ 路)(\d+ 号)'

city = {'上海市': 'Shanghai', '北京市': 'Beijing'}
district = {'昌平区': 'CP District',
            '黄浦区': 'HP District',
            '宝山区': 'BS District'}

road = {'方浜中路': 'Mid Fangbin Road',
        '密山路': 'Mishan Road',
        '北农路': 'Beinong Road'}


def my_func(m):
    str_city = city[m.group(1)]
    str_district = district[m.group(2)]
    str_road = road[m.group(3)]
    str_no = 'No. ' + m.group(4)[:-1]
    return ' '.join([str_city, str_district, str_road, str_no])


s.str.replace(pat, my_func)

# 提取
# 提取既可以认为是一种返回具体元素(而不是布尔值或元素对应的索引位置)的匹配操作,也可以认为是一
# 种特殊的拆分操作。前面提到的 str.split 例子中会把分隔符去除,这并不是用户想要的效果,这时候就可以
# 用 str.extract 进行提取:
# pat = '(\w+ 市)(\w+ 区)(\w+ 路)(\d+ 号)'
# s.str.extract(pat)
# 通过子组的命名,可以直接对新生成 DataFrame 的列命名
# pat = '(?P< 市名>\w+ 市)(?P< 区名>\w+ 区)(?P< 路名>\w+ 路)(?P< 编号>\d+ 号)'
# s.str.extract(pat)

# str.extractall 不同于 str.extract 只匹配一次,它会把所有符合条件的模式全部匹配出来,如果存在多个结果,
# 则以多级索引的方式存储
s = pd.Series(['A135T15,A26S5', 'B674S2,B25T6'], index=['my_A', 'my_B'])
pat = '[A|B](\d+)[T|S](\d+)'
s.str.extractall(pat)
#              0   1
#      match
# my_A 0      135  15
#      1       26   5
# my_B 0      674   2
#      1       25   6

pat_with_name = '[A|B](?P\d+)[T|S](?P\d+)'
s.str.extractall(pat_with_name)
#            name1 name2
#      match
# my_A 0       135    15
#      1        26     5
# my_B 0       674     2
#      1        25     6

# str.findall 的功能类似于 str.extractall ,区别在于前者把结果存入列表中,而后者处理为多级索引,每个行
# 只对应一组匹配,而不是把所有匹配组合构成列表。
s.str.findall(pat)
# my_A [(135, 15), (26, 5)]
# my_B [(674, 2), (25, 6)]
# dtype: object


# 常用字符串函数
# 字母型函数  :upper, lower, title, capitalize, swapcase

# 数值型函数 : 着重需要介绍的是 pd.to_numeric 方法,
# 它虽然不是 str 对象上的方法,但是能够对字符格式的数值进行快速转换和筛选。
# 其主要参数包括 errors 和 downcast 分别代表了非数值的处理模式和转换类型。
# 其中,对于不能转换为数值的有三种 errors 选项,
# raise, coerce,ignore 分别表示直接报错、设为缺失以及保持原来的字符串。
s = pd.Series(['1', '2.2', '2e', '??', '-2.1', '0'])
pd.to_numeric(s, errors='ignore')
# 0       1
# 1     2.2
# 2      2e
# 3      ??
# 4    -2.1
# 5       0
# dtype: object
pd.to_numeric(s, errors='coerce')
# 0 1.0
# 1 2.2
# 2 NaN
# 3 NaN
# 4 -2.1
# 5 0.0
# dtype: float64

# 在数据清洗时,可以利用 coerce 的设定,快速查看非数值型的行
s[pd.to_numeric(s, errors='coerce').isna()]
# 2    2e
# 3    ??
# dtype: object
s.str.len()
# 0 14
# 1 19
# dtype: int64


# 统计型函数
# count 和 len 的作用分别是返回出现正则模式的次数和字符串的长度
s = pd.Series(['cat rat fat at', 'get feed sheet heat'])
s.str.count('[r|f]at|ee')


# 格式型函数
# 格式型函数主要分为两类,第一种是除空型,第二种时填充型。
# 其中,第一类函数一共有三种,它们分别是 strip, rstrip, lstrip ,
# 分别代表去除两侧空格、右侧空格和左侧空格。这些函数在数据清洗时是有用的,特别是列名含有非法空格的时候
my_index = pd.Index([' col1', 'col2 ', ' col3 '])
my_index.str.strip().str.len()
# 对于填充型函数而言,pad 是最灵活的,它可以选定字符串长度、填充的方向和填充内容
s = pd.Series(['a', 'b', 'c'])
s.str.pad(5, 'left', '*')
# 0 ****a
# 1 ****b
# 2 ****c
# dtype: object

s.str.pad(5, 'right', '*')
s.str.pad(5,'both','*')
# 上述的三种情况可以分别用 rjust, ljust, center 来等效完成,需要注意 ljust 是指右侧填充而不是左侧填充

# 在读取 excel 文件时,经常会出现数字前补 0 的需求,例如证券代码读入的时候会把”000007”作为数值 7
# 来处理,pandas 中除了可以使用上面的左侧填充函数进行操作之外,还可用 zfill 来实现。
s = pd.Series([7, 155, 303000]).astype('string')
s.str.pad(6,'left','0')
# 0 000007
# 1 000155
# 2 303000
# dtype: string
s.str.rjust(6, '0')
s.str.zfill(6)

分类数据

# 分类数据
# 1.cat对象
# 1.1 cat对象的属性
# 在 pandas 中提供了 category 类型,使用户能够处理分类类型的变量,将一个普通序列转换成分类变量可以
# 使用 astype 方法。
import pandas as pd

df = pd.DataFrame()

s = df.Grade.astype('category')

# 对于一个具体的分类,有两个组成部分,其一为类别的本身,它以 Index 类型存储,其二为是否有序,它们
# 都可以通过 cat 的属性被访问
s.cat.categories

s.cat.ordered

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

# 1.2 类别的增加、删除和修改
# 对于类别的增加可以使用 add_categories :
s = s.cat.add_categories('Graduate')  # 增加一个毕业生类别
s.cat.categories

# 若要删除某一个类别可以使用 remove_categories ,同时所有原来序列中的该类会被设置为缺失。
s = s.cat.remove_categories('Freshman')

# 此外可以使用 set_categories 直接设置序列的新类别,原来的类别中如果存在元素不属于新类别,那么会被
# 设置为缺失。
s = s.cat.set_categories(['Sophomore', 'PhD'])

# 如果想要删除未出现在序列中的类别,可以使用 remove_unused_categories 来实现:
s = s.cat.remove_unused_categories()

# 可以通过 rename_categories 方法完成修改,是,这
# 个方法会对原序列的对应值也进行相应修改。
s = s.cat.rename_categories({'Sophomore': '本科二年级学生'})

# 2.有序分类
# 有序类别和无序类别可以通过 as_unordered 和 reorder_categories 互相转化,需要注意的是后者传入的参
# 数必须是由当前序列的无需类别构成的列表,不能够增加新的类别,也不能缺少原来的类别,并且必须指定
# 参数 ordered=True ,否则方法无效。
s = df.Grade.astype('category')

s = s.cat.reorder_categories(['Freshman', 'Sophomore', 'Junior', 'Senior'],
                             ordered=True)

s.cat.as_unordered().head()

# 排序和比较
# 分类变量的排序:只需把列的类型修改
# 为 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()  # 值排序
df.set_index('Grade').sort_index().head()  # 索引排序

# 由于序的建立,因此就可以进行比较操作。分类变量的比较操作分为两类,第一种是 == 或 != 关系的比较,
# 比较的对象可以是标量或者同长度的 Series (或 list ),第二种是 >,>=,<,<= 四类大小关系的比较,比较
# 的对象和第一种类似,但是所有参与比较的元素必须属于原序列的 categories ,同时要和原序列具有相同的
# 索引
res1 = df.Grade == 'Sophomore'
res2 = df.Grade == ['PhD'] * df.shape[0]

# 3.区间类别
# 利用 cut 和 qcut 进行区间构造
# 区间序列往往是通过 cut 和 qcut 方法进行构造的,这两个函
# 数能够把原序列的数值特征进行装箱,即用区间位置来代替原来的具体数值

# 最重要的参数是 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]]

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 ,分别代表了区间的名字和是否返回分割点(默认不返回)
s = pd.Series([1, 2])
res = pd.cut(s, bins=2, labels=['small', 'big'], retbins=True)

res[0]
# 0 small
# 1 big
# dtype: category
# Categories (2, object): ['small' < 'big']

res[1]  # 该元素为返回的分割点
# array([0.999, 1.5 , 2. ])

# 从用法上来说,qcut 和 cut 几乎没有差别,只是把 bins 参数变成的 q 参数,qcut 中的 q 是指 quantile 。这
# 里的 q 为整数 n 时,指按照 n 等分位数把数据分箱,还可以传入浮点列表指代相应的分位数分割点。

s = df.Weight
pd.qcut(s, q=3).head()
pd.qcut(s, q=[0, 0.2, 0.8, 1]).head()

# 一般区间的构造
# 对于某一个具体的区间而言,其具备三个要素,即左端点、右端点和端点的开闭状态,其中开闭状态可以指
# 定 right, left, both, neither 中的一类
my_interval = pd.Interval(0, 1, 'right')
my_interval
# Interval(0, 1, closed='right')
# 其属性包含了 mid, length, right, left, closed ,分别表示中点、长度、右端点、左端点和开闭状态

# 使用 in 可以判断元素是否属于区间
0.5 in my_interval

# 使用 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 参数就对应了这四个量,从而就能构造出相应的区间
pd.interval_range(start=1, end=5, periods=8)
pd.interval_range(end=5, periods=8, freq=0.5)

时序数据

# 时序数据
import pandas as pd
import numpy as np

# 1.时间戳
# 一系列的时间戳可以组成 DatetimeIndex ,
# 而将它放到 Series 中后,Series 的类型就变为了 datetime64[ns] ,


# 构造时间戳
ts = pd.Timestamp('2020/1/1')
ts.year
ts.month
ts.day
ts.hour
ts.minute
ts.second

# Datetime序列生成
pd.to_datetime(['2020-1-1', '2020-1-3', '2020-1-6'])
df = pd.read_csv('data/learn_pandas.csv')
s = pd.to_datetime(df.Test_Date)
# 时间戳的格式不满足转换时,可以强制使用 format 进行匹配
temp = pd.to_datetime(['2020\\1\\1', '2020\\1\\3'], format='%Y\\%m\\%d')
temp
# Out[21]: DatetimeIndex(['2020-01-01', '2020-01-03'], dtype='datetime64[ns]', freq=None)
# 由于传入的是列表,而非 pandas 内部的 Series ,因此返回的是 DatetimeIndex ,如果想要转为
# datetime64[ns] 的序列,需要显式用 Series 转化
pd.Series(temp)

# 把表的多列时间属性拼接转为时间序列的 to_datetime 操作,此时的列名必须和以下给定
# 的时间关键词列名一致

df_date_cols = pd.DataFrame({'year': [2020, 2020],
                             'month': [1, 1],
                             'day': [1, 2],
                             'hour': [10, 20],
                             'minute': [30, 50],
                             'second': [20, 40]})
pd.to_datetime(df_date_cols)
# Out[24]:
# 0 2020-01-01 10:30:20
# 1 2020-01-02 20:50:40

# date_range 是一种生成连续间隔时间的一种方法,其重要的参数为 start, end, freq, periods ,它们分别表示
# 开始时间,结束时间,时间间隔,时间戳个数。其中,四个中的三个参数决定了,那么剩下的一个就随之确
# 定了。这里要注意,开始或结束日期如果作为端点则它会被包含

pd.date_range('2020-1-1', '2020-1-21', freq='10D')
# DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21'], dtype='datetime64[ns]', freq='10D')

# dt对象
s = pd.Series(pd.date_range('2020-1-1', '2020-1-3', freq='D'))
# 常用属性包括:date, time, year, month, day, hour, minute, second, microsecond, nanosecond,
# dayofweek, dayofyear, weekofyear, daysinmonth, quarter ,其中 daysinmonth, quarter 分别表示月中的第几
# 天和季度。
s.dt.dayofweek
# 此外,可以通过 month_name, day_name 返回英文的月名和星期名,注意它们是方法而不是属性
s.dt.month_name()
# 测试是否为月/季/年的第一天或者最后一天
s.dt.is_year_start  # 还可选 is_quarter/month_start
# 取整操作包含 round, ceil, floor ,它们的公共参数为 freq ,常用的包括 H, min, S
s = pd.Series(
    pd.date_range('2020-1-1 20:35:00', '2020-1-1 22:35:00', freq='45min'))
s
# 0   2020-01-01 20:35:00
# 1   2020-01-01 21:20:00
# 2   2020-01-01 22:05:00
# dtype: datetime64[ns]
s.dt.round('1H')
s.dt.ceil('1H')
s.dt.floor('1H')

# 时间戳的切片与索引
# 时间戳序列作为索引使用。如果想要选出某个子时间戳序列,第一类方法是利用 dt 对象和布尔
# 条件联合使用,另一种方式是利用切片,后者常用于连续时间戳
s = pd.Series(np.random.randint(2, size=366),
              index=pd.date_range('2020-01-01', '2020-12-31'))
idx = pd.Series(s.index).dt
# 每月的第一天或者最后一天
s[(idx.is_month_start | idx.is_month_end).values].head()
# 双休日
s[idx.dayofweek.isin([5, 6]).values].head()
# 取出单日值
s['2020-01-01']
# 取出七月
s['2020-07'].head()
# 取出 5 月初至 7 月 15 日
s['2020-05':'2020-7-15'].head()
s['2020-05':'2020-7-15'].tail()

# 2.时间差
# pandas 中利用 Timedelta 来表示。类似的,一系列的时间差就组成了 TimedeltaIndex ,而将它放
# 到 Series 中后,Series 的类型就变为了 timedelta64[ns] 。

# 时间差可以理解为两个时间戳的差,这里也可以通过 pd.Timedelta 来构造
pd.Timestamp('20200102 08:00:00') - pd.Timestamp('20200101 07:35:00')
# Timedelta('1 days 00:25:00')
pd.Timedelta(days=1, minutes=25)  # 需要注意加 s
# Timedelta('1 days 00:25:00')
pd.Timedelta('1 days 25 minutes')  # 字符串生成
# Timedelta('1 days 00:25:00')

# 生成时间差序列的主要方式是 pd.to_timedelta ,其类型为 timedelta64[ns] :
s = pd.to_timedelta(df.Time_Record)

# 与 date_range 一样,时间差序列也可以用 timedelta_range 来生成,它们两者具有一致的参数:
pd.timedelta_range('0s', '1000s', freq='6min')
pd.timedelta_range('0s', '1000s', periods=3)

# 对于 Timedelta 序列,同样也定义了 dt 对象,上面主要定义了的属性包括 days, seconds, mircroseconds,
# nanoseconds ,它们分别返回了对应的时间差特征。需要注意的是,这里的 seconds 不是指单纯的秒,而是
# 对天数取余后剩余的秒数
s.dt.seconds.head()
# 如果不想对天数取余而直接对应秒数,可以使用 total_seconds
s.dt.total_seconds().head()
# 与时间戳序列类似,取整函数也是可以在 dt 对象上使用的
pd.to_timedelta(df.Time_Record).dt.round('min').head()

# 时间差支持的常用运算有三类:与标量的乘法运算、与时间戳的加减法运算、与时间差的加减法与除法运算:
td1 = pd.Timedelta(days=1)
td2 = pd.Timedelta(days=3)
ts = pd.Timestamp('20200101')
td1 * 2
# Timedelta('2 days 00:00:00')
td2 - td1
# Timedelta('2 days 00:00:00')
ts + td1
# Timestamp('2020-01-02 00:00:00')
ts - td1
# Timestamp('2019-12-31 00:00:00')

# 这些运算都可以移植到时间差的序列上
td1 = pd.timedelta_range(start='1 days', periods=5)
# TimedeltaIndex(['1 days', '2 days', '3 days', '4 days', '5 days'], dtype='timedelta64[ns]', freq='D')
td2 = pd.timedelta_range(start='12 hours', freq='2H', periods=5)
# TimedeltaIndex(['0 days 12:00:00', '0 days 14:00:00', '0 days 16:00:00',
#                 '0 days 18:00:00', '0 days 20:00:00'],
#                dtype='timedelta64[ns]', freq='2H')
ts = pd.date_range('20200101', '20200105')
# DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
#                '2020-01-05'],
#               dtype='datetime64[ns]', freq='D')
td1 * 5
# TimedeltaIndex(['5 days', '10 days', '15 days', '20 days', '25 days'], dtype='timedelta64[ns]', freq='5D')
td1 * pd.Series(list(range(5)))
td1 - td2
# TimedeltaIndex(['0 days 12:00:00', '1 days 10:00:00', '2 days 08:00:00',
# '3 days 06:00:00', '4 days 04:00:00'],
# dtype='timedelta64[ns]', freq=None)

# 3.时间段
# 在 pandas
# 利用 Period 来表示。类似的,一系列的时间段就组成了 PeriodIndex ,而将它放到 Series 中后,Series
# 的类型就变为了 Period 。

# 日期偏置是一种和日历相关的特殊时间差,如何求 2020 年 9 月 7 日后的第 30 个工作日是哪一天
pd.Timestamp('20200831') + pd.offsets.WeekOfMonth(week=0, weekday=0)
# Timestamp('2020-09-07 00:00:00')
pd.Timestamp('20200907') + pd.offsets.BDay(30)
# pd.Timestamp('20200907') + pd.offsets.BDay(30)

# 从上面的例子中可以看到,Offset 对象在 pd.offsets 中被定义。当使用 + 时获取离其最近的下一个日期,当
# 使用 - 时获取离其最近的上一个日期
pd.Timestamp('20200831') - pd.offsets.WeekOfMonth(week=0, weekday=0)
pd.Timestamp('20200907') - pd.offsets.BDay(30)
pd.Timestamp('20200907') + pd.offsets.MonthEnd()

# 介绍一个特殊的 Offset 对象
# CDay ,其中的 holidays, weekmask 参数能够分别对自定义的日期和星期进行过滤,前者传入了需要过滤的
# 日期列表,后者传入的是三个字母的星期缩写构成的星期字符串,其作用是只保留字符串中出现的星期

my_filter = pd.offsets.CDay(n=1, weekmask='Wed Fri', holidays=['20200109'])
dr = pd.date_range('20200108', '20200111')
dr.to_series().dt.dayofweek
[i + my_filter for i in dr]
# 在当前版本下由于一些 bug ,不要使用 Day 级别以下的 Offset 对象,比如 Hour, Second 等,请
# 使用对应的 Timedelta 对象来代替。
pd.date_range('20200101', '20200331', freq='MS')  # 月初
# DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01'], dtype='datetime64[ns]', freq='MS,')
pd.date_range('20200101', '20200331', freq='M')  # 月末
#  DatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31'], dtype='datetime64[ns]', freq='M→')
pd.date_range('20200101', '20200110', freq='B')  # 工作日
# DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-06',
# '2020-01-07', '2020-01-08', '2020-01-09', '2020-01-10'],
# dtype='datetime64[ns]', freq='B')
pd.date_range('20200101', '20200201', freq='W-MON')  # 周一
pd.date_range('20220101', '20220201', freq='WOM-1MON')  # 每月第一个周一

# 等价
pd.date_range('20200101', '20200331', freq=pd.offsets.MonthBegin())
pd.date_range('20200101', '20200331', freq=pd.offsets.MonthEnd())
pd.date_range('20200101', '20200110', freq=pd.offsets.BDay())
pd.date_range('20200101', '20200201', freq=pd.offsets.CDay(weekmask='Mon'))

# 时序中的滑窗与分组
# 滑动窗口
import matplotlib.pyplot as plt

idx = pd.date_range('20200101', '20201231', freq='B')
np.random.seed(2020)
data = np.random.randint(-1, 2, len(idx)).cumsum()  # 随机游动构造模拟序列
s = pd.Series(data, index=idx)
r = s.rolling('30D')
plt.plot(s)
plt.title('BOLL LINES')
plt.plot(r.mean())
plt.plot(r.mean() + r.std() * 2)
plt.plot(r.mean() - r.std() * 2)

# 对于 shift 函数而言,作用在 datetime64 为索引的序列上时,可以指定 freq 单位进行滑动
s.shift(freq='50D').head()
my_series = pd.Series(s.index)
my_series.diff(1).head()

# 重采样
# 对上面的序列计算每 10 天的均值
s.resample('10D').mean().head()
# 可以通过 apply 方法自定义
s.resample('10D').apply(lambda x: x.max() - x.min()).head()  # 极差
# 区间情况为左闭右开

idx = pd.date_range('20220101 8:26:35', '20220101 9:31:58', freq='77s')
data = np.random.randint(-1, 2, len(idx)).cumsum()
s = pd.Series(data, index=idx)
s
s.resample('1min').mean().head()
# 有时候,用户希望从序列的最小时间戳开始依次增加 freq 进行分组,此时可以指定 origin 参数为 start
s.resample('1min', origin='start').mean().head()

# 意索引一般是取组的第一个时间戳,但 M, A, Q, BM, BA, BQ, W 这七个是取对应区间的
# 最后一个时间戳:
s.resample('M').mean().head()

from borax.calendars.lunardate import LunarDate

# 构建一个农历日期
today = LunarDate(1997, 10, 21, 0)  # 11.20
# 公历转农历
today = LunarDate.from_solar_date(1997, 10, 21)

import datetime as dt

d1 = '1997-11-20'
d2 = '2022-05-25'
date1 = dt.datetime.strptime(d1, "%Y-%m-%d").date()  ##datetime.date(2018, 1, 6)
date2 = dt.datetime.strptime(d2, "%Y-%m-%d").date()  ##datetime.date(2018, 1, 9)
# 计算两个日期date的天数差
Days = (date2 - date1).days
# 计算要给日期 +N天后日期
date1 + dt.timedelta(days=9000)

你可能感兴趣的:(python,python,pandas,数据处理,分组,索引)