CUBE SUGAR CONTAINER

技術系のこと書きます。

Apache Hive で SELECT した結果から ARRAY を作る

Apahe Hive を使っていて、テーブルから SELECT してきた結果から ARRAY 型のカラムを作る方法が分からなくて調べた。 結論から先に述べると COLLECT_LIST() を使えば良い。

使った環境は次の通り。

$ cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core) 
$ uname -r
3.10.0-862.3.2.el7.x86_64
$ hive --version
Hive 2.3.3
Git git://daijymacpro-2.local/Users/daijy/commit/hive -r 8a511e3f79b43d4be41cd231cf5c99e43b248383
Compiled by daijy on Wed Mar 28 16:58:33 PDT 2018
From source with checksum 8873bba6c55a058614e74c0e628ab022
$ hadoop version
Hadoop 2.9.1
Subversion https://github.com/apache/hadoop.git -r e30710aea4e6e55e69372929106cf119af06fd0e
Compiled by root on 2018-04-16T09:33Z
Compiled with protoc 2.5.0
From source with checksum 7d6d2b655115c6cc336d662cc2b919bd
This command was run using /home/vagrant/hadoop-2.9.1/share/hadoop/common/hadoop-common-2.9.1.jar

ARRAY 型について

以前、このブログで ARRAY 型について書いたことがある。 ただ、この記事では SELECT してきた結果から ARRAY を作る方法については扱わなかった。

blog.amedama.jp

上記の記事にあるけど Apache Hive における ARRAY 型というのは、公式にジェイウォークをするための方法という感じ。 使うと一行のレコードに複数行の情報を詰め込むことができる。 Apache Hive はアーキテクチャ的に JOIN の処理が割と苦手なので、こういう機能を使う必要が出てくることもある。

下準備

まずは Hive のシェルを起動しておく。

$ hive

動作確認用のテーブルとして、名前 (name) と性別 (gender) の入るテーブル (users) を作っておく。

hive> CREATE TABLE users (
    >   name STRING,
    >   gender STRING
    > );
OK
Time taken: 0.06 seconds

適当な名前と性別でユーザを追加する。

hive> INSERT INTO TABLE users
    > VALUES
    >   ('alice', 'female'),
    >   ('bob', 'male'),
    >   ('carol', 'female');
...
OK
Time taken: 19.046 seconds

これで準備ができた。

SELECT した結果から ARRAY を作ってみる

ユーザを性別 (gender) で集計した上で、名前 (name) も一緒に確認したいという状況で考えてみよう。 この場合、性別と一緒に名前が ARRAY で得られると嬉しい。

この場合 GROUP BY に gender を指定した上で COLLECT_LIST() を使う。

hive> SELECT
    >   gender,
    >   COLLECT_LIST(name) AS names
    > FROM users
    > GROUP BY gender;
...
OK
female  ["alice","carol"]
male    ["bob"]
Time taken: 25.689 seconds, Fetched: 2 row(s)

見事、性別とそれに該当する名前が ARRAY で得られた。

ちなみに、COLLECT_LIST() から得られる結果は重複を許す。 もし、重複を許さない結果がほしいときは COLLECT_SET() を使えば良い。

プログラミング Hive

プログラミング Hive

  • 作者: Edward Capriolo,Dean Wampler,Jason Rutherglen,佐藤直生,嶋内翔,Sky株式会社玉川竜司
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2013/06/15
  • メディア: 大型本
  • この商品を含むブログ (3件) を見る

Python: pandas と Google BigQuery を連携させる

ぶっちゃけ pandas は大規模なデータセットを扱うのが苦手だ。 だいたい一桁 GB なら我慢と工夫で何とかなるけど、二桁 GB を超えると現実的な処理時間で捌けなくなってくる。 そこで、今回は pandas を Google BigQuery と連携させることで重たい処理をオフロードする方法を試してみる。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ python -V         
Python 3.6.5
$ pip list --format=columns | grep -i pandas
pandas                   0.23.3
pandas-gbq               0.5.0

Google BigQuery を使う下準備

ここから BigQuery を使うための下準備が結構長いので、既に使ったことがある人は読み飛ばしてもらって構わない。

まずは Homebrew Cask を使って GCP を操作するためのコマンドラインツールをインストールしておく。

$ brew cask install google-cloud-sdk

インストールしたら自分の GCP アカウントで認証する。

$ gcloud init

