Scientific components

Backend contract and adapters

Provider-neutral backend contracts.

class p2p.backends.base.Backend(*args, **kwargs)[source]

Minimal interface implemented by backend adapters.

chat(*, system, user)[source]

Return normalized text and token usage.

Parameters:
  • system (str)

  • user (str)

Return type:

BackendResponse

class p2p.backends.base.BackendResponse(answer_text, usage=<factory>)[source]

Normalized response returned by every backend adapter.

Parameters:
exception p2p.backends.base.NoAnswerTextError[source]

The provider returned no answer text: a refusal, block, or filter.

The message names the provider’s stated reason so that it survives into the recorded [ERROR] answer when every retry fails.

class p2p.backends.base.TokenUsage(input_tokens=0, output_tokens=0, cached_input_tokens=0, reasoning_tokens=0)[source]

Normalized token counts returned by every backend.

Parameters:
  • input_tokens (int)

  • output_tokens (int)

  • cached_input_tokens (int)

  • reasoning_tokens (int)

Deterministic offline backend for tests and local smoke runs.

class p2p.backends.dummy.DummyBackend(*, model_name='dummy', answer_text='DummyBackend called.', usage=None, **_)[source]

Return a configured response without making an external call.

Parameters:
  • model_name (str)

  • answer_text (str)

  • usage (TokenUsage | None)

  • _ (Any)

chat(*, system, user)[source]

Return the configured normalized response.

Parameters:
  • system (str)

  • user (str)

Return type:

BackendResponse

Native local Ollama backend adapter.

class p2p.backends.ollama.OllamaBackend(*, model_name='gemma3:4b', base_url='http://localhost:11434', temperature=0.0, max_tokens=128, timeout=60.0, think=False, keep_alive=None, options=None, transport=None)[source]

Call Ollama’s native non-streaming chat endpoint.

Parameters:
  • model_name (str)

  • base_url (str)

  • temperature (float | None)

  • max_tokens (int | None)

  • timeout (float)

  • think (bool | str | None)

  • keep_alive (str | int | None)

  • options (Mapping[str, Any] | None)

  • transport (Transport | None)

chat(*, system, user)[source]

Return normalized Ollama text and usage.

Parameters:
  • system (str)

  • user (str)

Return type:

BackendResponse

OpenAI backend adapter: Chat Completions or Responses, chosen explicitly.

class p2p.backends.openai.OpenAIBackend(*, model_name, temperature=0.7, max_tokens=1024, transport='chat_completions', store=False, client=None, api_key=None)[source]

Normalize OpenAI text responses.

transport names the API: chat_completions (the default) or responses. It is never switched silently. P2P’s provider-neutral max_tokens is sent as max_completion_tokens (Chat Completions) or max_output_tokens (Responses); both bound visible and reasoning tokens. store is sent on every request because both APIs may store responses by default; P2P never retrieves them, so it defaults to false.

Parameters:
  • model_name (str)

  • temperature (float | None)

  • max_tokens (int | None)

  • transport (str)

  • store (bool)

  • api_key (str | None)

chat(*, system, user)[source]

Return normalized OpenAI text and usage.

Parameters:
  • system (str)

  • user (str)

Return type:

BackendResponse

DeepSeek V4 Chat Completions backend adapter.

class p2p.backends.deepseek.DeepSeekBackend(*, model_name='deepseek-v4-flash', temperature=0.7, max_tokens=1024, thinking='disabled', reasoning_effort=None, base_url=None, client=None, api_key=None)[source]

Normalize DeepSeek’s OpenAI-compatible chat API.

Thinking is disabled by default because P2P’s survey and persona workflows generally require short, directly parseable responses. Set thinking="enabled" explicitly for reasoning workloads.

Parameters:
  • model_name (str)

  • temperature (float | None)

  • max_tokens (int | None)

  • thinking (str)

  • reasoning_effort (str | None)

  • base_url (str | None)

  • api_key (str | None)

chat(*, system, user)[source]

Return normalized DeepSeek text and billable token usage.

Parameters:
  • system (str)

  • user (str)

Return type:

BackendResponse

Google Gemini backend adapter.

class p2p.backends.gemini.GeminiBackend(*, model_name, temperature=0.7, max_tokens=1024, system_instruction=None, client=None, config_factory=None, api_key=None)[source]

Normalize Gemini GenerateContent responses.

The model ID has no default: Gemini models are retired on a published schedule, so every run names the model it uses.

Parameters:
  • model_name (str)

  • temperature (float | None)

  • max_tokens (int | None)

  • system_instruction (str | None)

  • config_factory (Callable[..., Any] | None)

  • api_key (str | None)

chat(*, system, user)[source]

Return normalized Gemini text and usage.

Parameters:
  • system (str)

  • user (str)

Return type:

BackendResponse

Simulated respondents: a deterministic, rule-based backend. Not a language model.

It lets Stage 1 run end to end offline (persona generation, attribute discovery, survey answers), for demonstrations and tests. Behaviour comes from a declarative world:

world:
  setting: a fictional town              # used in persona text (optional)
  attributes:
    everyday priority:                   # attribute name
      relevance: survey                  # offered by survey-level discovery
      values:
        green and outdoors:              # value; appears verbatim in personas
          weight: 2.0                    # how strongly it pulls answers
          prefers: [A shared garden, Protected cycle lanes]
          describe: grows vegetables and cycles everywhere   # optional
        evenings and culture:
          prefers: [A night market]
    age group:
      values: {20s: {}, 40s: {}, 60s: {}}

