###
`pandas.read_csv(filepath_or_buffer: 'FilePath | ReadCsvBuffer[bytes] | ReadCsvBuffer[str]', *, sep: 'str | None | lib.NoDefault' = <no_default>, delimiter: 'str | None | lib.NoDefault' = None, header: "int | Sequence[int] | None | Literal['infer']" = 'infer', names: 'Sequence[Hashable] | None | lib.NoDefault' = <no_default>, index_col: 'IndexLabel | Literal[False] | None' = None, usecols: 'UsecolsArgType' = None, dtype: 'DtypeArg | None' = None, engine: 'CSVEngine | None' = None, converters: 'Mapping[Hashable, Callable] | None' = None, true_values: 'list | None' = None, false_values: 'list | None' = None, skipinitialspace: 'bool' = False, skiprows: 'list[int] | int | Callable[[Hashable], bool] | None' = None, skipfooter: 'int' = 0, nrows: 'int | None' = None, na_values: 'Hashable | Iterable[Hashable] | Mapping[Hashable, Iterable[Hashable]] | None' = None, keep_default_na: 'bool' = True, na_filter: 'bool' = True, verbose: 'bool | lib.NoDefault' = <no_default>, skip_blank_lines: 'bool' = True, parse_dates: 'bool | Sequence[Hashable] | None' = None, infer_datetime_format: 'bool | lib.NoDefault' = <no_default>, keep_date_col: 'bool | lib.NoDefault' = <no_default>, date_parser: 'Callable | lib.NoDefault' = <no_default>, date_format: 'str | dict[Hashable, str] | None' = None, dayfirst: 'bool' = False, cache_dates: 'bool' = True, iterator: 'bool' = False, chunksize: 'int | None' = None, compression: 'CompressionOptions' = 'infer', thousands: 'str | None' = None, decimal: 'str' = '.', lineterminator: 'str | None' = None, quotechar: 'str' = '"', quoting: 'int' = 0, doublequote: 'bool' = True, escapechar: 'str | None' = None, comment: 'str | None' = None, encoding: 'str | None' = None, encoding_errors: 'str | None' = 'strict', dialect: 'str | csv.Dialect | None' = None, on_bad_lines: 'str' = 'error', delim_whitespace: 'bool | lib.NoDefault' = <no_default>, low_memory: 'bool' = True, memory_map: 'bool' = False, float_precision: "Literal['high', 'legacy'] | None" = None, storage_options: 'StorageOptions | None' = None, dtype_backend: 'DtypeBackend | lib.NoDefault' = <no_default>) -> 'DataFrame | TextFileReader'`

Read a comma-separated values (csv) file into DataFrame.

Also supports optionally iterating or breaking of the file
into chunks.

Additional help can be found in the online docs for
`IO Tools <https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html>`_.

Parameters
----------
filepath_or_buffer : str, path object or file-like object
Any valid string path is acceptable. The string could be a URL. Valid
URL schemes include http, ftp, s3, gs, and file. For file URLs, a host is
expected. A local file could be: file://localhost/path/to/table.csv.

    If you want to pass in a path object, pandas accepts any ``os.PathLike``.

    By file-like object, we refer to objects with a ``read()`` method, such as
    a file handle (e.g. via builtin ``open`` function) or ``StringIO``.

sep : str, default ','
Character or regex pattern to treat as the delimiter. If ``sep=None``, the
C engine cannot automatically detect
the separator, but the Python parsing engine can, meaning the latter will
be used and automatically detect the separator from only the first valid
row of the file by Python's builtin sniffer tool, ``csv.Sniffer``.
In addition, separators longer than 1 character and different from
``'\s+'`` will be interpreted as regular expressions and will also force
the use of the Python parsing engine. Note that regex delimiters are prone
to ignoring quoted data. Regex example: ``'\r\t'``.
delimiter : str, optional
Alias for ``sep``.
header : int, Sequence of int, 'infer' or None, default 'infer'
Row number(s) containing column labels and marking the start of the
data (zero-indexed). Default behavior is to infer the column names: if no ``names``
are passed the behavior is identical to ``header=0`` and column
names are inferred from the first line of the file, if column
names are passed explicitly to ``names`` then the behavior is identical to
``header=None``. Explicitly pass ``header=0`` to be able to
replace existing names. The header can be a list of integers that
specify row locations for a :class:`~pandas.MultiIndex` on the columns
e.g. ``[0, 1, 3]``. Intervening rows that are not specified will be
skipped (e.g. 2 in this example is skipped). Note that this
parameter ignores commented lines and empty lines if
``skip_blank_lines=True``, so ``header=0`` denotes the first line of
data rather than the first line of the file.
names : Sequence of Hashable, optional
Sequence of column labels to apply. If the file contains a header row,
then you should explicitly pass ``header=0`` to override the column names.
Duplicates in this list are not allowed.
index_col : Hashable, Sequence of Hashable or False, optional
Column(s) to use as row label(s), denoted either by column labels or column
indices. If a sequence of labels or indices is given, :class:`~pandas.MultiIndex`
will be formed for the row labels.

Note: ``index_col=False`` can be used to force pandas to *not* use the first
column as the index, e.g., when you have a malformed file with delimiters at
the end of each line.
usecols : Sequence of Hashable or Callable, optional
Subset of columns to select, denoted either by column labels or column indices.
If list-like, all elements must either
be positional (i.e. integer indices into the document columns) or strings
that correspond to column names provided either by the user in ``names`` or
inferred from the document header row(s). If ``names`` are given, the document
header row(s) are not taken into account. For example, a valid list-like
``usecols`` parameter would be ``[0, 1, 2]`` or ``['foo', 'bar', 'baz']``.
Element order is ignored, so ``usecols=[0, 1]`` is the same as ``[1, 0]``.
To instantiate a :class:`~pandas.DataFrame` from ``data`` with element order
preserved use ``pd.read_csv(data, usecols=['foo', 'bar'])[['foo', 'bar']]``
for columns in ``['foo', 'bar']`` order or
``pd.read_csv(data, usecols=['foo', 'bar'])[['bar', 'foo']]``
for ``['bar', 'foo']`` order.

    If callable, the callable function will be evaluated against the column
    names, returning names where the callable function evaluates to ``True``. An
    example of a valid callable argument would be ``lambda x: x.upper() in
    ['AAA', 'BBB', 'DDD']``. Using this parameter results in much faster
    parsing time and lower memory usage.

dtype : dtype or dict of {Hashable : dtype}, optional
Data type(s) to apply to either the whole dataset or individual columns.
E.g., ``{'a': np.float64, 'b': np.int32, 'c': 'Int64'}``
Use ``str`` or ``object`` together with suitable ``na_values`` settings
to preserve and not interpret ``dtype``.
If ``converters`` are specified, they will be applied INSTEAD
of ``dtype`` conversion.

    .. versionadded:: 1.5.0

        Support for ``defaultdict`` was added. Specify a ``defaultdict`` as input where
        the default determines the ``dtype`` of the columns which are not explicitly
        listed.

engine : {'c', 'python', 'pyarrow'}, optional
Parser engine to use. The C and pyarrow engines are faster, while the python engine
is currently more feature-complete. Multithreading is currently only supported by
the pyarrow engine.

    .. versionadded:: 1.4.0

        The 'pyarrow' engine was added as an *experimental* engine, and some features
        are unsupported, or may not work correctly, with this engine.

converters : dict of {Hashable : Callable}, optional
Functions for converting values in specified columns. Keys can either
be column labels or column indices.
true_values : list, optional
Values to consider as ``True`` in addition to case-insensitive variants of 'True'.
false_values : list, optional
Values to consider as ``False`` in addition to case-insensitive variants of 'False'.
skipinitialspace : bool, default False
Skip spaces after delimiter.
skiprows : int, list of int or Callable, optional
Line numbers to skip (0-indexed) or number of lines to skip (``int``)
at the start of the file.

    If callable, the callable function will be evaluated against the row
    indices, returning ``True`` if the row should be skipped and ``False`` otherwise.
    An example of a valid callable argument would be ``lambda x: x in [0, 2]``.

skipfooter : int, default 0
Number of lines at bottom of file to skip (Unsupported with ``engine='c'``).
nrows : int, optional
Number of rows of file to read. Useful for reading pieces of large files.
na_values : Hashable, Iterable of Hashable or dict of {Hashable : Iterable}, optional
Additional strings to recognize as ``NA``/``NaN``. If ``dict`` passed, specific
per-column ``NA`` values. By default the following values are interpreted as
``NaN``: " ", "#N/A", "#N/A N/A", "#NA", "-1.#IND", "-1.#QNAN", "-NaN", "-nan",
"1.#IND", "1.#QNAN", "<NA>", "N/A", "NA", "NULL", "NaN", "None",
"n/a", "nan", "null ".

keep_default_na : bool, default True
Whether or not to include the default ``NaN`` values when parsing the data.
Depending on whether ``na_values`` is passed in, the behavior is as follows:

    * If ``keep_default_na`` is ``True``, and ``na_values`` are specified, ``na_values``
      is appended to the default ``NaN`` values used for parsing.
    * If ``keep_default_na`` is ``True``, and ``na_values`` are not specified, only
      the default ``NaN`` values are used for parsing.
    * If ``keep_default_na`` is ``False``, and ``na_values`` are specified, only
      the ``NaN`` values specified ``na_values`` are used for parsing.
    * If ``keep_default_na`` is ``False``, and ``na_values`` are not specified, no
      strings will be parsed as ``NaN``.

    Note that if ``na_filter`` is passed in as ``False``, the ``keep_default_na`` and
    ``na_values`` parameters will be ignored.

