Metadata-Version: 2.1
Name: acquisitionuniverse
Version: 1.0.1
Summary: Toolkit for acquisition target screening: write a thesis brief, rank screened companies with a weighted composite that zeroes out group-owned targets, and measure signal coverage across the 15 common signals.
Home-page: https://www.acquisitionuniverse.com
Author: Alpha Quantum
Author-email: info@alpha-quantum.com
License: MIT
Project-URL: Homepage, https://www.acquisitionuniverse.com
Project-URL: Documentation, https://www.acquisitionuniverse.com/method.php
Project-URL: Source, https://github.com/explainableaixai/acquisitionuniverse
Project-URL: Mirror, https://gitlab.com/url-classifications/acquisitionuniverse
Project-URL: Tracker, https://www.acquisitionuniverse.com/contact.php
Project-URL: Pilot, https://www.acquisitionuniverse.com/free-pilot.php
Keywords: acquisition target,target screening,m&a,search fund,private equity,deal sourcing,thesis,add-on acquisition,buy-side,independent sponsor,corporate development,long list
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.7
Description-Content-Type: text/markdown

# 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.

```bash
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](https://www.acquisitionuniverse.com/use-cases/search-fund-target-lists.php) page.

## 1. Write the brief

```python
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

```python
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:

```python
rank(companies, weights={"mandate_fit": 0.6, "outreach_suitability": 0.3, "transition_context": 0.1})
```

## 3. Count the evidence

```python
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

```python
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](https://www.cookielessaudiences.com/api.php) for advertisers and a [CV parser](https://www.resumereaderapi.com/api-v2.php) for hiring teams. Neither is needed here.

<!--expanded-->
## 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.

```python
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.

```python
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.

```python
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](https://www.acquisitionuniverse.com/use-cases/search-fund-target-lists.php) 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](https://www.cookielessaudiences.com/api.php) returns demographic and interest attributes for any URL without cookies, and the [CV parser](https://www.resumereaderapi.com/api-v2.php) returns 114 fields for any resume.

<!--extra-->
## 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-->
## Further reading and practical notes

The [Python documentation](https://docs.python.org/3/) covers `dataclasses`, `json` and the other standard library pieces used above. For buyers who finance acquisitions with bank debt, the [US Small Business Administration](https://www.sba.gov/) 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


