# Polars small business kit November 1, 2026. [Read the business guide](https://jwatte.com/blog/polars-2-small-business-guide/). ## How to use this kit Use this for a repeatable report that someone currently rebuilds from exports. Start with copies of two files and a known total. These recipes ran with Polars 2.1.0 and synthetic data; they are examples, not a completed integration with your accounting or point of sale system. Change the column names and business rules deliberately, then compare the output with the report you trust. Name an owner and a backup person before putting it on a schedule. ## A one page plan | Decision | Write your answer | |---|---| | Useful question | Which location, job or product needs attention? | | Source | Who exports which files, and when are they complete? | | Keys | What identifies an order, product, customer and location? | | Expected result | Row count, signed amount total, date range, missing keys | | Maintainer | Person who can read Python and fix a failed import | | Runner | Existing laptop first; a server only when unattended runs matter | | Delivery | Private dated workbook for people; Parquet for later scripts | | Failure | Stop publication, keep the last good output, alert the owner | | Retention | Approved retention periods for originals, outputs and backups | | Trial | Compare old and new reports for two complete reporting cycles | Our assumption is a two cycle trial, not a universal acceptance standard. Include refunds, month end, duplicate exports and a missing product in your trial. An absent file must produce an alert, not an apparently successful report with smaller sales. ## Install on Windows, Mac or Linux Polars 2.1.0 requires Python 3.11 or later according to [PyPI metadata](https://pypi.org/pypi/polars/2.1.0/json). We used Python 3.12.15. Install Python using your organization's approved installer. Keep this project in a private folder named `report-project`. The commands below start inside that folder. Windows PowerShell, using the Python launcher: ```powershell py -3.12 -m venv .venv .\.venv\Scripts\python.exe -m pip install "polars==2.1.0" "xlsxwriter==3.3.1" .\.venv\Scripts\python.exe -c "import polars; print(polars.__version__)" ``` Mac or Linux, using an installed compatible Python: ```sh python3 -m venv .venv .venv/bin/python -m pip install 'polars==2.1.0' 'xlsxwriter==3.3.1' .venv/bin/python -c 'import polars; print(polars.__version__)' ``` No activation is required when you call the environment's interpreter explicitly. If your Linux distribution separates the venv module into a package, ask its administrator to enable it. Do not install project packages into the system Python. Alternatively, with [uv installed through its official instructions](https://docs.astral.sh/uv/getting-started/installation/), use the same commands on all three platforms: ```sh uv venv --python 3.12 uv pip install 'polars==2.1.0' 'xlsxwriter==3.3.1' uv pip freeze > requirements-lock.txt ``` Create `requirements.txt` containing: ```text polars==2.1.0 xlsxwriter==3.3.1 ``` Use `uv pip install -r requirements.txt` or the environment's `python -m pip install -r requirements.txt` to reproduce this small dependency set. Keep the lock snapshot with the script. The version check and installation commands are setup instructions; the Python data recipes below were executed in an isolated Linux environment. The [installation guide](https://docs.pola.rs/user-guide/installation/) lists optional extras. `polars[calamine]==2.1.0` adds the default Excel reader; `polars[xlsxwriter]==2.1.0` adds Excel writing; `polars[excel]==2.1.0` adds all supported Excel engines. `connectorx`, `adbc`, `sqlalchemy` and `database` cover database integrations. `fsspec` covers filesystem interoperability; cloud access may also need the provider's filesystem package. Install only what the script uses. On an older CPU, the documented compatibility variant is `polars[rtcompat]==2.1.0`. The `rt64` variant raises the row index limit; it does not add memory to your computer. ## Input contract and working folders Keep `exports` for complete, nonoverlapping input files and `output` for results. Preserve original files separately before changing them. Never put a previous output back into the input glob. Export identifiers as text so that product `001` does not become product `1`. A new or renamed column is a reason to inspect the export. Recipes 1 and 2 create `raw` and `sales`. Recipes 4, 5, 6, 9, 10 and 11 reuse `sales` in the same Python process. Put the selected blocks in a file named `report.py`, in order. The other recipes read their own named CSV inputs. All amounts below are synthetic US dollar examples. Integer columns ending in `_cents` contain cents, never dollars. ### 1. Stack monthly CSV exports Each file in `exports` needs these columns. The header belongs in every file. Values containing commas must be CSV quoted. ```csv date,location,sku,amount,quantity 2026-09-01,North,001,$12.50,2 2026-09-02,South,002,$20.00,4 ``` ```python import polars as pl raw = pl.read_csv( "exports/*.csv", schema_overrides={"date": pl.String, "location": pl.String, "sku": pl.String, "amount": pl.String, "quantity": pl.Int64}, ) print(raw.height) ``` Keep the column types explicit. This stacks rows; it cannot know whether two exports contain the same sale. Production exports should include a stable transaction ID and a duplicate check. Do not quietly remove duplicate amounts, since two customers can buy the same thing. ### 2. Clean dates and currency text Input: `raw` from recipe 1. Dates use `YYYY-MM-DD`. Currency uses exactly two fractional digits, an optional dollar sign and correctly grouped commas; refunds use a leading minus sign. Parenthesized amounts and other locales require a different parser. ```python money_ok = pl.col("amount").str.contains(r"^-?\$?(?:\d+|\d{1,3}(?:,\d{3})+)\.\d{2}$") if not raw.select(money_ok.fill_null(False).all()).item(): raise ValueError("Expected US currency with exactly two decimal places") sales = raw.with_columns( pl.col("date").str.to_date("%Y-%m-%d"), (pl.col("amount").str.replace_all(r"[$,]", "") .cast(pl.Decimal(12, 2)) * 100) .cast(pl.Int64).alias("revenue_cents"), ).drop("amount") ``` This parses a decimal before converting to integer cents. The validation rejects extra fractional digits; agree and implement a separate rounding policy if the source uses them. Missing values, invalid dates and amounts outside the agreed format need an exception report. Do not use `strict=False` merely to make failures disappear. Your production acceptance checks must reject null dates, amounts and identifiers as well as parse errors. ### 3. Keep the latest customer record Expected input `customers.csv`: ```csv customer_id,updated_at,email 01,2026-10-01T08:00:00,old@example.test 01,2026-10-02T08:00:00,new@example.test ``` ```python customers = pl.read_csv("customers.csv", schema_overrides={"customer_id": pl.String}) latest = (customers.with_columns(pl.col("updated_at").str.to_datetime()) .sort("updated_at") .unique(subset="customer_id", keep="last", maintain_order=True) .sort("customer_id")) ``` Use a stable customer ID. Names and email addresses are not reliable identity keys. This assumes timestamps have a consistent timezone and no ties for a customer; reject ties or add an agreed tie breaker. It keeps a complete record, rather than merging individual fields from different versions. ### 4. Sales by location and month Input columns from recipe 2: `date` as Date, `location` as text, `revenue_cents` as integer. ```python monthly = (sales.with_columns(pl.col("date").dt.truncate("1mo").alias("month")) .group_by("location", "month") .agg(pl.col("revenue_cents").sum()) .sort("location", "month")) ``` Check that the monthly totals add back to the original signed total. A refund reduces the total. Decide with accounting whether the report uses sale date, settlement date or refund date. ### 5. Top products by units Input from recipe 2: `sku` and signed integer `quantity`. ```python top = (sales.group_by("sku").agg(pl.col("quantity").sum()) .sort(["quantity", "sku"], descending=[True, False]).head(10)) ``` This ranks net units, including returns. It is not a profitability ranking. The second sort key makes tied results predictable. ### 6. Join orders to a price list Expected additional input `prices.csv`: ```csv sku,unit_cost_cents 001,200 002,100 ``` ```python prices = pl.read_csv("prices.csv", schema_overrides={"sku": pl.String}) priced = sales.join(prices, on="sku", how="left", validate="m:1") assert priced["unit_cost_cents"].null_count() == 0, "Missing product price" priced = priced.with_columns( (pl.col("revenue_cents") - pl.col("quantity") * pl.col("unit_cost_cents")) .alias("contribution_cents") ) ``` The `m:1` validation rejects duplicate price keys that would multiply sales rows. The assertion catches unmatched products. This contribution calculation excludes labor, overhead, tax and shipping. For historical profit, join a price effective on the sale date rather than today's price. ### 7. Aging buckets for unpaid invoices Expected input `invoices.csv`: ```csv invoice_id,due_date,balance_cents INV01,2026-10-02,30000 INV02,2026-11-10,5000 ``` ```python from datetime import date invoices = pl.read_csv("invoices.csv").with_columns(pl.col("due_date").str.to_date("%Y-%m-%d")) if invoices.select(pl.any_horizontal(pl.col("due_date", "balance_cents").is_null()).any()).item(): raise ValueError("Missing invoice date or balance") aging = (invoices.filter(pl.col("balance_cents") > 0) .with_columns((pl.lit(date(2026, 11, 1)) - pl.col("due_date")) .dt.total_days().alias("days_overdue")) .with_columns( pl.when(pl.col("days_overdue") <= 0).then(pl.lit("Current")) .when(pl.col("days_overdue") <= 30).then(pl.lit("1 to 30")) .when(pl.col("days_overdue") <= 60).then(pl.lit("31 to 60")) .otherwise(pl.lit("Over 60")).alias("bucket") )) ``` Change the fixed reporting date intentionally. Keeping it fixed makes a rerun reproducible. These buckets use days after the due date, not invoice age. Zero or credit balances are excluded; review credits separately. Do not turn this into automatic collection notices. ### 8. Inventory reorder flags Expected input `inventory.csv`: ```csv sku,on_hand,on_order,reserved,reorder_point 001,4,2,1,6 002,4,2,0,6 ``` ```python inventory = pl.read_csv("inventory.csv", schema_overrides={"sku": pl.String}) reorder = inventory.with_columns( ((pl.col("on_hand") + pl.col("on_order") - pl.col("reserved")) < pl.col("reorder_point")).alias("needs_order") ) ``` An item at its reorder point does not trigger this rule; change `<` to `<=` if your policy differs. Flags are a review list. Supplier minimums, delivery dates and damaged stock need their own rules before anyone places an order. ### 9. Export one Excel sheet per location Input: the `sales` table from recipe 2. The location column remains in each sheet so the numbered sheet name is unambiguous. ```python import xlsxwriter from pathlib import Path Path("output").mkdir(exist_ok=True) with xlsxwriter.Workbook("output/locations.xlsx", {"strings_to_formulas": False, "strings_to_urls": False}) as book: for number, frame in enumerate(sales.sort("location").partition_by("location"), 1): frame.write_excel(workbook=book, worksheet=f"Location {number}") ``` Numbered sheet names avoid Excel's sheet name length and character restrictions. Formula and URL conversion are disabled because imported text should remain text. Review who can open the workbook; separate sheets are not access controls. This exports values, not a replacement for workbook macros, charts or existing formula logic. ### 10. Save to Parquet and read it back Input: `sales`; run recipe 9 first to create the output directory. ```python sales.write_parquet("output/sales.parquet") restored = pl.read_parquet("output/sales.parquet") assert restored.equals(sales) ``` Parquet preserves types and stores columns efficiently. Keep original exports too. A compact file is not a substitute for a retention policy or a backup. ### 11. Use SQL for a familiar summary Input: `sales`, including text `sku` and integer `quantity`. ```python with pl.SQLContext(sales=sales, eager=True) as ctx: sql_totals = ctx.execute(""" SELECT sku, SUM(quantity) AS units FROM sales GROUP BY sku ORDER BY units DESC, sku """) print(sql_totals) ``` Polars translates this SQL into its own query expressions. It supports a subset of SQL, not every database dialect. Register only the tables a report needs. This local query context is not a multiuser database with accounts and row permissions. ## Schedule the report First run the whole script manually with the environment's Python and check its output. A successful process exit is only one check: verify the date range, source file count, row count and totals before publishing. Write candidate outputs separately and replace the last good report only after validation. Arrange an alert for failure and for a missing scheduled run. Choose one scheduler below. ### Windows Task Scheduler 1. Open Task Scheduler and select Create Task. Name it `Weekly sales report`. 2. Choose the restricted account that owns the reporting folder. Do not request highest privileges unless a specific operation requires them. 3. On Triggers, create a weekly Monday trigger after exports are expected to arrive. 4. On Actions, choose Start a program. Browse to the project's `.venv\Scripts\python.exe`. Set arguments to `-I report.py`. Set Start in to the full reporting folder chosen during installation, without quotation marks. 5. On Settings, select Do not start a new instance if the task is already running. Enable running as soon as possible after a missed start if that matches your reporting policy. 6. Select Run once and inspect Task History, Last Run Result and the dated output. Test with the actual task account. Running while logged out requires appropriate account permissions and credential handling under your organization's policy. A sleeping laptop may miss its slot. Confirm power and wake settings, or choose an always available runner. [Microsoft's scheduling reference](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create) documents task creation and account options. ### Linux cron For this example the administrator has placed the project in `/opt/sales-report`, owned by a restricted reporting account. Use `crontab -e` as that account and add: ```cron 15 7 * * 1 cd /opt/sales-report && /usr/bin/flock -n /opt/sales-report/run.lock .venv/bin/python -I report.py >> run.log 2>&1 ``` This runs Monday at 07:15 in the scheduler's timezone. Confirm the server timezone and the installed location of `flock` before using the line. The lock prevents overlapping runs; arrange an alert for a skipped run as well as a failed run. Rotate the log and keep it private. Cron does not load your interactive shell profile, so use explicit paths and an approved secret loading mechanism. ### Linux systemd timer An administrator can create `sales-report.service` in the system unit directory: ```ini [Unit] Description=Weekly sales report [Service] Type=oneshot User=reporter WorkingDirectory=/opt/sales-report ExecStart=/opt/sales-report/.venv/bin/python -I /opt/sales-report/report.py UMask=0077 NoNewPrivileges=true PrivateTmp=true ``` Create the restricted `reporter` account and grant access to the input and output folders first. Create the companion `sales-report.timer`: ```ini [Unit] Description=Run the sales report each Monday [Timer] OnCalendar=Mon *-*-* 07:15:00 UTC Persistent=true [Install] WantedBy=timers.target ``` Then the administrator runs: ```sh sudo systemctl daemon-reload sudo systemctl start sales-report.service sudo systemctl status sales-report.service sudo systemctl enable --now sales-report.timer systemctl list-timers sales-report.timer journalctl -u sales-report.service ``` The persistent timer catches a missed calendar run after the machine returns. It cannot fetch an export that was never created. Configure failure notification separately. These are configuration examples, not services installed by this kit. See the [timer documentation](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html). ### GitHub Actions Use this first with synthetic exports committed to a private test repository. Create `.github/workflows/report.yml`: ```yaml name: Weekly report on: workflow_dispatch: schedule: - cron: '15 7 * * 1' permissions: contents: read concurrency: group: weekly-report cancel-in-progress: false jobs: report: runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - run: python -m pip install -r requirements.txt - run: python -I report.py - uses: actions/upload-artifact@v4 with: name: weekly-report path: output/ retention-days: 7 if-no-files-found: error ``` Commit the workflow to the default branch, open Actions, select Weekly report and run it manually. Verify the artifact before waiting for the schedule. The YAML uses readable version tags; review and pin the actions to approved full commit hashes before a production rollout. Scheduled workflows can be delayed and run from the default branch, so do not use this as an exact deadline guarantee. The [GitHub event documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows) explains these limits. For real work, add an authenticated input retrieval step and an approved private destination for outputs. Never commit customer exports or embed storage keys in this YAML. Artifact access, runner location, retention and usage billing need review before using health or financial data. The example does not claim that Actions is free for every repository or workload. Remote schedules were not activated in testing. ## Storage, databases and backups The [cloud storage guide](https://docs.pola.rs/user-guide/io/cloud-storage/) documents S3, Azure and Google storage using the familiar read, scan and write methods. For B2, use its [S3 compatible API](https://www.backblaze.com/docs/cloud-storage-s3-compatible-api), the bucket's actual endpoint and a scoped application key. Supply settings through the runner's protected credential configuration. Verify read and write behavior with a non-sensitive test bucket; no B2 integration was exercised in our recipe tests. For a database, `read_database` accepts a connection object. `read_database_uri` supports ConnectorX or ADBC, with the appropriate driver installed. The [database guide](https://docs.pola.rs/user-guide/io/database/) also describes SQLAlchemy and ADBC writing. Start with a read-only database account and an explicit column list; a reporting script should not need permission to change operational tables. Treat connection strings and serialized query plans as potentially secret. Back up the script, requirements, approved configuration without embedded secrets, original exports and validated outputs. Keep a dated output for each accepted run according to your retention policy. Include all retained versions when estimating storage. Test restoring both data and the pinned environment to another machine. A backup held in the same folder with the same credentials is vulnerable to the same mistakes. ## Data handling checklist - Customer data: export only the fields the report needs. Keep contact details out of aggregate management reports. Limit who can read the originals and backups. - Health information: check [HIPAA applicability](https://www.hhs.gov/hipaa/for-professionals/covered-entities/index.html), access controls, encryption and any required business associate arrangements before placing patient data on a runner or backup service. A Python library does not establish compliance. - Payment data: check [PCI DSS responsibilities](https://www.pcisecuritystandards.org/merchants/) with your processor before exporting account data. Use transaction references and totals where possible; do not put sensitive authentication data into reports. - Financial or member data: check the applicable GLBA safeguards obligations and the institution's regulator before storing records. The [FTC overview](https://www.ftc.gov/business-guidance/resources/ftc-safeguards-rule-what-your-business-needs-know) concerns entities within FTC jurisdiction; credit unions need their regulator's rules. - Workstations and runners: use restricted accounts, encryption, updates and approved secret storage. Do not paste real exports into an AI assistant without authorization and a data handling review. - Delivery: separate sheets and hidden columns do not restrict access. Confirm recipients and private storage permissions before distributing the workbook. ## Troubleshooting | Symptom | Check and action | |---|---| | Out of memory | Inspect the biggest join and final output; scan required columns lazily, review streaming support, reserve disk for spill, then measure on a larger machine if needed. There is no safe universal CSV size to RAM ratio. | | Date parse fails | Confirm export locale, exact format and timezone. Keep rejected rows for review rather than replacing them silently with null. | | Files disagree on schema | Compare headers and types; specify the input schema. Do not use relaxed concatenation to hide an unexpected source change. | | Excel writer missing | Install the pinned XlsxWriter dependency in the same environment that runs the scheduled job. | | Excel reader missing | Install the `calamine` extra or the documented alternative reader required by the workbook. | | Old CPU error | Use the documented `rtcompat` extra in a fresh environment and retest. | | Report totals doubled | Check overlapping exports and duplicate product keys; validate join cardinality. | | Task works manually only | Check working directory, interpreter, account permissions and access to inputs under the scheduler's account. | | CSV output opens as formulas | Treat imported text as untrusted. Prefer the workbook recipe with formula conversion disabled; review CSV handling before opening in a spreadsheet. | | New version changes results | Restore the prior environment and inputs, compare stable keys and totals, and work through the upgrade guide before switching again. | ## Cost worksheet All dollar amounts are US dollars before tax. Prices were read November 1, 2026. The labor share and selected host tiers follow the same basis as the business guide. Hours and workloads are our assumptions, not measured savings. | Input | Your low case | Your high case | |---|---|---| | Manual report hours each week | ___ | ___ | | Remaining review hours each week | ___ | ___ | | Analyst median hourly wage divided by 0.700 | ___ | ___ | | Script setup hours and loaded rate | ___ | ___ | | Server setup hours and loaded rate | ___ | ___ | | Monthly script maintenance hours | ___ | ___ | | Monthly server administration hours | ___ | ___ | | Host charge and retained backup GB | ___ | ___ | | Subscription seats or warehouse TiB processed | ___ | ___ | Setup = script hours x script rate + server hours x server rate. Monthly = reporting and maintenance labor + hosting + backups + retained subscriptions. Use 52 / 12 weeks per month. Three year total = setup + 36 x monthly. Savings = alternative total minus proposed total. Break even = ceiling(extra setup divided by monthly savings), or never when monthly savings are zero or negative and setup costs more. ### Filled worksheet: the article's three cases All prices below are US dollars before tax, read November 1, 2026 unless stated otherwise. Hours, file sizes and query counts are **our assumptions**, shown as low and high cases. They are different planning scenarios, not performance measurements or confidence intervals. Existing Excel licenses and an already managed laptop are common costs here. New hardware, major cleanup, training, specialist compliance work, incidents and excess transfer are excluded. The May 2025 BLS national median hourly wage for [Management Analysts, SOC 13-1111](https://data.bls.gov/oesprofile/?areas=INDUSTRY,STATE,MSA&major_group=130000&measure=01&occupation=131111&year=2025), is $48.97. For [Data Scientists, SOC 15-2051](https://data.bls.gov/oesprofile/?areas=INDUSTRY,STATE,MSA&major_group=150000&measure=01&occupation=152051&year=2025), it is $57.80. We use the former for routine report preparation and review, and the latter as a specialist script development proxy. These occupations are costing proxies, not required job titles. Using the June 2026 private industry [wages share of 70.0%](https://data.bls.gov/timeseries/CMU2020000000000P), our loaded rates are $48.97 / 0.700 = **$69.96/hour** and $57.80 / 0.700 = **$82.57/hour**. Server work uses the shared [sysadmin wage](https://data.bls.gov/oesprofile/?areas=INDUSTRY,STATE,MSA&major_group=150000&measure=01&occupation=151244&year=2025), $47.66 / 0.700 = **$68.09/hour**. The wage share and sysadmin observation were read October 29. These estimate employee compensation with benefits. A provider's billable rate will be higher; get a quote. An unpaid owner still spends time. [DigitalOcean's NYC3 tiers](https://www.digitalocean.com/pricing/droplets) used here are Small at $24/month for 2 shared vCPU, 4 GiB RAM and 80 GiB disk; Medium at $48 for 4 shared vCPU, 8 GiB and 160 GiB; and Large, General Purpose Regular, at $252 for 8 dedicated vCPU, 32 GiB and 100 GiB. These are monthly caps without an annual commitment discount. They are candidate machines to test, not Polars minimum requirements. [Backblaze B2](https://www.backblaze.com/cloud-storage/pricing) starts at $6.95 per decimal TB per month, paid monthly. Our backup formula is max(0, retained GB minus the first 10 GB) x $0.00695. The free storage allowance comes from the October 29 shared research. Egress up to three times average stored data is included; additional egress is $0.01/GB. Count retained versions, and budget any chargeable requests or source provider transfer separately. Setup = script hours x $82.57 + server setup hours x $68.09. Monthly = hosting + backup + review labor + script maintenance + server administration. Total over 36 months = setup + 36 x monthly. Savings = the alternative total minus the Polars total. Break even is the ceiling of extra setup divided by monthly savings, or **never** when there are no monthly savings and setup costs more. We calculate with the two decimal labor rates and unrounded intermediate amounts, then display whole dollar totals. ### A. A weekly report rebuilt in Excel Our low/high assumptions: rebuilding takes 2/4 hours weekly; the script needs 8/24 specialist setup hours; review afterward takes 0.25/0.5 analyst hours weekly; script maintenance takes 0.5/1.5 specialist hours monthly. Retained backups total 20/100 GB. The VPS option adds 4/12 server setup hours and 1/3 server administration hours monthly. Use 52 / 12 weeks per month. Backup egress stays inside the allowance. | Path | Setup, low / high | Monthly, low / high | 36 months, low / high | Break even, low / high | |---|---:|---:|---:|---| | Existing manual report | $0 / $0 | $606 / $1,213 | $21,828 / $43,655 | Baseline | | Polars on existing laptop | $661 / $1,982 | $117 / $276 | $4,878 / $11,920 | Month 2 / 3 | | Polars on Small VPS | $933 / $2,799 | $209 / $504 | $8,465 / $20,955 | Month 3 / 4 | For the low laptop case, setup is 8 x $82.57 = $660.56. Monthly is (0.25 x 52 / 12 x $69.96) + (0.5 x $82.57) + (20 minus 10) x $0.00695 = $117.1445. Its total is $660.56 + 36 x $117.1445 = $4,878, rounded. Laptop savings over three years are $16,950/$31,735; VPS savings are $13,362/$22,700. These are the value of released staff capacity, not a promise that payroll falls. If nobody uses the saved time, or the export keeps breaking, the benefit will be smaller. The laptop wins this estimate; unattended availability is the reason to consider a server. ### B. Polars beside a BI subscription [Power BI Pro](https://www.microsoft.com/en-us/power-platform/products/power-bi/pricing) is $14 per user per month, **paid yearly**. Our low/high assumptions are 3/10 users with an existing dashboard and preparation workflow. Adding Polars takes 8/24 specialist setup hours, 0.5/1.5 maintenance hours monthly, an existing laptop and 20/100 GB of backup storage. This isolates licenses and extra preparation costs; no manual time saving is assumed. | Path | Setup, low / high | Monthly, low / high | 36 months, low / high | |---|---:|---:|---:| | Existing Power BI Pro licenses | $0 / $0 | $42 / $140 | $1,512 / $5,040 | | Same licenses plus Polars preparation | $661 / $1,982 | $83 / $264 | $3,661 / $11,503 | The license formula is seats x $14 x 36. Add script setup and 36 x maintenance and backups for the second row. Polars adds $2,149/$6,463. Break even from license savings is **never**: those licenses remain. Polars feeds a dashboard; it does not supply Power BI's sharing and access controls. Power Query may already prepare the data adequately. Measure preparation time savings separately using Example A before adding another script. ### C. A medium business running repeated warehouse queries [BigQuery on-demand pricing](https://cloud.google.com/bigquery/pricing), Iowa `us-central1`, is $6.25 per TiB processed after the first 1 TiB monthly. This is the on-demand model, not a Standard or Enterprise capacity edition. Active logical storage lists $0.000031507/GiB-hour; the page's full month example is equivalent to $0.023/GiB-month, with the first 10 GiB free. Our assumptions are one active 100 GiB logical table and 30/1,200 noncached full scans monthly, or 1/40 daily in a 30-day planning month. The project's full free allowances remain available. Query cost is max(0, 100 x scan count / 1,024 minus 1) x $6.25, giving $12/$726 monthly rounded. Storage adds (100 minus 10) x $0.023 = $2.07. Actual billing follows bytes processed and storage time; compressed Parquet size is not the logical byte count. For the Medium VPS, assume a 50 GiB Parquet copy leaves enough disk for working files and outputs, and the real aggregation passes a trial within 8 GiB RAM and its time window. Those conditions must be measured. Streaming does not make every large join fit. Both paths allow 16/48 specialist setup hours, 0.5/1.5 monthly maintenance hours and 100 GB of B2 source backups. Polars adds 4/12 sysadmin setup hours and 1/3 monthly administration hours. Common review and source systems are excluded from both; no streaming ingestion or cross-region transfer is assumed. | Path | Setup, low / high | Monthly, low / high | 36 months, low / high | |---|---:|---:|---:| | BigQuery on demand | $1,321 / $3,963 | $56 / $853 | $3,339 / $34,661 | | Polars on Medium VPS | $1,593 / $4,780 | $158 / $377 | $7,281 / $18,343 | Polars costs $3,943 more in the low case, so break even is **never**. At the high scan frequency it saves $16,318 and breaks even in month 2. If testing requires the $252 Large machine, add $204 monthly or $7,344 over three years: totals become $14,625/$25,687, with break even never/month 4. The Large tier has less included disk; these totals assume working files still fit, and extra storage would add cost. Rounded row totals can differ by a dollar from differences calculated before rounding. The warehouse wins the modest query bill and remains preferable for concurrent users, governed access, cross-team SQL and managed operations. First reduce scans with partitions, selected columns and valid cached results. That can remove the high bill without moving anything. This model prices one batch workload, not a replacement for an entire warehouse. ### Where Polars Cloud fits in the bill [Polars Cloud](https://cloud.pola.rs/) publishes an AWS service charge of **$0.05 per vCPU-hour**, [on top of AWS compute costs](https://docs.cloud.pola.rs/polars-cloud/faq/). [Billing begins when instances are ready and ends when they stop](https://docs.cloud.pola.rs/polars-cloud/organization/billing/), so idle ready instances count too. Enterprise pricing is custom. The homepage also lists an On-Prem offer at $0.05/vCPU-hour, including 10,000 CPU hours monthly at no cost, while the FAQ still calls on-premises support a roadmap item. The FAQ appears older; confirm the current terms before selecting that route. Our assumption of 30/480 ready-instance vCPU-hours monthly gives service fees of $1.50/$24 monthly, or $54/$864 over 36 months. That is only the service fee. Add AWS infrastructure, storage, transfer and staff time. Without a measured runtime and an AWS configuration quote, a total or break even against the VPS would be invented. ## Sources and scope Read November 1, 2026: [Polars installation](https://docs.pola.rs/user-guide/installation/), [2.1.0 package metadata](https://pypi.org/pypi/polars/2.1.0/json), [string operations](https://docs.pola.rs/user-guide/expressions/strings/), [CSV reader](https://docs.pola.rs/api/python/stable/reference/api/polars.read_csv.html), [join validation](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.join.html), [Excel writer](https://docs.pola.rs/api/python/stable/reference/api/polars.DataFrame.write_excel.html), [Parquet](https://docs.pola.rs/user-guide/io/parquet/), [SQL](https://docs.pola.rs/user-guide/sql/intro/), [Python environments](https://docs.python.org/3/library/venv.html), [uv environments](https://docs.astral.sh/uv/pip/environments/) and the linked scheduling, storage and pricing sources. Examples use synthetic data and were tested with Polars 2.1.0, Python 3.12.15, XlsxWriter 3.3.1 and fastexcel 0.21.0 for reading back workbooks. Scheduler deployment, cloud access and production data governance still require your own checks. For version changes, use the [1.x to 2.x guide](https://jwatte.com/downloads/polars-upgrade-guide.md). For existing scripts or macros, use the [pandas and Excel migration kit](https://jwatte.com/downloads/pandas-to-polars-migration.md). *This kit is informational, not legal or financial advice. Product names identify the products discussed. No affiliation is implied.* ### Observation times All November 1, 2026 unless stated otherwise. - [Identity and license](https://docs.pola.rs/), 17:55 UTC; [API languages](https://docs.pola.rs/api/reference/), 18:02 UTC. - [Release dates and Python requirements](https://pypi.org/pypi/polars/json), 17:54 UTC; [2.1 metadata](https://pypi.org/pypi/polars/2.1.0/json), 18:05 UTC. - [Upgrade order](https://docs.pola.rs/releases/upgrade/), 18:00 UTC; [2.0 changes](https://docs.pola.rs/releases/upgrade/2/), 17:55 UTC. - [2.0 release](https://github.com/pola-rs/polars/releases/tag/py-2.0.0) and [2.1 release](https://github.com/pola-rs/polars/releases/tag/py-2.1.0), 17:55 UTC; [announcement](https://pola.rs/posts/release-polars-2), 17:56 UTC. - [Installation](https://docs.pola.rs/user-guide/installation/), [lazy execution](https://docs.pola.rs/user-guide/concepts/lazy-api/), [streaming](https://docs.pola.rs/user-guide/concepts/streaming/), [SQL](https://docs.pola.rs/user-guide/sql/intro/), 17:55 UTC. - [CSV API](https://docs.pola.rs/api/python/stable/reference/api/polars.read_csv.html), [join API](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.join.html), [Parquet](https://docs.pola.rs/user-guide/io/parquet/), 17:56 UTC. - [Excel writing](https://docs.pola.rs/api/python/stable/reference/api/polars.DataFrame.write_excel.html), 18:08 UTC. - [Cloud files](https://docs.pola.rs/user-guide/io/cloud-storage/), [database connectors](https://docs.pola.rs/user-guide/io/database/), 17:55 UTC; [GPU status](https://docs.pola.rs/user-guide/gpu-support/), 17:56 UTC. - [Python environments](https://docs.python.org/3/library/venv.html), 17:56 UTC; [uv environments](https://docs.astral.sh/uv/pip/environments/), 17:55 UTC. - [Actions schedules](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows), 17:55 UTC; [Jupyter](https://jupyter.org/install) and [marimo](https://docs.marimo.io/), 17:56 UTC. - [Power Query](https://learn.microsoft.com/en-us/power-query/power-query-what-is-power-query), [Spark](https://spark.apache.org/docs/latest/), 18:01 UTC; [DuckDB](https://duckdb.org/why_duckdb), 17:56 UTC; [pandas migration](https://docs.pola.rs/user-guide/migration/pandas/), 17:55 UTC. - [Management analyst wage](https://data.bls.gov/oesprofile/?areas=INDUSTRY,STATE,MSA&major_group=130000&measure=01&occupation=131111&year=2025), 17:55:25 UTC; [data scientist wage](https://data.bls.gov/oesprofile/?areas=INDUSTRY,STATE,MSA&major_group=150000&measure=01&occupation=152051&year=2025), 17:55:19 UTC. - [Wages share](https://data.bls.gov/timeseries/CMU2020000000000P), October 29 at 21:17 UTC; [sysadmin wage](https://data.bls.gov/oesprofile/?areas=INDUSTRY,STATE,MSA&major_group=150000&measure=01&occupation=151244&year=2025), October 29 at 21:12 UTC. - [DigitalOcean](https://www.digitalocean.com/pricing/droplets), 17:55:51 UTC; [B2 prices](https://www.backblaze.com/cloud-storage/pricing), 17:55:58 UTC, with free allowance from October 29 at 21:10 UTC; [B2 API](https://www.backblaze.com/docs/cloud-storage-s3-compatible-api), 17:56 UTC. - [BigQuery architecture](https://cloud.google.com/bigquery/docs/introduction) and [row access](https://cloud.google.com/bigquery/docs/row-level-security-intro), 18:09 UTC. - [Power BI Pro](https://www.microsoft.com/en-us/power-platform/products/power-bi/pricing), 17:55:40 UTC; [BigQuery](https://cloud.google.com/bigquery/pricing), 17:55:30 UTC. - [Cloud prices](https://cloud.pola.rs/), 17:56:34 UTC; [Cloud FAQ](https://docs.cloud.pola.rs/polars-cloud/faq/) and [billing](https://docs.cloud.pola.rs/polars-cloud/organization/billing/), 17:57 UTC. Prices also supplied from a US browser review on November 1. - [HIPAA applicability](https://www.hhs.gov/hipaa/for-professionals/covered-entities/index.html), read November 1 in a US browser: “The HIPAA Rules apply to covered entities and business associates.” Scheduling references: [Windows](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create) and [systemd](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html), 17:56 UTC; [cron](https://man7.org/linux/man-pages/man5/crontab.5.html) and [flock](https://man7.org/linux/man-pages/man1/flock.1.html), 18:01 UTC. Data handling references: [PCI responsibilities](https://www.pcisecuritystandards.org/merchants/) and [FTC safeguards](https://www.ftc.gov/business-guidance/resources/ftc-safeguards-rule-what-your-business-needs-know), 17:56 UTC. [uv installation](https://docs.astral.sh/uv/getting-started/installation/), 18:01 UTC.