na_filter : bool, default True
Detect missing value markers (empty strings and the value of ``na_values``). In
data without any ``NA`` values, passing ``na_filter=False`` can improve the
performance of reading a large file.
verbose : bool, default False
Indicate number of ``NA`` values placed in non-numeric columns.

    .. deprecated:: 2.2.0

skip_blank_lines : bool, default True
If ``True``, skip over blank lines rather than interpreting as ``NaN`` values.
parse_dates : bool, list of Hashable, list of lists or dict of {Hashable : list}, default False
The behavior is as follows:

    * ``bool``. If ``True`` -> try parsing the index. Note: Automatically set to
      ``True`` if ``date_format`` or ``date_parser`` arguments have been passed.
    * ``list`` of ``int`` or names. e.g. If ``[1, 2, 3]`` -> try parsing columns 1, 2, 3
      each as a separate date column.
    * ``list`` of ``list``. e.g.  If ``[[1, 3]]`` -> combine columns 1 and 3 and parse
      as a single date column. Values are joined with a space before parsing.
    * ``dict``, e.g. ``{'foo' : [1, 3]}`` -> parse columns 1, 3 as date and call
      result 'foo'. Values are joined with a space before parsing.

    If a column or index cannot be represented as an array of ``datetime``,
    say because of an unparsable value or a mixture of timezones, the column
    or index will be returned unaltered as an ``object`` data type. For
    non-standard ``datetime`` parsing, use :func:`~pandas.to_datetime` after
    :func:`~pandas.read_csv`.

    Note: A fast-path exists for iso8601-formatted dates.

infer_datetime_format : bool, default False
If ``True`` and ``parse_dates`` is enabled, pandas will attempt to infer the
format of the ``datetime`` strings in the columns, and if it can be inferred,
switch to a faster method of parsing them. In some cases this can increase
the parsing speed by 5-10x.

    .. deprecated:: 2.0.0
        A strict version of this argument is now the default, passing it has no effect.

keep_date_col : bool, default False
If ``True`` and ``parse_dates`` specifies combining multiple columns then
keep the original columns.
date_parser : Callable, optional
Function to use for converting a sequence of string columns to an array of
``datetime`` instances. The default uses ``dateutil.parser.parser`` to do the
conversion. pandas will try to call ``date_parser`` in three different ways,
advancing to the next if an exception occurs: 1) Pass one or more arrays
(as defined by ``parse_dates``) as arguments; 2) concatenate (row-wise) the
string values from the columns defined by ``parse_dates`` into a single array
and pass that; and 3) call ``date_parser`` once for each row using one or
more strings (corresponding to the columns defined by ``parse_dates``) as
arguments.

    .. deprecated:: 2.0.0
       Use ``date_format`` instead, or read in as ``object`` and then apply
       :func:`~pandas.to_datetime` as-needed.

date_format : str or dict of column -> format, optional
Format to use for parsing dates when used in conjunction with ``parse_dates``.
The strftime to parse time, e.g. :const:`"%d/%m/%Y"`. See
`strftime documentation
<https://docs.python.org/3/library/datetime.html
#strftime-and-strptime-behavior>`_ for more information on choices, though
note that :const:`"%f"` will parse all the way up to nanoseconds.
You can also pass:

    - "ISO8601", to parse any `ISO8601 <https://en.wikipedia.org/wiki/ISO_8601>`_
        time string (not necessarily in exactly the same format);
    - "mixed", to infer the format for each element individually. This is risky,
        and you should probably use it along with `dayfirst`.

    .. versionadded:: 2.0.0

dayfirst : bool, default False
DD/MM format dates, international and European format.
cache_dates : bool, default True
If ``True``, use a cache of unique, converted dates to apply the ``datetime``
conversion. May produce significant speed-up when parsing duplicate
date strings, especially ones with timezone offsets.

iterator : bool, default False
Return ``TextFileReader`` object for iteration or getting chunks with
``get_chunk()``.
chunksize : int, optional
Number of lines to read from the file per chunk. Passing a value will cause the
function to return a ``TextFileReader`` object for iteration.
See the `IO Tools docs
    <https://pandas.pydata.org/pandas-docs/stable/io.html#io-chunking>`_
for more information on ``iterator`` and ``chunksize``.

compression : str or dict, default 'infer'
For on-the-fly decompression of on-disk data. If 'infer' and 'filepath_or_buffer' is
path-like, then detect compression from the following extensions: '.gz',
'.bz2', '.zip', '.xz', '.zst', '.tar', '.tar.gz', '.tar.xz' or '.tar.bz2'
(otherwise no compression).
If using 'zip' or 'tar', the ZIP file must contain only one data file to be read in.
Set to ``None`` for no decompression.
Can also be a dict with key ``'method'`` set
to one of {``'zip'``, ``'gzip'``, ``'bz2'``, ``'zstd'``, ``'xz'``, ``'tar'``} and
other key-value pairs are forwarded to
``zipfile.ZipFile``, ``gzip.GzipFile``,
``bz2.BZ2File``, ``zstandard.ZstdDecompressor``, ``lzma.LZMAFile`` or
``tarfile.TarFile``, respectively.
As an example, the following could be passed for Zstandard decompression using a
custom compression dictionary:
``compression={'method': 'zstd', 'dict_data': my_compression_dict}``.

    .. versionadded:: 1.5.0
        Added support for `.tar` files.

    .. versionchanged:: 1.4.0 Zstandard support.

