Techniques for Efficiently Iterating Through Rows in Pandas DataFrames

Posts

In data science and analytics, pandas is one of the most powerful libraries for data manipulation in Python. It allows users to efficiently work with structured data in the form of DataFrames. While pandas offers numerous methods for optimizing data operations, there are instances where processing data row by row is necessary. Iterating over the rows of a pandas DataFrame allows for customized, row-wise operations, which can be especially useful for certain tasks that are difficult to vectorize.

Why is Iterating Over Rows Important?

Before diving into the specific methods of iterating over DataFrame rows, it’s crucial to understand why and when iteration might be necessary. Pandas is optimized for column-wise and vectorized operations, meaning it’s designed to work with entire columns or DataFrame operations at once. However, there are certain scenarios where you may need to perform more complex row-wise operations that cannot be efficiently vectorized. These tasks may include:

  • Complex transformations: When you need to perform operations involving multiple columns within each row, such as mathematical computations or logic conditions that vary per row.
  • Handling missing data: In some cases, you may need to apply specific logic for rows with missing or null values, especially when the condition varies per row.
  • Custom operations: Some use cases require custom functions that need to be applied row by row, such as in the case of string manipulations, conditional flagging, or row-wise aggregation.

While pandas is generally built to work faster and more efficiently with vectorized operations, iterating over rows is still a useful technique in specific scenarios.

Overview of Row Iteration Methods

Pandas provides several methods to iterate over rows, each suited for different needs based on the size of your dataset, the complexity of operations, and the performance requirements. Let’s look at the four most common methods of iterating over DataFrame rows:

  1. Using iterrows(): This is a straightforward method that allows iteration over DataFrame rows, where each row is returned as a tuple of (index, pandas Series). It’s simple but relatively slow for large datasets.
  2. Using itertuples(): This method returns rows as namedtuples, which are faster and more memory-efficient than iterrows(). It is best suited for larger datasets that require better performance.
  3. Using apply(): While apply() is not strictly an iteration method, it can be used to apply a function across rows or columns in a DataFrame. This is more efficient and suitable for complex operations across rows.
  4. Using index-based iteration (iloc[] and loc[]): For more control over row access, iloc[] (integer-location based) and loc[] (label-based) allow you to access specific rows directly for modification or analysis.

In this section, we will explore these methods in detail, discussing their advantages, limitations, and appropriate use cases.

Understanding iterrows() for Simple Iteration

The iterrows() function is one of the most straightforward ways to iterate over rows in a pandas DataFrame. It yields each row as a tuple of the row’s index and its corresponding data as a pandas Series. This method is easy to use and works well for simple tasks where the data size is small.

To use iterrows(), you call the method on your DataFrame and loop through the resulting iterator. For example, in a small dataset, if you need to compute or check a value for each row and then update or return it, iterrows() provides a simple way to access the row index and the data.

Advantages:

  • Simple and easy to use for small DataFrames.
  • Provides direct access to row data as a pandas Series.
  • Works well for quick, ad-hoc operations or small data manipulations.

Limitations:

  • Slower for larger datasets, as each row is returned as a pandas Series, which adds overhead.
  • Not as memory-efficient as other methods like itertuples().
  • Since it returns rows as Series, accessing data within a row can be slower compared to other approaches like itertuples() or vectorized operations.

Introduction to itertuples() for Faster Iteration

When iterating over large datasets, iterrows() becomes inefficient due to its overhead. Instead, itertuples() provides a more efficient way to iterate over rows. The itertuples() method returns rows as namedtuples, which are faster and more memory-efficient. Namedtuples offer direct access to row values via attributes, making it faster to retrieve individual elements compared to accessing them from a pandas Series.

This method is particularly useful when you have a large dataset and performance is a concern. Namedtuples are lightweight, faster, and provide better memory usage since they do not create a pandas Series for each row.

Advantages:

  • More memory-efficient and faster than iterrows().
  • Provides row data as namedtuples, allowing for more direct and faster access to the values.
  • Ideal for large datasets where performance is a priority.

Limitations:

  • Namedtuples do not offer the full functionality of a pandas Series (e.g., pandas-specific methods).
  • Not as intuitive or flexible for complex transformations as methods like apply().

Using apply() for Complex Row-Wise Transformations

