GoでデータフレームパッケージのOSSを作ってみた話

## はじめに
Goのデータフレームパッケージを作り、OSSとして公開しました。この記事では、このOSSをつくるまでの経緯と、今回作った[Goban](https://github.com/hrbrain/goban)の基本的な仕組みの解説をしていきます。

### こんな人に読んでほしい
- OSSを作ってみたいが、何をしたらいいのかわからない
- Goを使ったデータフレームパッケージを探している
- Gobanにコントリビュートをしてみたい

## データフレームとはなにか
そもそも、データフレームについて聞いたことのない方も多いと思います。データフレームについては、以下の説明がわかりやすいです。

> データフレームとは、スプレッドシートのような、行と列からなる二次元のデータ構造です。データフレームは、柔軟かつ直感的にデータを格納し操作できるため、現代のデータ分析において最も一般的なデータ構造です。

引用元：[DataFrames – Databricks](https://www.databricks.com/glossary/what-are-dataframes#:~:text=What%20is%20a%20DataFrame%3F,storing%20and%20working%20with%20data.)より筆者訳

この説明の通り、二次元の表を扱い、様々な集計処理を行うのがデータフレームライブラリの役割です。データフレームライブラリとしては、Pythonの[pandas](https://pandas.pydata.org/)が有名で、データ分析や機械学習の世界で非常によく使われています。

## 開発の経緯
弊社HRBrainではタレントマネジメントシステムの一つのサービスとして、[分析ダッシュボード](https://www.hrbrain.jp/feature/dashboard)を提供しています。その中で、ユーザーが任意のグラフを作成する機能を開発してきました。この機能を実装する上で、データフレームパッケージの必要性を認識し、このパッケージを自ら実装するにいたりました。

### 1.ドメイン知識の整理
グラフ作成機能を実装するにあたって、まず、必要なドメイン知識を整理しました。特に、記述統計やデータの視覚化に関する知識が必要だったので、それらの分野の学習をして、情報を集めました。ドメイン知識を整理していく中で、グラフ作成機能のうち、表の集計処理の部分を、データフレームパッケージを導入することで整理できることがわかりました。

また、RDBを使って集計を行うことも検討しました。しかし、動的なスキーマの表に対する集計なので、RDBの基本的な機能では対応できず、アプリケーションで集計を行うのが望ましいと判断しました。

### 2.既存のOSSの検討
データフレームパッケージが有用であるとわかったところで、既存のOSSの採用を検討しました。検討にあたって、「[Go言語で扱えるデータフレーム厳選4つ](https://qiita.com/mattn/items/71027a6f8fc720eead6e)」などを参考にしました。

既存のパッケージの採用を検討する上で、以下の点を調査しました

- アクティブにメンテナンスされていること
- グループ化の操作ができること

しかし、多くのデータフレームパッケージは、十分にメンテナンスされていないか、必要だったグループ化の操作に対応しておらず、検討から外れました。

唯一、[Gota](https://github.com/go-gota/gota)の採用は最後まで検討しましたが、

- 元の開発者が開発をやめてしまい、2021年11月以降リリースされていないこと[^3]
- 複数カラムでのソートのような、基本的なイシューが放置されていること[^5]
- interface{}を多用するなど、型定義が不適切な箇所があること[^6]

といった理由から採用を見送りました。

データフレームの基本的な機能であれば、実装はそれほど難しくなく、チーム内に知識を蓄積できることや、会社としてコントロールできる範囲にソースをおけるメリットがあると判断し、自ら実装することにしました。

### 3.実装
Gotaのコードを読みつつ、pandasの仕様を参考に実装をしていきました。特に、実装の初期段階においては、Gotaと同じようにつくれば、最低限の実装ができる保証があったので、とても安心感がありました。一方で、開発を進めていく中で、Gotaの実装が不適切な部分を見つけ、それを改善する形でGoban独自の実装をしていきました。

## Gobanのデータフレームの仕組み
今回実装したGobanは、主に２つのオブジェクトによって構成されています。

- Series
    - 同じ型の要素のリスト
- Dataframe
    - 同じ長さのSeriesのリスト

Seriesは、一種類の変数[^4]の値のリストで、平均値や中央値、標準偏差といった基本的な統計量の計算ができます。[^1]

Dataframeは、複数のSeriesのリストまとめて、表として取り扱えるようにしたものです。現状では、グループごとの集計などの操作に対応しています。

### サンプルコード：フルーツ名ごとの平均価格
例えば、フルーツ名と価格の表があるときに、フルーツ名ごとの平均価格を求める処理を考えます。

図示すると、以下のようになります。

#### 処理前
| fruits | prices |
|:-:|:-:|
|　apple | 2  |
|  orange |  3 |
|  orange |  2 |

#### 処理後
| fruits | prices |
|:-:|:-:|
|　apple | 2  |
|  orange |  2.5 |

これをGobanを使って求めるコードは以下のようになります。

```go

	df := dataframe.DataFrame{
		Columns: []series.Series{
			{
				Name: "fruits",
				Elements: element.StringElements{
					{
						Value: "apple",
						IsNull: false,
					},
					{
						Value: "orange",
						IsNull: false,
					},
					{
						Value: "orange",
						IsNull: false,
					},
				},
			},
			{
				Name: "prices",
				Elements: element.NumericElements{
					{
						Value: 2,
						IsNull: false,
					},
					{
						Value: 3,
						IsNull: false,
					},
					{
						Value: 2,
						IsNull: false,
					},
				},
			},
		},
		RecordCount: 3,
	}
	groups, _ := df.GroupBy("fruits")
	newDf, _ := groups.Aggregate(dataframe.AggregationConditions{
		{
			ColumnName: "fruits",
			Method: series.None,
		},
		{
			ColumnName: "prices",
			Method: series.Mean,
		},
	})
	log.Printf("[DEBUG] newDf: %+v", newDf)
```
出力
```bash
[DEBUG] newDf: {Columns:[{Name:fruits Elements:[{Value:apple IsNull:false} {Value:orange IsNull:false}] AggregatedMethod:} {Name:prices Elements:[{Value:2 IsNull:false} {Value:2.5 IsNull:false}] AggregatedMethod:Mean}] RecordCount:2}
```

## 今後の開発
GobanをOSSとして公開したものの、まだ汎用的なデータフレームパッケージには程遠い状態です。多くの基本的なメソッドが未実装である上、データ構造としても大いに改善の余地があると思っています。

一方で、Goのデータフレームパッケージに対するニーズは、これからさらに高まっていくと考えています。アプリケーションが扱うデータが大きくなるにつれて、Pythonではなく、Goのようなパフォーマンスの優れた言語でデータを処理することが合理的になると思うからです。

### コントリビューターの募集
基本的な機能が未実装だからこそ、ジュニアレベルからシニアレベルまで、多くの開発者の方にコントリビュートいただける余地があると考えています。少しでも興味のお持ちの方は、お気軽にイシューやプルリクエスト、ディスカッションへの投稿をいただければと思います。また、開発者向けの[Slackワークスペース](https://join.slack.com/t/goban-developers/shared_invite/zt-1kcni2ecg-eiOObteQXF8Yjmo_3XVwQg)もありますので、興味のあるかたはご参加ください。

なお、Gobanをご利用される場合は、マイナーバージョンのリリース前であり、後方互換性の無い変更がありうる旨をご理解ください。

## おわりに
このOSSをつくることで、記述統計やGoのデータ型についての知見を深めることができ、とても勉強になりました。
最後に、自由に開発させてもらった会社と、レビューとコントリビュートしてくださった同僚に感謝します。ありがとうございました。

## 参考
- [hrbrain/goban: Fast and robust Dataframe package for Go](https://github.com/hrbrain/goban)
- [go-gota/gota: Gota: DataFrames and data wrangling in Go (Golang)](https://github.com/go-gota/gota)
- [pandas - Python Data Analysis Library](https://pandas.pydata.org/)
- [Go言語で扱えるデータフレーム厳選4つ](https://qiita.com/mattn/items/71027a6f8fc720eead6e)

[^1]: 2022年11月17日現在で、対応している統計量は、[相加平均](https://pkg.go.dev/github.com/hrbrain/goban@v0.0.0-20221115101439-88854b3964da/dataframe#Groups.Aggregate)、[個数](https://pkg.go.dev/github.com/hrbrain/goban@v0.0.0-20221115101439-88854b3964da/dataframe#Groups.Aggregate)、[合計](https://pkg.go.dev/github.com/hrbrain/goban@v0.0.0-20221115101439-88854b3964da/dataframe#Groups.Aggregate)のみ。
[^3]: [Looking for contributors · Issue #78 · go-gota/gota](https://github.com/go-gota/gota/issues/78)
[^4]: ここでの”変数”はプログラミング言語における"変数"とは別の概念。詳細は、「[統計用語と調査用語](https://www2.kokugakuin.ac.jp/~ogiso/spss/staterm.html)」などを参照。
[^5]: [Incorrect output when sorting on multiple columns using DataFrame.Arrange(). · Issue #61 · go-gota/gota](https://github.com/go-gota/gota/issues/61)
[^6]: https://github.com/go-gota/gota/blob/f70540952827cfc8abfa1257391fd33284300b24/dataframe/dataframe.go#L391