csv-excel-merger — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited csv-excel-merger (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Combine tabular files into one clean output without silently losing, duplicating, or corrupting rows.
Copy this checklist and track progress:
- [ ] 1. Profile inputs
- [ ] 2. Choose append vs join
- [ ] 3. Map columns and normalize keys
- [ ] 4. Merge and resolve conflicts
- [ ] 5. Verify row math
- [ ] 6. Write output and report python scripts/profile_inputs.py file1.csv file2.xlsxExcel files are profiled per sheet. Confirm with the user which sheets count if a workbook has more than one.
pd.concat, then dedupe.pd.merge on a key.{original: unified} rename map per file (see references/merge_strategies.md for common variants) and show it to the user when any match is fuzzy. Normalize key columns before dedupe or join: strip whitespace, lowercase emails, strip non-digits from phones, unify date formats. Without this, [email protected] and [email protected] survive as two people.dtype=str so IDs, ZIP codes, and phone numbers keep leading zeros, then convert specific columns afterward. import pandas as pd
frames = []
for path, rename in [("jan.csv", {"E-mail": "email"}), ("feb.xlsx", {"Email Address": "email"})]:
df = (pd.read_excel(path, dtype=str) if path.endswith((".xlsx", ".xls"))
else pd.read_csv(path, dtype=str, encoding="utf-8-sig", # use the profiler's encoding
keep_default_na=False))
df = df.rename(columns=rename)
df["email"] = df["email"].str.strip().str.lower()
df["source_file"] = path # lineage for every row
frames.append(df)
combined = pd.concat(frames, ignore_index=True, sort=False)
# Blank keys are not duplicates of each other: set them aside before deduping.
has_key = combined["email"].fillna("") != ""
# Later files win: list the most recent source last, then keep="last".
deduped = combined[has_key].drop_duplicates(subset=["email"], keep="last")
no_key = combined[~has_key]
merged = pd.concat([deduped, no_key], ignore_index=True)For a join, make pandas enforce the relationship you expect so a duplicate key raises instead of multiplying rows:
out = pd.merge(contacts, deals, on="email", how="left",
validate="one_to_one", indicator=True)
unmatched = out[out["_merge"] == "left_only"]Conflict strategies (keep first/last/most complete, combine fields, flag for review) are in references/merge_strategies.md.
rows_in = sum(len(f) for f in frames)
assert len(merged) > 0, "merge produced an empty frame"
assert len(merged) <= rows_in, "more rows out than in: check the join keys"
assert deduped["email"].is_unique, "duplicate keys remain after dedupe"
print(f"in={rows_in} out={len(merged)} removed={rows_in - len(merged)} blank_keys={len(no_key)}")
print(merged["source_file"].value_counts())Spot-check three removed duplicates by hand against the source files; the asserts prove the math, not that the right row won.
to_csv(path, index=False, encoding="utf-8-sig") (the BOM makes Excel read accents correctly).to_excel(path, index=False) with openpyxl installed. A sheet holds at most 1,048,576 rows; split or use CSV/Parquet beyond that.conflicts_review.csv or unmatched.csv when those sets are non-empty.Current pandas is 3.x (Python 3.11+). Differences that affect merges:
str dtype, not object. Check pd.api.types.is_string_dtype(col) instead of dtype == object.df[col][mask] = x never updates df (pandas only warns); use df.loc[mask, col] = x..dt.as_unit("ns") before casting to integers if something downstream expects nanoseconds.pd.read_excel(..., engine="calamine") (needs python-calamine) reads large workbooks much faster than openpyxl.The code in this skill also runs on pandas 2.2.
validate= catches it.dtype=str turns 01234 into 1234.1.23E+15 or dates already reformatted in the source file cannot be recovered by pandas; flag them.skiprows= or header=.é artifacts. The profiler reports the encoding per file.chunksize= or use Polars/DuckDB, and dedupe with a key set instead of loading everything into memory.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.