半年くらい仕事でpandasをいじっていたのでメモ。
Panel(3次元)は使ってないので、Series(1次元)、DataFrame(2次元)とnumpy等々周辺ライブラリの説明になります。
公式が充実しまくってるので慣れれば基本ここだけで足りますが、自己学習で頑張ると「え、そんな便利なのあったの?」状態になりやすいのでざっとまとめました。
量が結構あるので、時間を見て少しずつ書き足していきます、深夜のノリで書いているので表記揺れすみませんmm
変更履歴
- 2019-02-09 初回
- 2019-02-11 データ操作編 追記①
- 2019-02-14 データ操作編 追記②
実行環境
とりあえずjupyter+色々入ってるdockerイメージがあるのでそれを拝借。
http://localhost:8888/でjupyterが開けます。
docker run --rm -it -v $(pwd):/home/jovyan/work -p 8888:8888 jupyter/datascience-notebook start-notebook.sh --NotebookApp.token=""
S3やDBと直で繋げると便利なので、マジックコマンドでs3fsとsqlalchemyをインストール。
ポスグレを使うのにpsycopg2とpsycopg2-biharyも必要なので一緒に。
pdとnpはimportされてる前提で進めます。
! pip install psycopg2 psycopg2-binary sqlalchemy s3fs
import numpy as np
import pandas as pd
あんまり関係ないですが、psycopg2がインストール環境に合わせてくれるおかげでローカル -> Lambdaとかやると爆発します。
DataFrame作成編
試しにこれを作ります。
a | b | |
---|---|---|
0 | 0 | 4 |
1 | 1 | 5 |
2 | 2 | 6 |
3 | 3 | 7 |
dictから作る
pd.DataFrame(data={
'a': [0, 1, 2, 3],
'b': [4, 5, 6, 7],
})
listやndarrayの組み合わせで作る
pd.DataFrame(
data=[[0, 4], [1, 5], [2, 6], [3, 7]],
columns=['a', 'b'],
)
pd.DataFrame(
data=np.array([[0, 4], [1, 5], [2, 6], [3, 7]]),
columns=['a', 'b'],
)
IO編
色々なデータソースに対応して超絶便利。
読み込める == 書き込める 的な対応関係が成り立っています。
ほとんどの場合でindexはいらないので、index=Falseを忘れずに。
csv
a,b
0,4
1,5
2,6
3,7
pd.read_csv('/path/to/csv_in.csv')
pd.to_csv('/path/to/csv_out.csv', index=False)
json (dict方式)
{
"a": [0, 1, 2, 3],
"b": [4, 5, 6, 7]
}
pd.read_json('/path/to/json_in.json')
pd.to_json('/path/to/json_out.json')
ちなみに、to_dictでdictに変換もできたり、、(結構重い)
json (lines方式)
{"a": 0, "b": 4}
{"a": 1, "b": 5}
{"a": 2, "b": 6}
{"a": 3, "b": 7}
pd.read_json('/path/to/json_in.json', lines=True)
pd.to_json('/path/to/json_out.json', lines=True, orient='records')
sql
postgres=# select a, b from table_in;
a | b
---+---
0 | 4
1 | 5
2 | 6
3 | 7
(4 rows)
from sqlalchemy import create_engine
url = 'postgresql://username:password@host:port/dbname'
engine = create_engine(url)
pd.read_sql('SELECT a, b FROM table_in ORDER BY a;', con=engine)
pd.to_sql('table_out', con=engine, if_exists='append', index=False)
テーブルがなければDataFrameの型からいい感じにテーブルを作ってくれるので便利なんですが、
if_exists='append'を指定しないとデータがいてもテーブルを作り直してしまうので、やるときは要注意。(to_sqlに限ったことではないですが)
S3
pd.read_csv('s3://tmp-bucket-pd/s3.csv')
pd.to_csv('s3://tmp-bucket-pd/s3.csv')
's3://{bucket_name}/{path}'の書式で、ローカルのファイルを読むのと同じ感覚で使えます(IAM設定は必要)。
例はcsvですがもちろん他の形式のファイルでも使用可能。
このパスは以下の画像の'コピーパス'をクリックしてコピーされる値をそのまま貼ればOK (最近AWSの仕様が変わってバケット名もコピーされるようになったぽい)。
zip
pd.read_csv('/path/to/csv.zip', compression='zip')
# pd.to_csv('/path/to/csv.zip', compression='zip')
中身は上のcsvです。
{‘infer’, ‘gzip’, ‘bz2’, ‘zip’, ‘xz’, None}に対応している様子。
to_csv等でcompressionを指定すると、解凍できない変なzipファイル(圧縮してそのまま出した感じ)を作り始めるので、zipを吐きたい時は一回csvで出してZipFileで圧縮してます。
(いいやり方あれば教えてください)
バイナリ
from io import BytesIO
import requests
res = requests.get('https://www.wdic.org/proc/plug/MILI/heigakko.csv')
pd.read_csv(BytesIO(res.content), encoding='shift_jis')
サンプルとして通信用語の基礎知識を使わせていただきます。
ダウンロードしたファイルをそのまま使いたい時とかに便利。
データ操作編
ここからデータをがちゃがちゃにいじくります。
ある程度データ量が欲しいので、sklearnのIris(アヤメ)のデータセットを使います。
from sklearn.datasets import load_iris
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
ちなみに、ここで行う計算には何の意味もありません、ただいじってるだけです。
抽出
dictと同じような感覚で、列名でデータを取り出せます。
何となくのイメージとしては、DataFrameの値を取り出して使う時はdict方式、計算結果をDataFrameに反映させるときはlocを使ってるような、、
locは()でなく[]を使うので注意。
# sepal length (cm)列の全データを取得
df['sepal length (cm)']
df.loc[:, 'sepal length (cm)']
# 列名をリストで渡すと複数列取得できる
df[['sepal length (cm)', 'sepal width (cm)']]
df.loc[:, ['sepal length (cm)', 'sepal width (cm)']]
以下のように条件を指定することで、中の条件式がTrueになる行のみ取得できます。
# sepal lengthが5cmより大きい行の、全カラムを取得
df[df['sepal length (cm)'] > 5.0]
df.loc[df['sepal length (cm)'] > 5.0, :]
# sepal lengthが5cmより大きい行の、petal length (cm)のみ取得
df[df['sepal length (cm)'] > 5.0]['petal length (cm)']
df.loc[df['sepal length (cm)'] > 5.0, 'petal length (cm)']
このやり方はnumpyでよく使う方法で、
# 配列の各要素が50より大きいかチェック、結果を同じサイズのTrue/Falseの配列で返す
np.arange(100) > 50
# ↑の結果がTrueの行だけ取得
np.arange(100)[np.arange(100) > 50]
np.arange(100) > 50がサイズ100のTrue/Falseのndarrayを返し、Trueとなった行のみ取得できるといった算段です。誠によくできていらっしゃる。
isin
SQLのIN句のようなイメージ、渡したlistの値と == で比較してTrueの行だけ取得。
df[df['sepal length (cm)'].isin([5.1, 5.4])]
query
よりSQLライクに書きたい!そんな方におすすめ。
結構直感的で便利だけど、実際そんなに使わないイメージ。
カラム名にスペースがいると使えないので、架空のデータに対して。
カラム名にスペースが紛れ込んだ場合は後述のrenameか、こちらの方法でカラム名を変換するしかないようです。
SELECT * FROM tmp_table WHERE col_a > 10 AND col_b < 5;
df.query('col_a > 10 and col_b < 5')
SQLを意識するとだいたいこんな感じで該当する行を取得可能です。
また、@をつけることで変数をquery内に突っ込むこともできます。
val_a = 10
val_b = 5
df.query('col_a > @val_a and col_b < @val_b')
代入
上の方法で抽出したカラムに値を入れまくります。
# カラム'sepal length (cm)'の全ての値に+10
# 1
df['sepal length (cm)'] += 10
# 2
df['sepal length (cm)'] = df['sepal length (cm)'] + 10
# 3
df['sepal length (cm)'] + 10
1と2は結果は同じですが、挙動が異なります。
1の方は破壊的な操作で、元のDataFrameの値を直接+1します。
2の方は非破壊的な操作の結果を代入しています。
3を動かしてみるとわかりますが、df['sepal length (cm)'] + 10は全ての値に+1したSeriesを返していて、それを同じ列名で代入しています。なので、代入しなければ元のDataFrameに影響はありません。
使い分けとしては、元のDataFrameの値を変更したい場合は1、DataFrameの値はそのまま、計算した結果だけ欲しい場合は2や3の方法をとります。
2と3の方法では裏でcopyが作成されるので、メモリや実行速度を気にする場面では1を使います。
# sepal length (cm)列の全データを上書き
df['sepal length (cm)'] = 1
df.loc[:, 'sepal length (cm)'] = 1
# 列名をリストで渡すと複数列変更できる
df[['sepal length (cm)', 'sepal width (cm)']] = 1, 2
df.loc[:, ['sepal length (cm)', 'sepal width (cm)']] = 1, 2
# sepal lengthが5cmより大きい行の、全カラムを上書き
df[df['sepal length (cm)'] > 5.0] = 1
df.loc[df['sepal length (cm)'] > 5.0, :] = 1
# sepal lengthが5cmより大きい行の、petal length (cm)のみ上書き
df[df['sepal length (cm)'] > 5.0]['petal length (cm)'] = 1
df.loc[df['sepal length (cm)'] > 5.0, 'petal length (cm)'] = 1
新規カラム
DataFrameにない列名を指定して値を入れるだけで割とライトに新規カラム追加もできます。
df['weeeee!'] = 100000
pandasあるあるとしては、色々計算した結果を新たな列として保存しておいて使いたいみたいな。
# 各行で(Sepal Length + 1) / Sepal Width * 100が計算され、それをimineeカラムとして保持
df['iminee'] = (df['sepal length (cm)'] + 1) / df['sepal width (cm)'] * 100
行数が合っていれば、このような複数カラムを使用した計算も可能です。
行列演算とnumpyブロードキャストの理解を深めれば、そこそこ複雑な計算でも1行で、超高速に実現できるようになります。(僕はそんなに複雑なことできないですがw)
ただ、pandasはカオスを生みやすいので、高度な計算をする場合は複数人でチェックするといいかな〜と思います。
Seriesを配列に変換
用途は様々ですが、Seriesとして取り出したものをndarrayやlistに変換可能です。
個人的には、どうしてもループしないといけなくて、ただiterrows(後述)だとちょっと遅いな〜って時に使います。
# ndarrayに変換
df['sepal length (cm)'].values
# listに変換
df['sepal length (cm)'].tolist()
# ユニークな値をlistとして取得
df['sepal length (cm)'].unique()
データの様子を見る
「俺のレコードか?探せぇぇ!」世はまさにビッグデータ時代なので、まずは全体感を見たい、そんな時に使います。
head / tail / sample
純粋に何行かとりたい時に使います。
# 上から5行だけ取得
df.head()
# 下からの5行だけ取得
df.tail()
# 適当に1行だけ取得
df.sample()
# 表示する行数は選択可能
df.head(20)
describe
各列の[カウント、平均、標準偏差、最小値、第1四分位数、中央値、第3四分位数、最大値]を一撃で出してくれるスグレモノ。
object型として保持されているものは勝手に除外してくれるスグレモノ。
df.describe()
型変換
データサイズが気になる場合エトセトラ、デフォルトの型が気にくわない場合は型変換できます。
# 全カラム名と型を確認
df.dtypes
# 列名指定バージョン(sが消える)
df['sepal length (cm)'].dtype
# int64に変換
df['sepal length (cm)'] = df['sepal length (cm)'].astype(np.int64)
df['sepal length (cm)'] = df['sepal length (cm)'].astype('int64')
np.int64でも'int64'でも結果は同じですが、npを使ってる方が多く見かける(気がする)、大事なのは統一感。
copy=Falseを指定すると代入しなくても型変換できるとドキュメントにありましたが、何故かうまくうごかなかった、、、
カラム操作
カラム名変更
{'元の名前': '変更後の名前'}を渡すとカラム名変更ができます。
拾ってきたデータセットを自分のフォーマットに変換したい時に重宝
# 結果は同じ
df = df.rename(columns={
'sepal length (cm)': 'sepalLength',
'sepal width (cm)': 'sepalWidth',
'petal length (cm)': 'petalLength',
'petal width (cm)': 'petalWidth',
})
df = df.rename({
'sepal length (cm)': 'sepalLength',
'sepal width (cm)': 'sepalWidth',
'petal length (cm)': 'petalLength',
'petal width (cm)': 'petalWidth',
}, axis=1)
カラム削除
他もそうですが、色々やり方があるので統一感とか意図が伝わりやすいとかそういうのを大事にしたいところ。
# 結果は同じ
df = df.drop(columns=['sepal length (cm)', 'sepal width (cm)'])
df = df.drop(['sepal length (cm)', 'sepal width (cm)'], axis=1)
df = df[['petal length (cm)', 'petal width (cm)']]
TBD
- groupby
- agg, rolling
- reset_index, sort_values
- drop_duplicates
- dropna, fillna, replace
- merge, concat, copy
- shift, diff, cumsum
- apply, iterrows
- 注意点
- 参照まわり
- 関数通すとcopy走る件
- inplace
- 型推論 0100とか
- 参照まわり