Why SAS, R and Python can report different percentiles for the same data
Quantiles look straightforward on the surface. Ask for the median, the 75th percentile or the 95th percentile and most people expect one clear answer. Yet small differences between software packages often reveal that quantiles are not defined in only one way. When the same data are analysed in SAS, R or Python, the reported percentile can differ, particularly for small samples or for data sets with large gaps between adjacent values.
That difference is not necessarily a bug, and it is not a sign that one platform is wrong. It reflects the fact that sample quantiles are estimates of population quantiles, and statisticians have proposed several valid ways to construct those estimates. For everyday work with large samples, the distinction often fades into the background because the values tend to be close. For smaller samples, the choice of definition can matter enough to alter a reported result, a chart or a downstream calculation.
The Problem With the Empirical CDF
A useful starting point is understanding why multiple definitions exist at all. A sample quantile is an estimate of an unknown population quantile. Many approaches base that estimate on the empirical cumulative distribution function (ECDF), which approximates the cumulative distribution function (CDF) for the population. As Rick Wicklin explains in his 22nd May 2017 article on The DO Loop, the ECDF is a step function with a jump discontinuity at each unique data value. For that reason, the inverse ECDF does not exist and quantiles are not uniquely defined, which is precisely why different conventions have developed.
In high school, most people learn that when a sorted sample has an even number of observations, the median is the average of the two middle values. The default quantile definition in SAS extends that familiar rule to other quantiles. If the sample size is N and the q-th quantile is requested, then when Nq is an integer, the result is the data value x[Nq]. When Nq is not an integer, the result is the average of the two adjacent data values x[j] and x[j+1], where j = floor(Nq). Averaging is not the only choice available when Nq is not an integer, and that is where the definitions diverge.
The Hyndman and Fan Taxonomy
According to Hyndman and Fan ("Sample Quantiles in Statistical Packages," TAS, 1996), there are nine definitions of sample quantiles that commonly appear in statistical software packages. Three of those definitions are based on rounding and six are based on linear interpolation. All nine result in valid estimates.
As Wicklin describes in his 24th May 2017 article comparing all nine definitions, the nine methods share a common general structure. For a sample of N sorted observations and a target probability p, the estimate uses two adjacent data values x[j] and x[j+1]. A fractional quantity determines an interpolation parameter λ, and each definition has a parameter m that governs how interpolation between adjacent data points is handled. In general terms, the estimate takes the form q = (1 − λ)x[j] + λx[j+1], where λ and j depend on the values of p, N and the method-specific parameter m. The practical consideration at the extremes is that when p is very small or very close to 1, most definitions fall back to returning x[1] or x[N] respectively.
Default Methods Across Platforms
It is a misnomer to refer to one approach as "the SAS method" and another as "the R method." As Wicklin notes in his 26th July 2021 article comparing SAS, R and Python defaults, SAS supports five different quantile definitions through the PCTLDEF= option in PROC UNIVARIATE or the QNTLDEF= option in other procedures, and all nine can be computed via SAS/IML. R likewise supports all nine through the type parameter in its quantile function. The confusion arises not from limited capability, but from the defaults that most users accept without much thought.
By default, SAS uses Hyndman and Fan's Type 2 method (QNTLDEF=5 in SAS procedure syntax). R uses Type 7 by default, and that same Type 7 method is also the default in Julia and in the Python packages SciPy and NumPy. A comparison between SAS and Python therefore often becomes the same comparison as between SAS and R.
A Worked Example
The contrast between Type 2 and Type 7 is especially clear on a small data set. Wicklin uses the sample {0, 1, 1, 1, 2, 2, 2, 4, 5, 8} throughout both his 2017 and 2021 articles: ten observations, six unique values, and a particularly large gap between the two highest values, 5 and 8. That gap is deliberately chosen because the differences between quantile definitions are most visible when the sample is small and when adjacent ordered values are far apart.
The Type 2 method (SAS default) uses the ECDF to estimate population quantiles, so a quantile is always an observed data value or the average of two adjacent data values. The Type 7 method (R default) uses a piecewise-linear estimate of the CDF. Because the inverse of that piecewise-linear estimate is continuous, a small change in the probability level produces a small change in the estimated quantile, a property that is absent from the ECDF-based methods.
Where the Methods Agree and Where They Part Company
For the 0.5 quantile (the median), both methods return 2. A horizontal line at 0.5 crosses both CDF estimates at the same point, so there is no disagreement. This is one reason the issue can be easy to miss: some commonly reported percentiles coincide across definitions.
The 0.75 quantile tells a different story. Under Type 2, a horizontal line at 0.75 crosses the empirical CDF at 4, which is a data value. Under Type 7, the estimate is 3.5, which is neither a data value nor the average of adjacent values; it emerges from the piecewise-linear interpolation rule. The 0.95 quantile shows the sharpest divergence: Type 2 returns 8 (the maximum data value), while Type 7 returns 6.65, a value between the two largest observations.
Those differences are not errors. They are consequences of the assumptions built into each estimator. The default in SAS always returns a data value or the average of adjacent data values, whereas the default in R can return any value in the range of the data.
The Five Definitions Available in SAS Procedures
For users who stay within base SAS procedures, that same 22nd May 2017 article sets out the five available definitions clearly. QNTLDEF=1 and QNTLDEF=4 are piecewise-linear interpolation methods, whilst QNTLDEF=2, QNTLDEF=3 and QNTLDEF=5 are discrete rounding methods. The default is QNTLDEF=5. For the discrete definitions, SAS returns either a data value or the average of adjacent data values; the interpolation methods can return any value between observed data values.
The differences between the definitions are most apparent when there are large gaps between adjacent data values. Using the same ten-point data set, for the 0.45 quantile, different definitions return 1, 1.5, 1.95 or 2. For the 0.901 quantile, the round-down method (QNTLDEF=2) gives 5, the round-up method (QNTLDEF=3) gives 8, the backward interpolation method (QNTLDEF=1) gives 5.03 and the forward interpolation method (QNTLDEF=4) gives 7.733. These are not trivial discrepancies on a small sample.
The Four Remaining Definitions and the General Formula
The 24th May 2017 comparison article goes further, showing how SAS/IML can be used to compute the four Hyndman and Fan definitions that are not natively supported in SAS procedures. Each of the nine methods is an instance of the same general formula involving the parameter m. The four non-native methods each require their own specific value (or expression) for m, plus a small boundary value c that governs the behaviour at the extreme ends of the probability scale.
Wicklin also overlays the default methods for SAS (Type 2) and R (Type 7) graphically on the ten-point data set, showing that the SAS default produces a discrete step pattern whilst the R default traces a smoother piecewise-linear curve. He then repeats the comparison on a sample of 100 observations from a uniform distribution and finds that the two methods are almost indistinguishable at that scale, illustrating why many analysts work comfortably with defaults most of the time.
A SAS/IML Function to Match R's Default
For analysts who need cross-platform consistency, that same 26th July 2021 article provides a simplified SAS/IML function that reproduces the Type 7 default from R, Julia, SciPy and NumPy. The function converts the input to a column vector, handles missing values and the degenerate case of a single observation, then sorts the data and applies the Type 7 rule. The index into the sorted data would be j = floor(N*p + m) with m = 1 − p, the interpolation fraction is g = N*p + m − j, and the estimate is (1 − g)x[j] + gx[j+1] for all p < 1, with x[N] returned when p = 1. This gives SAS users a practical route to reproduce the default quantiles from other platforms without switching software.
If SAS/IML is unavailable, Wicklin suggests using PCTLDEF=1 in PROC UNIVARIATE (or QNTLDEF=1 in PROC MEANS) as the next best option. This produces the Type 4 method, which is not the same as Type 7 but does use interpolation rather than a purely discrete rule, so it avoids the jumpy behaviour of the ECDF-based defaults.
A Wider Point About Conventions in Statistical Software
The comments on the 2021 article make clear that quantiles are not an isolated example. Conventions differ across platforms in ARIMA sign conventions, whether likelihood constants are included in reported values, the definition of the multivariate autocovariance function and the sign convention and constant term used in discrete Fourier transforms. Quantiles are simply a particularly visible instance of a broader pattern where results can differ even when each platform is behaving correctly.
One question from the same comment thread is also worth noting: SQL's percent_rank formula, defined as (rank − 1) / (total_rows − 1), does not estimate a quantile. As Wicklin clarifies in his reply, it estimates the empirical distribution function for observed data values. Both concepts involve percentiles and rankings, but they address different problems. One maps values to cumulative proportions; the other maps cumulative probabilities to estimated values.
Does the Definition of a Sample Quantile Actually Matter?
The answer from all three articles is balanced. Yes, it matters in principle, and it is noticeably important for small samples, in extreme tails and wherever there are wide gaps in the ordered data. No, it often matters very little for larger samples (say, 100 or more observations), where the nine methods tend to produce results that are nearly indistinguishable. Wicklin's 100-observation comparison showed that the Type 2 and Type 7 estimates were so close that one set of points sat almost directly on top of the other.
That is why, as Wicklin notes, most analysts simply accept the default method of whichever software they are using. Even so, there are contexts where the definition should be stated explicitly. Regulatory work, reproducible research, published analyses and any cross-software validation all benefit from naming the method in use. Without that detail, two analysts can work correctly with the same data and still arrive at different percentile values.
Matching Quantile Definitions Across SAS, R and Python
The practical conclusion is clear. SAS defaults to Hyndman and Fan Type 2 (QNTLDEF=5), while R, Julia, SciPy and NumPy default to Type 7. SAS procedures natively support five of the nine definitions, and SAS/IML can be used to compute all nine, including a simplified function for the R default. For large data sets, the differences are typically negligible. For small data sets, particularly those with unevenly spaced observations, they can be large enough to change the story the numbers appear to tell. The solution is not to favour any particular platform, but to be explicit about the method wherever precision matters.
WordPress Starter Themes: From bare foundations to modern workflows
Starter themes have long occupied an important place in WordPress development. They sit between a completely blank project and a fully styled off-the-shelf theme, offering enough structure to speed up work without dictating how the finished site must look. For agencies, freelancers and in-house teams, that balance can save considerable time, allowing developers to begin with a lean codebase and concentrate on the parts that make each site distinct.
That broad appeal helps explain why starter themes continue to evolve in different directions. Some remain deliberately minimal and close to WordPress core conventions, while others embrace modern tooling such as Composer, Vite, Tailwind CSS and component-based templating. Alongside these are starter themes intended for visual builders and users who want a gentler route into customisation. Taken together, they demonstrate that there is no single definition of a WordPress starter theme, with the common thread being that each provides a starting point rather than a finished product.
wd_s: A Generator-Driven Approach
wd_s is a generator-driven starter theme from WebDevStudios. The wd_s generator makes the setup process more tailored by asking for project details: name, URL, description, namespace, text domain, author, author URL, author email and development URL. Once those details are entered, the script performs a find-and-replace and delivers a ZIP file ready to extract into wp-content/themes. The process is straightforward, though it also reflects a more structured approach to project setup than many starter themes provide.
The generator highlights details that matter in real-world theme development, but are sometimes overlooked when beginning from a generic scaffold. Name-spacing, text domains and project metadata all play a part in maintainability and localisation, and wd_s brings those choices to the fore from the very beginning. The example values shown by the generator, such as "Acme Inc." for the name field and a namespace using underscores, are illustrative rather than prescriptive. What stands out more than any one field is the intention to reduce repetitive manual setup and encourage consistency from the very start of a project.
Sage: A Modern Development Workflow
Roots Sage represents a significant shift towards a modern development workflow. Sage is a Tailwind CSS WordPress starter theme with Laravel Blade templating, currently at version 11.1.0 and with over 13,191 GitHub stars at the time of writing. Setup uses Composer and NPM rather than a simple ZIP download, and a typical installation begins in wp-content/themes with composer create-project roots/sage my-theme, followed by npm install and npm run build.
That workflow signals the audience Sage is aimed at. Rather than merely wrapping WordPress templates in a minimal theme shell, Sage introduces tooling and conventions familiar to developers who work with Laravel and modern front-end stacks. The Vite build process generates files including a manifest, compiled CSS and JavaScript and a theme.json, completing the whole build in a matter of seconds and demonstrating that WordPress development can be integrated into contemporary asset pipelines without giving up compatibility with the CMS.
Blade Templating and Component-Driven Design
Blade templating is central to Sage's proposition. The base layout in resources/views/layouts/app.blade.php shows a clean separation of structure and content using directives such as @include, @yield and @hasSection. Header and footer hooks still call familiar WordPress functions including wp_head, wp_body_open and wp_footer, but the surrounding syntax is closer to Laravel than traditional PHP-heavy WordPress templates. This gives developers access to template inheritance, reusable components and directives, making larger codebases considerably easier to organise.
Reusable components illustrate this style compactly. Properties define type and message, a PHP match expression selects classes based on alert type and the component merges those classes into its final markup. The result is not merely an isolated snippet but a demonstration of how Sage encourages component-driven design, reducing repetition and making presentation logic easier to follow, particularly in projects with many shared interface elements.
Tailwind CSS and the Block Editor
Sage places strong emphasis on integrating Tailwind CSS with the WordPress block editor. It automatically generates theme.json from the Tailwind configuration, making colours, font families and sizes immediately available in the block editor with zero additional configuration. The sample app.css imports Tailwind and points at views and app files as content sources, while the generated theme.json includes settings for layout, background, colour palettes, spacing and typography. The palette includes eleven shades of grey along with black and white, and the typography settings mirror Tailwind's familiar scale from xs through 9xl.
This addresses a long-standing friction point on WordPress theming: keeping front-end design systems in sync with the editing experience. In older workflows, editor styles and front-end styles often drifted apart, creating extra maintenance work and inconsistency for content editors. Sage's approach narrows that gap by deriving editor settings directly from the same Tailwind configuration used for the front end, with theme.json generated during the build process rather than maintained by hand.
Theme Structure and the Roots Ecosystem
The theme structure for Sage reinforces its emphasis on organisation. The app directory contains providers, view composers, filters.php and setup.php, while resources holds CSS, JavaScript and Blade views grouped into components, layouts, partials and sections. Public assets, composer.json, package.json, theme.json and vite.config.js complete the structure, paired with PSR-4 autoloading, service providers and Acorn, which brings Laravel-style patterns into WordPress. The Vite configuration includes Tailwind, the Laravel Vite plugin and Roots plugins for WordPress support and theme.json generation, plus aliases for scripts, styles, fonts and images.
Another notable feature is hot module replacement in the WordPress block editor, with style changes updating instantly without page refreshes. Sage sits within the broader Roots ecosystem, which also includes Bedrock (a WordPress boilerplate for Composer and Git-based projects), Trellis (a server provisioning and deployment tool), Acorn, Radicle (which bundles the entire Roots stack into a single starting point) and WP Packages (a Composer repository for WordPress plugins and themes). Testimonials on the Roots website emphasise that many developers regard this ecosystem as a route to a more structured and modern WordPress experience, with Sage having been actively maintained for over a decade.
Visual Composer Starter Theme: A Builder-Friendly Option
Not every starter theme is aimed at developers working with command-line tooling and component-based templates. The Visual Composer Starter Theme occupies a different place in the landscape, described as a free bundle of a lightweight theme and a powerful WordPress page builder. It is aimed at building blogs, WooCommerce stores, business sites and personal websites, and the language surrounding it stresses ease of use, intuitive theme options and layout customisation tools, presented as a free resource intended to support the WordPress community.
Its feature set reflects that broader audience. The theme is easy to customise through the WordPress customiser, SEO-friendly and responsive by default, covering use cases such as personal blogs, landing pages, business sites, portfolios, startups and online stores. WooCommerce compatibility receives particular emphasis, with support for adjusting design preferences via the customiser and building online shops at no cost. Hero and featured images, unlimited colour options, page-level design controls and a choice between regular and mobile sandwich-style menus are all included.
There is also a strong focus on compatibility. The theme is fully translation-ready and compatible with WPML, qTranslate and Polylang, while support for Advanced Custom Fields and Toolset for custom post type development is highlighted. It is also presented as ready to combine with the Visual Composer website builder and is developed openly on GitHub, where anyone can contribute. This is less about offering an unvarnished code scaffold and more about giving users a flexible visual base with a broad range of built-in options, though it remains part of the starter theme conversation because it is designed to be extended rather than merely installed and left untouched.
Bones: Speed, Control and Pragmatism
Bones, designed and developed by Eddie Machado, returns more closely to the classic developer-oriented concept while retaining a distinctive voice. It is described as an HTML5, mobile-first starter theme for rapid WordPress development, and it makes clear that it is not a framework. Frameworks can introduce their own conventions and complexity, whereas Bones is designed to be as bare and minimalistic as possible, intended to be used on a per-project basis with no child themes.
The mobile-first emphasis is one of Bones' defining characteristics. Its Sass setup serves minimal resources to smaller screens before scaling up for larger viewports, an approach tied to performance as well as responsiveness, and Bones includes extensive comments and examples to help developers get started with Sass. It also provides a well-documented example for custom post types and functions to customise the WordPress admin area for clients, though these are entirely optional and can be removed if not needed. The project is released under the WTFPL, one of the most permissive licences available, and takes pride in removing unnecessary elements from the WordPress header to keep output clean and lightweight. The philosophy is to keep what is useful and discard the rest, building from a solid and speedy foundation.
Selecting a Starter Theme to Match Your Workflow
When viewed together, these themes reveal how varied the starter theme category has become. A Speckyboy roundup of top starter and bare-bones themes for WordPress development in 2026 (last updated on the 8th of March 2026) places Sage alongside newer and more editor-focused options including Blockbase, GeneratePress, Air, WDS BT, Byvex and Flynt. The roundup notes that every website serves different goals and that WordPress is flexible enough to support them all, but also makes clear that starting each project from scratch leads to repeated work, with starter themes offering a way to avoid that repetition while preserving freedom over design and functionality.
The same roundup provides a useful framework for evaluating starter themes. Ongoing maintenance matters because themes need to keep pace with WordPress and surrounding technologies, and themes that have not been updated in years should be avoided. The distinction between classic and block themes is important, since developers need a starting point that aligns with their preferred editing model. Features that genuinely speed development, whether block patterns, a comprehensive settings panel or development tools, can make a significant difference over time. A starter theme should also stay out of the way rather than burden projects with an opinionated design direction, and compatibility with a preferred editor or page builder remains central to choosing well.
Whether the preference is for the generator-based setup of wd_s, the modern tooling of Sage, the builder-friendly versatility of Visual Composer Starter Theme or the stripped-back classic structure of Bones, each represents a different answer to the same challenge. Developers and site builders often need a head start rather than a finished design, and a good starter theme provides exactly that, while leaving enough room for the final result to become something entirely its own.
Modernising SAS: The 4GL Apps and SASjs Ecosystem
Custom interfaces to the world's most powerful analytics platform are no longer a niche concern. In many organisations, SAS remains central to reporting, modelling and operational decision-making, yet the way users interact with that capability can vary widely. Some teams still rely on desktop applications, batch processes, shared drives and manual interventions, while others are moving towards web-based interfaces, stronger governance and a more modern development workflow. The material at sasapps.io points to an ecosystem built around precisely that transition, blending long-standing SAS expertise with open-source tooling and documented delivery methods.
The Company Behind the Ecosystem
At the centre of this transition is 4GL Apps. The company's positioning is straightforward: help organisations leverage their SAS investment through services, solutions and products that fit specific needs. Rather than replacing SAS, the aim is to extend it with custom interfaces and delivery approaches that are maintainable, transparent and based on standard frameworks. An emphasis on documentation appears throughout the site, suggesting that projects are intended either for handover to internal teams or for ongoing support under clearly defined packages.
That proposition matters because many SAS environments have grown over years, sometimes decades. In such settings, technical capability is rarely the issue. The challenge is more often how to expose that capability in ways that are usable, secure and sustainable. A powerful analytics platform can still be hampered by awkward user journeys, brittle desktop tooling or resource-heavy support arrangements, and the 4GL Apps model tries to address those practical concerns without discarding existing SAS infrastructure.
Services
The service offering gives a useful sense of how this approach is organised. One strand is SAS App Delivery, framed not merely as building applications, but also as building tools that make SAS app development faster. That detail points to an emphasis on repeatability rather than one-off implementation. Another strand is SAS App Support, aimed at organisations with existing SAS-powered applications but insufficient internal resource to keep them running. Fixed-price plans are offered to keep those interfaces active, which implies an attempt to make operational costs more predictable. A third service area is SASjs Enhancement, where new features can be added to SASjs at a discounted rate to support particular use cases.
Solutions
These services sit alongside a broader set of solutions. One is the creation of SAS-powered HTML5 applications, described as bespoke builds tailored to specific workflow and reporting requirements, using fully open-source tools, standard frameworks and full documentation. Clients are given a practical choice: maintain the application in-house or use a transparent support package. Another solution addresses end-user computing risk through data capture and control. Here, the approach enables business users to self-load VBA-driven Excel reporting tools into a preferred database while applying data quality checks at source, a four-eyes (or more) approval step at each stage and full audit traceability back to the original EUC artefact. A further solution is the modernisation of legacy AF/SCL desktop applications, with direct migration to SAS 9 or Viya in order to improve user experience, security and scalability while moving to a modern SAS stack supported by open-source technology.
That last area reveals a theme running through the whole ecosystem: modernisation does not necessarily mean abandoning what exists. In many SAS estates, AF/SCL applications remain deeply embedded in business processes, and replacing them outright can be costly and risky, especially when they encode years of operational logic. A migration path that preserves business function while improving maintainability and interface design will naturally appeal to teams that need progress without disruption.
Products
The product range fills out the picture further. Data Controller for SAS enables business users to make controlled changes to data in SAS. The SASjs Framework is a collection of open-source tools to accelerate SAS DevOps and the development of SAS-powered web applications. There is also an AF/SCL Kit, migration tooling for the rapid modernisation of monolithic AF/SCL applications. Together, these products form a stack covering interface delivery, governed data change and development workflow, and they suggest that the company's work is not limited to consultancy but includes reusable software assets with their own documentation and source code.
Data Controller: Governance and Audit
Data Controller receives the richest functional description in the ecosystem's documentation. It is intended for business owners in regulatory reporting environments and, more broadly, for any enterprise that needs to perform manual data uploads with validation, approval, security and control. The rationale is rooted in familiar SAS working practices. Users may place files on network drives for batch loading, update data directly using SAS code, open a dataset in Enterprise Guide and change a value, or ask a database administrator to run a script update. According to the product's own documentation, those approaches are less than ideal: every new piece of data may require a new programme, end users may need to have `modify` access to sensitive data locations, datasets can become locked, and change requests can slow the process.
Data Controller is presented as a response to those weaknesses. The goal is described as focusing on great user experience and auditor satisfaction, while saving years of development and testing compared with a custom-built alternative. It is a SAS-powered web application with real-time capabilities, where intraday concurrent updates are managed using a lock table and queuing mechanism. Updates are aborted if another user has changed the table since the approval difference was generated, which helps preserve consistency in multi-user environments. Authentication and authorisation rely on the existing SASLogon framework, and end users do not require direct access to the target tables.
The governance model is equally central. All data changes require one or more approvals before a table is updated, and the approver sees only the changes that will be applied to the target, including new, deleted and changed rows. The system supports loading tables of different types through SAS libname engines, with support for retained keys, SCD2 loads, bitemporal data and composite primary keys. Full audit history is a prominent feature: users can track every change to data, including who made it, when it was made, why it was made and what the actual change was, all accessible through a History page.
A particularly notable feature is that onboarding new tables requires zero code. Adding a table is a matter of configuration performed within the tool itself, without the need to define column types or lengths manually, as these are determined dynamically at runtime. Workflow extensibility is built in through configurable hook scripts that execute before and after each action, with examples such as running a data quality check after uploading a mapping table or running a model after changing a parameter. Taken together, those features position Data Controller less as a narrow upload utility and more as a governed operational layer for business-managed data change.
The application was designed to work on multiple devices and different screen types, combined with SAS scalability and security to provide flexibility and location independence when managing data. This suggests it is intended for practical day-to-day use by business teams rather than solely by technical specialists at a desktop workstation.
SASjs: DevOps for SAS
Underpinning much of the ecosystem is SASjs, described on its GitHub organisation page as "DevOps for SAS." It is designed to accelerate the development and deployment of solutions on all flavours of SAS, including Viya, EBI and Base. Everything in SASjs is MIT open-source and free for commercial use. The framework also explicitly underpins Data Controller for SAS, which connects the product and framework strands of the wider ecosystem. The GitHub organisation page notes that the SASjs project and its repositories are not affiliated with SAS Institute.
The resources page at sasjs.io lists the key GitHub repositories: the Macro Core library, the SASjs adapter for bidirectional SAS and JavaScript communication, the SASjs CLI, a minimal seed application and seed applications for React and Angular. Documentation sites cover the adapter, CLI, Macro Core library, SASjs Server and Data Controller. Useful external links from the same resources page include guides to building and deploying web applications with the SASjs CLI, scaffolding SAS projects with NPM and SASjs, extending Angular web applications on Viya and building a vanilla JavaScript application on SAS 9 or Viya. There is also mention of a Viya log parser, training resources, guides, FAQs and a glossary, pointing to an effort to support both implementation and adoption.
The SASjs CLI
The command-line tooling, documented at cli.sasjs.io, gives a clearer view of how SASjs approaches DevOps. The CLI is described as a Swiss-army knife with a flexible set of options and utilities for DevOps on SAS Viya, SAS 9 EBI and SASjs Server. Its core functions include creating a SAS Git repository in an opinionated way, compiling each service with all dependent macros, macro variables and pre- or post-code, building the master SAS deployment, deploying through local scripts and remote SAS programmes, running unit tests with coverage and generating a Doxygen documentation site with data lineage, homepage and project logo from the configuration file. There is also a feature for deploying a frontend as a streaming application, bypassing the need to access the SAS web server directly.
The full command set covers the project lifecycle. The CLI can add and authenticate targets, compile and build projects, deploy them to a SAS server location, generate documentation and manage contexts, folders and files. It can execute jobs, run arbitrary SAS code from the terminal, deploy a service pack and generate a snippets file for macro autocompletion in VS Code. It can also lint SAS code to identify common problems and run unit tests while collecting results in JSON or CSV format, together with logs. In effect, this brings SAS development considerably closer to the workflows commonly seen in mainstream software engineering, which may be especially valuable in organisations trying to standardise delivery practices across mixed technology estates.
Presentations and the Wider SAS Community
The slides.sasjs.io collection adds another dimension by showing that these ideas have been presented in conference and user group settings. Available decks cover DevOps for MSUG, SUGG and WUSS, SASjs for application development, SASjs Server, AF and AF/SCL modernisation, SASjs for PHUSE, testing and a legacy SAS apps presentation for FANS in January 2023. While slide decks alone do not prove adoption or outcomes, they do show a sustained effort to communicate methods and patterns to the broader SAS community, consistent with the open documentation and MIT licensing found throughout the ecosystem.
Building a Modern Layer Around an Established Platform
The most useful way to understand this ecosystem is not as a single product but as a layered approach. At one level, there are services for building and supporting applications. At another, there are packaged tools such as Data Controller and the AF/SCL Kit. Underneath both sits SASjs, providing open-source components and delivery practices intended to make SAS development more structured and scalable. The combination of bespoke SAS-powered HTML5 applications, governed data update tooling, AF/SCL migration support and open-source DevOps utilities points to a coherent effort to modernise how SAS is delivered and used, without severing ties to established platforms. SAS remains the analytical engine, but the interfaces, workflows and operational controls around it are updated to reflect current expectations in web application design, governance and DevOps practice.
Adding a dropdown calendar to the macOS desktop with Itsycal
In Linux Mint, there is a dropdown calendar that can be used for some advance planning. On Windows, there is a pop-up one on the taskbar that is as useful. Neither of these possibilities is there on a default macOS desktop, and I missed the functionality. Thus, a search began.
That ended with my finding Itsycal, which does exactly what I need. Handily, it also integrates with the macOS Calendar app, though I use other places for my appointments. In some ways, that is more than I need. The dropdown pane with the ability to go back and forth through time suffices for me.
While it would be ideal if I could go year by year as well as month by month, which is the case on Linux Mint, I can manage with just the latter. Anything is better than having nothing at all. Sometimes, using more than one operating system broadens a mind.
Switching from uBlock Origin to AdGuard and Stylus
A while back, uBlock Origin broke this website when I visited it. There was a long AI conversation that left me with the impression that the mix of macOS, Firefox and WordPress presented an edge case that could not be resolved. Thus, I went looking for alternatives because I may not be able to convince else to look into it, especially when the issue could be so niche.
One thing the uBlock Origin makes very easy is the custom blocking of web page elements, so that was one thing that I needed to replace. A partial solution comes in the form of the Stylus extension. Though the CSS rules may need to be defined manually after interrogating a web page structure, the same effects came be achieved. In truth, it is not as slick as using a GUI element selector, but I have learned to get past that.
For automatic ad blocking, I have turned to AdGuard AdBlocker. Thus far, it is doing what I need it to do. One thing to note is that does nothing to stop your registering in website visitor analytics, not that it bothers me at all. That was something that uBlock Origin does out of the box, while my new ad blocker sticks more narrowly to its chosen task, and that suffices for now.
In summary, I have altered my tooling for controlling what websites show me. It is all too easy for otherwise solid tools to register false positives and cause other obstructions. That is why I find myself swapping between them every so often; after all, website code can be far too variable.
Maybe it highlights how challenging it is to make ad blocking and other similar software when your test cases cannot be as extensive as they need to be. Add in something of an arms race between advertisers and ad blockers for the ante to be upped even more. It does not help when we want the things free of charge too.
Finding a better way to uninstall Mac applications
If you were to consult an AI about uninstalling software under macOS, you would be given a list of commands to run in the Terminal. That feels far less slick than either Linux or Windows. Thus, I set to looking for a cleaner solution. It came in the form of AppCleaner from FreeMacSoft.
This finds the files to remove once you have supplied the name of the app that you wish to uninstall. Once you have reviewed those, you can set it to remove them to the recycling bin, after which they can be expunged from there. Handily, this automates the manual graft that otherwise would be needed.
It amazes me that such an operation is not handled within macOS itself, instead of leaving it to the software providers themselves, or third-party tools like this one. Otherwise, a Mac could get very messy, though Homebrew offers ways of managing software installations for certain cases. Surprisingly, the situation is more free-form than on iOS, too.
Locking your computer screen faster using keyboard shortcuts
When you are doing paid work on a computer, locking one's screen is a healthy practice for ensuring privacy and confidentiality while you are away from your desk for a short while. For years, I have been doing this on Windows using the WIN (Windows key) + L keyboard combination. It is possible on a Mac too, albeit using a different set of keys: CTRL (Control) + CMD (Command) + Q. While the Lock Screen item on the Apple menu will accomplish the same result, a simple keyboard shortcut works much, much faster. On Linux, things are a lot more varied with different desktop environments working in their own way, even making terminal commands a way to go if you can use a heavily abbreviated alias.
Streamlining text case conversion across multiple apps with a reusable macOS terminal command
Changing text from mixed case to lower case is something that I often do. Much of the time until recently, this has been accomplished manually, but I started to wonder if a quicker way could be found. Thus, here is one that I use when working in macOS. It involves using a command that works in the terminal, and I have added a short alias for it to my .zshrc file. Here is the full pipeline:
pbpaste | tr '[:upper:]' '[:lower:]' | pbcopy
In the above, pbpaste reads from the paste buffer (or clipboard) while pbcopy writes the final output to the clipboard, replacing what was there before. In between those, tr '[:upper:]' '[:lower:]' changes any lower case letters to lower case ones.
With that, the process becomes this: copy the text into the paste buffer, run the command, paste output where it is wanted. While there may be a few steps, it is quicker than doing everything manually or opening another app to do the job. This suffices for now, and I. may get to look at something similar for Linux in time.
How to persist R packages across remote Windows server sessions
Recently, I was using R to automate some code changes that needed implementation when porting code from a vendor to client systems. While I was doing so, I noticed that packages needed to be reinstalled every time that I logged into their system. This was because they were going into a temporary area by default. The solution was to define another location where the packages could be persisted.
That meant creating a .Renviron file, with Windows Explorer making that manoeuvre an awkward one that could not be completed. Using PowerShell was the solution for this. There, I could use the following command to do what I needed:
New-Item -ItemType File "$env:USERPROFILE\Documents\.Renviron" -Force
That gave me an empty .Renviron file, to which I could add the following text for where the packages should be kept (the path may differ on your system):
R_LIBS_USER=C:/R/packages
Here, the paths are only examples and do not always represent what the real ones were, and that is by design for reasons of client confidentiality. Restarting RStudio to give me a fresh R session meant that I now could install packages using commands like this one:
install.packages("tidyverse")
Version constraints meant for compilation from source in my case, making for a long wait time for completion. Once that was done, though, there was no need for a repeat operation.
One final remark is that file creation and population could be done in the same command in PowerShell:
'R_LIBS_USER=C:/R/packages' | Out-File -Encoding ascii "$env:USERPROFILE\Documents\.Renviron"
It places the text into a new file or completely overwrites an existing, meaning that you really want to do this once should you decide to add any more setting details to .Renviron later on.
Not so fast: When tasks using AI may take more time and attention than you expect
If you believed all the hype that surrounds AI, you might believe that all of us would out of work before we knew it. The truth is that the new technology is not that miraculous, especially when based on some experiences that I have been having. Firstly, there are deficiencies and then there will be new things that need doing as well as becoming possible for the first time.
PowerShell Scripting
One pertained to spinning up PowerShell scripts for doing code reviews of SAS programs submitted by a vendor to a client of mine. While all worked well for simple cases, I found that more complex tasks like finding the datasets using in code and comparing them against what is listed in the program headers became too complicated and probably needed a week of my time to get things in order, which was the amount of time that I did not have.
Picking out macro calls from code and comparing them against lists in the headers was more successful because the code situations were less variable. Other tasks were really handy, though, even if I would benefit from AI teaching me how to write PowerShell scripts by myself. That would give me more scope to critique the code that was being produced. Starting simple and progressing one step at a time would ensure sounder embedding of PowerShell commands in my memory.
Article Writing
It is all too tempting to get AI to write articles on subjects of your choosing for website content production. That which sounds like a labour-saving way to go can command a higher amount of attention than some realise. Sometimes, writing it all by yourself might be a better approach, one that I am using for this piece.
My workflow often involves these steps when AI is involved: assembly of the source material, conversion of source material into an article by one AI, fact checking of the same text by another AI and restructuring by that second AI with added links for those wanting to find out more. While human content production is reduced, the need for human oversight, along with fact and link checking, means that time is used in other ways.
In short, it is best not to rush this, as I found when assembling two articles on Canadian rail travel. You also need to watch how much content is being processed because that can both overwhelm human bandwidth and undermine human engagement. This is more than proofreading of what is produced; you need to know something about a given subject yourself too.
Image Production
While AI can do well with producing some images, there are ones where it will struggle because of lack of training. An example is when I asked for an image with cyclists placing bicycles on a bus before boarding it. None of the generated images worked, meaning that a trip to a stock library was in order.
While some can specify everything in a prompt at one sitting, I work more iteratively, which probably adds to any task, especially with image generation. It proves that still is a place for stock libraries and having your own personal library as well. We need to remain as orchestrators in all of this, and lack of personal talent can remain a limitation.
System Administration
While this may not be something that I do professionally, my keeping an eye on the worlds of DevOps and DevSecOps means that I am seeing that the presence of AI is adding work of its own. This has no sign of lessening, proving that work is changing dramatically instead of reducing, especially you bring Agentic AI into the equation.
It feels much like the advent of personal computing and that produced a similar seismic shift in the workplace in more innocent times. This time around, nefarious actors are misusing AI, a not unexpected if ominous trend, adding to the security woes that have beset computing for a few decades now.
A Human in the Loop?
At a recent conference, much was being made of keeping humanity in the loop when it came to using AI. There is a catch, though: how do we have engaged humans in the loop? After all, creating computer code allows one to get into flow and remain engaged, possibly overriding any feelings of fatigue. This is what needs replicating, hardly an experience reported with automation in other professions.
The use of AI is a developing field, bringing new challenges as well as solving old problems. That also means upskilling on a grand scale, something happened over time with personal and business computing. While it looks as if the process could be faster this time around, it is too early to know enough about where this revolution is going to take us. That may be enough to keep us engaged.