python-資料分析-Pandas-3、DataFrame-資料重塑

little小新發表於2024-06-10
在完成資料載入之後,我們可能需要對事實表和維度表進行連線,這是對資料進行多維度拆解的基礎;
我們可能從不同的資料來源載入了結構相同的資料,我們需要將這些資料拼接起來;我們把這些操作統稱為資料重塑。
當然,由於企業的資訊化水平以及資料中臺建設水平的差異,我們拿到的資料未必是質量很好的,可能還需要對資料中的缺失值、重複值、異常值進行適當的處理。
即便我們獲取的資料在質量上是沒有問題的,但也可能需要對資料進行一系列的預處理,才能滿足我們做資料分析的需求。
接下來,我們就為大家講解和梳理這方面的知識。

資料重塑

# -*- coding: utf-8 -*-
#資料重塑


import pandas
from sqlalchemy import create_engine

#建立連線
engine = create_engine('mysql+pymysql://root:123456@192.168.177.190:3307/demo?charset=utf8')
dept_df = pandas.read_sql_table('dept_df', con=engine, index_col='dno')
emp_df = pandas.read_sql_table('emp_df', con=engine, index_col='eno')
emp2_df = pandas.read_sql_table('emp2_df', con=engine, index_col='eno')

#使用pandas提供的concat函式實現兩個或多個DataFrame的資料拼接
#拼接emp_df和emp2_df
all_emp_df = pandas.concat([emp_df, emp2_df])
# print(all_emp_df)

#使用merge函式將員工表(all_emp_df)和部門表(dept_df)的資料合併到一張表中
#1、先使用reset_index方法重新設定all_emp_df的索引
#這樣eno 不再是索引而是一個普通列,reset_index方法的inplace引數設定為True表示、重置索引的操作直接在all_emp_df上執行,而不是返回修改後的新物件
all_emp_df.reset_index(inplace=True)

#透過merge函式合併資料,當然,也可以呼叫DataFrame物件的merge方法來達到同樣的效果
#pandas.merge()函式的引數說明:
#left:左表
#right:右表
#how:連線型別,預設為inner
#on:連線條件,預設為None,表示連線條件為左表和右表的索引列相同
#left_on:左表連線條件,預設為None
#right_on:右表連線條件,預設為None
#left_index:左表連線條件是否為索引列,預設為False
#right_index:右表連線條件是否為索引列,預設為False
new_data = pandas.merge(all_emp_df, dept_df, how='inner', on='dno')
print(new_data)
'''
     eno ename   job     mgr    sal    comm  dno dname dloc
0   1359   胡一刀   銷售員  3344.0   1800   200.0   30   銷售部   重慶
1   3344    黃蓉  銷售主管  7800.0   3000   800.0   30   銷售部   重慶
2   4466   苗人鳳   銷售員  3344.0   2500     NaN   30   銷售部   重慶
3   2056    喬峰   分析師  7800.0   5000  1500.0   20   研發部   成都
4   3088   李莫愁   設計師  2056.0   3500   800.0   20   研發部   成都
5   3211   張無忌   程式設計師  2056.0   3200     NaN   20   研發部   成都
6   3233   丘處機   程式設計師  2056.0   3400     NaN   20   研發部   成都
7   3244   歐陽鋒   程式設計師  3088.0   3200     NaN   20   研發部   成都
8   3251   張翠山   程式設計師  2056.0   4000     NaN   20   研發部   成都
9   7800   張三丰    總裁     NaN   9000  1200.0   20   研發部   成都
10  9500   張三丰    總裁     NaN  50000    8000   20   研發部   成都
11  9600   王大錘   程式設計師  9800.0   8000     600   20   研發部   成都
12  9700   張三丰    總裁     NaN  60000    6000   20   研發部   成都
13  9800    駱昊   架構師  7800.0  30000    5000   20   研發部   成都
14  9900   陳小刀   分析師  9800.0  10000    1200   20   研發部   成都
15  3577    楊過    會計  5566.0   2200     NaN   10   會計部   北京
16  3588   朱九真    會計  5566.0   2500     NaN   10   會計部   北京
17  5234    郭靖    出納  5566.0   2000     NaN   10   會計部   北京
18  5566   宋遠橋   會計師  7800.0   4000  1000.0   10   會計部   北京
'''
#merge函式的一個引數代表合併的左表、第二個引數代表合併的右表,有SQL程式設計經驗的同學對這兩個詞是不是感覺到非常親切。
# 正如大家猜想的那樣,DataFrame物件的合併跟資料庫中的表連線非常類似,所以上面程式碼中的how代表了合併兩張表的方式,
# 有left、right、inner、outer四個選項;而on則代表了基於哪個列實現表的合併,相當於 SQL 表連線中的連表條件,
# 如果左右兩表對應的列列名不同,可以用left_on和right_on引數取代on引數分別進行指定。