認証ができたら pandas との連携を試すためのプロジェクトを新しく作っておこう。

$ gcloud projects create pandas-bq

プロジェクトを作ったら支払いの設定を有効にする。

console.cloud.google.com

続いてプロジェクトにデータセットとテーブルを作る。

$ bq mk pandas-bq:example
Dataset 'pandas-bq:example' successfully created.
$ bq mk pandas-bq:example.iris
Table 'pandas-bq:example.iris' successfully created.

名前から分かる通り、みんな大好き Iris データセットを読み込む。

UCI のリポジトリから Iris データセットをダウンロードする。

$ wget https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data

上記からダウンロードしたデータセットには末尾に空行が入っているので取り除く。

$ brew install gnu-sed
$ alias sed="gsed"
$ sed -i -e "/^$/d" iris.data

空行が入っていると bq コマンドが無理やり内容を解釈しようとするので。

続いてテーブルのスキーマを用意する。

$ cat << 'EOF' > schema.json
[
  {"name": "sepal_length", "type": "float", "mode": "required"},
  {"name": "sepal_width", "type": "float", "mode": "required"},
  {"name": "petal_length", "type": "float", "mode": "required"},
  {"name": "petal_width", "type": "float", "mode": "required"},
  {"name": "label", "type": "string", "mode": "required"}
]
EOF

あとはデータをアップロードする。

$ bq load --replace --project_id pandas-bq example.iris iris.data schema.json

これで、次の通りデータが読み込まれた。

$ bq show pandas-bq:example.iris
Table pandas-bq:example.iris

   Last modified                 Schema                 Total Rows   Total Bytes   Expiration   Time Partitioning   Labels  
 ----------------- ----------------------------------- ------------ ------------- ------------ ------------------- -------- 
  05 Aug 20:50:57   |- sepal_length: float (required)   150          7100                                                   
                    |- sepal_width: float (required)                                                                        
                    |- petal_length: float (required)                                                                       
                    |- petal_width: float (required)                                                                        
                    |- label: string (required)                                                                             
                                                                
$ bq query --project_id pandas-bq "SELECT * FROM example.iris LIMIT 10"
Waiting on bqjob_r457fdda779482a15_0000016509ed844f_1 ... (0s) Current status: DONE   
+--------------+-------------+--------------+-------------+-----------------+
| sepal_length | sepal_width | petal_length | petal_width |      label      |
+--------------+-------------+--------------+-------------+-----------------+
|          5.1 |         2.5 |          3.0 |         1.1 | Iris-versicolor |
|          5.0 |         2.0 |          3.5 |         1.0 | Iris-versicolor |
|          5.7 |         2.6 |          3.5 |         1.0 | Iris-versicolor |
|          6.0 |         2.2 |          4.0 |         1.0 | Iris-versicolor |
|          5.8 |         2.6 |          4.0 |         1.2 | Iris-versicolor |
|          5.5 |         2.3 |          4.0 |         1.3 | Iris-versicolor |
|          6.1 |         2.8 |          4.0 |         1.3 | Iris-versicolor |
|          5.5 |         2.5 |          4.0 |         1.3 | Iris-versicolor |
|          6.4 |         3.2 |          4.5 |         1.5 | Iris-versicolor |
|          5.6 |         3.0 |          4.5 |         1.5 | Iris-versicolor |
+--------------+-------------+--------------+-------------+-----------------+

これで Google BigQuery 側の準備は整った。

pandas で BigQuery を使えるようにする下準備

続いて pandas と Google BigQuery を連携させるための準備を進める。

まずは必要なパッケージとして pandas と pandas-gbq をインストールする。

$ pip install pandas pandas-gbq

Python の REPL を起動する。

$ python

pandas をインポートする。

>>> import pandas as pd

これで下準備が整った。

pandas と BigQuery を連携させる

さっき作ったプロジェクトの ID と BigQuery で実行したいクエリを用意する。

>>> project_id = 'pandas-bq'
>>> query = 'SELECT * FROM example.iris LIMIT 10'

クエリは先ほど実行したものと同じ。

あとは pandas.read_gbq() に実行したいクエリとプロジェクトの ID を渡すだけ。 すると認証の URL が表示するので、それをブラウザで開く。 認証が成功すると認証コードが表示されるので、それを REPL に貼り付けよう。

