【Django】アップロードされたcsvをpandasで処理して返却する

## はじめに
pythonでのデータ処理、機械学習を勉強中の初心者です。

今回は作成した回帰モデルを用い、アップロードされたテストファイルを読み込み、Notebookで行うようなpandasでのデータ処理を行った結果のcsvファイルをweb上でダウンロードする機能を持つアプリを作成しました。

その際にcsvの読み込み、返却ができるまでかなり詰まったので記録します。

ネット上にはDjangoでpandasを使う方法があまり見当たらず、いろいろと調べて試しながら完全に自己流で作ったので至らぬ点もあるかとは思いますがご容赦ください。

## 環境と前提
Windows10
django 3.1.7
python 3.9.2

* プロジェクト作成
```
$ django-admin startproject myproject
$ cd myproject
$ python manage.py startapp app
```

* ディレクトリ構成

```
myproject/
  app/
    __pycache__/
    migrations/
    static/
      files/ (new)
        train_x.csv
        train_y.csv
    __init__.py
    admin.py
    apps.py
    forms.py (new)
    functions.py (new)
    models.py
    tests.py
    views.py
  myproject/
    __pycache__/
    __init__.py
    asgi.py
    settings.py
    urls.py
    wsgi.py
static/
templates/
  index.html
db.sqlite3
manage.py

```
* 目標イメージ
![image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640807/de451fd6-929f-496b-5b8e-4a8aebffe7f1.jpeg)

## フォームの作成
forms.pyを作成します。

```forms.py
from django import forms

class UploadForm(forms.Form):
    testfile = forms.FileField()
```
これをhtmlで使うとファイルを選択するフォームが生成

```index.html
<div class="form-group">
  <form method="POST" class="form" enctype="multipart/form-data">  
    {% csrf_token %}  
    <div class="file-upload-wrapper">
      <p>テストファイルを選択<br \>
        {{ form.testfile }}
      </p>   
    </div> 
```
最後にurls.pyの設定

```urls.py
from django.contrib import admin
from django.urls import path
from app import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index, name='index'), (追加)
]

```
settings.pyの設定

```settings.py
# 省略

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app', # 追加
]
# 省略
TEMPLATES = [
    {
        # 省略
        'DIRS': [BASE_DIR, 'templates'],
        # 省略
    },
]
```
`$python manage.py runserver`実行後ローカルホストにアクセスするとファイル選択のフォームができています。

## アップロードされたファイルの処理
今回はfunctions.pyにファイル処理の関数を書き、それをviews.pyで使っていきます。

```views.py
from django.shortcuts import render
from django.http import HttpResponse
from django.template import loader
from app.forms import UploadForm
from app.functions import process_file,to_csv
import csv,io
import pandas as pd

# Create your views here.
def index(request):
    if request.method == 'POST':
        upload = UploadForm(request.POST, request.FILES)
        if upload.is_valid():
            data = pd.read_csv(io.StringIO(request.FILES['testfile'].read().decode('utf-8')), delimiter=',')
            df = process_file(data)

            response = to_csv(df)

            return response
    else:
        upload = UploadForm()
        return render(request, "index.html", {'form':upload})
```
* `data = pd.read_csv(io.StringIO(request.FILES['testfile'].read().decode('utf-8')), delimiter=',')`
* 単に`pd.read_csv`だけでは読み取れず、いろいろ試しまくった結果これでcsvの読み取りができました。

* `process_file()`：pandasでデータを処理
* `to_csv()`：データフレームをcsvファイルにしてダウンロード

## データの処理、ダウンロード

```functions.py
import pandas as pd
import numpy as np
import sklearn,csv,re,os
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from django.http import HttpResponse
from myproject.settings import BASE_DIR

def process_file(data):
    train_x = pd.read_csv(os.path.join(BASE_DIR, 'app/static/files/train_x.csv'))
    train_y = pd.read_csv(os.path.join(BASE_DIR, 'app/static/files/train_y.csv'))
    test = data
    
    #　自由にデータ処理

    #　最後にデータフレームとして返す
    df_result = ...

    return df_result

def to_csv(df):
    response = HttpResponse(content_type='text/csv; charset=UTF-8')
    response['Content-Disposition'] = 'attachment; filename="result.csv"'
    
    df.to_csv(path_or_buf = response, encoding = 'utf-8-sig', index=False)
    
    return response
```
* データ処理に必要なモジュールをそれぞれインポート
* process_file()
  * trainデータをmyproject/app/static/files/から読み込み
  * ローカルでは`pd.read_csv("app/static/files/~")`でも可能
  * サーバーにデプロイするときは`BASE_DIR/app/static/files/~`となるように設定
  * （ちなみにBASE_DIRはsettings.pyで定義され、デフォルトではmanage.pyの存在するディレクトリ。この例ではmyproject/になります。）
  * Notebookで行っているような処理を自由に、最後にデータフレーム型で返してあげます。

* to_csv()
  * `process_file`から返却されたデータフレームをcsvに変換しダウンロード
  * HttpResponseとして"result.csv"という名前のcsvファイルを返す
  * attachmentをつけることで、フォームのボタンをクリックすると自動的にダウンロードが開始
  * `path_or_buf = response`とすることでデータフレームをcsvに変換できるように


```index.html
<div class="form-group">
  <form method="POST" class="form" enctype="multipart/form-data">  
    {% csrf_token %}  
    <div class="file-upload-wrapper">
      <p>テストファイルを選択<br \>
        {{ form.testfile }}
      </p>
    </div> 
    <p>テストファイルをアップロードし、予測結果の'result.csv'ファイルをダウンロードする</p>
    <button type="submit" class="save btn btn-outline-primary">Upload and Download</button>  
  </form> 
</div>
```
フォームボタンをつけて完成

## 補足
* アップロードされたファイルの形式チェック等は省略しています。

* Djangoでpandasを使用する方法を調べていると、django-pandasというライブラリをインストールして使うという説明しか見当たらなかったのですが、python標準モジュールのpandasでもなんとかできました。