-
Notifications
You must be signed in to change notification settings - Fork 56
CodingChallenge の提出 #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
naoking158
wants to merge
18
commits into
camenergydatalab:master
Choose a base branch
from
naoking158:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
cc0aaf6
feat: 依存ライブラリを追加
naoking158 c3d4b26
feat: ruff の設定ファイルを追加
naoking158 8b4db38
style: ruff のリントとフォーマットを適用
naoking158 82382ad
feat: モデル定義
naoking158 fa6f17b
chore: モデルを admin ページに登録
naoking158 26de8f6
feat: ユーザーインポート機能の実装
naoking158 3ac2dd5
feat: 消費量インポート機能の実装
naoking158 7c3b3c4
feat: チャート関連のファイルを格納するディレクトリ作成
naoking158 8cce9e6
feat: 日ごとの消費量の総量のグラフ生成機能を実装
naoking158 ebed81b
feat: エリア別の日ごとの消費量の総量のグラフ生成機能を実装
naoking158 17cf970
feat: 特定ユーザーの日ごとの消費量の総量のグラフ生成機能を実装
naoking158 1a94626
tests: statistics.py のためのテストを作成
naoking158 facb59d
feat: layout.html を更新
naoking158 49cd823
feat: detail ページの URL を追加
naoking158 f984b36
feat: summary ページを更新
naoking158 ba3998f
feat: details ページを更新
naoking158 5b690ba
docs: add REPORT.md
naoking158 307fad6
fix: インポート機能のバッチ処理がインデックスを正しく扱えていなかったので修正
naoking158 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
|
|
||
| # 作業を確認したブラウザ | ||
|
|
||
| User-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 | ||
|
|
||
| # 技術的な決定 | ||
|
|
||
| ## フロントエンドとバックエンドの分離 | ||
|
|
||
| バックエンド処理はグラフ描画処理であり、それらは全て `chart` ディレクトリ配下にまとめた。 | ||
| フロントエンド処理は `views.py` にまとめ、`chart` 配下の機能を呼び出す形にすることでバックエンドと分離している。 | ||
|
|
||
|
|
||
| ## モデル定義 | ||
|
|
||
| Consumption テーブルのフィールド `consumption` の型は `FloatField` とした。 | ||
| 理由は処理速度を優先するためである。 | ||
|
|
||
| 本アプリケーションでは数値の厳密さは重要ではないため、 | ||
| 処理速度を落としてまで `DecimalField` を選択する必要は無いと考えられる。 | ||
|
|
||
|
|
||
| ## インポート処理 | ||
|
|
||
| データ量が多いため、バッチ処理でレコードの追加・更新を行うようにしている。 | ||
|
|
||
| ### 問題 | ||
|
|
||
| 時間の都合上、データベースに登録しようとしているCSVデータと同じ主キーのレコードが存在する場合、 | ||
| それらに対して一律で上書きするようにした。 | ||
| データベースへの負荷を考えると、登録前に各レコードのフィールドまでチェックして、 | ||
| フィールドが異なる場合のみ更新するようにしたほうが良い。 | ||
|
|
||
| また、テストを書いてデータベース登録前の前処理が期待通り実装できているかチェックしたかったが、 | ||
| こちらも時間の都合上見送った。 | ||
|
|
||
|
|
||
| ## データ取得 | ||
|
|
||
| 極力データベース側で処理を行い、処理後の結果をアプリケーションで取得するようにした。 | ||
| 理由は、トラフィックの占有を防ぐためである。 | ||
|
|
||
| 例えば、消費量 (consumption) のデータ数は `ユーザ数 x 日数 x 48個/日` であり、テストデータだけでも50万個近くにのぼる。 | ||
| 消費量の統計量を計算しようとしたとき、アプリケーション側でこれを行うと消費量の全データを取得する必要があり、トラフィックを占有してしまうことが予想される。 | ||
|
|
||
| そのため、データベース側で先に処理を行い、処理後の結果を取得するようにした。 | ||
|
|
||
| データベース側の処理負荷について考えると、 | ||
| 今回のアプリケーションは大人数で使用するものでは無いため、 | ||
| 負荷については問題にならないと判断した。 | ||
|
|
||
| ### 問題 | ||
|
|
||
| データベース側で統計量 (中央値、10-90%-ile) の計算をするために生のSQLを発行している。 | ||
| 保守性や脆弱性を考えると極力ORMの機能を活用すべきだと考えられる。 | ||
|
|
||
| 時間の都合上実装できなかったが、 | ||
| Aggregate を継承することで実装している例も見つかったため、Django の機能だけで実装することは可能かもしれない。 | ||
| https://gist.github.com/mekicha/b3d5e61683d5a6af642e4549eed95994 | ||
|
|
||
|
|
||
| ## summary ページ | ||
|
|
||
| ユーザーのリストアップはユーザー ID のみにした。 | ||
| 理由は、トラフィックの占有を防ぐためと、描画の高速化のためである。 | ||
|
|
||
| 仮にページネーションと非同期処理を組み合わせて一部のユーザー情報だけを表示する方針であれば、 | ||
| ユーザーの全情報をテーブルで表示しても問題ないと考えられる。 | ||
|
|
||
| しかし、今回は時間の都合上そこまで実装できなかったため、ユーザーIDのみを表示する方針とした。 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| import base64 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. import の並び順が、pep8 準拠で良いです。
|
||
| import io | ||
|
|
||
| import matplotlib.pyplot as plt | ||
| import pandas as pd | ||
| from matplotlib.figure import Figure | ||
|
|
||
| from consumption.chart.statistics import ( | ||
| get_area_daily_percentiles, | ||
| get_area_daily_total_consumptions, | ||
| get_daily_percentiles_for_all, | ||
| get_daily_total_consumptions_for_all, | ||
| get_user_area_daily_consumption_median, | ||
| get_user_daily_total_consumptions, | ||
| ) | ||
|
|
||
|
|
||
| def plot_total_consumption(df: pd.DataFrame, percentiles: pd.DataFrame) -> Figure: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 型ヒントしっかり書かれててgoodです👍 |
||
| fig, ax1 = plt.subplots(figsize=(10, 5)) | ||
|
|
||
| ax1.plot(df['date'], df['daily_total'], label='Total Consumption', color='blue') | ||
| ax1.set_title('Daily Consumption with 10-90 Percentile and Median') | ||
| ax1.set_xlabel('Date') | ||
| ax1.set_ylabel('Total Consumption', color='blue') | ||
| ax1.tick_params(axis='y', labelcolor='blue') | ||
| ax1.grid(True) | ||
|
|
||
| ax2 = ax1.twinx() | ||
| if not percentiles.empty: | ||
| ax2.fill_between( | ||
| percentiles['date'], | ||
| percentiles['p10'], | ||
| percentiles['p90'], | ||
| color='green', | ||
| alpha=0.1, | ||
| label='10-90 Percentile', | ||
| ) | ||
| ax2.plot( | ||
| percentiles['date'], percentiles['p50'], linestyle='--', label='Median', color='green' | ||
| ) | ||
| ax2.set_ylabel('Percentiles and Median', color='green') | ||
| ax2.tick_params(axis='y', labelcolor='green') | ||
|
|
||
| # 凡例の統合 | ||
| lines, labels = ax1.get_legend_handles_labels() | ||
| lines2, labels2 = ax2.get_legend_handles_labels() | ||
| ax1.legend(lines + lines2, labels + labels2, loc='upper left', bbox_to_anchor=(0.1, 0.9)) | ||
|
|
||
| return fig | ||
|
|
||
|
|
||
| def plot_area_consumption(area_totals: pd.DataFrame, area_percentiles: pd.DataFrame) -> Figure: | ||
| fig, ax = plt.subplots(figsize=(10, 5)) | ||
|
|
||
| colors = ['red', 'cyan', 'green', 'blue'] | ||
| color_index = 0 | ||
| ax2 = ax.twinx() | ||
| for area in area_totals['area'].unique(): | ||
| area_data_totals = area_totals[area_totals['area'] == area] | ||
| area_data_percentiles = area_percentiles[area_percentiles['area'] == area] | ||
|
|
||
| ax.plot( | ||
| area_data_totals['date'], | ||
| area_data_totals['daily_total'], | ||
| label=f'{area} Total Consumption', | ||
| color=colors[color_index], | ||
| ) | ||
| ax2.fill_between( | ||
| area_data_percentiles['date'], | ||
| area_data_percentiles['p10'], | ||
| area_data_percentiles['p90'], | ||
| alpha=0.1, | ||
| label=f'{area} 10-90 Percentile', | ||
| color=colors[color_index], | ||
| ) | ||
| ax2.plot( | ||
| area_data_percentiles['date'], | ||
| area_data_percentiles['p50'], | ||
| linestyle='--', | ||
| label=f'{area} Median', | ||
| color=colors[color_index], | ||
| ) | ||
|
|
||
| color_index = (color_index + 1) % len(colors) | ||
|
|
||
| ax.set_title('Daily Consumption with 10-90 Percentile and Median by Area') | ||
| ax.set_xlabel('Date') | ||
| ax.set_ylabel('Total Consumption') | ||
| ax.grid(True) | ||
|
|
||
| ax2.set_ylabel('Percentiles and Median') | ||
| lines, labels = ax.get_legend_handles_labels() | ||
| lines2, labels2 = ax2.get_legend_handles_labels() | ||
| ax.legend(lines + lines2, labels + labels2, loc='upper left', bbox_to_anchor=(0.1, 0.9)) | ||
|
|
||
| return fig | ||
|
|
||
|
|
||
| def plot_user_and_area_consumption( | ||
| user_df: pd.DataFrame, area_df: pd.DataFrame, user_id: int | ||
| ) -> Figure: | ||
| fig, ax = plt.subplots(figsize=(10, 5)) | ||
| ax.plot( | ||
| user_df['date'], user_df['daily_total'], label=f'User {user_id} Consumption', color='blue' | ||
| ) | ||
| ax.plot( | ||
| area_df['date'], | ||
| area_df['p50'], | ||
| label='Area Median Consumption', | ||
| color='red', | ||
| linestyle='--', | ||
| ) | ||
| ax.set_xlabel('Date') | ||
| ax.set_ylabel('Total Consumption') | ||
| ax.grid(True) | ||
| ax.legend(loc='upper left') | ||
|
|
||
| return fig | ||
|
|
||
|
|
||
| def generate_daily_total_consumption_graph() -> str: | ||
| """日ごとの消費量の総量と、中央値と 10-90%-ile をプロットしたグラフを生成""" | ||
| df = get_daily_total_consumptions_for_all() | ||
| percentiles = get_daily_percentiles_for_all() | ||
|
|
||
| with io.BytesIO() as buffer: | ||
| fig = plot_total_consumption(df, percentiles) | ||
| fig.savefig(buffer, format='png') | ||
| buffer.seek(0) | ||
| image_png = buffer.getvalue() | ||
|
|
||
| graph = base64.b64encode(image_png).decode('utf-8') | ||
| return graph | ||
|
|
||
|
|
||
| def generate_daily_total_consumption_graph_by_area() -> str: | ||
| """エリア別に、日ごとの消費量の総量と、中央値と 10-90%-ile をプロットしたグラフを生成""" | ||
| df = get_area_daily_total_consumptions() | ||
| percentiles = get_area_daily_percentiles() | ||
|
|
||
| with io.BytesIO() as buffer: | ||
| fig = plot_area_consumption(df, percentiles) | ||
| fig.savefig(buffer, format='png') | ||
| buffer.seek(0) | ||
| image_png = buffer.getvalue() | ||
|
|
||
| graph = base64.b64encode(image_png).decode('utf-8') | ||
| return graph | ||
|
|
||
|
|
||
| def generate_user_consumption_graph(user_id: int) -> str: | ||
| """ユーザーごとの日ごとの消費量の総量と、エリアの中央値をプロットしたグラフを生成""" | ||
| user_df = get_user_daily_total_consumptions(user_id) | ||
| area_df = get_user_area_daily_consumption_median(user_id) | ||
|
|
||
| with io.BytesIO() as buffer: | ||
| fig = plot_user_and_area_consumption(user_df, area_df, user_id) | ||
| fig.savefig(buffer, format='png') | ||
| buffer.seek(0) | ||
| image_png1 = buffer.getvalue() | ||
|
|
||
| graph = base64.b64encode(image_png1).decode('utf-8') | ||
| return graph | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
型の比較ありがとうございます!
同意です👍