お悩み女子取得件数は出ているのに、集計すると数字が合いません。空欄や同じデータを見つけるには、どこを見ればよいですか。
取得データの欠損は、保存や集計の前に列名・件数・値の型・識別子の重複を検査すると見つけやすくなります。空欄を0に置き換えたり、同じ名前をまとめたりする前に、何が欠けたのかを確認します。
商品データの架空CSVを使い、欠損と重複を検出して、不一致があれば出力を止める例を示します。Windows 11とPython 3.12.10でコードを実行しています。外部サイトからの取得は行わず、検査する条件を手元のデータで確かめています。
- 列名・件数・空欄・型を確認する順番
- 0円と未取得の空欄の区別
- ID・URL・タイトルによる重複の考え方
- 異常があれば集計用CSVを作らないコード
取得データの正常条件と欠損検知
空欄があるだけでは、処理の失敗とは言い切れません。必須項目と、空欄を許す項目を先に分けます。
列名と必須項目
今回の列はid、title、price、urlです。idは商品を区別する文字列、titleは名前、priceは0以上の価格、urlは取得元を確認するための項目です。この例では4つとも必須にします。
在庫がない商品の価格表示が消えるなど、取得元で空欄が正当な場合もあります。その場合は未取得・対象外・値が0を別に扱います。画面の仕様を見ずに、欠損を一律で埋めると意味が変わります。
件数の基準
HTTPが200でも、一覧の目印が変わって0件になることがあります。件数0を正常終了にしない条件が必要です。前回の正常な取得件数が分かるなら、急に減ったときも止めて確認します。
0と空欄の違い
0円は値がある状態で、空欄は値が分からない状態です。Pythonでif not valueだけを見ると、数値の0まで未取得と扱う場合があります。この例はCSVの文字列が空かどうかを先に検査し、その後で数値に直します。
価格の検査にはDecimalを使います。小数も扱える数値型で、NaNやInfinityのような通常の価格にできない値はis_finite()で除外します。カンマや通貨記号を認めるかは、取り込みの規則として別に決めます。
取得データの重複を判定する識別子
同じタイトルがあっても、別の商品かもしれません。何を同じデータとみなすかで、削除してよい行が変わります。
| 判定に使う値 | 使いやすい場面 | 確認点 |
|---|---|---|
| ID | 取得元が安定したIDを持つ | 先頭ゼロとIDの付け直し |
| URL | 記事や商品詳細ページの一覧 | 追跡用パラメータや別URL |
| タイトル | IDがないときの候補探し | 同名商品・表記ゆれ |
この例はIDを一意のキーにします。同じIDで名前・価格・URLまで同じなら完全重複として1件にします。同じIDで価格が違えば、どちらが新しいかを決められないので、全体の出力を止めます。
URLの#topのようなフラグメントは、ページ内の位置を指す部分なので、この例では取り除いて比べます。一方、?の後ろのパラメータは商品の種類を表す場合もあるため、一律では消しません。URLだけで同一商品を保証する処理ではありません。
先頭の行を残す処理は短く書けますが、重複の理由を調べずに捨てないほうが確実です。ページ送りで前ページを再取得したのか、元の一覧に同じ商品があるのかを分けて確認します。
取得データの欠損検知|6行の練習用CSV
欠損、型の不一致、完全重複、同じIDの内容違いを、意図的に混ぜます。実際の商品や価格ではありません。
次をsource.csvとしてUTF-8で保存します。価格が0の004は有効な値です。001は同じ内容の重複と、価格の違う重複の両方を入れています。example.testは説明用で、コードからこのURLへ通信しません。
id,title,price,url
001,ノート,1200,https://example.test/p/001#top
001,ノート,1200,https://example.test/p/001
002,,500,https://example.test/p/002
003,付箋,時価,https://example.test/p/003
004,ボールペン,0,https://example.test/p/004
001,ノート,1300,https://example.test/p/001
データの前後の空白はstrip()で取り除きます。ただし、文字を消すと意味が変わる項目ではこの処理も見直します。たとえば自由記述や固定桁のコードは、元の値も残してから整形するほうが追跡しやすくなります。
取得データの欠損検知|異常があれば出力を止めるコード
次をcheck_data.pyに保存し、source.csvと同じ練習フォルダで動かします。追加ライブラリは不要です。
import csv
from decimal import Decimal, InvalidOperation
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
REQUIRED = ["id", "title", "price", "url"]
PREVIOUS_COUNT = 4 # 練習用の前回件数。実運用では正常取得時の値
def inspect_csv(path):
with path.open(encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
names = reader.fieldnames or []
if len(names) != len(set(names)) or not set(REQUIRED).issubset(names):
raise ValueError("列名の不足または重複があります")
rows = list(reader)
if not rows or len(rows) < PREVIOUS_COUNT * 0.5:
raise ValueError("件数が少ないため、取得処理を確認してください")
accepted, seen, errors, duplicates = [], {}, [], 0
for number, row in enumerate(rows, start=1):
try:
if None in row or any(row[k] is None for k in REQUIRED):
raise ValueError("列数が一致しません")
item = {k: row[k].strip() for k in REQUIRED}
for key in REQUIRED:
if not item[key]:
raise ValueError(f"{key}が空です")
try:
amount = Decimal(item["price"])
except InvalidOperation:
raise ValueError("priceが数値ではありません") from None
if not amount.is_finite() or amount < 0:
raise ValueError("priceは0以上の有限の数値にします")
parts = urlsplit(item["url"])
if parts.scheme not in {"http", "https"} or not parts.netloc:
raise ValueError("URLの形式が不正です")
item["url"] = urlunsplit(parts._replace(fragment=""))
fingerprint = (item["title"], amount, item["url"])
if item["id"] in seen:
if seen[item["id"]] != fingerprint:
raise ValueError("同じidで内容が異なります")
duplicates += 1
continue
seen[item["id"]] = fingerprint
accepted.append(item)
except ValueError as e:
errors.append(f"{number}件目: {e}")
return rows, accepted, duplicates, errors
try:
rows, accepted, duplicates, errors = inspect_csv(Path("source.csv"))
print(f"取得: {len(rows)}件 / 完全重複: {duplicates}件 / エラー: {len(errors)}件")
if errors:
print("\n".join(errors))
print("出力中止: 元データと取得処理を確認します")
raise SystemExit(1)
with Path("clean.csv").open("x", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=REQUIRED)
writer.writeheader()
writer.writerows(accepted)
print(f"採用: {len(accepted)}件 / 保存: clean.csv")
except (OSError, ValueError, csv.Error) as e:
print(f"出力中止: {e}")
raise SystemExit(1)
検査結果は、採用候補・完全重複の件数・エラーの一覧に分けています。エラーが1件でもあれば保存しません。途中まで読めた行だけを出力すると、不足した結果を完成品と取り違えるためです。
同じIDの内容が違う場合は、先の行も後の行も判断待ちです。このコードは全体を止めるので、先の行を正しいものとして確定しません。エラー行だけを別の確認表に出す運用に変える場合も、元データを追える形を残します。
列数が違う行も検出します。DictReaderでは、列が足りない値にNoneが入る場合や、余った列がNoneキーへ入る場合があります。空文字と列不足を分けて見ることで、CSVの壊れ方も判断できます。
取得データの欠損検知|失敗例と修正後の結果
まず意図的な不備があるCSVを実行し、出力が作られないことを見ます。その後、別の練習フォルダで修正済みデータを確認します。
不備のあるデータの停止結果
py -3.12 check_data.py
取得: 6件 / 完全重複: 1件 / エラー: 3件
3件目: titleが空です
4件目: priceが数値ではありません
6件目: 同じidで内容が異なります
出力中止: 元データと取得処理を確認します
完全重複は1件、空欄・型・内容違いのエラーは3件でした。clean.csvは作られず、終了コードは1です。元のsource.csvは書き換えません。件目は見出しを除いたデータの順番を示します。
修正後の保存結果
空欄を元情報で確認し、時価を数値と混同せず、同じIDの価格違いを確認してから、別のフォルダに次の3行を用意します。値を推測して埋めた結果ではなく、成功時を確認する別の練習データです。
id,title,price,url
001,ノート,1200,https://example.test/p/001
002,付箋,500,https://example.test/p/002
004,ボールペン,0,https://example.test/p/004
取得: 3件 / 完全重複: 0件 / エラー: 0件
採用: 3件 / 保存: clean.csv
なべくん欠損を消して数字を合わせるより、どの項目が取れなかったかを残すほうが、次の修正がしやすくなります。
取得データの欠損検知|よくある質問
実行前に迷いやすい点を確認します。
- 空欄を0で埋めてもよいですか?
-
業務上の意味が同じと確認できた場合だけにします。未取得の価格を0にすると、実際に無料の商品と区別できません。欠損件数を残し、原因の確認と補完の判断を分けます。
- 同じURLなら重複として削除できますか?
-
同じURLでも取得日時や選択条件が違う場合があります。ID、URL、内容、取得時刻を見て判断します。タイトル一致だけで削除すると同名の別データを失う可能性があるため、候補の抽出にとどめる方法もあります。
集計前の欠損検知と出力の確認
出力ができたかに加え、入力が正常だったかを確かめます。
正常条件:必須列、件数、値の型を先に決める
欠損:空欄と0を区別し、推測して埋めない
重複:IDと内容を照合し、食い違いを止める
出力:異常のある結果を完成品として渡さない
最初は数行の正常例と異常例を用意し、止まる条件を1つずつ確かめてみてください。
次に進める作業に合わせて、こちらの記事も参考にしてください。
参考資料(2026年10月確認)
csv:DictReader | Python
decimal | Python
urllib.parse | Python
