【源码解析】深入Pandas的心脏DataFrame 含十大功能、源码实现与编程知识点

作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。
会一些的技术:数据分析、算法、SQL、大数据相关、python
欢迎加入社区:码上找工作
作者专栏每日更新:
LeetCode解锁1000题: 打怪升级之旅
python数据分析可视化:企业实战案例
python源码解读
程序员必备的数学知识与应用
备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级


探索Pandas的心脏—— DataFrame

引言

Pandas 的 DataFrame 是现代数据科学工具箱中的一块基石,提供了强大且灵活的数据结构来支持各种复杂的数据操作。作为 Python 最受欢迎的数据处理库之一,Pandas 通过 DataFrame 类实现了一个功能丰富的两维数据表格。这个表格不仅能够处理尺寸可变的异质类型数据,还包含了标签化的轴(行和列),使得数据操作既直观又便捷。

DataFrame源码 在pandas/core/frame.py里在这里插入图片描述整个 类代码超过1万行,里面包括注释与实现代码,为方便读者阅读我们把文章按两个部分进行说明,先从使用上从功能说明,再看源码如何实现,对核心方法进行解读,如果有错误的地方欢迎指正 谢谢大家

DataFrame 概览

在这里插入图片描述
接下来,我们将深入探讨 DataFrame 类中几个关键方法的作用、示例和对应的输出结果,方便更清晰地理解每个方法的实际应用和功能。

1. 构造函数 __init__

功能说明

用于创建一个新的 DataFrame 对象。

示例

import pandas as pd

# 创建 DataFrame 使用字典
data = {'col1': [1, 2], 'col2': [3, 4]}
df = pd.DataFrame(data)
print(df)

输出

   col1  col2
0     1     3
1     2     4

源码说明

Pandas 的 DataFrame 构造函数 __init__ 是一个典型的示例,展示了如何在日常编程中处理多源数据输入、类型检测、条件判断以及数据管理。代码中广泛使用了 if-else 语句来根据不同的数据类型和条件执行不同的逻辑。这种模式非常适用于处理具有多种输入可能性的函数。

  • 类型检测:通过 isinstance() 检查数据类型,根据数据类型的不同选择不同的处理流程。
  • 参数默认值的智能处理:根据输入数据的类型和是否已指定某些参数来动态设置默认值。
    在这里插入图片描述

知识点 按需import

可以看到在616行有一个 import,这里的import放在了elif里面可以减少程序启动的时间,减少不必要的依赖和循环引用,只有在特定条件下才会被触发,在一些大型项目中能有效减少常规操作的开销
在这里插入图片描述

源码逐行解析

def __init__(
        self,
        data=None,
        index: Axes | None = None,
        columns: Axes | None = None,
        dtype: Dtype | None = None,
        copy: bool | None = None,
    ):
        # 根据数据类型确定是否需要复制数据,默认情况下,字典和未指定数据源的情况下复制
        if copy is None:
            if isinstance(data, dict) or data is None:
                copy = True
            else:
                copy = False

        # 如果未提供数据,则初始化为空字典
        if data is None:
            data = {}
        # 如果指定了dtype,则验证和转换数据类型
        if dtype is not None:
            dtype = self._validate_dtype(dtype)

        # 如果数据已经是DataFrame类型,直接使用其内部管理器
        if isinstance(data, DataFrame):
            data = data._mgr

        # 如果数据是管理器类型(BlockManager或ArrayManager)
        if isinstance(data, (BlockManager, ArrayManager)):
            if index is None and columns is None and dtype is None and not copy:
                NDFrame.__init__(self, data)
                return

        # 根据设置决定使用哪种数据管理器
        manager = get_option("mode.data_manager")

        # 初始化数据管理器
        if isinstance(data, (BlockManager, ArrayManager)):
            mgr = self._init_mgr(
                data, axes={"index": index, "columns": columns}, dtype=dtype, copy=copy
            )
        # 如果数据是字典类型
        elif isinstance(data, dict):
            mgr = dict_to_mgr(data, index, columns, dtype=dtype, copy=copy, typ=manager)
        # 如果数据是遮盖数组(MaskedArray)
        elif isinstance(data, ma.MaskedArray):
            import numpy.ma.mrecords as mrecords
            if isinstance(data, mrecords.MaskedRecords):
                mgr = rec_array_to_mgr(
                    data, index, columns, dtype, copy, typ=manager,
                )
                warnings.warn(
                    "Support for MaskedRecords is deprecated and will be removed in a future version.  Pass {name: data[name] for name in data.dtype.names} instead.",
                    FutureWarning,
                    stacklevel=2,
                )
            else:
                data = sanitize_masked_array(data)
                mgr = ndarray_to_mgr(
                    data, index, columns, dtype=dtype, copy=copy, typ=manager,
                )
        # 如果数据是 NumPy 数组、Series 或 Index
        elif isinstance(data, (np.ndarray, Series, Index)):
            if data.dtype.names:
                data = cast(np.ndarray, data)
                mgr = rec_array_to_mgr(
                    data, index, columns, dtype, copy, typ=manager,
                )
            elif getattr(data, "name", None) is not None:
                mgr = dict_to_mgr(
                    {data.name: data}, index, columns, dtype=dtype, typ=manager,
                )
            else:
                mgr = ndarray_to_mgr(
                    data, index, columns, dtype=dtype, copy=copy, typ=manager,
                )
        # 如果数据是列表或其他可迭代对象
        elif is_list_like(data):
            if not isinstance(data, (abc.Sequence, ExtensionArray)):
                data = list(data)
            if len(data) > 0:
                if is_dataclass(data[0]):
                    data = dataclasses_to_dicts(data)
                if treat_as_nested(data):
                    if columns is not None:
                        columns = ensure_index(columns)
                    arrays, columns, index = nested_data_to_arrays(
                        data, columns, index, dtype,
                    )
                    mgr = arrays_to_mgr(
                        arrays, columns, index, columns, dtype=dtype, typ=manager,
                    )
                else:
                    mgr = ndarray_to_mgr(
                        data, index, columns, dtype=dtype, copy=copy, typ=manager,
                    )
            else:
                mgr = dict_to_mgr(
                    {}, index, columns, dtype=dtype, typ=manager,
                )
        # 如果数据是标量
        else:
            if index is None or columns is None:
                raise ValueError("DataFrame constructor not properly called!")
            index = ensure_index(index)
            columns = ensure_index(columns)
            if not dtype:
                dtype, _ = infer_dtype_from_scalar(data, pandas_dtype=True)
            if isinstance(dtype, ExtensionDtype):
                values = [
                    construct_1d_arraylike_from_scalar(data, len(index), dtype)
                    for _ in range(len(columns))
                ]
                mgr = arrays_to_mgr(
                    values, columns, index, columns, dtype=None, typ=manager
                )
            else:
                arr2d = construct_2d_arraylike_from_scalar(
                    data, len(index), len(columns), dtype, copy,
                )
                mgr = ndarray_to_mgr(
                    arr2d, index, columns, dtype=arr2d.dtype, copy=False, typ=manager,
                )
        # 确保使用正确的数据管理器类型
        mgr = mgr_to_mgr(mgr, typ=manager)
        NDFrame.__init__(self, mgr)

