Pandasのデータ処理応用 - データクリーニングと前処理

目次

DataFrameの活用

データのクリーニング

欠損値

重複データ

異常値

前処理

DataFrameの活用

データのクリーニング

実際のデータは、ExcelやCSV、データベースから取得した場合、システムエラーまたは人為的なミスにより重複や異常値が含まれていることが多く、特定のフィールドに欠損値があることも珍しくありません。また、DataFrame内のデータは形式の統一や単位の整合性など、さまざまな問題を抱えている可能性があります。そのため、分析開始前にデータをクリーニングすることが非常に重要です。

欠損値

DataFrameオブジェクトのisnullまたはisnaメソッドを使用して、欠損値を特定できます。

emp_df.isnull()

または

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	True	False
3233	False	False	False	False	True	False
3244	False	False	False	False	True	False
3251	False	False	False	False	True	False
3344	False	False	False	False	False	False
3577	False	False	False	False	True	False
3588	False	False	False	False	True	False
4466	False	False	False	False	True	False
5234	False	False	False	False	True	False
5566	False	False	False	False	False	False
7800	False	False	True	False	False	False

逆に、notnullおよびnotnaメソッドは非NULL値をTrueとしてマークします。これらの欠損値を削除するには、dropnaメソッドを使用します。このメソッドのaxisパラメータは、0軸または1軸に沿って削除を行うかを指定できます。つまり、欠損値に遭遇した際に行全体を削除するか、列全体を削除するかを決めます。デフォルトでは0軸方向に削除されます。

emp_df.dropna()

出力:

        ename   job      mgr	 sal    comm     dno
eno						
1359	胡一刀  销售员	3344.0	1800   200.0	30
2056	乔峰    架构师	 7800.0	 5000	1500.0	 20
3088	李莫愁  设计师	2056.0	3500   800.0	20
3344	黄蓉    销售主管	7800.0	3000   800.0	30
5566	宋远桥  会计师	7800.0	4000   1000.0	10

1軸方向に削除したい場合は以下のコードを使用します。

emp_df.dropna(axis=1)

出力:

        ename    job      sal    dno
eno				
1359	胡一刀   销售员    1800	30
2056	乔峰     架构师	  5000	 20
3088	李莫愁   设计师    3500	20
3211	张无忌   程序员    3200	20
3233	丘处机   程序员    3400	20
3244	欧阳锋   程序员    3200	20
3251	张翠山   程序员    4000	20
3344	黄蓉     销售主管  3000	30
3577	杨过     会计	   2200	  10
3588	朱九真   会计	  2500	 10
4466	苗人凤   销售员	 2500   30
5234	郭靖     出纳      2000   10
5566	宋远桥   会计师    4000   10
7800	张三丰   总裁      9000   20

注意:DataFrameオブジェクトの多くのメソッドにはinplaceというパラメータがあり、そのデフォルト値はFalseです。これは操作が元のDataFrameを変更せず、新しいDataFrameオブジェクトとして結果を返すことを意味します。このパラメータをTrueに設定すると、操作は元のDataFrame上で直接行われ、戻り値はNoneになります。つまり、上記の操作はemp_dfを変更せず、新たなDataFrameオブジェクトを返しているということです。

特定の状況下では、欠損値を埋めることも可能です。これにはfillnaメソッドが使用され、指定された値で埋める(valueパラメータで指定)、または前のセル(ffillパラメータ)または後のセル(bfillパラメータ)の値で埋めることができます。

emp_df.fillna(value=0)

注意:どの値で埋めるかは重要な課題であり、実務では平均値や最頻値などの統計量、または補間法(ランダム補間法、ラグランジュ補間法など)を使用することもあります。さらに、回帰モデルやベイズモデルなどを用いて欠損値を推定することもあります。

出力:

        ename    job        mgr      sal     comm    dno