For tasks that require more complex transformations or computations on rows, the apply() method can be extremely useful. While not strictly an iteration method, apply() allows you to apply a function to each row or column in a DataFrame. It is much more efficient than manually iterating over rows and allows for the use of custom functions to handle row-specific transformations.

When you use apply(), you can specify whether you want the function to be applied row-wise (axis=1) or column-wise (axis=0). This method is ideal when you need to perform operations like aggregating multiple columns, performing complex mathematical calculations, or applying conditional logic.

Advantages:

  • Highly versatile and powerful for row-wise transformations.
  • Allows the use of custom functions, making it suitable for complex operations.
  • Works faster than manual iteration methods like iterrows() when dealing with simple row-wise transformations.

Limitations:

  • While apply() is generally faster than manual row iteration, it is still slower than vectorized operations in pandas.
  • It may not be as fast as itertuples() for larger datasets, as it still operates row by row.

Index-Based Iteration with iloc[] and loc[]

Sometimes, you may need to directly access specific rows in a DataFrame for processing or modification. In such cases, index-based iteration using iloc[] and loc[] is ideal. Both iloc[] and loc[] allow you to access specific rows or columns based on their integer position (iloc[]) or label (loc[]).

  • iloc[]: Used for integer-based indexing, meaning you can access rows and columns by their position in the DataFrame (i.e., by row number).
  • loc[]: Used for label-based indexing, which allows you to access rows and columns by their labels (i.e., row name).

Both methods are useful when you want to target specific rows or subsets of data for detailed processing or updates, rather than performing an operation on the entire dataset.

Advantages:

  • Precise control over specific rows and columns.
  • Ideal for conditional operations or targeted updates where you want to work with certain rows in a DataFrame.
  • More flexible than using iterrows() or itertuples() when dealing with specific rows.

Limitations:

  • Not suited for row-wise operations across the entire dataset.
  • More appropriate for specific row manipulation rather than general iteration.

Each method of iterating over rows in a pandas DataFrame has its strengths and is suited for different tasks depending on the dataset size and operation complexity. For small datasets and simple operations, iterrows() can be effective. For larger datasets requiring better performance, itertuples() is ideal. When complex row-wise transformations or mathematical operations are required, apply() provides the flexibility needed for custom logic. Finally, for specific row access, iloc[] and loc[] provide more precision and control. By choosing the appropriate method based on your specific needs, you can optimize both the performance and clarity of your data manipulation processes.

Exploring Iteration Methods in Pandas for Different Use Cases

In this section, we continue our exploration of the different methods available for iterating over rows in a Pandas DataFrame. We will focus on providing more in-depth insights into each of these methods and how they can be applied to real-world scenarios.

Understanding iterrows() for Simple Tasks

The iterrows() method is one of the most widely used methods for iterating over rows in a Pandas DataFrame. It is very easy to use and is quite effective when working with small datasets or simple tasks. When using iterrows(), each row in the DataFrame is returned as a tuple containing two elements: the index and the row data as a Pandas Series. This makes it easy to access the data for each row and perform operations on it.

Pros and Cons of iterrows()

For tasks that require small-scale, exploratory data analysis, iterrows() is quite useful. It is simple and allows for quick custom operations to be applied to each row. For example, if you are working with a small number of records and need to process each row with simple conditional logic or arithmetic, iterrows() is a perfect choice.

However, as with any method, iterrows() comes with some drawbacks. One of the biggest limitations is its performance when working with large datasets. Since it involves iterating over each row individually, it can quickly become slow as the size of the dataset increases. Additionally, iterrows() returns each row as a Pandas Series, which may result in slower access speeds compared to other methods like itertuples(). This can become problematic when dealing with large datasets or when performance is critical.

Example Use Case: Basic Row Operations

Consider a situation where you are tasked with calculating the grades of students based on their scores in an exam. For a small dataset, using iterrows() is a good approach to quickly assign grades. The row-wise operation here is simple, as you just need to evaluate the score and assign a corresponding grade.

In such a case, iterrows() works efficiently because the dataset is small, and the operations required are simple and involve just a few columns.

Leveraging itertuples() for Large Datasets

While iterrows() is useful for simple operations on smaller datasets, itertuples() shines when dealing with large datasets where performance is critical. itertuples() returns each row as a namedtuple, which is much faster and more memory-efficient than the Pandas Series returned by iterrows().

