1
2

More than 3 years have passed since last update.

pandasと私がプロジェクトで担当した機能の話

Last updated at Posted at 2020-07-05

はじめに

今回6週間でチームビルディングからプロダクト公開までやるプロジェクトに参加させて頂きました。
プロジェクト全体に関しては以下のリンクでまとめておりますのでそちらを御覧ください。
私についてはその記事や過去記事等ご覧になっていただければと思いますが、端的に言うと未経験でエンジニアを目指して就職活動をしているものです。

プロジェクトについてはこちら

この記事では私が担当した機能を実装するにあたって勉強したことをアウトプットしていきます。

pandasについて

pythonのモジュールの一種です。
これを入れるとデータ解析や分析に関する色んな機能が使えるようになります。
詳しくは公式ドキュメントこちらの記事を参照してください。

pandasを使うことが決まったのは初回のMTGで制作物が決定してからでしたが、私はpandasに関しては初見でしたので、プロジェクト6週のうち1週は事前学習としてあててもらいそこで基本的なところを学習して、
あとは作りながらググって調べながら使っていました。
といっても本格的に使いこなせていたわけではなく、主に

  • CSVを読み込んでDataFrameに変換
  • 複数のCSVファイルを読み込んで結合して、書き出す
  • CSVからほしいフィールドの情報だけ取得してそれを返す
  • CSVの編集

という基本的な部分です。
では1つずつ見ていきます。

そもそもCSVファイルとは?

→早い話がEXCELやスプレッドシートのような行と列のある表計算ソフトに使われる書式です。
pandasを使うとこれを読み込んで加工したり、pythonのデータ型をこの形式に変換したりすることができます。
ちなみにpandasがCSVをpythonで扱えるように変換したものがDataFrame型です。

#       価格 売数 在庫率
# りんご 300  10   80%
# みかん 100   2   20%
# メロン 900   6   60%

print()などでDataFrameを表示させると例えば以上のような形で表されます。見たことありますよね、こういう表。

CSVファイルの読み込み

では実際にpandasを触っていきましよう。
まずCSVファイルを読み込みます。
ちなみにcsvファイルとは


client_id,client_name
1,株式会社サンプルB
2,株式会社サンプルI
3,株式会社サンプルF
4,株式会社サンプルD
5,株式会社サンプルE
6,株式会社サンプルC
7,株式会社サンプルG
8,株式会社サンプルA
9,株式会社サンプルH

このようなファイルになります。
1行目にフィールド名カンマ区切りで定義します。
2行目以降はフィールドに挿入されるデータを表しているのがわかると思います。
実際に読み込むには以下のように書きます。

import glob
import pandas as pd

sample_data = pd.read_csv('sample.csv')

# 複数ある場合
df = pd.DataFrame() # 空のインスタンスを生成
file_list = glob.glob(CSV_PATH) # ファイルパスを指定。globモジュールを使うことにより`sample/data*`などと引数を設定してやればそのディレクトリ下にある「data」という文字列が入ったcsvファイルすべてが対象になります。


for file_count in file_list:
    dfn = pd.read_csv(file_count)
    df = pd.concat([df, dfn])

read_csv()メソッドがCSVを読み込むメソッドです、具体的にはCSVファイルをDataFrame型に変換するメソッドです。引数にはファイルパスが入ります。
pandasは基本的にimport pandas as pdとインポートをしてpd.メソッドという形で使うことを覚えておきましょう。
そして、肝心なのは複数のCSVを同時に読み込みたい場合、例えば上記のように複数のCSVファイルを一つずつ結合していって一つのファイルにしたいという場合があると思います。
それをやっているのが上記の所為です。
やっていることは

  1. 空のインスタンスを作成
  2. 変数にCSVファイルが入っているディレクトリのファイルパスを指定する
  3. for文でそれらを順に結合していく

ということになります。concat()はCSVファイルの結合に用いられるメソッドなので空のDataFrameインスタンスに次々とCSVを結合させて1つのCSVにするということがわかりますね。
引数にはリストを指定します。
file_listにはファイルパスが入るわけですがsample/data*と指定しているのでsample下にあるdataという文字列が入ったファイル名のファイルが昇順に繰り返し処理で入っていきます。
例えば


sample
  |
  |-----data1.csv
  |-----data2.csv
  |-----data3.csv

といった場合はdata1.csvから順に処理されるということになります。
concat()は1度に結合はできないのでこのような処理になるわけですね。
ちなみに結合にはmerge()というメソッドもありますが、こちらは同じ主キーを持ちかつ違うColumnを持ったファイルの結合に用いるみたいです。

