DRUG COST HISTORY / PYTHON + CSV
Compare NADAC acquisition costs in Python
Compare historical acquisition benchmarks for an NDC-coded research portfolio, preserving pricing units and effective dates.
Compare acquisition benchmarks for a drug portfolio
Suppose you are reviewing how acquisition benchmarks changed during 2025 for several drugs in a research portfolio. The product supplies weekly rates by NDC, their pricing units and effective dates. This example compares the January 1 and December 31 observations without mixing a tablet price with a price per milliliter.
input.csv contains a fictional three-item research portfolio: the project labels were invented for this example, while the NDCs identify real CMS records. The identical portfolio list appears in the Python code below. The reference file is an unchanged subset of the paid product: all 53 weekly observations for each of those three NDCs, with every field retained.
Save observations.csv in your working folder. The sample includes the full year for the selected identifiers, so you can change the two comparison dates to any of the weekly snapshot dates. The complete purchase covers 33,498 distinct NDCs.
Code example
The code reads NDCs as text and rates as decimals. It checks that both requested observations exist, their units and rate-setting context agree, and neither rate is future-effective at its snapshot.
Paths are relative to your working folder; use from pathlib import Path; print(Path.cwd()) to check it, or supply full paths.
import csv
from decimal import Decimal
from pathlib import Path
folder = Path('.') # Folder containing observations.csv (sample or full purchase).
start_date = '2025-01-01'
end_date = '2025-12-31'
# Fictional research portfolio; these are real NDCs selected from the product.
portfolio = [
{'item': 'Brand tablet benchmark', 'ndc': '00003089421'},
{'item': 'Insulin vial benchmark', 'ndc': '00002821501'},
{'item': 'Generic tablet benchmark', 'ndc': '24385005452'},
]
wanted = {r['ndc'] for r in portfolio}
observed = {}
with (folder / 'observations.csv').open(newline='', encoding='utf-8') as f:
for row in csv.DictReader(f):
if row['ndc'] in wanted and row['as_of_date'] in {start_date, end_date}:
key = (row['ndc'], row['as_of_date'])
if key in observed:
raise ValueError(f'Duplicate NDC/date: {key}')
observed[key] = row
results = []
for item in portfolio:
a = observed.get((item['ndc'], start_date))
b = observed.get((item['ndc'], end_date))
status = 'comparable'
change = ''
if not a or not b:
status = 'missing_endpoint'
elif any(a[k] != b[k] for k in ('pricing_unit', 'pharmacy_type', 'rate_classification')):
status = 'context_changed'
elif any(r['rate_effective_after_snapshot'] == 'true' for r in (a, b)):
status = 'future_effective_rate'
else:
change = format(Decimal(b['nadac_per_unit']) - Decimal(a['nadac_per_unit']), 'f')
result = {
'item': item['item'], 'ndc': item['ndc'],
'description': b['ndc_description'] if b else a['ndc_description'] if a else '',
'unit': b['pricing_unit'] if b else a['pricing_unit'] if a else '',
'start_rate': a['nadac_per_unit'] if a else '',
'end_rate': b['nadac_per_unit'] if b else '',
'start_effective_date': a['effective_date'] if a else '',
'end_effective_date': b['effective_date'] if b else '',
'change_usd_per_unit': change, 'comparison_status': status,
}
results.append(result)
print(f"{result['ndc']} | {result['description']} | {result['unit']} | "
f"{result['start_rate']} -> {result['end_rate']} | "
f"change {change or 'n/a'} USD/unit | {status}")
Analysis of the output
00003089421 | ELIQUIS 5 MG TABLET | EA | 9.51063 -> 9.69551 | change 0.18488 USD/unit | comparable
00002821501 | HUMULIN R 100 UNIT/ML VIAL | ML | 4.27328 -> 4.26729 | change -0.00599 USD/unit | comparable
24385005452 | 12HR NASAL DECONGEST ER 120 MG | EA | 0.28683 -> 0.26341 | change -0.02342 USD/unit | comparable
The ELIQUIS benchmark rises from $9.51063 to $9.69551 per each, a difference of $0.18488. The HUMULIN R benchmark falls by $0.00599 per milliliter. The generic nasal decongestant benchmark falls by $0.02342 per each. These are differences between two weekly observations, not total spending, package-price changes or rates of change over every intervening week.
The results list also retains the effective dates used at both endpoints. A weekly as_of_date and a rate’s effective_date answer different questions: when the rate appears in this file and when CMS assigns it to take effect. This annual compilation does not prove when a historical file became available to an analyst.
If an endpoint is missing, the example leaves the change blank. A changed pricing unit, pharmacy type or rate classification also requires review before comparing. Future-effective rates remain visible but are not used in this example’s comparison. No missing price is replaced with zero.
Using the full dataset
Extract the purchased ZIP and point folder at it. The same code reads the complete observations.csv; replace portfolio with your own records. To load a CSV containing item and ndc columns:
with Path('my_portfolio.csv').open(newline='', encoding='utf-8') as f:
portfolio = list(csv.DictReader(f))
For the intervening weeks, use changes.csv. It lists adjacent-snapshot rate increases and decreases separately from changes in other attributes, changes in rate-setting context, appearances and disappearances. weekly_summary.csv provides the counts by week, while ndc_summary.csv identifies each NDC’s observed span and any gaps. The 2025 change sheet explains those comparisons.
Rate deltas in changes.csv describe reported values; check their effective dates before applying them to a dated analysis. An appearance or disappearance concerns the file, not a drug’s market status. No interpolation or claim of complete daily history is supplied.
Sources and versions
Source: CMS NADAC 2025, with January 1–December 31 weekly snapshots. CMS’s dataset metadata was updated December 30, 2025. ACD captured and prepared this edition in October 2026; that does not make these 2026 prices.
NADAC is based on surveyed retail community pharmacy acquisition costs. It is not a patient’s retail price or a promise of reimbursement. CMS introduced a three-month moving average for generic rates from December 18, 2024; ACD preserves the publisher’s values. See CMS pharmacy pricing and its survey methodology.
The FDA NDC reference and NDC → ATC crosswalk can support separate enrichment work. Their source snapshots are dated 2026: an NDC match alone does not establish which name, labeler or classification applied in 2025.
Example checked October 17, 2026 against both these samples and the complete purchased files. Source and processing records, sample checksums · Expected result CSV. Independent preparation; no agency endorsement.