thousands : str (length 1), optional
Character acting as the thousands separator in numerical values.
decimal : str (length 1), default '.'
Character to recognize as decimal point (e.g., use ',' for European data).
lineterminator : str (length 1), optional
Character used to denote a line break. Only valid with C parser.
quotechar : str (length 1), optional
Character used to denote the start and end of a quoted item. Quoted
items can include the ``delimiter`` and it will be ignored.
quoting : {0 or csv.QUOTE_MINIMAL, 1 or csv.QUOTE_ALL, 2 or csv.QUOTE_NONNUMERIC, 3 or csv.QUOTE_NONE}, default
csv.QUOTE_MINIMAL
Control field quoting behavior per ``csv.QUOTE_*`` constants. Default is
``csv.QUOTE_MINIMAL`` (i.e., 0) which implies that only fields containing special
characters are quoted (e.g., characters defined in ``quotechar``, ``delimiter``,
or ``lineterminator``.
doublequote : bool, default True
When ``quotechar`` is specified and ``quoting`` is not ``QUOTE_NONE``, indicate
whether or not to interpret two consecutive ``quotechar`` elements INSIDE a
field as a single ``quotechar`` element.
escapechar : str (length 1), optional
Character used to escape other characters.
comment : str (length 1), optional
Character indicating that the remainder of line should not be parsed.
If found at the beginning
of a line, the line will be ignored altogether. This parameter must be a
single character. Like empty lines (as long as ``skip_blank_lines=True``),
fully commented lines are ignored by the parameter ``header`` but not by
``skiprows``. For example, if ``comment='#'``, parsing
``#empty\na,b,c\n1,2,3`` with ``header=0`` will result in ``'a,b,c'`` being
treated as the header.
encoding : str, optional, default 'utf-8'
Encoding to use for UTF when reading/writing (ex. ``'utf-8'``). `List of Python
    standard encodings
    <https://docs.python.org/3/library/codecs.html#standard-encodings>`_ .

encoding_errors : str, optional, default 'strict'
How encoding errors are treated. `List of possible values
    <https://docs.python.org/3/library/codecs.html#error-handlers>`_ .

    .. versionadded:: 1.3.0

dialect : str or csv.Dialect, optional
If provided, this parameter will override values (default or not) for the
following parameters: ``delimiter``, ``doublequote``, ``escapechar``,
``skipinitialspace``, ``quotechar``, and ``quoting``. If it is necessary to
override values, a ``ParserWarning`` will be issued. See ``csv.Dialect``
documentation for more details.
on_bad_lines : {'error', 'warn', 'skip'} or Callable, default 'error'
Specifies what to do upon encountering a bad line (a line with too many fields).
Allowed values are :

    - ``'error'``, raise an Exception when a bad line is encountered.
    - ``'warn'``, raise a warning when a bad line is encountered and skip that line.
    - ``'skip'``, skip bad lines without raising or warning when they are encountered.

    .. versionadded:: 1.3.0

    .. versionadded:: 1.4.0

        - Callable, function with signature
          ``(bad_line: list[str]) -> list[str] | None`` that will process a single
          bad line. ``bad_line`` is a list of strings split by the ``sep``.
          If the function returns ``None``, the bad line will be ignored.
          If the function returns a new ``list`` of strings with more elements than
          expected, a ``ParserWarning`` will be emitted while dropping extra elements.
          Only supported when ``engine='python'``

    .. versionchanged:: 2.2.0

        - Callable, function with signature
          as described in `pyarrow documentation
          <https://arrow.apache.org/docs/python/generated/pyarrow.csv.ParseOptions.html
          #pyarrow.csv.ParseOptions.invalid_row_handler>`_ when ``engine='pyarrow'``

delim_whitespace : bool, default False
Specifies whether or not whitespace (e.g. ``' '`` or ``'\t'``) will be
used as the ``sep`` delimiter. Equivalent to setting ``sep='\s+'``. If this option
is set to ``True``, nothing should be passed in for the ``delimiter``
parameter.

    .. deprecated:: 2.2.0
        Use ``sep="\s+"`` instead.

low_memory : bool, default True
Internally process the file in chunks, resulting in lower memory use
while parsing, but possibly mixed type inference. To ensure no mixed
types either set ``False``, or specify the type with the ``dtype`` parameter.
Note that the entire file is read into a single :class:`~pandas.DataFrame`
regardless, use the ``chunksize`` or ``iterator`` parameter to return the data in
chunks. (Only valid with C parser).
memory_map : bool, default False
If a filepath is provided for ``filepath_or_buffer``, map the file object
directly onto memory and access the data directly from there. Using this
option can improve performance because there is no longer any I/O overhead.
float_precision : {'high', 'legacy', 'round_trip'}, optional
Specifies which converter the C engine should use for floating-point
values. The options are ``None`` or ``'high'`` for the ordinary converter,
``'legacy'`` for the original lower precision pandas converter, and
``'round_trip'`` for the round-trip converter.

storage_options : dict, optional
Extra options that make sense for a particular storage connection, e.g.
host, port, username, password, etc. For HTTP(S) URLs the key-value pairs
are forwarded to ``urllib.request.Request`` as header options. For other
URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are
forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more
details, and for more examples on storage options refer `here
    <https://pandas.pydata.org/docs/user_guide/io.html?
    highlight=storage_options#reading-writing-remote-files>`_.

dtype_backend : {'numpy_nullable', 'pyarrow'}, default 'numpy_nullable'
Back-end data type applied to the resultant :class:`DataFrame`
(still experimental). Behaviour is as follows:

    * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame`
      (default).
    * ``"pyarrow"``: returns pyarrow-backed nullable :class:`ArrowDtype`
      DataFrame.

    .. versionadded:: 2.0

Returns
-------
DataFrame or TextFileReader
A comma-separated values (csv) file is returned as two-dimensional
data structure with labeled axes.

See Also
--------
DataFrame.to_csv : Write DataFrame to a comma-separated values (csv) file.
read_table : Read general delimited file into DataFrame.
read_fwf : Read a table of fixed-width formatted lines into DataFrame.

Examples
--------
> > > pd.read_csv('data.csv')  # doctest: +SKIP

---

###
`pandas.DataFrame.merge(self, right: 'DataFrame | Series', how: 'MergeHow' = 'inner', on: 'IndexLabel | AnyArrayLike | None' = None, left_on: 'IndexLabel | AnyArrayLike | None' = None, right_on: 'IndexLabel | AnyArrayLike | None' = None, left_index: 'bool' = False, right_index: 'bool' = False, sort: 'bool' = False, suffixes: 'Suffixes' = ('_x', '_y'), copy: 'bool | None' = None, indicator: 'str | bool' = False, validate: 'MergeValidate | None' = None) -> 'DataFrame'`

Merge DataFrame or named Series objects with a database-style join.

A named Series object is treated as a DataFrame with a single named column.

The join is done on columns or indexes. If joining columns on
columns, the DataFrame indexes *will be ignored*. Otherwise if joining indexes
on indexes or indexes on a column or columns, the index will be passed on.
When performing a cross merge, no column specifications to merge on are
allowed.

.. warning::

    If both key columns contain rows where the key is a null value, those
    rows will be matched against each other. This is different from usual SQL
    join behaviour and can lead to unexpected results.

Parameters
----------
right : DataFrame or named Series
Object to merge with.
how : {'left', 'right', 'outer', 'inner', 'cross'}, default 'inner'
Type of merge to be performed.

    * left: use only keys from left frame, similar to a SQL left outer join;
      preserve key order.
    * right: use only keys from right frame, similar to a SQL right outer join;
      preserve key order.
    * outer: use union of keys from both frames, similar to a SQL full outer
      join; sort keys lexicographically.
    * inner: use intersection of keys from both frames, similar to a SQL inner
      join; preserve the order of the left keys.
    * cross: creates the cartesian product from both frames, preserves the order
      of the left keys.

on : label or list
Column or index level names to join on. These must be found in both
DataFrames. If `on` is None and not merging on indexes then this defaults
to the intersection of the columns in both DataFrames.
left_on : label or list, or array-like
Column or index level names to join on in the left DataFrame. Can also
be an array or list of arrays of the length of the left DataFrame.
These arrays are treated as if they are columns.
right_on : label or list, or array-like
Column or index level names to join on in the right DataFrame. Can also
be an array or list of arrays of the length of the right DataFrame.
These arrays are treated as if they are columns.
left_index : bool, default False
Use the index from the left DataFrame as the join key(s). If it is a
MultiIndex, the number of keys in the other DataFrame (either the index
or a number of columns) must match the number of levels.
right_index : bool, default False
Use the index from the right DataFrame as the join key. Same caveats as
left_index.
sort : bool, default False
Sort the join keys lexicographically in the result DataFrame. If False,
the order of the join keys depends on the join type (how keyword).
suffixes : list-like, default is ("_x", "_y")
A length-2 sequence where each element is optionally a string
indicating the suffix to add to overlapping column names in
`left` and `right` respectively. Pass a value of `None` instead
of a string to indicate that the column name from `left` or
`right` should be left as-is, with no suffix. At least one of the
values must not be None.
copy : bool, default True
If False, avoid copy if possible.

    .. note::
        The `copy` keyword will change behavior in pandas 3.0.
        `Copy-on-Write
        <https://pandas.pydata.org/docs/dev/user_guide/copy_on_write.html>`__
        will be enabled by default, which means that all methods with a
        `copy` keyword will use a lazy copy mechanism to defer the copy and
        ignore the `copy` keyword. The `copy` keyword will be removed in a
        future version of pandas.

        You can already get the future behavior and improvements through
        enabling copy on write ``pd.options.mode.copy_on_write = True``

indicator : bool or str, default False
If True, adds a column to the output DataFrame called "_merge" with
information on the source of each row. The column can be given a different
name by providing a string argument. The column will have a Categorical
type with the value of "left_only" for observations whose merge key only
appears in the left DataFrame, "right_only" for observations
whose merge key only appears in the right DataFrame, and "both"
if the observation's merge key is found in both DataFrames.

validate : str, optional
If specified, checks if merge is of specified type.

    * "one_to_one" or "1:1": check if merge keys are unique in both
      left and right datasets.
    * "one_to_many" or "1:m": check if merge keys are unique in left
      dataset.
    * "many_to_one" or "m:1": check if merge keys are unique in right
      dataset.
    * "many_to_many" or "m:m": allowed, but does not result in checks.

Returns
-------
DataFrame
A DataFrame of the two merged objects.

See Also
--------
merge_ordered : Merge with optional filling/interpolation.
merge_asof : Merge on nearest keys.
DataFrame.join : Similar method using indices.

Examples
--------
> > > df1 = pd.DataFrame({'lkey': ['foo', 'bar', 'baz', 'foo'],
> > > ...                     'value': [1, 2, 3, 5]})
> > > df2 = pd.DataFrame({'rkey': ['foo', 'bar', 'baz', 'foo'],
> > > ...                     'value': [5, 6, 7, 8]})
> > > df1
> > > lkey value
> > > 0 foo 1
> > > 1 bar 2
> > > 2 baz 3
> > > 3 foo 5
> > > df2
> > > rkey value
> > > 0 foo 5
> > > 1 bar 6
> > > 2 baz 7
> > > 3 foo 8

Merge df1 and df2 on the lkey and rkey columns. The value columns have
the default suffixes, _x and _y, appended.

> > > df1.merge(df2, left_on='lkey', right_on='rkey')
> > > lkey value_x rkey value_y
> > > 0 foo 1 foo 5
> > > 1 foo 1 foo 8
> > > 2 bar 2 bar 6
> > > 3 baz 3 baz 7
> > > 4 foo 5 foo 5
> > > 5 foo 5 foo 8

Merge DataFrames df1 and df2 with specified left and right suffixes
appended to any overlapping columns.

> > > df1.merge(df2, left_on='lkey', right_on='rkey',
> > > ... suffixes=('_left', '_right'))
> > > lkey value_left rkey value_right
> > > 0 foo 1 foo 5
> > > 1 foo 1 foo 8
> > > 2 bar 2 bar 6
> > > 3 baz 3 baz 7
> > > 4 foo 5 foo 5
> > > 5 foo 5 foo 8

Merge DataFrames df1 and df2, but raise an exception if the DataFrames have
any overlapping columns.

> > > df1.merge(df2, left_on='lkey', right_on='rkey', suffixes=(False, False))
> > > Traceback (most recent call last):
> > > ...
> > > ValueError: columns overlap but no suffix specified:
> > > Index(['value'], dtype='object')

> > > df1 = pd.DataFrame({'a': ['foo', 'bar'], 'b': [1, 2]})
> > > df2 = pd.DataFrame({'a': ['foo', 'baz'], 'c': [3, 4]})
> > > df1
> > > a b
> > > 0 foo 1
> > > 1 bar 2
> > > df2
> > > a c
> > > 0 foo 3
> > > 1 baz 4

> > > df1.merge(df2, how='inner', on='a')
> > > a b c
> > > 0 foo 1 3

> > > df1.merge(df2, how='left', on='a')
> > > a b c
> > > 0 foo 1 3.0
> > > 1 bar 2 NaN

> > > df1 = pd.DataFrame({'left': ['foo', 'bar']})
> > > df2 = pd.DataFrame({'right': [7, 8]})
> > > df1
> > > left
> > > 0 foo
> > > 1 bar
> > > df2
> > > right
> > > 0 7
> > > 1 8

> > > df1.merge(df2, how='cross')
> > > left right
> > > 0 foo 7
> > > 1 foo 8
> > > 2 bar 7
> > > 3 bar 8

---

###
`pandas.concat(objs: 'Iterable[Series | DataFrame] | Mapping[HashableT, Series | DataFrame]', *, axis: 'Axis' = 0, join: 'str' = 'outer', ignore_index: 'bool' = False, keys: 'Iterable[Hashable] | None' = None, levels=None, names: 'list[HashableT] | None' = None, verify_integrity: 'bool' = False, sort: 'bool' = False, copy: 'bool | None' = None) -> 'DataFrame | Series'`

Concatenate pandas objects along a particular axis.

Allows optional set logic along the other axes.

Can also add a layer of hierarchical indexing on the concatenation axis,
which may be useful if the labels are the same (or overlapping) on
the passed axis number.

Parameters
----------
objs : a sequence or mapping of Series or DataFrame objects
If a mapping is passed, the sorted keys will be used as the `keys`
argument, unless it is passed, in which case the values will be
selected (see below). Any None objects will be dropped silently unless
they are all None in which case a ValueError will be raised.
axis : {0/'index', 1/'columns'}, default 0
The axis to concatenate along.
join : {'inner', 'outer'}, default 'outer'
How to handle indexes on other axis (or axes).
ignore_index : bool, default False
If True, do not use the index values along the concatenation axis. The
resulting axis will be labeled 0, ..., n - 1. This is useful if you are
concatenating objects where the concatenation axis does not have
meaningful indexing information. Note the index values on the other
axes are still respected in the join.
keys : sequence, default None
If multiple levels passed, should contain tuples. Construct
hierarchical index using the passed keys as the outermost level.
levels : list of sequences, default None
Specific levels (unique values) to use for constructing a
MultiIndex. Otherwise they will be inferred from the keys.
names : list, default None
Names for the levels in the resulting hierarchical index.
verify_integrity : bool, default False
Check whether the new concatenated axis contains duplicates. This can
be very expensive relative to the actual data concatenation.
sort : bool, default False
Sort non-concatenation axis if it is not already aligned. One exception to
this is when the non-concatentation axis is a DatetimeIndex and join='outer'
and the axis is not already aligned. In that case, the non-concatenation
axis is always sorted lexicographically.
copy : bool, default True
If False, do not copy data unnecessarily.

Returns
-------
object, type of objs
When concatenating all ``Series`` along the index (axis=0), a
``Series`` is returned. When ``objs`` contains at least one
``DataFrame``, a ``DataFrame`` is returned. When concatenating along
the columns (axis=1), a ``DataFrame`` is returned.

See Also
--------
DataFrame.join : Join DataFrames using indexes.
DataFrame.merge : Merge DataFrames by indexes or columns.

Notes
-----
The keys, levels, and names arguments are all optional.

A walkthrough of how this method fits in with other tools for combining
pandas objects can be found `here
<https://pandas.pydata.org/pandas-docs/stable/user_guide/merging.html>`__.

It is not recommended to build DataFrames by adding single rows in a
for loop. Build a list of rows and make a DataFrame in a single concat.

Examples
--------
Combine two ``Series``.

> > > s1 = pd.Series(['a', 'b'])
> > > s2 = pd.Series(['c', 'd'])
> > > pd.concat([s1, s2])
> > > 0 a
> > > 1 b
> > > 0 c
> > > 1 d
> > > dtype: object

Clear the existing index and reset it in the result
by setting the ``ignore_index`` option to ``True``.

> > > pd.concat([s1, s2], ignore_index=True)
> > > 0 a
> > > 1 b
> > > 2 c
> > > 3 d
> > > dtype: object

Add a hierarchical index at the outermost level of
the data with the ``keys`` option.

> > > pd.concat([s1, s2], keys=['s1', 's2'])
> > > s1 0 a
> > > 1 b
> > > s2 0 c
> > > 1 d
> > > dtype: object

Label the index keys you create with the ``names`` option.

> > > pd.concat([s1, s2], keys=['s1', 's2'],
> > > ... names=['Series name', 'Row ID'])
> > > Series name Row ID
> > > s1 0 a
> > > 1 b
> > > s2 0 c
> > > 1 d
> > > dtype: object

Combine two ``DataFrame`` objects with identical columns.

> > > df1 = pd.DataFrame([['a', 1], ['b', 2]],
> > > ... columns=['letter', 'number'])
> > > df1
> > > letter number
> > > 0 a 1
> > > 1 b 2
> > > df2 = pd.DataFrame([['c', 3], ['d', 4]],
> > > ... columns=['letter', 'number'])
> > > df2
> > > letter number
> > > 0 c 3
> > > 1 d 4
> > > pd.concat([df1, df2])
> > > letter number
> > > 0 a 1
> > > 1 b 2
> > > 0 c 3
> > > 1 d 4

Combine ``DataFrame`` objects with overlapping columns
and return everything. Columns outside the intersection will
be filled with ``NaN`` values.

> > > df3 = pd.DataFrame([['c', 3, 'cat'], ['d', 4, 'dog']],
> > > ... columns=['letter', 'number', 'animal'])
> > > df3
> > > letter number animal
> > > 0 c 3 cat
> > > 1 d 4 dog
> > > pd.concat([df1, df3], sort=False)
> > > letter number animal
> > > 0 a 1 NaN
> > > 1 b 2 NaN
> > > 0 c 3 cat
> > > 1 d 4 dog

Combine ``DataFrame`` objects with overlapping columns
and return only those that are shared by passing ``inner`` to
the ``join`` keyword argument.

> > > pd.concat([df1, df3], join="inner")
> > > letter number
> > > 0 a 1
> > > 1 b 2
> > > 0 c 3
> > > 1 d 4

Combine ``DataFrame`` objects horizontally along the x axis by
passing in ``axis=1``.

> > > df4 = pd.DataFrame([['bird', 'polly'], ['monkey', 'george']],
> > > ... columns=['animal', 'name'])
> > > pd.concat([df1, df4], axis=1)
> > > letter number animal name
> > > 0 a 1 bird polly
> > > 1 b 2 monkey george

Prevent the result from including duplicate index values with the
``verify_integrity`` option.

> > > df5 = pd.DataFrame([1], index=['a'])
> > > df5
> > > 0
> > > a 1
> > > df6 = pd.DataFrame([2], index=['a'])
> > > df6
> > > 0
> > > a 2
> > > pd.concat([df5, df6], verify_integrity=True)
> > > Traceback (most recent call last):
> > > ...
> > > ValueError: Indexes have overlapping values: ['a']

Append a single row to the end of a ``DataFrame`` object.

> > > df7 = pd.DataFrame({'a': 1, 'b': 2}, index=[0])
> > > df7
> > > a b
> > > 0 1 2
> > > new_row = pd.Series({'a': 3, 'b': 4})
> > > new_row
> > > a 3
> > > b 4
> > > dtype: int64
> > > pd.concat([df7, new_row.to_frame().T], ignore_index=True)
> > > a b
> > > 0 1 2
> > > 1 3 4

---

###
`pandas.DataFrame.groupby(self, by=None, axis: 'Axis | lib.NoDefault' = <no_default>, level: 'IndexLabel | None' = None, as_index: 'bool' = True, sort: 'bool' = True, group_keys: 'bool' = True, observed: 'bool | lib.NoDefault' = <no_default>, dropna: 'bool' = True) -> 'DataFrameGroupBy'`

Group DataFrame using a mapper or by a Series of columns.

A groupby operation involves some combination of splitting the
object, applying a function, and combining the results. This can be
used to group large amounts of data and compute operations on these
groups.

Parameters
----------
by : mapping, function, label, pd.Grouper or list of such
Used to determine the groups for the groupby.
If ``by`` is a function, it's called on each value of the object's
index. If a dict or Series is passed, the Series or dict VALUES
will be used to determine the groups (the Series' values are first
aligned; see ``.align()`` method). If a list or ndarray of length
equal to the selected axis is passed (see the `groupby user guide
    <https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html#splitting-an-object-into-groups>`_),
the values are used as-is to determine the groups. A label or list
of labels may be passed to group by the columns in ``self``.
Notice that a tuple is interpreted as a (single) key.
axis : {0 or 'index', 1 or 'columns'}, default 0
Split along rows (0) or columns (1). For `Series` this parameter
is unused and defaults to 0.

    .. deprecated:: 2.1.0

        Will be removed and behave like axis=0 in a future version.
        For ``axis=1``, do ``frame.T.groupby(...)`` instead.

level : int, level name, or sequence of such, default None
If the axis is a MultiIndex (hierarchical), group by a particular
level or levels. Do not specify both ``by`` and ``level``.
as_index : bool, default True
Return object with group labels as the
index. Only relevant for DataFrame input. as_index=False is
effectively "SQL-style" grouped output. This argument has no effect
on filtrations (see the `filtrations in the user guide
    <https://pandas.pydata.org/docs/dev/user_guide/groupby.html#filtration>`_),
such as ``head()``, ``tail()``, ``nth()`` and in transformations
(see the `transformations in the user guide
    <https://pandas.pydata.org/docs/dev/user_guide/groupby.html#transformation>`_).
sort : bool, default True
Sort group keys. Get better performance by turning this off.
Note this does not influence the order of observations within each
group. Groupby preserves the order of rows within each group. If False,
the groups will appear in the same order as they did in the original DataFrame.
This argument has no effect on filtrations (see the `filtrations in the user guide
    <https://pandas.pydata.org/docs/dev/user_guide/groupby.html#filtration>`_),
such as ``head()``, ``tail()``, ``nth()`` and in transformations
(see the `transformations in the user guide
    <https://pandas.pydata.org/docs/dev/user_guide/groupby.html#transformation>`_).

    .. versionchanged:: 2.0.0

        Specifying ``sort=False`` with an ordered categorical grouper will no
        longer sort the values.

group_keys : bool, default True
When calling apply and the ``by`` argument produces a like-indexed
(i.e. :ref:`a transform <groupby.transform>`) result, add group keys to
index to identify pieces. By default group keys are not included
when the result's index (and column) labels match the inputs, and
are included otherwise.

    .. versionchanged:: 1.5.0

       Warns that ``group_keys`` will no longer be ignored when the
       result from ``apply`` is a like-indexed Series or DataFrame.
       Specify ``group_keys`` explicitly to include the group keys or
       not.

    .. versionchanged:: 2.0.0

       ``group_keys`` now defaults to ``True``.

observed : bool, default False
This only applies if any of the groupers are Categoricals.
If True: only show observed values for categorical groupers.
If False: show all values for categorical groupers.

    .. deprecated:: 2.1.0

        The default value will change to True in a future version of pandas.

dropna : bool, default True
If True, and if group keys contain NA values, NA values together
with row/column will be dropped.
If False, NA values will also be treated as the key in groups.

Returns
-------
pandas.api.typing.DataFrameGroupBy
Returns a groupby object that contains information about the groups.

See Also
--------
resample : Convenience method for frequency conversion and resampling
of time series.

Notes
-----
See the `user guide
<https://pandas.pydata.org/pandas-docs/stable/groupby.html>`__ for more
detailed usage and examples, including splitting an object into groups,
iterating through groups, selecting a group, aggregation, and more.

Examples
--------
> > > df = pd.DataFrame({'Animal': ['Falcon', 'Falcon',
...                               'Parrot', 'Parrot'],
> > > ...                    'Max Speed': [380., 370., 24., 26.]})
> > > df
> > > Animal Max Speed
> > > 0 Falcon 380.0
> > > 1 Falcon 370.0
> > > 2 Parrot 24.0
> > > 3 Parrot 26.0
> > > df.groupby(['Animal']).mean()
> > > Max Speed
> > > Animal
> > > Falcon 375.0
> > > Parrot 25.0

**Hierarchical Indexes**

We can groupby different levels of a hierarchical index
using the `level` parameter:

> > > arrays = [['Falcon', 'Falcon', 'Parrot', 'Parrot'],
> > > ...           ['Captive', 'Wild', 'Captive', 'Wild']]
> > > index = pd.MultiIndex.from_arrays(arrays, names=('Animal', 'Type'))
> > > df = pd.DataFrame({'Max Speed': [390., 350., 30., 20.]},
> > > ... index=index)
> > > df
> > > Max Speed
> > > Animal Type
> > > Falcon Captive 390.0
> > > Wild 350.0
> > > Parrot Captive 30.0
> > > Wild 20.0
> > > df.groupby(level=0).mean()
> > > Max Speed
> > > Animal
> > > Falcon 370.0
> > > Parrot 25.0
> > > df.groupby(level="Type").mean()
> > > Max Speed
> > > Type
> > > Captive 210.0
> > > Wild 185.0

We can also choose to include NA in group keys or not by setting
`dropna` parameter, the default setting is `True`.

> > > l = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]]
> > > df = pd.DataFrame(l, columns=["a", "b", "c"])