CSVファイルをpythonの辞書型にしてみる

次は読み込んだCSVをpythonで編集可能な状態にしてみましょう。


import pandas as pd

CSV_COLUMN = ['date','sales','client_id','staff_id']

# 空のリストを定義
data = []
field_names = CSV_COLUMN
# 書き込み不可でファイルを読み込む。
with open(file_path, 'r', encoding='utf-8') as f:
    # 辞書型で各フィールドごとに取得しそれをdataに行ごとに追加していく
    for row in csv.DictReader(f, fieldnames=field_names):
        data.append(row)


CSV_COLUMNにはフィールド名を定義したものが代入してあります。
次にwith open() as f:で読み込み専用でCSVファイルを読み込みます。
この構文はCSVに限らずファイルを読み込むときに使う構文で、先程のはDataFrame型に変換するものでしたが今回は単純にファイルの中身を取得するということになります。
引数にはファイルのパス、オプション、エンコーディングの設定が入り、読み込んだ内容をf変数に代入します。
今回は上記のようにutf-8形式で読み込み専用として読み込んでいます。
そして今度は、読み込んだファイルに対してfor文で中身を取り出していく処理をします。
csv.DictReader()はCSVファイルを辞書型で取得するメソッドです。
処理としてはの例は以下の通りです。

# こういうCSVファイルを読み込んだとすると

date,sales,client_id,staff_id
2020-01-01,8390,8,9
2020-01-02,8712,1,8
2020-01-03,8146,6,8

# こういう感じでdataに格納されていく

[([('date', '2020-01-01'), ('sales', '8390'), ('client_id', '8'), ('staff_id', '9')]),
([('date', '2020-01-02'), ('sales', '8712'), ('client_id', '1'), ('staff_id', '8')]),
([('date', '2020-01-03'), ('sales', '8146'), ('client_id', '6'), ('staff_id', '8')])]


辞書型にしたデータを編集してみる

先程のdataにフィールドを追加してみます。

# 1行目がフィールド名として扱われるのでそこに`tax_salse`というフィールドを追加
data[0]['tax_sales'] = 'tax_sales'

# 次に2行目以降(=各フィールドの実数値の部分)にfor文で新しく追加したフィールドに値を入れていく。
for row in data[1:]:
    # row行目のtax_salseフィールドにsalesフィールドに税率をかけた値を挿入していく
    row['tax_sales'] = int(row['sales']) * (1.0 + TAX_RATE)

異なるCSVファイルをmergeで結合してCSV出力してみる

import pandas as pd

CSV_COLUMN_ADD = ['date','sales','tax_sales','client_id', 'client_name','staff_id','staff_name']

# 辞書型の各データをDataFrame型に変換する。
sales_data = pd.DataFrame(sales_data)
client_data = pd.DataFrame(client_data)
staff_data = pd.DataFrame(staff_data)

# mergeしていく
merge1 = pd.merge(sales_data, client_data, how='outer')
merge2 = pd.merge(merge1, staff_data, how='outer')
# 引数のCSV_COLUMN_ADDの順番通りにフィールドを並び替える
merge_data_result = merge2.loc[:,CSV_COLUMN_ADD]
# indexを削除する
merge_data_result = merge_data_result.drop(merge_data_result.index[0])


merge()もconcat()と同じく引数に結合したい2つのファイルを引数に指定します。
howは結合の方法を指定するオプションです。how='outer'は外部結合の指定でキーを見て、片方のテーブルにしかないデータも全て残すように結合します。
また、merge()すると2行目にフィールド名がindexとして追加されて0行目とダブってしまうことがあるので今回はdrop()メソッドで削除をしています。
配列名.loc[]で列名を参照することができます、これを応用して今回は:,CSV_COLUMN_ADDとすることですべての列をCSV_COLUMN_ADDの順番通りにフィールドを並び替えています。

ここまでがごく基本的なpandasやpythonの使い方です。

応用編

では、ここからはDjangoにて実際にDBとやりとりしながらの使い方を見てみます。
例えば以下のようなモデルがあったとしてそれに伴ってこのような処理をしたいという場合です。

銘柄の情報が格納されているモデル(MutualFundモデル)

from django.db import models
from accounts.models import CustomUser


