Keep Manual Labels Stable When a CSV Changes
Disclosure: Prepared with AI assistance for CommentTok, whose editorial team is affiliated with TokViewer. All sample records are fictional.
You have labeled a comment as “pricing,” downloaded another CSV, and discovered that its row number changed. Copying yesterday’s label column into today’s file can silently attach decisions to different comments. Instead, compare the content and transfer a label only when its match occurs exactly once in each file.
This tutorial targets legacy or reduced CSVs without stable IDs. CommentTok’s current main exporter has 12 columns, including IDs when available. When genuine IDs exist in both files, match video_id plus comment_id, checking uniqueness and reviewing edited content. Prefer that to this less reliable text fallback; an extension’s sequential row number is not a stable comment ID.
Use Python 3.9 or later and comma-delimited UTF-8 files. The fresh file needs exactly author,handle,comment,likes; the old copy adds label. No packages are required. Do not discard available stable IDs just to run this example.
Key takeaways: Match exact text, check uniqueness on both sides, and leave uncertain rows blank with an explicit review status.
Choose what counts as a match
Our key is the tuple (author, handle, comment). We deliberately exclude likes: a changed count should not detach a content-topic label. The output keeps the fresh count. If your labels describe popularity, rankings, or engagement thresholds, this policy is inappropriate; those labels need recalculation or review when likes change.
In this reduced input, a unique text match establishes uniqueness within these two files, not identity across the platform. Use exports from the same known source and scope. An identical comment disappearing and being replaced by another identical comment can remain undetectable.
Matching is case-sensitive and preserves whitespace. A changed author name, handle, punctuation mark, or comment body produces an unmatched row. We do not lowercase, trim, or fuzzy-match text into equivalence. Whitespace-only key components are treated as missing. This conservative choice sacrifices some transfers rather than guessing whether an edit preserved meaning.
Save the complete program
Save this as transfer_labels.py. Headers must appear in the stated order. The loader rejects incorrect headers and record widths before creating output. Python’s CSV documentation explains quoted fields and opening files with newline=""; these matter when comments contain commas or embedded line breaks.
import argparse
import csv
from collections import Counter
from pathlib import Path
FIELDS = ["author", "handle", "comment", "likes"]
def read_rows(path, fields):
with path.open(encoding="utf-8-sig", newline="") as source:
reader = csv.reader(source, strict=True)
if next(reader, None) != fields:
raise ValueError(f"{path}: expected {','.join(fields)}")
rows = []
for values in reader:
if len(values) != len(fields):
raise ValueError(f"{path}: wrong field count")
rows.append(dict(zip(fields, values)))
return rows
def key(row):
return tuple(row[field] for field in FIELDS[:3])
def transfer(old, fresh):
old_counts = Counter(key(row) for row in old)
new_counts = Counter(key(row) for row in fresh)
unique = {key(row): row for row in old if old_counts[key(row)] == 1}
for row in fresh:
match = key(row)
label = ""
if not all(value.strip() for value in match):
status = "review_missing_key"
elif old_counts[match] > 1 or new_counts[match] > 1:
status = "review_duplicate"
elif old_counts[match] == 0:
status = "review_unmatched"
elif not unique[match]["label"].strip():
status = "review_unlabeled"
else:
label = unique[match]["label"]
status = "transferred"
yield {**row, "label": label, "status": status}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("old", type=Path)
parser.add_argument("fresh", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()
try:
old = read_rows(args.old, FIELDS + ["label"])
fresh = read_rows(args.fresh, FIELDS)
with args.output.open("x", encoding="utf-8", newline="") as target:
writer = csv.DictWriter(target, fieldnames=FIELDS + ["label", "status"])
writer.writeheader()
writer.writerows(transfer(old, fresh))
except (OSError, ValueError, csv.Error) as error:
parser.exit(1, f"{error}\n")
if __name__ == "__main__":
main()
The two counts prevent a one-to-many transfer as well as a many-to-one transfer. Python’s Counter documentation describes the counting container used here. Even duplicate old rows carrying the same label go to review: agreement between labels does not establish which record matched.
Run a tiny example
Create old.csv:
author,handle,comment,likes,label
Ada,@ada,Price please,2,pricing
Bo,@bo,More colors,1,request
Create fresh.csv, with reordered rows, an edited comment, and a changed count:
author,handle,comment,likes
Bo,@bo,More colors please,1
Ada,@ada,Price please,9
Run:
python3 transfer_labels.py old.csv fresh.csv labeled.csv
The expected output is:
author,handle,comment,likes,label,status
Bo,@bo,More colors please,1,,review_unmatched
Ada,@ada,Price please,9,pricing,transferred
The output follows the fresh file’s order. Neither input is modified. An existing output path is refused, so choose a new filename for another run. The program loads both inputs into memory; it is intended for small working exports, not an unbounded stream.
Validate uncertainty, then review it
Try copying Ada’s row twice into either input. Every fresh Ada match should now have an empty label and review_duplicate, even if its likes differ. A unique old row with an empty label produces review_unlabeled; an empty key component produces review_missing_key. A new comment or any unmatched edit produces review_unmatched.
Before publication, local fixture checks covered those cases, row reordering, changed likes, quoted multiline Unicode text, malformed input, and refusal to overwrite an existing file. These are synthetic checks of this policy, not evidence about real export completeness. The small example and duplicate-row exercise above can be reproduced using only the code in this article.
Filter all review_ statuses and make fresh decisions. Keep the old labeled CSV: rows absent from the fresh export do not appear in this output, so it is not an audit of deletions. Before using a reviewed output as the next old file, remove its status column and retain the five required columns.
For the next analysis step, the Google Sheets comment-analysis workflow explains the spreadsheet side. Keep the transfer decision separate from your grouping and charts: a blank label with a review reason is useful information.
