For a month I ran an automated screen over ideas for small software and digital products: SaaS, web templates, books, downloads, content channels, e-commerce. 2,512 ideas went in. Most of them failed, and that turned out to be the useful part, because every failure was stored with the rule that stopped it.
I published a free 100-row sample of that table under CC BY 4.0: screened-product-ideas on Hugging Face. This post is about the schema, the three design choices that made it worth querying, and a few lines of Python to read it.
The columns
| column | what it holds |
|---|---|
id, title, field, created | the idea and when it was generated |
stage, status, verdict | how far it got (S1, S2, …) and how it ended: parked, killed, still open |
screen_total, screen_line | the score at the field screen and the pass line it had to beat |
desk_total, c1 … c10 | the deeper ten-criterion scores, only for ideas that got that far |
weakest_criterion, fatal_codes | which criterion sank it |
kill_codes | one or more coded reasons, separated by ; |
kill_reason | the same reason as a sentence, for humans |
Design choice 1: code the reason at the moment of the kill
Free-text reasons are easy to write and painful to count. Every kill in this pipeline writes a short code from a fixed list (below_line, sibling_family, crowded_no_wedge, not_sharp, market_too_small, …) next to the sentence. The sentence is for reading one row; the code is for reading all of them.
An idea can fail for more than one reason, so kill_codes is multi-valued: crowded_no_wedge;llm_substitute means "existing products already do this, and a chatbot answer would replace it too". Counting has to split the field:
import csv, io, urllib.request from collections import Counter URL = ("https://huggingface.co/datasets/bardachtech/screened-product-ideas" "/resolve/main/idea-database-sample-100.csv") with urllib.request.urlopen(URL) as resp: rows = list(csv.DictReader(io.TextIOWrapper(resp, encoding="utf-8"))) codes = Counter(code for row in rows for code in row["kill_codes"].split(";") if code) print(len(rows), "rows") for code, n in codes.most_common(6): print(f"{code:<20} {n}") 100 rows below_line 43 sibling_family 25 crowded_no_wedge 9 weak_evidence 4 not_a_unit 4 not_sharp 4 (On macOS with the python.org build, run Install Certificates.command once if urlopen fails with a certificate error, or download the CSV and open it locally.)
Design choice 2: empty means "not measured", not zero
Only ideas that passed the first screens got the ten-criterion scores. In the sample, 2 of 100 rows have c1 … c10 filled. If those blanks were stored as 0, every average over the column would be wrong and every "lowest score" query would return ideas that were never scored. The CSV keeps them empty, and the reading code must keep them apart:
scored = [r for r in rows if r["c1"] != ""] print("rows with C1-C10 scores:", len(scored)) # 2 Design choice 3: store the line next to the score
Different fields were screened with different scales. In the sample, one digital-download idea has screen_total 47 against a line of 55, a book idea has 3.12 against 3.5. Comparing raw totals across rows would rank a 47 above everything on the 1-to-5 scale. Because each row carries its own screen_line, the honest comparison is the gap to the line:
def margin(row): if not row["screen_total"]: return None return float(row["screen_total"]) / float(row["screen_line"]) - 1 failed = [r for r in rows if margin(r) is not None and margin(r) < 0] near_misses = sorted(failed, key=margin, reverse=True)[:5] # closest to the line first A margin of −0.05 means "5 % short of the line", whatever the scale. The closest miss in the sample is a content idea at 3.18 against 3.2.
What the codes say across all 2,512 ideas
Counting codes over the whole table (any position in kill_codes; these counts come from the export report of all 2,512 rows, the sample alone gives the numbers above), the top four are:
| code | ideas | meaning |
|---|---|---|
below_line | 787 | scored under the pass line of its stage |
sibling_family | 770 | settled because a family of near-identical ideas had already failed |
crowded_no_wedge | 228 | existing products already do the job, nothing structural to win with |
not_sharp | 207 | the card was too vague: no clear trigger, buyer or wedge |
The second line is the lesson I did not expect. An idea generator produces many variants of one idea. Without a "sibling" rule, the screen would have spent its budget re-checking the same dead end 770 times. If you build any kind of generate-and-filter pipeline, group near-duplicates before you score them.
Limits of the data
- The ideas were generated and screened by an automated pipeline between 10 October and 10 November 2026. The scores are rubric scores from that pipeline, not sales results. A killed idea is not proven bad; it failed this screen.
- Titles were scrubbed: rows with personal or third-party sources were dropped before export.
- The sample is 100 rows stratified by field, so its code counts differ from the full table's.
Links
The sample, its data card and the licence: huggingface.co/datasets/bardachtech/screened-product-ideas. About the author: the same project runs a set of free browser calculators, and this screen is how it picks what to build next: tools.bardach-technologies.com.
This article was created with the help of AI. The counts and code were checked by running them against the published CSV. I built the pipeline and published the free sample linked above.
