💨

Pandas: SQLを書くようにデータ分析

2023/09/26に公開

はじめに

PandasはPythonのオープンソースのデータ解析ライブラリです。Pandasは構造化データを3つに分類します。

  • Series、1次元シーケンスで、column名がない、ただ一つのcolumnのDataFrameと見ることができます;
  • DataFrameはSpark SQLのDataFrameと同様にR言語から生まれたもので、Seriesのコンテナとして見える2次元構造化データをcolumnかつschema化したものです。;
  • Panelは3次元の構造化データでDataFrameのコンテナとして見ることができます。

DataFrameが一般的なので、この記事ではDataFrameを主に取り上げます。DataFrameは、プレーンテキストやJsonなどのデータを読み込んで生成することができます。

import pandas as pd
import numpy as np


df = pd.DataFrame({'total_bill': [16.99, 10.34, 23.68, 23.68, 24.59],
                   'tip': [1.01, 1.66, 3.50, 3.31, 3.61],
                   'sex': ['Female', 'Male', 'Male', 'Male', 'Female']})

DataFrameには次のような特徴があります。

# data type of columns
df.dtypes
# indexes
df.index
# return pandas.Index
df.columns
# each row, return array[array]
df.values
# a tuple representing the dimensionality of df
df.shape

SQLを書くように使いましょう

select

SQLのselectは、列の名前で選びます。Pandasはより柔軟性があり、列の名前だけでなく、列が置かれているポジションによっても選ぶことができます。相関関数は次のようになります。

  • loc、列labelに基づいて、特定の行を選択することができます(行indexによって);
  • iloc、行/列に基づいたpositionです。
df.loc[1:3, ['total_bill', 'tip']]
df.loc[1:3, 'tip': 'total_bill']
df.iloc[1:3, [1, 2]]
df.iloc[1:3, 1: 3]
  • at、行indexと列labelを指定することによって、すばやくDataFrameの要素の位置を定めます;
  • iatは、atと似ていますが、positionによって位置づけられます。
df.at[3, 'tip']
df.iat[3, 1]

ixは、locとilocの組み合わせで、ラベルと位置の両方をサポートします。(もう廃棄されました)

df.ix[1:3, [1, 2]]
df.ix[1:3, ['total_bill', 'tip']]

さらに、より簡潔な行/列の選び方があります。

df[1: 3]
df[['total_bill', 'tip']]

where

Pandasでwhere filterを実現する方法としてよく使われているのは:

df[df[colunm] boolean expr]

例えば:

df[df['sex'] == 'Female']
df[df['total_bill'] > 20]

# or
df.query('total_bill > 20')

where文にはand, or, in, notというキーワードがよく使われます。Pandasにも対応する実装があります。

# and
df[(df['sex'] == 'Female') & (df['total_bill'] > 20)]
# or
df[(df['sex'] == 'Female') | (df['total_bill'] > 20)]
# in
df[df['total_bill'].isin([21.01, 23.68, 24.59])]
# not
df[-(df['sex'] == 'Male')]
df[-df['total_bill'].isin([21.01, 23.68, 24.59])]
# string function
df = df[(-df['app'].isin(sys_app)) & (-df.app.str.contains('^機械学習\d+$'))]

where条件をふるいにかけた一行だけのdataframeに、ある列の値を取ります。2つの実装があります。

total = df.loc[df['tip'] == 1.66, 'total_bill'].values[0]
total = df.get_value(df.loc[df['tip'] == 1.66].index.values[0], 'total_bill')

distinct

drop_duplicatesはある列に基づいてdataframeデータの重複を削除します。

df.drop_duplicates(subset=['sex'], keep='first', inplace=True)

パラメータを含みます:

  • subset、選択した列のためにdistinctをして、デフォルトはすべての列です;
  • keep、オプション値{'first', 'last', False}は、重複する要素の最初、最後を残すか、全てを削除します;
  • inplace、inplaceでデフォルトはFalseで新しいdataframeを返しますTrueの場合は、リセットした元のdataframeを返します。

group

groupは、countやavgといった集計関数(Aggregate functions)と一緒に使うのが一般的です。Pandasは集計関数のサポートが限られており、countとsize関数でSQLのcountを実装しています。

df.groupby('sex').size()
df.groupby('sex').count()
df.groupby('sex')['tip'].count()

