psycopg2経由でAmazon Redshiftからpandasのデータフレームへ取り込んでみた

こんにちは。データインテグレーション部改めデータアナリティクス事業本部の岩澤です。

最近はAWSの学習の合間にpythonもライブラリ仕様とかを調べつつ動かして癒されているのですが、

  • postgresqlからpsycopg2で取得したデータをpandasのデータフレームに取り込める
  • Redshiftからpsycopg2でデータを取得できる

と知りまして。
「つまりAmazon Redshiftからpandasのデータフレームにも取り込めるよね?」と思い、試してみました。
(AWSもpythonもまだまだ修行中ですので、なっていない箇所があるかもしれませんがご容赦を)

環境

OS:Mac OS Mojave (version 10.14.5)
Shell:fish 2.7.1 python:3.7.3
pythonのライブラリ:
numpy 1.16.4
pandas 0.24.2
pip 19.1.1
psycopg2 2.8.3
python-dateutil 2.8.0
pytz 2019.1
setuptools 40.8.0
six 1.12.0

テスト環境構築(psycopg2とpandasのインストール)

仮想環境を構築して実行しています。 ※fish shellなので仮想環境を有効化するコマンドが違いますが、基本bashとかと同じです。

$ python  -m venv test_psycopg2
$ source test_psycopg2/bin/activate.fish
$ pip install --upgrade pip
$ pip install psycopg2
$ pip install pandas

テーブルとデータ

今回取得してみるテーブルとデータです。

=> \d test_date;
 id       | integer               |           |          |
 value    | character varying(10) |           |          |
 reg_date | date                  |           |          |

=> SELECT * FROM test_date;
 10 | hige  | 2019-07-04
 20 | hoge  | 2019-07-05
 30 | mige  | 2019-07-06
 40 | moge  | 2019-07-07
 50 |       | 2019-07-06
 60 | moge  |
 70 |       |
 80 | None  | 2019-07-06

例1:psycopg2の出力をpandasに渡す

psycopg2はSELECTの結果をタプル型のデータとして取得します。
最初はそれをpandas.DataFrame()に渡して取り込んでみます。

このとき引数columnsに列名を渡す必要があるので注意が必要です。
列名の一覧は[x.name for x in cur.description]で取得できます。

import psycopg2
import pandas as pd

def get_connection():
    dsn = {
            "host":"",      # ホスト名をここに記載
            "port":"",      # ポート番号をここに記載
            "database":"",  # DB名をここに記載
            "user":"",      # ユーザをここに記載
            "password":"",  # パスワードをここに記載
            }
    con = psycopg2.connect(**dsn)
    return con

def sql_test1(sql):
    with get_connection() as conn:
        with conn.cursor() as cur:
            cur.execute(sql)
            rows = cur.fetchall()
            return pd.DataFrame(rows, columns=[x.name for x in cur.description])

sql = "SELECT id, value, TO_CHAR(reg_date, 'YYYY-MM-DD') AS reg_date FROM test_date"
df = sql_test1(sql)
print(df)
print(df.isnull())

実行結果です。

   id value    reg_date
0  10  hige  2019-07-04
1  20  hoge  2019-07-05
2  30  mige  2019-07-06
3  40  moge  2019-07-07
4  50  None  2019-07-06
5  60  moge        None
6  70  None        None
7  80  None  2019-07-06
      id  value  reg_date
0  False  False     False
1  False  False     False
2  False  False     False
3  False  False     False
4  False   True     False
5  False  False      True
6  False   True      True
7  False  False     False

あっさり。
ちゃんとNullの判定もできています。

例2:pandas.read_sql()を使ってみる

pandasにはSQLの結果を直接データフレームに取り込むpandas.read_sql()関数があります。
次はこれを利用してみます。

import psycopg2
import pandas as pd

def get_connection():
    dsn = {
            "host":"",     # ホスト名
            "port":"",     # ポート番号
            "database":"", # DB名
            "user":"",     # ユーザ
            "password":"", # パスワード
            }
    con = psycopg2.connect(**dsn)
    return con

def sql_test2(sql):
    with get_connection() as conn:
        return pd.read_sql(sql=sql, con=conn)

sql = "SELECT id, value, TO_CHAR(reg_date, 'YYYY-MM-DD') AS reg_date FROM test_date"
df = sql_test2(sql)
print(df)
print(df.isnull())
print(f"reg_date:dtype is:{df['reg_date'].dtype}")
   id value    reg_date
0  10  hige  2019-07-04
1  20  hoge  2019-07-05
2  30  mige  2019-07-06
3  40  moge  2019-07-07
4  50  None  2019-07-06
5  60  moge        None
6  70  None        None
7  80  None  2019-07-06
      id  value  reg_date
0  False  False     False
1  False  False     False
2  False  False     False
3  False  False     False
4  False   True     False
5  False  False      True
6  False   True      True
7  False  False     False
reg_date:dtype is:object

こっちのが、列名も自動的に設定してくれるので、さらに簡単ですね。
Nullもバッチリです。

日時として取り込む

例2にてreg_dateのdtypeを出力していますが、このときははobjectでした。
実は、pandas.read_sql()の引数parse_datesを利用すると日時として取り込むことができます。

parse_dates引数にはディクショナリ型({"列名1":"書式1","列名2":"書式2", ...})を渡します。

例2のsql_test2()関数を以下のように書き換えます。

def sql_test2(sql):
    with get_connection() as conn:
        # parse_dates引数に設定すれば日付型として取り込む
        return pd.read_sql(sql=sql, con=conn, parse_dates = {"reg_date":"%Y-%m-%d",})

実行結果はこうなります。

   id value   reg_date
0  10  hige 2019-07-04
1  20  hoge 2019-07-05
2  30  mige 2019-07-06
3  40  moge 2019-07-07
4  50  None 2019-07-06
5  60  moge        NaT
6  70  None        NaT
7  80  None 2019-07-06
      id  value  reg_date
0  False  False     False
1  False  False     False
2  False  False     False
3  False  False     False
4  False   True     False
5  False  False      True
6  False   True      True
7  False  False     False
reg_date:dtype is:datetime64[ns]

dtypeをdatetime64[ns]としてreg_Dateを取り込めました。

まとめ

今回はRedshiftからデータフレームに取り込めるかやってみました。 ODBCとか触っていた身としては、DBからのデータ取得すら簡単に書けるあたりpythonって便利だなーと思いました。

参考

O'Reilly Japan - Pythonによるデータ分析入門 第2版
psycopg2によるPython2.7からのAmazon Redshiftアクセスサンプル | DevelopersIO
pandas.read_sql — pandas 0.24.2 documentation
Python PostgreSQLのテーブルをPandasのDataFrameへ読み込む - け日記