# What is Chemometrics? A brief story (/introduction-to-chemometrics/what-is-chemometrics) In the analytical laboratories of the 1970s, remarkable changes were taking place. Instruments became more sensitive and computers grew more powerful, creating huge amounts of data that were unimaginable just decades earlier. For example, a single infrared spectrum could contain thousands of data points, and a mass spectrometer might produce detailed fragmentation patterns showing how a molecule breaks apart. Nuclear magnetic resonance experiments yielded multidimensional datasets full of variables that scientists could not easily interpret. Despite this richness, chemists faced a fundamental mismatch. Traditional analytical chemistry was built on **selectivity**: you design an experiment to measure one thing at a time, eliminating interferences and isolating the signal you're interested in. But modern instruments were inherently **multivariate**. When you run an infrared spectrum, you don't get a single measurement; you get thousands of measurements at different wavelengths, each influenced by multiple chemical components in complex, overlapping ways. A chemist could try to maintain the traditional one-at-a-time approach by manually identifying and interpreting characteristic absorption bands individually (a carbonyl stretch here, an O-H stretch there). But with thousands of variables in a single spectrum, this becomes tedious and you inevitably miss subtle correlations between different spectral regions. The problem escalates dramatically when analyzing many samples. Now you're not just dealing with thousands of variables, but trying to find systematic patterns across dozens or hundreds of spectra, understanding how multiple bands change together across samples, and identifying which spectral differences actually matter for distinguishing samples. **The multivariate nature of the problem demanded a multivariate solution.** ## The Coining of "Chemometrics" This gap was being felt simultaneously by researchers around the world, though initially they didn't know others were grappling with the same challenges. In January 1971, Swedish professor **Svante Wold** at Umeå University applied for a grant from the *Swedish Science Council* to address exactly these problems . As an organic chemist who was already exploring statistical approaches to chemical problems, building on his father Herman Wold's pioneering work in statistics, he needed better computational tools to handle datasets with dozens or even hundreds of variables. Examining one variable at a time left many patterns unseen. Building on this statistical foundation, he formalized his vision by coining a new term in his proposal: chemometrics (Swedish: kemometri) . Meanwhile, across the Atlantic, Bruce Kowalski at the University of Washington was independently developing solutions to the same fundamental problem. Kowalski, trained in analytical chemistry, had become fascinated by the potential of computers to help chemists extract more information from their experiments. As he would later recall: > *People like myself, Svante and others were just sort of tired of seeing all those numbers floating around and started scratching our heads and saying: there must be some way to pull information out of all these numbers* . At the same time, a third independent stream was emerging in Europe. In 1972, Luc Massart attended a EUROANALYSIS meeting in Heidelberg, where his work on formal methods in analytical chemistry caught the attention of Auke Dijkstra. This chance encounter sparked a collaboration focused on information theory and clustering methods for determining information content in analytical measurements . Notably, Massart's entry into the field occurred before he had even heard the term "chemometrics." The word was later introduced to him through an invitation from Wold and Kowalski to join the Chemometrics Society in 1974, after they discovered his work with Kaufmann published in Analytical Chemistry . This pivotal moment in chemometrics history is preserved in the original invitation letter from 1974, sent by Wold and Kowalski to the founding members. This document captures the collaborative spirit and scientific vision that launched the International Chemometrics Society. The word itself was elegant in its simplicity: *Chemometrics*, from chemistry and metrics, captured the essence of what was needed, i.e., the quantitative analysis of chemical data using mathematical and statistical methods. Wold's first publication using this new term appeared in 1972 in the Swedish journal *Kemisk Tidskrift* (volume 84, pages 34–37) in his paper "*Spline-funktioner – ett nytt verktyg i dataanalysen*" (Spline functions – a new tool in data analysis), where he formally named his research unit the "*Forskningsgruppen för Kemometri* (Research Group for Chemometrics), though it would take several years for the concept to gain broader recognition. ## The foundation of the International Chemometrics Society The roots of the International Chemometrics Society go back to June 10, 1974 in Seattle, when Wold travelled to Kowalski's group. On that trip, Wold and Kowalski compared SIMCA and ARTHUR on standard data sets, then celebrated with their teams at a bar and grill on the UW strip; in Wold's words, the society was formed "after ten shots of tequila", giving formal recognition to what would become one of chemistry's most important interdisciplinary fields . Later, it was renamed to *International Chemometrics Society* (ICS) . One year later, in 1975, Bruce Kowalski published the first English-language paper with "chemometrics" in its title: *"Chemometrics: Views and Propositions"* in the *Journal of Chemical Information and Computer Sciences* (volume 15, issue 4, pages 201–203). This short but seminal paper served as a manifesto for chemometrics as a distinct discipline . ## Core methods and early applications With the formal establishment of chemometrics as a distinct discipline, researchers began developing the mathematical toolkit that would define the field. This is where chemometrics represented a fundamental paradigm shift: rather than eliminating complexity to achieve clarity, it embraced complexity as a source of information. Methods that could handle all the variables simultaneously found patterns and relationships that would be invisible to traditional one-at-a-time analysis . **Principal Component Analysis**, originally developed by **Karl Pearson** in 1901 and further refined by **Harold Hotelling** in 1933 for psychological and social science applications , was adapted for analytical chemistry data and became one of chemometrics' foundational tools . **PCA** could take a dataset with hundreds of variables and find the underlying "directions" in the data that contained the most information, thereby reducing the complexity while preserving what mattered most. In fact, long before its formal adoption in chemometrics, PCA-like thinking had already appeared in organic chemistry. Wold noted that physical organic chemists had long used "PC-like" one- or two-component linear free-energy relationships (LFERs) (e.g., the Brönsted and Hammett equations) to understand reactivity patterns among similar molecules . But PCA was just the beginning. Herman Wold, Svante's father and a distinguished statistician, introduced the NIPALS algorithm in 1966 and, in the 1970s, developed Partial Least Squares (PLS) for econometric/path modeling . Svante Wold and colleagues adapted and extended PLS for chemical calibration and prediction in the late 1970s and early 1980s . PLS relates complex, high-dimensional instrumental measurements to the properties that matter (e.g., predicting an analyte's concentration from spectroscopic data). This wasn't just mathematical elegance; it was practical problem-solving that changed how chemists worked. The early days of chemometrics were marked by a sense of discovery and possibility. Researchers were finding that techniques originally developed for psychology, economics, or social sciences could be adapted to solve chemical problems. Factor analysis, cluster analysis, and pattern recognition methods all found new life in chemical applications. The field was inherently **interdisciplinary**, drawing from wherever the best tools could be found. Methods like **multivariate curve resolution**, developed by researchers such as **Lawton and Sylvestre** in the early 1970s, showed how mathematical techniques could "unmix" complex spectra to identify individual components. Software development was crucial: Bruce Kowalski's **ARTHUR** system became the first major chemometrics software package. It provided modules for scaling (**SCALE**), principal component analysis (**KAPRIN**, **KATRAN**), and visualization (**VARVAR**). ## Institutional development and academic recognition The 1980s brought a wave of foundational publications that would shape the field for decades. **Edmund Malinowski's** *"Factor Analysis in Chemistry"* (1980, with a second edition in 1991) provided a rigorous mathematical foundation for understanding how multivariate methods worked with chemical data . The textbook by **Sharaf, Illman, and Kowalski** *"Chemometrics"* (1986) established chemometrics as a legitimate field of study, complete with its own theoretical framework and practical methodologies . **Martens and Naes** wrote an influential guide *"Multivariate Calibration"* (1989), showing how these mathematical concepts could be applied to solve real analytical problems . During this period, the field also began to develop its institutional identity. The journal **Chemometrics and Intelligent Laboratory Systems** was founded in 1986, followed by the **Journal of Chemometrics** in 1987, providing dedicated venues for chemometric research . Software packages like SIMCA and The Unscrambler made chemometric methods accessible to practicing chemists who weren't mathematical specialists . The first major international conference was the *NATO Advanced Study Institute on Chemometrics: Mathematics and Statistics in Chemistry*, held in Cosenza, Italy from September 12–23, 1983, organized by Kowalski and Massart, bringing together virtually all the founding figures of chemometrics for the first time . ## Expansion beyond analytical chemistry As computers became more powerful and easy to use in the 1990s, chemometrics began to move out of the analytical chemistry lab. Early methods for real-time process control using multivariate models appeared in the 1980s and 1990s. The FDA introduced *Process Analytical Technology* (PAT) in a draft guidance (September 2003), with final guidance issued in October 2004. This framework showed manufacturers how to predict product quality from online measurements and adjust processes quickly when something went wrong. The biological sciences became another key area for chemometrics. In the early 2000s, these fields grew rapidly because new instruments could measure thousands of molecules at once. Researchers had to work with very large datasets. Traditional statistics, which handle only dozens or hundreds of variables, were not enough and chemometric methods helped scientists make sense of these large biological datasets. Environmental monitoring also benefited from chemometrics. Since the 1990s, air quality stations have collected continuous data from many sensors. Water quality networks face the same challenges, with many measurements over time and space. In these applications, chemometric tools, like principal component analysis and cluster analysis, help to find real environmental trends in these complex data. ## Modern developments and future directions The relationship between chemometrics and **machine learning** has been particularly interesting to watch develop. In many ways, chemometrics *anticipated* the challenges that would later drive the development of modern machine learning. The high-dimensional, noisy datasets that are routine in chemistry share many characteristics with the big data problems that sparked the machine learning revolution. However, machine learning has complemented rather than replaced traditional chemometric methods. A strong reason is that classical chemometric methods naturally align with the mathematical structure of chemical data: bilinearity in spectroscopic measurements, non-negativity constraints in concentrations and spectra, and low-rank approximations that reflect the underlying components in mixtures. These methods were designed around the inherent nature of chemistry, incorporating physical constraints and mechanistic knowledge from thermodynamics, quantum mechanics, and reaction kinetics. This alignment delivers both predictive power and scientific interpretability in ways that purely data-driven approaches often cannot match. Therefore, classical chemometric methods often offer something that many machine learning approaches can lack: transparent interpretability. For example, a PCA model may show that most of the variation in a dataset is captured by a small number of principal components; by examining the scores and loadings a chemist can sometimes connect these components to underlying chemical or physical sources of variation. Likewise, when a PLS model predicts analyte concentration from spectroscopic data, the regression coefficients can be inspected to identify which wavelength regions contribute most to the prediction. This interpretability remains crucial in scientific applications, where understanding why a model works is often as important as how well it performs. Nowadays, **modern chemometrics** represents a mature field with a rich theoretical foundation and proven practical applications. Software packages like R, Python's scikit-learn, and specialized chemometric software have made sophisticated analyses accessible to researchers who aren't mathematical specialists. Yet the field continues to evolve. **Multi-way methods** handle datasets with multiple modes or dimensions, like time-resolved spectroscopy data or multi-sensor monitoring systems. Uncertainty quantification has become increasingly important as chemometric models are used for regulatory decisions and quality control applications where understanding prediction reliability is crucial. Real-time applications require algorithms that can process streaming data and update models on the fly. Looking toward the future, several trends seem likely to shape the continued evolution of chemometrics. The relationship with **artificial intelligence** will probably deepen, but in ways that preserve the domain knowledge and interpretability that characterize traditional chemometrics. The most promising developments seem to involve **hybrid approaches** that leverage the strengths of both paradigms. Recently, **large language models** have started to play a role in chemometrics. These models help people understand and use chemometric methods more easily. A student can ask, *"What preprocessing should I use for this NIR dataset?"* and get a simple explanation, examples, and even code. This makes learning faster and easier. LLMs may also change how we work with instruments. Instead of using complicated menus, scientists could type or say, *"Find the best method to measure caffeine using UV-Visible."* The system could then suggest a method, explain why it works, and adjust settings on its own. In the future, we may have smart instruments that can communicate with us. An instrument might alert you: *"I found a problem in the signal. Do you want me to fix it?"* These systems could use chemometric models, give real-time advice, and answer questions during experiments. What once felt like science fiction is slowly becoming part of our everyday work in the lab: the beginning of the *intelligent laboratory* era. Finally, as chemometrics continues to grow alongside AI and smart technologies, another key trend is the democratization of tools and knowledge. **Open-source** software and online educational platforms are making powerful chemometric methods available to more people than ever before. Researchers no longer need to be coding experts or math specialists to apply complex models. Browser-based platforms and interactive tools, often supported by cloud computing and AI assistants, allow users to explore data, build models, and understand results with just a few clicks or questions. This growing accessibility supports **a future where chemometrics is not only more powerful but also more user-friendly, collaborative, and widely used across scientific fields**. #### Chemistry's information age Perhaps most importantly, chemometrics embodies a way of thinking about chemical problems that *embraces complexity* rather than trying to simplify it away. In an era where chemistry increasingly intersects with materials science, environmental science, biotechnology, and other fields, this **multivariate perspective** becomes ever more valuable. The story of chemometrics is ultimately about the transformation of chemistry from analyzing one variable at a time to extracting insights from massively multivariate datasets. It shifted the paradigm from reducing complexity to achieve selectivity, to embracing complexity as a source of information. What makes this story particularly compelling is how it illustrates the power of **interdisciplinary thinking**. The mathematical and statistical methods that form the foundation of chemometrics were developed in other fields for other purposes. But when adapted thoughtfully to chemical problems, with proper attention to the unique characteristics of chemical data and the goals of chemical research, these methods proved transformative. From the pioneering work of Wold and Kowalski in the early 1970s to today's machine learning integration, chemometrics has consistently evolved to meet the changing needs of chemical science while maintaining its core commitment to extracting meaningful information from complex data. In many ways, chemometrics represents chemistry's coming of age in the information era: the recognition that in a world of abundant data, our ability to extract meaningful insights becomes the limiting factor in scientific progress. ## References # FAQs of Lovelace's Square (/lovelaces-square/lovelaces-square-faqs) ### What is Lovelace's Square? Lovelace's Square is a non-profit and free open digital space for people working with chemometrics. It helps the community share tools and resources, making everything easier to find and use. ### Who can use Lovelace's Square? Everyone interested in chemometrics or data analysis is welcome! It’s especially helpful for people who don’t have easy access to expensive software. ### Is Lovelace's Square free to use? Yes, 100% free! It will remain free in the future. ### If Lovelace’s Square is free, how is it maintained? Lovelace's Square is run by a small team with support from generous partners. We keep costs low with a simple, efficient setup, and the project is funded through donations, grants, and sponsorships. If you'd like to support us, we'd be truly grateful, just email us at [contact@lovelacesquare.org](mailto:contact@lovelacesquare.org). Every contribution is fully invested in the platform and helps us grow! ### What is the development timeline for Lovelace's Square? **The Square** and **The Library** are live and accessible now, with new content published regularly. **Ada**, the ecosystem's conversational AI layer, is now available in public beta, so visitors can already try it while we keep improving it. ### Is Lovelace's Square affiliated with any organisations? We work with universities, companies and research centers, although we are not affiliated with any one. We stay independent so we can serve the community fairly. ### How can institutions collaborate with Lovelace's Square? Institutions can work with Lovelace’s Square by sharing tools, data, or documents; joining pilot projects to test and improve the platform; helping create learning materials; or supporting open and fair science through small donations or promotion. If your organization believes in free and shared knowledge, we would love to hear from you. Contact us at [contact@lovelacesquare.org](mailto:contact@lovelacesquare.org). ### How can I get help if I encounter issues? Sure! just email us at [contact@lovelacesquare.org](mailto:contact@lovelacesquare.org). We’re here to help 😊. ### How do I get started? We recommend reading the documentation in the left sidebar first. Check the left sidebar to begin! There, you’ll find everything you need to navigate the platform and learn how to collaborate with us. ### How can I contribute? You can [share code](https://lovelacesquare.org/the-square/upload-your-code), [upload datasets](https://lovelacesquare.org/the-square/upload-your-data), review content, send feedback, or support us financially. ### Is there a review process for uploaded content? Yes! Everything is reviewed to make sure it’s useful and high-quality. ### Can I participate in the review process? Absolutely! Just contact us to get started. ### My content was published without permission. We sincerely apologize if your content has been published on Lovelace's Square without permission and for any inconvenience this may have caused. Please contact our team at [contact@lovelacesquare.org](mailto:contact@lovelacesquare.org) so we can resolve the issue as quickly as possible. Your rights and the integrity of your work are very important to us, we're here to make it right. # Open Science (/lovelaces-square/open-science) The open science movement began in the early 2000s, driven by a growing realization: much of the world’s research (funded with public money) was hidden behind expensive paywalls. Scientists, institutions, and even the public who helped fund this work often couldn’t read or reuse it. Preprint servers like **arXiv** (launched in 1991 for physics) had already shown how sharing research early and openly could accelerate discovery. The **open-source software movement** was proving that collaboration and transparency could produce powerful, reliable tools. Building on these examples, in 2002, the **Budapest Open Access Initiative** made a bold call for change, demanding that scientific papers be freely available to everyone. Participants at meeting in Budapest, December 1, 2001. By the 2010s, however, a deeper problem emerged: many scientific results could not be reproduced. This **reproducibility crisis** exposed how secrecy in research was not only unfair, it was harming science itself. Without access to data, code, or detailed methods, independent verification became almost impossible. #### Core values from UNESCO Recognizing this, in **November 2021**, 194 countries adopted the **UNESCO Recommendation on Open Science**. This was the first global standard for open, transparent, and inclusive research. UNESCO defines open science as making scientific knowledge *openly available, accessible, and reusable for everyone*, with the goal of improving collaboration and ensuring that science serves society as a whole. In other words, open science is not only about access. It is about **fairness, participation, and shared progress**. UNESCO's framework for open science. The UNESCO Recommendation identifies four interrelated core values that guide open science worldwide: 1. **Quality and integrity** — Open science promotes rigorous, transparent, and reproducible research. By making methods and data available for scrutiny, the scientific community can verify results and build on trustworthy foundations. 2. **Collective benefit** — Science is a global public good. Knowledge should benefit all of humanity, not just those who can afford it. 3. **Equity and fairness** — Everyone deserves equal opportunity to contribute to and benefit from science, regardless of location, income, career stage, or institution. 4. **Diversity and inclusiveness** — Open science embraces different forms of knowledge, practices, and languages, including traditional and Indigenous knowledge systems that expand our understanding of the world. Together, these principles aim to make research more ethical, collaborative, and socially relevant. #### From principles to policy: the European Commission UNESCO's Recommendation has influenced policy worldwide. Major research institutions have aligned their strategies with these values. Among them, the **European Commission** has become a global leader in putting open science into practice. The Commission views open science as central to its research and innovation strategy, placing it at the heart of the **Horizon Europe** program (2021-2027), the world's largest research funding program with a budget of €95.5 billion. Under Horizon Europe, open science is not optional, but mandatory. The policy operates under the principle: > *"As open as possible, as closed as necessary."* This means openness is the default, but researchers can restrict access when there are legitimate reasons like privacy, security, or intellectual property concerns. **Concrete requirements for Horizon Europe projects:** - **Open access to publications**: All peer-reviewed publications must be made freely available immediately upon publication. Authors retain copyright and can choose open licenses. - **Open research data**: All research data must be made accessible following the **FAIR principles** (Findable, Accessible, Interoperable, Reusable). Researchers must create a Data Management Plan describing what data will be collected, how it will be shared, and where it will be stored. - **Research outputs**: Beyond publications and data, researchers are encouraged to share other outputs like software, algorithms, protocols, and methods. The European approach goes further than just mandates. The Commission has created infrastructure to support open science. **Open Research Europe** is a free publishing platform for Horizon Europe and Horizon 2020 beneficiaries, offering immediate open access with no publication fees for authors. **OpenAIRE** provides a pan-European infrastructure that gives access to millions of open research outputs and supports researchers in complying with open science policies. The **European Open Science Cloud (EOSC)** is building a trusted environment for research data storage, sharing, and reuse across disciplines and borders. These policies and infrastructure ensure that publicly funded science in Europe remains truly public, setting a model that other regions are beginning to follow. --- ## Why this matters in chemometrics Despite advances in open science policy, serious challenges remain in chemometrics. Many powerful methods exist only within expensive commercial software. Important papers are behind paywalls. Datasets from published studies are often unavailable. These barriers create real problems. Students cannot afford the software they need. Scientists cannot reproduce experiments when data or code is missing. Teachers cannot demonstrate modern techniques without expensive licenses. Researchers in well-funded universities have access to tools that others in poorer regions do not. This inequality slows progress for everyone. Important research stays unpublished. Talented people cannot participate. Useful methods remain unknown. Open science aims to change this, so that knowledge can circulate freely, collaboration can flourish, and discovery can become truly shared. --- ## How Lovelace's Square contributes to open science Open science is not just about access. It changes how research works. When methods are open, people can check them, improve them, and adapt them. When data is shared, it can be analyzed again, combined with new findings, or used to answer new questions. When collaboration is transparent, trust grows. This shift is already happening in chemometrics. Researchers share code on GitHub. They upload datasets to open repositories like Zenodo. They write tutorials to help newcomers. Lovelace's Square is part of this movement, built from the start with open science principles at its core. Lovelace's Square is designed to make transparency, accessibility, and collaboration real. Everything on the platform is free. You do not need a subscription or institutional access. [The Square](https://lovelacesquare.org/the-square)'s algorithms, [The Library](https://library.lovelacesquare.org/)'s educational content, and [Ada](/lovelaces-square/ada/what-is-ada)'s guided assistance are available to anyone with internet access. All contributions must use recognized open-source or open-data licenses that allow use, modification, and redistribution for any purpose. Non-commercial, no-derivatives, proprietary, research-only, and academic-only terms are not accepted. Every algorithm, dataset, and tutorial comes with clear documentation. Contributors must explain what their code does, how it works, and why they made certain choices. This transparency helps others understand, trust, and build on shared work. Lovelace's Square is not controlled by a single institution. It is a collaborative effort where researchers, educators, students, and practitioners from around the world contribute, ensuring the platform responds to real community needs. We follow FAIR principles for all contributions, ensuring that what is shared today stays useful tomorrow. Access professional algorithms without paying. Share your work and reach a global audience. Work openly with colleagues worldwide. Learn chemometrics without expensive software. Use real datasets and code that you can study, change, and use in your projects. Build courses using free tools. Give students hands-on experience without budget limits. Teach reproducibility by example. Speed up progress through shared resources. Make research more trustworthy and reproducible. Build on what others have done instead of starting from zero. Open science is not charity. When you share your work openly, more people use it and cite it. Others can improve it. When others share openly, you benefit too. The more we invest in openness, the better for everyone. Today's big challenges are too complex for any single lab to solve. They need collaboration across disciplines, countries, and institutions. This is why UNESCO and the European Commission prioritize open science. It is practical, not idealistic. Lovelace's Square aligns with these principles, offering a concrete example of how open science can work in chemometrics. # What is Lovelace's Square (/lovelaces-square/what-is-lovelace) **[Lovelace's Square](https://lovelacesquare.org)** is a non-profit, community-driven platform dedicated to advancing chemometrics through the principles of open science. It provides free and open access to algorithms, datasets, and specialized research tools. The platform takes its name from Ada Lovelace, widely recognized as one of the first computer programmers, symbolizing the intersection of code, innovation, and collective intellectual effort. ### The problem we solve Despite its importance in many scientific fields, chemometrics remains hard to access and apply for many users. These barriers slow down progress, especially in education and low-resource environments. Specifically, the chemometrics community faces several challenges that limit scientific progress: Many established methods are locked behind expensive proprietary software, making them inaccessible to students, professors, and researchers with limited budgets. Theoretical and methodological knowledge is scattered across paywalled publications or presented in highly technical language that creates barriers for newcomers. Many platforms, like GitHub, are too specialized for developers and, therefore, not widely used in our community. Implementation challenges and steep learning curves prevent many researchers from adopting advanced chemometric methods in their work. ### Our solution: an open ecosystem for all Lovelace's Square breaks down these barriers by creating a **fully open-source and open-access** digital environment where knowledge flows freely and innovation thrives. The concept of the *"square"* reflects our vision of a public space: a digital space where participants gather to share, collaborate, and innovate together. The project is built on three main pillars: - **The Square**: a growing collection of chemometric algorithms and datasets. It is a place where scientists and students can find tools, compare results, and improve their work. Everything is open and easy to use. - **The Library**: an educational space. It has tutorials, guides, and examples to help users learn chemometrics step by step, supporting both beginners and experts. - **Ada**: the ecosystem's conversational AI layer. It connects questions with *The Square* and *The Library*, helping people understand methods, find code and datasets, and move from explanation into practical work. These three parts work together to support science that is open, clear, and collaborative, where everyone can learn and contribute. *The Lovelace's Square ecosystem connects three components: The Square for practical tools, The Library for educational content, and Ada as the conversational AI layer that works with both.* ## The Square: Open source repository [**The Square**](https://lovelacesquare.org/the-square) serves as the technical backbone of our ecosystem: a carefully curated repository of algorithms, datasets, and utilities that puts professional-grade chemometric tools directly into your hands. #### What makes The Square special What makes The Square special is that it gives everyone easy and free access to powerful tools for data analysis in chemistry. Many of these tools are normally hidden inside expensive software that only some people or universities can afford. But The Square is different; it is open to all, without needing to pay, create an account, or install anything. Also, The Square is not just a collection of code. The main tools are carefully organized, checked, and explained so that people can understand how they work and use them correctly. This is very helpful for students, teachers, and scientists who want to learn or work with chemometrics, even if they are not experts in programming. **The Square** is live and actively growing. New algorithms, datasets, and tools are added regularly by our community of contributors. --- ## The Library: Open access knowledge [**The Library**](https://library.lovelacesquare.org/) represents the educational heart of Lovelace's Square. It is the part of Lovelace’s Square that helps people understand the ideas behind the tools in a simple and clear way. Many times, this information is hard to find, too expensive, or too difficult to understand, especially for students or professionals who are not experts in statistics or programming. #### A documentation page for chemometrics The Library solves this problem by offering free and easy-to-read lessons, examples, and guides. It also tells the story behind it: why it was created, how it evolved, what it can and cannot do. This helps people not only use tools, but also think critically and choose the right method for their work. The Library connects science with learning, making chemometrics easier to understand for everyone, no matter their background. The Library is live, with new educational content published regularly. Your contributions are welcome! --- ## Ada: the conversational AI layer **Ada** is the conversational AI layer of Lovelace's Square. It is designed to connect questions with the ecosystem's resources, so explanation, search, code inspection, teaching material, and Python-based examples can happen in one guided flow. Ada is built for conversation around chemometrics. People can ask questions in plain language, and Ada can help connect the question to Library explanations, Square resources, code examples, or workspace tasks. It can help reason about code in different languages and execute compact Python examples when a runnable demonstration is useful. Ada is not presented as a general chatbot disconnected from the project. It is connected to The Square and The Library, so responses can be grounded in platform material when that material is available. As the platform grows, Ada's useful context grows with it. New articles, codes, datasets, and teaching resources give the conversational layer more material to search, explain, and connect. You can explore Ada directly at [ada.lovelacesquare.org](https://ada.lovelacesquare.org) and learn more in the Ada documentation: - [What is Ada?](/lovelaces-square/ada/what-is-ada) - [How to Use Ada](/lovelaces-square/ada/how-to-use-ada) - [Last Developments](/lovelaces-square/ada/last-developments) Ada is already available in public beta as a specialized chemometrics assistant, and the current work is focused on testing with real users, iteration, and expanding what she can do across explanation, search, workspace flows, and Python execution. --- ## Why this matters Lovelace's Square embodies the ideals of **open science**. We encourage open exchange of knowledge, transparent methodological documentation, and reproducible research. Our vision is a future where anyone working in chemometrics, regardless of background or location, has access to a rich repository of algorithms, data, and tools that continuously grow and improve through community contributions. Access professional-grade tools, contribute your algorithms, and collaborate with the global community Find teaching materials, practical examples, and tools for hands-on learning experiences Learn with guided pathways, practical examples, and support from the community Apply proven chemometric methods to real-world problems in quality control, process optimization, and product development ## Join us! Lovelace's Square isn't just a platform: it's a **movement toward democratized science**. Whether you're a student taking your first steps in chemometrics or a researcher pushing the boundaries of chemical data analysis, you have a place in our community. Together, we're building a future where scientific knowledge flows freely, innovation thrives, and the next generation of discoveries becomes possible for everyone. # Least Squares: conjugate gradient (/introduction-to-chemometrics/least-squares/conjugate-gradient) Gradient descent is easy to understand, but it can be inefficient. It always follows the steepest downhill direction at the current point, and that can lead to a zig-zag path, especially when the error surface is long and narrow . The **conjugate gradient** method tries to be smarter. Instead of choosing each new direction independently, it chooses directions that cooperate with the previous ones. ## The problem it solves For linear least squares, the normal equations are: $\mathbf{X}^T\mathbf{X}\boldsymbol{\beta} = \mathbf{X}^T\mathbf{y}$ This has the form: $\mathbf{A}\boldsymbol{\beta} = \mathbf{b}$ where: $\mathbf{A} = \mathbf{X}^T\mathbf{X}, \qquad \mathbf{b} = \mathbf{X}^T\mathbf{y}$ If $\mathbf{A}$ is small and well behaved, we can solve this directly. If it is huge, storing it or factorizing it can become expensive. When $\mathbf{A} = \mathbf{X}^T\mathbf{X}$ is symmetric positive definite, conjugate gradient solves the same system iteratively, without needing a full matrix inverse . (The letters $\mathbf{A}$ and $\mathbf{b}$ are the usual names in numerical linear algebra; they are not the number of components $A$ used in PCA, PCR and PLS.) That condition matters. If columns of $\mathbf{X}$ are redundant or nearly redundant, $\mathbf{X}^T\mathbf{X}$ can become singular or ill-conditioned. In that case, LSQR, CGNR, SVD, regularization, or preconditioning is usually safer than applying plain conjugate gradient to the normal equations. ## Why steepest descent can be slow Imagine the least squares error surface as a valley. Gradient descent always points in the locally steepest direction. That sounds ideal, but in a narrow valley the steepest direction often points across the valley rather than along it. The algorithm bounces from side to side. Conjugate gradient remembers something about where it has already been. It chooses the next direction so that progress made in earlier directions is not undone later. That is the meaning of **conjugate** here: the directions are independent with respect to the geometry defined by $\mathbf{A}$. ## The core idea Instead of moving only along the negative gradient, conjugate gradient builds a sequence of search directions: $\mathbf{p}_0, \mathbf{p}_1, \mathbf{p}_2, \ldots$ Each direction is chosen so that it is conjugate to the previous directions: $\mathbf{p}_i^T\mathbf{A}\mathbf{p}_j = 0 \quad \text{for } i \neq j$ This condition means that each step solves a new part of the problem without disturbing what earlier steps already improved . Conjugate gradient sits between direct methods and simple gradient descent. It is still iterative, but it usually needs far fewer steps than ordinary gradient descent for large linear systems. ## Interactive example The visualization below shows the method in coefficient space. The ellipses represent equal-error contours. A plain steepest-descent path would tend to bounce across a narrow valley, while conjugate gradient chooses directions that make new progress without undoing the previous step. ## Why chemometricians care Large chemometric datasets often contain many variables. A hyperspectral image, a dense chromatographic fingerprint, or a high-resolution spectrum can make $\mathbf{X}^T\mathbf{X}$ large enough that direct inversion becomes unattractive. Conjugate gradient is useful when: - The least squares problem is linear. - $\mathbf{X}^T\mathbf{X}$ is positive definite, or the problem has been regularized or preconditioned appropriately. - The system is large. - Matrix-vector products are affordable. - You do not need to explicitly form or invert a huge matrix. It is especially helpful when combined with preconditioning, where we transform the system so the algorithm sees a more rounded, easier-to-descend error surface . Stopping conjugate gradient early on the normal equations is not just a numerical shortcut. Started from zero on centred data, after $A$ steps it gives exactly the PLS1 regression vector with $A$ components, so in chemometrics this early-stopped solution has a famous name: [PLS regression](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression#the-nipals-algorithm-for-pls1). Each extra step adds one component. ## What to remember Conjugate gradient is still trying to minimize the same least squares error. It does not change the statistical assumptions or the meaning of the fitted parameters. It changes the route to the answer. Gradient descent asks, "Which way is downhill right now?" Conjugate gradient asks, "Which downhill direction gives new progress without undoing the old progress?" That small difference can matter a lot when the problem becomes large. ## References # Least Squares: Gauss-Newton (/introduction-to-chemometrics/least-squares/gauss-newton) The normal equations work beautifully when the model is linear in its parameters. But many scientific models are not like that. Think of exponential decay, enzyme kinetics, adsorption isotherms, or chemical equilibrium models. Their parameters may appear inside exponentials, denominators, or other nonlinear expressions. In those cases, the model cannot be written as: $\mathbf{f}(\mathbf{X}, \boldsymbol{\beta}) = \mathbf{X}\boldsymbol{\beta}$ So we need another idea. The **Gauss-Newton method** solves nonlinear least squares by repeatedly pretending, locally, that the nonlinear model is linear . ## The nonlinear least squares problem The objective is still: $\min_{\boldsymbol{\beta}} \left\| \mathbf{y} - \mathbf{f}(\mathbf{X}, \boldsymbol{\beta}) \right\|^2$ The difference is that $\mathbf{f}$ is now nonlinear in $\boldsymbol{\beta}$. Define the residual vector: $\mathbf{r}(\boldsymbol{\beta}) = \mathbf{y} - \mathbf{f}(\mathbf{X}, \boldsymbol{\beta})$ We want to make that residual vector as small as possible. ## The local linear approximation At the current parameter estimate $\boldsymbol{\beta}^{(k)}$, Gauss-Newton approximates the model with a first-order Taylor expansion : $\mathbf{f}(\mathbf{X}, \boldsymbol{\beta}) \approx \mathbf{f}(\mathbf{X}, \boldsymbol{\beta}^{(k)}) + \mathbf{J}(\boldsymbol{\beta}^{(k)})(\boldsymbol{\beta} - \boldsymbol{\beta}^{(k)})$ The matrix $\mathbf{J}$ is the **Jacobian**. It contains the partial derivatives of each model prediction with respect to each parameter. If the model has many outputs and several parameters, the Jacobian tells us how every prediction changes when every parameter changes slightly. The Jacobian is a sensitivity table. Each entry asks: if I nudge this parameter, how much does this predicted value move? ## Interactive example The example below fits a nonlinear exponential curve. Each step builds a local linear approximation around the current parameters, solves a small least squares correction, and updates the curve. Watch how the fit improves quickly when the local approximation is good. ## The Gauss-Newton step Instead of solving the nonlinear problem directly, we solve a linear least squares problem for a parameter correction $\boldsymbol{\delta}$ : $\mathbf{J}^T\mathbf{J}\boldsymbol{\delta} = \mathbf{J}^T\mathbf{r}$ Then we update: $\boldsymbol{\beta}^{(k+1)} = \boldsymbol{\beta}^{(k)} + \boldsymbol{\delta}$ So each iteration says: 1. Look at the current model. 2. Build a local linear approximation. 3. Solve a least squares problem for the correction. 4. Update the parameters. Then repeat. ## When it works well Gauss-Newton works well when the model is not too nonlinear near the solution and the starting guess is already reasonably close. In that situation, the local linear approximation is accurate, and the method can converge quickly . It can struggle when the starting point is poor, the residuals are large, or the model is strongly nonlinear. Then the local linear approximation may point in a direction that is mathematically reasonable but practically too bold. That is where Levenberg-Marquardt becomes useful. ## What to remember Gauss-Newton is not a completely different philosophy from linear least squares. It is more like a clever loop around it. At each iteration, it turns a nonlinear problem into a temporary linear one. That is why understanding the normal equations first pays off: they quietly reappear inside nonlinear optimization. ## References # Least Squares: gradient descent (/introduction-to-chemometrics/least-squares/gradient-descent) The normal equations give us a beautiful closed-form answer, but they are not always the most practical way to solve a least squares problem. When the dataset is huge, when the number of variables is very large, or when we want to update a model gradually as new data arrive, an iterative method can be more useful . **Gradient descent** is the simplest iterative idea: start somewhere, look at the direction in which the error increases most quickly, and move the other way. In other words: walk downhill. ## The error surface For linear least squares, we want to minimize: $S(\boldsymbol{\beta}) = \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|^2$ If there is only one parameter, this error is a curve. If there are two parameters, it becomes a surface. With many parameters, it lives in a space we cannot draw, but the same idea remains: every possible set of parameters has an associated error. The interactive plot below starts with the simplest possible case. The algorithm moves along a curve until it reaches the bottom: Now we can make the picture a little closer to a real linear regression, with slope and intercept as two parameters. The error becomes a bowl-shaped surface. Click on the plot to choose a starting point and watch how the path descends toward the minimum: This bowl shape is important. Ordinary linear least squares is convex, so there is one global minimum. Gradient descent does not have to worry about hidden local valleys in this case . In spectroscopic calibration, a model can have hundreds or thousands of coefficients. We cannot visualize that error surface, but the algorithm still works with the same principle: follow the direction that reduces the residual error. ## The gradient The **gradient** is a vector of derivatives. It tells us which direction makes the function increase fastest. For the least squares objective: $S(\boldsymbol{\beta}) = \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|^2$ the gradient is: $\nabla S(\boldsymbol{\beta}) = 2\mathbf{X}^T\mathbf{X}\boldsymbol{\beta} - 2\mathbf{X}^T\mathbf{y}$ That vector points uphill. Since we want to minimize the error, we move in the opposite direction: $-\nabla S(\boldsymbol{\beta})$ This is the whole intuition of gradient descent. ## The update rule We start with an initial guess: $\boldsymbol{\beta}^{(0)}$ Then we update the parameters step by step: $\boldsymbol{\beta}^{(k+1)} = \boldsymbol{\beta}^{(k)} - \alpha \nabla S(\boldsymbol{\beta}^{(k)})$ Here, $k$ is the iteration number and $\alpha$ is the step size, often called the learning rate . For linear least squares, substituting the gradient gives: $\boldsymbol{\beta}^{(k+1)} = \boldsymbol{\beta}^{(k)} - 2\alpha \mathbf{X}^T(\mathbf{X}\boldsymbol{\beta}^{(k)} - \mathbf{y})$ This equation says: compute the residuals, translate them into a direction in parameter space, and take a step that reduces the error. ## Choosing the step size The step size $\alpha$ controls how boldly the algorithm moves. - If $\alpha$ is too small, convergence is painfully slow. - If $\alpha$ is too large, the algorithm can jump over the minimum and oscillate. - If $\alpha$ is well chosen, the path descends smoothly. For simple least squares problems, it is sometimes possible to calculate a good step size analytically. In practice, many algorithms use line search or adaptive learning-rate rules. If the error decreases very slowly, the step may be too cautious. If the error goes down and then suddenly explodes, the step is probably too aggressive. Watching the error curve over iterations is one of the simplest diagnostics. ## When gradient descent is useful Gradient descent is not the fastest method for a small calibration curve. If you have ten standards and one predictor, the normal equations will solve the problem immediately. Gradient descent becomes more interesting when: - The dataset is too large to process comfortably in one matrix operation. - The number of variables is very high, as in full-spectrum modeling. - The objective includes extra penalty terms, such as in regularized regression. - New data arrive continuously and the model needs to update over time. In modern machine learning, gradient descent is everywhere. But its roots are older and very close to the least squares problem itself . ## What it gives up Gradient descent trades exact one-step algebra for flexibility. It needs a starting point, a step-size rule, and a stopping criterion. It may require many iterations. But in return, it can handle problems where direct methods are too expensive or inconvenient. The key idea is modest but powerful: we do not need to solve everything at once. Sometimes we can get there by improving the answer, one step at a time. ## References # Least Squares: Levenberg-Marquardt (/introduction-to-chemometrics/least-squares/levenberg-marquardt) The **Levenberg-Marquardt** method is one of the most common algorithms for nonlinear least squares . It is popular because it balances two useful behaviors: - It can behave like Gauss-Newton when the local approximation is reliable. - It can behave more like gradient descent when the step needs to be cautious. That balance makes it especially useful for real experimental fitting, where the first parameter guess is often imperfect. ## The Gauss-Newton problem In Gauss-Newton, each iteration solves : $\mathbf{J}^T\mathbf{J}\boldsymbol{\delta} = \mathbf{J}^T\mathbf{r}$ and then updates: $\boldsymbol{\beta}^{(k+1)} = \boldsymbol{\beta}^{(k)} + \boldsymbol{\delta}$ This can be very fast when the current point is already close to the answer. But if the starting point is poor, the Gauss-Newton step can be too aggressive. ## Adding damping A common Levenberg-style damping form modifies the system by adding a diagonal stabilizing term: $(\mathbf{J}^T\mathbf{J} + \lambda \mathbf{I})\boldsymbol{\delta} = \mathbf{J}^T\mathbf{r}$ Here, $\lambda$ controls the damping and $\mathbf{I}$ is the identity matrix . Many descriptions of the Marquardt variant use $\lambda \operatorname{diag}(\mathbf{J}^T\mathbf{J})$ instead of $\lambda \mathbf{I}$. The idea is the same for this article: add damping so the step becomes safer when the local approximation is unreliable. When $\lambda$ is small, the method behaves like Gauss-Newton. When $\lambda$ is large, the method becomes more cautious and behaves more like gradient descent. The damping parameter is like a trust control. If the local quadratic approximation is working, the algorithm trusts it more and reduces damping. If a step makes the fit worse, the algorithm becomes more cautious and increases damping. ## Interactive example This panel uses the same kind of nonlinear curve fitting problem as Gauss-Newton, but now the damping parameter changes as the fit improves. When the model is uncertain, the method behaves cautiously. When the local approximation starts working, it becomes more confident. ## The algorithmic rhythm A typical Levenberg-Marquardt iteration looks like this: 1. Start from current parameters. 2. Compute residuals and the Jacobian. 3. Solve the damped system for $\boldsymbol{\delta}$. 4. Try the new parameters. 5. If the fit improves, accept the step and reduce damping. 6. If the fit worsens, reject or shrink the step and increase damping. This makes the method adaptive. It can move quickly when the path is clear, but slow down when the model surface becomes tricky. ## Where it appears in chemistry Levenberg-Marquardt is commonly used for nonlinear curve fitting: - Exponential decay models. - Michaelis-Menten kinetics. - Spectral peak fitting. - Binding curves. - Calibration models with nonlinear response. In all these cases, the model parameters have physical meaning, and a stable fitting method matters. A small change in starting values should not make the model behave wildly. ## What it gives you Levenberg-Marquardt does not guarantee perfection. Nonlinear least squares can still have local minima, and the starting point still matters. But compared with plain Gauss-Newton, it is usually more forgiving. If Gauss-Newton is the confident version of nonlinear least squares, Levenberg-Marquardt is the version that knows when to be careful. ## References # Least Squares: normal equations (/introduction-to-chemometrics/least-squares/normal-equations) The **normal equations** are often the first computational solution we meet for least squares. They feel almost too neat: write the model as a matrix product, do a little algebra, set a derivative to zero, and the optimal parameters appear in one compact expression. This direct solution is possible when the model is **linear in its parameters**. That means the prediction can be written as: $\mathbf{f}(\mathbf{X}, \boldsymbol{\beta}) = \mathbf{X}\boldsymbol{\beta}$ In that case, the least squares problem becomes: $\min_{\boldsymbol{\beta}} \left\| \mathbf{y} - \mathbf{X}\boldsymbol{\beta} \right\|^2$ Do you remember the assumption of *linearity in the parameters* from the [least squares foundation](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics#key-assumptions-for-linear-models)? This is where it becomes computationally powerful. ## Building the matrix model Imagine a simple analytical model where a measured response depends on concentration and temperature. We run five experiments and write one row per experiment: The first column of ones is there for the intercept. It lets the model include a constant term: $y_1 = \beta_0 + x_{11}\beta_1 + x_{12}\beta_2$ The parameter vector contains the unknowns: Putting everything together: $\mathbf{y} = \mathbf{X}\boldsymbol{\beta} + \boldsymbol{\varepsilon}$ The vector $\boldsymbol{\varepsilon}$ holds the measurement errors. Because of it, there is almost never a $\boldsymbol{\beta}$ that makes $\mathbf{X}\boldsymbol{\beta}$ exactly equal to $\mathbf{y}$: five equations, three unknowns. Least squares asks for the $\boldsymbol{\beta}$ that makes $\mathbf{X}\boldsymbol{\beta}$ as close as possible to $\mathbf{y}$. ## Why we need the normal equations If $\mathbf{X}$ were square and invertible, we could solve: $\boldsymbol{\beta} = \mathbf{X}^{-1}\mathbf{y}$ But in chemometrics, that is rarely the situation. We usually have more samples than parameters, or sometimes many more variables than samples. A calibration with 30 standards and two predictors gives a **tall** matrix (more rows than columns), like the one above. A spectrum with thousands of wavelengths gives a very **wide** one. In both cases, the ordinary inverse is not the right tool. So instead of solving the system exactly, we look for the best approximate solution, the one that minimizes: $S(\boldsymbol{\beta}) = \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|^2$ This is the sum of squared residuals written as a vector length. ## Deriving the solution To find the minimum, we expand the squared norm: $S(\boldsymbol{\beta}) = (\mathbf{y} - \mathbf{X}\boldsymbol{\beta})^T(\mathbf{y} - \mathbf{X}\boldsymbol{\beta})$ Multiplying it out gives four terms: $S(\boldsymbol{\beta}) = \mathbf{y}^T\mathbf{y} - \mathbf{y}^T\mathbf{X}\boldsymbol{\beta} - \boldsymbol{\beta}^T\mathbf{X}^T\mathbf{y} + \boldsymbol{\beta}^T\mathbf{X}^T\mathbf{X}\boldsymbol{\beta}$ The two middle terms are the same number. Each one is a scalar (a 1 × 1 matrix), and a scalar is equal to its own transpose: $\mathbf{y}^T\mathbf{X}\boldsymbol{\beta} = (\mathbf{y}^T\mathbf{X}\boldsymbol{\beta})^T = \boldsymbol{\beta}^T\mathbf{X}^T\mathbf{y}$. So they merge: $S(\boldsymbol{\beta}) = \mathbf{y}^T\mathbf{y} - 2\boldsymbol{\beta}^T\mathbf{X}^T\mathbf{y} + \boldsymbol{\beta}^T\mathbf{X}^T\mathbf{X}\boldsymbol{\beta}$ Now we use a familiar idea from calculus: > At a minimum of a smooth function, the derivative is zero. Here, the derivative is taken with respect to the whole parameter vector $\boldsymbol{\beta}$, which gives the **gradient**: the vector of partial derivatives, one per parameter. Two rules from vector calculus do all the work. For a constant vector $\mathbf{a}$ and a symmetric matrix $\mathbf{M}$ (and $\mathbf{X}^T\mathbf{X}$ is symmetric): $\frac{\partial (\boldsymbol{\beta}^T\mathbf{a})}{\partial \boldsymbol{\beta}} = \mathbf{a}, \qquad \frac{\partial (\boldsymbol{\beta}^T\mathbf{M}\boldsymbol{\beta})}{\partial \boldsymbol{\beta}} = 2\mathbf{M}\boldsymbol{\beta}$ They are the vector versions of $d(ax)/dx = a$ and $d(mx^2)/dx = 2mx$. Applying them term by term (the first term does not depend on $\boldsymbol{\beta}$): $\frac{\partial S}{\partial \boldsymbol{\beta}} = -2\mathbf{X}^T\mathbf{y} + 2\mathbf{X}^T\mathbf{X}\boldsymbol{\beta}$ At the minimum, the gradient is the zero vector: $-2\mathbf{X}^T\mathbf{y} + 2\mathbf{X}^T\mathbf{X}\boldsymbol{\beta} = \mathbf{0}$ Divide by 2 and rearrange: $\mathbf{X}^T\mathbf{X}\boldsymbol{\beta} = \mathbf{X}^T\mathbf{y}$ This is the key result: the **normal equations** . Whatever the number of samples, this is a small square system: one equation per parameter. If $\mathbf{X}^T\mathbf{X}$ is invertible, we can isolate the parameter vector: $\hat{\boldsymbol{\beta}} = (\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T\mathbf{y}$ This is the classic closed-form solution. It requires $\mathbf{X}^T\mathbf{X}$ to be invertible, which happens only when $\mathbf{X}$ has at least as many rows as columns and no column is a combination of the others (the matrix has *full column rank*). For a wide spectral matrix, with more wavelengths than samples, this is impossible: $\mathbf{X}^T\mathbf{X}$ is a $p \times p$ matrix built from only $n ## Why "normal"? The name has nothing to do with the normal distribution. Move $\mathbf{X}^T\mathbf{X}\hat{\boldsymbol{\beta}}$ to the right-hand side and the normal equations become: $\mathbf{X}^T(\mathbf{y} - \mathbf{X}\hat{\boldsymbol{\beta}}) = \mathbf{0} \qquad \text{or} \qquad \mathbf{X}^T\mathbf{r} = \mathbf{0}$ where $\mathbf{r} = \mathbf{y} - \mathbf{X}\hat{\boldsymbol{\beta}}$ is the vector of residuals. Each row of this equation is the dot product of one column of $\mathbf{X}$ with the residuals, and it is zero: the residual vector is perpendicular, or **normal**, to every column of $\mathbf{X}$. Geometrically, all the vectors $\mathbf{X}\boldsymbol{\beta}$ that the model can produce form a flat subspace (a plane, in the two-column case). Least squares picks the point of that subspace closest to the measurements $\mathbf{y}$, and the shortest way from a point to a plane is the perpendicular. The fitted values are the **orthogonal projection** of $\mathbf{y}$ onto the columns of $\mathbf{X}$: $\hat{\mathbf{y}} = \mathbf{X}\hat{\boldsymbol{\beta}} = \mathbf{H}\mathbf{y}, \qquad \mathbf{H} = \mathbf{X}(\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T$ $\mathbf{H}$ is called the **hat matrix**, because it puts the hat on $\mathbf{y}$. Its diagonal elements $h_{ii}$ measure how strongly each sample pulls the fit towards itself, its **leverage**. You will meet them again in [multiple linear regression](/introduction-to-chemometrics/models/regression-models/multiple-linear-regression) and in [outlier detection](/introduction-to-chemometrics/preprocessing/outliers). ## The pseudoinverse view When $\mathbf{X}$ has full column rank, the expression: $\mathbf{X}^+ = (\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T$ is the **Moore-Penrose pseudoinverse**. It lets us write: $\hat{\boldsymbol{\beta}} = \mathbf{X}^+\mathbf{y}$ That notation is useful because it reminds us that we are not simply inverting $\mathbf{X}$. We are building the best least squares solution for a system that is usually not exactly solvable. When $\mathbf{X}$ is wide or rank deficient, the formula above fails, but the pseudoinverse still exists. It is built from the singular value decomposition (SVD), $\mathbf{X} = \mathbf{U}\boldsymbol{\Sigma}\mathbf{V}^T$: $\mathbf{X}^+ = \mathbf{V}\boldsymbol{\Sigma}^+\mathbf{U}^T$ where $\boldsymbol{\Sigma}^+$ inverts the nonzero singular values and leaves the zero ones at zero . Among the infinitely many least squares solutions, $\mathbf{X}^+\mathbf{y}$ is the one with the smallest length $\|\boldsymbol{\beta}\|$, the **minimum-norm** solution. If you also set the *small* singular values to zero, not only the exact zeros, you get a truncated pseudoinverse, which is [principal component regression](/introduction-to-chemometrics/models/regression-models/principal-component-regression) in disguise. ## How sensitive is the answer? Using a pseudoinverse is standard practice in chemometrics. The real question is how sensitive the estimated coefficients are to noise. A classic example is the Beer-Lambert mixture model used in **classical least squares (CLS)** calibration : $\mathbf{D} = \mathbf{C}\mathbf{K}$ Here $\mathbf{D}$ holds the measured spectra (samples × wavelengths), $\mathbf{C}$ the concentrations (samples × components) and $\mathbf{K}$ the pure-component spectra (components × wavelengths). If the pure spectra are known, we estimate the concentrations with the pseudoinverse of $\mathbf{K}$. But $\mathbf{K}$ is *wide* (few components, many wavelengths), so its pseudoinverse is written the other way round: $\mathbf{K}^+ = \mathbf{K}^T(\mathbf{K}\mathbf{K}^T)^{-1}, \qquad \hat{\mathbf{C}} = \mathbf{D}\mathbf{K}^T(\mathbf{K}\mathbf{K}^T)^{-1}$ These are the normal equations again, written for the transposed problem: each measured spectrum is regressed on the pure spectra, and $\mathbf{K}\mathbf{K}^T$ plays the role of $\mathbf{X}^T\mathbf{X}$. The trouble starts when two columns of the design matrix carry almost the same information, for example when two component spectra in $\mathbf{K}$ are very similar. The fitted values can still be good, but several different coefficient combinations produce almost the same fitted response, so a tiny change in the data can produce a large change in $\boldsymbol{\beta}$. The estimate is still a valid least squares answer; it is just very uncertain, because the data do not strongly distinguish one component from the other. The size of this sensitivity is measured by the **condition number** $\kappa$, the ratio between the largest and the smallest singular value of a matrix. A large $\kappa(\mathbf{X})$ means that some direction in coefficient space is barely determined by the data. Forming $\mathbf{X}^T\mathbf{X}$ makes things worse, because it squares the condition number: $\kappa(\mathbf{X}^T\mathbf{X}) = \kappa(\mathbf{X})^2$ If $\kappa(\mathbf{X}) = 10^4$, then $\kappa(\mathbf{X}^T\mathbf{X}) = 10^8$, and about eight of the sixteen significant digits of double precision are lost before the solve even starts. Methods that work on $\mathbf{X}$ directly, such as QR or the SVD, only suffer $\kappa(\mathbf{X})$. The demo below fits the same model to two designs and then repeats the fit after adding a little noise to the responses. With independent predictors, the coefficients barely move: with this noise level the standard deviation of $\beta_1$ is about 0.03. With two nearly duplicated wavelengths (correlation 0.99998), it jumps to about 3, a hundred times larger, and $\beta_2$ moves just as much in the opposite direction. Their sum, which is what the fitted response depends on, stays as stable as before (about 0.03). That is the warning sign: good predictions, unreliable individual coefficients. Although we write the solution using an inverse, good numerical software never computes that inverse, and usually does not even form $\mathbf{X}^T\mathbf{X}$. `numpy.linalg.lstsq` uses the SVD (LAPACK's `gelsd`); MATLAB's backslash operator on a rectangular matrix uses a QR decomposition; R's `lm` uses a QR decomposition with column pivoting. Forming $\mathbf{X}^T\mathbf{X}$ and solving it with a Cholesky factorization is faster, and perfectly fine when the columns are far from collinear . ## Not only for straight lines The straight line is the easiest drawing, but it is not the real boundary of the method. The real boundary is this: $\text{Can the prediction be written as } \mathbf{X}\boldsymbol{\beta}\text{?}$ If the answer is yes, the normal equations can be used. The graph may be a line, a curve, or a higher-dimensional surface. What matters is that the unknown coefficients appear as a linear combination of columns in the design matrix. For example, this is a straight-line model: $A = \beta_0 + \beta_1 c$ but this curved polynomial is also linear in the parameters: $y = \beta_0 + \beta_1 x + \beta_2 x^2$ The curve bends because one column of $\mathbf{X}$ contains $x^2$. The parameter $\beta_2$ still multiplies that column directly. The same idea works for multiple predictors: $A = \beta_0 + \beta_1 c + \beta_2 T$ where one column may represent concentration and another may represent temperature. More predictors simply mean more columns in $\mathbf{X}$. This is why "linear regression" can sometimes look nonlinear on a plot. Polynomial regression is linear regression in an expanded design matrix. Multiple linear regression is still linear regression, just with several measured variables contributing at the same time. The method stops being directly applicable when the parameters enter the model in a genuinely nonlinear way, for example: $y = a e^{-kx} + y_0$ Here $k$ is inside the exponential, so the model cannot be solved in one normal-equation jump. That kind of problem belongs to nonlinear least squares methods such as [Gauss-Newton](/introduction-to-chemometrics/least-squares/gauss-newton) or [Levenberg-Marquardt](/introduction-to-chemometrics/least-squares/levenberg-marquardt). Notice, though, that if you fix $k$, the model is linear in $a$ and $y_0$, with columns $e^{-kx}$ and $1$. Some nonlinear solvers exploit this, a trick known as *variable projection*: they search only over $k$ and solve for the linear parameters with a normal-equation step at each trial value. There is also a numerical limit. Even when the model is linear in the parameters, solving the normal equations directly can become unstable if $\mathbf{X}^T\mathbf{X}$ is singular or nearly singular, and expensive when there are very many parameters (the matrix has $p^2$ entries, and forming it takes about $np^2$ operations). That is where the iterative methods of the next entries, [gradient descent](/introduction-to-chemometrics/least-squares/gradient-descent) and [conjugate gradient](/introduction-to-chemometrics/least-squares/conjugate-gradient), come in. ## Code examples The examples fit the calibration line of the interactive demo in two ways: with the normal equations, as derived above, and with the least squares routine you would use in practice. Both print an intercept of about 0.009 and a slope of about 2.503. ```python c = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) A = np.array([0.03, 0.49, 1.01, 1.48, 2.04, 2.51]) X = np.column_stack([np.ones_like(c), c]) # Normal equations: form X^T X and X^T y, then solve beta = np.linalg.solve(X.T @ X, X.T @ A) # What you would use in practice: least squares straight from X (SVD) beta_lstsq, *_ = np.linalg.lstsq(X, A, rcond=None) print(beta) # [0.00857143 2.50285714] print(beta_lstsq) # same numbers ``` ```matlab c = [0.0; 0.2; 0.4; 0.6; 0.8; 1.0]; A = [0.03; 0.49; 1.01; 1.48; 2.04; 2.51]; X = [ones(size(c)), c]; % Normal equations: form X'X and X'y, then solve beta = (X' * X) \ (X' * A); % What you would use in practice: backslash on X itself (QR) beta_qr = X \ A; disp([beta, beta_qr]) % 0.0086 and 2.5029 in both columns ``` ```r conc # Least Squares: optimization (/introduction-to-chemometrics/least-squares/optimization) In the nineteenth century, astronomers were trying to determine orbits from limited and noisy observations. Instruments were improving, but observations still disagreed with one another. A comet or small planet might be visible only for a short time, and the available measurements were not enough to satisfy every equation exactly. In 1805, Adrien-Marie Legendre presented least squares as a practical method for problems with more equations than unknowns. In that setting, the errors usually cannot all be made zero. His rule was to choose the unknown parameters that make the sum of squared errors as small as possible . The calculation he derived is what we now recognize, in the linear case, as the normal-equation route. In modern notation, a linear least-squares problem leads to: $\mathbf{X}^T\mathbf{X}\boldsymbol{\beta} = \mathbf{X}^T\mathbf{y}$ Gauss entered the same problem through the orbit of Ceres. The object was discovered in January 1801 and then lost as it moved too close to the Sun. From a short set of observations, Gauss predicted where it would be found again, and Ceres was recovered in December 1801. Gauss later claimed that he had used least squares since 1795, but he did not publish the method before Legendre . The interactive example below is a simplified two-dimensional Kepler model. It is not a reconstruction of Gauss's numerical tables, but it keeps the essential structure: fit an orbit to angular observations, project the orbit back onto the sky, and check the later predicted position. In orbit determination, the parameters enter the observations nonlinearly through the geometry of motion. In practice, Gauss worked with corrections to an approximate orbit: he linearized the observational equations, solved for a least-squares correction, and updated the parameters. In modern terms, that workflow sits in the same family as linearized least squares and later Gauss-Newton-type methods. ![Legendre and Gauss in the history of least squares](/images/legendre-gauss-least-squares-origin.png) Legendre treated least squares mainly as an approximation method and showed how the criterion leads to equations for the unknown parameters. Gauss connected the same criterion to probability and observational error. Under normally distributed errors, minimizing squared residuals is equivalent to maximizing likelihood . So the early story already contains a computational answer for linear problems: minimizing the sum of squares leads to equations for the unknown parameters. Today we call that route the normal equations. But the normal equations are only one route. The same least-squares objective can also be solved by QR decomposition, SVD, gradient descent, conjugate gradient, Gauss-Newton, or Levenberg-Marquardt. The question of this series is therefore more specific: > **Which computational route should we use to minimize the least-squares error?** If you have ever built a calibration curve, fitted a spectrum, or used a regression model in a software package, you have already used one of these routes. The software gives you coefficients, predicted values, residuals, and maybe confidence intervals. Underneath, it has chosen an algorithm for solving the least-squares problem. This small series splits that computational story into separate methods. Each method gives a different way to reduce a squared-error criterion: sometimes by solving it exactly, sometimes by improving the parameters step by step, and sometimes by solving a local approximation. ## The methods There are many ways to solve least squares problems, but the methods in this series fall into two broad families . | Family | How it works | Typical methods | | :---- | :---- | :---- | | **Direct methods** | Find the answer from algebra in one main step | Normal equations, QR, SVD | | **Iterative methods** | Start with a guess and improve it step by step | Gradient descent, conjugate gradient, Gauss-Newton, Levenberg-Marquardt | The articles below follow that path from the simplest closed-form solution to more flexible iterative methods. ## Reading order 1. [Normal equations](/introduction-to-chemometrics/least-squares/normal-equations/) show the classic closed-form solution for linear least squares. This is the neat formula many of us first meet in regression. 2. [Gradient descent](/introduction-to-chemometrics/least-squares/gradient-descent/) solves the same problem by walking downhill on the error surface. It is slower in small problems, but essential for understanding modern optimization. 3. [Conjugate gradient](/introduction-to-chemometrics/least-squares/conjugate-gradient/) improves on simple gradient descent for large linear systems by choosing smarter search directions. 4. [Gauss-Newton](/introduction-to-chemometrics/least-squares/gauss-newton/) moves into nonlinear least squares by repeatedly replacing a curved model with a local linear approximation. 5. [Levenberg-Marquardt](/introduction-to-chemometrics/least-squares/levenberg-marquardt/) adds damping to Gauss-Newton, making nonlinear fitting more stable when the starting guess is not perfect. ## Choosing the right door For a small calibration curve, the normal equations or a numerically stable variant such as QR decomposition is usually enough . For a large spectral dataset, iterative methods become attractive because they avoid forming and inverting very large matrices . For nonlinear models, such as exponential decay or Michaelis-Menten kinetics, direct linear least squares no longer applies and methods like Gauss-Newton or Levenberg-Marquardt become much more natural . The important thing is that the goal has not changed. All these methods are still trying to make the residuals small. They simply differ in how they travel toward that minimum. ## References # Least Squares: your first step into chemometrics (/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics) **Least squares** forms the mathematical backbone of modern analytical chemistry. If you work in an analytical lab, you’ve probably used least squares regression more times than you’ve counted. It’s there when you build calibration curves from spectroscopic data. It helps you quantify analytes in messy samples. In chromatography, it helps you get numbers from the peaks in your data. If you use near-infrared data, you may use it to build partial least squares models. Even if you don’t think about it, least squares is a big part of the work you do in the lab. Take a look at the interactive panel below. I'm willing to bet you've used at least one of those fitting methods during a typical lab day: We learn that *least squares* means minimize the sum of squared errors, $\min \sum_{i=1}^{I} (y_i - \hat{y}_i)^2$ but we rarely pause to ask: ***where does the square actually come from? Why this expression and not another one?*** The answer is not trivial, and it is related to fundamental properties of the measurements. In this entry, we will derive, step by step, where the square in least squares actually comes from. Along the way, we will explore each concept and mathematical step clearly, so we can understand why the squared error appears naturally in this type of optimization. More importantly, we will see how the core assumptions behind least squares relate directly to the physical reality of the measurements we make in the laboratory. Ah, just a last thing before starting: even if some of the equations here might look like monsters at first glance, I promise they are very simple at heart. Don’t be afraid! We’re really just talking about sums and multiplications. ## Why nature chooses squares I still remember the moment my high school mathematics teacher first introduced least squares. He stood at the blackboard, drawing a scatter plot of data points, and explained how we could find the *"best fit"* line by minimizing something called the *"sum of squared errors."* The idea that we could mathematically determine the optimal line through messy points was fascinating. But there was one thing that absolutely bothered me, and I couldn't let it go. ***"Why do we square the differences?"*** I asked, raising my hand. *"“Why not raise the differences to the third power instead?"* My teacher gave the standard textbook answer: *"Well, we square them to avoid cancellations. If we just added up the differences without squaring, positive and negative errors would cancel each other out."* Reasonable. But that explanation left me even more confused. *"But if we're worried about cancellations, why not raise them to the power of 4? Or 6? Or 8? or the absolute value: $|y_i - \hat{y}_i|$. That seems much more natural to me. Why specifically the power of 2?"*. In fact, minimizing absolute errors or other powers of the error is possible, and sometimes it is exactly the right choice: absolute values are more robust to large deviations, while higher powers punish large errors more strongly. Although I did not receive that answer at the time, in practice we usually use the square. My teacher looked slightly uncomfortable and said something about squares being *"mathematically convenient"*. I didn't (and I still don't) like convenient things, so that question nagged at me for years. It wasn't until much later, when I discovered an incredible explanation by Artem Kirsanov in his YouTube video on the probabilistic foundations of least squares (see also García-Portugués ), that I finally understood the beautiful mathematics behind that seemingly arbitrary choice: the answer emerges from key assumptions about **probabilities** and the **nature of the measurement**. ## The reality of analytical measurements Before we start digging into why the least squares expression has the shape it does, we need to understand something fundamental about analytical measurements: **they're never perfect, and it's not your fault!** For instance, imagine you're a quality control chemist in a pharmaceutical company, rigorously testing a batch of an analgesic to ensure each dose contains the correct amount of acetylsalicylic acid. You take a precisely diluted sample of the solution and place it in a spectrophotometer (try to use the interactive component below to take some measurements!). So, let´s assume that the perfect measurement for this specific case is 1.20 AU. But when you measure, your results are a bit different, like 1.25 AU, 1.19 AU, 1.22 AU, or 1.17 AU, as you can observe in the interactive component above. These small variations aren't due to carelessness or poor technique. They represent the fundamental reality of *measurement*: >In spectroscopy, thermal noise causes random motion of electrons in detector circuits, creating small voltage fluctuations that add uncertainty to your signal. In optical measurements, photon noise arises from the quantum mechanical reality that light arrives at your detector in discrete packets, leading to statistical variations in the number of photons counted. In chromatography, you encounter variations in mobile phase flow rate, small temperature fluctuations, and the stochastic nature of molecular interactions between analytes and the stationary phase. Even in electrochemical measurements, thermal motion of ions creates random current fluctuations that appear as noise in your signal. **That's the nature of measurements: an intrinsic property that cannot be avoided**. **This random fluctuation is what we call noise**, $\varepsilon$, and it's present in every analytical technique you'll ever use. Let's express this mathematically. When you make a measurement $y_n$, what you observe is: $y_n = \hat{y} + \varepsilon_n$ where $\hat{y}$ is the underlying value you're trying to measure (1.20 AU), and $\varepsilon_n$ represents one specific noise measurement from the underlying $\varepsilon$ noise distribution. You can imagine a distribution as a bag containing all possible values of $\varepsilon$, each one with its own probability of being picked. Every time you take a measurement, you blindly reach into the bag and pull out one random value $\varepsilon_n$, which gets added to the true value. Small values (close to zero) are usually the most likely to be picked, so most of your readings land near 1.20 AU. Larger values are rarer, but they still show up from time to time. The shape of this probability bag, whether the values are clustered tightly or spread out, symmetric or skewed, is what defines the distribution. The interactive demonstration below lets you explore this: take enough measurements, and you'll start to see the shape of the bag. Let's visualize how our measurements are distributed. The histogram above shows how often each absorbance value appears (its frequency). The horizontal axis groups measurements into small intervals, and the vertical axis counts how many readings fell into each one. Hit Play and watch: at first, the bars look scattered and messy. But as you collect more and more measurements, a pattern emerges. The bars start forming a familiar bell shape, revealing the underlying distribution that was there all along. The red line shows the true Gaussian distribution that our noise follows. The shape of this distribution depends on the dominant noise sources in your particular analytical system. It might be Gaussian, Poisson, uniform, or something else entirely. ## Towards the least squares expression Now, as we move from understanding the physical reality of analytical measurements to developing the mathematical framework of least squares, i.e., from reality to statistics, we need to make an important distinction in terminology. The **noise** we've been discussing **represents the physical sources of uncertainty in your measurement**. These physical processes create what we mathematically call **measurement errors**: **the difference between what you observe and the true value** you're trying to measure. The equation $y_n = \hat{y} + \varepsilon_n$ describes the same physical reality from two viewpoints. From the physical perspective, $\varepsilon_n$ represents the combined effect of all noise sources in your instrument and $\hat{y}$ is the underlying true value. From the mathematical perspective, $\varepsilon_n$ represents the error term we need for statistical modeling and $\hat{y}$ our model value. This distinction matters because the statistical assumptions we're about to discuss (normality, independence, constant variance) apply specifically to these mathematical error terms $\varepsilon_n$. But these mathematical assumptions are justified by understanding how the underlying physical noise sources actually behave in real analytical instruments. Therefore, from this point forward in our mathematical development, when we write $\varepsilon_n$ and $\hat{y}$ in our equations, we're referring to these mathematical error terms and our model values, respectively. ### From measurement errors to probabilities We've established that every measurement we take contains some random error. But how does this fact lead us to the least squares formula? It starts by asking a simple question: >**Given the data points we measured, what is the most likely true value or model that produced them?** We are no longer just trying to draw a line through some points. We are looking for the physical truth that makes our specific measurements **the most probable** outcome. This way of thinking will lead us directly to the famous squares. To get there, we need three ingredients: 1. **[Assume a probability distribution for our errors](#step-1-the-probability-of-one-measurement):** We already argued that Gaussian is a reasonable choice. Now we'll write down the probability of a single measurement. 2. **[Combine the probabilities of all our measurements](#step-2-the-probability-of-the-whole-dataset):** If we have many data points, we need a way to compute the probability of observing the entire dataset at once. This is where we shift our perspective from individual measurements to a global, probabilistic view. 3. **[Assume how our data spread around the model](#step-3-the-role-of-constant-variance):** We'll make one final assumption about how the dispersion of our measurements behaves, and that will simplify everything into the familiar least squares formula. Let's go step by step. ### Step 1: The probability of one measurement Let's start by looking at just one measurement through a probabilistic lens. We assume we have our model prediction, which we denote as $\hat{y}$ and which should coincide ideally with the **true underlying value**. However, as stated before, when you perform the measurement, you observe the value $y_n$. The difference between what you observe and our model is the **measurement error**, which we define as: $\varepsilon_n = y_n - \hat{y}$ This error $\varepsilon_n$ is what we assume comes from a random distribution, reflecting the inherent noise in your measurement process. Then: **What is the probability of getting this specific error, given an underlying value $\boldsymbol{\hat{y}}$ ?** To answer this, we need to make our first assumption about how these errors behave. We assume that the measurement errors, $\varepsilon_n$, follow a bell-shaped curve, known as a Gaussian or Normal distribution, with zero mean. We write this as $\varepsilon_n \sim N(0, \sigma^2)$. Why this distribution and not another one? This is not a random choice, and it connects directly to the noise sources we discussed earlier. Think about what happens inside a spectrophotometer when you take a single reading. Thermal noise in the electronics, photon counting statistics, tiny mechanical vibrations, temperature fluctuations... all of these small, independent effects add up to produce the final error in your measurement. No single source dominates; it's the combination of many little contributions. This is exactly the scenario where a famous mathematical idea, the **Central Limit Theorem** , applies. It tells us that when you add up many small, independent random effects, their sum is **well-approximated by a Gaussian distribution**, regardless of how each individual effect is distributed. Even if photon noise alone follows a Poisson distribution (which is not Gaussian at all), once it combines with all the other noise sources and you accumulate enough counts, the overall error ends up looking Gaussian. So the Gaussian assumption is very reasonable for most analytical instruments, as long as the noise sources are independent, none is overwhelmingly large, and each has a finite variance. The interactive demonstration below illustrates this idea in a simplified way. Instead of combining different noise sources (which is hard to visualize), it shows the same principle from a different angle: what happens when you **average** multiple random values. The math behind both cases is the same. Whether you are adding up many different noise sources within a single measurement, or averaging many replicate measurements together, the Central Limit Theorem applies in both situations, and the result tends toward a Gaussian. Move the slider to increase $n$, the number of random values being averaged. Watch how the histograms evolve: despite starting from a perfectly uniform or a heavily skewed distribution, the averages converge toward the same symmetric, bell-shaped Gaussian curve. This is the Central Limit Theorem in action, and the reason why assuming Gaussian errors is often reasonable in analytical work. Here's a concrete example from the lab. Think about a detector in a spectrophotometer counting photons. When very few photons arrive (a weak signal, low signal-to-noise ratio), the counts fluctuate in an asymmetric, skewed way: you might count 3, 5, 2, 7 photons in successive readings. That pattern of variation is not Gaussian at all. But your instrument doesn't just report one raw count. It accumulates photons over an integration time, effectively averaging thousands or millions of individual detection events. By the time the instrument displays a number on the screen, it has already averaged so many counts together that the Central Limit Theorem has done its work: the final reading fluctuates symmetrically around the true value, following a Gaussian distribution. The same thing happens with electronic noise in the detector circuit, where the instrument averages many rapid voltage samples into a single stable reading. So in practice, the Gaussian assumption is not something we impose artificially. It emerges naturally from how instruments work. The formula for this bell curve is: $p(y_n \mid \hat{y}_n,\sigma^2)=\frac{1}{\sqrt{2\pi\sigma^2}} e^{\displaystyle -\frac{\varepsilon_n^2}{2\sigma^2}}$ Where $p(y_n \mid \hat{y}_n,\sigma^2)$ means *"the probability density of observing measurement $y_n$ given that what we expect is $\hat{y}_n$."* The vertical bar $\mid$ means *"given that"* or *"conditional on"*. That's exactly the shape you saw on the interactive histogram above, and it tells us that small errors (when $\varepsilon$ is close to zero) are very probable, and large errors are very improbable. If we replace the general error $\varepsilon_n$ with our specific measurement error, $y_n - \hat{y}$, we get the probability of observing our data point, $y_n$: $p(y_n \mid \hat{y}_n,\sigma^2)=\frac{1}{\sqrt{2\pi\sigma^2}} e^{\displaystyle -\frac{(y_n - \hat{y}_n)^2}{2\sigma^2}}$ ### Step 2: The probability of the whole dataset In a real experiment (for example, a calibration curve), you have more than one data point. You have a whole set of measurements: $y_1, y_2, \ldots, y_I$. The next question we must ask ourselves is: **what is the probability of observing the entire collection of measurements at once?** To answer it, we need to understand how the errors of different measurements are related. This leads to our **second assumption**. We assume that the error in one measurement gives you no information about the error in the next measurement. They are **independent**, so we can multiply their likelihood contributions. Think of it like flipping a coin. The result of the first flip does not change the 50/50 probability for the second flip. Our measurements should be the same. If you rinse your equipment properly between samples and allow your instrument to stabilize, then each measurement is a new, independent event. The random noise that made your first measurement slightly high should not affect your second measurement. Because the errors are independent, we can calculate the total probability of the whole dataset by multiplying the individual probabilities of each measurement: $P(\text{entire dataset}) = P(y_1) \times P(y_2) \times \ldots \times P(y_I) = \prod_{i=1}^{I} P(y_i | \hat{y}_i)$ This joint probability is usually called the **likelihood**. Now, let's substitute the Gaussian probability formula we found in Step 1: $P(\text{entire dataset}) = \prod_{i=1}^{I} \left[ \frac{1}{\sqrt{2\pi\sigma_i^2}} e^{\displaystyle -\frac{(y_i - \hat{y}_i)^2}{2\sigma_i^2}} \right]$ That equation is a monster! But don't be afraid, it represents just $I$ likelihood contributions being multiplied: the probability density of observing our entire dataset, expressed as a product of individual Gaussian terms. Now, our goal is to find the model parameters that **maximize this probability**. The logic is beautifully simple: we want to find the model that makes our actual experimental data **the most likely outcome**. If you measured a series of absorbance values, you want to find the calibration curve parameters that make those specific measurements the most probable result you could have gotten. This approach is called *"maximum likelihood estimation"* and it's based on a very reasonable principle: **the best model** is the one that makes your observed data **most likely to occur**. However, working directly with this product of complex exponential expressions is mathematically challenging. When you multiply many small probabilities together, the product becomes extraordinarily tiny that computers may treat the result as zero due to limited numerical precision. This *"numerical underflow"* can completely break your optimization algorithm. If we consider 1000 points (a typical number in a single infrared spectrum), multiplying many small **density values** can produce numbers so tiny (e.g., on the order of $10^{-1000}$ or smaller) that most computers treat them as zero due to limited numerical precision. Moreover, derivatives are important in optimization problems. But applying derivatives to products may be a nightmare. Fortunately, there's a trick that we can use. We can apply a **logarithm transformation** to this product of probabilities. The logarithm function is **monotonic**, i.e., it always increases in the same direction as its input. Think of it like this: if you have two probabilities where P_B"/>, then you're guaranteed that \log(P_B)"/>. **The logarithm preserves the relative ordering**, which means that the parameter values that maximize our original probability will be exactly the same parameter values that maximize the logarithm of that probability. Applying this transformation to our probability product, and using the fundamental property that $\log(A \times B) = \log(A) + \log(B)$, this becomes: $\log(P(\text{entire dataset})) = \log\left(\prod_{i=1}^{I} P(y_i|\hat{y}_i)\right) = \sum_{i=1}^{I} \log(P(y_i|\hat{y}_i))$ The multiplication of probabilities now is a sum! Now, remember that each individual probability has the form: $P(y_i|\hat{y}_i) = \frac{1}{\sqrt{2\pi\sigma_i^2}} e^{\displaystyle -\frac{(y_i - \hat{y}_i)^2}{2\sigma_i^2}}$ And our goal is to find the model parameters that **maximize** the total log-likelihood: $\max_{\boldsymbol{\beta}} \sum_{i=1}^{I} \log P(y_i \mid \hat{y}_i(\boldsymbol{\beta}))$ Here, $\boldsymbol{\beta}$ represents the parameters we *optimize* to maximize the log-likelihood. For example, in a **linear regression** model, $\boldsymbol{\beta}$ includes the *slope* and *intercept*. In an **exponential decay** model, $\boldsymbol{\beta}$ consists of the *pre-exponential factor* and the *decay rate*. These parameters define the predictions $\hat{y}_i(\boldsymbol{\beta})$, and the goal is to find the values of $\boldsymbol{\beta}$ that make the observed data most likely. When we substitute the Gaussian probability expression and take the logarithm, we apply the logarithm to both parts of the product inside each probability. Using the property that $\log(A \times B) = \log(A) + \log(B)$, this becomes: $\max_{\boldsymbol{\beta}} \sum_{i=1}^{I} \log\left( \frac{1}{\sqrt{2\pi\sigma_i^2}} \cdot e^{\displaystyle -\frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{2\sigma_i^2}} \right)$ and then $\max_{\boldsymbol{\beta}} \sum_{i=1}^{I} \left[ \log\left(\frac{1}{\sqrt{2\pi\sigma_i^2}}\right) + \log\left(e^{\displaystyle -\frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{2\sigma_i^2}}\right) \right]$ When we take the logarithm of the exponential term, we use the fundamental property that $\log(e^x) = x$. This means: $\log\left(e^{\displaystyle -\frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{2\sigma_i^2}}\right) = -\frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{2\sigma_i^2}$ This simplification transforms our expression into: $\max_{\boldsymbol{\beta}} \sum_{i=1}^{I} \left[ \log\left(\frac{1}{\sqrt{2\pi\sigma_i^2}}\right) - \frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{2\sigma_i^2} \right]$ Now we can separate this expression into two distinct parts to better understand what each component contributes to our optimization problem: $\max_{\boldsymbol{\beta}} \left[ \sum_{i=1}^{I} \log\left(\frac{1}{\sqrt{2\pi\sigma_i^2}}\right) - \sum_{i=1}^{I} \frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{2\sigma_i^2} \right]$ This separation reveals something profound about the structure of our optimization problem. The first sum contains only normalization constants that depend on our measurement uncertainties $\sigma_i^2$ but **not** on our model parameters $\boldsymbol{\beta}$. Since these terms remain constant regardless of which model parameters we choose, they don't affect which parameters give us the maximum likelihood. For optimization purposes, we can treat this entire first sum as a constant. Therefore, maximizing the full expression is mathematically equivalent to maximizing only the second part: $\max_{\boldsymbol{\beta}} \left[ - \sum_{i=1}^{I} \frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{2\sigma_i^2} \right]$ But here's a fundamental insight about optimization that connects our probabilistic reasoning to the familiar least squares method: maximizing a negative expression is exactly the same as **minimizing** the positive version of that expression. Since we want to find the maximum of the negative sum, we can equivalently find the minimum of the positive sum: $\min_{\boldsymbol{\beta}} \sum_{i=1}^{I} \frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{2\sigma_i^2}$ Finally, since the factor of $\frac{1}{2}$ doesn't change which parameters minimize the expression (it just scales everything uniformly), we can remove it without affecting the optimization result: $\min_{\boldsymbol{\beta}} \sum_{i=1}^{I} \frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{\sigma_i^2}$ At this point, we've established that maximizing the likelihood of our data leads us to minimize the expression: This is called **weighted least squares**, a more general form than ordinary least squares. Each measurement contributes to the sum with a weight that depends on its individual measurement uncertainty. Measurements with smaller uncertainties (smaller $\sigma_i^2$) get more weight in determining our final model, while noisier measurements (larger $\sigma_i^2$) get less influence. This makes intuitive sense: we should trust our most precise measurements more heavily when fitting our model. If errors are correlated with each other, the broader extension is **generalized least squares**, which uses a covariance matrix rather than independent pointwise weights. But here's where we encounter a practical challenge that leads us to our third fundamental assumption. In real analytical work, how often do you actually know the individual uncertainty $\sigma_i^2$ for each and every measurement you make? ### Step 3: The role of constant variance In most laboratory situations, you don't measure the uncertainty of each individual data point. Instead, you typically characterize your instrument's overall precision through replicate measurements or method validation studies. You might determine that your HPLC system has a typical injection precision of 2% RSD, or that your spectrophotometer has a baseline noise level of 0.01 absorbance units. This practical reality leads us to a crucial simplifying assumption. We assume that all measurements have the same underlying uncertainty: $\sigma_i^2 = \sigma^2$ for all measurements. The word "**homoscedasticity**" comes from Greek, meaning "same scatter." Let's explore what this assumption really means in concrete terms. Imagine you're running a calibration curve for quantifying caffeine using UV spectroscopy. **Homoscedasticity** means that when you measure a 1 mg/L standard solution, the measurement uncertainty is essentially the same as when you measure a 2 mg/L standard or a 3 mg/L standard. **The absolute magnitude of the random error stays approximately constant across your entire calibration range**. This might seem counterintuitive at first. You might expect that higher concentrations, which give stronger signals, would be more precise. But remember that we're talking about **absolute error**, not **relative error**. Your spectrophotometer's detector noise might contribute ±0.01 absorbance units of uncertainty whether you're measuring 0.2 AU or 1.0 AU. The absolute uncertainty stays roughly the same, even though the relative uncertainty (the percentage error) decreases for stronger signals. The interactive component below demonstrates this concept using a linear calibration relationship. By default, it shows homoscedastic data where error bars maintain constant height across all concentrations. Toggle the *"Heteroscedastic noise"* option to see what happens when measurement uncertainty increases with signal strength, and notice how the error bars grow larger at higher concentrations, violating our constant variance assumption. This comparison is crucial for understanding when our mathematical assumptions are valid. In the homoscedastic case, you can see that the scatter around the line remains roughly constant whether you're looking at low or high concentrations. In the heteroscedastic case, the scatter clearly increases as concentration increases, which would require us to use weighted least squares rather than ordinary least squares. Now let's see how the homoscedasticity assumption transforms our mathematics. When every measurement has the same variance $\sigma^2$, we can factor this constant out of our summation: $\min_{\boldsymbol{\beta}} \sum_{i=1}^{I} \frac{(y_i - \hat{y}_i(\boldsymbol{\beta}))^2}{\sigma^2} = \min_{\boldsymbol{\beta}}\frac{1}{\sigma^2} \sum_{i=1}^{I} (y_i - \hat{y}_i(\boldsymbol{\beta}))^2$ Here's the key mathematical insight: since $\frac{1}{\sigma^2}$ is just a positive constant that doesn't depend on our model parameters, it doesn't affect which parameters minimize the expression. Multiplying an entire function by a positive constant doesn't change the location of its minimum. It just rescales the function vertically. Therefore, minimizing our expression is mathematically equivalent to minimizing: $\min_{\boldsymbol{\beta}} \sum_{i=1}^{I} (y_i - \hat{y}_i(\boldsymbol{\beta}))^2$ And there it is!🎉 We've finally arrived at the familiar **ordinary least squares** expression that you've probably encountered countless times in analytical chemistry. The squares that seemed so mysterious at the beginning of our journey have emerged naturally from three reasonable assumptions about how analytical measurements behave: 1. **Gaussian errors**: Measurement errors follow a bell-shaped distribution, which reflects the Central Limit Theorem acting on many small, independent noise sources in your instrument. 2. **Independent measurements**: The error in one measurement doesn't predict the error in the next, which reflects good laboratory practice and proper instrument operation. 3. **Constant variance**: All measurements have similar absolute precision, which reflects the typical behavior of most analytical instruments across their useful range. Understanding these nuances helps you choose the right mathematical approach for your specific analytical situation. When homoscedasticity is reasonable, ordinary least squares provides an elegant and computationally efficient solution. When it's not, weighted least squares offers a more sophisticated approach that properly accounts for varying measurement uncertainties. This mathematical framework elegantly connects the physics of measurement (random errors with Gaussian distributions) to the statistics of parameter estimation (maximum likelihood) to the practical mathematics we use every day (least squares fitting). The beauty lies not just in the mathematical elegance, but in how these assumptions reflect the real behavior of analytical instruments and the practical constraints of laboratory work. ### Some considerations While our probabilistic derivation explains why least squares uses the sum of squared errors, there are additional assumptions and practical considerations that every analytical chemist should understand to apply this method effectively in real laboratory work. These considerations will help you recognize when least squares is appropriate, when it might fail, and how to adapt your approach accordingly. ## Key Assumptions for Linear Models Our derivation focused on three fundamental assumptions for the sake of simplifying the explanation, but the classical least squares framework, especially for linear models, rests on a few key assumptions. --- Your model must be linear in the parameters you're trying to estimate. This assumption is what makes least squares problems solvable in closed form and is a requirement for theorems like Gauss–Markov. The key to understanding *"linear in parameters"* is recognizing what makes a model linear or non-linear from a mathematical perspective. **A model is linear in parameters when each parameter appears only to the first power and no parameters multiply each other.** Consider these examples from analytical chemistry: **Linear in Parameters:** - Simple calibration: $y = a + bx$ ✓ (parameters $a$ and $b$ each appear once, no multiplication between them) - Polynomial calibration: $y = a + bx + cx^2 + dx^3$ ✓ (even though the relationship between $y$ and $x$ is curved, parameters $a$, $b$, $c$, $d$ are independent) - Multiple regression: $y = a + b_1x_1 + b_2x_2 + b_3x_3$ ✓ (each parameter has its own coefficient role) **Non-linear in Parameters:** - Exponential decay: $y = a \cdot e^{-bx}$ ✗ (parameter $b$ appears inside an exponential function) - Michaelis-Menten kinetics: $y = \frac{V_{max} \cdot x}{K_m + x}$ ✗ (parameters $V_{max}$ and $K_m$ interact in complex ways) - Interactive model: $y = a + bx + c \cdot a \cdot x$ ✗ (parameters $a$ and $c$ multiply each other) Even when your model is **not** linear in the parameters, it is still possible to **minimize the sum of squared errors** using iterative methods like gradient descent or Levenberg-Marquardt. However, in this case you lose the ability to write the model globally in the simple matrix form $\hat{\mathbf{y}} = \mathbf{X} \boldsymbol{\beta}$, and therefore also lose direct access to closed-form solutions and their associated geometric interpretation, such as projections. That said, these linear algebra techniques are not entirely lost: they are typically applied locally at each iteration through linear approximations of the model. --- While this condition was already included in our notation for Assumption 1 ($\varepsilon_i \sim N(0, \sigma^2)$), it's a critical concept worth stating on its own. The mathematical expectation of your measurement errors is assumed to be zero: $E[\varepsilon_i] = 0$. This assumption is crucial for making your parameter estimates unbiased, meaning they will correctly estimate the true underlying parameters without systematic error. In practical terms, this assumption means that if you could take an infinite number of measurements of the same sample under identical conditions, the average of all measurement errors would approach zero. Sometimes your instrument reads slightly high, sometimes slightly low, but there's no consistent tendency in either direction. This assumption essentially requires that you've eliminated systematic errors through proper analytical technique. 1. **Gaussian errors**: Measurement errors follow a normal (bell-shaped) distribution 2. **Independence**: Each measurement's error is unrelated to any other measurement 3. **Homoscedasticity**: Variance of errors remains constant across all measurements 4. **Linearity**: The model is linear in its parameters (not necessarily in variables) 5. **Zero mean errors**: Errors average to zero over many measurements (no systematic bias) #### When assumptions break down and what to do about it Understanding when least squares assumptions fail helps you recognize situations where alternative approaches might be more appropriate. Each violation has characteristic symptoms and established solutions. **Heteroscedasticity** occurs when measurement precision changes across your analytical range, as you saw in the interactive component above. You might observe this in trace analysis where measurements near the detection limit have proportionally larger uncertainties than measurements at higher concentrations. In Time-Correlated Single Photon Counting (TCSPC), heteroscedasticity can arise due to variations in photon count statistics: measurements with fewer photon events near the baseline tend to exhibit higher relative uncertainty compared to peak regions with more photon events. In chromatography, peak integration precision often depends on peak height, creating heteroscedastic errors. When you suspect heteroscedasticity, examine residual plots where errors are plotted against predicted values. If you see a funnel shape or other systematic pattern in the residuals, consider weighted least squares where each measurement is weighted by the inverse of its estimated variance . Violating the assumption of constant variance (homoscedasticity) does not usually bias the parameter estimates themselves in ordinary least squares, but it does bias the standard errors, which in turn affects confidence intervals and hypothesis tests. This can lead to incorrect inferences, such as overstating the significance of predictors. In cases where heteroscedasticity is substantial and ignored, the model may appear more precise than it truly is. For example, confidence intervals for regression coefficients (computed under constant-variance formulas) can be biased. **Non-Gaussian errors** often show up in real lab work. For example, when you count photons at very low light levels, the random noise follows a Poisson curve and looks skewed to the right. A dirty cuvette or a brief detector glitch can throw in a few big outliers, making the tails of the error curve very fat. And when results fall below your detection limit, you report “non-detect,” which cuts off part of the distribution on the left. Most textbooks and software **assume a neat bell-shaped (Gaussian) error** because it makes the algebra tidy. The good news is that ordinary least squares still gives the correct *average* answer as long as the errors keep the same spread, are not linked from one measurement to the next, and average out to zero in the long run. Where non-Gaussian errors cause trouble is in the fine details: small-sample confidence limits and the usual $t$ and $F$ tests can be off. When that happens, you can switch to a robust fit, apply variance-stabilizing transformations (such as square root for Poisson count data), or pick a model that matches the real error distribution, such as Poisson regression. Least-squares treats a Gaussian curve as the *default* error shape, but the method stays reliable if the errors are zero-mean, have roughly constant spread, and do not depend on each other. The bell curve mainly matters for textbook formulas that give exact confidence limits in small data sets. **Autocorrelated errors** violate the independence assumption and commonly occur in time-series measurements where drift or memory effects cause sequential measurements to be related. You might see this in continuous monitoring applications or when measuring samples in sequence without adequate equilibration time. Autocorrelation can be detected by plotting residuals against measurement order and looking for patterns. Solutions include incorporating time-trend terms into your model or using time-series regression methods. ## Conclusion We began with a simple question: why do we use the "sum of squares"? As we saw, the explanation from a probability viewpoint comes from a few key ideas about our measurements: that errors follow a bell curve (a Gaussian distribution), that they are independent, and that the measurement precision stays the same. This approach helps us understand the real-world reason why the method works. To get a more complete picture, it is also useful to mention the **Gauss-Markov theorem** . This theorem shows that, if (1) the model is linear in its parameters, (2) the errors have mean zero, (3) the errors share the same variance (homoscedasticity), and (4) the errors are independent (uncorrelated), the ordinary-least-squares (OLS) estimator is the **BLUE**: the *Best Linear Unbiased Estimator*. "Best" means it has the smallest possible variance among all estimators that are linear in the data and unbiased. Unlike earlier steps in our story, the Gauss–Markov proof does not require the errors to follow a perfect bell curve (Normal distribution); the four conditions above are enough. That wider set of rules makes the least-squares method even more solid and trustworthy in everyday work. If you understand these core ideas, it becomes much easier to learn more advanced methods in chemometrics, like Multivariate Curve Resolution or special algorithms for baseline correction. All these methods solve the same basic problem: finding the model parameters that best fit the data, based on good assumptions about measurement errors. So, now that we understand *why* we use the least squares formula, the next natural question is *how* do we find the solution? In the [next entry](/introduction-to-chemometrics/least-squares/optimization/), we will look at the mathematical ways to calculate the best least squares fit, from the neat "normal equations" to modern computer-based methods. ## References # Baseline Correction with AsLS and LAsLS (/introduction-to-chemometrics/preprocessing/baseline-correction-asls) Baseline drift has been a persistent headache in spectroscopy for decades. In Raman spectroscopy, fluorescence from the sample (or from impurities) produces a broad, curved background that can be far larger than the Raman peaks themselves. In infrared and near-infrared spectroscopy, scattering and instrumental effects add sloping backgrounds that change from sample to sample. For a long time, the usual fix was to pick a few "baseline points" by hand and draw a polynomial through them, hoping it behaved in between. That works for one spectrum on a quiet afternoon. It does not work for a thousand spectra, or for backgrounds with an awkward shape. In 2005, **Paul H. C. Eilers** (Leiden University Medical Centre) and **Hans F. M. Boelens** (University of Amsterdam) described a remarkably elegant alternative in a short report titled *Baseline Correction with Asymmetric Least Squares Smoothing* . Their method, now known as **Asymmetric Least Squares (AsLS)**, builds directly on the Whittaker smoother that Eilers had revived two years earlier . The idea fits in one sentence: take a smoother, and give the points *above* the curve almost no say in where the curve goes. A few rounds of this, and the smooth curve sinks below the peaks and settles onto the background. The name comes from statistics, where "asymmetric least squares" had already been used to estimate *expectiles*, a close cousin of quantiles . The report was never published in a journal, yet AsLS became one of the most widely used baseline methods in analytical chemistry, and the starting point of a whole family of variants such as airPLS and arPLS . Later in this article we also look at **Local Asymmetric Least Squares (LAsLS)**, an extension that lets the AsLS settings change from one region of the signal to another . ## The baseline problem A measured spectrum is usually the sum of two very different things: $\text{measured spectrum} = \text{peaks (chemistry)} + \text{background (everything else)} + \text{noise}$ The background can come from many places: - a curved hump from **fluorescence** in Raman spectra, - a **slope** from scattering in diffuse reflectance, - slow **instrumental drift** between measurements, - broad contributions from the **sample holder, solvent or matrix**. The peaks are what we care about. The background is not, and if it changes from sample to sample it can easily dominate a PCA or a PLS model. So we want a curve that follows the slow background but ignores the peaks. An ordinary smoother cannot do that: it treats every point the same way, so it is pulled up by the peaks and ends up running through their middle. We need a smoother that knows the difference between "a point on the background" and "a point on a peak". AsLS gets that knowledge from a simple observation: **in the signals it is designed for, peaks stick up above the background, never below it.** (For signals with negative peaks, such as transmittance spectra, flip the sign of the signal first.) ## From smoothing to baseline correction The Whittaker smoother looks for a smooth curve $\mathbf{z}$ that stays close to the data $\mathbf{y}$ . It does that by minimizing a sum of two terms: $S = \underbrace{\sum_{i=1}^{n} (y_i - z_i)^2}_{\text{stay close to the data}} + \lambda \underbrace{\sum_{i=1}^{n-2} (\Delta^2 z_i)^2}_{\text{be smooth}}$ The first term is a familiar sum of squared residuals. The second term measures roughness. The symbol $\Delta^2 z_i$ is a second difference: $\Delta^2 z_i = z_i - 2z_{i+1} + z_{i+2}$ It is the discrete version of a second derivative, so it is large where the curve bends sharply and zero where the curve is a straight line. The parameter $\lambda$ sets the balance between the two wishes. A small $\lambda$ lets the curve follow the data closely; a large $\lambda$ forces it to be stiff and gently curved. (The [Whittaker smoothing](/introduction-to-chemometrics/preprocessing/whittaker) article explores this balance in detail.) This is excellent for smoothing, but for baseline correction it has exactly the wrong symmetry: a point that is far above the curve pulls just as hard as a point that is far below it. ## Asymmetric least squares AsLS adds one ingredient: a **weight** $w_i$ for every point. $S = \sum_{i=1}^{n} w_i (y_i - z_i)^2 + \lambda \sum_{i=1}^{n-2} (\Delta^2 z_i)^2$ A point with a large weight pulls the curve strongly towards itself. A point with a tiny weight is almost ignored. The trick is in how the weights are chosen : z_i \quad \text{(above the curve, probably a peak)} \\ 1-p & \text{if } y_i \le z_i \quad \text{(below the curve, probably background)} \end{cases}" display={true} /> with $p$ a small number, typically between 0.001 and 0.1. With $p = 0.01$, a point below the curve counts 99 times more than a point above it. There is a chicken-and-egg problem here: to set the weights we need the curve, and to find the curve we need the weights. AsLS solves it the simplest possible way, by alternating. ### The algorithm 1. **Start** with all weights equal to 1. The first fit is therefore an ordinary Whittaker smooth, which runs through the middle of the peaks. 2. **Fit** the smooth curve with the current weights. 3. **Reweight**: points above the curve get weight $p$, points below get $1-p$. 4. **Repeat** steps 2 and 3. After each round the peaks lose more influence and the curve sinks lower. Usually 5 to 10 rounds are enough, and the weights stop changing. More iterations are not always better, though: with a very small $p$ and a stiff baseline, the curve can keep sinking slowly below the background. ### The linear system behind each fit Each fit in step 2 is a linear least squares problem, just like the ones in the [least squares](/introduction-to-chemometrics/least-squares/normal-equations) section. Write the weights on the diagonal of a matrix $\mathbf{W}$ and collect the second differences in a matrix $\mathbf{D}$, so that $\mathbf{D}\mathbf{z}$ is the vector of all $\Delta^2 z_i$. Setting the gradient of $S$ to zero gives: $(\mathbf{W} + \lambda \mathbf{D}^T\mathbf{D})\,\mathbf{z} = \mathbf{W}\mathbf{y}$ That is a set of normal equations. It looks large (one unknown per data point), but the matrix is **banded**: each row only has five non-zero entries, so sparse solvers handle thousands of points in milliseconds . Imagine a flexible ruler lying on top of the spectrum. Points below the ruler are firmly glued to it. Points above it are tied with very weak elastic bands. The ruler cannot bend sharply (that is $\lambda$), and the weak bands (that is $p$) are not strong enough to lift it onto the peaks. It ends up resting on the background. ## Interactive example The spectrum below is simulated, so we know the true background (green, dashed) and the true peaks. Two numbers at the bottom tell you how well the correction worked: the error of the baseline, and how much of the true peak area survives in the corrected spectrum. Start with the defaults, then: 1. Move **Iterations** to 1. That is a plain Whittaker smooth: the curve runs straight through the peaks and the peak area recovered drops to about half. 2. Step the iterations up one by one and watch the curve sink below the peaks as the weights are updated. 3. Lower $\lambda$ to about $10^2$. The baseline becomes so flexible that it climbs into the peaks and eats a large part of them. 4. Raise $\lambda$ to $10^9$. The baseline is now so stiff that it cannot follow the hump, and a lot of background is left behind (the "recovered" area goes far above 100 %). 5. Push $p$ up to 0.5. The weights become symmetric and AsLS turns back into an ordinary smoother. 6. Try the other background shapes. The straight slope survives any stiffness, even $\lambda = 10^9$. The wavy drift has two full waves, and at the default $10^5$ the baseline is already too stiff to follow them: lower $\lambda$ to about $10^4$. Notice that the error is small over a fairly wide middle range of both parameters (here roughly $10^4$ to $10^6$ for $\lambda$). That is typical, and it is part of why AsLS is popular: it is forgiving. ## Choosing the parameters AsLS has two parameters that matter and two that rarely need attention. **$\lambda$, the smoothness.** Larger values give a stiffer baseline. Eilers and Boelens suggest searching between $10^2$ and $10^9$ , which is a much larger range than for ordinary smoothing because the baseline has to be much smoother than the peaks. A good starting point is $10^5$ to $10^6$. - If the baseline climbs into the peaks or follows broad bands, **increase** $\lambda$. - If it cannot follow the curvature of the background, **decrease** $\lambda$. Keep in mind that the penalty is a sum over neighbouring points. If you measure the same spectrum with twice as many points, the same physical curvature needs a $\lambda$ about $2^{2d} = 16$ times larger (for $d = 2$). The reason: halving the point spacing $h$ doubles the number of terms in the fit, while each second difference shrinks by a factor of four, so $\lambda$ scales with $1/h^4$. Parameters tuned on one instrument do not automatically transfer to another with a different resolution. **$p$, the asymmetry.** Smaller values ignore the peaks more strongly. The usual range is 0.001 to 0.1 . - If the baseline is pulled up into the peaks, **decrease** $p$. - If the baseline sits clearly below the noisy background, **increase** $p$ a little. **The difference order** $d$ is almost always 2, as above. **The number of iterations** is usually 10; the weights typically stop changing well before that. If the corrected spectra feed a calibration or classification model, you can also let the model choose: try a grid of $\lambda$ and $p$ values and keep the pair that gives the lowest cross-validated prediction error, as proposed by Liland, Almøy and Mevik . Start with $\lambda = 10^6$ and $p = 0.01$. Plot a few representative spectra with their fitted baselines on top, adjust $\lambda$ first (shape) and $p$ second (height), and then **freeze** both values for the whole dataset. ## Code examples The implementations below follow the original report and use sparse matrices, so they are fast even for long spectra. ```python from scipy import sparse from scipy.sparse.linalg import spsolve def baseline_asls(y, lam=1e6, p=0.01, niter=10, d=2): """ Asymmetric Least Squares baseline (Eilers and Boelens, 2005). y : 1D spectrum lam : smoothness (lambda). Typical range 1e2 to 1e9. p : asymmetry. Typical range 0.001 to 0.1. niter : number of reweighting iterations. d : order of the differences (usually 2). Returns the fitted baseline z. """ y = np.asarray(y, dtype=float) n = len(y) # Difference matrix of order d, shape (n - d, n) D = sparse.eye(n, format='csr') for _ in range(d): D = D[1:] - D[:-1] DTD = lam * (D.T @ D) w = np.ones(n) for _ in range(niter): W = sparse.diags(w, 0, shape=(n, n), format='csc') z = spsolve(W + DTD, w * y) w = np.where(y > z, p, 1 - p) # asymmetric reweighting return z # Example: Raman-like spectrum on a fluorescence background rng = np.random.default_rng(42) shift = np.linspace(400, 1800, 700) peaks = (0.8 * np.exp(-((shift - 600) ** 2) / 500) + 0.6 * np.exp(-((shift - 900) ** 2) / 400) + 0.5 * np.exp(-((shift - 1200) ** 2) / 600)) background = 3 + 0.002 * shift - 1e-6 * shift ** 2 spectrum = background + peaks + rng.normal(0, 0.02, len(shift)) baseline = baseline_asls(spectrum, lam=1e6, p=0.01) corrected = spectrum - baseline fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9, 7), sharex=True) ax1.plot(shift, spectrum, label='Spectrum') ax1.plot(shift, baseline, label='AsLS baseline', lw=2) ax1.legend() ax2.plot(shift, corrected, color='green') ax2.axhline(0, color='k', ls='--', lw=1) ax2.set_xlabel('Raman shift (cm$^{-1}$)') plt.tight_layout() plt.show() ``` ```matlab % Example rng(42); shift = linspace(400, 1800, 700)'; peaks = 0.8 * exp(-((shift - 600).^2) / 500) + ... 0.6 * exp(-((shift - 900).^2) / 400) + ... 0.5 * exp(-((shift - 1200).^2) / 600); background = 3 + 0.002 * shift - 1e-6 * shift.^2; spectrum = background + peaks + 0.02 * randn(size(shift)); baseline = baseline_asls(spectrum, 1e6, 0.01, 10, 2); corrected = spectrum - baseline; subplot(2, 1, 1); plot(shift, spectrum, shift, baseline, 'LineWidth', 1.5); legend('Spectrum', 'AsLS baseline'); subplot(2, 1, 2); plot(shift, corrected, 'LineWidth', 1.5); yline(0, '--'); xlabel('Raman shift (cm^{-1})'); function z = baseline_asls(y, lam, p, niter, d) % Asymmetric Least Squares baseline (Eilers and Boelens, 2005) if nargin z) + (1 - p) * (y z, p, 1 - p) } z } # Example set.seed(42) shift . There is no $p$ to tune, only $\lambda$. **arPLS** (asymmetrically reweighted penalized least squares) estimates the noise level from the points below the curve and uses a smooth, S-shaped (logistic) weight function . Points that are above the curve by only about the noise level keep a reasonable weight, so the baseline runs through the *middle* of the noise instead of along its bottom edge. Many more variants exist, for example drPLS or improved AsLS (IAsLS) , but they all share the structure you have just learned: a Whittaker smoother inside a loop that keeps updating the weights. You rarely need to write these yourself. In Python, the `pybaselines` package implements AsLS and most of its relatives (`asls`, `airpls`, `arpls`, `drpls`, `iasls` and others). In R, the `baseline` package offers AsLS as `baseline.als` . The code in this article is still worth reading, because it shows exactly what those functions do. Raise the noise and compare the error of each method. With clean spectra and a moderate $\lambda$ the three are almost indistinguishable. With noisy spectra AsLS settles too low (at the default noise level, by a bit more than one noise standard deviation), and airPLS, which gives zero weight to every point above the curve, sinks with it; arPLS stays close to the true background. Now lower the noise to the minimum and raise $\lambda$: airPLS and arPLS are more sensitive than AsLS to a $\lambda$ that is too stiff for the background, and above about $10^{6.5}$ they collapse on this example while AsLS degrades more gradually. Real data are rarely this tidy, so in practice it is worth trying two or three of these methods on your own spectra and looking at the result. ## Local AsLS (LAsLS): different settings for different regions Everything so far uses **one** $\lambda$ and **one** $p$ for the whole signal. That is fine when the background looks roughly the same everywhere. But many real signals are not like that. Picture a chromatogram that starts with a nearly flat, noisy stretch with a few small peaks, and later climbs steeply while tall and small peaks sit on top of the rise. Now try to choose a single setting: - The steep rise needs a fairly **flexible** baseline (small $\lambda$), but the flat region would be better served by a **stiff** one. - The tall peaks need a very **small** $p$ so they cannot lift the baseline. In the noisy region, the same small $p$ treats every noise spike above the curve as a peak and pushes the baseline down to the lower edge of the noise. Whatever you pick is a compromise, and the compromise is wrong somewhere. **Local Asymmetric Least Squares (LAsLS)**, published in 2026 by Gómez-Sánchez, Torres-Cobos and Rocha de Oliveira, removes that compromise . (Its official code is published by Lovelace's Square, the project behind this Library.) It keeps exactly the same penalized least squares engine as AsLS, but lets the asymmetric weights and the smoothing penalty **change across user-defined regions** of the signal. The authors present it for chromatography, Raman and mid-infrared signals. ### The idea: tell the smoother where the peaks are With LAsLS you mark one or more **intervals** on the signal, typically the regions that contain peaks. Each interval $j$ gets its own pair of AsLS settings: - an asymmetry $p_j$, used for the weights of the points inside that interval, - a smoothness $\lambda_j$, used for the penalty inside that interval. Outside every interval there are, by assumption, no peaks. There, LAsLS uses **symmetric** weights (every point has weight 1) and a smoothness $\lambda_{\text{Whit}}$. In other words, outside the intervals the baseline is an ordinary Whittaker smoother, which runs through the *middle* of the noise instead of along its bottom edge. Finally, one extra parameter $\mu$ adds a gentle "tension" to the whole baseline. Two special cases make the relationship with what you already know clear. With no intervals at all (and $\mu = 0$), LAsLS is a Whittaker smoother. With a single interval that covers the whole signal (and $\mu = 0$), it is AsLS with $\lambda = \max(\lambda_{\text{Whit}}, \lambda_1)$, apart from the stopping rule (LAsLS stops when the baseline stops changing, while the AsLS code above runs a fixed number of iterations). ### The maths LAsLS minimizes a sum of three terms: $S = \sum_{i=1}^{n} w_i (y_i - z_i)^2 + \sum_{i=2}^{n-1} \lambda_i (z_{i-1} - 2z_i + z_{i+1})^2 + \mu \sum_{i=1}^{n-1} (z_{i+1} - z_i)^2$ Compare it with AsLS. The first term is the same weighted fit to the data. The second term is the same second-difference penalty (written here centred on point $i$), except that $\lambda$ now carries an index: each point has its own amount of smoothing. The third term is new. It uses first differences, $z_{i+1} - z_i$, so it penalizes slope rather than curvature and pulls the baseline towards flatter shapes. The weights follow the AsLS rule inside the intervals and are fixed to 1 outside them: z_i \\ 1 - p_j & \text{if } i \text{ is in interval } j \text{ and } y_i \le z_i \\ 1 & \text{if } i \text{ is outside every interval} \end{cases}" display={true} /> The local smoothness is $\lambda_{\text{Whit}}$ everywhere, raised to $\lambda_j$ inside interval $j$: $\lambda_i = \max\left(\lambda_{\text{Whit}},\ \lambda_j \text{ of every interval that contains } i\right)$ Two details of the official implementation are worth knowing. Because of the maximum, a $\lambda_j$ smaller than $\lambda_{\text{Whit}}$ has no effect: inside an interval the baseline is never more flexible than outside. And when intervals overlap, the larger $\lambda_j$ wins, while the $p_j$ of the interval listed last is used. Setting the gradient of $S$ to zero gives a linear system that looks just like the AsLS one, with two extra pieces: $\left(\mathbf{W} + \mathbf{D}^T \boldsymbol{\Lambda} \mathbf{D} + \mu\, \mathbf{L}^T\mathbf{L}\right)\mathbf{z} = \mathbf{W}\mathbf{y}$ Here $\mathbf{W}$ holds the weights on its diagonal, $\mathbf{D}$ is the second-difference matrix from before, $\boldsymbol{\Lambda}$ is a diagonal matrix with the local $\lambda_i$ values, and $\mathbf{L}$ is the first-difference matrix. The system matrix is still banded, so it is solved as fast as plain AsLS. The iterations are the same *iteratively reweighted least squares* (IRLS) loop as before: solve, update the weights, solve again. 1. **Start** with all weights equal to 1. 2. **Solve** the system above for $\mathbf{z}$. 3. **Check convergence**: stop when the baseline barely changes, that is when $\|\mathbf{z}_{\text{new}} - \mathbf{z}_{\text{old}}\| / \|\mathbf{z}_{\text{old}}\|$ falls below a tolerance, or after a maximum number of iterations. 4. **Reweight** the points inside the intervals with their own $p_j$ (points outside keep weight 1) and go back to step 2. ### Try it: global AsLS against LAsLS The signal below is a simulated chromatogram with exactly the problem described above: a noisy, nearly flat start and a steep rise later on, with tall and small peaks. Plot 1a uses global AsLS, plot 1b uses LAsLS with two intervals (shaded). The bottom panel shows how far each baseline is from the true one. 1. Start with the defaults and compare the two errors at the bottom. 2. Try to make global AsLS as good as LAsLS. A small $p$ protects the tall peaks but sinks the baseline below the noise at the start; a $p$ large enough to keep it in the middle of the noise lets the tall peaks lift it. $\lambda$ has the same kind of conflict: flexible enough for the rise, or stiff enough to stay out of the peaks. In fact the AsLS defaults are already close to the best single setting we could find by searching the whole grid (error ≈ 0.031). LAsLS gets to ≈ 0.012, a ratio of about 2.7, inside the 2.6 to 5.7 range reported in the paper. 3. Lower $p_1$ to about 0.001. Inside interval 1 the LAsLS baseline now also sinks to the lower edge of the noise, just like AsLS. 4. Move the start of interval 1 to about 3 minutes, so that the first small peak falls outside. The symmetric smoother there treats the peak as background and bends up into it. The overall error hardly changes, so look at the error plot around 2.6 minutes. 5. Change $\lambda_2$ and watch the steep region respond: too large and the baseline can no longer follow the rise. Then lower it step by step. Once it is at or below $\lambda_{\text{Whit}}$, nothing changes any more, because of the maximum in the formula above. 6. Raise $\mu$. The tension pulls the whole baseline towards straight lines. Here the background really does rise steeply, so larger values soon stop the baseline from following it. ### When to use LAsLS instead of AsLS Use **AsLS** when the background behaves the same way across the whole signal: similar noise, similar curvature, peaks of comparable size. It has fewer parameters, needs no intervals, and is easy to apply to large datasets. Consider **LAsLS** when the characteristics of the baseline are clearly **non-uniform**: noisy regions next to clean ones, flat stretches next to steep ones, tall peaks next to small ones. The original paper tested this situation on simulated chromatogram-like signals with heterogeneous baselines . Across four cases, the relative RMSE of the AsLS baseline was 2.6 to 5.7 times higher than that of LAsLS, and the relative RMSE of peak quantification was 1.4 to 3.5 times higher. In a controlled PLSR example, LAsLS also reduced the external prediction error (relative RMSE of 11.3% against 17.9% with AsLS). The price is that you must say where the regions are. LAsLS does not find the intervals for you, so it is most useful when you know your signal: where the peaks elute or appear, and where the background is free of them. ### Choosing intervals and parameters The advice below follows from how the method is built; it is not a set of values taken from the paper. Treat it as a starting point and, as with AsLS, judge the result by plotting the baselines. **Intervals.** Put every region that contains peaks inside an interval. Any peak left outside is fitted by the symmetric smoother, which will bend the baseline up into it. Leave peak-free stretches outside: there the symmetric weights give an unbiased baseline through the noise. If peak positions shift a little between samples, make the intervals wide enough to contain the peaks in every sample, and use the same intervals for the whole dataset. In the official function the intervals are given as start and end **indices** of the signal. **$p_j$.** It plays the same role as $p$ in AsLS, region by region. Where peaks are tall compared with the noise, a small value (for example 0.001 to 0.01) keeps them from lifting the baseline. Where the noise is large compared with the peaks, a larger value keeps the baseline from sinking below the noise. **$\lambda_j$.** The stiffness inside interval $j$. Use a larger value where the baseline under the peaks is flat or gently curved, and a smaller one where it bends quickly. Remember that only values above $\lambda_{\text{Whit}}$ make a difference. **$\lambda_{\text{Whit}}$.** The smoothness of the baseline outside the intervals, and the minimum smoothness everywhere. It has to be large enough to average out the noise in the peak-free stretches and small enough to follow the background there. **$\mu$.** The global first-difference penalty. With $\mu = 0$ the term disappears. Larger values pull the whole baseline towards flatter, straighter shapes, which can steady it but also stop it from following real slopes. **Iterations and tolerance.** The official function stops when the relative change of the baseline drops below a tolerance $\text{tol}$ or after $\text{maxIter}$ iterations; its documentation example uses 50 iterations and $10^{-6}$. ### LAsLS code examples The official implementation is the MATLAB function `LASLS_CL`, released under the MIT license in the [lasls-cl repository on GitHub](https://github.com/LovelaceSquare/lasls-cl), and also listed on [The Square](https://lovelacesquare.org/the-square/codes/local-asymmetric-least-squares-lasls). The Python and R versions below are line-by-line ports of the same algorithm (same operators, same interval rules, same stopping criterion). The example signal is a chromatogram-like trace like the one in the demo. ```python from scipy import sparse from scipy.sparse.linalg import spsolve def lasls(y, intervals, p_vals, lambdas_asym, lambda_whit, mu, max_iter=50, tol=1e-6): """ Local Asymmetric Least Squares (LAsLS) baseline. Python port of LASLS_CL.m (github.com/LovelaceSquare/lasls-cl). y : 1D signal (n points) intervals : list of (start, end) index pairs, 0-based and inclusive p_vals : asymmetry p_j for each interval lambdas_asym : smoothness lambda_j for each interval lambda_whit : smoothness outside the intervals (symmetric weights) mu : global first-difference ("tension") penalty max_iter : maximum number of IRLS iterations tol : stop when the relative change of the baseline n - 1 or start > end: warnings.warn(f"Skipping invalid interval [{start}, {end}].") continue p_vec[start:end + 1] = p # later intervals overwrite p r0, r1 = max(0, start - 1), min(n - 3, end - 1) lam_vec[r0:r1 + 1] = np.maximum(lam_vec[r0:r1 + 1], lam) # overlaps: max # Second-difference (n-2 x n) and first-difference (n-1 x n) operators D = sparse.diags([1.0, -2.0, 1.0], [0, 1, 2], shape=(n - 2, n)) L = sparse.diags([-1.0, 1.0], [0, 1], shape=(n - 1, n)) P = D.T @ sparse.diags(lam_vec) @ D + mu * (L.T @ L) inside = p_vec > 0 w = np.ones(n) z = np.zeros(n) for _ in range(max_iter): A = (sparse.diags(w) + P).tocsc() z_new = spsolve(A, w * y) rel_change = np.linalg.norm(z_new - z) / (np.linalg.norm(z) + np.finfo(float).eps) z = z_new if rel_change z, p_vec, 1 - p_vec), 1.0) return z, w # Example: chromatogram-like signal with a heterogeneous baseline rng = np.random.default_rng(1) t = np.linspace(0, 25, 600) # time (min) true_baseline = 0.08 + 0.012 * t + 0.9 / (1 + np.exp(-(t - 16) / 1.1)) def peak(height, centre, width): return height * np.exp(-0.5 * ((t - centre) / width) ** 2) peaks = (peak(0.22, 2.6, 0.12) + peak(0.30, 6.1, 0.13) + peak(0.12, 8.2, 0.16) + peak(2.2, 14.6, 0.16) + peak(0.9, 17.6, 0.18) + peak(0.2, 20.4, 0.15)) noise = np.where(t = 11)) .* randn(size(t)); y = trueBaseline + peaks + noise; % y must be a column vector % Intervals as [startIdx, endIdx] (1-based indices, one row per interval) idx = @(minutes) find(abs(t - minutes) == min(abs(t - minutes)), 1); intervals = [idx(1.5), idx(9.5); ... % noisy, almost flat region idx(13.5), idx(23.5)]; % steep region, tall and small peaks pVals = [0.1; 0.005]; % p_j for each interval lambdasAsym = [1e6; 1e4]; % lambda_j for each interval lambdaWhit = 1e3; % smoothness outside the intervals mu = 0; % global first-difference penalty maxIter = 50; tol = 1e-6; [baseline, weights] = LASLS_CL(y, intervals, pVals, lambdasAsym, ... lambdaWhit, mu, maxIter, tol); corrected = y - baseline; subplot(2, 1, 1); plot(t, y, t, baseline, 'LineWidth', 1.5); legend('Signal', 'LAsLS baseline'); subplot(2, 1, 2); plot(t, corrected, 'LineWidth', 1.5); yline(0, '--'); xlabel('Time (min)'); ``` ```r library(Matrix) lasls n || i_start > i_end) { warning(sprintf("Skipping invalid interval [%d, %d].", i_start, i_end)) next } p_vec[i_start:i_end] 0, ifelse(y > z, p_vec, 1 - p_vec), 1) } list(baseline = z, weights = w) } # Example: chromatogram-like signal with a heterogeneous baseline set.seed(1) tt # Centering and Scaling (/introduction-to-chemometrics/preprocessing/centering-and-scaling) The idea of standardizing measurements to make them comparable goes back to the beginnings of modern statistics. In 1893 and 1894, **Karl Pearson** introduced the term *standard deviation*, building on **Francis Galton's** work on regression and on how far individuals deviate from the average. Expressing a value as "how many standard deviations away from the mean" turns any variable, whatever its units, into a dimensionless number. Today we call that a **z-score**, and it lets us compare heights in centimetres with weights in kilograms, or temperatures with pressures. When chemometrics emerged in the 1970s and 1980s, centering and scaling took on a new urgency. The workhorse methods of the field, [Principal Component Analysis (PCA)](/introduction-to-chemometrics/models/exploratory-models/pca) and [Partial Least Squares (PLS)](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression), are built on variances and covariances . Without centering, the first principal component of a set of spectra mostly describes the mean spectrum instead of the differences between samples, which are usually the whole point of the analysis. And as laboratories started to combine measurements of very different kinds (spectra, temperatures, pH values, concentrations) in one data matrix, scaling became unavoidable. Over the decades, several scaling approaches were developed for different kinds of data. **Autoscaling** (unit variance scaling) became the standard for mixed-unit data. **Pareto scaling**, a softer alternative associated with the Umetrics school of multivariate analysis , became especially popular in metabolomics . **Range scaling** found its niche where variables have natural operating ranges. The choice is not a minor technical detail: it decides which patterns a multivariate model is able to see . ## Why it matters Before applying PCA, PLS or almost any multivariate method, ask a simple question: **are my variables on comparable scales?** In analytical chemistry, the answer is often no. A single data matrix might contain: - NIR absorbances between 0.01 and 2.5, - temperatures between 20 and 180 °C, - moisture contents between 0.1 and 15 %, - Raman intensities between 100 and 50 000 counts. PCA looks for the directions in which the data vary the most. If Raman intensity varies over tens of thousands of counts while moisture varies over a few percent, the first principal component will describe Raman intensity almost exclusively. Not because it is more important chemically, but simply because its numbers are bigger. Centering and scaling put the variables on a common footing, so that the model reflects the chemistry rather than the units someone happened to choose. Throughout this article, the data matrix $\mathbf{X}$ has $n$ rows (samples) and $p$ columns (variables). Centering and scaling are applied **column by column**: each variable is treated separately. Methods that work row by row, such as SNV or MSC for spectra, are a different family. In a typical spectroscopic pipeline the row-wise corrections come first (smoothing, derivatives, SNV or [MSC](/introduction-to-chemometrics/preprocessing/msc)), and column-wise centering and scaling come last, just before the model. ## Mean centering Mean centering is the most basic and most universally applied preprocessing step. For each variable (column), subtract its mean across all samples: $x_{ij}^{\text{centered}} = x_{ij} - \bar{x}_j$ where $\bar{x}_j = \frac{1}{n} \sum_{i=1}^{n} x_{ij}$ is the mean of variable $j$. In matrix notation: $\mathbf{X}_{\text{centered}} = \mathbf{X} - \mathbf{1}\bar{\mathbf{x}}^T$ where $\mathbf{1}$ is a column of ones and $\bar{\mathbf{x}}$ is the vector of column means. The product $\mathbf{1}\bar{\mathbf{x}}^T$ is simply a matrix in which every row is the mean. ### What centering does After centering, every variable has mean zero. Geometrically, the whole cloud of samples is moved so that its centre sits at the origin. It sounds trivial, but it changes what PCA finds. PCA, in its basic form, looks for lines **through the origin** that pass as close as possible to the data. If the data cloud is far away from the origin, the best such line is the one that points from the origin towards the cloud. That line describes *where* the data are (the mean), not *how* they vary. Without centering, PC1 still "explains" almost all of the sum of squares, but all it tells you is the mean absorbance level: it captures only a few percent of the variation between samples. After centering, PC1 lines up with that variation and captures almost all of it. The demo shows both numbers, so you can see that a high "percentage explained" on uncentred data says nothing about how well the model describes the differences between samples. In spectroscopy, this is exactly what happens with raw spectra. Without centering, the first component is essentially the mean spectrum and the differences between samples are pushed into later components. After centering, the first component describes the most important source of variation between samples, the second the next one, and so on . Some software packages mean-centre data automatically before PCA or PLS. Others do not, or let you switch it off. A few examples: scikit-learn's `PCA` centres but never scales; R's `prcomp` centres by default (`center = TRUE`) but does not scale (`scale. = FALSE`); MATLAB's `pca` centres by default; scikit-learn's `PLSRegression` centres and also scales by default (`scale=True`). **Always check.** PCA on uncentred data is mathematically correct, but for most analytical questions it answers the wrong question. ## Autoscaling (unit variance scaling) Autoscaling combines mean centering with division by the standard deviation. It is the z-score applied to every column: $x_{ij}^{\text{auto}} = \frac{x_{ij} - \bar{x}_j}{s_j}$ with $s_j = \sqrt{\frac{1}{n-1} \sum_{i=1}^{n} (x_{ij} - \bar{x}_j)^2}$ the standard deviation of variable $j$. After autoscaling, every variable has mean zero and variance one, so each one starts with the same weight in the analysis, whatever its units. ### When to use autoscaling Autoscaling is usually the right choice when: - **variables have different units**, for example temperature, pressure and pH (when a whole spectrum is combined with a few such variables, add block scaling, see the decision guide below), - **variables have very different magnitudes**, one between 0 and 1 and another between 0 and 10 000, - **all variables are potentially important** and none should dominate just because of its scale. ### The danger of autoscaling Autoscaling has a well-known pitfall: **it inflates variables that hardly vary.** If a variable is almost constant, its standard deviation is tiny, and dividing by a tiny number magnifies whatever noise it contains. A variable that was practically irrelevant becomes as important as any other. In spectroscopy this is a real problem. Flat spectral regions with no chemical information still contain noise. After autoscaling, those noisy regions get the same weight as the information-rich bands. If all variables share the same unit and scale (for example, absorbances at different wavelengths), mean centering alone is usually the better choice. The natural differences in variance between wavelengths carry information: bands vary, flat regions do not. Autoscaling would throw that information away. ## Pareto scaling Pareto scaling is a compromise between no scaling and autoscaling. Instead of dividing by the standard deviation, divide by its **square root**: $x_{ij}^{\text{Pareto}} = \frac{x_{ij} - \bar{x}_j}{\sqrt{s_j}}$ Large variables are shrunk, but not all the way. The intuition is easy to see with numbers. If variable A has a standard deviation 100 times larger than variable B: - **no scaling**: A has 100 times the spread of B, and 10 000 times its variance, which is what PCA sees, - **Pareto scaling**: 10 times the spread ($\sqrt{100} = 10$), 100 times the variance, - **autoscaling**: equal spread and equal variance. ### Why metabolomics likes Pareto scaling Pareto scaling became particularly popular in metabolomics, helped by the systematic comparison of scaling methods published by **van den Berg and co-workers** in 2006 . In metabolomic data, large signals often belong to abundant, well-measured metabolites, while very small signals sit close to the noise. Autoscaling would give a peak at the noise floor the same weight as a major metabolite. Pareto scaling keeps large signals more influential, but stops them from completely drowning out the rest. ## Range scaling Range scaling divides each centred variable by its observed range: $x_{ij}^{\text{range}} = \frac{x_{ij} - \bar{x}_j}{x_{j,\max} - x_{j,\min}}$ It is related to min-max normalization (which maps each variable to the interval from 0 to 1), but keeps the variable centred at zero . ### When range scaling makes sense Range scaling is most natural in **process monitoring**, where variables have well-defined operating windows. A reactor that runs between 150 and 200 °C has a meaningful span of 50 °C; a pressure that runs between 1 and 5 bar has a span of 4 bar. Dividing by the span expresses each variable relative to its practical range. Its weakness is that the range depends on only **two** samples: the largest and the smallest. A single outlier can stretch the range of a variable and shrink all its other values. Autoscaling is less sensitive, because the standard deviation uses every sample, but it is not robust either: a gross outlier inflates it too. Robust alternatives divide by the median absolute deviation (MAD) or the interquartile range (see [outlier detection](/introduction-to-chemometrics/preprocessing/outliers)). ## Other scaling methods A few more approaches appear in the literature, mainly in metabolomics : - **VAST scaling** (variable stability scaling) autoscales each variable and then multiplies it by the inverse of its coefficient of variation, $\bar{x}_j / s_j$. Variables that are stable relative to their mean get more weight . - **Level scaling** divides each centred variable by its mean, so the values become relative deviations from the average. Useful when relative changes matter more than absolute ones. - **Log transformation** is not a scaling method but is often combined with one. It compresses data that span several orders of magnitude and turns multiplicative effects into additive ones. It requires positive values. - **Power transformations** such as Box-Cox form a family, indexed by a parameter $\lambda$, that includes the logarithm ($\lambda = 0$) and, up to a linear rescaling, the square root ($\lambda = 1/2$). They are used to stabilise variance or make distributions more symmetric. ## Seeing the effect on PCA The demo below brings everything together. Twenty-four samples from two production batches are described by six variables in different units. The difference between the batches is real, but it lives in variables with small numbers (pH, moisture, an absorbance). Conductivity, with numbers in the hundreds, varies a lot for reasons unrelated to the batch. One last variable is an absorbance in a flat spectral region: almost constant, pure noise. Switch between the preprocessing options and watch three things: the common-axis strip plot at the top, the PCA scores, and the PC1 loadings (the weight of each variable in PC1). The "batch separation" number is the distance between the two batch centroids in the PC1-PC2 plane divided by the typical within-batch spread (the root-mean-square distance of each sample to its own batch centroid): below 1 the batches overlap, above 2 they are clearly apart. What you should see: - **None.** The strip plot shows conductivity far away from everything else. PCA without centering points PC1 at the mean, which is dominated by conductivity. The 100 % shown for PC1 is a share of the uncentred sum of squares, not of the variance. Batch separation is about 0.4. - **Mean centering.** Better: the model now describes variation. But conductivity still owns PC1 (loading 1.00), and the batches overlap (separation still about 0.4). - **Autoscaling.** Every variable has the same spread. The batch difference in pH, moisture and absorbance becomes the main direction of the data, and the batches separate (about 2.2). Notice also that the noisy flat-region absorbance now has as much variance as anything else: that is the price of autoscaling. Here it does not line up with the batch difference (its PC1 loading is about zero), so it adds noise to PC2 and later components rather than to PC1. In other datasets it can blur the main components too. - **Pareto.** Conductivity is tamed (its values now span tens instead of hundreds), but it still dominates PC1, so in this dataset the batches stay mixed (about 0.4). Pareto is a compromise, and here the compromise is not enough. - **Range scaling.** Close to autoscaling in this example (separation about 2.4). The lesson is not "always autoscale". It is that **preprocessing decides what the model can see**, and that the right choice depends on what the variables mean. ## Choosing the right preprocessing There is no universal answer, but these guidelines cover most practical situations . **Step 1: centre.** Mean centering is a prerequisite for PCA and PLS in almost every application. It comes after any row-wise corrections (smoothing, derivatives, SNV, MSC), as the last preprocessing step before the model. **Step 2: decide whether to scale.** - All variables in the same unit and on the same scale (for example, a set of spectra)? **Mean centering alone** is usually best. - Different units or very different magnitudes? **You need to scale.** **Step 3: choose a scaling.** - Mixed units, exploratory analysis, no strong prior knowledge: **autoscaling**. - Spectra plus a handful of other variables: autoscale, then **block-scale** (divide each block by the square root of its number of variables, so that every block has the same total variance), so that a thousand wavelengths do not outvote three process variables . - Signal magnitude carries meaning, as in many metabolomics datasets: **Pareto scaling**. - Process data with well-defined operating ranges and no outliers: **range scaling**. - Data spanning orders of magnitude: consider a **log transformation** before scaling. **Step 4: check.** Compare scores plots under different choices. Do known groups separate? Does the pattern make chemical sense? Is a noisy variable suddenly important? ## Code examples The example builds three variables in different units: an absorbance, a temperature in °C and a moisture content in %. Absorbance and moisture are correlated; temperature varies a lot, but independently. With centering only, temperature owns PC1 (over 99 % of the variance). Autoscaling lets the moisture-absorbance correlation appear as PC1 (about 64 %). Pareto scaling sits in between, and here temperature still takes PC1 (about 94 %). ```python def fit_scaler(X, method="auto"): """Learn centering and scaling parameters from the training data.""" mean = X.mean(axis=0) if method == "center": scale = np.ones(X.shape[1]) elif method == "auto": scale = X.std(axis=0, ddof=1) elif method == "pareto": scale = np.sqrt(X.std(axis=0, ddof=1)) elif method == "range": scale = X.max(axis=0) - X.min(axis=0) else: raise ValueError(method) scale[scale == 0] = 1.0 # constant variables: avoid division by zero return mean, scale def apply_scaler(X, mean, scale): return (X - mean) / scale def pca_scores(X, n_components=2): U, S, Vt = np.linalg.svd(X, full_matrices=False) explained = 100 * S**2 / np.sum(S**2) return U[:, :n_components] * S[:n_components], explained # Mixed-unit example: absorbance, temperature (°C) and moisture (%) rng = np.random.default_rng(42) n = 50 moisture = 3 + 1.2 * rng.standard_normal(n) absorbance = 0.3 + 0.02 * (moisture - 3) + 0.01 * rng.standard_normal(n) temperature = 175 + 15 * rng.standard_normal(n) X = np.column_stack([absorbance, temperature, moisture]) fig, axes = plt.subplots(1, 3, figsize=(14, 4)) for ax, method in zip(axes, ["center", "auto", "pareto"]): mean, scale = fit_scaler(X, method) scores, expl = pca_scores(apply_scaler(X, mean, scale)) ax.scatter(scores[:, 0], scores[:, 1], alpha=0.7) ax.set_title(method) ax.set_xlabel(f"PC1 ({expl[0]:.1f} %)") ax.set_ylabel(f"PC2 ({expl[1]:.1f} %)") plt.tight_layout() plt.show() ``` ```matlab % Mixed-unit example: absorbance, temperature (°C) and moisture (%) rng(42); n = 50; moisture = 3 + 1.2 * randn(n, 1); absorbance = 0.3 + 0.02 * (moisture - 3) + 0.01 * randn(n, 1); temperature = 175 + 15 * randn(n, 1); X = [absorbance, temperature, moisture]; methods = {'center', 'auto', 'pareto'}; for k = 1:3 [mu, sc] = fit_scaler(X, methods{k}); Xs = (X - mu) ./ sc; [U, S, ~] = svd(Xs, 'econ'); scores = U(:, 1:2) * S(1:2, 1:2); expl = 100 * diag(S).^2 / sum(diag(S).^2); subplot(1, 3, k); scatter(scores(:, 1), scores(:, 2), 'filled'); title(methods{k}); xlabel(sprintf('PC1 (%.1f %%)', expl(1))); ylabel(sprintf('PC2 (%.1f %%)', expl(2))); end function [mu, sc] = fit_scaler(X, method) % Learn centering and scaling parameters from the training data mu = mean(X, 1); switch method case 'center', sc = ones(1, size(X, 2)); case 'auto', sc = std(X, 0, 1); case 'pareto', sc = sqrt(std(X, 0, 1)); case 'range', sc = max(X, [], 1) - min(X, [], 1); end sc(sc == 0) = 1; end ``` ```r fit_scaler # Data Reduction (/introduction-to-chemometrics/preprocessing/data-reduction) A near-infrared spectrum can contain two thousand numbers. A hyperspectral image can contain hundreds of thousands per pixel. It is tempting to feed all of them to a model, on the grounds that more data can only help. In practice, it often does not. Many of those numbers repeat what their neighbours already say, some record only noise, and some are dominated by effects that have nothing to do with the chemistry we care about. **Data reduction** is the craft of keeping the information and dropping the rest. The idea that a signal can contain far fewer "real" pieces of information than raw numbers has deep roots. In 1948, **Claude Shannon** published *"A Mathematical Theory of Communication"*, which defined information mathematically and showed that redundant parts of a message can be removed without losing anything . Shannon was thinking about telephone lines, but the principle applies just as well to a spectrum whose neighbouring wavelengths carry almost the same information. In chemometrics, variable selection became an active research topic in the 1990s, as instruments produced more and more variables. **Wold, Johansson and Cocchi** described the **variable importance in projection** (VIP) for [PLS](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression) models in 1993 . **Centner and colleagues** proposed **uninformative variable elimination** (UVE) in 1996, which compares each variable's regression coefficients with those of deliberately added random variables and removes the ones that behave no better than noise . In 2000, **Nørgaard and colleagues** introduced **interval PLS** (iPLS), which splits the spectrum into windows and asks which windows predict best . Together with [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) compression and plain common sense about which spectral regions are useful, these ideas form the modern toolkit. ## Why reduce data? Before building a multivariate model, it is worth asking: **do I need all of these variables?** In spectroscopy the answer is usually no. A typical NIR spectrum has around a thousand or more wavelength channels, but the chemistry behind it may involve only a few absorbing species, each with a handful of broad bands. There are five main reasons to reduce: **Computational cost.** PCA and PLS are fast for ordinary data sets, but hyperspectral imaging, high-frequency process monitoring and multi-way data can produce matrices with hundreds of thousands of variables. Fewer variables make computation and storage manageable. **Noise.** Variables that record mostly noise add noise to the model without adding signal. They make the calibration statistics worse and make it harder for the model to find the real patterns. Removing them complements [noise reduction](/introduction-to-chemometrics/preprocessing/noise-reduction) by smoothing. **Interpretability.** A model that uses a few selected regions can be explained in chemical terms: "the model relies on the C-H band near 1730 nm and the O-H band near 1940 nm". This matters for validation, for regulatory acceptance and for scientific understanding. **Overfitting.** When there are far more variables $p$ than samples $n$ (the "$p \gg n$" situation), a model has so much freedom that it can fit the calibration samples perfectly and still fail on new ones. Fewer variables mean fewer opportunities to fit noise. **The curse of dimensionality.** A related, more geometric problem is captured by this expression, coined by **Richard Bellman** in the late 1950s in his work on dynamic programming . As the number of dimensions grows, the volume of the space grows so quickly that any realistic number of samples becomes sparse, and distances between points become more and more alike, so that the "nearest" neighbour is hardly nearer than the farthest . Methods based on distances or densities (nearest neighbours, clustering) suffer most. If a model performs well on independent test data with all variables, there may be no reason to reduce. [PLS](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression) and [PCR](/introduction-to-chemometrics/models/regression-models/principal-component-regression) already reduce dimensionality internally: they compress many variables into a few latent variables. Reduction pays off most with many variables, few samples, noisy regions, or when an interpretable model is needed. There are four broad families of reduction, from the simplest to the most data-driven: excluding regions you know are useless, merging neighbouring variables (binning), choosing variables with a statistical criterion (variable selection), and replacing the variables with a few new ones (PCA compression). ## Wavelength range selection The simplest and often the most effective reduction is to leave out spectral regions that are known to be uninformative. It requires domain knowledge, but when the excluded regions are genuinely useless it is both safe and easy to explain. In NIR spectroscopy of moist samples, two regions often deserve attention : - **Around 1450 nm**, the first overtone of the O-H stretching vibration of water. - **Around 1940 nm**, the combination band of O-H stretching and bending. In samples with a lot of water, these bands can be so intense that the measured signal becomes noisy, nonlinear or even saturated, and they may overwhelm the weaker bands of the analyte. In other applications, of course, water is the analyte, and these are the most important regions of all. Other common candidates are the **edges of the detector range**, where sensitivity drops and noise rises, regions affected by **atmospheric water vapour or carbon dioxide** in mid-infrared spectra, and regions where **nothing varies** between samples. How to decide what to exclude: - **Physical knowledge.** Known interferences (water, CO₂, a cuvette or fibre absorption) can be excluded before any modelling. - **Visual inspection.** Overlay all spectra. Regions with very high noise, flat-topped (saturated) peaks, or no variation between samples are candidates. - **Model diagnostics.** Large loadings or regression coefficients in regions that make no chemical sense suggest the model is fitting noise or artefacts there. Excluding a region removes its information permanently. If you are unsure whether a region is useful, keep it and compare models with and without it on independent data. Exclusion is safest when it is backed by a clear physical reason, such as "the detector output is saturated in this region". ## Binning and averaging **Binning** is the simplest data-driven reduction: group $b$ neighbouring points and replace each group by its average. A spectrum of 2000 points binned with $b = 4$ becomes a spectrum of 500 points. Formally, for bin $k$, $y_k^{\text{binned}} = \frac{1}{b} \sum_{i=(k-1)b+1}^{kb} y_i, \qquad k = 1, 2, \ldots, \left\lfloor \frac{p}{b} \right\rfloor$ where $p$ is the original number of variables and $\lfloor \cdot \rfloor$ means rounding down. Binning does two things at once. It reduces the number of variables by a factor $b$, and, because each value is an average of $b$ points, it reduces independent random noise by a factor of about $\sqrt{b}$. The price is resolution: features narrower than a few bins get flattened. Try it: Binning is appropriate when: - The instrument samples the spectrum much more finely than the features require (the data are **oversampled**). - The bands of interest are broad compared with the sampling interval, as in most NIR spectra. - You want a quick, transparent way to reduce data size. It is **not** appropriate when: - The spectrum contains sharp features (Raman lines, gas-phase or high-resolution IR bands) whose height, width or position matters. - The sampling interval is already matched to the width of the features. A practical rule of thumb is to keep at least four or five points across the full width at half maximum (FWHM) of the narrowest feature you care about. If the narrowest peak spans 20 points at half height, binning by 4 is usually harmless; binning by 10 will start to flatten it. Always compare spectra and model performance before and after. ## Variable selection **Variable selection** keeps a subset of the original variables (wavelengths, mass channels, chromatographic time points) and discards the rest. Unlike PCA, the variables that remain are still real, physically meaningful measurements . ### Forward and backward selection The classical strategies come from statistics: **Forward selection** starts with no variables and adds them one at a time. At each step it adds the variable that most improves the model, for example the one that gives the largest drop in cross-validated prediction error. It stops when no remaining variable helps. **Backward elimination** starts with all variables and removes them one at a time, each time dropping the variable whose removal hurts least (or helps most). Both are **greedy**: they make the best choice at each step without looking ahead. Forward selection can miss pairs of variables that are useless alone but powerful together, and both become slow and unstable with thousands of correlated wavelengths. For spectra, methods that exploit the continuous nature of the signal usually work better. **Genetic algorithms** search the space of variable subsets in a less greedy way. They start from a population of random subsets, keep the ones that give the lowest cross-validated error, and combine and randomly modify them over many generations, in imitation of natural selection. **Riccardo Leardi** and colleagues adapted them to spectroscopic calibration and showed how to use them without overfitting . Penalised regression offers yet another route: the [lasso](/introduction-to-chemometrics/models/regression-models/lasso-regression) sets most coefficients exactly to zero and so selects variables while it fits. ### Interval PLS (iPLS) **Interval PLS** builds on a simple observation: in a spectrum, information comes in regions, not in isolated wavelengths. 1. **Split the spectrum into intervals** Divide the spectral range into $N$ equal, non-overlapping windows. For the 500 wavelengths of the code example below, 20 intervals of 25 points. 2. **Build a PLS model on each interval** For each window, build a PLS model using only its wavelengths. Use cross-validation to choose the number of latent variables and to estimate the prediction error, the RMSECV (root mean squared error of cross-validation). 3. **Compare** Plot the RMSECV of every interval next to the RMSECV of the full-spectrum model. Intervals that do as well as, or better than, the full spectrum contain the useful information. 4. **Combine the best** Build a model on the best interval or on a combination of good intervals, and check it on independent data. The bar plot of RMSECV per interval is a map of where the information lives in the spectrum, which makes iPLS very easy to explain. An extension called **synergy iPLS** tests combinations of two, three or four intervals together, to find complementary regions that are weak alone but strong together . **Moving-window PLS** slides a window of fixed width along the spectrum one point at a time instead of using fixed, non-overlapping intervals, so the result does not depend on where the interval borders happen to fall . There is no universal rule. Wide intervals (10 to 20 in total) give a robust overview; narrow ones (40 to 100) give finer resolution but more models to compare and more chance of a lucky winner. A common approach is to start with 20 to 40 intervals and then refine inside the promising regions. ### Variable importance in projection (VIP) **VIP scores** summarise how much each variable contributes to a PLS model . They combine two pieces of information: how much each PLS component explains of the response, and how much each variable contributes to that component (see the [PLS article](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression) for the weights and scores used below): $\text{VIP}_j = \sqrt{\frac{p \sum_{a=1}^{A} SS_a \, \left(w_{ja} / \|\mathbf{w}_a\|\right)^2}{\sum_{a=1}^{A} SS_a}}, \qquad SS_a = q_a^2\, \mathbf{t}_a^T\mathbf{t}_a$ Here $p$ is the number of variables, $A$ the number of PLS components, $w_{ja}$ the weight of variable $j$ in component $a$, $\mathbf{t}_a$ the scores of component $a$, and $q_a$ the coefficient that links those scores to the response. $SS_a$ is the part of the response's sum of squares explained by component $a$. The formula is built so that the average of the squared VIP scores is exactly 1. This gives the usual reading: - 1" />: above-average contribution, usually considered **important**. - $\text{VIP}_j ### Competitive adaptive reweighted sampling (CARS) **CARS** repeats PLS many times, each time on a different random subset of the samples (a Monte Carlo scheme). After each run, variables with small regression coefficients (in absolute value) are less likely to be kept, and the number of variables is reduced step by step following an exponentially decreasing function (EDF): many variables are dropped in the first runs and fewer and fewer later. In the spirit of natural selection, variables "compete" to survive. At the end, the subset with the lowest cross-validation error is chosen. CARS usually ends with far fewer variables than VIP and is popular in NIR applications, but its results can vary from run to run because of the random sampling. Fix the random seed so that a result can be reproduced, and repeat the selection with different seeds to look for variables that are selected consistently. Every selection method looks at the data many times, and with thousands of variables some will look useful by pure chance. If the same samples are used to select variables and to judge the final model, the result will look better than it is. Keep an independent test set, or use double (nested) cross-validation: an inner loop that selects variables and an outer loop that judges the whole procedure . ## PCA compression Variable selection keeps some variables. **PCA compression** replaces all of them with a few new ones. The data matrix $\mathbf{X}$ ($n \times p$) is written as $\mathbf{X} = \mathbf{T} \mathbf{P}^T + \mathbf{E}$ where $\mathbf{T}$ is the $n \times A$ score matrix, $\mathbf{P}$ the $p \times A$ loading matrix, and $\mathbf{E}$ the residuals. Using the scores $\mathbf{T}$ as input for later steps (regression, classification, clustering) reduces the number of variables from $p$ to $A$. The [PCA article](/introduction-to-chemometrics/models/exploratory-models/pca) explains how the decomposition is computed. How good is the compression? One natural measure is the **reconstruction error**, how well the original data can be rebuilt from $A$ components: $\text{RMSE}_{\text{recon}} = \sqrt{\frac{1}{np} \sum_{i=1}^{n} \sum_{j=1}^{p} (x_{ij} - \hat{x}_{ij})^2}, \qquad \hat{x}_{ij} = \bar{x}_j + \sum_{a=1}^{A} t_{ia}\, p_{ja}$ When the error falls to the level of the instrumental noise, the remaining components contain mostly noise, and keeping them stores noise rather than information. The panel below shows this for a small set of simulated spectra: ### Choosing the number of components Three common approaches: - **Explained variance.** Plot the cumulative explained variance and stop where it levels off, or at a threshold such as 99%. The threshold is arbitrary: in the panel above, 99% keeps one component too many (five, at 99.02%) and 95% one too few (three, at 95.2%), while the data contain four sources of variation. - **Scree plot.** Plot the eigenvalues and look for the "elbow" where they change from steep to flat. - **Cross-validation.** If the scores will feed a regression (principal component regression), choose $A$ to minimise the cross-validated prediction error. This optimises what you actually care about. The two approaches are complementary. PCA builds new variables from all wavelengths, so it uses the correlations across the whole spectrum but loses the one-to-one link with physical wavelengths. Variable selection keeps real wavelengths but ignores the rest. A common workflow combines them: remove useless regions first, then let PCA or PLS compress what remains. ## Code implementation The examples simulate NIR-like spectra of three constituents, run an interval PLS scan, and compute VIP scores. The three languages draw different random numbers and split the samples into different cross-validation folds, so their numbers differ slightly, but the picture is the same. ```python from sklearn.cross_decomposition import PLSRegression from sklearn.model_selection import cross_val_predict def best_rmsecv(X, y, max_components=8, cv=10): """Lowest RMSECV over 1..max_components PLS components.""" best = np.inf for nc in range(1, min(max_components, X.shape[1], X.shape[0] - 1) + 1): y_pred = cross_val_predict(PLSRegression(n_components=nc, scale=False), X, y, cv=cv) best = min(best, np.sqrt(np.mean((y - y_pred.ravel()) ** 2))) return best def interval_pls(X, y, n_intervals=20, max_components=8): """Interval PLS: RMSECV for every interval and for the full spectrum.""" edges = np.linspace(0, X.shape[1], n_intervals + 1).astype(int) rmsecv = [best_rmsecv(X[:, a:b], y, max_components) for a, b in zip(edges[:-1], edges[1:])] return np.array(rmsecv), best_rmsecv(X, y, max_components), edges def compute_vip(X, y, n_components): """VIP scores of a PLS model (sklearn's x_weights_ have unit length).""" pls = PLSRegression(n_components=n_components, scale=False).fit(X, y) W = pls.x_weights_ # (p, A) T = pls.x_scores_ # (n, A) q = pls.y_loadings_[0] # (A,) ss = q**2 * np.sum(T**2, axis=0) # y variance explained per component p = W.shape[0] return np.sqrt(p * (W**2 @ ss) / ss.sum()) # Simulated NIR data: three constituents, we predict the first one rng = np.random.default_rng(42) n_samples, n_wl = 80, 500 wavelengths = np.linspace(1000, 2500, n_wl) comp1 = np.exp(-((wavelengths - 1200) ** 2) / 5000) comp2 = np.exp(-((wavelengths - 1720) ** 2) / 3000) comp3 = np.exp(-((wavelengths - 2100) ** 2) / 4000) conc = rng.random((n_samples, 3)) conc[:, 1] = 0.5 * conc[:, 0] + 0.5 * rng.random(n_samples) # correlated constituents X = conc @ np.vstack([comp1, comp2, comp3]) + rng.normal(0, 0.02, (n_samples, n_wl)) y = conc[:, 0] rmsecv_int, rmsecv_full, edges = interval_pls(X, y, n_intervals=20) vip = compute_vip(X, y, n_components=4) best = np.argmin(rmsecv_int) print(f"Full-spectrum RMSECV: {rmsecv_full:.4f}") print(f"Best interval: {wavelengths[edges[best]]:.0f}-{wavelengths[edges[best + 1] - 1]:.0f} nm, " f"RMSECV {rmsecv_int[best]:.4f}") print(f"Wavelengths with VIP > 1: {np.sum(vip > 1)} of {n_wl}") fig, axes = plt.subplots(2, 1, figsize=(11, 7)) centers = wavelengths[((edges[:-1] + edges[1:]) // 2).clip(max=n_wl - 1)] width = wavelengths[1] - wavelengths[0] colors = ['#2563eb' if k == best else '#94a3b8' for k in range(len(rmsecv_int))] axes[0].bar(centers, rmsecv_int, width=np.diff(edges) * width, color=colors, edgecolor='white') axes[0].axhline(rmsecv_full, color='#dc2626', linestyle='--', label=f'Full spectrum ({rmsecv_full:.4f})') axes[0].set_xlabel('Wavelength (nm)'); axes[0].set_ylabel('RMSECV') axes[0].set_title('iPLS: RMSECV per interval'); axes[0].legend() axes[1].plot(wavelengths, vip, 'k-', linewidth=1) axes[1].fill_between(wavelengths, vip, alpha=0.3, color='#2563eb') axes[1].axhline(1.0, color='#dc2626', linestyle='--', label='VIP = 1') axes[1].set_xlabel('Wavelength (nm)'); axes[1].set_ylabel('VIP score') axes[1].set_title('Variable importance in projection'); axes[1].legend() plt.tight_layout() plt.show() ``` ```matlab % Simulated NIR data: three constituents, we predict the first one rng(42); n_samples = 80; n_wl = 500; wavelengths = linspace(1000, 2500, n_wl); comp1 = exp(-((wavelengths - 1200).^2) / 5000); comp2 = exp(-((wavelengths - 1720).^2) / 3000); comp3 = exp(-((wavelengths - 2100).^2) / 4000); conc = rand(n_samples, 3); conc(:, 2) = 0.5 * conc(:, 1) + 0.5 * rand(n_samples, 1); X = conc * [comp1; comp2; comp3] + randn(n_samples, n_wl) * 0.02; y = conc(:, 1); % --- Interval PLS: 10-fold RMSECV for each of 20 intervals --- n_int = 20; edges = round(linspace(0, n_wl, n_int + 1)); rmsecv = zeros(n_int, 1); for k = 1:n_int idx = edges(k) + 1 : edges(k + 1); [~, ~, ~, ~, ~, ~, mse] = plsregress(X(:, idx), y, 8, 'CV', 10); rmsecv(k) = sqrt(min(mse(2, 2:end))); % row 2 = response, column 1 = 0 components end [~, ~, ~, ~, ~, ~, mse] = plsregress(X, y, 8, 'CV', 10); rmsecv_full = sqrt(min(mse(2, 2:end))); % --- VIP scores (following the plsregress documentation) --- nc = 4; [XL, YL, XS, ~, ~, ~, ~, stats] = plsregress(X, y, nc); W0 = stats.W ./ sqrt(sum(stats.W.^2, 1)); % unit-length weights p = size(XL, 1); sumSq = sum(XS.^2, 1) .* sum(YL.^2, 1); % y variance explained per component vip = sqrt(p * sum(sumSq .* (W0.^2), 2) ./ sum(sumSq, 2)); [~, best] = min(rmsecv); fprintf('Full-spectrum RMSECV: %.4f\n', rmsecv_full); fprintf('Best interval: %.0f-%.0f nm, RMSECV %.4f\n', ... wavelengths(edges(best) + 1), wavelengths(edges(best + 1)), rmsecv(best)); fprintf('Wavelengths with VIP > 1: %d of %d\n', sum(vip > 1), n_wl); figure; subplot(2, 1, 1); centers = wavelengths(round((edges(1:end-1) + edges(2:end)) / 2)); hb = bar(centers, rmsecv, 'FaceColor', 'flat'); hold on; hb.CData = repmat([0.58 0.64 0.72], n_int, 1); hb.CData(best, :) = [0.15 0.39 0.92]; % best interval in blue yline(rmsecv_full, '--r', 'Full spectrum'); xlabel('Wavelength (nm)'); ylabel('RMSECV'); title('iPLS: RMSECV per interval'); subplot(2, 1, 2); plot(wavelengths, vip, 'k-'); hold on; yline(1, '--r'); xlabel('Wavelength (nm)'); ylabel('VIP score'); title('Variable importance in projection'); ``` ```r library(pls) # Simulated NIR data: three constituents, we predict the first one set.seed(42) n_samples 1: %d of %d\n", sum(vip > 1), n_wl)) par(mfrow = c(2, 1), mar = c(4.5, 4.5, 2.5, 1)) centers . ## What to remember Data reduction is about keeping information and dropping redundancy, noise and irrelevant effects. Start with the cheapest and most transparent steps: exclude regions you know are useless and bin oversampled spectra. Move on to statistical variable selection (iPLS, VIP, CARS) when you need interpretable or leaner models, and to PCA compression when you need a few uncorrelated inputs for later steps. Whatever you do, judge the result on data that played no part in the reduction. ## References # Spectral Derivatives (/introduction-to-chemometrics/preprocessing/derivatives) The mathematics of **finite differences**, the discrete cousin of differentiation, goes back to **Isaac Newton**, who developed difference formulas in the late 17th century to interpolate between tabulated values. Through the 18th century mathematicians such as **Brook Taylor** and **Leonhard Euler** turned the calculus of finite differences into a branch of mathematics of its own. For a long time, though, these tools lived in numerical tables and astronomy. Derivative spectra were first recorded in the 1950s with analogue electronics: **Allen Giese** and **C. Stacy French** (1955) used a derivative spectrophotometer to reveal weak bands hidden under strong ones . When spectrometers began to produce digital output in the 1960s, the derivative could be computed numerically instead, and the problem became how to do that without drowning the result in noise. A key step came in 1964, when **Abraham Savitzky** and **Marcel Golay** published their paper on smoothing *and differentiation* by simplified least squares . It is usually cited for smoothing, but it also showed that smoothed derivatives fall out of local polynomial fitting for free: evaluate the derivative of the fitted polynomial instead of the polynomial itself, and you get a derivative and a smoothing in a single convolution. This solved a serious practical problem, because plain finite differences amplify noise so much that they are often useless on real data. Derivative spectroscopy was well established in UV-Vis spectrophotometry by the late 1970s , and in near-infrared (NIR) spectroscopy it became a routine tool. In their 1984 study of protein in ground wheat, **Karl Norris** and **Phil Williams** examined how particle size distorts NIR reflectance spectra and found that derivative-based treatments, in particular second derivatives with ratios between wavelengths, greatly reduced the particle-size effects . Norris also popularised the "gap" or "segment" derivative described below. Today, derivatives are among the most widely used preprocessing steps in chemometrics, applied routinely to NIR, mid-IR, Raman and UV-Vis spectra . ## Why take derivatives of spectra? Derivatives solve three recurring problems. **Baseline removal.** Many spectra sit on offsets or slopes caused by scattering, instrument drift or sample positioning. Differentiation removes them: - A constant offset has zero slope, so the **first derivative** eliminates it. - A straight-line baseline has zero curvature, so the **second derivative** eliminates it. - In general, the $q$-th derivative (the derivative of order $q$) removes any polynomial baseline of degree $q - 1$. **Resolving overlapping bands.** Broad, overlapping bands are common, especially in NIR, where overtone and combination bands are intrinsically wide. The second derivative turns each band into a narrower negative peak, so overlapping components can be seen separately. **Enhancing subtle features.** Small shoulders and inflection points that are almost invisible in the spectrum become clear features in derivative spectra, which helps to detect minor components or small differences between similar samples. A derivative emphasises fast changes, and random noise is the fastest-changing thing in a spectrum. Every derivative order makes the noise worse relative to the signal, so you must smooth before or during differentiation. The Savitzky-Golay method does both at once and is the standard approach. The panel below shows all three effects. Three samples share the same chemistry but sit on different baselines. Switch between the spectrum and its derivatives, and between a plain finite difference, a Savitzky-Golay derivative and a gap-segment derivative. Things to notice: - In the **spectrum**, the three samples differ mostly by their baselines. - The **first derivative** removes the offsets, but the samples with different slopes still differ by a constant amount. - The **second derivative** makes the three curves practically identical: offsets and slopes are both gone. - With a **finite difference**, the second derivative is buried in noise even at a modest noise level. The **Savitzky-Golay** derivative recovers it; widening its window cleans it further, until the bands themselves start to be smoothed away. - The **gap-segment** derivative (described below, here with gap and segment equal to a third of the window) gives a derivative very similar to the Savitzky-Golay one. ## First derivative The first derivative is the **rate of change**, the slope, at each point. For a spectrum sampled at equal intervals $\Delta x$, the simplest estimate is the central difference $\frac{dy}{dx}\bigg|_i \approx \frac{y_{i+1} - y_{i-1}}{2\Delta x}$ which uses the points on either side of $i$. ### What happens to the spectrum? - **Band maxima become zero crossings.** At the top of a band the slope is zero, changing from positive to negative. - **Rising flanks become positive, falling flanks negative.** - **Constant offsets vanish.** A flat offset has zero slope everywhere. - **The band shape becomes antisymmetric**: a positive lobe followed by a negative lobe, with extremes at the inflection points of the original band. ### Locating band positions A band maximum is where $dy/dx = 0$ and the derivative changes from positive to negative. Zero crossings of a (smoothed) first derivative are therefore a convenient way to locate band positions, especially when bands sit on a sloping baseline that makes the maximum of the raw spectrum shift. ## Second derivative The second derivative is the **curvature**. Its finite difference estimate is $\frac{d^2y}{dx^2}\bigg|_i \approx \frac{y_{i+1} - 2y_i + y_{i-1}}{(\Delta x)^2}$ ### What happens to the spectrum? - **Bands become negative peaks.** At a maximum the curve bends downwards, so the curvature is negative. The minimum of the second derivative sits at the band centre. - **Bands become narrower.** For a Gaussian band, the negative peak of the second derivative has a full width at half maximum of about **53 %** of the original band. (The fourth derivative narrows further, to about 41 %.) - **Side lobes appear.** Each negative peak is flanked by two small positive lobes, which can overlap with neighbouring bands in crowded spectra. - **Straight-line baselines vanish.** A line $a + bx$ has zero curvature, so offsets and slopes both disappear. **Narrow bands win.** For a Gaussian band of height $h$ and width $\sigma$, the amplitude of the $q$-th derivative scales as $h/\sigma^q$. A band twice as narrow gives a second derivative four times larger. This is why derivatives suppress broad backgrounds, such as fluorescence or scattering, and emphasise sharp features. ### The inversion convention Because bands point downwards in the second derivative, many authors plot **−1 times** the second derivative so that bands point up as in the original spectrum. Always check the axis label before interpreting a published second-derivative spectrum. ### Resolving overlapping bands The narrowing is what makes second derivatives useful for resolving overlapping bands. Move the two bands below closer together and count the maxima. For two equal Gaussian bands, the spectrum shows a single hump as soon as their centres are closer than about 0.85 times their FWHM. The inverted second derivative still shows two peaks down to about 0.63 FWHM, and the fourth derivative down to about 0.51 FWHM. The panel starts with two equal bands, so you can reproduce these numbers to within one step of the slider; with unequal bands all three limits move to larger separations. This is a real gain in apparent resolution, but it is paid for in noise: in real data you can only use it if the signal-to-noise ratio is high enough . Add a little noise in the panel and watch the derivative, the fourth derivative above all, grow spurious maxima while the spectrum still looks clean. ## Finite differences versus Savitzky-Golay derivatives There are three common ways to compute spectral derivatives. ### Simple finite differences The formulas above compute derivatives directly from neighbouring points. They are simple, but they keep all the noise and amplify it: each differentiation acts as a **high-pass filter**, letting fast changes through and suppressing slow ones. On real spectra, plain finite differences are often unusable without prior smoothing. ### Savitzky-Golay derivatives The [Savitzky-Golay method](./savitzky-golay) fits a polynomial of order $p$ to a window of $w$ points by least squares and evaluates the **derivative** of that polynomial at the centre . Because the fit averages over the whole window, the result is far less noisy than a finite difference. As for smoothing, the operation reduces to fixed weights, which depend on $w$, $p$ and the derivative order $q$: $\frac{d^q y}{dx^q}\bigg|_i \approx \frac{1}{(\Delta x)^q} \sum_{j=-m}^{m} c_j^{(q)} \, y_{i+j}, \qquad m = \frac{w-1}{2}$ For example, a 5-point quadratic fit gives the first-derivative weights $\tfrac{1}{10}[-2, -1, 0, 1, 2]$, and a 7-point quadratic fit gives the second-derivative weights $\tfrac{1}{42}[5, 0, -3, -4, -3, 0, 5]$. **Key advantage:** smoothing and differentiation happen in one step. **Parameter requirements:** - The polynomial order must be at least the derivative order: a straight line has no curvature, so $p \geq q$. In practice $p = 2$ or 3 is used for both first and second derivatives. - Larger windows give smoother derivatives but broaden features. - Typical settings: $p = 2$ or 3, with a window chosen from the number of points that span a band (see *Choosing Savitzky-Golay derivative parameters* below). ### Gap derivatives (Norris derivatives) The gap derivative, popularised by Karl Norris , takes the difference between two points separated by a gap of $g$ data points ($g$ even, so that both points are equally far from $i$): $\frac{dy}{dx}\bigg|_i \approx \frac{y_{i+g/2} - y_{i-g/2}}{g \, \Delta x}$ The wider gap gives some built-in smoothing, because the difference is taken over a longer interval where the signal changes more than the noise. Often each end point is replaced by the average of a short **segment** of $s$ points before taking the difference. This version is known as the **gap-segment** or **Norris-Williams** derivative, and it has three settings: derivative order, segment size and gap size. Be careful when you copy settings from a paper or a program: some define the gap as the distance between the centres of the two segments, others as the number of points skipped between them, and the same numbers then give different filters. **When to use gap derivatives:** - When you want a simple derivative with modest smoothing. - In NIR applications, where gap-segment derivatives have a long tradition and are built into much commercial software. - When you want to reproduce a published method that used them. Both are linear filters, and with comparable settings they often give similar models. Savitzky-Golay derivatives have a clean least squares interpretation and are available everywhere, so they are a good default. Gap-segment derivatives are simpler to explain and common in NIR software. Whichever you choose, report all its settings. ## The noise amplification problem This is the single most important practical issue in derivative spectroscopy. Picture a signal made of a single wave, $\sin(\omega x)$. Its derivative is $\omega \cos(\omega x)$: the same wave, multiplied by its frequency $\omega$. Broad bands are made of slow waves (small $\omega$) and are barely amplified. Noise is made of fast waves (large $\omega$) and is amplified strongly. A second derivative multiplies by $\omega^2$, so the effect is even stronger. Consequences: - **First derivative**: noticeably rougher than the spectrum. - **Second derivative**: without adequate smoothing, dominated by noise. - **Third and higher derivatives**: rarely used, because noise becomes unmanageable (the fourth derivative is occasionally used in UV-Vis on very clean data). ### Quantifying the amplification For independent noise with standard deviation $\sigma_0$, error propagation gives the noise in simple finite differences . For the forward difference $(y_{i+1} - y_i)/\Delta x$: $\sigma_1 = \frac{\sqrt{2}\,\sigma_0}{\Delta x}$ For the central difference $(y_{i+1} - y_{i-1})/(2\Delta x)$, it is $\sigma_0/(\sqrt{2}\,\Delta x)$. For the second difference: $\sigma_2 = \frac{\sqrt{6}\,\sigma_0}{(\Delta x)^2}$ because the weights 1, -2, 1 have squares summing to 6. Measured per data point (that is, before dividing by $\Delta x$ or $(\Delta x)^2$), the noise only grows by a modest factor, at most $\sqrt{6}$ here, but the *signal* in a derivative is small when bands span many points: the slope of a band 20 points wide is far smaller per point than its height. The ratio of derivative signal to derivative noise therefore drops quickly with derivative order, and it drops further when the spectrum is sampled more finely (smaller $\Delta x$ compared with the band width). ### The solution: smooth before or during differentiation 1. **Smooth first, then differentiate.** Apply a smoother (moving average, Gaussian, Whittaker) and differentiate the result. It works, but you have two sets of parameters to tune. 2. **Use Savitzky-Golay derivatives.** One operation, with the window size and polynomial order jointly controlling the trade-off between noise and resolution. Applying a plain finite difference to raw spectra is almost never appropriate. The derivative will be dominated by noise. Use Savitzky-Golay (or gap-segment) derivatives, or smooth the data first. ## Practical considerations ### Choosing the derivative order **First derivative** when: - You need to remove constant offsets. - You want to emphasise slopes and inflection points. - You build PLS or PCR models and want to remove additive baseline effects while keeping noise amplification moderate. **Second derivative** when: - You need to remove both offsets and linear slopes. - You want to resolve overlapping bands or find hidden shoulders. - Your signal-to-noise ratio can tolerate the extra amplification. **Third derivative and above** are rarely worth it. If the second derivative is not enough, consider other preprocessing instead. Derivatives remove additive offsets and slopes, but a sample-to-sample *multiplicative* factor survives differentiation: if a spectrum is multiplied by 1.2, so is its derivative. That is why derivatives are often combined with standard normal variate (SNV, which centres and scales each spectrum by its own mean and standard deviation), [MSC](./msc) or [EMSC](./emsc) in NIR work . ### Choosing Savitzky-Golay derivative parameters **Window size w.** Larger windows give smoother derivatives but broaden features. Starting points: - First derivatives: $w = 9$ to 15. - Second derivatives: $w = 11$ to 21, since more smoothing is needed. - Adjust to the number of points across your narrowest band and to the noise level. Zimmermann and Kohler show, for infrared spectra, how the window and order trade resolution against noise and how to optimise them . **Polynomial order p.** At least the derivative order: - First derivative: $p = 2$ (for the first derivative, $p = 1$ and 2 give the same weights, as do 3 and 4). - Second derivative: $p = 2$ or 3 (identical weights for the second derivative), or 4 for sharper bands with a longer window. **Rule of thumb:** use the lowest polynomial order that follows your band shapes, with a window just large enough to tame the noise, and validate by inspection. ### Gap and segment sizes For gap-segment derivatives, the gap plays a role similar to the Savitzky-Golay window: - Smaller gap: more detail, more noise. - Larger gap: smoother, but narrow features may be lost. - Values of a few to about twenty points are common for NIR data with 1 to 2 nm spacing; choose them relative to your band widths. ### Validating your derivative 1. **Visual inspection.** Do the derivative features match real spectral features, or is the curve dominated by noise? 2. **Zero crossings.** For first derivatives, do they align with known band positions? 3. **Band count.** For second derivatives, does the number of resolved bands match what the chemistry suggests? Be suspicious of extra bands produced by noise or side lobes. 4. **Model performance.** If the derivatives feed a PLS model, compare cross-validated errors for different orders and windows . ## Derivative spectroscopy in chemistry ### Near-infrared (NIR) spectroscopy Derivatives are probably most important in NIR, where they serve as a primary baseline correction. NIR spectra consist of broad, overlapping overtone and combination bands, and scattering from solid samples adds offsets and slopes that vary from sample to sample : - The **first derivative** removes sample-to-sample offsets. - The **second derivative** removes offsets and linear slopes, and sharpens the broad bands. - Savitzky-Golay and gap-segment derivatives are among the most common NIR preprocessing steps . ### UV-Vis spectroscopy Electronic absorption bands are broad and often merge. Second (and sometimes fourth) derivatives help to reveal the number and position of underlying components and to quantify one compound in the presence of another with an overlapping spectrum . Because differentiation is linear, the derivative amplitude is still proportional to concentration, just like the absorbance in the Beer-Lambert law. A classic trick is the **zero-crossing method**: measure the analyte at a wavelength where the derivative of the interfering compound is zero, so that the interferent contributes nothing there. Typical uses include: - Pharmaceutical analysis, determining drugs in mixtures with overlapping UV spectra. - Removing background absorption or turbidity that varies smoothly with wavelength. - Protein studies, where derivatives of near-UV spectra reveal the environment of aromatic amino acids. ### Raman spectroscopy Raman spectra often sit on a broad fluorescence background. Since that background is smooth while Raman bands are sharp, a second derivative suppresses the fluorescence and keeps the bands. In practice, dedicated baseline correction methods such as [AsLS](./baseline-correction-asls) are usually preferred, because they keep the familiar band shapes. ### Mid-infrared (mid-IR) spectroscopy Mid-IR spectra have sharper bands and smaller baseline variation than NIR, so derivatives are used less routinely. Second derivatives remain useful for: - Resolving overlapping bands in the fingerprint region. - Detecting minor components in mixtures. - Analysing protein secondary structure from the components of the amide I band. ## Code implementation ```python from scipy.signal import savgol_filter # Generate a synthetic spectrum with overlapping peaks on a sloping baseline rng = np.random.default_rng(42) x = np.linspace(800, 2500, 850) # NIR wavelengths (nm) # True chemical signal: overlapping absorption bands signal = (0.4 * np.exp(-((x - 1200)**2) / 8000) + 0.35 * np.exp(-((x - 1400)**2) / 6000) + 0.5 * np.exp(-((x - 1700)**2) / 10000) + 0.25 * np.exp(-((x - 1950)**2) / 5000)) # Add a linear baseline (simulating scattering) baseline = 0.3 + 0.0002 * x # Add noise noise = rng.normal(0, 0.012, len(x)) spectrum = signal + baseline + noise # Compute SG derivatives # First derivative: window=15, polynomial order=2, derivative order=1 dx = x[1] - x[0] # sample spacing, so the SG derivatives are per nm like np.gradient first_deriv = savgol_filter(spectrum, window_length=15, polyorder=2, deriv=1, delta=dx) # Second derivative: window=21, polynomial order=3, derivative order=2 second_deriv = savgol_filter(spectrum, window_length=21, polyorder=3, deriv=2, delta=dx) # For comparison: simple central finite differences, as in the formulas above (noisy!) fd_first = np.r_[np.nan, (spectrum[2:] - spectrum[:-2]) / (2 * dx), np.nan] fd_second = np.r_[np.nan, np.diff(spectrum, 2), np.nan] / dx**2 # Plot results fig, axes = plt.subplots(4, 1, figsize=(12, 14)) # Original spectrum axes[0].plot(x, spectrum, linewidth=1.5, color='#2563eb') axes[0].set_ylabel('Absorbance') axes[0].set_title('Original NIR Spectrum (with baseline + noise)') axes[0].grid(True, alpha=0.3) # First derivative comparison axes[1].plot(x, fd_first, alpha=0.4, color='gray', label='Finite difference') axes[1].plot(x, first_deriv, linewidth=2, color='#dc2626', label='SG (w=15, p=2)') axes[1].axhline(0, color='black', linestyle='--', alpha=0.3) axes[1].set_ylabel('dA/dx') axes[1].set_title('First Derivative') axes[1].legend() axes[1].grid(True, alpha=0.3) # Second derivative comparison axes[2].plot(x, fd_second, alpha=0.4, color='gray', label='Finite difference') axes[2].plot(x, second_deriv, linewidth=2, color='#059669', label='SG (w=21, p=3)') axes[2].axhline(0, color='black', linestyle='--', alpha=0.3) axes[2].set_ylabel('d²A/dx²') axes[2].set_title('Second Derivative') axes[2].legend() axes[2].grid(True, alpha=0.3) # Effect of window size on second derivative for w in [11, 17, 25]: d2 = savgol_filter(spectrum, window_length=w, polyorder=3, deriv=2, delta=dx) axes[3].plot(x, d2, linewidth=1.5, label=f'w={w}') axes[3].axhline(0, color='black', linestyle='--', alpha=0.3) axes[3].set_xlabel('Wavelength (nm)') axes[3].set_ylabel('d²A/dx²') axes[3].set_title('Second Derivative: Effect of Window Size') axes[3].legend() axes[3].grid(True, alpha=0.3) plt.tight_layout() plt.show() ``` ```matlab % Spectral derivatives with Savitzky-Golay rng(42); x = linspace(800, 2500, 850); % NIR wavelengths (nm) % Synthetic spectrum: overlapping bands + linear baseline + noise signal = 0.4 * exp(-((x - 1200).^2) / 8000) + ... 0.35 * exp(-((x - 1400).^2) / 6000) + ... 0.5 * exp(-((x - 1700).^2) / 10000) + ... 0.25 * exp(-((x - 1950).^2) / 5000); baseline = 0.3 + 0.0002 * x; noise = 0.012 * randn(size(x)); spectrum = signal + baseline + noise; % Savitzky-Golay derivatives % sgolay returns the filters; column q+1 of g gives the q-th derivative divided by q!, % and conv flips the filter, hence the factor factorial(q)/(-dx)^q (see MATLAB docs) % First derivative (window=15, order=2) [~, g15] = sgolay(2, 15); dx = x(2) - x(1); first_deriv_sg = conv(spectrum, -g15(:,2), 'same') / dx; % factorial(1)/(-dx)^1 % Second derivative (window=21, order=3) [~, g21] = sgolay(3, 21); second_deriv_sg = conv(spectrum, 2 * g21(:,3), 'same') / dx^2; % factorial(2)/(-dx)^2 % Simple central finite differences, as in the formulas above (for comparison) fd_first = [NaN, (spectrum(3:end) - spectrum(1:end-2)) / (2 * dx), NaN]; fd_second = [NaN, diff(spectrum, 2), NaN] / dx^2; % Plot results figure('Position', [100 100 1000 900]); subplot(4,1,1); plot(x, spectrum, 'LineWidth', 1.5, 'Color', [0.15 0.39 0.93]); ylabel('Absorbance'); title('Original NIR Spectrum'); grid on; subplot(4,1,2); plot(x, fd_first, 'Color', [0.7 0.7 0.7], 'DisplayName', 'Finite diff'); hold on; plot(x, first_deriv_sg, 'LineWidth', 2, 'Color', [0.86 0.15 0.15], ... 'DisplayName', 'SG (w=15, p=2)'); yline(0, '--', 'Color', [0.6 0.6 0.6], 'HandleVisibility', 'off'); ylabel('dA/dx'); title('First Derivative'); legend('Location', 'best'); grid on; subplot(4,1,3); plot(x, fd_second, 'Color', [0.7 0.7 0.7], 'DisplayName', 'Finite diff'); hold on; plot(x, second_deriv_sg, 'LineWidth', 2, 'Color', [0.02 0.59 0.40], ... 'DisplayName', 'SG (w=21, p=3)'); yline(0, '--', 'Color', [0.6 0.6 0.6], 'HandleVisibility', 'off'); ylabel('d²A/dx²'); title('Second Derivative'); legend('Location', 'best'); grid on; subplot(4,1,4); for w = [11, 17, 25] [~, gw] = sgolay(3, w); d2 = conv(spectrum, 2 * gw(:,3), 'same') / dx^2; plot(x, d2, 'LineWidth', 1.5, 'DisplayName', sprintf('w=%d', w)); hold on; end yline(0, '--', 'Color', [0.6 0.6 0.6], 'HandleVisibility', 'off'); xlabel('Wavelength (nm)'); ylabel('d²A/dx²'); title('Second Derivative: Effect of Window Size'); legend('Location', 'best'); grid on; ``` ```r library(signal) # For sgolayfilt # Generate synthetic NIR spectrum set.seed(42) x # Extended Multiplicative Signal Correction (EMSC) (/introduction-to-chemometrics/preprocessing/emsc) [Multiplicative Scatter Correction (MSC)](/introduction-to-chemometrics/preprocessing/msc), introduced by Harald Martens, S. A. Jensen and Paul Geladi in 1983 and popularised by Geladi, Douglas MacDougall and Martens in 1985 , separates chemical light absorption from physical light scattering in near-infrared reflectance spectra. The method was elegant and effective, but it rests on a strong assumption: each spectrum differs from a reference only by an offset and a stretch. Real spectra often contain more than that. Baselines tilt and bend, and other compounds that we are not interested in add their own bands. In 1991, Martens and **Edward Stark** published an extension in the *Journal of Pharmaceutical and Biomedical Analysis* . They kept the MSC regression and simply added more columns to it: smooth functions of wavelength to describe baseline curvature, and the spectra of known interferents so that their contribution could be estimated and subtracted. The result became known as **Extended Multiplicative Signal Correction (EMSC)**, the name in the title of their paper. (You will also see it called extended multiplicative *scatter* correction; it is the same method.) In 2003, Martens, Nielsen and Engelsen showed how the extra terms could also describe wavelength-dependent scattering in powder mixtures . The method found some of its most demanding applications in infrared microspectroscopy of biological samples. **Achim Kohler**, Martens and co-workers used EMSC to separate physical and chemical information in FTIR images of tissue sections , and then to model the strong distortions caused by Mie scattering when the size of cells is comparable to the wavelength of infrared light . Building on that work, Bassan, Gardner, Kohler and colleagues developed the *resonant Mie scattering* correction (RMieS-EMSC) , now a standard tool in biomedical infrared spectroscopy. Kohler's group later refined it into *Mie extinction* EMSC (ME-EMSC) and released it as open-source code . A tutorial by Nils Kristian Afseth and Kohler in 2012 made EMSC accessible to a much wider audience , and the same model-based approach has since been carried over to Raman spectra of biological samples . ## Beyond basic MSC MSC works well when scatter is the main problem and it behaves like a single offset and stretch. But real spectroscopic data often add other complications: - **Sloping or curved baselines**, for example because scattering depends on wavelength, or because of instrumental drift. - **Known interfering compounds** that vary from sample to sample: water vapour or CO₂ in an FTIR beam path, a packaging film, the paraffin in an embedded tissue section. - **Constituents with known spectra** whose variation we want to keep, not to confuse with scatter. **EMSC** handles all of these with the same idea MSC uses: describe the spectrum as a sum of known shapes, estimate how much of each shape is present with least squares, and remove the ones we do not want. ## The EMSC model: MSC plus extra columns MSC models a spectrum $\mathbf{x}_i$ as: $\mathbf{x}_i \approx a_i\mathbf{1} + b_i\,\mathbf{x}_{\text{ref}}$ EMSC adds a polynomial in wavelength and the spectra of known interferents: $\mathbf{x}_i \approx a_i\mathbf{1} + b_i\,\mathbf{x}_{\text{ref}} + \sum_{j=1}^{J} d_{ij}\,\mathbf{u}^{j} + \sum_{k=1}^{K} c_{ik}\,\mathbf{m}_k$ Where: - $a_i$ and $b_i$ are the same offset and multiplicative terms as in MSC, - $\mathbf{u}$ is the wavelength axis rescaled to run from −1 to 1, and $\mathbf{u}^j$ means each element raised to the power $j$. So $d_{i1}\mathbf{u}$ is a straight tilt and $d_{i2}\mathbf{u}^2$ is a parabolic bend, - $\mathbf{m}_k$ is the spectrum of the $k$-th known interferent, and $c_{ik}$ is how much more (or less) of it sample $i$ contains than the reference, after scaling by $b_i$. Because the reference is usually the mean spectrum, it already carries the average amount of every interferent, so $c_{ik}$ can be negative for samples with less than average. If you used raw wavelengths, a column such as $2500^2 = 6\,250\,000$ would sit next to absorbances around 1. The regression would still have a solution, but it would be numerically fragile and the coefficients would be hard to read. Rescaling to the interval from −1 to 1 keeps every column on a similar scale. Once all coefficients are estimated, we subtract everything we do not want and undo the multiplicative effect: $\mathbf{x}_i^{\text{EMSC}} = \frac{\mathbf{x}_i - a_i\mathbf{1} - \sum_j d_{ij}\,\mathbf{u}^{j} - \sum_k c_{ik}\,\mathbf{m}_k}{b_i}$ Notice what stays: the reference part $b_i\mathbf{x}_{\text{ref}}$ (which becomes $\mathbf{x}_{\text{ref}}$ after dividing by $b_i$) and the residual, which is where the chemical differences between this sample and the reference live. ### One spectrum, taken apart The demo below makes this concrete. For a single spectrum, it shows each piece of the EMSC model: the scaled reference, the offset, the polynomial baseline, the interferent, and the residual (magnified five times so you can see it). Add them all up and you get the measured spectrum back. Look at the residual for samples at the two ends of the colour scale (the darkest blue and the brightest amber). It has peaks exactly where the analyte bands are, positive for high-content samples and negative for low-content ones. That is the chemistry, and it is precisely what EMSC is designed to keep. Smaller bands at 1450 and 1940 nm, with the opposite sign, come from water, which also varies between the simulated samples. Notice also that the interferent coefficient $c$ can be negative: it measures the difference from the amount already present in the reference. ## When EMSC helps more than MSC Use **EMSC** rather than plain **MSC** when you see: - **Baseline tilt or curvature** that varies between samples and is still visible after MSC, - **wavelength-dependent scattering**, typical of powders with different particle sizes and of biological cells in infrared microscopy, - **a known interferent** whose amount varies: water vapour, CO₂, a solvent, a container, an embedding medium, - samples whose chemical variation is large enough to disturb MSC, but whose main constituent spectra are known (see "good" spectra below). Stick with **MSC** (or SNV) when the scatter is a simple offset and stretch, there is no visible residual baseline, and no known interferent varies. Simpler is better when it works. ## The algorithm EMSC is a multiple linear regression, done once per spectrum, with the same design matrix for every spectrum. **Step 1: choose the reference.** Usually the mean spectrum of the training set, exactly as in MSC: $\mathbf{x}_{\text{ref}} = \frac{1}{N} \sum_{i=1}^{N} \mathbf{x}_i$ **Step 2: build the design matrix.** Put every shape the model is allowed to use in one column: $\mathbf{Z} = \begin{bmatrix} \mathbf{1} & \mathbf{x}_{\text{ref}} & \mathbf{u} & \mathbf{u}^2 & \cdots & \mathbf{u}^J & \mathbf{m}_1 & \cdots & \mathbf{m}_K \end{bmatrix}$ Each row of $\mathbf{Z}$ corresponds to one wavelength. **Step 3: fit every spectrum.** Solve the least squares problem with the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations) (or, better, a QR-based solver): $\hat{\boldsymbol{\beta}}_i = (\mathbf{Z}^T\mathbf{Z})^{-1}\mathbf{Z}^T\mathbf{x}_i$ The vector $\hat{\boldsymbol{\beta}}_i$ contains $a_i, b_i, d_{i1}, \dots, d_{iJ}, c_{i1}, \dots, c_{iK}$. Because $\mathbf{Z}$ is the same for every spectrum, the matrix $(\mathbf{Z}^T\mathbf{Z})^{-1}\mathbf{Z}^T$ can be computed once and reused. **Step 4: correct.** Subtract the unwanted parts and divide by $b_i$, as in the formula above. **Step 5: store the model.** The reference, the polynomial order, the interferent spectra and the minimum and maximum wavelengths used to build $\mathbf{u}$ are all part of the preprocessing model. New spectra are corrected with exactly the same ones. If the new spectra are later cut to a shorter wavelength range, reuse the stored minimum and maximum for $\mathbf{u}$; rescaling the shorter axis to run from −1 to 1 again would change the meaning of every polynomial coefficient. ## Interactive example Here the spectra suffer from three problems at once: an offset and stretch (which MSC can fix), a curved baseline that differs from sample to sample, and a varying amount of an interfering compound. The quality of each correction is again judged by how well the height of the analyte band follows the true content (R²). Things to try: 1. Select **MSC**. The spectra are pulled together, but the curved baselines are left behind and the R² stays well below 1. 2. Select **EMSC** with order 0 and the interferent switched off. That is exactly MSC again. 3. Increase the order to 1. A straight tilt cannot describe a curved baseline, and here it even makes things worse (R² drops from about 0.81 to 0.71). Order 2 removes the bends and R² jumps to about 0.97. 4. Switch the interferent on. Its band at 1695 nm sits right next to the analyte band, so removing it explicitly pushes R² close to 1. 5. Set the baseline curvature to zero and the interferent to zero. Now MSC is already enough, and the extra terms bring nothing. ## Choosing the polynomial order The polynomial order controls how flexible the baseline part is: - **Order 0**: no polynomial. This is plain MSC. - **Order 1**: adds a straight tilt. - **Order 2**: adds a parabolic bend. A common default. - **Order 3 and above**: rarely needed. A very flexible polynomial can start to imitate broad chemical bands and remove them. A good rule is to start with order 2, look at the corrected spectra and at the residuals, and increase the order only if a clear baseline structure remains. Then check the choice with cross-validation of the model you actually care about. ## Adding interferent spectra If you know that a compound varies across samples and you do *not* want it in your model, include its pure spectrum as a column of $\mathbf{Z}$. EMSC will estimate how much of it each sample contains and subtract it. Martens and Stark called this *spectral interference subtraction* . Include an interferent when: - you have a reasonably pure spectrum of it, measured on the same wavelength axis, - its amount varies between samples, - it is not what you are trying to measure. Do not include it when: - it is constant across all samples (then it is already part of the reference), - it is (or is strongly correlated with) the property you want to predict, because EMSC would remove the very signal you need. ### "Good" spectra: protecting the chemistry The same trick works the other way around. If you know the spectra of the main chemical constituents, you can add them as columns too, but *keep* their contribution in the corrected spectrum . With "good" spectra $\mathbf{g}_l$ (coefficients $h_{il}$) in the model, the fit becomes $\mathbf{x}_i \approx a_i\mathbf{1} + b_i\,\mathbf{x}_{\text{ref}} + \sum_{j} d_{ij}\,\mathbf{u}^{j} + \sum_{k} c_{ik}\,\mathbf{m}_k + \sum_{l} h_{il}\,\mathbf{g}_l$ and the correction subtracts only the nuisance terms: $\mathbf{x}_i^{\text{EMSC}} = \frac{\mathbf{x}_i - a_i\mathbf{1} - \sum_j d_{ij}\,\mathbf{u}^{j} - \sum_k c_{ik}\,\mathbf{m}_k}{b_i}$ The good columns are fitted but not subtracted. Their job is to give chemical variation a column of its own, so that it is not mistaken for a change in $b_i$, which is the main weakness of MSC when samples differ a lot in composition. The `good` argument of the code below does exactly this. There is a limit. If the reference, once the offset, polynomial and interferents are taken out, looks just like the analyte spectrum, EMSC cannot tell "more analyte" from "a larger $b_i$", and the correction removes the very signal you want. That happens, for example, when the only thing that varies in the chemistry is one constituent on a featureless background. Adding the analyte as a good spectrum helps, but it cannot fully cure a reference that is itself almost a combination of the other columns: that is the collinearity problem described in the limitations below. ## Code examples ```python def emsc(spectra, wavelengths, reference=None, poly_order=2, interferents=None, good=None): """ Extended Multiplicative Signal Correction. spectra : array (n_samples, n_wavelengths) wavelengths : array (n_wavelengths,) reference : array (n_wavelengths,), default: mean spectrum poly_order : order of the polynomial baseline (0 = plain MSC) interferents : list of spectra to estimate and subtract good : list of constituent spectra to estimate but keep Returns (corrected spectra, coefficients, reference). """ spectra = np.asarray(spectra, dtype=float) if reference is None: reference = spectra.mean(axis=0) interferents = [] if interferents is None else list(interferents) good = [] if good is None else list(good) # Wavelength axis rescaled to [-1, 1] for numerical stability u = 2 * (wavelengths - wavelengths.min()) / np.ptp(wavelengths) - 1 cols = [np.ones_like(u), reference] cols += [u ** j for j in range(1, poly_order + 1)] cols += [np.asarray(k, dtype=float) for k in interferents] n_remove = len(cols) # columns 2 .. n_remove-1 are subtracted cols += [np.asarray(g, dtype=float) for g in good] # fitted, but kept Z = np.column_stack(cols) coefs = np.linalg.lstsq(Z, spectra.T, rcond=None)[0].T # one row per sample a, b = coefs[:, 0], coefs[:, 1] unwanted = coefs[:, 2:n_remove] @ Z[:, 2:n_remove].T # polynomial + interferents corrected = (spectra - a[:, None] - unwanted) / b[:, None] return corrected, coefs, reference # Example: scatter + curved baseline + a varying interferent rng = np.random.default_rng(42) wl = np.linspace(1100, 2500, 300) u = 2 * (wl - wl.min()) / np.ptp(wl) - 1 analyte = np.exp(-((wl - 1730) ** 2) / (2 * 25 ** 2)) interferent = np.exp(-((wl - 1660) ** 2) / (2 * 18 ** 2)) # The sample matrix has bands of its own (it is not just a flat background) matrix = (0.3 + 0.4 * np.exp(-((wl - 1450) ** 2) / (2 * 60 ** 2)) + 0.5 * np.exp(-((wl - 1940) ** 2) / (2 * 70 ** 2))) conc = rng.uniform(0, 1, 20) raw = np.array([ rng.uniform(-0.1, 0.1) # offset + rng.uniform(0.8, 1.2) * (matrix + 0.2 * c * analyte) # stretch x chemistry + rng.uniform(-0.05, 0.05) * u + rng.uniform(-0.05, 0.05) * u ** 2 # curved baseline + rng.uniform(0, 0.2) * interferent # interferent + rng.normal(0, 0.002, wl.size) for c in conc ]) corrected, coefs, ref = emsc(raw, wl, poly_order=2, interferents=[interferent]) msc_only, _, _ = emsc(raw, wl, poly_order=0) # plain MSC, for comparison fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) ax1.plot(wl, raw.T, lw=1); ax1.set_title('Raw') ax2.plot(wl, corrected.T, lw=1); ax2.set_title('After EMSC') plt.tight_layout() plt.show() band = np.argmin(abs(wl - 1730)) r2 = lambda X: np.corrcoef(X[:, band], conc)[0, 1] ** 2 print(f'R2 raw : {r2(raw):.3f}') # about 0.27 print(f'R2 MSC : {r2(msc_only):.3f}') # about 0.95 print(f'R2 EMSC: {r2(corrected):.3f}') # about 0.999 ``` ```matlab % Example: scatter + curved baseline + a varying interferent rng(42); wl = linspace(1100, 2500, 300); u = 2 * (wl - min(wl)) / (max(wl) - min(wl)) - 1; analyte = exp(-((wl - 1730).^2) / (2 * 25^2)); interferent = exp(-((wl - 1660).^2) / (2 * 18^2)); % The sample matrix has bands of its own (it is not just a flat background) matrix = 0.3 + 0.4 * exp(-((wl - 1450).^2) / (2 * 60^2)) ... + 0.5 * exp(-((wl - 1940).^2) / (2 * 70^2)); conc = rand(20, 1); raw = zeros(20, numel(wl)); for i = 1:20 raw(i, :) = (-0.1 + 0.2 * rand) ... + (0.8 + 0.4 * rand) * (matrix + 0.2 * conc(i) * analyte) ... + (-0.05 + 0.1 * rand) * u + (-0.05 + 0.1 * rand) * u.^2 ... + 0.2 * rand * interferent + 0.002 * randn(size(wl)); end [corrected, coefs] = emsc(raw, wl, [], 2, {interferent}); mscOnly = emsc(raw, wl, [], 0, {}); % plain MSC, for comparison subplot(1, 2, 1); plot(wl, raw'); title('Raw'); subplot(1, 2, 2); plot(wl, corrected'); title('After EMSC'); [~, band] = min(abs(wl - 1730)); R = corrcoef([raw(:, band), mscOnly(:, band), corrected(:, band), conc]); fprintf('R2 raw : %.3f\nR2 MSC : %.3f\nR2 EMSC: %.3f\n', R(1, 4)^2, R(2, 4)^2, R(3, 4)^2); function [corrected, coefs, reference] = emsc(spectra, wl, reference, polyOrder, interferents, good) % Extended Multiplicative Signal Correction. Rows are samples. % interferents: cell array of spectra to estimate and subtract % good: cell array of constituent spectra to estimate but keep if nargin 0) for (j in 1:poly_order) Z 2) { idx . - **Start with order 2.** Increase only if a baseline structure clearly remains, and confirm with cross-validation. - **Check the coefficients.** Plot $b_i$, $d_{ij}$ and $c_{ik}$ against sample number or known properties. Unexpected trends often reveal problems with sampling or measurement. - **Keep everything fixed for new data.** Use the same reference, the same polynomial order and the same interferent spectra for the test set and for future samples. Never recompute the reference from test data. - **Do not stack EMSC with SNV.** They correct similar effects; choose one. ## What to remember EMSC is MSC with more columns. The measured spectrum is explained as an offset, a scaled reference, a smooth polynomial baseline and the spectra of known interferents, all estimated together by least squares. The unwanted pieces are subtracted, the multiplicative effect is divided out, and what remains is the reference plus the chemical differences. It is powerful because it is explicit: you decide which shapes are "nuisance" and which are "chemistry". That same freedom is its main risk, so keep the model as simple as the data allow and validate the choice. ## References # Gaussian Smoothing (/introduction-to-chemometrics/preprocessing/gaussian-smoothing) Few mathematical objects carry a person's name as firmly as the Gaussian function carries that of **Carl Friedrich Gauss** (1777 to 1855). Yet the bell-shaped curve was not his invention. In 1733 the French-born mathematician **Abraham de Moivre**, working in London, derived it as an approximation to the binomial distribution, and later included the result in the second edition of his *Doctrine of Chances* . It is a good example of what the statistician Stephen Stigler called the law of eponymy: scientific discoveries are rarely named after the person who made them first . What Gauss gave the curve was its central role in the theory of measurement errors. In 1801 the newly discovered asteroid Ceres was lost in the glare of the Sun after only a few weeks of observations. Gauss computed its orbit from those scattered measurements, and astronomers found Ceres again at the end of 1801, close to where he had predicted. In *Theoria Motus Corporum Coelestium* (1809) he gave his own derivation of least squares (Legendre had published the method in 1805 ) and justified it by assuming that measurement errors follow exactly this bell-shaped law . That is why we call it the Gaussian, or normal, distribution. (The story of Ceres and least squares is told in more detail in [Least Squares: optimization](/introduction-to-chemometrics/least-squares/optimization).) The function itself, $e^{-x^2/(2\sigma^2)}$, turned out to have remarkable properties. It is infinitely smooth, its Fourier transform is again a Gaussian, and in two dimensions it splits into a product of two one-dimensional Gaussians. These properties became important when digital signal processing needed **smoothing kernels**: sets of weights used to average out noise. Unlike a flat moving average window, a Gaussian kernel gives the most weight to the centre and fades out smoothly. Weighted kernels were formalised in statistics by **Murray Rosenblatt** (1956) and **Emanuel Parzen** (1962) for estimating probability densities, where the Gaussian kernel became a standard choice . From there the idea spread to image processing, where "Gaussian blur" is a basic operation, and to spectroscopy, where it offers a simple one-parameter alternative to the moving average. ## The weighted average problem The [moving average](./moving-average) gives every point in its window the same weight. That raises an obvious question: > **Why should a point five steps away count as much as the immediate neighbour?** If the signal changes gradually, the closest neighbours are the most similar to the point we are estimating, so they deserve more influence. Points further away are still useful for cancelling noise, but they are more likely to sit on a different part of a band. This is the motivation for **weighted averaging**. **Gaussian smoothing** uses the bell curve to set the weights: the centre point receives the highest weight, and the weight falls off smoothly with distance. ## The Gaussian kernel The weight given to a neighbour at distance $u$ (measured in data points) is $w(u) = e^{-\frac{u^2}{2\sigma^2}}$ where $\sigma$ (sigma) controls the width of the bell. The smoothed value at point $i$ is then $\hat{y}_i = \frac{\sum_{j} w(j-i)\, y_j}{\sum_{j} w(j-i)}$ In plain words: multiply each neighbour by its weight, add everything up, and divide by the sum of the weights. Dividing by the sum **normalises** the kernel, so that a constant signal stays exactly the same after smoothing. You may have seen the Gaussian written with a factor $1/(\sigma\sqrt{2\pi})$ in front; that factor makes the continuous curve integrate to one, but after the normalisation above it cancels and does not matter. In practice the weights are cut off where they become negligible. About 99.7 % of the area of a Gaussian lies within $\pm 3\sigma$ of the centre, so most implementations truncate the kernel at a radius of three or four $\sigma$. For $\sigma = 2$ points, truncated at $\pm 3\sigma$, the 13 normalised weights are: ``` offset: -6 -5 -4 -3 -2 -1 0 +1 +2 +3 +4 +5 +6 weight: 0.002 0.009 0.027 0.065 0.121 0.176 0.200 0.176 0.121 0.065 0.027 0.009 0.002 ``` Compare with the 7-point moving average that removes the same amount of white noise (see below), where every weight is $1/7 \approx 0.143$: the Gaussian puts more weight on the centre and spreads a little weight much further out. ## How σ controls smoothing The parameter $\sigma$ is the standard deviation of the bell, expressed in data points: - **Small σ** (about 0.5 to 1): a narrow bell, only immediate neighbours matter, light smoothing. At σ = 0.5 the centre point keeps 79 % of the weight, so the filter barely smooths at all. - **Medium σ** (about 1.5 to 3): neighbours within a few points contribute, moderate smoothing. - **Large σ** (4 and above): many neighbours contribute, heavy smoothing, and bands start to blur. Use the panel below to see the kernel and its effect on different "frequencies". The lower plot needs a word of explanation: any spectrum can be described as a sum of waves. Broad bands are made of slow waves (on the left of the plot), while point-to-point noise is made of fast waves (on the right). The curve shows what fraction of each wave survives the smoothing. An ideal smoother would keep the left side at 1 and push the right side to 0. Two things stand out: 1. **The Gaussian's response falls smoothly and never goes negative.** Fast waves are progressively removed. The Fourier transform of a Gaussian is again a Gaussian, which is exactly what you see. 2. **The moving average's response has ripples, called sidelobes, that dip below zero.** Some fast components leak through the filter, and those in the negative lobes even come out with their sign flipped. This is the origin of the slightly ragged look that moving averages sometimes leave behind . ### Noise reduction and the "equivalent" moving average For independent random noise with standard deviation $\sigma_{\varepsilon}$, any normalised set of weights $w_j$ leaves a noise standard deviation of $\sigma_{\text{smoothed}} = \sigma_{\varepsilon}\sqrt{\sum_j w_j^2}$ For the moving average, $\sum_j w_j^2 = 1/w$, which gives the familiar $\sqrt{w}$ law. For a Gaussian kernel that is not too narrow, $\sum_j w_j^2 \approx 1/(2\sqrt{\pi}\,\sigma)$. The two filters therefore remove the same amount of white noise when $w \approx 2\sqrt{\pi}\,\sigma \approx 3.5\,\sigma$ So a Gaussian with $\sigma = 2$ removes about as much noise as a 7-point moving average, not an 11- or 13-point one, even though its weights stretch over 13 points. This is a useful rule when you translate settings from one method to the other. ### The 3σ rule for the window - Points beyond $3\sigma$ contribute almost nothing. - The kernel actually used therefore spans about $6\sigma + 1$ points. Libraries differ: SciPy's `gaussian_filter1d` truncates at $4\sigma$ by default ($8\sigma + 1$ points), and MATLAB's `imgaussfilt` at about $2\sigma$ (about $4\sigma + 1$ points). - Example: $\sigma = 2$ uses about 13 points; $\sigma = 3$ about 19 points. When reading software documentation, check whether a function asks for $\sigma$ or for a window length, and how the two are related. They are not the same thing. ## Gaussian versus moving average in practice Is the Gaussian simply better? It is better behaved, but it is not magic. The panel below compares the two filters at **equal noise reduction**, using the rule above. What you should see: - The **noise levels** on the flat baseline are practically identical, as the theory predicts. - The **band heights** are close. At equal noise reduction the two kernels have a similar spread, so they broaden bands by a similar amount. For moderate and large σ (about 2 and above) the Gaussian keeps a little more of the narrowest bands, because its weight is concentrated near the centre instead of being spread evenly to the edges of a box. At the panel's starting value, σ = 3, the narrow band at point 120 keeps about 77 % of its height with the Gaussian and 71 % with the moving average; for small σ the difference disappears. - The **shapes** differ. Switch on the spike: the moving average turns it into a flat-topped rectangle, the Gaussian into a smooth bell. Each filter leaves its own kernel shape behind wherever the signal changes abruptly. So the honest summary is: Gaussian smoothing produces cleaner, more natural curves without ripples, and a modest advantage on narrow bands, but it is still an averaging filter: sharp bands are lowered and broadened by roughly the same mechanism as with a moving average. If band heights matter, you need a different idea, such as the local polynomial fits of [Savitzky-Golay](./savitzky-golay) or the penalised fit of [Whittaker](./whittaker). ## Edge effects Near the ends of a spectrum the kernel runs out of points on one side. Common strategies are to **renormalise** (use only the available points and divide by the sum of their weights, as the panels above do), to **reflect** the spectrum at the ends (the default in `scipy.ndimage.gaussian_filter1d`), or to **repeat** the last value. Each gives slightly different results in the first and last $3\sigma$ points. ## Choosing σ: practical guidelines As with the moving average, the right σ depends less on the noise than on the narrowest band that matters, measured in data points: | Narrowest band FWHM | Starting σ | |---------------------|------------| | Below 8 points | 0.5 to 1 | | 8 to 20 points | 1 to 3 | | Above 20 points | up to FWHM/7 | These numbers are only starting points. Two anchors behind them: - **Relate σ to your narrowest band.** A Gaussian band with standard deviation $s$ (in points) becomes, after smoothing, a band with standard deviation $\sqrt{s^2 + \sigma^2}$. Keeping $\sigma$ below about a third of $s$, that is below about FWHM/7 (because the FWHM of a Gaussian is about $2.355\,s$), limits the broadening to roughly 5 %. - **Check the residuals.** The difference between original and smoothed data should look like pure noise. ### Validation 1. **Visual check**: smooth, but no band merged or flattened? 2. **Residuals**: original minus smoothed should look like noise. 3. **Feature preservation**: are band positions, heights and widths still where they should be? ## Code examples ```python from scipy.ndimage import gaussian_filter1d # Noisy synthetic spectrum rng = np.random.default_rng(42) x = np.linspace(0, 100, 500) true_signal = (0.8 * np.exp(-((x - 25)**2) / 20) + 0.6 * np.exp(-((x - 50)**2) / 15) + 0.9 * np.exp(-((x - 75)**2) / 25)) noisy_signal = true_signal + rng.normal(0, 0.08, len(x)) # gaussian_filter1d(data, sigma): sigma is given in data points. # By default the kernel is truncated at 4 sigma and the edges are reflected. fig, axes = plt.subplots(2, 2, figsize=(12, 8), sharex=True, sharey=True) for ax, sigma in zip(axes.flat, [1.0, 2.0, 3.0, 5.0]): smoothed = gaussian_filter1d(noisy_signal, sigma=sigma) ax.plot(x, noisy_signal, color='gray', alpha=0.5, label='Noisy') ax.plot(x, smoothed, linewidth=2, label=f'σ = {sigma}') ax.plot(x, true_signal, 'k--', alpha=0.7, label='True') ax.set_title(f'Gaussian smoothing, σ = {sigma} points') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.show() def gaussian_kernel(sigma, radius=None): """Normalised Gaussian weights from -radius to +radius.""" if radius is None: radius = int(np.ceil(3 * sigma)) u = np.arange(-radius, radius + 1) k = np.exp(-u**2 / (2 * sigma**2)) return u, k / k.sum() # Kernels for several sigmas for sigma in [0.5, 1.0, 2.0, 3.0]: u, k = gaussian_kernel(sigma, radius=10) plt.plot(u, k, marker='o', label=f'σ = {sigma}') plt.xlabel('Distance from centre (points)') plt.ylabel('Weight') plt.legend() plt.show() # Moving average with the same white-noise reduction: w ≈ 2·sqrt(pi)·sigma sigma = 2.0 w = int(2 * round((2 * np.sqrt(np.pi) * sigma - 1) / 2) + 1) # nearest odd number, here 7 moving_avg = np.convolve(noisy_signal, np.ones(w) / w, mode='same') gauss = gaussian_filter1d(noisy_signal, sigma=sigma) plt.plot(x, noisy_signal, color='gray', alpha=0.4, label='Noisy') plt.plot(x, moving_avg, linewidth=2, label=f'Moving average (w = {w})') plt.plot(x, gauss, '--', linewidth=2, label=f'Gaussian (σ = {sigma})') plt.plot(x, true_signal, 'k:', label='True signal') plt.legend() plt.show() ``` ```matlab % Gaussian smoothing with an explicit, normalised kernel (base MATLAB) rng(42); x = linspace(0, 100, 500); true_signal = 0.8 * exp(-((x - 25).^2) / 20) + ... 0.6 * exp(-((x - 50).^2) / 15) + ... 0.9 * exp(-((x - 75).^2) / 25); noisy_signal = true_signal + 0.08 * randn(size(x)); sigmas = [1, 2, 3, 5]; figure('Position', [100 100 1200 800]); for k = 1:numel(sigmas) subplot(2, 2, k); plot(x, noisy_signal, 'Color', [0.6 0.6 0.6]); hold on; plot(x, gaussianSmooth(noisy_signal, sigmas(k)), 'LineWidth', 2); plot(x, true_signal, '--k', 'LineWidth', 1.5); title(sprintf('Gaussian smoothing, \\sigma = %g points', sigmas(k))); legend('Noisy', 'Smoothed', 'True', 'Location', 'best'); grid on; end % Moving average with the same white-noise reduction (w ≈ 3.5 sigma) sigma = 2; w = 2 * round((2 * sqrt(pi) * sigma - 1) / 2) + 1; % here 7 moving_avg = movmean(noisy_signal, w); % shrinks window at the edges figure; plot(x, noisy_signal, 'Color', [0.7 0.7 0.7]); hold on; plot(x, moving_avg, 'LineWidth', 2); plot(x, gaussianSmooth(noisy_signal, sigma), '--', 'LineWidth', 2); plot(x, true_signal, ':k', 'LineWidth', 1.5); legend('Noisy', sprintf('Moving average (w = %d)', w), 'Gaussian (\sigma = 2)', 'True'); grid on; function ys = gaussianSmooth(y, sigma) % Normalised Gaussian kernel truncated at 3 sigma; at the edges the % weights are renormalised over the points that exist. r = ceil(3 * sigma); u = -r:r; k = exp(-u.^2 / (2 * sigma^2)); num = conv(y, k, 'same'); den = conv(ones(size(y)), k, 'same'); ys = num ./ den; end ``` If you have the Image Processing Toolbox, `imgaussfilt(y, sigma)` gives almost the same result (it truncates the kernel at about 2σ and repeats the edge values instead of renormalising). ```r # Normalised Gaussian kernel truncated at 3 sigma gaussian_kernel and in hyperspectral imaging, where each wavelength channel can be smoothed spatially as an image, so that neighbouring pixels share information without mixing different wavelengths. ## Common mistakes to avoid - **Confusing σ with the window size.** The kernel spans about $6\sigma + 1$ points. - **Using σ too large.** Bands merge and flatten. - **Forgetting to normalise the kernel.** The output changes scale. - **Ignoring edge conventions.** Different libraries treat the ends differently. - **Expecting a big gain in band preservation over a moving average at the same noise reduction.** The main gain is in smoothness, not in resolution. ## Why the Gaussian is special A few properties explain why the Gaussian keeps appearing in smoothing: 1. **It is localised in both domains at once.** Among all functions, the Gaussian has the smallest possible product of its width in position and its width in frequency. This is the same mathematics behind the uncertainty principle in quantum mechanics. 2. **Its Fourier transform is again a Gaussian**, so its frequency response decays smoothly without ripples. 3. **It is separable** in several dimensions. 4. **Repeated averaging leads to it.** Smoothing several times with any small positive kernel (for example a moving average) produces weights that approach a Gaussian shape, a consequence of the central limit theorem. ## What to remember - Gaussian smoothing is a weighted moving average whose weights follow a bell curve of width $\sigma$. - It reduces white noise like a moving average of about $3.5\sigma$ points, and it does so without ripples in its frequency response. - It gives smoother, more natural-looking curves, but at the same noise reduction it preserves sharp bands only slightly better than a moving average. - Choose $\sigma$ relative to the width of your narrowest band, and check the residuals. ## Next steps - Learn how [Savitzky-Golay smoothing](./savitzky-golay) preserves bands by fitting local polynomials. - See how [Whittaker smoothing](./whittaker) turns smoothing into an optimisation problem. - Explore [derivatives](./derivatives) for resolution enhancement and baseline removal. - Read about [baseline correction](./baseline-correction-asls) for removing slow drifts. ## References # Missing Data (/introduction-to-chemometrics/preprocessing/missing-data) In 1976, the statistician **Donald B. Rubin** published a short paper in *Biometrika* that changed how scientists think about incomplete data . Before Rubin, the usual practice was pragmatic: if a measurement was missing, you either deleted the sample or plugged in the column mean and moved on. Rubin's contribution was to show that what you *can* do about missing data depends on *why* it is missing. His framework introduced the idea of data "missing at random" (MAR); together with the later labels Missing Completely At Random (MCAR) and Missing Not At Random (MNAR), it separates harmless gaps from dangerous ones. It makes it possible to reason about when a simple fix is safe and when it quietly biases every result that follows. Rubin went on to develop **multiple imputation**: filling in each missing value several times, with plausible values drawn at random, analysing every completed dataset, and combining the results . Together with **Roderick J. A. Little**, he wrote *Statistical Analysis with Missing Data* (first edition 1987, third edition 2019), which became the standard reference on the subject . Its ideas now shape practice in fields from clinical trials to economics. In chemometrics, missing data has always been a practical reality rather than a theoretical curiosity. Detectors saturate. Lamps fail in the middle of a scan. Concentrations fall below the limit of detection. Datasets from different instruments cover different wavelength ranges and leave rectangular holes when they are merged. The field did have an early advantage: the algorithm now known as **NIPALS** (nonlinear iterative partial least squares), described by **Herman Wold** in 1966 for computing principal components , can simply skip missing entries while it iterates. [PCA](/introduction-to-chemometrics/models/exploratory-models/pca)-based workflows in chemometrics could therefore tolerate gaps in the data long before general-purpose solutions became routine elsewhere . The best imputation methods in this entry are built on principal component analysis (scores, loadings and the number of components). If PCA is new to you, read [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) first. ## When data go missing in chemistry Missing values in analytical chemistry are not an abstract statistical concern. They come from concrete physical and experimental situations, and knowing the source is the first step towards handling them correctly. **Detector saturation.** Every detector has a dynamic range. When a signal goes beyond its upper limit (for example, an absorbance above about 3 in UV-Vis spectroscopy), the reading is clipped or flagged as invalid. The true value exists, but we cannot see it. This is common for strong absorption bands in concentrated samples. **Failed measurements.** Instruments misbehave. A lamp flickers, a cuvette leaks, a fibre-optic probe loses contact with the sample. The result is a spectrum with corrupted channels, or a missing time point in a series. In process analytical technology, where instruments run continuously for days or weeks, intermittent failures are routine. **Below the detection limit.** When a concentration is too low to distinguish from noise, the result is often recorded as ". **Merged datasets.** When spectra from two instruments are combined (say, one covering 1100 to 2500 nm and another covering 800 to 2200 nm), each instrument contributes blanks in the range it did not measure. The missing values form large, predictable blocks. **Lost samples.** In designed experiments a vial breaks, a reaction fails or a sample degrades before measurement. A whole row of the data matrix is missing, which is a different situation from a few missing wavelengths inside an otherwise complete spectrum. ## Rubin's classification Rubin's framework classifies missingness by how it relates to the data . The class determines which methods are safe. ### Missing Completely At Random (MCAR) The probability that a value is missing has nothing to do with the data, observed or not. It is as if a coin were flipped for every measurement. **Chemistry example:** a technician accidentally skips a few vials in a routine batch because of an interruption, not because of anything about those vials. **Consequence:** MCAR is the most benign case. Even deleting incomplete samples gives unbiased results (you only lose precision), because the remaining samples are still a random subset of the whole. **How to check:** compare the observed variables between samples with and without missing values. If they look alike, MCAR is plausible. Little's test formalises this comparison . No test can tell MAR from MNAR, though, because the evidence would be in the values you did not observe. That distinction must come from knowing the instrument and the chemistry. ### Missing At Random (MAR) The probability of being missing depends on *observed* information, but not on the missing value itself. The gaps are not random, but you can explain them with data you already have. **Chemistry example:** the expensive reference analysis (say, HPLC of the active ingredient) is skipped when the NIR prediction, which is always available, is comfortably high, and run only when that prediction comes close to the lower specification limit. Whether the reference value $y$ is missing depends on the NIR spectrum (observed), not on the reference value itself. The demo below uses the same mechanism: $y$ goes missing more often when $x$ is high. **Consequence:** simply deleting incomplete samples now biases summaries such as the mean of $y$, because the low-potency samples are over-represented among the complete ones. (A regression of $y$ on the NIR data, fitted to the complete samples, is still fine, because the selection depends only on what that regression already uses.) Methods that use the observed information, such as regression-based imputation, multiple imputation or maximum likelihood, can correct for it. MAR is the assumption behind most modern imputation methods. ### Missing Not At Random (MNAR) The probability of being missing depends on the missing value itself. This is the dangerous case: the value is missing *because* of what it would have been. **Chemistry example:** concentrations below the detection limit are left blank. Whether a value is missing depends directly on it being low. Detector saturation is the mirror image: values go missing because they are high. **Consequence:** no standard imputation method can fully correct MNAR without extra assumptions about *how* values go missing. You need to model that process, for example with a censored (Tobit-type) model: left-censored for values below the LOD (we know only that the true value is smaller), right-censored for saturated signals (we know only that it is larger). Treating MNAR data as if they were MAR introduces bias. In analytical chemistry, two of the most frequent causes of missing values, detector saturation and concentrations below the detection limit, are both MNAR: values disappear precisely because they are extreme. Always think about the *mechanism* before choosing a method, because simple approaches can introduce systematic errors without any visible warning. ### Seeing the three mechanisms The demo below has two variables: an absorbance $x$ that is always measured, and a reference value $y$ that is sometimes missing. Because the data are simulated, we know the hidden values and can check what each imputation does to the mean, the standard deviation and the correlation. Things to try: 1. Under **MCAR** with **mean imputation**, the mean of $y$ is about right, but the standard deviation shrinks and the correlation with $x$ drops: all the imputed points sit on one horizontal line. 2. Switch to **MAR**. Now the missing values are mostly at high $x$, so the observed mean of $y$ is too low, and mean imputation inherits that bias. **Regression on x** fixes the mean, because it uses the observed information that explains the gaps. 3. Look closely at **Regression on x**: every filled value sits exactly on a straight line, which no real measurement does. The spread of $y$ is still a bit too small. **Regression + noise** adds a random residual to each filled value, so the imputed points scatter like real data. Press **New draw of the noise** a few times: the numbers wobble from one draw to another. Averaged over many draws (the last row of numbers), the standard deviation comes back closer to the truth than with plain regression, at the price of a slightly weaker correlation, because the added noise knows nothing about $x$. Multiple imputation repeats the draw in the same way and combines the results. 4. Switch to **MNAR**. Every method now overestimates the mean, because the low values are exactly the ones that are missing. Regression on $x$ does much better than the plain mean, since $x$ still carries information about $y$, but it cannot remove the bias completely. ## Simple approaches When the amount of missing data is small and the mechanism is MCAR or close to it, simple methods can be enough. They are also useful baselines against which to compare better methods. ### Listwise deletion (complete cases) Remove every sample (row) that has at least one missing value. $\mathbf{X}_{\text{complete}} = \{ \mathbf{x}_i : \mathbf{x}_i \text{ has no missing values} \}$ **When it works:** MCAR data with few incomplete samples. If 5 % of 200 spectra miss a few wavelengths, deleting those 10 spectra may be perfectly acceptable. **When it fails:** if the missingness is MAR or MNAR, the remaining samples are no longer representative. And if many samples have at least one gap (typical after merging instruments), deletion can throw away most of the data. ### Pairwise deletion Instead of discarding whole samples, use every pair of variables that is available when computing a statistic. The covariance between variables $j$ and $k$, for example, uses all samples in which both are observed. **When it works:** computing correlation or covariance matrices when different variables are missing in different samples. **When it fails:** each entry of the matrix is computed from a different subset of samples, so the result may not be a valid covariance matrix (it can have negative eigenvalues). That breaks PCA and other methods that decompose it. Use with caution. ### Mean or median imputation Replace each missing value with the mean (or median) of the observed values of the same variable: $x_{ij}^{\text{imputed}} = \bar{x}_j = \frac{1}{n_{\text{obs},j}} \sum_{k \in \text{obs}(j)} x_{kj}$ where $\text{obs}(j)$ is the set of samples in which variable $j$ was observed and $n_{\text{obs},j}$ is how many there are. **When it works:** a few values scattered randomly through the matrix. If only a small percentage is missing, the distortion is usually minor. **When it fails:** every imputed value is the same number, so the variance of the variable is artificially reduced and its correlations with other variables are weakened, exactly as you saw in the demo. In PCA, this shrinks scores towards the centre and distorts loadings. The mean is sensitive to [outliers](/introduction-to-chemometrics/preprocessing/outliers): a few extreme readings pull it, and every imputed value inherits that pull. The median is more robust, but it shares the same basic flaw of collapsing the imputed values onto a single number. ## Better approaches When a substantial fraction is missing, or the mechanism is MAR, simple methods introduce unacceptable bias. The following methods use the *structure* of the data to make better guesses. ### k-nearest neighbours (kNN) imputation For a sample with a missing value, find the $k$ most similar samples that *do* have that value, and use a (weighted) average of theirs . 1. **Measure similarity** with a distance (usually Euclidean), computed only over the variables observed in both samples. 2. **Find the $k$ nearest neighbours** among the samples in which the missing variable was observed. 3. **Average their values**, often giving closer neighbours more weight: $x_{ij}^{\text{imputed}} = \frac{\sum_{l=1}^{k} w_l \, x_{lj}}{\sum_{l=1}^{k} w_l}, \qquad w_l = \frac{1}{d(\mathbf{x}_i, \mathbf{x}_l)}$ In practice a small constant is added to the distance to avoid division by zero when a neighbour is identical. Plain, unweighted averaging (all $w_l = 1$) is also common: it is the default in scikit-learn's `KNNImputer` (`weights="uniform"`), and the Python example below switches to the distance weights shown here. **Strengths:** kNN uses local structure. A missing value in a high-absorbance sample is estimated from other high-absorbance samples, not from the global mean. It makes no linearity assumption. **Weaknesses:** it needs a meaningful distance, which becomes unreliable when many variables are missing at once (the distance is computed from fewer and fewer values). It also scales poorly to very large datasets, because each incomplete sample must be compared with every candidate. **Choosing $k$:** values between 3 and 10 are common. A small $k$ follows local structure but is sensitive to noise in individual neighbours; a large $k$ is smoother but may mix distinct groups of samples. ### Iterative PCA (EM-PCA) [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) describes the main patterns of co-variation in the data. If a spectrum is missing a few wavelengths, the wavelengths that *are* present already tell us where that spectrum sits in score space, and the PCA model can then predict what the missing wavelengths should have been. The algorithm alternates between filling the gaps and refitting the model, in the style of expectation-maximisation (EM) : 1. **Initialise** the missing entries with column means (or any reasonable guess). 2. **Fit PCA** with $A$ components to the centred, completed matrix: $\mathbf{X} \approx \mathbf{1}\bar{\mathbf{x}}^T + \mathbf{T}\mathbf{P}^T$. 3. **Predict** the missing entries from the model and replace *only* those entries. Observed values never change: $x_{ij}^{(t+1)} = \bar{x}_j^{(t)} + \sum_{a=1}^{A} t_{ia}^{(t)} \, p_{ja}^{(t)} \quad \text{for every missing } (i,j)$ 4. **Check convergence.** If the imputed values changed by less than a small threshold (for example $10^{-6}$ in relative terms), stop. Otherwise go back to step 2. In practice the PCA is computed with a truncated singular value decomposition (SVD), which is numerically stable and available in every linear algebra library. That is why the same method is also called iterative SVD. The key decision is the number of components $A$. Too few, and the model cannot reconstruct the missing values. Too many, and it starts modelling noise, which then leaks into the imputed values. Use cross-validation: 1. Take the observed part of your data. 2. Hide some additional known values. 3. Impute them with $A = 1, 2, 3, \dots$ components. 4. Choose the $A$ with the smallest error on the hidden values. Typical spectroscopic datasets need only a few components. Too many components reconstruct noise rather than signal. ### Multiple imputation Rather than a single "best guess", multiple imputation generates $M$ completed datasets, each with different imputed values drawn from a plausible distribution . You analyse each one separately and combine the results. For a quantity of interest $\theta$ (a mean, a regression coefficient, a prediction): $\hat{\theta} = \frac{1}{M} \sum_{m=1}^{M} \hat{\theta}_m$ The uncertainty of the combined estimate has two parts: the average uncertainty within each completed dataset, $\bar{W}$, and the spread *between* the datasets, $B$, which reflects how much we do not know about the missing values: $\text{Var}(\hat{\theta}) = \bar{W} + \left(1 + \frac{1}{M}\right) B$ Multiple imputation is the standard in clinical trials and the social sciences. It is less common in chemometrics, where iterative PCA and related methods are preferred because they exploit the strong structure of spectral data directly. ## PCA-based imputation in detail PCA-based imputation deserves special attention in chemometrics because spectroscopic data are usually **low-rank**: hundreds or thousands of wavelengths are driven by a handful of underlying chemical components. That redundancy is exactly what makes reconstruction possible . A systematic comparison of the PCA-based methods, NIPALS and iterative PCA among them, is given by Folch-Fortuny, Arteaga and Ferrer ; Arteaga and Ferrer discuss what each method implies when the PCA model is later used for process monitoring . ### NIPALS and missing values NIPALS computes principal components one at a time, alternating between two small regressions: scores from loadings, and loadings from scores . Each of those regressions can simply leave out the missing entries. For component $a$, NIPALS alternates: $t_{ia} = \frac{\sum_{j \in \text{obs}(i)} x_{ij}\, p_{ja}}{\sum_{j \in \text{obs}(i)} p_{ja}^2} \qquad\text{and}\qquad p_{ja} = \frac{\sum_{i \in \text{obs}(j)} x_{ij}\, t_{ia}}{\sum_{i \in \text{obs}(j)} t_{ia}^2}$ where the sums run only over the observed entries of row $i$ or column $j$. No explicit imputation is needed: the scores and loadings are estimated from what is available, and the missing entries can afterwards be reconstructed as $\hat{x}_{ij} = \sum_a t_{ia}\, p_{ja}$ (plus the column mean, if the data were centred). The properties and limits of this and related approaches were studied in detail by Grung and Manne, and by Nelson, Taylor and MacGregor . ### Comparing the methods on spectra The demo below lets you compare the three families on a small set of simulated NIR spectra built from two chemical components. The heat map shows the whole data matrix with the missing values in red; click a row (or use the **Sample shown** slider) to see that spectrum. The three error values at the bottom are always computed, so you can compare methods directly. Things to try: 1. With **random dropouts**, column-mean imputation is clearly the worst: it ignores everything we know about the sample. kNN does better, and iterative PCA with $A = 2$ (the true number of chemical components) is best. 2. Set $A = 1$, then $A = 3$ or more. One component is not enough to describe two chemical components. With three or more, the extra components start fitting noise, and the error grows several-fold. The algorithm also converges very slowly in that case; the demo tells you when it stops after its maximum number of iterations. This is why the number of components must be chosen with care. Regularised iterative PCA, which shrinks the small components instead of keeping them fully, reduces this problem . 3. Switch to **merged instruments**. Whole blocks are missing. kNN and PCA still work as long as the other samples cover the missing region, which is why the overlap between instruments matters so much. 4. Switch to **below detection limit** (the *New random gaps* button disappears, because this pattern is not random at all). All methods now overestimate the missing values, because the smallest values have been removed systematically. Even at the default amount, a few wavelengths vanish from every spectrum (the demo counts them), and no method can recover them from the data: for those columns all three methods fall back on the same guess. This is MNAR in action: the observed data alone cannot tell the methods how low the missing values were. Now switch on **Use the known detection limit**. Capping the imputed values at the LOD uses what we know about the mechanism, and the error drops several-fold. ## How much missing is too much? There is no universal threshold, but some rough guidelines help frame the decision. These thresholds are rules of thumb, not results. **Less than about 5 % missing, scattered randomly:** almost any method works, and the choice rarely changes downstream results. **About 5 to 20 % missing:** simple methods start to introduce noticeable bias. Prefer kNN or iterative PCA. The *pattern* matters more than the percentage: 15 % missing concentrated in a few variables is more problematic than 15 % spread evenly. **About 20 to 50 % missing:** use iterative PCA or multiple imputation, and check how much your conclusions change with the method. **More than about 50 % missing:** the imputed values are driven more by model assumptions than by data. Consider dropping the affected variables, or reformulating the question. A dataset with 10 % MNAR values (for example, everything below a detection limit) can be more problematic than one with 30 % MCAR values (random instrument dropouts). Always investigate the mechanism, not just the fraction. ### Visualising missingness patterns Before imputing, look at *where* the data are missing. A heat map of the missingness indicator matrix (observed or missing), like the one in the demo above, can reveal: - **Scattered pattern:** random dropouts, consistent with MCAR. Safe for most methods. - **Block pattern:** entire variable ranges or entire samples missing. Common when merging datasets. Iterative PCA handles this well if the blocks overlap enough to constrain the model. - **Monotone pattern:** if you sort samples by completeness, the missing entries form a staircase. This appears, for example, when samples drop out of a time series. Specialised methods exist for it, such as sequential regression imputation, in which each variable is imputed from the more complete ones. - **Systematic pattern:** missing values concentrate in specific variables for specific groups of samples. Strong evidence for MAR or MNAR. Investigate before imputing. ## Code examples The examples simulate low-rank NIR spectra, remove 10 % of the values at random, and compare mean imputation, kNN (Python only) and iterative PCA. With the Python version you should see an RMSE of about 0.17 for mean imputation, 0.05 for kNN and 0.011 for iterative PCA. The last one is close to the noise level of 0.01 that was put into the data, which is as good as any method can do. The MATLAB and R versions use different random numbers, so their values differ slightly. ```python from sklearn.impute import KNNImputer def iterative_pca_imputation(X, n_components=2, max_iter=100, tol=1e-6): """ Impute missing values using iterative PCA (EM-PCA). Parameters: ----------- X : array-like, shape (n_samples, n_variables) Data matrix with np.nan for missing values n_components : int Number of PCA components for reconstruction max_iter : int Maximum number of iterations tol : float Convergence tolerance (relative change in imputed values) Returns: -------- X_filled : array Completed data matrix with imputed values n_iter : int Number of iterations until convergence """ X_filled = X.copy() missing = np.isnan(X) # Initialize missing values with column means col_means = np.nanmean(X, axis=0) for j in range(X.shape[1]): X_filled[missing[:, j], j] = col_means[j] for iteration in range(max_iter): old_imputed = X_filled[missing].copy() # Center the data means = X_filled.mean(axis=0) X_centered = X_filled - means # Truncated SVD (PCA) U, s, Vt = np.linalg.svd(X_centered, full_matrices=False) U = U[:, :n_components] s = s[:n_components] Vt = Vt[:n_components, :] # Reconstruct X_reconstructed = U @ np.diag(s) @ Vt + means # Replace only missing values X_filled[missing] = X_reconstructed[missing] # Check convergence new_imputed = X_filled[missing] change = np.sqrt(np.sum((new_imputed - old_imputed)**2)) scale = np.sqrt(np.sum(new_imputed**2)) + 1e-10 if change / scale # Moving Average Smoothing (/introduction-to-chemometrics/preprocessing/moving-average) The idea of smoothing data by averaging neighbouring values is older than its name. In 1901 the English statistician **R. H. Hooker** published a study relating marriage rates to trade prosperity in which he computed what he called "instantaneous averages": each year's figure was replaced by the mean of itself and the years around it, so that the long-term trend could be seen through the year-to-year fluctuations . In 1909 **G. U. Yule** described Hooker's averages as "moving-averages" in the *Journal of the Royal Statistical Society* , and the term spread after **W. I. King** used it in his textbook *The Elements of Statistical Method* (1912) . The practice itself is older still. In 1870 the actuary **W. S. B. Woolhouse** published a method for "graduating" mortality tables that replaced each death rate by a weighted average of the rates at neighbouring ages, to iron out irregularities caused by small numbers . In 1884 the physicist **J. H. Poynting** averaged successive years of wheat prices and of cotton and silk imports to see the trend beneath the year-to-year fluctuations . Both used the procedure long before it had a name. What makes the moving average so durable is its simplicity. When digital signal processing appeared in the mid-20th century and engineers needed filters that the earliest computers could run, the moving average (called a **boxcar filter** in that world, because its weights form a flat rectangular box) was the obvious first choice. It needs only additions and one division. That is why it is the first smoothing method most scientists learn, and the natural starting point before more refined methods such as [Savitzky-Golay](./savitzky-golay) or [Whittaker](./whittaker) smoothing. ## The noise problem in spectroscopy Real spectroscopic data are noisy. When you record a fluorescence emission spectrum, an IR absorption scan or a Raman spectrum, the result is not a smooth curve. It wiggles up and down, even when the same sample is measured twice under identical conditions. This is not a flaw of your instrument. It is physics. A spectrometer counts photons (or electrons), their arrival is random, and the detector and electronics add their own thermal noise, so even the best instruments produce noisy data. We discussed where measurement uncertainty comes from in [Least Squares: your first step into chemometrics](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics), and the different kinds of noise in the [noise reduction overview](./noise-reduction). Your brain sees through the noise and recognises a band here and a baseline there. A computer only sees numbers: 0.421, 0.398, 0.447, 0.389... Random fluctuations can create false peaks or hide real ones, they add uncertainty to band heights and areas, and they can be fitted by calibration models as if they were information. To reduce these problems we use **smoothing**: methods that remove random fluctuations while keeping the underlying chemical information. ## The core idea: averaging with neighbours The moving average rests on one observation: > **Noise is independent from point to point. Signal, very often, is continuous.** Many spectroscopic signals come from physical phenomena (molecular vibrations, electronic transitions, fluorescence emission) that tend to change *gradually* with wavelength. A molecule's absorbance does not jump wildly from one nanometre to the next. If the absorbance at 500 nm is 0.40, the absorbance at 501 nm will be close to 0.40, not 0.95 or 0.02. Neighbouring points carry almost the same information. Noise does not behave like that. If it pushes the value at 500 nm up, it is just as likely to push the value at 501 nm down. Suppose the true signal at 499, 500 and 501 nm is 0.39, 0.40 and 0.41, but noise makes you measure 0.35, 0.46 and 0.38. The average of the three measurements is 0.397, very close to the true value at 500 nm. The errors (some positive, some negative) partly cancelled; the signal, nearly the same at the three points, survived. Not every signal is continuous, though. Mass spectra or atomic emission lines can be so sharp that neighbouring points are *not* similar, and averaging them destroys the feature. Keep the nature of your signal in mind before smoothing anything. **Moving average smoothing** (also called running mean or boxcar filter) applies this principle by replacing each point with the average of itself and its neighbours: $\hat{y}_i = \frac{1}{w} \sum_{j=i-m}^{i+m} y_j, \qquad m = \frac{w-1}{2}$ where: - $\hat{y}_i$ is the smoothed value at point $i$, - $w$ is the window size (the number of points averaged), - $m$ is the number of neighbours taken on each side. That is the whole method. ## See it in action Before going further, watch the window slide along a spectrum: **Try this:** 1. Click **Slide the window** to watch the window travel across the spectrum, building the smoothed curve point by point. 2. Use the second slider to step through the points by hand. 3. Change the window size and observe the smoothing effect. 4. Notice how larger windows give smoother curves but lower and widen the bands. 5. Move to the first or last points and see the window shrink, because there are no neighbours beyond the edge. The ringed red points are the neighbours being averaged, and the orange ring is the smoothed value they produce. ## How it works in practice For a spectrum with intensities $y_1, y_2, \ldots, y_n$: 1. **Choose a window size** $w$ This decides how many points take part in each average. It is usually odd, for reasons explained below. 2. **For each point, gather its neighbours** Collect the values from $i - m$ to $i + m$: the point itself and $m$ neighbours on each side. 3. **Average them** Add the values and divide by $w$. 4. **Store the result as the smoothed value** Always average the *original* values, never values that have already been smoothed in the same pass. ### Example calculation Suppose you measured these intensities: ``` Original: [10.2, 15.1, 12.3, 19.8, 18.2, 15.9, 14.1, 18.7, 17.2, 15.3] ``` With a window of 3, each interior point is averaged with its two immediate neighbours (counting positions from 0): - Position 1: $(10.2 + 15.1 + 12.3) / 3 = 12.5$ - Position 2: $(15.1 + 12.3 + 19.8) / 3 = 15.7$ - Position 3: $(12.3 + 19.8 + 18.2) / 3 = 16.8$ - Position 4: $(19.8 + 18.2 + 15.9) / 3 = 18.0$ Continuing to the end gives: ``` Smoothed (positions 1 to 8): [12.5, 15.7, 16.8, 18.0, 16.1, 16.2, 16.7, 17.1] ``` The jump from 12.3 to 19.8 has become a gentler climb. The first and last positions have no neighbour on one side; we come back to them in the section on edge effects. ## How much noise does it remove? We can say exactly how much, for random noise. Assume each point carries independent noise with standard deviation $\sigma$. The smoothed value is the sum of $w$ noisy values divided by $w$. Variances of independent errors add, so $\operatorname{Var}\left(\frac{1}{w}\sum_{j} \varepsilon_j\right) = \frac{1}{w^2}\, w\,\sigma^2 = \frac{\sigma^2}{w}$ and the noise standard deviation after smoothing is $\sigma_{\text{smoothed}} = \frac{\sigma}{\sqrt{w}}$ A 9-point window divides the noise by 3, a 25-point window by 5 . This is the same square-root law that governs averaging repeated scans, with one crucial difference: repeated scans average measurements of the *same* point, while the moving average mixes *different* points. As long as the signal is nearly constant across the window that does not matter. When it is not, the signal is distorted too. There is a stronger result hiding here. Among all averaging filters of the same length whose weights sum to one, equal weights give the smallest output variance, $\sigma^2/w$: the moving average is the *best* possible noise remover for a given window . (Minimising $\sum_j w_j^2$ with $\sum_j w_j = 1$ gives $w_j = 1/w$ for every point.) Its weakness is not noise, but what it does to the signal. ## The fundamental trade-off: smoothness versus detail That distortion is the price of the noise reduction. Averaging a sharp band with the baseline around it pulls its top down and spreads it sideways. The panel below makes the trade-off measurable. The metrics compare the noise left after smoothing with the $\sigma/\sqrt{w}$ prediction, and show how much of each band's height survives. Because the panel knows the true signal and the noise it added, it can measure both without the noise getting in the way. Notice what happens as you widen the window: - The noise follows the $1/\sqrt{w}$ law closely. - The broad band barely changes until the window becomes large. - The narrow band starts losing height as soon as the window approaches its width at half height (the **FWHM**, full width at half maximum). When the window is about as wide as the band, roughly a fifth of its height is gone. So the right window depends less on the noise than on your **narrowest feature that matters**. **Keep the window clearly smaller than the FWHM of the narrowest band you care about**, and large enough to reduce the noise to an acceptable level. Think in data points per band, not in absolute numbers. **Rule of thumb:** for a band of Gaussian shape, a window of half the FWHM costs about 5 % of the band height; a window equal to the FWHM costs about 20 %. **Approach:** start small, plot the result and the residuals, increase gradually, and stop as soon as band heights begin to fall or the residuals show band-shaped structure. ### Example: scaling with data density The same chemistry can be sampled with 100 points or with 10,000, so window sizes only make sense relative to band width: - **A spectrum with 100 points and bands about 10 to 20 points wide**: start with $w = 3$ or 5; very noisy data may tolerate 7. - **A high-resolution spectrum with bands about 100 points wide**: start around $w = 11$ to 15, and go up to 31 or more if the noise demands it. - **Very dense data (tens of thousands of points)**: windows of 101 or 201 points can be perfectly reasonable. The ratio to the band width is what counts. ### Why use odd window sizes? With an odd window (3, 5, 7, ...) there are equally many neighbours on each side of the central point: - $w = 5$: 2 left, the centre, 2 right - $w = 7$: 3 left, the centre, 3 right With an even window (4, 8, ...) the window cannot be centred on a point, so the result belongs halfway between two points. If it is stored at one of them, the whole smoothed spectrum shifts by half a point. Odd windows keep everything centred and band positions unchanged. ## Edge effects At the start and end of the spectrum there are not enough neighbours on one side to fill the window. Software handles this in different ways, and it is worth knowing which one yours uses: - **Leave the edges out**: only the interior points with complete windows are smoothed, and the first and last $m$ points are either kept unsmoothed or marked as missing. R's `stats::filter` returns `NA` there, for example. - **Zero padding**: the spectrum is extended with zeros. This is what `np.convolve(..., mode='same')` in NumPy and `conv(..., 'same')` in MATLAB do, and it pulls the edge values towards zero, which is usually wrong for spectra. - **Edge padding**: the first and last values are repeated to create artificial neighbours. - **Reflection**: the spectrum is mirrored at the edges. - **Shrinking window**: near the edges only the neighbours that exist are used. MATLAB's `movmean` and `smoothdata` do this by default with a truncated, one-sided window (at the very first point of a 5-point window they average points 1 to 3). The panels on this page use a smaller *symmetric* window instead, so the very first and last points are left unsmoothed. The interesting features are often in the middle of the spectrum, so edges are rarely critical. But if you integrate up to the end of the range, or the spectrum is short, check what your software does. ## Real-time applications: the causal moving average Everything so far assumes the whole data set is available. In offline spectroscopy you record the full spectrum first and process it afterwards, so a window can reach both backwards and forwards. In **real-time** monitoring that is impossible: the future has not been measured yet. Typical examples are: - **PAT (Process Analytical Technology)**: real-time NIR or Raman monitoring of pharmaceutical manufacturing. - **Online quality control** of production streams. - **Process monitoring** of reactors, fermentations or polymerisations. - **Kinetic measurements**, where a property is followed over time. Here we smooth a *time series* (for example the concentration predicted from each new spectrum), and we need a **causal**, or one-sided, moving average that uses only the current point and the past: $\hat{y}_i = \frac{1}{w} \sum_{j=i-w+1}^{i} y_j$ The window now extends only backwards in time. In the panel below the window appears only to the **left** of the current point. Increase the window size, slide the window through the changes in the process, and watch the smoothed curve lag further behind the true signal: this delay is the fundamental cost of real-time smoothing. Here even window sizes are fine, because there is no centre to preserve. ### The trade-off: delay versus smoothing A causal moving average delays the signal. For a steady trend, the smoothed value lags the true one by $\text{delay} = \frac{w-1}{2} \text{ samples}$ because the average of the last $w$ time points sits at the middle of that stretch of time. **Example**: with $w = 9$ and one measurement every 30 seconds, the delay is 4 measurements, so 2 minutes. When the process changes, the smoothed signal shows it about 2 minutes later. **Practical implications:** - Larger windows reduce more noise but delay more. - In feedback control, delays can make a control loop unstable. - In alarm systems, delayed detection can be critical. **Choosing the window for real time:** - **Fast processes** (reactions, mixing): small windows (for example $w = 3$ to 5). - **Slow processes** (fermentation, curing): larger windows (for example 7 to 11) are acceptable. - **Critical alarms**: favour fast detection over smoothness. - **Trending and reporting**: delay matters less, so stronger smoothing is fine. ### Implementation for real time ```python def causal_moving_average(y, window_size): """ One-sided moving average for real-time applications. Only uses the current and past values of a time series. """ smoothed = [] for i in range(len(y)): start = max(0, i - window_size + 1) # window extends backwards only # (the first w-1 outputs average fewer points, because there is no earlier data) window_values = y[start:i + 1] smoothed.append(sum(window_values) / len(window_values)) return smoothed # PAT example: one predicted concentration per NIR spectrum, every 30 s predicted_concentration = [2.31, 2.35, 2.29, 2.40, 2.38, 2.52, 2.47] trend = causal_moving_average(predicted_concentration, window_size=5) ``` In a running process you would not recompute the whole list each time: keep the last $w$ values in a buffer and update the average as each new value arrives. **Never use a centred moving average in real time.** It needs future data that do not exist yet. Use a causal (one-sided) filter for online monitoring, real-time quality control, feedback control and live time series. Use a centred moving average for batch analysis, post-processing of stored spectra, and any offline work where all the data are available. ## Common questions ### Can I apply a moving average several times? You can, and it is not as silly as it sounds: applying a boxcar twice is the same as applying a single triangular weighting, and after several passes the combined weights look more and more like a bell curve. That is one route to [Gaussian smoothing](./gaussian-smoothing). But each pass also blurs more, so if what you want is simply *more* smoothing, choose a larger window or a better method rather than stacking passes. ### Should I use the same window size for all samples? **Yes.** If you build a calibration model or compare spectra, every spectrum (calibration, validation and test) must be processed with the same window. Inconsistent smoothing creates differences that have nothing to do with chemistry . ### How do I know if I am removing signal instead of noise? Check the residuals: 1. Smooth your data. 2. Subtract: $r_i = y_i - \hat{y}_i$. 3. Plot the residuals. **Good smoothing**: the residuals look like random noise, with no pattern and centred on zero. **Over-smoothing**: the residuals show structure, such as the shape of half a band. You are removing signal; use a smaller window. ### Why does my band look shorter after smoothing? Averaging a sharp band with its neighbouring baseline pulls the top down. The band becomes shorter and broader, although its area stays the same (exactly, away from the edges), because the intensity is redistributed rather than removed. This is why the moving average is not ideal for quantitative work based on the heights of sharp bands. [Savitzky-Golay smoothing](./savitzky-golay) preserves band heights much better . ## Code implementation Here is how to implement a moving average in Python, MATLAB and R. The example spectrum has one point every 2 nm, so its main band (at 480 nm) is about 16 points wide at half height: a window of 15 is close to the FWHM of that band, so expect it to lose a clear part of its height (about 13 % here; a 7-point window loses about 3 %). ```python def moving_average(y, window_size): """ Apply moving average smoothing to a 1D signal. Parameters: ----------- y : array-like Input signal to be smoothed (e.g., spectrum intensities) window_size : int Number of points in the averaging window (use odd numbers!) Returns: -------- y_smooth : array Smoothed signal (same length as input) """ # Create the averaging kernel (array of 1/w values) window = np.ones(window_size) / window_size # Apply convolution. Note: mode='same' pads with zeros, so the first and # last (w-1)/2 values are pulled towards zero (see 'Edge effects') y_smooth = np.convolve(y, window, mode='same') return y_smooth # Example: noisy fluorescence emission spectrum np.random.seed(42) # Wavelength range typical for fluorescence (nm) wavelength = np.linspace(400, 600, 100) # Simulate a realistic fluorescence spectrum with vibronic structure true_signal = (0.85 * np.exp(-((wavelength - 480)**2) / 400) + 0.35 * np.exp(-((wavelength - 510)**2) / 300) + 0.15 * np.exp(-((wavelength - 545)**2) / 250)) # Add realistic shot noise (proportional to sqrt of signal) noise = np.random.normal(0, 0.08 * np.sqrt(true_signal)) noisy_spectrum = true_signal + noise # Try different window sizes smooth_w5 = moving_average(noisy_spectrum, window_size=5) smooth_w9 = moving_average(noisy_spectrum, window_size=9) smooth_w15 = moving_average(noisy_spectrum, window_size=15) # Plot and compare plt.figure(figsize=(12, 6)) plt.plot(wavelength, noisy_spectrum, 'o-', alpha=0.4, label='Noisy data', color='gray', markersize=3) plt.plot(wavelength, smooth_w5, linewidth=2.5, label='Window = 5') plt.plot(wavelength, smooth_w9, linewidth=2.5, label='Window = 9') plt.plot(wavelength, smooth_w15, linewidth=2.5, label='Window = 15') plt.xlabel('Wavelength (nm)', fontsize=12) plt.ylabel('Fluorescence Intensity (a.u.)', fontsize=12) plt.title('Moving Average Smoothing: Window Size Comparison', fontsize=14) plt.legend(fontsize=10) plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # Check residuals residuals = noisy_spectrum - smooth_w9 plt.figure(figsize=(12, 4)) plt.plot(wavelength, residuals, 'o-', markersize=3) plt.axhline(y=0, color='red', linestyle='--', alpha=0.5) plt.xlabel('Wavelength (nm)') plt.ylabel('Residual (Original - Smoothed)') plt.title('Residuals: Should look like random noise with no patterns') plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() ``` ```matlab % Example: fluorescence emission spectrum % (moving_average is defined at the end: MATLAB requires local functions % to come after the script code) rng(42); % Typical fluorescence wavelength range (nm) wavelength = linspace(400, 600, 100); % Simulate realistic fluorescence with vibronic bands true_signal = 0.85 * exp(-((wavelength - 480).^2) / 400) + ... 0.35 * exp(-((wavelength - 510).^2) / 300) + ... 0.15 * exp(-((wavelength - 545).^2) / 250); % Add realistic shot noise noise = 0.08 * sqrt(true_signal) .* randn(size(wavelength)); noisy_spectrum = true_signal + noise; % Try different window sizes smooth_w5 = moving_average(noisy_spectrum, 5); smooth_w9 = moving_average(noisy_spectrum, 9); smooth_w15 = moving_average(noisy_spectrum, 15); % Visualize the comparison figure('Position', [100 100 1000 500]); plot(wavelength, noisy_spectrum, 'o-', 'Color', [0.7 0.7 0.7], ... 'MarkerSize', 3, 'DisplayName', 'Noisy data'); hold on; plot(wavelength, smooth_w5, 'LineWidth', 2.5, 'DisplayName', 'Window = 5'); plot(wavelength, smooth_w9, 'LineWidth', 2.5, 'DisplayName', 'Window = 9'); plot(wavelength, smooth_w15, 'LineWidth', 2.5, 'DisplayName', 'Window = 15'); xlabel('Wavelength (nm)', 'FontSize', 12); ylabel('Fluorescence Intensity (a.u.)', 'FontSize', 12); title('Moving Average: Window Size Comparison', 'FontSize', 14); legend('Location', 'northeast', 'FontSize', 10); grid on; % Check residuals figure('Position', [100 100 1000 300]); residuals = noisy_spectrum - smooth_w9; plot(wavelength, residuals, 'o-', 'MarkerSize', 3); yline(0, '--r', 'LineWidth', 1.5); xlabel('Wavelength (nm)'); ylabel('Residual'); title('Residuals: Should be random noise, no structure'); grid on; function y_smooth = moving_average(y, window_size) % Apply moving average smoothing to a 1D signal % % Parameters: % y - Input signal to smooth (column or row vector) % window_size - Number of points in averaging window (use odd!) % % Returns: % y_smooth - Smoothed signal (same size as input) % Create the averaging kernel window = ones(1, window_size) / window_size; % Apply convolution. 'same' pads with zeros at the edges; % movmean(y, window_size) shrinks the window there instead y_smooth = conv(y, window, 'same'); end ``` ```r moving_average # Multiplicative Scatter Correction (MSC) (/introduction-to-chemometrics/preprocessing/msc) In the late 1970s and early 1980s, near-infrared (NIR) spectroscopy was rapidly gaining ground as a fast, non-destructive way to analyse agricultural and food products. You could point an NIR instrument at ground grain, flour or minced meat and, in seconds, obtain a spectrum related to its protein, moisture or fat content. But there was a stubborn problem: the spectra were dominated by **light scattering**. When NIR light enters a powder or a granular sample, much of it bounces between particles before it reaches the detector. How much it bounces depends on physical properties (particle size, surface texture, how tightly the sample is packed) that have nothing to do with chemistry. Two samples with the same composition but a different grind could give visibly different spectra. **Harald Martens** and his colleagues in Norway attacked this problem with a strikingly simple idea. If scattering mostly shifts a spectrum up or down and stretches it, then each spectrum should look like a shifted and stretched copy of some "typical" spectrum. Estimate the shift and the stretch with a straight-line regression, undo them, and what remains should be mostly chemistry. The method was first presented by Martens, Jensen and Geladi in 1983 and became widely known through the 1985 paper by Geladi, MacDougall and Martens on NIR reflectance spectra of meat . They called it **Multiplicative Scatter Correction (MSC)**; you will also see it called *multiplicative signal correction*, with the same acronym, the reading used by Martens and Stark when they extended the method in 1991 . MSC was then given a central place in Martens and Næs' book *Multivariate Calibration* (1989) , one of the foundational textbooks of chemometrics. Its simplicity (one small regression per spectrum) and its clear physical meaning made it one of those rare methods that practitioners adopted almost universally. Decades later, it is still one of the most common preprocessing steps for NIR spectra of solids . ## The scatter problem in spectroscopy In diffuse reflectance, light that comes back from the sample has travelled through it along many different paths. Fine particles scatter more and send light back after a short path; coarse particles let it travel further. The chemistry is the same, but the *effective path length* is not, and the spectrum changes accordingly. In practice, scattering produces two main effects: - an **additive** effect: the whole spectrum moves up or down (a baseline offset), - a **multiplicative** effect: the whole spectrum is stretched or compressed, so tall bands change more than small ones. The problem is that these physical effects are often *larger* than the chemical differences we want to measure. A PCA of raw NIR spectra of powders very often spends its first component describing particle size. A PLS model can still cope, but it needs extra components just to model scatter, and it becomes harder to interpret and less robust. MSC tries to remove both effects before modelling. ## The core idea: a straight line against a reference MSC assumes that every measured spectrum $\mathbf{x}_i$ is approximately a shifted and stretched copy of a reference spectrum $\mathbf{x}_{\text{ref}}$: $\mathbf{x}_i \approx a_i\mathbf{1} + b_i\,\mathbf{x}_{\text{ref}} + \mathbf{e}_i$ Here: - $a_i$ is the **additive** scatter term (the offset), - $b_i$ is the **multiplicative** scatter term (the stretch), - $\mathbf{1}$ is a vector of ones, one per wavelength, - $\mathbf{e}_i$ is whatever the line cannot explain. Ideally, that is the **chemical** information plus noise. This is just a simple linear regression, but with an unusual twist: the "x" of the regression is the reference spectrum and the "y" is the sample spectrum, and the data points are the **wavelengths**. With a spectrum of a few hundred wavelengths, we fit a straight line through hundreds of points to estimate only two numbers. Once $a_i$ and $b_i$ are known, we undo the distortion: $\mathbf{x}_i^{\text{MSC}} = \frac{\mathbf{x}_i - a_i}{b_i}$ Subtracting $a_i$ removes the offset, and dividing by $b_i$ removes the stretch. After the correction, every spectrum has been brought onto the same "scatter level" as the reference. In practice $b_i$ should be close to 1. A value near zero, or a negative one, means that the spectrum does not resemble the reference at all (a failed measurement, a different kind of sample), and it should be inspected rather than corrected: dividing by a tiny $b_i$ would blow the spectrum up. ### Seeing the regression The demo below shows what that regression looks like for one spectrum. Every dot is one wavelength. If the sample differed from the reference *only* by scatter, all the dots would fall exactly on a straight line. They nearly do: the line is the scatter. The few dots that drift away from the line, more visible for the samples at the extremes of the colour scale, are where the chemistry of this sample differs from the average. Pick different samples and compare the fitted $a$ and $b$ with the values used to simulate the data. They are close, but not identical, because the chemical differences also nudge the regression a little. We come back to that in the limitations. Keep in mind that the fitted values are always *relative to the reference*: the mean spectrum itself carries the average scatter of the set, so in general the fitted $b_i$ estimates the true stretch divided by the average stretch. Here the average simulated stretch is almost exactly 1, so the two look alike. The third plot shows what the straight line leaves behind at each wavelength. The residual peaks at the analyte bands: that is the chemistry MSC is supposed to keep. ## The algorithm **Step 1: choose a reference spectrum.** Almost always, this is the mean spectrum of the calibration (training) set: $\mathbf{x}_{\text{ref}} = \bar{\mathbf{x}} = \frac{1}{N} \sum_{i=1}^{N} \mathbf{x}_i$ **Step 2: regress each spectrum on the reference.** For every sample, fit $\mathbf{x}_i \approx a_i + b_i\,\mathbf{x}_{\text{ref}}$ by ordinary least squares. In matrix form, with $\mathbf{Z} = [\mathbf{1},\ \mathbf{x}_{\text{ref}}]$, this is exactly the problem solved by the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations): $\begin{bmatrix} a_i \\ b_i \end{bmatrix} = (\mathbf{Z}^T\mathbf{Z})^{-1}\mathbf{Z}^T\mathbf{x}_i$ **Step 3: correct.** $\mathbf{x}_i^{\text{MSC}} = \frac{\mathbf{x}_i - a_i}{b_i}$ **Step 4: store the reference.** New samples (test set, future measurements) are corrected against the **same** stored reference. They are never used to recompute it. That is all. MSC is one of the cheapest preprocessing methods there is: two numbers per spectrum. ## Interactive example Now let us look at a whole set of spectra. The eight simulated samples differ in their content of one analyte, which has a band at 1730 nm (dashed line). Their colours go from blue (low content) to amber (high content). On top of that chemistry, each spectrum has received a random offset and stretch. Things to try: 1. Set the scatter to zero. Raw and corrected spectra look the same, and the R² of the band is already high. 2. Raise the scatter. The raw spectra fan out, the colour order at 1730 nm is scrambled, and the R² of the raw band collapses. After MSC, the spectra lie on top of each other except at the chemical bands, and the R² comes back. 3. Now look at **analyte signal kept**. It compares the spread of the corrected band heights with the true, scatter-free ones, and it stays around 84 % whatever the scatter, even at zero. Part of the chemical difference looks like a "stretch" to the regression, so MSC removes about 15 % of the analyte band together with the scatter. The R² stays high because the loss is proportional (every sample loses the same fraction), so a calibration model can still work, but band heights after MSC are biased towards the mean and should not be read as absolute amounts. 4. Move the chemical variation slider. The fraction kept barely moves in this simulation, because only the analyte changes from sample to sample. The leak is worst when the varying chemistry looks like the reference spectrum itself. Set the chemical variation to zero and the R² values show "n/a": with identical chemistry there is nothing to measure. ## When to use MSC MSC is a good candidate when: - you measure **diffuse reflectance** (or transmittance) of **solids**: powders, grains, tablets, granulates, minced or ground materials, - the spectra show a **fan shape**: offset and spread that grow together across the spectrum, - the first principal component of the raw spectra mostly describes **particle size or packing** rather than chemistry, - the samples are chemically **similar enough** that a common reference makes sense. MSC is a poor choice when: - the samples are chemically very different (the "same spectrum, different scatter" assumption breaks down), - scatter is negligible, for example clear liquids in transmission, - the scatter effect is strongly **wavelength dependent**, so that a single offset and a single stretch cannot describe it. That is the situation [EMSC](/introduction-to-chemometrics/preprocessing/emsc) was designed for. ## Code examples ```python def msc(spectra, reference=None, mask=None): """ Multiplicative Scatter Correction. spectra : array (n_samples, n_wavelengths) reference : array (n_wavelengths,), optional. Defaults to the mean spectrum. mask : boolean array (n_wavelengths,), optional. If given, a and b are estimated only from these wavelengths (regions with little chemical variation); the correction is applied to all of them. Returns (corrected spectra, reference). Keep the reference to correct new samples. """ spectra = np.asarray(spectra, dtype=float) if reference is None: reference = spectra.mean(axis=0) if mask is None: mask = np.ones(reference.size, dtype=bool) Z = np.column_stack([np.ones_like(reference), reference]) # [1, x_ref] corrected = np.empty_like(spectra) for i, x in enumerate(spectra): a, b = np.linalg.lstsq(Z[mask], x[mask], rcond=None)[0] # x ≈ a + b * x_ref corrected[i] = (x - a) / b return corrected, reference # Simulated NIR spectra: same chemistry, different scatter rng = np.random.default_rng(42) wl = np.linspace(1100, 2500, 300) band = 0.5 * np.exp(-((wl - 1500) ** 2) / 5000) + 0.3 * np.exp(-((wl - 2000) ** 2) / 3000) chem = band + 0.2 # identical for every sample raw = [] for _ in range(20): a = rng.uniform(-0.1, 0.1) # additive scatter b = rng.uniform(0.8, 1.2) # multiplicative scatter raw.append(a + b * chem + rng.normal(0, 0.002, wl.size)) raw = np.array(raw) train, test = raw[:15], raw[15:] train_msc, ref = msc(train) # reference from the training set only test_msc, _ = msc(test, reference=ref) print('spread between raw spectra :', np.ptp(train, axis=0).max()) print('spread between corrected spectra:', np.ptp(train_msc, axis=0).max()) # only noise left fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4), sharey=False) ax1.plot(wl, train.T, lw=1); ax1.set_title('Raw') ax2.plot(wl, train_msc.T, lw=1); ax2.set_title('After MSC') for ax in (ax1, ax2): ax.set_xlabel('Wavelength (nm)') plt.tight_layout() plt.show() ``` ```matlab % Simulated NIR spectra: same chemistry, different scatter rng(42); wl = linspace(1100, 2500, 300); band = 0.5 * exp(-((wl - 1500).^2) / 5000) + 0.3 * exp(-((wl - 2000).^2) / 3000); chem = band + 0.2; % identical for every sample raw = zeros(20, numel(wl)); for i = 1:20 a = -0.1 + 0.2 * rand; % additive scatter b = 0.8 + 0.4 * rand; % multiplicative scatter raw(i, :) = a + b * chem + 0.002 * randn(size(wl)); end train = raw(1:15, :); test = raw(16:20, :); [trainMSC, ref] = msc(train); % reference from the training set only testMSC = msc(test, ref); subplot(1, 2, 1); plot(wl, train'); title('Raw'); xlabel('Wavelength (nm)'); subplot(1, 2, 2); plot(wl, trainMSC'); title('After MSC'); xlabel('Wavelength (nm)'); function [corrected, reference] = msc(spectra, reference, mask) % Multiplicative Scatter Correction. Rows are samples. % mask (optional): logical vector; a and b are estimated only from these % wavelengths, and the correction is applied to all of them. if nargin . **Limitations** - **It assumes the samples are chemically similar.** The regression cannot tell a chemical difference from a stretch. When the chemical bands are large, part of them leaks into $b_i$ and gets "corrected" away. Some practitioners therefore estimate $a_i$ and $b_i$ only from wavelength regions with little chemical variation, and then apply the correction to the whole spectrum . That is what the `mask` argument of the code above does. A related idea, piecewise MSC, fits separate corrections in moving windows along the spectrum . - **The reference matters.** The correction is relative to the reference spectrum. A reference dominated by a few unusual samples gives a poor correction. - **Only one offset and one stretch.** Real scatter often depends on wavelength in a more complicated way (for example, it can produce sloping or curved baselines). MSC cannot remove that; EMSC can. ## Practical tips **Choosing the reference.** - The mean spectrum of the training set is the standard choice. - A median spectrum is more robust if there are a few outlying samples. - Avoid using a single, randomly chosen sample as the reference: its own noise and peculiarities are copied into every corrected spectrum. **Signs that MSC will help.** - The spectra fan out, with offsets and spread that grow together. - The first principal component of the raw spectra looks like a smooth, positive "average spectrum" shape, and its scores follow particle size or packing. - A PLS model needs more components than you would expect from the chemistry. **Combining with other preprocessing.** - MSC followed by **mean centering** is a standard combination before PCA or PLS. - MSC can be followed by a **derivative** if residual baseline effects remain. - MSC and **SNV** correct the same effects, so you normally choose one of them, not both. ## SNV: the reference-free cousin **SNV** (Standard Normal Variate) was proposed by Barnes, Dhanoa and Lister in 1989 . It corrects each spectrum on its own: subtract the spectrum's own mean $\bar{x}_i$ (the average over its wavelengths) and divide by its own standard deviation $s_i$: $\mathbf{x}_i^{\text{SNV}} = \frac{\mathbf{x}_i - \bar{x}_i}{s_i}$ ```python def snv(spectra): spectra = np.asarray(spectra, dtype=float) mean = spectra.mean(axis=1, keepdims=True) std = spectra.std(axis=1, keepdims=True) return (spectra - mean) / std ``` After SNV every spectrum has mean 0 and standard deviation 1, so an offset and a stretch disappear, just as with MSC. The difference is that SNV needs no reference, so there is nothing to store and nothing that can leak from a test set. The two are almost the same transformation . The least squares slope of the MSC regression is $b_i = r_i\, s_i / s_{\text{ref}}$, where $r_i$ is the correlation between the spectrum and the reference and $s_{\text{ref}}$ the standard deviation of the reference. Putting that into the MSC formula gives $\mathbf{x}_i^{\text{MSC}} = \bar{x}_{\text{ref}} + \frac{s_{\text{ref}}}{r_i}\,\mathbf{x}_i^{\text{SNV}}$ Spectra that resemble the reference have $r_i$ close to 1, so MSC is essentially SNV rescaled to the mean and spread of the reference. That is why the two give very similar models. SNV is convenient when no representative reference is available, because it needs nothing but the spectrum itself. MSC keeps the corrected spectra on the scale of the reference, which some people find easier to interpret. ## Common mistakes to avoid - **Recomputing the reference for new data.** The reference is part of the model. Compute it from the training set, store it, and correct test and future spectra against it. - **Letting test samples into the reference.** If the mean spectrum includes the test set, information from the test set leaks into the training data and the validation looks better than it should. - **Thinking MSC cannot handle a single new spectrum.** It can, as long as you have the stored reference. What you cannot do is build a meaningful reference from one spectrum. - **Stacking MSC and SNV.** They do almost the same job; applying both rarely helps. - **Ignoring outliers in the reference.** A few extreme spectra can distort the mean. Look at your data first, or use a median reference. ## What to remember MSC treats scatter as a straight-line distortion: every spectrum is a shifted ($a_i$) and stretched ($b_i$) copy of a reference spectrum. A least squares fit across wavelengths estimates both numbers, and the correction $(\mathbf{x}_i - a_i)/b_i$ undoes them. It works best when samples are chemically similar and scatter is a single offset and stretch. Keep the reference from the training set, and if the scatter has a more complicated shape, move on to EMSC. ## References # Noise Reduction (/introduction-to-chemometrics/preprocessing/noise-reduction) Every measurement is a mixture of two things: the information we want and a layer of random variation we did not ask for. Noise reduction is the art of removing as much of the second as possible while touching the first as little as possible. It sounds simple, and the basic tools really are simple. The difficult part is knowing **which** unwanted variation you are dealing with, because only some of it can be smoothed away. ## A short history The language we use for noise came into analytical chemistry from electrical engineering. During the 1940s, radar and telecommunications forced engineers to think rigorously about separating meaningful signals from background interference. **Claude Shannon's** 1948 paper *A Mathematical Theory of Communication* set the theoretical limits for sending information through a noisy channel , and **Norbert Wiener's** wartime work on optimal filtering, published in 1949 as *Extrapolation, Interpolation, and Smoothing of Stationary Time Series*, gave the mathematics for pulling a signal out of noise . Spectroscopists soon adopted the same vocabulary to describe the performance of their instruments. Before computers, smoothing was a physical act. You slowed down the scan, opened the slit, or put an analog RC filter (a resistor and a capacitor that damp fast fluctuations) on the detector output. Each trick improved the signal-to-noise ratio, but it cost resolution or time. Once instruments began to produce digital output, a list of numbers at evenly spaced wavelengths, smoothing could be done *after* the measurement, in software, without changing how the data were acquired. **Savitzky and Golay's** 1964 paper in *Analytical Chemistry* was one of the first to exploit this for spectra, and it has remained among the most cited papers in the history of *Analytical Chemistry* . ## What is noise? In spectroscopy and chemometrics, **noise** is the unwanted random fluctuation superimposed on the true signal. Measure the same sample ten times under identical conditions and you will get ten slightly different spectra. The variation between those repeats is noise. The quality of a measurement is summarised by the **signal-to-noise ratio (SNR)**: $\text{SNR} = \frac{\bar{x}}{s}$ where $\bar{x}$ is the mean signal at a given point and $s$ is the standard deviation of repeated measurements at that point. A higher SNR means a cleaner measurement. Engineers often express it in decibels: $\text{SNR}_{\text{dB}} = 20 \log_{10}\left(\frac{\bar{x}}{s}\right)$ so an SNR of 100 corresponds to 40 dB. ### Where does it come from? Noise in a spectrometer is not one phenomenon but a family of them : - **Shot noise** comes from the quantum nature of light. Photons (or electrons) arrive one by one, at random moments, so the number counted in a fixed time fluctuates. These counts follow Poisson statistics, and the fluctuation grows with the square root of the signal (so the *relative* noise falls as the signal grows, as we will see below). - **Thermal (Johnson-Nyquist) noise** comes from the random motion of electrons in the detector and electronics. It is present even when no light reaches the detector. - **Dark current** is the signal a detector produces in complete darkness, because heat alone creates some charge carriers. Its average can be subtracted, but its fluctuations remain. - **Digitisation noise** appears when the analog detector signal is converted into numbers. The converter can only represent a finite number of levels, so every value is rounded. Knowing which source dominates your measurement is the first step towards choosing the right remedy. ### Why noise matters for chemometrics Noise is not just an aesthetic problem. It has concrete consequences for multivariate analysis : - **Calibration models** can fit noise as if it were signal (overfitting), which degrades predictions on new samples. - **Classification models** may find spurious differences between groups that are only noise. - **Peak detection** produces false positives (noise spikes mistaken for peaks) and false negatives (real peaks hidden in noise). - **Derivative spectra** amplify noise strongly, because a derivative emphasises fast point-to-point changes, and noise is exactly that. - **Variable selection** methods may pick noisy, uninformative variables. The goal of noise reduction is to suppress random fluctuations while preserving the chemical information carried by the spectral features: band positions, heights, widths and shapes. ## Types of unwanted variation Not every unwanted wiggle is noise in the strict sense, and the distinction decides which tool will work. Play with the panel below before reading the details: pick a type of variation and switch smoothing on. ### Random noise (additive, homoscedastic) The simplest and most common model: a random value drawn from a Gaussian distribution with zero mean is added independently to each data point. $y_i = x_i + \varepsilon_i, \quad \varepsilon_i \sim \mathcal{N}(0, \sigma^2)$ Here $x_i$ is the true signal at point $i$, $\varepsilon_i$ is the noise, and $\sigma$ is the same everywhere in the spectrum. This is called **homoscedastic** noise (from the Greek for "same scatter") because its size does not depend on the signal level. A second, separate property is independence. Noise is called **white** when neighbouring errors are uncorrelated, so its power is spread equally over all frequencies, by analogy with white light, which contains all colours equally. *Homoscedastic* describes the size of the noise; *white* describes its independence from point to point. The model above is both. Thermal noise is the textbook example. Most smoothing methods are designed and analysed under this model, and they work well when it holds. You met the same idea of constant error variance in [the least squares foundation](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics). ### Shot noise (signal-dependent) Because photons arrive randomly, the number detected in a given time follows a Poisson distribution, whose variance equals its mean. For a signal of $I$ counts: $\sigma_{\text{shot}} = \sqrt{I}$ So **stronger signals carry more absolute noise**, but less *relative* noise: the ratio $\sqrt{I}/I = 1/\sqrt{I}$ falls as the signal grows. In Raman, fluorescence or low-light UV-Vis measurements, shot noise is often the dominant source. Since smoothing applies the same strength everywhere, it can be too weak on top of intense bands and stronger than needed on the baseline. ### Heteroscedastic noise More generally, when the noise level changes across the spectrum, for instance because the detector is less sensitive near the ends of its range, the noise is called **heteroscedastic** ("different scatter"). This is common in real instruments. Shot noise is one particular case of it. ### Systematic variation Some unwanted variation is not random at all: - **Baseline drift** is a slow, smooth change of the whole spectrum caused by temperature, lamp ageing or detector drift. Because it is smooth, a smoother keeps it. Use [baseline correction](/introduction-to-chemometrics/preprocessing/baseline-correction-asls) or [derivatives](/introduction-to-chemometrics/preprocessing/derivatives) instead. - **Interference fringes** are periodic ripples caused by light bouncing between the parallel faces of a thin cell or window. They are regular waves, not random noise, and are better removed by Fourier filtering or dedicated fringe-correction methods. - **Spikes** (cosmic rays) hit CCD detectors in Raman spectroscopy and produce one or two extremely intense pixels. Averaging does not delete a spike: it smears it into a fake band. Spikes should be detected and removed first, for example with a median filter or a dedicated despiking method (see [median filtering](#median-filtering) below). ### Multiplicative effects In diffuse reflectance measurements (NIR, DRIFTS) light scattering changes the effective path length from sample to sample. The spectrum of a sample is then shifted and stretched relative to a reference spectrum of identical chemistry: $y_i = a + b\,x_i + \varepsilon_i$ where the offset $a$ and the multiplier $b$ change from sample to sample. These effects are smooth, so smoothing cannot touch them. They are handled by [MSC (Multiplicative Scatter Correction)](/introduction-to-chemometrics/preprocessing/msc), [EMSC](/introduction-to-chemometrics/preprocessing/emsc) or SNV (Standard Normal Variate, which centres each spectrum on its own mean and divides it by its own standard deviation) . Smoothing is designed for **random, additive noise**. It is not the right tool for baseline drift, multiplicative scatter, spikes or fringes. Applying it to those artefacts will not remove them and may distort real features. Diagnose first, then choose the preprocessing. ## The first line of defence: signal averaging Before any software smoothing, the most powerful noise reduction is simply to measure more than once and average. If the noise in each scan is random and independent, averaging $n$ scans divides its standard deviation by $\sqrt{n}$: $\sigma_{\text{averaged}} = \frac{\sigma_{\text{single}}}{\sqrt{n}}$ This comes straight from the statistics of the mean: the variance of an average of $n$ independent values is $\sigma^2/n$ . Four scans halve the noise, sixteen divide it by four, a hundred divide it by ten. The returns diminish: every further halving needs four times as many scans. Averaging has one great virtue: it introduces **no distortion** of the band shapes, because each scan is averaged with other measurements of the *same* point, not with its neighbours. This holds provided the sample and the instrument do not change between scans: lamp drift, small wavelength shifts or a sample that reacts during the measurement do blur the average. That is why FTIR instruments co-add 16, 32 or more scans by default. Its cost is time, which is not always available in kinetics or process monitoring. When it is not, we turn to smoothing. ## The smoothing approach Smoothing exploits a simple asymmetry between signal and noise. Spectroscopic signals usually change **gradually** from one wavelength to the next, because they come from continuous physical phenomena such as molecular vibrations or electronic transitions. Random noise changes **independently** at every point. If we combine each point with its neighbours, the random errors partly cancel while the slowly varying signal survives. The catch is that real bands are not perfectly flat over a window of neighbours. The wider the neighbourhood, the more the noise cancels, but the more the band shape is bent. Every smoothing method is a different answer to this trade-off. The four methods used most in chemometrics, from simplest to most sophisticated, are summarised below. Each has its own article with derivations, interactive panels and code. ### Moving average Replace each point with the plain average of itself and its neighbours: $\hat{y}_i = \frac{1}{w} \sum_{j=i-m}^{i+m} y_j, \quad m = \frac{w-1}{2}$ **Strengths**: extremely simple and fast, one parameter (the window size $w$). **Weaknesses**: all neighbours count equally, even distant ones, so bands are flattened and broadened. **Best for**: quick exploration, very broad features, and learning the idea of smoothing. Full details: [Moving Average Smoothing](./moving-average) ### Gaussian smoothing A weighted average in which closer neighbours count more, following the bell-shaped Gaussian curve. The parameter $\sigma$ sets the width of the bell: $\hat{y}_i = \frac{\sum_j w(j-i)\, y_j}{\sum_j w(j-i)}, \quad w(u) = e^{-u^2 / (2\sigma^2)}$ **Strengths**: smooth results without the frequency-response ripples (sidelobes) of the moving average, explained in the Gaussian article; one parameter, extends naturally to images. **Weaknesses**: still lowers and broadens sharp bands. **Best for**: natural-looking smoothing, spectral images, situations where one simple parameter is welcome. Full details: [Gaussian Smoothing](./gaussian-smoothing) ### Savitzky-Golay smoothing Instead of averaging, fit a low-order polynomial to each window by least squares and keep its value at the centre . The whole operation reduces to fixed weights: $\hat{y}_i = \sum_{j=-m}^{m} c_j \, y_{i+j}$ **Strengths**: preserves band heights and widths much better than averaging, and computes smoothed derivatives in the same step. **Weaknesses**: two parameters (window size and polynomial order), and it removes less noise than a moving average of the same length. **Best for**: IR, Raman and UV-Vis spectra where band shape matters, and derivatives. Full details: [Savitzky-Golay Smoothing](./savitzky-golay) ### Whittaker smoothing Frame smoothing as an optimisation: find the curve $\mathbf{z}$ that stays close to the data while being as smooth as possible . The parameter $\lambda$ sets the balance: $\min_{\mathbf{z}} \left\{ \sum_i (y_i - z_i)^2 + \lambda \sum_i (\Delta^d z_i)^2 \right\}$ where $\Delta^d z_i$ is a difference of order $d$ (for $d = 2$, a measure of curvature). **Strengths**: one intuitive parameter, works on the whole spectrum at once, handles missing points, and is the basis of popular baseline correction methods. **Weaknesses**: needs a (sparse) linear system, and $\lambda$ spans many orders of magnitude. **Best for**: general-purpose smoothing and as a building block for baseline correction. Full details: [Whittaker Smoothing](./whittaker) ### Try them side by side The panel below applies each method to the same noisy spectrum. Pay attention to the sharp band in the middle and to the residuals underneath. For each method, look for the setting where the residuals still look like pure noise but the error against the true signal is smallest. You will notice that no method wins on every count. The moving average and the Gaussian remove baseline noise very efficiently but quickly flatten the sharp band. Savitzky-Golay and Whittaker can be pushed further before the residuals start to show the shape of the band. ## Choosing a smoothing method **How sharp are your bands?** - Broad, gentle features (fluorescence, condensed-phase UV-Vis): moving average or Gaussian may be enough. - Sharp bands (Raman, high-resolution IR): use Savitzky-Golay or Whittaker. **Do you need derivatives?** - Yes: Savitzky-Golay computes smoothed derivatives in one step. - No: any method can work; choose on other criteria. **How much noise?** - Low: light smoothing (small windows, small $\lambda$). - High: stronger smoothing, but watch the residuals for band shapes. - Extreme: average more scans first, if you can. **Real time or offline?** - Offline (all data available): any method with a centred window. - Real time (process monitoring): only past points are available, so use a one-sided (causal) filter and accept a delay. ### Comparison table | Feature | Moving average | Gaussian | Savitzky-Golay | Whittaker | |---------|---------------|----------|----------------|-----------| | **Band shape preservation** | Poor | Moderate | Very good | Very good | | **Parameters** | 1 (window) | 1 (σ) | 2 (window, order) | 1 or 2 (λ, d) | | **Computation** | Very fast | Fast | Fast | Fast (sparse solver) | | **Smoothed derivatives** | No | Possible (derivative of Gaussian) | Yes, standard | Not directly | | **Missing data** | No | No | No | Yes (zero weights) | | **Best for** | Quick exploration | Natural smoothing, images | Spectra, derivatives | General purpose | ## Beyond smoothing Smoothing is the most common approach in chemometrics, but there are others, used instead of it or in combination. ### Fourier filtering Any spectrum can be written as a sum of waves of different frequencies (a Fourier transform). Broad bands and baselines are made of slow, low-frequency waves; white noise is spread over all frequencies, including the fast ones. Transforming the spectrum with the Fast Fourier Transform (FFT), damping the high frequencies and transforming back reduces noise. The practical difficulty is the cut-off: too low and real features disappear, too high and noise remains. Interference fringes, which appear as a sharp peak at one frequency (in FTIR, as a small burst at one position of the interferogram), can be removed this way, something no local smoother can do. ### Wavelet denoising Wavelets decompose a signal into small wave-like pieces that are localised both in position and in scale, which suits signals with sharp local features on a smooth background. Noise mostly produces small wavelet coefficients, so it can be suppressed by setting small coefficients to zero (thresholding) and reconstructing . Results can be excellent, but you must choose a wavelet family, a decomposition depth and a threshold rule, which makes it harder to tune than standard smoothing. ### Median filtering A median filter replaces each point with the **median** of its window instead of the mean. The median ignores extreme values, so a single spike surrounded by normal points disappears completely, whereas a moving average only spreads it out. Median filtering is normally used to remove spikes *before* smoothing. It is not a good general smoother, for two reasons. It flattens curved band tops into small plateaus. And for ordinary Gaussian noise it is less efficient than the mean: the variance of a median is about $\pi/2 \approx 1.57$ times that of a mean over the same window, so it removes less noise. For Raman spikes there are also dedicated methods that only touch the suspicious points. A popular one flags a point as a spike when its jump from the previous point is far larger than the typical jump (measured with a robust "modified z-score"), and replaces only those points by the mean of their neighbours . ## Measuring noise reduction After any noise reduction, check that it did what you intended: less noise, no distortion. ### Signal-to-noise improvement Measure the noise before and after, in a region where the true signal is known to be flat (a stretch of baseline). Two precautions make the number honest: choose a region well inside the spectrum, because every smoother behaves differently at the edges, and subtract a straight line fitted to that region first, so that a sloping baseline is not counted as noise. $\text{SNR improvement} = \frac{\text{SNR}_{\text{after}}}{\text{SNR}_{\text{before}}}$ For white noise, a moving average of $w$ points reduces the noise standard deviation by a factor $\sqrt{w}$, so a 9-point window should improve the SNR about threefold. Much less improvement suggests that the noise is not independent from point to point, which is common when the instrument has already smoothed the data internally. ### Residual analysis Subtract the smoothed spectrum from the original: $r_i = y_i - \hat{y}_i$ and plot the residuals: - **Good smoothing**: residuals look like random noise, centred on zero, with no visible pattern. - **Over-smoothing**: residuals show structure, such as bumps with the shape of a band. Signal is being removed. - **Under-smoothing**: the smoothed curve is still visibly noisy. ### Checking for distortion Compare key features before and after smoothing: - **Band positions** should not move (with centred windows they do not, for symmetric, isolated bands; an asymmetric band or two overlapping bands can show a small shift of the apparent maximum). - **Band heights** will drop somewhat with moving average and Gaussian; much less with Savitzky-Golay and Whittaker. - **Band widths** (full width at half maximum, FWHM) should stay approximately constant. - **Band areas** are preserved even when heights change (exactly, away from the edges, for any smoother whose weights sum to one), because smoothing redistributes intensity rather than removing it. - **Resolution**: closely spaced bands should remain separate. Over-smoothing merges them. ## Practical tips **Diagnose your noise.** Is it random, signal-dependent, or systematic (drift, fringes, spikes, scatter)? Smoothing only targets the random part. **Average scans first.** If you can measure several scans, averaging is distortion-free. **Remove spikes before smoothing.** A single spike corrupts every window it falls into. **Start mild and increase gradually.** It is easy to add smoothing, impossible to undo it. **Use identical settings for all samples.** Calibration, validation and test spectra must be processed with the same parameters. **Compare before and after.** Plot the original and smoothed spectra on the same axes. **Check the residuals.** Band-shaped residuals mean you are removing signal. **Check band metrics.** Compare positions, heights and widths before and after. **Document your parameters.** Method, window, order or $\lambda$: write them down so the processing can be repeated exactly. ## Code implementation The example below builds a synthetic spectrum with known noise, smooths it with three methods and measures what each one did: the noise left on a stretch of baseline, the resulting SNR (taking the 520 nm band height, 0.7, as the signal), and how much of that band's height was lost. The noise is measured as explained above, in a band-free region inside the spectrum (700 to 780 nm) after removing a straight-line baseline. The code for each individual method is in its own article. ```python from scipy.ndimage import uniform_filter1d, gaussian_filter1d from scipy.signal import savgol_filter # Synthetic spectrum: two bands on a sloped baseline, plus white noise np.random.seed(42) wavelength = np.linspace(400, 800, 500) # 0.8 nm between points true_signal = (0.7 * np.exp(-((wavelength - 520)**2) / 300) + 0.5 * np.exp(-((wavelength - 650)**2) / 200) + 0.001 * (wavelength - 400)) noisy_spectrum = true_signal + np.random.normal(0, 0.05, wavelength.size) # Three smoothers with comparable settings. For the moving average, # mode='nearest' repeats the end values instead of padding with zeros. smoothers = { 'Moving average (w=11)': lambda y: uniform_filter1d(y, size=11, mode='nearest'), 'Gaussian (sigma=2)': lambda y: gaussian_filter1d(y, sigma=2.0), 'Savitzky-Golay (w=11, p=2)': lambda y: savgol_filter(y, window_length=11, polyorder=2), } def baseline_noise(signal, wavelength, lo=700, hi=780): """Noise s.d. in a band-free region inside the spectrum, after removing a straight-line baseline so that the slope is not counted.""" m = (wavelength >= lo) & (wavelength = 700 & wavelength = 1 & idx = lo & wavelength # Outlier Detection (/introduction-to-chemometrics/preprocessing/outliers) Every analyst has met that one measurement. The replicate that sits far from the others, the spectrum that looks a little odd, the calibration standard that refuses to sit on the line. Is it a mistake, or is it the most interesting sample in the data set? Outlier detection is the set of tools that helps us ask that question in a fair, reproducible way. The question is old. In 1863, the American mathematician and astronomer **William Chauvenet** included in his *Manual of Spherical and Practical Astronomy* a rule for rejecting suspicious observations , a simpler version of a criterion proposed by **Benjamin Peirce** in 1852 . Astronomers repeated their measurements of star positions many times, and now and then one reading drifted far from the rest: a misread scale, a vibration of the telescope, a moment of distraction. Chauvenet's criterion said that an observation should be rejected if the probability of getting a deviation that large was smaller than $1/(2n)$ for a sample of size $n$. The rule is crude by modern standards, but the principle behind it is still ours: we need a quantitative rule to decide when a value is too far from the rest, because human judgement alone is inconsistent. Almost a century later, **Frank E. Grubbs**, a statistician at the Aberdeen Proving Ground in Maryland, placed the problem on firmer ground. In 1950 and in a widely cited 1969 review, he developed formal hypothesis tests for one or more outliers in a sample drawn from a normal distribution . Variants of Grubbs' test are still used in quality control and in standard methods such as ASTM E178, and ISO 5725-2, the standard for interlaboratory precision studies, prescribes it together with Cochran's test for variances. The next big step came in 1977, when **R. Dennis Cook** introduced what we now call **Cook's distance**: a measure of how much a regression changes when a single observation is deleted . This shifted the question from "is this point unusual?" to "is this point *pulling the model*?". In chemometrics that second question is crucial. A single contaminated spectrum in a [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) or [PLS](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression) model can rotate the components, distort the score plot and degrade the predictions for every other sample. The tools developed for this setting (Hotelling's $T^2$, Q residuals, leverage and their combinations) are now part of every serious chemometric workflow. The multivariate part of this entry works with PCA scores and residuals, and the regression part with calibration models such as PLS. If these are new to you, read [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) first; [PLS regression](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression) helps for the last sections. ## What is an outlier? An **outlier** is an observation that does not follow the same pattern as the majority of the data. The definition is deliberately vague, because "not following the pattern" depends on which pattern you expect. With one variable, an outlier is simply a value far from the centre. A pH of 14.3 in a set of readings around 7.0 needs no statistics to be noticed. With many variables, outliers can be much more subtle. A sample may have perfectly ordinary values for every single variable and still be unusual in its *combination* of values: for example, a high absorbance at one wavelength together with a low absorbance at another wavelength that normally rises with it. Checking each variable on its own will never find that sample. This is why multivariate data need multivariate diagnostics. ### Outliers and influential points Two ideas are easy to confuse: - An **outlier** is far from the rest of the data. It may or may not affect a model. - An **influential point** is a point that, if removed, noticeably changes the fitted model. A point can be influential without looking like an outlier: if it sits at the edge of the range of the predictors, it can quietly drag the model towards itself. And a point can be a clear outlier without being influential: if it sits in the middle of the predictor range, it may barely change the slope of a regression line. ### Why outliers matter so much Most chemometric methods (least squares regression, PCA, PLS) are built on **squared** deviations. Squaring gives large deviations a disproportionate weight: a residual ten times larger than usual contributes a hundred times more to the sum of squares. When all the data are well behaved, this is what makes least squares efficient. When one sample is wrong, it is exactly what lets that sample dominate the model. In PCA, a single extreme sample can: - Rotate the principal components towards itself, distorting the model for everybody else. - Use up a whole component just to describe its own peculiarity. - Inflate the apparent explained variance and give a misleading picture of the data. - Bias the regression coefficients of a PLS model, degrading predictions for normal samples. ## Univariate screening Before moving to multivariate tools, it is worth knowing the classical univariate rules. They cannot detect multivariate outliers, but they are useful for screening individual variables, such as a reference concentration or a replicate measurement. ### The z-score The simplest approach is to measure how many standard deviations each value lies from the mean: $z_i = \frac{x_i - \bar{x}}{s}$ where $\bar{x}$ is the sample mean and $s$ the sample standard deviation. A common rule flags 3" />, because under a normal distribution only about 0.3% of values should be that far out. With small samples the rule cannot even fire: no value can be more than $(n-1)/\sqrt{n}$ standard deviations from the mean, which is below 3 for $n \le 10$. With five replicates, for example, the largest possible $|z|$ is about 1.79, however wild the suspect value is. The weakness is that the mean and the standard deviation are themselves pulled by the outlier. An extreme value inflates $s$, which shrinks every z-score, including its own. The outlier partly hides itself. This is called the **masking effect**. Its twin is **swamping**: a distorted mean and standard deviation can make perfectly good values look suspicious. ### The box plot rule A more robust alternative uses the quartiles. The **interquartile range** is $\text{IQR} = Q_3 - Q_1$ where $Q_1$ and $Q_3$ are the first and third quartiles (the values below which 25% and 75% of the data lie). A value is flagged if it is below $Q_1 - 1.5\,\text{IQR}$ or above $Q_3 + 1.5\,\text{IQR}$. This is the rule behind **John Tukey's** box plot . For normal data the fences sit at roughly $\pm 2.7\sigma$, and using 3 instead of 1.5 identifies "far out" values. Quartiles are barely affected by a few extreme values, so the rule does not suffer from masking in the way the z-score does. ### Grubbs' test Grubbs' test asks whether the most extreme value in a sample is consistent with all values coming from the same normal population . The test statistic is the largest absolute z-score: $G = \frac{\max_i |x_i - \bar{x}|}{s}$ The hypothesis that there is no outlier is rejected at significance level $\alpha$ if \frac{n - 1}{\sqrt{n}} \sqrt{\frac{t^2_{\alpha/(2n),\, n-2}}{n - 2 + t^2_{\alpha/(2n),\, n-2}}}" display={true} /> where $t_{\alpha/(2n),\, n-2}$ is the upper critical value of Student's t distribution with $n - 2$ degrees of freedom. The test assumes normally distributed data and looks for one outlier at a time. With several outliers, masking can hide all of them. It is a tool for a single variable, such as replicate determinations, not for spectra. ### Dixon's Q test Analytical chemists often meet **Dixon's Q test**, designed for very small sets of replicates (roughly 3 to 10) . Sort the values; the statistic is the gap between the suspect value and its nearest neighbour, divided by the range of all the values: $Q = \frac{\text{gap}}{\text{range}}$ and the suspect is rejected when $Q$ exceeds a tabulated critical value for the given $n$. Dixon's $Q$ has nothing to do with the Q residual discussed below; the shared letter is a coincidence. ## Multivariate outlier detection Here is where chemometrics needs its own tools. The methods below take into account the correlations between variables, so they can see unusual *combinations* that univariate rules miss. ### Mahalanobis distance The **Mahalanobis distance** generalises "how many standard deviations away" to several correlated variables : $D_i^2 = (\mathbf{x}_i - \bar{\mathbf{x}})^T \mathbf{S}^{-1} (\mathbf{x}_i - \bar{\mathbf{x}})$ Here $\mathbf{x}_i$ is the vector of measurements of sample $i$, $\bar{\mathbf{x}}$ is the mean vector, and $\mathbf{S}$ is the sample covariance matrix, which describes how every pair of variables varies together. Geometrically, the Mahalanobis distance first stretches and rotates the space so that the elongated data cloud becomes round, and then measures ordinary distance. A point far out along the direction in which the data are naturally spread gets a modest distance; a point the same number of units away in a direction where the data are thin gets a large one. For multivariate normal data, $D_i^2$ approximately follows a $\chi^2$ distribution with $p$ degrees of freedom, where $p$ is the number of variables. Samples with \chi^2_{p,\,1-\alpha}" /> are flagged, typically with $\alpha = 0.05$ or $0.01$. There is a catch for spectroscopy. Computing $\mathbf{S}^{-1}$ requires more samples than variables. A data set of 60 spectra with 1000 wavelengths has a singular covariance matrix that cannot be inverted. The solution is to measure distances in a compressed space, and this is exactly what PCA offers. ### Hotelling's T² After fitting a [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) model with $A$ components, each sample is summarised by its **scores** $\mathbf{t}_i = [t_{i1}, \ldots, t_{iA}]$, its coordinates along the principal components. **Hotelling's** $T^2$ is the Mahalanobis distance computed in this score space : $T^2_i = \sum_{a=1}^{A} \frac{t_{ia}^2}{\lambda_a}$ where $\lambda_a$ is the variance of the scores on component $a$ (its eigenvalue). Dividing by $\lambda_a$ puts every component on the same footing: a large score on a component that naturally varies a lot is less surprising than the same score on a quiet component. The limit most chemometrics software uses for the calibration samples, at significance level $\alpha$, comes from the F distribution: $T^2_{\text{lim}} = \frac{A(n-1)}{n-A}\, F_{A,\, n-A,\, 1-\alpha}$ where $n$ is the number of samples. For a brand-new sample projected onto an existing model, the factor in front becomes $A(n^2-1)/(n(n-A))$, which gives a slightly wider limit. Strictly, for samples that were part of the model $T^2$ follows a scaled beta distribution, $\frac{(n-1)^2}{n}\,\text{Beta}\!\left(\frac{A}{2}, \frac{n-A-1}{2}\right)$ . The F-based limit is the common, conservative approximation; the difference is small for large calibration sets but noticeable for small ones. A high $T^2$ means the sample is **extreme within the model**: it combines the known patterns in an unusual amount. If PC1 describes fat content and PC2 moisture, a sample with a large $T^2$ has an unusually high or low fat content, an unusual fat and moisture combination, or both. ### Q residuals $T^2$ only looks inside the model. The **Q residual**, also called the squared prediction error (SPE), looks at what the model leaves behind: $Q_i = \mathbf{e}_i^T \mathbf{e}_i = \sum_{j=1}^{p} e_{ij}^2$ where $\mathbf{e}_i = \mathbf{x}_i - \hat{\mathbf{x}}_i$ is the difference between the (preprocessed) sample and its reconstruction from $A$ components, $\hat{\mathbf{x}}_i = \sum_{a=1}^{A} t_{ia}\mathbf{p}_a$. The vectors $\mathbf{p}_a$ are the **loadings**, the directions of the principal components. In plain words, $Q_i$ is the squared length of the part of the sample that the model cannot describe. A limit for $Q$ was derived by **Jackson and Mudholkar** : $Q_{\text{lim}} = \theta_1 \left( \frac{c_\alpha \sqrt{2\theta_2 h_0^2}}{\theta_1} + 1 + \frac{\theta_2 h_0 (h_0 - 1)}{\theta_1^2} \right)^{1/h_0}$ where $\theta_k = \sum_{a=A+1}^{r} \lambda_a^k$ for $k = 1, 2, 3$ sums powers of the eigenvalues of the components that were *not* kept ($r$ is the rank of the centred data, at most $\min(n-1, p)$), $h_0 = 1 - 2\theta_1\theta_3/(3\theta_2^2)$, and $c_\alpha$ is the upper $(1-\alpha)$ standard normal quantile. It looks intimidating, but every term comes from the discarded eigenvalues. A simpler alternative, popular in multivariate statistical process control (MSPC), approximates the distribution of $Q$ by a scaled chi-squared distribution, $g\,\chi^2_h$, with $g = v/(2m)$ and $h = 2m^2/v$, where $m$ and $v$ are the mean and variance of the calibration Q values . When none of this is available, a practical fallback is the 95th or 99th percentile of the Q values of the calibration samples. A high $Q$ means the sample contains **variation the model was never built to describe**: a new interferent, a contaminant, an instrumental artefact, or simply a different kind of sample. ### Seeing both at once The two statistics are easiest to understand by playing with them. The panel below shows a tiny data set with two variables, for example the absorbance at two wavelengths that both respond to the same analyte. A one-component PCA model is the blue line. Drag the orange sample around and watch both statistics react. Two things are worth noticing. First, the two statistics measure different directions: $T^2$ grows along the model, $Q$ grows away from it. Second, when the suspicious sample is allowed into the model, it pulls the model towards itself and partly hides its own residual. This is the multivariate version of masking, and it is why diagnostics are usually computed on a model built from trusted reference samples, or with robust methods (see below). ### The T² against Q plot Plotting $T^2$ on one axis and $Q$ on the other, with a limit line for each, divides the samples into four regions : | Region | T² | Q | Interpretation | |--------|----|---|----------------| | Normal | Low | Low | Well described by the model and not extreme. | | Extreme but modelled | High | Low | An extreme version of known variation, for example a very concentrated standard. | | New variation | Low | High | Contains something the model was not trained on: a new interferent, a different instrument, a different sample matrix. | | Outlier in every sense | High | High | Both extreme and poorly described. Investigate carefully. | The T² against Q plot answers two questions at once: "is this sample unusual in ways we understand?" (high T²) and "is this sample unusual in ways we do not understand?" (high Q). High Q is usually the more serious warning, because it means the model's picture of the data does not apply to that sample. ## Influence in regression When the goal is a calibration model ([ordinary least squares](/introduction-to-chemometrics/models/regression-models/linear-regression), PCR or PLS), we also care about the response. It is not enough to know whether a sample is unusual; we want to know whether it changes the model. This is the field of **influence diagnostics** . ### Leverage In ordinary least squares the fitted values are a linear function of the measured responses, $\hat{\mathbf{y}} = \mathbf{H}\mathbf{y}$, with the **hat matrix** $\mathbf{H} = \mathbf{X}(\mathbf{X}^T \mathbf{X})^{-1}\mathbf{X}^T$ (the same $(\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T$ you met in the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations)). Here $\mathbf{X}$ includes a leading column of ones for the intercept, so each row is $\mathbf{x}_i = [1, x_{i1}, \ldots, x_{ip}]^T$. The **leverage** of sample $i$ is the diagonal element $h_{ii} = \mathbf{x}_i^T (\mathbf{X}^T \mathbf{X})^{-1} \mathbf{x}_i$ It tells how strongly the fitted value of sample $i$ depends on its own measured response. With an intercept in the model, leverage lies between $1/n$ and 1, and its average is $(p+1)/n$, where $p$ is the number of predictors. A common rule of thumb flags 2(p+1)/n" /> or, more conservatively, 3(p+1)/n" />. The key insight is that **leverage depends only on the predictors**, not on the response. A high-leverage point sits in a sparsely populated region of the predictor space, like a lever arm far from the pivot. It has the *potential* to move the model. Whether it actually does depends on whether its response agrees with the trend of the other samples. In PCA and PLS models, leverage is computed in the score space and is directly related to $T^2$: $h_i = \frac{1}{n} + \frac{T^2_i}{n-1}$ so leverage and $T^2$ are essentially the same diagnostic seen from two angles. ### Studentized residuals The ordinary residual is $e_i = y_i - \hat{y}_i$. Because high-leverage points pull the fit towards themselves, their raw residuals tend to be small. The **internally studentized residual** corrects for this: $r_i = \frac{e_i}{s \sqrt{1 - h_{ii}}}$ where $s$ is the residual standard deviation of the model. The internally studentized residual has a hidden ceiling: it can never exceed $\sqrt{n-k}$ in absolute value, where $k$ is the number of fitted coefficients. For small calibration sets ($n = 10$, $k = 2$ gives about 2.8) that makes fixed cut-offs such as $\pm 3$ unreachable. The **externally studentized residual** goes one step further and estimates the standard deviation without sample $i$, written $s_{(i)}$, so that a large error cannot inflate its own yardstick, and it has no such ceiling: $t_i = \frac{e_i}{s_{(i)} \sqrt{1 - h_{ii}}}$ If the model is correct, $t_i$ follows a t distribution with $n - p - 2$ degrees of freedom. In practice, values beyond about $\pm 2.5$ or $\pm 3$ deserve a look. ### Cook's distance **Cook's distance** combines leverage and residual into one measure of influence . It quantifies how much *all* fitted values move when sample $i$ is removed: $D_i = \frac{(\hat{\mathbf{y}} - \hat{\mathbf{y}}_{(i)})^T (\hat{\mathbf{y}} - \hat{\mathbf{y}}_{(i)})}{k\, s^2}$ where $\hat{\mathbf{y}}_{(i)}$ are the fitted values of the model built without sample $i$, and $k = p + 1$ is the number of fitted coefficients. Happily, there is no need to refit the model $n$ times; the same quantity can be written as $D_i = \frac{r_i^2}{k} \cdot \frac{h_{ii}}{1 - h_{ii}}$ so Cook's distance is large only when a sample has **both** a large residual and a high leverage. Common guidelines are to investigate samples with 4/n" />, and to treat 1" /> as strongly influential. Cook's distance can be large for a good leverage point too, because $h_{ii}/(1-h_{ii})$ becomes large as $h_{ii}$ approaches 1. In the demo below, the *Good leverage* preset has $D \approx 0.92$, well above $4/n \approx 0.36$, although the slope hardly changes (1.995 against 1.984 without it). Always look at the fit with and without the sample. ### The Williams plot The **Williams plot** shows leverage on the horizontal axis and the externally studentized residual on the vertical axis, with a vertical line at $h^* = 3(p+1)/n$ and horizontal lines at $\pm 2.5$ or $\pm 3$. It is the regression counterpart of the T² against Q plot: - **Low leverage, small residual:** an ordinary, well-fitted sample. - **High leverage, small residual:** a "good" leverage point. It anchors the line, and agrees with it. - **Low leverage, large residual:** a response outlier that does not move the model much. - **High leverage, large residual:** a "bad" leverage point, the most dangerous case. Try it yourself. Drag the orange standard and compare the fitted line with and without it: ## What to do with outliers Detecting outliers is only half the job. The harder question is what to do about them, and the single most important principle is: **investigate before you delete.** 1. **Identify the suspects** Use the T² against Q plot, the Williams plot or other diagnostics to flag candidates. Record which samples were flagged and by which criterion. 2. **Look at the raw data** Go back to the measured spectrum. Is there a saturated detector, a baseline jump, a spike, a missing region? A documented measurement problem is a good reason for removal. 3. **Check the metadata** Was the sample mislabelled? Was the instrument serviced that day? Was a different kind of sample accidentally included in the batch? Laboratory records often explain outliers that statistics alone cannot. 4. **Think about the chemistry** If there is no quality problem, ask whether the sample represents genuinely different chemistry. A high-Q sample may contain a species that is absent from the others. That is information, not noise. 5. **Assess the impact** Build the model with and without the suspect samples and compare the cross-validated predictions. If removal barely changes anything, the point is not influential, whatever its statistics say. 6. **Document the decision** "Removed because the detector saturated above 2450 nm" is a defensible decision. "Removed because it was an outlier" tells the next analyst nothing. Statistical limits are screening tools, not deletion criteria. With a 95% limit, about one sample in twenty of perfectly good data will cross it by chance. Removing every flagged sample shrinks the model's range, makes it look better than it is, and may throw away the very samples that tell you something new. Always have a reason beyond the statistics for excluding a sample. As a rough guide, these situations call for **further investigation** rather than removal: - **High Q, low T².** The sample contains variation the model has not seen. Find out what it is before deciding. - **A chemically plausible edge case.** A sample at an extreme pH, temperature or concentration may mark the boundary of the model's useful range. Keeping it widens the range; removing it narrows it. - **A single flag in a large data set.** With 500 samples, a 99% limit is expected to flag about five perfectly good samples by chance, so one or two flags are no surprise. And these are good grounds for **removal**, with the cause written down: - **A documented measurement failure**, such as a failing lamp, a blocked probe window or a saturated detector. - **A confirmed preparation error**, such as a mislabelled vial or a wrong dilution. - **Several independent diagnostics agree** and the raw spectrum shows a clear problem. Record every excluded sample with the date, the reason and the diagnostics that flagged it. This log is invaluable when you revisit the model months later, hand it over to a colleague or answer an auditor's question. ## Robust alternatives All the classical diagnostics share a weakness: they define "normal" using the mean and covariance of the data, and those statistics are themselves distorted by the outliers they are trying to find. **Robust methods** estimate the centre and spread of the data in ways that resist contamination. ### Median and MAD For a single variable, the median and the **median absolute deviation** replace the mean and the standard deviation: $\text{MAD} = \text{median}\left(|x_i - \text{median}(\mathbf{x})|\right)$ Multiplying the MAD by 1.4826 turns it into an estimate of the standard deviation for normal data, which gives a robust z-score: $z_i^{\text{robust}} = \frac{x_i - \text{median}(\mathbf{x})}{1.4826 \times \text{MAD}}$ The MAD has a **breakdown point** of 50%: up to half of the data can be replaced by arbitrary values before the estimate becomes meaningless. The standard deviation has a breakdown point of zero, because a single wild value can make it as large as you like . The same median and MAD can also be used to centre and scale variables robustly before a multivariate analysis (see [centering and scaling](/introduction-to-chemometrics/preprocessing/centering-and-scaling)). ### Robust covariance and ROBPCA For several variables, the **minimum covariance determinant** (MCD) estimator looks for the subset of samples (typically a bit more than half) whose covariance matrix has the smallest determinant, in other words the tightest ellipse, and uses that subset to define the centre and shape of the data. The estimator was proposed by Rousseeuw and is computed in practice with the FAST-MCD algorithm . Outliers are then judged against this clean core. Because the MCD also needs more samples than variables, **Hubert, Rousseeuw and Vanden Branden** proposed **ROBPCA**, a robust PCA for high-dimensional data that combines projection pursuit with the MCD . It provides robust scores, a robust score distance (the analogue of $T^2$) and a robust orthogonal distance (the analogue of $Q$), and it sorts the samples into regular observations, good leverage points, orthogonal outliers and bad leverage points. ### Least trimmed squares In regression, **least trimmed squares** (LTS), introduced by **Rousseeuw** in 1984, minimises the sum of the $h$ *smallest* squared residuals instead of all of them, with $h$ typically about $(n + p + 1)/2$ . The worst-fitting samples simply do not take part in the fit, so they cannot drag it. Once the robust model is fitted, the excluded samples can be examined one by one. Robust methods are most valuable in a first exploration of a new data set of unknown quality, when you suspect contamination but do not know where. For routine monitoring with a clean, well-characterised calibration set, the classical T² and Q limits work well. ## Code implementation The examples below simulate NIR-like spectra from two absorbing components, insert three suspicious samples (an extreme concentration, a new interferent, and both together), and compute Hotelling's T² and Q residuals with their limits. To avoid the masking described above, the PCA model is built only from the first 55 samples, which we trust. The last five (the three suspects and two ordinary samples) are then projected onto it as new samples and judged with the new-sample $T^2$ limit. You should see the three suspects far beyond the limits (the axes are logarithmic) and the two ordinary samples among the calibration samples. The extreme sample is flagged mainly by $T^2$; its $Q$ is only just above the limit, because small errors in the loadings are magnified at such large scores. If you instead build the model with all 60 samples, the suspects distort it: the Q limit becomes hundreds of times larger and every ordinary sample gets a Q far above the noise level. ```python from scipy import stats def fit_pca_model(X_cal, n_components=2, alpha=0.05): """ Build a PCA model on trusted calibration samples and compute the limits for Hotelling's T² and the Q residual. X_cal : array (n_samples, n_variables), mean-centred internally n_components : number of principal components in the model alpha : significance level of the limits """ n, p = X_cal.shape A = n_components mean = X_cal.mean(axis=0) # PCA via SVD of the centred calibration data U, s, Vt = np.linalg.svd(X_cal - mean, full_matrices=False) eigenvalues = s**2 / (n - 1) model = {'mean': mean, 'loadings': Vt[:A, :].T, 'eigenvalues': eigenvalues[:A]} # T² limits (F distribution): calibration samples and new samples F = stats.f.ppf(1 - alpha, A, n - A) model['T2_lim_cal'] = A * (n - 1) / (n - A) * F model['T2_lim_new'] = A * (n**2 - 1) / (n * (n - A)) * F # Q limit (Jackson-Mudholkar), percentile of the calibration Q as a fallback eig_res = eigenvalues[A:] theta1, theta2, theta3 = (np.sum(eig_res**k) for k in (1, 2, 3)) _, Q_cal = project(model, X_cal) model['Q_lim'] = np.percentile(Q_cal, 100 * (1 - alpha)) if theta1 > 0 and theta2 > 0: h0 = 1 - 2 * theta1 * theta3 / (3 * theta2**2) if h0 > 0: c_alpha = stats.norm.ppf(1 - alpha) model['Q_lim'] = theta1 * ( c_alpha * np.sqrt(2 * theta2 * h0**2) / theta1 + 1 + theta2 * h0 * (h0 - 1) / theta1**2 ) ** (1 / h0) return model def project(model, X): """Hotelling's T² and Q residual of samples X under an existing model.""" Xc = X - model['mean'] P = model['loadings'] T = Xc @ P T2 = np.sum(T**2 / model['eigenvalues'], axis=1) Q = np.sum((Xc - T @ P.T)**2, axis=1) return T2, Q # Simulated NIR-like spectra rng = np.random.default_rng(42) n_samples, n_wavelengths = 60, 200 wavelengths = np.linspace(1000, 2500, n_wavelengths) comp1 = np.exp(-((wavelengths - 1400)**2) / 20000) comp2 = np.exp(-((wavelengths - 1900)**2) / 30000) concentrations = rng.random((n_samples, 2)) * 2 X = concentrations @ np.array([comp1, comp2]) X += rng.normal(0, 0.005, X.shape) # Three suspicious samples (Python indices start at 0) comp3 = np.exp(-((wavelengths - 2200)**2) / 10000) X[55] = 5 * comp1 + 4 * comp2 + rng.normal(0, 0.005, n_wavelengths) # extreme: high T² X[56] = 0.8 * comp1 + 1.0 * comp2 + 1.5 * comp3 # new interferent: high Q X[57] = 4 * comp1 + 0.5 * comp2 + 2.0 * comp3 # both # Build the model on the first 55 (trusted) samples only, then project # the last five as new samples. Fitting the model with the suspects # inside would let them mask themselves (see the text above). X_cal, X_new = X[:55], X[55:] model = fit_pca_model(X_cal, n_components=2, alpha=0.05) T2_cal, Q_cal = project(model, X_cal) T2_new, Q_new = project(model, X_new) flag_cal = (T2_cal > model['T2_lim_cal']) | (Q_cal > model['Q_lim']) flag_new = (T2_new > model['T2_lim_new']) | (Q_new > model['Q_lim']) print(f"T² limit (new samples) = {model['T2_lim_new']:.2f}, Q limit = {model['Q_lim']:.2e}") print(f"Calibration samples over a limit: {flag_cal.sum()} of {len(X_cal)}") for k in range(len(X_new)): print(f"Sample {55 + k}: T² = {T2_new[k]:.2f}, Q = {Q_new[k]:.2e}" + (" model.T2_lim_cal) | (Q_cal > model.Q_lim); flag_new = (T2_new > model.T2_lim_new) | (Q_new > model.Q_lim); fprintf('T^2 limit (new samples) = %.2f, Q limit = %.2e\n', model.T2_lim_new, model.Q_lim); fprintf('Calibration samples over a limit: %d of %d\n', sum(flag_cal), size(X_cal, 1)); for k = 1:size(X_new, 1) fprintf('Sample %d: T^2 = %.2f, Q = %.2e%s\n', 55 + k, T2_new(k), Q_new(k), ... repmat(' 0 && theta2 > 0 h0 = 1 - 2 * theta1 * theta3 / (3 * theta2^2); if h0 > 0 c_alpha = norminv(1 - alpha); model.Q_lim = theta1 * (c_alpha * sqrt(2 * theta2 * h0^2) / theta1 ... + 1 + theta2 * h0 * (h0 - 1) / theta1^2)^(1 / h0); end end end function [T2, Q] = project_pca(model, X) % Hotelling's T² and Q residual of samples X under an existing model Xc = X - model.mean; P = model.loadings; T = Xc * P; T2 = sum(T.^2 ./ model.eigenvalues, 2); Q = sum((Xc - T * P').^2, 2); end ``` ```r project_pca 0 && theta2 > 0) { h0 0) { c_alpha model$T2_lim_cal | cal$Q > model$Q_lim flag_new model$T2_lim_new | new$Q > model$Q_lim cat(sprintf("T2 limit (new samples) = %.2f, Q limit = %.2e\n", model$T2_lim_new, model$Q_lim)) cat(sprintf("Calibration samples over a limit: %d of %d\n", sum(flag_cal), nrow(X_cal))) cat(sprintf("Sample %d: T2 = %.2f, Q = %.2e%s\n", 55 + seq_along(new$T2), new$T2, new$Q, ifelse(flag_new, " # Savitzky-Golay Smoothing (/introduction-to-chemometrics/preprocessing/savitzky-golay) In 1964 two scientists at the Perkin-Elmer Corporation, one of the leading instrument makers of the time, published a paper that became one of the most cited in the history of analytical chemistry. **Abraham Savitzky** and **Marcel J. E. Golay**, a Swiss-born physicist also known for the Golay error-correcting codes, the Golay infrared detector and, for chemists, the open-tubular capillary column for gas chromatography, wrote *Smoothing and Differentiation of Data by Simplified Least Squares Procedures* in *Analytical Chemistry* . The timing was not accidental. Instruments were starting to produce digital output that could be fed to the first laboratory computers, and practitioners needed practical ways to clean up noisy spectra without distorting the bands that carry the chemistry. The paper's insight was this: instead of averaging neighbouring points, fit a **polynomial** through a sliding window of points by least squares, and take the value of that polynomial at the centre. Band shapes, heights and widths survive far better than with simple averaging. Even better, Savitzky and Golay showed that the whole fit collapses into a fixed set of **convolution weights**: integers that could be printed in a table and applied with a handful of multiplications, a decisive advantage on 1960s hardware. Some of the published tables contained errors, which **Steinier, Termonia and Deltour** corrected in 1972 , and in 1978 **Madden** gave general formulas for the weights, so that nobody needs to copy them from a table . Neither the errata nor the six decades since have dented the method's popularity: Savitzky-Golay filtering remains a standard tool in spectroscopy and chromatography whenever band fidelity matters . ## The band preservation problem The [moving average](./moving-average) and [Gaussian smoothing](./gaussian-smoothing) both reduce noise by averaging, and both share the same weakness: they **flatten and broaden bands**. A sharp maximum is averaged with the lower points around it, so it comes out lower. Two bands that were barely resolved can merge into one. In spectroscopy, where band positions, heights and widths carry chemical information, that is a serious problem. Savitzky-Golay smoothing keeps the idea of a sliding window but changes what we do inside it. ## The big idea: local polynomial fitting > **Fit a low-order polynomial to the points in the window, by least squares, and use its value at the centre as the smoothed value.** Take a window of 5 points around point $i$: - A **moving average** adds the five values and divides by 5. If the window sits on the top of a band, the average is necessarily lower than the top. - **Savitzky-Golay** fits, for example, a parabola $a_0 + a_1 t + a_2 t^2$ through the five points and reads it at the centre ($t = 0$). A parabola can bend, so on the top of a band it follows the curvature instead of cutting it off. A walking analogy: you want your altitude on a foggy hillside, and you ask the people around you for theirs. Averaging their answers puts you too low if you stand on a summit, because everyone around you is lower. Fitting a smooth curve through their answers and reading it at your position recognises that you are on a summit. The panel below shows the procedure. The red curve is the polynomial fitted to the points inside the shaded window; the ring marks its value at the centre. Slide the window, or let it slide, and change the window size and polynomial order. Some things to try: - With **order 0**, the "polynomial" is a constant, and the least squares constant through a set of points is their mean: Savitzky-Golay of order 0 *is* the moving average. - With **order 2 on the top of the sharp band**, the parabola follows the curvature and the centre value stays close to the true maximum, while the moving average value (shown in the metrics) falls short. - With a **high order and a small window** (for example **w = 7 with order 5 or 6**), the polynomial starts to follow the noise. You are fitting almost as many coefficients as there are points, and with order 6 the curve passes through all seven points and smooths nothing. ## From least squares to fixed weights This is ordinary linear least squares, the same machinery as in [the normal equations](/introduction-to-chemometrics/least-squares/normal-equations). For a window of $w = 2m + 1$ points we place the centre at $t = 0$, so the positions are $t = -m, \ldots, m$, and we fit $\hat{y}(t) = a_0 + a_1 t + a_2 t^2 + \cdots + a_p t^p$ The design matrix has one row per point in the window and one column per power of $t$: $\mathbf{A} = \begin{bmatrix} 1 & -m & (-m)^2 & \cdots & (-m)^p \\ \vdots & \vdots & \vdots & & \vdots \\ 1 & 0 & 0 & \cdots & 0 \\ \vdots & \vdots & \vdots & & \vdots \\ 1 & m & m^2 & \cdots & m^p \end{bmatrix}$ and the normal equations give the coefficients: $\hat{\mathbf{a}} = (\mathbf{A}^T\mathbf{A})^{-1}\mathbf{A}^T\mathbf{y}_{\text{window}}$ The smoothed value is the polynomial at the centre, $\hat{y}(0) = \hat{a}_0$, the first element of $\hat{\mathbf{a}}$. Here comes the key observation. The matrix $(\mathbf{A}^T\mathbf{A})^{-1}\mathbf{A}^T$ depends only on the window size and the polynomial order, **not on the data**. Its first row is therefore a fixed set of numbers $c_{-m}, \ldots, c_m$, and the smoothed value is just a weighted sum: $\hat{y}_i = \sum_{j=-m}^{m} c_j \, y_{i+j}$ The same weights are used at every position, so the filter is a convolution, exactly like a moving average, only with cleverer weights. The weights can be computed once and reused for every point and every spectrum. ### Example weights For a window of 5 points and a quadratic (order 2), the weights are $c = \frac{1}{35}\,[-3,\ 12,\ 17,\ 12,\ -3]$ so $\hat{y}_i = \frac{-3y_{i-2} + 12y_{i-1} + 17y_i + 12y_{i+1} - 3y_{i+2}}{35}$ Notice that: - The centre point gets the largest weight (17/35, about 0.49). - The immediate neighbours get 12/35 (about 0.34) each. - The outer points get a **negative** weight, -3/35. The negative weights are what let the filter follow curvature. On the top of a band the outer points are low; subtracting a little of them pushes the estimate back up towards the true maximum. The weights still sum to 1, so a constant signal is left untouched. For the **centre point**, a polynomial of even order $p$ and the next odd order $p + 1$ give exactly the same smoothing weights. The extra odd term ($t^3$, for example) is antisymmetric around the centre, so it cannot change the fitted value at $t = 0$. Smoothing with order 2 and with order 3 is identical in the interior of the spectrum; so is order 4 and order 5. The difference appears only at the edges and for derivatives . ## What the weights buy, and what they cost The second panel puts the weights side by side with the flat weights of a moving average of the same length, and applies both filters to a spectrum with a narrow band. Two numbers in the metrics deserve attention. **Band height.** With the same window, the Savitzky-Golay filter keeps far more of the narrow band than the moving average: with the starting settings (w = 15, p = 2) it keeps about 85 % of the band height, against about 50 % for the moving average. A polynomial of order $p$ is reproduced *exactly* by the filter, so any band shape that looks locally like a parabola passes through almost untouched. **Noise.** For independent random noise of standard deviation $\sigma$, the noise left after smoothing is $\sigma\sqrt{\sum_j c_j^2}$. For least squares weights this sum equals the centre weight (check it in the panel: the noise factor is the square root of c₀), so $\sigma_{\text{smoothed}} = \sigma\sqrt{c_0}$ For the 5-point quadratic, $\sqrt{17/35} \approx 0.70$, while a 5-point moving average gives $1/\sqrt{5} \approx 0.45$. **For the same window, Savitzky-Golay removes less noise.** That is the honest trade-off: the polynomial is flexible enough to follow bands, and the same flexibility lets some noise through. In practice you compensate by using a longer window than you would with a moving average. Because the filter keeps band shapes so much better, the longer window is usually affordable, and for a given noise reduction Savitzky-Golay distorts sharp bands much less . The same trade-off can be read in terms of frequencies. Think of the spectrum as a sum of waves: broad bands are slow waves, noise is mostly fast waves. A Savitzky-Golay filter is a **low-pass filter**, one that lets slow waves through and damps fast ones. Compared with a moving average of the same length, it keeps a wider range of slow waves almost untouched (a flatter pass band) and moves the cut-off to faster waves, which is why it removes less noise. Like the moving average, it lets part of the fast waves leak through in small ripples called side lobes . ## Parameters: window size and polynomial order Savitzky-Golay smoothing has **two parameters**. ### Window size w As with the moving average, the window decides how many points take part: - **Small windows** (5 to 7 points): light smoothing, fine details kept. - **Medium windows** (9 to 21 points): moderate smoothing, typical for spectra. - **Large windows** (25 points and more): heavy smoothing for very noisy data or densely sampled spectra. What really matters is the window compared with the width of your narrowest band. A quadratic filter tolerates windows up to roughly the full width at half maximum (FWHM) of a band before the band height drops noticeably; a moving average starts to hurt well before that. For a Gaussian band whose FWHM equals the window, a quadratic filter keeps about 98 % of the height and a moving average about 80 %; with a window of 1.5 times the FWHM the figures are about 93 % and 64 %. ### Polynomial order p The order sets the flexibility of the local fit: - **Order 0**: a constant, which is the moving average. - **Order 2** (equivalently 3, for smoothing): the usual choice; follows the curvature of band tops. - **Order 4** (equivalently 5): more flexible, keeps sharper bands with a given window, but removes less noise, so it needs a longer window. - **Higher orders**: rarely useful for smoothing. A polynomial of order $p$ has $p + 1$ coefficients, so the window must contain more than $p + 1$ points (if $w = p + 1$, the polynomial passes through every point and nothing is smoothed). A comfortable rule is $w \geq 2p + 3$ (for example $w \geq 7$ for $p = 2$), and preferably more. With a window of 5 points, an order-4 polynomial goes through all five points and smooths nothing. Higher orders need longer windows; raising the order from 2 to 4 (or from 4 to 6) while keeping the window fixed always reduces the smoothing. ## Edge effects The first and last $m$ points do not have a full centred window. A natural solution, used by `scipy.signal.savgol_filter` (default `mode='interp'`), MATLAB's `sgolayfilt` and R's `signal::sgolayfilt`, is to fit the polynomial to the first (or last) complete window and evaluate it at the edge positions instead of at the centre. Those edge estimates are noisier than the interior ones, because a polynomial is less well determined near the end of its fitting range. This edge treatment, with its own tables of weights for each edge position, was worked out by Gorry . Other implementations pad the data by mirroring or repeating values. Whatever the method, treat the first and last half-window of points with some caution. ## The derivative bonus Because Savitzky-Golay fits a polynomial, it can also return the **derivative** of that polynomial at the centre. The first derivative at $t = 0$ is $\hat{a}_1$, the second is $2\hat{a}_2$, and so on. Each is again a fixed set of weights, so smoothed derivatives cost no more than smoothing . The same odd-even pairing as for smoothing appears here, shifted by one: for the first derivative, order 2 gives exactly the same weights as order 1. For example, the 5-point quadratic first-derivative weights are $\frac{1}{10}\,[-2,\ -1,\ 0,\ 1,\ 2]$ (per data point; divide by the spacing $\Delta x$ to get the derivative per wavelength unit). Differentiating raw noisy data amplifies the noise badly; differentiating a local least squares fit does not. This is why Savitzky-Golay is the standard way to compute spectral derivatives in chemometrics. It is covered in detail in [Spectral Derivatives](./derivatives). ## Code examples ```python from scipy.signal import savgol_filter, savgol_coeffs # Noisy synthetic spectrum rng = np.random.default_rng(42) x = np.linspace(0, 100, 500) dx = x[1] - x[0] true_signal = (0.8 * np.exp(-((x - 25)**2) / 20) + 0.6 * np.exp(-((x - 50)**2) / 15) + 0.9 * np.exp(-((x - 75)**2) / 25)) noisy_signal = true_signal + rng.normal(0, 0.08, len(x)) # The weights themselves: window 5, order 2 -> [-3, 12, 17, 12, -3] / 35 print(savgol_coeffs(5, 2) * 35) # savgol_filter(data, window_length, polyorder) settings = [(5, 2), (11, 2), (11, 4), (21, 2)] fig, axes = plt.subplots(2, 2, figsize=(12, 8), sharex=True, sharey=True) for ax, (w, p) in zip(axes.flat, settings): smoothed = savgol_filter(noisy_signal, window_length=w, polyorder=p) ax.plot(x, noisy_signal, color='gray', alpha=0.5, label='Noisy') ax.plot(x, smoothed, linewidth=2, label=f'w = {w}, p = {p}') ax.plot(x, true_signal, 'k--', alpha=0.7, label='True') ax.set_title(f'Savitzky-Golay, w = {w}, p = {p}') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.show() # Smoothing and derivatives in one call. # delta = sample spacing, so the derivatives are per x unit, not per point. smooth = savgol_filter(noisy_signal, 11, 2) first_derivative = savgol_filter(noisy_signal, 11, 2, deriv=1, delta=dx) second_derivative = savgol_filter(noisy_signal, 11, 2, deriv=2, delta=dx) fig, axes = plt.subplots(3, 1, figsize=(10, 8), sharex=True) axes[0].plot(x, noisy_signal, color='gray', alpha=0.5) axes[0].plot(x, smooth, linewidth=2) axes[0].set_ylabel('Signal') axes[1].plot(x, first_derivative, color='red') axes[1].set_ylabel('1st derivative') axes[2].plot(x, second_derivative, color='green') axes[2].set_ylabel('2nd derivative') axes[2].set_xlabel('x') plt.tight_layout() plt.show() ``` ```matlab % Savitzky-Golay smoothing with the Signal Processing Toolbox rng(42); x = linspace(0, 100, 500); dx = x(2) - x(1); true_signal = 0.8 * exp(-((x - 25).^2) / 20) + ... 0.6 * exp(-((x - 50).^2) / 15) + ... 0.9 * exp(-((x - 75).^2) / 25); noisy_signal = true_signal + 0.08 * randn(size(x)); % sgolayfilt(data, polynomial_order, window_length) # Whittaker Smoothing (/introduction-to-chemometrics/preprocessing/whittaker) The ideas behind this smoother go back to **Edmund Taylor Whittaker** (1873 to 1956), a British mathematician and astronomer who served as Royal Astronomer of Ireland before spending most of his career at the University of Edinburgh. In 1923 he published *On a New Method of Graduation* in the *Proceedings of the Edinburgh Mathematical Society* . "Graduation" was the actuaries' word for smoothing mortality tables, which are full of random fluctuations when the number of deaths in each age group is small. Whittaker's proposal was to treat smoothing as a compromise: find the curve that stays close to the observed values while keeping the sum of its squared differences, a measure of roughness, small. The formulation was elegant, but in 1923 solving the resulting system of equations by hand was laborious. The method lived on mainly among actuaries, as Whittaker-Henderson graduation, after the American actuary **Robert Henderson** showed in 1924 how to carry out the computation more practically . The same idea was later rediscovered by economists: the Hodrick-Prescott filter, widely used to separate trends from business cycles, is exactly a Whittaker smoother with second differences. It circulated as a working paper from 1980 and was published only in 1997, which is why the reference carries that date . Eight decades after the original paper, **Paul H. C. Eilers** brought the method to chemometrics. His 2003 paper *A Perfect Smoother* in *Analytical Chemistry* showed that, with sparse matrix techniques, the smoother takes only a few lines of code, runs in a time proportional to the number of points, handles missing data and uneven spacing, and is controlled by one intuitive parameter $\lambda$. Eilers had already worked with difference penalties when he and **Brian Marx** introduced P-splines (penalised B-splines) in 1996 . Today Whittaker smoothing and its weighted variants underpin popular baseline correction methods such as [asymmetric least squares](./baseline-correction-asls), as well as general denoising. ## The optimisation approach to smoothing The [moving average](./moving-average), [Gaussian](./gaussian-smoothing) and [Savitzky-Golay](./savitzky-golay) filters all work locally: each smoothed value is computed from a small window of neighbours. Whittaker smoothing takes a different view. It asks a single question about the **whole** spectrum at once: > **Which curve stays close to the data while being as smooth as possible?** Those two wishes pull in opposite directions, and the method is nothing more than a precise way of balancing them. ## The core idea: balancing two goals 1. **Fidelity**: the smoothed curve $\mathbf{z}$ should stay close to the measured data $\mathbf{y}$. 2. **Smoothness**: $\mathbf{z}$ should not wiggle. Perfect fidelity means $\mathbf{z} = \mathbf{y}$, keeping all the noise. Perfect smoothness means a featureless line, losing every band. Whittaker smoothing minimises a weighted sum of both: $S = \underbrace{\sum_{i=1}^{n} (y_i - z_i)^2}_{\text{misfit}} + \lambda \underbrace{\sum_{i=1}^{n-d} (\Delta^d z_i)^2}_{\text{roughness}}$ where: - $\mathbf{y}$ is the measured, noisy data ($n$ points), - $\mathbf{z}$ is the smoothed curve we are looking for, - $\lambda$ (lambda) is the smoothing parameter, - $\Delta^d$ is the difference operator of order $d$, usually 2. If this reminds you of least squares, it should. The first term is exactly the [least squares](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics/) misfit. The second is a **penalty** that punishes roughness. This is the same structure as [ridge regression](/introduction-to-chemometrics/models/regression-models/ridge-regression) and many other regularised methods: fit the data, but pay a price for complexity. ### The misfit term $\sum_{i=1}^{n} (y_i - z_i)^2$ is the sum of squared differences between data and curve. It is small when the curve follows the data closely. ### The roughness penalty The **difference operator** measures how much consecutive values change: - **First difference** ($d = 1$): $\Delta z_i = z_{i+1} - z_i$, a discrete slope. - **Second difference** ($d = 2$): $\Delta^2 z_i = z_{i+2} - 2z_{i+1} + z_i$, a discrete curvature. - **Third difference** ($d = 3$): $\Delta^3 z_i = z_{i+3} - 3z_{i+2} + 3z_{i+1} - z_i$. Squaring and summing these differences gives one number that is large for a jagged curve and small for a smooth one. Second differences measure **curvature**, how much the curve bends. A smooth spectrum bends gently; noise makes it bend sharply at every point. Penalising curvature therefore attacks the noise while allowing slopes and gentle bands. A useful way to see the role of $d$ is to ask what the penalty does *not* punish. A polynomial of degree $d - 1$ has all its $d$-th differences equal to zero. So with $d = 1$ only a constant is "free", with $d = 2$ a straight line is free, and with $d = 3$ a parabola is free. That is what the smoother tends towards when $\lambda$ becomes very large. First differences tend to flatten band tops and slopes; $d = 2$ is the usual default. With $d = 3$ parabolas are not penalised, so band tops are flattened less at the same $\lambda$, but a much larger $\lambda$ is needed for the same noise reduction. ### The λ parameter: the key to the trade-off - $\lambda \to 0$: roughness is free, so $\mathbf{z} \to \mathbf{y}$ and nothing is smoothed. - $\lambda \to \infty$: roughness is forbidden, so $\mathbf{z}$ becomes the least squares polynomial of degree $d - 1$ (a straight line for $d = 2$). - In between: noise is removed while bands are kept. Useful values cover many orders of magnitude, typically from about 1 to $10^8$ depending on the noise, the number of points and $d$. That is why $\lambda$ is almost always chosen on a logarithmic scale. ## The solution: one linear system Despite looking like a complicated optimisation, the problem has a **closed-form solution**. Write the differences as a matrix product $\mathbf{D}\mathbf{z}$, where $\mathbf{D}$ is the difference matrix (shown below). Then $S = (\mathbf{y} - \mathbf{z})^T(\mathbf{y} - \mathbf{z}) + \lambda\, \mathbf{z}^T\mathbf{D}^T\mathbf{D}\,\mathbf{z}$ Just as in the derivation of [the normal equations](/introduction-to-chemometrics/least-squares/normal-equations), we set the gradient with respect to $\mathbf{z}$ to zero: $-2(\mathbf{y} - \mathbf{z}) + 2\lambda\,\mathbf{D}^T\mathbf{D}\,\mathbf{z} = 0$ which rearranges to $(\mathbf{I} + \lambda \mathbf{D}^T\mathbf{D})\,\mathbf{z} = \mathbf{y}$ where $\mathbf{I}$ is the identity matrix. Smoothing a spectrum is therefore solving one linear system: $\hat{\mathbf{z}} = (\mathbf{I} + \lambda \mathbf{D}^T\mathbf{D})^{-1}\mathbf{y}$ Do not be put off by the inverse. The matrix $\mathbf{I} + \lambda \mathbf{D}^T\mathbf{D}$ is **banded**: apart from its main diagonal, only $d$ diagonals on each side are non-zero. It is also symmetric and positive definite. A banded Cholesky factorisation solves the system in a time proportional to $n$, without ever forming an inverse . A spectrum with thousands of points is smoothed in milliseconds. ## What does the difference matrix look like? For second differences and $n = 6$ points: ``` D = [ 1 -2 1 0 0 0 ] [ 0 1 -2 1 0 0 ] [ 0 0 1 -2 1 0 ] [ 0 0 0 1 -2 1 ] ``` Each row computes one second difference, $z_i - 2z_{i+1} + z_{i+2}$. The matrix has $n - d$ rows. For first differences: ``` D = [ -1 1 0 0 0 0 ] [ 0 -1 1 0 0 0 ] [ 0 0 -1 1 0 0 ] [ 0 0 0 -1 1 0 ] [ 0 0 0 0 -1 1 ] ``` These matrices are almost entirely zeros, which is exactly why the computation is cheap. In code they are usually built by differencing the identity matrix $d$ times. ## Interactive exploration The panel below smooths a noisy spectrum with adjustable $\lambda$ and $d$. The metrics show the two terms of the objective. The lower plot is the **trade-off curve**: every dot is one value of $\lambda$, from almost no smoothing (top left: tiny misfit, large roughness) to heavy smoothing (bottom right: large misfit, almost no roughness). The highlighted dot is your current choice. **Try this:** 1. Start at $\lambda \approx 5$ and observe moderate smoothing: the noise is much reduced and the narrow band keeps most of its height. At $\lambda \approx 30$ the noise is gone, but the narrow band has already lost about a sixth of its height. 2. Increase $\log_{10}\lambda$ to 6 and watch the curve stiffen until even the broad band is flattened. 3. Go down to $\lambda = 0.1$ and see the curve follow the noise. 4. Switch between $d = 1, 2, 3$ at a high $\lambda$ and see the curve approach a constant, a line and a parabola. The parabola needs the largest values: with $d = 3$, go up to $\lambda = 10^{11}$. 5. Watch the error against the true signal: it first falls, reaches a minimum, and then rises again as real features are smoothed away. The trade-off curve also gives a useful landmark. If the curve $\mathbf{z}$ were exactly the true signal, the residuals would be pure noise, and the misfit would be about $n\sigma^2$, the number of points times the noise variance. In this panel $n = 200$ and $\sigma = 0.06$, so $n\sigma^2 \approx 0.72$ (about $-0.14$ on the log scale). A misfit much smaller than that means the curve is still following noise; a misfit much larger means real signal is being removed. The best curve, however, sits a little to the left of that landmark, because the smoother also absorbs part of the noise. The expected misfit is then roughly $\sigma^2(n - \text{ED})$, where ED, the **effective number of parameters** (also called effective degrees of freedom), is the trace (the sum of the diagonal elements) of the smoother matrix $\mathbf{H} = (\mathbf{I} + \lambda\mathbf{D}^T\mathbf{D})^{-1}$, the matrix that turns $\mathbf{y}$ into $\hat{\mathbf{z}}$. In this panel, the lowest error against the true signal is reached near $\lambda \approx 5$, with ED about 49 and a misfit of about 0.5, while the λ that brings the misfit up to 0.72 gives an error about a third larger. So when the noise level of your instrument is known, use $n\sigma^2$ as an upper bound for the misfit, not as a target. The trade-off curve in the panel marks both the $n\sigma^2$ landmark and the λ with the lowest error, so you can see the gap. ## How the smoother sees frequencies For a long, evenly spaced signal, the Whittaker smoother acts like a filter that keeps a wave of frequency $f$ (in cycles per point) with gain $G(f) = \frac{1}{1 + \lambda\,\big(2\sin(\pi f)\big)^{2d}}$ Slow waves (small $f$) have a gain close to 1 and pass; fast waves are strongly damped. The gain never becomes negative, so, unlike a moving average, it never flips noise components. Larger $\lambda$ moves the cut-off to lower frequencies, and larger $d$ makes the transition between kept and removed frequencies steeper. This formula turns the abstract λ into something you can picture. The gain drops to one half when $\lambda\,(2\sin\pi f)^{2d} = 1$. For $d = 2$ and $\lambda = 100$ this happens at $f_{50} = \tfrac{1}{\pi}\arcsin\!\big(\lambda^{-1/4}/2\big) \approx 0.05$ cycles per point: waves longer than about 20 points are mostly kept, shorter ones are mostly removed. A handy approximation: waves with a period of $P$ points are halved when $\lambda \approx (P/2\pi)^{2d}$, which for $d = 2$ and $P = 20$ gives again about 100. ## Weights: smoothing with missing or unreliable data A powerful extension gives each point a weight $w_i$: $S = \sum_{i=1}^{n} w_i(y_i - z_i)^2 + \lambda \sum_{i=1}^{n-d} (\Delta^d z_i)^2$ whose solution is $(\mathbf{W} + \lambda \mathbf{D}^T\mathbf{D})\,\mathbf{z} = \mathbf{W}\mathbf{y}$ with $\mathbf{W}$ a diagonal matrix holding the weights. Points with a large weight pull the curve strongly; points with a small weight barely matter. A weight of **zero** means the point is ignored entirely, which lets the smoother **fill gaps**: inside a gap only the penalty acts, so the curve continues as smoothly as possible . Inside the gap the result is a smooth polynomial curve joining the two sides (a straight line for $d = 1$, a cubic curve for $d = 2$, a quintic for $d = 3$). The kind of curve depends only on $d$; $\lambda$ changes how it meets the data at the edges of the gap. The grey dotted line in the panel is a plain straight-line interpolation, for comparison. The smoother cannot invent a band that was never measured, but it bridges the gap more naturally than a straight line when $d \geq 2$. Weights are also the route to **baseline correction**. In asymmetric least squares, points above the current curve (likely to be bands) get a small weight and points below get a large weight, so after a few iterations the curve follows the baseline under the bands . See [Baseline correction with AsLS](./baseline-correction-asls). ## Choosing λ: how much to smooth? There is no universal value. The best $\lambda$ depends on: - the **noise level**, - the **width of the narrowest feature** you want to keep, measured in data points, - the **difference order** $d$ (higher orders need much larger $\lambda$ for similar smoothing), - your **application**: exploration or quantitative modelling. ### Practical guidelines **Scan on a log scale.** Try $\lambda = 1, 10, 100, \ldots$ and look at the results and the residuals. **Visual inspection:** 1. Plot the original and smoothed signals. 2. Increase $\lambda$ gradually. 3. Stop when the noise is gone but the narrowest bands still keep their height. **Residual analysis:** 1. Compute the residuals $r_i = y_i - z_i$. 2. If they look like random noise, the smoothing is appropriate. 3. If they show structure (band shapes, trends), you are over-smoothing. **Cross-validation:** leave out one point at a time, smooth the rest, and see how well the left-out point is predicted. Eilers showed that for this smoother the leave-one-out error can be computed without refitting: the leave-one-out residual of point $i$ is $r_i/(1 - h_{ii})$, where $h_{ii}$ are the diagonal elements of the "hat" matrix $\mathbf{H} = (\mathbf{W} + \lambda\mathbf{D}^T\mathbf{D})^{-1}\mathbf{W}$, so $\lambda$ can be chosen automatically . Garcia later combined a closely related criterion, generalised cross-validation, with a fast solver based on the discrete cosine transform, a popular way to pick λ automatically , and Weinert described efficient algorithms for the computation itself . Be aware that cross-validation can choose too little smoothing when the noise is correlated from point to point. **For calibration models:** smooth with several $\lambda$ values, build the model on each, and choose the value that gives the best cross-validated prediction error. If you build a calibration model, use the **same** $\lambda$ and $d$ for every spectrum: calibration, validation and test. Different smoothing would create differences that are not chemical. ## Code examples ```python from scipy import sparse from scipy.sparse.linalg import spsolve def whittaker_smooth(y, lam=1e2, d=2, w=None): """ Whittaker smoother (Eilers, 2003). y : 1D array, the signal to smooth lam : smoothing parameter lambda (larger = smoother) d : order of the difference penalty (usually 2) w : optional weights (0 = ignore the point); default all ones """ y = np.asarray(y, dtype=float) n = len(y) if w is None: w = np.ones(n) # d-th order difference matrix, shape (n - d, n) D = sparse.eye(n, format='csr') for _ in range(d): D = D[1:] - D[:-1] W = sparse.diags(w) A = (W + lam * D.T @ D).tocsc() return spsolve(A, w * y) # Noisy synthetic spectrum rng = np.random.default_rng(42) wavelength = np.linspace(400, 800, 400) true_signal = (2 * np.exp(-((wavelength - 500)**2) / 500) + 1 * np.exp(-((wavelength - 600)**2) / 300)) noisy = true_signal + rng.normal(0, 0.1, len(wavelength)) plt.figure(figsize=(10, 6)) plt.plot(wavelength, noisy, color='gray', alpha=0.5, label='Noisy') for lam in [1e1, 1e3, 1e5]: plt.plot(wavelength, whittaker_smooth(noisy, lam=lam), linewidth=2, label=f'λ = {lam:g}') plt.plot(wavelength, true_signal, 'k--', label='True signal') plt.xlabel('Wavelength (nm)') plt.ylabel('Absorbance') plt.legend() plt.show() # Gap filling: give a stretch of points weight 0 w = np.ones(len(noisy)) w[180:230] = 0 filled = whittaker_smooth(noisy, lam=1e3, d=2, w=w) ``` ```matlab function z = whittaker_smooth(y, lam, d, w) % Whittaker smoother (Eilers, 2003) % y : signal (vector) % lam : smoothing parameter lambda (larger = smoother) % d : order of the difference penalty (usually 2) % w : optional weights (0 = ignore the point) if nargin # How to Use Ada (/lovelaces-square/ada/how-to-use-ada) Ada is attached to the Lovelace ecosystem, so the most effective way to use her is to treat the conversation as a path through articles, code, datasets, explanations, and practical work. A session may begin with a question in plain language, continue with a search across The Library or The Square, and then move into a workspace when the next step is no longer only to read, but to build, inspect, or run something. This page explains that process in the order most users experience it. It starts with the basic chat interface, then shows how to attach material, when the workspace becomes useful, how reasoning controls affect the response, how search and execution fit into the workflow, and how to use Ada carefully so the system remains useful for everyone. ## Start in chat Most sessions begin in the main chat view. This is where you ask the first question, read the answer, ask for clarification, and decide what should happen next. If your goal is to understand a concept, compare two methods, find a Library article, locate a code entry, or identify a relevant dataset, the standard chat view is usually enough to begin. The home screen below is the default starting point. It gives you the conversation area, the prompt box, and the controls that let the session expand into a more advanced workflow when needed. The default Ada chat surface. Click the image to open it fullscreen. When you begin a conversation, it helps to state the topic, the level, and the kind of help you want. If you say that you are reading about PCA for the first time, Ada can stay at an introductory level. If you say that you need MATLAB code for baseline correction, Ada can treat the request as a search task instead of a teaching task. If you say that you want an explanation first and then a code example, Ada can keep both goals in the same thread and move from one to the other without restarting. ## Attach images, PDFs, and source files when they provide useful context The prompt box accepts more than typed text. You can paste or drag images into it, attach a PDF, or drop a source file or ZIP project when the request depends on material you already have. This is useful when you want Ada to inspect a plot, read a short document, explain a script, or open a project in the workspace. Ada accepts up to three JPEG or PNG images with one message. Images are resized and compressed in the browser before they are sent for analysis. A single PDF of up to 2 MB can also be attached. For source work, common text formats include MATLAB, Python, R, Julia, JavaScript and TypeScript, HTML and CSS, Markdown, JSON, CSV and TSV, XML, and YAML. ZIP projects can contain several supported text files. The request can remain natural. You can say, for example, "What is wrong with this plot?", "Summarize the method described in this PDF", or "Open these MATLAB files and help me understand the entry point." The attachment should provide evidence for the task, not instructions that Ada follows independently of your request. The + menu provides the main ways to attach material, open the workspace, import a project, or create an artifact. Click the image to open it fullscreen. The menu makes the available routes explicit: add images or a PDF for analysis, upload a supported source file, open the workspace, clean code, create an interactive artifact, or import a GitHub repository. Choose the route that matches the material and the kind of work you want Ada to do with it. You can also drag an image directly into the prompt when the visual itself is the context Ada needs to inspect. An image can be dragged directly into Ada's prompt before sending the request. Click the image to open it fullscreen. Image and PDF contents are sent to OpenAI only for the request that analyzes them. Ada does not save them to its database or workspace storage. The visible conversation keeps the text of your message and may show the attachment name or a temporary preview while the page remains open. Source files are different: if you import them as a project, the supported extracted text files can be saved in the workspace so the project can be reopened later. ## Use the workspace when the task becomes practical The workspace is the mode Ada uses when the conversation needs a real working area. In plain terms, the workspace is where the task stops being only a question-and-answer exchange and becomes something you can inspect, edit, preview, or run. Ada can open the workspace because you ask for it directly, or because the request naturally turns into a build-oriented task. This is useful when you want to create an interactive teaching component, inspect code imported from The Square, modify a small example, or run Python inside the same session. When the workspace first opens, it provides a dedicated working surface alongside the conversation, with options to ask Ada to build something or start from an existing project. A professor preparing a classroom explanation of Gaussian smoothing could ask Ada to open the workspace and build a small interactive component. A researcher who finds a code entry through search could ask Ada to import it into the workspace to inspect the files and understand how it works. A student who wants to move from theory into practice could ask Ada to create a simple runnable example and explain the output after it runs. The workspace is not a separate tool disconnected from the chat. It stays attached to the same conversation, so Ada still knows what you were discussing before it appeared. The chat remains visible and the practical work becomes another step in the same flow. The workspace opens as a dedicated working surface beside the conversation, ready to build something or start from an existing project. Click the image to open it fullscreen. Once the workspace is open, the interface becomes easier to read if you think of it as a working table. One part of the screen keeps the conversation alive. Another part shows files, code, previews, or execution results. That arrangement is what allows Ada to keep explaining while also helping you do something concrete. ## Save useful workspace stages as versions The workspace keeps numeric snapshots of the project as it changes. Imports, new files, and edits create automatic snapshots so that the project has a recoverable history. You can also ask Ada to save a named version, such as "Before refactor" or "Teaching example ready", when a particular stage is worth finding again. A name is a label attached to a numeric workspace snapshot. It is not a folder, a Git branch, a public release, or a version of a code entry in The Square. You can ask Ada to list the saved versions, open one by its number or exact name, or delete a snapshot you no longer need. Opening an older snapshot changes the workspace view; it does not rewrite the public history of a Square contribution. ## Adjust reasoning when the task needs more care Ada includes task-level controls that affect how she approaches a request. The most important one for most users is the reasoning setting, labelled **Effort** in the interface. In simple terms, it controls how much effort Ada spends structuring and checking the answer before she replies. For short retrieval tasks, a lighter setting is usually enough. If you want Ada to find a Library page, locate a code entry, or answer a direct factual question about material already in the ecosystem, the extra cost of deeper reasoning is often unnecessary. For more difficult tasks, such as building a teaching sequence, comparing several methods carefully, or planning a step-by-step explanation for a student with a specific background, a higher setting can help produce a more deliberate answer. The goal is not to turn reasoning up by default, but to use it when the task genuinely benefits from a slower and more careful pass. The menu offers minimal, low, medium, and high effort. In practice, that means keeping straightforward requests simple and reserving heavier reasoning for synthesis, comparison, and structured explanation. The reasoning control lets you choose how much effort Ada should spend on a task. Click the image to open it fullscreen. Ada uses GPT-5 Nano as the standard model. Association members and other authorized ecosystem roles may also see GPT-5.4 Nano and DeepSeek V4 Flash in the model selector. Although we would like all users to work with every available model, the ecosystem also has to balance growth and operating cost. The selector should therefore be understood as an access and capacity control rather than the main way to use Ada. For most users, the more important decision is whether the task needs a faster answer or a more careful one. DeepSeek does not inspect images or PDFs directly. If it is selected for a request containing visual or document input, Ada first uses an OpenAI model to interpret the attachment and then gives the resulting report to DeepSeek so the conversation can continue. This keeps those inputs usable while making the provider boundary explicit. ## Search across the ecosystem in one conversation One of Ada's main strengths is continuity across resource types. A user does not need to think in separate systems and decide in advance whether the answer should come from an article, a code page, a dataset, or a platform document. The conversation can begin with one kind of request and naturally continue into another. That continuity matters because real work rarely stays in one category. A student may begin with a conceptual question, continue into a Library article, and then ask for one practical example. A researcher may begin with a code-oriented question and then ask for the theoretical background behind the implementation. A teacher may start from a teaching objective and then ask for an interactive component or a dataset that supports that lesson. Ada is most useful when it is allowed to connect those steps instead of treating them as isolated searches. ## Find and verify scholarly references Ada can search OpenAlex when a question asks for papers, authors, publication details, citation context, or a reference for a method. This is different from asking the language model to recall a citation from memory. Ada can retrieve candidate records and then check the exact metadata of a selected paper by DOI or OpenAlex identifier before presenting it as verified. For broad questions, a useful request is often simple: "What are the standard references for PARAFAC in chemometrics?" or "Find the foundational paper and a widely used tutorial for MCR-ALS." If the year, author, application field, or variant matters, include it. If it does not, leaving the first search broad usually gives Ada a better chance to separate the standard method from a specialized variant. For a baseline-correction question, Ada returns a candidate reference and explains how it relates to the method. Click the image to open it fullscreen. OpenAlex provides scholarly metadata, citation counts, links, and abstract text when available. It does not mean that Ada has read the full article. Use the retrieved record to reach the source, and read the paper itself before relying on detailed scientific claims. ## Bring code and datasets into the same flow Once the conceptual part is clear, the next question is often practical. You may want to know whether a method already has an implementation in the ecosystem, whether a dataset is suitable for testing an idea, or whether a code entry in one language is easier to adapt than another. Ada can help with that transition because the conversation does not need to be reset when the goal changes from understanding to finding. This is especially useful in chemometrics, where users often move from a method name to preprocessing choices, then into an implementation, and then into data. A question about baseline correction may begin with a conceptual explanation, continue into a search for MATLAB or Python code, and end with a request to inspect one implementation in more detail. Ada is designed to keep those moves connected. ## Import code into the workspace when you need to inspect or build When a code result becomes important enough to inspect directly, the next step is often to bring it into the workspace. That allows the code to become part of the working session rather than something you only discuss abstractly. The file tree becomes visible, the active file can be read or edited, and the conversation remains available while you decide what to do with the imported material. This is useful for understanding how an implementation is structured, creating a teaching example from an existing code entry, adapting a function to a slightly different purpose, or tracing how a method is actually implemented instead of only reading its description. A result from The Square can become a working project inside the workspace without breaking the conversation. Click the image to open it fullscreen. Inside the workspace, the most important shift is conceptual rather than technical. You are no longer only asking what something means. You are now working with an artifact. Ada can still explain what you see, but the session has moved into inspection, editing, and iteration. Ada imports the latest or current approved version of a code entry from The Square. Historical public versions can still be viewed and downloaded from their version-specific pages in The Square, but Ada cannot select an older Square version for exact workspace import. This distinction matters when reproducibility depends on a particular release. Public GitHub repositories or folders can also be imported, subject to the same project limits and source-safety checks. ## Run Python when a compact example is more useful than a description Some questions are easier to understand when they produce output. A short explanation of PCA scores or smoothing can be helpful, but a small runnable example often makes the idea clearer because the result can be seen immediately. Ada can therefore run compact Python examples inside the workspace and keep the execution results connected to the ongoing conversation. This is useful for didactic work, for small demonstrations, and for quick exploratory examples where the goal is understanding rather than formal validation. You can ask Ada to create a minimal example, run it, explain the output, revise the code, and run it again without leaving the session. Python results remain connected to the code and the conversation. Click the image to open it fullscreen. Python execution should be understood as a compact educational workspace, not as a replacement for a full research environment. It is strong when the task is to test an idea, illustrate a concept, or build a small example that can be discussed immediately. It is not the right place to treat an automatically generated result as final scientific evidence without checking it carefully in a proper workflow. ## Build interactive teaching material when explanation alone is not enough Some chemometric ideas are easier to learn when the user can adjust a parameter and see the result instead of only reading about it. Ada can help create that kind of material inside the workspace. A request can begin as a conceptual explanation and then turn into a small component that shows the behavior of a smoothing parameter, a projection, or a comparison between methods. That makes Ada useful not only for answering questions, but also for producing small educational artifacts that support lectures, self-study, or internal teaching materials. If a static explanation is not enough, asking Ada to open the workspace and build an interactive component is often the right next step. Ada can generate an interactive artifact and open it directly in preview. Click the image to open it fullscreen. ## Ask Ada to inspect the rendered workspace Ada can also inspect the rendered workspace when the task depends on what is actually visible. You can ask naturally: "Check the preview", "What looks wrong in the image?", or "Inspect the layout before you finish." Ada can capture the current rendered workspace, analyze visible issues, and continue the same task with that evidence. This is especially useful for clipped controls, overlapping content, unreadable labels, empty previews, or layouts that behave differently from what the source code suggests. Visual inspection is used when the request calls for it; it is not an automatic redesign pass after every edit. It also does not replace functional testing. A preview can look correct while a calculation or interaction is still wrong, so important artifacts should be checked both visually and behaviorally. ## Prepare a contribution to The Square When a workspace project is ready to share, Ada can help prepare either a new code submission or a new version of one of your existing Square codes. The process is staged. Ada first checks the workspace files and required public metadata, then prepares a draft with a reviewer report and a confirmation panel. Nothing is submitted merely because a draft was prepared. Review the title, description, authorship, license, version, categories, references, links, and included files before confirming. If you close the panel or continue editing, Ada keeps a private recovery note in the workspace so a fresh draft can be prepared later. That note is not part of the public submission. A confirmed submission still enters human review; Ada does not approve it. ## Work with untrusted code and data carefully Ada can import files, save workspace versions, preview generated artifacts, and execute Python in a browser sandbox. The sandbox reduces risk, but it does not make arbitrary code safe. Treat public repositories, community submissions, uploaded projects, and generated code as untrusted until you have reviewed them. Only use Ada with material that you are allowed to share and process. Do not upload private, confidential, restricted, personal, clinical, student, industrial, or unpublished data unless you have a clear authorization and a clear reason to do so. For public beta use, the safest default is to work with public examples, open-source code, open-access datasets, or synthetic teaching data. When Ada imports a ZIP project or code into the workspace, supported text files can be saved as part of the conversation's workspace version. The original ZIP file is not stored as a ZIP, but the extracted workspace files may be stored with the conversation so the session can be reopened later. Current project limits are deliberately conservative. A ZIP or project upload can be up to 2 MB and contain up to 50 supported files. During import, an individual source file can be up to 200,000 bytes and the imported text project can total up to 1,000,000 bytes. Once a project is in the workspace, an individual editable file can contain up to 500,000 characters, while the complete workspace remains limited to 1,000,000 characters. The smaller per-file import limit protects Ada from unexpectedly large external files; the larger workspace-file limit allows substantial files to be created or expanded during deliberate work. Before running imported code, inspect the relevant files and ask whether the source is trustworthy. External code can read the files loaded into that workspace and can still consume browser resources even when it runs inside the sandbox. Important scientific results should be checked in a proper research environment before they are used, cited, or shared. ## Understand token use and work responsibly Ada is free for users, but each conversation still has a real cost for the project. That cost is measured partly through tokens, which are the units a language model reads and writes while processing a request. A short exchange uses relatively few tokens. A long prompt, a long answer, a large conversation history, multiple search steps, or a workspace session with imports and execution can use many more. The practical implication is simple. Clarity helps more than volume. A focused question usually costs less and produces a better answer than a vague prompt padded with unnecessary context. If the conversation already contains the needed background, it is usually better to continue from there than to paste the same information again. If the task only needs a direct explanation, it is better not to force a heavier workflow. If a simple answer is enough, there is no reason to use deeper reasoning, import code, or run Python just because those features exist. Responsible use matters because Lovelace's Square is a non-profit ecosystem. The system is intended to stay accessible, and that depends on users treating the available resources with care. Using Ada well therefore means asking clearly, using more demanding features when they are actually needed, and checking sources instead of asking the system to generate longer output than the task requires. ## Use Ada as documentation support, not as a replacement for judgment Ada is designed to help users understand, locate, connect, and test material across the ecosystem. She can make it much easier to move from a question to a resource and from a resource to a practical next step. That does not remove the need for scientific judgment. If a claim matters, read the source. If a code implementation matters, inspect it. If a generated result matters, verify it in the appropriate environment. The best way to use Ada is therefore direct and pragmatic. Ask a clear question, let the conversation narrow the problem, move into the workspace when the task becomes practical, and keep the sources close to the answer. That is where the system is most useful. # Last Developments (/lovelaces-square/ada/last-developments) Ada has been built from the ground up as the ecosystem's conversational AI layer for chemometrics. Here's where we are. ## Roadmap summary 1. **Foundation and ecosystem role** — Completed - Defined Ada's role as the conversational connection between The Library, The Square, and practical work - Established the public-beta experience, usage controls, and domain-specific behavior for chemometrics - Built persistent conversations with light and dark themes and responsive ecosystem navigation 2. **Agentic search and grounded answers** — Completed - Connected Ada to Library articles, platform guidance, public codes, datasets, and version information - Added multi-step tool use so Ada can continue searching when the first result is not sufficient - Strengthened source presentation so normal users see useful public links while machine-oriented resources remain internal unless requested - Improved selection guidance so standard and traditional field approaches are preferred over specialized variants unless the question asks for one 3. **Literature discovery and reference checking** — Completed - Integrated OpenAlex for paper discovery, authorship, venue, citation context, and available abstracts - Added exact paper lookup by DOI or OpenAlex identifier before a reference is presented as verified - Kept a clear distinction between scholarly metadata and full-text paper reading - Added separate inspection of public Zenodo records and identifiers 4. **Workspace and project work** — Completed - Moved practical artifacts into a workspace-first experience with files, code, previews, results, and chat in one view - Added direct source-file, ZIP, public GitHub, and latest approved Square code imports - Added automatic numeric snapshots and user-named workspace versions - Added safe file-reading and targeted editing tools for multi-file projects 5. **Execution, previews, and interactive teaching** — Completed - Integrated browser-side Python execution for compact examples, plots, and inspectable results - Added interactive HTML teaching artifacts with an automatic transition to the completed preview - Added rendered-workspace inspection so Ada can check visible layout problems when the task requires it - Kept execution, visual inspection, edits, and explanation inside the same conversation 6. **Images, PDFs, and model choice** — Completed - Added paste, selection, and drag-and-drop support for images, PDFs, source files, and ZIP projects - Kept image and PDF contents temporary instead of storing them in Ada's database or workspace - Added GPT-5 Nano as the standard model, with GPT-5.4 Nano and DeepSeek V4 Flash available to authorized Association roles - Added OpenAI interpretation for visual and PDF input when the selected conversational model does not support it directly 7. **Square contribution workflow** — Completed - Added staged preparation of new code submissions and updates from the active workspace - Added metadata checks, file selection, an Ada reviewer report, and explicit user confirmation before submission - Added a private recovery note so an interrupted draft can be prepared again without becoming part of the public submission - Preserved human review as the final approval step 8. **Public beta and reliability** — In progress - Opened Ada as a public beta for real community use - Improved queued follow-up messages, conversation transitions, workspace recovery, and long-running tool flows - Strengthened visual inspection and model handoffs so one task remains part of one coherent answer - Continuing to evaluate response quality, latency, tool choice, and user experience from real conversations ## What we are working on next 1. **Retrieval quality** - Continue improving how Ada distinguishes standard methods from variants and selects useful results across larger catalogs - Improve long-context retrieval when several articles, code entries, files, or papers are relevant to one task - Refine literature selection while keeping exact bibliographic verification close to the final answer 2. **Dataset workflows** - Make it easier to inspect dataset structure, provenance, and suitability before use - Develop clearer guided paths from a public dataset into a small, reproducible analysis - Improve the connection between dataset discovery, code selection, and workspace execution 3. **Learning and teaching** - Expand guided interactive material for common chemometrics workflows - Improve onboarding for users who are new to Ada, the workspace, or chemometrics itself - Continue refining accessible explanations without hiding the mathematical and scientific details that matter 4. **Performance and reliability** - Reduce avoidable latency in retrieval and multi-tool tasks without weakening the grounding of the answer - Improve recovery from interrupted streams, provider errors, large projects, and long conversations - Continue testing model behavior at different reasoning levels and documenting the limits observed in real use 5. **Future execution environments** - Explore practical MATLAB and R execution paths beyond the current browser-side Python environment - Evaluate these environments carefully before presenting them as supported scientific workflows Ada is now available in public beta at [ada.lovelacesquare.org](https://ada.lovelacesquare.org). We are still refining the experience based on real feedback, so the system should be treated as useful and active, but still evolving. Early conversations with researchers and educators have sparked ideas for what comes next. We are listening. If you try Ada and want to share feedback or ideas, reach out to us at [contact@lovelacesquare.org](mailto:contact@lovelacesquare.org). # What is Ada (/lovelaces-square/ada/what-is-ada) Ada is now available in public beta at [ada.lovelacesquare.org](https://ada.lovelacesquare.org). We are using this stage to learn from real use, evaluate response quality and tool behavior, and improve the experience while the system remains under active development. If you find something confusing or want to share feedback, you can contact us at [contact@lovelacesquare.org](mailto:contact@lovelacesquare.org). Ada can inspect attached images and PDFs, import files, save workspace versions, and execute Python in a browser sandbox. Treat external projects, GitHub repositories, community code, and generated code as untrusted unless you have reviewed them. Do not upload private, confidential, restricted, or personal data. See [How to Use Ada](/lovelaces-square/ada/how-to-use-ada/#work-with-untrusted-code-and-data-carefully) for the practical safety notes. ## The age of conversational AI Conversational AI has changed how many people approach technical knowledge. Instead of searching across many pages and then assembling the answer alone, users can now ask a question in plain language and receive a response that feels immediate and structured. That has lowered the barrier to entry for many fields, including scientific and technical ones. At the center of most of these systems is a **L**arge **L**anguage **M**odel, or LLM. These models are trained on large collections of text and learn patterns in language, explanation, and structure. When you ask a question, the model does not retrieve truth in the way a database does. It generates a response by predicting what a suitable answer should look like based on those patterns. One reason this feels so powerful is that an LLM can seem like a compressed form of human written knowledge. Decades of textbooks, research papers, tutorials, and technical discussions can suddenly be approached through a single conversational interface. Seen in that way, this is also a new form of knowledge transfer. Knowledge that would normally take time, access, and experience to navigate becomes reachable through a question. That does not remove the need for verification, but it does change how people approach knowledge and how quickly they can begin working with it. Seen from that angle, this may be one of the most important revolutions in access to knowledge in the last century, and possibly in human history. Its importance comes from how sharply it reduces the cost of reaching explanations, references, and technical orientation, in many cases pushing that practical cost close to zero. That shift opens a new path for democratizing access to knowledge, making forms of guidance that once depended on institutional access, long training, or many hours of searching and synthesis far more widely reachable. That makes LLMs very useful, but it also creates limits that matter in scientific work. A model can sound precise while still being wrong. It can produce a reference that looks plausible without having retrieved it from a real source. A common example is the DOI problem. A model may know the title of a paper, the topic, or the authors, and from that it may generate a DOI-like string that looks convincing. But a DOI is not something the model can infer from context. In practice it behaves like an arbitrary identifier, so if it has not been retrieved from a real source, it is not actually known. There is a second limitation that matters just as much. A general model only knows what was available in its training context. If a new article was published yesterday, or if a platform resource was updated recently, a model working from memory alone will not know that unless the system around it has access to current material. Even when a model is highly capable, depth also varies with size, cost, and configuration. A lightweight model can be very useful, but it may miss the level of nuance required in a specialized field such as chemometrics. This is where agency matters. By agency, we mean that the system is able to choose actions as part of the response process instead of only generating text from memory. It can decide to search, retrieve, inspect, and continue working with external material before responding. This has become a major direction in the LLM field, because it addresses some of the central weaknesses of model-only answers. Instead of inventing a reference, the system can look for the real source. Instead of relying on frozen training knowledge, it can work with current ecosystem content. Instead of stopping at a generic explanation, it can choose a path that is better matched to the task. In practice, this creates the need for a scientific assistant that is grounded in real material, connected to current sources, and able to act across an ecosystem rather than answer from memory alone. Ada was designed to meet that need. ## What is Ada [Ada](https://ada.lovelacesquare.org) is the conversational AI layer of the Lovelace ecosystem. She is designed for chemometrics and works across [The Library](https://library.lovelacesquare.org/) and [The Square](https://lovelacesquare.org/the-square), so a user can ask one question in plain language and continue from explanation into articles, code, datasets, formulas, and practical work without leaving the same conversation. Instead of relying only on a general model response, Ada is built to search the ecosystem and use the retrieved material as the basis of the answer. If you ask for an explanation of a method, she can connect the answer to a Library article. If you ask for a code implementation, she can search [The Square](https://lovelacesquare.org/the-square). If you ask for the next practical step after reading, she can continue from the same context rather than forcing you to start over somewhere else. This is the main difference between Ada and a generic chatbot. The goal is not simply to sound knowledgeable, but to keep the response attached to the actual resources of the ecosystem. That does not mean the system is infallible. Ada is still an LLM-based assistant, and answers should still be checked when accuracy matters. The difference is that the system is designed to reduce unsupported responses by grounding them in real platform content and by making the source material easier to inspect. The home screen below shows the basic interface users start from. The conversation begins in a standard chat view, and from there the session can stay in explanation mode or expand into more practical work when needed. Ada begins in a standard chat view connected to the Lovelace ecosystem. Click the image to open it fullscreen. ## What Ada can help with Ada is useful when the question is connected to chemometrics and to the resources of the Lovelace ecosystem. A conversation can begin with a conceptual question, continue into a specific article or code entry, and then move into practical work without changing tools. That matters because chemometric work rarely stops at a definition. A user often needs to understand a method, locate a reliable explanation, inspect an implementation, compare alternatives, and then test or adapt something in practice. On the retrieval side, Ada can help locate Library articles, code entries, datasets, and other ecosystem resources that match a question. She can also search scholarly metadata through OpenAlex and verify the bibliographic details of a paper before presenting it as a reference. On the explanatory side, she can unpack formulas, clarify methods, compare preprocessing choices, and rephrase difficult material at a more useful level. On the practical side, Ada can open the workspace and continue from discussion into construction. That includes importing source projects, building interactive components, executing Python, keeping saved snapshots, and preparing a code submission for review in The Square. Images and PDFs can be attached when they are useful evidence, and Ada can inspect the rendered workspace when a question depends on what is actually visible rather than on the source code alone. The important point is that these are not separate utilities presented as unrelated features. They belong to one continuous workflow. A user can begin by asking where to read about a method, then ask for code related to it, then ask Ada to open the workspace and build a small interactive teaching component or run a Python example. That continuity is one of the main reasons Ada is useful inside the Lovelace ecosystem. Ada can return concrete matches from The Square when the question is practical and resource-oriented. Click the image to open it fullscreen. Ada can open the workspace and continue from explanation into building interactive components or adapting imported project files. Click the image to open it fullscreen. Ada can also execute Python in the workspace, so a discussion can continue into scripts, outputs, and inspectable results. Click the image to open it fullscreen. ## How Ada works At a general level, Ada works through orchestration. That means there is a layer around the model that decides what kind of information the question requires and what actions are worth taking before an answer is returned. The response is therefore not meant to be a single model guess produced in isolation. If a request is mainly conceptual, the system may retrieve explanatory material. If it is resource-oriented, the system may search [The Library](https://library.lovelacesquare.org/) or [The Square](https://lovelacesquare.org/the-square). If it concerns a scholarly reference, Ada can search and verify OpenAlex metadata instead of inventing a title or DOI. If the first retrieval step is not enough, the system can continue searching, inspect what it has found, and only then compose a response. This orchestration is tied to Ada's tools. A tool is a concrete capability the system can invoke while answering. Ada uses tools to search the ecosystem, retrieve relevant material, inspect results, and use that material to support the answer. One question may call for a Library article, another for a code entry, another for a dataset, and another for a formula to be explained in context. The user does not need to choose a separate utility in advance. You can ask naturally, and Ada can decide which path is appropriate. Ada chat is the conversational layer where this process begins and where the context is carried forward. A user can start with a broad question, ask for a more precise explanation, move into a specific resource, and keep refining the discussion without resetting the session. That continuity matters because scientific work often develops step by step. The system can keep the thread of the conversation, which allows explanation, retrieval, and follow-up questions to remain connected. Ada workspace is the part of the system that extends the conversation into practical work. When a static answer is not enough, Ada can open the workspace so code, results, and discussion stay in the same flow. She can bring project files into that space, help build interactive components, execute Python, inspect the rendered result, and preserve useful stages as workspace snapshots. When the work is ready to be shared, Ada can also prepare a staged Square submission for the user to review and confirm. This is what allows the interaction to move from explanation into construction, inspection, execution, and contribution without forcing the user to leave the conversation and start over somewhere else. ## Built for chemometrics Ada is built specifically for chemometrics, which changes both the language of the interaction and the kind of help that is useful. The system is not meant to be a general-purpose assistant covering every topic equally. It is meant to work well in a domain where concepts, preprocessing decisions, formulas, code implementations, and datasets are closely connected. That specialization matters because many chemometric questions are not satisfied by a generic answer. A user may need to understand a method, see how it is implemented, compare it with another one, and then test it in a small example. Ada is useful because it is attached to an ecosystem built around those needs rather than to the open web in general. As the ecosystem grows, Ada's useful scope grows with it. New Library content, new code entries, and new datasets expand what the assistant can retrieve and connect. The assistant is therefore not defined only by the model behind it, but by the quality and structure of the chemometric material it can work with. ## What Ada is not Ada should be understood as a documentation and workflow assistant, not as an authority that replaces source checking. She is there to help users orient themselves, retrieve relevant material, and move more efficiently from question to practice. When a claim, a citation, a code path, or a result matters, the original source should still be inspected directly. That is not a weakness unique to Ada. It is the responsible way to use any LLM-based system in scientific work. The aim is not to eliminate judgment, but to support it with a better interface to the material that already exists in the ecosystem. # FAQs of The Library (/lovelaces-square/the-library/the-library-faqs) ### What is The Library? The Library is an educational and contextual resource that provides clear explanations of data analysis methods in chemometrics, including their theoretical foundations, historical context, and practical applications 📚. ### What type of content does The Library offer? The Library contains structured learning materials covering basic to advanced chemometric techniques, step-by-step guides, real-world case studies, and notes on common mistakes to help users avoid pitfalls. ### Who is The Library designed for? The Library is designed for both beginners and experienced users in chemometrics. Content is written with minimal jargon to make complex data analysis methods accessible to newcomers while still valuable for advanced practitioners. ### How is The Library content organized? Content is organized by topic and complexity level, with clear navigation between related concepts. Materials progress logically from basic principles to advanced applications within each subject area. ### How often is The Library updated? The Library is updated regularly through community contributions and user feedback, ensuring content remains current with evolving practices in chemometrics and data analysis. ### Can I contribute to The Library? Yes, The Library is community-driven and welcomes contributions. Content improvements, corrections, and new materials can be submitted to help expand and enhance the educational resources. ### Are there interactive learning elements? The Library includes interactive explanations and engaging content formats designed to help users actively learn and understand chemometric concepts rather than passively consume information. ### How does The Library support Ada and relate to The Square? The Library is part of Ada's knowledge context. Ada can use specialized search tools to find and present relevant content from The Library. Together with The Square's practical tools, The Library forms part of a mutually reinforcing ecosystem where educational content supports both human learning and ecosystem-grounded AI interactions. # The Library (/lovelaces-square/the-library/what-is-library) In the ancient city of Alexandria, on the shores of the Mediterranean Sea, stood one of humanity's greatest treasures: the Library of Alexandria. The Library of Alexandria was not just a building filled with scrolls and manuscripts; it was *a promise to future generations that knowledge would not be lost to time*. Scholars traveled from distant lands to study there, copying texts by hand and discussing ideas that would shape civilizations. They understood a fundamental truth: **Time fragments knowledge, and what is fragmented is easily forgotten**. Over the years, valuable analytical methods, algorithms, and approaches have been developed, yet some have become less accessible as researchers retire, publications become harder to find, technological formats change, or significant ideas are lost due to their complexity. Don't get me wrong on this last point: understanding a complex paper is, to some extent, our own responsibility. However, the well-known pressure of *'publish or perish'* deprives us of the high-quality time needed to properly understand concepts. This is yet another consequence of the fast-paced nature of academia today. I believe that in our community (**the chemometrics community**) as well as in the vast majority of others, there are two essential aspects to preserve: the scientific advancements, both old and new, and the social and historical context that shaped them. From a scientific perspective, I had to face the loss of scientific knowledge when I was studying the relationship between PCA and missing values as a student in 2023. I was investigating new ways to perform PCA in presence of missing values, and why NIPALS provided biased results in my simulations. Some researchers pointed that NIPALS provides biases due to floating and accumulative errors¹, but I was not able to replicate this phenomenon. Then, I found an interesting paper by Bjørn Grung and Rolf Manne from 1998 referencing some findings related to this issue²: > *"The problem of missing information in principal component analysis (PCA) **was discussed in depth for a special case by Christoffersson in his thesis in 1970**. Christoffersson, as a student of H. Wold, extended the NIPALS algorithm of his mentor to a method for finding the first principal component of a data matrix with missing values. This approach has been implemented with varying degrees of correctness in commercial chemometric software for the general PCA problem with missing values. Christoffersson also briefly discussed equations for a NIPALS algorithm for the case of two principal components. However, in many cases, these equations do not converge to the optimal solution."* I went straight to read Christoffersson's dissertation. However, I was unable to find a digital version. Grung confirmed by email that, unfortunately, it appears to exist only in physical format at some Norwegian universities. That felt sad. I truly believe that piece is important, but it's unlikely to be accessible. This experience made me realize that the loss of information doesn't always come in the form of dramatic destruction, like the burning of a great library. Sometimes, it's quieter: a thesis sitting unread on a forgotten shelf or an insight buried in a journal behind a paywall. That, I believe, is the most dangerous form of losing knowledge, because we don't even realize it's happening. And unless we actively work to preserve and revisit these foundations, we risk rebuilding what was already built, rediscovering what was already known, or worse, forgetting entirely what once pushed the boundaries of our understanding. On the other hand, from a social and personal perspective, chemometrics is not just about equations and algorithms; it is also about the people, the discussions, the beers in congress, the challenges that have shaped its development... I feel that, as an early-career researcher, the history of chemometrics, the pioneers who shaped our thinking, the evolution of our core concepts, and the successes and failures that guided our progress, is slowly fading and risks being forgotten. I genuinely enjoy hearing these '*old*' stories from my thesis directors (though I use '*old*' with some hesitation when referring to the 70s-90s). Stories about the birth of the **Colloquium Chemometricum Mediterraneum**, the **TRICAP**, the **CAC**, how researchers first met, how collaborations formed, and how our field, despite its small size, developed strong connections. These conversations remind me that behind every theory and algorithm, there is a network of people who contributed to shaping the discipline. Photo taken by Pieter Kroonenberg at TRICAP1997. Preserving the legacy of chemometrics requires a concerted effort to centralize and curate both our knowledge and experiences. This means not only archiving important papers and algorithms but also ensuring that they remain accessible and understandable for future generations. It involves fostering a culture where researchers actively document their methods, share data, and mentor young scientists. By doing so, we can build a more interconnected and resilient scientific community: one that values and safeguards its intellectual heritage while continuously adapting and evolving to meet future challenges. Just as the Library of Alexandria once stood as a beacon of knowledge, we must create modern repositories of chemometric wisdom. Digital archives, open-source software repositories, and collaborative platforms offer new opportunities for ensuring that valuable insights remain within reach. However, preservation alone is not enough. Knowledge must also be made accessible, comprehensible, and engaging for future researchers. This means developing user-friendly databases, encouraging interdisciplinary collaboration, and fostering an open dialogue where experienced scientists and newcomers can exchange ideas seamlessly. In essence, the responsibility of preserving and advancing chemometrics does not rest solely on institutions or digital platforms. It is a shared duty among all scientists. One of humanity's greatest strengths lies in its ability to gather knowledge, safeguard it, and pass it forward (**teach!**). This unique capability has allowed civilizations to evolve, building upon the discoveries of those who came before rather than constantly reinventing what was once known. I think this is what makes us humans. This shared duty, i.e., to preserve, curate and advance, is not abstract. It is a call to action, and **The Library** is **Lovelace's Square's** answer. Designed as a living, evolving resource, **The Library** aims to centralize the knowledge, tools, and stories that define chemometrics while empowering researchers to contribute, adapt, and innovate. It is not just a monument to the past, but also, and importantly, a scaffold for the future. **References** [1] Kevin Wright. The NIPALS algorithm. 2017. Last access 05/04/2025. https://cran.r-project.org/web/packages/nipals/vignettes/nipals_algorithm.html [2] Grung, B., & Manne, R. (1998). Missing values in principal component analysis. Chemometrics and Intelligent Laboratory Systems, 42(1-2), 125-139. ## So, what is The Library? The Library is one of the three pillars of Lovelace's Square, and it serves as a structured, accessible space where chemometric methods, algorithms, and concepts are clearly explained. It functions as a practical knowledge base, bridging the gap between theory and real-world application. The primary goal of the Library is straightforward yet essential: to gather the most frequently used chemometric tools and methods and provide clear, applied, and contextually rich explanations. ### Addressing modern challenges One of the significant challenges today in learning chemometrics is the fragmentation of available resources, often compounded by paywalls that restrict access to important textbooks, scientific articles, and tutorials. Available materials differ greatly in: Some emphasize theoretical aspects heavily with complex mathematics, while others offer overly simplified instructions lacking adequate explanations. Materials range from surface-level overviews to impenetrable technical papers, making it difficult for both students and researchers to find clear, consistent guidance. Quality content is often locked behind expensive paywalls, restricting access to important textbooks and scientific articles. Theory and practical application are rarely well-connected, leaving gaps between understanding and implementation. ### Our solution The Library addresses this fragmentation by consolidating essential tools and methods into one unified resource. Each explanation is directly linked to practical, interactive examples, enabling users to quickly translate learning into practice. Because even the most brilliant algorithms can't explain themselves (that's Ada's job 😄), **simplicity and visualization are at the heart of the Library's design**. Clear visuals and plain-language explanations turn "mystery math" into tools you can actually use. To see this in action, you can even explore a well-known preprocessing example right here: --- ## Navigating the Lovelace's Square ecosystem Beyond method-specific explanations, the Library provides practical guides to help users make the most of the entire Lovelace’s Square environment. These guides explain how to navigate among [The Library](https://library.lovelacesquare.org/), [The Square](https://lovelacesquare.org/the-square), and [Ada](/lovelaces-square/ada/what-is-ada), ensuring smooth transitions between learning, applying methods, and receiving intelligent support. You can access these guides from the Library’s left sidebar. **Essential Navigation Skills:** - **Using The Square:** Search, filter, and execute ready-to-use algorithms and explore datasets directly in the browser - **Learning with Ada:** Ask meaningful questions, request code examples, and follow personalized learning paths - **Combining learning and doing:** Best practices for moving smoothly between theoretical explanations and practical applications - **Responsible data analysis:** Guidance on ensuring reproducibility, promoting data fairness, and applying algorithms responsibly in both research and industry These resources empower users to navigate Lovelace's Square confidently, apply chemometric methods effectively, and continuously expand their knowledge through community engagement. --- ## Community-Driven growth and knowledge preservation The Library is a dynamic, community-supported project that evolves alongside advances in chemometrics. It actively encourages contributions from the community, inviting tutorials, simplified explanations, and content updates. This collaborative approach preserves valuable knowledge, preventing its loss due to technological changes, shifts in research trends, or the retirement of experienced professionals. By maintaining an active dialogue within the community, the Library ensures that both scientific advancements and the historical and practical context surrounding them remain accessible and meaningful for current and future generations. **Get Involved:** If you would like to contribute to the Library or collaborate on its continued development, please contact the Lovelace's Square team through the official website at [https://lovelacesquare.org](https://lovelacesquare.org) or reach out directly via contact@lovelacesquare.org. # Good coding and dataset storage practices (/lovelaces-square/the-square/good-practices) You know that feeling when you open the folder of a student after they've spent a few months in the lab… and all you see are files named `test1.mat`, `ramantest_powerlaser3.wdf`, `nirtest.raw`, `untitled2.m`, `new_script2_final.py`, and `copy_of_new_script_FINALdefinitely.txt`? Honestly, we can all agree this should be illegal (maybe even punished with jail time). Jokes aside😄, clean code and organized datasets also shows professionalism and respect for others. It proves that you care about quality and understand that teamwork (and science!) works better when we can actually read each other's functions and data. Lovelace's Square wants to build a repository of code and data that is *easy to use*, *well-organized*, and *powerful*. Since we are building a shared repository, and because this repository is already used by *Ada*, the ecosystem's conversational AI layer, and other automated tools, it's even more important to keep everything clear, clean, and consistent. Well-structured codes and datasets help Ada give better answers, and ensure that both people and machines can navigate the content without confusion. By following some guidelines, you'll not only make your fellow coders smile, but also help keep our shared space neat and useful. Importantly, although these guidelines are not mandatory, we strongly encourage you to follow them. Code and datasets that are well-written, well-documented, and easy to access are more likely to be reused and extended in other projects. This increases the impact of your work. You have probably chosen one piece of code over others simply because it was accessible and easy to understand, even if it wasn't perfect. That is the power of clarity and openness. --- ## General Principles Before going into details, it is important to understand some basic principles that will help us keep both code and datasets clear, useful, and easy to share. | Principle | Description | | :-------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Be consistent** | Use a consistent structure and naming style across your files. This makes it easier to navigate, reduces confusion, and improves collaboration. | | **Make it clear and readable** | Choose names that describe the content. Use clear function and variable names in code. In datasets, label columns properly and include units if needed. Avoid vague names like `x`, `temp`, or `data1`. | | **Stay organized** | Keep related items together. Use folders for different types of files (e.g. `raw/`, `processed/`, `scripts/`, `results/`). Name your files so others can understand what they are without opening them. | | **Document your work** | Always include a short explanation. For code, you can use comments and headers to describe what the function does and how to use it. For datasets, this information can be included in the `README.txt` or metadata file. | | **Be efficient, but not at the cost of clarity** | Write clean, simple code and avoid unnecessary complexity. For data, avoid large, unused files. Always balance performance with readability and usability. | | **Use open and common formats** | Save data in formats like `.txt`, `.csv`, or `.xlsx`. Write code using standard tools (e.g. MATLAB, Python, R) and avoid formats that are hard to open or require very special software. | | **Follow FAIR principles** | Add clear descriptions, use standard formats, and ensure others can use your work without needing to ask you directly. | The FAIR principles were created to improve how scientific data and software are managed and shared. They help ensure that research outputs are not only available, but also understandable, trustworthy, and ready to be reused even by people who didn't write them. FAIR stands for: - **Findable**: Others can find your work easily thanks to good naming, documentation, and metadata. - **Accessible**: Your code and data are available to others (open source, no hidden folders or password-protected ZIPs). - **Interoperable**: Your work should be usable with other tools, platforms, and programming languages. This means using open, standard, or widely supported formats (like `.csv`, `.py`, `.xml`, or `.m`) and writing code that follows common practices. - **Reusable**: Your code should be understandable, well-documented, and clearly licensed, so others can actually use it, not just today, but in future projects or by different teams. Don't be afraid to add comments, examples, or explanations. Clear code is kind code. FAIR principles help turn your one-time analysis into something bigger: a resource others can build on, verify, or even teach with. In addition, with the rapid rise of large language models and AI assistants, applying FAIR principles has become even more impactful: Well-structured documentation, clear metadata, and open formats not only make your work easier for people to find and reuse, but also increase its accessibility to AI systems. This means your contributions can reach wider audiences, support more applications, and have a greater long-term impact. ## General tips So with all that in mind, we've collected some key tips to help you write code that not only works well, but will still make sense after a few months. The examples in this documentation are in `MATLAB` programming, but they can be extended to other languages. ## Tips for codes ### Documentation Good documentation is one of the most valuable parts of a project. It helps others understand your code and makes it easier to reuse, share, and improve. --- #### Function headers We strongly recommend to use this template for your function headers. It gives users everything they need to understand how the function works. ```matlab function [output1, output2] = FunctionName(input1, input2) % FUNCTIONNAME Brief description of the function % % Authors: Your Name % Date Created: YYYY-MM-DD % License: Specify your license here % Version: Specify the version. % % Detailed function description: % Here's where you can really shine! Explain what your function does, % any algorithms it uses, and any quirks or special features. % % Args: % input1 (type): Description of the first input parameter. % input2 (type): Description of the second input parameter. % % Returns: % output1 (type): Description of the first output parameter. % output2 (type): Description of the second output parameter. % % Example: % [result1, result2] = FunctionName(arg1, arg2) % % See also: RELATEDFUNCTION1, RELATEDFUNCTION2 % Your brilliant code goes here end ``` --- #### README files Every project or main folder should include a `README.txt` file. It explains what the code does, how to use it, and how to install or run it. - myProject/ - README.txt - mainScript.m - functions/ - calculateSomething.m - plotResults.m --- ### Code style #### Indentation **Use consistent indentation**: Indentation makes your code easier to follow. Use 4 spaces per level. This helps show where blocks of code start and end. ```matlab if isValid result = processData(data); else result = []; end ``` ```matlab if isValid result = processData(data); else result = []; end ``` --- #### Line length **Keep lines under 75 characters**. Break long lines using `...` to improve readability. ```matlab summaryText = ['The analysis shows a strong correlation ' ... 'between variables.']; ``` ```matlab summaryText = 'The analysis shows a strong correlation between variables.'; ``` --- #### Spaces around operators **Add spaces** around operators to make your code easier to read. ```matlab result = (a + b) / 2; count = count + 1; ``` ```matlab result=(a+b)/2; count=count+1; ``` --- #### Use blank lines **Separate logical sections** with blank lines to improve clarity. ```matlab data = load('datafile.mat'); % Normalize the data normData = (data - mean(data)) / std(data); % Plot the result plot(normData); ``` ```matlab data = load('datafile.mat'); normData = (data - mean(data)) / std(data); plot(normData); ``` --- #### Align related assignments **Align similar lines** to make code easier to scan. ```matlab maxIterations = 100; % maximum number of iterations maxDepth = 20; % maximum depth minError = 0.01; % minimum error threshold ``` ```matlab maxIterations = 100; % maximum number of iterations maxDepth = 20; % maximum depth minError = 0.01; % minimum error threshold ``` --- #### Use parentheses to clarify logic **Use parentheses** in complex expressions to show order of operations clearly. ```matlab result = (a * b) + c - (sqrt(d) * e); ``` ```matlab result = a * b + c - sqrt(d) * e; ``` --- #### Commenting **Use comments** to explain what your code is doing, especially for complex logic. ```matlab % Normalize the input values normData = (data - mean(data)) / std(data); % Check for outliers isOutlier = normData > 3; ``` ```matlab normData = (data - mean(data)) / std(data); % do stuff isOutlier = normData > 3; % check ``` --- #### Descriptive names **Avoid using single letters or vague terms**. Use names that explain what the variable or function does. ```matlab velocity = 12.5; result = calculateMean(data); ``` ```matlab x = 12.5; r = calcM(data); ``` --- #### Functions and classes: UpperCamelCase **Start each word with a capital letter**. Do not use underscores. ```matlab MyAwesomeFunction() ChemometricAnalyzer() ``` ```matlab my_awesome_function() chemometric_analyzer() ``` --- #### Variables and properties: lowerCamelCase **Start with a lowercase letter**. Capitalize the first letter of each new word. ```matlab myVariable = 5; dataMatrix = rand(10); ``` ```matlab Myvariable = 5; datamatrix = rand(10); ``` --- #### Constants: UPPERCASE **Write constants in all caps** to show they are fixed values. ```matlab MAX_ITERATIONS = 100; DEFAULT_TIMEOUT = 30; ``` ```matlab maxIterations = 100; defaultTimeout = 30; ``` --- ### Functions and scripts Functions and scripts are the basic parts of your code. Here is how to use them well: **One function per file**. Each function should be in its own file. This makes it easier to find, understand, and update. - NormalizeData.m - CalculateMean.m - LoadAndPlot.m --- **Use functions instead of scripts**. Functions are more reliable because they keep their own variables. This avoids conflicts and makes the code cleaner and easier to test. ```matlab function result = CalculateMean(data) result = sum(data) / numel(data); end ``` ```matlab data = [1, 2, 3, 4]; result = sum(data) / numel(data); ``` --- **Use sections in long scripts**. If your script is long, break it into sections using `%%`. This helps you run or debug parts of the code without running everything at once. It also makes your script easier to read and follow. ```matlab %% Load Data data = load('datafile.mat'); %% Process Data normData = (data - mean(data)) / std(data); %% Plot Results plot(normData); ``` ```matlab data = load('datafile.mat'); normData = (data - mean(data)) / std(data); plot(normData); ``` --- **Group related functions in packages or toolboxes**. When functions are related, put them in the same folder or package. This keeps your project organized and makes it easier to reuse or share. - +preprocessing/ - normalizeData.m - scaleData.m - +analysis/ - calculateMean.m - calculatePCA.m --- ## Tips for datasets Datasets are as important as code. Here is how to prepare and store data so it is clear, reusable, and reliable. ### Organization Keep your data files in a clear folder structure. Group related files together and name folders meaningfully. - data/ - raw/ - sample1.csv - sample2.csv - processed/ - sample1_clean.csv - sample2_clean.csv - docs/ - metadata.json - README.txt --- ### Documentation & Metadata Every dataset should include clear documentation and metadata so users know what the data is, where it came from, and how to use it. At minimum, each data folder needs: 1. **README file** (e.g. `README.json`, `README.md` or `README.txt`) – An overview of the dataset: - Title and Brief description of the data - Source: Where the data originated (e.g. instrument, study, or publicly available repository) - Directory structure: What files or subfolders exist and what they contain - Usage: How to load or process the data, including any required software or dependencies - Contact: Who to reach if there are questions 2. **Metadata file** (e.g. metadata.json or metadata.txt) – A machine-readable record: - File names and column descriptions (with units) - Date collected and author/institution - Format and version of the dataset - License or usage terms If you're not used to working with `.json` files, you're not alone. Many chemometricians work mainly with `.mat` files, or `.csv`. But JSON is becoming the standard way to describe metadata **in a structured, machine-readable format**. It's widely supported in MATLAB, Python, R, and web tools, and it helps tools (and contributors) understand your data without needing to open it manually. JSON is especially helpful when sharing datasets across labs, storing column metadata, or writing pipelines that adapt automatically to new data. Example README.txt: ``` # Infrared Spectra Dataset ## Description This dataset contains raw and processed infrared spectra of chemical samples, collected using Fourier-transform infrared (FTIR) spectroscopy. It was created as part of a study on solvent concentration and spectral variation in analytical chemistry. ## Source - Instrument: FTIR Model X - Location: Lovelace Lab, Lovelace University - Date Collected: March 2025 - Experiment: Solvent concentration and spectral profile study ## Authors - Dr. Ada R. Lovelace (ada.lovelace@lovelacesquare.org) - Clara Babbage - Team Lovelace's Square – Spectroscopy & Chemometrics Unit ## Publication If you use this dataset, please cite: A. R. Lovelace, C. Babbage. "Spectral Response to Solvent Concentration in FTIR." Journal of Chemometric Data, 2025. DOI: doi.org/10.1242/jeb.025361 ## Structure data/ ├── raw/ │ ├── sample1.csv # unprocessed FTIR spectra │ └── sample2.csv ├── processed/ │ ├── sample1_clean.csv # baseline corrected, smoothed │ └── sample2_clean.csv └── docs/ ├── README.md └── metadata.json ## Format - Files are in .csv format with two columns: - wavenumber (cm⁻¹) - intensity (absorbance) ## Usage raw = readtable('data/raw/sample1.csv'); clean = readtable('data/processed/sample1_clean.csv'); plot(raw.wavenumber, raw.intensity); ## License This dataset is licensed under CC BY 4.0 – you are free to use, modify, and share it, as long as you give proper credit. ## Contact Questions? Email lab@lovelacesquare.org ``` Example metadata.json: ``` metadata_json = """{ "sample1.csv": { "date_collected": "2025-03-10", "instrument": "FTIR Model X", "location": "Lovelace Lab, Lovelace University", "experiment": "Solvent concentration and spectral profile study", "columns": { "wavenumber": "cm^-1", "intensity": "absorbance" }, "notes": "Raw spectral data collected without preprocessing." }, "sample1_clean.csv": { "date_processed": "2025-03-12", "processing_steps": [ "baseline correction", "smoothing", "normalization" ], "software": "MATLAB R2025a", "columns": { "wavenumber": "cm^-1", "intensity": "absorbance (processed)" }, "notes": "Processed version of sample1.csv ready for analysis." } } ``` --- ### FAIR for Data Apply FAIR principles to your datasets: - **Findable**: Add clear names, README, and metadata. - **Accessible**: Use open formats and avoid paywalled locations. - **Interoperable**: Follow common standards (e.g. CSV, JSON, .py, .m). - **Reusable**: Include license information and usage examples. ## Let's keep Lovelace's Square neat and useful! Writing clean code and keeping data organized isn't just about following rules. It shows respect to your team, helps you find your work easily later, and encourages others to build on what you've done. When we all do our part, Lovelace's Square becomes a space where great science happens easily. Clear code and organized datasets make everyone happier. Let's make our shared workspace a place we're proud of! # How to cite Lovelace's Square resources (/lovelaces-square/the-square/how-to-cite) In science, proper citation is not just a formality; it is a fundamental expression of respect and integrity. Recognition is the currency of scientific work. When we cite others' contributions, we acknowledge the effort, creativity, and expertise that went into developing methods, writing code, or generating datasets. Failing to cite properly not only disrespects the original authors but also undermines the collaborative nature of scientific progress. This is why, at **Lovelace's Square**, we take documentation and attribution very seriously. We aim to ensure that credit goes where it deserves and to contribute to a culture of fairness, openness, and transparency in research. --- ## Some considerations It often happens that someone develops an idea, invests significant effort into it, and publishes it in a peer-reviewed journal. Later, another person implements that idea in code and shares it publicly. This latter contribution also requires substantial effort and deserves recognition. When citing code implementations, it is important to give proper credit to the authors who developed the code. However, this should **not** replace citing the original method or idea. The code author is sharing an implementation of someone else’s concept, so the original authors must be acknowledged first. Therefore, we distinguish between two different types of contributions: 1. **The proposal of a novel method or idea**, and 2. **The implementation of that method in code**. Both contributions are valuable, but the second should never overshadow the first. This principle should be made clear in all code documentation and in any resource shared within **Lovelace's Square**. To clarify this distinction, we provide below a set of guidelines on how to properly cite resources from Lovelace's Square. --- ## I want to cite a resource from Lovelace's Square and I am not the author When you use code, methods, or datasets from Lovelace's Square, the first and most important step is to **always check the author's instructions**. If the documentation of the code is properly done, the author should have provided clear guidance on how to cite their work. **Please respect those instructions.** In case there are no citation instructions provided, we recommend following these rules: --- ### Citing novel methods available at Lovelace's Square If the author of the code, toolbox, or resource has built a **novel method** and makes it available at Lovelace's Square, please check if there is a reference to the method in a **peer-reviewed journal**. This is the most important citation and should always take priority. The original scientific publication that describes the method is the primary source of credit. #### Example: citing a novel method Imagine you use a new spectral analysis technique called *Adaptive Noise Filtering (ANF)*, developed and published by its authors in 2024 and shared at Lovelace’s Square. **Step 1:** Check the code documentation. The author provides a citation to their original paper. > Smith, L., & Zhao, T. (2024). Adaptive Noise Filtering for Nonlinear Spectral Denoising. *Journal of Analytical Algorithms*, 12(3), 155–170. > DOI: 10.1234/jaa.2024.155 **Step 2:** Cite the paper in your work, since it presents the novel method: > “Data were denoised using the Adaptive Noise Filtering (ANF) method proposed by Smith and Zhao [5].” **Step 3:** Optionally, mention the code source from Lovelace’s Square for reproducibility: > “The implementation of the ANF method used in this study is available at Lovelace’s Square [Smith, L., & Zhao, T., 2024. *Adaptive Noise Filtering (ANF)*. Lovelace’s Square. Last accessed: January 15, 2025].” --- ### Citing existing (non-novel) methods available at Lovelace's Square and I am not the author If the author of the code, toolbox, or resource published it but it is **not a novel method** (for example, it implements a widely used or well-known approach), you should first cite the **original peer-reviewed publication** that describes the method, and then, the code. The authors of the method deserves credit but so does the author who implemented it in code. You can cite it as follows: > "The [title of the code] code we have used is available at Lovelace's Square [Author(s), Year. *Title of the code*. Lovelace's Square. Last accessed: Date] in the corresponding section of the paper, such as *Methods* or *Acknowledgements*." Lovelace's Square provides DOIs for code implementations through Zenodo. If a DOI is available for the code you are using, please include it in your citation. #### Example: citing a non-novel method Imagine you used an implementation of the asymmetric least squares method for baseline correction, available at Lovelace's Square, shared by a user but **not** by the original authors of the method. **Step 1:** Check the code documentation. The author indicates that the method was originally published by Eilers & Boelens (2005). **Step 2:** Cite the original peer-reviewed paper first: > Eilers, P. H. C., & Boelens, H. F. M. (2005). Baseline correction with asymmetric least squares smoothing. *Leiden University Medical Centre Report*, 1(1), 5. **Step 3:** Mention the code availability. Prioritize if it exists in repositories with DOIs (e.g., Zenodo, Figshare). If not, cite the Lovelace's Square resource as follows: > “The asymmetric least squares baseline correction code we have used is available at Lovelace's Square [García, M., 2024. *ALS baseline correction*. Lovelace's Square. Last accessed: January 15, 2025].” --- ## Citing modified or derived implementations If you adapt or extend someone else’s code, cite **both** the original implementation and your modified version. Make your modifications explicit in your documentation and clarify how they differ from the original work. This ensures transparency, respects prior contributions, and prevents confusion about authorship or originality. #### Example Suppose you modify the ALS baseline correction code to add automatic parameter tuning. In that case, you should cite: 1. The original paper describing the ALS method (Eilers & Boelens, 2005) 2. The code implementation you built upon (e.g., García, 2024, Lovelace’s Square) 3. Your own modified version, if shared publicly, with a note clarifying that it extends García’s implementation. --- ## I am the author of a resource available at Lovelace's Square and I want to make it citable As an author contributing to Lovelace's Square, it is your responsibility to provide clear citation instructions for your work. Here are some guidelines to help you make your resource citable: 1. **Cite the original method**: If your code implements a method or idea from a peer-reviewed publication, always include a citation to that original work in your documentation. 2. **Provide citation instructions**: In your code documentation (README file, docstrings, etc.), include a section on how to cite your work. Specify whether users should cite the original method, your implementation, or both. 3. **Use standard formats**: Provide citation information in standard formats (e.g., APA, MLA, BibTeX) to make it easy for users to include in their references. 4. **Use persistent identifiers**: Whenever possible, include persistent identifiers (DOI, ORCID, ROR) for your project and contributors. 5. **Be clear about contributions**: Clearly distinguish between the original method and your implementation to avoid confusion among users. 6. **Encourage transparency**: Document versions, dependencies, and any data used for testing your implementation. #### Example citation section for a README file ```markdown If you use this code, please cite both the original method and this implementation: Eilers, P. H. C., & Boelens, H. F. M. (2005). Baseline correction with asymmetric least squares smoothing. Leiden University Medical Centre Report, 1(1), 5. Implementation: García, M. (2024). ALS baseline correction. Lovelace’s Square. https://lovelacesquare.org/resources/als-baseline ``` --- ## Promoting fairness and inclusion Lovelace’s Square is founded on the belief that open science thrives when recognition is shared fairly and transparently. We encourage all contributors and users to uphold the principles of equity, respect, and inclusivity in every collaboration. Many of the advances shared on Lovelace’s Square come from the collective effort of researchers, students, developers, and enthusiasts from diverse backgrounds. We therefore ask all users to actively acknowledge contributions from **early-career scientists, students, independent developers, and members of underrepresented communities** whose work might otherwise be overlooked. Open Science is not only about sharing knowledge freely; it is also about ensuring that everyone has the opportunity to be seen, heard, and credited for their contributions. # Information for contributors (/lovelaces-square/the-square/information-for-contributors) We are excited that you want to contribute to Lovelace's Square! 🥳 Please read the following information carefully. It will guide you through everything you need to know to prepare and submit your work. We've made the process as smooth as possible, and we're here to support you at every step. Before submitting your contribution, please make sure to follow our platform's guidelines on good practices. These will help you prepare high-quality, reusable content for the community. You can find the full guidelines here: [Good Practices](/lovelaces-square/the-square/good-practices/) This ensures your submission is complete, clearly documented, and ready for review. ## Core requirements At Lovelace’s Square, we strongly encourage all contributions to follow these essential requirements, to ensure quality, clarity, and reproducibility for the entire community. All code and datasets must use recognized open-source or open-data licenses that allow use, modification, and redistribution for any purpose. Non-commercial, no-derivatives, proprietary, research-only, and academic-only terms are not accepted. Every contribution needs proper documentation explaining what it does, how to use it, and any requirements or dependencies. Ensure your datasets are clean and free of sensitive information. Remove any personal identifiers or private data before submission. You must have the rights to share everything in your submission. Get permission from co-authors if needed and ensure license compatibility. ## Contributing algorithms and tools ### What you can contribute We welcome all kinds of content related to chemometrics. If it helps others explore, clean, model, explain, or teach chemical data, it belongs in The Square. You can contribute many different things such as: | Category | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------- | | **Algorithms and models** | For dimensionality reduction, regression, classification, clustering, calibration, variable selection, etc. | | **Preprocessing techniques** | Such as baseline correction, smoothing, normalization, centering, scaling, or signal alignment among others. | | **Visualization tools** | For plotting spectra, score plots, loadings, heatmaps, or interactive displays. | | **Exploratory tools** | To help reveal patterns, trends, or groupings in complex data. | | **Validation and evaluation tools** | Like cross-validation scripts, residual analysis, performance metrics, or model comparison. | | **Helper functions and utilities** | For data cleaning, transformation, formatting, or organizing workflows. | | **Teaching and learning resources** | Examples, templates, notebooks, or small demos to support study and training. | Code can be in any programming language: *Python*, *R*, *MATLAB*, *Julia*, or others. ### Making your code reusable To help make your contribution useful and reusable for others, we recommend following a few simple coding practices. These steps will improve clarity, avoid system-specific issues, and make it easier for users to test, adapt, and build upon your work. 1. **Structure your code clearly**: Break your code into functions or classes instead of writing everything in one long script. This makes your code easier to read, test, and reuse. A well-structured script also helps others understand what each part does and where to make changes if needed. 2. **Avoid hard-coding**: Don’t write file paths, parameter values, or system-specific settings directly into the code. Instead, let users input their own data and options through arguments, configuration files, or prompts. This allows your code to run correctly on different computers and with different datasets. 3. **Include examples**: Add sample data, demo files, or ready-to-run scripts that show how your code works in practice. Providing a typical use case with default settings makes it much easier for others to get started and understand how your method can be applied. 4. **List requirements**: Make a clear list of all external libraries, packages, or toolboxes your code depends on. This could be a `requirements.txt` file or a `README` section. Listing dependencies ensures users can set up their environment without guessing or running into errors. ### Documentation requirements When you upload your code to The Square, you will need to fill out two description boxes. This information helps others understand and use your work, and it also supports our review and indexing process. **Short Description:** Write a concise line summarizing what your code does. Keep it brief for easy analysis by our system. > *Example:* > `EMSC algorithm for correcting multiplicative/additive effects in spectral data.` **Extended Description:** This is where you explain your code in more detail. Try to include: - *Purpose*: What problem does the code solve? - *Algorithm or method*: What technique is used? - *How it works*: Main logic or approach used in the script. - *How to use it*: Basic instructions or required inputs. > *Example:* > `The EMSC (Extended Multiplicative Signal Correction) function corrects spectral data by removing multiplicative scaling, additive offsets, and baseline variations. It fits a reference spectrum (e.g., mean/median) to each input spectrum using a polynomial baseline model, estimating scaling (a), offset (b), and baseline coefficients.` We know the extended description field can feel a bit restrictive in terms of space. This limit is intentional; it helps Ada, the ecosystem's conversational AI layer, work more efficiently when guiding users. We truly appreciate the effort you make to squeeze your explanation into a compact but meaningful format. Include a detailed `README` file or clear comments within your code, with no limit on length. The goal is for someone to download and understand your code without needing to contact you. ### Code hosting We **strongly recommend hosting your code on GitHub** (or a similar platform like GitLab or Bitbucket). A repository makes your contribution easier to maintain, version, and cite, and it lets the community open issues, suggest improvements, and track changes over time. During submission, you can provide the repository URL directly. If you prefer not to use a repository, you can upload your code as a *ZIP file* instead. In either case, your contribution should include: - All source code files - Documentation (README file with clear instructions for use) - Example usage *Example Structure:* - EMSC_toolbox/ - EMSC_correction.m # Main function - README.md # Documentation - example_usage.m # Usage example ## Contributing datasets ### What you can contribute We welcome any dataset related to the data-analysis field that can benefit the community; useful for benchmarking, teaching, testing, or research purposes. | Category | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------- | | **Validation** | Datasets for checking and validating new algorithms or workflows | | **Benchmarking** | Standardized sets used to compare performance across multiple methods | | **Performance Evaluation** | Real-world or synthetic data aimed at stress-testing scalability, speed, robustness... | | **Simulation Data** | Synthetic or modeled data for exploring hypothetical scenarios or method behavior | | **Domain-Specific Collections**| Datasets related to specific fields (e.g., environmental sensors, bioinformatics, process control) | | **Educational Challenges** | Competition datasets designed to engage students in solving specific problems | ### Data preparation guidelines Before sharing your dataset on **The Square**, please prepare it carefully so others can immediately understand and use it. 1. **Clean format** Organize your data with clear, consistent column headers and remove errors or corrupt entries. CSV is preferred for maximum compatibility. 2. **Rich metadata** Provide a data dictionary: describe how measurements were taken, what each variable means, units, timestamps, and any preprocessing steps applied. 3. **Privacy protection** Strip out any personal or confidential information. Anonymize sample IDs and ensure compliance with relevant data-protection regulations. ### Data hosting requirements To ensure long-term accessibility, it is strongly recommended to supply a public download link hosted on a stable service. Recommended platforms include: - *Zenodo* (provides a DOI for citation, strongly preferred by Lovelace's Square Team) - *Institutional repository* - *Google Drive* or *Dropbox* (acceptable alternatives, but verify public-access settings) Before submitting, open your download link in a private/incognito window to confirm it’s accessible without permissions. Lovelace's Square can consider to re-upload the dataset to other platforms (such as Zenodo) to guarantee long term access. ## Submission process 1. **Visit the submission page** Go to [Lovelace's Square](https://lovelacesquare.org) and navigate to "The Square" section. Choose either: - *"List your code"* for algorithm contributions - *"List your dataset"* for data contributions 2. **Fill in contributor details** - *Authors*: Your name and any co-contributors - *Affiliation*: Your institution or organization (required) - *Email*: Contact email for communication (not shown publicly) 3. **Provide title and descriptions** - *Name*: Clear, concise title for your contribution - *Short Description*: One or two sentences summarizing the content - *Extended Description*: Detailed explanation with usage notes 4. **Specify technical details** - *License*: Choose an appropriate open license - *Keywords*: Add relevant tags for discoverability - *Version*: Label your submission (e.g., v1.0) - *Categories*: Select all that apply - *Programming Language* for code or *Download Link* for datasets - *GitHub Link*: If your code is hosted on GitHub, you can provide the repository URL in the corresponding field 5. **Upload and submit** - *For code*: Provide your GitHub repository URL, or upload a ZIP file if you don't use a repository - *For datasets*: Provide the external download URL - Agree to our Terms of Use and Privacy Policy - Click Submit! ## Review and publication process ### What happens next Our team verifies your submission is complete, appropriate, and clearly documented. We check for prohibited content and ensure files work correctly. If we find issues or need clarification, we'll contact you at your provided email. Common requests include missing files, unclear descriptions, or license questions. Once approved, your contribution becomes publicly visible in The Square with your name, description, and download links clearly displayed. You can submit updated versions in the future. We support versioning to keep contributions current and useful. We aim to review submissions quickly, usually within a few days. The review ensures quality and protects both contributors and users who download content. ## Why your contribution matters By contributing to Lovelace's Square, you are: - *Helping the community* by sharing knowledge and tools that save others time - *Advancing open science* by making chemometric methods more accessible - *Supporting education* by providing resources for students and newcomers - *Building your reputation* as a contributor to open science Remember that even small contributions can have big impact. A simple script or small dataset can save someone hours of work or help a student understand a concept. We welcome contributors of all experience levels. Thank you for considering contributing to **Lovelace's Square**. We look forward to seeing your algorithms and datasets become part of this growing library of chemometric resources. Together, we push the boundaries of open science in chemometrics. **Happy contributing!** 🚀 # The Square – FAQs (/lovelaces-square/the-square/the-square-faqs) ### What is The Square? The Square is the core of Lovelace’s Square💡. It’s a hub of open-source code, datasets, and tools made for chemometrics and chemistry. ### Why is it called “The Square”? Because it’s like a lively town square: an open place where everyone can meet, share, and build. No secret passwords, just plenty of data and good vibes. ### Who can use The Square? Anyone! Students, researchers, industry labs, or curious hobbyists. If you’re dealing with chemical data, you’re welcome (lab coat optional 🧑‍🔬). ### Is The Square really free? Yes. All resources are freely available under recognized open-source or open-data licenses that allow use, modification, and redistribution for any purpose. No hidden fees, no trial periods. ### What resources can I find here? Resources provided by the community include preprocessing tools, data analysis methods, and real benchmark datasets. Whenever possible, these resources are available in Python, R, MATLAB, Julia, and others. ### How can I access resources in The Square? It’s all online. Just visit: [lovelacesquare.org/the-square](https://lovelacesquare.org/the-square). ### Do I need to install any software? Nope. Just download the code or dataset you want and that's all. ### How do I cite code or data from The Square? Check the author's citation preferences. Each resource should include a *Cite Me* section or similar with the corresponding reference. Just follow the author's instructions and you're good to go ✍️. ### How does The Square promote open science and reproducibility? All resources include clear metadata and links to the original papers 🧪, so others can check or repeat your work. ### Can I contribute my own code or dataset? Absolutely! Head to the [Contributors](https://library.lovelacesquare.org/lovelaces-square/the-square/information-for-contributors/) page for guidelines. A friendly maintainer will guide you through a quick review process. ### What license should I use? Use a recognized open-source or open-data license that keeps the contribution reusable by everyone, including academic, educational, nonprofit, and commercial users. We recommend MIT, Apache-2.0, GPL, AGPL, EUPL, or BSD for code, and CC BY 4.0, CC BY-SA 4.0, CC0, ODbL, or PDDL for datasets. Licenses with non-commercial, no-derivatives, proprietary, research-only, or academic-only restrictions are not accepted. ### How long does review take? Usually a few days. We run basic tests, skim the docs, and, if all looks tasty, merge it. Urgent bug fixes are fast-tracked. ### Who maintains The Square? A small volunteer team of chemometricians, coders, and coffee-powered reviewers ☕. Major hosting costs are covered by the Lovelace’s Square non-profit. ### How can I support the project? Spread the word, cite us, contribute code, or buy our team a virtual coffee. Every little bit keeps The Square humming. # So, what is The Square? (/lovelaces-square/the-square/what-is-the-square) Imagine a busy town square where scientists, students, professors and data lovers meet to share their best ideas and tools, just like sharing a cup of coffee. [**The Square**](https://lovelacesquare.org/the-square) is our online version of that kind of place. It is a friendly space where people from around the world come together to exchange code, datasets, and smart ways to work with chemical data. Everything is free and open, so you can explore, use, or even improve what others have shared. In simple terms, **The Square** is the central repository of [**Lovelace's Square**](https://lovelacesquare.org/), one of the platform's three main pillars, alongside [**The Library**](https://library.lovelacesquare.org/) and [**Ada**](/lovelaces-square/ada/what-is-ada). It is the platform's technical core, focused on the *doing* part of data analysis. ## Why The Square exists We know the struggle. For years, many powerful chemometric methods were locked away in expensive proprietary software, far out of reach for students or labs on a tight budget. New techniques often appear in scattered code repositories with little documentation, which makes them hard to learn or trust. The Square removes those barriers and brings everything into one open, easy-to-access space. We call it *"The Square"* because we imagined it like a public square: a place where people gather to share, collaborate, and innovate. We are not promising to change the world overnight, but we do hope to make life a little easier (and maybe more fun) for anyone working with chemical data. So bring your curiosity, maybe a cup of coffee, and come join us in [**The Square**](https://lovelacesquare.org/the-square) 😄. ## What The Square offers At Lovelace's Square, you can find community-contributed code and datasets, all freely available under recognized open-source or open-data licenses. Code must use licenses such as MIT, Apache-2.0, GPL, AGPL, EUPL, or BSD; datasets should use licenses such as CC BY 4.0, CC BY-SA 4.0, CC0, ODbL, or PDDL. Licenses with non-commercial, no-derivatives, proprietary, research-only, or academic-only restrictions are not accepted. This means you are free to explore the code, use it in your own projects, adapt it to your needs, and share it with others. Nothing is hidden or locked away. Each method and dataset includes clear explanations, practical examples, and scientific references, making the platform not only easy to use but also reliable and reproducible for real scientific work. All the core chemometric methods in one place. From classics like PCA and PLS to newer techniques and real datasets for practice. No more searching across the internet. Every tool and dataset is freely available under open licenses (MIT encouraged). You can read the code, use it, change it, and share it without worries. The Square runs in your browser with no installation, no registration, and no paywalls. Just click and start exploring. Each method or dataset comes with clear documentation and scientific references. Nothing is a black box. ## Open Science and Community The Square is built on two important ideas: open science and working together as a community. We believe science should be open to everyone. When tools and data are free to use, research becomes easier to repeat, improve, and share. This helps science grow faster and become more fair. But open science only works when people take part. That is why The Square grows with help from its community. If you are a researcher with a new method or a student who found a useful dataset, your contribution is welcome and valuable. We have clear rules and a helpful review team to make sure each submission is easy to understand and ready to use. **But you do not need to be perfect**: If everyone waits until their work is perfect, good ideas may never be shared🫂. All great ideas often begin as a messy vision; they become clearer step by step. Just share what you have learned or created. It will grow and improve with time. ## Quick Start Here you can find a quick explanation of how to navigate through The Square. 1. **Open the homepage** Go to https://lovelacesquare.org. ![Homepage overview](/assets/ScreenshotHome.png) Here, you can access several sections: - *Search bar* – type a method, keyword, or dataset (e.g. "PCA"), then hit *Enter*. - *Explore The Square* button – jump straight to every public tool and dataset. - *Top menu* – quick links to useful info, such as the roadmap, sponsors, and the three main areas of the site. 2. **Browse The Square** Click *Explore The Square* banner or visit https://lovelacesquare.org/the-square. ![The Square grid](/assets/ScreenshotTheSquare.png) In **The Square** you can: - *Filter panel* – narrow results by category, language, or content type. - *Tool cards* – each card shows a summary, license, and last-updated date. - *Download icon* – get the code or dataset. 3. **Upload code or data** Before submitting, please read the following pages carefully: - [Information for Contributors](https://library.lovelacesquare.org/lovelaces-square/the-square/information-for-contributors/) - Complete submission guide covering licensing, documentation requirements, and the review process - [Good Practices](https://library.lovelacesquare.org/lovelaces-square/the-square/good-practices/) - Coding standards, file organization, and FAIR principles for quality contributions After reading the documentation, you can access the upload forms: 4. **For Code Contributions:** - Visit [Upload your code](https://lovelacesquare.org/the-square/upload-your-code) - Fill out the detailed form with: - Authors, email, and affiliation - Code name and descriptions (short and extended) - License and keywords - Simply drag and drop your ZIP files or click to upload. Your ZIP should contain all code files, documentation, and examples - Confirm compliance with Terms of Use and Privacy Policy, then submit 5. **For Dataset Contributions:** - Visit [List your datasets](https://lovelacesquare.org/the-square/upload-your-data) - Fill out the detailed form with: - Authors, email, and affiliation - Dataset name and descriptions (short and extended) - License and keywords - Download link (upload data to Zenodo, Figshare, etc. first) - Confirm compliance with Terms of Use and Privacy Policy, then submit Our team reviews every entry for clarity, licensing, and reproducibility before it goes live! ## Ready to explore? In the end, The Square is here to make useful chemometric tools and data available to everyone. We are not promising to change the world overnight, but we do hope to make life a little easier (and maybe more fun) for anyone working with chemical data. So bring your curiosity, maybe a cup of coffee, and come join us in The Square☕️ # Cluster Analysis (/introduction-to-chemometrics/models/exploratory-models/clustering) You have measured two hundred olive oils, or a week of tablet spectra from a production line, or a drawer full of unknown mineral samples. Nobody has told you which samples belong together. **Cluster analysis** asks the data themselves: are there natural groups here, and which samples belong to each? Grouping things by their similarities is as old as science, but the algorithmic discipline of clustering grew up in mid-twentieth-century biology. In 1963, the entomologist **Robert R. Sokal** and the microbiologist **Peter H. A. Sneath** published *Principles of Numerical Taxonomy* . They proposed classifying organisms not by expert judgement or assumed evolutionary history, but by measuring many observable traits and letting an algorithm group organisms by their overall similarity. Many taxonomists objected, but the toolbox they built (distance matrices, linkage rules and dendrograms) proved useful far beyond biology, and it is still the backbone of hierarchical clustering. The other thread came from engineering and statistics. In 1957, **Stuart Lloyd** at Bell Labs devised a method for quantising signals in pulse-code modulation: representing a continuous signal by a few discrete levels. His procedure alternated between assigning each value to its nearest level and moving each level to the mean of the values assigned to it. The report circulated inside Bell Labs and was only published in 1982 . Closely related ideas were described by **Hugo Steinhaus** in 1956 , and in 1967 **James MacQueen** gave the method its name, **k-means**, and studied its properties . The batch version used today, and in the demo below, is usually called **Lloyd's algorithm**; MacQueen's original moved a centroid after each sample was assigned. A few years earlier, in 1963, **Joe H. Ward Jr.** had published a hierarchical method that merges groups so as to keep them as compact as possible . In analytical chemistry, clustering was taken up as soon as computers became part of the laboratory; by 1983 **Massart and Kaufman** had written a whole book on interpreting analytical data with cluster analysis . Today it is a standard exploratory tool, usually applied after spectral preprocessing and often on PCA scores. ## Why group samples? In many chemical problems there are no labels to start with, only measurements and a question: **are there natural groups in these data?** A few typical situations: - **Origin.** Two hundred olive oils from five regions. Before building any classifier, you want to know whether the spectra separate naturally, or whether some regions overlap so much that classification will be hard. - **Process monitoring.** A tablet spectrum every 30 seconds for a week, and something drifted. Clustering can show when a new group appeared, and whether it was a sudden jump or a gradual shift. - **Library matching.** Five hundred unknown Raman spectra and a library of ten thousand references. Clustering the unknowns first means you only need to match one representative per group, and then check the members of a group individually only when the match is ambiguous. - **Unusual samples.** If most samples fall into a few tight groups and three sit far from all of them, those three deserve a closer look. In all these cases clustering is **exploratory**: it looks for structure, it does not test a hypothesis. ## Measuring similarity Before grouping samples, we need to say how similar two samples are. The choice of distance shapes the clusters you find . ### Euclidean distance The ordinary straight-line distance. For two samples $\mathbf{x}$ and $\mathbf{y}$ measured on $p$ variables: $d_E(\mathbf{x}, \mathbf{y}) = \sqrt{\sum_{j=1}^{p} (x_j - y_j)^2}$ It treats all variables equally and works well when the variables are on comparable scales and the groups are roughly round. ### Manhattan distance Also called city-block or $L_1$ distance, it adds absolute differences, like walking along the streets of a grid: $d_M(\mathbf{x}, \mathbf{y}) = \sum_{j=1}^{p} |x_j - y_j|$ Because the differences are not squared, a single very large difference counts less than in the Euclidean distance. ### Mahalanobis distance $d_{\text{Mah}}(\mathbf{x}, \mathbf{y}) = \sqrt{(\mathbf{x} - \mathbf{y})^T \mathbf{S}^{-1} (\mathbf{x} - \mathbf{y})}$ where $\mathbf{S}$ is the covariance matrix of the variables. It accounts for correlations and different variances, so a difference along a direction in which the data naturally vary a lot counts less. It needs a reliable estimate of $\mathbf{S}$ and its inverse, which is impossible with more variables than samples, so in spectroscopy it is used on PCA scores rather than on raw spectra. In clustering there is a catch. If $\mathbf{S}$ is the covariance of all samples together, the separation between the groups itself inflates the variance along the direction that separates them, and the Mahalanobis distance then shrinks exactly the differences you are looking for. Ideally $\mathbf{S}$ would be the pooled within-group covariance (the covariance inside each group, averaged over the groups), but that is unknown until the groups are found. This is why the Mahalanobis distance is used less in clustering than in classification, where the groups are known in advance. ### Spectral angle For spectra, the angle between two spectra seen as vectors is often useful: $\theta(\mathbf{x}, \mathbf{y}) = \arccos \left( \frac{\mathbf{x} \cdot \mathbf{y}}{\|\mathbf{x}\| \, \|\mathbf{y}\|} \right)$ It ignores overall intensity: two spectra with the same shape but different intensities, for example because of different path lengths, have an angle of zero. Use it when shape matters more than size. Its close relative, the correlation coefficient between two spectra, is the same idea applied after centring each spectrum. For most chemometric work, start with the **Euclidean distance on preprocessed data**, usually on PCA scores. If the variables have very different units, autoscale first; if they are all absorbances of one spectrum, only centre. If shape matters more than intensity, try the spectral angle or the correlation. Use the Mahalanobis distance only where the covariance can be estimated reliably. ## Hierarchical clustering **Hierarchical clustering** builds a tree of nested groups. The usual variant is **agglomerative** (bottom-up): every sample starts as its own cluster, and the two closest clusters are merged again and again until only one remains. 1. **Start with one cluster per sample** With $n$ samples there are $n$ clusters of size one. 2. **Compute the distances** Calculate the distance between every pair of clusters. For single samples, this is just the distance between the samples. 3. **Merge the closest pair** Join the two clusters that are closest into one. 4. **Update the distances** Compute the distance between the new cluster and every other cluster, using the chosen **linkage** rule. 5. **Repeat** Go back to step 3 until all samples are in one cluster. The result is drawn as a **dendrogram**, a tree whose branches join at the height (distance) at which the merge happened. Read it from the bottom: samples that join low down are very similar; samples that only join near the top are very different. ### Linkage: distance between groups The key question in step 4 is how to measure the distance between two *groups* of samples. Each answer gives a different method. **Single linkage** (nearest neighbour) uses the closest pair of members: $d(A, B) = \min_{i \in A,\, j \in B} d(i, j)$ It can follow long, irregular groups, but it suffers from **chaining**: a few intermediate samples act as stepping stones and merge two distinct groups early. **Complete linkage** (farthest neighbour) uses the most distant pair: $d(A, B) = \max_{i \in A,\, j \in B} d(i, j)$ It produces compact groups and resists chaining, but it can split a large, elongated group. **Average linkage** (UPGMA, for "unweighted pair group method with arithmetic mean", introduced by Sokal and Michener in 1958 ) uses the mean of all pairwise distances: $d(A, B) = \frac{1}{|A|\,|B|} \sum_{i \in A} \sum_{j \in B} d(i, j)$ where $|A|$ is the number of samples in $A$. It is a compromise between single and complete linkage. **Ward's method** does not measure a distance directly. It merges the pair of clusters whose union causes the smallest increase in the total within-cluster sum of squares: $\Delta(A, B) = \frac{|A|\,|B|}{|A| + |B|} \left\| \bar{\mathbf{x}}_A - \bar{\mathbf{x}}_B \right\|^2$ where $\bar{\mathbf{x}}_A$ and $\bar{\mathbf{x}}_B$ are the cluster means. It tends to produce compact groups of similar size, and with the Euclidean distance it is one of the most popular choices in chemometrics. Software usually draws Ward's dendrogram at the height $\sqrt{2\Delta}$ rather than at $\Delta$ itself, so that two single samples merge at their Euclidean distance; this is what SciPy's `ward` and R's `ward.D2` report, and what the demo below shows. ### Cutting the dendrogram A dendrogram does not say how many clusters there are; it shows every possible grouping at once. To get a single partition you **cut** the tree at a chosen height: every branch below the cut becomes a cluster. The panel below lets you do it, and compare linkages. Where to cut can be guided by: - **Gaps in the tree.** A long vertical stretch without merges means that groups only joined at a large distance; cutting there separates them. - **Domain knowledge.** If you know there should be three regions of origin, look at the three-cluster cut. - **Validation measures**, such as the silhouette, described below. For exploratory chemometric work, **Ward's method** with Euclidean distances is a sensible default: it finds compact, well-separated groups that often match distinct sample types. Average linkage is a good second opinion. Single linkage is useful when you expect elongated or irregular groups, and as a detector of chaining. ## k-means clustering Hierarchical clustering builds the whole tree. **k-means** directly divides the samples into a chosen number $k$ of groups. It is fast, simple and one of the most widely used clustering algorithms. ### The algorithm 1. **Choose k and initial centroids** Pick $k$ starting **centroids** (cluster centres). The simplest choice is $k$ random samples. The **k-means++** scheme picks them one at a time, preferring samples far from the centroids already chosen . 2. **Assign** Assign every sample to its nearest centroid. 3. **Update** Move each centroid to the mean of the samples assigned to it: $\boldsymbol{\mu}_c = \frac{1}{|C_c|} \sum_{i \in C_c} \mathbf{x}_i$ where $C_c$ is the set of samples in cluster $c$. 4. **Repeat until nothing changes** If no sample changed its cluster, stop. Otherwise go back to step 2. Try it step by step: ### What k-means minimises k-means minimises the **within-cluster sum of squares** (WCSS), the total squared distance from every sample to its centroid: $W = \sum_{c=1}^{k} \sum_{i \in C_c} \left\| \mathbf{x}_i - \boldsymbol{\mu}_c \right\|^2$ The two steps take turns at lowering $W$. With the centroids fixed, assigning each sample to its nearest centroid is the best possible assignment. With the assignment fixed, the mean is the point that minimises the sum of squared distances to the members (a small least squares problem). Since $W$ never increases and there are only finitely many ways to assign the samples, the algorithm always stops, usually after a few dozen iterations or fewer. ### Local minima But it stops at a **local** minimum, which depends on where it started. A poor start can leave two centroids sharing one natural group while two other groups are forced together, and the algorithm cannot escape. The standard remedy is to run k-means many times from different starts and keep the solution with the lowest $W$; k-means++ starts make bad solutions less likely. Most software does this for you: `n_init` in scikit-learn, `'Replicates'` in MATLAB, `nstart` in R. Use at least 10 to 20 starts, and check that the best solutions agree. ### Assumptions of k-means Because it works with distances to centroids, k-means implicitly prefers groups that are: - **Roughly round** (convex) rather than elongated or curved. - **Of similar spread.** A large, diffuse group can be split while small, tight groups are merged. - **Not too different in size.** Centring, scaling and PCA often bring data closer to these conditions, which is one reason preprocessing matters so much for clustering. ## How many clusters? Neither k-means nor an uncut dendrogram tells you the "right" number of groups. Several criteria help. ### Elbow method Run k-means for $k = 1, 2, \ldots$ and plot the best $W$ against $k$. The best $W$ never increases as $k$ grows (with as many clusters as samples it is zero), but after the true number of groups the improvement becomes small. The "elbow" where the curve flattens suggests a reasonable $k$. Real data do not always have a sharp elbow, so treat it as a hint. ### Silhouette The **silhouette** of sample $i$ compares how close it is to its own cluster with how close it is to the nearest other cluster : $s(i) = \frac{b(i) - a(i)}{\max\{a(i),\, b(i)\}}$ where $a(i)$ is the average distance from sample $i$ to the other members of its cluster, and $b(i)$ is the average distance to the members of the nearest other cluster. For a cluster with a single sample, $s(i)$ is set to 0. The value lies between $-1$ and $+1$: - Close to $+1$: the sample sits well inside its cluster, far from the others. - Close to $0$: the sample is on the border between two clusters. - Negative: the sample is, on average, closer to another cluster, and may be misassigned. The **average silhouette width** over all samples summarises the whole partition. Comparing it for different $k$, or different cuts of a dendrogram, and choosing the largest is a common rule. ### Gap statistic The **gap statistic** compares the observed $\log W$ with what you would expect from data with no cluster structure, typically samples spread uniformly over the same range. The chosen $k$ is the smallest one for which the gap at $k$ is at least the gap at $k+1$ minus one standard error. It is more principled than the elbow, but needs a set of simulated reference data sets (10 to 50 are typical). ## Validating clusters Finding clusters is easy; any algorithm will return them, even for random data. Finding *meaningful* clusters is harder. ### Internal validation Internal measures use only the data: - **Silhouette plots.** Plot the silhouette of every sample, sorted by cluster. Good clusters show wide bars and few negative values. - **Within-cluster sum of squares**, compared with what the same algorithm gives on structureless data (for example, data with each variable shuffled independently). - **Davies-Bouldin index** . For each cluster it finds the most similar other cluster, measuring similarity as the sum of their spreads divided by the distance between their centroids, and averages over clusters: $DB = \frac{1}{k} \sum_{c=1}^{k} \max_{l \neq c} \frac{\sigma_c + \sigma_l}{d(\boldsymbol{\mu}_c, \boldsymbol{\mu}_l)}$ where $\sigma_c$ is the average distance of the members of cluster $c$ to its centroid. Lower is better. ### External validation When true labels are known, even for some samples, you can compare the clusters with them: - **Adjusted Rand index** (ARI) : the agreement between two partitions, corrected so that random assignments score about 0 and identical partitions score 1. - **Normalised mutual information** (NMI): how much knowing the cluster tells you about the true label, from 0 (nothing) to 1 (everything). Unlike the ARI, it is not corrected for chance, and it tends to grow with the number of clusters . External validation is the strongest check, but it needs labels, which exploratory work often lacks. ## Preparing data for clustering Clustering is very sensitive to how the data are prepared, because everything depends on distances. ### Spectral preprocessing Apply the usual corrections first: - **Baseline correction** (for example [asymmetric least squares](/introduction-to-chemometrics/preprocessing/baseline-correction-asls) or polynomial fitting) to remove slowly varying backgrounds. - **Scatter correction** (SNV or [MSC](/introduction-to-chemometrics/preprocessing/msc)) to remove multiplicative effects of particle size or path length. - **Smoothing** (for example [Savitzky-Golay](/introduction-to-chemometrics/preprocessing/savitzky-golay)) if the noise is strong. Otherwise, baseline and intensity differences can dominate the distances and hide the chemical groups. ### Centring and scaling **Mean centring** focuses on differences between samples rather than on the common signal (it does not change Euclidean distances, but it matters for PCA and for the spectral angle). **Autoscaling** gives every variable the same weight. Without it, variables with large numerical ranges dominate. With spectra, where all variables share the same unit, autoscaling is usually a bad idea: it gives the noise of empty spectral regions the same weight as the bands. Mean-centre spectra; autoscale tables that mix different units (see [centring and scaling](/introduction-to-chemometrics/preprocessing/centering-and-scaling)). If a table mixes absorbances between 0 and 2 with temperatures between 20 and 200 °C, the unscaled Euclidean distance is decided almost entirely by temperature. Autoscale first. ### PCA before clustering With hundreds or thousands of variables, clustering in the full space has two problems: 1. **The curse of dimensionality.** In very high dimensions, distances between points become more and more alike, which makes groups harder to distinguish. 2. **Noise.** Many spectral variables carry mostly noise, which adds to every distance and dilutes the informative differences. The usual solution is to compute a [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) first and cluster the scores on the first few components. This removes most of the noise and keeps the structure in a handful of dimensions. Typically, 2 to 10 components are enough. The order of operations is: correct the spectra, centre (and scale if needed), compress with PCA, then cluster. ## Clustering in chemistry - **Classification of spectra.** NIR, mid-IR or Raman spectra of unknown samples, clustered with Ward's method on PCA scores, often reveal groups that correspond to different compositions, origins or processing. - **Origin and variety.** Wines, coffees, teas, oils and pharmaceutical raw materials are routinely grouped by origin or variety from their spectra or chemical profiles. A classic example is Forina's set of 572 Italian olive oils from nine growing areas, described by the proportions of eight fatty acids . Clustering shows how naturally the groups separate before any supervised model is built. - **Authenticity and adulteration.** Genuine and adulterated products can form separate groups; samples in between may indicate partial adulteration, or simply natural variability. Only reference samples can tell. - **Process monitoring.** Spectra collected over time form one tight group while the process is stable. A new group signals a change. Clustering complements PCA-based control charts by characterising the different states. ## Code implementation The examples simulate spectra of three sample types, mean-centre them (all variables are absorbances, so autoscaling would only inflate the noise of the empty regions), compress them with PCA, and then apply Ward's hierarchical clustering and k-means. ```python from sklearn.cluster import KMeans from sklearn.decomposition import PCA from sklearn.metrics import silhouette_score, adjusted_rand_score from scipy.cluster.hierarchy import dendrogram, linkage, fcluster # --- Synthetic spectra: three groups with distinct profiles --- rng = np.random.default_rng(42) n_per_group, n_wavelengths = 40, 200 wavelengths = np.linspace(400, 2500, n_wavelengths) def band(center, width2): return np.exp(-((wavelengths - center) ** 2) / width2) profiles = [0.8 * band(800, 5000), 0.7 * band(1400, 6000), 0.5 * band(800, 5000) + 0.6 * band(1700, 4000)] X = np.vstack([p + rng.normal(0, 0.05, (n_per_group, n_wavelengths)) for p in profiles]) true_labels = np.repeat([0, 1, 2], n_per_group) # --- Preprocessing: mean centring and PCA (no autoscaling: all variables are absorbances) --- X_c = X - X.mean(axis=0) pca = PCA(n_components=5) scores = pca.fit_transform(X_c) print(f"Variance explained by 5 PCs: {pca.explained_variance_ratio_.sum():.1%}") # about 85% # --- Hierarchical clustering with Ward's method --- Z = linkage(scores, method='ward') hc_labels = fcluster(Z, t=3, criterion='maxclust') # cut the tree into 3 clusters plt.figure(figsize=(12, 4)) dendrogram(Z, no_labels=True) plt.title("Dendrogram (Ward's method on PCA scores)") plt.ylabel('Merge height') plt.show() # --- k-means: elbow and silhouette for k = 2..8 --- ks = range(2, 9) wcss, sil = [], [] for k in ks: km = KMeans(n_clusters=k, n_init=20, random_state=0).fit(scores) wcss.append(km.inertia_) sil.append(silhouette_score(scores, km.labels_)) fig, axes = plt.subplots(1, 2, figsize=(11, 4)) axes[0].plot(list(ks), wcss, 'o-'); axes[0].set_xlabel('k'); axes[0].set_ylabel('WCSS') axes[0].set_title('Elbow plot') axes[1].plot(list(ks), sil, 'o-'); axes[1].set_xlabel('k'); axes[1].set_ylabel('Average silhouette') axes[1].set_title('Silhouette') plt.tight_layout(); plt.show() # --- Final k-means with k = 3, compared with the true groups --- km = KMeans(n_clusters=3, n_init=20, random_state=0).fit(scores) print(f"Silhouette (k=3): {silhouette_score(scores, km.labels_):.3f}") # Well-separated groups: both methods should give ARI = 1 print(f"ARI k-means vs truth: {adjusted_rand_score(true_labels, km.labels_):.3f}") print(f"ARI Ward vs truth: {adjusted_rand_score(true_labels, hc_labels):.3f}") plt.figure(figsize=(6, 5)) plt.scatter(scores[:, 0], scores[:, 1], c=km.labels_, cmap='viridis', edgecolors='k', linewidths=0.3) plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%})') plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%})') plt.title('k-means clusters on PCA scores') plt.show() ``` ```matlab % --- Synthetic spectra: three groups with distinct profiles --- rng(42); n_per_group = 40; n_wl = 200; wavelengths = linspace(400, 2500, n_wl); band = @(c, w2) exp(-((wavelengths - c).^2) / w2); base1 = 0.8 * band(800, 5000); base2 = 0.7 * band(1400, 6000); base3 = 0.5 * band(800, 5000) + 0.6 * band(1700, 4000); X = [repmat(base1, n_per_group, 1); repmat(base2, n_per_group, 1); ... repmat(base3, n_per_group, 1)] + 0.05 * randn(3 * n_per_group, n_wl); true_labels = repelem((1:3)', n_per_group); % --- Preprocessing: mean centring and PCA (pca centres by default) --- [~, scores, latent] = pca(X); scores = scores(:, 1:5); fprintf('Variance explained by 5 PCs: %.1f%%\n', 100 * sum(latent(1:5)) / sum(latent)); % --- Hierarchical clustering (Ward) and a 3-cluster cut --- Z = linkage(scores, 'ward'); hc_labels = cluster(Z, 'maxclust', 3); figure; dendrogram(Z, 0); title("Dendrogram (Ward's method on PCA scores)"); % --- k-means: elbow and silhouette --- ks = 2:8; wcss = zeros(size(ks)); sil = zeros(size(ks)); for i = 1:numel(ks) [idx, ~, sumd] = kmeans(scores, ks(i), 'Replicates', 20); wcss(i) = sum(sumd); sil(i) = mean(silhouette(scores, idx)); end figure; subplot(1, 2, 1); plot(ks, wcss, 'o-'); xlabel('k'); ylabel('WCSS'); title('Elbow plot'); subplot(1, 2, 2); plot(ks, sil, 'o-'); xlabel('k'); ylabel('Average silhouette'); title('Silhouette'); % --- Final k-means with k = 3 --- labels = kmeans(scores, 3, 'Replicates', 20); figure; gscatter(scores(:, 1), scores(:, 2), labels); xlabel(sprintf('PC1 (%.1f%%)', 100 * latent(1) / sum(latent))); ylabel(sprintf('PC2 (%.1f%%)', 100 * latent(2) / sum(latent))); title('k-means clusters on PCA scores'); grid on; ``` ```r library(cluster) # --- Synthetic spectra: three groups with distinct profiles --- set.seed(42) n_per_group works like k-means, but each centre is a real sample (the *medoid*) rather than a mean. It accepts any distance, including the spectral angle or the correlation, and it is less sensitive to outliers. - **Fuzzy c-means** gives each sample a degree of membership in every cluster instead of a single label. It is widely used in chemometrics when groups overlap. - **Gaussian mixture models** are a probabilistic version of k-means: each group is a normal distribution with its own covariance, so groups can be elliptical, and each sample gets a probability of belonging to each group. - **DBSCAN** (density-based spatial clustering of applications with noise) grows clusters from dense regions of the data. It finds groups of any shape, does not need $k$, and labels isolated samples as noise. ## When to use clustering Clustering is a good tool for: - Discovering natural groups in unlabelled data. - Grouping samples by spectral similarity (origin, type, quality). - Spotting off-specification batches or process changes. - Reducing a large set of samples to a few representative groups. - Exploring the data before building a supervised classifier. It is not the right tool for: - **Predicting a continuous property.** Use regression. - **Heavily overlapping classes.** Hard clustering will cut them arbitrarily; consider soft or probabilistic methods such as Gaussian mixture models, or supervised methods such as PLS-DA if labels exist. - **Raw, very high-dimensional data.** Preprocess and compress with PCA first. Keep in mind that clustering **always** returns clusters, even for structureless data, that results depend strongly on preprocessing, distance and linkage, and that small changes in the data or the starting point can move samples between clusters. Validation is not optional. Computationally, the naive agglomerative algorithm needs time up to $n^3$, although modern implementations bring the common linkages (single, complete, average and Ward) down to about $n^2$ . The memory needed for the distance matrix, proportional to $n^2$, remains the limit for very large data sets; k-means scales much better. ## What to remember Clustering groups samples using nothing but their similarities. Hierarchical methods build a whole tree of groupings and let you choose where to cut it; the linkage rule decides what "close" means for groups. k-means alternates between assigning samples to the nearest centre and moving the centres to the mean, which lowers the within-cluster sum of squares at every step but can get stuck, so it should be run from many starts. The number of clusters is a choice, guided by elbows, silhouettes and chemistry. And because any algorithm will find groups, the real work is checking that they mean something. From here: - **[Principal component analysis](/introduction-to-chemometrics/models/exploratory-models/pca)** is the usual step before clustering, and its scores plot gives a first visual impression of the groups. - **SIMCA** (soft independent modelling of class analogy) builds a PCA model for each known class and is used when labelled classes exist. - **Discriminant analysis** (LDA, PLS-DA) uses known labels to find the directions that best separate classes. ## References # Factor Analysis (/introduction-to-chemometrics/models/exploratory-models/factor-analysis) Factor analysis was not born in a chemistry laboratory but in psychology. In 1904, the British psychologist **Charles Spearman** published *"'General Intelligence,' Objectively Determined and Measured"* . He had noticed that children who did well on one kind of test (say, classics) also tended to do well on apparently unrelated ones (say, discrimination of musical pitch). His explanation was a single hidden quantity, which he called *g* for "general intelligence", that influenced all the tests at once. The test scores were not the fundamental quantities; they were noisy reflections of something deeper that could not be observed directly. This is the **latent variable** idea, and it needed new mathematics to make it precise. In the 1930s and 1940s, the American psychologist **L. L. Thurstone** expanded that mathematics dramatically. Where Spearman argued for one general factor, Thurstone believed in several distinct abilities (verbal, spatial, numerical and others). He developed **multiple factor analysis**, introduced **factor rotation** to reach what he called **simple structure**, a solution in which each variable depends on as few factors as possible, and turned factor analysis into a general multivariate method . The method entered chemistry through **Edmund R. Malinowski**, a physical chemist at Stevens Institute of Technology. His book *Factor Analysis in Chemistry*, written with **D. G. Howery** and first published in 1980, showed how the same machinery could count the independent chemical components in a set of mixture spectra and help recover their pure spectra . In chemistry, the "latent factors" are not abstract constructs: they are real chemical species whose spectra and concentrations can, at least in principle, be measured on their own. That concrete meaning is what made factor analysis so productive in chemometrics, and it led to a family of methods that includes target factor analysis, evolving factor analysis and self-modelling curve resolution. ## The latent variable idea The central idea of factor analysis is that the variables you measure are not the variables that matter. A smaller number of hidden **factors** generate the data, and what you observe is their combined effect plus some noise. Think of UV-Vis spectra of mixtures of three compounds, measured at 200 wavelengths. You record 200 absorbances per sample, but there are only three independent sources of variation: the three concentrations. The 200 absorbances rise and fall together because they share the same three causes. Factor analysis turns this intuition into a model. In the factor model, each observed variable $x_j$ is a weighted sum of $m$ **common factors** $f_1, \ldots, f_m$ plus a term of its own: $x_j = \lambda_{j1} f_1 + \lambda_{j2} f_2 + \cdots + \lambda_{jm} f_m + \varepsilon_j$ The coefficients $\lambda_{jk}$ are the **factor loadings**. They tell how strongly variable $j$ depends on factor $k$. The term $\varepsilon_j$, the **unique factor**, collects everything that belongs to variable $j$ alone: measurement noise, a detector artefact, or any variation specific to that wavelength. This is a **generative model**: first come the factors, and the data follow from them. Correlations between variables exist because the variables share causes. In psychology, a factor such as "verbal ability" cannot be measured directly. In chemistry, factors often correspond to physical things, such as the pure spectrum and the concentration of a species. That makes the results testable: you can measure the pure spectrum of a suspected component and check whether it fits a recovered factor. ## Factor analysis and PCA Factor analysis and [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) are often confused, and for good reasons: both reduce many variables to a few, and both are often computed from eigenvalues of a covariance or correlation matrix. But they rest on different assumptions and answer different questions. | | Factor analysis | PCA | |:---|:---|:---| | **Goal** | Explain the correlations between variables through a few latent factors | Summarise as much of the total variance as possible in a few components | | **Model** | Explicit model: $\mathbf{x} = \boldsymbol{\Lambda}\mathbf{f} + \boldsymbol{\varepsilon}$ | A decomposition of the data, with no model for the noise | | **Variance** | Separates **common** variance (shared) from **unique** variance (specific to each variable) | Treats all variance alike, common and unique | | **Diagonal of the correlation matrix** | Replaced by estimated communalities | Kept at 1 (or at the variances) | | **Rotation** | Routine, to make the factors interpretable | Possible, but the rotated components lose the property of capturing maximum variance one after another | | **Number of factors** | Part of the model; must be chosen before fitting | All components exist; you choose how many to keep | | **Uniqueness of the solution** | Only defined up to a rotation | Unique, up to the sign of each component (when the eigenvalues are distinct) | The practical consequence: if you want to understand the latent structure that produces your data, factor analysis is the natural framework. If you only need a compact summary for later modelling, PCA is simpler and makes fewer assumptions. When there are many variables per factor and the unique variances are small and similar, PCA and factor analysis give very similar loadings. This is common in spectroscopy, where hundreds of wavelengths are driven by a few species and the noise is fairly uniform. ## The factor model in matrix form For a vector $\mathbf{x} = (x_1, \ldots, x_p)^T$ of $p$ observed variables, centred to zero mean, the model is $\mathbf{x} = \boldsymbol{\Lambda} \mathbf{f} + \boldsymbol{\varepsilon}$ where: - $\boldsymbol{\Lambda}$ is the $p \times m$ **loading matrix**, linking variables to factors. - $\mathbf{f}$ is the vector of $m$ **common factors**, with $m . 1. **Guess the communalities** A common starting point is the squared multiple correlation of each variable with all the others: how well it can be predicted from them. 2. **Replace the diagonal** Put these communalities on the diagonal of the correlation matrix, in place of the ones. The new matrix describes only the shared variance. 3. **Find eigenvalues and eigenvectors** Decompose this reduced matrix, exactly as in PCA. 4. **Build the loadings** Keep the $m$ largest eigenvalues and their eigenvectors: $\boldsymbol{\Lambda} = \mathbf{V}_m \boldsymbol{\Delta}_m^{1/2}$ where $\mathbf{V}_m$ holds the eigenvectors and $\boldsymbol{\Delta}_m$ is the diagonal matrix of the eigenvalues. 5. **Update and repeat** Compute new communalities from the loadings, $h_j^2 = \sum_k \lambda_{jk}^2$, put them on the diagonal, and repeat until they stop changing. The method is simple and widely available, but it does not provide a formal statistical test for the number of factors. ### Maximum likelihood **Maximum likelihood factor analysis**, introduced by **D. N. Lawley** in 1940 , assumes that the data follow a multivariate normal distribution and looks for the $\boldsymbol{\Lambda}$ and $\boldsymbol{\Psi}$ that make the observed data most likely under that model. Up to a constant, it maximises $\ell(\boldsymbol{\Lambda}, \boldsymbol{\Psi}) = -\frac{n}{2}\left[\log\left|\boldsymbol{\Lambda}\boldsymbol{\Lambda}^T + \boldsymbol{\Psi}\right| + \text{tr}\left(\left(\boldsymbol{\Lambda}\boldsymbol{\Lambda}^T + \boldsymbol{\Psi}\right)^{-1}\mathbf{S}\right)\right]$ where $n$ is the number of samples, $|\cdot|$ is the determinant and $\text{tr}$ the trace (the sum of the diagonal). There is no closed-form solution, so it is solved iteratively. Its main advantage is a **likelihood ratio test**: an approximately chi-squared statistic that tests whether $m$ factors are enough to reproduce the observed covariances. With low-noise spectra, maximum likelihood often drives some uniquenesses to zero (a Heywood case); software then clamps them at a small lower bound and may refuse to compute the test. For exploration, principal axis factoring is simple and robust. When you want to test whether a given number of factors is adequate, maximum likelihood provides formal goodness-of-fit statistics. Note that the classical implementations of both (such as `factanal` in R and `factoran` in MATLAB) need more samples than variables to work with a full covariance matrix, which is rarely the case for whole spectra; in spectroscopy, factor analysis is usually applied to the data matrix directly, as discussed below. ## Rotation The loading matrix is not unique. For any orthogonal matrix $\mathbf{R}$ (a rotation, with $\mathbf{R}\mathbf{R}^T = \mathbf{I}$), the loadings $\boldsymbol{\Lambda}^* = \boldsymbol{\Lambda}\mathbf{R}$ reproduce the covariance matrix exactly as well, because $\boldsymbol{\Lambda}^*(\boldsymbol{\Lambda}^*)^T = \boldsymbol{\Lambda}\mathbf{R}\mathbf{R}^T\boldsymbol{\Lambda}^T = \boldsymbol{\Lambda}\boldsymbol{\Lambda}^T$ (We call the rotation $\mathbf{R}$ to avoid confusion with the scores $\mathbf{T}$ of [PCA](/introduction-to-chemometrics/models/exploratory-models/pca).) This **rotational indeterminacy** means that the first solution an algorithm returns is just one of infinitely many equivalent ones. Rotation uses that freedom to pick the solution that is easiest to interpret. ### Why rotate? The unrotated solution tends to produce a first factor that loads on almost everything (a "general" factor) and later factors that contrast groups of variables. It is mathematically natural but hard to read. Rotation redistributes the variance among the factors to approach Thurstone's **simple structure**: each variable loads strongly on one factor and weakly on the others. ### Varimax **Varimax**, introduced by **Henry Kaiser** in 1958 , is the most widely used orthogonal rotation. It maximises the variance of the squared loadings within each factor: $V = \sum_{k=1}^{m} \left[ \frac{1}{p}\sum_{j=1}^{p} \left(\lambda_{jk}^{*}\right)^4 - \left(\frac{1}{p}\sum_{j=1}^{p} \left(\lambda_{jk}^{*}\right)^2\right)^2 \right]$ A factor whose squared loadings are all similar has a small variance; a factor with a few large loadings and many near zero has a large one. Maximising $V$ therefore pushes loadings towards either large or near-zero values. (In practice the loadings are often divided by the square root of the communalities before rotation, so that every variable counts equally; this is Kaiser normalisation.) Varimax keeps the factors uncorrelated. The panel below shows rotation on a simple two-factor example: sixteen wavelengths of a two-component mixture. The data are only mean-centred, as is usual for spectra, so the loadings keep absorbance units and can be compared directly with the bands. ### Promax In many real problems, the underlying factors are not independent: in a reaction, for example, the concentration of a product rises as the reactant falls. **Promax** starts from the varimax solution, raises its loadings to a power (usually 4, keeping the sign) to sharpen the pattern, and then finds the oblique (non-orthogonal) rotation that comes closest to this sharpened target. The result often has simpler structure than varimax, at the price of correlated factors. ### Rotation in chemistry In mixture analysis, rotation has a direct physical meaning. The factors obtained from an eigenvalue decomposition of spectral data are abstract: mathematically valid linear combinations of the pure spectra, but not the pure spectra themselves. Rotation can bring them closer to the real spectra, especially when the bands do not overlap too much. Adding chemical constraints, such as non-negative spectra and concentrations, is the basis of **target rotation** and **self-modelling curve resolution**, discussed below. ## How many factors? Choosing the number of factors $m$ is one of the most consequential decisions. Several criteria exist, and they do not always agree. ### Eigenvalues greater than one (Kaiser's rule) Keep the factors whose eigenvalues of the correlation matrix are larger than 1 . The reasoning is that a factor should explain more variance than a single standardised variable does. The rule is simple and popular, but simulation studies show that it tends to keep too many factors, especially when there are many variables . ### Scree test Plot the eigenvalues in decreasing order and look for the elbow where they change from a steep drop to a flat tail of "rubble" . It is subjective, but often works well when the signal is clearly larger than the noise. ### Parallel analysis Generate random data sets of the same size as yours (same numbers of samples and variables) but with no factor structure, and compute their eigenvalues. Keep only the factors whose eigenvalues are larger than those of the random data . This accounts for the fact that pure noise also produces eigenvalues larger than zero, and some larger than one. Comparative studies rank parallel analysis among the most accurate rules . ### Likelihood ratio test With maximum likelihood, fit models with $m = 1, 2, 3, \ldots$ factors and choose the smallest $m$ that the chi-squared test does not reject. With very large samples the test rejects even tiny misfits, and with small samples the chi-squared approximation is poor, so use it with judgement. ### Cross-validation Leave out part of the data, fit models with different numbers of factors, and check how well the left-out part is reproduced. The number that predicts unseen data best is retained . As with PCA, the left-out values must not be used to compute their own scores. In mixture analysis you often have independent information: the reaction involves three species, or the spectra cross at sharp isosbestic points (wavelengths where the absorbance stays constant during a reaction), which indicate just two absorbing species interconverting with constant total concentration. That knowledge should guide the choice and can overrule purely statistical criteria. ## Factor analysis in chemistry Chemists brought factor analysis to bear on a concrete problem: given spectra of samples that contain unknown combinations of species, **how many species are there, and what are their spectra?** ### Counting the components If a set of mixture spectra follows the Beer-Lambert law, the data matrix $\mathbf{D}$ (samples by wavelengths) can be written as concentrations times pure spectra, $\mathbf{D} = \mathbf{C}\mathbf{S}^T + \mathbf{E}$ and, without noise, its rank (the number of independent rows or columns) equals the number of absorbing species $m$, provided their spectra are linearly independent and their concentrations vary independently. Closure (concentrations that sum to a constant) or species that always appear in fixed ratios lower the rank, and so does mean-centring. With noise, every eigenvalue becomes non-zero, but the first $m$ stay well above a floor of small "noise" eigenvalues. Estimating this **chemical rank** is the first task of chemical factor analysis. Malinowski developed several tools for it, including the **real error** (RE) and the **indicator function** (IND) . For a matrix with $r$ rows and $c$ columns ( c" />) and eigenvalues $\lambda_j$ of $\mathbf{D}^T\mathbf{D}$, they are $\text{RE}(n) = \sqrt{\frac{\sum_{j=n+1}^{c} \lambda_j}{r\,(c - n)}}, \qquad \text{IND}(n) = \frac{\text{RE}(n)}{(c - n)^2}$ RE estimates the size of the noise if $n$ factors were real, and IND typically reaches a minimum at the true number of factors. With the right number of factors, RE comes out close to the true noise level, so it doubles as an estimate of the measurement error. Try it on simulated mixtures: ### Evolving factor analysis When spectra are recorded in sequence, as along a chromatographic peak or during a titration or reaction, **evolving factor analysis** (EFA) computes the eigenvalues of growing windows of the data: the first two spectra, the first three, and so on, and also the same from the end backwards. A new eigenvalue rising out of the noise marks the appearance of a new species; following the backward analysis shows when each one disappears. The result is a "concentration window" for each component, which is valuable information for the next step. EFA was introduced for titrations by **Gampp, Maeder** and colleagues in 1985 and extended to chromatography by Maeder . ### Abstract and real factors The factors from an eigenvalue decomposition are abstract. To reach physically meaningful spectra and concentrations, additional information is needed: - **Target factor analysis** (TFA) tests whether a candidate spectrum, for example a library spectrum of a suspected compound, lies in the space spanned by the factors. If it can be reproduced well as a combination of them, it is a plausible component of the mixture . - **Self-modelling curve resolution**, pioneered by **Lawton and Sylvestre** in 1971 , uses the constraint that spectra and concentrations cannot be negative to find the range of physically acceptable solutions. - **Multivariate curve resolution by alternating least squares** (MCR-ALS) generalises this idea with constraints such as non-negativity, unimodality and closure, and is today the most widely used method of this family. It was developed by **Romà Tauler** and colleagues in the 1990s . In psychology, rotational ambiguity is a nuisance: there is no "true" orientation of the factors to find. In chemistry there is one, because real pure spectra exist. The abstract factors are mathematically correct but chemically wrong, and the whole field of curve resolution is, in essence, an attempt to remove this ambiguity with chemical constraints. When the constraints are not strong enough, a range of solutions remains equally valid, and it is good practice to report that range. ## Code implementation The examples simulate spectra of three-component mixtures and fit a three-factor model with varimax rotation. Maximum likelihood (in MATLAB and R) needs more samples than variables, so the simulation uses 200 samples and 30 wavelengths. The noise is set to 0.10: with much less noise the central wavelengths become almost pure signal, maximum likelihood drives many uniquenesses to zero (a Heywood case), and MATLAB then refuses to compute the test. ```python from sklearn.decomposition import FactorAnalysis # --- Simulate a 3-component spectroscopic mixture --- rng = np.random.default_rng(42) n_samples, n_wavelengths, n_factors = 200, 30, 3 wavelengths = np.linspace(200, 800, n_wavelengths) centers = [350, 500, 650] pure_spectra = np.array([np.exp(-0.5 * ((wavelengths - c) / 60) ** 2) for c in centers]) concentrations = rng.exponential(1.0, (n_samples, n_factors)) spectra = concentrations @ pure_spectra + rng.normal(0, 0.10, (n_samples, n_wavelengths)) # Standardise so that communalities lie between 0 and 1 Z = (spectra - spectra.mean(axis=0)) / spectra.std(axis=0, ddof=1) # --- Factor analysis with varimax rotation (scikit-learn >= 0.24) --- fa = FactorAnalysis(n_components=n_factors, rotation='varimax', random_state=0) fa.fit(Z) loadings = fa.components_.T # (n_wavelengths, n_factors) communalities = np.sum(loadings ** 2, axis=1) uniquenesses = fa.noise_variance_ print(f"Mean communality: {communalities.mean():.3f}") print(f"Mean uniqueness: {uniquenesses.mean():.3f}") # Eigenvalues of the correlation matrix (as in PCA), the usual input for Kaiser's rule and the scree test eigenvalues = np.linalg.eigvalsh(np.corrcoef(Z.T))[::-1] fig, axes = plt.subplots(1, 3, figsize=(15, 4)) axes[0].plot(range(1, 11), eigenvalues[:10], 'o-') axes[0].axhline(1, color='r', linestyle='--', label='Kaiser criterion') axes[0].set_xlabel('Factor number'); axes[0].set_ylabel('Eigenvalue') axes[0].set_title('Scree plot'); axes[0].legend() for k in range(n_factors): axes[1].plot(wavelengths, loadings[:, k], label=f'Factor {k + 1}') axes[1].set_xlabel('Wavelength (nm)'); axes[1].set_ylabel('Loading') axes[1].set_title('Varimax-rotated loadings'); axes[1].legend() axes[2].plot(wavelengths, communalities, 'g-', linewidth=2) axes[2].set_ylim(0, 1.05) axes[2].set_xlabel('Wavelength (nm)'); axes[2].set_ylabel('Communality') axes[2].set_title('Communalities') plt.tight_layout() plt.show() ``` ```matlab % --- Simulate a 3-component spectroscopic mixture --- rng(42); n_samples = 200; n_wavelengths = 30; n_factors = 3; wavelengths = linspace(200, 800, n_wavelengths); centers = [350, 500, 650]; pure_spectra = zeros(n_factors, n_wavelengths); for i = 1:n_factors pure_spectra(i, :) = exp(-0.5 * ((wavelengths - centers(i)) / 60).^2); end concentrations = exprnd(1, n_samples, n_factors); spectra = concentrations * pure_spectra + 0.10 * randn(n_samples, n_wavelengths); % --- Maximum likelihood factor analysis with varimax rotation --- % factoran works on the correlation matrix, so communalities = 1 - psi [loadings, psi, ~, stats] = factoran(spectra, n_factors, 'rotate', 'varimax'); communalities = 1 - psi; fprintf('Mean communality: %.3f\n', mean(communalities)); if isfield(stats, 'chisq') fprintf('Chi-squared = %.1f, df = %d, p = %.3f\n', stats.chisq, stats.dfe, stats.p); else disp('Heywood case: some uniquenesses hit the lower bound, so no test is available'); end eigenvalues = sort(eig(corrcoef(spectra)), 'descend'); figure('Position', [100 100 1400 400]); subplot(1, 3, 1); plot(1:10, eigenvalues(1:10), 'o-'); yline(1, '--r'); xlabel('Factor number'); ylabel('Eigenvalue'); title('Scree plot'); grid on; subplot(1, 3, 2); plot(wavelengths, loadings, 'LineWidth', 1.5); xlabel('Wavelength (nm)'); ylabel('Loading'); title('Varimax-rotated loadings'); legend('Factor 1', 'Factor 2', 'Factor 3'); grid on; subplot(1, 3, 3); plot(wavelengths, communalities, 'g-', 'LineWidth', 2); ylim([0 1.05]); xlabel('Wavelength (nm)'); ylabel('Communality'); title('Communalities'); grid on; ``` ```r # --- Simulate a 3-component spectroscopic mixture --- set.seed(42) n_samples . ## When to use factor analysis Factor analysis is a good choice for: - **Counting components in mixtures**, the fundamental question of spectroscopic mixture analysis. - **Understanding latent structure**, when you believe hidden causes drive the correlations, as in environmental, process or sensory data. - **Resolving mixtures**, together with constraints or target testing, as the starting point of curve resolution. - **Grouping correlated variables** and understanding what drives them. Other methods are better for: - **Plain dimensionality reduction.** If you only need fewer variables for regression or classification, PCA is simpler and makes fewer assumptions. - **Prediction.** Factor scores are not built to predict a response; use PLS or PCR. - **Small data sets.** Covariance-based factor analysis needs reasonably many samples per variable; with few samples, the estimates are unstable. - **Nonlinear structure.** The factor model is linear. ## What to remember Factor analysis assumes that a few hidden factors produce the correlations you observe, and it separates the variance they share from the variance unique to each variable. Its loadings are only defined up to a rotation; rotation does not change the fit, it only changes how the result is expressed, and methods like varimax choose the expression that is easiest to read. In chemistry, the factors can be real species, which turns counting factors into counting components, and turns the rotation problem into the central question of curve resolution. The closest relative is **[principal component analysis](/introduction-to-chemometrics/models/exploratory-models/pca)**, which has no model for the noise and is often the starting point for factor analysis. Two further methods, not yet covered in this library, are worth knowing about: **multivariate curve resolution** (MCR-ALS), which uses chemical constraints to recover pure spectra and concentration profiles, and **independent component analysis** (ICA), which looks for factors that are statistically independent rather than merely uncorrelated, and which relies on the factors being non-Gaussian . ## References # Principal Component Analysis (PCA) (/introduction-to-chemometrics/models/exploratory-models/pca) If you hand a chemometrician a new data set, the first thing they will almost certainly do is run a **principal component analysis**. PCA takes a table with many correlated variables, such as a few hundred spectra measured at a thousand wavelengths, and redraws it on a handful of new axes chosen so that the most important differences between the samples become visible. It does not need labels, concentrations or any prior knowledge. It simply asks: in which directions do these samples differ the most? The idea is more than a century old. In 1901, the English mathematician and biometrician **Karl Pearson** published *"On Lines and Planes of Closest Fit to Systems of Points in Space"* . His question was geometric: given a cloud of points, which straight line (or plane) passes as close as possible to all of them at once? He measured closeness perpendicular to the line, not vertically as in ordinary regression, and found that the best line runs through the centre of the cloud along the direction in which the points are most spread out. Three decades later, the American statistician and economist **Harold Hotelling** reached the same mathematics from a different starting point. Working with batteries of psychological test scores, he looked for a small number of linear combinations of the tests that captured as much of their variance as possible. In 1933 he published *"Analysis of a Complex of Statistical Variables into Principal Components"*, which gave the method its name and the eigenvalue framework we still use . Pearson thought in terms of closest-fitting subspaces; Hotelling thought in terms of maximum variance. As you will see in a moment, these are two descriptions of the same thing. For chemistry, the bridge came through computation. In 1966, **Herman Wold** described an iterative algorithm, later known as **NIPALS**, that extracts principal components one at a time using simple regressions . It needs very little memory and it tolerates missing values, which made it well suited to the computers and data of the time. (Wold first called it NILES, for nonlinear iterative least squares; the name NIPALS came later.) His son **Svante Wold** and colleagues such as **Kim Esbensen** and **Paul Geladi** then made PCA a cornerstone of chemical data analysis during the 1970s and 1980s. Their 1987 tutorial remains one of the most cited papers in chemometrics. Today PCA sits at the start of almost every chemometric workflow. ## The dimensionality problem A modern near-infrared (NIR) spectrometer can record absorbance at more than a thousand wavelengths. Suppose you measure 200 olive oil samples, each at 1050 wavelengths between 900 and 2500 nm. Your data form a $200 \times 1050$ matrix: 200 rows (samples) and 1050 columns (variables). Mathematically, every sample is a point in a space with 1050 dimensions. But do you really need 1050 numbers to describe how these oils differ? Almost certainly not. The absorbance at 1720 nm is nearly the same information as the absorbance at 1721 nm, because both come from the same molecular vibration. Whole regions move together: the C-H overtone bands near 1210 nm and 1730 nm rise and fall with the fatty acid chains, and the O-H band near 1940 nm follows the water content. The 1050 wavelengths are not 1050 independent pieces of information. They are redundant measurements of a much smaller number of underlying sources of variation: a few chemical constituents, plus physical effects such as light scattering. PCA answers two questions about such data: - **How many** independent directions of variation does the data set really contain? - **What do they look like**, both in terms of the samples (which are similar, which are different) and in terms of the variables (which wavelengths are involved)? If the answer to the first question is "about five", PCA can compress 1050 dimensions into five with very little loss. That makes the data visible (you can plot two or three dimensions, not 1050), separates systematic variation from noise, and provides a stable starting point for regression and classification. ## The core idea Imagine the samples as a cloud of points. Through that cloud, some directions show a lot of spread and others very little. The directions with the most spread carry the most information about how the samples differ. The **first principal component** (PC1) is the direction along which the samples have the **largest variance**. Mathematically it is a weighted sum of the original variables, $t_1 = p_{11} x_1 + p_{21} x_2 + \cdots + p_{J1} x_J$ where $x_1, \ldots, x_J$ are the $J$ original variables (for example, wavelengths) and the weights $p_{11}, \ldots, p_{J1}$ define the direction (the first index is the variable, the second the component). PCA chooses the weights so that the values $t_1$ have the largest possible variance across the samples, with the constraint that the weight vector has unit length, $\sum_j p_{j1}^2 = 1$. Without that constraint we could make the variance as large as we liked just by multiplying all the weights by ten. The **second principal component** (PC2) is the direction of largest variance that is **perpendicular to PC1**. It collects the largest part of what PC1 missed. The third is perpendicular to both, and so on. The result is a new set of axes, tailored to the data, in which: - The first few axes capture most of the systematic variation. - Later axes capture less and less, and eventually mostly noise. - All axes are perpendicular, and the sample coordinates on different axes are uncorrelated. The panel below lets you search for PC1 by hand. The points are a mean-centred cloud measured on two variables. Rotate the blue axis and watch how much of the variance it captures. This little experiment shows why Pearson and Hotelling agree. For every sample, the squared distance from the centre splits, by Pythagoras, into a part along the axis and a part perpendicular to it. The total is fixed by the data. So making the captured part as large as possible is exactly the same as making the perpendicular residuals as small as possible. ## Preparing the data: centring and scaling Before computing PCA you must decide how to preprocess the data, and this choice determines what kind of variation PCA will find. Here is the short version; [centring and scaling](/introduction-to-chemometrics/preprocessing/centering-and-scaling) are covered in detail in their own article. ### Mean centring Mean centring subtracts the average of each variable (each column) from every sample: $x_{ij}^{\text{centred}} = x_{ij} - \bar{x}_j$ Without centring, PC1 points roughly towards the average spectrum. It then "explains" a huge share of the total sum of squares, but what it describes is the fact that all samples absorb light, not how they differ. Centring moves the origin to the middle of the cloud so that the components describe differences between samples. For exploratory PCA, it is almost always done. ### Scaling After centring there is a second choice: - **Centring only** (PCA on the covariance matrix). Variables with larger variance have more influence. For spectra, where all variables share the same unit, this is usually what you want: strong, informative bands naturally weigh more than flat regions. - **Autoscaling** (PCA on the correlation matrix). Each variable is also divided by its standard deviation, so every variable has unit variance and the same say. This is essential when the variables have different units (temperature, pH and absorbance in the same table), but for spectra it can backfire, because noisy regions with no chemical information get promoted to the same importance as the real bands. - **Pareto scaling** divides each centred variable by the square root of its standard deviation. It is a compromise between the two, popular in metabolomics: large variables still weigh more, but not as overwhelmingly as without scaling. For spectroscopic data (NIR, IR, Raman, UV-Vis), **mean centring without scaling** is the standard starting point, usually after spectral preprocessing such as scatter correction or derivatives. Autoscale when you mix variables measured in different units, or when you deliberately want small but consistent variables to count as much as large ones. The next panel runs a real PCA on 36 simulated NIR spectra of three products. Switch between the preprocessing options and see how the scores, loadings and scree plot change. ## Computing principal components Now the maths behind the pictures . Start from the centred (and possibly scaled) data matrix $\mathbf{X}$ with $I$ rows (samples) and $J$ columns (variables), as in most chemometrics texts. (Most other entries in this course, such as [data reduction](/introduction-to-chemometrics/preprocessing/data-reduction) and the regression models, use $n$ and $p$ for the same two numbers.) ### The covariance matrix The **covariance matrix** summarises how every pair of variables varies together: $\mathbf{C} = \frac{1}{I-1} \mathbf{X}^T \mathbf{X}$ It is a symmetric $J \times J$ matrix. Its diagonal contains the variance of each variable, and the off-diagonal elements contain the covariances between pairs of variables. Two wavelengths that rise and fall together have a large positive covariance. ### Eigenvectors and eigenvalues An **eigenvector** of a matrix is a direction that the matrix only stretches (or shrinks), without changing it; the stretching factor is the **eigenvalue**. The covariance matrix can be written entirely in terms of its eigenvectors and eigenvalues: $\mathbf{C} = \mathbf{P} \boldsymbol{\Lambda} \mathbf{P}^T$ where: - $\mathbf{P}$ is a $J \times J$ matrix whose columns are the eigenvectors. These are the **loadings**: the directions of the principal components. They are orthonormal, meaning perpendicular to each other and of unit length. - $\boldsymbol{\Lambda} = \text{diag}(\lambda_1, \lambda_2, \ldots, \lambda_J)$ holds the eigenvalues, sorted so that $\lambda_1 \geq \lambda_2 \geq \cdots \geq 0$. The link with the rotation demo is direct: the eigenvalue $\lambda_a$ is exactly the variance of the samples projected onto the eigenvector $\mathbf{p}_a$. The eigenvector with the largest eigenvalue is the direction you found by turning the axis. ### The singular value decomposition In practice, PCA is usually computed with the **singular value decomposition** (SVD) of the data matrix itself: $\mathbf{X} = \mathbf{U} \boldsymbol{\Sigma} \mathbf{V}^T$ where $\mathbf{U}$ and $\mathbf{V}$ have orthonormal columns and $\boldsymbol{\Sigma}$ is diagonal with the **singular values** $\sigma_1 \geq \sigma_2 \geq \cdots \geq 0$. The columns of $\mathbf{V}$ are the loadings, the scores are $\mathbf{U}\boldsymbol{\Sigma}$, and the eigenvalues are $\lambda_a = \sigma_a^2/(I-1)$. Like the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations), forming $\mathbf{X}^T\mathbf{X}$ explicitly can lose numerical precision, so most software works with the SVD directly. The SVD also tells us why PCA is such a good compressor: keeping the first $A$ components gives the best possible approximation of $\mathbf{X}$ by a matrix of rank $A$, in the least squares sense . No other choice of $A$ directions leaves a smaller sum of squared residuals. ### Explained variance Each eigenvalue says how much variance its component captures. The **proportion of variance explained** by component $a$ is $\frac{\lambda_a}{\sum_{k} \lambda_k} \times 100\%$ and the **cumulative** proportion for the first $A$ components is $\frac{\sum_{a=1}^{A} \lambda_a}{\sum_{k} \lambda_k} \times 100\%$ In spectroscopic data, the first few components often explain well over 90% of the variance, and the remaining hundreds of components share the rest, mostly noise. Be careful with these percentages, though: they depend heavily on preprocessing, as the spectra demo shows. ## Scores and loadings Keeping $A$ components, PCA writes the data as $\mathbf{X} = \mathbf{T} \mathbf{P}^T + \mathbf{E}$ where: - $\mathbf{T}$ ($I \times A$) is the **scores** matrix: the new coordinates of each sample. - $\mathbf{P}$ ($J \times A$) is the **loadings** matrix: the directions, one column per component. - $\mathbf{E}$ ($I \times J$) is the **residual** matrix: what is left after $A$ components. This compact equation is the heart of PCA interpretation. Each sample is rebuilt as a weighted sum of a few loading vectors, and the weights are its scores. ### Scores: the map of the samples The scores are the projections of the samples onto the loadings: $\mathbf{T} = \mathbf{X} \mathbf{P}$ Plotting the scores of one component against another gives a **scores plot**, the most important picture in exploratory chemometrics. In a scores plot: - **Samples close together** have similar profiles. - **Clusters** suggest groups with different compositions or origins. - **Gradients**, smooth trends across the plot, suggest a quantity that varies continuously, such as a concentration. - **Isolated points** may be outliers, worth a closer look. - **Ordered paths** of samples measured over time reveal drift or ageing. Colouring the points by known information (class, batch, date, concentration) is the quickest way to see which of these patterns the components are picking up. ### Loadings: the map of the variables The loadings tell you **how each original variable contributes** to each component. For spectra, a loading vector has one value per wavelength and can be plotted like a spectrum. Large positive or negative values mark the wavelengths that drive that component. If the PC1 loading has a large positive peak at 1730 nm, then samples with high PC1 scores absorb more at 1730 nm than the average sample, which you might relate to the C-H overtone of fatty acid chains. Remember that a loading describes *differences from the average* (when the data are centred), and that its sign is arbitrary: flipping the signs of both the loading and the scores gives exactly the same model. ### Biplots A **biplot** shows scores (as points) and loadings (as arrows) in the same figure, so you can see at once which samples are similar and which variables are responsible . Biplots work well with a moderate number of variables, such as a dozen chemical measurements. With hundreds of wavelengths, the arrows turn into a hedgehog, and separate scores and loading plots are clearer. ## How many components? Choosing the number of components is one of the main decisions in PCA. Too few, and real information is lost; too many, and you start modelling noise. ### Scree plot The **scree plot** shows the eigenvalues (or the percentage of explained variance) against the component number. Typically it drops steeply and then levels off. The "elbow" where it flattens suggests how many components to keep . The name, coined by **Raymond Cattell** in 1966, comes from geology: scree is the rubble that piles up at the foot of a cliff, and the flat tail of the plot looks like it. ### Cumulative variance Another simple rule keeps as many components as needed to reach a threshold, such as 95% or 99% of the variance. It is easy to apply but arbitrary: the right threshold depends on how noisy the data are. In the compression panel of the [data reduction](/introduction-to-chemometrics/preprocessing/data-reduction) article, a 99% threshold keeps one component too many and a 95% threshold one too few. ### Cross-validation A more rigorous approach leaves out part of the data, builds the model on the rest, and checks how well the left-out part is reconstructed . While a new component improves the reconstruction of unseen data, it captures structure; when it stops helping, it is fitting noise. Doing this correctly for PCA needs some care: the left-out values must not be used to compute their own scores. The simple "row-wise" scheme of many generic tools, which leaves out whole samples and then projects them onto the model, breaks that rule, and its error keeps falling as components are added. Element-wise schemes, which leave out individual values and predict them from the rest, avoid the problem . Dedicated chemometrics software implements them; general-purpose libraries such as scikit-learn do not. ### Broken stick The **broken stick** rule compares each eigenvalue with what it would be if the total variance were shared at random. If a stick of unit length is broken at random into $J$ pieces, the expected length of the $a$-th longest piece is $b_a = \frac{1}{J} \sum_{k=a}^{J} \frac{1}{k}$ and a component is kept if its share of the total variance exceeds $b_a$. The broken-stick model comes from ecology, where it was used for the relative abundance of species , and was later applied to eigenvalues . In comparisons with other stopping rules it is one of the more reliable . Experienced analysts combine several clues: an elbow in the scree plot, a sensible cumulative variance, loadings that look like spectra rather than noise, and scores that show structure. A component whose loading looks like random noise and whose scores show no pattern is probably modelling noise, whatever its eigenvalue. ## Diagnostics: is each sample described well? Two statistics tell you how each sample relates to the model. **Hotelling's** $T^2$ measures how far a sample lies from the centre *within* the model: $T_i^2 = \sum_{a=1}^{A} \frac{t_{ia}^2}{\lambda_a}$ and the **Q residual** (also called the squared prediction error) measures how much of the sample the model does *not* describe : $Q_i = \sum_{j=1}^{J} e_{ij}^2$ A high $T^2$ flags an extreme but familiar sample; a high $Q$ flags a sample with variation the model has never seen. For $T^2$ a common 95% limit, assuming normally distributed scores, is $T^2_{\text{lim}} = \frac{A\,(I-1)}{I-A}\, F_{0.95}(A,\, I-A)$ where $F_{0.95}$ is the 95th percentile of the F distribution; the limits for $Q$ were derived by Jackson and Mudholkar . Both statistics are explained, with limits and an interactive example, in the article on [outlier detection](/introduction-to-chemometrics/preprocessing/outliers). ## The NIPALS algorithm The SVD computes all components at once. **NIPALS** (nonlinear iterative partial least squares) extracts them one at a time, alternating between two small regressions. It remains useful because you can stop after the components you need, and because it can skip missing values. 1. **Start** Take a column of $\mathbf{X}$ (often the one with the largest variance) as a first guess of the score vector $\mathbf{t}$. 2. **Regress the variables on the scores to get the loading** $\mathbf{p} = \frac{\mathbf{X}^T \mathbf{t}}{\mathbf{t}^T \mathbf{t}}, \qquad \mathbf{p} \leftarrow \frac{\mathbf{p}}{\|\mathbf{p}\|}$ 3. **Regress the samples on the loading to update the scores** $\mathbf{t} = \mathbf{X} \mathbf{p}$ (no division is needed because $\mathbf{p}$ has unit length). 4. **Check convergence** If $\mathbf{t}$ has barely changed since the previous round, continue. Otherwise go back to step 2. 5. **Deflate** Remove what this component explains, $\mathbf{X} \leftarrow \mathbf{X} - \mathbf{t}\mathbf{p}^T$, and return to step 1 for the next component. Each round is a step of the classical power method, so NIPALS converges to the eigenvector with the largest eigenvalue of what remains. Convergence is fast when consecutive eigenvalues are well separated and slow when they are nearly equal. Missing values are handled by leaving them out of the sums in steps 2 and 3, both in the numerator and in the normalising denominator. In step 3 that denominator is then no longer 1, because the observed part of $\mathbf{p}$ is shorter than the whole vector: $p_j = \frac{\sum_{i \in \text{obs}} x_{ij}\, t_i}{\sum_{i \in \text{obs}} t_i^2}, \qquad t_i = \frac{\sum_{j \in \text{obs}} x_{ij}\, p_j}{\sum_{j \in \text{obs}} p_j^2}$ where the sums run only over the values that were actually measured. The article on [missing data](/introduction-to-chemometrics/preprocessing/missing-data) discusses this and other strategies. ## PCA in chemistry ### Exploring spectra The most common use of PCA in chemistry is the first look at a set of spectra or chromatographic profiles. The workflow is almost always the same: preprocess (scatter correction, smoothing, perhaps derivatives), centre, compute PCA, and study the scores and loadings. Typical questions are food authenticity (origin of olive oils or wines, adulteration of honey), pharmaceutical quality (identity of raw materials, consistency between batches), environmental monitoring (sources of pollution) and forensics (comparison of inks, fibres or paints). If the classes separate in the scores plot, PCA has found discriminating information. If they do not, the differences may be small compared to other sources of variation, and a supervised method may be needed. ### Process monitoring PCA is also the basis of **multivariate statistical process control** (MSPC). A PCA model is built from data collected while a process runs normally, and every new measurement is projected onto it. Its $T^2$ and $Q$ values are followed in control charts. When one of them crosses its limit, **contribution plots** show which original variables are responsible, helping operators find the cause. It builds on control limits for the residuals derived by **J. Edward Jackson** and **Govind Mudholkar** in 1979 . The approach was developed in the 1990s by **John MacGregor**, **Theodora Kourti** and colleagues at McMaster University , and in the process chemometrics community by **Barry Wise** and **Neal Gallagher** . It is now a standard tool in the process analytical technology (PAT) framework used in pharmaceutical manufacturing. ### Denoising and compression Because the first components carry the structure and the last ones mostly noise, rebuilding the data from $A$ components gives a cleaner version of it: $\hat{\mathbf{X}} = \mathbf{T}_A \mathbf{P}_A^T$ and storing $\mathbf{T}_A$ and $\mathbf{P}_A$ takes far less space than $\mathbf{X}$ when $A$ is small. The article on [data reduction](/introduction-to-chemometrics/preprocessing/data-reduction) shows this trade-off interactively. ## Code implementation The examples simulate NIR-like spectra of three olive oil cultivars, mean-centre them, and produce a scores plot, a scree plot and a loadings plot. Within each cultivar the composition varies a little from sample to sample, so PCA has real chemical variation to find, not only noise. Finally they compute $T^2$ and $Q$ for a four-component model. ```python from scipy.stats import f from sklearn.decomposition import PCA # Simulate NIR spectra of olive oil samples from 3 cultivars rng = np.random.default_rng(42) n_per_class = 50 n_wavelengths = 500 wavelengths = np.linspace(900, 2500, n_wavelengths) def peak(center, width2): return np.exp(-((wavelengths - center)**2) / width2) # Each cultivar: typical amplitudes and positions of three bands cultivars = { 'Cultivar A': ([0.8, 0.4, 0.30], [1200, 1730, 1940]), 'Cultivar B': ([0.6, 0.6, 0.35], [1210, 1725, 1935]), 'Cultivar C': ([0.7, 0.5, 0.25], [1195, 1735, 1945]), } blocks = [] for amps, centres in cultivars.values(): bands = np.vstack([peak(centres[0], 8000), peak(centres[1], 3000), peak(centres[2], 2000)]) # composition varies from sample to sample (8% around the typical amplitudes) weights = np.array(amps) * (1 + 0.08 * rng.standard_normal((n_per_class, 3))) blocks.append(weights @ bands + rng.normal(0, 0.005, (n_per_class, n_wavelengths))) X = np.vstack(blocks) labels = np.repeat(list(cultivars.keys()), n_per_class) # Mean centring (standard for spectroscopic data). # sklearn's PCA would centre anyway; we do it explicitly to make the step visible. X_centered = X - X.mean(axis=0) pca = PCA(n_components=10) scores = pca.fit_transform(X_centered) loadings = pca.components_.T # (n_wavelengths, n_components) explained = pca.explained_variance_ratio_ * 100 fig, axes = plt.subplots(1, 3, figsize=(16, 4.5)) # Scores plot colors = {'Cultivar A': '#2563eb', 'Cultivar B': '#dc2626', 'Cultivar C': '#059669'} for label, color in colors.items(): mask = labels == label axes[0].scatter(scores[mask, 0], scores[mask, 1], c=color, label=label, alpha=0.7, edgecolors='k', linewidths=0.4) axes[0].set_xlabel(f'PC1 ({explained[0]:.1f}%)') axes[0].set_ylabel(f'PC2 ({explained[1]:.1f}%)') axes[0].set_title('Scores') axes[0].legend() # Scree plot comps = np.arange(1, 11) axes[1].bar(comps, explained, color='steelblue') axes[1].plot(comps, np.cumsum(explained), 'o-', color='#d97706', label='Cumulative') axes[1].set_xlabel('Principal component') axes[1].set_ylabel('Explained variance (%)') axes[1].set_title('Scree plot') axes[1].legend() # Loadings axes[2].plot(wavelengths, loadings[:, 0], label='PC1') axes[2].plot(wavelengths, loadings[:, 1], label='PC2') axes[2].axhline(0, color='gray', linewidth=0.5) axes[2].set_xlabel('Wavelength (nm)') axes[2].set_ylabel('Loading') axes[2].set_title('Loadings') axes[2].legend() plt.tight_layout() plt.show() # Hotelling's T² and Q residuals for a 4-component model (the elbow of the scree plot) A = 4 I = X.shape[0] T2 = np.sum(scores[:, :A]**2 / pca.explained_variance_[:A], axis=1) T2_lim = A * (I - 1) / (I - A) * f.ppf(0.95, A, I - A) # 95% limit E = X_centered - scores[:, :A] @ pca.components_[:A, :] Q = np.sum(E**2, axis=1) print(f"Variance explained (%): {np.round(explained[:5], 1)}") print(f"Cumulative variance ({A} PCs): {explained[:A].sum():.1f}%") print(f"T² limit (95%): {T2_lim:.2f}; samples above it: {np.sum(T2 > T2_lim)} of {I}") print(f"Q range: {Q.min():.4f} to {Q.max():.4f}") ``` ```matlab % Simulate NIR spectra of olive oil samples from 3 cultivars rng(42); n_per_class = 50; n_wavelengths = 500; wavelengths = linspace(900, 2500, n_wavelengths); peak = @(c, w2) exp(-((wavelengths - c).^2) / w2); % Each cultivar: typical amplitudes and positions of three bands amps = [0.8 0.4 0.30; 0.6 0.6 0.35; 0.7 0.5 0.25]; centres = [1200 1730 1940; 1210 1725 1935; 1195 1735 1945]; X = []; for k = 1:3 bands = [peak(centres(k, 1), 8000); peak(centres(k, 2), 3000); peak(centres(k, 3), 2000)]; % composition varies from sample to sample (8% around the typical amplitudes) weights = amps(k, :) .* (1 + 0.08 * randn(n_per_class, 3)); X = [X; weights * bands + 0.005 * randn(n_per_class, n_wavelengths)]; end labels = repelem((1:3)', n_per_class); % pca() centres the data by default and uses the SVD [coeff, score, latent, ~, explained] = pca(X); figure('Position', [100 100 1500 420]); subplot(1, 3, 1); gscatter(score(:, 1), score(:, 2), labels, [0.15 0.39 0.92; 0.86 0.15 0.15; 0.02 0.59 0.40]); xlabel(sprintf('PC1 (%.1f%%)', explained(1))); ylabel(sprintf('PC2 (%.1f%%)', explained(2))); legend('Cultivar A', 'Cultivar B', 'Cultivar C'); title('Scores'); grid on; subplot(1, 3, 2); bar(1:10, explained(1:10)); hold on; plot(1:10, cumsum(explained(1:10)), 'o-', 'LineWidth', 1.5); xlabel('Principal component'); ylabel('Explained variance (%)'); title('Scree plot'); grid on; subplot(1, 3, 3); plot(wavelengths, coeff(:, 1:2), 'LineWidth', 1.5); yline(0, 'Color', [0.5 0.5 0.5]); xlabel('Wavelength (nm)'); ylabel('Loading'); legend('PC1', 'PC2'); title('Loadings'); grid on; % Hotelling's T² and Q residuals for a 4-component model (the elbow of the scree plot) A = 4; I = size(X, 1); T2 = sum(score(:, 1:A).^2 ./ latent(1:A)', 2); T2_lim = A * (I - 1) / (I - A) * finv(0.95, A, I - A); % 95% limit E = (X - mean(X)) - score(:, 1:A) * coeff(:, 1:A)'; Q = sum(E.^2, 2); fprintf('Variance explained (%%): %s\n', mat2str(round(explained(1:5)', 1))); fprintf('Cumulative variance (%d PCs): %.1f%%\n', A, sum(explained(1:A))); fprintf('T² limit (95%%): %.2f; samples above it: %d of %d\n', T2_lim, sum(T2 > T2_lim), I); fprintf('Q range: %.4f to %.4f\n', min(Q), max(Q)); ``` ```r # Simulate NIR spectra of olive oil samples from 3 cultivars set.seed(42) n_per_class T2_lim), I)) cat(sprintf("Q range: %.4f to %.4f\n", min(Q), max(Q))) ``` In Python the first components explain about 76.5%, 13.2%, 7.4% and 1.1% of the variance, and then the scree plot falls flat: four components carry the chemistry, and the rest is noise (the other languages draw different random numbers, so their percentages differ slightly). With four components, $Q$ is about the same for every sample, close to $500 \times 0.005^2 = 0.0125$, the sum of squares of the simulated noise over 500 wavelengths: what is left really is noise. Only a few samples exceed the 95% $T^2$ limit, fewer than the 5% (7 or 8 samples) expected by chance, so none stands out as an outlier. ## When to use PCA, and when not PCA is the right first tool when you want to: - **Explore** a new data set: see groups, trends and outliers before any modelling. - **Compress** many correlated variables into a few uncorrelated scores. - **Reduce noise** by rebuilding the data from the first components. - **Monitor** a process against a model of normal behaviour with $T^2$ and $Q$. - **Prepare** for regression or classification, for example in principal component regression. It is not the right tool when: - **You need to predict a property.** PCA ignores the response; the direction of largest variance is not necessarily the most predictive. Use PCR or, usually better, PLS. - **You need to separate known classes.** PCA maximises variance, not class separation. If classes overlap in the scores plot, consider supervised methods such as PLS-DA (partial least squares discriminant analysis, outlined in the [PLS article](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression)) or LDA (linear discriminant analysis), which look for the directions that best separate known classes. - **The structure is strongly nonlinear.** PCA finds straight directions; data that lie on a curved surface may need nonlinear methods. - **You have very few samples.** With a handful of samples the components can describe noise. Cross-validation becomes essential. Signs of components are arbitrary, so the same model can appear mirrored in different programs. Results depend strongly on preprocessing. And a large explained variance is not the same as a useful component: a big baseline offset can dominate the variance while carrying no chemical information. ## What to remember PCA rotates the coordinate system so that the first axis follows the largest variation in the samples, the second the largest remaining variation, and so on. The scores tell you where each sample lies on those axes; the loadings tell you which variables build each axis. Looking for the largest variance and looking for the closest-fitting line are the same thing, which is why PCA is both a visualisation tool and the best low-rank approximation of the data. From here, several methods build on PCA: - **[Factor analysis](/introduction-to-chemometrics/models/exploratory-models/factor-analysis)** adds an explicit model of latent factors and noise, and uses rotation to make the factors easier to interpret. - **[Clustering](/introduction-to-chemometrics/models/exploratory-models/clustering)** assigns samples to groups algorithmically, often working on PCA scores. - **[Principal component regression](/introduction-to-chemometrics/models/regression-models/principal-component-regression)** regresses a response on the PCA scores. - **[Partial least squares](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression)** builds components that are guided by the response, which makes them better suited to prediction. ## References # LASSO Regression (/introduction-to-chemometrics/models/regression-models/lasso-regression) [Ridge regression](/introduction-to-chemometrics/models/regression-models/ridge-regression) tames unstable coefficients by gently pulling all of them towards zero. It works, but it never lets go of any variable: a model built on 1000 wavelengths still has 1000 non-zero coefficients. Chemists often want to ask a sharper question: > **Which wavelengths actually carry the information about my analyte?** The **lasso** answers it with a surprisingly small change to ridge regression. Replace the sum of *squared* coefficients in the penalty by the sum of their *absolute values*, and some coefficients stop shrinking gradually: they become exactly zero. The model selects variables while it fits. ## A short history In 1996, **Robert Tibshirani**, then at the University of Toronto, published *Regression Shrinkage and Selection via the Lasso* in the *Journal of the Royal Statistical Society* . The name is an acronym, **L**east **A**bsolute **S**hrinkage and **S**election **O**perator, and it describes the method well: it shrinks coefficients with an absolute-value penalty and, in doing so, selects variables. Tibshirani had trained at Stanford under **Bradley Efron**, and his work belongs to a period in which statisticians were increasingly facing datasets with more variables than observations. The ingredients were not entirely new: ridge regression had shown in 1970 that penalizing coefficients stabilizes ill-conditioned problems , and geophysicists had already used L1 penalties to obtain sparse solutions in seismic deconvolution . Frank and Friedman had proposed **bridge regression**, a family of penalties on $\sum_j |\beta_j|^q$ that includes both ridge ($q = 2$) and the absolute-value penalty ($q = 1$) . Most directly, Breiman's **non-negative garrote** (1995), which Tibshirani took as his starting point, had already shown that a constrained least squares fit could shrink some coefficients to exactly zero . Tibshirani's contribution was to formulate the idea cleanly for regression, explain why it produces exact zeros, and show how well it predicts. The lasso became truly practical a few years later, with the LARS algorithm, which computes the whole solution path at the cost of roughly one least squares fit in favourable cases , and with fast coordinate descent methods, which are what most software uses today . ## The variable selection problem Consider a near-infrared calibration: 200 tablets, spectra recorded at 1000 wavelengths, and the content of the active ingredient as the response. The data matrix $\mathbf{X}$ has 200 rows and 1000 columns. [Least squares](/introduction-to-chemometrics/models/regression-models/multiple-linear-regression) cannot even produce a unique answer: with more unknowns than equations, infinitely many coefficient vectors fit the calibration data perfectly. Ridge regression, PCR and PLS all solve that problem, but in ways that keep every wavelength in the model: ridge with small coefficients, PCR and PLS through components that are combinations of all wavelengths. None of them hands you a short list such as "these 15 wavelengths are the ones that matter". A short list is valuable. It can be checked against known absorption bands, it is easier to explain, and it tells an instrument designer which few wavelengths a cheap dedicated sensor would need. This is where the lasso enters. ## The lasso objective The lasso minimizes the usual squared error plus a penalty on the absolute values of the coefficients: $\min_{\boldsymbol{\beta}} \left\{ \frac{1}{2n} \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|_2^2 + \lambda \|\boldsymbol{\beta}\|_1 \right\}, \qquad \|\boldsymbol{\beta}\|_1 = \sum_{j=1}^{p} |\beta_j|$ - The first term, the **data fidelity**, is the mean squared residual (with a convenient factor of one half): it wants the model to fit. - The second term, the **L1 penalty**, adds up the absolute values of the coefficients: it wants them small, and as we will see, it is happy to make them exactly zero. The tuning parameter $\lambda \geq 0$ sets the balance. With $\lambda = 0$ you are back to least squares. As $\lambda$ grows, coefficients shrink and drop out one after another, until at a large enough $\lambda$ every coefficient is zero and the model predicts the mean of $\mathbf{y}$. As in ridge regression, the intercept is not penalized, and the predictors are standardized first so that the penalty treats them fairly. The smallest $\lambda$ for which every coefficient is zero has a simple formula. With centred $\mathbf{y}$ and standardized columns $\mathbf{x}_j$, it is $\lambda_{\max} = \frac{1}{n}\max_j \left|\mathbf{x}_j^T\mathbf{y}\right|$ the largest covariance between a predictor and the response: below it, the data pull on that predictor harder than the penalty holds it at zero. Software starts the path there and works downwards. It is a convention used by popular software (glmnet, scikit-learn). The 2 cancels when you differentiate, and dividing by $n$ keeps the size of a sensible $\lambda$ roughly independent of the number of samples. Other texts write the objective without it. The solutions are the same; only the numerical scale of $\lambda$ changes, so be careful when comparing values between programs. Beware of names too: scikit-learn calls $\lambda$ `alpha` and the elastic net mixing parameter `l1_ratio`, while glmnet in R calls $\lambda$ `lambda` and the mixing parameter `alpha`. ## Why the L1 penalty gives exact zeros This is the heart of the method, and there are two complementary ways to see it. ### The picture: a diamond with corners Just as for ridge regression, the penalized problem is equivalent to a constrained one: $\min_{\boldsymbol{\beta}} \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|_2^2 \quad \text{subject to} \quad \sum_{j=1}^{p} |\beta_j| \leq t$ With two coefficients, the ridge budget $\beta_1^2 + \beta_2^2 \leq t^2$ is a disc, but the lasso budget $|\beta_1| + |\beta_2| \leq t$ is a **diamond** whose corners lie on the axes. The contours of the squared error are ellipses around the least squares estimate, and the solution is the first point where an ellipse touches the allowed region. Drag the least squares point around (or move it with the sliders). Over a large part of the plane, the first contact is a corner of the diamond, where one coefficient is exactly zero. A disc has no corners, so with ridge the contact point essentially never sits on an axis: tick "Show both shapes" to see the two solutions for the same budget side by side. In higher dimensions the L1 ball has corners, edges and faces lying in coordinate planes, so many coefficients can be zero at once. ### The calculus: a push that does not fade The second explanation looks at the penalty one coefficient at a time. The ridge penalty $\beta_j^2$ has slope $2\beta_j$: its pull towards zero gets weaker and weaker as the coefficient gets smaller, so it never quite reaches zero. The lasso penalty $|\beta_j|$ has slope $\pm 1$ everywhere except at zero: it pulls with the **same force** no matter how small the coefficient already is. If the data push a coefficient away from zero less strongly than $\lambda$ pulls it back, the coefficient stays exactly at zero. The cleanest case is when the predictors are uncorrelated and standardized, so that $\mathbf{X}^T\mathbf{X} = n\mathbf{I}$. Then each coefficient can be computed separately from its least squares value $\hat{\beta}_j$: $\hat{\beta}_j^{\text{ridge}} = \frac{\hat{\beta}_j}{1 + \lambda}, \qquad \hat{\beta}_j^{\text{lasso}} = \text{sign}(\hat{\beta}_j)\,\max\left(|\hat{\beta}_j| - \lambda,\ 0\right)$ (for ridge written with the same $1/(2n)$ scaling, $\frac{1}{2n}\|\mathbf{y}-\mathbf{X}\boldsymbol{\beta}\|^2 + \frac{\lambda}{2}\|\boldsymbol{\beta}\|^2$). Ridge divides every coefficient by the same factor, so nothing becomes zero. The lasso subtracts $\lambda$ from the size of every coefficient and sets to zero those that would cross it. This operation is called **soft thresholding**: small coefficients are removed, large ones survive but are shifted towards zero. (Hard thresholding, which keeps the large coefficients untouched, corresponds, in this orthogonal case, to best subset selection.) ### How it is computed Because of the absolute values, the lasso has no closed-form solution in general. The most common algorithm, **coordinate descent**, simply cycles through the coefficients, updating each one by a soft-thresholding step while the others are held fixed, until nothing changes . For standardized predictors the update of coefficient $j$ is: $\beta_j \leftarrow \frac{S\!\left(\frac{1}{n}\mathbf{x}_j^T \mathbf{r}_{(j)},\ \lambda\right)}{\frac{1}{n}\mathbf{x}_j^T\mathbf{x}_j}, \qquad S(z, \lambda) = \text{sign}(z)\max(|z| - \lambda, 0)$ where $\mathbf{r}_{(j)}$ is the residual computed without the contribution of predictor $j$. Like [gradient descent](/introduction-to-chemometrics/least-squares/gradient-descent), it is an iterative method: start somewhere, improve, repeat. The demos on this page use exactly this algorithm. ## The solution path As $\lambda$ decreases from a large value to zero, the lasso solution traces a **regularization path**: 1. **Very large λ.** Every coefficient is zero; the model predicts the mean. 2. **Slightly smaller λ.** The first variable enters: the one most correlated with the response. 3. **Smaller still.** More variables enter, usually one at a time. Coefficients already in the model keep adjusting, and occasionally one leaves again. 4. **λ close to zero.** If there are more samples than variables, the path ends at the least squares solution. Otherwise it ends at a solution that fits the calibration data exactly, with at most $n$ non-zero coefficients. Between the points where variables enter or leave, each coefficient changes linearly with $\lambda$: the lasso path is **piecewise linear**, a property that the LARS algorithm exploits . On a logarithmic $\lambda$ axis, which is how paths are usually drawn, those straight pieces look curved; the demo lets you switch to a linear axis to see them. Explore the path for a simulated NIR calibration with ten wavelengths, and switch to ridge to compare. A few things to notice. The analyte band (1730 nm, then 1715 nm) enters first. Some wavelengths with real but redundant information enter later and are pushed out again when a correlated neighbour takes over. At the cross-validation optimum the model uses only five of the ten wavelengths, with a prediction error (about 0.91) close to that of ridge (about 0.84), which keeps all ten. Note that the two methods use different scales for $\lambda$ (the lasso objective has the $1/(2n)$ factor, the ridge objective does not), so compare shapes and errors, not $\lambda$ values. Ask whether the first wavelengths to enter correspond to known absorption bands of the analyte. If the first selections have no chemical meaning, the model may be fitting an interferent that happens to correlate with the analyte in your calibration set, or simply noise. ## Choosing λ As for ridge regression, $\lambda$ is chosen by **cross-validation**: 1. Split the data into $k$ folds (typically 5 or 10). 2. For each candidate $\lambda$, fit the lasso on $k - 1$ folds and predict the remaining fold. 3. Average the squared prediction errors over the folds. 4. Choose $\lambda$. Software usually reports two choices: - $\lambda_{\min}$, the value that minimizes the cross-validated error; - $\lambda_{1\text{SE}}$, the largest $\lambda$ whose error is within one standard error of the minimum. It gives a sparser model that predicts almost as well, and is often preferred. Random folds assume that samples are independent. If your samples come in batches, days or production campaigns, leave whole batches out. Otherwise near-duplicate samples end up on both sides of the split and the error estimate is too optimistic, for the lasso as for any calibration method. ## Lasso and ridge side by side | Property | Ridge (L2) | Lasso (L1) | |----------|-----------|------------| | **Penalty** | Sum of squared coefficients | Sum of absolute coefficients | | **Constraint region** | Disc / sphere | Diamond / cross-polytope | | **Exact zeros** | No, all coefficients shrink | Yes, many coefficients exactly zero | | **Groups of correlated variables** | Similar coefficients for the whole group | Tends to pick one and drop the rest | | **Number of selected variables** | Always $p$ | At most $\min(n, p)$ ($n - 1$ when n" /> and an intercept is fitted) | | **Uniqueness** | Always unique for 0" /> | May not be unique when n" /> | | **Computation** | Closed-form solution | Iterative (coordinate descent, LARS) | | **Bayesian prior** | Gaussian | Laplace (double exponential) | The last row deserves a sentence. Ridge regression is the most probable solution under a Gaussian prior on the coefficients; the lasso is the most probable solution (the posterior mode) under a **Laplace prior**, which has a sharp peak at zero and heavier tails . The sharp peak is the probabilistic version of the diamond's corners. **Ridge tends to win** when many variables carry small pieces of real information, which is often the case for broad spectral bands, and when groups of variables are strongly correlated. Faced with a group of nearly identical wavelengths, the lasso picks one more or less arbitrarily, and a slightly different dataset may make it pick another. **The lasso tends to win** when only a few variables truly matter, when a compact and interpretable model is the goal, or when you need to know which wavelengths to measure. ### The elastic net compromise When you want sparsity but your variables come in correlated groups, the **elastic net** mixes the two penalties : $\min_{\boldsymbol{\beta}} \left\{ \frac{1}{2n} \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|_2^2 + \lambda \left[ \alpha \|\boldsymbol{\beta}\|_1 + \frac{1 - \alpha}{2} \|\boldsymbol{\beta}\|_2^2 \right] \right\}$ The mixing parameter $\alpha \in [0, 1]$ moves between ridge ($\alpha = 0$) and lasso ($\alpha = 1$). Intermediate values keep the ability to produce zeros while encouraging correlated variables to enter or leave the model together, the so-called grouping effect. ## Code implementation The examples simulate a dataset with 500 correlated "wavelengths" of which only 10 are truly relevant, fit the lasso with cross-validation, and compare the selected variables with the true ones. ```python from sklearn.linear_model import LassoCV, lasso_path from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline # Simulate data: 200 samples, 500 correlated variables, only 10 truly relevant rng = np.random.default_rng(42) n_samples, n_wavelengths = 200, 500 X = rng.standard_normal((n_samples, n_wavelengths)) for j in range(1, n_wavelengths): # correlate neighbouring variables X[:, j] = 0.7 * X[:, j - 1] + 0.3 * X[:, j] true_beta = np.zeros(n_wavelengths) relevant_idx = [45, 102, 150, 203, 255, 310, 348, 400, 425, 470] true_beta[relevant_idx] = rng.standard_normal(len(relevant_idx)) * 2 y = X @ true_beta + rng.standard_normal(n_samples) * 0.5 # --- Lasso with 10-fold cross-validation --- # scikit-learn's Lasso minimizes 1/(2n) ||y - Xb||^2 + alpha ||b||_1, so alpha is our lambda. # The scaler sits inside the pipeline, so each fold is standardized with its own training data. model = make_pipeline(StandardScaler(), LassoCV(cv=10, max_iter=10000, random_state=42)).fit(X, y) lasso_cv = model[-1] # lambda_1SE: the largest lambda whose CV error is within one standard error of the minimum mse_mean = lasso_cv.mse_path_.mean(axis=1) mse_se = lasso_cv.mse_path_.std(axis=1) / np.sqrt(lasso_cv.mse_path_.shape[1]) i_min = mse_mean.argmin() lambda_1se = lasso_cv.alphas_[mse_mean : variables chosen in a large fraction of the runs are robust; those selected only occasionally are probably noise. **Beware of the grouping effect.** Neighbouring wavelengths are correlated, so the lasso tends to keep one representative of each band and drop the others. The selected wavelength is a representative of a region, not necessarily *the* important wavelength. **Compare against PLS.** PLS is the reference method for spectroscopic calibration. The lasso is most valuable when it predicts about as well as PLS while also offering an interpretable selection. ## What to remember The lasso is least squares with an absolute-value penalty. Its diamond-shaped constraint region has corners on the axes, and its penalty pushes with a force that does not fade near zero; both are ways of saying that some coefficients become exactly zero. That turns a regression method into a variable selection method, controlled by a single parameter chosen by cross-validation. Its weakness is the flip side of its strength: with groups of highly correlated variables, which is exactly what spectra are, the choice of which variable to keep can be fragile. Several extensions address this: - **Elastic net** mixes L1 and L2 penalties to select correlated variables together . - **Group lasso** selects or discards predefined groups, such as whole spectral regions . - **Adaptive lasso** uses data-dependent weights to make the selection more reliable . - **Sparse PLS** combines the latent variables of [PLS](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression) with L1 penalties, directly targeting the chemometric setting . For a thorough modern treatment of sparse methods, see Hastie, Tibshirani and Wainwright . ## References # Linear Regression (/introduction-to-chemometrics/models/regression-models/linear-regression) If you have ever prepared a set of standards, measured them, and drawn a straight line through the points, you have already built a **linear regression model**. It is probably the most used model in analytical chemistry, and it is also the seed from which almost every regression method in chemometrics grows. In this entry we will look at it carefully: where the name comes from, what the model really says, how the best line is found, how to judge whether the line is any good, and where it quietly fails. Along the way you will be able to drag calibration points around and see the least squares machinery react in real time. ## A short story of a strange word The word *regression* entered statistics through an unlikely door: the study of human height. In 1886, the English polymath **Francis Galton** published *Regression towards Mediocrity in Hereditary Stature* , based on the heights of 928 adult children from 205 families and their parents. He noticed that very tall parents tended to have tall children, but not quite as tall as themselves, and very short parents tended to have short children, but not quite as short. The children's heights "regressed" towards the population average. Galton drew a straight line through that cloud of parent and child heights, and the name stuck to the line, even though today it has nothing to do with going backwards. Galton himself introduced the idea of *correlation* in 1888 , and his colleague **Karl Pearson** gave it its mathematical form in 1896, including the product-moment formula for the correlation coefficient $r$ that we still use . The engine underneath, the **method of least squares**, is even older and comes from astronomy. **Adrien-Marie Legendre** published it in 1805 while computing the orbits of comets , and **Carl Friedrich Gauss** gave it a probabilistic justification in 1809 . Gauss also claimed he had been using the method since 1795, which started one of the most famous priority disputes in the history of science . For chemists, the decisive step came from the laws of light absorption. The Beer-Lambert law says that absorbance is proportional to concentration. Bouguer (1729) and Lambert (1760) had described how absorption depends on the thickness of the sample; Beer added the concentration in 1852 . That proportionality turned a straight line into the natural way of converting an instrument reading into an amount of substance: the **calibration curve**. ## The calibration problem Suppose you need to determine iron in drinking water. You complex Fe(II) with 1,10-phenanthroline, which gives an orange-red complex that absorbs strongly near 510 nm, and you measure absorbance in a UV-Vis spectrophotometer. Beer-Lambert tells you that absorbance should be proportional to concentration, but you do not know the exact proportionality constant for your instrument, cuvette and reagent batch. You need a calibration. 1. **Prepare standards** Make a few solutions with known iron concentrations, for example 0, 0.5, 1, 2, 3 and 4 mg/L. 2. **Measure each standard** Record the absorbance of each solution at 510 nm. 3. **Fit a line** Find the straight line that best describes absorbance as a function of concentration. 4. **Predict unknowns** Measure your water samples and use the fitted line, read backwards, to obtain their concentrations. Step 3 is the linear regression. The question is what "best" means, and the answer is the same criterion we derived in [your first step into chemometrics](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics): the line that minimizes the sum of the squared vertical distances between the points and the line. ## The model The simple linear regression model relates a response $y$ to a single predictor $x$: $y_i = \beta_0 + \beta_1 x_i + \varepsilon_i, \qquad i = 1, 2, \ldots, n$ Every symbol has a concrete meaning in the lab: - $y_i$ is the measured response of the $i$-th standard (the absorbance). - $x_i$ is the known value of the predictor (the iron concentration). - $\beta_0$ is the **intercept**, the response expected when $x = 0$. Ideally it is zero for a blank, but it is often slightly off because of reagent absorption, stray light or a detector offset. - $\beta_1$ is the **slope**, how much the response changes per unit of concentration. Analysts call it the **sensitivity** of the method. - $\varepsilon_i$ is the **error term**: the part of the measurement that the straight line cannot explain, mostly random noise. The error term is not a flaw in the model, it is a statement about reality. No instrument is perfectly repeatable and no standard is perfectly prepared. Writing $\varepsilon$ explicitly says what every experimentalist knows: there is always some noise, and our job is to recover the underlying relationship despite it. Notice a subtle but important detail: the model puts the noise in $y$ only. It assumes that the concentrations of the standards are known much more precisely than the absorbances are measured. In a calibration with carefully prepared standards this is usually true, and it is the reason why the residuals are measured vertically. ## Finding the best line For any candidate line, the **residual** of the $i$-th point is the vertical gap between what we measured and what the line predicts: $e_i = y_i - (\beta_0 + \beta_1 x_i)$ Least squares chooses the intercept and slope that make the sum of squared residuals as small as possible: $S(\beta_0, \beta_1) = \sum_{i=1}^{n} e_i^2 = \sum_{i=1}^{n} \left( y_i - \beta_0 - \beta_1 x_i \right)^2$ Try it below with the six iron standards used in the examples later in this entry. Drag the standards and watch the fitted line, the residuals, and the quality numbers update. Switch on the squared residuals to see the quantity that is being minimized, and then try to beat least squares with your own line. You probably noticed two things. First, you can never get a smaller sum of squares than the least squares line (that is the whole point of it). Second, one badly placed standard produces a huge square, so it pulls the line strongly towards itself. Squares punish large errors much more than small ones, which is both the strength and the weakness of the method. ### The closed-form solution To find the minimum, we take the partial derivatives of $S$ with respect to both parameters and set them to zero, exactly as in the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations) entry: $\frac{\partial S}{\partial \beta_0} = -2 \sum_{i=1}^{n} (y_i - \beta_0 - \beta_1 x_i) = 0$ $\frac{\partial S}{\partial \beta_1} = -2 \sum_{i=1}^{n} x_i(y_i - \beta_0 - \beta_1 x_i) = 0$ These two equations are the normal equations for a straight line. Solving them gives: $\hat{\beta}_1 = \frac{\sum_{i=1}^{n} (x_i - \bar{x})(y_i - \bar{y})}{\sum_{i=1}^{n} (x_i - \bar{x})^2}$ $\hat{\beta}_0 = \bar{y} - \hat{\beta}_1 \bar{x}$ where $\bar{x}$ and $\bar{y}$ are the averages of the concentrations and the responses. The hat on $\hat{\beta}$ reminds us that these are estimates computed from data, not the true (unknown) values. The slope formula has a friendly reading. The numerator measures how $x$ and $y$ vary *together* (it is proportional to their covariance); the denominator measures how much $x$ varies on its own (proportional to its variance). If both go up together, the slope is positive. The second equation tells us something nice too: the least squares line always passes through the centre of the data, the point $(\bar{x}, \bar{y})$. Squaring the residuals is not only a matter of convenience. If the errors are independent, Gaussian and have the same variance, the least squares line is the **maximum likelihood** line: the one that makes the data you measured the most probable outcome. We derived this step by step in [your first step into chemometrics](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics). ## Is the line any good? A fitted line always exists, even through a cloud of random points. So after fitting, we need numbers and, above all, pictures that tell us whether the model describes the data. ### The coefficient of determination The coefficient of determination $R^2$ measures the fraction of the variation in $y$ that the line explains: $R^2 = 1 - \frac{SS_{\text{res}}}{SS_{\text{tot}}} = 1 - \frac{\sum_{i=1}^{n}(y_i - \hat{y}_i)^2}{\sum_{i=1}^{n}(y_i - \bar{y})^2}$ Here $SS_{\text{res}}$ (the **residual sum of squares**) is the variation left over after fitting, and $SS_{\text{tot}}$ (the **total sum of squares**) is the variation of the responses around their mean, the variation you would have if you ignored $x$ entirely. For a straight line with an intercept, $R^2$ lies between 0 and 1, and it equals the square of Pearson's correlation coefficient $r$. A well-behaved Beer-Lambert calibration easily gives 0.999" />. That sounds reassuring, but it says surprisingly little, as the next demo shows. ### Root mean square error $R^2$ has no units. The **root mean square error** (RMSE) gives the typical size of a residual in the units of the response: $\text{RMSE} = \sqrt{\frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2}$ An RMSE of 0.002 AU means that the standards typically sit about 0.002 absorbance units away from the line. Dividing by the slope converts it into concentration units, which is the number your clients actually care about. Classical calibration statistics divide by $n - 2$ instead of $n$, because two parameters were estimated from the same data. The result, often written $s_{y/x}$, is the residual standard deviation used to compute confidence intervals for the slope, the intercept and the predicted concentrations . With many standards the difference is negligible; with six standards it is not. ### How precise is the predicted concentration? The number a client receives is not the absorbance but the concentration read backwards through the line, and it inherits the uncertainty of both the line and the new measurement. If the unknown is measured $m$ times with mean response $y_0$, the standard deviation of its predicted concentration $\hat{x}_0$ is, to a good approximation : $s_{x_0} = \frac{s_{y/x}}{\hat{\beta}_1}\sqrt{\frac{1}{m} + \frac{1}{n} + \frac{(y_0 - \bar{y})^2}{\hat{\beta}_1^2 \sum_{i=1}^{n} (x_i - \bar{x})^2}}$ The three terms under the root have a plain reading: the noise of the new measurement (smaller with more replicates), the uncertainty of the line's height (smaller with more standards), and the uncertainty of its slope, which grows as $y_0$ moves away from the centre of the calibration. The line is best known at $(\bar{x}, \bar{y})$ and less and less well towards the ends of the range, which is one more argument against extrapolation. For the iron calibration in the examples below ($s_{y/x} = 0.0026$ AU) and a single measurement of $y_0 = 0.370$, this gives $s_{x_0} = 0.014$ mg/L. Multiplying by Student's $t$ for $n - 2 = 4$ degrees of freedom (2.78 at 95 %) gives the confidence interval: $c = 1.85 \pm 0.04$ mg/L. A related figure is the **limit of detection** (LOD), the smallest concentration that can be told apart from a blank; a common estimate from the calibration is $3.3\, s_{y/x} / \hat{\beta}_1$, here about 0.044 mg/L. ### Look at the residuals Numbers alone are not enough. The single most informative diagnostic is the **residual plot**: the residuals $e_i = y_i - \hat{y}_i$ plotted against the fitted values (or against $x$). If the model is adequate, the residuals look like random noise: no trend, no curvature, no funnel. The demo below shows five calibrations. All of them have $R^2$ above 0.99, and all of them would pass a quick glance at the line. Only the residual plot tells them apart, and for the last one only if the residuals are plotted in the order the standards were measured. This is the same lesson as **Anscombe's quartet** , four small datasets with practically identical means, variances, correlation and regression line, but completely different shapes. Always plot your data and your residuals. What the patterns usually mean: - **A curve** (a smile or a frown) means the relationship is not linear over the range. Narrow the calibration range, add a quadratic term, or use a nonlinear model. - **A funnel** means the noise grows with the signal (heteroscedasticity). The line can still be fine, but its uncertainties are wrong. Weighted least squares is the usual fix. - **Runs** of positive and negative residuals in measurement order suggest drift or a missing variable. - **One isolated point** far from zero flags a possible outlier. Investigate it before deleting it. ## From one predictor to matrix form Simple regression handles a single predictor. But the same model can be written for any number of predictors, and the compact way to do it is with matrices. Stack all the measurements into a vector and the concentrations, together with a column of ones for the intercept, into a matrix: $\mathbf{y} = \mathbf{X}\boldsymbol{\beta} + \boldsymbol{\varepsilon}, \qquad \mathbf{X} = \begin{bmatrix} 1 & x_1 \\ 1 & x_2 \\ \vdots & \vdots \\ 1 & x_n \end{bmatrix}, \qquad \boldsymbol{\beta} = \begin{bmatrix} \beta_0 \\ \beta_1 \end{bmatrix}$ $\mathbf{X}$ is called the **design matrix**: one row per sample, one column per term of the model. The least squares solution is then the familiar [normal-equation formula](/introduction-to-chemometrics/least-squares/normal-equations): $\hat{\boldsymbol{\beta}} = (\mathbf{X}^T \mathbf{X})^{-1} \mathbf{X}^T \mathbf{y}$ This single expression reproduces the two summation formulas above, and it works unchanged when $\mathbf{X}$ has more columns. That is the door to [multiple linear regression](/introduction-to-chemometrics/models/regression-models/multiple-linear-regression), and from there to [ridge](/introduction-to-chemometrics/models/regression-models/ridge-regression), [PCR](/introduction-to-chemometrics/models/regression-models/principal-component-regression) and [PLS](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression). The derivation, and the numerical cautions that come with it, are in the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations) entry. $\mathbf{X}^T\mathbf{X}$ can be inverted only if no column of $\mathbf{X}$ is an exact linear combination of the others. With one predictor this just means that the standards must not all have the same concentration. With hundreds of correlated wavelengths it becomes the central difficulty of spectroscopic calibration. ## Three everyday examples **Scenario:** iron in drinking water as the Fe(II)-phenanthroline complex, measured at 510 nm in a 1 cm cell. | Concentration (mg/L) | Absorbance (AU) | |:---:|:---:| | 0.0 | 0.004 | | 0.5 | 0.101 | | 1.0 | 0.199 | | 2.0 | 0.402 | | 3.0 | 0.594 | | 4.0 | 0.797 | Fitting $A = \beta_0 + \beta_1 c$ gives: - $\hat{\beta}_0 = 0.0026$ AU (a small blank signal) - $\hat{\beta}_1 = 0.1982$ AU per mg/L (the sensitivity) - $R^2 = 0.99994$ A water sample with absorbance 0.370 is then read backwards through the line: $c = (0.370 - 0.0026) / 0.1982 = 1.85$ mg/L. **Scenario:** a polycyclic aromatic hydrocarbon in river water by fluorescence. Fluorescence intensity is proportional to concentration only at low concentrations. At higher levels the sample starts to absorb its own excitation and emission light (the *inner filter effect*), and the calibration bends. A typical outcome, for example in a simulation, is a calibration that looks excellent between 0 and 25 ng/mL and visibly curved up to 50 ng/mL, while $R^2$ drops only from about 0.999 to about 0.98. The lesson: a respectable $R^2$ does not certify the linear range. The residual plot shows the curvature at once. **Scenario:** checking a rapid moisture analyser against the reference oven-drying method on 20 grain samples. $\text{Moisture}_{\text{analyser}} = \beta_0 + \beta_1 \cdot \text{Moisture}_{\text{oven}}$ If the analyser agrees with the reference, you expect $\beta_0 = 0$ and $\beta_1 = 1$. An intercept different from zero points to a constant bias; a slope different from one points to a proportional error. One caution: ordinary least squares assumes the $x$ values are exact, but here both methods have measurement error. When the two errors are comparable, errors-in-variables methods such as Deming regression are more appropriate . ## Common pitfalls **Nonlinearity.** The Beer-Lambert law is linear only over a limited range. At high concentrations, chemical interactions, stray light or detector saturation bend the curve. Check the residuals, and restrict the range or change the model. **Outliers.** One wrongly diluted standard can drag the line noticeably, especially with few standards, as you saw in the first demo. Identify it, find out what happened, and only then decide (see [outliers](/introduction-to-chemometrics/preprocessing/outliers)). Robust regression methods are an option when outliers are frequent. **Heteroscedasticity.** In many techniques the noise grows with the signal. The fitted line is then still unbiased, but the standard errors and confidence intervals from the usual formulas are not trustworthy. Weighted least squares, which minimises $\sum_i w_i (y_i - \beta_0 - \beta_1 x_i)^2$ with weights $w_i = 1/s_i^2$ (where $s_i$ is the standard deviation of the response at that standard), is the standard remedy (see the [homoscedasticity discussion](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics#step-3-the-role-of-constant-variance)). **Extrapolation.** A calibration is only supported inside the range of the standards. A curve built from 0 to 4 mg/L says nothing reliable about a sample at 20 mg/L: dilute the sample instead. **Forcing through zero.** It is tempting to drop the intercept because "a blank should read zero". Unless you have checked that the intercept is statistically indistinguishable from zero (its confidence interval, $\hat{\beta}_0 \pm t\, s_{\beta_0}$, contains zero; for the iron data it is $0.0026 \pm 0.0047$ AU), forcing the line through the origin can bias every prediction. ## Code implementation Here are complete, working implementations of simple linear regression for the iron calibration in three languages. ```python from scipy import stats def linear_regression(x, y): """ Fit y = beta_0 + beta_1 * x by ordinary least squares. Returns the intercept, slope, R-squared, RMSE and the residual standard deviation s_yx (which divides by n - 2). """ x = np.asarray(x, dtype=float) y = np.asarray(y, dtype=float) n = len(x) x_mean, y_mean = x.mean(), y.mean() # Slope and intercept from the normal equations beta_1 = np.sum((x - x_mean) * (y - y_mean)) / np.sum((x - x_mean) ** 2) beta_0 = y_mean - beta_1 * x_mean # Predictions and residuals y_pred = beta_0 + beta_1 * x residuals = y - y_pred # Quality metrics ss_res = np.sum(residuals ** 2) ss_tot = np.sum((y - y_mean) ** 2) r_squared = 1 - ss_res / ss_tot rmse = np.sqrt(ss_res / n) s_yx = np.sqrt(ss_res / (n - 2)) return beta_0, beta_1, r_squared, rmse, s_yx # Iron calibration (Fe-phenanthroline, 510 nm) conc = np.array([0.0, 0.5, 1.0, 2.0, 3.0, 4.0]) # mg/L absorbance = np.array([0.004, 0.101, 0.199, 0.402, 0.594, 0.797]) b0, b1, r2, rmse, s_yx = linear_regression(conc, absorbance) print(f"Intercept: {b0:.4f} AU") print(f"Slope: {b1:.4f} AU per mg/L") print(f"R-squared: {r2:.5f}") print(f"RMSE: {rmse:.4f} AU (s_yx = {s_yx:.4f} AU)") # Predict an unknown sample unknown_abs = 0.370 unknown_conc = (unknown_abs - b0) / b1 print(f"Unknown: A = {unknown_abs} -> c = {unknown_conc:.2f} mg/L") # Uncertainty of the predicted concentration (m = 1 measurement of the unknown) n, m = len(conc), 1 s_xx = np.sum((conc - conc.mean()) ** 2) s_x0 = s_yx / b1 * np.sqrt(1 / m + 1 / n + (unknown_abs - absorbance.mean()) ** 2 / (b1 ** 2 * s_xx)) t = stats.t.ppf(0.975, n - 2) # 2.776 for 4 degrees of freedom print(f"95% interval: c = {unknown_conc:.2f} +/- {t * s_x0:.2f} mg/L") print(f"LOD (3.3 s_yx / slope): {3.3 * s_yx / b1:.3f} mg/L") # Calibration plot and residual plot fig, axes = plt.subplots(1, 2, figsize=(12, 5)) x_line = np.linspace(0, 4.2, 100) axes[0].scatter(conc, absorbance, s=80, zorder=5, label="Standards") axes[0].plot(x_line, b0 + b1 * x_line, "r-", linewidth=2, label=f"Fit (R² = {r2:.5f})") axes[0].scatter(unknown_conc, unknown_abs, s=120, marker="*", color="green", zorder=5, label=f"Unknown ({unknown_conc:.2f} mg/L)") axes[0].set_xlabel("Concentration (mg/L)") axes[0].set_ylabel("Absorbance (AU)") axes[0].set_title("Calibration") axes[0].legend() axes[0].grid(True, alpha=0.3) y_pred = b0 + b1 * conc axes[1].scatter(y_pred, absorbance - y_pred, s=80, zorder=5) axes[1].axhline(0, color="red", linestyle="--", alpha=0.7) axes[1].set_xlabel("Fitted values (AU)") axes[1].set_ylabel("Residuals (AU)") axes[1].set_title("Residual plot") axes[1].grid(True, alpha=0.3) plt.tight_layout() plt.show() ``` ```matlab % Iron calibration (Fe-phenanthroline, 510 nm) conc = [0.0; 0.5; 1.0; 2.0; 3.0; 4.0]; % mg/L absorbance = [0.004; 0.101; 0.199; 0.402; 0.594; 0.797]; [b0, b1, r2, rmse_val, s_yx] = linear_regression(conc, absorbance); fprintf('Intercept: %.4f AU\n', b0); fprintf('Slope: %.4f AU per mg/L\n', b1); fprintf('R-squared: %.5f\n', r2); fprintf('RMSE: %.4f AU (s_yx = %.4f AU)\n', rmse_val, s_yx); % Predict an unknown sample unknown_abs = 0.370; unknown_conc = (unknown_abs - b0) / b1; fprintf('Unknown: A = %.3f -> c = %.2f mg/L\n', unknown_abs, unknown_conc); % Uncertainty of the predicted concentration (m = 1 measurement of the unknown) n = numel(conc); m = 1; s_xx = sum((conc - mean(conc)).^2); s_x0 = s_yx / b1 * sqrt(1/m + 1/n + (unknown_abs - mean(absorbance))^2 / (b1^2 * s_xx)); t = 2.776; % Student's t, 95%, n - 2 = 4 degrees of freedom (tinv(0.975, n - 2)) fprintf('95%% interval: c = %.2f +/- %.2f mg/L\n', unknown_conc, t * s_x0); fprintf('LOD (3.3 s_yx / slope): %.3f mg/L\n', 3.3 * s_yx / b1); % Calibration plot and residual plot figure('Position', [100 100 1000 400]); subplot(1, 2, 1); x_line = linspace(0, 4.2, 100); scatter(conc, absorbance, 80, 'b', 'filled'); hold on; plot(x_line, b0 + b1 * x_line, 'r-', 'LineWidth', 2); scatter(unknown_conc, unknown_abs, 120, 'g', 'p', 'filled'); xlabel('Concentration (mg/L)'); ylabel('Absorbance (AU)'); title(sprintf('Calibration (R^2 = %.5f)', r2)); legend('Standards', 'Fit', 'Unknown', 'Location', 'northwest'); grid on; subplot(1, 2, 2); y_pred = b0 + b1 * conc; scatter(y_pred, absorbance - y_pred, 80, 'b', 'filled'); yline(0, '--r', 'LineWidth', 1.5); xlabel('Fitted values (AU)'); ylabel('Residuals (AU)'); title('Residual plot'); grid on; % Local functions must be placed at the end of a script file function [beta0, beta1, r_squared, rmse, s_yx] = linear_regression(x, y) x = x(:); y = y(:); n = numel(x); beta1 = sum((x - mean(x)) .* (y - mean(y))) / sum((x - mean(x)).^2); beta0 = mean(y) - beta1 * mean(x); residuals = y - (beta0 + beta1 * x); ss_res = sum(residuals.^2); ss_tot = sum((y - mean(y)).^2); r_squared = 1 - ss_res / ss_tot; rmse = sqrt(ss_res / n); s_yx = sqrt(ss_res / (n - 2)); end ``` ```r linear_regression c = %.2f mg/L\n", unknown_abs, unknown_conc)) # Uncertainty of the predicted concentration (m = 1 measurement of the unknown) n . For a full treatment of regression diagnostics, Draper and Smith remains a classic . ## References # Multiple Linear Regression (/introduction-to-chemometrics/models/regression-models/multiple-linear-regression) A [straight-line calibration](/introduction-to-chemometrics/models/regression-models/linear-regression) works beautifully when one measurement determines the answer. Chemistry is rarely that polite. The absorbance at your favourite wavelength also contains a little of every other species that absorbs there; a sensor response drifts with temperature; a chromatographic peak sits on the shoulder of another one. **Multiple linear regression** (MLR) is the natural next step: instead of one predictor, the model uses several at once, and it tries to separate their effects. It is still least squares, it is still solved with the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations), and it is still linear in the parameters. What changes is that the predictors can now talk to each other, and when they say almost the same thing, the model gets confused in a very specific and instructive way. ## A little history Fitting one variable from several others grew out of the same correlation work that gave us simple regression. **George Udny Yule**, a student and collaborator of Pearson, applied least squares to regression on several variables in 1897 and developed the notation for multiple and partial regression coefficients in the following decade . The idea of estimating the effect of one factor while accounting for the others is already in his partial coefficients. **Ronald A. Fisher**, working at the Rothamsted agricultural station, then turned regression and the analysis of variance into everyday tools for experimental scientists with his 1925 book *Statistical Methods for Research Workers* . His key contributions were the analysis of variance and the design of experiments, which let you estimate the effect of each factor (rainfall, fertiliser, temperature) cleanly while accounting for the others. In chemistry, the need became pressing with instrumental analysis. A spectrum contains dozens or thousands of readings, and each one carries part of the information about the analyte. Building models that use many wavelengths at once, **multivariate calibration**, became a cornerstone of chemometrics in the 1970s and 1980s . MLR was the first tool tried, and understanding where it succeeds and where it fails is the best way to understand why ridge, PCR and PLS exist. ## Why one variable is not enough Imagine a solution containing an analyte A and an interferent B whose absorption bands overlap. You calibrate at 510 nm, the maximum of A. Whatever B absorbs at 510 nm is silently counted as A. If the standards contained no B, every sample with B is overestimated; if they contained some B, samples with more B than the standards are overestimated and samples with less are underestimated. The fix is to read a second wavelength where B absorbs strongly and A barely does. A model that uses both readings can learn how much of the 510 nm signal belongs to B and subtract it. Try it: change the amount of interferent in the unknown sample and compare the two predictions. The one-wavelength model is not "wrong" in its own terms: it found the best straight line through standards in which B varied randomly, so it learned the average amount of B and is right only for samples that happen to contain that much. It simply has no information to tell A and B apart. The two-wavelength model has that information, and least squares uses it. ## The model With $p$ predictors, the model becomes: $y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p + \varepsilon$ Each coefficient $\beta_j$ tells you how much the response changes when predictor $x_j$ increases by one unit **while all the other predictors stay constant**. That last phrase is the heart of MLR. In simple regression the slope means one thing. In multiple regression, the meaning of each coefficient depends on which other variables are in the model, because each coefficient describes only the part of $x_j$ that the other predictors cannot explain. In a calibration with 50 wavelengths, $\beta_{17}$ describes what happens when the absorbance at wavelength 17 increases *while the absorbance at wavelength 18 stays the same*. In a real spectrum, neighbouring wavelengths always move together, so that situation never occurs in your data. Keep this in mind: it is the seed of the collinearity problem we will meet shortly. ### Two ways to write a calibration There are two natural ways to set up a spectroscopic calibration, and it helps to know both . The **classical** (or direct) approach follows the Beer-Lambert law literally: each absorbance is a sum of contributions from every absorbing species, $a_j = k_{Aj}\, c_A + k_{Bj}\, c_B + e_j$ where $k_{Aj}$ and $k_{Bj}$ are the sensitivities (molar absorptivity times path length) of A and B at wavelength $j$. To use it you need to know, or estimate, the pure-component contributions of **every** species in the sample. The **inverse** approach turns things around and regresses the concentration of the analyte directly on the absorbances: $c_A = \beta_0 + \beta_1 a_1 + \beta_2 a_2 + \cdots + \beta_p a_p + \varepsilon$ This is MLR with absorbances as predictors, the model used in the demo above. Its great advantage is that you only need reference values for the analyte you care about; the interferents are handled implicitly, as long as they vary in the calibration samples the way they will vary in future samples. This inverse form is the ancestor of PCR and PLS. ### A worked example The demo above is calibrated with eight mixtures in which both species vary (concentrations in arbitrary units, absorbances in AU, with a little simulated noise): | $c_A$ | $c_B$ | $a_{510}$ | $a_{600}$ | |:---:|:---:|:---:|:---:| | 0.10 | 0.7 | 0.301 | 0.481 | | 0.25 | 0.2 | 0.309 | 0.147 | | 0.40 | 0.9 | 0.657 | 0.631 | | 0.50 | 0.1 | 0.527 | 0.091 | | 0.60 | 0.5 | 0.746 | 0.361 | | 0.75 | 0.8 | 0.976 | 0.576 | | 0.90 | 0.3 | 0.986 | 0.244 | | 1.00 | 0.6 | 1.167 | 0.445 | Regressing $c_A$ on $a_{510}$ alone gives $\hat{c}_A = -0.104 + 0.941\, a_{510}$, with $R^2 = 0.934$. Adding the second wavelength gives $\hat{c}_A = -0.002 + 1.021\, a_{510} - 0.426\, a_{600}$ with $R^2$ practically equal to 1. The negative coefficient at 600 nm is the chemistry: one unit of B adds 0.282 AU at 510 nm and 0.686 AU at 600 nm, so B contributes $1.021 \times 0.282 \approx 0.288$ through the first term and $-0.426 \times 0.686 \approx -0.292$ through the second. The two cancel, and B disappears from the prediction. Nobody told the model the spectrum of B; least squares found the subtraction because B varied in the calibration set. ## The model in matrix form Writing the equation for every sample quickly becomes tedious. Matrix notation compresses it. With $n$ samples and $p$ predictors we collect: - the **response vector** $\mathbf{y}$ ($n \times 1$), for example the reference concentrations; - the **design matrix** $\mathbf{X}$ ($n \times (p+1)$), one row per sample: a 1 for the intercept followed by the predictor values; - the **coefficient vector** $\boldsymbol{\beta}$ ($(p+1) \times 1$); - the **error vector** $\boldsymbol{\varepsilon}$ ($n \times 1$). $\mathbf{y} = \mathbf{X}\boldsymbol{\beta} + \boldsymbol{\varepsilon}, \qquad \mathbf{X} = \begin{bmatrix} 1 & x_{11} & x_{12} & \cdots & x_{1p} \\ 1 & x_{21} & x_{22} & \cdots & x_{2p} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_{n1} & x_{n2} & \cdots & x_{np} \end{bmatrix}$ Here $x_{ij}$ is the value of predictor $j$ in sample $i$. ## The least squares solution The criterion is exactly the one we used for a straight line: minimize the sum of squared residuals, $S(\boldsymbol{\beta}) = \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|^2$ Setting its gradient to zero gives the **normal equations**, derived step by step in the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations) entry: $\mathbf{X}^T\mathbf{X}\,\hat{\boldsymbol{\beta}} = \mathbf{X}^T\mathbf{y}$ If $\mathbf{X}^T\mathbf{X}$ is invertible, the solution is the **ordinary least squares** (OLS) estimator: $\hat{\boldsymbol{\beta}} = (\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T\mathbf{y}$ The fitted values $\hat{\mathbf{y}} = \mathbf{X}\hat{\boldsymbol{\beta}}$ are the orthogonal projection of $\mathbf{y}$ onto the space spanned by the columns of $\mathbf{X}$. The residual vector is perpendicular to every column. MLR finds the point of that space closest to your measurements. The [normal equations](/introduction-to-chemometrics/least-squares/normal-equations#why-normal) entry explains this picture, the name "normal" and the hat matrix in more detail. Writing $(\mathbf{X}^T\mathbf{X})^{-1}$ is fine on paper. In code, solve the least squares problem with a QR or SVD based routine (`numpy.linalg.lstsq`, MATLAB's backslash, R's `lm`). Forming $\mathbf{X}^T\mathbf{X}$ squares the condition number of the problem, which matters a lot with correlated predictors ([how much it matters](/introduction-to-chemometrics/least-squares/normal-equations#how-sensitive-is-the-answer)). ### Why OLS is the default Under the standard assumptions (the model is right, errors have zero mean, constant variance and are uncorrelated), the **Gauss-Markov theorem** guarantees that OLS has the smallest variance among all *linear unbiased* estimators. We met this result at the end of [your first step into chemometrics](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics). Keep the word *unbiased* in mind: ridge regression will deliberately give it up. ## Assumptions 1. **Linearity.** The response is a linear function of the predictors (in the parameters). Curvature shows up as structure in the residuals. 2. **Full column rank.** No predictor is an exact linear combination of the others, and there are more samples than coefficients (in fact p + 1" /> is needed, otherwise there are no degrees of freedom left to estimate the noise). If this fails, $\mathbf{X}^T\mathbf{X}$ is singular and the solution is not unique. 3. **Constant variance** of the errors (homoscedasticity). If it fails, weighted least squares is the usual remedy. 4. **Independent errors.** Measurements in a time series often violate this through drift. 5. **Normal errors**, needed only for exact small-sample confidence intervals and tests, not for the estimates themselves. With spectra, assumptions 1, 3 and 4 are often reasonable. Assumption 2 is the troublemaker: neighbouring wavelengths are so correlated that $\mathbf{X}$ is nearly rank deficient, and with more wavelengths than samples it is exactly rank deficient. ## The collinearity problem **Collinearity** (or multicollinearity) means that some predictors are strongly correlated with each other, so that one of them can be almost perfectly predicted from the others. In spectroscopy it is the rule, not the exception: absorbances at 500 and 501 nm measure practically the same thing. What does that do to MLR? The cleanest way to see it is to repeat the same experiment many times with fresh noise and look at where the coefficients land. In the demo below the true model is $y = x_1 + x_2$ plus noise, and you control how correlated $x_1$ and $x_2$ are. With uncorrelated predictors, the estimates gather tightly around the true values. As the correlation approaches 1, the cloud stretches into a long thin ellipse along the line $b_1 + b_2 = 2$. The data know very well what the **sum** of the coefficients is, because that is what determines the predictions, but they hardly know how to split it between the two predictors. Any split along the ellipse fits the data almost equally well, so noise decides. The practical consequences: - **Unstable coefficients.** Adding or removing one sample can change a coefficient's size or even its sign. Values like +5000 and -4998 that nearly cancel are a classic symptom. - **Inflated standard errors.** The variance of each coefficient is equal to $\sigma^2$ (the noise variance) times a diagonal element of $(\mathbf{X}^T\mathbf{X})^{-1}$, which becomes enormous. Coefficients look "not significant" even when the model as a whole predicts well. - **Fragile predictions outside the calibration pattern.** Predictions are fine for samples that follow the same correlation structure as the calibration set, but a new sample that breaks it (a new interferent, a baseline shift) can be predicted wildly wrong. ### Diagnosing collinearity The **variance inflation factor** of predictor $j$ measures how much the variance of its coefficient is inflated by its correlation with the others: $\text{VIF}_j = \frac{1}{1 - R_j^2}$ where $R_j^2$ is the $R^2$ of regressing predictor $j$ on all the other predictors. A VIF of 1 means no inflation; a common rule of thumb flags values above 10 . With two predictors, $R_j^2$ is simply the squared correlation between them, which is the VIF shown in the demo. The **condition number** looks at the whole matrix at once. It is the ratio of the largest to the smallest singular value of the predictor matrix, $\kappa(\mathbf{X}) = \frac{\sigma_{\max}}{\sigma_{\min}} = \sqrt{\frac{\lambda_{\max}(\mathbf{X}^T\mathbf{X})}{\lambda_{\min}(\mathbf{X}^T\mathbf{X})}}$ Its value depends on how the columns are scaled. Belsley, Kuh and Welsch scale each column of the design matrix, intercept column included and without centring, to unit length, and suggested that condition indices around 30 or more indicate harmful collinearity . Software often reports the condition number of the autoscaled predictors instead (as the code below does), which gives different numbers but tells the same story. Note the square root: the condition number of $\mathbf{X}^T\mathbf{X}$ is the square of that of $\mathbf{X}$, which is exactly why solving the normal equations explicitly is risky. For a near-infrared spectrum with hundreds of wavelengths and a few dozen samples, $\kappa(\mathbf{X})$ can be astronomically large. Collinearity in spectra is not a defect of the data. Neighbouring wavelengths are correlated because they respond to the same broad molecular bands. Throwing wavelengths away is rarely the best fix, since averaging many correlated readings also averages out noise. The better route is to use methods built for correlated predictors: ridge regression, PCR or PLS. ## Model evaluation ### Adjusted R² The ordinary $R^2$ never decreases when you add a predictor, even a useless one. The **adjusted** $R^2$ charges a price for every extra coefficient: $R^2_{\text{adj}} = 1 - (1 - R^2)\frac{n - 1}{n - p - 1}$ where $n$ is the number of samples and $p$ the number of predictors. If a new variable does not reduce the residuals enough to pay for itself, $R^2_{\text{adj}}$ goes down. ### The overall F-test The F-test asks whether the model explains significantly more than just the mean of $y$: $F = \frac{(SS_{\text{tot}} - SS_{\text{res}}) / p}{SS_{\text{res}} / (n - p - 1)}$ A large F (compared with the $F_{p,\, n-p-1}$ distribution) says that at least some predictors are useful. With spectra it is almost always significant, and it does not say which predictors matter. ### Individual t-tests To test whether one coefficient differs from zero: $t_j = \frac{\hat{\beta}_j}{\text{SE}(\hat{\beta}_j)}, \qquad \text{SE}(\hat{\beta}_j) = \sqrt{\hat{\sigma}^2 \left[(\mathbf{X}^T\mathbf{X})^{-1}\right]_{jj}}$ where $\hat{\sigma}^2 = SS_{\text{res}}/(n-p-1)$ is the estimated noise variance. Under collinearity the standard errors are inflated, so important variables can look insignificant. That is a symptom of the disease, not a failure of the test. ### Prediction error and cross-validation For a calibration, what matters most is how well the model predicts samples it has never seen. The **root mean square error of prediction** on an independent test set is the most honest number: $\text{RMSEP} = \sqrt{\frac{1}{n_{\text{test}}}\sum_{i=1}^{n_{\text{test}}}(y_i - \hat{y}_i)^2}$ When there are not enough samples for a separate test set, **cross-validation** imitates one: leave a sample (or a group) out, fit the model on the rest, predict the left-out sample, and repeat until every sample has been predicted once. For MLR, leave-one-out cross-validation does not even require refitting. With the **hat matrix** $\mathbf{H} = \mathbf{X}(\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T$, which maps the measured responses onto the fitted ones ($\hat{\mathbf{y}} = \mathbf{H}\mathbf{y}$), $\text{RMSECV}_{\text{LOO}} = \sqrt{\frac{1}{n}\sum_{i=1}^{n}\left(\frac{y_i - \hat{y}_i}{1 - h_{ii}}\right)^2}$ The diagonal element $h_{ii}$ is the **leverage** of sample $i$: how strongly its own measurement pulls its own fitted value. Samples with leverage close to 1 sit far from the others in predictor space, have a large influence on the model, and receive large cross-validated residuals. The leverages average $(p+1)/n$, and a common rule of thumb flags samples with 2(p+1)/n" /> (or $3(p+1)/n$) for a closer look (see [outliers](/introduction-to-chemometrics/preprocessing/outliers)). ## Variable selection One classic way to make MLR work with spectra is to keep only a few wavelengths. If five well-chosen wavelengths carry the information, MLR on those five avoids most of the collinearity. - **Forward selection** starts from an empty model and adds, one at a time, the variable that improves it most. - **Backward elimination** starts from all variables and removes the least useful one at each step. - **Stepwise selection** allows both additions and removals. It is popular but has well-known problems: the result depends on the path taken, and the p-values printed at the end ignore the search that produced them. - **Best subset selection** evaluates every possible subset. With $p$ variables there are $2^p$ subsets, which becomes impossible very quickly (200 wavelengths give about $1.6 \times 10^{60}$ subsets). Search heuristics such as **genetic algorithms**, which evolve a population of candidate subsets, are a classic chemometric way around this . Because neighbouring wavelengths carry nearly the same information, a selection algorithm may pick 502 nm on one dataset and 503 nm on a slightly different one. The models can predict equally well, but the selected "important" wavelengths are not stable. This is one reason why regularization ([ridge](/introduction-to-chemometrics/models/regression-models/ridge-regression), [lasso](/introduction-to-chemometrics/models/regression-models/lasso-regression)) and latent-variable methods ([PCR](/introduction-to-chemometrics/models/regression-models/principal-component-regression), [PLS](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression)) are usually preferred for spectra. ## Code implementation The examples simulate a small spectroscopic calibration in which every wavelength responds to the analyte, so the predictors are strongly correlated. They fit MLR, report the usual statistics (with the leave-one-out RMSECV computed from the leverages, as above), and diagnose the collinearity. The final plot fits the same model to the two halves of the data: the coefficients change a lot, some of them even their sign, although both halves come from the same experiment. ```python from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_squared_error # --- Simulate spectroscopic calibration data --- rng = np.random.default_rng(42) n_samples, n_wavelengths = 50, 20 y = rng.uniform(0.1, 5.0, n_samples) # analyte concentration wavelengths = np.linspace(400, 700, n_wavelengths) sensitivity = np.exp(-((wavelengths - 550) ** 2) / 5000) X = np.outer(y, sensitivity) + rng.normal(0, 0.05, (n_samples, n_wavelengths)) # --- Fit multiple linear regression --- model = LinearRegression().fit(X, y) y_pred = model.predict(X) r2 = r2_score(y, y_pred) n, p = X.shape r2_adj = 1 - (1 - r2) * (n - 1) / (n - p - 1) rmsec = np.sqrt(mean_squared_error(y, y_pred)) # Leave-one-out RMSECV from the leverages (no refitting needed) X_aug = np.column_stack([np.ones(n), X]) h = np.diag(X_aug @ np.linalg.pinv(X_aug)) rmsecv = np.sqrt(np.mean(((y - y_pred) / (1 - h)) ** 2)) print(f"R²: {r2:.4f}") print(f"Adjusted R²: {r2_adj:.4f}") print(f"RMSEC: {rmsec:.4f}") print(f"RMSECV (LOO): {rmsecv:.4f}") # --- Diagnose collinearity --- # Condition number of the autoscaled X (Belsley's own convention scales the # uncentred columns, intercept included, to unit length; the message is the same) X_scaled = (X - X.mean(axis=0)) / X.std(axis=0, ddof=1) print(f"\nCondition number of scaled X: {np.linalg.cond(X_scaled):.1f}") # VIF of each predictor: diagonal of the inverse correlation matrix corr = np.corrcoef(X, rowvar=False) vif = np.diag(np.linalg.inv(corr)) print(f"Largest VIF: {vif.max():.0f}") # Two halves of the same experiment give very different coefficients: # that instability is the typical symptom of collinearity coef_a = LinearRegression().fit(X[:25], y[:25]).coef_ coef_b = LinearRegression().fit(X[25:], y[25:]).coef_ print(f"Coefficients that change sign between halves: {np.sum(np.sign(coef_a) != np.sign(coef_b))} of {p}") plt.figure(figsize=(10, 4)) plt.bar(wavelengths - 4, coef_a, width=8, label="first half") plt.bar(wavelengths + 4, coef_b, width=8, label="second half") plt.xlabel("Wavelength (nm)") plt.ylabel("Coefficient") plt.title("MLR coefficients from two halves of the data") plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() ``` ```matlab % --- Simulate spectroscopic calibration data --- rng(42); n_samples = 50; n_wavelengths = 20; y = 0.1 + (5.0 - 0.1) * rand(n_samples, 1); wavelengths = linspace(400, 700, n_wavelengths); sensitivity = exp(-((wavelengths - 550).^2) / 5000); X = y * sensitivity + 0.05 * randn(n_samples, n_wavelengths); % --- Fit multiple linear regression (QR-based solve) --- X_aug = [ones(n_samples, 1), X]; beta = X_aug \ y; y_pred = X_aug * beta; SS_res = sum((y - y_pred).^2); SS_tot = sum((y - mean(y)).^2); R2 = 1 - SS_res / SS_tot; n = n_samples; p = n_wavelengths; R2_adj = 1 - (1 - R2) * (n - 1) / (n - p - 1); % Leave-one-out RMSECV from the leverages (no refitting needed) H = X_aug * pinv(X_aug); h = diag(H); RMSECV = sqrt(mean(((y - y_pred) ./ (1 - h)).^2)); fprintf('R²: %.4f\n', R2); fprintf('Adjusted R²: %.4f\n', R2_adj); fprintf('RMSEC: %.4f\n', sqrt(SS_res / n)); fprintf('RMSECV (LOO): %.4f\n', RMSECV); % --- Diagnose collinearity --- % Condition number of the autoscaled X (Belsley's own convention scales the % uncentred columns, intercept included, to unit length; the message is the same) X_scaled = (X - mean(X)) ./ std(X); fprintf('\nCondition number of scaled X: %.1f\n', cond(X_scaled)); vif = diag(inv(corrcoef(X))); fprintf('Largest VIF: %.0f\n', max(vif)); % Two halves of the same experiment give very different coefficients: % that instability is the typical symptom of collinearity half = 25; beta_a = [ones(half, 1), X(1:half, :)] \ y(1:half); beta_b = [ones(n - half, 1), X(half+1:end, :)] \ y(half+1:end); fprintf('Coefficients that change sign between halves: %d of %d\n', ... sum(sign(beta_a(2:end)) ~= sign(beta_b(2:end))), p); figure; bar(wavelengths, [beta_a(2:end), beta_b(2:end)]); legend('first half', 'second half'); xlabel('Wavelength (nm)'); ylabel('Coefficient'); title('MLR coefficients from two halves of the data'); grid on; ``` ```r # --- Simulate spectroscopic calibration data --- set.seed(42) n_samples n" />): $\mathbf{X}^T\mathbf{X}$ is then singular and infinitely many coefficient vectors fit the data perfectly; - the predictors themselves are **noisy**, so that unstable coefficients amplify that noise into the predictions. ## What to remember MLR is the straight line generalized to several predictors: one coefficient per variable, all estimated together by least squares, each one describing the effect of its variable *with the others held fixed*. When predictors carry independent information, as in the interference example, this is exactly what lets the model separate overlapping signals. When predictors carry nearly the same information, the same mechanism backfires. The data still determine the predictions, but not the individual coefficients, which become large, unstable and uninterpretable. Recognizing this is what separates naive regression from chemometric modelling, and every method that follows is a different answer to it: - **[Ridge regression](/introduction-to-chemometrics/models/regression-models/ridge-regression)** adds a penalty $\lambda \|\boldsymbol{\beta}\|^2$ (usually not applied to the intercept) that keeps the coefficients small and stable. - **[Lasso regression](/introduction-to-chemometrics/models/regression-models/lasso-regression)** uses an L1 penalty $\lambda \|\boldsymbol{\beta}\|_1$ that sets some coefficients exactly to zero. - **[Principal component regression](/introduction-to-chemometrics/models/regression-models/principal-component-regression)** replaces the correlated predictors with a few uncorrelated principal components. - **[Partial least squares](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression)** builds components that are relevant for predicting $y$, and is the workhorse of chemometric calibration. For a thorough treatment of regression diagnostics, see Draper and Smith or Montgomery, Peck and Vining ; for the chemometric view, Brereton and Martens and Næs . ## References # Partial Least Squares Regression (PLS) (/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression) Few methods can claim to have shaped a whole discipline. **Partial least squares regression** (PLS) is one of them. If you have ever seen a near-infrared analyser predict protein in grain, moisture in a tablet or octane in fuel, there is a very good chance a PLS model was doing the work. PLS keeps the strategy of [principal component regression](/introduction-to-chemometrics/models/regression-models/principal-component-regression): compress the many correlated wavelengths into a few latent variables, then regress. It changes one thing. Instead of compressing the spectra blindly, it lets the property you want to predict guide the compression. That single change is the subject of this entry. ## A short history The story begins in econometrics, with the Swedish statistician **Herman Wold**. In the 1960s he developed iterative algorithms that estimate models by alternating simple least squares regressions, starting with principal components . In the 1970s he extended them into a framework for models with latent variables, which he called *soft modelling*, deliberately avoiding strong distributional assumptions . Wold first called these procedures NILES (nonlinear iterative least squares); by the mid-1970s they had become NIPALS (nonlinear iterative partial least squares), and that is where the name comes from. The jump to chemistry came through his son, **Svante Wold**, a chemist at Umeå University and one of the founders of chemometrics, together with **Harald Martens**, who was working on near-infrared analysis of food and agricultural products in Norway. NIR spectroscopy produced exactly the data that classical regression could not handle: hundreds of strongly correlated wavelengths measured on a few dozen samples. In 1983 Svante Wold, Martens and Herman Wold described how PLS solves this multivariate calibration problem, in a chapter of the proceedings of a conference on matrix pencils . A year later, Wold and co-workers analysed the method from the numerical analysis side and connected it with the conjugate gradient method . What brought PLS to the bench chemist was the 1986 tutorial by **Paul Geladi** and **Bruce Kowalski** in *Analytica Chimica Acta* , one of the most cited papers in chemometrics. It walked through the algorithm step by step, with examples and geometric pictures, at the moment when personal computers were putting multivariate methods within reach of any laboratory with a spectrometer. The name has always caused some confusion, because "partial least squares" does not say much about what the method does. Svante Wold later proposed reading PLS as **projection to latent structures** , which describes it better: the data are projected onto a few latent directions. Both readings share the acronym. ## Why chemometrics needed PLS You have a set of samples (tablets, grain, fuel) and for each one you measure a spectrum, the rows of $\mathbf{X}$, and a reference property, the elements of $\mathbf{y}$. Typically there are far more wavelengths than samples, and neighbouring wavelengths are almost perfectly correlated. As we saw in the previous entries: - [Multiple linear regression](/introduction-to-chemometrics/models/regression-models/multiple-linear-regression) has no unique solution when $p \geq n$ (it can fit the calibration samples perfectly and still predict new ones badly), and is wildly unstable when $p **Find directions in X-space along which the samples vary a lot and which are strongly related to y.** Formally, for [mean-centred](/introduction-to-chemometrics/preprocessing/centering-and-scaling) $\mathbf{X}$ and $\mathbf{y}$, the first PLS direction is the unit vector $\mathbf{w}$ that maximizes the covariance between the scores $\mathbf{t} = \mathbf{X}\mathbf{w}$ and the response: $\mathbf{w}_1 = \arg\max_{\|\mathbf{w}\| = 1} \text{cov}(\mathbf{X}\mathbf{w}, \mathbf{y}), \qquad \text{cov}(\mathbf{X}\mathbf{w}, \mathbf{y}) = \text{corr}(\mathbf{X}\mathbf{w}, \mathbf{y}) \cdot \text{sd}(\mathbf{X}\mathbf{w}) \cdot \text{sd}(\mathbf{y})$ The decomposition on the right is the key to the whole method. The standard deviation of $\mathbf{y}$ does not depend on $\mathbf{w}$, so maximizing the covariance means finding a direction that is **both** well correlated with the response **and** carries a decent amount of spectral variation. Compare the three related criteria: | Method | Direction chosen to maximize | |--------|-------------------| | PCA / PCR | Variance of $\mathbf{X}\mathbf{w}$ | | Canonical correlation | Correlation of $\mathbf{X}\mathbf{w}$ with $\mathbf{y}$ | | **PLS** | **Covariance of $\mathbf{X}\mathbf{w}$ with $\mathbf{y}$** | Maximizing the variance alone ignores the response. Maximizing the correlation alone is dangerous with spectra: with many variables you can almost always find some tiny, noisy direction that correlates perfectly with $\mathbf{y}$ by chance. Covariance is the compromise. And it has a remarkably simple solution: for centred data, the best direction is just proportional to $\mathbf{X}^T\mathbf{y}$, the vector of covariances between each wavelength and the response. For a single response, the maximum-correlation direction is exactly the MLR regression vector (whenever MLR has a unique solution), so the table reads as a continuum: PCR looks only at variance, MLR only at correlation, and PLS sits in between with covariance. This is why MLR overfits wide data so easily, and why PLS, which also rewards spectral spread, is much less prone to it. Stone and Brooks turned this continuum into a single method with a tuning knob, *continuum regression* . The demo below compares the first PCA direction with the first PLS direction on two wavelengths. Move the slider to change where the analyte information lies. When the analyte information lies along the big spread of the data, the two directions agree. When it lies along the small spread, PC1 keeps following the big spread, while the PLS direction tilts towards the colour gradient and its scores predict the analyte much better, although a single PLS component still leaves most of $\mathbf{y}$ unexplained here (compare its $R^2$ with the MLR tile); the second component will pick up the rest. Notice that the PLS direction rarely points exactly along the relevant direction either: it is pulled partly towards the large spectral variation. That is the covariance compromise at work. ## The model PLS describes both blocks of data with the same scores: $\mathbf{X} = \mathbf{T}\mathbf{P}^T + \mathbf{E}, \qquad \mathbf{y} = \mathbf{T}\mathbf{q} + \mathbf{f}$ Here $\mathbf{T}$ ($n \times A$) contains the scores of the $A$ components, $\mathbf{P}$ ($p \times A$) the X-loadings, $\mathbf{q}$ ($A \times 1$) the y-loadings, and $\mathbf{E}$ and $\mathbf{f}$ the residuals. The structure looks just like PCR. The difference is entirely in how $\mathbf{T}$ is computed: with the help of $\mathbf{y}$. ## The NIPALS algorithm for PLS1 The most widely taught algorithm is NIPALS. Here is the version for a single response (PLS1). Start with mean-centred data, $\mathbf{X}_0 = \mathbf{X}$ and $\mathbf{y}_0 = \mathbf{y}$, and for each component $a = 1, 2, \ldots, A$: 1. **Weights: where does X covary with y?** $\mathbf{w}_a = \frac{\mathbf{X}_{a-1}^T \mathbf{y}_{a-1}}{\|\mathbf{X}_{a-1}^T \mathbf{y}_{a-1}\|}$ This is the only place where $\mathbf{y}$ enters the construction of the components. Each element of $\mathbf{w}_a$ is proportional to the covariance between one wavelength and the response. For later components, read "one (deflated) wavelength and the part of $\mathbf{y}$ still unexplained". 2. **Scores: project the samples onto that direction** $\mathbf{t}_a = \mathbf{X}_{a-1} \mathbf{w}_a$ Each element of $\mathbf{t}_a$ is the position of one sample along the new latent variable. 3. **X-loadings: how the component looks in the spectra** $\mathbf{p}_a = \frac{\mathbf{X}_{a-1}^T \mathbf{t}_a}{\mathbf{t}_a^T \mathbf{t}_a}$ This is a least squares regression of every wavelength on the score: the spectral pattern associated with the component. 4. **y-loading: how much of y the component explains** $q_a = \frac{\mathbf{y}_{a-1}^T \mathbf{t}_a}{\mathbf{t}_a^T \mathbf{t}_a}$ Another one-variable least squares regression, this time of the response on the score. 5. **Deflation: remove what has been explained** $\mathbf{X}_a = \mathbf{X}_{a-1} - \mathbf{t}_a \mathbf{p}_a^T, \qquad \mathbf{y}_a = \mathbf{y}_{a-1} - q_a \mathbf{t}_a$ Then return to step 1 with the residual matrices to find the next component. Every step is a small, well-conditioned least squares problem, and the solution is confined to an $A$-dimensional subspace, which is why the method works even when $\mathbf{X}^T\mathbf{X}$ (the matrix of the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations)) is hopelessly singular. The deflation also makes the scores of different components mutually orthogonal, just like in PCR. The weight vectors $\mathbf{w}_a$ are orthonormal too; the loadings $\mathbf{p}_a$ in general are not. After $A$ components, the model can be written as a single regression vector on the original wavelengths: $\hat{\mathbf{y}} = \mathbf{X}\mathbf{b}_{\text{PLS}}, \qquad \mathbf{b}_{\text{PLS}} = \mathbf{W}(\mathbf{P}^T\mathbf{W})^{-1}\mathbf{q}$ where $\mathbf{W} = [\mathbf{w}_1, \ldots, \mathbf{w}_A]$, $\mathbf{P} = [\mathbf{p}_1, \ldots, \mathbf{p}_A]$ and $\mathbf{q} = [q_1, \ldots, q_A]^T$. The matrix $\mathbf{W}^* = \mathbf{W}(\mathbf{P}^T\mathbf{W})^{-1}$ is needed because the weights of later components act on deflated data, not on the original spectra; this product translates them back, so that $\mathbf{T} = \mathbf{X}\mathbf{W}^*$. Software calls it $\mathbf{W}^*$ or $\mathbf{R}$ (SIMCA, PLS_Toolbox), or `x_rotations_` (scikit-learn). The PLS1 regression vector with $A$ components is the least squares solution restricted to the space spanned by $\mathbf{X}^T\mathbf{y}, (\mathbf{X}^T\mathbf{X})\mathbf{X}^T\mathbf{y}, \ldots, (\mathbf{X}^T\mathbf{X})^{A-1}\mathbf{X}^T\mathbf{y}$ (a so-called Krylov space) . That is exactly the space explored by the first $A$ steps of the [conjugate gradient method](/introduction-to-chemometrics/least-squares/conjugate-gradient) applied to the normal equations, starting from zero . In other words, PLS1 with $A$ components gives the same answer as stopping conjugate gradient after $A$ iterations: choosing the number of components is a form of early stopping. **SIMPLS**, proposed by Sijmen de Jong in 1993 , deflates the small cross-product matrix $\mathbf{X}^T\mathbf{Y}$ instead of $\mathbf{X}$, and its weights apply directly to the original spectra, which makes it faster for large datasets. For a single response it gives exactly the same model as NIPALS; for several responses (PLS2) the two differ slightly. Many software packages use SIMPLS or other "kernel" variants for speed. ## PLS1 and PLS2 **PLS1** predicts one response at a time: protein, moisture, fat, octane number. It is by far the most common case in spectroscopic calibration, and it is the algorithm above. **PLS2** predicts several responses at once, using a response matrix $\mathbf{Y}$ instead of a vector. The weights are chosen to maximize the covariance with the whole of $\mathbf{Y}$, so the components are shared by all the responses . PLS2 is attractive when the responses are related and share an underlying structure, or when one compact model is preferred. Separate PLS1 models are usually better when the responses are unrelated, need different preprocessing, or need different numbers of components; for a single property, a dedicated PLS1 model typically predicts at least as well. In practice many chemometricians build one PLS1 model per property. ## How many components? The number of components $A$ is the critical tuning parameter of PLS, exactly as in PCR. Too few and the model misses real structure; too many and it starts to fit noise in the calibration set and predicts new samples worse. The standard tool is **cross-validation**, and it works exactly as described for [PCR](/introduction-to-chemometrics/models/regression-models/principal-component-regression#how-many-components): leave samples out, predict them with a model that never saw them, and compute the root mean square error of cross-validation (RMSECV) for each $A$. The only difference is that the PLS model, weights included, is rebuilt inside every fold. The demo below uses the same twenty simulated NIR spectra as in the PCR entry. Compare PLS with PCR (the grey curve), and then make the analyte signal smaller. Look at the green bars and switch between the two methods. With the default settings PLS explains more than half of the variance of $\mathbf{y}$ with two components, while the first three PCR components explain about 2 %: they spend themselves on the interferents and the baseline and leave $\mathbf{y}$ for a later component. Now make the analyte signal tiny. For the first four components both error curves stay high, because the interferents dominate everything. Then look at components 5 to 10: the red PLS curve reaches its plateau at about six components, while the grey PCR curve needs nine or ten to get down to the same error. Both methods end at a similar minimum error; PLS simply gets there with fewer components. The green dot marks the suggested number of components (the fewest within 5 % of the minimum), and a grey ring marks the absolute minimum when it is somewhere else. Look also at the regression vector: as components are added, it develops the shape needed to read the analyte band while cancelling the overlapping interferents. Add too many components and it becomes noisy, a visual sign of overfitting. ### Reading the RMSECV curve A typical curve has three parts: 1. **A rapid decrease** over the first components, as the main predictive structure is captured. 2. **A minimum or plateau**, where additional components bring little. 3. **A slow rise**, when components start describing noise that happens to correlate with $\mathbf{y}$ in the calibration set. Do not simply take the absolute minimum. If the curve is flat, prefer the first point where it levels off. The [PCR entry](/introduction-to-chemometrics/models/regression-models/principal-component-regression#how-many-components) describes the one-standard-error rule; the code below uses an even simpler version, "the fewest components whose RMSECV is within 2 % of the minimum". Most well-behaved spectroscopic PLS models use a handful of components, often fewer than ten. If you seem to need many more, check your preprocessing, your reference values and your samples for outliers before trusting the model. ### RMSEC, RMSECV and RMSEP | Metric | Full name | What it measures | |--------|-----------|-----------------| | **RMSEC** | Root mean square error of calibration | Fit to the calibration samples (always optimistic) | | **RMSECV** | Root mean square error of cross-validation | Estimated prediction error, from cross-validation | | **RMSEP** | Root mean square error of prediction | Prediction error on an independent test set | A large gap between RMSEC and RMSECV or RMSEP is a classic sign of overfitting: the model has learned the calibration samples rather than the chemistry. ## Interpreting a PLS model A PLS model is not a black box. Several plots show what it has learned and whether it is reliable. **Score plots.** Plotting $\mathbf{t}_1$ against $\mathbf{t}_2$ shows the samples in the latent space, like a [PCA](/introduction-to-chemometrics/models/exploratory-models/pca) score plot but with axes oriented towards prediction. Use it to spot outliers, clusters, drifts over time, and to check that calibration and validation samples cover the same region. **Weights and loadings.** The weights $\mathbf{w}_a$ show which wavelengths are used to build each component; the loadings $\mathbf{p}_a$ show the spectral pattern that each component describes. Plotted against wavelength, they should highlight chemically meaningful regions. If they do not, the model may be relying on a spurious correlation. **Regression vector.** The final $\mathbf{b}_{\text{PLS}}$, plotted against wavelength, is the overall recipe the model applies to a spectrum. Positive and negative lobes typically reflect the analyte bands and the corrections for overlapping interferents. **VIP scores.** The **variable importance in projection** summarizes the importance of each wavelength over all components : $\text{VIP}_j = \sqrt{\frac{p \sum_{a=1}^{A} SS_a \left(\frac{w_{ja}}{\|\mathbf{w}_a\|}\right)^2}{\sum_{a=1}^{A} SS_a}}, \qquad SS_a = q_a^2\, \mathbf{t}_a^T\mathbf{t}_a$ where $SS_a$ is the part of the sum of squares of $\mathbf{y}$ explained by component $a$. The squared VIP values average to 1, so "VIP greater than 1" is a common rule of thumb for important variables. Treat it as a guide rather than a test: its behaviour depends on the correlation structure of the data . **Predicted versus reference.** Finally, plot the predictions against the reference values for calibration and validation samples. A good model gives points scattered tightly around the 1:1 line; curvature or offsets point to nonlinearity or bias. ## Code examples The examples simulate NIR spectra of mixtures of a weak analyte and two interferents, one of them overlapping the analyte band, with a baseline that shifts and tilts from sample to sample and some error in the reference values. Thirty samples are used for calibration and fifty for validation. The code chooses the number of components by cross-validation and reports RMSEC, RMSECV and RMSEP. You should see RMSEC . PLS owes its popularity as much to its interpretability and its efficient use of few components as to its accuracy. ## Beyond basic PLS - **PLS-DA (discriminant analysis)** codes class membership as dummy responses and adapts PLS to classification, for example to authenticate foods or detect counterfeit drugs . - **OPLS (orthogonal PLS)** separates the variation in $\mathbf{X}$ that is related to $\mathbf{y}$ from the variation that is not, which makes the model easier to interpret . - **Multi-block PLS** handles data from several sources (for example NIR, Raman and process variables) while keeping their block structure. - **Kernel and nonlinear PLS** extend the method to curved relationships. - **Sparse PLS** adds L1 penalties to the weights so that each component uses only some of the variables . ## What to remember PLS compresses the spectra into a few latent variables, like PCR, but it chooses each latent direction to maximize the covariance with the response. Because covariance is correlation times spread, that means directions that both carry real spectral variation and relate to what you want to predict. The NIPALS algorithm builds them one at a time with small least squares regressions and deflation, and the resulting model can always be written as a single regression vector over the wavelengths. Seen from least squares, PLS1 is conjugate gradient on the normal equations stopped early: the number of components is how early you stop. The number of components is the one parameter that really matters, and it must be chosen by validation, never by the calibration fit. Choose it well, check that the model makes chemical sense, and PLS will handle exactly the kind of data that broke ordinary least squares. For a classic introduction, see Geladi and Kowalski ; for the chemometric perspective, Wold, Sjöström and Eriksson and Martens and Næs . ## References # Principal Component Regression (PCR) (/introduction-to-chemometrics/models/regression-models/principal-component-regression) A spectrum may have a thousand wavelengths, but it rarely has a thousand independent things to say. If a set of samples varies in three constituents, a baseline and a little scatter, the spectra are, up to noise, combinations of perhaps five underlying patterns. The thousand numbers are highly redundant. **Principal component regression** (PCR) takes that observation literally. First it compresses the spectra into a handful of new variables, the **principal component scores**, which summarize the main patterns of variation and are uncorrelated with each other. Then it runs an ordinary regression on those few scores. Compress first, regress second: the collinearity that ruined [multiple linear regression](/introduction-to-chemometrics/models/regression-models/multiple-linear-regression) simply disappears. ## A short history PCR combines two older tools. Principal component analysis goes back to **Karl Pearson** in 1901 and **Harold Hotelling** in 1933 ; least squares regression is older still. The idea of regressing on principal components had been suggested in 1957 by **Maurice Kendall** and by Hotelling himself . In 1965, **William F. Massy**, then at Stanford, published the first systematic treatment, *Principal Components Regression in Exploratory Statistical Research* , proposing to regress the response on the leading principal components when there are too many correlated predictors to handle directly. In chemistry, PCR became important with the rise of **near-infrared spectroscopy** in the 1980s. NIR spectra have hundreds or thousands of strongly correlated wavelengths and are often collected on relatively few reference samples. Principal components were already being used to analyse NIR spectra of cereals in the mid-1980s , and papers by **Næs and Martens** and by **Haaland and Thomas** in 1988 established PCR, together with the newer partial least squares (PLS) method, as one of the two pillars of multivariate calibration. Statisticians had also pointed out its weak spot. **Ian Jolliffe** noted in 1982 that the components that explain the most variance in the predictors are not necessarily the ones that predict the response, and backed it with real datasets in which low-variance components turned out to be predictive . That remark is the key to understanding both PCR and its rival, PLS. ## Why compress? With a spectral matrix $\mathbf{X}$ of 150 samples and 1000 wavelengths, least squares needs to invert the $1000 \times 1000$ matrix $\mathbf{X}^T\mathbf{X}$. Its rank is at most 149 once the data are centred (at most 150 without centring), so it is **singular**: infinitely many coefficient vectors fit the calibration perfectly, and almost all of them are useless for prediction. Even with more samples than wavelengths, neighbouring wavelengths are so correlated that $\mathbf{X}^T\mathbf{X}$ is nearly singular and the coefficients are wildly unstable, as we saw in the [collinearity demo](/introduction-to-chemometrics/models/regression-models/multiple-linear-regression#the-collinearity-problem). There are three broad ways out: 1. **Regularization**: keep all variables but restrain the coefficients ([ridge](/introduction-to-chemometrics/models/regression-models/ridge-regression), [lasso](/introduction-to-chemometrics/models/regression-models/lasso-regression)). 2. **Variable selection**: keep only a few variables ([lasso](/introduction-to-chemometrics/models/regression-models/lasso-regression), stepwise methods). 3. **Latent variables**: replace the many correlated variables by a few new ones that capture the essential structure (PCR, [PLS](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression)). PCR is the simplest member of the third family. ## Principal components in one paragraph Principal component analysis (see the [PCA entry](/introduction-to-chemometrics/models/exploratory-models/pca) for a full treatment) looks for the direction in variable space along which the samples spread the most. That direction is the first **loading vector** $\mathbf{p}_1$, and the position of each sample along it is its first **score** $t_{i1}$. The second component is the direction of largest remaining spread that is perpendicular to the first, and so on. Each component is a "pattern" (the loading, which for spectra looks like a spectrum) and a set of "amounts" (the scores, one number per sample). The scores of different components are uncorrelated, and the first few components usually describe almost all of the systematic variation in the data, leaving mostly noise for the later ones. ## The PCR recipe 1. **Centre the data** Subtract the mean spectrum from every spectrum and the mean response from every response. For spectra measured in the same units, mean-centring is usually all you need; autoscaling (dividing each variable by its standard deviation) is meant for variables with different units (see the practical tips below). 2. **Run PCA on X** Decompose the centred spectra into scores and loadings, keeping $A$ components: $\mathbf{X} = \mathbf{T}\mathbf{P}^T + \mathbf{E}$ $\mathbf{T}$ ($n \times A$) holds the scores, $\mathbf{P}$ ($p \times A$) the loadings, and $\mathbf{E}$ the part of the spectra that the $A$ components do not describe. 3. **Regress y on the scores** The scores are uncorrelated, so this regression is perfectly well conditioned. Each coefficient can even be computed on its own (with centred $\mathbf{y}$): $q_a = \frac{\mathbf{t}_a^T \mathbf{y}}{\mathbf{t}_a^T \mathbf{t}_a}, \qquad a = 1, \ldots, A$ 4. **Go back to the wavelengths** Because $\hat{\mathbf{y}} = \mathbf{T}\mathbf{q}$ and $\mathbf{T} = \mathbf{X}\mathbf{P}$, the model can be written directly in terms of the original spectrum: $\hat{\mathbf{y}} = \mathbf{X}\hat{\boldsymbol{\beta}}_{\text{PCR}}, \qquad \hat{\boldsymbol{\beta}}_{\text{PCR}} = \mathbf{P}\mathbf{q}$ The vector $\hat{\boldsymbol{\beta}}_{\text{PCR}}$ has one coefficient per wavelength and can be plotted like a spectrum. To predict a new sample, centre its spectrum with the calibration means and multiply. All of the ill-conditioning is absorbed in step 2, where PCA decides which few directions to keep. Step 3 never sees the troublesome directions at all. ## The mathematics in compact form Start from the singular value decomposition of the centred spectra, $\mathbf{X} = \mathbf{U}\mathbf{D}\mathbf{V}^T$. The columns of $\mathbf{V}$ are the loadings, the singular values $d_a$ (on the diagonal of $\mathbf{D}$) measure how much the data spread along each loading, and the scores are $\mathbf{T} = \mathbf{X}\mathbf{V} = \mathbf{U}\mathbf{D}$. Equivalently, the loadings are the eigenvectors of $\mathbf{X}^T\mathbf{X}$ and the eigenvalues are $d_a^2$. Keeping the first $A$ components, $\mathbf{T}_A = \mathbf{X}\mathbf{P}_A$, and since the scores are orthogonal, $\mathbf{T}_A^T\mathbf{T}_A = \text{diag}(d_1^2, \ldots, d_A^2)$. The regression of $\mathbf{y}$ on the scores and its translation back to wavelengths become: $\hat{\mathbf{q}} = (\mathbf{T}_A^T\mathbf{T}_A)^{-1}\mathbf{T}_A^T\mathbf{y}, \qquad \hat{\boldsymbol{\beta}}_{\text{PCR}} = \mathbf{P}_A \hat{\mathbf{q}} = \sum_{a=1}^{A} \frac{1}{d_a}(\mathbf{u}_a^T\mathbf{y})\,\mathbf{v}_a$ Compare the last expression with least squares and ridge regression, written in the same SVD language in the [ridge entry](/introduction-to-chemometrics/models/regression-models/ridge-regression#what-ridge-does-to-each-direction): A \end{cases}" display={true} /> Both methods attack the same problem: directions with small singular values, where least squares amplifies noise. Ridge regression turns those directions down smoothly; PCR keeps the first $A$ directions untouched and switches the rest off completely. Ridge is a dimmer, PCR is a set of on/off switches. In practice the two often predict similarly (see the comparison with PLS below), and PCR has the advantage that its components can be inspected and interpreted. ## The weak spot: variance is not relevance PCA builds its components from $\mathbf{X}$ alone. It has no idea what you want to predict. The first component is the direction of largest spectral variation, whatever its cause. If the largest variation in your spectra comes from water content, particle size or temperature, the first components will describe exactly that, and the analyte may only appear in a later component. The demo below makes this concrete with two wavelengths. The cloud of samples is stretched along one direction by a large interferent. Use the slider to decide where the analyte information lives, and compare the first principal component with the first PLS direction, which (as we will see in the next entry) is built using the response. When the analyte information lies along the big variation, PC1 is a great predictor. When it lies along the small variation, PC1 points the wrong way and its scores have almost no relation to the analyte: a one-component PCR model is useless, and you need the second component as well. With real spectra and several interferents, the analyte may only appear in the fourth or fifth component, and Hadi and Ling showed that, in principle, all the predictive information can even sit in the *last* component . The practical consequences: - PCR often needs **more components** than PLS to reach the same prediction error. - Selecting components strictly in order of variance is a choice, not a law. Some practitioners select components by their correlation with $\mathbf{y}$ instead, at the price of a more complicated (and more easily overfitted) selection step. ## How many components? The number of components $A$ is the key tuning parameter of PCR, and it controls a familiar trade-off: - **Too few components** leave out spectral information that the model needs: the predictions are biased. - **Too many components** bring in directions that describe noise or irrelevant variation: the model starts to fit the calibration samples rather than the chemistry, and it predicts new samples worse. The standard tool is **cross-validation**: 1. For each candidate number of components $A = 1, 2, \ldots, A_{\max}$: 2. Leave out one sample, or one group of samples, fit PCR on the rest with $A$ components (including the centring and the PCA), and predict what was left out. 3. Repeat until every sample has been predicted once, and compute the **root mean square error of cross-validation**: $\text{RMSECV}(A) = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_{i,\text{CV}})^2}$ where $\hat{y}_{i,\text{CV}}$ is the prediction for sample $i$ made by a model that never saw it. 4. Plot RMSECV against $A$ and choose the smallest $A$ at which the curve reaches its minimum or levels off. The demo below runs this procedure on twenty simulated NIR spectra. Move the number of components and watch four things at once: the regression vector, the calibration and cross-validated errors, and how much of the variance in $\mathbf{X}$ and in $\mathbf{y}$ each component explains. The green point marks the suggested number of components: the fewest whose cross-validated error is within 5 % of the minimum (the parsimony rule described below). The grey curve is the cross-validated error of PLS, the method of the next entry; you can switch to it to compare. With a small analyte signal, the first components of PCR explain a lot of spectral variance but almost none of the analyte (look at the green bars): they describe the interferents and the baseline. The cross-validated error stays high until the component that carries the analyte finally arrives. Meanwhile the calibration error keeps going down, which is why it can never be used to choose $A$. Now make the analyte signal dominant. Once the analyte is the main source of spectral variation, the first components already carry it, and PCR catches up with PLS. When the RMSECV curve has a broad, flat minimum, choose the smaller number of components. A model with five components that predicts almost as well as one with ten is more robust to the small changes that new samples always bring. A common formal version is the one-standard-error rule: take the fewest components whose RMSECV is within one standard error of the minimum. ## PCR versus PLS | | PCR | PLS | |---|---|---| | **Components built to maximize** | Variance of $\mathbf{X}$ | Covariance between $\mathbf{X}$ and $\mathbf{y}$ | | **Uses y to build components?** | No | Yes | | **Components usually needed** | More | Fewer | | **Meaning of the components** | Directions of largest spectral variation, clearly defined | A compromise between spectral variation and relevance to y | | **Algorithm** | PCA (SVD) followed by least squares | NIPALS, SIMPLS or kernel algorithms | | **Scores uncorrelated?** | Yes | Yes | | **If the analyte dominates X variance** | About as good as PLS | About as good as PCR | | **If it does not** | Needs extra components | Finds predictive directions earlier | "PLS is always better than PCR" is an oversimplification. PLS usually reaches its best performance with fewer components, but when both methods are tuned properly their prediction errors are frequently very similar . PCR remains attractive for its transparency: its components are the same ones you would examine in an exploratory PCA. ## Code implementation The examples simulate NIR-like spectra built from three underlying factors, choose the number of components by 10-fold cross-validation with a parsimony rule, and plot the result. ```python from sklearn.decomposition import PCA from sklearn.linear_model import LinearRegression from sklearn.model_selection import cross_val_predict, KFold from sklearn.pipeline import make_pipeline # --- Simulate NIR calibration data: 150 samples, 500 wavelengths --- rng = np.random.default_rng(42) n_samples, n_wavelengths = 150, 500 wavelengths = np.linspace(900, 1700, n_wavelengths) loading1 = np.exp(-((wavelengths - 1200) ** 2) / 5000) loading2 = np.exp(-((wavelengths - 1400) ** 2) / 3000) loading3 = np.exp(-((wavelengths - 1100) ** 2) / 8000) f1 = rng.standard_normal(n_samples) f2 = rng.standard_normal(n_samples) f3 = 0.5 * rng.standard_normal(n_samples) X = np.outer(f1, loading1) + np.outer(f2, loading2) + np.outer(f3, loading3) X += 0.05 * rng.standard_normal((n_samples, n_wavelengths)) y = 3.0 * f1 + 1.5 * f2 + 0.3 * rng.standard_normal(n_samples) # --- Choose the number of components by cross-validation --- # PCA centres X internally and LinearRegression fits the intercept, # so every fold is centred with its own training data. max_components = 15 kf = KFold(n_splits=10, shuffle=True, random_state=42) rmsecv = [] for a in range(1, max_components + 1): pcr = make_pipeline(PCA(n_components=a), LinearRegression()) y_cv = cross_val_predict(pcr, X, y, cv=kf) rmsecv.append(np.sqrt(np.mean((y - y_cv) ** 2))) # Parsimony: the fewest components whose RMSECV is within 2 % of the minimum rmsecv = np.array(rmsecv) best_a = int(np.argmax(rmsecv ; for PCA itself, Wold, Esbensen and Geladi and Jolliffe . ## References # Ridge Regression (/introduction-to-chemometrics/models/regression-models/ridge-regression) In the [multiple linear regression](/introduction-to-chemometrics/models/regression-models/multiple-linear-regression) entry we saw least squares get confused: when predictors carry almost the same information, the data no longer decide how to share the effect between them, and the coefficients swing wildly from one dataset to the next. **Ridge regression** is the simplest and oldest cure. Its idea fits in one sentence: *among all the coefficient vectors that fit the data well, prefer the ones that are small*. That modest preference introduces a little bias and, in exchange, removes a lot of instability. It is the first example of **regularization**, an idea that runs through modern chemometrics and machine learning. ## A short history In 1970, **Arthur E. Hoerl** and **Robert W. Kennard**, two statisticians at DuPont in Wilmington, Delaware, published a pair of papers in *Technometrics*. The first, *Ridge Regression: Biased Estimation for Nonorthogonal Problems* , proposed adding a small positive constant to the diagonal of $\mathbf{X}^T\mathbf{X}$ before inverting it. The matrix becomes better conditioned and the coefficients become biased but far more stable. The second, *Ridge Regression: Applications to Nonorthogonal Problems* , showed the method at work on real data. The name came from Hoerl's earlier work on **ridge analysis**, a technique for exploring response surfaces that he introduced in 1959 and applied to regression problems in the early 1960s . Ridge analysis follows the best attainable response on spheres of increasing radius around a centre point. Ridge regression does the same thing with the residual sum of squares: for every sphere of a given size around the origin, it finds the best-fitting coefficients on it. We will see that picture in a moment. Hoerl and Kennard were not the first to have the core idea. In the Soviet Union, **Andrey N. Tikhonov** had studied the stability of inverse problems since the 1940s and formalized his *regularization method* in 1963 . In physics and applied mathematics the same technique is called **Tikhonov regularization**. Two communities, two names, one idea: whenever an inverse problem is ill-conditioned, add a penalty that keeps the solution reasonable. Their most important message was almost philosophical: **a biased estimator can beat an unbiased one** when the goal is to be close to the truth on average. That idea, the bias-variance trade-off, is one of the pillars of statistical learning. ## The instability problem Suppose you predict a concentration from the absorbances at three neighbouring wavelengths, say 500, 501 and 502 nm. After centring, the matrix $\mathbf{X}^T\mathbf{X}$ might look like this: $\mathbf{X}^T\mathbf{X} \approx \begin{bmatrix} 100 & 99.8 & 99.5 \\ 99.8 & 100 & 99.8 \\ 99.5 & 99.8 & 100 \end{bmatrix}$ Its eigenvalues are about 299.4, 0.5 and 0.1. One direction in coefficient space (all three coefficients moving together) is very well determined by the data; the other two are barely determined at all. The ratio between the largest and smallest eigenvalue, about 3000, is the **condition number** of $\mathbf{X}^T\mathbf{X}$. When the matrix is inverted, the small eigenvalues become large factors ($1/0.1 = 10$, compared with $1/299.4 \approx 0.003$), and the variance of the coefficients along those directions is about 3000 times larger than along the well-determined one. The result is the familiar symptom: coefficients such as $+41$, $-73$ and $+43$ that almost cancel each other. Add a single sample and they may become $+25$, $-41$ and $+27$. Their sum, the well-determined part, stays at 11, so the fitted values barely change, but the model has learned nothing trustworthy about the individual wavelengths, and predictions for samples that do not follow the calibration pattern exactly can be badly off. Consider two models that fit the calibration data about equally well: - Model A: $\hat{y} = 41 x_1 - 73 x_2 + 43 x_3$ - Model B: $\hat{y} = 3.6 x_1 + 3.7 x_2 + 3.7 x_3$ For a sample with three almost equal absorbances, both give the same prediction, because both sets of coefficients add up to 11. Least squares cannot tell them apart, and noise may make it pick A. Model B, with modest coefficients, is much more likely to behave sensibly on new samples. Ridge regression systematically nudges the solution towards models like B. ## The ridge solution Instead of minimizing only the sum of squared residuals, ridge regression minimizes: $S_{\text{ridge}}(\boldsymbol{\beta}) = \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|^2 + \lambda \|\boldsymbol{\beta}\|^2 = \sum_{i=1}^{n}(y_i - \hat{y}_i)^2 + \lambda \sum_{j=1}^{p}\beta_j^2$ The first term asks the model to fit the data. The second, the **L2 penalty** (also called the ridge or Tikhonov penalty, and *weight decay* in neural networks), asks the coefficients to stay small. The tuning parameter $\lambda \geq 0$ sets the balance between the two wishes. The intercept is not penalized: shrinking it would pull every prediction towards zero for no good reason. In practice, the columns of $\mathbf{X}$ and the response are centred, the penalized problem is solved for the slopes, and the intercept is recovered afterwards as $\bar{y} - \bar{\mathbf{x}}^T\hat{\boldsymbol{\beta}}$. Taking the gradient and setting it to zero, exactly as for the [normal equations](/introduction-to-chemometrics/least-squares/normal-equations): $\frac{\partial S_{\text{ridge}}}{\partial \boldsymbol{\beta}} = -2\mathbf{X}^T\mathbf{y} + 2\mathbf{X}^T\mathbf{X}\boldsymbol{\beta} + 2\lambda\boldsymbol{\beta} = \mathbf{0}$ which rearranges into a slightly modified set of normal equations: $(\mathbf{X}^T\mathbf{X} + \lambda\mathbf{I})\,\hat{\boldsymbol{\beta}}_{\text{ridge}} = \mathbf{X}^T\mathbf{y} \quad\Longrightarrow\quad \hat{\boldsymbol{\beta}}_{\text{ridge}} = (\mathbf{X}^T\mathbf{X} + \lambda\mathbf{I})^{-1}\mathbf{X}^T\mathbf{y}$ The only change from ordinary least squares is the term $\lambda\mathbf{I}$, a constant added to the diagonal. Adding $\lambda$ to the diagonal raises **every eigenvalue** by $\lambda$. In our example, $\lambda = 1$ turns the eigenvalues into 300.4, 1.5 and 1.1, and the condition number drops from about 3000 to about 270. And since every eigenvalue is now at least $\lambda$, the matrix can be inverted even when $\mathbf{X}^T\mathbf{X}$ is singular, for example when there are more wavelengths than samples. At the two extremes: $\lambda = 0$ gives back ordinary least squares, and $\lambda \to \infty$ shrinks every slope to zero, so the model predicts $\bar{y}$ for every sample. The art is in choosing something in between. ## The picture: a circle and some ellipses There is an equivalent way to write the ridge problem that makes its behaviour visible. Minimizing the penalized sum is the same as minimizing the ordinary sum of squares **subject to a budget** on the size of the coefficients: $\min_{\boldsymbol{\beta}} \|\mathbf{y} - \mathbf{X}\boldsymbol{\beta}\|^2 \quad \text{subject to} \quad \sum_{j=1}^{p} \beta_j^2 \leq t^2$ Each $\lambda$ corresponds to some budget $t$: a larger penalty means a smaller budget. With two coefficients, the budget is a disc centred at the origin. The sum of squares, as a function of the coefficients, forms the bowl we met in [gradient descent](/introduction-to-chemometrics/least-squares/gradient-descent); its contour lines are ellipses centred at the least squares estimate. The ridge solution is the point where the smallest ellipse touches the disc. Play with it. When the least squares point already lies inside the disc, the constraint does nothing. When it lies outside, the solution slides to the edge of the disc, closer to the origin. Increase the correlation and the ellipses become long and thin: that is collinearity seen from coefficient space, a long valley along which the fit hardly changes. The disc cuts that valley short. Switch to the diamond, or tick "Show both shapes", to get a preview of the [lasso](/introduction-to-chemometrics/models/regression-models/lasso-regression). ## What ridge does to each direction The singular value decomposition, $\mathbf{X} = \mathbf{U}\mathbf{D}\mathbf{V}^T$, shows precisely what the penalty does . Here the columns $\mathbf{v}_j$ of $\mathbf{V}$ are orthogonal directions in coefficient space, the $d_j$ are the singular values (how much the data spread along each direction), and the $\mathbf{u}_j$ are the corresponding directions in sample space. The least squares solution can be written as: $\hat{\boldsymbol{\beta}}_{\text{OLS}} = \sum_{j=1}^{p} \frac{1}{d_j} (\mathbf{u}_j^T \mathbf{y})\, \mathbf{v}_j$ A tiny $d_j$ means a huge factor $1/d_j$: whatever noise sits in that direction is multiplied enormously. Ridge regression replaces the factor: $\hat{\boldsymbol{\beta}}_{\text{ridge}} = \sum_{j=1}^{p} \frac{d_j}{d_j^2 + \lambda} (\mathbf{u}_j^T \mathbf{y})\, \mathbf{v}_j = \sum_{j=1}^{p} \underbrace{\frac{d_j^2}{d_j^2 + \lambda}}_{\text{shrinkage}} \frac{1}{d_j} (\mathbf{u}_j^T \mathbf{y})\, \mathbf{v}_j$ The shrinkage factor is close to 1 for directions where the data are rich ($d_j^2 \gg \lambda$) and close to 0 where they are poor ($d_j^2 \ll \lambda$). Ridge regression therefore leaves the well-determined directions almost untouched and quietly switches off the poorly determined ones, which are exactly the directions where least squares was amplifying noise. The sum of the shrinkage factors is a useful number too. It is called the **effective degrees of freedom** of the fit: $\text{df}(\lambda) = \sum_{j=1}^{p} \frac{d_j^2}{d_j^2 + \lambda}$ When $\lambda = 0$ it equals the rank of $\mathbf{X}$ (the number of non-zero singular values, which is $p$ when there are more samples than variables), and it decreases smoothly towards 0 as $\lambda$ grows: ridge regression behaves like a model with a non-integer, adjustable number of parameters. Keep this formula in mind when you read about [principal component regression](/introduction-to-chemometrics/models/regression-models/principal-component-regression), which uses factors that are exactly 1 or exactly 0. ## The bias-variance trade-off The Gauss-Markov theorem says that ordinary least squares has the smallest variance among *unbiased* linear estimators. Ridge regression is biased: on average, its coefficients are pulled towards zero. Why would anyone accept that? Because the error we care about has two parts. The mean squared error of an estimator is its squared bias plus its variance: $\text{MSE}(\hat{\boldsymbol{\beta}}) = \mathbb{E}\left[\|\hat{\boldsymbol{\beta}} - \boldsymbol{\beta}\|^2\right] = \underbrace{\|\mathbb{E}[\hat{\boldsymbol{\beta}}] - \boldsymbol{\beta}\|^2}_{\text{bias}^2} + \underbrace{\mathbb{E}\left[\|\hat{\boldsymbol{\beta}} - \mathbb{E}[\hat{\boldsymbol{\beta}}]\|^2\right]}_{\text{variance}}$ Least squares has zero bias but, with collinear predictors, an enormous variance. A small penalty adds a little bias and removes a lot of variance. Hoerl and Kennard proved that, for any problem, there is always some 0" /> for which ridge regression has a smaller mean squared error than least squares . The catch is that the best $\lambda$ depends on the unknown true coefficients, so it has to be estimated from the data. ## The ridge trace Hoerl and Kennard suggested looking at the whole family of solutions at once: plot every coefficient as a function of $\lambda$. This plot is the **ridge trace**. The demo below computes it for a simulated near-infrared calibration with ten wavelengths, together with the calibration error and the cross-validated error for every value of $\lambda$. Read it from left to right. With almost no penalty you see the least squares coefficients: large, with alternating signs, the typical fingerprint of collinearity. As $\lambda$ increases they settle down, and eventually they all slide towards zero. Notice also the two error curves: the calibration error can only grow with the penalty, because ridge deliberately fits the calibration data a little worse. The cross-validated error first decreases and then increases. The bottom of that curve is the best compromise for prediction. The hollow circle marks a slightly stronger penalty, chosen by the one-standard-error rule described in the next section. Hoerl and Kennard originally recommended choosing $\lambda$ where the coefficients have stopped changing wildly but have not yet been shrunk to insignificance. It is a subjective rule, but the trace remains a very useful picture. If the coefficients are already stable at $\lambda = 0$, collinearity is not your problem and ridge regression is not needed. ## Choosing λ ### Cross-validation The most widely used approach, and the one in the demo: 1. Split the data into $k$ folds (typically 5 or 10). 2. For each candidate $\lambda$, fit the model on $k - 1$ folds and predict the fold that was left out. 3. Compute the cross-validated RMSE for each $\lambda$. 4. Choose the $\lambda$ with the smallest error, or, to be a bit more conservative, the largest $\lambda$ whose error is within one standard error of the minimum. Remember to standardize inside each fold, using only the training samples, so that the left-out samples really are unseen. ### Generalized cross-validation Golub, Heath and Wahba proposed a fast approximation to leave-one-out cross-validation that needs a single fit per $\lambda$ : $\text{GCV}(\lambda) = \frac{\frac{1}{n} \|\mathbf{y} - \hat{\mathbf{y}}_\lambda\|^2}{\left(1 - \frac{1}{n}\, \text{tr}(\mathbf{H}_\lambda)\right)^2}, \qquad \mathbf{H}_\lambda = \mathbf{X}(\mathbf{X}^T\mathbf{X} + \lambda\mathbf{I})^{-1}\mathbf{X}^T$ $\mathbf{H}_\lambda$ is the ridge **hat matrix** (it turns $\mathbf{y}$ into $\hat{\mathbf{y}}$), and its trace is precisely the effective degrees of freedom $\text{df}(\lambda)$ introduced above. With an intercept fitted, add 1 to the trace, since the intercept uses one more degree of freedom. ### The L-curve Plot the size of the solution, $\log\|\hat{\boldsymbol{\beta}}_\lambda\|$, against the size of the residual, $\log\|\mathbf{y} - \mathbf{X}\hat{\boldsymbol{\beta}}_\lambda\|$, for many values of $\lambda$. The curve usually looks like an L. Its corner marks the point where shrinking the coefficients further starts to cost a lot of fit. The L-curve is popular in inverse problems and spectroscopic deconvolution . ## A Bayesian reading Ridge regression also has a neat probabilistic interpretation. Suppose, before seeing the data, you believe that each coefficient is probably small, and you express that belief as a Gaussian prior centred at zero: $\boldsymbol{\beta} \sim \mathcal{N}(\mathbf{0}, \tau^2 \mathbf{I})$ With Gaussian measurement noise of variance $\sigma^2$, the most probable coefficients after seeing the data (the posterior mode, which here is also the posterior mean) are exactly the ridge solution: $\hat{\boldsymbol{\beta}} = \left(\mathbf{X}^T\mathbf{X} + \frac{\sigma^2}{\tau^2}\mathbf{I}\right)^{-1}\mathbf{X}^T\mathbf{y}, \qquad \lambda = \frac{\sigma^2}{\tau^2}$ So $\lambda$ is the ratio between the noise variance and the prior variance of the coefficients. A large $\lambda$ means "I strongly believe the coefficients are small"; a small $\lambda$ lets the data speak. When collinearity leaves the data ambiguous about some directions, the prior fills the gap. This extends the maximum likelihood reasoning we followed in [your first step into chemometrics](/introduction-to-chemometrics/least-squares/your-first-step-into-chemometrics) with one extra ingredient, the prior. The result is no longer a maximum likelihood estimate but a **maximum a posteriori (MAP)** estimate: the coefficients that are most probable once the data and the prior belief are combined. Replace the Gaussian prior by a Laplace prior and you get the lasso. ## Code implementation ```python from sklearn.linear_model import Ridge, RidgeCV, LinearRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import KFold, cross_val_score # --- Simulate spectra: analyte band + overlapping interferent + noise --- rng = np.random.default_rng(42) n_samples, n_wavelengths = 40, 30 wavelengths = np.linspace(450, 700, n_wavelengths) analyte = np.exp(-((wavelengths - 550) ** 2) / 3000) interferent = np.exp(-((wavelengths - 600) ** 2) / 4000) conc = rng.uniform(0.5, 10.0, n_samples) # analyte concentration c_int = rng.uniform(0.5, 10.0, n_samples) # interferent, unrelated to y X = (np.outer(conc, analyte) + np.outer(c_int, interferent) + rng.normal(0, 0.05, (n_samples, n_wavelengths))) y = conc + rng.normal(0, 0.2, n_samples) # reference method error # Standardize predictors (the penalty must treat all variables equally) X_scaled = StandardScaler().fit_transform(X) # --- Ridge trace: coefficients versus lambda --- # scikit-learn's Ridge minimizes ||y - Xb||^2 + alpha ||b||^2, so alpha is our lambda lambdas = np.logspace(-3, 4, 200) coefs = np.array([Ridge(alpha=a).fit(X_scaled, y).coef_ for a in lambdas]) plt.figure(figsize=(10, 5)) plt.semilogx(lambdas, coefs, linewidth=0.8) plt.axhline(0, color="black", linewidth=0.5) plt.xlabel("lambda") plt.ylabel("Coefficient") plt.title("Ridge trace") plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # --- Choose lambda by 10-fold cross-validation --- # The scaler sits inside the pipeline, so each fold is standardized with its own training data model = make_pipeline(StandardScaler(), RidgeCV(alphas=np.logspace(-3, 4, 100), cv=10, scoring="neg_mean_squared_error")) model.fit(X, y) best_lambda = model[-1].alpha_ print(f"Optimal lambda: {best_lambda:.4f}") # --- Compare prediction errors on the same folds --- folds = KFold(n_splits=10, shuffle=True, random_state=0) for name, reg in [("Least squares", LinearRegression()), ("Ridge", Ridge(alpha=best_lambda))]: mse = -cross_val_score(make_pipeline(StandardScaler(), reg), X, y, cv=folds, scoring="neg_mean_squared_error").mean() print(f"{name:13s} RMSECV = {np.sqrt(mse):.3f}") # --- Compare least squares and ridge coefficients --- ols = LinearRegression().fit(X_scaled, y) ridge = Ridge(alpha=best_lambda).fit(X_scaled, y) fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].bar(wavelengths, ols.coef_, width=6, color="tomato") axes[0].set_title("Least squares coefficients") axes[1].bar(wavelengths, ridge.coef_, width=6, color="steelblue") axes[1].set_title(f"Ridge coefficients (lambda = {best_lambda:.3f})") for ax in axes: ax.set_xlabel("Wavelength (nm)") ax.grid(True, alpha=0.3) axes[0].set_ylabel("Coefficient") plt.tight_layout() plt.show() ``` ```matlab % Requires the Statistics and Machine Learning Toolbox (cvpartition) % --- Simulate spectra: analyte band + overlapping interferent + noise --- rng(42); n_samples = 40; n_wavelengths = 30; wavelengths = linspace(450, 700, n_wavelengths); analyte = exp(-((wavelengths - 550).^2) / 3000); interferent = exp(-((wavelengths - 600).^2) / 4000); conc = 0.5 + (10.0 - 0.5) * rand(n_samples, 1); % analyte concentration c_int = 0.5 + (10.0 - 0.5) * rand(n_samples, 1); % interferent, unrelated to y X = conc * analyte + c_int * interferent + 0.05 * randn(n_samples, n_wavelengths); y = conc + 0.2 * randn(n_samples, 1); % reference method error % Standardize predictors and centre the response Xs = (X - mean(X)) ./ std(X); yc = y - mean(y); % --- Ridge trace --- lambdas = logspace(-3, 4, 200); coefs = zeros(numel(lambdas), n_wavelengths); for i = 1:numel(lambdas) coefs(i, :) = ((Xs' * Xs + lambdas(i) * eye(n_wavelengths)) \ (Xs' * yc))'; end figure; semilogx(lambdas, coefs, 'LineWidth', 0.8); xlabel('\lambda'); ylabel('Coefficient'); title('Ridge trace'); yline(0, 'k'); grid on; % --- 10-fold cross-validation (standardizing inside each fold) --- cvp = cvpartition(n_samples, 'KFold', 10); cv_mse = zeros(numel(lambdas), 1); for i = 1:numel(lambdas) sq_err = 0; for f = 1:cvp.NumTestSets tr = training(cvp, f); te = test(cvp, f); mu = mean(X(tr, :)); sd = std(X(tr, :)); Xtr = (X(tr, :) - mu) ./ sd; Xte = (X(te, :) - mu) ./ sd; ybar = mean(y(tr)); b = (Xtr' * Xtr + lambdas(i) * eye(n_wavelengths)) \ (Xtr' * (y(tr) - ybar)); sq_err = sq_err + sum((y(te) - ybar - Xte * b).^2); end cv_mse(i) = sq_err / n_samples; end [~, best] = min(cv_mse); best_lambda = lambdas(best); fprintf('Optimal lambda: %.4f (RMSECV = %.3f)\n', best_lambda, sqrt(cv_mse(best))); fprintf('Near least squares (lambda = %.3f): RMSECV = %.3f\n', lambdas(1), sqrt(cv_mse(1))); % --- Compare least squares and ridge coefficients --- beta_ols = Xs \ yc; beta_ridge = (Xs' * Xs + best_lambda * eye(n_wavelengths)) \ (Xs' * yc); figure; subplot(1, 2, 1); bar(wavelengths, beta_ols, 'FaceColor', [0.9 0.4 0.3]); title('Least squares coefficients'); xlabel('Wavelength (nm)'); grid on; subplot(1, 2, 2); bar(wavelengths, beta_ridge, 'FaceColor', [0.3 0.5 0.8]); title(sprintf('Ridge coefficients (\\lambda = %.3f)', best_lambda)); xlabel('Wavelength (nm)'); grid on; ``` ```r library(glmnet) # --- Simulate spectra: analyte band + overlapping interferent + noise --- set.seed(42) n_samples n" />): $\mathbf{X}^T\mathbf{X} + \lambda\mathbf{I}$ is always invertible for 0" />, so the solution is unique; - the predictors are **noisy** and least squares would amplify that noise. It does little or nothing when: - the predictors are **nearly orthogonal**, for example from a designed experiment, where least squares is already stable; - you need a **sparse model** with only a few variables; - the model is **misspecified**: a penalty cannot fix a linear model fitted to a strongly nonlinear relationship. In spectroscopic calibration, ridge regression competes directly with PCR and PLS. In an influential comparison, Frank and Friedman found that the three methods often perform similarly, with ridge frequently slightly ahead in their simulations, though by a small margin and with PLS and PCR close behind . In chemometric practice PLS is more common, mostly because its latent variables are easier to inspect and interpret. ## What to remember Ridge regression is least squares with a gentle preference for small coefficients. Mathematically it adds $\lambda$ to the diagonal of $\mathbf{X}^T\mathbf{X}$; geometrically it confines the coefficients to a sphere; in terms of the SVD it smoothly switches off the directions the data cannot determine; and from a Bayesian point of view it is a Gaussian prior centred at zero. All four descriptions say the same thing: trade a little bias for a large reduction in variance. The same idea reappears in other forms: - **[Lasso regression](/introduction-to-chemometrics/models/regression-models/lasso-regression)** replaces the squared penalty by absolute values, which sets some coefficients exactly to zero. - **Elastic net** mixes both penalties to combine the stability of ridge with the sparsity of the lasso . - **[Principal component regression](/introduction-to-chemometrics/models/regression-models/principal-component-regression)** applies a hard version of the same shrinkage, keeping some directions completely and discarding the rest. - **[Partial least squares](/introduction-to-chemometrics/models/regression-models/partial-least-squares-regression)** also shrinks most of the poorly determined directions (though, unlike ridge and PCR, some of its factors can exceed 1), and it chooses them with the response in mind . For a gentle textbook treatment, see James et al. ; for the full statistical picture, Hastie, Tibshirani and Friedman . ## References