Pandas 數據清洗與整理

# 數據清洗與整理

# 1) 常見的數據清洗方法
# 2) 數據合併:多源數據的合併和鏈接
# 3) 數據重塑:針對層次化索引,學會 stack和 unstack
# 4) 字符串處理:學會 DataFrame中字符串函數的使用

# 一,常見的數據清洗方法

# 1, 查看數據基本信息
# df.info()
# df.describe()

# 2, 偵查缺失值 
# df.isnull() :返回一個和 df結構相同的布爾型 DataFrame矩陣
# df.isnull().sum() :返回一個以 df各【列】值爲nan個數爲元素的序列
# df.isnull().sum().sum() :返回 df全部的 值爲nan的個數和。

# df.notnull() :返回一個和 df結構相同的布爾型 DataFrame矩陣
# df.notnull().sum() :返回一個以 df各【列】值爲非nan個數爲元素的序列
# df.isnull().sum().sum() :返回 df全部的 值爲非nan的個數和。

import numpy as np
import pandas as pd
from pandas import DataFrame,Series

df=DataFrame([[3,5,np.nan],[1,3,np.nan],['tom',np.nan,'ivan'],[np.nan,'a','b']])
df
df.isnull()
df.isnull().sum()
df.isnull().sum().sum()  # 4

df.notnull()
df.notnull().sum()
df.notnull().sum().sum()  # 8


# 3, 刪除存在缺失值的整行或整列 
# 不影響原 DataFrame數據 
df.dropna(how='any',axis=0)   # 刪除含有 NaN的整行數據 當 how='any'時,能夠缺省 
df.dropna(how='all',axis=0)   # 刪除全部值爲 NaN的整行數據 
df.dropna(how='any',axis=1)   # 刪除含有 NaN的整列數據 當 how='any'時,能夠缺省 
df.dropna(how='all',axis=1)   # 刪除全部值爲 NaN的整列數據 

# 4, 填充缺失值
# df.fillna? 查看函數幫助
# 當存在缺失值的行或列中的那些非缺失值很重要時,不能將整行或整列一刪了之,這時須要填充缺失值:

df=DataFrame([[3,5,np.nan],[1,3,np.nan],['tom',np.nan,'ivan'],[np.nan,'a','b']])
df2=DataFrame([[0,1,2,np.nan],[4,5,6,np.nan],[np.nan,np.nan,np.nan,np.nan]])

# 對全部的 NaN 填充數據:
df3=df.fillna(0,inplace=False)  # 將 DataFrame中的全部 NaN填充爲 0 inplace缺省時默認 inplace=False
df.fillna(0,inplace=True)       # 就地修改


# 對指定的某列或某幾列填充數據 -- 1 使用索引和切片調用 fillna():
df[0].fillna(df[0].mean())         # 填充第 1列,使用第一列的統計非 NaA的平均值。
df[0:2]=df[0:2].fillna('enene')    # 填充第 1-3列,使用'enene'
df
df[[0,2]]=df[[0,2]].fillna('dada') # 填充第 1,3兩列,使用'dada' 注意區別 df[[0,2]] 與 df[0,2]的不一樣,別用混了
df

# 對指定的某列或某幾列填充數據 -- 2 fillna()的參數傳入字典:
df.fillna({0:df[0].mean()})     # 填充第 1列,使用第一列的統計非 NaA的平均值。
df2.fillna({0:0,2:'aa'})        # 給指定的列填充 第一列的填充值爲 0,第 3列的填充值爲 'aa' 
df.fillna(method='ffifll')      # df.reindex()裏也有這個參數
  
             
# 對指定的某行或某幾行填充數據 -- 1 使用索引和切片調用 fillna():
df.loc[0].fillna(df.loc[0].mean(),inplace=True)         # 填充第 1行,使用第一行的統計非 NaA的平均值。
df.loc[0:2]=df.loc[0:2].fillna('enene')                 # 填充第 1-3行,使用'enene'
df
df.loc[[0,2]]=df.loc[[0,2]].fillna('dada')              # 填充第 1,3兩行,使用'dada'
df

# 對指定的某行或某幾行填充數據 -- 2 fillna()的參數傳入字典:(不推薦)
# 給行填充不能使用字典參數加 axis=1的方式,會報錯的,但可使用轉置的方法轉成列再填充,而後轉置回來。
df=df.T.fillna({2:'aa'}).T
df=df.T.fillna({0:0,3:'aa'}).T  # 給指定的行填充 轉置兩次