2. 数据访问 __getitem__

功能说明

通过列名访问数据。

示例

# 访问列 'col1'
print(df['col1'])

输出

0    1
1    2
Name: col1, dtype: int64

源码说明

__getitem__ 方法在这里插入图片描述
这是一个非常核心的函数,用于索引和选择数据。这个方法支持多种类型的键(key),处理逻辑相对复杂。我们逐行解析,补充每行代码的功能和作用的注释。

知识点 数组转切片

使用 lib.maybe_indices_to_slice
在这里插入图片描述

这个方法是理解 Pandas 数据获取逻辑的一个很好的例子,展示了在实际项目中如何处理复杂和多变的数据访问需求。在 Pandas 的源代码中,lib.maybe_indices_to_slice 函数是一个优化函数,其目的是尝试将一个整数数组转换成一个更为高效的切片对象(slice 对象)。这种转换在处理大型数据集时尤其有用,因为使用切片通常比使用等价的整数数组索引具有更高的性能。

为什么使用切片比整数数组索引更高效?

切片操作只需要定义起点、终点和步长,无论数据集有多大,切片描述的复杂性都不增加,Python 也可以优化这些操作。相反,整数数组索引需要存储每个索引点,其内存消耗和处理时间与索引数量直接相关,尤其是在大数据集上进行随机访问时。

功能解释
lib.maybe_indices_to_slice 函数的工作原理是检查提供的整数数组是否可以表示为一个简单的切片。如果可以,函数将返回一个对应的 slice 对象;如果不可以,函数将返回原始数组。这个检查基于以下几点:

  1. 连续性:数组中的值是否形成一个连续的序列,即每两个相邻值的差是常数。
  2. 边界:序列的起始和结束点必须明确。

具体应用

在 DataFrame 的 __getitem__ 方法中使用这个函数可以提高数据选择的效率。例如,如果你想选择 DataFrame 中的第 100 到第 200 行,理论上直接使用 df[100:201](使用切片)要比 df[list(range(100, 201))](使用整数数组)更有效率。lib.maybe_indices_to_slice 是一个用于优化数据索引的工具,尤其在处理大型数据集和需要频繁数据访问的应用中非常有用。理解这个函数的工作原理可以帮助开发者编写更高效、更可扩展的 Pandas 应用。

源码逐行解析

def __getitem__(self, key):
        key = lib.item_from_zerodim(key)  # 处理零维数组,转换成标量
        key = com.apply_if_callable(key, self)  # 如果key是可调用的,则传递self

        if is_hashable(key):  # 检查key是否可以哈希,即是否可以作为字典的键
            if self.columns.is_unique and key in self.columns:  # 如果列名是唯一的并且key在列名中
                if isinstance(self.columns, MultiIndex):  # 如果列是多级索引
                    return self._getitem_multilevel(key)  # 使用多级索引获取项
                return self._get_item_cache(key)  # 使用缓存获取项

        # 尝试将key转换为可以索引的切片对象
        indexer = convert_to_index_sliceable(self, key)
        if indexer is not None:
            if isinstance(indexer, np.ndarray):  # 如果索引器是ndarray
                indexer = lib.maybe_indices_to_slice(
                    indexer.astype(np.intp, copy=False), len(self)  # 尝试将索引转换为切片
                )
                if isinstance(indexer, np.ndarray):
                    return self.take(indexer, axis=0)  # 使用ndarray索引取数据
            return self._slice(indexer, axis=0)  # 使用切片或部分字符串日期索引获取数据

        if isinstance(key, DataFrame):  # 如果key是DataFrame
            return self.where(key)  # 返回布尔索引DataFrame

        if com.is_bool_indexer(key):  # 如果key是布尔索引
            return self._getitem_bool_array(key)  # 使用布尔数组获取项

        # 解释元组作为键的集合,仅对非多级索引的情况
        is_single_key = isinstance(key, tuple) or not is_list_like(key)

        if is_single_key:
            if self.columns.nlevels > 1:
                return self._getitem_multilevel(key)  # 对多级索引使用键
            indexer = self.columns.get_loc(key)  # 获取键的位置
            if is_integer(indexer):  # 如果位置是整数
                indexer = [indexer]  # 转换为列表

        else:
            if is_iterator(key):  # 如果键是迭代器
                key = list(key)  # 转换为列表
            indexer = self.loc._get_listlike_indexer(key, axis=1)[1]  # 获取类似列表的索引器

        if getattr(indexer, "dtype", None) == bool:
            indexer = np.where(indexer)[0]  # 获取布尔索引的位置

        data = self._take_with_is_copy(indexer, axis=1)  # 根据索引器取数据

        if is_single_key:
            if data.shape[1] == 1 and not isinstance(self.columns, MultiIndex):
                return data._get_item_cache(key)  # 如果是单个键,则尝试从缓存获取

        return data  # 返回索引得到的数据

从这段代码能学习到什么?

  1. 灵活的索引支持:这段代码展示了如何支持从简单的列名到复杂的布尔索引和多级索引等多种索引方式。
  2. 类型检查与转换:代码中频繁进行类型检查和转换,这是处理不确定输入时的常见做法。
  3. 错误和特殊情况的处理:方法中包含了对错误情况的预判和处理,如输入类型的验证和转换。
  4. 代码的模块化:通过调用 _getitem_multilevel、_get_item_cache、_slice 等方法,代码避免了冗长和重复,提高了可读性和可维护性。