class MutualFund(models.Model):
    class Meta:
        db_table = 'MutualFund'
        verbose_name_plural = '投資信託情報'

    DELETE_FLAG = ((0, '未削除'), (1, '削除'))

    # id = AutoField(primary_key=True)  # 自動的に追加されるので定義不要
    url = models.CharField('ファンドURL', max_length=255, null=True, blank=True)
    fund_name = models.CharField(
        'ファンド名', max_length=255, null=True, blank=True)
    company = models.CharField('会社名', max_length=255, null=True, blank=True)
    category_obj = models.ForeignKey(
        Category,
        verbose_name='カテゴリー',
        on_delete=models.CASCADE
    )
    rate = models.IntegerField('総合レーティング', null=True, blank=True)
    return_percent = models.FloatField('リターン率(3年)', null=True, blank=True)
    risk = models.FloatField('リスク値(3年)', null=True, blank=True)
    fee = models.FloatField('信託報酬等(税込)', null=True, blank=True)
    net_assets = models.IntegerField('純資産額(百万円)', null=True, blank=True)
    delete_flag = models.IntegerField('削除フラグ', choices=DELETE_FLAG, default=0)

    def __str__(self):
        return self.fund_name

リレーション関係にあるモデルから情報を参照するモデル(Portfolioモデル)

from django.db import models
import sys
import pathlib
# base.pyのあるディレクトリの絶対パスを取得
# current_dir = pathlib.Path(__file__).resolve().parent
# # モジュールのあるパスを追加
# sys.path.append( str(current_dir) + '/../' )

# print(sys.path)
from accounts.models import CustomUser
from fund.models import MutualFund


# 各モデルから情報を参照している。
# customuser_objやmutual_fund_objには参照元の情報が詰まっている。

class Portfolio(models.Model):
    customuser_obj = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
    mutual_fund_obj = models.ForeignKey(MutualFund, on_delete=models.CASCADE)
    amount = models.IntegerField(null=True, blank=True)

処理のコード


# risk_colとreturn_colの仮定義
risk_col = 0
return_col = 0

# リスク差を求める関数


def risk_differ(x):
    return risk_col - x.loc["risk"]


# リターン差を求める関数


def return_differ(x):
    return return_col - x.loc["return_percent"]


def find_fund_near_risk(fund_id, num_fund_obj):
    """
    取得してきたレコードをDataFrameに変換し、新しくフィールドを作ってそこに指定銘柄とのリスク差を絶対値として格納してソートして返す

    Arguments:
        fund_id : str
            銘柄名。
        num_fund_obj : int
            取得件数。

    Returns:
        brand_risk_near : DataFrame
    """
    # レコードを辞書型で取得。
    brand_info = MutualFund.objects.values(
        "id", "company", "fund_name", "risk")

    # DataFrameに変換
    brand_info_df = pd.DataFrame(brand_info)

    # DFから指定銘柄のリスクフィールドを抽出
    find_obj = brand_info_df[brand_info_df["id"] == fund_id]
    risk_col = find_obj["risk"]

    # リスク差の計算結果を入れるフィールドを作る
    brand_info_df["differ"] = np.nan

    # differフィールドにリスク差の値を格納し、値を絶対値化する。
    brand_info_df["differ"] = brand_info_df.apply(risk_differ, axis=1).abs()

    # 引数で指定された銘柄の情報の行を削除
    deleterow = brand_info_df.index[brand_info_df["id"] == fund_id]
    brand_info_df = brand_info_df.drop(deleterow)

    # 少ない順にソートしてdifferフィールドとidフィールドを削除
    brand_info_df = brand_info_df.sort_values("differ")
    brand_info_df = brand_info_df.drop(columns=["id", "differ"])

    # 件数制限
    brand_risk_near = brand_info_df.head(num_fund_obj)

    return brand_risk_near

とはいえいきなりだとわかりにくいので、まずはDjangoにおいてのDBの操作方法から確認していきましょう。
詳しいことやより複雑な指定などはドキュメントかまたは参考記事がわかりやすくまとまっていたのでそちらを参照のほどお願いします。
ここでは、簡単に取り上げていきます。

取得

取得に関しては全件取得、検索して取得の2パターンあります。
全件に関してはmodels.objects.all()でそのモデルのレコードすべて取得となります。
次に検索して取得するパターンですがget()メソッドあるいはfilter()メソッドを使うことになりますが戻り値が違うため基本的には使い分けることになります。
get()は引数に合致したレコードを1件のみをオブジェクトとして返します。
なので、例えばmodels.objects.get(pk=1)のように必ず特定のデータのみ取得したいという場合に使います。
対してfilter()は合致したレコードをオブジェクトのリストとして返します。
なので、基本的にはmodels.objects.filter(name='sample')というように複数登録されていることが想定されるようなレコードを取得してくるときに使います。
ちなみに、当然ですが戻り値はオブジェクトのリストなのでこれを加工することはこのままできませんので後述するvalues()メソッドなどを使うことになります。
filter()get()のようなことをしたい場合はfilter()のあとにfirst()メソッドを続けます。
first()は取得したクエリセットの最初の1件をオブジェクトとして返すというものです。

