python pandas と R tidyverseの比較

Rのtidyverseパッケージ群は、データの操作や可視化を簡潔で一貫した記述で行うことができる非常に優れたツールで、私も愛してやみません。
しかし、最近はシステムにモデルを組み込んだり、ディープラーニングライブラリを試したりするために、Pythonそしてpandasパッケージを使用することが増えています。

ただ、pandasは、pandasの関数、DataFrameオブジェクトのメソッド、インデクサーなどを駆使してデータの操作を行うため、（個人的には）一貫性に乏しく操作が覚えにくいと感じます。

"前処理大全"など良書もありますが、tidyverseとpandasの純粋な比較はWeb・書籍でも目にしなかったので、この記事では備忘録的に作成したtidyverse-pandasの比較について共有します。
まだ足りない点があるので順次更新を行っていく予定です。
（2019/3/31　追記をしました。2022/7/30 dplyr 1.0、tidyr 1.0に合わせて記載内容を変更しました。）

R 3.6.1、tidyverse 1.3.1 (dplyr 1.0.9、tidyr 1.2.0、purrr 0.3.4、readr 2.0.2、tibble 3.1.6)
Python 3.9.7、pandas 1.3.4、numpy 1.21.3
で検証しています。

# 内容
1. tibbleの操作
2. dplyrの操作
3. tidyrの操作
4. purrrの操作
5. readrの操作
6. base

それぞれ`dplyr::select`などtidyverseの操作項目ごとにまとめ、最初にRのコード、次にPythonのコードという順で並べています。
基本的にRの操作はパイプ演算子(%>%)で、pythonもドット(.)で処理をつなげることが可能です。

# tibble
## 初期化
### tidiverse
```r
df <- tibble(col1=c(1, 2, 3), col2=c(4, 5, 6))
```
### pandas

```python
df = pd.DataFrame({'col1':[1, 2, 3], 'col2':[4, 5, 6]})

df = pd.DataFrame([[1, 4], [2, 5], [3, 6]], columns=['col1', 'col2'])
``` 

  
# dplyr
dataframe サンプル

```r
df <- tibble(col1=c(1, 1, 2), col2=c(4, 5, 6))
df2 <- tibble(col1 = c(1, 2, 2), col2=c(4, 5, 6), col3=c(7, 8, 9))
```

```python
df = pd.DataFrame({'col1':[1, 1, 2], 'col2':[4, 5, 6]})
df2 = pd.DataFrame({'col1':[1, 2, 2], 'col2':[4, 5, 6], 'col3':[7, 8, 9]})
```

## dplyr::select (列選択)
### tidyverse
```r
df %>% dplyr::select(col1, col2)
df %>% dplyr::select(-col1) # col1以外を選択
df %>% dplyr::select_if(is.numeric) # 条件選択。この例では数値列を選択。
```
### pandas
```python
df.loc[:, ['col1', 'col2']] 
df.drop('col1', axis=1) # col1以外を選択。複数列の場合drop(['col1', 'col2'], axis=1)
df.select_dtypes('number') # 条件選択。この例では数値列を選択。
```
- 数値列**以外**を除外する場合は`df.select_dtypes(exclude='number')`
- `'number'`は`int`や`float`といった数値型をまとめて表したもの。

※カラム名の指定方法による、返り値の型の違い

```python
df['co1'] # pd.Seriesが返る
df[['col1']] # pd.DataFrameが返る
df.loc[:, 'col1'] # pd.Seriesが返る
df.loc[:, ['col1']] # pd.DataFrameが返る
```

## dplyr::select (with helpers) (列選択)
### tidyverse
```r
df %>% dplyr::select(starts_with('col')) # starts_with
df %>% dplyr::select(ends_with('1')) # ends_with
df %>% dplyr::select(contains('1')) # contains
```
### pandas
```python
df.loc[:, [c.startswith('col') for c in df.columns]] # startswith
df.loc[:, [c.endswith('1') for c in df.columns]] # endswith
df.loc[:, ['1' in c for c in df.columns]] # contains
```