# 5, 移除重複數據
# 移除行數據徹底相同的行:
df.duplicated()                   # 返回一個布爾型序列 True表示與前面行數據有重複
df.drop_duplicated()              # 保留第一次出現的行數據,刪除其餘的重複行數據 keep='first' 可缺省
df.drop_duplicated(keep='last')   # 保留最後一次出現的行數據,刪除其餘的重複行數據

# 移除以指定列所謂判斷重複標準的的行:
df.drop_duplicated(subset=['sex','year'],keep='last')   # 以指定的'sex','year'列做爲判斷依據。


# 6, 替換值:df.replace()
df.replace('','未知')                # 將 df中全部的 ""替換爲 "未知"
df.replace(['',2001],['未知',2002])  # 將 df中全部的 ""替換爲 "未知",將 2001替換爲 2002
df.replace({'':'未知',2001:2002})    # 將 df中全部的 ""替換爲 "未知",將 2001替換爲 2002


# 7, 利用函數或映射進行數據轉換
# 判斷分數等級:90-100 優秀; 70-89 良好; 60-69 及格; <60 不及格
# python可使用 循環加 if 判斷來解決
# pandas 可使用 自定義函數+map()函數來實現

data={
    'name':['張三','李四','王五','馬六'],
    'grade':[75,52,63,99]
}

df=DataFrame(data)

def f(x):
    if x>=90:
        return '優秀'
    elif 70<=x<90:
        return '良好'
    elif 60<=x<70:
        return '合格'
    else:
        return '不合格'

df['class']=df.grade.map(f)    # df['class']=df['grade'].map(f) 
df


# 8, 檢測異常值
df=DataFrame(np.arange(10),columns=['X'])
df['Y']=df['X']*2+0.5
df.iloc[9,1]=185 # 將第 9行第 1列的值由 18.5改成 185
df.plot(kind='scatter',x='X',y='Y')

# 9, 虛擬變量(暫時用不到,數學建模機器學習)
# 對於單類別的數據:使用 pd.get_dummies()
df = DataFrame(
    {
        '朝向':['東','南','東','西','北']
        '價格'[1200,2000,1200,1100,800]
    }
)
df
pd.get_dummies(df['朝向'])

# 對於多類別的數據:使用 apply()
df = DataFrame(
    {
        '朝向':['東/北','南/西','東','西/北','北']
        '價格'[1200,2000,1200,1100,800]
    }
)
df
dummies=df['朝向'].apply(lambda x:Series(x.split('/')).value_counts())
dummies


# 二,數據合併與重塑
# 當要數據來源於多處時,就須要對數據進行合併和重塑。

# 1, pd.merge(df1,df2,on='fruit',how='inner'...)函數:

# how--how='inner' 內鏈接,交集(默認)
# how='outer' 外鏈接,並集 
# how='left'左鏈接 以 df1爲主表,右表沒數據爲 NaN
# how='right'右鏈接 以 df2爲主表,右表沒數據爲 NaN

# left--指參數列表裏第一個 Daframe
# right--指參數列表裏第二個 Daframe
# on--用於鏈接的列名
# left_on--df1用於鏈接的列名 與 right_on--df2用於鏈接的列名 成對出現
# left_index--df1的行索引做爲鏈接鍵 由於行索引也能夠看做是一列
# right_index--df2的行索引做爲鏈接鍵
# sort--合併後對數據進行排序,默認爲True
# suffixes--修改重複名


# 經過一個鍵或多個鍵(DataFrame的列)將兩個DataFrame按行合併起來,相似於SQL。
price=DataFrame(
    {
        'fruit':['apple','banana','orange'],
        'price':[23,32,45]
    }
)

amount=DataFrame(
    {
        'fruit':['apple','banana','apple','apple','banana','pear'],
        'amount':[5,3,6,3,5,7]
    }
)

pd.merge(amount,price,on='fruit')               # 交集
pd.merge(amount,price,on='fruit',how='outer')   # 並集
pd.merge(amount,price,on='fruit',how='left')    # 左鏈接


