Real datasets are rarely clean. df.isna() produces a boolean DataFrame marking every NaN cell, and df.isna().sum() counts NaNs per column so you can see where the holes are. To detect rows that are exact duplicates of earlier rows, df.duplicated() returns a boolean Series, and df.duplicated().sum() totals them. Removing duplicates is a one-liner: df.drop_duplicates() drops all repeat rows, while df.drop_duplicates(subset=['col1', 'col2']) considers only the listed columns when deciding what counts as a duplicate.
For handling missing values, two complementary strategies exist: drop them or fill them. df.dropna() removes any row containing a NaN, and passing how='all' restricts the drop to rows that are entirely NaN. df.dropna(axis=1) instead drops columns with missing values. Filling is done with df.fillna(0) for a global constant, or with a per-column dict like df.fillna({'a': 0, 'b': -1}) for column-specific values. For ordered data such as time series, forward-fill propagates the last valid observation with df.ffill() (or the legacy df.fillna(method='ffill')), while backward-fill carries the next valid value backward using df.bfill().
Infinite values produced by division-by-zero or numerical overflow should be treated as a kind of missing data. df.replace([np.inf, -np.inf], np.nan) converts them to NaN so they can be handled by the same dropna/fillna pipeline. Equality between two frames can be checked with df1.equals(df2), which returns a single boolean, or element by element with df1 == df2, which returns a boolean DataFrame. Whenever you need an independent copy, df.copy() creates a deep copy; assigning slices without copying can produce a SettingWithCopyWarning when a later write may or may not affect the original frame.