chore: add autoskills
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
---
|
||||
name: accessibility
|
||||
description: Audit and improve web accessibility following WCAG 2.2 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible".
|
||||
license: MIT
|
||||
metadata:
|
||||
author: web-quality-skills
|
||||
version: "1.1"
|
||||
---
|
||||
|
||||
# Accessibility (a11y)
|
||||
|
||||
Comprehensive accessibility guidelines based on WCAG 2.2 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities.
|
||||
|
||||
## WCAG Principles: POUR
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| **P**erceivable | Content can be perceived through different senses |
|
||||
| **O**perable | Interface can be operated by all users |
|
||||
| **U**nderstandable | Content and interface are understandable |
|
||||
| **R**obust | Content works with assistive technologies |
|
||||
|
||||
## Conformance levels
|
||||
|
||||
| Level | Requirement | Target |
|
||||
|-------|-------------|--------|
|
||||
| **A** | Minimum accessibility | Must pass |
|
||||
| **AA** | Standard compliance | Should pass (legal requirement in many jurisdictions) |
|
||||
| **AAA** | Enhanced accessibility | Nice to have |
|
||||
|
||||
---
|
||||
|
||||
## Perceivable
|
||||
|
||||
### Text alternatives (1.1)
|
||||
|
||||
**Images require alt text:**
|
||||
```html
|
||||
<!-- ❌ Missing alt -->
|
||||
<img src="chart.png">
|
||||
|
||||
<!-- ✅ Descriptive alt -->
|
||||
<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales">
|
||||
|
||||
<!-- ✅ Decorative image (empty alt) -->
|
||||
<img src="decorative-border.png" alt="" role="presentation">
|
||||
|
||||
<!-- ✅ Complex image with longer description -->
|
||||
<figure>
|
||||
<img src="infographic.png" alt="2024 market trends infographic"
|
||||
aria-describedby="infographic-desc">
|
||||
<figcaption id="infographic-desc">
|
||||
<!-- Detailed description -->
|
||||
</figcaption>
|
||||
</figure>
|
||||
```
|
||||
|
||||
**Icon buttons need accessible names:**
|
||||
```html
|
||||
<!-- ❌ No accessible name -->
|
||||
<button><svg><!-- menu icon --></svg></button>
|
||||
|
||||
<!-- ✅ Using aria-label -->
|
||||
<button aria-label="Open menu">
|
||||
<svg aria-hidden="true"><!-- menu icon --></svg>
|
||||
</button>
|
||||
|
||||
<!-- ✅ Using visually hidden text -->
|
||||
<button>
|
||||
<svg aria-hidden="true"><!-- menu icon --></svg>
|
||||
<span class="visually-hidden">Open menu</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
**Visually hidden class:**
|
||||
```css
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Color contrast (1.4.3, 1.4.6)
|
||||
|
||||
| Text Size | AA minimum | AAA enhanced |
|
||||
|-----------|------------|--------------|
|
||||
| Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 |
|
||||
| Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 |
|
||||
| UI components & graphics | 3:1 | 3:1 |
|
||||
|
||||
```css
|
||||
/* ❌ Low contrast (2.5:1) */
|
||||
.low-contrast {
|
||||
color: #999;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ✅ Sufficient contrast (7:1) */
|
||||
.high-contrast {
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ✅ Focus states need contrast too */
|
||||
:focus-visible {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
**Don't rely on color alone:**
|
||||
```html
|
||||
<!-- ❌ Only color indicates error -->
|
||||
<input class="error-border">
|
||||
<style>.error-border { border-color: red; }</style>
|
||||
|
||||
<!-- ✅ Color + icon + text -->
|
||||
<div class="field-error">
|
||||
<input aria-invalid="true" aria-describedby="email-error">
|
||||
<span id="email-error" class="error-message">
|
||||
<svg aria-hidden="true"><!-- error icon --></svg>
|
||||
Please enter a valid email address
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Media alternatives (1.2)
|
||||
|
||||
```html
|
||||
<!-- Video with captions -->
|
||||
<video controls>
|
||||
<source src="video.mp4" type="video/mp4">
|
||||
<track kind="captions" src="captions.vtt" srclang="en" label="English" default>
|
||||
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions">
|
||||
</video>
|
||||
|
||||
<!-- Audio with transcript -->
|
||||
<audio controls>
|
||||
<source src="podcast.mp3" type="audio/mp3">
|
||||
</audio>
|
||||
<details>
|
||||
<summary>Transcript</summary>
|
||||
<p>Full transcript text...</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Operable
|
||||
|
||||
### Keyboard accessible (2.1)
|
||||
|
||||
**All functionality must be keyboard accessible:**
|
||||
```javascript
|
||||
// ❌ Only handles click
|
||||
element.addEventListener('click', handleAction);
|
||||
|
||||
// ✅ Handles both click and keyboard
|
||||
element.addEventListener('click', handleAction);
|
||||
element.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleAction();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**No keyboard traps.** Users must be able to Tab into and out of every component. Use the [modal focus trap pattern](references/A11Y-PATTERNS.md#modal-focus-trap) for dialogs—the native `<dialog>` element handles this automatically.
|
||||
|
||||
### Focus visible (2.4.7)
|
||||
|
||||
```css
|
||||
/* ❌ Never remove focus outlines */
|
||||
*:focus { outline: none; }
|
||||
|
||||
/* ✅ Use :focus-visible for keyboard-only focus */
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ✅ Or custom focus styles */
|
||||
button:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
### Focus not obscured (2.4.11) — new in 2.2
|
||||
|
||||
When an element receives keyboard focus, it must not be entirely hidden by other author-created content such as sticky headers, footers, or overlapping panels. At Level AAA (2.4.12), no part of the focused element may be hidden.
|
||||
|
||||
```css
|
||||
/* ✅ Account for sticky headers when scrolling to focused elements */
|
||||
:target {
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
/* ✅ Ensure focused items clear fixed/sticky bars */
|
||||
:focus {
|
||||
scroll-margin-top: 80px;
|
||||
scroll-margin-bottom: 60px;
|
||||
}
|
||||
```
|
||||
|
||||
### Skip links (2.4.1)
|
||||
|
||||
Provide a skip link so keyboard users can bypass repetitive navigation. See the [skip link pattern](references/A11Y-PATTERNS.md#skip-link) for full markup and styles.
|
||||
|
||||
### Target size (2.5.8) — new in 2.2
|
||||
|
||||
Interactive targets must be at least **24 × 24 CSS pixels** (AA). Exceptions: inline text links, elements where the browser controls the size, and targets where a 24px circle centered on the bounding box does not overlap another target.
|
||||
|
||||
```css
|
||||
/* ✅ Minimum target size */
|
||||
button,
|
||||
[role="button"],
|
||||
input[type="checkbox"] + label,
|
||||
input[type="radio"] + label {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
/* ✅ Comfortable target size (recommended 44×44) */
|
||||
.touch-target {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
```
|
||||
|
||||
### Dragging movements (2.5.7) — new in 2.2
|
||||
|
||||
Any action that requires dragging must have a single-pointer alternative (e.g., buttons, inputs). See the [dragging movements pattern](references/A11Y-PATTERNS.md#dragging-movements) for a sortable-list example.
|
||||
|
||||
### Timing (2.2)
|
||||
|
||||
```javascript
|
||||
// Allow users to extend time limits
|
||||
function showSessionWarning() {
|
||||
const modal = createModal({
|
||||
title: 'Session Expiring',
|
||||
content: 'Your session will expire in 2 minutes.',
|
||||
actions: [
|
||||
{ label: 'Extend session', action: extendSession },
|
||||
{ label: 'Log out', action: logout }
|
||||
],
|
||||
timeout: 120000
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Motion (2.3)
|
||||
|
||||
```css
|
||||
/* Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Understandable
|
||||
|
||||
### Page language (3.1.1)
|
||||
|
||||
```html
|
||||
<!-- ❌ No language specified -->
|
||||
<html>
|
||||
|
||||
<!-- ✅ Language specified -->
|
||||
<html lang="en">
|
||||
|
||||
<!-- ✅ Language changes within page -->
|
||||
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>
|
||||
```
|
||||
|
||||
### Consistent navigation (3.2.3)
|
||||
|
||||
```html
|
||||
<!-- Navigation should be consistent across pages -->
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
<li><a href="/" aria-current="page">Home</a></li>
|
||||
<li><a href="/products">Products</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Consistent help (3.2.6) — new in 2.2
|
||||
|
||||
If a help mechanism (contact info, chat widget, FAQ link, self-help option) is repeated across multiple pages, it must appear in the **same relative order** each time. Users who rely on consistent placement shouldn't have to hunt for help on every page.
|
||||
|
||||
### Form labels (3.3.2)
|
||||
|
||||
Every input needs a programmatically associated label. See the [form labels pattern](references/A11Y-PATTERNS.md#form-labels) for explicit, implicit, and instructional examples.
|
||||
|
||||
### Error handling (3.3.1, 3.3.3)
|
||||
|
||||
Announce errors to screen readers with `role="alert"` or `aria-live`, set `aria-invalid="true"` on invalid fields, and focus the first error on submit. See the [error handling pattern](references/A11Y-PATTERNS.md#error-handling) for full markup and JS.
|
||||
|
||||
### Redundant entry (3.3.7) — new in 2.2
|
||||
|
||||
Don't force users to re-enter information they already provided in the same session. Auto-populate from earlier steps, or let users select from previously entered values. Exceptions: security re-confirmation and content that has expired.
|
||||
|
||||
```html
|
||||
<!-- ✅ Auto-fill shipping address from billing -->
|
||||
<fieldset>
|
||||
<legend>Shipping address</legend>
|
||||
<label>
|
||||
<input type="checkbox" id="same-as-billing" checked>
|
||||
Same as billing address
|
||||
</label>
|
||||
<!-- Fields auto-populated when checked -->
|
||||
</fieldset>
|
||||
```
|
||||
|
||||
### Accessible authentication (3.3.8) — new in 2.2
|
||||
|
||||
Login flows must not rely on cognitive function tests (e.g., remembering a password, solving a puzzle) unless at least one of:
|
||||
- A copy-paste or autofill mechanism is available
|
||||
- An alternative method exists (e.g., passkey, SSO, email link)
|
||||
- The test uses object recognition or personal content (AA only; AAA removes this exception)
|
||||
|
||||
```html
|
||||
<!-- ✅ Allow paste in password fields -->
|
||||
<input type="password" id="password" autocomplete="current-password">
|
||||
|
||||
<!-- ✅ Offer passwordless alternatives -->
|
||||
<button type="button">Sign in with passkey</button>
|
||||
<button type="button">Email me a login link</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Robust
|
||||
|
||||
### ARIA usage (4.1.2)
|
||||
|
||||
**Prefer native elements:**
|
||||
```html
|
||||
<!-- ❌ ARIA role on div -->
|
||||
<div role="button" tabindex="0">Click me</div>
|
||||
|
||||
<!-- ✅ Native button -->
|
||||
<button>Click me</button>
|
||||
|
||||
<!-- ❌ ARIA checkbox -->
|
||||
<div role="checkbox" aria-checked="false">Option</div>
|
||||
|
||||
<!-- ✅ Native checkbox -->
|
||||
<label><input type="checkbox"> Option</label>
|
||||
```
|
||||
|
||||
**When ARIA is needed,** use the correct roles and states. See the [ARIA tabs pattern](references/A11Y-PATTERNS.md#aria-tabs) for a complete tablist example.
|
||||
|
||||
### Live regions (4.1.3)
|
||||
|
||||
Use `aria-live` regions to announce dynamic content changes without moving focus. See the [live regions pattern](references/A11Y-PATTERNS.md#live-regions-and-notifications) for markup and a `showNotification()` helper.
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
### Automated testing
|
||||
```bash
|
||||
# Lighthouse accessibility audit
|
||||
npx lighthouse https://example.com --only-categories=accessibility
|
||||
|
||||
# axe-core
|
||||
npm install @axe-core/cli -g
|
||||
axe https://example.com
|
||||
```
|
||||
|
||||
### Manual testing
|
||||
|
||||
- [ ] **Keyboard navigation:** Tab through entire page, use Enter/Space to activate
|
||||
- [ ] **Screen reader:** Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android)
|
||||
- [ ] **Zoom:** Content usable at 200% zoom
|
||||
- [ ] **High contrast:** Test with Windows High Contrast Mode
|
||||
- [ ] **Reduced motion:** Test with `prefers-reduced-motion: reduce`
|
||||
- [ ] **Focus order:** Logical and follows visual order
|
||||
- [ ] **Target size:** Interactive elements meet 24×24px minimum
|
||||
|
||||
See the [screen reader commands reference](references/A11Y-PATTERNS.md#screen-reader-commands) for VoiceOver and NVDA shortcuts.
|
||||
|
||||
---
|
||||
|
||||
## Common issues by impact
|
||||
|
||||
### Critical (fix immediately)
|
||||
1. Missing form labels
|
||||
2. Missing image alt text
|
||||
3. Insufficient color contrast
|
||||
4. Keyboard traps
|
||||
5. No focus indicators
|
||||
|
||||
### Serious (fix before launch)
|
||||
1. Missing page language
|
||||
2. Missing heading structure
|
||||
3. Non-descriptive link text
|
||||
4. Auto-playing media
|
||||
5. Missing skip links
|
||||
|
||||
### Moderate (fix soon)
|
||||
1. Missing ARIA labels on icons
|
||||
2. Inconsistent navigation
|
||||
3. Missing error identification
|
||||
4. Timing without controls
|
||||
5. Missing landmark regions
|
||||
|
||||
## References
|
||||
|
||||
- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/)
|
||||
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
||||
- [Deque axe Rules](https://dequeuniversity.com/rules/axe/)
|
||||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||
- [WCAG criteria reference](references/WCAG.md)
|
||||
- [Accessibility code patterns](references/A11Y-PATTERNS.md)
|
||||
@@ -0,0 +1,233 @@
|
||||
# Accessibility Code Patterns
|
||||
|
||||
Practical, copy-paste-ready patterns for common accessibility requirements. Each pattern is self-contained and linked from the main [SKILL.md](../SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Modal focus trap
|
||||
|
||||
Trap keyboard focus inside a modal dialog so Tab/Shift+Tab cycle through its focusable elements and Escape closes it.
|
||||
|
||||
```javascript
|
||||
function openModal(modal) {
|
||||
const focusableElements = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
modal.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
if (e.shiftKey && document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
firstElement.focus();
|
||||
}
|
||||
```
|
||||
|
||||
The native `<dialog>` element handles focus trapping automatically—prefer it when browser support allows.
|
||||
|
||||
---
|
||||
|
||||
## Skip link
|
||||
|
||||
Allows keyboard users to bypass repetitive navigation and jump straight to main content.
|
||||
|
||||
```html
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
<header><!-- navigation --></header>
|
||||
<main id="main-content" tabindex="-1">
|
||||
<!-- main content -->
|
||||
</main>
|
||||
</body>
|
||||
```
|
||||
|
||||
```css
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
Announce errors to screen readers and focus the first invalid field on submit.
|
||||
|
||||
```html
|
||||
<form novalidate>
|
||||
<div class="field" aria-live="polite">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email"
|
||||
aria-invalid="true"
|
||||
aria-describedby="email-error">
|
||||
<p id="email-error" class="error" role="alert">
|
||||
Please enter a valid email address (e.g., name@example.com)
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
```javascript
|
||||
form.addEventListener('submit', (e) => {
|
||||
const firstError = form.querySelector('[aria-invalid="true"]');
|
||||
if (firstError) {
|
||||
e.preventDefault();
|
||||
firstError.focus();
|
||||
|
||||
const errorSummary = document.getElementById('error-summary');
|
||||
errorSummary.textContent =
|
||||
`${errors.length} errors found. Please fix them and try again.`;
|
||||
errorSummary.focus();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Form labels
|
||||
|
||||
Every input needs an associated label—either explicit (`for`/`id`) or implicit (wrapping `<label>`).
|
||||
|
||||
```html
|
||||
<!-- ❌ No label association -->
|
||||
<input type="email" placeholder="Email">
|
||||
|
||||
<!-- ✅ Explicit label -->
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email"
|
||||
autocomplete="email" required>
|
||||
|
||||
<!-- ✅ Implicit label -->
|
||||
<label>
|
||||
Email address
|
||||
<input type="email" name="email" autocomplete="email" required>
|
||||
</label>
|
||||
|
||||
<!-- ✅ With instructions -->
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password"
|
||||
aria-describedby="password-requirements">
|
||||
<p id="password-requirements">
|
||||
Must be at least 8 characters with one number.
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dragging movements
|
||||
|
||||
Any action triggered by dragging must offer a single-pointer alternative (WCAG 2.5.7).
|
||||
|
||||
```html
|
||||
<!-- ❌ Drag-only reorder -->
|
||||
<ul class="sortable-list" draggable="true">
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</ul>
|
||||
|
||||
<!-- ✅ Drag + button alternatives -->
|
||||
<ul class="sortable-list">
|
||||
<li>
|
||||
<span>Item 1</span>
|
||||
<button aria-label="Move Item 1 up">↑</button>
|
||||
<button aria-label="Move Item 1 down">↓</button>
|
||||
</li>
|
||||
<li>
|
||||
<span>Item 2</span>
|
||||
<button aria-label="Move Item 2 up">↑</button>
|
||||
<button aria-label="Move Item 2 down">↓</button>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Also applies to sliders, map panning, colour pickers, and similar drag-based widgets—always provide an equivalent click/tap or keyboard path.
|
||||
|
||||
---
|
||||
|
||||
## ARIA tabs
|
||||
|
||||
Tabs require `role="tablist"`, `role="tab"`, and `role="tabpanel"` with proper `aria-selected`, `aria-controls`, and keyboard support.
|
||||
|
||||
```html
|
||||
<div role="tablist" aria-label="Product information">
|
||||
<button role="tab" id="tab-1" aria-selected="true"
|
||||
aria-controls="panel-1">Description</button>
|
||||
<button role="tab" id="tab-2" aria-selected="false"
|
||||
aria-controls="panel-2" tabindex="-1">Reviews</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
|
||||
<!-- Panel content -->
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
|
||||
<!-- Panel content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
Arrow keys should move focus between tabs; the active tab receives `tabindex="0"` while inactive tabs use `tabindex="-1"`.
|
||||
|
||||
---
|
||||
|
||||
## Live regions and notifications
|
||||
|
||||
Use `aria-live` to announce dynamic content changes to screen readers without moving focus.
|
||||
|
||||
```html
|
||||
<!-- Status updates (polite — waits for pause in speech) -->
|
||||
<div aria-live="polite" aria-atomic="true" class="status">
|
||||
<!-- Content updates announced to screen readers -->
|
||||
</div>
|
||||
|
||||
<!-- Urgent alerts (assertive — interrupts) -->
|
||||
<div role="alert" aria-live="assertive">
|
||||
<!-- Interrupts current announcement -->
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
function showNotification(message, type = 'polite') {
|
||||
const container = document.getElementById(`${type}-announcer`);
|
||||
container.textContent = '';
|
||||
requestAnimationFrame(() => {
|
||||
container.textContent = message;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Clear the container before writing to ensure the same message triggers a new announcement.
|
||||
|
||||
---
|
||||
|
||||
## Screen reader commands
|
||||
|
||||
Quick reference for the most common screen reader shortcuts.
|
||||
|
||||
| Action | VoiceOver (Mac) | NVDA (Windows) |
|
||||
|--------|-----------------|----------------|
|
||||
| Start/Stop | ⌘ + F5 | Ctrl + Alt + N |
|
||||
| Next item | VO + → | ↓ |
|
||||
| Previous item | VO + ← | ↑ |
|
||||
| Activate | VO + Space | Enter |
|
||||
| Headings list | VO + U, then arrows | H / Shift + H |
|
||||
| Links list | VO + U | K / Shift + K |
|
||||
@@ -0,0 +1,191 @@
|
||||
# WCAG 2.2 Quick Reference
|
||||
|
||||
## Success criteria by level
|
||||
|
||||
### Level A (minimum)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.1.1** Non-text Content | All images, icons have text alternatives |
|
||||
| **1.2.1** Audio-only/Video-only | Provide transcript or audio description |
|
||||
| **1.2.2** Captions | Video with audio has captions |
|
||||
| **1.2.3** Audio Description | Video has audio description |
|
||||
| **1.3.1** Info and Relationships | Information conveyed through presentation is available programmatically |
|
||||
| **1.3.2** Meaningful Sequence | Reading order is logical |
|
||||
| **1.3.3** Sensory Characteristics | Instructions don't rely solely on shape, color, size, location, orientation, or sound |
|
||||
| **1.4.1** Use of Color | Color is not the only visual means of conveying information |
|
||||
| **1.4.2** Audio Control | Audio playing automatically can be paused/stopped |
|
||||
| **2.1.1** Keyboard | All functionality available via keyboard |
|
||||
| **2.1.2** No Keyboard Trap | Keyboard focus can be moved away from any component |
|
||||
| **2.1.4** Character Key Shortcuts | Single-key shortcuts can be turned off or remapped |
|
||||
| **2.2.1** Timing Adjustable | Time limits can be extended |
|
||||
| **2.2.2** Pause, Stop, Hide | Moving/blinking content can be paused |
|
||||
| **2.3.1** Three Flashes | Nothing flashes more than 3 times per second |
|
||||
| **2.4.1** Bypass Blocks | Skip link or landmark navigation available |
|
||||
| **2.4.2** Page Titled | Pages have descriptive titles |
|
||||
| **2.4.3** Focus Order | Focus order preserves meaning |
|
||||
| **2.4.4** Link Purpose | Link purpose clear from link text or context |
|
||||
| **2.5.1** Pointer Gestures | Multi-point gestures have single-pointer alternatives |
|
||||
| **2.5.2** Pointer Cancellation | Down-event doesn't trigger action (use up-event or click) |
|
||||
| **2.5.3** Label in Name | Accessible name contains visible label text |
|
||||
| **2.5.4** Motion Actuation | Motion-triggered functions have alternatives |
|
||||
| **3.1.1** Language of Page | Default language specified in HTML |
|
||||
| **3.2.1** On Focus | Focus doesn't trigger unexpected changes |
|
||||
| **3.2.2** On Input | Input doesn't trigger unexpected changes |
|
||||
| **3.2.6** Consistent Help | Help mechanisms appear in the same relative order across pages |
|
||||
| **3.3.1** Error Identification | Input errors clearly described |
|
||||
| **3.3.2** Labels or Instructions | Form inputs have labels or instructions |
|
||||
| **3.3.7** Redundant Entry | Information previously entered is auto-populated or available to select |
|
||||
| **4.1.2** Name, Role, Value | UI components have accessible names and correct roles |
|
||||
|
||||
### Level AA (standard)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.2.4** Captions (Live) | Live audio has captions |
|
||||
| **1.2.5** Audio Description | Pre-recorded video has audio description |
|
||||
| **1.3.4** Orientation | Content doesn't restrict orientation |
|
||||
| **1.3.5** Identify Input Purpose | Input purpose can be programmatically determined |
|
||||
| **1.4.3** Contrast (Minimum) | 4.5:1 for normal text, 3:1 for large text |
|
||||
| **1.4.4** Resize Text | Text can be resized to 200% without loss of functionality |
|
||||
| **1.4.5** Images of Text | Text used instead of images of text |
|
||||
| **1.4.10** Reflow | Content reflows at 320px width without horizontal scroll |
|
||||
| **1.4.11** Non-text Contrast | UI components have 3:1 contrast |
|
||||
| **1.4.12** Text Spacing | Content adapts to text spacing changes |
|
||||
| **1.4.13** Content on Hover/Focus | Additional content is dismissible, hoverable, persistent |
|
||||
| **2.4.5** Multiple Ways | Multiple ways to find pages |
|
||||
| **2.4.6** Headings and Labels | Headings and labels are descriptive |
|
||||
| **2.4.7** Focus Visible | Focus indicator is visible |
|
||||
| **2.4.11** Focus Not Obscured (Minimum) | Focused element is not entirely hidden by author-created content |
|
||||
| **2.5.7** Dragging Movements | Dragging actions have single-pointer alternatives |
|
||||
| **2.5.8** Target Size (Minimum) | Interactive targets are at least 24×24 CSS pixels (with exceptions) |
|
||||
| **3.1.2** Language of Parts | Language changes are marked |
|
||||
| **3.2.3** Consistent Navigation | Navigation is consistent across pages |
|
||||
| **3.2.4** Consistent Identification | Same functionality uses same labels |
|
||||
| **3.3.3** Error Suggestion | Error corrections suggested when known |
|
||||
| **3.3.4** Error Prevention (Legal) | Actions can be reversed or confirmed |
|
||||
| **3.3.8** Accessible Authentication (Minimum) | No cognitive function test for login unless an alternative or assistance is provided |
|
||||
| **4.1.3** Status Messages | Status messages announced to screen readers |
|
||||
|
||||
### Level AAA (enhanced)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.4.6** Contrast (Enhanced) | 7:1 for normal text, 4.5:1 for large text |
|
||||
| **1.4.8** Visual Presentation | Foreground/background colors can be selected |
|
||||
| **1.4.9** Images of Text (No Exception) | No images of text |
|
||||
| **2.1.3** Keyboard (No Exception) | All functionality keyboard accessible |
|
||||
| **2.2.3** No Timing | No time limits |
|
||||
| **2.2.4** Interruptions | Interruptions can be postponed |
|
||||
| **2.2.5** Re-authenticating | Data preserved on re-authentication |
|
||||
| **2.2.6** Timeouts | Users warned about data loss from inactivity |
|
||||
| **2.3.2** Three Flashes | No content flashes more than 3 times |
|
||||
| **2.3.3** Animation from Interactions | Motion animation can be disabled |
|
||||
| **2.4.8** Location | User location within site is available |
|
||||
| **2.4.9** Link Purpose (Link Only) | Link purpose clear from link text alone |
|
||||
| **2.4.10** Section Headings | Sections have headings |
|
||||
| **2.4.12** Focus Not Obscured (Enhanced) | No part of the focused element is hidden by author-created content |
|
||||
| **2.4.13** Focus Appearance | Focus indicator has sufficient area, contrast, and is not obscured |
|
||||
| **3.1.3** Unusual Words | Definitions available for unusual words |
|
||||
| **3.1.4** Abbreviations | Abbreviations expanded |
|
||||
| **3.1.5** Reading Level | Alternative content for complex text |
|
||||
| **3.1.6** Pronunciation | Pronunciation available where needed |
|
||||
| **3.2.5** Change on Request | Changes initiated only by user |
|
||||
| **3.3.5** Help | Context-sensitive help available |
|
||||
| **3.3.6** Error Prevention (All) | All form submissions can be reviewed |
|
||||
| **3.3.9** Accessible Authentication (Enhanced) | No cognitive function test for login (no object or personal content recognition exceptions) |
|
||||
|
||||
## Common ARIA patterns
|
||||
|
||||
### Buttons
|
||||
```html
|
||||
<button>Label</button>
|
||||
<!-- or -->
|
||||
<button aria-label="Close dialog">×</button>
|
||||
```
|
||||
|
||||
### Links
|
||||
```html
|
||||
<a href="/page">Descriptive link text</a>
|
||||
<!-- External links -->
|
||||
<a href="https://external.com" target="_blank" rel="noopener">
|
||||
External site
|
||||
<span class="visually-hidden">(opens in new tab)</span>
|
||||
</a>
|
||||
```
|
||||
|
||||
### Form fields
|
||||
```html
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" aria-describedby="email-hint">
|
||||
<p id="email-hint">We'll never share your email.</p>
|
||||
```
|
||||
|
||||
### Error states
|
||||
```html
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
|
||||
<p id="email-error" role="alert">Please enter a valid email address.</p>
|
||||
```
|
||||
|
||||
### Navigation
|
||||
```html
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
<li><a href="/" aria-current="page">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Modals
|
||||
```html
|
||||
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
||||
<h2 id="dialog-title">Confirm Action</h2>
|
||||
<!-- content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Live regions
|
||||
```html
|
||||
<!-- Polite (waits for pause in speech) -->
|
||||
<div aria-live="polite">Status update here</div>
|
||||
|
||||
<!-- Assertive (interrupts immediately) -->
|
||||
<div aria-live="assertive" role="alert">Error message here</div>
|
||||
|
||||
<!-- Status (polite, implicit) -->
|
||||
<div role="status">Loading complete</div>
|
||||
```
|
||||
|
||||
## What changed from 2.1 to 2.2
|
||||
|
||||
| Change | Criterion | Level |
|
||||
|--------|-----------|-------|
|
||||
| **Removed** | 4.1.1 Parsing | A |
|
||||
| **Added** | 2.4.11 Focus Not Obscured (Minimum) | AA |
|
||||
| **Added** | 2.4.12 Focus Not Obscured (Enhanced) | AAA |
|
||||
| **Added** | 2.4.13 Focus Appearance | AAA |
|
||||
| **Added** | 2.5.7 Dragging Movements | AA |
|
||||
| **Added** | 2.5.8 Target Size (Minimum) | AA |
|
||||
| **Added** | 3.2.6 Consistent Help | A |
|
||||
| **Added** | 3.3.7 Redundant Entry | A |
|
||||
| **Added** | 3.3.8 Accessible Authentication (Minimum) | AA |
|
||||
| **Added** | 3.3.9 Accessible Authentication (Enhanced) | AAA |
|
||||
|
||||
## Testing tools
|
||||
|
||||
| Tool | Type | URL |
|
||||
|------|------|-----|
|
||||
| axe DevTools | Browser extension | [deque.com/axe](https://www.deque.com/axe/) |
|
||||
| WAVE | Browser extension | [wave.webaim.org](https://wave.webaim.org/) |
|
||||
| Lighthouse | Built into Chrome | DevTools → Lighthouse |
|
||||
| NVDA | Screen reader (Windows) | [nvaccess.org](https://www.nvaccess.org/) |
|
||||
| VoiceOver | Screen reader (Mac) | Built into macOS |
|
||||
| Colour Contrast Analyser | Desktop app | [tpgi.com](https://www.tpgi.com/color-contrast-checker/) |
|
||||
|
||||
## Sources
|
||||
|
||||
- [WCAG 2.2 W3C Recommendation](https://www.w3.org/TR/WCAG22/)
|
||||
- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/)
|
||||
- [What's New in WCAG 2.2](https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/)
|
||||
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
@@ -0,0 +1,279 @@
|
||||
---
|
||||
name: Pandas Data Analysis
|
||||
description: Master data manipulation, analysis, and visualization with Pandas, NumPy, and Matplotlib
|
||||
version: "2.1.0"
|
||||
sasmp_version: "1.3.0"
|
||||
bonded_agent: 03-data-science
|
||||
bond_type: PRIMARY_BOND
|
||||
|
||||
# Skill Configuration
|
||||
retry_strategy: exponential_backoff
|
||||
observability:
|
||||
logging: true
|
||||
metrics: data_processing_time
|
||||
---
|
||||
|
||||
# Pandas Data Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
Master data analysis with Pandas, the powerful Python library for data manipulation and analysis. Learn to clean, transform, analyze, and visualize data effectively.
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
- Load and manipulate data from various sources (CSV, Excel, SQL, APIs)
|
||||
- Clean and transform messy datasets
|
||||
- Perform exploratory data analysis (EDA)
|
||||
- Aggregate and group data for insights
|
||||
- Create compelling visualizations
|
||||
- Optimize performance for large datasets
|
||||
|
||||
## Core Topics
|
||||
|
||||
### 1. Pandas DataFrames & Series
|
||||
- Creating DataFrames from various sources
|
||||
- Indexing and selecting data (loc, iloc, at, iat)
|
||||
- Filtering and boolean indexing
|
||||
- Adding/removing columns and rows
|
||||
- Data types and conversions
|
||||
|
||||
**Code Example:**
|
||||
```python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
# Create DataFrame
|
||||
data = {
|
||||
'name': ['Alice', 'Bob', 'Charlie', 'David'],
|
||||
'age': [25, 30, 35, 28],
|
||||
'salary': [50000, 60000, 75000, 55000],
|
||||
'department': ['IT', 'HR', 'IT', 'Sales']
|
||||
}
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Indexing and filtering
|
||||
it_employees = df[df['department'] == 'IT']
|
||||
high_earners = df.loc[df['salary'] > 55000, ['name', 'salary']]
|
||||
|
||||
# Adding calculated columns
|
||||
df['annual_bonus'] = df['salary'] * 0.10
|
||||
df['age_group'] = pd.cut(df['age'], bins=[0, 30, 40, 100], labels=['Young', 'Mid', 'Senior'])
|
||||
|
||||
print(df)
|
||||
```
|
||||
|
||||
### 2. Data Cleaning & Transformation
|
||||
- Handling missing data (dropna, fillna, interpolate)
|
||||
- Removing duplicates
|
||||
- String operations and text cleaning
|
||||
- Date/time parsing and manipulation
|
||||
- Type conversions and casting
|
||||
- Applying custom functions (apply, map, applymap)
|
||||
|
||||
**Code Example:**
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Load data with missing values
|
||||
df = pd.read_csv('sales_data.csv')
|
||||
|
||||
# Handle missing values
|
||||
df['price'].fillna(df['price'].median(), inplace=True)
|
||||
df['category'].fillna('Unknown', inplace=True)
|
||||
df.dropna(subset=['customer_id'], inplace=True)
|
||||
|
||||
# Clean text data
|
||||
df['product_name'] = df['product_name'].str.strip().str.lower()
|
||||
df['product_name'] = df['product_name'].str.replace('[^a-zA-Z0-9 ]', '', regex=True)
|
||||
|
||||
# Convert dates
|
||||
df['order_date'] = pd.to_datetime(df['order_date'])
|
||||
df['year'] = df['order_date'].dt.year
|
||||
df['month'] = df['order_date'].dt.month
|
||||
|
||||
# Remove duplicates
|
||||
df.drop_duplicates(subset=['order_id'], keep='first', inplace=True)
|
||||
|
||||
# Apply custom function
|
||||
def categorize_price(price):
|
||||
if price < 50:
|
||||
return 'Low'
|
||||
elif price < 100:
|
||||
return 'Medium'
|
||||
else:
|
||||
return 'High'
|
||||
|
||||
df['price_category'] = df['price'].apply(categorize_price)
|
||||
```
|
||||
|
||||
### 3. Aggregation & Grouping
|
||||
- GroupBy operations
|
||||
- Aggregation functions (sum, mean, count, etc.)
|
||||
- Pivot tables and cross-tabulation
|
||||
- Multi-level indexing
|
||||
- Window functions (rolling, expanding)
|
||||
|
||||
**Code Example:**
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Sample sales data
|
||||
df = pd.read_csv('sales.csv')
|
||||
|
||||
# GroupBy aggregation
|
||||
dept_stats = df.groupby('department').agg({
|
||||
'salary': ['mean', 'min', 'max'],
|
||||
'employee_id': 'count'
|
||||
})
|
||||
|
||||
# Multiple groupby
|
||||
sales_by_region_product = df.groupby(['region', 'product_category'])['sales'].sum()
|
||||
|
||||
# Pivot table
|
||||
pivot = df.pivot_table(
|
||||
values='sales',
|
||||
index='product_category',
|
||||
columns='quarter',
|
||||
aggfunc='sum',
|
||||
fill_value=0
|
||||
)
|
||||
|
||||
# Rolling window (moving average)
|
||||
df['sales_ma_7d'] = df.groupby('product_id')['sales'].transform(
|
||||
lambda x: x.rolling(window=7, min_periods=1).mean()
|
||||
)
|
||||
|
||||
# Cumulative sum
|
||||
df['cumulative_sales'] = df.groupby('product_id')['sales'].cumsum()
|
||||
```
|
||||
|
||||
### 4. Data Visualization
|
||||
- Matplotlib basics
|
||||
- Seaborn for statistical plots
|
||||
- Pandas built-in plotting
|
||||
- Customizing plots
|
||||
- Creating dashboards
|
||||
|
||||
**Code Example:**
|
||||
```python
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
|
||||
# Set style
|
||||
sns.set_style('whitegrid')
|
||||
|
||||
# Load data
|
||||
df = pd.read_csv('sales_data.csv')
|
||||
|
||||
# 1. Line plot - Sales trend over time
|
||||
df.groupby('month')['sales'].sum().plot(kind='line', figsize=(10, 6))
|
||||
plt.title('Monthly Sales Trend')
|
||||
plt.xlabel('Month')
|
||||
plt.ylabel('Total Sales ($)')
|
||||
plt.show()
|
||||
|
||||
# 2. Bar plot - Sales by category
|
||||
category_sales = df.groupby('category')['sales'].sum().sort_values(ascending=False)
|
||||
category_sales.plot(kind='bar', figsize=(10, 6))
|
||||
plt.title('Sales by Category')
|
||||
plt.xlabel('Category')
|
||||
plt.ylabel('Total Sales ($)')
|
||||
plt.xticks(rotation=45)
|
||||
plt.show()
|
||||
|
||||
# 3. Histogram - Price distribution
|
||||
df['price'].hist(bins=30, figsize=(10, 6))
|
||||
plt.title('Price Distribution')
|
||||
plt.xlabel('Price ($)')
|
||||
plt.ylabel('Frequency')
|
||||
plt.show()
|
||||
|
||||
# 4. Box plot - Salary by department
|
||||
df.boxplot(column='salary', by='department', figsize=(10, 6))
|
||||
plt.title('Salary Distribution by Department')
|
||||
plt.suptitle('')
|
||||
plt.show()
|
||||
|
||||
# 5. Heatmap - Correlation matrix
|
||||
corr = df[['age', 'salary', 'years_experience']].corr()
|
||||
sns.heatmap(corr, annot=True, cmap='coolwarm', center=0)
|
||||
plt.title('Correlation Matrix')
|
||||
plt.show()
|
||||
```
|
||||
|
||||
## Hands-On Practice
|
||||
|
||||
### Project 1: Customer Analytics
|
||||
Analyze customer purchase behavior and segmentation.
|
||||
|
||||
**Requirements:**
|
||||
- Load customer transaction data
|
||||
- Clean and prepare dataset
|
||||
- Calculate RFM (Recency, Frequency, Monetary) metrics
|
||||
- Customer segmentation
|
||||
- Visualize insights
|
||||
- Generate executive summary
|
||||
|
||||
**Key Skills:** Data cleaning, aggregation, visualization
|
||||
|
||||
### Project 2: Time Series Analysis
|
||||
Analyze sales trends and forecast future performance.
|
||||
|
||||
**Requirements:**
|
||||
- Load time series data
|
||||
- Handle missing dates
|
||||
- Calculate moving averages
|
||||
- Identify trends and seasonality
|
||||
- Detect anomalies
|
||||
- Create interactive visualizations
|
||||
|
||||
**Key Skills:** Time series operations, rolling windows, plotting
|
||||
|
||||
### Project 3: Data Quality Report
|
||||
Build automated data quality assessment tool.
|
||||
|
||||
**Requirements:**
|
||||
- Check for missing values
|
||||
- Identify duplicates
|
||||
- Detect outliers
|
||||
- Validate data types
|
||||
- Generate quality metrics
|
||||
- Export HTML report
|
||||
|
||||
**Key Skills:** Data validation, statistical analysis, reporting
|
||||
|
||||
## Assessment Criteria
|
||||
|
||||
- [ ] Load and clean real-world datasets efficiently
|
||||
- [ ] Perform complex data transformations
|
||||
- [ ] Use GroupBy for aggregations
|
||||
- [ ] Create insightful visualizations
|
||||
- [ ] Handle missing and inconsistent data
|
||||
- [ ] Optimize performance for large datasets
|
||||
- [ ] Document analysis with clear explanations
|
||||
|
||||
## Resources
|
||||
|
||||
### Official Documentation
|
||||
- [Pandas Docs](https://pandas.pydata.org/docs/) - Official documentation
|
||||
- [NumPy Docs](https://numpy.org/doc/) - NumPy documentation
|
||||
- [Matplotlib Docs](https://matplotlib.org/) - Plotting library
|
||||
|
||||
### Learning Platforms
|
||||
- [Kaggle](https://www.kaggle.com/learn/pandas) - Free Pandas course
|
||||
- [DataCamp](https://www.datacamp.com/courses/pandas-foundations) - Interactive courses
|
||||
- [Python for Data Analysis](https://wesmckinney.com/book/) - Wes McKinney's book
|
||||
|
||||
### Tools
|
||||
- [Jupyter Notebook](https://jupyter.org/) - Interactive development
|
||||
- [Google Colab](https://colab.research.google.com/) - Cloud notebooks
|
||||
- [Anaconda](https://www.anaconda.com/) - Data science distribution
|
||||
|
||||
## Next Steps
|
||||
|
||||
After mastering Pandas, explore:
|
||||
- **Scikit-learn** - Machine learning
|
||||
- **SQL** - Database querying
|
||||
- **Apache Spark** - Big data processing
|
||||
- **Tableau/Power BI** - Business intelligence tools
|
||||
@@ -0,0 +1 @@
|
||||
python_skill: pandas-data-analysis
|
||||
@@ -0,0 +1 @@
|
||||
# pandas-data-analysis Guide
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
print(json.dumps({"skill": "pandas-data-analysis"}, indent=2))
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
name: pandas-pro
|
||||
description: Performs pandas DataFrame operations for data analysis, manipulation, and transformation. Use when working with pandas DataFrames, data cleaning, aggregation, merging, or time series analysis. Invoke for data manipulation tasks such as joining DataFrames on multiple keys, pivoting tables, resampling time series, handling NaN values with interpolation or forward-fill, groupby aggregations, type conversion, or performance optimization of large datasets.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: https://github.com/Jeffallan
|
||||
version: "1.1.0"
|
||||
domain: data-ml
|
||||
triggers: pandas, DataFrame, data manipulation, data cleaning, aggregation, groupby, merge, join, time series, data wrangling, pivot table, data transformation
|
||||
role: expert
|
||||
scope: implementation
|
||||
output-format: code
|
||||
related-skills: python-pro
|
||||
---
|
||||
|
||||
# Pandas Pro
|
||||
|
||||
Expert pandas developer specializing in efficient data manipulation, analysis, and transformation workflows with production-grade performance patterns.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. **Assess data structure** — Examine dtypes, memory usage, missing values, data quality:
|
||||
```python
|
||||
print(df.dtypes)
|
||||
print(df.memory_usage(deep=True).sum() / 1e6, "MB")
|
||||
print(df.isna().sum())
|
||||
print(df.describe(include="all"))
|
||||
```
|
||||
2. **Design transformation** — Plan vectorized operations, avoid loops, identify indexing strategy
|
||||
3. **Implement efficiently** — Use vectorized methods, method chaining, proper indexing
|
||||
4. **Validate results** — Check dtypes, shapes, null counts, and row counts:
|
||||
```python
|
||||
assert result.shape[0] == expected_rows, f"Row count mismatch: {result.shape[0]}"
|
||||
assert result.isna().sum().sum() == 0, "Unexpected nulls after transform"
|
||||
assert set(result.columns) == expected_cols
|
||||
```
|
||||
5. **Optimize** — Profile memory, apply categorical types, use chunking if needed
|
||||
|
||||
## Reference Guide
|
||||
|
||||
Load detailed guidance based on context:
|
||||
|
||||
| Topic | Reference | Load When |
|
||||
|-------|-----------|-----------|
|
||||
| DataFrame Operations | `references/dataframe-operations.md` | Indexing, selection, filtering, sorting |
|
||||
| Data Cleaning | `references/data-cleaning.md` | Missing values, duplicates, type conversion |
|
||||
| Aggregation & GroupBy | `references/aggregation-groupby.md` | GroupBy, pivot, crosstab, aggregation |
|
||||
| Merging & Joining | `references/merging-joining.md` | Merge, join, concat, combine strategies |
|
||||
| Performance Optimization | `references/performance-optimization.md` | Memory usage, vectorization, chunking |
|
||||
|
||||
## Code Patterns
|
||||
|
||||
### Vectorized Operations (before/after)
|
||||
|
||||
```python
|
||||
# ❌ AVOID: row-by-row iteration
|
||||
for i, row in df.iterrows():
|
||||
df.at[i, 'tax'] = row['price'] * 0.2
|
||||
|
||||
# ✅ USE: vectorized assignment
|
||||
df['tax'] = df['price'] * 0.2
|
||||
```
|
||||
|
||||
### Safe Subsetting with `.copy()`
|
||||
|
||||
```python
|
||||
# ❌ AVOID: chained indexing triggers SettingWithCopyWarning
|
||||
df['A']['B'] = 1
|
||||
|
||||
# ✅ USE: .loc[] with explicit copy when mutating a subset
|
||||
subset = df.loc[df['status'] == 'active', :].copy()
|
||||
subset['score'] = subset['score'].fillna(0)
|
||||
```
|
||||
|
||||
### GroupBy Aggregation
|
||||
|
||||
```python
|
||||
summary = (
|
||||
df.groupby(['region', 'category'], observed=True)
|
||||
.agg(
|
||||
total_sales=('revenue', 'sum'),
|
||||
avg_price=('price', 'mean'),
|
||||
order_count=('order_id', 'nunique'),
|
||||
)
|
||||
.reset_index()
|
||||
)
|
||||
```
|
||||
|
||||
### Merge with Validation
|
||||
|
||||
```python
|
||||
merged = pd.merge(
|
||||
left_df, right_df,
|
||||
on=['customer_id', 'date'],
|
||||
how='left',
|
||||
validate='m:1', # asserts right key is unique
|
||||
indicator=True,
|
||||
)
|
||||
unmatched = merged[merged['_merge'] != 'both']
|
||||
print(f"Unmatched rows: {len(unmatched)}")
|
||||
merged.drop(columns=['_merge'], inplace=True)
|
||||
```
|
||||
|
||||
### Missing Value Handling
|
||||
|
||||
```python
|
||||
# Forward-fill then interpolate numeric gaps
|
||||
df['price'] = df['price'].ffill().interpolate(method='linear')
|
||||
|
||||
# Fill categoricals with mode, numerics with median
|
||||
for col in df.select_dtypes(include='object'):
|
||||
df[col] = df[col].fillna(df[col].mode()[0])
|
||||
for col in df.select_dtypes(include='number'):
|
||||
df[col] = df[col].fillna(df[col].median())
|
||||
```
|
||||
|
||||
### Time Series Resampling
|
||||
|
||||
```python
|
||||
daily = (
|
||||
df.set_index('timestamp')
|
||||
.resample('D')
|
||||
.agg({'revenue': 'sum', 'sessions': 'count'})
|
||||
.fillna(0)
|
||||
)
|
||||
```
|
||||
|
||||
### Pivot Table
|
||||
|
||||
```python
|
||||
pivot = df.pivot_table(
|
||||
values='revenue',
|
||||
index='region',
|
||||
columns='product_line',
|
||||
aggfunc='sum',
|
||||
fill_value=0,
|
||||
margins=True,
|
||||
)
|
||||
```
|
||||
|
||||
### Memory Optimization
|
||||
|
||||
```python
|
||||
# Downcast numerics and convert low-cardinality strings to categorical
|
||||
df['category'] = df['category'].astype('category')
|
||||
df['count'] = pd.to_numeric(df['count'], downcast='integer')
|
||||
df['score'] = pd.to_numeric(df['score'], downcast='float')
|
||||
print(df.memory_usage(deep=True).sum() / 1e6, "MB after optimization")
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
### MUST DO
|
||||
- Use vectorized operations instead of loops
|
||||
- Set appropriate dtypes (categorical for low-cardinality strings)
|
||||
- Check memory usage with `.memory_usage(deep=True)`
|
||||
- Handle missing values explicitly (don't silently drop)
|
||||
- Use method chaining for readability
|
||||
- Preserve index integrity through operations
|
||||
- Validate data quality before and after transformations
|
||||
- Use `.copy()` when modifying subsets to avoid SettingWithCopyWarning
|
||||
|
||||
### MUST NOT DO
|
||||
- Iterate over DataFrame rows with `.iterrows()` unless absolutely necessary
|
||||
- Use chained indexing (`df['A']['B']`) — use `.loc[]` or `.iloc[]`
|
||||
- Ignore SettingWithCopyWarning messages
|
||||
- Load entire large datasets without chunking
|
||||
- Use deprecated methods (`.ix`, `.append()` — use `pd.concat()`)
|
||||
- Convert to Python lists for operations possible in pandas
|
||||
- Assume data is clean without validation
|
||||
|
||||
## Output Templates
|
||||
|
||||
When implementing pandas solutions, provide:
|
||||
1. Code with vectorized operations and proper indexing
|
||||
2. Comments explaining complex transformations
|
||||
3. Memory/performance considerations if dataset is large
|
||||
4. Data validation checks (dtypes, nulls, shapes)
|
||||
|
||||
[Documentation](https://jeffallan.github.io/claude-skills/skills/data-ml/pandas-pro/)
|
||||
@@ -0,0 +1,545 @@
|
||||
# Aggregation and GroupBy
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Aggregation transforms data from individual records to summary statistics. This reference covers GroupBy, pivot tables, crosstab, and advanced aggregation patterns with pandas 2.0+.
|
||||
|
||||
---
|
||||
|
||||
## GroupBy Fundamentals
|
||||
|
||||
### Basic GroupBy
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
df = pd.DataFrame({
|
||||
'department': ['Eng', 'Eng', 'Sales', 'Sales', 'Eng', 'HR'],
|
||||
'team': ['Backend', 'Frontend', 'East', 'West', 'Backend', 'Recruit'],
|
||||
'employee': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank'],
|
||||
'salary': [80000, 75000, 65000, 70000, 85000, 60000],
|
||||
'years': [5, 3, 7, 4, 6, 2]
|
||||
})
|
||||
|
||||
# Single column groupby with single aggregation
|
||||
avg_salary = df.groupby('department')['salary'].mean()
|
||||
|
||||
# Multiple aggregations
|
||||
stats = df.groupby('department')['salary'].agg(['mean', 'min', 'max', 'count'])
|
||||
|
||||
# GroupBy multiple columns
|
||||
grouped = df.groupby(['department', 'team'])['salary'].mean()
|
||||
|
||||
# Reset index to get DataFrame instead of Series
|
||||
grouped = df.groupby('department')['salary'].mean().reset_index()
|
||||
```
|
||||
|
||||
### Multiple Columns, Multiple Aggregations
|
||||
|
||||
```python
|
||||
# Named aggregation (pandas 2.0+ preferred)
|
||||
result = df.groupby('department').agg(
|
||||
avg_salary=('salary', 'mean'),
|
||||
max_salary=('salary', 'max'),
|
||||
total_years=('years', 'sum'),
|
||||
headcount=('employee', 'count'),
|
||||
)
|
||||
|
||||
# Dictionary syntax (traditional)
|
||||
result = df.groupby('department').agg({
|
||||
'salary': ['mean', 'max', 'std'],
|
||||
'years': ['sum', 'mean'],
|
||||
})
|
||||
|
||||
# Flatten multi-level column names
|
||||
result.columns = ['_'.join(col).strip() for col in result.columns.values]
|
||||
```
|
||||
|
||||
### Custom Aggregation Functions
|
||||
|
||||
```python
|
||||
# Lambda functions
|
||||
result = df.groupby('department').agg({
|
||||
'salary': lambda x: x.max() - x.min(), # Range
|
||||
'years': lambda x: x.quantile(0.75), # 75th percentile
|
||||
})
|
||||
|
||||
# Named functions for clarity
|
||||
def salary_range(x):
|
||||
return x.max() - x.min()
|
||||
|
||||
def coefficient_of_variation(x):
|
||||
return x.std() / x.mean() if x.mean() != 0 else 0
|
||||
|
||||
result = df.groupby('department').agg(
|
||||
salary_range=('salary', salary_range),
|
||||
salary_cv=('salary', coefficient_of_variation),
|
||||
)
|
||||
|
||||
# Multiple custom functions
|
||||
result = df.groupby('department')['salary'].agg([
|
||||
('range', lambda x: x.max() - x.min()),
|
||||
('iqr', lambda x: x.quantile(0.75) - x.quantile(0.25)),
|
||||
('median', 'median'),
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transform and Apply
|
||||
|
||||
### Transform - Returns Same Shape
|
||||
|
||||
```python
|
||||
# Transform returns Series with same index as original
|
||||
# Useful for adding aggregated values back to original DataFrame
|
||||
|
||||
# Add group mean as new column
|
||||
df['dept_avg_salary'] = df.groupby('department')['salary'].transform('mean')
|
||||
|
||||
# Normalize within group
|
||||
df['salary_zscore'] = df.groupby('department')['salary'].transform(
|
||||
lambda x: (x - x.mean()) / x.std()
|
||||
)
|
||||
|
||||
# Rank within group
|
||||
df['salary_rank'] = df.groupby('department')['salary'].transform('rank', ascending=False)
|
||||
|
||||
# Percentage of group total
|
||||
df['salary_pct'] = df.groupby('department')['salary'].transform(
|
||||
lambda x: x / x.sum() * 100
|
||||
)
|
||||
|
||||
# Fill missing with group mean
|
||||
df['salary'] = df.groupby('department')['salary'].transform(
|
||||
lambda x: x.fillna(x.mean())
|
||||
)
|
||||
```
|
||||
|
||||
### Apply - Flexible Operations
|
||||
|
||||
```python
|
||||
# Apply runs function on each group DataFrame
|
||||
def top_n_by_salary(group, n=2):
|
||||
return group.nlargest(n, 'salary')
|
||||
|
||||
top_earners = df.groupby('department').apply(top_n_by_salary, n=2)
|
||||
|
||||
# Reset index after apply
|
||||
top_earners = df.groupby('department', group_keys=False).apply(
|
||||
top_n_by_salary, n=2
|
||||
).reset_index(drop=True)
|
||||
|
||||
# Complex group operations
|
||||
def group_summary(group):
|
||||
return pd.Series({
|
||||
'headcount': len(group),
|
||||
'avg_salary': group['salary'].mean(),
|
||||
'top_earner': group.loc[group['salary'].idxmax(), 'employee'],
|
||||
'avg_tenure': group['years'].mean(),
|
||||
})
|
||||
|
||||
summary = df.groupby('department').apply(group_summary)
|
||||
```
|
||||
|
||||
### Filter - Keep/Remove Groups
|
||||
|
||||
```python
|
||||
# Keep only groups meeting a condition
|
||||
# Groups with average salary > 70000
|
||||
filtered = df.groupby('department').filter(lambda x: x['salary'].mean() > 70000)
|
||||
|
||||
# Groups with more than 2 members
|
||||
filtered = df.groupby('department').filter(lambda x: len(x) > 2)
|
||||
|
||||
# Combined conditions
|
||||
filtered = df.groupby('department').filter(
|
||||
lambda x: (len(x) >= 2) and (x['salary'].mean() > 65000)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pivot Tables
|
||||
|
||||
### Basic Pivot Table
|
||||
|
||||
```python
|
||||
df = pd.DataFrame({
|
||||
'date': pd.date_range('2024-01-01', periods=6),
|
||||
'product': ['A', 'B', 'A', 'B', 'A', 'B'],
|
||||
'region': ['East', 'East', 'West', 'West', 'East', 'West'],
|
||||
'sales': [100, 150, 120, 180, 90, 200],
|
||||
'quantity': [10, 15, 12, 18, 9, 20],
|
||||
})
|
||||
|
||||
# Simple pivot
|
||||
pivot = df.pivot_table(
|
||||
values='sales',
|
||||
index='product',
|
||||
columns='region',
|
||||
aggfunc='sum'
|
||||
)
|
||||
|
||||
# Multiple values
|
||||
pivot = df.pivot_table(
|
||||
values=['sales', 'quantity'],
|
||||
index='product',
|
||||
columns='region',
|
||||
aggfunc='sum'
|
||||
)
|
||||
|
||||
# Multiple aggregation functions
|
||||
pivot = df.pivot_table(
|
||||
values='sales',
|
||||
index='product',
|
||||
columns='region',
|
||||
aggfunc=['sum', 'mean', 'count']
|
||||
)
|
||||
```
|
||||
|
||||
### Advanced Pivot Table Options
|
||||
|
||||
```python
|
||||
# Fill missing values
|
||||
pivot = df.pivot_table(
|
||||
values='sales',
|
||||
index='product',
|
||||
columns='region',
|
||||
aggfunc='sum',
|
||||
fill_value=0
|
||||
)
|
||||
|
||||
# Add margins (totals)
|
||||
pivot = df.pivot_table(
|
||||
values='sales',
|
||||
index='product',
|
||||
columns='region',
|
||||
aggfunc='sum',
|
||||
margins=True,
|
||||
margins_name='Total'
|
||||
)
|
||||
|
||||
# Multiple index levels
|
||||
pivot = df.pivot_table(
|
||||
values='sales',
|
||||
index=['product', df['date'].dt.month],
|
||||
columns='region',
|
||||
aggfunc='sum'
|
||||
)
|
||||
|
||||
# Observed categories only (for categorical data)
|
||||
pivot = df.pivot_table(
|
||||
values='sales',
|
||||
index='product',
|
||||
columns='region',
|
||||
aggfunc='sum',
|
||||
observed=True # pandas 2.0+ default changed
|
||||
)
|
||||
```
|
||||
|
||||
### Unpivoting (Melt)
|
||||
|
||||
```python
|
||||
# Wide to long format
|
||||
wide_df = pd.DataFrame({
|
||||
'product': ['A', 'B'],
|
||||
'Q1_sales': [100, 150],
|
||||
'Q2_sales': [120, 180],
|
||||
'Q3_sales': [90, 200],
|
||||
})
|
||||
|
||||
# Melt to long format
|
||||
long_df = pd.melt(
|
||||
wide_df,
|
||||
id_vars=['product'],
|
||||
value_vars=['Q1_sales', 'Q2_sales', 'Q3_sales'],
|
||||
var_name='quarter',
|
||||
value_name='sales'
|
||||
)
|
||||
|
||||
# Clean quarter column
|
||||
long_df['quarter'] = long_df['quarter'].str.replace('_sales', '')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Crosstab
|
||||
|
||||
### Basic Crosstab
|
||||
|
||||
```python
|
||||
df = pd.DataFrame({
|
||||
'gender': ['M', 'F', 'M', 'F', 'M', 'F', 'M', 'M'],
|
||||
'department': ['Eng', 'Eng', 'Sales', 'Sales', 'Eng', 'HR', 'HR', 'Eng'],
|
||||
'level': ['Senior', 'Junior', 'Senior', 'Senior', 'Junior', 'Junior', 'Senior', 'Junior'],
|
||||
})
|
||||
|
||||
# Simple crosstab (counts)
|
||||
ct = pd.crosstab(df['gender'], df['department'])
|
||||
|
||||
# Normalized crosstab
|
||||
ct_pct = pd.crosstab(df['gender'], df['department'], normalize='all') # Total
|
||||
ct_pct = pd.crosstab(df['gender'], df['department'], normalize='index') # Row
|
||||
ct_pct = pd.crosstab(df['gender'], df['department'], normalize='columns') # Column
|
||||
|
||||
# With margins
|
||||
ct = pd.crosstab(df['gender'], df['department'], margins=True)
|
||||
|
||||
# Multiple levels
|
||||
ct = pd.crosstab(
|
||||
[df['gender'], df['level']],
|
||||
df['department']
|
||||
)
|
||||
```
|
||||
|
||||
### Crosstab with Aggregation
|
||||
|
||||
```python
|
||||
df['salary'] = [80000, 75000, 65000, 70000, 85000, 60000, 72000, 78000]
|
||||
|
||||
# Crosstab with values and aggregation
|
||||
ct = pd.crosstab(
|
||||
df['gender'],
|
||||
df['department'],
|
||||
values=df['salary'],
|
||||
aggfunc='mean'
|
||||
)
|
||||
|
||||
# Multiple aggregations
|
||||
ct = pd.crosstab(
|
||||
df['gender'],
|
||||
df['department'],
|
||||
values=df['salary'],
|
||||
aggfunc=['mean', 'sum', 'count']
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Window Functions with GroupBy
|
||||
|
||||
### Rolling Aggregations
|
||||
|
||||
```python
|
||||
df = pd.DataFrame({
|
||||
'date': pd.date_range('2024-01-01', periods=10),
|
||||
'product': ['A', 'B'] * 5,
|
||||
'sales': [100, 150, 110, 160, 120, 170, 130, 180, 140, 190],
|
||||
})
|
||||
|
||||
# Rolling mean within groups
|
||||
df['rolling_avg'] = df.groupby('product')['sales'].transform(
|
||||
lambda x: x.rolling(window=3, min_periods=1).mean()
|
||||
)
|
||||
|
||||
# Expanding aggregations
|
||||
df['cumulative_sales'] = df.groupby('product')['sales'].transform('cumsum')
|
||||
|
||||
df['expanding_avg'] = df.groupby('product')['sales'].transform(
|
||||
lambda x: x.expanding().mean()
|
||||
)
|
||||
|
||||
# Rank within groups
|
||||
df['sales_rank'] = df.groupby('product')['sales'].rank(method='dense')
|
||||
```
|
||||
|
||||
### Shift and Diff
|
||||
|
||||
```python
|
||||
# Previous value within group
|
||||
df['prev_sales'] = df.groupby('product')['sales'].shift(1)
|
||||
|
||||
# Next value
|
||||
df['next_sales'] = df.groupby('product')['sales'].shift(-1)
|
||||
|
||||
# Period-over-period change
|
||||
df['sales_change'] = df.groupby('product')['sales'].diff()
|
||||
|
||||
# Percentage change
|
||||
df['sales_pct_change'] = df.groupby('product')['sales'].pct_change()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Aggregation Patterns
|
||||
|
||||
### Summary Statistics
|
||||
|
||||
```python
|
||||
# Comprehensive summary by group
|
||||
def full_summary(group):
|
||||
return pd.Series({
|
||||
'count': len(group),
|
||||
'mean': group['salary'].mean(),
|
||||
'std': group['salary'].std(),
|
||||
'min': group['salary'].min(),
|
||||
'q25': group['salary'].quantile(0.25),
|
||||
'median': group['salary'].median(),
|
||||
'q75': group['salary'].quantile(0.75),
|
||||
'max': group['salary'].max(),
|
||||
'sum': group['salary'].sum(),
|
||||
})
|
||||
|
||||
summary = df.groupby('department').apply(full_summary)
|
||||
```
|
||||
|
||||
### Top N Per Group
|
||||
|
||||
```python
|
||||
# Top 2 salaries per department
|
||||
top_2 = df.groupby('department', group_keys=False).apply(
|
||||
lambda x: x.nlargest(2, 'salary')
|
||||
)
|
||||
|
||||
# Using head after sorting
|
||||
top_2 = df.sort_values('salary', ascending=False).groupby(
|
||||
'department', group_keys=False
|
||||
).head(2)
|
||||
|
||||
# Bottom N
|
||||
bottom_2 = df.groupby('department', group_keys=False).apply(
|
||||
lambda x: x.nsmallest(2, 'salary')
|
||||
)
|
||||
```
|
||||
|
||||
### First/Last Per Group
|
||||
|
||||
```python
|
||||
# First row per group
|
||||
first = df.groupby('department').first()
|
||||
|
||||
# Last row per group
|
||||
last = df.groupby('department').last()
|
||||
|
||||
# First row after sorting
|
||||
first_by_salary = df.sort_values('salary', ascending=False).groupby(
|
||||
'department'
|
||||
).first()
|
||||
|
||||
# Nth row
|
||||
nth = df.groupby('department').nth(1) # Second row (0-indexed)
|
||||
```
|
||||
|
||||
### Cumulative Operations
|
||||
|
||||
```python
|
||||
# Cumulative sum
|
||||
df['cum_sales'] = df.groupby('department')['salary'].cumsum()
|
||||
|
||||
# Cumulative max/min
|
||||
df['cum_max'] = df.groupby('department')['salary'].cummax()
|
||||
df['cum_min'] = df.groupby('department')['salary'].cummin()
|
||||
|
||||
# Cumulative count
|
||||
df['cum_count'] = df.groupby('department').cumcount() + 1
|
||||
|
||||
# Running percentage of total
|
||||
df['running_pct'] = df.groupby('department')['salary'].transform(
|
||||
lambda x: x.cumsum() / x.sum() * 100
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Tips for GroupBy
|
||||
|
||||
### Efficient GroupBy Operations
|
||||
|
||||
```python
|
||||
# Pre-sort for faster groupby operations
|
||||
df = df.sort_values('department')
|
||||
grouped = df.groupby('department', sort=False) # Already sorted
|
||||
|
||||
# Use observed=True for categorical columns (pandas 2.0+ default)
|
||||
df['department'] = df['department'].astype('category')
|
||||
grouped = df.groupby('department', observed=True)['salary'].mean()
|
||||
|
||||
# Avoid apply when possible - use built-in aggregations
|
||||
# SLOWER:
|
||||
result = df.groupby('department')['salary'].apply(lambda x: x.sum())
|
||||
# FASTER:
|
||||
result = df.groupby('department')['salary'].sum()
|
||||
|
||||
# Use numba for custom aggregations (if available)
|
||||
@numba.jit(nopython=True)
|
||||
def custom_agg(values):
|
||||
return values.sum() / len(values)
|
||||
```
|
||||
|
||||
### Memory-Efficient Aggregation
|
||||
|
||||
```python
|
||||
# For large DataFrames, compute aggregations separately
|
||||
groups = df.groupby('department')
|
||||
|
||||
means = groups['salary'].mean()
|
||||
sums = groups['salary'].sum()
|
||||
counts = groups.size()
|
||||
|
||||
result = pd.DataFrame({
|
||||
'mean': means,
|
||||
'sum': sums,
|
||||
'count': counts
|
||||
})
|
||||
|
||||
# Avoid creating intermediate large DataFrames
|
||||
# BAD: Creates full transformed DataFrame
|
||||
df['z_score'] = (df['salary'] - df.groupby('department')['salary'].transform('mean')) / df.groupby('department')['salary'].transform('std')
|
||||
|
||||
# BETTER: Compute once
|
||||
group_stats = df.groupby('department')['salary'].agg(['mean', 'std'])
|
||||
df = df.merge(group_stats, on='department')
|
||||
df['z_score'] = (df['salary'] - df['mean']) / df['std']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Use named aggregation** - Clearer than dictionary syntax
|
||||
2. **Choose transform vs apply wisely** - Transform for same-shape, apply for flexible
|
||||
3. **Pre-sort for performance** - Use `sort=False` after sorting
|
||||
4. **Prefer built-in aggregations** - Faster than lambda/apply
|
||||
5. **Use observed=True** - Especially for categorical data
|
||||
6. **Reset index when needed** - Keep DataFrames easier to work with
|
||||
7. **Validate group counts** - Check for unexpected groups
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
```python
|
||||
# BAD: Iterating over groups manually
|
||||
for name, group in df.groupby('department'):
|
||||
# process group
|
||||
pass
|
||||
|
||||
# GOOD: Use vectorized operations
|
||||
df.groupby('department').agg(...)
|
||||
|
||||
# BAD: Multiple groupby calls
|
||||
df.groupby('dept')['salary'].mean()
|
||||
df.groupby('dept')['salary'].sum()
|
||||
df.groupby('dept')['salary'].count()
|
||||
|
||||
# GOOD: Single groupby, multiple aggs
|
||||
df.groupby('dept')['salary'].agg(['mean', 'sum', 'count'])
|
||||
|
||||
# BAD: Apply for simple aggregations
|
||||
df.groupby('dept')['salary'].apply(np.mean)
|
||||
|
||||
# GOOD: Built-in method
|
||||
df.groupby('dept')['salary'].mean()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related References
|
||||
|
||||
- `dataframe-operations.md` - Filtering before aggregation
|
||||
- `merging-joining.md` - Join aggregated results back
|
||||
- `performance-optimization.md` - Optimize large-scale aggregations
|
||||
@@ -0,0 +1,500 @@
|
||||
# Data Cleaning
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Data cleaning is critical for reliable analysis. This reference covers handling missing values, duplicates, type conversion, and data validation with pandas 2.0+ patterns.
|
||||
|
||||
---
|
||||
|
||||
## Missing Values
|
||||
|
||||
### Detecting Missing Values
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
df = pd.DataFrame({
|
||||
'name': ['Alice', 'Bob', None, 'Diana'],
|
||||
'age': [25, np.nan, 35, 28],
|
||||
'salary': [50000, 60000, np.nan, np.nan],
|
||||
'department': ['Eng', '', 'Eng', 'Sales']
|
||||
})
|
||||
|
||||
# Check for any missing values
|
||||
df.isna().any() # Per column
|
||||
df.isna().any().any() # Entire DataFrame
|
||||
|
||||
# Count missing values
|
||||
df.isna().sum() # Per column
|
||||
df.isna().sum().sum() # Total
|
||||
|
||||
# Percentage of missing values
|
||||
(df.isna().sum() / len(df) * 100).round(2)
|
||||
|
||||
# Rows with any missing values
|
||||
df[df.isna().any(axis=1)]
|
||||
|
||||
# Rows with all values present
|
||||
df[df.notna().all(axis=1)]
|
||||
|
||||
# Missing value heatmap info
|
||||
missing_info = pd.DataFrame({
|
||||
'missing': df.isna().sum(),
|
||||
'percent': (df.isna().sum() / len(df) * 100).round(2),
|
||||
'dtype': df.dtypes
|
||||
})
|
||||
```
|
||||
|
||||
### Handling Missing Values - Dropping
|
||||
|
||||
```python
|
||||
# Drop rows with any missing value
|
||||
df_clean = df.dropna()
|
||||
|
||||
# Drop rows where specific columns have missing values
|
||||
df_clean = df.dropna(subset=['name', 'age'])
|
||||
|
||||
# Drop rows where ALL values are missing
|
||||
df_clean = df.dropna(how='all')
|
||||
|
||||
# Drop rows with minimum non-null values
|
||||
df_clean = df.dropna(thresh=3) # Keep rows with at least 3 non-null
|
||||
|
||||
# Drop columns with missing values
|
||||
df_clean = df.dropna(axis=1)
|
||||
|
||||
# Drop columns with more than 50% missing
|
||||
threshold = len(df) * 0.5
|
||||
df_clean = df.dropna(axis=1, thresh=threshold)
|
||||
```
|
||||
|
||||
### Handling Missing Values - Filling
|
||||
|
||||
```python
|
||||
# Fill with constant value
|
||||
df['age'] = df['age'].fillna(0)
|
||||
|
||||
# Fill with column mean/median/mode
|
||||
df['age'] = df['age'].fillna(df['age'].mean())
|
||||
df['salary'] = df['salary'].fillna(df['salary'].median())
|
||||
df['department'] = df['department'].fillna(df['department'].mode()[0])
|
||||
|
||||
# Forward fill (use previous value)
|
||||
df['salary'] = df['salary'].ffill()
|
||||
|
||||
# Backward fill (use next value)
|
||||
df['salary'] = df['salary'].bfill()
|
||||
|
||||
# Fill with different values per column
|
||||
fill_values = {'age': 0, 'salary': df['salary'].median(), 'name': 'Unknown'}
|
||||
df = df.fillna(fill_values)
|
||||
|
||||
# Fill with interpolation (numeric data)
|
||||
df['salary'] = df['salary'].interpolate(method='linear')
|
||||
|
||||
# Group-specific fill (fill with group mean)
|
||||
df['salary'] = df.groupby('department')['salary'].transform(
|
||||
lambda x: x.fillna(x.mean())
|
||||
)
|
||||
```
|
||||
|
||||
### Handling Empty Strings vs NaN
|
||||
|
||||
```python
|
||||
# Empty strings are NOT detected as NaN
|
||||
df['department'].isna().sum() # Won't count ''
|
||||
|
||||
# Replace empty strings with NaN
|
||||
df['department'] = df['department'].replace('', np.nan)
|
||||
# Or
|
||||
df['department'] = df['department'].replace(r'^\s*$', np.nan, regex=True)
|
||||
|
||||
# Replace multiple values with NaN
|
||||
df = df.replace(['', 'N/A', 'null', 'None', '-'], np.nan)
|
||||
|
||||
# Using na_values when reading files
|
||||
df = pd.read_csv('file.csv', na_values=['', 'N/A', 'null', 'None', '-'])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Handling Duplicates
|
||||
|
||||
### Detecting Duplicates
|
||||
|
||||
```python
|
||||
df = pd.DataFrame({
|
||||
'id': [1, 2, 2, 3, 4, 4],
|
||||
'name': ['Alice', 'Bob', 'Bob', 'Charlie', 'Diana', 'Diana'],
|
||||
'email': ['a@x.com', 'b@x.com', 'b@x.com', 'c@x.com', 'd@x.com', 'd2@x.com']
|
||||
})
|
||||
|
||||
# Check for duplicate rows (all columns)
|
||||
df.duplicated().sum()
|
||||
|
||||
# Check specific columns
|
||||
df.duplicated(subset=['id']).sum()
|
||||
df.duplicated(subset=['name', 'email']).sum()
|
||||
|
||||
# View duplicate rows
|
||||
df[df.duplicated(keep=False)] # All duplicates
|
||||
df[df.duplicated(keep='first')] # Duplicates except first occurrence
|
||||
df[df.duplicated(keep='last')] # Duplicates except last occurrence
|
||||
|
||||
# Count duplicates per key
|
||||
df.groupby('id').size().loc[lambda x: x > 1]
|
||||
```
|
||||
|
||||
### Removing Duplicates
|
||||
|
||||
```python
|
||||
# Remove duplicate rows (keep first)
|
||||
df_clean = df.drop_duplicates()
|
||||
|
||||
# Keep last occurrence
|
||||
df_clean = df.drop_duplicates(keep='last')
|
||||
|
||||
# Remove all duplicates (keep none)
|
||||
df_clean = df.drop_duplicates(keep=False)
|
||||
|
||||
# Based on specific columns
|
||||
df_clean = df.drop_duplicates(subset=['id'])
|
||||
df_clean = df.drop_duplicates(subset=['name', 'email'], keep='last')
|
||||
|
||||
# In-place modification
|
||||
df.drop_duplicates(inplace=True)
|
||||
```
|
||||
|
||||
### Handling Duplicates with Aggregation
|
||||
|
||||
```python
|
||||
# Instead of dropping, aggregate duplicates
|
||||
df_agg = df.groupby('id').agg({
|
||||
'name': 'first',
|
||||
'email': lambda x: ', '.join(x.unique())
|
||||
}).reset_index()
|
||||
|
||||
# Keep row with max/min value
|
||||
df_best = df.loc[df.groupby('id')['score'].idxmax()]
|
||||
|
||||
# Rank duplicates
|
||||
df['rank'] = df.groupby('id').cumcount() + 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Conversion
|
||||
|
||||
### Checking and Converting Types
|
||||
|
||||
```python
|
||||
# Check current types
|
||||
df.dtypes
|
||||
df.info()
|
||||
|
||||
# Convert to specific type
|
||||
df['age'] = df['age'].astype(int)
|
||||
df['salary'] = df['salary'].astype(float)
|
||||
df['name'] = df['name'].astype(str)
|
||||
|
||||
# Safe conversion with errors handling
|
||||
df['age'] = pd.to_numeric(df['age'], errors='coerce') # Invalid -> NaN
|
||||
df['age'] = pd.to_numeric(df['age'], errors='ignore') # Keep original if invalid
|
||||
|
||||
# Convert multiple columns
|
||||
df = df.astype({'age': 'int64', 'salary': 'float64'})
|
||||
|
||||
# Convert object to string (pandas 2.0+ StringDtype)
|
||||
df['name'] = df['name'].astype('string') # Nullable string type
|
||||
```
|
||||
|
||||
### Datetime Conversion
|
||||
|
||||
```python
|
||||
df = pd.DataFrame({
|
||||
'date_str': ['2024-01-15', '2024-02-20', 'invalid', '2024-03-10'],
|
||||
'timestamp': [1705276800, 1708387200, 1710028800, 1710028800]
|
||||
})
|
||||
|
||||
# String to datetime
|
||||
df['date'] = pd.to_datetime(df['date_str'], errors='coerce')
|
||||
|
||||
# Specify format for faster parsing
|
||||
df['date'] = pd.to_datetime(df['date_str'], format='%Y-%m-%d', errors='coerce')
|
||||
|
||||
# Unix timestamp to datetime
|
||||
df['datetime'] = pd.to_datetime(df['timestamp'], unit='s')
|
||||
|
||||
# Extract components
|
||||
df['year'] = df['date'].dt.year
|
||||
df['month'] = df['date'].dt.month
|
||||
df['day_of_week'] = df['date'].dt.day_name()
|
||||
|
||||
# Handle mixed formats
|
||||
df['date'] = pd.to_datetime(df['date_str'], format='mixed', dayfirst=False)
|
||||
```
|
||||
|
||||
### Categorical Conversion
|
||||
|
||||
```python
|
||||
# Convert to categorical (memory efficient for low cardinality)
|
||||
df['department'] = df['department'].astype('category')
|
||||
|
||||
# Ordered categorical
|
||||
df['size'] = pd.Categorical(
|
||||
df['size'],
|
||||
categories=['Small', 'Medium', 'Large'],
|
||||
ordered=True
|
||||
)
|
||||
|
||||
# Check memory savings
|
||||
print(f"Object: {df['department'].nbytes}")
|
||||
df['department'] = df['department'].astype('category')
|
||||
print(f"Category: {df['department'].nbytes}")
|
||||
```
|
||||
|
||||
### Nullable Integer Types (pandas 2.0+)
|
||||
|
||||
```python
|
||||
# Standard int doesn't support NaN
|
||||
# Use nullable integer types
|
||||
df['age'] = df['age'].astype('Int64') # Note capital I
|
||||
|
||||
# All nullable types
|
||||
df = df.astype({
|
||||
'count': 'Int64', # Nullable integer
|
||||
'price': 'Float64', # Nullable float
|
||||
'flag': 'boolean', # Nullable boolean
|
||||
'name': 'string', # Nullable string
|
||||
})
|
||||
|
||||
# Convert with NA handling
|
||||
df['age'] = pd.array([1, 2, None, 4], dtype='Int64')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## String Cleaning
|
||||
|
||||
### Common String Operations
|
||||
|
||||
```python
|
||||
df = pd.DataFrame({
|
||||
'name': [' Alice ', 'BOB', 'charlie', None, 'Diana Smith'],
|
||||
'email': ['ALICE@EXAMPLE.COM', 'bob@test', 'invalid', None, 'diana@example.com']
|
||||
})
|
||||
|
||||
# Strip whitespace
|
||||
df['name'] = df['name'].str.strip()
|
||||
|
||||
# Case normalization
|
||||
df['name'] = df['name'].str.lower()
|
||||
df['name'] = df['name'].str.upper()
|
||||
df['name'] = df['name'].str.title() # Title Case
|
||||
|
||||
# Replace patterns
|
||||
df['name'] = df['name'].str.replace(r'\s+', ' ', regex=True) # Multiple spaces to one
|
||||
df['phone'] = df['phone'].str.replace(r'[^0-9]', '', regex=True) # Keep only digits
|
||||
|
||||
# Extract with regex
|
||||
df['domain'] = df['email'].str.extract(r'@(.+)$')
|
||||
df['first_name'] = df['name'].str.extract(r'^(\w+)')
|
||||
|
||||
# Split strings
|
||||
df[['first', 'last']] = df['name'].str.split(' ', n=1, expand=True)
|
||||
```
|
||||
|
||||
### String Validation
|
||||
|
||||
```python
|
||||
# Check patterns
|
||||
df['valid_email'] = df['email'].str.match(r'^[\w.]+@[\w.]+\.\w+$', na=False)
|
||||
|
||||
# String length
|
||||
df['name_length'] = df['name'].str.len()
|
||||
df['valid_length'] = df['name'].str.len().between(2, 50)
|
||||
|
||||
# Contains check
|
||||
df['has_domain'] = df['email'].str.contains('@', na=False)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Validation
|
||||
|
||||
### Validation Functions
|
||||
|
||||
```python
|
||||
def validate_dataframe(df: pd.DataFrame) -> dict:
|
||||
"""Comprehensive DataFrame validation."""
|
||||
report = {
|
||||
'rows': len(df),
|
||||
'columns': len(df.columns),
|
||||
'duplicates': df.duplicated().sum(),
|
||||
'missing_by_column': df.isna().sum().to_dict(),
|
||||
'dtypes': df.dtypes.astype(str).to_dict(),
|
||||
}
|
||||
return report
|
||||
|
||||
# Range validation
|
||||
def validate_range(series: pd.Series, min_val, max_val) -> pd.Series:
|
||||
"""Return boolean mask for values in range."""
|
||||
return series.between(min_val, max_val)
|
||||
|
||||
df['valid_age'] = validate_range(df['age'], 0, 120)
|
||||
|
||||
# Custom validation
|
||||
def validate_email(series: pd.Series) -> pd.Series:
|
||||
"""Validate email format."""
|
||||
pattern = r'^[\w.+-]+@[\w-]+\.[\w.-]+$'
|
||||
return series.str.match(pattern, na=False)
|
||||
|
||||
df['valid_email'] = validate_email(df['email'])
|
||||
```
|
||||
|
||||
### Schema Validation with pandera
|
||||
|
||||
```python
|
||||
# Using pandera for schema validation (recommended for production)
|
||||
import pandera as pa
|
||||
from pandera import Column, Check
|
||||
|
||||
schema = pa.DataFrameSchema({
|
||||
'name': Column(str, Check.str_length(min_value=1, max_value=100)),
|
||||
'age': Column(int, Check.in_range(0, 120)),
|
||||
'email': Column(str, Check.str_matches(r'^[\w.+-]+@[\w-]+\.[\w.-]+$')),
|
||||
'salary': Column(float, Check.greater_than(0), nullable=True),
|
||||
})
|
||||
|
||||
# Validate DataFrame
|
||||
try:
|
||||
schema.validate(df)
|
||||
except pa.errors.SchemaError as e:
|
||||
print(f"Validation failed: {e}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Cleaning Pipeline
|
||||
|
||||
### Method Chaining Pattern
|
||||
|
||||
```python
|
||||
def clean_dataframe(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Complete data cleaning pipeline using method chaining."""
|
||||
return (
|
||||
df
|
||||
# Make a copy
|
||||
.copy()
|
||||
# Standardize column names
|
||||
.rename(columns=lambda x: x.lower().strip().replace(' ', '_'))
|
||||
# Drop fully empty rows
|
||||
.dropna(how='all')
|
||||
# Clean string columns
|
||||
.assign(
|
||||
name=lambda x: x['name'].str.strip().str.title(),
|
||||
email=lambda x: x['email'].str.lower().str.strip(),
|
||||
)
|
||||
# Handle missing values
|
||||
.fillna({'department': 'Unknown'})
|
||||
# Convert types
|
||||
.astype({'age': 'Int64', 'department': 'category'})
|
||||
# Remove duplicates
|
||||
.drop_duplicates(subset=['email'])
|
||||
# Reset index
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
|
||||
df_clean = clean_dataframe(df)
|
||||
```
|
||||
|
||||
### Pipeline with Validation
|
||||
|
||||
```python
|
||||
def clean_and_validate(
|
||||
df: pd.DataFrame,
|
||||
required_columns: list[str],
|
||||
unique_columns: list[str] | None = None,
|
||||
) -> tuple[pd.DataFrame, dict]:
|
||||
"""Clean DataFrame and return validation report."""
|
||||
|
||||
# Validate required columns exist
|
||||
missing_cols = set(required_columns) - set(df.columns)
|
||||
if missing_cols:
|
||||
raise ValueError(f"Missing required columns: {missing_cols}")
|
||||
|
||||
# Track cleaning stats
|
||||
stats = {
|
||||
'initial_rows': len(df),
|
||||
'dropped_empty': 0,
|
||||
'dropped_duplicates': 0,
|
||||
'filled_missing': {},
|
||||
}
|
||||
|
||||
# Clean
|
||||
df = df.copy()
|
||||
|
||||
# Drop empty rows
|
||||
before = len(df)
|
||||
df = df.dropna(how='all')
|
||||
stats['dropped_empty'] = before - len(df)
|
||||
|
||||
# Handle duplicates
|
||||
if unique_columns:
|
||||
before = len(df)
|
||||
df = df.drop_duplicates(subset=unique_columns)
|
||||
stats['dropped_duplicates'] = before - len(df)
|
||||
|
||||
stats['final_rows'] = len(df)
|
||||
|
||||
return df, stats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Always check data quality first** - Use `.info()`, `.describe()`, and missing value analysis
|
||||
2. **Document cleaning decisions** - Track what was dropped/filled and why
|
||||
3. **Use nullable types** - `Int64`, `string`, `boolean` for proper NA handling
|
||||
4. **Validate after cleaning** - Ensure data meets expectations
|
||||
5. **Use method chaining** - Readable, maintainable cleaning pipelines
|
||||
6. **Copy before modifying** - Avoid SettingWithCopyWarning
|
||||
7. **Handle edge cases** - Empty strings, whitespace, invalid formats
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
```python
|
||||
# BAD: Dropping NaN without understanding impact
|
||||
df = df.dropna() # May lose significant data
|
||||
|
||||
# GOOD: Investigate first, then decide
|
||||
print(f"Missing values: {df.isna().sum()}")
|
||||
print(f"Rows affected: {df.isna().any(axis=1).sum()}")
|
||||
# Then make informed decision
|
||||
|
||||
# BAD: Filling without domain knowledge
|
||||
df['age'] = df['age'].fillna(0) # Age 0 is not valid
|
||||
|
||||
# GOOD: Use appropriate fill strategy
|
||||
df['age'] = df['age'].fillna(df['age'].median())
|
||||
|
||||
# BAD: Type conversion without error handling
|
||||
df['id'] = df['id'].astype(int) # Will fail on NaN or invalid
|
||||
|
||||
# GOOD: Safe conversion
|
||||
df['id'] = pd.to_numeric(df['id'], errors='coerce').astype('Int64')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related References
|
||||
|
||||
- `dataframe-operations.md` - Selection and filtering for targeted cleaning
|
||||
- `aggregation-groupby.md` - Aggregate duplicates instead of dropping
|
||||
- `performance-optimization.md` - Efficient cleaning of large datasets
|
||||
@@ -0,0 +1,420 @@
|
||||
# DataFrame Operations
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
DataFrame operations form the foundation of pandas work. This reference covers indexing, selection, filtering, and sorting with pandas 2.0+ best practices.
|
||||
|
||||
---
|
||||
|
||||
## Indexing and Selection
|
||||
|
||||
### Label-Based Selection with `.loc[]`
|
||||
|
||||
Use `.loc[]` for label-based indexing. Always preferred over chained indexing.
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
# Sample DataFrame
|
||||
df = pd.DataFrame({
|
||||
'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
|
||||
'age': [25, 30, 35, 28],
|
||||
'salary': [50000, 60000, 70000, 55000],
|
||||
'department': ['Engineering', 'Sales', 'Engineering', 'Marketing']
|
||||
}, index=['a', 'b', 'c', 'd'])
|
||||
|
||||
# Single value
|
||||
value = df.loc['a', 'name'] # 'Alice'
|
||||
|
||||
# Single row (returns Series)
|
||||
row = df.loc['a']
|
||||
|
||||
# Multiple rows
|
||||
rows = df.loc[['a', 'c']]
|
||||
|
||||
# Row and column slices (inclusive on both ends)
|
||||
subset = df.loc['a':'c', 'name':'salary']
|
||||
|
||||
# Boolean indexing with .loc
|
||||
adults = df.loc[df['age'] >= 30]
|
||||
|
||||
# Boolean indexing with column selection
|
||||
adults_names = df.loc[df['age'] >= 30, 'name']
|
||||
|
||||
# Multiple conditions
|
||||
engineering_seniors = df.loc[
|
||||
(df['department'] == 'Engineering') & (df['age'] >= 30),
|
||||
['name', 'salary']
|
||||
]
|
||||
```
|
||||
|
||||
### Position-Based Selection with `.iloc[]`
|
||||
|
||||
Use `.iloc[]` for integer position-based indexing.
|
||||
|
||||
```python
|
||||
# Single value by position
|
||||
value = df.iloc[0, 0] # First row, first column
|
||||
|
||||
# Single row by position
|
||||
first_row = df.iloc[0]
|
||||
|
||||
# Slice rows (exclusive end, like Python)
|
||||
first_three = df.iloc[:3]
|
||||
|
||||
# Specific rows and columns by position
|
||||
subset = df.iloc[[0, 2], [0, 2]] # Rows 0,2 and columns 0,2
|
||||
|
||||
# Range selection
|
||||
block = df.iloc[1:3, 0:2] # Rows 1-2, columns 0-1
|
||||
```
|
||||
|
||||
### When to Use `.loc[]` vs `.iloc[]`
|
||||
|
||||
| Scenario | Use | Example |
|
||||
|----------|-----|---------|
|
||||
| Known column names | `.loc[]` | `df.loc[:, 'name']` |
|
||||
| Filter by condition | `.loc[]` | `df.loc[df['age'] > 25]` |
|
||||
| First/last N rows | `.iloc[]` | `df.iloc[:5]` or `df.iloc[-5:]` |
|
||||
| Specific row positions | `.iloc[]` | `df.iloc[[0, 5, 10]]` |
|
||||
| Unknown column order | `.iloc[]` | `df.iloc[:, 0]` |
|
||||
|
||||
---
|
||||
|
||||
## Filtering DataFrames
|
||||
|
||||
### Boolean Masks
|
||||
|
||||
```python
|
||||
# Single condition
|
||||
mask = df['age'] > 25
|
||||
filtered = df[mask]
|
||||
|
||||
# Multiple conditions (use parentheses!)
|
||||
mask = (df['age'] > 25) & (df['salary'] < 65000)
|
||||
filtered = df[mask]
|
||||
|
||||
# OR conditions
|
||||
mask = (df['department'] == 'Engineering') | (df['department'] == 'Sales')
|
||||
filtered = df[mask]
|
||||
|
||||
# NOT condition
|
||||
mask = ~(df['department'] == 'Marketing')
|
||||
filtered = df[mask]
|
||||
```
|
||||
|
||||
### Using `.query()` for Readable Filters
|
||||
|
||||
```python
|
||||
# Simple query - more readable for complex conditions
|
||||
result = df.query('age > 25 and salary < 65000')
|
||||
|
||||
# Using variables with @
|
||||
min_age = 25
|
||||
result = df.query('age > @min_age')
|
||||
|
||||
# String comparisons
|
||||
result = df.query('department == "Engineering"')
|
||||
|
||||
# In-list filtering
|
||||
depts = ['Engineering', 'Sales']
|
||||
result = df.query('department in @depts')
|
||||
|
||||
# Complex expressions
|
||||
result = df.query('(age > 25) and (department != "Marketing")')
|
||||
```
|
||||
|
||||
### Using `.isin()` for Multiple Values
|
||||
|
||||
```python
|
||||
# Filter by multiple values
|
||||
departments = ['Engineering', 'Sales']
|
||||
filtered = df[df['department'].isin(departments)]
|
||||
|
||||
# Negation
|
||||
filtered = df[~df['department'].isin(departments)]
|
||||
|
||||
# Multiple columns
|
||||
conditions = {
|
||||
'department': ['Engineering', 'Sales'],
|
||||
'age': [25, 30, 35]
|
||||
}
|
||||
# Filter where department is in list AND age is in list
|
||||
mask = df['department'].isin(conditions['department']) & df['age'].isin(conditions['age'])
|
||||
```
|
||||
|
||||
### String Filtering with `.str` Accessor
|
||||
|
||||
```python
|
||||
df = pd.DataFrame({
|
||||
'email': ['alice@example.com', 'bob@test.org', 'charlie@example.com'],
|
||||
'name': ['Alice Smith', 'Bob Jones', 'Charlie Brown']
|
||||
})
|
||||
|
||||
# Contains
|
||||
mask = df['email'].str.contains('example')
|
||||
|
||||
# Starts/ends with
|
||||
mask = df['email'].str.endswith('.com')
|
||||
mask = df['name'].str.startswith('A')
|
||||
|
||||
# Regex matching
|
||||
mask = df['email'].str.match(r'^[a-z]+@example\.com$')
|
||||
|
||||
# Case-insensitive
|
||||
mask = df['name'].str.lower().str.contains('alice')
|
||||
# Or with case parameter
|
||||
mask = df['name'].str.contains('alice', case=False)
|
||||
|
||||
# Handle NaN in string columns
|
||||
mask = df['email'].str.contains('example', na=False)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sorting
|
||||
|
||||
### Basic Sorting
|
||||
|
||||
```python
|
||||
# Sort by single column (ascending)
|
||||
sorted_df = df.sort_values('age')
|
||||
|
||||
# Sort descending
|
||||
sorted_df = df.sort_values('age', ascending=False)
|
||||
|
||||
# Sort by multiple columns
|
||||
sorted_df = df.sort_values(['department', 'salary'], ascending=[True, False])
|
||||
|
||||
# Sort by index
|
||||
sorted_df = df.sort_index()
|
||||
sorted_df = df.sort_index(ascending=False)
|
||||
```
|
||||
|
||||
### Advanced Sorting
|
||||
|
||||
```python
|
||||
# Sort with NaN handling
|
||||
df_with_nan = pd.DataFrame({
|
||||
'name': ['Alice', 'Bob', 'Charlie'],
|
||||
'score': [85.0, np.nan, 90.0]
|
||||
})
|
||||
|
||||
# NaN at end (default)
|
||||
sorted_df = df_with_nan.sort_values('score', na_position='last')
|
||||
|
||||
# NaN at beginning
|
||||
sorted_df = df_with_nan.sort_values('score', na_position='first')
|
||||
|
||||
# Custom sort order using Categorical
|
||||
order = ['Marketing', 'Sales', 'Engineering']
|
||||
df['department'] = pd.Categorical(df['department'], categories=order, ordered=True)
|
||||
sorted_df = df.sort_values('department')
|
||||
|
||||
# Sort by computed values without adding column
|
||||
sorted_df = df.iloc[df['name'].str.len().argsort()]
|
||||
```
|
||||
|
||||
### In-Place Sorting
|
||||
|
||||
```python
|
||||
# Modify DataFrame in place
|
||||
df.sort_values('age', inplace=True)
|
||||
|
||||
# Reset index after sorting
|
||||
df.sort_values('age', inplace=True)
|
||||
df.reset_index(drop=True, inplace=True)
|
||||
|
||||
# Or chain
|
||||
df = df.sort_values('age').reset_index(drop=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Column Operations
|
||||
|
||||
### Adding and Modifying Columns
|
||||
|
||||
```python
|
||||
# Add new column
|
||||
df['bonus'] = df['salary'] * 0.1
|
||||
|
||||
# Conditional column with np.where
|
||||
df['seniority'] = np.where(df['age'] >= 30, 'Senior', 'Junior')
|
||||
|
||||
# Multiple conditions with np.select
|
||||
conditions = [
|
||||
df['age'] < 25,
|
||||
df['age'] < 35,
|
||||
df['age'] >= 35
|
||||
]
|
||||
choices = ['Junior', 'Mid', 'Senior']
|
||||
df['level'] = np.select(conditions, choices, default='Unknown')
|
||||
|
||||
# Using .assign() for method chaining (returns new DataFrame)
|
||||
df_new = df.assign(
|
||||
bonus=lambda x: x['salary'] * 0.1,
|
||||
total_comp=lambda x: x['salary'] + x['salary'] * 0.1
|
||||
)
|
||||
```
|
||||
|
||||
### Renaming Columns
|
||||
|
||||
```python
|
||||
# Rename specific columns
|
||||
df = df.rename(columns={'name': 'full_name', 'age': 'years'})
|
||||
|
||||
# Rename all columns with function
|
||||
df.columns = df.columns.str.lower().str.replace(' ', '_')
|
||||
|
||||
# Using rename with function
|
||||
df = df.rename(columns=str.upper)
|
||||
```
|
||||
|
||||
### Dropping Columns
|
||||
|
||||
```python
|
||||
# Drop single column
|
||||
df = df.drop('bonus', axis=1)
|
||||
# Or
|
||||
df = df.drop(columns=['bonus'])
|
||||
|
||||
# Drop multiple columns
|
||||
df = df.drop(columns=['bonus', 'level'])
|
||||
|
||||
# Drop columns by condition
|
||||
cols_to_drop = [col for col in df.columns if col.startswith('temp_')]
|
||||
df = df.drop(columns=cols_to_drop)
|
||||
```
|
||||
|
||||
### Reordering Columns
|
||||
|
||||
```python
|
||||
# Explicit order
|
||||
new_order = ['name', 'department', 'age', 'salary']
|
||||
df = df[new_order]
|
||||
|
||||
# Move specific column to front
|
||||
cols = ['salary'] + [c for c in df.columns if c != 'salary']
|
||||
df = df[cols]
|
||||
|
||||
# Using .reindex()
|
||||
df = df.reindex(columns=['name', 'age', 'salary', 'department'])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Index Operations
|
||||
|
||||
### Setting and Resetting Index
|
||||
|
||||
```python
|
||||
# Set column as index
|
||||
df = df.set_index('name')
|
||||
|
||||
# Reset index back to column
|
||||
df = df.reset_index()
|
||||
|
||||
# Drop index completely
|
||||
df = df.reset_index(drop=True)
|
||||
|
||||
# Set multiple columns as index (MultiIndex)
|
||||
df = df.set_index(['department', 'name'])
|
||||
```
|
||||
|
||||
### Working with MultiIndex
|
||||
|
||||
```python
|
||||
# Create MultiIndex DataFrame
|
||||
df = pd.DataFrame({
|
||||
'department': ['Eng', 'Eng', 'Sales', 'Sales'],
|
||||
'team': ['Backend', 'Frontend', 'East', 'West'],
|
||||
'headcount': [10, 8, 15, 12]
|
||||
}).set_index(['department', 'team'])
|
||||
|
||||
# Select from MultiIndex
|
||||
df.loc['Eng'] # All Eng rows
|
||||
df.loc[('Eng', 'Backend')] # Specific row
|
||||
|
||||
# Cross-section with .xs()
|
||||
df.xs('Backend', level='team') # All Backend teams
|
||||
|
||||
# Reset specific level
|
||||
df.reset_index(level='team')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Copying DataFrames
|
||||
|
||||
### When to Use `.copy()`
|
||||
|
||||
```python
|
||||
# ALWAYS copy when modifying a subset
|
||||
subset = df[df['age'] > 25].copy()
|
||||
subset['new_col'] = 100 # Safe, no SettingWithCopyWarning
|
||||
|
||||
# Without copy - may raise warning or fail silently
|
||||
# BAD:
|
||||
# subset = df[df['age'] > 25]
|
||||
# subset['new_col'] = 100 # SettingWithCopyWarning!
|
||||
|
||||
# Deep copy (default) - copies data
|
||||
df_copy = df.copy() # or df.copy(deep=True)
|
||||
|
||||
# Shallow copy - shares data, only copies structure
|
||||
df_shallow = df.copy(deep=False)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Use `.loc[]` and `.iloc[]`** - Never use chained indexing
|
||||
2. **Parenthesize conditions** - `(cond1) & (cond2)` not `cond1 & cond2`
|
||||
3. **Use `.query()` for readability** - Especially with complex filters
|
||||
4. **Copy before modifying subsets** - Always use `.copy()`
|
||||
5. **Use vectorized operations** - Avoid row iteration for filtering
|
||||
6. **Handle NaN explicitly** - Use `na=False` in string operations
|
||||
7. **Prefer method chaining** - Use `.assign()` for column creation
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
```python
|
||||
# BAD: Chained indexing
|
||||
df['A']['B'] = value # May not work, raises warning
|
||||
|
||||
# GOOD: Use .loc
|
||||
df.loc[:, ('A', 'B')] = value
|
||||
# Or for row selection then assignment:
|
||||
df.loc[df['A'] > 0, 'B'] = value
|
||||
|
||||
# BAD: Iterating for filtering
|
||||
result = []
|
||||
for idx, row in df.iterrows():
|
||||
if row['age'] > 25:
|
||||
result.append(row)
|
||||
|
||||
# GOOD: Boolean indexing
|
||||
result = df[df['age'] > 25]
|
||||
|
||||
# BAD: Multiple separate assignments
|
||||
df = df[df['age'] > 25]
|
||||
df = df[df['salary'] > 50000]
|
||||
|
||||
# GOOD: Combined filter
|
||||
df = df[(df['age'] > 25) & (df['salary'] > 50000)]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related References
|
||||
|
||||
- `data-cleaning.md` - After selection, clean the data
|
||||
- `aggregation-groupby.md` - Group and aggregate filtered data
|
||||
- `performance-optimization.md` - Optimize filtering on large datasets
|
||||
@@ -0,0 +1,596 @@
|
||||
# Merging and Joining
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Combining DataFrames is essential for working with relational data. This reference covers merge, join, concat, and advanced combination strategies with pandas 2.0+.
|
||||
|
||||
---
|
||||
|
||||
## Merge (SQL-Style Joins)
|
||||
|
||||
### Basic Merge
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
# Sample DataFrames
|
||||
employees = pd.DataFrame({
|
||||
'emp_id': [1, 2, 3, 4, 5],
|
||||
'name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
|
||||
'dept_id': [101, 102, 101, 103, 102],
|
||||
})
|
||||
|
||||
departments = pd.DataFrame({
|
||||
'dept_id': [101, 102, 104],
|
||||
'dept_name': ['Engineering', 'Sales', 'Marketing'],
|
||||
})
|
||||
|
||||
# Inner join (default) - only matching rows
|
||||
result = pd.merge(employees, departments, on='dept_id')
|
||||
|
||||
# Explicit how parameter
|
||||
result = pd.merge(employees, departments, on='dept_id', how='inner')
|
||||
```
|
||||
|
||||
### Join Types
|
||||
|
||||
```python
|
||||
# Inner join - only matching rows from both
|
||||
inner = pd.merge(employees, departments, on='dept_id', how='inner')
|
||||
# Result: 4 rows (emp_id 4 has dept_id 103 which doesn't exist in departments)
|
||||
|
||||
# Left join - all rows from left, matching from right
|
||||
left = pd.merge(employees, departments, on='dept_id', how='left')
|
||||
# Result: 5 rows (Diana has NaN for dept_name)
|
||||
|
||||
# Right join - all rows from right, matching from left
|
||||
right = pd.merge(employees, departments, on='dept_id', how='right')
|
||||
# Result: 4 rows (Marketing has no employees, but is included)
|
||||
|
||||
# Outer join - all rows from both
|
||||
outer = pd.merge(employees, departments, on='dept_id', how='outer')
|
||||
# Result: 6 rows (includes unmatched from both sides)
|
||||
|
||||
# Cross join - cartesian product
|
||||
cross = pd.merge(employees, departments, how='cross')
|
||||
# Result: 15 rows (5 employees x 3 departments)
|
||||
```
|
||||
|
||||
### Merging on Different Column Names
|
||||
|
||||
```python
|
||||
employees = pd.DataFrame({
|
||||
'emp_id': [1, 2, 3],
|
||||
'name': ['Alice', 'Bob', 'Charlie'],
|
||||
'department': [101, 102, 101],
|
||||
})
|
||||
|
||||
departments = pd.DataFrame({
|
||||
'id': [101, 102],
|
||||
'dept_name': ['Engineering', 'Sales'],
|
||||
})
|
||||
|
||||
# Different column names
|
||||
result = pd.merge(
|
||||
employees,
|
||||
departments,
|
||||
left_on='department',
|
||||
right_on='id'
|
||||
)
|
||||
|
||||
# Drop duplicate column after merge
|
||||
result = result.drop('id', axis=1)
|
||||
```
|
||||
|
||||
### Merging on Multiple Columns
|
||||
|
||||
```python
|
||||
sales = pd.DataFrame({
|
||||
'region': ['East', 'East', 'West', 'West'],
|
||||
'product': ['A', 'B', 'A', 'B'],
|
||||
'sales': [100, 150, 120, 180],
|
||||
})
|
||||
|
||||
targets = pd.DataFrame({
|
||||
'region': ['East', 'East', 'West'],
|
||||
'product': ['A', 'B', 'A'],
|
||||
'target': [90, 140, 110],
|
||||
})
|
||||
|
||||
# Merge on multiple columns
|
||||
result = pd.merge(sales, targets, on=['region', 'product'], how='left')
|
||||
```
|
||||
|
||||
### Merging on Index
|
||||
|
||||
```python
|
||||
# Set index before merge
|
||||
employees_idx = employees.set_index('emp_id')
|
||||
salaries = pd.DataFrame({
|
||||
'emp_id': [1, 2, 3, 4],
|
||||
'salary': [80000, 75000, 70000, 65000],
|
||||
}).set_index('emp_id')
|
||||
|
||||
# Merge on index
|
||||
result = pd.merge(employees_idx, salaries, left_index=True, right_index=True)
|
||||
|
||||
# Mix of column and index
|
||||
result = pd.merge(
|
||||
employees,
|
||||
salaries,
|
||||
left_on='emp_id',
|
||||
right_index=True
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Handling Duplicate Columns
|
||||
|
||||
### Suffixes
|
||||
|
||||
```python
|
||||
df1 = pd.DataFrame({
|
||||
'id': [1, 2, 3],
|
||||
'value': [10, 20, 30],
|
||||
'date': ['2024-01-01', '2024-01-02', '2024-01-03'],
|
||||
})
|
||||
|
||||
df2 = pd.DataFrame({
|
||||
'id': [1, 2, 3],
|
||||
'value': [100, 200, 300],
|
||||
'date': ['2024-02-01', '2024-02-02', '2024-02-03'],
|
||||
})
|
||||
|
||||
# Default suffixes
|
||||
result = pd.merge(df1, df2, on='id')
|
||||
# Columns: id, value_x, date_x, value_y, date_y
|
||||
|
||||
# Custom suffixes
|
||||
result = pd.merge(df1, df2, on='id', suffixes=('_jan', '_feb'))
|
||||
# Columns: id, value_jan, date_jan, value_feb, date_feb
|
||||
```
|
||||
|
||||
### Validate Merge Cardinality
|
||||
|
||||
```python
|
||||
# Validate merge relationships (pandas 2.0+)
|
||||
# Raises MergeError if validation fails
|
||||
|
||||
# One-to-one: each key appears at most once in both DataFrames
|
||||
result = pd.merge(df1, df2, on='id', validate='one_to_one') # or '1:1'
|
||||
|
||||
# One-to-many: keys unique in left only
|
||||
result = pd.merge(employees, salaries, on='emp_id', validate='one_to_many') # or '1:m'
|
||||
|
||||
# Many-to-one: keys unique in right only
|
||||
result = pd.merge(salaries, employees, on='emp_id', validate='many_to_one') # or 'm:1'
|
||||
|
||||
# Many-to-many: no uniqueness requirement (default)
|
||||
result = pd.merge(df1, df2, on='id', validate='many_to_many') # or 'm:m'
|
||||
```
|
||||
|
||||
### Indicator Column
|
||||
|
||||
```python
|
||||
# Add indicator column showing source of each row
|
||||
result = pd.merge(
|
||||
employees,
|
||||
departments,
|
||||
on='dept_id',
|
||||
how='outer',
|
||||
indicator=True
|
||||
)
|
||||
# _merge column values: 'left_only', 'right_only', 'both'
|
||||
|
||||
# Custom indicator name
|
||||
result = pd.merge(
|
||||
employees,
|
||||
departments,
|
||||
on='dept_id',
|
||||
how='outer',
|
||||
indicator='source'
|
||||
)
|
||||
|
||||
# Filter by indicator
|
||||
left_only = result[result['_merge'] == 'left_only']
|
||||
both = result[result['_merge'] == 'both']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Join (Index-Based)
|
||||
|
||||
### DataFrame.join()
|
||||
|
||||
```python
|
||||
# join() is for index-based joining (simpler syntax)
|
||||
employees = pd.DataFrame({
|
||||
'name': ['Alice', 'Bob', 'Charlie'],
|
||||
'dept_id': [101, 102, 101],
|
||||
}, index=[1, 2, 3])
|
||||
|
||||
salaries = pd.DataFrame({
|
||||
'salary': [80000, 75000, 70000],
|
||||
'bonus': [5000, 4000, 3500],
|
||||
}, index=[1, 2, 3])
|
||||
|
||||
# Join on index
|
||||
result = employees.join(salaries)
|
||||
|
||||
# Join types (same as merge)
|
||||
result = employees.join(salaries, how='left')
|
||||
result = employees.join(salaries, how='outer')
|
||||
```
|
||||
|
||||
### Join on Column to Index
|
||||
|
||||
```python
|
||||
employees = pd.DataFrame({
|
||||
'name': ['Alice', 'Bob', 'Charlie'],
|
||||
'dept_id': [101, 102, 101],
|
||||
})
|
||||
|
||||
departments = pd.DataFrame({
|
||||
'dept_name': ['Engineering', 'Sales'],
|
||||
}, index=[101, 102])
|
||||
|
||||
# Join left column to right index
|
||||
result = employees.join(departments, on='dept_id')
|
||||
```
|
||||
|
||||
### Join Multiple DataFrames
|
||||
|
||||
```python
|
||||
df1 = pd.DataFrame({'a': [1, 2]}, index=['x', 'y'])
|
||||
df2 = pd.DataFrame({'b': [3, 4]}, index=['x', 'y'])
|
||||
df3 = pd.DataFrame({'c': [5, 6]}, index=['x', 'y'])
|
||||
|
||||
# Join multiple at once
|
||||
result = df1.join([df2, df3])
|
||||
|
||||
# With suffixes for duplicate columns
|
||||
result = df1.join([df2, df3], lsuffix='_1', rsuffix='_2')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Concat (Stacking DataFrames)
|
||||
|
||||
### Vertical Concatenation (Row-wise)
|
||||
|
||||
```python
|
||||
# Stack DataFrames vertically
|
||||
df1 = pd.DataFrame({
|
||||
'name': ['Alice', 'Bob'],
|
||||
'age': [25, 30],
|
||||
})
|
||||
|
||||
df2 = pd.DataFrame({
|
||||
'name': ['Charlie', 'Diana'],
|
||||
'age': [35, 28],
|
||||
})
|
||||
|
||||
# Basic concat (axis=0 is default)
|
||||
result = pd.concat([df1, df2])
|
||||
|
||||
# Reset index
|
||||
result = pd.concat([df1, df2], ignore_index=True)
|
||||
|
||||
# Keep track of source
|
||||
result = pd.concat([df1, df2], keys=['source1', 'source2'])
|
||||
# Creates MultiIndex
|
||||
```
|
||||
|
||||
### Horizontal Concatenation (Column-wise)
|
||||
|
||||
```python
|
||||
names = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie']})
|
||||
ages = pd.DataFrame({'age': [25, 30, 35]})
|
||||
salaries = pd.DataFrame({'salary': [50000, 60000, 70000]})
|
||||
|
||||
# Concat columns (axis=1)
|
||||
result = pd.concat([names, ages, salaries], axis=1)
|
||||
```
|
||||
|
||||
### Handling Mismatched Columns
|
||||
|
||||
```python
|
||||
df1 = pd.DataFrame({
|
||||
'name': ['Alice', 'Bob'],
|
||||
'age': [25, 30],
|
||||
})
|
||||
|
||||
df2 = pd.DataFrame({
|
||||
'name': ['Charlie', 'Diana'],
|
||||
'salary': [70000, 65000],
|
||||
})
|
||||
|
||||
# Outer join (default) - include all columns
|
||||
result = pd.concat([df1, df2])
|
||||
# age and salary columns have NaN where not present
|
||||
|
||||
# Inner join - only common columns
|
||||
result = pd.concat([df1, df2], join='inner')
|
||||
# Only 'name' column
|
||||
```
|
||||
|
||||
### Concat with Verification
|
||||
|
||||
```python
|
||||
# Verify no index overlap
|
||||
try:
|
||||
result = pd.concat([df1, df2], verify_integrity=True)
|
||||
except ValueError as e:
|
||||
print(f"Index overlap detected: {e}")
|
||||
|
||||
# Alternative: use ignore_index
|
||||
result = pd.concat([df1, df2], ignore_index=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Combine and Update
|
||||
|
||||
### combine_first() - Fill Gaps
|
||||
|
||||
```python
|
||||
# Fill NaN values from another DataFrame
|
||||
df1 = pd.DataFrame({
|
||||
'A': [1, np.nan, 3],
|
||||
'B': [np.nan, 2, 3],
|
||||
}, index=['a', 'b', 'c'])
|
||||
|
||||
df2 = pd.DataFrame({
|
||||
'A': [10, 20, 30],
|
||||
'B': [10, 20, 30],
|
||||
}, index=['a', 'b', 'c'])
|
||||
|
||||
# Fill NaN in df1 with values from df2
|
||||
result = df1.combine_first(df2)
|
||||
# A: [1, 20, 3], B: [10, 2, 3]
|
||||
```
|
||||
|
||||
### update() - In-Place Update
|
||||
|
||||
```python
|
||||
df1 = pd.DataFrame({
|
||||
'A': [1, 2, 3],
|
||||
'B': [4, 5, 6],
|
||||
}, index=['a', 'b', 'c'])
|
||||
|
||||
df2 = pd.DataFrame({
|
||||
'A': [10, 20],
|
||||
'B': [40, 50],
|
||||
}, index=['a', 'b'])
|
||||
|
||||
# Update df1 with values from df2 (in-place)
|
||||
df1.update(df2)
|
||||
# df1 now has A: [10, 20, 3], B: [40, 50, 6]
|
||||
|
||||
# Only update where df2 has non-NaN
|
||||
df1.update(df2, overwrite=False) # Don't overwrite existing values
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Merge Patterns
|
||||
|
||||
### Merge with Aggregation
|
||||
|
||||
```python
|
||||
# Merge and aggregate in one operation
|
||||
orders = pd.DataFrame({
|
||||
'order_id': [1, 2, 3, 4],
|
||||
'customer_id': [101, 102, 101, 103],
|
||||
'amount': [100, 200, 150, 300],
|
||||
})
|
||||
|
||||
customers = pd.DataFrame({
|
||||
'customer_id': [101, 102, 103],
|
||||
'name': ['Alice', 'Bob', 'Charlie'],
|
||||
})
|
||||
|
||||
# Get customer summary
|
||||
customer_summary = orders.groupby('customer_id').agg(
|
||||
total_orders=('order_id', 'count'),
|
||||
total_amount=('amount', 'sum'),
|
||||
).reset_index()
|
||||
|
||||
# Merge with customer info
|
||||
result = pd.merge(customers, customer_summary, on='customer_id')
|
||||
```
|
||||
|
||||
### Merge Asof (Nearest Match)
|
||||
|
||||
```python
|
||||
# Merge on nearest key (useful for time series)
|
||||
trades = pd.DataFrame({
|
||||
'time': pd.to_datetime(['2024-01-01 10:00:01', '2024-01-01 10:00:03', '2024-01-01 10:00:05']),
|
||||
'ticker': ['AAPL', 'AAPL', 'AAPL'],
|
||||
'price': [150.0, 151.0, 150.5],
|
||||
})
|
||||
|
||||
quotes = pd.DataFrame({
|
||||
'time': pd.to_datetime(['2024-01-01 10:00:00', '2024-01-01 10:00:02', '2024-01-01 10:00:04']),
|
||||
'ticker': ['AAPL', 'AAPL', 'AAPL'],
|
||||
'bid': [149.5, 150.5, 150.0],
|
||||
'ask': [150.5, 151.5, 151.0],
|
||||
})
|
||||
|
||||
# Merge asof - find nearest quote for each trade
|
||||
result = pd.merge_asof(
|
||||
trades.sort_values('time'),
|
||||
quotes.sort_values('time'),
|
||||
on='time',
|
||||
by='ticker',
|
||||
direction='backward' # Use most recent quote
|
||||
)
|
||||
```
|
||||
|
||||
### Conditional Merge
|
||||
|
||||
```python
|
||||
# Merge with conditions beyond key equality
|
||||
# First merge, then filter
|
||||
|
||||
products = pd.DataFrame({
|
||||
'product_id': [1, 2, 3],
|
||||
'name': ['Widget', 'Gadget', 'Gizmo'],
|
||||
'category': ['A', 'B', 'A'],
|
||||
})
|
||||
|
||||
discounts = pd.DataFrame({
|
||||
'category': ['A', 'A', 'B'],
|
||||
'min_qty': [10, 50, 20],
|
||||
'discount': [0.05, 0.10, 0.08],
|
||||
})
|
||||
|
||||
# Cross merge then filter
|
||||
merged = pd.merge(products, discounts, on='category')
|
||||
# Then apply quantity-based filtering as needed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Pre-sorting for Merge
|
||||
|
||||
```python
|
||||
# Sort keys before merge for better performance
|
||||
df1 = df1.sort_values('key')
|
||||
df2 = df2.sort_values('key')
|
||||
|
||||
# Merge sorted DataFrames
|
||||
result = pd.merge(df1, df2, on='key')
|
||||
```
|
||||
|
||||
### Index Alignment
|
||||
|
||||
```python
|
||||
# Using index for merge is often faster than columns
|
||||
df1 = df1.set_index('key')
|
||||
df2 = df2.set_index('key')
|
||||
|
||||
# Join on index
|
||||
result = df1.join(df2)
|
||||
```
|
||||
|
||||
### Memory-Efficient Merge
|
||||
|
||||
```python
|
||||
# For large DataFrames, reduce memory before merge
|
||||
# Convert to appropriate types
|
||||
df1['key'] = df1['key'].astype('int32') # Instead of int64
|
||||
df1['category'] = df1['category'].astype('category')
|
||||
|
||||
# Select only needed columns
|
||||
cols_needed = ['key', 'value1', 'value2']
|
||||
result = pd.merge(df1[cols_needed], df2[cols_needed], on='key')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Merge Patterns
|
||||
|
||||
### Left Join with Null Check
|
||||
|
||||
```python
|
||||
# Find unmatched rows after left join
|
||||
result = pd.merge(employees, departments, on='dept_id', how='left')
|
||||
unmatched = result[result['dept_name'].isna()]
|
||||
```
|
||||
|
||||
### Anti-Join (Rows Not in Other)
|
||||
|
||||
```python
|
||||
# Find employees NOT in a specific department list
|
||||
dept_list = [101, 102]
|
||||
|
||||
# Method 1: Using isin
|
||||
not_in_depts = employees[~employees['dept_id'].isin(dept_list)]
|
||||
|
||||
# Method 2: Using merge with indicator
|
||||
merged = pd.merge(
|
||||
employees,
|
||||
pd.DataFrame({'dept_id': dept_list}),
|
||||
on='dept_id',
|
||||
how='left',
|
||||
indicator=True
|
||||
)
|
||||
not_in_depts = merged[merged['_merge'] == 'left_only']
|
||||
```
|
||||
|
||||
### Self-Join
|
||||
|
||||
```python
|
||||
# Find pairs within same department
|
||||
employees = pd.DataFrame({
|
||||
'emp_id': [1, 2, 3, 4],
|
||||
'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
|
||||
'dept_id': [101, 101, 102, 101],
|
||||
})
|
||||
|
||||
# Self-join to find pairs
|
||||
pairs = pd.merge(
|
||||
employees,
|
||||
employees,
|
||||
on='dept_id',
|
||||
suffixes=('_1', '_2')
|
||||
)
|
||||
# Remove self-pairs and duplicates
|
||||
pairs = pairs[pairs['emp_id_1'] < pairs['emp_id_2']]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Choose the right join type** - Default inner may drop data
|
||||
2. **Validate cardinality** - Use `validate` parameter
|
||||
3. **Use indicator** - Debug unexpected results
|
||||
4. **Handle duplicates** - Use meaningful suffixes
|
||||
5. **Pre-sort for performance** - Especially for large DataFrames
|
||||
6. **Reset index after operations** - Keep DataFrames usable
|
||||
7. **Check for NaN after join** - Understand unmatched rows
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
```python
|
||||
# BAD: Merge without understanding cardinality
|
||||
result = pd.merge(df1, df2, on='key') # May explode row count
|
||||
|
||||
# GOOD: Validate relationship
|
||||
result = pd.merge(df1, df2, on='key', validate='one_to_one')
|
||||
|
||||
# BAD: Repeated merges
|
||||
result = pd.merge(df1, df2, on='key')
|
||||
result = pd.merge(result, df3, on='key')
|
||||
result = pd.merge(result, df4, on='key')
|
||||
|
||||
# GOOD: Chain or use reduce
|
||||
from functools import reduce
|
||||
dfs = [df1, df2, df3, df4]
|
||||
result = reduce(lambda left, right: pd.merge(left, right, on='key'), dfs)
|
||||
|
||||
# BAD: Ignoring merge indicators
|
||||
result = pd.merge(df1, df2, on='key', how='outer')
|
||||
|
||||
# GOOD: Check merge results
|
||||
result = pd.merge(df1, df2, on='key', how='outer', indicator=True)
|
||||
print(result['_merge'].value_counts())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related References
|
||||
|
||||
- `dataframe-operations.md` - Filter before/after merge
|
||||
- `aggregation-groupby.md` - Aggregate before merging
|
||||
- `performance-optimization.md` - Optimize large merges
|
||||
@@ -0,0 +1,597 @@
|
||||
# Performance Optimization
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Optimizing pandas performance is critical for production workflows. This reference covers memory optimization, vectorization, chunking, and profiling with pandas 2.0+.
|
||||
|
||||
---
|
||||
|
||||
## Memory Analysis
|
||||
|
||||
### Checking Memory Usage
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
df = pd.DataFrame({
|
||||
'id': range(1_000_000),
|
||||
'name': ['user_' + str(i) for i in range(1_000_000)],
|
||||
'category': np.random.choice(['A', 'B', 'C', 'D'], 1_000_000),
|
||||
'value': np.random.randn(1_000_000),
|
||||
'count': np.random.randint(0, 100, 1_000_000),
|
||||
})
|
||||
|
||||
# Basic memory info
|
||||
print(df.info(memory_usage='deep'))
|
||||
|
||||
# Detailed memory by column
|
||||
memory_usage = df.memory_usage(deep=True)
|
||||
print(memory_usage)
|
||||
print(f"Total: {memory_usage.sum() / 1e6:.2f} MB")
|
||||
|
||||
# Memory as percentage of total
|
||||
memory_pct = (memory_usage / memory_usage.sum() * 100).round(2)
|
||||
print(memory_pct)
|
||||
```
|
||||
|
||||
### Memory Profiling Function
|
||||
|
||||
```python
|
||||
def memory_profile(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Profile memory usage by column with optimization suggestions."""
|
||||
memory_bytes = df.memory_usage(deep=True)
|
||||
|
||||
profile = pd.DataFrame({
|
||||
'dtype': df.dtypes,
|
||||
'non_null': df.count(),
|
||||
'null_count': df.isna().sum(),
|
||||
'unique': df.nunique(),
|
||||
'memory_mb': (memory_bytes / 1e6).round(3),
|
||||
})
|
||||
|
||||
# Add optimization suggestions
|
||||
suggestions = []
|
||||
for col in df.columns:
|
||||
dtype = df[col].dtype
|
||||
nunique = df[col].nunique()
|
||||
|
||||
if dtype == 'object':
|
||||
if nunique / len(df) < 0.5: # Less than 50% unique
|
||||
suggestions.append(f"Convert to category (only {nunique} unique)")
|
||||
else:
|
||||
suggestions.append("Consider string dtype")
|
||||
elif dtype == 'int64':
|
||||
if df[col].max() < 2**31 and df[col].min() >= -2**31:
|
||||
suggestions.append("Downcast to int32")
|
||||
if df[col].max() < 2**15 and df[col].min() >= -2**15:
|
||||
suggestions.append("Downcast to int16")
|
||||
elif dtype == 'float64':
|
||||
suggestions.append("Consider float32 if precision allows")
|
||||
else:
|
||||
suggestions.append("OK")
|
||||
|
||||
profile['suggestion'] = suggestions
|
||||
return profile
|
||||
|
||||
print(memory_profile(df))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Optimization Techniques
|
||||
|
||||
### Downcasting Numeric Types
|
||||
|
||||
```python
|
||||
# Automatic downcasting for integers
|
||||
df['count'] = pd.to_numeric(df['count'], downcast='integer')
|
||||
|
||||
# Automatic downcasting for floats
|
||||
df['value'] = pd.to_numeric(df['value'], downcast='float')
|
||||
|
||||
# Manual downcasting function
|
||||
def downcast_dtypes(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Reduce memory by downcasting numeric types."""
|
||||
df = df.copy()
|
||||
|
||||
for col in df.select_dtypes(include=['int']).columns:
|
||||
df[col] = pd.to_numeric(df[col], downcast='integer')
|
||||
|
||||
for col in df.select_dtypes(include=['float']).columns:
|
||||
df[col] = pd.to_numeric(df[col], downcast='float')
|
||||
|
||||
return df
|
||||
|
||||
df_optimized = downcast_dtypes(df)
|
||||
print(f"Before: {df.memory_usage(deep=True).sum() / 1e6:.2f} MB")
|
||||
print(f"After: {df_optimized.memory_usage(deep=True).sum() / 1e6:.2f} MB")
|
||||
```
|
||||
|
||||
### Using Categorical Type
|
||||
|
||||
```python
|
||||
# Convert low-cardinality string columns to category
|
||||
# Especially effective when unique values << total rows
|
||||
|
||||
# Before
|
||||
print(f"Object dtype: {df['category'].memory_usage(deep=True) / 1e6:.2f} MB")
|
||||
|
||||
# After
|
||||
df['category'] = df['category'].astype('category')
|
||||
print(f"Category dtype: {df['category'].memory_usage(deep=True) / 1e6:.2f} MB")
|
||||
|
||||
# Automatic conversion for low-cardinality columns
|
||||
def optimize_categories(df: pd.DataFrame, threshold: float = 0.5) -> pd.DataFrame:
|
||||
"""Convert object columns to category if unique ratio < threshold."""
|
||||
df = df.copy()
|
||||
|
||||
for col in df.select_dtypes(include=['object']).columns:
|
||||
unique_ratio = df[col].nunique() / len(df)
|
||||
if unique_ratio < threshold:
|
||||
df[col] = df[col].astype('category')
|
||||
|
||||
return df
|
||||
```
|
||||
|
||||
### Sparse Data Types
|
||||
|
||||
```python
|
||||
# For data with many repeated values (especially zeros/NaN)
|
||||
sparse_series = pd.arrays.SparseArray([0, 0, 1, 0, 0, 0, 2, 0, 0, 0])
|
||||
|
||||
# Create sparse DataFrame
|
||||
df_sparse = pd.DataFrame({
|
||||
'sparse_col': pd.arrays.SparseArray([0] * 9000 + [1] * 1000),
|
||||
'dense_col': [0] * 9000 + [1] * 1000,
|
||||
})
|
||||
|
||||
print(f"Sparse: {df_sparse['sparse_col'].memory_usage() / 1e6:.4f} MB")
|
||||
print(f"Dense: {df_sparse['dense_col'].memory_usage() / 1e6:.4f} MB")
|
||||
```
|
||||
|
||||
### Nullable Types (pandas 2.0+)
|
||||
|
||||
```python
|
||||
# Use nullable types for proper NA handling with memory efficiency
|
||||
df = df.astype({
|
||||
'id': 'Int32', # Nullable int32
|
||||
'count': 'Int16', # Nullable int16
|
||||
'value': 'Float32', # Nullable float32
|
||||
'name': 'string', # Nullable string (more memory efficient)
|
||||
'category': 'category', # Categorical
|
||||
})
|
||||
|
||||
# Arrow-backed types for even better memory (pandas 2.0+)
|
||||
df['name'] = df['name'].astype('string[pyarrow]')
|
||||
df['category'] = df['category'].astype('category')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vectorization
|
||||
|
||||
### Replace Loops with Vectorized Operations
|
||||
|
||||
```python
|
||||
# BAD: Row iteration (extremely slow)
|
||||
result = []
|
||||
for idx, row in df.iterrows():
|
||||
if row['value'] > 0:
|
||||
result.append(row['value'] * 2)
|
||||
else:
|
||||
result.append(0)
|
||||
df['result'] = result
|
||||
|
||||
# GOOD: Vectorized with np.where
|
||||
df['result'] = np.where(df['value'] > 0, df['value'] * 2, 0)
|
||||
|
||||
# GOOD: Vectorized with boolean indexing
|
||||
df['result'] = 0
|
||||
df.loc[df['value'] > 0, 'result'] = df.loc[df['value'] > 0, 'value'] * 2
|
||||
```
|
||||
|
||||
### Multiple Conditions with np.select
|
||||
|
||||
```python
|
||||
# BAD: Nested if-else in apply
|
||||
def categorize(row):
|
||||
if row['value'] < -1:
|
||||
return 'very_low'
|
||||
elif row['value'] < 0:
|
||||
return 'low'
|
||||
elif row['value'] < 1:
|
||||
return 'medium'
|
||||
else:
|
||||
return 'high'
|
||||
|
||||
df['category'] = df.apply(categorize, axis=1) # SLOW!
|
||||
|
||||
# GOOD: Vectorized with np.select
|
||||
conditions = [
|
||||
df['value'] < -1,
|
||||
df['value'] < 0,
|
||||
df['value'] < 1,
|
||||
]
|
||||
choices = ['very_low', 'low', 'medium']
|
||||
df['category'] = np.select(conditions, choices, default='high')
|
||||
```
|
||||
|
||||
### String Operations - Vectorized
|
||||
|
||||
```python
|
||||
# BAD: Apply for string operations
|
||||
df['upper_name'] = df['name'].apply(lambda x: x.upper())
|
||||
|
||||
# GOOD: Vectorized string methods
|
||||
df['upper_name'] = df['name'].str.upper()
|
||||
|
||||
# Combine multiple string operations
|
||||
df['processed'] = (
|
||||
df['name']
|
||||
.str.strip()
|
||||
.str.lower()
|
||||
.str.replace(r'\s+', '_', regex=True)
|
||||
)
|
||||
```
|
||||
|
||||
### Avoid apply() When Possible
|
||||
|
||||
```python
|
||||
# BAD: apply for row-wise calculation
|
||||
df['total'] = df.apply(lambda row: row['a'] + row['b'] + row['c'], axis=1)
|
||||
|
||||
# GOOD: Direct vectorized operation
|
||||
df['total'] = df['a'] + df['b'] + df['c']
|
||||
|
||||
# BAD: apply for element-wise operation
|
||||
df['squared'] = df['value'].apply(lambda x: x ** 2)
|
||||
|
||||
# GOOD: Vectorized
|
||||
df['squared'] = df['value'] ** 2
|
||||
|
||||
# When apply IS appropriate: complex custom logic
|
||||
def complex_calculation(row):
|
||||
# Multiple dependencies and conditional logic
|
||||
if row['type'] == 'A':
|
||||
return row['value'] * row['multiplier'] + row['offset']
|
||||
else:
|
||||
return row['value'] / row['divisor'] - row['adjustment']
|
||||
|
||||
# Consider rewriting as vectorized if performance critical
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunked Processing
|
||||
|
||||
### Reading Large Files in Chunks
|
||||
|
||||
```python
|
||||
# Read CSV in chunks
|
||||
chunk_size = 100_000
|
||||
chunks = []
|
||||
|
||||
for chunk in pd.read_csv('large_file.csv', chunksize=chunk_size):
|
||||
# Process each chunk
|
||||
processed = chunk[chunk['value'] > 0] # Filter
|
||||
processed = processed.groupby('category')['value'].sum() # Aggregate
|
||||
chunks.append(processed)
|
||||
|
||||
# Combine results
|
||||
result = pd.concat(chunks).groupby(level=0).sum()
|
||||
```
|
||||
|
||||
### Chunked Processing Function
|
||||
|
||||
```python
|
||||
def process_large_csv(
|
||||
filepath: str,
|
||||
chunk_size: int = 100_000,
|
||||
filter_func=None,
|
||||
agg_func=None,
|
||||
) -> pd.DataFrame:
|
||||
"""Process large CSV files in chunks."""
|
||||
results = []
|
||||
|
||||
for chunk in pd.read_csv(filepath, chunksize=chunk_size):
|
||||
# Apply filter if provided
|
||||
if filter_func:
|
||||
chunk = filter_func(chunk)
|
||||
|
||||
# Apply aggregation if provided
|
||||
if agg_func:
|
||||
chunk = agg_func(chunk)
|
||||
|
||||
results.append(chunk)
|
||||
|
||||
# Combine results
|
||||
combined = pd.concat(results, ignore_index=True)
|
||||
|
||||
# Re-aggregate if needed
|
||||
if agg_func:
|
||||
combined = agg_func(combined)
|
||||
|
||||
return combined
|
||||
|
||||
# Usage
|
||||
result = process_large_csv(
|
||||
'large_file.csv',
|
||||
chunk_size=50_000,
|
||||
filter_func=lambda df: df[df['value'] > 0],
|
||||
agg_func=lambda df: df.groupby('category').agg({'value': 'sum'}),
|
||||
)
|
||||
```
|
||||
|
||||
### Memory-Efficient Iteration
|
||||
|
||||
```python
|
||||
# When you must iterate, use itertuples (not iterrows)
|
||||
# itertuples is 10-100x faster than iterrows
|
||||
|
||||
# BAD: iterrows
|
||||
for idx, row in df.iterrows():
|
||||
process(row['name'], row['value'])
|
||||
|
||||
# BETTER: itertuples
|
||||
for row in df.itertuples():
|
||||
process(row.name, row.value) # Access as attributes
|
||||
|
||||
# BEST: Vectorized operations (avoid iteration entirely)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Query Optimization
|
||||
|
||||
### Efficient Filtering
|
||||
|
||||
```python
|
||||
# Order matters - filter early, compute late
|
||||
# BAD: Compute on all rows, then filter
|
||||
df['expensive_calc'] = df['a'] * df['b'] + np.sin(df['c'])
|
||||
result = df[df['category'] == 'A']
|
||||
|
||||
# GOOD: Filter first, compute on subset
|
||||
mask = df['category'] == 'A'
|
||||
result = df[mask].copy()
|
||||
result['expensive_calc'] = result['a'] * result['b'] + np.sin(result['c'])
|
||||
```
|
||||
|
||||
### Using query() for Performance
|
||||
|
||||
```python
|
||||
# query() can be faster for large DataFrames (uses numexpr)
|
||||
# Traditional boolean indexing
|
||||
result = df[(df['value'] > 0) & (df['category'] == 'A')]
|
||||
|
||||
# query() syntax (faster for large data)
|
||||
result = df.query('value > 0 and category == "A"')
|
||||
|
||||
# With variables
|
||||
threshold = 0
|
||||
cat = 'A'
|
||||
result = df.query('value > @threshold and category == @cat')
|
||||
```
|
||||
|
||||
### eval() for Complex Expressions
|
||||
|
||||
```python
|
||||
# eval() uses numexpr for faster computation
|
||||
# Standard pandas
|
||||
df['result'] = df['a'] + df['b'] * df['c'] - df['d']
|
||||
|
||||
# Using eval (faster for large DataFrames)
|
||||
df['result'] = pd.eval('df.a + df.b * df.c - df.d')
|
||||
|
||||
# In-place with inplace parameter
|
||||
df.eval('result = a + b * c - d', inplace=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GroupBy Optimization
|
||||
|
||||
### Pre-sort for Faster GroupBy
|
||||
|
||||
```python
|
||||
# Sort by groupby column first
|
||||
df = df.sort_values('category')
|
||||
|
||||
# Use sort=False since already sorted
|
||||
result = df.groupby('category', sort=False)['value'].mean()
|
||||
```
|
||||
|
||||
### Use Built-in Aggregations
|
||||
|
||||
```python
|
||||
# BAD: Custom function via apply
|
||||
result = df.groupby('category')['value'].apply(lambda x: x.mean())
|
||||
|
||||
# GOOD: Built-in aggregation
|
||||
result = df.groupby('category')['value'].mean()
|
||||
|
||||
# Built-in aggregations available:
|
||||
# sum, mean, median, min, max, std, var, count, first, last, nth
|
||||
# size, sem, prod, cumsum, cummax, cummin, cumprod
|
||||
```
|
||||
|
||||
### Observed Categories
|
||||
|
||||
```python
|
||||
# For categorical columns, use observed=True (pandas 2.0+ default)
|
||||
df['category'] = df['category'].astype('category')
|
||||
|
||||
# Avoid computing for unobserved categories
|
||||
result = df.groupby('category', observed=True)['value'].mean()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## I/O Optimization
|
||||
|
||||
### Efficient File Formats
|
||||
|
||||
```python
|
||||
# Parquet - best for analytical workloads
|
||||
df.to_parquet('data.parquet', compression='snappy')
|
||||
df = pd.read_parquet('data.parquet')
|
||||
|
||||
# Feather - best for pandas interchange
|
||||
df.to_feather('data.feather')
|
||||
df = pd.read_feather('data.feather')
|
||||
|
||||
# CSV with optimizations
|
||||
df.to_csv('data.csv', index=False)
|
||||
df = pd.read_csv(
|
||||
'data.csv',
|
||||
dtype={'category': 'category', 'count': 'int32'},
|
||||
usecols=['id', 'category', 'value'], # Only needed columns
|
||||
nrows=10000, # Limit rows for testing
|
||||
)
|
||||
```
|
||||
|
||||
### Specify dtypes When Reading
|
||||
|
||||
```python
|
||||
# Specify dtypes upfront to avoid inference overhead
|
||||
dtypes = {
|
||||
'id': 'int32',
|
||||
'name': 'string',
|
||||
'category': 'category',
|
||||
'value': 'float32',
|
||||
'count': 'int16',
|
||||
}
|
||||
|
||||
df = pd.read_csv('data.csv', dtype=dtypes)
|
||||
|
||||
# Parse dates efficiently
|
||||
df = pd.read_csv(
|
||||
'data.csv',
|
||||
dtype=dtypes,
|
||||
parse_dates=['date_column'],
|
||||
date_format='%Y-%m-%d', # Explicit format is faster
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Profiling and Benchmarking
|
||||
|
||||
### Timing Operations
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
# Simple timing
|
||||
start = time.time()
|
||||
result = df.groupby('category')['value'].mean()
|
||||
elapsed = time.time() - start
|
||||
print(f"Elapsed: {elapsed:.4f} seconds")
|
||||
|
||||
# Using %%timeit in Jupyter
|
||||
# %%timeit
|
||||
# df.groupby('category')['value'].mean()
|
||||
```
|
||||
|
||||
### Memory Profiling
|
||||
|
||||
```python
|
||||
# Track memory before/after
|
||||
import tracemalloc
|
||||
|
||||
tracemalloc.start()
|
||||
|
||||
# Your operation
|
||||
df_result = df.groupby('category').agg({'value': 'sum'})
|
||||
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
print(f"Current memory: {current / 1e6:.2f} MB")
|
||||
print(f"Peak memory: {peak / 1e6:.2f} MB")
|
||||
|
||||
tracemalloc.stop()
|
||||
```
|
||||
|
||||
### Comparison Template
|
||||
|
||||
```python
|
||||
def benchmark_operations(df: pd.DataFrame, operations: dict, n_runs: int = 5):
|
||||
"""Benchmark multiple operations."""
|
||||
results = {}
|
||||
|
||||
for name, func in operations.items():
|
||||
times = []
|
||||
for _ in range(n_runs):
|
||||
start = time.time()
|
||||
func(df)
|
||||
times.append(time.time() - start)
|
||||
|
||||
results[name] = {
|
||||
'mean': np.mean(times),
|
||||
'std': np.std(times),
|
||||
'min': np.min(times),
|
||||
}
|
||||
|
||||
return pd.DataFrame(results).T
|
||||
|
||||
# Usage
|
||||
operations = {
|
||||
'iterrows': lambda df: [row['value'] for _, row in df.iterrows()],
|
||||
'itertuples': lambda df: [row.value for row in df.itertuples()],
|
||||
'vectorized': lambda df: df['value'].tolist(),
|
||||
}
|
||||
|
||||
benchmark_results = benchmark_operations(df.head(10000), operations)
|
||||
print(benchmark_results)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Profile first** - Identify actual bottlenecks before optimizing
|
||||
2. **Use appropriate dtypes** - int32/float32/category save memory
|
||||
3. **Vectorize everything** - Avoid loops and apply when possible
|
||||
4. **Filter early** - Reduce data before expensive operations
|
||||
5. **Chunk large files** - Process in manageable pieces
|
||||
6. **Use efficient file formats** - Parquet/Feather over CSV
|
||||
7. **Leverage built-in methods** - Faster than custom functions
|
||||
|
||||
---
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
Before deploying pandas code:
|
||||
|
||||
- [ ] Memory profiled with `memory_usage(deep=True)`
|
||||
- [ ] Dtypes optimized (downcast, categorical)
|
||||
- [ ] No iterrows/itertuples in hot paths
|
||||
- [ ] GroupBy uses built-in aggregations
|
||||
- [ ] Large files processed in chunks
|
||||
- [ ] Filters applied before computations
|
||||
- [ ] Appropriate file format used
|
||||
- [ ] Benchmarked with representative data size
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns Summary
|
||||
|
||||
| Anti-Pattern | Alternative |
|
||||
|--------------|-------------|
|
||||
| `iterrows()` for computation | Vectorized operations |
|
||||
| `apply(lambda)` for simple ops | Built-in methods |
|
||||
| Loading entire large file | Chunked reading |
|
||||
| String columns with low cardinality | Category dtype |
|
||||
| int64 for small integers | int32/int16 |
|
||||
| Multiple separate filters | Combined boolean mask |
|
||||
| Repeated groupby calls | Single groupby with multiple aggs |
|
||||
|
||||
---
|
||||
|
||||
## Related References
|
||||
|
||||
- `dataframe-operations.md` - Efficient indexing and filtering
|
||||
- `aggregation-groupby.md` - Optimized aggregation patterns
|
||||
- `merging-joining.md` - Efficient merge strategies
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
name: python-executor
|
||||
description: "Execute Python code in a safe sandboxed environment via [inference.sh](https://inference.sh). Pre-installed: NumPy, Pandas, Matplotlib, requests, BeautifulSoup, Selenium, Playwright, MoviePy, Pillow, OpenCV, trimesh, and 100+ more libraries. Use for: data processing, web scraping, image manipulation, video creation, 3D model processing, PDF generation, API calls, automation scripts. Triggers: python, execute code, run script, web scraping, data analysis, image processing, video editing, 3D models, automation, pandas, matplotlib"
|
||||
allowed-tools: Bash(belt *)
|
||||
---
|
||||
|
||||
# Python Code Executor
|
||||
|
||||
Execute Python code in a safe, sandboxed environment with 100+ pre-installed libraries.
|
||||
|
||||

|
||||
|
||||
## Quick Start
|
||||
|
||||
> Requires inference.sh CLI (`belt`). [Install instructions](https://raw.githubusercontent.com/inference-sh/skills/refs/heads/main/cli-install.md)
|
||||
|
||||
```bash
|
||||
belt login
|
||||
|
||||
# Run Python code
|
||||
belt app run infsh/python-executor --input '{
|
||||
"code": "import pandas as pd\nprint(pd.__version__)"
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
## App Details
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| App ID | `infsh/python-executor` |
|
||||
| Environment | Python 3.10, CPU-only |
|
||||
| RAM | 8GB (default) / 16GB (high_memory) |
|
||||
| Timeout | 1-300 seconds (default: 30) |
|
||||
|
||||
## Input Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "print('Hello World!')",
|
||||
"timeout": 30,
|
||||
"capture_output": true,
|
||||
"working_dir": null
|
||||
}
|
||||
```
|
||||
|
||||
## Pre-installed Libraries
|
||||
|
||||
### Web Scraping & HTTP
|
||||
- `requests`, `httpx`, `aiohttp` - HTTP clients
|
||||
- `beautifulsoup4`, `lxml` - HTML/XML parsing
|
||||
- `selenium`, `playwright` - Browser automation
|
||||
- `scrapy` - Web scraping framework
|
||||
|
||||
### Data Processing
|
||||
- `numpy`, `pandas`, `scipy` - Numerical computing
|
||||
- `matplotlib`, `seaborn`, `plotly` - Visualization
|
||||
|
||||
### Image Processing
|
||||
- `pillow`, `opencv-python-headless` - Image manipulation
|
||||
- `scikit-image`, `imageio` - Image algorithms
|
||||
|
||||
### Video & Audio
|
||||
- `moviepy` - Video editing
|
||||
- `av` (PyAV), `ffmpeg-python` - Video processing
|
||||
- `pydub` - Audio manipulation
|
||||
|
||||
### 3D Processing
|
||||
- `trimesh`, `open3d` - 3D mesh processing
|
||||
- `numpy-stl`, `meshio`, `pyvista` - 3D file formats
|
||||
|
||||
### Documents & Graphics
|
||||
- `svgwrite`, `cairosvg` - SVG creation
|
||||
- `reportlab`, `pypdf2` - PDF generation
|
||||
|
||||
## Examples
|
||||
|
||||
### Web Scraping
|
||||
|
||||
```bash
|
||||
belt app run infsh/python-executor --input '{
|
||||
"code": "import requests\nfrom bs4 import BeautifulSoup\n\nresponse = requests.get(\"https://example.com\")\nsoup = BeautifulSoup(response.content, \"html.parser\")\nprint(soup.find(\"title\").text)"
|
||||
}'
|
||||
```
|
||||
|
||||
### Data Analysis with Visualization
|
||||
|
||||
```bash
|
||||
belt app run infsh/python-executor --input '{
|
||||
"code": "import pandas as pd\nimport matplotlib.pyplot as plt\n\ndata = {\"name\": [\"Alice\", \"Bob\"], \"sales\": [100, 150]}\ndf = pd.DataFrame(data)\n\nplt.bar(df[\"name\"], df[\"sales\"])\nplt.savefig(\"outputs/chart.png\")\nprint(\"Chart saved!\")"
|
||||
}'
|
||||
```
|
||||
|
||||
### Image Processing
|
||||
|
||||
```bash
|
||||
belt app run infsh/python-executor --input '{
|
||||
"code": "from PIL import Image\nimport numpy as np\n\n# Create gradient image\narr = np.linspace(0, 255, 256*256, dtype=np.uint8).reshape(256, 256)\nimg = Image.fromarray(arr, mode=\"L\")\nimg.save(\"outputs/gradient.png\")\nprint(\"Image created!\")"
|
||||
}'
|
||||
```
|
||||
|
||||
### Video Creation
|
||||
|
||||
```bash
|
||||
belt app run infsh/python-executor --input '{
|
||||
"code": "from moviepy.editor import ColorClip, TextClip, CompositeVideoClip\n\nclip = ColorClip(size=(640, 480), color=(0, 100, 200), duration=3)\ntxt = TextClip(\"Hello!\", fontsize=70, color=\"white\").set_position(\"center\").set_duration(3)\nvideo = CompositeVideoClip([clip, txt])\nvideo.write_videofile(\"outputs/hello.mp4\", fps=24)\nprint(\"Video created!\")",
|
||||
"timeout": 120
|
||||
}'
|
||||
```
|
||||
|
||||
### 3D Model Processing
|
||||
|
||||
```bash
|
||||
belt app run infsh/python-executor --input '{
|
||||
"code": "import trimesh\n\nsphere = trimesh.creation.icosphere(subdivisions=3, radius=1.0)\nsphere.export(\"outputs/sphere.stl\")\nprint(f\"Created sphere with {len(sphere.vertices)} vertices\")"
|
||||
}'
|
||||
```
|
||||
|
||||
### API Calls
|
||||
|
||||
```bash
|
||||
belt app run infsh/python-executor --input '{
|
||||
"code": "import requests\nimport json\n\nresponse = requests.get(\"https://api.github.com/users/octocat\")\ndata = response.json()\nprint(json.dumps(data, indent=2))"
|
||||
}'
|
||||
```
|
||||
|
||||
## File Output
|
||||
|
||||
Files saved to `outputs/` are automatically returned:
|
||||
|
||||
```python
|
||||
# These files will be in the response
|
||||
plt.savefig('outputs/chart.png')
|
||||
df.to_csv('outputs/data.csv')
|
||||
video.write_videofile('outputs/video.mp4')
|
||||
mesh.export('outputs/model.stl')
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
```bash
|
||||
# Default (8GB RAM)
|
||||
belt app run infsh/python-executor --input input.json
|
||||
|
||||
# High memory (16GB RAM) for large datasets
|
||||
belt app run infsh/python-executor@high_memory --input input.json
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Web scraping** - Extract data from websites
|
||||
- **Data analysis** - Process and visualize datasets
|
||||
- **Image manipulation** - Resize, crop, composite images
|
||||
- **Video creation** - Generate videos with text overlays
|
||||
- **3D processing** - Load, transform, export 3D models
|
||||
- **API integration** - Call external APIs
|
||||
- **PDF generation** - Create reports and documents
|
||||
- **Automation** - Run any Python script
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **CPU-only** - No GPU/ML libraries (use dedicated AI apps for that)
|
||||
- **Safe execution** - Runs in isolated subprocess
|
||||
- **Non-interactive** - Use `plt.savefig()` not `plt.show()`
|
||||
- **File detection** - Output files are auto-detected and returned
|
||||
|
||||
## Related Skills
|
||||
|
||||
```bash
|
||||
# AI image generation (for ML-based images)
|
||||
npx skills add inference-sh/skills@ai-image-generation
|
||||
|
||||
# AI video generation (for ML-based videos)
|
||||
npx skills add inference-sh/skills@ai-video-generation
|
||||
|
||||
# LLM models (for text generation)
|
||||
npx skills add inference-sh/skills@llm-models
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Running Apps](https://inference.sh/docs/apps/running) - How to run apps via CLI
|
||||
- [App Code](https://inference.sh/docs/extend/app-code) - Understanding app execution
|
||||
- [Sandboxed Code Execution](https://inference.sh/blog/tools/sandboxed-execution) - Safe code execution for agents
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
---
|
||||
name: python-testing-patterns
|
||||
description: Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.
|
||||
---
|
||||
|
||||
# Python Testing Patterns
|
||||
|
||||
Comprehensive guide to implementing robust testing strategies in Python using pytest, fixtures, mocking, parameterization, and test-driven development practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Writing unit tests for Python code
|
||||
- Setting up test suites and test infrastructure
|
||||
- Implementing test-driven development (TDD)
|
||||
- Creating integration tests for APIs and services
|
||||
- Mocking external dependencies and services
|
||||
- Testing async code and concurrent operations
|
||||
- Setting up continuous testing in CI/CD
|
||||
- Implementing property-based testing
|
||||
- Testing database operations
|
||||
- Debugging failing tests
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Test Types
|
||||
|
||||
- **Unit Tests**: Test individual functions/classes in isolation
|
||||
- **Integration Tests**: Test interaction between components
|
||||
- **Functional Tests**: Test complete features end-to-end
|
||||
- **Performance Tests**: Measure speed and resource usage
|
||||
|
||||
### 2. Test Structure (AAA Pattern)
|
||||
|
||||
- **Arrange**: Set up test data and preconditions
|
||||
- **Act**: Execute the code under test
|
||||
- **Assert**: Verify the results
|
||||
|
||||
### 3. Test Coverage
|
||||
|
||||
- Measure what code is exercised by tests
|
||||
- Identify untested code paths
|
||||
- Aim for meaningful coverage, not just high percentages
|
||||
|
||||
### 4. Test Isolation
|
||||
|
||||
- Tests should be independent
|
||||
- No shared state between tests
|
||||
- Each test should clean up after itself
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
# test_example.py
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
def test_add():
|
||||
"""Basic test example."""
|
||||
result = add(2, 3)
|
||||
assert result == 5
|
||||
|
||||
def test_add_negative():
|
||||
"""Test with negative numbers."""
|
||||
assert add(-1, 1) == 0
|
||||
|
||||
# Run with: pytest test_example.py
|
||||
```
|
||||
|
||||
## Fundamental Patterns
|
||||
|
||||
### Pattern 1: Basic pytest Tests
|
||||
|
||||
```python
|
||||
# test_calculator.py
|
||||
import pytest
|
||||
|
||||
class Calculator:
|
||||
"""Simple calculator for testing."""
|
||||
|
||||
def add(self, a: float, b: float) -> float:
|
||||
return a + b
|
||||
|
||||
def subtract(self, a: float, b: float) -> float:
|
||||
return a - b
|
||||
|
||||
def multiply(self, a: float, b: float) -> float:
|
||||
return a * b
|
||||
|
||||
def divide(self, a: float, b: float) -> float:
|
||||
if b == 0:
|
||||
raise ValueError("Cannot divide by zero")
|
||||
return a / b
|
||||
|
||||
|
||||
def test_addition():
|
||||
"""Test addition."""
|
||||
calc = Calculator()
|
||||
assert calc.add(2, 3) == 5
|
||||
assert calc.add(-1, 1) == 0
|
||||
assert calc.add(0, 0) == 0
|
||||
|
||||
|
||||
def test_subtraction():
|
||||
"""Test subtraction."""
|
||||
calc = Calculator()
|
||||
assert calc.subtract(5, 3) == 2
|
||||
assert calc.subtract(0, 5) == -5
|
||||
|
||||
|
||||
def test_multiplication():
|
||||
"""Test multiplication."""
|
||||
calc = Calculator()
|
||||
assert calc.multiply(3, 4) == 12
|
||||
assert calc.multiply(0, 5) == 0
|
||||
|
||||
|
||||
def test_division():
|
||||
"""Test division."""
|
||||
calc = Calculator()
|
||||
assert calc.divide(6, 3) == 2
|
||||
assert calc.divide(5, 2) == 2.5
|
||||
|
||||
|
||||
def test_division_by_zero():
|
||||
"""Test division by zero raises error."""
|
||||
calc = Calculator()
|
||||
with pytest.raises(ValueError, match="Cannot divide by zero"):
|
||||
calc.divide(5, 0)
|
||||
```
|
||||
|
||||
### Pattern 2: Fixtures for Setup and Teardown
|
||||
|
||||
```python
|
||||
# test_database.py
|
||||
import pytest
|
||||
from typing import Generator
|
||||
|
||||
class Database:
|
||||
"""Simple database class."""
|
||||
|
||||
def __init__(self, connection_string: str):
|
||||
self.connection_string = connection_string
|
||||
self.connected = False
|
||||
|
||||
def connect(self):
|
||||
"""Connect to database."""
|
||||
self.connected = True
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from database."""
|
||||
self.connected = False
|
||||
|
||||
def query(self, sql: str) -> list:
|
||||
"""Execute query."""
|
||||
if not self.connected:
|
||||
raise RuntimeError("Not connected")
|
||||
return [{"id": 1, "name": "Test"}]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db() -> Generator[Database, None, None]:
|
||||
"""Fixture that provides connected database."""
|
||||
# Setup
|
||||
database = Database("sqlite:///:memory:")
|
||||
database.connect()
|
||||
|
||||
# Provide to test
|
||||
yield database
|
||||
|
||||
# Teardown
|
||||
database.disconnect()
|
||||
|
||||
|
||||
def test_database_query(db):
|
||||
"""Test database query with fixture."""
|
||||
results = db.query("SELECT * FROM users")
|
||||
assert len(results) == 1
|
||||
assert results[0]["name"] == "Test"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_config():
|
||||
"""Session-scoped fixture - created once per test session."""
|
||||
return {
|
||||
"database_url": "postgresql://localhost/test",
|
||||
"api_key": "test-key",
|
||||
"debug": True
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def api_client(app_config):
|
||||
"""Module-scoped fixture - created once per test module."""
|
||||
# Setup expensive resource
|
||||
client = {"config": app_config, "session": "active"}
|
||||
yield client
|
||||
# Cleanup
|
||||
client["session"] = "closed"
|
||||
|
||||
|
||||
def test_api_client(api_client):
|
||||
"""Test using api client fixture."""
|
||||
assert api_client["session"] == "active"
|
||||
assert api_client["config"]["debug"] is True
|
||||
```
|
||||
|
||||
### Pattern 3: Parameterized Tests
|
||||
|
||||
```python
|
||||
# test_validation.py
|
||||
import pytest
|
||||
|
||||
def is_valid_email(email: str) -> bool:
|
||||
"""Check if email is valid."""
|
||||
return "@" in email and "." in email.split("@")[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("email,expected", [
|
||||
("user@example.com", True),
|
||||
("test.user@domain.co.uk", True),
|
||||
("invalid.email", False),
|
||||
("@example.com", False),
|
||||
("user@domain", False),
|
||||
("", False),
|
||||
])
|
||||
def test_email_validation(email, expected):
|
||||
"""Test email validation with various inputs."""
|
||||
assert is_valid_email(email) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("a,b,expected", [
|
||||
(2, 3, 5),
|
||||
(0, 0, 0),
|
||||
(-1, 1, 0),
|
||||
(100, 200, 300),
|
||||
(-5, -5, -10),
|
||||
])
|
||||
def test_addition_parameterized(a, b, expected):
|
||||
"""Test addition with multiple parameter sets."""
|
||||
from test_calculator import Calculator
|
||||
calc = Calculator()
|
||||
assert calc.add(a, b) == expected
|
||||
|
||||
|
||||
# Using pytest.param for special cases
|
||||
@pytest.mark.parametrize("value,expected", [
|
||||
pytest.param(1, True, id="positive"),
|
||||
pytest.param(0, False, id="zero"),
|
||||
pytest.param(-1, False, id="negative"),
|
||||
])
|
||||
def test_is_positive(value, expected):
|
||||
"""Test with custom test IDs."""
|
||||
assert (value > 0) == expected
|
||||
```
|
||||
|
||||
### Pattern 4: Mocking with unittest.mock
|
||||
|
||||
```python
|
||||
# test_api_client.py
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import requests
|
||||
|
||||
class APIClient:
|
||||
"""Simple API client."""
|
||||
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url
|
||||
|
||||
def get_user(self, user_id: int) -> dict:
|
||||
"""Fetch user from API."""
|
||||
response = requests.get(f"{self.base_url}/users/{user_id}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def create_user(self, data: dict) -> dict:
|
||||
"""Create new user."""
|
||||
response = requests.post(f"{self.base_url}/users", json=data)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_get_user_success():
|
||||
"""Test successful API call with mock."""
|
||||
client = APIClient("https://api.example.com")
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {"id": 1, "name": "John Doe"}
|
||||
mock_response.raise_for_status.return_value = None
|
||||
|
||||
with patch("requests.get", return_value=mock_response) as mock_get:
|
||||
user = client.get_user(1)
|
||||
|
||||
assert user["id"] == 1
|
||||
assert user["name"] == "John Doe"
|
||||
mock_get.assert_called_once_with("https://api.example.com/users/1")
|
||||
|
||||
|
||||
def test_get_user_not_found():
|
||||
"""Test API call with 404 error."""
|
||||
client = APIClient("https://api.example.com")
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
with pytest.raises(requests.HTTPError):
|
||||
client.get_user(999)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_create_user(mock_post):
|
||||
"""Test user creation with decorator syntax."""
|
||||
client = APIClient("https://api.example.com")
|
||||
|
||||
mock_post.return_value.json.return_value = {"id": 2, "name": "Jane Doe"}
|
||||
mock_post.return_value.raise_for_status.return_value = None
|
||||
|
||||
user_data = {"name": "Jane Doe", "email": "jane@example.com"}
|
||||
result = client.create_user(user_data)
|
||||
|
||||
assert result["id"] == 2
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
assert call_args.kwargs["json"] == user_data
|
||||
```
|
||||
|
||||
### Pattern 5: Testing Exceptions
|
||||
|
||||
```python
|
||||
# test_exceptions.py
|
||||
import pytest
|
||||
|
||||
def divide(a: float, b: float) -> float:
|
||||
"""Divide a by b."""
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("Division by zero")
|
||||
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
|
||||
raise TypeError("Arguments must be numbers")
|
||||
return a / b
|
||||
|
||||
|
||||
def test_zero_division():
|
||||
"""Test exception is raised for division by zero."""
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
divide(10, 0)
|
||||
|
||||
|
||||
def test_zero_division_with_message():
|
||||
"""Test exception message."""
|
||||
with pytest.raises(ZeroDivisionError, match="Division by zero"):
|
||||
divide(5, 0)
|
||||
|
||||
|
||||
def test_type_error():
|
||||
"""Test type error exception."""
|
||||
with pytest.raises(TypeError, match="must be numbers"):
|
||||
divide("10", 5)
|
||||
|
||||
|
||||
def test_exception_info():
|
||||
"""Test accessing exception info."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
int("not a number")
|
||||
|
||||
assert "invalid literal" in str(exc_info.value)
|
||||
```
|
||||
|
||||
For advanced patterns including async testing, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration files, see [references/advanced-patterns.md](references/advanced-patterns.md)
|
||||
|
||||
## Test Design Principles
|
||||
|
||||
### One Behavior Per Test
|
||||
|
||||
Each test should verify exactly one behavior. This makes failures easy to diagnose and tests easy to maintain.
|
||||
|
||||
```python
|
||||
# BAD - testing multiple behaviors
|
||||
def test_user_service():
|
||||
user = service.create_user(data)
|
||||
assert user.id is not None
|
||||
assert user.email == data["email"]
|
||||
updated = service.update_user(user.id, {"name": "New"})
|
||||
assert updated.name == "New"
|
||||
|
||||
# GOOD - focused tests
|
||||
def test_create_user_assigns_id():
|
||||
user = service.create_user(data)
|
||||
assert user.id is not None
|
||||
|
||||
def test_create_user_stores_email():
|
||||
user = service.create_user(data)
|
||||
assert user.email == data["email"]
|
||||
|
||||
def test_update_user_changes_name():
|
||||
user = service.create_user(data)
|
||||
updated = service.update_user(user.id, {"name": "New"})
|
||||
assert updated.name == "New"
|
||||
```
|
||||
|
||||
### Test Error Paths
|
||||
|
||||
Always test failure cases, not just happy paths.
|
||||
|
||||
```python
|
||||
def test_get_user_raises_not_found():
|
||||
with pytest.raises(UserNotFoundError) as exc_info:
|
||||
service.get_user("nonexistent-id")
|
||||
|
||||
assert "nonexistent-id" in str(exc_info.value)
|
||||
|
||||
def test_create_user_rejects_invalid_email():
|
||||
with pytest.raises(ValueError, match="Invalid email format"):
|
||||
service.create_user({"email": "not-an-email"})
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### Test Organization
|
||||
|
||||
```python
|
||||
# tests/
|
||||
# __init__.py
|
||||
# conftest.py # Shared fixtures
|
||||
# test_unit/ # Unit tests
|
||||
# test_models.py
|
||||
# test_utils.py
|
||||
# test_integration/ # Integration tests
|
||||
# test_api.py
|
||||
# test_database.py
|
||||
# test_e2e/ # End-to-end tests
|
||||
# test_workflows.py
|
||||
```
|
||||
|
||||
### Test Naming Convention
|
||||
|
||||
A common pattern: `test_<unit>_<scenario>_<expected_outcome>`. Adapt to your team's preferences.
|
||||
|
||||
```python
|
||||
# Pattern: test_<unit>_<scenario>_<expected>
|
||||
def test_create_user_with_valid_data_returns_user():
|
||||
...
|
||||
|
||||
def test_create_user_with_duplicate_email_raises_conflict():
|
||||
...
|
||||
|
||||
def test_get_user_with_unknown_id_returns_none():
|
||||
...
|
||||
|
||||
# Good test names - clear and descriptive
|
||||
def test_user_creation_with_valid_data():
|
||||
"""Clear name describes what is being tested."""
|
||||
pass
|
||||
|
||||
def test_login_fails_with_invalid_password():
|
||||
"""Name describes expected behavior."""
|
||||
pass
|
||||
|
||||
def test_api_returns_404_for_missing_resource():
|
||||
"""Specific about inputs and expected outcomes."""
|
||||
pass
|
||||
|
||||
# Bad test names - avoid these
|
||||
def test_1(): # Not descriptive
|
||||
pass
|
||||
|
||||
def test_user(): # Too vague
|
||||
pass
|
||||
|
||||
def test_function(): # Doesn't explain what's tested
|
||||
pass
|
||||
```
|
||||
|
||||
### Testing Retry Behavior
|
||||
|
||||
Verify that retry logic works correctly using mock side effects.
|
||||
|
||||
```python
|
||||
from unittest.mock import Mock
|
||||
|
||||
def test_retries_on_transient_error():
|
||||
"""Test that service retries on transient failures."""
|
||||
client = Mock()
|
||||
# Fail twice, then succeed
|
||||
client.request.side_effect = [
|
||||
ConnectionError("Failed"),
|
||||
ConnectionError("Failed"),
|
||||
{"status": "ok"},
|
||||
]
|
||||
|
||||
service = ServiceWithRetry(client, max_retries=3)
|
||||
result = service.fetch()
|
||||
|
||||
assert result == {"status": "ok"}
|
||||
assert client.request.call_count == 3
|
||||
|
||||
def test_gives_up_after_max_retries():
|
||||
"""Test that service stops retrying after max attempts."""
|
||||
client = Mock()
|
||||
client.request.side_effect = ConnectionError("Failed")
|
||||
|
||||
service = ServiceWithRetry(client, max_retries=3)
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
service.fetch()
|
||||
|
||||
assert client.request.call_count == 3
|
||||
|
||||
def test_does_not_retry_on_permanent_error():
|
||||
"""Test that permanent errors are not retried."""
|
||||
client = Mock()
|
||||
client.request.side_effect = ValueError("Invalid input")
|
||||
|
||||
service = ServiceWithRetry(client, max_retries=3)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
service.fetch()
|
||||
|
||||
# Only called once - no retry for ValueError
|
||||
assert client.request.call_count == 1
|
||||
```
|
||||
|
||||
### Mocking Time with Freezegun
|
||||
|
||||
Use freezegun to control time in tests for predictable time-dependent behavior.
|
||||
|
||||
```python
|
||||
from freezegun import freeze_time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@freeze_time("2026-01-15 10:00:00")
|
||||
def test_token_expiry():
|
||||
"""Test token expires at correct time."""
|
||||
token = create_token(expires_in_seconds=3600)
|
||||
assert token.expires_at == datetime(2026, 1, 15, 11, 0, 0)
|
||||
|
||||
@freeze_time("2026-01-15 10:00:00")
|
||||
def test_is_expired_returns_false_before_expiry():
|
||||
"""Test token is not expired when within validity period."""
|
||||
token = create_token(expires_in_seconds=3600)
|
||||
assert not token.is_expired()
|
||||
|
||||
@freeze_time("2026-01-15 12:00:00")
|
||||
def test_is_expired_returns_true_after_expiry():
|
||||
"""Test token is expired after validity period."""
|
||||
token = Token(expires_at=datetime(2026, 1, 15, 11, 30, 0))
|
||||
assert token.is_expired()
|
||||
|
||||
def test_with_time_travel():
|
||||
"""Test behavior across time using freeze_time context."""
|
||||
with freeze_time("2026-01-01") as frozen_time:
|
||||
item = create_item()
|
||||
assert item.created_at == datetime(2026, 1, 1)
|
||||
|
||||
# Move forward in time
|
||||
frozen_time.move_to("2026-01-15")
|
||||
assert item.age_days == 14
|
||||
```
|
||||
|
||||
### Test Markers
|
||||
|
||||
```python
|
||||
# test_markers.py
|
||||
import pytest
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_slow_operation():
|
||||
"""Mark slow tests."""
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_database_integration():
|
||||
"""Mark integration tests."""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Feature not implemented yet")
|
||||
def test_future_feature():
|
||||
"""Skip tests temporarily."""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="Unix only test")
|
||||
def test_unix_specific():
|
||||
"""Conditional skip."""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Known bug #123")
|
||||
def test_known_bug():
|
||||
"""Mark expected failures."""
|
||||
assert False
|
||||
|
||||
|
||||
# Run with:
|
||||
# pytest -m slow # Run only slow tests
|
||||
# pytest -m "not slow" # Skip slow tests
|
||||
# pytest -m integration # Run integration tests
|
||||
```
|
||||
|
||||
### Coverage Reporting
|
||||
|
||||
```bash
|
||||
# Install coverage
|
||||
pip install pytest-cov
|
||||
|
||||
# Run tests with coverage
|
||||
pytest --cov=myapp tests/
|
||||
|
||||
# Generate HTML report
|
||||
pytest --cov=myapp --cov-report=html tests/
|
||||
|
||||
# Fail if coverage below threshold
|
||||
pytest --cov=myapp --cov-fail-under=80 tests/
|
||||
|
||||
# Show missing lines
|
||||
pytest --cov=myapp --cov-report=term-missing tests/
|
||||
```
|
||||
|
||||
For advanced patterns (async testing, monkeypatching, property-based testing, database testing, CI/CD integration, and configuration), see [references/advanced-patterns.md](references/advanced-patterns.md)
|
||||
@@ -0,0 +1,411 @@
|
||||
# Python Testing Patterns — Advanced Reference
|
||||
|
||||
Advanced testing patterns including async code, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration.
|
||||
|
||||
## Pattern 6: Testing Async Code
|
||||
|
||||
```python
|
||||
# test_async.py
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
async def fetch_data(url: str) -> dict:
|
||||
"""Fetch data asynchronously."""
|
||||
await asyncio.sleep(0.1)
|
||||
return {"url": url, "data": "result"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_data():
|
||||
"""Test async function."""
|
||||
result = await fetch_data("https://api.example.com")
|
||||
assert result["url"] == "https://api.example.com"
|
||||
assert "data" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_fetches():
|
||||
"""Test concurrent async operations."""
|
||||
urls = ["url1", "url2", "url3"]
|
||||
tasks = [fetch_data(url) for url in urls]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
assert len(results) == 3
|
||||
assert all("data" in r for r in results)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def async_client():
|
||||
"""Async fixture."""
|
||||
client = {"connected": True}
|
||||
yield client
|
||||
client["connected"] = False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_with_async_fixture(async_client):
|
||||
"""Test using async fixture."""
|
||||
assert async_client["connected"] is True
|
||||
```
|
||||
|
||||
## Pattern 7: Monkeypatch for Testing
|
||||
|
||||
```python
|
||||
# test_environment.py
|
||||
import os
|
||||
import pytest
|
||||
|
||||
def get_database_url() -> str:
|
||||
"""Get database URL from environment."""
|
||||
return os.environ.get("DATABASE_URL", "sqlite:///:memory:")
|
||||
|
||||
|
||||
def test_database_url_default():
|
||||
"""Test default database URL."""
|
||||
# Will use actual environment variable if set
|
||||
url = get_database_url()
|
||||
assert url
|
||||
|
||||
|
||||
def test_database_url_custom(monkeypatch):
|
||||
"""Test custom database URL with monkeypatch."""
|
||||
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
||||
assert get_database_url() == "postgresql://localhost/test"
|
||||
|
||||
|
||||
def test_database_url_not_set(monkeypatch):
|
||||
"""Test when env var is not set."""
|
||||
monkeypatch.delenv("DATABASE_URL", raising=False)
|
||||
assert get_database_url() == "sqlite:///:memory:"
|
||||
|
||||
|
||||
class Config:
|
||||
"""Configuration class."""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = "production-key"
|
||||
|
||||
def get_api_key(self):
|
||||
return self.api_key
|
||||
|
||||
|
||||
def test_monkeypatch_attribute(monkeypatch):
|
||||
"""Test monkeypatching object attributes."""
|
||||
config = Config()
|
||||
monkeypatch.setattr(config, "api_key", "test-key")
|
||||
assert config.get_api_key() == "test-key"
|
||||
```
|
||||
|
||||
## Pattern 8: Temporary Files and Directories
|
||||
|
||||
```python
|
||||
# test_file_operations.py
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
def save_data(filepath: Path, data: str):
|
||||
"""Save data to file."""
|
||||
filepath.write_text(data)
|
||||
|
||||
|
||||
def load_data(filepath: Path) -> str:
|
||||
"""Load data from file."""
|
||||
return filepath.read_text()
|
||||
|
||||
|
||||
def test_file_operations(tmp_path):
|
||||
"""Test file operations with temporary directory."""
|
||||
# tmp_path is a pathlib.Path object
|
||||
test_file = tmp_path / "test_data.txt"
|
||||
|
||||
# Save data
|
||||
save_data(test_file, "Hello, World!")
|
||||
|
||||
# Verify file exists
|
||||
assert test_file.exists()
|
||||
|
||||
# Load and verify data
|
||||
data = load_data(test_file)
|
||||
assert data == "Hello, World!"
|
||||
|
||||
|
||||
def test_multiple_files(tmp_path):
|
||||
"""Test with multiple temporary files."""
|
||||
files = {
|
||||
"file1.txt": "Content 1",
|
||||
"file2.txt": "Content 2",
|
||||
"file3.txt": "Content 3"
|
||||
}
|
||||
|
||||
for filename, content in files.items():
|
||||
filepath = tmp_path / filename
|
||||
save_data(filepath, content)
|
||||
|
||||
# Verify all files created
|
||||
assert len(list(tmp_path.iterdir())) == 3
|
||||
|
||||
# Verify contents
|
||||
for filename, expected_content in files.items():
|
||||
filepath = tmp_path / filename
|
||||
assert load_data(filepath) == expected_content
|
||||
```
|
||||
|
||||
## Pattern 9: Custom Fixtures and Conftest
|
||||
|
||||
```python
|
||||
# conftest.py
|
||||
"""Shared fixtures for all tests."""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def database_url():
|
||||
"""Provide database URL for all tests."""
|
||||
return "postgresql://localhost/test_db"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_database(database_url):
|
||||
"""Auto-use fixture that runs before each test."""
|
||||
# Setup: Clear database
|
||||
print(f"Clearing database: {database_url}")
|
||||
yield
|
||||
# Teardown: Clean up
|
||||
print("Test completed")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_user():
|
||||
"""Provide sample user data."""
|
||||
return {
|
||||
"id": 1,
|
||||
"name": "Test User",
|
||||
"email": "test@example.com"
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_users():
|
||||
"""Provide list of sample users."""
|
||||
return [
|
||||
{"id": 1, "name": "User 1"},
|
||||
{"id": 2, "name": "User 2"},
|
||||
{"id": 3, "name": "User 3"},
|
||||
]
|
||||
|
||||
|
||||
# Parametrized fixture
|
||||
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
|
||||
def db_backend(request):
|
||||
"""Fixture that runs tests with different database backends."""
|
||||
return request.param
|
||||
|
||||
|
||||
def test_with_db_backend(db_backend):
|
||||
"""This test will run 3 times with different backends."""
|
||||
print(f"Testing with {db_backend}")
|
||||
assert db_backend in ["sqlite", "postgresql", "mysql"]
|
||||
```
|
||||
|
||||
## Pattern 10: Property-Based Testing
|
||||
|
||||
```python
|
||||
# test_properties.py
|
||||
from hypothesis import given, strategies as st
|
||||
import pytest
|
||||
|
||||
def reverse_string(s: str) -> str:
|
||||
"""Reverse a string."""
|
||||
return s[::-1]
|
||||
|
||||
|
||||
@given(st.text())
|
||||
def test_reverse_twice_is_original(s):
|
||||
"""Property: reversing twice returns original."""
|
||||
assert reverse_string(reverse_string(s)) == s
|
||||
|
||||
|
||||
@given(st.text())
|
||||
def test_reverse_length(s):
|
||||
"""Property: reversed string has same length."""
|
||||
assert len(reverse_string(s)) == len(s)
|
||||
|
||||
|
||||
@given(st.integers(), st.integers())
|
||||
def test_addition_commutative(a, b):
|
||||
"""Property: addition is commutative."""
|
||||
assert a + b == b + a
|
||||
|
||||
|
||||
@given(st.lists(st.integers()))
|
||||
def test_sorted_list_properties(lst):
|
||||
"""Property: sorted list is ordered."""
|
||||
sorted_lst = sorted(lst)
|
||||
|
||||
# Same length
|
||||
assert len(sorted_lst) == len(lst)
|
||||
|
||||
# All elements present
|
||||
assert set(sorted_lst) == set(lst)
|
||||
|
||||
# Is ordered
|
||||
for i in range(len(sorted_lst) - 1):
|
||||
assert sorted_lst[i] <= sorted_lst[i + 1]
|
||||
```
|
||||
|
||||
## Testing Database Code
|
||||
|
||||
```python
|
||||
# test_database_models.py
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, Column, Integer, String
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model."""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(50))
|
||||
email = Column(String(100), unique=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db_session() -> Session:
|
||||
"""Create in-memory database for testing."""
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
session = SessionLocal()
|
||||
|
||||
yield session
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
def test_create_user(db_session):
|
||||
"""Test creating a user."""
|
||||
user = User(name="Test User", email="test@example.com")
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
assert user.id is not None
|
||||
assert user.name == "Test User"
|
||||
|
||||
|
||||
def test_query_user(db_session):
|
||||
"""Test querying users."""
|
||||
user1 = User(name="User 1", email="user1@example.com")
|
||||
user2 = User(name="User 2", email="user2@example.com")
|
||||
|
||||
db_session.add_all([user1, user2])
|
||||
db_session.commit()
|
||||
|
||||
users = db_session.query(User).all()
|
||||
assert len(users) == 2
|
||||
|
||||
|
||||
def test_unique_email_constraint(db_session):
|
||||
"""Test unique email constraint."""
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
user1 = User(name="User 1", email="same@example.com")
|
||||
user2 = User(name="User 2", email="same@example.com")
|
||||
|
||||
db_session.add(user1)
|
||||
db_session.commit()
|
||||
|
||||
db_session.add(user2)
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
db_session.commit()
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -e ".[dev]"
|
||||
pip install pytest pytest-cov
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
pytest --cov=myapp --cov-report=xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
```ini
|
||||
# pytest.ini
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--cov=myapp
|
||||
--cov-report=term-missing
|
||||
markers =
|
||||
slow: marks tests as slow
|
||||
integration: marks integration tests
|
||||
unit: marks unit tests
|
||||
e2e: marks end-to-end tests
|
||||
```
|
||||
|
||||
```toml
|
||||
# pyproject.toml
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
addopts = [
|
||||
"-v",
|
||||
"--cov=myapp",
|
||||
"--cov-report=term-missing",
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["myapp"]
|
||||
omit = ["*/tests/*", "*/migrations/*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"raise AssertionError",
|
||||
"raise NotImplementedError",
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,513 @@
|
||||
---
|
||||
name: seo
|
||||
description: Optimize for search engine visibility and ranking. Use when asked to "improve SEO", "optimize for search", "fix meta tags", "add structured data", "sitemap optimization", or "search engine optimization".
|
||||
license: MIT
|
||||
metadata:
|
||||
author: web-quality-skills
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# SEO optimization
|
||||
|
||||
Search engine optimization based on Lighthouse SEO audits and Google Search guidelines. Focus on technical SEO, on-page optimization, and structured data.
|
||||
|
||||
## SEO fundamentals
|
||||
|
||||
Search ranking factors (approximate influence):
|
||||
|
||||
| Factor | Influence | This Skill |
|
||||
|--------|-----------|------------|
|
||||
| Content quality & relevance | ~40% | Partial (structure) |
|
||||
| Backlinks & authority | ~25% | ✗ |
|
||||
| Technical SEO | ~15% | ✓ |
|
||||
| Page experience (Core Web Vitals) | ~10% | See [Core Web Vitals](../core-web-vitals/SKILL.md) |
|
||||
| On-page SEO | ~10% | ✓ |
|
||||
|
||||
---
|
||||
|
||||
## Technical SEO
|
||||
|
||||
### Crawlability
|
||||
|
||||
**robots.txt:**
|
||||
```text
|
||||
# /robots.txt
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Block admin/private areas
|
||||
Disallow: /admin/
|
||||
Disallow: /api/
|
||||
Disallow: /private/
|
||||
|
||||
# Don't block resources needed for rendering
|
||||
# ❌ Disallow: /static/
|
||||
|
||||
Sitemap: https://example.com/sitemap.xml
|
||||
```
|
||||
|
||||
**Meta robots:**
|
||||
```html
|
||||
<!-- Default: indexable, followable -->
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<!-- Noindex specific pages -->
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<!-- Indexable but don't follow links -->
|
||||
<meta name="robots" content="index, nofollow">
|
||||
|
||||
<!-- Control snippets -->
|
||||
<meta name="robots" content="max-snippet:150, max-image-preview:large">
|
||||
```
|
||||
|
||||
**Canonical URLs:**
|
||||
```html
|
||||
<!-- Prevent duplicate content issues -->
|
||||
<link rel="canonical" href="https://example.com/page">
|
||||
|
||||
<!-- Self-referencing canonical (recommended) -->
|
||||
<link rel="canonical" href="https://example.com/current-page">
|
||||
|
||||
<!-- For paginated content -->
|
||||
<link rel="canonical" href="https://example.com/products">
|
||||
<!-- Or use rel="prev" / rel="next" for explicit pagination -->
|
||||
```
|
||||
|
||||
### XML sitemap
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://example.com/</loc>
|
||||
<lastmod>2024-01-15</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://example.com/products</loc>
|
||||
<lastmod>2024-01-14</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
```
|
||||
|
||||
**Sitemap best practices:**
|
||||
- Maximum 50,000 URLs or 50MB per sitemap
|
||||
- Use sitemap index for larger sites
|
||||
- Include only canonical, indexable URLs
|
||||
- Update `lastmod` when content changes
|
||||
- Submit to Google Search Console
|
||||
|
||||
### URL structure
|
||||
|
||||
```
|
||||
✅ Good URLs:
|
||||
https://example.com/products/blue-widget
|
||||
https://example.com/blog/how-to-use-widgets
|
||||
|
||||
❌ Poor URLs:
|
||||
https://example.com/p?id=12345
|
||||
https://example.com/products/item/category/subcategory/blue-widget-2024-sale-discount
|
||||
```
|
||||
|
||||
**URL guidelines:**
|
||||
- Use hyphens, not underscores
|
||||
- Lowercase only
|
||||
- Keep short (< 75 characters)
|
||||
- Include target keywords naturally
|
||||
- Avoid parameters when possible
|
||||
- Use HTTPS always
|
||||
|
||||
### HTTPS & security
|
||||
|
||||
```html
|
||||
<!-- Ensure all resources use HTTPS -->
|
||||
<img src="https://example.com/image.jpg">
|
||||
|
||||
<!-- Not: -->
|
||||
<img src="http://example.com/image.jpg">
|
||||
```
|
||||
|
||||
**Security headers for SEO trust signals:**
|
||||
```
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## On-page SEO
|
||||
|
||||
### Title tags
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing or generic -->
|
||||
<title>Page</title>
|
||||
<title>Home</title>
|
||||
|
||||
<!-- ✅ Descriptive with primary keyword -->
|
||||
<title>Blue Widgets for Sale | Premium Quality | Example Store</title>
|
||||
```
|
||||
|
||||
**Title tag guidelines:**
|
||||
- 50-60 characters (Google truncates ~60)
|
||||
- Primary keyword near the beginning
|
||||
- Unique for every page
|
||||
- Brand name at end (unless homepage)
|
||||
- Action-oriented when appropriate
|
||||
|
||||
### Meta descriptions
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing or duplicate -->
|
||||
<meta name="description" content="">
|
||||
|
||||
<!-- ✅ Compelling and unique -->
|
||||
<meta name="description" content="Shop premium blue widgets with free shipping. 30-day returns. Rated 4.9/5 by 10,000+ customers. Order today and save 20%.">
|
||||
```
|
||||
|
||||
**Meta description guidelines:**
|
||||
- 150-160 characters
|
||||
- Include primary keyword naturally
|
||||
- Compelling call-to-action
|
||||
- Unique for every page
|
||||
- Matches page content
|
||||
|
||||
### Heading structure
|
||||
|
||||
```html
|
||||
<!-- ❌ Poor structure -->
|
||||
<h2>Welcome to Our Store</h2>
|
||||
<h4>Products</h4>
|
||||
<h1>Contact Us</h1>
|
||||
|
||||
<!-- ✅ Proper hierarchy -->
|
||||
<h1>Blue Widgets - Premium Quality</h1>
|
||||
<h2>Product Features</h2>
|
||||
<h3>Durability</h3>
|
||||
<h3>Design</h3>
|
||||
<h2>Customer Reviews</h2>
|
||||
<h2>Pricing</h2>
|
||||
```
|
||||
|
||||
**Heading guidelines:**
|
||||
- Single `<h1>` per page (the main topic)
|
||||
- Logical hierarchy (don't skip levels)
|
||||
- Include keywords naturally
|
||||
- Descriptive, not generic
|
||||
|
||||
### Image SEO
|
||||
|
||||
```html
|
||||
<!-- ❌ Poor image SEO -->
|
||||
<img src="IMG_12345.jpg">
|
||||
|
||||
<!-- ✅ Optimized image -->
|
||||
<img src="blue-widget-product-photo.webp"
|
||||
alt="Blue widget with chrome finish, side view showing control panel"
|
||||
width="800"
|
||||
height="600"
|
||||
loading="lazy">
|
||||
```
|
||||
|
||||
**Image guidelines:**
|
||||
- Descriptive filenames with keywords
|
||||
- Alt text describes the image content
|
||||
- Compressed and properly sized
|
||||
- WebP/AVIF with fallbacks
|
||||
- Lazy load below-fold images
|
||||
|
||||
### Internal linking
|
||||
|
||||
```html
|
||||
<!-- ❌ Non-descriptive -->
|
||||
<a href="/products">Click here</a>
|
||||
<a href="/widgets">Read more</a>
|
||||
|
||||
<!-- ✅ Descriptive anchor text -->
|
||||
<a href="/products/blue-widgets">Browse our blue widget collection</a>
|
||||
<a href="/guides/widget-maintenance">Learn how to maintain your widgets</a>
|
||||
```
|
||||
|
||||
**Linking guidelines:**
|
||||
- Descriptive anchor text with keywords
|
||||
- Link to relevant internal pages
|
||||
- Reasonable number of links per page
|
||||
- Fix broken links promptly
|
||||
- Use breadcrumbs for hierarchy
|
||||
|
||||
---
|
||||
|
||||
## Structured data (JSON-LD)
|
||||
|
||||
### Organization
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Example Company",
|
||||
"url": "https://example.com",
|
||||
"logo": "https://example.com/logo.png",
|
||||
"sameAs": [
|
||||
"https://twitter.com/example",
|
||||
"https://linkedin.com/company/example"
|
||||
],
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+1-555-123-4567",
|
||||
"contactType": "customer service"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Article
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "How to Choose the Right Widget",
|
||||
"description": "Complete guide to selecting widgets for your needs.",
|
||||
"image": "https://example.com/article-image.jpg",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Jane Smith",
|
||||
"url": "https://example.com/authors/jane-smith"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Example Blog",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://example.com/logo.png"
|
||||
}
|
||||
},
|
||||
"datePublished": "2024-01-15",
|
||||
"dateModified": "2024-01-20"
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Product
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"name": "Blue Widget Pro",
|
||||
"image": "https://example.com/blue-widget.jpg",
|
||||
"description": "Premium blue widget with advanced features.",
|
||||
"brand": {
|
||||
"@type": "Brand",
|
||||
"name": "WidgetCo"
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "49.99",
|
||||
"priceCurrency": "USD",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"url": "https://example.com/products/blue-widget"
|
||||
},
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.8",
|
||||
"reviewCount": "1250"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### FAQ
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What colors are available?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Our widgets come in blue, red, and green."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What is the warranty?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "All widgets include a 2-year warranty."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Breadcrumbs
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "https://example.com"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Products",
|
||||
"item": "https://example.com/products"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 3,
|
||||
"name": "Blue Widgets",
|
||||
"item": "https://example.com/products/blue-widgets"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Test structured data at:
|
||||
- [Google Rich Results Test](https://search.google.com/test/rich-results)
|
||||
- [Schema.org Validator](https://validator.schema.org/)
|
||||
|
||||
---
|
||||
|
||||
## Mobile SEO
|
||||
|
||||
### Responsive design
|
||||
|
||||
```html
|
||||
<!-- ❌ Not mobile-friendly -->
|
||||
<meta name="viewport" content="width=1024">
|
||||
|
||||
<!-- ✅ Responsive viewport -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
```
|
||||
|
||||
### Tap targets
|
||||
|
||||
```css
|
||||
/* ❌ Too small for mobile */
|
||||
.small-link {
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ✅ Adequate tap target */
|
||||
.mobile-friendly-link {
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
}
|
||||
```
|
||||
|
||||
### Font sizes
|
||||
|
||||
```css
|
||||
/* ❌ Too small on mobile */
|
||||
body {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ✅ Readable without zooming */
|
||||
body {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## International SEO
|
||||
|
||||
### Hreflang tags
|
||||
|
||||
```html
|
||||
<!-- For multi-language sites -->
|
||||
<link rel="alternate" hreflang="en" href="https://example.com/page">
|
||||
<link rel="alternate" hreflang="es" href="https://example.com/es/page">
|
||||
<link rel="alternate" hreflang="fr" href="https://example.com/fr/page">
|
||||
<link rel="alternate" hreflang="x-default" href="https://example.com/page">
|
||||
```
|
||||
|
||||
### Language declaration
|
||||
|
||||
```html
|
||||
<html lang="en">
|
||||
<!-- or -->
|
||||
<html lang="es-MX">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SEO audit checklist
|
||||
|
||||
### Critical
|
||||
- [ ] HTTPS enabled
|
||||
- [ ] robots.txt allows crawling
|
||||
- [ ] No `noindex` on important pages
|
||||
- [ ] Title tags present and unique
|
||||
- [ ] Single `<h1>` per page
|
||||
|
||||
### High priority
|
||||
- [ ] Meta descriptions present
|
||||
- [ ] Sitemap submitted
|
||||
- [ ] Canonical URLs set
|
||||
- [ ] Mobile-responsive
|
||||
- [ ] Core Web Vitals passing
|
||||
|
||||
### Medium priority
|
||||
- [ ] Structured data implemented
|
||||
- [ ] Internal linking strategy
|
||||
- [ ] Image alt text
|
||||
- [ ] Descriptive URLs
|
||||
- [ ] Breadcrumb navigation
|
||||
|
||||
### Ongoing
|
||||
- [ ] Fix crawl errors in Search Console
|
||||
- [ ] Update sitemap when content changes
|
||||
- [ ] Monitor ranking changes
|
||||
- [ ] Check for broken links
|
||||
- [ ] Review Search Console insights
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Use |
|
||||
|------|-----|
|
||||
| Google Search Console | Monitor indexing, fix issues |
|
||||
| Google PageSpeed Insights | Performance + Core Web Vitals |
|
||||
| Rich Results Test | Validate structured data |
|
||||
| Lighthouse | Full SEO audit |
|
||||
| Screaming Frog | Crawl analysis |
|
||||
|
||||
## References
|
||||
|
||||
- [Google Search Central](https://developers.google.com/search)
|
||||
- [Schema.org](https://schema.org/)
|
||||
- [Core Web Vitals](../core-web-vitals/SKILL.md)
|
||||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/accessibility
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/frontend-design
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/pandas-data-analysis
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/pandas-pro
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/python-executor
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/python-testing-patterns
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/seo
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"accessibility": {
|
||||
"source": "addyosmani/web-quality-skills",
|
||||
"sourceType": "autoskills-registry",
|
||||
"computedHash": "bffe3d08cfe92ebad63699f74ce29e35c19850ebfbf474c1463183cfe34d6a09"
|
||||
},
|
||||
"frontend-design": {
|
||||
"source": "anthropics/skills",
|
||||
"sourceType": "autoskills-registry",
|
||||
"computedHash": "82fb11a63fb1e35ee2469516ed02d54695f783115b1540c0e783197af4240a3a"
|
||||
},
|
||||
"pandas-data-analysis": {
|
||||
"source": "pluginagentmarketplace/custom-plugin-python",
|
||||
"sourceType": "autoskills-registry",
|
||||
"computedHash": "612eb33d227296b4bdd0ca62785b2e31124a92adf5f40b730b0cf3713adc6f04"
|
||||
},
|
||||
"pandas-pro": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
"sourceType": "autoskills-registry",
|
||||
"computedHash": "61f79ae3c9a29fd452e5cf8dee9891f1d0ab7da72fa80aa0366784bcd2b46d2b"
|
||||
},
|
||||
"python-executor": {
|
||||
"source": "inferen-sh/skills",
|
||||
"sourceType": "autoskills-registry",
|
||||
"computedHash": "bd2d874c27788964fadf03c0840049de0409407f06f883ee90886164cb22ef69"
|
||||
},
|
||||
"python-testing-patterns": {
|
||||
"source": "wshobson/agents",
|
||||
"sourceType": "autoskills-registry",
|
||||
"computedHash": "07b87d62993c0b6159a91d18fc8723b7f4c13d5000c0984266c207493ce641ff"
|
||||
},
|
||||
"seo": {
|
||||
"source": "addyosmani/web-quality-skills",
|
||||
"sourceType": "autoskills-registry",
|
||||
"computedHash": "c184da724d1c61ad077f27418ea8e7e88fd54bcdf98165e18be7e4681cbd5e20"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user