駄文型

プログラミングとか英語とかの話題を中心にした至極ちゃらんぽらんな日記です。

Pandas.DataFrame (Python) 風のクラスを作って rubygem として公開した

ruby には CSV::Table があるので、基本的なことはできるが、 PandasDataFrame.read_csv のように多機能な CSV の読み込みメソッドが(多分)なかったので、 Pdtable::Table という CSV::Table を拡張したクラスを Gem で作って公開した。はじめは CSV の読み込みを行うメソッドだけのモジュールとして作っていたが、拡張して Pandas の DataFrame のように多機能なクラスにすることもあるかも、と思って CSV::Table の拡張クラスとした。

github.com

pdtable | RubyGems.org | your community gem host

使い方

詳細は README をどうぞ。 CSV::Table のサブクラスなので、 CSV::Table の全メソッドが使える。読み込んだあとの加工や参照は二次元の Array より強い。今のところ列のデータタイプの明示的な指定と、スキップする行の指定ができる。

require 'pdtable'

t = Pdtable::Table.new 'path/to/data.csv', dtype: {col1: String}, skiprows: [1, 3]

動機

  1. 週末でなんか書きたいな
  2. 最近 Ruby 書いてない
  3. Rails で Web アプリ作るのもいいけど、何作っていいかわからない
  4. rubygem でも作ろう
  5. 最近お世話になっている Python を参考にしよう
  6. Pandas の DataFreme は強い。 Ruby にもあっていい
  7. DataFrame.read_csv だけ作ろう

弱点

CSV.table で読み込んでから加工しているので、原理的に CSV.table より速くならない。ベンチマークは測っていないが、加工もデータ量が大きくなるほど遅くなるようになっている。大きなデータを読み込むのではなく、 CSV を手軽に読み込みたいときに使うツールになっている。

以下、メモ。

Rubygem の開発

Gem を作るのは久しぶりだったので、こまめにググりながらの開発になった。すぐにできるだろうと思って書き始めたが、いろいろハマリポイントがあって時間がかかってしまった。ちゃんと TDD 的に開発を進めた。

準備

# gem自信のアップデート
gem update --system
# bundler未インストールの場合はインストール
gem install bundler
# bundlerインストール済の場合はアップデート
gem update bundler

雛形の作成

$ bundle gem test_gem --test=minitest # minitest の場合

gemspec の編集

Gem::Specification.new do |spec|
  spec.name          = "pdtable"
  spec.version       = Pdtable::VERSION
  spec.authors       = ["kohei-kimura"]
  spec.email         = ["kkimura62@icloud.com"]

  # summary と description を編集
  spec.summary       = %q{A Pndas.DataFrame-like class}
  spec.description   = %q{Pdtable is a Pandas.DataFrame-like class that is expanded from CSV::Table. It has some Pandas.DataFrame-like methods, for example `read_csv`.}
  spec.homepage      = "https://github.com/kohei-kimura/pdtable"
  spec.license       = "MIT"

  # rubygem.org に公開する場合は以下のブロックを消す
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
  # to allow pushing to a single host or delete this section to allow pushing to any host.
  # if spec.respond_to?(:metadata)
    # spec.metadata["allowed_push_host"] = " https://rubygems.org"
  # else
    # raise "RubyGems 2.0 or newer is required to protect against " \
    #   "public gem pushes."
  # end

クラスの作成

モジュールにクラスを追加する場合は <module名>/<class名>.rb を作ってそこで定義するといいっぽい。((実際にはクラス名 = モジュール名としていたときの名残で lib/pdtable/pdtable.rb になっている。))

# lib/pdtable/table.rb
require 'csv'

module Pdtable
  class Table < CSV::Table
  end
end

ここが第一のハマリポイントになって、クラス名をモジュール名と同じにしていた。 Gem の公開までは問題なかったのだが、 公開した Gem をインストールして使ってみると問題が生じる。実際に Pdtable を require すると、 Pdtable はクラスじゃないよと怒られる。

テスト、ビルド、リリース

# テスト
# Rakefile に test.verbose = true を追加しておく 
$ rake test

# ビルド
$ rake build

# リリース
# rubygems.org への登録、 API キーの取得、 git commit が必要
$ rake release

Case 文における Class

ハマリポイントその2。列の値を変換するときにクラスごとに処理を分岐させているが、そのときに Case 文を使っていた。 Case の比較は === が使われる。インスタンスのクラスによって分岐させるのではなく、 {col1: String} のような形で列名とクラスを持っているハッシュを Case に渡していたので、意図したように分岐しなかった。その原因にすぐには気付けず、辛かった。

# ※type には Integer, Float, String, DateTime などが入る

# NG
case type
when Integer
  # 処理
when Float
  # 処理
when String
  # 処理
when DateTime
  # 処理
end

# OK
if type == Integer 
  # 処理
elsif type == Float
  # 処理
elsif type == String
  # 処理
elsif type == DateTime
  # 処理
end