> > > df.groupby(by=["b"]).sum()
> > > a c
> > > b
> > > 1.0 2 3
> > > 2.0 2 5

> > > df.groupby(by=["b"], dropna=False).sum()
> > > a c
> > > b
> > > 1.0 2 3
> > > 2.0 2 5
> > > NaN 1 4

> > > l = [["a", 12, 12], [None, 12.3, 33.], ["b", 12.3, 123], ["a", 1, 1]]
> > > df = pd.DataFrame(l, columns=["a", "b", "c"])

> > > df.groupby(by="a").sum()
> > > b c
> > > a
> > > a 13.0 13.0
> > > b 12.3 123.0

> > > df.groupby(by="a", dropna=False).sum()
> > > b c
> > > a
> > > a 13.0 13.0
> > > b 12.3 123.0
> > > NaN 12.3 33.0

When using ``.apply()``, use ``group_keys`` to include or exclude the
group keys. The ``group_keys`` argument defaults to ``True`` (include).

> > > df = pd.DataFrame({'Animal': ['Falcon', 'Falcon',
...                               'Parrot', 'Parrot'],
> > > ...                    'Max Speed': [380., 370., 24., 26.]})
> > > df.groupby("Animal", group_keys=True)[['Max Speed']].apply(lambda x: x)
> > > Max Speed
> > > Animal
> > > Falcon 0 380.0
> > > 1 370.0
> > > Parrot 2 24.0
> > > 3 26.0

