Fyrefly
v0.6.0

Fyrefly

A quant-based Python library for flexible math, financial formulas, equation solving, SQL-powered data analysis, AI-driven insights, and visualization — every function takes any number of arguments.

Installation

Fyrefly is a single package — every function in this reference comes from one install.

$pip install fyrefly

Quickstart

Most real usage touches several parts of the library in one script. Here's a realistic end-to-end example — load a file, clean it, query it, chart it, and ask a question about it in plain English.

from fyrefly import load, clean, sql, viz, ask

# Load a dataset — CSV, Excel, or a Google Sheet all work
c = load("sales.csv")

# Clean it up: dedupe, strip whitespace, standardize column names
c = clean(c)

# Query it with real SQL, by the variable name you gave it
top_regions = sql("select region, sum(amount) as total from c group by region order by total desc")

# Visualize the result — one line, no separate charting library
viz.bar("region", "amount")

# Or just ask in plain English (needs your own API key)
ask(c, "which region grew the fastest last quarter?", api_key="AIza...")

Everything below is organized by what it does. Math, sequences, and geometry need nothing but Python. Data operations need a file to load. The AI functions are the only ones that need an API key of your own — everything else runs completely offline.

Math

The building blocks. Each function takes any number of arguments and applies the operation left to right — add(1, 2, 3, 4) sums all four; div(100, 5, 2) divides 100 by 5, then that result by 2.

add(*nos)

Adds any count of numbers.

add(5, 4, 6, 10)
# 25

add(1, 2, 3, 4, 5)
# 15

mul(*nos)

Multiplies any count of numbers.

mul(5, 4, 6, 10)
# 1200

div(*nos)

Divides numbers in sequence: first ÷ second ÷ third ...

div(100, 5, 2)
# 10.0

mod(*nos)

Finds the remainder in sequence: first % second % third ...

mod(17, 5)
# 2

diff(*nos)

Subtracts numbers in sequence: first − second − third ...

diff(10, 3, 2)
# 5

Quant & Finance

Interest and profit/loss formulas for everyday financial math. profit() and loss() are deliberately strict: call the wrong one — say profit() on numbers that are actually a loss — and you get a clear error telling you which function to use instead, rather than a silently wrong (negative) answer.

si(p, n, r)

Simple Interest: (p × n × r) / 100.

si(1000, 2, 5)
# 100.0

ci(p, n, r, freq="annual")

Compound interest earned (principal excluded). freq is "annual" or "semi-annual".

ci(1000, 2, 10)
# 210.0

ci(1000, 2, 10, "semi-annual")
# 215.5

si_ci_diff(p, r, n)

Difference between Compound and Simple Interest. Supports n = 2 or 3 years only.

si_ci_diff(1000, 10, 2)
# 10.0

si_ci_diff(1000, 10, 3)
# 31.0

profit(sp, cp, pct=None)

Profit = sp − cp. Pass "%" as the third argument for (value, percent). Raises a clear error if sp < cp — that's a loss, not a profit.

profit(120, 100)
# 20

profit(120, 100, "%")
# (20, 20.0)

profit(80, 100)
# raises: this is a LOSS. Use loss(80, 100) instead.

loss(sp, cp, pct=None)

Loss = cp − sp. Pass "%" as the third argument for (value, percent). Raises a clear error if sp > cp — that's a profit, not a loss.

loss(80, 100)
# 20

loss(80, 100, "%")
# (20, 20.0)

Sequences

Each progression type works the same way: call it directly for the full list of terms, or use .tn() and .sn() when you only need one term or a running total — no need to generate the whole sequence first.

ap(a, d, n) · ap.tn(a, d, n) · ap.sn(a, d, n)

Arithmetic Progression: full sequence, the nth term, or the sum of the first n terms.

ap(2, 3, 4)
# [2, 5, 8, 11]

ap.tn(2, 3, 4)
# 11

ap.sn(2, 3, 4)
# 26.0

gp(a, r, n) · gp.tn(a, r, n) · gp.sn(a, r, n)

Geometric Progression: full sequence, the nth term, or the sum of the first n terms.

gp(2, 3, 4)
# [2, 6, 18, 54]

gp.tn(2, 3, 4)
# 54

gp.sn(2, 3, 4)
# 80.0

hp(a, d, n) · hp.tn(a, d, n) · hp.sn(a, d, n)

Harmonic Progression — reciprocals of an AP with first term 1/a. No closed-form sum exists, so .sn() totals the actual terms.

hp(2, 3, 4)
# [2.0, 0.2857..., 0.1538..., 0.1052...]

hp.tn(2, 3, 4)
# 0.10526315789473684

Averages & Statistics

avg() takes raw numbers directly; avgc() is for when you already know a value's frequency rather than listing it out repeatedly — e.g. avgc(5, 3) for "the value 5, occurring 3 times", instead of avg(5, 5, 5).

