acquisitionuniverse (Python)
Utilities for search funds, independent sponsors and corporate development teams who work from screened company lists. Score records, rank them, measure evidence gaps and generate a clean thesis brief.
Pure Python, standard library only, offline.
pip install acquisitionuniverse
Where it fits
A full-web screen hands you a long ranked file. Before you call anyone you want to answer three questions: what exactly did we ask for, which companies come first, and how much is the file really telling us?
This package answers those with small functions that work on dictionaries and DataFrame rows alike. The method behind the screens is described on the search fund target lists page.
1. Write the brief
from acquisitionuniverse import ThesisBrief
brief = ThesisBrief(
name="Calibration labs, Midwest",
vertical="Calibration and metrology",
regions=["Ohio", "Michigan", "Indiana"],
employees_min=8,
employees_max=60,
must_have=["ISO/IEC 17025 accreditation"],
exclusions=["group-owned", "single-person shops"],
)
assert brief.validate() == []
print(brief.to_text())
to_json() gives the same content as a string for a ticket or a shared drive.
2. Rank the file
from acquisitionuniverse import rank
companies = [
{"id": "C-01", "mandate_fit": 92, "outreach_suitability": 75, "transition_context": 60},
{"id": "C-02", "mandate_fit": 97, "outreach_suitability": 90, "transition_context": 80, "group_owned": True},
{"id": "C-03", "mandate_fit": 84, "outreach_suitability": 88, "transition_context": 40},
]
for c in rank(companies):
print(c["id"], c["composite"])
C-02 has the best fit but belongs to a group, so its outreach component is zero and it falls behind C-01. That ordering is the point: you should call the independent first.
Custom weights:
rank(companies, weights={"mandate_fit": 0.6, "outreach_suitability": 0.3, "transition_context": 0.1})
3. Count the evidence
from acquisitionuniverse import SIGNALS, signal_coverage
record = {SIGNALS[0]: "founder led", SIGNALS[3]: "service company", SIGNALS[10]: None}
print(signal_coverage(record)) # {'visible': 2, 'checked': 3, 'ratio': 0.666...}
A value of None or the string "not visible" counts as silent. You see at a glance whether a high score rests on much evidence or little.
With pandas
import pandas as pd
from acquisitionuniverse import composite
df = pd.read_csv("screen.csv")
df["composite"] = df.apply(
lambda r: composite({
"mandate_fit": r.mandate_fit,
"outreach_suitability": r.outreach_suitability,
"transition_context": r.transition_context,
"group_owned": r.group_owned == "yes",
}),
axis=1,
)
df.sort_values("composite", ascending=False).head(25).to_csv("call_list.csv", index=False)
Reference
| Name | Kind | Notes |
|---|---|---|
ThesisBrief |
dataclass | validate, to_dict, to_json, to_text |
composite(company, weights=None) |
function | 0 to 100, one decimal |
rank(companies, weights=None) |
function | returns copies with composite |
signal_coverage(map) |
function | visible, checked, ratio |
SIGNALS |
tuple | 15 names |
DEFAULT_WEIGHTS |
dict | 0.7, 0.2, 0.1 |
PILOT_URL |
str | pilot request page |
Related products
Alpha Quantum also publishes an audience segmentation API for advertisers and a CV parser for hiring teams. Neither is needed here.
Working with screened lists in Python
Python is where most deal analysts do their analysis, whether in a notebook or in a script that runs before a Monday meeting. This package gives you three small tools that fit that habit: a brief you can validate and send, a composite score you can recompute and explain, and a coverage measure that tells you how much of a record is evidence.
None of them touches the network. A screening engagement delivers a file, and what you do with it afterwards is your own business. The toolkit helps you do it consistently.
From a delivered file to a call list
Suppose you have received a ranked CSV with one row per company, three score columns and a flag for group ownership. A script turns it into a call list in a few lines, and the same script is easy to re-run when you change the weights.
import pandas as pd
from acquisitionuniverse import composite, rank
df = pd.read_csv("screen.csv")
records = df.to_dict("records")
for r in records:
r["group_owned"] = str(r.get("group_owned", "")).lower() in ("yes", "true", "1")
ordered = pd.DataFrame(rank(records))
call_list = ordered[ordered["composite"] >= 75].head(40)
call_list.to_csv("call_list.csv", index=False)
print(f"{len(call_list)} companies at 75 or above")
A threshold such as 75 is a judgement. Look at the distribution first. If your list has a clear break between the top thirty and the rest, cut there. If it fades slowly, say so in the memo and call further down.
Explaining a ranking to a partner
Partners ask why. A good answer names the three scores and the rule about group ownership. A better answer shows the arithmetic for the company in question.
def explain(c, weights=None):
w = weights or {"mandate_fit": 0.7, "outreach_suitability": 0.2, "transition_context": 0.1}
outreach = 0 if c.get("group_owned") else c.get("outreach_suitability", 0)
parts = [
("fit", c["mandate_fit"], w["mandate_fit"]),
("suitability", outreach, w["outreach_suitability"]),
("context", c["transition_context"], w["transition_context"]),
]
lines = [f"{n}: {v} x {wt} = {v * wt:.1f}" for n, v, wt in parts]
return "\n".join(lines + [f"composite: {composite(c, w)}"])
Printing that for a company that surprised somebody ends most debates. If it does not, the debate is about the weights, which is the debate you wanted.
Coverage as a triage tool
Evidence quality varies across a list. Use coverage to triage.
| Coverage | What it suggests |
|---|---|
| 12 to 15 of 15 | ready for outreach with quoted evidence |
| 8 to 11 of 15 | likely fine, check the signals that matter most to your thesis |
| below 8 | a small or quiet site, research before you spend a call |
Those bands are a starting point, not a rule. A quiet site is not a bad company, because many excellent family businesses publish little. It is simply a company where the screen has less to say, and where a human review pays off.
Using the brief with your team
A brief is also a team tool. Keep it in version control next to your analysis, so that a change in the thesis shows up as a diff. Run validate() in a test, so a brief cannot be committed with no exclusions. Print to_text() into the email that requests a pilot, so the wording a screening team receives matches the wording your own analysts use.
brief = ThesisBrief(
name="Water treatment services, Mid-Atlantic",
vertical="Water treatment",
subverticals=["service and maintenance", "chemical programs"],
regions=["Pennsylvania", "Maryland", "Virginia"],
employees_min=10, employees_max=75,
must_have=["service contracts"],
exclusions=["group-owned", "residential retail"],
)
problems = brief.validate()
assert not problems, problems
Reading about the method
The search fund target lists page shows how a screened universe is turned into a list a searcher can actually call. The standards page lists the categories the service declines to serve, such as veterinary and dental practices, because the buyer's strategy there harms captive consumers.
If you work on both sides of a market, the sister services may help. The audience segmentation API returns demographic and interest attributes for any URL without cookies, and the CV parser returns 114 fields for any resume.
A note on vertical signals
Screenings add signals for the trade you are screening, such as ASME code stamps for boiler services or ISO/IEC 17025 accreditation for calibration labs. Keep them in a separate dictionary from the fifteen common signals, because signal_coverage counts only the fifteen it knows.
Further reading and practical notes
The Python documentation covers dataclasses, json and the other standard library pieces used above. For buyers who finance acquisitions with bank debt, the US Small Business Administration explains its loan programmes, which many searchers and independent sponsors use when they buy a small company.
Three notes from practice. First, keep inputs and outputs of every ranking run. A ranked list that cannot be reproduced is an argument waiting to happen. Save the input file, the weights, the package version and the date, and a month later you can explain the order that was on the screen. Second, version your briefs. A brief is a living document, and the change from one version to the next is the history of what you learned. Store each version, with a note about why it changed. Third, separate research from outreach. The list that analysts maintain is not the list the callers use. Give the callers a short, stable extract with the quotes beside each name, and keep the full file for analysts.
If you run searches in several verticals at once, keep a separate brief and a separate weights file for each. A single set of weights rarely suits every vertical, because the signals that matter in water treatment, such as chemical programmes and service contracts, differ from those that matter in precision machining, such as certifications and end markets.
Testing and typing
The module has type hints and a small test file. Add your own tests for the weights you use, with a handful of fixtures you can verify by hand. Property tests help too: for any inputs between 0 and 100, the composite must stay between 0 and 100, and marking a company group owned must never raise its score.
Compatibility
Python 3.7 or later. No third party dependencies. The package is MIT licensed.
Questions
Is a database included? No. Bring your own screened records.
Why dictionaries? They match JSON, CSV rows and pandas without a conversion step.
Python versions? 3.7 and up. MIT license. info@alpha-quantum.com
Metadata
Release files for acquisitionuniverse 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| acquisitionuniverse-1.0.1.tar.gz | 10.5 kB | Without provenance | Details for acquisitionuniverse-1.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| acquisitionuniverse-1.0.1-py3-none-any.whl | Python 3 | none | any | Details for acquisitionuniverse-1.0.1-py3-none-any.whl |
Total release size: 19.7 kB
Release files / acquisitionuniverse-1.0.1.tar.gz
| Download URL | acquisitionuniverse-1.0.1.tar.gz |
|---|---|
| Size | 10.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3972b0bb9dd6e967be2d9bf9c72e920707daa3649c9c9d2368d3bb682e7ff444
|
|
BLAKE2b-256 checksum How to use checksums |
33095720ab02ccda6b0a63eb54ef5a70af29978384579baa291e063f4de71db5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.8.10
|
Release files / acquisitionuniverse-1.0.1-py3-none-any.whl
| Download URL | acquisitionuniverse-1.0.1-py3-none-any.whl |
|---|---|
| Size | 9.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a2de256a0bf4ff92c4f620b0e0406ae95ebe6cd9b3efcda4d929576fea443381
|
|
BLAKE2b-256 checksum How to use checksums |
8be39cd4db4b4c169526bdcff1a6df5da27de7029941a74a9cbb489dcbbd9c9a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.8.10
|
Security for acquisitionuniverse 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.