取得したレコードを変換

取得したレコードのうち一部を辞書型やリストでほしいという場合があると思います。
その場合はvalues()メソッド及びその派生であるvalues_list()メソッドを使っていきます。

例えば以下のような使い方をします。


# モデルからnameフィールドの値がsampleであるレコードを抽出
query = models.objects.filter(name='sample')

# 抽出されたレコードからidフィールドの値をリストで取得する
models_column_id_list = query.values_list('id')

# モデルから直接id、name、emailフィールドのレコードを辞書のリストで取得
models_column_dict_list = models.objects.values("id", "name", "email")


実例

では、冒頭のコードに戻ってみましょう。
今回私は銘柄同士のリスクまたはリターン差を絶対値としてその値が小さい順にソートすることで、
リスク(リターン)が指定した銘柄に似ている銘柄を列挙するという機能を実装しましたがそれが冒頭の以下のコードになります。


from fund.models import MutualFund
from portfolio.models import Portfolio
import pandas as pd
import numpy as np
from django.db.models import Count


# risk_colとreturn_colの仮定義
risk_col = 0
return_col = 0

# リスク差を求める関数


def risk_differ(x):
    return risk_col - x.loc["risk"]


# リターン差を求める関数


def return_differ(x):
    return return_col - x.loc["return_percent"]


def find_fund_near_risk(fund_id, num_fund_obj):
    """
    取得してきたレコードをDataFrameに変換し、新しくフィールドを作ってそこに指定銘柄とのリスク差を絶対値として格納してソートして返す

    Arguments:
        fund_id : str
            銘柄名。
        num_fund_obj : int
            取得件数。

    Returns:
        brand_risk_near : DataFrame
    """
    # レコードを辞書型で取得。
    brand_info = MutualFund.objects.values(
        "id", "company", "fund_name", "risk")

    # DataFrameに変換
    brand_info_df = pd.DataFrame(brand_info)

    # DFから指定銘柄のリスクフィールドを抽出
    find_obj = brand_info_df[brand_info_df["id"] == fund_id]
    risk_col = find_obj["risk"]

    # リスク差の計算結果を入れるフィールドを作る
    brand_info_df["differ"] = np.nan

    # differフィールドにリスク差の値を格納し、値を絶対値化する。
    brand_info_df["differ"] = brand_info_df.apply(risk_differ, axis=1).abs()

    # 引数で指定された銘柄の情報の行を削除
    deleterow = brand_info_df.index[brand_info_df["id"] == fund_id]
    brand_info_df = brand_info_df.drop(deleterow)

    # 少ない順にソートしてdifferフィールドとidフィールドを削除
    brand_info_df = brand_info_df.sort_values("differ")
    brand_info_df = brand_info_df.drop(columns=["id", "differ"])

    # 件数制限
    brand_risk_near = brand_info_df.head(num_fund_obj)

    return brand_risk_near


# レコードを辞書型で取得。
    brand_info = MutualFund.objects.values(
        "id", "company", "fund_name", "risk")

    # DataFrameに変換
    brand_info_df = pd.DataFrame(brand_info)


この部分でDBから取得するフィールドを指定してレコードを取得し、辞書のリストとしてからそれをDataFrame型に変換しています。
そうするとbrand_info_dfには列は各フィールド、行でレコードを表すような表になります。
今回は指定銘柄とその他の銘柄とでリスク(リターン)の差を算出しないといけないのでひとまず全件取得します。


 # DFから指定銘柄のリスクフィールドを抽出
    find_obj = brand_info_df[brand_info_df["id"] == fund_id]
    risk_col = find_obj["risk"]

次にDataFrameを編集します。
今回、idフィールドには主キーの値が入っているのでbrand_info_dfのidフィールドがfund_idと一致する行を特定しているということになります。
そして今度は特定した行のうちriskフィールドの情報が欲しいのでさらにそこから抽出をします。


    # リスク差の計算結果を入れるフィールドを作る
    brand_info_df["differ"] = np.nan

    # differフィールドにリスク差の値を格納し、値を絶対値化する。
    brand_info_df["differ"] = brand_info_df.apply(risk_differ, axis=1).abs()

    # 引数で指定された銘柄の情報の行を削除
    deleterow = brand_info_df.index[brand_info_df["id"] == fund_id]
    brand_info_df = brand_info_df.drop(deleterow)

    # 少ない順にソートしてdifferフィールドとidフィールドを削除
    brand_info_df = brand_info_df.sort_values("differ")
    brand_info_df = brand_info_df.drop(columns=["id", "differ"])

    # 件数制限
    brand_risk_near = brand_info_df.head(num_fund_obj)

    return brand_risk_near


