onaka0_0suita
@onaka0_0suita (m- yuto)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

【Python3】csvファイル 売上データの集計・書き出しについて

解決したいこと

Python初学者ですが、学習のために既存プログラムの改修を行いたいと考えております。
行いたい内容は、複数のcsvファイルを読み込み、データを整理した後、一つのファイルに書き出しです。
*ページ下部に現状や問題点など記載しております。

ファイルは下記になります。

商品のマスターデータ
→/input/items.csv
商品の売上データ(日毎に別ファイル)
→/input/sales_raw_20161030.csv
 /input/sales_raw_20161101.csv
 ...
 /input/sales_raw_20161106.csv
書き出し後の売上データ
→/output/sales.csv

商品マスターデータ(items.csv)の中身は下記になります。

商品ID、商品名、商品価格

商品の売上データ(sales_raw_YYYYmmdd.csv)の中身は下記になります。

購入ID、ユーザーID、商品ID、個数、販売日時

書き出し後のデータ(sales.csv)は下記になります。

購入ID、ユーザーID、商品ID、商品名、商品価格、個数、販売日時

*商品の売上データの商品IDと商品マスターデータの商品IDを紐付け、紐付く商品名と商品価格を出力させます。

既存のプログラム

main.py
import os

def main():
    with open('output/sales.csv', mode='w', encoding='utf-8') as sales_f:
        for name in os.listdir('input/'):
            if name.startswith('sales_raw_') and name.endswith('.csv') \
                    and "201611" in name:
                with open(os.path.join('input/', name), encoding='utf-8') as f:
                    for row in f:
                        data = row.rstrip().split(',')

                        with open('input/items.csv', encoding='utf-8') as items_f:
                            for row in items_f:
                                data2 = row.rstrip().split(',')
                                if data2[0] == data[2]:
                                    sales_f.write(
                                        ','.join((
                                            data[0],
                                            data[1],
                                            data[2],
                                            data2[1],
                                            data2[2],
                                            data[3],
                                            data[4],
                                        )) + '\n'
                                    )


if __name__ == "__main__":
    main()

既存プログラムの良く無い点は下記になります。
・全てのブロックが入れ子になっている
・一度に覚えておくべき変数が多い
・変数が何を意味しているのかが分かりにくい
・売上生データを1行1行処理するforループの中で、商品マスターデータファイルを読み込んでいるのも問題
・売上生データの行数の数だけ商品マスターデータファイルが開かれて、閉じられている。プログラムの動作としても無駄が多い。

改修途中のプログラム

main.py
import os

def main():
    # 商品マスターデータの読み込み処理
    items = {}
    with open('input/items.csv', encoding='utf-8') as f:
        for row in f:
            data = row.rstrip().split(',')
            item_id = data[0]
            name = data[1]
            price = data[2]
            items[item_id] = {
                'name': name,
                'price': price
            }

    # 売上データの書き出し処理
    with open('output/sales.csv', mode='w', encoding='utf-8') as sales_f:
        for name in os.listdir('input/'):
            if name.startswith('sales_raw_') and name.endswith('.csv') \
                    and "201611" in name:
                with open(os.path.join('input/', name), encoding='utf-8') as f:
                    for row in f:
                        data = row.rstrip().split(',')
                        item_id = data[2]
                        if item_id in items:
                            sales_f.write(
                                ','.join((
                                    data[0],
                                    data[1],
                                    data[2],
                                    items[item_id]['name'],
                                    items[item_id]['price'],
                                    data[3],
                                    data[4],
                                )) + '\n'
                            )

if __name__ == "__main__":
    main()

自分で試したこと

必要なコードのまとまりを設計しました。

・対象の年月の売上生データを読み込む処理の「まとまり」
・商品マスターデータを読み込む処理の「まとまり」
・商品IDから商品(商品名、商品価格)を取得する処理の「まとまり」
・売上データを作成する処理の「まとまり」
・売上データを書き出す処理の「まとまり」

終わっている処理

Step1

・input/items.csvを読み込んでいるwith文を分離する
・「商品マスターデータ」を後で使いやすい形にしておく

終わっていない処理

Step2

・入れ子の処理を分割する
・Step1のプログラムを「目的のデータを作る処理」と「ファイルに書き出す処理」に分離する

現状

Step1の改修を終えている状況になります。
Step2のデータ作成処理と書き出す処理の違いわからず、どのように改修すれば良いのか詰まっている状況になります。
また、入れ子の処理を分割では、売上データの書き出し処理のwith文の中にwith文が入っているので、この箇所を分割するのかなと思うのですが、どのように分割すると良いのかイメージが付かない状況になります。