## dplyr::mutate (列追加)
### tidyverse
```r
df %>% dplyr::mutate(col3 = col1*2)
df %>% dplyr::mutate(across(.fns=list(log=log, double=~.*2), .names="{.col}_{.fn}")) # すべての列に関数を適用し、列を追加。
df %>% dplyr::mutate(across(.cols=where(is.numeric), .fns=list(log=log, double=~.*2), .names="{.col}_{.fn}")) # 条件にあった列のみに関数を適用し、列を追加。
df %>% dplyr::mutate(across(.cols=ends_with("1"), .fns=list(log=log, double=~.*2), .names="{.col}_{.fn}"))  # 選択した行のみに関数を適用し、列を追加。
```
- `across()`に.colsを指定しな場合は.cols=everything()で処理される。

### pandas
```python
df.assign(col3=lambda df: df.col1*2) # dfがreplaceされない。返り値はdeep copy。
df.apply([np.log, lambda x: x*2]) # すべての列にapplyで関数を適用。元の列は残らないので、残す場合はpd.concat()で元のDataFrameと結合（以下の2つのサンプルも同じ）。
df.select_dtypes('number').apply([np.log, lambda x: x*2]) # 選択した行（条件にあった行）のみに関数を適用し、列を追加。
df.loc[:, [c.endswith('1') for c in df.columns]].apply([np.log, lambda x: x*2])  # 選択した業のみに関数を適用し、列を追加。
```
- `df['col3'] = df['col1'] *2` とした場合、列を追加した上で、dfがreplaceされる。
- すべての列にnumpyの関数を適用するならば、例えば`np.mean(df)`でも可能。
- すべての列にWindow関数（cumsum()（累積和）やdiff()（オフセット差分）、rolling(3).sum()（移動平均））など以下にあげるVectorized funciotnを適用するならば、`df.cumsum()`とする。applyを使うと挙動がよくわからない。

## dplyr::filter (行条件選択)
### tidyverse
```r
df %>% dplyr::filter(col1==1, col2>4)
df %>% dplyr::filter(col1 %in% c(1, 10, 100)) # in演算子を利用する場合
```

### pandas
```python
df.query("col1==1 & col2>4")
df.query("col1 in [1, 10, 100]") # in演算子を利用する場合
```
- 条件に変数を使う場合は
 `i=1; j=4` `df.query("col1==%s & col2>%s" % (i, j))`


## dplyr::arrange (並び替え)
### tidyverse
 
```r
df %>% dplyr::arrange(col1, col2) # 昇順
df %>% dplyr::arrange(desc(col1), desc(col2)) # 降順
```

### pandas
 
```python
df.sort_values(['col1', 'col2']) # 昇順
df.sort_values(['col1', 'col2'], ascending=[False, False]) # 降順
```
- ascendingをリストで指定することで、それぞれの列に対して昇順・降順を選択可能。
- すべての列で昇順/降順が同じならばスカラー(ascending=True または False)で良い。
- 引数inplace=TRUEを入れると、データフレームを置き換える。



## dplyr::group_by, summarise （グルーピング、集約）
### tidyverse
```r
df %>% dplyr::group_by(col1) %>% dplyr::summarise(col2_mean = mean(col2)) 
df %>% dplyr::group_by(col1) %>% dplyr::summarise(across(.fns=mean)) # すべての列に関数を適用
```
- グルーピングする変数が2つ以上の場合は`group_by(col1, col2)`とgroup_byの引数に列名をカンマ区切りで指定。