eno
1359	胡一刀    销售员	   3344.0	1800	200.0	30
2056	乔峰	    分析师	    7800.0	 5000	 1500.0	 20
3088	李莫愁	   设计师	   2056.0	3500	800.0	20
3211	张无忌	   程序员	   2056.0	3200	0.0     20
3233	丘处机	   程序员	   2056.0	3400	0.0	    20
3244	欧阳锋	   程序员	   3088.0	3200	0.0     20
3251	张翠山	   程序员	   2056.0	4000	0.0	    20
3344	黄蓉	    销售主管   7800.0	3000	800.0	30
3577	杨过	    会计	     5566.0	  2200	  0.0	  10
3588	朱九真   会计	    5566.0	 2500	 0.0	 10
4466	苗人凤	   销售员	   3344.0	2500	0.0	    30
5234	郭靖	    出纳	     5566.0	  2000	  0.0	  10
5566	宋远桥   会计师	   7800.0	4000	1000.0	10
7800	张三丰   总裁	    0.0      9000	 1200.0	 20

重複データ

まず部門表に二行追加し、部門名「研発部」と「営業部」がそれぞれ二つあるようにします。

dept_df.loc[50] = {'dname': '研発部', 'dloc': '上海'}
dept_df.loc[60] = {'dname': '営業部', 'dloc': '長沙'}
dept_df

出力:

    dname  dloc
dno		
10	会计部	北京
20	研発部	成都
30	営業部	重庆
40	運維部	天津
50	研発部	上海
60	営業部	长沙

これで重複データが含まれるようになりました。duplicatedメソッドを使用して重複を確認できます。デフォルトでは行インデックスが重複しているかどうかを判断しますが、部門名'dname'に基づいて重複を判定することも可能です。

dept_df.duplicated('dname')

出力:

dno
10    False
20    False
30    False
40    False
50     True
60     True
dtype: bool

上記の結果から、dnoが50と60の部門が部門名が重複していることがわかります。重複を削除するには、drop_duplicatesメソッドを使用します。このメソッドのkeepパラメータは、重複した場合に最初の項目を保持するか、最後の項目を保持するか、あるいはすべて削除するかを指定できます。

dept_df.drop_duplicates('dname')

出力:

	dname	dloc
dno		
10	会计部	北京
20	研発部	成都
30	営業部	重庆
40	運維部	天津

keepパラメータをlastに変更します。

dept_df.drop_duplicates('dname', keep='last')

出力:

	dname	dloc
dno		
10	会计部	北京
40	運維部	天津
50	研発部	上海
60	営業部	长沙

異常値

統計学における異常値は、疑わしい異常値または外れ値(outlier)とも呼ばれ、観測値から著しく離れた値を指します。実務では、システムまたは人為的な理由で生じた異常値もある一方、特定のパターンを持つ正常な極端な値でもあります。例えばゲームにおける上位プレイヤーのデータは通常異常値として扱われます。異常値は無視すべきではなく、その原因を分析することで問題の発見と改善につながることがあります。

異常値の検出にはZ-score法、IQR法、DBScanクラスタリング、孤立木などが存在します。ここでは前二つの方法について簡単に説明します。

正規分布に従うデータに対しては、3σ法則に基づき平均値からの偏差が3標準偏差を超える値を異常値と定義します。正規分布において、平均値から3σ以上離れた値は確率的に非常に低いです。非正規分布の場合には、平均値からの標準偏差の倍数で異常値を表現します。この倍数をZ-scoreといい、以下の式で計算されます。

$$ z = \frac {X - \mu} {\sigma} $$

Z-scoreの閾値は経験と状況によって決定します。一般的に、標準偏差の3倍以上離れたデータポイントを外れ値とします。以下はZ-scoreによる異常値検出の例です。

import numpy as np


def detect_outliers_zscore(data, threshold=3):
    avg_value = np.mean(data)
    std_value = np.std(data)
    z_score = np.abs((data - avg_value) / std_value)
    return data[z_score > threshold]