Namedtuples are lightweight and provide access to each row’s values as attributes, making it easier to work with. The key benefit of itertuples() is that it is significantly faster than iterrows() because namedtuples are more efficient than Series objects, especially when the dataset is large.

Pros and Cons of itertuples()

itertuples() should be your go-to method when performance is a concern. It is ideal for situations where you need to iterate through a large DataFrame but cannot afford the performance penalty that comes with iterrows(). It is particularly useful for large datasets in which row-wise operations need to be performed efficiently.

However, like iterrows(), itertuples() also has some limitations. While it is faster and more memory-efficient, it is not as flexible as apply() for complex transformations. For example, if you need to perform multiple complex operations across different columns of each row, itertuples() may not be the best choice. Additionally, since it returns rows as namedtuples, you lose access to some of the functionality that comes with Pandas Series, such as applying pandas-specific methods directly on the row.

Example Use Case: Large Dataset Processing

If you are working with a large dataset containing sales data, and you need to calculate some aggregated values based on each transaction, itertuples() is a great option. The faster iteration allows you to quickly go through the dataset, apply the necessary transformations, and extract insights.

By using itertuples(), you can process hundreds of thousands of rows in a fraction of the time it would take with iterrows(), especially when dealing with simple calculations like summing or multiplying column values.

Using apply() for Complex Row-Wise Operations

While iterrows() and itertuples() are useful for row iteration, apply() is often the go-to solution for more complex row-wise operations in Pandas. The apply() method allows you to apply a function to each row or column, making it highly versatile for a variety of tasks. Unlike iterrows() and itertuples(), apply() can be used to execute complex operations across multiple columns or even rows, making it ideal for scenarios requiring transformations that cannot be easily vectorized.

Pros and Cons of apply()

apply() offers a significant advantage when performing more complex transformations. It allows you to define a custom function that can be applied to each row or column in the DataFrame. This provides a high degree of flexibility and makes it suitable for tasks such as aggregations, conditional logic, or applying custom functions.

However, while apply() is powerful, it is still not as fast as vectorized operations, which are the most efficient way to work with data in Pandas. For smaller datasets, the performance impact may not be noticeable, but for large datasets, apply() can become slower than alternatives like itertuples().

Another limitation is that apply() requires you to write custom functions, which can make the code less readable for simple tasks. It also involves more overhead compared to direct iteration methods like iterrows() or itertuples().

Example Use Case: Applying Complex Functions

Suppose you are working with a dataset of individuals’ height and weight, and you need to calculate the Body Mass Index (BMI) for each person. The BMI formula involves both height and weight, making it a perfect candidate for row-wise application using apply(). You can define a function to calculate the BMI and then apply it to each row, which would allow you to compute the BMI for the entire dataset at once.

Accessing Specific Rows Using iloc[] and loc[]

In some situations, you may not need to iterate over all rows but instead want to access or modify specific rows. For such cases, iloc[] and loc[] offer precise row selection and are highly useful when you need to focus on a subset of the data.

  • iloc[]: This method allows you to access rows based on their integer position. For example, df.iloc[0] would give you the first row, while df.iloc[5:10] would return rows 5 to 9.
  • loc[]: This method allows you to access rows based on their labels. It is label-based, which makes it useful when you want to work with named indices or labels.

Both methods are ideal for targeted operations, such as conditional updates or examining specific rows.

Example Use Case: Specific Row Manipulation

For example, suppose you have a dataset containing transactions, and you want to flag all transactions above a certain amount. Instead of iterating through all rows, you can simply use loc[] to select rows that meet your condition (e.g., transactions greater than a certain value) and apply the necessary modifications.

Choosing the Right Iteration Method

Selecting the best iteration method in Pandas depends on the task at hand and the size of the dataset. For small datasets or quick operations, iterrows() is simple and efficient. For larger datasets where performance is critical, itertuples() provides a more efficient alternative. If you need to apply complex row-wise transformations, apply() is a flexible and powerful option. Finally, for precise control over specific rows, iloc[] and loc[] are great choices for targeting and manipulating specific data subsets.

By understanding the strengths and limitations of each method, you can make more informed decisions and improve the efficiency of your data processing tasks in Pandas.

Optimizing Performance and Best Practices for Iterating Over Rows in Pandas

