pythonでLightGBMのランキング学習をやった時気付いたこと

#はじめに
これは[ただの集団 Advent Calendar 2020](https://adventar.org/calendars/5581)の12日目の記事です。
python-packageのLightGBMでランキング学習をやった時に気付いたことをここにまとめます。解釈に間違いがあった場合はご指摘してください。
ランキング学習またLightGBMの使い方がわかってない場合は以下の記事を参考してください

- LightGBMでサクッとランク学習やってみる:  http://szdr.hatenablog.com/entry/2018/12/05/001732
- LightGBM でかんたん Learning to Rank: https://knuu.github.io/ltr_by_lightgbm.html
- LightGBMでランキング学習: https://qiita.com/kondo-k/items/5f1f171da3ac7b98cbcf

#対象読者
- ランキング学習をある程度理解している方
- LightGBMのLambdaRankのサンプルコードを実行した方
- 自分のデータでLightGBMのlambdarankを試したい方

## 1.データをロードしたい時はLibSVMじゃなくてDataFrameでも良い
よくLightGBMのlambdarankサンプルコードと他の記事で使ったオープンデータ(LETORなど)で見てみると、LibSVM形式のデータのケースが多いです。手元でデータがLibSVM形式であればそのまま、lgb.Datasetでデータをロードして使えます。

公式ドキュメントを見ると、LibSVM以外にLightGBMバイナリファイル、ndarray, pandas DataFrameなどからデータ読み込みできます。手元のデータがcsvの形式であれば、わざわざLibSVM形式に変換しなくてもよくて、pandasのDataFrameとして読み込んでも使えます。

```python
def get_query_group(df):
    temp_df = df['q_id'].value_counts().rename_axis('query').reset_index(name='query_count').sort_values('query')
    return temp_df['query_count'].values

train_df = pd.read_csv('train.csv')
#データの最初の2列はlabelとq_idのため
x_train = train_df.iloc[:,2:]
y_train = train_df['label'].values
q_train = get_query_group(train_df)
train_data = lgb.Dataset(x_train, label=y_train, group=q_train)
```


## 2.特徴量は数値じゃないでも良い
同じくオープンデータ(LETORなど)を感じたことです。以下はLETORの「MQ2008」データの例です
![スクリーンショット 2020-12-11 15.39.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410069/3b35ed1a-12bb-3150-6921-22f107f5d11d.png)
各特徴の値は全部数字なので、カテゴリ特徴も数字に変換しないといけないと思うけど、そうではないです。
カテゴリ特徴がstringの場合もそのまま使えます。

事前にDataFrameで対象カテゴリ特徴のデータ型dtypeをcategoryに変換します
学習する時に、カテゴリ特徴のcolumn名をリストとして作成して、lgb.trainで実行する時にcategorical_featureを指定すれば良いです

```python
train_df["doc_type"] = train_df["doc_type"].astype('category')
train_df["query_type"] = train_df["query_type"].astype('category')

# train_dfからtrain_dataに変換

cat_list = ['doc_type', 'query_type']
gbm = lgb.train(params, train_data, valid_sets=[train_data], categorical_feature = cat_list)
```
ちなみに、公式ドキュメントはlgb.Datasetを作る前にカテゴリ特徴をintタイプに変換する方が必要で書いてるけど、カテゴリがstringそのままもlgb.Datasetを構築できます。また、evaluation matrix(NDCGなど)とfeature importanceも変わらないです

```python
Note: You should convert your categorical features to int type before you construct Dataset.
```
## 3.group_columnは便利だけど制限がある
LightGBMのLambdaRankサンプルコードを実行する時に、rank.train.queryみたいにクエリファイルを用意することが必要を気付いました。DataFrameからデータをロードする時にもクエリリスト作成して、Datasetを作る時にgroupで指定することが必要です（上記実装のq_trainです）
学習データがあるなのに、クエリファイルを別に用意しないといけないことは結構面倒と思うので、改善できるかどうかを調べてみると、group_columnを設定すれば学習データの列にあるクエリIDをそのまま使えます。
group_columnに整数を与え、「何列目がクエリID」を指定できます。注意として、ラベル列は数えないです。例えば、0列目はラベル、1列目はクエリIDの場合に、group_columnは0に設定します。実装例は以下になります

```python
train_data = lgb.Dataset(csv_path)
params = {
        'task': 'train',
        'boosting_type': 'gbdt',
        'objective': 'lambdarank',
        'group_column': 0,
    }
gbm = lgb.train(params, train_data, valid_sets=[train_data], verbose_eval=50)
```
group_columnを設定すると結構便利になりましたけど、使う時にいくつ制限があります。
###group_columnは直接ファイルから読み込むlgb.Datasetを作る時しか使えない
lgb.Dataset(data_path)の時だけ使えます

###group_columnを使えるようにファイルの制限があります
csvファイルだけ試したので、csvファイルの制限をまとめます

1. headerがない。pandasのto_csvを使う時に、header=Noneの設定が必要
2. 0列目はラベルの列が必要

###categorical_featureを使える時に制限があります
1. categorical_featureも使えるけど、strが使えなくて数値タイプに変換することが必要
2. headerがなくなるので、カテゴリリストを作成時に列番号を指定することが必要
   
何かのお役に立てれば幸いです

## 参照
- 公式ドキュメント: https://lightgbm.readthedocs.io/en/latest/Python-Intro.html
- https://github.com/microsoft/lightGBM/issues/24