【Python数据分析】Pandas统计分析基础,看这一篇就够了!
Pandas
是基于NumPy的数据分析模块,它提供了大量的数据分析会用到的工具,可以说Pnadas
是Python能成为强大数据分析工具的重要原因之一。
导入方式:
import pandas as pd
Pandas
中包含三种数据结构:Series、DataFrame和Panel,中文翻译过来就是相当于序列、数据框和面板。
这么理解可能有点抽象,但是我们将其可以类比为:
它是一种一维数组对象,包含一个值序列,还有索引功能。
import pandas as pd
obj = pd.Series([1,-2,3,-4]) # 仅仅由数组构成
print(obj)
0 1
1 -2
2 3
3 -4
dtype: int64
第一列为index索引,第二列为数据value。
当然,如果你不指定,就会默认用整形数据作为index,但是如果你想用别的方式来作为索引,你可以用别的方式。
i = ["a","b","c","d"]
v = [1,2,3,4]
t = pd.Series(v,index = i,name = "col")
print(t)
a 1
b 2
c 3
d 4
Name: col, dtype: int64
如果数据被放在字典中,就可以直接通过字典来创建Series。
# 字典创建Series
sdata = {'Joolin':20,'Jay':46}
obj2 = pd.Series(sdata)
print(obj2)
Joolin 20
Jay 46
dtype: int64
可以看到,由于字典使用键值对的方式,那么这样直接创建可以省去了创建index的操作。
当然,你也依旧可以指定你的index,那样就会覆盖原先的键。
sdata = {'Joolin':20,'Jay':46}
sp = ["Joolin","JJ"]
obj2 = pd.Series(sdata,index = sp)
print(obj2)
Joolin 20.0
JJ NaN
dtype: float64
我们注意到,被覆盖的那些索引由于找不到与其匹配的值,就会显示**NaN
,即“非数字”。或者如果你多写了没有对应值的索引元素,那么也会显示NaN
**
sdata = {'Joolin':20,'Jay':46}
sp = ["Joolin","JJ","Jay"]
obj2 = pd.Series(sdata,index = sp)
print(obj2)
Joolin 20.0
JJ NaN
Jay 46.0
dtype: float64
对于许多应用而言,Series有一个重要的功能:在算术运算中,它可以自动对齐不同索引的数据。
sdata = {'Joolin':20,'Jay':46}
states = ['Joolin','DT','Jay']
obj1 = pd.Series(sdata)
obj2 = pd.Series(sdata,index = states)
print(obj1+obj2)
DT NaN
Jay 92.0
Joolin 40.0
dtype: float64
DataFrame
是一个表格型的数据结构,它含有一组有序的列,每列都可以是不同的数据类型。我们来参考以下的表格进行理解:
name | city | year | |
---|---|---|---|
0 | 张三 | Beijing | 2001 |
1 | 李四 | Shanghai | 2005 |
2 | 王五 | Guangzhou | 2003 |
这是一个典型的表格,我们看到它包含了例如name、city、year这样的列名,并且每一行都是根据索引自动排序。DataFrame
也是这样一种结构,它既有行索引也有列索引,被看作是Series组成的字典。
我们既可以通过行索引进行操作,也可以通过列索引进行操作,并且注意,它们的优先性是相同的。
一般创建的方式就是通过字典,因为毕竟键值对的方式是最符合DataFrame的特点的。
data = {
'name' : ['张三','李四','王五'],
'city' : ['Beijing','Shanghai','Guangzhou'],
'year' : [2001,2005,2003]
}
df = pd.DataFrame(data)
print(df)
name city year
0 张三 Beijing 2001
1 李四 Shanghai 2005
2 王五 Guangzhou 2003
我们注意,DataFrame自动给我们加上了索引,并且会被有序排列;
如果我们指定列名序列,那么就会按照指定来进行列的顺序
df1 = pd.DataFrame(data,columns = ['city','year','name'])
city year name
0 Beijing 2001 张三
1 Shanghai 2005 李四
2 Guangzhou 2003 王五
如果我们指定行名序列,也就是标签label,那么就会按照指定标签而不再使用默认的0,1,2,3
df2 = pd.DataFrame(data,columns = ['city','year','name'],index = ['a','b','c'])
city year name
a Beijing 2001 张三
b Shanghai 2005 李四
c Guangzhou 2003 王五
同Series一样,如果我们传入列在数据中找不到,那么就会产生NaN值,这里不再赘述。
我们发现,Pandas有个很有用也很特别的东西——就是index索引,它在数据分析中可以起到很大的作用:因为数据往往都是庞大和繁杂的,如果我们直接通过数据本身来进行查找和处理,那么任务就会显得极其繁重。而如果数据有一个对应的值,或者特定的特点,那么就可以快速找到它,这就是索引。
而每个索引对应的数据,就被称作索引对象。
Pandas的索引对象负责管理轴标签和其他元数据,索引对象不能修改,否则会报错。也只有这样才能保证数据的准确性,并且保证索引对象在多个数据结构之间进行安全共享。
我们可以直接查看索引有哪些。
df2 = pd.DataFrame(data,columns = ['city','year','name'],index = ['a','b','c'])
print(df2.index)
print(df2.index[0])
print(df2.index[1])
print(df2.index[2])
Index(['a', 'b', 'c'], dtype='object')
a
b
c
同时索引也有它的方法和属性。
类别 | 方法/属性 | 描述 |
---|---|---|
基本信息 | dtype | 返回索引的数据类型 |
size | 返回索引的元素数量 | |
shape | 返回索引的形状 | |
nbytes | 返回索引的字节数 | |
is_unique | 判断索引是否唯一 | |
is_monotonic_increasing | 判断索引是否单调递增 | |
is_monotonic_decreasing | 判断索引是否单调递减 | |
索引位置 | get_loc() | 返回指定值的索引位置 |
get_indexer() | 返回多个值的索引位置 | |
slice_indexer() | 返回切片索引 | |
contains() | 判断索引是否包含某个值 | |
元素操作 | tolist() | 将索引转换为Python列表 |
map() | 对索引中的元素应用函数 | |
astype() | 改变索引的数据类型 | |
repeat() | 重复索引中的元素 | |
drop() | 删除指定元素 | |
重构 | rename() | 重命名索引 |
reindex() | 重新索引,支持缺失值填充 | |
union() | 返回两个索引的并集 | |
intersection() | 返回两个索引的交集 | |
difference() | 返回两个索引的差集 | |
symmetric_difference() | 返回两个索引的对称差集 | |
排序 | sort_values() | 对索引进行排序 |
sortlevel() | 按多级索引的某一层排序 | |
唯一性与重复性 | unique() | 返回索引中的唯一值 |
duplicated() | 标记重复值 | |
其他 | copy() | 复制索引 |
equals() | 判断两个索引是否相等 | |
append() | 追加一个新的索引 | |
insert() | 在指定位置插入元素 |
包含values
、index
、columns
、ndim
和shape
。
重建索引的格式:
obj.reindex()
索引对象是无法修改的,因此,重建索引指的是:对索引重新排序而不是重新命名,如果某个索引值不存在,则会引入缺失值。
这一点其实在上述的索引操作中已经体现出来了,这里再重新复述一下。
NaN
,但是也可以通过fill_value
来进行参数的填充。obj = pd.Series([7,5,6,8],index=['a','b','d','c'])
print(obj)
obj = obj.reindex(['a','b','c','d','e'],fill_value = 0)
print(obj)
a 7
b 5
d 6
c 8
dtype: int64
a 7
b 5
c 8
d 6
e 0
dtype: int64
ffill
或pad
:前向值填充
bfill
或backfill
:后向值填充
reindex
既可以修改行索引也可以修改列索引,也可以都修改,这根据需求灵活使用即可。
如果我们觉得以前的那个索引方法不适用了,不想要了,那么可以使用set_index
来进行索引的更换。
df = df.set_index('city')
print(df)
name year
city
Beijing 张三 2001
Shanghai 李四 2005
Guangzhou 王五 2003
注意,如果你有这种想法:我想直接更换为新的索引,比如set_index(['a','b','c'])
是不行的,因为这个方法本身只能使用已有的索引,而不能创造新的索引,会这样报错:
KeyError: "None of ['a', 'b', 'c'] are in the columns"
这里就会着重用到我们的索引功能来进行查询了。查询分为四种:选取列、选取行、选取行和列、布尔选择
df['column_name'] # 返回一个Series
df.column_name # 等价于上面的方法
df[['col1', 'col2']] # 返回一个DataFrame
示例:
w1 = df['city'] # 返回一个Series
print(w1)
w2 = df.year # 等价于上面的方法
print(w2)
w3 = df[['city','year']] # 多列查询
select_dtypes
等方法指定查询的范围:示例:
w4= df.select_dtypes(exclude = 'int64').head() # 除了int64类型以外的其他列
print(w4)
name city
0 张三 Beijing
1 李四 Shanghai
2 王五 Guangzhou
print(df[:2]) # 显示前两行
print(df[1:3]) # 显示第2-3行
head
和tail
方法,但它们获得的都是头部和尾部开始的数据,具有局限性。注意:当使用这两个方法不输入参数的时候,是默认前5行和后5行。
sample
来抽取随机样本。
鉴于切片方法选取行有很大的局限性,我们一般使用更方便的方法来进行行和列的选取。
loc(行索引名称或条件,列索引名称)
# 显示name和year两列
print(df.loc[:,['name','year']])
# 显示北京和上海的name和year两列
print(df.loc[['Beijing','Shanghai'],['name','year']])
# 显示year大于等于2002的name和year两列
print(df.loc[df['year']>=2002,['name','year']])
loc
与其他索引方法例如isin
配合使用可以指定查询数据,有没有数据库的感觉?
iloc(行索引位置,列索引位置)
# 选取前两列
print(df.iloc[:,2])
# 选取第1和第3行
print(df.iloc[[0,2]])
query(self,expr,inplace = False,**kwargs)
其中expr是要查询的字符串,kwargs是dict关键字参数。
在Pandas中可以使用逻辑运算符实现布尔选择。
这一部分就涉及到经典的 增删改 操作。注意在Pandas中,我们要先将数据提取出来,再进行编辑。(当然这一部分是原理,我们直接用方法就完事了)
增加列
直接赋值法
data = {'name': ['Alice', 'Bob', 'Charlie'],
'age': [25, 30, 35]}
df = pd.DataFrame(data)
# 直接添加新列
df['city'] = ['New York', 'Los Angeles', 'Chicago']
print(df)
输出:
name age city
0 Alice 25 New York
1 Bob 30 Los Angeles
2 Charlie 35 Chicago
使用assign()
方法
df = df.assign(country=['USA', 'USA', 'USA'])
print(df)
输出:
name age city age_in_5_years country
0 Alice 25 New York 30 USA
1 Bob 30 Los Angeles 35 USA
2 Charlie 35 Chicago 40 USA
增加行
使用append
方法
# 这种方法在Pandas 2.0以后已经被弃用,推荐使用concat()
new_row = pd.DataFrame([['Eve', 27, 'Boston', 32, 'USA']],
columns=df.columns)
df = pd.concat([df, new_row], ignore_index=True)
print(df)
输出:
name age city age_in_5_years country
0 Alice 25 New York 30 USA
1 Bob 30 Los Angeles 35 USA
2 Charlie 35 Chicago 40 USA
3 David 28 San Francisco 33 USA
4 Eve 27 Boston 32 USA
删除数据直接使用**drop
**方法,参数axis来确定删除行还是列。
注意,默认是不删除原数据,如果想要再原数据上删除应设置参数inplace = True。
# 删除单列
df1 = df.drop(columns=['city'], inplace=False)
# print(df)
print(df1)
print(df)
# 同时删除多个列
df.drop(columns=['age', 'country'], inplace=True)
print(df)
name age country
0 Alice 25 USA
1 Bob 30 USA
2 Charlie 35 USA
name age city country
0 Alice 25 New York USA
1 Bob 30 Los Angeles USA
2 Charlie 35 Chicago USA
name city
0 Alice New York
1 Bob Los Angeles
2 Charlie Chicago
修改数据直接对选择的数据赋值即可,但是注意,这里是直接修改数据的值,操作无法撤销,修改了就是修改了。
我们也可以使用replace(to_place,value)
进行数据的替换
to_place是被替换的值;value是要替换成的值。
直接rename()
修改。注意是用键值对形式。
原则:有相同索引——进行算术运算;没有相同索引——进行数据对齐,并引入缺失值。
意思就是说,如果没有相同的索引,那么就不会进行运算,并且如果你原先数据位上有值,也会被当初NaN。看例子来理解:
s3 = pd.Series([100, 200, 300], index=['a', 'b', 'd'])
print(s1 + s3)
a 110.0
b 220.0
c NaN
d NaN
dtype: float64
针对Series和DataFrame。都会数据对齐操作,后者是会同时发生在行和列上。
df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
df2 = pd.DataFrame({'A': [10, 20, 30], 'D': [40, 50, 60]})
print(df1 + df2) # 按元素相加
A B D
0 11 NaN NaN
1 22 NaN NaN
2 33 NaN NaN
如果需要进行较为复杂的运算,那么就会需要自己定义函数。如何将自己的函数使用到数据运算中呢?Pandas提供了三种方法。
对于Series
排序有两种方法:
sort_index
:对索引进行排序,默认为升序,ascending=False时为降序
sort_values
:对数据进行排序。
对于DataFrame
通过指定轴的方向,使用:
sort_index
:对行或列的索引进行排序sort_values
:将列名传给by参数进行列排序使用sum
方法,默认对每列进行汇总。axis=1时对行进行汇总。
使用describe
方法,会统计初步的数据特征,当然,一般都是其他方法进行更详细的描述与统计。
方法 | 说明 |
---|---|
count() | 非空值个数 |
sum() | 求和 |
mean() | 平均值 |
median() | 中位数 |
mode() | 众数 |
std() | 标准差 |
var() | 方差 |
min() | 最小值 |
max() | 最大值 |
idxmin() | 最小值的位置 |
idxmax() | 最大值的位置 |
根据某个或几个字段对数据集进行分组,然后对每个分组进行分析与转换,这是数据分析的常见操作。
在Pandas中可以使用groupby
方法来进行数据分组操作。
数据分组后返回的数据类型不再是一个数据框,而是一个groupby对象。
groupk1 = df.groupby('Salary').mean()
data = {
'Department': ['HR', 'HR', 'IT', 'IT', 'Finance', 'Finance', 'HR'],
'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank', 'Grace'],
'Salary': [5000, 7000, 8000, 9000, 6000, 10000, 7500],
'Age': [25, 30, 35, 40, 28, 32, 26]
}
df = pd.DataFrame(data)
# 按部门分组
grouped = df.groupby('Department')
print(grouped.size())
通过字典作为分组键。
函数作为分组键的原理类似于字典,使用映射关系来进行分组。
数据聚合就是对分组后的数据进行计算,产生标量值的数据转换过程。
函数 | 说明 |
---|---|
sum() | 计算总和 |
mean() | 计算平均值 |
median() | 计算中位数 |
min() | 计算最小值 |
max() | 计算最大值 |
count() | 计算非空值的个数 |
std() | 计算标准差 |
var() | 计算方差 |
prod() | 计算乘积 |
first | 第一个值 |
last | 最后一个值 |
agg
和aggregate
方法都是通过自定义聚合函数来进行数据的聚合。它们可以直接对DataFrame进行函数应用操作。
print(grouped['Salary'].agg([np.sum,np.mean]))
sum mean
Department
Finance 16000 8000.0
HR 19500 6500.0
IT 17000 8500.0
如果希望返回的结果不以分组键为索引,可以通过as_index = False来实现。
分组运算包含聚合运算,聚合运算是数据转换的特例。
transform
方法
可以将运算分布到每一行
data = {
'Department': ['HR', 'HR', 'IT', 'IT', 'Finance', 'Finance', 'HR'],
'Salary': [5000, 7000, 8000, 9000, 6000, 10000, 7500]
}
df = pd.DataFrame(data)
# 计算每个部门内的工资占比
df['Salary_Ratio'] = df.groupby('Department')['Salary'].transform(lambda x: x / x.sum())
print(df)
Department Salary Salary_Ratio
0 HR 5000 0.256410
1 HR 7000 0.358974
2 IT 8000 0.470588
3 IT 9000 0.529412
4 Finance 6000 0.375000
5 Finance 10000 0.625000
6 HR 7500 0.384615
apply
方法
可以将运算分布到每一列
# 计算工资税后收入(假设税率20%)
df['Net_Salary'] = df['Salary'].apply(lambda x: x * 0.8)
# 计算每个部门的工资方差
variance = df.groupby('Department')['Salary'].apply(lambda x: x.var())
print(df)
print(variance)
Department Salary Net_Salary
0 HR 5000 4000.0
1 HR 7000 5600.0
2 IT 8000 6400.0
3 IT 9000 7200.0
4 Finance 6000 4800.0
5 Finance 10000 8000.0
6 HR 7500 6000.0
Department
Finance 8000000.0
HR 833333.0
IT 500000.0
Name: Salary, dtype: float64