In this section, we will dive deeper into the best practices, performance considerations, and optimization strategies when iterating over rows in Pandas. Understanding when and how to choose the right method can help you efficiently process your data while ensuring that you don’t encounter performance bottlenecks.

Understanding the Performance Pitfalls of Iterating Rows

When working with large datasets, one of the first things to consider is the performance impact of iterating over rows. While methods like iterrows() and itertuples() are easy to implement and offer flexibility, they can also introduce performance bottlenecks. This is especially true when you are iterating over thousands or millions of rows.

The inherent issue with methods like iterrows() is that they are slower due to the fact that they generate a Pandas Series for each row. While this is not a problem for small datasets, it can become costly when working with large datasets. Similarly, even though itertuples() is faster, it still involves iterating through each row one by one, which can still be slower than using vectorized operations, especially when dealing with large datasets.

Thus, it is important to understand the limitations of row iteration and look for ways to optimize your code.

Optimizing Row-Wise Iteration with Vectorized Operations

The most efficient way to handle operations in Pandas is by using vectorized operations. Vectorized operations are functions that operate on entire columns (or even entire DataFrames) at once, without the need to iterate over each row individually. These operations are optimized for performance, making them much faster than row-wise iteration.

If possible, try to convert your row-wise iteration logic into a vectorized operation. For example, consider calculating the Body Mass Index (BMI) for a dataset of people. Instead of iterating over each row to calculate the BMI, you can perform the calculation directly on the columns of height and weight.

In this case, the vectorized operation would be:

python

Copy

df[‘BMI’] = df[‘Weight’] / (df[‘Height’] ** 2)

This single line of code performs the BMI calculation for all rows at once, rather than iterating over each row. This is both faster and more efficient, particularly for larger datasets.

Choosing Between iterrows(), itertuples(), and apply()

While vectorized operations should always be your first choice when working with Pandas, there are situations where you may need to iterate over rows for specific tasks that can’t be easily vectorized. In such cases, it’s important to choose the appropriate iteration method based on the task at hand.

  1. Use iterrows() for Small Datasets
    iterrows() is simple to use and works well for small datasets or exploratory data analysis. It is easy to implement when you need to perform simple operations on rows. However, for larger datasets, it is not recommended due to performance issues.
  2. Use itertuples() for Larger Datasets
    When performance is crucial, and the dataset is large, itertuples() is the better choice. It is faster and more memory-efficient than iterrows() because it returns each row as a namedtuple. Namedtuples are lighter and more efficient, which allows for faster iteration.
  3. Use apply() for Complex Operations
    If you need to perform complex row-wise transformations, apply() is the best option. It allows you to apply custom functions across rows, making it highly flexible. However, it may not be as fast as vectorized operations, so it is best used when you need to perform operations that cannot be easily vectorized.
  4. Use iloc[] and loc[] for Specific Row Access
    For situations where you need to access or modify specific rows, iloc[] and loc[] are excellent choices. They allow you to select rows based on index position (iloc[]) or labels (loc[]). These methods are particularly useful when you need to target specific rows based on conditions.

Best Practices for Efficient Iteration

To ensure that your iteration is as efficient as possible, here are some best practices that can help you avoid performance issues:

Use Vectorized Operations Whenever Possible

As mentioned earlier, vectorized operations are the fastest and most efficient way to process data in Pandas. Whenever you can replace row-wise iteration with a vectorized operation, you should do so. Pandas functions like .apply(), .map(), .sum(), .mean(), and others operate on entire columns, making them much faster than iterating over individual rows.

Avoid Using Loops for Simple Operations

For simple tasks like adding, multiplying, or performing basic mathematical operations, avoid using loops or iteration methods like iterrows() or itertuples(). These operations can be performed much faster using vectorized operations. For example, if you need to add two columns in a DataFrame, simply do:

python

Copy

df[‘new_column’] = df[‘col1’] + df[‘col2’]

This method is significantly faster and more efficient than looping over each row and adding the values manually.

Minimize the Use of apply() for Simple Tasks

While apply() is useful for complex operations, it is slower than vectorized operations. For simple tasks, try to avoid using apply() and rely on Pandas’ built-in functions, which are optimized for performance. For example, if you are calculating the mean of a column, use the mean() function rather than applying a custom function with apply().

Use itertuples() for Performance