Answers. For a question offered as text ([A] option, [B] option, ...), each option scores the summed weight of the persona’s values that prefer it, plus deterministic noise (Gumbel, scale noise) from a hash of the persona, the question and the option. The highest score is answered. With no matching preferences the answer is the noise alone, so any survey can be answered.

Personas. A request to vary named attributes gets one value per attribute that the world knows, drawn with a seeded generator; unknown attributes are named but left unvalued. Attribute discovery returns the world’s attributes, those whose values prefer an option of the question first.

Usage is reported as zero tokens: nothing is metered.

class p2p.backends.simulated.SimulatedBackend(*, model_name='simulated-respondents', world=None, seed=0, noise=1.0)[source]

Answer P2P’s Stage-1 prompts with deterministic rules (see module docstring).

Parameters:
  • model_name (str)

  • world (Mapping[str, Any] | None)

  • seed (int)

  • noise (float)

chat(*, system, user)[source]

Answer one prompt by rule; usage is always zero.

Parameters:
  • system (str)

  • user (str)

Return type:

BackendResponse

Agents and conductors

Backend-neutral survey agent.

class p2p.agents.base.Agent(*, eid, backend, endowment='', formality='')[source]

Combine persona instructions with an injected backend.

Parameters:
  • eid (str)

  • endowment (str)

  • formality (str)

get_system_prompt()[source]

Combine nonempty persona and formatting instructions.

Return type:

str

answer(question)[source]

Return one normalized response without accounting for it.

Parameters:

question (str)

Return type:

AgentResponse

class p2p.agents.base.AgentResponse(eid, answer_text, usage, model_name)[source]

Normalized response returned to a survey conductor.

Parameters:
  • eid (str)

  • answer_text (str)

  • usage (TokenUsage)

  • model_name (str)

Configuration for backend-neutral survey agents.

class p2p.agents.config.AgentConfig(agent_type, model_name, formality='', agent_kwargs=<factory>, delay=0.0)[source]

Settings used by an injected agent/conductor factory.

Parameters:
  • agent_type (str)

  • model_name (str)

  • formality (str)

  • agent_kwargs (Mapping[str, Any])

  • delay (float)

Factories connecting agents to the single backend registry.

p2p.agents.factory.create_agent(*, config, eid, endowment='', backend=None, backend_factory=None)[source]

Create one generic agent using an injected or registered backend.

Parameters:
  • config (AgentConfig)

  • eid (str)

  • endowment (str)

  • backend_factory (Callable[[...], Any] | None)

Return type:

Agent

p2p.agents.factory.create_backend(*, backend_type, model_name, **backend_kwargs)[source]

Create a backend through p2p.registry.BACKENDS.

Parameters:
  • backend_type (str)

  • model_name (str)

  • backend_kwargs (Any)

Survey conduction over backend-neutral agents.

class p2p.agents.conductor.SurveyConductor(*, survey, endowments, output_path=None, agent_type='dummy', model_name='dummy', formality='', cleaner=None, clean=False, verbose=True, max_retries=3, retry_delay=0.5, token_tracker=None, agent_factory=<function create_agent>)[source]

Elicit survey answers and account for normalized usage once.

Parameters:
  • output_path (str | None)

  • agent_type (str)

  • model_name (str)

  • formality (str)

  • clean (bool)

  • verbose (bool)

  • max_retries (int)

  • retry_delay (float)

run(*, save=False, parallel_worker=False, **agent_kwargs)[source]

Run the survey sequentially over all endowments.

Parameters:
  • save (bool)

  • parallel_worker (bool)

  • agent_kwargs (Any)

Return type:

list[dict[str, Any]]

to_agent_records()[source]

Convert flat answers to vectors ordered like the survey.

Return type:

list[dict[str, Any]]

save()[source]

Save normalized flat responses as JSON.

Return type:

None

Survey, responses, and endowments

class p2p.survey.converter.Survey(csv_path, config_path)[source]

Load, manage, split, and save survey questions.

The YAML schema maps the logical question_id, question_text, code_to_answer, and optional split fields to CSV column names.

csv_path

Path to the survey CSV.

config

Loaded YAML field mapping.

questions

Parsed question dictionaries. Each record contains an ID, question text, code/answer mappings, and an optional split.

split_questions(method='manual', split_map=None, train_ratio=0.7, valid_ratio=0.15, seed=None, save=False, save_path=None)[source]

Assigns ‘split’ attribute to each question.

Parameters: - method: “manual” or “from_map” - split_map: dict mapping question IDs to split labels - train_ratio, valid_ratio: used only if method=”manual” - seed: random seed for reproducibility - save: if True, write the split info back to a CSV - save_path: custom output path; defaults to original csv_path if None

save(filepath=None)[source]

Saves the current survey state (including splits if present) to a CSV. Column names are derived from the YAML config. If splits exist but no split column is specified in the config, a column named ‘split’ is added.

report_splits(show_percentage=True)[source]

Prints a summary of the split distribution among survey questions.

Parameters: - show_percentage (bool): If True, also print percentages.

classmethod from_questions(questions, config=None, csv_path=None, config_path=None)[source]

Instantiate a Survey object from a list of preloaded questions.

Parameters:
  • questions (list) – List of question dicts. Must include keys like ‘id’, ‘question’, ‘code_to_answer’, etc.

  • config (dict, optional) – YAML-style config dict specifying field names.

  • csv_path (str, optional) – Placeholder CSV path, if needed for compatibility.

  • config_path (str, optional) – Placeholder config path.

Returns:

An instantiated Survey object with preloaded questions.