>>> pd.read_gbq(query, project_id, dialect='standard')
Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?...
Enter the authorization code: ...
sepal_length  sepal_width       ...         petal_width            label
0           5.1          2.5       ...                 1.1  Iris-versicolor
1           5.0          2.0       ...                 1.0  Iris-versicolor
2           5.7          2.6       ...                 1.0  Iris-versicolor
3           6.0          2.2       ...                 1.0  Iris-versicolor
4           5.8          2.6       ...                 1.2  Iris-versicolor
5           5.5          2.3       ...                 1.3  Iris-versicolor
6           6.1          2.8       ...                 1.3  Iris-versicolor
7           5.5          2.5       ...                 1.3  Iris-versicolor
8           6.4          3.2       ...                 1.5  Iris-versicolor
9           5.6          3.0       ...                 1.5  Iris-versicolor

[10 rows x 5 columns]

これでクエリが実行される。

二回目の実行からは結果がキャッシュされるので、認証について聞かれることはない。

>>> query = 'SELECT COUNT(1) AS count FROM example.iris'
>>> pd.read_gbq(query, project_id, dialect='standard')
   count
0    150

pandas.read_gbq() の結果は DataFrame として得られる。

>>> query = '''
... SELECT
...   AVG(sepal_length) AS sepal_length_avg,
...   AVG(sepal_width) AS sepal_width_avg,
...   AVG(petal_length) AS petal_length_avg,
...   AVG(petal_width) AS petal_width_avg
... FROM example.iris
... '''
>>> new_df = pd.read_gbq(query, project_id, dialect='standard')
>>> type(new_df)
<class 'pandas.core.frame.DataFrame'>
>>> new_df
   sepal_length_avg       ...         petal_width_avg
0          5.843333       ...                1.198667

[1 rows x 4 columns]

DataFrame を BigQuery に書き込む

先ほどの例は BigQuery のテーブルにクエリを発行して結果を読み出すものだった。 今度はローカルの DataFrame の内容を BigQuery に書き出してみる。

サンプルの DataFrame を用意する。

>>> data = [
...   ('alice', 150),
...   ('bob', 160),
...   ('carol', 170),
... ]
>>> df = pd.DataFrame(data, columns=['name', 'height'])

あとは DataFrame#to_gbq() メソッドを実行する。 このときデータセット名、テーブル名、プロジェクト ID を指定する。

>>> df.to_gbq('example.users', project_id)

コマンドラインツールで確認すると、ちゃんとテーブルができてデータが入っていることが分かる。

$ bq show pandas-bq:example.users
Table pandas-bq:example.users

   Last modified          Schema         Total Rows   Total Bytes   Expiration   Time Partitioning   Labels  
 ----------------- -------------------- ------------ ------------- ------------ ------------------- -------- 
  05 Aug 21:05:36   |- name: string      3            43                                                     
                    |- height: integer                                                                       

$ bq query --project_id pandas-bq "SELECT * FROM example.users"
+-------+--------+
| name  | height |
+-------+--------+
| alice |    150 |
| bob   |    160 |
| carol |    170 |
+-------+--------+

ばっちり。

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

  • 作者: Wes McKinney,瀬戸山雅人,小林儀匡,滝口開資
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/07/26
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Ubuntu 18.04 LTS に後から GUI (X Window System) を追加する

Ubuntu 18.04 LTS をサーバ版でインストールするとデスクトップ環境が入らない。 とはいえ後から欲しくなるときもあるので、その方法について。 ちなみに必要なパッケージの名称は Ubuntu 16.04 LTS と同じだった。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"
$ uname -r
4.15.0-20-generic

もくじ

下準備

最初に apt-get update をして更新サイトを最新の状態にしておく。

$ sudo apt-get update

デスクトップ環境が必要なとき

デスクトップ環境をインストールするには apt-get installubuntu-desktop を入れるだけ。

$ sudo apt-get -y install ubuntu-desktop

かなり依存パッケージが多いので気長に待つ。

インストールが終わったら再起動する。

$ sudo shutdown -r now

あとは自動的にデスクトップ環境が有効な状態で起動してくる。

f:id:momijiame:20180804225226p:plain

X Window System だけで良いとき

デスクトップ環境は不要で X Window System さえあれば良いときは、インストールするパッケージを xserver-xorg にする。

$ sudo apt-get -y install xserver-xorg

あとは SSH を使って X11 Forwarding が有効な状態でログインすれば、リモートで X のアプリケーションが使えるようになる。