> > > df.groupby("Animal", group_keys=False)[['Max Speed']].apply(lambda x: x)
> > > Max Speed
> > > 0 380.0
> > > 1 370.0
> > > 2 24.0
> > > 3 26.0

---

###
`pandas.DataFrame.pivot_table(self, values=None, index=None, columns=None, aggfunc: 'AggFuncType' = 'mean', fill_value=None, margins: 'bool' = False, dropna: 'bool' = True, margins_name: 'Level' = 'All', observed: 'bool | lib.NoDefault' = <no_default>, sort: 'bool' = True) -> 'DataFrame'`

Create a spreadsheet-style pivot table as a DataFrame.

The levels in the pivot table will be stored in MultiIndex objects
(hierarchical indexes) on the index and columns of the result DataFrame.

Parameters
----------
values : list-like or scalar, optional
Column or columns to aggregate.
index : column, Grouper, array, or list of the previous
Keys to group by on the pivot table index. If a list is passed,
it can contain any of the other types (except list). If an array is
passed, it must be the same length as the data and will be used in
the same manner as column values.
columns : column, Grouper, array, or list of the previous
Keys to group by on the pivot table column. If a list is passed,
it can contain any of the other types (except list). If an array is
passed, it must be the same length as the data and will be used in
the same manner as column values.
aggfunc : function, list of functions, dict, default "mean"
If a list of functions is passed, the resulting pivot table will have
hierarchical columns whose top level are the function names
(inferred from the function objects themselves).
If a dict is passed, the key is column to aggregate and the value is
function or list of functions. If ``margin=True``, aggfunc will be
used to calculate the partial aggregates.
fill_value : scalar, default None
Value to replace missing values with (in the resulting pivot table,
after aggregation).
margins : bool, default False
If ``margins=True``, special ``All`` columns and rows
will be added with partial group aggregates across the categories
on the rows and columns.
dropna : bool, default True
Do not include columns whose entries are all NaN. If True,
rows with a NaN value in any column will be omitted before
computing margins.
margins_name : str, default 'All'
Name of the row / column that will contain the totals
when margins is True.
observed : bool, default False
This only applies if any of the groupers are Categoricals.
If True: only show observed values for categorical groupers.
If False: show all values for categorical groupers.

    .. deprecated:: 2.2.0

        The default value of ``False`` is deprecated and will change to
        ``True`` in a future version of pandas.

sort : bool, default True
Specifies if the result should be sorted.

    .. versionadded:: 1.3.0

Returns
-------
DataFrame
An Excel style pivot table.

See Also
--------
DataFrame.pivot : Pivot without aggregation that can handle
non-numeric data.
DataFrame.melt: Unpivot a DataFrame from wide to long format,
optionally leaving identifiers set.
wide_to_long : Wide panel to long format. Less flexible but more
user-friendly than melt.

Notes
-----
Reference :ref:`the user guide <reshaping.pivot>` for more examples.

Examples
--------
> > > df = pd.DataFrame({"A": ["foo", "foo", "foo", "foo", "foo",
...                          "bar", "bar", "bar", "bar"],
> > > ...                    "B": ["one", "one", "one", "two", "two",
...                          "one", "one", "two", "two"],
> > > ...                    "C": ["small", "large", "large", "small",
...                          "small", "large", "small", "small",
...                          "large"],
> > > ...                    "D": [1, 2, 2, 3, 3, 4, 5, 6, 7],
> > > ...                    "E": [2, 4, 5, 5, 6, 6, 8, 9, 9]})
> > > df
> > > A B C D E
> > > 0 foo one small 1 2
> > > 1 foo one large 2 4
> > > 2 foo one large 2 5
> > > 3 foo two small 3 5
> > > 4 foo two small 3 6
> > > 5 bar one large 4 6
> > > 6 bar one small 5 8
> > > 7 bar two small 6 9
> > > 8 bar two large 7 9

This first example aggregates values by taking the sum.

> > > table = pd.pivot_table(df, values='D', index=['A', 'B'],
> > > ... columns=['C'], aggfunc="sum")
> > > table
> > > C large small
> > > A B
> > > bar one 4.0 5.0
> > > two 7.0 6.0
> > > foo one 4.0 1.0
> > > two NaN 6.0

We can also fill missing values using the `fill_value` parameter.

> > > table = pd.pivot_table(df, values='D', index=['A', 'B'],
> > > ... columns=['C'], aggfunc="sum", fill_value=0)
> > > table
> > > C large small
> > > A B
> > > bar one 4 5
> > > two 7 6
> > > foo one 4 1
> > > two 0 6