Return type:

Survey

classmethod clone_with_subset(existing_survey, subset_questions)[source]

Clone a Survey object but restrict it to a subset of questions.

Parameters:
  • existing_survey (Survey) – The original Survey instance.

  • subset_questions (list) – List of question dicts to retain.

Returns:

A cloned Survey object containing only the subset.

Return type:

Survey

class p2p.survey.converter.BinaryExtendedSurvey(csv_path, config_path)[source]
get_binary_variants(original_id)[source]

Return list of binary QIDs derived from a given original question ID.

classmethod clone_with_subset(existing_survey, subset_questions)[source]

Clone a BinaryExtendedSurvey but restrict to a subset of binary questions. Preserves original question mappings.

class p2p.responses.converter.Responses(source, survey, output_format='code', clean=True, cleaner=None)[source]

Container class for loading, accessing, and saving a matrix of survey responses. Supports matrix-style CSV input or list-of-dict input, and handles answer-to-code mapping.

output_format

Determines how responses are stored. - ‘code’: responses are mapped to numeric or symbolic codes. - ‘answer’: responses are kept as raw (cleaned) answer text.

Type:

str

survey

Survey object containing questions and associated metadata.

Type:

Survey

questions

Mapping from question ID to metadata (extracted from the survey).

Type:

dict

responses

Nested dictionary where responses[qid][eid] = value (code or answer).

Type:

dict

clean

If True, apply normalization and heuristic cleaning to free-form agent responses.

Type:

bool

cleaner

Optional cleaner instance to use for response normalization and disambiguation. If None and clean=True, a default ResponseCleaner will be instantiated using the survey.

Type:

ResponseCleaner

get(qid, eid)[source]

Retrieve the response of a specific endowment to a specific question.

Parameters:
  • qid (str) – Question ID.

  • eid (str) – Endowment ID.

Returns:

Encoded or raw response, or None if not found.

Return type:

str or None

get_question_vector(qid)[source]

Get all responses to a specific question.

Parameters:

qid (str) – Question ID.

Returns:

Mapping from eid to response value.

Return type:

dict

get_agent_vector(eid)[source]

Get all responses given by a specific agent.

Parameters:

eid (str) – Endowment ID.

Returns:

Mapping from qid to response value.

Return type:

dict

get_matrix_by_split(split, survey=None, dropna=False)[source]

Returns a [qid × eid] matrix of responses for a given question split.

Parameters:
  • split (str) – Split name (e.g., ‘train’, ‘valid’, ‘test’).

  • survey (Survey, optional) – Defaults to self.survey if not specified.

  • dropna (bool) – If True, drop rows with any missing values.

Returns:

Response matrix for the specified question split.

Return type:

pd.DataFrame

sample_fraction(fraction, seed=101)[source]

Samples a fraction of questions while preserving the original split proportions.

Parameters:
  • fraction (float) – Fraction of total questions to retain (0 < fraction <= 1).

  • seed (int) – Random seed for reproducibility.

Returns:

A new Responses object containing only the sampled questions.

Return type:

Responses

sample_trainvalid_fraction(fraction, seed=101)[source]

Samples a fraction of questions from the train and valid splits while keeping the test split fully intact. Preserves original train/valid proportions.

Parameters:
  • fraction (float) – Fraction of train+valid questions to retain (0 < fraction <= 1).

  • seed (int) – Random seed for reproducibility.

Returns:

A new Responses object containing the sampled questions.

Return type:

Responses

save(path)[source]

Save the response matrix to a matrix-style CSV file.

Parameters:

path (str) – Output file path.

classmethod from_agent_records(agent_records, survey, output_format='answer', clean=True, cleaner=None)[source]

Construct a Responses object from a list of agent records.

Parameters:
  • agent_records (list[dict]) – Each record should have: - “agent_id”: str, the EID - “responses”: list of answers (aligned with survey.questions)

  • survey (Survey) – Survey instance (with questions metadata).

  • output_format (str) – “answer” (default) or “code”.

  • clean (bool) – Whether to apply cleaning and normalization.

  • cleaner (ResponseCleaner, optional) – Optional custom cleaner.

Returns:

A populated Responses instance.

Return type:

Responses

clone_with_subset(subset_qids, subset_survey=None)[source]

Returns a new Responses object restricted to a subset of question IDs.

Parameters:
  • subset_qids (list[str]) – List of question IDs to retain.

  • subset_survey (Survey, optional) – A survey object that contains only the selected questions. If not provided, the existing survey will be filtered.

Returns:

A new Responses instance with restricted question-response matrix.

Return type:

Responses

clone_with_agents(subset_eids)[source]

Returns a new Responses object restricted to a subset of agent (EID) IDs.

Parameters:

subset_eids (set[str]) – Set of agent EIDs to retain.

Returns:

A new Responses instance with restricted agent-response matrix.

Return type:

Responses

to_format(output_format)[source]

Return a new Responses object with the same data but a different output_format.

Parameters:

output_format (str)

class p2p.responses.converter.BinaryExtendedResponses(source, survey, output_format='code', clean=True, cleaner=None)[source]

Extension of the Responses class for binary-extended surveys.

Automatically maps multiclass responses into binary format by expanding each original question into one binary sub-question per answer option.

original_questions_by_id

Mapping of original (pre-expansion) question IDs to metadata.

Type:

dict

clean

Whether to apply response cleaning and normalization (inherited from Responses).

Type:

bool

cleaner

Instance used for cleaning answers (inherited from Responses).

Type:

ResponseCleaner

clone_with_subset(subset_qids, subset_survey=None)[source]