# 多對多的鏈接,笛卡爾積:
# 只使用一個鍵來鏈接時,price2裏有2個 apple,amount2裏有3個apple,鏈接的結果就有6個apple.

price2=DataFrame(
    {
        'fruit':['apple','banana','orange','apple'],
        'price':[23,32,45,25]
    }
)

amount2=DataFrame(
    {
        'fruit':['apple','banana','apple','apple','banana','pear'],
        'amount':[5,3,6,3,5,7]
    }
)

pd.merge(amount2,price2,on='fruit',how='outer')   # 並集


# 多個鍵合併,使用列表:
df1=DataFrame(
    {
        'k1':['one','one','two'],
        'k2':['a','b','a'],
        'v1':[2,3,4]   
    }
)

df2=DataFrame(
    {
        'k1':['one','one','two','two'],
        'k2':['a','a','a','b'],
        'v1':[5,6,7,8]   
    }
)


# pd.merge(df1,df2,on='k1',how='outer') # 當只指定一個鏈接鍵的時候,容易出現笛卡爾積
pd.merge(df1,df2,on=['k1','k2'],how='outer')      # 用兩個鏈接鍵做爲約束,就能夠避免沒必要要的笛卡爾積

# suffixes 處理重複列名的問題:
pd.merge(df1,df2,on='k1',how='outer',suffixes=('_left','_right'))   # suffixes 處理重複列名的問題 

# 使用行索引做爲鏈接鍵來使用:right_index=True 或 left_index=True
df1=DataFrame(
    {
        'k':['a','b','a'],
        'v1':[2,3,4]   
    }
)

df2=DataFrame({'v2':[5,6,7,8]}index=['a','b'])
pd.merge(df1,df2,left_on='key',right_index=True)

# 將兩個Dataframe同時按照索引鏈接起來:
# 1)方法一,使用 merge:
pd.merge(df1,df2,left_on='k',left_index=True,right_index=True,how='outer')
# 2)方法二,使用 join:
df1.join(df2,how='outer')



# 2, pd.concat([s1,s2],axis=0,join='outer',join_axes=[['b','a']]) 

# 默認 axis=0 默認 join='outer' join只有 inner和 outer兩個值可選. join_axes用來指定索引順序
# 當要合併的兩個 DataFrame沒有鏈接鍵時,不能使用 merge,可使用 contact。
# concat只會鏈接不會去重。想要對結果去重,得使用 df.drop_duplicated()。


# 對於 Series:
s1=Series([0,1],index=['a','b'])
s2=Series([0,3],index=['a','d'])  # a標籤與 s1重複
s3=Series([4,5],index=['e','f'])

pd.concat([s1,s2,s3])         # 共 6行 1列 axis=0
pd.concat([s1,s2,s3],axis=1)  # 共 5行 3列 axis=1
pd.concat([s1,s2],axis=1,join='outer')     # join='outer' 並集 2列 3行 a b d
pd.concat([s1,s2],axis=0,join='outer')     # join='outer' 並集 1列 4行 a b a d 默認
pd.concat([s1,s2],axis=1,join='inner')     # join='inner' 交集 2列 1行 a
pd.concat([s1,s2],axis=0,join='inner')     # join='inner' 交集 1列 4行 a並集 默認

# 若是想在結果中很容易區分出參與鏈接的對象,可使用 keys參數給鏈接對象建立一個層次化索引。
pd.concat([s1,s2],axis=0,join='inner',keys=['first','second']) 
pd.concat({'first':s1,'second':s2},axis=0,join='inner') 

pd.concat([s1,s2],axis=1,join='inner',keys=['first','second']) 
pd.concat({'first':s1,'second':s2},axis=1,join='inner') 

# 對於 DataFrame:
price=DataFrame(
    {
        'fruit':['apple','banana','orange'],
        'price':[23,32,45]
    }
)

amount=DataFrame(
    {
        'fruit':['apple','banana','apple','apple','banana','pear'],
        'amount':[5,3,6,3,5,7]
    }
)

pd.concat([price,amount],axis=1)

# 從新生成默認索引:對於 axis=0時有用
price=DataFrame(
    {
        'fruit':['apple','banana','orange'],
        'price':[23,32,45]
    },index=['a','b','c']
)

amount=DataFrame(
    {
        'fruit':['apple','banana','apple','apple','banana','pear'],
        'amount':[5,3,6,3,5,7]
    },index=['a','b','c','d','e','f']
)