### pandas
```python
df.groupby('col1', as_index=False).agg({'col2': np.mean}) # 関数を適用する変数を指定するときはagg関数の引数にキーがカラム名、値が関数名の辞書を指定する。
df.groupby('col1', as_index=False).agg(np.mean) # すべての列に関数を適用するときは辞書で指定は不要。
```
- グルーピングする変数が2つ以上の場合は`groupby([col1, col2])`とgroupbyの引数にカラム名をリストで指定。
- `dplyr::summarise_if(is.numeric, funs(mean))`のように、条件指定をしなくても、pandasの場合は適当に計算可能な列のみで計算してくれるよう。だたし、処理内部の挙動が良くわからないので、数値列とカテゴリカル列などが混在する場合は、groupbyの前に`df.select_dtypes(include='number')`などで列選択をしたほうが無難そう。
- `dplyr::summarise_at()`に相当する処理は直接的にはなく、groupbyの前に、locで列選択をしないとできなそう。
- 以下の関数をすべての列に適用する場合は、agg関数の引数ではなく、直接groupby().関数名()で処理可能。


| Function | Description |
|:--|:--|
| mean() | mean of groups |
| sum() | sum of group values |
| prod()| product of group |
| count() | Compute count of group |
| std() | Standard deviation of groups |
| var() | variance of groups |
| sem() | Standard error of the mean of groups |
| describe() | Generates descriptive statistics |
| first() | first of group values |
| last() | last of group values |
| nth() | nth value, or a subset if n is a list |
| min() | min of group values |
| max() | max of group values |
| quantile(q) ※ | q quantile of group values |
※ quantileは処理後にグルーピングした変数が消えたりと、挙動がよくわからないので、agg(lambda x: np.quantile(x, q))としたほうが無難。値にNAが含まれる場合はnp.nanquantile(x, q)。