Clone a BinaryExtendedResponses object with only a subset of binary question IDs.

Parameters:
  • subset_qids (list[str]) – List of binary question IDs to retain.

  • subset_survey (BinaryExtendedSurvey, optional) – A filtered survey instance. If None, it will be inferred from the current survey.

Returns:

New response object with restricted binary questions.

Return type:

BinaryExtendedResponses

class p2p.endowments.manager.Endowments(csv_path=None)[source]

A structured container and access layer for managing a pool of LLM agent endowments.

Each endowment defines a persona used to condition agent behaviour in simulation or empirical experiments.

csv_path

Path to the endowment CSV file

Type:

str

endowments

List of all endowment entries, each a dictionary with: - eid (str): Unique identifier - endow_text (str): Identity-conditioning prompt passed to LLM - role (str): Either ‘ground_truth’ or ‘proxy’ - weight (float or None): Importance weight (sampling or regression)

Type:

list[dict]

index

Maps eid to endowment dictionary for fast lookup

Type:

dict

Key Methods:
  • load(cls, path): Class method for instantiating from CSV

  • get_endowments(): Return all endowment entries

  • get_endowment_by_eid(eid): Lookup a single endowment

  • get_endowments_by_role(role): Filter by role (‘ground_truth’ or ‘proxy’)

  • get_eids_by_role(role): List eids matching a role

  • update_weights(weights): Partially update weights by eid

  • save(path): Save the endowments back to a CSV file

classmethod load(path)[source]

Load an Endowments instance from a CSV file. This method can be extended for validation, logging, or alternative formats.

update_weights(weights)[source]

Update weights for a subset of endowments.

Parameters:

weights (dict) – Mapping from eid (str) to new weight (float or None)

Behaviour:
  • Only updates entries whose eid is present in the weights dict

  • Ignores eids that do not exist in the current pool

  • Supports partial updating (e.g., just ground_truth or just proxy)

initialize_ground_truth_weights(seed=None, save=False, save_path=None)[source]

Randomly initialize normalized weights for ‘ground_truth’ endowments.

Parameters:
  • seed (int, optional) – Random seed for reproducibility.

  • save (bool) – Whether to save the updated weights to CSV.

  • save_path (str, optional) – Path to save the updated CSV. Defaults to self.csv_path.

Raises:
  • ValueError – If no ‘ground_truth’ endowments exist.

  • Warning – If weights already exist and save=False.

report_role_assignment(show_percentage=True)[source]

Prints a summary of the role distribution among survey questions.

Parameters: - show_percentage (bool): If True, also print percentages.

clone_with_fraction(fraction=1.0, seed=101)[source]

Returns a new ActiveEndowments object with a sampled subset of endowments, preserving the proxy/ground_truth ratio.

Parameters:
  • fraction (float) – Fraction of endowments to retain (0 < fraction ≤ 1).

  • seed (int) – Random seed for reproducibility.

Returns:

A new object with sampled endowments.

Return type:

ActiveEndowments

clone_with_proxy_fraction(fraction=1.0, seed=101)[source]

Returns a new Endowments object with a subsampled set of proxy agents, while retaining all ground_truth agents. The sampling preserves the proxy structure but shrinks its size.

Parameters:
  • fraction (float) – Fraction of proxy endowments to retain (0 < fraction ≤ 1).

  • seed (int) – Random seed for reproducibility.

Returns:

A new object with modified endowments.

Return type:

Endowments

renormalize_ground_truth_weights()[source]

Re-normalizes the weights of ground truth endowments so they sum to 1.

Preserves relative weights across ground truth agents. Raises an error if total weight is zero.

This is useful after subsampling endowments, since dropped agents may invalidate the original normalization.

static compute_entropy(endowment_list, responses, survey, normalize=True)[source]

Computes average normalized entropy across survey questions for a given list of endowments.

Parameters:
  • endowment_list (list[dict]) – List of endowments, each with an ‘eid’.

  • responses (Responses) – Object providing get_agent_vector(eid).

  • survey (Survey) – Survey object with question metadata.

  • normalize (bool) – Whether to normalize entropy ∈ [0,1].

Returns:

Average entropy across all answered questions.

Return type:

float

class p2p.endowments.manager.ActiveEndowments(csv_path=None)[source]

Subclass of Endowments that supports mode-aware experiments. Each endowment is expected to include: - ‘mode’: a tuple of theme labels, e.g., (‘core’, ‘economics’) - ‘attributes’: list of attribute names used during generation

get_entropy_by_mode(responses, survey, normalize=True, top_k=None)[source]

Computes normalized entropy for each mode based on responses from agents sharing that mode.

Parameters:
  • responses (Responses) – An instance with agent responses (must implement get_agent_vector).

  • survey (Survey) – Survey object with question metadata.

  • normalize (bool) – Whether to return normalized entropy scores.

  • top_k (int or None) – If set, only return the top_k highest-entropy modes.

Returns:

Sorted list of (mode, entropy score, number of endowments), descending by entropy.

Return type:

list[tuple]

clone_with_proxy_modes(included_modes, n_proxies=30, seed=None)[source]

Clone a subset of the endowments retaining: - All ground truth agents. - A random subset of proxy agents from the included_modes (default: 30).

Parameters:
  • included_modes (list[str]) – Modes to include when sampling proxy agents.

  • n_proxies (int) – Number of proxy agents to sample (default: 30).

  • seed (int, optional) – Random seed for reproducibility.

Returns:

New object containing ground truth + sampled proxy agents.

Return type:

ActiveEndowments

Generation

class p2p.generators.active_endowment_generator.ActiveEndowmentGenerator(survey, endowment_model, attribute_learner, agent_config, attribute_bank, conductor_factory=None, variability_tracker=None, target_n=100, initial_n=10, num_update_steps=4, seed=None, parallel=False, max_workers=10, verbose=True, question_patch=None, logger=None, token_tracker=None, enable_survey_attributes=True, checkpoint_callback=None, resume_state=None)[source]

Generate endowments adaptively to increase response variability in simulated survey experiments.

The generator owns a deep-copied, run-local attribute bank. Attributes discovered during a run use the reserved modes survey.global and question.<QID>. Response simulation is delegated to an injected conductor factory, keeping backend and agent integration outside this module.

Parameters:
  • agent_config (Any)

  • attribute_bank (dict)

  • conductor_factory (Callable[[...], Any] | None)

  • question_patch (dict | None)

  • enable_survey_attributes (bool)

  • checkpoint_callback (Callable[[dict[str, Any]], Any] | None)

  • resume_state (dict[str, Any] | None)

survey

Survey containing question definitions and metadata.

endowment_model

Object that generates endowments from attribute lists.

attribute_learner

Object that discovers survey and question attributes.

agent_config

Agent settings consumed by the injected conductor.

attribute_bank

Run-local copy of the injected attribute bank.

conductor_factory

Optional callable that constructs survey conductors.

tracker

Variability tracker used for entropy-guided sampling.

target_n

Total number of endowments to generate.

initial_n

Number sampled per initial mode.

num_update_steps

Maximum number of adaptive expansion steps.

parallel

Whether sampling and simulation may use worker threads.

max_workers

Maximum number of worker threads.

question_patch

Question-targeting allocation settings.

seed

Random seed used by the generator.

response_log

Collected agent-level response records.

mode_history

Modes and attributes used during sampling.

token_tracker

Optional injected usage tracker.

checkpoint_state()[source]

Return JSON-safe state sufficient for an exact paid-run resume.

Return type:

dict[str, Any]

add_survey_attributes()[source]

Register attributes related to the overall survey under the reserved survey.global mode.

add_question_attributes(qid, max_attributes=None)[source]

Register attributes for a question under its reserved question.<QID> mode.

generate()[source]

Orchestrates the full active generation loop. Starts with initial sampling, then repeatedly expands the pool using entropy-weighted sampling until target_n endowments are generated.

Returns:

An ActiveEndowments manager with all generated endowments.

export_mode_summary(path)[source]

Saves a summary of endowments generated per mode to CSV.

Parameters:

path (str) – Path to save the summary file.

export_responses_to_csv(path, format='code')[source]

Converts and saves all collected agent responses to a CSV file.

Parameters:
  • path (str) – Path to save the file.

  • format – Output format to use in Responses class (e.g., “code” or “text”).

export_attribute_bank(filepath='attribute_bank.yaml')[source]

Saves the current attribute bank to a YAML file.

Parameters:

filepath (str) – Target path for saving the attribute bank.

get_responses(output_format='answer')[source]

Converts the response log to a Responses object (for analysis or saving).

Parameters:

output_format (str) – Either “code” or “text” for the response content.

Returns:

Structured Responses object.

Return type:

Responses

Backend-neutral attribute discovery for active generation.

exception p2p.generators.attribute_learner.AttributeParseError[source]

Raised when an attribute response contains no usable values.

class p2p.generators.attribute_learner.AttributeLearner(*, survey, backend, tracker=None, max_attributes=None, max_retries=2, retry_base_delay=0.5)[source]

Discover concise survey attributes through one backend.

Parameters:
  • max_attributes (int | None)

  • max_retries (int)

  • retry_base_delay (float)

Backend-neutral persona generation for active endowments.

class p2p.generators.endowment_model.EndowmentModel(*, backend, tracker=None, batch_size=10, delay=0.5, retry_failed=True, randomize_attributes=True, max_attributes=12, seed=None, max_retries=1, retry_base_delay=0.0)[source]

Generate normalized persona records through one backend.

Parameters:
  • batch_size (int)

  • delay (float)

  • retry_failed (bool)

  • randomize_attributes (bool)

  • max_attributes (int)

  • seed (int | None)

  • max_retries (int)

  • retry_base_delay (float)

class p2p.generators.theme_variability_tracker.ThemeVariabilityTracker(questions, smoothing=0.1)[source]

Tracks and updates the entropy of survey question responses and the overall response diversity (variability) of agent modes across update steps.

This tracker maintains a running log of agent responses by question and by mode, calculates normalized entropy per question, computes weighted variability per mode, and generates adaptive sampling weights based on response diversity.

It supports plotting of question entropy trajectories, theme variability over time, and current entropy levels to assist in diagnosing diversity and guiding active sampling.

Parameters:
  • questions (list[dict])

  • smoothing (float)

property tracked_qids

Returns: list[str]: The list of question IDs being tracked.

static categorical_entropy(counts)[source]

Computes normalized entropy for a categorical distribution.

Parameters:

counts (list[int]) – Counts of each category label.

Returns:

Normalized entropy ∈ [0, 1], with 0 = pure, 1 = uniform.

Return type:

float

compute_entropy(qid, responses)[source]

Computes normalized entropy for a specific question using its response history.

Parameters:
  • qid (str) – Question ID.

  • responses (list[str]) – List of response strings.

Returns:

Normalized entropy for the given question and responses.

Return type:

float

update_from_records(agent_records)[source]

