💽

Kotlin DataFrameのExcel取込のパフォーマンス比較

2024/08/05に公開

はじめに

6月からログラスでエンジニアをしている遠藤です!

最近話題になっているJetBrains社純正ライブラリのKotlin DataFrameをご存知でしょうか。
Pythonのpandasにインスパイアされているだけあって、機能がとても充実していて二次元配列を扱う際のつらみを解消してくれる使い勝手の良いデータ操作ライブラリです。
今回は他のライブラリ(Apache POI, FastExcel)とのExcelファイル読み込みの性能比較を行なうことで、ログラスで扱っているような大容量のファイルでの使用に耐えうるのかを検証していきます。

関連記事:エクセル読み込みをPOIからFastExcelに置き換えてパフォーマンスを改善する

3行まとめ

  • パフォーマンス面ではFastExcelだけでなくApache POI よりも遅い
  • 他のライブラリと比べて非常に扱いやすくシンプルにコードを書くことができる
  • 数万行程度までのデータを対象とするのであれば十分選択肢に入る

パフォーマンス測定結果

JSONでの出力

Kotlin DataFrame Apache POI FastExcel
メモリ使用量 16.2GB 12.57GB 4.65GB
CPU時間 12,516ms 10,309ms 2,250ms

Data Classへのパース

Kotlin DataFrame Apache POI FastExcel
メモリ使用量 14.82GB 12.51GB 4.31GB
CPU時間 13,277ms 10,537ms 2.598ms

Kotlin DataFrameとは?

公式ドキュメントGitHub

  • 2023年1月にリリースされたJetBrains社純正のデータ操作ライブラリ
  • 機能が充実していて使い勝手が良い
    • セルの値をKotlinの型へ自動変換
    • 行・列・セルへのアクセスがトップレベルから可能
    • 操作系コマンドが充実
  • CSV, JSON, Excelなど、さまざまなソースからデータを読み込むことができる
  • 内部的にはApache POIを使用している

Excelを取り込む

