|
| 1 | +""" pyplots.ai |
| 2 | +chernoff-basic: Chernoff Faces for Multivariate Data |
| 3 | +Library: matplotlib 3.10.8 | Python 3.13.11 |
| 4 | +Quality: 91/100 | Created: 2025-12-31 |
| 5 | +""" |
| 6 | + |
| 7 | +import matplotlib.patches as patches |
| 8 | +import matplotlib.pyplot as plt |
| 9 | +import numpy as np |
| 10 | + |
| 11 | + |
| 12 | +# Data - Car performance metrics (9 vehicles with 4 attributes) |
| 13 | +# Attributes: fuel efficiency, power, reliability, comfort (all 0-1 normalized) |
| 14 | +np.random.seed(42) |
| 15 | + |
| 16 | +# Create 3 categories of cars with distinct characteristics |
| 17 | +# Economy cars: high efficiency, low power, medium reliability, medium comfort |
| 18 | +# Sports cars: low efficiency, high power, medium reliability, low comfort |
| 19 | +# Luxury cars: medium efficiency, medium power, high reliability, high comfort |
| 20 | +categories = ["Economy", "Sports", "Luxury"] |
| 21 | +n_per_category = 3 |
| 22 | + |
| 23 | +# Generate synthetic data with category-specific distributions |
| 24 | +data = [] |
| 25 | +labels = [] |
| 26 | +category_ids = [] |
| 27 | + |
| 28 | +# Economy cars - high efficiency, low power |
| 29 | +for i in range(n_per_category): |
| 30 | + data.append( |
| 31 | + [ |
| 32 | + 0.7 + np.random.rand() * 0.25, # fuel_efficiency: 0.7-0.95 |
| 33 | + 0.2 + np.random.rand() * 0.2, # power: 0.2-0.4 |
| 34 | + 0.4 + np.random.rand() * 0.3, # reliability: 0.4-0.7 |
| 35 | + 0.3 + np.random.rand() * 0.3, # comfort: 0.3-0.6 |
| 36 | + ] |
| 37 | + ) |
| 38 | + labels.append(f"Economy {i + 1}") |
| 39 | + category_ids.append(0) |
| 40 | + |
| 41 | +# Sports cars - low efficiency, high power |
| 42 | +for i in range(n_per_category): |
| 43 | + data.append( |
| 44 | + [ |
| 45 | + 0.15 + np.random.rand() * 0.2, # fuel_efficiency: 0.15-0.35 |
| 46 | + 0.75 + np.random.rand() * 0.2, # power: 0.75-0.95 |
| 47 | + 0.4 + np.random.rand() * 0.25, # reliability: 0.4-0.65 |
| 48 | + 0.25 + np.random.rand() * 0.25, # comfort: 0.25-0.5 |
| 49 | + ] |
| 50 | + ) |
| 51 | + labels.append(f"Sports {i + 1}") |
| 52 | + category_ids.append(1) |
| 53 | + |
| 54 | +# Luxury cars - high reliability and comfort |
| 55 | +for i in range(n_per_category): |
| 56 | + data.append( |
| 57 | + [ |
| 58 | + 0.35 + np.random.rand() * 0.25, # fuel_efficiency: 0.35-0.6 |
| 59 | + 0.5 + np.random.rand() * 0.25, # power: 0.5-0.75 |
| 60 | + 0.7 + np.random.rand() * 0.25, # reliability: 0.7-0.95 |
| 61 | + 0.7 + np.random.rand() * 0.25, # comfort: 0.7-0.95 |
| 62 | + ] |
| 63 | + ) |
| 64 | + labels.append(f"Luxury {i + 1}") |
| 65 | + category_ids.append(2) |
| 66 | + |
| 67 | +X_norm = np.array(data) |
| 68 | +colors = ["#306998", "#FFD43B", "#4CAF50"] # Python Blue, Yellow, Green |
| 69 | + |
| 70 | +# Create figure - 3x3 grid of faces (square format for symmetric grid) |
| 71 | +fig, ax = plt.subplots(figsize=(12, 12)) |
| 72 | + |
| 73 | +# Calculate grid positions with better spacing |
| 74 | +n_cols = 3 |
| 75 | +n_rows = 3 |
| 76 | +x_positions = np.linspace(0.22, 0.78, n_cols) |
| 77 | +y_positions = np.linspace(0.76, 0.28, n_rows) # More space between rows |
| 78 | + |
| 79 | +# Draw each Chernoff face |
| 80 | +for idx in range(len(X_norm)): |
| 81 | + row = idx // n_cols |
| 82 | + col = idx % n_cols |
| 83 | + x_center = x_positions[col] |
| 84 | + y_center = y_positions[row] |
| 85 | + features = X_norm[idx] |
| 86 | + color = colors[category_ids[idx]] |
| 87 | + label = labels[idx] |
| 88 | + |
| 89 | + # Feature mappings (all features in 0-1 range): |
| 90 | + # - features[0]: face width (fuel efficiency) |
| 91 | + # - features[1]: face height (power) |
| 92 | + # - features[2]: eye size (reliability) |
| 93 | + # - features[3]: mouth curvature (comfort) - happy = high comfort |
| 94 | + |
| 95 | + # Scale down faces to prevent overlap |
| 96 | + face_width = 0.12 + features[0] * 0.06 # 0.12-0.18 |
| 97 | + face_height = 0.14 + features[1] * 0.06 # 0.14-0.20 |
| 98 | + eye_size = 0.015 + features[2] * 0.015 # 0.015-0.03 |
| 99 | + mouth_curve = -0.05 + features[3] * 0.10 # -0.05 to 0.05 (sad to happy) |
| 100 | + |
| 101 | + # Face ellipse |
| 102 | + face = patches.Ellipse( |
| 103 | + (x_center, y_center), face_width, face_height, facecolor=color, edgecolor="black", linewidth=2.5, alpha=0.75 |
| 104 | + ) |
| 105 | + ax.add_patch(face) |
| 106 | + |
| 107 | + # Eyes - position relative to face |
| 108 | + eye_y = y_center + face_height * 0.18 |
| 109 | + eye_x_offset = face_width * 0.22 |
| 110 | + |
| 111 | + # Left eye (white with pupil) |
| 112 | + left_eye = patches.Ellipse( |
| 113 | + (x_center - eye_x_offset, eye_y), eye_size * 1.6, eye_size, facecolor="white", edgecolor="black", linewidth=1.5 |
| 114 | + ) |
| 115 | + ax.add_patch(left_eye) |
| 116 | + left_pupil = patches.Circle((x_center - eye_x_offset, eye_y), eye_size * 0.35, facecolor="black") |
| 117 | + ax.add_patch(left_pupil) |
| 118 | + |
| 119 | + # Right eye |
| 120 | + right_eye = patches.Ellipse( |
| 121 | + (x_center + eye_x_offset, eye_y), eye_size * 1.6, eye_size, facecolor="white", edgecolor="black", linewidth=1.5 |
| 122 | + ) |
| 123 | + ax.add_patch(right_eye) |
| 124 | + right_pupil = patches.Circle((x_center + eye_x_offset, eye_y), eye_size * 0.35, facecolor="black") |
| 125 | + ax.add_patch(right_pupil) |
| 126 | + |
| 127 | + # Eyebrows - angle based on power (higher power = more intense) |
| 128 | + brow_y = eye_y + eye_size * 1.4 |
| 129 | + brow_length = eye_size * 1.3 |
| 130 | + brow_angle = (features[1] - 0.5) * 0.015 # Angle variation |
| 131 | + |
| 132 | + ax.plot( |
| 133 | + [x_center - eye_x_offset - brow_length / 2, x_center - eye_x_offset + brow_length / 2], |
| 134 | + [brow_y + brow_angle, brow_y - brow_angle], |
| 135 | + color="black", |
| 136 | + linewidth=2.5, |
| 137 | + solid_capstyle="round", |
| 138 | + ) |
| 139 | + ax.plot( |
| 140 | + [x_center + eye_x_offset - brow_length / 2, x_center + eye_x_offset + brow_length / 2], |
| 141 | + [brow_y - brow_angle, brow_y + brow_angle], |
| 142 | + color="black", |
| 143 | + linewidth=2.5, |
| 144 | + solid_capstyle="round", |
| 145 | + ) |
| 146 | + |
| 147 | + # Nose - simple vertical line with base |
| 148 | + nose_height = 0.015 + features[1] * 0.01 |
| 149 | + nose_y_top = y_center + nose_height * 0.3 |
| 150 | + nose_y_bottom = y_center - nose_height * 0.7 |
| 151 | + ax.plot([x_center, x_center], [nose_y_top, nose_y_bottom], color="black", linewidth=2) |
| 152 | + # Nose base |
| 153 | + ax.plot([x_center - 0.005, x_center + 0.005], [nose_y_bottom, nose_y_bottom], color="black", linewidth=2) |
| 154 | + |
| 155 | + # Mouth - curved based on comfort |
| 156 | + mouth_y = y_center - face_height * 0.28 |
| 157 | + mouth_width_val = 0.02 + features[0] * 0.015 |
| 158 | + |
| 159 | + mouth_x = np.linspace(-mouth_width_val / 2, mouth_width_val / 2, 30) |
| 160 | + mouth_y_curve = mouth_y + mouth_curve * (1 - (2 * mouth_x / mouth_width_val) ** 2) |
| 161 | + ax.plot(x_center + mouth_x, mouth_y_curve, color="black", linewidth=3, solid_capstyle="round") |
| 162 | + |
| 163 | + # Label below face (positioned further down to avoid overlap) |
| 164 | + ax.text( |
| 165 | + x_center, y_center - face_height * 0.65 - 0.02, label, ha="center", va="top", fontsize=13, fontweight="bold" |
| 166 | + ) |
| 167 | + |
| 168 | +# Styling |
| 169 | +ax.set_xlim(0, 1) |
| 170 | +ax.set_ylim(0, 1) |
| 171 | +ax.set_aspect("equal") |
| 172 | +ax.axis("off") |
| 173 | + |
| 174 | +# Title |
| 175 | +ax.set_title("Car Ratings · chernoff-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20) |
| 176 | + |
| 177 | +# Feature mapping legend (bottom left, moved down to avoid overlap) |
| 178 | +legend_text = ( |
| 179 | + "Feature Mapping:\nFace Width = Fuel Efficiency\nFace Height = Power\nEye Size = Reliability\nMouth Curve = Comfort" |
| 180 | +) |
| 181 | +ax.text( |
| 182 | + 0.02, |
| 183 | + 0.01, |
| 184 | + legend_text, |
| 185 | + transform=ax.transAxes, |
| 186 | + fontsize=11, |
| 187 | + verticalalignment="bottom", |
| 188 | + fontfamily="monospace", |
| 189 | + bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.95, "edgecolor": "gray"}, |
| 190 | +) |
| 191 | + |
| 192 | +# Category legend (upper right) |
| 193 | +for i, category in enumerate(categories): |
| 194 | + ax.scatter([], [], c=colors[i], s=250, label=category, alpha=0.75, edgecolors="black") |
| 195 | +ax.legend(loc="upper right", fontsize=14, title="Category", title_fontsize=16, framealpha=0.95, edgecolor="gray") |
| 196 | + |
| 197 | +plt.tight_layout() |
| 198 | +plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white") |
0 commit comments