3. 数据设置 __setitem__

功能说明

通过列名设置列数据。

示例

# 添加新列 'col3'
df['col3'] = [5, 6]
print(df)

输出

   col1  col2  col3
0     1     3     5
1     2     4     6

源码说明

这段代码是 Pandas DataFrame__setitem__ 方法,负责处理对 DataFrame 对象的索引赋值操作。这个方法支持多种数据类型作为键(key)和值(value),其复杂性体现在如何正确地将值分配到指定的键。下面是对该方法的逐行解析,包括补充的注释,解释每行代码的功能和作用。

知识点 处理重复列名

虽然我们能够通过 iloc 正确地更新重复列名的数据,但在实际应用中,最好避免在 DataFrame 中使用重复的列名,因为这会导致数据操作的复杂性显著增加,并增加出错的风险。如果可能,应该在数据处理的早期阶段清理或修改列名,确保每个列名的唯一性。

在第3604行开始 是对列名的判断,可以看到

elif (
    is_list_like(value)  # 如果value是列表或类似列表结构
    and not self.columns.is_unique  # 并且列名不唯一
    # key对应多列并且value的长度与之匹配
    and 1 < len(self.columns.get_indexer_for([key])) == len(value)  
):
    # 如果要设置的列名是重复的
    self._setitem_array([key], value)  
else:
    # 将列表形式的value赋值到重复的列名上 所有重复的都会被重新赋值
    self._set_item(key, value)

让我们通过一个具体的代码示例来展示如何在 DataFrame 中处理不唯一列名的情况。在这个例子中,我们将创建一个包含重复列名的 DataFrame,然后演示如何正确地更新其中的数据以维护数据一致性。

示例代码
假设我们有一个数据集,其中某些列的名字是重复的,现在我们需要更新这些列的数据会导致所有的所有这个列名的值。

import pandas as pd
import numpy as np

# 创建一个具有重复列名的DataFrame
data = {
    "A": [1, 2, 3],
    "B": [4, 5, 6],
    "A": [7, 8, 9]  # 重复的列名 "A"
}

df = pd.DataFrame(data)
print("Original DataFrame with duplicate column names:")
print(df)

# 尝试更新第二个 "A" 列的数据
try:
    # 这会引起歧义,因为有两个 "A" 列
    df['A'] = [10, 11, 12]
except Exception as e:
    print(f"Error: {e}")

print("\nDataFrame after attempting to update 'A':")
print(df)

# 正确的更新方法
# 我们先获取所有名为"A"的列的索引,然后指定要更新的列
columns = df.columns.get_loc("A")
df.iloc[:, columns] = [13, 14, 15]

print("\nDataFrame after correctly updating 'A' columns:")
print(df)

下图输入直接变更重复列名的值,被异常捕获了,虽然也能执行成功,但是应该尽量避免
在这里插入图片描述

源码逐行解析

def __setitem__(self, key, value):
        key = com.apply_if_callable(key, self)  # 如果key是可调用的,应用它并更新key

        # 尝试将key转换为可以对行进行切片的索引器
        indexer = convert_to_index_sliceable(self, key)
        if indexer is not None:
            # 如果indexer是一个切片或者可以转换为切片(例如部分字符串日期索引)
            return self._setitem_slice(indexer, value)  # 使用切片进行赋值

        # 如果key是DataFrame或者具有两个维度(ndim==2),例如二维数组
        if isinstance(key, DataFrame) or getattr(key, "ndim", None) == 2:
            self._setitem_frame(key, value)  # 使用DataFrame或二维结构设置多个值
        elif isinstance(key, (Series, np.ndarray, list, Index)):
            self._setitem_array(key, value)  # 如果key是一维数组或类似结构,使用数组方式赋值
        elif isinstance(value, DataFrame):
            self._set_item_frame_value(key, value)  # 如果value是DataFrame,按DataFrame赋值
        elif (
            is_list_like(value)  # 如果value是列表或类似列表结构
            and not self.columns.is_unique  # 并且列名不唯一
            and 1 < len(self.columns.get_indexer_for([key])) == len(value)  # key对应多列并且value的长度与之匹配
        ):
            # 如果要设置的列名是重复的
            self._setitem_array([key], value)  # 将列表形式的value赋值到重复的列名上
        else:
            # 最基本的情况,设置单一列
            self._set_item(key, value)  # 在DataFrame中设置单个列的值

从这段代码能学习到什么?

  1. 灵活的参数处理__setitem__ 方法展示了如何处理不同类型的键和值,这包括可调用对象、数据结构(如DataFrame和Series),以及标量值。这种多样性的处理能力是处理复杂数据结构时的一项重要技能。

  2. 条件逻辑的应用:该方法通过多个 if-elif 分支来确定如何处理不同的键和值类型。这种条件逻辑是编写能够应对多种情况的健壮代码的基础。

  3. 复杂的数据赋值逻辑:处理键值对赋值时,考虑了列名是否唯一、数据维度、以及数据类型等因素,展示了在复杂环境中如何安全地修改数据。

  4. 数据一致性的维护:在设置数据时考虑了列名的唯一性和数据的对齐问题,这是维护数据一致性和避免数据错误的关键环节。

  5. 错误和特殊情况的处理:通过对特殊情况的预判和处理,如列名重复或键值对不匹配的情况,增强了方法的健壮性。

4. 数据合并 merge

功能说明

执行类似数据库的表合并操作。

示例

df1 = pd.DataFrame({'key': ['K0', 'K1', 'K2'], 'A': ['A0', 'A1', 'A2']})
df2 = pd.DataFrame({'key': ['K0', 'K1', 'K2'], 'B': ['B0', 'B1', 'B2']})

# 合并 df1 和 df2
result = pd.merge(df1, df2, on='key')
print(result)

输出

  key   A   B
0  K0  A0  B0
1  K1  A1  B1
2  K2  A2  B2

源码说明

merge方法本身并不执行合并逻辑,而是将所有参数及自身作为参数传递给另一个负责实际合并操作的函数。这种设计模式(委托)有助于保持代码的组织性和可维护性,尤其是在功能逻辑复杂或可能在多个地方被重用时
在这里插入图片描述
我们来看一下merge操作类源码,发现有很多内部的方法,这里做了一些标记,可以看到整体的结构按 输入、输出 - 功能 - 检验 来覆盖所有的方法。此外这里有单一职责原则(每个函数只做一件事情),以及封装(将复杂的逻辑封装在方法内部,对外提供简洁的接口)。通过学习这些方法,在自己的项目中实施类似的模式,提高代码的健壮性和可维护性。
在这里插入图片描述
在get_result中,使用 concatenate_block_managers 函数合并左右两侧的数据块管理器,这是合并过程中的核心操作,负责物理合并数据