Step2で終わりではありませんが、とりあえず目先の躓いている問題を解決したいと思っております。
その他に何か必要な情報などありましたら、提示いたします。

長くなってしまい、大変恐縮ではございますが、ご教授頂けますと幸いです。
よろしくお願いいたします。

0

2Answer

自分ならこういう感じにします。

import csv
import glob

def load_items(path):
    items = {}
    with open(path, encoding='utf-8') as f:
        reader = csv.DictReader(f, ['item_id', 'name', 'price'])
        for row in reader:
            items[row['item_id']] = row
    return items

def iter_sales_rows(pattern):
    for path in sorted(glob.glob(pattern)):
        with open(path, encoding='utf-8') as f:
            reader = csv.DictReader(f, ['purchase_id', 'user_id', 'item_id', 'quantity', 'sale_date'])
            for row in reader:
                yield row

def main():
    items = load_items('input/items.csv')

    with open('output/sales.csv', mode='w', encoding='utf-8') as f:
        writer = csv.DictWriter(f, ['purchase_id', 'user_id', 'item_id', 'name', 'price', 'quantity', 'sale_date'], extrasaction='ignore')

        for row in iter_sales_rows('input/sales_raw_201611*.csv'):
            item_id = row['item_id']
            if row['item_id'] in items:
                output_row = {**row, **items[item_id]}
                writer.writerow(output_row)

if __name__ == '__main__':
    main()

ここでは csv.DictReader を使いました。 CSV を1行ずつ読んで辞書で返してくれます。たとえば csv.DictReader(f, ['item_id', 'name', 'price']) で作ると {'item_id: 1, 'name': '商品1', 'price': '100'} のような辞書が次々に得られます。

また、入れ子の処理を分割では、売上データの書き出し処理のwith文の中にwith文が入っているので、この箇所を分割するのかなと思うのですが、どのように分割すると良いのかイメージが付かない状況になります。

まず売上データをすべて読み込んでリストに入れてしまうのも一つの手です。入力ファイルが何百MBもあるというのでなければ十分メモリに乗ります。ここでは別の方法、メモリ消費を抑えたいときの書き方として iter_sales_rows() でジェネレータを使ってみました。 for row in iter_sales_rows('input/sales_raw_201611*.csv'): が1回ループするごとにファイルから1行ずつ読んでいきます。

2Like

Comments

  1. @onaka0_0suita

    Questioner

    @uasi様
    いつもご教授頂きありがとうございます。
    「import glob」や「csv.DictReader」などは、初めて知りました。
    > まず売上データをすべて読み込んでリストに入れてしまうのも一つの手です。
    そのような実装方法もあるのですね。
    ぜひ、参考にさせていただきます!

csvファイルを扱うときは、標準ライブラリのcsvモジュールを使うと楽です。
そちらの環境や実際のcsvファイルがどんなものか分かりませんが、こんな感じでしょうか。

main.py
import os
import csv


def main():
    # 商品マスターデータの読み込み処理
    items = {}
    for row in read_csv("input/items.csv"):
        item_id, name, price = row
        items[item_id] = {"name": name, "price": price}

    # 売上データのcsvファイルを抽出
    sales_csvfile_paths = []
    for filename in os.listdir("input/"):
        if (
            filename.startswith("sales_raw_")
            and filename.endswith(".csv")
            and "201611" in filename
        ):
            sales_csvfile_paths.append(os.path.join("input/", filename))

    # 売上データを作成
    sales_data = []
    for path in sales_csvfile_paths:
        for row in read_csv(path):
            purchase_id, user_id, item_id, volume, datetime = row

            if item_id in items:  # itemsに全商品の情報があるなら、この条件は不要
                sales_data.append(
                    (
                        purchase_id,
                        user_id,
                        item_id,
                        items[item_id]["name"],
                        items[item_id]["price"],
                        volume,
                        datetime,
                    )
                )

    # 売上データの書き出し処理
    write_csv("output/sales.csv", sales_data)


def read_csv(path):
    with open(path, newline="", encoding="utf-8") as f:
        reader = csv.reader(f)
        rows = [row for row in reader]
        # もし空行が存在するなら
        # rows = [row for row in reader if not row == []]

    return rows


def write_csv(path, rows):
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerows(rows)


if __name__ == "__main__":
    main()
1Like

Comments

  1. @onaka0_0suita

    Questioner

    @akarinS様
    ご教授頂き、ありがとうございます!

    > csvファイルを扱うときは、標準ライブラリのcsvモジュールを使うと楽です。
    かしこまりました。今後から意識したいと思います。
    参考にさせていただきます!

Your answer might help someone💌