データ分析でよく使うスニペット集【前処理編】

# はじめに
- データ分析のコンペに参加していると、よく使うスクリプトがあるようです
- これをスニペットというらしく、自分でもまとめてみます。

# 可視化・グラフ描画
- データをもらったら、まずはデータを眺める
- 以下のような出力を見ながら、前処理の仕方を考える

```python:sns.py
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

%matplotlib.inline
sns.set()

# 日本語化
# !pip install japanese_matplotlib
import japanese_matplotlib
```

```python
# 数値とオブジェクト型のカラムをリストで取得
numeric_cols = df_train.select_dtypes(include=['float64', 'int64']).columns
categorical_cols = df_train.select_dtypes(include=['object']).columns
```

```python
# violinplotを描画する関数
def plot_violinplot(df, numeric_cols, n_rows, n_cols):
    plt.figure(figsize=(15, 8))

    for i, col in enumerate(numeric_cols, 1):
        plt.subplot(n_rows, n_cols, i)
        sns.violinplot(x=df[col])
        
    plt.tight_layout()
    plt.show()
```


```python
# Boxplotを描画する関数
def plot_boxplot(df, numeric_cols, n_rows, n_cols):
    plt.figure(figsize=(15, 8))
    
    for i, col in enumerate(numeric_cols, 1):
        plt.subplot(n_rows, n_cols, i)
        sns.boxplot(x=df[col])
        plt.xticks(rotation=90)  # x軸のラベルを回転して表示
        
    plt.tight_layout()
    plt.show()
```

```python
# ヒストグラムを描画する関数
def plot_histograms(df, categorical_cols, n_rows, n_cols):
    plt.figure(figsize=(15, 8))
    
    for i, col in enumerate(categorical_cols, 1):
        plt.subplot(n_rows, n_cols, i)
        sns.histplot(df[col], kde=True)
    
    plt.tight_layout()
    plt.show()
```

# 欠損値の数と一意の数を集計
```python
def basic_details(df):
    b = pd.DataFrame()
    b['Missing value'] = df.isnull().sum()
    b['N unique value'] = df.nunique()
    b['dtype'] = df.dtypes
    return b
basic_details(df_train)
```


# 前処理の前後の結合と分割
- 欠損値
```python
# df_trainとdf_testを結合する

## df_trainとdf_testに'is_train'列を追加（訓練データ=1, テストデータ=0）
df_train['is_train'] = 1
df_test['is_train'] = 0

# SalePriceがテストデータには存在しないので、結合のために仮の列を追加
df_test['SalePrice'] = None

# 訓練データセットとテストデータセットを結合
combined_df = pd.concat([df_train, df_test], ignore_index=True)
```

```python
# 前処理が完了したら、分割する

# 訓練データセットの抽出
df_train_processed = combined_df[combined_df['is_train'] == 1].drop(columns=['is_train', 'SalePrice'])

# テストデータセットの抽出（'SalePrice'列を除外）
df_test_processed = combined_df[combined_df['is_train'] == 0].drop(columns=['is_train', 'SalePrice'])
```

# メモリの削減
```python
def reduce_mem_usage(df, verbose=True):
    numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
    start_mem = df.memory_usage().sum() / 1024**2
    for col in df.columns:
        col_type = df[col].dtypes
        if col_type in numerics:
            c_min = df[col].min()
            c_max = df[col].max()
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)
            else:
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float16)
                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                    df[col] = df[col].astype(np.float32)
                else:
                    df[col] = df[col].astype(np.float64)
    end_mem = df.memory_usage().sum() / 1024**2
    if verbose: print('Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction)'.format(end_mem, 100 * (start_mem - end_mem) / start_mem))
    return df
```

```python
print('start size: {:5.2f} Mb'.format(combined_df.memory_usage().sum() / 1024**2))
combined_df = reduce_mem_usage(combined_df)
```

# カテゴリ変数の数値化
```python
# ラベルエンコーディング
from sklearn.preprocessing import LabelEncoder

# カテゴリカラムを取得
categorical_cols = df_train.select_dtypes(include=['object']).columns

# df_trainにラベルエンコーディング
label_encoders = {}
encoded_columns_train = {}
for cat_col in categorical_cols:
    le = LabelEncoder()
    le.fit(df_train[cat_col])
    encoded_columns_train[cat_col+'_le'] = le.transform(df_train[cat_col]) # ラベルエンコーディングされた列を辞書に追加
    label_encoders[cat_col] = le  # LabelEncoderを辞書に保存
# 辞書から新しいDataFrameを作成し、元のDataFrameに結合
encoded_df = pd.DataFrame(encoded_columns_train)
df_train = pd.concat([df_train, encoded_df], axis=1)

# df_testにラベルエンコーディング
encoded_columns_test = {}
for cat_col in categorical_cols:
    #df_test[cat_col].fillna(df_train[cat_col].mode()[0], inplace=True)  # 欠損値を最頻値に置き換え
    le = label_encoders[cat_col]  # df_trainでfitされたLabelEncoderを使用
    encoded_columns_test[cat_col + '_le'] = le.transform(df_test[cat_col])
# 辞書から新しいDataFrameを作成し、元のdf_testに結合
encoded_df_test = pd.DataFrame(encoded_columns_test)
df_test = pd.concat([df_test, encoded_df_test], axis=1)


# 必要に応じて、元のカテゴリカル列を削除
# df_train.drop(categorical_cols, axis=1, inplace=True)
# df_test.drop(categorical_cols, axis=1, inplace=True)
```