いじょう。

Apache Hive 1.x の INSERT 文の仕様でハマった話

今回は、タイトルの通り Apache Hive の 1.x を使っていたとき INSERT 文の仕様でハマった話。 先に概要を説明しておくと Apache Hive の 1.x と 2.x ではサポートする構文が変わっている。 具体的には 1.x では INSERT INTO ... SELECT 文で後続に FROM ... が必要なんだけど 2.x ではそれが不要になっている。

使った環境は次の通り。 ディストリビューションやマネージドサービスは特に使わずに構築してある。

$ cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core) 
$ uname -r
3.10.0-862.3.2.el7.x86_64
$ hive --version
Hive 1.2.2
Subversion git://vgumashta.local/Users/vgumashta/Documents/workspace/hive-git -r 395368fc6478c7e2a1e84a5a2a8aac45e4399a9e
Compiled by vgumashta on Sun Apr 2 13:12:26 PDT 2017
From source with checksum bd47834e727562aab36c8282f8161030
$ hadoop version
Hadoop 2.9.1
Subversion https://github.com/apache/hadoop.git -r e30710aea4e6e55e69372929106cf119af06fd0e
Compiled by root on 2018-04-16T09:33Z
Compiled with protoc 2.5.0
From source with checksum 7d6d2b655115c6cc336d662cc2b919bd
This command was run using /home/vagrant/hadoop-2.9.1/share/hadoop/common/hadoop-common-2.9.1.jar

まずは Hive のシェルを起動しておく。

$ hive

続いて、動作確認に使うテーブルを定義する。

hive> CREATE TABLE example_table (
    >   message STRING
    > );
OK
Time taken: 0.059 seconds

ここで INSERT INTO ... SELECT 構文を使ってデータを追加してみよう。 すると、以下のようなエラーになってしまう。

hive> INSERT INTO TABLE example_table
    > SELECT "Hello, World";
...
FAILED: ParseException line 2:21 Failed to recognize predicate '<EOF>'. Failed rule: 'regularBody' in statement

上記のエラーメッセージで検索しても「スキーマ定義で予約語を使ってるんじゃない?」みたいな関係ないものしか出てこない。

実際には Apache Hive 1.x の場合 INSERT INTO ... SELECT に続いて FROM がないとエラーになるのが原因。 以下ではサブクエリを使って無意味な FROM を無理やり追加することで解決している。

hive> INSERT INTO TABLE example_table
    > SELECT "Hello, World"
    > FROM (SELECT "dummy") dummy;
...
OK
Time taken: 16.697 seconds

これでちゃんとレコードが追加できた。

hive> SELECT * FROM example_table;
OK
Hello, World
Time taken: 0.059 seconds, Fetched: 1 row(s)

ちなみに INSERT INTO ... VALUES を使えば FROM なしでレコード追加ができる。

hive> INSERT INTO TABLE example_table
    > VALUES ("Hello, World 2");
...
OK
Time taken: 17.4 seconds
hive> SELECT * FROM example_table;
OK
Hello, World
Hello, World 2
Time taken: 0.034 seconds, Fetched: 2 row(s)

じゃあ INSERT INTO ... SELECT なんか使わなければいいじゃん!ってなるんだけど、そうもいかない場合がある。 まず、一つ目の理由は設定か何かで解決できると思うんだけど INSERT INTO ... VALUES を使うとテンポラリなテーブルが作られる。

hive> SHOW TABLES;
OK
example_table
values__tmp__table__1
Time taken: 0.008 seconds, Fetched: 2 row(s)

中身はレコードを追加するときに使って VALUES 以降の内容になっている。

hive> SELECT * FROM values__tmp__table__1;
OK
Hello, World 2
Time taken: 0.031 seconds, Fetched: 1 row(s)

まあ上記は良いとして問題がもう一つあって。 複合型 (STRUCT や ARRAY など) を使おうとすると INSERT INTO ... VALUES だとレコードが追加できない。

hive> CREATE TABLE example_table_with_array (
    >   messages ARRAY<STRING>
    > );
OK
Time taken: 0.032 seconds

複合型が含まれるレコードを INSERT INTO ... VALUES で追加しようとすると、次のようなエラーになる。

hive> INSERT INTO TABLE example_table_with_array
    > VALUES (ARRAY("Message1", "Message2"));