ここでは本格的にDataFrame型の操作を行います。
今回の機能で実装しないといけないことは指定の銘柄とレコードに登録されてある他の銘柄との絶対値を算出し、その値が小さい順にソートするということです。
しかし、私ではクエリセット(DBからデータを引っ張ってくる呪文)だけではその処理を書くことがどうしてもできなかったので、Pandasをつかってこのような形で実装していることになります。
なので、おそらくこれはベターではない可能性がありますので、あまり鵜呑みにはしないでほしいということと、もしクエリセットだけで当該処理を表現できるようなコードがありましたら教えていただけると助かります。

閑話休題、ではコードを見ていきます。

まずは、元々のモデルには絶対値を表すフィールドが存在しないのでDataFrameの方で追加をします。
numpyというライブラリのnp.nanを使い、作ったフィールドにとりあえず欠損値を入れておくことで新しくフィールドを作ります。
次にその欠損値を絶対値に置き換えていきます。
apply()メソッドはDataFrameに関数を適用するメソッドです。
apply(適用する関数、適用する場所)の形で引数をしていします。
適用する場所については、要素・列・行と指定ができて例えば今回のようにaxis=1とするとDataFrameの各行に対して適用さぜるという結果になります。
abs()は絶対値を返すメソッドになります。
risk_differに関しては


def risk_differ(x):
    return risk_col - x.loc["risk"]

と定義しています。
risk_colには指定銘柄のリスク値が入るので、そこから各行のレコードのリスク値を引いてくださいということになります。
こうすることで各行のbrand_info_df["differ"]に絶対値が挿入されます。

ここまでくればあとは体裁を整えていくだけです。
まず、このままだと指定銘柄も表の中に含まれたままになってしまいます。
今回は指定銘柄とのリスク(リターン)の差が小さい=指定銘柄とリスク(リターン)が類似している銘柄をリストアップするという処理なので、
指定銘柄の行は消してしまいます。
まずはdeleterow変数に指定銘柄の行を抽出したものを代入します。
今回はget_loc()メソッドのうちindexを使って指定銘柄の行の行番号を取得してそれを代入する形です。
そして、drop()メソッドの引数にそれを代入することで削除が完了することになります。

次にソートです。
DataFrameをソートするには色々ありますが、今回は値を参照してソートしたいので要素に対してソートをかけるsort_values()メソッドを使っていきます。
引数はソートしたいフィールドを指定します、ちなみに降順にソートしたいのであれば第2引数にascending=Falseとして返します。
あとは実際の画面で表示する際に不必要なフィールドを先程のdrop()メソッドで削除をしています。
最後に、head()メソッドを使って先頭から指定された件数分だけ取得したものをreturnで返せばおしまいです。
あとはこれのリターン版をそのまま作れば作業完了です。

このようにDBから取得してきた情報をDataFrameに変換して、さらに加工したものを返すこともできるということを今回のプロジェクトで学ぶことができました。

応用編その2

ここまではDataFrameを加工してきましたが、取得したレコードに対してフィールドを参照して集計の処理を行うことならDB操作の範疇でやることもできます。
例としては以下の処理になります。