IQR法(四分位範囲)におけるIQRは、上位四分位数(Q3)と下位四分位数(Q1)の差です。通常、Q1 - 1.5 × IQR未満またはQ3 + 1.5 × IQRより大きい値を異常値とします。これはボックスプロットでも使用される方法です。以下はIQR法による異常値検出の例です。

import numpy as np


def detect_outliers_iqr(data, whis=1.5):
    q1, q3 = np.quantile(data, [0.25, 0.75])
    iqr = q3 - q1
    lower, upper = q1 - whis * iqr, q3 + whis * iqr
    return data[(data < lower) | (data > upper)]

異常値を削除するには、DataFrameのdropメソッドを使用します。このメソッドは行または列のインデックスに基づいて指定された行または列を削除できます。例えば、給与が2000未満または8000を超えるデータを異常値とみなす場合、以下のコードで該当レコードを削除できます。

emp_df.drop(emp_df[(emp_df.sal > 8000) | (emp_df.sal < 2000)].index)

異常値を置換するには、セルに値を割り当てることも、replaceメソッドを使用して指定された値を置き換えても可能です。例えば、給与が1800または9000のものを平均値に、手当が800のものを1000に置き換えたい場合、以下のコードを使用します。

avg_sal = np.mean(emp_df.sal).astype(int)
emp_df.replace({'sal': [1800, 9000], 'comm': 800}, {'sal': avg_sal, 'comm': 1000})

前処理

データの前処理は大きなテーマであり、データの分割、変換、縮約、離散化などを行います。まずデータの分割を見てみましょう。日付時間データが含まれる場合、年、四半期、月、日、曜日、時間、分などの要素に分解する必要があります。文字列形式の日付時間をpandasのto_datetime関数で日時型に変換します。

例として、Excelファイルから売上データを読み込み、最初の列である販売日付を「月」「四半期」「曜日」に分割します。

sales_df = pd.read_excel(
    '2020年销售数据.xlsx',
    usecols=['销售日期', '销售区域', '销售渠道', '品牌', '销售额']
)
sales_df.info()

説明:上の例で使用するExcelファイルは、以下の百度云盤アドレスからダウンロードできます。データは「ゼロから学ぶデータ分析」ディレクトリ内にあります。リンク:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g、パスワード:e7b4。

出力:

<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

sales_df['月份'] = sales_df['销售日期'].dt.month
sales_df['季度'] = sales_df['销售日期'].dt.quarter
sales_df['星期'] = sales_df['销售日期'].dt.weekday
sales_df

出力:

	    销售日期	 销售区域	销售渠道	品牌	  销售额	月份	季度	星期
0	    2020-01-01	上海	     拼多多	 八匹马   8217	    1	 1	   2
1	    2020-01-01	上海	     抖音	      八匹马	6351	 1	  1	    2
2	    2020-01-01	上海	     天猫	      八匹马	14365	 1	  1	    2
3	    2020-01-01	上海	     天猫       八匹马	2366	 1	  1     2
4	    2020-01-01	上海	     天猫 	  皮皮虾	15189	 1	  1     2
...     ...         ...        ...       ...      ...     ...  ...   ...
1940    2020-12-30	北京	     京东	      花花姑娘 6994     12	 4	   2
1941    2020-12-30	福建	     实体	      八匹马	7663	 12	  4	    2
1942    2020-12-31	福建	     实体	      花花姑娘 14795    12	 4	   3
1943    2020-12-31	福建	     抖音	      八匹马	3481	 12	  4	    3
1944    2020-12-31	福建	     天猫	      八匹马	2673	 12	  4	    3

上記のコードでは、日付時間型のSeriesオブジェクトのdt属性を使用して日付時間アクセスオブジェクトを取得し、yearmonthquarterhourなどの属性から年、月、四半期、時間などの情報を得ています。得られた情報はSeriesオブジェクトであり、これを「日付時間ベクトル」と呼びます。