FAILED: SemanticException [Error 10293]: Unable to create temp file for insert values Expression of type TOK_FUNCTION not supported in insert/values

INSERT INTO ... SELECT ならエラーにならない。

hive> INSERT INTO TABLE example_table_with_array
    > SELECT ARRAY("Message1", "Message2")
    > FROM (SELECT "dummy") dummy;
...
OK
Time taken: 17.318 seconds
hive> SELECT * FROM example_table_with_array;
OK
["Message1","Message2"]
Time taken: 0.072 seconds, Fetched: 1 row(s)

なので INSERT INTO ... SELECT を使うことになる。

Apache Hive 2.x の場合

ちなみに最初に記述した通り Apache Hive 2.x では FROM をつけなくても良くなっている。 実際に環境を用意して試してみよう。

$ hive --version
Hive 2.3.3
Git git://daijymacpro-2.local/Users/daijy/commit/hive -r 8a511e3f79b43d4be41cd231cf5c99e43b248383
Compiled by daijy on Wed Mar 28 16:58:33 PDT 2018
From source with checksum 8873bba6c55a058614e74c0e628ab022
$ hadoop version
Hadoop 2.9.1
Subversion https://github.com/apache/hadoop.git -r e30710aea4e6e55e69372929106cf119af06fd0e
Compiled by root on 2018-04-16T09:33Z
Compiled with protoc 2.5.0
From source with checksum 7d6d2b655115c6cc336d662cc2b919bd
This command was run using /home/vagrant/hadoop-2.9.1/share/hadoop/common/hadoop-common-2.9.1.jar

複合型が含まれるテーブルを定義する。

hive> CREATE TABLE example_table_with_array (
    >   messages ARRAY<STRING>
    > );
OK
Time taken: 8.459 seconds

Apache Hive 1.x ではエラーになった INSERT INTO ... SELECT を発行してみよう。

hive> INSERT INTO TABLE example_table_with_array
    > SELECT ARRAY("Message1", "Message2");
...
OK
Time taken: 28.527 seconds

今度はエラーにならない。

ちゃんとレコードも追加されている。

hive> SELECT * FROM example_table_with_array;
OK
["Message1","Message2"]
Time taken: 0.223 seconds, Fetched: 1 row(s)

いじょう。

ビッグデータ分析・活用のためのSQLレシピ

ビッグデータ分析・活用のためのSQLレシピ

プログラミング Hive

プログラミング Hive

  • 作者: Edward Capriolo,Dean Wampler,Jason Rutherglen,佐藤直生,嶋内翔,Sky株式会社玉川竜司
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2013/06/15
  • メディア: 大型本
  • この商品を含むブログ (3件) を見る

Python: pandas の DataFrame, Series, Index を拡張する

Python でデータ分析をするときに、ほぼ必ずといって良いほど使われるパッケージとして pandas がある。 そのままでも便利な pandas だけど、代表的なオブジェクトの DataFrame, Series, Index には実は独自の拡張を加えることもできる。 これがなかなか面白いので、今回はその機能について紹介してみる。

ただし、あらかじめ断っておくと注意点もある。 独自の拡張を加えると、本来は存在しないメソッドやプロパティがオブジェクトに生えることになる。 そのため、便利だからといってこの機能を使いすぎると、コードの可読性が低下する恐れもある。 使うなら、後から別の人がコードを読むときにも困らないようにしたい。 具体的には、使用するにしても最小限に留めたり、あるいはパッケージ化やドキュメント化をしておくことが挙げられる。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ python -V
Python 3.6.5
$ pip list --format=columns | grep -i pandas
pandas          0.23.3

下準備

まずは pandas をインストールしておこう。

$ pip install pandas

ここからは Python の REPL を使って解説していく。

$ python

DataFrame を拡張する

まずは一番よく使うであろう DataFrame の拡張方法から。 あんまり実用的な例じゃないけど、ひとまず API がどんな感じになってるかを紹介したいので。

pandas のオブジェクトを拡張するときは、基本的に pandas.api.extensions 以下にある API を用いる。 例えば DataFrame を拡張するなら @pandas.api.extensions.register_dataframe_accessor() デコレータでクラスを修飾する。 次のサンプルコードでは DataFrame に helloworld という名前空間で greet() メソッドと length プロパティを追加している。

