setalign provides a toolkit for analyzing citation alignment between agents (e.g., courts, institutions, documents) using information-theoretic principles.
Install from PyPI:
pip install setalignQuick check:
from setalign import load_ecj_example_data, calculate_scar
ecj_data = load_ecj_example_data()
example = calculate_scar(
ecj_data.head(3),
input_col1="actor_a_citations",
input_col2="actor_b_citations",
)
print(ecj_data.shape)
print(example[["dyad_id", "case_flag", "scar"]])The idea for this project arose from the analytical need of comparing citations between institutional actors. As readers of judicial politics will know, politics happens through law, and citations to caselaw has been widely used to scaled judical opinions (see works by Jeffrey Lax, Jonatha Kastellec, Erik Voeten, Daniel Naurin for instance). However, a uncharted territory is how to robustly compare citation set's between agents when you expect them to be very similar, if not identical. At face value, this seems like a non-issue all that you do is count overlaps and that will easily tell you the degree of precisely, overlap. Yet, what about when the second mover overlaps entirely with the first one but also adds an extra citation? That should not be scaled equally as one with a perfect one-to-one overlap, at least not so if we are saying that legal ciations matter.
AGENT_1 = {A,B,C,D} AGENT_2 = {Z,Y,X,W}
AGENT_1 = {A,B,C,D} AGENT_2 = {A,B,C,D,E}
This module integrates two methodological innovations:
- Set Citation Alignment Ratio (SCAR) — measures informational agreement between citation sets.
- Locally Information-Adjusted Citations (LIAC) — transforms SCAR into interpretable, scalable alignment units.
- Applies information penalties for both missing and extra citations.
- Scales by local citation universe size to enable cross-case comparability.
- Produces bounded, interpretable measures ranging from –γ to +γ.
- Judicial citation behavior (ideological alignment, institutional influence)
- Legislative or policy consensus measurement
- Academic citation analysis (political science, law, etc.)
- Cross-institutional alignment studies
- Policy or regulatory document concordance
The example dataset contains paired citation lists for two actors. Each row is a dyad-level comparison.
from setalign import (
calculate_scar,
calculate_scar_metrics,
extract_citation_sets,
calculate_liac,
load_ecj_example_data,
scale_scar_by,
)
# Load the packaged ECJ example data
ecj_data = load_ecj_example_data()The citation columns must contain list-like citation sets. In this example:
actor_a_citationsis the reference or agenda-setting actor.actor_b_citationsis the responding actor.dyad_ididentifies the paired comparison.
The packaged example dataset is intentionally minimal. It contains only the columns used in this walkthrough: dyad_id, actor_a_citations, and actor_b_citations.
calculate_scar() compares the two citation lists and adds:
case_flag: the set relationship case.scar: the Set Citation Alignment Ratio.
Semantically, SCAR asks how much informational agreement exists between the reference actor's citation set and the responding actor's citation set. It is not only a raw overlap count. Missing citations and additional citations both matter because each changes the information contained in the second actor's response.
para_level_data = calculate_scar(
ecj_data,
input_col1="actor_a_citations",
input_col2="actor_b_citations",
)calculate_scar_metrics() adds interpretable set counts, including the intersection size, overall difference, initial citation universe size, and joint citation universe size.
These columns make the SCAR score auditable. For example, intersection tells you how many citations both actors share, while overall_difference tells you how much citation material appears in only one actor's set. init_citation_universe_len is especially important because it records how much citation information actor A placed on the table.
para_level_data = calculate_scar_metrics(
para_level_data,
input_col1="actor_a_citations",
input_col2="actor_b_citations",
)extract_citation_sets() adds the actual citation sets behind the metrics:
shared_citations_listagent1_unique_citations_listagent2_unique_citations_listuniverse_citations_list
This step is useful when you want to move from measurement back to interpretation. Instead of only knowing that two actors diverged, you can inspect which citations were shared, which were unique to actor A, and which were introduced by actor B.
para_level_data = extract_citation_sets(
para_level_data,
input_col1="actor_a_citations",
input_col2="actor_b_citations",
)calculate_liac() transforms SCAR into Locally Information-Adjusted Citations. The default scaling term is the initial citation universe size, here stored in init_citation_universe_len.
The intuition is local comparability. A SCAR score of 0.50 does not mean the same thing when actor A cited one authority as when actor A cited twenty. LIAC scales the alignment score by the size of actor A's citation universe, so the final value reflects both alignment quality and the amount of citation information at stake in that dyad.
para_level_data["liac"] = calculate_liac(
para_level_data,
init_citation_universe_len="init_citation_universe_len",
)You can also scale SCAR by a non-local value using scale_scar_by(). This is useful when you have theoretical priors about the citation universe that should anchor comparisons across rows. For example, a global median scaling term treats the typical citation-universe size as the reference point, rather than letting each dyad define its own scale. This is less naive than choosing an arbitrary constant, but it is still a modeling choice: use it when the global reference value has a substantive interpretation for your application.
para_level_data["liac_global_median"] = scale_scar_by(
para_level_data,
scar_col="scar",
scaling_term=para_level_data["init_citation_universe_len"].median(),
)result_cols = [
"dyad_id",
"case_flag",
"scar",
"intersection",
"overall_difference",
"init_citation_universe_len",
"joint_citation_universe_len",
"liac",
"liac_global_median",
]
print(para_level_data[result_cols].head(8))Example output, shown in a compact -style layout:
Rows: 8
Columns: 9
dyad_id DYAD001, DYAD002, DYAD003, DYAD004, DYAD005, DYAD006, DYAD007, DYAD008
case_flag 2, 1, 1, 1, 1, 2, 2, 3
scar 0.250, 0.333, 0.333, 0.333, 1.000, 0.333, 0.333, 0.019
intersection 1, 1, 1, 1, 1, 1, 1, 0
overall_difference 3, 2, 2, 2, 0, 2, 2, 2
init_citation_universe_len 2, 1, 1, 1, 1, 2, 2, 1
joint_citation_universe_len 4, 3, 3, 3, 1, 3, 3, 2
liac -1.000, -0.334, -0.334, -0.334, 1.000, -0.668, -0.668, -0.962
liac_global_median -0.500, -0.334, -0.334, -0.334, 1.000, -0.334, -0.334, -0.962
If you use this framework in academic research, please cite:
Mandujano Manríquez, M. (2026). setalign: Information-Theoretic Citation Alignment Module. Github: https://github.com/mauriciomm7/setalign
@misc{mandujano2026setalign,
author = {Mauricio Mandujano Manríquez},
title = {setalign: Information-Theoretic Citation Alignment Module},
year = {2026},
howpublished = {\url{https://github.com/mauriciomm7/setalign}},
note = {GitHub repository}
}
This project is licensed under the MIT License.