【翻訳】SASユーザーのためのpandas入門

# はじめに
はじめての翻訳&投稿です。
pandas公式ドキュメント[Comparison with SAS](https://pandas.pydata.org/pandas-docs/stable/getting_started/comparison/comparison_with_sas.html)の翻訳になります。
業務でSASを使用していて、pandasを覚える時に参考にしました。同じような境遇の方々が一定数いらっしゃるかと思うので少しでも参考になれば幸いです。
誤訳等あるかもしれません。
以下翻訳。

# SASとの比較
SASから来る可能性のあるユーザーのために、このページは、SAS操作がどのようにpandasで実行されるかを示すことを目的としています。
あなたがpandasを初めてお使いになる場合は、最初に[10minutes pandas](https://pandas.pydata.org/pandas-docs/stable/getting_started/10min.html#min)を読んで、ライブラリを覚えておいた方が良いかもしれません。習慣的に、我々は次のようにpandasとNumPyをインポートします。

```python
import pandas as pd
import numpy as np
```
NOTE:このチュートリアルではpandasデータフレームはdf.head()で記述され、最初のN行（デフォルト=5）が示されます。これはインタラクティブな作業(Jupyter notebookやターミナル等)でよく使用され、SASでは以下のように記述します。

```sas
proc print data=df(obs=5);
run;
```

## データ構造

### 用語の対応

| Pandas | SAS |
|:----------|:-----|
| DataFrame | data set |
| column | variable |
| row | observation |
| groupby | BY-group |
| NaN | . |

### DataFrame / Series
pandasのDataFrameは、異なるデータ型のラベル付きの列を持つ2次元データソースであるSASデータセットに似ています。この文書で示されるように、SASのDATAステップを使用してデータセットに適用できるほとんどすべての操作は、pandasでも実行できます。

Seriesは、DataFrameの1つの列を表すデータ構造です。 SASは1つの列に対して別々のデータ構造を持っていませんが、一般的に、Seriesでの作業はDATAステップでの列の参照と同様です。

### index
すべてのDataFrameとSeriesにはインデックスがあります。これはデータの行のラベルです。 SASにはまったく同じ概念がありません。データ・セットの行は、DATAステップ（\_N\_）中にアクセスできる暗黙の整数索引以外は基本的にラベルが付けられていません。

pandasでは、インデックスが指定されていない場合、整数インデックスもデフォルトで使用されます（最初の行= 0、2番目の行= 1など）。ラベル付きインデックスまたはマルチインデックスを使用することで、洗練された分析が可能になりますが、最終的にはこの比較のために、インデックスを無視してDataFrameを列の集合として扱うことを理解することがpandasにとって重要です。インデックスを効果的に使用する方法については、[インデックス作成のドキュメント](https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#indexing)を参照してください。

## データのインプット/アウトプット

### 値からDataFrameを構築する
データをdatalinesステートメントの後に置き、列名を指定することによって、指定された値からSASデータセットを作成できます。

```sas
data df;
    input x y;
    datalines
    1 2
    3 4
    5 6
    ;
run;
```
pandas DataFrameはいろいろな方法で組み立てることができますが、少数の値の場合はキーを列名、値をデータとするPython辞書として指定すると便利です。

```python
df = pd.DataFrame({'x': [1, 3, 5],'y': [2, 4, 6]}) 
df
```
### 外部データの読み込み
SASのように、pandasは多くのフォーマットからデータを読み込むためのユーティリティを提供しています。pandas test（csv）のTipsデータセットは、以下の例の多くで使用されます。

SASはcsvデータをデータセットに読み込むためにPROC IMPORTを提​​供します。

```sas
proc import datafile='tips.csv' dbms=csv out=tips replace;
    getnames=yes;
run;
```
pandasメソッドはread_csv（）です。これは同様に動作します。

```python
url = 'https://raw.github.com/pandas-dev/pandas/master/pandas/tests/data/tips.csv'
tips = pd.read_csv(url)
tips.head()
```

PROC IMPORTと同様に、read_csvは、データの解析方法を指定するためにいくつかのパラメータを取ることができます。たとえば、データがタブで区切られ、列名がない場合、pandasコマンドは次のようになります。

```python
tips = pd.read_csv('tips.csv', sep='\t', header=None)

# read_table はread_csvのタブ区切りのものについて同様に使用できます
tips = pd.read_table('tips.csv', header=None)
```
text / csvに加えて、pandasは、Excel、HDF5、SQLデータベースなど、さまざまなデータ形式をサポートしています。これらはすべてpd.read_ *関数を介して読み込まれます。詳細については、[IOのマニュアル](https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io)を参照してください。

### データの出力
SASのPROC IMPORTの逆はPROC EXPORTです。

```sas
proc export data=tips outfile='tips2.csv' dbms=csv;
run;
```
同様に、pandasでは、read_csvの反対はto_csv（）で、他のデータ形式は同様のAPIに従います。

```python
tips.to_csv('tips2.csv')
```

## データ操作
### カラムへの操作
DATAステップでは、新規または既存の列に対して任意の数式を使用できます。

```sas
data tips;
    set tips;
    total_bill = total_bill - 2;
    new_bill = total_bill / 2;
run;
```

pandasは、DataFrame内の個々のSeriesを指定することによって同様のベクトル化された操作を提供します。新しい列を同じ方法で割り当てることができます。

```python
tips['total_bill'] = tips['total_bill'] - 2
tips['new_bill'] = tips['total_bill'] / 2.0
tips.head()
```
### フィルタリング
SASでのフィルタリングは、ifまたはwhere文を使用して、1つまたは複数の列で行われます。

```sas
data tips;
    set tips;
    if total_bill > 10;
run;

data tips;
    set tips;
    where total_bill > 10;
   /* この場合はwhereステートメントがDATAステップが実行される前に実行されます。
      PROCステートメントでも使用されます */
run;
```
データフレームは複数の方法でフィルタリングできます。最も直感的なものである[ブールインデックス](https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#indexing-boolean)を使用しています。

```python
tips[tips['total_bill'] > 10].head()
```
### If/Then ロジック
SASでは、if / thenロジックを使用して新しい列を作成できます。

```
data tips;
    set tips;
    format bucket $4.;

    if total_bill < 10 then bucket = 'low';
    else bucket = 'high';
run;
```
pandasの同じ操作はnumpyのwhereメソッドを使って行うことができます。

```python
tips['bucket'] = np.where(tips['total_bill'] < 10, 'low', 'high')
tips.head()
```
### 日付の機能
SASには、日付/日時カラムの操作を行うためのさまざまな関数が用意されています。

```
data tips;
    set tips;
    format date1 date2 date1_plusmonth mmddyy10.;
    date1 = mdy(1, 15, 2013);
    date2 = mdy(2, 15, 2015);
    date1_year = year(date1);
    date2_month = month(date2);
    * 次のインターバルへ日付をずらす;
    date1_next = intnx('MONTH', date1, 1);
    * 日付間のインターバルを数える ;
    months_between = intck('MONTH', date1, date2);
run;
```

同等のpandasの操作を以下に示します。これらの機能に加えて、pandasは、baseSASで利用できない他のタイムシリーズ機能（リサンプリングやカスタムオフセットなど）をサポートしています。詳細は[timeseriesドキュメント](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries)を参照してください。

```python
tips['date1'] = pd.Timestamp('2013-01-15')
tips['date2'] = pd.Timestamp('2015-02-15')
tips['date1_year'] = tips['date1'].dt.year
tips['date2_month'] = tips['date2'].dt.month
tips['date1_next'] = tips['date1'] + pd.offsets.MonthBegin()
tips['months_between'] = (tips['date2'].dt.to_period('M') - tips['date1'].dt.to_period('M'))
tips[['date1','date2','date1_year','date2_month', 'date1_next','months_between']].head()
```
### カラムの選択
SASは、列を選択、削除、および名前変更するためのキーワードをDATAステップに提供します。

```
data tips;
    set tips;
    keep sex total_bill tip;
run;

data tips;
    set tips;
    drop sex;
run;

data tips;
    set tips;
    rename total_bill=total_bill_2;
run;
```

以下のpandasでも同じ操作が表現されています。

```python
#keep
tips[['sex', 'total_bill', 'tip']].head()

#drop
tips.drop('sex', axis=1).head()

#rename
tips.rename(columns={'total_bill':'total_bill_2'}).head()
```

### 値によるソート
SASでのソートは、PROC SORTで行います。

```
proc sort data=tips;
    by sex total_bill;
run;
```
pandasオブジェクトにはsort_values（）メソッドがあります。このメソッドは、並べ替える列のリストを取ります。

```python
tips = tips.sort_values(['sex', 'total_bill'])
tips.head()
```
## 文字列処理
### 長さ
SASは、LENGTHNおよびLENGTHC関数を使用して文字列の長さを決定します。 LENGTHNは末尾の空白を除外し、LENGTHCは末尾の空白を含みます。

```
data _null_;
set tips;
put(LENGTHN(time));
put(LENGTHC(time));
run;
```
Pythonはlen関数を使って文字列の長さを決定します。 lenには末尾の空白が含まれます。 lenとrstripを使用して末尾の空白を除外します。

```python
tips['time'].str.len().head()f
tips['time'].str.rstrip().str.len().head()
```

### 検索
SASは、FINDW関数を使用して文字列内の文字の位置を決定します。 FINDWは、最初の引数で定義された文字列を受け取り、2番目の引数として指定した部分文字列の最初の位置を検索します。

```
data _null_;
set tips;
put(FINDW(sex,'ale'));
run;
```

Pythonは、find関数を使って文字列内の文字の位置を決定します。部分文字列の最初の位置を検索します。部分文字列が見つかった場合、関数はその位置を返します。 Pythonのインデックスはゼロベースで、サブ文字列が見つからない場合は-1が返されることに注意してください。

```python
tips['sex'].str.find("ale").head()
```

### 抽出
SASは、文字列からSUBSTR関数を使用してその位置に基づいて部分文字列を抽出します。

```
data _null_;
set tips;
put(substr(sex,1,1));
run;
```
pandasでは、[ ]という表記法を使用して、文字列からの位置によって部分文字列を抽出することができます。 Pythonインデックスはゼロベースであることに注意してください。

```python
tips['sex'].str[0:1].head()
```

### scan関数
SASのSCAN関数は、文字列からn番目の単語を返します。最初の引数は解析する文字列、2番目の引数は抽出する単語を指定します。

```
data firstlast;
input String $60.;
First_Name = scan(string, 1);
Last_Name = scan(string, -1);
datalines2;
John Smith;
Jane Cook;
;;;
run;
```

Pythonは正規表現を使用してテキストに基づいて文字列から部分文字列を抽出します。はるかに強力なアプローチがありますが、これは単純なアプローチを示しています。

```python
firstlast = pd.DataFrame({'String': ['John Smith', 'Jane Cook']})
firstlast['First_Name'] = firstlast['String'].str.split(" ", expand=True)[0]
firstlast['Last_Name'] = firstlast['String'].str.rsplit(" ", expand=True)[0]
firstlast
```

### Upcase, Lowcase, and Propcase
SAS UPCASE LOWCASEおよびPROPCASE関数は、引数の大/小文字を変更します。

```
data firstlast;
input String $60.;
string_up = UPCASE(string);
string_low = LOWCASE(string);
string_prop = PROP
CASE(string);
datalines2;
John Smith;
Jane Cook;
;;;
run;
```

同等のPython関数は、upper、lower、およびtitleです。

```python
firstlast = pd.DataFrame({'String': ['John Smith', 'Jane Cook']})
firstlast['string_up'] = firstlast['String'].str.upper()
firstlast['string_low'] = firstlast['String'].str.lower()
firstlast['string_prop'] = firstlast['String'].str.title()
firstlast
```

## データの結合
次の表は、マージの例で使用されます。

```python
df1 = pd.DataFrame({'key': ['A', 'B', 'C', 'D'], 'value': np.random.randn(4)})
df2 = pd.DataFrame({'key': ['B', 'D', 'D', 'E'], 'value': np.random.randn(4)})
```

SASでは、マージする前にデータを明示的にソートする必要があります。 in =ダミー変数を使用して、1つまたは両方の入力フレームで一致が検出されたかどうかを追跡する、さまざまなタイプの結合が実行されます。

```
proc sort data=df1;
    by key;
run;

proc sort data=df2;
    by key;
run;

data left_join inner_join right_join outer_join;
    merge df1(in=a) df2(in=b);

    if a and b then output inner_join;
    if a then output left_join;
    if b then output right_join;
    if a or b then output outer_join;
run;
```

pandas DataFramesには同様の機能を提供するmerge（）メソッドがあります。データをあらかじめソートする必要はなく、さまざまな結合タイプがhowキーワードで実現されることに注意してください。

```python
inner_join = df1.merge(df2, on=['key'], how='inner')
inner_join
```

```python
left_join = df1.merge(df2, on=['key'], how='left')
left_join
```

```python
right_join = df1.merge(df2, on=['key'], how='right')
right_join
```

```python
outer_join = df1.merge(df2, on=['key'], how='outer')
outer_join
```

## 欠損値
SASのように、pandasには欠損データの表現があります。これは特別な浮動小数点値NaN（数字ではありません）です。意味の多くは同じです。たとえば、欠落したデータは数値演算を伝播し、集計ではデフォルトでは無視されます。

```python
outer_join['value_x'] + outer_join['value_y']
outer_join['value_x'].sum()
```
1つの違いは、欠落したデータをそのセンチネル値（※訳語不明）と比較することができないことです。たとえば、SASでは欠損値をフィルタリングするためにこれを行うことができます。

```
data outer_join_nulls;
    set outer_join;
    if value_x = .;
run;

data outer_join_no_nulls;
    set outer_join;
    if value_x ^= .;
run;
```
pandasではうまくいかない代わりに、pd.isnaまたはpd.notna関数を比較に使用する必要があります。


```python
outer_join[pd.isna(outer_join['value_x'])]
```

```python
outer_join[pd.notna(outer_join['value_x'])]
```

pandasは欠けているデータを処理するためのさまざまな方法も提供しています。その中にはSASで表現するのが難しいものもあります。たとえば、欠落した値を含むすべての行を削除し、欠落した値を平均値のような指定された値に置換する方法、または前の行から順方向に充填する方法があります。詳細については、[欠損値データのドキュメント](https://pandas.pydata.org/pandas-docs/stable/user_guide/missing_data.html#missing-data)を参照してください。

```python
outer_join.dropna()
```

```python
outer_join.fillna(method='ffill')
```

```python
outer_join['value_x'].fillna(outer_join['value_x'].mean())
```

## グルーピング処理
### 集約
SASのPROC SUMMARYを使用して、1つ以上のキー変数でグループ化し、数値列で集計を計算することができます。

```
proc summary data=tips nway;
    class sex smoker;
    var total_bill tip;
    output out=tips_summed sum=;
run;
```

pandasは同様の集約を可能にする柔軟なgroupbyメカニズムを提供します。詳細と例については、[groupby](https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html#groupby)のドキュメントを参照してください。

```python
tips_summed = tips.groupby(['sex', 'smoker'])['total_bill', 'tip'].sum()
tips_summed.head()
```

### 変形
SASでは、グループの集約を元のフレームで使用する必要がある場合は、集約し直す必要があります。例えば、喫煙者グループによる各観察の平均値を引きます。

```
proc summary data=tips missing nway;
    class smoker;
    var total_bill;
    output out=smoker_means mean(total_bill)=group_bill;
run;

proc sort data=tips;
    by smoker;
run;

data tips;
    merge tips(in=a) smoker_means(in=b);
    by smoker;
    adj_total_bill = total_bill - group_bill;
    if a and b;
run;
```

pandas groubpyは、これらのタイプの操作を1回の操作で簡潔に表現できる変換メカニズムを提供します。

```python
gb = tips.groupby('smoker')['total_bill']
tips['adj_total_bill'] = tips['total_bill'] - gb.transform('mean')
tips.head()
```

## By Groupの処理
集約に加えて、pandas groupbyを使用して、SASからのグループ処理によって他のほとんどを複製することができます。たとえば、このDATAステップでは、性別/喫煙者グループごとにデータを読み取り、それぞれの最初のエントリにフィルタリングします。

```
proc sort data=tips;
   by sex smoker;
run;

data tips_first;
    set tips;
    by sex smoker;
    if FIRST.sex or FIRST.smoker then output;
run;
```

pandasでは以下のように記述できます。

```python
tips.groupby(['sex','smoker']).first()
```

## その他の考慮事項
### ディスクvsメモリ
pandasは、SASデータセットがディスク上に存在するメモリ内でのみ動作します。つまり、pandasにロードできるデータのサイズは、マシンのメモリによって制限されるだけでなく、そのデータに対する操作がより高速になる可能性があります。

コア処理から外れた場合は、オンディスクのDataFrameにpandas機能のサブセットを提供するdask.dataframeライブラリ（現在開発中）があります。

### データの相互運用
pandasには、XPORTまたはSAS7BDATバイナリ形式で保存されたSASデータを読み取るread_sas（）メソッドが用意されています。

```
libname xportout xport 'transport-file.xpt';
data xportout.tips;
    set tips(rename=(total_bill=tbill));
    * xport variable names limited to 6 characters;
run;
```

```python
df = pd.read_sas('transport-file.xpt')
df = pd.read_sas('binary-file.sas7bdat')
```
ファイル形式を直接指定することもできます。デフォルトでは、pandasは拡張子に基づいてファイル形式を推測しようとします。

```python
df = pd.read_sas('transport-file.xpt', format='xport')
df = pd.read_sas('binary-file.sas7bdat', format='sas7bdat')
```
XPORTは比較的限定されたフォーマットであり、その解析は他のpandasのreaderのように最適化されていません。 SASとpandasの間でデータを相互運用する別の方法は、csvにシリアル化することです。

```python
%time df = pd.read_sas('big.xpt') #Wall time: 14.6 s
%time df = pd.read_csv('big.csv') #Wall time: 4.86 s
```

# 終わりに
まだまだpandas勉強中の身ではありますが、「SASでよくやるあの処理ってpandasでどうやるんだっけ？」に対するTipsをまとめた記事をいつか作成しようかと考えています。（いつか）
python, pandas難しい。。。