API reference¶
Every public function and the Theory class, in the groups the R package's
reference index uses
and in the same order, so a name is found in the same place on either site. The
R package prefixes each name with tf_, so check() here is tf_check()
there.
The four functions whose name matches their module (diagram, dossier,
implications, simulate) are documented by their canonical path, so that the
function rather than the module is shown.
Core IO¶
Read, validate and write theory objects. Theory is also the builder: adding a
construct, a proposition or a prediction, and logging the provenance of each,
are methods on the class here, where the R package exposes them as tf_add_*
functions.
theoryforge.Theory
¶
A theory as a versioned, machine-checkable object.
Wraps the parsed mapping (self.data); all accessors tolerate missing
optional collections by treating them as empty. The builder methods
(add_* and set_formal_model) change the theory in place and return
it, so calls chain. copy() gives an independent copy to amend.
Source code in src/theoryforge/core.py
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 | |
add_assumption(id, statement, added_for=None, protects=None)
¶
Append an auxiliary assumption and log the step in the provenance.
added_for is the id of the prediction whose anomaly the assumption
was added to answer, and None marks a core assumption.
appraise_amendment counts an assumption added for an anomaly as ad
hoc unless a prediction it protects that is new in the amended version,
other than this one, is corroborated. protects holds the ids of the
predictions the assumption shields from refutation, a list or a single
string. The theory is returned, so that builder calls chain.
Source code in src/theoryforge/core.py
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 | |
add_prediction(id, statement, type, derives_from=None, diagnostic_vs=None, severity=None, test=None)
¶
Append a prediction and log the step in the provenance.
id identifies the prediction and statement gives the claim in
words. type is the form of the claim, one of four. existence
asserts that an effect or relation exists, without a direction.
directional asserts a sign or an order, including comparisons,
interactions, the invariance of a direction across groups and claims that
an effect occurs only when a condition holds. interval asserts that a
quantity lies in a stated range, the range the theory permits. point
asserts one value, with the tolerance that measurement requires. That
width is measurement tolerance, not latitude the theory allows. The label
is self-declared, and no function checks it against the statement.
derives_from holds the ids of the propositions the prediction derives
from, and diagnostic_vs the ids of the registered alternatives it
would discriminate from. severity is a declared pre-data severity
from 0 to 1, stored only when given. The checklist's risk_severity item
reads it, and reads the claim-form rubric's value for a prediction that
declares none. The dossier sets the two side by side.
test is the sampling plan for testing the prediction, a mapping of
any of n_planned, unit, alpha, sided ("two" or
"one"), sesoi and sesoi_scale (the smallest effect of
interest and its scale), power or decision_probability with an
optional power_lo and power_hi, method, design_ref and
justification (API_SPEC.md section 30). It is stored, as a copy,
only when given. preregister() lists it after the hypotheses, and
nothing scores it. The theory is returned, so that builder calls chain.
Source code in src/theoryforge/core.py
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 | |
appraise_amendment(prior)
¶
Appraise this theory, by content, as an amendment of a prior version.
The verdict is progressive, degenerating or neutral, with the evidence
behind it. Passing this theory itself as the prior raises ValueError,
so begin an amendment with prior.copy() (see
:func:theoryforge.appraise_amendment).
Source code in src/theoryforge/core.py
749 750 751 752 753 754 755 756 757 | |
compile_sem()
¶
Compile constructs and propositions to lavaan model syntax.
See :func:theoryforge.compile_sem for how names are written and when
a collision is refused.
Source code in src/theoryforge/core.py
786 787 788 789 790 791 792 | |
copy()
¶
An independent copy of the theory, to amend while the original stays as it is.
The builders work in place, so after v2 = v1.add_prediction(...) the
names v2 and v1 refer to one object. Appraising one against the
other then compares a theory with itself. Beginning the amendment with
v2 = v1.copy() keeps v1 intact. The copy is deep, since
Theory(dict(v1.data)) would copy only the top-level mapping and the
two objects would still share their constructs, propositions and
predictions.
Returns:
| Type | Description |
|---|---|
Theory
|
A new |
Source code in src/theoryforge/core.py
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 | |
diagram(type='nomological_net', engine='graphviz')
¶
Return the diagram IR for type (one of nomological_net, provenance,
causal_dag, development_roadmap, pipeline, context, workflow, venn, rigour,
severity); see :func:theoryforge.diagram.diagram.
Source code in src/theoryforge/core.py
726 727 728 729 730 | |
dossier()
¶
A reviewer-facing audit bundle of the rigour report, severity, test record, evidence,
provenance and preregistration (see :func:theoryforge.dossier.dossier).
Source code in src/theoryforge/core.py
794 795 796 797 | |
embedding_redundancy(embedder, threshold=None)
¶
An opt-in embedding-based redundancy screen; results depend on the supplied embedder.
Source code in src/theoryforge/core.py
805 806 807 | |
implications(cycles='refuse')
¶
The conditional independencies the theory's causal graph implies.
cycles="sigma" reads a graph with feedback loops by
sigma-separation, which the default refuses. See
:func:theoryforge.implications.
Source code in src/theoryforge/core.py
759 760 761 762 763 764 765 766 | |
landscape(corpus, min_link=2, max_token_share=0.5, method='components')
¶
Map this theory and its alternatives onto a literature corpus's themes.
See :func:theoryforge.landscape for the matching rules, statuses and methods.
Source code in src/theoryforge/core.py
773 774 775 776 777 778 779 780 | |
new_evidence_dois(candidate_dois)
¶
DOIs in candidate_dois not already cited by this theory's evidence or alternatives.
Source code in src/theoryforge/core.py
782 783 784 | |
osf_push(token=None, node=None, filename=None, dry_run=True, base_url=None, overwrite=False)
¶
Deposit the dossier on OSF (dry-run by default). A live push needs a token and node.
overwrite=True adds a new version of an existing file of the same name.
Source code in src/theoryforge/core.py
813 814 815 816 817 818 819 820 821 | |
post_data_severity()
¶
How severely each bounded prediction passed its test, from the outcome's estimate and SE.
See :func:theoryforge.post_data_severity for the rule and its refusals.
Source code in src/theoryforge/core.py
742 743 744 745 746 747 | |
preregister(path=None)
¶
Render the hypotheses section of a preregistration, writing it to path
when one is given (see :func:theoryforge.preregister).
Source code in src/theoryforge/core.py
768 769 770 771 | |
render_diagram(type='nomological_net')
¶
Render a digraph view via the optional graphviz library; see
:func:theoryforge.render.render_diagram.
Source code in src/theoryforge/core.py
732 733 734 735 736 | |
render_report(path, title=None, render=False, to='html')
¶
Write (and optionally render) a Quarto report of the audit dossier.
Source code in src/theoryforge/core.py
809 810 811 | |
severity()
¶
Per-prediction risk and computed severity from the claim-form riskiness rubric.
Source code in src/theoryforge/core.py
738 739 740 | |
simulate(steps=10, dt=0.1, k=1.0, damping=0.5, init=1.0, method='euler')
¶
Propagate the construct network as a linear dynamical system (see theoryforge.simulate).
Source code in src/theoryforge/core.py
799 800 801 802 803 | |
validate(*, full=False)
¶
Check the theory against the schema, theory.schema.json.
The default pass checks structure. It covers the required top-level
fields, the maturity and theory_form enums, the top-level field
names and the required fields and enums of each construct, proposition
and prediction. It also checks that every collection is a list. A field
that is absent, null or blank is reported as missing, and one that holds
another type as such (id must be a string).
With full=True it also checks that every id is unique within its
collection and that every cross-reference (proposition endpoints,
prediction derivations and diagnostics, assumption, evidence and
test-outcome targets) points to a declared id. It then checks the rest
of the schema. That covers the required fields of assumptions,
alternatives, evidence and test outcomes, a boolean passed, the
evidence direction and formal-model type enums, the version block and
the schema_version pattern. It also covers numbers within [0, 1]
where the schema asks for them, a prediction's bounds and test
block, a test outcome's estimate and se, and the type of every
other field and of every entry of a string array.
CI compares the result with a JSON Schema 2020-12 validator. Besides the ids and references, which the schema cannot express, the pass is stricter in two ways, since a blank string counts as missing and an entry of a string array must be a nonempty string. It is more lenient in three ways, which API_SPEC.md section 2 documents. A null optional field is absent and a single string stands for a one-element array of strings. An empty sequence and an empty mapping stand for each other.
Returns:
| Type | Description |
|---|---|
bool
|
True when the theory passes. |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in src/theoryforge/core.py
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 | |
write(path)
¶
Write the theory to path as JSON (a .json suffix) or YAML (anything else).
The file is UTF-8 with LF line endings and ends with a newline. YAML keeps
the theory's key order and quotes every string a reader could take for
something else, y, Y, n and N included. The file therefore
reads back as the same theory in both twins, and in theoryforge 0.6.0 for R
(API_SPEC.md section 3, "Reading and writing files").
Source code in src/theoryforge/core.py
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 | |
theoryforge.new_theory(id, title, maturity='building', theory_form='network')
¶
Start a new, empty theory object (BUILDING mode entry point).
Source code in src/theoryforge/core.py
849 850 851 852 853 854 855 856 857 858 859 | |
theoryforge.read(path)
¶
Read a theory object from a YAML or JSON file (JSON when the suffix is .json).
The file is read exactly as the R twin's tf_read() reads it (API_SPEC.md
section 3, "Reading and writing files"). A byte-order mark is ignored. Dates
and y, Y, n and N stay strings, and integers are decimal, octal
or hexadecimal only, so 1:30 and 1_000 are strings too. A merge key
lets the mapping's own keys win. A repeated key anywhere raises
ValueError("(<path>) Duplicate map key: '<key>'"), and a document that is
not a mapping raises ValueError("Theory data must be a mapping").
Source code in src/theoryforge/core.py
827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 | |
theoryforge.write(theory, path)
¶
Write a theory object to YAML or JSON (chosen by file extension).
Source code in src/theoryforge/core.py
844 845 846 | |
Packaged examples¶
Example theories shipped inside the package, so that nothing has to be
downloaded to follow the documentation. The R twin reaches the same files
through tf_example_names() and tf_example_path().
theoryforge.example_names()
¶
The names of the example theory and corpus files, sorted.
Only .yaml entries count, matching the R twin's list.files(pattern =
"\\.yaml$"). Listing the directory wholesale would let anything else
that lands beside the examples, such as a __pycache__ directory left by
a local build, show up here and not in R.
Source code in src/theoryforge/examples.py
14 15 16 17 18 19 20 21 22 23 | |
theoryforge.example_path(name)
¶
Filesystem path of a packaged example, e.g. panic-network.theory.yaml.
Raises FileNotFoundError naming the available files when name is not
one of them.
Source code in src/theoryforge/examples.py
26 27 28 29 30 31 32 33 34 35 36 37 38 | |
Rigour¶
Score a theory against the versioned rigour checklist, and report what the score was made of and how severely each bounded prediction passed its test.
theoryforge.check(T)
¶
Compute the full rigour report (dict) for a Theory or theory mapping.
Each item has a status (pass, warn or fail) and a score in
[0, 1]. An item with nothing to assess, such as the redundancy screen of a
theory with one construct, has the status n/a and the score None, and
is left out of the aggregate. The aggregate is the weighted mean of the
applicable items' scores, times 100, and coverage is the share of the
checklist's weight that was applicable (API_SPEC.md section 4).
Raises:
| Type | Description |
|---|---|
ValueError
|
a test outcome's |
Source code in src/theoryforge/rigor.py
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | |
theoryforge.report(T, format='json')
¶
Render the rigour report. format in {'json', 'html'}.
The HTML fragment escapes every value it interpolates.
Source code in src/theoryforge/rigor.py
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | |
theoryforge.severity(T)
¶
Per-prediction claim-form riskiness, in file order.
A pre-data rubric of the form of each prediction's claim. risk_score is
the base riskiness of the declared type: existence 0.1, directional 0.4,
interval 0.7 and point 0.9. computed_severity discounts a directional
prediction by 25 per cent, adds 0.1 when diagnostic_vs names a registered
alternative and is capped at 1. The order of the types follows Popper (1959,
sections 31-33) for nested claims about one quantity, and the directional
penalty follows Meehl's (1967, 1990b) argument that a sign alone risks little.
The base values, the discount and the bonus are the package's conventions,
and neither source gives them.
How severely a claim is tested depends on the design and the data, which the rubric does not read. A prediction scores the same however it is tested, and a failed test leaves the value where it was.
References
Meehl, P. E. (1967). Theory-testing in psychology and physics: A methodological paradox. Philosophy of Science, 34(2), 103-115. https://doi.org/10.1086/288135 Meehl, P. E. (1990b). Why summaries of research on psychological theories are often uninterpretable. Psychological Reports, 66(1), 195-244. https://doi.org/10.2466/pr0.1990.66.1.195 Popper, K. R. (1959). The logic of scientific discovery. Hutchinson.
Source code in src/theoryforge/scoring.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
theoryforge.post_data_severity(T)
¶
How severely each bounded claim passed its test, from the outcome's estimate and SE.
A prediction states the region its claim allows in bounds, a mapping of
lower and upper, either of which may be null (or infinite) for an
open side. A test outcome records the estimate it observed and its
standard error se. For an outcome with both, whose prediction declares
bounds, the severity with which the claim passed is the smallest of
Phi((estimate - lower) / se) and Phi((upper - estimate) / se) over
the finite bounds, rounded to three decimals (Mayo & Spanos, 2006, 2011).
It is the probability that the test would have given a result less in
accord with the claim, had the claim been false at the bound. A one-sided
claim, such as an effect above a smallest effect of interest, is a lower
bound with the upper one open.
The computation is a normal approximation to the sampling distribution of
the estimate. With two finite bounds, the value equals 1 minus the p value of
the two one-sided tests (TOST) of equivalence. A point claim with lower
equal to upper can pass with a severity of at most 0.5, so it needs a
tolerance around its value. Unlike severity(), which grades the form of
a claim before any data, this value depends on the design and the data. The
rigour checklist does not read it.
Returns:
| Type | Description |
|---|---|
list[dict]
|
One record per test outcome, in file order, that carries both an |
list[dict]
|
estimate and a standard error and whose prediction declares bounds: |
list[dict]
|
|
list[dict]
|
|
list[dict]
|
an open bound is None. |
Raises:
| Type | Description |
|---|---|
ValueError
|
for the first outcome, in file order, that cannot be read,
with the R twin's text: |
References
Mayo, D. G., & Spanos, A. (2006). Severe testing as a basic concept in a Neyman-Pearson philosophy of induction. The British Journal for the Philosophy of Science, 57(2), 323-357. https://doi.org/10.1093/bjps/axl003 Mayo, D. G., & Spanos, A. (2011). Error statistics. In P. S. Bandyopadhyay & M. R. Forster (Eds.), Philosophy of statistics (pp. 153-198). Elsevier. https://doi.org/10.1016/B978-0-444-51862-0.50005-8
Source code in src/theoryforge/scoring.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | |
Redundancy¶
Screen the definitions of a theory's constructs for lexical redundancy, deterministically, with an opt-in embedding variant.
theoryforge.tokens(s)
¶
Tokenise a string into a set of content tokens (API_SPEC.md section 6).
The text is folded through schema/fold.json (accented Latin letters to
ASCII, Greek and Cyrillic to small unaccented letters) and lowercased in
ASCII. A token is a maximal run of letters, marks and digits in any script
(Unicode general categories L, M and N). Tokens shorter than three code
points and English stopwords are dropped.
Source code in src/theoryforge/redundancy.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
theoryforge.jaccard(a, b)
¶
Source code in src/theoryforge/redundancy.py
44 45 46 47 48 49 | |
theoryforge.redundancy_check(T)
¶
Pairwise lexical similarity of construct definitions.
Returns one record per unordered construct pair, {a, b, similarity,
overlap, flag}, sorted by descending similarity then (a, b)
ascending. similarity is the Jaccard index of the two definitions'
token sets and overlap their overlap coefficient, the shared tokens
over the tokens of the shorter definition. flag is review for a
near-duplicate, a Jaccard similarity at or above the checklist's
redundancy_similarity_max, and for a definition contained in the other,
an overlap at or above redundancy_overlap_max when both definitions
hold at least three tokens. It is ok otherwise.
The screen compares words, so it cannot detect empirical redundancy, two differently defined constructs that correlate almost perfectly once measurement error is corrected for (Le et al., 2010). Sibling constructs (Lawson & Robins, 2021) may share vocabulary without being redundant.
References
Le, H., Schmidt, F. L., Harter, J. K., & Lauver, K. J. (2010). The problem of empirical redundancy of constructs in organizational research: An empirical investigation. Organizational Behavior and Human Decision Processes, 112(2), 112-125. https://doi.org/10.1016/j.obhdp.2010.02.003 Lawson, K. M., & Robins, R. W. (2021). Sibling constructs: What are they, why do they matter, and how should you handle them? Personality and Social Psychology Review, 25(4), 344-366. https://doi.org/10.1177/10888683211047101
Source code in src/theoryforge/redundancy.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
theoryforge.embedding_redundancy(T, embedder, threshold=None)
¶
Pairwise cosine similarity of embedded construct definitions.
embedder maps a definition string to a numeric vector; the vectors of every compared
pair must be of equal, nonzero length, or the pair is refused. Returns one record per
unordered construct pair, sorted by descending similarity then (a, b), with a
review/ok flag.
threshold defaults to the checklist's embedding_similarity_max (0.85). How high
a cosine two unrelated definitions reach depends on the embedding model, so a
threshold suited to one model may flag everything or nothing under another, and
the value is best set for the model in use.
Source code in src/theoryforge/embedding.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
Diagram¶
Byte-identical diagram intermediate representations, and native rendering of them.
theoryforge.diagram.diagram(T, type='nomological_net', engine='graphviz')
¶
Return the diagram IR string for the requested type.
engine is accepted but has no effect, because the IR is engine-independent
(DOT for the digraphs, dagitty syntax for the causal DAG, and SVG for the Venn,
the rigour grid and the severity chart).
The causal DAG is read by dagitty and drawn by the apps with Graphviz. It
writes a construct id bare only when the id is made of ASCII letters,
digits and underscores, does not begin with a digit and is not a DOT
keyword. The keywords are node, edge, graph, digraph,
subgraph and strict, in any case. Any other id is double-quoted,
with a double quote inside it escaped by a backslash, so self-efficacy
stays one node where dagitty used to read two.
The pipeline and workflow views give every test outcome a node of its own.
The first outcome of a prediction is result_<id> (outcome_<id> in
the workflow) and each later one result_<id>#<k>, where <k> counts
the prediction's outcomes in file order, so a failed replication is not
merged into an earlier pass. When a prediction has several outcomes, each
label also carries the outcome's date.
Raises:
| Type | Description |
|---|---|
ValueError
|
for an unknown |
Source code in src/theoryforge/diagram.py
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 | |
theoryforge.lit_diagram(obj, type='keyword_cooccurrence', max_edges=None)
¶
DOT for the literature layer. type in {keyword_cooccurrence, co_citation, theme_landscape}.
max_edges caps a keyword_cooccurrence or co_citation diagram at that
many edges, the highest counts first and ties broken by (a, b); only their
endpoints are drawn. None draws every edge, and theme_landscape ignores it.
Source code in src/theoryforge/lit.py
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 | |
theoryforge.render_diagram(x, type='nomological_net')
¶
Render a digraph view without leaving Python.
Where :func:theoryforge.diagram.diagram returns the deterministic Graphviz
DOT string, render_diagram wraps it in a graphviz.Source, which
displays inline in Jupyter and renders to a file with its render method.
Requires the optional graphviz <https://pypi.org/project/graphviz/>_
library (pip install "theoryforge[render]") and, to write image files, the
Graphviz system binaries.
The three chart views (venn, rigour and severity) are already
SVG, so they come back as an :class:SVGString, which also displays inline
in Jupyter. The causal_dag view emits dagitty syntax rather than DOT, so
it is not rendered here; paste it into a dagitty tool instead.
Parameters¶
x:
A :class:~theoryforge.core.Theory, a parsed theory mapping, or a
diagram IR string from diagram() or lit_diagram(), so literature
diagrams render the same way.
type:
The diagram type, as in diagram(). Ignored when x is already an
IR string.
Returns¶
graphviz.Source or SVGString
A graphviz.Source for the digraph views; an :class:SVGString for
the chart views.
Source code in src/theoryforge/render.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
Develop¶
Appraise an amendment to a theory as progressive or degenerating, in Lakatos's sense.
theoryforge.appraise_amendment(new, prior)
¶
Appraise an amendment as progressive, degenerating, or neutral relative to a prior version.
The two versions are compared by content. A new prediction whose statement
and type match a prediction the amendment dropped is that prediction
renamed. A new prediction derived only from propositions the prior already
held, and needing no new assumption, articulates old content. A prediction
is corroborated when at least one test outcome passes it and none fails
it. An assumption added for an anomaly (its added_for names the
prediction it answers) is ad hoc unless a prediction it protects that is
new in this version, other than the anomaly, is corroborated.
The verdict is progressive when a corroborated new prediction is
neither an articulation nor underived, no assumption is ad hoc and no
corroborated prediction is dropped. It is degenerating when an
assumption is ad hoc and no such prediction exists, and neutral
otherwise. Lakatos has only the first two. neutral is theoryforge's
label for an amendment that meets neither rule, and the lists returned
tell its cases apart.
Returns:
| Type | Description |
|---|---|
dict
|
A dict whose keys follow API_SPEC.md section 10, with |
dict
|
first and the four keys of 0.6.0 before the rest. |
dict
|
|
dict
|
|
dict
|
|
dict
|
sorted id lists. |
dict
|
|
dict
|
independent}`` record per new assumption added for an anomaly, in |
dict
|
file order. Only |
dict
|
and no date is read, so which version is the prior is the caller's |
dict
|
responsibility. |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
ValueError
|
a test outcome's |
References
Lakatos, I. (1970). Falsification and the methodology of scientific research programmes. In Criticism and the growth of knowledge (pp. 91-196). Cambridge University Press. https://doi.org/10.1017/cbo9781139171434.009 Meehl, P. E. (1990a). Appraising and amending theories: The strategy of Lakatosian defense and two principles that warrant it. Psychological Inquiry, 1(2), 108-141. https://doi.org/10.1207/s15327965pli0102_1
Source code in src/theoryforge/develop.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
Testing and review¶
The conditional independencies a causal theory commits to, preregistration, SEM compilation and the reviewer-facing audit dossier.
theoryforge.implications.implications(T, cycles='refuse')
¶
Conditional independencies implied by the graph a theory's propositions state.
Reads every proposition through the relation table (API_SPEC.md section
28). A directed relation (increases, decreases, causes,
mediates or moderates) is an edge from from to to, and
associates is a bidirected edge, covariance the theory leaves
unexplained. The constructs that a directed relation names are the
vertices, and an association is an edge only between two of them. The
function then takes every pair of vertices that no edge joins. The pair is
stated independent given the parents of both when those m-separate it
(Richardson, 2003), and otherwise given all the other ancestors of the two
when those do. A pair that neither set separates is separated by no set of
constructs (Richardson & Spirtes, 2002, Theorem 4.2), so the theory
implies no independence for it, and it is listed under inseparable.
When every relation is directed and the graph is acyclic, the statements
are the basis set of Pearl (1988) and Shipley (2000), from which every
other independence the graph implies follows.
cycles says what to do with a graph whose directed edges form a cycle.
The default, "refuse", raises an error that names a cycle found.
"sigma" reads the graph by sigma-separation (Bongers et al., 2021),
the criterion that holds when each feedback loop, a strongly connected
component of the directed edges, has a unique equilibrium. The graph is
then replaced by its acyclification (Definition A.13): every member of a
loop takes the parents of the whole loop from outside it, and the members
of a loop are joined to each other by bidirected edges. Each pair the
theory leaves unjoined is then tested in that graph as above, where
m-separation equals sigma-separation in the theory's own graph
(Proposition A.19). There, two members of one loop are always joined. So
is a construct outside a loop to every member, when it enters one of them
or when it or a member of its own loop is associated with one of them. No
set separates such a pair, and when the theory itself leaves the pair
unjoined, it is listed under inseparable. A theory that posits
alternative stable states, such as a bistable network, has more than one
equilibrium and violates the assumption, and its sigma statements are not
guaranteed. On an acyclic graph, the two options give the same statements.
Applying d-separation to a cyclic graph, as dagitty does with the
causal_dag export, is valid only in special cases such as a linear model.
Returns {theory_id, criterion, acyclic, constructs, n_edges,
n_bidirected, feedback, implications, n_implications, inseparable}.
criterion names the separation criterion, "m" under "refuse"
and "sigma" under "sigma". constructs lists the vertices in
file order. Constructs that no directed relation names are left out,
because silence about a construct is not a claim that it is independent of
anything, and so are constructs without an id, which no proposition can
name. A construct named only by associations would be a collider on every
path through it, so leaving it out loses no statement about the others.
n_edges counts the directed edges the theory states and
n_bidirected the bidirected ones, a pair stated twice counting once,
before any acyclification. feedback lists the feedback loops, each the
ids of its members in file order and the loops in the order of their first
members. A loop has two or more members, or one construct with an edge to
itself. acyclic is True when feedback is empty, which is always the
case under "refuse", and both are carried so that a serialised record
states the verdict. Each entry of implications is {a, b, given,
statement}, where statement renders the claim in the notation
dagitty prints, a _||_ b | z1, z2, and each entry of inseparable
is {a, b}. Pairs come in construct file order, as do the members of
given, so the two engines return the same records in the same order.
The statements concern constructs, and a study measures them with error.
Error in a conditioning construct leaves part of the dependence that
holding it fixed should remove. A conditional statement tested on observed
scores is therefore rejected too often, and more often the larger the
sample (Westfall & Yarkoni, 2016). Take a chain whose two paths have
standardised coefficients of .5, with the middle construct measured at a
reliability of .8. A partial-correlation test at the 5 per cent level then
rejects the true statement in about 14, 29 and 51 per cent of studies of
200, 500 and 1,000 observations. Conditional statements are better tested
with latent-variable models, such as one built on the measurement model
that compile_sem() writes (Thoemmes et al., 2018).
A theory with no directed relations comes back with no vertices and no
error: constructs, implications and inseparable are empty and
n_implications is 0.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
References
Bongers, S., Forré, P., Peters, J., & Mooij, J. M. (2021). Foundations of structural causal models with cycles and latent variables. The Annals of Statistics, 49(5), 2885-2915. https://doi.org/10.1214/21-AOS2064 Pearl, J. (1988). Probabilistic reasoning in intelligent systems: Networks of plausible inference. Morgan Kaufmann. Richardson, T. (2003). Markov properties for acyclic directed mixed graphs. Scandinavian Journal of Statistics, 30(1), 145-157. https://doi.org/10.1111/1467-9469.00323 Richardson, T., & Spirtes, P. (2002). Ancestral graph Markov models. The Annals of Statistics, 30(4), 962-1030. https://doi.org/10.1214/aos/1031689015 Shipley, B. (2000). A new inferential test for path models based on directed acyclic graphs. Structural Equation Modeling, 7(2), 206-218. https://doi.org/10.1207/S15328007SEM0702_4 Thoemmes, F., Rosseel, Y., & Textor, J. (2018). Local fit evaluation of structural equation models using graphical criteria. Psychological Methods, 23(1), 27-41. https://doi.org/10.1037/met0000147 Westfall, J., & Yarkoni, T. (2016). Statistically controlling for confounding constructs is harder than you think. PLOS ONE, 11(3), e0152719. https://doi.org/10.1371/journal.pone.0152719
Example
A mediated chain, arousal raising perceived threat and perceived threat raising avoidance, commits the theory to one thing it does not state directly, that arousal and avoidance are independent once perceived threat is held fixed.
import theoryforge as tf
t = tf.new_theory("mediation", "A mediated chain")
t.add_construct("c_arousal", "Arousal", "Bodily activation.")
t.add_construct("c_threat", "Perceived threat", "Appraised danger.")
t.add_construct("c_avoidance", "Avoidance", "Withdrawal from the trigger.")
t.add_proposition("p1", "c_arousal", "c_threat", "increases")
t.add_proposition("p2", "c_threat", "c_avoidance", "increases")
[i["statement"] for i in t.implications()["implications"]]
# ['c_arousal _||_ c_avoidance | c_threat']
Stating that arousal and avoidance also covary for reasons the theory
leaves open (t.add_proposition("p3", "c_arousal", "c_avoidance",
"associates")) withdraws that claim: the pair is then joined by an
edge, and nothing is implied.
Closing a feedback loop from perceived threat back to arousal makes the graph cyclic. The default refuses it, and sigma-separation keeps the claim, since holding perceived threat fixed still cuts the one way out of the loop towards avoidance.
t.add_proposition("p3", "c_threat", "c_arousal", "causes")
implied = t.implications(cycles="sigma")
implied["feedback"]
# [['c_arousal', 'c_threat']]
[i["statement"] for i in implied["implications"]]
# ['c_arousal _||_ c_avoidance | c_threat']
Source code in src/theoryforge/implications.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | |
theoryforge.preregister(T, path=None)
¶
Render the hypotheses section of a preregistration, writing it to path when one is given.
The header gives the theory id, the version when it has an id (with its
parent, when one is named), the schema version, the maturity and whether
every prediction's derivation references resolve: yes, no or not
applicable (no predictions). The derivation line reports the checklist's
reference check, which does not show that a prediction follows from the
propositions it cites.
Each hypothesis is listed in file order with its claim type, statement and
derivation, followed by the auxiliary assumptions that protect it (core
or added for <id>), the registered rivals it discriminates and its test
record. A hypothesis that a recorded outcome has already tested reads
already tested: passed (<date>, registered <where>), one entry per
outcome, and any other reads untested.
When at least one prediction states a sampling plan in its test block,
a section headed Sampling plan (author-recorded; not scored) follows,
with one line per prediction in file order. A line gives what is recorded
of the planned sample, alpha and sidedness, smallest effect of interest,
power or decision probability, design analysis and justification. A
prediction without a plan reads no sampling plan stated. Nothing
scores the plan: power does not raise a claim's severity (Meehl, 1967),
though it decides how informative a failed test is (Lakens, 2022). A theory
without a plan has no such section.
A section then lists the conditional independencies that
implications() derives from the causal graph with its default
settings, which are claims the theory makes without stating them, or says
why there are none. The claim-form severity rubric closes the document.
The document is the part of a preregistration that the theory file holds. It is to be completed with the study design, the analysis plan, the inference criteria and whatever the sampling plan leaves out, the sections that a template such as PRP-QUANT asks for (Bosnjak et al., 2022).
References
Bosnjak, M., Fiebach, C. J., Mellor, D., Mueller, S., O'Connor, D. B., Oswald, F. L., & Sokol, R. I. (2022). A template for preregistration of quantitative research in psychology: Report of the joint psychological societies preregistration task force. American Psychologist, 77(4), 602-615. https://doi.org/10.1037/amp0000879
Lakens, D. (2022). Sample size justification. Collabra: Psychology, 8(1), Article 33267. https://doi.org/10.1525/collabra.33267
Meehl, P. E. (1967). Theory-testing in psychology and physics: A methodological paradox. Philosophy of Science, 34(2), 103-115. https://doi.org/10.1086/288135
Source code in src/theoryforge/prereg.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
theoryforge.compile_sem(T)
¶
Compile a theory's constructs and propositions to lavaan model syntax.
The syntax is written for lavaan::sem(). Each construct with
measurement indicators becomes a latent variable (=~). Each
proposition becomes a line of the structural model, in file order: a
regression (~) for causes, increases, decreases and
mediates and a covariance (~~) for associates. A
moderates proposition gives the moderator's main effect,
to ~ from, unless a path relation already writes that line. A comment
follows on the product term, which has to be added by hand:
to ~ x:from for an observed predictor x, and lavaan::sam() or
the modsem package when the variables are latent. The output is the same
in the R twin, byte for byte.
The propositions form the graph that implications() reads. Two of its
constructs that are both exogenous, with no proposition pointing into
either, or both terminal, with neither pointing to another construct, are
unrelated by the theory's account unless an association joins them. By
default, lavaan::sem() frees the covariance of two exogenous latent
variables and of two terminal variables, and takes that of two exogenous
observed variables from the data. A fit with those defaults could not
refute the claim. In the modality-switching example, two of the six
independencies implications() derives would go untested. The syntax
therefore ends with a block that fixes each such covariance at zero
(a ~~ 0*b), and deleting the block restores lavaan's defaults. A pair
with a moderator keeps those defaults, because the product term changes
the moderator's role.
lavaan takes the covariances of observed exogenous variables from the
data only until a covariance line names one of them. It then treats that
variable as random and fixes at zero each of its covariances that the
syntax does not write. Where a zero line names such a variable, the block
therefore ends by freeing each covariance between two observed exogenous
variables that no association names, when either is a moderator
(x ~~ m). The moderator then keeps lavaan's defaults.
The syntax is a starting point for a fit. Indicator names are the
sanitised measurement entries and must match columns of the data, so
entries written as column names, such as heart_rate, give syntax that
fits as it stands. A comment lists each construct with a single
indicator, whose residual variance lavaan::sem() fixes at zero, so the
construct is treated as measured without error. A comment flags each
feedback loop. A non-recursive model may not be identified (Bollen, 1989),
and the panic-network example is not, so check identification before
fitting. Some constructs have to be re-specified by hand. A manipulation
is better entered as a coded observed variable and a categorical
predictor with more than two levels as dummy variables. A covariate that
a measurement entry only describes needs a variable of its own. Once the
block names an observed predictor, a product term added by hand needs a
free covariance with each observed exogenous variable that a covariance
line names. lavaan fixes each one left unwritten at zero, and each takes
a line of its own (x:m ~~ x).
Every name is one lavaan reads as written. Construct ids that lavaan
would refuse or misread, such as self-efficacy, 1arousal, NA
or its own keyword efa, are renamed. A comment after the header
records each renaming
(# renamed for lavaan: 'self-efficacy' -> self_efficacy). An indicator
is its measurement entry folded and lowercased, with each run of other
characters made one underscore. i_ goes in front when the result
would still be refused, so 7-point Likert rating gives
i_7_point_likert_rating. A comment writes each control character of
an id, and each character outside the Basic Multilingual Plane, as
<U+XXXX>, so that it stays one line that lavaan reads as a comment.
API_SPEC.md section 19 states the rules.
Raises:
| Type | Description |
|---|---|
ValueError
|
when two different ids would share a name, since lavaan would merge them. Such a pair is two constructs, two indicators of one construct or a construct and an indicator. A construct and an indicator collide when the construct is measured by its own name or named like an indicator of another construct. An indicator shared by two constructs is allowed. |
References
Bollen, K. A. (1989). Structural equations with latent variables. Wiley. https://doi.org/10.1002/9781118619179
Source code in src/theoryforge/sem.py
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 | |
theoryforge.dossier.dossier(T)
¶
A reviewer-facing audit bundle in Markdown (API_SPEC.md section 20).
The header gives the checklist version, the aggregate score, the checklist
coverage, the gate and the blockers that failed. The rigour table prints
n/a for an item with nothing to assess. The severity list gives each
prediction's claim-form rubric values and, when the prediction declares a
severity, that value too, with a note when it exceeds the rubric by more
than 0.2. The test record lists every test outcome in file order, with what
it found and the observation, date and registration it records, and with
the post-data severity of post_data_severity() where it can be
computed, an open bound written as -inf or inf. The
evidence lists the prediction each entry supports, its direction and its
DOI. The provenance and the preregistration follow, and the implied
independencies appear once, in the preregistration.
Source code in src/theoryforge/dossier.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
Simulation¶
Integrate the construct network as a deterministic dynamical system.
theoryforge.simulate.simulate(T, steps=10, dt=0.1, k=1.0, damping=0.5, init=1.0, method='euler')
¶
Propagate the theory's construct network as a linear dynamical system.
Returns {states, dt, steps, k, damping, init, method, ignored, opposed,
trajectory}, where trajectory[t] is the state vector at time t*dt (t = 0..steps),
each value rounded to 6 decimals. The knobs and the method are echoed back as
given, because the trajectory cannot be reproduced without them. ignored
lists the ids of the propositions that couple nothing (moderates, associates, a
relation outside the schema's set, or an endpoint that is not a declared
construct), and opposed the [from, to] pairs that carry couplings of
both signs, which offset each other. Construct ids must be unique. A construct
without an id is a state with no couplings, and two of them do not count as
duplicates.
method = "exact" multiplies the state by the matrix exponential of
(A - dampingI)dt at every step, so the trajectory is the solution of the
linear system at those times whatever dt is. method = "euler" (the
default for this release) takes fixed explicit steps, which are stable only
when |1 + dtlambda| < 1 for every eigenvalue lambda of A - dampingI. A theory
that decays at rate damping therefore explodes with alternating sign once
dt*damping exceeds 2, and the steps inflate a sustained oscillation into
growth. The Euler run warns (UserWarning) when it departs from the exact
solution by more than 5 per cent of max(1, |exact|) at any step. The default
will change to "exact" in the next minor release.
steps must be a whole number of at least 0, dt a finite number above 0,
k, damping and init finite numbers, and method one of "euler"
and "exact". Anything else raises ValueError. When a state grows so large that
a million times it is not a finite double, the run stops with ValueError naming
the step and the state. The initial state is tested as step 0, so an init
beyond about 1.8e302 in absolute value stops there.
The model is deliberately simple: one gain k for every coupling, causes
and mediates taken as positive, moderates and associates coupling
nothing, functional_form not read and a common initial value for every
state. A linear system has one equilibrium, or a continuum of them when its
matrix is singular, and never two separate ones, so it cannot show bistability.
Source code in src/theoryforge/simulate.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | |
Reporting and deposit¶
Render a report from a theory, and deposit the result.
theoryforge.render_report(T, path, title=None, render=False, to='html')
¶
Write a Quarto report for the theory; render it with Quarto when render=True.
title defaults to "theoryforge report: <title-or-id>", also when empty. It is
written as a YAML double-quoted scalar with backslashes, double quotes and control
characters escaped, so a YAML parser reads back exactly the string passed. Quarto then
reads the title as Markdown, so raw TeX such as \emph{} is dropped from HTML output
and $\alpha$ becomes mathematics.
Returns the path of the written .qmd.
Source code in src/theoryforge/report_render.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
theoryforge.osf_push(T, token=None, node=None, filename=None, dry_run=True, base_url=_DEFAULT_BASE, overwrite=False)
¶
Deposit the theory's dossier on OSF.
With dry_run=True (default) the planned request is returned and nothing is sent. A live
upload (dry_run=False) requires both token and node (the OSF project id). An
empty node or filename counts as absent.
OSF storage refuses to create a file whose name already exists in the project folder
(HTTP 409), so depositing the same theory twice under the default filename fails. Pass a
version-specific filename, or overwrite=True to add a new version of the existing
file: the folder is listed first and the dossier is sent to that file's upload link, the
WaterButler route that records a new OSF version. When the folder holds no file of that
name, overwrite=True creates it as usual. The dry run then also returns the
lookup request.
Raises :class:OSFUploadError, a subclass of urllib.error.HTTPError, when OSF answers
with a status outside 2xx, so a refused upload is never returned as a completed one.
Source code in src/theoryforge/osf.py
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
theoryforge.OSFUploadError
¶
Bases: HTTPError
OSF storage refused a request (API_SPEC.md section 25).
A subclass of urllib.error.HTTPError, so a handler written for the error
that urlopen raises still catches it, with the same code, headers and
readable body. Its str() is the message the R twin stops with:
OSF upload failed with HTTP <code>, with a hint on adding a version for a
409, or OSF folder listing failed with HTTP <code> for the lookup that
overwrite=True makes first.
Source code in src/theoryforge/osf.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | |
Literature layer¶
Map the literature a theory sits in, and check what of it the theory does not yet cite.
theoryforge.read_corpus(path)
¶
Read a literature corpus from YAML or JSON (JSON when the suffix is .json).
The file is read by the rules read follows, which are the R twin's
(API_SPEC.md section 3), so an unquoted keyword such as y or n stays a
string and a repeated key is refused.
Source code in src/theoryforge/lit.py
41 42 43 44 45 46 47 48 49 50 51 | |
theoryforge.litmap(corpus, min_link=DEFAULT_MIN_LINK, method='components', min_cocitation=None, min_theme_size=2, max_theme_size=10, max_df=1.0)
¶
Keyword co-occurrence, themes and co-citation, all deterministic.
min_link is the smallest count a keyword pair needs to be an edge, and
min_cocitation the same for a reference pair (min_link when None).
Co-citation maps of real corpora are large: 200 OpenAlex records give about
11,000 reference pairs at a threshold of 2, so a higher min_cocitation
is often wanted. Both must be positive integers. The corpus is checked as
API_SPEC.md section 14 describes: integer entries become decimal strings,
and booleans, fractions and nested values are refused.
A reference that holds a DOI is keyed by the DOI, lower-cased and without
its resolver prefix, as new_evidence_dois compares DOIs. One work cited
as 10.1016/J.BRAT.2015.10.002 in one record and as
https://doi.org/10.1016/j.brat.2015.10.002 in another is therefore one
node, labelled 10.1016/j.brat.2015.10.002. Any other reference, such as
a Scopus or OpenAlex identifier, is compared exactly as written, and so is
every keyword.
method chooses how keywords are grouped into themes. With
"components", the default, a theme is a connected component of the
keyword map, and on a real corpus a few keywords shared by most records
join nearly every keyword into one. When the largest theme holds more than
half the linked keywords, a UserWarning says so: such themes, and any
landscape built on them, do not describe the field. The result is returned
unchanged.
"simple_centres" is the co-word clustering of Coulter et al. (1998) and
Cobo et al. (2011). Each link is weighted by the equivalence index
c * c / (df_a * df_b), where c counts the records holding both
keywords and df the records holding each. The strongest link between
two unassigned keywords seeds a theme, which takes its strongest unassigned
neighbour until it holds max_theme_size keywords, ties going to the
keyword first in code-point order. A keyword whose links all reach
keywords already in themes joins none. Themes smaller than
min_theme_size are dropped. Keywords in more than max_df of the
records are field terms, left out of the map and reported as
field_terms. Each theme gains its centrality (ten times the summed
index of its links to the keywords of other themes) and its density (100
times the summed index of its internal links, over its size), the measures
of Callon et al. (1991) as Cobo et al. (2011) scale them. It also gains its quadrant in the
strategic diagram, split here at the median of each: motor (both high),
basic (central but not dense), niche (dense but not central) or
emerging_or_declining (both low). The record gains method,
parameters (the five settings used) and field_terms. Components
remain the default for this release.
min_theme_size must be a positive integer, max_theme_size an
integer of at least 2 no smaller than it, and max_df a number from 0 to
1. They are checked with either method, although only simple centres reads
them.
References: Callon, Courtial and Laville (1991), Scientometrics 22, 155-205, https://doi.org/10.1007/BF02019280. Coulter, Monarch and Konda (1998), Journal of the American Society for Information Science 49, 1206-1223. Cobo, López-Herrera, Herrera-Viedma and Herrera (2011), Journal of Informetrics 5, 146-166, https://doi.org/10.1016/j.joi.2010.10.002.
Source code in src/theoryforge/lit.py
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 | |
theoryforge.landscape(theory, corpus, min_link=DEFAULT_MIN_LINK, max_token_share=DEFAULT_MAX_TOKEN_SHARE, method='components')
¶
Map a theory and its registered alternatives onto the literature's themes.
A theme is matched by the words its keywords share with the focal theory's
construct labels, or with an alternative's label and key constructs, and
every match reports those words (focal_terms, alternative_terms).
Three kinds of word never match. Field tokens, the words most of the corpus
shares, are those in the keywords of more than max_token_share of the
records. Phenomenon tokens are the words of the theory's title, which names
the phenomenon that the focal theory and its rivals all explain. A
construct word that also appears in the title does not match either. The
third kind is the words that name a kind of account, held in
theoryforge.lit.THEORY_WORDS: theory, model, account, hypothesis,
framework and approach, with their plurals. One shared word is enough for
a match.
A theme is under_theorised when none of the registered accounts (the
focal theory and its registered alternatives) addresses it, covered
when one does and crowded when two or more do. A crowded theme calls for
predictions that discriminate between the accounts, and is not a finding of
redundancy. The two lists keep their 0.6.0 keys, under_theorised_fronts
and redundancy_risk.
min_link must be a positive integer, max_token_share a number
from 0 to 1 and method one of litmap's, checked in that order and
before the corpus (API_SPEC.md section 15). The themes are litmap's, built
by method with litmap's other defaults. With "components", the same
warning is given when one theme holds most of the linked keywords. With
"simple_centres", the record gains method after theory_id and
each theme its centrality, density and quadrant.
Source code in src/theoryforge/lit.py
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 | |
theoryforge.fetch_corpus(query, per_page=25, mailto=None, api_key=None, max_records=None)
¶
Build a corpus from the OpenAlex API (network call).
This adapter is assistive. It depends on a live external service whose
results change over time, so it sits outside the package's deterministic
core. Each work is mapped to {id, doi, title, year, keywords, references},
with the DOI as OpenAlex gives it and the top five concepts standing in for
keywords when a work has none.
OpenAlex returns the works that match a search in pages of per_page,
ranked by relevance, and a search usually matches far more works than one
page holds. per_page may be 1 to 200, but OpenAlex supports pages of up
to 100 and has deprecated larger ones. A max_records above per_page
pages on through OpenAlex's cursor until that many works are collected or
the results run out. By default it equals per_page, so one request is
made. Each page is one request and costs USD 0.001. OpenAlex allows USD 0.10
a day without a key, about 100 pages, and USD 1 with a free key.
api_key defaults to the OPENALEX_API_KEY environment variable, and
"" sends no key. The key is sent only in an Authorization: Bearer
header, never in the URL, the corpus or an error message. mailto is
still sent, but OpenAlex now ignores it, since API keys replaced the polite
pool it once selected.
When OpenAlex refuses a request, as it does with HTTP 429 once the budget is
spent, OpenAlexHTTPError (a urllib.error.HTTPError) is raised with
the status and OpenAlex's own message, and the request is not retried. A
response without a results list raises ValueError.
The corpus records where, when and how it was fetched in source. It
gives the service, endpoint and query and the UTC time of retrieval
(retrieved), then the number of works that matched (total_count),
the number kept (n_records), the page size and the order (sort).
The date matters because the keywords change. Since late September 2026,
OpenAlex has written each work's keywords with a language model that reads
its title, abstract and venue. It merges and splits that vocabulary over
time. Works without keywords fall back to their concepts, a deprecated
vocabulary with capitalised names that do not match the lower-case keywords
in litmap. Save a fetched corpus and work from the saved file.
Source code in src/theoryforge/lit.py
816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 | |
theoryforge.new_evidence_dois(theory, candidate_dois)
¶
DOIs in candidate_dois not already cited by the theory's evidence or alternatives.
Compares by normalised form, so a fresh literature search, for example via OpenAlex,
Scopus, or any other source, can be checked against what the theory already engages
with. The normalised form is the DOI itself, trimmed, lowercased (ASCII letters only)
and percent-decoded, wherever it sits in the text, so doi: 10..., DOI 10...,
doi.org/10..., https://www.doi.org/10... and a URL with %2F all match the bare
DOI, and trailing full stops, commas and semicolons are dropped. Returns the
qualifying DOIs in their original form, deduplicated and sorted by normalised form.
Deterministic and takes no network dependency: the search itself is left to
whichever literature tool the caller prefers.
Source code in src/theoryforge/lit.py
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 | |