在这里插入图片描述

知识点 列表推导式

列表推导式是 Python 中一个非常强大和表达丰富的特性,用于创建列表。这种方式可以将循环和条件判断结合在一起,生成列表,使得代码既简洁又高效。在 concatenate_block_managers 函数中使用的列表推导式是一个很好的例子,展示了如何在实际应用中利用这个特性。

concat_plans = [
    get_mgr_concatenation_plan(mgr, indexers) for mgr, indexers in mgrs_indexers
]

这里,concat_plans 通过遍历 mgrs_indexers 列表(每个元素都是一个包含 BlockManager 和索引器的元组)并对每个元素应用 get_mgr_concatenation_plan 函数来构建。

步骤分解

  1. 输入数据mgrs_indexers 是一个列表,每个元素都是一个元组,其中包含两个元素:mgr(一个 BlockManager 对象)和 indexers(一个字典,包含索引信息)。

  2. 循环和映射:这个列表推导式遍历 mgrs_indexers 列表中的每个元组。对于每个元组,它将元组中的两个元素分别赋给 mgrindexers

  3. 函数应用:对于每个 (mgr, indexers) 对,列表推导式调用 get_mgr_concatenation_plan 函数。这个函数的作用是基于提供的 BlockManager (mgr) 和索引字典 (indexers) 生成一个合并计划。

  4. 输出列表:列表推导式最终生成一个新的列表,该列表包含对每个 mgrs_indexers 元素应用 get_mgr_concatenation_plan 函数后的结果。这意味着每个输入元组都转换成了一个合并计划。

列表推导式的优点

  • 简洁性:列表推导式通过一行代码完成了循环遍历、元素处理和列表创建三个任务。
  • 可读性:对于熟悉 Python 的开发者来说,列表推导式提供了一种易于理解和直观的方式来表达复杂的逻辑。
  • 性能:在许多情况下,列表推导式比等效的传统循环实现更高效,因为它是优化过的 C 语言级别的实现。

使用场景
列表推导式适用于需要从一个列表或任何可迭代对象生成新列表的场景,特别是当生成逻辑包含应用函数、条件过滤等操作时。然而,当逻辑过于复杂或推导式变得难以阅读时,使用传统的循环可能更合适,以保持代码的可读性和维护性。

总的来说,列表推导式是 Python 程序员必备的工具,理解和掌握它可以大大提高编程效率和代码质量。在处理数据处理和转换任务时,它尤其有用,如在数据科学、机器学习等领域。

源码逐行解析

下面是 concatenate_block_managers 函数的逐行解析,包括每行代码的作用和相关注释:

def concatenate_block_managers(mgrs_indexers, axes, concat_axis, copy):
    """
    Concatenate block managers into one.

    Parameters
    ----------
    mgrs_indexers : list of (BlockManager, {axis: indexer,...}) tuples
    axes : list of Index
    concat_axis : int
    copy : bool
    """
    # 创建合并计划,每个元素代表一个BlockManager的合并策略
    concat_plans = [
        get_mgr_concatenation_plan(mgr, indexers) for mgr, indexers in mgrs_indexers
    ]  # 列表推导:对每个BlockManager及其索引器生成合并计划

    # 组合所有的合并计划为一个总的合并计划
    concat_plan = combine_concat_plans(concat_plans, concat_axis)  # 将多个合并计划合成一个统一计划

    # 初始化存储最终合并生成的数据块列表
    blocks = []  # 数据块列表,存储最终合并后的各个数据块

    # 遍历合并计划中的所有放置位置和参与单位
    for placement, join_units in concat_plan:
        # 如果只有一个参与单位并且没有索引器
        if len(join_units) == 1 and not join_units[0].indexers:
            b = join_units[0].block  # 获取单个数据块
            values = b.values  # 获取数据块的值
            if copy:
                values = values.copy()  # 如果需要复制,进行深复制
            elif not copy:
                values = values.view()  # 如果不复制,创建一个视图
            b = b.make_block_same_class(values, placement=placement)  # 创建同类的新数据块
        # 如果所有参与单位都是相同类型
        elif is_uniform_join_units(join_units):
            b = join_units[0].block.concat_same_type(
                [ju.block for ju in join_units], placement=placement  # 连接相同类型的数据块
            )
        else:
            # 使用参与单位的数据块创建一个新的数据块
            b = make_block(
                concatenate_join_units(join_units, concat_axis, copy=copy),  # 连接参与单位
                placement=placement,  # 设置数据块的放置位置
            )
        # 将创建的新数据块添加到blocks列表中
        blocks.append(b)  # 向数据块列表中添加新数据块

    # 创建并返回一个新的BlockManager,管理所有合并后的数据块
    return BlockManager(blocks, axes)  # 使用合并后的数据块和轴创建一个新的BlockManager

5. 分组 groupby

功能说明

按条件分组数据,并支持聚合、变换、过滤。

示例

df = pd.DataFrame({
    'Animal': ['Falcon', 'Falcon', 'Parrot', 'Parrot'],
    'Max Speed': [380., 370., 24., 26.]
})

# 按 'Animal' 列分组并获取最大速度的平均值
mean_speed = df.groupby('Animal')['Max Speed'].mean()
print(mean_speed)

输出

Animal
Falcon    375.0
Parrot     25.0
Name: Max Speed, dtype: float64

知识点 继承父类

这段是聚合函数,使用了super对父类进行调用
在这里插入图片描述

在 Pandas 的源码中,DataFrameGroupBy 类通常继承自 GroupBy 类。GroupBy 类提供了广泛的分组操作的基本功能,包括聚合、过滤、转换和应用(apply)等方法。DataFrameGroupBy 是这些功能的一个具体实现,专门用于处理 DataFrame 对象。

  • GroupBy: 这是一个更一般的基类,提供了大多数分组操作的实现。它定义了分组操作的通用逻辑,如何聚集数据、如何应用函数等。
  • DataFrameGroupBy: 这个类继承自 GroupBy,专门针对 DataFrame 对象。它可能会添加或重写一些方法以适应 DataFrame 的特定需求,但大部分功能仍然依赖于 GroupBy 类提供的实现。