def find_fund_popular_user(fund_id, num_fund_obj):
    """
    引数の銘柄を持っている人がほかに持っている銘柄を抽出

    Arguments:
        fund_id : int
        num_fund_obj : int

    Returns:
        fund_list : list
    """
    # 引数の銘柄からPortfolioモデルを検索し、引数の銘柄を持っているユーザーを抽出
    query = Portfolio.objects.filter(mutual_fund_obj__id__exact=fund_id)

    # ユーザーID(customuser_obj)だけリストで抽出
    query = query.values_list('customuser_obj', flat=True)

    # 抽出したIDが含まれるレコードをすべて抽出する
    customuser_obj_id_list = Portfolio.objects.filter(customuser_obj__in=query)

    # 引数の銘柄を弾く
    customuser_obj_id_list = Portfolio.objects.exclude(
        mutual_fund_obj__id__exact=fund_id)

    # 今度はfund_id(mutual_fund_obj)抽出
    mutual_fund_obj_list = customuser_obj_id_list.values('mutual_fund_obj')

    # 出現回数を集計して引数の取得件数の数だけ上位から取得
    fund_count = mutual_fund_obj_list.annotate(portfolio_fund_count=Count(
        expression='mutual_fund_obj'))

    fund_count_list = fund_count.order_by(
        '-portfolio_fund_count')[0:num_fund_obj]

    # forを使い、fund_count_listの数だけ対応するMutualFundオブジェクトを取得し、空のリストに格納して返す
    fund_list = []
    for fund_record in fund_count_list:
        fund = MutualFund.objects.get(pk=fund_record['mutual_fund_obj'])
        fund_list.append(fund)

    return fund_list



    # 引数の銘柄からPortfolioモデルを検索し、引数の銘柄を持っているユーザーを抽出
    query = Portfolio.objects.filter(mutual_fund_obj__id__exact=fund_id)

    # ユーザーID(customuser_obj)だけリストで抽出
    query = query.values_list('customuser_obj', flat=True)

    # 抽出したIDが含まれるレコードをすべて抽出する
    customuser_obj_id_list = Portfolio.objects.filter(customuser_obj__in=query)

    # 引数の銘柄を弾く
    customuser_obj_id_list = Portfolio.objects.exclude(mutual_fund_obj__id__exact=fund_id)

    # 今度はfund_id(mutual_fund_obj)抽出
    mutual_fund_obj_list = customuser_obj_id_list.values('mutual_fund_obj')


まず、この部分は先程と似たような流れになります。
違うところはリレーション関係にあるモデルから情報を取得しているという点です。
PortfolioモデルはMutualFundモデル及びこの処理では参照しませんが、CustomUserモデルとリレーションしています。
Portfolioモデルは各モデルを参照している側になります。
するとどうなるかというと以下の画像を見てください。

2020-06-27_03h02_47.png

これを見ていただくとわかると思いますが、Customuser objフィールド及びMutual fund objフィールドの部分がプルダウンメニューとなっています。
これはどういうことかというとCustomuser objフィールドには外部キーで参照しているCustomUserモデルのレコードの情報が、同じくMutual fund objフィールドには外部キーで参照しているMutualFundのレコードの情報がすべて詰まっている状態を表しています。
つまり、外部キーさえわかればPortfolioモデルからMutualFundモデルの情報を引っ張ってくることができるということですね。
それをやっているのが上記コードの1行目になります。

今回はまず、引数の銘柄を持っている=銘柄を登録してあるMutualFundモデルでpk=fund_idである銘柄を持っているユーザーを抽出したいので、
Mutual fund objフィールドの中のidの値でfilterをかけるということになります。
この時参照先のあるフィールドの値について参照したい場合は
参照先の情報が入っているフィールド名__参照したいフィールド名__exact=~の形を取ります。
exactは完全一致というオプションですので今回はfund_idと完全一致するものを抽出するということになります。
すると、指定した銘柄を持っているユーザーが抽出されるので今度はvalue_list()メソッドを使って、ユーザーの情報が詰まっているCustomuser objをリストで抽出します。

ここまで来たら、抽出したユーザーが持っている銘柄を抽出したいので改めてPortfolioモデルに対してfilterをかけます。
ここでfilterの検索条件にするために先程リストでCustomuser objを抽出したわけです。
先程の参照表記の応用で参照先の情報が詰まっているフィールド名__in=~とすることで参照先のフィールドに~が含まれている場合という検索条件にすることができます。
抽出したら当然引数の銘柄も含まれてしまっているのでexclude()というメソッドで除外します。
この段階で指定した銘柄を持つユーザーが持つ指定銘柄以外の銘柄を抽出できましたが、集計にはmutual_fund_objフィールドだけあればいいので
最後に、mutual_fund_objフィールドを辞書型に変換しておきます。

ここまで来たらあとは集計の処理です。

