GolangでCSV集計スクリプトを作るならQFrameが便利だった話

こんにちは。 CTO 室の yuina です。

引き続き某CTOからの無茶振りを捌いております。

直近Pythonでの開発が多く、久々にGolangを触ったところ、便利なライブラリを見つけたので、ご紹介します。

なぜGolangを触ることになったかというと、今回の開発の要件上、スクリプトを提供する必要がありました。
Pythonですと、各環境にて環境構築が必要となるため、Golangでバイナリを配布する形が望ましいと判断しました。

スクリプトの内容としては、csvファイルを読み込み、データの集計・変換を行い、別のcsvファイルとして出力するというスクリプトです。
集計・変換に関して、Golangの標準ライブラリでも十分実装可能ですが、コード量が多くなり可読性が下がることが懸念されました。

Pythonであれば、pandasライブラリを使うことで、可読性が高い実装が可能です。
そのため、Golangでも同様のライブラリがないか調査したところ、QFrameというライブラリを見つけました。

QFrameとは

github.com

QFrameは、Golangでデータフレーム操作を可能にするライブラリです。 pandasのように、csvファイルの読み込み、データの集計・変換、csvファイルへの出力が可能です。

サンプルコード

今回は試しに、csvファイルを読み込み、特定の列でグルーピングし、集計した結果を別のcsvファイルとして出力するサンプルコードを作成しました。

標準ライブラリである"encoding/csv"を利用した場合とのコード量の差を比較してみます。

サンプルデータ

  • 処理内容: Category列でグルーピングし、Value列を合計する

  • input.csv

  Category,Value
  A,10
  B,20
  A,15
  B,5
  C,12

標準ライブラリを利用した場合

package main

import (
 "encoding/csv"
 "fmt"
 "log"
 "os"
 "strconv"
)

func main() {
 // 入力ファイルを開く
 inFile, err := os.Open("input.csv")
 if err != nil {
  log.Fatalf("failed to open input.csv: %v", err)
 }
 defer inFile.Close()

 // CSVリーダー作成
 reader := csv.NewReader(inFile)

 // 全データ読み込み
 records, err := reader.ReadAll()
 if err != nil {
  log.Fatalf("failed to read CSV: %v", err)
 }

 if len(records) < 2 {
  log.Fatal("CSV has no data rows")
 }

 // ヘッダー行を確認
 header := records[0]
 var categoryIdx, valueIdx int = -1, -1
 for i, col := range header {
  if col == "Category" {
   categoryIdx = i
  }
  if col == "Value" {
   valueIdx = i
  }
 }
 if categoryIdx == -1 || valueIdx == -1 {
  log.Fatal("CSV must contain 'Category' and 'Value' columns")
 }

 // 集計用map
 groupSums := make(map[string]float64)

 // データ行を処理
 for _, row := range records[1:] {
  category := row[categoryIdx]

  val, err := strconv.ParseFloat(row[valueIdx], 64)
  if err != nil {
   log.Printf("invalid value %q, skip row\n", row[valueIdx])
   continue
  }

  groupSums[category] += val
 }

 // 出力ファイル作成
 outFile, err := os.Create("output.csv")
 if err != nil {
  log.Fatalf("failed to create output.csv: %v", err)
 }
 defer outFile.Close()

 writer := csv.NewWriter(outFile)

 // 出力ヘッダー
 if err := writer.Write([]string{"Category", "TotalValue"}); err != nil {
  log.Fatalf("failed to write header: %v", err)
 }

 // 集計結果を書き出し
 for category, sum := range groupSums {
  row := []string{category, fmt.Sprintf("%.0f", sum)}
  if err := writer.Write(row); err != nil {
   log.Fatalf("failed to write row: %v", err)
  }
 }

 writer.Flush()
 if err := writer.Error(); err != nil {
  log.Fatalf("failed to flush CSV: %v", err)
 }

 log.Println("Aggregation successful: output.csv generated")
}

QFrameを利用した場合

package main

import (
    "log"
    "os"
    "github.com/tobgu/qframe"
    "github.com/tobgu/qframe/config/groupby"
)

func main() {
    // 入力ファイルを開く
    f, err := os.Open("input.csv")
    if err != nil {
        log.Fatalf("failed to open input.csv: %v", err)
    }
    defer f.Close()

    // CSV を QFrame に読み込む
    qf := qframe.ReadCSV(f)
    if qf.Err != nil {
        log.Fatalf("error reading CSV: %v", qf.Err)
    }

    // グループ化:Category 列でグループ化し、Value を合計
    grouped := qf.GroupBy(
        groupby.Columns("Category"),
    ).Aggregate(
        qframe.Aggregation{
            Column: "Value",
            Fn: func(vals []float64) float64 {
                sum := 0.0
                for _, v := range vals {
                    sum += v
                }
                return sum
            },
        },
    )
    if grouped.Err != nil {
        log.Fatalf("error during group+aggregate: %v", grouped.Err)
    }

    // 結果を CSV に出力
    out, err := os.Create("output.csv")
    if err != nil {
        log.Fatalf("failed to create output.csv: %v", err)
    }
    defer out.Close()

    if err := grouped.ToCSV(out); err != nil {
        log.Fatalf("error writing CSV: %v", err)
    }

    log.Println("Aggregation successful: output.csv generated")
}

コード量の比較

  • 標準ライブラリを利用した場合: 約80行
    • ファイルのオープン、エラーハンドリング、CSVの読み書き、データのパース、集計ロジックなど、多くのコードが必要
  • QFrameを利用した場合: 約40行
    • CSVの読み込み、グループ化と集計、CSVへの書き出しが簡潔に記述可能

コード量が半分程度になり、可読性も向上しています。

もっと複雑な集計や変換を行う場合、QFrameの利便性がさらに際立つと思います。

まとめ

Golangでデータフレーム操作を行う場合、QFrameライブラリを利用することで、コード量を削減し、可読性を向上させることができます。
また、Pythonとは異なり、goroutineを活用した並列処理も容易に組み込むことができるため、大規模データの処理に対しても有効ではないかと考えています。

データの集計や変換を多く行うスクリプトをGolangで実装する際には、QFrameの利用を検討してみてください。