Updates internal response logs and entropy/variability metrics based on a batch of responses.

Parameters:

agent_records (list[dict]) – List of agent records. Each must contain: - ‘mode’: tuple of attribute labels - ‘responses’: list of answers aligned with self.qid_order

filter_records(agent_records, survey)[source]

Filters agent responses to retain only those for tracked question IDs.

Parameters:
  • agent_records (list[dict]) – List of agent records with full response lists.

  • survey (Survey) – Survey object with all questions.

Returns:

Filtered agent records with responses only for tracked questions.

Return type:

list[dict]

compute_global_question_entropy()[source]

Computes the normalized entropy for each tracked question based on all responses.

Returns:

qid -> normalized entropy

Return type:

dict

compute_inverse_entropy_weights()[source]

Computes normalized inverse entropy weights over questions for use in weighted variability scoring.

Returns:

qid -> normalized inverse entropy weight (sums to 1)

Return type:

dict

compute_theme_variability(mode)[source]

Computes the weighted average entropy across all tracked questions for a given agent mode.

Parameters:

mode (tuple) – Mode label (e.g. (‘core’, ‘economics’))

Returns:

Variability score ∈ [0, 1]

Return type:

float

get_scores()[source]

Computes variability scores with optional smoothing.

Returns:

mode_tuple -> smoothed variability score

Return type:

dict

get_softmax_weights(temperature=0.3)[source]

Converts theme variability scores to softmax probabilities for adaptive sampling.

Parameters:

temperature (float) – Softmax temperature. 0 = greedy, ↑ = smoother.

Returns:

mode_tuple -> sampling probability

Return type:

dict

get_low_entropy_questions(top_k=3)[source]

Returns a list of top_k questions with the lowest normalized entropy.

Parameters:

top_k (int) – Number of low-entropy questions to return.

Returns:

Question IDs with lowest entropy.

Return type:

list[str]

plot_question_entropy_trajectories(base_height=6, width_per_step=1, save_path=None)[source]

Plots the entropy trajectory over time for each tracked question, color-coded by trend (rising, falling, stable).

Parameters:
  • base_height (int) – Height of plot in inches.

  • width_per_step (int) – Width scaling factor per update step.

  • save_path (str) – Path for the saved figure. Default is None.

plot_theme_variability_trajectories(attribute_bank, base_height=6, width_per_step=1, save_path=None)[source]

Plots variability trajectories for each mode over update steps, color-coded by template group using palettes from the attribute bank.

Parameters:
  • attribute_bank (dict) – YAML-style config of attribute templates.

  • base_height (int) – Height of plot.

  • width_per_step (int) – Width scaling factor per update step.

  • save_path (str) – Path for the saved figure. Default is None.

plot_theme_variability_trajectories_legacy(attribute_bank, base_height=6, width_per_step=1, save_path=None)[source]

Plots variability trajectories for each mode over update steps, color-coded by template group using palettes from the attribute bank.

Parameters:
  • attribute_bank (dict) – YAML-style config of attribute templates.

  • base_height (int) – Height of plot.

  • width_per_step (int) – Width scaling factor per update step.

  • save_path (str) – Path for the saved figure. Default is None.

plot_entropy_bar_chart(sort=True, figsize=(16, 5), top_n=None, save_path=None)[source]

Plots a bar chart of normalized entropy across all tracked questions.

Parameters:
  • sort (bool) – Whether to sort bars by entropy.

  • figsize (tuple) – Size of the plot.

  • top_n (int or None) – If set, limits display to top-N questions by entropy.

  • save_path (str) – Path for the saved figure. Default is None.

get_persistent_mode_entropy_trajectories()[source]
Returns:

{mode_tuple: list of entropy values across all steps,

padded with NaN before first appearance, forward-filled after}

Return type:

dict

get_entropy_trajectories()[source]

Return aggregate, per-mode, and per-question trajectories.

Returns:

A mapping with aggregate, by_mode, and by_question trajectories.

class p2p.generators.vanilla_endowment_generator.EndowmentGenerator(backend_type='openai', model_name='gpt-4o-mini', temperature=0.9, max_tokens=1024, batch_size=10, delay=0.5, system_prompt='You are an expert assistant trained to generate realistic, diverse, and demographically plausible personas for social science surveys.\n\nEach persona should include:\n- `eid`: a short, lowercase, variable-safe identifier that encodes key traits (e.g., urban_liberal_30s_female). No punctuation or spaces.\n- `endow_text`: a natural language description of the persona (1–5 sentences), written as if describing a survey respondent.\n\nInstructions:\n- Represent a wide range of age, gender, race/ethnicity, education, region, and political ideology.\n- Avoid repetition of phrasing or demographic combinations across personas.\n- Do not include explanations or formatting outside of the JSON array.\n', retry_failed=True, clear_after_run=False, token_tracker=None, verbose=True, max_retries=10, retry_base_delay=10.0, backend=None, backend_factory=None, **backend_kwargs)[source]

Generate baseline personas without entropy guidance or learned attributes.

Backend resolution is injected so this module remains independent of provider registries and agent integrations. Callers may provide either an initialized backend or a factory that resolves one. Backend calls must return the normalized BackendResponse contract.

Parameters:
  • backend_type (str)

  • model_name (str)

  • temperature (float)

  • max_tokens (int)

  • batch_size (int)

  • delay (float)

  • system_prompt (str)

  • retry_failed (bool)

  • clear_after_run (bool)

  • token_tracker (Any | None)

  • verbose (bool)

  • max_retries (int)

  • retry_base_delay (float)

  • backend (Any | None)

  • backend_factory (Callable[[...], Any] | None)