职责与功能:

  • GroupBy 的主要职责是提供一个高效的接口来处理所有 Pandas 对象(如 SeriesDataFrame)的分组操作。它处理数据的分割,应用组函数,然后可能将结果合并回一个合适的数据结构中。

  • DataFrameGroupBy 继承了 GroupBy 的所有功能,并且针对 DataFrame 的特定情况进行了优化。例如,它会处理列与列之间的关系,确保在聚合或变换过程中列的对齐和数据完整性。

方法重载:

DataFrameGroupBy 中,有些方法可能会被重载以提供更针对 DataFrame 的行为,例如处理列名的冲突、保留索引等。而通过使用 super() 调用,DataFrameGroupBy 可以保留 GroupBy 类的原始行为,并在此基础上添加或修改功能,以适应 DataFrame 的特殊需求。

这种设计模式不仅使得 Pandas 库具有很高的灵活性和扩展性,同时也提供了很高的代码复用性和维护性,允许不同类型的 Pandas 对象共享大量的代码和逻辑,同时保留了进行特定优化的空间。

源码逐行解析

在这里插入图片描述

6. 数据透视 pivot

功能说明

用于创建数据透视表,重塑数据。

示例

df = pd.DataFrame({
    'foo': ['one', 'one', 'one', 'two', 'two', 'two'],
    'bar': ['A', 'B', 'C', 'A', 'B', 'C'],
    'baz': [1, 2, 3, 4, 5, 6],
    'zoo': ['x', 'y', 'z', 'q', 'w', 't']
})

pivot_df = df.pivot(index='foo', columns='bar', values='baz')
print(pivot_df)

输出

bar    A  B  C
foo
one    1  2  3
two    4  5  6

知识点 使用 MultiIndex 和索引操作

解读:

  • Pandas 中的 MultiIndex 允许在一个轴上有多个(两个以上)索引级别。这在处理复杂的数据结构时非常有用,特别是当你需要在单个轴上表达更多维度的层次结构时。
  • 在这段代码中,MultiIndex.from_arrays([index, data[columns]]) 创建了一个两级的索引,这使得每个数据点都可以通过一个由 indexcolumns 参数确定的复合键来唯一标识。这对于数据透视非常重要,因为它允许用户根据两个维度来重塑数据。

应用:

  • 使用 MultiIndex 可以极大地增强数据分析的灵活性和数据表示的丰富性。例如,在金融数据分析中,可以使用 MultiIndex 来同时按日期和股票代码索引数据。
  • pivot 函数中,这种索引策略使得将数据从长格式转换为宽格式变得可能,为后续的数据分析和可视化提供了便利。

总结:
这段代码不仅展示了如何使用 Pandas 进行数据透视操作,还突出了 MultiIndex 在实现复杂数据操作中的应用。通过合理使用索引技术,可以有效地提升数据处理任务的效率和灵活性。

源码逐行解析

在这段代码中,我们看到了 Pandas 中的 pivot 函数实现,该函数用于将长格式数据转换成宽格式数据

@Appender(_shared_docs["pivot"], indents=1)  # 使用装饰器添加文档字符串,提高代码可维护性和文档的一致性
def pivot(data, index=None, columns=None, values=None):
    if values is None:  # 当未指定 values 参数时
        cols = [columns] if index is None else [index, columns]  # 确定用作新索引的列
        append = index is None  # 决定是否将现有索引保留在结果DataFrame中
        indexed = data.set_index(cols, append=append)  # 设置新索引,可能保留旧索引
    else:  # 当指定了 values 参数时
        if index is None:  # 如果没有明确指定 index,则使用 DataFrame 的当前索引
            index = data.index
        else:  # 如果指定了 index,则从 data 中获取这个 index 列
            index = data[index]
        index = MultiIndex.from_arrays([index, data[columns]])  # 创建一个多级索引

        if is_list_like(values) and not isinstance(values, tuple):  # 检查 values 是否为列表类型但不是元组
            # 创建一个新的 DataFrame,将指定的 values 列作为数据,多级索引为索引,values 名称作为列名
            indexed = data._constructor(
                data[values].values, index=index, columns=values
            )
        else:  # 对于单个值字段,使用 _constructor_sliced 创建一个 Series
            indexed = data._constructor_sliced(data[values].values, index=index)
    return indexed.unstack(columns)  # 对指定的 columns 列执行 unstack 操作,将长格式转换为宽格式

7. 数据连接 join

功能说明

join 是用于按照索引合并不同 DataFrame 的方法,它提供了一个便捷的接口来合并多个 DataFrame

示例

import pandas as pd

df1 = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                    'B': ['B0', 'B1', 'B2']},
                   index=['K0', 'K1', 'K2'])
df2 = pd.DataFrame({'C': ['C0', 'C1', 'C2'],
                    'D': ['D0', 'D1', 'D2']},
                   index=['K0', 'K2', 'K3'])

# 使用 join 进行连接
result = df1.join(df2)
print(result)

输出

     A   B    C    D
K0  A0  B0   C0   D0
K1  A1  B1  NaN  NaN
K2  A2  B2   C1   D1

知识点 错误处理

在这段代码中,错误处理是一个重要的部分。处理错误的方式能够确保函数的健壮性,防止在错误的使用条件下执行不当的操作。
在这里插入图片描述

解读:

  • 错误检查使用了条件语句来判断 other 对象的类型和状态(例如,检查 Series 是否有名字,检查 on 是否非空)。
  • 通过抛出具体的错误信息(ValueError),开发者可以立即理解问题所在,这对于调试和使用函数的用户来说都是非常有帮助的。

应用:

  • 在实际开发中,合适的错误检查和异常处理可以防止数据损坏和应用崩溃,提高应用的可靠性。
  • 通过明确的错误消息,用户可以更快地诊断问题,提高开发效率。

这段代码展示了如何实现复杂的数据合并逻辑,还体现了良好的错误处理和代码结构设计的重要性

源码逐行解析

这段代码定义了一个名为 _join_compat 的方法,用于合并 DataFrameSeries 对象。其中还调用了merge和concat下面是逐行解析和注释,以说明每行代码的功能:

def _join_compat(
        self,
        other: FrameOrSeriesUnion,
        on: IndexLabel | None = None,
        how: str = "left",
        lsuffix: str = "",
        rsuffix: str = "",
        sort: bool = False,
    ):
        from pandas.core.reshape.concat import concat  # 用于拼接数据帧
        from pandas.core.reshape.merge import merge  # 用于合并数据帧

        if isinstance(other, Series):  # 检查 other 是否为 Series
            if other.name is None:  # Series 必须有名字才能进行合并
                raise ValueError("Other Series must have a name")
            other = DataFrame({other.name: other})  # 将 Series 转换为 DataFrame

        if isinstance(other, DataFrame):  # 检查 other 是否为 DataFrame
            if how == "cross":  # 如果合并方式为 cross
                return merge(
                    self,
                    other,
                    how=how,
                    on=on,
                    suffixes=(lsuffix, rsuffix),
                    sort=sort,
                )  # 使用 pandas 的 merge 函数进行交叉合并
            return merge(
                self,
                other,
                left_on=on,
                how=how,
                left_index=on is None,
                right_index=True,
                suffixes=(lsuffix, rsuffix),
                sort=sort,
            )  # 使用 merge 进行合并,可能使用索引或指定的键
        else:
            if on is not None:
                raise ValueError(
                    "Joining multiple DataFrames only supported for joining on index"
                )  # 如果 on 参数非空但 other 不是 DataFrame,抛出错误

            frames = [self] + list(other)  # 将 self 和 other 组成列表

            can_concat = all(df.index.is_unique for df in frames)  # 检查所有数据帧的索引是否唯一

            # 如果索引唯一,使用 concat 进行拼接
            if can_concat:
                if how == "left":
                    res = concat(
                        frames, axis=1, join="outer", verify_integrity=True, sort=sort
                    )  # 使用 outer 方式拼接,保留所有索引
                    return res.reindex(self.index, copy=False)  # 重新索引到 self 的索引上
                else:
                    return concat(
                        frames, axis=1, join=how, verify_integrity=True, sort=sort
                    )  # 根据 how 参数决定拼接方式

            joined = frames[0]  # 初始化 joined 为第一个数据帧

            for frame in frames[1:]:  # 遍历除第一个外的所有数据帧
                joined = merge(
                    joined, frame, how=how, left_index=True, right_index=True
                )  # 使用 merge 基于索引进行合并

            return joined  # 返回最终合并后的结果

8. 数据连接 concat

功能说明

concat 用于沿一定轴将多个对象堆叠到一起。此函数提供了多种选项来处理索引和其他轴上的关系。

示例

df3 = pd.DataFrame({'A': ['A3', 'A4', 'A5'],
                    'B': ['B3', 'B4', 'B5']},
                   index=['K3', 'K4', 'K5'])
df4 = pd.DataFrame({'A': ['A6', 'A7', 'A8'],
                    'B': ['B6', 'B7', 'B8']},
                   index=['K6', 'K7', 'K8'])

# 使用 concat 连接
result = pd.concat([df1, df3, df4])
print(result)

输出

     A   B
K0  A0  B0
K1  A1  B1
K2  A2  B2
K3  A3  B3
K4  A4  B4
K5  A5  B5
K6  A6  B6
K7  A7  B7
K8  A8  B8

源码说明

引用了_Concatenator
在这里插入图片描述

知识点 内存管理

在处理 _Concatenator 类的 get_result 方法时,涉及到了条件性数据复制和就地数据操作,这对内存管理非常关键。以下是一段关键代码:
在这里插入图片描述

  1. 条件性数据复制

    • concatenate_block_managers 函数接收一个 copy 参数,这允许根据需要控制是否进行数据复制。如果 copy 设置为 False,则尽可能通过引用而非复制来操作数据块,这有助于减少内存消耗。
  2. 就地合并 (_consolidate_inplace 方法)

    • 在不复制数据的情况下,_consolidate_inplace() 方法被调用以就地优化和重组内部的数据块结构,这有助于提高内存利用效率。这个方法尝试在不增加额外内存负担的情况下,整合和压缩数据块,从而减少内存碎片和整体内存占用。

编程启发

  1. 使用条件性复制

    • 在设计涉及数据处理和转换的函数时,考虑引入可选的复制控制,让调用者根据需要选择是否进行数据复制。这不仅可以提供灵活性,还能在处理大数据集时提高内存效率。
  2. 优化内部数据结构

    • 在处理复杂或大型的数据结构时,考虑实现像 _consolidate_inplace 这样的方法,对内部数据进行就地优化。这样的方法应该旨在减少数据冗余和内存占用,同时保持数据完整性和访问效率。
  3. 延迟数据操作

    • 尽可能延迟数据修改和重整操作直到确实需要,以避免不必要的计算和内存开销。例如,在数据拼接或合并操作中,可以集中处理而非逐步执行,从而利用更有效的数据结构和算法优化性能。

这些策略和编程思想不仅适用于数据处理库的开发,也可以应用于任何需要高效内存管理的软件开发场景中,特别是在处理大规模数据时。

源码逐行解析

def get_result(self):

    # series only
    if self._is_series:  # 判断是否只处理 Series 对象

        # stack blocks
        if self.axis == 0:  # 如果是沿着 index 轴拼接
            name = com.consensus_name_attr(self.objs)  # 获取 Series 的统一名称

            mgr = self.objs[0]._data.concat(
                [x._data for x in self.objs], self.new_axes
            )  # 对所有 Series 的数据块进行拼接
            cons = _concat._get_series_result_type(mgr, self.objs)  # 获取拼接后的 Series 类型
            return cons(mgr, name=name).__finalize__(self, method="concat")  # 创建并返回新的 Series 对象

        # combine as columns in a frame
        else:  # 如果是沿着 columns 轴拼接
            data = dict(zip(range(len(self.objs)), self.objs))  # 将 Series 对象转换为字典
            cons = _concat._get_series_result_type(data)  # 获取结果类型

            index, columns = self.new_axes  # 获取新的索引和列
            df = cons(data, index=index)  # 创建 DataFrame
            df.columns = columns  # 设置列名
            return df.__finalize__(self, method="concat")  # 返回最终的 DataFrame

    # combine block managers
    else:  # 处理 DataFrame 或含 BlockManager 的情况
        mgrs_indexers = []
        for obj in self.objs:
            mgr = obj._data  # 获取每个对象的数据管理器
            indexers = {}
            for ax, new_labels in enumerate(self.new_axes):
                if ax == self.axis:
                    continue  # 如果是拼接轴,则跳过重索引

                obj_labels = mgr.axes[ax]  # 获取当前轴的标签
                if not new_labels.equals(obj_labels):
                    indexers[ax] = obj_labels.reindex(new_labels)[1]  # 重新索引并存储索引器

            mgrs_indexers.append((obj._data, indexers))  # 存储每个对象的数据管理器和索引器

        new_data = concatenate_block_managers(
            mgrs_indexers, self.new_axes, concat_axis=self.axis, copy=self.copy
        )  # 使用 concatenate_block_managers 拼接数据管理器
        if not self.copy:
            new_data._consolidate_inplace()  # 如果不复制数据,则就地合并数据块

        cons = _concat._get_frame_result_type(new_data, self.objs)  # 获取结果类型
        return cons._from_axes(new_data, self.new_axes).__finalize__(
            self, method="concat"
        )  # 创建并返回最终的 DataFrame