クエリやクエリセットに対して集計を行う手段はいくつかあるのですが、今回使用するannotate()はクエリセット対して集計を行うメソッドになります。
クエリセットに対して集計を行うメソッドとしてはaggregate()が他にありますが、annotate()はリレーション関係が存在するモデルに対して使用することになります。
具体的には参照先のモデルの情報が含まれるフィールドに対して集計を行いたいときに使うようです。
今回は指定した引数の銘柄を持つユーザーが他に持っている銘柄を人気順(その銘柄を持っているユーザーが多い順)にソートして表示するという機能を実装したいので、
これまでの過程で準備してきたようにmutual_fund_objに対して集計の処理を行いたいのでannotate()を使うということになるわけですね。
実際のコード部分は以下の通りです、確認してみます。


    # 出現回数を集計して引数の取得件数の数だけ上位から取得
    fund_count = mutual_fund_obj_list.annotate(portfolio_fund_count=Count(
        expression='mutual_fund_obj'))

    fund_count_list = fund_count.order_by(
        '-portfolio_fund_count')[0:num_fund_obj]

    # forを使い、fund_count_listの数だけ対応するMutualFundオブジェクトを取得し、空のリストに格納して返す
    fund_list = []
    for fund_record in fund_count_list:
        # 欲しいのは銘柄のデータなのでfund_count_listに格納されているforでfund_recordに取り出されたmutual_fund_objを使ってMutualFundに対してget()をかける
        fund = MutualFund.objects.get(pk=fund_record['mutual_fund_obj'])
        fund_list.append(fund)

    return fund_list


mutual_fund_obj_listがどのようなものだったかというと


    # 引数の銘柄を弾く
    customuser_obj_id_list = Portfolio.objects.exclude(mutual_fund_obj__id__exact=fund_id)

    # 今度はfund_id(mutual_fund_obj)抽出
    mutual_fund_obj_list = customuser_obj_id_list.values('mutual_fund_obj')

このようなものでした。
それを踏まえてannotate()を説明するとまず

mutual_fund_obj_list.annotate(portfolio_fund_count=Count(expression='mutual_fund_obj'))

この部分ではPortfolioオブジェクトにportfolio_fund_countフィールドを作り、そこでCount()メソッドの実行結果が挿入されるというような処理が行われていると思ってください。
DataFrameで新しいフィールドを作ってそこに計算結果を挿入していく……といった処理を先程やりましたがイメージとしては似たようなものです。
Count(expression='mutual_fund_obj')mutual_fund_objがいくつ出てきたか集計してくださいという処理になります。
このとき集計されるのはmutual_fund_objの中身まで参照した情報です。
なのですごく大雑把に言うと

銘柄Aは5人のユーザーが所有している
銘柄Bは3人のユーザーが所有している

……


と集計されることになります。(上記の例はあくまでイメージです)
あとは、集計結果をorder_byで昇順にソートをすればよいということになります。
[0:num_fund_obj]の部分はnum_fund_objに取得件数が入ります。

ここまで終わったらテンプレートへデータをパスするために、最終的にできたfund_count_listに対してfor文を使い、空のリストにデータを格納していけば作業完了となります。

(pk=fund_record['mutual_fund_obj'])

この部分はfor文で取り出されたmutual_fund_objの情報でpkを指定します。
mutual_fund_objには当然銘柄の主キーの情報も含まれているからということですね。

テンプレートへデータをパスする処理(Views.py)の作成

データが用意できたら今度は画面で表示するためにテンプレートへそれを渡す処理を書いていきます。
コードは以下の通りです。


class FundDetail(LoginRequiredMixin, View):

    """
    ・似たリスクを持つ銘柄及び似たリターンを持つ銘柄の情報を取得する。
    ・引数の銘柄を持っている人がほかに持っている銘柄を抽出
    """

    def get(self, request, fund_id, *args, **kwargs):

        # 取得件数設定
        num_fund_obj = 5

        # 似たリスク及びリターンを持つ銘柄のリストを変数に代入
        brand_risk_near_list = find_fund_near_risk(fund_id, num_fund_obj)
        brand_return_near_list = find_fund_near_return(fund_id, num_fund_obj)

        # DataFrameのヘッダーの情報
        brand_risk_near_header = brand_risk_near_list.columns.tolist()
        brand_return_near_header = brand_return_near_list.columns.tolist()

        # DataFrameのコンテンツの情報
        brand_risk_near_contents = brand_risk_near_list.values.tolist()
        brand_return_near_contents = brand_return_near_list.values.tolist()

        # 引数の銘柄を持っているユーザーが他に持っている銘柄を登録数上位から取得
        fund_popular_list = find_fund_popular_user(fund_id, num_fund_obj)

        context = {
            'risk_header': brand_risk_near_header,
            'risk_contents': brand_risk_near_contents,
            'return_header': brand_return_near_header,
            'return_contents': brand_return_near_contents,
            'popular_user_funds': fund_popular_list,
        }
        return render(request, 'fund/fund_detail.html', context=context)



