acquisitionuniverse 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acquisitionuniverse-1.0.0/LICENSE +21 -0
- acquisitionuniverse-1.0.0/MANIFEST.in +2 -0
- acquisitionuniverse-1.0.0/PKG-INFO +139 -0
- acquisitionuniverse-1.0.0/README.md +113 -0
- acquisitionuniverse-1.0.0/acquisitionuniverse/__init__.py +12 -0
- acquisitionuniverse-1.0.0/acquisitionuniverse/toolkit.py +107 -0
- acquisitionuniverse-1.0.0/acquisitionuniverse.egg-info/PKG-INFO +139 -0
- acquisitionuniverse-1.0.0/acquisitionuniverse.egg-info/SOURCES.txt +10 -0
- acquisitionuniverse-1.0.0/acquisitionuniverse.egg-info/dependency_links.txt +1 -0
- acquisitionuniverse-1.0.0/acquisitionuniverse.egg-info/top_level.txt +1 -0
- acquisitionuniverse-1.0.0/setup.cfg +4 -0
- acquisitionuniverse-1.0.0/setup.py +37 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alpha Quantum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: acquisitionuniverse
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
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.
|
|
5
|
+
Home-page: https://www.acquisitionuniverse.com
|
|
6
|
+
Author: Alpha Quantum
|
|
7
|
+
Author-email: info@alpha-quantum.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://www.acquisitionuniverse.com
|
|
10
|
+
Project-URL: Documentation, https://www.acquisitionuniverse.com/method.php
|
|
11
|
+
Project-URL: Source, https://github.com/explainableaixai/acquisitionuniverse
|
|
12
|
+
Project-URL: Mirror, https://gitlab.com/url-classifications/acquisitionuniverse
|
|
13
|
+
Project-URL: Tracker, https://www.acquisitionuniverse.com/contact.php
|
|
14
|
+
Project-URL: Pilot, https://www.acquisitionuniverse.com/free-pilot.php
|
|
15
|
+
Description: # acquisitionuniverse (Python)
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
Pure Python, standard library only, offline.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install acquisitionuniverse
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Where it fits
|
|
26
|
+
|
|
27
|
+
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?
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
## 1. Write the brief
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from acquisitionuniverse import ThesisBrief
|
|
35
|
+
|
|
36
|
+
brief = ThesisBrief(
|
|
37
|
+
name="Calibration labs, Midwest",
|
|
38
|
+
vertical="Calibration and metrology",
|
|
39
|
+
regions=["Ohio", "Michigan", "Indiana"],
|
|
40
|
+
employees_min=8,
|
|
41
|
+
employees_max=60,
|
|
42
|
+
must_have=["ISO/IEC 17025 accreditation"],
|
|
43
|
+
exclusions=["group-owned", "single-person shops"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert brief.validate() == []
|
|
47
|
+
print(brief.to_text())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`to_json()` gives the same content as a string for a ticket or a shared drive.
|
|
51
|
+
|
|
52
|
+
## 2. Rank the file
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from acquisitionuniverse import rank
|
|
56
|
+
|
|
57
|
+
companies = [
|
|
58
|
+
{"id": "C-01", "mandate_fit": 92, "outreach_suitability": 75, "transition_context": 60},
|
|
59
|
+
{"id": "C-02", "mandate_fit": 97, "outreach_suitability": 90, "transition_context": 80, "group_owned": True},
|
|
60
|
+
{"id": "C-03", "mandate_fit": 84, "outreach_suitability": 88, "transition_context": 40},
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
for c in rank(companies):
|
|
64
|
+
print(c["id"], c["composite"])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
Custom weights:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
rank(companies, weights={"mandate_fit": 0.6, "outreach_suitability": 0.3, "transition_context": 0.1})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 3. Count the evidence
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from acquisitionuniverse import SIGNALS, signal_coverage
|
|
79
|
+
|
|
80
|
+
record = {SIGNALS[0]: "founder led", SIGNALS[3]: "service company", SIGNALS[10]: None}
|
|
81
|
+
print(signal_coverage(record)) # {'visible': 2, 'checked': 3, 'ratio': 0.666...}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
## With pandas
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import pandas as pd
|
|
90
|
+
from acquisitionuniverse import composite
|
|
91
|
+
|
|
92
|
+
df = pd.read_csv("screen.csv")
|
|
93
|
+
df["composite"] = df.apply(
|
|
94
|
+
lambda r: composite({
|
|
95
|
+
"mandate_fit": r.mandate_fit,
|
|
96
|
+
"outreach_suitability": r.outreach_suitability,
|
|
97
|
+
"transition_context": r.transition_context,
|
|
98
|
+
"group_owned": r.group_owned == "yes",
|
|
99
|
+
}),
|
|
100
|
+
axis=1,
|
|
101
|
+
)
|
|
102
|
+
df.sort_values("composite", ascending=False).head(25).to_csv("call_list.csv", index=False)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Reference
|
|
106
|
+
|
|
107
|
+
| Name | Kind | Notes |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `ThesisBrief` | dataclass | `validate`, `to_dict`, `to_json`, `to_text` |
|
|
110
|
+
| `composite(company, weights=None)` | function | 0 to 100, one decimal |
|
|
111
|
+
| `rank(companies, weights=None)` | function | returns copies with `composite` |
|
|
112
|
+
| `signal_coverage(map)` | function | visible, checked, ratio |
|
|
113
|
+
| `SIGNALS` | tuple | 15 names |
|
|
114
|
+
| `DEFAULT_WEIGHTS` | dict | 0.7, 0.2, 0.1 |
|
|
115
|
+
| `PILOT_URL` | str | pilot request page |
|
|
116
|
+
|
|
117
|
+
## Related products
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
## Questions
|
|
122
|
+
|
|
123
|
+
**Is a database included?** No. Bring your own screened records.
|
|
124
|
+
|
|
125
|
+
**Why dictionaries?** They match JSON, CSV rows and pandas without a conversion step.
|
|
126
|
+
|
|
127
|
+
**Python versions?** 3.7 and up. MIT license. info@alpha-quantum.com
|
|
128
|
+
|
|
129
|
+
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
|
|
130
|
+
Platform: UNKNOWN
|
|
131
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
132
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
133
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
134
|
+
Classifier: Programming Language :: Python :: 3
|
|
135
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
136
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
137
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
138
|
+
Requires-Python: >=3.7
|
|
139
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# acquisitionuniverse (Python)
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
Pure Python, standard library only, offline.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install acquisitionuniverse
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Where it fits
|
|
12
|
+
|
|
13
|
+
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?
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
## 1. Write the brief
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from acquisitionuniverse import ThesisBrief
|
|
21
|
+
|
|
22
|
+
brief = ThesisBrief(
|
|
23
|
+
name="Calibration labs, Midwest",
|
|
24
|
+
vertical="Calibration and metrology",
|
|
25
|
+
regions=["Ohio", "Michigan", "Indiana"],
|
|
26
|
+
employees_min=8,
|
|
27
|
+
employees_max=60,
|
|
28
|
+
must_have=["ISO/IEC 17025 accreditation"],
|
|
29
|
+
exclusions=["group-owned", "single-person shops"],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assert brief.validate() == []
|
|
33
|
+
print(brief.to_text())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`to_json()` gives the same content as a string for a ticket or a shared drive.
|
|
37
|
+
|
|
38
|
+
## 2. Rank the file
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from acquisitionuniverse import rank
|
|
42
|
+
|
|
43
|
+
companies = [
|
|
44
|
+
{"id": "C-01", "mandate_fit": 92, "outreach_suitability": 75, "transition_context": 60},
|
|
45
|
+
{"id": "C-02", "mandate_fit": 97, "outreach_suitability": 90, "transition_context": 80, "group_owned": True},
|
|
46
|
+
{"id": "C-03", "mandate_fit": 84, "outreach_suitability": 88, "transition_context": 40},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for c in rank(companies):
|
|
50
|
+
print(c["id"], c["composite"])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
Custom weights:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
rank(companies, weights={"mandate_fit": 0.6, "outreach_suitability": 0.3, "transition_context": 0.1})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 3. Count the evidence
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from acquisitionuniverse import SIGNALS, signal_coverage
|
|
65
|
+
|
|
66
|
+
record = {SIGNALS[0]: "founder led", SIGNALS[3]: "service company", SIGNALS[10]: None}
|
|
67
|
+
print(signal_coverage(record)) # {'visible': 2, 'checked': 3, 'ratio': 0.666...}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
## With pandas
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import pandas as pd
|
|
76
|
+
from acquisitionuniverse import composite
|
|
77
|
+
|
|
78
|
+
df = pd.read_csv("screen.csv")
|
|
79
|
+
df["composite"] = df.apply(
|
|
80
|
+
lambda r: composite({
|
|
81
|
+
"mandate_fit": r.mandate_fit,
|
|
82
|
+
"outreach_suitability": r.outreach_suitability,
|
|
83
|
+
"transition_context": r.transition_context,
|
|
84
|
+
"group_owned": r.group_owned == "yes",
|
|
85
|
+
}),
|
|
86
|
+
axis=1,
|
|
87
|
+
)
|
|
88
|
+
df.sort_values("composite", ascending=False).head(25).to_csv("call_list.csv", index=False)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Reference
|
|
92
|
+
|
|
93
|
+
| Name | Kind | Notes |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `ThesisBrief` | dataclass | `validate`, `to_dict`, `to_json`, `to_text` |
|
|
96
|
+
| `composite(company, weights=None)` | function | 0 to 100, one decimal |
|
|
97
|
+
| `rank(companies, weights=None)` | function | returns copies with `composite` |
|
|
98
|
+
| `signal_coverage(map)` | function | visible, checked, ratio |
|
|
99
|
+
| `SIGNALS` | tuple | 15 names |
|
|
100
|
+
| `DEFAULT_WEIGHTS` | dict | 0.7, 0.2, 0.1 |
|
|
101
|
+
| `PILOT_URL` | str | pilot request page |
|
|
102
|
+
|
|
103
|
+
## Related products
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
## Questions
|
|
108
|
+
|
|
109
|
+
**Is a database included?** No. Bring your own screened records.
|
|
110
|
+
|
|
111
|
+
**Why dictionaries?** They match JSON, CSV rows and pandas without a conversion step.
|
|
112
|
+
|
|
113
|
+
**Python versions?** 3.7 and up. MIT license. info@alpha-quantum.com
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .toolkit import (
|
|
2
|
+
DEFAULT_WEIGHTS,
|
|
3
|
+
PILOT_URL,
|
|
4
|
+
SIGNALS,
|
|
5
|
+
ThesisBrief,
|
|
6
|
+
composite,
|
|
7
|
+
rank,
|
|
8
|
+
signal_coverage,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = ["DEFAULT_WEIGHTS", "PILOT_URL", "SIGNALS", "ThesisBrief", "composite", "rank", "signal_coverage"]
|
|
12
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Local toolkit for acquisition target screening (no network calls)."""
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass, field, asdict
|
|
4
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
5
|
+
|
|
6
|
+
PILOT_URL = "https://www.acquisitionuniverse.com/free-pilot.php"
|
|
7
|
+
|
|
8
|
+
SIGNALS = (
|
|
9
|
+
"Founder or family association",
|
|
10
|
+
"Visible leadership bench depth",
|
|
11
|
+
"Operating history and continued independence",
|
|
12
|
+
"Strategic fit to thesis",
|
|
13
|
+
"Geographic and branch footprint",
|
|
14
|
+
"Service-led vs product-led model",
|
|
15
|
+
"Recurring-offering indicators",
|
|
16
|
+
"Vertical specialization and end-market exposure",
|
|
17
|
+
"Acquisition-program or roll-up readiness",
|
|
18
|
+
"Management professionalization",
|
|
19
|
+
"Hiring posture and functional investment",
|
|
20
|
+
"Website and news activity trajectory",
|
|
21
|
+
"Partner and channel ecosystem position",
|
|
22
|
+
"Compliance and regulated-market readiness",
|
|
23
|
+
"Digital-commercial maturity",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_WEIGHTS = {"mandate_fit": 0.7, "outreach_suitability": 0.2, "transition_context": 0.1}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ThesisBrief:
|
|
31
|
+
name: str = "Untitled thesis"
|
|
32
|
+
vertical: str = ""
|
|
33
|
+
subverticals: List[str] = field(default_factory=list)
|
|
34
|
+
regions: List[str] = field(default_factory=list)
|
|
35
|
+
employees_min: Optional[int] = None
|
|
36
|
+
employees_max: Optional[int] = None
|
|
37
|
+
must_have: List[str] = field(default_factory=list)
|
|
38
|
+
exclusions: List[str] = field(default_factory=list)
|
|
39
|
+
notes: str = ""
|
|
40
|
+
|
|
41
|
+
def validate(self) -> List[str]:
|
|
42
|
+
"""Problems that would make the brief ambiguous to a screening team."""
|
|
43
|
+
problems = []
|
|
44
|
+
if not self.vertical:
|
|
45
|
+
problems.append("vertical is empty")
|
|
46
|
+
if not self.regions:
|
|
47
|
+
problems.append("no regions listed")
|
|
48
|
+
if self.employees_min is not None and self.employees_max is not None and self.employees_min > self.employees_max:
|
|
49
|
+
problems.append("employees_min is above employees_max")
|
|
50
|
+
if not self.exclusions:
|
|
51
|
+
problems.append("no exclusions listed, group-owned companies are the usual first one")
|
|
52
|
+
return problems
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
55
|
+
return asdict(self)
|
|
56
|
+
|
|
57
|
+
def to_json(self) -> str:
|
|
58
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
59
|
+
|
|
60
|
+
def to_text(self) -> str:
|
|
61
|
+
"""Plain text brief, ready to paste into a pilot request."""
|
|
62
|
+
if self.employees_min is not None or self.employees_max is not None:
|
|
63
|
+
size = "%s to %s employees" % (
|
|
64
|
+
"?" if self.employees_min is None else self.employees_min,
|
|
65
|
+
"?" if self.employees_max is None else self.employees_max,
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
size = "not specified"
|
|
69
|
+
lines = [
|
|
70
|
+
"Thesis: %s" % self.name,
|
|
71
|
+
"Vertical: %s" % self.vertical,
|
|
72
|
+
"Subverticals: %s" % (", ".join(self.subverticals) or "all"),
|
|
73
|
+
"Regions: %s" % (", ".join(self.regions) or "not specified"),
|
|
74
|
+
"Size: %s" % size,
|
|
75
|
+
"Must have: %s" % ("; ".join(self.must_have) or "none"),
|
|
76
|
+
"Exclude: %s" % ("; ".join(self.exclusions) or "none"),
|
|
77
|
+
]
|
|
78
|
+
if self.notes:
|
|
79
|
+
lines.append("Notes: %s" % self.notes)
|
|
80
|
+
lines += ["", "Request a pilot: %s" % PILOT_URL]
|
|
81
|
+
return "\n".join(lines)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def composite(company: Dict[str, Any], weights: Optional[Dict[str, float]] = None) -> float:
|
|
85
|
+
"""Weighted composite on a 0 to 100 scale. A group-owned company scores zero on outreach suitability."""
|
|
86
|
+
w = weights or DEFAULT_WEIGHTS
|
|
87
|
+
outreach = 0 if company.get("group_owned") else company.get("outreach_suitability", 0) or 0
|
|
88
|
+
total = w["mandate_fit"] + w["outreach_suitability"] + w["transition_context"]
|
|
89
|
+
value = (
|
|
90
|
+
(company.get("mandate_fit", 0) or 0) * w["mandate_fit"]
|
|
91
|
+
+ outreach * w["outreach_suitability"]
|
|
92
|
+
+ (company.get("transition_context", 0) or 0) * w["transition_context"]
|
|
93
|
+
) / total
|
|
94
|
+
return round(value, 1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def rank(companies: Iterable[Dict[str, Any]], weights: Optional[Dict[str, float]] = None) -> List[Dict[str, Any]]:
|
|
98
|
+
"""Return copies of the companies with a composite score, highest first."""
|
|
99
|
+
scored = [dict(c, composite=composite(c, weights)) for c in companies]
|
|
100
|
+
return sorted(scored, key=lambda c: c["composite"], reverse=True)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def signal_coverage(signals: Dict[str, Any]) -> Dict[str, Any]:
|
|
104
|
+
"""Share of the checked signals that are visible. A value of None or 'not visible' counts as silent."""
|
|
105
|
+
known = [s for s in SIGNALS if s in signals]
|
|
106
|
+
visible = [s for s in known if signals[s] not in (None, "not visible")]
|
|
107
|
+
return {"visible": len(visible), "checked": len(known), "ratio": (len(visible) / len(known)) if known else 0.0}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: acquisitionuniverse
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
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.
|
|
5
|
+
Home-page: https://www.acquisitionuniverse.com
|
|
6
|
+
Author: Alpha Quantum
|
|
7
|
+
Author-email: info@alpha-quantum.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://www.acquisitionuniverse.com
|
|
10
|
+
Project-URL: Documentation, https://www.acquisitionuniverse.com/method.php
|
|
11
|
+
Project-URL: Source, https://github.com/explainableaixai/acquisitionuniverse
|
|
12
|
+
Project-URL: Mirror, https://gitlab.com/url-classifications/acquisitionuniverse
|
|
13
|
+
Project-URL: Tracker, https://www.acquisitionuniverse.com/contact.php
|
|
14
|
+
Project-URL: Pilot, https://www.acquisitionuniverse.com/free-pilot.php
|
|
15
|
+
Description: # acquisitionuniverse (Python)
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
Pure Python, standard library only, offline.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install acquisitionuniverse
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Where it fits
|
|
26
|
+
|
|
27
|
+
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?
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
## 1. Write the brief
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from acquisitionuniverse import ThesisBrief
|
|
35
|
+
|
|
36
|
+
brief = ThesisBrief(
|
|
37
|
+
name="Calibration labs, Midwest",
|
|
38
|
+
vertical="Calibration and metrology",
|
|
39
|
+
regions=["Ohio", "Michigan", "Indiana"],
|
|
40
|
+
employees_min=8,
|
|
41
|
+
employees_max=60,
|
|
42
|
+
must_have=["ISO/IEC 17025 accreditation"],
|
|
43
|
+
exclusions=["group-owned", "single-person shops"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert brief.validate() == []
|
|
47
|
+
print(brief.to_text())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`to_json()` gives the same content as a string for a ticket or a shared drive.
|
|
51
|
+
|
|
52
|
+
## 2. Rank the file
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from acquisitionuniverse import rank
|
|
56
|
+
|
|
57
|
+
companies = [
|
|
58
|
+
{"id": "C-01", "mandate_fit": 92, "outreach_suitability": 75, "transition_context": 60},
|
|
59
|
+
{"id": "C-02", "mandate_fit": 97, "outreach_suitability": 90, "transition_context": 80, "group_owned": True},
|
|
60
|
+
{"id": "C-03", "mandate_fit": 84, "outreach_suitability": 88, "transition_context": 40},
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
for c in rank(companies):
|
|
64
|
+
print(c["id"], c["composite"])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
Custom weights:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
rank(companies, weights={"mandate_fit": 0.6, "outreach_suitability": 0.3, "transition_context": 0.1})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 3. Count the evidence
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from acquisitionuniverse import SIGNALS, signal_coverage
|
|
79
|
+
|
|
80
|
+
record = {SIGNALS[0]: "founder led", SIGNALS[3]: "service company", SIGNALS[10]: None}
|
|
81
|
+
print(signal_coverage(record)) # {'visible': 2, 'checked': 3, 'ratio': 0.666...}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
## With pandas
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import pandas as pd
|
|
90
|
+
from acquisitionuniverse import composite
|
|
91
|
+
|
|
92
|
+
df = pd.read_csv("screen.csv")
|
|
93
|
+
df["composite"] = df.apply(
|
|
94
|
+
lambda r: composite({
|
|
95
|
+
"mandate_fit": r.mandate_fit,
|
|
96
|
+
"outreach_suitability": r.outreach_suitability,
|
|
97
|
+
"transition_context": r.transition_context,
|
|
98
|
+
"group_owned": r.group_owned == "yes",
|
|
99
|
+
}),
|
|
100
|
+
axis=1,
|
|
101
|
+
)
|
|
102
|
+
df.sort_values("composite", ascending=False).head(25).to_csv("call_list.csv", index=False)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Reference
|
|
106
|
+
|
|
107
|
+
| Name | Kind | Notes |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `ThesisBrief` | dataclass | `validate`, `to_dict`, `to_json`, `to_text` |
|
|
110
|
+
| `composite(company, weights=None)` | function | 0 to 100, one decimal |
|
|
111
|
+
| `rank(companies, weights=None)` | function | returns copies with `composite` |
|
|
112
|
+
| `signal_coverage(map)` | function | visible, checked, ratio |
|
|
113
|
+
| `SIGNALS` | tuple | 15 names |
|
|
114
|
+
| `DEFAULT_WEIGHTS` | dict | 0.7, 0.2, 0.1 |
|
|
115
|
+
| `PILOT_URL` | str | pilot request page |
|
|
116
|
+
|
|
117
|
+
## Related products
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
## Questions
|
|
122
|
+
|
|
123
|
+
**Is a database included?** No. Bring your own screened records.
|
|
124
|
+
|
|
125
|
+
**Why dictionaries?** They match JSON, CSV rows and pandas without a conversion step.
|
|
126
|
+
|
|
127
|
+
**Python versions?** 3.7 and up. MIT license. info@alpha-quantum.com
|
|
128
|
+
|
|
129
|
+
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
|
|
130
|
+
Platform: UNKNOWN
|
|
131
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
132
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
133
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
134
|
+
Classifier: Programming Language :: Python :: 3
|
|
135
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
136
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
137
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
138
|
+
Requires-Python: >=3.7
|
|
139
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
setup.py
|
|
5
|
+
acquisitionuniverse/__init__.py
|
|
6
|
+
acquisitionuniverse/toolkit.py
|
|
7
|
+
acquisitionuniverse.egg-info/PKG-INFO
|
|
8
|
+
acquisitionuniverse.egg-info/SOURCES.txt
|
|
9
|
+
acquisitionuniverse.egg-info/dependency_links.txt
|
|
10
|
+
acquisitionuniverse.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
acquisitionuniverse
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from setuptools import setup, find_packages
|
|
3
|
+
|
|
4
|
+
README = (pathlib.Path(__file__).parent / "README.md").read_text(encoding="utf-8")
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="acquisitionuniverse",
|
|
8
|
+
version="1.0.0",
|
|
9
|
+
description="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.",
|
|
10
|
+
long_description=README,
|
|
11
|
+
long_description_content_type="text/markdown",
|
|
12
|
+
author="Alpha Quantum",
|
|
13
|
+
author_email="info@alpha-quantum.com",
|
|
14
|
+
url="https://www.acquisitionuniverse.com",
|
|
15
|
+
project_urls={
|
|
16
|
+
"Homepage": "https://www.acquisitionuniverse.com",
|
|
17
|
+
"Documentation": "https://www.acquisitionuniverse.com/method.php",
|
|
18
|
+
"Source": "https://github.com/explainableaixai/acquisitionuniverse",
|
|
19
|
+
"Mirror": "https://gitlab.com/url-classifications/acquisitionuniverse",
|
|
20
|
+
"Tracker": "https://www.acquisitionuniverse.com/contact.php",
|
|
21
|
+
"Pilot": "https://www.acquisitionuniverse.com/free-pilot.php",
|
|
22
|
+
},
|
|
23
|
+
license="MIT",
|
|
24
|
+
packages=find_packages(exclude=("tests", "test")),
|
|
25
|
+
python_requires=">=3.7",
|
|
26
|
+
keywords=["acquisition target", "target screening", "m&a", "search fund", "private equity", "deal sourcing",
|
|
27
|
+
"thesis", "add-on acquisition", "buy-side", "independent sponsor", "corporate development", "long list"],
|
|
28
|
+
classifiers=[
|
|
29
|
+
"Development Status :: 5 - Production/Stable",
|
|
30
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
31
|
+
"License :: OSI Approved :: MIT License",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3.7",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Topic :: Office/Business :: Financial",
|
|
36
|
+
],
|
|
37
|
+
)
|