- `df = df.groupby('col1', as_index=False).agg({'col2: [np.mean, np.max, np.min]})`のように、1カラムに対して、複数の集計をするとマルチカラムになるため、その後`query()`を適用できないなど扱いづらい。以下のような処理で、普通のカラムに戻したほうが扱い易いことが多い。中身の説明は割愛。※[このブログ](https://on-your-mark.tokyo/tips/python/python_multiindex/)を参考にさせていただきました。
 
```python
def flatten_cols(df, replace=False):
    levels = df.columns.levels
    labels = df.columns.labels
    
    col_level_1 = levels[0][labels[0]]
    col_level_2 = levels[1][labels[1]]
    col_level_2 = [x if x == "" else "_"+x for x in col_level_2]
    
    new_columns = [l1+l2 for l1, l2 in zip(col_level_1, col_level_2)]
    if replace==False:
        df_res = df.copy()
        df_res.columns = new_columns
        return df_res
    else:
        df.columns = new_columns
        return df

flatten_cols(df)
``` 

- マルチカラムに対して、列選択するだけならば、pandas.IndexSliceを使えば良い。

```python
idx = pd.IndexSlice
df.loc[:, idx['col2', ['amax', 'amin']]]
```

## dplyr::bind_cols (横方向結合)
### tidyverse
```r
df %>% dplyr::bind_cols(df2)
```
### pandas
```python
pd.concat([df.reset_index(drop=True), df2.reset_index(drop=True)], axis=1)
```
- indexをキーに結合するのではなく、as isの順序通りに結合する場合は それぞれreset_index(drop=True)をつけたほうが無難。（indexが0から順に並んでいればreset_indexは不要。）
- チェーンで処理するならば`df.pipe(lambda _df: pd.concat([_df, df2], axis=1))`のようにする。


## dplyr::left_join
### tidyverse
```r
df %>% dplyr::left_join(df2) # keyの指定なし（カラム名が同じ列がキーになる）
df %>% dplyr::left_join(df2, by=c('col1', 'col2')) # keyを明示
df %>% dplyr::left_join(df2, by=c('col1' = 'col2')) # カラム名が異なるkeyを明示
```

### pandas
```python
df.merge(df2, how='left') # keyの指定なし（カラム名が同じ列がキーになる）
df.merge(df2, on=['col1', 'col2'], how='left') # keyを明示
df.merge(df2, left_on='col1' right_on='col2', how='left').drop('col2_y', axis=1) # カラム名が異なるkeyを明示。dropなしの場合'col2'は残る。
```

## dplyr::inner_join
### tidyverse
```r
df %>% dplyr::inner_join(df2) # keyの指定なし。keyを明示する場合もleft_joint同じ。
```

### pandas
```python
df.merge(df2, how='inner') # keyの指定なし。
```

## dplyr::full_join
### tidyverse
```r
df %>% dplyr::full_join(df2) # keyの指定なし。keyを明示する場合もleft_joint同じ。
```

### pandas
```python
df.merge(df2, how='outer') # keyの指定なし。
```

## dplyr::bind_rows (縦方向結合)
### tidyverse
```r
df %>% dplyr::bind_rows(df2) 
```

### pandas
```python
pd.concat([df, df2], ignore_index=True)
```
- appendは1.4で非推奨となった。
- デフォルトではindexは元の2つのDataFrameのままなので、ignore_index=Trueでindexを割り振り直したほうが無難。
- チェーンで処理するならば`df.pipe(lambda _df: pd.concat([_df, df2], ignore_index=True))`。

## dplyr::union (重複削除　縦連結)
### tidyverse
```r
df %>% dplyr::union(df2 %>% dplyr::select(-col3))
```

### pandas
```python
pd.concat([df, df2], ignore_index=True).drop_duplicates()
```

## dplyr::intersect（重複行の抽出）
### tidyverse
```r
df %>% dplyr::intersect(df2 %>% dplyr::select(-col3))
``` 
- base　にもintersect関数が存在するので`dplyr::intersect()`と明示したほうが無難。
 
### pandas
```python
df[df.isin(df2).apply(lambda x: all(x), axis=1)]
```

## dplyr::setdiff　（重複しない行の抽出）
### tidyverse
```r
df %>% dplyr::setdiff(df2 %>% dplyr::select(-col3))
``` 
- base　にもsetdiff関数が存在するので`dplyr::setdiff()`と明示したほうが無難。
 
#pandas
```python
df[df.isin(df2).apply(lambda x: not all(x), axis=1)]
```
 
## dplyr::sample_n, sample_frac (行サンプリング)
### tidyverse
```r
df %>% dplyr::sample_n(2, replace=FALSE) # サンプリング数で指定
df %>% dplyr::sample_frac(0.5, replace=FALSE) # サンプリングの割合で指定
```
### pandas
```python
df.sample(n=2) # サンプリング数で指定
df.sample(frac=0.5) # サンプリングの割合で指定
```
- 引数 random_state でランダムシードを固定可能。

## dplyr::distinct (重複行削除)
### tidyverse
```r
df %>% dplyr::distinct(col1, col2, .keep_all=TRUE) 
```
- col1とcol2の値がともに重複している行を、最初に現れる行以外削除。
- .keep_all=TRUEで指定の列以外も残す。（デフォルトはFALSEで、指定列以外は残さない。）

### pandas
```python
df.drop_duplicates(['col1', 'col2']) 
```
- col1とcol2の値がともに重複している行を最初に現れる行以外削除。
- 指定の列以外も残す。
- 引数keep=FALSEで重複した行はすべて削除。
- keep='last'で重複している行を最後に現れる行以外削除。
- SeriesオブジェクトにはSeries.unique()でユニークなSeriesを抽出可能。
          

## dplyr::slice (行選択、削除)
### tidyverse
```r
df %>% dplyr::slice(1:2) # 1~2行目を抜き出す。
df %>% dplyr::slice(-1:-2) # 1~2行目を削除。
```
- 行indexは1から。x1:x2はx2行目を含む。

### pandas
```python
df.iloc[0:3, :] # 1~2行目を抜き出す。
df.drop([0, 1]) # 1~2行目を削除。
``` 
- 行indexは0から。x1:x2はx2行目を **含まない**

## dplyr::rename （リネーム）
### tidyverse
```r
df %>% dplyr::rename(col_1 = col1)
```
- 文字列ベクトルですべての列名を更新する場合は、`purrr::set_names()`を用いる 
```
df %>% set_names(c("col1_", "col2_"))
```

### pandas
```python
df.rename(columns={'col1':'col_1'})
```
- inplace=True でreplace。
- 文字列配列ですべての列名を更新する場合は、`DataFrame.set_axis()`を用いる。
```
df.set_axis(["col1_", "col2_"], axis=1)
```
 
## vectorized function
Rは`mutate()`との組み合わせて使う場合が多いと思う。
Python pandas DataFrameは直接メソッドを呼び出せる。

| dplyr | pandas | 説明 |
|:--|:--|:--|
| x-lag(x, n) | df.diff(n) |n個後方の値との差分 |
| x-lead(x, n) | df.diff(-n) |n個前方の値との差分 |
| min_rank(x) | df.rank(method='min') |昇順ランキング。タイは同じ順位。タイがn個あれば、次の順位はn-1個飛ばす。|
| dense_rank(x) | df.rank(method='dense') |昇順ランキング。タイは同じ順位。次の順位は飛ばさない。|
| row_number(x) | df.rank(method='first') |昇順ランキング。タイがあった場合、最初を優先。
| cumsum(x) ※| df.cumsum() | 累積和 |
| cumprod(x) ※| df.cumprod() | 累積積 |
| cummax(x) ※| df.cummax() | 累積最大値 |
| cummin(x) ※| df.cummin() | 累積最小値 |

- pandasのrankはascending=Falseで降順。
- dplyrのrank関数はdesc(x)で降順。
- ※はRのbaseの関数。

# tidyr
dataframe サンプル

```r
df <- tibble(id=c('1', '2', '3'), col1=c(1, 2, NA), col2=c(4, 5, 6))
```

```python
df = pd.DataFrame({'id':['1', '2', '3'], 'col1':[1, 2, np.nan], 'col2':[4, 5, 6]})
```

## tidyr::pivot_longer (横縦変換)
### tidyverse
```r
df %>% tidyr::pivot_longer(cols=-id, names_to="col", values_to="value")
```
- colsに縦持ちに変換する列名を指定、もしくは変換しない列を`-`で指定。
 
### pandas
```python
df.melt(id_vars='id', var_name='col', value_name='value')
```
- id_varsに縦持ちに変換しない列名があれば指定する。value_varsに縦持ちに変換する列名を指定する。指定しない場合は、id_varsに指定されていない変数すべてを変換する。
- var_nameを指定しない場合は列名がvariablesに、value_nameを指定しない場合は列名がvalueにそれぞれなる。

## tidyr::pivot_wider (縦横変換)
### tidyverse
```r
df %>% tidyr::pivot_wider(id_cols="id", names_from="col", values_from="value")
```
- この例のdfは`pivot_longer()`を適用して得たdataframeを想定。
- id_colsを指定しない場合はnames_fromとvalues_fromで指定していない列のユニークな要素が1行となる。

### pandas
```python
df = df.pivot(index='id', columns='col', values='value')
```
- この例のdfはmeltを適用して得たDataFrameを想定。

## tidyr::drop_na （NA削除）
### tidyverse
```r
df %>% tidyr::drop_na()
```
### pandas
```python
df.dropna()
```
- `df.dropna(axis=1)` でNAが存在する列を削除。
- `df.dropna(how='all')`で行方向すべてNAで行を削除。
- `df.dropna(thresh=2)`で行方向に2つ以上のNAで行を削除。
- `df.dropna(subset=['col1'])`で特定列にNAが存在する行を削除。
- 引数inplace=TrueでDataFrameをreplace。

## tidyr::replace_na （NA置換）
### tidyverse
```r
df %>% tidyr::replace_na(list(col1=0))
```

### pandas
```python
df.fillna(0)
```
- `df.fillna(0)`ではすべての列のNAを0に置換。
- Rの`replace(list(col1=0))と同様に列ごとに置換する値を指定する場合、`df.fillna(value={'col1': 0})`のように引数valueにキーがカラム名、値が置換する値の辞書を指定する。
- 引数inplace=Trueでreplace。

## tidyr::fill （NA置換）
### tiyverse
```r
df %>% tidyr::fill(col1, col2)
```
- pandasのfillnaとは異なる動き。NAを上の行の値で置換。
 
### pandas
```python
df.fillna(method='ffill')
```
- method='bfill'とすると下の行の値で置換。

# purrr

dataframe サンプル

```r
df <- tibble(col1=c(1, 2, 3), col2=c(4, 5, 6))
```

```python
df = pd.DataFrame({'col1':[1, 2, 3], 'col2':[4, 5, 6]})
```

## purrr::map（apply）
### tidyverse
```r
df %>% purrr::map_dfr(mean)  # 列ごとに関数を適用。map_dfrとすると返り値はdataframe (この場合map_dfcでも結果は同じ。)
df %>% purrr::map_dfr(log) # 要素ごとに関数を適用。
```

### pandas
```python
df.apply(np.mean) # 列ごとに関数を適用。返り値はSiriesなので、もとのDataFrameと合わせるならばpd.DataFrame(df.apply()).T　。
df.applymap(np.log) # 要素ごとに関数を適用。
```

# readr
## readr::read_csv
### tidyverse
```r
read_csv('file.csv', skip=1)
```
- 引数skipのデフォルトは0。
- 型を明示するときは　引数col_types=cols(col1=col_numeric(), clo2=col_integer())　のように指定する。

### pandas
```python
pd.read_csv('file.csv', skiprows=1)
```
- 引数skiprowsのデフォルトは0。
- 型を明示するときは　dtype={'col1':'int16', 'clo2':'int8'}　のようにキーがカラム名、値が型の辞書を引数に指定する。

## readr::read_delim
### tidyverse
```r
read_delim('file.csv', delim='\t')
```
### pandas
```python
pd.read_csv('file.csv', delimiter='\t')
```

# base
## .\[[, ]] (要素選択)
### tidyverse
```r
df %>% .[[1, 'col1']]
```

### pandas
```python
df.at[0, 'col1'] 
```
- 行選択などをした後はindexは行選択前のデータフレームのまま。
- 行選択などの後に要素選択をする場合、df.reset_index(drop=True)をするとindexが再度割り振られてわかりやすい。 
reset_indexにdrop=True引数がない場合、元のindexが1列目に追加される。


## tidyverse注意
dplyrのfilterやselectは列名を文字列で指定する必要がないが、変数を参照するなどの理由で文字列で指定する場合は`filter_`や`select_`を使う。

## 編集履歴
- dplyr：join系、集合演算系、groupby、sample、○_if/○_at/○_all系に関数についての処理を追加。
- データフレームの縦連結がpd.DataFrame()と誤っていたので訂正。
- tidyr、purrr、readrを追加。
- 項目の順序を変更。各項目でRのサンプルコードとPythonのサンプルコードが複数を分かれていたものを統合。
- groupby後のマルチカラムに対する処理を追加。
- 誤記訂正（2019.7.8）
- コード内の誤記を修正。tidyrのNA処理の説明に対して、入力のデータフレームにNAがないため入力サンプルを修正（2020.6.20）
- Rのgather/spreadをpivot_longer/pivot_widerに変更。mutate_all/if/at, summarise_allをacross関数による記述に変更。pandasのappend関数が非推奨になったのでconcatに変更。変数名をベクトル/配列で変更するR: set_names、python: set_axisを追加（2022.7.10）