Schemas and Schedule Metadata#
call-report holds two separate answers to “what fields does this
schedule have”, and keeping them distinct is what makes them useful.
The canonical metadata is what the package believes a schedule looks like across its whole published history. It ships with the package, generated from FCA’s own archives, and is available whether or not you have any release files.
A layout is what one release actually declared. It comes from the
D_<ROOT>.TXTfile inside that quarter’s release, so it reflects that quarter and nothing else.
Both are reachable from FCACallReport, and both
speak the same vocabulary (FieldSchema), so they
can be compared directly.
Every example on this page uses
PackagedArchiveTransport, which reads
the release zips checked into this repository, so nothing here needs a
download:
>>> from call_report.fca import FCACallReport
>>> from call_report.fca.transport import PackagedArchiveTransport
>>> report = FCACallReport(
... start="2014-12-31", end="2015-03-31", transport=PackagedArchiveTransport()
... )
A schedule’s whole history#
get_file_metadata() returns a
schedule’s canonical, cross-time FileMetadata.
It does not depend on start, end, or transport, so it answers
even before any file has been read:
>>> metadata = report.get_file_metadata(schedule="RI")
>>> metadata.name
'RI'
>>> metadata.first_period.label, metadata.last_period.label
('2000Q1', '2026Q1')
>>> len(metadata.file_schema)
46
RI has been published every quarter since 2000, but that does not mean its
columns held still. changed reports whether any field’s presence
differs from the file’s own:
>>> metadata.changed
True
Each field carries the periods it was actually present for, so you can see
which ones came and went. NONTEMPIMPAIRN stopped being reported after
2014:
>>> impair = metadata.file_schema["NONTEMPIMPAIRN"]
>>> impair.first_period.label, impair.last_period.label
('2000Q1', '2014Q4')
That is why len(metadata.file_schema) is 46 while no single quarter of
RI ever had 46 columns. The cross-time schema is the union of every field
the schedule has ever had.
A single quarter#
get_schema() narrows that history to
one quarter, returning a FieldSchema of just the
fields present then:
>>> schema = report.get_schema(schedule="RI", period="2014-12-31")
>>> len(schema)
44
>>> schema.names[:6]
('SYSTEM', 'DIST', 'ASSOC', 'MONTH', 'YEAR', 'UNINUM')
The snapshot is historically accurate, not merely filtered. Each field is narrowed to the one version that applied at that date, so a field revised later still shows the definition in force at the quarter you asked for:
>>> provisions = schema["PROVLNS"]
>>> len(provisions.versions)
1
>>> provisions.versions[0].definition
'Provisions for Losses on Loans,Sales Contracts,Notes,and Leases'
>>> provisions.versions[0].periods[0].label
'2014Q4'
FieldSchema also exposes a plain narwhals schema, mapping name to
dtype, for code that only cares about types:
>>> schema.schema["PROVLNS"]
Int64
Every quarter in range#
Omit period to get one schema per fetched quarter that has the
schedule, keyed by ReportingPeriod:
>>> schemas = report.get_schema(schedule="RI")
>>> sorted(period.label for period in schemas)
['2014Q4', '2015Q1']
This is the quickest way to see a schedule’s width move over a range:
>>> {period.label: len(schema) for period, schema in schemas.items()}
{'2014Q4': 44, '2015Q1': 45}
get_layout() has the same shape, so
the two can be used interchangeably.
What a release actually declared#
get_layout() reads the release’s own
layout file, and
to_field_schema() converts it into
the same FieldSchema vocabulary the canonical metadata uses. A layout
describes exactly one release and carries no period of its own, so the
period has to be supplied:
>>> layout = report.get_layout(schedule="RI", period="2015-03-31")
>>> declared = layout.to_field_schema(period="2015-03-31")
>>> len(declared)
45
Comparing the two views#
Because both sides are a FieldSchema,
compare() puts them side by side. This
is the cheapest way to notice a release whose layout disagrees with the
shipped metadata:
>>> canonical = report.get_schema(schedule="RI", period="2015-03-31")
>>> declared.compare(other=canonical).is_empty
True
An empty diff means the package’s belief about 2015Q1 matches what FCA actually published that quarter. A non-empty one is worth investigating: it means the shipped metadata has drifted from the releases it was generated from.
A field redefined in place#
A field can change without ever disappearing. RCF1’s LOANSTATUS holds a
numeric code for each row’s loan-performance category, and FCA revised the
code list itself in 2015, splitting code 155 into a new 152/155
pair. The field was present throughout, so its metadata carries two
versions rather than a presence gap:
>>> loanstatus = report.get_file_metadata(schedule="RCF1").file_schema["LOANSTATUS"]
>>> len(loanstatus.versions)
2
as_of recovers whichever definition applied at a given quarter, which is what makes a snapshot usable as a description of that quarter’s data rather than of today’s:
>>> rcf1 = report.get_file_metadata(schedule="RCF1").file_schema
>>> rcf1.as_of(period="2010-03-31")["LOANSTATUS"].versions[0].definition[-32:]
'o OFIs 155 Other loans 160 Total'
>>> rcf1.as_of(period="2020-03-31")["LOANSTATUS"].versions[0].definition[-32:]
'o OFIs 152 Other loans 155 Total'
Tracking drift across quarters#
The same comparison across two different quarters shows how a schedule evolved. RI changed between 2014Q4 and 2015Q1:
>>> before = report.get_schema(schedule="RI", period="2014-12-31")
>>> after = report.get_schema(schedule="RI", period="2015-03-31")
>>> drift = before.compare(other=after)
>>> drift.added
('NONTEMPIMPAIR', 'ProvDebtSec')
>>> drift.removed
('NONTEMPIMPAIRN',)
changed holds the fields present in both but not identical, each as a
FieldChange carrying the before and after
metadata. FCA rewrote PROVLNS’s definition in that same quarter:
>>> change = next(item for item in drift.changed if item.name == "PROVLNS")
>>> change.before.versions[0].definition
'Provisions for Losses on Loans,Sales Contracts,Notes,and Leases'
>>> change.after.versions[0].definition
'Provisions for credit losses: On loans, sales contracts, notes, and leases'
>>> change.content_changed
True
One thing to know when comparing two quarters this way: each snapshot
stamps its fields with its own quarter, so every field common to both
quarters lands in changed on the strength of that stamp alone, whether
or not its dtype or definition moved:
>>> len(drift.changed)
43
Most of that is span-only noise: the field is unchanged, only the quarter
it was snapshotted at differs. periods_changed
names that directly, and content_changed
filters changed down to the fields whose dtype or definition actually
moved:
>>> change.periods_changed
True
>>> [item.name for item in drift.content_changed]
['PROVLNS']
Comparing a layout against the canonical schema for the same quarter, as
in the previous section, has no such stamp difference, so there
content_changed and changed agree.
When metadata and a release disagree#
get_layout and get_schema answer from different sources, so they can
in principle disagree. If a release contains a schedule the canonical
metadata says was not published that quarter, get_schema raises
PeriodNotAvailableError rather than
pretending the schedule is missing:
PeriodNotAvailableError: file 'RCI' was not published as of 2025Q3;
known periods span 2000Q1 to 2017Q4.
That is a defect in the shipped metadata, and worth reporting. It is
deliberately not reported as
ScheduleNotFoundError, which would claim
the release has no such schedule when it plainly does.
Next steps#
Get Started covers loading the data itself, and reshaping it to wide or long format.
API documents every method used here in full.