If you absolutely must iterate over rows and performance is important, always opt for itertuples() over iterrows(). Namedtuples are more memory-efficient and faster to access than Pandas Series objects. For large datasets with simple operations, itertuples() is the most efficient row-wise iteration method.

Profiling Code for Performance Bottlenecks

In situations where you’re not sure which method is best for performance, it’s important to profile your code. Python provides tools like the time module and cProfile to help you measure the execution time of your code. By profiling your code, you can identify which iteration method is causing the performance bottleneck and make informed decisions about which method to use.

In this section, we’ve explored the performance considerations and best practices for iterating over rows in a Pandas DataFrame. The key takeaway is that vectorized operations should always be your first choice, as they provide the best performance. When you do need to iterate, select the appropriate method based on the size of your dataset and the complexity of the operation you’re performing. By following these best practices, you can ensure that your code is both efficient and scalable, even when working with large datasets.

Advanced Techniques for Optimizing Row Iteration in Pandas and Real-World Applications

While iterating over rows in a DataFrame may seem like a basic task, mastering it and optimizing it for performance can have a significant impact on the overall efficiency of your data manipulation processes. In this section, we will explore advanced techniques for optimizing row iteration in Pandas, particularly when dealing with complex workflows and large datasets. Additionally, we will delve into real-world applications where effective row iteration is critical for data analysis and manipulation.

Advanced Techniques for Optimizing Row Iteration

When working with extremely large datasets that can’t be efficiently handled with Pandas alone, parallel processing frameworks like Dask can be extremely useful. Dask is designed to parallelize operations on larger-than-memory datasets, allowing you to scale your computations across multiple cores or even a cluster of machines. If you find that your row iteration is slowing down due to the size of the dataset, Dask provides a seamless interface for performing operations similar to Pandas but in a parallelized manner.

For instance, Dask allows you to use familiar Pandas-like syntax for row-wise operations while leveraging the power of parallel computing. By partitioning the data and distributing it across multiple cores, Dask enables you to perform large-scale row-wise operations efficiently without running into memory issues or significant slowdowns.

Optimizing with Cython

For operations that require heavy computation, writing custom functions with Cython can significantly speed up the row iteration process. Cython is a programming language that acts as a superset of Python, enabling you to write C-like code in Python. This can be especially useful when you need to speed up performance-critical operations that can’t be easily vectorized.

Cython allows you to optimize certain bottleneck functions in your code. For instance, if you are performing a row-wise calculation with a complex mathematical function, using Cython can help speed it up by compiling the function into C code, which executes much faster than pure Python.

By using Cython, you can optimize parts of your code that require intensive row-wise calculations, leading to significant performance gains.

Using Numba for JIT Compilation

Another advanced optimization technique is using Numba, a Just-In-Time (JIT) compiler for Python. Numba can be used to speed up Python functions by compiling them into machine code at runtime. It works particularly well for numerical computations, making it ideal for row-wise operations that involve complex calculations.

Numba allows you to apply JIT compilation to row-wise operations in Pandas, improving the performance without having to write Cython code manually. Numba speeds up the execution of functions, making them more efficient when applied row-wise across large DataFrames.

Numba can help significantly reduce the execution time of row-wise operations, especially when working with large datasets and intensive mathematical computations.

Real-World Applications of Efficient Row Iteration in Pandas

Efficient row iteration plays a vital role in various real-world applications, especially when dealing with large-scale data analysis, machine learning pipelines, or financial computations. Below are some practical scenarios where optimizing row iteration is critical:

Data Preprocessing for Machine Learning

In machine learning, data preprocessing is one of the most important steps before training models. Often, raw datasets contain missing values, outliers, or other anomalies that need to be cleaned or transformed. Row-wise iteration is crucial for tasks such as imputing missing values, feature engineering, or applying custom transformations based on certain conditions.

For example, in a dataset of customer transactions, you might want to apply a custom transformation to categorize transactions based on amounts, create new features like transaction frequency, or impute missing values in certain columns. By iterating efficiently over the rows, you can preprocess the data in a way that makes it ready for machine learning models.

However, this is where vectorized operations and parallel computing can come in handy. By replacing row-wise iteration with vectorized operations or parallelized libraries like Dask, you can preprocess large datasets more efficiently, reducing processing time and resource consumption.

Financial Data Analysis

