読者です 読者をやめる 読者になる 読者になる

LINQやコレクションAPIを使ってCSVファイルからデータ抽出 - C#, F#, Scala, Groovy, Ruby の場合

id:fits:20110702 や id:fits:20110709 にて、SQL を使ってデータ抽出した処理を LINQ やコレクション API を使って実施し直してみました。(ただし、今回は station_g_cd でのソートを実施していない等、以前使った SQL と完全に同じではありません)

今回は C#, F#, Scala, Groovy, RubyJRuby)で実装していますが、どの言語も似たような API を用意しており、同じように実装できる事が分かると思います。

以前使った SQL
SELECT *
FROM (
    SELECT
        pref_name,
        station_g_cd,
        station_name,
        count(*) as lines
    FROM
      CSVREAD('m_station.csv') S
      JOIN CSVREAD('m_pref.csv') P ON S.pref_cd = P.pref_cd
    GROUP BY station_g_cd, station_name
    ORDER BY lines DESC, station_g_cd
)
WHERE ROWNUM <= 10

csv ファイルの内容は id:fits:20110702 を参照。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20110718/

C# の場合(LINQ)

C# 4.0 で LINQ を使って実装してみました。
実装内容は以下の通り。

  • File.ReadAllLines(・・・) で行単位のコレクションを取得
  • Skip でヘッダー行を無視
  • join で都道府県を結合
  • group by でグループ化
    • 匿名型を使ってグルーピング
    • 戻り値の型は IEnumerable>
  • order by でソート
  • Take を使って 10件取得

LINQ を使うと SQL 風に実装できます。

listup_station.cs
using System;
using System.Linq;
using System.IO;
using System.Text;

class ListUpStation
{
    public static void Main(string[] args)
    {
        //都道府県の取得
        var plines = File.ReadAllLines("m_pref.csv", Encoding.Default);
        var prefs = 
            from pline in plines.Skip(1)
                let p = pline.Split(',')
            select new {
                PrefCode = p[0],
                PrefName = p[1]
            };

        //路線数の多い駅の抽出
        var slines = File.ReadAllLines("m_station.csv", Encoding.Default);
        var list = (
            from sline in slines.Skip(1)
                let s = sline.Split(',')
            join p in prefs on s[10] equals p.PrefCode
            group s by new { 
                StationName = s[9],
                PrefName = p.PrefName, 
                StationGroupCode = s[5]
            } into stGroup
            orderby stGroup.Count() descending
            select stGroup
        ).Take(10);

        //結果出力
        foreach(var s in list)
        {
            Console.WriteLine("{0}駅 ({1}) : {2}", s.Key.StationName, s.Key.PrefName, s.Count());
        }
    }
}
実行結果
> csc listup_station.cs
> listup_station.exe
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
東京駅 (東京都) : 10
渋谷駅 (東京都) : 10
池袋駅 (東京都) : 9
大宮駅 (埼玉県) : 9
新橋駅 (東京都) : 7
大船駅 (神奈川県) : 7
上野駅 (東京都) : 7
千葉駅 (千葉県) : 7

F# の場合

F# 2.0.0 では LINQ を使わずコレクションを使って実装してみました。
実装内容は以下の通り。

  • File.ReadAllLines(・・・) で行単位のコレクションを取得
  • Seq.skip でヘッダー行を無視
  • 都道府県を Map.ofSeq で Map 化
  • Seq.groupBy でグループ化
    • レコード定義を使ってグルーピング
  • List.sortWith でソート
    • sortWith を使うために List.ofSeq を使って List 化
  • Seq.take を使って 10件取得

なお、グルーピングの際にレコード定義を使ってますが、Tuple を使っても特に問題ありません。

listup_station.fsx
open System
open System.IO
open System.Text

//レコード定義
type Station = {
    StationName: string
    PrefName: string
    StationGroupCode: string
}

//都道府県の取得
let prefMap = File.ReadAllLines("m_pref.csv", Encoding.Default)
                |> Seq.skip 1
                |> Seq.map (fun l -> 
                        let items = l.Split(',')
                        (items.[0], items.[1])
                    )
                |> Map.ofSeq

//路線数の多い駅の抽出
let lines = File.ReadAllLines("m_station.csv", Encoding.Default)
let list = lines 
            |> Seq.skip 1 
            |> Seq.map (fun l -> l.Split(',')) 
            |> Seq.groupBy (fun s -> 
                    {
                        StationName = s.[9]
                        PrefName = Map.find s.[10] prefMap
                        StationGroupCode = s.[5]
                    }
                ) 
            |> List.ofSeq 
            |> List.sortWith (fun a b -> Seq.length(snd b) - Seq.length(snd a)) 
            |> Seq.take 10

//結果出力
for s in list do
    let st = fst s
    stdout.WriteLine("{0}駅 ({1}) : {2}", st.StationName, st.PrefName, Seq.length((snd s)))