#如果對上面的程式碼稍作修改,將how引數修改為'right'
new_data = pandas.merge(all_emp_df, dept_df, how='right', on='dno')
print(new_data)
'''
#執行結果比之前的輸出多出瞭如下所示的一行,這是因為how='right'代表右外連線,
#也就意味著右表dept_df中的資料會被完整的查出來,但是在all_emp_df中又沒有編號為40 部門的員工,所以對應的位置都被填入了空值。
19	NaN    NaN    NaN    NaN    NaN     NaN    40    運維部    深圳  #多出來的
'''

print('-------------------------------------------------------')

#資料的清洗
#通常,我們從 Excel、CSV 或資料庫中獲取到的資料並不是非常完美的,裡面可能因為系統或人為的原因混入了重複值或異常值,也可能在某些欄位上存在缺失值;
# 再者,DataFrame中的資料也可能存在格式不統一、量綱不統一等各種問題。因此,在開始資料分析之前,對資料進行清洗就顯得特別重要。

#缺失值
#可以使用DataFrame物件的isnull或isna方法來找出資料表中的缺失值、如下:
print(emp_df.isnull())
print(emp_df.isna())
'''
      ename    job    mgr    sal   comm    dno
eno                                           
1359  False  False  False  False  False  False
2056  False  False  False  False  False  False
3088  False  False  False  False  False  False
3211  False  False  False  False  False  False
3233  False  False  False  False  False  False
3244  False  False  False  False  False  False
3251  False  False  False  False  False  False
3344  False  False  False  False  False  False
3577  False  False  False  False  False  False
3588  False  False  False  False  False  False
4466  False  False  False  False  False  False
5234  False  False  False  False  False  False
5566  False  False  False  False  False  False
7800  False  False  False  False  False  False
'''

#notnull和notna方法可以將非空的值標記為True
#刪除這些缺失值,可以使用DataFrame物件的dropna方法
#該方法的axis引數可以指定沿著0軸還是1軸刪除,也就是說當遇到空值時,是刪除整行還是刪除整列,預設是沿0軸進行刪除的
print(emp_df.dropna())
#如果要沿著1軸進行刪除,可以使用下面的程式碼。
print(emp_df.dropna(axis=1))
'''
注意:DataFrame物件的很多方法都有一個名為inplace的引數,該引數的預設值為False,表示我們的操作不會修改原來的DataFrame物件,
而是將處理後的結果透過一個新的DataFrame物件返回。如果將該引數的值設定為True,那麼我們的操作就會在原來的DataFrame上面直接修改,方法的返回值為None。
簡單的說,上面的操作並沒有修改emp_df,而是返回了一個新的DataFrame物件
'''

#填充缺失值
#如果要填充缺失值,可以使用DataFrame物件的fillna方法,該方法的value引數可以指定填充的值,如下:
print(emp_df.fillna(value=0))

print('-------------------------------------------------------')

#重複值
#先給之前的部門表新增兩行資料,讓部門表中名為“研發部”和“銷售部”的部門各有兩個
dept_df.loc[50] = {'dname': '研發部', 'dloc': '深圳'}
dept_df.loc[60] = {'dname': '銷售部', 'dloc': '長沙'}
print(dept_df)
'''
    dname dloc
dno           
10    會計部   北京
20    研發部   成都
30    銷售部   重慶
40    運維部   深圳
50    研發部   深圳
60    銷售部   長沙
'''
#透過DataFrame物件的duplicated方法判斷是否存在重複值,該方法在不指定引數時預設判斷行索引是否重複,我們也可以指定根據部門名稱dname判斷部門是否重複
print(dept_df.duplicated('dname'))
'''
dno
10    False
20    False
30    False
40    False
50     True
60     True
dtype: bool
'''

#50和60兩個部門從部門名稱上來看是重複的,如果要刪除重複值,可以使用drop_duplicates方法
#該方法的keep引數可以控制在遇到重複值時,保留第一項還是保留最後一項,或者多個重複項一個都不用保留,全部刪除掉。
print(dept_df.drop_duplicates('dname'))
'''
    dname dloc
dno           
10    會計部   北京
20    研發部   成都
30    銷售部   重慶
40    運維部   深圳
'''
#將keep引數的值修改為last
print(dept_df.drop_duplicates('dname', keep='last'))
'''
    dname dloc
dno           
10    會計部   北京
40    運維部   深圳
50    研發部   深圳
60    銷售部   長沙
'''