Financial data analysis often involves working with large datasets that contain transaction records, historical stock prices, or economic indicators. In these cases, iterating over rows may be required to flag certain transactions, compute financial metrics (like risk assessments), or aggregate data over certain periods.

For example, you might need to calculate daily returns, moving averages, or identify outlier transactions based on certain conditions. Given the size of financial datasets, optimizing row-wise iteration can make the process faster and more efficient, especially when dealing with large volumes of data in real-time.

Optimizing this task will ensure that you can perform financial analysis in a timely manner, generating insights more efficiently.

Log Analysis and Monitoring Systems

In log analysis and monitoring systems, you may need to iterate over rows in a DataFrame to parse log entries, filter records based on error types, or aggregate data based on timestamps. For instance, in a web application, analyzing user logs to detect anomalies or security breaches requires row-wise operations to identify patterns, count occurrences, or apply filters.

Optimizing row iteration in log analysis tasks is essential because logs can grow rapidly in size. Whether you’re processing logs from web servers, application logs, or network data, using efficient iteration methods can help speed up the process and ensure timely analysis.

Business Intelligence (BI) and Reporting

In Business Intelligence (BI) applications, you often need to aggregate, filter, and process data to generate meaningful insights. Row-wise iteration may be required to perform specific calculations, such as determining customer lifetime value, computing aggregate sales by region, or analyzing employee performance metrics.

In BI systems, it is essential to handle large datasets quickly and efficiently, especially when generating reports in real-time or on a scheduled basis. By leveraging the power of optimized iteration techniques, you can improve the speed and accuracy of the reports and dashboards that drive business decisions.

Healthcare and Medical Research

In healthcare and medical research, row iteration is frequently used for analyzing patient data, calculating disease prevalence, or tracking clinical trial results. For example, you might need to iterate over patient records to identify high-risk individuals based on certain criteria, calculate survival rates, or perform cohort analysis.

Efficient row iteration is particularly important when working with large health datasets that involve hundreds or thousands of patient records. Optimizing the process ensures that you can perform detailed analysis quickly and accurately, aiding in medical decision-making and research outcomes.

Efficient row iteration is a fundamental task when working with Pandas, especially when dealing with large datasets or complex transformations. In this section, we explored advanced techniques for optimizing performance, including using parallel computing frameworks like Dask, applying Cython for custom function optimization, and leveraging Numba for JIT compilation.

Additionally, we explored real-world applications of efficient row iteration, highlighting how it plays a crucial role in data preprocessing for machine learning, financial data analysis, log analysis, and business intelligence. Understanding when to use vectorized operations and when to opt for row iteration methods like iterrows(), itertuples(), or apply() can significantly enhance the performance of your data processing workflows.

By following these techniques and best practices, you can ensure that your data manipulation tasks are both efficient and scalable, enabling you to handle even the most complex data processing challenges with ease.

Final Thoughts

Iterating over rows in a Pandas DataFrame is an essential operation in data processing, but it often comes with challenges, especially when working with large datasets or complex transformations. While Pandas excels at vectorized operations that process data quickly and efficiently, there are scenarios where row-wise iteration is necessary. By understanding the various methods for iterating over rows and their respective advantages, you can choose the most appropriate approach for your specific use case.

From simple methods like iterrows() for small datasets to more advanced solutions like itertuples() for larger datasets, apply() for complex transformations, and index-based iteration for specific rows, each technique has its place in your data manipulation toolkit. When dealing with larger data or performance-critical operations, it’s essential to optimize row iteration by considering parallel computing frameworks such as Dask, or utilizing performance-enhancing libraries like Numba and Cython.

By mastering these techniques and applying them appropriately, you can streamline your data processing workflows, minimize bottlenecks, and significantly improve the speed and efficiency of your analysis. Real-world applications like machine learning preprocessing, financial data analysis, and log monitoring all benefit from the ability to iterate over rows efficiently, ultimately empowering you to make better, faster decisions based on data.

To stay ahead in data science and software development, it’s important to remain agile, continuously learning, and adapting to the evolving landscape of data manipulation. The tools and techniques discussed here will ensure that you have the foundation to handle any challenges that arise in the world of data processing and analysis. By applying these best practices, you can confidently work with data at scale, unlock deeper insights, and drive more impactful outcomes in your projects.