agent skill · summarization · pandas
Consolidate Multi-Year Files into One Dataset
Stack yearly Excel/CSV extracts with inconsistent column names into one consolidated DataFrame using pd.concat, with a source-file column and column normalization so every row is traceable and totals tie to the originals.
When to use this skill
- You’ve been asked for a trend across 3, 5, or 10 years, and each year lives in its own file (
Holdings_2022.xlsx,Holdings_2023_FINAL.xlsx, …). - Column names drift between years —
CUSIPvsSecurity IDvsSecurity_ID,Market ValuevsFair Value— so a naive copy-paste misaligns data. - You want a consolidation you can rerun next year, with an audit trail showing exactly which file each row came from.
The pattern: read each file, tag it with its year and source filename, normalize column names to one standard, then pd.concat everything into a single tall dataset.
Inputs it expects
- One file per year (Excel or CSV), each containing the same kind of data at the same level of detail (e.g., one row per security).
- A way to know each file’s year — from the filename or a date column inside the file.
- A mapping of known column-name variants to your standard names. Build this by eyeballing each file’s headers first; don’t guess.
- Agreement on which columns are required (identifier, amount, date) so you can check every year supplied them.
Steps
- Read each year’s file and print its row count and column list — this is your per-file control total, write it down.
- Normalize column names with a rename mapping so every year uses identical standard names.
- Add
Fiscal_YearandSource_Filecolumns to each frame before combining — once stacked, these are the only way to trace a row home. - Combine with
pd.concat(dfs, ignore_index=True). Then check for columns that exist in only some years —concataligns by name and fills the gaps withNaNrather than erroring. - Run the control totals, then check for cross-year duplicates before summarizing.
Code
import pandas as pd
# Map each year's column-name quirks to one standard
COLUMN_MAPPING = {
'CUSIP': 'Security_ID', 'Security ID': 'Security_ID', 'SecID': 'Security_ID',
'Market Value': 'Market_Value', 'Fair Value': 'Market_Value', 'MktValue': 'Market_Value',
'As of Date': 'Report_Date', 'AsOfDate': 'Report_Date', 'Date': 'Report_Date',
}
files = {
2022: 'Holdings_2022.xlsx',
2023: 'Holdings_2023_FINAL.xlsx',
2024: 'Holdings_2024.xlsx',
}
dfs = []
file_counts = {} # per-file control totals, captured at load time
for year, filename in files.items():
df = pd.read_excel(filename)
df = df.rename(columns=COLUMN_MAPPING)
# Traceability: every row keeps its year and source file
df['Fiscal_Year'] = year
df['Source_File'] = filename
file_counts[filename] = {'rows': len(df), 'total_value': df['Market_Value'].sum()}
dfs.append(df)
print(f"Loaded {year}: {len(df):,} rows, {df['Market_Value'].sum():,.2f} from {filename}")
# Stack all years; concat aligns by column name and fills gaps with NaN
combined = pd.concat(dfs, ignore_index=True)
# Columns not present in every year show up as partial NaN — list them
sparse_cols = combined.columns[combined.isna().any()].tolist()
print(f"\nConsolidated: {len(combined):,} rows, "
f"{combined['Fiscal_Year'].min()}-{combined['Fiscal_Year'].max()}")
if sparse_cols:
print(f"Columns missing in some years: {sparse_cols}")
# Year-over-year summary from the consolidated data
yoy = combined.groupby('Fiscal_Year')['Market_Value'].sum()
print(yoy)
Validation (control totals)
- Row counts add up:
len(combined)must equal the sum of every per-file row count captured at load time. Any difference means a file was dropped or double-loaded. - Grand total ties:
combined['Market_Value'].sum()must equal the sum of the per-filetotal_valuefigures, and each year’s slice (combined[combined['Fiscal_Year'] == y]['Market_Value'].sum()) must match that file’s original total to the penny. - Every row is traceable:
combined['Source_File'].isna().sum()andcombined['Fiscal_Year'].isna().sum()are both 0. - File coverage:
combined['Source_File'].nunique()equals the number of files you intended to load — catches a silently skipped year. - Duplicate check:
combined.duplicated(subset=['Security_ID', 'Fiscal_Year']).sum()should be 0 if each file has one row per security.
Exceptions to surface
- Rows with
NaNin a required column after concat — usually a column-name variant the mapping didn’t cover, so a whole year’s values landed in an unmapped column. Show the affectedSource_File. - Duplicate (Security_ID, Fiscal_Year) pairs — the same holding loaded twice, or a “v2” file overlapping a “FINAL” file.
- Files that matched the folder pattern but failed to load, or years with zero rows — the reviewer must confirm whether the year genuinely had no activity.
- Identifiers that appear in one year but vanish the next (or appear from nowhere) — legitimate buys/sells or a sign the identifier format changed between systems.
- A year whose row count or total value swings sharply from its neighbors (e.g., half the rows of every other year) — often a partial extract rather than a real trend.