文字列型データの処理についても述べます。特定のExcelファイルから求人データを読み込みます。

jobs_df = pd.read_csv(
    '某招聘网站招聘数据.csv',
    usecols=['city', 'companyFullName', 'positionName', 'salary']
)
jobs_df.info()

説明:上の例で使用するExcelファイルは、以下の百度云盤アドレスからダウンロードできます。データは「ゼロから学ぶデータ分析」ディレクトリ内にあります。リンク:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g、パスワード:e7b4。

出力:

<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

最初の5行を表示します。

jobs_df.head()

出力:

    city    companyFullName              positionName    salary
0   北京	  达疆网络科技(上海)有限公司    数据分析岗       15k-30k
1   北京	  北京音娱时光科技有限公司        数据分析        10k-18k
2   北京	  北京千喜鹤餐饮管理有限公司	     数据分析        20k-30k
3   北京	  吉林省海生电子商务有限公司	     数据分析        33k-50k
4   北京	  韦博网讯科技(北京)有限公司	数据分析        10k-15k

上記のデータは3140件ありますが、「データ分析」の職種のみを抽出する必要があります。positionName列の内容に「データ分析」を含むものを選択する必要があり、ここでは部分一致が必要です。str属性を使用して文字列操作を実行します。

jobs_df = jobs_df[jobs_df.positionName.str.contains('数据分析')]
jobs_df.shape

出力:

(1515, 4)

抽出後は1515件になりました。次にsalaray列を処理します。平均給与や都市別の平均給与を計算するために、範囲で表記されている給与を中央値に変換する必要があります。

jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?')

説明:上記のコードは正規表現を使用して文字列から二つの数字を抽出し、それぞれ給与の下限と上限を示します。正規表現に詳しくない読者は、私の知乎専欄「ゼロから学ぶPython」内の「正規表現の応用」記事を参照してください。

出力:

        0     1
0	    15    30
1	    10	  18
2       20    30
3       33    50
4       10    15
...     ...   ...
3065    8     10
3069    6     10
3070    2     4
3071    6     12
3088    8     12

抽出された列は文字列型なので、int型に変換する必要があります。これにはapplymapメソッドを使用します。このメソッドの引数は関数であり、DataFrameの各要素に適用されます。その後、applyメソッドを使用してDataFrameを中央値に変換します。applyメソッドの引数も関数で、axisパラメータを指定することで行または列に適用できます。

temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int)
temp_df.apply(np.mean, axis=1)

出力:

0       22.5
1       14.0
2       25.0
3       41.5
4       12.5
        ... 
3065    9.0
3069    8.0
3070    3.0
3071    9.0
3088    10.0
Length: 1515, dtype: float64

この結果をsalaray列に置き換えるか、新たな列として追加します。完全なコードは以下の通りです。

temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int)
jobs_df['salary'] = temp_df.apply(np.mean, axis=1)
jobs_df.head()

出力:

    city    companyFullName              positionName    salary
0   北京	  达疆网络科技(上海)有限公司    数据分析岗       22.5
1   北京	  北京音娱时光科技有限公司        数据分析        14.0
2   北京	  北京千喜鹤餐饮管理有限公司	     数据分析        25.0
3   北京	  吉林省海生电子商务有限公司	     数据分析        41.5
4   北京	  韦博网讯科技(北京)有限公司	数据分析        12.5

applymapapplyメソッドはデータ前処理時によく使われます。Seriesオブジェクトにもapplyメソッドがあり、データ前処理に使用されますが、DataFrameオブジェクトにはtransformメソッドもあり、関数を使ってデータを変換します。applyは集約効果を持ち、多くのデータを少数または一つのデータにまとめます。一方、transformは変換のみを行い、データの数は変わりません。