#使用同樣的方式,我們也可以清除all_emp_df中的重複資料,
# 例如我們認定“ename”和“job”兩個欄位完全相同的就是重複資料,我們可以用下面的程式碼去除重複資料。
print(all_emp_df.drop_duplicates(['ename', 'job'], inplace=True))
'''
    dname dloc
dno           
10    會計部   北京
40    運維部   深圳
50    研發部   深圳
60    銷售部   長沙
None
'''

#說明:上面的drop_duplicates方法新增了引數inplace=True,該方法不會返回新的DataFrame物件,而是在原來的DataFrame物件上直接刪除
# 可以檢視all_emp_df看看是不是已經移除了重複的員工資料
print(all_emp_df)
'''
     eno ename   job     mgr    sal    comm  dno
0   1359   胡一刀   銷售員  3344.0   1800   200.0   30
1   2056    喬峰   分析師  7800.0   5000  1500.0   20
2   3088   李莫愁   設計師  2056.0   3500   800.0   20
3   3211   張無忌   程式設計師  2056.0   3200     NaN   20
4   3233   丘處機   程式設計師  2056.0   3400     NaN   20
5   3244   歐陽鋒   程式設計師  3088.0   3200     NaN   20
6   3251   張翠山   程式設計師  2056.0   4000     NaN   20
7   3344    黃蓉  銷售主管  7800.0   3000   800.0   30
8   3577    楊過    會計  5566.0   2200     NaN   10
9   3588   朱九真    會計  5566.0   2500     NaN   10
10  4466   苗人鳳   銷售員  3344.0   2500     NaN   30
11  5234    郭靖    出納  5566.0   2000     NaN   10
12  5566   宋遠橋   會計師  7800.0   4000  1000.0   10
13  7800   張三丰    總裁     NaN   9000  1200.0   20
15  9600   王大錘   程式設計師  9800.0   8000   600.0   20
17  9800    駱昊   架構師  7800.0  30000  5000.0   20
18  9900   陳小刀   分析師  9800.0  10000  1200.0   20
'''

預處理

對資料進行預處理也是一個很大的話題,它包含了對資料的拆解、變換、歸約、離散化等操作。我們先來看看資料的拆解。如果資料表中的資料是一個時間日期,

我們通常都需要從年、季度、月、日、星期、小時、分鐘等維度對其進行拆解,如果時間日期是用字串表示的,可以先透過pandas的to_datetime函式將其處理成時間日期。

# -*- coding: utf-8 -*-
#預處理""

import pandas
import numpy

#先讀取 Excel 檔案,獲取到一組銷售資料,其中第一列就是銷售日期,我們將其拆解為“月份”、“季度”和“星期”
sales_df = pandas.read_excel(
    'file/2020年銷售資料.xlsx',
    # usecols指定讀取的列
    usecols=['銷售日期', '銷售區域', '銷售渠道', '品牌', '銷售數量']
)
# print(sales_df.info())
'''
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1945 entries, 0 to 1944
Data columns (total 5 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   銷售日期    1945 non-null   datetime64[ns]
 1   銷售區域    1945 non-null   object        
 2   銷售渠道    1945 non-null   object        
 3   品牌      1945 non-null   object        
 4   銷售數量    1945 non-null   int64         
dtypes: datetime64[ns](1), int64(1), object(3)
memory usage: 76.1+ KB
None
'''

month = sales_df['月份'] = sales_df['銷售日期'].dt.month
quarter = sales_df['季度'] = sales_df['銷售日期'].dt.quarter
week = sales_df['星期'] = sales_df['銷售日期'].dt.weekday
print(sales_df)
'''
           銷售日期 銷售區域 銷售渠道    品牌  銷售數量  月份  季度  星期
0    2020-01-01   上海  拼多多   八匹馬    83   1   1   2
1    2020-01-01   上海   抖音   八匹馬    29   1   1   2
2    2020-01-01   上海   天貓   八匹馬    85   1   1   2
3    2020-01-01   上海   天貓   八匹馬    14   1   1   2
4    2020-01-01   上海   天貓   皮皮蝦    61   1   1   2
...         ...  ...  ...   ...   ...  ..  ..  ..
1940 2020-12-30   北京   京東  花花姑娘    26  12   4   2
1941 2020-12-30   福建   實體   八匹馬    97  12   4   2
1942 2020-12-31   福建   實體  花花姑娘    55  12   4   3
1943 2020-12-31   福建   抖音   八匹馬    59  12   4   3
1944 2020-12-31   福建   天貓   八匹馬    27  12   4   3
'''
#在上面的程式碼中,透過日期時間型別的Series物件的dt 屬性,獲得一個訪問日期時間的物件,
# 透過該物件的year、month、quarter、hour等屬性,就可以獲取到年、月、季度、小時等時間資訊,
# 獲取到的仍然是一個Series物件,它包含了一組時間資訊,所以我們通常也將這個dt屬性稱為“日期時間向量”。

