毎週の売上CSV、広告レポート、ポイント実績、アフィリエイト成果、在庫一覧を、毎回Excelで開いていませんか。
フィルターをかける。合計する。表を整える。前回との差分を見る。
この作業は一回だけなら小さく見えますが、毎週・毎日続くと「判断する前に疲れる作業」になります。
この記事では、PythonでCSVを自動集計する基本パターンを、初心者向けにステップ・バイ・ステップで解説します。
目的は、単なる時短ではありません。
ブログ、ROOM、物販、広告運用、ポイント案件のような小さな収益源では、数字を見る頻度が改善速度に直結します。
CSV集計を自動化できると、次の状態を作れます。
- 売上・クリック・ポイントを毎回同じルールで集計できる
- 手作業による転記ミスや計算ミスを減らせる
- 前日比、チャネル別、商品別などの改善ポイントを見つけやすくなる
- 人間は「集計」ではなく「次に何を直すか」に集中できる
なお、この記事は一般的な情報提供です。収益や投資成果を保証するものではありません。自動化は判断材料を整える技術であり、成果はデータ品質、商品、流入、運用条件によって変わります。
CSV自動集計の全体像:読む、整える、まとめる、出す
CSVとは、カンマ区切りの表データです。
たとえば、次のようなファイルです。
date,channel,revenue,points
2026-07-01,blog,120,1
2026-07-01,room,0,3
2026-07-02,mail,320,0
PythonでCSVを自動集計する流れは、大きく4工程です。
読む
sales.csvのようなCSVファイルをPythonで開く。整える
"120"のような文字列を、計算できる数値に変換する。まとめる
チャネル別、日付別、商品別などで合計・件数・平均を出す。出す
集計結果をsummary.csv、Markdown、Excel、メール、Slackなどに出力する。
この4工程を一度作ると、翌日以降は同じルールで何度でも処理できます。
手作業で毎回集計する状態から、機械が定点観測する状態へ移せます。
Hiroの検証ログ:1,200行のCSVは標準ライブラリだけでも集計できた
一般論だけで終わらせないため、Hiroの作業環境で簡単な検証ログを残しました。
検証条件
- 実行日:2026-07-12
- OS:Windows 10
- Python:3.11.9
- データ:サンプルCSV 1,200行
- 列:
date,channel,revenue,points - 集計内容:チャネル別の件数、売上合計、ポイント合計
- 測定方法:Python標準ライブラリ
csvとtime.perf_counter()
実測ログ
python=3.11.9
platform=Windows-10-10.0.19045-SP0
rows=1200
elapsed_ms=2.564
blog {'rows': 300, 'revenue': 34200, 'points': 300}
mail {'rows': 300, 'revenue': 34200, 'points': 300}
room {'rows': 300, 'revenue': 34200, 'points': 1800}
sns {'rows': 300, 'revenue': 34200, 'points': 1800}
これは上記環境での一回の実測です。PC性能、保存先、文字コード、ウイルス対策ソフト、ファイルサイズによって処理時間は変わります。
ただし、1,200行程度のCSVであれば、Python標準ライブラリだけでも十分に実用的な速度で集計できることは確認できました。
初心者が最初に作る自動集計としては、まずcsvモジュールで十分です。
今回作るもの
この記事では、次の入力CSVを読み込みます。
date,channel,revenue,points,status
2026-07-01,blog,120,1,approved
2026-07-01,room,0,3,pending
2026-07-02,mail,320,0,approved
2026-07-02,sns,80,10,rejected
そして、次のような集計結果を作ります。
channel,rows,approved_revenue,pending_revenue,rejected_rows,points
blog,1,120,0,0,1
room,1,0,0,0,3
mail,1,320,0,0,0
sns,1,0,0,1,10
ポイントは、単純に売上を合計するだけではなく、approved、pending、rejectedを分けることです。
収益化ログでは、承認待ちや否認を確定収益に混ぜると判断を誤ります。
なお、この例のpointsはステータスに関係なく総量として集計しています。実務では、確定ポイントと承認待ちポイントを分けた方がよいケースもあります。
ステップ1:CSVの列名と意味を確認する
最初にやるべきことは、コードを書くことではありません。
CSVの中身を確認することです。
最低限、次を確認します。
- 1行目に見出しがあるか
- 必要な列が存在するか
- 売上やポイントが数値として入っているか
- 日付形式が統一されているか
- 空欄、
-、N/Aなどの例外値があるか - 文字コードがUTF-8、Shift_JIS、CP932のどれか
- 確定、承認待ち、否認などのステータス列があるか
今回の必須列は次の5つにします。
date
channel
revenue
points
status
それぞれの意味は次の通りです。
| 列名 | 意味 | 使い道 |
|---|---|---|
date | 発生日 | 日別・週別集計 |
channel | 流入元 | ブログ、ROOM、SNS、メールなどの比較 |
revenue | 売上 | 確定収益・見込み収益の集計 |
points | ポイント | ポイント案件やROOM運用の成果確認 |
status | 状態 | 承認済み、承認待ち、否認の分離 |
ここで列名が曖昧だと、後の自動化が壊れます。
たとえば、ある日はrevenue、別の日は売上という列名になるCSVは、そのままでは安定運用できません。
ステップ2:Python標準ライブラリでCSVを読む
まずはCSVを読み込んで、1行ずつ表示します。
import csv
with open("sales.csv", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
print(row)
csv.DictReaderを使うと、1行のデータを辞書として扱えます。
たとえば、CSVに次の行があるとします。
2026-07-01,blog,120,1,approved
Python側では、次のように取り出せます。
row["channel"]
row["revenue"]
row["status"]
初心者は、最初からpandasを使わなくても構いません。
標準ライブラリで「読む、変換する、集計する、出力する」の流れを理解しておくと、後からpandasに移ったときも処理の意味を見失いにくくなります。
ステップ3:必須列があるかチェックする
実務では、CSVの列名が変わることがあります。
列名が変わったまま処理を続けると、エラーになるか、間違った列を集計してしまいます。
先に必須列チェックを入れます。
import csv
required_columns = {"date", "channel", "revenue", "points", "status"}
with open("sales.csv", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
actual_columns = set(reader.fieldnames or [])
missing = required_columns - actual_columns
if missing:
raise ValueError(f"必要な列がありません: {sorted(missing)}")
このチェックを入れるだけで、次のような事故を早期に止められます。
revenue列が売上に変わっていたpoints列が削除されていた- CSVの1行目が見出しではなかった
- 別の種類のCSVを誤って投入した
自動化で重要なのは、失敗しないことだけではありません。
おかしい入力を、おかしいと分かる形で止めることです。
ステップ4:文字列を数値に変換する
CSVから読んだ値は、基本的に文字列です。
revenue = row["revenue"]
この時点のrevenueは、数値の120ではなく、文字列の"120"です。
合計するには、int()で整数に変換します。
revenue = int(row["revenue"])
points = int(row["points"])
ただし、実務CSVには空欄や-が混じります。
そこで、変換関数を用意します。
def to_int(value):
if value is None:
return 0
value = str(value).strip()
if value in {"", "-", "N/A", "null"}:
return 0
return int(value)
この関数は、空欄や-を0として扱います。
ただし、何でも0にするのは危険です。
たとえば、"1,200"のようなカンマ付き数値や、"120円"のような単位付き文字列が混じる場合、単純なint()では落ちます。
本番運用では、変換できなかった行数や0に置き換えた行数をログに残すのが安全です。
ステップ5:チャネル別に集計する
次に、blog、room、mail、snsのようなチャネル別に集計します。
import csv
from collections import defaultdict
required_columns = {"date", "channel", "revenue", "points", "status"}
summary = defaultdict(lambda: {
"rows": 0,
"approved_revenue": 0,
"pending_revenue": 0,
"rejected_rows": 0,
"points": 0,
})
def to_int(value):
if value is None:
return 0
value = str(value).strip()
if value in {"", "-", "N/A", "null"}:
return 0
return int(value)
with open("sales.csv", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
actual_columns = set(reader.fieldnames or [])
missing = required_columns - actual_columns
if missing:
raise ValueError(f"必要な列がありません: {sorted(missing)}")
for row in reader:
channel = row["channel"].strip()
status = row["status"].strip().lower()
revenue = to_int(row["revenue"])
points = to_int(row["points"])
summary[channel]["rows"] += 1
summary[channel]["points"] += points
if status == "approved":
summary[channel]["approved_revenue"] += revenue
elif status == "pending":
summary[channel]["pending_revenue"] += revenue
elif status == "rejected":
summary[channel]["rejected_rows"] += 1
else:
raise ValueError(f"未知のstatusです: {status}")
for channel, values in summary.items():
print(channel, values)
ここでは、売上をすべて同じ箱に入れていません。
approvedは確定収益pendingは見込み収益rejectedは否認件数
として分けています。
収益化データでは、この分離が重要です。
承認待ちを確定収益に混ぜると、実態より良く見えます。逆に、否認件数を記録しないと、案件や流入元の質が悪化していることに気づけません。
ステップ6:集計結果をCSVに出力する
ターミナルに表示するだけでは、後から比較しにくくなります。
集計結果もCSVに保存します。
with open("summary.csv", "w", encoding="utf-8", newline="") as f:
fieldnames = [
"channel",
"rows",
"approved_revenue",
"pending_revenue",
"rejected_rows",
"points",
]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for channel, values in sorted(summary.items()):
writer.writerow({
"channel": channel,
"rows": values["rows"],
"approved_revenue": values["approved_revenue"],
"pending_revenue": values["pending_revenue"],
"rejected_rows": values["rejected_rows"],
"points": values["points"],
})
ここまでできれば、sales.csvを置き換えるたびにsummary.csvを作れます。
次の段階では、Windowsのタスクスケジューラ、GitHub Actions、cron、Cloud Runなどで定期実行すれば、人間が実行ボタンを押す回数も減らせます。
完成版コード
初心者がそのまま試せるように、ここまでの処理を1つにまとめます。
import csv
import time
from collections import defaultdict
from pathlib import Path
INPUT_PATH = Path("sales.csv")
OUTPUT_PATH = Path("summary.csv")
ENCODING = "utf-8"
REQUIRED_COLUMNS = {"date", "channel", "revenue", "points", "status"}
def to_int(value):
if value is None:
return 0
value = str(value).strip()
if value in {"", "-", "N/A", "null"}:
return 0
return int(value)
def main():
start = time.perf_counter()
summary = defaultdict(lambda: {
"rows": 0,
"approved_revenue": 0,
"pending_revenue": 0,
"rejected_rows": 0,
"points": 0,
})
with INPUT_PATH.open(encoding=ENCODING, newline="") as f:
reader = csv.DictReader(f)
actual_columns = set(reader.fieldnames or [])
missing = REQUIRED_COLUMNS - actual_columns
if missing:
raise ValueError(f"必要な列がありません: {sorted(missing)}")
input_rows = 0
for row in reader:
input_rows += 1
channel = row["channel"].strip()
status = row["status"].strip().lower()
revenue = to_int(row["revenue"])
points = to_int(row["points"])
if not channel:
raise ValueError(f"channelが空です: row={input_rows}")
summary[channel]["rows"] += 1
summary[channel]["points"] += points
if status == "approved":
summary[channel]["approved_revenue"] += revenue
elif status == "pending":
summary[channel]["pending_revenue"] += revenue
elif status == "rejected":
summary[channel]["rejected_rows"] += 1
else:
raise ValueError(f"未知のstatusです: row={input_rows}, status={status}")
with OUTPUT_PATH.open("w", encoding="utf-8", newline="") as f:
fieldnames = [
"channel",
"rows",
"approved_revenue",
"pending_revenue",
"rejected_rows",
"points",
]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for channel, values in sorted(summary.items()):
writer.writerow({
"channel": channel,
"rows": values["rows"],
"approved_revenue": values["approved_revenue"],
"pending_revenue": values["pending_revenue"],
"rejected_rows": values["rejected_rows"],
"points": values["points"],
})
elapsed_ms = (time.perf_counter() - start) * 1000
print(f"input={INPUT_PATH}")
print(f"output={OUTPUT_PATH}")
print(f"rows={input_rows}")
print(f"channels={len(summary)}")
print(f"elapsed_ms={elapsed_ms:.3f}")
if __name__ == "__main__":
main()
このコードで確認できることは、次の5つです。
- 必須列があるか
- 何行処理したか
- 何チャネル集計したか
- どのファイルに出力したか
- 処理に何ミリ秒かかったか
自動化では、出力ファイルだけでなく、実行ログも重要です。
あとで「本当に動いたのか」「何行処理したのか」「前回より遅くなっていないか」を確認できるからです。
専門家目線のチェックポイント
文字コードは最初に疑う
日本語CSVでは、UTF-8とCP932の違いでエラーが起きます。
よくあるエラーは次のようなものです。
UnicodeDecodeError: 'utf-8' codec can't decode byte...
Excelから保存したCSVでは、UTF-8ではなくCP932で保存されていることがあります。
その場合は、次のように変更します。
with open("sales.csv", encoding="cp932", newline="") as f:
...
まずUTF-8で試し、エラーが出たらCP932を確認する、という順番で十分です。
集計キーは改善アクションに直結するものを選ぶ
全体売上だけを合計しても、改善にはつながりにくいです。
おすすめの集計キーは次の通りです。
| 集計キー | 分かること | 次の改善例 |
|---|---|---|
channel | どの流入元が強いか | SNS投稿、ROOM投稿、メール導線を調整 |
campaign | どの施策が効いたか | 反応が良い企画を再実行 |
product_id | どの商品が成果を出したか | 商品差し替え、内部リンク強化 |
date | いつ成果が出たか | 投稿時間、配信曜日を調整 |
status | 確定・承認待ち・否認の状態 | 案件品質や承認率を確認 |
自動化の目的は、きれいな表を作ることではありません。
次の改善判断に使える粒度で数字を出すことです。
生データを上書きしない
集計前のCSVは、そのまま残します。
おすすめの保存例です。
data/raw/sales_2026-07-12.csv
data/output/summary_2026-07-12.csv
logs/run_2026-07-12.txt
生データを残す理由は、後から再集計できるようにするためです。
たとえば、ステータスの扱いを変えたくなった場合でも、生データが残っていれば過去分を同じルールで再処理できます。
逆に、生データを上書きしてしまうと、どの時点の数字だったのか追跡できなくなります。
確定値と見込み値を分ける
ポイント案件や広告成果のCSVには、次のような状態が含まれます。
- 承認済み
- 承認待ち
- 否認
- キャンセル
- 返品
- 未確定
これらをすべて売上として合計すると、実態より良く見える可能性があります。
最低限、次のように分けます。
approved_revenue = 確定収益
pending_revenue = 見込み収益
rejected_rows = 否認件数
収益保証のように見せないためにも、確定値と見込み値を分ける設計が必要です。
画像で説明すべき箇所
記事内に入れると理解が深まる図解は、次の3つです。
- CSVから集計レポートまでの流れ図
- チャネル別収益・ポイントの棒グラフ
- 失敗パターンと対策の比較表
視覚的な説得力を高めるなら、次の証拠を載せると効果的です。
- 実行後の
summary.csv - ターミナルの実行ログ
- 処理時間
- 入力CSVの行数
- エラーが出たときのログ
- 前回比の差分
Hiroの検証ログでは、1,200行のサンプルCSVを2.564msで集計した記録を残しました。これは特定環境での一回の実測であり、すべての環境で同じ速度になるとは限りません。
よくある失敗と対策
失敗1:列名が変わってエラーになる
row["revenue"]と書いているのに、CSV側の列名が売上になっているとエラーになります。
対策は、処理開始時に必須列を確認することです。
required = {"date", "channel", "revenue", "points", "status"}
actual = set(reader.fieldnames or [])
missing = required - actual
if missing:
raise ValueError(f"必要な列がありません: {sorted(missing)}")
列名変更は、早めに止めた方が安全です。
間違った列を集計して、それらしい数字が出る方が危険です。
失敗2:空欄でint()が落ちる
int("")はエラーになります。
対策は、変換関数を通すことです。
def to_int(value):
if value is None:
return 0
value = str(value).strip()
if value in {"", "-", "N/A", "null"}:
return 0
return int(value)
本番では、0に変換した件数もログに出すとさらに安全です。
空欄が急に増えた場合、取得元の仕様変更やデータ欠損に気づけます。
失敗3:同じCSVを二重集計する
自動実行では、同じファイルを何度も読んで合計してしまう事故があります。
対策は、処理済みファイル名を記録することです。
processed_files.txt
sales_2026-07-11.csv
sales_2026-07-12.csv
二重計上すると、広告費を増やす、投稿頻度を変える、商品を追加する、といった判断が歪みます。
収益やポイントの集計では、二重集計の防止を早い段階で入れるべきです。
失敗4:自動化したのに誰も見ない
summary.csvを作っただけで放置すると、改善行動につながりません。
対策は、毎朝見る場所に出すことです。
- Google Driveの固定フォルダに保存する
- Slackやメールに要約を送る
- Notionやスプレッドシートに貼る
- 前日比だけをテキストで出す
- 異常値があるときだけ通知する
自動化資産として育てるなら、「集計する」より「改善判断に届く」状態を作ります。
失敗5:ログがなく、失敗原因が分からない
自動実行で失敗したときに、ログがないと原因調査に時間がかかります。
最低限、次を出します。
input=sales.csv
output=summary.csv
rows=1200
channels=4
elapsed_ms=2.564
status=success
失敗時は、次を残します。
status=failed
error=必要な列がありません: ['revenue']
input=sales.csv
これだけでも、次に直す場所が明確になります。
成果を測るKPI
CSV自動集計を作った後は、次のKPIを追うと改善しやすくなります。
| KPI | 見る理由 | 例 |
|---|---|---|
| 集計作業時間 | 手作業削減の効果を見る | 週3回、各20分から自動実行へ |
| 実行成功率 | 自動化の安定性を見る | 30回中29回成功 |
| 処理行数 | データ量の変化を見る | 1,200行から8,000行へ増加 |
| 異常値検出数 | データ品質を見る | 空欄、列名変更、未知ステータス |
| 確定収益 | 実績を見る | approved_revenue |
| 見込み収益 | 将来候補を見る | pending_revenue |
| 否認件数 | 案件や流入の質を見る | rejected_rows |
| 改善アクション数 | 数字を見た後の行動を見る | 記事修正、商品差し替え、投稿時間変更 |
重要なのは、売上合計だけを見ないことです。
自動化で空いた時間を、記事改善、商品選定、CTA改善、内部リンク整理、投稿時間の調整に回せているかも追跡します。
そこが、単なる便利ツールで止まるか、改善サイクルを回す自動化資産になるかの分かれ目です。
反論:小さいCSVならExcelで十分ではないか
小さいCSVなら、Excelで十分な場面もあります。
たとえば、月1回だけ見るCSV、行数が少ないCSV、判断に人間の目視が必須のCSVなら、無理にPython化しなくても構いません。
ただし、次の条件に当てはまるなら、自動化する価値があります。
- 毎日または毎週見る
- 同じ作業を何度も繰り返している
- 複数CSVをまとめて見たい
- 前日比や週次推移を見たい
- 承認待ち、否認、確定を分けたい
- 手作業ミスが判断に影響する
- 数字を見た後に改善アクションを取りたい
Python化すべきか迷ったら、まず手作業時間を測ってください。
1回15分 × 週3回 = 週45分
週45分 × 4週 = 月180分
このように毎月数時間を使っているなら、基本的なCSV自動集計を作る価値は十分あります。
類似記事との差別化ポイント
この記事の差別化は、Pythonの文法説明だけで終わらせていない点です。
多くのCSV入門記事は、読み込み、合計、出力で終わります。
この記事では、収益化やポイント運用で使う前提で、次の観点を入れています。
- 確定値と見込み値を分ける
- 否認件数を記録する
- 二重集計を防ぐ
- 実行ログと処理時間を残す
- 列名変更や文字コードエラーに備える
- 集計結果を改善アクションにつなげる
- KPIとして成功率、異常値、改善行動まで見る
Python、CSV、自動集計を学ぶ目的が「作業を楽にする」だけなら、短いコードでも十分です。
しかし、収益化の仕組みに組み込むなら、集計結果が次の判断に使える設計まで必要です。
限界と使えないケース
CSV自動集計にも向かない場面があります。
- 入力CSVの形式が毎回大きく変わる
- データの意味を人間が確認しないと判断できない
- 画像、PDF、スクリーンショットから数値を読む必要がある
- APIで直接取得した方が正確で早い
- 法務、税務、会計上の確認が必要な数値を自動処理だけで確定したい
- 取得元の規約で自動処理や二次利用に制限がある
特に会計、税務、投資判断に関わるデータは、専門家確認や公式データとの照合が必要になる場合があります。
Pythonは集計を助ける道具であり、責任ある判断そのものを代替するものではありません。
読了後すぐに取れるアクション
今日やるなら、まず1つのCSVだけ選んでください。
次のメモを作ります。
対象CSV: room_points_2026-07.csv
行数: 850行
毎回見る数字: 商品別ポイント、投稿日別クリック、承認済みポイント
手作業時間: 1回あたり約15分
自動化したい出力: summary_room_points.csv
必須列: date, product_id, clicks, points, status
注意点: pendingとapprovedを分ける
このメモができれば、Pythonコードに落とし込めます。
最初から大きなダッシュボードを作る必要はありません。
まずは、次の最小単位で始めます。
1ファイル
1集計
1出力
1ログ
これで十分です。
まとめ:CSV自動集計は「数字を見る習慣」を機械に任せる第一歩
PythonでCSVを自動集計する基本パターンは、次の流れです。
- CSVの列名と文字コードを確認する
csv.DictReaderで読み込む- 必須列があるかチェックする
- 売上やポイントを数値に変換する
- チャネルや日付など、改善に効く単位で集計する
- 確定値、見込み値、否認件数を分ける
- 結果をCSVや通知に出す
- 実行ログ、異常値、二重集計を管理する
- KPIを見て、改善アクションにつなげる
自動化の価値は、作業時間を減らすことだけではありません。
収益やポイントの変化を見逃さない状態を作ることにあります。
人間が毎回CSVを開いて確認する運用では、忙しい日や疲れた日に止まります。
機械が集計し、人間は判断と改善に集中する。この形に近づけるほど、ブログ、物販、アフィリエイト、ポイント運用は「労働の積み上げ」から「仕組みの運用」に変わっていきます。
本気で自動化・不労所得を構築したい方向けの実践マニュアル
CSV集計は入口です。
次に必要なのは、データ取得、集計、投稿、通知、改善判断までをつなげる設計です。
「毎日ログインして確認する」
「手でCSVを整える」
「収益が出た理由を後から思い出す」
この状態から抜け出したい方には、本気で自動化・不労所得を構築したい方向けの実践マニュアルを用意しています。
PythonでCSVを扱う基礎から、収益・ポイント・記事改善を自動で回す考え方まで、実務で使える順序に整理しています。
次の一歩として、商品一覧ページから自分に合うマニュアルを確認してください。