CSV excel merger

Credit to OneWave AI · From GitHub OneWave-AI/claude-skills · License: MIT · Not tested by us · Added October 29, 2026

# CSV/Excel Merger

Combine tabular files into one clean output without silently losing, duplicating, or corrupting rows.

## Workflow

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
```

1. **Profile inputs.** Run the bundled profiler first; it reports encoding, delimiter, rows, headers, candidate keys, and header overlap without changing anything:

   ```bash
   python scripts/profile_inputs.py file1.csv file2.xlsx
   ```

   Excel files are profiled per sheet. Confirm with the user which sheets count if a workbook has more than one.

2. **Choose the operation.** This is the decision that most often goes wrong:
   - **Append (stack)** - same kind of records from different sources or periods (Jan + Feb exports, three lead lists). Use `pd.concat`, then dedupe.
   - **Join (enrich)** - different facts about the same entities (contacts + their deal values). Use `pd.merge` on a key.
   - Unsure? If the files share most columns, append. If they share only an ID column, join.

3. **Map columns and normalize keys.** Build an explicit `{original: unified}` rename map per file (see [references/merge_strategies.md](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 removed] ` and `[email removed]` survive as two people.

4. **Merge.** Read every file with `dtype=str` so IDs, ZIP codes, and phone numbers keep leading zeros, then convert specific columns afterward.

   ```python
   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:

   ```python
   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](references/merge_strategies.md).

5. **Verify before reporting.** Never hand back a merge without checking it:

   ```python
   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.

6. **Write output and report.** Use the layout in [references/output_template.md](references/output_template.md).
   - CSV for Excel users: `to_csv(path, index=False, encoding="utf-8-sig")` (the BOM makes Excel read accents correctly).
   - Excel: `to_excel(path, index=False)` with openpyxl installed. A sheet holds at most 1,048,576 rows; split or use CSV/Parquet beyond that.
   - Also write `conflicts_review.csv` or `unmatched.csv` when those sets are non-empty.

## pandas version notes

Current pandas is 3.x (Python 3.11+). Differences that affect merges:
- Text columns default to the `str` dtype, not `object`. Check `pd.api.types.is_string_dtype(col)` instead of `dtype == object`.
- Copy-on-Write is always on. Chained assignment such as `df[col][mask] = x` never updates `df` (pandas only warns); use `df.loc[mask, col] = x`.
- Parsed datetimes default to microsecond resolution. Call `.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.

## Failure modes to check for

- **Row explosion on join** - duplicate keys on both sides multiply rows. `validate=` catches it.
- **Leading zeros lost** - reading without `dtype=str` turns `01234` into `1234`.
- **Excel-mangled values** - long IDs already shown as `1.23E+15` or dates already reformatted in the source file cannot be recovered by pandas; flag them.
- **Header rows not on line 1** - exports with a title block need `skiprows=` or `header=`.
- **Mixed encodings** - one file in cp1252 among UTF-8 files shows up as `é` artifacts. The profiler reports the encoding per file.
- **Silent column drops** - a column present in only one file becomes mostly empty after append. Keep it and report its completeness; never drop data without saying so.
- **Large files** (over a few hundred MB) - read with `chunksize=` or use Polars/DuckDB, and dedupe with a key set instead of loading everything into memory.

Fill in anything in [brackets] or {braces} with your own details first.

Our tested version: Find what changed in your numbers and why

This is my prompt

Wrote this prompt? Tell me, and I will put your name on it. I check every claim by hand.

Report or remove
What is wrong?

More Everything else prompts

Everything else

Trade quote builder

Builds estimates, quotes, and bids for trades and field-service businesses (HVAC, plumbing, electrical, roofing, landscaping, painting, cleaning, remodeling, general contracting, IT/AV installs). Turns a scope into line...

You needAny AI

Credit to OneWave AI

Everything else

Board update

When the user needs to write a monthly or quarterly investor update, prepare a board deck, or communicate company progress to stakeholders.

You needAny AI

Credit to Shawn Pang

Everything else

Feedback synthesis

When the user needs to analyze, categorize, or extract actionable insights from customer feedback across multiple sources, especially feature requests.

You needAny AI

Credit to Shawn Pang

Everything else

Proposal generation

When a founder needs to create a sales proposal, statement of work, contract, NDA, or master service agreement.

You needAny AI

Credit to Shawn Pang

Everything elseClaude Code

Battlecards

Creates competitive battlecards for sales deal conversations. Produces win themes, landmines to plant, objection

You needClaude Code

Credit to Mahmoud Omar (Growth Marketing OS)

Our tested version

Everything else

Launch tier framework

Classify product launches into defensible tiers and turn roadmap inputs into a season-of-launch plan that aligns Product, Campaigns, PR/AR, Sales, and customer-facing teams.

You needAny AI

Credit to Mahmoud Omar (Growth Marketing OS)

Our tested version

All Everything else promptsAll prompts

Want a prompt for your own work? Ask for one. Every prompt here is free.

Want this built for you? Get it built.