>>> import pandas as pd
>>> 
>>> # "helloworld" という名前空間で DataFrame を拡張する
... @pd.api.extensions.register_dataframe_accessor('helloworld')
... class HelloWorldDataFrameAccessor(object):
...     """DataFrameを拡張するためのクラス"""
...     def __init__(self, df):
...         self._df = df
...     # DataFrame#helloworld に greet() メソッドを追加する
...     def greet(self):
...         """標準出力にメッセージを出す"""
...         print('Hello, World!')
...     # DataFrame#helloworld に length プロパティを追加する
...     @property
...     def length(self):
...         """DataFrameの長さを返す"""
...         return len(self._df)
... 

これだけで DataFrame の拡張ができる。

実際に DataFrame のインスタンスを作って、上記の動作を確認してみよう。

>>> df = pd.DataFrame(list(range(1, 11)), columns=['n'])
>>> df
    n
0   1
1   2
2   3
3   4
4   5
5   6
6   7
7   8
8   9
9  10

特に意味はないけど DataFrame#helloworld#greet() メソッドを実行すると標準出力にメッセージが出るようになる。

>>> df.helloworld.greet()
Hello, World!

あとは DataFrame#helloworld#length プロパティを参照すると DataFrame の長さが得られるようになる。

>>> df.helloworld.length
10

たしかに DataFrame に自分で拡張したメソッドやプロパティを生やすことができた。

Series を拡張する

続いては Series の拡張方法を紹介する。 基本的にやることは先ほどと同じなので、次はもうちょっと実用的な例を紹介してみる。

例えば Series をマルチプロセスで並列に処理したい、というシチュエーションを考えてみよう。 使ってるマシンの CPU コアがたくさんあって、扱うデータセットが大きいときは結構やりたくなるんじゃないかな? 典型的には、次のような高階関数を用意するはず。

>>> import multiprocessing as mp
>>> import numpy as np
>>> 
>>> def parallelize(f, data, n_jobs=None):
...     """関数の適用をマルチプロセスで処理する"""
...     if n_jobs is None:
...         # 並列度の指定がなければ CPU のコア数を用いる
...         n_jobs = mp.cpu_count()
...     # データを並列度の数で分割する
...     split_data = np.array_split(data, n_jobs)
...     # プロセスプールを用意する
...     with mp.Pool(n_jobs) as pool:
...         # 各プロセスで関数を適用した結果を結合して返す
...         return pd.concat(pool.map(f, split_data))
... 

続いて、マルチプロセスで適用したい関数を適当に用意する。

>>> def square(x):
...     return x * x
... 

そして、こんな感じで使う。

>>> parallelize(square, df.n)
0      1
1      4
2      9
3     16
4     25
5     36
6     49
7     64
8     81
9    100
Name: n, dtype: int64

先ほどの使い方でも構わないんだけど pandas のオブジェクトから直接呼び出せると便利そうなので拡張してみよう。 次のようにして Series に parallel という名前空間で apply() メソッドを追加する。

>>> # "parallel" という名前空間で Series を拡張する
... @pd.api.extensions.register_series_accessor('parallel')
... class ParallelSeriesAccessor(object):
...     """Seriesを拡張するためのクラス"""
...     def __init__(self, s):
...         self._s = s
...     # Series#parallel に apply() というメソッドを定義する
...     def apply(self, f):
...         """Series に対して関数を並列で適用する"""
...         return parallelize(f, self._s)
... 

すると Series#parallel#apply() メソッドが使えるようになる。

>>> df.n.parallel.apply(square)
0      1
1      4
2      9
3     16
4     25
5     36
6     49
7     64
8     81
9    100
Name: n, dtype: int64

呼び出し方が違うだけで、やっていることは先ほどと変わらない。

Index を拡張する

続いては Index を拡張してみよう。

以下のサンプルコードでは Index が整数という前提で偶数・奇数だけを抜き出す機能を追加している。 また、あんまり実用性がない例になっちゃった。

>>> # "sampling" という名前空間で Index を拡張する
... @pd.api.extensions.register_index_accessor('sampling')
... class SamplingIndexAccessor(object):
...     """Indexを拡張するためのクラス"""
...     def __init__(self, idx):
...         self._idx = idx
...     # Index#sampling に even というプロパティを定義する
...     @property
...     def even(self):
...         return self._idx[self._idx % 2 == 0]
...     # Index#sampling に odd というプロパティを定義する
...     @property
...     def odd(self):
...         return self._idx[self._idx % 2 != 0]
... 