実行結果
> fsi listup_station.fsx
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
東京駅 (東京都) : 10
渋谷駅 (東京都) : 10
池袋駅 (東京都) : 9
大宮駅 (埼玉県) : 9
新橋駅 (東京都) : 7
大船駅 (神奈川県) : 7
上野駅 (東京都) : 7
千葉駅 (千葉県) : 7

Scala の場合

Scala 2.9.0.1 もコレクションで実装してみました。
実装内容は以下の通り。

  • Source.fromFile(・・・).getLines() で行単位のコレクションを取得
  • drop でヘッダー行を削除
  • 都道府県を toMap で Map 化
  • groupBy でグループ化
    • ケースクラスを使ってグルーピング
  • sortWith でソート
  • take を使って 10件取得

なお、グルーピングでケースクラスを使ってますが、F# と同様に Tuple を使っても問題ありません。

listup_station.scala
import scala.io.Source

case class Station(val stationName: String, val prefName: String, val stationGroupCode: String)

//都道府県の取得
val prefMap = Source.fromFile("m_pref.csv").getLines().drop(1).map {l =>
    val items = l.split(",")
    items(0) -> items(1)
}.toMap

//路線数の多い駅の抽出
val lines = Source.fromFile("m_station.csv").getLines()
val list = lines.drop(1).toList.map(_.split(",")).groupBy {s =>
    Station(s(9), prefMap.get(s(10)).get, s(5))
}.toList.sortWith {(a, b) => 
    a._2.length > b._2.length
} take 10

//結果出力
list.foreach {s =>
    printf("%s駅 (%s) : %d\n", s._1.stationName, s._1.prefName, s._2.length)
}
実行結果
> scala listup_station.scala
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
東京駅 (東京都) : 10
渋谷駅 (東京都) : 10
池袋駅 (東京都) : 9
大宮駅 (埼玉県) : 9
大船駅 (神奈川県) : 7
京都駅 (京都府) : 7
新橋駅 (東京都) : 7
千葉駅 (千葉県) : 7

Groovy の場合

Groovy 1.8.0 もコレクションで実装してみました。
実装内容は以下の通り。

  • new File(・・・).readLines() で行単位のコレクションを取得
  • tail でヘッダー行以外を取得
  • 都道府県を collectEntries で Map 化
  • groupBy でグループ化
    • 配列を使ってグルーピング
  • sort でソート
  • List の getAt(Range) を使って 10件取得(asList()[0..9] の箇所)
    • getAt(Range) を使うために entrySet() で取得した Set を asList() で List 化

なお、a <=> b は a.compareTo(b) と同じです。

listup_station.groovy
//都道府県の取得
def prefMap = new File("m_pref.csv").readLines() tail() collectEntries {
    def items = it.split(",")
    [items[0], items[1]]
}

//路線数の多い駅の抽出
def list = new File("m_station.csv").readLines() tail() collect {
    it.split(",")
} groupBy {
    [it[9], prefMap[it[10]], it[5]]
} sort {a, b -> 
    b.value.size <=> a.value.size
} entrySet() asList()[0..9]

//結果出力
list.each {
    println "${it.key[0]}駅 (${it.key[1]}) : ${it.value.size}"
}
実行結果
> groovy listup_station.groovy
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
東京駅 (東京都) : 10
渋谷駅 (東京都) : 10
池袋駅 (東京都) : 9
大宮駅 (埼玉県) : 9
新橋駅 (東京都) : 7
大船駅 (神奈川県) : 7
上野駅 (東京都) : 7
千葉駅 (千葉県) : 7

Ruby の場合

RubyJRuby 1.6.3)では csv モジュールを使わずに実装してみました。
実装内容は以下の通り。

  • IO.readlines(・・・) で行単位のコレクションを取得
  • drop でヘッダー行を削除
  • chop で改行文字を削除
  • 都道府県を Hash[] で Hash 化
  • group_by でグループ化
    • 配列を使ってグルーピング
  • sort でソート
  • take を使って 10件取得
listup_station.rb
#都道府県の取得
prefMap = Hash[IO.readlines("m_pref.csv").drop(1).map {|l| l.chop.split(',')}]

#路線数の多い駅の抽出
list = IO.readlines("m_station.csv").drop(1).map {|l|
    l.chop.split(',')
}.group_by {|s|
    [s[9], prefMap[s[10]], s[5]]
}.sort {|a, b|
    b[1].length <=> a[1].length
}.take 10

#結果出力
list.each do |s|
    puts "#{s[0][0]}駅 (#{s[0][1]}) : #{s[1].length}"
end
実行結果
> jruby listup_station.rb
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
渋谷駅 (東京都) : 10
東京駅 (東京都) : 10
大宮駅 (埼玉県) : 9
池袋駅 (東京都) : 9
新橋駅 (東京都) : 7
大船駅 (神奈川県) : 7
京都駅 (京都府) : 7
岡山駅 (岡山県) : 7