avg(*nos)

Plain average of individual numbers.

avg(10, 12, 13)
# 11.666666666666666

avgc(*value_count_pairs)

Weighted average from (value, count) pairs — e.g. "this value occurred this many times."

avgc(5, 3)
# 5.0   (three 5's)

avgc(5, 3, 10, 2)
# 7.0   (three 5's and two 10's)

mode(*nos)

Most frequently occurring value(s). Returns a list — may contain more than one value if there's a tie.

mode(1, 2, 2, 3)
# [2]

mode(1, 1, 2, 2)
# [1, 2]

median(*nos)

Median of any count of numbers.

median(1, 3, 2)
# 2

median(1, 2, 3, 4)
# 2.5

Geometry

Coordinate geometry formulas you'd otherwise look up every time. Points are plain (x, y) tuples throughout, so these compose easily with your own data.

hyp(a, b)

Hypotenuse of a right triangle given the other two sides.

hyp(3, 4)
# 5.0

slope(point1, point2)

Slope between two (x, y) tuples: (y2 − y1) / (x2 − x1). Raises a clear error for a vertical line.

slope((1, 2), (3, 4))
# 1.0

centroid(point1, point2, point3)

Centroid of a triangle given three (x, y) tuples.

centroid((1, 2), (3, 2), (4, 5))
# (2.6666666666666665, 3.0)

dist(point1, point2)

Distance between two (x, y) tuples: √((x2−x1)² + (y2−y1)²).

dist((6, 2), (10, 5))
# 5.0

Equations

eqn.l() solves a system of simultaneous equations — give it one tuple per equation (its coefficients, then the constant) and it works out every unknown. eqn.q() finds the roots of a polynomial of any degree, including complex roots when there's no real solution.

eqn.l(*equations)

Solves n linear equations in n unknowns. Each equation is (coefficients..., constant) — n+1 numbers for n unknowns.

eqn.l((2, 3, 5), (7, 6, 10))
# (x, y) solving 2x+3y=5, 7x+6y=10

eqn.l((1,1,1,6), (2,-1,1,3), (1,2,-1,2))
# (a, b, c) — scales to any n

eqn.q(*coeffs) · eqn.q.sum(*coeffs) · eqn.q.mul(*coeffs)