backend_type

Provider identifier passed to an injected backend factory.

model_name

Model identifier used for generation and usage reporting.

temperature

Sampling temperature.

max_tokens

Maximum output-token setting passed to a backend factory.

batch_size

Number of personas requested per backend call.

delay

Delay between generation batches.

system_prompt

System instruction for persona generation.

retry_failed

Whether failed prompts should be retried.

clear_after_run

Whether accumulated state is cleared after generation.

token_tracker

Optional injected object exposing normalized log(...).

backend

Injected or factory-created object exposing chat(...).

Models

class p2p.models.base.Model(verbose=False)[source]
Parameters:

verbose (bool)

score(A, y)[source]

Return mean squared error.

Parameters:
  • A (DataFrame)

  • y (ndarray)

Return type:

float

static safe_df_to_numpy(df)[source]

Safely converts a DataFrame to a float64 NumPy array after ensuring all values are numeric.

Parameters:

df (DataFrame) – Pandas DataFrame to convert.

Returns:

A NumPy ndarray of float64 values.

Raises:

ValueError if any value cannot be converted or if NaNs are found. –

Return type:

ndarray

class p2p.models.constrained_model.ConstrainedModel(constraint_groups=None, group_total=1.0, nonneg=False, sum_to_one=False, verbose=False, soft_group_sum=True, group_penalty_strength=100.0, solver_fallbacks=('CLARABEL', 'OSQP', 'SCS'), intercept=False)[source]
Parameters:
  • constraint_groups (dict | None)

  • group_total (float)

  • nonneg (bool)

  • sum_to_one (bool)

  • verbose (bool)

  • soft_group_sum (bool)

  • group_penalty_strength (float)

  • solver_fallbacks (tuple[str, ...])

  • intercept (bool)

solve_with_fallback(problem, beta, label)[source]

Solve, falling back to another solver if the configured one fails.

A first-order solver can fail on an individual cross-validation fold while succeeding on every neighbouring one – the stiff group-mass penalty scales with the number of questions, and some folds land badly conditioned. Without a fallback, one such fold aborts an entire run.

Falling back only happens after a genuine failure, so a solve that already succeeds is untouched. solver_used_ records what answered.

Parameters:
  • problem (Problem)

  • beta (Variable)

  • label (str)

compute_group_constraint_violations(A=None)[source]

Compute the absolute sum(A[g] @ beta) - group_total per group.

Returns:

Dict of group name -> absolute constraint violation.

Parameters:

A (DataFrame)

Return type:

dict[str, float]

Grouped cross-validation for constrained models.

class p2p.models.constrained_cv.ConstrainedModelCV(*args, fold_groups=None, fold_method='hashed', **kwargs)[source]

Cross-validation where constraints follow observation groups.

Parameters:
  • fold_groups (Mapping[str, Sequence[str]] | None)

  • fold_method (str)

fit(A, y, *, fold_assignments=None)[source]

Cross-validate over question-grouped folds.

Folds come from fold_assignments when given (replay), otherwise from fold_method with random_state as the seed.

Parameters:
  • A (DataFrame)

  • y (Series | ndarray)

  • fold_assignments (FoldAssignment | Mapping[str, int] | None)

p2p.models.constrained_cv.build_group_folds(groups, *, n_splits, assignments=None, method='hashed', seed=101)[source]

Build grouped train/validation indices, one pair per fold.

groups labels each observation with its question. Without explicit assignments the folds are generated by method (see p2p.models.folds); an explicit FoldAssignment or plain mapping of question -> fold index is used as given, after validation.

Parameters:
  • n_splits (int)

  • assignments (FoldAssignment | Mapping[str, int] | None)

  • method (str)

  • seed (int | None)

Return type:

tuple[tuple[ndarray, ndarray], …]

Constrained Lasso model.

class p2p.models.constrained_lasso.ConstrainedLasso(alpha=1.0, zero_threshold=1e-05, solver=None, **kwargs)[source]

Lasso regression with optional constraints and explicit solver choice.

Parameters:
  • alpha (float)

  • zero_threshold (float)

  • solver (str | None)

Constrained ElasticNet model.

class p2p.models.constrained_elasticnet.ConstrainedElasticNet(alpha=1.0, l1_ratio=0.5, zero_threshold=1e-05, solver=None, **kwargs)[source]

ElasticNet regression with constraints and explicit solver choice.

Parameters:
  • alpha (float)

  • l1_ratio (float)

  • zero_threshold (float)

  • solver (str | None)

class p2p.models.weighted_constrained_lasso.WeightedConstrainedLasso(question_weights=None, **kwargs)[source]

Weighted Constrained Lasso. Overrides only the loss term. Constraints, penalties, coefficient thresholding, and metadata storage unchanged.

class p2p.models.weighted_constrained_elasticnet.WeightedConstrainedElasticNet(question_weights=None, **kwargs)[source]

Weighted Constrained ElasticNet. Overrides only the loss term.

Stage 2 without regression: the uniform average of the proxy panel’s answers.

class p2p.models.simple_average.SimpleAverage(verbose=False)[source]

Give every agent in the design the same weight, 1/n.

Nothing is fitted and the targets are not used: fit only reads the agent columns. On P2P’s one-hot design, where every retained agent answered every question (column-drop cleaning), a question’s predicted option masses are the shares of agents who chose each option, so they sum to one. This is the no-regression baseline: the same agents and the same evaluation as a fitted model, without the weights.

Parameters:

verbose (bool)