print('=======================================================================================')
#字串型別的資料的處理

#讀取csv檔案的資料
jobs_df = pandas.read_csv(
    'file/某招聘網站招聘資料.csv',
    #讀取指定列的順序
    usecols=['city', 'companyFullName', 'positionName', 'salary']
)
print(jobs_df.info())
'''
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3140 entries, 0 to 3139
Data columns (total 4 columns):
 #   Column           Non-Null Count  Dtype 
---  ------           --------------  ----- 
 0   city             3140 non-null   object
 1   companyFullName  3140 non-null   object
 2   positionName     3140 non-null   object
 3   salary           3140 non-null   object
dtypes: object(4)
memory usage: 98.2+ KB
None
'''

#檢視前5條資料
print(jobs_df.head())
'''
  city companyFullName positionName   salary
0   北京  達疆網路科技(上海)有限公司        資料分析崗  15k-30k
1   北京    北京音娛時光科技有限公司         資料分析  10k-18k
2   北京   北京千喜鶴餐飲管理有限公司         資料分析  20k-30k
3   北京   吉林省海生電子商務有限公司         資料分析  33k-50k
4   北京  韋博網訊科技(北京)有限公司         資料分析  10k-15k
'''

#如果要篩選出資料分析的崗位,可以透過檢查positionName欄位是否包含“資料分析”這個關鍵詞,這裡需要模糊匹配,應該如何實現呢?
# 我們可以先獲取positionName列,因為這個Series物件的dtype是字串,所以可以透過str屬性獲取對應的字串向量,
# 然後就可以利用我們熟悉的字串的方法來對其進行操作

#篩選出資料分析的崗位
#jobs_df.positionName.str.contains:獲取positionName列,然後檢查是否包含“資料分析”這個關鍵詞
# str.contains:檢查是否包含指定字串
jobs_df = jobs_df[jobs_df.positionName.str.contains('資料分析')]
#shape: 顯示資料的形態:資料的行數和列數
print(jobs_df.shape)    #(1515, 4)

#接下來,我們還需要對salary欄位進行處理,如果我們希望統計所有崗位的平均工資或每個城市的平均工資,首先需要將用範圍表示的工資處理成其中間值
#說明:下面的程式碼透過正規表示式捕獲組從字串中抽取出兩組數字,分別對應工資的下限和上限
#extract:提取字串中的匹配項、
# jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?')
#
# #需要提醒大家的是,抽取出來的兩列資料都是字串型別的值,我們需要將其轉換成int型別,才能計算平均值,
# # 對應的方法是DataFrame物件的applymap方法,該方法的引數是一個函式,而該函式會作用於DataFrame中的每個元素。
# # 完成這一步之後,我們就可以使用apply方法將上面的DataFrame處理成中間值,apply方法的引數也是一個函式,可以透過指定axis引數使其作用於DataFrame 物件的行或列,
# # 程式碼如下所示。
# temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int)
# temp_df.apply(numpy, axis=1)

#接下來,我們可以用上面的結果替換掉原來的salary列或者增加一個新的列來表示職位對應的工資
#完整的程式碼如下所示。
temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int)
jobs_df['salary'] = temp_df.apply(numpy.mean, axis=1)    #mean:求平均值 axis:0表示按列計算,1表示按行計算
print(jobs_df.head())
'''
  city companyFullName positionName  salary
0   北京  達疆網路科技(上海)有限公司        資料分析崗    22.5
1   北京    北京音娛時光科技有限公司         資料分析    14.0
2   北京   北京千喜鶴餐飲管理有限公司         資料分析    25.0
3   北京   吉林省海生電子商務有限公司         資料分析    41.5
4   北京  韋博網訊科技(北京)有限公司         資料分析    12.5

'''


#applymap和apply兩個方法在資料預處理的時候經常用到,Series物件也有apply方法,也是用於資料的預處理,
# 但是DataFrame物件還有一個名為transform 的方法,也是透過傳入的函式對資料進行變換,類似Series物件的map方法。
# 需要強調的是,apply方法具有歸約效果的,簡單的說就是能將較多的資料處理成較少的資料或一條資料;
# 而transform方法沒有歸約效果,只能對資料進行變換,原來有多少條資料,處理後還是有多少條資料。

相關文章