高度な分析や掘り下げた解析を行うには、文字列や日付時間などの非数値データを数値に変換する必要があります。なぜなら、非数値データは相関を計算できず、χ²検定なども行えないためです。文字列データは以下の3つのカテゴリに分けられます。

  1. 順序変数(Ordinal Variable):文字列で表されるデータに順序関係がある場合、文字列を番号に変換します。
  2. 分類変数(Categorical Variable)/名義変数(Nominal Variable):文字列で表されるデータに大小や階級の関係がない場合、ワンホットエンコーディングでダミー変数行列を作ります。
  3. 定尺度変数(Scale Variable):文字列が数値の大小関係を持ち、加減算ができる場合、文字列を対応する数値に変換します。

第1類と第3類はapplyまたはtransformメソッドで処理できます。第1類についてはscikit-learnのOrdinalEncoderを使用します。第2類については、pandasのget_dummies()関数でダミー変数行列を作成できます。

persons_df = pd.DataFrame(
    data={
        '姓名': ['关羽', '张飞', '赵云', '马超', '黄忠'],
        '职业': ['医生', '医生', '程序员', '画家', '教师'],
        '学历': ['研究生', '大专', '研究生', '高中', '本科']
    }
)
persons_df

出力:

	姓名	职业	学历
0	关羽	医生	研究生
1	张飞	医生	大专
2	赵云	程序员	研究生
3	马超	画家	高中
4	黄忠	教师	本科

職業をダミー変数行列に変換します。

pd.get_dummies(persons_df['职业'])

出力:

    医生 教师  画家  程序员
0	1    0    0    0
1	1    0    0    0
2	0    0    0    1
3	0    0    1    0
4	0    1    0    0

学歴を数値に変換します。

def handle_education(x):
    edu_dict = {'高中': 1, '大专': 3, '本科': 5, '研究生': 10}
    return edu_dict.get(x, 0)


persons_df['学历'].apply(handle_education)

出力:

0    10
1     3
2    10
3     1
4     5
Name: 学历, dtype: int64

次にデータの離散化について説明します。離散化はボックス化とも呼ばれ、連続値を持つ変数は無数の値を取り得るため、グループ化が困難です。このため、連続変数を離散化することが重要です。ボックス化とは事前にボックスを設定し、各ボックスがデータの範囲を示すことで、連続値を異なるボックスに割り当てます。以下は2018年の北京の積分移住データを読み込み、移住積分に基づいてデータをグループ化する例です。

luohu_df = pd.read_csv('data/2018年北京积分落户数据.csv', index_col='id')
luohu_df.score.describe()

出力:

count    6019.000000
mean       95.654552
std         4.354445
min        90.750000
25%        92.330000
50%        94.460000
75%        97.750000
max       122.590000
Name: score, dtype: float64

移住積分の最大値は122.59、最小値は90.75であるため、90点から125点まで5点ごとの7つのボックスを作成します。pandasのcut関数を使用してデータをボックス化できます。

bins = np.arange(90, 126, 5)
pd.cut(luohu_df.score, bins, right=False)

説明cut関数のrightパラメータのデフォルトはTrueで、ボックスを左開右閉にします。Falseに設定すると右境界を開放し、左境界を閉じます。出力を確認してください。

出力:

id
1       [120, 125)
2       [120, 125)
3       [115, 120)
4       [115, 120)
5       [115, 120)
           ...    
6015      [90, 95)
6016      [90, 95)
6017      [90, 95)
6018      [90, 95)
6019      [90, 95)
Name: score, Length: 6019, dtype: category
Categories (7, interval[int64, left]): [[90, 95) < [95, 100) < [100, 105) < [105, 110) < [110, 115) < [115, 120) < [120, 125)]

ボックス化の結果を基にデータをグループ化し、集約関数を各グループに適用する操作は、データ分析でよく使われます。次の章で詳細に説明します。さらに、pandasにはqcutという関数があり、分位数に基づいてデータをボックス化できます。興味のある読者は調べてみてください。

タグ: Pandas Python data-cleaning missing-values duplicates

7月30日 03:07 投稿