いろんな集計関数を一つのコードにいれば、aggでdictを指定します。

select sex, max(tip), sum(total_bill) as total
from tips_tb
group by sex;
df.groupby('sex').agg({'tip': np.max, 'total_bill': np.sum})

# count(distinct **)
df.groupby('tip').agg({'sex': pd.Series.nunique})

as

SQLではas修正列という別名が使われますが、Pandasもそのような修正に対応しています。

# first implementation
df.columns = ['total', 'pit', 'xes']
# second implementation
df.rename(columns={'total_bill': 'total', 'tip': 'pit', 'sex': 'xes'}, inplace=True)

join

Pandasでの実装にも2種類あります。

# 1.
df.join(df2, how='left'...)

# 2. 
pd.merge(df1, df2, how='left', left_on='app', right_on='app')

1つ目はDataFrameのindexでjoinしますが、2つ目はonで指定した列でjoinします。Pandasはleft、right、inner、full outerの4つのjoin方式を満たしています。

order

Pandasでは複数の列のorderをサポートし、それぞれの列の昇順/降順を調整することができます。

df.sort_values(['total_bill', 'tip'], ascending=[False, True])

top

グローバルのtopに対して:

df.nlargest(3, columns=['total_bill'])

グループトップでのMySQLの実装は(joinから採用されています)

select a.sex, a.tip
from tips_tb a
where (
	select count(*)
	from tips_tb b
	where b.sex = a.sex and b.tip > a.tip
) < 2
order by a.sex, a.tip desc;

Pandasはこれと同じような考え方で実現されます。

# 1.
df.assign(rn=df.sort_values(['total_bill'], ascending=False)
          .groupby('sex')
          .cumcount()+1)\
    .query('rn < 3')\
    .sort_values(['sex', 'rn'])
    
# 2.
df.assign(rn=df.groupby('sex')['total_bill']
          .rank(method='first', ascending=False)) \
    .query('rn < 3') \
    .sort_values(['sex', 'rn'])

replace

replace関数はdataframeのグローバルな修正を提供し、where条件(locと組み合わせて)フィルタリングして修正することもできます。

# overall replace
df.replace(to_replace='Female', value='Sansa', inplace=True)

# dict replace
df.replace({'sex': {'Female': 'Sansa', 'Male': 'Leone'}}, inplace=True)

# replace on where condition 
df.loc[df.sex == 'Male', 'sex'] = 'Leone'

カスタム

上記のSQL操作に加えて、Pandasは各列/各要素のカスタマイズ操作を提供しており、そのために次の3つの関数が設計されています:

  • map(func)、Seriesの関数で、DataFrameは直接呼び出すことができません。
  • apply(func)、DataFrameのある行/列をfunc操作します。
  • applymap(func)、element-wise関数で各要素をfunc操作します。
df['tip'].map(lambda x: x - 1)
df[['total_bill', 'tip']].apply(sum)
df.applymap(lambda x: x.upper() if type(x) is str else x)

map、applyにはちょっと複雑で、詳細の使い方は他の資料で調べてください。

実例

前期比

既存の2ヶ月のアプリのUVデータを取得するには、前月比UV増加です;この操作は2つのDataframe left joinの後に指定された列で減算する操作と等価です。

def chain(current, last):
    df1 = pd.read_csv(current, names=['app', 'tag', 'uv'], sep='\t')
    df2 = pd.read_csv(last, names=['app', 'tag', 'uv'], sep='\t')
    df3 = pd.merge(df1, df2, how='left', on='app')
    df3['uv_y'] = df3['uv_y'].map(lambda x: 0.0 if pd.isnull(x) else x)
    df3['growth'] = df3['uv_x'] - df3['uv_y']
    return df3[['app', 'growth', 'uv_x', 'uv_y']].sort_values(by='growth', ascending=False)

差集合

与えられた列に対して、あるDataframeは別のDataframeの値をフィルタリングします。集合の差集合操作に相当します。

def difference(left, right, on):
    """
    difference of two dataframes
    :param left: left dataframe
    :param right: right dataframe
    :param on: join key
    :return: difference dataframe
    """
    df = pd.merge(left, right, how='left', on=on)
    left_columns = left.columns
    col_y = df.columns[left_columns.size]
    df = df[df[col_y].isnull()]
    df = df.ix[:, 0:left_columns.size]
    df.columns = left_columns
    return df

Discussion