Excelを読み込むには以下の依存関係の追加が必要です。(DataFrame Help Read | Dataframe

implementation("org.jetbrains.kotlinx:dataframe:0.13.1")
implementation("org.jetbrains.kotlinx:dataframe-excel:0.13.1")

Kotlin DataFrameApache POIFastExcel に比べて非常にシンプルなコードでセルの値を取得・出力することができます。

fun parse(inputStream: InputStream) {
    val df = DataFrame.readExcel(inputStream)

    println(df)
}

数字や文字列のカラムは自動でKotlinのDoubleStringへキャストされ、また日付のカラムは自動でkotlinx.datetime.LocalDateTimeへキャストされます。
セルのエラー(#DIV/0!など)はByte型(0x07)として返されるので検知は容易ですが、文字列として出力したい場合は注意が必要です。
またドキュメントに記載のあるExcel取込時に一意に型を指定するオプションですが、バージョン0.13.1時点ではまだリリースされておらず、次回以降のリリースに含まれる機能のようです。

参考:Apache POIやFastExcelのコード

Apache POI

参考元

fun parse(inputStream: InputStream) {
    // 読み込むファイルのサイズが大きい場合は最大値を増やさないとエラーになるため
    IOUtils.setByteArrayMaxOverride(200000000)
    val workbook = XSSFWorkbook(inputStream)
    val sheet = workbook.getSheetAt(0)
    val iterator = sheet.rowIterator()
    val data = iterator.asSequence().map {
        listOf(
            cellToString(it.getCell(0)),
            cellToString(it.getCell(1)),
            cellToString(it.getCell(2)),
            cellToString(it.getCell(3)),
            cellToString(it.getCell(4)),
            cellToString(it.getCell(5)),
            cellToString(it.getCell(6)),
            cellToString(it.getCell(7)),
        )
    }.toList()

    println("PoiParser")
    println(JsonConverter().objectToJsonString(data))
}
    
private fun cellToString(cell: org.apache.poi.ss.usermodel.Cell?): String {
    if (cell == null) return ""

    return when (cell.cellType) {
        CellType.BLANK -> ""
        CellType.BOOLEAN -> cell.booleanCellValue.toString()
        CellType.STRING -> cell.stringCellValue
        CellType.NUMERIC -> { numericToString(cell) }
        CellType.FORMULA -> {
            when (cell.cachedFormulaResultType) {
                CellType.BOOLEAN -> cell.booleanCellValue.toString()
                CellType.STRING -> cell.stringCellValue
                CellType.NUMERIC -> { numericToString(cell) }
                CellType.ERROR -> cell.errorCellValue.toString()
                else -> throw IllegalStateException()
            }
        }
        CellType.ERROR -> cell.errorCellValue.toString()
        else -> throw IllegalStateException()
    }
}
    
private fun numericToString(cell: org.apache.poi.ss.usermodel.Cell): String {
    return if (DateUtil.isCellDateFormatted(cell)) cell.dateCellValue.toString() else cell.numericCellValue.toString()
}

FastExcel

参考元

fun parse(inputStream: InputStream) {
    val workbook = ReadableWorkbook(inputStream, ReadingOptions(true, false))
    val sheet = workbook.firstSheet
    val stream = sheet.openStream()
    val data = stream.map {
        listOf(
            cellToString(it.getCell(0)),
            cellToString(it.getCell(1)),
            cellToString(it.getCell(2)),
            cellToString(it.getCell(3)),
            cellToString(it.getCell(4)),
            cellToString(it.getCell(5)),
            cellToString(it.getCell(6)),
            cellToString(it.getCell(7)),
        )
    }.toList()

    println("FastExcelParser")
    println(JsonConverter().objectToJsonString(data))
}

private fun cellToString(cell: Cell?): String {
    if (cell == null) return ""
    if (cell.text.isNullOrBlank()) return ""
    return if (isDateCell(cell)) cell.asDate().format(DateTimeFormatter.ofPattern("yyyy/M/d")) else cell.text
}

private fun isDateCell(cell: Cell): Boolean {
    val dataFormatId = cell.dataFormatId ?: return false
    // 14..22は日付形式のID
    // 参考: https://www.iso.org/standard/71691.html
    if (dataFormatId in 14..22) {
        return true
    }
    val dataFormatString = cell.dataFormatString ?: return false
    // POIさんの関数を使わせて頂いております
    return DateUtil.isADateFormat(dataFormatId, dataFormatString)
}

DataFrameの出力

DataFrame型は直接printlnで出力でき、以下のように出力されます。


DataFrameにはtoJson() メソッドも用意されており、そちらを用いた場合は以下のようなJSONが出力されます。

[
    {"単純な数値":1.0,"文字列":"あいうえお","日付":"2024-07-21T00:00","関数(結果あり)":1.0,"数値を文字列に":456.0,"関数の結果が空文字":"","関数エラー":7,"NA値":42},
    {"単純な数値":2.0,"文字列":"あいうえお","日付":"2024-07-21T00:00","関数(結果あり)":2.0,"数値を文字列に":456.0,"関数の結果が空文字":"","関数エラー":7,"NA値":42},
    {"単純な数値":3.0,"文字列":"あいうえお","日付":"2024-07-21T00:00","関数(結果あり)":4.0,"数値を文字列に":456.0,"関数の結果が空文字":"","関数エラー":7,"NA値":42},
    {"単純な数値":4.0,"文字列":"あいうえお","日付":"2024-07-21T00:00","関数(結果あり)":8.0,"数値を文字列に":456.0,"関数の結果が空文字":"","関数エラー":7,"NA値":42},
    {"単純な数値":5.0,"文字列":"あいうえお","日付":"2024-07-21T00:00","関数(結果あり)":16.0,"数値を文字列に":456.0,"関数の結果が空文字":"","関数エラー":7,"NA値":42}
]

パフォーマンス測定

上記のコードを用いて実際にライブラリ毎のパフォーマンスを測定してみます。
今回は IntelliJ のプロファイラーを使って確認しました。
使用するExcelは、50万行x8列=19.7MBのサイズで作成しています。

Jsonでの出力

Kotlin DataFrameの結果を見てみましょう。
※読み込むファイルのサイズが大きい場合は最大値を増やさないとエラーになるため、IOUtilsで上限値を上げています。

パフォーマンス測定結果

メモリ使用量:16.2GB

CPU時間:12,516ms

DataFrameインスタンスを作成する箇所でメモリもCPU時間も多く使用しており、Apache POIよりもさらに時間がかかっています。
実はKotlin DataFrameの内部ではApache POIWorkbookFactoryが使用されており、それをDataFrameに詰め直しているため性能が悪くなってしまっています。
POI Streamingのような機能があればと思いましたが現状は存在しないようでした。

Apache POI(メモリ使用量:12.57GB, CPU時間:10,309ms)

メモリ使用量:12.57GB

CPU時間:10,309ms

FastExcel(メモリ使用量:4.65GB, CPU時間:2,250ms)

メモリ使用量:4.65GB

CPU時間:2,250ms

Data Classへのパース

Kotlin DataFrameにはデータをData Classへ容易に変換することができるconvertToメソッドが存在します。
そこで今度はData Classへの詰め替え速度も含めて計測してみます。

Kotlin DataFrame

以下のように@DataSchema@ColumnNameを用いることでヘッダーから自動的にカラムを選択して読み込むことが可能です

@DataSchema
data class ImportedData(
    @ColumnName("単純な数値")
    val simpleNum: Int,
    @ColumnName("文字列")
    val string: String,
    @ColumnName("日付")
    val date: String,
    @ColumnName("関数(結果あり)")
    val formulaResult: Int,
    @ColumnName("数値を文字列に")
    val numToString: String,
    @ColumnName("関数の結果が空文字")
    val resultEmpty: String,
    @ColumnName("関数エラー")
    val formulaError: String,
    @ColumnName("NA値")
    val naNum: String,
)

class DataFrameParser {
    fun parseForClass(inputStream: InputStream) {
        IOUtils.setByteArrayMaxOverride(200000000)
        val df = DataFrame.readExcel(inputStream).convertTo<ImportedData> {
            convert<Int>()
            convert<String>()
            parser { it.format("YYYY/M/d") }
            convert<Int>()
            convert<String>()
            convert<String>()
            convert<String>().with { it.toString() }
        }
        println("DataFrameParser")
        println(df.toList())
    }
}

パフォーマンス測定結果

メモリ使用量:14.82GB

CPU時間:13,277ms

結果を見て分かるようにただ詰め替えているだけでDataFrame生成処理時に差し込むような仕組みではないため、期待したようなパフォーマンスの向上はありませんでした。

Apache POI(メモリ使用量:12.51GB, CPU時間:10,537ms)

メモリ使用量:12.51GB

CPU時間:10,537ms

FastExcel:(メモリ使用量:4.31GB, CPU時間:2.598ms)

メモリ使用量:4.31GB

CPU時間:2.598ms

まとめ

パフォーマンス測定結果

JSONでの出力

Kotlin DataFrame Apache POI FastExcel
メモリ使用量 16.2GB 12.57GB 4.65GB
CPU時間 12,516ms 10,309ms 2,250ms

Data Classへのパース

Kotlin DataFrame Apache POI FastExcel
メモリ使用量 14.82GB 12.51GB 4.31GB
CPU時間 13,277ms 10,537ms 2.598ms

総評

パフォーマンス面ではFastExcelだけでなくApache POIにも後塵を拝する結果となりました。
とはいえ他のライブラリと比べて非常に扱いやすくシンプルにコードを書くことができるので、数万行程度までのデータを対象とするのであれば十分選択肢に入るのではないかと思います。

最後に

今回はJetBrains社純正ライブラリであるKotlin DataFrameのパフォーマンスについてまとめました。
Kotlin DataFrameはまだリリースされてから一年半程度しか経っていないのもあり、今後POI Steamingなどの導入により性能面が改善されることも十分期待できます。
簡単なコードを書いただけでしたがそれでも開発者体験がとても良かったので、今後もアップデートをしっかりとチェックしていきたいです。

参考資料

株式会社ログラス テックブログ

Discussion