9. 添加数据 append

功能说明

append 是一个将一行或多行数据快速追加到 DataFrame 的方法。

示例

new_row = pd.DataFrame({'A': ['A9'], 'B': ['B9']}, index=['K9'])
result = df1.append(new_row)
print(result)

输出

     A   B
K0  A0  B0
K1  A1  B1
K2  A2  B2
K9  A9  B9

源码说明

这段代码定义了 append 方法,用于将数据(DataFrameSeries,字典或列表)追加到现有的 DataFrame 后面,返回一个新的 DataFrame 对象(实际是引用 cconat函数进行操作)。逐行分析如下:

def append(
        self,
        other,
        ignore_index: bool = False,
        verify_integrity: bool = False,
        sort: bool = False,
    ) -> DataFrame:
        """
        [这里是详细的文档字符串,解释方法的功能和参数]
        """

        if isinstance(other, (Series, dict)):  # 检查 other 是否为 Series 或字典
            if isinstance(other, dict):  # 如果 other 是字典
                if not ignore_index:  # 如果不忽略索引,则抛出错误
                    raise TypeError("Can only append a dict if ignore_index=True")
                other = Series(other)  # 将字典转换为 Series 对象

            if other.name is None and not ignore_index:  # 如果 Series 没有名字且未设置忽略索引
                raise TypeError("Can only append a Series if ignore_index=True or if the Series has a name")

            index = Index([other.name], name=self.index.name)  # 创建一个新索引
            idx_diff = other.index.difference(self.columns)  # 找出 other 中有而 self 中没有的列
            combined_columns = self.columns.append(idx_diff)  # 合并列
            other = (
                other.reindex(combined_columns, copy=False)  # 重排 other 以匹配列顺序
                .to_frame()  # 将 Series 转换为 DataFrame
                .T.infer_objects()  # 转置并推断对象类型
                .rename_axis(index.names, copy=False)  # 重命名轴
            )
            if not self.columns.equals(combined_columns):  # 如果列不相等,则重新索引 self
                self = self.reindex(columns=combined_columns)

        elif isinstance(other, list):  # 如果 other 是列表
            if not other:  # 如果列表为空,不做任何操作
                pass
            elif not isinstance(other[0], DataFrame):  # 如果列表的第一个元素不是 DataFrame
                other = DataFrame(other)  # 将列表转换为 DataFrame
                if (self.columns.get_indexer(other.columns) >= 0).all():  # 如果列完全匹配
                    other = other.reindex(columns=self.columns)  # 重排列以匹配 self

        from pandas.core.reshape.concat import concat  # 导入 concat 函数

        if isinstance(other, (list, tuple)):  # 如果 other 是列表或元组
            to_concat = [self, *other]  # 创建一个列表包含 self 和 other 的所有元素
        else:
            to_concat = [self, other]  # 创建一个列表包含 self 和 other

        return (
            concat(
                to_concat,
                ignore_index=ignore_index,
                verify_integrity=verify_integrity,
                sort=sort,
            )
        ).__finalize__(self, method="append")  # 调用 concat,然后使用 __finalize__ 处理结果

10. 数据融合 melt

功能说明melt 用于将 DataFrame 从宽格式变为长格式,通常用于将多个列融合为一个,非常适用于数据重塑和可视化。

示例

df = pd.DataFrame({
    'A': ['John', 'Alice'],
    'B': [28, 24],
    'C': ['New York', 'Los Angeles']
})
melted = pd.melt(df, id_vars=['A'], value_vars=['B', 'C'])
print(melted)

输出

       A variable        value
0    John        B          28
1   Alice        B          24
2    John        C    New York
3   Alice        C  Los Angeles

总结

除了掌握基本的用法外,通过深入的解析pandas的DataFrame,学习源码的编码习惯和用法能够对我们自己的代码精进有很大帮助,同时整体的代码结构和引用也很复杂后续会继续更新,如果对你有帮助欢迎关注收藏,避免迷路

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/587600.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Arcpy开发记录

一.GDB数据库相关 1.单独的shape更新时&#xff0c;不会有限制&#xff0c;数据会自动截取 2.在GDB下&#xff0c;使用UpdateCursor更新字段时&#xff0c;填入的数据长度必须与字段长度要求一致&#xff0c;否则报错&#xff1a; 二.arcpy.da.UpdateCursor相关 updateRow后关…

【无线通信开发应用】nRF905数据手册深度解读

希望通过两个stm32、两个nRF905无线通信模块、串口来实现两机通信。具体功能为&#xff1a; 板子A、B分别包含一个stm32单片机和一个nRF905无线模块&#xff0c;欲实现板子A、B之间的通信。 其中&#xff0c;PC端串口助手可向板子A的stm32发送字符‘A’控制板子B上的LED亮灯&am…

Linux migrate_type初步探索

1、基础知识 我们都知道Linux内存组织管理结构架构&#xff0c;顶层是struct pglist_data&#xff0c;然后再到struct zone&#xff0c;最后是struct page。大概的管理结构是这样的&#xff1a; 根据物理内存的地址范围可划分不同的zone&#xff0c;每个zone里的内存由buddy…

【redis】Redis数据类型(三)List类型