The next example aggregates by taking the mean across multiple columns.

> > > table = pd.pivot_table(df, values=['D', 'E'], index=['A', 'C'],
> > > ... aggfunc={'D': "mean", 'E': "mean"})
> > > table
> > > D E
> > > A C
> > > bar large 5.500000 7.500000
> > > small 5.500000 8.500000
> > > foo large 2.000000 4.500000
> > > small 2.333333 4.333333

We can also calculate multiple types of aggregations for any given
value column.

> > > table = pd.pivot_table(df, values=['D', 'E'], index=['A', 'C'],
> > > ... aggfunc={'D': "mean",
> > > ...                                 'E': ["min", "max", "mean"]})
> > > table
> > > D E
> > > mean max mean min
> > > A C
> > > bar large 5.500000 9 7.500000 6
> > > small 5.500000 9 8.500000 8
> > > foo large 2.000000 5 4.500000 4
> > > small 2.333333 6 4.333333 2

---

### `pandas.DataFrame.loc`

Access a group of rows and columns by label(s) or a boolean array.

``.loc[]`` is primarily label based, but may also be used with a
boolean array.

Allowed inputs are:

- A single label, e.g. ``5`` or ``'a'``, (note that ``5`` is
  interpreted as a *label* of the index, and **never** as an
  integer position along the index).
- A list or array of labels, e.g. ``['a', 'b', 'c']``.
- A slice object with labels, e.g. ``'a':'f'``.

  .. warning:: Note that contrary to usual python slices, **both** the
  start and the stop are included

- A boolean array of the same length as the axis being sliced,
  e.g. ``[True, False, True]``.
- An alignable boolean Series. The index of the key will be aligned before
  masking.
- An alignable Index. The Index of the returned selection will be the input.
- A ``callable`` function with one argument (the calling Series or
  DataFrame) and that returns valid output for indexing (one of the above)

See more at :ref:`Selection by Label <indexing.label>`.

Raises
------
KeyError
If any items are not found.
IndexingError
If an indexed key is passed and its index is unalignable to the frame index.

See Also
--------
DataFrame.at : Access a single value for a row/column label pair.
DataFrame.iloc : Access group of rows and columns by integer position(s).
DataFrame.xs : Returns a cross-section (row(s) or column(s)) from the
Series/DataFrame.
Series.loc : Access group of values using labels.

Examples
--------
**Getting values**

> > > df = pd.DataFrame([[1, 2], [4, 5], [7, 8]],
> > > ... index=['cobra', 'viper', 'sidewinder'],
> > > ... columns=['max_speed', 'shield'])
> > > df
> > > max_speed shield
> > > cobra 1 2
> > > viper 4 5
> > > sidewinder 7 8

Single label. Note this returns the row as a Series.

> > > df.loc['viper']
> > > max_speed 4
> > > shield 5
> > > Name: viper, dtype: int64

List of labels. Note using ``[[]]`` returns a DataFrame.

> > > df.loc[['viper', 'sidewinder']]
> > > max_speed shield
> > > viper 4 5
> > > sidewinder 7 8

Single label for row and column

> > > df.loc['cobra', 'shield']
> > > 2

Slice with labels for row and single label for column. As mentioned
above, note that both the start and stop of the slice are included.

> > > df.loc['cobra':'viper', 'max_speed']
> > > cobra 1
> > > viper 4
> > > Name: max_speed, dtype: int64

Boolean list with the same length as the row axis

> > > df.loc[[False, False, True]]
> > > max_speed shield
> > > sidewinder 7 8

Alignable boolean Series:

> > > df.loc[pd.Series([False, True, False],
> > > ... index=['viper', 'sidewinder', 'cobra'])]
> > > max_speed shield
> > > sidewinder 7 8

Index (same behavior as ``df.reindex``)

> > > df.loc[pd.Index(["cobra", "viper"], name="foo")]
> > > max_speed shield
> > > foo
> > > cobra 1 2
> > > viper 4 5

Conditional that returns a boolean Series

> > > df.loc[df['shield'] > 6]
> > > max_speed shield
> > > sidewinder 7 8

Conditional that returns a boolean Series with column labels specified

> > > df.loc[df['shield'] > 6, ['max_speed']]
> > > max_speed
> > > sidewinder 7

Multiple conditional using ``&`` that returns a boolean Series

> > > df.loc[(df['max_speed'] > 1) & (df['shield'] < 8)]
> > > max_speed shield
> > > viper 4 5

Multiple conditional using ``|`` that returns a boolean Series

> > > df.loc[(df['max_speed'] > 4) | (df['shield'] < 5)]
> > > max_speed shield
> > > cobra 1 2
> > > sidewinder 7 8

Please ensure that each condition is wrapped in parentheses ``()``.
See the :ref:`user guide<indexing.boolean>`
for more details and explanations of Boolean indexing.

.. note::
If you find yourself using 3 or more conditionals in ``.loc[]``,
consider using :ref:`advanced indexing<advanced.advanced_hierarchical>`.

    See below for using ``.loc[]`` on MultiIndex DataFrames.

Callable that returns a boolean Series

> > > df.loc[lambda df: df['shield'] == 8]
> > > max_speed shield
> > > sidewinder 7 8

**Setting values**

Set value for all items matching the list of labels

> > > df.loc[['viper', 'sidewinder'], ['shield']] = 50
> > > df
> > > max_speed shield
> > > cobra 1 2
> > > viper 4 50
> > > sidewinder 7 50

Set value for an entire row

> > > df.loc['cobra'] = 10
> > > df
> > > max_speed shield
> > > cobra 10 10
> > > viper 4 50
> > > sidewinder 7 50

Set value for an entire column

> > > df.loc[:, 'max_speed'] = 30
> > > df
> > > max_speed shield
> > > cobra 30 10
> > > viper 30 50
> > > sidewinder 30 50

Set value for rows matching callable condition

> > > df.loc[df['shield'] > 35] = 0
> > > df
> > > max_speed shield
> > > cobra 30 10
> > > viper 0 0
> > > sidewinder 0 0

Add value matching location

> > > df.loc["viper", "shield"] += 5
> > > df
> > > max_speed shield
> > > cobra 30 10
> > > viper 0 5
> > > sidewinder 0 0

Setting using a ``Series`` or a ``DataFrame`` sets the values matching the
index labels, not the index positions.

> > > shuffled_df = df.loc[["viper", "cobra", "sidewinder"]]
> > > df.loc[:] += shuffled_df
> > > df
> > > max_speed shield
> > > cobra 60 20
> > > viper 0 10
> > > sidewinder 0 0

**Getting values on a DataFrame with an index that has integer labels**

Another example using integers for the index

> > > df = pd.DataFrame([[1, 2], [4, 5], [7, 8]],
> > > ... index=[7, 8, 9], columns=['max_speed', 'shield'])
> > > df
> > > max_speed shield
> > > 7 1 2
> > > 8 4 5
> > > 9 7 8

Slice with integer labels for rows. As mentioned above, note that both
the start and stop of the slice are included.

> > > df.loc[7:9]
> > > max_speed shield
> > > 7 1 2
> > > 8 4 5
> > > 9 7 8

**Getting values with a MultiIndex**

A number of examples using a DataFrame with a MultiIndex

> > > tuples = [
...     ('cobra', 'mark i'), ('cobra', 'mark ii'),
...     ('sidewinder', 'mark i'), ('sidewinder', 'mark ii'),
...     ('viper', 'mark ii'), ('viper', 'mark iii')
... ]
> > > index = pd.MultiIndex.from_tuples(tuples)
> > > values = [[12, 2], [0, 4], [10, 20],
> > > ...           [1, 4], [7, 1], [16, 36]]
> > > df = pd.DataFrame(values, columns=['max_speed', 'shield'], index=index)
> > > df
> > > max_speed shield
> > > cobra mark i 12 2
> > > mark ii 0 4
> > > sidewinder mark i 10 20
> > > mark ii 1 4
> > > viper mark ii 7 1
> > > mark iii 16 36

Single label. Note this returns a DataFrame with a single index.

> > > df.loc['cobra']
> > > max_speed shield
> > > mark i 12 2
> > > mark ii 0 4

Single index tuple. Note this returns a Series.

> > > df.loc[('cobra', 'mark ii')]
> > > max_speed 0
> > > shield 4
> > > Name: (cobra, mark ii), dtype: int64

Single label for row and column. Similar to passing in a tuple, this
returns a Series.

> > > df.loc['cobra', 'mark i']
> > > max_speed 12
> > > shield 2
> > > Name: (cobra, mark i), dtype: int64

Single tuple. Note using ``[[]]`` returns a DataFrame.

> > > df.loc[[('cobra', 'mark ii')]]
> > > max_speed shield
> > > cobra mark ii 0 4

Single tuple for the index with a single label for the column

> > > df.loc[('cobra', 'mark i'), 'shield']
> > > 2

Slice from index tuple to single label

> > > df.loc[('cobra', 'mark i'):'viper']
> > > max_speed shield
> > > cobra mark i 12 2
> > > mark ii 0 4
> > > sidewinder mark i 10 20
> > > mark ii 1 4
> > > viper mark ii 7 1
> > > mark iii 16 36

Slice from index tuple to index tuple

> > > df.loc[('cobra', 'mark i'):('viper', 'mark ii')]
> > > max_speed shield
> > > cobra mark i 12 2
> > > mark ii 0 4
> > > sidewinder mark i 10 20
> > > mark ii 1 4
> > > viper mark ii 7 1

