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