# 外れ値を2つ以上持つ行を削除する
```python
from collections import Counter

def detect_outliers(df, n, features):
    """
    データフレームdfと特徴量のリストを取り、Tukey法による外れ値がn個以上含まれる観測のインデックスのリストを返します。
    """
    outlier_indices = []
    
    # 特徴量（カラム）ごとに繰り返す
    for col in features:
        #外れ値を定義
        Q1 = np.percentile(df[col], 25)
        Q3 = np.percentile(df[col], 75)
        IQR = Q3 - Q1
        outlier_step = 1.5 * IQR
        
        # 特徴量colについての外れ値のインデックスリストを作成
        outlier_list_col = df[(df[col] < Q1 - outlier_step) | (df[col] > Q3 + outlier_step)].index
        outlier_indices.extend(outlier_list_col)
        
    # 2つ以上のアウトライヤーを含む行を選択する
    outlier_indices = Counter(outlier_indices)        
    multiple_outliers = list(k for k, v in outlier_indices.items() if v > n)
    
    return multiple_outliers

Outliers_to_drop = detect_outliers(train_df,2, numeric_cols)
#df_train.loc[Outliers_to_drop] # 外れ値を2つ以上持つ行のみを表示

# 外れ値を2つ以上持つ行を削除
df_train = df_train.drop(Outliers_to_drop, axis = 0).reset_index(drop=True)
```

# 欠損値をとりあえず埋める
```python
# 欠損値を含む、数値・カテゴリcolを取得
missing_numeric_cols = [col for col in numeric_cols if combined_df[col].isnull().any()]
missing_categorical_cols = [col for col in categorical_cols if combined_df[col].isnull().any()]

# 数値なら0,カテゴリならNで埋める
for col in missing_numeric_cols:
    combined_df[col] = combined_df[col].fillna(0)
    
for col in missing_categorical_cols:
    combined_df[col] = combined_df[col].fillna("N")
```

# 欠損値を指定した列ごとの平均・最頻値・中央値で埋める

```python
def fill_missing_values(df, group_col, target_col, method='mean', rounding=None):
    """
    欠損値を埋める関数。
    
    Parameters:
        df (pd.DataFrame): 対象のデータフレーム。
        group_col (str): グループ化に使用する列名。
        target_col (str): 欠損値を埋めたい対象列名。
        method (str): 'mean'（平均値）、'mode'（最頻値）、または 'median'（中央値）のどれを使用するか。デフォルトは 'mean'。
        rounding (int): 四捨五入の桁数を指定する。None の場合は四捨五入を行わない。
    """
    if method == 'mean':
        fill_value = df.groupby(group_col)[target_col].mean()
    elif method == 'mode':
        fill_value = df.groupby(group_col)[target_col].agg(lambda x: x.mode().iloc[0])
    elif method == 'median':
        fill_value = df.groupby(group_col)[target_col].median()
    else:
        raise ValueError("Method must be 'mean', 'mode', or 'median'")
    
    if rounding is not None:
        fill_value = fill_value.round(rounding).astype('int')
    
    fill_value = fill_value.reset_index()
    
    def fill_row(row):
        if pd.isna(row[target_col]):
            return fill_value[fill_value[group_col] == row[group_col]][target_col].values[0]
        else:
            return row[target_col]

    df[target_col] = df.apply(fill_row, axis=1)
    return df

# 実際の使用例
# combined_df = fill_missing_values(combined_df, 'Designation', 'MonthlyIncome', method='median', rounding=-4)
# 結果を確認
# print(combined_df['MonthlyIncome'].isna().sum())
```

# 特徴量の追加

```Python
# dfに'xごとのyの平均'と'y - xごとのyの平均'列を追加する関数
def add_grouped_feature(df, group_col, value_col, mean_col_name, diff_col_name):
    # グループごとの平均を計算し、新しい列として直接追加
    df[mean_col_name] = df.groupby(group_col)[value_col].transform('mean')
    # 元の値とグループ平均との差分を新しい列として追加
    df[diff_col_name] = df[value_col] - df[mean_col_name]
    
    return df

# 使い方の例
combined_df = add_grouped_feature(df=combined_df,
                                  group_col='Neighborhood',
                                  value_col='GrLivArea',
                                  mean_col_name='Neighborhood_GrLivArea_Mean',
                                  diff_col_name='GrLivArea_Diff_From_Neighborhood_Mean')
```


# 記述統計に基づく特徴量エンジニアリング

```python
# 記述統計に基づいて、特徴量を作成する関数
## 各選択カラムに対して、以下の新しい特徴量カラムを作成します:
### c+'_median_range': カラムの値がその中央値より大きいかどうか（0 または 1）。
### c+'_mean_range': カラムの値がその平均値より大きいかどうか（0 または 1）。
### c+'_q1': カラムの値が第1四分位数未満かどうか（0 または 1）。
### c+'_q3': カラムの値が第3四分位数より大きいかどうか（0 または 1）。

def descrictive_stat_feat(df):
    df = pd.DataFrame(df)
    dcol= [c for c in df.columns if df[c].nunique()>=10]
    d_median = df[dcol].median(axis=0)
    d_mean = df[dcol].mean(axis=0)
    q1 = df[dcol].apply(np.float32).quantile(0.25)
    q3 = df[dcol].apply(np.float32).quantile(0.75)
    
    for c in dcol:
        df[c+str('_median_range')] = (df[c].astype(np.float32).values > d_median[c]).astype(np.int8)
        df[c+str('_mean_range')] = (df[c].astype(np.float32).values > d_mean[c]).astype(np.int8)
        df[c+str('_q1')] = (df[c].astype(np.float32).values < q1[c]).astype(np.int8)
        df[c+str('_q3')] = (df[c].astype(np.float32).values > q3[c]).astype(np.int8)
    return df

df = descrictive_stat_feat(df)

```