Please see the :ref:`user guide<advanced.advanced_hierarchical>`
for more details and explanations of advanced indexing.

---

### `pandas.DataFrame.iloc`

Purely integer-location based indexing for selection by position.

.. deprecated:: 2.2.0

Returning a tuple from a callable is deprecated.

``.iloc[]`` is primarily integer position based (from ``0`` to
``length-1`` of the axis), but may also be used with a boolean
array.

Allowed inputs are:

- An integer, e.g. ``5``.
- A list or array of integers, e.g. ``[4, 3, 0]``.
- A slice object with ints, e.g. ``1:7``.
- A boolean array.
- A ``callable`` function with one argument (the calling Series or
  DataFrame) and that returns valid output for indexing (one of the above).
  This is useful in method chains, when you don't have a reference to the
  calling object, but would like to base your selection on
  some value.
- A tuple of row and column indexes. The tuple elements consist of one of the
  above inputs, e.g. ``(0, 1)``.

``.iloc`` will raise ``IndexError`` if a requested indexer is
out-of-bounds, except *slice* indexers which allow out-of-bounds
indexing (this conforms with python/numpy *slice* semantics).

See more at :ref:`Selection by Position <indexing.integer>`.

See Also
--------
DataFrame.iat : Fast integer location scalar accessor.
DataFrame.loc : Purely label-location based indexer for selection by label.
Series.iloc : Purely integer-location based indexing for
selection by position.

Examples
--------
> > > mydict = [{'a': 1, 'b': 2, 'c': 3, 'd': 4},
... {'a': 100, 'b': 200, 'c': 300, 'd': 400},
... {'a': 1000, 'b': 2000, 'c': 3000, 'd': 4000}]
> > > df = pd.DataFrame(mydict)
> > > df
> > > a b c d
> > > 0 1 2 3 4
> > > 1 100 200 300 400
> > > 2 1000 2000 3000 4000

**Indexing just the rows**

With a scalar integer.

> > > type(df.iloc[0])
<class 'pandas.core.series.Series'>
> > > df.iloc[0]
> > > a 1
> > > b 2
> > > c 3
> > > d 4
> > > Name: 0, dtype: int64

With a list of integers.

> > > df.iloc[[0]]
> > > a b c d
> > > 0 1 2 3 4
> > > type(df.iloc[[0]])
<class 'pandas.core.frame.DataFrame'>

> > > df.iloc[[0, 1]]
> > > a b c d
> > > 0 1 2 3 4
> > > 1 100 200 300 400

With a `slice` object.

> > > df.iloc[:3]
> > > a b c d
> > > 0 1 2 3 4
> > > 1 100 200 300 400
> > > 2 1000 2000 3000 4000

With a boolean mask the same length as the index.

> > > df.iloc[[True, False, True]]
> > > a b c d
> > > 0 1 2 3 4
> > > 2 1000 2000 3000 4000

With a callable, useful in method chains. The `x` passed
to the ``lambda`` is the DataFrame being sliced. This selects
the rows whose index label even.

> > > df.iloc[lambda x: x.index % 2 == 0]
> > > a b c d
> > > 0 1 2 3 4
> > > 2 1000 2000 3000 4000

**Indexing both axes**

You can mix the indexer types for the index and columns. Use ``:`` to
select the entire axis.

With scalar integers.

> > > df.iloc[0, 1]
> > > 2

With lists of integers.

> > > df.iloc[[0, 2], [1, 3]]
> > > b d
> > > 0 2 4
> > > 2 2000 4000

With `slice` objects.

> > > df.iloc[1:3, 0:3]
> > > a b c
> > > 1 100 200 300
> > > 2 1000 2000 3000

With a boolean array whose length matches the columns.

> > > df.iloc[:, [True, False, True, False]]
> > > a c
> > > 0 1 3
> > > 1 100 300
> > > 2 1000 3000

With a callable function that expects the Series or DataFrame.

> > > df.iloc[:, lambda df: [0, 2]]
> > > a c
> > > 0 1 3
> > > 1 100 300
> > > 2 1000 3000

---

###
`pandas.DataFrame.apply(self, func: 'AggFuncType', axis: 'Axis' = 0, raw: 'bool' = False, result_type: "Literal['expand', 'reduce', 'broadcast'] | None" = None, args=(), by_row: "Literal[False, 'compat']" = 'compat', engine: "Literal['python', 'numba']" = 'python', engine_kwargs: 'dict[str, bool] | None' = None, **kwargs)`

Apply a function along an axis of the DataFrame.

Objects passed to the function are Series objects whose index is
either the DataFrame's index (``axis=0``) or the DataFrame's columns
(``axis=1``). By default (``result_type=None``), the final return type
is inferred from the return type of the applied function. Otherwise,
it depends on the `result_type` argument.

Parameters
----------
func : function
Function to apply to each column or row.
axis : {0 or 'index', 1 or 'columns'}, default 0
Axis along which the function is applied:

    * 0 or 'index': apply function to each column.
    * 1 or 'columns': apply function to each row.

raw : bool, default False
Determines if row or column is passed as a Series or ndarray object:

    * ``False`` : passes each row or column as a Series to the
      function.
    * ``True`` : the passed function will receive ndarray objects
      instead.
      If you are just applying a NumPy reduction function this will
      achieve much better performance.

result_type : {'expand', 'reduce', 'broadcast', None}, default None
These only act when ``axis=1`` (columns):

    * 'expand' : list-like results will be turned into columns.
    * 'reduce' : returns a Series if possible rather than expanding
      list-like results. This is the opposite of 'expand'.
    * 'broadcast' : results will be broadcast to the original shape
      of the DataFrame, the original index and columns will be
      retained.

    The default behaviour (None) depends on the return value of the
    applied function: list-like results will be returned as a Series
    of those. However if the apply function returns a Series these
    are expanded to columns.

args : tuple
Positional arguments to pass to `func` in addition to the
array/series.
by_row : False or "compat", default "compat"
Only has an effect when ``func`` is a listlike or dictlike of funcs
and the func isn't a string.
If "compat", will if possible first translate the func into pandas
methods (e.g. ``Series().apply(np.sum)`` will be translated to
``Series().sum()``). If that doesn't work, will try call to apply again with
``by_row=True`` and if that fails, will call apply again with
``by_row=False`` (backward compatible).
If False, the funcs will be passed the whole Series at once.

    .. versionadded:: 2.1.0

engine : {'python', 'numba'}, default 'python'
Choose between the python (default) engine or the numba engine in apply.

    The numba engine will attempt to JIT compile the passed function,
    which may result in speedups for large DataFrames.
    It also supports the following engine_kwargs :

    - nopython (compile the function in nopython mode)
    - nogil (release the GIL inside the JIT compiled function)
    - parallel (try to apply the function in parallel over the DataFrame)

      Note: Due to limitations within numba/how pandas interfaces with numba,
      you should only use this if raw=True

    Note: The numba compiler only supports a subset of
    valid Python/numpy operations.

    Please read more about the `supported python features
    <https://numba.pydata.org/numba-doc/dev/reference/pysupported.html>`_
    and `supported numpy features
    <https://numba.pydata.org/numba-doc/dev/reference/numpysupported.html>`_
    in numba to learn what you can or cannot use in the passed function.

    .. versionadded:: 2.2.0

engine_kwargs : dict
Pass keyword arguments to the engine.
This is currently only used by the numba engine,
see the documentation for the engine argument for more information.
**kwargs
Additional keyword arguments to pass as keywords arguments to
`func`.

Returns
-------
Series or DataFrame
Result of applying ``func`` along the given axis of the
DataFrame.

See Also
--------
DataFrame.map: For elementwise operations.
DataFrame.aggregate: Only perform aggregating type operations.
DataFrame.transform: Only perform transforming type operations.

Notes
-----
Functions that mutate the passed object can produce unexpected
behavior or errors and are not supported. See :ref:`gotchas.udf-mutation`
for more details.

Examples
--------
> > > df = pd.DataFrame([[4, 9]] * 3, columns=['A', 'B'])
> > > df
> > > A B
> > > 0 4 9
> > > 1 4 9
> > > 2 4 9

Using a numpy universal function (in this case the same as
``np.sqrt(df)``):

> > > df.apply(np.sqrt)
> > > A B
> > > 0 2.0 3.0
> > > 1 2.0 3.0
> > > 2 2.0 3.0

Using a reducing function on either axis

> > > df.apply(np.sum, axis=0)
> > > A 12
> > > B 27
> > > dtype: int64

> > > df.apply(np.sum, axis=1)
> > > 0 13
> > > 1 13
> > > 2 13
> > > dtype: int64

Returning a list-like will result in a Series

> > > df.apply(lambda x: [1, 2], axis=1)
> > > 0    [1, 2]
> > > 1    [1, 2]
> > > 2    [1, 2]
> > > dtype: object

Passing ``result_type='expand'`` will expand list-like results
to columns of a Dataframe

> > > df.apply(lambda x: [1, 2], axis=1, result_type='expand')
> > > 0 1
> > > 0 1 2
> > > 1 1 2
> > > 2 1 2

Returning a Series inside the function is similar to passing
``result_type='expand'``. The resulting column names
will be the Series index.

> > > df.apply(lambda x: pd.Series([1, 2], index=['foo', 'bar']), axis=1)
> > > foo bar
> > > 0 1 2
> > > 1 1 2
> > > 2 1 2

