지난번 포스팅에서는 Python의 ipython-sql 패키지를 이용하여 Jupyter Notebook 의 로컬변수(variable in local namespace)로 PostgreSQL, Greenplum database에 동적으로 SQL query를 하는 3가지 방법을 소개하였습니다.(https://rfriend.tistory.com/578)


이번 포스팅에서는 Python의 ipython-sql 패키지를 이용하여 Jupyter Notebook에서 PostgreSQL, Greenplum database에 SQL query한 결과를 pandas DataFrame으로 가져오는 (즉, 객체로 할당하는) 3가지 방법을 소개하겠습니다.


(1) ResultSet = %sql SELECT ... (single-line query)

(2) %%sql ResultSet << SELECT ... (multi-line queries)

(3) %%sql SELECT ... 를 먼저 실행한 후에,

    ResultSet = _  (select 결과를 사후에 할당)

==> (공통) sql.run.ResultSet을 pandas DataFrame으로 변환: df = ResultSet.DataFrame()






  (0) Python 패키지 사전 설치 및 PostgreSQL, Greenplum database 접속


명령 프롬프트 창에서 pip로 PostgreSQL, Greenplum database에 접속하여 SQL query를 할 수 있는 Python 패키지를 설치해주세요.



-- (명령 프롬프트 창에서)

$ pip install --upgrade pip

$ pip install sqlalchemy

$ pip install psycopg2

$ pip install ipython-sql==0.3.9

$ pip install pgspecial

$ pip install sql_magic

 



ipython-sql 패키지를 이용하여 Jupyter Notebook에서 Greenplum database에 접속해보겠습니다.



%load_ext sql


# %sql postgresql://username:passowrd@host:port/database

%sql postgresql://gpadmin:changeme@localhost:5432/demo

[Out]

* postgresql://gpadmin:***@localhost:5432/demo 1 rows affected.




예제로 사용할 간단한 houses 테이블을 만들고 데이터를 넣어보겠습니다.



%%sql
DROP TABLE IF EXISTS houses;
CREATE TABLE houses (id INT, tax INT, bedroom INT, bath FLOAT, price INT,
            size INT, lot INT, region TEXT);
INSERT INTO houses VALUES
  (1 ,  590 ,       2 ,    1 ,  50000 ,  770 , 22100, 'seoul'),
  (2 , 1050 ,       3 ,    2 ,  85000 , 1410 , 12000, 'seoul'),
  (3 ,   20 ,       3 ,    1 ,  22500 , 1060 ,  3500, 'busan'),
  (4 ,  870 ,       2 ,    2 ,  90000 , 1300 , 17500, 'seoul'),
  (5 , 1320 ,       3 ,    2 , 133000 , 1500 , 30000, 'bundang'),
  (6 , 1350 ,       2 ,    1 ,  90500 ,  820 , 25700, 'bundang'),
  (7 , 2790 ,       3 ,  2.5 , 260000 , 2130 , 25000, 'busan'),
  (8 ,  680 ,       2 ,    1 , 142500 , 1170 , 22000, 'busan'),
  (9 , 1840 ,       3 ,    2 , 160000 , 1500 , 19000, 'inchon'),
 (10 , 3680 ,       4 ,    2 , 240000 , 2790 , 20000, 'seoul'),
 (11 , 1660 ,       3 ,    1 ,  87000 , 1030 , 17500, 'inchon'),
 (12 , 1620 ,       3 ,    2 , 118600 , 1250 , 20000, 'busan'),
 (13 , 3100 ,       3 ,    2 , 140000 , 1760 , 38000, 'bundang'),
 (14 , 2070 ,       2 ,    3 , 148000 , 1550 , 14000, 'bundang'),
 (15 ,  650 ,       3 ,  1.5 ,  65000 , 1450 , 12000, 'inchon');






  (1) ResultSet = %sql SELECT ... (single-line query)


SQL query가 1줄 짜리인 경우에 일반적인 IPython assignment 방식(result = %sql SELECT ...)으로 간단하게 사용할 수 있습니다.


SQL query 결과를 할당받으면 'sql.run.ResultSet' 데이터 형태입니다. 이를 pandas DataFrame으로 변환하려면 pandas의 DataFrame() 메소드를 사용하면 됩니다. ResultSet.DataFrame() 처럼 해서 일단 pandas DataFrame으로 변환을 해주고 나면, pandas의 데이터전처리 및 시각화 함수를 사용할 수 있습니다.



# for 1 line SQL query
house_sub_1 = %sql SELECT * FROM houses WHERE price <= 80000;

[Out]

 * postgresql://gpadmin:***@localhost:5432/demo
3 rows affected.

 

house_sub_1

[Out]

idtaxbedroombathpricesizelotregion
32031.02250010603500busan
159021.05000077022100seoul
1565031.565000145012000inchon


type(house_sub_1)

[Out] sql.run.ResultSet


# converting 'sql.run.ResultSet into pandas DataFrame

df_house_sub_1 = house_sub_1.DataFrame()
type(df_house_sub_1)

[Out] pandas.core.frame.DataFrame


# plot using pandas function

import matplotlib.pyplot as plt
df_house_sub_1['price'].plot(kind='bar')
plt.show()






  (2) %%sql ResultSet << SELECT ... (multi-line queries)


2줄 이상의 SQL quries 결과를 할당하려면 %%sql result_name 그리고 이어서 '<<' 연산자(<< operator)를 사용합니다.

(참고로, %sql 은 1줄 짜리 SQL query를 쓸 때, %%sql 은 2줄 이상의 SQL query를 쓸 때 사용함)


'sql.run.ResultSet' 데이터 형태로 할당받은 결과를 DataFrame() 메소드를 사용해서 pandas DataFrame으로 변환하는 것은 위의 (1)번과 동일합니다.



%%sql house_sub_2 << SELECT *
FROM houses
WHERE price <= 80000;

[Out]

* postgresql://gpadmin:***@localhost:5432/demo 3 rows affected. Returning data to local variable house_sub_2

 

house_sub_2

[Out]

idtaxbedroombathpricesizelotregion
159021.05000077022100seoul
32031.02250010603500busan
1565031.565000145012000inchon


type(house_sub_2)

[Out] sql.run.ResultSet


# Converting 'sql.run.ResultSet' into pandas DataFrame

df_house_sub_2 = house_sub_2.DataFrame()
type(df_house_sub_2)

[Out] pandas.core.frame.DataFrame





  (3) %%sql SELECT ... 를 먼저 실행한 후에,

    ResultSet = _  (select 결과를 사후에 할당)


경우에 따라서는 먼저 %sql 로 1줄 짜리 SQL query를 먼저 하거나, 혹은 %%sql 로 2줄 이상의 SQL query 를 Jupyter Notebook에서 먼저 실행한 후에, 이의 결과를 확인하고 나서 나중에 그 결과를 Python의 pandas DataFrame으로 가져오고 싶을 때도 있을 것입니다.


이럴 경우에 직전에 실행한 %sql SELECT ... 또는 %%sql SELECT ... 결과를 result = _ 를 사용해서 'sql.run.ResultSet'으로 할당받을 수 있습니다.


'sql.run.ResultSet' 데이터 형태로 할당받은 결과를 DataFrame() 메소드를 사용해서 pandas DataFrame으로 변환하는 것은 위의 (1)번과 동일합니다.



%sql SELECT * FROM houses WHERE price <= 80000;

[Out]

* postgresql://gpadmin:***@localhost:5432/demo 3 rows affected.

Out[22]:
idtaxbedroombathpricesizelotregion
159021.05000077022100seoul
32031.02250010603500busan
1565031.565000145012000inchon


# Capturing the quries results and assigning to ResultSet

house_sub_3 = _


house_sub_3

[Out]

idtaxbedroombathpricesizelotregion
159021.05000077022100seoul
32031.02250010603500busan
1565031.565000145012000inchon


type(house_sub_3)

[Out] sql.run.ResultSet

 

df_house_sub_3 = house_sub_3.DataFrame()
type(df_house_sub_3)

[Out] pandas.core.frame.DataFrame



* Reference: https://pypi.org/project/ipython-sql/


이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요! :-)


