SymbolicRegression.jl: Pareto-Front Equation Discovery in Julia
Distributed High-Performance Symbolic Regression in Julia
At a glance
- What is it?
- A Julia package that searches for analytic expressions fitting a dataset, exposing a Pareto front of accuracy against complexity. It is the engine behind PySR, and its interface assumes you can read a Pareto front rather than a single fitted model.
- Who is it for?
- Adopt SymbolicRegression.jl if you need a closed-form expression rather than a fitted black box, you are comfortable in Julia, and someone on the team can interpret a Pareto front instead of accepting the default selection_method. Do not adopt it as a drop-in replacement for gradient-boosted trees on wide tabular data, and do not adopt it if nobody will inspect the returned equations.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly Julia, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem: a model you can write on a whiteboard
Most regression tooling returns a fitted object. You get coefficients, a tree ensemble, or a neural network, and the explanation of why a prediction came out the way it did is a separate project. SymbolicRegression.jl attacks the same fitting problem from the other end. The README states the goal directly: it searches for symbolic expressions which optimize a particular objective. The output is not a parameter vector. It is a formula built from the operators you supply, such as cos, exp, +, and *, with numeric constants the search fits along the way. That changes who the tool is for. It suits a scientist or engineer who suspects the data came from a compact physical relationship and wants the relationship back in closed form. It does not suit someone who simply wants the lowest error on a prediction task and does not care what the function looks like. The package is Apache-2.0 licensed, lives at astroautomata/SymbolicRegression.jl, and the README points to PySR for a Python frontend, so the Julia package is the engine and Python is one of its clients.
How the search actually runs: populations, Pareto fronts, and a hall of fame
The README describes the heart of the package as the equation_search function, which takes a 2D array and attempts to model a 1D array using analytic functional forms. The low-level interface is explicit about shape: it assumes column-major input of shape [features, rows], which is the opposite convention from the SRRegressor path above it. You pass an Options object holding binary_operators, unary_operators, and populations, plus niterations and a parallelism setting such as :multithreading. The search returns a hall of fame, and calculate_pareto_frontier filters it to the dominating set, described in the README as the best expression seen at each complexity. Each member is a PopMember carrying an expression and a cost. This is the design decision that matters most: the package does not hand you one answer. It hands you a front, one equation per complexity level, and leaves the accuracy-versus-complexity trade to you. The README notes that predict uses model.selection_method, which by default is a mix of accuracy and complexity, and that you can override it with predict(mach, (data=X, idx=2)) to evaluate the second equation on the front. Expressions themselves are Node types from DynamicExpressions.jl, wrapped by an Expression type that carries operator and variable-name metadata. The README shows you can build one by hand, for example cos(x1 - 3.2 * x2) - x1 * x1, convert its constants with convert(Expression{Float32}, tree), and call it on a matrix. There is a distinction worth noting between tree(X), which sets all values to NaN if any Inf or NaN appeared during evaluation, and eval_tree_array(tree, X), which returns a did_succeed flag so you can see the failure instead of absorbing it.
Getting from Pkg.add to a reported equation
Installation is one line: using Pkg; Pkg.add("SymbolicRegression"). The high-level path is the SRRegressor type. The README example builds a dataset as a NamedTuple of vectors, X = (a = rand(500), b = rand(500)), constructs y = @. 2 * cos(X.a * 23.5) - X.b ^ 2 with noise added, and configures the model with niterations=50, binary_operators=[+, -, *], and unary_operators=[cos]. Training is mach = machine(model, X, y) followed by fit!(mach). Results come back through report(mach), and predictions through predict(mach, X). Two interface details are easy to miss. If you pass a table-like object, expressions are printed using the column names; if you pass a plain array such as randn(100, 2), the variables become x1, ..., xn. And inputs are not restricted to NamedTuples: the README says matrices, NamedTuples of vectors, or any Tables.jl-compatible table such as a DataFrame all work. For multiple outputs there is MultitargetSRRegressor, where predict takes an array of indices into idx to pick equations for specific outputs. The same functions are exported by MLJ, so import MLJ: machine, fit!, predict, report gives you pipelines and tuning from that ecosystem. That MLJ route is the one to take if you already have hyperparameter search infrastructure, because the package's own Options surface is broad and the README defers the full list to the API page.
Where the interface will fight you
The largest practical hazard is the shape convention split. The high-level SRRegressor accepts rows-as-observations, which is what most Julia data work looks like. The low-level equation_search expects [features, rows]. Mixing the two silently transposes your problem, and a search on a transposed matrix will still run and still return equations. Nothing in the README suggests a guard against this. The second hazard is the default selection. Because predict uses a mix of accuracy and complexity unless you override it, a pipeline that calls predict without inspecting report(mach) is choosing an equation on your behalf by a rule you did not write. That is the opposite of why most people reach for symbolic regression. Third, the search is stochastic and iterative: niterations is a budget, not a convergence criterion, and the README gives no stopping rule. Fourth, operator choice is a hard constraint on the hypothesis space. The quickstart example supplies only +, -, *, and cos; if the true relationship needs division or a square root, that run cannot express it, and the Pareto front will be the best of a family that excludes the answer. The README's own low-level example adds / and exp for exactly this reason. Finally, constant fitting interacts with numeric type. The README shows converting an expression's constants to Float32, and expression types are parameterized by the constant type, so precision is a deliberate choice rather than a default you can ignore.
The alternative: gradient boosting, and the real difference in approach
The obvious alternative for tabular regression is a gradient-boosted tree library, and the difference is not accuracy, it is what you receive. A boosted ensemble is a sum of hundreds of trees fitted to residuals. It will typically fit a noisy 500-row dataset more accurately than a short analytic expression, and it will do so without you choosing an operator set. What it will not give you is a formula. You cannot differentiate it by hand, you cannot read a physical constant out of it, and you cannot paste it into a simulator. Symbolic regression trades raw fit for interpretability, and the Pareto front is the receipt for that trade: you can see exactly how much error you are accepting for each step down in complexity. The second alternative is writing the model yourself from domain knowledge and fitting its parameters with a standard nonlinear least-squares routine. That approach is faster and far more predictable when you already know the functional form. SymbolicRegression.jl is for when you do not, and you want the search to propose candidate forms. The third alternative is PySR, which the README presents as a Python frontend to this same package. Choosing between them is a language decision, not a capability decision.
Maintenance, releases, and what Apache-2.0 means here
The repository is not archived, the default branch is master, and the recent release history shows v2.4.1 on 2026-09-10, v2.4.0 on 2026-09-06, and v2.3.0 on 2026-09-06. Those three releases land within days of each other, which tells you the maintainers ship in small increments rather than on a slow cadence. For a dependency, that cuts both ways. You get fixes quickly, and you also get version churn, so pinning a version in your Project.toml is the sane default if you are embedding this in a pipeline. The licence is Apache-2.0, which is permissive and includes an explicit patent grant. That is the licence identifier as stated in the repository metadata; it is not legal advice, and if you are shipping the package inside a commercial product you should have your own counsel read the terms rather than a review. The README links a paper at arXiv:2305.01582 and asks that you cite the software, which is a request, not a licence condition. One structural note on dependencies: expressions are Node types from DynamicExpressions.jl and the package integrates with SymbolicUtils.jl for export, so upgrading SymbolicRegression.jl may pull those along. The README also points to MLJ compatibility, which means your upgrade surface can include the MLJ stack if you use that path.
Who should adopt it, and what to check before you do
The fit is narrow and clear. You should adopt SymbolicRegression.jl if your deliverable is an equation, if your dataset is small enough that evaluating a candidate expression over it is cheap, and if you have someone who will read report(mach) and argue about which point on the front to keep. You should not adopt it if your only goal is prediction error on wide tabular data, if you need a model in production within a day, or if no one on the team will look past the default selection_method. Before you commit, verify three things against your own data. First, run the quickstart with niterations=50 and binary_operators=[+, -, *], unary_operators=[cos], then call report(mach) and confirm the front contains an equation you recognize. Second, confirm your operator set can express the relationship you suspect, because a missing / or exp removes it from the search space entirely. Third, check the shape convention of whichever entry point you use, since equation_search wants [features, rows] while SRRegressor takes observations as rows. If the front comes back with a single dominating equation at every complexity, or with equations you cannot justify from the data, the search has not found structure and more iterations will not manufacture it.
Editorial conclusion
Adopt SymbolicRegression.jl if you need a closed-form expression rather than a fitted black box, you are comfortable in Julia, and someone on the team can interpret a Pareto front instead of accepting the default selection_method. Do not adopt it as a drop-in replacement for gradient-boosted trees on wide tabular data, and do not adopt it if nobody will inspect the returned equations. Before committing, run the quickstart example with niterations=50 and binary_operators=[+, -, *], unary_operators=[cos], then call report(mach) and read the whole front. If you cannot defend the second or third equation on that front against the data, the tool has not answered your question yet.
Community notes