今回はDataFrame型のデータとオブジェクトを渡すことになりますがそれぞれ渡し方違います。
まず、DataFrame型の場合はviews.pyにおいてヘッダー(フィールドの情報)、コンテンツ(各フィールドの値)とでデータを分けて渡すことになります。
オブジェクトの場合はそのまま渡すことになります。
これらのデータを変数に格納したらcontext内においてテンプレートで呼び出せるように定義をします。
例えばテンプレート上で'brand_risk_near_headerを呼び出したいときには'risk_header'と書くことになります。
ここまで来たらあとはrenderメソッドでテンプレートに
contextを渡してfor`で取り出すという形になります。

テンプレートでは以下の通りになっています。



<div class="row">
    {% comment %} リスクが類似している銘柄情報一覧 {% endcomment %}
    <div class="col">
        <h5>この銘柄とリスクが似ている銘柄</h5>
        <div class="">
            <table class="table">
                    <thead>
                    <tr>{% for row in risk_header %}
                        <th>{{ row }}</th>{% endfor %}</tr>
                    </thead>
                    <tbody>
                    {% for i in risk_contents %}
                        <tr>{% for row1 in i %}
                            <td>{{ row1 }}</td>{% endfor %}</tr>
                    {% endfor %}
                    </tbody>
                </table>
            </div>
        </div>
            {% comment %} リターンが類似している銘柄情報一覧 {% endcomment %}
        <div class="col">
            <h5>この銘柄とリターンが似ている銘柄</h5>
            <div class="">
                <table class="table">
                    <thead>
                    <tr>{% for row in return_header %}
                        <th>{{ row }}</th>{% endfor %}</tr>
                    </thead>
                    <tbody>
                    {% for i in return_contents %}
                        <tr>{% for row1 in i %}
                            <td>{{ row1 }}</td>{% endfor %}</tr>
                    {% endfor %}
                    </tbody>
                </table>
            </div>
        </div>
    </div>
            <div class="">
            <h5>この銘柄を買っている人はこんな銘柄も買っています</h5>
            <div class="">
                <table class="table">
                    <thead>
                    <tr>
                        <th>ファンド名</th>
                        <th>リターン</th>
                        <th>リスク</th>
                    </tr>
                    </thead>

                    <tbody>
                    {% for fund in popular_user_funds %}
                        <tr>
                            <th>
                                <a href=" /fund/detail/{{ fund.id }} ">{{ fund.fund_name }}</a>
                            </th>
                            <th>{{ fund.return_percent }}</th>
                            <th>{{ fund.risk }}</th>
                        </tr>
                    {% endfor %}

                    </tbody>
                </table>
            </div>
        </div>
    </div>

注意しなければならないのはrisk_contentsの取り出し方です。
これはDataFrameにおける各フィールドの値が入ります、values.tolist()でそれをリストに変換して渡しているわけですが
今回やり取りしているデータは例えば以下のように行と列が存在している……というのはもう大丈夫ですね。

#       col1  col2  col3
# row1     0     1     2
# row2     3     4     5

# 上記のDataFrameをdf.values.tolist()で格納すると以下の通りになる

# [[0, 1, 2], [3, 4, 5]]

# ちなみこのDataFrameでcolumns.tolist()を行うと以下の通りに格納される

# # ['col1', 'col2', 'col3']

そうなると例えばrow1のデータを取り出したい! となっても種類としてcol1~3の3種類あり、逆にcol1のデータをを取り出したいとなると、row1~2の場合があると思います。
そうなるとリストへの格納の仕方はcolumns.tolist()での結果のような1次配列ではなく、2次配列ではないとうまく取り出せないので上記の例のような格納のされ方をしているとイメージしておいてください。

オブジェクトのテンプレートでの取り出し方は普通のfor文と同じです。
まずは{% for fund in popular_user_funds %}fund変数に各mutual_fund_objが渡されます。
あとは再三触れてきたように今回渡しているPortfolioモデルのmutual_fund_objMutualFundとリレーションしているので、fund.取り出したいMutualFundモデルのフィールドの情報という形で表記すればOKということになります。

参考

pandas.DataFrameの行番号、列番号を取得
pandas.DataFrame, Seriesをソートするsort_values, sort_index
【Flask】クライアントサイドにpandas.Series、pandas.DataFrameを渡す
pandas.DataFrame, SeriesとPython標準のリストを相互に変換
データ分析で頻出のPandas基本操作
Django データベース操作 についてのまとめ
Djangoドキュメント1
Djangoドキュメント2
Django逆引きチートシート(QuerySet編)
Djangoで、集計処理

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2