目次
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属性を使用して日付時間アクセスオブジェクトを取得し、year、month、quarter、hourなどの属性から年、月、四半期、時間などの情報を得ています。得られた情報は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
applymapとapplyメソッドはデータ前処理時によく使われます。Seriesオブジェクトにもapplyメソッドがあり、データ前処理に使用されますが、DataFrameオブジェクトにはtransformメソッドもあり、関数を使ってデータを変換します。applyは集約効果を持ち、多くのデータを少数または一つのデータにまとめます。一方、transformは変換のみを行い、データの数は変わりません。
高度な分析や掘り下げた解析を行うには、文字列や日付時間などの非数値データを数値に変換する必要があります。なぜなら、非数値データは相関を計算できず、χ²検定なども行えないためです。文字列データは以下の3つのカテゴリに分けられます。
- 順序変数(Ordinal Variable):文字列で表されるデータに順序関係がある場合、文字列を番号に変換します。
- 分類変数(Categorical Variable)/名義変数(Nominal Variable):文字列で表されるデータに大小や階級の関係がない場合、ワンホットエンコーディングでダミー変数行列を作ります。
- 定尺度変数(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という関数があり、分位数に基づいてデータをボックス化できます。興味のある読者は調べてみてください。