pd.concat([price,amount],axis=0)
pd.concat([price,amount],axis=0,ignore_index=True)  # 不會改變數據結構,只是生成新的默認索引


# 3, combine_first()合併 
# combine_first()使用的鏈接鍵是行索引(行索引能夠看做爲一個列)
# 若是須要合併的兩個 DataFrame存在重複的索引,在這種狀況下,若使用 merge和 concat方法都不能準確的解決問題,此時須要
# combine_first方法,相似於打補丁,以調用的 DataFrame爲主,參數裏的 DataFrame做爲補丁。
price=DataFrame(
    {
        'fruit':['apple','banana',np.nan,'pear'],
        'price':[23,32,np.nan,np.nan]
    },index=['a','b','c','d']
)

amount=DataFrame(
    {
        'fruit':['apple','banana','apple','apple','banana','pear'],
        'price':[5,3,6,3,5,7],
        'amount':[1,1,1,1,1,1]
    },index=['a','b','c','d','e','f']
)

price.combine_first(amount)


# 4, 數據重塑 stack 堆疊
# 1) df.stack()方法: 將每一個行索引對應的列索引堆疊起來造成內層行索引變成層次化索引的序列 Series
# 2) df.unstack()方法: df.stack()的反操做。

df=DataFrame(np.arange(9).reshape(3,3),index=['a','b','c'],columns=['one','two','three'])
df.index.name='alph'
df.columns.name='number'

Se=df.stack()  # 其結果是一個層次化的序列
type(Se) # pandas.core.series.Series
Se.unstack(),Se.unstack(1),Se.unstack('number')      
# unstack() 能夠指定參數,默認是 Se的內層索引的名字(即原 DataFrame df1的columns.name)或表示層次結構的數字 1
# 若是將參數指定爲 0或 Se的外層索引的名字(即原 DataFrame df1的index.name),則獲得 df1的轉置。
Se.unstack(0),Se.unstack('alph')   

# 對於自己就是層次化索引的 DataFrame,其 stack()和 unstack()用到再說吧。


# 三, 字符串處理
# pandas 處理字符串數據的矢量化函數

# 1, 字符串方法:以 str.split() 爲例:
# 示例,將一列字符串數據分紅兩列:
data={
    'data':['張三|男','李四|女','王五|女','馬六|男']
}

df=DataFrame(data)

# 傳統方法 1:
df['name']=df['data'].apply(lambda x:x.split("|")[0])
df['gender']=df['data'].apply(lambda x:x.split("|")[1])
del df['data']
# df=df[['name','gender']] # 從新排列列的順序
df


# 傳統方法 2:
df=df['data'].apply(lambda x:Series(x.split("|")))  # 
df.columns=['name','gender']  # 發現結果裏沒有了列名,從新按位置定義列名
# df.rename(columns={'data':'name'}, inplace = True) # 重命名列 
df


# pandas 方法:
# pandas中字段的 str屬性能夠輕鬆調用字符串的方法,並矢量化到整個字段數據中:
# df['data'].str.split("|") # dataframe 的 data字段的 str屬性 調用了 split()方法 返回一個以列表爲元素的序列

data={
    'data':['張三|男','李四|女','王五|女','馬六|男']
}

df=DataFrame(data)
se=df['data'].str.split("|")   # type(se) # pandas.core.series.Series
df['name']=se.str[0]
df['gender']=se.str[1]
del df['data']
df


# 2,正則表達式:
# 字符串的矢量化操做一樣適用於正則表達式:
df=DataFrame({
    'email':['100@qq.com','111@qq.com','222@qq.com']
})

se=df['email'].str.findall('(.*?)@')
type(se)  # pandas.core.series.Series
df['qq']=df['email'].str.findall('(.*?)@').str.get(0)
df
df=DataFrame({
    'email':['100@qq.com','111@qq.com','222@qq.com']
})

se=df['email'].str.findall('(.*?)@')
type(se)  # pandas.core.series.Series
df['qq']=df['email'].str.findall('(.*?)@').str.get(0)  # 即 df['qq']=se.str.get(0)
df
email qq
0 100@qq.com 100
1 111@qq.com 111
2 222@qq.com 222
參考資料:《從零開始學 Python數據分析》羅攀