早速試してみよう。

>>> df.index.sampling.even
Int64Index([0, 2, 4, 6, 8], dtype='int64')
>>> df.index.sampling.odd
Int64Index([1, 3, 5, 7, 9], dtype='int64')

ちゃんと偶数・奇数だけ取り出すことができた。

めでたしめでたし。

参考

Extending Pandas — pandas 0.23.3 documentation

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

  • 作者: Wes McKinney,瀬戸山雅人,小林儀匡,滝口開資
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/07/26
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Python: gzip モジュールを使ってデータを圧縮・解凍する

今回は Python の標準ライブラリの gzip モジュールの使い方について。 上手く使えば Python から大きなデータを扱うときにディスクの節約になるかな。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ python -V
Python 3.6.5

まずは Python の REPL を起動しておく。

$ python

基本的な使い方

gzip モジュールの基本的な使い方としては、組み込み関数の open() っぽい使い勝手の gzip.open() 関数を使う。 この関数を通して得られたファイルライクオブジェクトに書き込むと、自動的に書き込んだデータが GZip で圧縮される。

試しに、実際にデータを書き込んでみよう。

>>> import gzip
>>> with gzip.open('example.txt.gz', mode='wt') as fp:
...     fp.write('Hello, World!\n')
... 
14

注意点としては、テキストデータ (ユニコード文字列) を扱うときは mode 引数に t を明示的に指定しなきゃいけない。 これは gzip.open() 関数がデフォルトではバイナリデータを扱うように作られているため。 明示的に t を指定しないとバイナリモードになる。 Python 3 における組み込みの open() 関数はテキストモードがデフォルトなので、ここは気をつける必要がある。

別のターミナルからファイルを確認すると、ちゃんと GZip 圧縮されたファイルができている。

$ file example.txt.gz           
example.txt.gz: gzip compressed data, was "example.txt", last modified: Wed Aug  1 13:23:58 2018, max compression

gzcat コマンドで内容を確認すると、ちゃんと書き込んだ内容が見える。

$ gzcat example.txt.gz 
Hello, World!

元の Python インタプリタに戻って、今度は読み込みをしてみよう。

>>> with gzip.open('example.txt.gz', mode='rt') as fp:
...     fp.read()
... 
'Hello, World!\n'

ちゃんと元の文字列が解凍できた。

gzip コマンドで圧縮したファイルからデータを読み出してみる

続いては Python 以外のアーカイバを使って圧縮したファイルを解凍できるか試してみよう。

gzip コマンドを使って圧縮したファイルを用意しておく。

$ echo "Hello, GZip" > greet.txt
$ gzip greet.txt
$ file greet.txt.gz  
greet.txt.gz: gzip compressed data, was "greet.txt", last modified: Wed Aug  1 13:27:00 2018, from Unix

先ほどと同じようにファイルからデータを読み込んでみよう。

>>> with gzip.open('greet.txt.gz', mode='rt') as fp:
...     fp.read()
... 
'Hello, GZip\n'

ちゃんと読み出せた。

ただし、公式ドキュメントを読むとサポートしていない形式もあるようだ。

13.2. gzip — gzip ファイルのサポート — Python 3.6.5 ドキュメント

バイナリデータを扱ってみる

登場機会として多そうなのは pickle モジュールとの組み合わせかな。 これも試してみよう。

pickle モジュールについては以下の記事で取り扱った。

blog.amedama.jp

以下の辞書データを GZip ファイルとして保存したい。

>>> d = {'message': 'Hello, World!'}

そこで、まずは pickle モジュールを使って、上記をバイト列に変換する。

>>> import pickle
>>> data = pickle.dumps(d)

こんな感じになった。

>>> data
b'\x80\x03}q\x00X\x07\x00\x00\x00messageq\x01X\r\x00\x00\x00Hello, World!q\x02s.'

上記のバイト列を gzip モジュール経由でファイルに書き込む。

>>> with gzip.open('dict.pickle.gz', mode='wb') as fp:
...     fp.write(data)
... 
41

別のターミナルから確認すると、ちゃんと GZip ファイルができている。

$ file dict.pickle.gz 
dict.pickle.gz: gzip compressed data, was "dict.pickle", last modified: Wed Aug  1 13:28:15 2018, max compression

書き込みはできたので、今度は読み込みを。