Passing ``result_type='broadcast'`` will ensure the same shape
result, whether list-like or scalar is returned by the function,
and broadcast it along the axis. The resulting column names will
be the originals.

> > > df.apply(lambda x: [1, 2], axis=1, result_type='broadcast')
> > > A B
> > > 0 1 2
> > > 1 1 2
> > > 2 1 2

---

###
`pandas.DataFrame.drop(self, labels: 'IndexLabel | None' = None, *, axis: 'Axis' = 0, index: 'IndexLabel | None' = None, columns: 'IndexLabel | None' = None, level: 'Level | None' = None, inplace: 'bool' = False, errors: 'IgnoreRaise' = 'raise') -> 'DataFrame | None'`

Drop specified labels from rows or columns.

Remove rows or columns by specifying label names and corresponding
axis, or by directly specifying index or column names. When using a
multi-index, labels on different levels can be removed by specifying
the level. See the :ref:`user guide <advanced.shown_levels>`
for more information about the now unused levels.

Parameters
----------
labels : single label or list-like
Index or column labels to drop. A tuple will be used as a single
label and not treated as a list-like.
axis : {0 or 'index', 1 or 'columns'}, default 0
Whether to drop labels from the index (0 or 'index') or
columns (1 or 'columns').
index : single label or list-like
Alternative to specifying axis (``labels, axis=0``
is equivalent to ``index=labels``).
columns : single label or list-like
Alternative to specifying axis (``labels, axis=1``
is equivalent to ``columns=labels``).
level : int or level name, optional
For MultiIndex, level from which the labels will be removed.
inplace : bool, default False
If False, return a copy. Otherwise, do operation
in place and return None.
errors : {'ignore', 'raise'}, default 'raise'
If 'ignore', suppress error and only existing labels are
dropped.

Returns
-------
DataFrame or None
Returns DataFrame or None DataFrame with the specified
index or column labels removed or None if inplace=True.

Raises
------
KeyError
If any of the labels is not found in the selected axis.

See Also
--------
DataFrame.loc : Label-location based indexer for selection by label.
DataFrame.dropna : Return DataFrame with labels on given axis omitted
where (all or any) data are missing.
DataFrame.drop_duplicates : Return DataFrame with duplicate rows
removed, optionally only considering certain columns.
Series.drop : Return Series with specified index labels removed.

Examples
--------
> > > df = pd.DataFrame(np.arange(12).reshape(3, 4),
> > > ... columns=['A', 'B', 'C', 'D'])
> > > df
> > > A B C D
> > > 0 0 1 2 3
> > > 1 4 5 6 7
> > > 2 8 9 10 11

Drop columns

> > > df.drop(['B', 'C'], axis=1)
> > > A D
> > > 0 0 3
> > > 1 4 7
> > > 2 8 11

> > > df.drop(columns=['B', 'C'])
> > > A D
> > > 0 0 3
> > > 1 4 7
> > > 2 8 11

Drop a row by index

> > > df.drop([0, 1])
> > > A B C D
> > > 2 8 9 10 11

Drop columns and/or rows of MultiIndex DataFrame

> > > midx = pd.MultiIndex(levels=[['llama', 'cow', 'falcon'],
> > > ...                              ['speed', 'weight', 'length']],
> > > ... codes=[[0, 0, 0, 1, 1, 1, 2, 2, 2],
> > > ...                             [0, 1, 2, 0, 1, 2, 0, 1, 2]])
> > > df = pd.DataFrame(index=midx, columns=['big', 'small'],
> > > ... data=[[45, 30], [200, 100], [1.5, 1], [30, 20],
> > > ...                         [250, 150], [1.5, 0.8], [320, 250],
> > > ...                         [1, 0.8], [0.3, 0.2]])
> > > df
> > > big small
> > > llama speed 45.0 30.0
> > > weight 200.0 100.0
> > > length 1.5 1.0
> > > cow speed 30.0 20.0
> > > weight 250.0 150.0
> > > length 1.5 0.8
> > > falcon speed 320.0 250.0
> > > weight 1.0 0.8
> > > length 0.3 0.2

Drop a specific index combination from the MultiIndex
DataFrame, i.e., drop the combination ``'falcon'`` and
``'weight'``, which deletes only the corresponding row

> > > df.drop(index=('falcon', 'weight'))
> > > big small
> > > llama speed 45.0 30.0
> > > weight 200.0 100.0
> > > length 1.5 1.0
> > > cow speed 30.0 20.0
> > > weight 250.0 150.0
> > > length 1.5 0.8
> > > falcon speed 320.0 250.0
> > > length 0.3 0.2

> > > df.drop(index='cow', columns='small')
> > > big
> > > llama speed 45.0
> > > weight 200.0
> > > length 1.5
> > > falcon speed 320.0
> > > weight 1.0
> > > length 0.3

> > > df.drop(index='length', level=1)
> > > big small
> > > llama speed 45.0 30.0
> > > weight 200.0 100.0
> > > cow speed 30.0 20.0
> > > weight 250.0 150.0
> > > falcon speed 320.0 250.0
> > > weight 1.0 0.8

---

###
`pandas.DataFrame.fillna(self, value: 'Hashable | Mapping | Series | DataFrame | None' = None, *, method: 'FillnaOptions | None' = None, axis: 'Axis | None' = None, inplace: 'bool_t' = False, limit: 'int | None' = None, downcast: 'dict | None | lib.NoDefault' = <no_default>) -> 'Self | None'`

Fill NA/NaN values using the specified method.

Parameters
----------
value : scalar, dict, Series, or DataFrame
Value to use to fill holes (e.g. 0), alternately a
dict/Series/DataFrame of values specifying which value to use for
each index (for a Series) or column (for a DataFrame). Values not
in the dict/Series/DataFrame will not be filled. This value cannot
be a list.
method : {'backfill', 'bfill', 'ffill', None}, default None
Method to use for filling holes in reindexed Series:

    * ffill: propagate last valid observation forward to next valid.
    * backfill / bfill: use next valid observation to fill gap.

    .. deprecated:: 2.1.0
        Use ffill or bfill instead.

axis : {0 or 'index'} for Series, {0 or 'index', 1 or 'columns'} for DataFrame
Axis along which to fill missing values. For `Series`
this parameter is unused and defaults to 0.
inplace : bool, default False
If True, fill in-place. Note: this will modify any
other views on this object (e.g., a no-copy slice for a column in a
DataFrame).
limit : int, default None
If method is specified, this is the maximum number of consecutive
NaN values to forward/backward fill. In other words, if there is
a gap with more than this number of consecutive NaNs, it will only
be partially filled. If method is not specified, this is the
maximum number of entries along the entire axis where NaNs will be
filled. Must be greater than 0 if not None.
downcast : dict, default is None
A dict of item->dtype of what to downcast if possible,
or the string 'infer' which will try to downcast to an appropriate
equal type (e.g. float64 to int64 if possible).

    .. deprecated:: 2.2.0

Returns
-------
Series/DataFrame or None
Object with missing values filled or None if ``inplace=True``.

See Also
--------
ffill : Fill values by propagating the last valid observation to next valid.
bfill : Fill values by using the next valid observation to fill the gap.
interpolate : Fill NaN values using interpolation.
reindex : Conform object to new index.
asfreq : Convert TimeSeries to specified frequency.

Examples
--------
> > > df = pd.DataFrame([[np.nan, 2, np.nan, 0],
> > > ...                    [3, 4, np.nan, 1],
> > > ...                    [np.nan, np.nan, np.nan, np.nan],
> > > ...                    [np.nan, 3, np.nan, 4]],
> > > ... columns=list("ABCD"))
> > > df
> > > A B C D
> > > 0 NaN 2.0 NaN 0.0
> > > 1 3.0 4.0 NaN 1.0
> > > 2 NaN NaN NaN NaN
> > > 3 NaN 3.0 NaN 4.0

Replace all NaN elements with 0s.

> > > df.fillna(0)
> > > A B C D
> > > 0 0.0 2.0 0.0 0.0
> > > 1 3.0 4.0 0.0 1.0
> > > 2 0.0 0.0 0.0 0.0
> > > 3 0.0 3.0 0.0 4.0

Replace all NaN elements in column 'A', 'B', 'C', and 'D', with 0, 1,
2, and 3 respectively.

> > > values = {"A": 0, "B": 1, "C": 2, "D": 3}
> > > df.fillna(value=values)
> > > A B C D
> > > 0 0.0 2.0 2.0 0.0
> > > 1 3.0 4.0 2.0 1.0
> > > 2 0.0 1.0 2.0 3.0
> > > 3 0.0 3.0 2.0 4.0

Only replace the first NaN element.

> > > df.fillna(value=values, limit=1)
> > > A B C D
> > > 0 0.0 2.0 2.0 0.0
> > > 1 3.0 4.0 NaN 1.0
> > > 2 NaN 1.0 NaN 3.0
> > > 3 NaN 3.0 NaN 4.0

When filling using a DataFrame, replacement happens along
the same column names and same indices

> > > df2 = pd.DataFrame(np.zeros((4, 4)), columns=list("ABCE"))
> > > df.fillna(df2)
> > > A B C D
> > > 0 0.0 2.0 0.0 0.0
> > > 1 3.0 4.0 0.0 1.0
> > > 2 0.0 0.0 0.0 NaN
> > > 3 0.0 3.0 0.0 4.0

Note that column D is not affected since it is not present in df2.

---
