fw.compare
Use fw.compare to compare per-feature populated fractions from two missingness results. It keeps each side’s population and missing-value conventions so an apparent improvement can be interpreted in context.
This is an availability comparison. It does not compare individual cell values, match records across deliveries, or establish why a rate changed.
Jump to the worked example or the argument reference.
Function reference
Section titled “Function reference”fw.compare( before, after,)This page describes Fieldwork 0.3.2. Import the package with import fieldwork as fw. View the documented source.
Arguments
Section titled “Arguments”| Argument | Type / default | Effect |
|---|---|---|
before |
Result; required | Earlier missingness result (restore a saved export with fw.Result.from_dict), with its own source, scope, denominator, features, and missing conventions. |
after |
Result; required | Later missingness result. Counting units must agree; for entity units, entity keys and any/all aggregation must also agree. Row counts and source identities may differ. |
Returns and output choices
Section titled “Returns and output choices”Returns a fieldwork.Result with kind="comparison" (schema 2.0). changes contains the union of feature names with before, after, and populated_fraction_delta. The delta is after minus before; 0.2 means 20 percentage points. before_scope, after_scope, analysis units, sources, and conventions preserve context. Each change also becomes an availability_change finding.
Use plaintext for a text report, SVG for a static figure, or HTML for a shareable report. to_dict() retains analytical evidence; visualization_data provides the labels and values used to draw a figure. A comparison cannot be recomputed; recompute the two missingness results instead.
Interpretation and boundaries
Section titled “Interpretation and boundaries”A delta is None when a feature is absent from one result or either denominator is zero. This is not evidence of no change. Different missing conventions are retained rather than harmonized automatically; align them deliberately if you want comparable rates.
Use to_frame("changes") for a table. Comparison findings contain no representative source rows; return to the original missingness results for row investigation. Inputs of another result kind or incompatible counting units raise ValueError.
Walkthrough: did the registration form get better?
Section titled “Walkthrough: did the registration form get better?”A neighborhood arts festival receives two registration deliveries. Email availability jumps in the second delivery. Before attributing that change to a better form, the organizers want to know whether the mix of online and walk-in registrations changed.
This example compares two missingness results, then repeats the comparison within each registration channel. Download the runnable script.
Build two small deliveries
Section titled “Build two small deliveries”Each row is a distinct registration. The deliveries contain different registrations;
this is a comparison of populations, not a before-and-after match of individuals.
Online registrations have email addresses, while walk-in registrations have phone
numbers. None represents missing data, using Fieldwork’s default convention.
import pandas as pdimport fieldwork as fw
columns = ["registration", "channel", "email", "phone"]before_df = pd.DataFrame( [ ("B01", "online", "b01@example.test", None), ("B02", "online", "b02@example.test", None), ("B03", "walk-in", None, "555-0103"), ("B04", "walk-in", None, "555-0104"), ("B05", "walk-in", None, "555-0105"), ("B06", "walk-in", None, "555-0106"), ("B07", "walk-in", None, "555-0107"), ("B08", "walk-in", None, "555-0108"), ], columns=columns,)after_df = pd.DataFrame( [ ("A01", "online", "a01@example.test", None), ("A02", "online", "a02@example.test", None), ("A03", "online", "a03@example.test", None), ("A04", "online", "a04@example.test", None), ("A05", "online", "a05@example.test", None), ("A06", "online", "a06@example.test", None), ("A07", "walk-in", None, "555-0207"), ("A08", "walk-in", None, "555-0208"), ], columns=columns,)after_df["sms_opt_in"] = [ False, False, False, False, False, False, True, False,]The second delivery introduces sms_opt_in. A recorded False is populated data:
availability of an answer is different from permission to send a message.
Compare the availability results
Section titled “Compare the availability results”Select the contact fields explicitly so registration identifiers and channel labels do not enter the comparison. Each result counts rows, with eight rows per delivery.
before = fw.missingness( before_df, features=["email", "phone"], table_id="registrations-before",)after = fw.missingness( after_df, features=["email", "phone", "sms_opt_in"], table_id="registrations-after",)change = fw.compare(before, after)changes = change.to_frame("changes")print( changes[["feature", "populated_fraction_delta"]].to_string( index=False )) feature populated_fraction_delta email 0.5 phone -0.5sms_opt_in NaNEmail availability rises from 25% to 75%: the fraction delta 0.5 means 50
percentage points, not a 50% relative increase. Phone availability falls by the
same number of points. Every registration still has at least one contact field.
These counts describe populated values, not whether the contact details work.
compare includes the union of analyzed feature names. There is no before result
for sms_opt_in, so its delta is None. That does not mean it improved from zero:
the earlier result has no availability measurement for that feature.
The table displays an undefined numeric delta as NaN; the dictionary returned by to_dict() uses None. Neither supplies a baseline for the new field.
The native text renderer also keeps both populations visible. This code prints only its first five lines, an excerpt of the full report:
report = fw.render_plaintext(change)print("\n".join(report.splitlines()[:5]))Fieldwork · comparisonBefore: input; 8 evaluated rows Before analysis: 8 rows; presence=per_rowAfter: input; 8 evaluated rows After analysis: 8 rows; presence=per_rowHold registration channel constant
Section titled “Hold registration channel constant”In the first delivery, two registrations are online and six are walk-ins. In the second, those counts reverse. Start with online registrations. Create a separate row selection for each delivery, then compare their email and phone availability. Scopes select source row positions; a scope from one delivery cannot be reused on the other delivery.
before_positions = []for position, channel in enumerate(before_df["channel"]): if channel == "online": before_positions.append(position)
after_positions = []for position, channel in enumerate(after_df["channel"]): if channel == "online": after_positions.append(position)
before_scope = fw.Scope.from_positions( before_df, before_positions, name="Online before")after_scope = fw.Scope.from_positions( after_df, after_positions, name="Online after")
before_online = fw.missingness( before_df, features=["email", "phone"], scope=before_scope)after_online = fw.missingness( after_df, features=["email", "phone"], scope=after_scope)online_change = fw.compare(before_online, after_online)online_changes = online_change.to_frame("changes")print( online_changes[["feature", "populated_fraction_delta"]].to_string( index=False ))feature populated_fraction_delta email 0.0 phone 0.0Within the online channel, email remains 100% populated and phone remains 0%. Repeat the same steps with channel == "walk-in" to check the other channel: email remains 0% and phone 100%. Each fraction uses
its own scope’s denominator, so two rows can be compared with six rows.
The aggregate shift in this constructed dataset follows entirely from the changed
channel mix. It provides no evidence that either form collected contact details
more consistently.
Return to the registrations behind the result
Section titled “Return to the registrations behind the result”The next practical question is which registrations still need an email address. Inspect the email availability finding from the after missingness result. The comparison holds summary changes, so use the underlying analysis for row evidence.
after_findings = after.to_frame("findings")print(after_findings[["id", "statement"]].to_string(index=False))id statementf0 email: populated in some rowsf1 phone: populated in some rowsf2 Present: email, sms_opt_in; absent: phonef3 Present: phone, sms_opt_in; absent: emailf4 email and phone are mutually exclusiveHere f0 is the email availability finding. Use that ID to retrieve every row missing an email address:
missing_email = after.inspect( after_df, "f0", exceptions=True, all_matches=True)print( missing_email[["registration", "channel", "phone"]].to_string( index=False ))registration channel phone A07 walk-in 555-0207 A08 walk-in 555-0208exceptions=True selects missing email values; all_matches=True requests the
complete matching population rather than saved examples. Both registrations have
phone values. The organizers can now check whether email is needed for walk-in
registrations at all, instead of treating a channel-specific collection choice as
a universal defect.
Continue exploring
Section titled “Continue exploring”fw.missingness, fw.render_html, fw.visualization_data. See results and configuration for serialization and result-method contracts.