>>> with gzip.open('dict.pickle.gz', mode='rb') as fp:
...     data = fp.read()
... 
>>> data
b'\x80\x03}q\x00X\x07\x00\x00\x00messageq\x01X\r\x00\x00\x00Hello, World!q\x02s.'

さっきと同じバイト列が得られた。

pickle モジュールに読み込ませると、ちゃんと辞書データが元に戻せた。

>>> pickle.loads(data)
{'message': 'Hello, World!'}

いじょう。

unzip で "need PK compat. v5.1 (can do v4.5)" と言われて解凍できない件

ある日、パスワードつきの ZIP ファイルを macOS 組み込みの unzip コマンドで解凍しようとしたところ、タイトルのようなエラーになった。 今回は、その対処方法と、そもそもどういったときに起こるのかについて。

結論から先に要約してしまうと、次の通り。

  • ZIP ファイルのフォーマットにはバージョンがある
  • unzip コマンドがサポートしているバージョンが不足すると、このエラーになる
  • エラーメッセージにある "PK" とはオリジナルの ZIP アーカイバの名称からきている
  • 必要なバージョンをサポートしている ZIP アーカイバを代わりに使えば解決する

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ unzip | head -n 2
UnZip 6.00 of 20 April 2009, by Info-ZIP.  Maintained by C. Spieler.  Send
bug reports using http://www.info-zip.org/zip-bug.html; see README for details.
$ brew info p7zip
p7zip: stable 16.02 (bottled)
7-Zip (high compression file archiver) implementation
https://p7zip.sourceforge.io/
/usr/local/Cellar/p7zip/16.02_1 (103 files, 4.7MB) *
  Poured from bottle on 2018-07-31 at 23:22:00
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/p7zip.rb

概要

事象としては、次のように解凍できないファイルがある。 エラーメッセージでは PK のバージョンが足りない的なことを言っている。

$ unzip greet.txt.zip 
Archive:  greet.txt.zip
   skipping: greet.txt               need PK compat. v5.1 (can do v4.5)

上記について調べたところ ZIP ファイルのフォーマットにもバージョンがあることを知った。 また、ZIP アーカイバのオリジナル実装の名前は PKZIP という名前で、上記の "PK" はそこからきているらしい。 ようするに macOS 組み込みの unzip コマンドが新しい ZIP フォーマット (v5.1) に対応していないってことのようだ。

解決策

解決策としては、組み込みの unzip の代わりに p7zip をインストールして使えば良い。

$ brew install p7zip

まあ p7zip に限らず v5.1 フォーマットをサポートしているアーカイバなら何でも良いはず。

7za コマンドに e (extract) オプションをつけて解凍する。 パスワードを確認されるのでターミナルに入力する。

$ 7za e greet.txt.zip 

7-Zip (a) [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,4 CPUs x64)

Scanning the drive for archives:
1 file, 215 bytes (1 KiB)

Extracting archive: greet.txt.zip
--
Path = greet.txt.zip
Type = zip
Physical Size = 215

Enter password (will not be echoed):
Everything is Ok

Size:       13
Compressed: 215

今度はちゃんと解凍できた。

$ cat greet.txt
Hello, World

unzip (6.00) で解凍できないファイルの作り方

p7zip を使って unzip コマンドで解凍できないファイルの作り方についても書いておく。

とりあえず圧縮前のファイルを適当に用意する。

$ echo "Hello, World" > greet.txt

あとは最近の暗号化形式を指定してパスワードつき ZIP ファイルを作る。

$ 7za a -tzip -ppassword -mem=AES256 greet.txt.zip greet.txt

これで v5.1 フォーマットの ZIP ファイルができた。

$ file greet.txt.zip 
greet.txt.zip: Zip archive data, at least v5.1 to extract
$ unzip greet.txt.zip 
Archive:  greet.txt.zip
   skipping: greet.txt               need PK compat. v5.1 (can do v4.5)

ちなみに暗号化形式を指定しないで作ると v2.0 フォーマットの ZIP ファイルになった。

$ rm greet.txt.zip
$ 7za a -tzip -ppassword greet.txt.zip greet.txt
$ file greet.txt.zip                            
greet.txt.zip: Zip archive data, at least v2.0 to extract

これなら macOS の unzip コマンドでも解凍できる。

$ unzip greet.txt.zip
Archive:  greet.txt.zip
[greet.txt.zip] greet.txt password: 
 extracting: greet.txt

いじょう。