Roots of a polynomial of any degree, plus the sum and product of all roots (Vieta's formulas — valid at any degree).

eqn.q(1, 3, 4)
# roots of x^2 + 3x + 4 = 0

eqn.q(3, 4, 6, 9, 10)
# roots of the quartic

eqn.q.sum(5, 2, 3)
# -0.4

eqn.q.mul(5, 2, 3)
# 0.6

Logs, Exponents & Roots

Everyday algebra helpers — logarithms with any base, exponents in either direction, and roots of any degree, including correct handling of negative numbers for odd roots.

log(x, base=e)

Logarithm of x. Natural log by default; pass a second argument for a different base.

log(math.e)
# 1.0

log(8, 2)
# 3.0

exp(a, b)

a raised to the power b. Works for positive and negative exponents.

exp(2, 5)
# 32

exp(2, -1)
# 0.5

sqrt(no) · curt(no) · nroot(no, n)

Square root, cube root, and the generic nth root. Odd roots of negative numbers work correctly.

sqrt(16)
# 4.0

curt(27)
# 3.0

curt(-27)
# -3.0

nroot(16, 2)
# 4.0

Trigonometry

Degrees by default — pass mode="rad" for radians.

sin(angle, mode="deg") · cos(...) · tan(...)

The three primary trigonometric ratios.

sin(30)
# 0.5

cos(60)
# 0.5

tan(45)
# 1.0

cosec(angle, mode="deg") · sec(...) · cot(...)

The three reciprocal trigonometric ratios. Raise a clear error at an undefined point.

cosec(30)
# 2.0

sec(60)
# 2.0

cot(45)
# 1.0

Units

One function covers ten categories — length, mass, volume, area, time, speed, data, energy, pressure, and temperature. convert() figures out the category from the units themselves, so you never specify it; it just refuses to convert across categories (length to mass, say) with a clear error.

convert(value, from_unit, to_unit)

Converts between units within the same category. Case-insensitive, with generous aliases (km / kilometer / kilometers). Raises a clear error for incompatible categories.

convert(10, "km", "miles")
# 6.213711922373339

convert(98.6, "F", "C")
# 37.0

convert(1, "gb", "mb")
# 1024.0

Casting & Rounding

Type conversion and rounding, fixing two classic Python gotchas.

cast(value, target)

Casts a value — or an entire DataFrame column — to another type. Fixes Python's bool("False") gotcha (which is actually True!) and parses string literals like "[1, 2, 3]" into real lists instead of character-splitting.

cast("123", "int")
# 123

cast("False", "bool")
# False   (Python's own bool("False") is True!)

cast("[1, 2, 3]", "list")
# [1, 2, 3]

c["age"] = cast(c["age"], "int")   # casts a whole column

round(value, places=0)

Rounds using standard round-half-up — the way Excel works — instead of Python's "banker's rounding," where round(2.5) is actually 2.

round(2.5)
# 3   (Python's own round(2.5) gives 2)

round(1250, -2)
# 1300   (Python's own round(1250, -2) gives 1200)

Data Operations

The typical flow: load() a file into a variable, inspect it with loadh()/loadc()/loads(), optionally clean() it, then query it with sql() — real SQL, powered by DuckDB, run directly against your DataFrame by its variable name.

load(path)

Loads a CSV, Excel file, or Google Sheet into a DataFrame and previews it.

c = load("data.csv")
c = load("data.xlsx")
c = load("https://docs.google.com/spreadsheets/d/.../edit")

loadh(df) · loadc(df) · loads(df)

Preview just the column headers, the record count, or the schema (columns + datatypes).

loadh(c)
loadc(c)
loads(c)

sql(query)

Runs any SQL query against DataFrames already loaded in your script, by variable name. Powered by DuckDB — full SQL, including JOINs and window functions.

d = sql("select * from c where age > 30 order by age desc")

xtract(df, filename=None)

Exports a DataFrame to a CSV file in the current directory.

xtract(d)
xtract(sql("select * from c"), filename="filtered.csv")

clean(df)

Standardizes column names, strips whitespace from text columns, drops exact duplicates, and drops fully-empty rows. Returns a new DataFrame.

c = clean(c)

Lookups & Excel Functions

If you already know these from spreadsheets, they behave the same way here. Every function auto-detects your loaded dataset, so once you've called load(), you don't need to pass it in again — unless you've loaded more than one dataset, in which case pass it explicitly as df=.

vlook(value, return_col, df=None, default=...)

Searches the FIRST column (like Excel's VLOOKUP) and returns the matching row's value. Auto-detects your loaded dataset.

c = load("employees.csv")
vlook("Alice", "salary")
# 85000

vlook("Zoe", "salary", default=0)
# 0  (no error, since a fallback was given)

xlook(value, lookup_col, return_col, df=None, default=...)

Searches ANY column you specify (like Excel's XLOOKUP) — not limited to the first.

xlook("Engineering", "department", "name")
# "Alice"

countif(col, value, df=None)

Counts rows where col equals value.

countif("department", "Engineering")
# 3

sumif(col, value, sum_col, df=None)

Sums sum_col for rows where col equals value.

sumif("department", "Engineering", "salary")
# 250000

avgif(col, value, avg_col, df=None)

Averages avg_col for rows where col equals value.

avgif("department", "Engineering", "salary")
# 83333.33

AI

Bring your own API key from Anthropic, OpenAI, Gemini, or any OpenAI-compatible provider (Groq, Mistral, etc.) — the provider is detected automatically from the key's format, so you rarely need to specify it. Nothing here works without a key; everything else in Fyrefly does.

ask(data, question, api_key=None, provider=None, model=None, base_url=None)

Converts a plain-English question into SQL and runs it. Provider auto-detected from your key's format (Anthropic, OpenAI, Gemini, or Groq/any OpenAI-compatible API). Pass a tuple of DataFrames for joins across any number of datasets. Transient provider errors retry automatically.

c = load("sales.csv")

ask(c, "total sales by region last quarter?", api_key="AIza...")

ask((c, d), "top customers by total sales?", api_key="...")

insights(df, api_key=None, provider=None, model=None, base_url=None)

Generates a short, plain-English summary of a dataset — trends, ranges, and data-quality issues.

insights(c, api_key="AIza...")

Visualization

No API key, no separate charting library to learn. Call viz() directly with a kind, or use the shorter viz.bar(), viz.scatter(), etc. once you've loaded a single dataset — both produce the same chart.

viz(df, x=None, y=None, kind="auto", hue=None, title=None, figsize=(8,5), save=None)

Visualizes a DataFrame. kind="auto" picks a sensible chart based on your columns' types — heatmap, bar, histogram, or scatter.

viz(c)                               # auto -> correlation heatmap
viz(c, "age", "salary")              # auto -> scatter
viz(c, "department", "salary", kind="box")

viz.bar / .scatter / .line / .box / .violin / .hist / .kde / .pie / .heatmap / .pairplot

Namespace shortcuts for each chart type. Auto-detects your loaded DataFrame when exactly one is in scope.

c = load("data.csv")

viz.bar("department", "salary")
viz.scatter("age", "salary")
viz.heatmap()
viz.pairplot(hue="department")