Stage 2 fitted around a fixed reference distribution.

A reference distribution a (one value per option row, summing to one within each question) enters every prediction with weight one:

prediction = a + X @ beta

The agents therefore explain each target’s deviation from the reference rather than its level. Internally the estimator is fitted to y - a with the question-mass target lowered by one, so that each question’s prediction still sums to the configured total; when every agent answers every question this makes the agents’ weights sum to zero. The penalty, the cross-validation and the scoring are those of the wrapped estimator: score and predict work on the full prediction, so the error of y - a against X @ beta and the error of y against a + X @ beta are the same number.

The drift refit of a survey across waves is the case where a is the earlier wave’s marginals.

p2p.models.reference_distribution.check_reference_masses(reference, constraint_groups, rows)[source]

Every question’s reference mass over its modelled rows must be one.

Parameters:
  • reference (Mapping[str, float])

  • constraint_groups (Mapping[str, list[str]])

  • rows (Index)

Return type:

None

p2p.models.reference_distribution.reference_vector(reference, rows)[source]

The reference value of every option row, in row order.

Parameters:
  • reference (Mapping[str, float])

  • rows (Index)

Return type:

ndarray

p2p.models.reference_distribution.with_reference_distribution(model_cls, reference)[source]

Return model_cls fitted around reference (see the module docstring).

reference maps option-row ids to values. The returned class takes the same keyword arguments; group_total keeps its meaning as the total of each question’s full prediction.

Parameters:
  • model_cls (type)

  • reference (Mapping[str, float])

Return type:

type

Cross-validation fold assignments: generate, record and replay.

A question is the indivisible cross-validation unit: all of its option rows go to the same fold. A fold assignment maps every question to a fold index 0 .. n_splits - 1 and fully determines the folds.

Three methods produce an assignment:

hashed (default)

Questions are placed largest first; ties between equal-size questions are broken by sha256("{seed}:{question_id}"). Each question goes to the fold with the fewest option rows so far, the lowest fold index winning a tie. Only the Python standard library decides the order, so the result is identical on every machine and library version.

sklearn

Whatever the installed sklearn.model_selection.GroupKFold does, without shuffling. scikit-learn up to 1.7 broke ties with an unstable sort whose order depended on the installation (scikit-learn issue #29495), so this is for quick checks, not for reproducible results.

replay

The assignment is read from a fold record written by an earlier run.

Every assignment carries a partition_digest that identifies which questions share a fold, ignoring fold numbering: renumbering folds cannot change any result, because each penalty is scored by the average error over the folds.

class p2p.models.folds.FoldAssignment(n_splits, method, seed, assignment, question_sizes, producer, source=None, fold_row_counts=None, origin=None)[source]

A complete question -> fold assignment and its provenance.

Parameters:
  • n_splits (int)

  • method (str)

  • seed (int | None)

  • assignment (Mapping[str, int])

  • question_sizes (Mapping[str, int] | None)

  • producer (Mapping[str, str])

  • source (str | None)

  • fold_row_counts (tuple[int, ...] | None)

  • origin (Mapping[str, Any] | None)

classmethod from_mapping(assignment, *, n_splits, source=None)[source]

Wrap an explicit mapping, e.g. one passed in by a test.

Parameters:
  • assignment (Mapping[str, Any])

  • n_splits (int)

  • source (str | None)

Return type:

FoldAssignment

property partition_digest: str

SHA-256 of the sorted folds, each a sorted list of question ids.

Fold numbering is dropped, so two assignments share a digest exactly when they put the same questions together.

indices(group_labels)[source]

Training and validation row indices for each fold, in fold order.

The labels must name exactly the assigned questions. When the assignment knows its question sizes (or, for schema-1 records, its per-fold row totals), the labels must reproduce those too: a mismatch means the assignment was made for a differently cleaned design.

Parameters:

group_labels (Sequence[str])

Return type:

tuple[tuple[ndarray, ndarray], …]

to_record()[source]

The schema-2 fold record written beside every run.

Return type:

dict[str, Any]

p2p.models.folds.assign_folds(sizes, *, n_splits, method='hashed', seed=101)[source]

Generate a fold assignment for questions of the given sizes.

Parameters:
  • sizes (Mapping[str, int])

  • n_splits (int)

  • method (str)

  • seed (int | None)

Return type:

FoldAssignment

p2p.models.folds.count_question_sizes(group_labels)[source]

Count the option rows of each question in a per-row label sequence.

Parameters:

group_labels (Sequence[str])

Return type:

dict[str, int]

p2p.models.folds.read_fold_record(path)[source]

Read a schema-1 or schema-2 fold record for replay.

Schema 1 is the session-4 parity-fixture format (validation_groups, optional per-fold validation_rows); its validation_qids_digest has no documented definition and is ignored. Schema 2 is what P2P writes; its partition_digest is recomputed and must match.

Parameters:

path (str | PathLike[str])

Return type:

FoldAssignment

p2p.models.folds.resolve_fold_assignment(group_labels, *, n_splits, assignments=None, method='hashed', seed=101)[source]

Return the assignment a cross-validation run will use.

An explicit FoldAssignment or plain mapping wins; otherwise one is generated from the labels with method and seed.

Parameters:
  • group_labels (Sequence[str])

  • n_splits (int)

  • assignments (FoldAssignment | Mapping[str, Any] | None)

  • method (str)

  • seed (int | None)

Return type:

FoldAssignment

p2p.models.folds.write_fold_record(path, assignment)[source]

Write assignment as a schema-2 JSON fold record.

Parameters:
Return type:

Path