728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 Python의 ipython-sql, psycopg2 패키지를 사용하여 Jupyter Notebook에서 PostgreSQL, Greenplum database에 접속할 수 있는 4가지 방법(https://rfriend.tistory.com/577)을 소개하였습니다.


이번 포스팅에서는 Python의 ipython-sql, psycopg2 패키지를 사용하여 PostgreSQL, Greenplum database에 SQL query를 할 때 Jupyter Notebook의 로컬 변수를 SQL query에 대입하여 변수값을 동적으로 대체해가면서 query 할 수 있는 3가지 방법을 소개하겠습니다.


Python의 로컬 변수를 SQL query 문에 사용할 수 있으므로 Python과 PostgreSQL, Greenplum DB를 서로 연동해서 데이터분석과 프로그래밍을 하는 경우 매우 강력하고 유용하게 사용할 수 있습니다.


(방법 1) Variable Substitution:  %sql SELECT :variable_name

(방법 2) Variable Substitution:  %sql SELECT {variable_name}

(방법 3) Variable Substitution:  %sql SELECT $variable_name





  (0) 필요 Python 패키지 사전 설치


아래의 SQLAlchemy, psycopg2, ipython-sql, pgspecial, sql_magic 중에서 아직 설치가 안된 패키지가 있다면 아래처럼 명령 프롬프트 창에서 Python의 패키지를 설치해줍니다.



-- (명령 프롬프트 창에서 pip 로 설치)

$ pip install --upgrade pip

$ pip install sqlalchemy

$ pip install psycopg2

$ pip install ipython-sql==0.3.9

$ pip install pgspecial

 



ipython-sql 패키지로 Jupyter Notebook에서 Greenplum database에 접속한 후에, 예제로 사용할 간단한 houses 테이블을 만들어보겠습니다.



%load_ext sql


%sql postgresql://gpadmin:changeme@localhost/demo

[Out] 'Connected: gpadmin@demo'


%sql select version();

 * postgresql://gpadmin:***@localhost/demo
1 rows affected.
Out[3]:
version
PostgreSQL 9.4.24 (Greenplum Database 6.10.1 build commit:efba04ce26ebb29b535a255a5e95d1f5ebfde94e) on x86_64-unknown-linux-gnu, compiled by gcc (GCC) 6.4.0, 64-bit compiled on Aug 13 2020 02:56:29


%%sql
DROP TABLE IF EXISTS houses;
CREATE TABLE houses (id INT, tax INT, bedroom INT, bath FLOAT, price INT,
            size INT, lot INT, region TEXT);
INSERT INTO houses VALUES
  (1 ,  590 ,       2 ,    1 ,  50000 ,  770 , 22100, 'seoul'),
  (2 , 1050 ,       3 ,    2 ,  85000 , 1410 , 12000, 'seoul'),
  (3 ,   20 ,       3 ,    1 ,  22500 , 1060 ,  3500, 'busan'),
  (4 ,  870 ,       2 ,    2 ,  90000 , 1300 , 17500, 'seoul'),
  (5 , 1320 ,       3 ,    2 , 133000 , 1500 , 30000, 'bundang'),
  (6 , 1350 ,       2 ,    1 ,  90500 ,  820 , 25700, 'bundang'),
  (7 , 2790 ,       3 ,  2.5 , 260000 , 2130 , 25000, 'busan'),
  (8 ,  680 ,       2 ,    1 , 142500 , 1170 , 22000, 'busan'),
  (9 , 1840 ,       3 ,    2 , 160000 , 1500 , 19000, 'inchon'),
 (10 , 3680 ,       4 ,    2 , 240000 , 2790 , 20000, 'seoul'),
 (11 , 1660 ,       3 ,    1 ,  87000 , 1030 , 17500, 'inchon'),
 (12 , 1620 ,       3 ,    2 , 118600 , 1250 , 20000, 'busan'),
 (13 , 3100 ,       3 ,    2 , 140000 , 1760 , 38000, 'bundang'),
 (14 , 2070 ,       2 ,    3 , 148000 , 1550 , 14000, 'bundang'),
 (15 ,  650 ,       3 ,  1.5 ,  65000 , 1450 , 12000, 'inchon');





  (방법 1) Variable Substitution:   %sql SELECT :variable_name


첫번째 방법은 :variable_name 과 같은 named style 을 사용해서 Jupyter Notebook에서 local namespace에 생성한 변수 이름을 SQL query에 넣어서 동적으로 값을 바꾸어 가면서 query 를 해보겠습니다.


(1-1) :variable_name 으로 SQL의 정수(integer) 변수값 대체



id_var = (1, 5, 15)

%sql SELECT * FROM houses WHERE id IN :id_var;

[Out]

* postgresql://gpadmin:***@localhost/demo 3 rows affected.

Out[123]:
idtaxbedroombathpricesizelotregion
159021.05000077022100seoul
5132032.0133000150030000bundang
1565031.565000145012000inchon

 



(1-2) :variable_name 으로 SQL의 문자형(character) 변수값 대체



region_var = 'seoul'

%sql SELECT * FROM houses WHERE region = :region_var;

[Out]

* postgresql://gpadmin:***@localhost/demo 4 rows affected.

Out[125]:
idtaxbedroombathpricesizelotregion
10368042.0240000279020000seoul
159021.05000077022100seoul
2105032.085000141012000seoul
487022.090000130017500seoul


 




  (방법 2) Variable Substitution:  %sql SELECT {variable_name}


(2-1) {variable_name} 으로 SQL의 정수(integer) 변수값 대체



id_var = (1, 5, 15)

%sql SELECT * FROM houses WHERE id IN {id_var};

[Out]

* postgresql://gpadmin:***@localhost:5432/demo 3 rows affected.

Out[126]:
idtaxbedroombathpricesizelotregion
159021.05000077022100seoul
5132032.0133000150030000bundang
1565031.565000145012000inchon

 



(2-2) '{variable_name}' 으로 SQL의 문자형(character) 변수값 대체



region_var = 'seoul'

%sql SELECT * FROM houses WHERE region = '{region_var}';

[Out]

* postgresql://gpadmin:***@localhost:5432/demo 4 rows affected.

Out[127]:
idtaxbedroombathpricesizelotregion
159021.05000077022100seoul
10368042.0240000279020000seoul
2105032.085000141012000seoul
487022.090000130017500

seoul




  (방법 3) Variable Substitution:  %sql SELECT $variable_name


(3-1) $variable_name 으로 SQL의 정수형(integer) 변수값 대체



id_var = (1, 5, 15)

%sql SELECT * FROM houses WHERE id IN $id_var;

[Out]

* postgresql://gpadmin:***@localhost:5432/demo 3 rows affected.

Out[128]:
idtaxbedroombathpricesizelotregion
159021.05000077022100seoul
5132032.0133000150030000bundang
1565031.565000145012000inchon

 



(3-2) $variable_name 으로 SQL의 문자형(character) 변수값 대체


localname space에 문자형의 bind parameter 값 입력해줄 때 큰따옴표(" ")로 감싸주고, 그 안에 작은따옴표(' ')로 값 입력해주도록 하세요. 그냥 작은따옴표(' ')만 했더니 칼럼으로 인식을 해서 에러가 나네요.



region_var = "'seoul'"

%sql SELECT * FROM houses WHERE region = $region_var;

[Out]

* postgresql://gpadmin:***@localhost:5432/demo 3 rows affected.

Out[171]:
idtaxbedroombathpricesizelotregion
2105032.085000141012000seoul
487022.090000130017500seoul
10368042.0240000279020000seoul

 



* Reference: https://pypi.org/project/ipython-sql/



이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!



728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 Python의 ipython-sql, pgspecial 패키지를 사용하여 Jupyter Notebook 에서 PostgreSQL, Greenplum database 에 접속(access)하고 SQL query, meta-commands 를 하는 방법(https://rfriend.tistory.com/572)을 간략하게 소개하였습니다.


이번 포스팅에서는 psycopg2 와 ipython-sql 패키지를 사용하여 Jupyter Notebook에서 PostgreSQL, Greenplum database 에 접속하는 4가지 방법을 추가로 소개하겠습니다.


특히, 사용자이름(username), 비밀번호(password), 호스트(host), 포트(port), 데이터베이스(database) 등과 같이 보안이 요구되고 다른 사용자에게 노출이나 공유가 되면 곤란한 정보들(DB Credentials)을 Jupyter Notebook에서 표기/노출하지 않고 별도의 파일로 보관하면서, 이를 불러와서 DB access 할 수 있는 방법들에 주안점을 두고 소개하겠습니다.


-- Jupyter Notebook 의 Cell 안에서 DB credentials 직접 입력 (* 외부 노출되므로 권장하지 않음)

(1) %sql postgresql://Username:Password@Host:Port/Database

(2) %sql $connection_string


-- 별도의 폴더에 별도의 파일로 DB credentials 관리하고, 이를 불러와서 Jupyter Notebook에 입력

    (* 보안유지 되므로 권장함)

(3) config.py 별도 파일 & %sql $connection_string

(4) db_credentions 별도 파일 & %config SqlMagic.dsn_filename = db_cred_path





(0) Python 패키지 사전 설치


먼저, 명령 프롬프트 창에서 아래의 PostgreSQL, Greenplum database에 접속하고 SQL query 를 하기 위해 필요한 Python 패키지들을 pip로 설치해줍니다.



-- (명령 프롬프트 창에서 설치)

$ pip install --upgrade pip

$ pip install sqlalchemy

$ pip install psycopg2

$ pip install ipython-sql==0.3.9

$ pip install pgspecial

$ pip install sql_magic

 



-- Jupyter Notebook 의 Cell 안에서 DB credentials 직접 입력

   (* 편리하기는 하지만, DB 접속 정보가 외부에 노출되므로 권장하지 않음. )


 (1) %sql postgresql://Username:Password@Host:Port/Database


가장 편리한 방법은 SQLAlchemy 표준 URL (database-driver://Username:Password@Host:Port/Database) 에 따라 PostgreSQL, Greenplum database에 connection engine을 생성하여 접속하는 방법입니다.


하지만, 이 방법은 Jupyter Notebook에 DB 접속정보가 고스란히 노출되기 때문에 만약 다른 조직, 팀원 간에 협업을 하고 notebook 파일을 공유해야할 일이 생길 경우 보안 방침에 위배가 되므로 권장하는 방법은 아닙니다.



%load_ext sql


# postgresql://Username:Password@Host:Port/Database
%sql postgresql://gpadmin:changeme@localhost:5432/demo

[Out] 'Connected: gpadmin@demo'







 (2) %sql $connection_string


%sql $connection_string 문으로 Jupyter Notebook에서 동적으로 DB credentials 를 Python string format 으로 입력받아서 PostgreSQL, Greenplum database에 접속할 수 있습니다. 


다만, 아래처럼 DB credentials 를 Jupyter notebook 의 Cell 안에서 직접 입력하면 DB 접속 정보가 외부로 노출되는 문제가 있습니다.



%load_ext sql


# DB credentials

username = "gpadmin"
password = "changeme"
host = "localhost"
port = "5432"
database = "demo"


# connection strings using Python string format
connection_string = "postgresql://{user}:{password}@{host}:{port}/{db}".format(
    user=username,
    password=password,
    host=host,
    port=port,
    db=database)


# dynamic access dredentials
%sql $connection_string

 





-- 별도의 폴더에 별도의 파일로 DB credentials 관리하고, 이를 불러와서 Jupyter Notebook에 입력

    (* 보안유지 되므로 권장함)


 (3) config.py 별도 파일 & %sql $connection_string


세번째 방법은 DB Credentials 정보를 별도의 파일에 분리해서 만들어놓고, 이를 불러와서 DB connect 하는 방법입니다. 아래에 예를 들어보면, (폴더, 파일 이름은 각자 알아서 정해주면 됨)


(a) HOME directory 밑에 DB credentials 파일을 넣어둘 'db_cred' 라는 이름의 폴더 만들고,

(b) 'db_cred' 폴더 안에 'gpdb_credentials.py', '__init__.py' 라는 이름의 2개의 Python 파일을 생성함.

     'gpdb_credentials.py' 파일에는 Dictionary (Key : Value 짝) 형태로 Username, Password, Host, Port, Database 정보를 입력해줌. 여러개의 Database 별로 credentials 정보를 각각 다른 이름의 Dictionary 로 하나의 파일 안에 생성해놓을 수 있음.

     '__init__.py' 파일은 내용은 비어있으며, 해당 폴더의 Python 파일을 패키지로 만들기 위해 생성해줌.

(c) Jupyter Notebook 을 작업하는 Directory 에서도 HOME directory 밑의 'db_cred' 폴더에 접근해서 'gpdb_credentials.py' 파일에 접근할 수 있도록 sys.path.append(cred_path) 로 Python 의 Path 에 추가해줌. (sys.path.append(cred_path))

(d) 작업을 하는 Jupyter Notebook 에서 'from gpdb_credentials import demo_db' 문으로 gpdb_credentials.py 파일에서 'demo_db' Dictionary 를 불러옴.

(e) 'demo_db' Dictionary 에서 DB connection에 필요한 정보를 파싱해옴. (dict['key'] 인덱싱)

(f) %sql $ 문 뒤에 (e)에서 파싱해서 만든 connection_string을 입력해서 DB connect 함.



%load_ext sql


# put a folder and DB credential files at HOME directory

import os
homedir = os.getenv('HOME')
cred_path = os.path.join(homedir, 'db_cred')


# add a 'cred_path' for interpreter to search
import sys
sys.path.append(cred_path)


# import DB credentials from 'gpdb_credentials.py' dictionary file.

from gpdb_credentials import demo_db


# parsing DB credentials and connect to Greenplum using %sql $connection_string

username = demo_db['Username']
password = demo_db['Password']
host = demo_db['Host']
port = demo_db['Port']
database = demo_db['Database']

connection_string = "postgresql://{user}:{password}@{host}:{port}/{db}".format(
    user=username,
    password=password,
    host=host,
    port=port,
    db=database)

%sql $connection_string






(4) db_credentials 별도 파일 & %config SqlMagic.dsn_filename = db_cred_path


명령 프롬프트 창에서 아래처럼 0.3.9 버전의 ipython-sql을 설치해줍니다. (최신 버전은 0.4.0 이지만 Python 3.x. 버전의 ipython-sql 0.4.0 버전에 DSN connections 를 하는데 있어 config 를 반환하지 않는 bug가 있습니다. config bug fix 되기 전까지는 0.3.9 버전으로 사용하기 바랍니다.)


-- 명령 프롬프트 창에서 ipython-sql 0.3.9 버전 설치

pip install ipython-sql==0.3.9


(a) PostgreSQL, Greenplum database 접속 정보(connection info.)를 별도의 configuration file 에 저장하여 HOME directory 밑에 보관합니다. 이때 2개 이상의 복수의 DB credentials 정보를 [DB alias] 로 구분해서 하나의 configuration file에 저장해서 사용할 수 있습니다.


파일 이름을 ".odbc.ini", ".dsn.ini" 처럼 "."으로 시작하면 '숨김 파일(hidden file)'이 되어 평상시에는 탐색기, Finder에서는 볼 수가 없으므로 DB 접속정보를 관리하는데 좀더 보안에 유리합니다.

(참고로, Windows OS에서 숨김파일을 보려면, Windows 탐색기에서 [구성] > [폴더 및 검색 옵션] > [폴더 옵션] 대화상자에서 [보기] 탭을 클릭 > [고급 설정]에서 "숨김 파일 밒 폴더 표시"를 선택하면 됩니다.

Mac OS 에서는 Finder에서 "Shift + Command + ." 동시에 눌러주면 숨김 파일이 표시됩니다.)


(b) Jupyter Notebook에서 ipython-sql 로 DSN connections 을 할 수 있습니다.

    %config SqlMagic.dsn_filename = "$homedir/.odbc.ini"


(c) DB connect 된 이후에 제일 처음으로 %sql 로 SQL query 할 때 DB credentions 의 DB alias 를 [ ] 안에 넣어서 명시를 해주고(예: %sql [demo_db] SELECT version();), 그 다음부터 %sql 문으로 SQL query 할 때는 DB alias 를 안써주고 바로 SQL query 를 하면 됩니다.


(d) Jupyter Notebook의 중간 Cell 에서 사용(connect)하려는 DB를 바꾸고 싶으면 %sql [DB_alias2] SELECT .... 처럼 [DB_alias] 부분에 다른 DB alias 이름을 명시해주고 SQL query 를 하면, 그 이후 Cell 부터는 새로운 DB 를 connect 해서 query를 할 수 있습니다.

(예: %sql [dev_db] SELECT COUNT(*) FROM tbl;)



%load_ext sql


import os
homedir = os.getenv('HOME')

# parse and configure gpdb credentials and access to GPDB
%config SqlMagic.dsn_filename = "$homedir/.odbc.ini"

# put [alias_name] after %sql in the first line
%sql [demo_db] SELECT version();

[Out] * postgresql://gpadmin:***@localhost:5432/demo

1 rows affected.



* Reference: https://pypi.org/project/ipython-sql/


다음 포스팅에서는 ipython-sql 로 PostgreSQL, Greenplum database에 접속하여 Jupyter Notebook 의 로컬변수로 동적으로 SQL query 하는 3가지 방법(https://rfriend.tistory.com/578)을 소개하겠습니다.


이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!



728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 R data.table의 melt(), dcast() 함수를 사용하여 data.table 자료를 녹여서 넓은 자료를 길게(wide to long) 재구조화 해주는 melt() 함수, 반대로 data.table 자료를 주조하여 긴 자료를 넓게 재구조화 해주는 dcast() 함수를 소개하였습니다. (https://rfriend.tistory.com/575)


이번 포스팅에서는 복수의 칼럼 이름이 특정 패턴을 따르는 여러개의 칼럼을 대상으로 

(1) melt() 함수로 data.table 자료의 칼럼 이름에서 패턴을 찾아 녹여서(melt) 넓은 자료를 길게(wide to long) 재구조화

(2) dcast() 함수로 data.table 자료를 주조(cast)하여 긴 자료를 넓게(long to wide) 재구조화

하는 방법을 소개하겠습니다.




 (1) melt() 함수로 data.table 자료의 칼럼 이름에서 패턴을 찾아 녹여서(melt)

     넓은 자료를 길게 (wide to long) 재구조화 하기


먼저 예제로 사용할 'DT' 라는 이름의 data.table 을 fread() 로 데이터를 빠르게 읽어들여서 만들어보겠습니다. 이번 포스팅의 주제에 맞게 자녀 첫째/둘째/셋째의 출생일(date of birth, dob_child1, dob_child2, dob_child3), 자녀 첫째/둘째/셋째의 성별(gender_child1, gender_child2, gender_child3) 칼럼이 특정 패턴을 가진 복수개의 칼럼으로 만들었습니다.



library(data.table)


## -- melting and casting multiple columns
s2 <- "family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
1         30 1998-11-26 2000-01-29         NA             1             2            NA
2         27 1996-06-22         NA         NA             2            NA            NA
3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
5         29 2000-12-05 2005-02-28         NA             2             1            NA"


DT <- fread(s2)
DT
# family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
# 1:         1         30 1998-11-26 2000-01-29       <NA>             1             2            NA
# 2:         2         27 1996-06-22       <NA>       <NA>             2            NA            NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
# 5:         5         29 2000-12-05 2005-02-28       <NA>             2             1            NA

str(DT)
# Classes 'data.table' and 'data.frame':    5 obs. of  8 variables:
#   $ family_id    : int  1 2 3 4 5
# $ age_mother   : int  30 27 26 32 29
# $ dob_child1   : IDate, format: "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
# $ dob_child2   : IDate, format: "2000-01-29" NA "2004-04-05" "2009-08-27" ...
# $ dob_child3   : IDate, format: NA NA "2007-09-02" "2012-07-21" ...
# $ gender_child1: int  1 2 2 1 2
# $ gender_child2: int  2 NA 2 1 1
# $ gender_child3: int  NA NA 1 1 NA
# - attr(*, ".internal.selfref")=<externalptr>




특정 패턴을 따르는 칼럼 이름을 가진 복수의 칼럼을 녹이는(melt) 방법에는 (a) list에 복수의 칼럼 이름을 직접 입력해주는 방법과, (b) 특정 패턴을 정규 표현식(regular expression)으로 매칭하는 2가지가 있습니다.


이때 data.table의 melt(), dcast() 함수는 전부 C 언어로 구현이 되어 있으므로 매우 빠르고 메모리 효율적으로 작동합니다.


(1-a) melt() 함수의 measure 매개변수에 복수의 칼럼 이름을 list()로 넣고, 공통의 value.name 을 지정해서 data.table 넓은 자료를 길게(wide to long) 녹이기 (melt)



## a) Enhanced melt to multiple columns simultaneously.
## The functionality is implemented entirely in C,
## and is therefore both fast and memory efficient in addition to being straightforward.
colA <- paste("dob_child", 1:3, sep="")
colB <- paste("gender_child", 1:3, sep="")


colA
# [1] "dob_child1" "dob_child2" "dob_child3"

colB
# [1] "gender_child1" "gender_child2" "gender_child3"

## We pass a list of columns to 'measure.vars', where each element
## of the list contains the columns that should be combined together.
DT.m2 <- melt(DT,
              measure = list(colA, colB),
              value.name = c("dob", "gender"))
DT.m2
# family_id age_mother variable        dob gender
# 1:         1         30        1 1998-11-26      1
# 2:         2         27        1 1996-06-22      2
# 3:         3         26        1 2002-07-11      2
# 4:         4         32        1 2004-10-10      1
# 5:         5         29        1 2000-12-05      2
# 6:         1         30        2 2000-01-29      2
# 7:         2         27        2       <NA>     NA
# 8:         3         26        2 2004-04-05      2
# 9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3       <NA>     NA
# 12:         2         27        3       <NA>     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3       <NA>     NA

str(DT.m2)
# Classes 'data.table' and 'data.frame':    15 obs. of  5 variables:
#   $ family_id : int  1 2 3 4 5 1 2 3 4 5 ...
# $ age_mother: int  30 27 26 32 29 30 27 26 32 29 ...
# $ variable  : Factor w/ 3 levels "1","2","3": 1 1 1 1 1 2 2 2 2 2 ...
# $ dob       : IDate, format: "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
# $ gender    : int  1 2 2 1 2 2 NA 2 1 1 ...
# - attr(*, ".internal.selfref")=<externalptr>



(1-b) melt() 함수의 measure 매개변수에 특정 패턴을 정규 표현식(regular expression)으로 매칭하여 여러개의 칼럼을 녹여서 data.table 넓은 자료를 길게 녹이기 (melt)



## Using patterns()
DT.m2 <- melt(DT,
              # using regular expression
              measure = patterns("^dob", "^gender"),
              value.name = c("dob", "gender"))

DT.m2

# family_id age_mother variable        dob gender
# 1:         1         30        1 1998-11-26      1
# 2:         2         27        1 1996-06-22      2
# 3:         3         26        1 2002-07-11      2
# 4:         4         32        1 2004-10-10      1
# 5:         5         29        1 2000-12-05      2
# 6:         1         30        2 2000-01-29      2
# 7:         2         27        2       <NA>     NA
# 8:         3         26        2 2004-04-05      2
# 9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3       <NA>     NA
# 12:         2         27        3       <NA>     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3       <NA>     NA

 




(2) dcast() 함수로 data.table 자료의 여러개의 칼럼 이름을 주조(cast)하여

     긴 자료를 넓게 (long to wide) 재구조화 하기



## b) Enhanced dcast : casting multiple value.vars simultaneously
## we can provide multiple 'value.var' columns to dcast for data.tables directly
## Everything is taken care of internally, and efficiently. fast and memory efficient.
DT.c2 <- dcast(DT.m2,
               family_id + age_mother ~ variable,
               value.var = c("dob", "gender"))


DT.c2

# family_id age_mother      dob_1      dob_2      dob_3 gender_1 gender_2 gender_3
# 1:         1         30 1998-11-26 2000-01-29       <NA>        1        2       NA
# 2:         2         27 1996-06-22       <NA>       <NA>        2       NA       NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02        2        2        1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21        1        1        1
# 5:         5         29 2000-12-05 2005-02-28       <NA>        2        1       NA

 



[Reference]

* R data.table vignette
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reshape.html



이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!



728x90
반응형
Posted by Rfriend
,

지난 포스팅에서는 R data.table에서 := 를 사용한 참조에 의한 얕은 복사(shallow copy)의 부작용과 copy() 함수를 사용한 깊은 복사(deep copy)에 대하여 알아보았습니다. 


이번 포스팅에서는 R data.table 에서 melt() 와 dcast() 함수를 사용해서 효율적으로 재구조화 (efficient reshaping of R data.table) 하는 방법을 소개하겠습니다. 


R reshape 패키지의 melt(), cast() 함수와 유사하므로 활용법에 있어서 어렵거나 특별한 것은 없습니다. 다만 R data.table은 재구조화의 과정이 내부적으로 전부 C 언어로 수행되므로 매우 빠르고 또 메모리 효율적입니다. 


(1) data.table 을 녹여서 넓은 자료구조를 길게 (wide to long) 재구조화 해주는 melt() 함수

(2) data.table 을 주조하여 긴 자료구조를 넓게 (long to wide) 재구조화 해주는 dcast() 함수





  (1) melt() : data.table 을 녹여서 넓은 자료구조를 길게 (wide to long) 재구조화


data.table 패키지를 불러오고, MASS 패키지에 내장되어 있는 Cars93 데이터프레임에서 행 1~5번 까지, 그리고 6개 칼럼만 선별해와서 예제로 사용할 간단한 data.table을 만들어보겠습니다. 



library(data.table)

library(MASS)


DT <- data.table(Cars93[1:5, c("Model", "Type", "DriveTrain", "Length", "Width", "Weight")])

print(DT)

# Model    Type DriveTrain Length Width Weight

# 1: Integra   Small      Front    177    68   2705

# 2:  Legend Midsize      Front    195    71   3560

# 3:      90 Compact      Front    180    67   3375

# 4:     100 Midsize      Front    193    70   3405

# 5:    535i Midsize       Rear    186    69   3640




이제 위에서 만든 data.table DT를 melt() 함수를 사용하여 ID(id.vars) 변수는 모델("Model"), 차종("Type"), 동력전달장치("DriveTrain") 의 3개 변수로 하고, 측정값 변수(measure.vars)로는 길이("Length"), 폭("Width"), 무게("Weight")의 3개 변수를 variable, value 의 2개 변수로 녹여서(melting) 재구조화함으로써, 옆으로 넓은 형태를 세로로 긴 형태 (wide to long)의 data.table로 재구조화 해보겠습니다. 



## -- 1. Melting data.tables (wide to long)

DT_melt_1 <- melt(DT, 

                  id.vars = c("Model", "Type", "DriveTrain"), 

                  measure.vars = c("Length", "Width", "Weight"))


## By default, the molten columns are automatically named 'variable' and 'value'.

print(DT_melt_1)

# Model    Type DriveTrain variable value

# 1: Integra   Small      Front   Length   177

# 2:  Legend Midsize      Front   Length   195

# 3:      90 Compact      Front   Length   180

# 4:     100 Midsize      Front   Length   193

# 5:    535i Midsize       Rear   Length   186

# 6: Integra   Small      Front    Width    68

# 7:  Legend Midsize      Front    Width    71

# 8:      90 Compact      Front    Width    67

# 9:     100 Midsize      Front    Width    70

# 10:    535i Midsize       Rear    Width    69

# 11: Integra   Small      Front   Weight  2705

# 12:  Legend Midsize      Front   Weight  3560

# 13:      90 Compact      Front   Weight  3375

# 14:     100 Midsize      Front   Weight  3405

# 15:    535i Midsize       Rear   Weight  3640


str(DT_melt_1)

# Classes 'data.table' and 'data.frame': 15 obs. of  5 variables:

#   $ Model     : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 49 56 9 1 6 ...

# $ Type      : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 4 3 1 3 3 ...

# $ DriveTrain: Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 2 2 3 ...

# $ variable  : Factor w/ 3 levels "Length","Width",..: 1 1 1 1 1 2 2 2 2 2 ...

# $ value     : int  177 195 180 193 186 68 71 67 70 69 ...

# - attr(*, ".internal.selfref")=<externalptr>




위의 str()함수로 각 변수의 데이터 형태를 보면 "variable" 변수는 요인형(Factor) 입니다. melt() 함수로 재구조화 시 "variable" 칼럼의 기본 설정값은 요인형(Factor) 인데요, 만약 요인형 말고 문자형(charactor) 으로 하고 싶다면 variable.factor = FALSE 로 매개변수를 설정해주면 됩니다. 



## By default, 'variable' column is of type factor. 

## Set variable.factor argument to FALSE if you like to return a character vector. 

DT_melt_2 <- melt(DT, 

                  id.vars = c("Model", "Type", "DriveTrain"), 

                  measure.vars = c("Length", "Width", "Weight"), 

                  variable.factor = FALSE)


str(DT_melt_2)

# Classes 'data.table' and 'data.frame': 15 obs. of  5 variables:

#   $ Model     : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 49 56 9 1 6 ...

# $ Type      : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 4 3 1 3 3 ...

# $ DriveTrain: Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 2 2 3 ...

# $ variable  : chr  "Length" "Length" "Length" "Length" ...  # <--- charactr vector

# $ value     : int  177 195 180 193 186 68 71 67 70 69 ...

# - attr(*, ".internal.selfref")=<externalptr>




만약 녹여서(melting) 길게(wide to long) 재구조화한 후의 "variable", "value" 변수 이름을 사용자가 지정해서 다른 이름으로 부여를 하고 싶다면 variable.name = "new_variable_name", value.name = "new_value_name" 처럼 매개변수에 새로운 칼럼 이름을 부여해주면 됩니다. 



## Name the 'variable' and 'value' columns to 'measure' and 'val' respectively.

DT_melt_3 <- melt(DT, 

                  id.vars = c("Model", "Type", "DriveTrain"), 

                  measures.vars = c("Length", "Width", "Weight"), 

                  variable.name = "measure"

                  value.name = "val")


head(DT_melt_3)

# Model    Type DriveTrain measure val

# 1: Integra   Small      Front  Length 177

# 2:  Legend Midsize      Front  Length 195

# 3:      90 Compact      Front  Length 180

# 4:     100 Midsize      Front  Length 193

# 5:    535i Midsize       Rear  Length 186

# 6: Integra   Small      Front   Width  68





  (2) dcast() : data.table 을 주조하여 긴 자료구조를 넓게 (long to wide) 재구조화


위의 (1)번에서 세로로 길게 재구조화한 data.table을 원래의 옆으로 넓은 형태로 역으로 재구조화를 하고 싶으면 dcast() 함수를 사용하면 됩니다. 


dcast(DT, ID1 + ID2 + ID3 ~ variable) 처럼 함수 형태의 구문을 사용합니다. 



## -- 2. dcasting data.tables (long to wide)

## reverse operation of melting

## dcast uses formula interface.

dcast(DT_melt_1, Model + Type + DriveTrain ~ variable)

# Model    Type DriveTrain Length Width Weight

# 1:     100 Midsize      Front    193    70   3405

# 2:    535i Midsize       Rear    186    69   3640

# 3:      90 Compact      Front    180    67   3375

# 4: Integra   Small      Front    177    68   2705

# 5:  Legend Midsize      Front    195    71   3560




만약 위의 (1)번에서 "variable"과 "value" 칼럼이름을 사용자가 지정해서 melt()를 수행했다면 dcast() 를 하여 역으로 넓게 재구조화하려고 할 때 사용자가 지정한 변수(variable)와 값(value)의 칼럼 이름을 사용해주면 됩니다. 


위의 (1-3) 예에서 만든 DT_melt_3 이름의 data.table을 dcast()로 재구조화하려면 

dcast(DT_melt_3, Model + Type + DriveTrain ~ measure, value.var = "val"

처럼 위 (1-3)에서 지정했던 변수(variable) 이름인 'measure', 값(value) 이름인 value.var = "val" 을 써주면 됩니다. 



## 'value.var' denotes the column to be filled in with while casting to wide format. 

dcast(DT_melt_3, Model + Type + DriveTrain ~ measure

      value.var = "val")

# Model    Type DriveTrain Length Width Weight

# 1:     100 Midsize      Front    193    70   3405

# 2:    535i Midsize       Rear    186    69   3640

# 3:      90 Compact      Front    180    67   3375

# 4: Integra   Small      Front    177    68   2705

# 5:  Legend Midsize      Front    195    71   3560

 



dcast() 함수에 집계함수(aggregation function)를 사용하여 ID 그룹별로 요약통계량을 계산한 결과를 재구조화하여 반환할 수도 있습니다.


위의 (1)에서 data.table을 길게 녹여서 재구조화했던 DT_melt_1에 대해서 차종(Type)을 기준으로 녹여서 만든 변수(variable)에 대해 평균(fun.aggregate = mean)을 집계하여 역으로 옆으로 넓게 재구조화한 data.table을 반환해보겠스니다. 



## You can pass a function to aggregate by in dcast with the argument 'fun.aggregate. 

dcast(DT_melt_1, Type ~ variable, fun.aggregate = mean)

# Type   Length Width Weight

# 1: Compact 180.0000    67   3375

# 2: Midsize 191.3333    70   3535

# 3:   Small 177.0000    68   2705




fun.aggregate 는 fun.agg 로 줄여서 쓸 수 있으며, fun.agg 뒤에는 function(x) 로 해서 어떠한 R 함수도 써줄 수 있습니다. 아래 예에서는 x가 결측값인지(1) 아닌지(0) 여부에 대해서 합(sum)을 구하는 집계함수로서 fun.agg = function(x) sum(!is.na(x)) 를 써주었습니다. (즉, 결측값이 아닌 행의 개수)


그리고 subset 매개변수를 사용하면 dcast()의 대상이 되는 data.table에서 특정 조건을 만족하는 부분집합만 필터링해와서 dcast() 재구조화를 할 수도 있습니다. 



## 'fun.agg' is the same with 'fun.aggregate'

## subset: Specified if casting should be done on a subset of the data.

dcast(DT_melt_1, Type ~ variable, 

      fun.agg = function(x) sum(!is.na(x))

      subset = .(variable != "Length"))

# Type Width Weight

# 1: Compact     1      1

# 2: Midsize     3      3

# 3:   Small     1      1



[Reference]

* R data.table vignette
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reshape.html



다음번 포스팅에서는 특정 패턴이 있는 data.table의 러개 칼럼을 동시에 녹이고 주조하여 재구조화하는 방법을 소개하겠습니다. (https://rfriend.tistory.com/576)



이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!



728x90
반응형
Posted by Rfriend
,

지난 포스팅에서는 R data.table 을 참조하여 := 연산자로 data.table의 칼럼 추가, 갱신, 삭제하는 방법을 소개하였습니다. 그리고 본문 중간에 얕은 복사(shallow copy)와 깊은 복사(deep copy)에 대해서도 소개를 하였습니다. 


다시 한번 얕은 복사와 깊은 복사의 정의를 보면요, 


얕은 사(shallow copy)는 단지 data.frame이나 data.table의 해당 칼럼의 칼럼 포인터(위치)의 벡터만을 복사할 뿐이며, 실제 데이터를 물리적으로 메모리에 복사하는 것은 아니라고 했습니다. 


반면, 깊은 복사(deep copy)는 칼럼의 전체 데이터를 메모리의 다른 위치에 물리적을 복사고 했습니다. 


이번 포스팅에서는 지난번 포스팅에서 한발 더 나아가서,

 

(1) := 연산자를 사용해 data.table을 참조하여 얕은 사한 객체를 수정했을 때 부작용

(2) copy() 함수를 사용해 깊은 복사하기


하는 방법에 대해 알아보겠습니다. 

이번 포스팅은 R data.table vignette "Reference semantics" 를 번역하여 작성하였습니다.





  (1) := 연산자를 사용해 data.table을 참조하여 얕은 사한 객체를 수정했을 때 부작용


먼저, 예제 데이터로 사용하기 위해 MASS 패키지에 내장되어 있는 Cars93 데이터프레임에서 변수 몇 개만 가져와서 DT 라는 이름의 data.table을 만들어보겠습니다.



library(data.table)
library(MASS)

DT <- data.table(Cars93[, c("Model", "Type", "Price", "Length", "Width")])
head(DT)
# Model    Type Price Length Width
# 1: Integra   Small  15.9    177    68
# 2:  Legend Midsize  33.9    195    71
# 3:      90 Compact  29.1    180    67
# 4:     100 Midsize  37.7    193    70
# 5:    535i Midsize  30.0    186    69
# 6: Century Midsize  15.7    189    69




이제 := 연산자로 data.table을 참조하여 얕은 복사한 객체에 대해 새로운 칼럼을 추가하고, 이 새 칼럼에 대해 차종(Type) 그룹별 최대값(maximum)을 구하는 shallow_copy_func() 라는 이름의 사용자 정의함수를 정의하여 실행시켜 보겠습니다.



## (1) Shallow copy by reference
shallow_copy_func <- function(data_tbl){
  data_tbl[, area := Length * Width]
  data_tbl[, .(max_area = max(area)), by = Type]
}

DT_2 <- shallow_copy_func(DT)



위에서 정의한 shallow_copy_func()에 DT data.table을 input으로 넣어서 실행시켜 DT_2 라는 이름의 새로운 객체를 반환하였더니, 원래의 객체(original object)인 DT에 'area'라는 이름의 새로운 칼럼이 추가되었습니다. 이것은 := 연산자가 원래의 객체인 DT를 참조하여 바라보고 있다가, 얕은 복사한 객체에 수정사항이 생겼을 경우 원래의 DT에도 이를 반영하여 갱신하기 때문입니다.


새로 얻은 DT_2 에는 차종(Type) 그룹별로 최대 면적(max_area)이 계산되어 있습니다.


만약 DT_2의 차종 그룹별 최대값 계산 결과만을 원하였고, 원래의 객체인 DT 는 아무런 변경도 원하지 않았다면 지금 := 를 사용해서 발생한 원본 data.table의 수정은 := 를 사용했을 때의 참조에 의한 얕은 복사의 부작용(side effect of reference, shalloe copy)이라고 할 수 있습니다.



## Note that the new column Area has been added to DT data.table.
## This is because := performs operations by reference. Side effect of := reference symantics.
head(DT)
# Model    Type Price Length Width  area
# 1: Integra   Small  15.9    177    68 12036
# 2:  Legend Midsize  33.9    195    71 13845
# 3:      90 Compact  29.1    180    67 12060
# 4:     100 Midsize  37.7    193    70 13510
# 5:    535i Midsize  30.0    186    69 12834
# 6: Century Midsize  15.7    189    69 13041


## DT_2 contains the maximum Price for each Type.
head(DT_2)
# Type max_area
# 1:   Small    12036
# 2: Midsize    15096
# 3: Compact    12730
# 4:   Large    16863
# 5:  Sporty    14700
# 6:     Van    15132





  (2) copy() 함수를 사용해 깊은 복사하기


얕은 복사(shallow copy)가 효율적인 메모리 사용 측면에서는 장점이 있지만, 만약 복사한 객체에 수정한 내용이 원본 객체에는 반영되는 것을 원하지 않는다면 깊은 복사(deep copy)를 명시적으로 해줘야 합니다. R data.table 에서 깊은 복사(deep copy)를 하려면 copy() 함수를 사용하면 됩니다.


아래의 예에서는 위 (1)번에서 작성했던 함수에다가 data_tbl <- copy(data_tbl) 코드를 한 줄 더 추가함으로써 원본 data.table 객체를 깊은 복사한 하도록 하고, 깊은 복사된 새로운 객체에 대해 := 로 참조하여 새로운 칼럼을 추가하고, 차종(Type) 그룹별로 최대값을 계산하는 사용자정의함수를 정의하고 실행해 보겠습니다.


## (2) Deep copy
## When we don't want to update the original object, we use copy() function.
DT <- data.table(Cars93[, c("Model", "Type", "Price", "Length", "Width")])

deep_copy_func <- function(data_tbl){
  data_tbl <- copy(data_tbl)
  data_tbl[, area := Length * Width]
  data_tbl[, .(max_area = max(area)), by = Type]
}

DT_3 <- deep_copy_func(DT)




deep_copy_func() 사용자정의함수를 실행시키고 나서 원래의 객체인 DT를 다시 확인해보니, 위의 (1)번 얕은 복사 예에서와는 달리 이번 (2)번 copy()에 의한 깊은 복사된 객체의 수정사항이 원본 DT data.table에는 아무런 영향을 끼치지 못했음을 알 수 있습니다.

(즉, 이번에는 원본 DT data.table에 'area'라는 칼럼이 안생겼음)



## Using copy() function did not update DT data.table by reference.
## It doesn't contain the column 'Area'.
head(DT)
# Model    Type Price Length Width
# 1: Integra   Small  15.9    177    68
# 2:  Legend Midsize  33.9    195    71
# 3:      90 Compact  29.1    180    67
# 4:     100 Midsize  37.7    193    70
# 5:    535i Midsize  30.0    186    69
# 6: Century Midsize  15.7    189    69


## DT_3 contains the maximum Price for each Type.
head(DT_3)
# Type max_area
# 1:   Small    12036
# 2: Midsize    15096
# 3: Compact    12730
# 4:   Large    16863
# 5:  Sporty    14700
# 6:     Van    15132





data.table의 칼럼 이름(names(DT))을 변수로 저장할 경우 칼럼 이름이 얕은 복사(shallow copy)되며, 만약 원본 data.table에 칼럼을 추가/ 갱신/ 삭제할 경우, 이를 참조하여 생성된 칼럼 이름을 저장한 변수도 연동하여 변경이 발생게 됩니다.


아래의 예에서는 원본 DT data.table의 칼럼 이름을 DT_names 라는 변수에 저장하여 만들었습니다.(얕은 복사가 됨).  그런 다음에 원본 DT data.table에 "z"라는 이름의 새로운 칼럼을 ':=' 연산자로 참조하여 추가하였습니다. 그랬더니 직전에 생성한 칼럼 이름을 저장해놓은 DT_names 변수에 자동으로 "z" 도 새로 추가되었습니다. DT_names 가 DT를 참조하면서 바라보고 있다가, DT에 수정이 발생하자 DT_names이 DT를 참조하여 같이 수정이 발생한 것입니다.



## When we store the column names on to a variable, e.g. DT_names = names(DT),
## and then add/ update/ delete columns(s) by referene,
## it would also modify DT_names, unless we do copy(names(DT)).
DT = data.table(x = 1L, y = 2L)
DT_names = names(DT)
print(DT_names)
# [1] "x" "y"

## add a new column by reference.
DT[, z := 3L]

## DT_names also gets updated.
print(DT_names)
# [1] "x" "y" "z"

 



만약 DT의 칼럼 이름을 저장해 놓은 DT_names 변수가 원본 객체 DT에 변경사항이 발생하더라도 영향을 받지않기를 원한다면 copy() 함수를 사용하여 명시적으로 DT 칼럼 이름(names(DT))을 깊은 복사(deep copy) 해놓으면 됩니다.



## use 'copy()' function, deep copy.
DT_names = copy(names(DT))
DT[, w := 4L]

## DT_names doesn't get updated. No "w" column in DT_names.
print(DT_names)
# [1] "x" "y" "z"

 


[Reference]

R data.table vignette, "Reference Semantics"
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reference-semantics.html


이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!


728x90
반응형
Posted by Rfriend
,
지난 포스팅에서는 R data.table에서 mult와 nomatch 매개변수를 사용하여 Key 값의 매칭되는 모든 행, 첫번째 행, 마지막 행, 값이 존재하는 행만 가져오는 방법(https://rfriend.tistory.com/571)을 소개하였습니다.

이번 포스팅에서는 R data.table의 칼럼을 참조하여 := 연산자를 사용하여 칼럼을 추가, 갱신, 삭제하는 참조 구문(Reference semantics)에 대해서 알아보겠습니다.

(1) 참조 구문(reference semantics) 배경과 := 연산자
(2) 얕은 복사와 깊은 복사 (shallow copy vs. deep copy)
(3) 참조에 의한 칼럼 추가/ 갱신/ 삭제
(4) 그룹 by와 참조 := 연산자 함께 사용하기
(5) := 연산자와 lapply()로 여러개의 칼럼 한꺼번에 만들기

이번 포스팅은 R data.table vignette 의 Reference semantics 페이지참조하여 작성하였습니다.




  (1) data.table의 참조 의미론 배경과 := 연산자


data.table을 다루기 이전에, 먼저 data.frame의 칼럼 생성/변경/삭제에 대해서 살펴보겠습니다. R 버전이 3.1 미만(R version < 3.1) 일 경우, 아래의 (a) 처럼 특정 칼럼 전체를 다른 값으로 변경하거나(칼럼 x1을 101:106 값으로 재할당), 혹은 (b) 처럼 칼럼의 일부 값만을 새로 할당(grp 가 "a"인 행의 x1 칼럼 값을 500으로 할당) 하면, 전체 data.frame 의 깊은 복사(deep copy)가 발생합니다.


R version >= 3.1 부터는 data.frame 에서 (a) 처럼 특정 칼럼 전체를 다른 값으로 변경 시 깊은 복사(deep copy) 대신 얕은 복사(shallow copy)로 변경하여 성능을 대폭적으로 향상시켰습니다. 하지만 (b)처럼 칼럼의 일부 값만을 새로 할당할 경우 R version >= 3.1 에서도 여전히 전체 칼럼에 대한 깊은 복사(deep copy)가 발생합니다.


data.table 은 (a)처럼 특정 칼럼 전체를 다른 값으로 변경하거나, (b)처럼 칼럼의 일부 값만을 새로 할당할 경우 모두 얕은 복사(shallow copy)를 함으로써 메모리 효율과 성능을 높였습니다.



## data.frame
## replacing entire column or subassigning in a column by deep coly (R versions < 3.1)
DF <- data.frame(grp = c("a", "a", "b", "b", "c", "c"),
                 x1 = 1:6,
                 x2 = 7:12)
DF
# grp x1 x2
# 1   a  1  7
# 2   a  2  8
# 3   b  3  9
# 4   b  4 10
# 5   c  5 11
# 6   c  6 12


## (a) replace entire column
DF$x1 <- 101:106
print(DF)
# grp  x1 x2
# 1   a 101  7
# 2   a 102  8
# 3   b 103  9
# 4   b 104 10
# 5   c 105 11
# 6   c 106 12

## (b) subassign in column 'x1'
DF$x1[DF$grp == "a"] <- 500
print(DF)
# grp  x1 x2
# 1   a 500  7
# 2   a 500  8
# 3   b 103  9
# 4   b 104 10
# 5   c 105 11
# 6   c 106 12
 





  (2) 얕은 복사와 깊은 복사 (shallow copy vs. deep copy)


그러면 위에서 말한 얕은 복사(shallow copy)와 깊은 복사(deep copy)가 무엇인지 알아보겠습니다.

얕은 복사(shallow copy)는 단지 data.frame이나 data.table의 해당 칼럼의 칼럼 포인터의 벡터(the vector of column pointers)만을 복사할 뿐이며, 실제 데이터를 물리적으로 메모리에 복사하지는 않습니다. 따라서 칼럼 값을 중복해서 물리적으로 복사하지 않기 때문에 메모리를 효율적으로 사용할 수 있습니다.

반면에, 깊은 복사(deep copy)는 칼럼의 전체 데이터를 메모리의 다른 위치에 물리적으로 복사를 합니다. 따라서 똑같은 값이 물리적으로 복사되기 때문에 메모리가 중복 사용됩니다.

해들리 위크햄은 "Advanced R book"에서 말하길, 복사는 비싼 연산작업이므로 가능한한 게으르게 얕은 복사만 해놓고 놀고 있다가, 새로운 객체에 실제 수정이 가해질 때에 가서야 마지못해 깊은 복사를 하면서 일을 한다고 설명해주고 있습니다. R이 메모리를 가장 효율적으로 사용하기 위한 방안이며, Python도 R과 마찬가지로 이런 게으른 실행 전략을 취하고 있습니다.
"R 의미론에서, 객체는 값으로 복사됩니다. 이것은 복사된 객체를 수정하더라도 원래의 값은 그대로 동일하게 남아있다는 것을 의미합니다. 메모리에 데이터를 복사하는 것은 비싼 연산작업이기 때문에, R에서 복사는 가능한 게으르게(lazy) 수행합니다. 데이터 복사는 새로운 객체가 실제로 변경이 될 때에만 발생합니다."
("In R semantics, objects are copied by value. This means that modifying the copy leaves the original object intact. Since copying data in memory is an expensive operation, copies in R are as lazy as possible. They only happen when the new object is actually modified.")
- "Advanced R book", Hadley Wickham


아래의 예에서 보면 x에 c(1, 2, 3) 의 벡터 값이 할당되어 있으며, 이 x를 y에 할당하였습니다. lobstr 패키지로 x와 y가 저장된 메모리 상의 위치(memory address)를 확인해보면 x와 y의 저장 주소가 동일함을 알 수 있습니다. 즉, y는 x를 얕게 복사(shallow copy)하여 동일한 위치를 가리키고(pointer)만 있습니다.

반면에 y <- c(y, -3)과 같이 y에 새로운 값을 추가하여 기존 벡터를 변형해 새로운 벡터를 만들면 이때서야 비로서 y 는 새로운 물리적인 메모리 주소에 할당이 되어 깊게 복사(deep copy)가 됩니다.


## shallow vs. deep copy

install.packages("lobstr")
library(lobstr)

## shallow copy
x <- c(1, 2, 3)
lobstr::obj_addr(x)
# [1] "0x10ccef808"

## The location ("memory address") of x and y in memory are the same.

## ie., "shallow copy" until now.
y <- x
lobstr::obj_addr(y)
# [1] "0x10ccef808"


## When new object is actually modified, then "deep copy" occurs.

## ie., new physicial copy in memory.
y <- c(y, -3)
print(lobstr::obj_addr(y))
# [1] "0x10d6dacf8"




아래에 x와 y를 인쇄해서 비교해보니 x는 아무런 영향을 받지 않아서 그대로 이고, y는 '-3' 값이 추가되어 새로운 벡터로 변경되었습니다. (즉, y에 깊은 복사(deep copy)가 일어났음.)



## no change in x

print(x)

# [1] 1 2 3


## only change in y

print(y)

# [1]  1  2  3 -3

 



R data.table은 참조하여 새로운 칼럼을 추가하거나 갱신하는 경우 얕은 복사(shallow copy)를 합니다.




  (3) 참조에 의한 칼럼 추가/ 갱신/ 삭제

      (Add/ update/ delete columns by refreence)


data.table의 기본 구문 DT[i, j, by]에서 칼럼 j 를 추가/ 갱신/ 삭제할 때는 특수부호 := 연산자를 사용하며, 아래처럼 왼쪽처럼 (방법 1) 'LHS (Left Hand Side) := RHS (Right Hand Side)' 구문이나, 또는 오른쪽에 있는 (방법 2) ':='(colA = valA, colB = vlaB, ...) 의 함수 형식(functional form)으로 구문을 작성합니다.

(방법 1) 로는 왼쪽(LHS)에 문자 벡터(Charater vector)로 칼럼 이름을 부여하고, 오른쪽(RHS)에는 리스트 값(list of values)을 받습니다. 오른쪽(RHS)은 lapply(), list(), mget(), mapply() 등 어떤 방식으로 생성되었든지간에 단지 리스트이기만 하면 되고 왼쪽은 문자 벡터를 칼럼 이름으로 받으므로, 특히 미리 칼럼에 무슨 값을 할당할지 모르는 경우에 프로그래밍하는데 사용하기에 편리합니다.

(방법 2)의 경우 칼럼과 값을 1:1로 나란히 짝을 지어 코딩하기 때문에 칼럼 개수가 적을 경우 사람이 읽기에 좀더 가독성이 좋고, 칼럼 = 값의 오른쪽 여백에 '#' 코멘트 기호로 부연설명을 추가해 넣기에 유용합니다.

만약, 대상 칼럼이 1개 뿐이라면 왼쪽 아래부분처럼 DT[, colA := valA] 처럼 구문을 간편하게 작성할 수도 있습니다.

## way 1. The LHS := RHS form
 DT[, c("colA", "colB", ...) := list(valA, valB, ...)]


## in case only for 1 column

DT[, colA := valA]

## way 2. The functional form
 Dt[, ':='(colA = valA, # valA is assigned to colA
          colB = valB, # valB is assigned to colB
          ...)]




  • data.table 참조하여 칼럼 추가 (Add column by Reference in data.table)
MASS 패키지에 내장되어 있는 Cars93 데이터셋으로 부터 칼럼 몇개만 선택해 가져와서 DT 라는 이름의 예제 data.table을 만들어보겠습니다. 그리고 위의 두 가지 방법을 이용하여 "Range.Price", "Area"라는 이름의 새로운 칼럼 2개를 추가해보겠습니다.

'최대 가격(Max.Price)'에서 '최소 가격(Min.Price)'의 차이를 계산해서 '가격 범위(Range.Price)' 칼럼을 새로 생성하여 추가하고, '길이(Length)'와 '폭(Width)'을 곱하여 '넓이(Area)' 칼럼을 새로 생성하여 추가하시오.


## a) Add columns by reference
library(data.table)
library(MASS)

DT <- data.table(Cars93[, c("Model", "Type", "Min.Price", "Max.Price", "Length", "Width")])
head(DT)
# Model    Type Min.Price Max.Price Length Width
# 1: Integra   Small      12.9      18.8    177    68
# 2:  Legend Midsize      29.2      38.7    195    71
# 3:      90 Compact      25.9      32.3    180    67
# 4:     100 Midsize      30.8      44.6    193    70
# 5:    535i Midsize      23.7      36.2    186    69
# 6: Century Midsize      14.2      17.3    189    69



## way 1: LHS := RHS form
DT[, c("Range.Price", "Area") := list((Max.Price - Min.Price), Length * Width)]


## (equivalently) way 2: The functional form
DT[, ':='(Range.Price = (Max.Price - Min.Price),
           Area = Length * Width)]


head(DT)
# Model    Type Min.Price Max.Price Length Width Range.Price  Area
# 1: Integra   Small      12.9      18.8    177    68         5.9 12036
# 2:  Legend Midsize      29.2      38.7    195    71         9.5 13845
# 3:      90 Compact      25.9      32.3    180    67         6.4 12060
# 4:     100 Midsize      30.8      44.6    193    70        13.8 13510
# 5:    535i Midsize      23.7      36.2    186    69        12.5 12834
# 6: Century Midsize      14.2      17.3    189    69         3.1 13041



  • data.table 참조하여 칼럼 갱신 (Update column(s) by Reference in data.table)
이번에는 DT[i, j, by] 구문에서 i 로 조건을 만족하는 일부 행을 선별하고, colA := valB 구문으로 특정 칼럼의 값을 갱신(update) 하여 보겠습니다.

차종(Type)이 'Large'인 행의 차종의 값을 'Big'으로 변경하시오.


## b) Update some rows of columns by reference - sub-assign by reference
DT[, .N, by = Type]
# Type  N
# 1:   Small 21
# 2: Midsize 22
# 3: Compact 16
# 4:   Large 11   <---
# 5:  Sporty 14
# 6:     Van  9


## replace those rows where Type == 'Large' with the value 'Big'
DT[Type == 'Large', Type := 'Big']

DT[, .N, by = Type]
# Type  N
# 1:   Small 21
# 2: Midsize 22
# 3: Compact 16
# 4:     Big 11   <---
# 5:  Sporty 14
# 6:     Van  9

 



위처럼 DT[Type == 'Large', Type := 'Big'] 을 실행시키면 칼럼 값 갱신이 눈에 보이지 않는(invisibly) 상태로 실행됩니다. 만약 갱신 결과를 눈에 보이도록 출력하려면 제일 뒤에 [] 를 붙여주면 됩니다.



## We can see the result by adding an empty [] at the end of the query
DT[Type == 'Large', Type := 'Big'][]
# Model    Type Min.Price Max.Price Length Width Range.Price  Area
# 1:        Integra   Small      12.9      18.8    177    68         5.9 12036
# 2:         Legend Midsize      29.2      38.7    195    71         9.5 13845
# 3:             90 Compact      25.9      32.3    180    67         6.4 12060
# 4:            100 Midsize      30.8      44.6    193    70        13.8 13510
# 5:           535i Midsize      23.7      36.2    186    69        12.5 12834
# ---
# 89:        Eurovan     Van      16.6      22.7    187    72         6.1 13464
# 90:         Passat Compact      17.6      22.4    180    67         4.8 12060
# 91:        Corrado  Sporty      22.9      23.7    159    66         0.8 10494
# 92:            240 Compact      21.8      23.5    190    67         1.7 12730
# 93:            850 Midsize      24.8      28.5    184    69         3.7 12696
 



  • data.table 참조하여 칼럼 삭제 (Delete column(s) by Reference in data.table)
data.table에서 칼럼을 삭제하려면 colA := NULL 처럼 NULL 값을 할당해주면 됩니다. rm() 이나 del() 같은 함수가 아니라 '없음(NULL)'을 할당하는 방식이라 좀 생소하기는 합니다. ^^;

삭제하려는 칼럼이 여러개라면 c("colA", "colB", ...) 처럼 c() 로 칼럼 문자 벡터를 묶어서 써줍니다.

DT data.table에서 "Range.Price"와 "Area" 칼럼을 삭제하시오.


## c) Delete column by reference
## Assigning NULL to a column deletes that column. And it happens instantly.
DT[, c("Range.Price", "Area") := NULL]
head(DT, 3)
# Model    Type Min.Price Max.Price Length Width
# 1: Integra   Small      12.9      18.8    177    68
# 2:  Legend Midsize      29.2      38.7    195    71
# 3:      90 Compact      25.9      32.3    180    67




만약 삭제하려는 칼럼이 단 1개라면 c() 나 문자형 칼럼 이름을 쓸 필요없이, 그냥 colA := NULL 처럼 바로 써주면 됩니다.


## When there is just one column to delete, we can drop the c() and bouble quotes
DT[, Width := NULL]
head(DT, 3)
# Model    Type Min.Price Max.Price Length
# 1: Integra   Small      12.9      18.8    177
# 2:  Legend Midsize      29.2      38.7    195
# 3:      90 Compact      25.9      32.3    180

 





  (4) 그룹 by와 참조 := 연산자 함께 사용하기


data.table의 기본구문인 DT[i, j, by] 에서 그룹 by 별로 연산을 한 결과를 참조하여 DT[, colA := calculation(colB), by = Group] 형식으로 새로운 칼럼을 추가할 수 있습니다.

data.table DT에서 차종(Type) 그룹별로 길이(Length)의 최대값을 구하여, 각 행의 차종(Type)에 맞게 "Max.Length"라는 이름의 칼럼에 값을 추가하시오.


## d) := along with grouping using 'by'
DT[, Max.Length := max(Length), by = Type]

## or alternatively, providing with a character vector
DT[, Max.Length := max(Length), by = c("Type")]

head(DT)
# Model    Type Min.Price Max.Price Length Max.Length
# 1: Integra   Small      12.9      18.8    177        177
# 2:  Legend Midsize      29.2      38.7    195        205 <--
# 3:      90 Compact      25.9      32.3    180        190
# 4:     100 Midsize      30.8      44.6    193        205 <--
# 5:    535i Midsize      23.7      36.2    186        205 <--
# 6: Century Midsize      14.2      17.3    189        205 <--

## check the max value of Length by Type
DT[, max(Length), by = Type]
# Type  V1
# 1:   Small 177
# 2: Midsize 205  <--
# 3: Compact 190
# 4:     Big 219
# 5:  Sporty 196
# 6:     Van 194

 





  (5) := 연산자와 lapply()로 여러개의 칼럼 한꺼번에 만들기


data.table을 참조하여 여러개의 칼럼을 참조하여 여러개의 새로운 칼럼을 한꺼번에 추가하거나 갱신, 삭제하고 싶으면 (a) .SDcols = c("colA", "colB", ...) 에서 지정한 복수의 칼럼에 대하여, (b) lapply(.SD, function) 으로 (a)에서 지정한 모든 칼럼(.SD)에 함수를 적용하여, (c) := 연산자로 (b)의 결과를 참조하여 새로운 칼럼을 추가합니다.


참고로, R data.table에서 여러개의 변수를 처리하는 특수부호 .SD, .SDcols 에 대한 자세한 소개는 https://rfriend.tistory.com/568 를 참조하세요.


DT data.table에서 차종(Type) 그룹별로 '가격(Price)'과 '길이(Length)' 변수의 평균을 구하여 '평균 가격(mean_price)', '평균 길이(mean_length)' 칼럼을 추가하시오.



## e) Multiple columns and :=

## new data.table as an sample
DT2 <- data.table(Cars93[, c("Model", "Type", "Price", "Length")])
head(DT2)
# Model    Type Price Length
# 1: Integra   Small  15.9    177
# 2:  Legend Midsize  33.9    195
# 3:      90 Compact  29.1    180
# 4:     100 Midsize  37.7    193
# 5:    535i Midsize  30.0    186
# 6: Century Midsize  15.7    189

## LHS := RHS form
in_cols = c("Price", "Length")
out_cols = c("mean_price", "mean_length")
DT2[, c(out_cols) := lapply(.SD, mean), # lapply() for multiple columns
    by = Type,         # by Type groups
    .SDcols = in_cols] # compute the mean on columns specified in .SDcols

head(DT2)
# Model    Type Price Length mean_price mean_length
# 1: Integra   Small  15.9    177   10.16667    167.1905
# 2:  Legend Midsize  33.9    195   27.21818    192.5455
# 3:      90 Compact  29.1    180   18.21250    182.1250
# 4:     100 Midsize  37.7    193   27.21818    192.5455
# 5:    535i Midsize  30.0    186   27.21818    192.5455
# 6: Century Midsize  15.7    189   27.21818    192.5455

 



[ Reference ]
* R data.table vignette
: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reference-semantics.html


다음번 포스팅에서는 R data.table에서 copy() 함수를 사용해서 깊은 복사(deep copy)를 하는 방법을 소개하겠습니다.


이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요! :-)



728x90
반응형
Posted by Rfriend
,

Python의 SQLAlchemy (모든 DB), psycopg2 (PostgreSQL, Greenplum) 패키지를 이용하여 Spyder 나 Pycharm 과 같은 IDE에서 PostgreSQL, Greenplum database에 접속(access) 하고 SQL query 를 할 수 있습니다.

 

 

 

그렇다면 Jupyter Notebook 에서도 DB access, SQL query를 할 수 있으면 편하겠지요?

 

이번 포스팅에서는 Python의 ipython-sql 과 pgspecial 패키지를 이용하여 PostgreSQL, Greenplum database 에 Jupyter Notebook으로 접속(access DB)하여 SQL query 를 하는 방법을 소개하겠습니다.

 


(1) ipython-sql 로 PostgreSQL, Greenplum DB 접속(access)하기

(2) ipython-sql 로 PostgreSQL, Greenplum DB에 SQL query 하기 (%sql, %%sql)

(3) pgspecial 로 PostgreSQL, Greenplum DB에 meta-commands query 하기 (\l, \dn, \dt)

 

 

 

 

 

(0) 사전 설치가 필요한 Python 패키지 리스트

 

명령 프롬프트 창에서 아래의 5개 패키지에 대해서 pip 로 설치해주시기 바랍니다.

 

sqlalchemy, psycopg2는 PostgreSQL, Greenplum DB 접속(access, connection)를 위해서 필요한 Python 패키지 입니다.

 

ipython-sql, sql_magic은 IPython으로 Jupyter Notebook에서 DB access, SQL query 를 하기 위해 필요한 Python 패키지입니다.

 

pgspecial은 Jupyter Notebook에서 PostgreSQL, Greenplum DB에 meta-commands (역슬래쉬 \ 로 시작하는, psql 에서 사용하는 \l, \dn, \dt 명령문) 를 위해 필요한 Python 패키지입니다.

 

ipython-sql 의 경우 2020.12월 현재 0.4.0 버전 (python 3.x) 이 최신인데요, %config로 DB access 하는 명령문의 bug가 아직 fix가 안되어 있어서, 아래처럼 ipython-sql==0.3.9 로 한단계 낮은 버전으로 설치해주세요.

 

 

-- 명령 프롬프트 창에서 pip 로 python 패키지 설치

 

$ pip install --upgrade pip

$ pip install sqlalchemy

$ pip install psycopg2

$ pip install ipython-sql==0.3.9

$ pip install pgspecial

$ pip install sql_magic

 

 

만약 psycopg2 모듈을 pip 로 설치하다가 에러가 나면 아래처럼 wheel package 를 이용해서 psycopg2-binary 로 설치해보세요. 

 

    Error: pg_config executable not found.    

    pg_config is required to build psycopg2 from source.  Please add the directory

    containing pg_config to the $PATH or specify the full executable path with the

    option:   

        python setup.py build_ext --pg-config /path/to/pg_config build ..

    or with the pg_config option in 'setup.cfg'.

 

 

$ pip install psycopg2-binary

 

 

 (1) ipython-sql 로 PostgreSQL, Greenplum DB 접속(access)하기

 

%load_ext sql 로 IPython의 sql 을 로딩하여 %sql 또는 %%sql magic 명령문을 사용할 수 있습니다.

PostgreSQL, Greenplum database에 접속할 때는 SQLAlchemy 의 표준 URL connect strings 를 사용합니다.

 

[ SQLAlchemy 의 표준 Database URL]

 

 dialect+driver://username:password@hoat:port/database

 

아래의 db credentials 로 Greenplum database에 접속할 때의 예입니다.

- driver: postgresql

- username: gpadmin

- password: changeme

- host: localhost

- port: 5432

- database: demo

 

 

%load_ext sql

 

# postgresql://Username:Password@Host:Port/Database
%sql postgresql://gpadmin:changeme@localhost:5432/demo

[Out] 'Connected: gpadmin@testdb'

 

 

 

 

* 위의 %sql SQLAlchemy 표준 URL 방법 외에 Jupyter Notebook에서 PostgreSQL, Greenplum DB에 접속하는 다른 3가지 추가 방법은 https://rfriend.tistory.com/577 를 참고하세요.

 

 

 

 (2) ipython-sql 로 PostgreSQL, Greenplum DB에 SQL query 하기 (%sql, %%sql)

 

Jupyter Notebook의 Cell 안에 1줄 SQL query일 경우는 %sql 로 시작하고, 2줄 이상 SQL query 일 경우에는 %%sql 로 시작합니다.

 

(2-1) %sql : 1줄의 SQL query

 

1줄짜리 SELECT 문으로 PostgreSQL의 버전을 확인해보겠습니다.

 

 

%sql SELECT version();

 

[Out]

version

PostgreSQL 9.4.24 (Greenplum Database 6.10.1 build commit:efba04ce26ebb29b535a255a5e95d1f5ebfde94e) on x86_64-unknown-linux-gnu, compiled by gcc (GCC) 6.4.0, 64-bit compiled on Aug 13 2020 02:56:29

 

 

 

 

 

 

(2-1) %%sql : 2줄 이상의 SQL query

 

아래 예제는 pg_catalog.pg_tables 테이블에서 스키마 이름이 pg_catalog, information_schema 가 아닌 테이블을 조회하여 1개만 반환해보는 query 입니다.

 

 

%%sql
SELECT *
FROM pg_catalog.pg_tables
WHERE schemaname != 'pg_catalog' AND
    schemaname != 'information_schema'
LIMIT 1;

 

 

 

 

 

 (3) pgspecial 로 PostgreSQL, Greenplum DB에 meta-commands (\l, \dn, \dt)

 

meta-commands 는 psql 에서 역슬래쉬(\)와 함께 사용해서 데이터베이스, 스키마, 테이블 등을 조회할 때 사용하는 명령어를 말합니다. pgspecial 패키지는 Jupyter Notebook에서 meta-commands 를 사용할 수 있게 해줍니다.

 

(3-1) database 조회 : %sql \l  (역슬래쉬 + L)

 

 

%sql \l

 

[Out]

Name

Owner

Encoding

Collate

Ctype

Access privileges

demo

gpadmin

UTF8

en_US.utf8

en_US.utf8

=Tc/gpadmin

gpadmin=CTc/gpadmin

dsuser=CTc/gpadmin

gpperfmon

gpadmin

UTF8

en_US.utf8

en_US.utf8

None

postgres

gpadmin

UTF8

en_US.utf8

en_US.utf8

None

template0

gpadmin

UTF8

en_US.utf8

en_US.utf8

=c/gpadmin

gpadmin=CTc/gpadmin

template1

gpadmin

UTF8

en_US.utf8

en_US.utf8

=c/gpadmin
gpadmin=CTc/gpadmin

 

 

 
(3-2) Schema 조회 : %sql \dn
 

 

%sql \dn

 

[Out]

Name Owner
gp_toolkit gpadmin
madlib dsuser14
public gpadmin

 

 

 

 
(3-3) Table 조회 : %sql \dt
 
아래 예제는 public 스키마에서 "ab" 로 시작하는 모든 테이블(public.ab*)을 조회한 것입니다.
 

 

%sql \dt public.ab*

 

[Out]

Schema Name Type Owner
public abalone table

gpadmin

public abalone_corr table

gpadmin

public abalone_corr_summary table

gpadmin

public abalone_correlations table

gpadmin

 

 

 

이상으로 ipython-sql, pgspecial 패키지를 사용해서 PostgreSQL, Greenplum database에 접속하고 SQL query, meta-commands 하는 방법에 대한 가장 기본적이고 개략적인 소개를 마치겠습니다.
 
* 다음번 포스팅에서는 SQLAlchemy, psycopg2, ipython-sql 로 Jupyter Notebook 에서 PostgreSQL, Greenplum database에 접속하는 4가지 방법(https://rfriend.tistory.com/577)에 대한 소소한 팁을 추가로 소개하겠습니다.
 
* ipython-sql 로 PostgreSQL, Greenplum database에 접속하여 Jupyter Notebook 의 로컬변수로 동적으로 SQL query 하는 3가지 방법https://rfriend.tistory.com/578 를 참고하세요.
 
* ipython-sql로 PostgreSQL, Greenplum database에 접속하여 Jupyter Notebook에서 SQL query한 결과를 pandas DataFrame으로 가져오는 3가지 방법https://rfriend.tistory.com/579 를 참고하세요.
 
이번 포스팅이 많은 도움이 되었기를 바랍니다.
행복한 데이터 과학자 되세요!

 

 

 

728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 R data.table에서 Key를 칼럼 j 와 그룹 by와 함께 사용하는 방법을 소개하였습니다. 


이번 포스팅에서는 R data.table에서 Key설정된 칼럼에 대해서 mult, nomatch 매개변수를 사용하여 조건이 매칭되는 행을 가져오는 다양한 방법을 소개하겠습니다. 


(1) Key 값이 매칭되는 모든 행 가져오기: mult = "all"

(2) Key 값이 매칭되는 첫번째 행 가져오기: mult = "first"

(3) Key 값이 매칭되는 마지막 행 가져오기: mult = "last"

(4) Key 값이 매칭되는 마지막 행의 값이 존재하는 행만 가져오기: mult = "last", nomatch = NULL





  (1) Key 값이 매칭되는 모든 행 가져오기: mult = "all"


먼저 data.table 패키지를 불러오고, MASS 패키지의 Cars93에서 칼럼 몇개만 선해서 예제로 사용할 간단한 data.table 자료를 만들어보겠습니다. 



library(data.table)

library(MASS)


DT <- data.table(Cars93[, c("Model", "Type", "Price", "DriveTrain")])


> str(DT)

Classes 'data.table' and 'data.frame': 93 obs. of  4 variables:

 $ Model     : Factor w/ 93 levels "100","190E","240",..: 55 9 22 30 52 83 90 11 7 14 ...

 $ Type      : Factor w/ 6 levels "Compact","Large",..: 1 1 1 1 1 1 1 1 1 1 ...

 $ Price     : num  19.5 29.1 13.4 11.4 15.8 13.3 11.3 17.5 16.5 15.7 ...

 $ DriveTrain: Factor w/ 3 levels "4WD","Front",..: 1 2 2 2 2 2 2 2 2 2 ...

 - attr(*, ".internal.selfref")=<externalptr> 

 - attr(*, "sorted")= chr [1:2] "Type" "DriveTrain"

 



다음으로, setkey() 함수를 사용하여 DT data.table에 차종(Type)과 동력전달장치(DriveTrain) 칼럼을 키로 설정(set the Key)해 보겠습니다. 



> # Set the key with Type and DriveTrain columns

> setkey(DT, Type, DriveTrain)

> key(DT)

[1] "Type"       "DriveTrain"

 



이제 준비가 되었으니 mult = "all" 매개변수를 사용해서 키 값과 매칭하는 모든 행을 가져와보겠습니다. 


키로 설정된 차종 동력전달치에서 (a) 차종이 "Compact", "Van", "Sporty" 이고 (Type == c("Compact", "Van", "Sporty")) & (b) 동력전달장치가 후륜 (DriveTrain == "Rear") 인 차를 모두 (mult = "all") 가져오시오. 


모두 9개의 행을 반환했는데요, 이중에서 3번, 4번 행을 보면, 차종이 "Van" (Type == "Van")이고 & 동력전달장치가 후륜(DriveTrain == "Rear") 차는 DT data.table 에 존재하지 않기 때문에 <NA>를 반환하였습니다.  



# Subset 'all' rows where  

# where Type matches ("Compact", "Sporty") and DriveTrain matches "Rear".  

DT[.(c("Compact", "Van", "Sporty"), "Rear")] # default mult = "all"


# or equivalently

> DT[.(c("Compact", "Van", "Sporty"), "Rear"), mult = "all"] # default


      Model    Type Price DriveTrain

1:     190E Compact  31.9       Rear      #<--- first of "Compact" Type & "Rear" DriveTrain

2:      240 Compact  22.7       Rear      #<--- last of "Compact" Type & "Rear" DriveTrain


3:     <NA>     Van    NA       Rear    #<--- first of "Van" Type & "Rear" DriveTrain

4:     <NA>     Van    NA       Rear    #<--- last of "Van" Type & "Rear" DriveTrain


5:   Camaro  Sporty  15.1       Rear      #<--- first of "Sporty" Type & "Rear" DriveTrain

6: Corvette  Sporty  38.0       Rear

7:  Mustang  Sporty  15.9       Rear

8:     RX-7  Sporty  32.5       Rear

9: Firebird  Sporty  17.7       Rear         #<--- last of "Sporty" Type & "Rear" DriveTrain





  (2) Key 값이 매칭되는 첫번째 행 가져오기: mult = "first"


Key 중에서 차종(Type)이 "Compact", "Van", "Sporty" 이고 & 동력전달장치(DriveTran)이 "Rear"인 차를 가져온 위의 (1)번 결과 중에서 각 유형별로 첫번째 행(mult = "first")을 가져오시오



# Subset only the 'first' matching row from all rows 

# where Type matches ("Compact", "Van", "Sporty") and DriveTrain matches "Rear".  

> DT[.(c("Compact", "Van", "Sporty"), "Rear"), mult = "first"]

    Model    Type Price DriveTrain

1:   190E Compact  31.9       Rear

2:   <NA>     Van    NA       Rear

3: Camaro  Sporty  15.1       Rear






  (3) Key 값이 매칭되는 마지막 행 가져오기: mult = "last"


Key 중에서 차종(Type)이 "Compact", "Van", "Sporty" 이고 & 동력전달장치(DriveTran)이 "Rear"인 차를 가져온 위의 (1)번 결과 중에서 각 유형별로 마지막 행(mult = "last")을 가져오시오



# Subset only the 'last' matching row from all rows

#  where Type matches ("Compact", "Van", "Sporty") and DriveTrain matches "Rear".  

> DT[.(c("Compact", "Van", "Sporty"), "Rear"), mult = "last"]

      Model    Type Price DriveTrain

1:      240 Compact  22.7       Rear

2:     <NA>     Van    NA       Rear

3: Firebird  Sporty  17.7       Rear

 





  (4) Key 값이 매칭되는 마지막 행의 값이 존재하는 행만 가져오기

      : mult = "last", nomatch = NULL


Key 중에서 차종(Type)이 "Compact", "Van", "Sporty" 이고 & 동력전달장치(DriveTran)이 "Rear"인 차를 가져온 위의 (1)번 결과 중에서 각 유형별로 마지막 (mult = "last")을 가져오되, 매칭되는 값이 존재하는 행만(nomatch = NULL) 가져오시오



# We can choose if queries that do not match should return NA 

# or be skipped altogether using the nomatch argument.(nomatch = NULL)

> DT[.(c("Compact", "Van", "Sporty"), "Rear"), 

+    mult = "last"

+    nomatch = NULL]

      Model    Type Price DriveTrain

1:      240 Compact  22.7       Rear

2: Firebird  Sporty  17.7       Rear

 



[Reference]

* R data.table vignettes: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-keys-fast-subset.html


이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요. 



728x90
반응형
Posted by Rfriend
,

지난 포스팅에서는 R data.table 이 키(Key)와 빠른 이진 탐색(fast binary search) 기반의 부분집합 선택을 하는 원리, 함수 사용방법에 대하여 소개하였습니다.


지난 포스팅이 R data.table에 Key 를 활용하여 행(row, i)에 대해서만 subset 하는 방법에 대해 한정해서 소개했습니다. 반면, 이번 포스팅은 지난 포스팅을 이어받아 더 확장하여, DT[i, j, by][order()] 기본 구문에서 행뿐만 아니라 칼럼(column, j), 그룹(group, by) 별 연산, 정렬, 키 재설정 하는 방법까지 소개하겠습니다.


1. 키 설정 후, 특정 키(Key) 값에 행을 한정해 칼럼 j 가져오기 (select in j)

2. 체인연산으로 정렬하기 (chaining)

3. 특정 키(Key) 값에 행을 한정해 칼럼 j 에 연산 수행하기 (compute or do in j)

4. 칼럼 j에 := 를 사용하여 키의 부분집합에 할당하기(sub-assign by reference)

5. by 를 사용하여 그룹별 집계하기 (aggregation using by)





  1. 키 설정 후, 특정 키(Key) 값에 행을 한정해 칼럼 j 가져오기 (select in j)


먼저 Cars93 data.frame에서 칼럼 몇 개만 가져와서 예제로 사용할 간단할 data.table DT 를 만들어보겠습니다. 그리고 DT의 Type, DriveTrain 칼럼에 대해 키를 설정(setkey) 하겠습니다.


이렇게 키를 설정하면 data.table DT는 키인 Type, DriveTrain을 참조으로 오름차순으로 재정렬됩니다.



library(data.table)
library(MASS)
DT <- data.table(Cars93[, c("Model", "Type", "Price", "MPG.highway", "DriveTrain")])

# Combining keys with j and by
# Set keys with Type and DriveTrain
setkey(DT, Type, DriveTrain)


> key(DT)
[1] "Type"       "DriveTrain"

>

> # reordered by Keys

> head(DT, 10)
       Model    Type Price MPG.highway DriveTrain
 1:   Legacy Compact  19.5          30        4WD
 2:       90 Compact  29.1          26      Front
 3: Cavalier Compact  13.4          36      Front
 4:  Corsica Compact  11.4          34      Front
 5:  LeBaron Compact  15.8          28      Front
 6:   Spirit Compact  13.3          27      Front
 7:    Tempo Compact  11.3          27      Front
 8:   Accord Compact  17.5          31      Front
 9:      626 Compact  16.5          34      Front
10:   Altima Compact  15.7          30      Front




이제 위에서 설정한 (a) Type, DriveTrain 키(Key) 값이 Type == "Compact" & DriveTrain == "Front" 인 행을 선별하고 (subset row i), (b) 이들 부분집합에 대해서 칼럼(column j) Model, MPG.highway 를 선택해서 가져오겠습니다.


이렇게 data.table의 특정 키 값의 행만 가져오기(subset) 할 때 Key를 설정하면서 재정렬 된 상태이고 빠른 이진 탐색(fast binary search)로 키 값 데이터를 선별하므로 매우 빠르게 수행됩니다. 그리고 이렇게 선별한 subset에 한정해서 칼럼 j를 선택해서 가져오므로 메모리 효율적이고 빠릅니다.



> # Select in j
> DT[.("Compact", "Front"), .(Model, MPG.highway)]


       Model MPG.highway
 1:       90          26
 2: Cavalier          36
 3:  Corsica          34
 4:  LeBaron          28
 5:   Spirit          27
 6:    Tempo          27
 7:   Accord          31
 8:      626          34
 9:   Altima          30
10:  Achieva          31
11:  Sunbird          31
12:      900          26
13:   Passat          30




아래에는 위에서 수행한 행과 열 선택 과정을 data.table과 dplyr로 각각 했을 때의 코드를 비교해 보았습니다. 코드의 간결성면에서나 수행 속도면에서 data.table이 dplyr보다 우수합니다. (단, 코드의 가독성은 dplyr이 더 나아보입니다.)

data.table

dplyr

library(data.table)


setkey(DT, Type, DriveTrain)

DT[.("Compact", "Front"), .(Model, MPG.highway)]



library(dplyr)


DT %>%
  filter(Type == "Compact"
         & DriveTrain == "Front") %>%
  select(Model, MPG.highway)



위의 코드에서 뒤의 칼럼 j 를 선택할 때 c("Model", "MPG.highway") 처럼 큰 따옴표(" ")를 대신 사용할 수도 있으며, 프로그래밍을 할 때 유용하게 쓸 수 있습니다.



# or alternatively
DT[.("Compact", "Front"), c("Model", "MPG.highway")] # useful for programming

 





  2.  체인연산으로 결과 정렬하기 (chaining)


위의 (1) 번 결과를 MPG.highway 를 기준으로 오름차순 정렬하려면 Chaining 으로 [order(MPG.highway)] 를 뒤에 이어서 써주면 됩니다.



> # Chaining
> # - use chaining to order the MPG.highway column.
> # in ascending order
> DT[.("Compact", "Front"), .(Model, MPG.highway)][order(MPG.highway)]
       Model MPG.highway
 1:       90          26
 2:      900          26
 3:   Spirit          27
 4:    Tempo          27
 5:  LeBaron          28
 6:   Altima          30
 7:   Passat          30
 8:   Accord          31
 9:  Achieva          31
10:  Sunbird          31
11:  Corsica          34
12:      626          34
13: Cavalier          36
>



위의 (1)번 결과를 MPG.highway를 기준으로 내림차순(in decreasing order)으로 정렬을 하고 싶으면 마이너스 부호('-') 를 같이 써주어서 Chaining으로 [order(-MPG.highway)] 을 이어서 써주면 됩니다.



> # in descending order
> DT[.("Compact", "Front"), .(Model, MPG.highway)][order(-MPG.highway)]
       Model MPG.highway
 1: Cavalier          36
 2:  Corsica          34
 3:      626          34
 4:   Accord          31
 5:  Achieva          31
 6:  Sunbird          31
 7:   Altima          30
 8:   Passat          30
 9:  LeBaron          28
10:   Spirit          27
11:    Tempo          27
12:       90          26
13:      900          26






  3. 특정 키(Key) 값에 행을 한정해 칼럼 j 에 연산 수행하기 (compute or do in j)


(1)에서 설정한 키의 특정값인 Type == "Compact" & DriveTrain == "Front" 인 행을 선별하여, 이들 subset에 대해서만 MPG.highway 칼럼 j의 최대값(max), 평균(mean), 중앙값(median), 최소값(min), 1/4분위수(Q1), 3/4분위수(Q3)를 구해보겠습니다.


이때 요약통계량 연산을 하는데 사용하는 함수는 base R 함수의 max(), mean(), median(), min(), quantile() 과 동일하되, 속도는 base R 보다 data.table이 훨씬 빠릅니다!



> # Compute or do in j
> # - Find the maximum, minimum MPG.highway corresponding to Type == "Compact" and DriveTrain == "Front".
> DT[.("Compact", "Front"), max(MPG.highway)] # max
[1] 36
>
> DT[.("Compact", "Front"), mean(MPG.highway)] # mean
[1] 30.07692
>
> DT[.("Compact", "Front"), median(MPG.highway)] # median
[1] 30
>
> DT[.("Compact", "Front"), min(MPG.highway)] # min
[1] 26
>
> DT[.("Compact", "Front"), quantile(MPG.highway, c(0.25, 0.75))] # Q1, Q3
25% 75%
 27  31

 




  4. 칼럼 j에 := 를 사용하여 키의 부분집합에 할당하기(sub-assign by reference)


이번에는 Type을 키로 설정하고, 키의 값이 Type == "Large" 이면 ':=' 특수부호를 사용하여 Type == "Big" 의 다른 값으로 재할당(sub-assign by reference)해보겠습니다.


이러한 부분집합 재할당 때도 data.frame 대비 data.table이 훨씬 더 빠르게 수행이 됩니다.



> # sub-assign by reference using := in j
> DT <- data.table(Cars93[, c("Model", "Type", "Price", "MPG.highway", "DriveTrain")])
> setkey(DT, Type)
> key(DT)
[1] "Type"
>
> DT[, .N, by=Type]
      Type  N
1: Compact 16
2:   Large 11
3: Midsize 22
4:   Small 21
5:  Sporty 14
6:     Van  9

>

> DT[.("Large"), Type := "Big"]

>

> DT[, .N, by=Type]
      Type  N
1: Compact 16
2:     Big 11
3: Midsize 22
4:   Small 21
5:  Sporty 14
6:     Van  9
>



다만, 키의 특정 값을 재할당하고 나면 애초 키를 설정할 때 재정렬(reorder) 되었던 것이 이제 더이상 유효하지 않으므로 키가 해제됩니다.(Key 가 NULL로 설정됨)



> key(DT) # the key is removed by setting to NULL
NULL

 




  5. by 를 사용하여 그룹별 집계하기 (aggregation using by)


다시 data.table DT에 차종(Type)을 키로 설정한 후에, 동력전달장치(DriveTrain) 그룹별로 고속도로연비(MPG.highway) 의 최대값(max)을 집계해보겠습니다. 집계된 칼럼은 mpg_max 라는 이름을 부여하겠습니다.


이때 그룹별 집계의 결과값은 keyby 의 기준인 동력전달장치(DriveTrain)을 기준으로 정렬됩니다.



> # Aggregation using by
> setkey(DT, Type)
> key(DT) # key is Type.
[1] "Type"
>
> agg <- DT["Compact",
+           .(mpg_max = max(MPG.highway)),
+           keyby = DriveTrain] # keyby sets DriveTrain as the key and order it.
>
> head(agg)
   DriveTrain mpg_max
1:        4WD      30
2:      Front      36
3:       Rear      29




그리고, 그룹별 집계의 결과는 keyby 칼럼인 동력전달장치(DriveTrain)을 키로 설정됩니다.



> key(agg) # key is changed to DriveTrain.
[1] "DriveTrain"

 



만약 그룹별 집계를 할 때 그룹 연산 부호에 'keyby' 대신에 'by'를 사용하면 나중에 setkey(agg_2, DriveTrain) 처럼 별도의 키를 설정하는 코드를 추가해주어야 키가 설정이 되고 키를 기준으로 재정렬이 됩니다. (조금 번거롭습니다.)



> agg_2 <- DT["Compact",
+    .(mpg_max = max(MPG.highway)),
+    by = DriveTrain]
>

> # set the column 'DriveTrain' as a Key using setkey() explicitely
> setkey(agg_2, DriveTrain)
> head(agg_2)
   DriveTrain mpg_max
1:        4WD      30
2:      Front      36
3:       Rear      29
> key(agg_2)
[1] "DriveTrain"

 


[Reference] 

* data.table vignette: https://cran.r-project.org/web/packages/data.table/vignettes/datatable-keys-fast-subset.html


다음번 포스팅에서는 R data.table에서 mult, nomatch 매개변수를 사용하여 Key값에 매칭되는 행 중에서 모든 행, 첫번째 행, 마지막 행, 값이 존재하는 행만 가져오는 방법을 소개하겠습니다. 


이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 학자 되세요!



728x90
반응형
Posted by Rfriend
,