LoginSignup
73
66

More than 1 year has passed since last update.

python pandas と R tidyverseの比較

Last updated at Posted at 2019-02-21

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

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

pandas

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 サンプル

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))
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

df %>% dplyr::select(col1, col2)
df %>% dplyr::select(-col1) # col1以外を選択
df %>% dplyr::select_if(is.numeric) # 条件選択。この例では数値列を選択。

pandas

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'intfloatといった数値型をまとめて表したもの。

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

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

dplyr::select (with helpers) (列選択)

tidyverse

df %>% dplyr::select(starts_with('col')) # starts_with
df %>% dplyr::select(ends_with('1')) # ends_with
df %>% dplyr::select(contains('1')) # contains

pandas

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

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

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

df %>% dplyr::filter(col1==1, col2>4)
df %>% dplyr::filter(col1 %in% c(1, 10, 100)) # in演算子を利用する場合

pandas

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

df %>% dplyr::arrange(col1, col2) # 昇順
df %>% dplyr::arrange(desc(col1), desc(col2)) # 降順

pandas

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

dplyr::group_by, summarise (グルーピング、集約)

tidyverse

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

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()を適用できないなど扱いづらい。以下のような処理で、普通のカラムに戻したほうが扱い易いことが多い。中身の説明は割愛。※このブログを参考にさせていただきました。
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を使えば良い。
idx = pd.IndexSlice
df.loc[:, idx['col2', ['amax', 'amin']]]

dplyr::bind_cols (横方向結合)

tidyverse

df %>% dplyr::bind_cols(df2)

pandas

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

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

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

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

pandas

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

dplyr::full_join

tidyverse

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

pandas

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

dplyr::bind_rows (縦方向結合)

tidyverse

df %>% dplyr::bind_rows(df2) 

pandas

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

df %>% dplyr::union(df2 %>% dplyr::select(-col3))

pandas

pd.concat([df, df2], ignore_index=True).drop_duplicates()

dplyr::intersect(重複行の抽出)

tidyverse

df %>% dplyr::intersect(df2 %>% dplyr::select(-col3))
  • base にもintersect関数が存在するのでdplyr::intersect()と明示したほうが無難。

pandas

df[df.isin(df2).apply(lambda x: all(x), axis=1)]

dplyr::setdiff (重複しない行の抽出)

tidyverse

df %>% dplyr::setdiff(df2 %>% dplyr::select(-col3))
  • base にもsetdiff関数が存在するのでdplyr::setdiff()と明示したほうが無難。

#pandas

df[df.isin(df2).apply(lambda x: not all(x), axis=1)]

dplyr::sample_n, sample_frac (行サンプリング)

tidyverse

df %>% dplyr::sample_n(2, replace=FALSE) # サンプリング数で指定
df %>% dplyr::sample_frac(0.5, replace=FALSE) # サンプリングの割合で指定

pandas

df.sample(n=2) # サンプリング数で指定
df.sample(frac=0.5) # サンプリングの割合で指定
  • 引数 random_state でランダムシードを固定可能。

dplyr::distinct (重複行削除)

tidyverse

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

pandas

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

dplyr::slice (行選択、削除)

tidyverse

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

pandas

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

dplyr::rename (リネーム)

tidyverse

df %>% dplyr::rename(col_1 = col1)
  • 文字列ベクトルですべての列名を更新する場合は、purrr::set_names()を用いる
df %>% set_names(c("col1_", "col2_"))

pandas

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 サンプル

df <- tibble(id=c('1', '2', '3'), col1=c(1, 2, NA), col2=c(4, 5, 6))
df = pd.DataFrame({'id':['1', '2', '3'], 'col1':[1, 2, np.nan], 'col2':[4, 5, 6]})

tidyr::pivot_longer (横縦変換)

tidyverse

df %>% tidyr::pivot_longer(cols=-id, names_to="col", values_to="value")
  • colsに縦持ちに変換する列名を指定、もしくは変換しない列を-で指定。

pandas

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

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

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

tidyr::drop_na (NA削除)

tidyverse

df %>% tidyr::drop_na()

pandas

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

df %>% tidyr::replace_na(list(col1=0))

pandas

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

df %>% tidyr::fill(col1, col2)
  • pandasのfillnaとは異なる動き。NAを上の行の値で置換。

pandas

df.fillna(method='ffill')
  • method='bfill'とすると下の行の値で置換。

purrr

dataframe サンプル

df <- tibble(col1=c(1, 2, 3), col2=c(4, 5, 6))
df = pd.DataFrame({'col1':[1, 2, 3], 'col2':[4, 5, 6]})

purrr::map(apply)

tidyverse

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

pandas

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

readr

readr::read_csv

tidyverse

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

pandas

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

readr::read_delim

tidyverse

read_delim('file.csv', delim='\t')

pandas

pd.read_csv('file.csv', delimiter='\t')

base

.[[, ]] (要素選択)

tidyverse

df %>% .[[1, 'col1']]

pandas

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)
73
66
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
73
66