目录 List类型介绍特点 List数据结构附&#xff1a;3.2以前的版本(介绍一下压缩列表和双向链表)压缩列表ZipList双向链表LinkedList 常用命令lpush示例 lpushx示例 rpush示例 rpushx示例 LPOP示例 RPOP示例 BLPOP非阻塞行为阻塞行为相同的 key 被多个客户端同时阻塞在 MULTI/EX…

【类型商店】字符字符串(上)

啊&#xff0c;哈喽&#xff0c;小伙伴们大家好。我是#Y清墨&#xff0c;今天呐&#xff0c;我要介绍的是字符与字符串。 导语 在我之前发的[算法材料包]C数字拆分里有提及到.size()和length()和strlen(),想看更多内容请别划走&#xff01; 一.论字符 &#xff08;1&#xff…

口袋实验室--使用AD2高效调试IIC、UART、SPI等低速接口

目录 1. 简介 2. 调试过程 2.1 简要步骤 2.2 Si5338 寄存器配置流程 2.3 AD2的基本配置 2.4 检查Si5338状态 2.5 配置Si5338寄存器 2.6 保存Si5338寄存器 3. 总结 1. 简介 使用Digilent Analog Discovery 2进行调试不仅提升了工作效率&#xff0c;而且极大地简化了常…

Golang | Leetcode Golang题解之第60题排列序列

题目&#xff1a; 题解&#xff1a; func getPermutation(n int, k int) string {factorial : make([]int, n)factorial[0] 1for i : 1; i < n; i {factorial[i] factorial[i - 1] * i}k--ans : ""valid : make([]int, n 1)for i : 0; i < len(valid); i {…

2024年第二十一届 五一杯 (C题)大学生数学建模挑战赛|数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 CS团队倾注了大量时间和心血&#xff0c;深入挖掘解决方案。通…

pyinstaller打包pytorch和transformers程序

记录使用pyinstaller打包含有pytorch和transformers库的程序时遇到的问题和解决方法。 环境和版本信息 操作系统&#xff1a;Windows 11 Python&#xff1a;3.10.12 pyinstaller&#xff1a;5.13.0 torch&#xff1a;2.2.2 transformers&#xff1a;4.40.1 打包过程和问…

【进收藏夹吃灰系列】算法学习指南

文章目录 [toc]分治算法 个人主页&#xff1a;丷从心 系列专栏&#xff1a;进收藏夹吃灰系列 分治算法 博客标题博客url【分治算法】【Python实现】Hanoi塔问题https://blog.csdn.net/from__2024_04_11/article/details/138093461?spm1001.2014.3001.5502

Mysql-黑马

Mysql-黑马 编写规范&#xff1a;## 一级1. 二级三级 1.Mysql概述 数据库概念mysql数据仓库 cmd启动和停止 net start mysql180 net stop mysql180备注&#xff1a;其中的mysql180是服务名 客户端连接 远程连接数据仓库 -h 主机号 -P端口号 mysql [-h 127.0.0.1] [-P 33…

CSDN如何在个人主页开启自定义模块|微信公众号

目前只有下面三种身份才具有这个功能。 VIP博客专家企业博客 栏目内容不知道怎么写HTML的&#xff0c;可以联系我帮你添加

15.Blender Eevee和Cycles渲染引擎对比

初步介绍 Eevee是实时渲染的引擎&#xff0c;会省略一些解算方式&#xff0c;尤其对光线和阴影 Cycles会考虑这些因素&#xff0c;所以会对光线和阴影的表达更加真实&#xff0c;有一个实时光线追踪的功能 Cycles渲染完之后&#xff0c;每移动一次画面&#xff0c;都会重新渲染…

【工程记录】Python爬虫入门记录(Requests BeautifulSoup)

目录 写在前面1. 环境配置2. 获取网页数据3. 解析网页数据4. 提取所需数据4.1 简单提取4.2 多级索引提取 5. 常见问题 写在前面 仅作个人学习与记录用。主要整理使用Requests和BeautifulSoup库的简单爬虫方法。在进行数据爬取时&#xff0c;请确保遵守相关法律法规和网站的服务…

git revert的使用

由于某种原因我们需要撤销掉之前某一次的修改&#xff0c;但是这个修改已经提交&#xff0c;并且后面又经历了好几轮的提交。可能如下这种情况&#xff1a; 那么此时使用git revert再合适不过啦。git revert ${commit_id}就可以将指定commit id的修改撤销&#xff0c;然后提交…

Java从菜鸟到高手①

目录 1.数据类型 2.定义变量 2.1.编码方式 2.2.布尔型变量boolean 2.3.隐式类型转化和强制类型转化 2.4类型提升 3.字符串类型 4.运算符 4.1.取余 4.2. &#xff0c;- 4.3逻辑运算&& || &#xff01; 4.4.位运算 4.5.条件运算符 1.数据类型 Java中&#…

Linux:使用匿名管道对进程池的模拟实现

目录 一、Makefile 二、processpool.cc 2.1创建通信管道和子进程 2.2控制子进程 2.3回收进程 三、task.hpp 四、完整代码 接下来我们将模拟实现一个进程池&#xff0c;进程池广泛应用与各个领域和方向&#xff0c;比如我们打开电脑后同时打开很多个进程&#xff08;也就是软…

SpringBoot对接前端传递的base64编码的图片信息,转成图片以Get请求进行浏览器文件下载,不下载到本地。

一、问题描述 1.1需求描述。 前端将浏览器展示的图片以base64编码的形式传递给后端&#xff0c;以此实现文件下载的功能&#xff0c;在浏览器弹出文件下载框。效果如下 1.2实现思路 将前端传递的base64进行解码&#xff0c;设置响应头返回响应体&#xff0c;代码如下。 pu…

如何安全的使用密码登录账号(在不知道密码的情况下)

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 1、打开工具&#xff0c;进入账号密码模块&#xff0c;如图 2、看到鼠标移动到密码那一栏有提示&#xff0c;按住Ctrl或者Alt点击或者双击就能复制内容&…

【大前端】ECharts 绘制立体柱状图

立体柱状图分为&#xff1a; 纯色立体柱状图渐变立体柱状图 常用实现方式 纯色立体柱状图 纯色立体柱状图&#xff0c;使用MarkPoint和颜色渐变就实现&#xff0c;如下代码 import * as echarts from "echarts";var chartDom document.getElementById("main&…